[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n# 配置修改自：https://gitee.com/596392912/mica/blob/master/.editorconfig\n\nroot = true\n\n[*]\nindent_style = tab\ncharset = utf-8\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{json,yml}]\nindent_style = space\nindent_size = 2\n\n[*.md]\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitee/ISSUE_TEMPLATE.zh-CN.md",
    "content": "### 版本情况\n\nJDK版本：    openjdk_8_201\nhutool版本： 5.X.X（请确保最新尝试是否还有问题）\n\n### 问题描述（包括截图）\n\n1. 复现代码\n\n```java\nConsole.log(\"报错了\");\n```\n\n2. 堆栈信息\n\n3. 测试涉及到的文件（注意脱密）\n\n比如报错的Excel文件，有问题的图片等。"
  },
  {
    "path": ".gitee/PULL_REQUEST_TEMPLATE.zh-CN.md",
    "content": "#### 说明\n\n1. 请确认你提交的PR是到'v5-dev'分支，否则我会手动修改代码并关闭PR。\n2. 请确认没有更改代码风格（如tab缩进）\n3. 新特性添加请确认注释完备，如有必要，请在src/test/java下添加Junit测试用例\n\n### 修改描述(包括说明bug修复或者添加新特性)\n\n1. [bug修复] balabala……\n2. [新特性]  balabala……\n\n### 提交前自测\n> 请在提交前自测确保代码没有问题，提交新代码应包含：测试用例、通过(mvn javadoc:javadoc)检验详细注释。 \n\n1. 本地如有多个JDK版本，可以设置临时JDk版本,如：`export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_331.jdk/Contents/Home`，具体替换为本地jdk目录\n2. 确保本地测试使用JDK8最新版本，`echo $JAVA_HOME`、`mvn -v`、`java -version`均正确。\n3. 执行打包生成文档，使用`mvn clean package -Dmaven.test.skip=true -U`，并确认通过，会自动执行打包、生成文档\n4. 如需要单独执行文档生成，执行：`mvn javadoc:javadoc `，并确认通过\n5. 如需要单独执行测试用例，执行：`mvn clean test`，并确认通过\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [looly]\ncustom: ['https://gitee.com/chinabugotech/hutool']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "### 版本情况\n\nJDK版本：    openjdk_8_201\nhutool版本： 5.X.X（请确保最新尝试是否还有问题）\n\n### 问题描述（包括截图）\n\n1. 复现代码\n\n```java\nConsole.log(\"报错了\");\n```\n\n2. 堆栈信息\n\n3. 测试涉及到的文件（注意脱密）\n\n比如报错的Excel文件，有问题的图片等。"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "#### 说明\n\n1. 请确认你提交的PR是到'v5-dev'分支，否则我会手动修改代码并关闭PR。\n2. 请确认没有更改代码风格（如tab缩进）\n3. 新特性添加请确认注释完备，如有必要，请在src/test/java下添加Junit测试用例\n\n### 修改描述(包括说明bug修复或者添加新特性)\n\n1. [bug修复] balabala……\n2. [新特性]  balabala……\n\n### 提交前自测\n> 请在提交前自测确保代码没有问题，提交新代码应包含：测试用例、通过(mvn javadoc:javadoc)检验详细注释。\n\n1. 本地如有多个JDK版本，可以设置临时JDk版本,如：`export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_331.jdk/Contents/Home`，具体替换为本地jdk目录\n2. 确保本地测试使用JDK8最新版本，`echo $JAVA_HOME`、`mvn -v`、`java -version`均正确。\n3. 执行打包生成文档，使用`mvn clean package -Dmaven.test.skip=true -U`，并确认通过，会自动执行打包、生成文档\n4. 如需要单独执行文档生成，执行：`mvn javadoc:javadoc `，并确认通过\n5. 如需要单独执行测试用例，执行：`mvn clean test`，并确认通过"
  },
  {
    "path": ".github/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ v5-dev ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ v5-dev ]\n  schedule:\n    - cron: '45 6 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'java', 'javascript' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v1\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n          # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n      # If this step fails, then you should remove it and run the build manually (see below)\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v1\n\n      # ℹ️ Command-line programs to run using the OS shell.\n      # 📚 https://git.io/JvXDl\n\n      # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n      #    and modify them (or add more) to build your code if your project\n      #    uses a compiled language\n\n      #- run: |\n      #   make bootstrap\n      #   make release\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".gitignore",
    "content": "# Eclipse\n.project\n.classpath\n.settings/\n\n# Maven\ntarget/\ndependency-reduced-pom.xml\npom.xml.versionsBackup\n.factorypath\n\n# Gradle\n.gradle/\nbuild/\n\n#IDEA\n# idea ignore\n.idea/\n*.ipr\n*.iml\n*.iws\n\n# temp ignore\n*.log\n*.cache\n*.diff\n*.patch\n*.tmp\n.jython_cache/\n\n# system ignore\n.DS_Store\nThumbs.db\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: java\n\nsudo: false # faster builds\n\ninstall: true\n\njdk:\n  - openjdk8\n\nnotifications:\n  email: false\n\ncache:\n  directories:\n    - '$HOME/.m2'\n\nscript: \n  - export TZ=Asia/Shanghai\n  - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V\n  - mvn cobertura:cobertura -Dcobertura.report.format=xml -Dmaven.javadoc.skip.true\n\nafter_success:\n  - bash <(curl -s https://codecov.io/bash)\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "\n# 🚀Changelog\n-------------------------------------------------------------------------------------------------------------\n# 5.8.44(2026-03-11)\n### 🐣新特性\n* 【core   】      `NumberUtil.parseNumber`增加支持科学计数法（pr#4211@Github）\n* 【captcha】      `AbstractCaptcha`增加`setStroke`方法支持线条粗细（issue#IDJQ15@Gitee）\n* 【core   】      `BooleanUtil`新增 exactlyOneTrue 方法用于互斥条件校验（issue#IDJQ15@Gitee）\n* 【core   】      `DateUtil.normalize`方法中正则预编译提升效率（pr#4221@Gitee）\n* 【core   】      `AppendableWriter`增加checkNotClosed（issue#IDMZ5K@Gitee）\n* 【core   】      `FastDateParser`改进在JDK25下三字母时区警告（issue#4100@Github）\n* 【core   】      `ReflectUtil`增加二级缓存（pr#1433@Gitee）\n\n### 🐞Bug修复\n* 【json       】  修复`JSONUtil.wrap`忽略错误问题（issue#4210@Github）\n* 【http       】  修复`HttpUtil.normalizeParams `在极端输入下抛 StringIndexOutOfBoundsException（pr#4216@Github）\n* 【extra      】  修复`MailAccount.setAuth`参数与field不一致问题（issue#4217@Github）\n* 【core       】  修复`TransMap.computeIfAbsent`mappingFunction处理不一致问题（issue#IDM6UR@Gitee）\n* 【core       】  修复`MultiResource`游标歧义问题（issue#IDNAOY@Gitee）\n* 【core       】  修复`BufferUtil`copy歧义问题（issue#IDN097@Gitee）\n* 【core       】  修复`JschSessionPool`回收导致的session未关闭问题（issue#4223@Github）\n* 【core       】  修复`XmlUtil.xmlToBean`option参数无效问题（issue#4226@Github）\n* 【core       】  修复`ReUtil.replaceAll`空指针问题（issue#IDPHVW@Gitee）\n* 【core       】  修复`EnumUtil`枚举类静态初始化时触发 Recursive update 异常（pr#1432@Gitee）\n* 【core       】  修复`AbstractCache`高并发下 get+supplier 双重检查锁逻辑缺陷（pr#1432@Gitee）\n* 【core       】  修复`Quarter`fromMonth计算错误问题（issue#IF15CP@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.43(2026-01-04)\n\n### 🐣新特性\n* 【core   】      `ColorUtil`颜色名称DARKGOLD、LIGHTGOLD新增蛇形命名匹配（pr#1400@Github）\n* 【core   】      添加`BeanPath`方法中对“*”的处理逻辑（pr#1412@Gitee）\n* 【core   】      `StrUtil`添加`reverseByCodePoint`方法（pr#4187@Github）\n* 【core   】      `JdkUtil`添加`IS_AT_LEAST_JDK25`\n* 【core   】      `HexUtil.toHex`添加对float和double的支持，并提供反向方法（pr#4193@Github）\n* 【core   】      增强`BitSetBloomFilter`构造器的参数有效性校验逻辑（pr#4194@Github）\n* 【core   】      `HexUtil.isHexNumber`增加空检查（pr#1420@Gitee）\n* 【core   】      `BooleanUtil`增加中文、英文及符号布尔值的识别能力（pr#1429@Gitee）\n* 【core   】      `DataBetween`类的构造函数中做 defensive copy（pr#1426@Gitee）\n* 【core   】      `RadixUtil.decode`添加校验（pr#1422@Gitee）\n* 【core   】      `CharUtil.toCloseByNumber`增加下边界检查（pr#1421@Gitee）\n* 【ai     】      增加`gemini`支持（pr#4205@Github）\n\n### 🐞Bug修复\n* 【core       】  修复`Calculator.conversion`方法计算包含科学计数法表达式的值时逻辑有误，结果不符合预期（pr#4172@Github）\n* 【core       】  修复`NumberUtil.getBinaryStr`方法计算Double等丢失小数问题（pr#1411@Gitee）\n* 【core       】  修复`MathUtil.multiple`方法在大整数乘法运算中整数溢出风险（pr#4174@Github）\n* 【core       】  修复`CharSequenceUtil.move`方法在局部循环位移中输出不符合预期问题（issue#IDD181@Gitee）\n* 【bloomFilter】  修复`AbstractFilter`的`init`方法在`maxValue`小于`machineNum`时导致数组越界异常（pr#4189@Github）\n* 【ai         】  修复`Models`枚举命名大小写混用问题（pr#4185@Github）\n* 【core       】  修复`ThreadUtil.getMainThread`在JDK25中返回null的问题（pr#1416@Gitee）\n* 【core       】  修复`NumberUtil.parseNumber`使用中文逗号导致识别问题（issue#4197@Github）\n* 【crypto     】  修复`SecureUtil.hmacSha1`和`hmacSha256`生成随机密钥时存在逻辑问题（pr#4199@Github）\n* 【core       】  修复`StopWatch.stop`时间回拨时计算结果为负的问题（pr#1417@Gitee）\n* 【core       】  修复`SplitIter.reset`后无法重新迭代的问题（pr#1418@Gitee）\n* 【core       】  修复`StrMatcher`连续变量解析导致的歧义问题（pr#1419@Gitee）\n* 【ai         】  修复`BaseAIService`发送请求方法中try/catch块捕获的应该是Exception而不是自定义的AIException（pr#1430@Gitee）\n* 【core       】  修复`StrUtil.truncateByByteLength`在限制长度小于...时报错问题（issue#IDFTJS@Gitee）\n* 【core       】  修复`Calculator.conversion`方法计算包含%连接一元运算符的计算表达式的结果时逻辑缺陷（pr#4191@Github）\n* 【db         】  修复`SqlUtil.PATTERN_IN_CLAUSE`逻辑缺陷导致in语句参数不正确的问题（pr#4203@Github）\n* 【json       】  修复`ObjectMapper`过滤器对Bean复制无效的问题（pr#1431@Gitee）\n* 【core       】  修复`DateUnit`毫秒转换问题（issue#4209@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.42(2025-11-28)\n\n### 🐣新特性\n* 【core   】      `ListUtil`增加`zip`方法（pr#4052@Github）\n* 【http   】      增加`JakartaSoapClient`（issue#4103@Github）\n* 【ai     】      增加代理支持（pr#4107@Github）\n* 【core   】      `CharSequenceUtil`增加`builder`方法重载（pr#4107@Github）\n* 【core   】      `Combination`和`Arrangement `重构避免数组频繁拷贝，并避免溢出（pr#4144@Github）\n* 【core   】      优化`EscapeUtil`，兼容不规范的转义（pr#4150@Github）\n* 【core   】      优化`ObjectUtil.contains`String改为CharSequence（pr#4154@Github）\n* 【poi    】      `Word07Writer`增加addText重载，支持字体颜色（pr#1388@Gitee）\n* 【core   】      增强`HexUtil`自动去除`0x`和`#`前缀（pr#4163@Github）\n\n### 🐞Bug修复\n* 【jwt    】      修复verify方法在定义alg为`none`时验证失效问题（issue#4105@Github）\n* 【extra  】      修复`JschSessionPool.remove`逻辑错误问题（issue#ID4XZ7@gitee）\n* 【db     】      修复`Dialect.psForCount`未传入Wrapper导致大小写问题（issue#ID39G9@Gitee）\n* 【core   】      修复`PasswdStrength.check`indexOf逻辑问题（pr#4114@Github）\n* 【http   】      修复`HttpConnection.reflectSetMethod`反射在JDK9+权限问题（issue#4109@Github）\n* 【http   】      修复`JsonUtil.toJsonStr`对Boolean和Number返回错误问题（issue#4109@Github）\n* 【core   】      修复`FileUtil.listFileNames`相对路径index混乱问题（issue#4121@Github）\n* 【core   】      修复`NumberWithFormat`没有实现Comparable接口导致的JSON排序报错问题（issue#ID61QR@Gitee）\n* 【core   】      修复`ImgUtil.write`没有释放BufferedImage可能导致内存泄露（issue#ID6VNJ@Gitee）\n* 【core   】      修复`VersionUtil.matchEl`如果输入的版本范围表达式右边界为空时，会抛出数组越界访问错误的问题（pr#4130@Github）\n* 【core   】      修复`Validator.isBetween`在高精度Number类型下存在精度丢失问题（pr#4136@Github）\n* 【core   】      修复`FileNameUtil.extName`在特殊后缀判断逻辑过于宽松导致误判问题（pr#4142@Github）\n* 【core   】      修复`TypeUtil.getClass`无法识别`GenericArrayType`问题（pr#4138@Github）\n* 【core   】      修复`CreditCodeUtil.randomCreditCode`部分字母未使用问题（pr#4149@Github）\n* 【core   】      修复`CacheableAnnotationAttribute`可能并发问题（pr#4149@Github）\n* 【core   】      修复`URLUtil.url`未断开连接问题（pr#4149@Github）\n* 【core   】      修复`Bimap.put`重复put问题（pr#4150@Github）\n* 【core   】      修复`StrUtil.str(ByteBuffer, Charset)` 方法修改入参 `ByteBuffer` 的 `position`，导致入参变化 （pr#4153@Github）\n* 【core   】      修复`ReflectUtil.newInstanceIfPossible`传入Object逻辑错误（pr#4160@Github）\n* 【core   】      修复`DateModifier`处理AM和PM的ceiling和round问题（pr#4161@Github）\n* 【poi    】      修复`Word07Writer`run.setColor()的颜色十六进制转换逻辑（pr#4164@Github）\n* 【core   】      修复`Arrangement.iterate(int m)`方法的排列迭代器实现逻辑问题（pr#4166@Github）\n* 【core   】      修复`HexUtil.format`在处理长度小于2的字符串会抛异常，在处理长度为奇数的字符串时最后一个字符会被忽略的问题（pr#4168@Github）\n* 【core   】      修复`SplitIter.computeNext`递归调用可能导致栈溢出风险（pr#4168@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.41(2025-10-12)\n\n### 🐣新特性\n* 【core   】      增加`WeakKeyValueConcurrentMap`及其关联类，同时废弃`WeakConcurrentMap`并替换（issue#4039@Github）\n* 【core   】      `MapUtil`增加`removeByValue`和`removeIf`方法\n* 【core   】      `ObjectUtil`增加`apply`方法\n* 【core   】      `ReferenceUtil`增加`get`方法\n* 【db     】      `Condition`增加构造方法支持BETWEEN（issue#4041@Github）\n* 【core   】      `IoUtil.writeObjects`判空避免空指针（issue#4049@Github）\n* 【extra  】      `OsInfo`增加`isWindows11`方法（pr#4054@Github）\n* 【extra  】      `RedisDS`增加`getPool`和`getSetting`方法（issue#ICVWDI@Gitee）\n* 【core   】      `NumberUtil.pow`增加重载，支持指数自定义保留位数（pr#4052@Github）\n* 【core   】      `NumberUtil.isPrimes`优化判断（pr#4058@Github）\n* 【extra  】      `Mail.buildContent`改进，正文部分总在最前（issue#4072@Github）\n* 【core   】      `DataSizeUtil`改进，兼容`GiB`等单位名称（issue#ICXXVF@Github）\n* 【ai     】      `Message`增加setter和构造方法（issue#ICXTP2@Gitee）\n* 【extra  】      `PinyinUtil`增加判空（pr#4081@Github）\n* 【core   】      `LocalDateTimeUtil.parseDate`注释修正（pr#4085@Github）\n* 【core   】      `StrUtil`增加null检查处理（pr#4086@Github）\n* 【json   】      增加Record支持（pr#4096@Github）\n* 【crypto 】      增加`SpecUtil`，`KeyUtil`增加`generateRSAPrivateKey`重载，（issue#ID1EIK@Gitee）\n* 【core   】      `RandomUtil`增加`randomStringLower`方法\n\n### 🐞Bug修复\n* 【core   】      修复`ReflectUtil`中因class和Method关联导致的缓存无法回收问题（issue#4039@Github）\n* 【db     】      修复`Condition`的`Condition(\"discount_end_time\", \"!=\", (String) null)`方法生成SQL时，生成SQL不符合预期要求的错误（pr#4042@Github）\n* 【core   】      修复`IoUtil`的`closeIfPosible`拼写错误，新建一个`closeIfPossible`方法，原方法标记deprecated（issue#4047@Github）\n* 【http   】      修复`HttpRequest.sendRedirectIfPossible`未对308做判断问题。（issue#4053@Github）\n* 【cron   】      修复`CronPatternUtil.nextDateAfter`当日为L时计算错误问题。（issue#4056@Github）\n* 【db     】      修复`NamedSql.replaceVar`关键字处理问题（issue#4062@Github）\n* 【db     】      修复`DialectRunner.count`方法中，去除包含多字段order by子句的SQL语句时错误问题（issue#4066@Github）\n* 【extra  】      修复`JschSessionPool`并发问题（pr#4079@Github）\n* 【extra  】      修复`Sftp`递归删除目录时使用相对路径可能导致死循环的问题（pr#1380@Gitee）\n* 【db     】      修复`SqlUtil.removeOuterOrderBy`处理没有order by的语句导致异常问题（pr#4089@Github）\n* 【extra  】      修复`Sftp.upload`目标路径为null时空指针问题（issue#ID14WX@Gitee）\n* 【ai     】      修复`AIConfigBuilder`中方法名拼写错误（pr#1382@Gitee）\n* 【core   】      修复`StrBuilder`charAt越界判断错误（pr#4094@Github）\n* 【dfa    】      修复`WordTree.addWord`末尾为特殊字符导致的无法匹配问题（pr#4092@Github）\n* 【core   】      修复`ServiceLoaderUtil.loadFirstAvailable`在JDK24+后未捕获异常导致的报错问题（pr#4098@Github）\n* 【cron   】      修复`CronTimer`在任务非常多时，追赶系统时间导致遗漏任务的问题（issue#IB49EF@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.40(2025-08-26)\n\n### 🐣新特性\n* 【captcha】      `MathGenerator`四则运算方式支持不生成负数结果（pr#1363@Gitee）\n* 【core   】      增加`MapValueProvider`和`RecordConverter`并支持Record转换（issue#3985@Github）\n* 【core   】      `CalendarUtil`增加`isSameYear`和`calendar`方法（issue#3995@Github）\n* 【core   】      `DateUtil`增加`yyyy-MM-dd'T'HH:mmXXX`格式支持（pr#1367@Gitee）\n* 【core   】      `MapUtil`增加flatten方法（pr#1368@Gitee）\n* 【extra  】      `getClientIP`优先获取传入的请求头信息（pr#1373@Gitee）\n* 【db     】      增加`Gbase8s`驱动支持（issue#ICSFAM@Gitee）\n* 【db     】      增加TDSQL PostgreSQL版本、TDSQL-H LibraDB、Snowflake、Teradata 的驱动支持（pr#4024@Github）\n* 【core   】      `EnumUtil`增加缓存支持（pr#1376@Gitee）\n\n### 🐞Bug修复\n* 【extra  】      `Sftp``reconnectIfTimeout`方法改为捕获所有异常（issue#3989@Github）\n* 【core   】      修复`ChineseDate `闰年闰月节日获取问题（issue#ICL1BT@Gitee）\n* 【core   】      修复`TreeBuilder`append重复向idTreeMap中put问题（pr#3992@Github）\n* 【extra  】      修复`QLExpressEngine`allowClassSet无效问题（issue#3994@Github）\n* 【core   】      修复`StrBuilder`insert插入计算错误问题（issue#ICTSRZ@Gitee）\n* 【cron   】      修复`CronPatternUtil.nextDateAfter`计算下一个匹配表达式的日期时，计算错误问题（issue#4006@Github）\n* 【cache  】      `ReentrantCache`修改get逻辑key锁改为全局锁，保证安全（issue#4022@Github）\n* 【core   】      修复`NumberWordFormatter`formatSimple输出错误问题（pr#4034@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.39(2025-06-20)\n\n### 🐣新特性\n* 【ai     】      增加SSE流式返回函数参数callback，增加超时时间配置，豆包、grok新增文生图接口，豆包生成视频支持使用model,新增HutoolAI平台\n* 【core   】      DesensitizedUtil新增护照号码脱敏功能（pr#1347@Gitee）\n* 【core   】      优化XXXToMapCopier的部分性能（pr#1345@Gitee）\n* 【http   】      `HttpConfig`增加参数`setIgnoreContentLength`可选忽略读取响应contentLength头（issue#ICB1B8@Gitee）\n* 【core   】      `Assert`新增断言给定集合为空的方法以及单元测试用例（pr#3952@Github）\n* 【db     】      Db添加FetchSize的全局设置（pr#3978@Github）\n* 【core   】      增加可召回批处理线程池执行器`RecyclableBatchThreadPoolExecutor`（pr#1343@Gitee）\n* \n### 🐞Bug修复\n* 【core   】      修复`NumberUtil`isNumber方法以L结尾没有小数点判断问题（issue#3938@Github）\n* 【core   】      修复`CharsequenceUtil`toLowerCase方法拼写错误（issue#3941@Github）\n* 【core   】      修复`UUID`equals的问题，改为final类（issue#3948@Github）\n* 【core   】      修复`Money`中金额分配的问题bug（issue#IC9Y35@Gitee）\n* 【poi    】      修复`ExcelPicUtil`中可能的空指针异常\n* 【core   】      修复`LunarFestival`中重复节日问题（issue#ICC8X3@Gitee）\n* 【core   】      修复`ThreadUtil`中中断异常处理丢失中断信息的问题，解决ConcurrencyTester资源未释放的问题（pr#1358@Gitee）\n* 【core   】      修复`TEL_400_800`正则规则太窄问题（issue#3967@Github）\n* 【core   】      修复`ClassUti`isNormalClass判断未排除String问题（issue#3965@Github）\n* 【core   】      修复`ZipUtil`中zlib和unZlib调用后资源未释放问题（issue#3976@Github）\n* 【core   】      修复`Money`类的setAmount方法没有获取当前币种的小数位数而是使用的默认小数位和在遇到非2小数位的币种(如日元使用 0 位)会导致金额设置错误问题（pr#3970@Github）\n* 【cache  】      修复`AbstractCache`putWithoutLock方法可能导致的外部资源泄露问题（pr#3958@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.38(2025-05-13)\n\n### 🐣新特性\n* 【core   】      `PathUtil#del`增加null检查（pr#1331@Gitee）\n* 【db     】      增加SAP HANA识别及方言（pr#3914@Github）\n* 【crypto 】      增加`Argon2`类，实现Argon2算法（issue#3890@Github）\n* 【core   】      `CharSequenceUtil`增加toLoweCase和toUpperCase方法（issue#IC0H2B@Gitee）\n* 【core   】      增加分段锁实现`SegmentLock`（pr#1330@Gitee）\n* 【core   】      重载subtractToList方法，提供isLinked选项（pr#3923@Github）\n* 【extra  】      `TemplateConfig`增加`setUseCache`方法（issue#IC3JRY@Gitee）\n* 【extra  】      `AbstractFtp`增加`rename`方法（issue#IC3PMI@Gitee）\n* 【core   】      优化`PropDesc`缓存注解判断，提升性能（pr#1335@Gitee）\n* 【core   】      添加`RecordUtil`支持record类（issue#3931@Github）\n* 【core   】      `Dict`的customKey方法访问权限修改为protected（pr#1340@Gitee）\n* 【ai     】      增加hutool-ai模块，对AI大模型的封装实现（pr#3937@Github）\n\n### 🐞Bug修复\n* 【setting】      修复`Setting`autoLoad可能的加载为空的问题（issue#3919@Github）\n* 【db     】      修复某些数据库的getParameterMetaData会返回NULL，导致空指针的问题。（pr#3936@Github）\n* 【extra  】      修正`SshjSftp`在SftpSubsystem服务时报错问题（pr#1338@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.37(2025-03-31)\n\n### 🐣新特性\n* 【json   】      ObjectMapper删除重复trim（pr#3859@Github）\n* 【core   】      `FileWriter`增加方法，可选是否追加换行符（issue#3858@Github）\n* 【core   】      `IdcardUtil`验证10位身份证兼容中英文括号（issue#IBP6T1@Gitee）\n* 【extra  】      `PinyinUtil`增加重载可选是否返回声调（pr#3875@Github）\n* 【extra   】      `PinyinEngine`增加重载可选是否返回声调（pr#3883@Github）\n* 【core   】      增加`VersionUtil`版本比较工具（pr#3876@Github）\n* 【db     】      增加GoldenDB识别（pr#3886@Github）\n* 【http   】      改进`UrlQuery`对无参URL增加判断识别（issue#IBRVE4@Gitee）\n* 【core   】      改进`PropDesc`中去除Transient引用避免NoClassDefFoundError（issue#3901@Github）\n* 【core   】      `StrUtil.isBlank`增加`\\u200c`判断（issue#3903@Github）\n* 【core   】      优化`CombinationAnnotationElement`注解数组性能（pr#1323@Gitee）\n* 【core   】      完善季度相关 API（pr#1324@Gitee）\n\n### 🐞Bug修复\n* 【setting】      修复`SettingLoader`load未抛出异常导致配置文件无法正常遍历的问题（pr#3868@Github）\n* 【cache  】      修复`ReentrantCache#getOrRemoveExpired`方法丢失onRemove触发问题（pr#1315@Gitee）\n* 【json   】      修复`JsonUtil.toBean`泛型数组类型丢失问题（pr#3876@Github）\n* 【http   】      修复`HttpUtil.normalizeParams`规则问题（issue#IBQIYQ@Gitee）\n* 【http   】      修复`NumberChineseFormatter.format`中自定义单位在0时错误问题（issue#3888@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.36(2025-02-18)\n\n### 🐣新特性\n* 【crypto 】      增加BCUtil.decodeECPrivateKey方法（issue#3829@Github）\n* 【core   】      增加HtmlUtil.cleanEmptyTag方法（pr#3838@Github）\n* 【db     】      GlobalDbSetting优化默认配置读取规则，优先读取文件而非jar中的文件（issue#900@Github）\n* 【dfa    】      删除StopChar类中存在重复字符（pr#3841@Github）\n* 【http   】      支持鸿蒙设备 UA 解析（pr#1301@Gitee）\n\n### 🐞Bug修复\n* 【aop    】      修复ProxyUtil可能的空指针问题（issue#IBF20Z@Gitee）\n* 【core   】      修复XmlUtil转义调用方法错误问题，修复XmlEscape未转义单引号问题（pr#3837@Github）\n* 【core   】      修复FileUtil.isAbsolutePath没有判断smb路径问题（pr#1299@Gitee）\n* 【core   】      修复AbstractFilter没有检查参数长度问题（issue#3854@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.35(2024-12-25)\n\n### 🐣新特性\n* 【poi    】      优化ExcelWriter中使用比较器writer的方法，只对第一条数据进行排序（pr#3807@Github）\n* 【extra  】      优化Ftp.download，返回false抛出异常（issue#3805@Github）\n* 【core   】      优化MAC地址正则（issue#IB95X4@Gitee）\n* 【json   】      JSON的getByPath方法新增更为通用的指定出参类型重载（pr#3814@Github）\n* 【core   】      DateUtil.parseUTC方法标记废弃，改名为parseISO8601（issue#IBB6I5@Gitee）\n* 【core   】      添加EnumUtil#getBy(Class, Func1, Object)方法（pr#1283@Gitee）\n* 【db     】      添加Entity.addCondition方法（issue#IBCDL2@Gitee）\n* 【poi    】      添加StopReadException，定义sax读取时用户可手动终止（issue#3820@Github）\n\n### 🐞Bug修复\n* 【crypto 】      修复JWTSignerUtil.createSigner中algorithmId未转换问题（issue#3806@Github）\n* 【core   】      修复DateUtil.rangeContains未重置问题（issue#IB8OFS@Gitee）\n* 【cache  】      修复StampedCache类get方法并发问题（issue#IBCIQG@Gitee）\n* 【cache  】      修复FIFOCache类使用StampedCache导致并发读的并发问题（issue#IBCIQG@Gitee）\n* 【cache  】      废弃StampedCache，可能造成Map循环调用导致死锁（issue#IBDGBZ@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.34(2024-11-25)\n\n### 🐣新特性\n* 【http   】      增加Windows微信浏览器识别（issue#IB3SJF@Gitee）\n* 【core   】      ZipUtil.unzip增加编码容错（issue#I3UZ28@Gitee）\n* 【core   】      Calculator兼容`x`字符作为乘号（issue#3787@Github）\n* 【poi    】      Excel07SaxReader中，对于小数类型，增加精度判断（issue#IB0EJ9@Gitee）\n* 【extra  】      SpringUtil增加getBean重载（issue#3779@Github）\n* 【core   】      DataSizeUtil 新增format方法（issue#IB6UUX@Gitee）\n\n### 🐞Bug修复\n* 【core   】      修复DateUtil.rangeToList中step小于等于0时无限循环问题（issue#3783@Github）\n* 【cron   】      修复cron模块依赖log模块问题\n* 【extra  】      修复MailUtil发送html格式邮件无法正常展示图片问题(pr#1279@Gitee)\n* 【core   】      【可能的向下兼容问题】修复双引号转义符转义错误问题，修改规则后，对非闭合双引号字段的策略变更，如\"aa，则被识别为aa(issue#IB5UQ8@Gitee)\n* 【extra  】      修复Sftp中传入Session重连时逻辑错误问题(issue#IB69U8@Gitee)\n* 【json   】      修复JSONUtil.toBean()中将JSON数组字符串转Map对象返回错误问题(issue#3795@Github)\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.33(2024-11-05)\n\n### 🐣新特性\n* 【core   】      SyncFinisher增加setExecutorService方法（issue#IANKQ1@Gitee）\n* 【http   】      HttpConfig增加`setUseDefaultContentTypeIfNull`方法（issue#3719@Github）\n* 【core   】      用ArrayList重新实现权重随机类：WeightListRandom（pr#3720@Github）\n* 【crypto 】      SM2解密时，兼容GmSSL非压缩省略的04头的密文（issue#IAP1QJ@Gitee）\n* 【core   】      兼容NumberUtil.add方法传入整型自动类型转换为浮点类型的精度丢失问题（pr#3721@Github）\n* 【core   】      ModifierUtil明确注释，并增加hasAllModifiers方法（issue#IAQ2U0@Gitee）\n* 【http   】      HttpRequest增加setFixedContentLength方法（issue#3462@Github）\n* 【db     】      AbstractDb增加getDs方法（issue#IARKZL@Gitee）\n* 【db     】      QrCodeUtil添加二维码logo支持配置圆角（pr#3747@Github）\n* 【core   】      TreeUtil.buildSingle指定rootId节点存在时，作为根节点（issue#IAUSHR@Gitee）\n* 【core   】      EscapeUtil.escapeHtml4增加空处理（issue#IAZMYU@Gitee）\n* 【core   】      PropDesc.isTransientForGet使用className，避免Android下类找不到问题（issue#IB0JP5@Gitee）\n* 【core   】      优化NumberUtil.count（pr#3772@Github）\n* 【crypto 】      SM2.signHex改名为signHexFromHex，原名标记废弃，避免歧义（issue#IB0NVY@Gitee）\n* 【all    】      优化所调用的ObjectUtil#defaultIfNull避免重复创建（pr#1274@Gitee）\n* 【core   】      NetUtil.bigIntegerToIPv6增加长度修正（issue#IB27HV@Gitee）\n\n### 🐞Bug修复\n* 【json   】      修复JSONConfig.setDateFormat设置后toBean无效问题（issue#3713@Github）\n* 【core   】      修复RegexPool.CHINESE_NAME范围太大的问题（issue#IAOGDR@Gitee）\n* 【http   】      修复重定向没有按照RFC7231规范跳转的问题，修改为除了307外重定向使用GET方式（issue#3722@Github）\n* 【core   】      修复ArrayUtil.lastIndexOfSub死循环问题（issue#IAQ16E@Gitee）\n* 【core   】      修复ImgUtil.write写出临时文件未清理问题（issue#IAPZG7@Gitee）\n* 【json   】      修复ignoreNullValue在JSONArray中无效问题（issue#3759@Github）\n\n-------------------------------------------------------------------------------------------------------------\n**# 5.8.32(2024-08-30)\n\n### 🐣新特性\n* 【core   】      FileUtil.getTotalLines()支持CR换行符（issue#IAMZYR@Gitee）\n* 【json   】      GlobalSerializeMapping增加null检查（issue#IANH1Y@Gitee）\n\n### 🐞Bug修复\n* 【http   】      修复getFileNameFromDisposition不符合规范问题（issue#IAKBPD@Gitee）\n* 【crypto 】      修复SymmetricCrypto.setParams和setRandom没有加锁问题（issue#IAJIY3@Gitee）\n* 【crypto 】      修复ZipUtil压缩成流的方法检查文件时报错问题（issue#3697@Github）\n* 【core   】      修复CopyOptions.setFieldValueEditor后生成null值setIgnoreNullValue无效问题（issue#3702@Github）\n* 【json   】      修复JSONConfig.setDateFormat设置后setWriteLongAsString失效问题（issue#IALQ0N@Gitee）\n* 【core   】      修复Tree.cloneTree的Parent节点引用错误问题（issue#IANJTC@Gitee）\n\n-------------------------------------------------------------------------------------------------------------**\n# 5.8.31(2024-08-12)\n\n### 🐣新特性\n* 【core   】      TreeUtil增加build方法，可以构建Bean的树结构（pr#3692@Github）\n\n### 🐞Bug修复\n* 【extra  】      修复JakartaMailUtil引用javax的问题\n* 【core   】      修复GraphicsUtil.drawString方法签名变化导致的问题（issue#3694@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.30(2024-08-09)\n\n### 🐣新特性\n* 【core   】      Converter转换规则变更，空对象、空值转为Bean时，创建默认对象，而非null（issue#3649@Github）\n* 【core   】      UrlQuery增加remove方法\n* 【extra  】      增加JakartaMailUtil，支持新包名的mail\n* 【core   】      CharSequenceUtil增加removeAllPrefix和removeAllSuffix方法（pr#3655@Github）\n* 【core   】      CharSequenceUtil增加stripAll方法（pr#3659@Github）\n* 【crypto 】      支持\"RSA/ECB/OAEPWithSHA-1AndMGF1Padding\"的RSA加解密（pr#3675@Github）\n* 【core   】      Opt增加ifFail（pr#1239@Gitee）\n* 【poi    】      增加GlobalPoiConfig（issue#IAEHJH@Gitee）\n* 【core   】      优化IndexedComparator性能（pr#1240@Gitee）\n* 【http   】      改进ContentType.get忽略空格（pr#3664@Github）\n* 【http   】      CompressUtil.createExtractor支持tgz自动识别（pr#3674@Github）\n* 【poi    】      ExcelWriter.autoSizeColumn增加可选widthRatio参数，可配置中文字符宽度倍数（pr#3689@Github）\n* 【mail   】      MailAccount增加自定义参数支持（issue#3687@Github）\n* 【mail   】      增加文字颜色与背景颜色色差设置（pr#1252@gitee）\n* 【mail   】      XmlUtil增加xmlToBean重载，支持CopyOptions参数（issue#IAISBB@gitee）\n* 【core   】      增加默认色差方法（pr#1257@gitee）\n* 【all    】      单元测试由Junit4变更为Junit5\n\n### 🐞Bug修复\n* 【core   】      修复因RFC3986理解有误导致的UrlPath处理冒号转义问题（issue#IAAE88@Gitee）\n* 【core   】      修复FileUtil.cleanEmpty无法正确清空递归空目录问题（pr#1233@Gitee）\n* 【core   】      修复BeanUtil.copyProperties中mapToMap时key被转为String问题（issue#3645@Github）\n* 【core   】      修复FileUtil.file末尾换行符导致路径判断错误的问题（issue#IAB65V@Gitee）\n* 【core   】      修复FileTypeUtil.getType空指针问题（issue#IAD5JM@Gitee）\n* 【core   】      修复IdcardUtil.isValidHKCard校验问题（issue#IAFOLI@Gitee）\n* 【core   】      修复Convert.digitToChinese(0)输出金额无`元整问题`（issue#3662@Github）\n* 【core   】      修复CsvParser中对正文中双引号处理逻辑问题（pr#1244@Gitee）\n* 【core   】      修复ZipUtil.zip压缩到本目录时可能造成的死循环问题（issue#IAGYDG@Gitee）\n* 【cache  】      修复AbstractCache.get中锁不一致导致的并发问题（issue#3686@Github）\n* 【cron   】      修复CronPatternUtil.nextDateAfter栈溢出问题（issue#3685@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.29(2024-07-03)\n\n### 🐣新特性\n* 【core   】      DateUtil增加offsetYear方法\n* 【core   】      ListUtil增加move方法（issue#3603@Github）\n* 【core   】      CollUtil.subtract增加空判定（issue#3605@Github）\n* 【core   】      优化DateUtil.format(Date date, String format)接口效率（pr#1226@Gitee）\n* 【csv    】      CsvWriter.writeBeans增加重载，可选是否写出表头（issue#IA57W2@Gitee）\n* 【core   】      BetweenFormatter支持自定义设置单位（pr#1228@Gitee）\n* 【cache  】      Cache.put变更策略，对于替换的键值对，不清理队列（issue#3618@Github）\n* 【core   】      添加 Windows 资源管理器风格字符串比较器（pr#3620@Github）\n* 【core   】      Week.of支持中文名称（issue#3637@Github）\n* 【core   】      ThreadUtil.newExecutor等方法变更方法签名，返回值变更为ThreadPoolExecutor（pr#1230@Gitee）\n\n### 🐞Bug修复\n* 【core   】      修复AnnotationUtil可能的空指针错误\n* 【core   】      修复BeanUtil.isBean判断Dict错误问题（issue#I9VTZG@Gitee）\n* 【core   】      修复VersionComparator传入空字符串报错问题（pr#3614@Github）\n* 【core   】      修复CaseInsensitiveLinkedMap顺序错误问题（issue#IA4K4F@Gitee）\n* 【core   】      修复DateUtil.offset空指针问题（issue#3617@Github）\n* 【core   】      修复PathMover.moveContent问题（issue#IA5Q8D@Gitee）\n* 【db     】      修复PooledConnection可能的数据库驱动未找到问题（issue#IA6EUQ@Gitee）\n* 【http   】      修复Mac下的微信浏览器被识别为移动端问题（issue#IA74K2@Gitee）\n* 【core   】      修复Tailer指定初始读取行数的计算错误问题（issue#IA77ML@Gitee）\n* 【http   】      修复getFileNameFromDisposition获取头错误问题（issue#3632@Github）\n* 【core   】      修复\\n#出现在双引号中解析错误问题（issue#IA8WE0@Gitee）\n* 【core   】      修复FastDatePrinter处理YY错误问题（issue#3641@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.28(2024-05-29)\n\n### 🐣新特性\n* 【core   】      修正XmlUtil的omitXmlDeclaration描述注释（issue#I9CPC7@Gitee）\n* 【core   】      StrUtil增加toStringOrEmpty方法（issue#I9CPC7@Gitee）\n* 【extra  】      设置jsch登录认证方式，跳过Kerberos身份验证（pr#3530@Github）\n* 【extra  】      增加设置验证码大小和针对alias注释（pr#3533@Github）\n* 【json   】      JSONConfig增加setWriteLongAsString可选是否将Long写出为String类型（issue#3541@Github）\n* 【cache  】      CacheUtil.newTimedCache增加有schedulePruneDelay参数的重载方法（issue#I9HO25@Gitee）\n* 【core   】      NumberChineseFormatter提供阿拉伯转中文支持多位小数的方法（pr#3552@Github）\n* 【captcha】      Captcha.setBackground为null时背景透明（issue#3558@Github）\n* 【captcha】      HttpDownloader.downloadBytes增加超时参数重载（issue#3556@Github）\n* 【http   】      增加ExceptionFilter和DefaultExceptionFilter支持异常处理（issue#3568@Github）\n* 【poi    】      增加ExcelWriter.addIgnoredErrors，支持忽略警告小标\n* 【core   】      PropertyComparator增加compareSelf构造重载（issue#3569@Github）\n* 【db     】      增加OceanBase的driver推断（pr#1217@Gitee）\n* 【http   】      HttpRequest#get不再尝试File路径（issue#I9O6DA@Gitee）\n* 【core   】      增加IdConstants，提高Snowflake初始化性能（issue#3581@Github）\n* 【core   】      优化 CharSequenceUtil工具类 startWithAny()、startWithAnyIgnoreCase() 参数命名错误问题（pr#1219@Gitee）\n* 【core   】      ListUtil.setOrPadding增加重载，可选限制index大小（issue#3586@Github）\n* 【http   】      getFileNameFromDisposition更加规范，从多个头的值中获取，且`filename*`优先级更高（pr#3590@Gitee）\n* 【core   】      CsvWriter增加重载writeBeans方法，支持可选bean字段（pr#1222@Gitee）\n* 【core   】      LocalDateTimeUtil增加beginOfDay和endOfDay重载（issue#3594@Github）\n* 【core   】      NumberUtil.pow支持负数（issue#3598@Github）\n\n### 🐞Bug修复\n* 【http   】      修复HttpUtil.urlWithFormUrlEncoded方法重复编码问题（issue#3536@Github）\n* 【core   】      修复FileMagicNumber.getMagicNumber空指针问题（issue#I9FE8B@Gitee）\n* 【extra  】      修复CompressUtil工具多出\\问题（issue#I71K5V@Gitee）\n* 【db     】      解决oracle情况下setObject(inputStream)报错问题，java.sql.SQLException: 无效的列类型问题（pr#1207@Gitee）\n* 【core   】      解决CalendarUtil.isSameDay时区不同导致结果错误问题（pr#3548@Github）\n* 【core   】      修复RandomUtil.randomStringWithoutStr方法问题（pr#1209@Gitee）\n* 【http   】      修复HttpRequest.header相同key被覆盖问题（issue#I9I61C@Gitee）\n* 【core   】      修复TemporalAccessorConverter自定义格式转换问题（issue#I9HQQE@Gitee）\n* 【cron   】      修复CronPattern.nextMatchAfter匹配初始值问题（issue#I9FQUA@Gitee）\n* 【core   】      修复FileUtil.copyFile没有创建父目录导致的问题（issue#3557@Github）\n* 【http   】      修复HttpDownloader全局超时无效问题（issue#3556@Github）\n* 【core   】      修复ZipReader.checkZipBomb遇到空目录报错问题（issue#I9K494@Gitee）\n* 【db     】      修复Oracle下特殊表名导致meta信息获取不到问题（issue#I9BANE@Gitee）\n* 【db     】      修复FuncComparator.thenComparing不生效问题（issue#3569@Github）\n* 【core   】      修复EnumUtil空指针问题（issue#I9NSZ4@Gitee）\n* 【core   】      修复NumberWordFormatter.format小数问题（issue#3579@Github）\n* 【db     】      修复JndiDSFactory空指针问题\n* 【core   】      修复BiMap.put错误的返回值（pr#1218@Gitee）\n* 【core   】      修复BooleanUtil.andOfWrap针对null错误问题（issue#3587@Github）\n* 【core   】      修复FileUtil#getTotalLines在JDK9+结果错误问题（issue#3591@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.27(2024-03-29)\n\n### 🐣新特性\n* 【extra 】      FreemarkerEngine修改默认版本参数\n* 【db    】      增加达梦数据库方言（pr#1178@Gitee）\n* 【core  】      HexUtil#format方法增加prefix参数（issue#I93PU9@Gitee）\n* 【core  】      StrUtil.replace歧义，修改为replaceByCodePoint（issue#I96LWH@Gitee）\n* 【core  】      FileUtil和PathUtil增加Resource重载（issue#I97FJT@Gitee）\n* 【core  】      优化ThreadUtil.safeSleep，使用System.nanoTime()（issue#I9BMGK@Gitee）\n* 【db    】      新增数据库Wrapper支持反解（pr#1192@Gitee）\n* 【core  】      新增RFC2822日期格式解析支持（issue#I9C2D4@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复PathMover对目标已存在且只读文件报错错误问题（issue#I95CLT@Gitee）\n* 【json  】      修复JSONUtil序列化和反序列化预期的结果不一致问题（pr#3507@Github）\n* 【http  】      修复CVE-2022-22885，HttpGlobalConfig可选关闭信任host（issue#2042@Github）\n* 【core  】      修复DateUtil.betweenYear闰年2月问题（issue#I97U3J@Gitee）\n* 【captcha】     修复Graphics2D的资源没释放问题（issue#I98PYN@Gitee）\n* 【core  】      修复ClassUtil.getTypeArgument() 获取泛型存在null问题（issue#3516@Github）\n* 【core  】      修复图片操作未调用flush导致资源未释放问题（issue#I9C7NA@Gitee）\n* 【cron  】      修复cron中在小月时使用“L”的计算问题（pr#1189@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.26(2024-02-10)\n\n### 🐣新特性\n* 【db    】      RedisDS增加user支持（issue#I8XEQ4@Gitee）\n* 【core  】      MapUtil增加partition方法（pr#1170@Gitee）\n* 【core  】      增加Version类（issue#I8Z3VE@Gitee）\n\n### 🐞Bug修复\n* 【crypto】      修复BouncyCastleProvider导致graalvm应用报错UnsupportedFeatureError（pr#3464@Github）\n* 【http  】      修复UserAgentUtil对QQ浏览器识别问题（issue#I8X5XQ@Gitee）\n* 【core  】      修复BeanToMapCopier获取类型数组越界问题（issue#3468@Github）\n* 【extra 】      修复SshjSftpSession关闭导致的问题（issue#3472@Github）\n* 【http  】      修复HtmlUtil.removeHtmlAttr处理空格问题（issue#I8YV0K@Gitee）\n* 【core  】      修复CollUtil.containsAll在coll2长度大于coll1时逻辑歧义问题（issue#I8Z2Q4@Gitee）\n* 【poi   】      修复当sheetName 不存在时，ExcelUtil.getReader方法不会释放文件问题（issue#I8ZIQC@Gitee）\n* 【crypto】      通过添加系统属性hutool.crypto.decodeHex强制关闭hex识别以解决hex和Base64歧义问题（issue#I90M9D@Gitee）\n* 【core  】      修复VersionComparator违反传递问题（issue#I8Z3VE@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.25(2024-01-11)\n\n### 🐣新特性\n* 【core  】      WatchServer新增通过Path获取WatchKey方法（pr#1145@Gitee）\n* 【core  】      CopyOptions中增加setAutoTransCamelCase方法（issue#3452@Github）\n* 【captcha】     验证码生成器增加构造方法，可自定义随机数字符集（pr#1147@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复StrJoin当append内容后调用length()会出现空指针问题（issue#3444@Github）\n* 【core  】      修复PostgreSQL、H2使用upsert字段大小写问题（issue#I8PB4X@Gitee）\n* 【core  】      修复RandomUtil.randomInt,RandomUtil.randomLong边界问题（pr#3450@Github）\n* 【db    】      修复Druid连接池无法设置部分属性问题（issue#I8STFC@Gitee）\n* 【core  】      修复金额转换为英文时缺少 trillion 单位问题（pr#3454@Github）\n* 【json  】      增加ParseConfig，通过增加maxNestingDepth参数避免StackOverflowError问题，修复CVE-2022-45688漏洞（issue#2748@Github）\n* 【system】      修复UserInfo中用户名加/问题（pr#3458@Github）\n* 【core  】      修复NumberUtil.toBigDecimal方法报StackOverflowError(CVE-2023-51080)（issue#3423@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.24(2023-12-23)\n\n### 🐣新特性\n* 【cache 】      Cache增加get重载，可自定义超时时间（issue#I8G0DL@Gitee）\n* 【cache 】      JWT#sign增加重载，可选是否增加默认的typ参数（issue#3386@Github）\n* 【db    】      增加识别OpenGauss的驱动类（issue#I8K6C0@Gitee）\n* 【core  】      修复CharSequenceUtil注释和引用，避免循环引用\n* 【extra 】      SpringUtil增加getProperty重载（pr#1122@Gitee）\n* 【core  】      FileTypeUtil增加null判断（issue#3419@Github）\n* 【core  】      DateUtil.parse支持毫秒时间戳（issue#I8NMP7@Gitee）\n* 【extra 】      优化TokenizerEngine使用IK分词器支持并发（pr#3427@Github）\n* 【core  】      Opt.ofEmptyAble支持更多类型（issue#I8OOSY@Gitee）\n* 【http  】      HTMLFilter保留p标签（issue#3433@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复LocalDateTime#parseDate未判断空问题（issue#I8FN7F@Gitee）\n* 【http  】      修复RootAction send404 抛异常问题（pr#1107@Gitee）\n* 【extra 】      修复Archiver 最后一个 Entry 为空文件夹时未关闭 Entry问题（pr#1123@Gitee）\n* 【core  】      修复ImgUtil.convert png转jpg在jdk9+中失败问题（issue#I8L8UA@Gitee）\n* 【cache 】      修复StampedCache的get方法非原子问题（issue#I8MEIX@Gitee）\n* 【core  】      修复StrSplitter.splitByRegex使用空参数导致的OOM问题（issue#3421@Github）\n* 【db    】      修复嵌套SQL中order by子句错误截断问题（issue#I89RXV@Gitee）\n* 【http  】      修复graalvm编译后，未读取Content-Length可能导致的读取时间过长问题（issue#I6Q30X@Gitee）\n* 【core  】      修复JavaSourceCompiler.addSource目录处理错误问题（issue#3425@Github）\n* 【core  】      修复时间戳转Bean时异常问题（issue#I8NMP7@Gitee）\n* 【core  】      修复PostgreSQL使用upsert字段大小写问题（issue#I8PB4X@Gitee）\n* 【extra 】      修复TinyPinyinEngine可能的空指针问题（issue#3437@Github）\n* 【core  】      修复graalvm原生打包使用http工具被转为file协议问题（issue#I8PY3Y@Gitee）\n* 【poi   】      修复cloneSheet参数错误导致非XSSFWorkbook错误命名问题（issue#I8QIBB@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.23(2023-11-12)\n\n### 🐣新特性\n* 【json  】      改进TemporalAccessorSerializer支持dayOfMonth和month枚举名（issue#I82AM8@Gitee）\n* 【core  】      新增ProxySocketFactory\n* 【http  】      UserAgent增加百度浏览器识别（issue#I847JY@Gitee）\n* 【core  】      ReflectUtil.getFieldsValue增加Filter重载（pr#1090@Gitee）\n* 【core  】      Snowflake增加方法：根据传入时间戳，计算ID起终点（pr#1096@Gitee）\n* 【core  】      PathUtil增加loopFiles重载，可选是否追踪软链（issue#3353@Github）\n\n### 🐞Bug修复\n* 【cron  】      修复Cron表达式range解析错误问题（issue#I82CSH@Gitee）\n* 【core  】      修复VersionComparator在极端数据排序时候违反了自反性问题（issue#I81N3H@Gitee）\n* 【json  】      修复JSONStrFormatter:format函数对于转义符号处理逻辑错误问题（issue#I84V6I@Gitee）\n* 【core  】      修复特定情况下BiMap覆盖Value后，仍能通过旧Value查询到Key问题（issue#I88R5M@Gitee）\n* 【core  】      修复aop的afterException无法生效问题（issue#3329@Github）\n* 【core  】      修复TypeUtil.getClass方法强转报错问题（pr#1092@Github）\n* 【core  】      修复DataSize.parse(size)不支持空格问题（issue#I88Z4Z@Gitee）\n* 【http  】      修复SimpleServer在添加的HttpFilter中有获取请求参数时报错问题（issue#3343@Github）\n* 【http  】      修复options请求无响应体问题\n* 【core  】      ImgUtil的sliceByRowsAndCols背景无法透明问题（issue#3347@Github）\n* 【core  】      修复ClassUtil#scanJar未正确关闭文件问题（issue#3361@Github）\n* 【db    】      修复Column.getDigit返回值错误问题（issue#3370@Github）\n* 【core  】      修复合成注解在并发环境无法保证正确缓存属性值的问题（pr#1097@Gitee）\n* 【core  】      修复CollectorUtil.reduceListMap与collectors.groupby一起使用时出现与预期不符问题（pr#1102@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.22(2023-09-13)\n\n### 🐣新特性\n* 【core  】      NumberUtil.nullToZero增加重载（issue#I7PPD2@Gitee）\n* 【core  】      DesensitizedUtil增加清空策略（issue#I7PUJ2@Gitee）\n* 【all   】      修改异常包装策略：运行时异常不包装，只包装非运行时异常（issue#I7RJZT@Gitee）\n* 【core  】      增加IJSONTypeConverter，避免反射调用（pr#1051@Gitee）\n* 【http  】      优化HttpUtil.urlWithForm方法（pr#1052@Gitee）\n* 【http  】      优化HttpUtil.urlWithForm方法（pr#1052@Gitee）\n* 【cron  】      优化PatternParser支持年的步进（issue#I7SMP7@Gitee）\n* 【core  】      TreeUtil增加getParentsId方法（issue#I7TDCF@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复NumberUtil.toBigDecimal转换科学计数法问题（issue#3241@Github）\n* 【core  】      修复PathUtil.moveContent当target不存在时会报错问题（issue#3238@Github）\n* 【db    】      修复SqlUtil.formatSql 格式化的sql换行异常（pr#3247@Github）\n* 【core  】      修复DateUtil.parse 给定一个时间解析错误问题（issue#I7QI6R@Gitee）\n* 【core  】      去除默认的ACCEPT_LANGUAGE（issue#3258@Github）\n* 【core  】      修复FieldsComparator比较结果不正确问题（issue#3259@Github）\n* 【core  】      修复Db.findAll全局忽略大小写无效问题（issue#I7T30Y@Gitee）\n* 【core  】      修复Ipv4Util.getEndIpLong 取反符号导致数据越界（issue#I7U1OQ@Gitee）\n* 【http  】      修复302重定向时，Location中的问号被转义问题（issue#3265@Github）\n* 【core  】      修复CombinationAnnotationElement判断循环问题（pr#3267@Github）\n* 【core  】      修复StrUtil#containsAny NPE问题（pr#1063@Gitee）\n* 【all   】      修复SONArray的add()方法抛出OutOfMemory异常问题（issue#3286@Github）\n* 【core  】      修复fillColumns空指针问题（issue#3284@Github）\n* 【core  】      修复Convert不能转换Optional和Opt问题（issue#I7WJHH@Gitee）\n* 【core  】      修复DateUtil.age年龄计算问题（issue#I7XMYW@Gitee）\n* 【core  】      修复JSONUtil.parse()溢出问题（issue#3289@Github）\n* 【core  】      修复Tailer stop NPE问题（pr#1067@Gitee）\n* 【json  】      修复toJSONString导致CPU使用率高的问题（issue#3297@Github）\n* 【core  】      修复NumberUtil.parseInt 16进制解析错误的问题（pr#1071@Gitee）\n* 【core  】      修复CopyOptions.setIgnoreCase和setIgnoreProperties冲突问题（issue#I80FP4@Gitee）\n* 【core  】      修复LocalDateTimeUtil.of 某些特殊TemporalAccessor无法返回正确结果的问题（issue#3301@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.21(2023-07-29)\n\n### 🐣新特性\n* 【core  】      list 为空时，CollUtil.max等返回null而非异常（pr#1027@Gitee）\n* 【poi   】      ExcelReader.getWriter逻辑变更，当从非文件读取时，获取sheet，而非空表格。\n* 【core  】      Ipv4Util 新增方法：检测指定 IP 地址是否匹配通配符（pr#3171@Github）\n* 【core  】      DateUtil.parse适配6位毫秒格式（issue#I7H34N@Gitee）\n* 【core  】      RandomUtil增加可选是否包含边界的重载（issue#3182@Github）\n* 【core  】      StrUtil增加truncateByByteLength方法（pr#3176@Github）\n* 【core  】      身份证工具类isValidCard18、isValidCard15入参null直接返回null（pr#1034@Gitee）\n* 【http  】      使用multiparty方式支持body参数（issue#3158@Github）\n* 【core  】      ZipReader增加setMaxSizeDiff方法，自定义或关闭ZipBomb（issue#3018@Github）\n* 【db    】      Query.of(entity)构建时传入fields（issue#I7M5JU@Gitee）\n* 【db    】      clickhouse驱动名称变更为com.clickhouse.jdbc.ClickHouseDriver（issue#3224@Github）\n* 【core  】      UrlResource增加size方法（issue#3226@Github）\n\n### 🐞Bug修复\n* 【core  】      修复MapUtil工具使用filter方法构造传入参数结果问题（issue#3162@Github）\n* 【core  】      修复序列化和反序列化Class问题（issue#I7FQ29@Gitee）\n* 【setting】     修复utf8-bom的setting文件读取问题（issue#I7G34E@Gitee）\n* 【core  】      修复PathUtil.getMimeType可能造成的异常（issue#3179@Github）\n* 【core  】      修复Pair序列化转换无效问题（issue#I7GPGX@Github）\n* 【core  】      修复TypeUtil.getTypeArgument对实现接口获取不全面问题（issue#I7CRIW@Gitee）\n* 【core  】      修复BeanUtil.isCommonFieldsEqual判空导致的问题\n* 【extra 】      修复CompressUtil.createArchiver 将文件压缩为tgz时文件名规则无效问题（issue#I7LLL7@Gitee）\n* 【core  】      修复脱敏银行卡号长度bug（pr#3210@Github）\n* 【jwt   】      修复JWTSignerUtil中ES256签名不符合规范问题（issue#3205@Github）\n* 【core  】      修复UserInfo获取country问题（issue#I7MCKW@Gitee）\n* 【extra 】      修复MVEL加载错误问题（issue#3214@Github）\n* 【json  】      修复JSONBeanParser在遇到List时没有被正确递归问题（issue#I7M2GZ@Gitee）\n* 【core  】      修复VersionComparator对1.0.3及1.0.2a比较有误的问题（pr#1043@Gitee）\n* 【core  】      修复IOS系统下，chrome 浏览器的解析规则有误（pr#1044@Gitee）\n* 【extra 】      修复多线程下Sftp中Channel关闭的问题（issue#I7OHIB@Gitee）\n* 【extra 】      修复CVE-2023-24163漏洞（issue#I6AJWJ@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.20(2023-06-16)\n\n### 🐣新特性\n* 【core  】      UrlQuery增加setStrict方法，区分是否严格模式（issue#I78PB1@Gitee）\n* 【poi   】      添加系列方法writeCol，以支持按列输出（pr#1003@Gitee）\n* 【core  】      CollUtil新增anyMatch和allMatch方法（pr#1008@Gitee）\n* 【core  】      CsvWriter如果开启了append=true，默认自动开启endingLineBreak=true（pr#1010@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复TreeUtil.getParentsName()获取到的路径集合中存在值为null的路径名称问题（issue#I795IN@Gitee）\n* 【core  】      修复umberUtil.parseNumber对+解析问题（issue#I79VS7@Gitee）\n* 【core  】      修复IdcardUtil.getGenderByIdCard存在潜在的异常（pr#1007@Gitee）\n* 【core  】      修复Table#contains空指针问题（issue#3135@Gitee）\n* 【core  】      修复FileUtil.checkSlip方法缺陷（issue#3140@Github）\n* 【extra 】      修复Sftp中exists方法父目录不存在时报错（issue#I7CSQ9@Gitee）\n* 【extra 】      修复xml转json再转bean失败问题（issue#3139@Github）\n* 【poi   】      修复RowUtil传入参数错误问题（issue#3139@Github）\n* 【core  】      修复XmlUtil.xmlToBean空节点转换失败问题（issue#3136@Github）\n* 【core  】      修复CVE-2023-3276漏洞，XmlUtil.readBySax问题（issue#I7DX8W@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.19(2023-05-27)\n\n### 🐣新特性\n* 【db    】      优化HttpRequest.toString()内容打印（issue#3072@Github）\n* 【poi   】      优化Sax方式读取时空白行返回0，修改为返回-1（issue#I6WYF6@Gitee）\n* 【db    】      优化count查询兼容informix（issue#I713XQ@Gitee）\n* 【core  】      去除Opt头部的GPL协议头（pr#995@Gitee）\n* 【core  】      邮箱校验添加对中文的支持（pr#997@Gitee）\n* 【core  】      FileUtil.getMimeType增加webp识别（pr#997@Gitee）\n* 【core  】      SyncFinisher增加setExceptionHandler方法（issue#I716SX@Gitee）\n* 【core  】      FileTypeUtil.getType增加文件判断（pr#3112@Github）\n* 【core  】      增加CsvWriteConfig.setEndingLineBreak配置项（issue#I75K5G@Gitee）\n* 【core  】      增加Tailer追踪文件时文件被删除的处理情况（pr#3115@Github）\n* 【core  】      DelegatedExecutorService构造方法设置成public（issue#I77LUE@Gitee）\n* 【core  】      切面代理工具中的cglib支持多参数构造生成（issue#I74EX7@Gitee）\n* 【poi   】      添加writeCellValue的重载，以支持isHeader（pr#1002@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复URLUtil.decode无法解码UTF-16问题（issue#3063@Github）\n* 【db    】      修复insertOrUpdate更新中条件字段没有移除问题（issue#I6W91Z@Gitee）\n* 【core  】      修复VIN（车架号）正则问题（pr#3078@Github）\n* 【core  】      修复HtmlUtil的removeHtmlAttr方法匹配问题（issue#I6YNTF@Gitee）\n* 【core  】      修复JSONUtil.toBean目标存在Map字段无序问题（issue#I6YN2A@Gitee）\n* 【http  】      修复HttpDownloader.downloadFile 方法缺少static问题（issue#I6Z8VU@Gitee）\n* 【core  】      修复NumberUtil mul 传入null的string入参报错问题（issue#I70JB3@Gitee）\n* 【core  】      修复ZipReader.get调用reset异常问题（issue#3099@Github）\n* 【core  】      修复FileUtil.createTempFile可能导致的漏洞（issue#3103@Github）\n* 【cron  】      修复SystemTimer无法结束进程问题（issue#3090@Github）\n* 【core  】      修复BeanUtil.copyToList复制Long等类型错误问题（issue#3091@Github）\n* 【poi   】      修复MapRowHandler结果Map无序问题（issue#I71SE8@Github）\n* 【db    】      修复SqlExecutor.execute执行ORACLE insert into select报ORA-00933问题（issue#I778U7@Gitee）\n* 【db    】      修复AbstractDb#page分页查询异常问题（issue#I73770@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.18 (2023-04-27)\n\n### 🐣新特性\n* 【extra 】      JschUtil新增一个重载方法以支持私钥以byte数组形式载入（pr#3057@Github）\n* 【crypto】      优化MD5性能（issue#I6ZIQH@Gitee）\n\n### 🐞Bug修复\n* 【core  】      修复CollUtil.reverseNew针对非可变列表异常（issue#3056@Github）\n* 【all   】      修复junit被关联引入的bug（issue#3062@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.17 (2023-04-12)\n\n### 🐣新特性\n* 【core  】      SerializeUtil.deserialize增加白名单类，避免RCE vulnerability（issue#3021@Github）\n* 【poi   】      ExcelWriter在关闭后不清空currentRow，以便复用（issue#3025@Github）\n* 【core  】      完善HttpStatus，参考相关规范，补全缺失的状态码（pr#968@Gitee）\n* 【core  】      NumberUtil增加（pr#968@Gitee）\n* 【core  】      Number128增加hash和equals方法（pr#968@Gitee）\n* 【core  】      NamingCase.toCamelCase新增重载，可选是否转换其他字符为小写（issue#3031@ithub）\n* 【core  】      新增JdkUtil\n* 【core  】      DateUtil.getZodiac增加越界检查（issue#3036@Github）\n* 【core  】      CsvReader修改策略，添加可选是否关闭Reader重载，默认不关闭Reader（issue#I6UAX1@Gitee）\n* 【core  】      isNotEmpty修改规则，避开IDEA错误提示（pr#974@Gitee）\n\n### 🐞Bug修复\n* 【core  】      CollUtil.split优化切割列表参数判断，避免OOM（pr#3026@Github）\n* 【core  】      修复FileUtil.move传入相同目录或子目录丢失源目录的问题（pr#3032@Github）\n* 【core  】      修复SafeConcurrentHashMap.computeIfAbsent可能存在的结果为null的情况（issue#I6RVMY@Gitee）\n* 【json  】      修复Pair反序列化报错问题（issue#I6SZYB@Gitee）\n* 【core  】      修复使用AnnotationUtil.getAnnotationAlias获取注解时可能会出现空指针的问题（pr#975@Gitee）\n* 【json  】      修复没有属性的对象转json字符串抛异常问题（issue#3051@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.16 (2023-03-26)\n\n### 🐣新特性\n* 【core  】      改进Calculator.conversion，兼容乘法符号省略写法（issue#2964@Github）\n* 【core  】      改进XmlUtil.xmlToBean，支持xml转bean时父节点忽略大小写\n* 【core  】      优化ArrayUtil的空判断（pr#2969@Github）\n* 【extra 】      优化SpringUtil在非Spring环境下的异常（issue#2835@Github）\n* 【core  】      StrUtil增加commonPrefix和commonSuffix方法（pr#3007@Github）\n* 【core  】      NumberUtil增加重载parseXXX方法, 解析失败返回默认值（pr#3007@Github）\n* 【core  】      FileUtil增加readLines重载，支持filter（pr#3006@Github）\n* 【json  】      当用户选择ignoreError时，错误对象转JSON也忽略\n\n### 🐞Bug修复\n* 【crypto】      修复NoSuchMethodError未捕获问题（issue#2966@Github）\n* 【poi   】      修复SXSSFWorkbook调用setComment时错位的问题（issue#I6MBS5@Gitee）\n* 【core  】      修复BeanUtil.hasGetter没有跳过getClass方法的问题（issue#I6MBS5@Gitee）\n* 【core  】      修复FileMagicNumber长度判断问题导致的越界异常（issue#I6MACI@Gitee）\n* 【core  】      修复DateUtil针对ISO8601时间格式部分场景下的解析存在问题（issue#2981@Github）\n* 【core  】      修复JSONUtil.toBean可能的空指针问题（issue#2987@Github）\n* 【core  】      修复CalendarUtil.isSameMonth没有判断公元前导致不一致的问题（issue#3011@Github）\n* 【core  】      修复WatchUtil createModify maxDepth传递后没有使用问题（issue#3005@Github）\n* 【core  】      修复NullComparator反转无效问题（pr#964@Gitee）\n* 【setting】     修复props.toBean 数组字段未赋值问题（issue#3008@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.15 (2023-03-09)\n\n### 🐣新特性\n* 【http  】      新增followRedirectsCookie配置，支持开启自动重定向携带cookie（pr#2961@Github）\n\n### 🐞Bug修复\n* 【all   】      修复Automatic-Module-Name错误问题（issue#2952@Github）\n* 【core  】      修复NumberWithFormat导致转换Long异常问题（issue#I6L2LO@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.8.14 (2023-03-05)\n\n### 🐣新特性\n* 【core  】      增加PathMover（issue#I666HB@Github）\n\n### 🐞Bug修复\n* 【core  】      修复FileUtil.moveContent会删除源目录的问题（issue#I666HB@Github）\n* 【http  】      修复HttpBase.body导致的空指针问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.13 (2023-03-03)\n\n### 🐣新特性\n* 【core  】      PhoneUtil.isTel400800支持400-XXX-XXXX格式（issue#2929@Github）\n* 【core  】      build(pom): 添加 Automatic-Module-Name属性（pr#2926@Github）\n* 【core  】      根据JDK-8080225修改了部分新建文件输入流和文件输出流的创建方式（pr#2930@Github）\n* 【http  】      HttpRequest#body增加支持Resource重载（issue#2901@Github）\n* 【core  】      JavaSourceCompiler#compile增加自定义options重载（issue#I6IVZK@Gitee）\n\n### 🐞Bug修复\n* 【db    】      修复识别JDBC驱动时重复问题（pr#940@Gitee）\n* 【core  】      修复法定年龄计算的BUG（pr#935@Gitee）\n* 【core  】      修复FileUtil.rename报NoSuchFileException问题（pr#2894@Github）\n* 【core  】      修复StrUtil.split切分长度为0时的bug（pr#944@Gitee）\n* 【core  】      修复ReUtil.delAll方法当 content 仅为空格时的问题（issue#I6GIMT@Gitee）\n* 【core  】      修复ReUtil.delAll方法当 content 仅为空格时的问题（issue#I6GIMT@Gitee）\n* 【core  】      修复文件内容跟随在调用stop后，文件依旧被占用问题（issue#I6GFD2@Gitee）\n* 【core  】      修复ReflectUtil.invokeRaw方法中参数类型转换动作未生效的问题（pr#2912@Github）\n* 【core  】      修复isXXX转换时的匹配问题（issue#I6H0XF@Gitee）\n* 【core  】      修复MutableObj.equals空指针问题\n* 【core  】      修复JavaSourceFileObject在编译错误时抛出IOException异常而非CompilerException问题（pr#2942@Github）\n* 【jwt   】      修复JWT自定义时间格式后的时间戳转换问题（issue#I6IS5B@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.12 (2023-02-09)\n\n### 🐣新特性\n* 【http  】      HttpGlobalConfig.allowPatch()调用时忽略错误（issue#2832@Github）\n* 【core  】      重构根据file magic number判断文件类型（pr#2834@Github）\n* 【core  】      增加WGS84 坐标与墨卡托投影互转（pr#2811@Github）\n* 【extra 】      ServletUtil遵循rfc 3986优化（issue#I6ALAO@Gitee）\n* 【http  】      HttpUtil.decodeParams增加isFormUrlEncoded重载（pr#918@Gitee）\n* 【db    】      AbstractDb添加返回类型为PageResult的page重载方法（pr#916@Gitee）\n* 【core  】      DesensitizedUtil增加对IPv4和IPv6支持（issue#I6ABCS@Gitee）\n* 【core  】      针对CollUtil.subtract coll1 为只读集合的补偿（pr#2865@Github）\n* 【core  】      DateUtil.date方法统一修改规则，传入null返回null（pr#2877@Github）\n* 【core  】      DateUtil.parseUTC统一规范，舍弃3位毫秒数后的数字（pr#2889@Github）\n\n### 🐞Bug修复\n* 【core  】      修复HexUtil.isHexNumber()对\"-\"的判断问题（issue#2857@Github）\n* 【core  】      修复FileTypeUtil判断wav后缀的录音文件类型不能匹配问题（pr#2834@Github）\n* 【core  】      修复FileUtil的rename在newName与原文件夹名称一样时，文件夹会被删除问题（issue#2845@Github）\n* 【core  】      修复IoUtil.readBytes使用SocketInputStream读取不完整问题（issue#I6AT49@Gitee）\n* 【core  】      修复ClassScanner自定义classload无效问题（issue#I68TV2@Gitee）\n* 【core  】      【重要】删除XmlUtil.readObjectFromXml方法，避免漏洞（issue#2855@Github）\n* 【core  】      修复Ipv4Util.list()方法的bug（pr#929@Gitee）\n* 【poi   】      修复“sax方式读取excel2003版本，会调用两次doAfterAllAnalysed方法”问题。（pr#919@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.11 (2022-12-27)\n\n### 🐣新特性\n* 【core  】      CharUtil.isBlankChar增加\\u180e（pr#2738@Github）\n* 【core  】      SyncFinisher线程同步结束器添加立即结束方法（pr#879@Gitee）\n* 【core  】      HtmlUtil中escape方法，增加不断开空格（nbsp）转译，防止xss攻击（pr#2755@Github）\n* 【extra 】      修正sftp.cd方法 方法注释和实际效果不符（issue#2758@Github）\n* 【core  】      修改PhoneUtil容易歧义的注释（issue#I63GWK@Gitee）\n* 【crypto】      KeyUtil中的读取KeyStore文件的方法增加全局Provider（issue#I6796G@Gitee）\n* 【extra 】      CompressUtil 新增 stripComponents 参数（pr#904@Gitee）\n* 【extra 】      ServletUtil和JakartaServletUtil新增获取所有响应头的方法（pr#2828@Github）\n* 【core  】      BooleanUtil增加toString重载（pr#2816@Github）\n\n### 🐞Bug修复\n* 【json  】      修复普通byte数组转JSONArray时的异常（pr#875@Gitee）\n* 【core  】      修复ArrayUtil.insert()不支持原始类型数组的问题（pr#874@Gitee）\n* 【core  】      修复HexUtil.isHexNumber()判断逻辑超出long的精度问题（issue#I62H7K@Gitee）\n* 【core  】      修复BiMap中未重写computeIfAbsent和putIfAbsent导致双向查找出问题（issue#I62X8O@Gitee）\n* 【json  】      修复JSON解析栈溢出部分问题（issue#2746@Github）\n* 【json  】      修复getMultistageReverseProxyIp未去除空格问题（issue#I64P9J@Gitee）\n* 【db    】      修复NamedSql中in没有判断大小写问题（issue#2792@Github）\n* 【core  】      修复ZIP bomb漏洞（issue#2797@Github）\n* 【core  】      修复JSONXMLSerializer将Json转为XML时，遇到嵌套需要递归情况时会丢失contentKeys问题（pr#903@Gitee）\n* 【db    】      修复使用mariadb通过jdbcurl创建SimpleDataSource报NullPointException（pr#900@Gitee）\n* 【core  】      修复UrlBuilder中参数中包括\"://\"判断错误问题（pr#898@Gitee）\n* 【core  】      修复IndexedComparator导致的数据错乱问题（ExcelWriter使用部分别名导致字段丢失）（issue#I66Z6B@Gitee）\n* 【crypto】      修复sm2构造方法NullPointerException（pr#2820@Github）\n* 【core  】      修复ConverterRegistry中无效加载导致的问题（issue#2812@Github）\n* 【core  】      修复CoordinateUtil坐标转换参数错误（pr#895@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.10 (2022-11-17)\n\n### 🐣新特性\n* 【http  】      HttpResponse增加getFileNameFromDisposition方法（pr#2676@Github）\n* 【core  】      FileUtil.copy，当来源为文件时，返回文件而非目录（issue#I5YCVL@Gitee）\n* 【db    】      DialectFactory增加identifyDriver重载（issue#I5YWI6@Gitee）\n* 【core  】      去除ClassloaderUtil的Cache（issue#I5YWI6@Gitee）\n* 【core  】      ClassScanner 增加忽略加载错误类的扫描方法（pr#855@Gitee）\n* 【core  】      DateUtil和LocalDateTimeUtil添加区间退化为点，点与区间，点与点之间关系判断。（pr#2725@Github）\n* 【http  】      UserAgentUtil增加对钉钉PC端的支持（issue#I60UOP@Gitee）\n* 【extra 】      兼容ZipArchiveInputStream多参数情况（issue#2736@Github）\n\n### 🐞Bug修复\n* 【db    】      修复分页时order by截断问题（issue#I5X6FM@Gitee）\n* 【core  】      修复Partition计算size除数为0报错问题（pr#2677@Github）\n* 【core  】      由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此BCD标记为弃用（issue#I5XEC6@Gitee）\n* 【core  】      修复IoUtil.copyByNIO方法写出时没有flush的问题\n* 【core  】      修复TreeBuilder中使用HashMap导致默认乱序问题（issue#I5Z8C5@Gitee）\n* 【core  】      修复StrUtil.subWithLength负数问题（issue#I5YN49@Gitee）\n* 【core  】      修复DefaultTrustManager空指针问题（issue#2716@Github）\n* 【core  】      修复时间轮添加任务线程安全问题（pr#2712@Github）\n* 【core  】      修复 BeanUtil#copyProperties 源对象与目标对象都是 Map 时设置忽略属性无效问题（pr#2698@Github）\n* 【core  】      修复ChineseDate传入农历日期非闰月时获取公历错误问题（issue#I5YB1A@Gitee）\n* 【core  】      修复key为弱引用 value为强引用 会导致key无法被回收 弱引用失效问题（pr#2723@Github）\n* 【core  】      修复BeanUtil.copyProperties 包含EnumSet ，类型转换异常问题（pr#2684@Github）\n* 【extra 】      修复Ftp.uploadFileOrDirectory上传目录错误调用错误问题（issue#I5R2DE@Gitee）\n* 【extra 】      修复字节数组转float 返回类型却是double的bug（pr#867@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.9 (2022-10-22)\n\n### 🐣新特性\n* 【core   】     DateUtil增加isLastDayOfMonth、getLastDayOfMonth方法（pr#824@Gitee）\n* 【core   】     AnnotationUtil类支持Lambda获取某注解属性值（pr#827@Gitee）\n* 【core   】     CharUtil.isBlank添加Hangul Filler字符（issue#I5UGSQ@Gitee）\n* 【poi    】     优化合并单元格读取（issue#I5UJZ1@Gitee）\n* 【extra  】     增加QLExpress支持（issue#2653@Github）\n* 【core   】     UrlBuilder增加getPortWithDefault方法（pr#835@Gitee）\n* 【core   】     FuncKeyMap的子类，传入可被序列化的keyFunc（pr#838@Gitee）\n* 【extra  】     SpringUtil支持SpringBoot3自动配置（pr#839@Gitee）\n* 【core   】     CollectorUtil添加支持对值集合进行映射的分组方法（pr#844@Gitee）\n* 【core  】      FileTypeUtil增加ppt识别（issue#2663@Github）\n\n### 🐞Bug修复\n* 【poi    】     修复ExcelReader读取只有标题行报错问题（issue#I5U1JA@Gitee）\n* 【http   】     修复Http重定向时相对路径导致的问题（issue#I5TPSY@Gitee）\n* 【http   】     修复Http重定全局设置无效问题（pr#2639@Github）\n* 【core   】     修复ReUtil.replaceAll替换变量错误问题（pr#2639@Github）\n* 【core   】     修复FileNameUtil.mainName二级扩展名获取错误问题（issue#2642@Github）\n* 【cache  】     修复LRUCache移除事件监听失效问题（issue#2647@Github）\n* 【core   】     修复MapToMap中ignoreNullValue无效问题（issue#2647@Github）\n* 【core   】     修复ReflectUtil.invokeRaw方法转换失败抛出异常问题（pr#837@Gitee）\n* 【core   】     修复TableMap没有default方法导致的问题（issue#I5WMST@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.8 (2022-09-26)\n\n### 🐣新特性\n* 【core   】     StreamUtil.of方法新增对 Iterator 支持；StreamUtil.of(Iterable) 方法优化（pr#807@Gitee）\n* 【core   】     增加.wgt格式的MimeType（pr#2617@Github）\n* 【core   】     EnumUtil.getBy增加带默认值重载（issue#I5RZU6@Gitee）\n* 【core   】     ModifierUtil和ReflectUtil增加removeFinalModify（pr#810@Gitee）\n* 【core   】     AbsCollValueMap添加removeValue和removeValues方法，用于list value值移除（pr#813@Gitee）\n* 【extra  】     hutool-extra ftp 支持上传文件或目录（pr#821@Gitee）\n* 【core   】     CharsetDetector增加默认识别的长度（issue#2547@Github）\n\n### 🐞Bug修复\n* 【core   】     修复FileNameUtil.cleanInvalid无法去除换行符问题（issue#I5RMZV@Gitee）\n* 【core   】     修复murmur3_32实现错误（pr#2616@Github）\n* 【core   】     修复PunyCode处理域名的问题（pr#2620@Github）\n* 【core   】     修复ObjectUtil.defaultIfNull去掉误加的deprecated（issue#I5SIZT@Gitee）\n* 【core   】     修复ReflectUtil 反射方法中桥接判断问题（issue#2625@Github）\n* 【poi    】     修复ExcelWriter导出List<Map>引起的个数混乱问题（issue#2627@Github）\n* 【poi    】     修复ExcelReader读取时间变成12小时形式问题（issue#I5Q1TW@Gitee）\n* 【db     】     修复DB工具分页查询的时候oracle数据库会把ROWNUM_也带出来问题（issue#2618@Github）\n* 【crypto 】     修复部分环境下使用 Bouncy Castle可能的JCE cannot authenticate the provider BC问题（issue#2631@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.7 (2022-09-15)\n\n### 🐣新特性\n* 【core   】     BooleanUtil的andOfWrap和orOfWrap()忽略null（issue#2599@Github）\n* 【jwt    】     优化JWT自动识别header中的算法，并可自定义header中key的顺序（issue#I5QRUO@Gitee）\n* 【core   】     IdcardUtil增加convert18To15方法（issue#I5QYCP@Gitee）\n* 【core   】     新增AnsiColors(改自Spring Boot)、AnsiColorWrapper，优化QrCodeUtil（pr#778@Gitee）\n* 【core   】     TemplateUtil的实现类增加getRawEngine方法（issues#2530@Github）\n* 【core   】     ImgUtil中颜色相关方法剥离到ColorUtil中\n* 【core   】     增加SafeConcurrentHashMap\n\n### 🐞Bug修复\n* 【core   】     修复ObjectUtil.defaultIfXXX中NPE问题（pr#2603@Github）\n* 【db     】     修复Hive2驱动无法识别问题（issue#2606@Github）\n* 【core   】     修复computeIfAbsent问题（issue#I5PTN3@Gitee）\n* 【extra  】     修复Ftp中路径问题（issue#I5R2DE@Gitee）\n* 【core   】     修复ConcurrentHashMap.computeIfAbsent缺陷导致的问题\n* 【core   】     修复DateUtil.parseUTC时对-的处理问题（issue#2612@Github）\n* 【core   】     修复Convert.chineseMoneyToNumber角分丢失问题（issue#2611@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.6 (2022-09-05)\n\n### ❌不兼容特性\n* 【json   】     由于设计缺陷，导致JSONObject#write方法中Filter中key的泛型不得已变动为Object，以解决无法递归的bug（issue#I5OMSC@Gitee）\n\n### 🐣新特性\n* 【core   】     CollUtil新增addIfAbsent方法（pr#750@Gitee）\n* 【core   】     DateUtil.parseUTC支持只有时分的格式（issue#I5M6DP@Gitee）\n* 【core   】     NumberUtil.parseInt忽略科学计数法（issue#I5M55F@Gitee）\n* 【core   】     IterUtil.getFirst优化（pr#753@Gitee）\n* 【core   】     增加Tree add 类型校验（pr#2542@Github）\n* 【core   】     增加PunyCode处理完整域名（pr#2543@Github）\n* 【core   】     增加替换字符串中第一个指定字符串和最后一个指定字符串方法（pr#2533@Github）\n* 【jwt    】     JWT补充部分算法（pr#2546@Github）\n* 【core   】     NumberUtil.roundStr() 修改为使用toPlainString（pr#775@Gitee）\n* 【extra  】     QrCodeUtil新增SVG格式、Ascii Art字符画格式（pr#763@Gitee）\n* 【jwt    】     JWTUtil的parseToken增加空值异常抛出（issue#I5OCQB@Gitee）\n* 【extra  】     resource.loader等过期参数替换（issue#2571@Github）\n* 【core   】     添加ObjectUtil的别名工具类ObjUtil\n* 【core   】     扩展LocalDateTimeUtil.isIn方法使用场景（pr#2589@Github）\n* 【core   】     MapUtil增加根据entry分组（pr#2591@Github）\n* 【core   】     优化 getProcessorCount 潜在的获取不到的问题（pr#792@Gitee）\n* 【core   】     ImgUtil增加sliceByRowsAndCols重载方法支持自定义图片格式（pr#793@Gitee）\n* \n### 🐞Bug修复\n* 【http   】     修复https下可能的Patch、Get请求失效问题（issue#I3Z3DH@Gitee）\n* 【core   】     修复RandomUtil#randomString 入参length为负数时报错问题（issue#2515@Github）\n* 【core   】     修复SecureUtil传入null的key抛出异常问题（pr#2521@Github）\n* 【core   】     修复UrlBuilder的toURI方法将url重复编码（issue#2503@Github）\n* 【core   】     修复CollUtil.lastIndexOf序号错误问题\n* 【core   】     修复zip被识别成jar和apk被识别成jar或zip的问题（pr#2548@Github）\n* 【core   】     修复UrlBuilder.addPath 方法传入非有效路径字符串时，会出现空指针异常的问题（issue#I5O4ML@Gitee）\n* 【core   】     修复FilterIter当参数filter为空时存在问题（issue#I5OG7U@Gitee）\n* 【poi    】     修复Excel读取提示信息错误（issue#I5OSFC@Gitee）\n* 【json   】     解决JSONObject#write无法递归的bug（issue#I5OMSC@Gitee）\n* 【json   】     修复DayOfWeek转json异常问题（issue#2572@Github）\n* 【extra  】     Ftp方法isDir和exist修复及改进（pr#2574@Github）\n* 【json   】     修复JSON反序列化时，引用字段类型的自定义JsonDeserializer无效（issue#2555@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.5 (2022-07-29)\n\n### ❌不兼容特性\n* 【core   】     合成注解相关功能重构，增加@Link及其子注解（pr#702@Gitee）\n\n### 🐣新特性\n* 【core   】     NumberUtil新增isIn方法（pr#669@Gitee）\n* 【core   】     修复注解工具类getAnnotations的NPE问题，注解扫描器添新功能（pr#671@Gitee）\n* 【core   】     合成注解SyntheticAnnotation提取为接口，并为实现类添加注解选择器和属性处理器（pr#678@Gitee）\n* 【core   】     增加BeanValueProvider（issue#I5FBHV@Gitee）\n* 【core   】     Convert工具类中，新增中文大写数字金额转换为数字工具方法（pr#674@Gitee）\n* 【core   】     新增CollectorUtil.reduceListMap()（pr#676@Gitee）\n* 【core   】     CollStreamUtil为空返回空的集合变为可编辑（pr#681@Gitee）\n* 【core   】     增加StrUtil.containsAll（pr#2437@Github）\n* 【core   】     ForestMap添加getNodeValue方法（pr#699@Gitee）\n* 【http   】     优化HttpUtil.isHttp判断，避免NPE（pr#698@Gitee）\n* 【core   】     修复Dict#containsKey方法没区分大小写问题（pr#697@Gitee）\n* 【core   】     增加比较两个LocalDateTime是否为同一天（pr#693@Gitee）\n* 【core   】     增加TemporalAccessorUtil.isIn、LocalDateTimeUtil.isIn（issue#I5HBL0@Gitee）\n* 【core   】     ReUtil增加getAllGroups重载（pr#2455@Github）\n* 【core   】     PageUtil#totalPage增加totalCount为long类型的重载方法（pr#2442@Github）\n* 【crypto 】     PemUtil.readPemPrivateKey支持pkcs#1格式，增加OpensslKeyUtil（pr#2456@Github）\n* 【core   】     添加了通用的注解扫描器 `GenericAnnotationScanner`，并在 `AnnotationScanner` 接口中统一提供了提前配置好的扫描器静态实例（pr#715@Github）\n* 【json   】     JSONConfig增加允许重复key配置，解决不规整json序列化的问题（pr#720@Github）\n* 【core   】     完善了codec包下一些方法的入参空校验（pr#719@Gitee）\n* 【extra  】     完善QrCodeUtil对于DATA_MATRIX生成的形状随机不可指定的功能（pr#722@Gitee）\n* 【core   】     修改NetUtil.ipv6ToBigInteger，原方法标记为过期（pr#2485@Github）\n* 【core   】     ZipUtil新增zip文件解压大小限制，防止zip炸弹（pr#726@Gitee）\n* 【core   】     CompressUtil增加压缩和解压tgz（.tar.gz）文件（issue#I5J33E@Gitee）\n* \n### 🐞Bug修复\n* 【core   】     修复CollUtil里面关于可变参数传null造成的crash问题（pr#2428@Github）\n* 【socket 】     修复异常socket没有关闭问题（pr#690@Gitee）\n* 【core   】     修复当时间戳为Integer时时间转换问题（pr#2449@Github）\n* 【core   】     修复bmp文件判断问题（issue#I5H93G@Gitee）\n* 【core   】     修复CombinationAnnotationElement造成递归循环（issue#I5FQGW@Gitee）\n* 【core   】     修复Dict缺少putIfAbsent、computeIfAbsent问题（issue#I5FQGW@Gitee）\n* 【core   】     修复Console.log应该把异常信息输出位置错误问题（pr#716@Gitee）\n* 【core   】     修复UrlBuilder无法配置末尾追加“/”问题（issue#2459@Github）\n* 【core   】     修复SystemPropsUtil.getBoolean方法应该只有值为true时才返回true，其他情况都应该返回false（pr#717@Gitee）\n* 【core   】     修复isBase64判断不准确的问题（pr#727@Gitee）\n* 【core   】     修复Convert#toMap默认转成HashMap的问题（pr#729@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.4 (2022-06-27)\n\n### 🐣新特性\n* 【extra  】     Sftp增加构造重载，支持超时（pr#653@Gitee）\n* 【core   】     BeanUtil增加isCommonFieldsEqual（pr#653@Gitee）\n* 【json   】     修改byte[]统一转换为数组形式（issue#2377@Github）\n* 【http   】     HttpResponse增加body方法，支持自定义返回内容（pr#655@Gitee）\n* 【core   】     修改ObjectUtil.isNull逻辑（issue#I5COJF@Gitee）\n* 【core   】     BlockPolicy增加线程池关闭后的逻辑（pr#660@Gitee）\n* 【core   】     Ipv4Util增加ipv4ToLong重载（pr#661@Gitee）\n* 【core   】     LocalDateTimeUtil.parse改为blank检查（issue#I5CZJ9@Gitee）\n* 【core   】     BeanPath在空元素时默认加入map，修改根据下标类型赋值List or map（issue#2362@Github）\n* 【core   】     localAddressList 添加重构方法（pr#665@Gitee）\n* 【cron   】     从配置文件加载任务时，自定义ID避免重复从配置文件加载（issue#I5E7BM@Gitee）\n* 【core   】     新增注解扫描器和合成注解（pr#654@Gitee）\n* \n### 🐞Bug修复\n* 【extra  】     修复createExtractor中抛出异常后流未关闭问题（pr#2384@Github）\n* 【core   】     修复CsvData.getHeader没有判空导致空指针问题（issue#I5CK7Q@Gitee）\n* 【core   】     修复单字母转换为数字的问题（issue#I5C4K1@Gitee）\n* 【core   】     修复IterUtil.filter无效问题\n* 【core   】     修复NumberUtil传入null，返回了true(issue#I5DTSL@Gitee)\n* 【core   】     修复NumberUtil.isDouble问题(pr#2400@Github)\n* 【core   】     修复ZipUtil使用append替换文件时，父目录存在报错问题(issue#I5DRU0@Gitee)\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.3 (2022-06-10)\n\n### 🐣新特性\n* 【extra  】     mail增加writeTimeout参数支持（issue#2355@Github）\n* 【core   】     FileTypeUtil增加pptx扩展名支持（issue#I5A0GO@Gitee）\n* 【core   】     IterUtil.get增加判空（issue#I5B12A@Gitee）\n* 【core   】     FileTypeUtil增加webp类型判断（issue#I5BGTF@Gitee）\n### 🐞Bug修复\n* 【core   】     修复NumberUtil.isXXX空判断错误（issue#2356@Github）\n* 【core   】     修复Convert.toSBC空指针问题（issue#I5APKK@Gitee）\n* 【json   】     修复Bean中存在bytes，无法转换问题（issue#2365@Github）\n* 【core   】     ArrayUtil.setOrAppend()传入空数组时，抛出异常（issue#I5APJE@Gitee）\n* 【extra  】     JschSessionPool修复空指针检查问题（issue#I5BK4D@Gitee）\n* 【core   】     修复使用ValueProvider中setFieldMapping无效问题（issue#I5B4R7@Gitee）\n* 【json   】     修复byte[]作为JSONArray构造问题（issue#2369@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.2 (2022-05-27)\n\n### 🐣新特性\n* 【core   】     BeanUtil拷贝对象增加空检查（issue#I58CJ3@Gitee）\n* 【db     】     Column#size改为long\n* 【core   】     ClassUtil增加isInterface等方法（pr#623@Gitee）\n* 【socket 】     增加ChannelUtil\n\n### 🐞Bug修复\n* 【extra  】     修复SshjSftp初始化未能代入端口配置问题（issue#2333@Github）\n* 【core   】     修复Convert.numberToSimple转换问题（issue#2334@Github）\n* 【core   】     修复TemporalAccessorConverter导致的转换问题（issue#2341@Github）\n* 【core   】     修复NumberUtil除法空指针问题（issue#I58XKE@Gitee）\n* 【core   】     修复CAR_VIN正则（pr#624@Gitee）\n* 【db     】     修复count查询别名问题（issue#I590YB@Gitee）\n* 【json   】     修复json中byte[]无法转换问题（issue#I59LW4@Gitee）\n* 【core   】     修复NumberUtil.isXXX未判空问题（issue#2350@Github）\n* 【core   】     修复Singleton中ConcurrentHashMap在JDK8下的bug引起的可能的死循环问题（issue#2349@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.1 (2022-05-16)\n\n### 🐣新特性\n* 【core   】     BooleanUtil增加toBooleanObject方法（issue#I56AG3@Gitee）\n* 【core   】     CharSequenceUtil增加startWithAnyIgnoreCase方法（issue#2312@Github）\n* 【system 】     JavaInfo增加版本（issue#2310@Github）\n* 【core   】     新增CastUtil（pr#2313@Github）\n* 【core   】     ByteUtil新增bytesToShort重载（issue#I57FA7@Gitee）\n* 【core   】     ReflectUtil.invoke方法抛出运行时异常增加InvocationTargetRuntimeException（issue#I57GI2@Gitee）\n* 【core   】     NumberUtil.parseNumber支持16进制（issue#2328@Github）\n\n### 🐞Bug修复\n* 【core   】     MapUtil.map对null友好，且修复了测试用例中分组问题（pr#614@Gitee）\n* 【core   】     修复BeanUtil.beanToMap中properties为null的空指针问题（issue#2303@Github）\n* 【db     】     DialectName中修正为POSTGRESQL（issue#2308@Github）\n* 【core   】     修复BeanPath无法识别引号内的内容问题（issue#I56DE0@Gitee）\n* 【core   】     修复Map.entry方法返回可变不可变相反问题\n* 【jwt    】     修复jwt的过期容忍时间问题（issue#2329@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.0 (2022-05-06)\n\n### ❌不兼容特性\n* 【extra  】     升级jakarta.validation-api到3.x，包名变更导致不能向下兼容\n* 【core   】     BeanUtil删除了beanToMap(Object)方法，因为有可变参数的方法，这个删除可能导致直接升级找不到方法，重新编译项目即可。\n\n### 🐣新特性\n* 【core   】     Singleton增加部分方法（pr#609@Gitee）\n* 【core   】     BeanUtil增加beanToMap重载（pr#2292@Github）\n* 【core   】     Assert增加对应的equals及notEquals方法（pr#612@Gitee）\n* 【core   】     Assert增加对应的equals及notEquals方法（pr#612@Gitee）\n* 【core   】     DigestUtil增加sha512方法（issue#2298@Github）\n\n### 🐞Bug修复\n* 【db     】     修复RedisDS无法设置maxWaitMillis问题（issue#I54TZ9@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.0.M4 (2022-04-27)\n\n### ❌不兼容特性\n* 【json   】     【可能兼容问题】JSONArray删除部分构造\n* 【json   】     【可能兼容问题】JSONTokener使用InputStream作为源时，由系统编码变更为UTF-8\n\n### 🐣新特性\n* 【core   】     BeanUtil增加toBean重载（pr#598@Gitee）\n* 【json   】     新增JSONParser\n* 【json   】     JSON新增在解析时的过滤方法（issue#I52O85@Gitee）\n* 【core   】     添加ArrayUtil.distinct、CollUtil.distinct重载（issue#2256@Github）\n* 【core   】     添加TransMap、FuncMap、ReferenceConcurrentMap、WeakConcurrentMap\n* 【json   】     添加ObjectMapper\n* 【core   】     CHINESE_NAME正则条件放宽（pr#599@Gitee）\n* 【extra  】     增加JakartaServletUtil（issue#2271@Github）\n* 【poi    】     ExcelWriter支持重复别名的数据写出（issue#I53APY@Gitee）\n* 【core   】     增加Hashids（issue#I53APY@Gitee）\n* 【core   】     ReflectUtil.newInstanceIfPossible添加枚举、数组等类型的默认实现\n* 【core   】     CombinationAnnotationElement增加过滤（pr#605@Gitee）\n* 【all    】     精简CHANGELOG\n* 【core   】     新增AnsiEncoder\n* 【log    】     新增彩色日式输出风格ConsoleColorLog（pr#607@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复StrUtil.firstNonX非static问题（issue#2257@Github）\n* 【core   】     修复SimpleCache线程安全问题\n* 【core   】     修复ClassLoaderUtil中可能的关联ClassLoader错位问题\n* 【extra  】     修复Sftp错误内容解析大小写问题（issue#I53GPI@Gitee）\n* 【core   】     修复当文件内容为空时，会报异常问题（pr#602@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.0.M3 (2022-04-14)\n\n### ❌不兼容特性\n* 【core   】     StreamProgress#progress方法参数变更为2个（pr#594@Gitee）\n* 【core   】     SimpleCache的raw key使用Mutable\n* 【core   】     ArrayUtil.join删除已经弃用的无用原始类型重载\n* 【core   】     删除Holder类，ReUtil.extractMultiAndDelPre方法参数改为Mutable\n\n### 🐣新特性\n* 【core   】     CopyOptions支持以Lambda方式设置忽略属性列表（pr#590@Gitee）\n* 【core   】     增加中文姓名正则及其校验（pr#592@Gitee）\n* 【core   】     Snowflake支持sequence使用随机数（issue#I51EJY@Gitee）\n* 【core   】     JarClassLoader增加构造（pr#593@Gitee）\n* 【core   】     增加Pid，以便获取单例pid\n* 【core   】     Img增加全覆盖水印pressTextFull（pr#595@Gitee）\n* 【core   】     ByteUtil.numberToBytes增加Byte判断（issue#2252@Github）\n* 【core   】     CopyOptions添加converter，可以自定义非全局类型转换\n* 【core   】     添加了设置从绝对路径加载数据库配置文件的功能（pr#2253@Github）\n\n### 🐞Bug修复\n* 【core   】     修复UserAgentUtil识别Linux出错（issue#I50YGY@Gitee）\n* 【poi    】     修复ExcelWriter.getDisposition方法生成错误（issue#2239@Github）\n* 【core   】     修复UrlBuilder重复编码的问题（issue#2243@Github）\n* 【http   】     修复HttpRequest中urlQuery，处理get请求参数的时候会导致空指针异常（pr#2248@Github）\n* 【core   】     修复SimpleCache在get时未使用读锁可能导致的问题\n* 【aop    】     修复JdkInterceptor before 方法拦截 return false 仍然执行了 after 的拦截问题（issue#I5237G@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.0.M2 (2022-04-02)\n\n### ❌不兼容特性\n* 【extra  】     【可能兼容问题】BeanCopierCache的key结构变更\n* 【http   】     【可能兼容问题】HttpInterceptor增加泛型标识，HttpRequest中配置汇总于HttpConfig\n* 【core   】     【可能兼容问题】UrlQuery.addQuery参数2从String变更为Object\n* 【core   】     【可能兼容问题】WorkbookUtil.createBook实现改为WorkbookFactory.create\n\n### 🐣新特性\n* 【core   】     MapUtil增加entry、ofEntries方法\n* 【core   】     ZipWriter增加add方法重载\n* 【core   】     IterUtil增加filtered，增加FilterIter（issue#2228）\n* 【core   】     增加NodeListIter、ResettableIter\n* 【crypto 】     HmacAlgorithm增加SM4CMAC（issue#2206@Github）\n* 【http   】     增加HttpConfig，响应支持拦截（issue#2217@Github）\n* 【core   】     增加BlockPolicy，ThreadUtil增加newFixedExecutor方法（pr#2231@Github）\n* 【crypto 】     BCMacEngine、Mac、CBCBlockCipherMacEngine、SM4MacEngine（issue#2206@Github）\n\n### 🐞Bug修复\n* 【core   】     IdcardUtil#getCityCodeByIdCard位数问题（issue#2224@Github）\n* 【core   】     修复urlWithParamIfGet函数逻辑问题（issue#I50IUD@Gitee）\n* 【core   】     修复IoUtil.readBytes限制长度读取问题（issue#2230@Github）\n* 【http   】     修复HttpRequest中编码对URL无效的问题（issue#I50NHQ@Gitee）\n* 【poi    】     修复读取excel抛NPE错误（pr#2234@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.8.0.M1 (2022-03-28)\n\n### ❌不兼容特性\n* 【db     】     【不向下兼容  】增加MongoDB4.x支持返回MongoClient变更（pr#568@Gitee）\n* 【json   】     【可能兼容问题】修改JSONObject结构，继承自MapWrapper\n* 【core   】     【可能兼容问题】BeanCopier重构，新建XXXCopier，删除XXXValueProvider\n* 【core   】     【可能兼容问题】URLEncoder废弃，URLEncoderUtil使用RFC3986\n* 【core   】     【可能兼容问题】Base32分离编码和解码，以便减少数据加载，支持Hex模式\n* 【core   】     【可能兼容问题】Base58分离编码和解码\n* 【core   】     【可能兼容问题】Base62分离编码和解码，增加inverted模式支持\n* 【core   】     【兼容问题   】PunyCode参数由String改为Charsequence\n* 【cron   】     【可能兼容问题】SimpleValueParser改名为AbsValueParser，改为abstract\n* 【poi    】     【可能兼容问题】ExcelUtil.getBigWriter返回值改为BigExcelWriter\n* 【core   】     【可能兼容问题】Opt.ofEmptyAble参数由List改为Collection子类（pr#580@Gitee）\n* 【json   】     【可能兼容问题】JSON转Bean时，使用JSON本身的相关设置，而非默认（issue#2212@Github）\n* 【json   】     【可能兼容问题】JSONConfig中isOrder废弃，默认全部有序\n\n### 🐣新特性\n* 【http   】     HttpRequest.form采用TableMap方式（issue#I4W427@Gitee）\n* 【core   】     AnnotationUtil增加getAnnotationAlias方法（pr#554@Gitee）\n* 【core   】     FileUtil.extName增加对tar.gz特殊处理（issue#I4W5FS@Gitee）\n* 【crypto 】     增加XXTEA实现（issue#I4WH2X@Gitee）\n* 【core   】     增加Table实现（issue#2179@Github）\n* 【core   】     增加UniqueKeySet（issue#I4WUWR@Gitee）\n* 【core   】     阿拉伯数字转换成中文对发票票面金额转换的扩展（pr#570@Gitee）\n* 【core   】     ArrayUtil增加replace方法（pr#570@Gitee）\n* 【core   】     CsvReadConfig增加自定义标题行行号（issue#2180@Github）\n* 【core   】     FileAppender优化初始List大小（pr#2197@Github）\n* 【core   】     Base32增加pad支持（pr#2195@Github）\n* 【core   】     Dict增加setFields方法（pr#578@Gitee）\n* 【db     】     新加db.meta的索引相关接口（pr#563@Gitee）\n* 【db     】     Oracle中Column#typeName后的长度去掉（pr#563@Gitee）\n* 【poi    】     优化ExcelReader，采用只读模式（pr#2204@Gitee）\n* 【poi    】     优化ExcelBase，将alias放入\n* 【poi    】     优化ExcelBase，将alias放入\n* 【core   】     改进StrUtil#startWith、endWith性能\n* 【cron   】     增加CronPatternParser、MatcherTable\n* 【http   】     GlobalHeaders增加系统属性allowUnsafeServerCertChange、allowUnsafeRenegotiation\n* 【http   】     UserAgentUtil 解析，增加MiUI/XiaoMi浏览器判断逻辑（pr#581@Gitee）\n* 【core   】     FileAppender添加锁构造（pr#2211@Github）\n* 【poi    】     ExcelReader增加构造（pr#2213@Github）\n* 【core   】     MapUtil提供change函数，EnumUtil提供getBy函数，通过lambda进行枚举字段映射（pr#583@Gitee）\n* 【core   】     CompareUtil增加comparingIndexed（pr#585@Gitee）\n* 【db     】     DruidDataSource构建时支持自定义参数（issue#I4ZKCW@Gitee）\n* 【poi    】     ExcelWriter增加addImg重载（issue#2218@Github）\n* 【bloomFilter】 增加FuncFilter\n* 【http   】     增加GlobalInterceptor（issue#2217）\n\n### 🐞Bug修复\n* 【core   】     修复ObjectUtil.hasNull传入null返回true的问题（pr#555@Gitee）\n* 【core   】     修复NumberConverter对数字转换的问题（issue#I4WPF4@Gitee）\n* 【core   】     修复ReflectUtil.getMethods获取接口方法问题（issue#I4WUWR@Gitee）\n* 【core   】     修复NamingCase中大写转换问题（pr#572@Gitee）\n* 【http   】     修复GET重定向时，携带参数问题（issue#2189@Github）\n* 【core   】     修复FileUtil、FileCopier相对路径获取父路径错误问题（pr#2188@Github）\n* 【core   】     修复CopyOptions中fieldNameEditor无效问题（issue#2202@Github）\n* 【json   】     修复JSON对Map.Entry的解析问题\n* 【core   】     修复MapConverter中map与map转换兼容问题\n* 【poi    】     解决sax读取时，POI-5.2.x兼容性问题\n* 【core   】     修复判断两段时间区间交集问题（pr#2210@Github）\n* 【http   】     修复标签误删问题（issue#I4Z7BV@Gitee）\n* 【core   】     修复Win下文件名带*问题（pr#584@Gitee）\n* 【core   】     FileUtil.getMimeType增加rar、7z支持（issue#I4ZBN0@Gitee）\n* 【json   】     JSON修复transient设置无效问题（issue#2212@Github）\n* 【core   】     修复IterUtil.getElementType获取结果为null的问题（issue#2222@Github）\n* 【core   】     修复农历转公历在闰月时错误（issue#I4ZSGJ@Gitee）\n\n# 5.7.x 或更早版本\n* [https://gitee.com/chinabugotech/hutool/blob/v5-master/CHANGELOG_5.0-5.7.md](https://gitee.com/chinabugotech/hutool/blob/v5-master/CHANGELOG_5.0-5.7.md)"
  },
  {
    "path": "CHANGELOG_5.0-5.7.md",
    "content": "\n# 🚀Changelog\n\n# 5.7.22 (2022-03-01)\n\n### 🐣新特性\n* 【poi    】     ExcelUtil.readBySax增加对POI-5.2.0的兼容性（issue#I4TJF4@Gitee）\n* 【extra  】     Ftp增加构造（issue#I4TKXP@Gitee）\n* 【core   】     GenericBuilder支持Map构建（pr#540@Github）\n* 【json   】     新增TemporalAccessorSerializer\n* 【core   】     使多个xxxBuilder实现Builder接口，扩展CheckedUtil（pr#545@Gitee）\n* 【core   】     CheckedUtil删除第二个参数为RuntimeException的方法\n* 【core   】     FileUtil增加getTotalLines方法\n* 【db     】     MetaUtil增加getTableMeta重载（issue#2157@Github）\n* 【http   】     增加HttpGlobalConfig.setDecodeUrl（issue#I4U8YQ@Gitee）\n* 【core   】     增加Base58（pr#2162@Github）\n* 【core   】     增加AntPathMatcher（issue#I4T7K5@Gitee）\n* 【core   】     StrJoiner修改toString策略，调用不再修改Appendable\n* 【core   】     StrJoiner增加length和merge方法\n* 【core   】     CRC16增加getHexValue方法（issue#I4VO3U@Gitee）\n\n### 🐞Bug修复\n* 【cache  】     修复ReentrantCache.toString方法线程不安全问题（issue#2140@Github）\n* 【core   】     修复SystemPropsUtil.getInt返回long问题（pr#546@Gitee）\n* 【crypto 】     修复SM2.getD前导0问题（pr#2149@Github）\n* 【core   】     修复ChineseDate在1970年之前农历差一天问题（issue#I4UTPK@Gitee）\n* 【core   】     修复CoordinateUtil精准问题及转换bug（pr#551@Gitee）\n* 【json   】     修复JSONObject解析XML后没有返回的bug（issue#2160@Github）\n* 【extra  】     修复GanymedUtil错误信息读取位置问题（issue#I4VDZ2@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.7.21 (2022-02-14)\n\n### 🐣新特性\n* 【extra  】     增加jetbrick模板支持\n* 【extra  】     EmojiUtil增加方法（pr#519@Gitee）\n* 【core   】     DateUtil 添加两个日期是否同一周方法（pr#516@Gitee）\n* 【db     】     新增条件组，用于处理复杂的where条件（pr#514@Gitee）\n* 【core   】     新增LocalDateTimeUtil.weekOfYear（issue#I4RWXC@Gitee）\n* 【core   】     Month增加toJdkMonth、getValueBaseOne\n* 【core   】     CsvWriter修改规则，去除末尾多余换行符（issue#I4RSQY@Gitee）\n* 【core   】     DateUtil增加rangeFunc和rangeConsume（issue#I4RSQY@Gitee）\n* 【core   】     DateTime增加setUseJdkToStringStyle方法\n* 【core   】     CharSequenceUtil增加replace重载(issue#2122@Github)\n* 【core   】     IntMap和LongMap使用位运算快速求解取余运算(pr#2123@Github)\n* 【core   】     新增通用builder类：GenericBuilder(pr#526@Gitee)\n* 【core   】     新增copySafely方法与mkdirsSafely方法(pr#527@Gitee)\n* 【core   】     新增MetroHash(pr#532@Gitee)\n* 【core   】     SpringUtil增加publishEvent重载(pr#2139@Github)\n* 【core   】     DateUtil增加rangeContains、rangeNotContains(pr#537@Gitee)\n* 【core   】     Resource增加isModified默认方法\n* 【core   】     增加VfsResource\n* 【json   】     JSONConfig增加setKeyComparator、setNatureKeyComparator方法，支持自定义排序（issue#I4RBZ4@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复ChineseDate农历获取正月出现数组越界BUG（issue#2112@Github）\n* 【extra  】     修复EmojiUtil.toHtmlHex()方法（pr#519@Gitee）\n* 【system 】     修复CpuInfo.getUsed()方法（issue#2116@Github）\n* 【dfa    】     修复密集匹配和贪婪匹配冲突问题（issue#2126@Github）\n* 【db     】     修复c3p0丢失信息问题（issue#I4T7XZ@Gitee）\n* 【http   】     修复Action中HttpExchange没有关闭问题\n* 【http   】     修复Action中HttpExchange没有关闭问题\n\n-------------------------------------------------------------------------------------------------------------\n# 5.7.20 (2022-01-20)\n\n### 🐣新特性\n* 【core   】     增加对null值友好的groupingBy操作的Collector实现，可指定map类型（pr#498@Gitee）\n* 【core   】     增加KetamaHash（issue#2084@Github）\n* 【crypto 】     增加SignUtil\n* 【json   】     JSONGetter增加getBeanList方法\n* 【core   】     ObjectUtil 添加三个defaultIfXxxx方法，用于节省CPU及内存损耗(pr#2094@Github)\n* 【db     】     增加单条数据原生upsert语义支持(pr#501@Gitee)\n* 【core   】     在CollectorUtil提交Collectors.toMap的对null友好实现，避免NPE(pr#502@Gitee)\n* 【http   】     增加HttpGlobalConfig.setIgnoreEOFError(issue#2092@Github)\n* 【core   】     RandomUtil.randomStringWithoutStr排除字符串兼容大写字母(pr#503@Gitee)\n* 【core   】     LocalDateTime增加isOverlap方法(pr#512@Gitee)\n* 【core   】     Ipv4Util.getBeginIpLong、getEndIpLong改为public(pr#508@Gitee)\n* \n### 🐞Bug修复\n* 【core   】     修复setter重载导致匹配错误（issue#2082@Github）\n* 【core   】     修复RegexPool汉字匹配范围小问题（pr#2081@Github）\n* 【core   】     修复OS中的拼写错误（pr#500@Gitee）\n* 【core   】     修复CustomKeyMap的merge失效问题（issue#2086@Github）\n* 【core   】     修复FileUtil.appendLines换行问题（issue#I4QCEZ@Gitee）\n* 【core   】     修复java.time.Month解析问题（issue#2090@Github）\n* 【core   】     修复PathUtil.moveContent移动覆盖导致的问题（issue#I4QV0L@Gitee）\n* 【core   】     修复Opt.ofTry中并发环境下线程安全问题（pr#504@Gitee）\n* 【core   】     修复PatternFinder中end边界判断问题（issue#2099@Github）\n* 【core   】     修复格式化为中文日期时，0被处理为空串（pr#507@Gitee）\n* 【core   】     修复UrlPath转义冒号问题（issue#I4RA42@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.7.19 (2022-01-07)\n\n### 🐣新特性\n* 【db     】     优化Condition参数拆分（pr#2046@Github）\n* 【core   】     优化ArrayUtil.isAllEmpty性能（pr#2045@Github）\n* 【core   】     CharSequenceUtil.replace方法支持增补字符（pr#2041@Github）\n* 【extra  】     增加SshjSftp（pr#493@Gitee）\n* 【core   】     增加CheckedUtil（pr#491@Gitee）\n* 【extra  】     增加Sftp.isDir中的抛异常判断条件（issues#I4P9ED@Gitee）\n\n### 🐞Bug修复\n* 【http   】     HttpUtil重定向次数失效问题（issue#I4O28Q@Gitee）\n* 【core   】     修复UrlPath空白path多/问题（issue#I49KAL@Gitee）\n* 【core   】     修复ServletUtil写出文件时未添加双引号导致逗号等特殊符号引起的问题（issue#I4P1BF@Gitee）\n* 【core   】     NumberUtil增加equals重载解决long传入判断问题（pr#2064@Github）\n* 【core   】     修复CsvParser行号有误问题（pr#2065@Github）\n* 【http   】     修复HttpRequest.of无法自动添加http前缀问题（issue#I4PEYL@Gitee）\n* 【core   】     修复 `CharSequenceUtil.brief(str, maxLength)` 方法字符串越界问题，以及 `maxLength` 部分值时结果与预期不符的问题（pr#2068@Github）\n* 【core   】     修复NamingCase中转换下划线字母+数字转换问题（issue#2070@Github）\n* 【core   】     修复split空判断不一致问题（pr#496@Gitee）\n* 【crypto 】     修复SM2.getDHex()前导0丢失,然后导致获取密钥错误（pr#2073@Github）\n* 【core   】     修复关于Calculator.conversion()方法EmptyStackException的bug（pr#2076@Github）\n* 【core   】     修复StrUtil.subBetweenAll循环bug（issue#I4PT3M@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.7.18 (2021-12-25)\n\n### 🐣新特性\n* 【core   】     新增CollStreamUtil.groupKeyValue（pr#479@Gitee）\n* 【core   】     新增DatePattern.createFormatter（pr#483@Gitee）\n* 【core   】     增加IdUtil.getSnowflakeNextId（pr#485@Gitee）\n* 【log    】     log4j2的编译依赖改为api，core为test依赖（pr#2019@Github）\n* 【core   】     Img.scale缩小默认使用平滑模式，增加scale方法重载可选模式（issue#I4MY6X@Gitee）\n* 【core   】     excel添加写入图片的方法（pr#486@Gitee）\n* 【core   】     增加CollStreamUtil.groupBy（pr#484@Gitee）\n* 【core   】     增加CollUtil.setValueByMap（pr#482@Gitee）\n* 【core   】     LocalDateTimeUtil增加endOfDay重载（issue#2025@Github）\n* 【core   】     IoCopier增加setFlushEveryBuffer方法（issue#2022@Github）\n* \n### 🐞Bug修复\n* 【core   】     LineReadWatcher#onModify文件清空判断问题（issue#2013@Github）\n* 【core   】     修复4位bytes转换float问题（issue#I4M0E4@Gitee）\n* 【core   】     修复CharSequenceUtil.replace问题（issue#I4M16G@Gitee）\n* 【json   】     修复JSONObject 初始化大小值未被使用问题（issue#2016@Github）\n* 【core   】     修复StrUtil.startWith都为null返回错误问题（issue#I4MV7Q@Gitee）\n* 【core   】     修复PasswdStrength检测问题（issue#I4N48X@Gitee）\n* 【core   】     修复UserAgentUtil解析EdgA无法识别问题（issue#I4MCBP@Gitee）\n* 【extra  】     修复Archiver路径前带/问题（issue#I4NS0F@Gitee）\n* 【extra  】     修复getMainColor方法中参数rgbFilters无效问题（pr#2034@Github）\n* 【core   】     修复ChineseDate无法区分闰月问题（issue#I4NQQW@Gitee）\n* 【core   】     修复BeanDesc大小写误判问题（issue#2009@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.7.17 (2021-12-09)\n\n### 🐣新特性\n* 【core   】     增加AsyncUtil（pr#457@Gitee）\n* 【http   】     增加HttpResource（issue#1943@Github）\n* 【http   】     增加BytesBody、FormUrlEncodedBody\n* 【cron   】     TaskTable.remove增加返回值（issue#I4HX3B@Gitee）\n* 【core   】     Tree增加filter、filterNew、cloneTree、hasChild方法（issue#I4HFC6@Gitee）\n* 【poi    】     增加ColumnSheetReader及ExcelReader.readColumn，支持读取某一列\n* 【core   】     IdCardUtil.isValidCard不再自动trim（issue#I4I04O@Gitee）\n* 【core   】     改进TextFinder，支持限制结束位置及反向查找模式\n* 【core   】     Opt增加部分方法（pr#459@Gitee）\n* 【core   】     增加DefaultCloneable（pr#459@Gitee）\n* 【core   】     CollStreamUtil增加是否并行的重载（pr#467@Gitee）\n* 【core   】     ResourceClassLoader增加缓存（pr#1959@Github）\n* 【crypto 】     增加CipherWrapper，增加setRandom（issue#1958@Github）\n* 【core   】     Opt增加ofTry方法（pr#1956@Github）\n* 【core   】     DateUtil.toIntSecond标记为弃用（issue#I4JHPR@Gitee）\n* 【db     】     Db.executeBatch标记一个重载为弃用（issue#I4JIPH@Gitee）\n* 【core   】     增加CharSequenceUtil.subPreGbk重载（issue#I4JO2E@Gitee）\n* 【core   】     ReflectUtil.getMethod排除桥接方法（pr#1965@Github）\n* 【http   】     completeFileNameFromHeader在使用path为路径时，自动解码（issue#I4K0FS@Gitee）\n* 【core   】     CopyOptions增加override配置（issue#I4JQ1N@Gitee）\n* 【poi    】     SheetRidReader可以获取所有sheet名（issue#I4JA3M@Gitee）\n* 【core   】     AsyncUtil.waitAny增加返回值（pr#473@Gitee）\n* 【core   】     Calculator.compare改为private（issue#1982@Github）\n* 【core   】     NumberUtil增加isOdd、isEven方法（pr#474@Gitee）\n* 【http   】     增加HttpGlobalConfig.setBoundary，删除MultipartBody.BOUNDARY和getContentType（issue#I4KSLY@Gitee）\n* 【core   】     DateTime增加setMinimalDaysInFirstWeek（issue#1988@Github）\n* 【db     】     Db增加query重载，可支持自定义PreparedStatement，从而支持游标（issue#I4JXWN@Gitee）\n* 【cache  】     CacheObj增加getExpiredTime等方法（issue#I4LE80@Gitee）\n* 【extra  】     Ftp增加backToPwd方法（issue#2004@Github）\n* 【core   】     CollStreamUtil修改集合中null处理问题（pr#478@Gitee）\n* \n### 🐞Bug修复\n* 【core   】     修复FileResource构造fileName参数无效问题（issue#1942@Github）\n* 【cache  】     修复WeakCache键值强关联导致的无法回收问题（issue#1953@Github）\n* 【core   】     修复ZipUtil相对路径父路径获取null问题（issue#1961@Github）\n* 【http   】     修复HttpUtil.normalizeParams未判空导致的问题（issue#1975@Github）\n* 【poi    】     修复读取日期类型的自定义样式单元格时间结果为1899年问题（pr#1977@Github）\n* 【poi    】     修复SoapClient参数未使用问题\n* 【core   】     修复HashUtil.cityHash128参数未使用问题\n* 【core   】     修复DateUtil.formatChineseDate显示问题（issue#I4KK5F@Gitee）\n* 【poi    】     修复CellUtil.setCellValueStyle空导致值无法写入问题（issue#1995@Github）\n* 【poi    】     修复CellUtil.setComment参数设置错误问题\n* 【core   】     修复QueryBuilder解析路径导致的错误（issue#1989@Github）\n* 【core   】     修复DateTime.between中DateUnit无效问题\n* 【poi    】     修复StyleUtil.getFormat非static问题（issue#I4LGNP@Gitee）\n* 【crypto 】     修复SM2.getD返回bytes包含符号位的问题（issue#2001@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.16 (2021-11-07)\n\n### 🐣新特性\n* 【core   】     增加DateTime.toLocalDateTime\n* 【core   】     CharSequenceUtil增加normalize方法（pr#444@Gitee）\n* 【core   】     MailAccount增加setEncodefilename()方法，可选是否编码附件的文件名（issue#I4F160@Gitee）\n* 【core   】     MailAccount中charset增加null时的默认规则\n* 【core   】     NumberUtil.compare修正注释说明（issue#I4FAJ1@Gitee）\n* 【core   】     增加RFC3986类\n* 【extra  】     Sftp增加put和upload重载（issue#I4FGDH@Gitee）\n* 【core   】     TemporalUtil增加toChronoUnit、toTimeUnit方法（issue#I4FGDH@Gitee）\n* 【core   】     StopWatch增加prettyPrint重载（issue#1910@Github）\n* 【core   】     修改RegexPool中Ipv4正则\n* 【json   】     Filter改为MutablePair，以便编辑键值对（issue#1921@Github）\n* 【core   】     Opt增加peeks方法（pr#445@Gitee）\n* 【extra  】     MailAccount中user默认值改为邮箱全称（issue#I4FYVY@Gitee）\n* 【core   】     增加CoordinateUtil（pr#446@Gitee）\n* 【core   】     DateUtil增加rangeToList重载（pr#1925@Github）\n* 【core   】     CollUtil增加safeContains方法（pr#1926@Github）\n* 【core   】     ActualTypeMapperPool增加getStrKeyMap方法（pr#447@Gitee）\n* 【core   】     TreeUtil增加walk方法（pr#1932@Gitee）\n* 【crypto 】     SmUtil增加sm3WithSalt（pr#454@Gitee）\n* 【http   】     增加HttpInterceptor（issue#I4H1ZV@Gitee）\n* 【core   】     Opt增加flattedMap（issue#I4H1ZV@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复UrlBuilder.addPath歧义问题（issue#1912@Github）\n* 【core   】     修复StrBuilder中总长度计算问题（issue#I4F9L7@Gitee）\n* 【core   】     修复CharSequenceUtil.wrapIfMissing预定义长度计算问题（issue#I4FDZ2@Gitee）\n* 【poi    】     修复合并单元格为日期时，导出单元格数据为数字问题（issue#1911@Github）\n* 【core   】     修复CompilerUtil.getFileManager参数没有使用的问题（issue#I4FIO6@Gitee）\n* 【core   】     修复NetUtil.isInRange的cidr判断问题（pr#1917@Github）\n* 【core   】     修复RegexPool中对URL正则匹配问题（issue#I4GRKD@Gitee）\n* 【core   】     修复UrlQuery对于application/x-www-form-urlencoded问题（issue#1931@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.15 (2021-10-21)\n\n### 🐣新特性\n* 【db     】     Db.quietSetAutoCommit增加判空（issue#I4D75B@Gitee）\n* 【core   】     增加RingIndexUtil（pr#438@Gitee）\n* 【core   】     Assert增加checkBetween重载（pr#436@Gitee）\n* 【core   】     ReUtil增加命名分组重载（pr#439@Gitee）\n* 【json   】     toString和writer增加Filter（issue#I4DQNQ@Gitee）\n* 【core   】     ContentType增加build重载（pr#1898@Github）\n* 【bom    】     支持scope=import方式引入（issue#1561@Github）\n* 【core   】     新增Hash接口，HashXXX继承此接口\n* 【core   】     ZipUtil增加append方法（pr#441@Gitee）\n* 【core   】     CollUtil增加重载（issue#I4E9FS@Gitee）\n* 【core   】     CopyOptions新增setFieldValueEditor（issue#I4E08T@Gitee）\n* 【core   】     增加SystemPropsUtil（issue#1918@Gitee）\n* 【core   】     增加`hutool.date.lenient`系统属性（issue#1918@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复CollUtil.isEqualList两个null返回错误问题（issue#1885@Github）\n* 【poi    】     修复ExcelWriter多余调试信息导致的问题（issue#1884@Github）\n* 【poi    】     修复TemporalAccessorUtil.toInstant使用DateTimeFormatter导致问题（issue#1891@Github）\n* 【poi    】     修复sheet.getRow(y)为null导致的问题（issue#1893@Github）\n* 【cache  】     修复LRUCache线程安全问题（issue#1895@Github）\n* 【crypto 】     修复KeyUtil异常信息参数丢失问题（issue#1902@Github）\n* 【core   】     修复StrUtil.split和splittoArray不一致问题（issue#I4ELU5@Github）\n* 【core   】     修复SymmetricCrypto未关闭CipherOutputStream导致的问题（issue#I4EMST@Gitee）\n* 【core   】     修复QueryBuilder对/转义问题（issue#1904@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.14 (2021-10-09)\n\n### 🐣新特性\n* 【extra  】     修复HttpCookie设置cookies的方法，不符合RFC6265规范问题（issue#I4B70D@Gitee）\n* 【http   】     优化Browser版本正则判断\n* 【setting】     增加YamlUtil\n* 【extra  】     SenvenZExtractor改名为SevenZExtractor，增加getFirst、get方法\n* 【core   】     DateConverter修改返回java.util.Date而非DateTime（issue#I4BOAP@Gitee）\n* 【core   】     增加IterableIter、ComputeIter\n* 【core   】     CsvConfig增加disableComment方法（issue#1842@Github）\n* 【core   】     DateTime构造和DateUtil.parse可选是否宽松模式（issue#1849@Github）\n* 【core   】     TreeBuilder增加部分根节点set方法（issue#1848@Github）\n* 【core   】     优化Base64.isBase64方法：减少一次多余的判断（pr#1860@Github）\n* 【cache  】     优化FIFOCache未设置过期策略时，无需遍历判断过期对象（pr#425@Gitee）\n* 【core   】     增加Opt类（pr#426@Gitee）\n* 【core   】     Week增加of重载，支持DayOfWek（pr#1872@Github）\n* 【poi    】     优化read，避免多次创建CopyOptions（issue#1875@Github）\n* 【core   】     优化CsvReader，实现可控遍历（pr#1873@Github）\n* 【core   】     优化Base64.isBase64判断（pr#1879@Github）\n* 【core   】     新增StrFormatter.formatWith（pr#430@Gitee）\n\n### 🐞Bug修复\n* 【http   】     修复HttpCookie设置cookies的方法，不符合RFC6265规范问题（pr#418@Gitee）\n* 【http   】     修复Extractor中filter无效问题\n* 【json   】     修复JSONGetter.getJSONArray判断null的问题（issue#I4C15H@Gitee）\n* 【db     】     修复Condition没占位符的情况下sql没引号问题（issue#1846@Github）\n* 【cache  】     修复FIFOCache中remove回调无效问题（pr#1856@Github）\n* 【json   】     修复JSONArray.set中，index为0报错问题（issue#1858@Github）\n* 【core   】     修复FileUtil.checkSlip中getCanonicalPath异常引起的问题（issue#1858@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.13 (2021-09-17)\n\n### 🐣新特性\n* 【core   】     CsvReadConfig增加trimField选项（issue#I49M0C@Gitee）\n* 【http   】     HttpBase增加clearHeaders方法（issue#I49P23@Gitee）\n* 【core   】     CsvWriter的write和writeBeans参数改为Iterable（issue#I49O4S@Gitee）\n* 【core   】     BitStatusUtil添加来源声明（issue#1824@Github）\n* 【core   】     UrlQuery.build增加重载，支持可选是否转义（issue#I4AIX1@Gitee）\n* 【core   】     ListUtil增加swapTo和swapElement方法（pr#416@Gitee）\n* 【poi    】     ExcelWriter支持Hyperlink（issue#I49QAL@Gitee）\n* \n### 🐞Bug修复\n* 【core   】     修复FuncKey函数无效问题\n* 【core   】     修复ImgUtil.copyImage读取网络URL后宽高报错问题（issue#1821@Github）\n* 【core   】     修复StrJoiner.append配置丢失问题（issue#I49K1L@Gitee）\n* 【core   】     修复EscapeUtil特殊字符的hex长度不足导致的问题（issue#I49JU8@Gitee）\n* 【core   】     修复UrlBuilder对Fragment部分编码问题（issue#I49KAL@Gitee）\n* 【core   】     修复Enum转换的bug（issue#I49VZB@Gitee）\n* 【json   】     修复JSONUtil.parse对于MapWrapper识别问题\n* 【core   】     修复IdcardUtil.isValidCard判断问题（issue#I4AJ8S@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.12 (2021-09-09)\n\n### 🐣新特性\n* 【system 】     OshiUtil增加getCurrentProcess方法\n* 【extra  】     SpringUtil增加getApplicationName、publishEvent方法（issue#I485NZ@Gitee）\n* 【core   】     BeanUtil.getProperty增加判空（issue#I488HA@Gitee）\n* 【core   】     OptionalBean弃用（pr#1182@Github）\n* 【setting】     Setting、Props持有URL改为持有Resource（pr#1182@Github）\n* 【json   】     JSONUtil.toJsonStr增加重载，支持JSONConfig（issue#I48H5L@Gitee）\n* 【crypto 】     SymmetricCrypto增加setMode方法，update采用累加模式（pr#1642@Github）\n* 【core   】     ZipReader支持Filter\n* 【all    】     Sftp、Ftp、HttpDownloader增加download重载，支持避免传输文件损坏（pr#407@Gitee）\n* 【crypto 】     AES修改构造的IvParameterSpec为AlgorithmParameterSpec（issue#1814@Gitee）\n* 【crypto 】     增加FPE、ZUC（issue#1814@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复ListUtil.split方法越界问题（issue#I48Q0P@Gitee）\n* 【core   】     修复QrCode的isTryHarder、isPureBarcode设置无效问题（issue#1815@Github）\n* 【core   】     修复DatePattern.CHINESE_DATE_FORMATTER错误问题（issue#I48ZE3@Gitee）\n* 【core   】     修复ListUtil.split错误问题\n* 【core   】     修复NumberUtil.parseNumber长数字越界问题（issue#1818@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.11 (2021-08-31)\n\n### 🐣新特性\n* 【crypto 】     修改SymmetricCrypto初始化逻辑\n* 【core   】     FileTypeUtil增加对wps编辑的docx的识别（issue#I47JGH@Gitee）\n* 【core   】     Money修改构造，0表示读取所有分（issue#1796@Github）\n* 【json   】     增加JSONXMLParser和JSONXMLSerializer\n* 【json   】     XML支持自定义内容标签（issue#I47TV8@Gitee）\n### 🐞Bug修复\n* 【cron   】     **重要**修复Scheduler启动默认线程池为null的bug（issue#I47PZW@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.10 (2021-08-26)\n\n### 🐣新特性\n* 【core   】     增加NamingCase类\n* 【core   】     ListUtil增加page方法重载（pr#1761@Github）\n* 【crypto 】     增加ASN1Util\n* 【core   】     CsvConfig改为泛型形式\n* 【core   】     增加Partition\n* 【http   】     SoapClient.sendForResponse改为public（issue#I466NN@Gitee）\n* 【core   】     XmlUtil增加append重载（issue#I466Q0@Gitee）\n* 【poi    】     增加EscapeStrCellSetter（issue#I466ZZ@Gitee）\n* 【poi    】     ExcelBase增加renameSheet、cloneSheet（issue#I466ZZ@Gitee）\n* 【core   】     ListUtil增加splitAvg方法（pr#397@Gitee）\n* 【poi    】     Excel07SaxReader支持数字类型sheet名称、支持sheetName:名称前缀（issue#I46OMA@Gitee）\n* 【extra  】     Mail增加build方法（issue#I46LGE@Gitee）\n* 【core   】     XmlUtil增加beanToXml重载，支持忽略null\n* 【core   】     添加NullComparator、FuncComparator（issue#I471X7@Gitee）\n* 【core   】     LambdaUtil添加getFieldName（issue#I4750U@Gitee）\n* 【cron   】     Scheduler增加setThreadExecutor（issue#I47A6N@Gitee）\n* 【core   】     CharsetDetector增加detect重载，支持自定义缓存大小（issue#I478E5@Gitee）\n* 【core   】     增加PartitionIter（pr#402@Gitee）\n* 【all    】     增加异常爬栈开关（pr#403@Gitee）\n* 【core   】     优化Combination中C(n,n)的逻辑（pr#1792@Github）\n* 【core   】     Csv读写支持别名（issue#1791@Github）\n\n### 🐞Bug修复\n* 【core   】     修复MapUtil.sort比较器不一致返回原map的问题（issue#I46AQJ@Gitee）\n* 【core   】     修复JSONSupport默认循环引用导致的问题（issue#1779@Github）\n* 【poi    】     修复ExcelUtil.readBySax资源没有释放问题（issue#1789@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.9 (2021-08-16)\n\n### 🐣新特性\n* 【extra  】    FileUtil增加moveContent方法（issue#I45H30@Gitee）\n* 【extra  】    JschPool.getSession获取时检查是否连接状态（issue#I45N5I@Gitee）\n* \n### 🐞Bug修复\n* 【extra  】    修复TinyPinyinEngine空构造造成可能的误判问题\n* 【http   】    修复在gzip模式下Content-Length服务端设置异常导致的问题（issue#1766@Github）\n* 【db     】    修复PooledDataSource关闭逻辑错误问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.8 (2021-08-11)\n\n### 🐣新特性\n* 【core   】     MapProxy支持return this的setter方法（pr#392@Gitee）\n* 【core   】     BeeDSFactory移除sqlite事务修复代码，新版本BeeCP已修复\n* 【core   】     增加compress包，扩充Zip操作灵活性\n* 【json   】     增加JSONBeanParser\n* 【poi    】     增加CellSetter，可以自定义单元格值写出\n* 【poi    】     CsvReader增加readFromStr（pr#1755@Github）\n* 【socket 】     SocketUtil增加connection方法\n* 【extra  】     JschUtil增加bindPort重载方法（issue#I44UTH@Github）\n* 【core   】     DefaultTrustManager改为继承X509ExtendedTrustManager\n* 【core   】     增加IoCopier\n\n### 🐞Bug修复\n* 【core   】     改进NumberChineseFormatter算法，补充完整单元测试，解决零问题\n* 【core   】     修复Img变换操作图片格式问题（issue#I44JRB@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.7 (2021-08-02)\n\n### 🐣新特性\n* 【core   】     增加LookupFactory和MethodHandleUtil（issue#I42TVY@Gitee）\n* 【core   】     改进RegexPool.TEL支持无-号码（pr#387@Gitee）\n* 【core   】     PhoneUtil中新增获取固话号码中区号,以及固话号码中号码的方法（pr#387@Gitee）\n* 【json   】     JSONGetter增加getLocalDateTime方法（pr#387@Gitee）\n* 【core   】     增加JNDIUtil（issue#1727@Github）\n* 【core   】     NetUtil增加getDnsInfo方法（issue#1727@Github）\n* 【core   】     SpringUtil增加unregisterBean方法（pr#388@Gitee）\n* 【core   】     优化TextSimilarity公共子串算法（issue#I42A6V@Gitee）\n* 【core   】     优化DateUtil.parse对UTC附带时区字符串解析（issue#I437AP@Gitee）\n\n### 🐞Bug修复\n* 【jwt    】     修复JWTUtil中几个方法非static的问题（issue#1735@Github）\n* 【core   】     修复SpringUtil无法处理autowired问题（pr#388@Gitee）\n* 【core   】     修复AbsCollValueMap中常量拼写错误（pr#1736@Github）\n* 【core   】     修复FileUtil.del在文件只读情况下无法删除的问题（pr#389@Gitee）\n* 【core   】     修复FileUtil.move在不同分区下失败的问题（pr#390@Gitee）\n* 【core   】     修复FileUtil.copy强制覆盖参数无效问题\n* 【core   】     修复NumberChineseFormatter转换金额多零问题（issue#1739@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.6 (2021-07-28)\n\n### 🐣新特性\n* 【core   】     增加FieldsComparator（pr#374@Gitee）\n* 【core   】     FileUtil.del采用Files.delete实现\n* 【core   】     改进Base64.isBase64方法增加等号判断（issue#1710@Github）\n* 【core   】     Sftp增加syncUpload方法（pr#375@Gitee）\n* 【core   】     改进NetUtil.getLocalHost逻辑（issue#1717@Github）\n* 【core   】     UseragentUtil增加QQ、alipay、taobao、uc等浏览器识别支持（issue#1719@Github）\n* 【http   】     HttpRequest.form方法判断集合增强（pr#381@Gitee）\n* 【core   】     NumberUtil增加calculate方法\n* 【core   】     优化TextSimilarity.longestCommonSubstring性能（issue#I42A6V@Gitee）\n* 【core   】     MultipartRequestInputStream改为使用long以支持大文件（issue#I428AN@Gitee）\n* 【core   】     RobotUtil增加getDelay、getRobot方法（pr#1725@Github）\n* 【json   】     JSON输出支持ignoreNull（issue#1728@Github）\n* 【core   】     DateUtil和LocalDateTimeUtil增加isWeekend方法（issue#I42N5A@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复RobotUtil双击右键问题（pr#1721@Github）\n* 【core   】     修复FileTypeUtil判断wps修改过的xlsx误判为jar的问题（pr#380@Gitee）\n* 【core   】     修复Sftp.isDir异常bug（pr#378@Gitee）\n* 【core   】     修复BeanUtil.copyProperties集合元素复制成功，读取失败的问题（issue#I41WKP@Gitee）\n* 【core   】     修复NumberChineseFormatter.chineseToNumber十位数错误（issue#1726@github）\n* 【poi    】     修复BeanSheetReader.read中字段对象为空导致的报错（issue#1729@Github）\n* 【core   】     修复DateConverter转换java.sql.Date问题（issue#1729@Github）\n* 【extra  】     修复CompressUtil中部分方法非static的问题（pr#385@Gitee）\n* 【core   】     修复ByteUtil转换端序错误问题（pr#384@Gitee）\n* 【core   】     修复UserAgentUtil判断浏览器顺序问题（issue#I42LYW@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.5 (2021-07-19)\n\n### 🐣新特性\n* 【core   】     DateUtil增加ceiling重载，可选是否归零毫秒\n* 【core   】     IterUtil增加firstMatch方法\n* 【core   】     增加NanoId\n* 【core   】     MapBuilder增加put方法（pr#367@Gitee）\n* 【core   】     StrUtil.insert支持负数index\n* 【core   】     Calculator类支持取模运算（issue#I40DUW@Gitee）\n* 【core   】     增加Base64.isBase64方法（issue#1710@Github）\n* 【core   】     ManifestUtil新增方法getManifest(Class<?> cls)（pr#370@Gitee）\n* 【extra  】     AbstractFtp增加isDir方法（issue#1716@Github）\n* 【core   】     修改FileUtil异常信息内容（pr#1713@Github）\n\n### 🐞Bug修复\n* 【core   】     修复FileUtil.normalize处理上级路径的问题（issue#I3YPEH@Gitee）\n* 【core   】     修复ClassScanner扫描空包遗漏问题\n* 【core   】     修复FastDatePrinter歧义问题（pr#366@Gitee）\n* 【core   】     修复DateUtil.format格式化Instant报错问题（issue#I40CY2@Gitee）\n* 【core   】     修复StrUtil.toUnderlineCase大写问题（issue#I40CGS@Gitee）\n* 【jwt    】     修复JWT.validate报错问题（issue#I40MR2@Gitee）\n* 【core   】     修复StrUtil.brief越界问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.4 (2021-07-10)\n\n### 🐣新特性\n* 【crypto 】     SmUtil.sm4统一返回类型（issue#I3YKD4@Gitee）\n* 【core   】     修改MapUtil.get传入null返回默认值而非null（issue#I3YKBC@Gitee）\n* 【core   】     HexUtil增加hexToLong、hexToInt（issue#I3YQEV@Gitee）\n* 【core   】     CsvWriter增加writer.write(csvData)的方法重载（pr#353@Gitee）\n* 【core   】     新增AbsCollValueMap（issue#I3YXF0@Gitee）\n* 【crypto 】     HOTP缓存改为8位，新增方法（pr#356@Gitee）\n* 【setting】     Props增加toProperties方法（issue#1701@Github）\n* 【http   】     UserAgent增加getOsVersion方法（issue#I3YZUQ@Gitee）\n* 【jwt    】     JWT增加validate方法（issue#I3YDM4@Gitee）\n* 【core   】     CscReader支持指定读取开始行号和结束行号（issue#I3ZMZL@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复RadixUtil.decode非static问题（issue#I3YPEH@Gitee）\n* 【core   】     修复EqualsBuilder数组判断问题（pr#1694@Github）\n* 【setting】     修复Props中Charset对象无法序列化的问题（pr#1694@Github）\n* 【db     】     修复PageResult首页判断逻辑问题（issue#1699@Github）\n* 【core   】     修复IdcardUtil可能数组越界问题（pr#1702@Github）\n* 【core   】     修复FastByteArrayOutputStream索引越界问题（issue#I402ZP@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.3 (2021-06-29)\n\n### 🐣新特性\n* 【core   】     增加Convert.toSet方法（issue#I3XFG2@Gitee）\n* 【core   】     CsvWriter增加writeBeans方法（pr#345@Gitee）\n* 【core   】     新增JAXBUtil（pr#346@Gitee）\n* 【poi    】     ExcelWriter新增setColumnStyleIfHasData和setRowStyleIfHasData（pr#347@Gitee）\n* 【json   】     用户自定义日期时间格式时，解析也读取此格式\n* 【core   】     增加可自定义日期格式GlobalCustomFormat\n* 【jwt    】     JWT修改默认有序，并规定payload日期格式为秒数\n* 【json   】     增加JSONWriter\n* 【core   】     IdUtil增加getWorkerId和getDataCenterId（issueI3Y5NI@Gitee）\n* 【core   】     JWTValidator增加leeway重载\n* 【core   】     增加RegexPool（issue#I3W9ZF@Gitee）\n\n### 🐞Bug修复\n* 【json   】     修复XML转义字符的问题（issue#I3XH09@Gitee）\n* 【core   】     修复FormatCache中循环引用异常（pr#1673@Github）\n* 【core   】     修复IdcardUtil.getIdcardInfo.getProvinceCode获取为汉字的问题（issue#I3XP4Q@Gitee）\n* 【core   】     修复CollUtil.subtract使用非标准Set等空指针问题（issue#I3XN1Z@Gitee）\n* 【core   】     修复SqlFormatter部分SQL空指针问题（issue#I3XS44@Gitee）\n* 【core   】     修复DateRange计算问题（issue#I3Y1US@Gitee）\n* 【core   】     修复BeanCopier中setFieldNameEditor失效问题（pr#349@Gitee）\n* 【core   】     修复ArrayUtil.indexOfSub查找bug（issue#1683@Github）\n* 【core   】     修复Node的权重比较空指针问题（issue#1681@Github）\n* 【core   】     修复UrlQuery传入无参数路径解析问题（issue#1688@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.2 (2021-06-20)\n\n### 🐣新特性\n* 【core   】     增加UserPassAuthenticator\n* 【db     】     获取分组数据源时，移除公共属性项\n* 【core   】     增加StrJoiner\n* 【core   】     增加TreeBuilder\n* 【core   】     IterUtil增加getFirstNonNull方法\n* 【core   】     NumberUtil判空改为isBlank（issue#1664@Github）\n* 【jwt    】     增加JWTValidator、RegisteredPayload\n* 【db     】     增加Phoenix方言（issue#1656@Github）\n\n### 🐞Bug修复\n* 【db     】     修复Oracle下别名错误造成的SQL语法啊错误（issue#I3VTQW@Gitee）\n* 【core   】     修复ConcurrencyTester重复使用时开始测试未清空之前任务的问题（issue#I3VSDO@Gitee）\n* 【poi    】     修复使用BigWriter写出，ExcelWriter修改单元格值失败的问题（issue#I3VSDO@Gitee）\n* 【jwt    】     修复Hmac算法下生成签名是hex的问题（issue#I3W6IP@Gitee）\n* 【core   】     修复TreeUtil.build中deep失效问题（issue#1661@Github）\n* 【json   】     修复XmlUtil.xmlToBean判断问题（issue#1663@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.1 (2021-06-16)\n\n### 🐣新特性\n* 【db     】     NamedSql支持in操作(issue#1652@Github)\n* 【all    】     JWT模块加入到all和bom包中(issue#1654@Github)\n* 【core   】     CollUtil删除所有Map相关操作\n* 【all    】     **重要！** 删除过期方法\n* 【core   】     增加IterChian类\n\n### 🐞Bug修复\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.7.0 (2021-06-15)\n\n### 🐣新特性\n* 【jwt    】     添加JWT模块，实现了JWT的创建、解析和验证\n* 【crypto 】     SymmetricCrypto增加update方法（pr#1642@Github）\n* 【crypto 】     MacEngine增加接口update,doFinal,reset等接口\n* 【core   】     StrSpliter更名为StrSplitter\n* 【core   】     NumberUtil的decimalFormat增加数字检查\n* 【http   】     HttpBase的httpVersion方法设置为无效(issue#1644@Github)\n* 【extra  】     Sftp增加download重载(issue#I3VBSL@Gitee)\n* 【cache  】     修改FIFOCache初始大小(issue#1647@Github)\n\n### 🐞Bug修复\n* 【db     】     修复count方法丢失参数问题(issue#I3VBSL@Gitee)\n* 【db     】     修复SpringUtil工具在`@PostConstruct` 注解标注的方法下失效问题(pr#341@Gitee)\n* 【json   】     修复JSONUtil.parse方法未判断有序问题(issue#I3VHVY@Gitee)\n* 【json   】     修复JSONArray.put越界无法加入问题(issue#I3VMLU@Gitee)\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.7 (2021-06-08)\n\n### 🐣新特性\n* 【core   】     CharSequenceUtil增加join重载（issue#I3TFJ5@Gitee）\n* 【http   】     HttpRequest增加form方法重载（pr#337@Gitee）\n* 【http   】     ImgUtil增加getMainColor方法（pr#338@Gitee）\n* 【core   】     改进TreeUtil.buid算法性能（pr#1594@Github）\n* 【core   】     CsvConfig的setXXX返回this（issue#I3UIQF@Gitee）\n* 【all    】     增加jmh基准测试\n* 【core   】     增加StreamUtil和CollectorUtil\n* 【poi    】     增加content-type(pr#1639@Github)\n\n### 🐞Bug修复\n* 【core   】     修复FileUtil.normalize去掉末尾空格问题（issue#1603@Github）\n* 【core   】     修复CharsetDetector流关闭问题（issue#1603@Github）\n* 【core   】     修复RuntimeUtil.exec引号内空格被切分的问题（issue#I3UAYB@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.6 (2021-05-26)\n\n### 🐣新特性\n* 【cron   】     增加时间轮简单实现\n* 【core   】     BeanUtil.copyToList增加重载（pr#321@Gitee）\n* 【core   】     SyncFinisher增加stop方法（issue#1578@Github）\n* 【cache  】     CacheObj默认方法改为protected（issue#I3RIEI@Gitee）\n* 【core   】     FileUtil.isEmpty不存在时返回true（issue#1582@Github）\n* 【core   】     PhoneUtil增加中国澳门和中国台湾手机号校检方法（pr#331@Gitee）\n* 【db     】     分页查询，自定义sql查询，添加参数（pr#332@Gitee）\n* 【core   】     IdCardUtil.isValidCard增加非空判断\n* 【json   】     JSONObject构造增加SortedMap判断（pr#333@Gitee）\n* 【core   】     Tuple增加部分方法（pr#333@Gitee）\n* 【log    】     增加LogTube支持\n* 【core   】     增加BitStatusUtil（pr#1600@Github）\n\n### 🐞Bug修复\n* 【core   】     修复XmlUtil中omitXmlDeclaration参数无效问题（issue#1581@Github）\n* 【core   】     修复NumberUtil.decimalFormat参数传错的问题（issue#I3SDS3@Gitee）\n* 【json   】     修复JSONArray.put方法不能覆盖值的问题\n* 【poi    】     修复sax方式读取xls无法根据sheet名称获取数据（issue#I3S4NH@Gitee）\n* 【core   】     修复路径中多个~都被替换的问题（pr#1599@Github）\n* 【core   】     修复CRC16构造非public问题（issue#1601@Github）\n\n-------------------------------------------------------------------------------------------------------------\n# 5.6.5 (2021-05-08)\n\n### 🐣新特性\n* 【http   】     HttpUtil增加closeCookie方法\n* 【core   】     NumberUtil增加方法decimalFormat重载（issue#I3OSA2@Gitee）\n* 【extra  】     Ftp的remoteVerificationEnabled改为false（issue#I3OSA2@Gitee）\n* 【core   】     MaskBit增加掩码反向转换的方法getMaskBit()（pr#1563@Github）\n* 【core   】     ReUtil等增加indexOf、delLast等方法（pr#1555@Github）\n* 【poi    】     ExcelWriter增加writeSecHeadRow，增加合并单元格边框颜色样式（pr#318@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复createScheduledExecutor单位不是毫秒的问题（issue#I3OYIW@Gitee）\n* 【core   】     修复Tailer无stop问题（issue#I3PQLQ@Gitee）\n* 【core   】     修复空白excel读取报错问题（issue#1552@Github）\n* 【extra  】     修复Sftp.mkDirs报错问题（issue#1536@Github）\n* 【core   】     修复Bcrypt不支持$2y$盐前缀问题（pr#1560@Github）\n* 【system 】     修复isWindows8拼写问题（pr#1557@Github）\n* 【db     】     修复MongoDS默认分组参数失效问题（issue#1548@Github）\n* 【core   】     修复UrlPath编码的字符问题导致的URL编码异常（issue#1537@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.4 (2021-04-25)\n\n### 🐣新特性\n* 【core   】     DatePattern补充DateTimeFormatter（pr#308@Gitee）\n* 【core   】     DateUtil.compare增加支持给定格式比较（pr#310@Gitee）\n* 【core   】     BeanUtil增加edit方法（issue#I3J6BG@Gitee）\n* 【db     】     Column中加入columnDef字段默认值（issue#I3J6BG@Gitee）\n* 【core   】     BeanUtil增加copyToList方法（issue#1526@Github）\n* 【extra  】     MailAccount增加customProperty可以用户自定义属性（pr#317@Gitee）\n* 【system 】     SystemUtil.getUserInfo()中所有平台路径统一末尾加/（issue#I3NM39@Gitee）\n* 【http   】     新增HttpDownloader，默认开启自动跳转（issue#I3NM39@Gitee）\n\n### 🐞Bug修复\n* 【db     】     修复SQL分页时未使用别名导致的错误，同时count时取消order by子句（issue#I3IJ8X@Gitee）\n* 【extra  】     修复Sftp.reconnectIfTimeout方法判断错误（issue#1524@Github）\n* 【core   】     修复NumberChineseFormatter转数字问题（issue#I3IS3S@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.3 (2021-04-10)\n\n### 🐣新特性\n* 【core   】     修改数字转换的实现，增加按照指定端序转换（pr#1492@Github）\n* 【core   】     修改拆分byte数组时最后一组长度的规则（pr#1494@Github）\n* 【core   】     新增根据日期获取节气（pr#1496@Github）\n* 【core   】     mapToBean()添加对布尔值is前缀的识别（pr#294@Gitee）\n* 【core   】     农历十月十一月改为寒月和冬月（pr#301@Gitee）\n* 【core   】     增加港澳台电话正则（pr#301@Gitee）\n* 【core   】     增加银行卡号脱敏（pr#301@Gitee）\n* 【cache  】     使用LongAddr代替AtomicLong（pr#301@Gitee）\n* 【cache  】     EnumUtil使用LinkedHashMap（pr#304@Gitee）\n* 【crypto 】     SymmetricCrypto支持大量数据加密解密（pr#1497@Gitee）\n* 【http   】     SoapClient增加针对不同协议的头信息（pr#305@Gitee）\n* 【http   】     HttpRequest支持307、308状态码识别（issue#1504@Github）\n* 【core   】     CharUtil.isBlankChar增加\\u0000判断（pr#1505@Github）\n* 【extra  】     添加Houbb Pinyin支持（pr#1506@Github）\n* 【core   】     添加LambdaUtil（pr#295@Gitee）\n* 【core   】     添加StrPool和CharPool\n* 【extra  】     CglibUtil增加toBean和fillBean方法\n* 【db     】     增加DriverNamePool\n\n### 🐞Bug修复\n* 【core   】     修复Validator.isUrl()传空返回true（issue#I3ETTY@Gitee）\n* 【db     】     修复数据库driver根据url的判断识别错误问题（issue#I3EWBI@Gitee）\n* 【json   】     修复JSONStrFormatter换行多余空行问题（issue#I3FA8B@Gitee）\n* 【core   】     修复UrlPath中的+被转义为空格%20的问题（issue#1501@Github）\n* 【core   】     修复DateUtil.parse方法对UTC时间毫秒少于3位不识别问题（issue#1503@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.2 (2021-03-28)\n\n### 🐣新特性\n* 【core   】     Validator增加车架号(车辆识别码)验证、驾驶证（驾驶证档案编号）的正则校验（pr#280@Gitee）\n* 【core   】     CopyOptions增加propertiesFilter（pr#281@Gitee）\n* 【extra  】     增加Wit模板引擎支持\n* 【core   】     增加DesensitizedUtil（pr#282@Gitee）\n* 【core   】     增加DateTime字符串构造（issue#I3CQZG@Gitee）\n* 【core   】     修改ArrayUtil代码风格（pr#287@Gitee）\n* 【json   】     JSONConfig增加setStripTrailingZeros配置（issue#I3DJI8@Gitee）\n* 【db     】     升级兼容BeeCP3.x\n\n### 🐞Bug修复\n* 【core   】     修复FileTypeUtil中OFD格式判断问题（pr#1489@Github）\n* 【core   】     修复CamelCaseLinkedMap和CaseInsensitiveLinkedMap的Linked失效问题（pr#1490@Github）\n* 【core   】     修复UrlPath中=被转义的问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.1 (2021-03-18)\n\n### 🐣新特性\n* 【crypto 】     SecureUtil去除final修饰符（issue#1474@Github）\n* 【core   】     IoUtil增加lineIter方法\n* 【core   】     新增函数式懒加载加载器(pr#275@Gitee)\n* 【http   】     UserAgentUtil增加miniProgram判断(issue#1475@Github)\n* 【db     】     增加Ignite数据库驱动识别\n* 【core   】     DateUtil.parse支持带毫秒的UTC时间\n* 【core   】     IdcardUtil.Idcard增加toString（pr#1487@Github）\n* 【core   】     ChineseDate增加getGregorianXXX方法（issue#1481@Github）\n\n### 🐞Bug修复\n* 【core   】     修复IoUtil.readBytes的FileInputStream中isClose参数失效问题（issue#I3B7UD@Gitee）\n* 【core   】     修复DataUnit中KB不大写的问题\n* 【json   】     修复JSONUtil.getByPath类型错误问题（issue#I3BSDF@Gitee）\n* 【core   】     修复BeanUtil.toBean提供null未返回null的问题（issue#I3BQPV@Gitee）\n* 【core   】     修复ModifierUtil#modifiersToInt中逻辑判断问题（issue#1486@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.6.0 (2021-03-12)\n\n### 🐣新特性\n* 【poi    】     重要：不再兼容POI-3.x，增加兼容POI-5.x（issue#I35J6B@Gitee）\n* 【core   】     FileTypeUtil使用长匹配优先（pr#1457@Github）\n* 【core   】     IterUtil和CollUtil增加isEqualList方法（issue#I3A3PY@Gitee）\n* 【crypto 】     增加PBKDF2（issue#1416@Github）\n* 【core   】     增加FuncKeyMap（issue#1402@Github）\n* 【core   】     增加StrMatcher（issue#1379@Github）\n* 【core   】     NumberUtil增加factorial针对BigInterger方法（issue#1379@Github）\n* 【core   】     TreeNode增加equals方法（issue#1467@Github）\n* 【core   】     增加汉字转阿拉伯数字Convert.chineseToNumber（pr#1469@Github）\n* 【json   】     JSONUtil增加getByPath方法支持默认值（issue#1470@Github）\n* 【crypto 】     SecureUtil增加hmacSha256方法（pr#1473@Github）\n* 【core   】     FileTypeUtil判断流增加文件名辅助判断（pr#1471@Github）\n\n### 🐞Bug修复\n* 【socket 】     修复Client创建失败资源未释放问题。\n* 【core   】     修复DataSizeUtil中EB单位错误问题（issue#I39O7I@Gitee）\n* 【core   】     修复BeanDesc.isMatchSetter的ignoreCase未使用问题（issue#I3AXIJ@Gitee）\n* 【core   】     修复CRC16Checksum中（issue#I3AXIJ@Gitee）\n* 【core   】     修复UrlQuery中对空key解析丢失问题（issue#I3B3J6@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.9 (2021-02-26)\n\n### 🐣新特性\n* 【crypto 】     PemUtil.readPemKey支持EC（pr#1366@Github）\n* 【extra  】     Ftp等cd方法增加同步（issue#1397@Github）\n* 【core   】     StrUtil增加endWithAnyIgnoreCase（issue#I37I0B@Gitee）\n* 【crypto 】     Sm2增加getD和getQ方法（issue#I37Z4C@Gitee）\n* 【cache  】     AbstractCache增加keySet方法（issue#I37Z4C@Gitee）\n* 【core   】     NumberWordFormatter增加formatSimple方法（pr#1436@Github）\n* 【crypto 】     增加读取openSSL生成的sm2私钥\n* 【crypto 】     增加众多方法，SM2兼容各类密钥格式（issue#I37Z75@Gitee）\n\n### 🐞Bug修复\n* 【json   】     JSONUtil.isJson方法改变trim策略，解决特殊空白符导致判断失败问题\n* 【json   】     修复SQLEXception导致的栈溢出（issue#1399@Github）\n* 【extra  】     修复Ftp中异常参数没有传入问题（issue#1397@Github）\n* 【crypto 】     修复Sm2使用D构造空指针问题（issue#I37Z4C@Gitee）\n* 【poi    】     修复ExcelPicUtil中图表报错问题（issue#I38857@Gitee）\n* 【core   】     修复ListUtil.page方法返回空列表无法编辑问题（issue#1415@Github）\n* 【core   】     修复ListUtil.sub中step不通结果不一致问题（issue#1409@Github）\n* 【db     】     修复Condition转换参数值时未转换数字异常（issue#I38LTM@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.8 (2021-01-30)\n\n### 🐣新特性\n* 【extra  】     增加自动装配SpringUtil类（pr#1366@Github）\n* 【extra  】     ArrayUtil增加map方法重载\n* 【crypto 】     AsymmetricAlgorithm增加RSA_ECB(\"RSA/ECB/NoPadding\")（issue#1368@Github）\n* 【core   】     补充StrUtil.padXXX注释（issue#I2E1S7@Gitee）\n* 【core   】     修改上传文件检查逻辑\n* 【core   】     修正LocalDateTimeUtil.offset方法注释问题（issue#I2EEXC@Gitee）\n* 【extra  】     VelocityEngine的getRowEngine改为getRawEngine（issue#I2EGRG@Gitee）\n* 【cache  】     缓存降低锁的粒度，提高并发能力（pr#1385@Github）\n* 【core   】     SimpleCache缓存降低锁的粒度，提高并发能力（pr#1385@Github）\n* 【core   】     增加RadixUtil（pr#260@Gitee）\n* 【core   】     BeanUtil.getFieldValue支持获取字段集合（pr#254@Gitee）\n* 【core   】     DateConvert转换失败默认抛出异常（issue#I2M5GN@Gitee）\n* 【http   】     HttpServerRequest增加getParam方法\n* 【http   】     RootAction增加可选name参数，返回指定文件名称\n* 【db     】     支持人大金仓8的驱动识别\n* 【db     】     ThreadUtil增加createScheduledExecutor和schedule方法（issue#I2NUTC@Gitee）\n* 【core   】     ImgUtil增加getImage方法（issue#I2DU1Z@Gitee）\n* 【core   】     DateUtil.beginOfHour（pr#269@Gitee）\n* 【core   】     MapUtil增加sortByValue（pr#259@Gitee）\n* 【core   】     TypeUtil修正hasTypeVeriable为hasTypeVariable\n* 【core   】     RandomUtil.getRandom改为new SecureRandom，避免阻塞\n\n### 🐞Bug修复\n* 【core   】     修复FileUtil.move以及PathUtil.copy等无法自动创建父目录的问题（issue#I2CKTI@Gitee）\n* 【core   】     修复Console.input读取不全问题（pr#263@Gitee）\n* 【core   】     修复URLUtil.encodeAll未检查空指针问题（issue#I2CNPS@Gitee）\n* 【core   】     修复UrlBuilder.of的query中含有?丢失问题（issue#I2CNPS@Gitee）\n* 【crypto 】     修复BCrypt.checkpw报错问题（issue#1377@Github）\n* 【extra  】     修复Fftp中cd失败导致的问题（issue#1371@Github）\n* 【poi    】     修复ExcelWriter.merge注释问题（issue#I2DNPG@Gitee）\n* 【core   】     修复CsvReader读取注释行错误问题（issue#I2D87I@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.7 (2021-01-07)\n\n### 🐣新特性\n* 【core   】     DynaBean.create增加重载方法（pr#245@Gitee）\n* 【core   】     IdcardUtil增加重载是否忽略大小写（issue#1348@Github）\n* 【poi    】     SheetRidReader增加getRidByIndex方法（issue#1342@Github）\n* 【extra  】     MailAccount增加sslProtocols配置项（issue#IZN95@Gitee）\n* 【extra  】     MailUtil增加getSession方法\n* 【setting】     新增setByGroup和putByGroup，set和put标记为过期（issue#I2C42H@Gitee）\n* 【crypto 】     修改SymmetricAlgorithm注释（issue#1360@Github）\n* 【all    】     pom中将META-INF/maven下全部exclude（pr#1355@Github）\n* 【http   】     SimpleServer中增加addFilter等方法，并使用全局线程池\n* 【core   】     CollUtil.forEach 增加null 判断（pr#250@Gitee）\n* 【extra  】     FtpConfig增加serverLanguageCode和systemKey配置,Ftp.download增加重载（pr#248@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复CsvReader读取双引号未转义问题（issue#I2BMP1@Gitee）\n* 【json   】     JSONUtil.parse修复config无效问题（issue#1363@Github）\n* 【http   】     修复SimpleServer返回响应内容Content-Length不正确的问题（issue#1358@Github）\n* 【http   】     修复Https请求部分环境下报证书验证异常问题（issue#I2C1BZ@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.6 (2020-12-29)\n\n### 🐣新特性\n* 【core   】     手机号工具类 座机正则表达式统一管理（pr#243@Gitee）\n* 【extra  】     Mail增加setDebugOutput方法（issue#1335@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复ZipUtil.unzip从流解压关闭问题（issue#I2B0S1@Gitee）\n* 【poi    】     修复Excel07Writer写出表格错乱问题（issue#I2B57B@Gitee）\n* 【poi    】     修复SheetRidReader读取字段错误问题（issue#1342@Github）\n* 【core   】     修复FileUtil.getMimeType不支持css和js（issue#1341@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.5 (2020-12-27)\n\n### 🐣新特性\n* 【core   】     URLUtil.normalize新增重载（pr#233@Gitee）\n* 【core   】     PathUtil增加isSub和toAbsNormal方法\n* 【db     】     RedisDS实现序列化接口（pr#1323@Github）\n* 【poi    】     StyleUtil增加getFormat方法（pr#235@Gitee）\n* 【poi    】     增加ExcelDateUtil更多日期格式支持（issue#1316@Github）\n* 【core   】     NumberUtil.toBigDecimal支持各类数字格式，如1,234.56等（issue#1334@Github）\n* 【core   】     NumberUtil增加parseXXX方法（issue#1334@Github）\n* 【poi    】     Excel07SaxReader支持通过sheetName读取（issue#I2AOSE@Gitee）\n\n### 🐞Bug修复\n* 【core   】     FileUtil.isSub相对路径判断问题（pr#1315@Github）\n* 【core   】     TreeUtil增加空判定（issue#I2ACCW@Gitee）\n* 【db     】     解决Hive获取表名失败问题（issue#I2AGLU@Gitee）\n* 【core   】     修复DateUtil.parse未使用严格模式导致结果不正常的问题（issue#1332@Github）\n* 【core   】     修复RuntimeUtil.getUsableMemory非static问题（issue#I2AQ2M@Gitee）\n* 【core   】     修复ArrayUtil.equals方法严格判断问题（issue#I2AO8B@Gitee）\n* 【poi    】     修复SheetRidReader在获取rid时读取错误问题（issue#I2AOQW@Gitee）\n* 【core   】     修复强依赖了POI的问题（issue#1336@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.4 (2020-12-16)\n\n### 🐣新特性\n### 🐞Bug修复\n* 【core   】     修复IoUtil.readBytes的问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.3 (2020-12-11)\n\n### 🐣新特性\n* 【core   】     IdcardUtil增加行政区划83（issue#1277@Github）\n* 【core   】     multipart中int改为long，解决大文件上传越界问题（issue#I27WZ3@Gitee）\n* 【core   】     ListUtil.page增加检查（pr#224@Gitee）\n* 【db     】     Db增加使用sql的page方法（issue#247@Gitee）\n* 【cache  】     CacheObj的isExpired()逻辑修改（issue#1295@Github）\n* 【json   】     JSONStrFormater改为JSONStrFormatter\n* 【dfa    】     增加FoundWord（pr#1290@Github）\n* 【core   】     增加Segment（pr#1290@Github）\n* 【core   】     增加CharSequenceUtil\n* 【poi    】     Excel07SaxReader拆分出SheetDataSaxHandler\n* 【core   】     CollUtil.addAll增加判空（pr#228@Gitee）\n* 【core   】     修正DateUtil.betweenXXX注释错误（issue#I28XGW@Gitee）\n* 【core   】     增加NioUtil\n* 【core   】     增加GanymedUtil\n* 【poi    】     增加OFD支持，OfdWriter\n* 【poi    】     修复NumberUtil属性拼写错误（pr#1311@Github）\n* 【core   】     MapUtil增加getQuietly方法（issue#I29IWO@Gitee）\n\n### 🐞Bug修复\n* 【cache  】     修复Cache中get重复misCount计数问题（issue#1281@Github）\n* 【poi    】     修复sax读取自定义格式单元格无法识别日期类型的问题（issue#1283@Github）\n* 【core   】     修复CollUtil.get越界问题（issue#1292@Github）\n* 【core   】     修复TemporalAccessorUtil无法格式化LocalDate带时间问题（issue#1289@Github）\n* 【json   】     修复自定义日期格式的LocalDateTime没有包装引号问题（issue#1289@Github）\n* 【cache  】     get中unlock改为unlockRead（issue#1294@Github）\n* 【db     】     修复表名包含点导致的问题（issue#1300@Github）\n* 【poi    】     修复xdr:row标签导致的问题（issue#1297@Github）\n* 【core   】     修复FileUtil.loopFiles使用FileFilter无效问题（issue#I28V48@Gitee）\n* 【extra  】     修复JschUtil.execByShell返回空的问题（issue#1067@Github）\n* 【poi    】     修复特殊的excel使用sax读取时未读到值的问题（issue#1303@Github）\n* 【http   】     修复HttpUtil类条件判断错误（pr#232@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.2 (2020-12-01)\n\n### 🐣新特性\n* 【crypto 】     KeyUtil增加重载，AES构造增加重载（issue#I25NNZ@Gitee）\n* 【json   】     JSONUtil增加toList重载（issue#1228@Github）\n* 【core   】     新增CollStreamUtil（issue#1228@Github）\n* 【extra  】     新增Rhino表达式执行引擎（pr#1229@Github）\n* 【crypto 】     增加判空（issue#1230@Github）\n* 【core   】     xml.setXmlStandalone(true)格式优化（pr#1234@Github）\n* 【core   】     AnnotationUtil增加setValue方法（pr#1250@Github）\n* 【core   】     ZipUtil增加get方法（issue#I27CUF@Gitee）\n* 【cache  】     对CacheObj等变量使用volatile关键字\n* 【core   】     Base64增加encodeWithoutPadding方法（issue#I26J16@Gitee）\n* 【core   】     ExceptionUtil增加message消息包装为运行时异常的方法（pr#1253@Gitee）\n* 【core   】     DatePattern增加年月格式化常量（pr#220@Gitee）\n* 【core   】     ArrayUtil增加shuffle方法（pr#1255@Github）\n* 【core   】     ArrayUtil部分方法分离至PrimitiveArrayUtil\n* 【crypto 】     opt改为otp包（issue#1257@Github）\n* 【cache  】     增加CacheListener（issue#1257@Github）\n* 【core   】     TimeInterval支持分组（issue#1238@Github）\n* 【core   】     增加compile包（pr#1243@Github）\n* 【core   】     增加ResourceClassLoader、CharSequenceResource、FileObjectResource\n* 【core   】     修改IoUtil.read(Reader)逻辑默认关闭Reader\n* 【core   】     ZipUtil增加Zip方法（pr#222@Gitee）\n* 【all    】     增加Hutool.getAllUtils和printAllUtils方法\n* 【core   】     增加PunyCode（issue#1268@Gitee）\n* 【core   】     ArrayUtil增加isSorted方法（pr#1271@Github）\n* 【captcha】     增加GifCaptcha（pr#1273@Github）\n* 【core   】     增加SSLUtil、SSLContextBuilder\n\n### 🐞Bug修复\n* 【cron   】     修复CronTimer可能死循环的问题（issue#1224@Github）\n* 【core   】     修复Calculator.conversion单个数字越界问题（issue#1222@Github）\n* 【poi    】     修复ExcelUtil.getSaxReader使用非MarkSupport流报错问题（issue#1225@Github）\n* 【core   】     修复HexUtil.format问题（issue#I268XT@Gitee）\n* 【core   】     修复ZipUtil判断压缩文件是否位于压缩目录内的逻辑有误的问题（issue#1251@Github）\n* 【json   】     修复JSONObject.accumulate问题\n* 【poi    】     修复部分xlsx文件sax方式解析空指针问题（issue#1265@Github）\n* 【core   】     修复PatternPool中邮编的正则（issue#1274@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.1 (2020-11-16)\n\n### 🐣新特性\n* 【core   】     增加CopyVisitor和DelVisitor\n\n### 🐞Bug修复\n* 【core   】     修复在Linux下FileUtil.move失败问题（issue#I254Y3@Gitee）\n* 【http   】     修复UrlUtil和UrlBuilder中多个/被替换问题（issue#I25MZL@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.5.0 (2020-11-14)\n\n### 大版本特性\n* 【extra  】     增加jakarta.validation-api封装：ValidationUtil（pr#207@Gitee）\n* 【extra  】     增加表达式引擎封装：ExpressionUtil（pr#1203@Github）\n* 【extra  】     新增基于Apache-FtpServer封装：SimpleFtpServer\n* 【extra  】     新增基于Commons-Compress封装：CompressUtil\n\n### 🐣新特性\n* 【core   】     NumberUtil.parseInt等支持123,2.00这类数字（issue#I23ORQ@Gitee）\n* 【core   】     增加ArrayUtil.isSub、indexOfSub、lastIndexOfSub方法（issue#I23O1K@Gitee）\n* 【core   】     反射调用支持传递参数的值为null（pr#1205@Github）\n* 【core   】     HexUtil增加format方法（issue#I245NF@Gitee）\n* 【poi    】     ExcelWriter增加setCurrentRowToEnd方法（issue#I24A2R@Gitee）\n* 【core   】     ExcelWriter增加setCurrentRowToEnd方法（issue#I24A2R@Gitee）\n* 【core   】     增加enum转数字支持（issue#I24QZY@Gitee）\n* 【core   】     NumberUtil.toBigDecimal空白符转换为0（issue#I24MRP@Gitee）\n* 【core   】     CollUtil和IterUtil增加size方法（pr#208@Gitee）\n* 【poi    】     ExcelReader的read方法读取空单元格增加CellEditor处理（issue#1213@Github）\n\n### 🐞Bug修复\n* 【core   】     修复DateUtil.current使用System.nanoTime的问题（issue#1198@Github）\n* 【core   】     修复Excel03SaxReader判断日期出错问题（issue#I23M9H@Gitee）\n* 【core   】     修复ClassUtil.getTypeArgument方法在判断泛型时导致的问题（issue#1207@Github）\n* 【core   】     修复Ipv4Util分隔符问题（issue#I24A9I@Gitee）\n* 【core   】     修复Ipv4Util.longToIp的问题\n* 【poi    】     修复Excel07SaxReader读取公式的错误的问题（issue#I23VFL@Gitee）\n* 【http   】     修复HttpUtil.isHttp判断问题（pr#1208@Github）\n* 【http   】     修复Snowflake时间回拨导致ID重复的bug（issue#1206@Github）\n* 【core   】     修复StrUtil.lastIndexOf查找位于首位的字符串找不到的bug（issue#I24RSV@Gitee）\n* 【poi    】     修复BigExcelWriter的autoSizeColumnAll问题（pr#1221@Github）\n* 【core   】     修复StrUtil.subBetweenAll不支持相同字符的问题（pr#1217@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.7 (2020-10-31)\n\n### 🐣新特性\n* 【core   】     增加OptionalBean（pr#1182@Github）\n* 【core   】     Ganzhi增加方法（issue#1186@Github）\n* 【core   】     CollUtil增加forEach重载（issue#I22NA4@Gitee）\n* 【core   】     CollUtil.map忽略空值改规则为原数组中的元素和处理后的元素都会忽略空值（issue#I22N08@Gitee）\n* 【http   】     增加SoapClient增加addSOAPHeader重载\n* 【http   】     ArrayUtil增加containsAll方法\n* 【core   】     增加CharsetDetector\n* 【cron   】     增加CronTask，监听支持获取id（issue#I23315@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复BeanUtil.beanToMap方法中editor返回null没有去掉的问题\n* 【core   】     修复ImgUtil.toBufferedImage颜色模式的问题（issue#1194@Github）\n* 【cron   】     修复TimeZone设置无效的问题（issue#I23315@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.6 (2020-10-23)\n\n### 🐣新特性\n* 【http   】     HttpRequest增加basicProxyAuth方法（issue#I1YQGM@Gitee）\n* 【core   】     NumberUtil.toStr修改逻辑，去掉BigDecimal的科学计数表示（pr#196@Gitee）\n* 【core   】     ListUtil.page第一页页码使用PageUtil（pr#198@Gitee）\n* 【http   】     增加微信、企业微信ua识别（pr#1179@Github）\n* 【core   】     ObjectUtil增加defaultIfXXX（pr#199@Gitee）\n* 【json   】     JSONObject构建时不支持的对象类型抛出异常\n\n### 🐞Bug修复\n* 【core   】     修复ChineseDate没有忽略时分秒导致计算错误问题（issue#I1YW12@Gitee）\n* 【core   】     修复FileUtil中，copyFile方法断言判断参数传递错误（issue#I1Z2NY@Gitee）\n* 【core   】     修复BeanDesc读取父类属性覆盖子类属性导致的问题（pr#1175@Github）\n* 【aop    】     修复SimpleAspect一个重载导致的问题，去掉重载的after方法（issue#I1YUG9@Gitee）\n* 【poi    】     修复03 sax读取日期问题（issue#I1Z83N@Gitee）\n* 【core   】     修复FileUtil.size软链导致的问题（pr#200@Gitee）\n* 【core   】     修复JSONObject构造时传入JSONArray结果出错问题（issue#I22FDS@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.5 (2020-10-18)\n\n### 🐣新特性\n* 【core   】     ConsoleTable代码优化（pr#190@Gitee）\n* 【http   】     HttpRequest增加setProxy重载（pr#190@Gitee）\n* 【core   】     XmlUtil.cleanComment（pr#191@Gitee）\n* 【core   】     ArrayUtil.unWrap增加默认值（pr#1149@Github）\n* 【core   】     ArrayUtil.indexOf修改double的equals判断（pr#1147@Github）\n* 【core   】     优化StrUtil中部分参数校验以及逻辑处理（pr#1144@Github）\n* 【core   】     简化CreditCode逻辑去除无用Character.toUpperCase（pr#1145@Github）\n* 【core   】     NumberUtil增加generateRandomNumber重载，可自定义seed（issue#I1XTUT@Gitee）\n* 【core   】     DataSizeUtil支持小数（pr#1158@Github）\n* 【core   】     完善注释（pr#193@Gitee）\n* 【core   】     优化Combination.countAll（pr#1159@Github）\n* 【core   】     优化针对list的split方法（pr#194@Gitee）\n* 【poi    】     ExcelWriter增加setRowStyle方法\n* 【core   】     Assert增加函数接口（pr#1166@Github）\n* 【core   】     新增AtomicIntegerArray、AtomicLongArray转换\n* 【extra  】     PinyinUtil新增Bopomofo4j支持\n* 【core   】     新增TemporalUtil工具类，新增时间相关方法\n\n### 🐞Bug修复\n* 【core   】     解决农历判断节日未判断大小月导致的问题（issue#I1XHSF@Gitee）\n* 【core   】     解决ListUtil计算总量可能的int溢出问题（pr#1150@Github）\n* 【json   】     解决JSON中转换为double小数精度丢失问题（pr#192@Gitee）\n* 【core   】     修复CaseInsensitiveMap的remove等方法并没有忽略大小写的问题（pr#1163@Gitee）\n* 【poi    】     修复合并单元格值读取错误的问题\n* 【poi    】     修复NamedSql解析形如col::numeric出错问题（issue#I1YHBX@Gitee）\n* 【core   】     修复计算相差天数导致的问题\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.4 (2020-09-28)\n\n### 🐣新特性\n* 【core   】     ServiceLoaderUtil改为使用contextClassLoader（pr#183@Gitee）\n* 【core   】     NetUtil增加getLocalHostName（pr#1103@Github）\n* 【extra  】     FTP增加stat方法（issue#I1W346@Gitee）\n* 【core   】     Convert.toNumber支持类似12.2F这种形式字符串转换（issue#I1VYLJ@Gitee）\n* 【core   】     使用静态变量替换999等（issue#I1W8IB@Gitee）\n* 【core   】     URLUtil自动trim（issue#I1W803@Gitee）\n* 【crypto 】     RC4增加ecrypt（pr#1108@Github）\n* 【core   】     CharUtil and StrUtil增加@（pr#1106@Github）\n* 【extra  】     优化EMOJ查询逻辑（pr#1112@Github）\n* 【extra  】     优化CollUtil交并集结果集合设置初始化大小，避免扩容成本（pr#1110@Github）\n* 【core   】     优化PageUtil彩虹算法（issue#1110@Github）\n* 【core   】     IoUtil增加readUtf8方法\n* 【core   】     优化全局邮箱账户初始化逻辑（pr#1114@Github）\n* 【http   】     SoapClient增加addSOAPHeader方法\n* 【http   】     完善StrUtil的注释（pr#186@Gitee）\n* 【aop    】     去除调试日志（issue#1116@Github）\n* 【core   】     增加&apos;反转义（pr#1121@Github）\n* 【poi    】     增加SheetReader和XXXRowHandler（issue#I1WHJP@Gitee）\n* 【dfa    】     增加过滤符号（pr#1122@Github）\n* 【dfa    】     SensitiveUtil增加setCharFilter方法（pr#1123@Github）\n* 【all    】     优化常量大小写规范（pr#188@Gitee）\n* 【core   】     优化NumberUtil中针对BigDecimal的一些处理逻辑（pr#1127@Github）\n* 【core   】     NumberUtil.factorial注释明确（pr#1126@Github）\n* 【core   】     NumberUtil增加isPowerOfTwo方法（pr#1132@Github）\n* 【core   】     优化BooleanUtil的校验逻辑（pr#1137@Github）\n* 【poi    】     改进sax方式读取逻辑，支持sheetId（issue#1141@Github）\n* 【core   】     XmlUtil增加readBySax方法\n\n### 🐞Bug修复\n* 【crypto 】     修复SM2验签后无法解密问题（issue#I1W0VP@Gitee）\n* 【core   】     修复新建默认TreeSet没有默认比较器导致的问题（issue#1101@Github）\n* 【core   】     修复Linux下使用Windows路径分隔符导致的解压错误（issue#I1MW0E@Gitee）\n* 【core   】     修复Word07Writer写出map问题（issue#I1W49R@Gitee）\n* 【script 】     修复函数库脚本执行问题\n* 【core   】     修复RGB随机颜色的上限值不对且API重复（pr#1136@Gihub）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.3 (2020-09-16)\n\n### 🐣新特性\n* 【core   】     使用静态的of方法来new对象（pr#177@Gitee）\n* 【setting】     Setting增加store无参方法（issue#1072@Github）\n* 【setting】     StatementUtil增加null缓存（pr#1076@Github）\n* 【core   】     扩充Console功能，支持可变参数（issue#1077@Github）\n* 【crypto 】     增加ECKeyUtil（issue#I1UOF5@Gitee）\n* 【core   】     增加TransXXX（issue#I1TU1Y@Gitee）\n* 【core   】     增加Generator\n* 【db     】     Column增加是否主键、保留位数等字段\n* 【cache  】     Cache接口增加get重载（issue#1080@Github）\n* 【core   】     增加Interner和InternUtil（issue#I1TU1Y@Gitee）\n* 【core   】     增加Calculator（issue#1090@Github）\n* 【core   】     IdcardUtil增加getIdcardInfo方法（issue#1092@Github）\n* 【core   】     改进ObjectUtil.equal，支持BigDecimal判断\n* 【core   】     ArrayConverter增加可选是否忽略错误（issue#I1VNYQ@Gitee）\n* 【db     】     增加ConditionBuilder\n* 【setting】     Setting和Props增加create方法\n* 【log    】     增加TinyLog2支持（issue#1094@Github）\n\n### 🐞Bug修复\n* 【core   】     修复Dict.of错误（issue#I1UUO5@Gitee）\n* 【core   】     修复UrlBuilder地址参数问题（issue#I1UWCA@Gitee）\n* 【core   】     修复StrUtil.toSymbolCase转换问题（issue#1075@Github）\n* 【log    】     修复打印null对象显示{msg}异常问题（issue#1084@Github）\n* 【extra  】     修复ServletUtil.getReader中未关闭的问题\n* 【extra  】     修复QrCodeUtil在新版本zxing报错问题（issue#1088@Github）\n* 【core   】     修复LocalDateTimeUtil.parse无法解析yyyyMMddHHmmssSSS的bug（issue#1082@Github）\n* 【core   】     修复VersionComparator.equals递归调用问题（issue#1093@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.2 (2020-09-09)\n\n### 🐣新特性\n* 【core  】     lock放在try外边（pr#1050@Github）\n* 【core  】     MailUtil增加错误信息（issue#I1TAKJ@Gitee）\n* 【core  】     JschUtil添加远程转发功能（pr#171@Gitee）\n* 【db    】     AbstractDb增加executeBatch重载（issue#1053@Github）\n* 【extra 】     新增方便引入SpringUtil的注解@EnableSpringUtil（pr#172@Gitee）\n* 【poi   】     RowUtil增加插入和删除行（pr#1060@Github）\n* 【extra 】     SpringUtil增加注册bean（pr#174@Gitee）\n* 【core  】     修改NetUtil.getMacAddress避免空指针（issue#1057@Github）\n* 【core  】     增加EnumItem接口，枚举扩展转换，增加SPI自定义转换（pr#173@Github）\n* 【core  】     TypeUtil增加getActualType，增加ActualTypeMapperPool类（issue#I1TBWH@Gitee）\n* 【extra 】     QRConfig中添加qrVersion属性（pr#1068@Github）\n* 【core  】     ArrayUtil增加equals方法\n* 【core  】     BeanDesc增加方法\n* 【core  】     增加@PropIgnore注解（issue#I1U846@Gitee）\n\n### 🐞Bug修复\n* 【core  】     重新整理农历节假日，解决一个pr过来的玩笑导致的问题\n* 【poi   】     修复ExcelFileUtil.isXls判断问题（pr#1055@Github）\n* 【poi   】     修复CglibUtil.copyList参数错误导致的问题\n* 【http  】     修复GET请求附带body导致变POST的问题\n* 【core  】     修复double相等判断问题（pr#175@Gitee）\n* 【core  】     修复DateSizeUtil.format越界问题（issue#1069@Github）\n* 【core  】     修复ChineseDate.getChineseMonth问题（issue#I1UG72@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.1 (2020-08-29)\n\n### 🐣新特性\n* 【core  】     StrUtil增加firstNonXXX方法（issue#1020@Github）\n* 【core  】     BeanCopier修改规则，可选bean拷贝空字段报错问题（pr#160@Gitee）\n* 【http  】     HttpUtil增加downloadFileFromUrl（pr#1023@Github）\n* 【core  】     增加toEpochMilli方法\n* 【core  】     Validator修改isCitizenId校验（pr#1032@Github）\n* 【core  】     增加PathUtil和FileNameUtil，分离FileUtil中部分方法\n* 【core  】     改造IndexedComparator，增加InstanceComparator\n* 【extra 】     增加CglibUtil\n* 【core  】     增加Ipv4Util（pr#161@Gitee）\n* 【core  】     增加CalendarUtil和DateUtil增加isSameMonth方法（pr#161@Gitee）\n* 【core  】     Dict增加of方法（issue#1035@Github）\n* 【core  】     StrUtil.wrapAll方法不明确修改改为wrapAllWithPair（issue#1042@Github）\n* 【core  】     EnumUtil.getEnumAt负数返回null（pr#167@Gitee）\n* 【core  】     ChineseDate增加天干地支和转换为公历方法（pr#169@Gitee）\n* 【core  】     Img增加stroke描边方法（issue#1033@Github）\n\n### 🐞Bug修复#\n* 【poi   】     修复ExcelBase.isXlsx方法判断问题（issue#I1S502@Gitee）\n* 【poi   】     修复Excel03SaxReader日期方法判断问题（pr#1026@Github）\n* 【core  】     修复StrUtil.indexOf空指针问题（issue#1038@Github）\n* 【extra 】     修复VelocityEngine编码问题和路径前缀问题（issue#I1T0IG@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.4.0 (2020-08-06)\n\n### 🐣新特性\n* 【socket】     对NioServer和NioClient改造（pr#992@Github）\n* 【core  】     StrUtil增加filter方法（pr#149@Gitee）\n* 【core  】     DateUtil增加beginOfWeek重载\n* 【core  】     将有歧义的BeanUtil.mapToBean方法置为过期（使用toBean方法）\n* 【core  】     添加WatchAction（对Watcher的抽象）\n* 【core  】     修改UUID正则，更加严谨（issue#I1Q1IW@Gitee）\n* 【core  】     ArrayUtil增加isAllNull方法（issue#1004@Github）\n* 【core  】     CollUtil增加contains方法（pr#152@Gitee）\n* 【core  】     ArrayUtil增加isAllNotNull方法（pr#1008@Github）\n* 【poi   】     closeAfterRead参数无效，方法设为过期（issue#1007@Github）\n* 【core  】     CollUtil中部分方法返回null变更为返回empty\n* 【all   】     添加英文README（pr#153@Gitee）\n* 【extra 】     SpringUtil增加getBean(TypeReference)（pr#1009@Github）\n* 【core  】     Assert增加方法，支持自定义异常处理（pr#154@Gitee）\n* 【core  】     BooleanConverter增加数字转换规则（issue#I1R2AB@Gitee）\n* 【poi   】     sax方式读取增加一个sheet结束的回调（issue#155@Gitee）\n* 【db    】     增加BeeCP连接池支持\n* 【core  】     改进Img.pressImage方法，避免变色问题（issue#1001@Github）\n\n### 🐞Bug修复#\n* 【core  】     修复原始类型转换时，转换失败没有抛出异常的问题\n* 【core  】     修复BeanUtil.mapToBean中bean的class非空构造无法实例化问题\n* 【core  】     修复NamedSql多个连续变量出现替换问题\n* 【core  】     修复Bean重名字段（大小写区别）获取数据出错的问题（issue#I1QBQ4@Gitee）\n* 【http  】     修复SimpleServer响应头无效问题（issue#1006@Github）\n* 【core  】     修复ThreadLocalRandom共享seed导致获取随机数一样的问题（pr#151@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n# 5.3.11 (2020-08-01)\n\n### 🐣新特性\n* 【captcha】     AbstractCaptcha增加getImageBase64Data方法（pr#985@Github）\n* 【core   】     增加PhoneUtil（pr#990@Github）\n* 【core   】     改进Img，目标图片类型未定义使用源图片类型（issue#I1PB0B@Gitee）\n* 【json   】     JSONConfig增加Transient选项（issue#I1PLHN@Gitee）\n* 【core   】     MapUtil增加getXXX的默认值重载（issue#I1PTGI@Gitee）\n* 【core   】     CalendarUtil增加parseByPatterns方法（issue#993@Github）\n\n### 🐞Bug修复#\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.10 (2020-07-23)\n\n### 🐣新特性\n* 【db   】       增加DbUtil.setReturnGeneratedKeyGlobal（issue#I1NM0K@Gitee）\n* 【core 】       增加DataSize和DataSizeUtil（issue#967@Github）\n* 【core 】       ImgUtil增加异常，避免空指针（issue#I1NKXG@Gitee）\n* 【core 】       增加CRC16算法若干（pr#963@Github）\n* 【core 】       LocalDateTimeUtil增加format等方法（pr#140@Gitee）\n* 【http 】       UserAgentUtil增加Android原生浏览器识别（pr#975@Github）\n* 【crypto 】     增加ECIES算法类（issue#979@Github）\n* 【crypto 】     CollUtil增加padLeft和padRight方法（pr#141@Gitee）\n* 【core 】       IdCardUtil香港身份证去除首字母校验（issue#I1OOTB@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复ZipUtil中finish位于循环内的问题（issue#961@Github）\n* 【core   】     修复CollUtil.page未越界检查的问题（issue#I1O2LR@Gitee）\n* 【core   】     修复StrUtil.removeAny的bug（issue#977@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.9 (2020-07-12)\n\n### 🐣新特性\n* 【core   】     DateUtil增加formatChineseDate（pr#932@Github）\n* 【core   】     ArrayUtil.isEmpty修改逻辑（pr#948@Github）\n* 【core   】     增强StrUtil中空判断后返回数据性能（pr#949@Github）\n* 【core   】     deprecate掉millsecond，改为millisecond（issue#I1M9P8@Gitee）\n* 【core   】     增加LocalDateTimeUtil（issue#I1KUVC@Gitee）\n* 【core   】     Month增加getLastDay方法\n* 【core   】     ChineseDate支持到2099年\n\n### 🐞Bug修复\n* 【core   】     修复NumberUtil.partValue有余数问题（issue#I1KX66@Gitee）\n* 【core   】     修复BeanUtil.isEmpty不能忽略static字段问题（issue#I1KZI6@Gitee）\n* 【core   】     修复StrUtil.brief长度问题（pr#930@Github）\n* 【socket 】     修复AioSession构造超时无效问题（pr#941@Github）\n* 【setting】     修复GroupSet.contains错误（pr#943@Github）\n* 【core   】     修复ZipUtil没有调用finish问题（issue#944@Github）\n* 【extra  】     修复Ftp中ArrayList长度为负问题（pr#136@Github）\n* 【core   】     修复Dict中putAll大小写问题（issue#I1MU5B@Gitee）\n* 【core   】     修复POI中sax读取数字判断错误问题（issue#931@Github）\n* 【core   】     修复DateUtil.endOfQuarter错误问题（issue#I1NGZ7@Gitee）\n* 【core   】     修复URL中有空格转为+问题（issue#I1NGW4@Gitee）\n* 【core   】     修复CollUtil.intersectionDistinct空集合结果错误问题\n* 【core   】     修复ChineseDate在1996年计算错误问题（issue#I1N96I@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.8 (2020-06-16)\n\n### 🐣新特性\n* 【core   】     增加ISO8601日期格式（issue#904@Github）\n* 【setting】     Props异常规则修改（issue#907@Github）\n* 【setting】     增加GIF支持\n* 【core   】     复制创建一个Bean对象, 并忽略某些属性(pr#130@Gitee)\n* 【core   】     DateUtil.parse支持更多日期格式(issue#I1KHTB@Gitee)\n* 【crypto 】     增加获取密钥空指针的检查(issue#925@Github)\n* 【core   】     增加StrUtil.removeAny方法(issue#923@Github)\n* 【db     】     增加部分Connection参数支持(issue#924@Github)\n* 【core   】     FileUtil增加别名方法(pr#926@Github)\n* 【poi    】     ExcelReader中增加read重载，提供每个单元格单独处理的方法(issue#I1JZTL@Gitee)\n\n### 🐞Bug修复\n* 【json   】     修复append方法导致的JSONConfig传递失效问题（issue#906@Github）\n* 【core   】     修复CollUtil.subtractToList判断错误（pr#915@Github）\n* 【poi    】     修复WordWriter写表格问题（pr#914@Github）\n* 【core   】     修复IoUtil.readBytes缓存数组长度问题（issue#I1KIUE@Gitee）\n* 【core   】     修复BigExcelWriter多次flush导致的问题（issue#920@Github）\n* 【extra  】     绕过Pinyin4j最后一个分隔符失效的bug（issue#921@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.7 (2020-06-03)\n\n### 🐣新特性\n* 【core   】     ThreadFactoryBuilder的setUncaughtExceptionHandler返回this（issue#I1J4YJ@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复DateUtil.parse解析2020-5-8 3:12:13错误问题（issue#I1IZA3@Gitee）\n* 【core   】     修复Img.pressImg大小无效问题(issue#I1HSWU@Gitee)\n* 【core   】     修复CronUtil.stop没有清除任务的问题(issue#I1JACI@Gitee)\n\n-------------------------------------------------------------------------------------------------------------\n## 5.3.6 (2020-05-30)\n\n### 🐣新特性\n* 【core   】     NumberConverter Long类型增加日期转换（pr#872@Github）\n* 【all    】     StrUtil and SymmetricCrypto注释修正（pr#873@Github）\n* 【core   】     CsvReader支持返回Bean（issue#869@Github）\n* 【core   】     Snowflake循环等待下一个时间时避免长时间循环，加入对时钟倒退的判断（pr#874@Github）\n* 【extra  】     新增 QRCode base64 编码形式返回（pr#878@Github）\n* 【core   】     ImgUtil增加toBase64DateUri，URLUtil增加getDataUri方法\n* 【core   】     IterUtil添加List转Map的工具方法（pr#123@Gitee）\n* 【core   】     BeanValueProvider转换失败时，返回原数据，而非null\n* 【core   】     支持BeanUtil.toBean(object, Map.class)转换（issue#I1I4HC@Gitee）\n* 【core   】     MapUtil和CollUtil增加clear方法（issue#I1I4HC@Gitee）\n* 【core   】     增加FontUtil，可定义pressText是否从中间（issue#I1HSWU@Gitee）\n* 【http   】     SoapClient支持自定义请求头（issue#I1I0AO@Gitee）\n* 【script 】     ScriptUtil增加evalInvocable和invoke方法（issue#I1HHCP@Gitee）\n* 【core   】     ImgUtil增加去除背景色的方法（pr#124@Gitee）\n* 【system 】     OshiUtil增加获取CPU使用率的方法（pr#124@Gitee）\n* 【crypto 】     AsymmetricAlgorithm去除EC（issue#887@Github）\n* 【cache  】     超时缓存使用的线程池大小默认为1（issue#890@Github）\n* 【poi    】     ExcelSaxReader支持handleCell方法\n* 【core   】     Snowflake容忍2秒内的时间回拨（issue#I1IGDX@Gitee）\n* 【core   】     StrUtil增加isAllNotEmpty、isAllNotBlank方法（pr#895@Github）\n* 【core   】     DateUtil增加dayOfYear方法（pr#895@Github）\n* 【core   】     DateUtil增加dayOfYear方法（pr#895@Github）\n* 【http   】     HttpUtil增加downloadBytes方法（pr#895@Github）\n* 【core   】     isMactchRegex失效标记，增加isMatchRegex（issue#I1IPJG@Gitee）\n* 【core   】     优化Validator.isChinese\n* 【core   】     ArrayUtil.addAll增加原始类型支持（issue#898@Github）\n* 【core   】     DateUtil.parse支持2020-1-1这类日期解析（issue#I1HGWW@Github）\n\n### 🐞Bug修复\n* 【core   】     修复SimpleCache死锁问题（issue#I1HOKB@Gitee）\n* 【core   】     修复SemaphoreRunnable释放问题（issue#I1HLQQ@Gitee）\n* 【poi    】     修复Sax方式读取Excel行号错误问题（issue#882@Github）\n* 【poi    】     修复Sax方式读取Excel日期类型数据03和07不一致问题（issue#I1HL1C@Gitee）\n* 【poi    】     修复CamelCaseLinkedMap构造错误（issue#I1IZ30@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.5 (2020-05-13)\n\n### 🐣新特性\n* 【core   】     增加CollUtil.map方法\n* 【extra  】     增加Sftp.lsEntries方法，Ftp和Sftp增加recursiveDownloadFolder（pr#121@Gitee）\n* 【system 】     OshiUtil增加getNetworkIFs方法\n* 【core   】     CollUtil增加unionDistinct、unionAll方法（pr#122@Gitee）\n* 【core   】     增加IoUtil.readObj重载，通过ValidateObjectInputStream由用户自定义安全检查。\n* 【http   】     改造HttpRequest中文件上传部分，增加MultipartBody类\n\n### 🐞Bug修复\n* 【core   】     修复IoUtil.readObj中反序列化安全检查导致的一些问题，去掉安全检查。\n* 【http   】     修复SimpleServer文件访问404问题（issue#I1GZI3@Gitee）\n* 【core   】     修复BeanCopier中循环引用逻辑问题（issue#I1H2VN@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.4 (2020-05-10)\n\n### 🐣新特性\n* 【core   】     增加URLUtil.getContentLength方法（issue#I1GB1Z@Gitee）\n* 【extra  】     增加PinyinUtil（issue#I1GMIV@Gitee）\n\n### 🐞Bug修复\n* 【extra  】     修复Ftp设置超时问题（issue#I1GMTQ@Gitee）\n* 【core   】     修复TreeUtil根据id查找子节点时的NPE问题（pr#120@Gitee）\n* 【core   】     修复BeanUtil.copyProperties中Alias注解无效问题（issue#I1GK3M@Gitee）\n* 【core   】     修复CollUtil.containsAll空集合判断问题（issue#I1G9DE@Gitee）\n* 【core   】     修复XmlUtil.xmlToBean失败问题（issue#865@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.3 (2020-05-05)\n\n### 🐣新特性\n* 【core   】     ImgUtil.createImage支持背景透明（issue#851@Github）\n* 【json   】     更改JSON转字符串时\"</\"被转义的规则为不转义（issue#852@Github）\n* 【cron   】     表达式的所有段支持L关键字（issue#849@Github）\n* 【extra  】     增加PinyinUtil，封装TinyPinyin\n* 【extra  】     Ftp和Sftp增加FtpConfig，提供超时等更多可选参数\n* 【extra  】     SpringUtil增加getActiveProfiles、getBeansOfType、getBeanNamesForType方法（issue#I1FXF3@Gitee）\n* 【bloomFilter】 避免布隆过滤器数字溢出（pr#119@Gitee）\n* 【core   】     增加IoUtil.writeObj（issue#I1FZIE）\n* 【core   】     增加FastStringWriter\n* 【core   】     增加NumberUtil.ceilDiv方法（pr#858@Github）\n* 【core   】     IdcardUtil增加省份校验（issue#859@Github）\n* 【extra  】     TemplateFactory和TokenizerFactory增加单例的get方法\n\n### 🐞Bug修复\n* 【core   】     修复URLBuilder中请求参数有`&amp;`导致的问题（issue#850@Github）\n* 【core   】     修复URLBuilder中路径以`/`结尾导致的问题（issue#I1G44J@Gitee）\n* 【db     】     修复SqlBuilder中orderBy无效问题（issue#856@Github）\n* 【core   】     修复StrUtil.subBetweenAll错误问题（issue#861@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.2 (2020-04-23)\n\n### 🐣新特性\n* 【core   】     增加NetUtil.isOpen方法\n* 【core   】     增加ThreadUtil.sleep和safeSleep的重载\n* 【core   】     Sftp类增加toString方法（issue#I1F2T4@Gitee）\n* 【core   】     修改FileUtil.size逻辑，不存在的文件返回0\n* 【extra  】     Sftp.ls遇到文件不存在返回空集合，而非抛异常（issue#844@Github）\n* 【http   】     改进HttpRequest.toString()格式，添加url\n\n### 🐞Bug修复\n* 【db     】     修复PageResult.isLast计算问题\n* 【cron   】     修复更改系统时间后CronTimer被阻塞的问题（issue#838@Github）\n* 【db     】     修复Page.addOrder无效问题（issue#I1F9MZ@Gitee）\n* 【json   】     修复JSONConvert转换日期空指针问题（issue#I1F8M2@Gitee）\n* 【core   】     修复XML中带注释Xpath解析导致空指针问题（issue#I1F2WI@Gitee）\n* 【core   】     修复FileUtil.rename原文件无扩展名多点的问题（issue#839@Github）\n* 【db     】     修复DbUtil.close可能存在的空指针问题（issue#847@Github）\n\n-------------------------------------------------------------------------------------------------------------\n## 5.3.1 (2020-04-17)\n\n### 🐣新特性\n* 【core   】     ListUtil、MapUtil、CollUtil增加empty方法\n* 【poi    】     调整别名策略，clearHeaderAlias和addHeaderAlias同时清除aliasComparator（issue#828@Github）\n* 【core   】     修改StrUtil.equals逻辑，改为contentEquals\n* 【core   】     增加URLUtil.UrlDecoder\n* 【core   】     增加XmlUtil.setNamespaceAware，getByPath支持UniversalNamespaceCache\n* 【aop    】     增加Spring-cglib支持，改为SPI实现\n* 【json   】     增加JSONUtil.parseXXX增加JSONConfig参数\n* 【core   】     RandomUtil.randomNumber改为返回char\n* 【crypto 】     SM2支持设置Digest和DSAEncoding（issue#829@Github）\n\n### 🐞Bug修复\n* 【json   】     修复解析JSON字符串时配置无法传递问题（issue#I1EIDN@Gitee）\n* 【core   】     修复ServletUtil.readCookieMap空指针问题（issue#827@Github）\n* 【crypto 】     修复SM2中检查密钥导致的问题（issue#I1EC47@Gitee）\n* 【core   】     修复TableMap.isEmpty判断问题\n* 【http   】     修复编码后的URL传入导致二次编码的问题（issue#I1EIMN@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.3.0 (2020-04-07)\n\n### 🐣新特性\n* 【extra  】     JschUtil增加execByShell方法(issue#I1CYES@Gitee)\n* 【core   】     StrUtil增加subBetweenAll方法，Console增加where和lineNumber方法(issue#812@Github)\n* 【core   】     TableMap增加getKeys和getValues方法\n* 【json   】     JSONObject和JSONArray增加set方法，标识put弃用\n* 【http   】     增加SimpleHttpServer\n* 【script 】     增加createXXXScript，区别单例\n* 【core   】     修改FileUtil.writeFileToStream等方法返回值为long\n* 【core   】     CollUtil.split增加空集合判定（issue#814@Github）\n* 【core   】     NetUtil增加parseCookies方法\n* 【core   】     CollUtil增加toMap方法\n* 【core   】     CollUtil和IterUtil废弃一些方法\n* 【core   】     添加ValidateObjectInputStream避免对象反序列化漏洞风险\n* 【core   】     添加BiMap\n* 【all    】     cn.hutool.extra.servlet.multipart包迁移到cn.hutool.core.net下\n* 【core   】     XmlUtil.mapToXml方法支持集合解析（issue#820@Github）\n* 【json   】     解析Object中对是否为bean单独判断，而不是直接解析\n* 【core   】     SimHash锁改为StampedLock\n* 【core   】     Singleton改为SimpleCache实现\n* 【core   】     增加CalendarUtil，DateUtil相关方法全部迁移到此\n\n### 🐞Bug修复\n* 【extra  】     修复SpringUtil使用devtools重启报错问题\n* 【http   】     修复HttpUtil.encodeParams针对无参数URL问题（issue#817@Github）\n* 【extra  】     修复模板中无效引用的问题\n* 【extra  】     修复读取JSON文本配置未应用到子对象的问题（issue#818@Github）\n* 【extra  】     修复XmlUtil.createXml中namespace反向问题\n* 【core   】     修复WatchMonitor默认无event问题\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.2.5 (2020-03-26)\n\n### 🐣新特性\n* 【core   】     增加逻辑，对于原始类型注入，使用默认值（issue#797@Github）\n* 【core   】     增加CityHash算法\n* 【core   】     PageUtil支持setFirstPageNo自定义第一页的页码（issue#I1CGNZ@Gitee）\n* 【http   】     UserAgentUtil增加Chromium内核的Edge浏览器支持（issue#800@Github）\n* 【cache  】     修改FIFOCache中linkedHashMap的初始容量策略（pr#801@Github）\n* 【core   】     修改XmlUtil中setNamespaceAware默认为true\n* 【core   】     TreeNode增加extra\n* 【core   】     CollUtil.newHashSet重载歧义，更换为set方法\n* 【core   】     增加ListUtil，增加Hash32、Hash64、Hash128接口\n* 【crypto 】     BCUtil增加readPemPrivateKey和readPemPublicKey方法\n* 【cache  】     替换读写锁为StampedLock，增加LockUtil\n\n### 🐞Bug修复\n* 【core   】     修复NumberWordFormatter拼写错误（issue#799@Github）\n* 【poi    】     修复xls文件下拉列表无效问题（issue#I1C79P@Gitee）\n* 【poi    】     修复使用Cglib代理问题（issue#806@Github）\n* 【core   】     修复DateUtil.weekCount跨年计算问题\n\n-------------------------------------------------------------------------------------------------------------\n## 5.2.4\n\n### 🐣新特性\n* 【setting】     Setting中增加addSetting和autoLoad重载（pr#104@Gitee）\n* 【core   】     增加copyProperties，根据Class创建对象并进行属性拷贝（pr#105@Gitee）\n* 【core   】     添加获取class当前文件夹名称方法（pr#106@Gitee）\n* 【core   】     BooleanUtil中重载歧义修正，修改了包装参数的方法名（issue#I1BSK8@Gitee）\n* 【core   】     XmlUtil增加xmlToBean和beanToXml方法\n* 【db     】     设置全局忽略大小写DbUtil.setCaseInsensitiveGlobal(true)（issue#784@Github）\n* 【core   】     增加CallerUtil.getCallerMethodName方法\n* 【core   】     Tree增加getParent方法，可以获取父节点，抽象Node接口\n* 【core   】     增加社会信用代码工具CreditCodeUtil（pr#112@Gitee）\n* 【core   】     ChineseDate增加构造重载，增加toStringNormal（issue#792@Github）\n* 【core   】     BeanUtil.toBean增加重载（issue#797@Github）\n\n### 🐞Bug修复\n* 【core   】     修复TypeUtil无法获取泛型接口的泛型参数问题（issue#I1BRFI@Gitee）\n* 【core   】     修复MySQL中0000报错问题\n* 【core   】     修复BeanPath从Map取值为空的问题（issue#790@Github）\n* 【poi    】     修复添加图片尺寸的单位问题（issue#I1C2ER@Gitee）\n* 【setting】     修复getStr中逻辑问题（pr#113@Gitee）\n* 【json   】     修复JSONUtil.toXml汉字被编码的问题（pr#795@Gitee）\n* 【poi    】     修复导出的Map列表中每个map长度不同导致的对应不一致的问题（issue#793@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n## 5.2.3\n\n### 🐣新特性\n* 【http   】     UserAgentUtil增加识别ios和android等（issue#781@Github）\n* 【core   】     支持新领车牌（issue#I1BJHE@Gitee）\n\n### 🐞Bug修复\n* 【core   】     修复PageUtil第一页语义不明确的问题（issue#782@Github）\n* 【extra  】     修复TemplateFactory引入包导致的问题\n* 【core   】     修复ServiceLoaderUtil.loadFirstAvailable问题\n\n-------------------------------------------------------------------------------------------------------------\n## 5.2.2\n\n### 🐣新特性\n\n### 🐞Bug修复\n* 【http   】     修复body方法添加多余头的问题（issue#769@Github）\n* 【bloomFilter 】修复默认为int类型,左移超过32位后,高位丢失问题（pr#770@Github）\n* 【core   】     修复beginOfWeek和endOfWeek一周开始计算错误问题（issue#I1BDPW@Gitee）\n* 【db     】     修复Db.query使用命名方式查询产生的歧义（issue#776@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.2.1\n\n### 🐣新特性\n* 【core   】     修改FastDateParser策略，与JDK保持一致（issue#I1AXIN@Gitee）\n* 【core   】     增加tree（树状结构）（pr#100@Gitee）\n* 【core   】     增加randomEleList（pr#764@Github）\n### 🐞Bug修复\n* 【setting】     修复Props.toBean方法null的问题\n* 【core   】     修复DataUtil.parseLocalDateTime无时间部分报错问题（issue#I1B18H@Gitee）\n* 【core   】     修复NetUtil.isUsableLocalPort()判断问题（issue#765@Github）\n* 【poi    】     修复ExcelWriter写出多个sheet错误的问题（issue#766@Github）\n* 【extra  】     修复模板引擎自定义配置失效问题（issue#767@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.2.0\n\n### 🐣新特性\n* 【core  】     NumberUtil.decimalFormat增加Object对象参数支持\n* 【core  】     增加ReflectUtil.getFieldValue支持Alias注解\n* 【core  】     Bean字段支持Alias注解（包括转map,转bean等）\n* 【core  】     增加ValueListHandler，优化结果集获取方式\n* 【http  】     支持patch方法（issue#666@Github）\n* 【crypto】     BCUtil支持更加灵活的密钥类型，增加writePemObject方法\n* 【core  】     增加ServiceLoaderUtil\n* 【core  】     增加EnumUtil.getEnumAt方法\n* 【core  】     增强EnumConvert判断能力（issue#I17082@Gitee）\n* 【all   】     log、template、tokenizer使用SPI机制代替硬编码\n* 【poi   】     Word07Writer增加addPicture\n* 【crypto】     RSA算法中，BlockSize长度策略调整（issue#721@Github）\n* 【crypto】     删除SM2Engine，使用BC库中的对象替代\n* 【crypto】     增加PemUtil工具类\n* 【dfa   】     WordTree增加Filter，支持自定义特殊字符过滤器\n* 【poi   】     对于POI依赖升级到4.1.2\n* 【crypto】     增加国密SM2验签密钥格式支持（issue#686@Github）\n\n### 🐞Bug修复\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.5\n\n### 🐣新特性\n* 【poi  】     Excel合并单元格读取同一个值，不再为空\n* 【core 】     增加EscapeUtil.escapeAll（issue#758@Github）\n* 【core 】     增加formatLocalDateTime和parseLocalDateTime方法（pr#97@Gitee）\n\n### 🐞Bug修复\n* 【core 】     修复EscapeUtil.escape转义错误（issue#758@Github）\n* 【core 】     修复Convert.toLocalDateTime(Object value, Date defaultValue)返回结果不是LocalDateTime类型的问题（pr#97@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.4\n\n### 🐣新特性\n* 【poi  】     增加单元格位置引用（例如A11等方式获取单元格）\n* 【extra】     ServletUtil.fillBean支持数据和集合字段（issue#I19ZMK@Gitee）\n* 【core 】     修改ThreadUtil.newSingleExecutor默认队列大小（issue#754@Github）\n* 【core 】     修改ExecutorBuilder默认队列大小（issue#753@Github）\n* 【core 】     FileTypeUtil增加mp4的magic（issue#756@Github）\n\n### 🐞Bug修复\n* 【core 】     修复CombinationAnnotationElement数组判断问题（issue#752@Github）\n* 【core 】     修复log4j2使用debug行号打印问题（issue#I19NFJ@Github）\n* 【poi  】     修复sax读取excel03数组越界问题（issue#750@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.3\n\n### 🐣新特性\n* 【core 】     废弃isMactchRegex，改为isMatchRegex（方法错别字）\n* 【core 】     修正hasNull()方法上注释错误（issue#I18TAG@Gitee）\n* 【core 】     Snowflake的起始时间可以被指定（pr#95@Gitee）\n* 【core 】     增加PropsUtil及getFirstFound方法（issue#I1960O@Gitee）\n### 🐞Bug修复\n* 【core 】     CharsetUtil在不支持GBK的系统中运行报错问题（issue#731@Github）\n* 【core 】     RandomUtil的randomEleSet方法顺序不随机的问题（pr#741@Github）\n* 【core 】     修复StopWatch的toString判断问题（issue#I18VIK@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.2\n\n### 🐣新特性\n* 【core 】     XmlUtil支持可选是否输出omit xml declaration（pr#732@Github）\n* 【core 】     车牌号校验兼容新能源车牌（pr#92@Gitee）\n* 【core 】     在NetUtil中新增ping功能（pr#91@Gitee）\n* 【core 】     DateUtil.offset不支持ERA，增加异常提示（issue#I18KD5@Gitee）\n* 【http 】     改进HttpUtil访问HTTPS接口性能问题，SSL证书使用单例（issue#I18AL1@Gitee）\n\n### 🐞Bug修复\n* 【core 】     修复isExpired的bug（issue#733@Gtihub）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.1\n\n### 🐣新特性\n* 【core 】     ClassUtil.isSimpleValueType增加TemporalAccessor支持（issue#I170HK@Gitee）\n* 【core 】     增加Convert.toPrimitiveByteArray方法，Convert支持对象序列化和反序列化\n* 【core 】     DateUtil增加isExpired(Date startDate, Date endDate, Date checkDate)（issue#687@Github）\n* 【core 】     增加Alias注解\n* 【core 】     修正NumberChineseFormatter和NumberWordFormatter（类名拼写错误）\n* 【all  】     修正equals，避免可能存在的空指针问题（pr#692@Github）\n* 【core  】    提供一个自带默认值的Map（pr#87@Gitee）\n* 【core  】    修改Dict在非大小写敏感状态下get也不区分大小写（issue#722@Github）\n* 【core  】    StrUtil增加contains方法（issue#716@Github）\n* 【core  】    QrCodeUtil增加背景透明支持（pr#89@Gitee）\n* 【core  】    增加农历ChineseDate（pr#90@Gitee）\n* 【core  】    ZipUtil增加zip方法写出到流（issue#I17SCT@Gitee）\n* 【db    】    Db.use().query的方法中增加Map参数接口（issue#709@Github）\n* 【db    】    getDialect使用数据源作为锁（issue#720@Github）\n\n### 🐞Bug修复\n* 【core 】     修复NumberUtil.mul中null的结果错误问题（issue#I17Y4J@Gitee）\n* 【core 】     修复当金额大于等于1亿时，转换会多出一个万字的bug（pr#715@Github）\n* 【core 】     修复FileUtil.listFileNames位于jar内导致的文件找不到问题\n* 【core 】     修复TextSimilarity.similar去除字符导致的问题（issue#I17K2A@Gitee）\n* 【core 】     修复unzip文件路径问题（issue#I17VU7@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.1.0\n\n### 🐣新特性\n* 【core 】     新增WatchServer（issue#440@Github）\n* 【core 】     ReflectUtil.getFieldValue支持static（issue#662@Github）\n* 【core 】     改进Bean判断和注入逻辑：支持public字段注入（issue#I1689L@Gitee）\n* 【extra】     新增SpringUtil\n* 【http 】     Get请求支持body，移除body（JSON）方法（issue#671@Github）\n* 【core 】     ReflectUtil修正getFieldValue逻辑，防止歧义\n\n\n### 🐞Bug修复\n* 【db  】      修复SqlExecutor.callQuery关闭Statement导致的问题（issue#I16981@Gitee）\n* 【db  】      修复XmlUtil.xmlToMap中List节点的问题（pr#82@Gitee）\n* 【core】      修复ZipUtil中对于/结尾路径处理的问题（issue#I16PKP@Gitee）\n* 【core】      修复DateConvert对int不支持导致的问题（issue#677@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.7\n\n### 🐣新特性\n* 【core 】      解决NumberUtil导致的ambiguous问题（issue#630@Github）\n* 【core 】      BeanUtil.isEmpty()忽略字段支持，增加isNotEmpty（issue#629@Github）\n* 【extra】      邮件发送后获取message-id（issue#I15FKR@Gitee）\n* 【core 】      CaseInsensitiveMap/CamelCaseMap增加toString（issue#636@Github）\n* 【core 】      XmlUtil多节点改进（issue#I15I0R@Gitee）\n* 【core 】      Thread.excAsync修正为execAsync（issue#642@Github）\n* 【core 】      FileUtil.getAbsolutePath修正正则（issue#648@Github）\n* 【core 】      NetUtil增加getNetworkInterface方法（issue#I15WEL@Gitee）\n* 【core 】      增加ReflectUtil.getFieldMap方法（issue#I15WJ7@Gitee）\n\n### 🐞Bug修复\n* 【extra】      修复SFTP.upload上传失败的问题（issue#I15O40@Gitee）\n* 【db】         修复findLike匹配错误问题\n* 【core 】      修复scale方法透明无效问题（issue#I15L5S@Gitee）\n* 【extra】      修复exec返回无效（issue#I15L5S@Gitee）\n* 【cron】       修复CronPattern注释（pr#646@Github）\n* 【json】       修复LocalDateTime等JDK8时间对象不被支持的问题（issue#644@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.6\n\n### 🐣新特性\n* 【setting】    toBean改为泛型，增加class参数重载（pr#80@Gitee）\n* 【core】       XmlUtil使用JDK默认的实现，避免第三方实现导致的问题（issue#I14ZS1@Gitee）\n* 【poi】        写入单元格数据类型支持jdk8日期格式（pr#628@Github）\n\n### 🐞Bug修复\n* 【core】       修复DateUtil.format使用DateTime时区失效问题（issue#I150I7@Gitee）\n* 【core】       修复ZipUtil解压目录遗留问题（issue#I14NO3@Gitee）\n* 【core】       修复等比缩放给定背景色无效问题（pr#625@Github）\n* 【poi 】       修复sax方式读取excel中无样式表导致的空指针问题\n* 【core】       修复标准化URL时domain被转义的问题（pr#654@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.5\n\n### 🐣新特性\n* 【core】       增加MapUtil.removeAny（issue#612@Github）\n* 【core】       Convert.toList支持[1,2]字符串（issue#I149XN@Gitee）\n* 【core】       修正DateUtil.thisWeekOfMonth注释错误（issue#614@Github）\n* 【core】       DateUtil增加toLocalDate等方法，DateTime更好的支持时区\n* 【core】       BeanUtil.getProperty返回泛型对象（issue#I14PIW@Gitee）\n* 【core】       FileTypeUtil使用扩展名辅助判断类型（issue#I14JBH@Gitee）\n\n### 🐞Bug修复\n* 【db】         修复MetaUtil.getTableMeta()方法未释放ResultSet的bug（issue#I148GH@Gitee）\n* 【core】       修复DateUtil.age闰年导致的问题（issue#I14BVN@Gitee）\n* 【extra】      修复ServletUtil.getCookie大小写问题（pr#79@Gitee）\n* 【core】       修复IdcardUtil.isValidCard18报错问题（issue#I14LTJ@Gitee）\n* 【poi】        修复double值可能存在的精度问题（issue#I14FG1@Gitee）\n* 【core】       修复Linux下解压目录不正确的问题（issue#I14NO3@Gitee）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.4\n\n### 🐣新特性\n* 【setting】    增加System.getenv变量替换支持\n* 【core】       XmlUtil中mapToStr支持namespace（pr#599@Github）\n* 【core】       ZipUtil修改策略:默认关闭输入流（issue#604@Github）\n* 【core】       改进CsvReader，支持RowHandler按行处理（issue#608@Github）\n* 【core】       增加MapUtil.sortJoin，改进SecureUtil.signParams支持补充字符串（issue#606@Github）\n* 【core】       增加Money类（issue#605@Github）\n\n### 🐞Bug修复\n* 【core】       解决ConcurrentHashSet不能序列化的问题（issue#600@Github）\n* 【core】       解决CsvReader.setErrorOnDifferentFieldCount循环调用问题\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.3\n\n### 🐣新特性\n### 🐞Bug修复\n* 【extra】      修复遗留的getSession端口判断错误（issue#594@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.2\n\n### 🐣新特性\n* 【core】       强化java.time包的对象转换支持\n\n### 🐞Bug修复\n* 【db】         修正字段中含有as导致触发关键字不能包装字段的问题（issue#I13ML7@Gitee）\n* 【extra】      修复QrCode中utf-8不支持大写的问题。（issue#I13MT6@Gitee）\n* 【http】       修复请求defalte数据解析错误问题。（pr#593@Github）\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.1\n\n### 🐣新特性\n* 【json】       JSONUtil.toBean支持JSONArray\n### 🐞Bug修复\n* 【extra】      修复getSession端口判断错误\n\n-------------------------------------------------------------------------------------------------------------\n\n## 5.0.0\n\n### 🐣新特性\n* 【all】        升级JDK最低 支持到8\n* 【log】        Log接口添加get的static方法\n* 【all】        部分接口添加FunctionalInterface修饰\n* 【crypto】     KeyUtil增加readKeyStore重载\n* 【extra】      JschUtil增加私钥传入支持（issue#INKDR@Gitee）\n* 【core】       DateUtil、DateTime、Convert全面支持jdk8的time包\n\n### 🐞Bug修复\n* 【http】       修复Cookie中host失效导致的问题（issue#583@Github）\n"
  },
  {
    "path": "LICENSE",
    "content": "                     木兰宽松许可证, 第2版\n\n   木兰宽松许可证， 第2版\n   2020年1月 http://license.coscl.org.cn/MulanPSL2\n\n\n   您对“软件”的复制、使用、修改及分发受木兰宽松许可证，第2版（“本许可证”）的如下条款的约束：\n\n   0. 定义\n\n      “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。\n\n      “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。\n\n      “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。\n\n      “法人实体”是指提交贡献的机构及其“关联实体”。\n\n      “关联实体”是指，对“本许可证”下的行为方而言，控制、受控制或与其共同受控制的机构，此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。\n\n   1. 授予版权许可\n\n      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可，您可以复制、使用、修改、分发其“贡献”，不论修改与否。\n\n   2. 授予专利许可\n\n      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的（根据本条规定撤销除外）专利许可，供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求，不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地，就“软件”或其中的“贡献”对任何人发起专利侵权诉讼（包括反诉或交叉诉讼）或其他专利维权行动，指控其侵犯专利权，则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。\n\n   3. 无商标许可\n\n      “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可，但您为满足第4条规定的声明义务而必须使用除外。\n\n   4. 分发限制\n\n      您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发，不论修改与否，但您必须向接收者提供“本许可证”的副本，并保留“软件”中的版权、商标、专利及免责声明。\n\n   5. 免责声明与责任限制\n\n      “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下，“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任，不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。\n\n   6. 语言\n      “本许可证”以中英文双语表述，中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致，以中文版为准。\n\n   条款结束\n\n   如何将木兰宽松许可证，第2版，应用到您的软件\n\n   如果您希望将木兰宽松许可证，第2版，应用到您的新软件，为了方便接收者查阅，建议您完成如下三步：\n\n      1， 请您补充如下声明中的空白，包括软件名、软件的首次发表年份以及您作为版权人的名字；\n\n      2， 请您在软件包的一级目录下创建以“LICENSE”为名的文件，将整个许可证文本放入该文件中；\n\n      3， 请将如下声明文本放入每个源文件的头部注释中。\n\n   Copyright (c) [Year] [name of copyright holder]\n   [Software Name] is licensed under Mulan PSL v2.\n   You can use this software according to the terms and conditions of the Mulan PSL v2.\n   You may obtain a copy of Mulan PSL v2 at:\n            http://license.coscl.org.cn/MulanPSL2\n   THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n   See the Mulan PSL v2 for more details.\n\n\n                     Mulan Permissive Software License，Version 2\n\n   Mulan Permissive Software License，Version 2 (Mulan PSL v2)\n   January 2020 http://license.coscl.org.cn/MulanPSL2\n\n   Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:\n\n   0. Definition\n\n      Software means the program and related documents which are licensed under this License and comprise all Contribution(s).\n\n      Contribution means the copyrightable work licensed by a particular Contributor under this License.\n\n      Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.\n\n      Legal Entity means the entity making a Contribution and all its Affiliates.\n\n      Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.\n\n   1. Grant of Copyright License\n\n      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.\n\n   2. Grant of Patent License\n\n      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.\n\n   3. No Trademark License\n\n      No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.\n\n   4. Distribution Restriction\n\n      You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.\n\n   5. Disclaimer of Warranty and Limitation of Liability\n\n      THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n   6. Language\n\n      THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.\n\n   END OF THE TERMS AND CONDITIONS\n\n   How to Apply the Mulan Permissive Software License，Version 2 (Mulan PSL v2) to Your Software\n\n      To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:\n\n      i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;\n\n      ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;\n\n      iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.\n\n\n   Copyright (c) [Year] [name of copyright holder]\n   [Software Name] is licensed under Mulan PSL v2.\n   You can use this software according to the terms and conditions of the Mulan PSL v2.\n   You may obtain a copy of Mulan PSL v2 at:\n               http://license.coscl.org.cn/MulanPSL2\n   THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n   See the Mulan PSL v2 for more details."
  },
  {
    "path": "README-EN.md",
    "content": "<p align=\"center\">\n    <a href=\"https://hutool.cn/\"><img src=\"https://cdn.jsdelivr.net/gh/looly/hutool-site/images/logo.jpg\" width=\"45%\"></a>\n</p>\n<p align=\"center\">\n    <strong>🍬A set of tools that keep Java sweet.</strong>\n</p>\n<p align=\"center\">\n    👉 <a href=\"https://hutool.cn\">https://hutool.cn/</a> 👈\n</p>\n\n<p align=\"center\">\n    <a target=\"_blank\" href=\"https://search.maven.org/artifact/cn.hutool/hutool-all\">\n        <img src=\"https://img.shields.io/maven-central/v/cn.hutool/hutool-all.svg?label=Maven%20Central\" />\n    </a>\n    <a target=\"_blank\" href=\"https://license.coscl.org.cn/MulanPSL2\">\n        <img src=\"https://img.shields.io/:license-MulanPSL2-blue.svg\" />\n    </a>\n    <a target=\"_blank\" href=\"https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html\">\n        <img src=\"https://img.shields.io/badge/JDK-8+-green.svg\" />\n    </a>\n    <a target=\"_blank\" href=\"https://app.travis-ci.com/chinabugotech/hutool\">\n        <img src=\"https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master\" />\n    </a>\n    <a href=\"https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=chinabugotech/hutool&amp;utm_campaign=Badge_Grade\">\n        <img src=\"https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d\"/>\n    </a>\n    <a href=\"https://codecov.io/gh/chinabugotech/hutool\">\n        <img src=\"https://codecov.io/gh/chinabugotech/hutool/branch/v5-master/graph/badge.svg\" />\n    </a>\n    <a target=\"_blank\" href=\"https://gitter.im/hutool/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge\">\n        <img src=\"https://badges.gitter.im/hutool/Lobby.svg\" />\n    </a>\n    <a target=\"_blank\" href='https://gitee.com/chinabugotech/hutool/stargazers'>\n        <img src='https://gitee.com/chinabugotech/hutool/badge/star.svg?theme=gvp' alt='star'/>\n    </a>\n    <a target=\"_blank\" href='https://github.com/chinabugotech/hutool'>\n        <img src=\"https://img.shields.io/github/stars/chinabugotech/hutool.svg?style=social\" alt=\"github star\"/>\n    </a>\n    <a target=\"_blank\" href='https://gitcode.com/chinabugotech/hutool'>\n        <img src=\"https://gitcode.com/chinabugotech/hutool/star/badge.svg\" alt=\"gitcode star\"/>\n    </a>\n    <a href=\"https://deepwiki.com/chinabugotech/hutool\">\n        <img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\">\n    </a>\n</p>\n\n<br/>\n<p align=\"center\">\n    <a href=\"https://qm.qq.com/q/YsLhyheHSA\">\n    <img alt=\"\" src=\"https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A7-667030118-orange\"/></a>\n</p>\n\n-------------------------------------------------------------------------------\n\n[**🌎中文说明**](README.md)\n\n-------------------------------------------------------------------------------\n\n## 📚Introduction\n**Hutool** is a small but comprehensive library of Java tools, achieved by encapsulation through static methods, reduce the cost of learning related APIs, increase productivity, and make Java as elegant as a functional programming language,let the Java be \"sweet\" too.\n\n**Hutool** tools and methods from each user's crafted, it covers all aspects of the underlying code of Java development, it is a powerful tool for large project development to solve small problems, but also the efficiency of small projects;\n\n**Hutool** is a project \"util\" package friendly alternative, it saves developers on the project of common classes and common tool methods of encapsulation time, so that development focus on business, at the same time can minimize the encapsulation is not perfect to avoid the bugs.\n\n### 🎁Origin of the 'Hutool' name\n\n**Hutool = Hu + tool**，Is the original company project after the stripping of the underlying code of the open source library , \"Hu\" is the short name of the company , 'tool' that tool .\n\nHutool,' Hútú '(Chinese Pinyin)，On the one hand, it is simple and easy to understand, on the other hand, it means \"hard to be confused\".(note: confused means 'Hútú (糊涂)' in china )\n\n### 🍺How Hutool is changing the way we code\n\nThe goal of  **Hutool**  is to use a simple function instead of a complex piece of code, thus avoiding the problem of \"copy and paste\" code as much as possible and revolutionizing the way we write code.\n\nTo calculate MD5 for example:\n\n- 👴【Before】Open a search engine -> search \"Java MD5 encryption\" -> open a blog -> copy and paste -> change it to work.\n- 👦【Now   】import Hutool -> SecureUtil.md5()\n\nHutool exists to reduce code search costs and avoid bugs caused by imperfect code on the web.\n\n### Thanks\n> this README is PR by [chengxian-yi](https://gitee.com/yichengxian)\n-------------------------------------------------------------------------------\n\n## 🛠️Module\nA Java-based tool class for files, streams, encryption and decryption, transcoding, regular, thread, XML and other JDK methods for encapsulation，composing various Util tool classes, as well as providing the following modules：\n\n| module             |     description                                                                                                            |\n|--------------------|-------------------------------------------------------------------------------------------------------------------------|\n| hutool-aop         |     JDK dynamic proxy encapsulation to provide non-IOC faceting support                                                 |\n| hutool-bloomFilter |     Bloom filtering to provide some Hash algorithm Bloom filtering                                                      |\n| hutool-cache       |     Simple cache                                                                                                        |\n| hutool-core        |     Core, including Bean operations, dates, various Utils, etc.                                                         |\n| hutool-cron        |     Task scheduling with Cron expressions                                                                               |\n| hutool-crypto      |     Provides symmetric, asymmetric and digest algorithm encapsulation                                                   |\n| hutool-db          |     Db operations based on ActiveRecord thinking.                                                                       |\n| hutool-dfa         |     DFA models, such as multi-keyword lookups                                                                           |\n| hutool-extra       |     Extension modules, third-party wrappers (template engine, mail, servlet, QR code, Emoji, FTP, word splitting, etc.) |\n| hutool-http        |     Http client                                                                                                         |\n| hutool-log         |     Log (facade)                                                                                                        |\n| hutool-script      |     Script execution encapsulation, e.g. Javascript                                                                     |\n| hutool-setting     |     Stronger Setting Profile tools and Properties tools                                                                 |\n| hutool-system      |     System parameter tools (JVM information, etc.)                                                                      |\n| hutool-json        |     JSON                                                                                                                |\n| hutool-captcha     |     Image Captcha                                                                                                       |\n| hutool-poi         |     Tools for working with Excel and Word in POI                                                                        |\n| hutool-socket      |     Java-based tool classes for NIO and AIO sockets                                                                     |\n| hutool-jwt         |     JSON Web Token (JWT) implement                                                                                      |\n| hutool-ai          |     AI implement                                                                                      |\n\nEach module can be introduced individually, or all modules can be introduced by introducing `hutool-all` as required.\n\n-------------------------------------------------------------------------------\n\n## 📝Doc\n\n[📘Chinese documentation](https://doc.hutool.cn/pages/index/)\n\n[📘Chinese back-up documentation](https://plus.hutool.cn/)\n\n[📙API](https://plus.hutool.cn/apidocs/)\n\n[🎬Video](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2)\n\n-------------------------------------------------------------------------------\n\n\n\n## 📦Install\n\n### 🍊Maven\n```xml\n<dependency>\n    <groupId>cn.hutool</groupId>\n    <artifactId>hutool-all</artifactId>\n    <version>5.8.44</version>\n</dependency>\n```\n\n### 🍐Gradle\n```\nimplementation 'cn.hutool:hutool-all:5.8.44'\n```\n\n## 📥Download\n\n- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.44/)\n\n> 🔔️note:\n> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.\n> If your project uses JDK7, please use Hutool 4.x version.\n\n### 🚽Compile and install\n\nDownload the entire project source code\n\ngitee：[https://gitee.com/chinabugotech/hutool](https://gitee.com/chinabugotech/hutool) \n\ngithub:[https://github.com/chinabugotech/hutool](https://github.com/chinabugotech/hutool)\n\n```sh\ncd ${hutool}\n./hutool.sh install\n```\n\n-------------------------------------------------------------------------------\n\n## 🏗️Other\n\n### 🎋Branch Description\n\nHutool's source code is divided into two branches:\n\n| branch | description                                               |\n|-----------|---------------------------------------------------------------|\n| v5-master | The master branch, the branch used by the release version, is the same as the jar committed to the central repository and does not receive any pr or modifications. |\n| v5-dev    | Development branch, which defaults to the next SNAPSHOT version, accepts modifications or pr |\n\n### 🐞Provide feedback or suggestions on bugs\n\nWhen submitting feedback, please indicate which JDK version, Hutool version, and related dependency library version you are using.\n\n- [Gitee issue](https://gitee.com/chinabugotech/hutool/issues)\n- [Github issue](https://github.com/chinabugotech/hutool/issues)\n- [Gitcode issue](https://gitcode.com/chinabugotech/hutool/issues)\n\n### 🧬Principles of PR(pull request)\n\nHutool welcomes anyone to contribute code to Hutool, but the author suffers from OCD and needs to submit a pr (pull request) that meets some specifications in order to care for the patient.：\n\n1. Improve the comments, especially each new method should follow the Java documentation specification to indicate the method description, parameter description, return value description and other information, if necessary, please add unit tests, if you want, you can also add your name.\n2. Code indentation according to Eclipse.\n3. Newly added methods do not use third-party library methods，Unless the method tool is add to the '**extra module**'.\n4. Please pull request to the `v5-dev` branch. Hutool uses a new branch after 5.x: `v5-master` is the master branch, which indicates the version of the central library that has been released, and this branch does not allow pr or modifications.\n\n### 📖 Documentation source code\n\n[Documentation source code](https://gitee.com/loolly_admin/hutool-doc-handy)\n\n-------------------------------------------------------------------------------\n\n## ⭐Star Hutool\n\n[![Stargazers over time](https://starchart.cc/chinabugotech/hutool.svg)](https://starchart.cc/chinabugotech/hutool)\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://hutool.cn/\"><img src=\"https://cdn.jsdelivr.net/gh/looly/hutool-site/images/logo.jpg\" width=\"45%\"></a>\n</p>\n<p align=\"center\">\n    <strong>🍬A set of tools that keep Java sweet.</strong>\n</p>\n<p align=\"center\">\n    👉 <a href=\"https://hutool.cn\">https://hutool.cn/</a> 👈\n</p>\n\n<p align=\"center\">\n    <a target=\"_blank\" href=\"https://search.maven.org/artifact/cn.hutool/hutool-all\">\n        <img src=\"https://img.shields.io/maven-central/v/cn.hutool/hutool-all.svg?label=Maven%20Central\" />\n    </a>\n    <a target=\"_blank\" href=\"https://license.coscl.org.cn/MulanPSL2\">\n        <img src=\"https://img.shields.io/:license-MulanPSL2-blue.svg\" />\n    </a>\n    <a target=\"_blank\" href=\"https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html\">\n        <img src=\"https://img.shields.io/badge/JDK-8+-green.svg\" />\n    </a>\n    <a target=\"_blank\" href=\"https://app.travis-ci.com/chinabugotech/hutool\">\n        <img src=\"https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master\" />\n    </a>\n    <a href=\"https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=chinabugotech/hutool&amp;utm_campaign=Badge_Grade\">\n        <img src=\"https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d\"/>\n    </a>\n    <a href=\"https://codecov.io/gh/chinabugotech/hutool\">\n        <img src=\"https://codecov.io/gh/chinabugotech/hutool/branch/v5-master/graph/badge.svg\" />\n    </a>\n    <a target=\"_blank\" href='https://gitee.com/chinabugotech/hutool/stargazers'>\n        <img src='https://gitee.com/chinabugotech/hutool/badge/star.svg?theme=gvp' alt='star'/>\n    </a>\n    <a target=\"_blank\" href='https://github.com/chinabugotech/hutool'>\n        <img src=\"https://img.shields.io/github/stars/chinabugotech/hutool.svg?style=social\" alt=\"github star\"/>\n    </a>\n    <a target=\"_blank\" href='https://gitcode.com/chinabugotech/hutool'>\n        <img src=\"https://gitcode.com/chinabugotech/hutool/star/badge.svg\" alt=\"gitcode star\"/>\n    </a>\n    <a href=\"https://deepwiki.com/chinabugotech/hutool\">\n        <img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\">\n    </a>\n</p>\n\n<br/>\n<p align=\"center\">\n    <a href=\"https://qm.qq.com/q/YsLhyheHSA\">\n    <img alt=\"\" src=\"https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A7-667030118-orange\"/></a>\n</p>\n\n-------------------------------------------------------------------------------\n\n[**🌎English Documentation**](README-EN.md)\n\n-------------------------------------------------------------------------------\n\n## 📚简介\n\n`Hutool`是一个功能丰富且易用的**Java工具库**，通过诸多实用工具类的使用，旨在帮助开发者快速、便捷地完成各类开发任务。\n这些封装的工具涵盖了字符串、数字、集合、编码、日期、文件、IO、加密、数据库JDBC、JSON、HTTP客户端等一系列操作，\n可以满足各种不同的开发需求。\n\n### 🎁Hutool名称的由来\n\nHutool = Hu + tool，是原公司项目底层代码剥离后的开源库，“Hu”是公司名称的表示，tool表示工具。Hutool谐音“糊涂”，一方面简洁易懂，一方面寓意“难得糊涂”。\n\n### 🍺Hutool理念\n\n`Hutool`既是一个工具集，也是一个知识库，我们从不自诩代码原创，大多数工具类都是**搬运**而来，因此：\n\n- 你可以引入使用，也可以**拷贝**和修改使用，而**不必标注任何信息**，只是希望能把bug及时反馈回来。\n- 我们努力健全**中文**注释，为源码学习者提供良好地学习环境，争取做到人人都能看得懂。\n\n-------------------------------------------------------------------------------\n\n## 🛠️包含组件\n一个Java基础工具类，对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装，组成各种Util工具类，同时提供以下组件：\n\n| 模块                 |     介绍                                                                          |\n|--------------------|---------------------------------------------------------------------------------- |\n| hutool-aop         |     JDK动态代理封装，提供非IOC下的切面支持                                              |\n| hutool-bloomFilter |     布隆过滤，提供一些Hash算法的布隆过滤                                                |\n| hutool-cache       |     简单缓存实现                                                                     |\n| hutool-core        |     核心，包括Bean操作、日期、各种Util等                                               |\n| hutool-cron        |     定时任务模块，提供类Crontab表达式的定时任务                                          |\n| hutool-crypto      |     加密解密模块，提供对称、非对称和摘要算法封装                                          |\n| hutool-db          |     JDBC封装后的数据操作，基于ActiveRecord思想                                         |\n| hutool-dfa         |     基于DFA模型的多关键字查找                                                         |\n| hutool-extra       |     扩展模块，对第三方封装（模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等）            |\n| hutool-http        |     基于HttpUrlConnection的Http客户端封装                                            |\n| hutool-log         |     自动识别日志实现的日志门面                                                         |\n| hutool-script      |     脚本执行封装，例如Javascript                                                      |\n| hutool-setting     |     功能更强大的Setting配置文件和Properties封装                                        |\n| hutool-system      |     系统参数调用封装（JVM信息等）                                                      |\n| hutool-json        |     JSON实现                                                                       |\n| hutool-captcha     |     图片验证码实现                                                                   |\n| hutool-poi         |     针对POI中Excel和Word的封装                                                       |\n| hutool-socket      |     基于Java的NIO和AIO的Socket封装                                                   |\n| hutool-jwt         |     JSON Web Token (JWT)封装实现                                                    |\n| hutool-ai          |     AI大模型封装实现                                                    |\n\n可以根据需求对每个模块单独引入，也可以通过引入`hutool-all`方式引入所有模块。\n\n-------------------------------------------------------------------------------\n\n## 📝文档 \n\n[📘中文文档](https://doc.hutool.cn/pages/index/)\n\n[📘中文备用文档](https://plus.hutool.cn/)\n\n[📙参考API](https://plus.hutool.cn/apidocs/)\n\n[🎬视频介绍](https://www.bilibili.com/video/BV1bQ4y1M7d9?p=2)\n\n-------------------------------------------------------------------------------\n\n\n\n## 📦安装\n\n### 🍊Maven\n在项目的pom.xml的dependencies中加入以下内容:\n\n```xml\n<dependency>\n    <groupId>cn.hutool</groupId>\n    <artifactId>hutool-all</artifactId>\n    <version>5.8.44</version>\n</dependency>\n```\n\n### 🍐Gradle\n```\nimplementation 'cn.hutool:hutool-all:5.8.44'\n```\n\n### 📥下载jar\n\n点击以下链接，下载`hutool-all-X.X.X.jar`即可：\n\n- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.44/)\n\n> 🔔️注意\n> Hutool 5.x支持JDK8+，对Android平台没有测试，不能保证所有工具类或工具方法可用。\n> 如果你的项目使用JDK7，请使用Hutool 4.x版本（不再更新）\n\n### 🚽编译安装\n\n访问Hutool的Gitee主页：[https://gitee.com/chinabugotech/hutool](https://gitee.com/chinabugotech/hutool) 下载整个项目源码（v5-master或v5-dev分支都可）然后进入Hutool项目目录执行：\n\n```sh\n./hutool.sh install\n```\n\n然后就可以使用Maven引入了。\n\n-------------------------------------------------------------------------------\n\n## 🏗️添砖加瓦\n\n### 🎋分支说明\n\nHutool的源码分为两个分支，功能如下：\n\n| 分支       | 作用                                                          |\n|-----------|---------------------------------------------------------------|\n| v5-master | 主分支，release版本使用的分支，与中央库提交的jar一致，不接收任何pr或修改 |\n| v5-dev    | 开发分支，默认为下个版本的SNAPSHOT版本，接受修改或pr                 |\n\n### 🐞提供bug反馈或建议\n\n提交问题反馈请说明正在使用的JDK版本呢、Hutool版本和相关依赖库版本。\n\n- [Gitee issue](https://gitee.com/chinabugotech/hutool/issues)\n- [Github issue](https://github.com/chinabugotech/hutool/issues)\n- [Gitcode issue](https://gitcode.com/chinabugotech/hutool/issues)\n\n\n### 🧬贡献代码的步骤\n\n1. 在Gitee或者Github/Gitcode上fork项目到自己的repo\n2. 把fork过去的项目也就是你的项目clone到你的本地\n3. 修改代码（记得一定要修改v5-dev分支）\n4. commit后push到自己的库（v5-dev分支）\n5. 登录Gitee或Github/Gitcode在你首页可以看到一个 pull request 按钮，点击它，填写一些说明信息，然后提交即可。\n6. 等待维护者合并\n\n### 📐PR遵照的原则\n\nHutool欢迎任何人为Hutool添砖加瓦，贡献代码，不过维护者是一个强迫症患者，为了照顾病人，需要提交的pr（pull request）符合一些规范，规范如下：\n\n1. 注释完备，尤其每个新增的方法应按照Java文档规范标明方法说明、参数说明、返回值说明等信息，必要时请添加单元测试，如果愿意，也可以加上你的大名。\n2. Hutool的缩进按照Eclipse（~~不要跟我说IDEA多好用，维护者非常懒，学不会~~，IDEA真香，改了Eclipse快捷键后舒服多了）默认（tab）缩进，所以请遵守（不要和我争执空格与tab的问题，这是一个病人的习惯）。\n3. 新加的方法不要使用第三方库的方法，Hutool遵循无依赖原则（除非在extra模块中加方法工具）。\n4. 请pull request到`v5-dev`分支。Hutool在5.x版本后使用了新的分支：`v5-master`是主分支，表示已经发布中央库的版本，这个分支不允许pr，也不允许修改。\n5. 我们如果关闭了你的issue或pr，请不要诧异，这是我们保持问题处理整洁的一种方式，你依旧可以继续讨论，当有讨论结果时我们会重新打开。\n\n### 📖文档源码地址\n\n[文档源码地址](https://gitee.com/loolly_admin/hutool-doc-handy) 点击前往添砖加瓦\n\n-------------------------------------------------------------------------------\n\n## ⭐Star Hutool\n\n[![Stargazers over time](https://starchart.cc/chinabugotech/hutool.svg)](https://starchart.cc/chinabugotech/hutool)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions（支持的版本）\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 5.x.x   | :white_check_mark: |\n| 4.x.x   | :x:                |\n| 3.x.x   | :x:                |\n\n## Reporting a Vulnerability（报告漏洞）\n\n如果你发现有安全问题或漏洞，请发送邮件到`bugo@bugotech.cn`。\n\nTo report any found security issues or vulnerabilities, please send a mail to `bugo@bugotech.cn`."
  },
  {
    "path": "bin/check_dependency_updates.sh",
    "content": "#!/bin/bash\n\n#--------------------------------------\n# Check dependency, thanks to t-io\n#--------------------------------------\n\nmvn versions:display-dependency-updates\n"
  },
  {
    "path": "bin/cobertura.sh",
    "content": "#!/bin/bash\n\nexec mvn -T 1 cobertura:cobertura\n"
  },
  {
    "path": "bin/commit.sh",
    "content": "#!/bin/bash\n\ngit add .\ngit commit -am \"$1\"\n\nbin/push_dev.sh\n"
  },
  {
    "path": "bin/deploy.sh",
    "content": "#!/bin/bash\n\nmvn clean deploy -P release\n"
  },
  {
    "path": "bin/fast_install.sh",
    "content": "#!/bin/bash\n\nexec mvn -T 1C clean source:jar javadoc:javadoc install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dmaven.compile.fork=true\n"
  },
  {
    "path": "bin/install.sh",
    "content": "#!/bin/bash\n\nexec mvn -T 1C clean source:jar javadoc:javadoc install -Dmaven.test.skip=false -Dmaven.javadoc.skip=false\n"
  },
  {
    "path": "bin/javadoc.sh",
    "content": "#!/bin/bash\n\n#exec mvn javadoc:javadoc\n\n# 多模块聚合文档，生成在target/site/apidocs\nexec mvn javadoc:aggregate\n\nbin_home=\"$(dirname ${BASH_SOURCE[0]})\"\n\n# 拷贝自定义的index.html到聚合文档目录\ncp -vf $bin_home/../docs/apidocs/index.html $bin_home/../target/reports/apidocs/\n"
  },
  {
    "path": "bin/logo.sh",
    "content": "#!/bin/bash\n\necho '========================================'\necho '    __  __        __                 __ '\necho '   / / / /__  __ / /_ ____   ____   / / '\necho '  / /_/ // / / // __// __ \\ / __ \\ / /  '\necho ' / __  // /_/ // /_ / /_/ // /_/ // /   '\necho '/_/ /_/ \\____/ \\__/ \\____/ \\____//_/    '\necho ''\necho '-----------https://hutool.cn/-----------'\necho '========================================'\n"
  },
  {
    "path": "bin/package.sh",
    "content": "#!/bin/bash\n\nexec mvn -T 1C clean source:jar javadoc:javadoc package -Dmaven.test.skip=false -Dmaven.javadoc.skip=false\n"
  },
  {
    "path": "bin/push_dev.sh",
    "content": "#!/bin/bash\n\necho -e \"\\033[32mCheckout to v5-dev\\033[0m\"\ngit checkout v5-dev\n\necho -e \"\\033[32mPush to Github(origin) v5-dev\\033[0m\"\ngit push origin v5-dev\n\necho -e \"\\033[32mPush to Gitee v5-dev\\033[0m\"\ngit push osc v5-dev\n\necho -e \"\\033[32mPush to Gitcode v5-dev\\033[0m\"\ngit push gitcode v5-dev\n"
  },
  {
    "path": "bin/push_master.sh",
    "content": "#!/bin/bash\n\necho -e \"\\033[32mCheckout to v5-master\\033[0m\"\ngit checkout v5-master\n\necho -e \"\\033[32mMerge v5-dev branch\\033[0m\"\ngit merge v5-dev -m 'Prepare release'\n\necho -e \"\\033[32mPush to Github(origin) v5-master\\033[0m\"\ngit push origin v5-master\n\necho -e \"\\033[32mPush to Gitee v5-master\\033[0m\"\ngit push osc v5-master\n\necho -e \"\\033[32mPush to Gitcode v5-master\\033[0m\"\ngit push gitcode v5-master\n"
  },
  {
    "path": "bin/replaceVersion.sh",
    "content": "#!/bin/bash\n\n#-----------------------------------------------------------\n# 此脚本用于每次升级Hutool时替换相应位置的版本号\n#-----------------------------------------------------------\n\nset -o errexit\n\npwd=$(pwd)\n\necho \"当前路径：${pwd}\"\n\nif [ -n \"$1\" ];then\n    new_version=\"$1\"\n    old_version=`cat ${pwd}/bin/version.txt`\n    echo \"$old_version 替换为新版本 $new_version\"\nelse\n    # 参数错误，退出\n    echo \"ERROR: 请指定新版本！\"\n    exit\nfi\n\nif [ ! -n \"$old_version\" ]; then\n    echo \"ERROR: 旧版本不存在，请确认bin/version.txt中信息正确\"\n    exit\nfi\n\n# 替换README.md中的版本\nsed -i \"s/${old_version}/${new_version}/g\" $pwd/README.md\nsed -i \"s/${old_version}/${new_version}/g\" $pwd/README-EN.md\n# 替换docs/js/version.js中的版本\nsed -i \"s/${old_version}/${new_version}/g\" $pwd/docs/js/version.js\n\n# 保留新版本号\necho \"$new_version\" > $pwd/bin/version.txt\n"
  },
  {
    "path": "bin/simple_install.sh",
    "content": "#!/bin/bash\n\nexec mvn -T 1C clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true\n"
  },
  {
    "path": "bin/sync.sh",
    "content": "#!/bin/bash\n\ngit checkout v5-dev\ngit pull osc v5-dev\ngit pull origin v5-dev\ngit pull gitcode v5-dev\n"
  },
  {
    "path": "bin/test.sh",
    "content": "#!/bin/bash\n\nexec mvn test \n"
  },
  {
    "path": "bin/update_version.sh",
    "content": "#!/bin/bash\n\n#------------------------------------------------\n# 升级Hutool版本，包括：\n# 1. 升级pom.xml中的版本号\n# 2. 替换README.md和docs中的版本号\n#------------------------------------------------\n\nif [ ! -n \"$1\" ]; then\n        echo \"ERROR: 新版本不存在，请指定参数1\"\n        exit\nfi\n\n# 替换所有模块pom.xml中的版本\nmvn versions:set -DnewVersion=$1\n\n# 不带-SNAPSHOT的版本号，用于替换其它地方\nversion=${1%-SNAPSHOT}\n\n# 替换其它地方的版本\n$(pwd)/bin/replaceVersion.sh \"$version\"\n"
  },
  {
    "path": "bin/version.txt",
    "content": "5.8.44\n"
  },
  {
    "path": "docs/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/apidocs/index.html",
    "content": "<!DOCTYPE html>\r\n<html lang=\"zh\">\r\n\r\n<head>\r\n\t<meta charset=\"UTF-8\">\r\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n\t<title>Document</title>\r\n\t<style>\r\n\t\t* {\r\n\t\t\tpadding: 0;\r\n\t\t\tmargin: 0;\r\n\t\t\tborder: 0;\r\n\t\t}\r\n\r\n\t\t.left {\r\n\t\t\tposition: absolute;\r\n\t\t\twidth: 20%;\r\n\t\t\theight: 100%;\r\n\t\t}\r\n\r\n\t\t.top {\r\n\t\t\theight: 10%;\r\n\t\t\ttext-align: center;\r\n\t\t}\r\n\r\n\t\t.bottom {\r\n\t\t\theight: 90%;\r\n\t\t}\r\n\r\n\t\t.content {\r\n\t\t\tposition: absolute;\r\n\t\t\twidth: 80%;\r\n\t\t\theight: 100%;\r\n\t\t\tmargin-left: 20%;\r\n\t\t}\r\n\r\n\t\tiframe {\r\n\t\t\twidth: 100%;\r\n\t\t\tdisplay: block;\r\n\t\t}\r\n\t</style>\r\n\t<!-- 百度统计 -->\r\n\t<script>\r\n\t\tvar _hmt = _hmt || [];\r\n\t\t(function () {\r\n\t\t\tvar hm = document.createElement(\"script\");\r\n\t\t\thm.src = \"https://hm.baidu.com/hm.js?a76bc7a2d60207f04195af1c51cdb9ba\";\r\n\t\t\tvar s = document.getElementsByTagName(\"script\")[0];\r\n\t\t\ts.parentNode.insertBefore(hm, s);\r\n\t\t})();\r\n\t</script>\r\n</head>\r\n\r\n<body>\r\n<div class=\"left\">\r\n\t<div class=\"top\">\r\n\t\t<a href=\"https://hutool.cn\" target=\"_blank\">\r\n\t\t\t<img src=\"https://plus.hutool.cn/images/hutool.svg\" style=\"height: 100%; width: 100%;\" alt=\"hutool\">\r\n\t\t</a>\r\n\t</div>\r\n\t<div class=\"bottom\">\r\n\t\t<iframe src=\"overview-frame.html\" name=\"packageListFrame\" target=\"packageFrame\" title=\"所有程序包\"\r\n\t\t\t\tstyle=\"height: 30%;\"></iframe>\r\n\t\t<iframe src=\"allclasses-frame.html\" name=\"packageFrame\" target=\"classFrame\" title=\"所有类和接口 (除了非静态嵌套类型)\"\r\n\t\t\t\tstyle=\"height: 70%;\"></iframe>\r\n\t</div>\r\n</div>\r\n<div class=\"content\">\r\n\t<iframe src=\"overview-summary.html\" name=\"classFrame\" title=\"程序包, 类和接口说明\" style=\"height: 100%;\"></iframe>\r\n</div>\r\n<script type=\"text/javascript\" charset=\"UTF-8\" src=\"https://cdn.wwads.cn/js/makemoney.js\" async></script>\r\n<!-- wwads -->\r\n<div style=\"position: fixed; right: 16px; bottom: 45px; z-index: 99999;\">\r\n\t<div class=\"wwads-cn wwads-vertical\" data-id=\"126\" style=\"max-width:150px\"></div>\r\n</div>\r\n</body>\r\n\r\n</html>\r\n"
  },
  {
    "path": "docs/js/version.js",
    "content": "var version = '5.8.44'"
  },
  {
    "path": "hutool-ai/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-ai</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool AI大模型封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.ai</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-http</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/AIException.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 异常处理类\n */\npublic class AIException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param e 异常\n\t */\n\tpublic AIException(final Throwable e) {\n\t\tsuper(e);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param message 消息\n\t */\n\tpublic AIException(final String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param messageTemplate 消息模板\n\t * @param params          参数\n\t */\n\tpublic AIException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param message 消息\n\t * @param cause   被包装的子异常\n\t */\n\tpublic AIException(final String message, final Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param message            消息\n\t * @param cause              被包装的子异常\n\t * @param enableSuppression  是否启用抑制\n\t * @param writableStackTrace 堆栈跟踪是否应该是可写的\n\t */\n\tpublic AIException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {\n\t\tsuper(message, cause, enableSuppression, writableStackTrace);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param throwable           被包装的子异常\n\t * @param messageTemplate 消息模板\n\t * @param params          参数\n\t */\n\tpublic AIException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.AIServiceProvider;\nimport cn.hutool.core.util.ServiceLoaderUtil;\n\nimport java.util.Map;\nimport java.util.ServiceLoader;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 创建AIModelService的工厂类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class AIServiceFactory {\n\n\tprivate static final Map<String, AIServiceProvider> providers = new ConcurrentHashMap<>();\n\n\t// 加载所有 AIModelProvider 实现类\n\tstatic {\n\t\tfinal ServiceLoader<AIServiceProvider> loader = ServiceLoaderUtil.load(AIServiceProvider.class);\n\t\tfor (final AIServiceProvider provider : loader) {\n\t\t\tproviders.put(provider.getServiceName().toLowerCase(), provider);\n\t\t}\n\t}\n\n\t/**\n\t * 获取AI服务\n\t *\n\t * @param config AIConfig配置\n\t * @return AI服务实例\n\t * @since 5.8.38\n\t */\n\tpublic static AIService getAIService(final AIConfig config) {\n\t\treturn getAIService(config, AIService.class);\n\t}\n\n\t/**\n\t * 获取AI服务\n\t *\n\t * @param config AIConfig配置\n\t * @param clazz AI服务类\n\t * @return clazz对应的AI服务类实例\n\t * @since 5.8.38\n\t * @param <T> AI服务类\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends AIService> T getAIService(final AIConfig config, final Class<T> clazz) {\n\t\tfinal AIServiceProvider provider = providers.get(config.getModelName().toLowerCase());\n\t\tif (provider == null) {\n\t\t\tthrow new IllegalArgumentException(\"Unsupported model: \" + config.getModelName());\n\t\t}\n\n\t\tfinal AIService service = provider.create(config);\n\t\tif (!clazz.isInstance(service)) {\n\t\t\tthrow new AIException(\"Model service is not of type: \" + clazz.getSimpleName());\n\t\t}\n\n\t\treturn (T) service;\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.ai.model.deepseek.DeepSeekService;\nimport cn.hutool.ai.model.doubao.DoubaoService;\nimport cn.hutool.ai.model.gemini.GeminiService;\nimport cn.hutool.ai.model.grok.GrokService;\nimport cn.hutool.ai.model.hutool.HutoolService;\nimport cn.hutool.ai.model.openai.OpenaiService;\n\nimport java.util.List;\n\n/**\n * AI工具类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class AIUtil {\n\n\t/**\n\t * 获取AI模型服务，每个大模型提供的功能会不一样，可以调用此方法指定不同AI服务类，调用不同的功能\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @param clazz  AI模型服务类\n\t * @return AIModelService的实现类实例\n\t * @since 5.8.38\n\t * @param <T> AIService实现类\n\t */\n\tpublic static <T extends AIService> T getAIService(final AIConfig config, final Class<T> clazz) {\n\t\treturn AIServiceFactory.getAIService(config, clazz);\n\t}\n\n\t/**\n\t * 获取AI模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return AIModelService 其中只有公共方法\n\t * @since 5.8.38\n\t */\n\tpublic static AIService getAIService(final AIConfig config) {\n\t\treturn getAIService(config, AIService.class);\n\t}\n\n\t/**\n\t * 获取Hutool-AI服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return HutoolService\n\t * @since 5.8.39\n\t */\n\tpublic static HutoolService getHutoolService(final AIConfig config) {\n\t\treturn getAIService(config, HutoolService.class);\n\t}\n\n\t/**\n\t * 获取DeepSeek模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return DeepSeekService\n\t * @since 5.8.38\n\t */\n\tpublic static DeepSeekService getDeepSeekService(final AIConfig config) {\n\t\treturn getAIService(config, DeepSeekService.class);\n\t}\n\n\t/**\n\t * 获取Doubao模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return DoubaoService\n\t * @since 5.8.38\n\t */\n\tpublic static DoubaoService getDoubaoService(final AIConfig config) {\n\t\treturn getAIService(config, DoubaoService.class);\n\t}\n\n\t/**\n\t * 获取Grok模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return GrokService\n\t * @since 5.8.38\n\t */\n\tpublic static GrokService getGrokService(final AIConfig config) {\n\t\treturn getAIService(config, GrokService.class);\n\t}\n\n\t/**\n\t * 获取Openai模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return OpenAIService\n\t * @since 5.8.38\n\t */\n\tpublic static OpenaiService getOpenAIService(final AIConfig config) {\n\t\treturn getAIService(config, OpenaiService.class);\n\t}\n\n\t/**\n\t * 获取Gemini模型服务\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @return GeminiService\n\t * @since 5.8.43\n\t */\n\tpublic static GeminiService getGeminiService(final AIConfig config) {\n\t\treturn getAIService(config, GeminiService.class);\n\t}\n\n\t/**\n\t * AI大模型对话功能\n\t *\n\t * @param config 创建的AI服务模型的配置\n\t * @param prompt 需要对话的内容\n\t * @return AI模型返回的Response响应字符串\n\t * @since 5.8.38\n\t */\n\tpublic static String chat(final AIConfig config, final String prompt) {\n\t\treturn getAIService(config).chat(prompt);\n\t}\n\n\t/**\n\t * AI大模型对话功能\n\t *\n\t * @param config   创建的AI服务模型的配置\n\t * @param messages 由目前为止的对话组成的消息列表，可以设置role，content。详细参考官方文档\n\t * @return AI模型返回的Response响应字符串\n\t * @since 5.8.38\n\t */\n\tpublic static String chat(final AIConfig config, final List<Message> messages) {\n\t\treturn getAIService(config).chat(messages);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/ModelName.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai;\n\n/**\n * 模型厂商的名称（不指具体的模型）\n *\n * @author elichow\n * @since 5.8.38\n */\npublic enum ModelName {\n\n\t/**\n\t * hutool\n\t */\n\tHUTOOL(\"hutool\"),\n\t/**\n\t * deepSeek\n\t */\n\tDEEPSEEK(\"deepSeek\"),\n\t/**\n\t * openai\n\t */\n\tOPENAI(\"openai\"),\n\t/**\n\t * doubao\n\t */\n\tDOUBAO(\"doubao\"),\n\t/**\n\t * grok\n\t */\n\tGROK(\"grok\"),\n\t/**\n\t * ollama\n\t */\n\tOLLAMA(\"ollama\"),\n\t/**\n\t * gemini\n\t */\n\tGEMINI(\"gemini\");\n\n\tprivate final String value;\n\n\tModelName(final String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @return 值\n\t */\n\tpublic String getValue() {\n\t\treturn value;\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/Models.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai;\n\n/**\n * 各模型厂商包含的model（指具体的模型）\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class Models {\n\n\n\t// Hutool的模型\n\tpublic enum Hutool {\n\t\tHUTOOL(\"hutool\");\n\n\t\tprivate final String model;\n\n\t\tHutool(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// DeepSeek的模型\n\tpublic enum DeepSeek {\n\t\tDEEPSEEK_CHAT(\"deepseek-chat\"),\n\t\tDEEPSEEK_REASONER(\"deepseek-reasoner\");\n\n\t\tprivate final String model;\n\n\t\tDeepSeek(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// Openai的模型\n\tpublic enum Openai {\n\t\tGPT_4_5_PREVIEW(\"gpt-4.5-preview\"),\n\t\tGPT_4O(\"gpt-4o\"),\n\t\tCHATGPT_4O_LATEST(\"chatgpt-4o-latest\"),\n\t\tGPT_4O_MINI(\"gpt-4o-mini\"),\n\t\tO1(\"o1\"),\n\t\tO1_MINI(\"o1-mini\"),\n\t\tO1_PREVIEW(\"o1-preview\"),\n\t\tO3_MINI(\"o3-mini\"),\n\t\tGPT_4O_REALTIME_PREVIEW(\"gpt-4o-realtime-preview\"),\n\t\tGPT_4O_MINI_REALTIME_PREVIEW(\"gpt-4o-mini-realtime-preview\"),\n\t\tGPT_4O_AUDIO_PREVIEW(\"gpt-4o-audio-preview\"),\n\t\tGPT_4O_MINI_AUDIO_PREVIEW(\"gpt-4o-mini-audio-preview\"),\n\t\tGPT_4_TURBO(\"gpt-4-turbo\"),\n\t\tGPT_4_TURBO_PREVIEW(\"gpt-4-turbo-preview\"),\n\t\tGPT_4(\"gpt-4\"),\n\t\tGPT_3_5_TURBO_0125(\"gpt-3.5-turbo-0125\"),\n\t\tGPT_3_5_TURBO(\"gpt-3.5-turbo\"),\n\t\tGPT_3_5_TURBO_1106(\"gpt-3.5-turbo-1106\"),\n\t\tGPT_3_5_TURBO_INSTRUCT(\"gpt-3.5-turbo-instruct\"),\n\t\tDALL_E_3(\"dall-e-3\"),\n\t\tDALL_E_2(\"dall-e-2\"),\n\t\tTTS_1(\"tts-1\"),\n\t\tTTS_1_HD(\"tts-1-hd\"),\n\t\tWHISPER_1(\"whisper-1\"),\n\t\tTEXT_EMBEDDING_3_LARGE(\"text-embedding-3-large\"),\n\t\tTEXT_EMBEDDING_3_SMALL(\"text-embedding-3-small\"),\n\t\tTEXT_EMBEDDING_ADA_002(\"text-embedding-ada-002\"),\n\t\tOMNI_MODERATION_LATEST(\"omni-moderation-latest\"),\n\t\tOMNI_MODERATION_2024_09_26(\"omni-moderation-2024-09-26\"),\n\t\tTEXT_MODERATION_LATEST(\"text-moderation-latest\"),\n\t\tTEXT_MODERATION_STABLE(\"text-moderation-stable\"),\n\t\tTEXT_MODERATION_007(\"text-moderation-007\"),\n\t\tBABBAGE_002(\"babbage-002\"),\n\t\tDAVINCI_002(\"davinci-002\");\n\n\t\tprivate final String model;\n\n\t\tOpenai(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// Doubao的模型\n\tpublic enum Doubao {\n\t\tDOUBAO_1_5_PRO_32K(\"doubao-1.5-pro-32k-250115\"),\n\t\tDOUBAO_1_5_PRO_256K(\"doubao-1.5-pro-256k-250115\"),\n\t\tDOUBAO_1_5_LITE_32K(\"doubao-1.5-lite-32k-250115\"),\n\t\tDOUBAO_PRO_4K_240515(\"doubao-pro-4k-240515\"),\n\t\tDOUBAO_PRO_4K_CHARACTER_240728(\"doubao-pro-4k-character-240728\"),\n\t\tDOUBAO_PRO_4K_FUNCTIONCALL_240615(\"doubao-pro-4k-functioncall-240615\"),\n\t\tDOUBAO_PRO_4K_BROWSING_240524(\"doubao-pro-4k-browsing-240524\"),\n\t\tDOUBAO_PRO_32K_241215(\"doubao-pro-32k-241215\"),\n\t\tDOUBAO_PRO_32K_FUNCTIONCALL_241028(\"doubao-pro-32k-functioncall-241028\"),\n\t\tDOUBAO_PRO_32K_BROWSING_241115(\"doubao-pro-32k-browsing-241115\"),\n\t\tDOUBAO_PRO_32K_CHARACTER_241215(\"doubao-pro-32k-character-241215\"),\n\t\tDOUBAO_PRO_128K_240628(\"doubao-pro-128k-240628\"),\n\t\tDOUBAO_PRO_256K_240828(\"doubao-pro-256k-240828\"),\n\t\tDOUBAO_LITE_4K_240328(\"doubao-lite-4k-240328\"),\n\t\tDOUBAO_LITE_4K_PRETRAIN_CHARACTER_240516(\"doubao-lite-4k-pretrain-character-240516\"),\n\t\tDOUBAO_LITE_32K_240828(\"doubao-lite-32k-240828\"),\n\t\tDOUBAO_LITE_32K_CHARACTER_241015(\"doubao-lite-32k-character-241015\"),\n\t\tDOUBAO_LITE_128K_240828(\"doubao-lite-128k-240828\"),\n\t\tDEEPSEEK_R1(\"deepseek-r1-250120\"),\n\t\tDEEPSEEK_R1_DISTILL_QWEN_32B(\"deepseek-r1-distill-qwen-32b-250120\"),\n\t\tDEEPSEEK_R1_DISTILL_QWEN_7B(\"deepseek-r1-distill-qwen-7b-250120\"),\n\t\tDEEPSEEK_V3(\"deepseek-v3-241226\"),\n\t\tMOONSHOT_V1_8K(\"moonshot-v1-8k\"),\n\t\tMOONSHOT_V1_32K(\"moonshot-v1-32k\"),\n\t\tMOONSHOT_V1_128K(\"moonshot-v1-128k\"),\n\t\tCHATGLM3_130B_FC(\"chatglm3-130b-fc-v1.0\"),\n\t\tCHATGLM3_130_FIN(\"chatglm3-130-fin-v1.0-update\"),\n\t\tMISTRAL_7B(\"mistral-7b-instruct-v0.2\"),\n\t\tDOUBAO_1_5_VISION_PRO_32K(\"doubao-1.5-vision-pro-32k-250115\"),\n\t\tDOUBAO_VISION_PRO_32K(\"doubao-vision-pro-32k-241008\"),\n\t\tDOUBAO_VISION_LITE_32K(\"doubao-vision-lite-32k-241015\"),\n\t\tDOUBAO_EMBEDDING_LARGE(\"doubao-embedding-large-text-240915\"),\n\t\tDOUBAO_EMBEDDING_TEXT_240715(\"doubao-embedding-text-240715\"),\n\t\tDOUBAO_EMBEDDING_VISION(\"doubao-embedding-vision-241215\"),\n\t\tDOUBAO_SEEDREAM_3_0_T2I(\"doubao-seedream-3-0-t2i-250415\"),\n\t\tDOUBAO_SEEDDANCE_1_0_LITE_T2V(\"doubao-seedance-1-0-lite-t2v-250428\"),\n\t\tDOUBAO_SEEDDANCE_1_0_lite_I2V(\"doubao-seedance-1-0-lite-i2v-250428\"),\n\t\tWAN2_1_14B_T2V(\"wan2-1-14b-t2v-250225\"),\n\t\tWAN2_1_14B_I2V(\"wan2-1-14b-i2v-250225\");\n\n\t\tprivate final String model;\n\n\t\tDoubao(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// Grok的模型\n\tpublic enum Grok {\n\t\tGROK_3_BETA_LATEST(\"grok-3-beta\"),\n\t\tGROK_3_BETA(\"grok-3-beta\"),\n\t\tGROK_3(\"grok-3-beta\"),\n\t\tGROK_3_MINI_FAST_LATEST(\"grok-3-mini-fast-beta\"),\n\t\tGROK_3_MINI_FAST_BETA(\"grok-3-mini-fast-beta\"),\n\t\tGROK_3_MINI_FAST(\"grok-3-mini-fast-beta\"),\n\t\tGROK_3_FAST_LATEST(\"grok-3-fast-beta\"),\n\t\tGROK_3_FAST_BETA(\"grok-3-fast-beta\"),\n\t\tGROK_3_FAST(\"grok-3-fast-beta\"),\n\t\tGROK_3_MINI_LATEST(\"grok-3-mini-beta\"),\n\t\tGROK_3_MINI_BETA(\"grok-3-mini-beta\"),\n\t\tGROK_3_MINI(\"grok-3-mini-beta\"),\n\t\tGROK_2_IMAGE_LATEST(\"grok-2-image-1212\"),\n\t\tGROK_2_IMAGE(\"grok-2-image-1212\"),\n\t\tGROK_2_IMAGE_1212(\"grok-2-image-1212\"),\n\t\tGROK_2_LATEST(\"grok-2-1212\"),\n\t\tGROK_2(\"grok-2-1212\"),\n\t\tGROK_2_1212(\"grok-2-1212\"),\n\t\tGROK_2_VISION_1212(\"grok-2-vision-1212\"),\n\t\tGROK_BETA(\"grok-beta\"),\n\t\tGROK_VISION_BETA(\"grok-vision-beta\");\n\n\t\tprivate final String model;\n\n\t\tGrok(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// Ollama的模型\n\tpublic enum Ollama {\n\t\tQWEN3_32B(\"qwen3:32b\");\n\n\t\tprivate final String model;\n\n\t\tOllama(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n\t// Gemini的模型\n\tpublic enum Gemini {\n\t\tGEMINI_2_5_PRO_PREVIEW_TTS(\"gemini-2.5-pro-preview-tts\"),\n\t\tGEMINI_2_5_FLASH_PREVIEW_TTS(\"gemini-2.5-flash-preview-tts\"),\n\t\tVEO_2_0_GENERATE_001(\"veo-2.0-generate-001\"),\n\t\tVEO_3_0_FAST_GENERATE_001(\"veo-3.0-fast-generate-001\"),\n\t\tVEO_3_0_GENERATE_001(\"veo-3.0-generate-001\"),\n\t\tVEO_3_1_FAST_GENERATE_PREVIEW(\"veo-3.1-fast-generate-preview\"),\n\t\tVEO_3_1_GENERATE_PREVIEW(\"veo-3.1-generate-preview\"),\n\t\tIMAGEN_4_0_GENERATE_001(\"imagen-4.0-generate-001\"),\n\t\tIMAGEN_4_0_ULTRA_GENERATE_001(\"imagen-4.0-ultra-generate-001\"),\n\t\tIMAGEN_4_0_FAST_GENERATE_001(\"imagen-4.0-fast-generate-001\"),\n\t\tIMAGEN_3_0_GENERATE_002(\"imagen-3.0-generate-002\"),\n\t\tGEMINI_3_PRO_PREVIEW(\"gemini-3-pro-preview\"),\n\t\tGEMINI_3_FLASH(\"gemini-3-flash\"),\n\t\tGEMINI_2_5_PRO(\"gemini-2.5-pro\"),\n\t\tGEMINI_2_5_FLASH(\"gemini-2.5-flash\"),\n\t\tGEMINI_2_5_FLASH_LITE(\"gemini-2.5-flash-lite\"),\n\t\tGEMINI_2_5_FLASH_IMAGE(\"gemini-2.5-flash-image\"),\n\t\tGEMINI_2_0_FLASH(\"gemini-2.0-flash\"),\n\t\tGEMINI_2_0_FLASH_LITE(\"gemini-2.0-flash-lite\"),\n\t\tGEMINI_2_0_PRO_EXP(\"gemini-2.0-pro-exp\"),\n\t\tGEMINI_1_5_FLASH(\"gemini-1.5-flash\"),\n\t\tGEMINI_1_5_PRO(\"gemini-1.5-pro\"),\n\t\tGEMINI_1_5_FLASH_8B(\"gemini-1.5-flash-8b\"),\n\t\tGEMINI_1_0_PRO(\"gemini-1.0-pro\");\n\n\t\tprivate final String model;\n\n\t\tGemini(String model) {\n\t\t\tthis.model = model;\n\t\t}\n\n\t\tpublic String getModel() {\n\t\t\treturn model;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport java.net.Proxy;\nimport java.util.Map;\n\n/**\n * AI配置类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface AIConfig {\n\n\t/**\n\t * 获取模型（厂商）名称\n\t *\n\t * @return 模型（厂商）名称\n\t * @since 5.8.38\n\t */\n\tdefault String getModelName() {\n\t\treturn this.getClass().getSimpleName();\n\t}\n\n\t/**\n\t * 设置apiKey\n\t *\n\t * @param apiKey apiKey\n\t * @since 5.8.38\n\t */\n\tvoid setApiKey(String apiKey);\n\n\t/**\n\t * 获取apiKey\n\t *\n\t * @return apiKey\n\t * @since 5.8.38\n\t */\n\tString getApiKey();\n\n\t/**\n\t * 设置apiUrl\n\t *\n\t * @param apiUrl api请求地址\n\t * @since 5.8.38\n\t */\n\tvoid setApiUrl(String apiUrl);\n\n\t/**\n\t * 获取apiUrl\n\t *\n\t * @return apiUrl\n\t * @since 5.8.38\n\t */\n\tString getApiUrl();\n\n\t/**\n\t * 设置model\n\t *\n\t * @param model model\n\t * @since 5.8.38\n\t */\n\tvoid setModel(String model);\n\n\t/**\n\t * 返回model\n\t *\n\t * @return model\n\t * @since 5.8.38\n\t */\n\tString getModel();\n\n\t/**\n\t * 设置动态参数\n\t *\n\t * @param key   参数字段\n\t * @param value 参数值\n\t * @since 5.8.38\n\t */\n\tvoid putAdditionalConfigByKey(String key, Object value);\n\n\t/**\n\t * 获取动态参数\n\t *\n\t * @param key 参数字段\n\t * @return 参数值\n\t * @since 5.8.38\n\t */\n\tObject getAdditionalConfigByKey(String key);\n\n\t/**\n\t * 获取动态参数列表\n\t *\n\t * @return 参数列表Map\n\t * @since 5.8.38\n\t */\n\tMap<String, Object> getAdditionalConfigMap();\n\n\t/**\n\t * 设置连接超时时间\n\t *\n\t * @param timeout 连接超时时间\n\t * @since 5.8.39\n\t */\n\tvoid setTimeout(int timeout);\n\n\t/**\n\t * 获取连接超时时间\n\t *\n\t * @return timeout\n\t * @since 5.8.39\n\t */\n\tint getTimeout();\n\n\t/**\n\t * 设置读取超时时间\n\t *\n\t * @param readTimeout 连接超时时间\n\t * @since 5.8.39\n\t */\n\tvoid setReadTimeout(int readTimeout);\n\n\t/**\n\t * 获取读取超时时间\n\t *\n\t * @return readTimeout\n\t * @since 5.8.39\n\t */\n\tint getReadTimeout();\n\n\t/**\n\t * 获取是否使用代理\n\t *\n\t * @return hasProxy\n\t * @since 5.8.42\n\t */\n\tboolean getHasProxy();\n\n\t/**\n\t * 设置是否使用代理\n\t *\n\t * @param hasProxy 是否使用代理\n\t * @since 5.8.42\n\t */\n\tvoid setHasProxy(boolean hasProxy);\n\n\t/**\n\t * 获取代理配置\n\t *\n\t * @return proxy\n\t * @since 5.8.42\n\t */\n\tProxy getProxy();\n\n\t/**\n\t * 设置代理配置\n\t *\n\t * @param proxy 连接超时时间\n\t * @since 5.8.42\n\t */\n\tvoid setProxy(Proxy proxy);\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport java.lang.reflect.Constructor;\nimport java.net.Proxy;\n\n/**\n * 用于AIConfig的创建，创建同时支持链式设置参数\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class AIConfigBuilder {\n\n\tprivate final AIConfig config;\n\n\t/**\n\t * 构造\n\t *\n\t * @param modelName 模型厂商的名称（注意不是指具体的模型）\n\t */\n\tpublic AIConfigBuilder(final String modelName) {\n\t\ttry {\n\t\t\t// 获取配置类\n\t\t\tfinal Class<? extends AIConfig> configClass = AIConfigRegistry.getConfigClass(modelName);\n\t\t\tif (configClass == null) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported model: \" + modelName);\n\t\t\t}\n\n\t\t\t// 使用反射创建实例\n\t\t\tfinal Constructor<? extends AIConfig> constructor = configClass.getDeclaredConstructor();\n\t\t\tconfig = constructor.newInstance();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new RuntimeException(\"Failed to create AIConfig instance\", e);\n\t\t}\n\t}\n\n\t/**\n\t * 设置apiKey\n\t *\n\t * @param apiKey apiKey\n\t * @return config\n\t * @since 5.8.38\n\t */\n\tpublic synchronized AIConfigBuilder setApiKey(final String apiKey) {\n\t\tif (apiKey != null) {\n\t\t\tconfig.setApiKey(apiKey);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置AI模型请求API接口的地址，不设置为默认值\n\t *\n\t * @param apiUrl API接口地址\n\t * @return config\n\t * @since 5.8.38\n\t */\n\tpublic synchronized AIConfigBuilder setApiUrl(final String apiUrl) {\n\t\tif (apiUrl != null) {\n\t\t\tconfig.setApiUrl(apiUrl);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置具体的model，不设置为默认值\n\t *\n\t * @param model 具体model的名称\n\t * @return config\n\t * @since 5.8.38\n\t */\n\tpublic synchronized AIConfigBuilder setModel(final String model) {\n\t\tif (model != null) {\n\t\t\tconfig.setModel(model);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 动态设置Request请求体中的属性字段，每个模型功能支持的字段请参照对应的官方文档\n\t *\n\t * @param key   Request中的支持的属性名\n\t * @param value 设置的属性值\n\t * @return config\n\t * @since 5.8.38\n\t */\n\tpublic AIConfigBuilder putAdditionalConfig(final String key, final Object value) {\n\t\tif (value != null) {\n\t\t\tconfig.putAdditionalConfigByKey(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时时间，不设置为默认值\n\t *\n\t * @param timeout 超时时间\n\t * @return config\n\t * @since 5.8.39\n\t * @deprecated 请使用 {@link #setTimeout(int)}\n\t */\n\t@Deprecated\n\tpublic AIConfigBuilder setTimout(final int timeout) {\n\t\treturn setTimeout(timeout);\n\t}\n\n\t/**\n\t * 设置连接超时时间，不设置为默认值\n\t *\n\t * @param timeout 超时时间\n\t * @return config\n\t * @since 5.8.41\n\t */\n\tpublic synchronized AIConfigBuilder setTimeout(final int timeout) {\n\t\tif (timeout > 0) {\n\t\t\tconfig.setTimeout(timeout);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置读取超时时间，不设置为默认值\n\t *\n\t * @param readTimout 取超时时间\n\t * @return config\n\t * @since 5.8.39\n\t * @deprecated 请使用 {@link #setReadTimeout(int)}\n\t */\n\t@Deprecated\n\tpublic AIConfigBuilder setReadTimout(final int readTimout) {\n\t\treturn setReadTimeout(readTimout);\n\t}\n\n\t/**\n\t * 设置读取超时时间，不设置为默认值\n\t *\n\t * @param readTimeout 取超时时间\n\t * @return config\n\t * @since 5.8.41\n\t */\n\tpublic synchronized AIConfigBuilder setReadTimeout(final int readTimeout) {\n\t\tif (readTimeout > 0) {\n\t\t\tconfig.setReadTimeout(readTimeout);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置代理\n\t *\n\t * @param proxy 取超时时间\n\t * @return config\n\t * @since 5.8.42\n\t */\n\tpublic synchronized AIConfigBuilder setProxy(final Proxy proxy) {\n\t\tif (null != proxy) {\n\t\t\tconfig.setHasProxy(true);\n\t\t\tconfig.setProxy(proxy);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回config实例\n\t *\n\t * @return config\n\t * @since 5.8.38\n\t */\n\tpublic AIConfig build() {\n\t\treturn config;\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport cn.hutool.core.util.ServiceLoaderUtil;\n\nimport java.util.Map;\nimport java.util.ServiceLoader;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * AIConfig实现类的加载器\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class AIConfigRegistry {\n\n\tprivate static final Map<String, Class<? extends AIConfig>> configClasses = new ConcurrentHashMap<>();\n\n\t// 加载所有 AIConfig 实现类\n\tstatic {\n\t\tfinal ServiceLoader<AIConfig> loader = ServiceLoaderUtil.load(AIConfig.class);\n\t\tfor (final AIConfig config : loader) {\n\t\t\tconfigClasses.put(config.getModelName().toLowerCase(), config.getClass());\n\t\t}\n\t}\n\n\t/**\n\t * 根据模型名称获取AIConfig实现类\n\t *\n\t * @param modelName 模型名称\n\t * @return AIConfig实现类\n\t */\n\tpublic static Class<? extends AIConfig> getConfigClass(final String modelName) {\n\t\treturn configClasses.get(modelName.toLowerCase());\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * 模型公共的API功能，特有的功能在model.xx.XXService下定义\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface AIService {\n\n\t/**\n\t * 对话\n\t *\n\t * @param prompt user题词\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chat(String prompt){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn chat(messages);\n\t}\n\n\t/**\n\t * 对话-SSE流式输出\n\t * @param prompt user题词\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chat(String prompt, final Consumer<String> callback){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\tchat(messages, callback);\n\t}\n\n\t/**\n\t * 对话\n\t *\n\t * @param messages 由目前为止的对话组成的消息列表，可以设置role，content。详细参考官方文档\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString chat(final List<Message> messages);\n\n\n\t/**\n\t * 对话-SSE流式输出\n\t * @param messages 由目前为止的对话组成的消息列表，可以设置role，content。详细参考官方文档\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chat(final List<Message> messages, final Consumer<String> callback);\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/AIServiceProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\n/**\n * 用于加载AI服务,每一个通过SPI创建的AI服务都要实现此接口\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface AIServiceProvider {\n\n\t/**\n\t * 获取AI服务名称\n\t *\n\t * @return AI服务名称\n\t * @since 5.8.38\n\t */\n\tString getServiceName();\n\n\t/**\n\t * 创建AI服务实例\n\t *\n\t * @param config AIConfig配置\n\t * @param <T>    AIService实现类\n\t * @return AI服务实例\n\t * @since 5.8.38\n\t */\n\t<T extends AIService> T create(final AIConfig config);\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport cn.hutool.ai.AIException;\nimport cn.hutool.http.*;\nimport cn.hutool.json.JSONUtil;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\nimport static cn.hutool.core.thread.GlobalThreadPool.execute;\n\n/**\n * 基础AIService，包含基公共参数和公共方法\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class BaseAIService {\n\n\tprotected final AIConfig config;\n\n\t/**\n\t * 构造方法\n\t *\n\t * @param config AI配置\n\t */\n\tpublic BaseAIService(final AIConfig config) {\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * 发送Get请求\n\t * @param endpoint 请求节点\n\t * @return 请求响应\n\t */\n\tprotected HttpResponse sendGet(final String endpoint) {\n\t\t//链式构建请求\n\t\ttry {\n\t\t\t//设置超时3分钟\n\t\t\tHttpRequest httpRequest = HttpRequest.get(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.header(Header.AUTHORIZATION, \"Bearer \" + config.getApiKey())\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\treturn httpRequest.execute();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new AIException(\"Failed to send GET request: \" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t/**\n\t * 发送Post请求\n\t * @param endpoint 请求节点\n\t * @param paramJson 请求参数json\n\t * @return 请求响应\n\t */\n\tprotected HttpResponse sendPost(final String endpoint, final String paramJson) {\n\t\t//链式构建请求\n\t\ttry {\n\t\t\tHttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.CONTENT_TYPE, \"application/json\")\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.header(Header.AUTHORIZATION, \"Bearer \" + config.getApiKey())\n\t\t\t\t.body(paramJson)\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\treturn httpRequest.execute();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new AIException(\"Failed to send POST request：\" + e.getMessage(), e);\n\t\t}\n\n\t}\n\n\t/**\n\t * 发送表单请求\n\t * @param endpoint 请求节点\n\t * @param paramMap 请求参数map\n\t * @return 请求响应\n\t */\n\tprotected HttpResponse sendFormData(final String endpoint, final Map<String, Object> paramMap) {\n\t\t//链式构建请求\n\t\ttry {\n\t\t\t//设置超时3分钟\n\t\t\tHttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.CONTENT_TYPE, \"multipart/form-data\")\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.header(Header.AUTHORIZATION, \"Bearer \" + config.getApiKey())\n\t\t\t\t.form(paramMap)\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\treturn httpRequest.execute();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new AIException(\"Failed to send POST request：\" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t/**\n\t * 支持流式返回的 POST 请求\n\t *\n\t * @param endpoint 请求地址\n\t * @param paramMap 请求参数\n\t * @param callback 流式数据回调函数\n\t */\n\tprotected void sendPostStream(final String endpoint, final Map<String, Object> paramMap, Consumer<String> callback) {\n\t\t\tHttpURLConnection connection = null;\n\t\t\ttry {\n\t\t\t\t// 创建连接\n\t\t\t\tURL apiUrl = new URL(config.getApiUrl() + endpoint);\n\t\t\t\tconnection = (HttpURLConnection) apiUrl.openConnection();\n\t\t\t\tif (config.getHasProxy()) {\n\t\t\t\t\tconnection = (HttpURLConnection) apiUrl.openConnection(config.getProxy());\n\t\t\t\t}\n\t\t\t\tconnection.setRequestMethod(Method.POST.name());\n\t\t\t\tconnection.setRequestProperty(Header.CONTENT_TYPE.getValue(), \"application/json\");\n\t\t\t\tconnection.setRequestProperty(Header.AUTHORIZATION.getValue(), \"Bearer \" + config.getApiKey());\n\t\t\t\tconnection.setDoOutput(true);\n\t\t\t\t//5分钟\n\t\t\t\tconnection.setReadTimeout(config.getReadTimeout());\n\t\t\t\t//3分钟\n\t\t\t\tconnection.setConnectTimeout(config.getTimeout());\n\t\t\t\t// 发送请求体\n\t\t\t\ttry (OutputStream os = connection.getOutputStream()) {\n\t\t\t\t\tString jsonInputString = JSONUtil.toJsonStr(paramMap);\n\t\t\t\t\tos.write(jsonInputString.getBytes());\n\t\t\t\t\tos.flush();\n\t\t\t\t}\n\n\t\t\t\t// 读取流式响应\n\t\t\t\ttry (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {\n\t\t\t\t\tString line;\n\t\t\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\t\t\t// 调用回调函数处理每一行数据\n\t\t\t\t\t\tcallback.accept(line);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tcallback.accept(\"{\\\"error\\\": \\\"\" + e.getMessage() + \"\\\"}\");\n\t\t\t} finally {\n\t\t\t\t// 关闭连接\n\t\t\t\tif (connection != null) {\n\t\t\t\t\tconnection.disconnect();\n\t\t\t\t}\n\t\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\nimport java.net.Proxy;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Config基础类，定义模型配置的基本属性\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class BaseConfig implements AIConfig {\n\n\t//apiKey\n\tprotected volatile String apiKey;\n\t//API请求地址\n\tprotected volatile String apiUrl;\n\t//具体模型\n\tprotected volatile String model;\n\t//动态扩展字段\n\tprotected Map<String, Object> additionalConfig = new ConcurrentHashMap<>();\n\t//连接超时时间\n\tprotected volatile int timeout = 180000;\n\t//读取超时时间\n\tprotected volatile int readTimeout = 300000;\n\t//是否设置代理\n\tprotected volatile boolean hasProxy = false;\n\t//代理设置\n\tprotected volatile Proxy proxy;\n\n\t@Override\n\tpublic void setApiKey(final String apiKey) {\n\t\tthis.apiKey = apiKey;\n\t}\n\n\t@Override\n\tpublic String getApiKey() {\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic void setApiUrl(final String apiUrl) {\n\t\tthis.apiUrl = apiUrl;\n\t}\n\n\t@Override\n\tpublic String getApiUrl() {\n\t\treturn apiUrl;\n\t}\n\n\t@Override\n\tpublic void setModel(final String model) {\n\t\tthis.model = model;\n\t}\n\n\t@Override\n\tpublic String getModel() {\n\t\treturn model;\n\t}\n\n\t@Override\n\tpublic void putAdditionalConfigByKey(final String key, final Object value) {\n\t\tthis.additionalConfig.put(key, value);\n\t}\n\n\t@Override\n\tpublic Object getAdditionalConfigByKey(final String key) {\n\t\treturn additionalConfig.get(key);\n\t}\n\n\t@Override\n\tpublic Map<String, Object> getAdditionalConfigMap() {\n\t\treturn new ConcurrentHashMap<>(additionalConfig);\n\t}\n\n\t@Override\n\tpublic int getTimeout() {\n\t\treturn timeout;\n\t}\n\n\t@Override\n\tpublic void setTimeout(final int timeout) {\n\t\tthis.timeout = timeout;\n\t}\n\n\t@Override\n\tpublic int getReadTimeout() {\n\t\treturn readTimeout;\n\t}\n\n\t@Override\n\tpublic void setReadTimeout(final int readTimeout) {\n\t\tthis.readTimeout = readTimeout;\n\t}\n\n\t@Override\n\tpublic boolean getHasProxy() {\n\t\treturn hasProxy;\n\t}\n\n\t@Override\n\tpublic void setHasProxy(boolean hasProxy) {\n\t\tthis.hasProxy = hasProxy;\n\t}\n\n\t@Override\n\tpublic Proxy getProxy() {\n\t\treturn proxy;\n\t}\n\n\t@Override\n\tpublic void setProxy(Proxy proxy) {\n\t\tthis.proxy = proxy;\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/Message.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.core;\n\n/**\n * 公共Message类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class Message {\n\t//角色 注意：如果设置系统消息，请放在messages列表的第一位\n\tprivate String role;\n\t//内容\n\tprivate Object content;\n\n\t/**\n\t * 构造\n\t */\n\tpublic Message() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param role    角色\n\t * @param content 内容\n\t */\n\tpublic Message(final String role, final Object content) {\n\t\tthis.role = role;\n\t\tthis.content = content;\n\t}\n\n\t/**\n\t * 设置角色\n\t *\n\t * @param role 角色\n\t */\n\tpublic void setRole(final String role) {\n\t\tthis.role = role;\n\t}\n\n\t/**\n\t * 获取角色\n\t *\n\t * @return 角色\n\t */\n\tpublic String getRole() {\n\t\treturn role;\n\t}\n\n\t/**\n\t * 获取内容\n\t *\n\t * @return 内容\n\t */\n\tpublic Object getContent() {\n\t\treturn content;\n\t}\n\n\t/**\n\t * 设置内容\n\t *\n\t * @param content 内容\n\t */\n\tpublic void setContent(final Object content) {\n\t\tthis.content = content;\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * AI相关基础类\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.core;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\n/**\n * deepSeek公共类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DeepSeekCommon {\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * DeepSeek配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DeepSeekConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"https://api.deepseek.com\";\n\n\tprivate final String DEFAULT_MODEL = Models.DeepSeek.DEEPSEEK_CHAT.getModel();\n\n\tpublic DeepSeekConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic DeepSeekConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"deepSeek\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**\n * 创建DeepSeek服务实现类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DeepSeekProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"deepSeek\";\n\t}\n\n\t@Override\n\tpublic DeepSeekService create(final AIConfig config) {\n\t\treturn new DeepSeekServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\nimport cn.hutool.ai.core.AIService;\nimport java.util.function.Consumer;\n\n/**\n * deepSeek支持的扩展接口\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface DeepSeekService extends AIService {\n\n\t/**\n\t * 模型beta功能\n\t *\n\t * @param prompt 题词\n\t * @return AI的回答\n\t * @since 5.8.38\n\t */\n\tString beta(String prompt);\n\n\t/**\n\t * 模型beta功能-SSE流式输出\n\t * @param prompt 题词\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\t void beta(String prompt, final Consumer<String> callback);\n\n\t/**\n\t * 列出所有模型列表\n\t *\n\t * @return model列表\n\t * @since 5.8.38\n\t */\n\tString models();\n\n\t/**\n\t * 查询余额\n\t *\n\t * @return 余额\n\t * @since 5.8.38\n\t */\n\tString balance();\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/DeepSeekServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONUtil;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * DeepSeek服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DeepSeekServiceImpl extends BaseAIService implements DeepSeekService {\n\n\t//对话补全\n\tprivate final String CHAT_ENDPOINT = \"/chat/completions\";\n\t//FIM补全（beta）\n\tprivate final String BETA_ENDPOINT = \"/beta/completions\";\n\t//列出模型\n\tprivate final String MODELS_ENDPOINT = \"/models\";\n\t//余额查询\n\tprivate final String BALANCE_ENDPOINT = \"/user/balance\";\n\n\t/**\n\t * 构造函数\n\t *\n\t * @param config AI配置\n\t */\n\tpublic DeepSeekServiceImpl(final AIConfig config) {\n\t\t//初始化DeepSeek客户端\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tfinal String paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(final List<Message> messages, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"deepseek-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String beta(final String prompt) {\n\t\tfinal String paramJson = buildBetaRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(BETA_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void beta(final String prompt, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildBetaStreamRequestBody(prompt);\n\t\tThreadUtil.newThread(() -> sendPostStream(BETA_ENDPOINT, paramMap, callback::accept), \"deepseek-beta-sse\").start();\n\t}\n\n\t@Override\n\tpublic String models() {\n\t\tfinal HttpResponse response = sendGet(MODELS_ENDPOINT);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String balance() {\n\t\tfinal HttpResponse response = sendGet(BALANCE_ENDPOINT);\n\t\treturn response.body();\n\t}\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建chatStream请求体\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t// 构建beta请求体\n\tprivate String buildBetaRequestBody(final String prompt) {\n\t\t// 定义消息结构\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建betaStream请求体\n\tprivate Map<String, Object> buildBetaStreamRequestBody(final String prompt) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/deepseek/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对deepSeek的封装实现\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.model.deepseek;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\n/**\n * doubao公共类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DoubaoCommon {\n\n\t//doubao上下文缓存参数\n\tpublic enum DoubaoContext {\n\n\t\tSESSION(\"session\"),\n\t\tCOMMON_PREFIX(\"common_prefix\");\n\n\t\tprivate final String mode;\n\n\t\tDoubaoContext(String mode) {\n\t\t\tthis.mode = mode;\n\t\t}\n\n\t\tpublic String getMode() {\n\t\t\treturn mode;\n\t\t}\n\t}\n\n\t//doubao视觉参数\n\tpublic enum DoubaoVision {\n\n\t\tAUTO(\"auto\"),\n\t\tLOW(\"low\"),\n\t\tHIGH(\"high\");\n\n\t\tprivate final String detail;\n\n\t\tDoubaoVision(String detail) {\n\t\t\tthis.detail = detail;\n\t\t}\n\n\t\tpublic String getDetail() {\n\t\t\treturn detail;\n\t\t}\n\t}\n\n\t//doubao视频生成参数\n\tpublic enum DoubaoVideo {\n\n\t\t//宽高比例\n\t\tRATIO_16_9(\"--rt\", \"16:9\"),//[1280, 720]\n\t\tRATIO_4_3(\"--rt\", \"4:3\"),//[960, 720]\n\t\tRATIO_1_1(\"--rt\", \"1:1\"),//[720, 720]\n\t\tRATIO_3_4(\"--rt\", \"3:4\"),//[720, 960]\n\t\tRATIO_9_16(\"--rt\", \"9:16\"),//[720, 1280]\n\t\tRATIO_21_9(\"--rt\", \"21:9\"),//[1280, 544]\n\n\t\t//生成视频时长\n\t\tDURATION_5(\"--dur\", 5),//文生视频，图生视频\n\t\tDURATION_10(\"--dur\", 10),//文生视频\n\n\t\t//帧率，即一秒时间内视频画面数量\n\t\tFPS_5(\"--fps\", 24),\n\n\t\t//视频分辨率\n\t\tRESOLUTION_5(\"--rs\", \"720p\"),\n\n\t\t//生成视频是否包含水印\n\t\tWATERMARK_TRUE(\"--wm\", true),\n\t\tWATERMARK_FALSE(\"--wm\", false);\n\n\t\tprivate final String type;\n\t\tprivate final Object value;\n\n\t\tDoubaoVideo(String type, Object value) {\n\t\t\tthis.type = type;\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic String getType() {\n\t\t\treturn type;\n\t\t}\n\n\t\tpublic Object getValue() {\n\t\t\tif (value instanceof String) {\n\t\t\t\treturn (String) value;\n\t\t\t} else if (value instanceof Integer) {\n\t\t\t\treturn (Integer) value;\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn (Boolean) value;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * Doubao配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DoubaoConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"https://ark.cn-beijing.volces.com/api/v3\";\n\n\tprivate final String DEFAULT_MODEL = Models.Doubao.DOUBAO_1_5_LITE_32K.getModel();\n\n\tpublic DoubaoConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic DoubaoConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"doubao\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**\n * 创建Doubap服务实现类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DoubaoProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"doubao\";\n\t}\n\n\t@Override\n\tpublic DoubaoService create(final AIConfig config) {\n\t\treturn new DoubaoServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * doubao支持的扩展接口\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface DoubaoService extends AIService {\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 提问\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatVision(String prompt, final List<String> images) {\n\t\treturn chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail());\n\t}\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 提问\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatVision(String prompt, final List<String> images, final Consumer<String> callback) {\n\t\tchatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail(), callback);\n\t}\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 提问\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString chatVision(String prompt, final List<String> images, String detail);\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 提问\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatVision(String prompt, final List<String> images, String detail, final Consumer<String> callback);\n\n\t/**\n\t * 创建视频生成任务\n\t * 注意：调用该方法时，配置config中的model为您创建的推理接入点（Endpoint）ID。详细参考官方文档\n\t *\n\t * @param text        文本提示词\n\t * @param image       图片/或者图片Base64编码图片(URI形式)\n\t * @param videoParams 视频参数列表\n\t * @return 生成任务id\n\t * @since 5.8.38\n\t */\n\tString videoTasks(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams);\n\n\t/**\n\t * 创建视频生成任务\n\t * 注意：调用该方法时，配置config中的model为生成视频的模型或者您创建的推理接入点（Endpoint）ID。详细参考官方文档\n\t *\n\t * @param text  文本提示词\n\t * @param image 图片/或者图片Base64编码图片(URI形式)\n\t * @return 生成任务id\n\t * @since 5.8.38\n\t */\n\tdefault String videoTasks(String text, String image) {\n\t\treturn videoTasks(text, image, null);\n\t}\n\n\t/**\n\t * 查询视频生成任务信息\n\t *\n\t * @param taskId 通过创建生成视频任务返回的生成任务id\n\t * @return 生成任务信息\n\t * @since 5.8.38\n\t */\n\tString getVideoTasksInfo(String taskId);\n\n\t/**\n\t * 文本向量化\n\t *\n\t * @param input 需要向量化的内容列表，支持中文、英文\n\t * @return 处理后的向量信息\n\t * @since 5.8.38\n\t */\n\tString embeddingText(String[] input);\n\n\t/**\n\t * 图文向量化：仅支持单一文本、单张图片或文本与图片的组合输入（即一段文本 + 一张图片），暂不支持批量文本 / 图片的同时处理\n\t *\n\t * @param text  需要向量化的内容\n\t * @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)\n\t * @return 处理后的向量信息\n\t * @since 5.8.38\n\t */\n\tString embeddingVision(String text, String image);\n\n\t/**\n\t * 应用(Bot) config中model设置为您创建的应用ID\n\t *\n\t * @param messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString botsChat(final List<Message> messages);\n\n\t/**\n\t * 应用(Bot)-SSE流式输出 config中model设置为您创建的应用ID\n\t *\n\t * @param messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid botsChat(final List<Message> messages, final Consumer<String> callback);\n\n\t/**\n\t * 分词：可以将文本转换为模型可理解的 token id，并返回文本的 tokens 数量、token id、 token 在原始文本中的偏移量等信息\n\t *\n\t * @param text 需要分词的内容列表\n\t * @return 分词结果\n\t * @since 5.8.38\n\t */\n\tString tokenization(String[] text);\n\n\t/**\n\t * 批量推理 Chat\n\t * 注意：调用该方法时，配置config中的model为您创建的批量推理接入点（Endpoint）ID。详细参考官方文档\n\t * 该方法不支持流式\n\t *\n\t * @param prompt chat内容\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String batchChat(String prompt){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn batchChat(messages);\n\t}\n\n\t/**\n\t * 批量推理 Chat\n\t * 注意：调用该方法时，配置config中的model为您创建的批量推理接入点（Endpoint）ID。详细参考官方文档\n\t * 该方法不支持流式\n\t *\n\t * @param messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString batchChat(final List<Message> messages);\n\n\t/**\n\t * 创建上下文缓存： 创建上下文缓存，获得缓存 id字段后，在上下文缓存对话 API中使用。\n\t * 注意：调用该方法时，配置config中的model为您创建的推理接入点（Endpoint）ID,\n\t * 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档\n\t *\n\t * @param messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @param mode     上下文缓存的类型,详细参考官方文档 默认为session\n\t * @return 返回的缓存id\n\t * @since 5.8.38\n\t */\n\tString createContext(final List<Message> messages, String mode);\n\n\t/**\n\t * 创建上下文缓存： 创建上下文缓存，获得缓存 id字段后，在上下文缓存对话 API中使用。\n\t * 注意：调用该方法时，配置config中的model为您创建的推理接入点（Endpoint）ID,\n\t * 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档\n\t *\n\t * @param messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @return 返回的缓存id\n\t * @since 5.8.38\n\t */\n\tdefault String createContext(final List<Message> messages) {\n\t\treturn createContext(messages, DoubaoCommon.DoubaoContext.SESSION.getMode());\n\t}\n\n\t/**\n\t * 上下文缓存对话： 向大模型发起带上下文缓存的请求\n\t * 注意：配置config中的model可以为您创建的推理接入点（Endpoint）ID，也可以是支持chat的model\n\t *\n\t * @param prompt    对话的内容题词\n\t * @param contextId 创建上下文缓存后获取的缓存id\n\t * @return AI的回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatContext(String prompt, String contextId){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn chatContext(messages, contextId);\n\t}\n\n\t/**\n\t * 上下文缓存对话-SSE流式输出\n\t * 注意：配置config中的model可以为您创建的推理接入点（Endpoint）ID，也可以是支持chat的model\n\t *\n\t * @param prompt    对话的内容题词\n\t * @param contextId 创建上下文缓存后获取的缓存id\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatContext(String prompt, String contextId, final Consumer<String> callback){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\tchatContext(messages, contextId, callback);\n\t}\n\n\t/**\n\t * 上下文缓存对话： 向大模型发起带上下文缓存的请求\n\t * 注意：配置config中的model可以为您创建的推理接入点（Endpoint）ID，也可以是支持chat的model\n\t *\n\t * @param messages  对话的信息  不支持最后一个元素的role设置为assistant。如使用session 缓存（mode设置为session）传入最新一轮对话的信息，无需传入历史信息\n\t * @param contextId 创建上下文缓存后获取的缓存id\n\t * @return AI的回答\n\t * @since 5.8.38\n\t */\n\tString chatContext(final List<Message> messages, String contextId);\n\n\t/**\n\t * 上下文缓存对话-SSE流式输出\n\t * 注意：配置config中的model可以为您创建的推理接入点（Endpoint）ID，也可以是支持chat的model\n\t *\n\t * @param messages  对话的信息  不支持最后一个元素的role设置为assistant。如使用session 缓存（mode设置为session）传入最新一轮对话的信息，无需传入历史信息\n\t * @param contextId 创建上下文缓存后获取的缓存id\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatContext(final List<Message> messages, String contextId, final Consumer<String> callback);\n\n\t/**\n\t * 文生图\n\t * 请设置config中model为支持图片功能的模型，目前支持Doubao-Seedream-3.0-t2i\n\t *\n\t * @param prompt 题词\n\t * @return 包含生成图片的url\n\t * @since 5.8.39\n\t */\n\tString imagesGenerations(String prompt);\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/DoubaoServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONUtil;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * Doubao服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class DoubaoServiceImpl extends BaseAIService implements DoubaoService {\n\n\t//对话\n\tprivate final String CHAT_ENDPOINT = \"/chat/completions\";\n\t//文本向量化\n\tprivate final String EMBEDDING_TEXT = \"/embeddings\";\n\t//图文向量化\n\tprivate final String EMBEDDING_VISION = \"/embeddings/multimodal\";\n\t//应用bots\n\tprivate final String BOTS_CHAT = \"/bots/chat/completions\";\n\t//分词\n\tprivate final String TOKENIZATION = \"/tokenization\";\n\t//批量推理chat\n\tprivate final String BATCH_CHAT = \"/batch/chat/completions\";\n\t//创建上下文缓存\n\tprivate final String CREATE_CONTEXT = \"/context/create\";\n\t//上下文缓存对话\n\tprivate final String CHAT_CONTEXT = \"/context/chat/completions\";\n\t//创建视频生成任务\n\tprivate final String CREATE_VIDEO = \"/contents/generations/tasks\";\n\t//文生图\n\tprivate final String IMAGES_GENERATIONS = \"/images/generations\";\n\n\tpublic DoubaoServiceImpl(final AIConfig config) {\n\t\t//初始化doubao客户端\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tString paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(final List<Message> messages, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"doubao-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatVision(String prompt, final List<String> images, String detail) {\n\t\tString paramJson = buildChatVisionRequestBody(prompt, images, detail);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"doubao-chatVision-sse\").start();\n\t}\n\n\t@Override\n\tpublic String videoTasks(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {\n\t\tString paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);\n\t\tfinal HttpResponse response = sendPost(CREATE_VIDEO, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getVideoTasksInfo(String taskId) {\n\t\tfinal HttpResponse response = sendGet(CREATE_VIDEO + \"/\" + taskId);\n\t\treturn response.body();\n\t}\n\n\n\t@Override\n\tpublic String embeddingText(String[] input) {\n\t\tString paramJson = buildEmbeddingTextRequestBody(input);\n\t\tfinal HttpResponse response = sendPost(EMBEDDING_TEXT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String embeddingVision(String text, String image) {\n\t\tString paramJson = buildEmbeddingVisionRequestBody(text, image);\n\t\tfinal HttpResponse response = sendPost(EMBEDDING_VISION, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String botsChat(final List<Message> messages) {\n\t\tString paramJson = buildBotsChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(BOTS_CHAT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void botsChat(List<Message> messages, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildBotsChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(BOTS_CHAT, paramMap, callback::accept), \"doubao-botsChat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String tokenization(String[] text) {\n\t\tString paramJson = buildTokenizationRequestBody(text);\n\t\tfinal HttpResponse response = sendPost(TOKENIZATION, paramJson);\n\t\treturn response.body();\n\t}\n\n\n\t@Override\n\tpublic String batchChat(final List<Message> messages) {\n\t\tString paramJson = buildBatchChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(BATCH_CHAT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String createContext(final List<Message> messages, String mode) {\n\t\tString paramJson = buildCreateContextRequest(messages, mode);\n\t\tfinal HttpResponse response = sendPost(CREATE_CONTEXT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String chatContext(final List<Message> messages, String contextId) {\n\t\tString paramJson = buildChatContentRequestBody(messages, contextId);\n\t\tfinal HttpResponse response = sendPost(CHAT_CONTEXT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatContext(final List<Message> messages, String contextId, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatContentStreamRequestBody(messages, contextId);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_CONTEXT, paramMap, callback::accept), \"doubao-chatContext-sse\").start();\n\t}\n\n\t@Override\n\tpublic String imagesGenerations(String prompt) {\n\t\tString paramJson = buildImagesGenerationsRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);\n\t\treturn response.body();\n\t}\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建chatStream请求体\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建chatVision请求体\n\tprivate String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn paramMap;\n\t}\n\n\t//构建文本向量化请求体\n\tprivate String buildEmbeddingTextRequestBody(String[] input) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"input\", input);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建图文向量化请求体\n\tprivate String buildEmbeddingVisionRequestBody(String text, String image) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\n\t\tfinal List<Object> input = new ArrayList<>();\n\t\t//添加文本参数\n\t\tif (!StrUtil.isBlank(text)) {\n\t\t\tfinal Map<String, String> textMap = new HashMap<>();\n\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\ttextMap.put(\"text\", text);\n\t\t\tinput.add(textMap);\n\t\t}\n\t\t//添加图片参数\n\t\tif (!StrUtil.isBlank(image)) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", image);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tinput.add(imgUrlMap);\n\t\t}\n\n\t\tparamMap.put(\"input\", input);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建应用chat请求体\n\tprivate String buildBotsChatRequestBody(final List<Message> messages) {\n\t\treturn buildChatRequestBody(messages);\n\t}\n\n\tprivate Map<String, Object> buildBotsChatStreamRequestBody(final List<Message> messages) {\n\t\treturn buildChatStreamRequestBody(messages);\n\t}\n\n\t//构建分词请求体\n\tprivate String buildTokenizationRequestBody(String[] text) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"text\", text);\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建批量推理chat请求体\n\tprivate String buildBatchChatRequestBody(final List<Message> messages) {\n\t\treturn buildChatRequestBody(messages);\n\t}\n\n\tprivate Map<String, Object> buildBatchChatStreamRequestBody(final List<Message> messages) {\n\t\treturn buildChatStreamRequestBody(messages);\n\t}\n\n\t//构建创建上下文缓存请求体\n\tprivate String buildCreateContextRequest(final List<Message> messages, String mode) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"mode\", mode);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建上下文缓存对话请求体\n\tprivate String buildChatContentRequestBody(final List<Message> messages, String contextId) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"context_id\", contextId);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatContentStreamRequestBody(final List<Message> messages, String contextId) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"context_id\", contextId);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建创建视频任务请求体\n\tprivate String buildGenerationsTasksRequestBody(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\n\t\tfinal List<Object> content = new ArrayList<>();\n\t\t//添加文本参数\n\t\tfinal Map<String, String> textMap = new HashMap<>();\n\t\tif (!StrUtil.isBlank(text)) {\n\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\ttextMap.put(\"text\", text);\n\t\t\tcontent.add(textMap);\n\t\t}\n\t\t//添加图片参数\n\t\tif (!StrUtil.isBlank(image)) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", image);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\t//添加视频参数\n\t\tif (videoParams != null && !videoParams.isEmpty()) {\n\t\t\t//如果有文本参数就加在后面\n\t\t\tif (textMap != null && !textMap.isEmpty()) {\n\t\t\t\tint textIndex = content.indexOf(textMap);\n\t\t\t\tStringBuilder textBuilder = new StringBuilder(text);\n\t\t\t\tfor (DoubaoCommon.DoubaoVideo videoParam : videoParams) {\n\t\t\t\t\ttextBuilder.append(\" \").append(videoParam.getType()).append(\" \").append(videoParam.getValue());\n\t\t\t\t}\n\t\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\t\ttextMap.put(\"text\", textBuilder.toString());\n\n\t\t\t\tif (textIndex != -1) {\n\t\t\t\t\tcontent.set(textIndex, textMap);\n\t\t\t\t} else {\n\t\t\t\t\tcontent.add(textMap);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t//如果没有文本参数就重新增加\n\t\t\t\tStringBuilder textBuilder = new StringBuilder();\n\t\t\t\tfor (DoubaoCommon.DoubaoVideo videoParam : videoParams) {\n\t\t\t\t\ttextBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(\" \");\n\t\t\t\t}\n\t\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\t\ttextMap.put(\"text\", textBuilder.toString());\n\t\t\t\tcontent.add(textMap);\n\t\t\t}\n\t\t}\n\n\t\tparamMap.put(\"content\", content);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建文生图请求体\n\tprivate String buildImagesGenerationsRequestBody(String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/doubao/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对doubao的封装实现\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.model.doubao;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\n/**\n * gemini公共类\n *\n * @author elichow\n * @since 5.8.43\n */\npublic class GeminiCommon {\n\n\t//要生成的图片数量\n\tpublic enum GeminiImageCount {\n\n\t\tONE(1),\n\t\tTWO(2),\n\t\tTHREE(3),\n\t\tFOUR(4);\n\n\t\tprivate final int count;\n\n\t\tGeminiImageCount(int count) {\n\t\t\tthis.count = count;\n\t\t}\n\n\t\tpublic int getCount() {\n\t\t\treturn count;\n\t\t}\n\t}\n\n\t//生成的图片大小 (imageSize) - 仅限 Standard 和 Ultra\n\tpublic enum GeminiImageSize {\n\n\t\tSIZE_1K(\"1K\"),\n\t\tSIZE_2K(\"2K\");\n\n\t\tprivate final String value;\n\n\t\tGeminiImageSize(String value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic String getValue() {\n\t\t\treturn value;\n\t\t}\n\t}\n\n\t//宽高比\n\tpublic enum GeminiAspectRatio {\n\n\t\tSQUARE(\"1:1\"),\n\t\tPORTRAIT_3_4(\"3:4\"),\n\t\tLANDSCAPE_4_3(\"4:3\"),\n\t\tPORTRAIT_9_16(\"9:16\"),\n\t\tLANDSCAPE_16_9(\"16:9\");\n\n\t\tprivate final String ratio;\n\n\t\tGeminiAspectRatio(String ratio) {\n\t\t\tthis.ratio = ratio;\n\t\t}\n\n\t\tpublic String getRatio() {\n\t\t\treturn ratio;\n\t\t}\n\n\t}\n\n\t//人物生成权限\n\tpublic enum GeminiPersonGeneration {\n\n\t\tDONT_ALLOW(\"dont_allow\"),\n\t\tALLOW_ADULT(\"allow_adult\"),\n\t\tALLOW_ALL(\"allow_all\");\n\n\t\tprivate final String value;\n\n\t\tGeminiPersonGeneration(String value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic String getValue() {\n\t\t\treturn value;\n\t\t}\n\t}\n\n\t//生成的视频的时长\n\tpublic enum GeminiDurationSeconds {\n\n\t\tFOUR(4),\n\t\tSIX(6),\n\t\tEIGHT(8);\n\n\t\tprivate final Integer value;\n\n\t\tGeminiDurationSeconds(Integer value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic Integer getValue() {\n\t\t\treturn value;\n\t\t}\n\t}\n\n\t//语音音色\n\tpublic enum GeminiVoice {\n\n\t\tAOEDE(\"Aoede\"),\n\t\tCHARON(\"Charon\"),\n\t\tKORE(\"Kore\"),\n\t\tFENRIR(\"Fenrir\"),\n\t\tPUCK(\"Puck\");\n\n\t\tprivate final String value;\n\n\t\tGeminiVoice(String value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic String getValue() {\n\t\t\treturn value;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * Gemini配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.43\n */\npublic class GeminiConfig extends BaseConfig {\n\n\t// Google Generative AI 的基础 URL\n\tprivate final String API_URL = \"https://generativelanguage.googleapis.com/v1beta\";\n\n\t// 默认模型\n\tprivate final String DEFAULT_MODEL = Models.Gemini.GEMINI_2_5_FLASH.getModel();\n\n\tpublic GeminiConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic GeminiConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"gemini\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**\n * 创建Gemini服务实现类\n *\n * @author elichow\n * @since 5.8.43\n */\npublic class GeminiProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"gemini\";\n\t}\n\n\t@Override\n\tpublic GeminiService create(final AIConfig config) {\n\t\treturn new GeminiServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\n\nimport java.io.File;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Gemini服务支持的扩展接口\n *\n * @author elichow\n * @since 5.8.43\n */\npublic interface GeminiService extends AIService {\n\n\t/**\n\t * 全模态理解（图像/视频/音频/PDF）：模型会依据传入的媒体资源给出回复。\n\t *\n\t * @param prompt    指令\n\t * @param mediaList 媒体资源列表 (支持 Base64, URL, 或 File API 的 URI)\n\t * @return AI回答\n\t */\n\tString chatMultimodal(String prompt, final List<String> mediaList);\n\n\t/**\n\t * 全模态理解-SSE流式输出\n\t *\n\t * @param prompt    指令\n\t * @param mediaList 媒体资源列表\n\t * @param callback  流式数据回调函数\n\t */\n\tvoid chatMultimodal(String prompt, final List<String> mediaList, final Consumer<String> callback);\n\n\t/**\n\t * 结构化输出：强制要求模型返回 JSON 格式\n\t *\n\t * @param messages 消息列表\n\t * @return AI回答\n\t */\n\tString chatJson(final List<Message> messages);\n\n\t/**\n\t * 生成图像 (Imagen 模型集成)\n\t *\n\t * @param prompt 图像描述词\n\t * @return 包含图片数据的响应 (通常为 Base64)\n\t */\n\tString predictImage(String prompt);\n\n\t/**\n\t * 生成视频:根据文本提示语生成视频\n\t *\n\t * @param prompt 视频描述词\n\t * @return 包含 operationName 的 JSON 字符串\n\t */\n\tString predictVideo(String prompt);\n\n\t/**\n\t * 获取视频生成状态：用于轮询视频生成进度\n\t *\n\t * @param operationName 生成视频接口返回的任务名称\n\t * @return 包含视频状态（done）及结果的 JSON 字符串\n\t */\n\tString getVideoOperation(String operationName);\n\n\n\t/**\n\t * 下载生成的视频文件\n\t *\n\t * @param videoUri 视频文件的 URI\n\t * @param filePath 保存视频的文件路径\n\t */\n\tvoid downLoadVideo(String videoUri, String filePath);\n\n\t/**\n\t * 文本转语音 (TTS)\n\t *\n\t * @param prompt 文本或带有导演备注的内容\n\t * @return 语音文件的 Base64 编码字符串\n\t */\n\tString textToSpeech(String prompt);\n\n\t/**\n\t * 文本转语音 (TTS) - 指定音色\n\t *\n\t * @param prompt 文本或带有导演备注的内容\n\t * @param voice 预定义的音色常量\n\t * @return 语音文件的 Base64 编码字符串\n\t */\n\tString textToSpeech(String prompt, String voice);\n\n\t/**\n\t * 上传大文件到Gemini File API\n\t *\n\t * @param file 本地文件\n\t * @return 上传后的文件对象信息\n\t */\n\tString uploadFile(final File file);\n\n\t/**\n\t * 为原始 PCM 音频数据添加 WAV 头\n\t *\n\t * @param rawPcm 原始 PCM 音频字节数组\n\t * @return 带有 WAV 头的音频字节数组\n\t */\n\tbyte[] addWavHeader(final byte[] rawPcm);\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\nimport cn.hutool.ai.AIException;\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.*;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\n\nimport java.io.*;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.*;\nimport java.util.function.Consumer;\n\n/**\n * Gemini服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.43\n */\npublic class GeminiServiceImpl extends BaseAIService implements GeminiService {\n\n\tprivate final String GENERATE_CONTENT = \":generateContent\";\n\tprivate final String STREAM_GENERATE_CONTENT = \":streamGenerateContent\";\n\tprivate final String PREDICT = \":predict\";\n\tprivate final String PREDICT_LONG_RUNNING = \":predictLongRunning\";\n\tprivate final String UPLOAD_BASE_URL = \"https://generativelanguage.googleapis.com/upload/v1beta/files\";\n\n\tpublic GeminiServiceImpl(final AIConfig config) {\n\t\tsuper(config);\n\t}\n\n\tprivate String getEndpoint(final boolean stream) {\n\t\tString action = stream ? STREAM_GENERATE_CONTENT : GENERATE_CONTENT;\n\t\treturn \"/models/\" + config.getModel() + action;\n\t}\n\n\tprivate String getPredictImageEndpoint() {\n\t\treturn \"/models/\" + config.getModel() + PREDICT;\n\t}\n\n\tprivate String getPredictVideoEndpoint() {\n\t\treturn \"/models/\" + config.getModel() + PREDICT_LONG_RUNNING;\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tfinal Map<String, Object> paramMap = buildChatRequestMap(messages);\n\t\tfinal HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(final List<Message> messages, final Consumer<String> callback) {\n\t\tfinal Map<String, Object> paramMap = buildChatRequestMap(messages);\n\t\tfinal String endpoint = getEndpoint(true) + \"?alt=sse\";\n\t\tThreadUtil.newThread(() -> sendPostStream(endpoint, paramMap, callback), \"gemini-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatMultimodal(String prompt, final List<String> mediaList) {\n\t\tfinal Map<String, Object> paramMap = buildMultimodalRequestMap(prompt, mediaList);\n\t\tfinal HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatMultimodal(String prompt, final List<String> mediaList, final Consumer<String> callback) {\n\t\tfinal Map<String, Object> paramMap = buildMultimodalRequestMap(prompt, mediaList);\n\t\tfinal String endpoint = getEndpoint(true) + \"?alt=sse\";\n\t\tThreadUtil.newThread(() -> sendPostStream(endpoint, paramMap, callback), \"gemini-m-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatJson(final List<Message> messages) {\n\t\tfinal Map<String, Object> paramMap = buildChatRequestMap(messages);\n\t\tMap<String, Object> genConfig = MapUtil.get(paramMap, \"generationConfig\", Map.class);\n\t\tif (genConfig == null) {\n\t\t\tgenConfig = new HashMap<>();\n\t\t}\n\t\t//指定响应MIME类型为JSON\n\t\tgenConfig.put(\"response_mime_type\", \"application/json\");\n\n\t\tfinal HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String predictImage(String prompt) {\n\t\tfinal Map<String, Object> paramMap = buildPredictImageRequestMap(prompt);\n\t\tfinal HttpResponse response = sendPost(getPredictImageEndpoint(), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String predictVideo(String prompt) {\n\t\tfinal Map<String, Object> paramMap = buildPredictVideoRequestMap(prompt);\n\t\tfinal HttpResponse response = sendPost(getPredictVideoEndpoint(), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getVideoOperation(String operationName) {\n\t\tString endPoint = \"/\" + operationName;\n\t\tfinal HttpResponse response = sendGet(endPoint);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void downLoadVideo(String videoUri, String filePath) {\n\t\tif (StrUtil.isBlank(videoUri)) {\n\t\t\tthrow new AIException(\"Video URI is empty\");\n\t\t}\n\t\tfinal HttpResponse response = HttpRequest.get(videoUri)\n\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t.setFollowRedirects(true)\n\t\t\t.executeAsync();\n\t\tif (response.isOk()) {\n\t\t\tresponse.writeBody(FileUtil.file(filePath));\n\t\t} else {\n\t\t\tthrow new AIException(\"Download failed with status: \" + response.getStatus());\n\t\t}\n\t}\n\n\t@Override\n\tpublic String textToSpeech(String prompt) {\n\t\tfinal Map<String, Object> paramMap = buildTextToSpeechRequestMap(prompt);\n\t\tfinal HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String textToSpeech(String prompt, String voice) {\n\t\tfinal Map<String, Object> voiceConfig = MapUtil.of(\"prebuilt_voice_config\", MapUtil.of(\"voice_name\", voice));\n\t\tconfig.putAdditionalConfigByKey(\"speech_config\", MapUtil.of(\"voice_config\", voiceConfig));\n\t\treturn this.textToSpeech(prompt);\n\t}\n\n\t@Override\n\tpublic String uploadFile(final File file) {\n\t\tif (null == file || !file.exists()) {\n\t\t\tthrow new AIException(\"File not found!\");\n\t\t}\n\t\ttry {\n\t\t\t//自动获取MIME\n\t\t\tString mimeType = FileUtil.getMimeType(file.getName());\n\t\t\tif (StrUtil.isBlank(mimeType)) {\n\t\t\t\tmimeType = \"application/octet-stream\";\n\t\t\t}\n\n\t\t\tString uploadUrl = getUploadBaseUrl();\n\n\t\t\t//获取 Upload URL\n\t\t\tString metadata = JSONUtil.toJsonStr(MapUtil.of(\"file\", MapUtil.of(\"display_name\", file.getName())));\n\t\t\tfinal HttpResponse res = HttpRequest.post(uploadUrl)\n\t\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t\t.header(\"X-Goog-Upload-Protocol\", \"resumable\")\n\t\t\t\t.header(\"X-Goog-Upload-Command\", \"start\")\n\t\t\t\t.header(\"X-Goog-Upload-Header-Content-Length\", String.valueOf(file.length()))\n\t\t\t\t.header(\"X-Goog-Upload-Header-Content-Type\", mimeType)\n\t\t\t\t.body(metadata).execute();\n\n\t\t\tString sessionUrl = res.header(\"X-Goog-Upload-URL\");\n\n\t\t\t//上传二进制流\n\t\t\tfinal HttpResponse uploadRes = HttpRequest.put(sessionUrl)\n\t\t\t\t.header(\"X-Goog-Upload-Command\", \"upload, finalize\")\n\t\t\t\t.header(\"X-Goog-Upload-Offset\", \"0\")\n\t\t\t\t.body(FileUtil.readBytes(file)).execute();\n\n\t\t\t//返回 JSON，调用者可以从中解析出 file.uri\n\t\t\treturn uploadRes.body();\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"Upload failed\", e);\n\t\t}\n\t}\n\n\n\t@Override\n\tpublic byte[] addWavHeader(final byte[] pcmData) {\n\t\tfinal int totalDataLen = pcmData.length;\n\t\tfinal int totalAudioLen = totalDataLen + 36;\n\t\t// Gemini TTS 默认通常是 24k 或 16k\n\t\tfinal int sampleRate = 24000;\n\t\t// 单声道\n\t\tfinal int channels = 1;\n\t\t// 16bit\n\t\tfinal int byteRate = sampleRate * channels * 2;\n\n\t\tfinal byte[] header = new byte[44];\n\t\theader[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';\n\t\theader[4] = (byte) (totalAudioLen & 0xff);\n\t\theader[5] = (byte) ((totalAudioLen >> 8) & 0xff);\n\t\theader[6] = (byte) ((totalAudioLen >> 16) & 0xff);\n\t\theader[7] = (byte) ((totalAudioLen >> 24) & 0xff);\n\t\theader[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';\n\t\theader[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';\n\t\theader[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;\n\t\t// PCM 格式\n\t\theader[20] = 1; header[21] = 0;\n\t\theader[22] = (byte) channels; header[23] = 0;\n\t\theader[24] = (byte) (sampleRate & 0xff);\n\t\theader[25] = (byte) ((sampleRate >> 8) & 0xff);\n\t\theader[26] = (byte) ((sampleRate >> 16) & 0xff);\n\t\theader[27] = (byte) ((sampleRate >> 24) & 0xff);\n\t\theader[28] = (byte) (byteRate & 0xff);\n\t\theader[29] = (byte) ((byteRate >> 8) & 0xff);\n\t\theader[30] = (byte) ((byteRate >> 16) & 0xff);\n\t\theader[31] = (byte) ((byteRate >> 24) & 0xff);\n\t\theader[32] = (byte) (channels * 2); header[33] = 0;\n\t\t// 16 bits per sample\n\t\theader[34] = 16; header[35] = 0;\n\t\theader[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';\n\t\theader[40] = (byte) (totalDataLen & 0xff);\n\t\theader[41] = (byte) ((totalDataLen >> 8) & 0xff);\n\t\theader[42] = (byte) ((totalDataLen >> 16) & 0xff);\n\t\theader[43] = (byte) ((totalDataLen >> 24) & 0xff);\n\n\t\tfinal byte[] wavData = new byte[header.length + pcmData.length];\n\t\tSystem.arraycopy(header, 0, wavData, 0, header.length);\n\t\tSystem.arraycopy(pcmData, 0, wavData, header.length, pcmData.length);\n\t\treturn wavData;\n\t}\n\n\t/**\n\t * 动态根据 API 配置生成 Upload 地址\n\t */\n\tprivate String getUploadBaseUrl() {\n\t\tString apiUrl = config.getApiUrl();\n\t\t//自动提取域名部分\n\t\tif (StrUtil.contains(apiUrl, \"generativelanguage.googleapis.com\")) {\n\t\t\treturn \"https://generativelanguage.googleapis.com/upload/v1beta/files\";\n\t\t}\n\t\t//如果是反代或自定义节点，动态拼接\n\t\ttry {\n\t\t\tfinal URL url = new URL(apiUrl);\n\t\t\treturn new URL(url.getProtocol(), url.getHost(), url.getPort(), UPLOAD_BASE_URL).toString();\n\t\t} catch (Exception e) {\n\t\t\treturn apiUrl.replace(\"/models/\", \"/upload/v1beta/files\").split(\"/models\")[0];\n\t\t}\n\t}\n\n\tprivate Map<String, Object> buildChatRequestMap(final List<Message> messages) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tfinal List<Map<String, Object>> contents = new ArrayList<>();\n\t\tMap<String, Object> systemInstruction = null;\n\n\t\tfor (Message msg : messages) {\n\t\t\tif (\"system\".equalsIgnoreCase(msg.getRole())) {\n\t\t\t\tsystemInstruction = MapUtil.ofEntries(MapUtil.entry(\"parts\",\n\t\t\t\t\tCollections.singletonList(MapUtil.ofEntries(MapUtil.entry(\"text\", msg.getContent())))));\n\t\t\t} else {\n\t\t\t\tcontents.add(MapUtil.ofEntries(\n\t\t\t\t\tMapUtil.entry(\"role\", \"user\".equalsIgnoreCase(msg.getRole()) ? \"user\" : \"model\"),\n\t\t\t\t\tMapUtil.entry(\"parts\", Collections.singletonList(MapUtil.ofEntries(MapUtil.entry(\"text\", msg.getContent()))))\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\t\tparamMap.put(\"contents\", contents);\n\t\tif (ObjectUtil.isNotNull(systemInstruction)) {\n\t\t\tparamMap.put(\"system_instruction\", systemInstruction);\n\t\t}\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn paramMap;\n\t}\n\n\tprivate Map<String, Object> buildMultimodalRequestMap(String prompt, final List<String> mediaList) {\n\t\tfinal List<Map<String, Object>> parts = new ArrayList<>();\n\t\tparts.add(MapUtil.ofEntries(MapUtil.entry(\"text\", prompt)));\n\n\t\tif (ObjectUtil.isNotNull(mediaList)) {\n\t\t\tfor (String media : mediaList) {\n\t\t\t\tif (StrUtil.isBlank(media)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t//Gemini File资源\n\t\t\t\tif (media.contains(\"files/\")) {\n\t\t\t\t\tString fileUri = media;\n\t\t\t\t\tif (!media.startsWith(\"http\")) {\n\t\t\t\t\t\tfileUri = \"https://generativelanguage.googleapis.com/v1beta/\" + media;\n\t\t\t\t\t}\n\t\t\t\t\t//直接从服务端获取该文件上传时真实记录的 mimeType\n\t\t\t\t\tString realMimeType = getRemoteFileMimeType(fileUri);\n\t\t\t\t\tparts.add(MapUtil.ofEntries(\n\t\t\t\t\t\tMapUtil.entry(\"file_data\", MapUtil.ofEntries(\n\t\t\t\t\t\t\tMapUtil.entry(\"mime_type\", realMimeType),\n\t\t\t\t\t\t\tMapUtil.entry(\"file_uri\", fileUri)\n\t\t\t\t\t\t))\n\t\t\t\t\t));\n\t\t\t\t} else if (media.startsWith(\"http\")) {\n\t\t\t\t\t//普通网络图片 (下载并转 Base64)\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfinal byte[] bytes = HttpUtil.downloadBytes(media);\n\t\t\t\t\t\t//尝试识别下载文件的 MIME，无法识别则不强加后缀逻辑，通过流内容自适应\n\t\t\t\t\t\tString mime = FileUtil.getMimeType(media);\n\t\t\t\t\t\tif (StrUtil.isBlank(mime)) {\n\t\t\t\t\t\t\t// 基础兜底\n\t\t\t\t\t\t\tmime = \"image/jpeg\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparts.add(MapUtil.ofEntries(\n\t\t\t\t\t\t\tMapUtil.entry(\"inline_data\", MapUtil.ofEntries(\n\t\t\t\t\t\t\t\tMapUtil.entry(\"mime_type\", mime),\n\t\t\t\t\t\t\t\tMapUtil.entry(\"data\", Base64.encode(bytes))\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t));\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tthrow new AIException(\"Failed to download media from URL: \" + media, e.getMessage());\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t//Base64 数据\n\t\t\t\t\tparts.add(MapUtil.ofEntries(\n\t\t\t\t\t\tMapUtil.entry(\"inline_data\", MapUtil.ofEntries(\n\t\t\t\t\t\t\tMapUtil.entry(\"mime_type\", \"image/jpeg\"),\n\t\t\t\t\t\t\tMapUtil.entry(\"data\", media)\n\t\t\t\t\t\t))\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"contents\", Collections.singletonList(MapUtil.ofEntries(\n\t\t\tMapUtil.entry(\"role\", \"user\"),\n\t\t\tMapUtil.entry(\"parts\", parts)\n\t\t)));\n\n\t\t//合并其他参数\n\t\tif (MapUtil.isNotEmpty(config.getAdditionalConfigMap())) {\n\t\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\t}\n\t\treturn paramMap;\n\t}\n\n\tprivate Map<String, Object> buildPredictVideoRequestMap(String prompt) {\n\t\tfinal Map<String, Object> instance = new HashMap<>();\n\t\tinstance.put(\"prompt\", prompt);\n\n\t\tfinal Map<String, Object> parameters = new HashMap<>();\n\t\tparameters.put(\"durationSeconds\", GeminiCommon.GeminiDurationSeconds.EIGHT.getValue());\n\n\t\t//合并其他参数\n\t\tfinal Map<String, Object> additional = config.getAdditionalConfigMap();\n\t\tif (MapUtil.isNotEmpty(additional)) {\n\t\t\tparameters.putAll(additional);\n\t\t}\n\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"instances\", Collections.singletonList(instance));\n\t\tparamMap.put(\"parameters\", parameters);\n\t\treturn paramMap;\n\t}\n\n\tprivate Map<String, Object> buildPredictImageRequestMap(String prompt) {\n\t\tfinal Map<String, Object> instance = new HashMap<>();\n\t\tinstance.put(\"prompt\", prompt);\n\n\t\tfinal Map<String, Object> parameters = new HashMap<>();\n\t\t// 官方默认4，通常我们会按需设为1\n\t\tparameters.put(\"sampleCount\", GeminiCommon.GeminiImageCount.ONE.getCount());\n\t\t// 默认 1:1\n\t\tparameters.put(\"aspectRatio\", GeminiCommon.GeminiAspectRatio.SQUARE.getRatio());\n\t\t// 默认\n\t\tparameters.put(\"personGeneration\", GeminiCommon.GeminiPersonGeneration.ALLOW_ADULT.getValue());\n\n\t\t//合并其他参数\n\t\tfinal Map<String, Object> additional = config.getAdditionalConfigMap();\n\t\tif (MapUtil.isNotEmpty(additional)) {\n\t\t\tparameters.putAll(additional);\n\t\t\tif (additional.containsKey(\"numberOfImages\")) {\n\t\t\t\tparameters.put(\"sampleCount\", additional.get(\"numberOfImages\"));\n\t\t\t}\n\t\t}\n\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"instances\", Collections.singletonList(instance));\n\t\tparamMap.put(\"parameters\", parameters);\n\t\treturn paramMap;\n\t}\n\n\tprivate Map<String, Object> buildTextToSpeechRequestMap(String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tfinal Map<String, Object> part = new HashMap<>();\n\t\tpart.put(\"text\", prompt);\n\n\t\tfinal Map<String, Object> content = new HashMap<>();\n\t\tcontent.put(\"role\", \"user\");\n\t\tcontent.put(\"parts\", Collections.singletonList(part));\n\t\tparamMap.put(\"contents\", Collections.singletonList(content));\n\n\t\tfinal Map<String, Object> generationConfig = new HashMap<>();\n\t\t//基础固定参数：必须指定返回音频格式\n\t\tgenerationConfig.put(\"response_modalities\", Collections.singletonList(\"AUDIO\"));\n\n\t\t//合并其他参数\n\t\tfinal Map<String, Object> additionalMap = config.getAdditionalConfigMap();\n\t\tif (MapUtil.isNotEmpty(additionalMap)) {\n\t\t\tgenerationConfig.putAll(additionalMap);\n\t\t}\n\t\tparamMap.put(\"generation_config\", generationConfig);\n\t\treturn paramMap;\n\t}\n\n\t/**\n\t * 获取远程文件的 MIME 类型\n\t *\n\t * @param fileUri 文件URI\n\t * @return MIME类型\n\t */\n\tprivate String getRemoteFileMimeType(String fileUri) {\n\t\ttry {\n\t\t\tfinal HttpRequest httpRequest = HttpRequest.get(fileUri)\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\tString responseBody = httpRequest.execute().body();\n\t\t\tfinal JSONObject json = JSONUtil.parseObj(responseBody);\n\n\t\t\t//提取服务端的mimeType\n\t\t\tString mimeType = json.getStr(\"mimeType\");\n\t\t\tif (StrUtil.isNotBlank(mimeType)) {\n\t\t\t\treturn mimeType;\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"Failed to get remote file MIME type\", e.getMessage());\n\t\t}\n\t\treturn \"application/octet-stream\";\n\t}\n\n\t/**\n\t * 发送Get请求\n\t * @param endpoint 请求节点\n\t * @return 请求响应\n\t */\n\t@Override\n\tprotected HttpResponse sendGet(String endpoint) {\n\t\t//链式构建请求\n\t\ttry {\n\t\t\t//设置超时3分钟\n\t\t\tfinal HttpRequest httpRequest = HttpRequest.get(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\treturn httpRequest.execute();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new AIException(\"Failed to send GET request: \" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t@Override\n\tprotected HttpResponse sendPost(String endpoint, String paramJson) {\n\t\t//链式构建请求\n\t\ttry {\n\t\t\tfinal HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.CONTENT_TYPE, \"application/json\")\n\t\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t\t.body(paramJson)\n\t\t\t\t.timeout(config.getTimeout());\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\thttpRequest.setProxy(config.getProxy());\n\t\t\t}\n\t\t\treturn httpRequest.execute();\n\t\t} catch (final Exception e) {\n\t\t\tthrow new AIException(\"Failed to send POST request：\" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t/**\n\t * 支持流式返回的 POST 请求\n\t *\n\t * @param endpoint 请求地址\n\t * @param paramMap 请求参数\n\t * @param callback 流式数据回调函数\n\t */\n\t@Override\n\tprotected void sendPostStream(String endpoint, final Map<String, Object> paramMap, final Consumer<String> callback) {\n\t\tHttpURLConnection connection = null;\n\t\ttry {\n\t\t\t// 创建连接\n\t\t\tURL apiUrl = new URL(config.getApiUrl() + endpoint);\n\t\t\tconnection = (HttpURLConnection) apiUrl.openConnection();\n\t\t\tif (config.getHasProxy()) {\n\t\t\t\tconnection = (HttpURLConnection) apiUrl.openConnection(config.getProxy());\n\t\t\t}\n\t\t\tconnection.setRequestMethod(Method.POST.name());\n\t\t\tconnection.setRequestProperty(Header.CONTENT_TYPE.getValue(), \"application/json\");\n\t\t\tconnection.setRequestProperty(\"x-goog-api-key\", config.getApiKey());\n\t\t\tconnection.setDoOutput(true);\n\t\t\t//5分钟\n\t\t\tconnection.setReadTimeout(config.getReadTimeout());\n\t\t\t//3分钟\n\t\t\tconnection.setConnectTimeout(config.getTimeout());\n\t\t\t// 发送请求体\n\t\t\ttry (OutputStream os = connection.getOutputStream()) {\n\t\t\t\tString jsonInputString = JSONUtil.toJsonStr(paramMap);\n\t\t\t\tos.write(jsonInputString.getBytes());\n\t\t\t\tos.flush();\n\t\t\t}\n\n\t\t\t// 读取流式响应\n\t\t\ttry (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {\n\t\t\t\tString line;\n\t\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\t\t// 调用回调函数处理每一行数据\n\t\t\t\t\tcallback.accept(line);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tcallback.accept(\"{\\\"error\\\": \\\"\" + e.getMessage() + \"\\\"}\");\n\t\t} finally {\n\t\t\t// 关闭连接\n\t\t\tif (connection != null) {\n\t\t\t\tconnection.disconnect();\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/gemini/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对gemini的封装实现\n *\n * @author elichow\n * @since 5.8.43\n */\n\npackage cn.hutool.ai.model.gemini;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\n/**\n * grok公共类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class GrokCommon {\n\n\t//grok视觉参数\n\tpublic enum GrokVision {\n\n\t\tAUTO(\"auto\"),\n\t\tLOW(\"low\"),\n\t\tHIGH(\"high\");\n\n\t\tprivate final String detail;\n\n\t\tGrokVision(String detail) {\n\t\t\tthis.detail = detail;\n\t\t}\n\n\t\tpublic String getDetail() {\n\t\t\treturn detail;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * Grok配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class GrokConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"https://api.x.ai/v1\";\n\n\tprivate final String DEFAULT_MODEL = Models.Grok.GROK_2_1212.getModel();\n\n\n\tpublic GrokConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic GrokConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"grok\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**r\n * 创建Grok服务实现类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class GrokProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"grok\";\n\t}\n\n\t@Override\n\tpublic GrokService create(final AIConfig config) {\n\t\treturn new GrokServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * grok支持的扩展接口\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface GrokService extends AIService {\n\n\t/**\n\t * 创建消息回复\n\t *\n\t * @param prompt   题词\n\t * @param maxToken 最大token\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String message(String prompt, int maxToken){\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn message(messages, maxToken);\n\t}\n\n\t/**\n\t * 创建消息回复-SSE流式输出\n\t *\n\t * @param prompt   题词\n\t * @param maxToken 最大token\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void message(String prompt, int maxToken, final Consumer<String> callback){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\tmessage(messages, maxToken, callback);\n\t}\n\n\t/**\n\t * 创建消息回复\n\t *\n\t * @param messages messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @param maxToken 最大token\n\t * @return AI回答\n\t * @since 5.8.39\n\t */\n\tString message(List<Message> messages, int maxToken);\n\n\t/**\n\t * 创建消息回复-SSE流式输出\n\t *\n\t * @param messages messages 由对话组成的消息列表。如系统人设，背景信息等，用户自定义的信息\n\t * @param maxToken 最大token\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid message(List<Message> messages, int maxToken, final Consumer<String> callback);\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString chatVision(String prompt, final List<String> images, String detail);\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatVision(String prompt, final List<String> images) {\n\t\treturn chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail());\n\t}\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 传入｜的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatVision(String prompt, final List<String> images, final Consumer<String> callback){\n\t\tchatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail(), callback);\n\t}\n\n\t/**\n\t * 列出所有model列表\n\t *\n\t * @return model列表\n\t * @since 5.8.38\n\t */\n\tString models();\n\n\t/**\n\t * 获取模型信息\n\t *\n\t * @param modelId model ID\n\t * @return model信息\n\t * @since 5.8.38\n\t */\n\tString getModel(String modelId);\n\n\t/**\n\t * 列出所有语言model\n\t *\n\t * @return languageModel列表\n\t * @since 5.8.38\n\t */\n\tString languageModels();\n\n\t/**\n\t * 获取语言模型信息\n\t *\n\t * @param modelId model ID\n\t * @return model信息\n\t * @since 5.8.38\n\t */\n\tString getLanguageModel(String modelId);\n\n\t/**\n\t * 分词：可以将文本转换为模型可理解的 token 信息\n\t *\n\t * @param text 需要分词的内容\n\t * @return 分词结果\n\t * @since 5.8.38\n\t */\n\tString tokenizeText(String text);\n\n\t/**\n\t * 从延迟对话中获取结果\n\t *\n\t * @param requestId 延迟对话中的延迟请求ID\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString deferredCompletion(String requestId);\n\n\t/**\n\t * 文生图\n\t * 请设置config中model为支持图片功能的模型，目前支持GROK_2_IMAGE\n\t *\n\t * @param prompt 题词\n\t * @return 包含生成图片的url\n\t * @since 5.8.39\n\t */\n\tString imagesGenerations(String prompt);\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONUtil;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * Grok服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class GrokServiceImpl extends BaseAIService implements GrokService {\n\n\t//对话补全\n\tprivate final String CHAT_ENDPOINT = \"/chat/completions\";\n\t//创建消息回复\n\tprivate final String MESSAGES = \"/messages\";\n\t//列出模型\n\tprivate final String MODELS_ENDPOINT = \"/models\";\n\t//列出语言模型\n\tprivate final String LANGUAGE_MODELS = \"/language-models\";\n\t//分词\n\tprivate final String TOKENIZE_TEXT = \"/tokenize-text\";\n\t//获取延迟对话\n\tprivate final String DEFERRED_COMPLETION = \"/chat/deferred-completion\";\n\t//文生图\n\tprivate final String IMAGES_GENERATIONS = \"/images/generations\";\n\n\tpublic GrokServiceImpl(final AIConfig config) {\n\t\t//初始化grok客户端\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tString paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(List<Message> messages,Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"grok-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String message(final List<Message> messages, int maxToken) {\n\t\tString paramJson = buildMessageRequestBody(messages, maxToken);\n\t\tfinal HttpResponse response = sendPost(MESSAGES, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void message(List<Message> messages, int maxToken, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildMessageStreamRequestBody(messages, maxToken);\n\t\tThreadUtil.newThread(() -> sendPostStream(MESSAGES, paramMap, callback::accept), \"grok-message-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatVision(String prompt, final List<String> images, String detail) {\n\t\tString paramJson = buildChatVisionRequestBody(prompt, images, detail);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"grok-chatVision-sse\").start();\n\t}\n\n\t@Override\n\tpublic String models() {\n\t\tfinal HttpResponse response = sendGet(MODELS_ENDPOINT);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getModel(String modelId) {\n\t\tfinal HttpResponse response = sendGet(MODELS_ENDPOINT + \"/\" + modelId);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String languageModels() {\n\t\tfinal HttpResponse response = sendGet(LANGUAGE_MODELS);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getLanguageModel(String modelId) {\n\t\tfinal HttpResponse response = sendGet(LANGUAGE_MODELS + \"/\" + modelId);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String tokenizeText(String text) {\n\t\tString paramJson = buildTokenizeRequestBody(text);\n\t\tfinal HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String deferredCompletion(String requestId) {\n\t\tfinal HttpResponse response = sendGet(DEFERRED_COMPLETION + \"/\" + requestId);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String imagesGenerations(String prompt) {\n\t\tString paramJson = buildImagesGenerationsRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);\n\t\treturn response.body();\n\t}\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建chatVision请求体\n\tprivate String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn paramMap;\n\t}\n\n\t//构建消息回复请求体\n\tprivate String buildMessageRequestBody(final List<Message> messages, int maxToken) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"max_tokens\", maxToken);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildMessageStreamRequestBody(final List<Message> messages, int maxToken) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"max_tokens\", maxToken);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建分词请求体\n\tprivate String buildTokenizeRequestBody(String text) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"text\", text);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建文生图请求体\n\tprivate String buildImagesGenerationsRequestBody(String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/grok/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对grok的封装实现\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.model.grok;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\n/**\n * hutool公共类\n *\n * @author elichow\n * @since 5.8.39\n */\npublic class HutoolCommon {\n\n\t//hutool视觉参数\n\tpublic enum HutoolVision {\n\n\t\tAUTO(\"auto\"),\n\t\tLOW(\"low\"),\n\t\tHIGH(\"high\");\n\n\t\tprivate final String detail;\n\n\t\tHutoolVision(String detail) {\n\t\t\tthis.detail = detail;\n\t\t}\n\n\t\tpublic String getDetail() {\n\t\t\treturn detail;\n\t\t}\n\t}\n\n\t//hutool音频参数\n\tpublic enum HutoolSpeech {\n\n\t\tALLOY(\"alloy\"),\n\t\tASH(\"ash\"),\n\t\tCORAL(\"coral\"),\n\t\tECHO(\"echo\"),\n\t\tFABLE(\"fable\"),\n\t\tONYX(\"onyx\"),\n\t\tNOVA(\"nova\"),\n\t\tSAGE(\"sage\"),\n\t\tSHIMMER(\"shimmer\");\n\n\t\tprivate final String voice;\n\n\t\tHutoolSpeech(String voice) {\n\t\t\tthis.voice = voice;\n\t\t}\n\n\t\tpublic String getVoice() {\n\t\t\treturn voice;\n\t\t}\n\t}\n\n\t//hutool视频生成参数\n\tpublic enum HutoolVideo {\n\n\t\t//宽高比例\n\t\tRATIO_16_9(\"--rt\", \"16:9\"),//[1280, 720]\n\t\tRATIO_4_3(\"--rt\", \"4:3\"),//[960, 720]\n\t\tRATIO_1_1(\"--rt\", \"1:1\"),//[720, 720]\n\t\tRATIO_3_4(\"--rt\", \"3:4\"),//[720, 960]\n\t\tRATIO_9_16(\"--rt\", \"9:16\"),//[720, 1280]\n\t\tRATIO_21_9(\"--rt\", \"21:9\"),//[1280, 544]\n\n\t\t//生成视频时长\n\t\tDURATION_5(\"--dur\", 5),//文生视频，图生视频\n\t\tDURATION_10(\"--dur\", 10),//文生视频\n\n\t\t//帧率，即一秒时间内视频画面数量\n\t\tFPS_5(\"--fps\", 24),\n\n\t\t//视频分辨率\n\t\tRESOLUTION_5(\"--rs\", \"720p\"),\n\n\t\t//生成视频是否包含水印\n\t\tWATERMARK_TRUE(\"--wm\", true),\n\t\tWATERMARK_FALSE(\"--wm\", false);\n\n\t\tprivate final String type;\n\t\tprivate final Object value;\n\n\t\tHutoolVideo(String type, Object value) {\n\t\t\tthis.type = type;\n\t\t\tthis.value = value;\n\t\t}\n\n\t\tpublic String getType() {\n\t\t\treturn type;\n\t\t}\n\n\t\tpublic Object getValue() {\n\t\t\tif (value instanceof String) {\n\t\t\t\treturn (String) value;\n\t\t\t} else if (value instanceof Integer) {\n\t\t\t\treturn (Integer) value;\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn (Boolean) value;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * Hutool配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.39\n */\npublic class HutoolConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"https://api.hutool.cn/ai/api\";\n\n\tprivate final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel();\n\n\tpublic HutoolConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic HutoolConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"hutool\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**r\n * 创建Hutool服务实现类\n *\n * @author elichow\n * @since 5.8.39\n */\npublic class HutoolProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"hutool\";\n\t}\n\n\t@Override\n\tpublic HutoolService create(final AIConfig config) {\n\t\treturn new HutoolServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\nimport cn.hutool.ai.core.AIService;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * hutool支持的扩展接口\n *\n * @author elichow\n * @since 5.8.39\n */\npublic interface HutoolService extends AIService {\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @return AI回答\n\t * @since 5.8.39\n\t */\n\tString chatVision(String prompt, final List<String> images, String detail);\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @return AI回答\n\t * @since 5.8.39\n\t */\n\tdefault String chatVision(String prompt, final List<String> images) {\n\t\treturn chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail());\n\t}\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 传入｜的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatVision(String prompt, final List<String> images, final Consumer<String> callback){\n\t\tchatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback);\n\t}\n\n\t/**\n\t * 分词：可以将文本转换为模型可理解的 token 信息\n\t *\n\t * @param text 需要分词的内容\n\t * @return 分词结果\n\t * @since 5.8.39\n\t */\n\tString tokenizeText(String text);\n\n\t/**\n\t * 文生图\n\t *\n\t * @param prompt 题词\n\t * @return 包含生成图片的url\n\t * @since 5.8.39\n\t */\n\tString imagesGenerations(String prompt);\n\n\t/**\n\t * 图文向量化：仅支持单一文本、单张图片或文本与图片的组合输入（即一段文本 + 一张图片），暂不支持批量文本 / 图片的同时处理\n\t *\n\t * @param text  需要向量化的内容\n\t * @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)\n\t * @return 处理后的向量信息\n\t * @since 5.8.39\n\t */\n\tString embeddingVision(String text, String image);\n\n\t/**\n\t * TTS文本转语音\n\t *\n\t * @param input 需要转成语音的文本\n\t * @param voice AI的音色\n\t * @return 返回的音频mp3文件流\n\t * @since 5.8.39\n\t */\n\tInputStream tts(String input, final HutoolCommon.HutoolSpeech voice);\n\n\t/**\n\t * TTS文本转语音\n\t *\n\t * @param input 需要转成语音的文本\n\t * @return 返回的音频mp3文件流\n\t * @since 5.8.39\n\t */\n\tdefault InputStream tts(String input) {\n\t\treturn tts(input, HutoolCommon.HutoolSpeech.ALLOY);\n\t}\n\n\t/**\n\t * STT音频转文本\n\t *\n\t * @param file 需要转成文本的音频文件\n\t * @return 返回的文本内容\n\t * @since 5.8.39\n\t */\n\tString stt(final File file);\n\n\t/**\n\t * 创建视频生成任务\n\t *\n\t * @param text        文本提示词\n\t * @param image       图片/或者图片Base64编码图片(URI形式)\n\t * @param videoParams 视频参数列表\n\t * @return 生成任务id\n\t * @since 5.8.39\n\t */\n\tString videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams);\n\n\t/**\n\t * 创建视频生成任务\n\t *\n\t * @param text  文本提示词\n\t * @param image 图片/或者图片Base64编码图片(URI形式)\n\t * @return 生成任务id\n\t * @since 5.8.39\n\t */\n\tdefault String videoTasks(String text, String image) {\n\t\treturn videoTasks(text, image, null);\n\t}\n\n\t/**\n\t * 查询视频生成任务信息\n\t *\n\t * @param taskId 通过创建生成视频任务返回的生成任务id\n\t * @return 生成任务信息\n\t * @since 5.8.39\n\t */\n\tString getVideoTasksInfo(String taskId);\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\nimport cn.hutool.ai.AIException;\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * Hutool服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.39\n */\npublic class HutoolServiceImpl extends BaseAIService implements HutoolService {\n\n\t//对话补全\n\tprivate final String CHAT_ENDPOINT = \"/chat/completions\";\n\t//分词\n\tprivate final String TOKENIZE_TEXT = \"/tokenize/text\";\n\t//文生图\n\tprivate final String IMAGES_GENERATIONS = \"/images/generations\";\n\t//图文向量化\n\tprivate final String EMBEDDING_VISION = \"/embeddings/multimodal\";\n\t//文本转语音\n\tprivate final String TTS = \"/audio/tts\";\n\t//语音转文本\n\tprivate final String STT = \"/audio/stt\";\n\t//创建视频生成任务\n\tprivate final String CREATE_VIDEO = \"/video/generations\";\n\n\tpublic HutoolServiceImpl(final AIConfig config) {\n\t\t//初始化hutool客户端\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tString paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(List<Message> messages,Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"hutool-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatVision(String prompt, final List<String> images, String detail) {\n\t\tString paramJson = buildChatVisionRequestBody(prompt, images, detail);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);\n\t\tSystem.out.println(JSONUtil.toJsonStr(paramMap));\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"hutool-chatVision-sse\").start();\n\t}\n\n\t@Override\n\tpublic String tokenizeText(String text) {\n\t\tString paramJson = buildTokenizeRequestBody(text);\n\t\tfinal HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String imagesGenerations(String prompt) {\n\t\tString paramJson = buildImagesGenerationsRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);\n\t\treturn response.body();\n\t}\n\n\n\t@Override\n\tpublic String embeddingVision(String text, String image) {\n\t\tString paramJson = buildEmbeddingVisionRequestBody(text, image);\n\t\tfinal HttpResponse response = sendPost(EMBEDDING_VISION, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) {\n\t\ttry {\n\t\t\tString paramJson = buildTTSRequestBody(input, voice.getVoice());\n\t\t\tfinal HttpResponse response = sendPost(TTS, paramJson);\n\n\t\t\t// 检查响应内容类型\n\t\t\tString contentType = response.header(\"Content-Type\");\n\t\t\tif (contentType != null && contentType.startsWith(\"application/json\")) {\n\t\t\t\t// 如果是JSON响应，说明有错误\n\t\t\t\tString errorBody = response.body();\n\t\t\t\tthrow new AIException(\"TTS请求失败: \" + errorBody);\n\t\t\t}\n\t\t\t// 默认返回音频流\n\t\t\treturn response.bodyStream();\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"TTS处理失败: \" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String stt(final File file) {\n\t\tfinal Map<String, Object> paramMap = buildSTTRequestBody(file);\n\t\tfinal HttpResponse response = sendFormData(STT, paramMap);\n\t\treturn response.body();\n\t}\n\n\n\t@Override\n\tpublic String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {\n\t\tString paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);\n\t\tfinal HttpResponse response = sendPost(CREATE_VIDEO, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getVideoTasksInfo(String taskId) {\n\t\tfinal HttpResponse response = sendGet(CREATE_VIDEO + \"/\" + taskId);\n\t\treturn response.body();\n\t}\n\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建chatVision请求体\n\tprivate String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn paramMap;\n\t}\n\n\n\t//构建分词请求体\n\tprivate String buildTokenizeRequestBody(String text) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"text\", text);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建文生图请求体\n\tprivate String buildImagesGenerationsRequestBody(String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建图文向量化请求体\n\tprivate String buildEmbeddingVisionRequestBody(String text, String image) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\n\t\tfinal List<Object> input = new ArrayList<>();\n\t\t//添加文本参数\n\t\tif (!StrUtil.isBlank(text)) {\n\t\t\tfinal Map<String, String> textMap = new HashMap<>();\n\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\ttextMap.put(\"text\", text);\n\t\t\tinput.add(textMap);\n\t\t}\n\t\t//添加图片参数\n\t\tif (!StrUtil.isBlank(image)) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", image);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tinput.add(imgUrlMap);\n\t\t}\n\n\t\tparamMap.put(\"input\", input);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\tSystem.out.println(JSONUtil.toJsonStr(paramMap));\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\n\t//构建TTS请求体\n\tprivate String buildTTSRequestBody(String input, String voice) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"input\", input);\n\t\tparamMap.put(\"voice\", voice);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建STT请求体\n\tprivate Map<String, Object> buildSTTRequestBody(final File file) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"file\", file);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建创建视频任务请求体\n\tprivate String buildGenerationsTasksRequestBody(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\n\t\tfinal List<Object> content = new ArrayList<>();\n\t\t//添加文本参数\n\t\tfinal Map<String, String> textMap = new HashMap<>();\n\t\tif (!StrUtil.isBlank(text)) {\n\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\ttextMap.put(\"text\", text);\n\t\t\tcontent.add(textMap);\n\t\t}\n\t\t//添加图片参数\n\t\tif (!StrUtil.isBlank(image)) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", image);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\t//添加视频参数\n\t\tif (videoParams != null && !videoParams.isEmpty()) {\n\t\t\t//如果有文本参数就加在后面\n\t\t\tif (textMap != null && !textMap.isEmpty()) {\n\t\t\t\tint textIndex = content.indexOf(textMap);\n\t\t\t\tStringBuilder textBuilder = new StringBuilder(text);\n\t\t\t\tfor (HutoolCommon.HutoolVideo videoParam : videoParams) {\n\t\t\t\t\ttextBuilder.append(\" \").append(videoParam.getType()).append(\" \").append(videoParam.getValue());\n\t\t\t\t}\n\t\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\t\ttextMap.put(\"text\", textBuilder.toString());\n\n\t\t\t\tif (textIndex != -1) {\n\t\t\t\t\tcontent.set(textIndex, textMap);\n\t\t\t\t} else {\n\t\t\t\t\tcontent.add(textMap);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t//如果没有文本参数就重新增加\n\t\t\t\tStringBuilder textBuilder = new StringBuilder();\n\t\t\t\tfor (HutoolCommon.HutoolVideo videoParam : videoParams) {\n\t\t\t\t\ttextBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(\" \");\n\t\t\t\t}\n\t\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\t\ttextMap.put(\"text\", textBuilder.toString());\n\t\t\t\tcontent.add(textMap);\n\t\t\t}\n\t\t}\n\n\t\tparamMap.put(\"content\", content);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\tSystem.out.println(JSONUtil.toJsonStr(paramMap));\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对hutool的封装实现\n *\n * @author elichow\n * @since 5.8.39\n */\n\npackage cn.hutool.ai.model.hutool;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\n/**\n * Ollama公共类\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npublic class OllamaCommon {\n\n\t/**\n\t * Ollama模型格式枚举\n\t */\n\tpublic enum OllamaFormat {\n\t\t/**\n\t\t * JSON格式\n\t\t */\n\t\tJSON(\"json\"),\n\t\t/**\n\t\t * 无格式\n\t\t */\n\t\tNONE(\"\");\n\n\t\tprivate final String format;\n\n\t\tOllamaFormat(String format) {\n\t\t\tthis.format = format;\n\t\t}\n\n\t\tpublic String getFormat() {\n\t\t\treturn format;\n\t\t}\n\t}\n\n\t/**\n\t * Ollama选项常量\n\t */\n\tpublic static class Options {\n\t\t/**\n\t\t * 温度参数\n\t\t */\n\t\tpublic static final String TEMPERATURE = \"temperature\";\n\t\t/**\n\t\t * top_p参数\n\t\t */\n\t\tpublic static final String TOP_P = \"top_p\";\n\t\t/**\n\t\t * top_k参数\n\t\t */\n\t\tpublic static final String TOP_K = \"top_k\";\n\t\t/**\n\t\t * 最大token数\n\t\t */\n\t\tpublic static final String NUM_PREDICT = \"num_predict\";\n\t\t/**\n\t\t * 随机种子\n\t\t */\n\t\tpublic static final String SEED = \"seed\";\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * Ollama配置类，初始化API接口地址，设置默认的模型\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npublic class OllamaConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"http://localhost:11434\";\n\n\tprivate final String DEFAULT_MODEL = Models.Ollama.QWEN3_32B.getModel();\n\n\tpublic OllamaConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic OllamaConfig(String apiUrl) {\n\t\tthis();\n\t\tsetApiUrl(apiUrl);\n\t}\n\n\tpublic OllamaConfig(String apiUrl, String model) {\n\t\tthis();\n\t\tsetApiUrl(apiUrl);\n\t\tsetModel(model);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"ollama\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**\n * 创建Ollama服务实现类\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npublic class OllamaProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"ollama\";\n\t}\n\n\t@Override\n\tpublic OllamaService create(final AIConfig config) {\n\t\treturn new OllamaServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Ollama特有的功能\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npublic interface OllamaService extends AIService {\n\n\t/**\n\t * 生成文本补全\n\t *\n\t * @param prompt 输入提示\n\t * @return AI回答\n\t * @since 5.8.40\n\t */\n\tString generate(String prompt);\n\n\t/**\n\t * 生成文本补全-SSE流式输出\n\t *\n\t * @param prompt 输入提示\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.40\n\t */\n\tvoid generate(String prompt, Consumer<String> callback);\n\n\t/**\n\t * 生成文本补全（带选项）\n\t *\n\t * @param prompt 输入提示\n\t * @param format 响应格式\n\t * @return AI回答\n\t * @since 5.8.40\n\t */\n\tString generate(String prompt, String format);\n\n\t/**\n\t * 生成文本补全（带选项）-SSE流式输出\n\t *\n\t * @param prompt 输入提示\n\t * @param format 响应格式\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.40\n\t */\n\tvoid generate(String prompt, String format, Consumer<String> callback);\n\n\t/**\n\t * 生成文本嵌入向量\n\t *\n\t * @param prompt 输入文本\n\t * @return 嵌入向量结果\n\t * @since 5.8.40\n\t */\n\tString embeddings(String prompt);\n\n\t/**\n\t * 列出本地可用的模型\n\t *\n\t * @return 模型列表\n\t * @since 5.8.40\n\t */\n\tString listModels();\n\n\t/**\n\t * 显示模型信息\n\t *\n\t * @param modelName 模型名称\n\t * @return 模型信息\n\t * @since 5.8.40\n\t */\n\tString showModel(String modelName);\n\n\t/**\n\t * 拉取模型\n\t *\n\t * @param modelName 模型名称\n\t * @return 拉取结果\n\t * @since 5.8.40\n\t */\n\tString pullModel(String modelName);\n\n\t/**\n\t * 删除模型\n\t *\n\t * @param modelName 模型名称\n\t * @return 删除结果\n\t * @since 5.8.40\n\t */\n\tString deleteModel(String modelName);\n\n\t/**\n\t * 复制模型\n\t *\n\t * @param source 源模型名称\n\t * @param destination 目标模型名称\n\t * @return 复制结果\n\t * @since 5.8.40\n\t */\n\tString copyModel(String source, String destination);\n\n\t/**\n\t * 简化的对话方法\n\t *\n\t * @param prompt 对话题词\n\t * @return AI回答\n\t * @since 5.8.40\n\t */\n\tdefault String chat(String prompt) {\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn chat(messages);\n\t}\n\n\t/**\n\t * 简化的对话方法-SSE流式输出\n\t *\n\t * @param prompt 对话题词\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.40\n\t */\n\tdefault void chat(String prompt, Consumer<String> callback) {\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\tchat(messages, callback);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/OllamaServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\nimport cn.hutool.ai.AIException;\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.Header;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * Ollama服务，AI具体功能的实现\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npublic class OllamaServiceImpl extends BaseAIService implements OllamaService {\n\n\t// 对话补全\n\tprivate static final String CHAT_ENDPOINT = \"/api/chat\";\n\t// 文本生成\n\tprivate static final String GENERATE_ENDPOINT = \"/api/generate\";\n\t// 文本嵌入\n\tprivate static final String EMBEDDINGS_ENDPOINT = \"/api/embeddings\";\n\t// 列出模型\n\tprivate static final String LIST_MODELS_ENDPOINT = \"/api/tags\";\n\t// 显示模型信息\n\tprivate static final String SHOW_MODEL_ENDPOINT = \"/api/show\";\n\t// 拉取模型\n\tprivate static final String PULL_MODEL_ENDPOINT = \"/api/pull\";\n\t// 删除模型\n\tprivate static final String DELETE_MODEL_ENDPOINT = \"/api/delete\";\n\t// 复制模型\n\tprivate static final String COPY_MODEL_ENDPOINT = \"/api/copy\";\n\n\t/**\n\t * 构造函数\n\t *\n\t * @param config AI配置\n\t */\n\tpublic OllamaServiceImpl(final AIConfig config) {\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tfinal String paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\tJSONObject responseJson = JSONUtil.parseObj(response.body());\n\t\tObject errorMessage = BeanPath.create(\"error\").get(responseJson);\n\t\tif(errorMessage!=null){\n\t\t\tthrow new RuntimeException(errorMessage.toString());\n\t\t}\n\t\treturn BeanPath.create(\"message.content\").get(responseJson).toString();\n\t}\n\n\t@Override\n\tpublic void chat(final List<Message> messages, final Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"ollama-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String generate(String prompt) {\n\t\tfinal String paramJson = buildGenerateRequestBody(prompt, null);\n\t\tfinal HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void generate(String prompt, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildGenerateStreamRequestBody(prompt, null);\n\t\tThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), \"ollama-generate-sse\").start();\n\t}\n\n\t@Override\n\tpublic String generate(String prompt, String format) {\n\t\tfinal String paramJson = buildGenerateRequestBody(prompt, format);\n\t\tfinal HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void generate(String prompt, String format, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildGenerateStreamRequestBody(prompt, format);\n\t\tThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), \"ollama-generate-sse\").start();\n\t}\n\n\t@Override\n\tpublic String embeddings(String prompt) {\n\t\tfinal String paramJson = buildEmbeddingsRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(EMBEDDINGS_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String listModels() {\n\t\tfinal HttpResponse response = sendGet(LIST_MODELS_ENDPOINT);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String showModel(String modelName) {\n\t\tfinal String paramJson = buildShowModelRequestBody(modelName);\n\t\tfinal HttpResponse response = sendPost(SHOW_MODEL_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String pullModel(String modelName) {\n\t\tfinal String paramJson = buildPullModelRequestBody(modelName);\n\t\tfinal HttpResponse response = sendPost(PULL_MODEL_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String deleteModel(String modelName) {\n\t\tfinal String paramJson = buildDeleteModelRequestBody(modelName);\n\t\tfinal HttpResponse response = sendDeleteRequest(DELETE_MODEL_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String copyModel(String source, String destination) {\n\t\tfinal String paramJson = buildCopyModelRequestBody(source, destination);\n\t\tfinal HttpResponse response = sendPost(COPY_MODEL_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\",false);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t// 合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建chatStream请求体\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t// 合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t// 构建generate请求体\n\tprivate String buildGenerateRequestBody(final String prompt, final String format) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\tparamMap.put(\"format\", format);\n\t\t}\n\t\t// 合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建generateStream请求体\n\tprivate Map<String, Object> buildGenerateStreamRequestBody(final String prompt, final String format) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\tparamMap.put(\"format\", format);\n\t\t}\n\t\t// 合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t// 构建embeddings请求体\n\tprivate String buildEmbeddingsRequestBody(final String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t// 合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建showModel请求体\n\tprivate String buildShowModelRequestBody(final String modelName) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"name\", modelName);\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建pullModel请求体\n\tprivate String buildPullModelRequestBody(final String modelName) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"name\", modelName);\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t// 构建deleteModel请求体\n\tprivate String buildDeleteModelRequestBody(final String modelName) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"name\", modelName);\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t/**\n\t * 发送DELETE请求\n\t *\n\t * @param endpoint 请求端点\n\t * @param paramJson 请求参数JSON\n\t * @return 响应结果\n\t */\n\tprivate HttpResponse sendDeleteRequest(String endpoint, String paramJson) {\n\t\ttry {\n\t\t\treturn HttpRequest.delete(config.getApiUrl() + endpoint)\n\t\t\t\t.header(Header.CONTENT_TYPE, \"application/json\")\n\t\t\t\t.header(Header.ACCEPT, \"application/json\")\n\t\t\t\t.body(paramJson)\n\t\t\t\t.timeout(config.getTimeout())\n\t\t\t\t.execute();\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"Failed to send DELETE request: \" + e.getMessage(), e);\n\t\t}\n\t}\n\n\t// 构建copyModel请求体\n\tprivate String buildCopyModelRequestBody(final String source, final String destination) {\n\t\tMap<String, Object> requestBody = new HashMap<>();\n\t\trequestBody.put(\"source\", source);\n\t\trequestBody.put(\"destination\", destination);\n\t\treturn JSONUtil.toJsonStr(requestBody);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/ollama/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对Ollama的封装实现.\n * <p>\n * 使用方法：\n * // 创建AI服务\n * <pre>{@code\n * OllamaService aiService = AIServiceFactory.getAIService(\n * new AIConfigBuilder(ModelName.OLLAMA.getValue())\n * .setApiUrl(\"http://localhost:11434\")\n * .setModel(\"qwen2.5-coder:32b\")\n * .build(),\n * OllamaService.class\n * );\n *\n * // 构造上下文\n * List<Message> messageList=new ArrayList<>();\n * messageList.add(new Message(\"system\",\"你是一个疯疯癫癫的机器人\"));\n * messageList.add(new Message(\"user\",\"你能帮我做什么\"));\n *\n * // 输出对话结果\n * Console.log(aiService.chat(messageList));\n * }</pre>\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\npackage cn.hutool.ai.model.ollama;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiCommon.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\n/**\n * openai公共类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class OpenaiCommon {\n\n\t//openai推理参数\n\tpublic enum OpenaiReasoning {\n\n\t\tLOW(\"low\"),\n\t\tMEDIUM(\"medium\"),\n\t\tHIGH(\"high\");\n\n\t\tprivate final String effort;\n\n\t\tOpenaiReasoning(String effort) {\n\t\t\tthis.effort = effort;\n\t\t}\n\n\t\tpublic String getEffort() {\n\t\t\treturn effort;\n\t\t}\n\t}\n\n\t//openai视觉参数\n\tpublic enum OpenaiVision {\n\n\t\tAUTO(\"auto\"),\n\t\tLOW(\"low\"),\n\t\tHIGH(\"high\");\n\n\t\tprivate final String detail;\n\n\t\tOpenaiVision(String detail) {\n\t\t\tthis.detail = detail;\n\t\t}\n\n\t\tpublic String getDetail() {\n\t\t\treturn detail;\n\t\t}\n\t}\n\n\t//openai音频参数\n\tpublic enum OpenaiSpeech {\n\n\t\tALLOY(\"alloy\"),\n\t\tASH(\"ash\"),\n\t\tCORAL(\"coral\"),\n\t\tECHO(\"echo\"),\n\t\tFABLE(\"fable\"),\n\t\tONYX(\"onyx\"),\n\t\tNOVA(\"nova\"),\n\t\tSAGE(\"sage\"),\n\t\tSHIMMER(\"shimmer\");\n\n\t\tprivate final String voice;\n\n\t\tOpenaiSpeech(String voice) {\n\t\t\tthis.voice = voice;\n\t\t}\n\n\t\tpublic String getVoice() {\n\t\t\treturn voice;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiConfig.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.BaseConfig;\n\n/**\n * openai配置类，初始化API接口地址，设置默认的模型\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class OpenaiConfig extends BaseConfig {\n\n\tprivate final String API_URL = \"https://api.openai.com/v1\";\n\n\tprivate final String DEFAULT_MODEL = Models.Openai.GPT_4O.getModel();\n\n\tpublic OpenaiConfig() {\n\t\tsetApiUrl(API_URL);\n\t\tsetModel(DEFAULT_MODEL);\n\t}\n\n\tpublic OpenaiConfig(String apiKey) {\n\t\tthis();\n\t\tsetApiKey(apiKey);\n\t}\n\n\t@Override\n\tpublic String getModelName() {\n\t\treturn \"openai\";\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiProvider.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIServiceProvider;\n\n/**\n * 创建Openai服务实现类\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class OpenaiProvider implements AIServiceProvider {\n\n\t@Override\n\tpublic String getServiceName() {\n\t\treturn \"openai\";\n\t}\n\n\t@Override\n\tpublic OpenaiService create(final AIConfig config) {\n\t\treturn new OpenaiServiceImpl(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiService.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * openai支持的扩展接口\n *\n * @author elichow\n * @since 5.8.38\n */\npublic interface OpenaiService extends AIService {\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString chatVision(String prompt, final List<String> images, String detail);\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 题词\n\t * @param images 图片列表/或者图片Base64编码图片列表(URI形式)\n\t * @param detail 手动设置图片的质量，取值范围high、low、auto,默认为auto\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);\n\n\n\t/**\n\t * 图像理解：模型会依据传入的图片信息以及问题，给出回复。\n\t *\n\t * @param prompt 题词\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatVision(String prompt, final List<String> images) {\n\t\treturn chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail());\n\t}\n\n\t/**\n\t * 图像理解-SSE流式输出\n\t *\n\t * @param prompt 题词\n\t * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatVision(String prompt, final List<String> images, final Consumer<String> callback){\n\t\tchatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail(), callback);\n\t}\n\n\t/**\n\t * 文生图 请设置config中model为支持图片功能的模型 DALL·E系列\n\t *\n\t * @param prompt 题词\n\t * @return 包含生成图片的url\n\t * @since 5.8.38\n\t */\n\tString imagesGenerations(String prompt);\n\n\t/**\n\t * 图片编辑 该方法仅支持 DALL·E 2 model\n\t *\n\t * @param prompt 题词\n\t * @param image  需要编辑的图像必须是 PNG 格式\n\t * @param mask   如果提供，则是一个与编辑图像大小相同的遮罩图像应该是灰度图，白色表示需要编辑的区域，黑色表示不需要编辑的区域。\n\t * @return 包含生成图片的url\n\t * @since 5.8.38\n\t */\n\tString imagesEdits(String prompt, final File image, final File mask);\n\n\t/**\n\t * 图片编辑 该方法仅支持 DALL·E 2 model\n\t *\n\t * @param prompt 题词\n\t * @param image  需要编辑的图像必须是 PNG 格式\n\t * @return 包含生成图片的url\n\t * @since 5.8.38\n\t */\n\tdefault String imagesEdits(String prompt, final File image) {\n\t\treturn imagesEdits(prompt, image, null);\n\t}\n\n\t/**\n\t * 图片变形 该方法仅支持 DALL·E 2 model\n\t *\n\t * @param image 需要变形的图像必须是 PNG 格式\n\t * @return 包含生成图片的url\n\t * @since 5.8.38\n\t */\n\tString imagesVariations(final File image);\n\n\t/**\n\t * TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列\n\t *\n\t * @param input 需要转成语音的文本\n\t * @param voice AI的音色\n\t * @return 返回的音频mp3文件流\n\t * @since 5.8.38\n\t */\n\tInputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice);\n\n\t/**\n\t * TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列\n\t *\n\t * @param input 需要转成语音的文本\n\t * @return 返回的音频mp3文件流\n\t * @since 5.8.38\n\t */\n\tdefault InputStream textToSpeech(String input) {\n\t\treturn textToSpeech(input, OpenaiCommon.OpenaiSpeech.ALLOY);\n\t}\n\n\t/**\n\t * STT音频转文本 请设置config中model为支持STT功能的模型 whisper\n\t *\n\t * @param file 需要转成文本的音频文件\n\t * @return 返回的文本内容\n\t * @since 5.8.38\n\t */\n\tString speechToText(final File file);\n\n\t/**\n\t * 文本向量化 请设置config中model为支持文本向量化功能的模型 text-embedding系列\n\t *\n\t * @param input 需要向量化的内容\n\t * @return 处理后的向量信息\n\t * @since 5.8.38\n\t */\n\tString embeddingText(String input);\n\n\t/**\n\t * 检查文本或图像是否具有潜在的危害性\n\t * 仅支持omni-moderation-latest和text-moderation-latest模型\n\t *\n\t * @param text   需要检查的文本\n\t * @param imgUrl 需要检查的图片地址\n\t * @return AI返回结果\n\t * @since 5.8.38\n\t */\n\tString moderations(String text, String imgUrl);\n\n\t/**\n\t * 检查文本是否具有潜在的危害性\n\t * 仅支持omni-moderation-latest和text-moderation-latest模型\n\t *\n\t * @param text 需要检查的文本\n\t * @return AI返回结果\n\t * @since 5.8.38\n\t */\n\tdefault String moderations(String text) {\n\t\treturn moderations(text, null);\n\t}\n\n\t/**\n\t * 推理chat\n\t * 支持o3-mini和o1\n\t *\n\t * @param prompt          对话题词\n\t * @param reasoningEffort 推理程度\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatReasoning(String prompt, String reasoningEffort){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\treturn chatReasoning(messages, reasoningEffort);\n\t}\n\n\t/**\n\t * 推理chat-SSE流式输出\n\t * 支持o3-mini和o1\n\t *\n\t * @param prompt          对话题词\n\t * @param reasoningEffort 推理程度\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatReasoning(String prompt, String reasoningEffort, final Consumer<String> callback){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\", \"You are a helpful assistant\"));\n\t\tmessages.add(new Message(\"user\", prompt));\n\t\tchatReasoning(messages, reasoningEffort, callback);\n\t}\n\n\t/**\n\t * 推理chat\n\t * 支持o3-mini和o1\n\t *\n\t * @param prompt 对话题词\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatReasoning(String prompt) {\n\t\treturn chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());\n\t}\n\n\t/**\n\t * 推理chat-SSE流式输出\n\t * 支持o3-mini和o1\n\t *\n\t * @param prompt 对话题词\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatReasoning(String prompt, final Consumer<String> callback) {\n\t\tchatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);\n\t}\n\n\t/**\n\t * 推理chat\n\t * 支持o3-mini和o1\n\t *\n\t * @param messages        消息列表\n\t * @param reasoningEffort 推理程度\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tString chatReasoning(final List<Message> messages, String reasoningEffort);\n\n\t/**\n\t * 推理chat-SSE流式输出\n\t * 支持o3-mini和o1\n\t *\n\t * @param messages        消息列表\n\t * @param reasoningEffort 推理程度\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tvoid chatReasoning(final List<Message> messages, String reasoningEffort, final Consumer<String> callback);\n\n\t/**\n\t * 推理chat\n\t * 支持o3-mini和o1\n\t *\n\t * @param messages 消息列表\n\t * @return AI回答\n\t * @since 5.8.38\n\t */\n\tdefault String chatReasoning(final List<Message> messages) {\n\t\treturn chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());\n\t}\n\n\t/**\n\t * 推理chat-SSE流式输出\n\t * 支持o3-mini和o1\n\t *\n\t * @param messages 消息列表\n\t * @param callback 流式数据回调函数\n\t * @since 5.8.39\n\t */\n\tdefault void chatReasoning(final List<Message> messages, final Consumer<String> callback) {\n\t\tchatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/OpenaiServiceImpl.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.BaseAIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * openai服务，AI具体功能的实现\n *\n * @author elichow\n * @since 5.8.38\n */\npublic class OpenaiServiceImpl extends BaseAIService implements OpenaiService {\n\n\t//对话\n\tprivate final String CHAT_ENDPOINT = \"/chat/completions\";\n\t//文生图\n\tprivate final String IMAGES_GENERATIONS = \"/images/generations\";\n\t//图片编辑\n\tprivate final String IMAGES_EDITS = \"/images/edits\";\n\t//图片变形\n\tprivate final String IMAGES_VARIATIONS = \"/images/variations\";\n\t//文本转语音\n\tprivate final String TTS = \"/audio/speech\";\n\t//语音转文本\n\tprivate final String STT = \"/audio/transcriptions\";\n\t//文本向量化\n\tprivate final String EMBEDDINGS = \"/embeddings\";\n\t//检查文本或图片\n\tprivate final String MODERATIONS = \"/moderations\";\n\n\tpublic OpenaiServiceImpl(final AIConfig config) {\n\t\t//初始化Openai客户端\n\t\tsuper(config);\n\t}\n\n\t@Override\n\tpublic String chat(final List<Message> messages) {\n\t\tString paramJson = buildChatRequestBody(messages);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chat(List<Message> messages,Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatStreamRequestBody(messages);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"openai-chat-sse\").start();\n\t}\n\n\t@Override\n\tpublic String chatVision(String prompt, final List<String> images, String detail) {\n\t\tString paramJson = buildChatVisionRequestBody(prompt, images, detail);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"openai-chatVision-sse\").start();\n\t}\n\n\t@Override\n\tpublic String imagesGenerations(String prompt) {\n\t\tString paramJson = buildImagesGenerationsRequestBody(prompt);\n\t\tfinal HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String imagesEdits(String prompt, final File image, final File mask) {\n\t\tfinal Map<String, Object> paramMap = buildImagesEditsRequestBody(prompt, image, mask);\n\t\tfinal HttpResponse response = sendFormData(IMAGES_EDITS, paramMap);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String imagesVariations(final File image) {\n\t\tfinal Map<String, Object> paramMap = buildImagesVariationsRequestBody(image);\n\t\tfinal HttpResponse response = sendFormData(IMAGES_VARIATIONS, paramMap);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic InputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice) {\n\t\tString paramJson = buildTTSRequestBody(input, voice.getVoice());\n\t\tfinal HttpResponse response = sendPost(TTS, paramJson);\n\t\treturn response.bodyStream();\n\t}\n\n\t@Override\n\tpublic String speechToText(final File file) {\n\t\tfinal Map<String, Object> paramMap = buildSTTRequestBody(file);\n\t\tfinal HttpResponse response = sendFormData(STT, paramMap);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String embeddingText(String input) {\n\t\tString paramJson = buildEmbeddingTextRequestBody(input);\n\t\tfinal HttpResponse response = sendPost(EMBEDDINGS, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String moderations(String text, String imgUrl) {\n\t\tString paramJson = buileModerationsRequestBody(text, imgUrl);\n\t\tfinal HttpResponse response = sendPost(MODERATIONS, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String chatReasoning(final List<Message> messages, String reasoningEffort) {\n\t\tString paramJson = buildChatReasoningRequestBody(messages, reasoningEffort);\n\t\tfinal HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void chatReasoning(List<Message> messages, String reasoningEffort, Consumer<String> callback) {\n\t\tMap<String, Object> paramMap = buildChatReasoningStreamRequestBody(messages, reasoningEffort);\n\t\tThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), \"openai-chatReasoning-sse\").start();\n\t}\n\n\t// 构建chat请求体\n\tprivate String buildChatRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建chatVision请求体\n\tprivate String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {\n\t\t// 定义消息结构\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tfinal List<Object> content = new ArrayList<>();\n\n\t\tfinal Map<String, String> contentMap = new HashMap<>();\n\t\tcontentMap.put(\"type\", \"text\");\n\t\tcontentMap.put(\"text\", prompt);\n\t\tcontent.add(contentMap);\n\t\tfor (String img : images) {\n\t\t\tHashMap<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tHashMap<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", img);\n\t\t\turlMap.put(\"detail\", detail);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tcontent.add(imgUrlMap);\n\t\t}\n\n\t\tmessages.add(new Message(\"user\", content));\n\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn paramMap;\n\t}\n\n\t//构建文生图请求体\n\tprivate String buildImagesGenerationsRequestBody(String prompt) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建图片编辑请求体\n\tprivate Map<String, Object> buildImagesEditsRequestBody(String prompt, final File image, final File mask) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"prompt\", prompt);\n\t\tparamMap.put(\"image\", image);\n\t\tif (mask != null) {\n\t\t\tparamMap.put(\"mask\", mask);\n\t\t}\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建图片变形请求体\n\tprivate Map<String, Object> buildImagesVariationsRequestBody(final File image) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"image\", image);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建TTS请求体\n\tprivate String buildTTSRequestBody(String input, String voice) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"input\", input);\n\t\tparamMap.put(\"voice\", voice);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建STT请求体\n\tprivate Map<String, Object> buildSTTRequestBody(final File file) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"file\", file);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n\t//构建文本向量化请求体\n\tprivate String buildEmbeddingTextRequestBody(String input) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"input\", input);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建检查图片或文字请求体\n\tprivate String buileModerationsRequestBody(String text, String imgUrl) {\n\t\t//使用JSON工具\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\n\t\tfinal List<Object> input = new ArrayList<>();\n\t\t//添加文本参数\n\t\tif (!StrUtil.isBlank(text)) {\n\t\t\tfinal Map<String, String> textMap = new HashMap<>();\n\t\t\ttextMap.put(\"type\", \"text\");\n\t\t\ttextMap.put(\"text\", text);\n\t\t\tinput.add(textMap);\n\t\t}\n\t\t//添加图片参数\n\t\tif (!StrUtil.isBlank(imgUrl)) {\n\t\t\tfinal Map<String, Object> imgUrlMap = new HashMap<>();\n\t\t\timgUrlMap.put(\"type\", \"image_url\");\n\t\t\tfinal Map<String, String> urlMap = new HashMap<>();\n\t\t\turlMap.put(\"url\", imgUrl);\n\t\t\timgUrlMap.put(\"image_url\", urlMap);\n\t\t\tinput.add(imgUrlMap);\n\t\t}\n\n\t\tparamMap.put(\"input\", input);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\t//构建推理请求体\n\tprivate String buildChatReasoningRequestBody(final List<Message> messages, String reasoningEffort) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"reasoning_effort\", reasoningEffort);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn JSONUtil.toJsonStr(paramMap);\n\t}\n\n\tprivate Map<String, Object> buildChatReasoningStreamRequestBody(final List<Message> messages, String reasoningEffort) {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"stream\", true);\n\t\tparamMap.put(\"model\", config.getModel());\n\t\tparamMap.put(\"messages\", messages);\n\t\tparamMap.put(\"reasoning_effort\", reasoningEffort);\n\t\t//合并其他参数\n\t\tparamMap.putAll(config.getAdditionalConfigMap());\n\n\t\treturn paramMap;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/openai/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对openai的封装实现\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.model.openai;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 对各个AI大模型的相关封装\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai.model;\n"
  },
  {
    "path": "hutool-ai/src/main/java/cn/hutool/ai/package-info.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Hutool-ai主要用于AI大模型的封装，只需要对AI模型最基本的设置，即可调用AI大模型。\n *\n * @author elichow\n * @since 5.8.38\n */\n\npackage cn.hutool.ai;\n"
  },
  {
    "path": "hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig",
    "content": "cn.hutool.ai.model.hutool.HutoolConfig\ncn.hutool.ai.model.deepseek.DeepSeekConfig\ncn.hutool.ai.model.openai.OpenaiConfig\ncn.hutool.ai.model.doubao.DoubaoConfig\ncn.hutool.ai.model.grok.GrokConfig\ncn.hutool.ai.model.ollama.OllamaConfig\ncn.hutool.ai.model.gemini.GeminiConfig\n"
  },
  {
    "path": "hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider",
    "content": "cn.hutool.ai.model.hutool.HutoolProvider\ncn.hutool.ai.model.deepseek.DeepSeekProvider\ncn.hutool.ai.model.openai.OpenaiProvider\ncn.hutool.ai.model.doubao.DoubaoProvider\ncn.hutool.ai.model.grok.GrokProvider\ncn.hutool.ai.model.ollama.OllamaProvider\ncn.hutool.ai.model.gemini.GeminiProvider\n"
  },
  {
    "path": "hutool-ai/src/test/java/AIServiceFactoryTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.model.deepseek.DeepSeekService;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass AIServiceFactoryTest {\n\n\tString key = \"your key\";\n\n\t@Test\n\tvoid getAIService() {\n\t\tfinal AIService aiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());\n\t\tassertNotNull(aiService);\n\t}\n\n\t@Test\n\tvoid testGetAIService() {\n\t\tfinal DeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class);\n\t\tassertNotNull(deepSeekService);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/AIUtilTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport cn.hutool.ai.AIUtil;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.AIService;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.ai.model.deepseek.DeepSeekService;\nimport cn.hutool.ai.model.doubao.DoubaoService;\nimport cn.hutool.ai.model.gemini.GeminiService;\nimport cn.hutool.ai.model.grok.GrokService;\nimport cn.hutool.ai.model.hutool.HutoolService;\nimport cn.hutool.ai.model.openai.OpenaiService;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass AIUtilTest {\n\n\tString key = \"your key\";\n\n\t@Test\n\tvoid getAIService() {\n\t\tfinal DeepSeekService deepSeekService = AIUtil.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class);\n\t\tassertNotNull(deepSeekService);\n\t}\n\n\t@Test\n\tvoid testGetAIService() {\n\t\tfinal AIService aiService = AIUtil.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build());\n\t\tassertNotNull(aiService);\n\t}\n\n\t@Test\n\tvoid getHutoolService() {\n\t\tfinal HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build());\n\t\tassertNotNull(hutoolService);\n\t}\n\n\t@Test\n\tvoid getDeepSeekService() {\n\t\tfinal DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());\n\t\tassertNotNull(deepSeekService);\n\t}\n\n\t@Test\n\tvoid getDoubaoService() {\n\t\tfinal DoubaoService doubaoService = AIUtil.getDoubaoService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).build());\n\t\tassertNotNull(doubaoService);\n\t}\n\n\t@Test\n\tvoid getGrokService() {\n\t\tfinal GrokService grokService = AIUtil.getGrokService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build());\n\t\tassertNotNull(grokService);\n\t}\n\n\t@Test\n\tvoid getOpenAIService() {\n\t\tfinal OpenaiService openAIService = AIUtil.getOpenAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build());\n\t\tassertNotNull(openAIService);\n\t}\n\n\t@Test\n\tvoid getGeminiService() {\n\t\tfinal GeminiService geminiService = AIUtil.getGeminiService(new AIConfigBuilder(ModelName.GEMINI.getValue()).setApiKey(key).build());\n\t\tassertNotNull(geminiService);\n\t}\n\n\t@Test\n\tvoid chat() {\n\t\tfinal String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), \"写一首赞美我的诗\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\tvoid testChat() {\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是财神爷，只会说“我是财神”\"));\n\t\tmessages.add(new Message(\"user\",\"你是谁啊？\"));\n\t\tfinal String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), messages);\n\t\tassertNotNull(chat);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/deepseek/DeepSeekServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.deepseek;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass DeepSeekServiceTest {\n\n\tString key = \"your key\";\n\tDeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(),DeepSeekService.class);\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = deepSeekService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tdeepSeekService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = deepSeekService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid beta() {\n\t\tfinal String beta = deepSeekService.beta(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(beta);\n\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid betaStream() {\n\t\tString beta = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tdeepSeekService.beta(beta, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid models() {\n\t\tfinal String models = deepSeekService.models();\n\t\tassertNotNull(models);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid balance() {\n\t\tfinal String balance = deepSeekService.balance();\n\t\tassertNotNull(balance);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/doubao/DoubaoServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.doubao;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass DoubaoServiceTest {\n\n\tString key = \"your key\";\n\tDoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setModel(Models.Doubao.DOUBAO_1_5_LITE_32K.getModel()).setApiKey(key).build(), DoubaoService.class);\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = doubaoService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tdoubaoService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = doubaoService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatVision() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);\n\t\tfinal String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage(\"your imageUrl\"), \"png\");\n\t\tfinal String chatVision = doubaoService.chatVision(\"图片上有些什么？\", Arrays.asList(base64));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVision() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);\n\t\tfinal String chatVision = doubaoService.chatVision(\"图片上有些什么？\", Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\"),DoubaoCommon.DoubaoVision.HIGH.getDetail());\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVisionStream() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);\n\n\t\tString prompt = \"图片上有些什么？\";\n\t\tList<String> images = Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tdoubaoService.chatVision(prompt,images, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid videoTasks() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDDANCE_1_0_lite_I2V.getModel()).build(), DoubaoService.class);\n\t\tfinal String videoTasks = doubaoService.videoTasks(\"生成一段动画视频，主角是大耳朵图图，一个活泼可爱的小男孩。视频中图图在公园里玩耍，\" +\n\t\t\t\"画面采用明亮温暖的卡通风格，色彩鲜艳，动作流畅。背景音乐轻快活泼，带有冒险感，音效包括鸟叫声、欢笑声和山洞回声。\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(videoTasks);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid getVideoTasksInfo() {\n\t\t//cgt-20250306170051-6r9gk\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).build(), DoubaoService.class);\n\t\tfinal String videoTasksInfo = doubaoService.getVideoTasksInfo(\"cgt-20250306170051-6r9gk\");\n\t\tassertNotNull(videoTasksInfo);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid embeddingText() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_TEXT_240715.getModel()).build(), DoubaoService.class);\n\t\tfinal String embeddingText = doubaoService.embeddingText(new String[]{\"阿斯顿\", \"马丁\"});\n\t\tassertNotNull(embeddingText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid embeddingVision() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_VISION.getModel()).build(), DoubaoService.class);\n\t\tfinal String embeddingVision = doubaoService.embeddingVision(\"天空好难\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(embeddingVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid botsChat() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your bots id\").build(), DoubaoService.class);\n\t\tfinal ArrayList<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是什么都可以\"));\n\t\tmessages.add(new Message(\"user\",\"你想做些什么\"));\n\t\tfinal String botsChat = doubaoService.botsChat(messages);\n\t\tassertNotNull(botsChat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid botsChatStream() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your bots id\").build(), DoubaoService.class);\n\t\tfinal ArrayList<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是什么都可以\"));\n\t\tmessages.add(new Message(\"user\",\"你想做些什么\"));\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tdoubaoService.botsChat(messages, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid tokenization() {\n\t\tfinal String tokenization = doubaoService.tokenization(new String[]{\"阿斯顿\", \"马丁\"});\n\t\tassertNotNull(tokenization);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid batchChat() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal String batchChat = doubaoService.batchChat(\"写首歌词\");\n\t\tassertNotNull(batchChat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testBatchChat() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师\"));\n\t\tmessages.add(new Message(\"user\",\"写一个KFC的抽象广告\"));\n\t\tfinal String batchChat = doubaoService.batchChat(messages);\n\t\tassertNotNull(batchChat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid createContext() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师,你真的很抽象\"));\n\t\tfinal String context = doubaoService.createContext(messages);//ctx-20250307092153-cvslm\n\t\tassertNotNull(context);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testCreateContext() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"ep-20250305100610-bvbpc\").build(), DoubaoService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师,你真的很抽象\"));\n\t\tfinal String context = doubaoService.createContext(messages,DoubaoCommon.DoubaoContext.COMMON_PREFIX.getMode());\n\t\tassertNotNull(context);//ctx-20250307092153-cvslm\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatContext() {\n\t\t//ctx-20250307092153-cvslm\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal String chatContext = doubaoService.chatContext(\"你是谁？\", \"your contextId\");\n\t\tassertNotNull(chatContext);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatContext() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\",\"你怎么看待意大利面拌水泥？\"));\n\t\tfinal String chatContext = doubaoService.chatContext(messages, \"your contextId\");\n\t\tassertNotNull(chatContext);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatContextStream() {\n\t\tfinal DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(\"your Endpoint ID\").build(), DoubaoService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\",\"你怎么看待意大利面拌水泥？\"));\n\t\tString contextId  = \"your contextId\";\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tdoubaoService.chatContext(messages,contextId, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesGenerations() {\n\t\tfinal DoubaoService doubaoService  = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDREAM_3_0_T2I.getModel()).build(), DoubaoService.class);\n\t\tfinal String imagesGenerations = doubaoService.imagesGenerations(\"一位年轻的宇航员站在未来感十足的太空站内，透过巨大的弧形落地窗凝望浩瀚宇宙。窗外，璀璨的星河与五彩斑斓的星云交织，远处隐约可见未知星球的轮廓，仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映，象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘，充满对未知的渴望与无限可能的想象。\");\n\t\tassertNotNull(imagesGenerations);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/gemini/GeminiServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.gemini;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.AIConfig;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONArray;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n\nclass GeminiServiceTest {\n\n\tString key = \"your key\";\n\tGeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue()).setApiKey(key).build(), GeminiService.class);\n\n\t@Test\n\t@Disabled\n\tvoid chat() {\n\t\tfinal String chat = geminiService.chat(\"我应该怎么度过2025年的最后一天？\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tgeminiService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"finishReason\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testUpload() {\n\t\tString uploadFile = geminiService.uploadFile(new File(\"/Users/hdbuoge/Desktop/111.mov\"));\n\t\tassertNotNull(uploadFile);\n\t\t//https://generativelanguage.googleapis.com/v1beta/files/xuognhscnd12\n\t}\n\n\n\t@Test\n\t@Disabled\n\tvoid chatMultimodalImage() {\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_0_FLASH.getModel()).build(), GeminiService.class);\n\t\tfinal String chatVision = geminiService.chatMultimodal(\"图片上有些什么内容？\", Arrays.asList(\"https://generativelanguage.googleapis.com/v1beta/files/xuognhscnd12\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatMultimodalImageSteam() {\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_0_FLASH.getModel()).build(), GeminiService.class);\n\t\tString prompt = \"图片上有些什么内容？中文回答\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tgeminiService.chatMultimodal(prompt,  Arrays.asList(\"https://generativelanguage.googleapis.com/v1beta/files/xuognhscnd12\"), data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"finishReason\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatMultimodalVideo() {\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_0_FLASH.getModel()).build(), GeminiService.class);\n\t\tfinal String chatVision = geminiService.chatMultimodal(\"视频中第3秒发生了什么？\", Arrays.asList(\"https://generativelanguage.googleapis.com/v1beta/files/k1whwbqznecz\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatMultimodalVideoStream() {\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_0_FLASH.getModel()).build(), GeminiService.class);\n\t\tString prompt = \"视频中第3秒有什么物品？\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tgeminiService.chatMultimodal(prompt,  Arrays.asList(\"https://generativelanguage.googleapis.com/v1beta/files/k1whwbqznecz\"), data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"finishReason\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatJson() {\n\t\t// 测试结构化输出\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"user\", \"提取以下信息：张三，男，25岁。返回JSON格式。\"));\n\t\tfinal String jsonResponse = geminiService.chatJson(messages);\n\t\tassertNotNull(jsonResponse);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatImage() {\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_5_FLASH_IMAGE.getModel()).build(), GeminiService.class);\n\t\tfinal String response = geminiService.chat(\"一只在太空中行走的赛博朋克风格的猫\");\n\t\t// 注意：Gemini返回是包含base64数据的响应体\n\t\tassertNotNull(response);\n\t}\n\n\n\t@Test\n\t@Disabled\n\tvoid predictImage() {\n\t\t// 测试 Imagen 4 原生图片生成\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.IMAGEN_4_0_GENERATE_001.getModel()).build(), GeminiService.class);\n\t\t//暂时只支持英文提示词\n\t\tfinal String response = geminiService.predictImage(\"Oil painting of New Year's greetings\");\n\t\tassertNotNull(response);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid predictImageAndSave() {\n\t\tAIConfig config = new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.IMAGEN_4_0_GENERATE_001.getModel()).build();\n\t\tconfig.putAdditionalConfigByKey(\"numberOfImages\", GeminiCommon.GeminiImageCount.TWO.getCount());\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(config, GeminiService.class);\n\n\t\t//暂时只支持英文提示词\n\t\tfinal String response = geminiService.predictImage(\"A park in the spring next to a lake, the sun sets across the lake, golden hour, red wildflowers\");\n\n\t\t//解析JSON 结构\n\t\tJSONObject jsonObject = JSONUtil.parseObj(response);\n\t\tJSONArray predictions = jsonObject.getJSONArray(\"predictions\");\n\n\t\tif (predictions != null) {\n\t\t\tfor (int i = 0; i < predictions.size(); i++) {\n\t\t\t\tJSONObject item = predictions.getJSONObject(i);\n\n\t\t\t\t//提取Base64数据\n\t\t\t\tString base64Data = item.getStr(\"bytesBase64Encoded\");\n\n\t\t\t\t//划分并保存到本地文件\n\t\t\t\tString fileName = \"generated_image_\" + i + \".png\";\n\t\t\t\tFileUtil.writeBytes(Base64.decode(base64Data), fileName);\n\t\t\t\tFileUtil.writeBytes(Base64.decode(base64Data), \"your filePath\" + fileName);\n\n\t\t\t\tassertNotNull(base64Data);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid generateVideoTest() {\n\t\tAIConfig config = new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.VEO_3_1_GENERATE_PREVIEW.getModel()).build();\n\t\tconfig.putAdditionalConfigByKey(\"aspectRatio\", GeminiCommon.GeminiAspectRatio.LANDSCAPE_16_9.getRatio());\n\t\tconfig.putAdditionalConfigByKey(\"durationSeconds\", GeminiCommon.GeminiDurationSeconds.EIGHT.getValue());\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(config, GeminiService.class);\n\n\n\t\t// 4. 发起异步生成请求\n\t\tString initialRes = geminiService.predictVideo(\"在一艘即将沉没的轮船上，男主角从后面抱起展开双手的女主角，女主角说：”u jump, i jump“\");\n\t\tJSONObject resObj = JSONUtil.parseObj(initialRes);\n\n\t\tString operationName = resObj.getStr(\"name\");\n\n\t\t// 5. 轮询获取结果 (LRO 模式)\n\t\tString videoUri = null;\n\t\tint maxRetries = 30; // 约等待 5-10 分钟\n\t\tfor (int i = 0; i < maxRetries; i++) {\n\t\t\tThreadUtil.sleep(20000); // 每 20 秒查询一次\n\n\t\t\tString statusJson = geminiService.getVideoOperation(operationName);\n\t\t\tJSONObject statusObj = JSONUtil.parseObj(statusJson);\n\n\t\t\t// 判断是否完成\n\t\t\tif (statusObj.getBool(\"done\", false)) {\n\t\t\t\t// 路径参考：response.generateVideoResponse.generatedSamples[0].video.uri\n\t\t\t\tvideoUri = statusObj.getByPath(\"response.generateVideoResponse.generatedSamples[0].video.uri\", String.class);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tassertNotNull(videoUri);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid downLoadVideo() {\n\t\tString videoUri = \"geminiService.getVideoOperation返回的videoUri\";\n\t\tgeminiService.downLoadVideo(videoUri, \"your filePath\");\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testTTSWithBuildMethod() {\n\t\tAIConfig config = new AIConfigBuilder(ModelName.GEMINI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Gemini.GEMINI_2_5_PRO_PREVIEW_TTS.getModel()).build();\n\t\tconfig.putAdditionalConfigByKey(\"temperature\", 0.7);\n\t\tfinal GeminiService geminiService = AIServiceFactory.getAIService(config, GeminiService.class);\n\n\t\tString prompt = \"Hello, this is a test of the native text to speech system.\";\n\t\tString result = geminiService.textToSpeech(prompt, GeminiCommon.GeminiVoice.AOEDE.getValue());\n\n\t\tJSONObject json = JSONUtil.parseObj(result);\n\t\tString base64Data = json.getByPath(\"candidates[0].content.parts[0].inlineData.data\", String.class);\n\t\tbyte[] rawPcm = Base64.decode(base64Data);\n\n\t\t//接口返回的是裸PCM流\n\t\tbyte[] wavFile = geminiService.addWavHeader(rawPcm);\n\n\n\t\tif (StrUtil.isNotBlank(base64Data)) {\n\t\t\tFileUtil.writeBytes(wavFile, \"your filePath\");\n\t\t}\n\n\t\tassertNotNull(wavFile);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/grok/GrokServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.grok;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass GrokServiceTest {\n\n\tString key = \"your key\";\n\tGrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build(), GrokService.class);\n\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = grokService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\tgrokService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = grokService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid message() {\n\t\tfinal String message = grokService.message(\"给我一个KFC的广告词\", 4096);\n\t\tassertNotNull(message);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid messageStream() {\n\t\tString prompt = \"给我一个KFC的广告词\";\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tgrokService.message(prompt, 4096, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatVision() {\n\t\tfinal GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);\n\t\tfinal String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage(\"your imageUrl\"), \"png\");\n\t\tfinal String chatVision = grokService.chatVision(\"图片上有些什么？\", Arrays.asList(base64));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVisionStream() {\n\t\tfinal GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);\n\t\tString prompt = \"图片上有些什么？\";\n\t\tList<String> images = Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tgrokService.chatVision(prompt,images, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVision() {\n\t\tfinal GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);\n\t\tfinal String chatVision = grokService.chatVision(\"图片上有些什么？\", Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid models() {\n\t\tfinal String models = grokService.models();\n\t\tassertNotNull(models);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid getModel() {\n\t\tfinal String model = grokService.getModel(\"\");\n\t\tassertNotNull(model);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid languageModels() {\n\t\tfinal String languageModels = grokService.languageModels();\n\t\tassertNotNull(languageModels);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid getLanguageModel() {\n\t\tfinal String language = grokService.getLanguageModel(\"\");\n\t\tassertNotNull(language);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid tokenizeText() {\n\t\tfinal String tokenizeText = grokService.tokenizeText(key);\n\t\tassertNotNull(tokenizeText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid deferredCompletion() {\n\t\tfinal String deferred = grokService.deferredCompletion(key);\n\t\tassertNotNull(deferred);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesGenerations() {\n\t\tfinal GrokService grokService  = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Grok.GROK_2_IMAGE.getModel()).build(), GrokService.class);\n\t\tfinal String imagesGenerations = grokService.imagesGenerations(\"一位年轻的宇航员站在未来感十足的太空站内，透过巨大的弧形落地窗凝望浩瀚宇宙。窗外，璀璨的星河与五彩斑斓的星云交织，远处隐约可见未知星球的轮廓，仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映，象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘，充满对未知的渴望与无限可能的想象。\");\n\t\tassertNotNull(imagesGenerations);\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.hutool;\n\nimport cn.hutool.ai.AIException;\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass HutoolServiceTest {\n\n\tString key = \"请前往Hutool-AI官网：https://ai.hutool.cn 获取\";\n\tHutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class);\n\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = hutoolService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\thutoolService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = hutoolService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\n\t@Test\n\t@Disabled\n\tvoid chatVision() {\n\t\tfinal String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage(\"your imageUrl\"), \"png\");\n\t\tfinal String chatVision = hutoolService.chatVision(\"图片上有些什么？\", Arrays.asList(base64));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVisionStream() {\n\t\tString prompt = \"图片上有些什么？\";\n\t\tList<String> images = Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\thutoolService.chatVision(prompt,images, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVision() {\n\t\tfinal String chatVision = hutoolService.chatVision(\"图片上有些什么？\", Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid tokenizeText() {\n\t\tfinal String tokenizeText = hutoolService.tokenizeText(key);\n\t\tassertNotNull(tokenizeText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesGenerations() {\n\t\tfinal String imagesGenerations = hutoolService.imagesGenerations(\"一位年轻的宇航员站在未来感十足的太空站内，透过巨大的弧形落地窗凝望浩瀚宇宙。窗外，璀璨的星河与五彩斑斓的星云交织，远处隐约可见未知星球的轮廓，仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映，象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘，充满对未知的渴望与无限可能的想象。\");\n\t\tassertNotNull(imagesGenerations);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid embeddingVision() {\n\t\tfinal String embeddingVision = hutoolService.embeddingVision(\"天空好难\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(embeddingVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid textToSpeech() {\n\t\ttry {\n\t\t\t// 测试正常音频流返回\n\t\t\tfinal InputStream inputStream = hutoolService.tts(\"万里山河一夜白，\\n\" +\n\t\t\t\t\"千峰尽染玉龙哀。\\n\" +\n\t\t\t\t\"长风卷起琼花碎，\\n\" +\n\t\t\t\t\"直上九霄揽月来。\", HutoolCommon.HutoolSpeech.NOVA);\n\t\t\tassertNotNull(inputStream);\n\n\t\t\t// 保存音频文件\n\t\t\tfinal String filePath = \"your filePath\";\n\t\t\tFileUtil.writeFromStream(inputStream, new File(filePath));\n\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"TTS测试失败: \" + e.getMessage());\n\t\t}\n\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid speechToText() {\n\t\tfinal File file = FileUtil.file(\"your filePath\");\n\t\tfinal String speechToText = hutoolService.stt(file);\n\t\tassertNotNull(speechToText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid videoTasks() {\n\t\tfinal String videoTasks = hutoolService.videoTasks(\"生成一段动画视频，主角是大耳朵图图，一个活泼可爱的小男孩。视频中图图在公园里玩耍，\" +\n\t\t\t\"画面采用明亮温暖的卡通风格，色彩鲜艳，动作流畅。背景音乐轻快活泼，带有冒险感，音效包括鸟叫声、欢笑声和山洞回声。\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(videoTasks);//cgt-20250529154621-d7dq9\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid getVideoTasksInfo() {\n\t\tfinal String videoTasksInfo = hutoolService.getVideoTasksInfo(\"cgt-20250529154621-d7dq9\");\n\t\tassertNotNull(videoTasksInfo);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/ollama/OllamaServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.ollama;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSON;\nimport cn.hutool.json.JSONArray;\nimport cn.hutool.json.JSONUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicReference;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * OllamaService\n *\n * @author yangruoyu-yumeisoft\n * @since 5.8.40\n */\nclass OllamaServiceTest {\n\t// 创建service\n\tOllamaService ollamaService = AIServiceFactory.getAIService(\n\t\tnew AIConfigBuilder(ModelName.OLLAMA.getValue())\n\t\t\t// 这里填写Ollama服务的地址\n\t\t\t.setApiUrl(\"http://127.0.0.1:11434\")\n\t\t\t// 这里填写使用的模型\n\t\t\t.setModel(\"qwen2.5-coder:32b\")\n\t\t\t.build(),\n\t\tOllamaService.class\n\t);\n\n\t// 假设有一个Java工程师的Agent提示词\n\tString javaEngineerPrompt=\"# 角色  \\n\" +\n\t\t\"你是一位精通Spring Boot 3.0的资深Java全栈工程师，具备以下核心能力：  \\n\" +\n\t\t\"- 精通Spring Boot 3.0新特性与最佳实践  \\n\" +\n\t\t\"- 熟练整合Hutool工具包、Redis数据访问、Feign远程调用、FreeMarker模板引擎  \\n\" +\n\t\t\"- 能输出符合工程规范的代码结构和配置文件  \\n\" +\n\t\t\"- 注重代码可读性与注释规范  \\n\" +\n\t\t\"\\n\" +\n\t\t\"# 任务  \\n\" +\n\t\t\"请完成以下编程任务（按优先级排序）：  \\n\" +\n\t\t\"1. **核心要求**  \\n\" +\n\t\t\"   - 使用Spring Boot 3.0构建项目  \\n\" +\n\t\t\"   - 必须包含以下依赖：  \\n\" +\n\t\t\"     - `cn.hutool:hutool-all`（最新版）  \\n\" +\n\t\t\"     - `org.springframework.boot:spring-boot-starter-data-redis`  \\n\" +\n\t\t\"     - `org.springframework.cloud:spring-cloud-starter-openfeign`  \\n\" +\n\t\t\"     - `org.springframework.boot:spring-boot-starter-freemarker`  \\n\" +\n\t\t\"2. **约束条件**  \\n\" +\n\t\t\"   - 代码需符合Java 17语法规范  \\n\" +\n\t\t\"   - 每个类必须包含Javadoc风格的类注释  \\n\" +\n\t\t\"   - 关键方法需添加`@Api`/`@ApiOperation`注解（若涉及接口）  \\n\" +\n\t\t\"   - Redis操作需使用`RedisTemplate`实现  \\n\" +\n\t\t\"3. **实现流程**  \\n\" +\n\t\t\"   ```  \\n\" +\n\t\t\"   1. 生成pom.xml依赖配置  \\n\" +\n\t\t\"   2. 创建基础配置类（如RedisConfig）  \\n\" +\n\t\t\"   3. 编写Feign客户端接口  \\n\" +\n\t\t\"   4. 实现FreeMarker模板渲染服务  \\n\" +\n\t\t\"   5. 提供完整Controller示例  \\n\" +\n\t\t\"   ```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"# 输出要求  \\n\" +\n\t\t\"请以严格Markdown格式输出，每个模块独立代码块：  \\n\" +\n\t\t\"```markdown  \\n\" +\n\t\t\"## 1. 项目依赖配置（pom.xml片段）  \\n\" +\n\t\t\"```xml  \\n\" +\n\t\t\"<dependency>...</dependency>  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"## 2. Redis配置类  \\n\" +\n\t\t\"```java  \\n\" +\n\t\t\"@Configuration  \\n\" +\n\t\t\"public class RedisConfig { ... }  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"## 3. Feign客户端示例  \\n\" +\n\t\t\"```java  \\n\" +\n\t\t\"@FeignClient(name = \\\"...\\\")  \\n\" +\n\t\t\"public interface ... { ... }  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"## 4. FreeMarker模板服务  \\n\" +\n\t\t\"```java  \\n\" +\n\t\t\"@Service  \\n\" +\n\t\t\"public class TemplateService { ... }  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"## 5. 控制器示例  \\n\" +\n\t\t\"```java  \\n\" +\n\t\t\"@RestController  \\n\" +\n\t\t\"@RequestMapping(\\\"/example\\\")  \\n\" +\n\t\t\"public class ExampleController { ... }  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"# 示例片段（供格式参考）  \\n\" +\n\t\t\"```java  \\n\" +\n\t\t\"/** \\n\" +\n\t\t\" * 示例Feign客户端 \\n\" +\n\t\t\" * @since 1.0.0 \\n\" +\n\t\t\" */  \\n\" +\n\t\t\"@FeignClient(name = \\\"demo-service\\\", url = \\\"${demo.service.url}\\\")  \\n\" +\n\t\t\"public interface DemoClient {  \\n\" +\n\t\t\"\\n\" +\n\t\t\"    @GetMapping(\\\"/data/{id}\\\")  \\n\" +\n\t\t\"    @ApiOperation(\\\"获取示例数据\\\")  \\n\" +\n\t\t\"    ResponseEntity<String> getData(@PathVariable(\\\"id\\\") Long id);  \\n\" +\n\t\t\"}  \\n\" +\n\t\t\"```  \\n\" +\n\t\t\"\\n\" +\n\t\t\"请按此规范输出完整代码结构，确保自动化程序可直接解析生成项目文件。\";\n\n\t/**\n\t * 同步方式调用\n\t */\n\t@Test\n\t@Disabled\n\tvoid testSimple() {\n\t\tfinal String answer = ollamaService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(answer);\n\t}\n\n\t/**\n\t * 按流方式输出\n\t */\n\t@Test\n\t@Disabled\n\tvoid testStream() {\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tAtomicReference<String> errorMessage = new AtomicReference<>();\n\t\tollamaService.chat(\"写一个疯狂星期四广告词\", data -> {\n\t\t\t// 输出到控制台\n\t\t\tJSON streamData = JSONUtil.parse(data);\n\t\t\tif (streamData.getByPath(\"error\") != null) {\n\t\t\t\tisDone.set(true);\n\t\t\t\terrorMessage.set(streamData.getByPath(\"error\").toString());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\"true\".equals(streamData.getByPath(\"done\").toString())) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t\tif (errorMessage.get() != null) {\n\t\t\tthrow new RuntimeException(errorMessage.get());\n\t\t}\n\t}\n\n\t/**\n\t * 带历史上下文的同步方式调用\n\t */\n\t@Test\n\t@Disabled\n\tvoid testSimpleWithHistory(){\n\t\tList<Message> messageList=new ArrayList<>();\n\t\tmessageList.add(new Message(\"system\",javaEngineerPrompt));\n\t\tmessageList.add(new Message(\"user\",\"帮我写一个Java通过Post方式发送JSON给HTTP接口，请求头带有token\"));\n\t\tString result = ollamaService.chat(messageList);\n\t\tassertNotNull(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testStreamWithHistory(){\n\t\tList<Message> messageList=new ArrayList<>();\n\t\tmessageList.add(new Message(\"system\",javaEngineerPrompt));\n\t\tmessageList.add(new Message(\"user\",\"帮我写一个Java通过Post方式发送JSON给HTTP接口，请求头带有token\"));\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\tAtomicReference<String> errorMessage = new AtomicReference<>();\n\t\tollamaService.chat(messageList, data -> {\n\t\t\t// 输出到控制台\n\t\t\tJSON streamData = JSONUtil.parse(data);\n\t\t\tif (streamData.getByPath(\"error\") != null) {\n\t\t\t\tisDone.set(true);\n\t\t\t\terrorMessage.set(streamData.getByPath(\"error\").toString());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\"true\".equals(streamData.getByPath(\"done\").toString())) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t\tif (errorMessage.get() != null) {\n\t\t\tthrow new RuntimeException(errorMessage.get());\n\t\t}\n\t}\n\n\t/**\n\t * 列出所有已经拉取到服务器上的模型\n\t */\n\t@Test\n\t@Disabled\n\tvoid testListModels(){\n\t\tString models = ollamaService.listModels();\n\t\tJSONArray modelList = JSONUtil.parse(models).getByPath(\"models\", JSONArray.class);\n\t}\n\n\t/**\n\t * 让Ollama拉取模型\n\t */\n\t@Test\n\t@Disabled\n\tvoid testPullModel(){\n\t\tString result = ollamaService.pullModel(\"qwen2.5:0.5b\");\n\t\tList<String> lines = StrUtil.splitTrim(result, \"\\n\");\n\t\tfor (String line : lines) {\n\t\t\tif(line.contains(\"error\")){\n\t\t\t\tthrow new RuntimeException(JSONUtil.parse(line).getByPath(\"error\").toString());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 让Ollama删除已经存在的模型\n\t */\n\t@Test\n\t@Disabled\n\tvoid testDeleteModel(){\n\t\t// 不会返回任何信息\n\t\tollamaService.deleteModel(\"qwen2.5:0.5b\");\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/openai/OpenaiProxyServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass OpenaiProxyServiceTest {\n\n\tString key = \"your key\";\n\t//you proxy hostname\n\tString hostname = \"you proxy hostname\";\n\t//you proxy port\n\tint port = 7890;\n\tOpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).setProxy(new Proxy(Proxy.Type.HTTP,\n\t\tnew InetSocketAddress(hostname, port))).build(), OpenaiService.class);\n\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = openaiService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\topenaiService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = openaiService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatVision() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal String chatVision = openaiService.chatVision(\"图片上有些什么？\", Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVisionStream() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);\n\t\tString prompt = \"图片上有些什么？\";\n\t\tList<String> images = Collections.singletonList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\\\",\\\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800\");\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\topenaiService.chatVision(prompt,images, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesGenerations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class);\n\t\tfinal String imagesGenerations = openaiService.imagesGenerations(\"一位年轻的宇航员站在未来感十足的太空站内，透过巨大的弧形落地窗凝望浩瀚宇宙。窗外，璀璨的星河与五彩斑斓的星云交织，远处隐约可见未知星球的轮廓，仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映，象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘，充满对未知的渴望与无限可能的想象。\");\n\t\tassertNotNull(imagesGenerations);\n\t\t//https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesEdits() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your imgUrl\");\n\t\tfinal String imagesEdits = openaiService.imagesEdits(\"茂密的森林中，有一只九色鹿若隐若现\",file);\n\t\tassertNotNull(imagesEdits);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesVariations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your imgUrl\");\n\t\tfinal String imagesVariations = openaiService.imagesVariations(file);\n\t\tassertNotNull(imagesVariations);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid textToSpeech() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class);\n\t\tfinal InputStream inputStream = openaiService.textToSpeech(\"万里山河一夜白，\\n\" +\n\t\t\t\"千峰尽染玉龙哀。\\n\" +\n\t\t\t\"长风卷起琼花碎，\\n\" +\n\t\t\t\"直上九霄揽月来。\", OpenaiCommon.OpenaiSpeech.NOVA);\n\n\t\tfinal String filePath = \"your filePath\";\n\t\tfinal Path path = Paths.get(filePath);\n\t\ttry (final FileOutputStream outputStream = new FileOutputStream(filePath)) {\n\t\t\tFiles.createDirectories(path.getParent());\n\t\t\tfinal byte[] buffer = new byte[1024];\n\t\t\tint bytesRead;\n\t\t\twhile ((bytesRead = inputStream.read(buffer)) != -1) {\n\t\t\t\toutputStream.write(buffer, 0, bytesRead);\n\t\t\t}\n\t\t} catch (final IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid speechToText() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your filePath\");\n\t\tfinal String speechToText = openaiService.speechToText(file);\n\t\tassertNotNull(speechToText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid embeddingText() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class);\n\t\tfinal String embeddingText = openaiService.embeddingText(\"萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來\");\n\t\tassertNotNull(embeddingText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid moderations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);\n\t\tfinal String moderations = openaiService.moderations(\"你要玩游戏\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(moderations);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatReasoning() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是现代抽象家\"));\n\t\tmessages.add(new Message(\"user\",\"给我一个KFC疯狂星期四的文案\"));\n\t\tfinal String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort());\n\t\tassertNotNull(chatReasoning);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatReasoningStream() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是现代抽象家\"));\n\t\tmessages.add(new Message(\"user\",\"给我一个KFC疯狂星期四的文案\"));\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\topenaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-ai/src/test/java/cn/hutool/ai/model/openai/OpenaiServiceTest.java",
    "content": "/*\n * Copyright (c) 2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.ai.model.openai;\n\nimport cn.hutool.ai.AIServiceFactory;\nimport cn.hutool.ai.ModelName;\nimport cn.hutool.ai.Models;\nimport cn.hutool.ai.core.AIConfigBuilder;\nimport cn.hutool.ai.core.Message;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass OpenaiServiceTest {\n\n\tString key = \"your key\";\n\tOpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build(), OpenaiService.class);\n\n\n\t@Test\n\t@Disabled\n\tvoid chat(){\n\t\tfinal String chat = openaiService.chat(\"写一个疯狂星期四广告词\");\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatStream() {\n\t\tString prompt = \"写一个疯狂星期四广告词\";\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\n\t\topenaiService.chat(prompt, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChat(){\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是个抽象大师，会说很抽象的话，最擅长说抽象的笑话\"));\n\t\tmessages.add(new Message(\"user\",\"给我说一个笑话\"));\n\t\tfinal String chat = openaiService.chat(messages);\n\t\tassertNotNull(chat);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatVision() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal String chatVision = openaiService.chatVision(\"图片上有些什么？\", Arrays.asList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800\"));\n\t\tassertNotNull(chatVision);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid testChatVisionStream() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);\n\t\tString prompt = \"图片上有些什么？\";\n\t\tList<String> images = Collections.singletonList(\"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\\\",\\\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800\");\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\topenaiService.chatVision(prompt,images, data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesGenerations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class);\n\t\tfinal String imagesGenerations = openaiService.imagesGenerations(\"一位年轻的宇航员站在未来感十足的太空站内，透过巨大的弧形落地窗凝望浩瀚宇宙。窗外，璀璨的星河与五彩斑斓的星云交织，远处隐约可见未知星球的轮廓，仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映，象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘，充满对未知的渴望与无限可能的想象。\");\n\t\tassertNotNull(imagesGenerations);\n\t\t//https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesEdits() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your imgUrl\");\n\t\tfinal String imagesEdits = openaiService.imagesEdits(\"茂密的森林中，有一只九色鹿若隐若现\",file);\n\t\tassertNotNull(imagesEdits);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid imagesVariations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your imgUrl\");\n\t\tfinal String imagesVariations = openaiService.imagesVariations(file);\n\t\tassertNotNull(imagesVariations);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid textToSpeech() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class);\n\t\tfinal InputStream inputStream = openaiService.textToSpeech(\"万里山河一夜白，\\n\" +\n\t\t\t\"千峰尽染玉龙哀。\\n\" +\n\t\t\t\"长风卷起琼花碎，\\n\" +\n\t\t\t\"直上九霄揽月来。\", OpenaiCommon.OpenaiSpeech.NOVA);\n\n\t\tfinal String filePath = \"your filePath\";\n\t\tfinal Path path = Paths.get(filePath);\n\t\ttry (final FileOutputStream outputStream = new FileOutputStream(filePath)) {\n\t\t\tFiles.createDirectories(path.getParent());\n\t\t\tfinal byte[] buffer = new byte[1024];\n\t\t\tint bytesRead;\n\t\t\twhile ((bytesRead = inputStream.read(buffer)) != -1) {\n\t\t\t\toutputStream.write(buffer, 0, bytesRead);\n\t\t\t}\n\t\t} catch (final IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid speechToText() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class);\n\t\tfinal File file = FileUtil.file(\"your filePath\");\n\t\tfinal String speechToText = openaiService.speechToText(file);\n\t\tassertNotNull(speechToText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid embeddingText() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class);\n\t\tfinal String embeddingText = openaiService.embeddingText(\"萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來\");\n\t\tassertNotNull(embeddingText);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid moderations() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);\n\t\tfinal String moderations = openaiService.moderations(\"你要玩游戏\", \"https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\");\n\t\tassertNotNull(moderations);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatReasoning() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是现代抽象家\"));\n\t\tmessages.add(new Message(\"user\",\"给我一个KFC疯狂星期四的文案\"));\n\t\tfinal String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort());\n\t\tassertNotNull(chatReasoning);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid chatReasoningStream() {\n\t\tfinal OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())\n\t\t\t.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);\n\t\tfinal List<Message> messages = new ArrayList<>();\n\t\tmessages.add(new Message(\"system\",\"你是现代抽象家\"));\n\t\tmessages.add(new Message(\"user\",\"给我一个KFC疯狂星期四的文案\"));\n\n\t\t// 使用AtomicBoolean作为结束标志\n\t\tAtomicBoolean isDone = new AtomicBoolean(false);\n\t\topenaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> {\n\t\t\tassertNotNull(data);\n\t\t\tif (data.contains(\"[DONE]\")) {\n\t\t\t\t// 设置结束标志\n\t\t\t\tisDone.set(true);\n\t\t\t} else if (data.contains(\"\\\"error\\\"\")) {\n\t\t\t\tisDone.set(true);\n\t\t\t}\n\n\t\t});\n\t\t// 轮询检查结束标志\n\t\twhile (!isDone.get()) {\n\t\t\tThreadUtil.sleep(100);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-all/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-all</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool是一个小而全的Java工具类库，通过静态方法封装，降低相关API的学习成本，提高工作效率，使Java拥有函数式语言般的优雅，让Java语言也可以“甜甜的”。</description>\n\t<url>https://github.com/looly/hutool</url>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-aop</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-bloomFilter</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-cache</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-crypto</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-db</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-dfa</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-extra</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-http</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-script</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-system</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-cron</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-poi</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-captcha</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-socket</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-jwt</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-ai</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-shade-plugin</artifactId>\n\t\t\t\t<version>3.2.1</version>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>shade</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<!-- 创建源码jar -->\n\t\t\t\t\t\t\t<createSourcesJar>true</createSourcesJar>\n\t\t\t\t\t\t\t<artifactSet>\n\t\t\t\t\t\t\t\t<includes>\n\t\t\t\t\t\t\t\t\t<include>${project.groupId}:*:*</include>\n\t\t\t\t\t\t\t\t</includes>\n\t\t\t\t\t\t\t</artifactSet>\n\t\t\t\t\t\t\t<filters>\n\t\t\t\t\t\t\t\t<filter>\n\t\t\t\t\t\t\t\t\t<artifact>*:*</artifact>\n\t\t\t\t\t\t\t\t\t<excludes>\n\t\t\t\t\t\t\t\t\t\t<exclude>META-INF/maven/**</exclude>\n\t\t\t\t\t\t\t\t\t</excludes>\n\t\t\t\t\t\t\t\t</filter>\n\t\t\t\t\t\t\t</filters>\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n</project>\n"
  },
  {
    "path": "hutool-all/src/main/java/cn/hutool/Hutool.java",
    "content": "/*\n * Copyright (C) 2017 hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool;\n\nimport cn.hutool.core.lang.ConsoleTable;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Set;\n\n/**\n * <p>\n *     秋千水，竹马道，一眼见你，万物不及。<br>\n *     春水生，春林初胜，春风十里不如你。\n * </p>\n *\n * <p>\n * Hutool是一个小而全的Java工具类库，通过静态方法封装，降低相关API的学习成本，提高工作效率，使Java拥有函数式语言般的优雅，让Java语言也可以“甜甜的”。\n * </p>\n *\n * <p>\n * Hutool中的工具方法来自于每个用户的精雕细琢，它涵盖了Java开发底层代码中的方方面面，它既是大型项目开发中解决小问题的利器，也是小型项目中的效率担当；<br>\n * </p>\n *\n * <p>Hutool是项目中“util”包友好的替代，它节省了开发人员对项目中公用类和公用工具方法的封装时间，使开发专注于业务，同时可以最大限度的避免封装不完善带来的bug。</p>\n *\n * @author Looly\n */\npublic class Hutool {\n\n\tpublic static final String AUTHOR = \"Looly\";\n\n\tprivate Hutool() {\n\t}\n\n\t/**\n\t * 显示Hutool所有的工具类\n\t *\n\t * @return 工具类名集合\n\t * @since 5.5.2\n\t */\n\tpublic static Set<Class<?>> getAllUtils() {\n\t\treturn ClassUtil.scanPackage(\"cn.hutool\",\n\t\t\t\t(clazz) -> (false == clazz.isInterface()) && StrUtil.endWith(clazz.getSimpleName(), \"Util\"));\n\t}\n\n\t/**\n\t * 控制台打印所有工具类\n\t */\n\tpublic static void printAllUtils() {\n\t\tfinal Set<Class<?>> allUtils = getAllUtils();\n\t\tfinal ConsoleTable consoleTable = ConsoleTable.create().addHeader(\"工具类名\", \"所在包\");\n\t\tfor (Class<?> clazz : allUtils) {\n\t\t\tconsoleTable.addBody(clazz.getSimpleName(), clazz.getPackage().getName());\n\t\t}\n\t\tconsoleTable.print();\n\t}\n}\n"
  },
  {
    "path": "hutool-all/src/main/java/cn/hutool/package-info.java",
    "content": "/**\n * <p>\n * Hutool是一个小而全的Java工具类库，通过静态方法封装，降低相关API的学习成本，提高工作效率，使Java拥有函数式语言般的优雅，让Java语言也可以“甜甜的”。\n * </p>\n *\n * <p>\n * Hutool中的工具方法来自于每个用户的精雕细琢，它涵盖了Java开发底层代码中的方方面面，它既是大型项目开发中解决小问题的利器，也是小型项目中的效率担当；<br>\n * </p>\n *\n * <p>Hutool是项目中“util”包友好的替代，它节省了开发人员对项目中公用类和公用工具方法的封装时间，使开发专注于业务，同时可以最大限度的避免封装不完善带来的bug。</p>\n *\n * @author looly\n */\npackage cn.hutool;\n"
  },
  {
    "path": "hutool-aop/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-aop</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 动态代理（AOP）</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.aop</Automatic-Module-Name>\n\t\t<!-- versions -->\n\t\t<cglib.version>3.3.0</cglib.version>\n\t\t<spring.version>5.3.27</spring.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cglib</groupId>\n\t\t\t<artifactId>cglib</artifactId>\n\t\t\t<version>${cglib.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-core</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java",
    "content": "package cn.hutool.aop;\n\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Proxy;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.aop.proxy.ProxyFactory;\nimport cn.hutool.core.util.ClassUtil;\n\n/**\n * 代理工具类\n * @author Looly\n *\n */\npublic final class ProxyUtil {\n\n\t/**\n\t * 使用切面代理对象\n\t *\n\t * @param <T> 切面对象类型\n\t * @param target 目标对象\n\t * @param aspectClass 切面对象类\n\t * @return 代理对象\n\t */\n\tpublic static <T> T proxy(T target, Class<? extends Aspect> aspectClass){\n\t\treturn ProxyFactory.createProxy(target, aspectClass);\n\t}\n\n\t/**\n\t * 使用切面代理对象\n\t *\n\t * @param <T> 被代理对象类型\n\t * @param target 被代理对象\n\t * @param aspect 切面对象\n\t * @return 代理对象\n\t */\n\tpublic static <T> T proxy(T target, Aspect aspect){\n\t\treturn ProxyFactory.createProxy(target, aspect);\n\t}\n\n\t/**\n\t * 创建动态代理对象<br>\n\t * 动态代理对象的创建原理是：<br>\n\t * 假设创建的代理对象名为 $Proxy0<br>\n\t * 1、根据传入的interfaces动态生成一个类，实现interfaces中的接口<br>\n\t * 2、通过传入的classloder将刚生成的类加载到jvm中。即将$Proxy0类load<br>\n\t * 3、调用$Proxy0的$Proxy0(InvocationHandler)构造函数 创建$Proxy0的对象，并且用interfaces参数遍历其所有接口的方法，这些实现方法的实现本质上是通过反射调用被代理对象的方法<br>\n\t * 4、将$Proxy0的实例返回给客户端。 <br>\n\t * 5、当调用代理类的相应方法时，相当于调用 {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])} 方法\n\t *\n\t *\n\t * @param <T> 被代理对象类型\n\t * @param classloader 被代理类对应的ClassLoader\n\t * @param invocationHandler {@link InvocationHandler} ，被代理类通过实现此接口提供动态代理功能\n\t * @param interfaces 代理类中需要实现的被代理类的接口方法\n\t * @return 代理类\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T newProxyInstance(ClassLoader classloader, InvocationHandler invocationHandler, Class<?>... interfaces) {\n\t\treturn (T) Proxy.newProxyInstance(classloader, interfaces, invocationHandler);\n\t}\n\n\t/**\n\t * 创建动态代理对象\n\t *\n\t * @param <T> 被代理对象类型\n\t * @param invocationHandler {@link InvocationHandler} ，被代理类通过实现此接口提供动态代理功能\n\t * @param interfaces 代理类中需要实现的被代理类的接口方法\n\t * @return 代理类\n\t */\n\tpublic static <T> T newProxyInstance(InvocationHandler invocationHandler, Class<?>... interfaces) {\n\t\treturn newProxyInstance(ClassUtil.getClassLoader(), invocationHandler, interfaces);\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java",
    "content": "package cn.hutool.aop.aspects;\n\nimport java.lang.reflect.Method;\n\n/**\n * 切面接口\n *\n * @author looly\n * @author ted.L\n * @since 4.18\n */\npublic interface Aspect {\n\n\t/**\n\t * 目标方法执行前的操作\n\t *\n\t * @param target 目标对象\n\t * @param method 目标方法\n\t * @param args   参数\n\t * @return 是否继续执行接下来的操作\n\t */\n\tboolean before(Object target, Method method, Object[] args);\n\n\t/**\n\t * 目标方法执行后的操作<br>\n\t * 如果 target.method 抛出异常且 {@link Aspect#afterException} 返回true,则不会执行此操作<br>\n\t * 如果 {@link Aspect#afterException} 返回false,则无论target.method是否抛出异常，均会执行此操作<br>\n\t *\n\t * @param target    目标对象\n\t * @param method    目标方法\n\t * @param args      参数\n\t * @param returnVal 目标方法执行返回值\n\t * @return 是否允许返回值（接下来的操作）\n\t */\n\tboolean after(Object target, Method method, Object[] args, Object returnVal);\n\n\t/**\n\t * 目标方法抛出异常时的操作\n\t *\n\t * @param target 目标对象\n\t * @param method 目标方法\n\t * @param args   参数\n\t * @param e      异常\n\t * @return 是否允许抛出异常\n\t */\n\tboolean afterException(Object target, Method method, Object[] args, Throwable e);\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java",
    "content": "package cn.hutool.aop.aspects;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Method;\n\n/**\n * 简单切面类，不做任何操作<br>\n * 可以继承此类实现自己需要的方法即可\n *\n * @author Looly, ted.L\n */\npublic class SimpleAspect implements Aspect, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tpublic boolean before(Object target, Method method, Object[] args) {\n\t\t//继承此类后实现此方法\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic boolean after(Object target, Method method, Object[] args, Object returnVal) {\n\t\t//继承此类后实现此方法\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic boolean afterException(Object target, Method method, Object[] args, Throwable e) {\n\t\t//继承此类后实现此方法\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java",
    "content": "package cn.hutool.aop.aspects;\n\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.lang.Console;\n\nimport java.lang.reflect.Method;\n\n/**\n * 通过日志打印方法的执行时间的切面\n *\n * @author Looly\n */\npublic class TimeIntervalAspect extends SimpleAspect {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final TimeInterval interval = new TimeInterval();\n\n\t@Override\n\tpublic boolean before(Object target, Method method, Object[] args) {\n\t\tinterval.start();\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic boolean after(Object target, Method method, Object[] args, Object returnVal) {\n\t\tConsole.log(\"Method [{}.{}] execute spend [{}]ms return value [{}]\",\n\t\t\t\ttarget.getClass().getName(), //\n\t\t\t\tmethod.getName(), //\n\t\t\t\tinterval.intervalMs(), //\n\t\t\t\treturnVal);\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java",
    "content": "/**\n * 切面实现，提供一些基本的切面实现\n *\n * @author looly\n *\n */\npackage cn.hutool.aop.aspects;"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java",
    "content": "package cn.hutool.aop.interceptor;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport net.sf.cglib.proxy.MethodInterceptor;\nimport net.sf.cglib.proxy.MethodProxy;\n\nimport java.io.Serializable;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * Cglib实现的动态代理切面\n *\n * @author looly, ted.L\n */\npublic class CglibInterceptor implements MethodInterceptor, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Object target;\n\tprivate final Aspect aspect;\n\n\t/**\n\t * 构造\n\t *\n\t * @param target 被代理对象\n\t * @param aspect 切面实现\n\t */\n\tpublic CglibInterceptor(Object target, Aspect aspect) {\n\t\tthis.target = target;\n\t\tthis.aspect = aspect;\n\t}\n\n\tpublic Object getTarget() {\n\t\treturn this.target;\n\t}\n\n\t@Override\n\tpublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {\n\t\tfinal Object target = this.target;\n\t\tObject result = null;\n\t\t// 开始前回调\n\t\tif (aspect.before(target, method, args)) {\n\t\t\ttry {\n//\t\t\t\tresult = proxy.invokeSuper(obj, args);\n\t\t\t\tresult = proxy.invoke(target, args);\n\t\t\t} catch (final Throwable e) {\n\t\t\t\tThrowable throwable = e;\n\t\t\t\tif(throwable instanceof InvocationTargetException){\n\t\t\t\t\tthrowable = ((InvocationTargetException) throwable).getTargetException();\n\t\t\t\t}\n\t\t\t\t// 异常回调（只捕获业务代码导致的异常，而非反射导致的异常）\n\t\t\t\tif (aspect.afterException(target, method, args, throwable)) {\n\t\t\t\t\tthrow throwable;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 结束执行回调\n\t\tif (aspect.after(target, method, args, result)) {\n\t\t\treturn result;\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java",
    "content": "package cn.hutool.aop.interceptor;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * JDK实现的动态代理切面\n *\n * @author Looly\n * @author ted.L\n */\npublic class JdkInterceptor implements InvocationHandler, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Object target;\n\tprivate final Aspect aspect;\n\n\t/**\n\t * 构造\n\t *\n\t * @param target 被代理对象\n\t * @param aspect 切面实现\n\t */\n\tpublic JdkInterceptor(Object target, Aspect aspect) {\n\t\tthis.target = target;\n\t\tthis.aspect = aspect;\n\t}\n\n\tpublic Object getTarget() {\n\t\treturn this.target;\n\t}\n\n\t@Override\n\tpublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n\t\tfinal Object target = this.target;\n\t\tfinal Aspect aspect = this.aspect;\n\t\tObject result = null;\n\n\t\t// 开始前回调\n\t\tif (aspect.before(target, method, args)) {\n\t\t\tReflectUtil.setAccessible(method);\n\n\t\t\ttry {\n\t\t\t\tresult = method.invoke(ClassUtil.isStatic(method) ? null : target, args);\n\t\t\t} catch (InvocationTargetException e) {\n\t\t\t\t// 异常回调（只捕获业务代码导致的异常，而非反射导致的异常）\n\t\t\t\tif (aspect.afterException(target, method, args, e.getTargetException())) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 结束执行回调\n\t\t\tif (aspect.after(target, method, args, result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/interceptor/SpringCglibInterceptor.java",
    "content": "package cn.hutool.aop.interceptor;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport org.springframework.cglib.proxy.MethodInterceptor;\nimport org.springframework.cglib.proxy.MethodProxy;\n\nimport java.io.Serializable;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * Spring-cglib实现的动态代理切面\n *\n * @author looly\n */\npublic class SpringCglibInterceptor implements MethodInterceptor, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Object target;\n\tprivate final Aspect aspect;\n\n\t/**\n\t * 构造\n\t *\n\t * @param target 被代理对象\n\t * @param aspect 切面实现\n\t */\n\tpublic SpringCglibInterceptor(Object target, Aspect aspect) {\n\t\tthis.target = target;\n\t\tthis.aspect = aspect;\n\t}\n\n\t/**\n\t * 获得目标对象\n\t *\n\t * @return 目标对象\n\t */\n\tpublic Object getTarget() {\n\t\treturn this.target;\n\t}\n\n\t@Override\n\tpublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {\n\t\tfinal Object target = this.target;\n\t\tObject result = null;\n\t\t// 开始前回调\n\t\tif (aspect.before(target, method, args)) {\n\t\t\ttry {\n//\t\t\t\tresult = proxy.invokeSuper(obj, args);\n\t\t\t\tresult = proxy.invoke(target, args);\n\t\t\t} catch (Throwable e) {\n\t\t\t\tThrowable throwable = e;\n\t\t\t\tif(throwable instanceof InvocationTargetException){\n\t\t\t\t\tthrowable = ((InvocationTargetException) throwable).getTargetException();\n\t\t\t\t}\n\n\t\t\t\t// 异常回调（只捕获业务代码导致的异常，而非反射导致的异常）\n\t\t\t\tif (aspect.afterException(target, method, args, throwable)) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 结束执行回调\n\t\tif (aspect.after(target, method, args, result)) {\n\t\t\treturn result;\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java",
    "content": "/**\n * 代理拦截器实现\n *\n * @author looly\n *\n */\npackage cn.hutool.aop.interceptor;"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/package-info.java",
    "content": "/**\n * JDK动态代理封装，提供非IOC下的切面支持\n *\n * @author looly\n *\n */\npackage cn.hutool.aop;"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java",
    "content": "package cn.hutool.aop.proxy;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.aop.interceptor.CglibInterceptor;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport net.sf.cglib.proxy.Enhancer;\n\nimport java.lang.reflect.Constructor;\n\n/**\n * 基于Cglib的切面代理工厂\n *\n * @author looly\n *\n */\npublic class CglibProxyFactory extends ProxyFactory{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tpublic <T> T proxy(T target, Aspect aspect) {\n\t\tfinal Class<?> targetClass = target.getClass();\n\n\t\tfinal Enhancer enhancer = new Enhancer();\n\t\tenhancer.setSuperclass(target.getClass());\n\t\tenhancer.setCallback(new CglibInterceptor(target, aspect));\n\t\treturn create(enhancer, targetClass);\n\t}\n\n\t/**\n\t * 创建代理对象<br>\n\t * https://gitee.com/chinabugotech/hutool/issues/I74EX7<br>\n\t * 某些对象存在非空参数构造，则需遍历查找需要的构造完成代理对象构建。\n\t *\n\t * @param <T>         代理对象类型\n\t * @param enhancer    {@link org.springframework.cglib.proxy.Enhancer}\n\t * @param targetClass 目标类型\n\t * @return 代理对象\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static <T> T create(final Enhancer enhancer, final Class<?> targetClass) {\n\t\tfinal Constructor<?>[] constructors = ReflectUtil.getConstructors(targetClass);\n\t\tClass<?>[] parameterTypes;\n\t\tObject[] values;\n\t\tIllegalArgumentException finalException = null;\n\t\tfor (final Constructor<?> constructor : constructors) {\n\t\t\tparameterTypes = constructor.getParameterTypes();\n\t\t\tvalues = ClassUtil.getDefaultValues(parameterTypes);\n\n\t\t\ttry {\n\t\t\t\treturn (T) enhancer.create(parameterTypes, values);\n\t\t\t} catch (final IllegalArgumentException e) {\n\t\t\t\t//ignore\n\t\t\t\tfinalException = e;\n\t\t\t}\n\t\t}\n\t\tif (null != finalException) {\n\t\t\tthrow finalException;\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"No constructor provided\");\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java",
    "content": "package cn.hutool.aop.proxy;\n\nimport cn.hutool.aop.ProxyUtil;\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.aop.interceptor.JdkInterceptor;\n\n/**\n * JDK实现的切面代理\n *\n * @author looly\n */\npublic class JdkProxyFactory extends ProxyFactory {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 获取单例\n\t */\n\tpublic static JdkProxyFactory INSTANCE = new JdkProxyFactory();\n\n\t@Override\n\tpublic <T> T proxy(T target, Aspect aspect) {\n\t\treturn ProxyUtil.newProxyInstance(//\n\t\t\t\ttarget.getClass().getClassLoader(), //\n\t\t\t\tnew JdkInterceptor(target, aspect), //\n\t\t\t\ttarget.getClass().getInterfaces());\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java",
    "content": "package cn.hutool.aop.proxy;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.ServiceLoaderUtil;\n\nimport java.io.Serializable;\n\n/**\n * 代理工厂<br>\n * 根据用户引入代理库的不同，产生不同的代理对象\n *\n * @author looly\n */\npublic abstract class ProxyFactory implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建代理\n\t *\n\t * @param <T>         代理对象类型\n\t * @param target      被代理对象\n\t * @param aspectClass 切面实现类，自动实例化\n\t * @return 代理对象\n\t * @since 5.3.1\n\t */\n\tpublic <T> T proxy(T target, Class<? extends Aspect> aspectClass) {\n\t\treturn proxy(target, ReflectUtil.newInstanceIfPossible(aspectClass));\n\t}\n\n\t/**\n\t * 创建代理\n\t *\n\t * @param <T>    代理对象类型\n\t * @param target 被代理对象\n\t * @param aspect 切面实现\n\t * @return 代理对象\n\t */\n\tpublic abstract <T> T proxy(T target, Aspect aspect);\n\n\t/**\n\t * 根据用户引入Cglib与否自动创建代理对象\n\t *\n\t * @param <T>         切面对象类型\n\t * @param target      目标对象\n\t * @param aspectClass 切面对象类\n\t * @return 代理对象\n\t */\n\tpublic static <T> T createProxy(T target, Class<? extends Aspect> aspectClass) {\n\t\treturn createProxy(target, ReflectUtil.newInstance(aspectClass));\n\t}\n\n\t/**\n\t * 根据用户引入Cglib与否自动创建代理对象\n\t *\n\t * @param <T>    切面对象类型\n\t * @param target 被代理对象\n\t * @param aspect 切面实现\n\t * @return 代理对象\n\t */\n\tpublic static <T> T createProxy(T target, Aspect aspect) {\n\t\tProxyFactory factory = create();\n\t\tif(null == factory){\n\t\t\t// issue#IBF20Z\n\t\t\t// 可能的空指针问题\n\t\t\tfactory = JdkProxyFactory.INSTANCE;\n\t\t}\n\t\treturn factory.proxy(target, aspect);\n\t}\n\n\t/**\n\t * 根据用户引入Cglib与否创建代理工厂\n\t *\n\t * @return 代理工厂\n\t */\n\tpublic static ProxyFactory create() {\n\t\treturn ServiceLoaderUtil.loadFirstAvailable(ProxyFactory.class);\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/proxy/SpringCglibProxyFactory.java",
    "content": "package cn.hutool.aop.proxy;\n\nimport cn.hutool.aop.aspects.Aspect;\nimport cn.hutool.aop.interceptor.SpringCglibInterceptor;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport org.springframework.cglib.proxy.Enhancer;\n\nimport java.lang.reflect.Constructor;\n\n/**\n * 基于Spring-cglib的切面代理工厂\n *\n * @author looly\n *\n */\npublic class SpringCglibProxyFactory extends ProxyFactory{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tpublic <T> T proxy(T target, Aspect aspect) {\n\t\tfinal Class<?> targetClass = target.getClass();\n\n\t\tfinal Enhancer enhancer = new Enhancer();\n\t\tenhancer.setSuperclass(targetClass);\n\t\tenhancer.setCallback(new SpringCglibInterceptor(target, aspect));\n\t\treturn create(enhancer, targetClass);\n\t}\n\n\t/**\n\t * 创建代理对象<br>\n\t * https://gitee.com/chinabugotech/hutool/issues/I74EX7<br>\n\t * 某些对象存在非空参数构造，则需遍历查找需要的构造完成代理对象构建。\n\t *\n\t * @param <T>         代理对象类型\n\t * @param enhancer    {@link Enhancer}\n\t * @param targetClass 目标类型\n\t * @return 代理对象\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static <T> T create(final Enhancer enhancer, final Class<?> targetClass) {\n\t\tfinal Constructor<?>[] constructors = ReflectUtil.getConstructors(targetClass);\n\t\tClass<?>[] parameterTypes;\n\t\tObject[] values;\n\t\tIllegalArgumentException finalException = null;\n\t\tfor (final Constructor<?> constructor : constructors) {\n\t\t\tparameterTypes = constructor.getParameterTypes();\n\t\t\tvalues = ClassUtil.getDefaultValues(parameterTypes);\n\n\t\t\ttry {\n\t\t\t\treturn (T) enhancer.create(parameterTypes, values);\n\t\t\t} catch (final IllegalArgumentException e) {\n\t\t\t\t//ignore\n\t\t\t\tfinalException = e;\n\t\t\t}\n\t\t}\n\t\tif (null != finalException) {\n\t\t\tthrow finalException;\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"No constructor provided\");\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java",
    "content": "/**\n * 代理实现\n *\n * @author looly\n *\n */\npackage cn.hutool.aop.proxy;"
  },
  {
    "path": "hutool-aop/src/main/resources/META-INF/services/cn.hutool.aop.proxy.ProxyFactory",
    "content": "cn.hutool.aop.proxy.CglibProxyFactory\ncn.hutool.aop.proxy.SpringCglibProxyFactory\ncn.hutool.aop.proxy.JdkProxyFactory"
  },
  {
    "path": "hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java",
    "content": "package cn.hutool.aop.test;\n\nimport cn.hutool.aop.ProxyUtil;\nimport cn.hutool.aop.aspects.TimeIntervalAspect;\nimport cn.hutool.core.lang.Console;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * AOP模块单元测试\n *\n * @author Looly\n */\npublic class AopTest {\n\n\t@Test\n\tpublic void aopTest() {\n\t\tAnimal cat = ProxyUtil.proxy(new Cat(), TimeIntervalAspect.class);\n\t\tString result = cat.eat();\n\t\tassertEquals(\"猫吃鱼\", result);\n\t\tcat.seize();\n\t}\n\n\t@Test\n\tpublic void aopByAutoCglibTest() {\n\t\tDog dog = ProxyUtil.proxy(new Dog(), TimeIntervalAspect.class);\n\t\tString result = dog.eat();\n\t\tassertEquals(\"狗吃肉\", result);\n\n\t\tdog.seize();\n\t}\n\n\tinterface Animal {\n\t\tString eat();\n\n\t\tvoid seize();\n\t}\n\n\t/**\n\t * 有接口\n\t *\n\t * @author looly\n\t */\n\tstatic class Cat implements Animal {\n\n\t\t@Override\n\t\tpublic String eat() {\n\t\t\treturn \"猫吃鱼\";\n\t\t}\n\n\t\t@Override\n\t\tpublic void seize() {\n\t\t\tConsole.log(\"抓了条鱼\");\n\t\t}\n\t}\n\n\t/**\n\t * 无接口\n\t *\n\t * @author looly\n\t */\n\tstatic class Dog {\n\t\tpublic String eat() {\n\t\t\treturn \"狗吃肉\";\n\t\t}\n\n\t\tpublic void seize() {\n            Console.log(\"抓了只鸡\");\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testCGLIBProxy() {\n\t\tTagObj target = new TagObj();\n\t\t//目标类设置标记\n\t\ttarget.setTag(\"tag\");\n\n\t\tTagObj proxy = ProxyUtil.proxy(target, TimeIntervalAspect.class);\n\t\t//代理类获取标记tag (断言错误)\n\t\tassertEquals(\"tag\", proxy.getTag());\n\t}\n\n\t@Data\n\tpublic static class TagObj{\n\t\tprivate String tag;\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/test/java/cn/hutool/aop/test/IssueI74EX7Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.aop.test;\n\nimport cn.hutool.aop.aspects.SimpleAspect;\nimport cn.hutool.aop.proxy.CglibProxyFactory;\nimport cn.hutool.aop.proxy.JdkProxyFactory;\nimport cn.hutool.aop.proxy.ProxyFactory;\nimport cn.hutool.aop.proxy.SpringCglibProxyFactory;\nimport cn.hutool.core.lang.Console;\nimport lombok.Setter;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI74EX7Test {\n\t@Test\n\tpublic void proxyTest() {\n\t\tfinal SmsBlend smsBlend = new SmsBlendImpl(1);\n\t\tfinal ProxyFactory engine = new JdkProxyFactory();\n\t\tengine.proxy(smsBlend, new SimpleAspect());\n\t}\n\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I74EX7<br>\n\t * Enhancer.create()默认调用无参构造，有参构造或者多个构造没有很好的兼容。\n\t *\n\t */\n\t@Test\n\tpublic void cglibProxyTest() {\n\t\tfinal SmsBlend smsBlend = new SmsBlendImpl(1);\n\t\tfinal ProxyFactory engine = new CglibProxyFactory();\n\t\tengine.proxy(smsBlend, new SimpleAspect());\n\t}\n\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I74EX7<br>\n\t * Enhancer.create()默认调用无参构造，有参构造或者多个构造没有很好的兼容。\n\t *\n\t */\n\t@Test\n\tpublic void springCglibProxyTest() {\n\t\tfinal SmsBlend smsBlend = new SmsBlendImpl(1);\n\t\tfinal ProxyFactory engine = new SpringCglibProxyFactory();\n\t\tengine.proxy(smsBlend, new SimpleAspect());\n\t}\n\n\t@Test\n\tpublic void springCglibProxyWithoutConstructorTest() {\n\t\tfinal SmsBlend smsBlend = new SmsBlendImplWithoutConstructor();\n\t\tfinal ProxyFactory engine = new SpringCglibProxyFactory();\n\t\tengine.proxy(smsBlend, new SimpleAspect());\n\t}\n\n\tpublic interface SmsBlend{\n\t\tvoid send();\n\t}\n\n\tpublic static class SmsBlendImpl implements SmsBlend{\n\n\t\tprivate final int status;\n\n\t\tpublic SmsBlendImpl(final int status) {\n\t\t\tthis.status = status;\n\t\t}\n\n\t\t@Override\n\t\tpublic void send() {\n\t\t\tConsole.log(\"sms send.\" + status);\n\t\t}\n\t}\n\n\t@Setter\n\tpublic static class SmsBlendImplWithoutConstructor implements SmsBlend{\n\n\t\tprivate int status;\n\n\t\t@Override\n\t\tpublic void send() {\n\t\t\tConsole.log(\"sms send.\" + status);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-aop/src/test/java/cn/hutool/aop/test/IssueIBF20ZTest.java",
    "content": "package cn.hutool.aop.test;\n\nimport cn.hutool.aop.proxy.ProxyFactory;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueIBF20ZTest {\n\n\t@Test\n\tpublic void testLoadFirstAvailableConcurrent() throws InterruptedException {\n\t\t// 创建一个固定大小的线程池\n\t\tint threadCount = 1000;\n\t\tExecutorService executorService = ThreadUtil.newExecutor(threadCount);\n\n\t\t// 创建一个 CountDownLatch，用于等待所有任务完成\n\t\tCountDownLatch latch = new CountDownLatch(threadCount);\n\n\t\t// 计数器用于统计成功加载服务提供者的次数\n\t\tAtomicInteger successCount = new AtomicInteger(0);\n\n\t\t// 提交多个任务到线程池\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\texecutorService.submit(() -> {\n\t\t\t\tProxyFactory factory = ProxyFactory.create();\n\t\t\t\tif (factory != null) {\n\t\t\t\t\t//Console.log(factory.getClass());\n\t\t\t\t\tsuccessCount.incrementAndGet();\n\t\t\t\t}\n\t\t\t\tlatch.countDown(); // 每个任务完成时，计数减一\n\t\t\t});\n\t\t}\n\n\t\t// 等待所有任务完成\n\t\tlatch.await();\n\n\t\t// 关闭线程池并等待所有任务完成\n\t\texecutorService.shutdown();\n\n\t\t// 验证所有线程都成功加载了服务提供者\n\t\tassertEquals(threadCount, successCount.get());\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-bloomFilter</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 布隆过滤器</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.bloomfilter</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport cn.hutool.bloomfilter.filter.DefaultFilter;\nimport cn.hutool.bloomfilter.filter.ELFFilter;\nimport cn.hutool.bloomfilter.filter.JSFilter;\nimport cn.hutool.bloomfilter.filter.PJWFilter;\nimport cn.hutool.bloomfilter.filter.SDBMFilter;\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * BloomFilter 实现 <br>\n * 1.构建hash算法 <br>\n * 2.散列hash映射到数组的bit位置 <br>\n * 3.验证<br>\n * 此实现方式可以指定Hash算法\n *\n * @author Ansj\n */\npublic class BitMapBloomFilter implements BloomFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate BloomFilter[] filters;\n\n\t/**\n\t * 构造，使用默认的5个过滤器\n\t *\n\t * @param m M值决定BitMap的大小\n\t */\n\tpublic BitMapBloomFilter(int m) {\n\t\tlong mNum = NumberUtil.div(String.valueOf(m), String.valueOf(5)).longValue();\n\t\tlong size = mNum * 1024 * 1024 * 8;\n\n\t\tfilters = new BloomFilter[]{\n\t\t\t\tnew DefaultFilter(size),\n\t\t\t\tnew ELFFilter(size),\n\t\t\t\tnew JSFilter(size),\n\t\t\t\tnew PJWFilter(size),\n\t\t\t\tnew SDBMFilter(size)\n\t\t};\n\t}\n\n\t/**\n\t * 使用自定的多个过滤器建立BloomFilter\n\t *\n\t * @param m       M值决定BitMap的大小\n\t * @param filters Bloom过滤器列表\n\t */\n\tpublic BitMapBloomFilter(int m, BloomFilter... filters) {\n\t\tthis(m);\n\t\tthis.filters = filters;\n\t}\n\n\t/**\n\t * 增加字符串到Filter映射中\n\t *\n\t * @param str 字符串\n\t */\n\t@Override\n\tpublic boolean add(String str) {\n\t\tboolean flag = false;\n\t\tfor (BloomFilter filter : filters) {\n\t\t\tflag |= filter.add(str);\n\t\t}\n\t\treturn flag;\n\t}\n\n\t/**\n\t * 是否可能包含此字符串，此处存在误判\n\t *\n\t * @param str 字符串\n\t * @return 是否存在\n\t */\n\t@Override\n\tpublic boolean contains(String str) {\n\t\tfor (BloomFilter filter : filters) {\n\t\t\tif (filter.contains(str) == false) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HashUtil;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.BitSet;\n\n/**\n * BloomFilter实现方式2，此方式使用BitSet存储。<br>\n * Hash算法的使用使用固定顺序，只需指定个数即可\n *\n * @author loolly\n */\npublic class BitSetBloomFilter implements BloomFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final BitSet bitSet;\n\tprivate final int bitSetSize;\n\tprivate final int addedElements;\n\tprivate final int hashFunctionNumber;\n\n\t/**\n\t * 构造一个布隆过滤器，过滤器的容量为c * k 个bit.\n\t *\n\t * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍.\n\t * @param n 当前过滤器预计所要包含的记录.\n\t * @param k 哈希函数的个数，等同每条记录要占用的bit数，此处值取值为1~8\n\t */\n\tpublic BitSetBloomFilter(int c, int n, int k) {\n\t\tAssert.isTrue(c > 0, \"Parameter c must be positive\");\n\t\tAssert.isTrue(n > 0, \"Parameter n must be positive\");\n\t\tthis.hashFunctionNumber = Assert.checkBetween(k, 1, 8,\"hashFunctionNumber must be between 1 and 8\");\n\t\tthis.bitSetSize = (int) Math.ceil(c * k);\n\t\tthis.addedElements = n;\n\t\tthis.bitSet = new BitSet(this.bitSetSize);\n\t}\n\n\t/**\n\t * 通过文件初始化过滤器.\n\t *\n\t * @param path        文件路径\n\t * @param charsetName 字符集\n\t * @throws IOException IO异常\n\t * @deprecated 请使用 {@link #init(String, Charset)}\n\t */\n\t@Deprecated\n\tpublic void init(String path, String charsetName) throws IOException {\n\t\tinit(path, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 通过文件初始化过滤器.\n\t *\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @throws IOException IO异常\n\t * @since 5.8.0\n\t */\n\tpublic void init(String path, Charset charset) throws IOException {\n\t\tBufferedReader reader = FileUtil.getReader(path, charset);\n\t\ttry {\n\t\t\tString line;\n\t\t\twhile (true) {\n\t\t\t\tline = reader.readLine();\n\t\t\t\tif (line == null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthis.add(line);\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean add(String str) {\n\t\tif (contains(str)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tint[] positions = createHashes(str, hashFunctionNumber);\n\t\tfor (int value : positions) {\n\t\t\tint position = Math.abs(value % bitSetSize);\n\t\t\tbitSet.set(position, true);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 判定是否包含指定字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否包含，存在误差\n\t */\n\t@Override\n\tpublic boolean contains(String str) {\n\t\tint[] positions = createHashes(str, hashFunctionNumber);\n\t\tfor (int i : positions) {\n\t\t\tint position = Math.abs(i % bitSetSize);\n\t\t\tif (!bitSet.get(position)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * @return 得到当前过滤器的错误率.\n\t */\n\tpublic double getFalsePositiveProbability() {\n\t\t// (1 - e^(-k * n / m)) ^ k\n\t\treturn Math.pow((1 - Math.exp(-hashFunctionNumber * (double) addedElements / bitSetSize)), hashFunctionNumber);\n\t}\n\n\t/**\n\t * 将字符串的字节表示进行多哈希编码.\n\t *\n\t * @param str        待添加进过滤器的字符串字节表示.\n\t * @param hashNumber 要经过的哈希个数.\n\t * @return 各个哈希的结果数组.\n\t */\n\tpublic static int[] createHashes(String str, int hashNumber) {\n\t\tint[] result = new int[hashNumber];\n\t\tfor (int i = 0; i < hashNumber; i++) {\n\t\t\tresult[i] = hash(str, i);\n\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算Hash值\n\t *\n\t * @param str 被计算Hash的字符串\n\t * @param k   Hash算法序号\n\t * @return Hash值\n\t */\n\tpublic static int hash(String str, int k) {\n\t\tswitch (k) {\n\t\t\tcase 0:\n\t\t\t\treturn HashUtil.rsHash(str);\n\t\t\tcase 1:\n\t\t\t\treturn HashUtil.jsHash(str);\n\t\t\tcase 2:\n\t\t\t\treturn HashUtil.elfHash(str);\n\t\t\tcase 3:\n\t\t\t\treturn HashUtil.bkdrHash(str);\n\t\t\tcase 4:\n\t\t\t\treturn HashUtil.apHash(str);\n\t\t\tcase 5:\n\t\t\t\treturn HashUtil.djbHash(str);\n\t\t\tcase 6:\n\t\t\t\treturn HashUtil.sdbmHash(str);\n\t\t\tcase 7:\n\t\t\t\treturn HashUtil.pjwHash(str);\n\t\t\tdefault:\n\t\t\t\treturn 0;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport java.io.Serializable;\n\n/**\n * Bloom filter 是由 Howard Bloom 在 1970 年提出的二进制向量数据结构，它具有很好的空间和时间效率，被用来检测一个元素是不是集合中的一个成员。<br>\n * 如果检测结果为是，该元素不一定在集合中；但如果检测结果为否，该元素一定不在集合中。<br>\n * 因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内（可能错误）”和“不在集合内（绝对不在集合内）”两种情况。<br>\n * @author Looly\n *\n */\npublic interface BloomFilter extends Serializable{\n\n\t/**\n\t *\n\t * @param str 字符串\n\t * @return 判断一个字符串是否bitMap中存在\n\t */\n\tboolean contains(String str);\n\n\t/**\n\t * 在boolean的bitMap中增加一个字符串<br>\n\t * 如果存在就返回{@code false} .如果不存在.先增加这个字符串.再返回{@code true}\n\t *\n\t * @param str 字符串\n\t * @return 是否加入成功，如果存在就返回{@code false} .如果不存在返回{@code true}\n\t */\n\tboolean add(String str);\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java",
    "content": "package cn.hutool.bloomfilter;\n\n/**\n * 布隆过滤器工具\n *\n * @author looly\n * @since 4.1.5\n */\npublic class BloomFilterUtil {\n\n\t/**\n\t * 创建一个BitSet实现的布隆过滤器，过滤器的容量为c * k 个bit.\n\t *\n\t * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍.\n\t * @param n 当前过滤器预计所要包含的记录.\n\t * @param k 哈希函数的个数，等同每条记录要占用的bit数.\n\t * @return BitSetBloomFilter\n\t */\n\tpublic static BitSetBloomFilter createBitSet(int c, int n, int k) {\n\t\treturn new BitSetBloomFilter(c, n, k);\n\t}\n\n\t/**\n\t * 创建BitMap实现的布隆过滤器\n\t *\n\t * @param m BitMap的大小\n\t * @return BitMapBloomFilter\n\t */\n\tpublic static BitMapBloomFilter createBitMap(int m) {\n\t\treturn new BitMapBloomFilter(m);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java",
    "content": "package cn.hutool.bloomfilter.bitMap;\n\n/**\n * BitMap接口，用于将某个int或long值映射到一个数组中，从而判定某个值是否存在\n *\n * @author looly\n *\n */\npublic interface BitMap{\n\n\tint MACHINE32 = 32;\n\tint MACHINE64 = 64;\n\n\t/**\n\t * 加入值\n\t *\n\t * @param i 值\n\t */\n\tvoid add(long i);\n\n\t/**\n\t * 检查是否包含值\n\t *\n\t * @param i 值\n\t * @return 是否包含\n\t */\n\tboolean contains(long i);\n\n\t/**\n\t * 移除值\n\t *\n\t * @param i 值\n\t */\n\tvoid remove(long i);\n}"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java",
    "content": "package cn.hutool.bloomfilter.bitMap;\n\nimport java.io.Serializable;\n\n/**\n * 过滤器BitMap在32位机器上.这个类能发生更好的效果.一般情况下建议使用此类\n *\n * @author loolly\n *\n */\npublic class IntMap implements BitMap, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final int[] ints;\n\n\t/**\n\t * 构造\n\t */\n\tpublic IntMap() {\n\t\tints = new int[93750000];\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param size 容量\n\t */\n\tpublic IntMap(int size) {\n\t\tints = new int[size];\n\t}\n\n\t@Override\n\tpublic void add(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE32);\n\t\tint c = (int) (i & (BitMap.MACHINE32 - 1));\n\t\tints[r] = ints[r] | (1 << c);\n\t}\n\n\t@Override\n\tpublic boolean contains(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE32);\n\t\tint c = (int) (i & (BitMap.MACHINE32 - 1));\n\t\treturn ((ints[r] >>> c) & 1) == 1;\n\t}\n\n\t@Override\n\tpublic void remove(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE32);\n\t\tint c = (int) (i & (BitMap.MACHINE32 - 1));\n\t\tints[r] &= ~(1 << c);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java",
    "content": "package cn.hutool.bloomfilter.bitMap;\n\nimport java.io.Serializable;\n\n/**\n * 过滤器BitMap在64位机器上.这个类能发生更好的效果.一般机器不建议使用\n *\n * @author loolly\n *\n */\npublic class LongMap implements BitMap, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final long[] longs;\n\n\t/**\n\t * 构造\n\t */\n\tpublic LongMap() {\n\t\tlongs = new long[93750000];\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param size 容量\n\t */\n\tpublic LongMap(int size) {\n\t\tlongs = new long[size];\n\t}\n\n\t@Override\n\tpublic void add(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE64);\n\t\tlong c = i & (BitMap.MACHINE64 - 1);\n\t\tlongs[r] = longs[r] | (1L << c);\n\t}\n\n\t@Override\n\tpublic boolean contains(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE64);\n\t\tlong c = i & (BitMap.MACHINE64 - 1);\n\t\treturn ((longs[r] >>> c) & 1) == 1;\n\t}\n\n\t@Override\n\tpublic void remove(long i) {\n\t\tint r = (int) (i / BitMap.MACHINE64);\n\t\tlong c = i & (BitMap.MACHINE64 - 1);\n\t\tlongs[r] &= ~(1L << c);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java",
    "content": "/**\n * BitMap实现\n *\n * @author looly\n *\n */\npackage cn.hutool.bloomfilter.bitMap;"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.bloomfilter.BloomFilter;\nimport cn.hutool.bloomfilter.bitMap.BitMap;\nimport cn.hutool.bloomfilter.bitMap.IntMap;\nimport cn.hutool.bloomfilter.bitMap.LongMap;\nimport cn.hutool.core.lang.Assert;\n\n/**\n * 抽象Bloom过滤器\n *\n * @author loolly\n *\n */\npublic abstract class AbstractFilter implements BloomFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected static int DEFAULT_MACHINE_NUM = BitMap.MACHINE32;\n\n\tprivate BitMap bm = null;\n\n\tprotected long size;\n\n\t/**\n\t * 构造\n\t *\n\t * @param maxValue 最大值\n\t * @param machineNum 机器位数\n\t */\n\tpublic AbstractFilter(long maxValue, int machineNum) {\n\t\tinit(maxValue, machineNum);\n\t}\n\n\t/**\n\t * 构造32位\n\t *\n\t * @param maxValue 最大值\n\t */\n\tpublic AbstractFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param maxValue 最大值\n\t * @param machineNum 机器位数\n\t */\n\tpublic void init(long maxValue, int machineNum) {\n\t\tthis.size = Assert.checkBetween(maxValue, 1, Integer.MAX_VALUE);\n\t\tfinal int capacity = (int) ((this.size + machineNum - 1) / machineNum);\n\t\tswitch (machineNum) {\n\t\tcase BitMap.MACHINE32:\n\t\t\tbm = new IntMap(capacity);\n\t\t\tbreak;\n\t\tcase BitMap.MACHINE64:\n\t\t\tbm = new LongMap(capacity);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new RuntimeException(\"Error Machine number!\");\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean contains(String str) {\n\t\treturn bm.contains(Math.abs(hash(str)));\n\t}\n\n\t@Override\n\tpublic boolean add(String str) {\n\t\tfinal long hash = Math.abs(hash(str));\n\t\tif (bm.contains(hash)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tbm.add(hash);\n\t\treturn true;\n\t}\n\n\t/**\n\t * 自定义Hash方法\n\t *\n\t * @param str 字符串\n\t * @return HashCode\n\t */\n\tpublic abstract long hash(String str);\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\n/**\n * 默认Bloom过滤器，使用Java自带的Hash算法\n *\n * @author loolly\n */\npublic class DefaultFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic DefaultFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic DefaultFilter(long maxValue, int machineNumber) {\n\t\tsuper(maxValue, machineNumber, HashUtil::javaDefaultHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class ELFFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic ELFFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic ELFFilter(long maxValue, int machineNumber) {\n\t\tsuper(maxValue, machineNumber, HashUtil::elfHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class FNVFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic FNVFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic FNVFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::fnvHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FuncFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.bloomfilter.BloomFilter;\n\nimport java.util.function.Function;\n\n/**\n * 基于Hash函数方法的{@link BloomFilter}\n *\n * @author looly\n * @since 5.8.0\n */\npublic class FuncFilter extends AbstractFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Function<String, Number> hashFunc;\n\n\t/**\n\t * 构造\n\t *\n\t * @param maxValue 最大值\n\t * @param hashFunc Hash函数\n\t */\n\tpublic FuncFilter(long maxValue, Function<String, Number> hashFunc) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM, hashFunc);\n\t}\n\n\t/**\n\t * @param maxValue   最大值\n\t * @param machineNum 机器位数\n\t * @param hashFunc   Hash函数\n\t */\n\tpublic FuncFilter(long maxValue, int machineNum, Function<String, Number> hashFunc) {\n\t\tsuper(maxValue, machineNum);\n\t\tthis.hashFunc = hashFunc;\n\t}\n\n\t@Override\n\tpublic long hash(String str) {\n\t\treturn hashFunc.apply(str).longValue() % size;\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class HfFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic HfFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic HfFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::hfHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class HfIpFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic HfIpFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic HfIpFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::hfIpHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class JSFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic JSFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic JSFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::jsHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class PJWFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic PJWFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic PJWFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::pjwHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class RSFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic RSFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic RSFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::rsHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class SDBMFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic SDBMFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic SDBMFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::sdbmHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java",
    "content": "package cn.hutool.bloomfilter.filter;\n\nimport cn.hutool.core.util.HashUtil;\n\npublic class TianlFilter extends FuncFilter {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic TianlFilter(long maxValue) {\n\t\tthis(maxValue, DEFAULT_MACHINE_NUM);\n\t}\n\n\tpublic TianlFilter(long maxValue, int machineNum) {\n\t\tsuper(maxValue, machineNum, HashUtil::tianlHash);\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java",
    "content": "/**\n * 各种Hash算法的过滤器实现\n *\n * @author looly\n *\n */\npackage cn.hutool.bloomfilter.filter;"
  },
  {
    "path": "hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java",
    "content": "/**\n * 布隆过滤，提供一些Hash算法的布隆过滤\n *\n * @author looly\n *\n */\npackage cn.hutool.bloomfilter;"
  },
  {
    "path": "hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/AbstractFilterTest.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport cn.hutool.bloomfilter.bitMap.BitMap;\nimport cn.hutool.bloomfilter.filter.DefaultFilter;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\n\npublic class AbstractFilterTest {\n\n\t@Test\n\tvoid testInitWhenMaxValueLessThanMachineNum() {\n\t\tassertDoesNotThrow(() -> {\n\t\t\tDefaultFilter filter = new DefaultFilter(1, BitMap.MACHINE32);\n\t\t\tfilter.add(\"init\");\n\t\t}, \"maxValue=1且machineNum=32时add应无异常\");\n\t\tassertDoesNotThrow(() -> {\n\t\t\tDefaultFilter filter = new DefaultFilter(31, BitMap.MACHINE32);\n\t\t\tfilter.add(\"init\");\n\t\t}, \"maxValue=31且machineNum=32时add应无异常\");\n\t\tassertDoesNotThrow(() -> {\n\t\t\tDefaultFilter filter = new DefaultFilter(1, BitMap.MACHINE64);\n\t\t\tfilter.add(\"init\");\n\t\t}, \"maxValue=1且machineNum=64时add应无异常\");\n\t\tassertDoesNotThrow(() -> {\n\t\t\tDefaultFilter filter = new DefaultFilter(63, BitMap.MACHINE64);\n\t\t\tfilter.add(\"init\");\n\t\t}, \"maxValue=63且machineNum=64时add应无异常\");\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.bloomfilter.bitMap.IntMap;\nimport cn.hutool.bloomfilter.bitMap.LongMap;\n\npublic class BitMapBloomFilterTest {\n\n\t@Test\n\tpublic void filterTest() {\n\t\tBitMapBloomFilter filter = new BitMapBloomFilter(10);\n\t\tfilter.add(\"123\");\n\t\tfilter.add(\"abc\");\n\t\tfilter.add(\"ddd\");\n\n\t\tassertTrue(filter.contains(\"abc\"));\n\t\tassertTrue(filter.contains(\"ddd\"));\n\t\tassertTrue(filter.contains(\"123\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testIntMap(){\n\t\tIntMap intMap = new IntMap();\n\n\t\tfor (int i = 0 ; i < 32; i++) {\n\t\t\tintMap.add(i);\n\t\t}\n\t\tintMap.remove(30);\n\n\n\t\tfor (int i = 0; i < 32; i++) {\n\t\t\tSystem.out.println(i + \"是否存在-->\" + intMap.contains(i));\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testLongMap(){\n\t\tLongMap longMap = new LongMap();\n\n\t\tfor (int i = 0 ; i < 64; i++) {\n\t\t\tlongMap.add(i);\n\t\t}\n\t\tlongMap.remove(30);\n\n\n\t\tfor (int i = 0; i < 64; i++) {\n\t\t\tSystem.out.println(i + \"是否存在-->\" + longMap.contains(i));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitSetBloomFilterTest.java",
    "content": "package cn.hutool.bloomfilter;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class BitSetBloomFilterTest {\n\n\t@Test\n\tpublic void testConstructorWithInvalidParameters() {\n\t\t// 测试参数 c 的无效情况（c <= 0）\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(0, 100, 3);\n\t\t});\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(-5, 100, 3);\n\t\t});\n\t\t// 测试参数 n 的无效情况 (n <= 0)\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(200, 0, 3);\n\t\t});\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(200, -10, 3);\n\t\t});\n\t\t// 测试参数 k 的无效情况（k < 1 或 k > 8）\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(200, 100, 0);\n\t\t});\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(200, 100, 9);\n\t\t});\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tnew BitSetBloomFilter(200, 100, -2);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-bom/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>pom</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-bom</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>提供丰富的Java工具方法，此模块为Hutool所有模块汇总，最终形式为拆分开的多个jar包，可以通过exclude方式排除不需要的模块</description>\n\t<url>https://github.com/looly/hutool</url>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-aop</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-bloomFilter</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-cache</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-crypto</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-db</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-dfa</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-extra</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-http</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-script</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-system</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-cron</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-poi</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-captcha</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-socket</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-jwt</artifactId>\n\t\t\t\t<version>${project.parent.version}</version>\n\t\t\t</dependency>\n\t\t</dependencies>\n\t</dependencyManagement>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-aop</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-bloomFilter</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-cache</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-crypto</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-db</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-dfa</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-extra</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-http</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-script</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-system</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-cron</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-poi</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-captcha</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-socket</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-jwt</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-cache/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-cache</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 缓存</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.cache</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/Cache.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.CacheObj;\nimport cn.hutool.core.lang.func.Func0;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\n\n/**\n * 缓存接口\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly, jodd\n */\npublic interface Cache<K, V> extends Iterable<V>, Serializable {\n\n\t/**\n\t * 返回缓存容量，{@code 0}表示无大小限制\n\t *\n\t * @return 返回缓存容量，{@code 0}表示无大小限制\n\t */\n\tint capacity();\n\n\t/**\n\t * 缓存失效时长， {@code 0} 表示没有设置，单位毫秒\n\t *\n\t * @return 缓存失效时长， {@code 0} 表示没有设置，单位毫秒\n\t */\n\tlong timeout();\n\n\t/**\n\t * 将对象加入到缓存，使用默认失效时长\n\t *\n\t * @param key    键\n\t * @param object 缓存的对象\n\t * @see Cache#put(Object, Object, long)\n\t */\n\tvoid put(K key, V object);\n\n\t/**\n\t * 将对象加入到缓存，使用指定失效时长<br>\n\t * 如果缓存空间满了，{@link #prune()} 将被调用以获得空间来存放新对象\n\t *\n\t * @param key     键\n\t * @param object  缓存的对象\n\t * @param timeout 失效时长，单位毫秒\n\t */\n\tvoid put(K key, V object, long timeout);\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回{@code null}\n\t * <p>\n\t * 调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回{@code null}，否则返回值。\n\t * <p>\n\t * 每次调用此方法会刷新最后访问时间，也就是说会重新计算超时时间。\n\t *\n\t * @param key 键\n\t * @return 键对应的对象\n\t * @see #get(Object, boolean)\n\t */\n\tdefault V get(K key) {\n\t\treturn get(key, true);\n\t}\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t * <p>\n\t * 调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回返回supplier回调产生的对象，否则返回值。\n\t * <p>\n\t * 每次调用此方法会刷新最后访问时间，也就是说会重新计算超时时间。\n\t *\n\t * @param key      键\n\t * @param supplier 如果不存在回调方法，用于生产值对象\n\t * @return 值对象\n\t */\n\tdefault V get(K key, Func0<V> supplier) {\n\t\treturn get(key, true, supplier);\n\t}\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t * <p>\n\t * 调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回supplier回调产生的对象，否则返回值。\n\t * <p>\n\t * 每次调用此方法会可选是否刷新最后访问时间，{@code true}表示会重新计算超时时间。\n\t *\n\t * @param key                键\n\t * @param isUpdateLastAccess 是否更新最后访问时间，即重新计算超时时间。\n\t * @param supplier           如果不存在回调方法，用于生产值对象\n\t * @return 值对象\n\t */\n\tV get(K key, boolean isUpdateLastAccess, Func0<V> supplier);\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t * <p>\n\t * 调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回supplier回调产生的对象，否则返回值。\n\t * <p>\n\t * 每次调用此方法会可选是否刷新最后访问时间，{@code true}表示会重新计算超时时间。\n\t *\n\t * @param key                键\n\t * @param isUpdateLastAccess 是否更新最后访问时间，即重新计算超时时间。\n\t * @param timeout 自定义超时时间\n\t * @param supplier           如果不存在回调方法，用于生产值对象\n\t * @return 值对象\n\t */\n\tV get(K key, boolean isUpdateLastAccess, long timeout, Func0<V> supplier);\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回{@code null}\n\t * <p>\n\t * 调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回{@code null}，否则返回值。\n\t * <p>\n\t * 每次调用此方法会可选是否刷新最后访问时间，{@code true}表示会重新计算超时时间。\n\t *\n\t * @param key                键\n\t * @param isUpdateLastAccess 是否更新最后访问时间，即重新计算超时时间。\n\t * @return 键对应的对象\n\t */\n\tV get(K key, boolean isUpdateLastAccess);\n\n\t/**\n\t * 返回包含键和值得迭代器\n\t *\n\t * @return 缓存对象迭代器\n\t * @since 4.0.10\n\t */\n\tIterator<CacheObj<K, V>> cacheObjIterator();\n\n\t/**\n\t * 从缓存中清理过期对象，清理策略取决于具体实现\n\t *\n\t * @return 清理的缓存对象个数\n\t */\n\tint prune();\n\n\t/**\n\t * 缓存是否已满，仅用于有空间限制的缓存对象\n\t *\n\t * @return 缓存是否已满，仅用于有空间限制的缓存对象\n\t */\n\tboolean isFull();\n\n\t/**\n\t * 从缓存中移除对象\n\t *\n\t * @param key 键\n\t */\n\tvoid remove(K key);\n\n\t/**\n\t * 清空缓存\n\t */\n\tvoid clear();\n\n\t/**\n\t * 缓存的对象数量\n\t *\n\t * @return 缓存的对象数量\n\t */\n\tint size();\n\n\t/**\n\t * 缓存是否为空\n\t *\n\t * @return 缓存是否为空\n\t */\n\tboolean isEmpty();\n\n\t/**\n\t * 是否包含key\n\t *\n\t * @param key KEY\n\t * @return 是否包含key\n\t */\n\tboolean containsKey(K key);\n\n\t/**\n\t * 设置监听\n\t *\n\t * @param listener 监听\n\t * @return this\n\t * @since 5.5.2\n\t */\n\tdefault Cache<K, V> setListener(CacheListener<K, V> listener){\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/CacheListener.java",
    "content": "package cn.hutool.cache;\n\n/**\n * 缓存监听，用于实现缓存操作时的回调监听，例如缓存对象的移除事件等\n *\n * @param <K> 缓存键\n * @param <V> 缓存值\n * @author looly\n * @since 5.5.2\n */\npublic interface CacheListener<K, V> {\n\n\t/**\n\t * 对象移除回调\n\t *\n\t * @param key          键\n\t * @param cachedObject 被缓存的对象\n\t */\n\tvoid onRemove(K key, V cachedObject);\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.FIFOCache;\nimport cn.hutool.cache.impl.LFUCache;\nimport cn.hutool.cache.impl.LRUCache;\nimport cn.hutool.cache.impl.NoCache;\nimport cn.hutool.cache.impl.TimedCache;\nimport cn.hutool.cache.impl.WeakCache;\n\n/**\n * 缓存工具类\n *\n * @author Looly\n * @since 3.0.1\n */\npublic class CacheUtil {\n\n\t/**\n\t * 创建FIFO(first in first out) 先进先出缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @param timeout  过期时长，单位：毫秒\n\t * @return {@link FIFOCache}\n\t */\n\tpublic static <K, V> FIFOCache<K, V> newFIFOCache(int capacity, long timeout) {\n\t\treturn new FIFOCache<>(capacity, timeout);\n\t}\n\n\t/**\n\t * 创建FIFO(first in first out) 先进先出缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @return {@link FIFOCache}\n\t */\n\tpublic static <K, V> FIFOCache<K, V> newFIFOCache(int capacity) {\n\t\treturn new FIFOCache<>(capacity);\n\t}\n\n\t/**\n\t * 创建LFU(least frequently used) 最少使用率缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @param timeout  过期时长，单位：毫秒\n\t * @return {@link LFUCache}\n\t */\n\tpublic static <K, V> LFUCache<K, V> newLFUCache(int capacity, long timeout) {\n\t\treturn new LFUCache<>(capacity, timeout);\n\t}\n\n\t/**\n\t * 创建LFU(least frequently used) 最少使用率缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @return {@link LFUCache}\n\t */\n\tpublic static <K, V> LFUCache<K, V> newLFUCache(int capacity) {\n\t\treturn new LFUCache<>(capacity);\n\t}\n\n\n\t/**\n\t * 创建LRU (least recently used)最近最久未使用缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @param timeout  过期时长，单位：毫秒\n\t * @return {@link LRUCache}\n\t */\n\tpublic static <K, V> LRUCache<K, V> newLRUCache(int capacity, long timeout) {\n\t\treturn new LRUCache<>(capacity, timeout);\n\t}\n\n\t/**\n\t * 创建LRU (least recently used)最近最久未使用缓存.\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param capacity 容量\n\t * @return {@link LRUCache}\n\t */\n\tpublic static <K, V> LRUCache<K, V> newLRUCache(int capacity) {\n\t\treturn new LRUCache<>(capacity);\n\t}\n\n\t/**\n\t * 创建定时缓存，通过定时任务自动清除过期缓存对象\n\t *\n\t * @param <K>                Key类型\n\t * @param <V>                Value类型\n\t * @param timeout            过期时长，单位：毫秒\n\t * @param schedulePruneDelay 间隔时长，单位毫秒\n\t * @return {@link TimedCache}\n\t * @since 5.8.28\n\t */\n\tpublic static <K, V> TimedCache<K, V> newTimedCache(long timeout, long schedulePruneDelay) {\n\t\tfinal TimedCache<K, V> cache = newTimedCache(timeout);\n\t\tcache.schedulePrune(schedulePruneDelay);\n\t\treturn cache;\n\t}\n\n\t/**\n\t * 创建定时缓存.\n\t *\n\t * @param <K>     Key类型\n\t * @param <V>     Value类型\n\t * @param timeout 过期时长，单位：毫秒\n\t * @return {@link TimedCache}\n\t */\n\tpublic static <K, V> TimedCache<K, V> newTimedCache(long timeout) {\n\t\treturn new TimedCache<>(timeout);\n\t}\n\n\t/**\n\t * 创建弱引用缓存.\n\t *\n\t * @param <K>     Key类型\n\t * @param <V>     Value类型\n\t * @param timeout 过期时长，单位：毫秒\n\t * @return {@link WeakCache}\n\t * @since 3.0.7\n\t */\n\tpublic static <K, V> WeakCache<K, V> newWeakCache(long timeout) {\n\t\treturn new WeakCache<>(timeout);\n\t}\n\n\t/**\n\t * 创建无缓存实现.\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @return {@link NoCache}\n\t */\n\tpublic static <K, V> NoCache<K, V> newNoCache() {\n\t\treturn new NoCache<>();\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.List;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 全局缓存清理定时器池，用于在需要过期支持的缓存对象中超时任务池\n *\n * @author looly\n */\npublic enum GlobalPruneTimer {\n\t/**\n\t * 单例对象\n\t */\n\tINSTANCE;\n\n\t/**\n\t * 缓存任务计数\n\t */\n\tprivate final AtomicInteger cacheTaskNumber = new AtomicInteger(1);\n\n\t/**\n\t * 定时器\n\t */\n\tprivate ScheduledExecutorService pruneTimer;\n\n\t/**\n\t * 构造\n\t */\n\tGlobalPruneTimer() {\n\t\tcreate();\n\t}\n\n\t/**\n\t * 启动定时任务\n\t *\n\t * @param task  任务\n\t * @param delay 周期\n\t * @return {@link ScheduledFuture}对象，可手动取消此任务\n\t */\n\tpublic ScheduledFuture<?> schedule(Runnable task, long delay) {\n\t\treturn this.pruneTimer.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS);\n\t}\n\n\t/**\n\t * 创建定时器\n\t */\n\tpublic void create() {\n\t\tif (null != pruneTimer) {\n\t\t\tshutdownNow();\n\t\t}\n\t\tthis.pruneTimer = new ScheduledThreadPoolExecutor(1, r -> ThreadUtil.newThread(r, StrUtil.format(\"Pure-Timer-{}\", cacheTaskNumber.getAndIncrement())));\n\t}\n\n\t/**\n\t * 销毁全局定时器\n\t */\n\tpublic void shutdown() {\n\t\tif (null != pruneTimer) {\n\t\t\tpruneTimer.shutdown();\n\t\t}\n\t}\n\n\t/**\n\t * 销毁全局定时器\n\t *\n\t * @return 销毁时未被执行的任务列表\n\t */\n\tpublic List<Runnable> shutdownNow() {\n\t\tif (null != pruneTimer) {\n\t\t\treturn pruneTimer.shutdownNow();\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java",
    "content": "package cn.hutool.cache.file;\n\nimport java.io.File;\nimport java.io.Serializable;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\n\n/**\n * 文件缓存，以解决频繁读取文件引起的性能问题\n * @author Looly\n *\n */\npublic abstract class AbstractFileCache implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 容量 */\n\tprotected final int capacity;\n\t/** 缓存的最大文件大小，文件大于此大小时将不被缓存 */\n\tprotected final int maxFileSize;\n\t/** 默认超时时间，0表示无默认超时 */\n\tprotected final long timeout;\n\t/** 缓存实现 */\n\tprotected final Cache<File, byte[]> cache;\n\n\t/** 已使用缓存空间 */\n\tprotected int usedSize;\n\n\t/**\n\t * 构造\n\t * @param capacity 缓存容量\n\t * @param maxFileSize 文件最大大小\n\t * @param timeout 默认超时时间，0表示无默认超时\n\t */\n\tpublic AbstractFileCache(int capacity, int maxFileSize, long timeout) {\n\t\tthis.capacity = capacity;\n\t\tthis.maxFileSize = maxFileSize;\n\t\tthis.timeout = timeout;\n\t\tthis.cache = initCache();\n\t}\n\n\t/**\n\t * @return 缓存容量（byte数）\n\t */\n\tpublic int capacity() {\n\t\treturn capacity;\n\t}\n\n\t/**\n\t * @return 已使用空间大小（byte数）\n\t */\n\tpublic int getUsedSize() {\n\t\treturn usedSize;\n\t}\n\n\t/**\n\t * @return 允许被缓存文件的最大byte数\n\t */\n\tpublic int maxFileSize() {\n\t\treturn maxFileSize;\n\t}\n\n\t/**\n\t * @return 缓存的文件数\n\t */\n\tpublic int getCachedFilesCount() {\n\t\treturn cache.size();\n\t}\n\n\t/**\n\t * @return 超时时间\n\t */\n\tpublic long timeout() {\n\t\treturn this.timeout;\n\t}\n\n\t/**\n\t * 清空缓存\n\t */\n\tpublic void clear() {\n\t\tcache.clear();\n\t\tusedSize = 0;\n\t}\n\n\t// ---------------------------------------------------------------- get\n\n\t/**\n\t * 获得缓存过的文件bytes\n\t * @param path 文件路径\n\t * @return 缓存过的文件bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic byte[] getFileBytes(String path) throws IORuntimeException {\n\t\treturn getFileBytes(new File(path));\n\t}\n\n\t/**\n\t * 获得缓存过的文件bytes\n\t * @param file 文件\n\t * @return 缓存过的文件bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic byte[] getFileBytes(File file) throws IORuntimeException {\n\t\tbyte[] bytes = cache.get(file);\n\t\tif (bytes != null) {\n\t\t\treturn bytes;\n\t\t}\n\n\t\t// add file\n\t\tbytes = FileUtil.readBytes(file);\n\n\t\tif ((maxFileSize != 0) && (file.length() > maxFileSize)) {\n\t\t\t//大于缓存空间，不缓存，直接返回\n\t\t\treturn bytes;\n\t\t}\n\n\t\tusedSize += bytes.length;\n\n\t\t//文件放入缓存，如果usedSize > capacity，purge()方法将被调用\n\t\tcache.put(file, bytes);\n\n\t\treturn bytes;\n\t}\n\n\t// ---------------------------------------------------------------- protected method start\n\t/**\n\t * 初始化实现文件缓存的缓存对象\n\t * @return {@link Cache}\n\t */\n\tprotected abstract Cache<File, byte[]> initCache();\n\t// ---------------------------------------------------------------- protected method end\n\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java",
    "content": "package cn.hutool.cache.file;\n\nimport java.io.File;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.cache.impl.LFUCache;\n\n/**\n *  使用LFU缓存文件，以解决频繁读取文件引起的性能问题\n * @author Looly\n *\n */\npublic class LFUFileCache extends AbstractFileCache{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造<br>\n\t * 最大文件大小为缓存容量的一半<br>\n\t * 默认无超时\n\t * @param capacity 缓存容量\n\t */\n\tpublic LFUFileCache(int capacity) {\n\t\tthis(capacity, capacity / 2, 0);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 默认无超时\n\t * @param capacity 缓存容量\n\t * @param maxFileSize 最大文件大小\n\t */\n\tpublic LFUFileCache(int capacity, int maxFileSize) {\n\t\tthis(capacity, maxFileSize, 0);\n\t}\n\n\t/**\n\t * 构造\n\t * @param capacity 缓存容量\n\t * @param maxFileSize 文件最大大小\n\t * @param timeout 默认超时时间，0表示无默认超时\n\t */\n\tpublic LFUFileCache(int capacity, int maxFileSize, long timeout) {\n\t\tsuper(capacity, maxFileSize, timeout);\n\t}\n\n\t@Override\n\tprotected Cache<File, byte[]> initCache() {\n\t\treturn new LFUCache<File, byte[]>(LFUFileCache.this.capacity, LFUFileCache.this.timeout) {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t@Override\n\t\t\tpublic boolean isFull() {\n\t\t\t\treturn LFUFileCache.this.usedSize > this.capacity;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tprotected void onRemove(File key, byte[] cachedObject) {\n\t\t\t\tusedSize -= cachedObject.length;\n\t\t\t}\n\t\t};\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java",
    "content": "package cn.hutool.cache.file;\n\nimport java.io.File;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.cache.impl.LRUCache;\n\n/**\n *  使用LRU缓存文件，以解决频繁读取文件引起的性能问题\n * @author Looly\n *\n */\npublic class LRUFileCache extends AbstractFileCache{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造<br>\n\t * 最大文件大小为缓存容量的一半<br>\n\t * 默认无超时\n\t * @param capacity 缓存容量\n\t */\n\tpublic LRUFileCache(int capacity) {\n\t\tthis(capacity, capacity / 2, 0);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 默认无超时\n\t * @param capacity 缓存容量\n\t * @param maxFileSize 最大文件大小\n\t */\n\tpublic LRUFileCache(int capacity, int maxFileSize) {\n\t\tthis(capacity, maxFileSize, 0);\n\t}\n\n\t/**\n\t * 构造\n\t * @param capacity 缓存容量\n\t * @param maxFileSize 文件最大大小\n\t * @param timeout 默认超时时间，0表示无默认超时\n\t */\n\tpublic LRUFileCache(int capacity, int maxFileSize, long timeout) {\n\t\tsuper(capacity, maxFileSize, timeout);\n\t}\n\n\t@Override\n\tprotected Cache<File, byte[]> initCache() {\n\t\treturn new LRUCache<File, byte[]>(LRUFileCache.this.capacity, super.timeout) {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t@Override\n\t\t\tpublic boolean isFull() {\n\t\t\t\treturn LRUFileCache.this.usedSize > this.capacity;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tprotected void onRemove(File key, byte[] cachedObject) {\n\t\t\t\tusedSize -= cachedObject.length;\n\t\t\t}\n\t\t};\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java",
    "content": "/**\n * 提供针对文件的缓存实现\n *\n * @author looly\n *\n */\npackage cn.hutool.cache.file;"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.cache.CacheListener;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutableObj;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.stream.Collectors;\n\n/**\n * 超时和限制大小的缓存的默认实现<br>\n * 继承此抽象缓存需要：<br>\n * <ul>\n * <li>创建一个新的Map</li>\n * <li>实现 {@code prune} 策略</li>\n * </ul>\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly, jodd\n */\npublic abstract class AbstractCache<K, V> implements Cache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected Map<Mutable<K>, CacheObj<K, V>> cacheMap;\n\n\t/**\n\t * 写的时候每个key一把锁，降低锁的粒度\n\t */\n\tprotected final SafeConcurrentHashMap<K, Lock> keyLockMap = new SafeConcurrentHashMap<>();\n\n\t/**\n\t * 返回缓存容量，{@code 0}表示无大小限制\n\t */\n\tprotected int capacity;\n\t/**\n\t * 缓存失效时长， {@code 0} 表示无限制，单位毫秒\n\t */\n\tprotected long timeout;\n\n\t/**\n\t * 每个对象是否有单独的失效时长，用于决定清理过期对象是否有必要。\n\t */\n\tprotected boolean existCustomTimeout;\n\n\t/**\n\t * 命中数，即命中缓存计数\n\t */\n\tprotected LongAdder hitCount = new LongAdder();\n\t/**\n\t * 丢失数，即未命中缓存计数\n\t */\n\tprotected LongAdder missCount = new LongAdder();\n\n\t/**\n\t * 缓存监听\n\t */\n\tprotected CacheListener<K, V> listener;\n\n\t// ---------------------------------------------------------------- put start\n\t@Override\n\tpublic void put(K key, V object) {\n\t\tput(key, object, timeout);\n\t}\n\n\t/**\n\t * 加入元素，无锁\n\t *\n\t * @param key     键\n\t * @param object  值\n\t * @param timeout 超时时长\n\t * @since 4.5.16\n\t */\n\tprotected void putWithoutLock(K key, V object, long timeout) {\n\t\tCacheObj<K, V> co = new CacheObj<>(key, object, timeout);\n\t\tif (timeout != 0) {\n\t\t\texistCustomTimeout = true;\n\t\t}\n\n\t\tfinal MutableObj<K> mKey = MutableObj.of(key);\n\n\t\t// issue#3618 对于替换的键值对，不做满队列检查和清除\n\t\tfinal CacheObj<K, V> oldObj = cacheMap.get(mKey);\n\t\tif (null != oldObj) {\n\t\t\tonRemove(oldObj.key, oldObj.obj);\n\t\t\t// 存在相同key，覆盖之\n\t\t\tcacheMap.put(mKey, co);\n\t\t} else {\n\t\t\tif (isFull()) {\n\t\t\t\tpruneCache();\n\t\t\t}\n\t\t\tcacheMap.put(mKey, co);\n\t\t}\n\t}\n\t// ---------------------------------------------------------------- put end\n\n\t// ---------------------------------------------------------------- get start\n\t/**\n\t * @return 命中数\n\t */\n\tpublic long getHitCount() {\n\t\treturn hitCount.sum();\n\t}\n\n\t/**\n\t * @return 丢失数\n\t */\n\tpublic long getMissCount() {\n\t\treturn missCount.sum();\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess, Func0<V> supplier) {\n\t\treturn get(key, isUpdateLastAccess, this.timeout, supplier);\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess, long timeout, Func0<V> supplier) {\n\t\tV v = get(key, isUpdateLastAccess);\n\t\tif (null == v && null != supplier) {\n\t\t\t//每个key单独获取一把锁，降低锁的粒度提高并发能力，see pr#1385@Github\n\t\t\tfinal Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock());\n\t\t\tkeyLock.lock();\n\t\t\ttry {\n\t\t\t\t// 双重检查锁，防止在竞争锁的过程中已经有其它线程写入\n\t\t\t\t// issue#3686 由于这个方法内的加锁是get独立锁，不和put锁互斥，而put和pruneCache会修改cacheMap，导致在pruneCache过程中get会有并发问题\n\t\t\t\t// 因此此处需要使用带全局锁的get获取值\n\t\t\t\tv = get(key, isUpdateLastAccess);\n\t\t\t\t// fix issue#IDQGP2: 双重检查后若缓存已有值则直接返回，不再调用supplier\n\t\t\t\t// 原实现忽略了双重检查的结果，导致高并发下缓存值被重复覆盖\n\t\t\t\tif (null == v) {\n\t\t\t\t\tv = supplier.callWithRuntimeException();\n\t\t\t\t\tput(key, v, timeout);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tkeyLock.unlock();\n\t\t\t\tkeyLockMap.remove(key);\n\t\t\t}\n\t\t}\n\t\treturn v;\n\t}\n\n\t/**\n\t * 获取键对应的{@link CacheObj}\n\t * @param key 键，实际使用时会被包装为{@link MutableObj}\n\t * @return {@link CacheObj}\n\t * @since 5.8.0\n\t */\n\tprotected CacheObj<K, V> getWithoutLock(K key){\n\t\treturn this.cacheMap.get(MutableObj.of(key));\n\t}\n\t// ---------------------------------------------------------------- get end\n\n\t@Override\n\tpublic Iterator<V> iterator() {\n\t\tCacheObjIterator<K, V> copiedIterator = (CacheObjIterator<K, V>) this.cacheObjIterator();\n\t\treturn new CacheValuesIterator<>(copiedIterator);\n\t}\n\t// ---------------------------------------------------------------- prune start\n\t/**\n\t * 清理实现<br>\n\t * 子类实现此方法时无需加锁\n\t *\n\t * @return 清理数\n\t */\n\tprotected abstract int pruneCache();\n\t// ---------------------------------------------------------------- prune end\n\n\t// ---------------------------------------------------------------- common start\n\t@Override\n\tpublic int capacity() {\n\t\treturn capacity;\n\t}\n\n\t/**\n\t * @return 默认缓存失效时长。<br>\n\t * 每个对象可以单独设置失效时长\n\t */\n\t@Override\n\tpublic long timeout() {\n\t\treturn timeout;\n\t}\n\n\t/**\n\t * 只有设置公共缓存失效时长或每个对象单独的失效时长时清理可用\n\t *\n\t * @return 过期对象清理是否可用，内部使用\n\t */\n\tprotected boolean isPruneExpiredActive() {\n\t\treturn (timeout != 0) || existCustomTimeout;\n\t}\n\n\t@Override\n\tpublic boolean isFull() {\n\t\treturn (capacity > 0) && (cacheMap.size() >= capacity);\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn cacheMap.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn cacheMap.isEmpty();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.cacheMap.toString();\n\t}\n\t// ---------------------------------------------------------------- common end\n\n\t/**\n\t * 设置监听\n\t *\n\t * @param listener 监听\n\t * @return this\n\t * @since 5.5.2\n\t */\n\t@Override\n\tpublic AbstractCache<K, V> setListener(CacheListener<K, V> listener) {\n\t\tthis.listener = listener;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回所有键\n\t *\n\t * @return 所有键\n\t * @since 5.5.9\n\t */\n\tpublic Set<K> keySet(){\n\t\treturn this.cacheMap.keySet().stream().map(Mutable::get).collect(Collectors.toSet());\n\t}\n\n\t/**\n\t * 对象移除回调。默认无动作<br>\n\t * 子类可重写此方法用于监听移除事件，如果重写，listener将无效\n\t *\n\t * @param key          键\n\t * @param cachedObject 被缓存的对象\n\t */\n\tprotected void onRemove(K key, V cachedObject) {\n\t\tfinal CacheListener<K, V> listener = this.listener;\n\t\tif (null != listener) {\n\t\t\tlistener.onRemove(key, cachedObject);\n\t\t}\n\t}\n\n\t/**\n\t * 移除key对应的对象，不加锁\n\t *\n\t * @param key           键\n\t * @return 移除的对象，无返回null\n\t */\n\tprotected CacheObj<K, V> removeWithoutLock(K key) {\n\t\treturn cacheMap.remove(MutableObj.of(key));\n\t}\n\n\t/**\n\t * 获取所有{@link CacheObj}值的{@link Iterator}形式\n\t * @return {@link Iterator}\n\t * @since 5.8.0\n\t */\n\tprotected Iterator<CacheObj<K, V>> cacheObjIter(){\n\t\treturn this.cacheMap.values().iterator();\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.core.date.DateUtil;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.concurrent.atomic.AtomicLong;\n\n/**\n * 缓存对象\n *\n * @param <K> Key类型\n * @param <V> Value类型\n * @author Looly\n */\npublic class CacheObj<K, V> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected final K key;\n\tprotected final V obj;\n\n\t/**\n\t * 上次访问时间\n\t */\n\tprotected volatile long lastAccess;\n\t/**\n\t * 访问次数\n\t */\n\tprotected AtomicLong accessCount = new AtomicLong();\n\t/**\n\t * 对象存活时长，0表示永久存活\n\t */\n\tprotected final long ttl;\n\n\t/**\n\t * 构造\n\t *\n\t * @param key 键\n\t * @param obj 值\n\t * @param ttl 超时时长\n\t */\n\tprotected CacheObj(K key, V obj, long ttl) {\n\t\tthis.key = key;\n\t\tthis.obj = obj;\n\t\tthis.ttl = ttl;\n\t\tthis.lastAccess = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 获取键\n\t *\n\t * @return 键\n\t * @since 4.0.10\n\t */\n\tpublic K getKey() {\n\t\treturn this.key;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @return 值\n\t * @since 4.0.10\n\t */\n\tpublic V getValue() {\n\t\treturn this.obj;\n\t}\n\n\t/**\n\t * 获取对象存活时长，即超时总时长，0表示无限\n\t *\n\t * @return 对象存活时长\n\t * @since 5.7.17\n\t */\n\tpublic long getTtl() {\n\t\treturn this.ttl;\n\t}\n\n\t/**\n\t * 获取过期时间，返回{@code null}表示永不过期\n\t *\n\t * @return 此对象的过期时间，返回{@code null}表示永不过期\n\t * @since 5.7.17\n\t */\n\tpublic Date getExpiredTime(){\n\t\tif(this.ttl > 0){\n\t\t\treturn DateUtil.date(this.lastAccess + this.ttl);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取上次访问时间\n\t *\n\t * @return 上次访问时间\n\t * @since 5.7.17\n\t */\n\tpublic long getLastAccess() {\n\t\treturn this.lastAccess;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CacheObj [key=\" + key + \", obj=\" + obj + \", lastAccess=\" + lastAccess + \", accessCount=\" + accessCount + \", ttl=\" + ttl + \"]\";\n\t}\n\n\t/**\n\t * 判断是否过期\n\t *\n\t * @return 是否过期\n\t */\n\tprotected boolean isExpired() {\n\t\tif (this.ttl > 0) {\n\t\t\t// 此处不考虑时间回拨\n\t\t\treturn (System.currentTimeMillis() - this.lastAccess) > this.ttl;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @param isUpdateLastAccess 是否更新最后访问时间\n\t * @return 获得对象\n\t * @since 4.0.10\n\t */\n\tprotected V get(boolean isUpdateLastAccess) {\n\t\tif (isUpdateLastAccess) {\n\t\t\tlastAccess = System.currentTimeMillis();\n\t\t}\n\t\taccessCount.getAndIncrement();\n\t\treturn this.obj;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java",
    "content": "package cn.hutool.cache.impl;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/**\n * {@link cn.hutool.cache.impl.AbstractCache} 的CacheObj迭代器.\n *\n * @author looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 4.0.10\n */\npublic class CacheObjIterator<K, V> implements Iterator<CacheObj<K, V>>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Iterator<CacheObj<K, V>> iterator;\n\tprivate CacheObj<K, V> nextValue;\n\n\t/**\n\t * 构造\n\t *\n\t * @param iterator 原{@link Iterator}\n\t */\n\tCacheObjIterator(Iterator<CacheObj<K, V>> iterator) {\n\t\tthis.iterator = iterator;\n\t\tnextValue();\n\t}\n\n\t/**\n\t * @return 是否有下一个值\n\t */\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn nextValue != null;\n\t}\n\n\t/**\n\t * @return 下一个值\n\t */\n\t@Override\n\tpublic CacheObj<K, V> next() {\n\t\tif (false == hasNext()) {\n\t\t\tthrow new NoSuchElementException();\n\t\t}\n\t\tfinal CacheObj<K, V> cachedObject = nextValue;\n\t\tnextValue();\n\t\treturn cachedObject;\n\t}\n\n\t/**\n\t * 从缓存中移除没有过期的当前值，此方法不支持\n\t */\n\t@Override\n\tpublic void remove() {\n\t\tthrow new UnsupportedOperationException(\"Cache values Iterator is not support to modify.\");\n\t}\n\n\t/**\n\t * 下一个值，当不存在则下一个值为null\n\t */\n\tprivate void nextValue() {\n\t\twhile (iterator.hasNext()) {\n\t\t\tnextValue = iterator.next();\n\t\t\tif (nextValue.isExpired() == false) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tnextValue = null;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java",
    "content": "package cn.hutool.cache.impl;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\n\n/**\n *  {@link cn.hutool.cache.impl.AbstractCache} 的值迭代器.\n * @author looly\n *\n * @param <V> 迭代对象类型\n */\npublic class CacheValuesIterator<V> implements Iterator<V>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final CacheObjIterator<?, V> cacheObjIter;\n\n\t/**\n\t * 构造\n\t * @param iterator 原{@link CacheObjIterator}\n\t */\n\tCacheValuesIterator(CacheObjIterator<?, V> iterator) {\n\t\tthis.cacheObjIter = iterator;\n\t}\n\n\t/**\n\t * @return 是否有下一个值\n\t */\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn this.cacheObjIter.hasNext();\n\t}\n\n\t/**\n\t * @return 下一个值\n\t */\n\t@Override\n\tpublic V next() {\n\t\treturn cacheObjIter.next().getValue();\n\t}\n\n\t/**\n\t * 从缓存中移除没有过期的当前值，不支持此方法\n\t */\n\t@Override\n\tpublic void remove() {\n\t\tcacheObjIter.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\n\n/**\n * FIFO(first in first out) 先进先出缓存.\n *\n * <p>\n * 元素不停的加入缓存直到缓存满为止，当缓存满时，清理过期缓存对象，清理后依旧满则删除先入的缓存（链表首部对象）<br>\n * 优点：简单快速 <br>\n * 缺点：不灵活，不能保证最常用的对象总是被保留\n * </p>\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n */\npublic class FIFOCache<K, V> extends ReentrantCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造，默认对象不过期\n\t *\n\t * @param capacity 容量\n\t */\n\tpublic FIFOCache(int capacity) {\n\t\tthis(capacity, 0);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 容量\n\t * @param timeout  过期时长\n\t */\n\tpublic FIFOCache(int capacity, long timeout) {\n\t\tthis.capacity = capacity;\n\t\tthis.timeout = timeout;\n\t\tcacheMap = new LinkedHashMap<>(capacity + 1, 1.0f, false);\n\t}\n\n\t/**\n\t * 先进先出的清理策略<br>\n\t * 先遍历缓存清理过期的缓存对象，如果清理后还是满的，则删除第一个缓存对象\n\t */\n\t@Override\n\tprotected int pruneCache() {\n\t\tint count = 0;\n\t\tCacheObj<K, V> first = null;\n\n\t\t// 清理过期对象并找出链表头部元素（先入元素）\n\t\tfinal Iterator<CacheObj<K, V>> values = cacheObjIter();\n\t\tif (isPruneExpiredActive()) {\n\t\t\t// 清理过期对象并找出链表头部元素（先入元素）\n\t\t\twhile (values.hasNext()) {\n\t\t\t\tCacheObj<K, V> co = values.next();\n\t\t\t\tif (co.isExpired()) {\n\t\t\t\t\tvalues.remove();\n\t\t\t\t\tonRemove(co.key, co.obj);\n\t\t\t\t\tcount++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (first == null) {\n\t\t\t\t\tfirst = co;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfirst = values.hasNext() ? values.next() : null;\n\t\t}\n\n\t\t// 清理结束后依旧是满的，则删除第一个被缓存的对象\n\t\tif (isFull() && null != first) {\n\t\t\tremoveWithoutLock(first.key);\n\t\t\tonRemove(first.key, first.obj);\n\t\t\tcount++;\n\t\t}\n\t\treturn count;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport java.util.HashMap;\nimport java.util.Iterator;\n\n/**\n * LFU(least frequently used) 最少使用率缓存<br>\n * 根据使用次数来判定对象是否被持续缓存<br>\n * 使用率是通过访问次数计算的。<br>\n * 当缓存满时清理过期对象。<br>\n * 清理后依旧满的情况下清除最少访问（访问计数最小）的对象并将其他对象的访问数减去这个最小访问数，以便新对象进入后可以公平计数。\n *\n * @author Looly,jodd\n *\n * @param <K> 键类型\n * @param <V> 值类型\n */\npublic class LFUCache<K, V> extends ReentrantCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 容量\n\t */\n\tpublic LFUCache(int capacity) {\n\t\tthis(capacity, 0);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 容量\n\t * @param timeout 过期时长\n\t */\n\tpublic LFUCache(int capacity, long timeout) {\n\t\tif(Integer.MAX_VALUE == capacity) {\n\t\t\tcapacity -= 1;\n\t\t}\n\n\t\tthis.capacity = capacity;\n\t\tthis.timeout = timeout;\n\t\tcacheMap = new HashMap<>(capacity + 1, 1.0f);\n\t}\n\n\t// ---------------------------------------------------------------- prune\n\n\t/**\n\t * 清理过期对象。<br>\n\t * 清理后依旧满的情况下清除最少访问（访问计数最小）的对象并将其他对象的访问数减去这个最小访问数，以便新对象进入后可以公平计数。\n\t *\n\t * @return 清理个数\n\t */\n\t@Override\n\tprotected int pruneCache() {\n\t\tint count = 0;\n\t\tCacheObj<K, V> comin = null;\n\n\t\t// 清理过期对象并找出访问最少的对象\n\t\tIterator<CacheObj<K, V>> values = cacheObjIter();\n\t\tCacheObj<K, V> co;\n\t\twhile (values.hasNext()) {\n\t\t\tco = values.next();\n\t\t\tif (co.isExpired() == true) {\n\t\t\t\tvalues.remove();\n\t\t\t\tonRemove(co.key, co.obj);\n\t\t\t\tcount++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t//找出访问最少的对象\n\t\t\tif (comin == null || co.accessCount.get() < comin.accessCount.get()) {\n\t\t\t\tcomin = co;\n\t\t\t}\n\t\t}\n\n\t\t// 减少所有对象访问量，并清除减少后为0的访问对象\n\t\tif (isFull() && comin != null) {\n\t\t\tlong minAccessCount = comin.accessCount.get();\n\n\t\t\tvalues = cacheObjIter();\n\t\t\tCacheObj<K, V> co1;\n\t\t\twhile (values.hasNext()) {\n\t\t\t\tco1 = values.next();\n\t\t\t\tif (co1.accessCount.addAndGet(-minAccessCount) <= 0) {\n\t\t\t\t\tvalues.remove();\n\t\t\t\t\tonRemove(co1.key, co1.obj);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn count;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.map.FixedLinkedHashMap;\n\nimport java.util.Iterator;\n\n/**\n * LRU (least recently used)最近最久未使用缓存<br>\n * 根据使用时间来判定对象是否被持续缓存<br>\n * 当对象被访问时放入缓存，当缓存满了，最久未被使用的对象将被移除。<br>\n * 此缓存基于LinkedHashMap，因此当被缓存的对象每被访问一次，这个对象的key就到链表头部。<br>\n * 这个算法简单并且非常快，他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。<br>\n * 缺点是当缓存满时，不能被很快的访问。\n * @author Looly,jodd\n *\n * @param <K> 键类型\n * @param <V> 值类型\n */\npublic class LRUCache<K, V> extends ReentrantCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造<br>\n\t * 默认无超时\n\t * @param capacity 容量\n\t */\n\tpublic LRUCache(int capacity) {\n\t\tthis(capacity, 0);\n\t}\n\n\t/**\n\t * 构造\n\t * @param capacity 容量\n\t * @param timeout 默认超时时间，单位：毫秒\n\t */\n\tpublic LRUCache(int capacity, long timeout) {\n\t\tif(Integer.MAX_VALUE == capacity) {\n\t\t\tcapacity -= 1;\n\t\t}\n\n\t\tthis.capacity = capacity;\n\t\tthis.timeout = timeout;\n\n\t\t//链表key按照访问顺序排序，调用get方法后，会将这次访问的元素移至头部\n\t\tfinal FixedLinkedHashMap<Mutable<K>, CacheObj<K, V>> fixedLinkedHashMap = new FixedLinkedHashMap<>(capacity);\n\t\tfixedLinkedHashMap.setRemoveListener(entry -> {\n\t\t\tif(null != listener){\n\t\t\t\tlistener.onRemove(entry.getKey().get(), entry.getValue().getValue());\n\t\t\t}\n\t\t});\n\t\tcacheMap = fixedLinkedHashMap;\n\t}\n\n\t// ---------------------------------------------------------------- prune\n\n\t/**\n\t * 只清理超时对象，LRU的实现会交给{@code LinkedHashMap}\n\t */\n\t@Override\n\tprotected int pruneCache() {\n\t\tif (isPruneExpiredActive() == false) {\n\t\t\treturn 0;\n\t\t}\n\t\tint count = 0;\n\t\tIterator<CacheObj<K, V>> values = cacheObjIter();\n\t\tCacheObj<K, V> co;\n\t\twhile (values.hasNext()) {\n\t\t\tco = values.next();\n\t\t\tif (co.isExpired()) {\n\t\t\t\tvalues.remove();\n\t\t\t\tonRemove(co.key, co.obj);\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.func.Func0;\n\nimport java.util.Iterator;\n\n/**\n * 无缓存实现，用于快速关闭缓存\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly,jodd\n */\npublic class NoCache<K, V> implements Cache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tpublic int capacity() {\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic long timeout() {\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic void put(K key, V object) {\n\t\t// 跳过\n\t}\n\n\t@Override\n\tpublic void put(K key, V object, long timeout) {\n\t\t// 跳过\n\t}\n\n\t@Override\n\tpublic boolean containsKey(K key) {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic V get(K key) {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess) {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic V get(K key, Func0<V> supplier) {\n\t\treturn get(key, true, supplier);\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess, Func0<V> supplier) {\n\t\treturn get(key, isUpdateLastAccess, 0, supplier);\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess, long timeout, Func0<V> supplier) {\n\t\ttry {\n\t\t\treturn (null == supplier) ? null : supplier.call();\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Iterator<V> iterator() {\n\t\treturn new Iterator<V>() {\n\t\t\t@Override\n\t\t\tpublic boolean hasNext() {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic V next() {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\t}\n\n\t@Override\n\tpublic Iterator<CacheObj<K, V>> cacheObjIterator() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic int prune() {\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic boolean isFull() {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic void remove(K key) {\n\t\t// 跳过\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\t// 跳过\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.core.collection.CopiedIter;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.lang.mutable.Mutable;\n\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Set;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * 使用{@link ReentrantLock}保护的缓存，读写都使用悲观锁完成，主要避免某些Map无法使用读写锁的问题<br>\n * 例如使用了LinkedHashMap的缓存，由于get方法也会改变Map的结构，因此读写必须加互斥锁\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 5.7.15\n */\npublic abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 一些特殊缓存，例如使用了LinkedHashMap的缓存，由于get方法也会改变Map的结构，导致无法使用读写锁\n\t// TODO 最优的解决方案是使用Guava的ConcurrentLinkedHashMap，此处使用简化的互斥锁\n\tprotected final ReentrantLock lock = new ReentrantLock();\n\n\t@Override\n\tpublic void put(K key, V object, long timeout) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tputWithoutLock(key, object, timeout);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean containsKey(K key) {\n\t\treturn null != getOrRemoveExpired(key, false, false);\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess) {\n\t\treturn getOrRemoveExpired(key, isUpdateLastAccess, true);\n\t}\n\n\t@Override\n\tpublic V get(final K key, final boolean isUpdateLastAccess, final long timeout, final Func0<V> valueFactory) {\n\t\tV v = get(key, isUpdateLastAccess);\n\n\t\t// 对象不存在，则加锁创建\n\t\tif (null == v && null != valueFactory) {\n\t\t\t// 按照pr#1385提议，使用key锁可以避免对象创建等待问题，但是会带来循环锁问题，见：issue#4022\n\t\t\t// 因此此处依旧采用全局锁，在对象创建过程中，全局等待，避免循环锁依赖\n\t\t\t// 这样避免了循环锁，但是会存在一个缺点，即对象创建过程中，其它线程无法获得锁，从而无法使用缓存，因此需要考虑对象创建的耗时问题\n\t\t\tlock.lock();\n\t\t\ttry {\n\t\t\t\t// 双重检查锁，防止在竞争锁的过程中已经有其它线程写入\n\t\t\t\tfinal CacheObj<K, V> co = getWithoutLock(key);\n\t\t\t\tif (null == co) {\n\t\t\t\t\t// supplier的创建是一个耗时过程，此处创建与全局锁无关，而与key锁相关，这样就保证每个key只创建一个value，且互斥\n\t\t\t\t\tv = valueFactory.callWithRuntimeException();\n\t\t\t\t\tputWithoutLock(key, v, timeout);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tlock.unlock();\n\t\t\t}\n\t\t}\n\t\treturn v;\n\t}\n\n\t@Override\n\tpublic Iterator<CacheObj<K, V>> cacheObjIterator() {\n\t\tCopiedIter<CacheObj<K, V>> copiedIterator;\n\t\tlock.lock();\n\t\ttry {\n\t\t\tcopiedIterator = CopiedIter.copyOf(cacheObjIter());\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn new CacheObjIterator<>(copiedIterator);\n\t}\n\n\t@Override\n\tpublic final int prune() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\treturn pruneCache();\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void remove(K key) {\n\t\tlock.lock();\n\t\tCacheObj<K, V> co;\n\t\ttry {\n\t\t\tco = removeWithoutLock(key);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\tif (null != co) {\n\t\t\tonRemove(co.key, co.obj);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\t// 获取所有键的副本\n\t\t\tSet<Mutable<K>> keys = new HashSet<>(cacheMap.keySet());\n\t\t\tfor (Mutable<K> key : keys) {\n\t\t\t\tCacheObj<K, V> co = removeWithoutLock(key.get());\n\t\t\t\tif (co != null) {\n\t\t\t\t\tonRemove(co.key, co.obj); // 触发资源释放\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\treturn super.toString();\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获得值或清除过期值\n\t * @param key 键\n\t * @param isUpdateLastAccess 是否更新最后访问时间\n\t * @param isUpdateCount 是否更新计数器\n\t * @return 值或null\n\t */\n\tprivate V getOrRemoveExpired(final K key, final boolean isUpdateLastAccess, final boolean isUpdateCount) {\n\t\tCacheObj<K, V> co;\n\t\tlock.lock();\n\t\ttry {\n\t\t\tco = getWithoutLock(key);\n\t\t\tif(null != co && co.isExpired()){\n\t\t\t\t//过期移除\n\t\t\t\tremoveWithoutLock(key);\n\t\t\t\tonRemove(co.key, co.obj);\n\t\t\t\tco = null;\n\t\t\t}\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\n\t\t// 未命中\n\t\tif (null == co) {\n\t\t\tif(isUpdateCount){\n\t\t\t\tmissCount.increment();\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tif(isUpdateCount){\n\t\t\thitCount.increment();\n\t\t}\n\t\treturn co.get(isUpdateLastAccess);\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.core.collection.CopiedIter;\n\nimport java.util.Iterator;\nimport java.util.concurrent.locks.StampedLock;\n\n/**\n * 使用{@link StampedLock}保护的缓存，使用读写乐观锁\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 5.7.15\n * @deprecated Map使用StampedLock可能造成数据不一致甚至Map循环调用，此缓存废弃\n */\n@Deprecated\npublic abstract class StampedCache<K, V> extends AbstractCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 乐观锁，此处使用乐观锁解决读多写少的场景\n\t// get时乐观读，再检查是否修改，修改则转入悲观读重新读一遍，可以有效解决在写时阻塞大量读操作的情况。\n\t// see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html\n\tprotected final StampedLock lock = new StampedLock();\n\n\t@Override\n\tpublic void put(K key, V object, long timeout) {\n\t\tfinal long stamp = lock.writeLock();\n\t\ttry {\n\t\t\tputWithoutLock(key, object, timeout);\n\t\t} finally {\n\t\t\tlock.unlockWrite(stamp);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean containsKey(K key) {\n\t\treturn null != get(key, false, false);\n\t}\n\n\t@Override\n\tpublic V get(K key, boolean isUpdateLastAccess) {\n\t\treturn get(key, isUpdateLastAccess, true);\n\t}\n\n\t@Override\n\tpublic Iterator<CacheObj<K, V>> cacheObjIterator() {\n\t\tCopiedIter<CacheObj<K, V>> copiedIterator;\n\t\tfinal long stamp = lock.readLock();\n\t\ttry {\n\t\t\tcopiedIterator = CopiedIter.copyOf(cacheObjIter());\n\t\t} finally {\n\t\t\tlock.unlockRead(stamp);\n\t\t}\n\t\treturn new CacheObjIterator<>(copiedIterator);\n\t}\n\n\t@Override\n\tpublic final int prune() {\n\t\tfinal long stamp = lock.writeLock();\n\t\ttry {\n\t\t\treturn pruneCache();\n\t\t} finally {\n\t\t\tlock.unlockWrite(stamp);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void remove(K key) {\n\t\tfinal long stamp = lock.writeLock();\n\t\tCacheObj<K, V> co;\n\t\ttry {\n\t\t\tco = removeWithoutLock(key);\n\t\t} finally {\n\t\t\tlock.unlockWrite(stamp);\n\t\t}\n\t\tif (null != co) {\n\t\t\tonRemove(co.key, co.obj);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tfinal long stamp = lock.writeLock();\n\t\ttry {\n\t\t\tcacheMap.clear();\n\t\t} finally {\n\t\t\tlock.unlockWrite(stamp);\n\t\t}\n\t}\n\n\t/**\n\t * 获取值，使用乐观锁，但是此方法可能导致读取脏数据，但对于缓存业务可容忍。情况如下：\n\t * <pre>\n\t *     1. 读取时无写入，不冲突，直接获取值\n\t *     2. 读取时无写入，但是乐观读时触发了并发异常，此时获取同步锁，获取新值\n\t *     4. 读取时有写入，此时获取同步锁，获取新值\n\t * </pre>\n\t *\n\t * @param key                键\n\t * @param isUpdateLastAccess 是否更新最后修改时间\n\t * @param isUpdateCount      是否更新命中数，get时更新，contains时不更新\n\t * @return 值或null\n\t */\n\tprivate V get(K key, boolean isUpdateLastAccess, boolean isUpdateCount) {\n\t\t// 尝试读取缓存，使用乐观读锁\n\t\tCacheObj<K, V> co = null;\n\t\tlong stamp = lock.tryOptimisticRead();\n\t\tboolean isReadError = true;\n\t\tif(lock.validate(stamp)){\n\t\t\ttry{\n\t\t\t\t// 乐观读，可能读取脏数据，在缓存中可容忍，分两种情况\n\t\t\t\t// 1. 读取时无线程写入\n\t\t\t\t// 2. 读取时有线程写入，导致数据不一致，此时读取未更新的缓存值\n\t\t\t\tco = getWithoutLock(key);\n\t\t\t\tisReadError = false;\n\t\t\t} catch (final Exception ignore){\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\n\t\tif(isReadError){\n\t\t\t// 转换为悲观读\n\t\t\t// 原因可能为无锁读时触发并发异常，或者锁被占（正在写）\n\t\t\tstamp = lock.readLock();\n\t\t\ttry {\n\t\t\t\tco = getWithoutLock(key);\n\t\t\t} finally {\n\t\t\t\tlock.unlockRead(stamp);\n\t\t\t}\n\t\t}\n\n\t\t// 未命中\n\t\tif (null == co) {\n\t\t\tif (isUpdateCount) {\n\t\t\t\tmissCount.increment();\n\t\t\t}\n\t\t\treturn null;\n\t\t} else if (false == co.isExpired()) {\n\t\t\tif (isUpdateCount) {\n\t\t\t\thitCount.increment();\n\t\t\t}\n\t\t\treturn co.get(isUpdateLastAccess);\n\t\t}\n\n\t\t// 悲观锁，二次检查\n\t\treturn getOrRemoveExpired(key, isUpdateCount);\n\t}\n\n\t/**\n\t * 同步获取值，如果过期则移除之\n\t *\n\t * @param key           键\n\t * @param isUpdateCount 是否更新命中数，get时更新，contains时不更新\n\t * @return 有效值或null\n\t */\n\tprivate V getOrRemoveExpired(K key, boolean isUpdateCount) {\n\t\tfinal long stamp = lock.writeLock();\n\t\tCacheObj<K, V> co;\n\t\ttry {\n\t\t\tco = getWithoutLock(key);\n\t\t\tif (null == co) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (false == co.isExpired()) {\n\t\t\t\t// 首先尝试获取值，如果值存在且有效，返回之\n\t\t\t\tif (isUpdateCount) {\n\t\t\t\t\thitCount.increment();\n\t\t\t\t}\n\t\t\t\treturn co.getValue();\n\t\t\t}\n\n\t\t\t// 无效移除\n\t\t\tco = removeWithoutLock(key);\n\t\t} finally {\n\t\t\tlock.unlockWrite(stamp);\n\t\t}\n\t\tif (null != co) {\n\t\t\tonRemove(co.key, co.obj);\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.cache.GlobalPruneTimer;\nimport cn.hutool.core.lang.mutable.Mutable;\n\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.concurrent.ScheduledFuture;\n\n/**\n * 定时缓存<br>\n * 此缓存没有容量限制，对象只有在过期后才会被移除\n *\n * @author Looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n */\npublic class TimedCache<K, V> extends ReentrantCache<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 正在执行的定时任务 */\n\tprivate ScheduledFuture<?> pruneJobFuture;\n\n\t/**\n\t * 构造\n\t *\n\t * @param timeout 超时（过期）时长，单位毫秒\n\t */\n\tpublic TimedCache(long timeout) {\n\t\tthis(timeout, new HashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param timeout 过期时长\n\t * @param map 存储缓存对象的map\n\t */\n\tpublic TimedCache(long timeout, Map<Mutable<K>, CacheObj<K, V>> map) {\n\t\tthis.capacity = 0;\n\t\tthis.timeout = timeout;\n\t\tthis.cacheMap = map;\n\t}\n\n\t// ---------------------------------------------------------------- prune\n\t/**\n\t * 清理过期对象\n\t *\n\t * @return 清理数\n\t */\n\t@Override\n\tprotected int pruneCache() {\n\t\tint count = 0;\n\t\tfinal Iterator<CacheObj<K, V>> values = cacheObjIter();\n\t\tCacheObj<K, V> co;\n\t\twhile (values.hasNext()) {\n\t\t\tco = values.next();\n\t\t\tif (co.isExpired()) {\n\t\t\t\tvalues.remove();\n\t\t\t\tonRemove(co.key, co.obj);\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t// ---------------------------------------------------------------- auto prune\n\t/**\n\t * 定时清理\n\t *\n\t * @param delay 间隔时长，单位毫秒\n\t */\n\tpublic void schedulePrune(long delay) {\n\t\tthis.pruneJobFuture = GlobalPruneTimer.INSTANCE.schedule(this::prune, delay);\n\t}\n\n\t/**\n\t * 取消定时清理\n\t */\n\tpublic void cancelPruneSchedule() {\n\t\tif (null != pruneJobFuture) {\n\t\t\tpruneJobFuture.cancel(true);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java",
    "content": "package cn.hutool.cache.impl;\n\nimport cn.hutool.cache.CacheListener;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.ref.Ref;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\n/**\n * 弱引用缓存<br>\n * 对于一个给定的键，其映射的存在并不阻止垃圾回收器对该键的丢弃，这就使该键成为可终止的，被终止，然后被回收。<br>\n * 丢弃某个键时，其条目从映射中有效地移除。<br>\n *\n * @author Looly\n *\n * @param <K> 键\n * @param <V> 值\n * @author looly\n * @since 3.0.7\n */\npublic class WeakCache<K, V> extends TimedCache<K, V>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t * @param timeout 超时时常，单位毫秒，-1或0表示无限制\n\t */\n\tpublic WeakCache(long timeout) {\n\t\tsuper(timeout, new WeakKeyValueConcurrentMap<>());\n\t}\n\n\t@Override\n\tpublic WeakCache<K, V> setListener(CacheListener<K, V> listener) {\n\t\tsuper.setListener(listener);\n\n\t\tfinal WeakKeyValueConcurrentMap<Mutable<K>, CacheObj<K, V>> map = (WeakKeyValueConcurrentMap<Mutable<K>, CacheObj<K, V>>) this.cacheMap;\n\t\t// WeakKey回收之后，key对应的值已经是null了，因此此处的key也为null\n\t\tmap.setPurgeListener((key, value)-> listener.onRemove(\n\t\t\tOpt.ofNullable(key).map(Ref::get).map(Mutable::get).get(),\n\t\t\tOpt.ofNullable(value).map(Ref::get).map(CacheObj::getValue).get()));\n\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java",
    "content": "/**\n * 提供各种缓存实现\n *\n * @author looly\n *\n */\npackage cn.hutool.cache.impl;"
  },
  {
    "path": "hutool-cache/src/main/java/cn/hutool/cache/package-info.java",
    "content": "/**\n * 提供简易的缓存实现，此模块参考了jodd工具中的Cache模块\n *\n * @author looly\n *\n */\npackage cn.hutool.cache;"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/CacheConcurrentTest.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.FIFOCache;\nimport cn.hutool.cache.impl.LRUCache;\nimport cn.hutool.cache.impl.WeakCache;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ConcurrencyTester;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * 缓存单元测试\n *\n * @author looly\n *\n */\npublic class CacheConcurrentTest {\n\n\t@Test\n\t@Disabled\n\tpublic void fifoCacheTest() {\n\t\tint threadCount = 4000;\n\t\tfinal Cache<String, String> cache = new FIFOCache<>(3);\n\n\t\t// 由于缓存容量只有3，当加入第四个元素的时候，根据FIFO规则，最先放入的对象将被移除\n\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tThreadUtil.execute(() -> {\n\t\t\t\tcache.put(\"key1\", \"value1\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key2\", \"value2\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key3\", \"value3\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key4\", \"value4\", System.currentTimeMillis() * 3);\n\t\t\t\tThreadUtil.sleep(1000);\n\t\t\t\tcache.put(\"key5\", \"value5\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key6\", \"value6\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key7\", \"value7\", System.currentTimeMillis() * 3);\n\t\t\t\tcache.put(\"key8\", \"value8\", System.currentTimeMillis() * 3);\n\t\t\t\tConsole.log(\"put all\");\n\t\t\t});\n\t\t}\n\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tThreadUtil.execute(() -> show(cache));\n\t\t}\n\n\t\tSystem.out.println(\"==============================\");\n\t\tThreadUtil.sleep(10000);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lruCacheTest() {\n\t\tint threadCount = 40000;\n\t\tfinal Cache<String, String> cache = new LRUCache<>(1000);\n\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tfinal int index = i;\n\t\t\tThreadUtil.execute(() -> {\n\t\t\t\tcache.put(\"key1\"+ index, \"value1\");\n\t\t\t\tcache.put(\"key2\"+ index, \"value2\", System.currentTimeMillis() * 3);\n\n\t\t\t\tint size = cache.size();\n\t\t\t\tint capacity = cache.capacity();\n\t\t\t\tif(size > capacity) {\n\t\t\t\t\tConsole.log(\"{} {}\", size, capacity);\n\t\t\t\t}\n\t\t\t\tThreadUtil.sleep(1000);\n\t\t\t\tsize = cache.size();\n\t\t\t\tcapacity = cache.capacity();\n\t\t\t\tif(size > capacity) {\n\t\t\t\t\tConsole.log(\"## {} {}\", size, capacity);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tThreadUtil.sleep(5000);\n\t}\n\n\tprivate void show(Cache<String, String> cache) {\n\n\t\tfor (Object tt : cache) {\n\t\t\tConsole.log(tt);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void effectiveTest() {\n\t\t// 模拟耗时操作消耗时间\n\t\tint delay = 2000;\n\t\tAtomicInteger ai = new AtomicInteger(0);\n\t\tWeakCache<Integer, Integer> weakCache = new WeakCache<>(60 * 1000);\n\t\tConcurrencyTester concurrencyTester = ThreadUtil.concurrencyTest(32, () -> {\n\t\t\tint i = ai.incrementAndGet() % 4;\n\t\t\tweakCache.get(i, () -> {\n\t\t\t\tThreadUtil.sleep(delay);\n\t\t\t\treturn i;\n\t\t\t});\n\t\t});\n\t\tlong interval = concurrencyTester.getInterval();\n\t\t// 总耗时应与单次操作耗时在同一个数量级\n\t\tassertTrue(interval < delay * 2);\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/CacheTest.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.TimedCache;\nimport cn.hutool.core.date.DateUnit;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 缓存测试用例\n *\n * @author Looly\n */\npublic class CacheTest {\n\n\t@Test\n\tpublic void fifoCacheTest(){\n\t\tCache<String,String> fifoCache = CacheUtil.newFIFOCache(3);\n\t\tfifoCache.setListener((key, value)->{\n\t\t\t// 监听测试，此测试中只有key1被移除，测试是否监听成功\n\t\t\tassertEquals(\"key1\", key);\n\t\t\tassertEquals(\"value1\", value);\n\t\t});\n\n\t\tfifoCache.put(\"key1\", \"value1\", DateUnit.SECOND.getMillis() * 3);\n\t\tfifoCache.put(\"key2\", \"value2\", DateUnit.SECOND.getMillis() * 3);\n\t\tfifoCache.put(\"key3\", \"value3\", DateUnit.SECOND.getMillis() * 3);\n\t\tfifoCache.put(\"key4\", \"value4\", DateUnit.SECOND.getMillis() * 3);\n\n\t\t//由于缓存容量只有3，当加入第四个元素的时候，根据FIFO规则，最先放入的对象将被移除\n\t\tString value1 = fifoCache.get(\"key1\");\n\t\tassertNull(value1);\n\t}\n\n\t@Test\n\tpublic void fifoCacheCapacityTest(){\n\t\tCache<String,String> fifoCache = CacheUtil.newFIFOCache(100);\n\t\tfor (int i = 0; i < RandomUtil.randomInt(100, 1000); i++) {\n\t\t\tfifoCache.put(\"key\" + i, \"value\" + i);\n\t\t}\n\t\tassertEquals(100, fifoCache.size());\n\t}\n\n\t@Test\n\tpublic void lfuCacheTest(){\n\t\tCache<String, String> lfuCache = CacheUtil.newLFUCache(3);\n\t\tlfuCache.put(\"key1\", \"value1\", DateUnit.SECOND.getMillis() * 3);\n\t\t//使用次数+1\n\t\tlfuCache.get(\"key1\");\n\t\tlfuCache.put(\"key2\", \"value2\", DateUnit.SECOND.getMillis() * 3);\n\t\tlfuCache.put(\"key3\", \"value3\", DateUnit.SECOND.getMillis() * 3);\n\t\tlfuCache.put(\"key4\", \"value4\", DateUnit.SECOND.getMillis() * 3);\n\n\t\t//由于缓存容量只有3，当加入第四个元素的时候，根据LFU规则，最少使用的将被移除（2,3被移除）\n\t\tString value1 = lfuCache.get(\"key1\");\n\t\tString value2 = lfuCache.get(\"key2\");\n\t\tString value3 = lfuCache.get(\"key3\");\n\t\tassertNotNull(value1);\n\t\tassertNull(value2);\n\t\tassertNull(value3);\n\t}\n\n\t@Test\n\tpublic void lfuCacheTest2(){\n\t\tCache<String, String> lfuCache = CacheUtil.newLFUCache(3);\n\t\tfinal String s = lfuCache.get(null);\n\t\tassertNull(s);\n\t}\n\n\t@Test\n\tpublic void lruCacheTest(){\n\t\tCache<String, String> lruCache = CacheUtil.newLRUCache(3);\n\t\t//通过实例化对象创建\n//\t\tLRUCache<String, String> lruCache = new LRUCache<String, String>(3);\n\t\tlruCache.put(\"key1\", \"value1\", DateUnit.SECOND.getMillis() * 3);\n\t\tlruCache.put(\"key2\", \"value2\", DateUnit.SECOND.getMillis() * 3);\n\t\tlruCache.put(\"key3\", \"value3\", DateUnit.SECOND.getMillis() * 3);\n\t\t//使用时间推近\n\t\tlruCache.get(\"key1\");\n\t\tlruCache.put(\"key4\", \"value4\", DateUnit.SECOND.getMillis() * 3);\n\n\t\tString value1 = lruCache.get(\"key1\");\n\t\tassertNotNull(value1);\n\t\t//由于缓存容量只有3，当加入第四个元素的时候，根据LRU规则，最少使用的将被移除（2被移除）\n\t\tString value2 = lruCache.get(\"key2\");\n\t\tassertNull(value2);\n\t}\n\n\t@Test\n\tpublic void timedCacheTest(){\n\t\tTimedCache<String, String> timedCache = CacheUtil.newTimedCache(4);\n//\t\tTimedCache<String, String> timedCache = new TimedCache<String, String>(DateUnit.SECOND.getMillis() * 3);\n\t\ttimedCache.put(\"key1\", \"value1\", 1);//1毫秒过期\n\t\ttimedCache.put(\"key2\", \"value2\", DateUnit.SECOND.getMillis() * 5);//5秒过期\n\t\ttimedCache.put(\"key3\", \"value3\");//默认过期(4毫秒)\n\t\ttimedCache.put(\"key4\", \"value4\", Long.MAX_VALUE);//永不过期\n\n\t\t//启动定时任务，每5毫秒秒检查一次过期\n\t\ttimedCache.schedulePrune(5);\n\t\t//等待5毫秒\n\t\tThreadUtil.sleep(5);\n\n\t\t//5毫秒后由于value2设置了5毫秒过期，因此只有value2被保留下来\n\t\tString value1 = timedCache.get(\"key1\");\n\t\tassertNull(value1);\n\t\tString value2 = timedCache.get(\"key2\");\n\t\tassertEquals(\"value2\", value2);\n\n\t\t//5毫秒后，由于设置了默认过期，key3只被保留4毫秒，因此为null\n\t\tString value3 = timedCache.get(\"key3\");\n\t\tassertNull(value3);\n\n\t\tString value3Supplier = timedCache.get(\"key3\", () -> \"Default supplier\");\n\t\tassertEquals(\"Default supplier\", value3Supplier);\n\n\t\t// 永不过期\n\t\tString value4 = timedCache.get(\"key4\");\n\t\tassertEquals(\"value4\", value4);\n\n\t\t//取消定时清理\n\t\ttimedCache.cancelPruneSchedule();\n\t}\n\n\n\t/**\n\t * TimedCache的数据过期后不是每次都触发监听器onRemove，而是偶尔触发onRemove\n\t * https://gitee.com/chinabugotech/hutool/issues/IBP752\n\t */\n\t@Test\n\tpublic void whenContainsKeyTimeout_shouldCallOnRemove() {\n\t\tint timeout = 50;\n\t\tfinal TimedCache<Integer, String> ALARM_CACHE = new TimedCache<>(timeout);\n\n\t\tAtomicInteger counter = new AtomicInteger(0);\n\t\tALARM_CACHE.setListener((key, value) -> {\n\t\t\tcounter.incrementAndGet();\n\t\t});\n\n\t\tALARM_CACHE.put(1, \"value1\");\n\n\t\tThreadUtil.sleep(100);\n\n\t\tassertFalse(ALARM_CACHE.containsKey(1));\n\t\tassertEquals(1, counter.get());\n\t}\n\n\t/**\n\t * ReentrantCache类clear()方法、AbstractCache.putWithoutLock方法可能导致资源泄露\n\t * https://github.com/chinabugotech/hutool/issues/3957\n\t */\n\t@Test\n\tpublic void reentrantCache_clear_Method_Test() {\n\t\tfinal AtomicInteger removeCount = new AtomicInteger();\n\t\tfinal Cache<String, String> lruCache = CacheUtil.newLRUCache(4);\n\t\tlruCache.setListener((key, cachedObject) -> removeCount.getAndIncrement());\n\t\tlruCache.put(\"key1\",\"String1\");\n\t\tlruCache.put(\"key2\",\"String2\");\n\t\tlruCache.put(\"key3\",\"String3\");\n\t\tlruCache.put(\"key1\",\"String4\");//key已经存在，原始putWithoutLock方法存在资源泄露\n\t\tlruCache.put(\"key4\",\"String5\");\n\t\tlruCache.clear();//ReentrantCache类clear()方法存在资源泄露\n\t\tAssertions.assertEquals(5, removeCount.get());\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/FileCacheTest.java",
    "content": "package cn.hutool.cache;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.cache.file.LFUFileCache;\n\n/**\n * 文件缓存单元测试\n * @author looly\n *\n */\npublic class FileCacheTest {\n\t@Test\n\tpublic void lfuFileCacheTest() {\n\t\tLFUFileCache cache = new LFUFileCache(1000, 500, 2000);\n\t\tassertNotNull(cache);\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/Issue3618Test.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.FIFOCache;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3618Test {\n\t@Test\n\tpublic void putTest() {\n\t\tFIFOCache<Object, Object> cache = CacheUtil.newFIFOCache(3);\n\t\tcache.put(1, 1);\n\t\tcache.put(2, 1);\n\t\tcache.put(3, 1);\n\n\t\tassertEquals(3, cache.size());\n\n\t\t// issue#3618 对于替换的键值对，不做满队列检查和清除\n\t\tcache.put(3, 2);\n\n\t\tassertEquals(3, cache.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/IssueI8MEIXTest.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.TimedCache;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI8MEIXTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getRemoveTest() {\n\t\tfinal TimedCache<String, String> cache = new TimedCache<>(200);\n\t\tcache.put(\"a\", \"123\");\n\n\t\tThreadUtil.sleep(300);\n\n\t\t// 测试时，在get后的remove前加sleep测试在读取过程中put新值的问题\n\t\tThreadUtil.execute(()->{\n\t\t\tConsole.log(cache.get(\"a\"));\n\t\t});\n\n\t\tThreadUtil.execute(()->{\n\t\t\tcache.put(\"a\", \"456\");\n\t\t});\n\n\t\tThreadUtil.sleep(1000);\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.LRUCache;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 见：https://github.com/chinabugotech/hutool/issues/1895<br>\n * 并发问题测试，在5.7.15前，LRUCache存在并发问题，多线程get后，map结构变更，导致null的位置不确定，\n * 并可能引起死锁。\n */\npublic class LRUCacheTest {\n\n\t@Test\n\t@Disabled\n\tpublic void putTest(){\n\t\t//https://github.com/chinabugotech/hutool/issues/2227\n\t\tfinal LRUCache<String, String> cache = CacheUtil.newLRUCache(100, 10);\n\t\tfor (int i = 0; i < 10000; i++) {\n\t\t\t//ThreadUtil.execute(()-> cache.put(RandomUtil.randomString(5), \"1243\", 10));\n\t\t\tThreadUtil.execute(()-> cache.get(RandomUtil.randomString(5), ()->RandomUtil.randomString(10)));\n\t\t}\n\t\tThreadUtil.sleep(3000);\n\t}\n\n\t@Test\n\tpublic void readWriteTest() throws InterruptedException {\n\t\tfinal LRUCache<Integer, Integer> cache = CacheUtil.newLRUCache(10);\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tcache.put(i, i);\n\t\t}\n\n\t\tfinal CountDownLatch countDownLatch = new CountDownLatch(10);\n\t\t// 10个线程分别读0-9 10000次\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tfinal int finalI = i;\n\t\t\tnew Thread(() -> {\n\t\t\t\tfor (int j = 0; j < 10000; j++) {\n\t\t\t\t\tcache.get(finalI);\n\t\t\t\t}\n\t\t\t\tcountDownLatch.countDown();\n\t\t\t}).start();\n\t\t}\n\t\t// 等待读线程结束\n\t\tcountDownLatch.await();\n\t\t// 按顺序读0-9\n\t\tfinal StringBuilder sb1 = new StringBuilder();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tsb1.append(cache.get(i));\n\t\t}\n\t\tassertEquals(\"0123456789\", sb1.toString());\n\n\t\t// 新加11，此时0最久未使用，应该淘汰0\n\t\tcache.put(11, 11);\n\n\t\tfinal StringBuilder sb2 = new StringBuilder();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tsb2.append(cache.get(i));\n\t\t}\n\t\tassertEquals(\"null123456789\", sb2.toString());\n\t}\n\n\t@Test\n\tpublic void issue2647Test(){\n\t\tfinal AtomicInteger removeCount = new AtomicInteger();\n\n\t\tfinal LRUCache<String, Integer> cache = CacheUtil.newLRUCache(3,1);\n\t\tcache.setListener((key, value) -> {\n\t\t\t// 共移除7次\n\t\t\tremoveCount.incrementAndGet();\n\t\t\t//Console.log(\"Start remove k-v, key:{}, value:{}\", key, value);\n\t\t});\n\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tcache.put(StrUtil.format(\"key-{}\", i), i);\n\t\t}\n\n\t\tassertEquals(7, removeCount.get());\n\t\tassertEquals(3, cache.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-cache/src/test/java/cn/hutool/cache/WeakCacheTest.java",
    "content": "package cn.hutool.cache;\n\nimport cn.hutool.cache.impl.WeakCache;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WeakCacheTest {\n\n\t@Test\n\tpublic void removeTest(){\n\t\tfinal WeakCache<String, String> cache = new WeakCache<>(-1);\n\t\tcache.put(\"abc\", \"123\");\n\t\tcache.put(\"def\", \"456\");\n\n\t\tassertEquals(2, cache.size());\n\n\t\t// 检查被MutableObj包装的key能否正常移除\n\t\tcache.remove(\"abc\");\n\n\t\tassertEquals(1, cache.size());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void removeByGcTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I51O7M\n\t\tWeakCache<String, String> cache = new WeakCache<>(-1);\n\t\tcache.put(\"a\", \"1\");\n\t\tcache.put(\"b\", \"2\");\n\n\t\t// 监听\n\t\tassertEquals(2, cache.size());\n\t\tcache.setListener(Console::log);\n\n\t\t// GC测试\n\t\tint i=0;\n\t\twhile(true){\n\t\t\tif(2 == cache.size()){\n\t\t\t\ti++;\n\t\t\t\tConsole.log(\"Object is alive for {} loops - \", i);\n\t\t\t\tSystem.gc();\n\t\t\t}else{\n\t\t\t\tConsole.log(\"Object has been collected.\");\n\t\t\t\tConsole.log(cache.size());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-captcha</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 验证码工具</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.captcha</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.CodeGenerator;\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * 抽象验证码<br>\n * 抽象验证码实现了验证码字符串的生成、验证，验证码图片的写出<br>\n * 实现类通过实现{@link #createImage(String)} 方法生成图片对象\n *\n * @author looly\n */\npublic abstract class AbstractCaptcha implements ICaptcha {\n\tprivate static final long serialVersionUID = 3180820918087507254L;\n\n\t/**\n\t * 图片的宽度\n\t */\n\tprotected int width;\n\t/**\n\t * 图片的高度\n\t */\n\tprotected int height;\n\t/**\n\t * 验证码干扰元素个数\n\t */\n\tprotected int interfereCount;\n\t/**\n\t * 字体\n\t */\n\tprotected Font font;\n\t/**\n\t * 验证码\n\t */\n\tprotected String code;\n\t/**\n\t * 验证码图片\n\t */\n\tprotected byte[] imageBytes;\n\t/**\n\t * 验证码生成器\n\t */\n\tprotected CodeGenerator generator;\n\t/**\n\t * 背景色\n\t */\n\tprotected Color background = Color.WHITE;\n\t/**\n\t * 文字透明度\n\t */\n\tprotected AlphaComposite textAlpha;\n\tprotected Stroke stroke;\n\n\t/**\n\t * 构造，使用随机验证码生成器生成验证码\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount      字符个数\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic AbstractCaptcha(int width, int height, int codeCount, int interfereCount) {\n\t\tthis(width, height, new RandomGenerator(codeCount), interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic AbstractCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t\tthis.generator = generator;\n\t\tthis.interfereCount = interfereCount;\n\t\t// 字体高度设为验证码高度-2，留边距\n\t\tthis.font = new Font(Font.SANS_SERIF, Font.PLAIN, (int) (this.height * 0.75));\n\t}\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t * @param size \t\t\t 字体的大小 高度的倍数\n\t */\n\tpublic AbstractCaptcha(int width, int height, CodeGenerator generator, int interfereCount, float size) {\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t\tthis.generator = generator;\n\t\tthis.interfereCount = interfereCount;\n\t\t// 字体高度设为验证码高度-2，留边距\n\t\tthis.font = new Font(Font.SANS_SERIF, Font.PLAIN, (int) (this.height * size));\n\t}\n\n\t@Override\n\tpublic void createCode() {\n\t\tgenerateCode();\n\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\tImgUtil.writePng(createImage(this.code), out);\n\t\tthis.imageBytes = out.toByteArray();\n\t}\n\n\t/**\n\t * 生成验证码字符串\n\t *\n\t * @since 3.3.0\n\t */\n\tprotected void generateCode() {\n\t\tthis.code = generator.generate();\n\t}\n\n\t/**\n\t * 根据生成的code创建验证码图片\n\t *\n\t * @param code 验证码\n\t * @return Image\n\t */\n\tprotected abstract Image createImage(String code);\n\n\t@Override\n\tpublic String getCode() {\n\t\tif (null == this.code) {\n\t\t\tcreateCode();\n\t\t}\n\t\treturn this.code;\n\t}\n\n\t@Override\n\tpublic boolean verify(String userInputCode) {\n\t\treturn this.generator.verify(getCode(), userInputCode);\n\t}\n\n\t/**\n\t * 验证码写出到文件\n\t *\n\t * @param path 文件路径\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void write(String path) throws IORuntimeException {\n\t\tthis.write(FileUtil.touch(path));\n\t}\n\n\t/**\n\t * 验证码写出到文件\n\t *\n\t * @param file 文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void write(File file) throws IORuntimeException {\n\t\ttry (OutputStream out = FileUtil.getOutputStream(file)) {\n\t\t\tthis.write(out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void write(OutputStream out) {\n\t\tIoUtil.write(out, false, getImageBytes());\n\t}\n\n\t/**\n\t * 获取图形验证码图片bytes\n\t *\n\t * @return 图形验证码图片bytes\n\t * @since 4.5.17\n\t */\n\tpublic byte[] getImageBytes() {\n\t\tif (null == this.imageBytes) {\n\t\t\tcreateCode();\n\t\t}\n\t\treturn this.imageBytes;\n\t}\n\n\t/**\n\t * 获取验证码图\n\t *\n\t * @return 验证码图\n\t */\n\tpublic BufferedImage getImage() {\n\t\treturn ImgUtil.read(IoUtil.toStream(getImageBytes()));\n\t}\n\n\t/**\n\t * 获得图片的Base64形式\n\t *\n\t * @return 图片的Base64\n\t * @since 3.3.0\n\t */\n\tpublic String getImageBase64() {\n\t\treturn Base64.encode(getImageBytes());\n\t}\n\n\t/**\n\t * 获取图片带文件格式的 Base64\n\t *\n\t * @return 图片带文件格式的 Base64\n\t * @since 5.3.11\n\t */\n\tpublic String getImageBase64Data() {\n\t\treturn URLUtil.getDataUriBase64(\"image/png\", getImageBase64());\n\t}\n\n\t/**\n\t * 自定义字体\n\t *\n\t * @param font 字体\n\t */\n\tpublic void setFont(Font font) {\n\t\tthis.font = font;\n\t}\n\n\t/**\n\t * 获取验证码生成器\n\t *\n\t * @return 验证码生成器\n\t */\n\tpublic CodeGenerator getGenerator() {\n\t\treturn generator;\n\t}\n\n\t/**\n\t * 设置验证码生成器\n\t *\n\t * @param generator 验证码生成器\n\t */\n\tpublic void setGenerator(CodeGenerator generator) {\n\t\tthis.generator = generator;\n\t}\n\n\t/**\n\t * 设置背景色\n\t *\n\t * @param background 背景色\n\t * @since 4.1.22\n\t */\n\tpublic void setBackground(Color background) {\n\t\tthis.background = background;\n\t}\n\n\t/**\n\t * 设置文字透明度\n\t *\n\t * @param textAlpha 文字透明度，取值0~1，1表示不透明\n\t * @since 4.5.17\n\t */\n\tpublic void setTextAlpha(float textAlpha) {\n\t\tthis.textAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, textAlpha);\n\t}\n\n\t/**\n\t * 设置画笔线条特征，如线条宽度等\n\t *\n\t * @param stroke 画笔\n\t * @since 5.8.44\n\t */\n\tpublic void setStroke(Stroke stroke) {\n\t\tthis.stroke = stroke;\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.CodeGenerator;\n\n/**\n * 图形验证码工具\n *\n * @author looly\n * @since 3.1.2\n */\npublic class CaptchaUtil {\n\n\t/**\n\t * 创建线干扰的验证码，默认5位验证码，150条干扰线\n\t *\n\t * @param width  图片宽\n\t * @param height 图片高\n\t * @return {@link LineCaptcha}\n\t */\n\tpublic static LineCaptcha createLineCaptcha(int width, int height) {\n\t\treturn new LineCaptcha(width, height);\n\t}\n\n\t/**\n\t * 创建线干扰的验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param lineCount 干扰线条数\n\t * @return {@link LineCaptcha}\n\t */\n\tpublic static LineCaptcha createLineCaptcha(int width, int height, int codeCount, int lineCount) {\n\t\treturn new LineCaptcha(width, height, codeCount, lineCount);\n\t}\n\n\t/**\n\t * 创建线干扰的验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param generator 验证码生成器\n\t * @param lineCount 干扰线条数\n\t * @return {@link LineCaptcha}\n\t */\n\tpublic static LineCaptcha createLineCaptcha(int width, int height, CodeGenerator generator, int lineCount) {\n\t\treturn new LineCaptcha(width, height, generator, lineCount);\n\t}\n\n\t/**\n\t * 创建线干扰的验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param lineCount 干扰线条数\n\t * @param size      字体的大小 高度的倍数\n\t * @return {@link LineCaptcha}\n\t */\n\tpublic static LineCaptcha createLineCaptcha(int width, int height, int codeCount, int lineCount, float size) {\n\t\treturn new LineCaptcha(width, height, codeCount, lineCount, size);\n\t}\n\t// ------------------------- lineCaptcha end -------------------------\n\n\t/**\n\t * 创建圆圈干扰的验证码，默认5位验证码，15个干扰圈\n\t *\n\t * @param width  图片宽\n\t * @param height 图片高\n\t * @return {@link CircleCaptcha}\n\t * @since 3.2.3\n\t */\n\tpublic static CircleCaptcha createCircleCaptcha(int width, int height) {\n\t\treturn new CircleCaptcha(width, height);\n\t}\n\n\t/**\n\t * 创建圆圈干扰的验证码\n\t *\n\t * @param width       图片宽\n\t * @param height      图片高\n\t * @param codeCount   字符个数\n\t * @param circleCount 干扰圆圈条数\n\t * @return {@link CircleCaptcha}\n\t * @since 3.2.3\n\t */\n\tpublic static CircleCaptcha createCircleCaptcha(int width, int height, int codeCount, int circleCount) {\n\t\treturn new CircleCaptcha(width, height, codeCount, circleCount);\n\t}\n\n\t/**\n\t * 创建圆圈干扰的验证码\n\t *\n\t * @param width       图片宽\n\t * @param height      图片高\n\t * @param generator   验证码生成器\n\t * @param circleCount 干扰圆圈条数\n\t * @return {@link CircleCaptcha}\n\t */\n\tpublic static CircleCaptcha createCircleCaptcha(int width, int height, CodeGenerator generator, int circleCount) {\n\t\treturn new CircleCaptcha(width, height, generator, circleCount);\n\t}\n\n\t/**\n\t * 创建圆圈干扰的验证码\n\t *\n\t * @param width       图片宽\n\t * @param height      图片高\n\t * @param codeCount   字符个数\n\t * @param circleCount 干扰圆圈条数\n\t * @param size        字体的大小 高度的倍数\n\t * @return {@link CircleCaptcha}\n\t */\n\tpublic static CircleCaptcha createCircleCaptcha(int width, int height, int codeCount, int circleCount, float size) {\n\t\treturn new CircleCaptcha(width, height, codeCount, circleCount, size);\n\t}\n\t// ------------------------- circleCaptcha end -------------------------\n\n\t/**\n\t * 创建扭曲干扰的验证码，默认5位验证码\n\t *\n\t * @param width  图片宽\n\t * @param height 图片高\n\t * @return {@link ShearCaptcha}\n\t * @since 3.2.3\n\t */\n\tpublic static ShearCaptcha createShearCaptcha(int width, int height) {\n\t\treturn new ShearCaptcha(width, height);\n\t}\n\n\t/**\n\t * 创建扭曲干扰的验证码，默认5位验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param thickness 干扰线宽度\n\t * @return {@link ShearCaptcha}\n\t * @since 3.3.0\n\t */\n\tpublic static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness) {\n\t\treturn new ShearCaptcha(width, height, codeCount, thickness);\n\t}\n\n\t/**\n\t * 创建扭曲干扰的验证码，默认5位验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param generator 验证码生成器\n\t * @param thickness 干扰线宽度\n\t * @return {@link ShearCaptcha}\n\t */\n\tpublic static ShearCaptcha createShearCaptcha(int width, int height, CodeGenerator generator, int thickness) {\n\t\treturn new ShearCaptcha(width, height, generator, thickness);\n\t}\n\n\t/**\n\t * 创建扭曲干扰的验证码，默认5位验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param thickness 干扰线宽度\n\t * @param size      字体的大小 高度的倍数\n\t * @return {@link ShearCaptcha}\n\t */\n\tpublic static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness, float size) {\n\t\treturn new ShearCaptcha(width, height, codeCount, thickness, size);\n\t}\n\t// ------------------------- shearCaptcha end -------------------------\n\n\t/**\n\t * 创建GIF验证码\n\t *\n\t * @param width  宽\n\t * @param height 高\n\t * @return {@link GifCaptcha}\n\t */\n\tpublic static GifCaptcha createGifCaptcha(int width, int height) {\n\t\treturn new GifCaptcha(width, height);\n\t}\n\n\t/**\n\t * 创建GIF验证码\n\t *\n\t * @param width     宽\n\t * @param height    高\n\t * @param codeCount 字符个数\n\t * @return {@link GifCaptcha}\n\t */\n\tpublic static GifCaptcha createGifCaptcha(int width, int height, int codeCount) {\n\t\treturn new GifCaptcha(width, height, codeCount);\n\t}\n\n\t/**\n\t * 创建GIF验证码\n\t *\n\t * @param width     宽\n\t * @param height    高\n\t * @param generator 验证码生成器\n\t * @param thickness 验证码干扰元素个数\n\t * @return {@link GifCaptcha}\n\t */\n\tpublic static GifCaptcha createGifCaptcha(int width, int height, CodeGenerator generator, int thickness) {\n\t\treturn new GifCaptcha(width, height, generator, thickness);\n\t}\n\n\t/**\n\t * 创建圆圈干扰的验证码\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param thickness 验证码干扰元素个数\n\t * @param size      字体的大小 高度的倍数\n\t * @return {@link GifCaptcha}\n\t */\n\tpublic static GifCaptcha createGifCaptcha(int width, int height, int codeCount, int thickness, float size) {\n\t\treturn new GifCaptcha(width, height, codeCount, thickness, size);\n\t}\n\t// ------------------------- gifCaptcha end -------------------------\n\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.CodeGenerator;\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport cn.hutool.core.img.GraphicsUtil;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * 圆圈干扰验证码\n *\n * @author looly\n * @since 3.2.3\n *\n */\npublic class CircleCaptcha extends AbstractCaptcha {\n\tprivate static final long serialVersionUID = -7096627300356535494L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param width  图片宽\n\t * @param height 图片高\n\t */\n\tpublic CircleCaptcha(int width, int height) {\n\t\tthis(width, height, 5);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t */\n\tpublic CircleCaptcha(int width, int height, int codeCount) {\n\t\tthis(width, height, codeCount, 15);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount      字符个数\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic CircleCaptcha(int width, int height, int codeCount, int interfereCount) {\n\t\tthis(width, height, new RandomGenerator(codeCount), interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic CircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {\n\t\tsuper(width, height, generator, interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount      字符个数\n\t * @param interfereCount 验证码干扰元素个数\n\t * @param size \t\t\t 字体的大小 高度的倍数\n\t */\n\tpublic CircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) {\n\t\tsuper(width, height, new RandomGenerator(codeCount), interfereCount, size);\n\t}\n\n\n\t@Override\n\tpublic Image createImage(String code) {\n\t\tfinal BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB);\n\t\tfinal Graphics2D g = ImgUtil.createGraphics(image, this.background);\n\n\t\ttry {\n\t\t\t// 随机画干扰圈圈\n\t\t\tdrawInterfere(g);\n\n\t\t\t// 画字符串\n\t\t\tdrawString(g, code);\n\t\t} finally {\n\t\t\tg.dispose();\n\t\t}\n\n\t\treturn image;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 绘制字符串\n\t *\n\t * @param g    {@link Graphics2D}画笔\n\t * @param code 验证码\n\t */\n\tprivate void drawString(Graphics2D g, String code) {\n\t\t// 指定透明度\n\t\tif (null != this.textAlpha) {\n\t\t\tg.setComposite(this.textAlpha);\n\t\t}\n\t\tGraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);\n\t}\n\n\t/**\n\t * 画随机干扰\n\t *\n\t * @param g {@link Graphics2D}\n\t */\n\tprivate void drawInterfere(Graphics2D g) {\n\t\t// issue#IDJQ15 自定义线条特征（粗细等）\n\t\tif(null != this.stroke){\n\t\t\tg.setStroke(this.stroke);\n\t\t}\n\n\t\tfinal ThreadLocalRandom random = RandomUtil.getRandom();\n\n\t\tfor (int i = 0; i < this.interfereCount; i++) {\n\t\t\tg.setColor(ImgUtil.randomColor(random));\n\t\t\tg.drawOval(random.nextInt(width), random.nextInt(height), random.nextInt(height >> 1), random.nextInt(height >> 1));\n\t\t}\n\t}\n\t// ----------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/GifCaptcha.java",
    "content": "package cn.hutool.captcha;\n\n\nimport cn.hutool.captcha.generator.CodeGenerator;\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.img.gif.AnimatedGifEncoder;\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.awt.AlphaComposite;\nimport java.awt.Color;\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\n\n/**\n * Gif验证码类\n *\n * @author hsoftxl\n * @since 5.5.2\n */\npublic class GifCaptcha extends AbstractCaptcha {\n\tprivate static final long serialVersionUID = 7091627304326538464L;\n\n\t//量化器取样间隔 - 默认是10ms\n\tprivate int quality = 10;\n\t// 帧循环次数\n\tprivate int repeat = 0;\n\t//设置随机颜色时，最小的取色范围\n\tprivate int minColor = 0;\n\t//设置随机颜色时，最大的取色范围\n\tprivate int maxColor = 255;\n\n\n\t/**\n\t * 可以设置验证码宽度，高度的构造函数\n\t *\n\t * @param width  验证码宽度\n\t * @param height 验证码高度\n\t */\n\tpublic GifCaptcha(int width, int height) {\n\t\tthis(width, height, 5);\n\t}\n\n\t/**\n\t * @param width     验证码宽度\n\t * @param height    验证码高度\n\t * @param codeCount 验证码个数\n\t */\n\tpublic GifCaptcha(int width, int height, int codeCount) {\n\t\tthis(width, height, codeCount, 10);\n\t}\n\n\t/**\n\t * @param width          验证码宽度\n\t * @param height         验证码高度\n\t * @param codeCount      验证码个数\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic GifCaptcha(int width, int height, int codeCount, int interfereCount) {\n\t\tthis(width, height, new RandomGenerator(codeCount), interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic GifCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {\n\t\tsuper(width, height, generator, interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount      验证码个数\n\t * @param interfereCount 验证码干扰元素个数\n\t * @param size           字体的大小 高度的倍数\n\t */\n\tpublic GifCaptcha(int width, int height, int codeCount, int interfereCount, float size) {\n\t\tsuper(width, height, new RandomGenerator(codeCount), interfereCount, size);\n\t}\n\n\t/**\n\t * 设置图像的颜色量化(转换质量 由GIF规范允许的最大256种颜色)。\n\t * 低的值(最小值= 1)产生更好的颜色,但处理显著缓慢。\n\t * 10是默认,并产生良好的颜色而且有以合理的速度。\n\t * 值更大(大于20)不产生显著的改善速度\n\t *\n\t * @param quality 大于1\n\t * @return this\n\t */\n\tpublic GifCaptcha setQuality(int quality) {\n\t\tif (quality < 1) {\n\t\t\tquality = 1;\n\t\t}\n\t\tthis.quality = quality;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置GIF帧应该播放的次数。\n\t * 默认是 0; 0意味着无限循环。\n\t * 必须在添加的第一个图像之前被调用。\n\t *\n\t * @param repeat 必须大于等于0\n\t * @return this\n\t */\n\tpublic GifCaptcha setRepeat(int repeat) {\n\t\tthis.repeat = Math.max(repeat, 0);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置验证码字符颜色\n\t *\n\t * @param maxColor 颜色\n\t * @return this\n\t */\n\tpublic GifCaptcha setMaxColor(int maxColor) {\n\t\tthis.maxColor = maxColor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置验证码字符颜色\n\t *\n\t * @param minColor 颜色\n\t * @return this\n\t */\n\tpublic GifCaptcha setMinColor(int minColor) {\n\t\tthis.minColor = minColor;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void createCode() {\n\t\tgenerateCode();\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\n\t\tAnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();// gif编码类\n\t\t//生成字符\n\t\tgifEncoder.start(out);\n\t\tgifEncoder.setQuality(quality);//设置量化器取样间隔\n\t\t// 帧延迟 (默认100)\n\t\tint delay = 100;\n\t\tgifEncoder.setDelay(delay);//设置帧延迟\n\t\tgifEncoder.setRepeat(repeat);//帧循环次数\n\t\tBufferedImage frame;\n\t\tchar[] chars = code.toCharArray();\n\t\tColor[] fontColor = new Color[chars.length];\n\t\tfor (int i = 0; i < chars.length; i++) {\n\t\t\tfontColor[i] = getRandomColor(minColor, maxColor);\n\t\t\tframe = graphicsImage(chars, fontColor, chars, i);\n\t\t\tgifEncoder.addFrame(frame);\n\t\t\tframe.flush();\n\t\t}\n\t\tgifEncoder.finish();\n\t\tthis.imageBytes = out.toByteArray();\n\t}\n\n\t@Override\n\tprotected Image createImage(String code) {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 画随机码图\n\t *\n\t * @param fontColor 随机字体颜色\n\t * @param words     字符数组\n\t * @param flag      透明度使用\n\t * @return BufferedImage\n\t */\n\tprivate BufferedImage graphicsImage(char[] chars, Color[] fontColor, char[] words, int flag) {\n\t\tfinal BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB);\n\t\t//或得图形上下文\n\t\tfinal Graphics2D g2d = ImgUtil.createGraphics(image, this.background);\n\t\ttry {\n\t\t\t//利用指定颜色填充背景\n\t\t\tAlphaComposite ac;\n\t\t\t// 字符的y坐标\n\t\t\tfloat y = (height >> 1) + (font.getSize() >> 1);\n\t\t\tfloat m = 1.0f * (width - (chars.length * font.getSize())) / chars.length;\n\t\t\t//字符的x坐标\n\t\t\tfloat x = Math.max(m / 2.0f, 2);\n\t\t\tg2d.setFont(font);\n\t\t\t// 指定透明度\n\t\t\tif (null != this.textAlpha) {\n\t\t\t\tg2d.setComposite(this.textAlpha);\n\t\t\t}\n\t\t\tfor (int i = 0; i < chars.length; i++) {\n\t\t\t\tac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getAlpha(chars.length, flag, i));\n\t\t\t\tg2d.setComposite(ac);\n\t\t\t\tg2d.setColor(fontColor[i]);\n\t\t\t\tg2d.drawOval(\n\t\t\t\t\t\tRandomUtil.randomInt(width),\n\t\t\t\t\t\tRandomUtil.randomInt(height),\n\t\t\t\t\t\tRandomUtil.randomInt(5, 30), 5 + RandomUtil.randomInt(5, 30)\n\t\t\t\t);//绘制椭圆边框\n\t\t\t\tg2d.drawString(words[i] + \"\", x + (font.getSize() + m) * i, y);\n\t\t\t}\n\t\t} finally {\n\t\t\tg2d.dispose();\n\t\t}\n\t\treturn image;\n\t}\n\n\t/**\n\t * 获取透明度,从0到1,自动计算步长\n\t *\n\t * @return float 透明度\n\t */\n\tprivate float getAlpha(int v, int i, int j) {\n\t\tint num = i + j;\n\t\tfloat r = (float) 1 / v;\n\t\tfloat s = (v + 1) * r;\n\t\treturn num > v ? (num * r - s) : num * r;\n\t}\n\n\t/**\n\t * 通过给定范围获得随机的颜色\n\t *\n\t * @return Color 获得随机的颜色\n\t */\n\tprivate Color getRandomColor(int min, int max) {\n\t\tif (min > 255) {\n\t\t\tmin = 255;\n\t\t}\n\t\tif (max > 255) {\n\t\t\tmax = 255;\n\t\t}\n\t\tif (min < 0) {\n\t\t\tmin = 0;\n\t\t}\n\t\tif (max < 0) {\n\t\t\tmax = 0;\n\t\t}\n\t\tif (min > max) {\n\t\t\tmin = 0;\n\t\t\tmax = 255;\n\t\t}\n\t\treturn new Color(\n\t\t\t\tRandomUtil.randomInt(min, max),\n\t\t\t\tRandomUtil.randomInt(min, max),\n\t\t\t\tRandomUtil.randomInt(min, max));\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java",
    "content": "package cn.hutool.captcha;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\n\n/**\n * 验证码接口，提供验证码对象接口定义\n *\n * @author looly\n *\n */\npublic interface ICaptcha extends Serializable{\n\n\t/**\n\t * 创建验证码，实现类需同时生成随机验证码字符串和验证码图片\n\t */\n\tvoid createCode();\n\n\t/**\n\t * 获取验证码的文字内容\n\t *\n\t * @return 验证码文字内容\n\t */\n\tString getCode();\n\n\t/**\n\t * 验证验证码是否正确，建议忽略大小写\n\t *\n\t * @param userInputCode 用户输入的验证码\n\t * @return 是否与生成的一直\n\t */\n\tboolean verify(String userInputCode);\n\n\t/**\n\t * 将验证码写出到目标流中\n\t *\n\t * @param out 目标流\n\t */\n\tvoid write(OutputStream out);\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.CodeGenerator;\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport cn.hutool.core.img.GraphicsUtil;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * 使用干扰线方式生成的图形验证码\n *\n * @author looly\n * @since 3.1.2\n */\npublic class LineCaptcha extends AbstractCaptcha {\n\tprivate static final long serialVersionUID = 8691294460763091089L;\n\n\t// -------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，默认5位验证码，150条干扰线\n\t *\n\t * @param width  图片宽\n\t * @param height 图片高\n\t */\n\tpublic LineCaptcha(int width, int height) {\n\t\tthis(width, height, 5, 150);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width     图片宽\n\t * @param height    图片高\n\t * @param codeCount 字符个数\n\t * @param lineCount 干扰线条数\n\t */\n\tpublic LineCaptcha(int width, int height, int codeCount, int lineCount) {\n\t\tthis(width, height, new RandomGenerator(codeCount), lineCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic LineCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {\n\t\tsuper(width, height, generator, interfereCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount      字符个数\n\t * @param interfereCount 验证码干扰元素个数\n\t * @param size           字体的大小 高度的倍数\n\t */\n\tpublic LineCaptcha(int width, int height, int codeCount, int interfereCount, float size) {\n\t\tsuper(width, height, new RandomGenerator(codeCount), interfereCount, size);\n\t}\n\n\n\t// -------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic Image createImage(String code) {\n\t\t// 图像buffer\n\t\tfinal BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB);\n\t\tfinal Graphics2D g = ImgUtil.createGraphics(image, this.background);\n\n\t\ttry {\n\t\t\t// 干扰线\n\t\t\tdrawInterfere(g);\n\n\t\t\t// 字符串\n\t\t\tdrawString(g, code);\n\t\t} finally {\n\t\t\tg.dispose();\n\t\t}\n\n\t\treturn image;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 绘制字符串\n\t *\n\t * @param g    {@link Graphics}画笔\n\t * @param code 验证码\n\t */\n\tprivate void drawString(Graphics2D g, String code) {\n\t\t// 指定透明度\n\t\tif (null != this.textAlpha) {\n\t\t\tg.setComposite(this.textAlpha);\n\t\t}\n\t\tGraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);\n\t}\n\n\t/**\n\t * 绘制干扰线\n\t *\n\t * @param g {@link Graphics2D}画笔\n\t */\n\tprivate void drawInterfere(Graphics2D g) {\n\t\t// issue#IDJQ15 自定义线条特征（粗细等）\n\t\tif(null != this.stroke){\n\t\t\tg.setStroke(this.stroke);\n\t\t}\n\n\t\tfinal ThreadLocalRandom random = RandomUtil.getRandom();\n\t\t// 干扰线\n\t\tfor (int i = 0; i < this.interfereCount; i++) {\n\t\t\tint xs = random.nextInt(width);\n\t\t\tint ys = random.nextInt(height);\n\t\t\tint xe = xs + random.nextInt(width / 8);\n\t\t\tint ye = ys + random.nextInt(height / 8);\n\t\t\tg.setColor(ImgUtil.randomColor(random));\n\t\t\tg.drawLine(xs, ys, xe, ye);\n\t\t}\n\t}\n\t// ----------------------------------------------------------------------------------------------------- Private method start\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.CodeGenerator;\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport cn.hutool.core.img.GraphicsUtil;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.awt.Color;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\n\n/**\n * 扭曲干扰验证码\n *\n * @author looly\n * @since 3.2.3\n *\n */\npublic class ShearCaptcha extends AbstractCaptcha {\n\tprivate static final long serialVersionUID = -7096627300356535494L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param width 图片宽\n\t * @param height 图片高\n\t */\n\tpublic ShearCaptcha(int width, int height) {\n\t\tthis(width, height, 5);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width 图片宽\n\t * @param height 图片高\n\t * @param codeCount 字符个数\n\t */\n\tpublic ShearCaptcha(int width, int height, int codeCount) {\n\t\tthis(width, height, codeCount, 4);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width 图片宽\n\t * @param height 图片高\n\t * @param codeCount 字符个数\n\t * @param thickness 干扰线宽度\n\t */\n\tpublic ShearCaptcha(int width, int height, int codeCount, int thickness) {\n\t\tthis(width, height, new RandomGenerator(codeCount), thickness);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param generator      验证码生成器\n\t * @param interfereCount 验证码干扰元素个数\n\t */\n\tpublic ShearCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {\n\t\tsuper(width, height, generator, interfereCount);\n\t}\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param width          图片宽\n\t * @param height         图片高\n\t * @param codeCount \t 字符个数\n\t * @param interfereCount 验证码干扰元素个数\n\t * @param size           字体的大小 高度的倍数\n\t */\n\tpublic ShearCaptcha(int width, int height, int codeCount, int interfereCount, float size) {\n\t\tsuper(width, height, new RandomGenerator(codeCount), interfereCount, size);\n\t}\n\n\t@Override\n\tpublic Image createImage(String code) {\n\t\tfinal BufferedImage image = new BufferedImage(width, height, (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB);\n\t\tfinal Graphics2D g = ImgUtil.createGraphics(image, this.background);\n\n\t\ttry{\n\t\t\t// 画字符串\n\t\t\tdrawString(g, code);\n\n\t\t\t// 扭曲\n\t\t\tshear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE));\n\t\t\t// 画干扰线\n\t\t\tdrawInterfere(g, 0, RandomUtil.randomInt(this.height) + 1, this.width, RandomUtil.randomInt(this.height) + 1, this.interfereCount, ImgUtil.randomColor());\n\t\t} finally {\n\t\t\tg.dispose();\n\t\t}\n\n\t\treturn image;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 绘制字符串\n\t *\n\t * @param g {@link Graphics}画笔\n\t * @param code 验证码\n\t */\n\tprivate void drawString(Graphics2D g, String code) {\n\t\t// 指定透明度\n\t\tif (null != this.textAlpha) {\n\t\t\tg.setComposite(this.textAlpha);\n\t\t}\n\t\tGraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);\n\t}\n\n\t/**\n\t * 扭曲\n\t *\n\t * @param g {@link Graphics}\n\t * @param w1 w1\n\t * @param h1 h1\n\t * @param color 颜色\n\t */\n\tprivate void shear(Graphics g, int w1, int h1, Color color) {\n\t\tshearX(g, w1, h1, color);\n\t\tshearY(g, w1, h1, color);\n\t}\n\n\t/**\n\t * X坐标扭曲\n\t *\n\t * @param g {@link Graphics}\n\t * @param w1 宽\n\t * @param h1 高\n\t * @param color 颜色\n\t */\n\tprivate void shearX(Graphics g, int w1, int h1, Color color) {\n\n\t\tint period = RandomUtil.randomInt(this.width);\n\n\t\tint frames = 1;\n\t\tint phase = RandomUtil.randomInt(2);\n\n\t\tfor (int i = 0; i < h1; i++) {\n\t\t\tdouble d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);\n\t\t\tg.copyArea(0, i, w1, 1, (int) d, 0);\n\t\t\tg.setColor(color);\n\t\t\tg.drawLine((int) d, i, 0, i);\n\t\t\tg.drawLine((int) d + w1, i, w1, i);\n\t\t}\n\n\t}\n\n\t/**\n\t * Y坐标扭曲\n\t *\n\t * @param g {@link Graphics}\n\t * @param w1 宽\n\t * @param h1 高\n\t * @param color 颜色\n\t */\n\tprivate void shearY(Graphics g, int w1, int h1, Color color) {\n\n\t\tint period = RandomUtil.randomInt(this.height >> 1);\n\n\t\tint frames = 20;\n\t\tint phase = 7;\n\t\tfor (int i = 0; i < w1; i++) {\n\t\t\tdouble d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);\n\t\t\tg.copyArea(i, 0, 1, h1, 0, (int) d);\n\t\t\tg.setColor(color);\n\t\t\t// 擦除原位置的痕迹\n\t\t\tg.drawLine(i, (int) d, i, 0);\n\t\t\tg.drawLine(i, (int) d + h1, i, h1);\n\t\t}\n\n\t}\n\n\t/**\n\t * 干扰线\n\t *\n\t * @param g {@link Graphics}\n\t * @param x1 x1\n\t * @param y1 y1\n\t * @param x2 x2\n\t * @param y2 y2\n\t * @param thickness 粗细\n\t * @param c 颜色\n\t */\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate void drawInterfere(Graphics g, int x1, int y1, int x2, int y2, int thickness, Color c) {\n\t\t// The thick line is in fact a filled polygon\n\t\tg.setColor(c);\n\t\tint dX = x2 - x1;\n\t\tint dY = y2 - y1;\n\t\t// line length\n\t\tdouble lineLength = Math.sqrt(dX * dX + dY * dY);\n\n\t\tdouble scale = (double) (thickness) / (2 * lineLength);\n\n\t\t// The x and y increments from an endpoint needed to create a\n\t\t// rectangle...\n\t\tdouble ddx = -scale * (double) dY;\n\t\tdouble ddy = scale * (double) dX;\n\t\tddx += (ddx > 0) ? 0.5 : -0.5;\n\t\tddy += (ddy > 0) ? 0.5 : -0.5;\n\t\tint dx = (int) ddx;\n\t\tint dy = (int) ddy;\n\n\t\t// Now we can compute the corner points...\n\t\tint[] xPoints = new int[4];\n\t\tint[] yPoints = new int[4];\n\n\t\txPoints[0] = x1 + dx;\n\t\tyPoints[0] = y1 + dy;\n\t\txPoints[1] = x1 - dx;\n\t\tyPoints[1] = y1 - dy;\n\t\txPoints[2] = x2 - dx;\n\t\tyPoints[2] = y2 - dy;\n\t\txPoints[3] = x2 + dx;\n\t\tyPoints[3] = y2 + dy;\n\n\t\tg.fillPolygon(xPoints, yPoints, 4);\n\t}\n\t// ----------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java",
    "content": "package cn.hutool.captcha.generator;\n\nimport cn.hutool.core.util.RandomUtil;\n\n/**\n * 随机字符验证码生成器<br>\n * 可以通过传入的基础集合和长度随机生成验证码字符\n *\n * @author looly\n * @since 4.1.2\n */\npublic abstract class AbstractGenerator implements CodeGenerator {\n\tprivate static final long serialVersionUID = 8685744597154953479L;\n\n\t/** 基础字符集合，用于随机获取字符串的字符集合 */\n\tprotected final String baseStr;\n\t/** 验证码长度 */\n\tprotected final int length;\n\n\t/**\n\t * 构造，使用字母+数字做为基础\n\t *\n\t * @param count 生成验证码长度\n\t */\n\tpublic AbstractGenerator(int count) {\n\t\tthis(RandomUtil.BASE_CHAR_NUMBER, count);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param baseStr 基础字符集合，用于随机获取字符串的字符集合\n\t * @param length 生成验证码长度\n\t */\n\tpublic AbstractGenerator(String baseStr, int length) {\n\t\tthis.baseStr = baseStr;\n\t\tthis.length = length;\n\t}\n\n\t/**\n\t * 获取长度验证码\n\t *\n\t * @return 验证码长度\n\t */\n\tpublic int getLength() {\n\t\treturn this.length;\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java",
    "content": "package cn.hutool.captcha.generator;\n\nimport java.io.Serializable;\n\n/**\n * 验证码文字生成器\n *\n * @author looly\n * @since 4.1.2\n */\npublic interface CodeGenerator extends Serializable{\n\n\t/**\n\t * 生成验证码\n\t *\n\t * @return 验证码\n\t */\n\tString generate();\n\n\t/**\n\t * 验证用户输入的字符串是否与生成的验证码匹配<br>\n\t * 用户通过实现此方法定义验证码匹配方式\n\t *\n\t * @param code 生成的随机验证码\n\t * @param userInputCode 用户输入的验证码\n\t * @return 是否验证通过\n\t */\n\tboolean verify(String code, String userInputCode);\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java",
    "content": "package cn.hutool.captcha.generator;\n\nimport cn.hutool.core.math.Calculator;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 数字计算验证码生成器\n *\n * @author looly\n * @since 4.1.2\n */\npublic class MathGenerator implements CodeGenerator {\n\tprivate static final long serialVersionUID = -5514819971774091076L;\n\n\tprivate static final String operators = \"+-*\";\n\n\t/**\n\t * 参与计算数字最大长度\n\t */\n\tprivate final int numberLength;\n\n\t/**\n\t * 计算结果是否允许负数\n\t */\n\tprivate final boolean resultHasNegativeNumber;\n\n\t/**\n\t * 构造\n\t */\n\tpublic MathGenerator() {\n\t\tthis(2, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param resultHasNegativeNumber 结果是否允许负数\n\t */\n\tpublic MathGenerator(boolean resultHasNegativeNumber) {\n\t\tthis(2, resultHasNegativeNumber);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param numberLength 参与计算最大数字位数\n\t */\n\tpublic MathGenerator(int numberLength) {\n\t\tthis(numberLength, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param numberLength            参与计算最大数字位数\n\t * @param resultHasNegativeNumber 结果是否允许负数\n\t */\n\tpublic MathGenerator(int numberLength, boolean resultHasNegativeNumber) {\n\t\tthis.numberLength = numberLength;\n\t\tthis.resultHasNegativeNumber = resultHasNegativeNumber;\n\t}\n\n\t@Override\n\tpublic String generate() {\n\t\tfinal int limit = getLimit();\n\t\tchar operator = RandomUtil.randomChar(operators);\n\t\tint numberInt1 = 0;\n\t\tint numberInt2 = 0;\n\t\tnumberInt1 = RandomUtil.randomInt(limit);\n\t\t// 如果禁止了结果有负数，且计算方式正好计算为减法，需要第二个数小于第一个数\n\t\tif (!resultHasNegativeNumber && CharUtil.equals('-', operator, false)) {\n\t\t\t//如果第一个数为0，第二个数必须为0，随机[0,0)的数字会报错\n\t\t\tnumberInt2 = numberInt1 == 0 ? 0 : RandomUtil.randomInt(0, numberInt1);\n\t\t} else {\n\t\t\tnumberInt2 = RandomUtil.randomInt(limit);\n\t\t}\n\t\tString number1 = Integer.toString(numberInt1);\n\t\tString number2 = Integer.toString(numberInt2);\n\t\tnumber1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE);\n\t\tnumber2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE);\n\t\treturn StrUtil.builder()//\n\t\t\t.append(number1)//\n\t\t\t.append(operator)//\n\t\t\t.append(number2)//\n\t\t\t.append('=').toString();\n\t}\n\n\t@Override\n\tpublic boolean verify(String code, String userInputCode) {\n\t\tint result;\n\t\ttry {\n\t\t\tresult = Integer.parseInt(userInputCode);\n\t\t} catch (NumberFormatException e) {\n\t\t\t// 用户输入非数字\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal int calculateResult = (int) Calculator.conversion(code);\n\t\treturn result == calculateResult;\n\t}\n\n\t/**\n\t * 获取验证码长度\n\t *\n\t * @return 验证码长度\n\t */\n\tpublic int getLength() {\n\t\treturn this.numberLength * 2 + 2;\n\t}\n\n\t/**\n\t * 根据长度获取参与计算数字最大值\n\t *\n\t * @return 最大值\n\t */\n\tprivate int getLimit() {\n\t\treturn Integer.parseInt(\"1\" + StrUtil.repeat('0', this.numberLength));\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java",
    "content": "package cn.hutool.captcha.generator;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 随机字符验证码生成器<br>\n * 可以通过传入的基础集合和长度随机生成验证码字符\n *\n * @author looly\n * @since 4.1.2\n */\npublic class RandomGenerator extends AbstractGenerator {\n\tprivate static final long serialVersionUID = -7802758587765561876L;\n\n\t/**\n\t * 构造，使用字母+数字做为基础\n\t *\n\t * @param count 生成验证码长度\n\t */\n\tpublic RandomGenerator(int count) {\n\t\tsuper(count);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param baseStr 基础字符集合，用于随机获取字符串的字符集合\n\t * @param length 生成验证码长度\n\t */\n\tpublic RandomGenerator(String baseStr, int length) {\n\t\tsuper(baseStr, length);\n\t}\n\n\t@Override\n\tpublic String generate() {\n\t\treturn RandomUtil.randomString(this.baseStr, this.length);\n\t}\n\n\t@Override\n\tpublic boolean verify(String code, String userInputCode) {\n\t\tif (StrUtil.isNotBlank(userInputCode)) {\n\t\t\treturn StrUtil.equalsIgnoreCase(code, userInputCode);\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java",
    "content": "/**\n * 验证码生成策略实现\n *\n * @author looly\n * @since 4.1.2\n */\npackage cn.hutool.captcha.generator;"
  },
  {
    "path": "hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java",
    "content": "/**\n * 图片验证码实现\n *\n * @author looly\n *\n */\npackage cn.hutool.captcha;"
  },
  {
    "path": "hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.MathGenerator;\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\n\n/**\n * 直线干扰验证码单元测试\n *\n * @author looly\n */\npublic class CaptchaTest {\n\n\t@Test\n\tpublic void lineCaptchaTest1() {\n\t\t// 定义图形验证码的长和宽\n\t\tLineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);\n\t\tassertNotNull(lineCaptcha.getCode());\n\t\tassertTrue(lineCaptcha.verify(lineCaptcha.getCode()));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lineCaptchaTest3() {\n\t\t// 定义图形验证码的长和宽\n\t\tLineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 70, 4, 15);\n\t\tlineCaptcha.setBackground(null);\n\t\tlineCaptcha.write(\"d:/test/captcha/tellow.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lineCaptchaTestWithSize() {\n\t\t// 定义图形验证码的长和宽\n\t\tLineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 70, 4, 15, 0.65f);\n\t\tlineCaptcha.setBackground(Color.yellow);\n\t\tlineCaptcha.write(\"f:/test/captcha/tellow.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lineCaptchaWithMathTest() {\n\t\t// 定义图形验证码的长和宽\n\t\tLineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 80);\n\t\tlineCaptcha.setGenerator(new MathGenerator());\n\t\tlineCaptcha.setTextAlpha(0.8f);\n\t\tlineCaptcha.write(\"f:/captcha/math.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lineCaptchaTest2() {\n\n\t\t// 定义图形验证码的长和宽\n\t\tLineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);\n\t\t// LineCaptcha lineCaptcha = new LineCaptcha(200, 100, 4, 150);\n\t\t// 图形验证码写出，可以写出到文件，也可以写出到流\n\t\tlineCaptcha.write(\"f:/captcha/line.png\");\n\t\tConsole.log(lineCaptcha.getCode());\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tlineCaptcha.verify(\"1234\");\n\n\t\tlineCaptcha.createCode();\n\t\tlineCaptcha.write(\"f:/captcha/line2.png\");\n\t\tConsole.log(lineCaptcha.getCode());\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tlineCaptcha.verify(\"1234\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void circleCaptchaTest() {\n\n\t\t// 定义图形验证码的长和宽\n\t\tCircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);\n\t\t// CircleCaptcha captcha = new CircleCaptcha(200, 100, 4, 20);\n\t\t// 图形验证码写出，可以写出到文件，也可以写出到流\n\t\tcaptcha.write(\"f:/captcha/circle.png\");\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tcaptcha.verify(\"1234\");\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void circleCaptchaTestWithSize() {\n\t\t// 定义图形验证码的长和宽\n\t\tCircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 70, 4, 15, 0.65f);\n\t\tcaptcha.setBackground(Color.yellow);\n\t\tcaptcha.write(\"f:/test/captcha/circle.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void shearCaptchaTest() {\n\n\t\t// 定义图形验证码的长和宽\n\t\tShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 100, 4, 4);\n\t\t// ShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4);\n\t\t// 图形验证码写出，可以写出到文件，也可以写出到流\n\t\tcaptcha.write(\"f:/captcha/shear.png\");\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tcaptcha.verify(\"1234\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void shearCaptchaTest2() {\n\n\t\t// 定义图形验证码的长和宽\n\t\tShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4);\n\t\t// 图形验证码写出，可以写出到文件，也可以写出到流\n\t\tcaptcha.write(\"d:/test/shear.png\");\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tcaptcha.verify(\"1234\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void ShearCaptchaWithMathTest() {\n\t\t// 定义图形验证码的长和宽\n\t\tShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4);\n\t\tcaptcha.setGenerator(new MathGenerator());\n\t\t// ShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4);\n\t\t// 图形验证码写出，可以写出到文件，也可以写出到流\n\t\tcaptcha.write(\"f:/captcha/shear_math.png\");\n\t\t// 验证图形验证码的有效性，返回boolean值\n\t\tcaptcha.verify(\"1234\");\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void ShearCaptchaTestWithSize() {\n\t\t// 定义图形验证码的长和宽\n\t\tShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 70, 4, 15, 0.65f);\n\t\tcaptcha.setBackground(Color.yellow);\n\t\tcaptcha.write(\"f:/test/captcha/shear.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void GifCaptchaTest() {\n\t\tGifCaptcha captcha = CaptchaUtil.createGifCaptcha(200, 100, 4);\n\t\tcaptcha.write(\"d:/test/gif_captcha.gif\");\n\t\tassert captcha.verify(captcha.getCode());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void GifCaptchaTestWithSize() {\n\t\t// 定义图形验证码的长和宽\n\t\tGifCaptcha captcha = CaptchaUtil.createGifCaptcha(200, 70, 4, 15, 0.65f);\n\t\tcaptcha.setBackground(Color.yellow);\n\t\tcaptcha.write(\"f:/test/captcha/gif.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void bgTest() {\n\t\tLineCaptcha captcha = CaptchaUtil.createLineCaptcha(200, 100, 4, 1);\n\t\tcaptcha.setBackground(Color.WHITE);\n\t\tcaptcha.write(\"d:/test/test.jpg\");\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.core.img.GraphicsUtil;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Color;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\nimport java.util.concurrent.ThreadLocalRandom;\n\npublic class CaptchaUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void createTest() {\n\t\tfor(int i = 0; i < 1; i++) {\n\t\t\tCaptchaUtil.createShearCaptcha(320, 240);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void drawStringColourfulColorDistanceTest() {\n\t\tfor(int i = 0; i < 10; i++) {\n\t\t\tAbstractCaptcha lineCaptcha = new TestLineCaptchaColorDistance(200, 100, 5, 10);\n\t\t\tlineCaptcha.write(\"d:/captcha/line1-\"+i+\".png\");\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void drawStringColourfulDefaultColorDistanceTest() {\n\t\tfor(int i = 0; i < 10; i++) {\n\t\t\tAbstractCaptcha lineCaptcha = new TestLineCaptchaColorDistanceDefaultColorDistance(200, 100, 5, 10);\n\t\t\tlineCaptcha.write(\"d:/captcha/line2-\"+i+\".png\");\n\t\t}\n\t}\n\n\tstatic class TestLineCaptchaColorDistance extends AbstractCaptcha{\n\t\tprivate static final long serialVersionUID = -558846929114465692L;\n\n\t\tpublic TestLineCaptchaColorDistance(int width, int height, int codeCount, int interfereCount) {\n\t\t\tsuper(width, height, codeCount, interfereCount);\n\t\t}\n\n\t\t@Override\n\t\tprotected Image createImage(String code) {\n\t\t\t// 图像buffer\n\t\t\tfinal BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);\n\t\t\tfinal Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE));\n\n\t\t\t// 干扰线\n\t\t\tdrawInterfere(g);\n\n\t\t\t// 字符串\n\t\t\tdrawString(g, code);\n\n\t\t\treturn image;\n\t\t}\n\n\t\t// ----------------------------------------------------------------------------------------------------- Private method start\n\t\t/**\n\t\t * 绘制字符串\n\t\t *\n\t\t * @param g {@link Graphics}画笔\n\t\t * @param code 验证码\n\t\t */\n\t\tprotected void drawString(Graphics2D g, String code) {\n\t\t\t// 指定透明度\n\t\t\tif (null != this.textAlpha) {\n\t\t\t\tg.setComposite(this.textAlpha);\n\t\t\t}\n\t\t\t// 自定义与背景颜色的色差值，200是基于Color.WHITE较为居中的值\n\t\t\tGraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height,Color.WHITE,200);\n\t\t}\n\n\t\t/**\n\t\t * 绘制干扰线\n\t\t *\n\t\t * @param g {@link Graphics2D}画笔\n\t\t */\n\t\tprivate void drawInterfere(Graphics2D g) {\n\t\t\tfinal ThreadLocalRandom random = RandomUtil.getRandom();\n\t\t\t// 干扰线\n\t\t\tfor (int i = 0; i < this.interfereCount; i++) {\n\t\t\t\tint xs = random.nextInt(width);\n\t\t\t\tint ys = random.nextInt(height);\n\t\t\t\tint xe = xs + random.nextInt(width / 3);\n\t\t\t\tint ye = ys + random.nextInt(height / 3);\n\t\t\t\tg.setColor(ImgUtil.randomColor(random));\n\t\t\t\tg.drawLine(xs, ys, xe, ye);\n\t\t\t}\n\t\t}\n\t\t// ----------------------------------------------------------------------------------------------------- Private method start\n\t}\n\n\tstatic class TestLineCaptchaColorDistanceDefaultColorDistance extends TestLineCaptchaColorDistance {\n\n\n\t\tpublic TestLineCaptchaColorDistanceDefaultColorDistance(int width, int height, int codeCount, int interfereCount) {\n\t\t\tsuper(width, height, codeCount, interfereCount);\n\t\t}\n\n\t\t@Override\n\t\tprotected void drawString(Graphics2D g, String code) {\n\t\t\t// 指定透明度\n\t\t\tif (null != this.textAlpha) {\n\t\t\t\tg.setComposite(this.textAlpha);\n\t\t\t}\n\t\t\t// 使用默认色差设置\n\t\t\tGraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height,Color.WHITE);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/test/java/cn/hutool/captcha/GeneratorTest.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.MathGenerator;\nimport cn.hutool.core.math.Calculator;\nimport org.junit.jupiter.api.Test;\n\npublic class GeneratorTest {\n\n\t@Test\n\tpublic void mathGeneratorTest() {\n\t\tfinal MathGenerator mathGenerator = new MathGenerator();\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tmathGenerator.verify(mathGenerator.generate(), \"0\");\n\t\t}\n\n\t\tfinal MathGenerator mathGenerator1 = new MathGenerator(false);\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tString generate = mathGenerator1.generate();\n\t\t\tif( Calculator.conversion(generate) < 0){\n\t\t\t\tthrow new RuntimeException(\"No Pass\");\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/test/java/cn/hutool/captcha/GifCaptchaUtilTest.java",
    "content": "package cn.hutool.captcha;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class GifCaptchaUtilTest {\n\n\tprivate GifCaptcha captcha;\n\n\t@BeforeEach\n\tpublic void setUp() {\n\t\t// 初始化 GifCaptcha 类的实例\n\t\tcaptcha = new GifCaptcha(200, 100, 4, 10);  // width, height, codeCount, interfereCount\n\t}\n\n\t// 使用反射调用私有方法\n\tprivate Object invokePrivateMethod(String methodName, Class<?>[] parameterTypes, Object[] parameters) throws Exception {\n\t\tMethod method = GifCaptcha.class.getDeclaredMethod(methodName, parameterTypes);\n\t\tmethod.setAccessible(true);  // 允许访问私有方法\n\t\treturn method.invoke(captcha, parameters);\n\t}\n\n\t// 测试 setQuality() 方法\n\t@Test\n\tpublic void testSetQuality() throws Exception {\n\t\tcaptcha.setQuality(20);\n\t\t// 通过反射获取 quality 字段的值并进行断言\n\t\tassertEquals(20, getPrivateField(\"quality\"), \"Quality 应该设置为 20\");\n\n\t\tcaptcha.setQuality(0);  // 设置无效值，应该被设置为 1\n\t\tassertEquals(1, getPrivateField(\"quality\"), \"Quality 应该设置为 1，如果小于 1\");\n\t}\n\n\t// 测试 setRepeat() 方法\n\t@Test\n\tpublic void testSetRepeat() throws Exception {\n\t\tcaptcha.setRepeat(5);\n\t\t// 通过反射获取 repeat 字段的值并进行断言\n\t\tassertEquals(5, getPrivateField(\"repeat\"), \"Repeat 应该设置为 5\");\n\n\t\tcaptcha.setRepeat(-1);  // 设置无效值，应该保持为 0\n\t\tassertEquals(0, getPrivateField(\"repeat\"), \"Repeat 应该设置为 0，如果设置了负值\");\n\t}\n\n\t// 测试 setColorRange() 方法\n\t@Test\n\tpublic void testSetColorRange() throws Exception {\n\t\tcaptcha.setMinColor(100).setMaxColor(200);\n\t\t// 通过反射获取 minColor 和 maxColor 字段的值并进行断言\n\t\tassertEquals(100, getPrivateField(\"minColor\"), \"Min color 应该设置为 100\");\n\t\tassertEquals(200, getPrivateField(\"maxColor\"), \"Max color 应该设置为 200\");\n\t}\n\n\t// 测试生成验证码图像的方法 createCode()\n\t@Test\n\tpublic void testCreateCode() throws Exception {\n\t\tcaptcha.createCode();\n\t\tbyte[] imageBytes = captcha.getImageBytes();\n\n\t\t// 检查生成的图片字节是否不为 null 或空\n\t\tassertNotNull(imageBytes, \"生成的图片字节不应该为 null\");\n\t\tassertTrue(imageBytes.length > 0, \"生成的图片字节不应该为空\");\n\n\t\t// 可选：你也可以通过解码图片字节，检查它是否是有效的 GIF 格式\n\t\tByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\tout.write(imageBytes);\n\n\t\t// 解码图片检查它是否为有效的 GIF（假设你有库可以解码 GIF）\n\t\t// ImageIO.read(new ByteArrayInputStream(imageBytes)); // 可以取消注释来检查它是否是有效的 GIF\n\t}\n\n\t// 测试 graphicsImage() 方法\n\t@Test\n\tpublic void testGraphicsImage() throws Exception {\n\t\tchar[] chars = new char[]{'A', 'B', 'C', 'D'};\n\t\tColor[] colors = new Color[]{\n\t\t\tColor.RED, Color.GREEN, Color.BLUE, Color.YELLOW\n\t\t};\n\n\t\t// 使用反射调用 private 方法 graphicsImage\n\t\tObject result = invokePrivateMethod(\"graphicsImage\", new Class[]{char[].class, Color[].class, char[].class, int.class}, new Object[]{chars, colors, chars, 0});\n\n\t\tassertNotNull(result, \"生成的图片不应该为 null\");\n\t\tassertInstanceOf(BufferedImage.class, result, \"返回的结果应该是 BufferedImage 类型\");\n\t}\n\n\t// 测试 getRandomColor() 方法\n\t@Test\n\tpublic void testRandomColor() throws Exception {\n\t\t// 使用反射调用 private 方法 getRandomColor\n\t\tObject result = invokePrivateMethod(\"getRandomColor\", new Class[]{int.class, int.class}, new Object[]{0, 255});\n\n\t\tassertNotNull(result, \"生成的颜色不应该为 null\");\n\t\tassertInstanceOf(Color.class, result, \"返回的结果应该是 Color 类型\");\n\n\t\tColor color = (Color) result;\n\t\tassertTrue(color.getRed() >= 0 && color.getRed() <= 255, \"颜色的红色分量应该在 0 到 255 之间\");\n\t\tassertTrue(color.getGreen() >= 0 && color.getGreen() <= 255, \"颜色的绿色分量应该在 0 到 255 之间\");\n\t\tassertTrue(color.getBlue() >= 0 && color.getBlue() <= 255, \"颜色的蓝色分量应该在 0 到 255 之间\");\n\t}\n\n\t// 辅助方法：通过反射获取私有字段的值\n\tprivate Object getPrivateField(String fieldName) throws NoSuchFieldException, IllegalAccessException {\n\t\tField field = GifCaptcha.class.getDeclaredField(fieldName);\n\t\tfield.setAccessible(true);  // 允许访问私有字段\n\t\treturn field.get(captcha);\n\t}\n}\n"
  },
  {
    "path": "hutool-captcha/src/test/java/cn/hutool/captcha/ShearCaptchaTest.java",
    "content": "package cn.hutool.captcha;\n\nimport cn.hutool.captcha.generator.RandomGenerator;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.lang.reflect.Method;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ShearCaptchaTest {\n\n\tprivate ShearCaptcha captcha;\n\n\t@BeforeEach\n\tpublic void setUp() {\n\t\t// 初始化 ShearCaptcha 实例\n\t\tcaptcha = new ShearCaptcha(200, 100);\n\t}\n\n\t// 测试构造函数和基本功能\n\t@Test\n\tpublic void testConstructor() {\n\t\tassertNotNull(captcha, \"Captcha 实例应该被成功创建\");\n\t}\n\n\t// 测试生成验证码图片的功能\n\t@Test\n\tpublic void testCreateImage() {\n\t\tString code = \"ABCD\";\n\t\tImage image = captcha.createImage(code);\n\t\tassertNotNull(image, \"验证码图片不应该为 null\");\n\t\tassertInstanceOf(BufferedImage.class, image, \"生成的图片应该是 BufferedImage 类型\");\n\n\t\t// 可选：进一步测试图像的内容\n\t\tBufferedImage bufferedImage = (BufferedImage) image;\n\t\tassertEquals(200, bufferedImage.getWidth(), \"图像宽度应该为 200\");\n\t\tassertEquals(100, bufferedImage.getHeight(), \"图像高度应该为 100\");\n\t}\n\n\t// 测试绘制字符串的方法\n\t@Test\n\tpublic void testDrawString() throws Exception {\n\t\tString code = \"ABCD\";\n\t\tMethod drawStringMethod = ShearCaptcha.class.getDeclaredMethod(\"drawString\", Graphics2D.class, String.class);\n\t\tdrawStringMethod.setAccessible(true);\n\n\t\tGraphics2D g2d = (Graphics2D) new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();\n\t\tdrawStringMethod.invoke(captcha, g2d, code);\n\n\t\tassertNotNull(g2d, \"Graphics2D 对象不应该为 null\");\n\t\tassertTrue(g2d.getRenderingHints().containsKey(RenderingHints.KEY_ANTIALIASING), \"应该启用抗锯齿\");\n\t}\n\n\t// 测试 shear() 方法\n\t@Test\n\tpublic void testShear() throws Exception {\n\t\t// 使用反射测试 shear 方法\n\t\tMethod shearMethod = ShearCaptcha.class.getDeclaredMethod(\"shear\", Graphics.class, int.class, int.class, Color.class);\n\t\tshearMethod.setAccessible(true);\n\n\t\tGraphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();\n\t\tshearMethod.invoke(captcha, g, 200, 100, Color.WHITE);\n\n\t\t// 假设没有明显的错误输出，认为测试通过\n\t\tassertNotNull(g, \"Graphics 对象不应该为 null\");\n\t}\n\n\t// 测试 shearX() 方法\n\t@Test\n\tpublic void testShearX() throws Exception {\n\t\t// 使用反射测试 shearX 方法\n\t\tMethod shearXMethod = ShearCaptcha.class.getDeclaredMethod(\"shearX\", Graphics.class, int.class, int.class, Color.class);\n\t\tshearXMethod.setAccessible(true);\n\n\t\tGraphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();\n\t\tshearXMethod.invoke(captcha, g, 200, 100, Color.RED);\n\n\t\t// 假设没有明显的错误输出，认为测试通过\n\t\tassertNotNull(g, \"Graphics 对象不应该为 null\");\n\t}\n\n\t// 测试 shearY() 方法\n\t@Test\n\tpublic void testShearY() throws Exception {\n\t\t// 使用反射测试 shearY 方法\n\t\tMethod shearYMethod = ShearCaptcha.class.getDeclaredMethod(\"shearY\", Graphics.class, int.class, int.class, Color.class);\n\t\tshearYMethod.setAccessible(true);\n\n\t\tGraphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();\n\t\tshearYMethod.invoke(captcha, g, 200, 100, Color.BLUE);\n\n\t\t// 假设没有明显的错误输出，认为测试通过\n\t\tassertNotNull(g, \"Graphics 对象不应该为 null\");\n\t}\n\n\t// 测试 drawInterfere() 方法\n\t@Test\n\tpublic void testDrawInterfere() throws Exception {\n\t\t// 使用反射测试 drawInterfere 方法\n\t\tMethod drawInterfereMethod = ShearCaptcha.class.getDeclaredMethod(\"drawInterfere\", Graphics.class, int.class, int.class, int.class, int.class, int.class, Color.class);\n\t\tdrawInterfereMethod.setAccessible(true);\n\n\t\tGraphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();\n\t\tdrawInterfereMethod.invoke(captcha, g, 0, 0, 200, 100, 4, Color.GREEN);\n\n\t\t// 假设没有明显的错误输出，认为测试通过\n\t\tassertNotNull(g, \"Graphics 对象不应该为 null\");\n\t}\n\n\t// 测试验证码生成时的干扰线\n\t@Test\n\tpublic void testDrawInterfereLines() {\n\t\t// 设置干扰线数量\n\t\tcaptcha = new ShearCaptcha(200, 100, 4);\n\t\tImage image = captcha.createImage(\"ABCD\");\n\n\t\t// 检查图像内容，判断干扰线是否正确绘制\n\t\tassertNotNull(image, \"生成的验证码图片不应该为空\");\n\t}\n\n\t// 测试验证码的尺寸\n\t@Test\n\tpublic void testCaptchaSize() {\n\t\tcaptcha = new ShearCaptcha(300, 150);\n\n\t\tString code = \"XYZ\";\n\t\tImage image = captcha.createImage(code);\n\n\t\tBufferedImage bufferedImage = (BufferedImage) image;\n\t\tassertEquals(300, bufferedImage.getWidth(), \"图像宽度应该为 300\");\n\t\tassertEquals(150, bufferedImage.getHeight(), \"图像高度应该为 150\");\n\t}\n\n\t// 测试生成随机验证码字符\n\t@Test\n\tpublic void testRandomGenerator() {\n\t\tRandomGenerator randomGenerator = new RandomGenerator(4);\n\t\tString code = randomGenerator.generate();\n\t\tassertNotNull(code, \"生成的验证码字符不应该为 null\");\n\t\tassertEquals(4, code.length(), \"验证码字符长度应该为 4\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/README.md",
    "content": "<p align=\"center\">\n\t<a href=\"https://hutool.cn/\"><img src=\"https://cdn.jsdelivr.net/gh/looly/hutool-site/images/logo.jpg\" width=\"45%\"></a>\n</p>\n<p align=\"center\">\n\t<strong>🍬A set of tools that keep Java sweet.</strong>\n</p>\n<p align=\"center\">\n\t👉 <a href=\"https://hutool.cn\">https://hutool.cn/</a> 👈\n</p>\n\n## 📚Hutool-core 模块介绍\n\n`Hutool-core`提供了最常使用的基础工具类，包括集合、Map、IO、线程、Bean、图片处理、线程并发等便捷工具。\n\n-------------------------------------------------------------------------------\n\n## 🛠️包含内容\n\n### 注解(annotation)\n\n提供了注解工具类，以及一些注解封装。如`CombinationAnnotationElement`组合注解以及Alias别名注解等。\n\n### bean(bean)\n\n提供了Bean工具类，以及Bean属性解析、Bean拷贝、动态Bean等。\n\n### 构建器(builder)\n\n抽象了Builder接口，提供建造者模式的封装，并默认提供了包括equals封装、Bean构建封装、比较器封装等。\n\n### 克隆(clone)\n\n提供`Cloneable`接口，明确`clone`方法，并提供默认实现类。\n\n### 编码(codec)\n\n提供了BaseN编码（Base16、Base32、Base58、Base62、Base64）编码实现。并提供了包括BCD、PunyCode、百分号编码的实现。\n同时提供了包括莫尔斯电码、凯撒密码、RotN这类有趣功能的实现。\n\n### 集合(collection)\n\n集合中主要是提供了针对`Iterator`实现类的工具封装方法`IterUtil`和集合类封装的工具类`CollUtil`，并提供了一些特别的集合封装。\n\n### 比较器(comparator)\n\n主要是一些比较器的实现，如Bean字段比较器、自定义函数比较器、版本比较器等。\n\n### 动态编译(compiler)\n\n提供`javax.tools.JavaCompiler`的包装简化服务，形成源码动态编译工具类`CompilerUtil`，完成代码动态编译及热部署。\n\n### 压缩(compress)\n\n主要针对`java.util.zip`中的相关类封装工具，提供Zip、Gzip、Zlib等格式的压缩解压缩封装，为`ZipUtil`提供服务。\n\n### 转换(convert)\n\n“万能”转换器，提供整套的类型转换方式。通过`Converter`接口和`ConverterRegistry`转换登记中心，完成任意数据类型转换和自定义转换。\n\n### 日期时间(date)\n\n提供`Date`、`Calendar`、`java.time`相关API的工具化封装。包括时间解析、格式化、偏移等。\n\n### 异常(exceptions)\n\n提供异常工具`ExceptionUtil`，以及一些工具内部使用的异常。\n\n### getter接口(getter)\n\n提供各种类型的get操作接口封装。\n\n### 图片(img)\n\n提供图片、绘图、字体等工具封装，并提供GIF生成器和解析器实现。\n\n### IO流和文件(io)\n\n提供IO流工具、文件工具、文件类型工具等，并提供流拷贝、Checksum、文件监听功能实现。\n\n### 语言特性(lang)\n\n超级大杂项，提供一些设计模式的抽象实现（如单例模式`Singleton`），还有正则、Id生成器、函数、Hash算法、可变对象、树形结构、字典等。\n\n### Map(map)\n\n提供Map工具类和各类Map实现封装，如行列键的Table实现、自定义键值对转换的Map、线程安全的WeakMap实现等。\n\n### 数学(math)\n\n提供简单数学计算封装，如排列组合、货币类等。\n\n### 网络(net)\n\n提供网络相关工具封装，以及Ip地址工具类、SSL工具类、URL编码解码等。\n\n### StreamAPI封装(stream)\n\n提供简单的Stream相关封装。\n\n### Swing和AWT(swing)\n\n提供桌面应用API的工具封装，如启动应用、控制键盘鼠标操作、截屏等功能。\n\n### 文本字符串(text)\n\n提供强大的字符串文本封装，包括字符转换、字符串查找、字符串替换、字符串切分、Unicode工具等，并提供CSV格式封装。\n\n### 线程及并发(thread)\n\n线程并发封装，包括线程工具、锁工具、`CompletableFuture`封装工具、线程池构建等。\n\n### 工具杂项(util)\n\n提供其他不便归类的杂项工具类。如数组、编码、字符、Class、坐标系、身份证、组织机构代码、脱敏、枚举、转义、XML、进制转换、随机数、反射、正则、SPI等各种工具。"
  },
  {
    "path": "hutool-core/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-core</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool核心，包括集合、字符串、Bean等工具</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.core</Automatic-Module-Name>\n\t</properties>\n</project>\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AbstractAnnotationSynthesizer.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.annotation.scanner.AnnotationScanner;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * {@link AnnotationSynthesizer}的基本实现\n *\n * @author huangchengxing\n */\npublic abstract class AbstractAnnotationSynthesizer<T> implements AnnotationSynthesizer {\n\n\t/**\n\t * 合成注解来源最初来源\n\t */\n\tprotected final T source;\n\n\t/**\n\t * 包含根注解以及其元注解在内的全部注解实例\n\t */\n\tprotected final Map<Class<? extends Annotation>, SynthesizedAnnotation> synthesizedAnnotationMap;\n\n\t/**\n\t * 已经合成过的注解对象\n\t */\n\tprivate final Map<Class<? extends Annotation>, Annotation> synthesizedProxyAnnotations;\n\n\t/**\n\t * 合成注解选择器\n\t */\n\tprotected final SynthesizedAnnotationSelector annotationSelector;\n\n\t/**\n\t * 合成注解属性处理器\n\t */\n\tprotected final Collection<SynthesizedAnnotationPostProcessor> postProcessors;\n\n\t/**\n\t * 注解扫描器\n\t */\n\tprotected final AnnotationScanner annotationScanner;\n\n\t/**\n\t * 构造一个注解合成器\n\t *\n\t * @param source                   当前查找的注解对象\n\t * @param annotationSelector       合成注解选择器\n\t * @param annotationPostProcessors 注解后置处理器\n\t * @param annotationScanner        注解扫描器，该扫描器需要支持扫描注解类\n\t */\n\tprotected AbstractAnnotationSynthesizer(\n\t\tT source,\n\t\tSynthesizedAnnotationSelector annotationSelector,\n\t\tCollection<SynthesizedAnnotationPostProcessor> annotationPostProcessors,\n\t\tAnnotationScanner annotationScanner) {\n\t\tAssert.notNull(source, \"source must not null\");\n\t\tAssert.notNull(annotationSelector, \"annotationSelector must not null\");\n\t\tAssert.notNull(annotationPostProcessors, \"annotationPostProcessors must not null\");\n\t\tAssert.notNull(annotationPostProcessors, \"annotationScanner must not null\");\n\n\t\tthis.source = source;\n\t\tthis.annotationSelector = annotationSelector;\n\t\tthis.annotationScanner = annotationScanner;\n\t\tthis.postProcessors = CollUtil.unmodifiable(\n\t\t\tCollUtil.sort(annotationPostProcessors, Comparator.comparing(SynthesizedAnnotationPostProcessor::order))\n\t\t);\n\t\tthis.synthesizedProxyAnnotations = new LinkedHashMap<>();\n\t\tthis.synthesizedAnnotationMap = MapUtil.unmodifiable(loadAnnotations());\n\t\tannotationPostProcessors.forEach(processor ->\n\t\t\tsynthesizedAnnotationMap.values().forEach(synthesized -> processor.process(synthesized, this))\n\t\t);\n\t}\n\n\t/**\n\t * 加载合成注解的必要属性\n\t *\n\t * @return 合成注解\n\t */\n\tprotected abstract Map<Class<? extends Annotation>, SynthesizedAnnotation> loadAnnotations();\n\n\t/**\n\t * 根据指定的注解类型和对应注解对象，合成最终所需的合成注解\n\t *\n\t * @param annotationType 注解类型\n\t * @param annotation     合成注解对象\n\t * @param <A>            注解类型\n\t * @return 最终所需的合成注解\n\t */\n\tprotected abstract <A extends Annotation> A synthesize(Class<A> annotationType, SynthesizedAnnotation annotation);\n\n\t/**\n\t * 获取合成注解来源最初来源\n\t *\n\t * @return 合成注解来源最初来源\n\t */\n\t@Override\n\tpublic T getSource() {\n\t\treturn source;\n\t}\n\n\t/**\n\t * 合成注解选择器\n\t *\n\t * @return 注解选择器\n\t */\n\t@Override\n\tpublic SynthesizedAnnotationSelector getAnnotationSelector() {\n\t\treturn annotationSelector;\n\t}\n\n\t/**\n\t * 获取合成注解后置处理器\n\t *\n\t * @return 合成注解后置处理器\n\t */\n\t@Override\n\tpublic Collection<SynthesizedAnnotationPostProcessor> getAnnotationPostProcessors() {\n\t\treturn postProcessors;\n\t}\n\n\t/**\n\t * 获取已合成的注解\n\t *\n\t * @param annotationType 注解类型\n\t * @return 已合成的注解\n\t */\n\t@Override\n\tpublic SynthesizedAnnotation getSynthesizedAnnotation(Class<?> annotationType) {\n\t\treturn synthesizedAnnotationMap.get(annotationType);\n\t}\n\n\t/**\n\t * 获取全部的合成注解\n\t *\n\t * @return 合成注解\n\t */\n\t@Override\n\tpublic Map<Class<? extends Annotation>, SynthesizedAnnotation> getAllSynthesizedAnnotation() {\n\t\treturn synthesizedAnnotationMap;\n\t}\n\n\t/**\n\t * 获取合成注解\n\t *\n\t * @param annotationType 注解类型\n\t * @param <A>            注解类型\n\t * @return 类型\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic <A extends Annotation> A synthesize(Class<A> annotationType) {\n\t\tA annotation = (A)synthesizedProxyAnnotations.get(annotationType);\n\t\tif (Objects.nonNull(annotation)) {\n\t\t\treturn annotation;\n\t\t}\n\t\tsynchronized (synthesizedProxyAnnotations) {\n\t\t\tannotation = (A)synthesizedProxyAnnotations.get(annotationType);\n\t\t\tif (Objects.isNull(annotation)) {\n\t\t\t\tfinal SynthesizedAnnotation synthesizedAnnotation = synthesizedAnnotationMap.get(annotationType);\n\t\t\t\tannotation = synthesize(annotationType, synthesizedAnnotation);\n\t\t\t\tsynthesizedProxyAnnotations.put(annotationType, annotation);\n\t\t\t}\n\t\t}\n\t\treturn annotation;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AbstractLinkAnnotationPostProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * {@link SynthesizedAnnotationPostProcessor}的基本实现，\n * 用于处理注解中带有{@link Link}注解的属性。\n *\n * @author huangchengxing\n * @see MirrorLinkAnnotationPostProcessor\n * @see AliasLinkAnnotationPostProcessor\n */\npublic abstract class AbstractLinkAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor {\n\n\t/**\n\t * 若一个注解属性上存在{@link Link}注解，注解的{@link Link#type()}返回值在{@link #processTypes()}中存在，\n\t * 且此{@link Link}指定的注解对象在当前的{@link SynthesizedAggregateAnnotation}中存在，\n\t * 则从聚合器中获取类型对应的合成注解对象，与该对象中的指定属性，然后将全部关联数据交给\n\t * {@link #processLinkedAttribute}处理。\n\t *\n\t * @param synthesizedAnnotation 合成的注解\n\t * @param synthesizer           合成注解聚合器\n\t */\n\t@Override\n\tpublic void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) {\n\t\tfinal Map<String, AnnotationAttribute> attributeMap = new HashMap<>(synthesizedAnnotation.getAttributes());\n\t\tattributeMap.forEach((originalAttributeName, originalAttribute) -> {\n\t\t\t// 获取注解\n\t\t\tfinal Link link = getLinkAnnotation(originalAttribute, processTypes());\n\t\t\tif (ObjectUtil.isNull(link)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// 获取注解属性\n\t\t\tfinal SynthesizedAnnotation linkedAnnotation = getLinkedAnnotation(link, synthesizer, synthesizedAnnotation.annotationType());\n\t\t\tif (ObjectUtil.isNull(linkedAnnotation)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfinal AnnotationAttribute linkedAttribute = linkedAnnotation.getAttributes().get(link.attribute());\n\t\t\t// 处理\n\t\t\tprocessLinkedAttribute(\n\t\t\t\t\tsynthesizer, link,\n\t\t\t\t\tsynthesizedAnnotation, synthesizedAnnotation.getAttributes().get(originalAttributeName),\n\t\t\t\t\tlinkedAnnotation, linkedAttribute\n\t\t\t);\n\t\t});\n\t}\n\n\t// =========================== 抽象方法 ===========================\n\n\t/**\n\t * 当属性上存在{@link Link}注解时，仅当{@link Link#type()}在本方法返回值内存在时才进行处理\n\t *\n\t * @return 支持处理的{@link RelationType}类型\n\t */\n\tprotected abstract RelationType[] processTypes();\n\n\t/**\n\t * 对关联的合成注解对象及其关联属性的处理\n\t *\n\t * @param synthesizer        注解合成器\n\t * @param annotation         {@code originalAttribute}上的{@link Link}注解对象\n\t * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象\n\t * @param originalAttribute  {@code originalAnnotation}上的待处理的属性\n\t * @param linkedAnnotation   {@link Link}指向的关联注解对象\n\t * @param linkedAttribute    {@link Link}指向的{@code originalAnnotation}中的关联属性，该参数可能为空\n\t */\n\tprotected abstract void processLinkedAttribute(\n\t\t\tAnnotationSynthesizer synthesizer, Link annotation,\n\t\t\tSynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute,\n\t\t\tSynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute\n\t);\n\n\t// =========================== @Link注解的处理 ===========================\n\n\t/**\n\t * 从注解属性上获取指定类型的{@link Link}注解\n\t *\n\t * @param attribute     注解属性\n\t * @param relationTypes 类型\n\t * @return 注解\n\t */\n\tprotected Link getLinkAnnotation(AnnotationAttribute attribute, RelationType... relationTypes) {\n\t\treturn Opt.ofNullable(attribute)\n\t\t\t\t.map(t -> AnnotationUtil.getSynthesizedAnnotation(attribute.getAttribute(), Link.class))\n\t\t\t\t.filter(a -> ArrayUtil.contains(relationTypes, a.type()))\n\t\t\t\t.get();\n\t}\n\n\t/**\n\t * 从合成注解中获取{@link Link#type()}指定的注解对象\n\t *\n\t * @param annotation  {@link Link}注解\n\t * @param synthesizer 注解合成器\n\t * @param defaultType 默认类型\n\t * @return {@link SynthesizedAnnotation}\n\t */\n\tprotected SynthesizedAnnotation getLinkedAnnotation(Link annotation, AnnotationSynthesizer synthesizer, Class<? extends Annotation> defaultType) {\n\t\tfinal Class<?> targetAnnotationType = getLinkedAnnotationType(annotation, defaultType);\n\t\treturn synthesizer.getSynthesizedAnnotation(targetAnnotationType);\n\t}\n\n\t/**\n\t * 若{@link Link#annotation()}获取的类型{@code Annotation#getClass()}，则返回{@code defaultType}，\n\t * 否则返回{@link Link#annotation()}指定的类型\n\t *\n\t * @param annotation  {@link Link}注解\n\t * @param defaultType 默认注解类型\n\t * @return 注解类型\n\t */\n\tprotected Class<?> getLinkedAnnotationType(Link annotation, Class<?> defaultType) {\n\t\treturn ObjectUtil.equals(annotation.annotation(), Annotation.class) ?\n\t\t\t\tdefaultType : annotation.annotation();\n\t}\n\n\t// =========================== 注解属性的校验 ===========================\n\n\t/**\n\t * 校验两个注解属性的返回值类型是否一致\n\t *\n\t * @param original 原属性\n\t * @param alias    别名属性\n\t */\n\tprotected void checkAttributeType(AnnotationAttribute original, AnnotationAttribute alias) {\n\t\tAssert.equals(\n\t\t\t\toriginal.getAttributeType(), alias.getAttributeType(),\n\t\t\t\t\"return type of the linked attribute [{}] is inconsistent with the original [{}]\",\n\t\t\t\toriginal.getAttribute(), alias.getAttribute()\n\t\t);\n\t}\n\n\t/**\n\t * 检查{@link Link}指向的注解属性是否就是本身\n\t *\n\t * @param original {@link Link}注解的属性\n\t * @param linked   {@link Link}指向的注解属性\n\t */\n\tprotected void checkLinkedSelf(AnnotationAttribute original, AnnotationAttribute linked) {\n\t\tboolean linkSelf = (original == linked) || ObjectUtil.equals(original.getAttribute(), linked.getAttribute());\n\t\tAssert.isFalse(linkSelf, \"cannot link self [{}]\", original.getAttribute());\n\t}\n\n\t/**\n\t * 检查{@link Link}指向的注解属性是否存在\n\t *\n\t * @param original        {@link Link}注解的属性\n\t * @param linkedAttribute {@link Link}指向的注解属性\n\t * @param annotation      {@link Link}注解\n\t */\n\tprotected void checkLinkedAttributeNotNull(AnnotationAttribute original, AnnotationAttribute linkedAttribute, Link annotation) {\n\t\tAssert.notNull(linkedAttribute, \"cannot find linked attribute [{}] of original [{}] in [{}]\",\n\t\t\t\toriginal.getAttribute(), annotation.attribute(),\n\t\t\t\tgetLinkedAnnotationType(annotation, original.getAnnotationType())\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * {@link WrappedAnnotationAttribute}的基本实现\n *\n * @author huangchengxing\n * @see ForceAliasedAnnotationAttribute\n * @see AliasedAnnotationAttribute\n * @see MirroredAnnotationAttribute\n */\npublic abstract class AbstractWrappedAnnotationAttribute implements WrappedAnnotationAttribute {\n\n\tprotected final AnnotationAttribute original;\n\tprotected final AnnotationAttribute linked;\n\n\tprotected AbstractWrappedAnnotationAttribute(AnnotationAttribute original, AnnotationAttribute linked) {\n\t\tAssert.notNull(original, \"target must not null\");\n\t\tAssert.notNull(linked, \"linked must not null\");\n\t\tthis.original = original;\n\t\tthis.linked = linked;\n\t}\n\n\t@Override\n\tpublic AnnotationAttribute getOriginal() {\n\t\treturn original;\n\t}\n\n\t@Override\n\tpublic AnnotationAttribute getLinked() {\n\t\treturn linked;\n\t}\n\n\t@Override\n\tpublic AnnotationAttribute getNonWrappedOriginal() {\n\t\tAnnotationAttribute curr = null;\n\t\tAnnotationAttribute next = original;\n\t\twhile (next != null) {\n\t\t\tcurr = next;\n\t\t\tnext = next.isWrapped() ? ((WrappedAnnotationAttribute)curr).getOriginal() : null;\n\t\t}\n\t\treturn curr;\n\t}\n\n\t@Override\n\tpublic Collection<AnnotationAttribute> getAllLinkedNonWrappedAttributes() {\n\t\tList<AnnotationAttribute> leafAttributes = new ArrayList<>();\n\t\tcollectLeafAttribute(this, leafAttributes);\n\t\treturn leafAttributes;\n\t}\n\n\tprivate void collectLeafAttribute(AnnotationAttribute curr, List<AnnotationAttribute> leafAttributes) {\n\t\tif (ObjectUtil.isNull(curr)) {\n\t\t\treturn;\n\t\t}\n\t\tif (!curr.isWrapped()) {\n\t\t\tleafAttributes.add(curr);\n\t\t\treturn;\n\t\t}\n\t\tWrappedAnnotationAttribute wrappedAttribute = (WrappedAnnotationAttribute)curr;\n\t\tcollectLeafAttribute(wrappedAttribute.getOriginal(), leafAttributes);\n\t\tcollectLeafAttribute(wrappedAttribute.getLinked(), leafAttributes);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AggregateAnnotation.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Annotation;\n\n/**\n * 表示一组被聚合在一起的注解对象\n *\n * @author huangchengxing\n */\npublic interface AggregateAnnotation extends Annotation {\n\n\t/**\n\t * 在聚合中是否存在的指定类型注解对象\n\t *\n\t * @param annotationType 注解类型\n\t * @return 是否\n\t */\n\tboolean isAnnotationPresent(Class<? extends Annotation> annotationType);\n\n\t/**\n\t * 获取聚合中的全部注解对象\n\t *\n\t * @return 注解对象\n\t */\n\tAnnotation[] getAnnotations();\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/Alias.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 别名注解，使用此注解的字段、方法、参数等会有一个别名，用于Bean拷贝、Bean转Map等\n *\n * @author Looly\n * @since 5.1.1\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})\npublic @interface Alias {\n\n\t/**\n\t * 别名值，即使用此注解要替换成的别名名称\n\t *\n\t * @return 别名值\n\t */\n\tString value();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AliasAnnotationPostProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.map.ForestMap;\nimport cn.hutool.core.map.LinkedForestMap;\nimport cn.hutool.core.map.TreeEntry;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Map;\n\n/**\n * <p>用于处理注解对象中带有{@link Alias}注解的属性。<br>\n * 当该处理器执行完毕后，{@link Alias}注解指向的目标注解的属性将会被包装并替换为\n * {@link ForceAliasedAnnotationAttribute}。\n *\n * @author huangchengxing\n * @see Alias\n * @see ForceAliasedAnnotationAttribute\n */\npublic class AliasAnnotationPostProcessor implements SynthesizedAnnotationPostProcessor {\n\n\t@Override\n\tpublic int order() {\n\t\treturn Integer.MIN_VALUE;\n\t}\n\n\t@Override\n\tpublic void process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer) {\n\t\tfinal Map<String, AnnotationAttribute> attributeMap = synthesizedAnnotation.getAttributes();\n\n\t\t// 记录别名与属性的关系\n\t\tfinal ForestMap<String, AnnotationAttribute> attributeAliasMappings = new LinkedForestMap<>(false);\n\t\tattributeMap.forEach((attributeName, attribute) -> {\n\t\t\tfinal String alias = Opt.ofNullable(attribute.getAnnotation(Alias.class))\n\t\t\t\t.map(Alias::value)\n\t\t\t\t.orElse(null);\n\t\t\tif (ObjectUtil.isNull(alias)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfinal AnnotationAttribute aliasAttribute = attributeMap.get(alias);\n\t\t\tAssert.notNull(aliasAttribute, \"no method for alias: [{}]\", alias);\n\t\t\tattributeAliasMappings.putLinkedNodes(alias, aliasAttribute, attributeName, attribute);\n\t\t});\n\n\t\t// 处理别名\n\t\tattributeMap.forEach((attributeName, attribute) -> {\n\t\t\tfinal AnnotationAttribute resolvedAttribute = Opt.ofNullable(attributeName)\n\t\t\t\t.map(attributeAliasMappings::getRootNode)\n\t\t\t\t.map(TreeEntry::getValue)\n\t\t\t\t.orElse(attribute);\n\t\t\tAssert.isTrue(\n\t\t\t\tObjectUtil.isNull(resolvedAttribute)\n\t\t\t\t\t|| ClassUtil.isAssignable(attribute.getAttributeType(), resolvedAttribute.getAttributeType()),\n\t\t\t\t\"return type of the root alias method [{}] is inconsistent with the original [{}]\",\n\t\t\t\tresolvedAttribute.getClass(), attribute.getAttributeType()\n\t\t\t);\n\t\t\tif (attribute != resolvedAttribute) {\n\t\t\t\tattributeMap.put(attributeName, new ForceAliasedAnnotationAttribute(attribute, resolvedAttribute));\n\t\t\t}\n\t\t});\n\t\tsynthesizedAnnotation.setAttributes(attributeMap);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AliasFor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * <p>{@link Link}的子注解。表示“原始属性”将作为“关联属性”的别名。\n * <ul>\n *     <li>当“原始属性”为默认值时，获取“关联属性”将返回“关联属性”本身的值；</li>\n *     <li>当“原始属性”不为默认值时，获取“关联属性”将返回“原始属性”的值；</li>\n * </ul>\n * <b>注意，该注解与{@link Link}、{@link ForceAliasFor}或{@link MirrorFor}一起使用时，将只有被声明在最上面的注解会生效</b>\n *\n * @author huangchengxing\n * @see Link\n * @see RelationType#ALIAS_FOR\n */\n@Link(type = RelationType.ALIAS_FOR)\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})\npublic @interface AliasFor {\n\n\t/**\n\t * 产生关联的注解类型，当不指定时，默认指注释的属性所在的类\n\t *\n\t * @return 注解类型\n\t */\n\t@Link(annotation = Link.class, attribute = \"annotation\", type = RelationType.FORCE_ALIAS_FOR)\n\tClass<? extends Annotation> annotation() default Annotation.class;\n\n\t/**\n\t * {@link #annotation()}指定注解中关联的属性\n\t *\n\t * @return 关联属性\n\t */\n\t@Link(annotation = Link.class, attribute = \"attribute\", type = RelationType.FORCE_ALIAS_FOR)\n\tString attribute() default \"\";\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.function.BinaryOperator;\n\n/**\n * <p>用于处理注解对象中带有{@link Link}注解，且{@link Link#type()}为\n * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR}的属性。<br>\n * 当该处理器执行完毕后，{@link Link}注解指向的目标注解的属性将会被包装并替换为\n * {@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}。\n *\n * @author huangchengxing\n * @see RelationType#ALIAS_FOR\n * @see AliasedAnnotationAttribute\n * @see RelationType#FORCE_ALIAS_FOR\n * @see ForceAliasedAnnotationAttribute\n */\npublic class AliasLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor {\n\n\tprivate static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR };\n\n\t@Override\n\tpublic int order() {\n\t\treturn Integer.MIN_VALUE + 2;\n\t}\n\n\t/**\n\t * 该处理器只处理{@link Link#type()}类型为{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的注解属性\n\t *\n\t * @return 含有{@link RelationType#ALIAS_FOR}和{@link RelationType#FORCE_ALIAS_FOR}的数组\n\t */\n\t@Override\n\tprotected RelationType[] processTypes() {\n\t\treturn PROCESSED_RELATION_TYPES;\n\t}\n\n\t/**\n\t * 获取{@link Link}指向的目标注解属性，并根据{@link Link#type()}的类型是\n\t * {@link RelationType#ALIAS_FOR}或{@link RelationType#FORCE_ALIAS_FOR}\n\t * 将目标注解属性包装为{@link AliasedAnnotationAttribute}或{@link ForceAliasedAnnotationAttribute}，\n\t * 然后用包装后注解属性在对应的合成注解中替换原始的目标注解属性\n\t *\n\t * @param synthesizer        注解合成器\n\t * @param annotation         {@code originalAttribute}上的{@link Link}注解对象\n\t * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象\n\t * @param originalAttribute  {@code originalAnnotation}上的待处理的属性\n\t * @param linkedAnnotation   {@link Link}指向的关联注解对象\n\t * @param linkedAttribute    {@link Link}指向的{@code originalAnnotation}中的关联属性，该参数可能为空\n\t */\n\t@Override\n\tprotected void processLinkedAttribute(\n\t\tAnnotationSynthesizer synthesizer, Link annotation,\n\t\tSynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute,\n\t\tSynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) {\n\t\t// 校验别名关系\n\t\tcheckAliasRelation(annotation, originalAttribute, linkedAttribute);\n\t\t// 处理aliasFor类型的关系\n\t\tif (RelationType.ALIAS_FOR.equals(annotation.type())) {\n\t\t\twrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, AliasedAnnotationAttribute::new);\n\t\t\treturn;\n\t\t}\n\t\t// 处理forceAliasFor类型的关系\n\t\twrappingLinkedAttribute(synthesizer, originalAttribute, linkedAttribute, ForceAliasedAnnotationAttribute::new);\n\t}\n\n\t/**\n\t * 对指定注解属性进行包装，若该属性已被包装过，则递归以其为根节点的树结构，对树上全部的叶子节点进行包装\n\t */\n\tprivate void wrappingLinkedAttribute(\n\t\tAnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute, AnnotationAttribute aliasAttribute, BinaryOperator<AnnotationAttribute> wrapping) {\n\t\t// 不是包装属性\n\t\tif (!aliasAttribute.isWrapped()) {\n\t\t\tprocessAttribute(synthesizer, originalAttribute, aliasAttribute, wrapping);\n\t\t\treturn;\n\t\t}\n\t\t// 是包装属性\n\t\tfinal AbstractWrappedAnnotationAttribute wrapper = (AbstractWrappedAnnotationAttribute)aliasAttribute;\n\t\twrapper.getAllLinkedNonWrappedAttributes().forEach(\n\t\t\tt -> processAttribute(synthesizer, originalAttribute, t, wrapping)\n\t\t);\n\t}\n\n\t/**\n\t * 获取指定注解属性，然后将其再进行一层包装\n\t */\n\tprivate void processAttribute(\n\t\tAnnotationSynthesizer synthesizer, AnnotationAttribute originalAttribute,\n\t\tAnnotationAttribute target, BinaryOperator<AnnotationAttribute> wrapping) {\n\t\tOpt.ofNullable(target.getAnnotationType())\n\t\t\t.map(synthesizer::getSynthesizedAnnotation)\n\t\t\t.ifPresent(t -> t.replaceAttribute(target.getAttributeName(), old -> wrapping.apply(old, originalAttribute)));\n\t}\n\n\t/**\n\t * 基本校验\n\t */\n\tprivate void checkAliasRelation(Link annotation, AnnotationAttribute originalAttribute, AnnotationAttribute linkedAttribute) {\n\t\tcheckLinkedAttributeNotNull(originalAttribute, linkedAttribute, annotation);\n\t\tcheckAttributeType(originalAttribute, linkedAttribute);\n\t\tcheckCircularDependency(originalAttribute, linkedAttribute);\n\t}\n\n\t/**\n\t * 检查两个属性是否互为别名\n\t */\n\tprivate void checkCircularDependency(AnnotationAttribute original, AnnotationAttribute alias) {\n\t\tcheckLinkedSelf(original, alias);\n\t\tLink annotation = getLinkAnnotation(alias, RelationType.ALIAS_FOR, RelationType.FORCE_ALIAS_FOR);\n\t\tif (ObjectUtil.isNull(annotation)) {\n\t\t\treturn;\n\t\t}\n\t\tfinal Class<?> aliasAnnotationType = getLinkedAnnotationType(annotation, alias.getAnnotationType());\n\t\tif (ObjectUtil.notEqual(aliasAnnotationType, original.getAnnotationType())) {\n\t\t\treturn;\n\t\t}\n\t\tAssert.notEquals(\n\t\t\tannotation.attribute(), original.getAttributeName(),\n\t\t\t\"circular reference between the alias attribute [{}] and the original attribute [{}]\",\n\t\t\talias.getAttribute(), original.getAttribute()\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AliasedAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\n/**\n * <p>表示一个具有别名的属性。\n * 当别名属性值为默认值时，优先返回原属性的值，当别名属性不为默认值时，优先返回别名属性的值\n *\n * @author huangchengxing\n * @see AliasLinkAnnotationPostProcessor\n * @see RelationType#ALIAS_FOR\n */\npublic class AliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute {\n\n\tprotected AliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {\n\t\tsuper(origin, linked);\n\t}\n\n\t/**\n\t * 若{@link #linked}为默认值，则返回{@link #original}的值，否则返回{@link #linked}的值\n\t *\n\t * @return 属性值\n\t */\n\t@Override\n\tpublic Object getValue() {\n\t\treturn linked.isValueEquivalentToDefaultValue() ? super.getValue() : linked.getValue();\n\t}\n\n\t/**\n\t * 当{@link #original}与{@link #linked}都为默认值时返回{@code true}\n\t *\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean isValueEquivalentToDefaultValue() {\n\t\treturn linked.isValueEquivalentToDefaultValue() && original.isValueEquivalentToDefaultValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\n\n/**\n * <p>表示注解的某个属性，等同于绑定的调用对象的{@link Method}方法。<br>\n * 在{@link SynthesizedAggregateAnnotation}的解析以及取值过程中，\n * 可以通过设置{@link SynthesizedAnnotation}的注解属性，\n * 从而使得可以从一个注解对象中属性获取另一个注解对象的属性值\n *\n * <p>一般情况下，注解属性的处理会发生在{@link SynthesizedAnnotationPostProcessor}调用时\n *\n * @author huangchengxing\n * @see SynthesizedAnnotationPostProcessor\n * @see WrappedAnnotationAttribute\n * @see CacheableAnnotationAttribute\n * @see AbstractWrappedAnnotationAttribute\n * @see ForceAliasedAnnotationAttribute\n * @see AliasedAnnotationAttribute\n * @see MirroredAnnotationAttribute\n */\npublic interface AnnotationAttribute {\n\n\t/**\n\t * 获取注解对象\n\t *\n\t * @return 注解对象\n\t */\n\tAnnotation getAnnotation();\n\n\t/**\n\t * 获取注解属性对应的方法\n\t *\n\t * @return 注解属性对应的方法\n\t */\n\tMethod getAttribute();\n\n\t/**\n\t * 获取声明属性的注解类\n\t *\n\t * @return 声明注解的注解类\n\t */\n\tdefault Class<?> getAnnotationType() {\n\t\treturn getAttribute().getDeclaringClass();\n\t}\n\n\t/**\n\t * 获取属性名称\n\t *\n\t * @return 属性名称\n\t */\n\tdefault String getAttributeName() {\n\t\treturn getAttribute().getName();\n\t}\n\n\t/**\n\t * 获取注解属性\n\t *\n\t * @return 注解属性\n\t */\n\tdefault Object getValue() {\n\t\treturn ReflectUtil.invoke(getAnnotation(), getAttribute());\n\t}\n\n\t/**\n\t * 该注解属性的值是否等于默认值\n\t *\n\t * @return 该注解属性的值是否等于默认值\n\t */\n\tboolean isValueEquivalentToDefaultValue();\n\n\t/**\n\t * 获取属性类型\n\t *\n\t * @return 属性类型\n\t */\n\tdefault Class<?> getAttributeType() {\n\t\treturn getAttribute().getReturnType();\n\t}\n\n\t/**\n\t * 获取属性上的注解\n\t *\n\t * @param <T> 注解类型\n\t * @param annotationType 注解类型\n\t * @return 注解对象\n\t */\n\tdefault <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\treturn getAttribute().getAnnotation(annotationType);\n\t}\n\n\t/**\n\t * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装\n\t *\n\t * @return boolean\n\t */\n\tdefault boolean isWrapped() {\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationAttributeValueProvider.java",
    "content": "package cn.hutool.core.annotation;\n\n/**\n * 表示一个可以从当前接口的实现类中，获得特定的属性值\n */\n@FunctionalInterface\npublic interface AnnotationAttributeValueProvider {\n\n\t/**\n\t * 获取注解属性值\n\t *\n\t * @param attributeName  属性名称\n\t * @param attributeType  属性类型\n\t * @return 注解属性值\n\t */\n\tObject getAttributeValue(String attributeName, Class<?> attributeType);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationProxy.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 注解代理<br>\n * 通过代理指定注解，可以自定义调用注解的方法逻辑，如支持{@link Alias} 注解\n *\n * @param <T> 注解类型\n * @since 5.7.23\n */\npublic class AnnotationProxy<T extends Annotation> implements Annotation, InvocationHandler, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final T annotation;\n\tprivate final Class<T> type;\n\tprivate final Map<String, Object> attributes;\n\n\t/**\n\t * 构造\n\t *\n\t * @param annotation 注解\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic AnnotationProxy(T annotation) {\n\t\tthis.annotation = annotation;\n\t\tthis.type = (Class<T>) annotation.annotationType();\n\t\tthis.attributes = initAttributes();\n\t}\n\n\n\t@Override\n\tpublic Class<? extends Annotation> annotationType() {\n\t\treturn type;\n\t}\n\n\t@Override\n\tpublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n\n\t\t// 注解别名\n\t\tAlias alias = method.getAnnotation(Alias.class);\n\t\tif(null != alias){\n\t\t\tfinal String name = alias.value();\n\t\t\tif(StrUtil.isNotBlank(name)){\n\t\t\t\tif(false == attributes.containsKey(name)){\n\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"No method for alias: [{}]\", name));\n\t\t\t\t}\n\t\t\t\treturn attributes.get(name);\n\t\t\t}\n\t\t}\n\n\t\tfinal Object value = attributes.get(method.getName());\n\t\tif (value != null) {\n\t\t\treturn value;\n\t\t}\n\t\treturn method.invoke(this, args);\n\t}\n\n\t/**\n\t * 初始化注解的属性<br>\n\t * 此方法预先调用所有注解的方法，将注解方法值缓存于attributes中\n\t *\n\t * @return 属性（方法结果）映射\n\t */\n\tprivate Map<String, Object> initAttributes() {\n\t\tfinal Method[] methods = ReflectUtil.getMethods(this.type);\n\t\tfinal Map<String, Object> attributes = new HashMap<>(methods.length, 1);\n\n\t\tfor (Method method : methods) {\n\t\t\t// 跳过匿名内部类自动生成的方法\n\t\t\tif (method.isSynthetic()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattributes.put(method.getName(), ReflectUtil.invoke(this.annotation, method));\n\t\t}\n\n\t\treturn attributes;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationSynthesizer.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Annotation;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * <p>注解合成器，用于处理一组给定的与{@link #getSource()}具有直接或间接联系的注解对象，\n * 并返回与原始注解对象具有不同属性的“合成”注解。\n *\n * <p>合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象，\n * 当实例被创建时，会获取到这些注解对象，并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤，\n * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}，\n * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。<br>\n * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子，\n * 自定义选择器以拦截原始注解被扫描的过程。\n *\n * <p>当合成注解完成对待合成注解的扫描，并完成了必要属性的加载后，\n * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}，\n * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整，\n * 该钩子一般用于根据{@link Link}注解对属性进行调整。<br>\n * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子，\n * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。\n *\n * <p>使用{@link #synthesize(Class)}用于获取“合成”后的注解，\n * 该注解对象的属性可能会与原始的对象属性不同。\n *\n * @author huangchengxing\n */\npublic interface AnnotationSynthesizer {\n\n\t/**\n\t * 获取合成注解来源最初来源\n\t *\n\t * @return 合成注解来源最初来源\n\t */\n\tObject getSource();\n\n\t/**\n\t * 合成注解选择器\n\t *\n\t * @return 注解选择器\n\t */\n\tSynthesizedAnnotationSelector getAnnotationSelector();\n\n\t/**\n\t * 获取合成注解后置处理器\n\t *\n\t * @return 合成注解后置处理器\n\t */\n\tCollection<SynthesizedAnnotationPostProcessor> getAnnotationPostProcessors();\n\n\t/**\n\t * 获取已合成的注解\n\t *\n\t * @param annotationType 注解类型\n\t * @return 已合成的注解\n\t */\n\tSynthesizedAnnotation getSynthesizedAnnotation(Class<?> annotationType);\n\n\t/**\n\t * 获取全部的合成注解\n\t *\n\t * @return 合成注解\n\t */\n\tMap<Class<? extends Annotation>, SynthesizedAnnotation> getAllSynthesizedAnnotation();\n\n\t/**\n\t * 获取合成注解\n\t *\n\t * @param annotationType 注解类型\n\t * @param <T>            注解类型\n\t * @return 类型\n\t */\n\t<T extends Annotation> T synthesize(Class<T> annotationType);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.annotation.scanner.AnnotationScanner;\nimport cn.hutool.core.annotation.scanner.MetaAnnotationScanner;\nimport cn.hutool.core.annotation.scanner.MethodAnnotationScanner;\nimport cn.hutool.core.annotation.scanner.TypeAnnotationScanner;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.func.LambdaUtil;\nimport cn.hutool.core.util.*;\n\nimport java.lang.annotation.*;\nimport java.lang.invoke.SerializedLambda;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.*;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\n/**\n * 注解工具类<br>\n * 快速获取注解对象、注解值等工具封装\n *\n * @author looly\n * @since 4.0.9\n */\npublic class AnnotationUtil {\n\n\t/**\n\t * 元注解\n\t */\n\tstatic final Set<Class<? extends Annotation>> META_ANNOTATIONS = CollUtil.newHashSet(Target.class, //\n\t\t\tRetention.class, //\n\t\t\tInherited.class, //\n\t\t\tDocumented.class, //\n\t\t\tSuppressWarnings.class, //\n\t\t\tOverride.class, //\n\t\t\tDeprecated.class//\n\t);\n\n\t/**\n\t * 是否为Jdk自带的元注解。<br>\n\t * 包括：\n\t * <ul>\n\t *     <li>{@link Target}</li>\n\t *     <li>{@link Retention}</li>\n\t *     <li>{@link Inherited}</li>\n\t *     <li>{@link Documented}</li>\n\t *     <li>{@link SuppressWarnings}</li>\n\t *     <li>{@link Override}</li>\n\t *     <li>{@link Deprecated}</li>\n\t * </ul>\n\t *\n\t * @param annotationType 注解类型\n\t * @return 是否为Jdk自带的元注解\n\t */\n\tpublic static boolean isJdkMetaAnnotation(Class<? extends Annotation> annotationType) {\n\t\treturn META_ANNOTATIONS.contains(annotationType);\n\t}\n\n\t/**\n\t * 是否不为Jdk自带的元注解。<br>\n\t * 包括：\n\t * <ul>\n\t *     <li>{@link Target}</li>\n\t *     <li>{@link Retention}</li>\n\t *     <li>{@link Inherited}</li>\n\t *     <li>{@link Documented}</li>\n\t *     <li>{@link SuppressWarnings}</li>\n\t *     <li>{@link Override}</li>\n\t *     <li>{@link Deprecated}</li>\n\t * </ul>\n\t *\n\t * @param annotationType 注解类型\n\t * @return 是否为Jdk自带的元注解\n\t */\n\tpublic static boolean isNotJdkMateAnnotation(Class<? extends Annotation> annotationType) {\n\t\treturn false == isJdkMetaAnnotation(annotationType);\n\t}\n\n\t/**\n\t * 将指定的被注解的元素转换为组合注解元素\n\t *\n\t * @param annotationEle 注解元素\n\t * @return 组合注解元素\n\t */\n\tpublic static CombinationAnnotationElement toCombination(AnnotatedElement annotationEle) {\n\t\tif (annotationEle instanceof CombinationAnnotationElement) {\n\t\t\treturn (CombinationAnnotationElement) annotationEle;\n\t\t}\n\t\treturn new CombinationAnnotationElement(annotationEle);\n\t}\n\n\t/**\n\t * 获取指定注解\n\t *\n\t * @param annotationEle   {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param isToCombination 是否为转换为组合注解，组合注解可以递归获取注解的注解\n\t * @return 注解对象\n\t */\n\tpublic static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination) {\n\t\treturn getAnnotations(annotationEle, isToCombination, (Predicate<Annotation>) null);\n\t}\n\n\t/**\n\t * 获取组合注解\n\t *\n\t * @param <T>            注解类型\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 限定的\n\t * @return 注解对象数组\n\t * @since 5.8.0\n\t */\n\tpublic static <T> T[] getCombinationAnnotations(AnnotatedElement annotationEle, Class<T> annotationType) {\n\t\treturn getAnnotations(annotationEle, true, annotationType);\n\t}\n\n\t/**\n\t * 获取指定注解\n\t *\n\t * @param <T>             注解类型\n\t * @param annotationEle   {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param isToCombination 是否为转换为组合注解，组合注解可以递归获取注解的注解\n\t * @param annotationType  限定的\n\t * @return 注解对象数组\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Class<T> annotationType) {\n\t\tfinal Annotation[] annotations = getAnnotations(annotationEle, isToCombination,\n\t\t\t\t(annotation -> null == annotationType || annotationType.isAssignableFrom(annotation.getClass())));\n\n\t\tfinal T[] result = ArrayUtil.newArray(annotationType, annotations.length);\n\t\tfor (int i = 0; i < annotations.length; i++) {\n\t\t\tresult[i] = (T) annotations[i];\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取指定注解\n\t *\n\t * @param annotationEle   {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param isToCombination 是否为转换为组合注解，组合注解可以递归获取注解的注解\n\t * @param predicate       过滤器，{@link Predicate#test(Object)}返回{@code true}保留，否则不保留\n\t * @return 注解对象，如果提供的{@link AnnotatedElement}为{@code null}，返回{@code null}\n\t * @since 5.8.0\n\t */\n\tpublic static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isToCombination, Predicate<Annotation> predicate) {\n\t\tif (null == annotationEle) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (isToCombination) {\n\t\t\tif (null == predicate) {\n\t\t\t\treturn toCombination(annotationEle).getAnnotations();\n\t\t\t}\n\t\t\treturn CombinationAnnotationElement.of(annotationEle, predicate).getAnnotations();\n\t\t}\n\n\t\tfinal Annotation[] result = annotationEle.getAnnotations();\n\t\tif (null == predicate) {\n\t\t\treturn result;\n\t\t}\n\t\treturn ArrayUtil.filter(result, predicate::test);\n\t}\n\n\t/**\n\t * 获取指定注解\n\t *\n\t * @param <A>            注解类型\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类型\n\t * @return 注解对象\n\t */\n\tpublic static <A extends Annotation> A getAnnotation(AnnotatedElement annotationEle, Class<A> annotationType) {\n\t\treturn (null == annotationEle) ? null : toCombination(annotationEle).getAnnotation(annotationType);\n\t}\n\n\t/**\n\t * 检查是否包含指定注解指定注解\n\t *\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类型\n\t * @return 是否包含指定注解\n\t * @since 5.4.2\n\t */\n\tpublic static boolean hasAnnotation(AnnotatedElement annotationEle, Class<? extends Annotation> annotationType) {\n\t\treturn null != getAnnotation(annotationEle, annotationType);\n\t}\n\n\t/**\n\t * 检查是否包含指定注解<br>\n\t * 注解类传入全名，通过{@link Class#forName(String)}加载，避免不存在的注解导致的ClassNotFoundException\n\t *\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationTypeName 注解类型完整类名\n\t * @return 是否包含指定注解\n\t * @since 5.8.37\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic static boolean hasAnnotation(final AnnotatedElement annotationEle, final String annotationTypeName) {\n\t\tClass aClass = null;\n\t\ttry {\n\t\t\t// issue#IB0JP5，Android可能无这个类\n\t\t\taClass = Class.forName(annotationTypeName);\n\t\t} catch (final ClassNotFoundException e) {\n\t\t\t// ignore\n\t\t}\n\t\tif(null != aClass){\n\t\t\treturn hasAnnotation(annotationEle, aClass);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取指定注解默认值<br>\n\t * 如果无指定的属性方法返回null\n\t *\n\t * @param <T>            注解值类型\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类型\n\t * @return 注解对象\n\t * @throws UtilException 调用注解中的方法时执行异常\n\t */\n\tpublic static <T> T getAnnotationValue(AnnotatedElement annotationEle, Class<? extends Annotation> annotationType) throws UtilException {\n\t\treturn getAnnotationValue(annotationEle, annotationType, \"value\");\n\t}\n\n\t/**\n\t * 获取指定注解属性的值<br>\n\t * 如果无指定的属性方法返回null\n\t *\n\t * @param <T>            注解值类型\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类型\n\t * @param propertyName   属性名，例如注解中定义了name()方法，则 此处传入name\n\t * @return 注解对象\n\t * @throws UtilException 调用注解中的方法时执行异常\n\t */\n\tpublic static <T> T getAnnotationValue(AnnotatedElement annotationEle, Class<? extends Annotation> annotationType, String propertyName) throws UtilException {\n\t\tfinal Annotation annotation = getAnnotation(annotationEle, annotationType);\n\t\tif (null == annotation) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Method method = ReflectUtil.getMethodOfObj(annotation, propertyName);\n\t\tif (null == method) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ReflectUtil.invoke(annotation, method);\n\t}\n\n\t/**\n\t * 获取指定注解属性的值<br>\n\t * 如果无指定的属性方法返回null\n\t *\n\t * @param <A>           注解类型\n\t * @param <R>           注解类型值\n\t * @param annotationEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param propertyName  属性名，例如注解中定义了name()方法，则 此处传入name\n\t * @return 注解对象\n\t * @throws UtilException 调用注解中的方法时执行异常\n\t * @since 5.8.9\n\t */\n\tpublic static <A extends Annotation, R> R getAnnotationValue(AnnotatedElement annotationEle, Func1<A, R> propertyName) {\n\t\tif (propertyName == null) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\tfinal SerializedLambda lambda = LambdaUtil.resolve(propertyName);\n\t\t\tfinal String instantiatedMethodType = lambda.getInstantiatedMethodType();\n\t\t\tfinal Class<A> annotationClass = ClassUtil.loadClass(StrUtil.sub(instantiatedMethodType, 2, StrUtil.indexOf(instantiatedMethodType, ';')));\n\t\t\treturn getAnnotationValue(annotationEle, annotationClass, lambda.getImplMethodName());\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定注解中所有属性值<br>\n\t * 如果无指定的属性方法返回null\n\t *\n\t * @param annotationEle  {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类型\n\t * @return 注解对象\n\t * @throws UtilException 调用注解中的方法时执行异常\n\t */\n\tpublic static Map<String, Object> getAnnotationValueMap(AnnotatedElement annotationEle, Class<? extends Annotation> annotationType) throws UtilException {\n\t\tfinal Annotation annotation = getAnnotation(annotationEle, annotationType);\n\t\tif (null == annotation) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Method[] methods = ReflectUtil.getMethods(annotationType, t -> {\n\t\t\tif (ArrayUtil.isEmpty(t.getParameterTypes())) {\n\t\t\t\t// 只读取无参方法\n\t\t\t\tfinal String name = t.getName();\n\t\t\t\t// 跳过自有的几个方法\n\t\t\t\treturn (false == \"hashCode\".equals(name)) //\n\t\t\t\t\t\t&& (false == \"toString\".equals(name)) //\n\t\t\t\t\t\t&& (false == \"annotationType\".equals(name));\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\n\t\tfinal HashMap<String, Object> result = new HashMap<>(methods.length, 1);\n\t\tfor (Method method : methods) {\n\t\t\tresult.put(method.getName(), ReflectUtil.invoke(annotation, method));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取注解类的保留时间，可选值 SOURCE（源码时），CLASS（编译时），RUNTIME（运行时），默认为 CLASS\n\t *\n\t * @param annotationType 注解类\n\t * @return 保留时间枚举\n\t */\n\tpublic static RetentionPolicy getRetentionPolicy(Class<? extends Annotation> annotationType) {\n\t\tfinal Retention retention = annotationType.getAnnotation(Retention.class);\n\t\tif (null == retention) {\n\t\t\treturn RetentionPolicy.CLASS;\n\t\t}\n\t\treturn retention.value();\n\t}\n\n\t/**\n\t * 获取注解类可以用来修饰哪些程序元素，如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等\n\t *\n\t * @param annotationType 注解类\n\t * @return 注解修饰的程序元素数组\n\t */\n\tpublic static ElementType[] getTargetType(Class<? extends Annotation> annotationType) {\n\t\tfinal Target target = annotationType.getAnnotation(Target.class);\n\t\tif (null == target) {\n\t\t\treturn new ElementType[]{ElementType.TYPE, //\n\t\t\t\t\tElementType.FIELD, //\n\t\t\t\t\tElementType.METHOD, //\n\t\t\t\t\tElementType.PARAMETER, //\n\t\t\t\t\tElementType.CONSTRUCTOR, //\n\t\t\t\t\tElementType.LOCAL_VARIABLE, //\n\t\t\t\t\tElementType.ANNOTATION_TYPE, //\n\t\t\t\t\tElementType.PACKAGE//\n\t\t\t};\n\t\t}\n\t\treturn target.value();\n\t}\n\n\t/**\n\t * 是否会保存到 Javadoc 文档中\n\t *\n\t * @param annotationType 注解类\n\t * @return 是否会保存到 Javadoc 文档中\n\t */\n\tpublic static boolean isDocumented(Class<? extends Annotation> annotationType) {\n\t\treturn annotationType.isAnnotationPresent(Documented.class);\n\t}\n\n\t/**\n\t * 是否可以被继承，默认为 false\n\t *\n\t * @param annotationType 注解类\n\t * @return 是否会保存到 Javadoc 文档中\n\t */\n\tpublic static boolean isInherited(Class<? extends Annotation> annotationType) {\n\t\treturn annotationType.isAnnotationPresent(Inherited.class);\n\t}\n\n\t/**\n\t * 扫描注解类，以及注解类的{@link Class}层级结构中的注解，将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认注解外，\n\t * 按元注解对象与{@code annotationType}的距离和{@link Class#getAnnotations()}顺序排序的注解对象集合\n\t *\n\t * <p>比如：<br>\n\t * 若{@code annotationType}为 A，且A存在元注解B，B又存在元注解C和D，则有：\n\t * <pre>\n\t *                              |-&gt; C.class [@a, @b]\n\t *     A.class -&gt; B.class [@a] -|\n\t *                              |-&gt; D.class [@a, @c]\n\t * </pre>\n\t * 扫描A，则该方法最终将返回 {@code [@a, @a, @b, @a, @c]}\n\t *\n\t * @param annotationType 注解类\n\t * @return 注解对象集合\n\t * @see MetaAnnotationScanner\n\t */\n\tpublic static List<Annotation> scanMetaAnnotation(Class<? extends Annotation> annotationType) {\n\t\treturn AnnotationScanner.DIRECTLY_AND_META_ANNOTATION.getAnnotationsIfSupport(annotationType);\n\t}\n\n\t/**\n\t * <p>扫描类以及类的{@link Class}层级结构中的注解，将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外,\n\t * 全部类/接口的{@link Class#getAnnotations()}方法返回的注解对象。<br>\n\t * 层级结构将按广度优先递归，遵循规则如下：\n\t * <ul>\n\t *     <li>同一层级中，优先处理父类，然后再处理父接口；</li>\n\t *     <li>同一个接口在不同层级出现，优先选择层级距离{@code targetClass}更近的接口；</li>\n\t *     <li>同一个接口在相同层级出现，优先选择其子类/子接口被先解析的那个；</li>\n\t * </ul>\n\t * 注解根据其声明类/接口被扫描的顺序排序，若注解都在同一个{@link Class}中被声明，则还会遵循{@link Class#getAnnotations()}的顺序。\n\t *\n\t * <p>比如：<br>\n\t * 若{@code targetClass}为{@code A.class}，且{@code A.class}存在父类{@code B.class}、父接口{@code C.class}，\n\t * 三个类的注解声明情况如下：\n\t * <pre>\n\t *                   |-&gt; B.class [@a, @b]\n\t *     A.class [@a] -|\n\t *                   |-&gt; C.class [@a, @c]\n\t * </pre>\n\t * 则该方法最终将返回 {@code [@a, @a, @b, @a, @c]}\n\t *\n\t * @param targetClass 类\n\t * @return 注解对象集合\n\t * @see TypeAnnotationScanner\n\t */\n\tpublic static List<Annotation> scanClass(Class<?> targetClass) {\n\t\treturn AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(targetClass);\n\t}\n\n\t/**\n\t * <p>扫描方法，以及该方法所在类的{@link Class}层级结构中的具有相同方法签名的方法，\n\t * 将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认元注解外,\n\t * 全部匹配方法上{@link Method#getAnnotations()}方法返回的注解对象。<br>\n\t * 方法所在类的层级结构将按广度优先递归，遵循规则如下：\n\t * <ul>\n\t *     <li>同一层级中，优先处理父类，然后再处理父接口；</li>\n\t *     <li>同一个接口在不同层级出现，优先选择层级距离{@code targetClass}更近的接口；</li>\n\t *     <li>同一个接口在相同层级出现，优先选择其子类/子接口被先解析的那个；</li>\n\t * </ul>\n\t * 方法上的注解根据方法的声明类/接口被扫描的顺序排序，若注解都在同一个类的同一个方法中被声明，则还会遵循{@link Method#getAnnotations()}的顺序。\n\t *\n\t * <p>比如：<br>\n\t * 若方法X声明于{@code A.class}，且重载/重写自父类{@code B.class}，并且父类中的方法X由重写至其实现的接口{@code C.class}，\n\t * 三个类的注解声明情况如下：\n\t * <pre>\n\t *     A#X()[@a] -&gt; B#X()[@b] -&gt; C#X()[@c]\n\t * </pre>\n\t * 则该方法最终将返回 {@code [@a, @b, @c]}\n\t *\n\t * @param method 方法\n\t * @return 注解对象集合\n\t * @see MethodAnnotationScanner\n\t */\n\tpublic static List<Annotation> scanMethod(Method method) {\n\t\treturn AnnotationScanner.TYPE_HIERARCHY.getAnnotationsIfSupport(method);\n\t}\n\n\t/**\n\t * 设置新的注解的属性（字段）值\n\t *\n\t * @param annotation      注解对象\n\t * @param annotationField 注解属性（字段）名称\n\t * @param value           要更新的属性值\n\t * @since 5.5.2\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic static void setValue(Annotation annotation, String annotationField, Object value) {\n\t\tfinal Map memberValues = (Map) ReflectUtil.getFieldValue(Proxy.getInvocationHandler(annotation), \"memberValues\");\n\t\tmemberValues.put(annotationField, value);\n\t}\n\n\t/**\n\t * 该注解对象是否为通过代理类生成的合成注解\n\t *\n\t * @param annotation 注解对象\n\t * @return 是否\n\t * @see SynthesizedAnnotationProxy#isProxyAnnotation(Class)\n\t */\n\tpublic static boolean isSynthesizedAnnotation(Annotation annotation) {\n\t\treturn SynthesizedAnnotationProxy.isProxyAnnotation(annotation.getClass());\n\t}\n\n\t/**\n\t * 获取别名支持后的注解\n\t *\n\t * @param annotationEle  被注解的类\n\t * @param annotationType 注解类型Class\n\t * @param <T>            注解类型\n\t * @return 别名支持后的注解\n\t * @since 5.7.23\n\t */\n\tpublic static <T extends Annotation> T getAnnotationAlias(AnnotatedElement annotationEle, Class<T> annotationType) {\n\t\tfinal T annotation = getAnnotation(annotationEle, annotationType);\n\t\tif (null == annotation) {\n\t\t\treturn null;\n\t\t}\n\t\treturn aggregatingFromAnnotation(annotation).synthesize(annotationType);\n\t}\n\n\t/**\n\t * 将指定注解实例与其元注解转为合成注解\n\t *\n\t * @param annotationType 注解类\n\t * @param annotations    注解对象\n\t * @param <T>            注解类型\n\t * @return 合成注解\n\t * @see SynthesizedAggregateAnnotation\n\t */\n\tpublic static <T extends Annotation> T getSynthesizedAnnotation(Class<T> annotationType, Annotation... annotations) {\n\t\t// TODO 缓存合成注解信息，避免重复解析\n\t\treturn Opt.ofNullable(annotations)\n\t\t\t\t.filter(ArrayUtil::isNotEmpty)\n\t\t\t\t.map(AnnotationUtil::aggregatingFromAnnotationWithMeta)\n\t\t\t\t.map(a -> a.synthesize(annotationType))\n\t\t\t\t.get();\n\t}\n\n\t/**\n\t * <p>获取元素上距离指定元素最接近的合成注解\n\t * <ul>\n\t *     <li>若元素是类，则递归解析全部父类和全部父接口上的注解;</li>\n\t *     <li>若元素是方法、属性或注解，则只解析其直接声明的注解;</li>\n\t * </ul>\n\t *\n\t * <p>注解合成规则如下：\n\t * 若{@code AnnotatedEle}按顺序从上到下声明了A，B，C三个注解，且三注解存在元注解如下：\n\t * <pre>\n\t *    A -&gt; M3\n\t *    B -&gt; M1 -&gt; M2 -&gt; M3\n\t *    C -&gt; M2 -&gt; M3\n\t * </pre>\n\t * 此时入参{@code annotationType}类型为{@code M2}，则最终将优先返回基于根注解B合成的合成注解\n\t *\n\t * @param annotatedEle   {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类\n\t * @param <T>            注解类型\n\t * @return 合成注解\n\t * @see SynthesizedAggregateAnnotation\n\t */\n\tpublic static <T extends Annotation> T getSynthesizedAnnotation(AnnotatedElement annotatedEle, Class<T> annotationType) {\n\t\tT target = annotatedEle.getAnnotation(annotationType);\n\t\tif (ObjectUtil.isNotNull(target)) {\n\t\t\treturn target;\n\t\t}\n\t\treturn AnnotationScanner.DIRECTLY\n\t\t\t\t.getAnnotationsIfSupport(annotatedEle).stream()\n\t\t\t\t.map(annotation -> getSynthesizedAnnotation(annotationType, annotation))\n\t\t\t\t.filter(Objects::nonNull)\n\t\t\t\t.findFirst()\n\t\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 获取元素上所有指定注解\n\t * <ul>\n\t *     <li>若元素是类，则递归解析全部父类和全部父接口上的注解;</li>\n\t *     <li>若元素是方法、属性或注解，则只解析其直接声明的注解;</li>\n\t * </ul>\n\t *\n\t * <p>注解合成规则如下：\n\t * 若{@code AnnotatedEle}按顺序从上到下声明了A，B，C三个注解，且三注解存在元注解如下：\n\t * <pre>\n\t *    A -&gt; M1 -&gt; M2\n\t *    B -&gt; M3 -&gt; M1 -&gt; M2\n\t *    C -&gt; M2\n\t * </pre>\n\t * 此时入参{@code annotationType}类型为{@code M1}，则最终将返回基于根注解A与根注解B合成的合成注解。\n\t *\n\t * @param annotatedEle   {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param annotationType 注解类\n\t * @param <T>            注解类型\n\t * @return 合成注解\n\t * @see SynthesizedAggregateAnnotation\n\t */\n\tpublic static <T extends Annotation> List<T> getAllSynthesizedAnnotations(AnnotatedElement annotatedEle, Class<T> annotationType) {\n\t\treturn AnnotationScanner.DIRECTLY\n\t\t\t\t.getAnnotationsIfSupport(annotatedEle).stream()\n\t\t\t\t.map(annotation -> getSynthesizedAnnotation(annotationType, annotation))\n\t\t\t\t.filter(Objects::nonNull)\n\t\t\t\t.collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 对指定注解对象进行聚合\n\t *\n\t * @param annotations 注解对象\n\t * @return 聚合注解\n\t */\n\tpublic static SynthesizedAggregateAnnotation aggregatingFromAnnotation(Annotation... annotations) {\n\t\treturn new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.NOTHING);\n\t}\n\n\t/**\n\t * 对指定注解对象及其元注解进行聚合\n\t *\n\t * @param annotations 注解对象\n\t * @return 聚合注解\n\t */\n\tpublic static SynthesizedAggregateAnnotation aggregatingFromAnnotationWithMeta(Annotation... annotations) {\n\t\treturn new GenericSynthesizedAggregateAnnotation(Arrays.asList(annotations), AnnotationScanner.DIRECTLY_AND_META_ANNOTATION);\n\t}\n\n\t/**\n\t * 方法是否为注解属性方法。 <br>\n\t * 方法无参数，且有返回值的方法认为是注解属性的方法。\n\t *\n\t * @param method 方法\n\t */\n\tstatic boolean isAttributeMethod(Method method) {\n\t\treturn method.getParameterCount() == 0 && method.getReturnType() != void.class;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/CacheableAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\n\n/**\n * {@link AnnotationAttribute}的基本实现\n *\n * @author huangchengxing\n */\npublic class CacheableAnnotationAttribute implements AnnotationAttribute {\n\n\tprivate volatile boolean valueInvoked;\n\tprivate volatile Object value;\n\n\tprivate boolean defaultValueInvoked;\n\tprivate Object defaultValue;\n\n\tprivate final Annotation annotation;\n\tprivate final Method attribute;\n\n\tpublic CacheableAnnotationAttribute(Annotation annotation, Method attribute) {\n\t\tAssert.notNull(annotation, \"annotation must not null\");\n\t\tAssert.notNull(attribute, \"attribute must not null\");\n\t\tthis.annotation = annotation;\n\t\tthis.attribute = attribute;\n\t\tthis.valueInvoked = false;\n\t\tthis.defaultValueInvoked = false;\n\t}\n\n\t@Override\n\tpublic Annotation getAnnotation() {\n\t\treturn this.annotation;\n\t}\n\n\t@Override\n\tpublic Method getAttribute() {\n\t\treturn this.attribute;\n\t}\n\n\t@Override\n\tpublic Object getValue() {\n\t\tif (!valueInvoked) {\n\t\t\tsynchronized (this) {\n\t\t\t\tif (!valueInvoked) {\n\t\t\t\t\tvalueInvoked = true;\n\t\t\t\t\tvalue = ReflectUtil.invoke(annotation, attribute);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic boolean isValueEquivalentToDefaultValue() {\n\t\tif (!defaultValueInvoked) {\n\t\t\tdefaultValue = attribute.getDefaultValue();\n\t\t\tdefaultValueInvoked = true;\n\t\t}\n\t\treturn ObjectUtil.equals(getValue(), defaultValue);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.multi.RowKeyTable;\nimport cn.hutool.core.map.multi.Table;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.Objects;\n\n/**\n * <p>带缓存功能的{@link SynthesizedAnnotationAttributeProcessor}实现，\n * 构建时需要传入比较器，获取属性值时将根据比较器对合成注解进行排序，\n * 然后选择具有所需属性的，排序最靠前的注解用于获取属性值\n *\n * <p>通过该处理器获取合成注解属性值时会出现隐式别名，\n * 即子注解和元注解中同时存在类型和名称皆相同的属性时，元注解中属性总是会被该属性覆盖，\n * 并且该覆盖关系并不会通过{@link Alias}或{@link Link}被传递到关联的属性中。\n *\n * @author huangchengxing\n */\npublic class CacheableSynthesizedAnnotationAttributeProcessor implements SynthesizedAnnotationAttributeProcessor {\n\n\tprivate final Table<String, Class<?>, Object> valueCaches = new RowKeyTable<>();\n\tprivate final Comparator<Hierarchical> annotationComparator;\n\n\t/**\n\t * 创建一个带缓存的注解值选择器\n\t *\n\t * @param annotationComparator 注解比较器，排序更靠前的注解将被优先用于获取值\n\t */\n\tpublic CacheableSynthesizedAnnotationAttributeProcessor(Comparator<Hierarchical> annotationComparator) {\n\t\tAssert.notNull(annotationComparator, \"annotationComparator must not null\");\n\t\tthis.annotationComparator = annotationComparator;\n\t}\n\n\t/**\n\t * 创建一个带缓存的注解值选择器，\n\t * 默认按{@link SynthesizedAnnotation#getVerticalDistance()}和{@link SynthesizedAnnotation#getHorizontalDistance()}排序，\n\t * 越靠前的越优先被取值。\n\t */\n\tpublic CacheableSynthesizedAnnotationAttributeProcessor() {\n\t\tthis(Hierarchical.DEFAULT_HIERARCHICAL_COMPARATOR);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic <T> T getAttributeValue(String attributeName, Class<T> attributeType, Collection<? extends SynthesizedAnnotation> synthesizedAnnotations) {\n\t\tObject value = valueCaches.get(attributeName, attributeType);\n\t\tif (Objects.isNull(value)) {\n\t\t\tsynchronized (valueCaches) {\n\t\t\t\tvalue = valueCaches.get(attributeName, attributeType);\n\t\t\t\tif (Objects.isNull(value)) {\n\t\t\t\t\tvalue = synthesizedAnnotations.stream()\n\t\t\t\t\t\t.filter(ma -> ma.hasAttribute(attributeName, attributeType))\n\t\t\t\t\t\t.min(annotationComparator)\n\t\t\t\t\t\t.map(ma -> ma.getAttributeValue(attributeName))\n\t\t\t\t\t\t.orElse(null);\n\t\t\t\t\tvalueCaches.put(attributeName, attributeType, value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (T)value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.map.TableMap;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.io.Serializable;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\n/**\n * 组合注解 对JDK的原生注解机制做一个增强，支持类似Spring的组合注解。<br>\n * 核心实现使用了递归获取指定元素上的注解以及注解的注解，以实现复合注解的获取。\n *\n * @author Succy, Looly\n * @since 4.0.9\n **/\n\npublic class CombinationAnnotationElement implements AnnotatedElement, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建CombinationAnnotationElement\n\t *\n\t * @param element   需要解析注解的元素：可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param predicate 过滤器，{@link Predicate#test(Object)}返回{@code true}保留，否则不保留\n\t * @return CombinationAnnotationElement\n\t * @since 5.8.0\n\t */\n\tpublic static CombinationAnnotationElement of(AnnotatedElement element, Predicate<Annotation> predicate) {\n\t\treturn new CombinationAnnotationElement(element, predicate);\n\t}\n\n\t/**\n\t * 注解类型与注解对象对应表\n\t */\n\tprivate Map<Class<? extends Annotation>, Annotation> annotationMap;\n\t/**\n\t * 直接注解类型与注解对象对应表\n\t */\n\tprivate Map<Class<? extends Annotation>, Annotation> declaredAnnotationMap;\n\t/**\n\t * 过滤器\n\t */\n\tprivate final Predicate<Annotation> predicate;\n\n\t/**\n\t * 构造\n\t *\n\t * @param element 需要解析注解的元素：可以是Class、Method、Field、Constructor、ReflectPermission\n\t */\n\tpublic CombinationAnnotationElement(AnnotatedElement element) {\n\t\tthis(element, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param element   需要解析注解的元素：可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param predicate 过滤器，{@link Predicate#test(Object)}返回{@code true}保留，否则不保留\n\t * @since 5.8.0\n\t */\n\tpublic CombinationAnnotationElement(AnnotatedElement element, Predicate<Annotation> predicate) {\n\t\tthis.predicate = predicate;\n\t\tinit(element);\n\t}\n\n\t@Override\n\tpublic boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {\n\t\treturn annotationMap.containsKey(annotationClass);\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T extends Annotation> T getAnnotation(Class<T> annotationClass) {\n\t\tAnnotation annotation = annotationMap.get(annotationClass);\n\t\treturn (annotation == null) ? null : (T) annotation;\n\t}\n\n\t@Override\n\tpublic Annotation[] getAnnotations() {\n\t\tfinal Collection<Annotation> annotations = this.annotationMap.values();\n\t\treturn annotations.toArray(new Annotation[0]);\n\t}\n\n\t@Override\n\tpublic Annotation[] getDeclaredAnnotations() {\n\t\tfinal Collection<Annotation> annotations = this.declaredAnnotationMap.values();\n\t\treturn annotations.toArray(new Annotation[0]);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param element 元素\n\t */\n\tprivate void init(AnnotatedElement element) {\n\t\tfinal Annotation[] declaredAnnotations = element.getDeclaredAnnotations();\n\t\tthis.declaredAnnotationMap = new TableMap<>();\n\t\tparseDeclared(declaredAnnotations);\n\n\t\tfinal Annotation[] annotations = element.getAnnotations();\n\t\t// pr#1323 如果子类重写了父类的注解，虽然两者数组内部元素一样的，但是数组中的顺序可能不一样\n\t\t// getAnnotations()的包含父类，getDeclaredAnnotations()不包含父类。他们两是一个包含关系，只会存在后者的注解元素大于等于前者的情况。\n\t\tif (declaredAnnotations.length == annotations.length) {\n\t\t\tthis.annotationMap = this.declaredAnnotationMap;\n\t\t} else {\n\t\t\tthis.annotationMap = new TableMap<>();\n\t\t\tparse(annotations);\n\t\t}\n\t}\n\n\t/**\n\t * 进行递归解析注解，直到全部都是元注解为止\n\t *\n\t * @param annotations Class, Method, Field等\n\t */\n\tprivate void parseDeclared(Annotation[] annotations) {\n\t\tif(ArrayUtil.isEmpty(annotations)){\n\t\t\treturn;\n\t\t}\n\n\t\tClass<? extends Annotation> annotationType;\n\t\t// 直接注解\n\t\tfor (Annotation annotation : annotations) {\n\t\t\tannotationType = annotation.annotationType();\n\t\t\t// issue#I5FQGW@Gitee：跳过元注解和已经处理过的注解，防止递归调用\n\t\t\tif (AnnotationUtil.isNotJdkMateAnnotation(annotationType)\n\t\t\t\t\t&& false == declaredAnnotationMap.containsKey(annotationType)) {\n\t\t\t\tif(test(annotation)){\n\t\t\t\t\tdeclaredAnnotationMap.put(annotationType, annotation);\n\t\t\t\t}\n\t\t\t\t// 测试不通过的注解，不影响继续递归\n\t\t\t\tparseDeclared(annotationType.getDeclaredAnnotations());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 进行递归解析注解，直到全部都是元注解为止\n\t *\n\t * @param annotations Class, Method, Field等\n\t */\n\tprivate void parse(Annotation[] annotations) {\n\t\tif(ArrayUtil.isEmpty(annotations)){\n\t\t\treturn;\n\t\t}\n\n\t\tClass<? extends Annotation> annotationType;\n\t\tfor (Annotation annotation : annotations) {\n\t\t\tannotationType = annotation.annotationType();\n\t\t\t// issue#I5FQGW@Gitee：跳过元注解和已经处理过的注解，防止递归调用\n\t\t\tif (AnnotationUtil.isNotJdkMateAnnotation(annotationType)\n\t\t\t\t\t&& false == annotationMap.containsKey(annotationType)) {\n\t\t\t\tif(test(annotation)){\n\t\t\t\t\tannotationMap.put(annotationType, annotation);\n\t\t\t\t}\n\t\t\t\t// 测试不通过的注解，不影响继续递归\n\t\t\t\tparse(annotationType.getAnnotations());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 检查给定的注解是否符合过滤条件\n\t *\n\t * @param annotation 注解对象\n\t * @return 是否符合条件\n\t */\n\tprivate boolean test(Annotation annotation) {\n\t\treturn null == this.predicate || this.predicate.test(annotation);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasFor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * <p>{@link Link}的子注解。表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解，\n * 任何情况下，获取“关联属性”的值都将直接返回“原始属性”的值\n * <b>注意，该注解与{@link Link}、{@link AliasFor}或{@link MirrorFor}一起使用时，将只有被声明在最上面的注解会生效</b>\n *\n * @author huangchengxing\n * @see Link\n * @see RelationType#FORCE_ALIAS_FOR\n */\n@Link(type = RelationType.FORCE_ALIAS_FOR)\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})\npublic @interface ForceAliasFor {\n\n\t/**\n\t * 产生关联的注解类型，当不指定时，默认指注释的属性所在的类\n\t *\n\t * @return 关联注解类型\n\t */\n\t@Link(annotation = Link.class, attribute = \"annotation\", type = RelationType.FORCE_ALIAS_FOR)\n\tClass<? extends Annotation> annotation() default Annotation.class;\n\n\t/**\n\t * {@link #annotation()}指定注解中关联的属性\n\t *\n\t * @return 关联的属性\n\t */\n\t@Link(annotation = Link.class, attribute = \"attribute\", type = RelationType.FORCE_ALIAS_FOR)\n\tString attribute() default \"\";\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\n/**\n * 表示一个被指定了强制别名的注解属性。\n * 当调用{@link #getValue()}时，总是返回{@link #linked}的值\n *\n * @author huangchengxing\n * @see AliasAnnotationPostProcessor\n * @see AliasLinkAnnotationPostProcessor\n * @see RelationType#ALIAS_FOR\n * @see RelationType#FORCE_ALIAS_FOR\n */\npublic class ForceAliasedAnnotationAttribute extends AbstractWrappedAnnotationAttribute {\n\n\tprotected ForceAliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {\n\t\tsuper(origin, linked);\n\t}\n\n\t/**\n\t * 总是返回{@link #linked}的{@link AnnotationAttribute#getValue()}的返回值\n\t *\n\t * @return {@link #linked}的{@link AnnotationAttribute#getValue()}的返回值\n\t */\n\t@Override\n\tpublic Object getValue() {\n\t\treturn linked.getValue();\n\t}\n\n\t/**\n\t * 总是返回{@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值\n\t *\n\t * @return {@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值\n\t */\n\t@Override\n\tpublic boolean isValueEquivalentToDefaultValue() {\n\t\treturn linked.isValueEquivalentToDefaultValue();\n\t}\n\n\t/**\n\t * 总是返回{@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值\n\t *\n\t * @return {@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值\n\t */\n\t@Override\n\tpublic Class<?> getAttributeType() {\n\t\treturn linked.getAttributeType();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotation.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.annotation.scanner.AnnotationScanner;\nimport cn.hutool.core.annotation.scanner.MetaAnnotationScanner;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.*;\n\n/**\n * {@link SynthesizedAggregateAnnotation}的基本实现，表示基于多个注解对象，\n * 或多个根注解对象与他们的多层元注解对象的聚合得到的注解。\n *\n * <p>假设现有注解A，若指定的{@link #annotationScanner}支持扫描注解A的元注解，\n * 且A上存在元注解B，B上存在元注解C，则对注解A进行解析，将得到包含根注解A，以及其元注解B、C在内的合成元注解聚合{@link GenericSynthesizedAggregateAnnotation}。\n * 从{@link AnnotatedElement}的角度来说，得到的合成注解是一个同时承载有ABC三个注解对象的被注解元素，\n * 因此通过调用{@link AnnotatedElement}的相关方法将返回对应符合语义的注解对象。\n *\n * <p>在扫描指定根注解及其元注解时，若在不同的层级出现了类型相同的注解实例，\n * 将会根据实例化时指定的{@link SynthesizedAnnotationSelector}选择最优的注解，\n * 完成对根注解及其元注解的扫描后，合成注解中每种类型的注解对象都将有且仅有一个。<br>\n * 默认情况下，将使用{@link SynthesizedAnnotationSelector#NEAREST_AND_OLDEST_PRIORITY}作为选择器，\n * 此时若出现扫描时得到了多个同类型的注解对象，有且仅有最接近根注解的注解对象会被作为有效注解。\n *\n * <p>当扫描的注解对象经过{@link SynthesizedAnnotationSelector}处理后，\n * 将会被转为{@link MetaAnnotation}，并使用在实例化时指定的{@link AliasAnnotationPostProcessor}\n * 进行后置处理。<br>\n * 默认情况下，将注册以下后置处理器以对{@link Alias}与{@link Link}和其扩展注解提供支持：\n * <ul>\n *     <li>{@link AliasAnnotationPostProcessor}；</li>\n *     <li>{@link MirrorLinkAnnotationPostProcessor}；</li>\n *     <li>{@link AliasLinkAnnotationPostProcessor}；</li>\n * </ul>\n * 若用户需要自行扩展，则需要保证上述三个处理器被正确注入当前实例。\n *\n * <p>{@link GenericSynthesizedAggregateAnnotation}支持通过{@link #getAttributeValue(String, Class)}，\n * 或通过{@link #synthesize(Class)}获得注解代理对象后获取指定类型的注解属性值，\n * 返回的属性值将根据合成注解中对应原始注解属性上的{@link Alias}与{@link Link}注解而有所变化。\n * 通过当前实例获取属性值时，将经过{@link SynthesizedAnnotationAttributeProcessor}的处理。<br>\n * 默认情况下，实例将会注册{@link CacheableSynthesizedAnnotationAttributeProcessor}，\n * 该处理器将令元注解中与子注解类型与名称皆一致的属性被子注解的属性覆盖，并且缓存最终获取到的属性值。\n *\n * @author huangchengxing\n * @see AnnotationUtil\n * @see SynthesizedAnnotationProxy\n * @see SynthesizedAnnotationSelector\n * @see SynthesizedAnnotationAttributeProcessor\n * @see SynthesizedAnnotationPostProcessor\n * @see AnnotationSynthesizer\n * @see AnnotationScanner\n */\npublic class GenericSynthesizedAggregateAnnotation\n\textends AbstractAnnotationSynthesizer<List<Annotation>>\n\timplements SynthesizedAggregateAnnotation {\n\n\t/**\n\t * 根对象\n\t */\n\tprivate final Object root;\n\n\t/**\n\t * 距离根对象的垂直距离\n\t */\n\tprivate final int verticalDistance;\n\n\t/**\n\t * 距离根对象的水平距离\n\t */\n\tprivate final int horizontalDistance;\n\n\t/**\n\t * 合成注解属性处理器\n\t */\n\tprivate final SynthesizedAnnotationAttributeProcessor attributeProcessor;\n\n\t/**\n\t * 基于指定根注解，为其与其元注解的层级结构中的全部注解构造一个合成注解。\n\t * 当层级结构中出现了相同的注解对象时，将优先选择以距离根注解最近，且优先被扫描的注解对象,\n\t * 当获取值时，同样遵循该规则。\n\t *\n\t * @param source 源注解\n\t */\n\tpublic GenericSynthesizedAggregateAnnotation(Annotation... source) {\n\t\tthis(Arrays.asList(source), new MetaAnnotationScanner());\n\t}\n\n\t/**\n\t * 基于指定根注解，为其层级结构中的全部注解构造一个合成注解。\n\t * 若扫描器支持对注解的层级结构进行扫描，则若层级结构中出现了相同的注解对象时，\n\t * 将优先选择以距离根注解最近，且优先被扫描的注解对象，并且当获取注解属性值时同样遵循该规则。\n\t *\n\t * @param source            源注解\n\t * @param annotationScanner 注解扫描器，该扫描器必须支持扫描注解类\n\t */\n\tpublic GenericSynthesizedAggregateAnnotation(List<Annotation> source, AnnotationScanner annotationScanner) {\n\t\tthis(\n\t\t\tsource, SynthesizedAnnotationSelector.NEAREST_AND_OLDEST_PRIORITY,\n\t\t\tnew CacheableSynthesizedAnnotationAttributeProcessor(),\n\t\t\tArrays.asList(\n\t\t\t\tSynthesizedAnnotationPostProcessor.ALIAS_ANNOTATION_POST_PROCESSOR,\n\t\t\t\tSynthesizedAnnotationPostProcessor.MIRROR_LINK_ANNOTATION_POST_PROCESSOR,\n\t\t\t\tSynthesizedAnnotationPostProcessor.ALIAS_LINK_ANNOTATION_POST_PROCESSOR\n\t\t\t),\n\t\t\tannotationScanner\n\t\t);\n\t}\n\n\t/**\n\t * 基于指定根注解，为其层级结构中的全部注解构造一个合成注解\n\t *\n\t * @param source                   当前查找的注解对象\n\t * @param annotationSelector       合成注解选择器\n\t * @param attributeProcessor       注解属性处理器\n\t * @param annotationPostProcessors 注解后置处理器\n\t * @param annotationScanner        注解扫描器，该扫描器必须支持扫描注解类\n\t */\n\tpublic GenericSynthesizedAggregateAnnotation(\n\t\tList<Annotation> source,\n\t\tSynthesizedAnnotationSelector annotationSelector,\n\t\tSynthesizedAnnotationAttributeProcessor attributeProcessor,\n\t\tCollection<SynthesizedAnnotationPostProcessor> annotationPostProcessors,\n\t\tAnnotationScanner annotationScanner) {\n\t\tthis(\n\t\t\tnull, 0, 0,\n\t\t\tsource, annotationSelector, attributeProcessor, annotationPostProcessors, annotationScanner\n\t\t);\n\t}\n\n\t/**\n\t * 基于指定根注解，为其层级结构中的全部注解构造一个合成注解\n\t *\n\t * @param root                     根对象\n\t * @param verticalDistance         距离根对象的水平距离\n\t * @param horizontalDistance       距离根对象的垂直距离\n\t * @param source                   当前查找的注解对象\n\t * @param annotationSelector       合成注解选择器\n\t * @param attributeProcessor       注解属性处理器\n\t * @param annotationPostProcessors 注解后置处理器\n\t * @param annotationScanner        注解扫描器，该扫描器必须支持扫描注解类\n\t */\n\tGenericSynthesizedAggregateAnnotation(\n\t\tObject root, int verticalDistance, int horizontalDistance,\n\t\tList<Annotation> source,\n\t\tSynthesizedAnnotationSelector annotationSelector,\n\t\tSynthesizedAnnotationAttributeProcessor attributeProcessor,\n\t\tCollection<SynthesizedAnnotationPostProcessor> annotationPostProcessors,\n\t\tAnnotationScanner annotationScanner) {\n\t\tsuper(source, annotationSelector, annotationPostProcessors, annotationScanner);\n\t\tAssert.notNull(attributeProcessor, \"attributeProcessor must not null\");\n\n\t\tthis.root = ObjectUtil.defaultIfNull(root, this);\n\t\tthis.verticalDistance = verticalDistance;\n\t\tthis.horizontalDistance = horizontalDistance;\n\t\tthis.attributeProcessor = attributeProcessor;\n\t}\n\n\t/**\n\t * 获取根对象\n\t *\n\t * @return 根对象\n\t */\n\t@Override\n\tpublic Object getRoot() {\n\t\treturn root;\n\t}\n\n\t/**\n\t * 获取与根对象的垂直距离\n\t *\n\t * @return 与根对象的垂直距离\n\t */\n\t@Override\n\tpublic int getVerticalDistance() {\n\t\treturn verticalDistance;\n\t}\n\n\t/**\n\t * 获取与根对象的水平距离\n\t *\n\t * @return 获取与根对象的水平距离\n\t */\n\t@Override\n\tpublic int getHorizontalDistance() {\n\t\treturn horizontalDistance;\n\t}\n\n\t/**\n\t * 按广度优先扫描{@link #source}上的元注解\n\t */\n\t@Override\n\tprotected Map<Class<? extends Annotation>, SynthesizedAnnotation> loadAnnotations() {\n\t\tMap<Class<? extends Annotation>, SynthesizedAnnotation> annotationMap = new LinkedHashMap<>();\n\n\t\t// 根注解默认水平坐标为0，根注解的元注解坐标从1开始\n\t\tfor (int i = 0; i < source.size(); i++) {\n\t\t\tfinal Annotation sourceAnnotation = source.get(i);\n\t\t\tAssert.isFalse(AnnotationUtil.isSynthesizedAnnotation(sourceAnnotation), \"source [{}] has been synthesized\");\n\t\t\tannotationMap.put(sourceAnnotation.annotationType(), new MetaAnnotation(sourceAnnotation, sourceAnnotation, 0, i));\n\t\t\tAssert.isTrue(\n\t\t\t\tannotationScanner.support(sourceAnnotation.annotationType()),\n\t\t\t\t\"annotation scanner [{}] cannot support scan [{}]\",\n\t\t\t\tannotationScanner, sourceAnnotation.annotationType()\n\t\t\t);\n\t\t\tannotationScanner.scan(\n\t\t\t\t(index, annotation) -> {\n\t\t\t\t\tSynthesizedAnnotation oldAnnotation = annotationMap.get(annotation.annotationType());\n\t\t\t\t\tSynthesizedAnnotation newAnnotation = new MetaAnnotation(sourceAnnotation, annotation, index + 1, annotationMap.size());\n\t\t\t\t\tif (ObjectUtil.isNull(oldAnnotation)) {\n\t\t\t\t\t\tannotationMap.put(annotation.annotationType(), newAnnotation);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tannotationMap.put(annotation.annotationType(), annotationSelector.choose(oldAnnotation, newAnnotation));\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tsourceAnnotation.annotationType(), null\n\t\t\t);\n\t\t}\n\t\treturn annotationMap;\n\t}\n\n\t/**\n\t * 获取合成注解属性处理器\n\t *\n\t * @return 合成注解属性处理器\n\t */\n\t@Override\n\tpublic SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() {\n\t\treturn this.attributeProcessor;\n\t}\n\n\t/**\n\t * 根据指定的属性名与属性类型获取对应的属性值，若存在{@link Alias}则获取{@link Alias#value()}指定的别名属性的值\n\t * <p>当不同层级的注解之间存在同名同类型属性时，将优先获取更接近根注解的属性\n\t *\n\t * @param attributeName 属性名\n\t * @param attributeType 属性类型\n\t * @return 属性\n\t */\n\t@Override\n\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\treturn attributeProcessor.getAttributeValue(attributeName, attributeType, synthesizedAnnotationMap.values());\n\t}\n\n\t/**\n\t * 获取合成注解中包含的指定注解\n\t *\n\t * @param annotationType 注解类型\n\t * @param <T>            注解类型\n\t * @return 注解对象\n\t */\n\t@Override\n\tpublic <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\treturn Opt.ofNullable(annotationType)\n\t\t\t.map(synthesizedAnnotationMap::get)\n\t\t\t.map(SynthesizedAnnotation::getAnnotation)\n\t\t\t.map(annotationType::cast)\n\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 当前合成注解中是否存在指定元注解\n\t *\n\t * @param annotationType 注解类型\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {\n\t\treturn synthesizedAnnotationMap.containsKey(annotationType);\n\t}\n\n\t/**\n\t * 获取合成注解中包含的全部注解\n\t *\n\t * @return 注解对象\n\t */\n\t@Override\n\tpublic Annotation[] getAnnotations() {\n\t\treturn synthesizedAnnotationMap.values().stream()\n\t\t\t.map(SynthesizedAnnotation::getAnnotation)\n\t\t\t.toArray(Annotation[]::new);\n\t}\n\n\t/**\n\t * 若合成注解在存在指定元注解，则使用动态代理生成一个对应的注解实例\n\t *\n\t * @param annotationType 注解类型\n\t * @return 合成注解对象\n\t * @see SynthesizedAnnotationProxy#create(Class, AnnotationAttributeValueProvider, SynthesizedAnnotation)\n\t */\n\t@Override\n\tpublic <T extends Annotation> T synthesize(Class<T> annotationType, SynthesizedAnnotation annotation) {\n\t\treturn SynthesizedAnnotationProxy.create(annotationType, this, annotation);\n\t}\n\n\t/**\n\t * 注解包装类，表示{@link #source}以及{@link #source}所属层级结构中的全部关联注解对象\n\t *\n\t * @author huangchengxing\n\t */\n\tpublic static class MetaAnnotation extends GenericSynthesizedAnnotation<Annotation, Annotation> {\n\n\t\t/**\n\t\t * 创建一个合成注解\n\t\t *\n\t\t * @param root               根对象\n\t\t * @param annotation         被合成的注解对象\n\t\t * @param verticalDistance   距离根对象的水平距离\n\t\t * @param horizontalDistance 距离根对象的垂直距离\n\t\t */\n\t\tprotected MetaAnnotation(Annotation root, Annotation annotation, int verticalDistance, int horizontalDistance) {\n\t\t\tsuper(root, annotation, verticalDistance, horizontalDistance);\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/GenericSynthesizedAnnotation.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * {@link SynthesizedAnnotation}的基本实现\n *\n * @param <R> 根对象类型\n * @param <T> 注解类型\n * @author huangchengxing\n */\npublic class GenericSynthesizedAnnotation<R, T extends Annotation> implements SynthesizedAnnotation {\n\n\tprivate final R root;\n\tprivate final T annotation;\n\tprivate final Map<String, AnnotationAttribute> attributeMethodCaches;\n\tprivate final int verticalDistance;\n\tprivate final int horizontalDistance;\n\n\t/**\n\t * 创建一个合成注解\n\t *\n\t * @param root               根对象\n\t * @param annotation         被合成的注解对象\n\t * @param verticalDistance   距离根对象的水平距离\n\t * @param horizontalDistance 距离根对象的垂直距离\n\t */\n\tprotected GenericSynthesizedAnnotation(\n\t\tR root, T annotation, int verticalDistance, int horizontalDistance) {\n\t\tthis.root = root;\n\t\tthis.annotation = annotation;\n\t\tthis.verticalDistance = verticalDistance;\n\t\tthis.horizontalDistance = horizontalDistance;\n\t\tthis.attributeMethodCaches = new HashMap<>();\n\t\tthis.attributeMethodCaches.putAll(loadAttributeMethods());\n\t}\n\n\t/**\n\t * 加载注解属性\n\t *\n\t * @return 注解属性\n\t */\n\tprotected Map<String, AnnotationAttribute> loadAttributeMethods() {\n\t\treturn Stream.of(ClassUtil.getDeclaredMethods(annotation.annotationType()))\n\t\t\t.filter(AnnotationUtil::isAttributeMethod)\n\t\t\t.collect(Collectors.toMap(Method::getName, method -> new CacheableAnnotationAttribute(annotation, method)));\n\t}\n\n\t/**\n\t * 元注解是否存在该属性\n\t *\n\t * @param attributeName 属性名\n\t * @return 是否存在该属性\n\t */\n\tpublic boolean hasAttribute(String attributeName) {\n\t\treturn attributeMethodCaches.containsKey(attributeName);\n\t}\n\n\t/**\n\t * 元注解是否存在该属性，且该属性的值类型是指定类型或其子类\n\t *\n\t * @param attributeName 属性名\n\t * @param returnType    返回值类型\n\t * @return 是否存在该属性\n\t */\n\t@Override\n\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\treturn Opt.ofNullable(attributeMethodCaches.get(attributeName))\n\t\t\t.filter(method -> ClassUtil.isAssignable(returnType, method.getAttributeType()))\n\t\t\t.isPresent();\n\t}\n\n\t/**\n\t * 获取该注解的全部属性\n\t *\n\t * @return 注解属性\n\t */\n\t@Override\n\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\treturn this.attributeMethodCaches;\n\t}\n\n\t/**\n\t * 设置属性值\n\t *\n\t * @param attributeName 属性名称\n\t * @param attribute     注解属性\n\t */\n\t@Override\n\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\t\tattributeMethodCaches.put(attributeName, attribute);\n\t}\n\n\t/**\n\t * 替换属性值\n\t *\n\t * @param attributeName 属性名\n\t * @param operator      替换操作\n\t */\n\t@Override\n\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\t\tAnnotationAttribute old = attributeMethodCaches.get(attributeName);\n\t\tif (ObjectUtil.isNotNull(old)) {\n\t\t\tattributeMethodCaches.put(attributeName, operator.apply(old));\n\t\t}\n\t}\n\n\t/**\n\t * 获取属性值\n\t *\n\t * @param attributeName 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tpublic Object getAttributeValue(String attributeName) {\n\t\treturn Opt.ofNullable(attributeMethodCaches.get(attributeName))\n\t\t\t.map(AnnotationAttribute::getValue)\n\t\t\t.get();\n\t}\n\n\t/**\n\t * 获取该合成注解对应的根节点\n\t *\n\t * @return 合成注解对应的根节点\n\t */\n\t@Override\n\tpublic R getRoot() {\n\t\treturn root;\n\t}\n\n\t/**\n\t * 获取被合成的注解对象\n\t *\n\t * @return 注解对象\n\t */\n\t@Override\n\tpublic T getAnnotation() {\n\t\treturn annotation;\n\t}\n\n\t/**\n\t * 获取该合成注解与根对象的垂直距离。\n\t * 默认情况下，该距离即为当前注解与根对象之间相隔的层级数。\n\t *\n\t * @return 合成注解与根对象的垂直距离\n\t */\n\t@Override\n\tpublic int getVerticalDistance() {\n\t\treturn verticalDistance;\n\t}\n\n\t/**\n\t * 获取该合成注解与根对象的水平距离。\n\t * 默认情况下，该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。\n\t *\n\t * @return 合成注解与根对象的水平距离\n\t */\n\t@Override\n\tpublic int getHorizontalDistance() {\n\t\treturn horizontalDistance;\n\t}\n\n\t/**\n\t * 获取被合成的注解类型\n\t *\n\t * @return 被合成的注解类型\n\t */\n\t@Override\n\tpublic Class<? extends Annotation> annotationType() {\n\t\treturn annotation.annotationType();\n\t}\n\n\t/**\n\t * 获取注解属性值\n\t *\n\t * @param attributeName  属性名称\n\t * @param attributeType  属性类型\n\t * @return 注解属性值\n\t */\n\t@Override\n\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\treturn Opt.ofNullable(attributeMethodCaches.get(attributeName))\n\t\t\t.filter(method -> ClassUtil.isAssignable(attributeType, method.getAttributeType()))\n\t\t\t.map(AnnotationAttribute::getValue)\n\t\t\t.get();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/Hierarchical.java",
    "content": "package cn.hutool.core.annotation;\n\n\nimport java.util.Comparator;\n\n/**\n * <p>描述以一个参照物为对象，存在于该参照物的层级结构中的对象。\n *\n * <p>该对象可通过{@link #getVerticalDistance()}与{@link #getHorizontalDistance()}\n * 描述其在以参照物为基点的坐标坐标系中的位置。<br>\n * 在需要对该接口的实现类进行按优先级排序时，距离{@link #getRoot()}对象越近，则该实现类的优先级越高。\n * 默认提供了{@link #DEFAULT_HIERARCHICAL_COMPARATOR}用于实现该比较规则。<br>\n * 一般情况下，{@link #getRoot()}返回值相同的对象之间的比较才有意义。\n *\n * <p>此外，还提供了{@link Selector}接口用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象，\n * 默认提供了四个实现类：\n * <ul>\n *     <li>{@link Selector#NEAREST_AND_OLDEST_PRIORITY}: 返回距离根对象更近的对象，当距离一样时优先返回旧对象；</li>\n *     <li>{@link Selector#NEAREST_AND_NEWEST_PRIORITY}: 返回距离根对象更近的对象，当距离一样时优先返回新对象；</li>\n *     <li>{@link Selector#FARTHEST_AND_OLDEST_PRIORITY}: 返回距离根对象更远的对象，当距离一样时优先返回旧对象；</li>\n *     <li>{@link Selector#FARTHEST_AND_NEWEST_PRIORITY}: 返回距离根对象更远的对象，当距离一样时优先返回新对象；</li>\n * </ul>\n *\n * @author huangchengxing\n */\npublic interface Hierarchical extends Comparable<Hierarchical> {\n\n\t// ====================== compare  ======================\n\n\t/**\n\t * 默认{@link #getHorizontalDistance()}与{@link #getVerticalDistance()}排序的比较器\n\t */\n\tComparator<Hierarchical> DEFAULT_HIERARCHICAL_COMPARATOR = Comparator\n\t\t.comparing(Hierarchical::getVerticalDistance)\n\t\t.thenComparing(Hierarchical::getHorizontalDistance);\n\n\t/**\n\t * 按{@link #getVerticalDistance()}和{@link #getHorizontalDistance()}排序\n\t *\n\t * @param o {@link SynthesizedAnnotation}对象\n\t * @return 比较值\n\t */\n\t@Override\n\tdefault int compareTo(Hierarchical o) {\n\t\treturn DEFAULT_HIERARCHICAL_COMPARATOR.compare(this, o);\n\t}\n\n\t// ====================== hierarchical  ======================\n\n\t/**\n\t * 参照物，即坐标为{@code (0, 0)}的对象。\n\t * 当对象本身即为参照物时，该方法应当返回其本身\n\t *\n\t * @return 参照物\n\t */\n\tObject getRoot();\n\n\t/**\n\t * 获取该对象与参照物的垂直距离。\n\t * 默认情况下，该距离即为当前对象与参照物之间相隔的层级数。\n\t *\n\t * @return 合成注解与根对象的垂直距离\n\t */\n\tint getVerticalDistance();\n\n\t/**\n\t * 获取该对象与参照物的水平距离。\n\t * 默认情况下，该距离即为当前对象在与参照物{@link #getVerticalDistance()}相同的情况下条，\n\t * 该对象被扫描到的顺序。\n\t *\n\t * @return 合成注解与根对象的水平距离\n\t */\n\tint getHorizontalDistance();\n\n\t// ====================== selector  ======================\n\n\t/**\n\t * {@link Hierarchical}选择器，用于根据一定的规则从两个{@link Hierarchical}实现类中选择并返回一个最合适的对象\n\t */\n\t@FunctionalInterface\n\tinterface Selector {\n\n\t\t/**\n\t\t * 返回距离根对象更近的对象，当距离一样时优先返回旧对象\n\t\t */\n\t\tSelector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector();\n\n\t\t/**\n\t\t * 返回距离根对象更近的对象，当距离一样时优先返回新对象\n\t\t */\n\t\tSelector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector();\n\n\t\t/**\n\t\t * 返回距离根对象更远的对象，当距离一样时优先返回旧对象\n\t\t */\n\t\tSelector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector();\n\n\t\t/**\n\t\t * 返回距离根对象更远的对象，当距离一样时优先返回新对象\n\t\t */\n\t\tSelector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector();\n\n\t\t/**\n\t\t * 比较两个被合成的对象，选择其中的一个并返回\n\t\t *\n\t\t * @param <T>           复合注解类型\n\t\t * @param prev 上一对象，该参数不允许为空\n\t\t * @param next 下一对象，该参数不允许为空\n\t\t * @return 对象\n\t\t */\n\t\t<T extends Hierarchical> T choose(T prev, T next);\n\n\t\t/**\n\t\t * 返回距离根对象更近的注解，当距离一样时优先返回旧注解\n\t\t */\n\t\tclass NearestAndOldestPrioritySelector implements Selector {\n\t\t\t@Override\n\t\t\tpublic <T extends Hierarchical> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\t\treturn newAnnotation.getVerticalDistance() < oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * 返回距离根对象更近的注解，当距离一样时优先返回新注解\n\t\t */\n\t\tclass NearestAndNewestPrioritySelector implements Selector {\n\t\t\t@Override\n\t\t\tpublic <T extends Hierarchical> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\t\treturn newAnnotation.getVerticalDistance() <= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * 返回距离根对象更远的注解，当距离一样时优先返回旧注解\n\t\t */\n\t\tclass FarthestAndOldestPrioritySelector implements Selector {\n\t\t\t@Override\n\t\t\tpublic <T extends Hierarchical> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\t\treturn newAnnotation.getVerticalDistance() > oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * 返回距离根对象更远的注解，当距离一样时优先返回新注解\n\t\t */\n\t\tclass FarthestAndNewestPrioritySelector implements Selector {\n\t\t\t@Override\n\t\t\tpublic <T extends Hierarchical> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\t\treturn newAnnotation.getVerticalDistance() >= oldAnnotation.getVerticalDistance() ? newAnnotation : oldAnnotation;\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/Link.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * <p>用于在同一注解中，或具有一定关联的不同注解的属性中，表明这些属性之间具有特定的关联关系。\n * 在通过{@link SynthesizedAggregateAnnotation}获取合成注解后，合成注解获取属性值时会根据该注解进行调整。<br>\n *\n * <p>该注解存在三个字注解：{@link MirrorFor}、{@link ForceAliasFor}或{@link AliasFor}，\n * 使用三个子注解等同于{@link Link}。但是需要注意的是，\n * 当注解中的属性同时存在多个{@link Link}或基于{@link Link}的子注解时，\n * 仅有声明在被注解的属性最上方的注解会生效，其余注解都将被忽略。\n *\n * <b>注意：该注解的优先级低于{@link Alias}</b>\n *\n * @author huangchengxing\n * @see SynthesizedAggregateAnnotation\n * @see RelationType\n * @see AliasFor\n * @see MirrorFor\n * @see ForceAliasFor\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})\npublic @interface Link {\n\n\t/**\n\t * 产生关联的注解类型，当不指定时，默认指注释的属性所在的类\n\t *\n\t * @return 关联的注解类型\n\t */\n\tClass<? extends Annotation> annotation() default Annotation.class;\n\n\t/**\n\t * {@link #annotation()}指定注解中关联的属性\n\t *\n\t * @return 属性名\n\t */\n\tString attribute() default \"\";\n\n\t/**\n\t * {@link #attribute()}指定属性与当前注解的属性建的关联关系类型\n\t *\n\t * @return 关系类型\n\t */\n\tRelationType type() default RelationType.MIRROR_FOR;\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/MirrorFor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * <p>{@link Link}的子注解。表示注解的属性与指定的属性互为镜像，通过一个属性将能够获得对方的值。<br>\n * 它们遵循下述规则：\n * <ul>\n *     <li>互为镜像的两个属性，必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方；</li>\n *     <li>互为镜像的两个属性，类型必须一致；</li>\n *     <li>互为镜像的两个属性在获取值，且两者的值皆不同时，必须且仅允许有一个非默认值，该值被优先返回；</li>\n *     <li>互为镜像的两个属性，在值都为默认值或都不为默认值时，两者的值必须相等；</li>\n * </ul>\n * <b>注意，该注解与{@link Link}、{@link ForceAliasFor}或{@link AliasFor}一起使用时，将只有被声明在最上面的注解会生效</b>\n *\n * @author huangchengxing\n * @see Link\n * @see RelationType#MIRROR_FOR\n */\n@Link(type = RelationType.MIRROR_FOR)\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})\npublic @interface MirrorFor {\n\n\t/**\n\t * 产生关联的注解类型，当不指定时，默认指注释的属性所在的类\n\t *\n\t * @return 关联的注解类型\n\t */\n\t@Link(annotation = Link.class, attribute = \"annotation\", type = RelationType.FORCE_ALIAS_FOR)\n\tClass<? extends Annotation> annotation() default Annotation.class;\n\n\t/**\n\t * {@link #annotation()}指定注解中关联的属性\n\t *\n\t * @return 属性名\n\t */\n\t@Link(annotation = Link.class, attribute = \"attribute\", type = RelationType.FORCE_ALIAS_FOR)\n\tString attribute() default \"\";\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.text.CharSequenceUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\n/**\n * <p>用于处理注解对象中带有{@link Link}注解，且{@link Link#type()}为{@link RelationType#MIRROR_FOR}的属性。<br>\n * 当该处理器执行完毕后，原始合成注解中被{@link Link}注解的属性与{@link Link}注解指向的目标注解的属性，\n * 都将会被被包装并替换为{@link MirroredAnnotationAttribute}。\n *\n * @author huangchengxing\n * @see RelationType#MIRROR_FOR\n * @see MirroredAnnotationAttribute\n */\npublic class MirrorLinkAnnotationPostProcessor extends AbstractLinkAnnotationPostProcessor {\n\n\tprivate static final RelationType[] PROCESSED_RELATION_TYPES = new RelationType[]{ RelationType.MIRROR_FOR };\n\n\t@Override\n\tpublic int order() {\n\t\treturn Integer.MIN_VALUE + 1;\n\t}\n\n\t/**\n\t * 该处理器只处理{@link Link#type()}类型为{@link RelationType#MIRROR_FOR}的注解属性\n\t *\n\t * @return 仅有{@link RelationType#MIRROR_FOR}数组\n\t */\n\t@Override\n\tprotected RelationType[] processTypes() {\n\t\treturn PROCESSED_RELATION_TYPES;\n\t}\n\n\t/**\n\t * 将存在镜像关系的合成注解属性分别包装为{@link MirroredAnnotationAttribute}对象，\n\t * 并使用包装后{@link MirroredAnnotationAttribute}替换在它们对应合成注解实例中的{@link AnnotationAttribute}\n\t *\n\t * @param synthesizer        注解合成器\n\t * @param annotation         {@code originalAttribute}上的{@link Link}注解对象\n\t * @param originalAnnotation 当前正在处理的{@link SynthesizedAnnotation}对象\n\t * @param originalAttribute  {@code originalAnnotation}上的待处理的属性\n\t * @param linkedAnnotation   {@link Link}指向的关联注解对象\n\t * @param linkedAttribute    {@link Link}指向的{@code originalAnnotation}中的关联属性，该参数可能为空\n\t */\n\t@Override\n\tprotected void processLinkedAttribute(\n\t\tAnnotationSynthesizer synthesizer, Link annotation,\n\t\tSynthesizedAnnotation originalAnnotation, AnnotationAttribute originalAttribute,\n\t\tSynthesizedAnnotation linkedAnnotation, AnnotationAttribute linkedAttribute) {\n\n\t\t// 镜像属性必然成对出现，因此此处必定存在三种情况：\n\t\t// 1.两属性都不为镜像属性，此时继续进行后续处理；\n\t\t// 2.两属性都为镜像属性，并且指向对方，此时无需后续处理；\n\t\t// 3.两属性仅有任意一属性为镜像属性，此时镜像属性必然未指向当前原始属性，此时应该抛出异常；\n\t\tif (originalAttribute instanceof MirroredAnnotationAttribute\n\t\t\t|| linkedAttribute instanceof MirroredAnnotationAttribute) {\n\t\t\tcheckMirrored(originalAttribute, linkedAttribute);\n\t\t\treturn;\n\t\t}\n\n\t\t// 校验镜像关系\n\t\tcheckMirrorRelation(annotation, originalAttribute, linkedAttribute);\n\t\t// 包装这一对镜像属性，并替换原注解中的对应属性\n\t\tfinal AnnotationAttribute mirroredOriginalAttribute = new MirroredAnnotationAttribute(originalAttribute, linkedAttribute);\n\t\toriginalAnnotation.setAttribute(originalAttribute.getAttributeName(), mirroredOriginalAttribute);\n\t\tfinal AnnotationAttribute mirroredTargetAttribute = new MirroredAnnotationAttribute(linkedAttribute, originalAttribute);\n\t\tlinkedAnnotation.setAttribute(annotation.attribute(), mirroredTargetAttribute);\n\t}\n\n\t/**\n\t * 检查映射关系是否正确\n\t */\n\tprivate void checkMirrored(AnnotationAttribute original, AnnotationAttribute mirror) {\n\t\tfinal boolean originalAttributeMirrored = original instanceof MirroredAnnotationAttribute;\n\t\tfinal boolean mirrorAttributeMirrored = mirror instanceof MirroredAnnotationAttribute;\n\n\t\t// 校验通过\n\t\tfinal boolean passed = originalAttributeMirrored && mirrorAttributeMirrored\n\t\t\t&& ObjectUtil.equals(((MirroredAnnotationAttribute)original).getLinked(), ((MirroredAnnotationAttribute)mirror).getOriginal());\n\t\tif (passed) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 校验失败，拼装异常信息用于抛出异常\n\t\tString errorMsg;\n\t\t// 原始字段已经跟其他字段形成镜像\n\t\tif (originalAttributeMirrored && !mirrorAttributeMirrored) {\n\t\t\terrorMsg = CharSequenceUtil.format(\n\t\t\t\t\"attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]\",\n\t\t\t\toriginal.getAttribute(), mirror.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked()\n\t\t\t);\n\t\t}\n\t\t// 镜像字段已经跟其他字段形成镜像\n\t\telse if (!originalAttributeMirrored && mirrorAttributeMirrored) {\n\t\t\terrorMsg = CharSequenceUtil.format(\n\t\t\t\t\"attribute [{}] cannot mirror for [{}], because it's already mirrored for [{}]\",\n\t\t\t\tmirror.getAttribute(), original.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked()\n\t\t\t);\n\t\t}\n\t\t// 两者都形成了镜像，但是都未指向对方，理论上不会存在该情况\n\t\telse {\n\t\t\terrorMsg = CharSequenceUtil.format(\n\t\t\t\t\"attribute [{}] cannot mirror for [{}], because [{}] already mirrored for [{}] and  [{}] already mirrored for [{}]\",\n\t\t\t\tmirror.getAttribute(), original.getAttribute(),\n\t\t\t\tmirror.getAttribute(), ((MirroredAnnotationAttribute)mirror).getLinked(),\n\t\t\t\toriginal.getAttribute(), ((MirroredAnnotationAttribute)original).getLinked()\n\t\t\t);\n\t\t}\n\n\t\tthrow new IllegalArgumentException(errorMsg);\n\t}\n\n\t/**\n\t * 基本校验\n\t */\n\tprivate void checkMirrorRelation(Link annotation, AnnotationAttribute original, AnnotationAttribute mirror) {\n\t\t// 镜像属性必须存在\n\t\tcheckLinkedAttributeNotNull(original, mirror, annotation);\n\t\t// 镜像属性返回值必须一致\n\t\tcheckAttributeType(original, mirror);\n\t\t// 镜像属性上必须存在对应的注解\n\t\tfinal Link mirrorAttributeAnnotation = getLinkAnnotation(mirror, RelationType.MIRROR_FOR);\n\t\tAssert.isTrue(\n\t\t\tObjectUtil.isNotNull(mirrorAttributeAnnotation) && RelationType.MIRROR_FOR.equals(mirrorAttributeAnnotation.type()),\n\t\t\t\"mirror attribute [{}] of original attribute [{}] must marked by @Link, and also @LinkType.type() must is [{}]\",\n\t\t\tmirror.getAttribute(), original.getAttribute(), RelationType.MIRROR_FOR\n\t\t);\n\t\tcheckLinkedSelf(original, mirror);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/MirroredAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * 表示存在对应镜像属性的注解属性，当获取值时将根据{@link RelationType#MIRROR_FOR}的规则进行处理\n *\n * @author huangchengxing\n * @see MirrorLinkAnnotationPostProcessor\n * @see RelationType#MIRROR_FOR\n */\npublic class MirroredAnnotationAttribute extends AbstractWrappedAnnotationAttribute {\n\n\tpublic MirroredAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {\n\t\tsuper(origin, linked);\n\t}\n\n\t@Override\n\tpublic Object getValue() {\n\t\tfinal boolean originIsDefault = original.isValueEquivalentToDefaultValue();\n\t\tfinal boolean targetIsDefault = linked.isValueEquivalentToDefaultValue();\n\t\tfinal Object originValue = original.getValue();\n\t\tfinal Object targetValue = linked.getValue();\n\n\t\t// 都为默认值，或都为非默认值时，两方法的返回值必须相等\n\t\tif (originIsDefault == targetIsDefault) {\n\t\t\tAssert.equals(\n\t\t\t\toriginValue, targetValue,\n\t\t\t\t\"the values of attributes [{}] and [{}] that mirror each other are different: [{}] <==> [{}]\",\n\t\t\t\toriginal.getAttribute(), linked.getAttribute(), originValue, targetValue\n\t\t\t);\n\t\t\treturn originValue;\n\t\t}\n\n\t\t// 两者有一者不为默认值时，优先返回非默认值\n\t\treturn originIsDefault ? targetValue : originValue;\n\t}\n\n\t/**\n\t * 当{@link #original}与{@link #linked}都为默认值时返回{@code true}\n\t *\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean isValueEquivalentToDefaultValue() {\n\t\treturn original.isValueEquivalentToDefaultValue() && linked.isValueEquivalentToDefaultValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/PropIgnore.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 属性忽略注解，使用此注解的字段等会被忽略，主要用于Bean拷贝、Bean转Map等<br>\n * 此注解应用于字段时，忽略读取和设置属性值，应用于setXXX方法忽略设置值，应用于getXXX忽略读取值\n *\n * @author Looly\n * @since 5.4.2\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})\npublic @interface PropIgnore {\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/RelationType.java",
    "content": "package cn.hutool.core.annotation;\n\n/**\n * <p>注解属性的关系类型 <br>\n * 若将被{@link Link}注解的属性称为“原始属性”，而在{@link Link}注解中指向的注解属性称为“关联属性”，\n * 则该枚举用于描述“原始属性”与“关联属性”在{@link SynthesizedAggregateAnnotation}处理过程中的作用关系。<br>\n * 根据在{@link Link#type()}中指定的关系类型的不同，通过{@link SynthesizedAggregateAnnotation}合成的注解的属性值也将有所变化。\n *\n * <p>当一个注解中的所有属性同时具备多种关系时，将依次按下述顺序处理：\n * <ol>\n *     <li>属性上的{@link Alias}注解；</li>\n *     <li>属性上的{@link Link}注解，且{@link Link#type()}为{@link #MIRROR_FOR}；</li>\n *     <li>属性上的{@link Link}注解，且{@link Link#type()}为{@link #FORCE_ALIAS_FOR}；</li>\n *     <li>属性上的{@link Link}注解，且{@link Link#type()}为{@link #ALIAS_FOR}；</li>\n * </ol>\n *\n * @author huangchengxing\n * @see SynthesizedAggregateAnnotation\n * @see Link\n */\npublic enum RelationType {\n\n\t/**\n\t * <p>表示注解的属性与指定的属性互为镜像，通过一个属性将能够获得对方的值。<br>\n\t * 它们遵循下述规则：\n\t * <ul>\n\t *     <li>互为镜像的两个属性，必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方；</li>\n\t *     <li>互为镜像的两个属性，类型必须一致；</li>\n\t *     <li>互为镜像的两个属性在获取值，且两者的值皆不同时，必须且仅允许有一个非默认值，该值被优先返回；</li>\n\t *     <li>互为镜像的两个属性，在值都为默认值或都不为默认值时，两者的值必须相等；</li>\n\t * </ul>\n\t */\n\tMIRROR_FOR,\n\n\t/**\n\t * <p>表示“原始属性”将作为“关联属性”的别名。\n\t * <ul>\n\t *     <li>当“原始属性”为默认值时，获取“关联属性”将返回“关联属性”本身的值；</li>\n\t *     <li>当“原始属性”不为默认值时，获取“关联属性”将返回“原始属性”的值；</li>\n\t * </ul>\n\t */\n\tALIAS_FOR,\n\n\t/**\n\t * <p>表示“原始属性”将强制作为“关联属性”的别名。效果等同于在“原始属性”上添加{@link Alias}注解，\n\t * 任何情况下，获取“关联属性”的值都将直接返回“原始属性”的值\n\t */\n\tFORCE_ALIAS_FOR;\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAggregateAnnotation.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Annotation;\n\n/**\n * <p>表示基于特定规则聚合，将一组注解聚合而来的注解对象，\n * 该注解对象允许根据一定规则“合成”一些跟原始注解属性不一样合成注解。\n *\n * <p>合成注解一般被用于处理类层级结果中具有直接或间接关联的注解对象，\n * 当实例被创建时，会获取到这些注解对象，并使用{@link SynthesizedAnnotationSelector}对类型相同的注解进行过滤，\n * 并最终得到类型不重复的有效注解对象。这些有效注解将被包装为{@link SynthesizedAnnotation}，\n * 然后最终用于“合成”一个{@link SynthesizedAggregateAnnotation}。<br>\n * {@link SynthesizedAnnotationSelector}是合成注解生命周期中的第一个钩子，\n * 自定义选择器以拦截原始注解被扫描的过程。\n *\n * <p>当合成注解完成对待合成注解的扫描，并完成了必要属性的加载后，\n * 将会按顺序依次调用{@link SynthesizedAnnotationPostProcessor}，\n * 注解后置处理器允许用于对完成注解的待合成注解进行二次调整，\n * 该钩子一般用于根据{@link Link}注解对属性进行调整。<br>\n * {@link SynthesizedAnnotationPostProcessor}是合成注解生命周期中的第二个钩子，\n * 自定义后置处理器以拦截原始在转为待合成注解后的初始化过程。\n *\n * <p>合成注解允许通过{@link #synthesize(Class)}合成一个指定的注解对象，\n * 该方法返回的注解对象可能是原始的注解对象，也有可能通过动态代理的方式生成，\n * 该对象实例的属性不一定来自对象本身，而是来自于经过{@link SynthesizedAnnotationAttributeProcessor}\n * 处理后的、用于合成当前实例的全部关联注解的相关属性。<br>\n * {@link SynthesizedAnnotationAttributeProcessor}是合成注解生命周期中的第三个钩子，\n * 自定义属性处理器以拦截合成注解的取值过程。\n *\n * @author huangchengxing\n * @see AnnotationSynthesizer\n * @see SynthesizedAnnotation\n * @see SynthesizedAnnotationSelector\n * @see SynthesizedAnnotationAttributeProcessor\n * @see SynthesizedAnnotationPostProcessor\n * @see GenericSynthesizedAggregateAnnotation\n */\npublic interface SynthesizedAggregateAnnotation extends AggregateAnnotation, Hierarchical, AnnotationSynthesizer, AnnotationAttributeValueProvider {\n\n\t// ================== hierarchical ==================\n\n\t/**\n\t * 距离{@link #getRoot()}返回值的垂直距离，\n\t * 默认聚合注解即为根对象，因此返回0\n\t *\n\t * @return 距离{@link #getRoot()}返回值的水平距离，\n\t */\n\t@Override\n\tdefault int getVerticalDistance() {\n\t\treturn 0;\n\t}\n\n\t/**\n\t * 距离{@link #getRoot()}返回值的水平距离，\n\t * 默认聚合注解即为根对象，因此返回0\n\t *\n\t * @return 距离{@link #getRoot()}返回值的水平距离，\n\t */\n\t@Override\n\tdefault int getHorizontalDistance() {\n\t\treturn 0;\n\t}\n\n\t// ================== synthesize ==================\n\n\t/**\n\t * 获取在聚合中存在的指定注解对象\n\t *\n\t * @param annotationType 注解类型\n\t * @param <T>            注解类型\n\t * @return 注解对象\n\t */\n\t<T extends Annotation> T getAnnotation(Class<T> annotationType);\n\n\t/**\n\t * 获取合成注解属性处理器\n\t *\n\t * @return 合成注解属性处理器\n\t */\n\tSynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor();\n\n\t/**\n\t * 获取当前的注解类型\n\t *\n\t * @return 注解类型\n\t */\n\t@Override\n\tdefault Class<? extends Annotation> annotationType() {\n\t\treturn this.getClass();\n\t}\n\n\t/**\n\t * 从聚合中获取指定类型的属性值\n\t *\n\t * @param attributeName 属性名称\n\t * @param attributeType 属性类型\n\t * @return 属性值\n\t */\n\t@Override\n\tObject getAttributeValue(String attributeName, Class<?> attributeType);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotation.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.collection.CollUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\n/**\n * <p>用于在{@link SynthesizedAggregateAnnotation}中表示一个处于合成状态的注解对象。<br>\n * 当对多个合成注解排序时，默认使用{@link #DEFAULT_HIERARCHICAL_COMPARATOR}进行排序，\n * 从保证合成注解按{@link #getVerticalDistance()}与{@link #getHorizontalDistance()}的返回值保持有序，\n * 从而使得距离根元素更接近的注解对象在被处理是具有更高的优先级。\n *\n * @author huangchengxing\n * @see SynthesizedAggregateAnnotation\n */\npublic interface SynthesizedAnnotation extends Annotation, Hierarchical, AnnotationAttributeValueProvider {\n\n\t/**\n\t * 获取被合成的注解对象\n\t *\n\t * @return 注解对象\n\t */\n\tAnnotation getAnnotation();\n\n\t/**\n\t * 获取该合成注解与根对象的垂直距离。\n\t * 默认情况下，该距离即为当前注解与根对象之间相隔的层级数。\n\t *\n\t * @return 合成注解与根对象的垂直距离\n\t */\n\t@Override\n\tint getVerticalDistance();\n\n\t/**\n\t * 获取该合成注解与根对象的水平距离。\n\t * 默认情况下，该距离即为当前注解与根对象之间相隔的已经被扫描到的注解数。\n\t *\n\t * @return 合成注解与根对象的水平距离\n\t */\n\t@Override\n\tint getHorizontalDistance();\n\n\t/**\n\t * 注解是否存在该名称相同，且类型一致的属性\n\t *\n\t * @param attributeName 属性名\n\t * @param returnType    返回值类型\n\t * @return 是否存在该属性\n\t */\n\tboolean hasAttribute(String attributeName, Class<?> returnType);\n\n\t/**\n\t * 获取该注解的全部属性\n\t *\n\t * @return 注解属性\n\t */\n\tMap<String, AnnotationAttribute> getAttributes();\n\n\t/**\n\t * 设置该注解的全部属性\n\t *\n\t * @param attributes 注解属性\n\t */\n\tdefault void setAttributes(Map<String, AnnotationAttribute> attributes) {\n\t\tif (CollUtil.isNotEmpty(attributes)) {\n\t\t\tattributes.forEach(this::setAttribute);\n\t\t}\n\t}\n\n\t/**\n\t * 设置属性值\n\t *\n\t * @param attributeName 属性名称\n\t * @param attribute     注解属性\n\t */\n\tvoid setAttribute(String attributeName, AnnotationAttribute attribute);\n\n\t/**\n\t * 替换属性值\n\t *\n\t * @param attributeName 属性名\n\t * @param operator      替换操作\n\t */\n\tvoid replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator);\n\n\t/**\n\t * 获取属性值\n\t *\n\t * @param attributeName 属性名\n\t * @return 属性值\n\t */\n\tObject getAttributeValue(String attributeName);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationAttributeProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.util.Collection;\n\n/**\n * 合成注解属性选择器。用于在{@link SynthesizedAggregateAnnotation}中从指定类型的合成注解里获取到对应的属性值\n *\n * @author huangchengxing\n */\n@FunctionalInterface\npublic interface SynthesizedAnnotationAttributeProcessor {\n\n\t/**\n\t * 从一批被合成注解中，获取指定名称与类型的属性值\n\t *\n\t * @param attributeName          属性名称\n\t * @param attributeType          属性类型\n\t * @param synthesizedAnnotations 被合成的注解\n\t * @param <R> 属性类型\n\t * @return 属性值\n\t */\n\t<R> R getAttributeValue(String attributeName, Class<R> attributeType, Collection<? extends SynthesizedAnnotation> synthesizedAnnotations);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationPostProcessor.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.comparator.CompareUtil;\n\nimport java.util.Comparator;\n\n/**\n * <p>被合成注解后置处理器，用于在{@link SynthesizedAggregateAnnotation}加载完所有待合成注解后，\n * 再对加载好的{@link SynthesizedAnnotation}进行后置处理。<br>\n * 当多个{@link SynthesizedAnnotationPostProcessor}需要一起执行时，将按照{@link #order()}的返回值进行排序，\n * 该值更小的处理器将被优先执行。\n *\n * <p>该接口存在多个实现类，调用者应当保证在任何时候，对一批后置处理器的调用顺序都符合：\n * <ul>\n *     <li>{@link AliasAnnotationPostProcessor}；</li>\n *     <li>{@link MirrorLinkAnnotationPostProcessor}；</li>\n *     <li>{@link AliasLinkAnnotationPostProcessor}；</li>\n *     <li>其他后置处理器；</li>\n * </ul>\n *\n * @author huangchengxing\n * @see AliasAnnotationPostProcessor\n * @see MirrorLinkAnnotationPostProcessor\n * @see AliasLinkAnnotationPostProcessor\n */\npublic interface SynthesizedAnnotationPostProcessor extends Comparable<SynthesizedAnnotationPostProcessor> {\n\n\t/**\n\t * 属性上带有{@link Alias}的注解对象的后置处理器\n\t */\n\tAliasAnnotationPostProcessor ALIAS_ANNOTATION_POST_PROCESSOR = new AliasAnnotationPostProcessor();\n\n\t/**\n\t * 属性上带有{@link Link}，且与其他注解的属性存在镜像关系的注解对象的后置处理器\n\t */\n\tMirrorLinkAnnotationPostProcessor MIRROR_LINK_ANNOTATION_POST_PROCESSOR = new MirrorLinkAnnotationPostProcessor();\n\n\t/**\n\t * 属性上带有{@link Link}，且与其他注解的属性存在别名关系的注解对象的后置处理器\n\t */\n\tAliasLinkAnnotationPostProcessor ALIAS_LINK_ANNOTATION_POST_PROCESSOR = new AliasLinkAnnotationPostProcessor();\n\n\t/**\n\t * 在一组后置处理器中被调用的顺序，越小越靠前\n\t *\n\t * @return 排序值\n\t */\n\tdefault int order() {\n\t\treturn Integer.MAX_VALUE;\n\t}\n\n\t/**\n\t * 比较两个后置处理器的{@link #order()}返回值\n\t *\n\t * @param o 比较对象\n\t * @return 大小\n\t */\n\t@Override\n\tdefault int compareTo(SynthesizedAnnotationPostProcessor o) {\n\t\treturn CompareUtil.compare(this, o, Comparator.comparing(SynthesizedAnnotationPostProcessor::order));\n\t}\n\n\t/**\n\t * 给定指定被合成注解与其所属的合成注解聚合器实例，经过处理后返回最终\n\t *\n\t * @param synthesizedAnnotation 合成的注解\n\t * @param synthesizer           注解合成器\n\t */\n\tvoid process(SynthesizedAnnotation synthesizedAnnotation, AnnotationSynthesizer synthesizer);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationProxy.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.text.CharSequenceUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.BiFunction;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * 合成注解代理类，用于为{@link SynthesizedAnnotation}生成对应的合成注解代理对象\n *\n * @author huangchengxing\n * @see SynthesizedAnnotation\n * @see AnnotationAttributeValueProvider\n */\npublic class SynthesizedAnnotationProxy implements InvocationHandler {\n\n\tprivate final AnnotationAttributeValueProvider annotationAttributeValueProvider;\n\tprivate final SynthesizedAnnotation annotation;\n\tprivate final Map<String, BiFunction<Method, Object[], Object>> methods;\n\n\t/**\n\t * 创建一个代理注解，生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。\n\t *\n\t * @param <T>                              注解类型\n\t * @param annotationType                   注解类型\n\t * @param annotationAttributeValueProvider 注解属性值获取器\n\t * @param annotation                       合成注解\n\t * @return 代理注解\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Annotation> T create(\n\t\t\tClass<T> annotationType,\n\t\t\tAnnotationAttributeValueProvider annotationAttributeValueProvider,\n\t\t\tSynthesizedAnnotation annotation) {\n\t\tif (ObjectUtil.isNull(annotation)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal SynthesizedAnnotationProxy proxyHandler = new SynthesizedAnnotationProxy(annotationAttributeValueProvider, annotation);\n\t\tif (ObjectUtil.isNull(annotation)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (T) Proxy.newProxyInstance(\n\t\t\t\tannotationType.getClassLoader(),\n\t\t\t\tnew Class[]{annotationType, SyntheticProxyAnnotation.class},\n\t\t\t\tproxyHandler\n\t\t);\n\t}\n\n\t/**\n\t * 创建一个代理注解，生成的代理对象将是{@link SyntheticProxyAnnotation}与指定的注解类的子类。\n\t *\n\t * @param <T>            注解类型\n\t * @param annotationType 注解类型\n\t * @param annotation     合成注解\n\t * @return 代理注解\n\t */\n\tpublic static <T extends Annotation> T create(\n\t\t\tClass<T> annotationType, SynthesizedAnnotation annotation) {\n\t\treturn create(annotationType, annotation, annotation);\n\t}\n\n\t/**\n\t * 该类是否为通过{@code SynthesizedAnnotationProxy}生成的代理类\n\t *\n\t * @param annotationType 注解类型\n\t * @return 是否\n\t */\n\tpublic static boolean isProxyAnnotation(Class<?> annotationType) {\n\t\treturn ClassUtil.isAssignable(SyntheticProxyAnnotation.class, annotationType);\n\t}\n\n\tSynthesizedAnnotationProxy(AnnotationAttributeValueProvider annotationAttributeValueProvider, SynthesizedAnnotation annotation) {\n\t\tAssert.notNull(annotationAttributeValueProvider, \"annotationAttributeValueProvider must not null\");\n\t\tAssert.notNull(annotation, \"annotation must not null\");\n\t\tthis.annotationAttributeValueProvider = annotationAttributeValueProvider;\n\t\tthis.annotation = annotation;\n\t\tthis.methods = new HashMap<>(9);\n\t\tloadMethods();\n\t}\n\n\t@Override\n\tpublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n\t\treturn Opt.ofNullable(methods.get(method.getName()))\n\t\t\t\t.map(m -> m.apply(method, args))\n\t\t\t\t.orElseGet(() -> ReflectUtil.invoke(annotation.getAnnotation(), method, args));\n\t}\n\n\t// ========================= 代理方法 =========================\n\n\tvoid loadMethods() {\n\t\t// 非用户属性\n\t\tmethods.put(\"toString\", (method, args) -> proxyToString());\n\t\tmethods.put(\"hashCode\", (method, args) -> proxyHashCode());\n\t\tmethods.put(\"getSynthesizedAnnotation\", (method, args) -> proxyGetSynthesizedAnnotation());\n\t\tmethods.put(\"getRoot\", (method, args) -> annotation.getRoot());\n\t\tmethods.put(\"getVerticalDistance\", (method, args) -> annotation.getVerticalDistance());\n\t\tmethods.put(\"getHorizontalDistance\", (method, args) -> annotation.getHorizontalDistance());\n\t\tmethods.put(\"hasAttribute\", (method, args) -> annotation.hasAttribute((String) args[0], (Class<?>) args[1]));\n\t\tmethods.put(\"getAttributes\", (method, args) -> annotation.getAttributes());\n\t\tmethods.put(\"setAttribute\", (method, args) -> {\n\t\t\tthrow new UnsupportedOperationException(\"proxied annotation can not reset attributes\");\n\t\t});\n\t\tmethods.put(\"getAttributeValue\", (method, args) -> annotation.getAttributeValue((String) args[0]));\n\t\tmethods.put(\"annotationType\", (method, args) -> annotation.annotationType());\n\n\t\t// 可以被合成的用户属性\n\t\tStream.of(ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType()))\n\t\t\t.filter(m -> !methods.containsKey(m.getName()))\n\t\t\t.forEach(m -> methods.put(m.getName(), (method, args) -> proxyAttributeValue(method)));\n\t}\n\n\tprivate String proxyToString() {\n\t\tfinal String attributes = Stream.of(ClassUtil.getDeclaredMethods(annotation.getAnnotation().annotationType()))\n\t\t\t\t.filter(AnnotationUtil::isAttributeMethod)\n\t\t\t\t.map(method -> CharSequenceUtil.format(\n\t\t\t\t\t\t\"{}={}\", method.getName(), proxyAttributeValue(method))\n\t\t\t\t)\n\t\t\t\t.collect(Collectors.joining(\", \"));\n\t\treturn CharSequenceUtil.format(\"@{}({})\", annotation.annotationType().getName(), attributes);\n\t}\n\n\tprivate int proxyHashCode() {\n\t\treturn Objects.hash(annotationAttributeValueProvider, annotation);\n\t}\n\n\tprivate Object proxyGetSynthesizedAnnotation() {\n\t\treturn annotation;\n\t}\n\n\tprivate Object proxyAttributeValue(Method attributeMethod) {\n\t\treturn annotationAttributeValueProvider.getAttributeValue(attributeMethod.getName(), attributeMethod.getReturnType());\n\t}\n\n\t/**\n\t * 通过代理类生成的合成注解\n\t *\n\t * @author huangchengxing\n\t */\n\tinterface SyntheticProxyAnnotation extends SynthesizedAnnotation {\n\n\t\t/**\n\t\t * 获取该代理注解对应的已合成注解\n\t\t *\n\t\t * @return 理注解对应的已合成注解\n\t\t */\n\t\tSynthesizedAnnotation getSynthesizedAnnotation();\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/SynthesizedAnnotationSelector.java",
    "content": "package cn.hutool.core.annotation;\n\n/**\n * 注解选择器，指定两个注解，选择其中一个返回。<br>\n * 该接口用于在{@link SynthesizedAggregateAnnotation}中用于从一批相同的注解对象中筛选最终用于合成注解对象。\n *\n * @author huangchengxing\n */\n@FunctionalInterface\npublic interface SynthesizedAnnotationSelector {\n\n\t/**\n\t * 返回距离根对象更近的注解，当距离一样时优先返回旧注解\n\t */\n\tSynthesizedAnnotationSelector NEAREST_AND_OLDEST_PRIORITY = new NearestAndOldestPrioritySelector();\n\n\t/**\n\t * 返回距离根对象更近的注解，当距离一样时优先返回新注解\n\t */\n\tSynthesizedAnnotationSelector NEAREST_AND_NEWEST_PRIORITY = new NearestAndNewestPrioritySelector();\n\n\t/**\n\t * 返回距离根对象更远的注解，当距离一样时优先返回旧注解\n\t */\n\tSynthesizedAnnotationSelector FARTHEST_AND_OLDEST_PRIORITY = new FarthestAndOldestPrioritySelector();\n\n\t/**\n\t * 返回距离根对象更远的注解，当距离一样时优先返回新注解\n\t */\n\tSynthesizedAnnotationSelector FARTHEST_AND_NEWEST_PRIORITY = new FarthestAndNewestPrioritySelector();\n\n\t/**\n\t * 比较两个被合成的注解，选择其中的一个并返回\n\t *\n\t * @param <T>           复合注解类型\n\t * @param oldAnnotation 已存在的注解，该参数不允许为空\n\t * @param newAnnotation 新获取的注解，该参数不允许为空\n\t * @return 被合成的注解\n\t */\n\t<T extends SynthesizedAnnotation> T choose(T oldAnnotation, T newAnnotation);\n\n\t/**\n\t * 返回距离根对象更近的注解，当距离一样时优先返回旧注解\n\t */\n\tclass NearestAndOldestPrioritySelector implements SynthesizedAnnotationSelector {\n\t\t@Override\n\t\tpublic <T extends SynthesizedAnnotation> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\treturn Hierarchical.Selector.NEAREST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation);\n\t\t}\n\t}\n\n\t/**\n\t * 返回距离根对象更近的注解，当距离一样时优先返回新注解\n\t */\n\tclass NearestAndNewestPrioritySelector implements SynthesizedAnnotationSelector {\n\t\t@Override\n\t\tpublic <T extends SynthesizedAnnotation> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\treturn Hierarchical.Selector.NEAREST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation);\n\t\t}\n\t}\n\n\t/**\n\t * 返回距离根对象更远的注解，当距离一样时优先返回旧注解\n\t */\n\tclass FarthestAndOldestPrioritySelector implements SynthesizedAnnotationSelector {\n\t\t@Override\n\t\tpublic <T extends SynthesizedAnnotation> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\treturn Hierarchical.Selector.FARTHEST_AND_OLDEST_PRIORITY.choose(oldAnnotation, newAnnotation);\n\t\t}\n\t}\n\n\t/**\n\t * 返回距离根对象更远的注解，当距离一样时优先返回新注解\n\t */\n\tclass FarthestAndNewestPrioritySelector implements SynthesizedAnnotationSelector {\n\t\t@Override\n\t\tpublic <T extends SynthesizedAnnotation> T choose(T oldAnnotation, T newAnnotation) {\n\t\t\treturn Hierarchical.Selector.FARTHEST_AND_NEWEST_PRIORITY.choose(oldAnnotation, newAnnotation);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/WrappedAnnotationAttribute.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\n\n/**\n * <p>表示一个被包装过的{@link AnnotationAttribute}，\n * 该实例中的一些方法可能会被代理到另一个注解属性对象中，\n * 从而使得通过原始的注解属性的方法获取到另一注解属性的值。<br>\n * 除了{@link #getValue()}以外，其他方法的返回值应当尽可能与{@link #getOriginal()}\n * 返回的{@link AnnotationAttribute}对象的方法返回值一致。\n *\n * <p>当包装类被包装了多层后，则规则生效优先级按包装的先后顺序倒序排序，\n * 比如a、b互为镜像，此时a、b两属性应当都被{@link MirroredAnnotationAttribute}包装，\n * 若再指定c为a的别名字段，则c、a、b都要在原基础上再次包装一层{@link AliasedAnnotationAttribute}。<br>\n * 此时a、b同时被包装了两层，则执行时，优先执行{@link AliasedAnnotationAttribute}的逻辑，\n * 当该规则不生效时，比如c只有默认值，此时上一次的{@link MirroredAnnotationAttribute}的逻辑才会生效。\n *\n * <p>被包装的{@link AnnotationAttribute}实际结构为一颗二叉树，\n * 当包装类再次被包装时，实际上等于又添加了一个新的根节点，\n * 此时需要同时更新树的全部关联叶子节点。\n *\n * @author huangchengxing\n * @see AnnotationAttribute\n * @see ForceAliasedAnnotationAttribute\n * @see AliasedAnnotationAttribute\n * @see MirroredAnnotationAttribute\n */\npublic interface WrappedAnnotationAttribute extends AnnotationAttribute {\n\n\t// =========================== 新增方法 ===========================\n\n\t/**\n\t * 获取被包装的{@link AnnotationAttribute}对象，该对象也可能是{@link AnnotationAttribute}\n\t *\n\t * @return 被包装的{@link AnnotationAttribute}对象\n\t */\n\tAnnotationAttribute getOriginal();\n\n\t/**\n\t * 获取最初的被包装的{@link AnnotationAttribute}\n\t *\n\t * @return 最初的被包装的{@link AnnotationAttribute}\n\t */\n\tAnnotationAttribute getNonWrappedOriginal();\n\n\t/**\n\t * 获取包装{@link #getOriginal()}的{@link AnnotationAttribute}对象，该对象也可能是{@link AnnotationAttribute}\n\t *\n\t * @return 包装对象\n\t */\n\tAnnotationAttribute getLinked();\n\n\t/**\n\t * 遍历以当前实例为根节点的树结构，获取所有未被包装的属性\n\t *\n\t * @return 叶子节点\n\t */\n\tCollection<AnnotationAttribute> getAllLinkedNonWrappedAttributes();\n\n\t// =========================== 代理实现 ===========================\n\n\t/**\n\t * 获取注解对象\n\t *\n\t * @return 注解对象\n\t */\n\t@Override\n\tdefault Annotation getAnnotation() {\n\t\treturn getOriginal().getAnnotation();\n\t}\n\n\t/**\n\t * 获取注解属性对应的方法\n\t *\n\t * @return 注解属性对应的方法\n\t */\n\t@Override\n\tdefault Method getAttribute() {\n\t\treturn getOriginal().getAttribute();\n\t}\n\n\t/**\n\t * 该注解属性的值是否等于默认值 <br>\n\t * 默认仅当{@link #getOriginal()}与{@link #getLinked()}返回的注解属性\n\t * 都为默认值时，才返回{@code true}\n\t *\n\t * @return 该注解属性的值是否等于默认值\n\t */\n\t@Override\n\tboolean isValueEquivalentToDefaultValue();\n\n\t/**\n\t * 获取属性类型\n\t *\n\t * @return 属性类型\n\t */\n\t@Override\n\tdefault Class<?> getAttributeType() {\n\t\treturn getOriginal().getAttributeType();\n\t}\n\n\t/**\n\t * 获取属性上的注解\n\t *\n\t * @param annotationType 注解类型\n\t * @return 注解对象\n\t */\n\t@Override\n\tdefault <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\treturn getOriginal().getAnnotation(annotationType);\n\t}\n\n\t/**\n\t * 当前注解属性是否已经被{@link WrappedAnnotationAttribute}包装\n\t *\n\t * @return boolean\n\t */\n\t@Override\n\tdefault boolean isWrapped() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/package-info.java",
    "content": "/**\n * 注解包，提供增强型注解和注解工具类\n *\n * @author looly\n *\n */\npackage cn.hutool.core.annotation;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AbstractTypeAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.annotation.AnnotationUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Proxy;\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\nimport java.util.function.UnaryOperator;\n\n/**\n * 为需要从类的层级结构中获取注解的{@link AnnotationScanner}提供基本实现\n *\n * @author huangchengxing\n */\npublic abstract class AbstractTypeAnnotationScanner<T extends AbstractTypeAnnotationScanner<T>> implements AnnotationScanner {\n\n\t/**\n\t * 是否允许扫描父类\n\t */\n\tprivate boolean includeSuperClass;\n\n\t/**\n\t * 是否允许扫描父接口\n\t */\n\tprivate boolean includeInterfaces;\n\n\t/**\n\t * 过滤器，若类型无法通过该过滤器，则该类型及其树结构将直接不被查找\n\t */\n\tprivate Predicate<Class<?>> filter;\n\n\t/**\n\t * 排除的类型，以上类型及其树结构将直接不被查找\n\t */\n\tprivate final Set<Class<?>> excludeTypes;\n\n\t/**\n\t * 转换器\n\t */\n\tprivate final List<UnaryOperator<Class<?>>> converters;\n\n\t/**\n\t * 是否有转换器\n\t */\n\tprivate boolean hasConverters;\n\n\t/**\n\t * 当前实例\n\t */\n\tprivate final T typedThis;\n\n\t/**\n\t * 构造一个类注解扫描器\n\t *\n\t * @param includeSuperClass 是否允许扫描父类\n\t * @param includeInterfaces 是否允许扫描父接口\n\t * @param filter            过滤器\n\t * @param excludeTypes      不包含的类型\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprotected AbstractTypeAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate<Class<?>> filter, Set<Class<?>> excludeTypes) {\n\t\tAssert.notNull(filter, \"filter must not null\");\n\t\tAssert.notNull(excludeTypes, \"excludeTypes must not null\");\n\t\tthis.includeSuperClass = includeSuperClass;\n\t\tthis.includeInterfaces = includeInterfaces;\n\t\tthis.filter = filter;\n\t\tthis.excludeTypes = excludeTypes;\n\t\tthis.converters = new ArrayList<>();\n\t\tthis.typedThis = (T) this;\n\t}\n\n\t/**\n\t * 是否允许扫描父类\n\t *\n\t * @return 是否允许扫描父类\n\t */\n\tpublic boolean isIncludeSuperClass() {\n\t\treturn includeSuperClass;\n\t}\n\n\t/**\n\t * 是否允许扫描父接口\n\t *\n\t * @return 是否允许扫描父接口\n\t */\n\tpublic boolean isIncludeInterfaces() {\n\t\treturn includeInterfaces;\n\t}\n\n\t/**\n\t * 设置过滤器，若类型无法通过该过滤器，则该类型及其树结构将直接不被查找\n\t *\n\t * @param filter 过滤器\n\t * @return 当前实例\n\t */\n\tpublic T setFilter(Predicate<Class<?>> filter) {\n\t\tAssert.notNull(filter, \"filter must not null\");\n\t\tthis.filter = filter;\n\t\treturn typedThis;\n\t}\n\n\t/**\n\t * 添加不扫描的类型，该类型及其树结构将直接不被查找\n\t *\n\t * @param excludeTypes 不扫描的类型\n\t * @return 当前实例\n\t */\n\tpublic T addExcludeTypes(Class<?>... excludeTypes) {\n\t\tCollUtil.addAll(this.excludeTypes, excludeTypes);\n\t\treturn typedThis;\n\t}\n\n\t/**\n\t * 添加转换器\n\t *\n\t * @param converter 转换器\n\t * @return 当前实例\n\t * @see JdkProxyClassConverter\n\t */\n\tpublic T addConverters(UnaryOperator<Class<?>> converter) {\n\t\tAssert.notNull(converter, \"converter must not null\");\n\t\tthis.converters.add(converter);\n\t\tif (!this.hasConverters) {\n\t\t\tthis.hasConverters = CollUtil.isNotEmpty(this.converters);\n\t\t}\n\t\treturn typedThis;\n\t}\n\n\t/**\n\t * 是否允许扫描父类\n\t *\n\t * @param includeSuperClass 是否\n\t * @return 当前实例\n\t */\n\tprotected T setIncludeSuperClass(boolean includeSuperClass) {\n\t\tthis.includeSuperClass = includeSuperClass;\n\t\treturn typedThis;\n\t}\n\n\t/**\n\t * 是否允许扫描父接口\n\t *\n\t * @param includeInterfaces 是否\n\t * @return 当前实例\n\t */\n\tprotected T setIncludeInterfaces(boolean includeInterfaces) {\n\t\tthis.includeInterfaces = includeInterfaces;\n\t\treturn typedThis;\n\t}\n\n\t/**\n\t * 则根据广度优先递归扫描类的层级结构，并对层级结构中类/接口声明的层级索引和它们声明的注解对象进行处理\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle 注解元素\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true);\n\t\tfinal Class<?> sourceClass = getClassFormAnnotatedElement(annotatedEle);\n\t\tfinal Deque<List<Class<?>>> classDeque = CollUtil.newLinkedList(CollUtil.newArrayList(sourceClass));\n\t\tfinal Set<Class<?>> accessedTypes = new LinkedHashSet<>();\n\t\tint index = 0;\n\t\twhile (!classDeque.isEmpty()) {\n\t\t\tfinal List<Class<?>> currClassQueue = classDeque.removeFirst();\n\t\t\tfinal List<Class<?>> nextClassQueue = new ArrayList<>();\n\t\t\tfor (Class<?> targetClass : currClassQueue) {\n\t\t\t\ttargetClass = convert(targetClass);\n\t\t\t\t// 过滤不需要处理的类\n\t\t\t\tif (isNotNeedProcess(accessedTypes, targetClass)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\taccessedTypes.add(targetClass);\n\t\t\t\t// 扫描父类\n\t\t\t\tscanSuperClassIfNecessary(nextClassQueue, targetClass);\n\t\t\t\t// 扫描接口\n\t\t\t\tscanInterfaceIfNecessary(nextClassQueue, targetClass);\n\t\t\t\t// 处理层级索引和注解\n\t\t\t\tfinal Annotation[] targetAnnotations = getAnnotationsFromTargetClass(annotatedEle, index, targetClass);\n\t\t\t\tfor (final Annotation annotation : targetAnnotations) {\n\t\t\t\t\tif (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) {\n\t\t\t\t\t\tconsumer.accept(index, annotation);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tindex++;\n\t\t\t}\n\t\t\tif (CollUtil.isNotEmpty(nextClassQueue)) {\n\t\t\t\tclassDeque.addLast(nextClassQueue);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从要搜索的注解元素上获得要递归的类型\n\t *\n\t * @param annotatedElement 注解元素\n\t * @return 要递归的类型\n\t */\n\tprotected abstract Class<?> getClassFormAnnotatedElement(AnnotatedElement annotatedElement);\n\n\t/**\n\t * 从类上获取最终所需的目标注解\n\t *\n\t * @param source      最初的注解元素\n\t * @param index       类的层级索引\n\t * @param targetClass 类\n\t * @return 最终所需的目标注解\n\t */\n\tprotected abstract Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class<?> targetClass);\n\n\t/**\n\t * 当前类是否不需要处理\n\t *\n\t * @param accessedTypes 访问类型\n\t * @param targetClass   目标类型\n\t * @return 是否不需要处理\n\t */\n\tprotected boolean isNotNeedProcess(Set<Class<?>> accessedTypes, Class<?> targetClass) {\n\t\treturn ObjectUtil.isNull(targetClass)\n\t\t\t\t|| accessedTypes.contains(targetClass)\n\t\t\t\t|| excludeTypes.contains(targetClass)\n\t\t\t\t|| filter.negate().test(targetClass);\n\t}\n\n\t/**\n\t * 若{@link #includeInterfaces}为{@code true}，则将目标类的父接口也添加到nextClasses\n\t *\n\t * @param nextClasses 下一个类集合\n\t * @param targetClass 目标类型\n\t */\n\tprotected void scanInterfaceIfNecessary(List<Class<?>> nextClasses, Class<?> targetClass) {\n\t\tif (includeInterfaces) {\n\t\t\tfinal Class<?>[] interfaces = targetClass.getInterfaces();\n\t\t\tif (ArrayUtil.isNotEmpty(interfaces)) {\n\t\t\t\tCollUtil.addAll(nextClasses, interfaces);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 若{@link #includeSuperClass}为{@code true}，则将目标类的父类也添加到nextClasses\n\t *\n\t * @param nextClassQueue 下一个类队列\n\t * @param targetClass    目标类型\n\t */\n\tprotected void scanSuperClassIfNecessary(List<Class<?>> nextClassQueue, Class<?> targetClass) {\n\t\tif (includeSuperClass) {\n\t\t\tfinal Class<?> superClass = targetClass.getSuperclass();\n\t\t\tif (!ObjectUtil.equals(superClass, Object.class) && ObjectUtil.isNotNull(superClass)) {\n\t\t\t\tnextClassQueue.add(superClass);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 若存在转换器，则使用转换器对目标类进行转换\n\t *\n\t * @param target 目标类\n\t * @return 转换后的类\n\t */\n\tprotected Class<?> convert(Class<?> target) {\n\t\tif (hasConverters) {\n\t\t\tfor (final UnaryOperator<Class<?>> converter : converters) {\n\t\t\t\ttarget = converter.apply(target);\n\t\t\t}\n\t\t}\n\t\treturn target;\n\t}\n\n\t/**\n\t * 若类型为jdk代理类，则尝试转换为原始被代理类\n\t */\n\tpublic static class JdkProxyClassConverter implements UnaryOperator<Class<?>> {\n\t\t@Override\n\t\tpublic Class<?> apply(Class<?> sourceClass) {\n\t\t\treturn Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/AnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.annotation.AnnotationUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.annotation.Inherited;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * <p>注解扫描器，用于从支持的可注解元素上获取所需注解\n *\n * <p>默认提供了以下扫描方式：\n * <ul>\n *     <li>{@link #NOTHING}：什么都不做，什么注解都不扫描；</li>\n *     <li>{@link #DIRECTLY}：扫描元素本身直接声明的注解，包括父类带有{@link Inherited}、被传递到元素上的注解；</li>\n *     <li>\n *         {@link #DIRECTLY_AND_META_ANNOTATION}：扫描元素本身直接声明的注解，包括父类带有{@link Inherited}、被传递到元素上的注解，\n *         以及这些注解的元注解；\n *     </li>\n *     <li>{@link #SUPERCLASS}：扫描元素本身以及父类的层级结构中声明的注解；</li>\n *     <li>{@link #SUPERCLASS_AND_META_ANNOTATION}：扫描元素本身以及父类的层级结构中声明的注解，以及这些注解的元注解；</li>\n *     <li>{@link #INTERFACE}：扫描元素本身以及父接口的层级结构中声明的注解；</li>\n *     <li>{@link #INTERFACE_AND_META_ANNOTATION}：扫描元素本身以及父接口的层级结构中声明的注解，以及这些注解的元注解；</li>\n *     <li>{@link #TYPE_HIERARCHY}：扫描元素本身以及父类、父接口的层级结构中声明的注解；</li>\n *     <li>{@link #TYPE_HIERARCHY_AND_META_ANNOTATION}：扫描元素本身以及父接口、父接口的层级结构中声明的注解，以及这些注解的元注解；</li>\n * </ul>\n *\n * @author huangchengxing\n * @see TypeAnnotationScanner\n * @see MethodAnnotationScanner\n * @see FieldAnnotationScanner\n * @see MetaAnnotationScanner\n * @see ElementAnnotationScanner\n * @see GenericAnnotationScanner\n */\npublic interface AnnotationScanner {\n\n\t// ============================ 预置的扫描器实例 ============================\n\n\t/**\n\t * 不扫描任何注解\n\t */\n\tAnnotationScanner NOTHING = new EmptyAnnotationScanner();\n\n\t/**\n\t * 扫描元素本身直接声明的注解，包括父类带有{@link Inherited}、被传递到元素上的注解的扫描器\n\t */\n\tAnnotationScanner DIRECTLY = new GenericAnnotationScanner(false, false, false);\n\n\t/**\n\t * 扫描元素本身直接声明的注解，包括父类带有{@link Inherited}、被传递到元素上的注解，以及这些注解的元注解的扫描器\n\t */\n\tAnnotationScanner DIRECTLY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, false);\n\n\t/**\n\t * 扫描元素本身以及父类的层级结构中声明的注解的扫描器\n\t */\n\tAnnotationScanner SUPERCLASS = new GenericAnnotationScanner(false, true, false);\n\n\t/**\n\t * 扫描元素本身以及父类的层级结构中声明的注解，以及这些注解的元注解的扫描器\n\t */\n\tAnnotationScanner SUPERCLASS_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, false);\n\n\t/**\n\t * 扫描元素本身以及父接口的层级结构中声明的注解的扫描器\n\t */\n\tAnnotationScanner INTERFACE = new GenericAnnotationScanner(false, false, true);\n\n\t/**\n\t * 扫描元素本身以及父接口的层级结构中声明的注解，以及这些注解的元注解的扫描器\n\t */\n\tAnnotationScanner INTERFACE_AND_META_ANNOTATION = new GenericAnnotationScanner(true, false, true);\n\n\t/**\n\t * 扫描元素本身以及父类、父接口的层级结构中声明的注解的扫描器\n\t */\n\tAnnotationScanner TYPE_HIERARCHY = new GenericAnnotationScanner(false, true, true);\n\n\t/**\n\t * 扫描元素本身以及父接口、父接口的层级结构中声明的注解，以及这些注解的元注解的扫描器\n\t */\n\tAnnotationScanner TYPE_HIERARCHY_AND_META_ANNOTATION = new GenericAnnotationScanner(true, true, true);\n\n\t// ============================ 静态方法 ============================\n\n\t/**\n\t * 给定一组扫描器，使用第一个支持处理该类型元素的扫描器获取元素上可能存在的注解\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param scanners     注解扫描器\n\t * @return 注解\n\t */\n\tstatic List<Annotation> scanByAnySupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) {\n\t\tif (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\treturn Stream.of(scanners)\n\t\t\t.filter(scanner -> scanner.support(annotatedEle))\n\t\t\t.findFirst()\n\t\t\t.map(scanner -> scanner.getAnnotations(annotatedEle))\n\t\t\t.orElseGet(Collections::emptyList);\n\t}\n\n\t/**\n\t * 根据指定的扫描器，扫描元素上可能存在的注解\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param scanners     注解扫描器\n\t * @return 注解\n\t */\n\tstatic List<Annotation> scanByAllSupported(AnnotatedElement annotatedEle, AnnotationScanner... scanners) {\n\t\tif (ObjectUtil.isNull(annotatedEle) && ArrayUtil.isNotEmpty(scanners)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\treturn Stream.of(scanners)\n\t\t\t.map(scanner -> scanner.getAnnotationsIfSupport(annotatedEle))\n\t\t\t.flatMap(Collection::stream)\n\t\t\t.collect(Collectors.toList());\n\t}\n\n\t// ============================ 抽象方法 ============================\n\n\t/**\n\t * 判断是否支持扫描该注解元素\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\tdefault boolean support(AnnotatedElement annotatedEle) {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取注解元素上的全部注解。调用该方法前，需要确保调用{@link #support(AnnotatedElement)}返回为true\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 注解\n\t */\n\tdefault List<Annotation> getAnnotations(AnnotatedElement annotatedEle) {\n\t\tfinal List<Annotation> annotations = new ArrayList<>();\n\t\tscan((index, annotation) -> annotations.add(annotation), annotatedEle, null);\n\t\treturn annotations;\n\t}\n\n\t/**\n\t * 若{@link #support(AnnotatedElement)}返回{@code true}，\n\t * 则调用并返回{@link #getAnnotations(AnnotatedElement)}结果，\n\t * 否则返回{@link Collections#emptyList()}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 注解\n\t */\n\tdefault List<Annotation> getAnnotationsIfSupport(AnnotatedElement annotatedEle) {\n\t\treturn support(annotatedEle) ? getAnnotations(annotatedEle) : Collections.emptyList();\n\t}\n\n\t/**\n\t * 扫描注解元素的层级结构（若存在），然后对获取到的注解和注解对应的层级索引进行处理。\n\t * 调用该方法前，需要确保调用{@link #support(AnnotatedElement)}返回为true\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\tdefault void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter, (a)->annotation -> true);\n\t\tfor (final Annotation annotation : annotatedEle.getAnnotations()) {\n\t\t\tif (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) {\n\t\t\t\tconsumer.accept(0, annotation);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 若{@link #support(AnnotatedElement)}返回{@code true}，则调用{@link #scan(BiConsumer, AnnotatedElement, Predicate)}\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\tdefault void scanIfSupport(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tif (support(annotatedEle)) {\n\t\t\tscan(consumer, annotatedEle, filter);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/ElementAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\n/**\n * 扫描{@link AnnotatedElement}上的注解，不支持处理层级对象\n *\n * @author huangchengxing\n */\npublic class ElementAnnotationScanner implements AnnotationScanner {\n\n\t/**\n\t * 判断是否支持扫描该注解元素，仅当注解元素不为空时返回{@code true}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn ObjectUtil.isNotNull(annotatedEle);\n\t}\n\n\t/**\n\t * 扫描{@link AnnotatedElement}上直接声明的注解，调用前需要确保调用{@link #support(AnnotatedElement)}返回为true\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter,a-> t -> true);\n\t\tStream.of(annotatedEle.getAnnotations())\n\t\t\t.filter(filter)\n\t\t\t.forEach(annotation -> consumer.accept(0, annotation));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/EmptyAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\n/**\n * 默认不扫描任何元素的扫描器\n *\n * @author huangchengxing\n */\npublic class EmptyAnnotationScanner implements AnnotationScanner {\n\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic List<Annotation> getAnnotations(AnnotatedElement annotatedEle) {\n\t\treturn Collections.emptyList();\n\t}\n\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\t// do nothing\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/FieldAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.annotation.AnnotationUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Field;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\n/**\n * 扫描{@link Field}上的注解\n *\n * @author huangchengxing\n */\npublic class FieldAnnotationScanner implements AnnotationScanner {\n\n\t/**\n\t * 判断是否支持扫描该注解元素，仅当注解元素是{@link Field}时返回{@code true}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn annotatedEle instanceof Field;\n\t}\n\n\t/**\n\t * 扫描{@link Field}上直接声明的注解，调用前需要确保调用{@link #support(AnnotatedElement)}返回为true\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter, a -> annotation -> true);\n\t\tfor (final Annotation annotation : annotatedEle.getAnnotations()) {\n\t\t\tif (AnnotationUtil.isNotJdkMateAnnotation(annotation.annotationType()) && filter.test(annotation)) {\n\t\t\t\tconsumer.accept(0, annotation);\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/GenericAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.map.multi.ListValueMap;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Method;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\n/**\n * <p>通用注解扫描器，支持按不同的层级结构扫描{@link AnnotatedElement}上的注解。\n *\n * <p>当{@link AnnotatedElement}类型不同时，“层级结构”指向的对象将有所区别：\n * <ul>\n *     <li>\n *         当元素为{@link Method}时，此处层级结构指声明方法的类的层级结构，\n *         扫描器将从层级结构中寻找与该方法签名相同的方法，并对其进行扫描；\n *     </li>\n *     <li>\n *         当元素为{@link Class}时，此处层级结构即指类本身与其父类、父接口共同构成的层级结构，\n *         扫描器将扫描层级结构中类、接口声明的注解；\n *     </li>\n *     <li>当元素不为{@link Method}或{@link Class}时，则其层级结构仅有其本身一层；</li>\n * </ul>\n * 此外，扫描器支持在获取到层级结构中的注解对象后，再对注解对象的元注解进行扫描。\n *\n * @author huangchengxing\n * @see TypeAnnotationScanner\n * @see MethodAnnotationScanner\n * @see MetaAnnotationScanner\n * @see ElementAnnotationScanner\n */\npublic class GenericAnnotationScanner implements AnnotationScanner {\n\n\t/**\n\t * 类型扫描器\n\t */\n\tprivate final AnnotationScanner typeScanner;\n\n\t/**\n\t * 方法扫描器\n\t */\n\tprivate final AnnotationScanner methodScanner;\n\n\t/**\n\t * 元注解扫描器\n\t */\n\tprivate final AnnotationScanner metaScanner;\n\n\t/**\n\t * 普通元素扫描器\n\t */\n\tprivate final AnnotationScanner elementScanner;\n\n\t/**\n\t * 通用注解扫描器支持扫描所有类型的{@link AnnotatedElement}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn true;\n\t}\n\n\t/**\n\t * 构造一个通用注解扫描器\n\t *\n\t * @param enableScanMetaAnnotation  是否扫描注解上的元注解\n\t * @param enableScanSupperClass     是否扫描父类\n\t * @param enableScanSupperInterface 是否扫描父接口\n\t */\n\tpublic GenericAnnotationScanner(\n\t\t\tboolean enableScanMetaAnnotation,\n\t\t\tboolean enableScanSupperClass,\n\t\t\tboolean enableScanSupperInterface) {\n\n\t\tthis.metaScanner = enableScanMetaAnnotation ? new MetaAnnotationScanner() : new EmptyAnnotationScanner();\n\t\tthis.typeScanner = new TypeAnnotationScanner(\n\t\t\t\tenableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet()\n\t\t);\n\t\tthis.methodScanner = new MethodAnnotationScanner(\n\t\t\t\tenableScanSupperClass, enableScanSupperInterface, a -> true, Collections.emptySet()\n\t\t);\n\t\tthis.elementScanner = new ElementAnnotationScanner();\n\t}\n\n\t/**\n\t * 扫描注解元素的层级结构（若存在），然后对获取到的注解和注解对应的层级索引进行处理\n\t *\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter, a -> t -> true);\n\t\tif (ObjectUtil.isNull(annotatedEle)) {\n\t\t\treturn;\n\t\t}\n\t\t// 注解元素是类\n\t\tif (annotatedEle instanceof Class) {\n\t\t\tscanElements(typeScanner, consumer, annotatedEle, filter);\n\t\t}\n\t\t// 注解元素是方法\n\t\telse if (annotatedEle instanceof Method) {\n\t\t\tscanElements(methodScanner, consumer, annotatedEle, filter);\n\t\t}\n\t\t// 注解元素是其他类型\n\t\telse {\n\t\t\tscanElements(elementScanner, consumer, annotatedEle, filter);\n\t\t}\n\t}\n\n\t/**\n\t * 扫描注解类的层级结构（若存在），然后对获取到的注解和注解对应的层级索引进行处理\n\t *\n\t * @param scanner      使用的扫描器\n\t * @param consumer     对获取到的注解和注解对应的层级索引的处理\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       注解过滤器，无法通过过滤器的注解不会被处理。该参数允许为空。\n\t */\n\tprivate void scanElements(\n\t\t\tAnnotationScanner scanner,\n\t\t\tBiConsumer<Integer, Annotation> consumer,\n\t\t\tAnnotatedElement annotatedEle,\n\t\t\tPredicate<Annotation> filter) {\n\t\t// 扫描类上注解\n\t\tfinal ListValueMap<Integer, Annotation> classAnnotations = new ListValueMap<>(new LinkedHashMap<>());\n\t\tscanner.scan((index, annotation) -> {\n\t\t\tif (filter.test(annotation)) {\n\t\t\t\tclassAnnotations.putValue(index, annotation);\n\t\t\t}\n\t\t}, annotatedEle, filter);\n\n\t\t// 扫描元注解\n\t\tclassAnnotations.forEach((index, annotations) ->\n\t\t\t\tannotations.forEach(annotation -> {\n\t\t\t\t\tconsumer.accept(index, annotation);\n\t\t\t\t\tmetaScanner.scan(consumer, annotation.annotationType(), filter);\n\t\t\t\t})\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MetaAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.annotation.AnnotationUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * 扫描注解类上存在的注解，支持处理枚举实例或枚举类型\n * 需要注意，当待解析是枚举类时，有可能与{@link TypeAnnotationScanner}冲突\n *\n * @author huangchengxing\n * @see TypeAnnotationScanner\n */\npublic class MetaAnnotationScanner implements AnnotationScanner {\n\n\t/**\n\t * 获取当前注解的元注解后，是否继续递归扫描的元注解的元注解\n\t */\n\tprivate final boolean includeSupperMetaAnnotation;\n\n\t/**\n\t * 构造一个元注解扫描器\n\t *\n\t * @param includeSupperMetaAnnotation 获取当前注解的元注解后，是否继续递归扫描的元注解的元注解\n\t */\n\tpublic MetaAnnotationScanner(boolean includeSupperMetaAnnotation) {\n\t\tthis.includeSupperMetaAnnotation = includeSupperMetaAnnotation;\n\t}\n\n\t/**\n\t * 构造一个元注解扫描器，默认在扫描当前注解上的元注解后，并继续递归扫描元注解\n\t */\n\tpublic MetaAnnotationScanner() {\n\t\tthis(true);\n\t}\n\n\t/**\n\t * 判断是否支持扫描该注解元素，仅当注解元素是{@link Annotation}接口的子类{@link Class}时返回{@code true}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn (annotatedEle instanceof Class && ClassUtil.isAssignable(Annotation.class, (Class<?>) annotatedEle));\n\t}\n\n\t/**\n\t * 获取注解元素上的全部注解。调用该方法前，需要确保调用{@link #support(AnnotatedElement)}返回为true\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 注解\n\t */\n\t@Override\n\tpublic List<Annotation> getAnnotations(AnnotatedElement annotatedEle) {\n\t\tfinal List<Annotation> annotations = new ArrayList<>();\n\t\tscan(\n\t\t\t\t(index, annotation) -> annotations.add(annotation), annotatedEle,\n\t\t\t\tannotation -> ObjectUtil.notEqual(annotation, annotatedEle)\n\t\t);\n\t\treturn annotations;\n\t}\n\n\t/**\n\t * 按广度优先扫描指定注解上的元注解，对扫描到的注解与层级索引进行操作\n\t *\n\t * @param consumer     当前层级索引与操作\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @param filter       过滤器\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic void scan(BiConsumer<Integer, Annotation> consumer, AnnotatedElement annotatedEle, Predicate<Annotation> filter) {\n\t\tfilter = ObjectUtil.defaultIfNull(filter, a -> t -> true);\n\t\tSet<Class<? extends Annotation>> accessed = new HashSet<>();\n\t\tfinal Deque<List<Class<? extends Annotation>>> deque = CollUtil.newLinkedList(CollUtil.newArrayList((Class<? extends Annotation>) annotatedEle));\n\t\tint distance = 0;\n\t\tdo {\n\t\t\tfinal List<Class<? extends Annotation>> annotationTypes = deque.removeFirst();\n\t\t\tfor (final Class<? extends Annotation> type : annotationTypes) {\n\t\t\t\tfinal List<Annotation> metaAnnotations = Stream.of(type.getAnnotations())\n\t\t\t\t\t\t.filter(a -> !AnnotationUtil.isJdkMetaAnnotation(a.annotationType()))\n\t\t\t\t\t\t.filter(filter)\n\t\t\t\t\t\t.collect(Collectors.toList());\n\t\t\t\tfor (final Annotation metaAnnotation : metaAnnotations) {\n\t\t\t\t\tconsumer.accept(distance, metaAnnotation);\n\t\t\t\t}\n\t\t\t\taccessed.add(type);\n\t\t\t\tList<Class<? extends Annotation>> next = metaAnnotations.stream()\n\t\t\t\t\t\t.map(Annotation::annotationType)\n\t\t\t\t\t\t.filter(t -> !accessed.contains(t))\n\t\t\t\t\t\t.collect(Collectors.toList());\n\t\t\t\tif (CollUtil.isNotEmpty(next)) {\n\t\t\t\t\tdeque.addLast(next);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdistance++;\n\t\t} while (includeSupperMetaAnnotation && !deque.isEmpty());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/MethodAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Method;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\n/**\n * 扫描{@link Method}上的注解\n *\n * @author huangchengxing\n */\npublic class MethodAnnotationScanner extends AbstractTypeAnnotationScanner<MethodAnnotationScanner> implements AnnotationScanner {\n\n\t/**\n\t * 构造一个类注解扫描器，仅扫描该方法上直接声明的注解\n\t */\n\tpublic MethodAnnotationScanner() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造一个类注解扫描器\n\t *\n\t * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法\n\t */\n\tpublic MethodAnnotationScanner(boolean scanSameSignatureMethod) {\n\t\tthis(scanSameSignatureMethod, targetClass -> true, CollUtil.newLinkedHashSet());\n\t}\n\n\t/**\n\t * 构造一个方法注解扫描器\n\t *\n\t * @param scanSameSignatureMethod 是否扫描类层级结构中具有相同方法签名的方法\n\t * @param filter                  过滤器\n\t * @param excludeTypes            不包含的类型\n\t */\n\tpublic MethodAnnotationScanner(boolean scanSameSignatureMethod, Predicate<Class<?>> filter, Set<Class<?>> excludeTypes) {\n\t\tsuper(scanSameSignatureMethod, scanSameSignatureMethod, filter, excludeTypes);\n\t}\n\n\t/**\n\t * 构造一个方法注解扫描器\n\t *\n\t * @param includeSuperClass 是否允许扫描父类中具有相同方法签名的方法\n\t * @param includeInterfaces 是否允许扫描父接口中具有相同方法签名的方法\n\t * @param filter            过滤器\n\t * @param excludeTypes      不包含的类型\n\t */\n\tpublic MethodAnnotationScanner(boolean includeSuperClass, boolean includeInterfaces, Predicate<Class<?>> filter, Set<Class<?>> excludeTypes) {\n\t\tsuper(includeSuperClass, includeInterfaces, filter, excludeTypes);\n\t}\n\n\t/**\n\t * 判断是否支持扫描该注解元素，仅当注解元素是{@link Method}时返回{@code true}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return boolean 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn annotatedEle instanceof Method;\n\t}\n\n\t/**\n\t * 获取声明该方法的类\n\t *\n\t * @param annotatedElement 注解元素\n\t * @return 要递归的类型\n\t * @see Method#getDeclaringClass()\n\t */\n\t@Override\n\tprotected Class<?> getClassFormAnnotatedElement(AnnotatedElement annotatedElement) {\n\t\treturn ((Method)annotatedElement).getDeclaringClass();\n\t}\n\n\t/**\n\t * 若父类/父接口中方法具有相同的方法签名，则返回该方法上的注解\n\t *\n\t * @param source      原始方法\n\t * @param index       类的层级索引\n\t * @param targetClass 类\n\t * @return 最终所需的目标注解\n\t */\n\t@Override\n\tprotected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class<?> targetClass) {\n\t\tfinal Method sourceMethod = (Method) source;\n\t\treturn Stream.of(ClassUtil.getDeclaredMethods(targetClass))\n\t\t\t.filter(superMethod -> !superMethod.isBridge())\n\t\t\t.filter(superMethod -> hasSameSignature(sourceMethod, superMethod))\n\t\t\t.map(AnnotatedElement::getAnnotations)\n\t\t\t.flatMap(Stream::of)\n\t\t\t.toArray(Annotation[]::new);\n\t}\n\n\t/**\n\t * 设置是否扫描类层级结构中具有相同方法签名的方法\n\t *\n\t * @param scanSuperMethodIfOverride 是否扫描类层级结构中具有相同方法签名的方法\n\t * @return 当前实例\n\t */\n\tpublic MethodAnnotationScanner setScanSameSignatureMethod(boolean scanSuperMethodIfOverride) {\n\t\tsetIncludeInterfaces(scanSuperMethodIfOverride);\n\t\tsetIncludeSuperClass(scanSuperMethodIfOverride);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 该方法是否具备与扫描的方法相同的方法签名\n\t */\n\tprivate boolean hasSameSignature(Method sourceMethod, Method superMethod) {\n\t\tif (false == StrUtil.equals(sourceMethod.getName(), superMethod.getName())) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal Class<?>[] sourceParameterTypes = sourceMethod.getParameterTypes();\n\t\tfinal Class<?>[] targetParameterTypes = superMethod.getParameterTypes();\n\t\tif (sourceParameterTypes.length != targetParameterTypes.length) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!ArrayUtil.containsAll(sourceParameterTypes, targetParameterTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ClassUtil.isAssignable(superMethod.getReturnType(), sourceMethod.getReturnType());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/TypeAnnotationScanner.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Proxy;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.function.UnaryOperator;\n\n/**\n * 扫描{@link Class}上的注解\n *\n * @author huangchengxing\n */\npublic class TypeAnnotationScanner extends AbstractTypeAnnotationScanner<TypeAnnotationScanner> implements AnnotationScanner {\n\n\t/**\n\t * 构造一个类注解扫描器\n\t *\n\t * @param includeSupperClass 是否允许扫描父类\n\t * @param includeInterfaces  是否允许扫描父接口\n\t * @param filter             过滤器\n\t * @param excludeTypes       不包含的类型\n\t */\n\tpublic TypeAnnotationScanner(boolean includeSupperClass, boolean includeInterfaces, Predicate<Class<?>> filter, Set<Class<?>> excludeTypes) {\n\t\tsuper(includeSupperClass, includeInterfaces, filter, excludeTypes);\n\t}\n\n\t/**\n\t * 构建一个类注解扫描器，默认允许扫描指定元素的父类以及父接口\n\t */\n\tpublic TypeAnnotationScanner() {\n\t\tthis(true, true, t -> true, CollUtil.newLinkedHashSet());\n\t}\n\n\t/**\n\t * 判断是否支持扫描该注解元素，仅当注解元素是{@link Class}接时返回{@code true}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 是否支持扫描该注解元素\n\t */\n\t@Override\n\tpublic boolean support(AnnotatedElement annotatedEle) {\n\t\treturn annotatedEle instanceof Class;\n\t}\n\n\t/**\n\t * 将注解元素转为{@link Class}\n\t *\n\t * @param annotatedEle {@link AnnotatedElement}，可以是Class、Method、Field、Constructor、ReflectPermission\n\t * @return 要递归的类型\n\t */\n\t@Override\n\tprotected Class<?> getClassFormAnnotatedElement(AnnotatedElement annotatedEle) {\n\t\treturn (Class<?>)annotatedEle;\n\t}\n\n\t/**\n\t * 获取{@link Class#getAnnotations()}\n\t *\n\t * @param source      最初的注解元素\n\t * @param index       类的层级索引\n\t * @param targetClass 类\n\t * @return 类上直接声明的注解\n\t */\n\t@Override\n\tprotected Annotation[] getAnnotationsFromTargetClass(AnnotatedElement source, int index, Class<?> targetClass) {\n\t\treturn targetClass.getAnnotations();\n\t}\n\n\t/**\n\t * 是否允许扫描父类\n\t *\n\t * @param includeSuperClass 是否允许扫描父类\n\t * @return 当前实例\n\t */\n\t@Override\n\tpublic TypeAnnotationScanner setIncludeSuperClass(boolean includeSuperClass) {\n\t\treturn super.setIncludeSuperClass(includeSuperClass);\n\t}\n\n\t/**\n\t * 是否允许扫描父接口\n\t *\n\t * @param includeInterfaces 是否允许扫描父类\n\t * @return 当前实例\n\t */\n\t@Override\n\tpublic TypeAnnotationScanner setIncludeInterfaces(boolean includeInterfaces) {\n\t\treturn super.setIncludeInterfaces(includeInterfaces);\n\t}\n\n\t/**\n\t * 若类型为jdk代理类，则尝试转换为原始被代理类\n\t */\n\tpublic static class JdkProxyClassConverter implements UnaryOperator<Class<?>> {\n\t\t@Override\n\t\tpublic Class<?> apply(Class<?> sourceClass) {\n\t\t\treturn Proxy.isProxyClass(sourceClass) ? apply(sourceClass.getSuperclass()) : sourceClass;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/annotation/scanner/package-info.java",
    "content": "/**\n * 注解包扫描封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.annotation.scanner;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.util.BooleanUtil;\nimport cn.hutool.core.util.ModifierUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Bean信息描述做为BeanInfo替代方案，此对象持有JavaBean中的setters和getters等相关信息描述<br>\n * 查找Getter和Setter方法时会：\n *\n * <pre>\n * 1. 忽略字段和方法名的大小写\n * 2. Getter查找getXXX、isXXX、getIsXXX\n * 3. Setter查找setXXX、setIsXXX\n * 4. Setter忽略参数值与字段值不匹配的情况，因此有多个参数类型的重载时，会调用首次匹配的\n * </pre>\n *\n * @author looly\n * @since 3.1.2\n */\npublic class BeanDesc implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * Bean类\n\t */\n\tprivate final Class<?> beanClass;\n\t/**\n\t * 属性Map\n\t */\n\tprivate final Map<String, PropDesc> propMap = new LinkedHashMap<>();\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanClass Bean类\n\t */\n\tpublic BeanDesc(Class<?> beanClass) {\n\t\tAssert.notNull(beanClass);\n\t\tthis.beanClass = beanClass;\n\t\tif(RecordUtil.isRecord(beanClass)){\n\t\t\tinitForRecord();\n\t\t}else{\n\t\t\tinit();\n\t\t}\n\t}\n\n\t/**\n\t * 获取Bean的全类名\n\t *\n\t * @return Bean的类名\n\t */\n\tpublic String getName() {\n\t\treturn this.beanClass.getName();\n\t}\n\n\t/**\n\t * 获取Bean的简单类名\n\t *\n\t * @return Bean的类名\n\t */\n\tpublic String getSimpleName() {\n\t\treturn this.beanClass.getSimpleName();\n\t}\n\n\t/**\n\t * 获取字段名-字段属性Map\n\t *\n\t * @param ignoreCase 是否忽略大小写，true为忽略，false不忽略\n\t * @return 字段名-字段属性Map\n\t */\n\tpublic Map<String, PropDesc> getPropMap(boolean ignoreCase) {\n\t\treturn ignoreCase ? new CaseInsensitiveMap<>(1, this.propMap) : this.propMap;\n\t}\n\n\t/**\n\t * 获取字段属性列表\n\t *\n\t * @return {@link PropDesc} 列表\n\t */\n\tpublic Collection<PropDesc> getProps() {\n\t\treturn this.propMap.values();\n\t}\n\n\t/**\n\t * 获取属性，如果不存在返回null\n\t *\n\t * @param fieldName 字段名\n\t * @return {@link PropDesc}\n\t */\n\tpublic PropDesc getProp(String fieldName) {\n\t\treturn this.propMap.get(fieldName);\n\t}\n\n\t/**\n\t * 获得字段名对应的字段对象，如果不存在返回null\n\t *\n\t * @param fieldName 字段名\n\t * @return 字段值\n\t */\n\tpublic Field getField(String fieldName) {\n\t\tfinal PropDesc desc = this.propMap.get(fieldName);\n\t\treturn null == desc ? null : desc.getField();\n\t}\n\n\t/**\n\t * 获取Getter方法，如果不存在返回null\n\t *\n\t * @param fieldName 字段名\n\t * @return Getter方法\n\t */\n\tpublic Method getGetter(String fieldName) {\n\t\tfinal PropDesc desc = this.propMap.get(fieldName);\n\t\treturn null == desc ? null : desc.getGetter();\n\t}\n\n\t/**\n\t * 获取Setter方法，如果不存在返回null\n\t *\n\t * @param fieldName 字段名\n\t * @return Setter方法\n\t */\n\tpublic Method getSetter(String fieldName) {\n\t\tfinal PropDesc desc = this.propMap.get(fieldName);\n\t\treturn null == desc ? null : desc.getSetter();\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 初始化<br>\n\t * 只有与属性关联的相关Getter和Setter方法才会被读取，无关的getXXX和setXXX都被忽略\n\t *\n\t * @return this\n\t */\n\tprivate BeanDesc init() {\n\t\tfinal Method[] gettersAndSetters = ReflectUtil.getMethods(this.beanClass, ReflectUtil::isGetterOrSetterIgnoreCase);\n\t\tPropDesc prop;\n\t\tfor (Field field : ReflectUtil.getFields(this.beanClass)) {\n\t\t\t// 排除静态属性和对象子类\n\t\t\tif (false == ModifierUtil.isStatic(field) && false == ReflectUtil.isOuterClassField(field)) {\n\t\t\t\tprop = createProp(field, gettersAndSetters);\n\t\t\t\t// 只有不存在时才放入，防止父类属性覆盖子类属性\n\t\t\t\tthis.propMap.putIfAbsent(prop.getFieldName(), prop);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 针对Record类的反射初始化\n\t */\n\tprivate void initForRecord() {\n\t\tfinal Class<?> beanClass = this.beanClass;\n\t\tfinal Map<String, PropDesc> propMap = this.propMap;\n\n\t\tfinal List<Method> getters = ReflectUtil.getPublicMethods(beanClass, method -> 0 == method.getParameterCount());\n\t\t// 排除静态属性和对象子类\n\t\tfinal Field[] fields = ReflectUtil.getFields(beanClass, field -> !ModifierUtil.isStatic(field) && !ReflectUtil.isOuterClassField(field));\n\t\tfor (final Field field : fields) {\n\t\t\tfor (final Method getter : getters) {\n\t\t\t\tif (field.getName().equals(getter.getName())) {\n\t\t\t\t\t//record对象，getter方法与字段同名\n\t\t\t\t\tfinal PropDesc prop = new PropDesc(field, getter, null);\n\t\t\t\t\tpropMap.putIfAbsent(prop.getFieldName(), prop);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 根据字段创建属性描述<br>\n\t * 查找Getter和Setter方法时会：\n\t *\n\t * <pre>\n\t * 1. 忽略字段和方法名的大小写\n\t * 2. Getter查找getXXX、isXXX、getIsXXX\n\t * 3. Setter查找setXXX、setIsXXX\n\t * 4. Setter忽略参数值与字段值不匹配的情况，因此有多个参数类型的重载时，会调用首次匹配的\n\t * </pre>\n\t *\n\t * @param field   字段\n\t * @param methods 类中所有的方法\n\t * @return {@link PropDesc}\n\t * @since 4.0.2\n\t */\n\tprivate PropDesc createProp(Field field, Method[] methods) {\n\t\tfinal PropDesc prop = findProp(field, methods, false);\n\t\t// 忽略大小写重新匹配一次\n\t\tif (null == prop.getter || null == prop.setter) {\n\t\t\tfinal PropDesc propIgnoreCase = findProp(field, methods, true);\n\t\t\tif (null == prop.getter) {\n\t\t\t\tprop.getter = propIgnoreCase.getter;\n\t\t\t}\n\t\t\tif (null == prop.setter) {\n\t\t\t\tprop.setter = propIgnoreCase.setter;\n\t\t\t}\n\t\t}\n\t\t// 所有属性完成填充后的初始化逻辑\n\t\tprop.initialize();\n\t\treturn prop;\n\t}\n\n\t/**\n\t * 查找字段对应的Getter和Setter方法\n\t *\n\t * @param field            字段\n\t * @param gettersOrSetters 类中所有的Getter或Setter方法\n\t * @param ignoreCase       是否忽略大小写匹配\n\t * @return PropDesc\n\t */\n\tprivate PropDesc findProp(Field field, Method[] gettersOrSetters, boolean ignoreCase) {\n\t\tfinal String fieldName = field.getName();\n\t\tfinal Class<?> fieldType = field.getType();\n\t\tfinal boolean isBooleanField = BooleanUtil.isBoolean(fieldType);\n\n\t\tMethod getter = null;\n\t\tMethod setter = null;\n\t\tString methodName;\n\t\tfor (Method method : gettersOrSetters) {\n\t\t\tmethodName = method.getName();\n\t\t\tif (method.getParameterCount() == 0) {\n\t\t\t\t// 无参数，可能为Getter方法\n\t\t\t\tif (isMatchGetter(methodName, fieldName, isBooleanField, ignoreCase)) {\n\t\t\t\t\t// 方法名与字段名匹配，则为Getter方法\n\t\t\t\t\tgetter = method;\n\t\t\t\t}\n\t\t\t} else if (isMatchSetter(methodName, fieldName, isBooleanField, ignoreCase)) {\n\t\t\t\t// setter方法的参数类型和字段类型必须一致，或参数类型是字段类型的子类\n\t\t\t\tif(fieldType.isAssignableFrom(method.getParameterTypes()[0])){\n\t\t\t\t\tsetter = method;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (null != getter && null != setter) {\n\t\t\t\t// 如果Getter和Setter方法都找到了，不再继续寻找\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn new PropDesc(field, getter, setter);\n\t}\n\n\t/**\n\t * 方法是否为Getter方法<br>\n\t * 匹配规则如下（忽略大小写）：\n\t *\n\t * <pre>\n\t * 字段名    -》 方法名\n\t * isName  -》 isName\n\t * isName  -》 isIsName\n\t * isName  -》 getIsName\n\t * name     -》 isName\n\t * name     -》 getName\n\t * </pre>\n\t *\n\t * @param methodName     方法名\n\t * @param fieldName      字段名\n\t * @param isBooleanField 是否为Boolean类型字段\n\t * @param ignoreCase     匹配是否忽略大小写\n\t * @return 是否匹配\n\t */\n\tprivate boolean isMatchGetter(String methodName, String fieldName, boolean isBooleanField, boolean ignoreCase) {\n\t\tfinal String handledFieldName;\n\t\tif (ignoreCase) {\n\t\t\t// 全部转为小写，忽略大小写比较\n\t\t\tmethodName = methodName.toLowerCase();\n\t\t\thandledFieldName = fieldName.toLowerCase();\n\t\t\tfieldName = handledFieldName;\n\t\t} else {\n\t\t\thandledFieldName = StrUtil.upperFirst(fieldName);\n\t\t}\n\n\t\t// 针对Boolean类型特殊检查\n\t\tif (isBooleanField) {\n\t\t\tif (fieldName.startsWith(\"is\")) {\n\t\t\t\t// 字段已经是is开头\n\t\t\t\tif (methodName.equals(fieldName) // isName -》 isName\n\t\t\t\t\t\t|| (\"get\" + handledFieldName).equals(methodName)// isName -》 getIsName\n\t\t\t\t\t\t|| (\"is\" + handledFieldName).equals(methodName)// isName -》 isIsName\n\t\t\t\t) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else if ((\"is\" + handledFieldName).equals(methodName)) {\n\t\t\t\t// 字段非is开头， name -》 isName\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 包括boolean的任何类型只有一种匹配情况：name -》 getName\n\t\treturn (\"get\" + handledFieldName).equals(methodName);\n\t}\n\n\t/**\n\t * 方法是否为Setter方法<br>\n\t * 匹配规则如下（忽略大小写）：\n\t *\n\t * <pre>\n\t * 字段名    -》 方法名\n\t * isName  -》 setName\n\t * isName  -》 setIsName\n\t * name     -》 setName\n\t * </pre>\n\t *\n\t * @param methodName     方法名\n\t * @param fieldName      字段名\n\t * @param isBooleanField 是否为Boolean类型字段\n\t * @param ignoreCase     匹配是否忽略大小写\n\t * @return 是否匹配\n\t */\n\tprivate boolean isMatchSetter(String methodName, String fieldName, boolean isBooleanField, boolean ignoreCase) {\n\t\tfinal String handledFieldName;\n\t\tif (ignoreCase) {\n\t\t\t// 全部转为小写，忽略大小写比较\n\t\t\tmethodName = methodName.toLowerCase();\n\t\t\thandledFieldName = fieldName.toLowerCase();\n\t\t\tfieldName = handledFieldName;\n\t\t} else {\n\t\t\thandledFieldName = StrUtil.upperFirst(fieldName);\n\t\t}\n\n\t\t// 非标准Setter方法跳过\n\t\tif (false == methodName.startsWith(\"set\")) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 针对Boolean类型特殊检查\n\t\tif (isBooleanField && fieldName.startsWith(\"is\")) {\n\t\t\t// 字段是is开头\n\t\t\tif ((\"set\" + StrUtil.removePrefix(fieldName, \"is\")).equals(methodName)// isName -》 setName\n\t\t\t\t\t|| (\"set\" + handledFieldName).equals(methodName)// isName -》 setIsName\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 包括boolean的任何类型只有一种匹配情况：name -》 setName\n\t\treturn (\"set\" + handledFieldName).equals(methodName);\n\t}\n\t// ------------------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\n/**\n * Bean属性缓存<br>\n * 缓存用于防止多次反射造成的性能问题\n *\n * @author Looly\n */\npublic enum BeanDescCache {\n\tINSTANCE;\n\n\tprivate final WeakKeyValueConcurrentMap<Class<?>, BeanDesc> bdCache = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 获得属性名和{@link BeanDesc}Map映射\n\t *\n\t * @param beanClass Bean的类\n\t * @param supplier  对象不存在时创建对象的函数\n\t * @return 属性名和{@link BeanDesc}映射\n\t * @since 5.4.2\n\t */\n\tpublic BeanDesc getBeanDesc(Class<?> beanClass, Func0<BeanDesc> supplier) {\n\t\treturn bdCache.computeIfAbsent(beanClass, (key)->supplier.callWithRuntimeException());\n\t}\n\n\t/**\n\t * 清空全局的Bean属性缓存\n\t *\n\t * @since 5.7.21\n\t */\n\tpublic void clear() {\n\t\tthis.bdCache.clear();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanException.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Bean异常\n * @author xiaoleilu\n */\npublic class BeanException extends RuntimeException{\n\tprivate static final long serialVersionUID = -8096998667745023423L;\n\n\tpublic BeanException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic BeanException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic BeanException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic BeanException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic BeanException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.map.reference.ReferenceConcurrentMap;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\nimport java.beans.PropertyDescriptor;\nimport java.util.Map;\n\n/**\n * Bean属性缓存<br>\n * 缓存用于防止多次反射造成的性能问题\n *\n * @author Looly\n */\npublic enum BeanInfoCache {\n\tINSTANCE;\n\n\tprivate final WeakKeyValueConcurrentMap<Class<?>, Map<String, PropertyDescriptor>> pdCache = new WeakKeyValueConcurrentMap<>();\n\tprivate final WeakKeyValueConcurrentMap<Class<?>, Map<String, PropertyDescriptor>> ignoreCasePdCache = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 获得属性名和{@link PropertyDescriptor}Map映射\n\t *\n\t * @param beanClass  Bean的类\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 属性名和{@link PropertyDescriptor}Map映射\n\t */\n\tpublic Map<String, PropertyDescriptor> getPropertyDescriptorMap(Class<?> beanClass, boolean ignoreCase) {\n\t\treturn getCache(ignoreCase).get(beanClass);\n\t}\n\n\t/**\n\t * 获得属性名和{@link PropertyDescriptor}Map映射\n\t *\n\t * @param beanClass  Bean的类\n\t * @param ignoreCase 是否忽略大小写\n\t * @param supplier   缓存对象产生函数\n\t * @return 属性名和{@link PropertyDescriptor}Map映射\n\t * @since 5.4.1\n\t */\n\tpublic Map<String, PropertyDescriptor> getPropertyDescriptorMap(\n\t\t\tClass<?> beanClass,\n\t\t\tboolean ignoreCase,\n\t\t\tFunc0<Map<String, PropertyDescriptor>> supplier) {\n\t\treturn getCache(ignoreCase).computeIfAbsent(beanClass, (key)->supplier.callWithRuntimeException());\n\t}\n\n\t/**\n\t * 加入缓存\n\t *\n\t * @param beanClass                      Bean的类\n\t * @param fieldNamePropertyDescriptorMap 属性名和{@link PropertyDescriptor}Map映射\n\t * @param ignoreCase                     是否忽略大小写\n\t */\n\tpublic void putPropertyDescriptorMap(Class<?> beanClass, Map<String, PropertyDescriptor> fieldNamePropertyDescriptorMap, boolean ignoreCase) {\n\t\tgetCache(ignoreCase).put(beanClass, fieldNamePropertyDescriptorMap);\n\t}\n\n\t/**\n\t * 清空缓存\n\t *\n\t * @since 5.7.21\n\t */\n\tpublic void clear() {\n\t\tthis.pdCache.clear();\n\t\tthis.ignoreCasePdCache.clear();\n\t}\n\n\t/**\n\t * 根据是否忽略字段名的大小写，返回不用Cache对象\n\t *\n\t * @param ignoreCase 是否忽略大小写\n\t * @return {@link ReferenceConcurrentMap}\n\t * @since 5.4.1\n\t */\n\tprivate ReferenceConcurrentMap<Class<?>, Map<String, PropertyDescriptor>> getCache(boolean ignoreCase) {\n\t\treturn ignoreCase ? ignoreCasePdCache : pdCache;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * Bean路径表达式，用于获取多层嵌套Bean中的字段值或Bean对象<br>\n * 根据给定的表达式，查找Bean中对应的属性值对象。 表达式分为两种：\n * <ol>\n * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n * </ol>\n * <p>\n * 表达式栗子：\n *\n * <pre>\n * persion\n * persion.name\n * persons[3]\n * person.friends[5].name\n * ['person']['friends'][5]['name']\n * </pre>\n *\n * @author Looly\n * @since 4.0.6\n */\npublic class BeanPath implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 表达式边界符号数组\n\t */\n\tprivate static final char[] EXP_CHARS = {CharUtil.DOT, CharUtil.BRACKET_START, CharUtil.BRACKET_END};\n\n\tprivate boolean isStartWith = false;\n\tprotected List<String> patternParts;\n\n\t/**\n\t * 解析Bean路径表达式为Bean模式<br>\n\t * Bean表达式，用于获取多层嵌套Bean中的字段值或Bean对象<br>\n\t * 根据给定的表达式，查找Bean中对应的属性值对象。 表达式分为两种：\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * ['person']['friends'][5]['name']\n\t * </pre>\n\t *\n\t * @param expression 表达式\n\t * @return BeanPath\n\t */\n\tpublic static BeanPath create(final String expression) {\n\t\treturn new BeanPath(expression);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param expression 表达式\n\t */\n\tpublic BeanPath(final String expression) {\n\t\tinit(expression);\n\t}\n\n\t/**\n\t * 获取表达式解析后的分段列表\n\t *\n\t * @return 表达式分段列表\n\t */\n\tpublic List<String> getPatternParts() {\n\t\treturn this.patternParts;\n\t}\n\n\t/**\n\t * 获取Bean中对应表达式的值\n\t *\n\t * @param bean Bean对象或Map或List等\n\t * @return 值，如果对应值不存在，则返回null\n\t */\n\tpublic Object get(final Object bean) {\n\t\treturn get(this.patternParts, bean, false);\n\t}\n\n\t/**\n\t * 设置表达式指定位置（或filed对应）的值<br>\n\t * 若表达式指向一个List则设置其坐标对应位置的值，若指向Map则put对应key的值，Bean则设置字段的值<br>\n\t * 注意：\n\t *\n\t * <pre>\n\t * 1. 如果为List，如果下标不大于List长度，则替换原有值，否则追加值\n\t * 2. 如果为数组，如果下标不大于数组长度，则替换原有值，否则追加值\n\t * </pre>\n\t *\n\t * @param bean  Bean、Map或List\n\t * @param value 值\n\t */\n\tpublic void set(final Object bean, final Object value) {\n\t\tset(bean, this.patternParts, lastIsNumber(this.patternParts), value);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.patternParts.toString();\n\t}\n\n\t//region Private Methods\n\n\t/**\n\t * 设置表达式指定位置（或filed对应）的值<br>\n\t * 若表达式指向一个List则设置其坐标对应位置的值，若指向Map则put对应key的值，Bean则设置字段的值<br>\n\t * 注意：\n\t *\n\t * <pre>\n\t * 1. 如果为List，如果下标不大于List长度，则替换原有值，否则追加值\n\t * 2. 如果为数组，如果下标不大于数组长度，则替换原有值，否则追加值\n\t * </pre>\n\t *\n\t * @param bean           Bean、Map或List\n\t * @param patternParts   表达式块列表\n\t * @param nextNumberPart 下一个值是否\n\t * @param value          值\n\t */\n\tprivate void set(Object bean, List<String> patternParts, boolean nextNumberPart, Object value) {\n\t\tObject subBean = this.get(patternParts, bean, true);\n\t\tif (null == subBean) {\n\t\t\t// 当前节点是空，则先创建父节点\n\t\t\tfinal List<String> parentParts = getParentParts(patternParts);\n\t\t\tthis.set(bean, parentParts, lastIsNumber(parentParts), nextNumberPart ? new ArrayList<>() : new HashMap<>());\n\t\t\t//set中有可能做过转换，因此此处重新获取bean\n\t\t\tsubBean = this.get(patternParts, bean, true);\n\t\t}\n\n\t\tfinal Object newSubBean = BeanUtil.setFieldValue(subBean, patternParts.get(patternParts.size() - 1), value);\n\t\tif(newSubBean != subBean){\n\t\t\t// 对象变更，重新加入\n\t\t\tthis.set(bean, getParentParts(patternParts), nextNumberPart, newSubBean);\n\t\t}\n\t}\n\n\t/**\n\t * 判断path列表中末尾的标记是否为数字\n\t *\n\t * @param patternParts path列表\n\t * @return 是否为数字\n\t */\n\tprivate static boolean lastIsNumber(List<String> patternParts) {\n\t\treturn NumberUtil.isInteger(patternParts.get(patternParts.size() - 1));\n\t}\n\n\t/**\n\t * 获取父级路径列表\n\t *\n\t * @param patternParts 路径列表\n\t * @return 父级路径列表\n\t */\n\tprivate static List<String> getParentParts(List<String> patternParts) {\n\t\treturn patternParts.subList(0, patternParts.size() - 1);\n\t}\n\n\t/**\n\t * 获取Bean中对应表达式的值\n\t *\n\t * @param patternParts 表达式分段列表\n\t * @param bean         Bean对象或Map或List等\n\t * @param ignoreLast   是否忽略最后一个值，忽略最后一个值则用于set，否则用于read\n\t * @return 值，如果对应值不存在，则返回null\n\t */\n\tprivate Object get(final List<String> patternParts, final Object bean, final boolean ignoreLast) {\n\t\tint length = patternParts.size();\n\t\tif (ignoreLast) {\n\t\t\tlength--;\n\t\t}\n\t\tObject subBean = bean;\n\t\tboolean isFirst = true;\n\t\tString patternPart;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tpatternPart = patternParts.get(i);\n\t\t\t\n\t\t\t// 支持通配符 * 语法，用于获取集合或数组中所有元素的指定字段\n\t\t\tif (\"*\".equals(patternPart)) {\n\t\t\t\t// 如果当前对象是集合或数组，且后面还有路径部分，则展开处理\n\t\t\t\tif (i < length - 1 && (subBean instanceof Collection || ArrayUtil.isArray(subBean))) {\n\t\t\t\t\treturn getByWildcard(subBean, patternParts, i + 1, length);\n\t\t\t\t}\n\t\t\t\t// 如果 * 是最后一个部分，直接返回当前集合/数组\n\t\t\t\treturn subBean;\n\t\t\t}\n\t\t\t\n\t\t\tsubBean = getFieldValue(subBean, patternPart);\n\t\t\tif (null == subBean) {\n\t\t\t\t// 支持表达式的第一个对象为Bean本身（若用户定义表达式$开头，则不做此操作）\n\t\t\t\tif (isFirst && false == this.isStartWith && BeanUtil.isMatchName(bean, patternPart, true)) {\n\t\t\t\t\tsubBean = bean;\n\t\t\t\t\tisFirst = false;\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn subBean;\n\t}\n\n\t/**\n\t * 处理通配符 * 的情况，对集合或数组中的每个元素应用剩余路径\n\t *\n\t * @param collectionOrArray 集合或数组对象\n\t * @param patternParts      完整的表达式分段列表\n\t * @param startIndex        剩余路径的起始索引\n\t * @param endIndex          路径的结束索引\n\t * @return 结果列表\n\t */\n\tprivate Object getByWildcard(final Object collectionOrArray, final List<String> patternParts, \n\t                              final int startIndex, final int endIndex) {\n\t\tfinal List<Object> results = new ArrayList<>();\n\t\t\n\t\t// 获取剩余的路径部分\n\t\tfinal List<String> remainingParts = patternParts.subList(startIndex, endIndex);\n\t\t\n\t\tif (collectionOrArray instanceof Collection) {\n\t\t\t// 处理集合\n\t\t\tfor (Object item : (Collection<?>) collectionOrArray) {\n\t\t\t\tif (item != null) {\n\t\t\t\t\tfinal Object value = get(remainingParts, item, false);\n\t\t\t\t\tif (value != null) {\n\t\t\t\t\t\tresults.add(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (ArrayUtil.isArray(collectionOrArray)) {\n\t\t\t// 处理数组\n\t\t\tfinal int arrayLength = ArrayUtil.length(collectionOrArray);\n\t\t\tfor (int i = 0; i < arrayLength; i++) {\n\t\t\t\tfinal Object item = ArrayUtil.get(collectionOrArray, i);\n\t\t\t\tif (item != null) {\n\t\t\t\t\tfinal Object value = get(remainingParts, item, false);\n\t\t\t\t\tif (value != null) {\n\t\t\t\t\t\tresults.add(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn results.isEmpty() ? null : results;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static Object getFieldValue(final Object bean, final String expression) {\n\t\tif (StrUtil.isBlank(expression)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (StrUtil.contains(expression, ':')) {\n\t\t\t// [start:end:step] 模式\n\t\t\tfinal List<String> parts = StrUtil.splitTrim(expression, ':');\n\t\t\tfinal int start = Integer.parseInt(parts.get(0));\n\t\t\tfinal int end = Integer.parseInt(parts.get(1));\n\t\t\tint step = 1;\n\t\t\tif (3 == parts.size()) {\n\t\t\t\tstep = Integer.parseInt(parts.get(2));\n\t\t\t}\n\t\t\tif (bean instanceof Collection) {\n\t\t\t\treturn CollUtil.sub((Collection<?>) bean, start, end, step);\n\t\t\t} else if (ArrayUtil.isArray(bean)) {\n\t\t\t\treturn ArrayUtil.sub(bean, start, end, step);\n\t\t\t}\n\t\t} else if (StrUtil.contains(expression, ',')) {\n\t\t\t// [num0,num1,num2...]模式或者['key0','key1']模式\n\t\t\tfinal List<String> keys = StrUtil.splitTrim(expression, ',');\n\t\t\tif (bean instanceof Collection) {\n\t\t\t\treturn CollUtil.getAny((Collection<?>) bean, Convert.convert(int[].class, keys));\n\t\t\t} else if (ArrayUtil.isArray(bean)) {\n\t\t\t\treturn ArrayUtil.getAny(bean, Convert.convert(int[].class, keys));\n\t\t\t} else {\n\t\t\t\tfinal String[] unWrappedKeys = new String[keys.size()];\n\t\t\t\tfor (int i = 0; i < unWrappedKeys.length; i++) {\n\t\t\t\t\tunWrappedKeys[i] = StrUtil.unWrap(keys.get(i), '\\'');\n\t\t\t\t}\n\t\t\t\tif (bean instanceof Map) {\n\t\t\t\t\t// 只支持String为key的Map\n\t\t\t\t\treturn MapUtil.getAny((Map<String, ?>) bean, unWrappedKeys);\n\t\t\t\t} else {\n\t\t\t\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(bean);\n\t\t\t\t\treturn MapUtil.getAny(map, unWrappedKeys);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 数字或普通字符串\n\t\t\treturn BeanUtil.getFieldValue(bean, expression);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param expression 表达式\n\t */\n\tprivate void init(final String expression) {\n\t\tfinal List<String> localPatternParts = new ArrayList<>();\n\t\tfinal int length = expression.length();\n\n\t\tfinal StringBuilder builder = new StringBuilder();\n\t\tchar c;\n\t\tboolean isNumStart = false;// 下标标识符开始\n\t\tboolean isInWrap = false; //标识是否在引号内\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = expression.charAt(i);\n\t\t\tif (0 == i && '$' == c) {\n\t\t\t\t// 忽略开头的$符，表示当前对象\n\t\t\t\tisStartWith = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ('\\'' == c) {\n\t\t\t\t// 结束\n\t\t\t\tisInWrap = (false == isInWrap);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (false == isInWrap && ArrayUtil.contains(EXP_CHARS, c)) {\n\t\t\t\t// 处理边界符号\n\t\t\t\tif (CharUtil.BRACKET_END == c) {\n\t\t\t\t\t// 中括号（数字下标）结束\n\t\t\t\t\tif (false == isNumStart) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Bad expression '{}':{}, we find ']' but no '[' !\", expression, i));\n\t\t\t\t\t}\n\t\t\t\t\tisNumStart = false;\n\t\t\t\t\t// 中括号结束加入下标\n\t\t\t\t} else {\n\t\t\t\t\tif (isNumStart) {\n\t\t\t\t\t\t// 非结束中括号情况下发现起始中括号报错（中括号未关闭）\n\t\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Bad expression '{}':{}, we find '[' but no ']' !\", expression, i));\n\t\t\t\t\t} else if (CharUtil.BRACKET_START == c) {\n\t\t\t\t\t\t// 数字下标开始\n\t\t\t\t\t\tisNumStart = true;\n\t\t\t\t\t}\n\t\t\t\t\t// 每一个边界符之前的表达式是一个完整的KEY，开始处理KEY\n\t\t\t\t}\n\t\t\t\tif (builder.length() > 0) {\n\t\t\t\t\tlocalPatternParts.add(builder.toString());\n\t\t\t\t}\n\t\t\t\tbuilder.setLength(0);\n\t\t\t} else {\n\t\t\t\t// 非边界符号，追加字符\n\t\t\t\tbuilder.append(c);\n\t\t\t}\n\t\t}\n\n\t\t// 末尾边界符检查\n\t\tif (isNumStart) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Bad expression '{}':{}, we find '[' but no ']' !\", expression, length - 1));\n\t\t} else {\n\t\t\tif (builder.length() > 0) {\n\t\t\t\tlocalPatternParts.add(builder.toString());\n\t\t\t}\n\t\t}\n\n\t\t// 不可变List\n\t\tthis.patternParts = ListUtil.unmodifiable(localPatternParts);\n\t}\n\t//endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.BeanCopier;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.lang.Editor;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.*;\n\nimport java.beans.*;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * Bean工具类\n *\n * <p>\n * 把一个拥有对属性进行set和get方法的类，我们就可以称之为JavaBean。\n * </p>\n *\n * @author Looly\n * @since 3.1.2\n */\npublic class BeanUtil {\n\n\t/**\n\t * 判断是否为可读的Bean对象，判定方法是：\n\t *\n\t * <pre>\n\t *     1、是否存在只有无参数的getXXX方法或者isXXX方法\n\t *     2、是否存在public类型的字段\n\t * </pre>\n\t *\n\t * @param clazz 待测试类\n\t * @return 是否为可读的Bean对象\n\t * @see #hasGetter(Class)\n\t * @see #hasPublicField(Class)\n\t */\n\tpublic static boolean isReadableBean(Class<?> clazz) {\n\t\treturn hasGetter(clazz) || hasPublicField(clazz);\n\t}\n\n\t/**\n\t * 判断是否为Bean对象，判定方法是：\n\t *\n\t * <pre>\n\t *     1、是否存在只有一个参数的setXXX方法\n\t *     2、是否存在public类型的字段\n\t * </pre>\n\t *\n\t * @param clazz 待测试类\n\t * @return 是否为Bean对象\n\t * @see #hasSetter(Class)\n\t * @see #hasPublicField(Class)\n\t */\n\tpublic static boolean isBean(Class<?> clazz) {\n\t\treturn hasSetter(clazz) || hasPublicField(clazz);\n\t}\n\n\t/**\n\t * 判断是否有Setter方法<br>\n\t * 判定方法是否存在只有一个参数的setXXX方法\n\t *\n\t * @param clazz 待测试类\n\t * @return 是否为Bean对象\n\t * @since 4.2.2\n\t */\n\tpublic static boolean hasSetter(Class<?> clazz) {\n\t\tif(null == clazz){\n\t\t\treturn false;\n\t\t}\n\t\t// issue#I9VTZG，排除定义setXXX的预定义类\n\t\tif(Dict.class == clazz){\n\t\t\treturn false;\n\t\t}\n\n\t\tif (ClassUtil.isNormalClass(clazz)) {\n\t\t\tfor (Method method : clazz.getMethods()) {\n\t\t\t\tif (method.getParameterCount() == 1 && method.getName().startsWith(\"set\")) {\n\t\t\t\t\t// 检测包含标准的setXXX方法即视为标准的JavaBean\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断是否为Bean对象<br>\n\t * 判定方法是否存在只有无参数的getXXX方法或者isXXX方法\n\t *\n\t * @param clazz 待测试类\n\t * @return 是否为Bean对象\n\t * @since 4.2.2\n\t */\n\tpublic static boolean hasGetter(Class<?> clazz) {\n\t\tif (ClassUtil.isNormalClass(clazz)) {\n\t\t\tfor (Method method : clazz.getMethods()) {\n\t\t\t\tif (method.getParameterCount() == 0) {\n\t\t\t\t\tfinal String name = method.getName();\n\t\t\t\t\tif (name.startsWith(\"get\") || name.startsWith(\"is\")) {\n\t\t\t\t\t\tif (false == \"getClass\".equals(name)) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 指定类中是否有public类型字段(static字段除外)\n\t *\n\t * @param clazz 待测试类\n\t * @return 是否有public类型字段\n\t * @since 5.1.0\n\t */\n\tpublic static boolean hasPublicField(Class<?> clazz) {\n\t\tif(null == clazz){\n\t\t\treturn false;\n\t\t}\n\t\tif (ClassUtil.isNormalClass(clazz)) {\n\t\t\tfor (Field field : clazz.getFields()) {\n\t\t\t\tif (ModifierUtil.isPublic(field) && false == ModifierUtil.isStatic(field)) {\n\t\t\t\t\t//非static的public字段\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 创建动态Bean\n\t *\n\t * @param bean 普通Bean或Map\n\t * @return {@link DynaBean}\n\t * @since 3.0.7\n\t */\n\tpublic static DynaBean createDynaBean(Object bean) {\n\t\treturn new DynaBean(bean);\n\t}\n\n\t/**\n\t * 查找类型转换器 {@link PropertyEditor}\n\t *\n\t * @param type 需要转换的目标类型\n\t * @return {@link PropertyEditor}\n\t */\n\tpublic static PropertyEditor findEditor(Class<?> type) {\n\t\treturn PropertyEditorManager.findEditor(type);\n\t}\n\n\t/**\n\t * 获取{@link BeanDesc} Bean描述信息\n\t *\n\t * @param clazz Bean类\n\t * @return {@link BeanDesc}\n\t * @since 3.1.2\n\t */\n\tpublic static BeanDesc getBeanDesc(Class<?> clazz) {\n\t\treturn BeanDescCache.INSTANCE.getBeanDesc(clazz, () -> new BeanDesc(clazz));\n\t}\n\n\t/**\n\t * 遍历Bean的属性\n\t *\n\t * @param clazz  Bean类\n\t * @param action 每个元素的处理类\n\t * @since 5.4.2\n\t */\n\tpublic static void descForEach(Class<?> clazz, Consumer<? super PropDesc> action) {\n\t\tgetBeanDesc(clazz).getProps().forEach(action);\n\t}\n\n\t// --------------------------------------------------------------------------------------------------------- PropertyDescriptor\n\n\t/**\n\t * 获得Bean字段描述数组\n\t *\n\t * @param clazz Bean类\n\t * @return 字段描述数组\n\t * @throws BeanException 获取属性异常\n\t */\n\tpublic static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeanException {\n\t\tBeanInfo beanInfo;\n\t\ttry {\n\t\t\tbeanInfo = Introspector.getBeanInfo(clazz);\n\t\t} catch (IntrospectionException e) {\n\t\t\tthrow new BeanException(e);\n\t\t}\n\t\treturn ArrayUtil.filter(beanInfo.getPropertyDescriptors(), t -> {\n\t\t\t// 过滤掉getClass方法\n\t\t\treturn false == \"class\".equals(t.getName());\n\t\t});\n\t}\n\n\t/**\n\t * 获得字段名和字段描述Map，获得的结果会缓存在 {@link BeanInfoCache}中\n\t *\n\t * @param clazz      Bean类\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 字段名和字段描述Map\n\t * @throws BeanException 获取属性异常\n\t */\n\tpublic static Map<String, PropertyDescriptor> getPropertyDescriptorMap(Class<?> clazz, boolean ignoreCase) throws BeanException {\n\t\treturn BeanInfoCache.INSTANCE.getPropertyDescriptorMap(clazz, ignoreCase, () -> internalGetPropertyDescriptorMap(clazz, ignoreCase));\n\t}\n\n\t/**\n\t * 获得字段名和字段描述Map。内部使用，直接获取Bean类的PropertyDescriptor\n\t *\n\t * @param clazz      Bean类\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 字段名和字段描述Map\n\t * @throws BeanException 获取属性异常\n\t */\n\tprivate static Map<String, PropertyDescriptor> internalGetPropertyDescriptorMap(Class<?> clazz, boolean ignoreCase) throws BeanException {\n\t\tfinal PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz);\n\t\tfinal Map<String, PropertyDescriptor> map = ignoreCase ? new CaseInsensitiveMap<>(propertyDescriptors.length, 1f)\n\t\t\t\t: new HashMap<>(propertyDescriptors.length, 1);\n\n\t\tfor (PropertyDescriptor propertyDescriptor : propertyDescriptors) {\n\t\t\tmap.put(propertyDescriptor.getName(), propertyDescriptor);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 获得Bean类属性描述，大小写敏感\n\t *\n\t * @param clazz     Bean类\n\t * @param fieldName 字段名\n\t * @return PropertyDescriptor\n\t * @throws BeanException 获取属性异常\n\t */\n\tpublic static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, final String fieldName) throws BeanException {\n\t\treturn getPropertyDescriptor(clazz, fieldName, false);\n\t}\n\n\t/**\n\t * 获得Bean类属性描述\n\t *\n\t * @param clazz      Bean类\n\t * @param fieldName  字段名\n\t * @param ignoreCase 是否忽略大小写\n\t * @return PropertyDescriptor\n\t * @throws BeanException 获取属性异常\n\t */\n\tpublic static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, final String fieldName, boolean ignoreCase) throws BeanException {\n\t\tfinal Map<String, PropertyDescriptor> map = getPropertyDescriptorMap(clazz, ignoreCase);\n\t\treturn (null == map) ? null : map.get(fieldName);\n\t}\n\n\t/**\n\t * 获得字段值，通过反射直接获得字段值，并不调用getXXX方法<br>\n\t * 对象同样支持Map类型，fieldNameOrIndex即为key\n\t *\n\t * <ul>\n\t *     <li>Map: fieldNameOrIndex需为key，获取对应value</li>\n\t *     <li>Collection: fieldNameOrIndex当为数字，返回index对应值，非数字遍历集合返回子bean对应name值</li>\n\t *     <li>Array: fieldNameOrIndex当为数字，返回index对应值，非数字遍历数组返回子bean对应name值</li>\n\t * </ul>\n\t *\n\t * @param bean             Bean对象\n\t * @param fieldNameOrIndex 字段名或序号，序号支持负数\n\t * @return 字段值\n\t */\n\tpublic static Object getFieldValue(Object bean, String fieldNameOrIndex) {\n\t\tif (null == bean || null == fieldNameOrIndex) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (bean instanceof Map) {\n\t\t\treturn ((Map<?, ?>) bean).get(fieldNameOrIndex);\n\t\t} else if (bean instanceof Collection) {\n\t\t\ttry {\n\t\t\t\treturn CollUtil.get((Collection<?>) bean, Integer.parseInt(fieldNameOrIndex));\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t// 非数字，see pr#254@Gitee\n\t\t\t\treturn CollUtil.map((Collection<?>) bean, (beanEle) -> getFieldValue(beanEle, fieldNameOrIndex), false);\n\t\t\t}\n\t\t} else if (ArrayUtil.isArray(bean)) {\n\t\t\ttry {\n\t\t\t\treturn ArrayUtil.get(bean, Integer.parseInt(fieldNameOrIndex));\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t// 非数字，see pr#254@Gitee\n\t\t\t\treturn ArrayUtil.map(bean, Object.class, (beanEle) -> getFieldValue(beanEle, fieldNameOrIndex));\n\t\t\t}\n\t\t} else {// 普通Bean对象\n\t\t\treturn ReflectUtil.getFieldValue(bean, fieldNameOrIndex);\n\t\t}\n\t}\n\n\t/**\n\t * 设置字段值，通过反射设置字段值，并不调用setXXX方法<br>\n\t * 对象同样支持Map类型，fieldNameOrIndex即为key，支持：\n\t * <ul>\n\t *     <li>Map</li>\n\t *     <li>List</li>\n\t *     <li>Bean</li>\n\t * </ul>\n\t *\n\t * @param bean             Bean\n\t * @param fieldNameOrIndex 字段名或序号，序号支持负数\n\t * @param value            值\n\t * @return bean，当为数组时，返回一个新的数组\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static Object setFieldValue(Object bean, String fieldNameOrIndex, Object value) {\n\t\tif (bean instanceof Map) {\n\t\t\t((Map) bean).put(fieldNameOrIndex, value);\n\t\t} else if (bean instanceof List) {\n\t\t\tListUtil.setOrPadding((List) bean, Convert.toInt(fieldNameOrIndex), value);\n\t\t} else if (ArrayUtil.isArray(bean)) {\n\t\t\t// issue#3008，追加产生新数组，此处返回新数组\n\t\t\treturn ArrayUtil.setOrAppend(bean, Convert.toInt(fieldNameOrIndex), value);\n\t\t} else {\n\t\t\t// 普通Bean对象\n\t\t\tReflectUtil.setFieldValue(bean, fieldNameOrIndex, value);\n\t\t}\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 解析Bean中的属性值\n\t *\n\t * @param <T>        属性值类型\n\t * @param bean       Bean对象，支持Map、List、Collection、Array\n\t * @param expression 表达式，例如：person.friend[5].name\n\t * @return Bean属性值，bean为{@code null}或者express为空，返回{@code null}\n\t * @see BeanPath#get(Object)\n\t * @since 3.0.7\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getProperty(Object bean, String expression) {\n\t\tif (null == bean || StrUtil.isBlank(expression)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (T) BeanPath.create(expression).get(bean);\n\t}\n\n\t/**\n\t * 解析Bean中的属性值\n\t *\n\t * @param bean       Bean对象，支持Map、List、Collection、Array\n\t * @param expression 表达式，例如：person.friend[5].name\n\t * @param value      属性值\n\t * @see BeanPath#get(Object)\n\t * @since 4.0.6\n\t */\n\tpublic static void setProperty(Object bean, String expression, Object value) {\n\t\tBeanPath.create(expression).set(bean, value);\n\t}\n\n\t// --------------------------------------------------------------------------------------------- mapToBean\n\n\t/**\n\t * Map转换为Bean对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           {@link Map}\n\t * @param beanClass     Bean Class\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t * @deprecated 请使用 {@link #toBean(Object, Class)} 或 {@link #toBeanIgnoreError(Object, Class)}\n\t */\n\t@Deprecated\n\tpublic static <T> T mapToBean(Map<?, ?> map, Class<T> beanClass, boolean isIgnoreError) {\n\t\treturn fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError);\n\t}\n\n\t/**\n\t * Map转换为Bean对象<br>\n\t * 忽略大小写\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           Map\n\t * @param beanClass     Bean Class\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t * @deprecated 请使用 {@link #toBeanIgnoreCase(Object, Class, boolean)}\n\t */\n\t@Deprecated\n\tpublic static <T> T mapToBeanIgnoreCase(Map<?, ?> map, Class<T> beanClass, boolean isIgnoreError) {\n\t\treturn fillBeanWithMapIgnoreCase(map, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError);\n\t}\n\n\t/**\n\t * Map转换为Bean对象\n\t *\n\t * @param <T>         Bean类型\n\t * @param map         {@link Map}\n\t * @param beanClass   Bean Class\n\t * @param copyOptions 转Bean选项\n\t * @return Bean\n\t * @deprecated 请使用 {@link #toBean(Object, Class, CopyOptions)}\n\t */\n\t@Deprecated\n\tpublic static <T> T mapToBean(Map<?, ?> map, Class<T> beanClass, CopyOptions copyOptions) {\n\t\treturn fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), copyOptions);\n\t}\n\n\t/**\n\t * Map转换为Bean对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           {@link Map}\n\t * @param beanClass     Bean Class\n\t * @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格\n\t * @param copyOptions   转Bean选项\n\t * @return Bean\n\t * @deprecated isToCamelCase参数无效，请使用 {@link #toBean(Object, Class, CopyOptions)}\n\t */\n\t@Deprecated\n\tpublic static <T> T mapToBean(Map<?, ?> map, Class<T> beanClass, boolean isToCamelCase, CopyOptions copyOptions) {\n\t\treturn fillBeanWithMap(map, ReflectUtil.newInstanceIfPossible(beanClass), isToCamelCase, copyOptions);\n\t}\n\n\t// --------------------------------------------------------------------------------------------- fillBeanWithMap\n\n\t/**\n\t * 使用Map填充Bean对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           Map\n\t * @param bean          Bean\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBeanWithMap(Map<?, ?> map, T bean, boolean isIgnoreError) {\n\t\treturn fillBeanWithMap(map, bean, false, isIgnoreError);\n\t}\n\n\t/**\n\t * 使用Map填充Bean对象，可配置将下划线转换为驼峰\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           Map\n\t * @param bean          Bean\n\t * @param isToCamelCase 是否将下划线模式转换为驼峰模式\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t * @deprecated isToCamelCase参数无效，请使用{@link #fillBeanWithMap(Map, Object, boolean)}\n\t */\n\t@Deprecated\n\tpublic static <T> T fillBeanWithMap(Map<?, ?> map, T bean, boolean isToCamelCase, boolean isIgnoreError) {\n\t\treturn fillBeanWithMap(map, bean, isToCamelCase, CopyOptions.create().setIgnoreError(isIgnoreError));\n\t}\n\n\t/**\n\t * 使用Map填充Bean对象，忽略大小写\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           Map\n\t * @param bean          Bean\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBeanWithMapIgnoreCase(Map<?, ?> map, T bean, boolean isIgnoreError) {\n\t\treturn fillBeanWithMap(map, bean, CopyOptions.create().setIgnoreCase(true).setIgnoreError(isIgnoreError));\n\t}\n\n\t/**\n\t * 使用Map填充Bean对象\n\t *\n\t * @param <T>         Bean类型\n\t * @param map         Map\n\t * @param bean        Bean\n\t * @param copyOptions 属性复制选项 {@link CopyOptions}\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBeanWithMap(Map<?, ?> map, T bean, CopyOptions copyOptions) {\n\t\tif (MapUtil.isEmpty(map)) {\n\t\t\treturn bean;\n\t\t}\n\t\tcopyProperties(map, bean, copyOptions);\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 使用Map填充Bean对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param map           Map\n\t * @param bean          Bean\n\t * @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格\n\t * @param copyOptions   属性复制选项 {@link CopyOptions}\n\t * @return Bean\n\t * @since 3.3.1\n\t * @deprecated isToCamelCase参数无效，请使用{@link #fillBeanWithMap(Map, Object, CopyOptions)}\n\t */\n\t@Deprecated\n\tpublic static <T> T fillBeanWithMap(Map<?, ?> map, T bean, boolean isToCamelCase, CopyOptions copyOptions) {\n\t\tif (MapUtil.isEmpty(map)) {\n\t\t\treturn bean;\n\t\t}\n\n\t\t// issue#3452，参数无效，MapToBeanCopier中已经有转驼峰逻辑\n//\t\tif (isToCamelCase) {\n//\t\t\tmap = MapUtil.toCamelCaseMap(map);\n//\t\t}\n\t\tcopyProperties(map, bean, copyOptions);\n\t\treturn bean;\n\t}\n\n\t// --------------------------------------------------------------------------------------------- fillBean\n\n\t/**\n\t * 对象或Map转Bean\n\t *\n\t * @param <T>    转换的Bean类型\n\t * @param source Bean对象或Map\n\t * @param clazz  目标的Bean类型\n\t * @return Bean对象\n\t * @since 4.1.20\n\t */\n\tpublic static <T> T toBean(Object source, Class<T> clazz) {\n\t\treturn toBean(source, clazz, null);\n\t}\n\n\t/**\n\t * 对象或Map转Bean，忽略字段转换时发生的异常\n\t *\n\t * @param <T>    转换的Bean类型\n\t * @param source Bean对象或Map\n\t * @param clazz  目标的Bean类型\n\t * @return Bean对象\n\t * @since 5.4.0\n\t */\n\tpublic static <T> T toBeanIgnoreError(Object source, Class<T> clazz) {\n\t\treturn toBean(source, clazz, CopyOptions.create().setIgnoreError(true));\n\t}\n\n\t/**\n\t * 对象或Map转Bean，忽略字段转换时发生的异常\n\t *\n\t * @param <T>         转换的Bean类型\n\t * @param source      Bean对象或Map\n\t * @param clazz       目标的Bean类型\n\t * @param ignoreError 是否忽略注入错误\n\t * @return Bean对象\n\t * @since 5.4.0\n\t */\n\tpublic static <T> T toBeanIgnoreCase(Object source, Class<T> clazz, boolean ignoreError) {\n\t\treturn toBean(source, clazz,\n\t\t\t\tCopyOptions.create()\n\t\t\t\t\t\t.setIgnoreCase(true)\n\t\t\t\t\t\t.setIgnoreError(ignoreError));\n\t}\n\n\t/**\n\t * 对象或Map转Bean\n\t *\n\t * @param <T>     转换的Bean类型\n\t * @param source  Bean对象或Map\n\t * @param clazz   目标的Bean类型\n\t * @param options 属性拷贝选项\n\t * @return Bean对象\n\t * @since 5.2.4\n\t */\n\tpublic static <T> T toBean(Object source, Class<T> clazz, CopyOptions options) {\n\t\treturn toBean(source, () -> ReflectUtil.newInstanceIfPossible(clazz), options);\n\t}\n\n\t/**\n\t * 对象或Map转Bean\n\t *\n\t * @param <T>            转换的Bean类型\n\t * @param source         Bean对象或Map\n\t * @param targetSupplier 目标的Bean创建器\n\t * @param options        属性拷贝选项\n\t * @return Bean对象\n\t * @since 5.8.0\n\t */\n\tpublic static <T> T toBean(Object source, Supplier<T> targetSupplier, CopyOptions options) {\n\t\tif (null == source || null == targetSupplier) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal T target = targetSupplier.get();\n\t\tcopyProperties(source, target, options);\n\t\treturn target;\n\t}\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>           Bean类型\n\t * @param beanClass     Bean Class\n\t * @param valueProvider 值提供者\n\t * @param copyOptions   拷贝选项，见 {@link CopyOptions}\n\t * @return Bean\n\t */\n\tpublic static <T> T toBean(Class<T> beanClass, ValueProvider<String> valueProvider, CopyOptions copyOptions) {\n\t\tif (null == beanClass || null == valueProvider) {\n\t\t\treturn null;\n\t\t}\n\t\treturn fillBean(ReflectUtil.newInstanceIfPossible(beanClass), valueProvider, copyOptions);\n\t}\n\n\t/**\n\t * 填充Bean的核心方法\n\t *\n\t * @param <T>           Bean类型\n\t * @param bean          Bean\n\t * @param valueProvider 值提供者\n\t * @param copyOptions   拷贝选项，见 {@link CopyOptions}\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBean(T bean, ValueProvider<String> valueProvider, CopyOptions copyOptions) {\n\t\tif (null == valueProvider) {\n\t\t\treturn bean;\n\t\t}\n\n\t\treturn BeanCopier.create(valueProvider, bean, copyOptions).copy();\n\t}\n\n\t// --------------------------------------------------------------------------------------------- beanToMap\n\n\t/**\n\t * 将bean的部分属性转换成map<br>\n\t * 可选拷贝哪些属性值，默认是不忽略值为{@code null}的值的。\n\t *\n\t * @param bean       bean\n\t * @param properties 需要拷贝的属性值，{@code null}或空表示拷贝所有值\n\t * @return Map\n\t * @since 5.8.0\n\t */\n\tpublic static Map<String, Object> beanToMap(Object bean, String... properties) {\n\t\tint mapSize = 16;\n\t\tEditor<String> keyEditor = null;\n\t\tif (ArrayUtil.isNotEmpty(properties)) {\n\t\t\tmapSize = properties.length;\n\t\t\tfinal Set<String> propertiesSet = CollUtil.set(false, properties);\n\t\t\tkeyEditor = property -> propertiesSet.contains(property) ? property : null;\n\t\t}\n\n\t\t// 指明了要复制的属性 所以不忽略null值\n\t\treturn beanToMap(bean, new LinkedHashMap<>(mapSize, 1), false, keyEditor);\n\t}\n\n\t/**\n\t * 对象转Map\n\t *\n\t * @param bean              bean对象\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue   是否忽略值为空的字段\n\t * @return Map\n\t */\n\tpublic static Map<String, Object> beanToMap(Object bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\tif (null == bean) {\n\t\t\treturn null;\n\t\t}\n\t\treturn beanToMap(bean, new LinkedHashMap<>(), isToUnderlineCase, ignoreNullValue);\n\t}\n\n\t/**\n\t * 对象转Map\n\t *\n\t * @param bean              bean对象\n\t * @param targetMap         目标的Map\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue   是否忽略值为空的字段\n\t * @return Map\n\t * @since 3.2.3\n\t */\n\tpublic static Map<String, Object> beanToMap(Object bean, Map<String, Object> targetMap, final boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\tif (null == bean) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn beanToMap(bean, targetMap, ignoreNullValue, key -> isToUnderlineCase ? StrUtil.toUnderlineCase(key) : key);\n\t}\n\n\t/**\n\t * 对象转Map<br>\n\t * 通过实现{@link Editor} 可以自定义字段值，如果这个Editor返回null则忽略这个字段，以便实现：\n\t *\n\t * <pre>\n\t * 1. 字段筛选，可以去除不需要的字段\n\t * 2. 字段变换，例如实现驼峰转下划线\n\t * 3. 自定义字段前缀或后缀等等\n\t * </pre>\n\t *\n\t * @param bean            bean对象\n\t * @param targetMap       目标的Map\n\t * @param ignoreNullValue 是否忽略值为空的字段\n\t * @param keyEditor       属性字段（Map的key）编辑器，用于筛选、编辑key，如果这个Editor返回null则忽略这个字段\n\t * @return Map\n\t * @since 4.0.5\n\t */\n\tpublic static Map<String, Object> beanToMap(Object bean, Map<String, Object> targetMap, boolean ignoreNullValue, Editor<String> keyEditor) {\n\t\tif (null == bean) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn BeanCopier.create(bean, targetMap,\n\t\t\t\tCopyOptions.create()\n\t\t\t\t\t\t.setIgnoreNullValue(ignoreNullValue)\n\t\t\t\t\t\t.setFieldNameEditor(keyEditor)\n\t\t).copy();\n\t}\n\n\t/**\n\t * 对象转Map<br>\n\t * 通过自定义{@link CopyOptions} 完成抓换选项，以便实现：\n\t *\n\t * <pre>\n\t * 1. 字段筛选，可以去除不需要的字段\n\t * 2. 字段变换，例如实现驼峰转下划线\n\t * 3. 自定义字段前缀或后缀等等\n\t * 4. 字段值处理\n\t * ...\n\t * </pre>\n\t *\n\t * @param bean        bean对象\n\t * @param targetMap   目标的Map\n\t * @param copyOptions 拷贝选项\n\t * @return Map\n\t * @since 5.7.15\n\t */\n\tpublic static Map<String, Object> beanToMap(Object bean, Map<String, Object> targetMap, CopyOptions copyOptions) {\n\t\tif (null == bean) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn BeanCopier.create(bean, targetMap, copyOptions).copy();\n\t}\n\n\t// --------------------------------------------------------------------------------------------- copyProperties\n\n\t/**\n\t * 按照Bean对象属性创建对应的Class对象，并忽略某些属性\n\t *\n\t * @param <T>              对象类型\n\t * @param source           源Bean对象\n\t * @param tClass           目标Class\n\t * @param ignoreProperties 不拷贝的的属性列表\n\t * @return 目标对象\n\t */\n\tpublic static <T> T copyProperties(Object source, Class<T> tClass, String... ignoreProperties) {\n\t\tif (null == source) {\n\t\t\treturn null;\n\t\t}\n\t\tT target = ReflectUtil.newInstanceIfPossible(tClass);\n\t\tcopyProperties(source, target, CopyOptions.create().setIgnoreProperties(ignoreProperties));\n\t\treturn target;\n\t}\n\n\t/**\n\t * 复制Bean对象属性<br>\n\t * 限制类用于限制拷贝的属性，例如一个类我只想复制其父类的一些属性，就可以将editable设置为父类\n\t *\n\t * @param source           源Bean对象\n\t * @param target           目标Bean对象\n\t * @param ignoreProperties 不拷贝的的属性列表\n\t */\n\tpublic static void copyProperties(Object source, Object target, String... ignoreProperties) {\n\t\tcopyProperties(source, target, CopyOptions.create().setIgnoreProperties(ignoreProperties));\n\t}\n\n\t/**\n\t * 复制Bean对象属性<br>\n\t *\n\t * @param source     源Bean对象\n\t * @param target     目标Bean对象\n\t * @param ignoreCase 是否忽略大小写\n\t */\n\tpublic static void copyProperties(Object source, Object target, boolean ignoreCase) {\n\t\tBeanCopier.create(source, target, CopyOptions.create().setIgnoreCase(ignoreCase)).copy();\n\t}\n\n\t/**\n\t * 复制Bean对象属性<br>\n\t * 限制类用于限制拷贝的属性，例如一个类我只想复制其父类的一些属性，就可以将editable设置为父类\n\t *\n\t * @param source      源Bean对象\n\t * @param target      目标Bean对象\n\t * @param copyOptions 拷贝选项，见 {@link CopyOptions}\n\t */\n\tpublic static void copyProperties(Object source, Object target, CopyOptions copyOptions) {\n\t\tif (null == source) {\n\t\t\treturn;\n\t\t}\n\t\tBeanCopier.create(source, target, ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create)).copy();\n\t}\n\n\t/**\n\t * 复制集合中的Bean属性<br>\n\t * 此方法遍历集合中每个Bean，复制其属性后加入一个新的{@link List}中。\n\t *\n\t * @param collection  原Bean集合\n\t * @param targetType  目标Bean类型\n\t * @param copyOptions 拷贝选项\n\t * @param <T>         Bean类型\n\t * @return 复制后的List\n\t * @since 5.6.4\n\t */\n\tpublic static <T> List<T> copyToList(Collection<?> collection, Class<T> targetType, CopyOptions copyOptions) {\n\t\tif (null == collection) {\n\t\t\treturn null;\n\t\t}\n\t\tif (collection.isEmpty()) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\t// issue#3091\n\t\tif(ClassUtil.isBasicType(targetType) || String.class == targetType){\n\t\t\treturn Convert.toList(targetType, collection);\n\t\t}\n\n\t\treturn collection.stream().map((source) -> {\n\t\t\tfinal T target = ReflectUtil.newInstanceIfPossible(targetType);\n\t\t\tcopyProperties(source, target, copyOptions);\n\t\t\treturn target;\n\t\t}).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 复制集合中的Bean属性<br>\n\t * 此方法遍历集合中每个Bean，复制其属性后加入一个新的{@link List}中。\n\t *\n\t * @param collection 原Bean集合\n\t * @param targetType 目标Bean类型\n\t * @param <T>        Bean类型\n\t * @return 复制后的List\n\t * @since 5.6.6\n\t */\n\tpublic static <T> List<T> copyToList(Collection<?> collection, Class<T> targetType) {\n\t\treturn copyToList(collection, targetType, CopyOptions.create());\n\t}\n\n\t/**\n\t * 给定的Bean的类名是否匹配指定类名字符串<br>\n\t * 如果isSimple为{@code true}，则只匹配类名而忽略包名，例如：cn.hutool.TestEntity只匹配TestEntity<br>\n\t * 如果isSimple为{@code false}，则匹配包括包名的全类名，例如：cn.hutool.TestEntity匹配cn.hutool.TestEntity\n\t *\n\t * @param bean          Bean\n\t * @param beanClassName Bean的类名\n\t * @param isSimple      是否只匹配类名而忽略包名，true表示忽略包名\n\t * @return 是否匹配\n\t * @since 4.0.6\n\t */\n\tpublic static boolean isMatchName(Object bean, String beanClassName, boolean isSimple) {\n\t\tif (null == bean || StrUtil.isBlank(beanClassName)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ClassUtil.getClassName(bean, isSimple).equals(isSimple ? StrUtil.upperFirst(beanClassName) : beanClassName);\n\t}\n\n\t/**\n\t * 编辑Bean的字段，static字段不会处理<br>\n\t * 例如需要对指定的字段做判空操作、null转\"\"操作等等。\n\t *\n\t * @param bean   bean\n\t * @param editor 编辑器函数\n\t * @param <T>    被编辑的Bean类型\n\t * @return bean\n\t * @since 5.6.4\n\t */\n\tpublic static <T> T edit(T bean, Editor<Field> editor) {\n\t\tif (bean == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Field[] fields = ReflectUtil.getFields(bean.getClass());\n\t\tfor (Field field : fields) {\n\t\t\tif (ModifierUtil.isStatic(field)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\teditor.edit(field);\n\t\t}\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 把Bean里面的String属性做trim操作。此方法直接对传入的Bean做修改。\n\t * <p>\n\t * 通常bean直接用来绑定页面的input，用户的输入可能首尾存在空格，通常保存数据库前需要把首尾空格去掉\n\t *\n\t * @param <T>          Bean类型\n\t * @param bean         Bean对象\n\t * @param ignoreFields 不需要trim的Field名称列表（不区分大小写）\n\t * @return 处理后的Bean对象\n\t */\n\tpublic static <T> T trimStrFields(T bean, String... ignoreFields) {\n\t\treturn edit(bean, (field) -> {\n\t\t\tif (ignoreFields != null && ArrayUtil.containsIgnoreCase(ignoreFields, field.getName())) {\n\t\t\t\t// 不处理忽略的Fields\n\t\t\t\treturn field;\n\t\t\t}\n\t\t\tif (String.class.equals(field.getType())) {\n\t\t\t\t// 只有String的Field才处理\n\t\t\t\tfinal String val = (String) ReflectUtil.getFieldValue(bean, field);\n\t\t\t\tif (null != val) {\n\t\t\t\t\tfinal String trimVal = StrUtil.trim(val);\n\t\t\t\t\tif (false == val.equals(trimVal)) {\n\t\t\t\t\t\t// Field Value不为null，且首尾有空格才处理\n\t\t\t\t\t\tReflectUtil.setFieldValue(bean, field, trimVal);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn field;\n\t\t});\n\t}\n\n\t/**\n\t * 判断Bean是否为非空对象，非空对象表示本身不为{@code null}或者含有非{@code null}属性的对象\n\t *\n\t * @param bean             Bean对象\n\t * @param ignoreFieldNames 忽略检查的字段名\n\t * @return 是否为非空，{@code true} - 非空 / {@code false} - 空\n\t * @since 5.0.7\n\t */\n\tpublic static boolean isNotEmpty(Object bean, String... ignoreFieldNames) {\n\t\treturn false == isEmpty(bean, ignoreFieldNames);\n\t}\n\n\t/**\n\t * 判断Bean是否为空对象，空对象表示本身为{@code null}或者所有属性都为{@code null}<br>\n\t * 此方法不判断static属性\n\t *\n\t * @param bean             Bean对象\n\t * @param ignoreFieldNames 忽略检查的字段名\n\t * @return 是否为空，{@code true} - 空 / {@code false} - 非空\n\t * @since 4.1.10\n\t */\n\tpublic static boolean isEmpty(Object bean, String... ignoreFieldNames) {\n\t\tif (null != bean) {\n\t\t\tfor (Field field : ReflectUtil.getFields(bean.getClass())) {\n\t\t\t\tif (ModifierUtil.isStatic(field)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif ((false == ArrayUtil.contains(ignoreFieldNames, field.getName()))\n\t\t\t\t\t\t&& null != ReflectUtil.getFieldValue(bean, field)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 判断Bean是否包含值为{@code null}的属性<br>\n\t * 对象本身为{@code null}也返回true\n\t *\n\t * @param bean             Bean对象\n\t * @param ignoreFieldNames 忽略检查的字段名\n\t * @return 是否包含值为<code>null</code>的属性，{@code true} - 包含 / {@code false} - 不包含\n\t * @since 4.1.10\n\t */\n\tpublic static boolean hasNullField(Object bean, String... ignoreFieldNames) {\n\t\tif (null == bean) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (Field field : ReflectUtil.getFields(bean.getClass())) {\n\t\t\tif (ModifierUtil.isStatic(field)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ((false == ArrayUtil.contains(ignoreFieldNames, field.getName()))\n\t\t\t\t\t&& null == ReflectUtil.getFieldValue(bean, field)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取Getter或Setter方法名对应的字段名称，规则如下：\n\t * <ul>\n\t *     <li>getXxxx获取为xxxx，如getName得到name。</li>\n\t *     <li>setXxxx获取为xxxx，如setName得到name。</li>\n\t *     <li>isXxxx获取为xxxx，如isName得到name。</li>\n\t *     <li>其它不满足规则的方法名抛出{@link IllegalArgumentException}</li>\n\t * </ul>\n\t *\n\t * @param getterOrSetterName Getter或Setter方法名\n\t * @return 字段名称\n\t * @throws IllegalArgumentException 非Getter或Setter方法\n\t * @since 5.7.23\n\t */\n\tpublic static String getFieldName(String getterOrSetterName) {\n\t\tif (getterOrSetterName.startsWith(\"get\") || getterOrSetterName.startsWith(\"set\")) {\n\t\t\treturn StrUtil.removePreAndLowerFirst(getterOrSetterName, 3);\n\t\t} else if (getterOrSetterName.startsWith(\"is\")) {\n\t\t\treturn StrUtil.removePreAndLowerFirst(getterOrSetterName, 2);\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"Invalid Getter or Setter name: \" + getterOrSetterName);\n\t\t}\n\t}\n\n\t/**\n\t * 判断source与target的所有公共字段的值是否相同\n\t *\n\t * @param source           待检测对象1\n\t * @param target           待检测对象2\n\t * @param ignoreProperties 不需要检测的字段\n\t * @return 判断结果，如果为true则证明所有字段的值都相同\n\t * @author Takak11\n\t * @since 5.8.4\n\t */\n\tpublic static boolean isCommonFieldsEqual(Object source, Object target, String... ignoreProperties) {\n\n\t\tif (null == source && null == target) {\n\t\t\treturn true;\n\t\t}\n\t\tif (null == source || null == target) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal Map<String, Object> sourceFieldsMap = BeanUtil.beanToMap(source);\n\t\tfinal Map<String, Object> targetFieldsMap = BeanUtil.beanToMap(target);\n\n\t\tfinal Set<String> sourceFields = sourceFieldsMap.keySet();\n\t\tsourceFields.removeAll(Arrays.asList(ignoreProperties));\n\n\t\tfor (String field : sourceFields) {\n\t\t\tif(sourceFieldsMap.containsKey(field) && targetFieldsMap.containsKey(field)){\n\t\t\t\tif (ObjectUtil.notEqual(sourceFieldsMap.get(field), targetFieldsMap.get(field))) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/DynaBean.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.clone.CloneSupport;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n/**\n * 动态Bean，通过反射对Bean的相关方法做操作<br>\n * 支持Map和普通Bean\n *\n * @author Looly\n * @since 3.0.7\n */\npublic class DynaBean extends CloneSupport<DynaBean> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<?> beanClass;\n\tprivate final Object bean;\n\n\t/**\n\t * 创建一个DynaBean\n\t *\n\t * @param bean 普通Bean\n\t * @return DynaBean\n\t */\n\tpublic static DynaBean create(Object bean) {\n\t\treturn new DynaBean(bean);\n\t}\n\n\t/**\n\t * 创建一个DynaBean\n\t *\n\t * @param beanClass Bean类\n\t * @return DynaBean\n\t */\n\tpublic static DynaBean create(Class<?> beanClass) {\n\t\treturn new DynaBean(beanClass);\n\t}\n\n\n\t/**\n\t * 创建一个DynaBean\n\t *\n\t * @param beanClass Bean类\n\t * @param params    构造Bean所需要的参数\n\t * @return DynaBean\n\t */\n\tpublic static DynaBean create(Class<?> beanClass, Object... params) {\n\t\treturn new DynaBean(beanClass, params);\n\t}\n\n\t//------------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanClass Bean类\n\t * @param params    构造Bean所需要的参数\n\t */\n\tpublic DynaBean(Class<?> beanClass, Object... params) {\n\t\tthis(ReflectUtil.newInstance(beanClass, params));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanClass Bean类\n\t */\n\tpublic DynaBean(Class<?> beanClass) {\n\t\tthis(ReflectUtil.newInstance(beanClass));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bean 原始Bean\n\t */\n\tpublic DynaBean(Object bean) {\n\t\tAssert.notNull(bean);\n\t\tif (bean instanceof DynaBean) {\n\t\t\tbean = ((DynaBean) bean).getBean();\n\t\t}\n\t\tthis.bean = bean;\n\t\tthis.beanClass = ClassUtil.getClass(bean);\n\t}\n\t//------------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 获得字段对应值\n\t *\n\t * @param <T>       属性值类型\n\t * @param fieldName 字段名\n\t * @return 字段值\n\t * @throws BeanException 反射获取属性值或字段值导致的异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T get(String fieldName) throws BeanException {\n\t\tif (Map.class.isAssignableFrom(beanClass)) {\n\t\t\treturn (T) ((Map<?, ?>) bean).get(fieldName);\n\t\t} else {\n\t\t\tfinal PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName);\n\t\t\tif (null == prop) {\n\t\t\t\tthrow new BeanException(\"No public field or get method for {}\", fieldName);\n\t\t\t}\n\t\t\treturn (T) prop.getValue(bean);\n\t\t}\n\t}\n\n\t/**\n\t * 检查是否有指定名称的bean属性\n\t *\n\t * @param fieldName 字段名\n\t * @return 是否有bean属性\n\t * @since 5.4.2\n\t */\n\tpublic boolean containsProp(String fieldName) {\n\t\tif (Map.class.isAssignableFrom(beanClass)) {\n\t\t\treturn ((Map<?, ?>) bean).containsKey(fieldName);\n\t\t} else{\n\t\t\treturn null != BeanUtil.getBeanDesc(beanClass).getProp(fieldName);\n\t\t}\n\t}\n\n\t/**\n\t * 获得字段对应值，获取异常返回{@code null}\n\t *\n\t * @param <T>       属性值类型\n\t * @param fieldName 字段名\n\t * @return 字段值\n\t * @since 3.1.1\n\t */\n\tpublic <T> T safeGet(String fieldName) {\n\t\ttry {\n\t\t\treturn get(fieldName);\n\t\t} catch (Exception e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 设置字段值\n\t *\n\t * @param fieldName 字段名\n\t * @param value     字段值\n\t * @throws BeanException 反射获取属性值或字段值导致的异常\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic void set(String fieldName, Object value) throws BeanException {\n\t\tif (Map.class.isAssignableFrom(beanClass)) {\n\t\t\t((Map) bean).put(fieldName, value);\n\t\t} else {\n\t\t\tfinal PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName);\n\t\t\tif (null == prop) {\n\t\t\t\tthrow new BeanException(\"No public field or set method for {}\", fieldName);\n\t\t\t}\n\t\t\tprop.setValue(bean, value);\n\t\t}\n\t}\n\n\t/**\n\t * 执行原始Bean中的方法\n\t *\n\t * @param methodName 方法名\n\t * @param params     参数\n\t * @return 执行结果，可能为null\n\t */\n\tpublic Object invoke(String methodName, Object... params) {\n\t\treturn ReflectUtil.invoke(this.bean, methodName, params);\n\t}\n\n\t/**\n\t * 获得原始Bean\n\t *\n\t * @param <T> Bean类型\n\t * @return bean\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getBean() {\n\t\treturn (T) this.bean;\n\t}\n\n\t/**\n\t * 获得Bean的类型\n\t *\n\t * @param <T> Bean类型\n\t * @return Bean类型\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> Class<T> getBeanClass() {\n\t\treturn (Class<T>) this.beanClass;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + ((bean == null) ? 0 : bean.hashCode());\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal DynaBean other = (DynaBean) obj;\n\t\tif (bean == null) {\n\t\t\treturn other.bean == null;\n\t\t} else return bean.equals(other.bean);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.bean.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/NullWrapperBean.java",
    "content": "package cn.hutool.core.bean;\n\n/**\n * 为了解决反射过程中,需要传递null参数,但是会丢失参数类型而设立的包装类\n *\n * @param <T> Null值对应的类型\n * @author Lillls\n * @since 5.5.0\n */\npublic class NullWrapperBean<T> {\n\n\tprivate final Class<T> clazz;\n\n\t/**\n\t * @param clazz null的类型\n\t */\n\tpublic NullWrapperBean(Class<T> clazz) {\n\t\tthis.clazz = clazz;\n\t}\n\n\t/**\n\t * 获取null值对应的类型\n\t *\n\t * @return 类型\n\t */\n\tpublic Class<T> getWrappedClass() {\n\t\treturn clazz;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.annotation.AnnotationUtil;\nimport cn.hutool.core.annotation.PropIgnore;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ModifierUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\n\n/**\n * 属性描述，包括了字段、getter、setter和相应的方法执行\n *\n * @author looly\n */\npublic class PropDesc {\n\n\t/**\n\t * Transient注解的类名\n\t */\n\tprivate static final String TRANSIENT_CLASS_NAME = \"java.beans.Transient\";\n\n\t/**\n\t * 字段\n\t */\n\tfinal Field field;\n\t/**\n\t * Getter方法\n\t */\n\tprotected Method getter;\n\t/**\n\t * Setter方法\n\t */\n\tprotected Method setter;\n\t/**\n\t * get方法或字段上有无transient关键字和@Transient注解\n\t */\n\tprivate boolean transientForGet;\n\t/**\n\t * set方法或字段上有无transient关键字和@Transient注解\n\t */\n\tprivate boolean transientForSet;\n\t/**\n\t * 检查set方法和字段有无@PropIgnore注解\n\t */\n\tprivate boolean ignoreGet;\n\t/**\n\t * 检查set方法和字段有无@PropIgnore注解\n\t */\n\tprivate boolean ignoreSet;\n\n\t/**\n\t * 构造<br>\n\t * Getter和Setter方法设置为默认可访问\n\t *\n\t * @param field  字段\n\t * @param getter get方法\n\t * @param setter set方法\n\t */\n\tpublic PropDesc(Field field, Method getter, Method setter) {\n\t\tthis.field = field;\n\t\tthis.getter = ClassUtil.setAccessible(getter);\n\t\tthis.setter = ClassUtil.setAccessible(setter);\n\t}\n\n\t/**\n\t * 在对象的所有属性设置完成后，执行初始化逻辑。\n\t * <p>\n\t * 预先计算transient关键字和@Transient注解、{@link PropIgnore}注解信息<br>\n\t * 见：https://gitee.com/chinabugotech/hutool/pulls/1335\n\t *\n\t * @since 5.8.38\n\t */\n\tpublic void initialize() {\n\t\ttransientForGet = isTransientForGet();\n\t\ttransientForSet = isTransientForSet();\n\t\tignoreGet = isIgnoreGet();\n\t\tignoreSet = isIgnoreSet();\n\t}\n\n\t/**\n\t * 获取字段名，如果存在Alias注解，读取注解的值作为名称\n\t *\n\t * @return 字段名\n\t */\n\tpublic String getFieldName() {\n\t\treturn ReflectUtil.getFieldName(this.field);\n\t}\n\n\t/**\n\t * 获取字段名称\n\t *\n\t * @return 字段名\n\t * @since 5.1.6\n\t */\n\tpublic String getRawFieldName() {\n\t\treturn null == this.field ? null : this.field.getName();\n\t}\n\n\t/**\n\t * 获取字段\n\t *\n\t * @return 字段\n\t */\n\tpublic Field getField() {\n\t\treturn this.field;\n\t}\n\n\t/**\n\t * 获得字段类型<br>\n\t * 先获取字段的类型，如果字段不存在，则获取Getter方法的返回类型，否则获取Setter的第一个参数类型\n\t *\n\t * @return 字段类型\n\t */\n\tpublic Type getFieldType() {\n\t\tif (null != this.field) {\n\t\t\treturn TypeUtil.getType(this.field);\n\t\t}\n\t\treturn findPropType(getter, setter);\n\t}\n\n\t/**\n\t * 获得字段类型<br>\n\t * 先获取字段的类型，如果字段不存在，则获取Getter方法的返回类型，否则获取Setter的第一个参数类型\n\t *\n\t * @return 字段类型\n\t */\n\tpublic Class<?> getFieldClass() {\n\t\tif (null != this.field) {\n\t\t\treturn TypeUtil.getClass(this.field);\n\t\t}\n\t\treturn findPropClass(getter, setter);\n\t}\n\n\t/**\n\t * 获取Getter方法，可能为{@code null}\n\t *\n\t * @return Getter方法\n\t */\n\tpublic Method getGetter() {\n\t\treturn this.getter;\n\t}\n\n\t/**\n\t * 获取Setter方法，可能为{@code null}\n\t *\n\t * @return {@link Method}Setter 方法对象\n\t */\n\tpublic Method getSetter() {\n\t\treturn this.setter;\n\t}\n\n\t/**\n\t * 检查属性是否可读（即是否可以通过{@link #getValue(Object)}获取到值）\n\t *\n\t * @param checkTransient 是否检查Transient关键字或注解\n\t * @return 是否可读\n\t * @since 5.4.2\n\t */\n\tpublic boolean isReadable(boolean checkTransient) {\n\t\t// 检查是否有getter方法或是否为public修饰\n\t\tif (null == this.getter && false == ModifierUtil.isPublic(this.field)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 检查transient关键字和@Transient注解\n\t\tif (checkTransient && transientForGet) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 检查@PropIgnore注解\n\t\treturn false == ignoreGet;\n\t}\n\n\t/**\n\t * 获取属性值<br>\n\t * 首先调用字段对应的Getter方法获取值，如果Getter方法不存在，则判断字段如果为public，则直接获取字段值<br>\n\t * 此方法不检查任何注解，使用前需调用 {@link #isReadable(boolean)} 检查是否可读\n\t *\n\t * @param bean Bean对象\n\t * @return 字段值\n\t * @since 4.0.5\n\t */\n\tpublic Object getValue(Object bean) {\n\t\tif (null != this.getter) {\n\t\t\treturn ReflectUtil.invoke(bean, this.getter);\n\t\t} else if (ModifierUtil.isPublic(this.field)) {\n\t\t\treturn ReflectUtil.getFieldValue(bean, this.field);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取属性值，自动转换属性值类型<br>\n\t * 首先调用字段对应的Getter方法获取值，如果Getter方法不存在，则判断字段如果为public，则直接获取字段值\n\t *\n\t * @param bean        Bean对象\n\t * @param targetType  返回属性值需要转换的类型，null表示不转换\n\t * @param ignoreError 是否忽略错误，包括转换错误和注入错误\n\t * @return this\n\t * @since 5.4.2\n\t */\n\tpublic Object getValue(Object bean, Type targetType, boolean ignoreError) {\n\t\tObject result = null;\n\t\ttry {\n\t\t\tresult = getValue(bean);\n\t\t} catch (Exception e) {\n\t\t\tif (false == ignoreError) {\n\t\t\t\tthrow new BeanException(e, \"Get value of [{}] error!\", getFieldName());\n\t\t\t}\n\t\t}\n\n\t\tif (null != result && null != targetType) {\n\t\t\t// 尝试将结果转换为目标类型，如果转换失败，返回null，即跳过此属性值。\n\t\t\t// 来自：issues#I41WKP@Gitee，当忽略错误情况下，目标类型转换失败应返回null\n\t\t\t// 如果返回原值，在集合注入时会成功，但是集合取值时会报类型转换错误\n\t\t\treturn Convert.convertWithCheck(targetType, result, null, ignoreError);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 检查属性是否可读（即是否可以通过{@link #getValue(Object)}获取到值）\n\t *\n\t * @param checkTransient 是否检查Transient关键字或注解\n\t * @return 是否可读\n\t * @since 5.4.2\n\t */\n\tpublic boolean isWritable(boolean checkTransient) {\n\t\t// 检查是否有getter方法或是否为public修饰\n\t\tif (null == this.setter && false == ModifierUtil.isPublic(this.field)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 检查transient关键字和@Transient注解\n\t\tif (checkTransient && transientForSet) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 检查@PropIgnore注解\n\t\treturn false == ignoreSet;\n\t}\n\n\t/**\n\t * 设置Bean的字段值<br>\n\t * 首先调用字段对应的Setter方法，如果Setter方法不存在，则判断字段如果为public，则直接赋值字段值<br>\n\t * 此方法不检查任何注解，使用前需调用 {@link #isWritable(boolean)} 检查是否可写\n\t *\n\t * @param bean  Bean对象\n\t * @param value 值，必须与字段值类型匹配\n\t * @return this\n\t * @since 4.0.5\n\t */\n\tpublic PropDesc setValue(Object bean, Object value) {\n\t\tif (null != this.setter) {\n\t\t\tReflectUtil.invoke(bean, this.setter, value);\n\t\t} else if (ModifierUtil.isPublic(this.field)) {\n\t\t\tReflectUtil.setFieldValue(bean, this.field, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置属性值，可以自动转换字段类型为目标类型\n\t *\n\t * @param bean        Bean对象\n\t * @param value       属性值，可以为任意类型\n\t * @param ignoreNull  是否忽略{@code null}值，true表示忽略\n\t * @param ignoreError 是否忽略错误，包括转换错误和注入错误\n\t * @return this\n\t * @since 5.4.2\n\t */\n\tpublic PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError) {\n\t\treturn setValue(bean, value, ignoreNull, ignoreError, true);\n\t}\n\n\t/**\n\t * 设置属性值，可以自动转换字段类型为目标类型\n\t *\n\t * @param bean        Bean对象\n\t * @param value       属性值，可以为任意类型\n\t * @param ignoreNull  是否忽略{@code null}值，true表示忽略\n\t * @param ignoreError 是否忽略错误，包括转换错误和注入错误\n\t * @param override    是否覆盖目标值，如果不覆盖，会先读取bean的值，{@code null}则写，否则忽略。如果覆盖，则不判断直接写\n\t * @return this\n\t * @since 5.7.17\n\t */\n\tpublic PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError, boolean override) {\n\t\tif (null == value && ignoreNull) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// issue#I4JQ1N@Gitee\n\t\t// 非覆盖模式下，如果目标值存在，则跳过\n\t\tif (false == override && null != getValue(bean)) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// 当类型不匹配的时候，执行默认转换\n\t\tif (null != value) {\n\t\t\tfinal Class<?> propClass = getFieldClass();\n\t\t\tif (false == propClass.isInstance(value)) {\n\t\t\t\tvalue = Convert.convertWithCheck(propClass, value, null, ignoreError);\n\t\t\t}\n\t\t}\n\n\t\t// 属性赋值\n\t\tif (null != value || false == ignoreNull) {\n\t\t\ttry {\n\t\t\t\tthis.setValue(bean, value);\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (false == ignoreError) {\n\t\t\t\t\tthrow new BeanException(e, \"Set value of [{}] error!\", getFieldName());\n\t\t\t\t}\n\t\t\t\t// 忽略注入失败\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t//------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 通过Getter和Setter方法中找到属性类型\n\t *\n\t * @param getter Getter方法\n\t * @param setter Setter方法\n\t * @return {@link Type}\n\t */\n\tprivate Type findPropType(Method getter, Method setter) {\n\t\tType type = null;\n\t\tif (null != getter) {\n\t\t\ttype = TypeUtil.getReturnType(getter);\n\t\t}\n\t\tif (null == type && null != setter) {\n\t\t\ttype = TypeUtil.getParamType(setter, 0);\n\t\t}\n\t\treturn type;\n\t}\n\n\t/**\n\t * 通过Getter和Setter方法中找到属性类型\n\t *\n\t * @param getter Getter方法\n\t * @param setter Setter方法\n\t * @return {@link Type}\n\t */\n\tprivate Class<?> findPropClass(Method getter, Method setter) {\n\t\tClass<?> type = null;\n\t\tif (null != getter) {\n\t\t\ttype = TypeUtil.getReturnClass(getter);\n\t\t}\n\t\tif (null == type && null != setter) {\n\t\t\ttype = TypeUtil.getFirstParamClass(setter);\n\t\t}\n\t\treturn type;\n\t}\n\n\t/**\n\t * 检查字段是否被忽略写，通过{@link PropIgnore} 注解完成，规则为：\n\t * <pre>\n\t *     1. 在字段上有{@link PropIgnore} 注解\n\t *     2. 在setXXX方法上有{@link PropIgnore} 注解\n\t * </pre>\n\t *\n\t * @return 是否忽略写\n\t * @since 5.4.2\n\t */\n\tprivate boolean isIgnoreSet() {\n\t\treturn AnnotationUtil.hasAnnotation(this.field, PropIgnore.class)\n\t\t\t|| AnnotationUtil.hasAnnotation(this.setter, PropIgnore.class);\n\t}\n\n\t/**\n\t * 检查字段是否被忽略读，通过{@link PropIgnore} 注解完成，规则为：\n\t * <pre>\n\t *     1. 在字段上有{@link PropIgnore} 注解\n\t *     2. 在getXXX方法上有{@link PropIgnore} 注解\n\t * </pre>\n\t *\n\t * @return 是否忽略读\n\t * @since 5.4.2\n\t */\n\tprivate boolean isIgnoreGet() {\n\t\treturn AnnotationUtil.hasAnnotation(this.field, PropIgnore.class)\n\t\t\t|| AnnotationUtil.hasAnnotation(this.getter, PropIgnore.class);\n\t}\n\n\t/**\n\t * 字段和Getter方法是否为Transient关键字修饰的\n\t *\n\t * @return 是否为Transient关键字修饰的\n\t * @since 5.3.11\n\t */\n\tprivate boolean isTransientForGet() {\n\t\tboolean isTransient = ModifierUtil.hasModifier(this.field, ModifierUtil.ModifierType.TRANSIENT);\n\n\t\t// 检查Getter方法\n\t\tif (false == isTransient && null != this.getter) {\n\t\t\tisTransient = ModifierUtil.hasModifier(this.getter, ModifierUtil.ModifierType.TRANSIENT);\n\n\t\t\t// 检查注解\n\t\t\tif (false == isTransient) {\n\t\t\t\tisTransient = AnnotationUtil.hasAnnotation(this.getter, TRANSIENT_CLASS_NAME);\n\t\t\t}\n\t\t}\n\n\t\treturn isTransient;\n\t}\n\n\t/**\n\t * 字段和Getter方法是否为Transient关键字修饰的\n\t *\n\t * @return 是否为Transient关键字修饰的\n\t * @since 5.3.11\n\t */\n\tprivate boolean isTransientForSet() {\n\t\tboolean isTransient = ModifierUtil.hasModifier(this.field, ModifierUtil.ModifierType.TRANSIENT);\n\n\t\t// 检查Getter方法\n\t\tif (false == isTransient && null != this.setter) {\n\t\t\tisTransient = ModifierUtil.hasModifier(this.setter, ModifierUtil.ModifierType.TRANSIENT);\n\n\t\t\t// 检查注解\n\t\t\tif (false == isTransient) {\n\t\t\t\tisTransient = AnnotationUtil.hasAnnotation(this.setter, TRANSIENT_CLASS_NAME);\n\t\t\t}\n\t\t}\n\n\t\treturn isTransient;\n\t}\n\t//------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.JdkUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.AbstractMap;\nimport java.util.Map;\n\n/**\n * java.lang.Record 相关工具类封装<br>\n * 来自于FastJSON2的BeanUtils\n *\n * @author fastjson2, Looly\n * @since 5.8.38\n */\npublic class RecordUtil {\n\n\tprivate static volatile Class<?> RECORD_CLASS;\n\n\tprivate static volatile Method METHOD_GET_RECORD_COMPONENTS;\n\tprivate static volatile Method METHOD_COMPONENT_GET_NAME;\n\tprivate static volatile Method METHOD_COMPONENT_GET_GENERIC_TYPE;\n\n\t/**\n\t * 判断给定类是否为Record类\n\t *\n\t * @param clazz 类\n\t * @return 是否为Record类\n\t */\n\tpublic static boolean isRecord(final Class<?> clazz) {\n\t\tif (JdkUtil.JVM_VERSION < 14) {\n\t\t\t// JDK14+支持Record类\n\t\t\treturn false;\n\t\t}\n\t\tfinal Class<?> superClass = clazz.getSuperclass();\n\t\tif (superClass == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (RECORD_CLASS == null) {\n\t\t\t// 此处不使用同步代码，重复赋值并不影响判断\n\t\t\tfinal String superclassName = superClass.getName();\n\t\t\tif (\"java.lang.Record\".equals(superclassName)) {\n\t\t\t\tRECORD_CLASS = superClass;\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn superClass == RECORD_CLASS;\n\t}\n\n\t/**\n\t * 获取Record类中所有字段名称，getter方法名与字段同名\n\t *\n\t * @param recordClass Record类\n\t * @return 字段数组\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static Map.Entry<String, Type>[] getRecordComponents(final Class<?> recordClass) {\n\t\tif (JdkUtil.JVM_VERSION < 14) {\n\t\t\t// JDK14+支持Record类\n\t\t\treturn new Map.Entry[0];\n\t\t}\n\t\tif (null == METHOD_GET_RECORD_COMPONENTS) {\n\t\t\tMETHOD_GET_RECORD_COMPONENTS = ReflectUtil.getMethod(Class.class, \"getRecordComponents\");\n\t\t}\n\n\t\tfinal Class<Object> recordComponentClass = ClassUtil.loadClass(\"java.lang.reflect.RecordComponent\");\n\t\tif (METHOD_COMPONENT_GET_NAME == null) {\n\t\t\tMETHOD_COMPONENT_GET_NAME = ReflectUtil.getMethod(recordComponentClass, \"getName\");\n\t\t}\n\t\tif (METHOD_COMPONENT_GET_GENERIC_TYPE == null) {\n\t\t\tMETHOD_COMPONENT_GET_GENERIC_TYPE = ReflectUtil.getMethod(recordComponentClass, \"getGenericType\");\n\t\t}\n\n\t\tfinal Object[] components = ReflectUtil.invoke(recordClass, METHOD_GET_RECORD_COMPONENTS);\n\t\tfinal Map.Entry<String, Type>[] entries = new Map.Entry[components.length];\n\t\tfor (int i = 0; i < components.length; i++) {\n\t\t\tentries[i] = new AbstractMap.SimpleEntry<>(\n\t\t\t\tReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_NAME),\n\t\t\t\tReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_GENERIC_TYPE)\n\t\t\t);\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * 实例化Record类\n\t *\n\t * @param recordClass   类\n\t * @param valueProvider 参数值提供器\n\t * @return Record类\n\t */\n\tpublic static Object newInstance(final Class<?> recordClass, final ValueProvider<String> valueProvider) {\n\t\tfinal Map.Entry<String, Type>[] recordComponents = getRecordComponents(recordClass);\n\t\tfinal Object[] args = new Object[recordComponents.length];\n\t\tfor (int i = 0; i < args.length; i++) {\n\t\t\targs[i] = valueProvider.value(recordComponents[i].getKey(), recordComponents[i].getValue());\n\t\t}\n\n\t\treturn ReflectUtil.newInstance(recordClass, args);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.lang.copier.Copier;\nimport cn.hutool.core.util.ObjectUtil;\n\n/**\n * 抽象的对象拷贝封装，提供来源对象、目标对象持有\n *\n * @param <S> 来源对象类型\n * @param <T> 目标对象类型\n * @author looly\n * @since 5.8.0\n */\npublic abstract class AbsCopier<S, T> implements Copier<T> {\n\n\tprotected final S source;\n\tprotected final T target;\n\t/**\n\t * 拷贝选项\n\t */\n\tprotected final CopyOptions copyOptions;\n\n\tpublic AbsCopier(S source, T target, CopyOptions copyOptions) {\n\t\tthis.source = source;\n\t\tthis.target = target;\n\t\tthis.copyOptions = ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.copier.Copier;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Bean拷贝，提供：\n *\n * <pre>\n *     1. Bean 转 Bean\n *     2. Bean 转 Map\n *     3. Map  转 Bean\n *     4. Map  转 Map\n * </pre>\n *\n * @author looly\n *\n * @param <T> 目标对象类型\n * @since 3.2.3\n */\npublic class BeanCopier<T> implements Copier<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Copier<T> copier;\n\n\t/**\n\t * 创建BeanCopier\n\t *\n\t * @param <T> 目标Bean类型\n\t * @param source 来源对象，可以是Bean或者Map\n\t * @param target 目标Bean对象\n\t * @param copyOptions 拷贝属性选项\n\t * @return BeanCopier\n\t */\n\tpublic static <T> BeanCopier<T> create(Object source, T target, CopyOptions copyOptions) {\n\t\treturn create(source, target, target.getClass(), copyOptions);\n\t}\n\n\t/**\n\t * 创建BeanCopier\n\t *\n\t * @param <T> 目标Bean类型\n\t * @param source 来源对象，可以是Bean或者Map\n\t * @param target 目标Bean对象\n\t * @param destType 目标的泛型类型，用于标注有泛型参数的Bean对象\n\t * @param copyOptions 拷贝属性选项\n\t * @return BeanCopier\n\t */\n\tpublic static <T> BeanCopier<T> create(Object source, T target, Type destType, CopyOptions copyOptions) {\n\t\treturn new BeanCopier<>(source, target, destType, copyOptions);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 来源对象，可以是Bean或者Map\n\t * @param target 目标Bean对象\n\t * @param targetType 目标的泛型类型，用于标注有泛型参数的Bean对象\n\t * @param copyOptions 拷贝属性选项\n\t */\n\tpublic BeanCopier(Object source, T target, Type targetType, CopyOptions copyOptions) {\n\t\tAssert.notNull(source, \"Source bean must be not null!\");\n\t\tAssert.notNull(target, \"Target bean must be not null!\");\n\t\tCopier<T> copier;\n\t\tif (source instanceof Map) {\n\t\t\tif (target instanceof Map) {\n\t\t\t\t//noinspection unchecked\n\t\t\t\tcopier = (Copier<T>) new MapToMapCopier((Map<?, ?>) source, (Map<?, ?>) target, targetType, copyOptions);\n\t\t\t} else {\n\t\t\t\tcopier = new MapToBeanCopier<>((Map<?, ?>) source, target, targetType, copyOptions);\n\t\t\t}\n\t\t}else if(source instanceof ValueProvider){\n\t\t\t//noinspection unchecked\n\t\t\tcopier = new ValueProviderToBeanCopier<>((ValueProvider<String>) source, target, targetType, copyOptions);\n\t\t} else {\n\t\t\tif (target instanceof Map) {\n\t\t\t\t//noinspection unchecked\n\t\t\t\tcopier = (Copier<T>) new BeanToMapCopier(source, (Map<?, ?>) target, targetType, copyOptions);\n\t\t\t} else {\n\t\t\t\tcopier = new BeanToBeanCopier<>(source, target, targetType, copyOptions);\n\t\t\t}\n\t\t}\n\t\tthis.copier = copier;\n\t}\n\n\t@Override\n\tpublic T copy() {\n\t\treturn copier.copy();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Bean属性拷贝到Bean中的拷贝器\n *\n * @author Admin\n * @param <S> 源Bean类型\n * @param <T> 目标Bean类型\n * @since 5.8.0\n */\npublic class BeanToBeanCopier<S, T> extends AbsCopier<S, T> {\n\n\t/**\n\t * 目标的类型（用于泛型类注入）\n\t */\n\tprivate final Type targetType;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      来源Map\n\t * @param target      目标Bean对象\n\t * @param targetType  目标泛型类型\n\t * @param copyOptions 拷贝选项\n\t */\n\tpublic BeanToBeanCopier(S source, T target, Type targetType, CopyOptions copyOptions) {\n\t\tsuper(source, target, copyOptions);\n\t\tthis.targetType = targetType;\n\t}\n\n\t@Override\n\tpublic T copy() {\n\t\tClass<?> actualEditable = target.getClass();\n\t\tif (null != copyOptions.editable) {\n\t\t\t// 检查限制类是否为target的父类或接口\n\t\t\tAssert.isTrue(copyOptions.editable.isInstance(target),\n\t\t\t\t\t\"Target class [{}] not assignable to Editable class [{}]\", actualEditable.getName(), copyOptions.editable.getName());\n\t\t\tactualEditable = copyOptions.editable;\n\t\t}\n\t\tfinal Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);\n\n\t\tfinal Map<String, PropDesc> sourcePropDescMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(copyOptions.ignoreCase);\n\t\tsourcePropDescMap.forEach((sFieldName, sDesc) -> {\n\t\t\tif (null == sFieldName || false == sDesc.isReadable(copyOptions.transientSupport)) {\n\t\t\t\t// 字段空或不可读，跳过\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsFieldName = copyOptions.editFieldName(sFieldName);\n\t\t\t// 对key做转换，转换后为null的跳过\n\t\t\tif (null == sFieldName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 忽略不需要拷贝的 key,\n\t\t\tif (false == copyOptions.testKeyFilter(sFieldName)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查目标字段可写性\n\t\t\tfinal PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sFieldName);\n\t\t\tif (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {\n\t\t\t\t// 字段不可写，跳过之\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查源对象属性是否过滤属性\n\t\t\tObject sValue = sDesc.getValue(this.source);\n\t\t\tif (false == copyOptions.testPropertyFilter(sDesc.getField(), sValue)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 获取目标字段真实类型并转换源值\n\t\t\tfinal Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType());\n\t\t\t//sValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError);\n\t\t\tsValue = this.copyOptions.convertField(fieldType, sValue);\n\t\t\tsValue = copyOptions.editFieldValue(sFieldName, sValue);\n\n\t\t\t// 目标赋值\n\t\t\ttDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);\n\t\t});\n\t\treturn this.target;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Bean属性拷贝到Map中的拷贝器\n *\n * @since 5.8.0\n */\n@SuppressWarnings(\"rawtypes\")\npublic class BeanToMapCopier extends AbsCopier<Object, Map> {\n\n\t// 提前获取目标值真实类型\n\tprivate final Type[] targetTypeArguments;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      来源Map\n\t * @param target      目标Map对象\n\t * @param targetType  目标泛型类型\n\t * @param copyOptions 拷贝选项\n\t */\n\tpublic BeanToMapCopier(Object source, Map target, Type targetType, CopyOptions copyOptions) {\n\t\tsuper(source, target, copyOptions);\n\t\tthis.targetTypeArguments = TypeUtil.getTypeArguments(targetType);\n\t}\n\n\t@Override\n\tpublic Map copy() {\n\t\tClass<?> actualEditable = source.getClass();\n\t\tif (null != copyOptions.editable) {\n\t\t\t// 检查限制类是否为target的父类或接口\n\t\t\tAssert.isTrue(copyOptions.editable.isInstance(source),\n\t\t\t\t\t\"Source class [{}] not assignable to Editable class [{}]\", actualEditable.getName(), copyOptions.editable.getName());\n\t\t\tactualEditable = copyOptions.editable;\n\t\t}\n\n\t\tfinal Map<String, PropDesc> sourcePropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);\n\t\tsourcePropDescMap.forEach((sFieldName, sDesc) -> {\n\t\t\tif (null == sFieldName || false == sDesc.isReadable(copyOptions.transientSupport)) {\n\t\t\t\t// 字段空或不可读，跳过\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsFieldName = copyOptions.editFieldName(sFieldName);\n\t\t\t// 对key做转换，转换后为null的跳过\n\t\t\tif (null == sFieldName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 忽略不需要拷贝的 key,\n\t\t\tif (false == copyOptions.testKeyFilter(sFieldName)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查源对象属性是否过滤属性\n\t\t\tObject sValue = sDesc.getValue(this.source);\n\t\t\tif (false == copyOptions.testPropertyFilter(sDesc.getField(), sValue)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 尝试转换源值\n\t\t\tif(null != targetTypeArguments && targetTypeArguments.length > 1){\n\t\t\t\t//sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);\n\t\t\t\tsValue = this.copyOptions.convertField(targetTypeArguments[1], sValue);\n\t\t\t}\n\n\t\t\t// 自定义值\n\t\t\tsValue = copyOptions.editFieldValue(sFieldName, sValue);\n\n\t\t\t// 目标赋值\n\t\t\tif(null != sValue || false == copyOptions.ignoreNullValue){\n\t\t\t\t//noinspection unchecked\n\t\t\t\ttarget.put(sFieldName, sValue);\n\t\t\t}\n\t\t});\n\t\treturn this.target;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.TypeConverter;\nimport cn.hutool.core.convert.impl.DateConverter;\nimport cn.hutool.core.lang.Editor;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.func.LambdaUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Type;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiFunction;\nimport java.util.function.BiPredicate;\n\n/**\n * 属性拷贝选项<br>\n * 包括：<br>\n * 1、限制的类或接口，必须为目标对象的实现接口或父类，用于限制拷贝的属性，例如一个类我只想复制其父类的一些属性，就可以将editable设置为父类<br>\n * 2、是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null<br>\n * 3、忽略的属性列表，设置一个属性列表，不拷贝这些属性值<br>\n *\n * @author Looly\n */\npublic class CopyOptions implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 限制的类或接口，必须为目标对象的实现接口或父类，用于限制拷贝的属性，例如一个类我只想复制其父类的一些属性，就可以将editable设置为父类<br>\n\t * 如果目标对象是Map，源对象是Bean，则作用于源对象上\n\t */\n\tprotected Class<?> editable;\n\t/**\n\t * 是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null\n\t */\n\tprotected boolean ignoreNullValue;\n\t/**\n\t * 属性过滤器，断言通过的属性才会被复制<br>\n\t * 断言参数中Field为源对象的字段对象,如果源对象为Map，使用目标对象，Object为源对象的对应值\n\t */\n\tprivate BiPredicate<Field, Object> propertiesFilter;\n\t/**\n\t * 是否忽略字段注入错误\n\t */\n\tprotected boolean ignoreError;\n\t/**\n\t * 是否忽略字段大小写\n\t */\n\tprotected boolean ignoreCase;\n\t/**\n\t * 字段属性编辑器，用于自定义属性转换规则，例如驼峰转下划线等<br>\n\t * 规则为，{@link Editor#edit(Object)}属性为源对象的字段名称或key，返回值为目标对象的字段名称或key\n\t */\n\tprivate Editor<String> fieldNameEditor;\n\t/**\n\t * 字段属性值编辑器，用于自定义属性值转换规则，例如null转\"\"等\n\t */\n\tprotected BiFunction<String, Object, Object> fieldValueEditor;\n\t/**\n\t * 是否支持transient关键字修饰和@Transient注解，如果支持，被修饰的字段或方法对应的字段将被忽略。\n\t */\n\tprotected boolean transientSupport = true;\n\t/**\n\t * 是否覆盖目标值，如果不覆盖，会先读取目标对象的值，非{@code null}则写，否则忽略。如果覆盖，则不判断直接写\n\t */\n\tprotected boolean override = true;\n\n\t/**\n\t * 是否自动转换为驼峰方式\n\t */\n\tprotected boolean autoTransCamelCase = true;\n\n\t/**\n\t * 源对象和目标对象都是 {@code Map} 时, 需要忽略的源对象 {@code Map} key\n\t */\n\tprivate Set<String> ignoreKeySet;\n\n\t/**\n\t * 自定义类型转换器，默认使用全局万能转换器转换\n\t */\n\tprotected TypeConverter converter = (type, value) -> {\n\t\tif (null == value) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 快速处理简单值类型的转换\n\t\tif (type instanceof Class){\n\t\t\tClass<?> targetType = (Class<?>) type;\n\t\t\tif (ClassUtil.isSimpleValueType(targetType) && targetType.isInstance(value)) {\n\t\t\t\treturn targetType.cast(value);\n\t\t\t}\n\t\t}\n\n\t\tif (value instanceof IJSONTypeConverter) {\n\t\t\treturn ((IJSONTypeConverter) value).toBean(ObjectUtil.defaultIfNull(type, Object.class));\n\t\t}\n\n\t\treturn Convert.convertWithCheck(type, value, null, ignoreError);\n\t};\n\n\t/**\n\t * 在Bean转换时，如果源是String，目标对象是Date或LocalDateTime，则可自定义转换格式\n\t */\n\tprivate String formatIfDate;\n\n\t//region create\n\n\t/**\n\t * 创建拷贝选项\n\t *\n\t * @return 拷贝选项\n\t */\n\tpublic static CopyOptions create() {\n\t\treturn new CopyOptions();\n\t}\n\n\t/**\n\t * 创建拷贝选项\n\t *\n\t * @param editable         限制的类或接口，必须为目标对象的实现接口或父类，用于限制拷贝的属性\n\t * @param ignoreNullValue  是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null\n\t * @param ignoreProperties 忽略的属性列表，设置一个属性列表，不拷贝这些属性值\n\t * @return 拷贝选项\n\t */\n\tpublic static CopyOptions create(Class<?> editable, boolean ignoreNullValue, String... ignoreProperties) {\n\t\treturn new CopyOptions(editable, ignoreNullValue, ignoreProperties);\n\t}\n\t//endregion\n\n\t/**\n\t * 构造拷贝选项\n\t */\n\tpublic CopyOptions() {\n\t}\n\n\t/**\n\t * 构造拷贝选项\n\t *\n\t * @param editable         限制的类或接口，必须为目标对象的实现接口或父类，用于限制拷贝的属性\n\t * @param ignoreNullValue  是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null\n\t * @param ignoreProperties 忽略的目标对象中属性列表，设置一个属性列表，不拷贝这些属性值\n\t */\n\tpublic CopyOptions(Class<?> editable, boolean ignoreNullValue, String... ignoreProperties) {\n\t\tthis.propertiesFilter = (f, v) -> true;\n\t\tthis.editable = editable;\n\t\tthis.ignoreNullValue = ignoreNullValue;\n\t\tthis.setIgnoreProperties(ignoreProperties);\n\t}\n\n\t/**\n\t * 设置限制的类或接口，必须为目标对象的实现接口或父类，用于限制拷贝的属性\n\t *\n\t * @param editable 限制的类或接口\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setEditable(Class<?> editable) {\n\t\tthis.editable = editable;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null\n\t *\n\t * @param ignoreNullVall 是否忽略空值，当源对象的值为null时，true: 忽略而不注入此值，false: 注入null\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setIgnoreNullValue(boolean ignoreNullVall) {\n\t\tthis.ignoreNullValue = ignoreNullVall;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略空值，当源对象的值为null时，忽略而不注入此值\n\t *\n\t * @return CopyOptions\n\t * @since 4.5.7\n\t */\n\tpublic CopyOptions ignoreNullValue() {\n\t\treturn setIgnoreNullValue(true);\n\t}\n\n\t/**\n\t * 属性过滤器，断言通过的属性才会被复制<br>\n\t * {@link BiPredicate#test(Object, Object)}返回{@code true}则属性通过，{@code false}不通过，抛弃之\n\t *\n\t * @param propertiesFilter 属性过滤器\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setPropertiesFilter(BiPredicate<Field, Object> propertiesFilter) {\n\t\tthis.propertiesFilter = propertiesFilter;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略的目标对象中属性列表，设置一个属性列表，不拷贝这些属性值\n\t *\n\t * @param ignoreProperties 忽略的目标对象中属性列表，设置一个属性列表，不拷贝这些属性值\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setIgnoreProperties(String... ignoreProperties) {\n\t\tthis.ignoreKeySet = CollUtil.newHashSet(ignoreProperties);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略的目标对象中属性列表，设置一个属性列表，不拷贝这些属性值，Lambda方式\n\t *\n\t * @param <P>   参数类型\n\t * @param <R>   返回值类型\n\t * @param funcs 忽略的目标对象中属性列表，设置一个属性列表，不拷贝这些属性值\n\t * @return CopyOptions\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <P, R> CopyOptions setIgnoreProperties(Func1<P, R>... funcs) {\n\t\tthis.ignoreKeySet = ArrayUtil.mapToSet(funcs, LambdaUtil::getFieldName);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否忽略字段的注入错误\n\t *\n\t * @param ignoreError 是否忽略注入错误\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setIgnoreError(boolean ignoreError) {\n\t\tthis.ignoreError = ignoreError;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略字段的注入错误\n\t *\n\t * @return CopyOptions\n\t * @since 4.5.7\n\t */\n\tpublic CopyOptions ignoreError() {\n\t\treturn setIgnoreError(true);\n\t}\n\n\t/**\n\t * 设置是否忽略字段的大小写\n\t *\n\t * @param ignoreCase 是否忽略大小写\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setIgnoreCase(boolean ignoreCase) {\n\t\tthis.ignoreCase = ignoreCase;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略字段的大小写\n\t *\n\t * @return CopyOptions\n\t * @since 4.5.7\n\t */\n\tpublic CopyOptions ignoreCase() {\n\t\treturn setIgnoreCase(true);\n\t}\n\n\t/**\n\t * 设置拷贝属性的字段映射，用于不同的属性之前拷贝做对应表用<br>\n\t * 需要注意的是，当使用ValueProvider作为数据提供者时，这个映射是相反的，即fieldMapping中key为目标Bean的名称，而value是提供者中的key\n\t *\n\t * @param fieldMapping 拷贝属性的字段映射，用于不同的属性之前拷贝做对应表用\n\t * @return CopyOptions\n\t */\n\tpublic CopyOptions setFieldMapping(Map<String, String> fieldMapping) {\n\t\treturn setFieldNameEditor((key -> fieldMapping.getOrDefault(key, key)));\n\t}\n\n\t/**\n\t * 设置字段属性编辑器，用于自定义属性转换规则，例如驼峰转下划线等<br>\n\t * 此转换器只针对源端的字段做转换，请确认转换后与目标端字段一致<br>\n\t * 当转换后的字段名为null时忽略这个字段<br>\n\t * 需要注意的是，当使用ValueProvider作为数据提供者时，这个映射是相反的，即fieldMapping中key为目标Bean的名称，而value是提供者中的key\n\t *\n\t * @param fieldNameEditor 字段属性编辑器，用于自定义属性转换规则，例如驼峰转下划线等\n\t * @return CopyOptions\n\t * @since 5.4.2\n\t */\n\tpublic CopyOptions setFieldNameEditor(Editor<String> fieldNameEditor) {\n\t\tthis.fieldNameEditor = fieldNameEditor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置字段属性值编辑器，用于自定义属性值转换规则，例如null转\"\"等<br>\n\t *\n\t * @param fieldValueEditor 字段属性值编辑器，用于自定义属性值转换规则，例如null转\"\"等\n\t * @return CopyOptions\n\t * @since 5.7.15\n\t */\n\tpublic CopyOptions setFieldValueEditor(BiFunction<String, Object, Object> fieldValueEditor) {\n\t\tthis.fieldValueEditor = fieldValueEditor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 编辑字段值\n\t *\n\t * @param fieldName  字段名\n\t * @param fieldValue 字段值\n\t * @return 编辑后的字段值\n\t * @since 5.7.15\n\t */\n\tprotected Object editFieldValue(String fieldName, Object fieldValue) {\n\t\treturn (null != this.fieldValueEditor) ?\n\t\t\tthis.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue;\n\t}\n\n\t/**\n\t * 设置是否支持transient关键字修饰和@Transient注解，如果支持，被修饰的字段或方法对应的字段将被忽略。\n\t *\n\t * @param transientSupport 是否支持\n\t * @return this\n\t * @since 5.4.2\n\t */\n\tpublic CopyOptions setTransientSupport(boolean transientSupport) {\n\t\tthis.transientSupport = transientSupport;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否覆盖目标值，如果不覆盖，会先读取目标对象的值，为{@code null}则写，否则忽略。如果覆盖，则不判断直接写\n\t *\n\t * @param override 是否覆盖目标值\n\t * @return this\n\t * @since 5.7.17\n\t */\n\tpublic CopyOptions setOverride(boolean override) {\n\t\tthis.override = override;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否自动转换为驼峰方式<br>\n\t * 一般用于map转bean和bean转bean出现非驼峰格式时，在尝试转换失败的情况下，是否二次检查转为驼峰匹配<br>\n\t * 此设置用于解决Bean和Map转换中的匹配问题而设置，并不是一个强制参数。\n\t * <ol>\n\t *     <li>当map转bean时，如果map中是下划线等非驼峰模式，自动匹配对应的驼峰字段，避免出现字段不拷贝问题。</li>\n\t *     <li>当bean转bean时，由于字段命名不规范，使用了非驼峰方式，增加兼容性。</li>\n\t * </ol>\n\t * <p>\n\t * 但是bean转Map和map转map时，没有使用这个参数，是因为没有匹配的必要，转map不存在无法匹配到的问题，因此此参数无效。\n\t *\n\t * @param autoTransCamelCase 是否自动转换为驼峰方式\n\t * @return this\n\t * @since 5.8.25\n\t */\n\tpublic CopyOptions setAutoTransCamelCase(final boolean autoTransCamelCase) {\n\t\tthis.autoTransCamelCase = autoTransCamelCase;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置自定义类型转换器，默认使用全局万能转换器转换。\n\t *\n\t * @param converter 转换器\n\t * @return this\n\t * @since 5.8.0\n\t */\n\tpublic CopyOptions setConverter(TypeConverter converter) {\n\t\tthis.converter = converter;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取日期格式，用于日期转字符串，默认为{@code null}\n\t * @return 日期格式\n\t */\n\tpublic String getFormatIfDate() {\n\t\treturn formatIfDate;\n\t}\n\n\t/**\n\t * 设置日期格式，用于日期转字符串，默认为{@code null}\n\t * @param formatIfDate 日期格式\n\t * @return this\n\t */\n\tpublic CopyOptions setFormatIfDate(String formatIfDate) {\n\t\tthis.formatIfDate = formatIfDate;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 使用自定义转换器转换字段值<br>\n\t * 如果自定义转换器为{@code null}，则返回原值。\n\t *\n\t * @param targetType 目标类型\n\t * @param fieldValue 字段值\n\t * @return 编辑后的字段值\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tprotected Object convertField(Type targetType, Object fieldValue) {\n\t\tif((targetType instanceof Class && Date.class.isAssignableFrom((Class<?>) targetType)) && null != this.formatIfDate){\n\t\t\treturn new DateConverter((Class) targetType, this.formatIfDate).convert(fieldValue, null);\n\t\t}\n\n\t\treturn (null != this.converter) ?\n\t\t\tthis.converter.convert(targetType, fieldValue) : fieldValue;\n\t}\n\n\t/**\n\t * 转换字段名为编辑后的字段名\n\t *\n\t * @param fieldName 字段名\n\t * @return 编辑后的字段名\n\t * @since 5.4.2\n\t */\n\tprotected String editFieldName(String fieldName) {\n\t\treturn (null != this.fieldNameEditor) ? this.fieldNameEditor.edit(fieldName) : fieldName;\n\t}\n\n\t/**\n\t * 测试是否保留字段，{@code true}保留，{@code false}不保留\n\t *\n\t * @param field 字段\n\t * @param value 值\n\t * @return 是否保留\n\t */\n\tprotected boolean testPropertyFilter(Field field, Object value) {\n\t\treturn null == this.propertiesFilter || this.propertiesFilter.test(field, value);\n\t}\n\n\t/**\n\t * 测试是否保留key, {@code true} 不保留， {@code false} 保留\n\t *\n\t * @param key {@link Map} key\n\t * @return 是否保留\n\t */\n\tprotected boolean testKeyFilter(Object key) {\n\t\tif (CollUtil.isEmpty(this.ignoreKeySet)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (ignoreCase) {\n\t\t\t// 忽略大小写时要遍历检查\n\t\t\tfor (final String ignoreKey : this.ignoreKeySet) {\n\t\t\t\tif (StrUtil.equalsIgnoreCase(key.toString(), ignoreKey)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false == this.ignoreKeySet.contains(key);\n\t}\n\n\t/**\n\t * 查找Map对应Bean的名称<br>\n\t * 尝试原名称、转驼峰名称、isXxx去掉is的名称\n\t *\n\t * @param targetPropDescMap 目标bean的属性描述Map\n\t * @param sKeyStr           键或字段名\n\t * @return {@link PropDesc}\n\t */\n\tprotected PropDesc findPropDesc(final Map<String, PropDesc> targetPropDescMap, final String sKeyStr) {\n\t\tPropDesc propDesc = targetPropDescMap.get(sKeyStr);\n\t\t// 转驼峰尝试查找\n\t\tif (null == propDesc && this.autoTransCamelCase) {\n\t\t\tfinal String camelCaseKey = StrUtil.toCamelCase(sKeyStr);\n\t\t\tif (!StrUtil.equals(sKeyStr, camelCaseKey)) {\n\t\t\t\t// 只有转换为驼峰后与原key不同才重复查询，相同说明本身就是驼峰，不需要二次查询\n\t\t\t\tpropDesc = targetPropDescMap.get(camelCaseKey);\n\t\t\t}\n\t\t}\n\t\treturn propDesc;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/IJSONTypeConverter.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport java.lang.reflect.Type;\n\n/**\n * JSON自定义转换扩展接口,因core模块无法直接调用json模块而创建,\n * 使用此接口避免使用反射调用toBean方法而性能太差。\n *\n * @author mkeq\n * @since 5.8.22\n */\npublic interface IJSONTypeConverter {\n\n\t/**\n\t * 转为实体类对象\n\t *\n\t * @param <T>  Bean类型\n\t * @param type {@link Type}\n\t * @return 实体类对象\n\t * @since 3.0.8\n\t */\n\t<T> T toBean(Type type);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapWrapper;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Map属性拷贝到Bean中的拷贝器\n *\n * @param <T> 目标Bean类型\n * @since 5.8.0\n */\npublic class MapToBeanCopier<T> extends AbsCopier<Map<?, ?>, T> {\n\n\t/**\n\t * 目标的类型（用于泛型类注入）\n\t */\n\tprivate final Type targetType;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      来源Map\n\t * @param target      目标Bean对象\n\t * @param targetType  目标泛型类型\n\t * @param copyOptions 拷贝选项\n\t */\n\tpublic MapToBeanCopier(Map<?, ?> source, T target, Type targetType, CopyOptions copyOptions) {\n\t\tsuper(source, target, copyOptions);\n\n\t\t// 针对MapWrapper特殊处理，提供的Map包装了忽略大小写的Map，则默认转Bean的时候也忽略大小写，如JSONObject\n\t\tif(source instanceof MapWrapper){\n\t\t\tfinal Map<?, ?> raw = ((MapWrapper<?, ?>) source).getRaw();\n\t\t\tif(raw instanceof CaseInsensitiveMap){\n\t\t\t\tcopyOptions.setIgnoreCase(true);\n\t\t\t}\n\t\t}\n\n\t\tthis.targetType = targetType;\n\t}\n\n\t@Override\n\tpublic T copy() {\n\t\tClass<?> actualEditable = target.getClass();\n\t\tif (null != copyOptions.editable) {\n\t\t\t// 检查限制类是否为target的父类或接口\n\t\t\tAssert.isTrue(copyOptions.editable.isInstance(target),\n\t\t\t\t\t\"Target class [{}] not assignable to Editable class [{}]\", actualEditable.getName(), copyOptions.editable.getName());\n\t\t\tactualEditable = copyOptions.editable;\n\t\t}\n\t\tfinal Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);\n\n\t\tthis.source.forEach((sKey, sValue) -> {\n\t\t\tif (null == sKey) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tString sKeyStr = copyOptions.editFieldName(sKey.toString());\n\t\t\t// 对key做转换，转换后为null的跳过\n\t\t\tif (null == sKeyStr) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 忽略不需要拷贝的 key,\n\t\t\tif (false == copyOptions.testKeyFilter(sKeyStr)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查目标字段可写性\n\t\t\tfinal PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sKeyStr);\n\t\t\tif (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {\n\t\t\t\t// 字段不可写，跳过之\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsKeyStr = tDesc.getFieldName();\n\n\t\t\t// 检查目标是否过滤属性\n\t\t\tif (false == copyOptions.testPropertyFilter(tDesc.getField(), sValue)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 获取目标字段真实类型并转换源值\n\t\t\tfinal Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType());\n\t\t\t//Object newValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError);\n\t\t\tObject newValue = this.copyOptions.convertField(fieldType, sValue);\n\t\t\t// 自定义值\n\t\t\tnewValue = copyOptions.editFieldValue(sKeyStr, newValue);\n\n\t\t\t// 目标赋值\n\t\t\ttDesc.setValue(this.target, newValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);\n\t\t});\n\t\treturn this.target;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Map属性拷贝到Map中的拷贝器\n *\n * @since 5.8.0\n */\n@SuppressWarnings({\"rawtypes\", \"unchecked\"})\npublic class MapToMapCopier extends AbsCopier<Map, Map> {\n\n\t// 提前获取目标值真实类型\n\tprivate final Type[] targetTypeArguments;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      来源Map\n\t * @param target      目标Bean对象\n\t * @param targetType  目标泛型类型\n\t * @param copyOptions 拷贝选项\n\t */\n\tpublic MapToMapCopier(Map source, Map target, Type targetType, CopyOptions copyOptions) {\n\t\tsuper(source, target, copyOptions);\n\t\ttargetTypeArguments = TypeUtil.getTypeArguments(targetType);\n\t}\n\n\t@Override\n\tpublic Map copy() {\n\t\tthis.source.forEach((sKey, sValue) -> {\n\t\t\tif (null == sKey) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif(sKey instanceof String){\n\t\t\t\tsKey = copyOptions.editFieldName((String) sKey);\n\t\t\t\t// 对key做转换，转换后为null的跳过\n\t\t\t\tif (null == sKey) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 忽略不需要拷贝的 key,\n\t\t\tif (false == copyOptions.testKeyFilter(sKey)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfinal Object targetValue = target.get(sKey);\n\t\t\t// 非覆盖模式下，如果目标值存在，则跳过\n\t\t\tif (false == copyOptions.override && null != targetValue) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 尝试转换源值\n\t\t\tif (null != targetTypeArguments) {\n\t\t\t\t//sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);\n\t\t\t\tsValue = this.copyOptions.convertField(targetTypeArguments[1], sValue);\n\t\t\t}\n\n\t\t\t// 自定义值\n\t\t\tsValue = copyOptions.editFieldValue(sKey.toString(), sValue);\n\n\t\t\t// 忽略空值\n\t\t\tif (true == copyOptions.ignoreNullValue && sValue == null) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 目标赋值\n\t\t\ttarget.put(sKey, sValue);\n\t\t});\n\t\treturn this.target;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport java.lang.reflect.Type;\n\n/**\n * 值提供者，用于提供Bean注入时参数对应值得抽象接口<br>\n * 继承或匿名实例化此接口<br>\n * 在Bean注入过程中，Bean获得字段名，通过外部方式根据这个字段名查找相应的字段值，然后注入Bean<br>\n *\n * @author Looly\n * @param <T> KEY类型，一般情况下为 {@link String}\n *\n */\npublic interface ValueProvider<T>{\n\n\t/**\n\t * 获取值<br>\n\t * 返回值一般需要匹配被注入类型，如果不匹配会调用默认转换 Convert#convert(Type, Object)实现转换\n\t *\n\t * @param key Bean对象中参数名\n\t * @param valueType 被注入的值的类型\n\t * @return 对应参数名的值\n\t */\n\tObject value(T key, Type valueType);\n\n\t/**\n\t * 是否包含指定KEY，如果不包含则忽略注入<br>\n\t * 此接口方法单独需要实现的意义在于：有些值提供者（比如Map）key是存在的，但是value为null，此时如果需要注入这个null，需要根据此方法判断\n\t *\n\t * @param key Bean对象中参数名\n\t * @return 是否包含指定KEY\n\t */\n\tboolean containsKey(T key);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * {@link ValueProvider}属性拷贝到Bean中的拷贝器\n *\n * @param <T> 目标Bean类型\n * @since 5.8.0\n */\npublic class ValueProviderToBeanCopier<T> extends AbsCopier<ValueProvider<String>, T> {\n\n\t/**\n\t * 目标的类型（用于泛型类注入）\n\t */\n\tprivate final Type targetType;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      来源Map\n\t * @param target      目标Bean对象\n\t * @param targetType  目标泛型类型\n\t * @param copyOptions 拷贝选项\n\t */\n\tpublic ValueProviderToBeanCopier(ValueProvider<String> source, T target, Type targetType, CopyOptions copyOptions) {\n\t\tsuper(source, target, copyOptions);\n\t\tthis.targetType = targetType;\n\t}\n\n\t@Override\n\tpublic T copy() {\n\t\tClass<?> actualEditable = target.getClass();\n\t\tif (null != copyOptions.editable) {\n\t\t\t// 检查限制类是否为target的父类或接口\n\t\t\tAssert.isTrue(copyOptions.editable.isInstance(target),\n\t\t\t\t\t\"Target class [{}] not assignable to Editable class [{}]\", actualEditable.getName(), copyOptions.editable.getName());\n\t\t\tactualEditable = copyOptions.editable;\n\t\t}\n\t\tfinal Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);\n\n\t\ttargetPropDescMap.forEach((tFieldName, tDesc) -> {\n\t\t\tif (null == tFieldName) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttFieldName = copyOptions.editFieldName(tFieldName);\n\t\t\t// 对key做转换，转换后为null的跳过\n\t\t\tif (null == tFieldName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 无字段内容跳过\n\t\t\tif(false == source.containsKey(tFieldName)){\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 忽略不需要拷贝的 key,\n\t\t\tif (false == copyOptions.testKeyFilter(tFieldName)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查目标字段可写性\n\t\t\tif (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {\n\t\t\t\t// 字段不可写，跳过之\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 获取目标字段真实类型\n\t\t\tfinal Type fieldType = TypeUtil.getActualType(this.targetType ,tDesc.getFieldType());\n\n\t\t\t// 检查目标对象属性是否过滤属性\n\t\t\tObject sValue = source.value(tFieldName, fieldType);\n\t\t\tif (false == copyOptions.testPropertyFilter(tDesc.getField(), sValue)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 自定义值\n\t\t\tsValue = copyOptions.editFieldValue(tFieldName, sValue);\n\n\t\t\t// 目标赋值\n\t\t\ttDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);\n\t\t});\n\t\treturn this.target;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/package-info.java",
    "content": "/**\n * Bean拷贝实现，包括拷贝选项等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.bean.copier;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java",
    "content": "package cn.hutool.core.bean.copier.provider;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.lang.Editor;\nimport cn.hutool.core.map.FuncKeyMap;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Type;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * Bean的值提供者\n *\n * @author looly\n */\npublic class BeanValueProvider implements ValueProvider<String> {\n\n\tprivate final Object source;\n\tprivate final boolean ignoreError;\n\tfinal Map<String, PropDesc> sourcePdMap;\n\n\t/**\n\t * 构造\n\t *\n\t * @param bean        Bean\n\t * @param ignoreCase  是否忽略字段大小写\n\t * @param ignoreError 是否忽略字段值读取错误\n\t */\n\tpublic BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError) {\n\t\tthis(bean, ignoreCase, ignoreError, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bean        Bean\n\t * @param ignoreCase  是否忽略字段大小写\n\t * @param ignoreError 是否忽略字段值读取错误\n\t * @param keyEditor   键编辑器\n\t */\n\tpublic BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError, Editor<String> keyEditor) {\n\t\tthis.source = bean;\n\t\tthis.ignoreError = ignoreError;\n\t\tfinal Map<String, PropDesc> sourcePdMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(ignoreCase);\n\t\t// issue#2202@Github\n\t\t// 如果用户定义了键编辑器，则提供的map中的数据必须全部转换key\n\t\t// issue#I5VRHW@Gitee 使Function可以被序列化\n\t\tthis.sourcePdMap = new FuncKeyMap<>(new HashMap<>(sourcePdMap.size(), 1), (Function<Object, String> & Serializable)(key) -> {\n\t\t\tif (ignoreCase && key instanceof CharSequence) {\n\t\t\t\tkey = key.toString().toLowerCase();\n\t\t\t}\n\t\t\tif (null != keyEditor) {\n\t\t\t\tkey = keyEditor.edit(key.toString());\n\t\t\t}\n\t\t\treturn key.toString();\n\t\t});\n\t\tthis.sourcePdMap.putAll(sourcePdMap);\n\t}\n\n\t@Override\n\tpublic Object value(String key, Type valueType) {\n\t\tfinal PropDesc sourcePd = getPropDesc(key, valueType);\n\n\t\tObject result = null;\n\t\tif (null != sourcePd) {\n\t\t\tresult = sourcePd.getValue(this.source, valueType, this.ignoreError);\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean containsKey(String key) {\n\t\tfinal PropDesc sourcePd = getPropDesc(key, null);\n\n\t\t// 字段描述不存在或忽略读的情况下，表示不存在\n\t\treturn null != sourcePd && sourcePd.isReadable(false);\n\t}\n\n\t/**\n\t * 获得属性描述\n\t *\n\t * @param key       字段名\n\t * @param valueType 值类型，用于判断是否为Boolean，可以为null\n\t * @return 属性描述\n\t */\n\tprivate PropDesc getPropDesc(String key, Type valueType) {\n\t\tPropDesc sourcePd = sourcePdMap.get(key);\n\t\tif (null == sourcePd && (null == valueType || Boolean.class == valueType || boolean.class == valueType)) {\n\t\t\t//boolean类型字段字段名支持两种方式\n\t\t\tsourcePd = sourcePdMap.get(StrUtil.upperFirstAndAddPre(key, \"is\"));\n\t\t}\n\n\t\treturn sourcePd;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/DynaBeanValueProvider.java",
    "content": "package cn.hutool.core.bean.copier.provider;\n\nimport cn.hutool.core.bean.DynaBean;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.convert.Convert;\n\nimport java.lang.reflect.Type;\n\n/**\n * DynaBean值提供者\n *\n * @author looly\n * @since 5.4.2\n */\npublic class DynaBeanValueProvider implements ValueProvider<String> {\n\n\tprivate final DynaBean dynaBean;\n\tprivate final boolean ignoreError;\n\n\t/**\n\t * 构造\n\t *\n\t * @param dynaBean        DynaBean\n\t * @param ignoreError 是否忽略错误\n\t */\n\tpublic DynaBeanValueProvider(DynaBean dynaBean, boolean ignoreError) {\n\t\tthis.dynaBean = dynaBean;\n\t\tthis.ignoreError = ignoreError;\n\t}\n\n\t@Override\n\tpublic Object value(String key, Type valueType) {\n\t\tfinal Object value = dynaBean.get(key);\n\t\treturn Convert.convertWithCheck(valueType, value, null, this.ignoreError);\n\t}\n\n\t@Override\n\tpublic boolean containsKey(String key) {\n\t\treturn dynaBean.containsProp(key);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java",
    "content": "package cn.hutool.core.bean.copier.provider;\n\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.convert.Convert;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Map值提供者\n *\n * @author Looly\n * @since 5.8.40\n */\n@SuppressWarnings(\"rawtypes\")\npublic class MapValueProvider implements ValueProvider<String> {\n\n\tprivate final Map map;\n\n\t/**\n\t * 构造\n\t *\n\t * @param map map\n\t */\n\tpublic MapValueProvider(final Map map) {\n\t\tthis.map = map;\n\t}\n\n\t@Override\n\tpublic Object value(String key, Type valueType) {\n\t\treturn Convert.convert(valueType, map.get(key));\n\t}\n\n\t@Override\n\tpublic boolean containsKey(String key) {\n\t\treturn map.containsKey(key);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java",
    "content": "/**\n * Bean值提供者方式封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.bean.copier.provider;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/bean/package-info.java",
    "content": "/**\n * Bean相关操作，包括Bean信息描述，Bean路径表达式、动态Bean、Bean工具等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.bean;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/Builder.java",
    "content": "package cn.hutool.core.builder;\n\nimport java.io.Serializable;\n\n/**\n * 建造者模式接口定义\n *\n * @param <T> 建造对象类型\n * @author Looly\n * @since 4.2.2\n */\npublic interface Builder<T> extends Serializable{\n\t/**\n\t * 构建\n\t *\n\t * @return 被构建的对象\n\t */\n\tT build();\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/CompareToBuilder.java",
    "content": "package cn.hutool.core.builder;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.lang.reflect.AccessibleObject;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.util.Collection;\nimport java.util.Comparator;\n\n/**\n * 用于构建 {@link java.lang.Comparable#compareTo(Object)} 方法的辅助工具\n *\n * <p>\n * 在Bean对象中，所有相关字段都参与比对，继承的字段不参与。使用方法如下：\n *\n * <pre>\n * public class MyClass {\n *   String field1;\n *   int field2;\n *   boolean field3;\n *\n *   ...\n *\n *   public int compareTo(Object o) {\n *     MyClass myClass = (MyClass) o;\n *     return new CompareToBuilder()\n *       .appendSuper(super.compareTo(o)\n *       .append(this.field1, myClass.field1)\n *       .append(this.field2, myClass.field2)\n *       .append(this.field3, myClass.field3)\n *       .toComparison();\n *   }\n * }\n * </pre>\n *\n * 字段值按照顺序比较，如果某个字段返回非0结果，比较终止，使用{@code toComparison()}返回结果，后续比较忽略。\n *\n * <p>\n * 也可以使用{@link #reflectionCompare(Object, Object) reflectionCompare} 方法通过反射比较字段，使用方法如下：\n *\n * <pre>\n * public int compareTo(Object o) {\n *   return CompareToBuilder.reflectionCompare(this, o);\n * }\n * </pre>\n *\n *TODO 待整理\n * 来自于Apache-Commons-Lang3\n * @author looly，Apache-Commons\n * @since 4.2.2\n */\npublic class CompareToBuilder implements Builder<Integer> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 当前比较状态 */\n    private int comparison;\n\n    /**\n     * 构造，构造后调用append方法增加比较项，然后调用{@link #toComparison()}获取结果\n     */\n    public CompareToBuilder() {\n        comparison = 0;\n    }\n\n    //-----------------------------------------------------------------------\n    /**\n     * 通过反射比较两个Bean对象，对象字段可以为private。比较规则如下：\n     *\n     * <ul>\n     * <li>static字段不比较</li>\n     * <li>Transient字段不参与比较</li>\n     * <li>父类字段参与比较</li>\n     * </ul>\n     *\n     *<p>\n     *如果被比较的两个对象都为<code>null</code>，被认为相同。\n     *\n     * @param lhs  第一个对象\n     * @param rhs  第二个对象\n     * @return a negative integer, zero, or a positive integer as <code>lhs</code>\n     *  is less than, equal to, or greater than <code>rhs</code>\n     * @throws NullPointerException  if either (but not both) parameters are\n     *  <code>null</code>\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     */\n    public static int reflectionCompare(final Object lhs, final Object rhs) {\n        return reflectionCompare(lhs, rhs, false, null);\n    }\n\n    /**\n     * <p>Compares two <code>Object</code>s via reflection.</p>\n     *\n     * <p>Fields can be private, thus <code>AccessibleObject.setAccessible</code>\n     * is used to bypass normal access control checks. This will fail under a\n     * security manager unless the appropriate permissions are set.</p>\n     *\n     * <ul>\n     * <li>Static fields will not be compared</li>\n     * <li>If <code>compareTransients</code> is <code>true</code>,\n     *     compares transient members.  Otherwise ignores them, as they\n     *     are likely derived fields.</li>\n     * <li>Superclass fields will be compared</li>\n     * </ul>\n     *\n     * <p>If both <code>lhs</code> and <code>rhs</code> are <code>null</code>,\n     * they are considered equal.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param compareTransients  whether to compare transient fields\n     * @return a negative integer, zero, or a positive integer as <code>lhs</code>\n     *  is less than, equal to, or greater than <code>rhs</code>\n     * @throws NullPointerException  if either <code>lhs</code> or <code>rhs</code>\n     *  (but not both) is <code>null</code>\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     */\n    public static int reflectionCompare(final Object lhs, final Object rhs, final boolean compareTransients) {\n        return reflectionCompare(lhs, rhs, compareTransients, null);\n    }\n\n    /**\n     * <p>Compares two <code>Object</code>s via reflection.</p>\n     *\n     * <p>Fields can be private, thus <code>AccessibleObject.setAccessible</code>\n     * is used to bypass normal access control checks. This will fail under a\n     * security manager unless the appropriate permissions are set.</p>\n     *\n     * <ul>\n     * <li>Static fields will not be compared</li>\n     * <li>If <code>compareTransients</code> is <code>true</code>,\n     *     compares transient members.  Otherwise ignores them, as they\n     *     are likely derived fields.</li>\n     * <li>Superclass fields will be compared</li>\n     * </ul>\n     *\n     * <p>If both <code>lhs</code> and <code>rhs</code> are <code>null</code>,\n     * they are considered equal.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param excludeFields  Collection of String fields to exclude\n     * @return a negative integer, zero, or a positive integer as <code>lhs</code>\n     *  is less than, equal to, or greater than <code>rhs</code>\n     * @throws NullPointerException  if either <code>lhs</code> or <code>rhs</code>\n     *  (but not both) is <code>null</code>\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     * @since 2.2\n     */\n    public static int reflectionCompare(final Object lhs, final Object rhs, final Collection<String> excludeFields) {\n        return reflectionCompare(lhs, rhs, ArrayUtil.toArray(excludeFields, String.class));\n    }\n\n    /**\n     * <p>Compares two <code>Object</code>s via reflection.</p>\n     *\n     * <p>Fields can be private, thus <code>AccessibleObject.setAccessible</code>\n     * is used to bypass normal access control checks. This will fail under a\n     * security manager unless the appropriate permissions are set.</p>\n     *\n     * <ul>\n     * <li>Static fields will not be compared</li>\n     * <li>If <code>compareTransients</code> is <code>true</code>,\n     *     compares transient members.  Otherwise ignores them, as they\n     *     are likely derived fields.</li>\n     * <li>Superclass fields will be compared</li>\n     * </ul>\n     *\n     * <p>If both <code>lhs</code> and <code>rhs</code> are <code>null</code>,\n     * they are considered equal.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param excludeFields  array of fields to exclude\n     * @return a negative integer, zero, or a positive integer as <code>lhs</code>\n     *  is less than, equal to, or greater than <code>rhs</code>\n     * @throws NullPointerException  if either <code>lhs</code> or <code>rhs</code>\n     *  (but not both) is <code>null</code>\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     * @since 2.2\n     */\n    public static int reflectionCompare(final Object lhs, final Object rhs, final String... excludeFields) {\n        return reflectionCompare(lhs, rhs, false, null, excludeFields);\n    }\n\n    /**\n     * <p>Compares two <code>Object</code>s via reflection.</p>\n     *\n     * <p>Fields can be private, thus <code>AccessibleObject.setAccessible</code>\n     * is used to bypass normal access control checks. This will fail under a\n     * security manager unless the appropriate permissions are set.</p>\n     *\n     * <ul>\n     * <li>Static fields will not be compared</li>\n     * <li>If the <code>compareTransients</code> is <code>true</code>,\n     *     compares transient members.  Otherwise ignores them, as they\n     *     are likely derived fields.</li>\n     * <li>Compares superclass fields up to and including <code>reflectUpToClass</code>.\n     *     If <code>reflectUpToClass</code> is <code>null</code>, compares all superclass fields.</li>\n     * </ul>\n     *\n     * <p>If both <code>lhs</code> and <code>rhs</code> are <code>null</code>,\n     * they are considered equal.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param compareTransients  whether to compare transient fields\n     * @param reflectUpToClass  last superclass for which fields are compared\n     * @param excludeFields  fields to exclude\n     * @return a negative integer, zero, or a positive integer as <code>lhs</code>\n     *  is less than, equal to, or greater than <code>rhs</code>\n     * @throws NullPointerException  if either <code>lhs</code> or <code>rhs</code>\n     *  (but not both) is <code>null</code>\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     * @since 2.2 (2.0 as <code>reflectionCompare(Object, Object, boolean, Class)</code>)\n     */\n    public static int reflectionCompare(\n        final Object lhs,\n        final Object rhs,\n        final boolean compareTransients,\n        final Class<?> reflectUpToClass,\n        final String... excludeFields) {\n\n        if (lhs == rhs) {\n            return 0;\n        }\n        if (lhs == null || rhs == null) {\n            throw new NullPointerException();\n        }\n        Class<?> lhsClazz = lhs.getClass();\n        if (!lhsClazz.isInstance(rhs)) {\n            throw new ClassCastException();\n        }\n        final CompareToBuilder compareToBuilder = new CompareToBuilder();\n        reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields);\n        while (lhsClazz.getSuperclass() != null && lhsClazz != reflectUpToClass) {\n            lhsClazz = lhsClazz.getSuperclass();\n            reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields);\n        }\n        return compareToBuilder.toComparison();\n    }\n\n    /**\n     * <p>Appends to <code>builder</code> the comparison of <code>lhs</code>\n     * to <code>rhs</code> using the fields defined in <code>clazz</code>.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param clazz  <code>Class</code> that defines fields to be compared\n     * @param builder  <code>CompareToBuilder</code> to append to\n     * @param useTransients  whether to compare transient fields\n     * @param excludeFields  fields to exclude\n     */\n    private static void reflectionAppend(\n        final Object lhs,\n        final Object rhs,\n        final Class<?> clazz,\n        final CompareToBuilder builder,\n        final boolean useTransients,\n        final String[] excludeFields) {\n\n        final Field[] fields = clazz.getDeclaredFields();\n        AccessibleObject.setAccessible(fields, true);\n        for (int i = 0; i < fields.length && builder.comparison == 0; i++) {\n            final Field f = fields[i];\n            if (false == ArrayUtil.contains(excludeFields, f.getName())\n                && (f.getName().indexOf('$') == -1)\n                && (useTransients || !Modifier.isTransient(f.getModifiers()))\n                && (!Modifier.isStatic(f.getModifiers()))) {\n                try {\n                    builder.append(f.get(lhs), f.get(rhs));\n                } catch (final IllegalAccessException e) {\n                    // This can't happen. Would get a Security exception instead.\n                    // Throw a runtime exception in case the impossible happens.\n                    throw new InternalError(\"Unexpected IllegalAccessException\");\n                }\n            }\n        }\n    }\n\n    //-----------------------------------------------------------------------\n    /**\n     * <p>Appends to the <code>builder</code> the <code>compareTo(Object)</code>\n     * result of the superclass.</p>\n     *\n     * @param superCompareTo  result of calling <code>super.compareTo(Object)</code>\n     * @return this - used to chain append calls\n     * @since 2.0\n     */\n    public CompareToBuilder appendSuper(final int superCompareTo) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = superCompareTo;\n        return this;\n    }\n\n    //-----------------------------------------------------------------------\n    /**\n     * <p>Appends to the <code>builder</code> the comparison of\n     * two <code>Object</code>s.</p>\n     *\n     * <ol>\n     * <li>Check if <code>lhs == rhs</code></li>\n     * <li>Check if either <code>lhs</code> or <code>rhs</code> is <code>null</code>,\n     *     a <code>null</code> object is less than a non-<code>null</code> object</li>\n     * <li>Check the object contents</li>\n     * </ol>\n     *\n     * <p><code>lhs</code> must either be an array or implement {@link Comparable}.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @return this - used to chain append calls\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     */\n    public CompareToBuilder append(final Object lhs, final Object rhs) {\n        return append(lhs, rhs, null);\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the comparison of\n     * two <code>Object</code>s.</p>\n     *\n     * <ol>\n     * <li>Check if <code>lhs == rhs</code></li>\n     * <li>Check if either <code>lhs</code> or <code>rhs</code> is <code>null</code>,\n     *     a <code>null</code> object is less than a non-<code>null</code> object</li>\n     * <li>Check the object contents</li>\n     * </ol>\n     *\n     * <p>If <code>lhs</code> is an array, array comparison methods will be used.\n     * Otherwise <code>comparator</code> will be used to compare the objects.\n     * If <code>comparator</code> is <code>null</code>, <code>lhs</code> must\n     * implement {@link Comparable} instead.</p>\n     *\n     * @param lhs  left-hand object\n     * @param rhs  right-hand object\n     * @param comparator  <code>Comparator</code> used to compare the objects,\n     *  <code>null</code> means treat lhs as <code>Comparable</code>\n     * @return this - used to chain append calls\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     * @since 2.0\n     */\n    public CompareToBuilder append(final Object lhs, final Object rhs, final Comparator<?> comparator) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.getClass().isArray()) {\n            // switch on type of array, to dispatch to the correct handler\n            // handles multi dimensional arrays\n            // throws a ClassCastException if rhs is not the correct array type\n            if (lhs instanceof long[]) {\n                append((long[]) lhs, (long[]) rhs);\n            } else if (lhs instanceof int[]) {\n                append((int[]) lhs, (int[]) rhs);\n            } else if (lhs instanceof short[]) {\n                append((short[]) lhs, (short[]) rhs);\n            } else if (lhs instanceof char[]) {\n                append((char[]) lhs, (char[]) rhs);\n            } else if (lhs instanceof byte[]) {\n                append((byte[]) lhs, (byte[]) rhs);\n            } else if (lhs instanceof double[]) {\n                append((double[]) lhs, (double[]) rhs);\n            } else if (lhs instanceof float[]) {\n                append((float[]) lhs, (float[]) rhs);\n            } else if (lhs instanceof boolean[]) {\n                append((boolean[]) lhs, (boolean[]) rhs);\n            } else {\n                // not an array of primitives\n                // throws a ClassCastException if rhs is not an array\n                append((Object[]) lhs, (Object[]) rhs, comparator);\n            }\n        } else {\n            // the simple case, not an array, just test the element\n            if (comparator == null) {\n                @SuppressWarnings(\"unchecked\") // assume this can be done; if not throw CCE as per Javadoc\n                final Comparable<Object> comparable = (Comparable<Object>) lhs;\n                comparison = comparable.compareTo(rhs);\n            } else {\n                @SuppressWarnings(\"unchecked\") // assume this can be done; if not throw CCE as per Javadoc\n                final Comparator<Object> comparator2 = (Comparator<Object>) comparator;\n                comparison = comparator2.compare(lhs, rhs);\n            }\n        }\n        return this;\n    }\n\n    //-------------------------------------------------------------------------\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>long</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final long lhs, final long rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = (Long.compare(lhs, rhs));\n        return this;\n    }\n\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>int</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final int lhs, final int rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = (Integer.compare(lhs, rhs));\n        return this;\n    }\n\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>short</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final short lhs, final short rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = (Short.compare(lhs, rhs));\n        return this;\n    }\n\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>char</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final char lhs, final char rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = (Character.compare(lhs, rhs));\n        return this;\n    }\n\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>byte</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final byte lhs, final byte rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = (Byte.compare(lhs, rhs));\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the comparison of\n     * two <code>double</code>s.</p>\n     *\n     * <p>This handles NaNs, Infinities, and <code>-0.0</code>.</p>\n     *\n     * <p>It is compatible with the hash code generated by\n     * <code>HashCodeBuilder</code>.</p>\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final double lhs, final double rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = Double.compare(lhs, rhs);\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the comparison of\n     * two <code>float</code>s.</p>\n     *\n     * <p>This handles NaNs, Infinities, and <code>-0.0</code>.</p>\n     *\n     * <p>It is compatible with the hash code generated by\n     * <code>HashCodeBuilder</code>.</p>\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final float lhs, final float rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        comparison = Float.compare(lhs, rhs);\n        return this;\n    }\n\n    /**\n     * Appends to the <code>builder</code> the comparison of\n     * two <code>booleans</code>s.\n     *\n     * @param lhs  left-hand value\n     * @param rhs  right-hand value\n     * @return this - used to chain append calls\n      */\n    public CompareToBuilder append(final boolean lhs, final boolean rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == false) {\n            comparison = -1;\n        } else {\n            comparison = +1;\n        }\n        return this;\n    }\n\n    //-----------------------------------------------------------------------\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>Object</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a short length array is less than a long length array</li>\n     *  <li>Check array contents element by element using {@link #append(Object, Object, Comparator)}</li>\n     * </ol>\n     *\n     * <p>This method will also will be called for the top level of multi-dimensional,\n     * ragged, and multi-typed arrays.</p>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     */\n    public CompareToBuilder append(final Object[] lhs, final Object[] rhs) {\n        return append(lhs, rhs, null);\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>Object</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a short length array is less than a long length array</li>\n     *  <li>Check array contents element by element using {@link #append(Object, Object, Comparator)}</li>\n     * </ol>\n     *\n     * <p>This method will also will be called for the top level of multi-dimensional,\n     * ragged, and multi-typed arrays.</p>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @param comparator  <code>Comparator</code> to use to compare the array elements,\n     *  <code>null</code> means to treat <code>lhs</code> elements as <code>Comparable</code>.\n     * @return this - used to chain append calls\n     * @throws ClassCastException  if <code>rhs</code> is not assignment-compatible\n     *  with <code>lhs</code>\n     * @since 2.0\n     */\n    public CompareToBuilder append(final Object[] lhs, final Object[] rhs, final Comparator<?> comparator) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i], comparator);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>long</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(long, long)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final long[] lhs, final long[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>int</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(int, int)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final int[] lhs, final int[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>short</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(short, short)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final short[] lhs, final short[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>char</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(char, char)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final char[] lhs, final char[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>byte</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(byte, byte)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final byte[] lhs, final byte[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>double</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(double, double)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final double[] lhs, final double[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>float</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(float, float)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final float[] lhs, final float[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    /**\n     * <p>Appends to the <code>builder</code> the deep comparison of\n     * two <code>boolean</code> arrays.</p>\n     *\n     * <ol>\n     *  <li>Check if arrays are the same using <code>==</code></li>\n     *  <li>Check if for <code>null</code>, <code>null</code> is less than non-<code>null</code></li>\n     *  <li>Check array length, a shorter length array is less than a longer length array</li>\n     *  <li>Check array contents element by element using {@link #append(boolean, boolean)}</li>\n     * </ol>\n     *\n     * @param lhs  left-hand array\n     * @param rhs  right-hand array\n     * @return this - used to chain append calls\n     */\n    public CompareToBuilder append(final boolean[] lhs, final boolean[] rhs) {\n        if (comparison != 0) {\n            return this;\n        }\n        if (lhs == rhs) {\n            return this;\n        }\n        if (lhs == null) {\n            comparison = -1;\n            return this;\n        }\n        if (rhs == null) {\n            comparison = +1;\n            return this;\n        }\n        if (lhs.length != rhs.length) {\n            comparison = (lhs.length < rhs.length) ? -1 : +1;\n            return this;\n        }\n        for (int i = 0; i < lhs.length && comparison == 0; i++) {\n            append(lhs[i], rhs[i]);\n        }\n        return this;\n    }\n\n    //-----------------------------------------------------------------------\n    /**\n     * Returns a negative integer, a positive integer, or zero as\n     * the <code>builder</code> has judged the \"left-hand\" side\n     * as less than, greater than, or equal to the \"right-hand\"\n     * side.\n     *\n     * @return final comparison result\n     * @see #build()\n     */\n    public int toComparison() {\n        return comparison;\n    }\n\n    /**\n     * Returns a negative Integer, a positive Integer, or zero as\n     * the <code>builder</code> has judged the \"left-hand\" side\n     * as less than, greater than, or equal to the \"right-hand\"\n     * side.\n     *\n     * @return final comparison result as an Integer\n     * @see #toComparison()\n     * @since 3.0\n     */\n    @Override\n    public Integer build() {\n        return toComparison();\n    }\n}\n\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java",
    "content": "package cn.hutool.core.builder;\n\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.lang.reflect.AccessibleObject;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * <p>{@link Object#equals(Object)} 方法的构建器</p>\n *\n * <p>两个对象equals必须保证hashCode值相等，hashCode值相等不能保证一定equals</p>\n *\n * <p>使用方法如下：</p>\n * <pre>\n * public boolean equals(Object obj) {\n *   if (obj == null) { return false; }\n *   if (obj == this) { return true; }\n *   if (obj.getClass() != getClass()) {\n *     return false;\n *   }\n *   MyClass rhs = (MyClass) obj;\n *   return new EqualsBuilder()\n *                 .appendSuper(super.equals(obj))\n *                 .append(field1, rhs.field1)\n *                 .append(field2, rhs.field2)\n *                 .append(field3, rhs.field3)\n *                 .isEquals();\n *  }\n * </pre>\n *\n * <p> 我们也可以通过反射判断所有字段是否equals：</p>\n * <pre>\n * public boolean equals(Object obj) {\n *   return EqualsBuilder.reflectionEquals(this, obj);\n * }\n * </pre>\n * <p>\n * 来自Apache Commons Lang改造\n */\npublic class EqualsBuilder implements Builder<Boolean> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * <p>\n\t * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops.\n\t * </p>\n\t */\n\tprivate static final ThreadLocal<Set<Pair<IDKey, IDKey>>> REGISTRY = new ThreadLocal<>();\n\n\t/**\n\t * <p>\n\t * Returns the registry of object pairs being traversed by the reflection\n\t * methods in the current thread.\n\t * </p>\n\t *\n\t * @return Set the registry of objects being traversed\n\t * @since 3.0\n\t */\n\tstatic Set<Pair<IDKey, IDKey>> getRegistry() {\n\t\treturn REGISTRY.get();\n\t}\n\n\t/**\n\t * <p>\n\t * Converters value pair into a register pair.\n\t * </p>\n\t *\n\t * @param lhs {@code this} object\n\t * @param rhs the other object\n\t * @return the pair\n\t */\n\tstatic Pair<IDKey, IDKey> getRegisterPair(final Object lhs, final Object rhs) {\n\t\tfinal IDKey left = new IDKey(lhs);\n\t\tfinal IDKey right = new IDKey(rhs);\n\t\treturn new Pair<>(left, right);\n\t}\n\n\t/**\n\t * <p>\n\t * Returns {@code true} if the registry contains the given object pair.\n\t * Used by the reflection methods to avoid infinite loops.\n\t * Objects might be swapped therefore a check is needed if the object pair\n\t * is registered in given or swapped order.\n\t * </p>\n\t *\n\t * @param lhs {@code this} object to lookup in registry\n\t * @param rhs the other object to lookup on registry\n\t * @return boolean {@code true} if the registry contains the given object.\n\t * @since 3.0\n\t */\n\tstatic boolean isRegistered(final Object lhs, final Object rhs) {\n\t\tfinal Set<Pair<IDKey, IDKey>> registry = getRegistry();\n\t\tfinal Pair<IDKey, IDKey> pair = getRegisterPair(lhs, rhs);\n\t\tfinal Pair<IDKey, IDKey> swappedPair = new Pair<>(pair.getKey(), pair.getValue());\n\n\t\treturn registry != null\n\t\t\t\t&& (registry.contains(pair) || registry.contains(swappedPair));\n\t}\n\n\t/**\n\t * <p>\n\t * Registers the given object pair.\n\t * Used by the reflection methods to avoid infinite loops.\n\t * </p>\n\t *\n\t * @param lhs {@code this} object to register\n\t * @param rhs the other object to register\n\t */\n\tstatic void register(final Object lhs, final Object rhs) {\n\t\tsynchronized (EqualsBuilder.class) {\n\t\t\tif (getRegistry() == null) {\n\t\t\t\tREGISTRY.set(new HashSet<>());\n\t\t\t}\n\t\t}\n\n\t\tfinal Set<Pair<IDKey, IDKey>> registry = getRegistry();\n\t\tfinal Pair<IDKey, IDKey> pair = getRegisterPair(lhs, rhs);\n\t\tregistry.add(pair);\n\t}\n\n\t/**\n\t * <p>\n\t * Unregisters the given object pair.\n\t * </p>\n\t *\n\t * <p>\n\t * Used by the reflection methods to avoid infinite loops.\n\t *\n\t * @param lhs {@code this} object to unregister\n\t * @param rhs the other object to unregister\n\t * @since 3.0\n\t */\n\tstatic void unregister(final Object lhs, final Object rhs) {\n\t\tSet<Pair<IDKey, IDKey>> registry = getRegistry();\n\t\tif (registry != null) {\n\t\t\tfinal Pair<IDKey, IDKey> pair = getRegisterPair(lhs, rhs);\n\t\t\tregistry.remove(pair);\n\t\t\tsynchronized (EqualsBuilder.class) {\n\t\t\t\t//read again\n\t\t\t\tregistry = getRegistry();\n\t\t\t\tif (registry != null && registry.isEmpty()) {\n\t\t\t\t\tREGISTRY.remove();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 是否equals，此值随着构建会变更，默认true\n\t */\n\tprivate boolean isEquals = true;\n\n\t/**\n\t * 构造，初始状态值为true\n\t */\n\tpublic EqualsBuilder() {\n\t\t// do nothing for now.\n\t}\n\n\t//-------------------------------------------------------------------------\n\n\t/**\n\t * <p>反射检查两个对象是否equals，此方法检查对象及其父对象的属性（包括私有属性）是否equals</p>\n\t *\n\t * @param lhs           此对象\n\t * @param rhs           另一个对象\n\t * @param excludeFields 排除的字段集合，如果有不参与计算equals的字段加入此集合即可\n\t * @return 两个对象是否equals，是返回{@code true}\n\t */\n\tpublic static boolean reflectionEquals(final Object lhs, final Object rhs, final Collection<String> excludeFields) {\n\t\treturn reflectionEquals(lhs, rhs, ArrayUtil.toArray(excludeFields, String.class));\n\t}\n\n\t/**\n\t * <p>反射检查两个对象是否equals，此方法检查对象及其父对象的属性（包括私有属性）是否equals</p>\n\t *\n\t * @param lhs           此对象\n\t * @param rhs           另一个对象\n\t * @param excludeFields 排除的字段集合，如果有不参与计算equals的字段加入此集合即可\n\t * @return 两个对象是否equals，是返回{@code true}\n\t */\n\tpublic static boolean reflectionEquals(final Object lhs, final Object rhs, final String... excludeFields) {\n\t\treturn reflectionEquals(lhs, rhs, false, null, excludeFields);\n\t}\n\n\t/**\n\t * <p>This method uses reflection to determine if the two {@code Object}s\n\t * are equal.</p>\n\t *\n\t * <p>It uses {@code AccessibleObject.setAccessible} to gain access to private\n\t * fields. This means that it will throw a security exception if run under\n\t * a security manager, if the permissions are not set up correctly. It is also\n\t * not as efficient as testing explicitly. Non-primitive fields are compared using\n\t * {@code equals()}.</p>\n\t *\n\t * <p>If the TestTransients parameter is set to {@code true}, transient\n\t * members will be tested, otherwise they are ignored, as they are likely\n\t * derived fields, and not part of the value of the {@code Object}.</p>\n\t *\n\t * <p>Static fields will not be tested. Superclass fields will be included.</p>\n\t *\n\t * @param lhs            {@code this} object\n\t * @param rhs            the other object\n\t * @param testTransients whether to include transient fields\n\t * @return {@code true} if the two Objects have tested equals.\n\t */\n\tpublic static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients) {\n\t\treturn reflectionEquals(lhs, rhs, testTransients, null);\n\t}\n\n\t/**\n\t * <p>This method uses reflection to determine if the two {@code Object}s\n\t * are equal.</p>\n\t *\n\t * <p>It uses {@code AccessibleObject.setAccessible} to gain access to private\n\t * fields. This means that it will throw a security exception if run under\n\t * a security manager, if the permissions are not set up correctly. It is also\n\t * not as efficient as testing explicitly. Non-primitive fields are compared using\n\t * {@code equals()}.</p>\n\t *\n\t * <p>If the testTransients parameter is set to {@code true}, transient\n\t * members will be tested, otherwise they are ignored, as they are likely\n\t * derived fields, and not part of the value of the {@code Object}.</p>\n\t *\n\t * <p>Static fields will not be included. Superclass fields will be appended\n\t * up to and including the specified superclass. A null superclass is treated\n\t * as java.lang.Object.</p>\n\t *\n\t * @param lhs              {@code this} object\n\t * @param rhs              the other object\n\t * @param testTransients   whether to include transient fields\n\t * @param reflectUpToClass the superclass to reflect up to (inclusive),\n\t *                         may be {@code null}\n\t * @param excludeFields    array of field names to exclude from testing\n\t * @return {@code true} if the two Objects have tested equals.\n\t * @since 2.0\n\t */\n\tpublic static boolean reflectionEquals(final Object lhs, final Object rhs, final boolean testTransients, final Class<?> reflectUpToClass,\n\t\t\t\t\t\t\t\t\t\t   final String... excludeFields) {\n\t\tif (lhs == rhs) {\n\t\t\treturn true;\n\t\t}\n\t\tif (lhs == null || rhs == null) {\n\t\t\treturn false;\n\t\t}\n\t\t// Find the leaf class since there may be transients in the leaf\n\t\t// class or in classes between the leaf and root.\n\t\t// If we are not testing transients or a subclass has no ivars,\n\t\t// then a subclass can test equals to a superclass.\n\t\tfinal Class<?> lhsClass = lhs.getClass();\n\t\tfinal Class<?> rhsClass = rhs.getClass();\n\t\tClass<?> testClass;\n\t\tif (lhsClass.isInstance(rhs)) {\n\t\t\ttestClass = lhsClass;\n\t\t\tif (!rhsClass.isInstance(lhs)) {\n\t\t\t\t// rhsClass is a subclass of lhsClass\n\t\t\t\ttestClass = rhsClass;\n\t\t\t}\n\t\t} else if (rhsClass.isInstance(lhs)) {\n\t\t\ttestClass = rhsClass;\n\t\t\tif (!lhsClass.isInstance(rhs)) {\n\t\t\t\t// lhsClass is a subclass of rhsClass\n\t\t\t\ttestClass = lhsClass;\n\t\t\t}\n\t\t} else {\n\t\t\t// The two classes are not related.\n\t\t\treturn false;\n\t\t}\n\t\tfinal EqualsBuilder equalsBuilder = new EqualsBuilder();\n\t\ttry {\n\t\t\tif (testClass.isArray()) {\n\t\t\t\tequalsBuilder.append(lhs, rhs);\n\t\t\t} else {\n\t\t\t\treflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields);\n\t\t\t\twhile (testClass.getSuperclass() != null && testClass != reflectUpToClass) {\n\t\t\t\t\ttestClass = testClass.getSuperclass();\n\t\t\t\t\treflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final IllegalArgumentException e) {\n\t\t\t// In this case, we tried to test a subclass vs. a superclass and\n\t\t\t// the subclass has ivars or the ivars are transient and\n\t\t\t// we are testing transients.\n\t\t\t// If a subclass has ivars that we are trying to test them, we get an\n\t\t\t// exception and we know that the objects are not equal.\n\t\t\treturn false;\n\t\t}\n\t\treturn equalsBuilder.isEquals();\n\t}\n\n\t/**\n\t * <p>Appends the fields and values defined by the given object of the\n\t * given Class.</p>\n\t *\n\t * @param lhs           the left hand object\n\t * @param rhs           the right hand object\n\t * @param clazz         the class to append details of\n\t * @param builder       the builder to append to\n\t * @param useTransients whether to test transient fields\n\t * @param excludeFields array of field names to exclude from testing\n\t */\n\tprivate static void reflectionAppend(\n\t\t\tfinal Object lhs,\n\t\t\tfinal Object rhs,\n\t\t\tfinal Class<?> clazz,\n\t\t\tfinal EqualsBuilder builder,\n\t\t\tfinal boolean useTransients,\n\t\t\tfinal String[] excludeFields) {\n\n\t\tif (isRegistered(lhs, rhs)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tregister(lhs, rhs);\n\t\t\tfinal Field[] fields = clazz.getDeclaredFields();\n\t\t\tAccessibleObject.setAccessible(fields, true);\n\t\t\tfor (int i = 0; i < fields.length && builder.isEquals; i++) {\n\t\t\t\tfinal Field f = fields[i];\n\t\t\t\tif (false == ArrayUtil.contains(excludeFields, f.getName())\n\t\t\t\t\t\t&& (f.getName().indexOf('$') == -1)\n\t\t\t\t\t\t&& (useTransients || !Modifier.isTransient(f.getModifiers()))\n\t\t\t\t\t\t&& (!Modifier.isStatic(f.getModifiers()))) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tbuilder.append(f.get(lhs), f.get(rhs));\n\t\t\t\t\t} catch (final IllegalAccessException e) {\n\t\t\t\t\t\t//this can't happen. Would get a Security exception instead\n\t\t\t\t\t\t//throw a runtime exception in case the impossible happens.\n\t\t\t\t\t\tthrow new InternalError(\"Unexpected IllegalAccessException\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tunregister(lhs, rhs);\n\t\t}\n\t}\n\n\t//-------------------------------------------------------------------------\n\n\t/**\n\t * <p>Adds the result of {@code super.equals()} to this builder.</p>\n\t *\n\t * @param superEquals the result of calling {@code super.equals()}\n\t * @return EqualsBuilder - used to chain calls.\n\t * @since 2.0\n\t */\n\tpublic EqualsBuilder appendSuper(final boolean superEquals) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = superEquals;\n\t\treturn this;\n\t}\n\n\t//-------------------------------------------------------------------------\n\n\t/**\n\t * <p>Test if two {@code Object}s are equal using their\n\t * {@code equals} method.</p>\n\t *\n\t * @param lhs the left hand object\n\t * @param rhs the right hand object\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final Object lhs, final Object rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tif (lhs == rhs) {\n\t\t\treturn this;\n\t\t}\n\t\tif (lhs == null || rhs == null) {\n\t\t\treturn setEquals(false);\n\t\t}\n\t\tif (ArrayUtil.isArray(lhs)) {\n\t\t\t// 判断数组的equals\n\t\t\treturn setEquals(ArrayUtil.equals(lhs, rhs));\n\t\t}\n\n\t\t// The simple case, not an array, just test the element\n\t\treturn setEquals(lhs.equals(rhs));\n\t}\n\n\t/**\n\t * <p>\n\t * Test if two {@code long} s are equal.\n\t * </p>\n\t *\n\t * @param lhs the left hand {@code long}\n\t * @param rhs the right hand {@code long}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final long lhs, final long rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Test if two {@code int}s are equal.</p>\n\t *\n\t * @param lhs the left hand {@code int}\n\t * @param rhs the right hand {@code int}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final int lhs, final int rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Test if two {@code short}s are equal.</p>\n\t *\n\t * @param lhs the left hand {@code short}\n\t * @param rhs the right hand {@code short}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final short lhs, final short rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Test if two {@code char}s are equal.</p>\n\t *\n\t * @param lhs the left hand {@code char}\n\t * @param rhs the right hand {@code char}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final char lhs, final char rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Test if two {@code byte}s are equal.</p>\n\t *\n\t * @param lhs the left hand {@code byte}\n\t * @param rhs the right hand {@code byte}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final byte lhs, final byte rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Test if two {@code double}s are equal by testing that the\n\t * pattern of bits returned by {@code doubleToLong} are equal.</p>\n\t *\n\t * <p>This handles NaNs, Infinities, and {@code -0.0}.</p>\n\t *\n\t * <p>It is compatible with the hash code generated by\n\t * {@code HashCodeBuilder}.</p>\n\t *\n\t * @param lhs the left hand {@code double}\n\t * @param rhs the right hand {@code double}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final double lhs, final double rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\treturn append(Double.doubleToLongBits(lhs), Double.doubleToLongBits(rhs));\n\t}\n\n\t/**\n\t * <p>Test if two {@code float}s are equal byt testing that the\n\t * pattern of bits returned by doubleToLong are equal.</p>\n\t *\n\t * <p>This handles NaNs, Infinities, and {@code -0.0}.</p>\n\t *\n\t * <p>It is compatible with the hash code generated by\n\t * {@code HashCodeBuilder}.</p>\n\t *\n\t * @param lhs the left hand {@code float}\n\t * @param rhs the right hand {@code float}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final float lhs, final float rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\treturn append(Float.floatToIntBits(lhs), Float.floatToIntBits(rhs));\n\t}\n\n\t/**\n\t * <p>Test if two {@code booleans}s are equal.</p>\n\t *\n\t * @param lhs the left hand {@code boolean}\n\t * @param rhs the right hand {@code boolean}\n\t * @return EqualsBuilder - used to chain calls.\n\t */\n\tpublic EqualsBuilder append(final boolean lhs, final boolean rhs) {\n\t\tif (isEquals == false) {\n\t\t\treturn this;\n\t\t}\n\t\tisEquals = (lhs == rhs);\n\t\treturn this;\n\t}\n\n\t/**\n\t * <p>Returns {@code true} if the fields that have been checked\n\t * are all equal.</p>\n\t *\n\t * @return boolean\n\t */\n\tpublic boolean isEquals() {\n\t\treturn this.isEquals;\n\t}\n\n\t/**\n\t * <p>Returns {@code true} if the fields that have been checked\n\t * are all equal.</p>\n\t *\n\t * @return {@code true} if all of the fields that have been checked\n\t * are equal, {@code false} otherwise.\n\t * @since 3.0\n\t */\n\t@Override\n\tpublic Boolean build() {\n\t\treturn isEquals();\n\t}\n\n\t/**\n\t * Sets the {@code isEquals} value.\n\t *\n\t * @param isEquals The value to set.\n\t * @return this\n\t */\n\tprotected EqualsBuilder setEquals(boolean isEquals) {\n\t\tthis.isEquals = isEquals;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Reset the EqualsBuilder so you can use the same object again\n\t *\n\t * @since 2.5\n\t */\n\tpublic void reset() {\n\t\tthis.isEquals = true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/GenericBuilder.java",
    "content": "package cn.hutool.core.builder;\n\nimport cn.hutool.core.lang.func.Consumer3;\nimport cn.hutool.core.lang.func.Supplier1;\nimport cn.hutool.core.lang.func.Supplier2;\nimport cn.hutool.core.lang.func.Supplier3;\nimport cn.hutool.core.lang.func.Supplier4;\nimport cn.hutool.core.lang.func.Supplier5;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * <p>通用Builder</p>\n * 参考: <a href=\"https://blog.csdn.net/weixin_43935907/article/details/105003719\">一看就会的java8通用Builder</a>\n * <p>使用方法如下：</p>\n * <pre>\n * Box box = GenericBuilder\n * \t\t.of(Box::new)\n * \t\t.with(Box::setId, 1024L)\n * \t\t.with(Box::setTitle, \"Hello World!\")\n * \t\t.with(Box::setLength, 9)\n * \t\t.with(Box::setWidth, 8)\n * \t\t.with(Box::setHeight, 7)\n * \t\t.build();\n *\n * </pre>\n *\n * <p> 我们也可以对已创建的对象进行修改：</p>\n * <pre>\n * Box boxModified = GenericBuilder\n * \t\t.of(() -&gt; box)\n * \t\t.with(Box::setTitle, \"Hello Friend!\")\n * \t\t.with(Box::setLength, 3)\n * \t\t.with(Box::setWidth, 4)\n * \t\t.with(Box::setHeight, 5)\n * \t\t.build();\n * </pre>\n * <p> 我们还可以对这样调用有参构造，这对于创建一些在有参构造中包含初始化函数的对象是有意义的：</p>\n * <pre>\n * Box box1 = GenericBuilder\n * \t\t.of(Box::new, 2048L, \"Hello Partner!\", 222, 333, 444)\n * \t\t.with(Box::alis)\n * \t\t.build();\n * </pre>\n * <p> 还可能这样构建Map对象：</p>\n * {@code\n * HashMap<String, String> colorMap = GenericBuilder\n * \t\t.of(HashMap<String,String>::new)\n * \t\t.with(Map::put, \"red\", \"#FF0000\")\n * \t\t.with(Map::put, \"yellow\", \"#FFFF00\")\n * \t\t.with(Map::put, \"blue\", \"#0000FF\")\n * \t\t.build();\n * }\n *\n * <p>注意：本工具类支持调用的构造方法的参数数量不超过5个，一般方法的参数数量不超过2个，更多的参数不利于阅读和维护。</p>\n *\n * @author TomXin\n * @since 5.7.21\n */\npublic class GenericBuilder<T> implements Builder<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 实例化器\n\t */\n\tprivate final Supplier<T> instant;\n\n\t/**\n\t * 修改器列表\n\t */\n\tprivate final List<Consumer<T>> modifiers = new ArrayList<>();\n\n\t/**\n\t * 构造\n\t *\n\t * @param instant 实例化器\n\t */\n\tpublic GenericBuilder(Supplier<T> instant) {\n\t\tthis.instant = instant;\n\t}\n\n\t/**\n\t * 通过无参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param <T>     目标类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T> GenericBuilder<T> of(Supplier<T> instant) {\n\t\treturn new GenericBuilder<>(instant);\n\t}\n\n\t/**\n\t * 通过1参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param p1      参数一\n\t * @param <T>     目标类型\n\t * @param <P1>    参数一类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T, P1> GenericBuilder<T> of(Supplier1<T, P1> instant, P1 p1) {\n\t\treturn of(instant.toSupplier(p1));\n\t}\n\n\t/**\n\t * 通过2参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param p1      参数一\n\t * @param p2      参数二\n\t * @param <T>     目标类型\n\t * @param <P1>    参数一类型\n\t * @param <P2>    参数二类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T, P1, P2> GenericBuilder<T> of(Supplier2<T, P1, P2> instant, P1 p1, P2 p2) {\n\t\treturn of(instant.toSupplier(p1, p2));\n\t}\n\n\t/**\n\t * 通过3参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param p1      参数一\n\t * @param p2      参数二\n\t * @param p3      参数三\n\t * @param <T>     目标类型\n\t * @param <P1>    参数一类型\n\t * @param <P2>    参数二类型\n\t * @param <P3>    参数三类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T, P1, P2, P3> GenericBuilder<T> of(Supplier3<T, P1, P2, P3> instant, P1 p1, P2 p2, P3 p3) {\n\t\treturn of(instant.toSupplier(p1, p2, p3));\n\t}\n\n\t/**\n\t * 通过4参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param p1      参数一\n\t * @param p2      参数二\n\t * @param p3      参数三\n\t * @param p4      参数四\n\t * @param <T>     目标类型\n\t * @param <P1>    参数一类型\n\t * @param <P2>    参数二类型\n\t * @param <P3>    参数三类型\n\t * @param <P4>    参数四类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T, P1, P2, P3, P4> GenericBuilder<T> of(Supplier4<T, P1, P2, P3, P4> instant, P1 p1, P2 p2, P3 p3, P4 p4) {\n\t\treturn of(instant.toSupplier(p1, p2, p3, p4));\n\t}\n\n\t/**\n\t * 通过5参数实例化器创建GenericBuilder\n\t *\n\t * @param instant 实例化器\n\t * @param p1      参数一\n\t * @param p2      参数二\n\t * @param p3      参数三\n\t * @param p4      参数四\n\t * @param p5      参数五\n\t * @param <T>     目标类型\n\t * @param <P1>    参数一类型\n\t * @param <P2>    参数二类型\n\t * @param <P3>    参数三类型\n\t * @param <P4>    参数四类型\n\t * @param <P5>    参数五类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic static <T, P1, P2, P3, P4, P5> GenericBuilder<T> of(Supplier5<T, P1, P2, P3, P4, P5> instant, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) {\n\t\treturn of(instant.toSupplier(p1, p2, p3, p4, p5));\n\t}\n\n\n\t/**\n\t * 调用无参数方法\n\t *\n\t * @param consumer 无参数Consumer\n\t * @return GenericBuilder对象\n\t */\n\tpublic GenericBuilder<T> with(Consumer<T> consumer) {\n\t\tmodifiers.add(consumer);\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 调用1参数方法\n\t *\n\t * @param consumer 1参数Consumer\n\t * @param p1       参数一\n\t * @param <P1>     参数一类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic <P1> GenericBuilder<T> with(BiConsumer<T, P1> consumer, P1 p1) {\n\t\tmodifiers.add(instant -> consumer.accept(instant, p1));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 调用2参数方法\n\t *\n\t * @param consumer 2参数Consumer\n\t * @param p1       参数一\n\t * @param p2       参数二\n\t * @param <P1>     参数一类型\n\t * @param <P2>     参数二类型\n\t * @return GenericBuilder对象\n\t */\n\tpublic <P1, P2> GenericBuilder<T> with(Consumer3<T, P1, P2> consumer, P1 p1, P2 p2) {\n\t\tmodifiers.add(instant -> consumer.accept(instant, p1, p2));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建\n\t *\n\t * @return 目标对象\n\t */\n\t@Override\n\tpublic T build() {\n\t\tT value = instant.get();\n\t\tmodifiers.forEach(modifier -> modifier.accept(value));\n\t\tmodifiers.clear();\n\t\treturn value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java",
    "content": "package cn.hutool.core.builder;\n\nimport java.lang.reflect.AccessibleObject;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\n\n/**\n * <p>\n * Assists in implementing {@link Object#hashCode()} methods.\n * </p>\n *\n * <p>\n * This class enables a good <code>hashCode</code> method to be built for any class. It follows the rules laid out in\n * the book <a href=\"http://www.oracle.com/technetwork/java/effectivejava-136174.html\">Effective Java</a> by Joshua Bloch. Writing a\n * good <code>hashCode</code> method is actually quite difficult. This class aims to simplify the process.\n * </p>\n *\n * <p>\n * The following is the approach taken. When appending a data field, the current total is multiplied by the\n * multiplier then a relevant value\n * for that data type is added. For example, if the current hashCode is 17, and the multiplier is 37, then\n * appending the integer 45 will create a hashcode of 674, namely 17 * 37 + 45.\n * </p>\n *\n * <p>\n * All relevant fields from the object should be included in the <code>hashCode</code> method. Derived fields may be\n * excluded. In general, any field used in the <code>equals</code> method must be used in the <code>hashCode</code>\n * method.\n * </p>\n *\n * <p>\n * To use this class write code as follows:\n * </p>\n *\n * <pre>\n * public class Person {\n *   String name;\n *   int age;\n *   boolean smoker;\n *   ...\n *\n *   public int hashCode() {\n *     // you pick a hard-coded, randomly chosen, non-zero, odd number\n *     // ideally different for each class\n *     return new HashCodeBuilder(17, 37).\n *       append(name).\n *       append(age).\n *       append(smoker).\n *       toHashCode();\n *   }\n * }\n * </pre>\n *\n * <p>\n * If required, the superclass <code>hashCode()</code> can be added using {@link #appendSuper}.\n * </p>\n *\n * <p>\n * Alternatively, there is a method that uses reflection to determine the fields to test. Because these fields are\n * usually private, the method, <code>reflectionHashCode</code>, uses <code>AccessibleObject.setAccessible</code>\n * to change the visibility of the fields. This will fail under a security manager, unless the appropriate permissions\n * are set up correctly. It is also slower than testing explicitly.\n * </p>\n *\n * <p>\n * A typical invocation for this method would look like:\n * </p>\n *\n * <pre>\n * public int hashCode() {\n *   return HashCodeBuilder.reflectionHashCode(this);\n * }\n * </pre>\n *\n * TODO 待整理\n * 来自于Apache-Commons-Lang3\n * @author looly，Apache-Commons\n * @since 4.2.2\n */\npublic class HashCodeBuilder implements Builder<Integer> {\n\tprivate static final long serialVersionUID = 1L;\n\n    /**\n     * The default initial value to use in reflection hash code building.\n     */\n    private static final int DEFAULT_INITIAL_VALUE = 17;\n\n    /**\n     * The default multipler value to use in reflection hash code building.\n     */\n    private static final int DEFAULT_MULTIPLIER_VALUE = 37;\n\n    /**\n     * <p>\n     * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops.\n     * </p>\n     *\n     * @since 2.3\n     */\n    private static final ThreadLocal<Set<IDKey>> REGISTRY = new ThreadLocal<>();\n\n    /*\n     * NOTE: we cannot store the actual objects in a HashSet, as that would use the very hashCode()\n     * we are in the process of calculating.\n     *\n     * So we generate a one-to-one mapping from the original object to a new object.\n     *\n     * Now HashSet uses equals() to determine if two elements with the same hashcode really\n     * are equal, so we also need to ensure that the replacement objects are only equal\n     * if the original objects are identical.\n     *\n     * The original implementation (2.4 and before) used the System.indentityHashCode()\n     * method - however this is not guaranteed to generate unique ids (e.g. LANG-459)\n     *\n     * We now use the IDKey helper class (adapted from org.apache.axis.utils.IDKey)\n     * to disambiguate the duplicate ids.\n     */\n\n    /**\n     * <p>\n     * Returns the registry of objects being traversed by the reflection methods in the current thread.\n     * </p>\n     *\n     * @return Set the registry of objects being traversed\n     * @since 2.3\n     */\n    private static Set<IDKey> getRegistry() {\n        return REGISTRY.get();\n    }\n\n    /**\n     * <p>\n     * Returns <code>true</code> if the registry contains the given object. Used by the reflection methods to avoid\n     * infinite loops.\n     * </p>\n     *\n     * @param value\n     *            The object to lookup in the registry.\n     * @return boolean <code>true</code> if the registry contains the given object.\n     * @since 2.3\n     */\n    private static boolean isRegistered(final Object value) {\n        final Set<IDKey> registry = getRegistry();\n        return registry != null && registry.contains(new IDKey(value));\n    }\n\n    /**\n     * <p>\n     * Appends the fields and values defined by the given object of the given <code>Class</code>.\n     * </p>\n     *\n     * @param object\n     *            the object to append details of\n     * @param clazz\n     *            the class to append details of\n     * @param builder\n     *            the builder to append to\n     * @param useTransients\n     *            whether to use transient fields\n     * @param excludeFields\n     *            Collection of String field names to exclude from use in calculation of hash code\n     */\n    private static void reflectionAppend(final Object object, final Class<?> clazz, final HashCodeBuilder builder, final boolean useTransients,\n            final String[] excludeFields) {\n        if (isRegistered(object)) {\n            return;\n        }\n        try {\n            register(object);\n            final Field[] fields = clazz.getDeclaredFields();\n            AccessibleObject.setAccessible(fields, true);\n            for (final Field field : fields) {\n                if (false == ArrayUtil.contains(excludeFields, field.getName())\n                    && (field.getName().indexOf('$') == -1)\n                    && (useTransients || !Modifier.isTransient(field.getModifiers()))\n                    && (!Modifier.isStatic(field.getModifiers()))) {\n                    try {\n                        final Object fieldValue = field.get(object);\n                        builder.append(fieldValue);\n                    } catch (final IllegalAccessException e) {\n                        // this can't happen. Would get a Security exception instead\n                        // throw a runtime exception in case the impossible happens.\n                        throw new InternalError(\"Unexpected IllegalAccessException\");\n                    }\n                }\n            }\n        } finally {\n            unregister(object);\n        }\n    }\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <p>\n     * Transient members will be not be used, as they are likely derived fields, and not part of the value of the\n     * <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be tested. Superclass fields will be included.\n     * </p>\n     *\n     * <p>\n     * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class,\n     * however this is not vital. Prime numbers are preferred, especially for the multiplier.\n     * </p>\n     *\n     * @param initialNonZeroOddNumber\n     *            a non-zero, odd number used as the initial value. This will be the returned\n     *            value if no fields are found to include in the hash code\n     * @param multiplierNonZeroOddNumber\n     *            a non-zero, odd number used as the multiplier\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the Object is <code>null</code>\n     * @throws IllegalArgumentException\n     *             if the number is zero or even\n     */\n    public static int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final Object object) {\n        return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, false, null);\n    }\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <p>\n     * If the TestTransients parameter is set to <code>true</code>, transient members will be tested, otherwise they\n     * are ignored, as they are likely derived fields, and not part of the value of the <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be tested. Superclass fields will be included.\n     * </p>\n     *\n     * <p>\n     * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class,\n     * however this is not vital. Prime numbers are preferred, especially for the multiplier.\n     * </p>\n     *\n     * @param initialNonZeroOddNumber\n     *            a non-zero, odd number used as the initial value. This will be the returned\n     *            value if no fields are found to include in the hash code\n     * @param multiplierNonZeroOddNumber\n     *            a non-zero, odd number used as the multiplier\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @param testTransients\n     *            whether to include transient fields\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the Object is <code>null</code>\n     * @throws IllegalArgumentException\n     *             if the number is zero or even\n     */\n    public static int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final Object object,\n            final boolean testTransients) {\n        return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, testTransients, null);\n    }\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <p>\n     * If the TestTransients parameter is set to <code>true</code>, transient members will be tested, otherwise they\n     * are ignored, as they are likely derived fields, and not part of the value of the <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be included. Superclass fields will be included up to and including the specified\n     * superclass. A null superclass is treated as java.lang.Object.\n     * </p>\n     *\n     * <p>\n     * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class,\n     * however this is not vital. Prime numbers are preferred, especially for the multiplier.\n     * </p>\n     *\n     * @param <T>\n     *            the type of the object involved\n     * @param initialNonZeroOddNumber\n     *            a non-zero, odd number used as the initial value. This will be the returned\n     *            value if no fields are found to include in the hash code\n     * @param multiplierNonZeroOddNumber\n     *            a non-zero, odd number used as the multiplier\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @param testTransients\n     *            whether to include transient fields\n     * @param reflectUpToClass\n     *            the superclass to reflect up to (inclusive), may be <code>null</code>\n     * @param excludeFields\n     *            array of field names to exclude from use in calculation of hash code\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the Object is <code>null</code>\n     * @throws IllegalArgumentException\n     *             if the number is zero or even\n     * @since 2.0\n     */\n    public static <T> int reflectionHashCode(final int initialNonZeroOddNumber, final int multiplierNonZeroOddNumber, final T object,\n            final boolean testTransients, final Class<? super T> reflectUpToClass, final String... excludeFields) {\n\n        if (object == null) {\n            throw new IllegalArgumentException(\"The object to build a hash code for must not be null\");\n        }\n        final HashCodeBuilder builder = new HashCodeBuilder(initialNonZeroOddNumber, multiplierNonZeroOddNumber);\n        Class<?> clazz = object.getClass();\n        reflectionAppend(object, clazz, builder, testTransients, excludeFields);\n        while (clazz.getSuperclass() != null && clazz != reflectUpToClass) {\n            clazz = clazz.getSuperclass();\n            reflectionAppend(object, clazz, builder, testTransients, excludeFields);\n        }\n        return builder.toHashCode();\n    }\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * This constructor uses two hard coded choices for the constants needed to build a hash code.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <P>\n     * If the TestTransients parameter is set to <code>true</code>, transient members will be tested, otherwise they\n     * are ignored, as they are likely derived fields, and not part of the value of the <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be tested. Superclass fields will be included. If no fields are found to include\n     * in the hash code, the result of this method will be constant.\n     * </p>\n     *\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @param testTransients\n     *            whether to include transient fields\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the object is <code>null</code>\n     */\n    public static int reflectionHashCode(final Object object, final boolean testTransients) {\n        return reflectionHashCode(DEFAULT_INITIAL_VALUE, DEFAULT_MULTIPLIER_VALUE, object,\n                testTransients, null);\n    }\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * This constructor uses two hard coded choices for the constants needed to build a hash code.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <p>\n     * Transient members will be not be used, as they are likely derived fields, and not part of the value of the\n     * <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be tested. Superclass fields will be included. If no fields are found to include\n     * in the hash code, the result of this method will be constant.\n     * </p>\n     *\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @param excludeFields\n     *            Collection of String field names to exclude from use in calculation of hash code\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the object is <code>null</code>\n     */\n    public static int reflectionHashCode(final Object object, final Collection<String> excludeFields) {\n        return reflectionHashCode(object, ArrayUtil.toArray(excludeFields, String.class));\n    }\n\n    // -------------------------------------------------------------------------\n\n    /**\n     * <p>\n     * Uses reflection to build a valid hash code from the fields of {@code object}.\n     * </p>\n     *\n     * <p>\n     * This constructor uses two hard coded choices for the constants needed to build a hash code.\n     * </p>\n     *\n     * <p>\n     * It uses <code>AccessibleObject.setAccessible</code> to gain access to private fields. This means that it will\n     * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is\n     * also not as efficient as testing explicitly.\n     * </p>\n     *\n     * <p>\n     * Transient members will be not be used, as they are likely derived fields, and not part of the value of the\n     * <code>Object</code>.\n     * </p>\n     *\n     * <p>\n     * Static fields will not be tested. Superclass fields will be included. If no fields are found to include\n     * in the hash code, the result of this method will be constant.\n     * </p>\n     *\n     * @param object\n     *            the Object to create a <code>hashCode</code> for\n     * @param excludeFields\n     *            array of field names to exclude from use in calculation of hash code\n     * @return int hash code\n     * @throws IllegalArgumentException\n     *             if the object is <code>null</code>\n     */\n    public static int reflectionHashCode(final Object object, final String... excludeFields) {\n        return reflectionHashCode(DEFAULT_INITIAL_VALUE, DEFAULT_MULTIPLIER_VALUE, object, false,\n                null, excludeFields);\n    }\n\n    /**\n     * <p>\n     * Registers the given object. Used by the reflection methods to avoid infinite loops.\n     * </p>\n     *\n     * @param value\n     *            The object to register.\n     */\n    static void register(final Object value) {\n        synchronized (HashCodeBuilder.class) {\n            if (getRegistry() == null) {\n                REGISTRY.set(new HashSet<IDKey>());\n            }\n        }\n        getRegistry().add(new IDKey(value));\n    }\n\n    /**\n     * <p>\n     * Unregisters the given object.\n     * </p>\n     *\n     * <p>\n     * Used by the reflection methods to avoid infinite loops.\n     *\n     * @param value\n     *            The object to unregister.\n     * @since 2.3\n     */\n    static void unregister(final Object value) {\n        Set<IDKey> registry = getRegistry();\n        if (registry != null) {\n            registry.remove(new IDKey(value));\n            synchronized (HashCodeBuilder.class) {\n                //read again\n                registry = getRegistry();\n                if (registry != null && registry.isEmpty()) {\n                    REGISTRY.remove();\n                }\n            }\n        }\n    }\n\n    /**\n     * Constant to use in building the hashCode.\n     */\n    private final int iConstant;\n\n    /**\n     * Running total of the hashCode.\n     */\n    private int iTotal;\n\n    /**\n     * <p>\n     * Uses two hard coded choices for the constants needed to build a <code>hashCode</code>.\n     * </p>\n     */\n    public HashCodeBuilder() {\n        iConstant = 37;\n        iTotal = 17;\n    }\n\n    /**\n     * <p>\n     * Two randomly chosen, odd numbers must be passed in. Ideally these should be different for each class,\n     * however this is not vital.\n     * </p>\n     *\n     * <p>\n     * Prime numbers are preferred, especially for the multiplier.\n     * </p>\n     *\n     * @param initialOddNumber\n     *            an odd number used as the initial value\n     * @param multiplierOddNumber\n     *            an odd number used as the multiplier\n     * @throws IllegalArgumentException\n     *             if the number is even\n     */\n    public HashCodeBuilder(final int initialOddNumber, final int multiplierOddNumber) {\n        Assert.isTrue(initialOddNumber % 2 != 0, \"HashCodeBuilder requires an odd initial value\");\n        Assert.isTrue(multiplierOddNumber % 2 != 0, \"HashCodeBuilder requires an odd multiplier\");\n        iConstant = multiplierOddNumber;\n        iTotal = initialOddNumber;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>boolean</code>.\n     * </p>\n     * <p>\n     * This adds <code>1</code> when true, and <code>0</code> when false to the <code>hashCode</code>.\n     * </p>\n     * <p>\n     * This is in contrast to the standard <code>java.lang.Boolean.hashCode</code> handling, which computes\n     * a <code>hashCode</code> value of <code>1231</code> for <code>java.lang.Boolean</code> instances\n     * that represent <code>true</code> or <code>1237</code> for <code>java.lang.Boolean</code> instances\n     * that represent <code>false</code>.\n     * </p>\n     * <p>\n     * This is in accordance with the <i>Effective Java</i> design.\n     * </p>\n     *\n     * @param value\n     *            the boolean to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final boolean value) {\n        iTotal = iTotal * iConstant + (value ? 0 : 1);\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>boolean</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final boolean[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final boolean element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    // -------------------------------------------------------------------------\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>byte</code>.\n     * </p>\n     *\n     * @param value\n     *            the byte to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final byte value) {\n        iTotal = iTotal * iConstant + value;\n        return this;\n    }\n\n    // -------------------------------------------------------------------------\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>byte</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final byte[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final byte element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>char</code>.\n     * </p>\n     *\n     * @param value\n     *            the char to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final char value) {\n        iTotal = iTotal * iConstant + value;\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>char</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final char[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final char element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>double</code>.\n     * </p>\n     *\n     * @param value\n     *            the double to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final double value) {\n        return append(Double.doubleToLongBits(value));\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>double</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final double[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final double element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>float</code>.\n     * </p>\n     *\n     * @param value\n     *            the float to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final float value) {\n        iTotal = iTotal * iConstant + Float.floatToIntBits(value);\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>float</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final float[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final float element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for an <code>int</code>.\n     * </p>\n     *\n     * @param value\n     *            the int to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final int value) {\n        iTotal = iTotal * iConstant + value;\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for an <code>int</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final int[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final int element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>long</code>.\n     * </p>\n     *\n     * @param value\n     *            the long to add to the <code>hashCode</code>\n     * @return this\n     */\n    // NOTE: This method uses >> and not >>> as Effective Java and\n    //       Long.hashCode do. Ideally we should switch to >>> at\n    //       some stage. There are backwards compat issues, so\n    //       that will have to wait for the time being. cf LANG-342.\n    public HashCodeBuilder append(final long value) {\n        iTotal = iTotal * iConstant + ((int) (value ^ (value >> 32)));\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>long</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final long[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final long element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for an <code>Object</code>.\n     * </p>\n     *\n     * @param object\n     *            the Object to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final Object object) {\n        if (object == null) {\n            iTotal = iTotal * iConstant;\n\n        } else {\n            if(object.getClass().isArray()) {\n                // 'Switch' on type of array, to dispatch to the correct handler\n                // This handles multi dimensional arrays\n                if (object instanceof long[]) {\n                    append((long[]) object);\n                } else if (object instanceof int[]) {\n                    append((int[]) object);\n                } else if (object instanceof short[]) {\n                    append((short[]) object);\n                } else if (object instanceof char[]) {\n                    append((char[]) object);\n                } else if (object instanceof byte[]) {\n                    append((byte[]) object);\n                } else if (object instanceof double[]) {\n                    append((double[]) object);\n                } else if (object instanceof float[]) {\n                    append((float[]) object);\n                } else if (object instanceof boolean[]) {\n                    append((boolean[]) object);\n                } else {\n                    // Not an array of primitives\n                    append((Object[]) object);\n                }\n            } else {\n                iTotal = iTotal * iConstant + object.hashCode();\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for an <code>Object</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final Object[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final Object element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>short</code>.\n     * </p>\n     *\n     * @param value\n     *            the short to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final short value) {\n        iTotal = iTotal * iConstant + value;\n        return this;\n    }\n\n    /**\n     * <p>\n     * Append a <code>hashCode</code> for a <code>short</code> array.\n     * </p>\n     *\n     * @param array\n     *            the array to add to the <code>hashCode</code>\n     * @return this\n     */\n    public HashCodeBuilder append(final short[] array) {\n        if (array == null) {\n            iTotal = iTotal * iConstant;\n        } else {\n            for (final short element : array) {\n                append(element);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * <p>\n     * Adds the result of super.hashCode() to this builder.\n     * </p>\n     *\n     * @param superHashCode\n     *            the result of calling <code>super.hashCode()</code>\n     * @return this HashCodeBuilder, used to chain calls.\n     * @since 2.0\n     */\n    public HashCodeBuilder appendSuper(final int superHashCode) {\n        iTotal = iTotal * iConstant + superHashCode;\n        return this;\n    }\n\n    /**\n     * <p>\n     * Return the computed <code>hashCode</code>.\n     * </p>\n     *\n     * @return <code>hashCode</code> based on the fields appended\n     */\n    public int toHashCode() {\n        return iTotal;\n    }\n\n    /**\n     * Returns the computed <code>hashCode</code>.\n     *\n     * @return <code>hashCode</code> based on the fields appended\n     *\n     * @since 3.0\n     */\n    @Override\n    public Integer build() {\n        return toHashCode();\n    }\n\n    /**\n     * <p>\n     * The computed <code>hashCode</code> from toHashCode() is returned due to the likelihood\n     * of bugs in mis-calling toHashCode() and the unlikeliness of it mattering what the hashCode for\n     * HashCodeBuilder itself is.</p>\n     *\n     * @return <code>hashCode</code> based on the fields appended\n     * @since 2.5\n     */\n    @Override\n    public int hashCode() {\n        return toHashCode();\n    }\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/IDKey.java",
    "content": "package cn.hutool.core.builder;\n\nimport java.io.Serializable;\n\n/**\n * 包装唯一键（System.identityHashCode()）使对象只有和自己 equals\n *\n * 此对象用于消除小概率下System.identityHashCode()产生的ID重复问题。\n *\n * 来自于Apache-Commons-Lang3\n * @author looly，Apache-Commons\n * @since 4.2.2\n */\nfinal class IDKey implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Object value;\n\tprivate final int id;\n\n\t/**\n\t * 构造\n\t *\n\t * @param obj 计算唯一ID的对象\n\t */\n\tpublic IDKey(final Object obj) {\n\t\tid = System.identityHashCode(obj);\n\t\t// There have been some cases (LANG-459) that return the\n\t\t// same identity hash code for different objects. So\n\t\t// the value is also added to disambiguate these cases.\n\t\tvalue = obj;\n\t}\n\n\t/**\n\t * returns hashcode - i.e. the system identity hashcode.\n\t *\n\t * @return the hashcode\n\t */\n\t@Override\n\tpublic int hashCode() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * checks if instances are equal\n\t *\n\t * @param other The other object to compare to\n\t * @return if the instances are for the same object\n\t */\n\t@Override\n\tpublic boolean equals(final Object other) {\n\t\tif (!(other instanceof IDKey)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal IDKey idKey = (IDKey) other;\n\t\tif (id != idKey.id) {\n\t\t\treturn false;\n\t\t}\n\t\t// Note that identity equals is used.\n\t\treturn value == idKey.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/builder/package-info.java",
    "content": "/**\n * 建造者工具<br>\n * 用于建造特定对象或结果\n *\n * @author looly\n *\n */\npackage cn.hutool.core.builder;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java",
    "content": "package cn.hutool.core.clone;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 克隆异常\n * @author xiaoleilu\n */\npublic class CloneRuntimeException extends RuntimeException{\n\tprivate static final long serialVersionUID = 6774837422188798989L;\n\n\tpublic CloneRuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic CloneRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic CloneRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic CloneRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic CloneRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/clone/CloneSupport.java",
    "content": "package cn.hutool.core.clone;\n\n/**\n * 克隆支持类，提供默认的克隆方法\n * @author Looly\n *\n * @param <T> 继承类的类型\n */\npublic class CloneSupport<T> implements Cloneable<T>{\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic T clone() {\n\t\ttry {\n\t\t\treturn (T) super.clone();\n\t\t} catch (CloneNotSupportedException e) {\n\t\t\tthrow new CloneRuntimeException(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/clone/Cloneable.java",
    "content": "package cn.hutool.core.clone;\n\n/**\n * 克隆支持接口\n * @author Looly\n *\n * @param <T> 实现克隆接口的类型\n */\npublic interface Cloneable<T> extends java.lang.Cloneable{\n\n\t/**\n\t * 克隆当前对象，浅复制\n\t * @return 克隆后的对象\n\t */\n\tT clone();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/clone/DefaultCloneable.java",
    "content": "package cn.hutool.core.clone;\n\n\nimport cn.hutool.core.util.ReflectUtil;\n\n/**\n * 克隆默认实现接口，用于实现返回指定泛型类型的克隆方法\n *\n * @param <T> 泛型类型\n * @since 5.7.17\n */\npublic interface DefaultCloneable<T> extends java.lang.Cloneable {\n\n\t/**\n\t * 浅拷贝，提供默认的泛型返回值的clone方法。\n\t *\n\t * @return obj\n\t */\n\tdefault T clone0() {\n\t\ttry {\n\t\t\treturn ReflectUtil.invoke(this, \"clone\");\n\t\t} catch (Exception e) {\n\t\t\tthrow new CloneRuntimeException(e);\n\t\t}\n\t}\n}\n\n\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/clone/package-info.java",
    "content": "/**\n * 克隆封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.clone;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/BCD.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * BCD码（Binary-Coded Decimal）亦称二进码十进数或二-十进制代码<br>\n * BCD码这种编码形式利用了四个位元来储存一个十进制的数码，使二进制和十进制之间的转换得以快捷的进行<br>\n * see http://cuisuqiang.iteye.com/blog/1429956\n * @author Looly\n *\n * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n */\n@Deprecated\npublic class BCD {\n\n\t/**\n\t * 字符串转BCD码\n\t * @param asc ASCII字符串\n\t * @return BCD\n\t */\n\tpublic static byte[] strToBcd(String asc) {\n\t\tAssert.notNull(asc, \"ASCII must not be null!\");\n\t\tint len = asc.length();\n\t\tint mod = len % 2;\n\t\tif (mod != 0) {\n\t\t\tasc = \"0\" + asc;\n\t\t\tlen = asc.length();\n\t\t}\n\t\tbyte[] abt;\n\t\tif (len >= 2) {\n\t\t\tlen >>= 1;\n\t\t}\n\t\tbyte[] bbt;\n\t\tbbt = new byte[len];\n\t\tabt = asc.getBytes();\n\t\tint j;\n\t\tint k;\n\t\tfor (int p = 0; p < asc.length() / 2; p++) {\n\t\t\tif ((abt[2 * p] >= '0') && (abt[2 * p] <= '9')) {\n\t\t\t\tj = abt[2 * p] - '0';\n\t\t\t} else if ((abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) {\n\t\t\t\tj = abt[2 * p] - 'a' + 0x0a;\n\t\t\t} else {\n\t\t\t\tj = abt[2 * p] - 'A' + 0x0a;\n\t\t\t}\n\t\t\tif ((abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) {\n\t\t\t\tk = abt[2 * p + 1] - '0';\n\t\t\t} else if ((abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) {\n\t\t\t\tk = abt[2 * p + 1] - 'a' + 0x0a;\n\t\t\t} else {\n\t\t\t\tk = abt[2 * p + 1] - 'A' + 0x0a;\n\t\t\t}\n\t\t\tint a = (j << 4) + k;\n\t\t\tbyte b = (byte) a;\n\t\t\tbbt[p] = b;\n\t\t}\n\t\treturn bbt;\n\t}\n\n\t/**\n\t * ASCII转BCD\n\t * @param ascii ASCII byte数组\n\t * @return BCD\n\t */\n\tpublic static byte[] ascToBcd(byte[] ascii) {\n\t\tAssert.notNull(ascii, \"Ascii must be not null!\");\n\t\treturn ascToBcd(ascii, ascii.length);\n\t}\n\n\t/**\n\t * ASCII转BCD\n\t * @param ascii ASCII byte数组\n\t * @param ascLength 长度\n\t * @return BCD\n\t */\n\tpublic static byte[] ascToBcd(byte[] ascii, int ascLength) {\n\t\tAssert.notNull(ascii, \"Ascii must be not null!\");\n\t\tbyte[] bcd = new byte[ascLength / 2];\n\t\tint j = 0;\n\t\tfor (int i = 0; i < (ascLength + 1) / 2; i++) {\n\t\t\tbcd[i] = ascToBcd(ascii[j++]);\n\t\t\tbcd[i] = (byte) (((j >= ascLength) ? 0x00 : ascToBcd(ascii[j++])) + (bcd[i] << 4));\n\t\t}\n\t\treturn bcd;\n\t}\n\n\t/**\n\t * BCD转ASCII字符串\n\t * @param bytes BCD byte数组\n\t * @return ASCII字符串\n\t */\n\tpublic static String bcdToStr(byte[] bytes) {\n\t\tAssert.notNull(bytes, \"Bcd bytes must be not null!\");\n\t\tchar[] temp = new char[bytes.length * 2];\n\t\tchar val;\n\n\t\tfor (int i = 0; i < bytes.length; i++) {\n\t\t\tval = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f);\n\t\t\ttemp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0');\n\n\t\t\tval = (char) (bytes[i] & 0x0f);\n\t\t\ttemp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0');\n\t\t}\n\t\treturn new String(temp);\n\t}\n\n\n\t//----------------------------------------------------------------- Private method start\n\t/**\n\t * 转换单个byte为BCD\n\t * @param asc ACSII\n\t * @return BCD\n\t */\n\tprivate static byte ascToBcd(byte asc) {\n\t\tbyte bcd;\n\n\t\tif ((asc >= '0') && (asc <= '9')) {\n\t\t\tbcd = (byte) (asc - '0');\n\t\t}else if ((asc >= 'A') && (asc <= 'F')) {\n\t\t\tbcd = (byte) (asc - 'A' + 10);\n\t\t}else if ((asc >= 'a') && (asc <= 'f')) {\n\t\t\tbcd = (byte) (asc - 'a' + 10);\n\t\t}else {\n\t\t\tbcd = (byte) (asc - 48);\n\t\t}\n\t\treturn bcd;\n\t}\n\t//----------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base16Codec.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Base16（Hex）编码解码器<br>\n * 十六进制（简写为hex或下标16）在数学中是一种逢16进1的进位制，一般用数字0到9和字母A到F表示（其中:A~F即10~15）。<br>\n * 例如十进制数57，在二进制写作111001，在16进制写作39。\n *\n * @author looly\n * @since 5.7.23\n */\npublic class Base16Codec implements Encoder<byte[], char[]>, Decoder<CharSequence, byte[]> {\n\n\tpublic static final Base16Codec CODEC_LOWER = new Base16Codec(true);\n\tpublic static final Base16Codec CODEC_UPPER = new Base16Codec(false);\n\n\tprivate final char[] alphabets;\n\n\t/**\n\t * 构造\n\t *\n\t * @param lowerCase 是否小写\n\t */\n\tpublic Base16Codec(boolean lowerCase) {\n\t\tthis.alphabets = (lowerCase ? \"0123456789abcdef\" : \"0123456789ABCDEF\").toCharArray();\n\t}\n\n\t@Override\n\tpublic char[] encode(byte[] data) {\n\t\tfinal int len = data.length;\n\t\tfinal char[] out = new char[len << 1];//len*2\n\t\t// two characters from the hex value.\n\t\tfor (int i = 0, j = 0; i < len; i++) {\n\t\t\tout[j++] = alphabets[(0xF0 & data[i]) >>> 4];// 高位\n\t\t\tout[j++] = alphabets[0x0F & data[i]];// 低位\n\t\t}\n\t\treturn out;\n\t}\n\n\t@Override\n\tpublic byte[] decode(CharSequence encoded) {\n\t\tif (StrUtil.isEmpty(encoded)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tencoded = StrUtil.cleanBlank(encoded);\n\t\tint len = encoded.length();\n\n\t\tif ((len & 0x01) != 0) {\n\t\t\t// 如果提供的数据是奇数长度，则前面补0凑偶数\n\t\t\tencoded = \"0\" + encoded;\n\t\t\tlen = encoded.length();\n\t\t}\n\n\t\tfinal byte[] out = new byte[len >> 1];\n\n\t\t// two characters form the hex value.\n\t\tfor (int i = 0, j = 0; j < len; i++) {\n\t\t\tint f = toDigit(encoded.charAt(j), j) << 4;\n\t\t\tj++;\n\t\t\tf = f | toDigit(encoded.charAt(j), j);\n\t\t\tj++;\n\t\t\tout[i] = (byte) (f & 0xFF);\n\t\t}\n\n\t\treturn out;\n\t}\n\n\t/**\n\t * 将指定char值转换为Unicode字符串形式，常用于特殊字符（例如汉字）转Unicode形式<br>\n\t * 转换的字符串如果u后不足4位，则前面用0填充，例如：\n\t *\n\t * <pre>\n\t * '你' =》'\\u4f60'\n\t * </pre>\n\t *\n\t * @param ch char值\n\t * @return Unicode表现形式\n\t */\n\tpublic String toUnicodeHex(char ch) {\n\t\treturn \"\\\\u\" +//\n\t\t\t\talphabets[(ch >> 12) & 15] +//\n\t\t\t\talphabets[(ch >> 8) & 15] +//\n\t\t\t\talphabets[(ch >> 4) & 15] +//\n\t\t\t\talphabets[(ch) & 15];\n\t}\n\n\t/**\n\t * 将byte值转为16进制并添加到{@link StringBuilder}中\n\t *\n\t * @param builder {@link StringBuilder}\n\t * @param b       byte\n\t */\n\tpublic void appendHex(StringBuilder builder, byte b) {\n\t\tint high = (b & 0xf0) >>> 4;//高位\n\t\tint low = b & 0x0f;//低位\n\t\tbuilder.append(alphabets[high]);\n\t\tbuilder.append(alphabets[low]);\n\t}\n\n\t/**\n\t * 将十六进制字符转换成一个整数\n\t *\n\t * @param ch    十六进制char\n\t * @param index 十六进制字符在字符数组中的位置\n\t * @return 一个整数\n\t * @throws UtilException 当ch不是一个合法的十六进制字符时，抛出运行时异常\n\t */\n\tprivate static int toDigit(char ch, int index) {\n\t\tint digit = Character.digit(ch, 16);\n\t\tif (digit < 0) {\n\t\t\tthrow new UtilException(\"Illegal hexadecimal character {} at index {}\", ch, index);\n\t\t}\n\t\treturn digit;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base32.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )<br>\n * base32就是用32（2的5次方）个特定ASCII码来表示256个ASCII码。<br>\n * 所以，5个ASCII字符经过base32编码后会变为8个字符（公约数为40），长度增加3/5.不足8n用“=”补足。<br>\n * 根据RFC4648 Base32规范，支持两种模式：\n * <ul>\n *     <li>Base 32 Alphabet                 (ABCDEFGHIJKLMNOPQRSTUVWXYZ234567)</li>\n *     <li>\"Extended Hex\" Base 32 Alphabet  (0123456789ABCDEFGHIJKLMNOPQRSTUV)</li>\n * </ul>\n *\n * @author Looly\n */\npublic class Base32 {\n\t//----------------------------------------------------------------------------------------- encode\n\n\t/**\n\t * 编码\n\t *\n\t * @param bytes 数据\n\t * @return base32\n\t */\n\tpublic static String encode(final byte[] bytes) {\n\t\treturn Base32Codec.INSTANCE.encode(bytes);\n\t}\n\n\t/**\n\t * base32编码\n\t *\n\t * @param source 被编码的base32字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(String source) {\n\t\treturn encode(source, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * base32编码\n\t *\n\t * @param source  被编码的base32字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(String source, Charset charset) {\n\t\treturn encode(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * 编码\n\t *\n\t * @param bytes 数据（Hex模式）\n\t * @return base32\n\t */\n\tpublic static String encodeHex(final byte[] bytes) {\n\t\treturn Base32Codec.INSTANCE.encode(bytes, true);\n\t}\n\n\t/**\n\t * base32编码（Hex模式）\n\t *\n\t * @param source 被编码的base32字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeHex(String source) {\n\t\treturn encodeHex(source, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * base32编码（Hex模式）\n\t *\n\t * @param source  被编码的base32字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeHex(String source, Charset charset) {\n\t\treturn encodeHex(StrUtil.bytes(source, charset));\n\t}\n\n\t//----------------------------------------------------------------------------------------- decode\n\n\t/**\n\t * 解码\n\t *\n\t * @param base32 base32编码\n\t * @return 数据\n\t */\n\tpublic static byte[] decode(String base32) {\n\t\treturn Base32Codec.INSTANCE.decode(base32);\n\t}\n\n\t/**\n\t * base32解码\n\t *\n\t * @param source 被解码的base32字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(String source) {\n\t\treturn decodeStr(source, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * base32解码\n\t *\n\t * @param source  被解码的base32字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(String source, Charset charset) {\n\t\treturn StrUtil.str(decode(source), charset);\n\t}\n\n\t/**\n\t * 解码\n\t *\n\t * @param base32 base32编码\n\t * @return 数据\n\t */\n\tpublic static byte[] decodeHex(String base32) {\n\t\treturn Base32Codec.INSTANCE.decode(base32, true);\n\t}\n\n\t/**\n\t * base32解码\n\t *\n\t * @param source 被解码的base32字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStrHex(String source) {\n\t\treturn decodeStrHex(source, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * base32解码\n\t *\n\t * @param source  被解码的base32字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStrHex(String source, Charset charset) {\n\t\treturn StrUtil.str(decodeHex(source), charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base32Codec.java",
    "content": "package cn.hutool.core.codec;\n\nimport java.util.Arrays;\n\n/**\n * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )<br>\n * base32就是用32（2的5次方）个特定ASCII码来表示256个ASCII码。<br>\n * 所以，5个ASCII字符经过base32编码后会变为8个字符（公约数为40），长度增加3/5.不足8n用“=”补足。<br>\n * 根据RFC4648 Base32规范，支持两种模式：\n * <ul>\n *     <li>Base 32 Alphabet                 (ABCDEFGHIJKLMNOPQRSTUVWXYZ234567)</li>\n *     <li>\"Extended Hex\" Base 32 Alphabet  (0123456789ABCDEFGHIJKLMNOPQRSTUV)</li>\n * </ul>\n *\n * @author Looly\n * @since 5.8.0\n */\npublic class Base32Codec implements Encoder<byte[], String>, Decoder<CharSequence, byte[]> {\n\n\tpublic static Base32Codec INSTANCE = new Base32Codec();\n\n\t@Override\n\tpublic String encode(byte[] data) {\n\t\treturn encode(data, false);\n\t}\n\n\t/**\n\t * 编码数据\n\t *\n\t * @param data   数据\n\t * @param useHex 是否使用Hex Alphabet\n\t * @return 编码后的Base32字符串\n\t */\n\tpublic String encode(byte[] data, boolean useHex) {\n\t\tfinal Base32Encoder encoder = useHex ? Base32Encoder.HEX_ENCODER : Base32Encoder.ENCODER;\n\t\treturn encoder.encode(data);\n\t}\n\n\t@Override\n\tpublic byte[] decode(CharSequence encoded) {\n\t\treturn decode(encoded, false);\n\t}\n\n\t/**\n\t * 解码数据\n\t *\n\t * @param encoded base32字符串\n\t * @param useHex  是否使用Hex Alphabet\n\t * @return 解码后的内容\n\t */\n\tpublic byte[] decode(CharSequence encoded, boolean useHex) {\n\t\tfinal Base32Decoder decoder = useHex ? Base32Decoder.HEX_DECODER : Base32Decoder.DECODER;\n\t\treturn decoder.decode(encoded);\n\t}\n\n\t/**\n\t * Bas32编码器\n\t */\n\tpublic static class Base32Encoder implements Encoder<byte[], String> {\n\t\tprivate static final String DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\t\tprivate static final String HEX_ALPHABET = \"0123456789ABCDEFGHIJKLMNOPQRSTUV\";\n\t\tprivate static final Character DEFAULT_PAD = '=';\n\t\tprivate static final int[] BASE32_FILL = {-1, 4, 1, 6, 3};\n\n\t\tpublic static final Base32Encoder ENCODER = new Base32Encoder(DEFAULT_ALPHABET, DEFAULT_PAD);\n\t\tpublic static final Base32Encoder HEX_ENCODER = new Base32Encoder(HEX_ALPHABET, DEFAULT_PAD);\n\n\t\tprivate final char[] alphabet;\n\t\tprivate final Character pad;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 自定义编码字母表，见 {@link #DEFAULT_ALPHABET}和 {@link #HEX_ALPHABET}\n\t\t * @param pad      补位字符\n\t\t */\n\t\tpublic Base32Encoder(String alphabet, Character pad) {\n\t\t\tthis.alphabet = alphabet.toCharArray();\n\t\t\tthis.pad = pad;\n\t\t}\n\n\t\t@Override\n\t\tpublic String encode(byte[] data) {\n\t\t\tint i = 0;\n\t\t\tint index = 0;\n\t\t\tint digit;\n\t\t\tint currByte;\n\t\t\tint nextByte;\n\n\t\t\tint encodeLen = data.length * 8 / 5;\n\t\t\tif (encodeLen != 0) {\n\t\t\t\tencodeLen = encodeLen + 1 + BASE32_FILL[(data.length * 8) % 5];\n\t\t\t}\n\n\t\t\tStringBuilder base32 = new StringBuilder(encodeLen);\n\n\t\t\twhile (i < data.length) {\n\t\t\t\t// unsign\n\t\t\t\tcurrByte = (data[i] >= 0) ? data[i] : (data[i] + 256);\n\n\t\t\t\t/* Is the current digit going to span a byte boundary? */\n\t\t\t\tif (index > 3) {\n\t\t\t\t\tif ((i + 1) < data.length) {\n\t\t\t\t\t\tnextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnextByte = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\tdigit = currByte & (0xFF >> index);\n\t\t\t\t\tindex = (index + 5) % 8;\n\t\t\t\t\tdigit <<= index;\n\t\t\t\t\tdigit |= nextByte >> (8 - index);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tdigit = (currByte >> (8 - (index + 5))) & 0x1F;\n\t\t\t\t\tindex = (index + 5) % 8;\n\t\t\t\t\tif (index == 0) {\n\t\t\t\t\t\ti++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbase32.append(alphabet[digit]);\n\t\t\t}\n\n\t\t\tif (null != pad) {\n\t\t\t\t// 末尾补充不足长度的\n\t\t\t\twhile (base32.length() < encodeLen) {\n\t\t\t\t\tbase32.append(pad.charValue());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn base32.toString();\n\t\t}\n\t}\n\n\t/**\n\t * Base32解码器\n\t */\n\tpublic static class Base32Decoder implements Decoder<CharSequence, byte[]> {\n\t\tprivate static final char BASE_CHAR = '0';\n\n\t\tpublic static final Base32Decoder DECODER = new Base32Decoder(Base32Encoder.DEFAULT_ALPHABET);\n\t\tpublic static final Base32Decoder HEX_DECODER = new Base32Decoder(Base32Encoder.HEX_ALPHABET);\n\n\t\tprivate final byte[] lookupTable;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 编码字母表\n\t\t */\n\t\tpublic Base32Decoder(String alphabet) {\n\t\t\tlookupTable = new byte[128];\n\t\t\tArrays.fill(lookupTable, (byte) -1);\n\n\t\t\tfinal int length = alphabet.length();\n\n\t\t\tchar c;\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\tc = alphabet.charAt(i);\n\t\t\t\tlookupTable[c - BASE_CHAR] = (byte) i;\n\t\t\t\t// 支持小写字母解码\n\t\t\t\tif(c >= 'A' && c <= 'Z'){\n\t\t\t\t\tlookupTable[Character.toLowerCase(c) - BASE_CHAR] = (byte) i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic byte[] decode(CharSequence encoded) {\n\t\t\tint i, index, lookup, offset, digit;\n\t\t\tfinal String base32 = encoded.toString();\n\t\t\tint len = base32.endsWith(\"=\") ? base32.indexOf(\"=\") * 5 / 8 : base32.length() * 5 / 8;\n\t\t\tbyte[] bytes = new byte[len];\n\n\t\t\tfor (i = 0, index = 0, offset = 0; i < base32.length(); i++) {\n\t\t\t\tlookup = base32.charAt(i) - BASE_CHAR;\n\n\t\t\t\t/* Skip chars outside the lookup table */\n\t\t\t\tif (lookup < 0 || lookup >= lookupTable.length) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdigit = lookupTable[lookup];\n\n\t\t\t\t/* If this digit is not in the table, ignore it */\n\t\t\t\tif (digit < 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (index <= 3) {\n\t\t\t\t\tindex = (index + 5) % 8;\n\t\t\t\t\tif (index == 0) {\n\t\t\t\t\t\tbytes[offset] |= digit;\n\t\t\t\t\t\toffset++;\n\t\t\t\t\t\tif (offset >= bytes.length) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbytes[offset] |= digit << (8 - index);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tindex = (index + 5) % 8;\n\t\t\t\t\tbytes[offset] |= (digit >>> index);\n\t\t\t\t\toffset++;\n\n\t\t\t\t\tif (offset >= bytes.length) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tbytes[offset] |= digit << (8 - index);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bytes;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base58.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.exceptions.ValidateException;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Arrays;\n\n\n/**\n * Base58工具类，提供Base58的编码和解码方案<br>\n * 参考： https://github.com/Anujraval24/Base58Encoding<br>\n * 规范见：https://en.bitcoin.it/wiki/Base58Check_encoding\n *\n * @author lin， looly\n * @since 5.7.22\n */\npublic class Base58 {\n\n\tprivate static final int CHECKSUM_SIZE = 4;\n\n\t// -------------------------------------------------------------------- encode\n\n\t/**\n\t * Base58编码<br>\n\t * 包含版本位和校验位\n\t *\n\t * @param version 编码版本，{@code null}表示不包含版本位\n\t * @param data    被编码的数组，添加校验和。\n\t * @return 编码后的字符串\n\t */\n\tpublic static String encodeChecked(Integer version, byte[] data) {\n\t\treturn encode(addChecksum(version, data));\n\t}\n\n\t/**\n\t * Base58编码\n\t *\n\t * @param data 被编码的数据，不带校验和。\n\t * @return 编码后的字符串\n\t */\n\tpublic static String encode(byte[] data) {\n\t\treturn Base58Codec.INSTANCE.encode(data);\n\t}\n\t// -------------------------------------------------------------------- decode\n\n\t/**\n\t * Base58解码<br>\n\t * 解码包含标志位验证和版本呢位去除\n\t *\n\t * @param encoded 被解码的base58字符串\n\t * @return 解码后的bytes\n\t * @throws ValidateException 标志位验证错误抛出此异常\n\t */\n\tpublic static byte[] decodeChecked(CharSequence encoded) throws ValidateException {\n\t\ttry {\n\t\t\treturn decodeChecked(encoded, true);\n\t\t} catch (ValidateException ignore) {\n\t\t\treturn decodeChecked(encoded, false);\n\t\t}\n\t}\n\n\t/**\n\t * Base58解码<br>\n\t * 解码包含标志位验证和版本呢位去除\n\t *\n\t * @param encoded     被解码的base58字符串\n\t * @param withVersion 是否包含版本位\n\t * @return 解码后的bytes\n\t * @throws ValidateException 标志位验证错误抛出此异常\n\t */\n\tpublic static byte[] decodeChecked(CharSequence encoded, boolean withVersion) throws ValidateException {\n\t\tbyte[] valueWithChecksum = decode(encoded);\n\t\treturn verifyAndRemoveChecksum(valueWithChecksum, withVersion);\n\t}\n\n\t/**\n\t * Base58解码\n\t *\n\t * @param encoded 被编码的base58字符串\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(CharSequence encoded) {\n\t\treturn Base58Codec.INSTANCE.decode(encoded);\n\t}\n\n\t/**\n\t * 验证并去除验证位和版本位\n\t *\n\t * @param data        编码的数据\n\t * @param withVersion 是否包含版本位\n\t * @return 载荷数据\n\t */\n\tprivate static byte[] verifyAndRemoveChecksum(byte[] data, boolean withVersion) {\n\t\tfinal byte[] payload = Arrays.copyOfRange(data, withVersion ? 1 : 0, data.length - CHECKSUM_SIZE);\n\t\tfinal byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length);\n\t\tfinal byte[] expectedChecksum = checksum(payload);\n\t\tif (false == Arrays.equals(checksum, expectedChecksum)) {\n\t\t\tthrow new ValidateException(\"Base58 checksum is invalid\");\n\t\t}\n\t\treturn payload;\n\t}\n\n\t/**\n\t * 数据 + 校验码\n\t *\n\t * @param version 版本，{@code null}表示不添加版本位\n\t * @param payload Base58数据（不含校验码）\n\t * @return Base58数据\n\t */\n\tprivate static byte[] addChecksum(Integer version, byte[] payload) {\n\t\tfinal byte[] addressBytes;\n\t\tif (null != version) {\n\t\t\taddressBytes = new byte[1 + payload.length + CHECKSUM_SIZE];\n\t\t\taddressBytes[0] = (byte) version.intValue();\n\t\t\tSystem.arraycopy(payload, 0, addressBytes, 1, payload.length);\n\t\t} else {\n\t\t\taddressBytes = new byte[payload.length + CHECKSUM_SIZE];\n\t\t\tSystem.arraycopy(payload, 0, addressBytes, 0, payload.length);\n\t\t}\n\t\tfinal byte[] checksum = checksum(payload);\n\t\tSystem.arraycopy(checksum, 0, addressBytes, addressBytes.length - CHECKSUM_SIZE, CHECKSUM_SIZE);\n\t\treturn addressBytes;\n\t}\n\n\t/**\n\t * 获取校验码<br>\n\t * 计算规则为对数据进行两次sha256计算，然后取{@link #CHECKSUM_SIZE}长度\n\t *\n\t * @param data 数据\n\t * @return 校验码\n\t */\n\tprivate static byte[] checksum(byte[] data) {\n\t\tbyte[] hash = hash256(hash256(data));\n\t\treturn Arrays.copyOfRange(hash, 0, CHECKSUM_SIZE);\n\t}\n\n\t/**\n\t * 计算数据的SHA-256值\n\t *\n\t * @param data 数据\n\t * @return sha-256值\n\t */\n\tprivate static byte[] hash256(byte[] data) {\n\t\ttry {\n\t\t\treturn MessageDigest.getInstance(\"SHA-256\").digest(data);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Arrays;\n\n/**\n * Base58编码器<br>\n * 此编码器不包括校验码、版本等信息\n *\n * @author lin， looly\n * @since 5.7.22\n */\npublic class Base58Codec implements Encoder<byte[], String>, Decoder<CharSequence, byte[]> {\n\n\tpublic static Base58Codec INSTANCE = new Base58Codec();\n\n\t/**\n\t * Base58编码\n\t *\n\t * @param data 被编码的数据，不带校验和。\n\t * @return 编码后的字符串\n\t */\n\t@Override\n\tpublic String encode(byte[] data) {\n\t\treturn Base58Encoder.ENCODER.encode(data);\n\t}\n\n\t/**\n\t * 解码给定的Base58字符串\n\t *\n\t * @param encoded Base58编码字符串\n\t * @return 解码后的bytes\n\t * @throws IllegalArgumentException 非标准Base58字符串\n\t */\n\t@Override\n\tpublic byte[] decode(CharSequence encoded) throws IllegalArgumentException {\n\t\treturn Base58Decoder.DECODER.decode(encoded);\n\t}\n\n\t/**\n\t * Base58编码器\n\t *\n\t * @since 5.8.0\n\t */\n\tpublic static class Base58Encoder implements Encoder<byte[], String> {\n\t\tprivate static final String DEFAULT_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\n\t\tpublic static final Base58Encoder ENCODER = new Base58Encoder(DEFAULT_ALPHABET.toCharArray());\n\n\t\tprivate final char[] alphabet;\n\t\tprivate final char alphabetZero;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 编码字母表\n\t\t */\n\t\tpublic Base58Encoder(char[] alphabet) {\n\t\t\tthis.alphabet = alphabet;\n\t\t\talphabetZero = alphabet[0];\n\t\t}\n\n\t\t@Override\n\t\tpublic String encode(byte[] data) {\n\t\t\tif (null == data) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (data.length == 0) {\n\t\t\t\treturn StrUtil.EMPTY;\n\t\t\t}\n\t\t\t// 计算开头0的个数\n\t\t\tint zeroCount = 0;\n\t\t\twhile (zeroCount < data.length && data[zeroCount] == 0) {\n\t\t\t\t++zeroCount;\n\t\t\t}\n\t\t\t// 将256位编码转换为58位编码\n\t\t\tdata = Arrays.copyOf(data, data.length); // since we modify it in-place\n\t\t\tfinal char[] encoded = new char[data.length * 2]; // upper bound\n\t\t\tint outputStart = encoded.length;\n\t\t\tfor (int inputStart = zeroCount; inputStart < data.length; ) {\n\t\t\t\tencoded[--outputStart] = alphabet[divmod(data, inputStart, 256, 58)];\n\t\t\t\tif (data[inputStart] == 0) {\n\t\t\t\t\t++inputStart; // optimization - skip leading zeros\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.\n\t\t\twhile (outputStart < encoded.length && encoded[outputStart] == alphabetZero) {\n\t\t\t\t++outputStart;\n\t\t\t}\n\t\t\twhile (--zeroCount >= 0) {\n\t\t\t\tencoded[--outputStart] = alphabetZero;\n\t\t\t}\n\t\t\t// Return encoded string (including encoded leading zeros).\n\t\t\treturn new String(encoded, outputStart, encoded.length - outputStart);\n\t\t}\n\t}\n\n\t/**\n\t * Base58解码器\n\t *\n\t * @since 5.8.0\n\t */\n\tpublic static class Base58Decoder implements Decoder<CharSequence, byte[]> {\n\n\t\tpublic static Base58Decoder DECODER = new Base58Decoder(Base58Encoder.DEFAULT_ALPHABET);\n\n\t\tprivate final byte[] lookupTable;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 编码字符表\n\t\t */\n\t\tpublic Base58Decoder(String alphabet) {\n\t\t\tfinal byte[] lookupTable = new byte['z' + 1];\n\t\t\tArrays.fill(lookupTable, (byte) -1);\n\n\t\t\tfinal int length = alphabet.length();\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\tlookupTable[alphabet.charAt(i)] = (byte) i;\n\t\t\t}\n\t\t\tthis.lookupTable = lookupTable;\n\t\t}\n\n\t\t@Override\n\t\tpublic byte[] decode(CharSequence encoded) {\n\t\t\tif (encoded.length() == 0) {\n\t\t\t\treturn new byte[0];\n\t\t\t}\n\t\t\t// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).\n\t\t\tfinal byte[] input58 = new byte[encoded.length()];\n\t\t\tfor (int i = 0; i < encoded.length(); ++i) {\n\t\t\t\tchar c = encoded.charAt(i);\n\t\t\t\tint digit = c < 128 ? lookupTable[c] : -1;\n\t\t\t\tif (digit < 0) {\n\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Invalid char '{}' at [{}]\", c, i));\n\t\t\t\t}\n\t\t\t\tinput58[i] = (byte) digit;\n\t\t\t}\n\t\t\t// Count leading zeros.\n\t\t\tint zeros = 0;\n\t\t\twhile (zeros < input58.length && input58[zeros] == 0) {\n\t\t\t\t++zeros;\n\t\t\t}\n\t\t\t// Convert base-58 digits to base-256 digits.\n\t\t\tbyte[] decoded = new byte[encoded.length()];\n\t\t\tint outputStart = decoded.length;\n\t\t\tfor (int inputStart = zeros; inputStart < input58.length; ) {\n\t\t\t\tdecoded[--outputStart] = divmod(input58, inputStart, 58, 256);\n\t\t\t\tif (input58[inputStart] == 0) {\n\t\t\t\t\t++inputStart; // optimization - skip leading zeros\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Ignore extra leading zeroes that were added during the calculation.\n\t\t\twhile (outputStart < decoded.length && decoded[outputStart] == 0) {\n\t\t\t\t++outputStart;\n\t\t\t}\n\t\t\t// Return decoded data (including original number of leading zeros).\n\t\t\treturn Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);\n\t\t}\n\t}\n\n\t/**\n\t * Divides a number, represented as an array of bytes each containing a single digit\n\t * in the specified base, by the given divisor. The given number is modified in-place\n\t * to contain the quotient, and the return value is the remainder.\n\t *\n\t * @param number     the number to divide\n\t * @param firstDigit the index within the array of the first non-zero digit\n\t *                   (this is used for optimization by skipping the leading zeros)\n\t * @param base       the base in which the number's digits are represented (up to 256)\n\t * @param divisor    the number to divide by (up to 256)\n\t * @return the remainder of the division operation\n\t */\n\tprivate static byte divmod(byte[] number, int firstDigit, int base, int divisor) {\n\t\t// this is just long division which accounts for the base of the input digits\n\t\tint remainder = 0;\n\t\tfor (int i = firstDigit; i < number.length; i++) {\n\t\t\tint digit = (int) number[i] & 0xFF;\n\t\t\tint temp = remainder * base + digit;\n\t\t\tnumber[i] = (byte) (temp / divisor);\n\t\t\tremainder = temp % divisor;\n\t\t}\n\t\treturn (byte) remainder;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base62.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Base62工具类，提供Base62的编码和解码方案<br>\n *\n * @author Looly\n * @since 4.5.9\n */\npublic class Base62 {\n\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\n\t// -------------------------------------------------------------------- encode\n\t/**\n\t * Base62编码\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source) {\n\t\treturn encode(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * Base62编码\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source, Charset charset) {\n\t\treturn encode(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * Base62编码\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(byte[] source) {\n\t\treturn new String(Base62Codec.INSTANCE.encode(source));\n\t}\n\n\t/**\n\t * Base62编码\n\t *\n\t * @param in 被编码Base62的流（一般为图片流或者文件流）\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(InputStream in) {\n\t\treturn encode(IoUtil.readBytes(in));\n\t}\n\n\t/**\n\t * Base62编码\n\t *\n\t * @param file 被编码Base62的文件\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(File file) {\n\t\treturn encode(FileUtil.readBytes(file));\n\t}\n\n\t/**\n\t * Base62编码（反转字母表模式）\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeInverted(CharSequence source) {\n\t\treturn encodeInverted(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * Base62编码（反转字母表模式）\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeInverted(CharSequence source, Charset charset) {\n\t\treturn encodeInverted(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * Base62编码（反转字母表模式）\n\t *\n\t * @param source 被编码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeInverted(byte[] source) {\n\t\treturn new String(Base62Codec.INSTANCE.encode(source, true));\n\t}\n\n\t/**\n\t * Base62编码\n\t *\n\t * @param in 被编码Base62的流（一般为图片流或者文件流）\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeInverted(InputStream in) {\n\t\treturn encodeInverted(IoUtil.readBytes(in));\n\t}\n\n\t/**\n\t * Base62编码（反转字母表模式）\n\t *\n\t * @param file 被编码Base62的文件\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encodeInverted(File file) {\n\t\treturn encodeInverted(FileUtil.readBytes(file));\n\t}\n\n\t// -------------------------------------------------------------------- decode\n\t/**\n\t * Base62解码\n\t *\n\t * @param source 被解码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStrGbk(CharSequence source) {\n\t\treturn decodeStr(source, CharsetUtil.CHARSET_GBK);\n\t}\n\n\t/**\n\t * Base62解码\n\t *\n\t * @param source 被解码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(CharSequence source) {\n\t\treturn decodeStr(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * Base62解码\n\t *\n\t * @param source 被解码的Base62字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(CharSequence source, Charset charset) {\n\t\treturn StrUtil.str(decode(source), charset);\n\t}\n\n\t/**\n\t * Base62解码\n\t *\n\t * @param Base62 被解码的Base62字符串\n\t * @param destFile 目标文件\n\t * @return 目标文件\n\t */\n\tpublic static File decodeToFile(CharSequence Base62, File destFile) {\n\t\treturn FileUtil.writeBytes(decode(Base62), destFile);\n\t}\n\n\t/**\n\t * Base62解码\n\t *\n\t * @param base62Str 被解码的Base62字符串\n\t * @param out 写出到的流\n\t * @param isCloseOut 是否关闭输出流\n\t */\n\tpublic static void decodeToStream(CharSequence base62Str, OutputStream out, boolean isCloseOut) {\n\t\tIoUtil.write(out, isCloseOut, decode(base62Str));\n\t}\n\n\t/**\n\t * Base62解码\n\t *\n\t * @param base62Str 被解码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static byte[] decode(CharSequence base62Str) {\n\t\treturn decode(StrUtil.bytes(base62Str, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * 解码Base62\n\t *\n\t * @param base62bytes Base62输入\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(byte[] base62bytes) {\n\t\treturn Base62Codec.INSTANCE.decode(base62bytes);\n\t}\n\n\t/**\n\t * Base62解码（反转字母表模式）\n\t *\n\t * @param source 被解码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStrInverted(CharSequence source) {\n\t\treturn decodeStrInverted(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * Base62解码（反转字母表模式）\n\t *\n\t * @param source 被解码的Base62字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStrInverted(CharSequence source, Charset charset) {\n\t\treturn StrUtil.str(decodeInverted(source), charset);\n\t}\n\n\t/**\n\t * Base62解码（反转字母表模式）\n\t *\n\t * @param Base62 被解码的Base62字符串\n\t * @param destFile 目标文件\n\t * @return 目标文件\n\t */\n\tpublic static File decodeToFileInverted(CharSequence Base62, File destFile) {\n\t\treturn FileUtil.writeBytes(decodeInverted(Base62), destFile);\n\t}\n\n\t/**\n\t * Base62解码（反转字母表模式）\n\t *\n\t * @param base62Str 被解码的Base62字符串\n\t * @param out 写出到的流\n\t * @param isCloseOut 是否关闭输出流\n\t */\n\tpublic static void decodeToStreamInverted(CharSequence base62Str, OutputStream out, boolean isCloseOut) {\n\t\tIoUtil.write(out, isCloseOut, decodeInverted(base62Str));\n\t}\n\n\t/**\n\t * Base62解码（反转字母表模式）\n\t *\n\t * @param base62Str 被解码的Base62字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static byte[] decodeInverted(CharSequence base62Str) {\n\t\treturn decodeInverted(StrUtil.bytes(base62Str, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * 解码Base62（反转字母表模式）\n\t *\n\t * @param base62bytes Base62输入\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decodeInverted(byte[] base62bytes) {\n\t\treturn Base62Codec.INSTANCE.decode(base62bytes, true);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Serializable;\n\n/**\n * Base62编码解码实现，常用于短URL<br>\n * From https://github.com/seruco/base62\n *\n * @author Looly, Sebastian Ruhleder, sebastian@seruco.io\n * @since 4.5.9\n */\npublic class Base62Codec implements Encoder<byte[], byte[]>, Decoder<byte[], byte[]>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int STANDARD_BASE = 256;\n\tprivate static final int TARGET_BASE = 62;\n\n\tpublic static Base62Codec INSTANCE = new Base62Codec();\n\n\t/**\n\t * 编码指定消息bytes为Base62格式的bytes\n\t *\n\t * @param data 被编码的消息\n\t * @return Base62内容\n\t */\n\t@Override\n\tpublic byte[] encode(byte[] data) {\n\t\treturn encode(data, false);\n\t}\n\n\t/**\n\t * 编码指定消息bytes为Base62格式的bytes\n\t *\n\t * @param data        被编码的消息\n\t * @param useInverted 是否使用反转风格，即将GMP风格中的大小写做转换\n\t * @return Base62内容\n\t */\n\tpublic byte[] encode(byte[] data, boolean useInverted) {\n\t\tfinal Base62Encoder encoder = useInverted ? Base62Encoder.INVERTED_ENCODER : Base62Encoder.GMP_ENCODER;\n\t\treturn encoder.encode(data);\n\t}\n\n\t/**\n\t * 解码Base62消息\n\t *\n\t * @param encoded Base62内容\n\t * @return 消息\n\t */\n\t@Override\n\tpublic byte[] decode(byte[] encoded) {\n\t\treturn decode(encoded, false);\n\t}\n\n\t/**\n\t * 解码Base62消息\n\t *\n\t * @param encoded     Base62内容\n\t * @param useInverted 是否使用反转风格，即将GMP风格中的大小写做转换\n\t * @return 消息\n\t */\n\tpublic byte[] decode(byte[] encoded, boolean useInverted) {\n\t\tfinal Base62Decoder decoder = useInverted ? Base62Decoder.INVERTED_DECODER : Base62Decoder.GMP_DECODER;\n\t\treturn decoder.decode(encoded);\n\t}\n\n\t/**\n\t * Base62编码器\n\t *\n\t * @since 5.8.0\n\t */\n\tpublic static class Base62Encoder implements Encoder<byte[], byte[]> {\n\t\t/**\n\t\t * GMP风格\n\t\t */\n\t\tprivate static final byte[] GMP = { //\n\t\t\t\t'0', '1', '2', '3', '4', '5', '6', '7', //\n\t\t\t\t'8', '9', 'A', 'B', 'C', 'D', 'E', 'F', //\n\t\t\t\t'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', //\n\t\t\t\t'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', //\n\t\t\t\t'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', //\n\t\t\t\t'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', //\n\t\t\t\t'm', 'n', 'o', 'p', 'q', 'r', 's', 't', //\n\t\t\t\t'u', 'v', 'w', 'x', 'y', 'z' //\n\t\t};\n\n\t\t/**\n\t\t * 反转风格，即将GMP风格中的大小写做转换\n\t\t */\n\t\tprivate static final byte[] INVERTED = { //\n\t\t\t\t'0', '1', '2', '3', '4', '5', '6', '7', //\n\t\t\t\t'8', '9', 'a', 'b', 'c', 'd', 'e', 'f', //\n\t\t\t\t'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', //\n\t\t\t\t'o', 'p', 'q', 'r', 's', 't', 'u', 'v', //\n\t\t\t\t'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', //\n\t\t\t\t'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', //\n\t\t\t\t'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', //\n\t\t\t\t'U', 'V', 'W', 'X', 'Y', 'Z' //\n\t\t};\n\n\t\tpublic static Base62Encoder GMP_ENCODER = new Base62Encoder(GMP);\n\t\tpublic static Base62Encoder INVERTED_ENCODER = new Base62Encoder(INVERTED);\n\n\t\tprivate final byte[] alphabet;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 字符表\n\t\t */\n\t\tpublic Base62Encoder(byte[] alphabet) {\n\t\t\tthis.alphabet = alphabet;\n\t\t}\n\n\t\t@Override\n\t\tpublic byte[] encode(byte[] data) {\n\t\t\tfinal byte[] indices = convert(data, STANDARD_BASE, TARGET_BASE);\n\t\t\treturn translate(indices, alphabet);\n\t\t}\n\t}\n\n\t/**\n\t * Base62解码器\n\t *\n\t * @since 5.8.0\n\t */\n\tpublic static class Base62Decoder implements Decoder<byte[], byte[]> {\n\n\t\tpublic static Base62Decoder GMP_DECODER = new Base62Decoder(Base62Encoder.GMP);\n\t\tpublic static Base62Decoder INVERTED_DECODER = new Base62Decoder(Base62Encoder.INVERTED);\n\n\t\tprivate final byte[] lookupTable;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param alphabet 字母表\n\t\t */\n\t\tpublic Base62Decoder(byte[] alphabet) {\n\t\t\tlookupTable = new byte['z' + 1];\n\t\t\tfor (int i = 0; i < alphabet.length; i++) {\n\t\t\t\tlookupTable[alphabet[i]] = (byte) i;\n\t\t\t}\n\t\t}\n\n\n\t\t@Override\n\t\tpublic byte[] decode(byte[] encoded) {\n\t\t\tfinal byte[] prepared = translate(encoded, lookupTable);\n\t\t\treturn convert(prepared, TARGET_BASE, STANDARD_BASE);\n\t\t}\n\t}\n\n\t// region Private Methods\n\n\t/**\n\t * 按照字典转换bytes\n\t *\n\t * @param indices    内容\n\t * @param dictionary 字典\n\t * @return 转换值\n\t */\n\tprivate static byte[] translate(byte[] indices, byte[] dictionary) {\n\t\tfinal byte[] translation = new byte[indices.length];\n\n\t\tfor (int i = 0; i < indices.length; i++) {\n\t\t\ttranslation[i] = dictionary[indices[i]];\n\t\t}\n\n\t\treturn translation;\n\t}\n\n\t/**\n\t * 使用定义的字母表从源基准到目标基准\n\t *\n\t * @param message    消息bytes\n\t * @param sourceBase 源基准长度\n\t * @param targetBase 目标基准长度\n\t * @return 计算结果\n\t */\n\tprivate static byte[] convert(byte[] message, int sourceBase, int targetBase) {\n\t\t// 计算结果长度，算法来自：http://codegolf.stackexchange.com/a/21672\n\t\tfinal int estimatedLength = estimateOutputLength(message.length, sourceBase, targetBase);\n\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream(estimatedLength);\n\n\t\tbyte[] source = message;\n\n\t\twhile (source.length > 0) {\n\t\t\tfinal ByteArrayOutputStream quotient = new ByteArrayOutputStream(source.length);\n\n\t\t\tint remainder = 0;\n\n\t\t\tfor (byte b : source) {\n\t\t\t\tfinal int accumulator = (b & 0xFF) + remainder * sourceBase;\n\t\t\t\tfinal int digit = (accumulator - (accumulator % targetBase)) / targetBase;\n\n\t\t\t\tremainder = accumulator % targetBase;\n\n\t\t\t\tif (quotient.size() > 0 || digit > 0) {\n\t\t\t\t\tquotient.write(digit);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tout.write(remainder);\n\n\t\t\tsource = quotient.toByteArray();\n\t\t}\n\n\t\t// pad output with zeroes corresponding to the number of leading zeroes in the message\n\t\tfor (int i = 0; i < message.length - 1 && message[i] == 0; i++) {\n\t\t\tout.write(0);\n\t\t}\n\n\t\treturn ArrayUtil.reverse(out.toByteArray());\n\t}\n\n\t/**\n\t * 估算结果长度\n\t *\n\t * @param inputLength 输入长度\n\t * @param sourceBase  源基准长度\n\t * @param targetBase  目标基准长度\n\t * @return 估算长度\n\t */\n\tprivate static int estimateOutputLength(int inputLength, int sourceBase, int targetBase) {\n\t\treturn (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength);\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base64.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Base64工具类，提供Base64的编码和解码方案<br>\n * base64编码是用64（2的6次方）个ASCII字符来表示256（2的8次方）个ASCII字符，<br>\n * 也就是三位二进制数组经过编码后变为四位的ASCII字符显示，长度比原来增加1/3。\n *\n * @author Looly\n */\npublic class Base64 {\n\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\t// -------------------------------------------------------------------- encode\n\n\t/**\n\t * 编码为Base64，非URL安全的\n\t *\n\t * @param arr     被编码的数组\n\t * @param lineSep 在76个char之后是CRLF还是EOF\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encode(byte[] arr, boolean lineSep) {\n\t\tif (arr == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn lineSep ?\n\t\t\t\tjava.util.Base64.getMimeEncoder().encode(arr) :\n\t\t\t\tjava.util.Base64.getEncoder().encode(arr);\n\t}\n\n\t/**\n\t * 编码为Base64，URL安全的\n\t *\n\t * @param arr     被编码的数组\n\t * @param lineSep 在76个char之后是CRLF还是EOF\n\t * @return 编码后的bytes\n\t * @since 3.0.6\n\t * @deprecated 按照RFC2045规范，URL安全的Base64无需换行\n\t */\n\t@Deprecated\n\tpublic static byte[] encodeUrlSafe(byte[] arr, boolean lineSep) {\n\t\treturn Base64Encoder.encodeUrlSafe(arr, lineSep);\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source) {\n\t\treturn encode(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64编码，URL安全\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(CharSequence source) {\n\t\treturn encodeUrlSafe(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source, String charset) {\n\t\treturn encode(source, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * base64编码，不进行padding(末尾不会填充'=')\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 编码\n\t * @return 被加密后的字符串\n\t * @since 5.5.2\n\t */\n\tpublic static String encodeWithoutPadding(CharSequence source, String charset) {\n\t\treturn encodeWithoutPadding(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * base64编码,URL安全\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t * @deprecated 请使用 {@link #encodeUrlSafe(CharSequence, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String encodeUrlSafe(CharSequence source, String charset) {\n\t\treturn encodeUrlSafe(source, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被编码后的字符串\n\t */\n\tpublic static String encode(CharSequence source, Charset charset) {\n\t\treturn encode(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * base64编码，URL安全的\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(CharSequence source, Charset charset) {\n\t\treturn encodeUrlSafe(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(byte[] source) {\n\t\tif (source == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn java.util.Base64.getEncoder().encodeToString(source);\n\t}\n\n\t/**\n\t * base64编码，不进行padding(末尾不会填充'=')\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t * @since 5.5.2\n\t */\n\tpublic static String encodeWithoutPadding(byte[] source) {\n\t\tif (source == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn java.util.Base64.getEncoder().withoutPadding().encodeToString(source);\n\t}\n\n\t/**\n\t * base64编码,URL安全的\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(byte[] source) {\n\t\tif (source == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(source);\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param in 被编码base64的流（一般为图片流或者文件流）\n\t * @return 被加密后的字符串\n\t * @since 4.0.9\n\t */\n\tpublic static String encode(InputStream in) {\n\t\treturn encode(IoUtil.readBytes(in));\n\t}\n\n\t/**\n\t * base64编码,URL安全的\n\t *\n\t * @param in 被编码base64的流（一般为图片流或者文件流）\n\t * @return 被加密后的字符串\n\t * @since 4.0.9\n\t */\n\tpublic static String encodeUrlSafe(InputStream in) {\n\t\treturn encodeUrlSafe(IoUtil.readBytes(in));\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param file 被编码base64的文件\n\t * @return 被加密后的字符串\n\t * @since 4.0.9\n\t */\n\tpublic static String encode(File file) {\n\t\treturn encode(FileUtil.readBytes(file));\n\t}\n\n\t/**\n\t * base64编码,URL安全的\n\t *\n\t * @param file 被编码base64的文件\n\t * @return 被加密后的字符串\n\t * @since 4.0.9\n\t */\n\tpublic static String encodeUrlSafe(File file) {\n\t\treturn encodeUrlSafe(FileUtil.readBytes(file));\n\t}\n\n\t/**\n\t * 编码为Base64字符串<br>\n\t * 如果isMultiLine为{@code true}，则每76个字符一个换行符，否则在一行显示\n\t *\n\t * @param arr         被编码的数组\n\t * @param isMultiLine 在76个char之后是CRLF还是EOF\n\t * @param isUrlSafe   是否使用URL安全字符，一般为{@code false}\n\t * @return 编码后的bytes\n\t * @since 5.7.2\n\t */\n\tpublic static String encodeStr(byte[] arr, boolean isMultiLine, boolean isUrlSafe) {\n\t\treturn StrUtil.str(encode(arr, isMultiLine, isUrlSafe), DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 编码为Base64<br>\n\t * 如果isMultiLine为{@code true}，则每76个字符一个换行符，否则在一行显示\n\t *\n\t * @param arr         被编码的数组\n\t * @param isMultiLine 在76个char之后是CRLF还是EOF\n\t * @param isUrlSafe   是否使用URL安全字符，一般为{@code false}\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encode(byte[] arr, boolean isMultiLine, boolean isUrlSafe) {\n\t\treturn Base64Encoder.encode(arr, isMultiLine, isUrlSafe);\n\t}\n\n\t// -------------------------------------------------------------------- decode\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source 被解码的base64字符串\n\t * @return 密文解密的结果\n\t * @since 4.3.2\n\t */\n\tpublic static String decodeStrGbk(CharSequence source) {\n\t\treturn Base64Decoder.decodeStr(source, CharsetUtil.CHARSET_GBK);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source 被解码的base64字符串\n\t * @return 密文解密的结果\n\t */\n\tpublic static String decodeStr(CharSequence source) {\n\t\treturn Base64Decoder.decodeStr(source);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source  被解码的base64字符串\n\t * @param charset 字符集\n\t * @return 密文解密的结果\n\t */\n\tpublic static String decodeStr(CharSequence source, String charset) {\n\t\treturn decodeStr(source, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source  被解码的base64字符串\n\t * @param charset 字符集\n\t * @return 密文解密的结果\n\t */\n\tpublic static String decodeStr(CharSequence source, Charset charset) {\n\t\treturn Base64Decoder.decodeStr(source, charset);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param base64   被解码的base64字符串\n\t * @param destFile 目标文件\n\t * @return 目标文件\n\t * @since 4.0.9\n\t */\n\tpublic static File decodeToFile(CharSequence base64, File destFile) {\n\t\treturn FileUtil.writeBytes(Base64Decoder.decode(base64), destFile);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param base64     被解码的base64字符串\n\t * @param out        写出到的流\n\t * @param isCloseOut 是否关闭输出流\n\t * @since 4.0.9\n\t */\n\tpublic static void decodeToStream(CharSequence base64, OutputStream out, boolean isCloseOut) {\n\t\tIoUtil.write(out, isCloseOut, Base64Decoder.decode(base64));\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param base64 被解码的base64字符串\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(CharSequence base64) {\n\t\treturn Base64Decoder.decode(base64);\n\t}\n\n\t/**\n\t * 解码Base64\n\t *\n\t * @param in 输入\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(byte[] in) {\n\t\treturn Base64Decoder.decode(in);\n\t}\n\n\t/**\n\t * 检查是否为Base64\n\t *\n\t * @param base64 Base64的bytes\n\t * @return 是否为Base64\n\t * @since 5.7.5\n\t */\n\tpublic static boolean isBase64(CharSequence base64) {\n\t\tif (base64 == null || base64.length() < 2) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal byte[] bytes = StrUtil.utf8Bytes(base64);\n\n\t\tif (bytes.length != base64.length()) {\n\t\t\t// 如果长度不相等，说明存在双字节字符，肯定不是Base64，直接返回false\n\t\t\treturn false;\n\t\t}\n\n\t\treturn isBase64(bytes);\n\t}\n\n\t/**\n\t * 检查是否为Base64\n\t *\n\t * @param base64Bytes Base64的bytes\n\t * @return 是否为Base64\n\t * @since 5.7.5\n\t */\n\tpublic static boolean isBase64(byte[] base64Bytes) {\n\t\tif (base64Bytes == null || base64Bytes.length < 3) {\n\t\t\treturn false;\n\t\t}\n\t\tboolean hasPadding = false;\n\t\tfor (byte base64Byte : base64Bytes) {\n\t\t\tif (hasPadding) {\n\t\t\t\tif ('=' != base64Byte) {\n\t\t\t\t\t// 前一个字符是'='，则后边的字符都必须是'='，即'='只能都位于结尾\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else if ('=' == base64Byte) {\n\t\t\t\t// 发现'=' 标记之\n\t\t\t\thasPadding = true;\n\t\t\t} else if (false == (Base64Decoder.isBase64Code(base64Byte) || isWhiteSpace(base64Byte))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tprivate static boolean isWhiteSpace(byte byteToCheck) {\n\t\tswitch (byteToCheck) {\n\t\t\tcase ' ':\n\t\t\tcase '\\n':\n\t\t\tcase '\\r':\n\t\t\tcase '\\t':\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.lang.mutable.MutableInt;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * Base64解码实现\n *\n * @author looly\n *\n */\npublic class Base64Decoder {\n\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\tprivate static final byte PADDING = -2;\n\n\t/** Base64解码表，共128位，-1表示非base64字符，-2表示padding */\n\tprivate static final byte[] DECODE_TABLE = {\n\t\t\t// 0 1 2 3 4 5 6 7 8 9 A B C D E F\n\t\t\t-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f\n\t\t\t-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f\n\t\t\t-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /\n\t\t\t52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, // 30-3f 0-9，-2的位置是'='\n\t\t\t-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O\n\t\t\t15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _\n\t\t\t-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o\n\t\t\t41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z\n\t};\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source 被解码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(CharSequence source) {\n\t\treturn decodeStr(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source 被解码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String decodeStr(CharSequence source, Charset charset) {\n\t\treturn StrUtil.str(decode(source), charset);\n\t}\n\n\t/**\n\t * base64解码\n\t *\n\t * @param source 被解码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static byte[] decode(CharSequence source) {\n\t\treturn decode(StrUtil.bytes(source, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * 解码Base64\n\t *\n\t * @param in 输入\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(byte[] in) {\n\t\tif (ArrayUtil.isEmpty(in)) {\n\t\t\treturn in;\n\t\t}\n\t\treturn decode(in, 0, in.length);\n\t}\n\n\t/**\n\t * 解码Base64\n\t *\n\t * @param in 输入\n\t * @param pos 开始位置\n\t * @param length 长度\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(byte[] in, int pos, int length) {\n\t\tif (ArrayUtil.isEmpty(in)) {\n\t\t\treturn in;\n\t\t}\n\n\t\tfinal MutableInt offset = new MutableInt(pos);\n\n\t\tbyte sestet0;\n\t\tbyte sestet1;\n\t\tbyte sestet2;\n\t\tbyte sestet3;\n\t\tint maxPos = pos + length - 1;\n\t\tint octetId = 0;\n\t\tbyte[] octet = new byte[length * 3 / 4];// over-estimated if non-base64 characters present\n\t\twhile (offset.intValue() <= maxPos) {\n\t\t\tsestet0 = getNextValidDecodeByte(in, offset, maxPos);\n\t\t\tsestet1 = getNextValidDecodeByte(in, offset, maxPos);\n\t\t\tsestet2 = getNextValidDecodeByte(in, offset, maxPos);\n\t\t\tsestet3 = getNextValidDecodeByte(in, offset, maxPos);\n\n\t\t\tif (PADDING != sestet1) {\n\t\t\t\toctet[octetId++] = (byte) ((sestet0 << 2) | (sestet1 >>> 4));\n\t\t\t}\n\t\t\tif (PADDING != sestet2) {\n\t\t\t\toctet[octetId++] = (byte) (((sestet1 & 0xf) << 4) | (sestet2 >>> 2));\n\t\t\t}\n\t\t\tif (PADDING != sestet3) {\n\t\t\t\toctet[octetId++] = (byte) (((sestet2 & 3) << 6) | sestet3);\n\t\t\t}\n\t\t}\n\n\t\tif (octetId == octet.length) {\n\t\t\treturn octet;\n\t\t} else {\n\t\t\t// 如果有非Base64字符混入，则实际结果比解析的要短，截取之\n\t\t\treturn (byte[]) ArrayUtil.copy(octet, new byte[octetId], octetId);\n\t\t}\n\t}\n\n\t/**\n\t * 给定的字符是否为Base64字符\n\t *\n\t * @param octet 被检查的字符\n\t * @return 是否为Base64字符\n\t * @since 5.7.5\n\t */\n\tpublic static boolean isBase64Code(byte octet) {\n\t\treturn octet == '=' || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- Private start\n\t/**\n\t * 获取下一个有效的byte字符\n\t *\n\t * @param in 输入\n\t * @param pos 当前位置，调用此方法后此位置保持在有效字符的下一个位置\n\t * @param maxPos 最大位置\n\t * @return 有效字符，如果达到末尾返回\n\t */\n\tprivate static byte getNextValidDecodeByte(byte[] in, MutableInt pos, int maxPos) {\n\t\tbyte base64Byte;\n\t\tbyte decodeByte;\n\t\twhile (pos.intValue() <= maxPos) {\n\t\t\tbase64Byte = in[pos.intValue()];\n\t\t\tpos.increment();\n\t\t\tif (base64Byte > -1) {\n\t\t\t\tdecodeByte = DECODE_TABLE[base64Byte];\n\t\t\t\tif (decodeByte > -1) {\n\t\t\t\t\treturn decodeByte;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// padding if reached max position\n\t\treturn PADDING;\n\t}\n\t// ----------------------------------------------------------------------------------------------- Private end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * Base64编码<br>\n * TODO 6.x移除此类，使用JDK自身\n *\n * @author looly\n * @since 3.2.0\n */\npublic class Base64Encoder {\n\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\t/**\n\t * 标准编码表\n\t */\n\tprivate static final byte[] STANDARD_ENCODE_TABLE = { //\n\t\t\t'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //\n\t\t\t'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', //\n\t\t\t'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', //\n\t\t\t'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', //\n\t\t\t'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', //\n\t\t\t'o', 'p', 'q', 'r', 's', 't', 'u', 'v', //\n\t\t\t'w', 'x', 'y', 'z', '0', '1', '2', '3', //\n\t\t\t'4', '5', '6', '7', '8', '9', '+', '/' //\n\t};\n\t/**\n\t * URL安全的编码表，将 + 和 / 替换为 - 和 _\n\t */\n\tprivate static final byte[] URL_SAFE_ENCODE_TABLE = { //\n\t\t\t'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //\n\t\t\t'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', //\n\t\t\t'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', //\n\t\t\t'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', //\n\t\t\t'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', //\n\t\t\t'o', 'p', 'q', 'r', 's', 't', 'u', 'v', //\n\t\t\t'w', 'x', 'y', 'z', '0', '1', '2', '3', //\n\t\t\t'4', '5', '6', '7', '8', '9', '-', '_' //\n\t};\n\n\t// -------------------------------------------------------------------- encode\n\n\t/**\n\t * 编码为Base64，非URL安全的\n\t *\n\t * @param arr     被编码的数组\n\t * @param lineSep 在76个char之后是CRLF还是EOF\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encode(byte[] arr, boolean lineSep) {\n\t\treturn encode(arr, lineSep, false);\n\t}\n\n\t/**\n\t * 编码为Base64，URL安全的\n\t *\n\t * @param arr     被编码的数组\n\t * @param lineSep 在76个char之后是CRLF还是EOF\n\t * @return 编码后的bytes\n\t * @since 3.0.6\n\t */\n\tpublic static byte[] encodeUrlSafe(byte[] arr, boolean lineSep) {\n\t\treturn encode(arr, lineSep, true);\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source) {\n\t\treturn encode(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64编码，URL安全\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(CharSequence source) {\n\t\treturn encodeUrlSafe(source, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(CharSequence source, Charset charset) {\n\t\treturn encode(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * base64编码，URL安全的\n\t *\n\t * @param source  被编码的base64字符串\n\t * @param charset 字符集\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(CharSequence source, Charset charset) {\n\t\treturn encodeUrlSafe(StrUtil.bytes(source, charset));\n\t}\n\n\t/**\n\t * base64编码\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t */\n\tpublic static String encode(byte[] source) {\n\t\treturn StrUtil.str(encode(source, false), DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * base64编码,URL安全的\n\t *\n\t * @param source 被编码的base64字符串\n\t * @return 被加密后的字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String encodeUrlSafe(byte[] source) {\n\t\treturn StrUtil.str(encodeUrlSafe(source, false), DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 编码为Base64字符串<br>\n\t * 如果isMultiLine为{@code true}，则每76个字符一个换行符，否则在一行显示\n\t *\n\t * @param arr         被编码的数组\n\t * @param isMultiLine 在76个char之后是CRLF还是EOF\n\t * @param isUrlSafe   是否使用URL安全字符，在URL Safe模式下，=为URL中的关键字符，不需要补充。空余的byte位要去掉，一般为{@code false}\n\t * @return 编码后的bytes\n\t * @since 5.7.2\n\t */\n\tpublic static String encodeStr(byte[] arr, boolean isMultiLine, boolean isUrlSafe) {\n\t\treturn StrUtil.str(encode(arr, isMultiLine, isUrlSafe), DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 编码为Base64<br>\n\t * 如果isMultiLine为{@code true}，则每76个字符一个换行符，否则在一行显示\n\t *\n\t * @param arr         被编码的数组\n\t * @param isMultiLine 在76个char之后是CRLF还是EOF\n\t * @param isUrlSafe   是否使用URL安全字符，在URL Safe模式下，=为URL中的关键字符，不需要补充。空余的byte位要去掉，一般为{@code false}\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encode(byte[] arr, boolean isMultiLine, boolean isUrlSafe) {\n\t\tif (null == arr) {\n\t\t\treturn null;\n\t\t}\n\n\t\tint len = arr.length;\n\t\tif (len == 0) {\n\t\t\treturn new byte[0];\n\t\t}\n\n\t\tint evenlen = (len / 3) * 3;\n\t\tint cnt = ((len - 1) / 3 + 1) << 2;\n\t\tint destlen = cnt + (isMultiLine ? (cnt - 1) / 76 << 1 : 0);\n\t\tbyte[] dest = new byte[destlen];\n\n\t\tbyte[] encodeTable = isUrlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;\n\n\t\tfor (int s = 0, d = 0, cc = 0; s < evenlen; ) {\n\t\t\tint i = (arr[s++] & 0xff) << 16 | (arr[s++] & 0xff) << 8 | (arr[s++] & 0xff);\n\n\t\t\tdest[d++] = encodeTable[(i >>> 18) & 0x3f];\n\t\t\tdest[d++] = encodeTable[(i >>> 12) & 0x3f];\n\t\t\tdest[d++] = encodeTable[(i >>> 6) & 0x3f];\n\t\t\tdest[d++] = encodeTable[i & 0x3f];\n\n\t\t\tif (isMultiLine && ++cc == 19 && d < destlen - 2) {\n\t\t\t\tdest[d++] = '\\r';\n\t\t\t\tdest[d++] = '\\n';\n\t\t\t\tcc = 0;\n\t\t\t}\n\t\t}\n\n\t\tint left = len - evenlen;// 剩余位数\n\t\tif (left > 0) {\n\t\t\tint i = ((arr[evenlen] & 0xff) << 10) | (left == 2 ? ((arr[len - 1] & 0xff) << 2) : 0);\n\n\t\t\tdest[destlen - 4] = encodeTable[i >> 12];\n\t\t\tdest[destlen - 3] = encodeTable[(i >>> 6) & 0x3f];\n\n\t\t\tif (isUrlSafe) {\n\t\t\t\t// 在URL Safe模式下，=为URL中的关键字符，不需要补充。空余的byte位要去掉。\n\t\t\t\tint urlSafeLen = destlen - 2;\n\t\t\t\tif (2 == left) {\n\t\t\t\t\tdest[destlen - 2] = encodeTable[i & 0x3f];\n\t\t\t\t\turlSafeLen += 1;\n\t\t\t\t}\n\t\t\t\tbyte[] urlSafeDest = new byte[urlSafeLen];\n\t\t\t\tSystem.arraycopy(dest, 0, urlSafeDest, 0, urlSafeLen);\n\t\t\t\treturn urlSafeDest;\n\t\t\t} else {\n\t\t\t\tdest[destlen - 2] = (left == 2) ? encodeTable[i & 0x3f] : (byte) '=';\n\t\t\t\tdest[destlen - 1] = '=';\n\t\t\t}\n\t\t}\n\t\treturn dest;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * 凯撒密码实现<br>\n * 算法来自：https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption\n *\n * @author looly\n */\npublic class Caesar {\n\n\t// 26个字母表\n\tpublic static final String TABLE = \"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz\";\n\n\t/**\n\t * 传入明文，加密得到密文\n\t *\n\t * @param message 加密的消息\n\t * @param offset  偏移量\n\t * @return 加密后的内容\n\t */\n\tpublic static String encode(String message, int offset) {\n\t\tAssert.notNull(message, \"message must be not null!\");\n\t\tfinal int len = message.length();\n\t\tfinal char[] plain = message.toCharArray();\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = message.charAt(i);\n\t\t\tif (false == Character.isLetter(c)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tplain[i] = encodeChar(c, offset);\n\t\t}\n\t\treturn new String(plain);\n\t}\n\n\t/**\n\t * 传入明文解密到密文\n\t *\n\t * @param cipherText 密文\n\t * @param offset     偏移量\n\t * @return 解密后的内容\n\t */\n\tpublic static String decode(String cipherText, int offset) {\n\t\tAssert.notNull(cipherText, \"cipherText must be not null!\");\n\t\tfinal int len = cipherText.length();\n\t\tfinal char[] plain = cipherText.toCharArray();\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = cipherText.charAt(i);\n\t\t\tif (false == Character.isLetter(c)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tplain[i] = decodeChar(c, offset);\n\t\t}\n\t\treturn new String(plain);\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 加密轮盘\n\t *\n\t * @param c      被加密字符\n\t * @param offset 偏移量\n\t * @return 加密后的字符\n\t */\n\tprivate static char encodeChar(char c, int offset) {\n\t\tint position = (TABLE.indexOf(c) + offset) % 52;\n\t\treturn TABLE.charAt(position);\n\n\t}\n\n\t/**\n\t * 解密轮盘\n\t *\n\t * @param c 字符\n\t * @return 解密后的字符\n\t */\n\tprivate static char decodeChar(char c, int offset) {\n\t\tint position = (TABLE.indexOf(c) - offset) % 52;\n\t\tif (position < 0) {\n\t\t\tposition += 52;\n\t\t}\n\t\treturn TABLE.charAt(position);\n\t}\n\t// ----------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java",
    "content": "package cn.hutool.core.codec;\n\n/**\n * 解码接口\n *\n * @param <T> 被解码的数据类型\n * @param <R> 解码后的数据类型\n * @author looly\n * @since 5.7.22\n */\npublic interface Decoder<T, R> {\n\n\t/**\n\t * 执行解码\n\t *\n\t * @param encoded 被解码的数据\n\t * @return 解码后的数据\n\t */\n\tR decode(T encoded);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java",
    "content": "package cn.hutool.core.codec;\n\n/**\n * 编码接口\n *\n * @param <T> 被编码的数据类型\n * @param <R> 编码后的数据类型\n * @author looly\n * @since 5.7.22\n */\npublic interface Encoder<T, R> {\n\n\t/**\n\t * 执行编码\n\t *\n\t * @param data 被编码的数据\n\t * @return 编码后的数据\n\t */\n\tR encode(T data);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Hashids.java",
    "content": "package cn.hutool.core.codec;\n\nimport java.math.BigInteger;\nimport java.util.Arrays;\nimport java.util.LinkedHashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.LongStream;\n\n/**\n * <a href=\"http://hashids.org/\">Hashids</a> 协议实现，以实现：\n * <ul>\n * <li>生成简短、唯一、大小写敏感并无序的hash值</li>\n * <li>自然数字的Hash值</li>\n * <li>可以设置不同的盐，具有保密性</li>\n * <li>可配置的hash长度</li>\n * <li>递增的输入产生的输出无法预测</li>\n * </ul>\n *\n * <p>\n * 来自：<a href=\"https://github.com/davidafsilva/java-hashids\">https://github.com/davidafsilva/java-hashids</a>\n * </p>\n *\n * <p>\n * {@code Hashids}可以将数字或者16进制字符串转为短且唯一不连续的字符串，采用双向编码实现，比如，它可以将347之类的数字转换为yr8之类的字符串，也可以将yr8之类的字符串重新解码为347之类的数字。<br>\n * 此编码算法主要是解决爬虫类应用对连续ID爬取问题，将有序的ID转换为无序的Hashids，而且一一对应。\n * </p>\n *\n * @author david\n */\npublic class Hashids implements Encoder<long[], String>, Decoder<String, long[]> {\n\n\tprivate static final int LOTTERY_MOD = 100;\n\tprivate static final double GUARD_THRESHOLD = 12;\n\tprivate static final double SEPARATOR_THRESHOLD = 3.5;\n\t// 最小编解码字符串\n\tprivate static final int MIN_ALPHABET_LENGTH = 16;\n\tprivate static final Pattern HEX_VALUES_PATTERN = Pattern.compile(\"[\\\\w\\\\W]{1,12}\");\n\n\t// 默认编解码字符串\n\tpublic static final char[] DEFAULT_ALPHABET = {\n\t\t\t'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',\n\t\t\t'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',\n\t\t\t'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',\n\t\t\t'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',\n\t\t\t'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'\n\t};\n\t// 默认分隔符\n\tprivate static final char[] DEFAULT_SEPARATORS = {\n\t\t\t'c', 'f', 'h', 'i', 's', 't', 'u', 'C', 'F', 'H', 'I', 'S', 'T', 'U'\n\t};\n\n\t// algorithm properties\n\tprivate final char[] alphabet;\n\t// 多个数字编解码的分界符\n\tprivate final char[] separators;\n\tprivate final Set<Character> separatorsSet;\n\tprivate final char[] salt;\n\t// 补齐至 minLength 长度添加的字符列表\n\tprivate final char[] guards;\n\t// 编码后最小的字符长度\n\tprivate final int minLength;\n\n\t// region create\n\n\t/**\n\t * 根据参数值，创建{@code Hashids}，使用默认{@link #DEFAULT_ALPHABET}作为字母表，不限制最小长度\n\t *\n\t * @param salt 加盐值\n\t * @return {@code Hashids}\n\t */\n\tpublic static Hashids create(final char[] salt) {\n\t\treturn create(salt, DEFAULT_ALPHABET, -1);\n\t}\n\n\t/**\n\t * 根据参数值，创建{@code Hashids}，使用默认{@link #DEFAULT_ALPHABET}作为字母表\n\t *\n\t * @param salt      加盐值\n\t * @param minLength 限制最小长度，-1表示不限制\n\t * @return {@code Hashids}\n\t */\n\tpublic static Hashids create(final char[] salt, final int minLength) {\n\t\treturn create(salt, DEFAULT_ALPHABET, minLength);\n\t}\n\n\t/**\n\t * 根据参数值，创建{@code Hashids}\n\t *\n\t * @param salt      加盐值\n\t * @param alphabet  hash字母表\n\t * @param minLength 限制最小长度，-1表示不限制\n\t * @return {@code Hashids}\n\t */\n\tpublic static Hashids create(final char[] salt, final char[] alphabet, final int minLength) {\n\t\treturn new Hashids(salt, alphabet, minLength);\n\t}\n\t// endregion\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt      加盐值\n\t * @param alphabet  hash字母表\n\t * @param minLength 限制最小长度，-1表示不限制\n\t */\n\tpublic Hashids(final char[] salt, final char[] alphabet, final int minLength) {\n\t\tthis.minLength = minLength;\n\t\tthis.salt = Arrays.copyOf(salt, salt.length);\n\n\t\t// filter and shuffle separators\n\t\tchar[] tmpSeparators = shuffle(filterSeparators(DEFAULT_SEPARATORS, alphabet), this.salt);\n\n\t\t// validate and filter the alphabet\n\t\tchar[] tmpAlphabet = validateAndFilterAlphabet(alphabet, tmpSeparators);\n\n\t\t// check separator threshold\n\t\tif (tmpSeparators.length == 0 ||\n\t\t\t\t((double) (tmpAlphabet.length / tmpSeparators.length)) > SEPARATOR_THRESHOLD) {\n\t\t\tfinal int minSeparatorsSize = (int) Math.ceil(tmpAlphabet.length / SEPARATOR_THRESHOLD);\n\t\t\t// check minimum size of separators\n\t\t\tif (minSeparatorsSize > tmpSeparators.length) {\n\t\t\t\t// fill separators from alphabet\n\t\t\t\tfinal int missingSeparators = minSeparatorsSize - tmpSeparators.length;\n\t\t\t\ttmpSeparators = Arrays.copyOf(tmpSeparators, tmpSeparators.length + missingSeparators);\n\t\t\t\tSystem.arraycopy(tmpAlphabet, 0, tmpSeparators,\n\t\t\t\t\t\ttmpSeparators.length - missingSeparators, missingSeparators);\n\t\t\t\tSystem.arraycopy(tmpAlphabet, 0, tmpSeparators,\n\t\t\t\t\t\ttmpSeparators.length - missingSeparators, missingSeparators);\n\t\t\t\ttmpAlphabet = Arrays.copyOfRange(tmpAlphabet, missingSeparators, tmpAlphabet.length);\n\t\t\t}\n\t\t}\n\n\t\t// shuffle the current alphabet\n\t\tshuffle(tmpAlphabet, this.salt);\n\n\t\t// check guards\n\t\tthis.guards = new char[(int) Math.ceil(tmpAlphabet.length / GUARD_THRESHOLD)];\n\t\tif (alphabet.length < 3) {\n\t\t\tSystem.arraycopy(tmpSeparators, 0, guards, 0, guards.length);\n\t\t\tthis.separators = Arrays.copyOfRange(tmpSeparators, guards.length, tmpSeparators.length);\n\t\t\tthis.alphabet = tmpAlphabet;\n\t\t} else {\n\t\t\tSystem.arraycopy(tmpAlphabet, 0, guards, 0, guards.length);\n\t\t\tthis.separators = tmpSeparators;\n\t\t\tthis.alphabet = Arrays.copyOfRange(tmpAlphabet, guards.length, tmpAlphabet.length);\n\t\t}\n\n\t\t// create the separators set\n\t\tseparatorsSet = IntStream.range(0, separators.length)\n\t\t\t\t.mapToObj(idx -> separators[idx])\n\t\t\t\t.collect(Collectors.toSet());\n\t}\n\n\t/**\n\t * 编码给定的16进制数字\n\t *\n\t * @param hexNumbers 16进制数字\n\t * @return 编码后的值, {@code null} if {@code numbers} 是 {@code null}.\n\t * @throws IllegalArgumentException 数字不支持抛出此异常\n\t */\n\tpublic String encodeFromHex(final String hexNumbers) {\n\t\tif (hexNumbers == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// remove the prefix, if present\n\t\tfinal String hex = hexNumbers.startsWith(\"0x\") || hexNumbers.startsWith(\"0X\") ?\n\t\t\t\thexNumbers.substring(2) : hexNumbers;\n\n\t\t// get the associated long value and encode it\n\t\tLongStream values = LongStream.empty();\n\t\tfinal Matcher matcher = HEX_VALUES_PATTERN.matcher(hex);\n\t\twhile (matcher.find()) {\n\t\t\tfinal long value = new BigInteger(\"1\" + matcher.group(), 16).longValue();\n\t\t\tvalues = LongStream.concat(values, LongStream.of(value));\n\t\t}\n\n\t\treturn encode(values.toArray());\n\t}\n\n\t/**\n\t * 编码给定的数字数组\n\t *\n\t * @param numbers 数字数组\n\t * @return 编码后的值, {@code null} if {@code numbers} 是 {@code null}.\n\t * @throws IllegalArgumentException 数字不支持抛出此异常\n\t */\n\t@Override\n\tpublic String encode(final long... numbers) {\n\t\tif (numbers == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// copy alphabet\n\t\tfinal char[] currentAlphabet = Arrays.copyOf(alphabet, alphabet.length);\n\n\t\t// determine the lottery number\n\t\tfinal long lotteryId = LongStream.range(0, numbers.length)\n\t\t\t\t.reduce(0, (state, i) -> {\n\t\t\t\t\tfinal long number = numbers[(int) i];\n\t\t\t\t\tif (number < 0) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"invalid number: \" + number);\n\t\t\t\t\t}\n\t\t\t\t\treturn state + number % (i + LOTTERY_MOD);\n\t\t\t\t});\n\t\tfinal char lottery = currentAlphabet[(int) (lotteryId % currentAlphabet.length)];\n\n\t\t// encode each number\n\t\tfinal StringBuilder global = new StringBuilder();\n\t\tIntStream.range(0, numbers.length)\n\t\t\t\t.forEach(idx -> {\n\t\t\t\t\t// derive alphabet\n\t\t\t\t\tderiveNewAlphabet(currentAlphabet, salt, lottery);\n\n\t\t\t\t\t// encode\n\t\t\t\t\tfinal int initialLength = global.length();\n\t\t\t\t\ttranslate(numbers[idx], currentAlphabet, global, initialLength);\n\n\t\t\t\t\t// prepend the lottery\n\t\t\t\t\tif (idx == 0) {\n\t\t\t\t\t\tglobal.insert(0, lottery);\n\t\t\t\t\t}\n\n\t\t\t\t\t// append the separator, if more numbers are pending encoding\n\t\t\t\t\tif (idx + 1 < numbers.length) {\n\t\t\t\t\t\tlong n = numbers[idx] % (global.charAt(initialLength) + 1);\n\t\t\t\t\t\tglobal.append(separators[(int) (n % separators.length)]);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t// add the guards, if there's any space left\n\t\tif (minLength > global.length()) {\n\t\t\tint guardIdx = (int) ((lotteryId + lottery) % guards.length);\n\t\t\tglobal.insert(0, guards[guardIdx]);\n\t\t\tif (minLength > global.length()) {\n\t\t\t\tguardIdx = (int) ((lotteryId + global.charAt(2)) % guards.length);\n\t\t\t\tglobal.append(guards[guardIdx]);\n\t\t\t}\n\t\t}\n\n\t\t// add the necessary padding\n\t\tint paddingLeft = minLength - global.length();\n\t\twhile (paddingLeft > 0) {\n\t\t\tshuffle(currentAlphabet, Arrays.copyOf(currentAlphabet, currentAlphabet.length));\n\n\t\t\tfinal int alphabetHalfSize = currentAlphabet.length / 2;\n\t\t\tfinal int initialSize = global.length();\n\t\t\tif (paddingLeft > currentAlphabet.length) {\n\t\t\t\t// entire alphabet with the current encoding in the middle of it\n\t\t\t\tint offset = alphabetHalfSize + (currentAlphabet.length % 2 == 0 ? 0 : 1);\n\n\t\t\t\tglobal.insert(0, currentAlphabet, alphabetHalfSize, offset);\n\t\t\t\tglobal.insert(offset + initialSize, currentAlphabet, 0, alphabetHalfSize);\n\t\t\t\t// decrease the padding left\n\t\t\t\tpaddingLeft -= currentAlphabet.length;\n\t\t\t} else {\n\t\t\t\t// calculate the excess\n\t\t\t\tfinal int excess = currentAlphabet.length + global.length() - minLength;\n\t\t\t\tfinal int secondHalfStartOffset = alphabetHalfSize + Math.floorDiv(excess, 2);\n\t\t\t\tfinal int secondHalfLength = currentAlphabet.length - secondHalfStartOffset;\n\t\t\t\tfinal int firstHalfLength = paddingLeft - secondHalfLength;\n\n\t\t\t\tglobal.insert(0, currentAlphabet, secondHalfStartOffset, secondHalfLength);\n\t\t\t\tglobal.insert(secondHalfLength + initialSize, currentAlphabet, 0, firstHalfLength);\n\n\t\t\t\tpaddingLeft = 0;\n\t\t\t}\n\t\t}\n\n\t\treturn global.toString();\n\t}\n\n\t//-------------------------\n\t// Decode\n\t//-------------------------\n\n\t/**\n\t * 解码Hash值为16进制数字\n\t *\n\t * @param hash hash值\n\t * @return 解码后的16进制值, {@code null} if {@code numbers} 是 {@code null}.\n\t * @throws IllegalArgumentException if the hash is invalid.\n\t */\n\tpublic String decodeToHex(final String hash) {\n\t\tif (hash == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tArrays.stream(decode(hash))\n\t\t\t\t.mapToObj(Long::toHexString)\n\t\t\t\t.forEach(hex -> sb.append(hex, 1, hex.length()));\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 解码Hash值为数字数组\n\t *\n\t * @param hash hash值\n\t * @return 解码后的16进制值, {@code null} if {@code numbers} 是 {@code null}.\n\t * @throws IllegalArgumentException if the hash is invalid.\n\t */\n\t@Override\n\tpublic long[] decode(final String hash) {\n\t\tif (hash == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// create a set of the guards\n\t\tfinal Set<Character> guardsSet = IntStream.range(0, guards.length)\n\t\t\t\t.mapToObj(idx -> guards[idx])\n\t\t\t\t.collect(Collectors.toSet());\n\t\t// count the total guards used\n\t\tfinal int[] guardsIdx = IntStream.range(0, hash.length())\n\t\t\t\t.filter(idx -> guardsSet.contains(hash.charAt(idx)))\n\t\t\t\t.toArray();\n\t\t// get the start/end index base on the guards count\n\t\tfinal int startIdx, endIdx;\n\t\tif (guardsIdx.length > 0) {\n\t\t\tstartIdx = guardsIdx[0] + 1;\n\t\t\tendIdx = guardsIdx.length > 1 ? guardsIdx[1] : hash.length();\n\t\t} else {\n\t\t\tstartIdx = 0;\n\t\t\tendIdx = hash.length();\n\t\t}\n\n\t\tLongStream decoded = LongStream.empty();\n\t\t// parse the hash\n\t\tif (hash.length() > 0) {\n\t\t\tfinal char lottery = hash.charAt(startIdx);\n\n\t\t\t// create the initial accumulation string\n\t\t\tfinal int length = hash.length() - guardsIdx.length - 1;\n\t\t\tStringBuilder block = new StringBuilder(length);\n\n\t\t\t// create the base salt\n\t\t\tfinal char[] decodeSalt = new char[alphabet.length];\n\t\t\tdecodeSalt[0] = lottery;\n\t\t\tfinal int saltLength = salt.length >= alphabet.length ? alphabet.length - 1 : salt.length;\n\t\t\tSystem.arraycopy(salt, 0, decodeSalt, 1, saltLength);\n\t\t\tfinal int saltLeft = alphabet.length - saltLength - 1;\n\n\t\t\t// copy alphabet\n\t\t\tfinal char[] currentAlphabet = Arrays.copyOf(alphabet, alphabet.length);\n\n\t\t\tfor (int i = startIdx + 1; i < endIdx; i++) {\n\t\t\t\tif (false == separatorsSet.contains(hash.charAt(i))) {\n\t\t\t\t\tblock.append(hash.charAt(i));\n\t\t\t\t\t// continue if we have not reached the end, yet\n\t\t\t\t\tif (i < endIdx - 1) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (block.length() > 0) {\n\t\t\t\t\t// create the salt\n\t\t\t\t\tif (saltLeft > 0) {\n\t\t\t\t\t\tSystem.arraycopy(currentAlphabet, 0, decodeSalt,\n\t\t\t\t\t\t\t\talphabet.length - saltLeft, saltLeft);\n\t\t\t\t\t}\n\n\t\t\t\t\t// shuffle the alphabet\n\t\t\t\t\tshuffle(currentAlphabet, decodeSalt);\n\n\t\t\t\t\t// prepend the decoded value\n\t\t\t\t\tfinal long n = translate(block.toString().toCharArray(), currentAlphabet);\n\t\t\t\t\tdecoded = LongStream.concat(decoded, LongStream.of(n));\n\n\t\t\t\t\t// create a new block\n\t\t\t\t\tblock = new StringBuilder(length);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// validate the hash\n\t\tfinal long[] decodedValue = decoded.toArray();\n\t\tif (!Objects.equals(hash, encode(decodedValue))) {\n\t\t\tthrow new IllegalArgumentException(\"invalid hash: \" + hash);\n\t\t}\n\n\t\treturn decodedValue;\n\t}\n\n\tprivate StringBuilder translate(final long n, final char[] alphabet,\n\t\t\t\t\t\t\t\t\tfinal StringBuilder sb, final int start) {\n\t\tlong input = n;\n\t\tdo {\n\t\t\t// prepend the chosen char\n\t\t\tsb.insert(start, alphabet[(int) (input % alphabet.length)]);\n\n\t\t\t// trim the input\n\t\t\tinput = input / alphabet.length;\n\t\t} while (input > 0);\n\n\t\treturn sb;\n\t}\n\n\tprivate long translate(final char[] hash, final char[] alphabet) {\n\t\tlong number = 0;\n\n\t\tfinal Map<Character, Integer> alphabetMapping = IntStream.range(0, alphabet.length)\n\t\t\t\t.mapToObj(idx -> new Object[]{alphabet[idx], idx})\n\t\t\t\t.collect(Collectors.groupingBy(arr -> (Character) arr[0],\n\t\t\t\t\t\tCollectors.mapping(arr -> (Integer) arr[1],\n\t\t\t\t\t\t\t\tCollectors.reducing(null, (a, b) -> a == null ? b : a))));\n\n\t\tfor (int i = 0; i < hash.length; ++i) {\n\t\t\tnumber += alphabetMapping.computeIfAbsent(hash[i], k -> {\n\t\t\t\tthrow new IllegalArgumentException(\"Invalid alphabet for hash\");\n\t\t\t}) * (long) Math.pow(alphabet.length, hash.length - i - 1);\n\t\t}\n\n\t\treturn number;\n\t}\n\n\tprivate char[] deriveNewAlphabet(final char[] alphabet, final char[] salt, final char lottery) {\n\t\t// create the new salt\n\t\tfinal char[] newSalt = new char[alphabet.length];\n\n\t\t// 1. lottery\n\t\tnewSalt[0] = lottery;\n\t\tint spaceLeft = newSalt.length - 1;\n\t\tint offset = 1;\n\t\t// 2. salt\n\t\tif (salt.length > 0 && spaceLeft > 0) {\n\t\t\tint length = Math.min(salt.length, spaceLeft);\n\t\t\tSystem.arraycopy(salt, 0, newSalt, offset, length);\n\t\t\tspaceLeft -= length;\n\t\t\toffset += length;\n\t\t}\n\t\t// 3. alphabet\n\t\tif (spaceLeft > 0) {\n\t\t\tSystem.arraycopy(alphabet, 0, newSalt, offset, spaceLeft);\n\t\t}\n\n\t\t// shuffle\n\t\treturn shuffle(alphabet, newSalt);\n\t}\n\n\tprivate char[] validateAndFilterAlphabet(final char[] alphabet, final char[] separators) {\n\t\t// validate size\n\t\tif (alphabet.length < MIN_ALPHABET_LENGTH) {\n\t\t\tthrow new IllegalArgumentException(String.format(\"alphabet must contain at least %d unique \" +\n\t\t\t\t\t\"characters: %d\", MIN_ALPHABET_LENGTH, alphabet.length));\n\t\t}\n\n\t\tfinal Set<Character> seen = new LinkedHashSet<>(alphabet.length);\n\t\tfinal Set<Character> invalid = IntStream.range(0, separators.length)\n\t\t\t\t.mapToObj(idx -> separators[idx])\n\t\t\t\t.collect(Collectors.toSet());\n\n\t\t// add to seen set (without duplicates)\n\t\tIntStream.range(0, alphabet.length)\n\t\t\t\t.forEach(i -> {\n\t\t\t\t\tif (alphabet[i] == ' ') {\n\t\t\t\t\t\tthrow new IllegalArgumentException(String.format(\"alphabet must not contain spaces: \" +\n\t\t\t\t\t\t\t\t\"index %d\", i));\n\t\t\t\t\t}\n\t\t\t\t\tfinal Character c = alphabet[i];\n\t\t\t\t\tif (!invalid.contains(c)) {\n\t\t\t\t\t\tseen.add(c);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t// create a new alphabet without the duplicates\n\t\tfinal char[] uniqueAlphabet = new char[seen.size()];\n\t\tint idx = 0;\n\t\tfor (char c : seen) {\n\t\t\tuniqueAlphabet[idx++] = c;\n\t\t}\n\t\treturn uniqueAlphabet;\n\t}\n\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate char[] filterSeparators(final char[] separators, final char[] alphabet) {\n\t\tfinal Set<Character> valid = IntStream.range(0, alphabet.length)\n\t\t\t\t.mapToObj(idx -> alphabet[idx])\n\t\t\t\t.collect(Collectors.toSet());\n\n\t\treturn IntStream.range(0, separators.length)\n\t\t\t\t.mapToObj(idx -> (separators[idx]))\n\t\t\t\t.filter(valid::contains)\n\t\t\t\t// ugly way to convert back to char[]\n\t\t\t\t.map(c -> Character.toString(c))\n\t\t\t\t.collect(Collectors.joining())\n\t\t\t\t.toCharArray();\n\t}\n\n\tprivate char[] shuffle(final char[] alphabet, final char[] salt) {\n\t\tfor (int i = alphabet.length - 1, v = 0, p = 0, j, z; salt.length > 0 && i > 0; i--, v++) {\n\t\t\tv %= salt.length;\n\t\t\tp += z = salt[v];\n\t\t\tj = (z + v + p) % i;\n\t\t\tfinal char tmp = alphabet[j];\n\t\t\talphabet[j] = alphabet[i];\n\t\t\talphabet[i] = tmp;\n\t\t}\n\t\treturn alphabet;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Morse.java",
    "content": "package cn.hutool.core.codec;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 莫尔斯电码的编码和解码实现<br>\n * 参考：https://github.com/TakWolf-Deprecated/Java-MorseCoder\n *\n * @author looly, TakWolf\n * @since 4.4.1\n */\npublic class Morse {\n\n\tprivate static final Map<Integer, String> ALPHABETS = new HashMap<>(); // code point -> morse\n\tprivate static final Map<String, Integer> DICTIONARIES = new HashMap<>(); // morse -> code point\n\n\t/**\n\t * 注册莫尔斯电码表\n\t *\n\t * @param abc 字母和字符\n\t * @param dict 二进制\n\t */\n\tprivate static void registerMorse(Character abc, String dict) {\n\t\tALPHABETS.put((int) abc, dict);\n\t\tDICTIONARIES.put(dict, (int) abc);\n\t}\n\n\tstatic {\n\t\t// Letters\n\t\tregisterMorse('A', \"01\");\n\t\tregisterMorse('B', \"1000\");\n\t\tregisterMorse('C', \"1010\");\n\t\tregisterMorse('D', \"100\");\n\t\tregisterMorse('E', \"0\");\n\t\tregisterMorse('F', \"0010\");\n\t\tregisterMorse('G', \"110\");\n\t\tregisterMorse('H', \"0000\");\n\t\tregisterMorse('I', \"00\");\n\t\tregisterMorse('J', \"0111\");\n\t\tregisterMorse('K', \"101\");\n\t\tregisterMorse('L', \"0100\");\n\t\tregisterMorse('M', \"11\");\n\t\tregisterMorse('N', \"10\");\n\t\tregisterMorse('O', \"111\");\n\t\tregisterMorse('P', \"0110\");\n\t\tregisterMorse('Q', \"1101\");\n\t\tregisterMorse('R', \"010\");\n\t\tregisterMorse('S', \"000\");\n\t\tregisterMorse('T', \"1\");\n\t\tregisterMorse('U', \"001\");\n\t\tregisterMorse('V', \"0001\");\n\t\tregisterMorse('W', \"011\");\n\t\tregisterMorse('X', \"1001\");\n\t\tregisterMorse('Y', \"1011\");\n\t\tregisterMorse('Z', \"1100\");\n\t\t// Numbers\n\t\tregisterMorse('0', \"11111\");\n\t\tregisterMorse('1', \"01111\");\n\t\tregisterMorse('2', \"00111\");\n\t\tregisterMorse('3', \"00011\");\n\t\tregisterMorse('4', \"00001\");\n\t\tregisterMorse('5', \"00000\");\n\t\tregisterMorse('6', \"10000\");\n\t\tregisterMorse('7', \"11000\");\n\t\tregisterMorse('8', \"11100\");\n\t\tregisterMorse('9', \"11110\");\n\t\t// Punctuation\n\t\tregisterMorse('.', \"010101\");\n\t\tregisterMorse(',', \"110011\");\n\t\tregisterMorse('?', \"001100\");\n\t\tregisterMorse('\\'', \"011110\");\n\t\tregisterMorse('!', \"101011\");\n\t\tregisterMorse('/', \"10010\");\n\t\tregisterMorse('(', \"10110\");\n\t\tregisterMorse(')', \"101101\");\n\t\tregisterMorse('&', \"01000\");\n\t\tregisterMorse(':', \"111000\");\n\t\tregisterMorse(';', \"101010\");\n\t\tregisterMorse('=', \"10001\");\n\t\tregisterMorse('+', \"01010\");\n\t\tregisterMorse('-', \"100001\");\n\t\tregisterMorse('_', \"001101\");\n\t\tregisterMorse('\"', \"010010\");\n\t\tregisterMorse('$', \"0001001\");\n\t\tregisterMorse('@', \"011010\");\n\t}\n\n\tprivate final char dit; // short mark or dot\n\tprivate final char dah; // longer mark or dash\n\tprivate final char split;\n\n\t/**\n\t * 构造\n\t */\n\tpublic Morse() {\n\t\tthis(CharUtil.DOT, CharUtil.DASHED, CharUtil.SLASH);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dit 点表示的字符\n\t * @param dah 横线表示的字符\n\t * @param split 分隔符\n\t */\n\tpublic Morse(char dit, char dah, char split) {\n\t\tthis.dit = dit;\n\t\tthis.dah = dah;\n\t\tthis.split = split;\n\t}\n\n\t/**\n\t * 编码\n\t *\n\t * @param text 文本\n\t * @return 密文\n\t */\n\tpublic String encode(String text) {\n\t\tAssert.notNull(text, \"Text should not be null.\");\n\n\t\ttext = text.toUpperCase();\n\t\tfinal StringBuilder morseBuilder = new StringBuilder();\n\t\tfinal int len = text.codePointCount(0, text.length());\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tint codePoint = text.codePointAt(i);\n\t\t\tString word = ALPHABETS.get(codePoint);\n\t\t\tif (word == null) {\n\t\t\t\tword = Integer.toBinaryString(codePoint);\n\t\t\t}\n\t\t\tmorseBuilder.append(word.replace('0', dit).replace('1', dah)).append(split);\n\t\t}\n\t\treturn morseBuilder.toString();\n\t}\n\n\t/**\n\t * 解码\n\t *\n\t * @param morse 莫尔斯电码\n\t * @return 明文\n\t */\n\tpublic String decode(String morse) {\n\t\tAssert.notNull(morse, \"Morse should not be null.\");\n\n\t\tfinal char dit = this.dit;\n\t\tfinal char dah = this.dah;\n\t\tfinal char split = this.split;\n\t\tif (false == StrUtil.containsOnly(morse, dit, dah, split)) {\n\t\t\tthrow new IllegalArgumentException(\"Incorrect morse.\");\n\t\t}\n\t\tfinal List<String> words = StrUtil.split(morse, split);\n\t\tfinal StringBuilder textBuilder = new StringBuilder();\n\t\tInteger codePoint;\n\t\tfor (String word : words) {\n\t\t\tif(StrUtil.isEmpty(word)){\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tword = word.replace(dit, '0').replace(dah, '1');\n\t\t\tcodePoint = DICTIONARIES.get(word);\n\t\t\tif (codePoint == null) {\n\t\t\t\tcodePoint = Integer.valueOf(word, 2);\n\t\t\t}\n\t\t\ttextBuilder.appendCodePoint(codePoint);\n\t\t}\n\t\treturn textBuilder.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/PercentCodec.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.BitSet;\n\n/**\n * 百分号编码(Percent-encoding), 也称作URL编码(URL encoding)。<br>\n * 百分号编码可用于URI的编码，也可以用于\"application/x-www-form-urlencoded\"的MIME准备数据。\n *\n * <p>\n * 百分号编码会对 URI 中不允许出现的字符或者其他特殊情况的允许的字符进行编码，对于被编码的字符，最终会转为以百分号\"%“开头，后面跟着两位16进制数值的形式。\n * 举个例子，空格符（SP）是不允许的字符，在 ASCII 码对应的二进制值是\"00100000”，最终转为\"%20\"。\n * </p>\n * <p>\n * 对于不同场景应遵循不同规范：\n *\n * <ul>\n *     <li>URI：遵循RFC 3986保留字规范</li>\n *     <li>application/x-www-form-urlencoded，遵循W3C HTML Form content types规范，如空格须转+</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.16\n */\npublic class PercentCodec implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 从已知PercentCodec创建PercentCodec，会复制给定PercentCodec的安全字符\n\t *\n\t * @param codec PercentCodec\n\t * @return PercentCodec\n\t */\n\tpublic static PercentCodec of(PercentCodec codec) {\n\t\treturn new PercentCodec((BitSet) codec.safeCharacters.clone());\n\t}\n\n\t/**\n\t * 创建PercentCodec，使用指定字符串中的字符作为安全字符\n\t *\n\t * @param chars 安全字符合集\n\t * @return PercentCodec\n\t */\n\tpublic static PercentCodec of(CharSequence chars) {\n\t\tAssert.notNull(chars, \"chars must not be null\");\n\t\tfinal PercentCodec codec = new PercentCodec();\n\t\tfinal int length = chars.length();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tcodec.addSafe(chars.charAt(i));\n\t\t}\n\t\treturn codec;\n\t}\n\n\t/**\n\t * 存放安全编码\n\t */\n\tprivate final BitSet safeCharacters;\n\n\t/**\n\t * 是否编码空格为+<br>\n\t * 如果为{@code true}，则将空格编码为\"+\"，此项只在\"application/x-www-form-urlencoded\"中使用<br>\n\t * 如果为{@code false}，则空格编码为\"%20\",此项一般用于URL的Query部分（RFC3986规范）\n\t */\n\tprivate boolean encodeSpaceAsPlus = false;\n\n\t/**\n\t * 构造<br>\n\t * [a-zA-Z0-9]默认不被编码\n\t */\n\tpublic PercentCodec() {\n\t\tthis(new BitSet(256));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param safeCharacters 安全字符，安全字符不被编码\n\t */\n\tpublic PercentCodec(BitSet safeCharacters) {\n\t\tthis.safeCharacters = safeCharacters;\n\t}\n\n\t/**\n\t * 增加安全字符<br>\n\t * 安全字符不被编码\n\t *\n\t * @param c 字符\n\t * @return this\n\t */\n\tpublic PercentCodec addSafe(char c) {\n\t\tsafeCharacters.set(c);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除安全字符<br>\n\t * 安全字符不被编码\n\t *\n\t * @param c 字符\n\t * @return this\n\t */\n\tpublic PercentCodec removeSafe(char c) {\n\t\tsafeCharacters.clear(c);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加安全字符到挡墙的PercentCodec\n\t *\n\t * @param codec PercentCodec\n\t * @return this\n\t */\n\tpublic PercentCodec or(PercentCodec codec) {\n\t\tthis.safeCharacters.or(codec.safeCharacters);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 组合当前PercentCodec和指定PercentCodec为一个新的PercentCodec，安全字符为并集\n\t *\n\t * @param codec PercentCodec\n\t * @return 新的PercentCodec\n\t */\n\tpublic PercentCodec orNew(PercentCodec codec) {\n\t\treturn of(this).or(codec);\n\t}\n\n\t/**\n\t * 是否将空格编码为+<br>\n\t * 如果为{@code true}，则将空格编码为\"+\"，此项只在\"application/x-www-form-urlencoded\"中使用<br>\n\t * 如果为{@code false}，则空格编码为\"%20\",此项一般用于URL的Query部分（RFC3986规范）\n\t *\n\t * @param encodeSpaceAsPlus 是否将空格编码为+\n\t * @return this\n\t */\n\tpublic PercentCodec setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) {\n\t\tthis.encodeSpaceAsPlus = encodeSpaceAsPlus;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将URL中的字符串编码为%形式\n\t *\n\t * @param path    需要编码的字符串\n\t * @param charset 编码, {@code null}返回原字符串，表示不编码\n\t * @param customSafeChar 自定义安全字符\n\t * @return 编码后的字符串\n\t */\n\tpublic String encode(CharSequence path, Charset charset, char... customSafeChar) {\n\t\tif (null == charset || StrUtil.isEmpty(path)) {\n\t\t\treturn StrUtil.str(path);\n\t\t}\n\n\t\tfinal StringBuilder rewrittenPath = new StringBuilder(path.length());\n\t\tfinal ByteArrayOutputStream buf = new ByteArrayOutputStream();\n\t\tfinal OutputStreamWriter writer = new OutputStreamWriter(buf, charset);\n\n\t\tchar c;\n\t\tfor (int i = 0; i < path.length(); i++) {\n\t\t\tc = path.charAt(i);\n\t\t\tif (safeCharacters.get(c) || ArrayUtil.contains(customSafeChar, c)) {\n\t\t\t\trewrittenPath.append(c);\n\t\t\t} else if (encodeSpaceAsPlus && c == CharUtil.SPACE) {\n\t\t\t\t// 对于空格单独处理\n\t\t\t\trewrittenPath.append('+');\n\t\t\t} else {\n\t\t\t\t// convert to external encoding before hex conversion\n\t\t\t\ttry {\n\t\t\t\t\twriter.write(c);\n\t\t\t\t\twriter.flush();\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tbuf.reset();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 兼容双字节的Unicode符处理（如部分emoji）\n\t\t\t\tbyte[] ba = buf.toByteArray();\n\t\t\t\tfor (byte toEncode : ba) {\n\t\t\t\t\t// Converting each byte in the buffer\n\t\t\t\t\trewrittenPath.append('%');\n\t\t\t\t\tHexUtil.appendHex(rewrittenPath, toEncode, false);\n\t\t\t\t}\n\t\t\t\tbuf.reset();\n\t\t\t}\n\t\t}\n\t\treturn rewrittenPath.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.List;\n\n/**\n * Punycode是一个根据RFC 3492标准而制定的编码系统，主要用于把域名从地方语言所采用的Unicode编码转换成为可用于DNS系统的编码\n * <p>\n * 参考：https://blog.csdn.net/a19881029/article/details/18262671\n *\n * @author looly\n * @since 5.5.2\n */\npublic class PunyCode {\n\tprivate static final int TMIN = 1;\n\tprivate static final int TMAX = 26;\n\tprivate static final int BASE = 36;\n\tprivate static final int INITIAL_N = 128;\n\tprivate static final int INITIAL_BIAS = 72;\n\tprivate static final int DAMP = 700;\n\tprivate static final int SKEW = 38;\n\tprivate static final char DELIMITER = '-';\n\n\tpublic static final String PUNY_CODE_PREFIX = \"xn--\";\n\n\t/**\n\t * punycode转码域名\n\t *\n\t * @param domain 域名\n\t * @return 编码后的域名\n\t * @throws UtilException 计算异常\n\t */\n\tpublic static String encodeDomain(String domain) throws UtilException {\n\t\tAssert.notNull(domain, \"domain must not be null!\");\n\t\tfinal List<String> split = StrUtil.split(domain, CharUtil.DOT);\n\t\tfinal StringBuilder result = new StringBuilder(domain.length() * 4);\n\t\tfor (final String str : split) {\n\t\t\tif (result.length() != 0) {\n\t\t\t\tresult.append(CharUtil.DOT);\n\t\t\t}\n\t\t\tresult.append(encode(str, true));\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 将内容编码为PunyCode\n\t *\n\t * @param input 字符串\n\t * @return PunyCode字符串\n\t * @throws UtilException 计算异常\n\t */\n\tpublic static String encode(CharSequence input) throws UtilException {\n\t\treturn encode(input, false);\n\t}\n\n\t/**\n\t * 将内容编码为PunyCode\n\t *\n\t * @param input      字符串\n\t * @param withPrefix 是否包含 \"xn--\"前缀\n\t * @return PunyCode字符串\n\t * @throws UtilException 计算异常\n\t */\n\tpublic static String encode(CharSequence input, boolean withPrefix) throws UtilException {\n\t\tAssert.notNull(input, \"input must not be null!\");\n\t\tint n = INITIAL_N;\n\t\tint delta = 0;\n\t\tint bias = INITIAL_BIAS;\n\t\tStringBuilder output = new StringBuilder();\n\t\t// Copy all basic code points to the output\n\t\tfinal int length = input.length();\n\t\tint b = 0;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tchar c = input.charAt(i);\n\t\t\tif (isBasic(c)) {\n\t\t\t\toutput.append(c);\n\t\t\t\tb++;\n\t\t\t}\n\t\t}\n\t\t// Append delimiter\n\t\tif (b > 0) {\n\t\t\tif(b == length){\n\t\t\t\t// 无需要编码的字符\n\t\t\t\treturn output.toString();\n\t\t\t}\n\t\t\toutput.append(DELIMITER);\n\t\t}\n\t\tint h = b;\n\t\twhile (h < length) {\n\t\t\tint m = Integer.MAX_VALUE;\n\t\t\t// Find the minimum code point >= n\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\tfinal char c = input.charAt(i);\n\t\t\t\tif (c >= n && c < m) {\n\t\t\t\t\tm = c;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (m - n > (Integer.MAX_VALUE - delta) / (h + 1)) {\n\t\t\t\tthrow new UtilException(\"OVERFLOW\");\n\t\t\t}\n\t\t\tdelta = delta + (m - n) * (h + 1);\n\t\t\tn = m;\n\t\t\tfor (int j = 0; j < length; j++) {\n\t\t\t\tint c = input.charAt(j);\n\t\t\t\tif (c < n) {\n\t\t\t\t\tdelta++;\n\t\t\t\t\tif (0 == delta) {\n\t\t\t\t\t\tthrow new UtilException(\"OVERFLOW\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (c == n) {\n\t\t\t\t\tint q = delta;\n\t\t\t\t\tfor (int k = BASE; ; k += BASE) {\n\t\t\t\t\t\tint t;\n\t\t\t\t\t\tif (k <= bias) {\n\t\t\t\t\t\t\tt = TMIN;\n\t\t\t\t\t\t} else if (k >= bias + TMAX) {\n\t\t\t\t\t\t\tt = TMAX;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tt = k - bias;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (q < t) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\toutput.append((char) digit2codepoint(t + (q - t) % (BASE - t)));\n\t\t\t\t\t\tq = (q - t) / (BASE - t);\n\t\t\t\t\t}\n\t\t\t\t\toutput.append((char) digit2codepoint(q));\n\t\t\t\t\tbias = adapt(delta, h + 1, h == b);\n\t\t\t\t\tdelta = 0;\n\t\t\t\t\th++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelta++;\n\t\t\tn++;\n\t\t}\n\n\t\tif (withPrefix) {\n\t\t\toutput.insert(0, PUNY_CODE_PREFIX);\n\t\t}\n\t\treturn output.toString();\n\t}\n\n\t/**\n\t * 解码punycode域名\n\t *\n\t * @param domain PunyCode域名\n\t * @return 解码后的域名\n\t * @throws UtilException 计算异常\n\t */\n\tpublic static String decodeDomain(String domain) throws UtilException {\n\t\tAssert.notNull(domain, \"domain must not be null!\");\n\t\tfinal List<String> split = StrUtil.split(domain, CharUtil.DOT);\n\t\tfinal StringBuilder result = new StringBuilder(domain.length() / 4 + 1);\n\t\tfor (final String str : split) {\n\t\t\tif (result.length() != 0) {\n\t\t\t\tresult.append(CharUtil.DOT);\n\t\t\t}\n\t\t\tresult.append(StrUtil.startWithIgnoreEquals(str, PUNY_CODE_PREFIX) ? decode(str) : str);\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 解码 PunyCode为字符串\n\t *\n\t * @param input PunyCode\n\t * @return 字符串\n\t * @throws UtilException 计算异常\n\t */\n\tpublic static String decode(String input) throws UtilException {\n\t\tAssert.notNull(input, \"input must not be null!\");\n\t\tinput = StrUtil.removePrefixIgnoreCase(input, PUNY_CODE_PREFIX);\n\n\t\tint n = INITIAL_N;\n\t\tint i = 0;\n\t\tint bias = INITIAL_BIAS;\n\t\tStringBuilder output = new StringBuilder();\n\t\tint d = input.lastIndexOf(DELIMITER);\n\t\tif (d > 0) {\n\t\t\tfor (int j = 0; j < d; j++) {\n\t\t\t\tfinal char c = input.charAt(j);\n\t\t\t\tif (isBasic(c)) {\n\t\t\t\t\toutput.append(c);\n\t\t\t\t}\n\t\t\t}\n\t\t\td++;\n\t\t} else {\n\t\t\td = 0;\n\t\t}\n\t\tfinal int length = input.length();\n\t\twhile (d < length) {\n\t\t\tint oldi = i;\n\t\t\tint w = 1;\n\t\t\tfor (int k = BASE; ; k += BASE) {\n\t\t\t\tif (d == length) {\n\t\t\t\t\tthrow new UtilException(\"BAD_INPUT\");\n\t\t\t\t}\n\t\t\t\tint c = input.charAt(d++);\n\t\t\t\tint digit = codepoint2digit(c);\n\t\t\t\tif (digit > (Integer.MAX_VALUE - i) / w) {\n\t\t\t\t\tthrow new UtilException(\"OVERFLOW\");\n\t\t\t\t}\n\t\t\t\ti = i + digit * w;\n\t\t\t\tint t;\n\t\t\t\tif (k <= bias) {\n\t\t\t\t\tt = TMIN;\n\t\t\t\t} else if (k >= bias + TMAX) {\n\t\t\t\t\tt = TMAX;\n\t\t\t\t} else {\n\t\t\t\t\tt = k - bias;\n\t\t\t\t}\n\t\t\t\tif (digit < t) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tw = w * (BASE - t);\n\t\t\t}\n\t\t\tbias = adapt(i - oldi, output.length() + 1, oldi == 0);\n\t\t\tif (i / (output.length() + 1) > Integer.MAX_VALUE - n) {\n\t\t\t\tthrow new UtilException(\"OVERFLOW\");\n\t\t\t}\n\t\t\tn = n + i / (output.length() + 1);\n\t\t\ti = i % (output.length() + 1);\n\t\t\toutput.insert(i, (char) n);\n\t\t\ti++;\n\t\t}\n\n\t\treturn output.toString();\n\t}\n\n\tprivate static int adapt(int delta, int numpoints, boolean first) {\n\t\tif (first) {\n\t\t\tdelta = delta / DAMP;\n\t\t} else {\n\t\t\tdelta = delta / 2;\n\t\t}\n\t\tdelta = delta + (delta / numpoints);\n\t\tint k = 0;\n\t\twhile (delta > ((BASE - TMIN) * TMAX) / 2) {\n\t\t\tdelta = delta / (BASE - TMIN);\n\t\t\tk = k + BASE;\n\t\t}\n\t\treturn k + ((BASE - TMIN + 1) * delta) / (delta + SKEW);\n\t}\n\n\tprivate static boolean isBasic(char c) {\n\t\treturn c < 0x80;\n\t}\n\n\t/**\n\t * 将数字转为字符，对应关系为：\n\t * <pre>\n\t *     0 -&gt; a\n\t *     1 -&gt; b\n\t *     ...\n\t *     25 -&gt; z\n\t *     26 -&gt; '0'\n\t *     ...\n\t *     35 -&gt; '9'\n\t * </pre>\n\t *\n\t * @param d 输入字符\n\t * @return 转换后的字符\n\t * @throws UtilException 无效字符\n\t */\n\tprivate static int digit2codepoint(int d) throws UtilException {\n\t\tAssert.checkBetween(d, 0, 35);\n\t\tif (d < 26) {\n\t\t\t// 0..25 : 'a'..'z'\n\t\t\treturn d + 'a';\n\t\t} else if (d < 36) {\n\t\t\t// 26..35 : '0'..'9';\n\t\t\treturn d - 26 + '0';\n\t\t} else {\n\t\t\tthrow new UtilException(\"BAD_INPUT\");\n\t\t}\n\t}\n\n\t/**\n\t * 将字符转为数字，对应关系为：\n\t * <pre>\n\t *     a -&gt; 0\n\t *     b -&gt; 1\n\t *     ...\n\t *     z -&gt; 25\n\t *     '0' -&gt; 26\n\t *     ...\n\t *     '9' -&gt; 35\n\t * </pre>\n\t *\n\t * @param c 输入字符\n\t * @return 转换后的字符\n\t * @throws UtilException 无效字符\n\t */\n\tprivate static int codepoint2digit(int c) throws UtilException {\n\t\tif (c - '0' < 10) {\n\t\t\t// '0'..'9' : 26..35\n\t\t\treturn c - '0' + 26;\n\t\t} else if (c - 'a' < 26) {\n\t\t\t// 'a'..'z' : 0..25\n\t\t\treturn c - 'a';\n\t\t} else {\n\t\t\tthrow new UtilException(\"BAD_INPUT\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/Rot.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * RotN（rotate by N places），回转N位密码，是一种简易的替换式密码，也是过去在古罗马开发的凯撒加密的一种变体。<br>\n * 代码来自：https://github.com/orclight/jencrypt\n *\n * @author looly,shuzhilong\n * @since 4.4.1\n */\npublic class Rot {\n\n\tprivate static final char aCHAR = 'a';\n\tprivate static final char zCHAR = 'z';\n\tprivate static final char ACHAR = 'A';\n\tprivate static final char ZCHAR = 'Z';\n\tprivate static final char CHAR0 = '0';\n\tprivate static final char CHAR9 = '9';\n\n\t/**\n\t * Rot-13编码，同时编码数字\n\t *\n\t * @param message 被编码的消息\n\t * @return 编码后的字符串\n\t */\n\tpublic static String encode13(String message) {\n\t\treturn encode13(message, true);\n\t}\n\n\t/**\n\t * Rot-13编码\n\t *\n\t * @param message 被编码的消息\n\t * @param isEncodeNumber 是否编码数字\n\t * @return 编码后的字符串\n\t */\n\tpublic static String encode13(String message, boolean isEncodeNumber) {\n\t\treturn encode(message, 13, isEncodeNumber);\n\t}\n\n\t/**\n\t * RotN编码\n\t *\n\t * @param message 被编码的消息\n\t * @param offset 位移，常用位移13\n\t * @param isEncodeNumber 是否编码数字\n\t * @return 编码后的字符串\n\t */\n\tpublic static String encode(String message, int offset, boolean isEncodeNumber) {\n\t\tAssert.notNull(message, \"message must not be null\");\n\t\tfinal int len = message.length();\n\t\tfinal char[] chars = new char[len];\n\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tchars[i] = encodeChar(message.charAt(i), offset, isEncodeNumber);\n\t\t}\n\t\treturn new String(chars);\n\t}\n\n\t/**\n\t * Rot-13解码，同时解码数字\n\t *\n\t * @param rot 被解码的消息密文\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decode13(String rot) {\n\t\treturn decode13(rot, true);\n\t}\n\n\t/**\n\t * Rot-13解码\n\t *\n\t * @param rot 被解码的消息密文\n\t * @param isDecodeNumber 是否解码数字\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decode13(String rot, boolean isDecodeNumber) {\n\t\treturn decode(rot, 13, isDecodeNumber);\n\t}\n\n\t/**\n\t * RotN解码\n\t *\n\t * @param rot 被解码的消息密文\n\t * @param offset 位移，常用位移13\n\t * @param isDecodeNumber 是否解码数字\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decode(String rot, int offset, boolean isDecodeNumber) {\n\t\tAssert.notNull(rot, \"rot must not be null\");\n\t\tfinal int len = rot.length();\n\t\tfinal char[] chars = new char[len];\n\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tchars[i] = decodeChar(rot.charAt(i), offset, isDecodeNumber);\n\t\t}\n\t\treturn new String(chars);\n\t}\n\n\t// ------------------------------------------------------------------------------------------ Private method start\n\t/**\n\t * 解码字符\n\t *\n\t * @param c 字符\n\t * @param offset 位移\n\t * @param isDecodeNumber 是否解码数字\n\t * @return 解码后的字符串\n\t */\n\tprivate static char encodeChar(char c, int offset, boolean isDecodeNumber) {\n\t\tif (isDecodeNumber) {\n\t\t\tif (c >= CHAR0 && c <= CHAR9) {\n\t\t\t\tc -= CHAR0;\n\t\t\t\tc = (char) ((c + offset) % 10);\n\t\t\t\tc += CHAR0;\n\t\t\t}\n\t\t}\n\n\t\t// A == 65, Z == 90\n\t\tif (c >= ACHAR && c <= ZCHAR) {\n\t\t\tc -= ACHAR;\n\t\t\tc = (char) ((c + offset) % 26);\n\t\t\tc += ACHAR;\n\t\t}\n\t\t// a == 97, z == 122.\n\t\telse if (c >= aCHAR && c <= zCHAR) {\n\t\t\tc -= aCHAR;\n\t\t\tc = (char) ((c + offset) % 26);\n\t\t\tc += aCHAR;\n\t\t}\n\t\treturn c;\n\t}\n\n\t/**\n\t * 编码字符\n\t *\n\t * @param c 字符\n\t * @param offset 位移\n\t * @param isDecodeNumber 是否编码数字\n\t * @return 编码后的字符串\n\t */\n\tprivate static char decodeChar(char c, int offset, boolean isDecodeNumber) {\n\t\tint temp = c;\n\t\t// if converting numbers is enabled\n\t\tif (isDecodeNumber) {\n\t\t\tif (temp >= CHAR0 && temp <= CHAR9) {\n\t\t\t\ttemp -= CHAR0;\n\t\t\t\ttemp = temp - offset;\n\t\t\t\twhile (temp < 0) {\n\t\t\t\t\ttemp += 10;\n\t\t\t\t}\n\t\t\t\ttemp += CHAR0;\n\t\t\t}\n\t\t}\n\n\t\t// A == 65, Z == 90\n\t\tif (temp >= ACHAR && temp <= ZCHAR) {\n\t\t\ttemp -= ACHAR;\n\n\t\t\ttemp = temp - offset;\n\t\t\twhile (temp < 0) {\n\t\t\t\ttemp = 26 + temp;\n\t\t\t}\n\t\t\ttemp += ACHAR;\n\t\t} else if (temp >= aCHAR && temp <= zCHAR) {\n\t\t\ttemp -= aCHAR;\n\n\t\t\ttemp = temp - offset;\n\t\t\tif (temp < 0)\n\t\t\t\ttemp = 26 + temp;\n\n\t\t\ttemp += aCHAR;\n\t\t}\n\t\treturn (char) temp;\n\t}\n\t// ------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/codec/package-info.java",
    "content": "/**\n * BaseN以及BCD编码封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.codec;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/ArrayIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Array;\nimport java.util.NoSuchElementException;\n\n/**\n * 数组Iterator对象\n *\n * @param <E> 元素类型\n * @author Looly\n * @since 4.1.1\n */\npublic class ArrayIter<E> implements IterableIter<E>, ResettableIter<E>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 数组\n\t */\n\tprivate final Object array;\n\t/**\n\t * 起始位置\n\t */\n\tprivate int startIndex;\n\t/**\n\t * 结束位置\n\t */\n\tprivate int endIndex;\n\t/**\n\t * 当前位置\n\t */\n\tprivate int index;\n\n\t/**\n\t * 构造\n\t *\n\t * @param array 数组\n\t * @throws IllegalArgumentException array对象不为数组抛出此异常\n\t * @throws NullPointerException     array对象为null\n\t */\n\tpublic ArrayIter(E[] array) {\n\t\tthis((Object) array);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param array 数组\n\t * @throws IllegalArgumentException array对象不为数组抛出此异常\n\t * @throws NullPointerException     array对象为null\n\t */\n\tpublic ArrayIter(Object array) {\n\t\tthis(array, 0);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param array      数组\n\t * @param startIndex 起始位置，当起始位置小于0或者大于结束位置，置为0。\n\t * @throws IllegalArgumentException array对象不为数组抛出此异常\n\t * @throws NullPointerException     array对象为null\n\t */\n\tpublic ArrayIter(Object array, int startIndex) {\n\t\tthis(array, startIndex, -1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param array      数组\n\t * @param startIndex 起始位置，当起始位置小于0或者大于结束位置，置为0。\n\t * @param endIndex   结束位置，当结束位置小于0或者大于数组长度，置为数组长度。\n\t * @throws IllegalArgumentException array对象不为数组抛出此异常\n\t * @throws NullPointerException     array对象为null\n\t */\n\tpublic ArrayIter(final Object array, final int startIndex, final int endIndex) {\n\t\tthis.endIndex = Array.getLength(array);\n\t\tif (endIndex > 0 && endIndex < this.endIndex) {\n\t\t\tthis.endIndex = endIndex;\n\t\t}\n\n\t\tif (startIndex >= 0 && startIndex < this.endIndex) {\n\t\t\tthis.startIndex = startIndex;\n\t\t}\n\t\tthis.array = array;\n\t\tthis.index = this.startIndex;\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn (index < endIndex);\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic E next() {\n\t\tif (hasNext() == false) {\n\t\t\tthrow new NoSuchElementException();\n\t\t}\n\t\treturn (E) Array.get(array, index++);\n\t}\n\n\t/**\n\t * 不允许操作数组元素\n\t *\n\t * @throws UnsupportedOperationException always\n\t */\n\t@Override\n\tpublic void remove() {\n\t\tthrow new UnsupportedOperationException(\"remove() method is not supported\");\n\t}\n\n\t// Properties\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 获得原始数组对象\n\t *\n\t * @return 原始数组对象\n\t */\n\tpublic Object getArray() {\n\t\treturn array;\n\t}\n\n\t/**\n\t * 重置数组位置\n\t */\n\t@Override\n\tpublic void reset() {\n\t\tthis.index = this.startIndex;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/AvgPartition.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.List;\n\n/**\n * 列表分区或分段<br>\n * 通过传入分区个数，将指定列表分区为不同的块，每块区域的长度均匀分布（个数差不超过1）<br>\n * <pre>\n *     [1,2,3,4] -》 [1,2], [3, 4]\n *     [1,2,3,4] -》 [1,2], [3], [4]\n *     [1,2,3,4] -》 [1], [2], [3], [4]\n *     [1,2,3,4] -》 [1], [2], [3], [4], []\n * </pre>\n * 分区是在原List的基础上进行的，返回的分区是不可变的抽象列表，原列表元素变更，分区中元素也会变更。\n *\n * @param <T> 元素类型\n * @author looly\n * @since 5.7.10\n */\npublic class AvgPartition<T> extends Partition<T> {\n\n\tfinal int limit;\n\t// 平均分完后剩余的个数，平均放在前remainder个分区中\n\tfinal int remainder;\n\n\t/**\n\t * 列表分区\n\t *\n\t * @param list  被分区的列表\n\t * @param limit 分区个数\n\t */\n\tpublic AvgPartition(List<T> list, int limit) {\n\t\tsuper(list, list.size() / (limit <= 0 ? 1 : limit));\n\t\tAssert.isTrue(limit > 0, \"Partition limit must be > 0\");\n\t\tthis.limit = limit;\n\t\tthis.remainder = list.size() % limit;\n\t}\n\n\t@Override\n\tpublic List<T> get(int index) {\n\t\tfinal int size = this.size;\n\t\tfinal int remainder = this.remainder;\n\t\t// 当limit个数超过list的size时，size为0，此时每个分区分1个元素，直到remainder个分配完，剩余分区为[]\n\t\tint start = index * size + Math.min(index, remainder);\n\t\tint end = start + size;\n\t\tif (index + 1 <= remainder) {\n\t\t\t// 将remainder个元素平均分布在前面，每个分区分1个\n\t\t\tend += 1;\n\t\t}\n\t\treturn list.subList(start, end);\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn limit;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.PriorityQueue;\n\n/**\n * 有界优先队列<br>\n * 按照给定的排序规则，排序元素，当队列满时，按照给定的排序规则淘汰末尾元素（去除末尾元素）\n * @author xiaoleilu\n *\n * @param <E> 成员类型\n */\npublic class BoundedPriorityQueue<E> extends PriorityQueue<E>{\n\tprivate static final long serialVersionUID = 3794348988671694820L;\n\n\t//容量\n\tprivate final int capacity;\n\tprivate final Comparator<? super E> comparator;\n\n\tpublic BoundedPriorityQueue(int capacity) {\n\t\tthis(capacity, null);\n\t}\n\n\t/**\n\t * 构造\n\t * @param capacity 容量，必须大于0\n\t * @param comparator 比较器\n\t */\n\tpublic BoundedPriorityQueue(int capacity, final Comparator<? super E> comparator) {\n\t\tsuper(capacity, (o1, o2) -> {\n\t\t\tint cResult;\n\t\t\tif(comparator != null) {\n\t\t\t\tcResult = comparator.compare(o1, o2);\n\t\t\t}else {\n\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\tComparable<E> o1c = (Comparable<E>)o1;\n\t\t\t\tcResult = o1c.compareTo(o2);\n\t\t\t}\n\n\t\t\treturn - cResult;\n\t\t});\n\t\tthis.capacity = capacity;\n\t\tthis.comparator = comparator;\n\t}\n\n\t/**\n\t * 加入元素，当队列满时，淘汰末尾元素\n\t * @param e 元素\n\t * @return 加入成功与否\n\t */\n\t@Override\n\tpublic boolean offer(E e) {\n\t\tif(size() >= capacity) {\n\t\t\tE head = peek();\n\t\t\tif (this.comparator().compare(e, head) <= 0){\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t//当队列满时，就要淘汰顶端队列\n\t\t\tpoll();\n\t\t}\n\t\treturn super.offer(e);\n\t}\n\n\t/**\n\t * 添加多个元素<br>\n\t * 参数为集合的情况请使用{@link PriorityQueue#addAll}\n\t * @param c 元素数组\n\t * @return 是否发生改变\n\t */\n\tpublic boolean addAll(E[] c) {\n\t\treturn this.addAll(Arrays.asList(c));\n\t}\n\n\t/**\n\t * @return 返回排序后的列表\n\t */\n\tpublic ArrayList<E> toList() {\n\t\tfinal ArrayList<E> list = new ArrayList<>(this);\n\t\tlist.sort(comparator);\n\t\treturn list;\n\t}\n\n\t@Override\n\tpublic Iterator<E> iterator() {\n\t\treturn toList().iterator();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java",
    "content": "package cn.hutool.core.collection;\n\n\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.stream.CollectorUtil;\nimport cn.hutool.core.stream.StreamUtil;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.stream.Collector;\nimport java.util.stream.Collectors;\n\n/**\n * 集合的stream操作封装\n *\n * @author 528910437@QQ.COM, VampireAchao&lt;achao1441470436@gmail.com&gt;Lion Li&gt;\n * @since 5.5.2\n */\npublic class CollStreamUtil {\n\n\t/**\n\t * 将collection转化为类型不变的map<br>\n\t * <B>{@code Collection<V>  ---->  Map<K,V>}</B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param key        V类型转化为K类型的lambda方法\n\t * @param <V>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @return 转化后的map\n\t */\n\tpublic static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {\n\t\treturn toIdentityMap(collection, key, false);\n\t}\n\n\n\t/**\n\t * 将collection转化为类型不变的map<br>\n\t * <B>{@code Collection<V>  ---->  Map<K,V>}</B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param key        V类型转化为K类型的lambda方法\n\t * @param isParallel 是否并行流\n\t * @param <V>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @return 转化后的map\n\t */\n\tpublic static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn toMap(collection, (v) -> Opt.ofNullable(v).map(key).get(), Function.identity(), isParallel);\n\t}\n\n\t/**\n\t * 将Collection转化为map(value类型与collection的泛型不同)<br>\n\t * <B>{@code Collection<E> -----> Map<K,V>  }</B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param key        E类型转化为K类型的lambda方法\n\t * @param value      E类型转化为V类型的lambda方法\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <V>        map中的value类型\n\t * @return 转化后的map\n\t */\n\tpublic static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {\n\t\treturn toMap(collection, key, value, false);\n\t}\n\n\t/**\n\t * @param collection 需要转化的集合\n\t * @param key        E类型转化为K类型的lambda方法\n\t * @param value      E类型转化为V类型的lambda方法\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <V>        map中的value类型\n\t * @return 转化后的map\n\t */\n\tpublic static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn StreamUtil.of(collection, isParallel)\n\t\t\t\t.collect(HashMap::new, (m, v) -> m.put(key.apply(v), value.apply(v)), HashMap::putAll);\n\t}\n\n\n\t/**\n\t * 将collection按照规则(比如有相同的班级id)分组成map<br>\n\t * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        分组的规则\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {\n\t\treturn groupByKey(collection, key, false);\n\t}\n\n\t/**\n\t * 将collection按照规则(比如有相同的班级id)分组成map<br>\n\t * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        键分组的规则\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn groupBy(collection, key, Collectors.toList(), isParallel);\n\t}\n\n\t/**\n\t * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map<br>\n\t * <B>{@code Collection<E>  --->  Map<T,Map<U,List<E>>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key1       第一个分组的规则\n\t * @param key2       第二个分组的规则\n\t * @param <E>        集合元素类型\n\t * @param <K>        第一个map中的key类型\n\t * @param <U>        第二个map中的key类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {\n\t\treturn groupBy2Key(collection, key1, key2, false);\n\t}\n\n\n\t/**\n\t * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map<br>\n\t * <B>{@code Collection<E>  --->  Map<T,Map<U,List<E>>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key1       第一个分组的规则\n\t * @param key2       第二个分组的规则\n\t * @param isParallel 是否并行流\n\t * @param <E>        集合元素类型\n\t * @param <K>        第一个map中的key类型\n\t * @param <U>        第二个map中的key类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tFunction<E, U> key2, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn groupBy(collection, key1, CollectorUtil.groupingBy(key2, Collectors.toList()), isParallel);\n\t}\n\n\t/**\n\t * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map<br>\n\t * <B>{@code Collection<E>  --->  Map<T,Map<U,E>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key1       第一个分组的规则\n\t * @param key2       第二个分组的规则\n\t * @param <T>        第一个map中的key类型\n\t * @param <U>        第二个map中的key类型\n\t * @param <E>        collection中的泛型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {\n\t\treturn group2Map(collection, key1, key2, false);\n\t}\n\n\t/**\n\t * 将collection按照两个规则(比如有相同的年级id,班级id)分组成双层map<br>\n\t * <B>{@code Collection<E>  --->  Map<T,Map<U,E>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key1       第一个分组的规则\n\t * @param key2       第二个分组的规则\n\t * @param isParallel 是否并行流\n\t * @param <T>        第一个map中的key类型\n\t * @param <U>        第二个map中的key类型\n\t * @param <E>        collection中的泛型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tFunction<E, T> key1, Function<E, U> key2, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn groupBy(collection, key1, CollectorUtil.toMap(key2, Function.identity(), (l, r) -> l), isParallel);\n\t}\n\n\t/**\n\t * 将collection按照规则(比如有相同的班级id)分组成map，map中的key为班级id，value为班级名<br>\n\t * <B>{@code Collection<E> -------> Map<K,List<V>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        键分组的规则\n\t * @param value      值分组的规则\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <V>        List中的value类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K, V> Map<K, List<V>> groupKeyValue(Collection<E> collection, Function<E, K> key,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t  Function<E, V> value) {\n\t\treturn groupKeyValue(collection, key, value, false);\n\t}\n\n\t/**\n\t * 将collection按照规则(比如有相同的班级id)分组成map，map中的key为班级id，value为班级名<br>\n\t * <B>{@code Collection<E> -------> Map<K,List<V>> } </B>\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        键分组的规则\n\t * @param value      值分组的规则\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <V>        List中的value类型\n\t * @return 分组后的map\n\t */\n\tpublic static <E, K, V> Map<K, List<V>> groupKeyValue(Collection<E> collection, Function<E, K> key,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t  Function<E, V> value, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn groupBy(collection, key, Collectors.mapping(v -> Opt.ofNullable(v).map(value).orElse(null), Collectors.toList()), isParallel);\n\t}\n\n\t/**\n\t * 作为所有groupingBy的公共方法，更接近于原生，灵活性更强\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        第一次分组时需要的key\n\t * @param downstream 分组后需要进行的操作\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <D>        后续操作的返回值\n\t * @return 分组后的map\n\t * @since 5.7.18\n\t */\n\tpublic static <E, K, D> Map<K, D> groupBy(Collection<E> collection, Function<E, K> key, Collector<E, ?, D> downstream) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn groupBy(collection, key, downstream, false);\n\t}\n\n\t/**\n\t * 作为所有groupingBy的公共方法，更接近于原生，灵活性更强\n\t *\n\t * @param collection 需要分组的集合\n\t * @param key        第一次分组时需要的key\n\t * @param downstream 分组后需要进行的操作\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <K>        map中的key类型\n\t * @param <D>        后续操作的返回值\n\t * @return 分组后的map\n\t * @see Collectors#groupingBy(Function, Collector)\n\t * @since 5.7.18\n\t */\n\tpublic static <E, K, D> Map<K, D> groupBy(Collection<E> collection, Function<E, K> key, Collector<E, ?, D> downstream, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t}\n\t\treturn StreamUtil.of(collection, isParallel).collect(CollectorUtil.groupingBy(key, downstream));\n\t}\n\n\t/**\n\t * 将collection转化为List集合，但是两者的泛型不同<br>\n\t * <B>{@code Collection<E>  ------>  List<T> } </B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param function   collection中的泛型转化为list泛型的lambda表达式\n\t * @param <E>        collection中的泛型\n\t * @param <T>        List中的泛型\n\t * @return 转化后的list\n\t */\n\tpublic static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {\n\t\treturn toList(collection, function, false);\n\t}\n\n\t/**\n\t * 将collection转化为List集合，但是两者的泛型不同<br>\n\t * <B>{@code Collection<E>  ------>  List<T> } </B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param function   collection中的泛型转化为list泛型的lambda表达式\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <T>        List中的泛型\n\t * @return 转化后的list\n\t */\n\tpublic static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn CollUtil.newArrayList();\n\t\t}\n\t\treturn StreamUtil.of(collection, isParallel)\n\t\t\t\t.map(function)\n\t\t\t\t.filter(Objects::nonNull)\n\t\t\t\t.collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 将collection转化为Set集合，但是两者的泛型不同<br>\n\t * <B>{@code Collection<E>  ------>  Set<T> } </B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param function   collection中的泛型转化为set泛型的lambda表达式\n\t * @param <E>        collection中的泛型\n\t * @param <T>        Set中的泛型\n\t * @return 转化后的Set\n\t */\n\tpublic static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {\n\t\treturn toSet(collection, function, false);\n\t}\n\n\t/**\n\t * 将collection转化为Set集合，但是两者的泛型不同<br>\n\t * <B>{@code Collection<E>  ------>  Set<T> } </B>\n\t *\n\t * @param collection 需要转化的集合\n\t * @param function   collection中的泛型转化为set泛型的lambda表达式\n\t * @param isParallel 是否并行流\n\t * @param <E>        collection中的泛型\n\t * @param <T>        Set中的泛型\n\t * @return 转化后的Set\n\t */\n\tpublic static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function, boolean isParallel) {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn CollUtil.newHashSet();\n\t\t}\n\t\treturn StreamUtil.of(collection, isParallel)\n\t\t\t\t.map(function)\n\t\t\t\t.filter(Objects::nonNull)\n\t\t\t\t.collect(Collectors.toSet());\n\t}\n\n\n\t/**\n\t * 合并两个相同key类型的map\n\t *\n\t * @param map1  第一个需要合并的 map\n\t * @param map2  第二个需要合并的 map\n\t * @param merge 合并的lambda，将key  value1 value2合并成最终的类型,注意value可能为空的情况\n\t * @param <K>   map中的key类型\n\t * @param <X>   第一个 map的value类型\n\t * @param <Y>   第二个 map的value类型\n\t * @param <V>   最终map的value类型\n\t * @return 合并后的map\n\t */\n\tpublic static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {\n\t\tif (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {\n\t\t\treturn MapUtil.newHashMap(0);\n\t\t} else if (MapUtil.isEmpty(map1)) {\n\t\t\tmap1 = MapUtil.newHashMap(0);\n\t\t} else if (MapUtil.isEmpty(map2)) {\n\t\t\tmap2 = MapUtil.newHashMap(0);\n\t\t}\n\t\tSet<K> key = new HashSet<>();\n\t\tkey.addAll(map1.keySet());\n\t\tkey.addAll(map2.keySet());\n\t\tMap<K, V> map = MapUtil.newHashMap(key.size());\n\t\tfor (K t : key) {\n\t\t\tX x = map1.get(t);\n\t\t\tY y = map2.get(t);\n\t\t\tV z = merge.apply(x, y);\n\t\t\tif (z != null) {\n\t\t\t\tmap.put(t, z);\n\t\t\t}\n\t\t}\n\t\treturn map;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.comparator.PinyinComparator;\nimport cn.hutool.core.comparator.PropertyComparator;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.*;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.hash.Hash32;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.*;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Type;\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.LinkedBlockingDeque;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n * 集合相关工具类\n * <p>\n * 此工具方法针对{@link Collection}及其实现类封装的工具。\n * <p>\n * 由于{@link Collection} 实现了{@link Iterable}接口，因此部分工具此类不提供，而是在{@link IterUtil} 中提供\n *\n * @author xiaoleilu\n * @see IterUtil\n * @since 3.1.1\n */\npublic class CollUtil {\n\n\t/**\n\t * 如果提供的集合为{@code null}，返回一个不可变的默认空集合，否则返回原集合<br>\n\t * 空集合使用{@link Collections#emptySet()}\n\t *\n\t * @param <T> 集合元素类型\n\t * @param set 提供的集合，可能为null\n\t * @return 原集合，若为null返回空集合\n\t * @since 4.6.3\n\t */\n\tpublic static <T> Set<T> emptyIfNull(Set<T> set) {\n\t\treturn (null == set) ? Collections.emptySet() : set;\n\t}\n\n\t/**\n\t * 如果提供的集合为{@code null}，返回一个不可变的默认空集合，否则返回原集合<br>\n\t * 空集合使用{@link Collections#emptyList()}\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 提供的集合，可能为null\n\t * @return 原集合，若为null返回空集合\n\t * @since 4.6.3\n\t */\n\tpublic static <T> List<T> emptyIfNull(List<T> list) {\n\t\treturn (null == list) ? Collections.emptyList() : list;\n\t}\n\n\t/**\n\t * 两个集合的并集<br>\n\t * 针对一个集合中存在多个相同元素的情况，计算两个集合中此元素的个数，保留最多的个数<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c, c, c]，此结果中只保留了三个c\n\t *\n\t * @param <T>   集合元素类型\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @return 并集的集合，返回 {@link ArrayList}\n\t */\n\tpublic static <T> Collection<T> union(Collection<T> coll1, Collection<T> coll2) {\n\t\tif (isEmpty(coll1) && isEmpty(coll2)) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\t\tif (isEmpty(coll1)) {\n\t\t\treturn new ArrayList<>(coll2);\n\t\t} else if (isEmpty(coll2)) {\n\t\t\treturn new ArrayList<>(coll1);\n\t\t}\n\n\t\tfinal ArrayList<T> list = new ArrayList<>(Math.max(coll1.size(), coll2.size()));\n\t\tfinal Map<T, Integer> map1 = countMap(coll1);\n\t\tfinal Map<T, Integer> map2 = countMap(coll2);\n\t\tfinal Set<T> elts = newHashSet(coll2);\n\t\telts.addAll(coll1);\n\t\tint m;\n\t\tfor (T t : elts) {\n\t\t\tm = Math.max(Convert.toInt(map1.get(t), 0), Convert.toInt(map2.get(t), 0));\n\t\t\tfor (int i = 0; i < m; i++) {\n\t\t\t\tlist.add(t);\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 多个集合的并集<br>\n\t * 针对一个集合中存在多个相同元素的情况，计算两个集合中此元素的个数，保留最多的个数<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c, c, c]，此结果中只保留了三个c\n\t *\n\t * @param <T>        集合元素类型\n\t * @param coll1      集合1\n\t * @param coll2      集合2\n\t * @param otherColls 其它集合\n\t * @return 并集的集合，返回 {@link ArrayList}\n\t */\n\t@SafeVarargs\n\tpublic static <T> Collection<T> union(Collection<T> coll1, Collection<T> coll2, Collection<T>... otherColls) {\n\t\tCollection<T> union = union(coll1, coll2);\n\t\tfor (Collection<T> coll : otherColls) {\n\t\t\tif (isEmpty(coll)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tunion = union(union, coll);\n\t\t}\n\t\treturn union;\n\t}\n\n\t/**\n\t * 多个集合的非重复并集，类似于SQL中的“UNION DISTINCT”<br>\n\t * 针对一个集合中存在多个相同元素的情况，只保留一个<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c]，此结果中只保留了一个c\n\t *\n\t * @param <T>        集合元素类型\n\t * @param coll1      集合1\n\t * @param coll2      集合2\n\t * @param otherColls 其它集合\n\t * @return 并集的集合，返回 {@link LinkedHashSet}\n\t */\n\t@SafeVarargs\n\tpublic static <T> Set<T> unionDistinct(Collection<T> coll1, Collection<T> coll2, Collection<T>... otherColls) {\n\t\tfinal Set<T> result;\n\t\tif (isEmpty(coll1)) {\n\t\t\tresult = new LinkedHashSet<>();\n\t\t} else {\n\t\t\tresult = new LinkedHashSet<>(coll1);\n\t\t}\n\n\t\tif (isNotEmpty(coll2)) {\n\t\t\tresult.addAll(coll2);\n\t\t}\n\n\t\tif (ArrayUtil.isNotEmpty(otherColls)) {\n\t\t\tfor (Collection<T> otherColl : otherColls) {\n\t\t\t\tif (isEmpty(otherColl)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tresult.addAll(otherColl);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 多个集合的完全并集，类似于SQL中的“UNION ALL”<br>\n\t * 针对一个集合中存在多个相同元素的情况，保留全部元素<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c, c, c, a, b, c, c]\n\t *\n\t * @param <T>        集合元素类型\n\t * @param coll1      集合1\n\t * @param coll2      集合2\n\t * @param otherColls 其它集合\n\t * @return 并集的集合，返回 {@link ArrayList}\n\t */\n\t@SafeVarargs\n\tpublic static <T> List<T> unionAll(Collection<T> coll1, Collection<T> coll2, Collection<T>... otherColls) {\n\t\tif (CollUtil.isEmpty(coll1) && CollUtil.isEmpty(coll2) && ArrayUtil.isEmpty(otherColls)) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\t// 计算元素总数\n\t\tint totalSize = 0;\n\t\ttotalSize += size(coll1);\n\t\ttotalSize += size(coll2);\n\t\tif (otherColls != null) {\n\t\t\tfor (final Collection<T> otherColl : otherColls) {\n\t\t\t\ttotalSize += size(otherColl);\n\t\t\t}\n\t\t}\n\n\t\t// 根据size创建，防止多次扩容\n\t\tfinal List<T> res = new ArrayList<>(totalSize);\n\t\tif (coll1 != null) {\n\t\t\tres.addAll(coll1);\n\t\t}\n\t\tif (coll2 != null) {\n\t\t\tres.addAll(coll2);\n\t\t}\n\t\tif (otherColls == null) {\n\t\t\treturn res;\n\t\t}\n\n\t\tfor (final Collection<T> otherColl : otherColls) {\n\t\t\tif (otherColl != null) {\n\t\t\t\tres.addAll(otherColl);\n\t\t\t}\n\t\t}\n\n\t\treturn res;\n\t}\n\n\t/**\n\t * 两个集合的交集<br>\n\t * 针对一个集合中存在多个相同元素的情况，计算两个集合中此元素的个数，保留最少的个数<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c, c]，此结果中只保留了两个c\n\t *\n\t * @param <T>   集合元素类型\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @return 交集的集合，返回 {@link ArrayList}\n\t */\n\tpublic static <T> Collection<T> intersection(Collection<T> coll1, Collection<T> coll2) {\n\t\tif (isNotEmpty(coll1) && isNotEmpty(coll2)) {\n\t\t\tfinal ArrayList<T> list = new ArrayList<>(Math.min(coll1.size(), coll2.size()));\n\t\t\tfinal Map<T, Integer> map1 = countMap(coll1);\n\t\t\tfinal Map<T, Integer> map2 = countMap(coll2);\n\t\t\tfinal Set<T> elts = newHashSet(coll2);\n\t\t\tint m;\n\t\t\tfor (T t : elts) {\n\t\t\t\tm = Math.min(Convert.toInt(map1.get(t), 0), Convert.toInt(map2.get(t), 0));\n\t\t\t\tfor (int i = 0; i < m; i++) {\n\t\t\t\t\tlist.add(t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list;\n\t\t}\n\n\t\treturn new ArrayList<>();\n\t}\n\n\t/**\n\t * 多个集合的交集<br>\n\t * 针对一个集合中存在多个相同元素的情况，计算两个集合中此元素的个数，保留最少的个数<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c, c]，此结果中只保留了两个c\n\t *\n\t * @param <T>        集合元素类型\n\t * @param coll1      集合1\n\t * @param coll2      集合2\n\t * @param otherColls 其它集合\n\t * @return 交集的集合，返回 {@link ArrayList}\n\t */\n\t@SafeVarargs\n\tpublic static <T> Collection<T> intersection(Collection<T> coll1, Collection<T> coll2, Collection<T>... otherColls) {\n\t\tCollection<T> intersection = intersection(coll1, coll2);\n\t\tif (isEmpty(intersection)) {\n\t\t\treturn intersection;\n\t\t}\n\t\tfor (Collection<T> coll : otherColls) {\n\t\t\tintersection = intersection(intersection, coll);\n\t\t\tif (isEmpty(intersection)) {\n\t\t\t\treturn intersection;\n\t\t\t}\n\t\t}\n\t\treturn intersection;\n\t}\n\n\t/**\n\t * 多个集合的交集<br>\n\t * 针对一个集合中存在多个相同元素的情况，只保留一个<br>\n\t * 例如：集合1：[a, b, c, c, c]，集合2：[a, b, c, c]<br>\n\t * 结果：[a, b, c]，此结果中只保留了一个c\n\t *\n\t * @param <T>        集合元素类型\n\t * @param coll1      集合1\n\t * @param coll2      集合2\n\t * @param otherColls 其它集合\n\t * @return 交集的集合，返回 {@link LinkedHashSet}\n\t * @since 5.3.9\n\t */\n\t@SafeVarargs\n\tpublic static <T> Set<T> intersectionDistinct(Collection<T> coll1, Collection<T> coll2, Collection<T>... otherColls) {\n\t\tfinal Set<T> result;\n\t\tif (isEmpty(coll1) || isEmpty(coll2)) {\n\t\t\t// 有一个空集合就直接返回空\n\t\t\treturn new LinkedHashSet<>();\n\t\t} else {\n\t\t\tresult = new LinkedHashSet<>(coll1);\n\t\t}\n\n\t\tif (ArrayUtil.isNotEmpty(otherColls)) {\n\t\t\tfor (Collection<T> otherColl : otherColls) {\n\t\t\t\tif (isNotEmpty(otherColl)) {\n\t\t\t\t\tresult.retainAll(otherColl);\n\t\t\t\t} else {\n\t\t\t\t\t// 有一个空集合就直接返回空\n\t\t\t\t\treturn new LinkedHashSet<>();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult.retainAll(coll2);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 两个集合的对称差集 (A-B)∪(B-A)<br>\n\t * 针对一个集合中存在多个相同元素的情况，计算两个集合中此元素的个数，保留两个集合中此元素个数差的个数<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t *     disjunction([a, b, c, c, c], [a, b, c, c]) -》 [c]\n\t *     disjunction([a, b], [])                    -》 [a, b]\n\t *     disjunction([a, b, c], [b, c, d])          -》 [a, d]\n\t * </pre>\n\t * 任意一个集合为空，返回另一个集合<br>\n\t * 两个集合无差集则返回空集合\n\t *\n\t * @param <T>   集合元素类型\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @return 差集的集合，返回 {@link ArrayList}\n\t */\n\tpublic static <T> Collection<T> disjunction(Collection<T> coll1, Collection<T> coll2) {\n\t\tif (isEmpty(coll1)) {\n\t\t\treturn coll2;\n\t\t}\n\t\tif (isEmpty(coll2)) {\n\t\t\treturn coll1;\n\t\t}\n\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tfinal Map<T, Integer> map1 = countMap(coll1);\n\t\tfinal Map<T, Integer> map2 = countMap(coll2);\n\t\tfinal Set<T> elts = newHashSet(coll2);\n\t\telts.addAll(coll1);\n\t\tint m;\n\t\tfor (T t : elts) {\n\t\t\tm = Math.abs(Convert.toInt(map1.get(t), 0) - Convert.toInt(map2.get(t), 0));\n\t\t\tfor (int i = 0; i < m; i++) {\n\t\t\t\tresult.add(t);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算集合的单差集，即只返回【集合1】中有，但是【集合2】中没有的元素，例如：\n\t *\n\t * <pre>\n\t *     subtract([1,2,3,4],[2,3,4,5]) -》 [1]\n\t * </pre>\n\t *\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @param <T>   元素类型\n\t * @return 单差集\n\t */\n\tpublic static <T> Collection<T> subtract(Collection<T> coll1, Collection<T> coll2) {\n\t\tif(isEmpty(coll1) || isEmpty(coll2)){\n\t\t\treturn coll1;\n\t\t}\n\n\t\tCollection<T> result = ObjectUtil.clone(coll1);\n\t\ttry {\n\t\t\tif (null == result) {\n\t\t\t\tresult = CollUtil.create(coll1.getClass());\n\t\t\t\tresult.addAll(coll1);\n\t\t\t}\n\t\t\tresult.removeAll(coll2);\n\t\t} catch (UnsupportedOperationException e) {\n\t\t\t// 针对 coll1 为只读集合的补偿\n\t\t\tresult = CollUtil.create(AbstractCollection.class);\n\t\t\tresult.addAll(coll1);\n\t\t\tresult.removeAll(coll2);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算集合的单差集，即只返回【集合1】中有，但是【集合2】中没有的元素，例如：\n\t *\n\t * <pre>\n\t *     subtractToList([1,2,3,4],[2,3,4,5]) -》 [1]\n\t * </pre>\n\t *\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @param <T>   元素类型\n\t * @return 单差集\n\t * @since 5.3.5\n\t */\n\tpublic static <T> List<T> subtractToList(Collection<T> coll1, Collection<T> coll2) {\n\t\treturn subtractToList(coll1, coll2, true);\n\t}\n\n\t/**\n\t * 计算集合的单差集，即只返回【集合1】中有，但是【集合2】中没有的元素\n\t * 只要【集合1】中的某个元素在【集合2】中存在（equals和hashcode），就会被排除\n\t *\n\t * <pre>\n\t * 示例：\n\t * 1. subtractToList([null, null, null, null], [null, null]) → []\n\t * 2. subtractToList([null, null, null, null], [null, null, \"c\"]) → []\n\t * 3. subtractToList([\"a\", \"b\", \"c\"], [\"a\", \"b\", \"c\"]) → []\n\t * 4. subtractToList([], [\"a\", \"b\", \"c\"]) → []\n\t * 5. subtractToList([\"a\", \"b\", \"c\"], []) → [\"a\", \"b\", \"c\"]\n\t * 6. subtractToList([\"a\", \"a\", \"b\", \"b\", \"c\", \"c\", \"d\"], [\"b\", \"c\"]) → [\"a\", \"a\", \"d\"]\n\t * 7. subtractToList([\"a\", null, \"b\"], [\"a\", \"c\"]) → [null, \"b\"]\n\t * 8. subtractToList([\"a\", \"b\", \"c\"], [\"d\", \"e\", \"f\"]) → [\"a\", \"b\", \"c\"]\n\t * 9. subtractToList([\"a\", \"a\", \"b\", \"b\", \"c\"], [\"d\", \"e\", \"f\"]) → [\"a\", \"a\", \"b\", \"b\", \"c\"]\n\t * </pre>\n\t *\n\t * @param coll1    集合1，需要计算差集的源集合\n\t * @param coll2    集合2，需要从集合1中排除的元素所在集合\n\t * @param isLinked 返回的集合类型是否是LinkedList，{@code true}返回{@link LinkedList}，{@code false}返回{@link ArrayList}\n\t * @param <T>      元素类型\n\t * @return 单差集结果。当【集合1】为空时返回空列表；当【集合2】为空时返回集合1的拷贝；否则返回【集合1】中排除【集合2】中所有元素后的结果\n\t */\n\tpublic static <T> List<T> subtractToList(Collection<T> coll1, Collection<T> coll2, boolean isLinked) {\n\t\tif (isEmpty(coll1)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tif (isEmpty(coll2)) {\n\t\t\treturn ListUtil.list(isLinked, coll1);\n\t\t}\n\n\t\t/*\n\t\t\t返回的集合最大不会超过 coll1.size\n\t\t \t所以这里创建 ArrayList 时 initialCapacity 给的是 coll1.size\n\t\t \t这样做可以避免频繁扩容\n\t\t */\n\t\tfinal List<T> result = isLinked\n\t\t\t? new LinkedList<>()\n\t\t\t: new ArrayList<>(coll1.size());\n\n\t\tSet<T> set = new HashSet<>(coll2);\n\t\tfor (T t : coll1) {\n\t\t\tif (!set.contains(t)) {\n\t\t\t\tresult.add(t);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 判断指定集合是否包含指定值，如果集合为空（null或者空），返回{@code false}，否则找到元素返回{@code true}\n\t *\n\t * @param collection 集合\n\t * @param value      需要查找的值\n\t * @return 如果集合为空（null或者空），返回{@code false}，否则找到元素返回{@code true}\n\t * @throws ClassCastException   如果类型不一致会抛出转换异常\n\t * @throws NullPointerException 当指定的元素 值为 null ,或集合类不支持null 时抛出该异常\n\t * @see Collection#contains(Object)\n\t * @since 4.1.10\n\t */\n\tpublic static boolean contains(Collection<?> collection, Object value) {\n\t\treturn isNotEmpty(collection) && collection.contains(value);\n\t}\n\n\t/**\n\t * 判断指定集合是否包含指定值，如果集合为空（null或者空），返回{@code false}，否则找到元素返回{@code true}\n\t *\n\t * @param collection 集合\n\t * @param value      需要查找的值\n\t * @return 如果集合为空（null或者空），返回{@code false}，否则找到元素返回{@code true}\n\t * @since 5.7.16\n\t */\n\tpublic static boolean safeContains(Collection<?> collection, Object value) {\n\n\t\ttry {\n\t\t\treturn contains(collection, value);\n\t\t} catch (ClassCastException | NullPointerException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\n\t/**\n\t * 自定义函数判断集合是否包含某类值\n\t *\n\t * @param collection  集合\n\t * @param containFunc 自定义判断函数\n\t * @param <T>         值类型\n\t * @return 是否包含自定义规则的值\n\t */\n\tpublic static <T> boolean contains(Collection<T> collection, Predicate<? super T> containFunc) {\n\t\tif (isEmpty(collection)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (T t : collection) {\n\t\t\tif (containFunc.test(t)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 其中一个集合在另一个集合中是否至少包含一个元素，即是两个集合是否至少有一个共同的元素\n\t *\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @return 其中一个集合在另一个集合中是否至少包含一个元素\n\t * @see #intersection\n\t * @since 2.1\n\t */\n\tpublic static boolean containsAny(Collection<?> coll1, Collection<?> coll2) {\n\t\tif (isEmpty(coll1) || isEmpty(coll2)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (coll1.size() < coll2.size()) {\n\t\t\tfor (Object object : coll1) {\n\t\t\t\tif (coll2.contains(object)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor (Object object : coll2) {\n\t\t\t\tif (coll1.contains(object)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 集合1中是否包含集合2中所有的元素。<br>\n\t * 当集合1和集合2都为空时，返回{@code true}\n\t * 当集合2为空时，返回{@code true}\n\t *\n\t * @param coll1 集合1\n\t * @param coll2 集合2\n\t * @return 集合1中是否包含集合2中所有的元素\n\t * @since 4.5.12\n\t */\n\t@SuppressWarnings(\"SuspiciousMethodCalls\")\n\tpublic static boolean containsAll(Collection<?> coll1, Collection<?> coll2) {\n\t\tif (isEmpty(coll1)) {\n\t\t\treturn isEmpty(coll2);\n\t\t}\n\n\t\tif (isEmpty(coll2)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Set直接判定\n\t\tif(coll1 instanceof Set){\n\t\t\treturn coll1.containsAll(coll2);\n\t\t}\n\n\t\t// 参考Apache commons collection4\n\t\t// 将时间复杂度降低到O(n + m)\n\t\tfinal Iterator<?> it = coll1.iterator();\n\t\tfinal Set<Object> elementsAlreadySeen = new HashSet<>(coll1.size(), 1);\n\t\tfor (final Object nextElement : coll2) {\n\t\t\tif (elementsAlreadySeen.contains(nextElement)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tboolean foundCurrentElement = false;\n\t\t\twhile (it.hasNext()) {\n\t\t\t\tfinal Object p = it.next();\n\t\t\t\telementsAlreadySeen.add(p);\n\t\t\t\tif (Objects.equals(nextElement, p)) {\n\t\t\t\t\tfoundCurrentElement = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!foundCurrentElement) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 根据集合返回一个元素计数的 {@link Map}<br>\n\t * 所谓元素计数就是假如这个集合中某个元素出现了n次，那将这个元素做为key，n做为value<br>\n\t * 例如：[a,b,c,c,c] 得到：<br>\n\t * a: 1<br>\n\t * b: 1<br>\n\t * c: 3<br>\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return {@link Map}\n\t * @see IterUtil#countMap(Iterator)\n\t */\n\tpublic static <T> Map<T, Integer> countMap(Iterable<T> collection) {\n\t\treturn IterUtil.countMap(null == collection ? null : collection.iterator());\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterable    {@link Iterable}\n\t * @param conjunction 分隔符\n\t * @param func        集合元素转换器，将元素转换为字符串\n\t * @return 连接后的字符串\n\t * @see IterUtil#join(Iterator, CharSequence, Function)\n\t * @since 5.6.7\n\t */\n\tpublic static <T> String join(Iterable<T> iterable, CharSequence conjunction, Function<T, ? extends CharSequence> func) {\n\t\tif (null == iterable) {\n\t\t\treturn null;\n\t\t}\n\t\treturn IterUtil.join(iterable.iterator(), conjunction, func);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串<br>\n\t * 如果集合元素为数组、{@link Iterable}或{@link Iterator}，则递归组合其为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterable    {@link Iterable}\n\t * @param conjunction 分隔符\n\t * @return 连接后的字符串\n\t * @see IterUtil#join(Iterator, CharSequence)\n\t */\n\tpublic static <T> String join(Iterable<T> iterable, CharSequence conjunction) {\n\t\tif (null == iterable) {\n\t\t\treturn null;\n\t\t}\n\t\treturn IterUtil.join(iterable.iterator(), conjunction);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterable    {@link Iterable}\n\t * @param conjunction 分隔符\n\t * @param prefix      每个元素添加的前缀，null表示不添加\n\t * @param suffix      每个元素添加的后缀，null表示不添加\n\t * @return 连接后的字符串\n\t * @since 5.3.0\n\t */\n\tpublic static <T> String join(Iterable<T> iterable, CharSequence conjunction, String prefix, String suffix) {\n\t\tif (null == iterable) {\n\t\t\treturn null;\n\t\t}\n\t\treturn IterUtil.join(iterable.iterator(), conjunction, prefix, suffix);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串<br>\n\t * 如果集合元素为数组、{@link Iterable}或{@link Iterator}，则递归组合其为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterator    集合\n\t * @param conjunction 分隔符\n\t * @return 连接后的字符串\n\t * @deprecated 请使用IterUtil#join(Iterator, CharSequence)\n\t */\n\t@Deprecated\n\tpublic static <T> String join(Iterator<T> iterator, CharSequence conjunction) {\n\t\treturn IterUtil.join(iterator, conjunction);\n\t}\n\n\t/**\n\t * 切取部分数据<br>\n\t * 切取后的栈将减少这些元素\n\t *\n\t * @param <T>             集合元素类型\n\t * @param surplusAlaDatas 原数据\n\t * @param partSize        每部分数据的长度\n\t * @return 切取出的数据或null\n\t */\n\tpublic static <T> List<T> popPart(Stack<T> surplusAlaDatas, int partSize) {\n\t\tif (isEmpty(surplusAlaDatas)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal List<T> currentAlaDatas = new ArrayList<>();\n\t\tint size = surplusAlaDatas.size();\n\t\t// 切割\n\t\tif (size > partSize) {\n\t\t\tfor (int i = 0; i < partSize; i++) {\n\t\t\t\tcurrentAlaDatas.add(surplusAlaDatas.pop());\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = 0; i < size; i++) {\n\t\t\t\tcurrentAlaDatas.add(surplusAlaDatas.pop());\n\t\t\t}\n\t\t}\n\t\treturn currentAlaDatas;\n\t}\n\n\t/**\n\t * 切取部分数据<br>\n\t * 切取后的栈将减少这些元素\n\t *\n\t * @param <T>             集合元素类型\n\t * @param surplusAlaDatas 原数据\n\t * @param partSize        每部分数据的长度\n\t * @return 切取出的数据或null\n\t */\n\tpublic static <T> List<T> popPart(Deque<T> surplusAlaDatas, int partSize) {\n\t\tif (isEmpty(surplusAlaDatas)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal List<T> currentAlaDatas = new ArrayList<>();\n\t\tint size = surplusAlaDatas.size();\n\t\t// 切割\n\t\tif (size > partSize) {\n\t\t\tfor (int i = 0; i < partSize; i++) {\n\t\t\t\tcurrentAlaDatas.add(surplusAlaDatas.pop());\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = 0; i < size; i++) {\n\t\t\t\tcurrentAlaDatas.add(surplusAlaDatas.pop());\n\t\t\t}\n\t\t}\n\t\treturn currentAlaDatas;\n\t}\n\n\t/**\n\t * 是否至少有一个符合判断条件\n\t *\n\t * @param <T> 集合元素类型\n\t * @param collection 集合\n\t * @param predicate 自定义判断函数\n\t * @return 是否有一个值匹配 布尔值\n\t */\n\tpublic static <T>boolean anyMatch(Collection<T> collection,Predicate<T> predicate){\n\t\tif(isEmpty(collection)){\n\t\t\treturn Boolean.FALSE;\n\t\t}\n\t\treturn collection.stream().anyMatch(predicate);\n\t}\n\n\t/**\n\t * 是否全部匹配判断条件\n\t *\n\t * @param <T> 集合元素类型\n\t * @param collection 集合\n\t * @param predicate  自定义判断函数\n\t * @return 是否全部匹配 布尔值\n\t */\n\tpublic static <T>boolean allMatch(Collection<T> collection,Predicate<T> predicate){\n\t\tif(isEmpty(collection)){\n\t\t\treturn Boolean.FALSE;\n\t\t}\n\t\treturn collection.stream().allMatch(predicate);\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- new HashSet\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T> 集合元素类型\n\t * @param ts  元素数组\n\t * @return HashSet对象\n\t */\n\t@SafeVarargs\n\tpublic static <T> HashSet<T> newHashSet(T... ts) {\n\t\treturn set(false, ts);\n\t}\n\n\t/**\n\t * 新建一个LinkedHashSet\n\t *\n\t * @param <T> 集合元素类型\n\t * @param ts  元素数组\n\t * @return HashSet对象\n\t * @since 4.1.10\n\t */\n\t@SafeVarargs\n\tpublic static <T> LinkedHashSet<T> newLinkedHashSet(T... ts) {\n\t\treturn (LinkedHashSet<T>) set(true, ts);\n\t}\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isSorted 是否有序，有序返回 {@link LinkedHashSet}，否则返回 {@link HashSet}\n\t * @param ts       元素数组\n\t * @return HashSet对象\n\t */\n\t@SafeVarargs\n\tpublic static <T> HashSet<T> set(boolean isSorted, T... ts) {\n\t\tif (null == ts) {\n\t\t\treturn isSorted ? new LinkedHashSet<>() : new HashSet<>();\n\t\t}\n\t\tint initialCapacity = Math.max((int) (ts.length / .75f) + 1, 16);\n\t\tfinal HashSet<T> set = isSorted ? new LinkedHashSet<>(initialCapacity) : new HashSet<>(initialCapacity);\n\t\tCollections.addAll(set, ts);\n\t\treturn set;\n\t}\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return HashSet对象\n\t */\n\tpublic static <T> HashSet<T> newHashSet(Collection<T> collection) {\n\t\treturn newHashSet(false, collection);\n\t}\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T>        集合元素类型\n\t * @param isSorted   是否有序，有序返回 {@link LinkedHashSet}，否则返回{@link HashSet}\n\t * @param collection 集合，用于初始化Set\n\t * @return HashSet对象\n\t */\n\tpublic static <T> HashSet<T> newHashSet(boolean isSorted, Collection<T> collection) {\n\t\treturn isSorted ? new LinkedHashSet<>(collection) : new HashSet<>(collection);\n\t}\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isSorted 是否有序，有序返回 {@link LinkedHashSet}，否则返回{@link HashSet}\n\t * @param iter     {@link Iterator}\n\t * @return HashSet对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> HashSet<T> newHashSet(boolean isSorted, Iterator<T> iter) {\n\t\tif (null == iter) {\n\t\t\treturn set(isSorted, (T[]) null);\n\t\t}\n\t\tfinal HashSet<T> set = isSorted ? new LinkedHashSet<>() : new HashSet<>();\n\t\twhile (iter.hasNext()) {\n\t\t\tset.add(iter.next());\n\t\t}\n\t\treturn set;\n\t}\n\n\t/**\n\t * 新建一个HashSet\n\t *\n\t * @param <T>         集合元素类型\n\t * @param isSorted    是否有序，有序返回 {@link LinkedHashSet}，否则返回{@link HashSet}\n\t * @param enumeration {@link Enumeration}\n\t * @return HashSet对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> HashSet<T> newHashSet(boolean isSorted, Enumeration<T> enumeration) {\n\t\tif (null == enumeration) {\n\t\t\treturn set(isSorted, (T[]) null);\n\t\t}\n\t\tfinal HashSet<T> set = isSorted ? new LinkedHashSet<>() : new HashSet<>();\n\t\twhile (enumeration.hasMoreElements()) {\n\t\t\tset.add(enumeration.nextElement());\n\t\t}\n\t\treturn set;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- List\n\n\t/**\n\t * 新建一个空List\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked) {\n\t\treturn ListUtil.list(isLinked);\n\t}\n\n\t/**\n\t * 新建一个List\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param values   数组\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\t@SafeVarargs\n\tpublic static <T> List<T> list(boolean isLinked, T... values) {\n\t\treturn ListUtil.list(isLinked, values);\n\t}\n\n\t/**\n\t * 新建一个List\n\t *\n\t * @param <T>        集合元素类型\n\t * @param isLinked   是否新建LinkedList\n\t * @param collection 集合\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Collection<T> collection) {\n\t\treturn ListUtil.list(isLinked, collection);\n\t}\n\n\t/**\n\t * 新建一个List<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param iterable {@link Iterable}\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Iterable<T> iterable) {\n\t\treturn ListUtil.list(isLinked, iterable);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param iter     {@link Iterator}\n\t * @return ArrayList对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Iterator<T> iter) {\n\t\treturn ListUtil.list(isLinked, iter);\n\t}\n\n\t/**\n\t * 新建一个List<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>         集合元素类型\n\t * @param isLinked    是否新建LinkedList\n\t * @param enumeration {@link Enumeration}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Enumeration<T> enumeration) {\n\t\treturn ListUtil.list(isLinked, enumeration);\n\t}\n\n\t/**\n\t * 新建一个ArrayList\n\t *\n\t * @param <T>    集合元素类型\n\t * @param values 数组\n\t * @return ArrayList对象\n\t * @see #toList(Object[])\n\t */\n\t@SafeVarargs\n\tpublic static <T> ArrayList<T> newArrayList(T... values) {\n\t\treturn ListUtil.toList(values);\n\t}\n\n\t/**\n\t * 数组转为ArrayList\n\t *\n\t * @param <T>    集合元素类型\n\t * @param values 数组\n\t * @return ArrayList对象\n\t * @since 4.0.11\n\t */\n\t@SafeVarargs\n\tpublic static <T> ArrayList<T> toList(T... values) {\n\t\treturn ListUtil.toList(values);\n\t}\n\n\t/**\n\t * 新建一个ArrayList\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return ArrayList对象\n\t */\n\tpublic static <T> ArrayList<T> newArrayList(Collection<T> collection) {\n\t\treturn ListUtil.toList(collection);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return ArrayList对象\n\t * @since 3.1.0\n\t */\n\tpublic static <T> ArrayList<T> newArrayList(Iterable<T> iterable) {\n\t\treturn ListUtil.toList(iterable);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> ArrayList<T> newArrayList(Iterator<T> iterator) {\n\t\treturn ListUtil.toList(iterator);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>         集合元素类型\n\t * @param enumeration {@link Enumeration}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> ArrayList<T> newArrayList(Enumeration<T> enumeration) {\n\t\treturn ListUtil.toList(enumeration);\n\t}\n\n\t// ----------------------------------------------------------------------new LinkedList\n\n\t/**\n\t * 新建LinkedList\n\t *\n\t * @param values 数组\n\t * @param <T>    类型\n\t * @return LinkedList\n\t * @since 4.1.2\n\t */\n\t@SafeVarargs\n\tpublic static <T> LinkedList<T> newLinkedList(T... values) {\n\t\treturn ListUtil.toLinkedList(values);\n\t}\n\n\t/**\n\t * 新建一个CopyOnWriteArrayList\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return {@link CopyOnWriteArrayList}\n\t */\n\tpublic static <T> CopyOnWriteArrayList<T> newCopyOnWriteArrayList(Collection<T> collection) {\n\t\treturn ListUtil.toCopyOnWriteArrayList(collection);\n\t}\n\n\t/**\n\t * 新建{@link BlockingQueue}<br>\n\t * 在队列为空时，获取元素的线程会等待队列变为非空。当队列满时，存储元素的线程会等待队列可用。\n\t *\n\t * @param <T>      集合类型\n\t * @param capacity 容量\n\t * @param isLinked 是否为链表形式\n\t * @return {@link BlockingQueue}\n\t * @since 3.3.0\n\t */\n\tpublic static <T> BlockingQueue<T> newBlockingQueue(int capacity, boolean isLinked) {\n\t\tfinal BlockingQueue<T> queue;\n\t\tif (isLinked) {\n\t\t\tqueue = new LinkedBlockingDeque<>(capacity);\n\t\t} else {\n\t\t\tqueue = new ArrayBlockingQueue<>(capacity);\n\t\t}\n\t\treturn queue;\n\t}\n\n\t/**\n\t * 创建新的集合对象\n\t *\n\t * @param <T>            集合类型\n\t * @param collectionType 集合类型\n\t * @return 集合类型对应的实例\n\t * @since 3.0.8\n\t */\n\tpublic static <T> Collection<T> create(Class<?> collectionType) {\n\t\treturn create(collectionType, null);\n\t}\n\n\t/**\n\t * 创建新的集合对象，返回具体的泛型集合\n\t *\n\t * @param <T>            集合元素类型\n\t * @param collectionType 集合类型，rawtype 如 ArrayList.class, EnumSet.class ...\n\t * @param elementType    集合元素类型\n\t * @return 集合类型对应的实例\n\t * @since v5\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T> Collection<T> create(Class<?> collectionType, Class<T> elementType) {\n\t\tfinal Collection<T> list;\n\t\tif (collectionType.isAssignableFrom(AbstractCollection.class)) {\n\t\t\t// 抽象集合默认使用ArrayList\n\t\t\tlist = new ArrayList<>();\n\t\t}\n\n\t\t// Set\n\t\telse if (collectionType.isAssignableFrom(HashSet.class)) {\n\t\t\tlist = new HashSet<>();\n\t\t} else if (collectionType.isAssignableFrom(LinkedHashSet.class)) {\n\t\t\tlist = new LinkedHashSet<>();\n\t\t} else if (collectionType.isAssignableFrom(TreeSet.class)) {\n\t\t\tlist = new TreeSet<>((o1, o2) -> {\n\t\t\t\t// 优先按照对象本身比较，如果没有实现比较接口，默认按照toString内容比较\n\t\t\t\tif (o1 instanceof Comparable) {\n\t\t\t\t\treturn ((Comparable<T>) o1).compareTo(o2);\n\t\t\t\t}\n\t\t\t\treturn CompareUtil.compare(o1.toString(), o2.toString());\n\t\t\t});\n\t\t} else if (collectionType.isAssignableFrom(EnumSet.class)) {\n\t\t\tlist = (Collection<T>) EnumSet.noneOf(Assert.notNull((Class<Enum>) elementType));\n\t\t}\n\n\t\t// List\n\t\telse if (collectionType.isAssignableFrom(ArrayList.class)) {\n\t\t\tlist = new ArrayList<>();\n\t\t} else if (collectionType.isAssignableFrom(LinkedList.class)) {\n\t\t\tlist = new LinkedList<>();\n\t\t}\n\n\t\t// Others，直接实例化\n\t\telse {\n\t\t\ttry {\n\t\t\t\tlist = (Collection<T>) ReflectUtil.newInstance(collectionType);\n\t\t\t} catch (final Exception e) {\n\t\t\t\t// 无法创建当前类型的对象，尝试创建父类型对象\n\t\t\t\tfinal Class<?> superclass = collectionType.getSuperclass();\n\t\t\t\tif (null != superclass && collectionType != superclass) {\n\t\t\t\t\treturn create(superclass);\n\t\t\t\t}\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 去重集合\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return {@link ArrayList}\n\t */\n\tpublic static <T> ArrayList<T> distinct(Collection<T> collection) {\n\t\tif (isEmpty(collection)) {\n\t\t\treturn new ArrayList<>();\n\t\t} else if (collection instanceof Set) {\n\t\t\treturn new ArrayList<>(collection);\n\t\t} else {\n\t\t\treturn new ArrayList<>(new LinkedHashSet<>(collection));\n\t\t}\n\t}\n\n\t/**\n\t * 根据函数生成的KEY去重集合，如根据Bean的某个或者某些字段完成去重。<br>\n\t * 去重可选是保留最先加入的值还是后加入的值\n\t *\n\t * @param <T>             集合元素类型\n\t * @param <K>             唯一键类型\n\t * @param collection      集合\n\t * @param uniqueGenerator 唯一键生成器\n\t * @param override        是否覆盖模式，如果为{@code true}，加入的新值会覆盖相同key的旧值，否则会忽略新加值\n\t * @return {@link ArrayList}\n\t * @since 5.8.0\n\t */\n\tpublic static <T, K> List<T> distinct(Collection<T> collection, Function<T, K> uniqueGenerator, boolean override) {\n\t\tif (isEmpty(collection)) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\n\t\tfinal UniqueKeySet<K, T> set = new UniqueKeySet<>(true, uniqueGenerator);\n\t\tif (override) {\n\t\t\tset.addAll(collection);\n\t\t} else {\n\t\t\tset.addAllIfAbsent(collection);\n\t\t}\n\t\treturn new ArrayList<>(set);\n\t}\n\n\t/**\n\t * 截取列表的部分\n\t *\n\t * @param <T>   集合元素类型\n\t * @param list  被截取的数组\n\t * @param start 开始位置（包含）\n\t * @param end   结束位置（不包含）\n\t * @return 截取后的数组，当开始位置超过最大时，返回空的List\n\t * @see ListUtil#sub(List, int, int)\n\t */\n\tpublic static <T> List<T> sub(List<T> list, int start, int end) {\n\t\treturn ListUtil.sub(list, start, end);\n\t}\n\n\t/**\n\t * 截取列表的部分\n\t *\n\t * @param <T>   集合元素类型\n\t * @param list  被截取的数组\n\t * @param start 开始位置（包含）\n\t * @param end   结束位置（不包含）\n\t * @param step  步进\n\t * @return 截取后的数组，当开始位置超过最大时，返回空的List\n\t * @see ListUtil#sub(List, int, int, int)\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sub(List<T> list, int start, int end, int step) {\n\t\treturn ListUtil.sub(list, start, end, step);\n\t}\n\n\t/**\n\t * 截取集合的部分\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 被截取的数组\n\t * @param start      开始位置（包含）\n\t * @param end        结束位置（不包含）\n\t * @return 截取后的数组，当开始位置超过最大时，返回null\n\t */\n\tpublic static <T> List<T> sub(Collection<T> collection, int start, int end) {\n\t\treturn sub(collection, start, end, 1);\n\t}\n\n\t/**\n\t * 截取集合的部分\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 被截取的数组\n\t * @param start      开始位置（包含）\n\t * @param end        结束位置（不包含）\n\t * @param step       步进\n\t * @return 截取后的数组，当开始位置超过最大时，返回空集合\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sub(Collection<T> collection, int start, int end, int step) {\n\t\tif (isEmpty(collection)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal List<T> list = collection instanceof List ? (List<T>) collection : ListUtil.toList(collection);\n\t\treturn sub(list, start, end, step);\n\t}\n\n\t/**\n\t * 对集合按照指定长度分段，每一个段为单独的集合，返回这个集合的列表\n\t * <p>\n\t * 需要特别注意的是，此方法调用{@link List#subList(int, int)}切分List，\n\t * 此方法返回的是原List的视图，也就是说原List有变更，切分后的结果也会变更。\n\t * </p>\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @param size 每个段的长度\n\t * @return 分段列表\n\t * @since 5.4.5\n\t * @deprecated 请使用 {@link ListUtil#partition(List, int)}\n\t */\n\t@Deprecated\n\tpublic static <T> List<List<T>> splitList(List<T> list, int size) {\n\t\treturn ListUtil.partition(list, size);\n\t}\n\n\t/**\n\t * 对集合按照指定长度分段，每一个段为单独的集合，返回这个集合的列表\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param size       每个段的长度\n\t * @return 分段列表\n\t */\n\tpublic static <T> List<List<T>> split(Collection<T> collection, int size) {\n\t\tfinal List<List<T>> result = new ArrayList<>();\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\treturn result;\n\t\t}\n\n\t\tfinal int initSize = Math.min(collection.size(), size);\n\t\tList<T> subList = new ArrayList<>(initSize);\n\t\tfor (T t : collection) {\n\t\t\tif (subList.size() >= size) {\n\t\t\t\tresult.add(subList);\n\t\t\t\tsubList = new ArrayList<>(initSize);\n\t\t\t}\n\t\t\tsubList.add(t);\n\t\t}\n\t\tresult.add(subList);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 编辑，此方法产生一个新集合<br>\n\t * 编辑过程通过传入的Editor实现来返回需要的元素内容，这个Editor实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，如果返回null表示这个元素对象抛弃\n\t * 2、修改元素对象，返回集合中为修改后的对象\n\t * </pre>\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param editor     编辑器接口，{@code null}返回原集合\n\t * @return 过滤后的集合\n\t */\n\tpublic static <T> Collection<T> edit(Collection<T> collection, Editor<T> editor) {\n\t\tif (null == collection || null == editor) {\n\t\t\treturn collection;\n\t\t}\n\n\t\tfinal Collection<T> collection2 = create(collection.getClass());\n\t\tif (isEmpty(collection)) {\n\t\t\treturn collection2;\n\t\t}\n\n\t\tT modified;\n\t\tfor (T t : collection) {\n\t\t\tmodified = editor.edit(t);\n\t\t\tif (null != modified) {\n\t\t\t\tcollection2.add(modified);\n\t\t\t}\n\t\t}\n\t\treturn collection2;\n\t}\n\n\t/**\n\t * 过滤<br>\n\t * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容，这个Filter实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中\n\t * </pre>\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param filter     过滤器，{@code null}返回原集合\n\t * @return 过滤后的数组\n\t * @since 3.1.0\n\t */\n\tpublic static <T> Collection<T> filterNew(Collection<T> collection, Filter<T> filter) {\n\t\tif (null == collection || null == filter) {\n\t\t\treturn collection;\n\t\t}\n\t\treturn edit(collection, t -> filter.accept(t) ? t : null);\n\t}\n\n\t/**\n\t * 去掉集合中的多个元素，此方法直接修改原集合\n\t *\n\t * @param <T>         集合类型\n\t * @param <E>         集合元素类型\n\t * @param collection  集合\n\t * @param elesRemoved 被去掉的元素数组\n\t * @return 原集合\n\t * @since 4.1.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Collection<E>, E> T removeAny(T collection, E... elesRemoved) {\n\t\tcollection.removeAll(newHashSet(elesRemoved));\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 去除指定元素，此方法直接修改原集合\n\t *\n\t * @param <T>        集合类型\n\t * @param <E>        集合元素类型\n\t * @param collection 集合\n\t * @param filter     过滤器\n\t * @return 处理后的集合\n\t * @since 4.6.5\n\t */\n\tpublic static <T extends Collection<E>, E> T filter(T collection, final Filter<E> filter) {\n\t\treturn IterUtil.filter(collection, filter);\n\t}\n\n\t/**\n\t * 去除{@code null} 元素，此方法直接修改原集合\n\t *\n\t * @param <T>        集合类型\n\t * @param <E>        集合元素类型\n\t * @param collection 集合\n\t * @return 处理后的集合\n\t * @since 3.2.2\n\t */\n\tpublic static <T extends Collection<E>, E> T removeNull(T collection) {\n\t\treturn filter(collection, Objects::nonNull);\n\t}\n\n\t/**\n\t * 去除{@code null}或者\"\" 元素，此方法直接修改原集合\n\t *\n\t * @param <T>        集合类型\n\t * @param <E>        集合元素类型\n\t * @param collection 集合\n\t * @return 处理后的集合\n\t * @since 3.2.2\n\t */\n\tpublic static <T extends Collection<E>, E extends CharSequence> T removeEmpty(T collection) {\n\t\treturn filter(collection, StrUtil::isNotEmpty);\n\t}\n\n\t/**\n\t * 去除{@code null}或者\"\"或者空白字符串 元素，此方法直接修改原集合\n\t *\n\t * @param <T>        集合类型\n\t * @param <E>        集合元素类型\n\t * @param collection 集合\n\t * @return 处理后的集合\n\t * @since 3.2.2\n\t */\n\tpublic static <T extends Collection<E>, E extends CharSequence> T removeBlank(T collection) {\n\t\treturn filter(collection, StrUtil::isNotBlank);\n\t}\n\n\t/**\n\t * 移除集合中的多个元素，并将结果存放到指定的集合\n\t * 此方法直接修改原集合\n\t *\n\t * @param <T>              集合类型\n\t * @param <E>              集合元素类型\n\t * @param resultCollection 存放移除结果的集合\n\t * @param targetCollection 被操作移除元素的集合\n\t * @param predicate        用于是否移除判断的过滤器\n\t * @return 移除结果的集合\n\t * @since 5.7.17\n\t */\n\tpublic static <T extends Collection<E>, E> T removeWithAddIf(T targetCollection, T resultCollection, Predicate<? super E> predicate) {\n\t\tObjects.requireNonNull(predicate);\n\t\tfinal Iterator<E> each = targetCollection.iterator();\n\t\twhile (each.hasNext()) {\n\t\t\tE next = each.next();\n\t\t\tif (predicate.test(next)) {\n\t\t\t\tresultCollection.add(next);\n\t\t\t\teach.remove();\n\t\t\t}\n\t\t}\n\t\treturn resultCollection;\n\t}\n\n\t/**\n\t * 移除集合中的多个元素，并将结果存放到生成的新集合中后返回<br>\n\t * 此方法直接修改原集合\n\t *\n\t * @param <T>              集合类型\n\t * @param <E>              集合元素类型\n\t * @param targetCollection 被操作移除元素的集合\n\t * @param predicate        用于是否移除判断的过滤器\n\t * @return 移除结果的集合\n\t * @since 5.7.17\n\t */\n\tpublic static <T extends Collection<E>, E> List<E> removeWithAddIf(T targetCollection, Predicate<? super E> predicate) {\n\t\tfinal List<E> removed = new ArrayList<>();\n\t\tremoveWithAddIf(targetCollection, removed, predicate);\n\t\treturn removed;\n\t}\n\n\t/**\n\t * 通过Editor抽取集合元素中的某些值返回为新列表<br>\n\t * 例如提供的是一个Bean列表，通过Editor接口实现获取某个字段值，返回这个字段值组成的新列表\n\t *\n\t * @param collection 原集合\n\t * @param editor     编辑器\n\t * @return 抽取后的新列表\n\t */\n\tpublic static List<Object> extract(Iterable<?> collection, Editor<Object> editor) {\n\t\treturn extract(collection, editor, false);\n\t}\n\n\t/**\n\t * 通过Editor抽取集合元素中的某些值返回为新列表<br>\n\t * 例如提供的是一个Bean列表，通过Editor接口实现获取某个字段值，返回这个字段值组成的新列表\n\t *\n\t * @param collection 原集合\n\t * @param editor     编辑器\n\t * @param ignoreNull 是否忽略空值\n\t * @return 抽取后的新列表\n\t * @see #map(Iterable, Function, boolean)\n\t * @since 4.5.7\n\t */\n\tpublic static List<Object> extract(Iterable<?> collection, Editor<Object> editor, boolean ignoreNull) {\n\t\treturn map(collection, editor::edit, ignoreNull);\n\t}\n\n\t/**\n\t * 通过func自定义一个规则，此规则将原集合中的元素转换成新的元素，生成新的列表返回<br>\n\t * 例如提供的是一个Bean列表，通过Function接口实现获取某个字段值，返回这个字段值组成的新列表\n\t *\n\t * @param <T>        集合元素类型\n\t * @param <R>        返回集合元素类型\n\t * @param collection 原集合\n\t * @param func       编辑函数\n\t * @param ignoreNull 是否忽略空值，这里的空值包括函数处理前和处理后的null值\n\t * @return 抽取后的新列表\n\t * @since 5.3.5\n\t */\n\tpublic static <T, R> List<R> map(Iterable<T> collection, Function<? super T, ? extends R> func, boolean ignoreNull) {\n\t\tfinal List<R> fieldValueList = new ArrayList<>();\n\t\tif (null == collection) {\n\t\t\treturn fieldValueList;\n\t\t}\n\n\t\tR value;\n\t\tfor (T t : collection) {\n\t\t\tif (null == t && ignoreNull) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvalue = func.apply(t);\n\t\t\tif (null == value && ignoreNull) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfieldValueList.add(value);\n\t\t}\n\t\treturn fieldValueList;\n\t}\n\n\t/**\n\t * 获取给定Bean列表中指定字段名对应字段值的列表<br>\n\t * 列表元素支持Bean与Map\n\t *\n\t * @param collection Bean集合或Map集合\n\t * @param fieldName  字段名或map的键\n\t * @return 字段值列表\n\t * @since 3.1.0\n\t */\n\tpublic static List<Object> getFieldValues(Iterable<?> collection, final String fieldName) {\n\t\treturn getFieldValues(collection, fieldName, false);\n\t}\n\n\t/**\n\t * 获取给定Bean列表中指定字段名对应字段值的列表<br>\n\t * 列表元素支持Bean与Map\n\t *\n\t * @param collection Bean集合或Map集合\n\t * @param fieldName  字段名或map的键\n\t * @param ignoreNull 是否忽略值为{@code null}的字段\n\t * @return 字段值列表\n\t * @since 4.5.7\n\t */\n\tpublic static List<Object> getFieldValues(Iterable<?> collection, final String fieldName, boolean ignoreNull) {\n\t\treturn map(collection, bean -> {\n\t\t\tif (bean instanceof Map) {\n\t\t\t\treturn ((Map<?, ?>) bean).get(fieldName);\n\t\t\t} else {\n\t\t\t\treturn ReflectUtil.getFieldValue(bean, fieldName);\n\t\t\t}\n\t\t}, ignoreNull);\n\t}\n\n\t/**\n\t * 获取给定Bean列表中指定字段名对应字段值的列表<br>\n\t * 列表元素支持Bean与Map\n\t *\n\t * @param <T>         元素类型\n\t * @param collection  Bean集合或Map集合\n\t * @param fieldName   字段名或map的键\n\t * @param elementType 元素类型类\n\t * @return 字段值列表\n\t * @since 4.5.6\n\t */\n\tpublic static <T> List<T> getFieldValues(Iterable<?> collection, final String fieldName, final Class<T> elementType) {\n\t\tList<Object> fieldValues = getFieldValues(collection, fieldName);\n\t\treturn Convert.toList(elementType, fieldValues);\n\t}\n\n\t/**\n\t * 字段值与列表值对应的Map，常用于元素对象中有唯一ID时需要按照这个ID查找对象的情况<br>\n\t * 例如：车牌号 =》车\n\t *\n\t * @param <K>       字段名对应值得类型，不确定请使用Object\n\t * @param <V>       对象类型\n\t * @param iterable  对象列表\n\t * @param fieldName 字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 5.0.6\n\t */\n\tpublic static <K, V> Map<K, V> fieldValueMap(Iterable<V> iterable, String fieldName) {\n\t\treturn IterUtil.fieldValueMap(IterUtil.getIter(iterable), fieldName);\n\t}\n\n\t/**\n\t * 两个字段值组成新的Map\n\t *\n\t * @param <K>               字段名对应值得类型，不确定请使用Object\n\t * @param <V>               值类型，不确定使用Object\n\t * @param iterable          对象列表\n\t * @param fieldNameForKey   做为键的字段名（会通过反射获取其值）\n\t * @param fieldNameForValue 做为值的字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 5.0.6\n\t */\n\tpublic static <K, V> Map<K, V> fieldValueAsMap(Iterable<?> iterable, String fieldNameForKey, String fieldNameForValue) {\n\t\treturn IterUtil.fieldValueAsMap(IterUtil.getIter(iterable), fieldNameForKey, fieldNameForValue);\n\t}\n\n\t/**\n\t * 查找第一个匹配元素对象\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param filter     过滤器，满足过滤条件的第一个元素将被返回\n\t * @return 满足过滤条件的第一个元素\n\t * @since 3.1.0\n\t */\n\tpublic static <T> T findOne(Iterable<T> collection, Filter<T> filter) {\n\t\tif (null != collection) {\n\t\t\tfor (T t : collection) {\n\t\t\t\tif (filter.accept(t)) {\n\t\t\t\t\treturn t;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 查找第一个匹配元素对象<br>\n\t * 如果集合元素是Map，则比对键和值是否相同，相同则返回<br>\n\t * 如果为普通Bean，则通过反射比对元素字段名对应的字段值是否相同，相同则返回<br>\n\t * 如果给定字段值参数是{@code null} 且元素对象中的字段值也为{@code null}则认为相同\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合，集合元素可以是Bean或者Map\n\t * @param fieldName  集合元素对象的字段名或map的键\n\t * @param fieldValue 集合元素对象的字段值或map的值\n\t * @return 满足条件的第一个元素\n\t * @since 3.1.0\n\t */\n\tpublic static <T> T findOneByField(Iterable<T> collection, final String fieldName, final Object fieldValue) {\n\t\treturn findOne(collection, t -> {\n\t\t\tif (t instanceof Map) {\n\t\t\t\tfinal Map<?, ?> map = (Map<?, ?>) t;\n\t\t\t\tfinal Object value = map.get(fieldName);\n\t\t\t\treturn ObjectUtil.equal(value, fieldValue);\n\t\t\t}\n\n\t\t\t// 普通Bean\n\t\t\tfinal Object value = ReflectUtil.getFieldValue(t, fieldName);\n\t\t\treturn ObjectUtil.equal(value, fieldValue);\n\t\t});\n\t}\n\n\t/**\n\t * 集合中匹配规则的数量\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @param matcher  匹配器，为空则全部匹配\n\t * @return 匹配数量\n\t */\n\tpublic static <T> int count(Iterable<T> iterable, Matcher<T> matcher) {\n\t\tint count = 0;\n\t\tif (null != iterable) {\n\t\t\tfor (T t : iterable) {\n\t\t\t\tif (null == matcher || matcher.match(t)) {\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 获取匹配规则定义中匹配到元素的第一个位置<br>\n\t * 此方法对于某些无序集合的位置信息，以转换为数组后的位置为准。\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param matcher    匹配器，为空则全部匹配\n\t * @return 第一个位置\n\t * @since 5.6.6\n\t */\n\tpublic static <T> int indexOf(Collection<T> collection, Matcher<T> matcher) {\n\t\tif (isNotEmpty(collection)) {\n\t\t\tint index = 0;\n\t\t\tfor (T t : collection) {\n\t\t\t\tif (null == matcher || matcher.match(t)) {\n\t\t\t\t\treturn index;\n\t\t\t\t}\n\t\t\t\tindex++;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 获取匹配规则定义中匹配到元素的最后位置<br>\n\t * 此方法对于某些无序集合的位置信息，以转换为数组后的位置为准。\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param matcher    匹配器，为空则全部匹配\n\t * @return 最后一个位置\n\t * @since 5.6.6\n\t */\n\tpublic static <T> int lastIndexOf(Collection<T> collection, Matcher<T> matcher) {\n\t\tif (collection instanceof List) {\n\t\t\t// List的查找最后一个有优化算法\n\t\t\treturn ListUtil.lastIndexOf((List<T>) collection, matcher);\n\t\t}\n\t\tint matchIndex = -1;\n\t\tif (isNotEmpty(collection)) {\n\t\t\tint index = 0;\n\t\t\tfor (T t : collection) {\n\t\t\t\tif (null == matcher || matcher.match(t)) {\n\t\t\t\t\tmatchIndex = index;\n\t\t\t\t}\n\t\t\t\tindex++;\n\t\t\t}\n\t\t}\n\t\treturn matchIndex;\n\t}\n\n\t/**\n\t * 获取匹配规则定义中匹配到元素的所有位置<br>\n\t * 此方法对于某些无序集合的位置信息，以转换为数组后的位置为准。\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param matcher    匹配器，为空则全部匹配\n\t * @return 位置数组\n\t * @since 5.2.5\n\t */\n\tpublic static <T> int[] indexOfAll(Collection<T> collection, Matcher<T> matcher) {\n\t\tfinal List<Integer> indexList = new ArrayList<>();\n\t\tif (null != collection) {\n\t\t\tint index = 0;\n\t\t\tfor (T t : collection) {\n\t\t\t\tif (null == matcher || matcher.match(t)) {\n\t\t\t\t\tindexList.add(index);\n\t\t\t\t}\n\t\t\t\tindex++;\n\t\t\t}\n\t\t}\n\t\treturn Convert.convert(int[].class, indexList);\n\t}\n\n\t// ---------------------------------------------------------------------- isEmpty\n\n\t/**\n\t * 集合是否为空\n\t *\n\t * @param collection 集合\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Collection<?> collection) {\n\t\treturn collection == null || collection.isEmpty();\n\t}\n\n\t/**\n\t * 如果给定集合为空，返回默认集合\n\t *\n\t * @param <T>               集合类型\n\t * @param <E>               集合元素类型\n\t * @param collection        集合\n\t * @param defaultCollection 默认数组\n\t * @return 非空（empty）的原集合或默认集合\n\t * @since 4.6.9\n\t */\n\tpublic static <T extends Collection<E>, E> T defaultIfEmpty(T collection, T defaultCollection) {\n\t\treturn isEmpty(collection) ? defaultCollection : collection;\n\t}\n\n\t/**\n\t * 如果给定集合为空，返回默认集合\n\t *\n\t * @param <T>        集合类型\n\t * @param <E>        集合元素类型\n\t * @param collection 集合\n\t * @param supplier   默认值懒加载函数\n\t * @return 非空（empty）的原集合或默认集合\n\t * @since 5.7.15\n\t */\n\tpublic static <T extends Collection<E>, E> T defaultIfEmpty(T collection, Supplier<? extends T> supplier) {\n\t\treturn isEmpty(collection) ? supplier.get() : collection;\n\t}\n\n\t/**\n\t * Iterable是否为空\n\t *\n\t * @param iterable Iterable对象\n\t * @return 是否为空\n\t * @see IterUtil#isEmpty(Iterable)\n\t */\n\tpublic static boolean isEmpty(Iterable<?> iterable) {\n\t\treturn IterUtil.isEmpty(iterable);\n\t}\n\n\t/**\n\t * Iterator是否为空\n\t *\n\t * @param Iterator Iterator对象\n\t * @return 是否为空\n\t * @see IterUtil#isEmpty(Iterator)\n\t */\n\tpublic static boolean isEmpty(Iterator<?> Iterator) {\n\t\treturn IterUtil.isEmpty(Iterator);\n\t}\n\n\t/**\n\t * Enumeration是否为空\n\t *\n\t * @param enumeration {@link Enumeration}\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Enumeration<?> enumeration) {\n\t\treturn null == enumeration || !enumeration.hasMoreElements();\n\t}\n\n\t/**\n\t * Map是否为空\n\t *\n\t * @param map 集合\n\t * @return 是否为空\n\t * @see MapUtil#isEmpty(Map)\n\t * @since 5.7.4\n\t */\n\tpublic static boolean isEmpty(Map<?, ?> map) {\n\t\treturn MapUtil.isEmpty(map);\n\t}\n\n\t// ---------------------------------------------------------------------- isNotEmpty\n\n\t/**\n\t * 集合是否为非空\n\t *\n\t * @param collection 集合\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(Collection<?> collection) {\n\t\treturn !isEmpty(collection);\n\t}\n\n\t/**\n\t * Iterable是否为空\n\t *\n\t * @param iterable Iterable对象\n\t * @return 是否为空\n\t * @see IterUtil#isNotEmpty(Iterable)\n\t */\n\tpublic static boolean isNotEmpty(Iterable<?> iterable) {\n\t\treturn IterUtil.isNotEmpty(iterable);\n\t}\n\n\t/**\n\t * Iterator是否为空\n\t *\n\t * @param Iterator Iterator对象\n\t * @return 是否为空\n\t * @see IterUtil#isNotEmpty(Iterator)\n\t */\n\tpublic static boolean isNotEmpty(Iterator<?> Iterator) {\n\t\treturn IterUtil.isNotEmpty(Iterator);\n\t}\n\n\t/**\n\t * Enumeration是否为空\n\t *\n\t * @param enumeration {@link Enumeration}\n\t * @return 是否为空\n\t */\n\tpublic static boolean isNotEmpty(Enumeration<?> enumeration) {\n\t\treturn null != enumeration && enumeration.hasMoreElements();\n\t}\n\n\t/**\n\t * 是否包含{@code null}元素\n\t *\n\t * @param iterable 被检查的Iterable对象，如果为{@code null} 返回true\n\t * @return 是否包含{@code null}元素\n\t * @see IterUtil#hasNull(Iterable)\n\t * @since 3.0.7\n\t */\n\tpublic static boolean hasNull(Iterable<?> iterable) {\n\t\treturn IterUtil.hasNull(iterable);\n\t}\n\n\t/**\n\t * Map是否为非空\n\t *\n\t * @param map 集合\n\t * @return 是否为非空\n\t * @see MapUtil#isNotEmpty(Map)\n\t * @since 5.7.4\n\t */\n\tpublic static boolean isNotEmpty(Map<?, ?> map) {\n\t\treturn MapUtil.isNotEmpty(map);\n\t}\n\n\t// ---------------------------------------------------------------------- zip\n\n\t/**\n\t * 映射键值（参考Python的zip()函数）<br>\n\t * 例如：<br>\n\t * keys = a,b,c,d<br>\n\t * values = 1,2,3,4<br>\n\t * delimiter = , 则得到的Map是 {a=1, b=2, c=3, d=4}<br>\n\t * 如果两个数组长度不同，则只对应最短部分\n\t *\n\t * @param keys      键列表\n\t * @param values    值列表\n\t * @param delimiter 分隔符\n\t * @param isOrder   是否有序\n\t * @return Map\n\t * @since 3.0.4\n\t */\n\tpublic static Map<String, String> zip(String keys, String values, String delimiter, boolean isOrder) {\n\t\treturn ArrayUtil.zip(StrUtil.splitToArray(keys, delimiter), StrUtil.splitToArray(values, delimiter), isOrder);\n\t}\n\n\t/**\n\t * 映射键值（参考Python的zip()函数），返回Map无序<br>\n\t * 例如：<br>\n\t * keys = a,b,c,d<br>\n\t * values = 1,2,3,4<br>\n\t * delimiter = , 则得到的Map是 {a=1, b=2, c=3, d=4}<br>\n\t * 如果两个数组长度不同，则只对应最短部分\n\t *\n\t * @param keys      键列表\n\t * @param values    值列表\n\t * @param delimiter 分隔符\n\t * @return Map\n\t */\n\tpublic static Map<String, String> zip(String keys, String values, String delimiter) {\n\t\treturn zip(keys, values, delimiter, false);\n\t}\n\n\t/**\n\t * 映射键值（参考Python的zip()函数）<br>\n\t * 例如：<br>\n\t * keys = [a,b,c,d]<br>\n\t * values = [1,2,3,4]<br>\n\t * 则得到的Map是 {a=1, b=2, c=3, d=4}<br>\n\t * 如果两个数组长度不同，则只对应最短部分\n\t *\n\t * @param <K>    键类型\n\t * @param <V>    值类型\n\t * @param keys   键列表\n\t * @param values 值列表\n\t * @return Map\n\t */\n\tpublic static <K, V> Map<K, V> zip(Collection<K> keys, Collection<V> values) {\n\t\tif (isEmpty(keys) || isEmpty(values)) {\n\t\t\treturn MapUtil.empty();\n\t\t}\n\n\t\tint entryCount = Math.min(keys.size(), values.size());\n\t\tfinal Map<K, V> map = MapUtil.newHashMap(entryCount);\n\n\t\tfinal Iterator<K> keyIterator = keys.iterator();\n\t\tfinal Iterator<V> valueIterator = values.iterator();\n\t\twhile (entryCount > 0) {\n\t\t\tmap.put(keyIterator.next(), valueIterator.next());\n\t\t\tentryCount--;\n\t\t}\n\n\t\treturn map;\n\t}\n\n\t/**\n\t * 将Entry集合转换为HashMap\n\t *\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @param entryIter entry集合\n\t * @return Map\n\t * @see IterUtil#toMap(Iterable)\n\t */\n\tpublic static <K, V> HashMap<K, V> toMap(Iterable<Entry<K, V>> entryIter) {\n\t\treturn IterUtil.toMap(entryIter);\n\t}\n\n\t/**\n\t * 将数组转换为Map（HashMap），支持数组元素类型为：\n\t *\n\t * <pre>\n\t * Map.Entry\n\t * 长度大于1的数组（取前两个值），如果不满足跳过此元素\n\t * Iterable 长度也必须大于1（取前两个值），如果不满足跳过此元素\n\t * Iterator 长度也必须大于1（取前两个值），如果不满足跳过此元素\n\t * </pre>\n\t *\n\t * <pre>\n\t * Map&lt;Object, Object&gt; colorMap = CollectionUtil.toMap(new String[][] {{\n\t *     {\"RED\", \"#FF0000\"},\n\t *     {\"GREEN\", \"#00FF00\"},\n\t *     {\"BLUE\", \"#0000FF\"}});\n\t * </pre>\n\t * <p>\n\t * 参考：commons-lang\n\t *\n\t * @param array 数组。元素类型为Map.Entry、数组、Iterable、Iterator\n\t * @return {@link HashMap}\n\t * @see MapUtil#of(Object[])\n\t * @since 3.0.8\n\t */\n\tpublic static HashMap<Object, Object> toMap(Object[] array) {\n\t\treturn MapUtil.of(array);\n\t}\n\n\t/**\n\t * 将集合转换为排序后的TreeSet\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param comparator 比较器\n\t * @return treeSet\n\t */\n\tpublic static <T> TreeSet<T> toTreeSet(Collection<T> collection, Comparator<T> comparator) {\n\t\tfinal TreeSet<T> treeSet = new TreeSet<>(comparator);\n\t\ttreeSet.addAll(collection);\n\t\treturn treeSet;\n\t}\n\n\t/**\n\t * Iterator转换为Enumeration\n\t * <p>\n\t * Adapt the specified {@link Iterator} to the {@link Enumeration} interface.\n\t *\n\t * @param <E>  集合元素类型\n\t * @param iter {@link Iterator}\n\t * @return {@link Enumeration}\n\t */\n\tpublic static <E> Enumeration<E> asEnumeration(Iterator<E> iter) {\n\t\treturn new IteratorEnumeration<>(iter);\n\t}\n\n\t/**\n\t * Enumeration转换为Iterator\n\t * <p>\n\t * Adapt the specified {@code Enumeration} to the {@code Iterator} interface\n\t *\n\t * @param <E> 集合元素类型\n\t * @param e   {@link Enumeration}\n\t * @return {@link Iterator}\n\t * @see IterUtil#asIterator(Enumeration)\n\t */\n\tpublic static <E> Iterator<E> asIterator(Enumeration<E> e) {\n\t\treturn IterUtil.asIterator(e);\n\t}\n\n\t/**\n\t * {@link Iterator} 转为 {@link Iterable}\n\t *\n\t * @param <E>  元素类型\n\t * @param iter {@link Iterator}\n\t * @return {@link Iterable}\n\t * @see IterUtil#asIterable(Iterator)\n\t */\n\tpublic static <E> Iterable<E> asIterable(final Iterator<E> iter) {\n\t\treturn IterUtil.asIterable(iter);\n\t}\n\n\t/**\n\t * {@link Iterable}转为{@link Collection}<br>\n\t * 首先尝试强转，强转失败则构建一个新的{@link ArrayList}\n\t *\n\t * @param <E>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return {@link Collection} 或者 {@link ArrayList}\n\t * @since 3.0.9\n\t */\n\tpublic static <E> Collection<E> toCollection(Iterable<E> iterable) {\n\t\treturn (iterable instanceof Collection) ? (Collection<E>) iterable : newArrayList(iterable.iterator());\n\t}\n\n\t/**\n\t * 行转列，合并相同的键，值合并为列表<br>\n\t * 将Map列表中相同key的值组成列表做为Map的value<br>\n\t * 是{@link #toMapList(Map)}的逆方法<br>\n\t * 比如传入数据：\n\t *\n\t * <pre>\n\t * [\n\t *  {a: 1, b: 1, c: 1}\n\t *  {a: 2, b: 2}\n\t *  {a: 3, b: 3}\n\t *  {a: 4}\n\t * ]\n\t * </pre>\n\t * <p>\n\t * 结果是：\n\t *\n\t * <pre>\n\t * {\n\t *   a: [1,2,3,4]\n\t *   b: [1,2,3,]\n\t *   c: [1]\n\t * }\n\t * </pre>\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param mapList Map列表\n\t * @return Map\n\t * @see MapUtil#toListMap(Iterable)\n\t */\n\tpublic static <K, V> Map<K, List<V>> toListMap(Iterable<? extends Map<K, V>> mapList) {\n\t\treturn MapUtil.toListMap(mapList);\n\t}\n\n\t/**\n\t * 列转行。将Map中值列表分别按照其位置与key组成新的map。<br>\n\t * 是{@link #toListMap(Iterable)}的逆方法<br>\n\t * 比如传入数据：\n\t *\n\t * <pre>\n\t * {\n\t *   a: [1,2,3,4]\n\t *   b: [1,2,3,]\n\t *   c: [1]\n\t * }\n\t * </pre>\n\t * <p>\n\t * 结果是：\n\t *\n\t * <pre>\n\t * [\n\t *  {a: 1, b: 1, c: 1}\n\t *  {a: 2, b: 2}\n\t *  {a: 3, b: 3}\n\t *  {a: 4}\n\t * ]\n\t * </pre>\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param listMap 列表Map\n\t * @return Map列表\n\t * @see MapUtil#toMapList(Map)\n\t */\n\tpublic static <K, V> List<Map<K, V>> toMapList(Map<K, ? extends Iterable<V>> listMap) {\n\t\treturn MapUtil.toMapList(listMap);\n\t}\n\n\t/**\n\t * 集合转换为Map，转换规则为：<br>\n\t * 按照keyFunc函数规则根据元素对象生成Key，元素作为值\n\t *\n\t * @param <K>     Map键类型\n\t * @param <V>     Map值类型\n\t * @param values  数据列表\n\t * @param map     Map对象，转换后的键值对加入此Map，通过传入此对象自定义Map类型\n\t * @param keyFunc 生成key的函数\n\t * @return 生成的map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterable<V> values, Map<K, V> map, Func1<V, K> keyFunc) {\n\t\treturn IterUtil.toMap(null == values ? null : values.iterator(), map, keyFunc);\n\t}\n\n\t/**\n\t * 集合转换为Map，转换规则为：<br>\n\t * 按照keyFunc函数规则根据元素对象生成Key，按照valueFunc函数规则根据元素对象生成value组成新的Map\n\t *\n\t * @param <K>       Map键类型\n\t * @param <V>       Map值类型\n\t * @param <E>       元素类型\n\t * @param values    数据列表\n\t * @param map       Map对象，转换后的键值对加入此Map，通过传入此对象自定义Map类型\n\t * @param keyFunc   生成key的函数\n\t * @param valueFunc 生成值的策略函数\n\t * @return 生成的map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V, E> Map<K, V> toMap(Iterable<E> values, Map<K, V> map, Func1<E, K> keyFunc, Func1<E, V> valueFunc) {\n\t\treturn IterUtil.toMap(null == values ? null : values.iterator(), map, keyFunc, valueFunc);\n\t}\n\n\t/**\n\t * 一个对象不为空且不存在于该集合中时，加入到该集合中<br>\n\t * <pre>\n\t *     null, null -&gt; false\n\t *     [], null -&gt; false\n\t *     null, \"123\" -&gt; false\n\t *     [\"123\"], \"123\" -&gt; false\n\t *     [], \"123\" -&gt; true\n\t *     [\"456\"], \"123\" -&gt; true\n\t *     [Animal{\"name\": \"jack\"}], Dog{\"name\": \"jack\"} -&gt; true\n\t * </pre>\n\t *\n\t * @param collection 被加入的集合\n\t * @param object     要添加到集合的对象\n\t * @param <T>        集合元素类型\n\t * @param <S>        要添加的元素类型【为集合元素类型的类型或子类型】\n\t * @return 是否添加成功\n\t * @author Cloud-Style\n\t */\n\tpublic static <T, S extends T> boolean addIfAbsent(Collection<T> collection, S object) {\n\t\tif (object == null || collection == null || collection.contains(object)) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn collection.add(object);\n\t}\n\n\t/**\n\t * 将指定对象全部加入到集合中<br>\n\t * 提供的对象如果为集合类型，会自动转换为目标元素类型<br>\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 被加入的集合\n\t * @param value      对象，可能为Iterator、Iterable、Enumeration、Array\n\t * @return 被加入集合\n\t */\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, Object value) {\n\t\treturn addAll(collection, value, TypeUtil.getTypeArgument(collection.getClass()));\n\t}\n\n\t/**\n\t * 将指定对象全部加入到集合中<br>\n\t * 提供的对象如果为集合类型，会自动转换为目标元素类型<br>\n\t * 如果为String，支持类似于[1,2,3,4] 或者 1,2,3,4 这种格式\n\t *\n\t * @param <T>         元素类型\n\t * @param collection  被加入的集合\n\t * @param value       对象，可能为Iterator、Iterable、Enumeration、Array，或者与集合元素类型一致\n\t * @param elementType 元素类型，为空时，使用Object类型来接纳所有类型\n\t * @return 被加入集合\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, Object value, Type elementType) {\n\t\tif (null == collection || null == value) {\n\t\t\treturn collection;\n\t\t}\n\t\tif (TypeUtil.isUnknown(elementType)) {\n\t\t\t// 元素类型为空时，使用Object类型来接纳所有类型\n\t\t\telementType = Object.class;\n\t\t}\n\n\t\tIterator iter;\n\t\tif (value instanceof Iterator) {\n\t\t\titer = (Iterator) value;\n\t\t} else if (value instanceof Iterable) {\n\t\t\tif(value instanceof Map && BeanUtil.isBean(TypeUtil.getClass(elementType))){\n\t\t\t\t//https://github.com/chinabugotech/hutool/issues/3139\n\t\t\t\t// 如果值为Map，而目标为一个Bean，则Map应整体转换为Bean，而非拆分成Entry转换\n\t\t\t\titer = new ArrayIter<>(new Object[]{value});\n\t\t\t}else{\n\t\t\t\titer = ((Iterable) value).iterator();\n\t\t\t}\n\t\t} else if (value instanceof Enumeration) {\n\t\t\titer = new EnumerationIter<>((Enumeration) value);\n\t\t} else if (ArrayUtil.isArray(value)) {\n\t\t\titer = new ArrayIter<>(value);\n\t\t} else if (value instanceof CharSequence) {\n\t\t\t// String按照逗号分隔的列表对待\n\t\t\tfinal String ArrayStr = StrUtil.unWrap((CharSequence) value, '[', ']');\n\t\t\titer = StrUtil.splitTrim(ArrayStr, CharUtil.COMMA).iterator();\n\t\t} else {\n\t\t\t// 其它类型按照单一元素处理\n\t\t\titer = CollUtil.newArrayList(value).iterator();\n\t\t}\n\n\t\tfinal ConverterRegistry convert = ConverterRegistry.getInstance();\n\t\twhile (iter.hasNext()) {\n\t\t\tcollection.add(convert.convert(elementType, iter.next()));\n\t\t}\n\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 加入全部\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 被加入的集合 {@link Collection}\n\t * @param iterator   要加入的{@link Iterator}\n\t * @return 原集合\n\t */\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, Iterator<T> iterator) {\n\t\tif (null != collection && null != iterator) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\tcollection.add(iterator.next());\n\t\t\t}\n\t\t}\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 加入全部\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 被加入的集合 {@link Collection}\n\t * @param iterable   要加入的内容{@link Iterable}\n\t * @return 原集合\n\t */\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, Iterable<T> iterable) {\n\t\tif (iterable == null) {\n\t\t\treturn collection;\n\t\t}\n\t\treturn addAll(collection, iterable.iterator());\n\t}\n\n\t/**\n\t * 加入全部\n\t *\n\t * @param <T>         集合元素类型\n\t * @param collection  被加入的集合 {@link Collection}\n\t * @param enumeration 要加入的内容{@link Enumeration}\n\t * @return 原集合\n\t */\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, Enumeration<T> enumeration) {\n\t\tif (null != collection && null != enumeration) {\n\t\t\twhile (enumeration.hasMoreElements()) {\n\t\t\t\tcollection.add(enumeration.nextElement());\n\t\t\t}\n\t\t}\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 加入全部\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 被加入的集合 {@link Collection}\n\t * @param values     要加入的内容数组\n\t * @return 原集合\n\t * @since 3.0.8\n\t */\n\tpublic static <T> Collection<T> addAll(Collection<T> collection, T[] values) {\n\t\tif (null != collection && null != values) {\n\t\t\tCollections.addAll(collection, values);\n\t\t}\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 将另一个列表中的元素加入到列表中，如果列表中已经存在此元素则忽略之\n\t *\n\t * @param <T>       集合元素类型\n\t * @param list      列表\n\t * @param otherList 其它列表\n\t * @return 此列表\n\t */\n\tpublic static <T> List<T> addAllIfNotContains(List<T> list, List<T> otherList) {\n\t\tfor (T t : otherList) {\n\t\t\tif (!list.contains(t)) {\n\t\t\t\tlist.add(t);\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 获取集合中指定下标的元素值，下标可以为负数，例如-1表示最后一个元素<br>\n\t * 如果元素越界，返回null\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param index      下标，支持负数\n\t * @return 元素值\n\t * @since 4.0.6\n\t */\n\tpublic static <T> T get(Collection<T> collection, int index) {\n\t\tif (null == collection) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal int size = collection.size();\n\t\tif (0 == size) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (index < 0) {\n\t\t\tindex += size;\n\t\t}\n\n\t\t// 检查越界\n\t\tif (index >= size || index < 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (collection instanceof List) {\n\t\t\tfinal List<T> list = ((List<T>) collection);\n\t\t\treturn list.get(index);\n\t\t} else {\n\t\t\treturn IterUtil.get(collection.iterator(), index);\n\t\t}\n\t}\n\n\t/**\n\t * 获取集合中指定多个下标的元素值，下标可以为负数，例如-1表示最后一个元素\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param indexes    下标，支持负数\n\t * @return 元素值列表\n\t * @since 4.0.6\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> List<T> getAny(Collection<T> collection, int... indexes) {\n\t\tfinal int size = collection.size();\n\t\tfinal ArrayList<T> result = new ArrayList<>();\n\t\tif (collection instanceof List) {\n\t\t\tfinal List<T> list = ((List<T>) collection);\n\t\t\tfor (int index : indexes) {\n\t\t\t\tif (index < 0) {\n\t\t\t\t\tindex += size;\n\t\t\t\t}\n\t\t\t\tresult.add(list.get(index));\n\t\t\t}\n\t\t} else {\n\t\t\tfinal Object[] array = collection.toArray();\n\t\t\tfor (int index : indexes) {\n\t\t\t\tif (index < 0) {\n\t\t\t\t\tindex += size;\n\t\t\t\t}\n\t\t\t\tresult.add((T) array[index]);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取集合的第一个元素\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return 第一个元素\n\t * @see IterUtil#getFirst(Iterable)\n\t * @since 3.0.1\n\t */\n\tpublic static <T> T getFirst(Iterable<T> iterable) {\n\t\treturn IterUtil.getFirst(iterable);\n\t}\n\n\t/**\n\t * 获取集合的第一个元素\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @return 第一个元素\n\t * @see IterUtil#getFirst(Iterator)\n\t * @since 3.0.1\n\t */\n\tpublic static <T> T getFirst(Iterator<T> iterator) {\n\t\treturn IterUtil.getFirst(iterator);\n\t}\n\n\t/**\n\t * 获取集合的最后一个元素\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection {@link Collection}\n\t * @return 最后一个元素\n\t * @since 4.1.10\n\t */\n\tpublic static <T> T getLast(Collection<T> collection) {\n\t\treturn get(collection, -1);\n\t}\n\n\t/**\n\t * 获得{@link Iterable}对象的元素类型（通过第一个非空元素判断）\n\t *\n\t * @param iterable {@link Iterable}\n\t * @return 元素类型，当列表为空或元素全部为null时，返回null\n\t * @see IterUtil#getElementType(Iterable)\n\t * @since 3.0.8\n\t * @deprecated 请使用 {@link IterUtil#getElementType(Iterable)}\n\t */\n\t@Deprecated\n\tpublic static Class<?> getElementType(Iterable<?> iterable) {\n\t\treturn IterUtil.getElementType(iterable);\n\t}\n\n\t/**\n\t * 获得{@link Iterator}对象的元素类型（通过第一个非空元素判断）\n\t *\n\t * @param iterator {@link Iterator}\n\t * @return 元素类型，当列表为空或元素全部为null时，返回null\n\t * @see IterUtil#getElementType(Iterator)\n\t * @since 3.0.8\n\t * @deprecated 请使用 {@link IterUtil#getElementType(Iterator)}\n\t */\n\t@Deprecated\n\tpublic static Class<?> getElementType(Iterator<?> iterator) {\n\t\treturn IterUtil.getElementType(iterator);\n\t}\n\n\t/**\n\t * 从Map中获取指定键列表对应的值列表<br>\n\t * 如果key在map中不存在或key对应值为null，则返回值列表对应位置的值也为null\n\t *\n\t * @param <K>  键类型\n\t * @param <V>  值类型\n\t * @param map  {@link Map}\n\t * @param keys 键列表\n\t * @return 值列表\n\t * @since 3.0.8\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, K... keys) {\n\t\treturn MapUtil.valuesOfKeys(map, new ArrayIter<>(keys));\n\t}\n\n\t/**\n\t * 从Map中获取指定键列表对应的值列表<br>\n\t * 如果key在map中不存在或key对应值为null，则返回值列表对应位置的值也为null\n\t *\n\t * @param <K>  键类型\n\t * @param <V>  值类型\n\t * @param map  {@link Map}\n\t * @param keys 键列表\n\t * @return 值列表\n\t * @since 3.0.9\n\t */\n\tpublic static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, Iterable<K> keys) {\n\t\treturn valuesOfKeys(map, keys.iterator());\n\t}\n\n\t/**\n\t * 从Map中获取指定键列表对应的值列表<br>\n\t * 如果key在map中不存在或key对应值为null，则返回值列表对应位置的值也为null\n\t *\n\t * @param <K>  键类型\n\t * @param <V>  值类型\n\t * @param map  {@link Map}\n\t * @param keys 键列表\n\t * @return 值列表\n\t * @since 3.0.9\n\t */\n\tpublic static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, Iterator<K> keys) {\n\t\treturn MapUtil.valuesOfKeys(map, keys);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------- sort\n\n\t/**\n\t * 将多个集合排序并显示不同的段落（分页）<br>\n\t * 采用{@link BoundedPriorityQueue}实现分页取局部\n\t *\n\t * @param <T>        集合元素类型\n\t * @param pageNo     页码，从0开始计数，0表示第一页\n\t * @param pageSize   每页的条目数\n\t * @param comparator 比较器\n\t * @param colls      集合数组\n\t * @return 分页后的段落内容\n\t */\n\t@SafeVarargs\n\tpublic static <T> List<T> sortPageAll(int pageNo, int pageSize, Comparator<T> comparator, Collection<T>... colls) {\n\t\tfinal List<T> list = new ArrayList<>(pageNo * pageSize);\n\t\tfor (Collection<T> coll : colls) {\n\t\t\tlist.addAll(coll);\n\t\t}\n\t\tif (null != comparator) {\n\t\t\tlist.sort(comparator);\n\t\t}\n\n\t\treturn page(pageNo, pageSize, list);\n\t}\n\n\t/**\n\t * 对指定List分页取值\n\t *\n\t * @param <T>      集合元素类型\n\t * @param pageNo   页码，从0开始计数，0表示第一页\n\t * @param pageSize 每页的条目数\n\t * @param list     列表\n\t * @return 分页后的段落内容\n\t * @since 4.1.20\n\t */\n\tpublic static <T> List<T> page(int pageNo, int pageSize, List<T> list) {\n\t\treturn ListUtil.page(pageNo, pageSize, list);\n\t}\n\n\t/**\n\t * 排序集合，排序不会修改原集合\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @param comparator 比较器\n\t * @return treeSet\n\t */\n\tpublic static <T> List<T> sort(Collection<T> collection, Comparator<? super T> comparator) {\n\t\tList<T> list = new ArrayList<>(collection);\n\t\tlist.sort(comparator);\n\t\treturn list;\n\t}\n\n\t/**\n\t * 针对List排序，排序会修改原List\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被排序的List\n\t * @param c    {@link Comparator}\n\t * @return 原list\n\t * @see Collections#sort(List, Comparator)\n\t */\n\tpublic static <T> List<T> sort(List<T> list, Comparator<? super T> c) {\n\t\treturn ListUtil.sort(list, c);\n\t}\n\n\t/**\n\t * 根据Bean的属性排序\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合，会被转换为List\n\t * @param property   属性名\n\t * @return 排序后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sortByProperty(Collection<T> collection, String property) {\n\t\treturn sort(collection, new PropertyComparator<>(property));\n\t}\n\n\t/**\n\t * 根据Bean的属性排序\n\t *\n\t * @param <T>      元素类型\n\t * @param list     List\n\t * @param property 属性名\n\t * @return 排序后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sortByProperty(List<T> list, String property) {\n\t\treturn ListUtil.sortByProperty(list, property);\n\t}\n\n\t/**\n\t * 根据汉字的拼音顺序排序\n\t *\n\t * @param collection 集合，会被转换为List\n\t * @return 排序后的List\n\t * @since 4.0.8\n\t */\n\tpublic static List<String> sortByPinyin(Collection<String> collection) {\n\t\treturn sort(collection, new PinyinComparator());\n\t}\n\n\t/**\n\t * 根据汉字的拼音顺序排序\n\t *\n\t * @param list List\n\t * @return 排序后的List\n\t * @since 4.0.8\n\t */\n\tpublic static List<String> sortByPinyin(List<String> list) {\n\t\treturn ListUtil.sortByPinyin(list);\n\t}\n\n\t/**\n\t * 排序Map\n\t *\n\t * @param <K>        键类型\n\t * @param <V>        值类型\n\t * @param map        Map\n\t * @param comparator Entry比较器\n\t * @return {@link TreeMap}\n\t * @since 3.0.9\n\t */\n\tpublic static <K, V> TreeMap<K, V> sort(Map<K, V> map, Comparator<? super K> comparator) {\n\t\tfinal TreeMap<K, V> result = new TreeMap<>(comparator);\n\t\tresult.putAll(map);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 通过Entry排序，可以按照键排序，也可以按照值排序，亦或者两者综合排序\n\t *\n\t * @param <K>             键类型\n\t * @param <V>             值类型\n\t * @param entryCollection Entry集合\n\t * @param comparator      {@link Comparator}\n\t * @return {@link LinkedList}\n\t * @since 3.0.9\n\t */\n\tpublic static <K, V> LinkedHashMap<K, V> sortToMap(Collection<Map.Entry<K, V>> entryCollection, Comparator<Map.Entry<K, V>> comparator) {\n\t\tList<Map.Entry<K, V>> list = new LinkedList<>(entryCollection);\n\t\tlist.sort(comparator);\n\n\t\tLinkedHashMap<K, V> result = new LinkedHashMap<>();\n\t\tfor (Map.Entry<K, V> entry : list) {\n\t\t\tresult.put(entry.getKey(), entry.getValue());\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 通过Entry排序，可以按照键排序，也可以按照值排序，亦或者两者综合排序\n\t *\n\t * @param <K>        键类型\n\t * @param <V>        值类型\n\t * @param map        被排序的Map\n\t * @param comparator {@link Comparator}\n\t * @return {@link LinkedList}\n\t * @since 3.0.9\n\t */\n\tpublic static <K, V> LinkedHashMap<K, V> sortByEntry(Map<K, V> map, Comparator<Map.Entry<K, V>> comparator) {\n\t\treturn sortToMap(map.entrySet(), comparator);\n\t}\n\n\t/**\n\t * 将Set排序（根据Entry的值）\n\t *\n\t * @param <K>        键类型\n\t * @param <V>        值类型\n\t * @param collection 被排序的{@link Collection}\n\t * @return 排序后的Set\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <K, V> List<Entry<K, V>> sortEntryToList(Collection<Entry<K, V>> collection) {\n\t\tList<Entry<K, V>> list = new LinkedList<>(collection);\n\t\tlist.sort((o1, o2) -> {\n\t\t\tV v1 = o1.getValue();\n\t\t\tV v2 = o2.getValue();\n\n\t\t\tif (v1 instanceof Comparable) {\n\t\t\t\treturn ((Comparable) v1).compareTo(v2);\n\t\t\t} else {\n\t\t\t\treturn v1.toString().compareTo(v2.toString());\n\t\t\t}\n\t\t});\n\t\treturn list;\n\t}\n\n\t// ------------------------------------------------------------------------------------------------- forEach\n\n\t/**\n\t * 循环遍历 {@link Iterable}，使用{@link Consumer} 接受遍历的每条数据，并针对每条数据做处理\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @param consumer {@link Consumer} 遍历的每条数据处理器\n\t * @since 5.4.7\n\t */\n\tpublic static <T> void forEach(Iterable<T> iterable, Consumer<T> consumer) {\n\t\tif (iterable == null) {\n\t\t\treturn;\n\t\t}\n\t\tforEach(iterable.iterator(), consumer);\n\t}\n\n\t/**\n\t * 循环遍历 {@link Iterator}，使用{@link Consumer} 接受遍历的每条数据，并针对每条数据做处理\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @param consumer {@link Consumer} 遍历的每条数据处理器\n\t */\n\tpublic static <T> void forEach(Iterator<T> iterator, Consumer<T> consumer) {\n\t\tif (iterator == null) {\n\t\t\treturn;\n\t\t}\n\t\tint index = 0;\n\t\twhile (iterator.hasNext()) {\n\t\t\tconsumer.accept(iterator.next(), index);\n\t\t\tindex++;\n\t\t}\n\t}\n\n\t/**\n\t * 循环遍历 {@link Enumeration}，使用{@link Consumer} 接受遍历的每条数据，并针对每条数据做处理\n\t *\n\t * @param <T>         集合元素类型\n\t * @param enumeration {@link Enumeration}\n\t * @param consumer    {@link Consumer} 遍历的每条数据处理器\n\t */\n\tpublic static <T> void forEach(Enumeration<T> enumeration, Consumer<T> consumer) {\n\t\tif (enumeration == null) {\n\t\t\treturn;\n\t\t}\n\t\tint index = 0;\n\t\twhile (enumeration.hasMoreElements()) {\n\t\t\tconsumer.accept(enumeration.nextElement(), index);\n\t\t\tindex++;\n\t\t}\n\t}\n\n\t/**\n\t * 循环遍历Map，使用{@link KVConsumer} 接受遍历的每条数据，并针对每条数据做处理<br>\n\t * 和JDK8中的map.forEach不同的是，此方法支持index\n\t *\n\t * @param <K>        Key类型\n\t * @param <V>        Value类型\n\t * @param map        {@link Map}\n\t * @param kvConsumer {@link KVConsumer} 遍历的每条数据处理器\n\t */\n\tpublic static <K, V> void forEach(Map<K, V> map, KVConsumer<K, V> kvConsumer) {\n\t\tif (map == null) {\n\t\t\treturn;\n\t\t}\n\t\tint index = 0;\n\t\tfor (Entry<K, V> entry : map.entrySet()) {\n\t\t\tkvConsumer.accept(entry.getKey(), entry.getValue(), index);\n\t\t\tindex++;\n\t\t}\n\t}\n\n\t/**\n\t * 分组，按照{@link Hash32}接口定义的hash算法，集合中的元素放入hash值对应的子列表中\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 被分组的集合\n\t * @param hash       Hash值算法，决定元素放在第几个分组的规则\n\t * @return 分组后的集合\n\t */\n\tpublic static <T> List<List<T>> group(Collection<T> collection, Hash32<T> hash) {\n\t\tfinal List<List<T>> result = new ArrayList<>();\n\t\tif (isEmpty(collection)) {\n\t\t\treturn result;\n\t\t}\n\t\tif (null == hash) {\n\t\t\t// 默认hash算法，按照元素的hashCode分组\n\t\t\thash = t -> (null == t) ? 0 : t.hashCode();\n\t\t}\n\n\t\tint index;\n\t\tList<T> subList;\n\t\tfor (T t : collection) {\n\t\t\tindex = hash.hash32(t);\n\t\t\tif (result.size() - 1 < index) {\n\t\t\t\twhile (result.size() - 1 < index) {\n\t\t\t\t\tresult.add(null);\n\t\t\t\t}\n\t\t\t\tresult.set(index, newArrayList(t));\n\t\t\t} else {\n\t\t\t\tsubList = result.get(index);\n\t\t\t\tif (null == subList) {\n\t\t\t\t\tresult.set(index, newArrayList(t));\n\t\t\t\t} else {\n\t\t\t\t\tsubList.add(t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 根据元素的指定字段名分组，非Bean都放在第一个分组中\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 集合\n\t * @param fieldName  元素Bean中的字段名，非Bean都放在第一个分组中\n\t * @return 分组列表\n\t */\n\tpublic static <T> List<List<T>> groupByField(Collection<T> collection, final String fieldName) {\n\t\treturn group(collection, new Hash32<T>() {\n\t\t\tprivate final List<Object> fieldNameList = new ArrayList<>();\n\n\t\t\t@Override\n\t\t\tpublic int hash32(T t) {\n\t\t\t\tif (null == t || !BeanUtil.isBean(t.getClass())) {\n\t\t\t\t\t// 非Bean放在同一子分组中\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tfinal Object value = ReflectUtil.getFieldValue(t, fieldName);\n\t\t\t\tint hash = fieldNameList.indexOf(value);\n\t\t\t\tif (hash < 0) {\n\t\t\t\t\tfieldNameList.add(value);\n\t\t\t\t\treturn fieldNameList.size() - 1;\n\t\t\t\t} else {\n\t\t\t\t\treturn hash;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 反序给定List，会在原List基础上直接修改\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被反转的List\n\t * @return 反转后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> reverse(List<T> list) {\n\t\treturn ListUtil.reverse(list);\n\t}\n\n\t/**\n\t * 反序给定List，会创建一个新的List，原List数据不变\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被反转的List\n\t * @return 反转后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> reverseNew(List<T> list) {\n\t\treturn ListUtil.reverseNew(list);\n\t}\n\n\t/**\n\t * 设置或增加元素。当index小于List的长度时，替换指定位置的值，否则在尾部追加\n\t *\n\t * @param <T>     元素类型\n\t * @param list    List列表\n\t * @param index   位置\n\t * @param element 新元素\n\t * @return 原List\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> setOrAppend(List<T> list, int index, T element) {\n\t\treturn ListUtil.setOrAppend(list, index, element);\n\t}\n\n\t/**\n\t * 获取指定Map列表中所有的Key\n\t *\n\t * @param <K>           键类型\n\t * @param mapCollection Map列表\n\t * @return key集合\n\t * @since 4.5.12\n\t */\n\tpublic static <K> Set<K> keySet(Collection<Map<K, ?>> mapCollection) {\n\t\tif (isEmpty(mapCollection)) {\n\t\t\treturn new HashSet<>();\n\t\t}\n\t\tfinal HashSet<K> set = new HashSet<>(mapCollection.size() * 16);\n\t\tfor (Map<K, ?> map : mapCollection) {\n\t\t\tset.addAll(map.keySet());\n\t\t}\n\n\t\treturn set;\n\t}\n\n\t/**\n\t * 获取指定Map列表中所有的Value\n\t *\n\t * @param <V>           值类型\n\t * @param mapCollection Map列表\n\t * @return Value集合\n\t * @since 4.5.12\n\t */\n\tpublic static <V> List<V> values(Collection<Map<?, V>> mapCollection) {\n\t\tfinal List<V> values = new ArrayList<>();\n\t\tfor (Map<?, V> map : mapCollection) {\n\t\t\tvalues.addAll(map.values());\n\t\t}\n\n\t\treturn values;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param <T>  元素类型\n\t * @param coll 集合\n\t * @return 最大值\n\t * @see Collections#max(Collection)\n\t * @since 4.6.5\n\t */\n\tpublic static <T extends Comparable<? super T>> T max(Collection<T> coll) {\n\t\treturn isEmpty(coll) ? null : Collections.max(coll);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param <T>  元素类型\n\t * @param coll 集合\n\t * @return 最小值\n\t * @see Collections#min(Collection)\n\t * @since 4.6.5\n\t */\n\tpublic static <T extends Comparable<? super T>> T min(Collection<T> coll) {\n\t\treturn isEmpty(coll) ? null : Collections.min(coll);\n\t}\n\n\t/**\n\t * 转为只读集合\n\t *\n\t * @param <T> 元素类型\n\t * @param c   集合\n\t * @return 只读集合\n\t * @since 5.2.6\n\t */\n\tpublic static <T> Collection<T> unmodifiable(Collection<? extends T> c) {\n\t\treturn Collections.unmodifiableCollection(c);\n\t}\n\n\t/**\n\t * 根据给定的集合类型，返回对应的空集合，支持类型包括：\n\t * *\n\t * <pre>\n\t *     1. NavigableSet\n\t *     2. SortedSet\n\t *     3. Set\n\t *     4. List\n\t * </pre>\n\t *\n\t * @param <E>             元素类型\n\t * @param <T>             集合类型\n\t * @param collectionClass 集合类型\n\t * @return 空集合\n\t * @since 5.3.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <E, T extends Collection<E>> T empty(Class<?> collectionClass) {\n\t\tif (null == collectionClass) {\n\t\t\treturn (T) Collections.emptyList();\n\t\t}\n\n\t\tif (Set.class.isAssignableFrom(collectionClass)) {\n\t\t\tif (NavigableSet.class == collectionClass) {\n\t\t\t\treturn (T) Collections.emptyNavigableSet();\n\t\t\t} else if (SortedSet.class == collectionClass) {\n\t\t\t\treturn (T) Collections.emptySortedSet();\n\t\t\t} else {\n\t\t\t\treturn (T) Collections.emptySet();\n\t\t\t}\n\t\t} else if (List.class.isAssignableFrom(collectionClass)) {\n\t\t\treturn (T) Collections.emptyList();\n\t\t}\n\n\t\t// 不支持空集合的集合类型\n\t\tthrow new IllegalArgumentException(StrUtil.format(\"[{}] is not support to get empty!\", collectionClass));\n\t}\n\n\t/**\n\t * 清除一个或多个集合内的元素，每个集合调用clear()方法\n\t *\n\t * @param collections 一个或多个集合\n\t * @since 5.3.6\n\t */\n\tpublic static void clear(Collection<?>... collections) {\n\t\tfor (Collection<?> collection : collections) {\n\t\t\tif (isNotEmpty(collection)) {\n\t\t\t\tcollection.clear();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 填充List，以达到最小长度\n\t *\n\t * @param <T>    集合元素类型\n\t * @param list   列表\n\t * @param minLen 最小长度\n\t * @param padObj 填充的对象\n\t * @since 5.3.10\n\t */\n\tpublic static <T> void padLeft(List<T> list, int minLen, T padObj) {\n\t\tObjects.requireNonNull(list);\n\t\tif (list.isEmpty()) {\n\t\t\tpadRight(list, minLen, padObj);\n\t\t\treturn;\n\t\t}\n\t\tfor (int i = list.size(); i < minLen; i++) {\n\t\t\tlist.add(0, padObj);\n\t\t}\n\t}\n\n\t/**\n\t * 填充List，以达到最小长度\n\t *\n\t * @param <T>    集合元素类型\n\t * @param list   列表\n\t * @param minLen 最小长度\n\t * @param padObj 填充的对象\n\t * @since 5.3.10\n\t */\n\tpublic static <T> void padRight(Collection<T> list, int minLen, T padObj) {\n\t\tObjects.requireNonNull(list);\n\t\tfor (int i = list.size(); i < minLen; i++) {\n\t\t\tlist.add(padObj);\n\t\t}\n\t}\n\n\t/**\n\t * 使用给定的转换函数，转换源集合为新类型的集合\n\t *\n\t * @param <F>        源元素类型\n\t * @param <T>        目标元素类型\n\t * @param collection 集合\n\t * @param function   转换函数\n\t * @return 新类型的集合\n\t * @since 5.4.3\n\t */\n\tpublic static <F, T> Collection<T> trans(Collection<F> collection, Function<? super F, ? extends T> function) {\n\t\treturn new TransCollection<>(collection, function);\n\t}\n\n\t/**\n\t * 使用给定的map将集合中的原素进行属性或者值的重新设定\n\t *\n\t * @param <E>         元素类型\n\t * @param <K>         替换的键\n\t * @param <V>         替换的值\n\t * @param iterable    集合\n\t * @param map         映射集\n\t * @param keyGenerate 映射键生成函数\n\t * @param biConsumer  封装映射到的值函数\n\t * @author nick_wys\n\t * @since 5.7.18\n\t */\n\tpublic static <E, K, V> void setValueByMap(Iterable<E> iterable, Map<K, V> map, Function<E, K> keyGenerate, BiConsumer<E, V> biConsumer) {\n\t\titerable.forEach(x -> Optional.ofNullable(map.get(keyGenerate.apply(x))).ifPresent(y -> biConsumer.accept(x, y)));\n\t}\n\n\t// ---------------------------------------------------------------------------------------------- Interface start\n\n\t/**\n\t * 针对一个参数做相应的操作<br>\n\t * 此函数接口与JDK8中Consumer不同是多提供了index参数，用于标记遍历对象是第几个。\n\t *\n\t * @param <T> 处理参数类型\n\t * @author Looly\n\t */\n\t@FunctionalInterface\n\tpublic interface Consumer<T> extends Serializable {\n\t\t/**\n\t\t * 接受并处理一个参数\n\t\t *\n\t\t * @param value 参数值\n\t\t * @param index 参数在集合中的索引\n\t\t */\n\t\tvoid accept(T value, int index);\n\t}\n\n\t/**\n\t * 针对两个参数做相应的操作，例如Map中的KEY和VALUE\n\t *\n\t * @param <K> KEY类型\n\t * @param <V> VALUE类型\n\t * @author Looly\n\t */\n\t@FunctionalInterface\n\tpublic interface KVConsumer<K, V> extends Serializable {\n\t\t/**\n\t\t * 接受并处理一对参数\n\t\t *\n\t\t * @param key   键\n\t\t * @param value 值\n\t\t * @param index 参数在集合中的索引\n\t\t */\n\t\tvoid accept(K key, V value, int index);\n\t}\n\t// ---------------------------------------------------------------------------------------------- Interface end\n\n\t/**\n\t * 获取Collection或者iterator的大小，此方法可以处理的对象类型如下：\n\t * <ul>\n\t * <li>Collection - the collection size\n\t * <li>Map - the map size\n\t * <li>Array - the array size\n\t * <li>Iterator - the number of elements remaining in the iterator\n\t * <li>Enumeration - the number of elements remaining in the enumeration\n\t * </ul>\n\t *\n\t * @param object 可以为空的对象\n\t * @return 如果object为空则返回0\n\t * @throws IllegalArgumentException 参数object不是Collection或者iterator\n\t * @since 5.5.0\n\t */\n\tpublic static int size(final Object object) {\n\t\tif (object == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tint total = 0;\n\t\tif (object instanceof Map<?, ?>) {\n\t\t\ttotal = ((Map<?, ?>) object).size();\n\t\t} else if (object instanceof Collection<?>) {\n\t\t\ttotal = ((Collection<?>) object).size();\n\t\t} else if (object instanceof Iterable<?>) {\n\t\t\ttotal = IterUtil.size((Iterable<?>) object);\n\t\t} else if (object instanceof Iterator<?>) {\n\t\t\ttotal = IterUtil.size((Iterator<?>) object);\n\t\t} else if (object instanceof Enumeration<?>) {\n\t\t\tfinal Enumeration<?> it = (Enumeration<?>) object;\n\t\t\twhile (it.hasMoreElements()) {\n\t\t\t\ttotal++;\n\t\t\t\tit.nextElement();\n\t\t\t}\n\t\t} else if (ArrayUtil.isArray(object)) {\n\t\t\ttotal = ArrayUtil.length(object);\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"Unsupported object type: \" + object.getClass().getName());\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * 判断两个{@link Collection} 是否元素和顺序相同，返回{@code true}的条件是：\n\t * <ul>\n\t *     <li>两个{@link Collection}必须长度相同</li>\n\t *     <li>两个{@link Collection}元素相同index的对象必须equals，满足{@link Objects#equals(Object, Object)}</li>\n\t * </ul>\n\t * 此方法来自Apache-Commons-Collections4。\n\t *\n\t * @param list1 列表1\n\t * @param list2 列表2\n\t * @return 是否相同\n\t * @since 5.6.0\n\t */\n\tpublic static boolean isEqualList(final Collection<?> list1, final Collection<?> list2) {\n\t\tif (list1 == list2) {\n\t\t\treturn true;\n\t\t}\n\t\tif (list1 == null || list2 == null || list1.size() != list2.size()) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn IterUtil.isEqualList(list1, list2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java",
    "content": "package cn.hutool.core.collection;\n\n/**\n * 集合相关工具类，包括数组，是 {@link CollUtil} 的别名工具类\n *\n * @author xiaoleilu\n * @see CollUtil\n */\npublic class CollectionUtil extends CollUtil{\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/ComputeIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/**\n * 带有计算属性的遍历器<br>\n * 通过继承此抽象遍历器，实现{@link #computeNext()}计算下一个节点，即可完成节点遍历<br>\n * 当调用{@link #hasNext()}时将此方法产生的节点缓存，直到调用{@link #next()}取出<br>\n * 当无下一个节点时，须返回{@code null}表示遍历结束\n *\n * @param <T> 节点类型\n * @author looly\n * @since 5.7.14\n */\npublic abstract class ComputeIter<T> implements Iterator<T> {\n\n\tprivate T next;\n\t/**\n\t * A flag indicating if the iterator has been fully read.\n\t */\n\tprivate boolean finished;\n\n\t/**\n\t * 计算新的节点，通过实现此方法，当调用{@link #hasNext()}时将此方法产生的节点缓存，直到调用{@link #next()}取出<br>\n\t * 当无下一个节点时，须返回{@code null}表示遍历结束\n\t *\n\t * @return 节点值\n\t */\n\tprotected abstract T computeNext();\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\tif (null != next) {\n\t\t\t// 用户读取了节点，但是没有使用\n\t\t\treturn true;\n\t\t} else if (finished) {\n\t\t\t// 读取结束\n\t\t\treturn false;\n\t\t}\n\n\t\tT result = computeNext();\n\t\tif (null == result) {\n\t\t\t// 不再有新的节点，结束\n\t\t\tthis.finished = true;\n\t\t\treturn false;\n\t\t} else {\n\t\t\tthis.next = result;\n\t\t\treturn true;\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic T next() {\n\t\tif (false == hasNext()) {\n\t\t\tthrow new NoSuchElementException(\"No more lines\");\n\t\t}\n\n\t\tT result = this.next;\n\t\t// 清空cache，表示此节点读取完毕，下次计算新节点\n\t\tthis.next = null;\n\t\treturn result;\n\t}\n\n\t/**\n\t * 手动结束遍历器，用于关闭操作等\n\t */\n\tpublic void finish(){\n\t\tthis.finished = true;\n\t\tthis.next = null;\n\t}\n\n\t/**\n\t * 重置状态，用于再次遍历\n\t */\n\tpublic void resetState() {\n\t\tthis.finished = false;\n\t\tthis.next = null;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.util.AbstractSet;\nimport java.util.Collection;\nimport java.util.Iterator;\n\n/**\n * 通过{@link SafeConcurrentHashMap}实现的线程安全HashSet\n *\n * @author Looly\n *\n * @param <E> 元素类型\n * @since 3.1.0\n */\npublic class ConcurrentHashSet<E> extends AbstractSet<E> implements java.io.Serializable {\n\tprivate static final long serialVersionUID = 7997886765361607470L;\n\n\t/** 持有对象。如果值为此对象表示有数据，否则无数据 */\n\tprivate static final Boolean PRESENT = true;\n\tprivate final SafeConcurrentHashMap<E, Boolean> map;\n\n\t// ----------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造<br>\n\t * 触发因子为默认的0.75\n\t */\n\tpublic ConcurrentHashSet() {\n\t\tmap = new SafeConcurrentHashMap<>();\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 触发因子为默认的0.75\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic ConcurrentHashSet(int initialCapacity) {\n\t\tmap = new SafeConcurrentHashMap<>(initialCapacity);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子。此参数决定数据增长时触发的百分比\n\t */\n\tpublic ConcurrentHashSet(int initialCapacity, float loadFactor) {\n\t\tmap = new SafeConcurrentHashMap<>(initialCapacity, loadFactor);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 触发因子。此参数决定数据增长时触发的百分比\n\t * @param concurrencyLevel 线程并发度\n\t */\n\tpublic ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) {\n\t\tmap = new SafeConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel);\n\t}\n\n\t/**\n\t * 从已有集合中构造\n\t * @param iter {@link Iterable}\n\t */\n\tpublic ConcurrentHashSet(Iterable<E> iter) {\n\t\tif(iter instanceof Collection) {\n\t\t\tfinal Collection<E> collection = (Collection<E>)iter;\n\t\t\tmap = new SafeConcurrentHashMap<>((int)(collection.size() / 0.75f));\n\t\t\tthis.addAll(collection);\n\t\t}else {\n\t\t\tmap = new SafeConcurrentHashMap<>();\n\t\t\tfor (E e : iter) {\n\t\t\t\tthis.add(e);\n\t\t\t}\n\t\t}\n\t}\n\t// ----------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic Iterator<E> iterator() {\n\t\treturn map.keySet().iterator();\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn map.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn map.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean contains(Object o) {\n\t\t//noinspection SuspiciousMethodCalls\n\t\treturn map.containsKey(o);\n\t}\n\n\t@Override\n\tpublic boolean add(E e) {\n\t\treturn map.put(e, PRESENT) == null;\n\t}\n\n\t@Override\n\tpublic boolean remove(Object o) {\n\t\treturn PRESENT.equals(map.remove(o));\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tmap.clear();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/CopiedIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * 复制 {@link Iterator}<br>\n * 为了解决并发情况下{@link Iterator}遍历导致的问题（当Iterator被修改会抛出ConcurrentModificationException）\n * ，故使用复制原Iterator的方式解决此问题。\n *\n * <p>\n * 解决方法为：在构造方法中遍历Iterator中的元素，装入新的List中然后遍历之。\n * 当然，修改这个复制后的Iterator是没有意义的，因此remove方法将会抛出异常。\n *\n * <p>\n * 需要注意的是，在构造此对象时需要保证原子性（原对象不被修改），最好加锁构造此对象，构造完毕后解锁。\n *\n * @param <E> 元素类型\n * @author Looly\n * @since 3.0.7\n */\npublic class CopiedIter<E> implements IterableIter<E>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Iterator<E> listIterator;\n\n\t/**\n\t * 根据已有{@link Iterator}，返回新的{@code CopiedIter}\n\t *\n\t * @param iterator {@link Iterator}\n\t * @param <E>      元素类型\n\t * @return {@code CopiedIter}\n\t */\n\tpublic static <E> CopiedIter<E> copyOf(Iterator<E> iterator) {\n\t\treturn new CopiedIter<>(iterator);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param iterator 被复制的Iterator\n\t */\n\tpublic CopiedIter(Iterator<E> iterator) {\n\t\tfinal List<E> eleList = ListUtil.toList(iterator);\n\t\tthis.listIterator = eleList.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn this.listIterator.hasNext();\n\t}\n\n\t@Override\n\tpublic E next() {\n\t\treturn this.listIterator.next();\n\t}\n\n\t/**\n\t * 此对象不支持移除元素\n\t *\n\t * @throws UnsupportedOperationException 当调用此方法时始终抛出此异常\n\t */\n\t@Override\n\tpublic void remove() throws UnsupportedOperationException {\n\t\tthrow new UnsupportedOperationException(\"This is a read-only iterator.\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/EnumerationIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.io.Serializable;\nimport java.util.Enumeration;\nimport java.util.Iterator;\n\n/**\n * {@link Enumeration}对象转{@link Iterator}对象\n * @author Looly\n *\n * @param <E> 元素类型\n * @since 4.1.1\n */\npublic class EnumerationIter<E> implements IterableIter<E>, Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Enumeration<E> e;\n\n\t/**\n\t * 构造\n\t * @param enumeration {@link Enumeration}对象\n\t */\n\tpublic EnumerationIter(Enumeration<E> enumeration) {\n\t\tthis.e = enumeration;\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn e.hasMoreElements();\n\t}\n\n\t@Override\n\tpublic E next() {\n\t\treturn e.nextElement();\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tthrow new UnsupportedOperationException();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/FilterIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\n\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/**\n * 包装 {@link Iterator}并根据{@link Filter}定义，过滤元素输出<br>\n * 类实现来自Apache Commons Collection\n *\n * @author apache commons, looly\n * @since 5.8.0\n */\npublic class FilterIter<E> implements Iterator<E> {\n\n\tprivate final Iterator<? extends E> iterator;\n\tprivate final Filter<? super E> filter;\n\n\t/**\n\t * 下一个元素\n\t */\n\tprivate E nextObject;\n\t/**\n\t * 标记下一个元素是否被计算\n\t */\n\tprivate boolean nextObjectSet = false;\n\n\t/**\n\t * 构造\n\t *\n\t * @param iterator 被包装的{@link Iterator}\n\t * @param filter   过滤函数，{@code null}表示不过滤\n\t */\n\tpublic FilterIter(final Iterator<? extends E> iterator, final Filter<? super E> filter) {\n\t\tthis.iterator = Assert.notNull(iterator);\n\t\tthis.filter = filter;\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn nextObjectSet || setNextObject();\n\t}\n\n\t@Override\n\tpublic E next() {\n\t\tif (false == nextObjectSet && false == setNextObject()) {\n\t\t\tthrow new NoSuchElementException();\n\t\t}\n\t\tnextObjectSet = false;\n\t\treturn nextObject;\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tif (nextObjectSet) {\n\t\t\tthrow new IllegalStateException(\"remove() cannot be called\");\n\t\t}\n\t\titerator.remove();\n\t}\n\n\t/**\n\t * 获取被包装的{@link Iterator}\n\t *\n\t * @return {@link Iterator}\n\t */\n\tpublic Iterator<? extends E> getIterator() {\n\t\treturn iterator;\n\t}\n\n\t/**\n\t * 获取过滤函数\n\t *\n\t * @return 过滤函数，可能为{@code null}\n\t */\n\tpublic Filter<? super E> getFilter() {\n\t\treturn filter;\n\t}\n\n\t/**\n\t * 设置下一个元素，如果存在返回{@code true}，否则{@code false}\n\t */\n\tprivate boolean setNextObject() {\n\t\twhile (iterator.hasNext()) {\n\t\t\tfinal E object = iterator.next();\n\t\t\tif (null == filter || filter.accept(object)) {\n\t\t\t\tnextObject = object;\n\t\t\t\tnextObjectSet = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/IterChain.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Chain;\n\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\n/**\n * 组合{@link Iterator}，将多个{@link Iterator}组合在一起，便于集中遍历。<br>\n * 来自Jodd\n *\n * @param <T> 元素类型\n * @author looly, jodd\n */\npublic class IterChain<T> implements Iterator<T>, Chain<Iterator<T>, IterChain<T>> {\n\n\tprotected final List<Iterator<T>> allIterators = new ArrayList<>();\n\n\t/**\n\t * 构造\n\t * 可以使用 {@link #addChain(Iterator)} 方法加入更多的集合。\n\t */\n\tpublic IterChain() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param iterators 多个{@link Iterator}\n\t */\n\t@SafeVarargs\n\tpublic IterChain(Iterator<T>... iterators) {\n\t\tfor (final Iterator<T> iterator : iterators) {\n\t\t\taddChain(iterator);\n\t\t}\n\t}\n\n\t@Override\n\tpublic IterChain<T> addChain(Iterator<T> iterator) {\n\t\tif (allIterators.contains(iterator)) {\n\t\t\tthrow new IllegalArgumentException(\"Duplicate iterator\");\n\t\t}\n\t\tallIterators.add(iterator);\n\t\treturn this;\n\t}\n\n\t// ---------------------------------------------------------------- interface\n\n\tprotected int currentIter = -1;\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\tif (currentIter == -1) {\n\t\t\tcurrentIter = 0;\n\t\t}\n\n\t\tfinal int size = allIterators.size();\n\t\tfor (int i = currentIter; i < size; i++) {\n\t\t\tfinal Iterator<T> iterator = allIterators.get(i);\n\t\t\tif (iterator.hasNext()) {\n\t\t\t\tcurrentIter = i;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic T next() {\n\t\tif (false == hasNext()) {\n\t\t\tthrow new NoSuchElementException();\n\t\t}\n\n\t\treturn allIterators.get(currentIter).next();\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tif (-1 == currentIter) {\n\t\t\tthrow new IllegalStateException(\"next() has not yet been called\");\n\t\t}\n\n\t\tallIterators.get(currentIter).remove();\n\t}\n\n\t@Override\n\tpublic Iterator<Iterator<T>> iterator() {\n\t\treturn this.allIterators.iterator();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Editor;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.Matcher;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.text.StrJoiner;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Dictionary;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * {@link Iterable} 和 {@link Iterator} 相关工具类\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class IterUtil {\n\n\t/**\n\t * 获取{@link Iterator}\n\t *\n\t * @param iterable {@link Iterable}\n\t * @param <T>      元素类型\n\t * @return 当iterable为null返回{@code null}，否则返回对应的{@link Iterator}\n\t * @since 5.7.2\n\t */\n\tpublic static <T> Iterator<T> getIter(Iterable<T> iterable) {\n\t\treturn null == iterable ? null : iterable.iterator();\n\t}\n\n\t/**\n\t * Iterable是否为空\n\t *\n\t * @param iterable Iterable对象\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Iterable<?> iterable) {\n\t\treturn null == iterable || isEmpty(iterable.iterator());\n\t}\n\n\t/**\n\t * Iterator是否为空\n\t *\n\t * @param Iterator Iterator对象\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Iterator<?> Iterator) {\n\t\treturn null == Iterator || false == Iterator.hasNext();\n\t}\n\n\t/**\n\t * Iterable是否为空\n\t *\n\t * @param iterable Iterable对象\n\t * @return 是否为空\n\t */\n\tpublic static boolean isNotEmpty(Iterable<?> iterable) {\n\t\treturn null != iterable && isNotEmpty(iterable.iterator());\n\t}\n\n\t/**\n\t * Iterator是否为空\n\t *\n\t * @param Iterator Iterator对象\n\t * @return 是否为空\n\t */\n\tpublic static boolean isNotEmpty(Iterator<?> Iterator) {\n\t\treturn null != Iterator && Iterator.hasNext();\n\t}\n\n\t/**\n\t * 是否包含{@code null}元素\n\t *\n\t * @param iter 被检查的{@link Iterable}对象，如果为{@code null} 返回true\n\t * @return 是否包含{@code null}元素\n\t */\n\tpublic static boolean hasNull(Iterable<?> iter) {\n\t\treturn hasNull(null == iter ? null : iter.iterator());\n\t}\n\n\t/**\n\t * 是否包含{@code null}元素\n\t *\n\t * @param iter 被检查的{@link Iterator}对象，如果为{@code null} 返回true\n\t * @return 是否包含{@code null}元素\n\t */\n\tpublic static boolean hasNull(Iterator<?> iter) {\n\t\tif (null == iter) {\n\t\t\treturn true;\n\t\t}\n\t\twhile (iter.hasNext()) {\n\t\t\tif (null == iter.next()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否全部元素为null\n\t *\n\t * @param iter iter 被检查的{@link Iterable}对象，如果为{@code null} 返回true\n\t * @return 是否全部元素为null\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isAllNull(Iterable<?> iter) {\n\t\treturn isAllNull(null == iter ? null : iter.iterator());\n\t}\n\n\t/**\n\t * 是否全部元素为null\n\t *\n\t * @param iter iter 被检查的{@link Iterator}对象，如果为{@code null} 返回true\n\t * @return 是否全部元素为null\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isAllNull(Iterator<?> iter) {\n\t\treturn null == getFirstNoneNull(iter);\n\t}\n\n\t/**\n\t * 根据集合返回一个元素计数的 {@link Map}<br>\n\t * 所谓元素计数就是假如这个集合中某个元素出现了n次，那将这个元素做为key，n做为value<br>\n\t * 例如：[a,b,c,c,c] 得到：<br>\n\t * a: 1<br>\n\t * b: 1<br>\n\t * c: 3<br>\n\t *\n\t * @param <T>  集合元素类型\n\t * @param iter {@link Iterator}，如果为null返回一个空的Map\n\t * @return {@link Map}\n\t */\n\tpublic static <T> Map<T, Integer> countMap(Iterator<T> iter) {\n\t\tfinal HashMap<T, Integer> countMap = new HashMap<>();\n\t\tif (null != iter) {\n\t\t\tT t;\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tt = iter.next();\n\t\t\t\tcountMap.put(t, countMap.getOrDefault(t, 0) + 1);\n\t\t\t}\n\t\t}\n\t\treturn countMap;\n\t}\n\n\t/**\n\t * 字段值与列表值对应的Map，常用于元素对象中有唯一ID时需要按照这个ID查找对象的情况<br>\n\t * 例如：车牌号 =》车\n\t *\n\t * @param <K>       字段名对应值得类型，不确定请使用Object\n\t * @param <V>       对象类型\n\t * @param iter      对象列表\n\t * @param fieldName 字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 4.0.4\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> fieldValueMap(Iterator<V> iter, String fieldName) {\n\t\treturn toMap(iter, new HashMap<>(), (value) -> (K) ReflectUtil.getFieldValue(value, fieldName));\n\t}\n\n\t/**\n\t * 两个字段值组成新的Map\n\t *\n\t * @param <K>               字段名对应值得类型，不确定请使用Object\n\t * @param <V>               值类型，不确定使用Object\n\t * @param iter              对象列表\n\t * @param fieldNameForKey   做为键的字段名（会通过反射获取其值）\n\t * @param fieldNameForValue 做为值的字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 4.0.10\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> fieldValueAsMap(Iterator<?> iter, String fieldNameForKey, String fieldNameForValue) {\n\t\treturn toMap(iter, new HashMap<>(),\n\t\t\t\t(value) -> (K) ReflectUtil.getFieldValue(value, fieldNameForKey),\n\t\t\t\t(value) -> (V) ReflectUtil.getFieldValue(value, fieldNameForValue)\n\t\t);\n\t}\n\n\t/**\n\t * 获取指定Bean列表中某个字段，生成新的列表\n\t *\n\t * @param <V>       对象类型\n\t * @param iterable  对象列表\n\t * @param fieldName 字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 4.6.2\n\t */\n\tpublic static <V> List<Object> fieldValueList(Iterable<V> iterable, String fieldName) {\n\t\treturn fieldValueList(getIter(iterable), fieldName);\n\t}\n\n\t/**\n\t * 获取指定Bean列表中某个字段，生成新的列表\n\t *\n\t * @param <V>       对象类型\n\t * @param iter      对象列表\n\t * @param fieldName 字段名（会通过反射获取其值）\n\t * @return 某个字段值与对象对应Map\n\t * @since 4.0.10\n\t */\n\tpublic static <V> List<Object> fieldValueList(Iterator<V> iter, String fieldName) {\n\t\tfinal List<Object> result = new ArrayList<>();\n\t\tif (null != iter) {\n\t\t\tV value;\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tvalue = iter.next();\n\t\t\t\tresult.add(ReflectUtil.getFieldValue(value, fieldName));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串<br>\n\t * 如果集合元素为数组、{@link Iterable}或{@link Iterator}，则递归组合其为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterator    集合\n\t * @param conjunction 分隔符\n\t * @return 连接后的字符串\n\t */\n\tpublic static <T> String join(Iterator<T> iterator, CharSequence conjunction) {\n\t\treturn StrJoiner.of(conjunction).append(iterator).toString();\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串<br>\n\t * 如果集合元素为数组、{@link Iterable}或{@link Iterator}，则递归组合其为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterator    集合\n\t * @param conjunction 分隔符\n\t * @param prefix      每个元素添加的前缀，null表示不添加\n\t * @param suffix      每个元素添加的后缀，null表示不添加\n\t * @return 连接后的字符串\n\t * @since 4.0.10\n\t */\n\tpublic static <T> String join(Iterator<T> iterator, CharSequence conjunction, String prefix, String suffix) {\n\t\treturn StrJoiner.of(conjunction, prefix, suffix)\n\t\t\t\t// 每个元素都添加前后缀\n\t\t\t\t.setWrapElement(true)\n\t\t\t\t.append(iterator)\n\t\t\t\t.toString();\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将集合转换为字符串<br>\n\t * 如果集合元素为数组、{@link Iterable}或{@link Iterator}，则递归组合其为字符串\n\t *\n\t * @param <T>         集合元素类型\n\t * @param iterator    集合\n\t * @param conjunction 分隔符\n\t * @param func        集合元素转换器，将元素转换为字符串\n\t * @return 连接后的字符串\n\t * @since 5.6.7\n\t */\n\tpublic static <T> String join(Iterator<T> iterator, CharSequence conjunction, Function<T, ? extends CharSequence> func) {\n\t\tif (null == iterator) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn StrJoiner.of(conjunction).append(iterator, func).toString();\n\t}\n\n\t/**\n\t * 将Entry集合转换为HashMap\n\t *\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @param entryIter entry集合\n\t * @return Map\n\t */\n\tpublic static <K, V> HashMap<K, V> toMap(Iterable<Entry<K, V>> entryIter) {\n\t\tfinal HashMap<K, V> map = new HashMap<>();\n\t\tif (isNotEmpty(entryIter)) {\n\t\t\tfor (Entry<K, V> entry : entryIter) {\n\t\t\t\tmap.put(entry.getKey(), entry.getValue());\n\t\t\t}\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 将键列表和值列表转换为Map<br>\n\t * 以键为准，值与键位置需对应。如果键元素数多于值元素，多余部分值用null代替。<br>\n\t * 如果值多于键，忽略多余的值。\n\t *\n\t * @param <K>    键类型\n\t * @param <V>    值类型\n\t * @param keys   键列表\n\t * @param values 值列表\n\t * @return 标题内容Map\n\t * @since 3.1.0\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterable<K> keys, Iterable<V> values) {\n\t\treturn toMap(keys, values, false);\n\t}\n\n\t/**\n\t * 将键列表和值列表转换为Map<br>\n\t * 以键为准，值与键位置需对应。如果键元素数多于值元素，多余部分值用null代替。<br>\n\t * 如果值多于键，忽略多余的值。\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param keys    键列表\n\t * @param values  值列表\n\t * @param isOrder 是否有序\n\t * @return 标题内容Map\n\t * @since 4.1.12\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterable<K> keys, Iterable<V> values, boolean isOrder) {\n\t\treturn toMap(null == keys ? null : keys.iterator(), null == values ? null : values.iterator(), isOrder);\n\t}\n\n\t/**\n\t * 将键列表和值列表转换为Map<br>\n\t * 以键为准，值与键位置需对应。如果键元素数多于值元素，多余部分值用null代替。<br>\n\t * 如果值多于键，忽略多余的值。\n\t *\n\t * @param <K>    键类型\n\t * @param <V>    值类型\n\t * @param keys   键列表\n\t * @param values 值列表\n\t * @return 标题内容Map\n\t * @since 3.1.0\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterator<K> keys, Iterator<V> values) {\n\t\treturn toMap(keys, values, false);\n\t}\n\n\t/**\n\t * 将键列表和值列表转换为Map<br>\n\t * 以键为准，值与键位置需对应。如果键元素数多于值元素，多余部分值用null代替。<br>\n\t * 如果值多于键，忽略多余的值。\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param keys    键列表\n\t * @param values  值列表\n\t * @param isOrder 是否有序\n\t * @return 标题内容Map\n\t * @since 4.1.12\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterator<K> keys, Iterator<V> values, boolean isOrder) {\n\t\tfinal Map<K, V> resultMap = MapUtil.newHashMap(isOrder);\n\t\tif (isNotEmpty(keys)) {\n\t\t\twhile (keys.hasNext()) {\n\t\t\t\tresultMap.put(keys.next(), (null != values && values.hasNext()) ? values.next() : null);\n\t\t\t}\n\t\t}\n\t\treturn resultMap;\n\t}\n\n\t/**\n\t * 将列表转成值为List的HashMap\n\t *\n\t * @param iterable  值列表\n\t * @param keyMapper Map的键映射\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <K, V> Map<K, List<V>> toListMap(Iterable<V> iterable, Function<V, K> keyMapper) {\n\t\treturn toListMap(iterable, keyMapper, v -> v);\n\t}\n\n\t/**\n\t * 将列表转成值为List的HashMap\n\t *\n\t * @param iterable    值列表\n\t * @param keyMapper   Map的键映射\n\t * @param valueMapper Map中List的值映射\n\t * @param <T>         列表值类型\n\t * @param <K>         键类型\n\t * @param <V>         值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <T, K, V> Map<K, List<V>> toListMap(Iterable<T> iterable, Function<T, K> keyMapper, Function<T, V> valueMapper) {\n\t\treturn toListMap(MapUtil.newHashMap(), iterable, keyMapper, valueMapper);\n\t}\n\n\t/**\n\t * 将列表转成值为List的HashMap\n\t *\n\t * @param resultMap   结果Map，可自定义结果Map类型\n\t * @param iterable    值列表\n\t * @param keyMapper   Map的键映射\n\t * @param valueMapper Map中List的值映射\n\t * @param <T>         列表值类型\n\t * @param <K>         键类型\n\t * @param <V>         值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <T, K, V> Map<K, List<V>> toListMap(Map<K, List<V>> resultMap, Iterable<T> iterable, Function<T, K> keyMapper, Function<T, V> valueMapper) {\n\t\tif (null == resultMap) {\n\t\t\tresultMap = MapUtil.newHashMap();\n\t\t}\n\t\tif (ObjectUtil.isNull(iterable)) {\n\t\t\treturn resultMap;\n\t\t}\n\n\t\tfor (T value : iterable) {\n\t\t\tresultMap.computeIfAbsent(keyMapper.apply(value), k -> new ArrayList<>()).add(valueMapper.apply(value));\n\t\t}\n\n\t\treturn resultMap;\n\t}\n\n\t/**\n\t * 将列表转成HashMap\n\t *\n\t * @param iterable  值列表\n\t * @param keyMapper Map的键映射\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterable<V> iterable, Function<V, K> keyMapper) {\n\t\treturn toMap(iterable, keyMapper, v -> v);\n\t}\n\n\t/**\n\t * 将列表转成HashMap\n\t *\n\t * @param iterable    值列表\n\t * @param keyMapper   Map的键映射\n\t * @param valueMapper Map的值映射\n\t * @param <T>         列表值类型\n\t * @param <K>         键类型\n\t * @param <V>         值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <T, K, V> Map<K, V> toMap(Iterable<T> iterable, Function<T, K> keyMapper, Function<T, V> valueMapper) {\n\t\treturn toMap(MapUtil.newHashMap(), iterable, keyMapper, valueMapper);\n\t}\n\n\t/**\n\t * 将列表转成Map\n\t *\n\t * @param resultMap   结果Map，通过传入map对象决定结果的Map类型\n\t * @param iterable    值列表\n\t * @param keyMapper   Map的键映射\n\t * @param valueMapper Map的值映射\n\t * @param <T>         列表值类型\n\t * @param <K>         键类型\n\t * @param <V>         值类型\n\t * @return HashMap\n\t * @since 5.3.6\n\t */\n\tpublic static <T, K, V> Map<K, V> toMap(Map<K, V> resultMap, Iterable<T> iterable, Function<T, K> keyMapper, Function<T, V> valueMapper) {\n\t\tif (null == resultMap) {\n\t\t\tresultMap = MapUtil.newHashMap();\n\t\t}\n\t\tif (ObjectUtil.isNull(iterable)) {\n\t\t\treturn resultMap;\n\t\t}\n\n\t\tfor (T value : iterable) {\n\t\t\tresultMap.put(keyMapper.apply(value), valueMapper.apply(value));\n\t\t}\n\n\t\treturn resultMap;\n\t}\n\n\t/**\n\t * Iterator转List<br>\n\t * 不判断，直接生成新的List\n\t *\n\t * @param <E>  元素类型\n\t * @param iter {@link Iterator}\n\t * @return List\n\t * @since 4.0.6\n\t */\n\tpublic static <E> List<E> toList(Iterable<E> iter) {\n\t\tif (null == iter) {\n\t\t\treturn null;\n\t\t}\n\t\treturn toList(iter.iterator());\n\t}\n\n\t/**\n\t * Iterator转List<br>\n\t * 不判断，直接生成新的List\n\t *\n\t * @param <E>  元素类型\n\t * @param iter {@link Iterator}\n\t * @return List\n\t * @since 4.0.6\n\t */\n\tpublic static <E> List<E> toList(Iterator<E> iter) {\n\t\treturn ListUtil.toList(iter);\n\t}\n\n\t/**\n\t * Enumeration转换为Iterator\n\t * <p>\n\t * Adapt the specified {@code Enumeration} to the {@code Iterator} interface\n\t *\n\t * @param <E> 集合元素类型\n\t * @param e   {@link Enumeration}\n\t * @return {@link Iterator}\n\t */\n\tpublic static <E> Iterator<E> asIterator(Enumeration<E> e) {\n\t\treturn new EnumerationIter<>(e);\n\t}\n\n\t/**\n\t * {@link Iterator} 转为 {@link Iterable}\n\t *\n\t * @param <E>  元素类型\n\t * @param iter {@link Iterator}\n\t * @return {@link Iterable}\n\t */\n\tpublic static <E> Iterable<E> asIterable(final Iterator<E> iter) {\n\t\treturn () -> iter;\n\t}\n\n\t/**\n\t * 遍历{@link Iterator}，获取指定index位置的元素\n\t *\n\t * @param iterator {@link Iterator}\n\t * @param index    位置\n\t * @param <E>      元素类型\n\t * @return 元素，找不到元素返回{@code null}\n\t * @since 5.8.0\n\t */\n\tpublic static <E> E get(final Iterator<E> iterator, int index) throws IndexOutOfBoundsException {\n\t\tif(null == iterator){\n\t\t\treturn null;\n\t\t}\n\t\tAssert.isTrue(index >= 0, \"[index] must be >= 0\");\n\t\twhile (iterator.hasNext()) {\n\t\t\tindex--;\n\t\t\tif (-1 == index) {\n\t\t\t\treturn iterator.next();\n\t\t\t}\n\t\t\titerator.next();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取集合的第一个元素，如果集合为空（null或者空集合），返回{@code null}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return 第一个元素，为空返回{@code null}\n\t */\n\tpublic static <T> T getFirst(Iterable<T> iterable) {\n\t\tif (iterable instanceof List) {\n\t\t\tfinal List<T> list = (List<T>) iterable;\n\t\t\treturn CollUtil.isEmpty(list) ? null: list.get(0);\n\t\t}\n\n\t\treturn getFirst(getIter(iterable));\n\t}\n\n\t/**\n\t * 获取集合的第一个非空元素\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return 第一个元素\n\t * @since 5.7.2\n\t */\n\tpublic static <T> T getFirstNoneNull(Iterable<T> iterable) {\n\t\tif (null == iterable) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getFirstNoneNull(iterable.iterator());\n\t}\n\n\t/**\n\t * 获取集合的第一个元素\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @return 第一个元素\n\t */\n\tpublic static <T> T getFirst(Iterator<T> iterator) {\n\t\treturn get(iterator, 0);\n\t}\n\n\t/**\n\t * 获取集合的第一个非空元素\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @return 第一个非空元素，null表示未找到\n\t * @since 5.7.2\n\t */\n\tpublic static <T> T getFirstNoneNull(Iterator<T> iterator) {\n\t\treturn firstMatch(iterator, Objects::nonNull);\n\t}\n\n\t/**\n\t * 返回{@link Iterator}中第一个匹配规则的值\n\t *\n\t * @param <T>      数组元素类型\n\t * @param iterator {@link Iterator}\n\t * @param matcher  匹配接口，实现此接口自定义匹配规则\n\t * @return 匹配元素，如果不存在匹配元素或{@link Iterator}为空，返回 {@code null}\n\t * @since 5.7.5\n\t */\n\tpublic static <T> T firstMatch(Iterator<T> iterator, Matcher<T> matcher) {\n\t\tAssert.notNull(matcher, \"Matcher must be not null !\");\n\t\tif (null != iterator) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\tfinal T next = iterator.next();\n\t\t\t\tif (matcher.match(next)) {\n\t\t\t\t\treturn next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得{@link Iterable}对象的元素类型（通过第一个非空元素判断）<br>\n\t * 注意，此方法至少会调用多次next方法\n\t *\n\t * @param iterable {@link Iterable}\n\t * @return 元素类型，当列表为空或元素全部为null时，返回null\n\t */\n\tpublic static Class<?> getElementType(Iterable<?> iterable) {\n\t\treturn getElementType(getIter(iterable));\n\t}\n\n\t/**\n\t * 获得{@link Iterator}对象的元素类型（通过第一个非空元素判断）<br>\n\t * 注意，此方法至少会调用多次next方法\n\t *\n\t * @param iterator {@link Iterator}，为 {@code null}返回{@code null}\n\t * @return 元素类型，当列表为空或元素全部为{@code null}时，返回{@code null}\n\t */\n\tpublic static Class<?> getElementType(Iterator<?> iterator) {\n\t\tif (null == iterator) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Object ele = getFirstNoneNull(iterator);\n\t\treturn null == ele ? null : ele.getClass();\n\t}\n\n\t/**\n\t * 编辑，此方法产生一个新{@link ArrayList}<br>\n\t * 编辑过程通过传入的Editor实现来返回需要的元素内容，这个Editor实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，如果返回null表示这个元素对象抛弃\n\t * 2、修改元素对象，返回集合中为修改后的对象\n\t * </pre>\n\t *\n\t * @param <T>    集合元素类型\n\t * @param iter   集合\n\t * @param editor 编辑器接口, {@code null}表示不编辑\n\t * @return 过滤后的集合\n\t * @since 5.7.1\n\t */\n\tpublic static <T> List<T> edit(Iterable<T> iter, Editor<T> editor) {\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tif (null == iter) {\n\t\t\treturn result;\n\t\t}\n\n\t\tT modified;\n\t\tfor (T t : iter) {\n\t\t\tmodified = (null == editor) ? t : editor.edit(t);\n\t\t\tif (null != modified) {\n\t\t\t\tresult.add(modified);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 过滤集合，此方法在原集合上直接修改<br>\n\t * 通过实现Filter接口，完成元素的过滤，这个Filter实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，{@link Filter#accept(Object)}方法返回false的对象将被使用{@link Iterator#remove()}方法移除\n\t * </pre>\n\t *\n\t * @param <T>    集合类型\n\t * @param <E>    集合元素类型\n\t * @param iter   集合\n\t * @param filter 过滤器接口\n\t * @return 编辑后的集合\n\t * @since 4.6.5\n\t */\n\tpublic static <T extends Iterable<E>, E> T filter(T iter, Filter<E> filter) {\n\t\tif (null == iter) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfilter(iter.iterator(), filter);\n\n\t\treturn iter;\n\t}\n\n\t/**\n\t * 过滤集合，此方法在原集合上直接修改<br>\n\t * 通过实现Filter接口，完成元素的过滤，这个Filter实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，{@link Filter#accept(Object)}方法返回false的对象将被使用{@link Iterator#remove()}方法移除\n\t * </pre>\n\t *\n\t * @param <E>    集合元素类型\n\t * @param iter   集合\n\t * @param filter 过滤器接口，删除{@link Filter#accept(Object)}为{@code false}的元素\n\t * @return 编辑后的集合\n\t * @since 4.6.5\n\t */\n\tpublic static <E> Iterator<E> filter(Iterator<E> iter, Filter<E> filter) {\n\t\tif (null == iter || null == filter) {\n\t\t\treturn iter;\n\t\t}\n\n\t\twhile (iter.hasNext()) {\n\t\t\tif (false == filter.accept(iter.next())) {\n\t\t\t\titer.remove();\n\t\t\t}\n\t\t}\n\t\treturn iter;\n\t}\n\n\t/**\n\t * 过滤{@link Iterator}并将过滤后满足条件的元素添加到List中\n\t *\n\t * @param <E>    元素类型\n\t * @param iter   {@link Iterator}\n\t * @param filter 过滤器，保留{@link Filter#accept(Object)}为{@code true}的元素\n\t * @return ArrayList\n\t * @since 5.7.22\n\t */\n\tpublic static <E> List<E> filterToList(Iterator<E> iter, Filter<E> filter) {\n\t\treturn toList(filtered(iter, filter));\n\t}\n\n\t/**\n\t * 获取一个新的 {@link FilterIter}，用于过滤指定元素\n\t *\n\t * @param iterator 被包装的 {@link Iterator}\n\t * @param filter   过滤断言，当{@link Filter#accept(Object)}为{@code true}时保留元素，{@code false}抛弃元素\n\t * @param <E>      元素类型\n\t * @return {@link FilterIter}\n\t * @since 5.8.0\n\t */\n\tpublic static <E> FilterIter<E> filtered(final Iterator<? extends E> iterator, final Filter<? super E> filter) {\n\t\treturn new FilterIter<>(iterator, filter);\n\t}\n\n\t/**\n\t * Iterator转换为Map，转换规则为：<br>\n\t * 按照keyFunc函数规则根据元素对象生成Key，元素作为值\n\t *\n\t * @param <K>      Map键类型\n\t * @param <V>      Map值类型\n\t * @param iterator 数据列表\n\t * @param map      Map对象，转换后的键值对加入此Map，通过传入此对象自定义Map类型\n\t * @param keyFunc  生成key的函数\n\t * @return 生成的map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V> Map<K, V> toMap(Iterator<V> iterator, Map<K, V> map, Func1<V, K> keyFunc) {\n\t\treturn toMap(iterator, map, keyFunc, (value) -> value);\n\t}\n\n\t/**\n\t * 集合转换为Map，转换规则为：<br>\n\t * 按照keyFunc函数规则根据元素对象生成Key，按照valueFunc函数规则根据元素对象生成value组成新的Map\n\t *\n\t * @param <K>       Map键类型\n\t * @param <V>       Map值类型\n\t * @param <E>       元素类型\n\t * @param iterator  数据列表\n\t * @param map       Map对象，转换后的键值对加入此Map，通过传入此对象自定义Map类型\n\t * @param keyFunc   生成key的函数\n\t * @param valueFunc 生成值的策略函数\n\t * @return 生成的map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V, E> Map<K, V> toMap(Iterator<E> iterator, Map<K, V> map, Func1<E, K> keyFunc, Func1<E, V> valueFunc) {\n\t\tif (null == iterator) {\n\t\t\treturn map;\n\t\t}\n\n\t\tif (null == map) {\n\t\t\tmap = MapUtil.newHashMap(true);\n\t\t}\n\n\t\tE element;\n\t\twhile (iterator.hasNext()) {\n\t\t\telement = iterator.next();\n\t\t\ttry {\n\t\t\t\tmap.put(keyFunc.call(element), valueFunc.call(element));\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 返回一个空Iterator\n\t *\n\t * @param <T> 元素类型\n\t * @return 空Iterator\n\t * @see Collections#emptyIterator()\n\t * @since 5.3.1\n\t */\n\tpublic static <T> Iterator<T> empty() {\n\t\treturn Collections.emptyIterator();\n\t}\n\n\t/**\n\t * 按照给定函数，转换{@link Iterator}为另一种类型的{@link Iterator}\n\t *\n\t * @param <F>      源元素类型\n\t * @param <T>      目标元素类型\n\t * @param iterator 源{@link Iterator}\n\t * @param function 转换函数\n\t * @return 转换后的{@link Iterator}\n\t * @since 5.4.3\n\t */\n\tpublic static <F, T> Iterator<T> trans(Iterator<F> iterator, Function<? super F, ? extends T> function) {\n\t\treturn new TransIter<>(iterator, function);\n\t}\n\n\t/**\n\t * 返回 Iterable 对象的元素数量\n\t *\n\t * @param iterable Iterable对象\n\t * @return Iterable对象的元素数量\n\t * @since 5.5.0\n\t */\n\tpublic static int size(Iterable<?> iterable) {\n\t\tif (null == iterable) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (iterable instanceof Collection<?>) {\n\t\t\treturn ((Collection<?>) iterable).size();\n\t\t} else {\n\t\t\treturn size(iterable.iterator());\n\t\t}\n\t}\n\n\t/**\n\t * 返回 Iterator 对象的元素数量\n\t *\n\t * @param iterator Iterator对象\n\t * @return Iterator对象的元素数量\n\t * @since 5.5.0\n\t */\n\tpublic static int size(Iterator<?> iterator) {\n\t\tint size = 0;\n\t\tif (iterator != null) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\titerator.next();\n\t\t\t\tsize++;\n\t\t\t}\n\t\t}\n\t\treturn size;\n\t}\n\n\t/**\n\t * 判断两个{@link Iterable} 是否元素和顺序相同，返回{@code true}的条件是：\n\t * <ul>\n\t *     <li>两个{@link Iterable}必须长度相同</li>\n\t *     <li>两个{@link Iterable}元素相同index的对象必须equals，满足{@link Objects#equals(Object, Object)}</li>\n\t * </ul>\n\t * 此方法来自Apache-Commons-Collections4。\n\t *\n\t * @param list1 列表1\n\t * @param list2 列表2\n\t * @return 是否相同\n\t * @since 5.6.0\n\t */\n\tpublic static boolean isEqualList(Iterable<?> list1, Iterable<?> list2) {\n\t\tif (list1 == list2) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfinal Iterator<?> it1 = list1.iterator();\n\t\tfinal Iterator<?> it2 = list2.iterator();\n\t\tObject obj1;\n\t\tObject obj2;\n\t\twhile (it1.hasNext() && it2.hasNext()) {\n\t\t\tobj1 = it1.next();\n\t\t\tobj2 = it2.next();\n\n\t\t\tif (false == Objects.equals(obj1, obj2)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// 当两个Iterable长度不一致时返回false\n\t\treturn false == (it1.hasNext() || it2.hasNext());\n\t}\n\n\t/**\n\t * 清空指定{@link Iterator}，此方法遍历后调用{@link Iterator#remove()}移除每个元素\n\t *\n\t * @param iterator {@link Iterator}\n\t * @since 5.7.23\n\t */\n\tpublic static void clear(Iterator<?> iterator) {\n\t\tif (null != iterator) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\titerator.next();\n\t\t\t\titerator.remove();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 遍历{@link Iterator}<br>\n\t * 当consumer为{@code null}表示不处理，但是依旧遍历{@link Iterator}\n\t *\n\t * @param iterator {@link Iterator}\n\t * @param consumer 节点消费，{@code null}表示不处理\n\t * @param <E>      元素类型\n\t * @since 5.8.0\n\t */\n\tpublic static <E> void forEach(final Iterator<E> iterator, final Consumer<? super E> consumer) {\n\t\tif (iterator != null) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\tfinal E element = iterator.next();\n\t\t\t\tif (null != consumer) {\n\t\t\t\t\tconsumer.accept(element);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 拼接 {@link Iterator}为字符串\n\t *\n\t * @param iterator {@link Iterator}\n\t * @param <E>      元素类型\n\t * @return 字符串\n\t * @since 5.8.0\n\t */\n\tpublic static <E> String toStr(final Iterator<E> iterator) {\n\t\treturn toStr(iterator, ObjectUtil::toString);\n\t}\n\n\t/**\n\t * 拼接 {@link Iterator}为字符串\n\t *\n\t * @param iterator  {@link Iterator}\n\t * @param transFunc 元素转字符串函数\n\t * @param <E>       元素类型\n\t * @return 字符串\n\t * @since 5.8.0\n\t */\n\tpublic static <E> String toStr(final Iterator<E> iterator, final Function<? super E, String> transFunc) {\n\t\treturn toStr(iterator, transFunc, \", \", \"[\", \"]\");\n\t}\n\n\t/**\n\t * 拼接 {@link Iterator}为字符串\n\t *\n\t * @param iterator  {@link Iterator}\n\t * @param transFunc 元素转字符串函数\n\t * @param delimiter 分隔符\n\t * @param prefix    前缀\n\t * @param suffix    后缀\n\t * @param <E>       元素类型\n\t * @return 字符串\n\t * @since 5.8.0\n\t */\n\tpublic static <E> String toStr(final Iterator<E> iterator,\n\t\t\t\t\t\t\t\t   final Function<? super E, String> transFunc,\n\t\t\t\t\t\t\t\t   final String delimiter,\n\t\t\t\t\t\t\t\t   final String prefix,\n\t\t\t\t\t\t\t\t   final String suffix) {\n\t\tfinal StrJoiner strJoiner = StrJoiner.of(delimiter, prefix, suffix);\n\t\tstrJoiner.append(iterator, transFunc);\n\t\treturn strJoiner.toString();\n\t}\n\n\t/**\n\t * 从给定的对象中获取可能存在的{@link Iterator}，规则如下：\n\t * <ul>\n\t *   <li>null - null</li>\n\t *   <li>Iterator - 直接返回</li>\n\t *   <li>Enumeration - {@link EnumerationIter}</li>\n\t *   <li>Collection - 调用{@link Collection#iterator()}</li>\n\t *   <li>Map - Entry的{@link Iterator}</li>\n\t *   <li>Dictionary - values (elements) enumeration returned as iterator</li>\n\t *   <li>array - {@link ArrayIter}</li>\n\t *   <li>NodeList - {@link NodeListIter}</li>\n\t *   <li>Node - 子节点</li>\n\t *   <li>object with iterator() public method，通过反射访问</li>\n\t *   <li>object - 单对象的{@link ArrayIter}</li>\n\t * </ul>\n\t *\n\t * @param obj 可以获取{@link Iterator}的对象\n\t * @return {@link Iterator}，如果提供对象为{@code null}，返回{@code null}\n\t */\n\tpublic static Iterator<?> getIter(final Object obj) {\n\t\tif (obj == null) {\n\t\t\treturn null;\n\t\t} else if (obj instanceof Iterator) {\n\t\t\treturn (Iterator<?>) obj;\n\t\t} else if (obj instanceof Iterable) {\n\t\t\treturn ((Iterable<?>) obj).iterator();\n\t\t} else if (ArrayUtil.isArray(obj)) {\n\t\t\treturn new ArrayIter<>(obj);\n\t\t} else if (obj instanceof Enumeration) {\n\t\t\treturn new EnumerationIter<>((Enumeration<?>) obj);\n\t\t} else if (obj instanceof Map) {\n\t\t\treturn ((Map<?, ?>) obj).entrySet().iterator();\n\t\t} else if (obj instanceof NodeList) {\n\t\t\treturn new NodeListIter((NodeList) obj);\n\t\t} else if (obj instanceof Node) {\n\t\t\t// 遍历子节点\n\t\t\treturn new NodeListIter(((Node) obj).getChildNodes());\n\t\t} else if (obj instanceof Dictionary) {\n\t\t\treturn new EnumerationIter<>(((Dictionary<?, ?>) obj).elements());\n\t\t}\n\n\t\t// 反射获取\n\t\ttry {\n\t\t\tfinal Object iterator = ReflectUtil.invoke(obj, \"iterator\");\n\t\t\tif (iterator instanceof Iterator) {\n\t\t\t\treturn (Iterator<?>) iterator;\n\t\t\t}\n\t\t} catch (final RuntimeException ignore) {\n\t\t\t// ignore\n\t\t}\n\t\treturn new ArrayIter<>(new Object[]{obj});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/IterableIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.Iterator;\n\n/**\n * 提供合成接口，共同提供{@link Iterable}和{@link Iterator}功能\n *\n * @param <T> 节点类型\n * @author looly\n * @since 5.7.14\n */\npublic interface IterableIter<T> extends Iterable<T>, Iterator<T> {\n\n\t@Override\n\tdefault Iterator<T> iterator() {\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.io.Serializable;\nimport java.util.Enumeration;\nimport java.util.Iterator;\n\n/**\n * {@link Iterator}对象转{@link Enumeration}\n * @author Looly\n *\n * @param <E> 元素类型\n * @since 3.0.8\n */\npublic class IteratorEnumeration<E> implements Enumeration<E>, Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Iterator<E> iterator;\n\n\t/**\n\t * 构造\n\t * @param iterator {@link Iterator}对象\n\t */\n\tpublic IteratorEnumeration(Iterator<E> iterator) {\n\t\tthis.iterator = iterator;\n\t}\n\n\t@Override\n\tpublic boolean hasMoreElements() {\n\t\treturn iterator.hasNext();\n\t}\n\n\t@Override\n\tpublic E nextElement() {\n\t\treturn iterator.next();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/LineIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.BufferedReader;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\n\n/**\n * 将Reader包装为一个按照行读取的Iterator<br>\n * 此对象遍历结束后，应关闭之，推荐使用方式:\n *\n * <pre>\n * LineIterator it = null;\n * try {\n * \tit = new LineIterator(reader);\n * \twhile (it.hasNext()) {\n * \t\tString line = it.nextLine();\n * \t\t// do something with line\n * \t}\n * } finally {\n * \t\tit.close();\n * }\n * </pre>\n *\n * 此类来自于Apache Commons io\n *\n * @author looly\n * @since 4.1.1\n */\npublic class LineIter extends ComputeIter<String> implements IterableIter<String>, Closeable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final BufferedReader bufferedReader;\n\n\t/**\n\t * 构造\n\t *\n\t * @param in {@link InputStream}\n\t * @param charset 编码\n\t * @throws IllegalArgumentException reader为null抛出此异常\n\t */\n\tpublic LineIter(InputStream in, Charset charset) throws IllegalArgumentException {\n\t\tthis(IoUtil.getReader(in, charset));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param reader {@link Reader}对象，不能为null\n\t * @throws IllegalArgumentException reader为null抛出此异常\n\t */\n\tpublic LineIter(Reader reader) throws IllegalArgumentException {\n\t\tAssert.notNull(reader, \"Reader must not be null\");\n\t\tthis.bufferedReader = IoUtil.getReader(reader);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tprotected String computeNext() {\n\t\ttry {\n\t\t\twhile (true) {\n\t\t\t\tString line = bufferedReader.readLine();\n\t\t\t\tif (line == null) {\n\t\t\t\t\treturn null;\n\t\t\t\t} else if (isValidLine(line)) {\n\t\t\t\t\treturn line;\n\t\t\t\t}\n\t\t\t\t// 无效行，则跳过进入下一行\n\t\t\t}\n\t\t} catch (IOException ioe) {\n\t\t\tclose();\n\t\t\tthrow new IORuntimeException(ioe);\n\t\t}\n\t}\n\n\t/**\n\t * 关闭Reader\n\t */\n\t@Override\n\tpublic void close() {\n\t\tsuper.finish();\n\t\tIoUtil.close(bufferedReader);\n\t}\n\n\t/**\n\t * 重写此方法来判断是否每一行都被返回，默认全部为true\n\t *\n\t * @param line 需要验证的行\n\t * @return 是否通过验证\n\t */\n\tprotected boolean isValidLine(String line) {\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.comparator.PinyinComparator;\nimport cn.hutool.core.comparator.PropertyComparator;\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Matcher;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.PageUtil;\n\nimport java.util.*;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\n\n/**\n * List相关工具类\n *\n * @author looly\n */\npublic class ListUtil {\n\t/**\n\t * 新建一个空List\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked) {\n\t\treturn isLinked ? new LinkedList<>() : new ArrayList<>();\n\t}\n\n\t/**\n\t * 新建一个List\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param values   数组\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\t@SafeVarargs\n\tpublic static <T> List<T> list(boolean isLinked, T... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn list(isLinked);\n\t\t}\n\t\tfinal List<T> arrayList = isLinked ? new LinkedList<>() : new ArrayList<>(values.length);\n\t\tCollections.addAll(arrayList, values);\n\t\treturn arrayList;\n\t}\n\n\t/**\n\t * 新建一个List\n\t *\n\t * @param <T>        集合元素类型\n\t * @param isLinked   是否新建LinkedList\n\t * @param collection 集合\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Collection<T> collection) {\n\t\tif (null == collection) {\n\t\t\treturn list(isLinked);\n\t\t}\n\t\treturn isLinked ? new LinkedList<>(collection) : new ArrayList<>(collection);\n\t}\n\n\t/**\n\t * 新建一个List<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param iterable {@link Iterable}\n\t * @return List对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Iterable<T> iterable) {\n\t\tif (null == iterable) {\n\t\t\treturn list(isLinked);\n\t\t}\n\t\treturn list(isLinked, iterable.iterator());\n\t}\n\n\t/**\n\t * 新建一个List<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param isLinked 是否新建LinkedList\n\t * @param iter     {@link Iterator}\n\t * @return ArrayList对象\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Iterator<T> iter) {\n\t\tfinal List<T> list = list(isLinked);\n\t\tif (null != iter) {\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tlist.add(iter.next());\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 新建一个List<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>        集合元素类型\n\t * @param isLinked   是否新建LinkedList\n\t * @param enumration {@link Enumeration}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> List<T> list(boolean isLinked, Enumeration<T> enumration) {\n\t\tfinal List<T> list = list(isLinked);\n\t\tif (null != enumration) {\n\t\t\twhile (enumration.hasMoreElements()) {\n\t\t\t\tlist.add(enumration.nextElement());\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 新建一个ArrayList\n\t *\n\t * @param <T>    集合元素类型\n\t * @param values 数组\n\t * @return ArrayList对象\n\t */\n\t@SafeVarargs\n\tpublic static <T> ArrayList<T> toList(T... values) {\n\t\treturn (ArrayList<T>) list(false, values);\n\t}\n\n\t/**\n\t * 新建LinkedList\n\t *\n\t * @param values 数组\n\t * @param <T>    类型\n\t * @return LinkedList\n\t * @since 4.1.2\n\t */\n\t@SafeVarargs\n\tpublic static <T> LinkedList<T> toLinkedList(T... values) {\n\t\treturn (LinkedList<T>) list(true, values);\n\t}\n\n\t/**\n\t * 数组转为一个不可变List<br>\n\t * 类似于Java9中的List.of\n\t *\n\t * @param ts  对象\n\t * @param <T> 对象类型\n\t * @return 不可修改List\n\t * @since 5.4.3\n\t */\n\t@SafeVarargs\n\tpublic static <T> List<T> of(T... ts) {\n\t\tif (ArrayUtil.isEmpty(ts)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\treturn Collections.unmodifiableList(toList(ts));\n\t}\n\n\t/**\n\t * 新建一个CopyOnWriteArrayList\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return {@link CopyOnWriteArrayList}\n\t */\n\tpublic static <T> CopyOnWriteArrayList<T> toCopyOnWriteArrayList(Collection<T> collection) {\n\t\treturn (null == collection) ? (new CopyOnWriteArrayList<>()) : (new CopyOnWriteArrayList<>(collection));\n\t}\n\n\t/**\n\t * 新建一个ArrayList\n\t *\n\t * @param <T>        集合元素类型\n\t * @param collection 集合\n\t * @return ArrayList对象\n\t */\n\tpublic static <T> ArrayList<T> toList(Collection<T> collection) {\n\t\treturn (ArrayList<T>) list(false, collection);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterable {@link Iterable}\n\t * @return ArrayList对象\n\t * @since 3.1.0\n\t */\n\tpublic static <T> ArrayList<T> toList(Iterable<T> iterable) {\n\t\treturn (ArrayList<T>) list(false, iterable);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>      集合元素类型\n\t * @param iterator {@link Iterator}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> ArrayList<T> toList(Iterator<T> iterator) {\n\t\treturn (ArrayList<T>) list(false, iterator);\n\t}\n\n\t/**\n\t * 新建一个ArrayList<br>\n\t * 提供的参数为null时返回空{@link ArrayList}\n\t *\n\t * @param <T>         集合元素类型\n\t * @param enumeration {@link Enumeration}\n\t * @return ArrayList对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> ArrayList<T> toList(Enumeration<T> enumeration) {\n\t\treturn (ArrayList<T>) list(false, enumeration);\n\t}\n\n\t/**\n\t * 对指定List分页取值\n\t *\n\t * @param <T>      集合元素类型\n\t * @param pageNo   页码，第一页的页码取决于{@link PageUtil#getFirstPageNo()}，默认0\n\t * @param pageSize 每页的条目数\n\t * @param list     列表\n\t * @return 分页后的段落内容\n\t * @since 4.1.20\n\t */\n\tpublic static <T> List<T> page(int pageNo, int pageSize, List<T> list) {\n\t\tif (CollUtil.isEmpty(list)) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\tint resultSize = list.size();\n\t\t// 每页条目数大于总数直接返回所有\n\t\tif (resultSize <= pageSize) {\n\t\t\tif (pageNo < (PageUtil.getFirstPageNo() + 1)) {\n\t\t\t\treturn unmodifiable(list);\n\t\t\t} else {\n\t\t\t\t// 越界直接返回空\n\t\t\t\treturn new ArrayList<>(0);\n\t\t\t}\n\t\t}\n\t\t// 相乘可能会导致越界 临时用long\n\t\tif (((long) (pageNo - PageUtil.getFirstPageNo()) * pageSize) > resultSize) {\n\t\t\t// 越界直接返回空\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\tfinal int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize);\n\t\tif (startEnd[1] > resultSize) {\n\t\t\tstartEnd[1] = resultSize;\n\t\t\tif (startEnd[0] > startEnd[1]) {\n\t\t\t\treturn new ArrayList<>(0);\n\t\t\t}\n\t\t}\n\n\t\treturn sub(list, startEnd[0], startEnd[1]);\n\t}\n\n\t/**\n\t * 对指定List进行分页，逐页返回数据\n\t *\n\t * @param <T>              集合元素类型\n\t * @param list             源数据列表\n\t * @param pageSize         每页的条目数\n\t * @param pageListConsumer 单页数据函数式返回\n\t * @since 5.7.10\n\t */\n\tpublic static <T> void page(List<T> list, int pageSize, Consumer<List<T>> pageListConsumer) {\n\t\tif (CollUtil.isEmpty(list) || pageSize <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal int total = list.size();\n\t\tfinal int totalPage = PageUtil.totalPage(total, pageSize);\n\t\tfor (int pageNo = PageUtil.getFirstPageNo(); pageNo < totalPage + PageUtil.getFirstPageNo(); pageNo++) {\n\t\t\t// 获取当前页在列表中对应的起止序号\n\t\t\tfinal int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize);\n\t\t\tif (startEnd[1] > total) {\n\t\t\t\tstartEnd[1] = total;\n\t\t\t}\n\n\t\t\t// 返回数据\n\t\t\tpageListConsumer.accept(sub(list, startEnd[0], startEnd[1]));\n\t\t}\n\t}\n\n\t/**\n\t * 针对List排序，排序会修改原List\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被排序的List\n\t * @param c    {@link Comparator}\n\t * @return 原list\n\t * @see Collections#sort(List, Comparator)\n\t */\n\tpublic static <T> List<T> sort(List<T> list, Comparator<? super T> c) {\n\t\tif (CollUtil.isEmpty(list)) {\n\t\t\treturn list;\n\t\t}\n\t\tlist.sort(c);\n\t\treturn list;\n\t}\n\n\t/**\n\t * 根据Bean的属性排序\n\t *\n\t * @param <T>      元素类型\n\t * @param list     List\n\t * @param property 属性名\n\t * @return 排序后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sortByProperty(List<T> list, String property) {\n\t\treturn sort(list, new PropertyComparator<>(property));\n\t}\n\n\t/**\n\t * 根据汉字的拼音顺序排序\n\t *\n\t * @param list List\n\t * @return 排序后的List\n\t * @since 4.0.8\n\t */\n\tpublic static List<String> sortByPinyin(List<String> list) {\n\t\treturn sort(list, new PinyinComparator());\n\t}\n\n\t/**\n\t * 反序给定List，会在原List基础上直接修改\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被反转的List\n\t * @return 反转后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> reverse(List<T> list) {\n\t\tCollections.reverse(list);\n\t\treturn list;\n\t}\n\n\t/**\n\t * 反序给定List，会创建一个新的List，原List数据不变\n\t *\n\t * @param <T>  元素类型\n\t * @param list 被反转的List\n\t * @return 反转后的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> reverseNew(List<T> list) {\n\t\tif (list == null) {\n\t\t\treturn null;\n\t\t}\n\t\tList<T> list2 = ObjectUtil.clone(list);\n\t\tif (null == list2) {\n\t\t\t// 不支持clone\n\t\t\tlist2 = new ArrayList<>(list);\n\t\t}\n\n\t\ttry {\n\t\t\treturn reverse(list2);\n\t\t} catch (final UnsupportedOperationException e) {\n\t\t\t// 提供的列表不可编辑,新建列表\n\t\t\treturn reverse(list(false, list));\n\t\t}\n\t}\n\n\t/**\n\t * 设置或增加元素。当index小于List的长度时，替换指定位置的值，否则在尾部追加\n\t *\n\t * @param <T>     元素类型\n\t * @param list    List列表\n\t * @param index   位置\n\t * @param element 新元素\n\t * @return 原List\n\t * @since 4.1.2\n\t */\n\tpublic static <T> List<T> setOrAppend(List<T> list, int index, T element) {\n\t\tAssert.notNull(list, \"List must be not null !\");\n\t\tif (index < list.size()) {\n\t\t\tlist.set(index, element);\n\t\t} else {\n\t\t\tlist.add(element);\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 在指定位置设置元素。当index小于List的长度时，替换指定位置的值，否则追加{@code null}直到到达index后，设置值\n\t *\n\t * @param <T>     元素类型\n\t * @param list    List列表\n\t * @param index   位置\n\t * @param element 新元素\n\t * @return 原List\n\t * @since 5。8.4\n\t */\n\tpublic static <T> List<T> setOrPadding(List<T> list, int index, T element) {\n\t\treturn setOrPadding(list, index, element, null);\n\t}\n\n\t/**\n\t * 在指定位置设置元素。当index小于List的长度时，替换指定位置的值，否则追加{@code paddingElement}直到到达index后，设置值<br>\n\t * 注意：为避免OOM问题，此方法限制index的最大值为{@code (list.size() + 1) * 10}\n\t *\n\t * @param <T>            元素类型\n\t * @param list           List列表\n\t * @param index          位置\n\t * @param element        新元素\n\t * @param paddingElement 填充的值\n\t * @return 原List\n\t * @since 5.8.4\n\t */\n\tpublic static <T> List<T> setOrPadding(List<T> list, int index, T element, T paddingElement) {\n\t\treturn setOrPadding(list, index, element, paddingElement, (list.size() + 1) * 10);\n\t}\n\n\t/**\n\t * 在指定位置设置元素。当index小于List的长度时，替换指定位置的值，否则追加{@code paddingElement}直到到达index后，设置值\n\t *\n\t * @param <T>            元素类型\n\t * @param list           List列表\n\t * @param index          位置\n\t * @param element        新元素\n\t * @param paddingElement 填充的值\n\t * @param indexLimit     最大索引限制\n\t * @return 原List\n\t * @since 5.8.28\n\t */\n\tpublic static <T> List<T> setOrPadding(List<T> list, int index, T element, T paddingElement, int indexLimit) {\n\t\tAssert.notNull(list, \"List must be not null !\");\n\t\tfinal int size = list.size();\n\t\tif (index < size) {\n\t\t\tlist.set(index, element);\n\t\t} else {\n\t\t\tif (indexLimit > 0) {\n\t\t\t\t// issue#3286, 增加安全检查\n\t\t\t\tif (index > indexLimit) {\n\t\t\t\t\tthrow new ValidateException(\"Index [{}] is too large for limit: [{}]\", index, indexLimit);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (int i = size; i < index; i++) {\n\t\t\t\tlist.add(paddingElement);\n\t\t\t}\n\t\t\tlist.add(element);\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 截取集合的部分\n\t *\n\t * @param <T>   集合元素类型\n\t * @param list  被截取的数组\n\t * @param start 开始位置（包含）\n\t * @param end   结束位置（不包含）\n\t * @return 截取后的数组，当开始位置超过最大时，返回空的List\n\t */\n\tpublic static <T> List<T>\n\tsub(List<T> list, int start, int end) {\n\t\treturn sub(list, start, end, 1);\n\t}\n\n\t/**\n\t * 截取集合的部分<br>\n\t * 此方法与{@link List#subList(int, int)} 不同在于子列表是新的副本，操作子列表不会影响原列表。\n\t *\n\t * @param <T>   集合元素类型\n\t * @param list  被截取的数组\n\t * @param start 开始位置（包含）\n\t * @param end   结束位置（不包含）\n\t * @param step  步进\n\t * @return 截取后的数组，当开始位置超过最大时，返回空的List\n\t * @since 4.0.6\n\t */\n\tpublic static <T> List<T> sub(List<T> list, int start, int end, int step) {\n\t\tif (list == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (list.isEmpty()) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\tfinal int size = list.size();\n\t\tif (start < 0) {\n\t\t\tstart += size;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += size;\n\t\t}\n\t\tif (start == size) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > size) {\n\t\t\tif (start >= size) {\n\t\t\t\treturn new ArrayList<>(0);\n\t\t\t}\n\t\t\tend = size;\n\t\t}\n\n\t\tif (step < 1) {\n\t\t\tstep = 1;\n\t\t}\n\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tfor (int i = start; i < end; i += step) {\n\t\t\tresult.add(list.get(i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取匹配规则定义中匹配到元素的最后位置<br>\n\t * 此方法对于某些无序集合的位置信息，以转换为数组后的位置为准。\n\t *\n\t * @param <T>     元素类型\n\t * @param list    List集合\n\t * @param matcher 匹配器，为空则全部匹配\n\t * @return 最后一个位置\n\t * @since 5.6.6\n\t */\n\tpublic static <T> int lastIndexOf(List<T> list, Matcher<T> matcher) {\n\t\tif (null != list) {\n\t\t\tfinal int size = list.size();\n\t\t\tif (size > 0) {\n\t\t\t\tfor (int i = size - 1; i >= 0; i--) {\n\t\t\t\t\tif (null == matcher || matcher.match(list.get(i))) {\n\t\t\t\t\t\treturn i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 获取匹配规则定义中匹配到元素的所有位置\n\t *\n\t * @param <T>     元素类型\n\t * @param list    列表\n\t * @param matcher 匹配器，为空则全部匹配\n\t * @return 位置数组\n\t * @since 5.2.5\n\t */\n\tpublic static <T> int[] indexOfAll(List<T> list, Matcher<T> matcher) {\n\t\treturn CollUtil.indexOfAll(list, matcher);\n\t}\n\n\t/**\n\t * 将对应List转换为不可修改的List\n\t *\n\t * @param list List\n\t * @param <T>  元素类型\n\t * @return 不可修改List\n\t * @since 5.2.6\n\t */\n\tpublic static <T> List<T> unmodifiable(List<T> list) {\n\t\tif (null == list) {\n\t\t\treturn null;\n\t\t}\n\t\treturn Collections.unmodifiableList(list);\n\t}\n\n\t/**\n\t * 获取一个空List，这个空List不可变\n\t *\n\t * @param <T> 元素类型\n\t * @return 空的List\n\t * @see Collections#emptyList()\n\t * @since 5.2.6\n\t */\n\tpublic static <T> List<T> empty() {\n\t\treturn Collections.emptyList();\n\t}\n\n\t/**\n\t * 通过传入分区长度，将指定列表分区为不同的块，每块区域的长度相同（最后一块可能小于长度）<br>\n\t * 分区是在原List的基础上进行的，返回的分区是不可变的抽象列表，原列表元素变更，分区中元素也会变更。\n\t *\n\t * <p>\n\t * 需要特别注意的是，此方法调用{@link List#subList(int, int)}切分List，\n\t * 此方法返回的是原List的视图，也就是说原List有变更，切分后的结果也会变更。\n\t * </p>\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表，为空时返回{@link #empty()}\n\t * @param size 每个段的长度，当长度超过list长度时，size按照list长度计算，即只返回一个节点\n\t * @return 分段列表\n\t * @since 5.4.5\n\t */\n\tpublic static <T> List<List<T>> partition(List<T> list, int size) {\n\t\tif (CollUtil.isEmpty(list)) {\n\t\t\treturn empty();\n\t\t}\n\n\t\treturn (list instanceof RandomAccess)\n\t\t\t? new RandomAccessPartition<>(list, size)\n\t\t\t: new Partition<>(list, size);\n\t}\n\n\t/**\n\t * 对集合按照指定长度分段，每一个段为单独的集合，返回这个集合的列表\n\t *\n\t * <p>\n\t * 需要特别注意的是，此方法调用{@link List#subList(int, int)}切分List，\n\t * 此方法返回的是原List的视图，也就是说原List有变更，切分后的结果也会变更。\n\t * </p>\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表，为空时返回{@link #empty()}\n\t * @param size 每个段的长度，当长度超过list长度时，size按照list长度计算，即只返回一个节点\n\t * @return 分段列表\n\t * @see #partition(List, int)\n\t * @since 5.4.5\n\t */\n\tpublic static <T> List<List<T>> split(List<T> list, int size) {\n\t\treturn partition(list, size);\n\t}\n\n\t/**\n\t * 将集合平均分成多个list，返回这个集合的列表\n\t * <p>例：</p>\n\t * <pre>\n\t *     ListUtil.splitAvg(null, 3);\t// []\n\t *     ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 2);\t// [[1, 2], [3, 4]]\n\t *     ListUtil.splitAvg(Arrays.asList(1, 2, 3), 5);\t// [[1], [2], [3], [], []]\n\t *     ListUtil.splitAvg(Arrays.asList(1, 2, 3), 2);\t// [[1, 2], [3]]\n\t * </pre>\n\t *\n\t * @param <T>   集合元素类型\n\t * @param list  集合\n\t * @param limit 要均分成几个list\n\t * @return 分段列表\n\t * @author lileming\n\t * @since 5.7.10\n\t */\n\tpublic static <T> List<List<T>> splitAvg(List<T> list, int limit) {\n\t\tif (CollUtil.isEmpty(list)) {\n\t\t\treturn empty();\n\t\t}\n\n\t\treturn (list instanceof RandomAccess)\n\t\t\t? new RandomAccessAvgPartition<>(list, limit)\n\t\t\t: new AvgPartition<>(list, limit);\n\t}\n\n\t/**\n\t * 将指定元素交换到指定索引位置,其他元素的索引值不变<br>\n\t * 交换会修改原List<br>\n\t * 如果集合中有多个相同元素，只交换第一个找到的元素\n\t *\n\t * @param <T>         元素类型\n\t * @param list        列表\n\t * @param element     需交换元素\n\t * @param targetIndex 目标索引\n\t * @since 5.7.13\n\t */\n\tpublic static <T> void swapTo(List<T> list, T element, Integer targetIndex) {\n\t\tif (CollUtil.isNotEmpty(list)) {\n\t\t\tfinal int index = list.indexOf(element);\n\t\t\tif (index >= 0) {\n\t\t\t\tCollections.swap(list, index, targetIndex);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将指定元素交换到指定元素位置,其他元素的索引值不变<br>\n\t * 交换会修改原List<br>\n\t * 如果集合中有多个相同元素，只交换第一个找到的元素\n\t *\n\t * @param <T>           元素类型\n\t * @param list          列表\n\t * @param element       需交换元素\n\t * @param targetElement 目标元素\n\t */\n\tpublic static <T> void swapElement(List<T> list, T element, T targetElement) {\n\t\tif (CollUtil.isNotEmpty(list)) {\n\t\t\tfinal int targetIndex = list.indexOf(targetElement);\n\t\t\tif (targetIndex >= 0) {\n\t\t\t\tswapTo(list, element, targetIndex);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将元素移动到指定列表的新位置。\n\t * <ul>\n\t *     <li>如果元素不在列表中，则将其添加到新位置。</li>\n\t *     <li>如果元素已在列表中，则先移除它，然后再将其添加到新位置。</li>\n\t * </ul>\n\t *\n\t * @param list        原始列表，元素将在这个列表上进行操作。\n\t * @param element     需要移动的元素。\n\t * @param newPosition 元素的新位置，从0开始计数，位置计算是以移除元素后的列表位置计算的\n\t * @param <T>         列表和元素的通用类型。\n\t * @return 更新后的列表。\n\t * @since 5.8.29\n\t */\n\tpublic static <T> List<T> move(List<T> list, T element, int newPosition) {\n\t\tAssert.notNull(list);\n\t\tif (false == list.contains(element)) {\n\t\t\tlist.add(newPosition, element);\n\t\t} else {\n\t\t\tlist.remove(element);\n\t\t\tlist.add(newPosition, element);\n\t\t}\n\t\treturn list;\n\t}\n\n\n\t/**\n\t * 将两个列表的元素按照索引一一配对，通过指定的函数进行合并，返回一个新的结果列表。\n\t * 新列表的长度将以两个输入列表中较短的那个为准。\n\t *\n\t * @param <A>    第一个列表的元素类型\n\t * @param <B>    第二个列表的元素类型\n\t * @param <R>    结果列表的元素类型\n\t * @param listA  第一个列表\n\t * @param listB  第二个列表\n\t * @param zipper 合并函数，接收来自listA和listB的两个元素，返回一个结果元素\n\t * @return 合并后的新列表\n\t * @since 5.8.42\n\t */\n\tpublic static <A, B, R> List<R> zip(List<A> listA, List<B> listB, BiFunction<A, B, R> zipper) {\n\t\tif (CollUtil.isEmpty(listA) || CollUtil.isEmpty(listB)) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\t\tAssert.notNull(zipper, \"Zipper function must not be null\");\n\n\t\tfinal int size = Math.min(listA.size(), listB.size());\n\t\tfinal List<R> result = new ArrayList<>(size);\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tresult.add(zipper.apply(listA.get(i), listB.get(i)));\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/NodeListIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/**\n * 包装 {@link NodeList} 的{@link Iterator}\n * <p>\n * 此 iterator 不支持 {@link #remove()} 方法。\n *\n * @author apache commons,looly\n * @see NodeList\n * @since 5.8.0\n */\npublic class NodeListIter implements ResettableIter<Node> {\n\n\tprivate final NodeList nodeList;\n\t/**\n\t * 当前位置索引\n\t */\n\tprivate int index = 0;\n\n\t/**\n\t * 构造, 根据给定{@link NodeList} 创建{@code NodeListIterator}\n\t *\n\t * @param nodeList {@link NodeList}，非空\n\t */\n\tpublic NodeListIter(final NodeList nodeList) {\n\t\tthis.nodeList = Assert.notNull(nodeList, \"NodeList must not be null.\");\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn nodeList != null && index < nodeList.getLength();\n\t}\n\n\t@Override\n\tpublic Node next() {\n\t\tif (nodeList != null && index < nodeList.getLength()) {\n\t\t\treturn nodeList.item(index++);\n\t\t}\n\t\tthrow new NoSuchElementException(\"underlying nodeList has no more elements\");\n\t}\n\n\t/**\n\t * Throws {@link UnsupportedOperationException}.\n\t *\n\t * @throws UnsupportedOperationException always\n\t */\n\t@Override\n\tpublic void remove() {\n\t\tthrow new UnsupportedOperationException(\"remove() method not supported for a NodeListIterator.\");\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\tthis.index = 0;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/Partition.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.AbstractList;\nimport java.util.List;\n\n/**\n * 列表分区或分段<br>\n * 通过传入分区长度，将指定列表分区为不同的块，每块区域的长度相同（最后一块可能小于长度）<br>\n * 分区是在原List的基础上进行的，返回的分区是不可变的抽象列表，原列表元素变更，分区中元素也会变更。\n * 参考：Guava的Lists#Partition\n *\n * @param <T> 元素类型\n * @author looly, guava\n * @since 5.7.10\n */\npublic class Partition<T> extends AbstractList<List<T>> {\n\n\tprotected final List<T> list;\n\tprotected final int size;\n\n\t/**\n\t * 列表分区\n\t *\n\t * @param list 被分区的列表，非空\n\t * @param size 每个分区的长度，必须&gt;0\n\t */\n\tpublic Partition(List<T> list, int size) {\n\t\tthis.list = Assert.notNull(list);\n\t\tthis.size = Math.min(list.size(), size);\n\t}\n\n\t@Override\n\tpublic List<T> get(int index) {\n\t\tfinal int start = index * size;\n\t\tfinal int end = Math.min(start + size, list.size());\n\t\treturn list.subList(start, end);\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\t// 此处采用动态计算，以应对list变\n\t\tfinal int size = this.size;\n\t\tif(0 == size){\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal int total = list.size();\n\t\t// 类似于判断余数，当总数非整份size时，多余的数>=1，则相当于被除数多一个size，做到+1目的\n\t\t// 类似于：if(total % size > 0){length += 1;}\n\t\treturn (total + size - 1) / size;\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn list.isEmpty();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/PartitionIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * 分批迭代工具，可以分批处理数据\n * <ol>\n *     <li>比如调用其他客户的接口，传入的入参有限，需要分批</li>\n *     <li>比如mysql/oracle用in语句查询，超过1000可以分批</li>\n *     <li>比如数据库取出游标，可以把游标里的数据一批一批处理</li>\n * </ol>\n *\n * @param <T> 字段类型\n * @author qiqi.chen\n * @since 5.7.10\n */\npublic class PartitionIter<T> implements IterableIter<List<T>>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 被分批的迭代器\n\t */\n\tprotected final Iterator<T> iterator;\n\t/**\n\t * 实际每批大小\n\t */\n\tprotected final int partitionSize;\n\n\t/**\n\t * 创建分组对象\n\t *\n\t * @param iterator      迭代器\n\t * @param partitionSize 每批大小，最后一批不满一批算一批\n\t */\n\tpublic PartitionIter(Iterator<T> iterator, int partitionSize) {\n\t\tthis.iterator = iterator;\n\t\tthis.partitionSize = partitionSize;\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn this.iterator.hasNext();\n\t}\n\n\t@Override\n\tpublic List<T> next() {\n\t\tfinal List<T> list = new ArrayList<>(this.partitionSize);\n\t\tfor (int i = 0; i < this.partitionSize; i++) {\n\t\t\tif (false == iterator.hasNext()) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlist.add(iterator.next());\n\t\t}\n\t\treturn list;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/RandomAccessAvgPartition.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.List;\nimport java.util.RandomAccess;\n\n/**\n * 列表分区或分段（可随机访问列表）<br>\n * 通过传入分区个数，将指定列表分区为不同的块，每块区域的长度均匀分布（个数差不超过1）<br>\n * <pre>\n *     [1,2,3,4] -》 [1,2], [3, 4]\n *     [1,2,3,4] -》 [1,2], [3], [4]\n *     [1,2,3,4] -》 [1], [2], [3], [4]\n *     [1,2,3,4] -》 [1], [2], [3], [4], []\n * </pre>\n * 分区是在原List的基础上进行的，返回的分区是不可变的抽象列表，原列表元素变更，分区中元素也会变更。\n *\n * @param <T> 元素类型\n * @author looly\n * @since 5.7.10\n */\npublic class RandomAccessAvgPartition<T> extends AvgPartition<T> implements RandomAccess {\n\n\t/**\n\t * 列表分区\n\t *\n\t * @param list  被分区的列表\n\t * @param limit 分区个数\n\t */\n\tpublic RandomAccessAvgPartition(List<T> list, int limit) {\n\t\tsuper(list, limit);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/RandomAccessPartition.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.List;\nimport java.util.RandomAccess;\n\n/**\n * 列表分区或分段（可随机访问列表）<br>\n * 通过传入分区长度，将指定列表分区为不同的块，每块区域的长度相同（最后一块可能小于长度）<br>\n * 分区是在原List的基础上进行的，返回的分区是不可变的抽象列表，原列表元素变更，分区中元素也会变更。\n * 参考：Guava的Lists#RandomAccessPartition\n *\n * @param <T> 元素类型\n * @author looly, guava\n * @since 5.7.10\n */\npublic class RandomAccessPartition<T> extends Partition<T> implements RandomAccess {\n\n\t/**\n\t * 构造\n\t *\n\t * @param list 被分区的列表，必须实现{@link RandomAccess}\n\t * @param size 每个分区的长度\n\t */\n\tpublic RandomAccessPartition(List<T> list, int size) {\n\t\tsuper(list, size);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/ResettableIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.Iterator;\n\n/**\n * 支持重置的{@link Iterator} 接口<br>\n * 通过实现{@link #reset()}，重置此{@link Iterator}后可实现复用重新遍历\n *\n * @param <E> 元素类型\n * @since 5.8.0\n */\npublic interface ResettableIter<E> extends Iterator<E> {\n\n\t/**\n\t * 重置，重置后可重新遍历\n\t */\n\tvoid reset();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/RingIndexUtil.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\n\n/**\n * 集合索引环形获取工具类\n *\n * @author ZhouChuGang\n * @since 5.7.15\n */\npublic class RingIndexUtil {\n\n\t/**\n\t * 通过cas操作 实现对指定值内的回环累加\n\t *\n\t * @param object        集合\n\t *                      <ul>\n\t *                      <li>Collection - the collection size\n\t *                      <li>Map - the map size\n\t *                      <li>Array - the array size\n\t *                      <li>Iterator - the number of elements remaining in the iterator\n\t *                      <li>Enumeration - the number of elements remaining in the enumeration\n\t *                      </ul>\n\t * @param atomicInteger 原子操作类\n\t * @return 索引位置\n\t */\n\tpublic static int ringNextIntByObj(Object object, AtomicInteger atomicInteger) {\n\t\tAssert.notNull(object);\n\t\tint modulo = CollUtil.size(object);\n\t\treturn ringNextInt(modulo, atomicInteger);\n\t}\n\n\t/**\n\t * 通过cas操作 实现对指定值内的回环累加\n\t *\n\t * @param modulo        回环周期值\n\t * @param atomicInteger 原子操作类\n\t * @return 索引位置\n\t */\n\tpublic static int ringNextInt(int modulo, AtomicInteger atomicInteger) {\n\t\tAssert.notNull(atomicInteger);\n\t\tAssert.isTrue(modulo > 0);\n\t\tif (modulo <= 1) {\n\t\t\treturn 0;\n\t\t}\n\t\tfor (; ; ) {\n\t\t\tint current = atomicInteger.get();\n\t\t\tint next = (current + 1) % modulo;\n\t\t\tif (atomicInteger.compareAndSet(current, next)) {\n\t\t\t\treturn next;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 通过cas操作 实现对指定值内的回环累加<br>\n\t * 此方法一般用于大量数据完成回环累加（如数据库中的值大于int最大值）\n\t *\n\t * @param modulo     回环周期值\n\t * @param atomicLong 原子操作类\n\t * @return 索引位置\n\t */\n\tpublic static long ringNextLong(long modulo, AtomicLong atomicLong) {\n\t\tAssert.notNull(atomicLong);\n\t\tAssert.isTrue(modulo > 0);\n\t\tif (modulo <= 1) {\n\t\t\treturn 0;\n\t\t}\n\t\tfor (; ; ) {\n\t\t\tlong current = atomicLong.get();\n\t\t\tlong next = (current + 1) % modulo;\n\t\t\tif (atomicLong.compareAndSet(current, next)) {\n\t\t\t\treturn next;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/SpliteratorUtil.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.Spliterator;\nimport java.util.function.Function;\n\n/**\n * {@link Spliterator}相关工具类\n *\n * @author looly\n * @since 5.4.3\n */\npublic class SpliteratorUtil {\n\n\t/**\n\t * 使用给定的转换函数，转换源{@link Spliterator}为新类型的{@link Spliterator}\n\t *\n\t * @param <F> 源元素类型\n\t * @param <T> 目标元素类型\n\t * @param fromSpliterator 源{@link Spliterator}\n\t * @param function 转换函数\n\t * @return 新类型的{@link Spliterator}\n\t */\n\tpublic static <F, T> Spliterator<T> trans(Spliterator<F> fromSpliterator, Function<? super F, ? extends T> function) {\n\t\treturn new TransSpliterator<>(fromSpliterator, function);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/TransCollection.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.AbstractCollection;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.Spliterator;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * 使用给定的转换函数，转换源集合为新类型的集合\n *\n * @param <F> 源元素类型\n * @param <T> 目标元素类型\n * @author looly\n * @since 5.4.3\n */\npublic class TransCollection<F, T> extends AbstractCollection<T> {\n\n\tprivate final Collection<F> fromCollection;\n\tprivate final Function<? super F, ? extends T> function;\n\n\t/**\n\t * 构造\n\t *\n\t * @param fromCollection 源集合\n\t * @param function       转换函数\n\t */\n\tpublic TransCollection(Collection<F> fromCollection, Function<? super F, ? extends T> function) {\n\t\tthis.fromCollection = Assert.notNull(fromCollection);\n\t\tthis.function = Assert.notNull(function);\n\t}\n\n\t@Override\n\tpublic Iterator<T> iterator() {\n\t\treturn IterUtil.trans(fromCollection.iterator(), function);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tfromCollection.clear();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn fromCollection.isEmpty();\n\t}\n\n\t@Override\n\tpublic void forEach(Consumer<? super T> action) {\n\t\tAssert.notNull(action);\n\t\tfromCollection.forEach((f) -> action.accept(function.apply(f)));\n\t}\n\n\t@Override\n\tpublic boolean removeIf(Predicate<? super T> filter) {\n\t\tAssert.notNull(filter);\n\t\treturn fromCollection.removeIf(element -> filter.test(function.apply(element)));\n\t}\n\n\t@Override\n\tpublic Spliterator<T> spliterator() {\n\t\treturn SpliteratorUtil.trans(fromCollection.spliterator(), function);\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn fromCollection.size();\n\t}\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/TransIter.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Iterator;\nimport java.util.function.Function;\n\n/**\n * 使用给定的转换函数，转换源{@link Iterator}为新类型的{@link Iterator}\n *\n * @param <F> 源元素类型\n * @param <T> 目标元素类型\n * @author looly\n * @since 5.4.3\n */\npublic class TransIter<F, T> implements Iterator<T> {\n\n\tprivate final Iterator<? extends F> backingIterator;\n\tprivate final Function<? super F, ? extends T> func;\n\n\t/**\n\t * 构造\n\t *\n\t * @param backingIterator 源{@link Iterator}\n\t * @param func            转换函数\n\t */\n\tpublic TransIter(final Iterator<? extends F> backingIterator, final Function<? super F, ? extends T> func) {\n\t\tthis.backingIterator = Assert.notNull(backingIterator);\n\t\tthis.func = Assert.notNull(func);\n\t}\n\n\t@Override\n\tpublic final boolean hasNext() {\n\t\treturn backingIterator.hasNext();\n\t}\n\n\t@Override\n\tpublic final T next() {\n\t\treturn func.apply(backingIterator.next());\n\t}\n\n\t@Override\n\tpublic final void remove() {\n\t\tbackingIterator.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/TransSpliterator.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.Spliterator;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * 使用给定的转换函数，转换源{@link Spliterator}为新类型的{@link Spliterator}\n *\n * @param <F> 源元素类型\n * @param <T> 目标元素类型\n * @author looly\n * @since 5.4.3\n */\npublic class TransSpliterator<F, T> implements Spliterator<T> {\n\tprivate final Spliterator<F> fromSpliterator;\n\tprivate final Function<? super F, ? extends T> function;\n\n\tpublic TransSpliterator(Spliterator<F> fromSpliterator, Function<? super F, ? extends T> function) {\n\t\tthis.fromSpliterator = fromSpliterator;\n\t\tthis.function = function;\n\t}\n\n\t@Override\n\tpublic boolean tryAdvance(Consumer<? super T> action) {\n\t\treturn fromSpliterator.tryAdvance(\n\t\t\t\tfromElement -> action.accept(function.apply(fromElement)));\n\t}\n\n\t@Override\n\tpublic void forEachRemaining(Consumer<? super T> action) {\n\t\tfromSpliterator.forEachRemaining(fromElement -> action.accept(function.apply(fromElement)));\n\t}\n\n\t@Override\n\tpublic Spliterator<T> trySplit() {\n\t\tSpliterator<F> fromSplit = fromSpliterator.trySplit();\n\t\treturn (fromSplit != null) ? new TransSpliterator<>(fromSplit, function) : null;\n\t}\n\n\t@Override\n\tpublic long estimateSize() {\n\t\treturn fromSpliterator.estimateSize();\n\t}\n\n\t@Override\n\tpublic int characteristics() {\n\t\treturn fromSpliterator.characteristics()\n\t\t\t\t& ~(Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.SORTED);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.map.MapBuilder;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Serializable;\nimport java.util.AbstractSet;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * 唯一键的Set<br>\n * 通过自定义唯一键，通过{@link #uniqueGenerator}生成节点对象对应的键作为Map的key，确定唯一<br>\n * 此Set与HashSet不同的是，HashSet依赖于{@link Object#equals(Object)}确定唯一<br>\n * 但是很多时候我们无法对对象进行修改，此时在外部定义一个唯一规则，即可完成去重。\n * <pre>\n * {@code Set<UniqueTestBean> set = new UniqueKeySet<>(UniqueTestBean::getId);}\n * </pre>\n *\n * @param <K> 唯一键类型\n * @param <V> 值对象\n * @author looly\n * @since 5.7.23\n */\npublic class UniqueKeySet<K, V> extends AbstractSet<V> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Map<K, V> map;\n\tprivate final Function<V, K> uniqueGenerator;\n\n\t//region 构造\n\n\t/**\n\t * 构造\n\t *\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t */\n\tpublic UniqueKeySet(Function<V, K> uniqueGenerator) {\n\t\tthis(false, uniqueGenerator);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t * @param c 初始化加入的集合\n\t * @since 5.8.0\n\t */\n\tpublic UniqueKeySet(Function<V, K> uniqueGenerator, Collection<? extends V> c) {\n\t\tthis(false, uniqueGenerator, c);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isLinked        是否保持加入顺序\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t */\n\tpublic UniqueKeySet(boolean isLinked, Function<V, K> uniqueGenerator) {\n\t\tthis(MapBuilder.create(isLinked), uniqueGenerator);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isLinked        是否保持加入顺序\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t * @param c 初始化加入的集合\n\t * @since 5.8.0\n\t */\n\tpublic UniqueKeySet(boolean isLinked, Function<V, K> uniqueGenerator, Collection<? extends V> c) {\n\t\tthis(isLinked, uniqueGenerator);\n\t\taddAll(c);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param loadFactor      增长因子\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t */\n\tpublic UniqueKeySet(int initialCapacity, float loadFactor, Function<V, K> uniqueGenerator) {\n\t\tthis(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)), uniqueGenerator);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param builder         初始Map，定义了Map类型\n\t * @param uniqueGenerator 唯一键生成规则函数，用于生成对象对应的唯一键\n\t */\n\tpublic UniqueKeySet(MapBuilder<K, V> builder, Function<V, K> uniqueGenerator) {\n\t\tthis.map = builder.build();\n\t\tthis.uniqueGenerator = uniqueGenerator;\n\t}\n\n\t//endregion\n\n\t@Override\n\tpublic Iterator<V> iterator() {\n\t\treturn map.values().iterator();\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn map.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn map.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean contains(Object o) {\n\t\t//noinspection unchecked\n\t\treturn map.containsKey(this.uniqueGenerator.apply((V) o));\n\t}\n\n\t@Override\n\tpublic boolean add(V v) {\n\t\treturn null == map.put(this.uniqueGenerator.apply(v), v);\n\t}\n\n\t/**\n\t * 加入值，如果值已经存在，则忽略之\n\t *\n\t * @param v 值\n\t * @return 是否成功加入\n\t */\n\tpublic boolean addIfAbsent(V v) {\n\t\treturn null == map.putIfAbsent(this.uniqueGenerator.apply(v), v);\n\t}\n\n\t/**\n\t * 加入集合中所有的值，如果值已经存在，则忽略之\n\t *\n\t * @param c 集合\n\t * @return 是否有一个或多个被加入成功\n\t */\n\tpublic boolean addAllIfAbsent(Collection<? extends V> c) {\n\t\tboolean modified = false;\n\t\tfor (V v : c)\n\t\t\tif (addIfAbsent(v)) {\n\t\t\t\tmodified = true;\n\t\t\t}\n\t\treturn modified;\n\t}\n\n\t@Override\n\tpublic boolean remove(Object o) {\n\t\t//noinspection unchecked\n\t\treturn null != map.remove(this.uniqueGenerator.apply((V) o));\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tmap.clear();\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic UniqueKeySet<K, V> clone() {\n\t\ttry {\n\t\t\tUniqueKeySet<K, V> newSet = (UniqueKeySet<K, V>) super.clone();\n\t\t\tnewSet.map = ObjectUtil.clone(this.map);\n\t\t\treturn newSet;\n\t\t} catch (CloneNotSupportedException e) {\n\t\t\tthrow new InternalError(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/collection/package-info.java",
    "content": "/**\n * 集合以及Iterator封装，包括集合工具CollUtil，Iterator和Iterable工具IterUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.collection;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/BaseFieldComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.util.Comparator;\n\n/**\n * Bean字段排序器<br>\n * 参阅feilong-core中的PropertyComparator\n *\n * @param <T> 被比较的Bean\n * @author jiangzeyin\n * @deprecated 此类不再需要，使用FuncComparator代替更加灵活\n */\n@Deprecated\npublic abstract class BaseFieldComparator<T> implements Comparator<T>, Serializable {\n\tprivate static final long serialVersionUID = -3482464782340308755L;\n\n\t/**\n\t * 比较两个对象的同一个字段值\n\t *\n\t * @param o1    对象1\n\t * @param o2    对象2\n\t * @param field 字段\n\t * @return 比较结果\n\t */\n\tprotected int compareItem(T o1, T o2, Field field) {\n\t\tif (o1 == o2) {\n\t\t\treturn 0;\n\t\t} else if (null == o1) {// null 排在后面\n\t\t\treturn 1;\n\t\t} else if (null == o2) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tComparable<?> v1;\n\t\tComparable<?> v2;\n\t\ttry {\n\t\t\tv1 = (Comparable<?>) ReflectUtil.getFieldValue(o1, field);\n\t\t\tv2 = (Comparable<?>) ReflectUtil.getFieldValue(o2, field);\n\t\t} catch (Exception e) {\n\t\t\tthrow new ComparatorException(e);\n\t\t}\n\n\t\treturn compare(o1, o2, v1, v2);\n\t}\n\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tprivate int compare(T o1, T o2, Comparable fieldValue1, Comparable fieldValue2) {\n\t\tint result = ObjectUtil.compare(fieldValue1, fieldValue2);\n\t\tif (0 == result) {\n\t\t\t//避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况\n\t\t\tresult = CompareUtil.compare(o1, o2, true);\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/ComparableComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\n\n/**\n * 针对 {@link Comparable}对象的默认比较器\n *\n * @param <E> 比较对象类型\n * @author Looly\n * @since 3.0.7\n */\npublic class ComparableComparator<E extends Comparable<? super E>> implements Comparator<E>, Serializable {\n\tprivate static final long serialVersionUID = 3020871676147289162L;\n\n\t/** 单例 */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static final ComparableComparator INSTANCE = new ComparableComparator<>();\n\n\t/**\n\t * 构造\n\t */\n\tpublic ComparableComparator() {\n\t}\n\n\t/**\n\t * 比较两个{@link Comparable}对象\n\t *\n\t * <pre>\n\t * obj1.compareTo(obj2)\n\t * </pre>\n\t *\n\t * @param obj1 被比较的第一个对象\n\t * @param obj2 the second object to compare\n\t * @return obj1小返回负数，大返回正数，否则返回0\n\t * @throws NullPointerException obj1为{@code null}或者比较中抛出空指针异常\n\t */\n\t@Override\n\tpublic int compare(final E obj1, final E obj2) {\n\t\treturn obj1.compareTo(obj2);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn \"ComparableComparator\".hashCode();\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object object) {\n\t\treturn this == object || null != object && object.getClass().equals(this.getClass());\n\t}\n\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorChain.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Chain;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.BitSet;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * 比较器链。此链包装了多个比较器，最终比较结果按照比较器顺序综合多个比较器结果。<br>\n * 按照比较器链的顺序分别比较，如果比较出相等则转向下一个比较器，否则直接返回<br>\n * 此类copy from Apache-commons-collections\n *\n * @author looly\n * @since 3.0.7\n */\npublic class ComparatorChain<E> implements Chain<Comparator<E>, ComparatorChain<E>>, Comparator<E>, Serializable {\n\tprivate static final long serialVersionUID = -2426725788913962429L;\n\n\t/**\n\t * 比较器链.\n\t */\n\tprivate final List<Comparator<E>> chain;\n\t/**\n\t * 对应比较器位置是否反序.\n\t */\n\tprivate final BitSet orderingBits;\n\t/**\n\t * 比较器是否被锁定。锁定的比较器链不能再添加新的比较器。比较器会在开始比较时开始加锁。\n\t */\n\tprivate boolean lock = false;\n\n\t//------------------------------------------------------------------------------------- Static method start\n\n\t/**\n\t * 构建 {@link ComparatorChain}\n\t *\n\t * @param <E>        被比较对象类型\n\t * @param comparator 比较器\n\t * @return {@link ComparatorChain}\n\t * @since 5.4.3\n\t */\n\tpublic static <E> ComparatorChain<E> of(Comparator<E> comparator) {\n\t\treturn of(comparator, false);\n\t}\n\n\t/**\n\t * 构建 {@link ComparatorChain}\n\t *\n\t * @param <E>        被比较对象类型\n\t * @param comparator 比较器\n\t * @param reverse    是否反向\n\t * @return {@link ComparatorChain}\n\t * @since 5.4.3\n\t */\n\tpublic static <E> ComparatorChain<E> of(Comparator<E> comparator, boolean reverse) {\n\t\treturn new ComparatorChain<>(comparator, reverse);\n\t}\n\n\t/**\n\t * 构建 {@link ComparatorChain}\n\t *\n\t * @param <E>         被比较对象类型\n\t * @param comparators 比较器数组\n\t * @return {@link ComparatorChain}\n\t * @since 5.4.3\n\t */\n\t@SafeVarargs\n\tpublic static <E> ComparatorChain<E> of(Comparator<E>... comparators) {\n\t\treturn of(Arrays.asList(comparators));\n\t}\n\n\t/**\n\t * 构建 {@link ComparatorChain}\n\t *\n\t * @param <E>         被比较对象类型\n\t * @param comparators 比较器列表\n\t * @return {@link ComparatorChain}\n\t * @since 5.4.3\n\t */\n\tpublic static <E> ComparatorChain<E> of(List<Comparator<E>> comparators) {\n\t\treturn new ComparatorChain<>(comparators);\n\t}\n\n\t/**\n\t * 构建 {@link ComparatorChain}\n\t *\n\t * @param <E>         被比较对象类型\n\t * @param comparators 比较器列表\n\t * @param bits        {@link Comparator} 列表对应的排序boolean值，true表示正序，false反序\n\t * @return {@link ComparatorChain}\n\t * @since 5.4.3\n\t */\n\tpublic static <E> ComparatorChain<E> of(List<Comparator<E>> comparators, BitSet bits) {\n\t\treturn new ComparatorChain<>(comparators, bits);\n\t}\n\t//------------------------------------------------------------------------------------- Static method start\n\n\t/**\n\t * 构造空的比较器链，必须至少有一个比较器，否则会在compare时抛出{@link UnsupportedOperationException}\n\t */\n\tpublic ComparatorChain() {\n\t\tthis(new ArrayList<>(), new BitSet());\n\t}\n\n\t/**\n\t * 构造，初始化单一比较器。比较器为正序\n\t *\n\t * @param comparator 在比较器链中的第一个比较器\n\t */\n\tpublic ComparatorChain(final Comparator<E> comparator) {\n\t\tthis(comparator, false);\n\t}\n\n\t/**\n\t * 构造，初始化单一比较器。自定义正序还是反序\n\t *\n\t * @param comparator 在比较器链中的第一个比较器\n\t * @param reverse    是否反序，true表示反序，false正序\n\t */\n\tpublic ComparatorChain(final Comparator<E> comparator, final boolean reverse) {\n\t\tchain = new ArrayList<>(1);\n\t\tchain.add(comparator);\n\t\torderingBits = new BitSet(1);\n\t\tif (reverse == true) {\n\t\t\torderingBits.set(0);\n\t\t}\n\t}\n\n\t/**\n\t * 构造，使用已有的比较器列表\n\t *\n\t * @param list 比较器列表\n\t * @see #ComparatorChain(List, BitSet)\n\t */\n\tpublic ComparatorChain(final List<Comparator<E>> list) {\n\t\tthis(list, new BitSet(list.size()));\n\t}\n\n\t/**\n\t * 构造，使用已有的比较器列表和对应的BitSet<br>\n\t * BitSet中的boolean值需与list中的{@link Comparator}一一对应，true表示正序，false反序\n\t *\n\t * @param list {@link Comparator} 列表\n\t * @param bits {@link Comparator} 列表对应的排序boolean值，true表示正序，false反序\n\t */\n\tpublic ComparatorChain(final List<Comparator<E>> list, final BitSet bits) {\n\t\tchain = list;\n\t\torderingBits = bits;\n\t}\n\n\t/**\n\t * 在链的尾部添加比较器，使用正向排序\n\t *\n\t * @param comparator {@link Comparator} 比较器，正向\n\t * @return this\n\t */\n\tpublic ComparatorChain<E> addComparator(final Comparator<E> comparator) {\n\t\treturn addComparator(comparator, false);\n\t}\n\n\t/**\n\t * 在链的尾部添加比较器，使用给定排序方式\n\t *\n\t * @param comparator {@link Comparator} 比较器\n\t * @param reverse    是否反序，true表示正序，false反序\n\t * @return this\n\t */\n\tpublic ComparatorChain<E> addComparator(final Comparator<E> comparator, final boolean reverse) {\n\t\tcheckLocked();\n\n\t\tchain.add(comparator);\n\t\tif (reverse == true) {\n\t\t\torderingBits.set(chain.size() - 1);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 替换指定位置的比较器，保持原排序方式\n\t *\n\t * @param index      位置\n\t * @param comparator {@link Comparator}\n\t * @return this\n\t * @throws IndexOutOfBoundsException if index &lt; 0 or index &gt;= size()\n\t */\n\tpublic ComparatorChain<E> setComparator(final int index, final Comparator<E> comparator) throws IndexOutOfBoundsException {\n\t\treturn setComparator(index, comparator, false);\n\t}\n\n\t/**\n\t * 替换指定位置的比较器，替换指定排序方式\n\t *\n\t * @param index      位置\n\t * @param comparator {@link Comparator}\n\t * @param reverse    是否反序，true表示正序，false反序\n\t * @return this\n\t */\n\tpublic ComparatorChain<E> setComparator(final int index, final Comparator<E> comparator, final boolean reverse) {\n\t\tcheckLocked();\n\n\t\tchain.set(index, comparator);\n\t\tif (reverse == true) {\n\t\t\torderingBits.set(index);\n\t\t} else {\n\t\t\torderingBits.clear(index);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 更改指定位置的排序方式为正序\n\t *\n\t * @param index 位置\n\t * @return this\n\t */\n\tpublic ComparatorChain<E> setForwardSort(final int index) {\n\t\tcheckLocked();\n\t\torderingBits.clear(index);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 更改指定位置的排序方式为反序\n\t *\n\t * @param index 位置\n\t * @return this\n\t */\n\tpublic ComparatorChain<E> setReverseSort(final int index) {\n\t\tcheckLocked();\n\t\torderingBits.set(index);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 比较器链中比较器个数\n\t *\n\t * @return Comparator count\n\t */\n\tpublic int size() {\n\t\treturn chain.size();\n\t}\n\n\t/**\n\t * 是否已经被锁定。当开始比较时（调用compare方法）此值为true\n\t *\n\t * @return true = ComparatorChain cannot be modified; false = ComparatorChain can still be modified.\n\t */\n\tpublic boolean isLocked() {\n\t\treturn lock;\n\t}\n\n\t@Override\n\tpublic Iterator<Comparator<E>> iterator() {\n\t\treturn this.chain.iterator();\n\t}\n\n\t@Override\n\tpublic ComparatorChain<E> addChain(Comparator<E> element) {\n\t\treturn this.addComparator(element);\n\t}\n\n\t/**\n\t * 执行比较<br>\n\t * 按照比较器链的顺序分别比较，如果比较出相等则转向下一个比较器，否则直接返回\n\t *\n\t * @param o1 第一个对象\n\t * @param o2 第二个对象\n\t * @return -1, 0, or 1\n\t * @throws UnsupportedOperationException 如果比较器链为空，无法完成比较\n\t */\n\t@Override\n\tpublic int compare(final E o1, final E o2) throws UnsupportedOperationException {\n\t\tif (lock == false) {\n\t\t\tcheckChainIntegrity();\n\t\t\tlock = true;\n\t\t}\n\n\t\tfinal Iterator<Comparator<E>> comparators = chain.iterator();\n\t\tComparator<? super E> comparator;\n\t\tint retval;\n\t\tfor (int comparatorIndex = 0; comparators.hasNext(); ++comparatorIndex) {\n\t\t\tcomparator = comparators.next();\n\t\t\tretval = comparator.compare(o1, o2);\n\t\t\tif (retval != 0) {\n\t\t\t\t// invert the order if it is a reverse sort\n\t\t\t\tif (true == orderingBits.get(comparatorIndex)) {\n\t\t\t\t\tretval = (retval > 0) ? -1 : 1;\n\t\t\t\t}\n\t\t\t\treturn retval;\n\t\t\t}\n\t\t}\n\n\t\t// if comparators are exhausted, return 0\n\t\treturn 0;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tint hash = 0;\n\t\tif (null != chain) {\n\t\t\thash ^= chain.hashCode();\n\t\t}\n\t\tif (null != orderingBits) {\n\t\t\thash ^= orderingBits.hashCode();\n\t\t}\n\t\treturn hash;\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object object) {\n\t\tif (this == object) {\n\t\t\treturn true;\n\t\t}\n\t\tif (null == object) {\n\t\t\treturn false;\n\t\t}\n\t\tif (object.getClass().equals(this.getClass())) {\n\t\t\tfinal ComparatorChain<?> otherChain = (ComparatorChain<?>) object;\n\t\t\t//\n\t\t\treturn Objects.equals(this.orderingBits, otherChain.orderingBits)\n\t\t\t\t\t&& this.chain.equals(otherChain.chain);\n\t\t}\n\t\treturn false;\n\t}\n\n\t//------------------------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 被锁定时抛出异常\n\t *\n\t * @throws UnsupportedOperationException 被锁定抛出此异常\n\t */\n\tprivate void checkLocked() {\n\t\tif (lock == true) {\n\t\t\tthrow new UnsupportedOperationException(\"Comparator ordering cannot be changed after the first comparison is performed\");\n\t\t}\n\t}\n\n\t/**\n\t * 检查比较器链是否为空，为空抛出异常\n\t *\n\t * @throws UnsupportedOperationException 为空抛出此异常\n\t */\n\tprivate void checkChainIntegrity() {\n\t\tif (chain.size() == 0) {\n\t\t\tthrow new UnsupportedOperationException(\"ComparatorChains must contain at least one Comparator\");\n\t\t}\n\t}\n\t//------------------------------------------------------------------------------------------------------------------------------- Private method start\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorException.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 比较异常\n * @author xiaoleilu\n */\npublic class ComparatorException extends RuntimeException{\n\tprivate static final long serialVersionUID = 4475602435485521971L;\n\n\tpublic ComparatorException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic ComparatorException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic ComparatorException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ComparatorException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic ComparatorException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/CompareUtil.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.util.Comparator;\nimport java.util.Objects;\nimport java.util.function.Function;\n\n/**\n * 比较工具类\n *\n * @author looly\n */\npublic class CompareUtil {\n\n\t/**\n\t * 获取自然排序器，即默认排序器\n\t *\n\t * @param <E> 排序节点类型\n\t * @return 默认排序器\n\t * @since 5.7.21\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <E extends Comparable<? super E>> Comparator<E> naturalComparator() {\n\t\treturn ComparableComparator.INSTANCE;\n\t}\n\n\t/**\n\t * 对象比较，比较结果取决于comparator，如果被比较对象为null，传入的comparator对象应处理此情况<br>\n\t * 如果传入comparator为null，则使用默认规则比较（此时被比较对象必须实现Comparable接口）\n\t *\n\t * <p>\n\t * 一般而言，如果c1 &lt; c2，返回数小于0，c1==c2返回0，c1 &gt; c2 大于0\n\t *\n\t * @param <T>        被比较对象类型\n\t * @param c1         对象1\n\t * @param c2         对象2\n\t * @param comparator 比较器\n\t * @return 比较结果\n\t * @see java.util.Comparator#compare(Object, Object)\n\t * @since 4.6.9\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic static <T> int compare(T c1, T c2, Comparator<T> comparator) {\n\t\tif (null == comparator) {\n\t\t\treturn compare((Comparable) c1, (Comparable) c2);\n\t\t}\n\t\treturn comparator.compare(c1, c2);\n\t}\n\n\t/**\n\t * {@code null}安全的对象比较，{@code null}对象小于任何对象\n\t *\n\t * @param <T> 被比较对象类型\n\t * @param c1  对象1，可以为{@code null}\n\t * @param c2  对象2，可以为{@code null}\n\t * @return 比较结果，如果c1 &lt; c2，返回数小于0，c1==c2返回0，c1 &gt; c2 大于0\n\t * @see java.util.Comparator#compare(Object, Object)\n\t */\n\tpublic static <T extends Comparable<? super T>> int compare(T c1, T c2) {\n\t\treturn compare(c1, c2, false);\n\t}\n\n\t/**\n\t * {@code null}安全的对象比较\n\t *\n\t * @param <T>           被比较对象类型（必须实现Comparable接口）\n\t * @param c1            对象1，可以为{@code null}\n\t * @param c2            对象2，可以为{@code null}\n\t * @param isNullGreater 当被比较对象为null时是否排在后面，true表示null大于任何对象，false反之\n\t * @return 比较结果，如果c1 &lt; c2，返回数小于0，c1==c2返回0，c1 &gt; c2 大于0\n\t * @see java.util.Comparator#compare(Object, Object)\n\t */\n\tpublic static <T extends Comparable<? super T>> int compare(T c1, T c2, boolean isNullGreater) {\n\t\tif (c1 == c2) {\n\t\t\treturn 0;\n\t\t} else if (c1 == null) {\n\t\t\treturn isNullGreater ? 1 : -1;\n\t\t} else if (c2 == null) {\n\t\t\treturn isNullGreater ? -1 : 1;\n\t\t}\n\t\treturn c1.compareTo(c2);\n\t}\n\n\t/**\n\t * 自然比较两个对象的大小，比较规则如下：\n\t *\n\t * <pre>\n\t * 1、如果实现Comparable调用compareTo比较\n\t * 2、o1.equals(o2)返回0\n\t * 3、比较hashCode值\n\t * 4、比较toString值\n\t * </pre>\n\t *\n\t * @param <T>           被比较对象类型\n\t * @param o1            对象1\n\t * @param o2            对象2\n\t * @param isNullGreater null值是否做为最大值\n\t * @return 比较结果，如果o1 &lt; o2，返回数小于0，o1==o2返回0，o1 &gt; o2 大于0\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T> int compare(T o1, T o2, boolean isNullGreater) {\n\t\tif (o1 == o2) {\n\t\t\treturn 0;\n\t\t} else if (null == o1) {// null 排在后面\n\t\t\treturn isNullGreater ? 1 : -1;\n\t\t} else if (null == o2) {\n\t\t\treturn isNullGreater ? -1 : 1;\n\t\t}\n\n\t\tif (o1 instanceof Comparable && o2 instanceof Comparable) {\n\t\t\t//如果bean可比较，直接比较bean\n\t\t\treturn ((Comparable) o1).compareTo(o2);\n\t\t}\n\n\t\tif (o1.equals(o2)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tint result = Integer.compare(o1.hashCode(), o2.hashCode());\n\t\tif (0 == result) {\n\t\t\tresult = compare(o1.toString(), o2.toString());\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 中文比较器\n\t *\n\t * @param keyExtractor 从对象中提取中文(参与比较的内容)\n\t * @param <T>          对象类型\n\t * @return 中文比较器\n\t * @since 5.4.3\n\t */\n\tpublic static <T> Comparator<T> comparingPinyin(Function<T, String> keyExtractor) {\n\t\treturn comparingPinyin(keyExtractor, false);\n\t}\n\n\t/**\n\t * 中文（拼音）比较器\n\t *\n\t * @param keyExtractor 从对象中提取中文(参与比较的内容)\n\t * @param reverse      是否反序\n\t * @param <T>          对象类型\n\t * @return 中文比较器\n\t * @since 5.4.3\n\t */\n\tpublic static <T> Comparator<T> comparingPinyin(Function<T, String> keyExtractor, boolean reverse) {\n\t\tObjects.requireNonNull(keyExtractor);\n\t\tPinyinComparator pinyinComparator = new PinyinComparator();\n\t\tif (reverse) {\n\t\t\treturn (o1, o2) -> pinyinComparator.compare(keyExtractor.apply(o2), keyExtractor.apply(o1));\n\t\t}\n\t\treturn (o1, o2) -> pinyinComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2));\n\t}\n\n\t/**\n\t * 索引比较器<br>\n\t * 通过keyExtractor函数，提取对象的某个属性或规则，根据提供的排序数组，完成比较<br>\n\t *\n\t * @param keyExtractor 从对象中提取中文(参与比较的内容)\n\t * @param objs         参与排序的数组，数组的元素位置决定了对象的排序先后\n\t * @param <T>          对象类型\n\t * @param <U>          数组对象类型\n\t * @return 索引比较器\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T, U> Comparator<T> comparingIndexed(Function<? super T, ? extends U> keyExtractor, U... objs) {\n\t\treturn comparingIndexed(keyExtractor, false, objs);\n\t}\n\n\t/**\n\t * 索引比较器<br>\n\t * 通过keyExtractor函数，提取对象的某个属性或规则，根据提供的排序数组，完成比较<br>\n\t *\n\t * @param keyExtractor 从对象中提取排序键的函数(参与比较的内容)\n\t * @param atEndIfMiss  如果不在列表中是否排在后边\n\t * @param objs         参与排序的数组，数组的元素位置决定了对象的排序先后\n\t * @param <T>          对象类型\n\t * @param <U>          数组对象类型\n\t * @return 索引比较器\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T, U> Comparator<T> comparingIndexed(Function<? super T, ? extends U> keyExtractor, boolean atEndIfMiss, U... objs) {\n\t\tObjects.requireNonNull(keyExtractor);\n\t\tIndexedComparator<U> indexedComparator = new IndexedComparator<>(atEndIfMiss, objs);\n\t\treturn (o1, o2) -> indexedComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.lang.reflect.Field;\n\n/**\n * Bean字段排序器<br>\n * 参阅feilong-core中的PropertyComparator\n *\n * @param <T> 被比较的Bean\n * @author Looly\n */\npublic class FieldComparator<T> extends FuncComparator<T> {\n\tprivate static final long serialVersionUID = 9157326766723846313L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanClass Bean类\n\t * @param fieldName 字段名\n\t */\n\tpublic FieldComparator(Class<T> beanClass, String fieldName) {\n\t\tthis(getNonNullField(beanClass, fieldName));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param field 字段\n\t */\n\tpublic FieldComparator(Field field) {\n\t\tthis(true, true, field);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param nullGreater 是否{@code null}在后\n\t * @param compareSelf 在字段值相同情况下，是否比较对象本身。\n\t *                    如果此项为{@code false}，字段值比较后为0会导致对象被认为相同，可能导致被去重。\n\t * @param field       字段\n\t */\n\tpublic FieldComparator(boolean nullGreater, boolean compareSelf, Field field) {\n\t\tsuper(nullGreater, compareSelf, (bean) ->\n\t\t\t(Comparable<?>) ReflectUtil.getFieldValue(bean,\n\t\t\t\tAssert.notNull(field, \"Field must be not null!\")));\n\t}\n\n\t/**\n\t * 获取字段，附带检查字段不存在的问题。\n\t *\n\t * @param beanClass Bean类\n\t * @param fieldName 字段名\n\t * @return 非null字段\n\t */\n\tprivate static Field getNonNullField(Class<?> beanClass, String fieldName) {\n\t\tfinal Field field = ClassUtil.getDeclaredField(beanClass, fieldName);\n\t\tif (field == null) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Field [{}] not found in Class [{}]\", fieldName, beanClass.getName()));\n\t\t}\n\t\treturn field;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/FieldsComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassUtil;\n\nimport java.lang.reflect.Field;\n\n/**\n * Bean字段排序器<br>\n * 参阅feilong-core中的PropertyComparator\n *\n * @param <T> 被比较的Bean\n * @author Looly\n */\npublic class FieldsComparator<T> extends NullComparator<T> {\n\tprivate static final long serialVersionUID = 8649196282886500803L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanClass  Bean类\n\t * @param fieldNames 多个字段名\n\t */\n\tpublic FieldsComparator(Class<T> beanClass, String... fieldNames) {\n\t\tthis(true, beanClass, fieldNames);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param nullGreater 是否{@code null}在后\n\t * @param beanClass   Bean类\n\t * @param fieldNames  多个字段名\n\t */\n\tpublic FieldsComparator(boolean nullGreater, Class<T> beanClass, String... fieldNames) {\n\t\tsuper(nullGreater, (a, b) -> {\n\t\t\tField field;\n\t\t\tfor (String fieldName : fieldNames) {\n\t\t\t\tfield = ClassUtil.getDeclaredField(beanClass, fieldName);\n\t\t\t\tAssert.notNull(field, \"Field [{}] not found in Class [{}]\", fieldName, beanClass.getName());\n\t\t\t\t// issue#3259，多个字段比较时，允许字段值重复\n\t\t\t\tfinal int compare = new FieldComparator<>(true, false, field).compare(a, b);\n\t\t\t\tif (0 != compare) {\n\t\t\t\t\treturn compare;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 0;\n\t\t});\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/FuncComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.util.function.Function;\n\n/**\n * 指定函数排序器\n *\n * @param <T> 被比较的对象\n * @author looly\n */\npublic class FuncComparator<T> extends NullComparator<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param nullGreater 是否{@code null}在后\n\t * @param func        比较项获取函数，此函数根据传入的一个对象，生成对应的可比较对象，然后根据这个返回值比较\n\t */\n\tpublic FuncComparator(boolean nullGreater, Function<T, Comparable<?>> func) {\n\t\tthis(nullGreater, true, func);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param nullGreater 是否{@code null}在后\n\t * @param compareSelf 在字段值相同情况下，是否比较对象本身。\n\t *                    如果此项为{@code false}，字段值比较后为0会导致对象被认为相同，可能导致被去重。\n\t * @param func        比较项获取函数\n\t */\n\tpublic FuncComparator(final boolean nullGreater, final boolean compareSelf, final Function<T, Comparable<?>> func) {\n\t\tsuper(nullGreater, (a, b)->{\n\t\t\t// 通过给定函数转换对象为指定规则的可比较对象\n\t\t\tfinal Comparable<?> v1;\n\t\t\tfinal Comparable<?> v2;\n\t\t\ttry {\n\t\t\t\tv1 = func.apply(a);\n\t\t\t\tv2 = func.apply(b);\n\t\t\t} catch (final Exception e) {\n\t\t\t\tthrow new ComparatorException(e);\n\t\t\t}\n\n\t\t\t// 首先比较用户自定义的转换结果，如果为0，根据compareSelf参数决定是否比较对象本身。\n\t\t\t// compareSelf为false时，主要用于多规则比较，比如多字段比较的情况\n\t\t\tint result = CompareUtil.compare(v1, v2, nullGreater);\n\t\t\tif (compareSelf && 0 == result) {\n\t\t\t\t//避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况\n\t\t\t\tresult = CompareUtil.compare(a, b, nullGreater);\n\t\t\t}\n\t\t\treturn result;\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 按照数组的顺序正序排列，数组的元素位置决定了对象的排序先后<br>\n * 默认的，如果参与排序的元素并不在数组中，则排序在前（可以通过atEndIfMiss设置)\n *\n * @param <T> 被排序元素类型\n * @author looly\n * @since 4.1.5\n */\npublic class IndexedComparator<T> implements Comparator<T> {\n\n\tprivate final boolean atEndIfMiss;\n\t/**\n\t * map存储对象类型所在列表的位置,k为对象，v为位置\n\t */\n\tprivate final Map<? super T, Integer> map;\n\n\t/**\n\t * 构造\n\t *\n\t * @param objs 参与排序的数组，数组的元素位置决定了对象的排序先后\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic IndexedComparator(T... objs) {\n\t\tthis(false, objs);\n\t}\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param atEndIfMiss 如果不在列表中是否排在后边\n\t * @param map         参与排序的map，map中的value值大小决定了对象的排序先后\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate IndexedComparator(boolean atEndIfMiss, Map<? super T, Integer> map) {\n\t\tthis.atEndIfMiss = atEndIfMiss;\n\t\tthis.map = map;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param atEndIfMiss 如果不在列表中是否排在后边\n\t * @param objs        参与排序的数组，数组的元素位置决定了对象的排序先后\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic IndexedComparator(boolean atEndIfMiss, T... objs) {\n\t\tAssert.notNull(objs, \"'objs' array must not be null\");\n\t\tthis.atEndIfMiss = atEndIfMiss;\n\t\tmap = new HashMap<>(objs.length, 1);\n\t\tfor (int i = 0; i < objs.length; i++) {\n\t\t\tmap.put(objs[i], i);\n\t\t}\n\t}\n\n\t@Override\n\tpublic int compare(T o1, T o2) {\n\t\tfinal int index1 = getOrder(o1);\n\t\tfinal int index2 = getOrder(o2);\n\n\t\tif (index1 == index2) {\n\t\t\tif (index1 < 0 || index1 == this.map.size()) {\n\t\t\t\t// 任意一个元素不在map中, 返回原顺序\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// 位置一样，认为是同一个元素\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn Integer.compare(index1, index2);\n\t}\n\n\t/**\n\t * 查找对象类型所对应的顺序值,即在原列表中的顺序\n\t *\n\t * @param object 对象\n\t * @return 位置，未找到位置根据{@link #atEndIfMiss}取不同值，false返回-1，否则返回map长度\n\t */\n\tprivate int getOrder(T object) {\n\t\tInteger order = map.get(object);\n\t\tif (order == null) {\n\t\t\torder = this.atEndIfMiss ? this.map.size() : -1;\n\t\t}\n\t\treturn order;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/InstanceComparator.java",
    "content": "/*\n * Copyright 2002-2019 the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Comparator;\n\n/**\n * 按照指定类型顺序排序，对象顺序取决于对象对应的类在数组中的位置。\n *\n * <p>如果对比的两个对象类型相同，返回{@code 0}，默认如果对象类型不在列表中，则排序在前</p>\n * <p>此类来自Spring，有所改造</p>\n *\n * @param <T> 用于比较的对象类型\n * @author Phillip Webb\n * @since 5.4.1\n */\npublic class InstanceComparator<T> implements Comparator<T> {\n\n\tprivate final boolean atEndIfMiss;\n\tprivate final Class<?>[] instanceOrder;\n\n\t/**\n\t * 构造\n\t *\n\t * @param instanceOrder 用于比较排序的对象类型数组，排序按照数组位置排序\n\t */\n\tpublic InstanceComparator(Class<?>... instanceOrder) {\n\t\tthis(false, instanceOrder);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param atEndIfMiss   如果不在列表中是否排在后边\n\t * @param instanceOrder 用于比较排序的对象类型数组，排序按照数组位置排序\n\t */\n\tpublic InstanceComparator(boolean atEndIfMiss, Class<?>... instanceOrder) {\n\t\tAssert.notNull(instanceOrder, \"'instanceOrder' array must not be null\");\n\t\tthis.atEndIfMiss = atEndIfMiss;\n\t\tthis.instanceOrder = instanceOrder;\n\t}\n\n\n\t@Override\n\tpublic int compare(T o1, T o2) {\n\t\tint i1 = getOrder(o1);\n\t\tint i2 = getOrder(o2);\n\t\treturn Integer.compare(i1, i2);\n\t}\n\n\t/**\n\t * 查找对象类型所在列表的位置\n\t *\n\t * @param object 对象\n\t * @return 位置，未找到位置根据{@link #atEndIfMiss}取不同值，false返回-1，否则返回列表长度\n\t */\n\tprivate int getOrder(T object) {\n\t\tif (object != null) {\n\t\t\tfor (int i = 0; i < this.instanceOrder.length; i++) {\n\t\t\t\tif (this.instanceOrder[i].isInstance(object)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this.atEndIfMiss ? this.instanceOrder.length : -1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/LengthComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.util.Comparator;\n\n/**\n * 字符串长度比较器，短在前\n *\n * @author looly\n * @since 5.8.9\n */\npublic class LengthComparator implements Comparator<CharSequence> {\n\t/**\n\t * 单例的字符串长度比较器，短在前\n\t */\n\tpublic static final LengthComparator INSTANCE = new LengthComparator();\n\n\t@Override\n\tpublic int compare(CharSequence o1, CharSequence o2) {\n\t\tint result = Integer.compare(o1.length(), o2.length());\n\t\tif (0 == result) {\n\t\t\tresult = CompareUtil.compare(o1.toString(), o2.toString());\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/NullComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\nimport java.util.Objects;\n\n/**\n * {@code null}友好的比较器包装，如果nullGreater，则{@code null} &gt; non-null，否则反之。<br>\n * 如果二者皆为{@code null}，则为相等，返回0。<br>\n * 如果二者都非{@code null}，则使用传入的比较器排序。<br>\n * 传入比较器为{@code null}，则看被比较的两个对象是否都实现了{@link Comparable}实现则调用{@link Comparable#compareTo(Object)}。\n * 如果两者至少一个未实现，则视为所有元素相等。\n *\n * @param <T> 被比较的对象\n * @author looly\n * @since 5.7.10\n */\npublic class NullComparator<T> implements Comparator<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected final boolean nullGreater;\n\tprotected final Comparator<T> comparator;\n\n\t/**\n\t * 构造\n\t * @param nullGreater 是否{@code null}最大，排在最后\n\t * @param comparator 实际比较器\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic NullComparator(boolean nullGreater, Comparator<? super T> comparator) {\n\t\tthis.nullGreater = nullGreater;\n\t\tthis.comparator = (Comparator<T>) comparator;\n\t}\n\n\t@Override\n\tpublic int compare(T a, T b) {\n\t\tif (a == b) {\n\t\t\treturn 0;\n\t\t}if (a == null) {\n\t\t\treturn nullGreater ? 1 : -1;\n\t\t} else if (b == null) {\n\t\t\treturn nullGreater ? -1 : 1;\n\t\t} else {\n\t\t\treturn doCompare(a, b);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Comparator<T> thenComparing(Comparator<? super T> other) {\n\t\tObjects.requireNonNull(other);\n\t\treturn new NullComparator<>(nullGreater, comparator == null ? other : comparator.thenComparing(other));\n\t}\n\n\t/**\n\t * 不检查{@code null}的比较方法<br>\n\t * 用户可自行重写此方法自定义比较方式\n\t *\n\t * @param a A值\n\t * @param b B值\n\t * @return 比较结果，-1:a小于b，0:相等，1:a大于b\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tprotected int doCompare(T a, T b) {\n\t\tif (null == comparator) {\n\t\t\tif (a instanceof Comparable && b instanceof Comparable) {\n\t\t\t\treturn ((Comparable) a).compareTo(b);\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn comparator.compare(a, b);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/PinyinComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.io.Serializable;\nimport java.text.Collator;\nimport java.util.Comparator;\nimport java.util.Locale;\n\n/**\n * 按照GBK拼音顺序对给定的汉字字符串排序\n *\n * @author looly\n * @since 4.0.8\n */\npublic class PinyinComparator implements Comparator<String>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tfinal Collator collator;\n\n\t/**\n\t * 构造\n\t */\n\tpublic PinyinComparator() {\n\t\tcollator = Collator.getInstance(Locale.CHINESE);\n\t}\n\n\t@Override\n\tpublic int compare(String o1, String o2) {\n\t\treturn collator.compare(o1, o2);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.bean.BeanUtil;\n\n/**\n * Bean属性排序器<br>\n * 支持读取Bean多层次下的属性\n *\n * @param <T> 被比较的Bean\n * @author Looly\n */\npublic class PropertyComparator<T> extends FuncComparator<T> {\n\tprivate static final long serialVersionUID = 9157326766723846313L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param property 属性名\n\t */\n\tpublic PropertyComparator(String property) {\n\t\tthis(property, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param property      属性名\n\t * @param isNullGreater null值是否排在后（从小到大排序）\n\t */\n\tpublic PropertyComparator(String property, boolean isNullGreater) {\n\t\tthis(property, true, isNullGreater);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param property      属性名\n\t * @param compareSelf   在字段值相同情况下，是否比较对象本身。\n\t *                      如果此项为{@code false}，字段值比较后为0会导致对象被认为相同，可能导致被去重。\n\t * @param isNullGreater null值是否排在后（从小到大排序）\n\t * @since 5.8.28\n\t */\n\tpublic PropertyComparator(String property, final boolean compareSelf, boolean isNullGreater) {\n\t\tsuper(isNullGreater, compareSelf, (bean) -> BeanUtil.getProperty(bean, property));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/ReverseComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\n\n/**\n * 反转比较器\n *\n * @author Looly\n *\n * @param <E> 被比较对象类型\n */\npublic class ReverseComparator<E> implements Comparator<E>, Serializable {\n\tprivate static final long serialVersionUID = 8083701245147495562L;\n\n\t/** 原始比较器 */\n\tprivate final Comparator<? super E> comparator;\n\n\t@SuppressWarnings(\"unchecked\")\n\tpublic ReverseComparator(Comparator<? super E> comparator) {\n\t\tthis.comparator = (null == comparator) ? ComparableComparator.INSTANCE : comparator;\n\t}\n\n\t//-----------------------------------------------------------------------------------------------------\n\t@Override\n\tpublic int compare(E o1, E o2) {\n\t\treturn comparator.compare(o2, o1);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn \"ReverseComparator\".hashCode() ^ comparator.hashCode();\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object object) {\n\t\tif (this == object) {\n\t\t\treturn true;\n\t\t}\n\t\tif (null == object) {\n\t\t\treturn false;\n\t\t}\n\t\tif (object.getClass().equals(this.getClass())) {\n\t\t\tfinal ReverseComparator<?> thatrc = (ReverseComparator<?>) object;\n\t\t\treturn comparator.equals(thatrc.comparator);\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Version;\nimport cn.hutool.core.util.*;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\n\n/**\n * 版本比较器<br>\n * 比较两个版本的大小<br>\n * 排序时版本从小到大排序，即比较时小版本在前，大版本在后<br>\n * 支持如：1.3.20.8，6.82.20160101，8.5a/8.5c等版本形式<br>\n * 参考：java.lang.module.ModuleDescriptor.Version\n *\n * @author Looly\n * @since 4.0.2\n */\npublic class VersionComparator implements Comparator<String>, Serializable {\n\tprivate static final long serialVersionUID = 8083701245147495562L;\n\n\t/** 单例 */\n\tpublic static final VersionComparator INSTANCE = new VersionComparator();\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic VersionComparator() {\n\t}\n\n\t// -----------------------------------------------------------------------------------------------------\n\t/**\n\t * 比较两个版本<br>\n\t * null版本排在最小：即：\n\t * <pre>\n\t * compare(null, \"v1\") &lt; 0\n\t * compare(\"v1\", \"v1\")  = 0\n\t * compare(null, null)   = 0\n\t * compare(\"v1\", null) &gt; 0\n\t * compare(\"1.0.0\", \"1.0.2\") &lt; 0\n\t * compare(\"1.0.2\", \"1.0.2a\") &lt; 0\n\t * compare(\"1.0.3\", \"1.0.2a\") &gt; 0\n\t * compare(\"1.13.0\", \"1.12.1c\") &gt; 0\n\t * compare(\"V0.0.20170102\", \"V0.0.20170101\") &gt; 0\n\t * </pre>\n\t *\n\t * @param version1 版本1\n\t * @param version2 版本2\n\t */\n\t@Override\n\tpublic int compare(String version1, String version2) {\n\t\tif(ObjectUtil.equal(version1, version2)) {\n\t\t\treturn 0;\n\t\t}\n\t\tif (version1 == null && version2 == null) {\n\t\t\treturn 0;\n\t\t} else if (version1 == null) {// null或\"\"视为最小版本，排在前\n\t\t\treturn -1;\n\t\t} else if (version2 == null) {\n\t\t\treturn 1;\n\t\t}\n\n\t\treturn CompareUtil.compare(Version.of(version1), Version.of(version2));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/WindowsExplorerStringComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Windows 资源管理器风格字符串比较器\n *\n * <p>此比较器模拟了 Windows 资源管理器的文件名排序方式，可得到与其相同的排序结果。</p>\n *\n * <p>假设有一个数组，包含若干个文件名 {@code {\"abc2.doc\", \"abc1.doc\", \"abc12.doc\"}}</p>\n * <p>在 Windows 资源管理器中以名称排序时，得到 {@code {\"abc1.doc\", \"abc2.doc\", \"abc12.doc\" }}</p>\n * <p>调用 {@code Arrays.sort(filenames);} 时，得到 {@code {\"abc1.doc\", \"abc12.doc\", \"abc2.doc\" }}</p>\n * <p>调用 {@code Arrays.sort(filenames, new WindowsExplorerStringComparator());} 时，得到 {@code {\"abc1.doc\", \"abc2.doc\",\n * \"abc12.doc\" }}，这与在资源管理器中看到的相同</p>\n *\n * @author YMNNs\n * @see <a href=\"https://stackoverflow.com/questions/23205020/java-sort-strings-like-windows-explorer\">Java - Sort Strings like Windows Explorer</a>\n */\npublic class WindowsExplorerStringComparator implements Comparator<CharSequence> {\n\n\t/**\n\t * 单例\n\t */\n\tpublic static final WindowsExplorerStringComparator INSTANCE = new WindowsExplorerStringComparator();\n\n\tprivate static final Pattern splitPattern = Pattern.compile(\"\\\\d+|\\\\.|\\\\s\");\n\n\t@Override\n\tpublic int compare(CharSequence str1, CharSequence str2) {\n\t\tIterator<String> i1 = splitStringPreserveDelimiter(str1).iterator();\n\t\tIterator<String> i2 = splitStringPreserveDelimiter(str2).iterator();\n\t\twhile (true) {\n\t\t\t//Til here all is equal.\n\t\t\tif (!i1.hasNext() && !i2.hasNext()) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t//first has no more parts -> comes first\n\t\t\tif (!i1.hasNext()) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\t//first has more parts than i2 -> comes after\n\t\t\tif (!i2.hasNext()) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tString data1 = i1.next();\n\t\t\tString data2 = i2.next();\n\t\t\tint result;\n\t\t\ttry {\n\t\t\t\t//If both data are numbers, then compare numbers\n\t\t\t\tresult = Long.compare(Long.parseLong(data1), Long.parseLong(data2));\n\t\t\t\t//If numbers are equal than longer comes first\n\t\t\t\tif (result == 0) {\n\t\t\t\t\tresult = -Integer.compare(data1.length(), data2.length());\n\t\t\t\t}\n\t\t\t} catch (NumberFormatException ex) {\n\t\t\t\t//compare text case insensitive\n\t\t\t\tresult = data1.compareToIgnoreCase(data2);\n\t\t\t}\n\n\t\t\tif (result != 0) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate List<String> splitStringPreserveDelimiter(CharSequence str) {\n\t\tMatcher matcher = splitPattern.matcher(str);\n\t\tList<String> list = new ArrayList<>();\n\t\tint pos = 0;\n\t\twhile (matcher.find()) {\n\t\t\tlist.add(StrUtil.sub(str, pos, matcher.start()));\n\t\t\tlist.add(matcher.group());\n\t\t\tpos = matcher.end();\n\t\t}\n\t\tlist.add(StrUtil.subSuf(str, pos));\n\t\treturn list;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/comparator/package-info.java",
    "content": "/**\n * 各种比较器（Comparator）实现和封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.comparator;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/CompilerException.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 编译异常\n *\n * @author looly\n * @since 5.5.2\n */\npublic class CompilerException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic CompilerException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic CompilerException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic CompilerException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic CompilerException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic CompilerException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/CompilerUtil.java",
    "content": "package cn.hutool.core.compiler;\n\nimport javax.tools.DiagnosticListener;\nimport javax.tools.JavaCompiler;\nimport javax.tools.JavaFileManager;\nimport javax.tools.JavaFileObject;\nimport javax.tools.StandardJavaFileManager;\nimport javax.tools.ToolProvider;\n\n/**\n * 源码编译工具类，主要封装{@link JavaCompiler} 相关功能\n *\n * @author looly\n * @since 5.5.2\n */\npublic class CompilerUtil {\n\n\t/**\n\t * java 编译器\n\t */\n\tpublic static final JavaCompiler SYSTEM_COMPILER = ToolProvider.getSystemJavaCompiler();\n\n\t/**\n\t * 编译指定的源码文件\n\t *\n\t * @param sourceFiles 源码文件路径\n\t * @return 0表示成功，否则其他\n\t */\n\tpublic static boolean compile(String... sourceFiles) {\n\t\treturn 0 == SYSTEM_COMPILER.run(null, null, null, sourceFiles);\n\t}\n\n\t/**\n\t * 获取{@link StandardJavaFileManager}\n\t *\n\t * @return {@link StandardJavaFileManager}\n\t */\n\tpublic static StandardJavaFileManager getFileManager() {\n\t\treturn getFileManager(null);\n\t}\n\n\t/**\n\t * 获取{@link StandardJavaFileManager}\n\t *\n\t * @param diagnosticListener 异常收集器\n\t * @return {@link StandardJavaFileManager}\n\t * @since 5.5.8\n\t */\n\tpublic static StandardJavaFileManager getFileManager(DiagnosticListener<? super JavaFileObject> diagnosticListener) {\n\t\treturn SYSTEM_COMPILER.getStandardFileManager(diagnosticListener, null, null);\n\t}\n\n\t/**\n\t * 新建编译任务\n\t *\n\t * @param fileManager        {@link JavaFileManager}，用于管理已经编译好的文件\n\t * @param diagnosticListener 诊断监听\n\t * @param options            选项，例如 -cpXXX等\n\t * @param compilationUnits   编译单元，即需要编译的对象\n\t * @return {@link JavaCompiler.CompilationTask}\n\t */\n\tpublic static JavaCompiler.CompilationTask getTask(\n\t\t\tJavaFileManager fileManager,\n\t\t\tDiagnosticListener<? super JavaFileObject> diagnosticListener,\n\t\t\tIterable<String> options,\n\t\t\tIterable<? extends JavaFileObject> compilationUnits) {\n\t\treturn SYSTEM_COMPILER.getTask(null, fileManager, diagnosticListener, options, null, compilationUnits);\n\t}\n\n\t/**\n\t * 获取{@link JavaSourceCompiler}\n\t *\n\t * @param parent 父{@link ClassLoader}\n\t * @return {@link JavaSourceCompiler}\n\t * @see JavaSourceCompiler#create(ClassLoader)\n\t */\n\tpublic static JavaSourceCompiler getCompiler(ClassLoader parent) {\n\t\treturn JavaSourceCompiler.create(parent);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/DiagnosticUtil.java",
    "content": "package cn.hutool.core.compiler;\n\nimport javax.tools.DiagnosticCollector;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * 诊断工具类\n *\n * @author looly\n * @since 5.5.2\n */\npublic class DiagnosticUtil {\n\n\t/**\n\t * 获取{@link DiagnosticCollector}收集到的诊断信息，以文本返回\n\t *\n\t * @param collector {@link DiagnosticCollector}\n\t * @return 诊断消息\n\t */\n\tpublic static String getMessages(DiagnosticCollector<?> collector) {\n\t\tfinal List<?> diagnostics = collector.getDiagnostics();\n\t\treturn diagnostics.stream().map(String::valueOf)\n\t\t\t\t.collect(Collectors.joining(System.lineSeparator()));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.io.resource.FileObjectResource;\nimport cn.hutool.core.lang.ResourceClassLoader;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport javax.tools.FileObject;\nimport javax.tools.ForwardingJavaFileManager;\nimport javax.tools.JavaFileManager;\nimport javax.tools.JavaFileObject;\nimport javax.tools.JavaFileObject.Kind;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Java 字节码文件对象管理器\n *\n * <p>\n * 正常我们使用javac命令编译源码时会将class文件写入到磁盘中，但在运行时动态编译类不适合保存在磁盘中\n * 我们采取此对象来管理运行时动态编译类生成的字节码。\n * </p>\n *\n * @author lzpeng\n * @since 5.5.2\n */\nclass JavaClassFileManager extends ForwardingJavaFileManager<JavaFileManager> {\n\n\t/**\n\t * 存储java字节码文件对象映射\n\t */\n\tprivate final Map<String, FileObjectResource> classFileObjectMap = new HashMap<>();\n\n\t/**\n\t * 加载动态编译生成类的父类加载器\n\t */\n\tprivate final ClassLoader parent;\n\n\t/**\n\t * 构造\n\t *\n\t * @param parent      父类加载器\n\t * @param fileManager 字节码文件管理器\n\t */\n\tprotected JavaClassFileManager(ClassLoader parent, JavaFileManager fileManager) {\n\t\tsuper(fileManager);\n\t\tthis.parent = ObjectUtil.defaultIfNull(parent, ClassLoaderUtil::getClassLoader);\n\t}\n\n\t/**\n\t * 获得动态编译生成的类的类加载器\n\t *\n\t * @param location 源码位置\n\t * @return 动态编译生成的类的类加载器\n\t */\n\t@Override\n\tpublic ClassLoader getClassLoader(final Location location) {\n\t\treturn new ResourceClassLoader<>(this.parent, this.classFileObjectMap);\n\t}\n\n\t/**\n\t * 获得Java字节码文件对象\n\t * 编译器编译源码时会将Java源码对象编译转为Java字节码对象\n\t *\n\t * @param location  源码位置\n\t * @param className 类名\n\t * @param kind      文件类型\n\t * @param sibling   Java源码对象\n\t * @return Java字节码文件对象\n\t */\n\t@Override\n\tpublic JavaFileObject getJavaFileForOutput(final Location location, final String className, final Kind kind, final FileObject sibling) {\n\t\tfinal JavaFileObject javaFileObject = new JavaClassFileObject(className);\n\t\tthis.classFileObjectMap.put(className, new FileObjectResource(javaFileObject));\n\t\treturn javaFileObject;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java",
    "content": "package cn.hutool.core.compiler;\n\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport javax.tools.SimpleJavaFileObject;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * Java 字节码文件对象，用于在内存中暂存class字节码，从而可以在ClassLoader中动态加载。\n *\n * @author lzpeng\n * @since 5.5.2\n */\nclass JavaClassFileObject extends SimpleJavaFileObject {\n\n\t/**\n\t * 字节码输出流\n\t */\n\tprivate final ByteArrayOutputStream byteArrayOutputStream;\n\n\t/**\n\t * 构造\n\t *\n\t * @param className 编译后的class文件的类名\n\t * @see JavaClassFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)\n\t */\n\tprotected JavaClassFileObject(String className) {\n\t\tsuper(URLUtil.getStringURI(className.replace(CharUtil.DOT, CharUtil.SLASH) + Kind.CLASS.extension), Kind.CLASS);\n\t\tthis.byteArrayOutputStream = new ByteArrayOutputStream();\n\t}\n\n\t/**\n\t * 获得字节码输入流\n\t * 编译器编辑源码后，我们将通过此输出流获得编译后的字节码，以便运行时加载类\n\t *\n\t * @return 字节码输入流\n\t * @see JavaClassFileManager#getClassLoader(javax.tools.JavaFileManager.Location)\n\t */\n\t@Override\n\tpublic InputStream openInputStream() {\n\t\treturn new ByteArrayInputStream(byteArrayOutputStream.toByteArray());\n\t}\n\n\t/**\n\t * 获得字节码输出流\n\t * 编译器编辑源码时，会将编译结果输出到本输出流中\n\t *\n\t * @return 字节码输出流\n\t */\n\t@Override\n\tpublic OutputStream openOutputStream() {\n\t\treturn this.byteArrayOutputStream;\n\t}\n\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/JavaFileObjectUtil.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.io.file.FileNameUtil;\nimport cn.hutool.core.util.ZipUtil;\n\nimport javax.tools.JavaFileObject;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.zip.ZipFile;\n\n/**\n * {@link JavaFileObject} 相关工具类封装\n *\n * @author lzpeng, looly\n * @since 5.5.2\n */\npublic class JavaFileObjectUtil {\n\n\t/**\n\t * 获取指定文件下的所有待编译的java文件，并以{@link JavaFileObject}形式返回\n\t *\n\t * @param file 文件或目录，文件支持.java、.jar和.zip文件\n\t * @return 所有待编译的 {@link JavaFileObject}\n\t */\n\tpublic static List<JavaFileObject> getJavaFileObjects(File file) {\n\t\tfinal List<JavaFileObject> result = new ArrayList<>();\n\t\tfinal String fileName = file.getName();\n\n\t\tif (isJavaFile(fileName)) {\n\t\t\tresult.add(new JavaSourceFileObject(file.toURI()));\n\t\t} else if (isJarOrZipFile(fileName)) {\n\t\t\tresult.addAll(getJavaFileObjectByZipOrJarFile(file));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 是否是jar 或 zip 文件\n\t *\n\t * @param fileName 文件名\n\t * @return 是否是jar 或 zip 文件\n\t */\n\tpublic static boolean isJarOrZipFile(String fileName) {\n\t\treturn FileNameUtil.isType(fileName, \"jar\", \"zip\");\n\t}\n\n\t/**\n\t * 是否是java文件\n\t *\n\t * @param fileName 文件名\n\t * @return 是否是.java文件\n\t */\n\tpublic static boolean isJavaFile(String fileName) {\n\t\treturn FileNameUtil.isType(fileName, \"java\");\n\t}\n\n\t/**\n\t * 通过zip包或jar包创建Java文件对象\n\t *\n\t * @param file 压缩文件\n\t * @return Java文件对象\n\t */\n\tprivate static List<JavaFileObject> getJavaFileObjectByZipOrJarFile(File file) {\n\t\tfinal List<JavaFileObject> collection = new ArrayList<>();\n\t\tfinal ZipFile zipFile = ZipUtil.toZipFile(file, null);\n\t\tZipUtil.read(zipFile, (zipEntry) -> {\n\t\t\tfinal String name = zipEntry.getName();\n\t\t\tif (isJavaFile(name)) {\n\t\t\t\tcollection.add(new JavaSourceFileObject(name, ZipUtil.getStream(zipFile, zipEntry)));\n\t\t\t}\n\t\t});\n\t\treturn collection;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.FileResource;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.io.resource.StringResource;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport javax.tools.DiagnosticCollector;\nimport javax.tools.JavaCompiler.CompilationTask;\nimport javax.tools.JavaFileObject;\nimport javax.tools.StandardLocation;\nimport java.io.File;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * Java 源码编译器\n * <p>通过此类可以动态编译java源码，并加载到ClassLoader，从而动态获取加载的类。</p>\n * <p>JavaSourceCompiler支持加载的源码类型包括：</p>\n * <ul>\n *     <li>源码文件</li>\n *     <li>源码文件源码字符串</li>\n * </ul>\n *\n * <p>使用方法如下：</p>\n * <pre>\n *     ClassLoader classLoader = JavaSourceCompiler.create(null)\n *         .addSource(FileUtil.file(\"test-compile/b/B.java\"))\n *         .addSource(\"c.C\", FileUtil.readUtf8String(\"test-compile/c/C.java\"))\n *         // 增加编译依赖的类库\n *         .addLibrary(libFile)\n *         .compile();\n *     Class&lt;?&gt; clazz = classLoader.loadClass(\"c.C\");\n * </pre>\n *\n * @author lzpeng\n */\npublic class JavaSourceCompiler {\n\n\t/**\n\t * 待编译的资源，支持：\n\t *\n\t * <ul>\n\t *     <li>源码字符串，使用{@link StringResource}</li>\n\t *     <li>源码文件、源码jar包或源码zip包，亦或者文件夹，使用{@link FileResource}</li>\n\t * </ul>\n\t * 可以是 .java文件 压缩文件 文件夹 递归搜索文件夹内的zip包和jar包\n\t */\n\tprivate final List<Resource> sourceList = new ArrayList<>();\n\n\t/**\n\t * 编译时需要加入classpath中的文件 可以是 压缩文件 文件夹递归搜索文件夹内的zip包和jar包\n\t */\n\tprivate final List<File> libraryFileList = new ArrayList<>();\n\n\t/**\n\t * 编译类时使用的父类加载器\n\t */\n\tprivate final ClassLoader parentClassLoader;\n\n\t/**\n\t * 创建Java源码编译器\n\t *\n\t * @param parent 父类加载器\n\t * @return Java源码编译器\n\t */\n\tpublic static JavaSourceCompiler create(ClassLoader parent) {\n\t\treturn new JavaSourceCompiler(parent);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param parent 父类加载器，null则使用默认类加载器\n\t */\n\tprivate JavaSourceCompiler(ClassLoader parent) {\n\t\tthis.parentClassLoader = ObjectUtil.defaultIfNull(parent, ClassLoaderUtil::getClassLoader);\n\t}\n\n\t/**\n\t * 向编译器中加入待编译的资源<br>\n\t * 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包\n\t *\n\t * @param resources 待编译的资源，支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包\n\t * @return Java源码编译器\n\t */\n\tpublic JavaSourceCompiler addSource(Resource... resources) {\n\t\tif (ArrayUtil.isNotEmpty(resources)) {\n\t\t\tthis.sourceList.addAll(Arrays.asList(resources));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 向编译器中加入待编译的文件<br>\n\t * 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包\n\t *\n\t * @param files 待编译的文件 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包\n\t * @return Java源码编译器\n\t */\n\tpublic JavaSourceCompiler addSource(File... files) {\n\t\tif (ArrayUtil.isNotEmpty(files)) {\n\t\t\tfor (File file : files) {\n\t\t\t\tthis.sourceList.add(new FileResource(file));\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 向编译器中加入待编译的源码Map\n\t *\n\t * @param sourceCodeMap 源码Map key: 类名 value 源码\n\t * @return Java源码编译器\n\t */\n\tpublic JavaSourceCompiler addSource(Map<String, String> sourceCodeMap) {\n\t\tif (MapUtil.isNotEmpty(sourceCodeMap)) {\n\t\t\tsourceCodeMap.forEach(this::addSource);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 向编译器中加入待编译的源码\n\t *\n\t * @param className  类名\n\t * @param sourceCode 源码\n\t * @return Java文件编译器\n\t */\n\tpublic JavaSourceCompiler addSource(String className, String sourceCode) {\n\t\tif (className != null && sourceCode != null) {\n\t\t\tthis.sourceList.add(new StringResource(sourceCode, className));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 加入编译Java源码时所需要的jar包，jar包中必须为字节码\n\t *\n\t * @param files 编译Java源码时所需要的jar包\n\t * @return Java源码编译器\n\t */\n\tpublic JavaSourceCompiler addLibrary(File... files) {\n\t\tif (ArrayUtil.isNotEmpty(files)) {\n\t\t\tthis.libraryFileList.addAll(Arrays.asList(files));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 编译所有文件并返回类加载器\n\t *\n\t * @return 类加载器\n\t */\n\tpublic ClassLoader compile() {\n\t\treturn compile(null);\n\t}\n\n\t/**\n\t * 编译所有文件并返回类加载器\n\t *\n\t * @param options 编译参数\n\t * @return 类加载器\n\t */\n\tpublic ClassLoader compile(List<String> options) {\n\t\t// 获得classPath\n\t\tfinal List<File> classPath = getClassPath();\n\t\tfinal URL[] urLs = URLUtil.getURLs(classPath.toArray(new File[0]));\n\t\tfinal URLClassLoader ucl = URLClassLoader.newInstance(urLs, this.parentClassLoader);\n\t\tif (sourceList.isEmpty()) {\n\t\t\t// 没有需要编译的源码文件返回加载zip或jar包的类加载器\n\t\t\treturn ucl;\n\t\t}\n\n\t\t// 创建编译器\n\t\tfinal JavaClassFileManager javaFileManager = new JavaClassFileManager(ucl, CompilerUtil.getFileManager());\n\n\t\t// classpath\n\t\tif (null == options) {\n\t\t\toptions = new ArrayList<>();\n\t\t}\n\t\tif (false == classPath.isEmpty()) {\n\t\t\tfinal List<String> cp = CollUtil.map(classPath, File::getAbsolutePath, true);\n\t\t\toptions.add(\"-cp\");\n\t\t\toptions.add(CollUtil.join(cp, FileUtil.isWindows() ? \";\" : \":\"));\n\t\t}\n\n\t\t// 编译文件\n\t\tfinal DiagnosticCollector<? super JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();\n\t\tfinal List<JavaFileObject> javaFileObjectList = getJavaFileObject();\n\t\tfinal CompilationTask task = CompilerUtil.getTask(javaFileManager, diagnosticCollector, options, javaFileObjectList);\n\t\ttry {\n\t\t\tif (task.call()) {\n\t\t\t\t// 加载编译后的类\n\t\t\t\treturn javaFileManager.getClassLoader(StandardLocation.CLASS_OUTPUT);\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(javaFileManager);\n\t\t}\n\t\t//编译失败,收集错误信息\n\t\tthrow new CompilerException(DiagnosticUtil.getMessages(diagnosticCollector));\n\t}\n\n\t/**\n\t * 获得编译源码时需要的classpath\n\t *\n\t * @return 编译源码时需要的classpath\n\t */\n\tprivate List<File> getClassPath() {\n\t\tList<File> classPathFileList = new ArrayList<>();\n\t\tfor (File file : libraryFileList) {\n\t\t\tList<File> jarOrZipFile = FileUtil.loopFiles(file, (subFile) -> JavaFileObjectUtil.isJarOrZipFile(subFile.getName()));\n\t\t\tclassPathFileList.addAll(jarOrZipFile);\n\t\t\tif (file.isDirectory()) {\n\t\t\t\tclassPathFileList.add(file);\n\t\t\t}\n\t\t}\n\t\treturn classPathFileList;\n\t}\n\n\t/**\n\t * 获得待编译的Java文件对象\n\t *\n\t * @return 待编译的Java文件对象\n\t */\n\tprivate List<JavaFileObject> getJavaFileObject() {\n\t\tfinal List<JavaFileObject> list = new ArrayList<>();\n\n\t\tfor (Resource resource : this.sourceList) {\n\t\t\tif (resource instanceof FileResource) {\n\t\t\t\tfinal File file = ((FileResource) resource).getFile();\n\t\t\t\tFileUtil.walkFiles(file, (subFile) -> list.addAll(JavaFileObjectUtil.getJavaFileObjects(subFile)));\n\t\t\t} else {\n\t\t\t\tlist.add(new JavaSourceFileObject(resource.getName(), resource.getStream()));\n\t\t\t}\n\t\t}\n\n\t\treturn list;\n\t}\n\n\t/**\n\t * 通过源码Map获得Java文件对象\n\t *\n\t * @param sourceCodeMap 源码Map\n\t * @return Java文件对象集合\n\t */\n\tprivate Collection<JavaFileObject> getJavaFileObjectByMap(final Map<String, String> sourceCodeMap) {\n\t\tif (MapUtil.isNotEmpty(sourceCodeMap)) {\n\t\t\treturn sourceCodeMap.entrySet().stream()\n\t\t\t\t\t.map(entry -> new JavaSourceFileObject(entry.getKey(), entry.getValue(), CharsetUtil.CHARSET_UTF_8))\n\t\t\t\t\t.collect(Collectors.toList());\n\t\t}\n\t\treturn Collections.emptySet();\n\t}\n\n\t/**\n\t * 通过.java文件创建Java文件对象\n\t *\n\t * @param file .java文件\n\t * @return Java文件对象\n\t */\n\tprivate JavaFileObject getJavaFileObjectByJavaFile(final File file) {\n\t\treturn new JavaSourceFileObject(file.toURI());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport javax.tools.SimpleJavaFileObject;\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.nio.charset.Charset;\n\n/**\n * Java 源码文件对象，支持：<br>\n * <ol>\n *     <li>源文件，通过文件的uri传入</li>\n *     <li>代码内容，通过流传入</li>\n * </ol>\n *\n * @author lzpeng\n * @since 5.5.2\n */\nclass JavaSourceFileObject extends SimpleJavaFileObject {\n\n\t/**\n\t * 输入流\n\t */\n\tprivate InputStream inputStream;\n\n\t/**\n\t * Source code.\n\t */\n\tprivate String sourceCode;\n\n\t/**\n\t * 构造，支持File等路径类型的源码\n\t *\n\t * @param uri  需要编译的文件uri\n\t */\n\tprotected JavaSourceFileObject(URI uri) {\n\t\tsuper(uri, Kind.SOURCE);\n\t}\n\n\t/**\n\t * 构造，支持String类型的源码\n\t *\n\t * @param className 需要编译的类名\n\t * @param code      需要编译的类源码\n\t */\n\tprotected JavaSourceFileObject(String className, String code, Charset charset) {\n\t\tthis(className, IoUtil.toStream(code, charset));\n\t}\n\n\t/**\n\t * 构造，支持流中读取源码（例如zip或网络等）\n\t *\n\t * @param name        需要编译的文件名\n\t * @param inputStream 输入流\n\t */\n\tprotected JavaSourceFileObject(String name, InputStream inputStream) {\n\t\tthis(URLUtil.getStringURI(name.replace(CharUtil.DOT, CharUtil.SLASH) + Kind.SOURCE.extension));\n\t\tthis.inputStream = inputStream;\n\t}\n\n\t/**\n\t * 获得类源码的输入流\n\t *\n\t * @return 类源码的输入流\n\t * @throws IOException IO 异常\n\t */\n\t@Override\n\tpublic InputStream openInputStream() throws IOException {\n\t\tif (inputStream == null) {\n\t\t\tinputStream = toUri().toURL().openStream();\n\t\t}\n\t\treturn new BufferedInputStream(inputStream);\n\t}\n\n\t/**\n\t * 获得类源码\n\t * 编译器编辑源码前，会通过此方法获取类的源码\n\t *\n\t * @param ignoreEncodingErrors 是否忽略编码错误\n\t * @return 需要编译的类的源码\n\t * @throws IOException IO异常\n\t */\n\t@Override\n\tpublic CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {\n\t\tif (sourceCode == null) {\n\t\t\ttry(final InputStream in = openInputStream()){\n\t\t\t\tsourceCode = IoUtil.readUtf8(in);\n\t\t\t}\n\t\t}\n\t\treturn sourceCode;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java",
    "content": "/**\n * 运行时编译java源码,动态从字符串或外部文件加载类\n *\n * @author : Lzpeng\n */\npackage cn.hutool.core.compiler;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/Deflate.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.zip.Deflater;\nimport java.util.zip.DeflaterOutputStream;\nimport java.util.zip.Inflater;\nimport java.util.zip.InflaterOutputStream;\n\n/**\n * Deflate算法<br>\n * Deflate是同时使用了LZ77算法与哈夫曼编码（Huffman Coding）的一个无损数据压缩算法。\n *\n * @author looly\n * @since 5.7.8\n */\npublic class Deflate implements Closeable {\n\n\tprivate final InputStream source;\n\tprivate OutputStream target;\n\tprivate final boolean nowrap;\n\n\t/**\n\t * 创建Deflate\n\t *\n\t * @param source 源流\n\t * @param target 目标流\n\t * @param nowrap {@code true}表示兼容Gzip压缩\n\t * @return this\n\t */\n\tpublic static Deflate of(InputStream source, OutputStream target, boolean nowrap) {\n\t\treturn new Deflate(source, target, nowrap);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 源流\n\t * @param target 目标流\n\t * @param nowrap {@code true}表示兼容Gzip压缩\n\t */\n\tpublic Deflate(InputStream source, OutputStream target, boolean nowrap) {\n\t\tthis.source = source;\n\t\tthis.target = target;\n\t\tthis.nowrap = nowrap;\n\t}\n\n\t/**\n\t * 获取目标流\n\t *\n\t * @return 目标流\n\t */\n\tpublic OutputStream getTarget() {\n\t\treturn this.target;\n\t}\n\n\t/**\n\t * 将普通数据流压缩\n\t *\n\t * @param level 压缩级别，0~9\n\t * @return this\n\t */\n\tpublic Deflate deflater(int level) {\n\t\ttarget= (target instanceof DeflaterOutputStream) ?\n\t\t\t\t(DeflaterOutputStream) target : new DeflaterOutputStream(target, new Deflater(level, nowrap));\n\t\tIoUtil.copy(source, target);\n\t\ttry {\n\t\t\t((DeflaterOutputStream)target).finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将压缩流解压到target中\n\t *\n\t * @return this\n\t */\n\tpublic Deflate inflater() {\n\t\ttarget = (target instanceof InflaterOutputStream) ?\n\t\t\t\t(InflaterOutputStream) target : new InflaterOutputStream(target, new Inflater(nowrap));\n\t\tIoUtil.copy(source, target);\n\t\ttry {\n\t\t\t((InflaterOutputStream)target).finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.target);\n\t\tIoUtil.close(this.source);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/Gzip.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.zip.GZIPInputStream;\nimport java.util.zip.GZIPOutputStream;\n\n/**\n * GZIP是用于Unix系统的文件压缩<br>\n * gzip的基础是DEFLATE\n *\n * @author looly\n * @since 5.7.8\n */\npublic class Gzip implements Closeable {\n\n\tprivate InputStream source;\n\tprivate OutputStream target;\n\n\t/**\n\t * 创建Gzip\n\t *\n\t * @param source 源流\n\t * @param target 目标流\n\t * @return Gzip\n\t */\n\tpublic static Gzip of(InputStream source, OutputStream target) {\n\t\treturn new Gzip(source, target);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 源流\n\t * @param target 目标流\n\t */\n\tpublic Gzip(InputStream source, OutputStream target) {\n\t\tthis.source = source;\n\t\tthis.target = target;\n\t}\n\n\t/**\n\t * 获取目标流\n\t *\n\t * @return 目标流\n\t */\n\tpublic OutputStream getTarget() {\n\t\treturn this.target;\n\t}\n\n\t/**\n\t * 将普通数据流压缩\n\t *\n\t * @return Gzip\n\t */\n\tpublic Gzip gzip() {\n\t\ttry {\n\t\t\ttarget = (target instanceof GZIPOutputStream) ?\n\t\t\t\t\t(GZIPOutputStream) target : new GZIPOutputStream(target);\n\t\t\tIoUtil.copy(source, target);\n\t\t\t((GZIPOutputStream) target).finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将压缩流解压到target中\n\t *\n\t * @return Gzip\n\t */\n\tpublic Gzip unGzip() {\n\t\ttry {\n\t\t\tsource = (source instanceof GZIPInputStream) ?\n\t\t\t\t\t(GZIPInputStream) source : new GZIPInputStream(source);\n\t\t\tIoUtil.copy(source, target);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.target);\n\t\tIoUtil.close(this.source);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.DirectoryNotEmptyException;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\n/**\n * Zip文件拷贝的FileVisitor实现，zip中追加文件，此类非线程安全<br>\n * 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。\n *\n * @author looly\n * @since 5.7.15\n */\npublic class ZipCopyVisitor extends SimpleFileVisitor<Path> {\n\n\t/**\n\t * 源Path，或基准路径，用于计算被拷贝文件的相对路径\n\t */\n\tprivate final Path source;\n\tprivate final FileSystem fileSystem;\n\tprivate final CopyOption[] copyOptions;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 源Path，或基准路径，用于计算被拷贝文件的相对路径\n\t * @param fileSystem 目标Zip文件\n\t * @param copyOptions 拷贝选项，如跳过已存在等\n\t */\n\tpublic ZipCopyVisitor(Path source, FileSystem fileSystem, CopyOption... copyOptions) {\n\t\tthis.source = source;\n\t\tthis.fileSystem = fileSystem;\n\t\tthis.copyOptions = copyOptions;\n\t}\n\n\t@Override\n\tpublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {\n\t\tfinal Path targetDir = resolveTarget(dir);\n\t\tif(StrUtil.isNotEmpty(targetDir.toString())){\n\t\t\t// 在目标的Zip文件中的相对位置创建目录\n\t\t\ttry {\n\t\t\t\tFiles.copy(dir, targetDir, copyOptions);\n\t\t\t} catch (final DirectoryNotEmptyException ignore) {\n\t\t\t\t// 目录已经存在，则跳过\n\t\t\t} catch (FileAlreadyExistsException e) {\n\t\t\t\tif (false == Files.isDirectory(targetDir)) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t\t// 目录非空情况下，跳过创建目录\n\t\t\t}\n\t\t}\n\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t@Override\n\tpublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n\t\t// 如果目标存在，无论目录还是文件都抛出FileAlreadyExistsException异常，此处不做特别处理\n\t\tFiles.copy(file, resolveTarget(file), copyOptions);\n\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t/**\n\t * 根据源文件或目录路径，拼接生成目标的文件或目录路径<br>\n\t * 原理是首先截取源路径，得到相对路径，再和目标路径拼接\n\t *\n\t * <p>\n\t * 如：源路径是 /opt/test/，需要拷贝的文件是 /opt/test/a/a.txt，得到相对路径 a/a.txt<br>\n\t * 目标路径是/home/，则得到最终目标路径是 /home/a/a.txt\n\t * </p>\n\t *\n\t * @param file 需要拷贝的文件或目录Path\n\t * @return 目标Path\n\t */\n\tprivate Path resolveTarget(Path file) {\n\t\treturn fileSystem.getPath(source.relativize(file).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.ZipUtil;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.util.Enumeration;\nimport java.util.function.Consumer;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\n\n/**\n * Zip文件或流读取器，一般用于Zip文件解压\n *\n * @author looly\n * @since 5.7.8\n */\npublic class ZipReader implements Closeable {\n\n\t// size of uncompressed zip entry shouldn't be bigger of compressed in MAX_SIZE_DIFF times\n\tprivate static final int DEFAULT_MAX_SIZE_DIFF = 100;\n\n\tprivate ZipFile zipFile;\n\tprivate ZipInputStream in;\n\t/**\n\t * 检查ZipBomb文件差异倍数，-1表示不检查ZipBomb\n\t */\n\tprivate int maxSizeDiff = DEFAULT_MAX_SIZE_DIFF;\n\n\t/**\n\t * 创建ZipReader\n\t *\n\t * @param zipFile 生成的Zip文件\n\t * @param charset 编码\n\t * @return ZipReader\n\t */\n\tpublic static ZipReader of(File zipFile, Charset charset) {\n\t\treturn new ZipReader(zipFile, charset);\n\t}\n\n\t/**\n\t * 创建ZipReader\n\t *\n\t * @param in      Zip输入的流，一般为输入文件流\n\t * @param charset 编码\n\t * @return ZipReader\n\t */\n\tpublic static ZipReader of(InputStream in, Charset charset) {\n\t\treturn new ZipReader(in, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param zipFile 读取的的Zip文件\n\t * @param charset 编码\n\t */\n\tpublic ZipReader(File zipFile, Charset charset) {\n\t\tthis.zipFile = ZipUtil.toZipFile(zipFile, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param zipFile 读取的的Zip文件\n\t */\n\tpublic ZipReader(ZipFile zipFile) {\n\t\tthis.zipFile = zipFile;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param in      读取的的Zip文件流\n\t * @param charset 编码\n\t */\n\tpublic ZipReader(InputStream in, Charset charset) {\n\t\tthis.in = new ZipInputStream(in, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param zin 读取的的Zip文件流\n\t */\n\tpublic ZipReader(ZipInputStream zin) {\n\t\tthis.in = zin;\n\t}\n\n\t/**\n\t * 设置检查ZipBomb文件差异倍数，-1表示不检查ZipBomb\n\t *\n\t * @param maxSizeDiff 检查ZipBomb文件差异倍数，-1表示不检查ZipBomb\n\t * @return this\n\t * @since 5.8.21\n\t */\n\tpublic ZipReader setMaxSizeDiff(final int maxSizeDiff) {\n\t\tthis.maxSizeDiff = maxSizeDiff;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取指定路径的文件流<br>\n\t * 如果是文件模式，则直接获取Entry对应的流，如果是流模式，则遍历entry后，找到对应流返回\n\t *\n\t * @param path 路径\n\t * @return 文件流\n\t */\n\tpublic InputStream get(String path) {\n\t\tif (null != this.zipFile) {\n\t\t\tfinal ZipFile zipFile = this.zipFile;\n\t\t\tfinal ZipEntry entry = zipFile.getEntry(path);\n\t\t\tif (null != entry) {\n\t\t\t\treturn ZipUtil.getStream(zipFile, entry);\n\t\t\t}\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tZipEntry zipEntry;\n\t\t\t\twhile (null != (zipEntry = in.getNextEntry())) {\n\t\t\t\t\tif (zipEntry.getName().equals(path)) {\n\t\t\t\t\t\treturn this.in;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 解压到指定目录中\n\t *\n\t * @param outFile 解压到的目录\n\t * @return 解压的目录\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File readTo(File outFile) throws IORuntimeException {\n\t\treturn readTo(outFile, null);\n\t}\n\n\t/**\n\t * 解压到指定目录中\n\t *\n\t * @param outFile     解压到的目录\n\t * @param entryFilter 过滤器，排除不需要的文件\n\t * @return 解压的目录\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.12\n\t */\n\tpublic File readTo(File outFile, Filter<ZipEntry> entryFilter) throws IORuntimeException {\n\t\tread((zipEntry) -> {\n\t\t\tif (null == entryFilter || entryFilter.accept(zipEntry)) {\n\t\t\t\t//gitee issue #I4ZDQI\n\t\t\t\tString path = zipEntry.getName();\n\t\t\t\tif (FileUtil.isWindows()) {\n\t\t\t\t\t// Win系统下\n\t\t\t\t\tpath = StrUtil.replace(path, \"*\", \"_\");\n\t\t\t\t}\n\t\t\t\t// FileUtil.file会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/\n\t\t\t\tfinal File outItemFile = FileUtil.file(outFile, path);\n\t\t\t\tif (zipEntry.isDirectory()) {\n\t\t\t\t\t// 目录\n\t\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\t\toutItemFile.mkdirs();\n\t\t\t\t} else {\n\t\t\t\t\tInputStream in;\n\t\t\t\t\tif (null != this.zipFile) {\n\t\t\t\t\t\tin = ZipUtil.getStream(this.zipFile, zipEntry);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tin = this.in;\n\t\t\t\t\t}\n\t\t\t\t\t// 文件\n\t\t\t\t\tFileUtil.writeFromStream(in, outItemFile, false);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 读取并处理Zip文件中的每一个{@link ZipEntry}\n\t *\n\t * @param consumer {@link ZipEntry}处理器\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic ZipReader read(Consumer<ZipEntry> consumer) throws IORuntimeException {\n\t\tif (null != this.zipFile) {\n\t\t\treadFromZipFile(consumer);\n\t\t} else {\n\t\t\treadFromStream(consumer);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() throws IORuntimeException {\n\t\tif (null != this.zipFile) {\n\t\t\tIoUtil.close(this.zipFile);\n\t\t} else {\n\t\t\tIoUtil.close(this.in);\n\t\t}\n\t}\n\n\t/**\n\t * 读取并处理Zip文件中的每一个{@link ZipEntry}\n\t *\n\t * @param consumer {@link ZipEntry}处理器\n\t */\n\tprivate void readFromZipFile(Consumer<ZipEntry> consumer) {\n\t\tfinal Enumeration<? extends ZipEntry> em = zipFile.entries();\n\t\twhile (em.hasMoreElements()) {\n\t\t\tconsumer.accept(checkZipBomb(em.nextElement()));\n\t\t}\n\t}\n\n\t/**\n\t * 读取并处理Zip流中的每一个{@link ZipEntry}\n\t *\n\t * @param consumer {@link ZipEntry}处理器\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void readFromStream(Consumer<ZipEntry> consumer) throws IORuntimeException {\n\t\ttry {\n\t\t\tZipEntry zipEntry;\n\t\t\twhile (null != (zipEntry = in.getNextEntry())) {\n\t\t\t\tconsumer.accept(zipEntry);\n\t\t\t\t// 检查ZipBomb放在读取内容之后，以便entry中的信息正常读取\n\t\t\t\tcheckZipBomb(zipEntry);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 检查Zip bomb漏洞\n\t *\n\t * @param entry {@link ZipEntry}\n\t * @return 检查后的{@link ZipEntry}\n\t */\n\tprivate ZipEntry checkZipBomb(ZipEntry entry) {\n\t\tif (null == entry) {\n\t\t\treturn null;\n\t\t}\n\t\tif(maxSizeDiff < 0 || entry.isDirectory()){\n\t\t\t// 目录不检查\n\t\t\treturn entry;\n\t\t}\n\n\t\tfinal long compressedSize = entry.getCompressedSize();\n\t\tfinal long uncompressedSize = entry.getSize();\n\t\tif (compressedSize < 0 || uncompressedSize < 0 ||\n\t\t\t\t// 默认压缩比例是100倍，一旦发现压缩率超过这个阈值，被认为是Zip bomb\n\t\t\t\tcompressedSize * maxSizeDiff < uncompressedSize) {\n\t\t\tthrow new UtilException(\"Zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}\",\n\t\t\t\t\tcompressedSize, uncompressedSize, entry.getName());\n\t\t}\n\t\treturn entry;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.ZipUtil;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.FileFilter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\n/**\n * Zip生成封装\n *\n * @author looly\n * @since 5.7.8\n */\n@SuppressWarnings(\"resource\")\npublic class ZipWriter implements Closeable {\n\n\t/**\n\t * 创建ZipWriter\n\t *\n\t * @param zipFile 生成的Zip文件\n\t * @param charset 编码\n\t * @return ZipWriter\n\t */\n\tpublic static ZipWriter of(File zipFile, Charset charset) {\n\t\treturn new ZipWriter(zipFile, charset);\n\t}\n\n\t/**\n\t * 创建ZipWriter\n\t *\n\t * @param out     Zip输出的流，一般为输出文件流\n\t * @param charset 编码\n\t * @return ZipWriter\n\t */\n\tpublic static ZipWriter of(OutputStream out, Charset charset) {\n\t\treturn new ZipWriter(out, charset);\n\t}\n\n\tprivate File zipFile;\n\tprivate final ZipOutputStream out;\n\n\t/**\n\t * 构造\n\t *\n\t * @param zipFile 生成的Zip文件\n\t * @param charset 编码\n\t */\n\tpublic ZipWriter(File zipFile, Charset charset) {\n\t\tthis.zipFile = zipFile;\n\t\tthis.out = getZipOutputStream(zipFile, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param out     {@link ZipOutputStream}\n\t * @param charset 编码\n\t */\n\tpublic ZipWriter(OutputStream out, Charset charset) {\n\t\tthis.out = ZipUtil.getZipOutputStream(out, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param out {@link ZipOutputStream}\n\t */\n\tpublic ZipWriter(ZipOutputStream out) {\n\t\tthis.out = out;\n\t}\n\n\t/**\n\t * 设置压缩级别，可选1~9，-1表示默认\n\t *\n\t * @param level 压缩级别\n\t * @return this\n\t */\n\tpublic ZipWriter setLevel(int level) {\n\t\tthis.out.setLevel(level);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置注释\n\t *\n\t * @param comment 注释\n\t * @return this\n\t */\n\tpublic ZipWriter setComment(String comment) {\n\t\tthis.out.setComment(comment);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取原始的{@link ZipOutputStream}\n\t *\n\t * @return {@link ZipOutputStream}\n\t */\n\tpublic ZipOutputStream getOut() {\n\t\treturn this.out;\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩\n\t *\n\t * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩），{@code null}表示不过滤\n\t * @param files      要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t * @since 5.1.1\n\t */\n\tpublic ZipWriter add(boolean withSrcDir, FileFilter filter, File... files) throws IORuntimeException {\n\t\tfor (File file : files) {\n\t\t\t// 如果只是压缩一个文件，则需要截取该文件的父目录\n\t\t\tString srcRootDir;\n\t\t\ttry {\n\t\t\t\tsrcRootDir = file.getCanonicalPath();\n\t\t\t\tif ((false == file.isDirectory()) || withSrcDir) {\n\t\t\t\t\t// 若是文件，则将父目录完整路径都截取掉；若设置包含目录，则将上级目录全部截取掉，保留本目录名\n\t\t\t\t\tsrcRootDir = file.getCanonicalFile().getParentFile().getCanonicalPath();\n\t\t\t\t}\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\n\t\t\t_add(file, srcRootDir, filter);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加资源到压缩包，添加后关闭资源流\n\t *\n\t * @param resources 需要压缩的资源，资源的路径为{@link Resource#getName()}\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic ZipWriter add(Resource... resources) throws IORuntimeException {\n\t\tfor (Resource resource : resources) {\n\t\t\tif (null != resource) {\n\t\t\t\tadd(resource.getName(), resource.getStream());\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加文件流到压缩包，添加后关闭输入文件流<br>\n\t * 如果输入流为{@code null}，则只创建空目录\n\t *\n\t * @param path 压缩的路径, {@code null}和\"\"表示根目录下\n\t * @param in   需要压缩的输入流，使用完后自动关闭，{@code null}表示加入空目录\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic ZipWriter add(String path, InputStream in) throws IORuntimeException {\n\t\tpath = StrUtil.nullToEmpty(path);\n\t\tif (null == in) {\n\t\t\t// 空目录需要检查路径规范性，目录以\"/\"结尾\n\t\t\tpath = StrUtil.addSuffixIfNot(path, StrUtil.SLASH);\n\t\t\tif (StrUtil.isBlank(path)) {\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\treturn putEntry(path, in);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t * 路径列表和流列表长度必须一致\n\t *\n\t * @param paths 流数据在压缩文件中的路径或文件名\n\t * @param ins   要压缩的源，添加完成后自动关闭流\n\t * @return 压缩文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.0\n\t */\n\tpublic ZipWriter add(String[] paths, InputStream[] ins) throws IORuntimeException {\n\t\tif (ArrayUtil.isEmpty(paths) || ArrayUtil.isEmpty(ins)) {\n\t\t\tthrow new IllegalArgumentException(\"Paths or ins is empty !\");\n\t\t}\n\t\tif (paths.length != ins.length) {\n\t\t\tthrow new IllegalArgumentException(\"Paths length is not equals to ins length !\");\n\t\t}\n\n\t\tfor (int i = 0; i < paths.length; i++) {\n\t\t\tadd(paths[i], ins[i]);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() throws IORuntimeException {\n\t\ttry {\n\t\t\tout.finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(this.out);\n\t\t}\n\t}\n\n\t/**\n\t * 获得 {@link ZipOutputStream}\n\t *\n\t * @param zipFile 压缩文件\n\t * @param charset 编码\n\t * @return {@link ZipOutputStream}\n\t */\n\tprivate static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) {\n\t\treturn ZipUtil.getZipOutputStream(FileUtil.getOutputStream(zipFile), charset);\n\t}\n\n\t/**\n\t * 递归压缩文件夹或压缩文件<br>\n\t * srcRootDir决定了路径截取的位置，例如：<br>\n\t * file的路径为d:/a/b/c/d.txt，srcRootDir为d:/a/b，则压缩后的文件与目录为结构为c/d.txt\n\t *\n\t * @param srcRootDir 被压缩的文件夹根目录\n\t * @param file       当前递归压缩的文件或目录对象\n\t * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩），{@code null}表示不过滤\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate ZipWriter _add(File file, String srcRootDir, FileFilter filter) throws IORuntimeException {\n\t\tif (null == file || (null != filter && false == filter.accept(file))) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// 获取文件相对于压缩文件夹根目录的子路径\n\t\tfinal String subPath = FileUtil.subPath(srcRootDir, file);\n\t\tif (file.isDirectory()) {\n\t\t\t// 如果是目录，则压缩压缩目录中的文件或子目录\n\t\t\tfinal File[] files = file.listFiles();\n\t\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\t\t// 加入目录，只有空目录时才加入目录，非空时会在创建文件时自动添加父级目录\n\t\t\t\tadd(subPath, null);\n\t\t\t} else {\n\t\t\t\t// 压缩目录下的子文件或目录\n\t\t\t\tfor (File childFile : files) {\n\t\t\t\t\t_add(childFile, srcRootDir, filter);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// issue#IAGYDG 检查加入的文件是否为压缩结果文件本身，避免死循环\n\t\t\tif (null != this.zipFile && FileUtil.equals(file, zipFile)) {\n\t\t\t\treturn this;\n\t\t\t}\n\n\t\t\t// 如果是文件或其它符号，则直接压缩该文件\n\t\t\tputEntry(subPath, FileUtil.getInputStream(file));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加文件流到压缩包，添加后关闭输入文件流<br>\n\t * 如果输入流为{@code null}，则只创建空目录\n\t *\n\t * @param path 压缩的路径, {@code null}和\"\"表示根目录下\n\t * @param in   需要压缩的输入流，使用完后自动关闭，{@code null}表示加入空目录\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate ZipWriter putEntry(String path, InputStream in) throws IORuntimeException {\n\t\ttry {\n\t\t\tout.putNextEntry(new ZipEntry(path));\n\t\t\tif (null != in) {\n\t\t\t\tIoUtil.copy(in, out);\n\t\t\t}\n\t\t\tout.closeEntry();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\n\t\tIoUtil.flush(this.out);\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/compress/package-info.java",
    "content": "/**\n * 压缩解压封装\n *\n * @author looly\n * @since 5.7.8\n */\npackage cn.hutool.core.compress;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n/**\n * 抽象转换器，提供通用的转换逻辑，同时通过convertInternal实现对应类型的专属逻辑<br>\n * 转换器不会抛出转换异常，转换失败时会返回{@code null}\n *\n * @param <T> 转换的目标类型\n * @author Looly\n */\npublic abstract class AbstractConverter<T> implements Converter<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 不抛异常转换<br>\n\t * 当转换失败时返回默认值\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @since 4.5.7\n\t */\n\tpublic T convertQuietly(Object value, T defaultValue) {\n\t\ttry {\n\t\t\treturn convert(value, defaultValue);\n\t\t} catch (Exception e) {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T convert(Object value, T defaultValue) {\n\t\tClass<T> targetType = getTargetType();\n\t\tif (null == targetType && null == defaultValue) {\n\t\t\tthrow new NullPointerException(StrUtil.format(\"[type] and [defaultValue] are both null for Converter [{}], we can not know what type to convert !\", this.getClass().getName()));\n\t\t}\n\t\tif (null == targetType) {\n\t\t\t// 目标类型不确定时使用默认值的类型\n\t\t\ttargetType = (Class<T>) defaultValue.getClass();\n\t\t}\n\t\tif (null == value) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\tif (null == defaultValue || targetType.isInstance(defaultValue)) {\n\t\t\tif (targetType.isInstance(value) && false == Map.class.isAssignableFrom(targetType)) {\n\t\t\t\t// 除Map外，已经是目标类型，不需要转换（Map类型涉及参数类型，需要单独转换）\n\t\t\t\treturn targetType.cast(value);\n\t\t\t}\n\t\t\tfinal T result = convertInternal(value);\n\t\t\treturn ((null == result) ? defaultValue : result);\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\tStrUtil.format(\"Default value [{}]({}) is not the instance of [{}]\", defaultValue, defaultValue.getClass(), targetType));\n\t\t}\n\t}\n\n\t/**\n\t * 内部转换器，被 {@link AbstractConverter#convert(Object, Object)} 调用，实现基本转换逻辑<br>\n\t * 内部转换器转换后如果转换失败可以做如下操作，处理结果都为返回默认值：\n\t *\n\t * <pre>\n\t * 1、返回{@code null}\n\t * 2、抛出一个{@link RuntimeException}异常\n\t * </pre>\n\t *\n\t * @param value 值\n\t * @return 转换后的类型\n\t */\n\tprotected abstract T convertInternal(Object value);\n\n\t/**\n\t * 值转为String，用于内部转换中需要使用String中转的情况<br>\n\t * 转换规则为：\n\t *\n\t * <pre>\n\t * 1、字符串类型将被强转\n\t * 2、数组将被转换为逗号分隔的字符串\n\t * 3、其它类型将调用默认的toString()方法\n\t * </pre>\n\t *\n\t * @param value 值\n\t * @return String\n\t */\n\tprotected String convertToStr(Object value) {\n\t\tif (null == value) {\n\t\t\treturn null;\n\t\t}\n\t\tif (value instanceof CharSequence) {\n\t\t\treturn value.toString();\n\t\t} else if (ArrayUtil.isArray(value)) {\n\t\t\treturn ArrayUtil.toString(value);\n\t\t} else if (CharUtil.isChar(value)) {\n\t\t\t//对于ASCII字符使用缓存加速转换，减少空间创建\n\t\t\treturn CharUtil.toString((char) value);\n\t\t}\n\t\treturn value.toString();\n\t}\n\n\t/**\n\t * 获得此类实现类的泛型类型\n\t *\n\t * @return 此类的泛型类型，可能为{@code null}\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic Class<T> getTargetType() {\n\t\treturn (Class<T>) ClassUtil.getTypeArgument(getClass());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.util.Map;\n\n/**\n * 基本变量类型的枚举<br>\n * 基本类型枚举包括原始类型和包装类型\n * @author xiaoleilu\n */\npublic enum BasicType {\n\tBYTE, SHORT, INT, INTEGER, LONG, DOUBLE, FLOAT, BOOLEAN, CHAR, CHARACTER, STRING;\n\n\t/** 包装类型为Key，原始类型为Value，例如： Integer.class =》 int.class. */\n\tpublic static final Map<Class<?>, Class<?>> WRAPPER_PRIMITIVE_MAP = new SafeConcurrentHashMap<>(8);\n\t/** 原始类型为Key，包装类型为Value，例如： int.class =》 Integer.class. */\n\tpublic static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = new SafeConcurrentHashMap<>(8);\n\n\tstatic {\n\t\tWRAPPER_PRIMITIVE_MAP.put(Boolean.class, boolean.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Byte.class, byte.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Character.class, char.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Double.class, double.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Float.class, float.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Integer.class, int.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Long.class, long.class);\n\t\tWRAPPER_PRIMITIVE_MAP.put(Short.class, short.class);\n\n\t\tfor (Map.Entry<Class<?>, Class<?>> entry : WRAPPER_PRIMITIVE_MAP.entrySet()) {\n\t\t\tPRIMITIVE_WRAPPER_MAP.put(entry.getValue(), entry.getKey());\n\t\t}\n\t}\n\n\t/**\n\t * 原始类转为包装类，非原始类返回原类\n\t * @param clazz 原始类\n\t * @return 包装类\n\t */\n\tpublic static Class<?> wrap(Class<?> clazz){\n\t\tif(null == clazz || false == clazz.isPrimitive()){\n\t\t\treturn clazz;\n\t\t}\n\t\tClass<?> result = PRIMITIVE_WRAPPER_MAP.get(clazz);\n\t\treturn (null == result) ? clazz : result;\n\t}\n\n\t/**\n\t * 包装类转为原始类，非包装类返回原类\n\t * @param clazz 包装类\n\t * @return 原始类\n\t */\n\tpublic static Class<?> unWrap(Class<?> clazz){\n\t\tif(null == clazz || clazz.isPrimitive()){\n\t\t\treturn clazz;\n\t\t}\n\t\tClass<?> result = WRAPPER_PRIMITIVE_MAP.get(clazz);\n\t\treturn (null == result) ? clazz : result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/CastUtil.java",
    "content": "package cn.hutool.core.convert;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 转换工具类，提供集合、Map等向上向下转换工具\n *\n * @author looly\n * @since 5.8.1\n */\npublic class CastUtil {\n\t/**\n\t * 泛型集合向上转型。例如将Collection&lt;Integer&gt;转换为Collection&lt;Number&gt;\n\t *\n\t * @param collection 集合\n\t * @param <T>        元素类型\n\t * @return 转换后的集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Collection<T> castUp(Collection<? extends T> collection) {\n\t\treturn (Collection<T>) collection;\n\t}\n\n\t/**\n\t * 泛型集合向下转型。例如将Collection&lt;Number&gt;转换为Collection&lt;Integer&gt;\n\t *\n\t * @param collection 集合\n\t * @param <T>        元素类型\n\t * @return 转换后的集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Collection<T> castDown(Collection<? super T> collection) {\n\t\treturn (Collection<T>) collection;\n\t}\n\n\t/**\n\t * 泛型集合向上转型。例如将Set&lt;Integer&gt;转换为Set&lt;Number&gt;\n\t *\n\t * @param set 集合\n\t * @param <T> 泛型\n\t * @return 泛化集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Set<T> castUp(Set<? extends T> set) {\n\t\treturn (Set<T>) set;\n\t}\n\n\t/**\n\t * 泛型集合向下转型。例如将Set&lt;Number&gt;转换为Set&lt;Integer&gt;\n\t *\n\t * @param set 集合\n\t * @param <T> 泛型子类\n\t * @return 泛化集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Set<T> castDown(Set<? super T> set) {\n\t\treturn (Set<T>) set;\n\t}\n\n\t/**\n\t * 泛型接口向上转型。例如将List&lt;Integer&gt;转换为List&lt;Number&gt;\n\t *\n\t * @param list 集合\n\t * @param <T>  泛型的父类\n\t * @return 泛化集合\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> List<T> castUp(List<? extends T> list) {\n\t\treturn (List<T>) list;\n\t}\n\n\t/**\n\t * 泛型集合向下转型。例如将List&lt;Number&gt;转换为List&lt;Integer&gt;\n\t *\n\t * @param list 集合\n\t * @param <T>  泛型的子类\n\t * @return 泛化集合\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> List<T> castDown(List<? super T> list) {\n\t\treturn (List<T>) list;\n\t}\n\n\t/**\n\t * 泛型集合向下转型。例如将Map&lt;Integer, Integer&gt;转换为Map&lt;Number,Number&gt;\n\t *\n\t * @param map 集合\n\t * @param <K> 泛型父类\n\t * @param <V> 泛型父类\n\t * @return 泛化集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> castUp(Map<? extends K, ? extends V> map) {\n\t\treturn (Map<K, V>) map;\n\t}\n\n\t/**\n\t * 泛型集合向下转型。例如将Map&lt;Number,Number&gt;转换为Map&lt;Integer, Integer&gt;\n\t *\n\t * @param map 集合\n\t * @param <K> 泛型子类\n\t * @param <V> 泛型子类\n\t * @return 泛化集合\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> castDown(Map<? super K, ? super V> map) {\n\t\treturn (Map<K, V>) map;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/Convert.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.convert.impl.CollectionConverter;\nimport cn.hutool.core.convert.impl.EnumConverter;\nimport cn.hutool.core.convert.impl.MapConverter;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.text.UnicodeUtil;\nimport cn.hutool.core.util.*;\n\nimport java.lang.reflect.Type;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.nio.charset.Charset;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 类型转换器\n *\n * @author xiaoleilu\n */\npublic class Convert {\n\n\t/**\n\t * 转换为字符串<br>\n\t * 如果给定的值为null，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static String toStr(Object value, String defaultValue) {\n\t\treturn convertQuietly(String.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为字符串<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static String toStr(Object value) {\n\t\treturn toStr(value, null);\n\t}\n\n\t/**\n\t * 转换为String数组\n\t *\n\t * @param value 被转换的值\n\t * @return String数组\n\t * @since 3.2.0\n\t */\n\tpublic static String[] toStrArray(Object value) {\n\t\treturn convert(String[].class, value);\n\t}\n\n\t/**\n\t * 转换为字符<br>\n\t * 如果给定的值为null，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Character toChar(Object value, Character defaultValue) {\n\t\treturn convertQuietly(Character.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为字符<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Character toChar(Object value) {\n\t\treturn toChar(value, null);\n\t}\n\n\t/**\n\t * 转换为Character数组\n\t *\n\t * @param value 被转换的值\n\t * @return Character数组\n\t * @since 3.2.0\n\t */\n\tpublic static Character[] toCharArray(Object value) {\n\t\treturn convert(Character[].class, value);\n\t}\n\n\t/**\n\t * 转换为byte<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Byte toByte(Object value, Byte defaultValue) {\n\t\treturn convertQuietly(Byte.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为byte<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Byte toByte(Object value) {\n\t\treturn toByte(value, null);\n\t}\n\n\t/**\n\t * 转换为Byte数组\n\t *\n\t * @param value 被转换的值\n\t * @return Byte数组\n\t * @since 3.2.0\n\t */\n\tpublic static Byte[] toByteArray(Object value) {\n\t\treturn convert(Byte[].class, value);\n\t}\n\n\t/**\n\t * 转换为Byte数组\n\t *\n\t * @param value 被转换的值\n\t * @return Byte数组\n\t * @since 5.1.1\n\t */\n\tpublic static byte[] toPrimitiveByteArray(Object value) {\n\t\treturn convert(byte[].class, value);\n\t}\n\n\t/**\n\t * 转换为Short<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Short toShort(Object value, Short defaultValue) {\n\t\treturn convertQuietly(Short.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为Short<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Short toShort(Object value) {\n\t\treturn toShort(value, null);\n\t}\n\n\t/**\n\t * 转换为Short数组\n\t *\n\t * @param value 被转换的值\n\t * @return Short数组\n\t * @since 3.2.0\n\t */\n\tpublic static Short[] toShortArray(Object value) {\n\t\treturn convert(Short[].class, value);\n\t}\n\n\t/**\n\t * 转换为Number<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Number toNumber(Object value, Number defaultValue) {\n\t\treturn convertQuietly(Number.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为Number<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Number toNumber(Object value) {\n\t\treturn toNumber(value, null);\n\t}\n\n\t/**\n\t * 转换为Number数组\n\t *\n\t * @param value 被转换的值\n\t * @return Number数组\n\t * @since 3.2.0\n\t */\n\tpublic static Number[] toNumberArray(Object value) {\n\t\treturn convert(Number[].class, value);\n\t}\n\n\t/**\n\t * 转换为int<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Integer toInt(Object value, Integer defaultValue) {\n\t\treturn convertQuietly(Integer.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为int<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Integer toInt(Object value) {\n\t\treturn toInt(value, null);\n\t}\n\n\t/**\n\t * 转换为Integer数组<br>\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Integer[] toIntArray(Object value) {\n\t\treturn convert(Integer[].class, value);\n\t}\n\n\t/**\n\t * 转换为long<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Long toLong(Object value, Long defaultValue) {\n\t\treturn convertQuietly(Long.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为long<br>\n\t * 如果给定的值为{@code null}，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Long toLong(Object value) {\n\t\treturn toLong(value, null);\n\t}\n\n\t/**\n\t * 转换为Long数组<br>\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Long[] toLongArray(Object value) {\n\t\treturn convert(Long[].class, value);\n\t}\n\n\t/**\n\t * 转换为double<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Double toDouble(Object value, Double defaultValue) {\n\t\treturn convertQuietly(Double.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为double<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Double toDouble(Object value) {\n\t\treturn toDouble(value, null);\n\t}\n\n\t/**\n\t * 转换为Double数组<br>\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Double[] toDoubleArray(Object value) {\n\t\treturn convert(Double[].class, value);\n\t}\n\n\t/**\n\t * 转换为Float<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Float toFloat(Object value, Float defaultValue) {\n\t\treturn convertQuietly(Float.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为Float<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Float toFloat(Object value) {\n\t\treturn toFloat(value, null);\n\t}\n\n\t/**\n\t * 转换为Float数组<br>\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Float[] toFloatArray(Object value) {\n\t\treturn convert(Float[].class, value);\n\t}\n\n\t/**\n\t * 转换为boolean<br>\n\t * String支持的值为：true、false、yes、ok、no，1,0 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static Boolean toBool(Object value, Boolean defaultValue) {\n\t\treturn convertQuietly(Boolean.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为boolean<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Boolean toBool(Object value) {\n\t\treturn toBool(value, null);\n\t}\n\n\t/**\n\t * 转换为Boolean数组<br>\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static Boolean[] toBooleanArray(Object value) {\n\t\treturn convert(Boolean[].class, value);\n\t}\n\n\t/**\n\t * 转换为BigInteger<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static BigInteger toBigInteger(Object value, BigInteger defaultValue) {\n\t\treturn convertQuietly(BigInteger.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为BigInteger<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static BigInteger toBigInteger(Object value) {\n\t\treturn toBigInteger(value, null);\n\t}\n\n\t/**\n\t * 转换为BigDecimal<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t */\n\tpublic static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) {\n\t\treturn convertQuietly(BigDecimal.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为BigDecimal<br>\n\t * 如果给定的值为空，或者转换失败，返回null<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static BigDecimal toBigDecimal(Object value) {\n\t\treturn toBigDecimal(value, null);\n\t}\n\n\t/**\n\t * 转换为Date<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t * @since 4.1.6\n\t */\n\tpublic static Date toDate(Object value, Date defaultValue) {\n\t\treturn convertQuietly(Date.class, value, defaultValue);\n\t}\n\n\t/**\n\t * LocalDateTime<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t * @since 5.0.7\n\t */\n\tpublic static LocalDateTime toLocalDateTime(Object value, LocalDateTime defaultValue) {\n\t\treturn convertQuietly(LocalDateTime.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为LocalDateTime<br>\n\t * 如果给定的值为空，或者转换失败，返回{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t */\n\tpublic static LocalDateTime toLocalDateTime(Object value) {\n\t\treturn toLocalDateTime(value, null);\n\t}\n\n\t/**\n\t * Instant<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value        被转换的值\n\t * @param defaultValue 转换错误时的默认值\n\t * @return 结果\n\t * @since 5.0.7\n\t */\n\tpublic static Date toInstant(Object value, Date defaultValue) {\n\t\treturn convertQuietly(Instant.class, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为Date<br>\n\t * 如果给定的值为空，或者转换失败，返回{@code null}<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value 被转换的值\n\t * @return 结果\n\t * @since 4.1.6\n\t */\n\tpublic static Date toDate(Object value) {\n\t\treturn toDate(value, null);\n\t}\n\n\t/**\n\t * 转换为Enum对象<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t *\n\t * @param <E>          枚举类型\n\t * @param clazz        Enum的Class\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return Enum\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value, E defaultValue) {\n\t\treturn (E) (new EnumConverter(clazz)).convertQuietly(value, defaultValue);\n\t}\n\n\t/**\n\t * 转换为Enum对象<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值{@code null}<br>\n\t *\n\t * @param <E>   枚举类型\n\t * @param clazz Enum的Class\n\t * @param value 值\n\t * @return Enum\n\t */\n\tpublic static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value) {\n\t\treturn toEnum(clazz, value, null);\n\t}\n\n\t/**\n\t * 转换为集合类\n\t *\n\t * @param collectionType 集合类型\n\t * @param elementType    集合中元素类型\n\t * @param value          被转换的值\n\t * @return {@link Collection}\n\t * @since 3.0.8\n\t */\n\tpublic static Collection<?> toCollection(Class<?> collectionType, Class<?> elementType, Object value) {\n\t\treturn new CollectionConverter(collectionType, elementType).convert(value, null);\n\t}\n\n\t/**\n\t * 转换为ArrayList，元素类型默认Object\n\t *\n\t * @param value 被转换的值\n\t * @return {@link List}\n\t * @since 4.1.11\n\t */\n\tpublic static List<?> toList(Object value) {\n\t\treturn convert(List.class, value);\n\t}\n\n\t/**\n\t * 转换为ArrayList\n\t *\n\t * @param <T>         元素类型\n\t * @param elementType 集合中元素类型\n\t * @param value       被转换的值\n\t * @return {@link ArrayList}\n\t * @since 4.1.20\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> List<T> toList(Class<T> elementType, Object value) {\n\t\treturn (List<T>) toCollection(ArrayList.class, elementType, value);\n\t}\n\n\t/**\n\t * 转换为HashSet\n\t *\n\t * @param <T>         元素类型\n\t * @param elementType 集合中元素类型\n\t * @param value       被转换的值\n\t * @return {@link HashSet}\n\t * @since 5.7.3\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Set<T> toSet(Class<T> elementType, Object value) {\n\t\treturn (Set<T>) toCollection(HashSet.class, elementType, value);\n\t}\n\n\t/**\n\t * 转换为Map，若value原本就是Map，则转为原始类型，若不是则默认转为HashMap\n\t *\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @param keyType   键类型\n\t * @param valueType 值类型\n\t * @param value     被转换的值\n\t * @return {@link Map}\n\t * @since 4.6.8\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> toMap(Class<K> keyType, Class<V> valueType, Object value) {\n\t\tif (value instanceof Map) {\n\t\t\treturn toMap((Class<? extends Map<?, ?>>) value.getClass(), keyType, valueType, value);\n\t\t} else {\n\t\t\treturn toMap(HashMap.class, keyType, valueType, value);\n\t\t}\n\t}\n\n\t/**\n\t * 转换为Map\n\t *\n\t * @param mapType   转后的具体Map类型\n\t * @param <K>       键类型\n\t * @param <V>       值类型\n\t * @param keyType   键类型\n\t * @param valueType 值类型\n\t * @param value     被转换的值\n\t * @return {@link Map}\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <K, V> Map<K, V> toMap(Class<? extends Map> mapType, Class<K> keyType, Class<V> valueType, Object value) {\n\t\treturn (Map<K, V>) new MapConverter(mapType, keyType, valueType).convert(value, null);\n\t}\n\n\t/**\n\t * 转换值为指定类型，类型采用字符串表示\n\t *\n\t * @param <T>       目标类型\n\t * @param className 类的字符串表示\n\t * @param value     值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t * @since 4.0.7\n\t */\n\tpublic static <T> T convertByClassName(String className, Object value) throws ConvertException {\n\t\treturn convert(ClassUtil.loadClass(className), value);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>   目标类型\n\t * @param type  类型\n\t * @param value 值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t * @since 4.0.0\n\t */\n\tpublic static <T> T convert(Class<T> type, Object value) throws ConvertException {\n\t\treturn convert((Type) type, value);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>       目标类型\n\t * @param reference 类型参考，用于持有转换后的泛型类型\n\t * @param value     值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t */\n\tpublic static <T> T convert(TypeReference<T> reference, Object value) throws ConvertException {\n\t\treturn convert(reference.getType(), value, null);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>   目标类型\n\t * @param type  类型\n\t * @param value 值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t */\n\tpublic static <T> T convert(Type type, Object value) throws ConvertException {\n\t\treturn convert(type, value, null);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>          目标类型\n\t * @param type         类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t * @since 4.0.0\n\t */\n\tpublic static <T> T convert(Class<T> type, Object value, T defaultValue) throws ConvertException {\n\t\treturn convert((Type) type, value, defaultValue);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>          目标类型\n\t * @param type         类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t */\n\tpublic static <T> T convert(Type type, Object value, T defaultValue) throws ConvertException {\n\t\treturn convertWithCheck(type, value, defaultValue, false);\n\t}\n\n\t/**\n\t * 转换值为指定类型，不抛异常转换<br>\n\t * 当转换失败时返回{@code null}\n\t *\n\t * @param <T>   目标类型\n\t * @param type  目标类型\n\t * @param value 值\n\t * @return 转换后的值，转换失败返回null\n\t * @since 4.5.10\n\t */\n\tpublic static <T> T convertQuietly(Type type, Object value) {\n\t\treturn convertQuietly(type, value, null);\n\t}\n\n\t/**\n\t * 转换值为指定类型，不抛异常转换<br>\n\t * 当转换失败时返回默认值\n\t *\n\t * @param <T>          目标类型\n\t * @param type         目标类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @since 4.5.10\n\t */\n\tpublic static <T> T convertQuietly(Type type, Object value, T defaultValue) {\n\t\treturn convertWithCheck(type, value, defaultValue, true);\n\t}\n\n\t/**\n\t * 转换值为指定类型，可选是否不抛异常转换<br>\n\t * 当转换失败时返回默认值\n\t *\n\t * @param <T>          目标类型\n\t * @param type         目标类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @param quietly      是否静默转换，true不抛异常\n\t * @return 转换后的值\n\t * @since 5.3.2\n\t */\n\tpublic static <T> T convertWithCheck(Type type, Object value, T defaultValue, boolean quietly) {\n\t\tfinal ConverterRegistry registry = ConverterRegistry.getInstance();\n\t\ttry {\n\t\t\treturn registry.convert(type, value, defaultValue);\n\t\t} catch (Exception e) {\n\t\t\tif (quietly) {\n\t\t\t\treturn defaultValue;\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------- 全角半角转换\n\n\t/**\n\t * 半角转全角，{@code null}返回{@code null}\n\t *\n\t * @param input String.\n\t * @return 全角字符串，{@code null}返回{@code null}\n\t */\n\tpublic static String toSBC(String input) {\n\t\treturn toSBC(input, null);\n\t}\n\n\t/**\n\t * 半角转全角，{@code null}返回{@code null}\n\t *\n\t * @param input         String\n\t * @param notConvertSet 不替换的字符集合\n\t * @return 全角字符串，{@code null}返回{@code null}\n\t */\n\tpublic static String toSBC(String input, Set<Character> notConvertSet) {\n\t\tif (StrUtil.isEmpty(input)) {\n\t\t\treturn input;\n\t\t}\n\t\tfinal char[] c = input.toCharArray();\n\t\tfor (int i = 0; i < c.length; i++) {\n\t\t\tif (null != notConvertSet && notConvertSet.contains(c[i])) {\n\t\t\t\t// 跳过不替换的字符\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (c[i] == CharUtil.SPACE) {\n\t\t\t\tc[i] = '\\u3000';\n\t\t\t} else if (c[i] < '\\177') {\n\t\t\t\tc[i] = (char) (c[i] + 65248);\n\t\t\t}\n\t\t}\n\t\treturn new String(c);\n\t}\n\n\t/**\n\t * 全角转半角\n\t *\n\t * @param input String.\n\t * @return 半角字符串\n\t */\n\tpublic static String toDBC(String input) {\n\t\treturn toDBC(input, null);\n\t}\n\n\t/**\n\t * 替换全角为半角\n\t *\n\t * @param text          文本\n\t * @param notConvertSet 不替换的字符集合\n\t * @return 替换后的字符\n\t */\n\tpublic static String toDBC(String text, Set<Character> notConvertSet) {\n\t\tif (StrUtil.isBlank(text)) {\n\t\t\treturn text;\n\t\t}\n\t\tfinal char[] c = text.toCharArray();\n\t\tfor (int i = 0; i < c.length; i++) {\n\t\t\tif (null != notConvertSet && notConvertSet.contains(c[i])) {\n\t\t\t\t// 跳过不替换的字符\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (c[i] == '\\u3000' || c[i] == '\\u00a0' || c[i] == '\\u2007' || c[i] == '\\u202F') {\n\t\t\t\t// \\u3000是中文全角空格，\\u00a0、\\u2007、\\u202F是不间断空格\n\t\t\t\tc[i] = ' ';\n\t\t\t} else if (c[i] > '\\uFF00' && c[i] < '\\uFF5F') {\n\t\t\t\tc[i] = (char) (c[i] - 65248);\n\t\t\t}\n\t\t}\n\n\t\treturn new String(c);\n\t}\n\n\t// --------------------------------------------------------------------- hex\n\n\t/**\n\t * 字符串转换成十六进制字符串，结果为小写\n\t *\n\t * @param str     待转换的ASCII字符串\n\t * @param charset 编码\n\t * @return 16进制字符串\n\t * @see HexUtil#encodeHexStr(String, Charset)\n\t */\n\tpublic static String toHex(String str, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(str, charset);\n\t}\n\n\t/**\n\t * byte数组转16进制串\n\t *\n\t * @param bytes 被转换的byte数组\n\t * @return 转换后的值\n\t * @see HexUtil#encodeHexStr(byte[])\n\t */\n\tpublic static String toHex(byte[] bytes) {\n\t\treturn HexUtil.encodeHexStr(bytes);\n\t}\n\n\t/**\n\t * Hex字符串转换为Byte值\n\t *\n\t * @param src Byte字符串，每个Byte之间没有分隔符\n\t * @return byte[]\n\t * @see HexUtil#decodeHex(char[])\n\t */\n\tpublic static byte[] hexToBytes(String src) {\n\t\treturn HexUtil.decodeHex(src.toCharArray());\n\t}\n\n\t/**\n\t * 十六进制转换字符串\n\t *\n\t * @param hexStr  Byte字符串(Byte之间无分隔符 如:[616C6B])\n\t * @param charset 编码 {@link Charset}\n\t * @return 对应的字符串\n\t * @see HexUtil#decodeHexStr(String, Charset)\n\t * @since 4.1.11\n\t */\n\tpublic static String hexToStr(String hexStr, Charset charset) {\n\t\treturn HexUtil.decodeHexStr(hexStr, charset);\n\t}\n\n\t/**\n\t * String的字符串转换成unicode的String\n\t *\n\t * @param strText 全角字符串\n\t * @return String 每个unicode之间无分隔符\n\t * @see UnicodeUtil#toUnicode(String)\n\t */\n\tpublic static String strToUnicode(String strText) {\n\t\treturn UnicodeUtil.toUnicode(strText);\n\t}\n\n\t/**\n\t * unicode的String转换成String的字符串\n\t *\n\t * @param unicode Unicode符\n\t * @return String 字符串\n\t * @see UnicodeUtil#toString(String)\n\t */\n\tpublic static String unicodeToStr(String unicode) {\n\t\treturn UnicodeUtil.toString(unicode);\n\t}\n\n\t/**\n\t * 给定字符串转换字符编码<br>\n\t * 如果参数为空，则返回原字符串，不报错。\n\t *\n\t * @param str           被转码的字符串\n\t * @param sourceCharset 原字符集\n\t * @param destCharset   目标字符集\n\t * @return 转换后的字符串\n\t * @see CharsetUtil#convert(String, String, String)\n\t */\n\tpublic static String convertCharset(String str, String sourceCharset, String destCharset) {\n\t\tif (StrUtil.hasBlank(str, sourceCharset, destCharset)) {\n\t\t\treturn str;\n\t\t}\n\n\t\treturn CharsetUtil.convert(str, sourceCharset, destCharset);\n\t}\n\n\t/**\n\t * 转换时间单位\n\t *\n\t * @param sourceDuration 时长\n\t * @param sourceUnit     源单位\n\t * @param destUnit       目标单位\n\t * @return 目标单位的时长\n\t */\n\tpublic static long convertTime(long sourceDuration, TimeUnit sourceUnit, TimeUnit destUnit) {\n\t\tAssert.notNull(sourceUnit, \"sourceUnit is null !\");\n\t\tAssert.notNull(destUnit, \"destUnit is null !\");\n\t\treturn destUnit.convert(sourceDuration, sourceUnit);\n\t}\n\n\t// --------------------------------------------------------------- 原始包装类型转换\n\n\t/**\n\t * 原始类转为包装类，非原始类返回原类\n\t *\n\t * @param clazz 原始类\n\t * @return 包装类\n\t * @see BasicType#wrap(Class)\n\t * @see BasicType#wrap(Class)\n\t */\n\tpublic static Class<?> wrap(Class<?> clazz) {\n\t\treturn BasicType.wrap(clazz);\n\t}\n\n\t/**\n\t * 包装类转为原始类，非包装类返回原类\n\t *\n\t * @param clazz 包装类\n\t * @return 原始类\n\t * @see BasicType#unWrap(Class)\n\t * @see BasicType#unWrap(Class)\n\t */\n\tpublic static Class<?> unWrap(Class<?> clazz) {\n\t\treturn BasicType.unWrap(clazz);\n\t}\n\n\t// -------------------------------------------------------------------------- 数字和英文转换\n\n\t/**\n\t * 将阿拉伯数字转为英文表达方式\n\t *\n\t * @param number {@link Number}对象\n\t * @return 英文表达式\n\t * @since 3.0.9\n\t */\n\tpublic static String numberToWord(Number number) {\n\t\treturn NumberWordFormatter.format(number);\n\t}\n\n\t/**\n\t * 将阿拉伯数字转为精简表示形式，例如:\n\t *\n\t * <pre>\n\t *     1200 -》 1.2k\n\t * </pre>\n\t *\n\t * @param number {@link Number}对象\n\t * @return 英文表达式\n\t * @since 5.5.9\n\t */\n\tpublic static String numberToSimple(Number number) {\n\t\treturn NumberWordFormatter.formatSimple(number.longValue());\n\t}\n\n\t/**\n\t * 将阿拉伯数字转为中文表达方式\n\t *\n\t * @param number           数字\n\t * @param isUseTraditional 是否使用繁体字（金额形式）\n\t * @return 中文\n\t * @since 3.2.3\n\t */\n\tpublic static String numberToChinese(double number, boolean isUseTraditional) {\n\t\treturn NumberChineseFormatter.format(number, isUseTraditional);\n\t}\n\n\t/**\n\t * 数字中文表示形式转数字\n\t * <ul>\n\t *     <li>一百一十二 -》 112</li>\n\t *     <li>一千零一十二 -》 1012</li>\n\t * </ul>\n\t *\n\t * @param number 数字中文表示\n\t * @return 数字\n\t * @since 5.6.0\n\t */\n\tpublic static int chineseToNumber(String number) {\n\t\treturn NumberChineseFormatter.chineseToNumber(number);\n\t}\n\n\t/**\n\t * 金额转为中文形式\n\t *\n\t * @param n 数字\n\t * @return 中文大写数字\n\t * @since 3.2.3\n\t */\n\tpublic static String digitToChinese(Number n) {\n\t\tif (null == n) {\n\t\t\tn = 0;\n\t\t}\n\t\treturn NumberChineseFormatter.format(n.doubleValue(), true, true);\n\t}\n\n\t/**\n\t * 中文大写数字金额转换为数字，返回结果以元为单位的BigDecimal类型数字<br>\n\t * 如：\n\t * “陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32”\n\t * “叁角贰分”返回“0.32”\n\t *\n\t * @param chineseMoneyAmount 中文大写数字金额\n\t * @return 返回结果以元为单位的BigDecimal类型数字\n\t * @since 5.8.5\n\t */\n\tpublic static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount) {\n\t\treturn NumberChineseFormatter.chineseMoneyToNumber(chineseMoneyAmount);\n\t}\n\n\t// -------------------------------------------------------------------------- 数字转换\n\n\t/**\n\t * int转byte\n\t *\n\t * @param intValue int值\n\t * @return byte值\n\t * @since 3.2.0\n\t */\n\tpublic static byte intToByte(int intValue) {\n\t\treturn (byte) intValue;\n\t}\n\n\t/**\n\t * byte转无符号int\n\t *\n\t * @param byteValue byte值\n\t * @return 无符号int值\n\t * @since 3.2.0\n\t */\n\tpublic static int byteToUnsignedInt(byte byteValue) {\n\t\t// Java 总是把 byte 当做有符处理；我们可以通过将其和 0xFF 进行二进制与得到它的无符值\n\t\treturn byteValue & 0xFF;\n\t}\n\n\t/**\n\t * byte数组转short<br>\n\t * 默认以小端序转换\n\t *\n\t * @param bytes byte数组\n\t * @return short值\n\t * @since 5.6.3\n\t */\n\tpublic static short bytesToShort(byte[] bytes) {\n\t\treturn ByteUtil.bytesToShort(bytes);\n\t}\n\n\t/**\n\t * short转byte数组<br>\n\t * 默认以小端序转换\n\t *\n\t * @param shortValue short值\n\t * @return byte数组\n\t * @since 5.6.3\n\t */\n\tpublic static byte[] shortToBytes(short shortValue) {\n\t\treturn ByteUtil.shortToBytes(shortValue);\n\t}\n\n\t/**\n\t * byte[]转int值<br>\n\t * 默认以小端序转换\n\t *\n\t * @param bytes byte数组\n\t * @return int值\n\t * @since 5.6.3\n\t */\n\tpublic static int bytesToInt(byte[] bytes) {\n\t\treturn ByteUtil.bytesToInt(bytes);\n\t}\n\n\t/**\n\t * int转byte数组<br>\n\t * 默认以小端序转换\n\t *\n\t * @param intValue int值\n\t * @return byte数组\n\t * @since 5.6.3\n\t */\n\tpublic static byte[] intToBytes(int intValue) {\n\t\treturn ByteUtil.intToBytes(intValue);\n\t}\n\n\t/**\n\t * long转byte数组<br>\n\t * 默认以小端序转换<br>\n\t * from: <a href=\"https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\">https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java</a>\n\t *\n\t * @param longValue long值\n\t * @return byte数组\n\t * @since 5.6.3\n\t */\n\tpublic static byte[] longToBytes(long longValue) {\n\t\treturn ByteUtil.longToBytes(longValue);\n\t}\n\n\t/**\n\t * byte数组转long<br>\n\t * 默认以小端序转换<br>\n\t * from: <a href=\"https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\">https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java</a>\n\t *\n\t * @param bytes byte数组\n\t * @return long值\n\t * @since 5.6.3\n\t */\n\tpublic static long bytesToLong(byte[] bytes) {\n\t\treturn ByteUtil.bytesToLong(bytes);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/ConvertException.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 转换异常\n * @author xiaoleilu\n */\npublic class ConvertException extends RuntimeException{\n\tprivate static final long serialVersionUID = 4730597402855274362L;\n\n\tpublic ConvertException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic ConvertException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic ConvertException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ConvertException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic ConvertException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/Converter.java",
    "content": "package cn.hutool.core.convert;\n\n/**\n * 转换器接口，实现类型转换\n *\n * @param <T> 转换到的目标类型\n * @author Looly\n */\npublic interface Converter<T> {\n\n\t/**\n\t * 转换为指定类型<br>\n\t * 如果类型无法确定，将读取默认值的类型做为目标类型\n\t *\n\t * @param value 原始值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @throws IllegalArgumentException 无法确定目标类型，且默认值为{@code null}，无法确定类型\n\t */\n\tT convert(Object value, T defaultValue) throws IllegalArgumentException;\n\n\t/**\n\t * 转换值为指定类型，可选是否不抛异常转换<br>\n\t * 当转换失败时返回默认值\n\t *\n\t * @param value 值\n\t * @param defaultValue 默认值\n\t * @param quietly 是否静默转换，true不抛异常\n\t * @return 转换后的值\n\t * @since 5.8.0\n\t * @see #convert(Object, Object)\n\t */\n\tdefault T convertWithCheck(Object value, T defaultValue, boolean quietly) {\n\t\ttry {\n\t\t\treturn convert(value, defaultValue);\n\t\t} catch (Exception e) {\n\t\t\tif(quietly){\n\t\t\t\treturn defaultValue;\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.RecordUtil;\nimport cn.hutool.core.convert.impl.*;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.*;\n\nimport java.io.Serializable;\nimport java.lang.ref.SoftReference;\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Type;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.time.*;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.*;\nimport java.util.concurrent.atomic.*;\n\n/**\n * 转换器登记中心\n * <p>\n * 将各种类型Convert对象放入登记中心，通过convert方法查找目标类型对应的转换器，将被转换对象转换之。\n * </p>\n * <p>\n * 在此类中，存放着默认转换器和自定义转换器，默认转换器是Hutool中预定义的一些转换器，自定义转换器存放用户自定的转换器。\n * </p>\n *\n * @author Looly\n */\npublic class ConverterRegistry implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 默认类型转换器\n\t */\n\tprivate Map<Class<?>, Converter<?>> defaultConverterMap;\n\t/**\n\t * 用户自定义类型转换器\n\t */\n\tprivate volatile Map<Type, Converter<?>> customConverterMap;\n\n\t/**\n\t * 类级的内部类，也就是静态的成员式内部类，该内部类的实例与外部类的实例 没有绑定关系，而且只有被调用到才会装载，从而实现了延迟加载\n\t */\n\tprivate static class SingletonHolder {\n\t\t/**\n\t\t * 静态初始化器，由JVM来保证线程安全\n\t\t */\n\t\tprivate static final ConverterRegistry INSTANCE = new ConverterRegistry();\n\t}\n\n\t/**\n\t * 获得单例的 ConverterRegistry\n\t *\n\t * @return ConverterRegistry\n\t */\n\tpublic static ConverterRegistry getInstance() {\n\t\treturn SingletonHolder.INSTANCE;\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic ConverterRegistry() {\n\t\tdefaultConverter();\n\t\tputCustomBySpi();\n\t}\n\n\t/**\n\t * 使用SPI加载转换器\n\t */\n\tprivate void putCustomBySpi() {\n\t\tServiceLoaderUtil.load(Converter.class).forEach(converter -> {\n\t\t\ttry {\n\t\t\t\tType type = TypeUtil.getTypeArgument(ClassUtil.getClass(converter));\n\t\t\t\tif (null != type) {\n\t\t\t\t\tputCustom(type, converter);\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 忽略注册失败的\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 登记自定义转换器\n\t *\n\t * @param type           转换的目标类型\n\t * @param converterClass 转换器类，必须有默认构造方法\n\t * @return ConverterRegistry\n\t */\n\tpublic ConverterRegistry putCustom(Type type, Class<? extends Converter<?>> converterClass) {\n\t\treturn putCustom(type, ReflectUtil.newInstance(converterClass));\n\t}\n\n\t/**\n\t * 登记自定义转换器\n\t *\n\t * @param type      转换的目标类型\n\t * @param converter 转换器\n\t * @return ConverterRegistry\n\t */\n\tpublic ConverterRegistry putCustom(Type type, Converter<?> converter) {\n\t\tif (null == customConverterMap) {\n\t\t\tsynchronized (this) {\n\t\t\t\tif (null == customConverterMap) {\n\t\t\t\t\tcustomConverterMap = new SafeConcurrentHashMap<>();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcustomConverterMap.put(type, converter);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得转换器<br>\n\t *\n\t * @param <T>           转换的目标类型\n\t * @param type          类型\n\t * @param isCustomFirst 是否自定义转换器优先\n\t * @return 转换器\n\t */\n\tpublic <T> Converter<T> getConverter(Type type, boolean isCustomFirst) {\n\t\tConverter<T> converter;\n\t\tif (isCustomFirst) {\n\t\t\tconverter = this.getCustomConverter(type);\n\t\t\tif (null == converter) {\n\t\t\t\tconverter = this.getDefaultConverter(type);\n\t\t\t}\n\t\t} else {\n\t\t\tconverter = this.getDefaultConverter(type);\n\t\t\tif (null == converter) {\n\t\t\t\tconverter = this.getCustomConverter(type);\n\t\t\t}\n\t\t}\n\t\treturn converter;\n\t}\n\n\t/**\n\t * 获得默认转换器\n\t *\n\t * @param <T>  转换的目标类型（转换器转换到的类型）\n\t * @param type 类型\n\t * @return 转换器\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> Converter<T> getDefaultConverter(Type type) {\n\t\tfinal Class<?> key = TypeUtil.getClass(type);\n\t\treturn (null == defaultConverterMap || null == key) ? null : (Converter<T>) defaultConverterMap.get(key);\n\t}\n\n\t/**\n\t * 获得自定义转换器\n\t *\n\t * @param <T>  转换的目标类型（转换器转换到的类型）\n\t * @param type 类型\n\t * @return 转换器\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> Converter<T> getCustomConverter(Type type) {\n\t\treturn (null == customConverterMap) ? null : (Converter<T>) customConverterMap.get(type);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>           转换的目标类型（转换器转换到的类型）\n\t * @param type          类型目标\n\t * @param value         被转换值\n\t * @param defaultValue  默认值\n\t * @param isCustomFirst 是否自定义转换器优先\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T convert(Type type, Object value, T defaultValue, boolean isCustomFirst) throws ConvertException {\n\t\tif (TypeUtil.isUnknown(type) && null == defaultValue) {\n\t\t\t// 对于用户不指定目标类型的情况，返回原值\n\t\t\treturn (T) value;\n\t\t}\n\t\tif (ObjectUtil.isNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\tif (TypeUtil.isUnknown(type)) {\n\t\t\ttype = defaultValue.getClass();\n\t\t}\n\n\t\t// issue#I7WJHH，Opt和Optional处理\n\t\tif (value instanceof Opt) {\n\t\t\tvalue = ((Opt<T>) value).get();\n\t\t\tif (ObjUtil.isNull(value)) {\n\t\t\t\treturn defaultValue;\n\t\t\t}\n\t\t}\n\t\tif (value instanceof Optional) {\n\t\t\tvalue = ((Optional<T>) value).orElse(null);\n\t\t\tif (ObjUtil.isNull(value)) {\n\t\t\t\treturn defaultValue;\n\t\t\t}\n\t\t}\n\n\t\tif (type instanceof TypeReference) {\n\t\t\ttype = ((TypeReference<?>) type).getType();\n\t\t}\n\n\t\t// 自定义对象转换\n\t\tif(value instanceof TypeConverter){\n\t\t\treturn ObjUtil.defaultIfNull((T) ((TypeConverter) value).convert(type, value), defaultValue);\n\t\t}\n\n\t\t// 标准转换器\n\t\tfinal Converter<T> converter = getConverter(type, isCustomFirst);\n\t\tif (null != converter) {\n\t\t\treturn converter.convert(value, defaultValue);\n\t\t}\n\n\t\tClass<T> rowType = (Class<T>) TypeUtil.getClass(type);\n\t\tif (null == rowType) {\n\t\t\tif (null != defaultValue) {\n\t\t\t\trowType = (Class<T>) defaultValue.getClass();\n\t\t\t} else {\n\t\t\t\t// 无法识别的泛型类型，按照Object处理\n\t\t\t\treturn (T) value;\n\t\t\t}\n\t\t}\n\n\t\t// 特殊类型转换，包括Collection、Map、强转、Array等\n\t\tfinal T result = convertSpecial(type, rowType, value, defaultValue);\n\t\tif (null != result) {\n\t\t\treturn result;\n\t\t}\n\n\t\t// 尝试转Bean\n\t\tif (BeanUtil.isBean(rowType)) {\n\t\t\treturn new BeanConverter<T>(type).convert(value, defaultValue);\n\t\t}\n\n\t\t// 无法转换\n\t\tthrow new ConvertException(\"Can not Converter from [{}] to [{}]\", value.getClass().getName(), type.getTypeName());\n\t}\n\n\t/**\n\t * 转换值为指定类型<br>\n\t * 自定义转换器优先\n\t *\n\t * @param <T>          转换的目标类型（转换器转换到的类型）\n\t * @param type         类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t * @throws ConvertException 转换器不存在\n\t */\n\tpublic <T> T convert(Type type, Object value, T defaultValue) throws ConvertException {\n\t\treturn convert(type, value, defaultValue, true);\n\t}\n\n\t/**\n\t * 转换值为指定类型\n\t *\n\t * @param <T>   转换的目标类型（转换器转换到的类型）\n\t * @param type  类型\n\t * @param value 值\n\t * @return 转换后的值，默认为{@code null}\n\t * @throws ConvertException 转换器不存在\n\t */\n\tpublic <T> T convert(Type type, Object value) throws ConvertException {\n\t\treturn convert(type, value, null);\n\t}\n\n\t// ----------------------------------------------------------- Private method start\n\n\t/**\n\t * 特殊类型转换<br>\n\t * 包括：\n\t *\n\t * <pre>\n\t * Collection\n\t * Map\n\t * 强转（无需转换）\n\t * 数组\n\t * </pre>\n\t *\n\t * @param <T>          转换的目标类型（转换器转换到的类型）\n\t * @param type         类型\n\t * @param value        值\n\t * @param defaultValue 默认值\n\t * @return 转换后的值\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate <T> T convertSpecial(Type type, Class<T> rowType, Object value, T defaultValue) {\n\t\tif (null == rowType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 集合转换（不可以默认强转）\n\t\tif (Collection.class.isAssignableFrom(rowType)) {\n\t\t\tfinal CollectionConverter collectionConverter = new CollectionConverter(type);\n\t\t\treturn (T) collectionConverter.convert(value, (Collection<?>) defaultValue);\n\t\t}\n\n\t\t// Map类型（不可以默认强转）\n\t\tif (Map.class.isAssignableFrom(rowType)) {\n\t\t\tfinal MapConverter mapConverter = new MapConverter(type);\n\t\t\treturn (T) mapConverter.convert(value, (Map<?, ?>) defaultValue);\n\t\t}\n\n\t\t// Map类型（不可以默认强转）\n\t\tif (Map.Entry.class.isAssignableFrom(rowType)) {\n\t\t\tfinal EntryConverter mapConverter = new EntryConverter(type);\n\t\t\treturn (T) mapConverter.convert(value, (Map.Entry<?, ?>) defaultValue);\n\t\t}\n\n\t\t// 默认强转\n\t\tif (rowType.isInstance(value)) {\n\t\t\treturn (T) value;\n\t\t}\n\n\t\t// 枚举转换\n\t\tif (rowType.isEnum()) {\n\t\t\treturn (T) new EnumConverter(rowType).convert(value, defaultValue);\n\t\t}\n\n\t\t// 数组转换\n\t\tif (rowType.isArray()) {\n\t\t\tfinal ArrayConverter arrayConverter = new ArrayConverter(rowType);\n\t\t\treturn (T) arrayConverter.convert(value, defaultValue);\n\t\t}\n\n\t\t// issue#I7FQ29 Class\n\t\tif(\"java.lang.Class\".equals(rowType.getName())){\n\t\t\tfinal ClassConverter converter = new ClassConverter();\n\t\t\treturn (T) converter.convert(value, (Class<?>) defaultValue);\n\t\t}\n\n\t\t// 空值转空Bean\n\t\tif(ObjectUtil.isEmpty(value)){\n\t\t\t// issue#3649 空值转空对象，则直接实例化\n\t\t\treturn ReflectUtil.newInstanceIfPossible(rowType);\n\t\t}\n\n\t\t// record\n\t\t// issue#3985@Github since 5.8.40\n\t\tif(RecordUtil.isRecord(rowType)){\n\t\t\treturn (T) new RecordConverter(rowType).convert(value, defaultValue);\n\t\t}\n\n\t\t// 表示非需要特殊转换的对象\n\t\treturn null;\n\t}\n\n\t/**\n\t * 注册默认转换器\n\t *\n\t * @return 转换器\n\t */\n\tprivate ConverterRegistry defaultConverter() {\n\t\tdefaultConverterMap = new SafeConcurrentHashMap<>();\n\n\t\t// 原始类型转换器\n\t\tdefaultConverterMap.put(int.class, new PrimitiveConverter(int.class));\n\t\tdefaultConverterMap.put(long.class, new PrimitiveConverter(long.class));\n\t\tdefaultConverterMap.put(byte.class, new PrimitiveConverter(byte.class));\n\t\tdefaultConverterMap.put(short.class, new PrimitiveConverter(short.class));\n\t\tdefaultConverterMap.put(float.class, new PrimitiveConverter(float.class));\n\t\tdefaultConverterMap.put(double.class, new PrimitiveConverter(double.class));\n\t\tdefaultConverterMap.put(char.class, new PrimitiveConverter(char.class));\n\t\tdefaultConverterMap.put(boolean.class, new PrimitiveConverter(boolean.class));\n\n\t\t// 包装类转换器\n\t\tdefaultConverterMap.put(Number.class, new NumberConverter());\n\t\tdefaultConverterMap.put(Integer.class, new NumberConverter(Integer.class));\n\t\tdefaultConverterMap.put(AtomicInteger.class, new NumberConverter(AtomicInteger.class));// since 3.0.8\n\t\tdefaultConverterMap.put(Long.class, new NumberConverter(Long.class));\n\t\tdefaultConverterMap.put(LongAdder.class, new NumberConverter(LongAdder.class));\n\t\tdefaultConverterMap.put(AtomicLong.class, new NumberConverter(AtomicLong.class));// since 3.0.8\n\t\tdefaultConverterMap.put(Byte.class, new NumberConverter(Byte.class));\n\t\tdefaultConverterMap.put(Short.class, new NumberConverter(Short.class));\n\t\tdefaultConverterMap.put(Float.class, new NumberConverter(Float.class));\n\t\tdefaultConverterMap.put(Double.class, new NumberConverter(Double.class));\n\t\tdefaultConverterMap.put(DoubleAdder.class, new NumberConverter(DoubleAdder.class));\n\t\tdefaultConverterMap.put(Character.class, new CharacterConverter());\n\t\tdefaultConverterMap.put(Boolean.class, new BooleanConverter());\n\t\tdefaultConverterMap.put(AtomicBoolean.class, new AtomicBooleanConverter());// since 3.0.8\n\t\tdefaultConverterMap.put(BigDecimal.class, new NumberConverter(BigDecimal.class));\n\t\tdefaultConverterMap.put(BigInteger.class, new NumberConverter(BigInteger.class));\n\t\tdefaultConverterMap.put(CharSequence.class, new StringConverter());\n\t\tdefaultConverterMap.put(String.class, new StringConverter());\n\n\t\t// URI and URL\n\t\tdefaultConverterMap.put(URI.class, new URIConverter());\n\t\tdefaultConverterMap.put(URL.class, new URLConverter());\n\n\t\t// 日期时间\n\t\tdefaultConverterMap.put(Calendar.class, new CalendarConverter());\n\t\tdefaultConverterMap.put(java.util.Date.class, new DateConverter(java.util.Date.class));\n\t\tdefaultConverterMap.put(DateTime.class, new DateConverter(DateTime.class));\n\t\tdefaultConverterMap.put(java.sql.Date.class, new DateConverter(java.sql.Date.class));\n\t\tdefaultConverterMap.put(java.sql.Time.class, new DateConverter(java.sql.Time.class));\n\t\tdefaultConverterMap.put(java.sql.Timestamp.class, new DateConverter(java.sql.Timestamp.class));\n\n\t\t// 日期时间 JDK8+(since 5.0.0)\n\t\tdefaultConverterMap.put(TemporalAccessor.class, new TemporalAccessorConverter(Instant.class));\n\t\tdefaultConverterMap.put(Instant.class, new TemporalAccessorConverter(Instant.class));\n\t\tdefaultConverterMap.put(LocalDateTime.class, new TemporalAccessorConverter(LocalDateTime.class));\n\t\tdefaultConverterMap.put(LocalDate.class, new TemporalAccessorConverter(LocalDate.class));\n\t\tdefaultConverterMap.put(LocalTime.class, new TemporalAccessorConverter(LocalTime.class));\n\t\tdefaultConverterMap.put(ZonedDateTime.class, new TemporalAccessorConverter(ZonedDateTime.class));\n\t\tdefaultConverterMap.put(OffsetDateTime.class, new TemporalAccessorConverter(OffsetDateTime.class));\n\t\tdefaultConverterMap.put(OffsetTime.class, new TemporalAccessorConverter(OffsetTime.class));\n\t\tdefaultConverterMap.put(DayOfWeek.class, new TemporalAccessorConverter(DayOfWeek.class));\n\t\tdefaultConverterMap.put(Month.class, new TemporalAccessorConverter(Month.class));\n\t\tdefaultConverterMap.put(MonthDay.class, new TemporalAccessorConverter(MonthDay.class));\n\t\tdefaultConverterMap.put(Period.class, new PeriodConverter());\n\t\tdefaultConverterMap.put(Duration.class, new DurationConverter());\n\n\t\t// Reference\n\t\tdefaultConverterMap.put(WeakReference.class, new ReferenceConverter(WeakReference.class));// since 3.0.8\n\t\tdefaultConverterMap.put(SoftReference.class, new ReferenceConverter(SoftReference.class));// since 3.0.8\n\t\tdefaultConverterMap.put(AtomicReference.class, new AtomicReferenceConverter());// since 3.0.8\n\n\t\t//AtomicXXXArray，since 5.4.5\n\t\tdefaultConverterMap.put(AtomicIntegerArray.class, new AtomicIntegerArrayConverter());\n\t\tdefaultConverterMap.put(AtomicLongArray.class, new AtomicLongArrayConverter());\n\n\t\t// 其它类型\n\t\tdefaultConverterMap.put(TimeZone.class, new TimeZoneConverter());\n\t\tdefaultConverterMap.put(Locale.class, new LocaleConverter());\n\t\tdefaultConverterMap.put(Charset.class, new CharsetConverter());\n\t\tdefaultConverterMap.put(Path.class, new PathConverter());\n\t\tdefaultConverterMap.put(Currency.class, new CurrencyConverter());// since 3.0.8\n\t\tdefaultConverterMap.put(UUID.class, new UUIDConverter());// since 4.0.10\n\t\tdefaultConverterMap.put(StackTraceElement.class, new StackTraceElementConverter());// since 4.5.2\n\t\tdefaultConverterMap.put(Optional.class, new OptionalConverter());// since 5.0.0\n\t\tdefaultConverterMap.put(Opt.class, new OptConverter());// since 5.7.16\n\t\tdefaultConverterMap.put(Pair.class, new PairConverter(Pair.class));// since 5.8.17\n\n\t\treturn this;\n\t}\n\t// ----------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 数字转中文类<br>\n * 包括：\n * <pre>\n * 1. 数字转中文大写形式，比如一百二十一\n * 2. 数字转金额用的大写形式，比如：壹佰贰拾壹\n * 3. 转金额形式，比如：壹佰贰拾壹整\n * </pre>\n *\n * @author fanqun, looly\n **/\npublic class NumberChineseFormatter {\n\n\t/**\n\t * 中文形式，奇数位置是简体，偶数位置是记账繁体，0共用<br>\n\t * 使用混合数组提高效率和数组复用\n\t **/\n\tprivate static final char[] DIGITS = {'零', '一', '壹', '二', '贰', '三', '叁', '四', '肆', '五', '伍',\n\t\t\t'六', '陆', '七', '柒', '八', '捌', '九', '玖'};\n\n\t/**\n\t * 汉字转阿拉伯数字的\n\t */\n\tprivate static final ChineseUnit[] CHINESE_NAME_VALUE = {\n\t\t\tnew ChineseUnit(' ', 1, false),\n\t\t\tnew ChineseUnit('十', 10, false),\n\t\t\tnew ChineseUnit('拾', 10, false),\n\t\t\tnew ChineseUnit('百', 100, false),\n\t\t\tnew ChineseUnit('佰', 100, false),\n\t\t\tnew ChineseUnit('千', 1000, false),\n\t\t\tnew ChineseUnit('仟', 1000, false),\n\t\t\tnew ChineseUnit('万', 1_0000, true),\n\t\t\tnew ChineseUnit('亿', 1_0000_0000, true),\n\t};\n\n\t/**\n\t * 口语化映射\n\t */\n\tprivate static final Map<String, String> COLLOQUIAL_WORDS = new HashMap<String, String>() {\n\t\tprivate static final long serialVersionUID = 1L;\n\t\t{\n\t\t\tput(\"一十\", \"十\");\n\t\t\tput(\"一拾\", \"拾\");\n\t\t\tput(\"负一十\", \"负十\");\n\t\t\tput(\"负一拾\", \"负拾\");\n\t\t}\n\t};\n\n\t/**\n\t * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换.\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 中文\n\t */\n\tpublic static String format(double amount, boolean isUseTraditional) {\n\t\treturn format(amount, isUseTraditional, false);\n\t}\n\n\t/**\n\t * 阿拉伯数字转换成中文.\n\t *\n\t * <p>主要是对发票票面金额转换的扩展\n\t * <p>如：-12.32\n\t * <p>发票票面转换为：(负数)壹拾贰圆叁角贰分\n\t * <p>而非：负壹拾贰元叁角贰分\n\t * <p>共两点不同：1、(负数) 而非 负；2、圆 而非 元\n\t * 2022/3/9\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @param isMoneyMode      是否金额模式\n\t * @param negativeName     负号转换名称 如：负、(负数)\n\t * @param unitName         单位名称 如：元、圆\n\t * @return java.lang.String\n\t * @author machuanpeng\n\t * @since 5.7.23\n\t */\n\tpublic static String format(double amount, boolean isUseTraditional, boolean isMoneyMode, String negativeName, String unitName) {\n\t\tif(StrUtil.isNullOrUndefined(unitName)){\n\t\t\tunitName = \"元\";\n\t\t}\n\n\t\tif (0 == amount) {\n\t\t\treturn isMoneyMode ? \"零\" + unitName + \"整\" : \"零\";\n\t\t}\n\t\tAssert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99,\n\t\t\t\t\"Number support only: (-99999999999999.99 ~ 99999999999999.99)！\");\n\n\t\tfinal StringBuilder chineseStr = new StringBuilder();\n\n\t\t// 负数\n\t\tif (amount < 0) {\n\t\t\tchineseStr.append(StrUtil.isNullOrUndefined(negativeName) ? \"负\" : negativeName);\n\t\t\tamount = -amount;\n\t\t}\n\n\t\tlong yuan = Math.round(amount * 100);\n\t\tfinal int fen = (int) (yuan % 10);\n\t\tyuan = yuan / 10;\n\t\tfinal int jiao = (int) (yuan % 10);\n\t\tyuan = yuan / 10;\n\n\t\t// 元\n\t\tif (false == isMoneyMode || 0 != yuan) {\n\t\t\t// 金额模式下，无需“零元”\n\t\t\tchineseStr.append(longToChinese(yuan, isUseTraditional));\n\t\t\tif (isMoneyMode) {\n\t\t\t\tchineseStr.append(unitName);\n\t\t\t}\n\t\t}\n\n\t\tif (0 == jiao && 0 == fen) {\n\t\t\t//无小数部分的金额结尾\n\t\t\tif (isMoneyMode) {\n\t\t\t\tchineseStr.append(\"整\");\n\t\t\t}\n\t\t\treturn chineseStr.toString();\n\t\t}\n\n\t\t// 小数部分\n\t\tif (false == isMoneyMode) {\n\t\t\tchineseStr.append(\"点\");\n\t\t}\n\n\t\t// 角\n\t\tif (0 == yuan && 0 == jiao) {\n\t\t\t// 元和角都为0时，只有非金额模式下补“零”\n\t\t\tif (false == isMoneyMode) {\n\t\t\t\tchineseStr.append(\"零\");\n\t\t\t}\n\t\t} else {\n\t\t\tchineseStr.append(numberToChinese(jiao, isUseTraditional));\n\t\t\tif (isMoneyMode && 0 != jiao) {\n\t\t\t\tchineseStr.append(\"角\");\n\t\t\t}\n\t\t}\n\n\t\t// 分\n\t\tif (0 != fen) {\n\t\t\tchineseStr.append(numberToChinese(fen, isUseTraditional));\n\t\t\tif (isMoneyMode) {\n\t\t\t\tchineseStr.append(\"分\");\n\t\t\t}\n\t\t}\n\n\t\treturn chineseStr.toString();\n\t}\n\n\t/**\n\t * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换.\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @param isMoneyMode      是否为金额模式\n\t * @return 中文\n\t */\n\tpublic static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) {\n\t\treturn format(amount, isUseTraditional, isMoneyMode, \"负\", \"元\");\n\t}\n\n\t/**\n\t * 阿拉伯数字（支持正负整数）转换成中文\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 中文\n\t * @since 5.7.17\n\t */\n\tpublic static String format(long amount, boolean isUseTraditional) {\n\t\tif (0 == amount) {\n\t\t\treturn \"零\";\n\t\t}\n\t\tAssert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99,\n\t\t\t\t\"Number support only: (-99999999999999.99 ~ 99999999999999.99)！\");\n\n\t\tfinal StringBuilder chineseStr = new StringBuilder();\n\n\t\t// 负数\n\t\tif (amount < 0) {\n\t\t\tchineseStr.append(\"负\");\n\t\t\tamount = -amount;\n\t\t}\n\n\t\tchineseStr.append(longToChinese(amount, isUseTraditional));\n\t\treturn chineseStr.toString();\n\t}\n\n\t/**\n\t * 阿拉伯数字（支持正负整数）四舍五入后转换成中文节权位简洁计数单位，例如 -5_5555 =》 -5.56万\n\t *\n\t * @param amount 数字\n\t * @return 中文\n\t */\n\tpublic static String formatSimple(long amount) {\n\t\tif (amount < 1_0000 && amount > -1_0000) {\n\t\t\treturn String.valueOf(amount);\n\t\t}\n\t\tString res;\n\t\tif (amount < 1_0000_0000 && amount > -1_0000_0000) {\n\t\t\tres = NumberUtil.div(amount, 1_0000, 2) + \"万\";\n\t\t} else if (amount < 1_0000_0000_0000L && amount > -1_0000_0000_0000L) {\n\t\t\tres = NumberUtil.div(amount, 1_0000_0000, 2) + \"亿\";\n\t\t} else {\n\t\t\tres = NumberUtil.div(amount, 1_0000_0000_0000L, 2) + \"万亿\";\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * 格式化-999~999之间的数字<br>\n\t * 这个方法显示10~19以下的数字时使用\"十一\"而非\"一十一\"。\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 中文\n\t * @since 5.7.17\n\t */\n\tpublic static String formatThousand(int amount, boolean isUseTraditional) {\n\t\tAssert.checkBetween(amount, -999, 999, \"Number support only: (-999 ~ 999)！\");\n\n\t\tfinal String chinese = thousandToChinese(amount, isUseTraditional);\n\t\tif (amount < 20 && amount >= 10) {\n\t\t\t// \"十一\"而非\"一十一\"\n\t\t\treturn chinese.substring(1);\n\t\t}\n\t\treturn chinese;\n\t}\n\n\t/**\n\t * 阿拉伯数字转换成中文. 使用于整数、小数的转换.\n\t * 支持多位小数\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @param isUseColloquial  是否使用口语化(e.g. 一十 -》 十)\n\t * @return 中文\n\t * @since 5.8.28\n\t */\n\tpublic static String format(BigDecimal amount, boolean isUseTraditional, boolean isUseColloquial) {\n\t\tString formatAmount;\n\t\tif (amount.scale() <= 0) {\n\t\t\tformatAmount = NumberChineseFormatter.format(amount.longValue(), isUseTraditional);\n\t\t} else {\n\t\t\tList<String> numberList = StrUtil.split(amount.toPlainString(), CharUtil.DOT);\n\t\t\t// 小数部分逐个数字转换为汉字\n\t\t\tStringBuilder decimalPartStr = new StringBuilder();\n\t\t\tfor (char decimalChar : numberList.get(1).toCharArray()) {\n\t\t\t\tdecimalPartStr.append(NumberChineseFormatter.numberCharToChinese(decimalChar, isUseTraditional));\n\t\t\t}\n\t\t\tformatAmount = NumberChineseFormatter.format(amount.longValue(), isUseTraditional) + \"点\" + decimalPartStr;\n\t\t}\n\t\tif (isUseColloquial) {\n\t\t\tfor (Map.Entry<String, String> colloquialWord : COLLOQUIAL_WORDS.entrySet()) {\n\t\t\t\tif (formatAmount.startsWith(colloquialWord.getKey())) {\n\t\t\t\t\tformatAmount = formatAmount.replaceFirst(colloquialWord.getKey(), colloquialWord.getValue());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn formatAmount;\n\t}\n\n\t/**\n\t * 数字字符转中文，非数字字符原样返回\n\t *\n\t * @param c                数字字符\n\t * @param isUseTraditional 是否繁体\n\t * @return 中文字符\n\t * @since 5.3.9\n\t */\n\tpublic static String numberCharToChinese(char c, boolean isUseTraditional) {\n\t\tif (c < '0' || c > '9') {\n\t\t\treturn String.valueOf(c);\n\t\t}\n\t\treturn String.valueOf(numberToChinese(c - '0', isUseTraditional));\n\t}\n\n\t/**\n\t * 中文大写数字金额转换为数字，返回结果以元为单位的BigDecimal类型数字\n\t * 如：\n\t * \t“陆万柒仟伍佰伍拾陆元叁角贰分”返回“67556.32”\n\t * \t“叁角贰分”返回“0.32”\n\t *\n\t * @param chineseMoneyAmount 中文大写数字金额\n\t * @return 返回结果以元为单位的BigDecimal类型数字\n\t */\n\t@SuppressWarnings(\"ConstantConditions\")\n\tpublic static BigDecimal chineseMoneyToNumber(String chineseMoneyAmount){\n\t\tif(StrUtil.isBlank(chineseMoneyAmount)){\n\t\t\treturn null;\n\t\t}\n\n\t\tint yi = chineseMoneyAmount.indexOf(\"元\");\n\t\tif(yi == -1){\n\t\t\tyi = chineseMoneyAmount.indexOf(\"圆\");\n\t\t}\n\t\tfinal int ji = chineseMoneyAmount.indexOf(\"角\");\n\t\tfinal int fi = chineseMoneyAmount.indexOf(\"分\");\n\n\t\t// 先找到单位为元的数字\n\t\tString yStr = null;\n\t\tif(yi > 0) {\n\t\t\tyStr = chineseMoneyAmount.substring(0, yi);\n\t\t}\n\n\t\t// 再找到单位为角的数字\n\t\tString jStr = null;\n\t\tif(ji > 0){\n\t\t\tif(yi >= 0){\n\t\t\t\t//前面有元,角肯定要在元后面\n\t\t\t\tif(ji > yi){\n\t\t\t\t\tjStr = chineseMoneyAmount.substring(yi+1, ji);\n\t\t\t\t}\n\t\t\t}else{\n\t\t\t\t//没有元，只有角\n\t\t\t\tjStr = chineseMoneyAmount.substring(0, ji);\n\t\t\t}\n\t\t}\n\n\t\t// 再找到单位为分的数字\n\t\tString fStr = null;\n\t\tif(fi > 0){\n\t\t\tif(ji >= 0){\n\t\t\t\t//有角，分肯定在角后面\n\t\t\t\tif(fi > ji){\n\t\t\t\t\tfStr = chineseMoneyAmount.substring(ji+1, fi);\n\t\t\t\t}\n\t\t\t}else if(yi > 0){\n\t\t\t\t//没有角，有元，那就坐元后面找\n\t\t\t\tif(fi > yi){\n\t\t\t\t\tfStr = chineseMoneyAmount.substring(yi+1, fi);\n\t\t\t\t}\n\t\t\t}else {\n\t\t\t\t//没有元、角，只有分\n\t\t\t\tfStr = chineseMoneyAmount.substring(0, fi);\n\t\t\t}\n\t\t}\n\n\t\t//元、角、分\n\t\tint y = 0, j = 0, f = 0;\n\t\tif(StrUtil.isNotBlank(yStr)) {\n\t\t\ty = NumberChineseFormatter.chineseToNumber(yStr);\n\t\t}\n\t\tif(StrUtil.isNotBlank(jStr)){\n\t\t\tj = NumberChineseFormatter.chineseToNumber(jStr);\n\t\t}\n\t\tif(StrUtil.isNotBlank(fStr)){\n\t\t\tf = NumberChineseFormatter.chineseToNumber(fStr);\n\t\t}\n\n\t\tBigDecimal amount = new BigDecimal(y);\n\t\tamount = amount.add(BigDecimal.valueOf(j).divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP));\n\t\tamount = amount.add(BigDecimal.valueOf(f).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP));\n\t\treturn amount;\n\t}\n\n\t/**\n\t * 阿拉伯数字整数部分转换成中文，只支持正数\n\t *\n\t * @param amount           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 中文\n\t */\n\tprivate static String longToChinese(long amount, boolean isUseTraditional) {\n\t\tif (0 == amount) {\n\t\t\treturn \"零\";\n\t\t}\n\n\t\t//将数字以万为单位分为多份\n\t\tint[] parts = new int[4];\n\t\tfor (int i = 0; amount != 0; i++) {\n\t\t\tparts[i] = (int) (amount % 10000);\n\t\t\tamount = amount / 10000;\n\t\t}\n\n\t\tfinal StringBuilder chineseStr = new StringBuilder();\n\t\tint partValue;\n\t\tString partChinese;\n\n\t\t// 千\n\t\tpartValue = parts[0];\n\t\tif (partValue > 0) {\n\t\t\tpartChinese = thousandToChinese(partValue, isUseTraditional);\n\t\t\tchineseStr.insert(0, partChinese);\n\n\t\t\tif (partValue < 1000) {\n\t\t\t\t// 和万位之间空0，则补零，如一万零三百\n\t\t\t\taddPreZero(chineseStr);\n\t\t\t}\n\t\t}\n\n\t\t// 万\n\t\tpartValue = parts[1];\n\t\tif (partValue > 0) {\n\t\t\tif ((partValue % 10 == 0 && parts[0] > 0)) {\n\t\t\t\t// 如果\"万\"的个位是0，则补零，如十万零八千\n\t\t\t\taddPreZero(chineseStr);\n\t\t\t}\n\t\t\tpartChinese = thousandToChinese(partValue, isUseTraditional);\n\t\t\tchineseStr.insert(0, partChinese + \"万\");\n\n\t\t\tif (partValue < 1000) {\n\t\t\t\t// 和亿位之间空0，则补零，如一亿零三百万\n\t\t\t\taddPreZero(chineseStr);\n\t\t\t}\n\t\t} else {\n\t\t\taddPreZero(chineseStr);\n\t\t}\n\n\t\t// 亿\n\t\tpartValue = parts[2];\n\t\tif (partValue > 0) {\n\t\t\tif ((partValue % 10 == 0 && parts[1] > 0)) {\n\t\t\t\t// 如果\"万\"的个位是0，则补零，如十万零八千\n\t\t\t\taddPreZero(chineseStr);\n\t\t\t}\n\n\t\t\tpartChinese = thousandToChinese(partValue, isUseTraditional);\n\t\t\tchineseStr.insert(0, partChinese + \"亿\");\n\n\t\t\tif (partValue < 1000) {\n\t\t\t\t// 和万亿位之间空0，则补零，如一万亿零三百亿\n\t\t\t\taddPreZero(chineseStr);\n\t\t\t}\n\t\t} else {\n\t\t\taddPreZero(chineseStr);\n\t\t}\n\n\t\t// 万亿\n\t\tpartValue = parts[3];\n\t\tif (partValue > 0) {\n\t\t\tif (parts[2] == 0) {\n\t\t\t\tchineseStr.insert(0, \"亿\");\n\t\t\t}\n\t\t\tpartChinese = thousandToChinese(partValue, isUseTraditional);\n\t\t\tchineseStr.insert(0, partChinese + \"万\");\n\t\t}\n\n\t\tif (StrUtil.isNotEmpty(chineseStr) && '零' == chineseStr.charAt(0)) {\n\t\t\treturn chineseStr.substring(1);\n\t\t}\n\n\t\treturn chineseStr.toString();\n\t}\n\n\t/**\n\t * 把一个 0~9999 之间的整数转换为汉字的字符串，如果是 0 则返回 \"\"\n\t *\n\t * @param amountPart       数字部分\n\t * @param isUseTraditional 是否使用繁体单位\n\t * @return 转换后的汉字\n\t */\n\tprivate static String thousandToChinese(int amountPart, boolean isUseTraditional) {\n\t\tif (amountPart == 0) {\n\t\t\t// issue#I4R92H@Gitee\n\t\t\treturn String.valueOf(DIGITS[0]);\n\t\t}\n\n\t\tint temp = amountPart;\n\n\t\tStringBuilder chineseStr = new StringBuilder();\n\t\tboolean lastIsZero = true; // 在从低位往高位循环时，记录上一位数字是不是 0\n\t\tfor (int i = 0; temp > 0; i++) {\n\t\t\tint digit = temp % 10;\n\t\t\tif (digit == 0) { // 取到的数字为 0\n\t\t\t\tif (false == lastIsZero) {\n\t\t\t\t\t// 前一个数字不是 0，则在当前汉字串前加“零”字;\n\t\t\t\t\tchineseStr.insert(0, \"零\");\n\t\t\t\t}\n\t\t\t\tlastIsZero = true;\n\t\t\t} else { // 取到的数字不是 0\n\t\t\t\tchineseStr.insert(0, numberToChinese(digit, isUseTraditional) + getUnitName(i, isUseTraditional));\n\t\t\t\tlastIsZero = false;\n\t\t\t}\n\t\t\ttemp = temp / 10;\n\t\t}\n\t\treturn chineseStr.toString();\n\t}\n\n\t/**\n\t * 把中文转换为数字 如 二百二十 220<br>\n\t * <ul>\n\t *     <li>一百一十二 -》 112</li>\n\t *     <li>一千零一十二 -》 1012</li>\n\t * </ul>\n\t *\n\t * @param chinese 中文字符\n\t * @return 数字\n\t * @since 5.6.0\n\t */\n\tpublic static int chineseToNumber(String chinese) {\n\t\tfinal int length = chinese.length();\n\t\tint result = 0;\n\n\t\t// 节总和\n\t\tint section = 0;\n\t\tint number = 0;\n\t\tChineseUnit unit = null;\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = chinese.charAt(i);\n\t\t\tfinal int num = chineseToNumber(c);\n\t\t\tif (num >= 0) {\n\t\t\t\tif (num == 0) {\n\t\t\t\t\t// 遇到零时节结束，权位失效，比如两万二零一十\n\t\t\t\t\tif (number > 0 && null != unit) {\n\t\t\t\t\t\tsection += number * (unit.value / 10);\n\t\t\t\t\t}\n\t\t\t\t\tunit = null;\n\t\t\t\t} else if (number > 0) {\n\t\t\t\t\t// 多个数字同时出现，报错\n\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Bad number '{}{}' at: {}\", chinese.charAt(i - 1), c, i));\n\t\t\t\t}\n\t\t\t\t// 普通数字\n\t\t\t\tnumber = num;\n\t\t\t} else {\n\t\t\t\tunit = chineseToUnit(c);\n\t\t\t\tif (null == unit) {\n\t\t\t\t\t// 出现非法字符\n\t\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Unknown unit '{}' at: {}\", c, i));\n\t\t\t\t}\n\n\t\t\t\t//单位\n\t\t\t\tif (unit.secUnit) {\n\t\t\t\t\t// 节单位，按照节求和\n\t\t\t\t\tsection = (section + number) * unit.value;\n\t\t\t\t\tresult += section;\n\t\t\t\t\tsection = 0;\n\t\t\t\t} else {\n\t\t\t\t\t// 非节单位，和单位前的单数字组合为值\n\t\t\t\t\tint unitNumber = number;\n\t\t\t\t\tif (0 == number && 0 == i) {\n\t\t\t\t\t\t// issue#1726，对于单位开头的数组，默认赋予1\n\t\t\t\t\t\t// 十二 -> 一十二\n\t\t\t\t\t\t// 百二 -> 一百二\n\t\t\t\t\t\tunitNumber = 1;\n\t\t\t\t\t}\n\t\t\t\t\tsection += (unitNumber * unit.value);\n\t\t\t\t}\n\t\t\t\tnumber = 0;\n\t\t\t}\n\t\t}\n\n\t\tif (number > 0 && null != unit) {\n\t\t\tnumber = number * (unit.value / 10);\n\t\t}\n\n\t\treturn result + section + number;\n\t}\n\n\t/**\n\t * 查找对应的权对象\n\t *\n\t * @param chinese 中文权位名\n\t * @return 权对象\n\t */\n\tprivate static ChineseUnit chineseToUnit(char chinese) {\n\t\tfor (ChineseUnit chineseNameValue : CHINESE_NAME_VALUE) {\n\t\t\tif (chineseNameValue.name == chinese) {\n\t\t\t\treturn chineseNameValue;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将汉字单个数字转换为int类型数字\n\t *\n\t * @param chinese 汉字数字，支持简体和繁体\n\t * @return 数字，-1表示未找到\n\t * @since 5.6.4\n\t */\n\tprivate static int chineseToNumber(char chinese) {\n\t\tif ('两' == chinese) {\n\t\t\t// 口语纠正\n\t\t\tchinese = '二';\n\t\t}\n\t\tfinal int i = ArrayUtil.indexOf(DIGITS, chinese);\n\t\tif (i > 0) {\n\t\t\treturn (i + 1) / 2;\n\t\t}\n\t\treturn i;\n\t}\n\n\t/**\n\t * 单个数字转汉字\n\t *\n\t * @param number           数字\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 汉字\n\t */\n\tprivate static char numberToChinese(int number, boolean isUseTraditional) {\n\t\tif (0 == number) {\n\t\t\treturn DIGITS[0];\n\t\t}\n\t\treturn DIGITS[number * 2 - (isUseTraditional ? 0 : 1)];\n\t}\n\n\t/**\n\t * 获取对应级别的单位\n\t *\n\t * @param index            级别，0表示各位，1表示十位，2表示百位，以此类推\n\t * @param isUseTraditional 是否使用繁体\n\t * @return 单位\n\t */\n\tprivate static String getUnitName(int index, boolean isUseTraditional) {\n\t\tif (0 == index) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn String.valueOf(CHINESE_NAME_VALUE[index * 2 - (isUseTraditional ? 0 : 1)].name);\n\t}\n\n\t/**\n\t * 权位\n\t *\n\t * @author totalo\n\t * @since 5.6.0\n\t */\n\tprivate static class ChineseUnit {\n\t\t/**\n\t\t * 中文权名称\n\t\t */\n\t\tprivate final char name;\n\t\t/**\n\t\t * 10的倍数值\n\t\t */\n\t\tprivate final int value;\n\t\t/**\n\t\t * 是否为节权位，它不是与之相邻的数字的倍数，而是整个小节的倍数。<br>\n\t\t * 例如二十三万，万是节权位，与三无关，而和二十三关联\n\t\t */\n\t\tprivate final boolean secUnit;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param name    名称\n\t\t * @param value   值，即10的倍数\n\t\t * @param secUnit 是否为节权位\n\t\t */\n\t\tpublic ChineseUnit(char name, int value, boolean secUnit) {\n\t\t\tthis.name = name;\n\t\t\tthis.value = value;\n\t\t\tthis.secUnit = secUnit;\n\t\t}\n\t}\n\n\tprivate static void addPreZero(StringBuilder chineseStr) {\n\t\tif (StrUtil.isEmpty(chineseStr)) {\n\t\t\treturn;\n\t\t}\n\t\tfinal char c = chineseStr.charAt(0);\n\t\tif ('零' != c) {\n\t\t\tchineseStr.insert(0, '零');\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/NumberWithFormat.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.convert.impl.DateConverter;\nimport cn.hutool.core.convert.impl.TemporalAccessorConverter;\n\nimport java.lang.reflect.Type;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Date;\n\n/**\n * 包含格式的数字转换器，主要针对带格式的时间戳\n *\n * @author looly\n * @since 5.8.13\n */\npublic class NumberWithFormat extends Number implements TypeConverter, Comparable<NumberWithFormat> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Number number;\n\tprivate final String format;\n\n\t/**\n\t * 构造\n\t *\n\t * @param number 数字\n\t * @param format 格式\n\t */\n\tpublic NumberWithFormat(final Number number, final String format) {\n\t\tthis.number = number;\n\t\tthis.format = format;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic Object convert(Type targetType, Object value) {\n\t\t// 自定义日期格式\n\t\tif (null != this.format && targetType instanceof Class) {\n\t\t\tfinal Class<?> clazz = (Class<?>) targetType;\n\t\t\t// https://gitee.com/chinabugotech/hutool/issues/I6IS5B\n\t\t\tif (Date.class.isAssignableFrom(clazz)) {\n\t\t\t\treturn new DateConverter((Class<? extends Date>) clazz, format).convert(this.number, null);\n\t\t\t} else if (TemporalAccessor.class.isAssignableFrom(clazz)) {\n\t\t\t\treturn new TemporalAccessorConverter(clazz, format).convert(this.number, null);\n\t\t\t} else if (String.class == clazz) {\n\t\t\t\treturn toString();\n\t\t\t}\n\n\t\t\t// 其他情况按照正常数字转换\n\t\t}\n\n\t\t// 按照正常数字转换\n\t\treturn Convert.convertWithCheck(targetType, this.number, null, false);\n\t}\n\n\t/**\n\t * 获取原始Number\n\t *\n\t * @return 原始Number\n\t * @since 5.8.32\n\t */\n\tpublic Object getNumber() {\n\t\treturn this.number;\n\t}\n\n\t@Override\n\tpublic int intValue() {\n\t\treturn this.number.intValue();\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn this.number.longValue();\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn this.number.floatValue();\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn this.number.doubleValue();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.number.toString();\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\t@Override\n\tpublic int compareTo(NumberWithFormat o) {\n\t\tif(this.number instanceof Comparable && o.getNumber() instanceof Comparable) {\n\t\t\treturn ((Comparable) this.number).compareTo(o.getNumber());\n\t\t}\n\t\treturn Double.compare(this.doubleValue(), o.doubleValue());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormatter.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 将浮点数类型的number转换成英语的表达方式 <br>\n * 参考博客：http://blog.csdn.net/eric_sunah/article/details/8713226<br>\n * 本质上此类为金额转英文表达，因此没有四舍五入考虑，小数点超过两位直接忽略。\n *\n * @author Looly,totalo\n * @since 3.0.9\n */\npublic class NumberWordFormatter {\n\n\tprivate static final String[] NUMBER = new String[] { \"\", \"ONE\", \"TWO\", \"THREE\", \"FOUR\", \"FIVE\", \"SIX\", \"SEVEN\",\n\t\t\"EIGHT\", \"NINE\" };\n\tprivate static final String[] NUMBER_TEEN = new String[] { \"TEN\", \"ELEVEN\", \"TWELVE\", \"THIRTEEN\", \"FOURTEEN\",\n\t\t\"FIFTEEN\", \"SIXTEEN\", \"SEVENTEEN\", \"EIGHTEEN\", \"NINETEEN\" };\n\tprivate static final String[] NUMBER_TEN = new String[] { \"TEN\", \"TWENTY\", \"THIRTY\", \"FORTY\", \"FIFTY\", \"SIXTY\",\n\t\t\"SEVENTY\", \"EIGHTY\", \"NINETY\" };\n\tprivate static final String[] NUMBER_MORE = new String[] { \"\", \"THOUSAND\", \"MILLION\", \"BILLION\", \"TRILLION\" };\n\n\tprivate static final String[] NUMBER_SUFFIX = new String[] { \"k\", \"w\", \"m\", \"b\", \"t\", \"p\", \"e\" };\n\n\t// 标准单位序列(k, m, b, t, p, e)在NUMBER_SUFFIX中的索引\n\tprivate static final int[] STANDARD_UNIT_INDICES = { 0, 2, 3, 4, 5, 6 };\n\n\t/**\n\t * 将阿拉伯数字转为英文表达式\n\t *\n\t * @param x 阿拉伯数字，可以为{@link Number}对象，也可以是普通对象，最后会使用字符串方式处理\n\t * @return 英文表达式\n\t */\n\tpublic static String format(Object x) {\n\t\tif (x != null) {\n\t\t\treturn format(x.toString());\n\t\t} else {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t}\n\n\t/**\n\t * 将阿拉伯数字转化为简洁计数单位，例如 2100 =》 2.1k\n\t * 范围默认只到w\n\t *\n\t * @param value 被格式化的数字\n\t * @return 格式化后的数字\n\t * @since 5.5.9\n\t */\n\tpublic static String formatSimple(long value) {\n\t\treturn formatSimple(value, true);\n\t}\n\n\t/**\n\t * 将阿拉伯数字转化为简介计数单位，例如 2100 =》 2.1k\n\t *\n\t * @param value 对应数字的值\n\t * @param isTwo 控制是否为只为k、w，例如当为{@code false}时返回4.38m，{@code true}返回438.43w\n\t * @return 格式化后的数字\n\t * @since 5.5.9\n\t */\n\tpublic static String formatSimple(long value, boolean isTwo) {\n\t\tif (value < 1000) {\n\t\t\treturn String.valueOf(value);\n\t\t}\n\n\t\tdouble res = value;\n\t\tfinal int index;\n\n\t\tif (isTwo) {\n\t\t\t// 当isTwo为true时，只使用k和w单位\n\t\t\tif (value >= 10000) {\n\t\t\t\t// 使用w单位（除以10000，即10k = 1w）\n\t\t\t\tres = value / 10000.0;\n\t\t\t\tindex = 1; // w在NUMBER_SUFFIX[1]\n\t\t\t} else {\n\t\t\t\t// 使用k单位（除以1000）\n\t\t\t\tres = value / 1000.0;\n\t\t\t\tindex = 0; // k在NUMBER_SUFFIX[0]\n\t\t\t}\n\t\t} else {\n\t\t\t// 当isTwo为false时，使用标准单位序列 (k, m, b, t, p, e)\n\t\t\t// 对应NUMBER_SUFFIX中的索引为 0, 2, 3, 4, 5, 6\n\t\t\tint unitIndex = -1;\n\t\t\twhile (res >= 1000 && unitIndex < STANDARD_UNIT_INDICES.length - 1) {\n\t\t\t\tres = res / 1000;\n\t\t\t\tunitIndex++;\n\t\t\t}\n\t\t\tindex = STANDARD_UNIT_INDICES[unitIndex];\n\t\t}\n\n\t\treturn String.format(\"%s%s\", NumberUtil.decimalFormat(\"#.##\", res), NUMBER_SUFFIX[index]);\n\t}\n\n\t/**\n\t * 将阿拉伯数字转为英文表达式\n\t *\n\t * @param x 阿拉伯数字字符串\n\t * @return 英文表达式\n\t */\n\tprivate static String format(String x) {\n\t\tint z = x.indexOf(\".\"); // 取小数点位置\n\t\tString lstr, rstr = \"\";\n\t\tif (z > -1) { // 看是否有小数，如果有，则分别取左边和右边\n\t\t\tlstr = x.substring(0, z);\n\t\t\trstr = x.substring(z + 1);\n\t\t} else {\n\t\t\t// 否则就是全部\n\t\t\tlstr = x;\n\t\t}\n\n\t\tString lstrrev = StrUtil.reverse(lstr); // 对左边的字串取反\n\t\tString[] a = new String[5]; // 定义5个字串变量来存放解析出来的叁位一组的字串\n\n\t\tswitch (lstrrev.length() % 3) {\n\t\t\tcase 1:\n\t\t\t\tlstrrev += \"00\";\n\t\t\t\tbreak;\n\t\t\tcase 2:\n\t\t\t\tlstrrev += \"0\";\n\t\t\t\tbreak;\n\t\t}\n\t\tStringBuilder lm = new StringBuilder(); // 用来存放转换后的整数部分\n\t\tfor (int i = 0; i < lstrrev.length() / 3; i++) {\n\t\t\ta[i] = StrUtil.reverse(lstrrev.substring(3 * i, 3 * i + 3)); // 截取第一个三位\n\t\t\tif (!\"000\".equals(a[i])) { // 用来避免这种情况：1000000 = one million\n\t\t\t\t// thousand only\n\t\t\t\tif (i != 0) {\n\t\t\t\t\tlm.insert(0, transThree(a[i]) + \" \" + parseMore(i) + \" \"); // 加:\n\t\t\t\t\t// thousand、million、billion\n\t\t\t\t} else {\n\t\t\t\t\t// 防止i=0时， 在多加两个空格.\n\t\t\t\t\tlm = new StringBuilder(transThree(a[i]));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlm.append(transThree(a[i]));\n\t\t\t}\n\t\t}\n\n\t\tString xs = lm.length() == 0 ? \"ZERO \" : \" \"; // 用来存放转换后小数部分\n\t\tif (z > -1) {\n\t\t\txs += \"AND CENTS \" + transTwo(rstr) + \" \"; // 小数部分存在时转换小数\n\t\t}\n\n\t\treturn lm.toString().trim() + xs + \"ONLY\";\n\t}\n\n\tprivate static String parseTeen(String s) {\n\t\treturn NUMBER_TEEN[Integer.parseInt(s) - 10];\n\t}\n\n\tprivate static String parseTen(String s) {\n\t\treturn NUMBER_TEN[Integer.parseInt(s.substring(0, 1)) - 1];\n\t}\n\n\tprivate static String parseMore(int i) {\n\t\treturn NUMBER_MORE[i];\n\t}\n\n\t// 两位\n\tprivate static String transTwo(String s) {\n\t\tString value;\n\t\t// 判断位数\n\t\tif (s.length() > 2) {\n\t\t\ts = s.substring(0, 2);\n\t\t} else if (s.length() < 2) {\n\t\t\ts = s + \"0\";\n\t\t}\n\n\t\tif (s.startsWith(\"0\")) {// 07 - seven 是否小於10\n\t\t\tvalue = parseLast(s);\n\t\t} else if (s.startsWith(\"1\")) {// 17 seventeen 是否在10和20之间\n\t\t\tvalue = parseTeen(s);\n\t\t} else if (s.endsWith(\"0\")) {// 是否在10与100之间的能被10整除的数\n\t\t\tvalue = parseTen(s);\n\t\t} else {\n\t\t\tvalue = parseTen(s) + \" \" + parseLast(s);\n\t\t}\n\t\treturn value;\n\t}\n\n\t// 制作叁位的数\n\t// s.length = 3\n\tprivate static String transThree(String s) {\n\t\tString value;\n\t\tif (s.startsWith(\"0\")) {// 是否小於100\n\t\t\tvalue = transTwo(s.substring(1));\n\t\t} else if (\"00\".equals(s.substring(1))) {// 是否被100整除\n\t\t\tvalue = parseLast(s.substring(0, 1)) + \" HUNDRED\";\n\t\t} else {\n\t\t\tvalue = parseLast(s.substring(0, 1)) + \" HUNDRED AND \" + transTwo(s.substring(1));\n\t\t}\n\t\treturn value;\n\t}\n\n\tprivate static String parseLast(String s) {\n\t\treturn NUMBER[Integer.parseInt(s.substring(s.length() - 1))];\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/TypeConverter.java",
    "content": "package cn.hutool.core.convert;\n\nimport java.lang.reflect.Type;\n\n/**\n * 类型转换接口函数，根据给定的值和目标类型，由用户自定义转换规则。\n *\n * @author looly\n * @since 5.8.0\n */\n@FunctionalInterface\npublic interface TypeConverter {\n\n\t/**\n\t * 转换为指定类型<br>\n\t * 如果类型无法确定，将读取默认值的类型做为目标类型\n\t *\n\t * @param targetType 目标Type，非泛型类使用\n\t * @param value      原始值\n\t * @return 转换后的值\n\t * @throws IllegalArgumentException 无法确定目标类型，且默认值为{@code null}，无法确定类型\n\t */\n\tObject convert(Type targetType, Object value);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ByteUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Array;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * 数组转换器，包括原始类型数组\n *\n * @author Looly\n */\npublic class ArrayConverter extends AbstractConverter<Object> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<?> targetType;\n\t/**\n\t * 目标元素类型\n\t */\n\tprivate final Class<?> targetComponentType;\n\n\t/**\n\t * 是否忽略元素转换错误\n\t */\n\tprivate boolean ignoreElementError;\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType 目标数组类型\n\t */\n\tpublic ArrayConverter(Class<?> targetType) {\n\t\tthis(targetType, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType         目标数组类型\n\t * @param ignoreElementError 是否忽略元素转换错误\n\t */\n\tpublic ArrayConverter(Class<?> targetType, boolean ignoreElementError) {\n\t\tif (null == targetType) {\n\t\t\t// 默认Object数组\n\t\t\ttargetType = Object[].class;\n\t\t}\n\n\t\tif (targetType.isArray()) {\n\t\t\tthis.targetType = targetType;\n\t\t\tthis.targetComponentType = targetType.getComponentType();\n\t\t} else {\n\t\t\t//用户传入类为非数组时，按照数组元素类型对待\n\t\t\tthis.targetComponentType = targetType;\n\t\t\tthis.targetType = ArrayUtil.getArrayType(targetType);\n\t\t}\n\n\t\tthis.ignoreElementError = ignoreElementError;\n\t}\n\n\t@Override\n\tprotected Object convertInternal(Object value) {\n\t\treturn value.getClass().isArray() ? convertArrayToArray(value) : convertObjectToArray(value);\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\t@Override\n\tpublic Class getTargetType() {\n\t\treturn this.targetType;\n\t}\n\n\t/**\n\t * 设置是否忽略元素转换错误\n\t *\n\t * @param ignoreElementError 是否忽略元素转换错误\n\t * @since 5.4.3\n\t */\n\tpublic void setIgnoreElementError(boolean ignoreElementError) {\n\t\tthis.ignoreElementError = ignoreElementError;\n\t}\n\n\t// -------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 数组对数组转换\n\t *\n\t * @param array 被转换的数组值\n\t * @return 转换后的数组\n\t */\n\tprivate Object convertArrayToArray(Object array) {\n\t\tfinal Class<?> valueComponentType = ArrayUtil.getComponentType(array);\n\n\t\tif (valueComponentType == targetComponentType) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfinal int len = ArrayUtil.length(array);\n\t\tfinal Object result = Array.newInstance(targetComponentType, len);\n\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tArray.set(result, i, convertComponentType(Array.get(array, i)));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 非数组对数组转换\n\t *\n\t * @param value 被转换值\n\t * @return 转换后的数组\n\t */\n\tprivate Object convertObjectToArray(Object value) {\n\t\tif (value instanceof CharSequence) {\n\t\t\tif (targetComponentType == char.class || targetComponentType == Character.class) {\n\t\t\t\treturn convertArrayToArray(value.toString().toCharArray());\n\t\t\t}\n\n\t\t\t//issue#2365\n\t\t\t// 字符串转bytes，首先判断是否为Base64，是则转换，否则按照默认getBytes方法。\n\t\t\tif(targetComponentType == byte.class){\n\t\t\t\tfinal String str = value.toString();\n\t\t\t\tif(Base64.isBase64(str)){\n\t\t\t\t\treturn Base64.decode(value.toString());\n\t\t\t\t}\n\t\t\t\treturn str.getBytes();\n\t\t\t}\n\n\t\t\t// 单纯字符串情况下按照逗号分隔后劈开\n\t\t\tfinal String[] strings = StrUtil.splitToArray(value.toString(), CharUtil.COMMA);\n\t\t\treturn convertArrayToArray(strings);\n\t\t}\n\n\t\tObject result;\n\t\tif (value instanceof List) {\n\t\t\t// List转数组\n\t\t\tfinal List<?> list = (List<?>) value;\n\t\t\tresult = Array.newInstance(targetComponentType, list.size());\n\t\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\t\tArray.set(result, i, convertComponentType(list.get(i)));\n\t\t\t}\n\t\t} else if (value instanceof Collection) {\n\t\t\t// 集合转数组\n\t\t\tfinal Collection<?> collection = (Collection<?>) value;\n\t\t\tresult = Array.newInstance(targetComponentType, collection.size());\n\n\t\t\tint i = 0;\n\t\t\tfor (Object element : collection) {\n\t\t\t\tArray.set(result, i, convertComponentType(element));\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else if (value instanceof Iterable) {\n\t\t\t// 可循环对象转数组，可循环对象无法获取长度，因此先转为List后转为数组\n\t\t\tfinal List<?> list = IterUtil.toList((Iterable<?>) value);\n\t\t\tresult = Array.newInstance(targetComponentType, list.size());\n\t\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\t\tArray.set(result, i, convertComponentType(list.get(i)));\n\t\t\t}\n\t\t} else if (value instanceof Iterator) {\n\t\t\t// 可循环对象转数组，可循环对象无法获取长度，因此先转为List后转为数组\n\t\t\tfinal List<?> list = IterUtil.toList((Iterator<?>) value);\n\t\t\tresult = Array.newInstance(targetComponentType, list.size());\n\t\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\t\tArray.set(result, i, convertComponentType(list.get(i)));\n\t\t\t}\n\t\t}else if (value instanceof Number && byte.class == targetComponentType) {\n\t\t\t// 用户可能想序列化指定对象\n\t\t\tresult = ByteUtil.numberToBytes((Number)value);\n\t\t} else if (value instanceof Serializable && byte.class == targetComponentType) {\n\t\t\t// 用户可能想序列化指定对象\n\t\t\tresult = ObjectUtil.serialize(value);\n\t\t} else {\n\t\t\t// everything else:\n\t\t\tresult = convertToSingleElementArray(value);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 单元素数组\n\t *\n\t * @param value 被转换的值\n\t * @return 数组，只包含一个元素\n\t */\n\tprivate Object[] convertToSingleElementArray(Object value) {\n\t\tfinal Object[] singleElementArray = ArrayUtil.newArray(targetComponentType, 1);\n\t\tsingleElementArray[0] = convertComponentType(value);\n\t\treturn singleElementArray;\n\t}\n\n\t/**\n\t * 转换元素类型\n\t *\n\t * @param value 值\n\t * @return 转换后的值，转换失败若{@link #ignoreElementError}为true，返回null，否则抛出异常\n\t * @since 5.4.3\n\t */\n\tprivate Object convertComponentType(Object value) {\n\t\treturn Convert.convertWithCheck(this.targetComponentType, value, null, this.ignoreElementError);\n\t}\n\t// -------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.BooleanUtil;\n\n/**\n * {@link AtomicBoolean}转换器\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class AtomicBooleanConverter extends AbstractConverter<AtomicBoolean> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected AtomicBoolean convertInternal(Object value) {\n\t\tif (value instanceof Boolean) {\n\t\t\treturn new AtomicBoolean((Boolean) value);\n\t\t}\n\t\tfinal String valueStr = convertToStr(value);\n\t\treturn new AtomicBoolean(BooleanUtil.toBoolean(valueStr));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicIntegerArrayConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.Convert;\n\nimport java.util.concurrent.atomic.AtomicIntegerArray;\n\n/**\n * {@link AtomicIntegerArray}转换器\n *\n * @author Looly\n * @since 5.4.5\n */\npublic class AtomicIntegerArrayConverter extends AbstractConverter<AtomicIntegerArray> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected AtomicIntegerArray convertInternal(Object value) {\n\t\treturn new AtomicIntegerArray(Convert.convert(int[].class, value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicLongArrayConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.Convert;\n\nimport java.util.concurrent.atomic.AtomicLongArray;\n\n/**\n * {@link AtomicLongArray}转换器\n *\n * @author Looly\n * @since 5.4.5\n */\npublic class AtomicLongArrayConverter extends AbstractConverter<AtomicLongArray> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected AtomicLongArray convertInternal(Object value) {\n\t\treturn new AtomicLongArray(Convert.convert(long[].class, value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.lang.reflect.Type;\nimport java.util.concurrent.atomic.AtomicReference;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.util.TypeUtil;\n\n/**\n * {@link AtomicReference}转换器\n *\n * @author Looly\n * @since 3.0.8\n */\n@SuppressWarnings(\"rawtypes\")\npublic class AtomicReferenceConverter extends AbstractConverter<AtomicReference> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected AtomicReference<?> convertInternal(Object value) {\n\n\t\t//尝试将值转换为Reference泛型的类型\n\t\tObject targetValue = null;\n\t\tfinal Type paramType = TypeUtil.getTypeArgument(AtomicReference.class);\n\t\tif(false == TypeUtil.isUnknown(paramType)){\n\t\t\ttargetValue = ConverterRegistry.getInstance().convert(paramType, value);\n\t\t}\n\t\tif(null == targetValue){\n\t\t\ttargetValue = value;\n\t\t}\n\n\t\treturn new AtomicReference<>(targetValue);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.BeanCopier;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.map.MapProxy;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * Bean转换器，支持：\n * <pre>\n * Map =》 Bean\n * Bean =》 Bean\n * ValueProvider =》 Bean\n * </pre>\n *\n * @param <T> Bean类型\n * @author Looly\n * @since 4.0.2\n */\npublic class BeanConverter<T> extends AbstractConverter<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Type beanType;\n\tprivate final Class<T> beanClass;\n\tprivate final CopyOptions copyOptions;\n\n\t/**\n\t * 构造，默认转换选项，注入失败的字段忽略\n\t *\n\t * @param beanType 转换成的目标Bean类型\n\t */\n\tpublic BeanConverter(Type beanType) {\n\t\tthis(beanType, CopyOptions.create().setIgnoreError(true));\n\t}\n\n\t/**\n\t * 构造，默认转换选项，注入失败的字段忽略\n\t *\n\t * @param beanClass 转换成的目标Bean类\n\t */\n\tpublic BeanConverter(Class<T> beanClass) {\n\t\tthis(beanClass, CopyOptions.create().setIgnoreError(true));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param beanType 转换成的目标Bean类\n\t * @param copyOptions Bean转换选项参数\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic BeanConverter(Type beanType, CopyOptions copyOptions) {\n\t\tthis.beanType = beanType;\n\t\tthis.beanClass = (Class<T>) TypeUtil.getClass(beanType);\n\t\tthis.copyOptions = copyOptions;\n\t}\n\n\t@Override\n\tprotected T convertInternal(Object value) {\n\t\tfinal Class<?>[] interfaces = this.beanClass.getInterfaces();\n\t\tfor (Class<?> anInterface : interfaces) {\n\t\t\tif(\"cn.hutool.json.JSONBeanParser\".equals(anInterface.getName())){\n\t\t\t\t// issue#I7M2GZ\n\t\t\t\tfinal T obj = ReflectUtil.newInstanceIfPossible(this.beanClass);\n\t\t\t\tReflectUtil.invoke(obj, \"parse\", value);\n\t\t\t\treturn obj;\n\t\t\t}\n\t\t}\n\n\t\tif(value instanceof Map ||\n\t\t\t\tvalue instanceof ValueProvider ||\n\t\t\t\tBeanUtil.isBean(value.getClass())) {\n\t\t\tif(value instanceof Map && this.beanClass.isInterface()) {\n\t\t\t\t// 将Map动态代理为Bean\n\t\t\t\treturn MapProxy.create((Map<?, ?>)value).toProxyBean(this.beanClass);\n\t\t\t}\n\n\t\t\t//限定被转换对象类型\n\t\t\treturn BeanCopier.create(value, ReflectUtil.newInstanceIfPossible(this.beanClass), this.beanType, this.copyOptions).copy();\n\t\t} else if(value instanceof byte[]){\n\t\t\t// 尝试反序列化\n\t\t\treturn ObjectUtil.deserialize((byte[])value);\n\t\t} else if(StrUtil.isEmptyIfStr(value)){\n\t\t\t// issue#3136\n\t\t\treturn null;\n\t\t}\n\n\t\tthrow new ConvertException(\"Unsupported source type: {}\", value.getClass());\n\t}\n\n\t@Override\n\tpublic Class<T> getTargetType() {\n\t\treturn this.beanClass;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.BooleanUtil;\n\n/**\n * 布尔转换器\n *\n * <p>\n * 对象转为boolean，规则如下：\n * </p>\n * <pre>\n *     1、数字0为false，其它数字为true\n *     2、转换为字符串，形如\"true\", \"yes\", \"y\", \"t\", \"ok\", \"1\", \"on\", \"是\", \"对\", \"真\", \"對\", \"√\"为true，其它字符串为false.\n * </pre>\n *\n * @author Looly\n */\npublic class BooleanConverter extends AbstractConverter<Boolean> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Boolean convertInternal(Object value) {\n\t\tif (value instanceof Number) {\n\t\t\t// 0为false，其它数字为true\n\t\t\treturn 0 != ((Number) value).doubleValue();\n\t\t}\n\t\t//Object不可能出现Primitive类型，故忽略\n\t\treturn BooleanUtil.toBoolean(convertToStr(value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 日期转换器\n *\n * @author Looly\n *\n */\npublic class CalendarConverter extends AbstractConverter<Calendar> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 日期格式化 */\n\tprivate String format;\n\n\t/**\n\t * 获取日期格式\n\t *\n\t * @return 设置日期格式\n\t */\n\tpublic String getFormat() {\n\t\treturn format;\n\t}\n\n\t/**\n\t * 设置日期格式\n\t *\n\t * @param format 日期格式\n\t */\n\tpublic void setFormat(String format) {\n\t\tthis.format = format;\n\t}\n\n\t@Override\n\tprotected Calendar convertInternal(Object value) {\n\t\t// Handle Date\n\t\tif (value instanceof Date) {\n\t\t\treturn DateUtil.calendar((Date)value);\n\t\t}\n\n\t\t// Handle Long\n\t\tif (value instanceof Long) {\n\t\t\t//此处使用自动拆装箱\n\t\t\treturn DateUtil.calendar((Long)value);\n\t\t}\n\n\t\tfinal String valueStr = convertToStr(value);\n\t\treturn DateUtil.calendar(StrUtil.isBlank(format) ? DateUtil.parse(valueStr) : DateUtil.parse(valueStr, format));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CastConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\n\n/**\n * 强转转换器\n *\n * @author Looly\n * @param <T> 强制转换到的类型\n * @since 4.0.2\n */\npublic class CastConverter<T> extends AbstractConverter<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Class<T> targetType;\n\n\t@Override\n\tprotected T convertInternal(Object value) {\n\t\t// 由于在AbstractConverter中已经有类型判断并强制转换，因此当在上一步强制转换失败时直接抛出异常\n\t\tthrow new ConvertException(\"Can not cast value to [{}]\", this.targetType);\n\t}\n\n\t@Override\n\tpublic Class<T> getTargetType() {\n\t\treturn this.targetType;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.BooleanUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 字符转换器\n *\n * @author Looly\n *\n */\npublic class CharacterConverter extends AbstractConverter<Character> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Character convertInternal(Object value) {\n\t\tif (value instanceof Boolean) {\n\t\t\treturn BooleanUtil.toCharacter((Boolean) value);\n\t\t} else {\n\t\t\tfinal String valueStr = convertToStr(value);\n\t\t\tif (StrUtil.isNotBlank(valueStr)) {\n\t\t\t\treturn valueStr.charAt(0);\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.nio.charset.Charset;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.CharsetUtil;\n\n/**\n * 编码对象转换器\n * @author Looly\n *\n */\npublic class CharsetConverter extends AbstractConverter<Charset>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Charset convertInternal(Object value) {\n\t\treturn CharsetUtil.charset(convertToStr(value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.ClassLoaderUtil;\n\n/**\n * 类转换器<br>\n * 将类名转换为类，默认初始化这个类（执行static块）\n *\n * @author Looly\n */\npublic class ClassConverter extends AbstractConverter<Class<?>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final boolean isInitialized;\n\n\t/**\n\t * 构造\n\t */\n\tpublic ClassConverter() {\n\t\tthis(true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isInitialized 是否初始化类（调用static模块内容和初始化static属性）\n\t * @since 5.5.0\n\t */\n\tpublic ClassConverter(boolean isInitialized) {\n\t\tthis.isInitialized = isInitialized;\n\t}\n\n\t@Override\n\tprotected Class<?> convertInternal(Object value) {\n\t\treturn ClassLoaderUtil.loadClass(convertToStr(value), isInitialized);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Converter;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Collection;\n\n/**\n * 各种集合类转换器\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class CollectionConverter implements Converter<Collection<?>> {\n\n\t/** 集合类型 */\n\tprivate final Type collectionType;\n\t/** 集合元素类型 */\n\tprivate final Type elementType;\n\n\t/**\n\t * 构造，默认集合类型使用{@link Collection}\n\t */\n\tpublic CollectionConverter() {\n\t\tthis(Collection.class);\n\t}\n\n\t// ---------------------------------------------------------------------------------------------- Constractor start\n\t/**\n\t * 构造\n\t *\n\t * @param collectionType 集合类型\n\t */\n\tpublic CollectionConverter(Type collectionType) {\n\t\tthis(collectionType, TypeUtil.getTypeArgument(collectionType));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param collectionType 集合类型\n\t */\n\tpublic CollectionConverter(Class<?> collectionType) {\n\t\tthis(collectionType, TypeUtil.getTypeArgument(collectionType));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param collectionType 集合类型\n\t * @param elementType 集合元素类型\n\t */\n\tpublic CollectionConverter(Type collectionType, Type elementType) {\n\t\tthis.collectionType = collectionType;\n\t\tthis.elementType = elementType;\n\t}\n\t// ---------------------------------------------------------------------------------------------- Constractor end\n\n\t@Override\n\tpublic Collection<?> convert(Object value, Collection<?> defaultValue) throws IllegalArgumentException {\n\t\tfinal Collection<?> result = convertInternal(value);\n\t\treturn ObjectUtil.defaultIfNull(result, defaultValue);\n\t}\n\n\t/**\n\t * 内部转换\n\t *\n\t * @param value 值\n\t * @return 转换后的集合对象\n\t */\n\tprotected Collection<?> convertInternal(Object value) {\n\t\tfinal Collection<?> collection = CollUtil.create(TypeUtil.getClass(this.collectionType), TypeUtil.getClass(this.elementType));\n\t\treturn CollUtil.addAll(collection, value, this.elementType);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.Currency;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\n/**\n * 货币{@link Currency} 转换器\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class CurrencyConverter extends AbstractConverter<Currency> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Currency convertInternal(Object value) {\n\t\treturn Currency.getInstance(convertToStr(value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/DateConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\n\n/**\n * 日期转换器\n *\n * @author Looly\n */\npublic class DateConverter extends AbstractConverter<java.util.Date> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<? extends java.util.Date> targetType;\n\t/**\n\t * 日期格式化\n\t */\n\tprivate String format;\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType 目标类型\n\t */\n\tpublic DateConverter(Class<? extends java.util.Date> targetType) {\n\t\tthis.targetType = targetType;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType 目标类型\n\t * @param format     日期格式\n\t */\n\tpublic DateConverter(Class<? extends java.util.Date> targetType, String format) {\n\t\tthis.targetType = targetType;\n\t\tthis.format = format;\n\t}\n\n\t/**\n\t * 获取日期格式\n\t *\n\t * @return 设置日期格式\n\t */\n\tpublic String getFormat() {\n\t\treturn format;\n\t}\n\n\t/**\n\t * 设置日期格式\n\t *\n\t * @param format 日期格式\n\t */\n\tpublic void setFormat(String format) {\n\t\tthis.format = format;\n\t}\n\n\t@Override\n\tprotected java.util.Date convertInternal(Object value) {\n\t\tif (value == null || (value instanceof CharSequence && StrUtil.isBlank(value.toString()))) {\n\t\t\treturn null;\n\t\t}\n\t\tif (value instanceof TemporalAccessor) {\n\t\t\treturn wrap(DateUtil.date((TemporalAccessor) value));\n\t\t} else if (value instanceof Calendar) {\n\t\t\treturn wrap(DateUtil.date((Calendar) value));\n\t\t} else if (value instanceof Number) {\n\t\t\treturn wrap(((Number) value).longValue());\n\t\t} else {\n\t\t\t// 统一按照字符串处理\n\t\t\tfinal String valueStr = convertToStr(value);\n\t\t\tfinal DateTime dateTime = StrUtil.isBlank(this.format) //\n\t\t\t\t\t? DateUtil.parse(valueStr) //\n\t\t\t\t\t: DateUtil.parse(valueStr, this.format);\n\t\t\tif (null != dateTime) {\n\t\t\t\treturn wrap(dateTime);\n\t\t\t}\n\t\t}\n\n\t\tthrow new ConvertException(\"Can not convert {}:[{}] to {}\", value.getClass().getName(), value, this.targetType.getName());\n\t}\n\n\t/**\n\t * java.util.Date转为子类型\n\t *\n\t * @param date Date\n\t * @return 目标类型对象\n\t */\n\tprivate java.util.Date wrap(DateTime date) {\n\t\t// 返回指定类型\n\t\tif (java.util.Date.class == targetType) {\n\t\t\treturn date.toJdkDate();\n\t\t}\n\t\tif (DateTime.class == targetType) {\n\t\t\treturn date;\n\t\t}\n\t\tif (java.sql.Date.class == targetType) {\n\t\t\treturn date.toSqlDate();\n\t\t}\n\t\tif (java.sql.Time.class == targetType) {\n\t\t\treturn new java.sql.Time(date.getTime());\n\t\t}\n\t\tif (java.sql.Timestamp.class == targetType) {\n\t\t\treturn date.toTimestamp();\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupported target Date type: {}\", this.targetType.getName()));\n\t}\n\n\t/**\n\t * java.util.Date转为子类型\n\t *\n\t * @param mills Date\n\t * @return 目标类型对象\n\t */\n\tprivate java.util.Date wrap(long mills) {\n\t\tif(GlobalCustomFormat.FORMAT_SECONDS.equals(this.format)){\n\t\t\t// Unix时间戳\n\t\t\treturn DateUtil.date(mills * 1000);\n\t\t}\n\n\t\t// 返回指定类型\n\t\tif (java.util.Date.class == targetType) {\n\t\t\treturn new java.util.Date(mills);\n\t\t}\n\t\tif (DateTime.class == targetType) {\n\t\t\treturn DateUtil.date(mills);\n\t\t}\n\t\tif (java.sql.Date.class == targetType) {\n\t\t\treturn new java.sql.Date(mills);\n\t\t}\n\t\tif (java.sql.Time.class == targetType) {\n\t\t\treturn new java.sql.Time(mills);\n\t\t}\n\t\tif (java.sql.Timestamp.class == targetType) {\n\t\t\treturn new java.sql.Timestamp(mills);\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupported target Date type: {}\", this.targetType.getName()));\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic Class<java.util.Date> getTargetType() {\n\t\treturn (Class<java.util.Date>) this.targetType;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/DurationConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\nimport java.time.Duration;\nimport java.time.temporal.TemporalAmount;\n\n/**\n *\n * {@link Duration}对象转换器\n *\n * @author Looly\n * @since 5.0.0\n */\npublic class DurationConverter extends AbstractConverter<Duration> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Duration convertInternal(Object value) {\n\t\tif(value instanceof TemporalAmount){\n\t\t\treturn Duration.from((TemporalAmount) value);\n\t\t} else if(value instanceof Long){\n\t\t\treturn Duration.ofMillis((Long) value);\n\t\t} else {\n\t\t\treturn Duration.parse(convertToStr(value));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/EntryConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\npublic class EntryConverter extends AbstractConverter<Map.Entry<?, ?>> {\n\n\t/** Pair类型 */\n\tprivate final Type pairType;\n\t/** 键类型 */\n\tprivate final Type keyType;\n\t/** 值类型 */\n\tprivate final Type valueType;\n\n\t/**\n\t * 构造，Pair的key和value泛型类型自动获取\n\t *\n\t * @param entryType Map类型\n\t */\n\tpublic EntryConverter(Type entryType) {\n\t\tthis(entryType, TypeUtil.getTypeArgument(entryType, 0), TypeUtil.getTypeArgument(entryType, 1));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param entryType Pair类型\n\t * @param keyType 键类型\n\t * @param valueType 值类型\n\t */\n\tpublic EntryConverter(Type entryType, Type keyType, Type valueType) {\n\t\tthis.pairType = entryType;\n\t\tthis.keyType = keyType;\n\t\tthis.valueType = valueType;\n\t}\n\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tprotected Map.Entry<?, ?> convertInternal(Object value) {\n\t\tMap map = null;\n\t\tif (value instanceof Pair) {\n\t\t\tfinal Pair pair = (Pair) value;\n\t\t\tmap = MapUtil.of(pair.getKey(), pair.getValue());\n\t\t}else if (value instanceof Map) {\n\t\t\tmap = (Map) value;\n\t\t} else if (value instanceof CharSequence) {\n\t\t\tfinal CharSequence str = (CharSequence) value;\n\t\t\tmap = strToMap(str);\n\t\t} else if (BeanUtil.isReadableBean(value.getClass())) {\n\t\t\tmap = BeanUtil.beanToMap(value);\n\t\t}\n\n\t\tif (null != map) {\n\t\t\treturn mapToEntry(pairType, keyType, valueType, map);\n\t\t}\n\n\t\tthrow new ConvertException(\"Unsupported to map from [{}] of type: {}\", value, value.getClass().getName());\n\t}\n\n\t/**\n\t * 字符串转单个键值对的Map，支持分隔符{@code :}、{@code =}、{@code ,}\n\t *\n\t * @param str 字符串\n\t * @return map or null\n\t */\n\tprivate static Map<CharSequence, CharSequence> strToMap(final CharSequence str) {\n\t\t// key:value  key=value  key,value\n\t\tfinal int index = StrUtil.indexOf(str, '=', 0, str.length());\n\n\t\tif (index > -1) {\n\t\t\treturn MapUtil.of(str.subSequence(0, index + 1), str.subSequence(index, str.length()));\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * Map转Entry\n\t *\n\t * @param targetType 目标的Map类型\n\t * @param keyType    键类型\n\t * @param valueType  值类型\n\t * @param map        被转换的map\n\t * @return Entry\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static Map.Entry<?, ?> mapToEntry(final Type targetType, final Type keyType, final Type valueType, final Map map) {\n\n\t\tObject key = null;\n\t\tObject value = null;\n\t\tif (1 == map.size()) {\n\t\t\tfinal Map.Entry entry = (Map.Entry) map.entrySet().iterator().next();\n\t\t\tkey = entry.getKey();\n\t\t\tvalue = entry.getValue();\n\t\t} else if (2 == map.size()) {\n\t\t\tkey = map.get(\"key\");\n\t\t\tvalue = map.get(\"value\");\n\t\t}\n\n\t\tfinal ConverterRegistry convert = ConverterRegistry.getInstance();\n\t\treturn (Map.Entry<?, ?>) ReflectUtil.newInstance(TypeUtil.getClass(targetType),\n\t\t\tTypeUtil.isUnknown(keyType) ? key : convert.convert(keyType, key),\n\t\t\tTypeUtil.isUnknown(valueType) ? value : convert.convert(valueType, value)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.lang.EnumItem;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.EnumUtil;\nimport cn.hutool.core.util.ModifierUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * 无泛型检查的枚举转换器\n *\n * @author Looly\n * @since 4.0.2\n */\n@SuppressWarnings({\"unchecked\", \"rawtypes\"})\npublic class EnumConverter extends AbstractConverter<Object> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final WeakKeyValueConcurrentMap<Class<?>, Map<Class<?>, Method>> VALUE_OF_METHOD_CACHE = new WeakKeyValueConcurrentMap<>();\n\n\tprivate final Class enumClass;\n\n\t/**\n\t * 构造\n\t *\n\t * @param enumClass 转换成的目标Enum类\n\t */\n\tpublic EnumConverter(Class enumClass) {\n\t\tthis.enumClass = enumClass;\n\t}\n\n\t@Override\n\tprotected Object convertInternal(Object value) {\n\t\tEnum enumValue = tryConvertEnum(value, this.enumClass);\n\t\tif (null == enumValue && false == value instanceof String) {\n\t\t\t// 最后尝试先将value转String，再valueOf转换\n\t\t\tenumValue = Enum.valueOf(this.enumClass, convertToStr(value));\n\t\t}\n\n\t\tif (null != enumValue) {\n\t\t\treturn enumValue;\n\t\t}\n\n\t\tthrow new ConvertException(\"Can not convert {} to {}\", value, this.enumClass);\n\t}\n\n\t@Override\n\tpublic Class getTargetType() {\n\t\treturn this.enumClass;\n\t}\n\n\t/**\n\t * 尝试转换，转换规则为：\n\t * <ul>\n\t *     <li>如果实现{@link EnumItem}接口，则调用fromInt或fromStr转换</li>\n\t *     <li>找到类似转换的静态方法调用实现转换且优先使用</li>\n\t *     <li>约定枚举类应该提供 valueOf(String) 和 valueOf(Integer)用于转换</li>\n\t *     <li>oriInt /name 转换托底</li>\n\t * </ul>\n\t *\n\t * @param value     被转换的值\n\t * @param enumClass enum类\n\t * @return 对应的枚举值\n\t */\n\tprotected static Enum tryConvertEnum(Object value, Class enumClass) {\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// EnumItem实现转换\n\t\tif (EnumItem.class.isAssignableFrom(enumClass)) {\n\t\t\tfinal EnumItem first = (EnumItem) EnumUtil.getEnumAt(enumClass, 0);\n\t\t\tif (null != first) {\n\t\t\t\tif (value instanceof Integer) {\n\t\t\t\t\treturn (Enum) first.fromInt((Integer) value);\n\t\t\t\t} else if (value instanceof String) {\n\t\t\t\t\treturn (Enum) first.fromStr(value.toString());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 用户自定义方法\n\t\t// 查找枚举中所有返回值为目标枚举对象的方法，如果发现方法参数匹配，就执行之\n\t\ttry {\n\t\t\tfinal Map<Class<?>, Method> methodMap = getMethodMap(enumClass);\n\t\t\tif (MapUtil.isNotEmpty(methodMap)) {\n\t\t\t\tfinal Class<?> valueClass = value.getClass();\n\t\t\t\tfor (Map.Entry<Class<?>, Method> entry : methodMap.entrySet()) {\n\t\t\t\t\tif (ClassUtil.isAssignable(entry.getKey(), valueClass)) {\n\t\t\t\t\t\treturn ReflectUtil.invokeStatic(entry.getValue(), value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception ignore) {\n\t\t\t//ignore\n\t\t}\n\n\t\t//oriInt 应该滞后使用 以 GB/T 2261.1-2003 性别编码为例，对应整数并非连续数字会导致数字转枚举时失败\n\t\t//0 - 未知的性别\n\t\t//1 - 男性\n\t\t//2 - 女性\n\t\t//5 - 女性改(变)为男性\n\t\t//6 - 男性改(变)为女性\n\t\t//9 - 未说明的性别\n\t\tEnum enumResult = null;\n\t\tif (value instanceof Integer) {\n\t\t\tenumResult = EnumUtil.getEnumAt(enumClass, (Integer) value);\n\t\t} else if (value instanceof String) {\n\t\t\ttry {\n\t\t\t\tenumResult = Enum.valueOf(enumClass, (String) value);\n\t\t\t} catch (IllegalArgumentException e) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\n\t\treturn enumResult;\n\t}\n\n\t/**\n\t * 获取用于转换为enum的所有static方法\n\t *\n\t * @param enumClass 枚举类\n\t * @return 转换方法map，key为方法参数类型，value为方法\n\t */\n\tprivate static Map<Class<?>, Method> getMethodMap(Class<?> enumClass) {\n\t\treturn VALUE_OF_METHOD_CACHE.computeIfAbsent(enumClass, (key) -> Arrays.stream(enumClass.getMethods())\n\t\t\t\t.filter(ModifierUtil::isStatic)\n\t\t\t\t.filter(m -> m.getReturnType() == enumClass)\n\t\t\t\t.filter(m -> m.getParameterCount() == 1)\n\t\t\t\t.filter(m -> false == \"valueOf\".equals(m.getName()))\n\t\t\t\t.collect(Collectors.toMap(m -> m.getParameterTypes()[0], m -> m, (k1, k2) -> k1)));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.Locale;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n *\n * {@link Locale}对象转换器<br>\n * 只提供String转换支持\n *\n * @author Looly\n * @since 4.5.2\n */\npublic class LocaleConverter extends AbstractConverter<Locale> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Locale convertInternal(Object value) {\n\t\ttry {\n\t\t\tString str = convertToStr(value);\n\t\t\tif (StrUtil.isEmpty(str)) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfinal String[] items = str.split(\"_\");\n\t\t\tif (items.length == 1) {\n\t\t\t\treturn new Locale(items[0]);\n\t\t\t}\n\t\t\tif (items.length == 2) {\n\t\t\t\treturn new Locale(items[0], items[1]);\n\t\t\t}\n\t\t\treturn new Locale(items[0], items[1], items[2]);\n\t\t} catch (Exception e) {\n\t\t\t// Ignore Exception\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.*;\n\n/**\n * {@link Map} 转换器\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class MapConverter extends AbstractConverter<Map<?, ?>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** Map类型 */\n\tprivate final Type mapType;\n\t/** 键类型 */\n\tprivate final Type keyType;\n\t/** 值类型 */\n\tprivate final Type valueType;\n\n\t/**\n\t * 构造，Map的key和value泛型类型自动获取\n\t *\n\t * @param mapType Map类型\n\t */\n\tpublic MapConverter(Type mapType) {\n\t\tthis(mapType, TypeUtil.getTypeArgument(mapType, 0), TypeUtil.getTypeArgument(mapType, 1));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mapType Map类型\n\t * @param keyType 键类型\n\t * @param valueType 值类型\n\t */\n\tpublic MapConverter(Type mapType, Type keyType, Type valueType) {\n\t\tthis.mapType = mapType;\n\t\tthis.keyType = keyType;\n\t\tthis.valueType = valueType;\n\t}\n\n\t@Override\n\t@SuppressWarnings({ \"rawtypes\", \"unchecked\" })\n\tprotected Map<?, ?> convertInternal(Object value) {\n\t\tMap map;\n\t\tif (value instanceof Map) {\n\t\t\tfinal Class<?> valueClass = value.getClass();\n\t\t\tif(valueClass.equals(this.mapType)){\n\t\t\t\tfinal Type[] typeArguments = TypeUtil.getTypeArguments(valueClass);\n\t\t\t\tif (null != typeArguments //\n\t\t\t\t\t\t&& 2 == typeArguments.length//\n\t\t\t\t\t\t&& Objects.equals(this.keyType, typeArguments[0]) //\n\t\t\t\t\t\t&& Objects.equals(this.valueType, typeArguments[1])) {\n\t\t\t\t\t//对于键值对类型一致的Map对象，不再做转换，直接返回原对象\n\t\t\t\t\treturn (Map) value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfinal Class<?> mapClass = TypeUtil.getClass(this.mapType);\n\t\t\tif (null == mapClass || mapClass.isAssignableFrom(AbstractMap.class)) {\n\t\t\t\t// issue#I6YN2A，默认有序\n\t\t\t\tmap =  new LinkedHashMap<>();\n\t\t\t} else{\n\t\t\t\tmap = MapUtil.createMap(mapClass);\n\t\t\t}\n\t\t\tconvertMapToMap((Map) value, map);\n\t\t} else if (BeanUtil.isBean(value.getClass())) {\n\t\t\tif(value.getClass().getName().equals(\"cn.hutool.json.JSONArray\")){\n\t\t\t\t// issue#3795 增加JSONArray转Map错误检查\n\t\t\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupported {} to Map.\", value.getClass().getName()));\n\t\t\t}\n\n\t\t\tmap = BeanUtil.beanToMap(value);\n\t\t\t// 二次转换，转换键值类型\n\t\t\tmap = convertInternal(map);\n\t\t} else {\n\t\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupported toMap value type: {}\", value.getClass().getName()));\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * Map转Map\n\t *\n\t * @param srcMap 源Map\n\t * @param targetMap 目标Map\n\t */\n\tprivate void convertMapToMap(Map<?, ?> srcMap, Map<Object, Object> targetMap) {\n\t\tfinal ConverterRegistry convert = ConverterRegistry.getInstance();\n\t\tsrcMap.forEach((key, value)->{\n\t\t\tkey = TypeUtil.isUnknown(this.keyType) ? key : convert.convert(this.keyType, key);\n\t\t\tvalue = TypeUtil.isUnknown(this.valueType) ? value : convert.convert(this.valueType, value);\n\t\t\ttargetMap.put(key, value);\n\t\t});\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic Class<Map<?, ?>> getTargetType() {\n\t\treturn (Class<Map<?, ?>>) TypeUtil.getClass(this.mapType);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.BooleanUtil;\nimport cn.hutool.core.util.ByteUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.DoubleAdder;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.function.Function;\n\n/**\n * 数字转换器<br>\n * 支持类型为：<br>\n * <ul>\n * <li>{@code java.lang.Byte}</li>\n * <li>{@code java.lang.Short}</li>\n * <li>{@code java.lang.Integer}</li>\n * <li>{@code java.util.concurrent.atomic.AtomicInteger}</li>\n * <li>{@code java.lang.Long}</li>\n * <li>{@code java.util.concurrent.atomic.AtomicLong}</li>\n * <li>{@code java.lang.Float}</li>\n * <li>{@code java.lang.Double}</li>\n * <li>{@code java.math.BigDecimal}</li>\n * <li>{@code java.math.BigInteger}</li>\n * </ul>\n *\n * @author Looly\n */\npublic class NumberConverter extends AbstractConverter<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<? extends Number> targetType;\n\n\t/**\n\t * 构造\n\t */\n\tpublic NumberConverter() {\n\t\tthis.targetType = Number.class;\n\t}\n\n\t/**\n\t * 构造<br>\n\t *\n\t * @param clazz 需要转换的数字类型，默认 {@link Number}\n\t */\n\tpublic NumberConverter(Class<? extends Number> clazz) {\n\t\tthis.targetType = (null == clazz) ? Number.class : clazz;\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic Class<Number> getTargetType() {\n\t\treturn (Class<Number>) this.targetType;\n\t}\n\n\t@Override\n\tprotected Number convertInternal(Object value) {\n\t\treturn convert(value, this.targetType, this::convertToStr);\n\t}\n\n\t@Override\n\tprotected String convertToStr(Object value) {\n\t\tfinal String result = StrUtil.trim(super.convertToStr(value));\n\t\tif (null != result && result.length() > 1) {\n\t\t\tfinal char c = Character.toUpperCase(result.charAt(result.length() - 1));\n\t\t\tif (c == 'D' || c == 'L' || c == 'F') {\n\t\t\t\t// 类型标识形式（例如123.6D）\n\t\t\t\treturn StrUtil.subPre(result, -1);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 转换对象为数字，支持的对象包括：\n\t * <ul>\n\t *     <li>Number对象</li>\n\t *     <li>Boolean</li>\n\t *     <li>byte[]</li>\n\t *     <li>String</li>\n\t * </ul>\n\t *\n\t *\n\t * @param value      对象值\n\t * @param targetType 目标的数字类型\n\t * @param toStrFunc  转换为字符串的函数\n\t * @return 转换后的数字\n\t * @since 5.5.0\n\t */\n\tprotected static Number convert(Object value, Class<? extends Number> targetType, Function<Object, String> toStrFunc) {\n\t\t// 枚举转换为数字默认为其顺序\n\t\tif (value instanceof Enum) {\n\t\t\treturn convert(((Enum<?>) value).ordinal(), targetType, toStrFunc);\n\t\t}\n\n\t\t// since 5.7.18\n\t\tif(value instanceof byte[]){\n\t\t\treturn ByteUtil.bytesToNumber((byte[])value, targetType, ByteUtil.DEFAULT_ORDER);\n\t\t}\n\n\t\tif (Byte.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn ((Number) value).byteValue();\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toByteObj((Boolean) value);\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply(value);\n\t\t\ttry{\n\t\t\t\treturn StrUtil.isBlank(valueStr) ? null : Byte.valueOf(valueStr);\n\t\t\t} catch (NumberFormatException e){\n\t\t\t\treturn NumberUtil.parseNumber(valueStr).byteValue();\n\t\t\t}\n\t\t} else if (Short.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn ((Number) value).shortValue();\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toShortObj((Boolean) value);\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\ttry{\n\t\t\t\treturn StrUtil.isBlank(valueStr) ? null : Short.valueOf(valueStr);\n\t\t\t} catch (NumberFormatException e){\n\t\t\t\treturn NumberUtil.parseNumber(valueStr).shortValue();\n\t\t\t}\n\t\t} else if (Integer.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn ((Number) value).intValue();\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toInteger((Boolean) value);\n\t\t\t} else if (value instanceof Date) {\n\t\t\t\treturn (int) ((Date) value).getTime();\n\t\t\t} else if (value instanceof Calendar) {\n\t\t\t\treturn (int) ((Calendar) value).getTimeInMillis();\n\t\t\t} else if (value instanceof TemporalAccessor) {\n\t\t\t\treturn (int) DateUtil.toInstant((TemporalAccessor) value).toEpochMilli();\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\treturn StrUtil.isBlank(valueStr) ? null : NumberUtil.parseInt(valueStr);\n\t\t} else if (AtomicInteger.class == targetType) {\n\t\t\tfinal Number number = convert(value, Integer.class, toStrFunc);\n\t\t\tif (null != number) {\n\t\t\t\treturn new AtomicInteger(number.intValue());\n\t\t\t}\n\t\t} else if (Long.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn ((Number) value).longValue();\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toLongObj((Boolean) value);\n\t\t\t} else if (value instanceof Date) {\n\t\t\t\treturn ((Date) value).getTime();\n\t\t\t} else if (value instanceof Calendar) {\n\t\t\t\treturn ((Calendar) value).getTimeInMillis();\n\t\t\t} else if (value instanceof TemporalAccessor) {\n\t\t\t\treturn DateUtil.toInstant((TemporalAccessor) value).toEpochMilli();\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\treturn StrUtil.isBlank(valueStr) ? null : NumberUtil.parseLong(valueStr);\n\t\t} else if (AtomicLong.class == targetType) {\n\t\t\tfinal Number number = convert(value, Long.class, toStrFunc);\n\t\t\tif (null != number) {\n\t\t\t\treturn new AtomicLong(number.longValue());\n\t\t\t}\n\t\t} else if (LongAdder.class == targetType) {\n\t\t\t//jdk8 新增\n\t\t\tfinal Number number = convert(value, Long.class, toStrFunc);\n\t\t\tif (null != number) {\n\t\t\t\tfinal LongAdder longValue = new LongAdder();\n\t\t\t\tlongValue.add(number.longValue());\n\t\t\t\treturn longValue;\n\t\t\t}\n\t\t} else if (Float.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn ((Number) value).floatValue();\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toFloatObj((Boolean) value);\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\treturn StrUtil.isBlank(valueStr) ? null : NumberUtil.parseFloat(valueStr);\n\t\t} else if (Double.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn NumberUtil.toDouble((Number) value);\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toDoubleObj((Boolean) value);\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\treturn StrUtil.isBlank(valueStr) ? null : NumberUtil.parseDouble(valueStr);\n\t\t} else if (DoubleAdder.class == targetType) {\n\t\t\t//jdk8 新增\n\t\t\tfinal Number number = convert(value, Double.class, toStrFunc);\n\t\t\tif (null != number) {\n\t\t\t\tfinal DoubleAdder doubleAdder = new DoubleAdder();\n\t\t\t\tdoubleAdder.add(number.doubleValue());\n\t\t\t\treturn doubleAdder;\n\t\t\t}\n\t\t} else if (BigDecimal.class == targetType) {\n\t\t\treturn toBigDecimal(value, toStrFunc);\n\t\t} else if (BigInteger.class == targetType) {\n\t\t\treturn toBigInteger(value, toStrFunc);\n\t\t} else if (Number.class == targetType) {\n\t\t\tif (value instanceof Number) {\n\t\t\t\treturn (Number) value;\n\t\t\t} else if (value instanceof Boolean) {\n\t\t\t\treturn BooleanUtil.toInteger((Boolean) value);\n\t\t\t}\n\t\t\tfinal String valueStr = toStrFunc.apply((value));\n\t\t\treturn StrUtil.isBlank(valueStr) ? null : NumberUtil.parseNumber(valueStr);\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupport Number type: {}\", targetType.getName()));\n\t}\n\n\t/**\n\t * 转换为BigDecimal<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value     被转换的值\n\t * @param toStrFunc 转换为字符串的函数规则\n\t * @return 结果\n\t */\n\tprivate static BigDecimal toBigDecimal(Object value, Function<Object, String> toStrFunc) {\n\t\tif (value instanceof Number) {\n\t\t\treturn NumberUtil.toBigDecimal((Number) value);\n\t\t} else if (value instanceof Boolean) {\n\t\t\treturn ((boolean) value) ? BigDecimal.ONE : BigDecimal.ZERO;\n\t\t}\n\n\t\t//对于Double类型，先要转换为String，避免精度问题\n\t\treturn NumberUtil.toBigDecimal(toStrFunc.apply(value));\n\t}\n\n\t/**\n\t * 转换为BigInteger<br>\n\t * 如果给定的值为空，或者转换失败，返回默认值<br>\n\t * 转换失败不会报错\n\t *\n\t * @param value     被转换的值\n\t * @param toStrFunc 转换为字符串的函数规则\n\t * @return 结果\n\t */\n\tprivate static BigInteger toBigInteger(Object value, Function<Object, String> toStrFunc) {\n\t\tif (value instanceof Long) {\n\t\t\treturn BigInteger.valueOf((Long) value);\n\t\t} else if (value instanceof Boolean) {\n\t\t\treturn (boolean) value ? BigInteger.ONE : BigInteger.ZERO;\n\t\t}\n\n\t\treturn NumberUtil.toBigInteger(toStrFunc.apply(value));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/OptConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.lang.Opt;\n\n/**\n *\n * {@link Opt}对象转换器\n *\n * @author Looly\n * @since 5.7.16\n */\npublic class OptConverter extends AbstractConverter<Opt<?>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Opt<?> convertInternal(Object value) {\n\t\treturn Opt.ofNullable(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/OptionalConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\nimport java.util.Optional;\n\n/**\n *\n * {@link Optional}对象转换器\n *\n * @author Looly\n * @since 5.0.0\n */\npublic class OptionalConverter extends AbstractConverter<Optional<?>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Optional<?> convertInternal(Object value) {\n\t\treturn Optional.ofNullable(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/PairConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\npublic class PairConverter extends AbstractConverter<Pair<?, ?>> {\n\n\t/** Pair类型 */\n\tprivate final Type pairType;\n\t/** 键类型 */\n\tprivate final Type keyType;\n\t/** 值类型 */\n\tprivate final Type valueType;\n\n\t/**\n\t * 构造，Pair的key和value泛型类型自动获取\n\t *\n\t * @param pairType Map类型\n\t */\n\tpublic PairConverter(Type pairType) {\n\t\tthis(pairType, null, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pairType Pair类型\n\t * @param keyType 键类型\n\t * @param valueType 值类型\n\t */\n\tpublic PairConverter(Type pairType, Type keyType, Type valueType) {\n\t\tthis.pairType = pairType;\n\t\tthis.keyType = keyType;\n\t\tthis.valueType = valueType;\n\t}\n\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tprotected Pair<?, ?> convertInternal(Object value) {\n\t\tMap map = null;\n\t\tif (value instanceof Map.Entry) {\n\t\t\tfinal Map.Entry entry = (Map.Entry) value;\n\t\t\tmap = MapUtil.of(entry.getKey(), entry.getValue());\n\t\t}else if (value instanceof Map) {\n\t\t\tmap = (Map) value;\n\t\t} else if (value instanceof CharSequence) {\n\t\t\tfinal CharSequence str = (CharSequence) value;\n\t\t\tmap = strToMap(str);\n\t\t} else if (BeanUtil.isReadableBean(value.getClass())) {\n\t\t\tmap = BeanUtil.beanToMap(value);\n\t\t}\n\n\t\tif (null != map) {\n\t\t\treturn mapToPair(pairType, keyType, valueType, map);\n\t\t}\n\n\t\tthrow new ConvertException(\"Unsupported to map from [{}] of type: {}\", value, value.getClass().getName());\n\t}\n\n\t/**\n\t * 字符串转单个键值对的Map，支持分隔符{@code :}、{@code =}、{@code ,}\n\t *\n\t * @param str 字符串\n\t * @return map or null\n\t */\n\tprivate static Map<CharSequence, CharSequence> strToMap(final CharSequence str) {\n\t\t// key:value  key=value  key,value\n\t\tfinal int index = StrUtil.indexOf(str, '=', 0, str.length());\n\n\t\tif (index > -1) {\n\t\t\treturn MapUtil.of(str.subSequence(0, index + 1), str.subSequence(index, str.length()));\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * Map转Entry\n\t *\n\t * @param targetType 目标的Map类型\n\t * @param keyType    键类型\n\t * @param valueType  值类型\n\t * @param map        被转换的map\n\t * @return Entry\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static Pair<?, ?> mapToPair(final Type targetType, final Type keyType, final Type valueType, final Map map) {\n\n\t\tObject key = null;\n\t\tObject value = null;\n\t\tif (1 == map.size()) {\n\t\t\tfinal Map.Entry entry = (Map.Entry) map.entrySet().iterator().next();\n\t\t\tkey = entry.getKey();\n\t\t\tvalue = entry.getValue();\n\t\t} else if (2 == map.size()) {\n\t\t\tkey = map.get(\"key\");\n\t\t\tvalue = map.get(\"value\");\n\t\t}\n\n\t\tfinal ConverterRegistry convert = ConverterRegistry.getInstance();\n\t\treturn (Pair<?, ?>) ReflectUtil.newInstance(TypeUtil.getClass(targetType),\n\t\t\tTypeUtil.isUnknown(keyType) ? key : convert.convert(keyType, key),\n\t\t\tTypeUtil.isUnknown(valueType) ? value : convert.convert(valueType, value)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/PathConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.io.File;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\n/**\n * 字符串转换器\n * @author Looly\n *\n */\npublic class PathConverter extends AbstractConverter<Path>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Path convertInternal(Object value) {\n\t\ttry {\n\t\t\tif(value instanceof URI){\n\t\t\t\treturn Paths.get((URI)value);\n\t\t\t}\n\n\t\t\tif(value instanceof URL){\n\t\t\t\treturn Paths.get(((URL)value).toURI());\n\t\t\t}\n\n\t\t\tif(value instanceof File){\n\t\t\t\treturn ((File)value).toPath();\n\t\t\t}\n\n\t\t\treturn Paths.get(convertToStr(value));\n\t\t} catch (Exception e) {\n\t\t\t// Ignore Exception\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/PeriodConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\nimport java.time.Period;\nimport java.time.temporal.TemporalAmount;\n\n/**\n *\n * {@link Period}对象转换器\n *\n * @author Looly\n * @since 5.0.0\n */\npublic class PeriodConverter extends AbstractConverter<Period> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected Period convertInternal(Object value) {\n\t\tif(value instanceof TemporalAmount){\n\t\t\treturn Period.from((TemporalAmount) value);\n\t\t}else if(value instanceof Integer){\n\t\t\treturn Period.ofDays((Integer) value);\n\t\t} else {\n\t\t\treturn Period.parse(convertToStr(value));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.function.Function;\n\n/**\n * 原始类型转换器<br>\n * 支持类型为：<br>\n * <ul>\n * \t\t<li>{@code byte}</li>\n * \t\t<li>{@code short}</li>\n * \t\t <li>{@code int}</li>\n * \t\t <li>{@code long}</li>\n * \t\t<li>{@code float}</li>\n * \t\t<li>{@code double}</li>\n * \t\t<li>{@code char}</li>\n * \t\t<li>{@code boolean}</li>\n * </ul>\n *\n * @author Looly\n */\npublic class PrimitiveConverter extends AbstractConverter<Object> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<?> targetType;\n\n\t/**\n\t * 构造<br>\n\t *\n\t * @param clazz 需要转换的原始\n\t * @throws IllegalArgumentException 传入的转换类型非原始类型时抛出\n\t */\n\tpublic PrimitiveConverter(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\tthrow new NullPointerException(\"PrimitiveConverter not allow null target type!\");\n\t\t} else if (false == clazz.isPrimitive()) {\n\t\t\tthrow new IllegalArgumentException(\"[\" + clazz + \"] is not a primitive class!\");\n\t\t}\n\t\tthis.targetType = clazz;\n\t}\n\n\t@Override\n\tprotected Object convertInternal(Object value) {\n\t\treturn PrimitiveConverter.convert(value, this.targetType, this::convertToStr);\n\t}\n\n\t@Override\n\tprotected String convertToStr(Object value) {\n\t\treturn StrUtil.trim(super.convertToStr(value));\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic Class<Object> getTargetType() {\n\t\treturn (Class<Object>) this.targetType;\n\t}\n\n\t/**\n\t * 将指定值转换为原始类型的值\n\t * @param value 值\n\t * @param primitiveClass 原始类型\n\t * @param toStringFunc 当无法直接转换时，转为字符串后再转换的函数\n\t * @return 转换结果\n\t * @since 5.5.0\n\t */\n\tprotected static Object convert(Object value, Class<?> primitiveClass, Function<Object, String> toStringFunc) {\n\t\tif (byte.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Byte.class, toStringFunc), 0);\n\t\t} else if (short.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Short.class, toStringFunc), 0);\n\t\t} else if (int.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Integer.class, toStringFunc), 0);\n\t\t} else if (long.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Long.class, toStringFunc), 0);\n\t\t} else if (float.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Float.class, toStringFunc), 0);\n\t\t} else if (double.class == primitiveClass) {\n\t\t\treturn ObjectUtil.defaultIfNull(NumberConverter.convert(value, Double.class, toStringFunc), 0);\n\t\t} else if (char.class == primitiveClass) {\n\t\t\treturn Convert.convert(Character.class, value);\n\t\t} else if (boolean.class == primitiveClass) {\n\t\t\treturn Convert.convert(Boolean.class, value);\n\t\t}\n\n\t\tthrow new ConvertException(\"Unsupported target type: {}\", primitiveClass);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/RecordConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.RecordUtil;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.bean.copier.provider.BeanValueProvider;\nimport cn.hutool.core.bean.copier.provider.MapValueProvider;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.convert.Converter;\n\nimport java.util.Map;\n\n/**\n * Record转换器\n *\n * @author looly\n */\npublic class RecordConverter implements Converter<Object> {\n\n\tprivate final Class<?> recordClass;\n\n\t/**\n\t * 构造\n\t * @param recordClass Record类\n\t */\n\tpublic RecordConverter(Class<?> recordClass) {\n\t\tthis.recordClass = recordClass;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic Object convert(Object value, Object defaultValue) throws IllegalArgumentException {\n\t\tValueProvider<String> valueProvider = null;\n\t\tif (value instanceof ValueProvider) {\n\t\t\tvalueProvider = (ValueProvider<String>) value;\n\t\t} else if (value instanceof Map) {\n\t\t\tvalueProvider = new MapValueProvider((Map<String, ?>) value);\n\t\t} else if (BeanUtil.isReadableBean(value.getClass())) {\n\t\t\tvalueProvider = new BeanValueProvider(value, false, false);\n\t\t}\n\n\t\tif (null != valueProvider) {\n\t\t\treturn RecordUtil.newInstance(recordClass, valueProvider);\n\t\t}\n\n\t\tthrow new ConvertException(\"Unsupported source type: [{}] to [{}]\", value.getClass(), recordClass);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.ref.Reference;\nimport java.lang.ref.SoftReference;\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Type;\n\n/**\n * {@link Reference}转换器\n *\n * @author Looly\n * @since 3.0.8\n */\n@SuppressWarnings(\"rawtypes\")\npublic class ReferenceConverter extends AbstractConverter<Reference> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<? extends Reference> targetType;\n\n\t/**\n\t * 构造\n\t * @param targetType {@link Reference}实现类型\n\t */\n\tpublic ReferenceConverter(Class<? extends Reference> targetType) {\n\t\tthis.targetType = targetType;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tprotected Reference<?> convertInternal(Object value) {\n\n\t\t//尝试将值转换为Reference泛型的类型\n\t\tObject targetValue = null;\n\t\tfinal Type paramType = TypeUtil.getTypeArgument(targetType);\n\t\tif(false == TypeUtil.isUnknown(paramType)){\n\t\t\ttargetValue = ConverterRegistry.getInstance().convert(paramType, value);\n\t\t}\n\t\tif(null == targetValue){\n\t\t\ttargetValue = value;\n\t\t}\n\n\t\tif(this.targetType == WeakReference.class){\n\t\t\treturn new WeakReference(targetValue);\n\t\t}else if(this.targetType == SoftReference.class){\n\t\t\treturn new SoftReference(targetValue);\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(StrUtil.format(\"Unsupport Reference type: {}\", this.targetType.getName()));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.Map;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\n/**\n * {@link StackTraceElement} 转换器<br>\n * 只支持Map方式转换\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class StackTraceElementConverter extends AbstractConverter<StackTraceElement> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected StackTraceElement convertInternal(Object value) {\n\t\tif (value instanceof Map) {\n\t\t\tfinal Map<?, ?> map = (Map<?, ?>) value;\n\n\t\t\tfinal String declaringClass = MapUtil.getStr(map, \"className\");\n\t\t\tfinal String methodName = MapUtil.getStr(map, \"methodName\");\n\t\t\tfinal String fileName = MapUtil.getStr(map, \"fileName\");\n\t\t\tfinal Integer lineNumber = MapUtil.getInt(map, \"lineNumber\");\n\n\t\t\treturn new StackTraceElement(declaringClass, methodName, fileName, ObjectUtil.defaultIfNull(lineNumber, 0));\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/StringConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.XmlUtil;\n\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.lang.reflect.Type;\nimport java.sql.Blob;\nimport java.sql.Clob;\nimport java.sql.SQLException;\nimport java.util.TimeZone;\n\n/**\n * 字符串转换器，提供各种对象转换为字符串的逻辑封装\n *\n * @author Looly\n */\npublic class StringConverter extends AbstractConverter<String> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected String convertInternal(Object value) {\n\t\tif (value instanceof TimeZone) {\n\t\t\treturn ((TimeZone) value).getID();\n\t\t} else if (value instanceof org.w3c.dom.Node) {\n\t\t\treturn XmlUtil.toStr((org.w3c.dom.Node) value);\n\t\t} else if (value instanceof Clob) {\n\t\t\treturn clobToStr((Clob) value);\n\t\t} else if (value instanceof Blob) {\n\t\t\treturn blobToStr((Blob) value);\n\t\t} else if (value instanceof Type) {\n\t\t\treturn ((Type) value).getTypeName();\n\t\t}\n\n\t\t// 其它情况\n\t\treturn convertToStr(value);\n\t}\n\n\t/**\n\t * Clob字段值转字符串\n\t *\n\t * @param clob {@link Clob}\n\t * @return 字符串\n\t * @since 5.4.5\n\t */\n\tprivate static String clobToStr(Clob clob) {\n\t\tReader reader = null;\n\t\ttry {\n\t\t\treader = clob.getCharacterStream();\n\t\t\treturn IoUtil.read(reader);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new ConvertException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t/**\n\t * Blob字段值转字符串\n\t *\n\t * @param blob    {@link Blob}\n\t * @return 字符串\n\t * @since 5.4.5\n\t */\n\tprivate static String blobToStr(Blob blob) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = blob.getBinaryStream();\n\t\t\treturn IoUtil.read(in, CharsetUtil.CHARSET_UTF_8);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new ConvertException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/TemporalAccessorConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.DayOfWeek;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.Month;\nimport java.time.MonthDay;\nimport java.time.OffsetDateTime;\nimport java.time.OffsetTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.chrono.Era;\nimport java.time.chrono.IsoEra;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * JDK8中新加入的java.time包对象解析转换器<br>\n * 支持的对象包括：\n *\n * <pre>\n * java.time.Instant\n * java.time.LocalDateTime\n * java.time.LocalDate\n * java.time.LocalTime\n * java.time.ZonedDateTime\n * java.time.OffsetDateTime\n * java.time.OffsetTime\n * </pre>\n *\n * @author looly\n * @since 5.0.0\n */\npublic class TemporalAccessorConverter extends AbstractConverter<TemporalAccessor> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Class<?> targetType;\n\t/**\n\t * 日期格式化\n\t */\n\tprivate String format;\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType 目标类型\n\t */\n\tpublic TemporalAccessorConverter(Class<?> targetType) {\n\t\tthis(targetType, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param targetType 目标类型\n\t * @param format     日期格式\n\t */\n\tpublic TemporalAccessorConverter(Class<?> targetType, String format) {\n\t\tthis.targetType = targetType;\n\t\tthis.format = format;\n\t}\n\n\t/**\n\t * 获取日期格式\n\t *\n\t * @return 设置日期格式\n\t */\n\tpublic String getFormat() {\n\t\treturn format;\n\t}\n\n\t/**\n\t * 设置日期格式\n\t *\n\t * @param format 日期格式\n\t */\n\tpublic void setFormat(String format) {\n\t\tthis.format = format;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic Class<TemporalAccessor> getTargetType() {\n\t\treturn (Class<TemporalAccessor>) this.targetType;\n\t}\n\n\t@Override\n\tprotected TemporalAccessor convertInternal(Object value) {\n\t\tif (value instanceof Number) {\n\t\t\treturn parseFromLong(((Number) value).longValue());\n\t\t} else if (value instanceof TemporalAccessor) {\n\t\t\treturn parseFromTemporalAccessor((TemporalAccessor) value);\n\t\t} else if (value instanceof Date) {\n\t\t\tfinal DateTime dateTime = DateUtil.date((Date) value);\n\t\t\treturn parseFromInstant(dateTime.toInstant(), dateTime.getZoneId());\n\t\t} else if (value instanceof Calendar) {\n\t\t\tfinal Calendar calendar = (Calendar) value;\n\t\t\treturn parseFromInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());\n\t\t} else if (value instanceof Map) {\n\t\t\tfinal Map<?, ?> map = (Map<?, ?>) value;\n\t\t\tif (LocalDate.class.equals(this.targetType)) {\n\t\t\t\treturn LocalDate.of(Convert.toInt(map.get(\"year\")), Convert.toInt(map.get(\"month\")), Convert.toInt(map.get(\"day\")));\n\t\t\t} else if (LocalDateTime.class.equals(this.targetType)) {\n\t\t\t\treturn LocalDateTime.of(Convert.toInt(map.get(\"year\")), Convert.toInt(map.get(\"month\")), Convert.toInt(map.get(\"day\")),\n\t\t\t\t\tConvert.toInt(map.get(\"hour\")), Convert.toInt(map.get(\"minute\")), Convert.toInt(map.get(\"second\")), Convert.toInt(map.get(\"second\")));\n\t\t\t} else if (LocalTime.class.equals(this.targetType)) {\n\t\t\t\treturn LocalTime.of(Convert.toInt(map.get(\"hour\")), Convert.toInt(map.get(\"minute\")), Convert.toInt(map.get(\"second\")), Convert.toInt(map.get(\"nano\")));\n\t\t\t}\n\t\t\tthrow new ConvertException(\"Unsupported type: [{}] from map: [{}]\", this.targetType, map);\n\t\t} else {\n\t\t\treturn parseFromCharSequence(convertToStr(value));\n\t\t}\n\t}\n\n\t/**\n\t * 通过反射从字符串转java.time中的对象\n\t *\n\t * @param value 字符串值\n\t * @return 日期对象\n\t */\n\tprivate TemporalAccessor parseFromCharSequence(CharSequence value) {\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (DayOfWeek.class.equals(this.targetType)) {\n\t\t\treturn DayOfWeek.valueOf(StrUtil.toString(value));\n\t\t} else if (Month.class.equals(this.targetType)) {\n\t\t\treturn Month.valueOf(StrUtil.toString(value));\n\t\t} else if (Era.class.equals(this.targetType)) {\n\t\t\treturn IsoEra.valueOf(StrUtil.toString(value));\n\t\t} else if (MonthDay.class.equals(this.targetType)) {\n\t\t\treturn MonthDay.parse(value);\n\t\t}\n\n\t\tfinal Instant instant;\n\t\tZoneId zoneId;\n\t\tif (null != this.format) {\n\t\t\tfinal DateTimeFormatter formatter = DateTimeFormatter.ofPattern(this.format);\n\n\t\t\t// issue#I9HQQE\n\t\t\tfinal TemporalAccessor temporalAccessor = parseWithFormat(this.targetType, value, formatter);\n\t\t\tif (null != temporalAccessor) {\n\t\t\t\treturn temporalAccessor;\n\t\t\t}\n\n\t\t\tinstant = formatter.parse(value, Instant::from);\n\t\t\tzoneId = formatter.getZone();\n\t\t} else {\n\t\t\tfinal DateTime dateTime = DateUtil.parse(value);\n\t\t\tinstant = Objects.requireNonNull(dateTime).toInstant();\n\t\t\tzoneId = dateTime.getZoneId();\n\t\t}\n\t\treturn parseFromInstant(instant, zoneId);\n\t}\n\n\t/**\n\t * 对于自定义格式的字符串，单独解析为{@link TemporalAccessor}\n\t *\n\t * @param targetClass 目标类型\n\t * @param value       日期字符串\n\t * @param formatter   格式\n\t * @return {@link TemporalAccessor}\n\t */\n\tprivate TemporalAccessor parseWithFormat(final Class<?> targetClass, final CharSequence value, final DateTimeFormatter formatter) {\n\t\t// issue#I9HQQE\n\t\tif (LocalDate.class == targetClass) {\n\t\t\treturn LocalDate.parse(value, formatter);\n\t\t} else if (LocalDateTime.class == targetClass) {\n\t\t\treturn LocalDateTime.parse(value, formatter);\n\t\t} else if (LocalTime.class == targetClass) {\n\t\t\treturn LocalTime.parse(value, formatter);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将Long型时间戳转换为java.time中的对象\n\t *\n\t * @param time 时间戳\n\t * @return java.time中的对象\n\t */\n\tprivate TemporalAccessor parseFromLong(Long time) {\n\t\tif (DayOfWeek.class.equals(this.targetType)) {\n\t\t\treturn DayOfWeek.of(Math.toIntExact(time));\n\t\t} else if (Month.class.equals(this.targetType)) {\n\t\t\treturn Month.of(Math.toIntExact(time));\n\t\t} else if (Era.class.equals(this.targetType)) {\n\t\t\treturn IsoEra.of(Math.toIntExact(time));\n\t\t}\n\n\t\tfinal Instant instant;\n\t\tif (GlobalCustomFormat.FORMAT_SECONDS.equals(this.format)) {\n\t\t\t// https://gitee.com/chinabugotech/hutool/issues/I6IS5B\n\t\t\t// Unix时间戳\n\t\t\tinstant = Instant.ofEpochSecond(time);\n\t\t} else {\n\t\t\tinstant = Instant.ofEpochMilli(time);\n\t\t}\n\t\treturn parseFromInstant(instant, null);\n\t}\n\n\t/**\n\t * 将TemporalAccessor型时间戳转换为java.time中的对象\n\t *\n\t * @param temporalAccessor TemporalAccessor对象\n\t * @return java.time中的对象\n\t */\n\tprivate TemporalAccessor parseFromTemporalAccessor(TemporalAccessor temporalAccessor) {\n\t\tif (DayOfWeek.class.equals(this.targetType)) {\n\t\t\treturn DayOfWeek.from(temporalAccessor);\n\t\t} else if (Month.class.equals(this.targetType)) {\n\t\t\treturn Month.from(temporalAccessor);\n\t\t} else if (MonthDay.class.equals(this.targetType)) {\n\t\t\treturn MonthDay.from(temporalAccessor);\n\t\t}\n\n\t\tTemporalAccessor result = null;\n\t\tif (temporalAccessor instanceof LocalDateTime) {\n\t\t\tresult = parseFromLocalDateTime((LocalDateTime) temporalAccessor);\n\t\t} else if (temporalAccessor instanceof ZonedDateTime) {\n\t\t\tresult = parseFromZonedDateTime((ZonedDateTime) temporalAccessor);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tresult = parseFromInstant(DateUtil.toInstant(temporalAccessor), null);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将TemporalAccessor型时间戳转换为java.time中的对象\n\t *\n\t * @param localDateTime {@link LocalDateTime}对象\n\t * @return java.time中的对象\n\t */\n\tprivate TemporalAccessor parseFromLocalDateTime(LocalDateTime localDateTime) {\n\t\tif (Instant.class.equals(this.targetType)) {\n\t\t\treturn DateUtil.toInstant(localDateTime);\n\t\t}\n\t\tif (LocalDate.class.equals(this.targetType)) {\n\t\t\treturn localDateTime.toLocalDate();\n\t\t}\n\t\tif (LocalTime.class.equals(this.targetType)) {\n\t\t\treturn localDateTime.toLocalTime();\n\t\t}\n\t\tif (ZonedDateTime.class.equals(this.targetType)) {\n\t\t\treturn localDateTime.atZone(ZoneId.systemDefault());\n\t\t}\n\t\tif (OffsetDateTime.class.equals(this.targetType)) {\n\t\t\treturn localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime();\n\t\t}\n\t\tif (OffsetTime.class.equals(this.targetType)) {\n\t\t\treturn localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime().toOffsetTime();\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将TemporalAccessor型时间戳转换为java.time中的对象\n\t *\n\t * @param zonedDateTime {@link ZonedDateTime}对象\n\t * @return java.time中的对象\n\t */\n\tprivate TemporalAccessor parseFromZonedDateTime(ZonedDateTime zonedDateTime) {\n\t\tif (Instant.class.equals(this.targetType)) {\n\t\t\treturn DateUtil.toInstant(zonedDateTime);\n\t\t}\n\t\tif (LocalDateTime.class.equals(this.targetType)) {\n\t\t\treturn zonedDateTime.toLocalDateTime();\n\t\t}\n\t\tif (LocalDate.class.equals(this.targetType)) {\n\t\t\treturn zonedDateTime.toLocalDate();\n\t\t}\n\t\tif (LocalTime.class.equals(this.targetType)) {\n\t\t\treturn zonedDateTime.toLocalTime();\n\t\t}\n\t\tif (OffsetDateTime.class.equals(this.targetType)) {\n\t\t\treturn zonedDateTime.toOffsetDateTime();\n\t\t}\n\t\tif (OffsetTime.class.equals(this.targetType)) {\n\t\t\treturn zonedDateTime.toOffsetDateTime().toOffsetTime();\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将TemporalAccessor型时间戳转换为java.time中的对象\n\t *\n\t * @param instant {@link Instant}对象\n\t * @param zoneId  时区ID，null表示当前系统默认的时区\n\t * @return java.time中的对象\n\t */\n\tprivate TemporalAccessor parseFromInstant(Instant instant, ZoneId zoneId) {\n\t\tif (Instant.class.equals(this.targetType)) {\n\t\t\treturn instant;\n\t\t}\n\n\t\tzoneId = ObjectUtil.defaultIfNull(zoneId, ZoneId::systemDefault);\n\n\t\tTemporalAccessor result = null;\n\t\tif (LocalDateTime.class.equals(this.targetType)) {\n\t\t\tresult = LocalDateTime.ofInstant(instant, zoneId);\n\t\t} else if (LocalDate.class.equals(this.targetType)) {\n\t\t\tresult = instant.atZone(zoneId).toLocalDate();\n\t\t} else if (LocalTime.class.equals(this.targetType)) {\n\t\t\tresult = instant.atZone(zoneId).toLocalTime();\n\t\t} else if (ZonedDateTime.class.equals(this.targetType)) {\n\t\t\tresult = instant.atZone(zoneId);\n\t\t} else if (OffsetDateTime.class.equals(this.targetType)) {\n\t\t\tresult = OffsetDateTime.ofInstant(instant, zoneId);\n\t\t} else if (OffsetTime.class.equals(this.targetType)) {\n\t\t\tresult = OffsetTime.ofInstant(instant, zoneId);\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport cn.hutool.core.convert.AbstractConverter;\nimport cn.hutool.core.date.TimeZoneUtil;\n\nimport java.util.TimeZone;\n\n/**\n * TimeZone转换器\n * @author Looly\n *\n */\npublic class TimeZoneConverter extends AbstractConverter<TimeZone>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected TimeZone convertInternal(Object value) {\n\t\treturn TimeZoneUtil.getTimeZone(convertToStr(value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/URIConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.io.File;\nimport java.net.URI;\nimport java.net.URL;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\n/**\n * URI对象转换器\n * @author Looly\n *\n */\npublic class URIConverter extends AbstractConverter<URI>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected URI convertInternal(Object value) {\n\t\ttry {\n\t\t\tif(value instanceof File){\n\t\t\t\treturn ((File)value).toURI();\n\t\t\t}\n\n\t\t\tif(value instanceof URL){\n\t\t\t\treturn ((URL)value).toURI();\n\t\t\t}\n\t\t\treturn new URI(convertToStr(value));\n\t\t} catch (Exception e) {\n\t\t\t// Ignore Exception\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/URLConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.io.File;\nimport java.net.URI;\nimport java.net.URL;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\n/**\n * URL对象转换器\n * @author Looly\n *\n */\npublic class URLConverter extends AbstractConverter<URL>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected URL convertInternal(Object value) {\n\t\ttry {\n\t\t\tif(value instanceof File){\n\t\t\t\treturn ((File)value).toURI().toURL();\n\t\t\t}\n\n\t\t\tif(value instanceof URI){\n\t\t\t\treturn ((URI)value).toURL();\n\t\t\t}\n\t\t\treturn new URL(convertToStr(value));\n\t\t} catch (Exception e) {\n\t\t\t// Ignore Exception\n\t\t}\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java",
    "content": "package cn.hutool.core.convert.impl;\n\nimport java.util.UUID;\n\nimport cn.hutool.core.convert.AbstractConverter;\n\n/**\n * UUID对象转换器转换器\n *\n * @author Looly\n * @since 4.0.10\n *\n */\npublic class UUIDConverter extends AbstractConverter<UUID> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected UUID convertInternal(Object value) {\n\t\treturn UUID.fromString(convertToStr(value));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/impl/package-info.java",
    "content": "/**\n * 各种类型转换的实现类，其都为Converter接口的实现，用于将未知的Object类型转换为指定类型\n *\n * @author looly\n *\n */\npackage cn.hutool.core.convert.impl;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/convert/package-info.java",
    "content": "/**\n * 万能类型转换器以及各种类型转换的实现类，其中Convert为转换器入口，提供各种toXXX方法和convert方法\n *\n * @author looly\n *\n */\npackage cn.hutool.core.convert;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/BetweenFormatter.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.function.Function;\n\n/**\n * 时长格式化器，用于格式化输出两个日期相差的时长<br>\n * 根据{@link Level}不同，调用{@link #format()}方法后返回类似于：\n * <ul>\n *    <li>XX小时XX分XX秒</li>\n *    <li>XX天XX小时</li>\n *    <li>XX月XX天XX小时</li>\n * </ul>\n *\n * @author Looly\n */\npublic class BetweenFormatter implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 时长毫秒数\n\t */\n\tprivate long betweenMs;\n\t/**\n\t * 格式化级别\n\t */\n\tprivate Level level;\n\t/**\n\t * 格式化级别的最大个数\n\t */\n\tprivate final int levelMaxCount;\n\t/**\n\t * 格式化器\n\t */\n\tprivate Function<Level, String> levelFormatter = Level::getName;\n\t/**\n\t * 分隔符\n\t */\n\tprivate String separator = StrUtil.EMPTY;\n\n\t/**\n\t * 构造\n\t *\n\t * @param betweenMs 日期间隔\n\t * @param level     级别，按照天、小时、分、秒、毫秒分为5个等级，根据传入等级，格式化到相应级别\n\t */\n\tpublic BetweenFormatter(long betweenMs, Level level) {\n\t\tthis(betweenMs, level, 0);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param betweenMs     日期间隔\n\t * @param level         级别，按照天、小时、分、秒、毫秒分为5个等级，根据传入等级，格式化到相应级别\n\t * @param levelMaxCount 格式化级别的最大个数，假如级别个数为1，但是级别到秒，那只显示一个级别\n\t */\n\tpublic BetweenFormatter(long betweenMs, Level level, int levelMaxCount) {\n\t\tthis.betweenMs = betweenMs;\n\t\tthis.level = level;\n\t\tthis.levelMaxCount = levelMaxCount;\n\t}\n\n\t/**\n\t * 格式化日期间隔输出<br>\n\t *\n\t * @return 格式化后的字符串\n\t */\n\tpublic String format() {\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tif (betweenMs > 0) {\n\t\t\tlong day = betweenMs / DateUnit.DAY.getMillis();\n\t\t\tlong hour = betweenMs / DateUnit.HOUR.getMillis() - day * 24;\n\t\t\tlong minute = betweenMs / DateUnit.MINUTE.getMillis() - day * 24 * 60 - hour * 60;\n\n\t\t\tfinal long BetweenOfSecond = ((day * 24 + hour) * 60 + minute) * 60;\n\t\t\tlong second = betweenMs / DateUnit.SECOND.getMillis() - BetweenOfSecond;\n\t\t\tlong millisecond = betweenMs - (BetweenOfSecond + second) * 1000;\n\n\t\t\tfinal int level = this.level.ordinal();\n\t\t\tint levelCount = 0;\n\n\t\t\tif (isLevelCountValid(levelCount) && day > 0) {\n\t\t\t\tsb.append(day).append(levelFormatter.apply(Level.DAY)).append(separator);\n\t\t\t\tlevelCount++;\n\t\t\t}\n\t\t\tif (isLevelCountValid(levelCount) && 0 != hour && level >= Level.HOUR.ordinal()) {\n\t\t\t\tsb.append(hour).append(levelFormatter.apply(Level.HOUR)).append(separator);\n\t\t\t\tlevelCount++;\n\t\t\t}\n\t\t\tif (isLevelCountValid(levelCount) && 0 != minute && level >= Level.MINUTE.ordinal()) {\n\t\t\t\tsb.append(minute).append(levelFormatter.apply(Level.MINUTE)).append(separator);\n\t\t\t\tlevelCount++;\n\t\t\t}\n\t\t\tif (isLevelCountValid(levelCount) && 0 != second && level >= Level.SECOND.ordinal()) {\n\t\t\t\tsb.append(second).append(levelFormatter.apply(Level.SECOND)).append(separator);\n\t\t\t\tlevelCount++;\n\t\t\t}\n\t\t\tif (isLevelCountValid(levelCount) && 0 != millisecond && level >= Level.MILLISECOND.ordinal()) {\n\t\t\t\tsb.append(millisecond).append(levelFormatter.apply(Level.MILLISECOND)).append(separator);\n\t\t\t\t// levelCount++;\n\t\t\t}\n\t\t}\n\n\t\tif (StrUtil.isEmpty(sb)) {\n\t\t\tsb.append(0).append(levelFormatter.apply(this.level));\n\t\t} else {\n\t\t\tif (StrUtil.isNotEmpty(separator)) {\n\t\t\t\tsb.delete(sb.length() - separator.length(), sb.length());\n\t\t\t}\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 获得 时长毫秒数\n\t *\n\t * @return 时长毫秒数\n\t */\n\tpublic long getBetweenMs() {\n\t\treturn betweenMs;\n\t}\n\n\t/**\n\t * 设置 时长毫秒数\n\t *\n\t * @param betweenMs 时长毫秒数\n\t */\n\tpublic void setBetweenMs(long betweenMs) {\n\t\tthis.betweenMs = betweenMs;\n\t}\n\n\t/**\n\t * 获得 格式化级别\n\t *\n\t * @return 格式化级别\n\t */\n\tpublic Level getLevel() {\n\t\treturn level;\n\t}\n\n\t/**\n\t * 设置格式化级别\n\t *\n\t * @param level 格式化级别\n\t */\n\tpublic void setLevel(Level level) {\n\t\tthis.level = level;\n\t}\n\n\t/**\n\t * 设置级别格式化器\n\t *\n\t * @param levelFormatter 级别格式化器\n\t * @return this\n\t */\n\tpublic BetweenFormatter setLevelFormatter(Function<Level, String> levelFormatter) {\n\t\tthis.levelFormatter = levelFormatter;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置分隔符\n\t *\n\t * @param separator 分割符\n\t * @return this\n\t */\n\tpublic BetweenFormatter setSeparator(String separator) {\n\t\tthis.separator = StrUtil.nullToEmpty(separator);\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 格式化等级枚举\n\t *\n\t * @author Looly\n\t */\n\tpublic enum Level {\n\n\t\t/**\n\t\t * 天\n\t\t */\n\t\tDAY(\"天\"),\n\t\t/**\n\t\t * 小时\n\t\t */\n\t\tHOUR(\"小时\"),\n\t\t/**\n\t\t * 分钟\n\t\t */\n\t\tMINUTE(\"分\"),\n\t\t/**\n\t\t * 秒\n\t\t */\n\t\tSECOND(\"秒\"),\n\t\t/**\n\t\t * 毫秒\n\t\t */\n\t\tMILLISECOND(\"毫秒\");\n\n\t\t/**\n\t\t * 级别名称\n\t\t */\n\t\tprivate final String name;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param name 级别名称\n\t\t */\n\t\tLevel(String name) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\t/**\n\t\t * 获取级别名称\n\t\t *\n\t\t * @return 级别名称\n\t\t */\n\t\tpublic String getName() {\n\t\t\treturn this.name;\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn format();\n\t}\n\n\t/**\n\t * 等级数量是否有效<br>\n\t * 有效的定义是：levelMaxCount大于0（被设置），当前等级数量没有超过这个最大值\n\t *\n\t * @param levelCount 登记数量\n\t * @return 是否有效\n\t */\n\tprivate boolean isLevelCountValid(int levelCount) {\n\t\treturn this.levelMaxCount <= 0 || levelCount < this.levelMaxCount;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/CalendarUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.convert.NumberChineseFormatter;\nimport cn.hutool.core.date.format.DateParser;\nimport cn.hutool.core.date.format.FastDateParser;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.text.ParsePosition;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.LinkedHashSet;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * 针对{@link Calendar} 对象封装工具类\n *\n * @author looly\n * @since 5.3.0\n */\npublic class CalendarUtil {\n\n\t/**\n\t * 创建Calendar对象，时间为默认时区的当前时间\n\t *\n\t * @return Calendar对象\n\t * @since 4.6.6\n\t */\n\tpublic static Calendar calendar() {\n\t\treturn Calendar.getInstance();\n\t}\n\n\t/**\n\t * 转换为Calendar对象\n\t *\n\t * @param date 日期对象\n\t * @return Calendar对象\n\t */\n\tpublic static Calendar calendar(Date date) {\n\t\tif (date instanceof DateTime) {\n\t\t\treturn ((DateTime) date).toCalendar();\n\t\t} else {\n\t\t\treturn calendar(date.getTime());\n\t\t}\n\t}\n\n\t/**\n\t * 转换为Calendar对象，使用当前默认时区\n\t *\n\t * @param millis 时间戳\n\t * @return Calendar对象\n\t */\n\tpublic static Calendar calendar(long millis) {\n\t\treturn calendar(millis, TimeZone.getDefault());\n\t}\n\n\t/**\n\t * 转换为Calendar对象\n\t *\n\t * @param millis   时间戳\n\t * @param timeZone 时区\n\t * @return Calendar对象\n\t * @since 5.7.22\n\t */\n\tpublic static Calendar calendar(long millis, TimeZone timeZone) {\n\t\tfinal Calendar cal = Calendar.getInstance(timeZone);\n\t\tcal.setTimeInMillis(millis);\n\t\treturn cal;\n\t}\n\n\t/**\n\t * 转换为指定时区的Calendar，返回新的Calendar\n\t *\n\t * @param calendar 时间\n\t * @param timeZone 新时区\n\t * @return 指定时区的新的calendar对象\n\t * @since 5.8.30\n\t */\n\tpublic static Calendar calendar(Calendar calendar, final TimeZone timeZone) {\n\t\t// 转换到统一时区，例如UTC\n\t\tcalendar = (Calendar) calendar.clone();\n\t\tcalendar.setTimeZone(timeZone);\n\t\treturn calendar;\n\t}\n\n\t/**\n\t * 是否为上午\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return 是否为上午\n\t */\n\tpublic static boolean isAM(Calendar calendar) {\n\t\treturn Calendar.AM == calendar.get(Calendar.AM_PM);\n\t}\n\n\t/**\n\t * 是否为下午\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return 是否为下午\n\t */\n\tpublic static boolean isPM(Calendar calendar) {\n\t\treturn Calendar.PM == calendar.get(Calendar.AM_PM);\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段起始时间\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField 保留到的时间字段，如定义为 {@link DateField#SECOND}，表示这个字段不变，这个字段以下字段全部归0\n\t * @return 原{@link Calendar}\n\t */\n\tpublic static Calendar truncate(Calendar calendar, DateField dateField) {\n\t\treturn DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.TRUNCATE);\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段四舍五入时间\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField 时间字段\n\t * @return 原{@link Calendar}\n\t */\n\tpublic static Calendar round(Calendar calendar, DateField dateField) {\n\t\treturn DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.ROUND);\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段结束时间\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField 保留到的时间字段，如定义为 {@link DateField#SECOND}，表示这个字段不变，这个字段以下字段全部取最大值\n\t * @return 原{@link Calendar}\n\t */\n\tpublic static Calendar ceiling(Calendar calendar, DateField dateField) {\n\t\treturn DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.CEILING);\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段结束时间<br>\n\t * 可选是否归零毫秒。\n\t *\n\t * <p>\n\t * 有时候由于毫秒部分必须为0（如MySQL数据库中），因此在此加上选项。\n\t * </p>\n\t *\n\t * @param calendar            {@link Calendar}\n\t * @param dateField           时间字段\n\t * @param truncateMillisecond 是否毫秒归零\n\t * @return 原{@link Calendar}\n\t */\n\tpublic static Calendar ceiling(Calendar calendar, DateField dateField, boolean truncateMillisecond) {\n\t\treturn DateModifier.modify(calendar, dateField.getValue(), DateModifier.ModifyType.CEILING, truncateMillisecond);\n\t}\n\n\t/**\n\t * 修改秒级别的开始时间，即忽略毫秒部分\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t * @since 4.6.2\n\t */\n\tpublic static Calendar beginOfSecond(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.SECOND);\n\t}\n\n\t/**\n\t * 修改秒级别的结束时间，即毫秒设置为999\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t * @since 4.6.2\n\t */\n\tpublic static Calendar endOfSecond(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.SECOND);\n\t}\n\n\t/**\n\t * 修改某小时的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfHour(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.HOUR_OF_DAY);\n\t}\n\n\t/**\n\t * 修改某小时的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfHour(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.HOUR_OF_DAY);\n\t}\n\n\t/**\n\t * 修改某分钟的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfMinute(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.MINUTE);\n\t}\n\n\t/**\n\t * 修改某分钟的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfMinute(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.MINUTE);\n\t}\n\n\t/**\n\t * 修改某天的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfDay(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.DAY_OF_MONTH);\n\t}\n\n\t/**\n\t * 修改某天的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfDay(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.DAY_OF_MONTH);\n\t}\n\n\t/**\n\t * 修改给定日期当前周的开始时间，周一定为一周的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfWeek(Calendar calendar) {\n\t\treturn beginOfWeek(calendar, true);\n\t}\n\n\t/**\n\t * 修改给定日期当前周的开始时间\n\t *\n\t * @param calendar           日期 {@link Calendar}\n\t * @param isMondayAsFirstDay 是否周一做为一周的第一天（false表示周日做为第一天）\n\t * @return {@link Calendar}\n\t * @since 3.1.2\n\t */\n\tpublic static Calendar beginOfWeek(Calendar calendar, boolean isMondayAsFirstDay) {\n\t\tcalendar.setFirstDayOfWeek(isMondayAsFirstDay ? Calendar.MONDAY : Calendar.SUNDAY);\n\t\t// WEEK_OF_MONTH为上限的字段（不包括），实际调整的为DAY_OF_MONTH\n\t\treturn truncate(calendar, DateField.WEEK_OF_MONTH);\n\t}\n\n\t/**\n\t * 修改某周的结束时间，周日定为一周的结束\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfWeek(Calendar calendar) {\n\t\treturn endOfWeek(calendar, true);\n\t}\n\n\t/**\n\t * 修改某周的结束时间\n\t *\n\t * @param calendar          日期 {@link Calendar}\n\t * @param isSundayAsLastDay 是否周日做为一周的最后一天（false表示周六做为最后一天）\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfWeek(Calendar calendar, boolean isSundayAsLastDay) {\n\t\tcalendar.setFirstDayOfWeek(isSundayAsLastDay ? Calendar.MONDAY : Calendar.SUNDAY);\n\t\t// WEEK_OF_MONTH为上限的字段（不包括），实际调整的为DAY_OF_MONTH\n\t\treturn ceiling(calendar, DateField.WEEK_OF_MONTH);\n\t}\n\n\t/**\n\t * 修改某月的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfMonth(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.MONTH);\n\t}\n\n\t/**\n\t * 修改某月的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfMonth(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.MONTH);\n\t}\n\n\t/**\n\t * 修改某季度的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t * @since 4.1.0\n\t */\n\tpublic static Calendar beginOfQuarter(Calendar calendar) {\n\t\t//noinspection MagicConstant\n\t\tcalendar.set(Calendar.MONTH, calendar.get(DateField.MONTH.getValue()) / 3 * 3);\n\t\tcalendar.set(Calendar.DAY_OF_MONTH, 1);\n\t\treturn beginOfDay(calendar);\n\t}\n\n\t/**\n\t * 获取某季度的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t * @since 4.1.0\n\t */\n\t@SuppressWarnings({\"MagicConstant\", \"ConstantConditions\"})\n\tpublic static Calendar endOfQuarter(Calendar calendar) {\n\t\tfinal int year = calendar.get(Calendar.YEAR);\n\t\tfinal int month = calendar.get(DateField.MONTH.getValue()) / 3 * 3 + 2;\n\n\t\tfinal Calendar resultCal = Calendar.getInstance(calendar.getTimeZone());\n\t\tresultCal.set(year, month, Month.of(month).getLastDay(DateUtil.isLeapYear(year)));\n\n\t\treturn endOfDay(resultCal);\n\t}\n\n\t/**\n\t * 修改某年的开始时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar beginOfYear(Calendar calendar) {\n\t\treturn truncate(calendar, DateField.YEAR);\n\t}\n\n\t/**\n\t * 修改某年的结束时间\n\t *\n\t * @param calendar 日期 {@link Calendar}\n\t * @return {@link Calendar}\n\t */\n\tpublic static Calendar endOfYear(Calendar calendar) {\n\t\treturn ceiling(calendar, DateField.YEAR);\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一天\n\t *\n\t * @param cal1 日期1\n\t * @param cal2 日期2\n\t * @return 是否为同一天\n\t */\n\tpublic static boolean isSameDay(Calendar cal1, Calendar cal2) {\n\t\tif (cal1 == null || cal2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\n\t\tif(ObjUtil.notEqual(cal1.getTimeZone(), cal2.getTimeZone())){\n\t\t\t// 统一时区\n\t\t\tcal2 = changeTimeZone(cal2, cal1.getTimeZone());\n\t\t}\n\n\t\treturn cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) && //\n\t\t\tcal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && //\n\t\t\tcal1.get(Calendar.ERA) == cal2.get(Calendar.ERA);\n\t}\n\n\t/**\n\t * 是否为本月最后一天\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return 是否为本月最后一天\n\t * @since 5.8.27\n\t */\n\tpublic static boolean isLastDayOfMonth(Calendar calendar) {\n\t\treturn calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH);\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一周\n\t *\n\t * @param cal1  日期1\n\t * @param cal2  日期2\n\t * @param isMon 是否为周一。国内第一天为星期一，国外第一天为星期日\n\t * @return 是否为同一周\n\t * @since 5.7.21\n\t */\n\tpublic static boolean isSameWeek(Calendar cal1, Calendar cal2, boolean isMon) {\n\t\tif (cal1 == null || cal2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\n\t\t// 防止比较前修改原始Calendar对象\n\t\tcal1 = (Calendar) cal1.clone();\n\t\tcal2 = (Calendar) cal2.clone();\n\n\t\t// 把所传日期设置为其当前周的第一天\n\t\t// 比较设置后的两个日期是否是同一天：true 代表同一周\n\t\tif (isMon) {\n\t\t\tcal1.setFirstDayOfWeek(Calendar.MONDAY);\n\t\t\tcal1.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);\n\t\t\tcal2.setFirstDayOfWeek(Calendar.MONDAY);\n\t\t\tcal2.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);\n\t\t} else {\n\t\t\tcal1.setFirstDayOfWeek(Calendar.SUNDAY);\n\t\t\tcal1.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);\n\t\t\tcal2.setFirstDayOfWeek(Calendar.SUNDAY);\n\t\t\tcal2.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);\n\t\t}\n\t\treturn isSameDay(cal1, cal2);\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一月<br>\n\t * 同一个月的意思是：ERA（公元）、year（年）、month（月）都一致。\n\t *\n\t * @param cal1 日期1\n\t * @param cal2 日期2\n\t * @return 是否为同一月\n\t * @since 5.4.1\n\t */\n\tpublic static boolean isSameMonth(Calendar cal1, Calendar cal2) {\n\t\tif (cal1 == null || cal2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\n\t\tif(ObjUtil.notEqual(cal1.getTimeZone(), cal2.getTimeZone())){\n\t\t\t// 统一时区\n\t\t\tcal2 = changeTimeZone(cal2, cal1.getTimeZone());\n\t\t}\n\n\t\treturn cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && //\n\t\t\tcal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&\n\t\t\t// issue#3011@Github\n\t\t\tcal1.get(Calendar.ERA) == cal2.get(Calendar.ERA);\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一年<br>\n\t * 同一个年的意思是：ERA（公元）、year（年）都一致。\n\t *\n\t * @param cal1 日期1\n\t * @param cal2 日期2\n\t * @return 是否为同一年\n\t * @since 5.8.30\n\t */\n\tpublic static boolean isSameYear(final Calendar cal1, Calendar cal2) {\n\t\tif (cal1 == null || cal2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\n\t\tif (!ObjUtil.equals(cal1.getTimeZone(), cal2.getTimeZone())) {\n\t\t\t// 统一时区\n\t\t\tcal2 = calendar(cal2, cal1.getTimeZone());\n\t\t}\n\n\t\treturn cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && //\n\t\t\t// issue#3011@Github\n\t\t\tcal1.get(Calendar.ERA) == cal2.get(Calendar.ERA);\n\t}\n\n\t/**\n\t * <p>检查两个Calendar时间戳是否相同。</p>\n\t *\n\t * <p>此方法检查两个Calendar的毫秒数时间戳是否相同。</p>\n\t *\n\t * @param date1 时间1\n\t * @param date2 时间2\n\t * @return 两个Calendar时间戳是否相同。如果两个时间都为{@code null}返回true，否则有{@code null}返回false\n\t * @since 5.3.11\n\t */\n\tpublic static boolean isSameInstant(Calendar date1, Calendar date2) {\n\t\tif (null == date1) {\n\t\t\treturn null == date2;\n\t\t}\n\t\tif (null == date2) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn date1.getTimeInMillis() == date2.getTimeInMillis();\n\t}\n\n\t/**\n\t * 获得指定日期区间内的年份和季度<br>\n\t *\n\t * @param startDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 季度列表 ，元素类似于 20132\n\t * @since 4.1.15\n\t */\n\tpublic static LinkedHashSet<String> yearAndQuarter(long startDate, long endDate) {\n\t\tLinkedHashSet<String> quarters = new LinkedHashSet<>();\n\t\tfinal Calendar cal = calendar(startDate);\n\t\twhile (startDate <= endDate) {\n\t\t\t// 如果开始时间超出结束时间，让结束时间为开始时间，处理完后结束循环\n\t\t\tquarters.add(yearAndQuarter(cal));\n\n\t\t\tcal.add(Calendar.MONTH, 3);\n\t\t\tstartDate = cal.getTimeInMillis();\n\t\t}\n\n\t\treturn quarters;\n\t}\n\n\t/**\n\t * 获得指定日期年份和季度<br>\n\t * 格式：[20131]表示2013年第一季度\n\t *\n\t * @param cal 日期\n\t * @return 年和季度，格式类似于20131\n\t */\n\tpublic static String yearAndQuarter(Calendar cal) {\n\t\treturn StrUtil.builder().append(cal.get(Calendar.YEAR)).append(cal.get(Calendar.MONTH) / 3 + 1).toString();\n\t}\n\n\t/**\n\t * 获取指定日期字段的最小值，例如分钟的最小值是0\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField {@link DateField}\n\t * @return 字段最小值\n\t * @see Calendar#getActualMinimum(int)\n\t * @since 5.4.2\n\t */\n\tpublic static int getBeginValue(Calendar calendar, DateField dateField) {\n\t\treturn getBeginValue(calendar, dateField.getValue());\n\t}\n\n\t/**\n\t * 获取指定日期字段的最小值，例如分钟的最小值是0\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField {@link DateField}\n\t * @return 字段最小值\n\t * @see Calendar#getActualMinimum(int)\n\t * @since 4.5.7\n\t */\n\tpublic static int getBeginValue(Calendar calendar, int dateField) {\n\t\tif (Calendar.DAY_OF_WEEK == dateField) {\n\t\t\treturn calendar.getFirstDayOfWeek();\n\t\t}\n\t\treturn calendar.getActualMinimum(dateField);\n\t}\n\n\t/**\n\t * 获取指定日期字段的最大值，例如分钟的最大值是59\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField {@link DateField}\n\t * @return 字段最大值\n\t * @see Calendar#getActualMaximum(int)\n\t * @since 5.4.2\n\t */\n\tpublic static int getEndValue(Calendar calendar, DateField dateField) {\n\t\treturn getEndValue(calendar, dateField.getValue());\n\t}\n\n\t/**\n\t * 获取指定日期字段的最大值，例如分钟的最大值是59\n\t *\n\t * @param calendar  {@link Calendar}\n\t * @param dateField {@link DateField}\n\t * @return 字段最大值\n\t * @see Calendar#getActualMaximum(int)\n\t * @since 4.5.7\n\t */\n\tpublic static int getEndValue(Calendar calendar, int dateField) {\n\t\tif (Calendar.DAY_OF_WEEK == dateField) {\n\t\t\treturn (calendar.getFirstDayOfWeek() + 6) % 7;\n\t\t}\n\t\treturn calendar.getActualMaximum(dateField);\n\t}\n\n\t/**\n\t * Calendar{@link Instant}对象\n\t *\n\t * @param calendar Date对象\n\t * @return {@link Instant}对象\n\t * @since 5.0.5\n\t */\n\tpublic static Instant toInstant(Calendar calendar) {\n\t\treturn null == calendar ? null : calendar.toInstant();\n\t}\n\n\t/**\n\t * {@link Calendar} 转换为 {@link LocalDateTime}，使用系统默认时区\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return {@link LocalDateTime}\n\t * @since 5.0.5\n\t */\n\tpublic static LocalDateTime toLocalDateTime(Calendar calendar) {\n\t\treturn LocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());\n\t}\n\n\t/**\n\t * {@code null}安全的{@link Calendar}比较，{@code null}小于任何日期\n\t *\n\t * @param calendar1 日期1\n\t * @param calendar2 日期2\n\t * @return 比较结果，如果calendar1 &lt; calendar2，返回数小于0，calendar1==calendar2返回0，calendar1 &gt; calendar2 大于0\n\t * @since 4.6.2\n\t */\n\tpublic static int compare(Calendar calendar1, Calendar calendar2) {\n\t\treturn CompareUtil.compare(calendar1, calendar2);\n\t}\n\n\t/**\n\t * 计算相对于dateToCompare的年龄，长用于计算指定生日在某年的年龄\n\t *\n\t * @param birthday      生日\n\t * @param dateToCompare 需要对比的日期\n\t * @return 年龄\n\t */\n\tpublic static int age(Calendar birthday, Calendar dateToCompare) {\n\t\treturn age(birthday.getTimeInMillis(), dateToCompare.getTimeInMillis());\n\t}\n\n\t/**\n\t * 将指定Calendar时间格式化为纯中文形式，比如：\n\t *\n\t * <pre>\n\t *     2018-02-24 12:13:14 转换为 二〇一八年二月二十四日（withTime为false）\n\t *     2018-02-24 12:13:14 转换为 二〇一八年二月二十四日十二时十三分十四秒（withTime为true）\n\t * </pre>\n\t *\n\t * @param calendar {@link Calendar}\n\t * @param withTime 是否包含时间部分\n\t * @return 格式化后的字符串\n\t * @since 5.3.9\n\t */\n\tpublic static String formatChineseDate(Calendar calendar, boolean withTime) {\n\t\tfinal StringBuilder result = StrUtil.builder();\n\n\t\t// 年\n\t\tfinal String year = String.valueOf(calendar.get(Calendar.YEAR));\n\t\tfinal int length = year.length();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tresult.append(NumberChineseFormatter.numberCharToChinese(year.charAt(i), false));\n\t\t}\n\t\tresult.append('年');\n\n\t\t// 月\n\t\tint month = calendar.get(Calendar.MONTH) + 1;\n\t\tresult.append(NumberChineseFormatter.formatThousand(month, false));\n\t\tresult.append('月');\n\n\t\t// 日\n\t\tint day = calendar.get(Calendar.DAY_OF_MONTH);\n\t\tresult.append(NumberChineseFormatter.formatThousand(day, false));\n\t\tresult.append('日');\n\n\t\t// 只替换年月日，时分秒中零不需要替换\n\t\tString temp = result.toString().replace('零', '〇');\n\t\tresult.delete(0, result.length());\n\t\tresult.append(temp);\n\n\n\t\tif (withTime) {\n\t\t\t// 时\n\t\t\tint hour = calendar.get(Calendar.HOUR_OF_DAY);\n\t\t\tresult.append(NumberChineseFormatter.formatThousand(hour, false));\n\t\t\tresult.append('时');\n\t\t\t// 分\n\t\t\tint minute = calendar.get(Calendar.MINUTE);\n\t\t\tresult.append(NumberChineseFormatter.formatThousand(minute, false));\n\t\t\tresult.append('分');\n\t\t\t// 秒\n\t\t\tint second = calendar.get(Calendar.SECOND);\n\t\t\tresult.append(NumberChineseFormatter.formatThousand(second, false));\n\t\t\tresult.append('秒');\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 计算相对于dateToCompare的年龄，常用于计算指定生日在某年的年龄<br>\n\t * 按照《最高人民法院关于审理未成年人刑事案件具体应用法律若干问题的解释》第二条规定刑法第十七条规定的“周岁”，按照公历的年、月、日计算，从周岁生日的第二天起算。\n\t * <ul>\n\t *     <li>2022-03-01出生，则相对2023-03-01，周岁为0，相对于2023-03-02才是1岁。</li>\n\t *     <li>1999-02-28出生，则相对2000-02-29，周岁为1</li>\n\t * </ul>\n\t *\n\t * @param birthday      生日\n\t * @param dateToCompare 需要对比的日期\n\t * @return 年龄\n\t */\n\tprotected static int age(long birthday, long dateToCompare) {\n\t\tif (birthday > dateToCompare) {\n\t\t\tthrow new IllegalArgumentException(\"Birthday is after dateToCompare!\");\n\t\t}\n\n\t\tfinal Calendar cal = Calendar.getInstance();\n\t\tcal.setTimeInMillis(dateToCompare);\n\n\t\tfinal int year = cal.get(Calendar.YEAR);\n\t\tfinal int month = cal.get(Calendar.MONTH);\n\t\tfinal int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);\n\n\t\t// 复用cal\n\t\tcal.setTimeInMillis(birthday);\n\t\tint age = year - cal.get(Calendar.YEAR);\n\n\t\t//当前日期，则为0岁\n\t\tif (age == 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal int monthBirth = cal.get(Calendar.MONTH);\n\t\tif (month == monthBirth) {\n\t\t\tfinal int dayOfMonthBirth = cal.get(Calendar.DAY_OF_MONTH);\n\t\t\t// issue#I6E6ZG，法定生日当天不算年龄，从第二天开始计算\n\t\t\tif (dayOfMonth <= dayOfMonthBirth) {\n\t\t\t\t// 如果生日在当月，但是未达到生日当天的日期，年龄减一\n\t\t\t\tage--;\n\t\t\t}\n\t\t} else if (month < monthBirth) {\n\t\t\t// 如果当前月份未达到生日的月份，年龄计算减一\n\t\t\tage--;\n\t\t}\n\n\t\treturn age;\n\t}\n\n\t/**\n\t * 通过给定的日期格式解析日期时间字符串。<br>\n\t * 传入的日期格式会逐个尝试，直到解析成功，返回{@link Calendar}对象，否则抛出{@link DateException}异常。\n\t * 方法来自：Apache Commons-Lang3\n\t *\n\t * @param str           日期时间字符串，非空\n\t * @param parsePatterns 需要尝试的日期时间格式数组，非空, 见SimpleDateFormat\n\t * @return 解析后的Calendar\n\t * @throws IllegalArgumentException if the date string or pattern array is null\n\t * @throws DateException            if none of the date patterns were suitable\n\t * @since 5.3.11\n\t */\n\tpublic static Calendar parseByPatterns(String str, String... parsePatterns) throws DateException {\n\t\treturn parseByPatterns(str, null, parsePatterns);\n\t}\n\n\t/**\n\t * 通过给定的日期格式解析日期时间字符串。<br>\n\t * 传入的日期格式会逐个尝试，直到解析成功，返回{@link Calendar}对象，否则抛出{@link DateException}异常。\n\t * 方法来自：Apache Commons-Lang3\n\t *\n\t * @param str           日期时间字符串，非空\n\t * @param locale        地区，当为{@code null}时使用{@link Locale#getDefault()}\n\t * @param parsePatterns 需要尝试的日期时间格式数组，非空, 见SimpleDateFormat\n\t * @return 解析后的Calendar\n\t * @throws IllegalArgumentException if the date string or pattern array is null\n\t * @throws DateException            if none of the date patterns were suitable\n\t * @since 5.3.11\n\t */\n\tpublic static Calendar parseByPatterns(String str, Locale locale, String... parsePatterns) throws DateException {\n\t\treturn parseByPatterns(str, locale, true, parsePatterns);\n\t}\n\n\t/**\n\t * 通过给定的日期格式解析日期时间字符串。<br>\n\t * 传入的日期格式会逐个尝试，直到解析成功，返回{@link Calendar}对象，否则抛出{@link DateException}异常。\n\t * 方法来自：Apache Commons-Lang3\n\t *\n\t * @param str           日期时间字符串，非空\n\t * @param locale        地区，当为{@code null}时使用{@link Locale#getDefault()}\n\t * @param lenient       日期时间解析是否使用严格模式\n\t * @param parsePatterns 需要尝试的日期时间格式数组，非空, 见SimpleDateFormat\n\t * @return 解析后的Calendar\n\t * @throws IllegalArgumentException if the date string or pattern array is null\n\t * @throws DateException            if none of the date patterns were suitable\n\t * @see java.util.Calendar#isLenient()\n\t * @since 5.3.11\n\t */\n\tpublic static Calendar parseByPatterns(String str, Locale locale, boolean lenient, String... parsePatterns) throws DateException {\n\t\tif (str == null || parsePatterns == null) {\n\t\t\tthrow new IllegalArgumentException(\"Date and Patterns must not be null\");\n\t\t}\n\n\t\tfinal TimeZone tz = TimeZone.getDefault();\n\t\tfinal Locale lcl = ObjectUtil.defaultIfNull(locale, Locale.getDefault());\n\t\tfinal ParsePosition pos = new ParsePosition(0);\n\t\tfinal Calendar calendar = Calendar.getInstance(tz, lcl);\n\t\tcalendar.setLenient(lenient);\n\n\t\tfor (final String parsePattern : parsePatterns) {\n\t\t\tif (GlobalCustomFormat.isCustomFormat(parsePattern)) {\n\t\t\t\tfinal Date parse = GlobalCustomFormat.parse(str, parsePattern);\n\t\t\t\tif (null == parse) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcalendar.setTime(parse);\n\t\t\t\treturn calendar;\n\t\t\t}\n\n\t\t\tfinal FastDateParser fdp = new FastDateParser(parsePattern, tz, lcl);\n\t\t\tcalendar.clear();\n\t\t\ttry {\n\t\t\t\tif (fdp.parse(str, pos, calendar) && pos.getIndex() == str.length()) {\n\t\t\t\t\treturn calendar;\n\t\t\t\t}\n\t\t\t} catch (final IllegalArgumentException ignore) {\n\t\t\t\t// leniency is preventing calendar from being set\n\t\t\t}\n\t\t\tpos.setIndex(0);\n\t\t}\n\n\t\tthrow new DateException(\"Unable to parse the date: {}\", str);\n\t}\n\n\t/**\n\t * 使用指定{@link DateParser}解析字符串为{@link Calendar}\n\t *\n\t * @param str     日期字符串\n\t * @param lenient 是否宽容模式\n\t * @param parser  {@link DateParser}\n\t * @return 解析后的 {@link Calendar}，解析失败返回{@code null}\n\t * @since 5.7.14\n\t */\n\tpublic static Calendar parse(CharSequence str, boolean lenient, DateParser parser) {\n\t\tfinal Calendar calendar = Calendar.getInstance(parser.getTimeZone(), parser.getLocale());\n\t\tcalendar.clear();\n\t\tcalendar.setLenient(lenient);\n\n\t\treturn parser.parse(StrUtil.str(str), new ParsePosition(0), calendar) ? calendar : null;\n\t}\n\n\t/**\n\t * 转换为默认时区的Calendar\n\t *\n\t * @param cal 时间\n\t * @return 默认时区的calendar对象\n\t */\n\tprivate static Calendar changeTimeZone(Calendar cal, TimeZone timeZone) {\n\t\t// 转换到统一时区，例如UTC\n\t\tcal = (Calendar) cal.clone();\n\t\tcal.setTimeZone(timeZone);\n\t\treturn cal;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/ChineseDate.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.convert.NumberChineseFormatter;\nimport cn.hutool.core.date.chinese.ChineseMonth;\nimport cn.hutool.core.date.chinese.GanZhi;\nimport cn.hutool.core.date.chinese.LunarFestival;\nimport cn.hutool.core.date.chinese.LunarInfo;\nimport cn.hutool.core.date.chinese.SolarTerms;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.LocalDate;\nimport java.util.Calendar;\nimport java.util.Date;\n\n\n/**\n * 农历日期工具，最大支持到2099年，支持：\n *\n * <ul>\n *     <li>通过公历日期构造获取对应农历</li>\n *     <li>通过农历日期直接构造</li>\n * </ul>\n *\n * @author zjw, looly\n * @since 5.1.1\n */\npublic class ChineseDate {\n\n\t//农历年\n\tprivate final int year;\n\t//农历月，润N月这个值就是N+1，其他月按照显示月份赋值\n\tprivate final int month;\n\t// 当前月份是否闰月\n\tprivate final boolean isLeapMonth;\n\t//农历日\n\tprivate final int day;\n\n\t//公历年\n\tprivate final int gyear;\n\t//公历月，从1开始计数\n\tprivate final int gmonthBase1;\n\t//公历日\n\tprivate final int gday;\n\n\t/**\n\t * 通过公历日期构造\n\t *\n\t * @param date 公历日期\n\t */\n\tpublic ChineseDate(Date date) {\n\t\tthis(LocalDateTimeUtil.ofDate(date.toInstant()));\n\t}\n\n\t/**\n\t * 通过公历日期构造\n\t *\n\t * @param localDate 公历日期\n\t * @since 5.7.22\n\t */\n\tpublic ChineseDate(LocalDate localDate) {\n\t\t// 公历\n\t\tgyear = localDate.getYear();\n\t\tgmonthBase1 = localDate.getMonthValue();\n\t\tgday = localDate.getDayOfMonth();\n\n\t\t// 求出和1900年1月31日相差的天数\n\t\tint offset = (int) (localDate.toEpochDay() - LunarInfo.BASE_DAY);\n\n\t\t// 计算农历年份\n\t\t// 用offset减去每农历年的天数，计算当天是农历第几天，offset是当年的第几天\n\t\tint daysOfYear;\n\t\tint iYear;\n\t\tfor (iYear = LunarInfo.BASE_YEAR; iYear <= LunarInfo.MAX_YEAR; iYear++) {\n\t\t\tdaysOfYear = LunarInfo.yearDays(iYear);\n\t\t\tif (offset < daysOfYear) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toffset -= daysOfYear;\n\t\t}\n\n\t\tyear = iYear;\n\t\t// 计算农历月份\n\t\tfinal int leapMonth = LunarInfo.leapMonth(iYear); // 闰哪个月,1-12\n\t\t// 用当年的天数offset,逐个减去每月（农历）的天数，求出当天是本月的第几天\n\t\tint month;\n\t\tint daysOfMonth;\n\t\tboolean hasLeapMonth = false;\n\t\tfor (month = 1; month < 13; month++) {\n\t\t\t// 闰月，如润的是五月，则5表示五月，6表示润五月\n\t\t\tif (leapMonth > 0 && month == (leapMonth + 1)) {\n\t\t\t\tdaysOfMonth = LunarInfo.leapDays(year);\n\t\t\t\thasLeapMonth = true;\n\t\t\t} else {\n\t\t\t\t// 普通月，当前面的月份存在闰月时，普通月份要-1，递补闰月的数字\n\t\t\t\t// 如2月是闰月，此时3月实际是第四个月\n\t\t\t\tdaysOfMonth = LunarInfo.monthDays(year, hasLeapMonth ? month - 1 : month);\n\t\t\t}\n\n\t\t\tif (offset < daysOfMonth) {\n\t\t\t\t// offset不足月，结束\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toffset -= daysOfMonth;\n\t\t}\n\n\t\tthis.isLeapMonth = leapMonth > 0 && (month == (leapMonth + 1));\n\t\tif (hasLeapMonth && false == this.isLeapMonth) {\n\t\t\t// 当前月份前有闰月，则月份显示要-1，除非当前月份就是润月\n\t\t\tmonth--;\n\t\t}\n\t\tthis.month = month;\n\t\tthis.day = offset + 1;\n\t}\n\n\t/**\n\t * 构造方法传入日期<br>\n\t * 此方法自动判断闰月，如果chineseMonth为本年的闰月，则按照闰月计算\n\t *\n\t * @param chineseYear  农历年\n\t * @param chineseMonth 农历月，1表示一月（正月）\n\t * @param chineseDay   农历日，1表示初一\n\t * @since 5.2.4\n\t */\n\tpublic ChineseDate(int chineseYear, int chineseMonth, int chineseDay) {\n\t\tthis(chineseYear, chineseMonth, chineseDay, chineseMonth == LunarInfo.leapMonth(chineseYear));\n\t}\n\n\t/**\n\t * 构造方法传入日期<br>\n\t * 通过isLeapMonth参数区分是否闰月，如五月是闰月，当isLeapMonth为{@code true}时，表示润五月，{@code false}表示五月\n\t *\n\t * @param chineseYear  农历年\n\t * @param chineseMonth 农历月，1表示一月（正月），如果isLeapMonth为{@code true}，1表示润一月\n\t * @param chineseDay   农历日，1表示初一\n\t * @param isLeapMonth  当前月份是否闰月\n\t * @since 5.7.18\n\t */\n\tpublic ChineseDate(int chineseYear, int chineseMonth, int chineseDay, boolean isLeapMonth) {\n\t\tif(chineseMonth != LunarInfo.leapMonth(chineseYear)){\n\t\t\t// issue#I5YB1A，用户传入的月份可能非闰月，此时此参数无效。\n\t\t\tisLeapMonth = false;\n\t\t}\n\n\t\tthis.day = chineseDay;\n\t\t// 当月是闰月的后边的月定义为闰月，如润的是五月，则5表示五月，6表示润五月\n\t\tthis.isLeapMonth = isLeapMonth;\n\t\t// 闰月时，农历月份+1，如6表示润五月\n\t\tthis.month = isLeapMonth ? chineseMonth + 1 : chineseMonth;\n\t\tthis.year = chineseYear;\n\n\t\tfinal DateTime dateTime = lunar2solar(chineseYear, chineseMonth, chineseDay, isLeapMonth);\n\t\tif (null != dateTime) {\n\t\t\t//初始化公历年\n\t\t\tthis.gday = dateTime.dayOfMonth();\n\t\t\t//初始化公历月\n\t\t\tthis.gmonthBase1 = dateTime.month() + 1;\n\t\t\t//初始化公历日\n\t\t\tthis.gyear = dateTime.year();\n\t\t} else {\n\t\t\t//初始化公历年\n\t\t\tthis.gday = -1;\n\t\t\t//初始化公历月\n\t\t\tthis.gmonthBase1 = -1;\n\t\t\t//初始化公历日\n\t\t\tthis.gyear = -1;\n\t\t}\n\t}\n\n\t/**\n\t * 获得农历年份\n\t *\n\t * @return 返回农历年份\n\t */\n\tpublic int getChineseYear() {\n\t\treturn this.year;\n\t}\n\n\t/**\n\t * 获取公历的年\n\t *\n\t * @return 公历年\n\t * @since 5.6.1\n\t */\n\tpublic int getGregorianYear() {\n\t\treturn this.gyear;\n\t}\n\n\t/**\n\t * 获取农历的月，从1开始计数<br>\n\t * 此方法返回实际的月序号，如一月是闰月，则一月返回1，润一月返回2\n\t *\n\t * @return 农历的月\n\t * @since 5.2.4\n\t */\n\tpublic int getMonth() {\n\t\treturn this.month;\n\t}\n\n\t/**\n\t * 获取公历的月，从1开始计数\n\t *\n\t * @return 公历月\n\t * @since 5.6.1\n\t */\n\tpublic int getGregorianMonthBase1() {\n\t\treturn this.gmonthBase1;\n\t}\n\n\t/**\n\t * 获取公历的月，从0开始计数\n\t *\n\t * @return 公历月\n\t * @since 5.6.1\n\t */\n\tpublic int getGregorianMonth() {\n\t\treturn this.gmonthBase1 - 1;\n\t}\n\n\t/**\n\t * 当前农历月份是否为闰月\n\t *\n\t * @return 是否为闰月\n\t * @since 5.4.2\n\t */\n\tpublic boolean isLeapMonth() {\n\t\treturn this.isLeapMonth;\n\t}\n\n\n\t/**\n\t * 获得农历月份（中文，例如二月，十二月，或者润一月）\n\t *\n\t * @return 返回农历月份\n\t */\n\tpublic String getChineseMonth() {\n\t\treturn getChineseMonth(false);\n\t}\n\n\t/**\n\t * 获得农历月称呼（中文，例如二月，腊月，或者润正月）\n\t *\n\t * @return 返回农历月份称呼\n\t */\n\tpublic String getChineseMonthName() {\n\t\treturn getChineseMonth(true);\n\t}\n\n\t/**\n\t * 获得农历月份（中文，例如二月，十二月，或者润一月）\n\t *\n\t * @param isTraditional 是否传统表示，例如一月传统表示为正月\n\t * @return 返回农历月份\n\t * @since 5.7.18\n\t */\n\tpublic String getChineseMonth(boolean isTraditional) {\n\t\treturn ChineseMonth.getChineseMonthName(isLeapMonth(),\n\t\t\t\tisLeapMonth() ? this.month - 1 : this.month, isTraditional);\n\t}\n\n\t/**\n\t * 获取农历的日，从1开始计数\n\t *\n\t * @return 农历的日，从1开始计数\n\t * @since 5.2.4\n\t */\n\tpublic int getDay() {\n\t\treturn this.day;\n\t}\n\n\t/**\n\t * 获取公历的日\n\t *\n\t * @return 公历日\n\t * @since 5.6.1\n\t */\n\tpublic int getGregorianDay() {\n\t\treturn this.gday;\n\t}\n\n\t/**\n\t * 获得农历日\n\t *\n\t * @return 获得农历日\n\t */\n\tpublic String getChineseDay() {\n\t\tString[] chineseTen = {\"初\", \"十\", \"廿\", \"卅\"};\n\t\tint n = (day % 10 == 0) ? 9 : (day % 10 - 1);\n\t\tif (day > 30) {\n\t\t\treturn \"\";\n\t\t}\n\t\tswitch (day) {\n\t\t\tcase 10:\n\t\t\t\treturn \"初十\";\n\t\t\tcase 20:\n\t\t\t\treturn \"二十\";\n\t\t\tcase 30:\n\t\t\t\treturn \"三十\";\n\t\t\tdefault:\n\t\t\t\treturn chineseTen[day / 10] + NumberChineseFormatter.format(n + 1, false);\n\t\t}\n\t}\n\n\t/**\n\t * 获取公历的Date\n\t *\n\t * @return 公历Date\n\t * @since 5.6.1\n\t */\n\tpublic Date getGregorianDate() {\n\t\treturn DateUtil.date(getGregorianCalendar());\n\t}\n\n\t/**\n\t * 获取公历的Calendar\n\t *\n\t * @return 公历Calendar\n\t * @since 5.6.1\n\t */\n\tpublic Calendar getGregorianCalendar() {\n\t\tfinal Calendar calendar = CalendarUtil.calendar();\n\t\t//noinspection MagicConstant\n\t\tcalendar.set(this.gyear, getGregorianMonth(), this.gday, 0, 0, 0);\n\t\treturn calendar;\n\t}\n\n\t/**\n\t * 获得节日，闰月不计入节日中\n\t *\n\t * @return 获得农历节日\n\t */\n\tpublic String getFestivals() {\n\t\treturn StrUtil.join(\",\", LunarFestival.getFestivals(this.year, this.isLeapMonth ? this.month - 1 : this.month, day));\n\t}\n\n\t/**\n\t * 获得年份生肖\n\t *\n\t * @return 获得年份生肖\n\t */\n\tpublic String getChineseZodiac() {\n\t\treturn Zodiac.getChineseZodiac(this.year);\n\t}\n\n\n\t/**\n\t * 获得年的天干地支\n\t *\n\t * @return 获得天干地支\n\t */\n\tpublic String getCyclical() {\n\t\treturn GanZhi.getGanzhiOfYear(this.year);\n\t}\n\n\t/**\n\t * 干支纪年信息\n\t *\n\t * @return 获得天干地支的年月日信息\n\t */\n\tpublic String getCyclicalYMD() {\n\t\tif (gyear >= LunarInfo.BASE_YEAR && gmonthBase1 > 0 && gday > 0) {\n\t\t\treturn cyclicalm(gyear, gmonthBase1, gday);\n\t\t}\n\t\treturn null;\n\t}\n\n\n\t/**\n\t * 获得节气\n\t *\n\t * @return 获得节气\n\t * @since 5.6.3\n\t */\n\tpublic String getTerm() {\n\t\treturn SolarTerms.getTerm(gyear, gmonthBase1, gday);\n\t}\n\n\t/**\n\t * 转换为标准的日期格式来表示农历日期，例如2020-01-13<br>\n\t * 如果存在闰月，显示闰月月份，如润二月显示2\n\t *\n\t * @return 标准的日期格式\n\t * @since 5.2.4\n\t */\n\tpublic String toStringNormal() {\n\t\treturn String.format(\"%04d-%02d-%02d\", this.year,\n\t\t\t\tisLeapMonth() ? this.month - 1 : this.month, this.day);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn String.format(\"%s%s年 %s%s\", getCyclical(), getChineseZodiac(), getChineseMonthName(), getChineseDay());\n\t}\n\n\t// ------------------------------------------------------- private method start\n\n\t/**\n\t * 这里同步处理年月日的天干地支信息\n\t *\n\t * @param year  公历年\n\t * @param month 公历月，从1开始\n\t * @param day   公历日\n\t * @return 天干地支信息\n\t */\n\tprivate String cyclicalm(int year, int month, int day) {\n\t\treturn StrUtil.format(\"{}年{}月{}日\",\n\t\t\t\tGanZhi.getGanzhiOfYear(this.year),\n\t\t\t\tGanZhi.getGanzhiOfMonth(year, month, day),\n\t\t\t\tGanZhi.getGanzhiOfDay(year, month, day));\n\t}\n\n\t/**\n\t * 通过农历年月日信息 返回公历信息 提供给构造函数\n\t *\n\t * @param chineseYear  农历年\n\t * @param chineseMonth 农历月\n\t * @param chineseDay   农历日\n\t * @param isLeapMonth  传入的月是不是闰月\n\t * @return 公历信息\n\t */\n\tprivate DateTime lunar2solar(int chineseYear, int chineseMonth, int chineseDay, boolean isLeapMonth) {\n\t\t//超出了最大极限值\n\t\tif ((chineseYear == 2100 && chineseMonth == 12 && chineseDay > 1) ||\n\t\t\t\t(chineseYear == LunarInfo.BASE_YEAR && chineseMonth == 1 && chineseDay < 31)) {\n\t\t\treturn null;\n\t\t}\n\t\tint day = LunarInfo.monthDays(chineseYear, chineseMonth);\n\t\tint _day = day;\n\t\tif (isLeapMonth) {\n\t\t\t_day = LunarInfo.leapDays(chineseYear);\n\t\t}\n\t\t//参数合法性效验\n\t\tif (chineseYear < LunarInfo.BASE_YEAR || chineseYear > 2100 || chineseDay > _day) {\n\t\t\treturn null;\n\t\t}\n\t\t//计算农历的时间差\n\t\tint offset = 0;\n\t\tfor (int i = LunarInfo.BASE_YEAR; i < chineseYear; i++) {\n\t\t\toffset += LunarInfo.yearDays(i);\n\t\t}\n\t\tint leap;\n\t\tboolean isAdd = false;\n\t\tfor (int i = 1; i < chineseMonth; i++) {\n\t\t\tleap = LunarInfo.leapMonth(chineseYear);\n\t\t\tif (false == isAdd) {//处理闰月\n\t\t\t\tif (leap <= i && leap > 0) {\n\t\t\t\t\toffset += LunarInfo.leapDays(chineseYear);\n\t\t\t\t\tisAdd = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\toffset += LunarInfo.monthDays(chineseYear, i);\n\t\t}\n\t\t//转换闰月农历 需补充该年闰月的前一个月的时差\n\t\tif (isLeapMonth) {\n\t\t\toffset += day;\n\t\t}\n\t\t//1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) -2203804800000\n\t\treturn DateUtil.date(((offset + chineseDay - 31) * 86400000L) - 2203804800000L);\n\t}\n\n\t// ------------------------------------------------------- private method end\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.Serializable;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 日期间隔\n *\n * @author Looly\n */\npublic class DateBetween implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 开始日期\n\t */\n\tprivate final Date begin;\n\t/**\n\t * 结束日期\n\t */\n\tprivate final Date end;\n\n\t/**\n\t * 创建<br>\n\t * 在前的日期做为起始时间，在后的做为结束时间，间隔只保留绝对值正数\n\t *\n\t * @param begin 起始时间\n\t * @param end   结束时间\n\t * @return DateBetween\n\t * @since 3.2.3\n\t */\n\tpublic static DateBetween create(Date begin, Date end) {\n\t\treturn new DateBetween(begin, end);\n\t}\n\n\t/**\n\t * 创建<br>\n\t * 在前的日期做为起始时间，在后的做为结束时间，间隔只保留绝对值正数\n\t *\n\t * @param begin 起始时间\n\t * @param end   结束时间\n\t * @param isAbs 日期间隔是否只保留绝对值正数\n\t * @return DateBetween\n\t * @since 3.2.3\n\t */\n\tpublic static DateBetween create(Date begin, Date end, boolean isAbs) {\n\t\treturn new DateBetween(begin, end, isAbs);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 在前的日期做为起始时间，在后的做为结束时间，间隔只保留绝对值正数\n\t *\n\t * @param begin 起始时间\n\t * @param end   结束时间\n\t */\n\tpublic DateBetween(Date begin, Date end) {\n\t\tthis(begin, end, true);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 在前的日期做为起始时间，在后的做为结束时间\n\t *\n\t * @param begin 起始时间\n\t * @param end   结束时间\n\t * @param isAbs 日期间隔是否只保留绝对值正数\n\t * @since 3.1.1\n\t */\n\tpublic DateBetween(Date begin, Date end, boolean isAbs) {\n\t\tAssert.notNull(begin, \"Begin date is null !\");\n\t\tAssert.notNull(end, \"End date is null !\");\n\n\t\t// defensive copy\n\t\tDate b = new Date(begin.getTime());\n\t\tDate e = new Date(end.getTime());\n\n\t\tif (isAbs && begin.after(end)) {\n\t\t\t// 间隔只为正数的情况下，如果开始日期晚于结束日期，置换之\n\t\t\tthis.begin = e;\n\t\t\tthis.end = b;\n\t\t} else {\n\t\t\tthis.begin = b;\n\t\t\tthis.end = e;\n\t\t}\n\t}\n\n\t/**\n\t * 判断两个日期相差的时长<br>\n\t * 返回 给定单位的时长差\n\t *\n\t * @param unit 相差的单位：相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等\n\t * @return 时长差\n\t */\n\tpublic long between(DateUnit unit) {\n\t\tlong diff = end.getTime() - begin.getTime();\n\t\treturn diff / unit.getMillis();\n\t}\n\n\t/**\n\t * 计算两个日期相差月数<br>\n\t * 在非重置情况下，如果起始日期的天大于结束日期的天，月数要少算1（不足1个月）\n\t *\n\t * @param isReset 是否重置时间为起始时间（重置天时分秒）\n\t * @return 相差月数\n\t * @since 3.0.8\n\t */\n\tpublic long betweenMonth(boolean isReset) {\n\t\tfinal Calendar beginCal = DateUtil.calendar(begin);\n\t\tfinal Calendar endCal = DateUtil.calendar(end);\n\n\t\tfinal int betweenYear = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR);\n\t\tfinal int betweenMonthOfYear = endCal.get(Calendar.MONTH) - beginCal.get(Calendar.MONTH);\n\n\t\tint result = betweenYear * 12 + betweenMonthOfYear;\n\t\tif (false == isReset) {\n\t\t\tendCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR));\n\t\t\tendCal.set(Calendar.MONTH, beginCal.get(Calendar.MONTH));\n\t\t\tlong between = endCal.getTimeInMillis() - beginCal.getTimeInMillis();\n\t\t\tif (between < 0) {\n\t\t\t\treturn result - 1;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算两个日期相差年数<br>\n\t * 在非重置情况下，如果起始日期的月大于结束日期的月，年数要少算1（不足1年）\n\t *\n\t * @param isReset 是否重置时间为起始时间（重置月天时分秒）\n\t * @return 相差年数\n\t * @since 3.0.8\n\t */\n\tpublic long betweenYear(boolean isReset) {\n\t\tfinal Calendar beginCal = DateUtil.calendar(begin);\n\t\tfinal Calendar endCal = DateUtil.calendar(end);\n\n\t\tint result = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR);\n\t\tif (false == isReset) {\n\t\t\tfinal int beginMonthBase0 = beginCal.get(Calendar.MONTH);\n\t\t\tfinal int endMonthBase0 = endCal.get(Calendar.MONTH);\n\t\t\tif (beginMonthBase0 < endMonthBase0) {\n\t\t\t\treturn result;\n\t\t\t} else if (beginMonthBase0 > endMonthBase0) {\n\t\t\t\treturn result - 1;\n\t\t\t} else if (Calendar.FEBRUARY == beginMonthBase0\n\t\t\t\t&& CalendarUtil.isLastDayOfMonth(beginCal)\n\t\t\t\t&& CalendarUtil.isLastDayOfMonth(endCal)) {\n\t\t\t\t// 考虑闰年的2月情况\n\t\t\t\t// 两个日期都位于2月的最后一天，此时月数按照相等对待，此时都设置为1号\n\t\t\t\tbeginCal.set(Calendar.DAY_OF_MONTH, 1);\n\t\t\t\tendCal.set(Calendar.DAY_OF_MONTH, 1);\n\t\t\t}\n\n\t\t\tendCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR));\n\t\t\tlong between = endCal.getTimeInMillis() - beginCal.getTimeInMillis();\n\t\t\tif (between < 0) {\n\t\t\t\treturn result - 1;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 格式化输出时间差\n\t *\n\t * @param unit  日期单位\n\t * @param level 级别\n\t * @return 字符串\n\t * @since 5.7.17\n\t */\n\tpublic String toString(DateUnit unit, BetweenFormatter.Level level) {\n\t\treturn DateUtil.formatBetween(between(unit), level);\n\t}\n\n\t/**\n\t * 格式化输出时间差\n\t *\n\t * @param level 级别\n\t * @return 字符串\n\t */\n\tpublic String toString(BetweenFormatter.Level level) {\n\t\treturn toString(DateUnit.MS, level);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn toString(BetweenFormatter.Level.MILLISECOND);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateException.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 工具类异常\n * @author xiaoleilu\n */\npublic class DateException extends RuntimeException{\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic DateException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic DateException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic DateException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic DateException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic DateException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateField.java",
    "content": "package cn.hutool.core.date;\n\nimport java.util.Calendar;\n\n/**\n * 日期各个部分的枚举<br>\n * 与Calendar相应值对应\n *\n * @author Looly\n *\n */\npublic enum DateField {\n\n\t/**\n\t * 世纪\n\t *\n\t * @see Calendar#ERA\n\t */\n\tERA(Calendar.ERA),\n\t/**\n\t * 年\n\t *\n\t * @see Calendar#YEAR\n\t */\n\tYEAR(Calendar.YEAR),\n\t/**\n\t * 月\n\t *\n\t * @see Calendar#MONTH\n\t */\n\tMONTH(Calendar.MONTH),\n\t/**\n\t * 一年中第几周\n\t *\n\t * @see Calendar#WEEK_OF_YEAR\n\t */\n\tWEEK_OF_YEAR(Calendar.WEEK_OF_YEAR),\n\t/**\n\t * 一月中第几周\n\t *\n\t * @see Calendar#WEEK_OF_MONTH\n\t */\n\tWEEK_OF_MONTH(Calendar.WEEK_OF_MONTH),\n\t/**\n\t * 一月中的第几天\n\t *\n\t * @see Calendar#DAY_OF_MONTH\n\t */\n\tDAY_OF_MONTH(Calendar.DAY_OF_MONTH),\n\t/**\n\t * 一年中的第几天\n\t *\n\t * @see Calendar#DAY_OF_YEAR\n\t */\n\tDAY_OF_YEAR(Calendar.DAY_OF_YEAR),\n\t/**\n\t * 周几，1表示周日，2表示周一\n\t *\n\t * @see Calendar#DAY_OF_WEEK\n\t */\n\tDAY_OF_WEEK(Calendar.DAY_OF_WEEK),\n\t/**\n\t * 天所在的周是这个月的第几周\n\t *\n\t * @see Calendar#DAY_OF_WEEK_IN_MONTH\n\t */\n\tDAY_OF_WEEK_IN_MONTH(Calendar.DAY_OF_WEEK_IN_MONTH),\n\t/**\n\t * 上午或者下午\n\t *\n\t * @see Calendar#AM_PM\n\t */\n\tAM_PM(Calendar.AM_PM),\n\t/**\n\t * 小时，用于12小时制\n\t *\n\t * @see Calendar#HOUR\n\t */\n\tHOUR(Calendar.HOUR),\n\t/**\n\t * 小时，用于24小时制\n\t *\n\t * @see Calendar#HOUR\n\t */\n\tHOUR_OF_DAY(Calendar.HOUR_OF_DAY),\n\t/**\n\t * 分钟\n\t *\n\t * @see Calendar#MINUTE\n\t */\n\tMINUTE(Calendar.MINUTE),\n\t/**\n\t * 秒\n\t *\n\t * @see Calendar#SECOND\n\t */\n\tSECOND(Calendar.SECOND),\n\t/**\n\t * 毫秒\n\t *\n\t * @see Calendar#MILLISECOND\n\t */\n\tMILLISECOND(Calendar.MILLISECOND);\n\n\t// ---------------------------------------------------------------\n\tprivate final int value;\n\n\tDateField(int value) {\n\t\tthis.value = value;\n\t}\n\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 将 {@link Calendar}相关值转换为DatePart枚举对象<br>\n\t *\n\t * @param calendarPartIntValue Calendar中关于Week的int值\n\t * @return DateField\n\t */\n\tpublic static DateField of(int calendarPartIntValue) {\n\t\tswitch (calendarPartIntValue) {\n\t\tcase Calendar.ERA:\n\t\t\treturn ERA;\n\t\tcase Calendar.YEAR:\n\t\t\treturn YEAR;\n\t\tcase Calendar.MONTH:\n\t\t\treturn MONTH;\n\t\tcase Calendar.WEEK_OF_YEAR:\n\t\t\treturn WEEK_OF_YEAR;\n\t\tcase Calendar.WEEK_OF_MONTH:\n\t\t\treturn WEEK_OF_MONTH;\n\t\tcase Calendar.DAY_OF_MONTH:\n\t\t\treturn DAY_OF_MONTH;\n\t\tcase Calendar.DAY_OF_YEAR:\n\t\t\treturn DAY_OF_YEAR;\n\t\tcase Calendar.DAY_OF_WEEK:\n\t\t\treturn DAY_OF_WEEK;\n\t\tcase Calendar.DAY_OF_WEEK_IN_MONTH:\n\t\t\treturn DAY_OF_WEEK_IN_MONTH;\n\t\tcase Calendar.AM_PM:\n\t\t\treturn AM_PM;\n\t\tcase Calendar.HOUR:\n\t\t\treturn HOUR;\n\t\tcase Calendar.HOUR_OF_DAY:\n\t\t\treturn HOUR_OF_DAY;\n\t\tcase Calendar.MINUTE:\n\t\t\treturn MINUTE;\n\t\tcase Calendar.SECOND:\n\t\t\treturn SECOND;\n\t\tcase Calendar.MILLISECOND:\n\t\t\treturn MILLISECOND;\n\t\tdefault:\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateModifier.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.util.Calendar;\n\n/**\n * 日期修改器<br>\n * 用于实现自定义某个日期字段的调整，包括：\n *\n * <pre>\n * 1. 获取指定字段的起始时间\n * 2. 获取指定字段的四舍五入时间\n * 3. 获取指定字段的结束时间\n * </pre>\n *\n * @author looly\n */\npublic class DateModifier {\n\n\t/**\n\t * 忽略的计算的字段\n\t */\n\tprivate static final int[] IGNORE_FIELDS = new int[]{ //\n\t\t\tCalendar.HOUR_OF_DAY, // 与HOUR同名\n\t\t\tCalendar.AM_PM, // 此字段单独处理，不参与计算起始和结束\n\t\t\tCalendar.DAY_OF_WEEK_IN_MONTH, // 不参与计算\n\t\t\tCalendar.DAY_OF_YEAR, // DAY_OF_MONTH体现\n\t\t\tCalendar.WEEK_OF_MONTH, // 特殊处理\n\t\t\tCalendar.WEEK_OF_YEAR // WEEK_OF_MONTH体现\n\t};\n\n\t/**\n\t * 修改日期\n\t *\n\t * @param calendar   {@link Calendar}\n\t * @param dateField  日期字段，即保留到哪个日期字段\n\t * @param modifyType 修改类型，包括舍去、四舍五入、进一等\n\t * @return 修改后的{@link Calendar}\n\t */\n\tpublic static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType) {\n\t\treturn modify(calendar, dateField, modifyType, false);\n\t}\n\n\t/**\n\t * 修改日期，取起始值或者结束值<br>\n\t * 可选是否归零毫秒。\n\t *\n\t * <p>\n\t * 在{@link ModifyType#TRUNCATE}模式下，毫秒始终要归零,\n\t * 但是在{@link ModifyType#CEILING}和{@link ModifyType#ROUND}模式下，\n\t * 有时候由于毫秒部分必须为0（如MySQL数据库中），因此在此加上选项。\n\t * </p>\n\t *\n\t * @param calendar            {@link Calendar}\n\t * @param dateField           日期字段，即保留到哪个日期字段\n\t * @param modifyType          修改类型，包括舍去、四舍五入、进一等\n\t * @param truncateMillisecond 是否归零毫秒\n\t * @return 修改后的{@link Calendar}\n\t * @since 5.7.5\n\t */\n\tpublic static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType, boolean truncateMillisecond) {\n\t\t// AM_PM上下午特殊处理\n\t\tif (Calendar.AM_PM == dateField) {\n\t\t\tboolean isAM = DateUtil.isAM(calendar);\n\t\t\tswitch (modifyType) {\n\t\t\t\tcase TRUNCATE:\n\t\t\t\t\tcalendar.set(Calendar.HOUR_OF_DAY, isAM ? 0 : 12);\n\t\t\t\t\tbreak;\n\t\t\t\tcase CEILING:\n\t\t\t\t\tcalendar.set(Calendar.HOUR_OF_DAY, isAM ? 11 : 23);\n\t\t\t\t\tbreak;\n\t\t\t\tcase ROUND:\n\t\t\t\t\tint min = isAM ? 0 : 12;\n\t\t\t\t\tint max = isAM ? 11 : 23;\n\t\t\t\t\tint href = min + (max - min) / 2 + 1;\n\t\t\t\t\tint value = calendar.get(Calendar.HOUR_OF_DAY);\n\t\t\t\t\tcalendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// 处理下一级别字段\n\t\t\treturn modify(calendar, dateField + 1, modifyType, truncateMillisecond);\n\t\t}\n\n\t\tfinal int endField = truncateMillisecond ? Calendar.SECOND : Calendar.MILLISECOND;\n\t\t// 循环处理各级字段，精确到毫秒字段\n\t\tfor (int i = dateField + 1; i <= endField; i++) {\n\t\t\tif (ArrayUtil.contains(IGNORE_FIELDS, i)) {\n\t\t\t\t// 忽略无关字段（WEEK_OF_MONTH）始终不做修改\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 在计算本周的起始和结束日时，月相关的字段忽略。\n\t\t\tif (Calendar.WEEK_OF_MONTH == dateField || Calendar.WEEK_OF_YEAR == dateField) {\n\t\t\t\tif (Calendar.DAY_OF_MONTH == i) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 其它情况忽略周相关字段计算\n\t\t\t\tif (Calendar.DAY_OF_WEEK == i) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmodifyField(calendar, i, modifyType);\n\t\t}\n\n\t\tif (truncateMillisecond) {\n\t\t\tcalendar.set(Calendar.MILLISECOND, 0);\n\t\t}\n\n\t\treturn calendar;\n\t}\n\n\t// -------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 修改日期字段值\n\t *\n\t * @param calendar   {@link Calendar}\n\t * @param field      字段，见{@link Calendar}\n\t * @param modifyType {@link ModifyType}\n\t */\n\tprivate static void modifyField(Calendar calendar, int field, ModifyType modifyType) {\n\t\tif (Calendar.HOUR == field) {\n\t\t\t// 修正小时。HOUR为12小时制，上午的结束时间为12:00，此处改为HOUR_OF_DAY: 23:59\n\t\t\tfield = Calendar.HOUR_OF_DAY;\n\t\t}\n\n\t\tswitch (modifyType) {\n\t\t\tcase TRUNCATE:\n\t\t\t\tcalendar.set(field, DateUtil.getBeginValue(calendar, field));\n\t\t\t\tbreak;\n\t\t\tcase CEILING:\n\t\t\t\tcalendar.set(field, DateUtil.getEndValue(calendar, field));\n\t\t\t\tbreak;\n\t\t\tcase ROUND:\n\t\t\t\tint min = DateUtil.getBeginValue(calendar, field);\n\t\t\t\tint max = DateUtil.getEndValue(calendar, field);\n\t\t\t\tint href;\n\t\t\t\tif (Calendar.DAY_OF_WEEK == field) {\n\t\t\t\t\t// 星期特殊处理，假设周一是第一天，中间的为周四\n\t\t\t\t\thref = (min + 3) % 7;\n\t\t\t\t} else {\n\t\t\t\t\thref = (max - min) / 2 + 1;\n\t\t\t\t}\n\t\t\t\tint value = calendar.get(field);\n\t\t\t\tcalendar.set(field, (value < href) ? min : max);\n\t\t\t\tbreak;\n\t\t}\n\t\t// Console.log(\"# {} -> {}\", DateField.of(field), calendar.get(field));\n\t}\n\t// -------------------------------------------------------------------------------------------------- Private method end\n\n\t/**\n\t * 修改类型\n\t *\n\t * @author looly\n\t */\n\tpublic enum ModifyType {\n\t\t/**\n\t\t * 取指定日期短的起始值.\n\t\t */\n\t\tTRUNCATE,\n\n\t\t/**\n\t\t * 指定日期属性按照四舍五入处理\n\t\t */\n\t\tROUND,\n\n\t\t/**\n\t\t * 指定日期属性按照进一法处理\n\t\t */\n\t\tCEILING\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.format.FastDateFormat;\n\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport java.util.regex.Pattern;\n\n/**\n * 日期格式化类，提供常用的日期格式化对象\n *\n * @author Looly\n */\npublic class DatePattern {\n\n\t/**\n\t * 标准日期时间正则，每个字段支持单个数字或2个数字，包括：\n\t * <pre>\n\t *     yyyy-MM-dd HH:mm:ss.SSSSSS\n\t *     yyyy-MM-dd HH:mm:ss.SSS\n\t *     yyyy-MM-dd HH:mm:ss\n\t *     yyyy-MM-dd HH:mm\n\t *     yyyy-MM-dd\n\t * </pre>\n\t *\n\t * @since 5.3.6\n\t */\n\tpublic static final Pattern REGEX_NORM = Pattern.compile(\"\\\\d{4}-\\\\d{1,2}-\\\\d{1,2}(\\\\s\\\\d{1,2}:\\\\d{1,2}(:\\\\d{1,2})?(.\\\\d{1,6})?)?\");\n\n\t//-------------------------------------------------------------------------------------------------------------------------------- Normal\n\t/**\n\t * 年格式：yyyy\n\t */\n\tpublic static final String NORM_YEAR_PATTERN = \"yyyy\";\n\t/**\n\t * 年月格式：yyyy-MM\n\t */\n\tpublic static final String NORM_MONTH_PATTERN = \"yyyy-MM\";\n\t/**\n\t * 年月格式 {@link FastDateFormat}：yyyy-MM\n\t */\n\tpublic static final FastDateFormat NORM_MONTH_FORMAT = FastDateFormat.getInstance(NORM_MONTH_PATTERN);\n\t/**\n\t * 年月格式 {@link FastDateFormat}：yyyy-MM\n\t */\n\tpublic static final DateTimeFormatter NORM_MONTH_FORMATTER = createFormatter(NORM_MONTH_PATTERN);\n\n\t/**\n\t * 简单年月格式：yyyyMM\n\t */\n\tpublic static final String SIMPLE_MONTH_PATTERN = \"yyyyMM\";\n\t/**\n\t * 简单年月格式 {@link FastDateFormat}：yyyyMM\n\t */\n\tpublic static final FastDateFormat SIMPLE_MONTH_FORMAT = FastDateFormat.getInstance(SIMPLE_MONTH_PATTERN);\n\t/**\n\t * 简单年月格式 {@link FastDateFormat}：yyyyMM\n\t */\n\tpublic static final DateTimeFormatter SIMPLE_MONTH_FORMATTER = createFormatter(SIMPLE_MONTH_PATTERN);\n\n\t/**\n\t * 标准日期格式：yyyy-MM-dd\n\t */\n\tpublic static final String NORM_DATE_PATTERN = \"yyyy-MM-dd\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy-MM-dd\n\t */\n\tpublic static final FastDateFormat NORM_DATE_FORMAT = FastDateFormat.getInstance(NORM_DATE_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy-MM-dd\n\t */\n\tpublic static final DateTimeFormatter NORM_DATE_FORMATTER = createFormatter(NORM_DATE_PATTERN);\n\n\t/**\n\t * 标准时间格式：HH:mm:ss\n\t */\n\tpublic static final String NORM_TIME_PATTERN = \"HH:mm:ss\";\n\t/**\n\t * 标准时间格式 {@link FastDateFormat}：HH:mm:ss\n\t */\n\tpublic static final FastDateFormat NORM_TIME_FORMAT = FastDateFormat.getInstance(NORM_TIME_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：HH:mm:ss\n\t */\n\tpublic static final DateTimeFormatter NORM_TIME_FORMATTER = createFormatter(NORM_TIME_PATTERN);\n\n\t/**\n\t * 标准日期时间格式，精确到分：yyyy-MM-dd HH:mm\n\t */\n\tpublic static final String NORM_DATETIME_MINUTE_PATTERN = \"yyyy-MM-dd HH:mm\";\n\t/**\n\t * 标准日期时间格式，精确到分 {@link FastDateFormat}：yyyy-MM-dd HH:mm\n\t */\n\tpublic static final FastDateFormat NORM_DATETIME_MINUTE_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MINUTE_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy-MM-dd HH:mm\n\t */\n\tpublic static final DateTimeFormatter NORM_DATETIME_MINUTE_FORMATTER = createFormatter(NORM_DATETIME_MINUTE_PATTERN);\n\n\t/**\n\t * 标准日期时间格式，精确到秒：yyyy-MM-dd HH:mm:ss\n\t */\n\tpublic static final String NORM_DATETIME_PATTERN = \"yyyy-MM-dd HH:mm:ss\";\n\t/**\n\t * 标准日期时间格式，精确到秒 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss\n\t */\n\tpublic static final FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN);\n\t/**\n\t * 标准日期时间格式，精确到秒 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss\n\t */\n\tpublic static final DateTimeFormatter NORM_DATETIME_FORMATTER = createFormatter(NORM_DATETIME_PATTERN);\n\n\t/**\n\t * 标准日期时间格式，精确到毫秒：yyyy-MM-dd HH:mm:ss.SSS\n\t */\n\tpublic static final String NORM_DATETIME_MS_PATTERN = \"yyyy-MM-dd HH:mm:ss.SSS\";\n\t/**\n\t * 标准日期时间格式，精确到毫秒 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss.SSS\n\t */\n\tpublic static final FastDateFormat NORM_DATETIME_MS_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MS_PATTERN);\n\t/**\n\t * 标准日期时间格式，精确到毫秒 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss.SSS\n\t */\n\tpublic static final DateTimeFormatter NORM_DATETIME_MS_FORMATTER = createFormatter(NORM_DATETIME_MS_PATTERN);\n\n\t/**\n\t * ISO8601日期时间格式，精确到毫秒：yyyy-MM-dd HH:mm:ss,SSS\n\t */\n\tpublic static final String ISO8601_PATTERN = \"yyyy-MM-dd HH:mm:ss,SSS\";\n\t/**\n\t * ISO8601日期时间格式，精确到毫秒 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss,SSS\n\t */\n\tpublic static final FastDateFormat ISO8601_FORMAT = FastDateFormat.getInstance(ISO8601_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy-MM-dd HH:mm:ss,SSS\n\t */\n\tpublic static final DateTimeFormatter ISO8601_FORMATTER = createFormatter(ISO8601_PATTERN);\n\n\t/**\n\t * 标准日期格式：yyyy年MM月dd日\n\t */\n\tpublic static final String CHINESE_DATE_PATTERN = \"yyyy年MM月dd日\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy年MM月dd日\n\t */\n\tpublic static final FastDateFormat CHINESE_DATE_FORMAT = FastDateFormat.getInstance(CHINESE_DATE_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy年MM月dd日\n\t */\n\tpublic static final DateTimeFormatter CHINESE_DATE_FORMATTER = createFormatter(CHINESE_DATE_PATTERN);\n\n\t/**\n\t * 标准日期格式：yyyy年MM月dd日 HH时mm分ss秒\n\t */\n\tpublic static final String CHINESE_DATE_TIME_PATTERN = \"yyyy年MM月dd日HH时mm分ss秒\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy年MM月dd日HH时mm分ss秒\n\t */\n\tpublic static final FastDateFormat CHINESE_DATE_TIME_FORMAT = FastDateFormat.getInstance(CHINESE_DATE_TIME_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyy年MM月dd日HH时mm分ss秒\n\t */\n\tpublic static final DateTimeFormatter CHINESE_DATE_TIME_FORMATTER = createFormatter(CHINESE_DATE_TIME_PATTERN);\n\n\t//-------------------------------------------------------------------------------------------------------------------------------- Pure\n\t/**\n\t * 标准日期格式：yyyyMMdd\n\t */\n\tpublic static final String PURE_DATE_PATTERN = \"yyyyMMdd\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMdd\n\t */\n\tpublic static final FastDateFormat PURE_DATE_FORMAT = FastDateFormat.getInstance(PURE_DATE_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMdd\n\t */\n\tpublic static final DateTimeFormatter PURE_DATE_FORMATTER = createFormatter(PURE_DATE_PATTERN);\n\n\t/**\n\t * 标准日期格式：HHmmss\n\t */\n\tpublic static final String PURE_TIME_PATTERN = \"HHmmss\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：HHmmss\n\t */\n\tpublic static final FastDateFormat PURE_TIME_FORMAT = FastDateFormat.getInstance(PURE_TIME_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：HHmmss\n\t */\n\tpublic static final DateTimeFormatter PURE_TIME_FORMATTER = createFormatter(PURE_TIME_PATTERN);\n\n\t/**\n\t * 标准日期格式：yyyyMMddHHmmss\n\t */\n\tpublic static final String PURE_DATETIME_PATTERN = \"yyyyMMddHHmmss\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMddHHmmss\n\t */\n\tpublic static final FastDateFormat PURE_DATETIME_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMddHHmmss\n\t */\n\tpublic static final DateTimeFormatter PURE_DATETIME_FORMATTER = createFormatter(PURE_DATETIME_PATTERN);\n\n\t/**\n\t * 标准日期格式：yyyyMMddHHmmssSSS\n\t */\n\tpublic static final String PURE_DATETIME_MS_PATTERN = \"yyyyMMddHHmmssSSS\";\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMddHHmmssSSS\n\t */\n\tpublic static final FastDateFormat PURE_DATETIME_MS_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_MS_PATTERN);\n\t/**\n\t * 标准日期格式 {@link FastDateFormat}：yyyyMMddHHmmssSSS\n\t */\n\tpublic static final DateTimeFormatter PURE_DATETIME_MS_FORMATTER = createFormatter(PURE_DATETIME_MS_PATTERN);\n\n\t//-------------------------------------------------------------------------------------------------------------------------------- Others\n\t/**\n\t * HTTP头中日期时间格式：EEE, dd MMM yyyy HH:mm:ss z\n\t */\n\tpublic static final String HTTP_DATETIME_PATTERN = \"EEE, dd MMM yyyy HH:mm:ss z\";\n\t/**\n\t * HTTP头中日期时间格式 {@link FastDateFormat}：EEE, dd MMM yyyy HH:mm:ss z\n\t */\n\tpublic static final FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN, TimeZone.getTimeZone(\"GMT\"), Locale.US);\n\t/**\n\t * HTTP头中日期时间格式 {@link FastDateFormat}：EEE, dd MMM yyyy HH:mm:ss z\n\t */\n\tpublic static final FastDateFormat HTTP_DATETIME_FORMAT_Z = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN, Locale.US);\n\n\t/**\n\t * JDK中日期时间格式：EEE MMM dd HH:mm:ss zzz yyyy\n\t */\n\tpublic static final String JDK_DATETIME_PATTERN = \"EEE MMM dd HH:mm:ss zzz yyyy\";\n\t/**\n\t * JDK中日期时间格式 {@link FastDateFormat}：EEE MMM dd HH:mm:ss zzz yyyy\n\t */\n\tpublic static final FastDateFormat JDK_DATETIME_FORMAT = FastDateFormat.getInstance(JDK_DATETIME_PATTERN, Locale.US);\n\n\t/**\n\t * ISO8601时间：yyyy-MM-dd'T'HH:mm:ss\n\t */\n\tpublic static final String UTC_SIMPLE_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss\";\n\t/**\n\t * ISO8601时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ss\n\t */\n\tpublic static final FastDateFormat UTC_SIMPLE_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_PATTERN);\n\n\t/**\n\t * ISO8601时间：yyyy-MM-dd'T'HH:mm:ss.SSS\n\t */\n\tpublic static final String UTC_SIMPLE_MS_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss.SSS\";\n\t/**\n\t * ISO8601时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ss.SSS\n\t */\n\tpublic static final FastDateFormat UTC_SIMPLE_MS_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_MS_PATTERN);\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ss'Z'\n\t */\n\tpublic static final String UTC_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss'Z'\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ss'Z'\n\t */\n\tpublic static final FastDateFormat UTC_FORMAT = FastDateFormat.getInstance(UTC_PATTERN, TimeZone.getTimeZone(\"UTC\"));\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ssZ\n\t */\n\tpublic static final String UTC_WITH_ZONE_OFFSET_PATTERN = \"yyyy-MM-dd'T'HH:mm:ssZ\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ssZ\n\t */\n\tpublic static final FastDateFormat UTC_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone(\"UTC\"));\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ssXXX\n\t */\n\tpublic static final String UTC_WITH_XXX_OFFSET_PATTERN = \"yyyy-MM-dd'T'HH:mm:ssXXX\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ssXXX\n\t */\n\tpublic static final FastDateFormat UTC_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_XXX_OFFSET_PATTERN);\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mmXXX\n\t */\n\tpublic static final String UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_PATTERN = \"yyyy-MM-dd'T'HH:mmXXX\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mmXXX\n\t */\n\tpublic static final FastDateFormat UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_PATTERN);\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\n\t */\n\tpublic static final String UTC_MS_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\n\t */\n\tpublic static final FastDateFormat UTC_MS_FORMAT = FastDateFormat.getInstance(UTC_MS_PATTERN, TimeZone.getTimeZone(\"UTC\"));\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ssZ\n\t */\n\tpublic static final String UTC_MS_WITH_ZONE_OFFSET_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ssZ\n\t */\n\tpublic static final FastDateFormat UTC_MS_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_MS_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone(\"UTC\"));\n\n\t/**\n\t * UTC时间：yyyy-MM-dd'T'HH:mm:ss.SSSXXX\n\t */\n\tpublic static final String UTC_MS_WITH_XXX_OFFSET_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\";\n\t/**\n\t * UTC时间{@link FastDateFormat}：yyyy-MM-dd'T'HH:mm:ss.SSSXXX\n\t */\n\tpublic static final FastDateFormat UTC_MS_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_MS_WITH_XXX_OFFSET_PATTERN);\n\n\t/**\n\t * 创建并为 {@link DateTimeFormatter} 赋予默认时区和位置信息，默认值为系统默认值。\n\t *\n\t * @param pattern 日期格式\n\t * @return {@link DateTimeFormatter}\n\t * @since 5.7.5\n\t */\n\tpublic static DateTimeFormatter createFormatter(String pattern) {\n\t\treturn DateTimeFormatter.ofPattern(pattern, Locale.getDefault())\n\t\t\t\t.withZone(ZoneId.systemDefault());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateRange.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Range;\n\nimport java.util.Date;\n\n/**\n * 日期范围\n *\n * @author looly\n * @since 4.1.0\n */\npublic class DateRange extends Range<DateTime> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造，包含开始和结束日期时间\n\t *\n\t * @param start 起始日期时间（包括）\n\t * @param end 结束日期时间（包括）\n\t * @param unit 步进单位\n\t */\n\tpublic DateRange(Date start, Date end, DateField unit) {\n\t\tthis(start, end, unit, 1);\n\t}\n\n\t/**\n\t * 构造，包含开始和结束日期时间\n\t *\n\t * @param start 起始日期时间（包括）\n\t * @param end 结束日期时间（包括）\n\t * @param unit 步进单位\n\t * @param step 步进数\n\t */\n\tpublic DateRange(Date start, Date end, DateField unit, int step) {\n\t\tthis(start, end, unit, step, true, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param start 起始日期时间\n\t * @param end 结束日期时间\n\t * @param unit 步进单位\n\t * @param step 步进数\n\t * @param isIncludeStart 是否包含开始的时间\n\t * @param isIncludeEnd 是否包含结束的时间\n\t */\n\tpublic DateRange(Date start, Date end, DateField unit, int step, boolean isIncludeStart, boolean isIncludeEnd) {\n\t\tsuper(DateUtil.date(start), DateUtil.date(end), (current, end1, index) -> {\n\t\t\tif(step <= 0){\n\t\t\t\t// issue#3783\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfinal DateTime dt = DateUtil.date(start).offsetNew(unit, (index + 1) * step);\n\t\t\tif (dt.isAfter(end1)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn dt;\n\t\t}, isIncludeStart, isIncludeEnd);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateTime.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.format.DateParser;\nimport cn.hutool.core.date.format.DatePrinter;\nimport cn.hutool.core.date.format.FastDateFormat;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.SystemPropsUtil;\n\nimport java.sql.Timestamp;\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * 包装{@link Date}<br>\n * 此类继承了{@link Date}，并提供扩展方法，如时区等。<br>\n * 此类重写了父类的{@code toString()}方法，返回值为\"yyyy-MM-dd HH:mm:ss\"格式\n *\n * @author xiaoleilu\n */\npublic class DateTime extends Date {\n\tprivate static final long serialVersionUID = -5395712593979185936L;\n\n\tprivate static boolean useJdkToStringStyle = false;\n\n\t/**\n\t * 设置全局的，是否使用{@link Date}默认的toString()格式<br>\n\t * 如果为{@code true}，则调用toString()时返回\"EEE MMM dd HH:mm:ss zzz yyyy\"格式，<br>\n\t * 如果为{@code false}，则返回\"yyyy-MM-dd HH:mm:ss\"，<br>\n\t * 默认为{@code false}\n\t *\n\t * @param customUseJdkToStringStyle 是否使用{@link Date}默认的toString()格式\n\t * @since 5.7.21\n\t */\n\tpublic static void setUseJdkToStringStyle(boolean customUseJdkToStringStyle){\n\t\tuseJdkToStringStyle = customUseJdkToStringStyle;\n\t}\n\n\t/**\n\t * 是否可变对象\n\t */\n\tprivate boolean mutable = true;\n\t/**\n\t * 一周的第一天，默认是周一， 在设置或获得 WEEK_OF_MONTH 或 WEEK_OF_YEAR 字段时，Calendar 必须确定一个月或一年的第一个星期，以此作为参考点。\n\t */\n\tprivate Week firstDayOfWeek = Week.MONDAY;\n\t/**\n\t * 时区\n\t */\n\tprivate TimeZone timeZone;\n\n\t/**\n\t * 第一周最少天数\n\t */\n\tprivate int minimalDaysInFirstWeek;\n\n\t/**\n\t * 转换时间戳为 DateTime\n\t *\n\t * @param timeMillis 时间戳，毫秒数\n\t * @return DateTime\n\t * @since 4.6.3\n\t */\n\tpublic static DateTime of(long timeMillis) {\n\t\treturn new DateTime(timeMillis);\n\t}\n\n\t/**\n\t * 转换JDK date为 DateTime\n\t *\n\t * @param date JDK Date\n\t * @return DateTime\n\t */\n\tpublic static DateTime of(Date date) {\n\t\tif (date instanceof DateTime) {\n\t\t\treturn (DateTime) date;\n\t\t}\n\t\treturn new DateTime(date);\n\t}\n\n\t/**\n\t * 转换 {@link Calendar} 为 DateTime\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return DateTime\n\t */\n\tpublic static DateTime of(Calendar calendar) {\n\t\treturn new DateTime(calendar);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dateStr Date字符串\n\t * @param format  格式\n\t * @return this\n\t * @see DatePattern\n\t */\n\tpublic static DateTime of(String dateStr, String format) {\n\t\treturn new DateTime(dateStr, format);\n\t}\n\n\t/**\n\t * 现在的时间\n\t *\n\t * @return 现在的时间\n\t */\n\tpublic static DateTime now() {\n\t\treturn new DateTime();\n\t}\n\n\t// -------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 当前时间\n\t */\n\tpublic DateTime() {\n\t\tthis(TimeZone.getDefault());\n\t}\n\n\t/**\n\t * 当前时间\n\t *\n\t * @param timeZone 时区\n\t * @since 4.1.2\n\t */\n\tpublic DateTime(TimeZone timeZone) {\n\t\tthis(System.currentTimeMillis(), timeZone);\n\t}\n\n\t/**\n\t * 给定日期的构造\n\t *\n\t * @param date 日期\n\t */\n\tpublic DateTime(Date date) {\n\t\tthis(\n\t\t\t\tdate,//\n\t\t\t\t(date instanceof DateTime) ? ((DateTime) date).timeZone : TimeZone.getDefault()\n\t\t);\n\t}\n\n\t/**\n\t * 给定日期的构造\n\t *\n\t * @param date     日期\n\t * @param timeZone 时区\n\t * @since 4.1.2\n\t */\n\tpublic DateTime(Date date, TimeZone timeZone) {\n\t\tthis(ObjectUtil.defaultIfNull(date, () -> new Date()).getTime(), timeZone);\n\t}\n\n\t/**\n\t * 给定日期的构造\n\t *\n\t * @param calendar {@link Calendar}\n\t */\n\tpublic DateTime(Calendar calendar) {\n\t\tthis(calendar.getTime(), calendar.getTimeZone());\n\t\tthis.setFirstDayOfWeek(Week.of(calendar.getFirstDayOfWeek()));\n\t}\n\n\t/**\n\t * 给定日期Instant的构造\n\t *\n\t * @param instant {@link Instant} 对象\n\t * @since 5.0.0\n\t */\n\tpublic DateTime(Instant instant) {\n\t\tthis(instant.toEpochMilli());\n\t}\n\n\t/**\n\t * 给定日期Instant的构造\n\t *\n\t * @param instant {@link Instant} 对象\n\t * @param zoneId  时区ID\n\t * @since 5.0.5\n\t */\n\tpublic DateTime(Instant instant, ZoneId zoneId) {\n\t\tthis(instant.toEpochMilli(), ZoneUtil.toTimeZone(zoneId));\n\t}\n\n\t/**\n\t * 给定日期TemporalAccessor的构造\n\t *\n\t * @param temporalAccessor {@link TemporalAccessor} 对象\n\t * @since 5.0.0\n\t */\n\tpublic DateTime(TemporalAccessor temporalAccessor) {\n\t\tthis(TemporalAccessorUtil.toInstant(temporalAccessor));\n\t}\n\n\t/**\n\t * 给定日期ZonedDateTime的构造\n\t *\n\t * @param zonedDateTime {@link ZonedDateTime} 对象\n\t * @since 5.0.5\n\t */\n\tpublic DateTime(ZonedDateTime zonedDateTime) {\n\t\tthis(zonedDateTime.toInstant(), zonedDateTime.getZone());\n\t}\n\n\t/**\n\t * 给定日期毫秒数的构造\n\t *\n\t * @param timeMillis 日期毫秒数\n\t * @since 4.1.2\n\t */\n\tpublic DateTime(long timeMillis) {\n\t\tthis(timeMillis, TimeZone.getDefault());\n\t}\n\n\t/**\n\t * 给定日期毫秒数的构造\n\t *\n\t * @param timeMillis 日期毫秒数\n\t * @param timeZone   时区\n\t * @since 4.1.2\n\t */\n\tpublic DateTime(long timeMillis, TimeZone timeZone) {\n\t\tsuper(timeMillis);\n\t\tthis.timeZone = ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault);\n\t}\n\n\t/**\n\t * 构造格式：<br>\n\t * <ol>\n\t * <li>yyyy-MM-dd HH:mm:ss</li>\n\t * <li>yyyy/MM/dd HH:mm:ss</li>\n\t * <li>yyyy.MM.dd HH:mm:ss</li>\n\t * <li>yyyy年MM月dd日 HH时mm分ss秒</li>\n\t * <li>yyyy-MM-dd</li>\n\t * <li>yyyy/MM/dd</li>\n\t * <li>yyyy.MM.dd</li>\n\t * <li>HH:mm:ss</li>\n\t * <li>HH时mm分ss秒</li>\n\t * <li>yyyy-MM-dd HH:mm</li>\n\t * <li>yyyy-MM-dd HH:mm:ss.SSS</li>\n\t * <li>yyyyMMddHHmmss</li>\n\t * <li>yyyyMMddHHmmssSSS</li>\n\t * <li>yyyyMMdd</li>\n\t * <li>EEE, dd MMM yyyy HH:mm:ss z</li>\n\t * <li>EEE MMM dd HH:mm:ss zzz yyyy</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ssZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZ</li>\n\t * </ol>\n\t *\n\t * @param dateStr Date字符串\n\t * @since 5.6.2\n\t */\n\tpublic DateTime(CharSequence dateStr) {\n\t\tthis(DateUtil.parse(dateStr));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dateStr Date字符串\n\t * @param format  格式\n\t * @see DatePattern\n\t */\n\tpublic DateTime(CharSequence dateStr, String format) {\n\t\tthis(GlobalCustomFormat.isCustomFormat(format)\n\t\t\t\t? GlobalCustomFormat.parse(dateStr, format)\n\t\t\t\t: parse(dateStr, DateUtil.newSimpleFormat(format)));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dateStr    Date字符串\n\t * @param dateFormat 格式化器 {@link SimpleDateFormat}\n\t * @see DatePattern\n\t */\n\tpublic DateTime(CharSequence dateStr, DateFormat dateFormat) {\n\t\tthis(parse(dateStr, dateFormat), dateFormat.getTimeZone());\n\t}\n\n\t/**\n\t * 构建DateTime对象\n\t *\n\t * @param dateStr   Date字符串\n\t * @param formatter 格式化器,{@link DateTimeFormatter}\n\t * @since 5.0.0\n\t */\n\tpublic DateTime(CharSequence dateStr, DateTimeFormatter formatter) {\n\t\tthis(TemporalAccessorUtil.toInstant(formatter.parse(dateStr)), formatter.getZone());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dateStr    Date字符串\n\t * @param dateParser 格式化器 {@link DateParser}，可以使用 {@link FastDateFormat}\n\t * @see DatePattern\n\t */\n\tpublic DateTime(CharSequence dateStr, DateParser dateParser) {\n\t\tthis(dateStr, dateParser, SystemPropsUtil.getBoolean(SystemPropsUtil.HUTOOL_DATE_LENIENT, true));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param dateStr    Date字符串\n\t * @param dateParser 格式化器 {@link DateParser}，可以使用 {@link FastDateFormat}\n\t * @param lenient    是否宽容模式\n\t * @see DatePattern\n\t */\n\tpublic DateTime(CharSequence dateStr, DateParser dateParser, boolean lenient) {\n\t\tthis(parse(dateStr, dateParser, lenient));\n\t}\n\n\t// -------------------------------------------------------------------- Constructor end\n\n\t// -------------------------------------------------------------------- offset start\n\n\t/**\n\t * 调整日期和时间<br>\n\t * 如果此对象为可变对象，返回自身，否则返回新对象，设置是否可变对象见{@link #setMutable(boolean)}\n\t *\n\t * @param datePart 调整的部分 {@link DateField}\n\t * @param offset   偏移量，正数为向后偏移，负数为向前偏移\n\t * @return 如果此对象为可变对象，返回自身，否则返回新对象\n\t */\n\tpublic DateTime offset(DateField datePart, int offset) {\n\t\tif (DateField.ERA == datePart) {\n\t\t\tthrow new IllegalArgumentException(\"ERA is not support offset!\");\n\t\t}\n\n\t\tfinal Calendar cal = toCalendar();\n\t\t//noinspection MagicConstant\n\t\tcal.add(datePart.getValue(), offset);\n\n\t\tDateTime dt = mutable ? this : ObjectUtil.clone(this);\n\t\treturn dt.setTimeInternal(cal.getTimeInMillis());\n\t}\n\n\t/**\n\t * 调整日期和时间<br>\n\t * 返回调整后的新DateTime，不影响原对象\n\t *\n\t * @param datePart 调整的部分 {@link DateField}\n\t * @param offset   偏移量，正数为向后偏移，负数为向前偏移\n\t * @return 如果此对象为可变对象，返回自身，否则返回新对象\n\t * @since 3.0.9\n\t */\n\tpublic DateTime offsetNew(DateField datePart, int offset) {\n\t\tfinal Calendar cal = toCalendar();\n\t\t//noinspection MagicConstant\n\t\tcal.add(datePart.getValue(), offset);\n\n\t\treturn ObjectUtil.clone(this).setTimeInternal(cal.getTimeInMillis());\n\t}\n\t// -------------------------------------------------------------------- offset end\n\n\t// -------------------------------------------------------------------- Part of Date start\n\n\t/**\n\t * 获得日期的某个部分<br>\n\t * 例如获得年的部分，则使用 getField(DatePart.YEAR)\n\t *\n\t * @param field 表示日期的哪个部分的枚举 {@link DateField}\n\t * @return 某个部分的值\n\t */\n\tpublic int getField(DateField field) {\n\t\treturn getField(field.getValue());\n\t}\n\n\t/**\n\t * 获得日期的某个部分<br>\n\t * 例如获得年的部分，则使用 getField(Calendar.YEAR)\n\t *\n\t * @param field 表示日期的哪个部分的int值 {@link Calendar}\n\t * @return 某个部分的值\n\t */\n\tpublic int getField(int field) {\n\t\treturn toCalendar().get(field);\n\t}\n\n\t/**\n\t * 设置日期的某个部分<br>\n\t * 如果此对象为可变对象，返回自身，否则返回新对象，设置是否可变对象见{@link #setMutable(boolean)}\n\t *\n\t * @param field 表示日期的哪个部分的枚举 {@link DateField}\n\t * @param value 值\n\t * @return this\n\t */\n\tpublic DateTime setField(DateField field, int value) {\n\t\treturn setField(field.getValue(), value);\n\t}\n\n\t/**\n\t * 设置日期的某个部分<br>\n\t * 如果此对象为可变对象，返回自身，否则返回新对象，设置是否可变对象见{@link #setMutable(boolean)}\n\t *\n\t * @param field 表示日期的哪个部分的int值 {@link Calendar}\n\t * @param value 值\n\t * @return this\n\t */\n\tpublic DateTime setField(int field, int value) {\n\t\tfinal Calendar calendar = toCalendar();\n\t\tcalendar.set(field, value);\n\n\t\tDateTime dt = this;\n\t\tif (false == mutable) {\n\t\t\tdt = ObjectUtil.clone(this);\n\t\t}\n\t\treturn dt.setTimeInternal(calendar.getTimeInMillis());\n\t}\n\n\t@Override\n\tpublic void setTime(long time) {\n\t\tif (mutable) {\n\t\t\tsuper.setTime(time);\n\t\t} else {\n\t\t\tthrow new DateException(\"This is not a mutable object !\");\n\t\t}\n\t}\n\n\t/**\n\t * 获得年的部分\n\t *\n\t * @return 年的部分\n\t */\n\tpublic int year() {\n\t\treturn getField(DateField.YEAR);\n\t}\n\n\t/**\n\t * 获得当前日期所属季度，从1开始计数<br>\n\t *\n\t * @return 第几个季度 {@link Quarter}\n\t */\n\tpublic int quarter() {\n\t\treturn month() / 3 + 1;\n\t}\n\n\t/**\n\t * 获得当前日期所属季度<br>\n\t *\n\t * @return 第几个季度 {@link Quarter}\n\t */\n\tpublic Quarter quarterEnum() {\n\t\treturn Quarter.of(quarter());\n\t}\n\n\t/**\n\t * 获得月份，从0开始计数\n\t *\n\t * @return 月份\n\t */\n\tpublic int month() {\n\t\treturn getField(DateField.MONTH);\n\t}\n\n\t/**\n\t * 获取月，从1开始计数\n\t *\n\t * @return 月份，1表示一月\n\t * @since 5.4.1\n\t */\n\tpublic int monthBaseOne() {\n\t\treturn month() + 1;\n\t}\n\n\t/**\n\t * 获得月份，从1开始计数<br>\n\t * 由于{@link Calendar} 中的月份按照0开始计数，导致某些需求容易误解，因此如果想用1表示一月，2表示二月则调用此方法\n\t *\n\t * @return 月份\n\t */\n\tpublic int monthStartFromOne() {\n\t\treturn month() + 1;\n\t}\n\n\t/**\n\t * 获得月份\n\t *\n\t * @return {@link Month}\n\t */\n\tpublic Month monthEnum() {\n\t\treturn Month.of(month());\n\t}\n\n\t/**\n\t * 获得指定日期是所在年份的第几周<br>\n\t * 此方法返回值与一周的第一天有关，比如：<br>\n\t * 2016年1月3日为周日，如果一周的第一天为周日，那这天是第二周（返回2）<br>\n\t * 如果一周的第一天为周一，那这天是第一周（返回1）<br>\n\t * 跨年的那个星期得到的结果总是1\n\t *\n\t * @return 周\n\t * @see #setFirstDayOfWeek(Week)\n\t */\n\tpublic int weekOfYear() {\n\t\treturn getField(DateField.WEEK_OF_YEAR);\n\t}\n\n\t/**\n\t * 获得指定日期是所在月份的第几周<br>\n\t * 此方法返回值与一周的第一天有关，比如：<br>\n\t * 2016年1月3日为周日，如果一周的第一天为周日，那这天是第二周（返回2）<br>\n\t * 如果一周的第一天为周一，那这天是第一周（返回1）\n\t *\n\t * @return 周\n\t * @see #setFirstDayOfWeek(Week)\n\t */\n\tpublic int weekOfMonth() {\n\t\treturn getField(DateField.WEEK_OF_MONTH);\n\t}\n\n\t/**\n\t * 获得指定日期是这个日期所在月份的第几天，从1开始\n\t *\n\t * @return 天，1表示第一天\n\t */\n\tpublic int dayOfMonth() {\n\t\treturn getField(DateField.DAY_OF_MONTH);\n\t}\n\n\t/**\n\t * 获得指定日期是这个日期所在年份的第几天，从1开始\n\t *\n\t * @return 天，1表示第一天\n\t * @since 5.3.6\n\t */\n\tpublic int dayOfYear() {\n\t\treturn getField(DateField.DAY_OF_YEAR);\n\t}\n\n\t/**\n\t * 获得指定日期是星期几，1表示周日，2表示周一\n\t *\n\t * @return 星期几\n\t */\n\tpublic int dayOfWeek() {\n\t\treturn getField(DateField.DAY_OF_WEEK);\n\t}\n\n\t/**\n\t * 获得天所在的周是这个月的第几周\n\t *\n\t * @return 天\n\t */\n\tpublic int dayOfWeekInMonth() {\n\t\treturn getField(DateField.DAY_OF_WEEK_IN_MONTH);\n\t}\n\n\t/**\n\t * 获得指定日期是星期几\n\t *\n\t * @return {@link Week}\n\t */\n\tpublic Week dayOfWeekEnum() {\n\t\treturn Week.of(dayOfWeek());\n\t}\n\n\t/**\n\t * 获得指定日期的小时数部分<br>\n\t *\n\t * @param is24HourClock 是否24小时制\n\t * @return 小时数\n\t */\n\tpublic int hour(boolean is24HourClock) {\n\t\treturn getField(is24HourClock ? DateField.HOUR_OF_DAY : DateField.HOUR);\n\t}\n\n\t/**\n\t * 获得指定日期的分钟数部分<br>\n\t * 例如：10:04:15.250 =》 4\n\t *\n\t * @return 分钟数\n\t */\n\tpublic int minute() {\n\t\treturn getField(DateField.MINUTE);\n\t}\n\n\t/**\n\t * 获得指定日期的秒数部分<br>\n\t *\n\t * @return 秒数\n\t */\n\tpublic int second() {\n\t\treturn getField(DateField.SECOND);\n\t}\n\n\t/**\n\t * 获得指定日期的毫秒数部分<br>\n\t *\n\t * @return 毫秒数\n\t */\n\tpublic int millisecond() {\n\t\treturn getField(DateField.MILLISECOND);\n\t}\n\n\t/**\n\t * 是否为上午\n\t *\n\t * @return 是否为上午\n\t */\n\tpublic boolean isAM() {\n\t\treturn Calendar.AM == getField(DateField.AM_PM);\n\t}\n\n\t/**\n\t * 是否为下午\n\t *\n\t * @return 是否为下午\n\t */\n\tpublic boolean isPM() {\n\t\treturn Calendar.PM == getField(DateField.AM_PM);\n\t}\n\n\t/**\n\t * 是否为周末，周末指周六或者周日\n\t *\n\t * @return 是否为周末，周末指周六或者周日\n\t * @since 4.1.14\n\t */\n\tpublic boolean isWeekend() {\n\t\tfinal int dayOfWeek = dayOfWeek();\n\t\treturn Calendar.SATURDAY == dayOfWeek || Calendar.SUNDAY == dayOfWeek;\n\t}\n\t// -------------------------------------------------------------------- Part of Date end\n\n\t/**\n\t * 是否闰年\n\t *\n\t * @return 是否闰年\n\t * @see DateUtil#isLeapYear(int)\n\t */\n\tpublic boolean isLeapYear() {\n\t\treturn DateUtil.isLeapYear(year());\n\t}\n\n\t/**\n\t * 转换为Calendar, 默认 {@link Locale}\n\t *\n\t * @return {@link Calendar}\n\t */\n\tpublic Calendar toCalendar() {\n\t\treturn toCalendar(Locale.getDefault(Locale.Category.FORMAT));\n\t}\n\n\t/**\n\t * 转换为Calendar\n\t *\n\t * @param locale 地域 {@link Locale}\n\t * @return {@link Calendar}\n\t */\n\tpublic Calendar toCalendar(Locale locale) {\n\t\treturn toCalendar(this.timeZone, locale);\n\t}\n\n\t/**\n\t * 转换为Calendar\n\t *\n\t * @param zone 时区 {@link TimeZone}\n\t * @return {@link Calendar}\n\t */\n\tpublic Calendar toCalendar(TimeZone zone) {\n\t\treturn toCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));\n\t}\n\n\t/**\n\t * 转换为Calendar\n\t *\n\t * @param zone   时区 {@link TimeZone}\n\t * @param locale 地域 {@link Locale}\n\t * @return {@link Calendar}\n\t */\n\tpublic Calendar toCalendar(TimeZone zone, Locale locale) {\n\t\tif (null == locale) {\n\t\t\tlocale = Locale.getDefault(Locale.Category.FORMAT);\n\t\t}\n\t\tfinal Calendar cal = (null != zone) ? Calendar.getInstance(zone, locale) : Calendar.getInstance(locale);\n\t\t//noinspection MagicConstant\n\t\tcal.setFirstDayOfWeek(firstDayOfWeek.getValue());\n\t\t// issue#1988@Github\n\t\tif (minimalDaysInFirstWeek > 0) {\n\t\t\tcal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);\n\t\t}\n\t\tcal.setTime(this);\n\t\treturn cal;\n\t}\n\n\t/**\n\t * 转换为 {@link Date}<br>\n\t * 考虑到很多框架（例如Hibernate）的兼容性，提供此方法返回JDK原生的Date对象\n\t *\n\t * @return {@link Date}\n\t * @since 3.2.2\n\t */\n\tpublic Date toJdkDate() {\n\t\treturn new Date(this.getTime());\n\t}\n\n\t/**\n\t * 转为{@link Timestamp}\n\t *\n\t * @return {@link Timestamp}\n\t */\n\tpublic Timestamp toTimestamp() {\n\t\treturn new Timestamp(this.getTime());\n\t}\n\n\t/**\n\t * 转为 {@link java.sql.Date}\n\t *\n\t * @return {@link java.sql.Date}\n\t */\n\tpublic java.sql.Date toSqlDate() {\n\t\treturn new java.sql.Date(getTime());\n\t}\n\n\t/**\n\t * 转换为 {@link LocalDateTime}\n\t *\n\t * @return {@link LocalDateTime}\n\t * @since 5.7.16\n\t */\n\tpublic LocalDateTime toLocalDateTime() {\n\t\treturn LocalDateTimeUtil.of(this);\n\t}\n\n\t/**\n\t * 计算相差时长\n\t *\n\t * @param date 对比的日期\n\t * @return {@link DateBetween}\n\t */\n\tpublic DateBetween between(Date date) {\n\t\treturn new DateBetween(this, date);\n\t}\n\n\t/**\n\t * 计算相差时长\n\t *\n\t * @param date 对比的日期\n\t * @param unit 单位 {@link DateUnit}\n\t * @return 相差时长\n\t */\n\tpublic long between(Date date, DateUnit unit) {\n\t\treturn new DateBetween(this, date).between(unit);\n\t}\n\n\t/**\n\t * 计算相差时长\n\t *\n\t * @param date        对比的日期\n\t * @param unit        单位 {@link DateUnit}\n\t * @param formatLevel 格式化级别\n\t * @return 相差时长\n\t */\n\tpublic String between(Date date, DateUnit unit, BetweenFormatter.Level formatLevel) {\n\t\treturn new DateBetween(this, date).toString(unit, formatLevel);\n\t}\n\n\t/**\n\t * 当前日期是否在日期指定范围内<br>\n\t * 起始日期和结束日期可以互换\n\t *\n\t * @param beginDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 是否在范围内\n\t * @since 3.0.8\n\t */\n\tpublic boolean isIn(Date beginDate, Date endDate) {\n\t\tlong beginMills = beginDate.getTime();\n\t\tlong endMills = endDate.getTime();\n\t\tlong thisMills = this.getTime();\n\n\t\treturn thisMills >= Math.min(beginMills, endMills) && thisMills <= Math.max(beginMills, endMills);\n\t}\n\n\t/**\n\t * 是否在给定日期之前\n\t *\n\t * @param date 日期\n\t * @return 是否在给定日期之前\n\t * @since 4.1.3\n\t */\n\tpublic boolean isBefore(Date date) {\n\t\tif (null == date) {\n\t\t\tthrow new NullPointerException(\"Date to compare is null !\");\n\t\t}\n\t\treturn compareTo(date) < 0;\n\t}\n\n\t/**\n\t * 是否在给定日期之前或与给定日期相等\n\t *\n\t * @param date 日期\n\t * @return 是否在给定日期之前或与给定日期相等\n\t * @since 3.0.9\n\t */\n\tpublic boolean isBeforeOrEquals(Date date) {\n\t\tif (null == date) {\n\t\t\tthrow new NullPointerException(\"Date to compare is null !\");\n\t\t}\n\t\treturn compareTo(date) <= 0;\n\t}\n\n\t/**\n\t * 是否在给定日期之后\n\t *\n\t * @param date 日期\n\t * @return 是否在给定日期之后\n\t * @since 4.1.3\n\t */\n\tpublic boolean isAfter(Date date) {\n\t\tif (null == date) {\n\t\t\tthrow new NullPointerException(\"Date to compare is null !\");\n\t\t}\n\t\treturn compareTo(date) > 0;\n\t}\n\n\t/**\n\t * 是否在给定日期之后或与给定日期相等\n\t *\n\t * @param date 日期\n\t * @return 是否在给定日期之后或与给定日期相等\n\t * @since 3.0.9\n\t */\n\tpublic boolean isAfterOrEquals(Date date) {\n\t\tif (null == date) {\n\t\t\tthrow new NullPointerException(\"Date to compare is null !\");\n\t\t}\n\t\treturn compareTo(date) >= 0;\n\t}\n\n\t/**\n\t * 对象是否可变<br>\n\t * 如果为不可变对象，以下方法将返回新方法：\n\t * <ul>\n\t * <li>{@link DateTime#offset(DateField, int)}</li>\n\t * <li>{@link DateTime#setField(DateField, int)}</li>\n\t * <li>{@link DateTime#setField(int, int)}</li>\n\t * </ul>\n\t * 如果为不可变对象，{@link DateTime#setTime(long)}将抛出异常\n\t *\n\t * @return 对象是否可变\n\t */\n\tpublic boolean isMutable() {\n\t\treturn mutable;\n\t}\n\n\t/**\n\t * 设置对象是否可变 如果为不可变对象，以下方法将返回新方法：\n\t * <ul>\n\t * <li>{@link DateTime#offset(DateField, int)}</li>\n\t * <li>{@link DateTime#setField(DateField, int)}</li>\n\t * <li>{@link DateTime#setField(int, int)}</li>\n\t * </ul>\n\t * 如果为不可变对象，{@link DateTime#setTime(long)}将抛出异常\n\t *\n\t * @param mutable 是否可变\n\t * @return this\n\t */\n\tpublic DateTime setMutable(boolean mutable) {\n\t\tthis.mutable = mutable;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得一周的第一天，默认为周一\n\t *\n\t * @return 一周的第一天\n\t */\n\tpublic Week getFirstDayOfWeek() {\n\t\treturn firstDayOfWeek;\n\t}\n\n\t/**\n\t * 设置一周的第一天<br>\n\t * JDK的Calendar中默认一周的第一天是周日，Hutool中将此默认值设置为周一<br>\n\t * 设置一周的第一天主要影响{@link #weekOfMonth()}和{@link #weekOfYear()} 两个方法\n\t *\n\t * @param firstDayOfWeek 一周的第一天\n\t * @return this\n\t * @see #weekOfMonth()\n\t * @see #weekOfYear()\n\t */\n\tpublic DateTime setFirstDayOfWeek(Week firstDayOfWeek) {\n\t\tthis.firstDayOfWeek = firstDayOfWeek;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取时区\n\t *\n\t * @return 时区\n\t * @since 5.0.5\n\t */\n\tpublic TimeZone getTimeZone() {\n\t\treturn this.timeZone;\n\t}\n\n\t/**\n\t * 获取时区ID\n\t *\n\t * @return 时区ID\n\t * @since 5.0.5\n\t */\n\tpublic ZoneId getZoneId() {\n\t\treturn this.timeZone.toZoneId();\n\t}\n\n\t/**\n\t * 设置时区\n\t *\n\t * @param timeZone 时区\n\t * @return this\n\t * @since 4.1.2\n\t */\n\tpublic DateTime setTimeZone(TimeZone timeZone) {\n\t\tthis.timeZone = ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置第一周最少天数\n\t *\n\t * @param minimalDaysInFirstWeek 第一周最少天数\n\t * @return this\n\t * @since 5.7.17\n\t */\n\tpublic DateTime setMinimalDaysInFirstWeek(int minimalDaysInFirstWeek) {\n\t\tthis.minimalDaysInFirstWeek = minimalDaysInFirstWeek;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否为本月最后一天\n\t * @return 是否为本月最后一天\n\t * @since 5.8.9\n\t */\n\tpublic boolean isLastDayOfMonth(){\n\t\treturn dayOfMonth() == getLastDayOfMonth();\n\t}\n\n\t/**\n\t * 获得本月的最后一天\n\t * @return 天\n\t * @since 5.8.9\n\t */\n\tpublic int getLastDayOfMonth(){\n\t\treturn monthEnum().getLastDay(isLeapYear());\n\t}\n\n\t// -------------------------------------------------------------------- toString start\n\n\t/**\n\t * 转为字符串，如果时区被设置，会转换为其时区对应的时间，否则转换为当前地点对应的时区<br>\n\t * 可以调用{@link DateTime#setUseJdkToStringStyle(boolean)} 方法自定义默认的风格<br>\n\t * 如果{@link #useJdkToStringStyle}为{@code true}，返回\"EEE MMM dd HH:mm:ss zzz yyyy\"格式，<br>\n\t * 如果为{@code false}，则返回\"yyyy-MM-dd HH:mm:ss\"\n\t *\n\t * @return 格式字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\tif(useJdkToStringStyle){\n\t\t\treturn super.toString();\n\t\t}\n\t\treturn toString(this.timeZone);\n\t}\n\n\t/**\n\t * 转为\"yyyy-MM-dd HH:mm:ss\" 格式字符串<br>\n\t * 时区使用当前地区的默认时区\n\t *\n\t * @return \"yyyy-MM-dd HH:mm:ss\" 格式字符串\n\t * @since 4.1.14\n\t */\n\tpublic String toStringDefaultTimeZone() {\n\t\treturn toString(TimeZone.getDefault());\n\t}\n\n\t/**\n\t * 转为\"yyyy-MM-dd HH:mm:ss\" 格式字符串<br>\n\t * 如果时区不为{@code null}，会转换为其时区对应的时间，否则转换为当前时间对应的时区\n\t *\n\t * @param timeZone 时区\n\t * @return \"yyyy-MM-dd HH:mm:ss\" 格式字符串\n\t * @since 4.1.14\n\t */\n\tpublic String toString(TimeZone timeZone) {\n\t\tif (null != timeZone) {\n\t\t\treturn toString(DateUtil.newSimpleFormat(DatePattern.NORM_DATETIME_PATTERN, null, timeZone));\n\t\t}\n\t\treturn toString(DatePattern.NORM_DATETIME_FORMAT);\n\t}\n\n\t/**\n\t * 转为\"yyyy-MM-dd\" 格式字符串\n\t *\n\t * @return \"yyyy-MM-dd\" 格式字符串\n\t * @since 4.0.0\n\t */\n\tpublic String toDateStr() {\n\t\tif (null != this.timeZone) {\n\t\t\treturn toString(DateUtil.newSimpleFormat(DatePattern.NORM_DATE_PATTERN, null, timeZone));\n\t\t}\n\t\treturn toString(DatePattern.NORM_DATE_FORMAT);\n\t}\n\n\t/**\n\t * 转为\"HH:mm:ss\" 格式字符串\n\t *\n\t * @return \"HH:mm:ss\" 格式字符串\n\t * @since 4.1.4\n\t */\n\tpublic String toTimeStr() {\n\t\tif (null != this.timeZone) {\n\t\t\treturn toString(DateUtil.newSimpleFormat(DatePattern.NORM_TIME_PATTERN, null, timeZone));\n\t\t}\n\t\treturn toString(DatePattern.NORM_TIME_FORMAT);\n\t}\n\n\t/**\n\t * 转为字符串\n\t *\n\t * @param format 日期格式，常用格式见： {@link DatePattern}\n\t * @return String\n\t */\n\tpublic String toString(String format) {\n\t\tif (null != this.timeZone) {\n\t\t\treturn toString(DateUtil.newSimpleFormat(format, null, timeZone));\n\t\t}\n\t\treturn toString(FastDateFormat.getInstance(format));\n\t}\n\n\t/**\n\t * 转为字符串\n\t *\n\t * @param format {@link DatePrinter} 或 {@link FastDateFormat}\n\t * @return String\n\t */\n\tpublic String toString(DatePrinter format) {\n\t\treturn format.format(this);\n\t}\n\n\t/**\n\t * 转为字符串\n\t *\n\t * @param format {@link SimpleDateFormat}\n\t * @return String\n\t */\n\tpublic String toString(DateFormat format) {\n\t\treturn format.format(this);\n\t}\n\n\t/**\n\t * @return 输出精确到毫秒的标准日期形式\n\t */\n\tpublic String toMsStr() {\n\t\treturn toString(DatePattern.NORM_DATETIME_MS_FORMAT);\n\t}\n\t// -------------------------------------------------------------------- toString end\n\n\t/**\n\t * 转换字符串为Date\n\t *\n\t * @param dateStr    日期字符串\n\t * @param dateFormat {@link SimpleDateFormat}\n\t * @return {@link Date}\n\t */\n\tprivate static Date parse(CharSequence dateStr, DateFormat dateFormat) {\n\t\tAssert.notBlank(dateStr, \"Date String must be not blank !\");\n\t\ttry {\n\t\t\treturn dateFormat.parse(dateStr.toString());\n\t\t} catch (Exception e) {\n\t\t\tString pattern;\n\t\t\tif (dateFormat instanceof SimpleDateFormat) {\n\t\t\t\tpattern = ((SimpleDateFormat) dateFormat).toPattern();\n\n\t\t\t\t// issue#3713 尝试使用US Locale解析\n\t\t\t\ttry {\n\t\t\t\t\treturn DateUtil.newSimpleFormat(pattern, Locale.US, null).parse(dateStr.toString());\n\t\t\t\t} catch (Exception ignore) {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tpattern = dateFormat.toString();\n\t\t\t}\n\t\t\tthrow new DateException(StrUtil.format(\"Parse [{}] with format [{}] error!\", dateStr, pattern), e);\n\t\t}\n\t}\n\n\t/**\n\t * 转换字符串为Date\n\t *\n\t * @param dateStr 日期字符串\n\t * @param parser  {@link FastDateFormat}\n\t * @param lenient 是否宽容模式\n\t * @return {@link Calendar}\n\t */\n\tprivate static Calendar parse(CharSequence dateStr, DateParser parser, boolean lenient) {\n\t\tAssert.notNull(parser, \"Parser or DateFromat must be not null !\");\n\t\tAssert.notBlank(dateStr, \"Date String must be not blank !\");\n\n\t\tfinal Calendar calendar = CalendarUtil.parse(dateStr, lenient, parser);\n\t\tif (null == calendar) {\n\t\t\tthrow new DateException(\"Parse [{}] with format [{}] error!\", dateStr, parser.getPattern());\n\t\t}\n\n\t\t//noinspection MagicConstant\n\t\tcalendar.setFirstDayOfWeek(Week.MONDAY.getValue());\n\t\treturn calendar;\n\t}\n\n\t/**\n\t * 设置日期时间\n\t *\n\t * @param time 日期时间毫秒\n\t * @return this\n\t */\n\tprivate DateTime setTimeInternal(long time) {\n\t\tsuper.setTime(time);\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateUnit.java",
    "content": "package cn.hutool.core.date;\n\nimport java.time.temporal.ChronoUnit;\n\n/**\n * 日期时间单位，每个单位都是以毫秒为基数\n *\n * @author Looly\n */\npublic enum DateUnit {\n\t/**\n\t * 一毫秒\n\t */\n\tMS(1),\n\t/**\n\t * 一秒的毫秒数\n\t */\n\tSECOND(1000),\n\t/**\n\t * 一分钟的毫秒数\n\t */\n\tMINUTE(SECOND.getMillis() * 60),\n\t/**\n\t * 一小时的毫秒数\n\t */\n\tHOUR(MINUTE.getMillis() * 60),\n\t/**\n\t * 一天的毫秒数\n\t */\n\tDAY(HOUR.getMillis() * 24),\n\t/**\n\t * 一周的毫秒数\n\t */\n\tWEEK(DAY.getMillis() * 7);\n\n\tprivate final long millis;\n\n\tDateUnit(long millis) {\n\t\tthis.millis = millis;\n\t}\n\n\t/**\n\t * @return 单位对应的毫秒数\n\t */\n\tpublic long getMillis() {\n\t\treturn this.millis;\n\t}\n\n\t/**\n\t * 单位兼容转换，将DateUnit转换为对应的{@link ChronoUnit}\n\t *\n\t * @return {@link ChronoUnit}\n\t * @since 5.4.5\n\t */\n\tpublic ChronoUnit toChronoUnit() {\n\t\treturn DateUnit.toChronoUnit(this);\n\t}\n\n\t/**\n\t * 单位兼容转换，将{@link ChronoUnit}转换为对应的DateUnit\n\t *\n\t * @param unit {@link ChronoUnit}\n\t * @return DateUnit，null表示不支持此单位\n\t * @since 5.4.5\n\t */\n\tpublic static DateUnit of(ChronoUnit unit) {\n\t\tswitch (unit) {\n\t\t\tcase MILLIS:\n\t\t\t\treturn DateUnit.MS;\n\t\t\tcase SECONDS:\n\t\t\t\treturn DateUnit.SECOND;\n\t\t\tcase MINUTES:\n\t\t\t\treturn DateUnit.MINUTE;\n\t\t\tcase HOURS:\n\t\t\t\treturn DateUnit.HOUR;\n\t\t\tcase DAYS:\n\t\t\t\treturn DateUnit.DAY;\n\t\t\tcase WEEKS:\n\t\t\t\treturn DateUnit.WEEK;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 单位兼容转换，将DateUnit转换为对应的{@link ChronoUnit}\n\t *\n\t * @param unit DateUnit\n\t * @return {@link ChronoUnit}\n\t * @since 5.4.5\n\t */\n\tpublic static ChronoUnit toChronoUnit(DateUnit unit) {\n\t\tswitch (unit) {\n\t\t\tcase MS:\n\t\t\t\treturn ChronoUnit.MILLIS;\n\t\t\tcase SECOND:\n\t\t\t\treturn ChronoUnit.SECONDS;\n\t\t\tcase MINUTE:\n\t\t\t\treturn ChronoUnit.MINUTES;\n\t\t\tcase HOUR:\n\t\t\t\treturn ChronoUnit.HOURS;\n\t\t\tcase DAY:\n\t\t\t\treturn ChronoUnit.DAYS;\n\t\t\tcase WEEK:\n\t\t\t\treturn ChronoUnit.WEEKS;\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.date.format.DateParser;\nimport cn.hutool.core.date.format.DatePrinter;\nimport cn.hutool.core.date.format.FastDateFormat;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.Year;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * 日期时间工具类\n *\n * @author xiaoleilu\n * @see LocalDateTimeUtil java8日期工具类\n * @see DatePattern 日期常用格式工具类\n */\npublic class DateUtil extends CalendarUtil {\n\n\t/**\n\t * java.util.Date EEE MMM zzz 缩写数组\n\t */\n\tprivate final static String[] wtb = { //\n\t\t\t\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", // 星期\n\t\t\t\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\", // 月份\n\t\t\t\"gmt\", \"ut\", \"utc\", \"est\", \"edt\", \"cst\", \"cdt\", \"mst\", \"mdt\", \"pst\", \"pdt\"// 时间标准\n\t};\n\t\n\t/**\n\t * 匹配日期分隔符（正斜杠、点、年、月），用于统一替换为横杠\n\t */\n\tprivate static final Pattern DATE_SEPARATOR_PATTERN = Pattern.compile(\"[/.年月]\");\n\n\t/**\n\t * 匹配时间单位（时、分、秒），用于统一替换为冒号 匹配时间单位（时、分、秒），用于统一替换为冒号\n\t */\n\tprivate static final Pattern TIME_UNIT_PATTERN = Pattern.compile(\"[时分秒]\");\n\n\t/**\n\t * 当前时间，转换为{@link DateTime}对象\n\t *\n\t * @return 当前时间\n\t */\n\tpublic static DateTime date() {\n\t\treturn new DateTime();\n\t}\n\n\t/**\n\t * 当前时间，转换为{@link DateTime}对象，忽略毫秒部分\n\t *\n\t * @return 当前时间\n\t * @since 4.6.2\n\t */\n\tpublic static DateTime dateSecond() {\n\t\treturn beginOfSecond(date());\n\t}\n\n\t/**\n\t * {@link Date}类型时间转为{@link DateTime}<br>\n\t * 如果date本身为DateTime对象，则返回强转后的对象，否则新建一个DateTime对象\n\t *\n\t * @param date Long类型Date（Unix时间戳），如果传入{@code null}，返回{@code null}\n\t * @return 时间对象\n\t * @since 3.0.7\n\t */\n\tpublic static DateTime date(Date date) {\n\t\tif (date == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (date instanceof DateTime) {\n\t\t\treturn (DateTime) date;\n\t\t}\n\t\treturn dateNew(date);\n\t}\n\n\t/**\n\t * 根据已有{@link Date} 产生新的{@link DateTime}对象\n\t *\n\t * @param date Date对象，如果传入{@code null}，返回{@code null}\n\t * @return {@link DateTime}对象\n\t * @since 4.3.1\n\t */\n\tpublic static DateTime dateNew(Date date) {\n\t\tif (date == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new DateTime(date);\n\t}\n\n\t/**\n\t * Long类型时间转为{@link DateTime}<br>\n\t * 只支持毫秒级别时间戳，如果需要秒级别时间戳，请自行×1000\n\t *\n\t * @param date Long类型Date（Unix时间戳）\n\t * @return 时间对象\n\t */\n\tpublic static DateTime date(long date) {\n\t\treturn new DateTime(date);\n\t}\n\n\t/**\n\t * {@link Calendar}类型时间转为{@link DateTime}<br>\n\t * 始终根据已有{@link Calendar} 产生新的{@link DateTime}对象\n\t *\n\t * @param calendar {@link Calendar}，如果传入{@code null}，返回{@code null}\n\t * @return 时间对象\n\t */\n\tpublic static DateTime date(Calendar calendar) {\n\t\tif (calendar == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new DateTime(calendar);\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}类型时间转为{@link DateTime}<br>\n\t * 始终根据已有{@link TemporalAccessor} 产生新的{@link DateTime}对象\n\t *\n\t * @param temporalAccessor {@link TemporalAccessor},常用子类： {@link LocalDateTime}、 LocalDate，如果传入{@code null}，返回{@code null}\n\t * @return 时间对象\n\t * @since 5.0.0\n\t */\n\tpublic static DateTime date(TemporalAccessor temporalAccessor) {\n\t\tif (temporalAccessor == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new DateTime(temporalAccessor);\n\t}\n\n\t/**\n\t * 当前时间的时间戳\n\t *\n\t * @return 时间\n\t */\n\tpublic static long current() {\n\t\treturn System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 当前时间的时间戳（秒）\n\t *\n\t * @return 当前时间秒数\n\t * @since 4.0.0\n\t */\n\tpublic static long currentSeconds() {\n\t\treturn System.currentTimeMillis() / 1000;\n\t}\n\n\t/**\n\t * 当前时间，格式 yyyy-MM-dd HH:mm:ss\n\t *\n\t * @return 当前时间的标准形式字符串\n\t */\n\tpublic static String now() {\n\t\treturn formatDateTime(new DateTime());\n\t}\n\n\t/**\n\t * 当前日期，格式 yyyy-MM-dd\n\t *\n\t * @return 当前日期的标准形式字符串\n\t */\n\tpublic static String today() {\n\t\treturn formatDate(new DateTime());\n\t}\n\n\t// -------------------------------------------------------------- Part of Date start\n\n\t/**\n\t * 获得年的部分\n\t *\n\t * @param date 日期\n\t * @return 年的部分\n\t */\n\tpublic static int year(Date date) {\n\t\treturn DateTime.of(date).year();\n\t}\n\n\t/**\n\t * 获得指定日期所属季度，从1开始计数\n\t *\n\t * @param date 日期\n\t * @return 第几个季度\n\t * @since 4.1.0\n\t */\n\tpublic static int quarter(Date date) {\n\t\treturn DateTime.of(date).quarter();\n\t}\n\n\t/**\n\t * 获得指定日期所属季度\n\t *\n\t * @param date 日期\n\t * @return 第几个季度枚举\n\t * @since 4.1.0\n\t */\n\tpublic static Quarter quarterEnum(Date date) {\n\t\treturn DateTime.of(date).quarterEnum();\n\t}\n\n\t/**\n\t * 获得月份，从0开始计数\n\t *\n\t * @param date 日期\n\t * @return 月份，从0开始计数\n\t */\n\tpublic static int month(Date date) {\n\t\treturn DateTime.of(date).month();\n\t}\n\n\t/**\n\t * 获得月份\n\t *\n\t * @param date 日期\n\t * @return {@link Month}\n\t */\n\tpublic static Month monthEnum(Date date) {\n\t\treturn DateTime.of(date).monthEnum();\n\t}\n\n\t/**\n\t * 获得指定日期是所在年份的第几周<br>\n\t * 此方法返回值与一周的第一天有关，比如：<br>\n\t * 2016年1月3日为周日，如果一周的第一天为周日，那这天是第二周（返回2）<br>\n\t * 如果一周的第一天为周一，那这天是第一周（返回1）<br>\n\t * 跨年的那个星期得到的结果总是1\n\t *\n\t * @param date 日期\n\t * @return 周\n\t * @see DateTime#setFirstDayOfWeek(Week)\n\t */\n\tpublic static int weekOfYear(Date date) {\n\t\treturn DateTime.of(date).weekOfYear();\n\t}\n\n\t/**\n\t * 获得指定日期是所在月份的第几周<br>\n\t *\n\t * @param date 日期\n\t * @return 周\n\t */\n\tpublic static int weekOfMonth(Date date) {\n\t\treturn DateTime.of(date).weekOfMonth();\n\t}\n\n\t/**\n\t * 获得指定日期是这个日期所在月份的第几天<br>\n\t *\n\t * @param date 日期\n\t * @return 天\n\t */\n\tpublic static int dayOfMonth(Date date) {\n\t\treturn DateTime.of(date).dayOfMonth();\n\t}\n\n\t/**\n\t * 获得指定日期是这个日期所在年的第几天\n\t *\n\t * @param date 日期\n\t * @return 天\n\t * @since 5.3.6\n\t */\n\tpublic static int dayOfYear(Date date) {\n\t\treturn DateTime.of(date).dayOfYear();\n\t}\n\n\t/**\n\t * 获得指定日期是星期几，1表示周日，2表示周一\n\t *\n\t * @param date 日期\n\t * @return 天\n\t */\n\tpublic static int dayOfWeek(Date date) {\n\t\treturn DateTime.of(date).dayOfWeek();\n\t}\n\n\t/**\n\t * 获得指定日期是星期几\n\t *\n\t * @param date 日期\n\t * @return {@link Week}\n\t */\n\tpublic static Week dayOfWeekEnum(Date date) {\n\t\treturn DateTime.of(date).dayOfWeekEnum();\n\t}\n\n\t/**\n\t * 是否为周末（周六或周日）\n\t *\n\t * @param date 判定的日期{@link Date}\n\t * @return 是否为周末（周六或周日）\n\t * @since 5.7.6\n\t */\n\tpublic static boolean isWeekend(Date date) {\n\t\tfinal Week week = dayOfWeekEnum(date);\n\t\treturn Week.SATURDAY == week || Week.SUNDAY == week;\n\t}\n\n\t/**\n\t * 获得指定日期的小时数部分<br>\n\t *\n\t * @param date          日期\n\t * @param is24HourClock 是否24小时制\n\t * @return 小时数\n\t */\n\tpublic static int hour(Date date, boolean is24HourClock) {\n\t\treturn DateTime.of(date).hour(is24HourClock);\n\t}\n\n\t/**\n\t * 获得指定日期的分钟数部分<br>\n\t * 例如：10:04:15.250 =》 4\n\t *\n\t * @param date 日期\n\t * @return 分钟数\n\t */\n\tpublic static int minute(Date date) {\n\t\treturn DateTime.of(date).minute();\n\t}\n\n\t/**\n\t * 获得指定日期的秒数部分<br>\n\t *\n\t * @param date 日期\n\t * @return 秒数\n\t */\n\tpublic static int second(Date date) {\n\t\treturn DateTime.of(date).second();\n\t}\n\n\t/**\n\t * 获得指定日期的毫秒数部分<br>\n\t *\n\t * @param date 日期\n\t * @return 毫秒数\n\t */\n\tpublic static int millisecond(Date date) {\n\t\treturn DateTime.of(date).millisecond();\n\t}\n\n\t/**\n\t * 是否为上午\n\t *\n\t * @param date 日期\n\t * @return 是否为上午\n\t */\n\tpublic static boolean isAM(Date date) {\n\t\treturn DateTime.of(date).isAM();\n\t}\n\n\t/**\n\t * 是否为下午\n\t *\n\t * @param date 日期\n\t * @return 是否为下午\n\t */\n\tpublic static boolean isPM(Date date) {\n\t\treturn DateTime.of(date).isPM();\n\t}\n\n\t/**\n\t * @return 今年\n\t */\n\tpublic static int thisYear() {\n\t\treturn year(date());\n\t}\n\n\t/**\n\t * @return 当前月份，从0开始计数\n\t */\n\tpublic static int thisMonth() {\n\t\treturn month(date());\n\t}\n\n\t/**\n\t * @return 当前月份 {@link Month}\n\t */\n\tpublic static Month thisMonthEnum() {\n\t\treturn monthEnum(date());\n\t}\n\n\t/**\n\t * @return 当前日期所在年份的第几周\n\t */\n\tpublic static int thisWeekOfYear() {\n\t\treturn weekOfYear(date());\n\t}\n\n\t/**\n\t * @return 当前日期所在月份的第几周\n\t */\n\tpublic static int thisWeekOfMonth() {\n\t\treturn weekOfMonth(date());\n\t}\n\n\t/**\n\t * @return 当前日期是这个日期所在月份的第几天\n\t */\n\tpublic static int thisDayOfMonth() {\n\t\treturn dayOfMonth(date());\n\t}\n\n\t/**\n\t * @return 当前日期是星期几\n\t */\n\tpublic static int thisDayOfWeek() {\n\t\treturn dayOfWeek(date());\n\t}\n\n\t/**\n\t * @return 当前日期是星期几 {@link Week}\n\t */\n\tpublic static Week thisDayOfWeekEnum() {\n\t\treturn dayOfWeekEnum(date());\n\t}\n\n\t/**\n\t * @param is24HourClock 是否24小时制\n\t * @return 当前日期的小时数部分<br>\n\t */\n\tpublic static int thisHour(boolean is24HourClock) {\n\t\treturn hour(date(), is24HourClock);\n\t}\n\n\t/**\n\t * @return 当前日期的分钟数部分<br>\n\t */\n\tpublic static int thisMinute() {\n\t\treturn minute(date());\n\t}\n\n\t/**\n\t * @return 当前日期的秒数部分<br>\n\t */\n\tpublic static int thisSecond() {\n\t\treturn second(date());\n\t}\n\n\t/**\n\t * @return 当前日期的毫秒数部分<br>\n\t */\n\tpublic static int thisMillisecond() {\n\t\treturn millisecond(date());\n\t}\n\t// -------------------------------------------------------------- Part of Date end\n\n\t/**\n\t * 获得指定日期年份和季节<br>\n\t * 格式：[20131]表示2013年第一季度\n\t *\n\t * @param date 日期\n\t * @return Quarter ，类似于 20132\n\t */\n\tpublic static String yearAndQuarter(Date date) {\n\t\treturn yearAndQuarter(calendar(date));\n\t}\n\n\t/**\n\t * 获得指定日期区间内的年份和季节<br>\n\t *\n\t * @param startDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 季度列表 ，元素类似于 20132\n\t */\n\tpublic static LinkedHashSet<String> yearAndQuarter(Date startDate, Date endDate) {\n\t\tif (startDate == null || endDate == null) {\n\t\t\treturn new LinkedHashSet<>(0);\n\t\t}\n\t\treturn yearAndQuarter(startDate.getTime(), endDate.getTime());\n\t}\n\t// ------------------------------------ Format start ----------------------------------------------\n\n\t/**\n\t * 格式化日期时间<br>\n\t * 格式 yyyy-MM-dd HH:mm:ss\n\t *\n\t * @param localDateTime 被格式化的日期\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String formatLocalDateTime(LocalDateTime localDateTime) {\n\t\treturn LocalDateTimeUtil.formatNormal(localDateTime);\n\t}\n\n\t/**\n\t * 根据特定格式格式化日期\n\t *\n\t * @param localDateTime 被格式化的日期\n\t * @param format        日期格式，常用格式见： {@link DatePattern}\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(LocalDateTime localDateTime, String format) {\n\t\treturn LocalDateTimeUtil.format(localDateTime, format);\n\t}\n\n\t/**\n\t * 根据特定格式格式化日期\n\t *\n\t * @param date   被格式化的日期\n\t * @param format 日期格式，常用格式见： {@link DatePattern} {@link DatePattern#NORM_DATETIME_PATTERN}\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(Date date, String format) {\n\t\tif (null == date || StrUtil.isBlank(format)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 检查自定义格式\n\t\tif (GlobalCustomFormat.isCustomFormat(format)) {\n\t\t\treturn GlobalCustomFormat.format(date, format);\n\t\t}\n\n\t\tTimeZone timeZone = null;\n\t\tif (date instanceof DateTime) {\n\t\t\ttimeZone = ((DateTime) date).getTimeZone();\n\t\t}\n\t\treturn format(date, FastDateFormat.getInstance(format, timeZone));\n\t}\n\n\t/**\n\t * 根据特定格式格式化日期\n\t *\n\t * @param date   被格式化的日期\n\t * @param format {@link DatePrinter} 或 {@link FastDateFormat} {@link DatePattern#NORM_DATETIME_FORMAT}\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(Date date, DatePrinter format) {\n\t\tif (null == format || null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn format.format(date);\n\t}\n\n\t/**\n\t * 根据特定格式格式化日期\n\t *\n\t * @param date   被格式化的日期\n\t * @param format {@link SimpleDateFormat}\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(Date date, DateFormat format) {\n\t\tif (null == format || null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn format.format(date);\n\t}\n\n\t/**\n\t * 根据特定格式格式化日期\n\t *\n\t * @param date   被格式化的日期\n\t * @param format {@link SimpleDateFormat} {@link DatePattern#NORM_DATETIME_FORMATTER}\n\t * @return 格式化后的字符串\n\t * @since 5.0.0\n\t */\n\tpublic static String format(Date date, DateTimeFormatter format) {\n\t\tif (null == format || null == date) {\n\t\t\treturn null;\n\t\t}\n\t\t// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra\n\t\t// 出现以上报错时，表示Instant时间戳没有时区信息，赋予默认时区\n\t\treturn TemporalAccessorUtil.format(date.toInstant(), format);\n\t}\n\n\t/**\n\t * 格式化日期时间<br>\n\t * 格式 yyyy-MM-dd HH:mm:ss\n\t *\n\t * @param date 被格式化的日期\n\t * @return 格式化后的日期\n\t */\n\tpublic static String formatDateTime(Date date) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn DatePattern.NORM_DATETIME_FORMAT.format(date);\n\t}\n\n\t/**\n\t * 格式化日期部分（不包括时间）<br>\n\t * 格式 yyyy-MM-dd\n\t *\n\t * @param date 被格式化的日期\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String formatDate(Date date) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn DatePattern.NORM_DATE_FORMAT.format(date);\n\t}\n\n\t/**\n\t * 格式化时间<br>\n\t * 格式 HH:mm:ss\n\t *\n\t * @param date 被格式化的日期\n\t * @return 格式化后的字符串\n\t * @since 3.0.1\n\t */\n\tpublic static String formatTime(Date date) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn DatePattern.NORM_TIME_FORMAT.format(date);\n\t}\n\n\t/**\n\t * 格式化为Http的标准日期格式<br>\n\t * 标准日期格式遵循RFC 1123规范，格式类似于：Fri, 31 Dec 1999 23:59:59 GMT\n\t *\n\t * @param date 被格式化的日期\n\t * @return HTTP标准形式日期字符串\n\t */\n\tpublic static String formatHttpDate(Date date) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn DatePattern.HTTP_DATETIME_FORMAT.format(date);\n\t}\n\n\t/**\n\t * 格式化为中文日期格式，如果isUppercase为false，则返回类似：2018年10月24日，否则返回二〇一八年十月二十四日\n\t *\n\t * @param date        被格式化的日期\n\t * @param isUppercase 是否采用大写形式\n\t * @param withTime    是否包含时间部分\n\t * @return 中文日期字符串\n\t * @since 5.3.9\n\t */\n\tpublic static String formatChineseDate(Date date, boolean isUppercase, boolean withTime) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (false == isUppercase) {\n\t\t\treturn (withTime ? DatePattern.CHINESE_DATE_TIME_FORMAT : DatePattern.CHINESE_DATE_FORMAT).format(date);\n\t\t}\n\n\t\treturn CalendarUtil.formatChineseDate(CalendarUtil.calendar(date), withTime);\n\t}\n\t// ------------------------------------ Format end ----------------------------------------------\n\n\t// ------------------------------------ Parse start ----------------------------------------------\n\n\t/**\n\t * 构建LocalDateTime对象<br>\n\t * 格式：yyyy-MM-dd HH:mm:ss\n\t *\n\t * @param dateStr 时间字符串（带格式）\n\t * @return LocalDateTime对象\n\t */\n\tpublic static LocalDateTime parseLocalDateTime(CharSequence dateStr) {\n\t\treturn parseLocalDateTime(dateStr, DatePattern.NORM_DATETIME_PATTERN);\n\t}\n\n\t/**\n\t * 构建LocalDateTime对象\n\t *\n\t * @param dateStr 时间字符串（带格式）\n\t * @param format  使用{@link DatePattern}定义的格式\n\t * @return LocalDateTime对象\n\t */\n\tpublic static LocalDateTime parseLocalDateTime(CharSequence dateStr, String format) {\n\t\treturn LocalDateTimeUtil.parse(dateStr, format);\n\t}\n\n\t/**\n\t * 构建DateTime对象\n\t *\n\t * @param dateStr    Date字符串\n\t * @param dateFormat 格式化器 {@link SimpleDateFormat}\n\t * @return DateTime对象\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, DateFormat dateFormat) {\n\t\treturn new DateTime(dateStr, dateFormat);\n\t}\n\n\t/**\n\t * 构建DateTime对象\n\t *\n\t * @param dateStr Date字符串\n\t * @param parser  格式化器,{@link FastDateFormat}\n\t * @return DateTime对象\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, DateParser parser) {\n\t\treturn new DateTime(dateStr, parser);\n\t}\n\n\t/**\n\t * 构建DateTime对象\n\t *\n\t * @param dateStr Date字符串\n\t * @param parser  格式化器,{@link FastDateFormat}\n\t * @param lenient 是否宽容模式\n\t * @return DateTime对象\n\t * @since 5.7.14\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, DateParser parser, boolean lenient) {\n\t\treturn new DateTime(dateStr, parser, lenient);\n\t}\n\n\t/**\n\t * 构建DateTime对象\n\t *\n\t * @param dateStr   Date字符串\n\t * @param formatter 格式化器,{@link DateTimeFormatter}\n\t * @return DateTime对象\n\t * @since 5.0.0\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, DateTimeFormatter formatter) {\n\t\treturn new DateTime(dateStr, formatter);\n\t}\n\n\t/**\n\t * 将特定格式的日期转换为Date对象\n\t *\n\t * @param dateStr 特定格式的日期\n\t * @param format  格式，例如yyyy-MM-dd\n\t * @return 日期对象\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, String format) {\n\t\treturn new DateTime(dateStr, format);\n\t}\n\n\t/**\n\t * 将特定格式的日期转换为Date对象\n\t *\n\t * @param dateStr 特定格式的日期\n\t * @param format  格式，例如yyyy-MM-dd\n\t * @param locale  区域信息\n\t * @return 日期对象\n\t * @since 4.5.18\n\t */\n\tpublic static DateTime parse(CharSequence dateStr, String format, Locale locale) {\n\t\tif (GlobalCustomFormat.isCustomFormat(format)) {\n\t\t\t// 自定义格式化器忽略Locale\n\t\t\treturn new DateTime(GlobalCustomFormat.parse(dateStr, format));\n\t\t}\n\t\treturn new DateTime(dateStr, DateUtil.newSimpleFormat(format, locale, null));\n\t}\n\n\t/**\n\t * 通过给定的日期格式解析日期时间字符串。<br>\n\t * 传入的日期格式会逐个尝试，直到解析成功，返回{@link DateTime}对象，否则抛出{@link DateException}异常。\n\t *\n\t * @param str           日期时间字符串，非空\n\t * @param parsePatterns 需要尝试的日期时间格式数组，非空, 见SimpleDateFormat\n\t * @return 解析后的Date\n\t * @throws IllegalArgumentException if the date string or pattern array is null\n\t * @throws DateException            if none of the date patterns were suitable\n\t * @since 5.3.11\n\t */\n\tpublic static DateTime parse(String str, String... parsePatterns) throws DateException {\n\t\treturn new DateTime(CalendarUtil.parseByPatterns(str, parsePatterns));\n\t}\n\n\t/**\n\t * 解析日期时间字符串，格式支持：\n\t *\n\t * <pre>\n\t * yyyy-MM-dd HH:mm:ss\n\t * yyyy/MM/dd HH:mm:ss\n\t * yyyy.MM.dd HH:mm:ss\n\t * yyyy年MM月dd日 HH:mm:ss\n\t * </pre>\n\t *\n\t * @param dateString 标准形式的时间字符串\n\t * @return 日期对象\n\t */\n\tpublic static DateTime parseDateTime(CharSequence dateString) {\n\t\tdateString = normalize(dateString);\n\t\treturn parse(dateString, DatePattern.NORM_DATETIME_FORMAT);\n\t}\n\n\t/**\n\t * 解析日期字符串，忽略时分秒，支持的格式包括：\n\t * <pre>\n\t * yyyy-MM-dd\n\t * yyyy/MM/dd\n\t * yyyy.MM.dd\n\t * yyyy年MM月dd日\n\t * </pre>\n\t *\n\t * @param dateString 标准形式的日期字符串\n\t * @return 日期对象\n\t */\n\tpublic static DateTime parseDate(CharSequence dateString) {\n\t\tdateString = normalize(dateString);\n\t\treturn parse(dateString, DatePattern.NORM_DATE_FORMAT);\n\t}\n\n\t/**\n\t * 解析时间，格式HH:mm:ss，日期部分默认为1970-01-01\n\t *\n\t * @param timeString 标准形式的日期字符串\n\t * @return 日期对象\n\t */\n\tpublic static DateTime parseTime(CharSequence timeString) {\n\t\ttimeString = normalize(timeString);\n\t\treturn parse(timeString, DatePattern.NORM_TIME_FORMAT);\n\t}\n\n\t/**\n\t * 解析时间，格式HH:mm 或 HH:mm:ss，日期默认为今天\n\t *\n\t * @param timeString 标准形式的日期字符串\n\t * @return 日期对象\n\t * @since 3.1.1\n\t */\n\tpublic static DateTime parseTimeToday(CharSequence timeString) {\n\t\t// issue#I9C2D4 处理时分秒\n\t\ttimeString = StrUtil.replaceChars(timeString, \"时分秒\", \":\");\n\n\t\ttimeString = StrUtil.format(\"{} {}\", today(), timeString);\n\t\tif (1 == StrUtil.count(timeString, ':')) {\n\t\t\t// 时间格式为 HH:mm\n\t\t\treturn parse(timeString, DatePattern.NORM_DATETIME_MINUTE_PATTERN);\n\t\t} else {\n\t\t\t// 时间格式为 HH:mm:ss\n\t\t\treturn parse(timeString, DatePattern.NORM_DATETIME_FORMAT);\n\t\t}\n\t}\n\n\t/**\n\t * 解析UTC时间，格式：<br>\n\t * <ol>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ssZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss+0800</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss+08:00</li>\n\t * </ol>\n\t *\n\t * @param utcString UTC时间\n\t * @return 日期对象\n\t * @since 4.1.14\n\t * @deprecated 方法歧义，带T的日期并不一定是UTC时间，请使用 {@link #parseISO8601(String)}\n\t */\n\t@Deprecated\n\tpublic static DateTime parseUTC(String utcString) {\n\t\treturn parseISO8601(utcString);\n\t}\n\n\t/**\n\t * 解析ISO8601时间，格式：<br>\n\t * <ol>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ssZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss+0800</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss+08:00</li>\n\t * </ol>\n\t *\n\t * @param iso8601String ISO8601时间\n\t * @return 日期对象\n\t * @since 5.8.34\n\t */\n\tpublic static DateTime parseISO8601(String iso8601String) {\n\t\tif (iso8601String == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = iso8601String.length();\n\t\tif (StrUtil.contains(iso8601String, 'Z')) {\n\t\t\tif (length == DatePattern.UTC_PATTERN.length() - 4) {\n\t\t\t\t// 格式类似：2018-09-13T05:34:31Z，-4表示减去4个单引号的长度\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_FORMAT);\n\t\t\t}\n\n\t\t\tfinal int patternLength = DatePattern.UTC_MS_PATTERN.length();\n\t\t\t// 格式类似：2018-09-13T05:34:31.999Z，-4表示减去4个单引号的长度\n\t\t\t// -4 ~ -6范围表示匹配毫秒1~3位的情况\n\t\t\tif (length <= patternLength && length >= patternLength - 6) {\n\t\t\t\t// issue#I7H34N，支持最多6位毫秒\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_MS_FORMAT);\n\t\t\t}\n\t\t} else if (StrUtil.contains(iso8601String, '+')) {\n\t\t\t// 去除类似2019-06-01T19:45:43 +08:00加号前的空格\n\t\t\tiso8601String = iso8601String.replace(\" +\", \"+\");\n\t\t\tfinal String zoneOffset = StrUtil.subAfter(iso8601String, '+', true);\n\t\t\tif (StrUtil.isBlank(zoneOffset)) {\n\t\t\t\tthrow new DateException(\"Invalid format: [{}]\", iso8601String);\n\t\t\t}\n\t\t\tif (false == StrUtil.contains(zoneOffset, ':')) {\n\t\t\t\t// +0800转换为+08:00\n\t\t\t\tfinal String pre = StrUtil.subBefore(iso8601String, '+', true);\n\t\t\t\tiso8601String = pre + \"+\" + zoneOffset.substring(0, 2) + \":\" + \"00\";\n\t\t\t}\n\n\t\t\tif (StrUtil.contains(iso8601String, CharUtil.DOT)) {\n\t\t\t\t// 带毫秒，格式类似：2018-09-13T05:34:31.999+08:00\n\t\t\t\tiso8601String = normalizeMillSeconds(iso8601String, \".\", \"+\");\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);\n\t\t\t} else if (iso8601String.length() == 22 && StrUtil.count(iso8601String, ':') == 2) {\n\t\t\t\t// 精确到分钟，格式类似：2025-07-28T20:00+08:00\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_FORMAT);\n\t\t\t} else {\n\t\t\t\t// 格式类似：2018-09-13T05:34:31+08:00\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);\n\t\t\t}\n\t\t} else if(ReUtil.contains(\"-\\\\d{2}:?00\", iso8601String)){\n\t\t\t// Issue#2612，类似 2022-09-14T23:59:00-08:00 或者 2022-09-14T23:59:00-0800\n\n\t\t\t// 去除类似2019-06-01T19:45:43 -08:00加号前的空格\n\t\t\tiso8601String = iso8601String.replace(\" -\", \"-\");\n\t\t\tif(':' != iso8601String.charAt(iso8601String.length() - 3)){\n\t\t\t\tiso8601String = iso8601String.substring(0, iso8601String.length() - 2) + \":00\";\n\t\t\t}\n\n\t\t\tif (StrUtil.contains(iso8601String, CharUtil.DOT)) {\n\t\t\t\t// 带毫秒，格式类似：2018-09-13T05:34:31.999-08:00\n\t\t\t\tiso8601String = normalizeMillSeconds(iso8601String, \".\", \"-\");\n\t\t\t\treturn new DateTime(iso8601String, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);\n\t\t\t} else {\n\t\t\t\t// 格式类似：2018-09-13T05:34:31-08:00\n\t\t\t\treturn new DateTime(iso8601String, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);\n\t\t\t}\n\t\t} else {\n\t\t\tif (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 2) {\n\t\t\t\t// 格式类似：2018-09-13T05:34:31\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_SIMPLE_FORMAT);\n\t\t\t} else if (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 5) {\n\t\t\t\t// 格式类似：2018-09-13T05:34\n\t\t\t\treturn parse(iso8601String + \":00\", DatePattern.UTC_SIMPLE_FORMAT);\n\t\t\t} else if (StrUtil.contains(iso8601String, CharUtil.DOT)) {\n\t\t\t\t// 可能为：  2021-03-17T06:31:33.99\n\t\t\t\tiso8601String = normalizeMillSeconds(iso8601String, \".\", null);\n\t\t\t\treturn parse(iso8601String, DatePattern.UTC_SIMPLE_MS_FORMAT);\n\t\t\t}\n\t\t}\n\t\t// 没有更多匹配的时间格式\n\t\tthrow new DateException(\"No format fit for date String [{}] !\", iso8601String);\n\t}\n\n\t/**\n\t * 解析CST时间，格式：<br>\n\t * <ol>\n\t * <li>EEE MMM dd HH:mm:ss z yyyy（例如：Wed Aug 01 00:00:00 CST 2012）</li>\n\t * </ol>\n\t *\n\t * @param cstString UTC时间\n\t * @return 日期对象\n\t * @since 4.6.9\n\t * @deprecated 理解错误，请使用{@link #parseRFC2822(CharSequence)}\n\t */\n\t@Deprecated\n\tpublic static DateTime parseCST(CharSequence cstString) {\n\t\treturn parseRFC2822(cstString);\n\t}\n\n\t/**\n\t * 解析RFC2822时间，格式：<br>\n\t * <ol>\n\t * <li>EEE MMM dd HH:mm:ss z yyyy（例如：Wed Aug 01 00:00:00 CST 2012）</li>\n\t * </ol>\n\t *\n\t * @param source RFC2822时间\n\t * @return 日期对象\n\t * @since 4.6.9\n\t */\n\tpublic static DateTime parseRFC2822(CharSequence source) {\n\t\tif (source == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// issue#I9C2D4\n\t\tif(StrUtil.contains(source, ',')){\n\t\t\tif(StrUtil.contains(source, \"星期\")){\n\t\t\t\treturn parse(source, FastDateFormat.getInstance(DatePattern.HTTP_DATETIME_PATTERN, Locale.CHINA));\n\t\t\t}\n\t\t\treturn parse(source, DatePattern.HTTP_DATETIME_FORMAT_Z);\n\t\t}\n\n\t\tif(StrUtil.contains(source, \"星期\")){\n\t\t\treturn parse(source, FastDateFormat.getInstance(DatePattern.JDK_DATETIME_PATTERN, Locale.CHINA));\n\t\t}\n\t\treturn parse(source, DatePattern.JDK_DATETIME_FORMAT);\n\t}\n\n\t/**\n\t * 将日期字符串转换为{@link DateTime}对象，格式：<br>\n\t * <ol>\n\t * <li>yyyy-MM-dd HH:mm:ss</li>\n\t * <li>yyyy/MM/dd HH:mm:ss</li>\n\t * <li>yyyy.MM.dd HH:mm:ss</li>\n\t * <li>yyyy年MM月dd日 HH时mm分ss秒</li>\n\t * <li>yyyy-MM-dd</li>\n\t * <li>yyyy/MM/dd</li>\n\t * <li>yyyy.MM.dd</li>\n\t * <li>HH:mm:ss</li>\n\t * <li>HH时mm分ss秒</li>\n\t * <li>yyyy-MM-dd HH:mm</li>\n\t * <li>yyyy-MM-dd HH:mm:ss.SSS</li>\n\t * <li>yyyy-MM-dd HH:mm:ss.SSSSSS</li>\n\t * <li>yyyyMMddHHmmss</li>\n\t * <li>yyyyMMddHHmmssSSS</li>\n\t * <li>yyyyMMdd</li>\n\t * <li>EEE, dd MMM yyyy HH:mm:ss z</li>\n\t * <li>EEE MMM dd HH:mm:ss zzz yyyy</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ssZ</li>\n\t * <li>yyyy-MM-dd'T'HH:mm:ss.SSSZ</li>\n\t * </ol>\n\t *\n\t * @param dateCharSequence 日期字符串\n\t * @return 日期\n\t */\n\tpublic static DateTime parse(CharSequence dateCharSequence) {\n\t\tif (StrUtil.isBlank(dateCharSequence)) {\n\t\t\treturn null;\n\t\t}\n\t\tString dateStr = dateCharSequence.toString();\n\t\t// 去掉两边空格并去掉中文日期中的“日”和“秒”，以规范长度\n\t\tdateStr = StrUtil.removeAll(dateStr.trim(), '日', '秒');\n\t\tint length = dateStr.length();\n\n\t\tif (NumberUtil.isNumber(dateStr)) {\n\t\t\t// 纯数字形式\n\t\t\tif (length == DatePattern.PURE_DATETIME_PATTERN.length()) {\n\t\t\t\treturn parse(dateStr, DatePattern.PURE_DATETIME_FORMAT);\n\t\t\t} else if (length == DatePattern.PURE_DATETIME_MS_PATTERN.length()) {\n\t\t\t\treturn parse(dateStr, DatePattern.PURE_DATETIME_MS_FORMAT);\n\t\t\t} else if (length == DatePattern.PURE_DATE_PATTERN.length()) {\n\t\t\t\treturn parse(dateStr, DatePattern.PURE_DATE_FORMAT);\n\t\t\t} else if (length == DatePattern.PURE_TIME_PATTERN.length()) {\n\t\t\t\treturn parse(dateStr, DatePattern.PURE_TIME_FORMAT);\n\t\t\t}else if(length == 13){\n\t\t\t\t// 时间戳\n\t\t\t\treturn date(NumberUtil.parseLong(dateStr));\n\t\t\t}\n\t\t} else if (ReUtil.isMatch(PatternPool.TIME, dateStr)) {\n\t\t\t// HH:mm:ss 或者 HH:mm 时间格式匹配单独解析\n\t\t\treturn parseTimeToday(dateStr);\n\t\t} else if (StrUtil.containsAnyIgnoreCase(dateStr, wtb)) {\n\t\t\t// JDK的Date对象toString默认格式，类似于：\n\t\t\t// Tue Jun 4 16:25:15 +0800 2019\n\t\t\t// Thu May 16 17:57:18 GMT+08:00 2019\n\t\t\t// Wed Aug 01 00:00:00 CST 2012\n\t\t\treturn parseRFC2822(dateStr);\n\t\t} else if (StrUtil.contains(dateStr, 'T')) {\n\t\t\t// ISO8601时间\n\t\t\treturn parseISO8601(dateStr);\n\t\t}\n\n\t\t//标准日期格式（包括单个数字的日期时间）\n\t\tdateStr = normalize(dateStr);\n\t\tif (ReUtil.isMatch(DatePattern.REGEX_NORM, dateStr)) {\n\t\t\tfinal int colonCount = StrUtil.count(dateStr, CharUtil.COLON);\n\t\t\tswitch (colonCount) {\n\t\t\t\tcase 0:\n\t\t\t\t\t// yyyy-MM-dd\n\t\t\t\t\treturn parse(dateStr, DatePattern.NORM_DATE_FORMAT);\n\t\t\t\tcase 1:\n\t\t\t\t\t// yyyy-MM-dd HH:mm\n\t\t\t\t\treturn parse(dateStr, DatePattern.NORM_DATETIME_MINUTE_FORMAT);\n\t\t\t\tcase 2:\n\t\t\t\t\tfinal int indexOfDot = StrUtil.indexOf(dateStr, CharUtil.DOT);\n\t\t\t\t\tif (indexOfDot > 0) {\n\t\t\t\t\t\tfinal int length1 = dateStr.length();\n\t\t\t\t\t\t// yyyy-MM-dd HH:mm:ss.SSS 或者 yyyy-MM-dd HH:mm:ss.SSSSSS\n\t\t\t\t\t\tif (length1 - indexOfDot > 4) {\n\t\t\t\t\t\t\t// 类似yyyy-MM-dd HH:mm:ss.SSSSSS，采取截断操作\n\t\t\t\t\t\t\tdateStr = StrUtil.subPre(dateStr, indexOfDot + 4);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn parse(dateStr, DatePattern.NORM_DATETIME_MS_FORMAT);\n\t\t\t\t\t}\n\t\t\t\t\t// yyyy-MM-dd HH:mm:ss\n\t\t\t\t\treturn parse(dateStr, DatePattern.NORM_DATETIME_FORMAT);\n\t\t\t}\n\t\t}\n\n\t\t// 没有更多匹配的时间格式\n\t\tthrow new DateException(\"No format fit for date String [{}] !\", dateStr);\n\t}\n\n\t// ------------------------------------ Parse end ----------------------------------------------\n\n\t// ------------------------------------ Offset start ----------------------------------------------\n\n\t/**\n\t * 修改日期为某个时间字段起始时间\n\t *\n\t * @param date      {@link Date}\n\t * @param dateField 保留到的时间字段，如定义为 {@link DateField#SECOND}，表示这个字段不变，这个字段以下字段全部归0\n\t * @return {@link DateTime}\n\t * @since 4.5.7\n\t */\n\tpublic static DateTime truncate(Date date, DateField dateField) {\n\t\treturn new DateTime(truncate(calendar(date), dateField));\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段四舍五入时间\n\t *\n\t * @param date      {@link Date}\n\t * @param dateField 时间字段\n\t * @return {@link DateTime}\n\t * @since 4.5.7\n\t */\n\tpublic static DateTime round(Date date, DateField dateField) {\n\t\treturn new DateTime(round(calendar(date), dateField));\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段结束时间\n\t *\n\t * @param date      {@link Date}\n\t * @param dateField 保留到的时间字段，如定义为 {@link DateField#SECOND}，表示这个字段不变，这个字段以下字段全部取最大值\n\t * @return {@link DateTime}\n\t * @since 4.5.7\n\t */\n\tpublic static DateTime ceiling(Date date, DateField dateField) {\n\t\treturn new DateTime(ceiling(calendar(date), dateField));\n\t}\n\n\t/**\n\t * 修改日期为某个时间字段结束时间<br>\n\t * 可选是否归零毫秒。\n\t *\n\t * <p>\n\t * 有时候由于毫秒部分必须为0（如MySQL数据库中），因此在此加上选项。\n\t * </p>\n\t *\n\t * @param date                {@link Date}\n\t * @param dateField           时间字段\n\t * @param truncateMillisecond 是否毫秒归零\n\t * @return {@link DateTime}\n\t * @since 4.5.7\n\t */\n\tpublic static DateTime ceiling(Date date, DateField dateField, boolean truncateMillisecond) {\n\t\treturn new DateTime(ceiling(calendar(date), dateField, truncateMillisecond));\n\t}\n\n\t/**\n\t * 获取秒级别的开始时间，即毫秒部分设置为0\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t * @since 4.6.2\n\t */\n\tpublic static DateTime beginOfSecond(Date date) {\n\t\treturn new DateTime(beginOfSecond(calendar(date)));\n\t}\n\n\t/**\n\t * 获取秒级别的结束时间，即毫秒设置为999\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t * @since 4.6.2\n\t */\n\tpublic static DateTime endOfSecond(Date date) {\n\t\treturn new DateTime(endOfSecond(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某小时的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfHour(Date date) {\n\t\treturn new DateTime(beginOfHour(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某小时的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfHour(Date date) {\n\t\treturn new DateTime(endOfHour(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某分钟的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfMinute(Date date) {\n\t\treturn new DateTime(beginOfMinute(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某分钟的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfMinute(Date date) {\n\t\treturn new DateTime(endOfMinute(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某天的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfDay(Date date) {\n\t\treturn new DateTime(beginOfDay(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某天的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfDay(Date date) {\n\t\treturn new DateTime(endOfDay(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某周的开始时间，周一定为一周的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfWeek(Date date) {\n\t\treturn new DateTime(beginOfWeek(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某周的开始时间\n\t *\n\t * @param date               日期\n\t * @param isMondayAsFirstDay 是否周一做为一周的第一天（false表示周日做为第一天）\n\t * @return {@link DateTime}\n\t * @since 5.4.0\n\t */\n\tpublic static DateTime beginOfWeek(Date date, boolean isMondayAsFirstDay) {\n\t\treturn new DateTime(beginOfWeek(calendar(date), isMondayAsFirstDay));\n\t}\n\n\t/**\n\t * 获取某周的结束时间，周日定为一周的结束\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfWeek(Date date) {\n\t\treturn new DateTime(endOfWeek(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某周的结束时间\n\t *\n\t * @param date              日期\n\t * @param isSundayAsLastDay 是否周日做为一周的最后一天（false表示周六做为最后一天）\n\t * @return {@link DateTime}\n\t * @since 5.4.0\n\t */\n\tpublic static DateTime endOfWeek(Date date, boolean isSundayAsLastDay) {\n\t\treturn new DateTime(endOfWeek(calendar(date), isSundayAsLastDay));\n\t}\n\n\t/**\n\t * 获取某月的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfMonth(Date date) {\n\t\treturn new DateTime(beginOfMonth(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某月的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfMonth(Date date) {\n\t\treturn new DateTime(endOfMonth(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某季度的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfQuarter(Date date) {\n\t\treturn new DateTime(beginOfQuarter(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某季度的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfQuarter(Date date) {\n\t\treturn new DateTime(endOfQuarter(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某年的开始时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime beginOfYear(Date date) {\n\t\treturn new DateTime(beginOfYear(calendar(date)));\n\t}\n\n\t/**\n\t * 获取某年的结束时间\n\t *\n\t * @param date 日期\n\t * @return {@link DateTime}\n\t */\n\tpublic static DateTime endOfYear(Date date) {\n\t\treturn new DateTime(endOfYear(calendar(date)));\n\t}\n\t// --------------------------------------------------- Offset for now\n\n\t/**\n\t * 昨天\n\t *\n\t * @return 昨天\n\t */\n\tpublic static DateTime yesterday() {\n\t\treturn offsetDay(new DateTime(), -1);\n\t}\n\n\t/**\n\t * 明天\n\t *\n\t * @return 明天\n\t * @since 3.0.1\n\t */\n\tpublic static DateTime tomorrow() {\n\t\treturn offsetDay(new DateTime(), 1);\n\t}\n\n\t/**\n\t * 上周\n\t *\n\t * @return 上周\n\t */\n\tpublic static DateTime lastWeek() {\n\t\treturn offsetWeek(new DateTime(), -1);\n\t}\n\n\t/**\n\t * 下周\n\t *\n\t * @return 下周\n\t * @since 3.0.1\n\t */\n\tpublic static DateTime nextWeek() {\n\t\treturn offsetWeek(new DateTime(), 1);\n\t}\n\n\t/**\n\t * 上个月\n\t *\n\t * @return 上个月\n\t */\n\tpublic static DateTime lastMonth() {\n\t\treturn offsetMonth(new DateTime(), -1);\n\t}\n\n\t/**\n\t * 下个月\n\t *\n\t * @return 下个月\n\t * @since 3.0.1\n\t */\n\tpublic static DateTime nextMonth() {\n\t\treturn offsetMonth(new DateTime(), 1);\n\t}\n\n\t/**\n\t * 偏移毫秒数\n\t *\n\t * @param date   日期\n\t * @param offset 偏移毫秒数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetMillisecond(Date date, int offset) {\n\t\treturn offset(date, DateField.MILLISECOND, offset);\n\t}\n\n\t/**\n\t * 偏移秒数\n\t *\n\t * @param date   日期\n\t * @param offset 偏移秒数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetSecond(Date date, int offset) {\n\t\treturn offset(date, DateField.SECOND, offset);\n\t}\n\n\t/**\n\t * 偏移分钟\n\t *\n\t * @param date   日期\n\t * @param offset 偏移分钟数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetMinute(Date date, int offset) {\n\t\treturn offset(date, DateField.MINUTE, offset);\n\t}\n\n\t/**\n\t * 偏移小时\n\t *\n\t * @param date   日期\n\t * @param offset 偏移小时数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetHour(Date date, int offset) {\n\t\treturn offset(date, DateField.HOUR_OF_DAY, offset);\n\t}\n\n\t/**\n\t * 偏移天\n\t *\n\t * @param date   日期\n\t * @param offset 偏移天数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetDay(Date date, int offset) {\n\t\treturn offset(date, DateField.DAY_OF_YEAR, offset);\n\t}\n\n\t/**\n\t * 偏移周\n\t *\n\t * @param date   日期\n\t * @param offset 偏移周数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetWeek(Date date, int offset) {\n\t\treturn offset(date, DateField.WEEK_OF_YEAR, offset);\n\t}\n\n\t/**\n\t * 偏移月\n\t *\n\t * @param date   日期\n\t * @param offset 偏移月数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offsetMonth(Date date, int offset) {\n\t\treturn offset(date, DateField.MONTH, offset);\n\t}\n\n\t/**\n\t * 偏移年\n\t *\n\t * @param date   日期\n\t * @param offset 偏移年数，正数向未来偏移，负数向历史偏移\n\t * @return 偏移后的日期\n\t * @since 5.8.29\n\t */\n\tpublic static DateTime offsetYear(final Date date, final int offset) {\n\t\treturn offset(date, DateField.YEAR, offset);\n\t}\n\n\t/**\n\t * 获取指定日期偏移指定时间后的时间，生成的偏移日期不影响原日期\n\t *\n\t * @param date      基准日期\n\t * @param dateField 偏移的粒度大小（小时、天、月等）{@link DateField}\n\t * @param offset    偏移量，正数为向后偏移，负数为向前偏移\n\t * @return 偏移后的日期\n\t */\n\tpublic static DateTime offset(Date date, DateField dateField, int offset) {\n\t\tif (date == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn dateNew(date).offset(dateField, offset);\n\t}\n\n\t// ------------------------------------ Offset end ----------------------------------------------\n\n\t/**\n\t * 判断两个日期相差的时长，只保留绝对值\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param unit      相差的单位：相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等\n\t * @return 日期差\n\t */\n\tpublic static long between(Date beginDate, Date endDate, DateUnit unit) {\n\t\treturn between(beginDate, endDate, unit, true);\n\t}\n\n\t/**\n\t * 判断两个日期相差的时长\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param unit      相差的单位：相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等\n\t * @param isAbs     日期间隔是否只保留绝对值正数\n\t * @return 日期差\n\t * @since 3.3.1\n\t */\n\tpublic static long between(Date beginDate, Date endDate, DateUnit unit, boolean isAbs) {\n\t\treturn new DateBetween(beginDate, endDate, isAbs).between(unit);\n\t}\n\n\t/**\n\t * 判断两个日期相差的毫秒数\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @return 日期差\n\t * @since 3.0.1\n\t */\n\tpublic static long betweenMs(Date beginDate, Date endDate) {\n\t\treturn new DateBetween(beginDate, endDate).between(DateUnit.MS);\n\t}\n\n\t/**\n\t * 判断两个日期相差的天数<br>\n\t *\n\t * <pre>\n\t * 有时候我们计算相差天数的时候需要忽略时分秒。\n\t * 比如：2016-02-01 23:59:59和2016-02-02 00:00:00相差一秒\n\t * 如果isReset为{@code false}相差天数为0。\n\t * 如果isReset为{@code true}相差天数将被计算为1\n\t * </pre>\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param isReset   是否重置时间为起始时间\n\t * @return 日期差\n\t * @since 3.0.1\n\t */\n\tpublic static long betweenDay(Date beginDate, Date endDate, boolean isReset) {\n\t\tif (isReset) {\n\t\t\tbeginDate = beginOfDay(beginDate);\n\t\t\tendDate = beginOfDay(endDate);\n\t\t}\n\t\treturn between(beginDate, endDate, DateUnit.DAY);\n\t}\n\n\t/**\n\t * 计算指定时间区间内的周数\n\t *\n\t * @param beginDate 开始时间\n\t * @param endDate   结束时间\n\t * @param isReset   是否重置时间为起始时间\n\t * @return 周数\n\t */\n\tpublic static long betweenWeek(Date beginDate, Date endDate, boolean isReset) {\n\t\tif (isReset) {\n\t\t\tbeginDate = beginOfDay(beginDate);\n\t\t\tendDate = beginOfDay(endDate);\n\t\t}\n\t\treturn between(beginDate, endDate, DateUnit.WEEK);\n\t}\n\n\t/**\n\t * 计算两个日期相差月数<br>\n\t * 在非重置情况下，如果起始日期的天大于结束日期的天，月数要少算1（不足1个月）\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param isReset   是否重置时间为起始时间（重置天时分秒）\n\t * @return 相差月数\n\t * @since 3.0.8\n\t */\n\tpublic static long betweenMonth(Date beginDate, Date endDate, boolean isReset) {\n\t\treturn new DateBetween(beginDate, endDate).betweenMonth(isReset);\n\t}\n\n\t/**\n\t * 计算两个日期相差年数<br>\n\t * 在非重置情况下，如果起始日期的月大于结束日期的月，年数要少算1（不足1年）\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param isReset   是否重置时间为起始时间（重置月天时分秒）\n\t * @return 相差年数\n\t * @since 3.0.8\n\t */\n\tpublic static long betweenYear(Date beginDate, Date endDate, boolean isReset) {\n\t\treturn new DateBetween(beginDate, endDate).betweenYear(isReset);\n\t}\n\n\t/**\n\t * 格式化日期间隔输出\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @param level     级别，按照天、小时、分、秒、毫秒分为5个等级\n\t * @return XX天XX小时XX分XX秒\n\t */\n\tpublic static String formatBetween(Date beginDate, Date endDate, BetweenFormatter.Level level) {\n\t\treturn formatBetween(between(beginDate, endDate, DateUnit.MS), level);\n\t}\n\n\t/**\n\t * 格式化日期间隔输出，精确到毫秒\n\t *\n\t * @param beginDate 起始日期\n\t * @param endDate   结束日期\n\t * @return XX天XX小时XX分XX秒\n\t * @since 3.0.1\n\t */\n\tpublic static String formatBetween(Date beginDate, Date endDate) {\n\t\treturn formatBetween(between(beginDate, endDate, DateUnit.MS));\n\t}\n\n\t/**\n\t * 格式化日期间隔输出\n\t *\n\t * @param betweenMs 日期间隔\n\t * @param level     级别，按照天、小时、分、秒、毫秒分为5个等级\n\t * @return XX天XX小时XX分XX秒XX毫秒\n\t */\n\tpublic static String formatBetween(long betweenMs, BetweenFormatter.Level level) {\n\t\treturn new BetweenFormatter(betweenMs, level).format();\n\t}\n\n\t/**\n\t * 格式化日期间隔输出，精确到毫秒\n\t *\n\t * @param betweenMs 日期间隔\n\t * @return XX天XX小时XX分XX秒XX毫秒\n\t * @since 3.0.1\n\t */\n\tpublic static String formatBetween(long betweenMs) {\n\t\treturn new BetweenFormatter(betweenMs, BetweenFormatter.Level.MILLISECOND).format();\n\t}\n\n\t/**\n\t * 当前日期是否在日期指定范围内<br>\n\t * 起始日期和结束日期可以互换\n\t *\n\t * @param date      被检查的日期\n\t * @param beginDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 是否在范围内\n\t * @since 3.0.8\n\t */\n\tpublic static boolean isIn(Date date, Date beginDate, Date endDate) {\n\t\tif (date instanceof DateTime) {\n\t\t\treturn ((DateTime) date).isIn(beginDate, endDate);\n\t\t} else {\n\t\t\treturn new DateTime(date).isIn(beginDate, endDate);\n\t\t}\n\t}\n\n\t/**\n\t * 是否为相同时间<br>\n\t * 此方法比较两个日期的时间戳是否相同\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 是否为相同时间\n\t * @since 4.1.13\n\t */\n\tpublic static boolean isSameTime(Date date1, Date date2) {\n\t\treturn date1.compareTo(date2) == 0;\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一天\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 是否为同一天\n\t * @since 4.1.13\n\t */\n\tpublic static boolean isSameDay(final Date date1, final Date date2) {\n\t\tif (date1 == null || date2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\t\treturn CalendarUtil.isSameDay(calendar(date1), calendar(date2));\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一周\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @param isMon 是否为周一。国内第一天为星期一，国外第一天为星期日\n\t * @return 是否为同一周\n\t */\n\tpublic static boolean isSameWeek(final Date date1, final Date date2, boolean isMon) {\n\t\tif (date1 == null || date2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\t\treturn CalendarUtil.isSameWeek(calendar(date1), calendar(date2), isMon);\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一月\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 是否为同一月\n\t * @since 5.4.1\n\t */\n\tpublic static boolean isSameMonth(final Date date1, final Date date2) {\n\t\tif (date1 == null || date2 == null) {\n\t\t\tthrow new IllegalArgumentException(\"The date must not be null\");\n\t\t}\n\t\treturn CalendarUtil.isSameMonth(calendar(date1), calendar(date2));\n\t}\n\n\n\t/**\n\t * 计时，常用于记录某段代码的执行时间，单位：纳秒\n\t *\n\t * @param preTime 之前记录的时间\n\t * @return 时间差，纳秒\n\t */\n\tpublic static long spendNt(long preTime) {\n\t\treturn System.nanoTime() - preTime;\n\t}\n\n\t/**\n\t * 计时，常用于记录某段代码的执行时间，单位：毫秒\n\t *\n\t * @param preTime 之前记录的时间\n\t * @return 时间差，毫秒\n\t */\n\tpublic static long spendMs(long preTime) {\n\t\treturn System.currentTimeMillis() - preTime;\n\t}\n\n\t/**\n\t * 格式化成yyMMddHHmm后转换为int型\n\t *\n\t * @param date 日期\n\t * @return int\n\t * @deprecated 2022年后结果溢出，此方法废弃\n\t */\n\t@Deprecated\n\tpublic static int toIntSecond(Date date) {\n\t\treturn Integer.parseInt(DateUtil.format(date, \"yyMMddHHmm\"));\n\t}\n\n\t/**\n\t * 计时器<br>\n\t * 计算某个过程花费的时间，精确到毫秒\n\t *\n\t * @return Timer\n\t */\n\tpublic static TimeInterval timer() {\n\t\treturn new TimeInterval();\n\n\t}\n\n\t/**\n\t * 计时器<br>\n\t * 计算某个过程花费的时间，精确到毫秒\n\t *\n\t * @param isNano 是否使用纳秒计数，false则使用毫秒\n\t * @return Timer\n\t * @since 5.2.3\n\t */\n\tpublic static TimeInterval timer(boolean isNano) {\n\t\treturn new TimeInterval(isNano);\n\t}\n\n\t/**\n\t * 创建秒表{@link StopWatch}，用于对代码块的执行时间计数\n\t * <p>\n\t * 使用方法如下：\n\t *\n\t * <pre>\n\t * StopWatch stopWatch = DateUtil.createStopWatch();\n\t *\n\t * // 任务1\n\t * stopWatch.start(\"任务一\");\n\t * Thread.sleep(1000);\n\t * stopWatch.stop();\n\t *\n\t * // 任务2\n\t * stopWatch.start(\"任务二\");\n\t * Thread.sleep(2000);\n\t * stopWatch.stop();\n\t *\n\t * // 打印出耗时\n\t * Console.log(stopWatch.prettyPrint());\n\t *\n\t * </pre>\n\t *\n\t * @return {@link StopWatch}\n\t * @since 5.2.3\n\t */\n\tpublic static StopWatch createStopWatch() {\n\t\treturn new StopWatch();\n\t}\n\n\t/**\n\t * 创建秒表{@link StopWatch}，用于对代码块的执行时间计数\n\t * <p>\n\t * 使用方法如下：\n\t *\n\t * <pre>\n\t * StopWatch stopWatch = DateUtil.createStopWatch(\"任务名称\");\n\t *\n\t * // 任务1\n\t * stopWatch.start(\"任务一\");\n\t * Thread.sleep(1000);\n\t * stopWatch.stop();\n\t *\n\t * // 任务2\n\t * stopWatch.start(\"任务二\");\n\t * Thread.sleep(2000);\n\t * stopWatch.stop();\n\t *\n\t * // 打印出耗时\n\t * Console.log(stopWatch.prettyPrint());\n\t *\n\t * </pre>\n\t *\n\t * @param id 用于标识秒表的唯一ID\n\t * @return {@link StopWatch}\n\t * @since 5.2.3\n\t */\n\tpublic static StopWatch createStopWatch(String id) {\n\t\treturn new StopWatch(id);\n\t}\n\n\t/**\n\t * 生日转为年龄，计算法定年龄\n\t *\n\t * @param birthDay 生日，标准日期字符串\n\t * @return 年龄\n\t */\n\tpublic static int ageOfNow(String birthDay) {\n\t\treturn ageOfNow(parse(birthDay));\n\t}\n\n\t/**\n\t * 生日转为年龄，计算法定年龄\n\t *\n\t * @param birthDay 生日\n\t * @return 年龄\n\t */\n\tpublic static int ageOfNow(Date birthDay) {\n\t\treturn age(birthDay, date());\n\t}\n\n\t/**\n\t * 是否闰年\n\t *\n\t * @param year 年\n\t * @return 是否闰年\n\t */\n\tpublic static boolean isLeapYear(int year) {\n\t\treturn Year.isLeap(year);\n\t}\n\n\t/**\n\t * 计算相对于dateToCompare的年龄，常用于计算指定生日在某年的年龄\n\t *\n\t * @param birthday      生日\n\t * @param dateToCompare 需要对比的日期\n\t * @return 年龄\n\t */\n\tpublic static int age(Date birthday, Date dateToCompare) {\n\t\tAssert.notNull(birthday, \"Birthday can not be null !\");\n\t\tif (null == dateToCompare) {\n\t\t\tdateToCompare = date();\n\t\t}\n\t\treturn age(birthday.getTime(), dateToCompare.getTime());\n\t}\n\n\t/**\n\t * 判定给定开始时间经过某段时间后是否过期\n\t *\n\t * @param startDate  开始时间\n\t * @param dateField  时间单位\n\t * @param timeLength 实际经过时长\n\t * @param endDate    被比较的时间，即有效期的截止时间。如果经过时长后的时间晚于截止时间，就表示过期\n\t * @return 是否过期\n\t * @since 3.1.1\n\t * @deprecated 此方法存在一定的歧义，容易产生误导，废弃。\n\t */\n\t@Deprecated\n\tpublic static boolean isExpired(Date startDate, DateField dateField, int timeLength, Date endDate) {\n\t\tfinal Date offsetDate = offset(startDate, dateField, timeLength);\n\t\treturn offsetDate.after(endDate);\n\t}\n\n\t/**\n\t * 判定在指定检查时间是否过期。\n\t *\n\t * <p>\n\t * 以商品为例，startDate即生产日期，endDate即保质期的截止日期，checkDate表示在何时检查是否过期（一般为当前时间）<br>\n\t * endDate和startDate的差值即为保质期（按照毫秒计），checkDate和startDate的差值即为实际经过的时长，实际时长大于保质期表示超时。\n\t * </p>\n\t *\n\t * @param startDate 开始时间\n\t * @param endDate   被比较的时间，即有效期的截止时间。如果经过时长后的时间晚于被检查的时间，就表示过期\n\t * @param checkDate 检查时间，可以是当前时间，既\n\t * @return 是否过期\n\t * @since 5.1.1\n\t * @deprecated 使用isIn方法\n\t */\n\t@Deprecated\n\tpublic static boolean isExpired(Date startDate, Date endDate, Date checkDate) {\n\t\treturn betweenMs(startDate, checkDate) > betweenMs(startDate, endDate);\n\t}\n\n\t/**\n\t * HH:mm:ss 时间格式字符串转为秒数<br>\n\t * 参考：<a href=\"https://github.com/iceroot\">https://github.com/iceroot</a>\n\t *\n\t * @param timeStr 字符串时分秒(HH:mm:ss)格式\n\t * @return 时分秒转换后的秒数\n\t * @since 3.1.2\n\t */\n\tpublic static int timeToSecond(String timeStr) {\n\t\tif (StrUtil.isEmpty(timeStr)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal List<String> hms = StrUtil.splitTrim(timeStr, StrUtil.C_COLON, 3);\n\t\tint lastIndex = hms.size() - 1;\n\n\t\tint result = 0;\n\t\tfor (int i = lastIndex; i >= 0; i--) {\n\t\t\tresult += Integer.parseInt(hms.get(i)) * Math.pow(60, (lastIndex - i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 秒数转为时间格式(HH:mm:ss)<br>\n\t * 参考：<a href=\"https://github.com/iceroot\">https://github.com/iceroot</a>\n\t *\n\t * @param seconds 需要转换的秒数\n\t * @return 转换后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String secondToTime(int seconds) {\n\t\tif (seconds < 0) {\n\t\t\tthrow new IllegalArgumentException(\"Seconds must be a positive number!\");\n\t\t}\n\n\t\tint hour = seconds / 3600;\n\t\tint other = seconds % 3600;\n\t\tint minute = other / 60;\n\t\tint second = other % 60;\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tif (hour < 10) {\n\t\t\tsb.append(\"0\");\n\t\t}\n\t\tsb.append(hour);\n\t\tsb.append(\":\");\n\t\tif (minute < 10) {\n\t\t\tsb.append(\"0\");\n\t\t}\n\t\tsb.append(minute);\n\t\tsb.append(\":\");\n\t\tif (second < 10) {\n\t\t\tsb.append(\"0\");\n\t\t}\n\t\tsb.append(second);\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 创建日期范围生成器\n\t *\n\t * @param start 起始日期时间（包括）\n\t * @param end   结束日期时间\n\t * @param unit  步进单位\n\t * @return {@link DateRange}\n\t */\n\tpublic static DateRange range(Date start, Date end, final DateField unit) {\n\t\treturn new DateRange(start, end, unit);\n\t}\n\n\t/**\n\t * 俩个时间区间取交集\n\t *\n\t * @param start 开始区间\n\t * @param end   结束区间\n\t * @return true 包含\n\t * @author handy\n\t * @since 5.7.21\n\t */\n\tpublic static List<DateTime> rangeContains(DateRange start, DateRange end) {\n\t\tList<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start.reset());\n\t\tList<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end.reset());\n\t\treturn startDateTimes.stream().filter(endDateTimes::contains).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 俩个时间区间取差集(end - start)\n\t *\n\t * @param start 开始区间\n\t * @param end   结束区间\n\t * @return true 包含\n\t * @author handy\n\t * @since 5.7.21\n\t */\n\tpublic static List<DateTime> rangeNotContains(DateRange start, DateRange end) {\n\t\tList<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start.reset());\n\t\tList<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end.reset());\n\t\treturn endDateTimes.stream().filter(item -> !startDateTimes.contains(item)).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 按日期范围遍历，执行 function\n\t *\n\t * @param start 起始日期时间（包括）\n\t * @param end   结束日期时间\n\t * @param unit  步进单位\n\t * @param func  每次遍历要执行的 function\n\t * @param <T>   Date经过函数处理结果类型\n\t * @return 结果列表\n\t * @since 5.7.21\n\t */\n\tpublic static <T> List<T> rangeFunc(Date start, Date end, final DateField unit, Function<Date, T> func) {\n\t\tif (start == null || end == null || start.after(end)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\tArrayList<T> list = new ArrayList<>();\n\t\tfor (DateTime date : range(start, end, unit)) {\n\t\t\tlist.add(func.apply(date));\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 按日期范围遍历，执行 consumer\n\t *\n\t * @param start    起始日期时间（包括）\n\t * @param end      结束日期时间\n\t * @param unit     步进单位\n\t * @param consumer 每次遍历要执行的 consumer\n\t * @since 5.7.21\n\t */\n\tpublic static void rangeConsume(Date start, Date end, final DateField unit, Consumer<Date> consumer) {\n\t\tif (start == null || end == null || start.after(end)) {\n\t\t\treturn;\n\t\t}\n\t\trange(start, end, unit).forEach(consumer);\n\t}\n\n\t/**\n\t * 根据步进单位获取起始日期时间和结束日期时间的时间区间集合\n\t *\n\t * @param start 起始日期时间\n\t * @param end   结束日期时间\n\t * @param unit  步进单位\n\t * @return {@link DateRange}\n\t */\n\tpublic static List<DateTime> rangeToList(Date start, Date end, DateField unit) {\n\t\treturn CollUtil.newArrayList((Iterable<DateTime>) range(start, end, unit));\n\t}\n\n\t/**\n\t * 根据步进单位和步进获取起始日期时间和结束日期时间的时间区间集合\n\t *\n\t * @param start 起始日期时间\n\t * @param end   结束日期时间\n\t * @param unit  步进单位\n\t * @param step  步进\n\t * @return {@link DateRange}\n\t * @since 5.7.16\n\t */\n\tpublic static List<DateTime> rangeToList(Date start, Date end, final DateField unit, int step) {\n\t\treturn CollUtil.newArrayList((Iterable<DateTime>) new DateRange(start, end, unit, step));\n\t}\n\n\t/**\n\t * 通过生日计算星座\n\t *\n\t * @param month 月，从0开始计数\n\t * @param day   天\n\t * @return 星座名\n\t * @since 4.4.3\n\t */\n\tpublic static String getZodiac(int month, int day) {\n\t\treturn Zodiac.getZodiac(month, day);\n\t}\n\n\t/**\n\t * 计算生肖，只计算1900年后出生的人\n\t *\n\t * @param year 农历年\n\t * @return 生肖名\n\t * @since 4.4.3\n\t */\n\tpublic static String getChineseZodiac(int year) {\n\t\treturn Zodiac.getChineseZodiac(year);\n\t}\n\n\t/**\n\t * {@code null}安全的日期比较，{@code null}对象排在末尾\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 比较结果，如果date1 &lt; date2，返回数小于0，date1==date2返回0，date1 &gt; date2 大于0\n\t * @since 4.6.2\n\t */\n\tpublic static int compare(Date date1, Date date2) {\n\t\treturn CompareUtil.compare(date1, date2);\n\t}\n\n\t/**\n\t * {@code null}安全的日期比较，并只比较指定格式； {@code null}对象排在末尾, 并指定日期格式；\n\t *\n\t * @param date1  日期1\n\t * @param date2  日期2\n\t * @param format 日期格式，常用格式见： {@link DatePattern}; 允许为空； date1 date2; eg: yyyy-MM-dd\n\t * @return 比较结果，如果date1 &lt; date2，返回数小于0，date1==date2返回0，date1 &gt; date2 大于0\n\t * @author dazer\n\t * @since 5.6.4\n\t */\n\tpublic static int compare(Date date1, Date date2, String format) {\n\t\tif (format != null) {\n\t\t\tif (date1 != null) {\n\t\t\t\tdate1 = parse(format(date1, format), format);\n\t\t\t}\n\t\t\tif (date2 != null) {\n\t\t\t\tdate2 = parse(format(date2, format), format);\n\t\t\t}\n\t\t}\n\t\treturn CompareUtil.compare(date1, date2);\n\t}\n\n\t/**\n\t * 纳秒转毫秒\n\t *\n\t * @param duration 时长\n\t * @return 时长毫秒\n\t * @since 4.6.6\n\t */\n\tpublic static long nanosToMillis(long duration) {\n\t\treturn TimeUnit.NANOSECONDS.toMillis(duration);\n\t}\n\n\t/**\n\t * 纳秒转秒，保留小数\n\t *\n\t * @param duration 时长\n\t * @return 秒\n\t * @since 4.6.6\n\t */\n\tpublic static double nanosToSeconds(long duration) {\n\t\treturn duration / 1_000_000_000.0;\n\t}\n\n\t/**\n\t * Date对象转换为{@link Instant}对象\n\t *\n\t * @param date Date对象\n\t * @return {@link Instant}对象\n\t * @since 5.0.2\n\t */\n\tpublic static Instant toInstant(Date date) {\n\t\treturn null == date ? null : date.toInstant();\n\t}\n\n\t/**\n\t * Date对象转换为{@link Instant}对象\n\t *\n\t * @param temporalAccessor Date对象\n\t * @return {@link Instant}对象\n\t * @since 5.0.2\n\t */\n\tpublic static Instant toInstant(TemporalAccessor temporalAccessor) {\n\t\treturn TemporalAccessorUtil.toInstant(temporalAccessor);\n\t}\n\n\t/**\n\t * {@link Instant} 转换为 {@link LocalDateTime}，使用系统默认时区\n\t *\n\t * @param instant {@link Instant}\n\t * @return {@link LocalDateTime}\n\t * @see LocalDateTimeUtil#of(Instant)\n\t * @since 5.0.5\n\t */\n\tpublic static LocalDateTime toLocalDateTime(Instant instant) {\n\t\treturn LocalDateTimeUtil.of(instant);\n\t}\n\n\t/**\n\t * {@link Date} 转换为 {@link LocalDateTime}，使用系统默认时区\n\t *\n\t * @param date {@link Date}\n\t * @return {@link LocalDateTime}\n\t * @see LocalDateTimeUtil#of(Date)\n\t * @since 5.0.5\n\t */\n\tpublic static LocalDateTime toLocalDateTime(Date date) {\n\t\treturn LocalDateTimeUtil.of(date);\n\t}\n\n\t/**\n\t * {@link Date} 转换时区\n\t *\n\t * @param date         {@link Date}\n\t * @param zoneId       {@link ZoneId}\n\t * @return {@link DateTime}\n\t * @since 5.8.3\n\t */\n\tpublic static DateTime convertTimeZone(Date date, ZoneId zoneId) {\n\t\treturn new DateTime(date, ZoneUtil.toTimeZone(zoneId));\n\t}\n\n\t/**\n\t * {@link Date} 转换时区\n\t *\n\t * @param date           {@link Date}\n\t * @param timeZone       {@link TimeZone}\n\t * @return {@link DateTime}\n\t * @since 5.8.3\n\t */\n\tpublic static DateTime convertTimeZone(Date date, TimeZone timeZone) {\n\t\treturn new DateTime(date, timeZone);\n\t}\n\n\t/**\n\t * 获得指定年份的总天数\n\t *\n\t * @param year 年份\n\t * @return 天\n\t * @since 5.3.6\n\t */\n\tpublic static int lengthOfYear(int year) {\n\t\treturn Year.of(year).length();\n\t}\n\n\t/**\n\t * 获得指定月份的总天数\n\t *\n\t * @param month      月份\n\t * @param isLeapYear 是否闰年\n\t * @return 天\n\t * @since 5.4.2\n\t */\n\tpublic static int lengthOfMonth(int month, boolean isLeapYear) {\n\t\treturn java.time.Month.of(month).length(isLeapYear);\n\t}\n\n\t/**\n\t * 创建{@link SimpleDateFormat}，注意此对象非线程安全！<br>\n\t * 此对象默认为严格格式模式，即parse时如果格式不正确会报错。\n\t *\n\t * @param pattern 表达式\n\t * @return {@link SimpleDateFormat}\n\t * @since 5.5.5\n\t */\n\tpublic static SimpleDateFormat newSimpleFormat(String pattern) {\n\t\treturn newSimpleFormat(pattern, null, null);\n\t}\n\n\t/**\n\t * 创建{@link SimpleDateFormat}，注意此对象非线程安全！<br>\n\t * 此对象默认为严格格式模式，即parse时如果格式不正确会报错。\n\t *\n\t * @param pattern  表达式\n\t * @param locale   {@link Locale}，{@code null}表示默认\n\t * @param timeZone {@link TimeZone}，{@code null}表示默认\n\t * @return {@link SimpleDateFormat}\n\t * @since 5.5.5\n\t */\n\tpublic static SimpleDateFormat newSimpleFormat(String pattern, Locale locale, TimeZone timeZone) {\n\t\tif (null == locale) {\n\t\t\tlocale = Locale.getDefault(Locale.Category.FORMAT);\n\t\t}\n\t\tfinal SimpleDateFormat format = new SimpleDateFormat(pattern, locale);\n\t\tif (null != timeZone) {\n\t\t\tformat.setTimeZone(timeZone);\n\t\t}\n\t\tformat.setLenient(false);\n\t\treturn format;\n\t}\n\n\t/**\n\t * 获取时长单位简写\n\t *\n\t * @param unit 单位\n\t * @return 单位简写名称\n\t * @since 5.7.16\n\t */\n\tpublic static String getShotName(TimeUnit unit) {\n\t\tswitch (unit) {\n\t\t\tcase NANOSECONDS:\n\t\t\t\treturn \"ns\";\n\t\t\tcase MICROSECONDS:\n\t\t\t\treturn \"μs\";\n\t\t\tcase MILLISECONDS:\n\t\t\t\treturn \"ms\";\n\t\t\tcase SECONDS:\n\t\t\t\treturn \"s\";\n\t\t\tcase MINUTES:\n\t\t\t\treturn \"min\";\n\t\t\tcase HOURS:\n\t\t\t\treturn \"h\";\n\t\t\tdefault:\n\t\t\t\treturn unit.name().toLowerCase();\n\t\t}\n\t}\n\n\t/**\n\t * 检查两个时间段是否有时间重叠<br>\n\t * 重叠指两个时间段是否有交集，注意此方法时间段重合时如：\n\t * <ul>\n\t *     <li>此方法未纠正开始时间小于结束时间</li>\n\t *     <li>当realStartTime和realEndTime或startTime和endTime相等时,退化为判断区间是否包含点</li>\n\t *     <li>当realStartTime和realEndTime和startTime和endTime相等时,退化为判断点与点是否相等</li>\n\t * </ul>\n\t * See <a href=\"https://www.ics.uci.edu/~alspaugh/cls/shr/allen.html\">准确的区间关系参考:艾伦区间代数</a>\n\t * @param realStartTime 第一个时间段的开始时间\n\t * @param realEndTime   第一个时间段的结束时间\n\t * @param startTime     第二个时间段的开始时间\n\t * @param endTime       第二个时间段的结束时间\n\t * @return true 表示时间有重合或包含或相等\n\t * @since 5.7.22\n\t */\n\tpublic static boolean isOverlap(Date realStartTime, Date realEndTime,\n\t\t\t\t\t\t\t\t\tDate startTime, Date endTime) {\n\n\t\t// x>b||a>y 无交集\n\t\t// 则有交集的逻辑为 !(x>b||a>y)\n\t\t// 根据德摩根公式，可化简为 x<=b && a<=y 即 realStartTime<=endTime && startTime<=realEndTime\n\t\treturn realStartTime.compareTo(endTime) <=0 && startTime.compareTo(realEndTime) <= 0;\n\t}\n\n\t/**\n\t * 是否为本月最后一天\n\t * @param date {@link Date}\n\t * @return 是否为本月最后一天\n\t * @since 5.8.9\n\t */\n\tpublic static boolean isLastDayOfMonth(Date date){\n\t\treturn date(date).isLastDayOfMonth();\n\t}\n\n\t/**\n\t * 获得本月的最后一天\n\t * @param date {@link Date}\n\t * @return 天\n\t * @since 5.8.9\n\t */\n\tpublic static int getLastDayOfMonth(Date date){\n\t\treturn date(date).getLastDayOfMonth();\n\t}\n\n\t// ------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 标准化日期，默认处理以空格区分的日期时间格式，空格前为日期，空格后为时间：<br>\n\t * 将以下字符替换为\"-\"\n\t *\n\t * <pre>\n\t * \".\"\n\t * \"/\"\n\t * \"年\"\n\t * \"月\"\n\t * </pre>\n\t * <p>\n\t * 将以下字符去除\n\t *\n\t * <pre>\n\t * \"日\"\n\t * </pre>\n\t * <p>\n\t * 将以下字符替换为\":\"\n\t *\n\t * <pre>\n\t * \"时\"\n\t * \"分\"\n\t * \"秒\"\n\t * </pre>\n\t * <p>\n\t * 当末位是\":\"时去除之（不存在毫秒时）\n\t *\n\t * @param dateStr 日期时间字符串\n\t * @return 格式化后的日期字符串\n\t */\n\tprivate static String normalize(CharSequence dateStr) {\n\t\tif (StrUtil.isBlank(dateStr)) {\n\t\t\treturn StrUtil.str(dateStr);\n\t\t}\n\n\t\t// 日期时间分开处理\n\t\tfinal List<String> dateAndTime = StrUtil.splitTrim(dateStr, ' ');\n\t\tfinal int size = dateAndTime.size();\n\t\tif (size < 1 || size > 2) {\n\t\t\t// 非可被标准处理的格式\n\t\t\treturn StrUtil.str(dateStr);\n\t\t}\n\n\t\tfinal StringBuilder builder = StrUtil.builder();\n\n\t\t// 日期部分（\"\\\"、\"/\"、\".\"、\"年\"、\"月\"都替换为\"-\"）\n\t\tString datePart = DATE_SEPARATOR_PATTERN.matcher(dateAndTime.get(0)).replaceAll(\"-\");\n\t\tdatePart = StrUtil.removeSuffix(datePart, \"日\");\n\t\tbuilder.append(datePart);\n\n\t\t// 时间部分\n\t\tif (size == 2) {\n\t\t\tbuilder.append(' ');\n\t\t\tString timePart = TIME_UNIT_PATTERN.matcher(dateAndTime.get(1)).replaceAll(\":\");\n\t\t\ttimePart = StrUtil.removeSuffix(timePart, \":\");\n\t\t\t//将ISO8601中的逗号替换为.\n\t\t\ttimePart = timePart.replace(',', '.');\n\t\t\tbuilder.append(timePart);\n\t\t}\n\n\t\treturn builder.toString();\n\t}\n\t// ------------------------------------------------------------------------ Private method end\n\n\t/**\n\t * 如果日期中的毫秒部分超出3位，会导致秒数增加，因此只保留前三位\n\t *\n\t * @param dateStr 日期字符串\n\t * @param before  毫秒部分的前一个字符\n\t * @param after   毫秒部分的后一个字符\n\t * @return 规范之后的毫秒部分\n\t */\n\tprivate static String normalizeMillSeconds(String dateStr, CharSequence before, CharSequence after) {\n\t\tif (StrUtil.isBlank(after)) {\n\t\t\tString millOrNaco = StrUtil.subPre(StrUtil.subAfter(dateStr, before, true), 3);\n\t\t\treturn StrUtil.subBefore(dateStr, before, true) + before + millOrNaco;\n\t\t}\n\t\tString millOrNaco = StrUtil.subPre(StrUtil.subBetween(dateStr, before, after), 3);\n\t\treturn StrUtil.subBefore(dateStr, before, true)\n\t\t\t\t+ before\n\t\t\t\t+ millOrNaco + after + StrUtil.subAfter(dateStr, after, true);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/GroupTimeInterval.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n/**\n * 分组计时器<br>\n * 计算某几个过程花费的时间，精确到毫秒或纳秒\n *\n * @author Looly\n * @since 5.5.2\n */\npublic class GroupTimeInterval implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final boolean isNano;\n\tprotected final Map<String, Long> groupMap;\n\n\t/**\n\t * 构造\n\t *\n\t * @param isNano 是否使用纳秒计数，false则使用毫秒\n\t */\n\tpublic GroupTimeInterval(boolean isNano) {\n\t\tthis.isNano = isNano;\n\t\tgroupMap = new SafeConcurrentHashMap<>();\n\t}\n\n\t/**\n\t * 清空所有定时记录\n\t *\n\t * @return this\n\t */\n\tpublic GroupTimeInterval clear(){\n\t\tthis.groupMap.clear();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 开始计时并返回当前时间\n\t *\n\t * @param id 分组ID\n\t * @return 开始计时并返回当前时间\n\t */\n\tpublic long start(String id) {\n\t\tfinal long time = getTime();\n\t\tthis.groupMap.put(id, time);\n\t\treturn time;\n\t}\n\n\t/**\n\t * 重新计时并返回从开始到当前的持续时间秒<br>\n\t * 如果此分组下没有记录，则返回0;\n\t *\n\t * @param id 分组ID\n\t * @return 重新计时并返回从开始到当前的持续时间\n\t */\n\tpublic long intervalRestart(String id) {\n\t\tfinal long now = getTime();\n\t\treturn now - ObjectUtil.defaultIfNull(this.groupMap.put(id, now), now);\n\t}\n\n\t//----------------------------------------------------------- Interval\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数）<br>\n\t * 如果使用纳秒计时，返回纳秒差，否则返回毫秒差<br>\n\t * 如果分组下没有开始时间，返回{@code null}\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic long interval(String id) {\n\t\tfinal Long lastTime = this.groupMap.get(id);\n\t\tif (null == lastTime) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn getTime() - lastTime;\n\t}\n\n\t/**\n\t * 从开始到当前的间隔时间\n\t *\n\t * @param id       分组ID\n\t * @param dateUnit 时间单位\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic long interval(String id, DateUnit dateUnit) {\n\t\tfinal long intervalMs = isNano ? interval(id) / 1000000L : interval(id);\n\t\tif (DateUnit.MS == dateUnit) {\n\t\t\treturn intervalMs;\n\t\t}\n\t\treturn intervalMs / dateUnit.getMillis();\n\t}\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数）\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic long intervalMs(String id) {\n\t\treturn interval(id, DateUnit.MS);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔秒数，取绝对值\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔秒数，取绝对值\n\t */\n\tpublic long intervalSecond(String id) {\n\t\treturn interval(id, DateUnit.SECOND);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔分钟数，取绝对值\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔分钟数，取绝对值\n\t */\n\tpublic long intervalMinute(String id) {\n\t\treturn interval(id, DateUnit.MINUTE);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔小时数，取绝对值\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔小时数，取绝对值\n\t */\n\tpublic long intervalHour(String id) {\n\t\treturn interval(id, DateUnit.HOUR);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔天数，取绝对值\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔天数，取绝对值\n\t */\n\tpublic long intervalDay(String id) {\n\t\treturn interval(id, DateUnit.DAY);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔周数，取绝对值\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔周数，取绝对值\n\t */\n\tpublic long intervalWeek(String id) {\n\t\treturn interval(id, DateUnit.WEEK);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数），返回XX天XX小时XX分XX秒XX毫秒\n\t *\n\t * @param id 分组ID\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic String intervalPretty(String id) {\n\t\treturn DateUtil.formatBetween(intervalMs(id));\n\t}\n\n\t/**\n\t * 获取时间的毫秒或纳秒数，纳秒非时间戳\n\t *\n\t * @return 时间\n\t */\n\tprivate long getTime() {\n\t\treturn this.isNano ? System.nanoTime() : System.currentTimeMillis();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.*;\nimport java.time.chrono.ChronoLocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeFormatterBuilder;\nimport java.time.temporal.*;\nimport java.util.Date;\nimport java.util.TimeZone;\n\n/**\n * JDK8+中的{@link LocalDateTime}工具类封装\n *\n * @author looly\n * @see DatePattern 常用格式工具类\n * @since 5.3.9\n */\npublic class LocalDateTimeUtil {\n\n\t/**\n\t * 当前时间，默认时区\n\t *\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime now() {\n\t\treturn LocalDateTime.now();\n\t}\n\n\t/**\n\t * {@link Instant}转{@link LocalDateTime}，使用默认时区\n\t *\n\t * @param instant {@link Instant}\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(Instant instant) {\n\t\treturn of(instant, ZoneId.systemDefault());\n\t}\n\n\t/**\n\t * {@link Instant}转{@link LocalDateTime}，使用UTC时区\n\t *\n\t * @param instant {@link Instant}\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime ofUTC(Instant instant) {\n\t\treturn of(instant, ZoneId.of(\"UTC\"));\n\t}\n\n\t/**\n\t * {@link ZonedDateTime}转{@link LocalDateTime}\n\t *\n\t * @param zonedDateTime {@link ZonedDateTime}\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(ZonedDateTime zonedDateTime) {\n\t\tif (null == zonedDateTime) {\n\t\t\treturn null;\n\t\t}\n\t\treturn zonedDateTime.toLocalDateTime();\n\t}\n\n\t/**\n\t * {@link Instant}转{@link LocalDateTime}\n\t *\n\t * @param instant {@link Instant}\n\t * @param zoneId  时区\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(Instant instant, ZoneId zoneId) {\n\t\tif (null == instant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn LocalDateTime.ofInstant(instant, ObjectUtil.defaultIfNull(zoneId, ZoneId::systemDefault));\n\t}\n\n\t/**\n\t * {@link Instant}转{@link LocalDateTime}\n\t *\n\t * @param instant  {@link Instant}\n\t * @param timeZone 时区\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(Instant instant, TimeZone timeZone) {\n\t\tif (null == instant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn of(instant, ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault).toZoneId());\n\t}\n\n\t/**\n\t * 毫秒转{@link LocalDateTime}，使用默认时区\n\t *\n\t * <p>注意：此方法使用默认时区，如果非UTC，会产生时间偏移</p>\n\t *\n\t * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(long epochMilli) {\n\t\treturn of(Instant.ofEpochMilli(epochMilli));\n\t}\n\n\t/**\n\t * 毫秒转{@link LocalDateTime}，使用UTC时区\n\t *\n\t * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime ofUTC(long epochMilli) {\n\t\treturn ofUTC(Instant.ofEpochMilli(epochMilli));\n\t}\n\n\t/**\n\t * 毫秒转{@link LocalDateTime}，根据时区不同，结果会产生时间偏移\n\t *\n\t * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数\n\t * @param zoneId     时区\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(long epochMilli, ZoneId zoneId) {\n\t\treturn of(Instant.ofEpochMilli(epochMilli), zoneId);\n\t}\n\n\t/**\n\t * 毫秒转{@link LocalDateTime}，结果会产生时间偏移\n\t *\n\t * @param epochMilli 从1970-01-01T00:00:00Z开始计数的毫秒数\n\t * @param timeZone   时区\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(long epochMilli, TimeZone timeZone) {\n\t\treturn of(Instant.ofEpochMilli(epochMilli), timeZone);\n\t}\n\n\t/**\n\t * {@link Date}转{@link LocalDateTime}，使用默认时区\n\t *\n\t * @param date Date对象\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(Date date) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (date instanceof DateTime) {\n\t\t\treturn of(date.toInstant(), ((DateTime) date).getZoneId());\n\t\t}\n\t\treturn of(date.toInstant());\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}转{@link LocalDateTime}，使用默认时区\n\t *\n\t * @param temporalAccessor {@link TemporalAccessor}\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime of(TemporalAccessor temporalAccessor) {\n\t\tif (null == temporalAccessor) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (temporalAccessor instanceof LocalDate) {\n\t\t\treturn ((LocalDate) temporalAccessor).atStartOfDay();\n\t\t} else if(temporalAccessor instanceof Instant){\n\t\t\treturn LocalDateTime.ofInstant((Instant) temporalAccessor, ZoneId.systemDefault());\n\t\t}\n\n\t\t// issue#3301\n\t\ttry{\n\t\t\treturn LocalDateTime.from(temporalAccessor);\n\t\t} catch (final Exception ignore){\n\t\t\t//ignore\n\t\t}\n\n\t\ttry{\n\t\t\treturn ZonedDateTime.from(temporalAccessor).toLocalDateTime();\n\t\t} catch (final Exception ignore){\n\t\t\t//ignore\n\t\t}\n\n\t\ttry{\n\t\t\treturn LocalDateTime.ofInstant(Instant.from(temporalAccessor), ZoneId.systemDefault());\n\t\t} catch (final Exception ignore){\n\t\t\t//ignore\n\t\t}\n\n\t\treturn LocalDateTime.of(\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.MONTH_OF_YEAR),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.DAY_OF_MONTH),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.HOUR_OF_DAY),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.MINUTE_OF_HOUR),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.SECOND_OF_MINUTE),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.NANO_OF_SECOND)\n\t\t);\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}转{@link LocalDate}，使用默认时区\n\t *\n\t * @param temporalAccessor {@link TemporalAccessor}\n\t * @return {@link LocalDate}\n\t * @since 5.3.10\n\t */\n\tpublic static LocalDate ofDate(TemporalAccessor temporalAccessor) {\n\t\tif (null == temporalAccessor) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (temporalAccessor instanceof LocalDateTime) {\n\t\t\treturn ((LocalDateTime) temporalAccessor).toLocalDate();\n\t\t} else if(temporalAccessor instanceof Instant){\n\t\t\treturn of(temporalAccessor).toLocalDate();\n\t\t}\n\n\t\treturn LocalDate.of(\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.YEAR),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.MONTH_OF_YEAR),\n\t\t\t\tTemporalAccessorUtil.get(temporalAccessor, ChronoField.DAY_OF_MONTH)\n\t\t);\n\t}\n\n\t/**\n\t * 解析日期时间字符串为{@link LocalDateTime}，仅支持yyyy-MM-dd'T'HH:mm:ss格式，例如：2007-12-03T10:15:30<br>\n\t * 即{@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}\n\t *\n\t * @param text 日期时间字符串\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime parse(CharSequence text) {\n\t\treturn parse(text, (DateTimeFormatter) null);\n\t}\n\n\t/**\n\t * 解析日期时间字符串为{@link LocalDateTime}，格式支持日期时间、日期、时间<br>\n\t * 如果formatter为{@code null}，则使用{@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}\n\t *\n\t * @param text      日期时间字符串\n\t * @param formatter 日期格式化器，预定义的格式见：{@link DateTimeFormatter}\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) {\n\t\tif (StrUtil.isBlank(text)) {\n\t\t\treturn null;\n\t\t}\n\t\tif (null == formatter) {\n\t\t\treturn LocalDateTime.parse(text);\n\t\t}\n\n\t\treturn of(formatter.parse(text));\n\t}\n\n\t/**\n\t * 解析日期时间字符串为{@link LocalDateTime}\n\t *\n\t * @param text   日期时间字符串\n\t * @param format 日期格式，类似于yyyy-MM-dd HH:mm:ss,SSS\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDateTime parse(CharSequence text, String format) {\n\t\tif (StrUtil.isBlank(text)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (GlobalCustomFormat.isCustomFormat(format)) {\n\t\t\treturn of(GlobalCustomFormat.parse(text, format));\n\t\t}\n\n\t\tDateTimeFormatter formatter = null;\n\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\t// 修复yyyyMMddHHmmssSSS格式不能解析的问题\n\t\t\t// fix issue#1082\n\t\t\t//see https://stackoverflow.com/questions/22588051/is-java-time-failing-to-parse-fraction-of-second\n\t\t\t// jdk8 bug at: https://bugs.openjdk.java.net/browse/JDK-8031085\n\t\t\tif (StrUtil.startWithIgnoreEquals(format, DatePattern.PURE_DATETIME_PATTERN)) {\n\t\t\t\tfinal String fraction = StrUtil.removePrefix(format, DatePattern.PURE_DATETIME_PATTERN);\n\t\t\t\tif (ReUtil.isMatch(\"[S]{1,2}\", fraction)) {\n\t\t\t\t\t//将yyyyMMddHHmmssS、yyyyMMddHHmmssSS的日期统一替换为yyyyMMddHHmmssSSS格式，用0补\n\t\t\t\t\ttext += StrUtil.repeat('0', 3 - fraction.length());\n\t\t\t\t}\n\t\t\t\tformatter = new DateTimeFormatterBuilder()\n\t\t\t\t\t\t.appendPattern(DatePattern.PURE_DATETIME_PATTERN)\n\t\t\t\t\t\t.appendValue(ChronoField.MILLI_OF_SECOND, 3)\n\t\t\t\t\t\t.toFormatter();\n\t\t\t} else {\n\t\t\t\tformatter = DateTimeFormatter.ofPattern(format);\n\t\t\t}\n\t\t}\n\n\t\treturn parse(text, formatter);\n\t}\n\n\t/**\n\t * 解析日期时间字符串为{@link LocalDate}，仅支持yyyy-MM-dd格式，例如：2007-12-03\n\t *\n\t * @param text 日期时间字符串\n\t * @return {@link LocalDate}\n\t * @since 5.3.10\n\t */\n\tpublic static LocalDate parseDate(CharSequence text) {\n\t\treturn parseDate(text, (DateTimeFormatter) null);\n\t}\n\n\t/**\n\t * 解析日期时间字符串为{@link LocalDate}，格式支持日期\n\t *\n\t * @param text      日期时间字符串\n\t * @param formatter 日期格式化器，预定义的格式见：{@link DateTimeFormatter}\n\t * @return {@link LocalDate}\n\t * @since 5.3.10\n\t */\n\tpublic static LocalDate parseDate(CharSequence text, DateTimeFormatter formatter) {\n\t\tif (StrUtil.isBlank(text)) {\n\t\t\treturn null;\n\t\t}\n\t\tif (null == formatter) {\n\t\t\treturn LocalDate.parse(text);\n\t\t}\n\n\t\treturn ofDate(formatter.parse(text));\n\t}\n\n\t/**\n\t * 解析日期字符串为{@link LocalDate}\n\t *\n\t * @param text   日期字符串\n\t * @param format 日期格式，类似于yyyy-MM-dd\n\t * @return {@link LocalDateTime}\n\t */\n\tpublic static LocalDate parseDate(CharSequence text, String format) {\n\t\tif (null == text) {\n\t\t\treturn null;\n\t\t}\n\t\treturn parseDate(text, DateTimeFormatter.ofPattern(format));\n\t}\n\n\t/**\n\t * 格式化日期时间为yyyy-MM-dd HH:mm:ss格式\n\t *\n\t * @param time {@link LocalDateTime}\n\t * @return 格式化后的字符串\n\t * @since 5.3.11\n\t */\n\tpublic static String formatNormal(LocalDateTime time) {\n\t\treturn format(time, DatePattern.NORM_DATETIME_FORMATTER);\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式\n\t *\n\t * @param time      {@link LocalDateTime}\n\t * @param formatter 日期格式化器，预定义的格式见：{@link DateTimeFormatter}\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(LocalDateTime time, DateTimeFormatter formatter) {\n\t\treturn TemporalAccessorUtil.format(time, formatter);\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式\n\t *\n\t * @param time   {@link LocalDateTime}\n\t * @param format 日期格式，类似于yyyy-MM-dd HH:mm:ss,SSS\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(LocalDateTime time, String format) {\n\t\treturn TemporalAccessorUtil.format(time, format);\n\t}\n\n\t/**\n\t * 格式化日期时间为yyyy-MM-dd格式\n\t *\n\t * @param date {@link LocalDate}\n\t * @return 格式化后的字符串\n\t * @since 5.3.11\n\t */\n\tpublic static String formatNormal(LocalDate date) {\n\t\treturn format(date, DatePattern.NORM_DATE_FORMATTER);\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式\n\t *\n\t * @param date      {@link LocalDate}\n\t * @param formatter 日期格式化器，预定义的格式见：{@link DateTimeFormatter}; 常量如： {@link DatePattern#NORM_DATE_FORMATTER}, {@link DatePattern#NORM_DATETIME_FORMATTER}\n\t * @return 格式化后的字符串\n\t * @since 5.3.10\n\t */\n\tpublic static String format(LocalDate date, DateTimeFormatter formatter) {\n\t\treturn TemporalAccessorUtil.format(date, formatter);\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式\n\t *\n\t * @param date   {@link LocalDate}\n\t * @param format 日期格式，类似于yyyy-MM-dd, 常量如 {@link DatePattern#NORM_DATE_PATTERN}, {@link DatePattern#NORM_DATETIME_PATTERN}\n\t * @return 格式化后的字符串\n\t * @since 5.3.10\n\t */\n\tpublic static String format(LocalDate date, String format) {\n\t\tif (null == date) {\n\t\t\treturn null;\n\t\t}\n\t\treturn format(date, DateTimeFormatter.ofPattern(format));\n\t}\n\n\t/**\n\t * 日期偏移,根据field不同加不同值（偏移会修改传入的对象）\n\t *\n\t * @param time   {@link LocalDateTime}\n\t * @param number 偏移量，正数为向后偏移，负数为向前偏移\n\t * @param field  偏移单位，见{@link ChronoUnit}，不能为null\n\t * @return 偏移后的日期时间\n\t */\n\tpublic static LocalDateTime offset(LocalDateTime time, long number, TemporalUnit field) {\n\t\treturn TemporalUtil.offset(time, number, field);\n\t}\n\n\t/**\n\t * 获取两个日期的差，如果结束时间早于开始时间，获取结果为负。\n\t * <p>\n\t * 返回结果为{@link Duration}对象，通过调用toXXX方法返回相差单位\n\t *\n\t * @param startTimeInclude 开始时间（包含）\n\t * @param endTimeExclude   结束时间（不包含）\n\t * @return 时间差 {@link Duration}对象\n\t * @see TemporalUtil#between(Temporal, Temporal)\n\t */\n\tpublic static Duration between(LocalDateTime startTimeInclude, LocalDateTime endTimeExclude) {\n\t\treturn TemporalUtil.between(startTimeInclude, endTimeExclude);\n\t}\n\n\t/**\n\t * 获取两个日期的差，如果结束时间早于开始时间，获取结果为负。\n\t * <p>\n\t * 返回结果为时间差的long值\n\t *\n\t * @param startTimeInclude 开始时间（包括）\n\t * @param endTimeExclude   结束时间（不包括）\n\t * @param unit             时间差单位\n\t * @return 时间差\n\t * @since 5.4.5\n\t */\n\tpublic static long between(LocalDateTime startTimeInclude, LocalDateTime endTimeExclude, ChronoUnit unit) {\n\t\treturn TemporalUtil.between(startTimeInclude, endTimeExclude, unit);\n\t}\n\n\t/**\n\t * 获取两个日期的表象时间差，如果结束时间早于开始时间，获取结果为负。\n\t * <p>\n\t * 比如2011年2月1日，和2021年8月11日，日相差了10天，月相差6月\n\t *\n\t * @param startTimeInclude 开始时间（包括）\n\t * @param endTimeExclude   结束时间（不包括）\n\t * @return 时间差\n\t * @since 5.4.5\n\t */\n\tpublic static Period betweenPeriod(LocalDate startTimeInclude, LocalDate endTimeExclude) {\n\t\treturn Period.between(startTimeInclude, endTimeExclude);\n\t}\n\n\t/**\n\t * 修改为一天的开始时间，例如：2020-02-02 00:00:00,000\n\t *\n\t * @param time 日期时间\n\t * @return 一天的开始时间\n\t */\n\tpublic static LocalDateTime beginOfDay(LocalDateTime time) {\n\t\treturn time.with(LocalTime.MIN);\n\t}\n\n\t/**\n\t * 修改为一天的开始时间，例如：2020-02-02 00:00:00,000\n\t *\n\t * @param date 日期时间\n\t * @return 一天的开始时间\n\t * @since 5.8.28\n\t */\n\tpublic static LocalDateTime beginOfDay(LocalDate date) {\n\t\treturn LocalDateTime.of(date, LocalTime.MIN);\n\t}\n\n\t/**\n\t * 修改为一天的结束时间，例如：2020-02-02 23:59:59,999\n\t *\n\t * @param time 日期时间\n\t * @return 一天的结束时间\n\t */\n\tpublic static LocalDateTime endOfDay(LocalDateTime time) {\n\t\treturn endOfDay(time, false);\n\t}\n\n\t/**\n\t * 修改为一天的结束时间，例如：2020-02-02 23:59:59,999\n\t *\n\t * @param date 日期时间\n\t * @return 一天的结束时间\n\t * @since 5.8.28\n\t */\n\tpublic static LocalDateTime endOfDay(LocalDate date) {\n\t\treturn endOfDay(date, false);\n\t}\n\n\t/**\n\t * 修改为一天的结束时间，例如：\n\t * <ul>\n\t * \t<li>毫秒不归零：2020-02-02 23:59:59,999</li>\n\t * \t<li>毫秒归零：2020-02-02 23:59:59,000</li>\n\t * </ul>\n\t *\n\t * @param time                日期时间\n\t * @param truncateMillisecond 是否毫秒归零\n\t * @return 一天的结束时间\n\t * @since 5.7.18\n\t */\n\tpublic static LocalDateTime endOfDay(LocalDateTime time, boolean truncateMillisecond) {\n\t\tif (truncateMillisecond) {\n\t\t\treturn time.with(LocalTime.of(23, 59, 59));\n\t\t}\n\t\treturn time.with(LocalTime.MAX);\n\t}\n\n\t/**\n\t * 修改为一天的结束时间，例如：\n\t * <ul>\n\t * \t<li>毫秒不归零：2020-02-02 23:59:59,999</li>\n\t * \t<li>毫秒归零：2020-02-02 23:59:59,000</li>\n\t * </ul>\n\t *\n\t * @param date                日期时间\n\t * @param truncateMillisecond 是否毫秒归零\n\t * @return 一天的结束时间\n\t * @since 5.7.18\n\t */\n\tpublic static LocalDateTime endOfDay(LocalDate date, boolean truncateMillisecond) {\n\t\tif (truncateMillisecond) {\n\t\t\treturn LocalDateTime.of(date, LocalTime.of(23, 59, 59));\n\t\t}\n\t\treturn LocalDateTime.of(date, LocalTime.MAX);\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}转换为 时间戳（从1970-01-01T00:00:00Z开始的毫秒数）\n\t *\n\t * @param temporalAccessor Date对象\n\t * @return {@link Instant}对象\n\t * @see TemporalAccessorUtil#toEpochMilli(TemporalAccessor)\n\t * @since 5.4.1\n\t */\n\tpublic static long toEpochMilli(TemporalAccessor temporalAccessor) {\n\t\treturn TemporalAccessorUtil.toEpochMilli(temporalAccessor);\n\t}\n\n\t/**\n\t * 是否为周末（周六或周日）\n\t *\n\t * @param localDateTime 判定的日期{@link LocalDateTime}\n\t * @return 是否为周末（周六或周日）\n\t * @since 5.7.6\n\t */\n\tpublic static boolean isWeekend(LocalDateTime localDateTime) {\n\t\treturn isWeekend(localDateTime.toLocalDate());\n\t}\n\n\t/**\n\t * 是否为周末（周六或周日）\n\t *\n\t * @param localDate 判定的日期{@link LocalDate}\n\t * @return 是否为周末（周六或周日）\n\t * @since 5.7.6\n\t */\n\tpublic static boolean isWeekend(LocalDate localDate) {\n\t\tfinal DayOfWeek dayOfWeek = localDate.getDayOfWeek();\n\t\treturn DayOfWeek.SATURDAY == dayOfWeek || DayOfWeek.SUNDAY == dayOfWeek;\n\t}\n\n\t/**\n\t * 获取{@link LocalDate}对应的星期值\n\t *\n\t * @param localDate 日期{@link LocalDate}\n\t * @return {@link Week}\n\t * @since 5.7.14\n\t */\n\tpublic static Week dayOfWeek(LocalDate localDate) {\n\t\treturn Week.of(localDate.getDayOfWeek());\n\t}\n\n\t/**\n\t * 检查两个时间段是否有时间重叠<br>\n\t * 重叠指两个时间段是否有交集，注意此方法时间段重合时如：\n\t * <ul>\n\t *     <li>此方法未纠正开始时间小于结束时间</li>\n\t *     <li>当realStartTime和realEndTime或startTime和endTime相等时,退化为判断区间是否包含点</li>\n\t *     <li>当realStartTime和realEndTime和startTime和endTime相等时,退化为判断点与点是否相等</li>\n\t * </ul>\n\t * See <a href=\"https://www.ics.uci.edu/~alspaugh/cls/shr/allen.html\">准确的区间关系参考:艾伦区间代数</a>\n\t * @param realStartTime 第一个时间段的开始时间\n\t * @param realEndTime   第一个时间段的结束时间\n\t * @param startTime     第二个时间段的开始时间\n\t * @param endTime       第二个时间段的结束时间\n\t * @return true 表示时间有重合或包含或相等\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isOverlap(ChronoLocalDateTime<?> realStartTime, ChronoLocalDateTime<?> realEndTime,\n\t\t\t\t\t\t\t\t\tChronoLocalDateTime<?> startTime, ChronoLocalDateTime<?> endTime) {\n\n\t\t// x>b||a>y 无交集\n\t\t// 则有交集的逻辑为 !(x>b||a>y)\n\t\t// 根据德摩根公式，可化简为 x<=b && a<=y 即 realStartTime<=endTime && startTime<=realEndTime\n\t\treturn realStartTime.compareTo(endTime) <=0 && startTime.compareTo(realEndTime) <= 0;\n\t}\n\n\t/**\n\t * 获得指定日期是所在年份的第几周，如：\n\t * <ul>\n\t *     <li>如果一年的第一天是星期一，则第一周从第一天开始，没有零周</li>\n\t *     <li>如果一年的第二天是星期一，则第一周从第二天开始，而第一天在零周</li>\n\t *     <li>如果一年中的第4天是星期一，则第1周从第4周开始，第1至第3周在零周开始</li>\n\t *     <li>如果一年中的第5天是星期一，则第二周从第5周开始，第1至第4周在第1周</li>\n\t * </ul>\n\t *\n\t *\n\t * @param date 日期（{@link LocalDate} 或者 {@link LocalDateTime}等）\n\t * @return 所在年的第几周\n\t * @since 5.7.21\n\t */\n\tpublic static int weekOfYear(TemporalAccessor date){\n\t\treturn TemporalAccessorUtil.get(date, WeekFields.ISO.weekOfYear());\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一天\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 是否为同一天\n\t * @since 5.8.5\n\t */\n\tpublic static boolean isSameDay(final LocalDateTime date1, final LocalDateTime date2) {\n\t\treturn date1 != null && date2 != null && isSameDay(date1.toLocalDate(), date2.toLocalDate());\n\t}\n\n\t/**\n\t * 比较两个日期是否为同一天\n\t *\n\t * @param date1 日期1\n\t * @param date2 日期2\n\t * @return 是否为同一天\n\t * @since 5.8.5\n\t */\n\tpublic static boolean isSameDay(final LocalDate date1, final LocalDate date2) {\n\t\treturn date1 != null && date2 != null && date1.isEqual(date2);\n\t}\n\n\t/**\n\t * 当前日期是否在日期指定范围内<br>\n\t * 起始日期和结束日期可以互换\n\t *\n\t * @param date      被检查的日期\n\t * @param beginDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 是否在范围内\n\t * @since 5.8.5\n\t */\n\tpublic static boolean isIn(ChronoLocalDateTime<?> date, ChronoLocalDateTime<?> beginDate, ChronoLocalDateTime<?> endDate) {\n\t\treturn TemporalAccessorUtil.isIn(date, beginDate, endDate);\n\t}\n\n\t/**\n\t * 判断当前时间（默认时区）是否在指定范围内<br>\n\t * 起始时间和结束时间可以互换<br>\n\t * 通过includeBegin, includeEnd参数控制时间范围区间是否为开区间，例如：传入参数：includeBegin=true, includeEnd=false，\n\t * 则本方法会判断 date ∈ (beginDate, endDate] 是否成立\n\t *\n\t * @param date 被判定的日期\n\t * @param beginDate    起始时间（包含）\n\t * @param endDate      结束时间（包含）\n\t * @param includeBegin 时间范围是否包含起始时间\n\t * @param includeEnd   时间范围是否包含结束时间\n\t * @return 是否在范围内\n\t * @author FengBaoheng\n\t * @since 5.8.6\n\t */\n\tpublic static boolean isIn(ChronoLocalDateTime<?> date, ChronoLocalDateTime<?> beginDate,\n\t\t\t\t\t\t\t   ChronoLocalDateTime<?> endDate, boolean includeBegin, boolean includeEnd) {\n\t\treturn TemporalAccessorUtil.isIn(date, beginDate, endDate, includeBegin, includeEnd);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/Month.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.time.format.TextStyle;\nimport java.util.Calendar;\nimport java.util.Locale;\n\n/**\n * 月份枚举<br>\n * 与Calendar中的月份int值对应\n *\n * @author Looly\n * @see Calendar#JANUARY\n * @see Calendar#FEBRUARY\n * @see Calendar#MARCH\n * @see Calendar#APRIL\n * @see Calendar#MAY\n * @see Calendar#JUNE\n * @see Calendar#JULY\n * @see Calendar#AUGUST\n * @see Calendar#SEPTEMBER\n * @see Calendar#OCTOBER\n * @see Calendar#NOVEMBER\n * @see Calendar#DECEMBER\n * @see Calendar#UNDECIMBER\n */\npublic enum Month {\n\t/**\n\t * 一月\n\t */\n\tJANUARY(Calendar.JANUARY),\n\t/**\n\t * 二月\n\t */\n\tFEBRUARY(Calendar.FEBRUARY),\n\t/**\n\t * 三月\n\t */\n\tMARCH(Calendar.MARCH),\n\t/**\n\t * 四月\n\t */\n\tAPRIL(Calendar.APRIL),\n\t/**\n\t * 五月\n\t */\n\tMAY(Calendar.MAY),\n\t/**\n\t * 六月\n\t */\n\tJUNE(Calendar.JUNE),\n\t/**\n\t * 七月\n\t */\n\tJULY(Calendar.JULY),\n\t/**\n\t * 八月\n\t */\n\tAUGUST(Calendar.AUGUST),\n\t/**\n\t * 九月\n\t */\n\tSEPTEMBER(Calendar.SEPTEMBER),\n\t/**\n\t * 十月\n\t */\n\tOCTOBER(Calendar.OCTOBER),\n\t/**\n\t * 十一月\n\t */\n\tNOVEMBER(Calendar.NOVEMBER),\n\t/**\n\t * 十二月\n\t */\n\tDECEMBER(Calendar.DECEMBER),\n\t/**\n\t * 十三月，仅用于农历\n\t */\n\tUNDECIMBER(Calendar.UNDECIMBER);\n\n\t// ---------------------------------------------------------------\n\t/**\n\t * Months aliases.\n\t */\n\tprivate static final String[] ALIASES = {\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"};\n\tprivate static final Month[] ENUMS = Month.values();\n\n\t/**\n\t * 对应值，见{@link Calendar}\n\t */\n\tprivate final int value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 对应值，见{@link Calendar}\n\t */\n\tMonth(int value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取{@link Calendar}中的对应值<br>\n\t * 此值从0开始，即0表示一月\n\t *\n\t * @return {@link Calendar}中的对应月份值，从0开始计数\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 获取月份值，此值与{@link java.time.Month}对应<br>\n\t * 此值从1开始，即1表示一月\n\t *\n\t * @return 月份值，对应{@link java.time.Month}，从1开始计数\n\t * @since 5.7.21\n\t */\n\tpublic int getValueBaseOne() {\n\t\tAssert.isFalse(this == UNDECIMBER, \"Unsupported UNDECIMBER Field\");\n\t\treturn getValue() + 1;\n\t}\n\n\t/**\n\t * 获取此月份最后一天的值，不支持的月份（例如UNDECIMBER）返回-1\n\t *\n\t * @param isLeapYear 是否闰年\n\t * @return 此月份最后一天的值\n\t */\n\tpublic int getLastDay(boolean isLeapYear) {\n\t\tswitch (this) {\n\t\t\tcase FEBRUARY:\n\t\t\t\treturn isLeapYear ? 29 : 28;\n\t\t\tcase APRIL:\n\t\t\tcase JUNE:\n\t\t\tcase SEPTEMBER:\n\t\t\tcase NOVEMBER:\n\t\t\t\treturn 30;\n\t\t\tdefault:\n\t\t\t\treturn 31;\n\t\t}\n\t}\n\n\t/**\n\t * 将 {@link Calendar}月份相关值转换为Month枚举对象<br>\n\t * 未找到返回{@code null}\n\t *\n\t * @param calendarMonthIntValue Calendar中关于Month的int值，从0开始\n\t * @return Month\n\t * @see Calendar#JANUARY\n\t * @see Calendar#FEBRUARY\n\t * @see Calendar#MARCH\n\t * @see Calendar#APRIL\n\t * @see Calendar#MAY\n\t * @see Calendar#JUNE\n\t * @see Calendar#JULY\n\t * @see Calendar#AUGUST\n\t * @see Calendar#SEPTEMBER\n\t * @see Calendar#OCTOBER\n\t * @see Calendar#NOVEMBER\n\t * @see Calendar#DECEMBER\n\t * @see Calendar#UNDECIMBER\n\t */\n\tpublic static Month of(int calendarMonthIntValue) {\n\t\tif (calendarMonthIntValue >= ENUMS.length || calendarMonthIntValue < 0) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ENUMS[calendarMonthIntValue];\n\t}\n\n\t/**\n\t * 解析别名为Month对象，别名如：jan或者JANUARY，不区分大小写\n\t *\n\t * @param name 别名值\n\t * @return 月份枚举Month，非空\n\t * @throws IllegalArgumentException 如果别名无对应的枚举，抛出此异常\n\t * @since 5.8.0\n\t */\n\tpublic static Month of(String name) throws IllegalArgumentException {\n\t\tAssert.notBlank(name);\n\t\tMonth of = of(ArrayUtil.indexOfIgnoreCase(ALIASES, name));\n\t\tif (null == of) {\n\t\t\tof = Month.valueOf(name.toUpperCase());\n\t\t}\n\t\treturn of;\n\t}\n\n\t/**\n\t * {@link java.time.Month}转换为Month对象\n\t * @param month {@link java.time.Month}\n\t * @return Month\n\t * @since 5.8.0\n\t */\n\tpublic static Month of(java.time.Month month){\n\t\treturn of(month.ordinal());\n\t}\n\n\t/**\n\t * 获得指定月的最后一天\n\t *\n\t * @param month      月份，从0开始\n\t * @param isLeapYear 是否为闰年，闰年只对二月有影响\n\t * @return 最后一天，可能为28,29,30,31\n\t * @since 5.4.7\n\t */\n\tpublic static int getLastDay(int month, boolean isLeapYear) {\n\t\tfinal Month of = of(month);\n\t\tAssert.notNull(of, \"Invalid Month base 0: \" + month);\n\t\treturn of.getLastDay(isLeapYear);\n\t}\n\n\t/**\n\t * 转换为{@link java.time.Month}\n\t *\n\t * @return {@link java.time.Month}\n\t * @since 5.7.21\n\t */\n\tpublic java.time.Month toJdkMonth() {\n\t\treturn java.time.Month.of(getValueBaseOne());\n\t}\n\n\t/**\n\t * 获取显示名称\n\t *\n\t * @param style 名称风格\n\t * @return 显示名称\n\t * @since 5.8.0\n\t */\n\tpublic String getDisplayName(TextStyle style) {\n\t\treturn getDisplayName(style, Locale.getDefault());\n\t}\n\n\t/**\n\t * 获取显示名称\n\t *\n\t * @param style  名称风格\n\t * @param locale {@link Locale}\n\t * @return 显示名称\n\t * @since 5.8.0\n\t */\n\tpublic String getDisplayName(TextStyle style, Locale locale) {\n\t\treturn toJdkMonth().getDisplayName(style, locale);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/Quarter.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.time.DateTimeException;\nimport java.time.MonthDay;\nimport java.time.temporal.ChronoField;\n\n/**\n * 季度枚举\n *\n * @see #Q1\n * @see #Q2\n * @see #Q3\n * @see #Q4\n *\n * @author zhfish(https://github.com/zhfish)\n *\n */\npublic enum Quarter {\n\n\t/** 第一季度 */\n\tQ1(1),\n\t/** 第二季度 */\n\tQ2(2),\n\t/** 第三季度 */\n\tQ3(3),\n\t/** 第四季度 */\n\tQ4(4);\n\n\t// ---------------------------------------------------------------\n\tprivate final int value;\n\n\tprivate final int firstMonth;\n\tprivate final int lastMonth;\n\n\tQuarter(int value) {\n\t\tthis.value = value;\n\n\t\tthis.lastMonth = value * 3;\n\t\tthis.firstMonth = lastMonth - 2;\n\t}\n\n\t/**\n\t * 获取季度值\n\t *\n\t * @return 季度值\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 将 季度int转换为Season枚举对象<br>\n\t *\n\t * @see #Q1\n\t * @see #Q2\n\t * @see #Q3\n\t * @see #Q4\n\t *\n\t * @param intValue 季度int表示\n\t * @return {@code Quarter}\n\t */\n\tpublic static Quarter of(int intValue) {\n\t\tswitch (intValue) {\n\t\t\tcase 1:\n\t\t\t\treturn Q1;\n\t\t\tcase 2:\n\t\t\t\treturn Q2;\n\t\t\tcase 3:\n\t\t\t\treturn Q3;\n\t\t\tcase 4:\n\t\t\t\treturn Q4;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 根据给定的月份值返回对应的季度\n\t *\n\t * @param monthValue 月份值，取值范围为1到12\n\t * @return 对应的季度\n\t * @throws IllegalArgumentException 如果月份值不在有效范围内（1到12），将抛出异常\n\t */\n\tpublic static Quarter fromMonth(int monthValue) {\n\t\tChronoField.MONTH_OF_YEAR.checkValidValue(monthValue);\n\t\treturn of(computeQuarterValueInternal(monthValue));\n\t}\n\n\t/**\n\t * 根据给定的月份返回对应的季度\n\t *\n\t * @param month 月份\n\t * @return 对应的季度\n\t */\n\tpublic static Quarter fromMonth(Month month) {\n\t\tAssert.notNull(month);\n\t\tfinal int monthValue = month.getValueBaseOne();\n\t\treturn of(computeQuarterValueInternal(monthValue));\n\t}\n\n\t/**\n\t * 根据指定的年份，获取一个新的 YearQuarter 实例\n\t * 此方法允许在保持当前季度信息不变的情况下，更改年份\n\t *\n\t * @param year 指定的年份\n\t * @return 返回一个新的 YearQuarter 实例，年份更新为指定的年份\n\t */\n\tpublic final YearQuarter atYear(int year) {\n\t\treturn YearQuarter.of(year, this);\n\t}\n\n\t// StaticFactoryMethods end\n\n\t// computes\n\n\t/**\n\t * 加上指定数量的季度\n\t *\n\t * @param quarters 所添加的季度数量\n\t * @return 计算结果\n\t */\n\tpublic Quarter plus(int quarters) {\n\t\tfinal int amount = (quarters % 4) + 4;\n\t\treturn Quarter.values()[(ordinal() + amount) % 4];\n\t}\n\n\t// computes end\n\n\t// Getters\n\n\t/**\n\t * 该季度的第一个月\n\t *\n\t * @return 结果\n\t */\n\tpublic Month firstMonth() {\n\t\treturn Month.of(firstMonthValue() - 1);\n\t}\n\n\t/**\n\t * 该季度的第一个月\n\t *\n\t * @return 结果。月份值从 1 开始，1 表示 1月，以此类推。\n\t */\n\tpublic int firstMonthValue() {\n\t\treturn this.firstMonth;\n\t}\n\n\t/**\n\t * 该季度最后一个月\n\t *\n\t * @return 结果\n\t */\n\tpublic Month lastMonth() {\n\t\treturn Month.of(lastMonthValue() - 1);\n\t}\n\n\t/**\n\t * 该季度最后一个月\n\t *\n\t * @return 结果。1 表示 1月，以此类推。\n\t */\n\tpublic int lastMonthValue() {\n\t\treturn this.lastMonth;\n\t}\n\n\t/**\n\t * 该季度的第一天\n\t *\n\t * @return 结果\n\t */\n\tpublic MonthDay firstMonthDay() {\n\t\treturn MonthDay.of(firstMonthValue(), 1);\n\t}\n\n\t/**\n\t * 该季度的最后一天\n\t *\n\t * @return 结果\n\t */\n\tpublic MonthDay lastMonthDay() {\n\t\t// 季度的最后一个月不可能是 2 月，不考虑闰年\n\t\tfinal Month month = lastMonth();\n\t\treturn MonthDay.of(month.toJdkMonth(), month.getLastDay(false));\n\t}\n\n\t// Getters end\n\n\t/**\n\t * 检查季度的值。（1~4）\n\t * @param value 季度值\n\t * @return 在取值范围内的值\n\t */\n\tpublic static int checkValidIntValue(int value) {\n\t\tAssert.isTrue(value >= 1 && value <= 4,\n\t\t\t\t() -> new DateTimeException(\"Invalid value for Quarter: \" + value));\n\t\treturn value;\n\t}\n\n\t// Internal\n\n\t/**\n\t * 计算给定月份对应的季度值\n\t *\n\t * @param monthValue 月份值，取值范围为1到12\n\t * @return 对应的季度值\n\t */\n\tprivate static int computeQuarterValueInternal(int monthValue) {\n\t\treturn (monthValue - 1) / 3 + 1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/StopWatch.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.text.NumberFormat;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 秒表封装<br>\n * 此工具用于存储一组任务的耗时时间，并一次性打印对比。<br>\n * 比如：我们可以记录多段代码耗时时间，然后一次性打印（StopWatch提供了一个prettyString()函数用于按照指定格式打印出耗时）\n *\n * <p>\n * 此工具来自：https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/util/StopWatch.java\n *\n * <p>\n * 使用方法如下：\n *\n * <pre>\n * StopWatch stopWatch = new StopWatch(\"任务名称\");\n *\n * // 任务1\n * stopWatch.start(\"任务一\");\n * Thread.sleep(1000);\n * stopWatch.stop();\n *\n * // 任务2\n * stopWatch.start(\"任务二\");\n * Thread.sleep(2000);\n * stopWatch.stop();\n *\n * // 打印出耗时\n * Console.log(stopWatch.prettyPrint());\n *\n * </pre>\n *\n * @author Spring Framework, Looly\n * @since 4.6.6\n */\npublic class StopWatch {\n\n\t/**\n\t * 创建计时任务（秒表）\n\t *\n\t * @param id 用于标识秒表的唯一ID\n\t * @return StopWatch\n\t * @since 5.5.2\n\t */\n\tpublic static StopWatch create(String id) {\n\t\treturn new StopWatch(id);\n\t}\n\n\t/**\n\t * 秒表唯一标识，用于多个秒表对象的区分\n\t */\n\tprivate final String id;\n\tprivate List<TaskInfo> taskList;\n\n\t/**\n\t * 任务名称\n\t */\n\tprivate String currentTaskName;\n\t/**\n\t * 开始时间\n\t */\n\tprivate long startTimeNanos;\n\n\t/**\n\t * 最后一次任务对象\n\t */\n\tprivate TaskInfo lastTaskInfo;\n\t/**\n\t * 总任务数\n\t */\n\tprivate int taskCount;\n\t/**\n\t * 总运行时间\n\t */\n\tprivate long totalTimeNanos;\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，不启动任何任务\n\t */\n\tpublic StopWatch() {\n\t\tthis(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 构造，不启动任何任务\n\t *\n\t * @param id 用于标识秒表的唯一ID\n\t */\n\tpublic StopWatch(String id) {\n\t\tthis(id, true);\n\t}\n\n\t/**\n\t * 构造，不启动任何任务\n\t *\n\t * @param id           用于标识秒表的唯一ID\n\t * @param keepTaskList 是否在停止后保留任务，{@code false} 表示停止运行后不保留任务\n\t */\n\tpublic StopWatch(String id, boolean keepTaskList) {\n\t\tthis.id = id;\n\t\tif (keepTaskList) {\n\t\t\tthis.taskList = new ArrayList<>();\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获取StopWatch 的ID，用于多个秒表对象的区分\n\t *\n\t * @return the ID 默认为空字符串\n\t * @see #StopWatch(String)\n\t */\n\tpublic String getId() {\n\t\treturn this.id;\n\t}\n\n\t/**\n\t * 设置是否在停止后保留任务，{@code false} 表示停止运行后不保留任务\n\t *\n\t * @param keepTaskList 是否在停止后保留任务\n\t */\n\tpublic void setKeepTaskList(boolean keepTaskList) {\n\t\tif (keepTaskList) {\n\t\t\tif (null == this.taskList) {\n\t\t\t\tthis.taskList = new ArrayList<>();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.taskList = null;\n\t\t}\n\t}\n\n\t/**\n\t * 开始默认的新任务\n\t *\n\t * @throws IllegalStateException 前一个任务没有结束\n\t */\n\tpublic void start() throws IllegalStateException {\n\t\tstart(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 开始指定名称的新任务\n\t *\n\t * @param taskName 新开始的任务名称\n\t * @throws IllegalStateException 前一个任务没有结束\n\t */\n\tpublic void start(String taskName) throws IllegalStateException {\n\t\tif (null != this.currentTaskName) {\n\t\t\tthrow new IllegalStateException(\"Can't start StopWatch: it's already running\");\n\t\t}\n\t\tthis.currentTaskName = taskName;\n\t\tthis.startTimeNanos = System.nanoTime();\n\t}\n\n\t/**\n\t * 停止当前任务\n\t *\n\t * @throws IllegalStateException 任务没有开始\n\t */\n\tpublic void stop() throws IllegalStateException {\n\t\tif (null == this.currentTaskName) {\n\t\t\tthrow new IllegalStateException(\"Can't stop StopWatch: it's not running\");\n\t\t}\n\n\t\tfinal long lastTime = System.nanoTime() - this.startTimeNanos;\n\t\tfinal long safeLastTime = Math.max(0, lastTime);\n\t\tthis.totalTimeNanos += safeLastTime;\n\t\tthis.lastTaskInfo = new TaskInfo(this.currentTaskName, safeLastTime);\n\t\tif (null != this.taskList) {\n\t\t\tthis.taskList.add(this.lastTaskInfo);\n\t\t}\n\t\t++this.taskCount;\n\t\tthis.currentTaskName = null;\n\t}\n\n\t/**\n\t * 检查是否有正在运行的任务\n\t *\n\t * @return 是否有正在运行的任务\n\t * @see #currentTaskName()\n\t */\n\tpublic boolean isRunning() {\n\t\treturn (this.currentTaskName != null);\n\t}\n\n\t/**\n\t * 获取当前任务名，{@code null} 表示无任务\n\t *\n\t * @return 当前任务名，{@code null} 表示无任务\n\t * @see #isRunning()\n\t */\n\tpublic String currentTaskName() {\n\t\treturn this.currentTaskName;\n\t}\n\n\t/**\n\t * 获取最后任务的花费时间（纳秒）\n\t *\n\t * @return 任务的花费时间（纳秒）\n\t * @throws IllegalStateException 无任务\n\t */\n\tpublic long getLastTaskTimeNanos() throws IllegalStateException {\n\t\tif (this.lastTaskInfo == null) {\n\t\t\tthrow new IllegalStateException(\"No tasks run: can't get last task interval\");\n\t\t}\n\t\treturn this.lastTaskInfo.getTimeNanos();\n\t}\n\n\t/**\n\t * 获取最后任务的花费时间（毫秒）\n\t *\n\t * @return 任务的花费时间（毫秒）\n\t * @throws IllegalStateException 无任务\n\t */\n\tpublic long getLastTaskTimeMillis() throws IllegalStateException {\n\t\tif (this.lastTaskInfo == null) {\n\t\t\tthrow new IllegalStateException(\"No tasks run: can't get last task interval\");\n\t\t}\n\t\treturn this.lastTaskInfo.getTimeMillis();\n\t}\n\n\t/**\n\t * 获取最后的任务名\n\t *\n\t * @return 任务名\n\t * @throws IllegalStateException 无任务\n\t */\n\tpublic String getLastTaskName() throws IllegalStateException {\n\t\tif (this.lastTaskInfo == null) {\n\t\t\tthrow new IllegalStateException(\"No tasks run: can't get last task name\");\n\t\t}\n\t\treturn this.lastTaskInfo.getTaskName();\n\t}\n\n\t/**\n\t * 获取最后的任务对象\n\t *\n\t * @return {@link TaskInfo} 任务对象，包括任务名和花费时间\n\t * @throws IllegalStateException 无任务\n\t */\n\tpublic TaskInfo getLastTaskInfo() throws IllegalStateException {\n\t\tif (this.lastTaskInfo == null) {\n\t\t\tthrow new IllegalStateException(\"No tasks run: can't get last task info\");\n\t\t}\n\t\treturn this.lastTaskInfo;\n\t}\n\n\t/**\n\t * 获取所有任务的总花费时间\n\t *\n\t * @param unit 时间单位，{@code null}表示默认{@link TimeUnit#NANOSECONDS}\n\t * @return 花费时间\n\t * @since 5.7.16\n\t */\n\tpublic long getTotal(TimeUnit unit){\n\t\treturn unit.convert(this.totalTimeNanos, TimeUnit.NANOSECONDS);\n\t}\n\n\t/**\n\t * 获取所有任务的总花费时间（纳秒）\n\t *\n\t * @return 所有任务的总花费时间（纳秒）\n\t * @see #getTotalTimeMillis()\n\t * @see #getTotalTimeSeconds()\n\t */\n\tpublic long getTotalTimeNanos() {\n\t\treturn this.totalTimeNanos;\n\t}\n\n\t/**\n\t * 获取所有任务的总花费时间（毫秒）\n\t *\n\t * @return 所有任务的总花费时间（毫秒）\n\t * @see #getTotalTimeNanos()\n\t * @see #getTotalTimeSeconds()\n\t */\n\tpublic long getTotalTimeMillis() {\n\t\treturn getTotal(TimeUnit.MILLISECONDS);\n\t}\n\n\t/**\n\t * 获取所有任务的总花费时间（秒）\n\t *\n\t * @return 所有任务的总花费时间（秒）\n\t * @see #getTotalTimeNanos()\n\t * @see #getTotalTimeMillis()\n\t */\n\tpublic double getTotalTimeSeconds() {\n\t\treturn DateUtil.nanosToSeconds(this.totalTimeNanos);\n\t}\n\n\t/**\n\t * 获取任务数\n\t *\n\t * @return 任务数\n\t */\n\tpublic int getTaskCount() {\n\t\treturn this.taskCount;\n\t}\n\n\t/**\n\t * 获取任务列表\n\t *\n\t * @return 任务列表\n\t */\n\tpublic TaskInfo[] getTaskInfo() {\n\t\tif (null == this.taskList) {\n\t\t\tthrow new UnsupportedOperationException(\"Task info is not being kept!\");\n\t\t}\n\t\treturn this.taskList.toArray(new TaskInfo[0]);\n\t}\n\n\t/**\n\t * 获取任务信息，类似于：\n\t * <pre>\n\t *     StopWatch '[id]': running time = [total] ns\n\t * </pre>\n\t *\n\t * @return 任务信息\n\t */\n\tpublic String shortSummary() {\n\t\treturn shortSummary(null);\n\t}\n\n\t/**\n\t * 获取任务信息，类似于：\n\t * <pre>\n\t *     StopWatch '[id]': running time = [total] [unit]\n\t * </pre>\n\t *\n\t * @param unit 时间单位，{@code null}则默认为{@link TimeUnit#NANOSECONDS}\n\t * @return 任务信息\n\t */\n\tpublic String shortSummary(TimeUnit unit) {\n\t\tif(null == unit){\n\t\t\tunit = TimeUnit.NANOSECONDS;\n\t\t}\n\t\treturn StrUtil.format(\"StopWatch '{}': running time = {} {}\",\n\t\t\t\tthis.id, getTotal(unit), DateUtil.getShotName(unit));\n\t}\n\n\t/**\n\t * 生成所有任务的一个任务花费时间表，单位纳秒\n\t *\n\t * @return 任务时间表\n\t */\n\tpublic String prettyPrint() {\n\t\treturn  prettyPrint(null);\n\t}\n\n\t/**\n\t * 生成所有任务的一个任务花费时间表\n\t *\n\t * @param unit 时间单位，{@code null}则默认{@link TimeUnit#NANOSECONDS} 纳秒\n\t * @return 任务时间表\n\t * @since 5.7.16\n\t */\n\tpublic String prettyPrint(TimeUnit unit) {\n\t\tif (null == unit) {\n\t\t\tunit = TimeUnit.NANOSECONDS;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder(shortSummary(unit));\n\t\tsb.append(FileUtil.getLineSeparator());\n\t\tif (null == this.taskList) {\n\t\t\tsb.append(\"No task info kept\");\n\t\t} else {\n\t\t\tsb.append(\"---------------------------------------------\").append(FileUtil.getLineSeparator());\n\t\t\tsb.append(DateUtil.getShotName(unit)).append(\"         %     Task name\").append(FileUtil.getLineSeparator());\n\t\t\tsb.append(\"---------------------------------------------\").append(FileUtil.getLineSeparator());\n\n\t\t\tfinal NumberFormat nf = NumberFormat.getNumberInstance();\n\t\t\tnf.setMinimumIntegerDigits(9);\n\t\t\tnf.setGroupingUsed(false);\n\n\t\t\tfinal NumberFormat pf = NumberFormat.getPercentInstance();\n\t\t\tpf.setMinimumIntegerDigits(2);\n\t\t\tpf.setGroupingUsed(false);\n\n\t\t\tfor (TaskInfo task : getTaskInfo()) {\n\t\t\t\tsb.append(nf.format(task.getTime(unit))).append(\"  \");\n\t\t\t\tsb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(\"   \");\n\t\t\t\tsb.append(task.getTaskName()).append(FileUtil.getLineSeparator());\n\t\t\t}\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder sb = new StringBuilder(shortSummary());\n\t\tif (null != this.taskList) {\n\t\t\tfor (TaskInfo task : this.taskList) {\n\t\t\t\tsb.append(\"; [\").append(task.getTaskName()).append(\"] took \").append(task.getTimeNanos()).append(\" ns\");\n\t\t\t\tlong percent = Math.round(100.0 * task.getTimeNanos() / getTotalTimeNanos());\n\t\t\t\tsb.append(\" = \").append(percent).append(\"%\");\n\t\t\t}\n\t\t} else {\n\t\t\tsb.append(\"; no task info kept\");\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 存放任务名称和花费时间对象\n\t *\n\t * @author Looly\n\t */\n\tpublic static final class TaskInfo {\n\n\t\tprivate final String taskName;\n\t\tprivate final long timeNanos;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param taskName  任务名称\n\t\t * @param timeNanos 花费时间（纳秒）\n\t\t */\n\t\tTaskInfo(String taskName, long timeNanos) {\n\t\t\tthis.taskName = taskName;\n\t\t\tthis.timeNanos = timeNanos;\n\t\t}\n\n\t\t/**\n\t\t * 获取任务名\n\t\t *\n\t\t * @return 任务名\n\t\t */\n\t\tpublic String getTaskName() {\n\t\t\treturn this.taskName;\n\t\t}\n\n\t\t/**\n\t\t * 获取指定单位的任务花费时间\n\t\t *\n\t\t * @param unit 单位\n\t\t * @return 任务花费时间\n\t\t * @since 5.7.16\n\t\t */\n\t\tpublic long getTime(TimeUnit unit) {\n\t\t\treturn unit.convert(this.timeNanos, TimeUnit.NANOSECONDS);\n\t\t}\n\n\t\t/**\n\t\t * 获取任务花费时间（单位：纳秒）\n\t\t *\n\t\t * @return 任务花费时间（单位：纳秒）\n\t\t * @see #getTimeMillis()\n\t\t * @see #getTimeSeconds()\n\t\t */\n\t\tpublic long getTimeNanos() {\n\t\t\treturn this.timeNanos;\n\t\t}\n\n\t\t/**\n\t\t * 获取任务花费时间（单位：毫秒）\n\t\t *\n\t\t * @return 任务花费时间（单位：毫秒）\n\t\t * @see #getTimeNanos()\n\t\t * @see #getTimeSeconds()\n\t\t */\n\t\tpublic long getTimeMillis() {\n\t\t\treturn getTime(TimeUnit.MILLISECONDS);\n\t\t}\n\n\t\t/**\n\t\t * 获取任务花费时间（单位：秒）\n\t\t *\n\t\t * @return 任务花费时间（单位：秒）\n\t\t * @see #getTimeMillis()\n\t\t * @see #getTimeNanos()\n\t\t */\n\t\tpublic double getTimeSeconds() {\n\t\t\treturn DateUtil.nanosToSeconds(this.timeNanos);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/SystemClock.java",
    "content": "package cn.hutool.core.date;\n\nimport java.sql.Timestamp;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 系统时钟<br>\n * 高并发场景下System.currentTimeMillis()的性能问题的优化\n * System.currentTimeMillis()的调用比new一个普通对象要耗时的多（具体耗时高出多少我还没测试过，有人说是100倍左右）\n * System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道\n * 后台定时更新时钟，JVM退出时，线程自动回收\n *\n * see： http://git.oschina.net/yu120/sequence\n * @author lry,looly\n */\npublic class SystemClock {\n\n\t/** 时钟更新间隔，单位毫秒 */\n\tprivate final long period;\n\t/** 现在时刻的毫秒数 */\n\tprivate volatile long now;\n\n\t/**\n\t * 构造\n\t * @param period 时钟更新间隔，单位毫秒\n\t */\n\tpublic SystemClock(long period) {\n\t\tthis.period = period;\n\t\tthis.now = System.currentTimeMillis();\n\t\tscheduleClockUpdating();\n\t}\n\n\t/**\n\t * 开启计时器线程\n\t */\n\tprivate void scheduleClockUpdating() {\n\t\tScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {\n\t\t\tThread thread = new Thread(runnable, \"System Clock\");\n\t\t\tthread.setDaemon(true);\n\t\t\treturn thread;\n\t\t});\n\t\tscheduler.scheduleAtFixedRate(() -> now = System.currentTimeMillis(), period, period, TimeUnit.MILLISECONDS);\n\t}\n\n\t/**\n\t * @return 当前时间毫秒数\n\t */\n\tprivate long currentTimeMillis() {\n\t\treturn now;\n\t}\n\n\t//------------------------------------------------------------------------ static\n\t/**\n\t * 单例\n\t * @author Looly\n\t *\n\t */\n\tprivate static class InstanceHolder {\n\t\tpublic static final SystemClock INSTANCE = new SystemClock(1);\n\t}\n\n\t/**\n\t * @return 当前时间\n\t */\n\tpublic static long now() {\n\t\treturn InstanceHolder.INSTANCE.currentTimeMillis();\n\t}\n\n\t/**\n\t * @return 当前时间字符串表现形式\n\t */\n\tpublic static String nowDate() {\n\t\treturn new Timestamp(InstanceHolder.INSTANCE.currentTimeMillis()).toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.DayOfWeek;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.Month;\nimport java.time.MonthDay;\nimport java.time.OffsetDateTime;\nimport java.time.OffsetTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.chrono.Era;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.TemporalAccessor;\nimport java.time.temporal.TemporalField;\nimport java.time.temporal.UnsupportedTemporalTypeException;\n\n/**\n * {@link TemporalAccessor} 工具类封装\n *\n * @author looly\n * @since 5.3.9\n */\npublic class TemporalAccessorUtil extends TemporalUtil{\n\n\t/**\n\t * 安全获取时间的某个属性，属性不存在返回最小值，一般为0<br>\n\t * 注意请谨慎使用此方法，某些{@link TemporalAccessor#isSupported(TemporalField)}为{@code false}的方法返回最小值\n\t *\n\t * @param temporalAccessor 需要获取的时间对象\n\t * @param field            需要获取的属性\n\t * @return 时间的值，如果无法获取则获取最小值，一般为0\n\t */\n\tpublic static int get(TemporalAccessor temporalAccessor, TemporalField field) {\n\t\tif (temporalAccessor.isSupported(field)) {\n\t\t\treturn temporalAccessor.get(field);\n\t\t}\n\n\t\treturn (int)field.range().getMinimum();\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式<br>\n\t * 如果为{@link Month}，调用{@link Month#toString()}\n\t *\n\t * @param time      {@link TemporalAccessor}\n\t * @param formatter 日期格式化器，预定义的格式见：{@link DateTimeFormatter}\n\t * @return 格式化后的字符串\n\t * @since 5.3.10\n\t */\n\tpublic static String format(TemporalAccessor time, DateTimeFormatter formatter) {\n\t\tif (null == time) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif(time instanceof Month){\n\t\t\treturn time.toString();\n\t\t}\n\n\t\tif(null == formatter){\n\t\t\tformatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;\n\t\t}\n\n\t\ttry {\n\t\t\treturn formatter.format(time);\n\t\t} catch (UnsupportedTemporalTypeException e){\n\t\t\tif(time instanceof LocalDate && e.getMessage().contains(\"HourOfDay\")){\n\t\t\t\t// 用户传入LocalDate，但是要求格式化带有时间部分，转换为LocalDateTime重试\n\t\t\t\treturn formatter.format(((LocalDate) time).atStartOfDay());\n\t\t\t}else if(time instanceof LocalTime && e.getMessage().contains(\"YearOfEra\")){\n\t\t\t\t// 用户传入LocalTime，但是要求格式化带有日期部分，转换为LocalDateTime重试\n\t\t\t\treturn formatter.format(((LocalTime) time).atDate(LocalDate.now()));\n\t\t\t} else if(time instanceof Instant){\n\t\t\t\t// 时间戳没有时区信息，赋予默认时区\n\t\t\t\treturn formatter.format(((Instant) time).atZone(ZoneId.systemDefault()));\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * 格式化日期时间为指定格式<br>\n\t * 如果为{@link Month}，调用{@link Month#toString()}\n\t *\n\t * @param time      {@link TemporalAccessor}\n\t * @param format 日期格式\n\t * @return 格式化后的字符串\n\t * @since 5.3.10\n\t */\n\tpublic static String format(TemporalAccessor time, String format) {\n\t\tif (null == time) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif(time instanceof DayOfWeek || time instanceof java.time.Month || time instanceof Era || time instanceof MonthDay){\n\t\t\treturn time.toString();\n\t\t}\n\n\t\t// 检查自定义格式\n\t\tif(GlobalCustomFormat.isCustomFormat(format)){\n\t\t\treturn GlobalCustomFormat.format(time, format);\n\t\t}\n\n\t\tfinal DateTimeFormatter formatter = StrUtil.isBlank(format)\n\t\t\t\t? null : DateTimeFormatter.ofPattern(format);\n\n\t\treturn format(time, formatter);\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}转换为 时间戳（从1970-01-01T00:00:00Z开始的毫秒数）<br>\n\t * 如果为{@link Month}，调用{@link Month#getValue()}\n\t *\n\t * @param temporalAccessor Date对象\n\t * @return {@link Instant}对象\n\t * @since 5.4.1\n\t */\n\tpublic static long toEpochMilli(TemporalAccessor temporalAccessor) {\n\t\tif(temporalAccessor instanceof Month){\n\t\t\treturn ((Month) temporalAccessor).getValue();\n\t\t} else if(temporalAccessor instanceof DayOfWeek){\n\t\t\treturn ((DayOfWeek) temporalAccessor).getValue();\n\t\t} else if(temporalAccessor instanceof Era){\n\t\t\treturn ((Era) temporalAccessor).getValue();\n\t\t}\n\t\treturn toInstant(temporalAccessor).toEpochMilli();\n\t}\n\n\t/**\n\t * {@link TemporalAccessor}转换为 {@link Instant}对象\n\t *\n\t * @param temporalAccessor Date对象\n\t * @return {@link Instant}对象\n\t * @since 5.3.10\n\t */\n\tpublic static Instant toInstant(TemporalAccessor temporalAccessor) {\n\t\tif (null == temporalAccessor) {\n\t\t\treturn null;\n\t\t}\n\n\t\tInstant result;\n\t\tif (temporalAccessor instanceof Instant) {\n\t\t\tresult = (Instant) temporalAccessor;\n\t\t} else if (temporalAccessor instanceof LocalDateTime) {\n\t\t\tresult = ((LocalDateTime) temporalAccessor).atZone(ZoneId.systemDefault()).toInstant();\n\t\t} else if (temporalAccessor instanceof ZonedDateTime) {\n\t\t\tresult = ((ZonedDateTime) temporalAccessor).toInstant();\n\t\t} else if (temporalAccessor instanceof OffsetDateTime) {\n\t\t\tresult = ((OffsetDateTime) temporalAccessor).toInstant();\n\t\t} else if (temporalAccessor instanceof LocalDate) {\n\t\t\tresult = ((LocalDate) temporalAccessor).atStartOfDay(ZoneId.systemDefault()).toInstant();\n\t\t} else if (temporalAccessor instanceof LocalTime) {\n\t\t\t// 指定本地时间转换 为Instant，取当天日期\n\t\t\tresult = ((LocalTime) temporalAccessor).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant();\n\t\t} else if (temporalAccessor instanceof OffsetTime) {\n\t\t\t// 指定本地时间转换 为Instant，取当天日期\n\t\t\tresult = ((OffsetTime) temporalAccessor).atDate(LocalDate.now()).toInstant();\n\t\t} else {\n\t\t\t// issue#1891@Github\n\t\t\t// Instant.from不能完成日期转换\n\t\t\t//result = Instant.from(temporalAccessor);\n\t\t\tresult = toInstant(LocalDateTimeUtil.of(temporalAccessor));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 当前日期是否在日期指定范围内<br>\n\t * 起始日期和结束日期可以互换\n\t *\n\t * @param date      被检查的日期\n\t * @param beginDate 起始日期（包含）\n\t * @param endDate   结束日期（包含）\n\t * @return 是否在范围内\n\t * @since 5.8.5\n\t */\n\tpublic static boolean isIn(TemporalAccessor date, TemporalAccessor beginDate, TemporalAccessor endDate) {\n\t\treturn isIn(date, beginDate, endDate, true, true);\n\t}\n\n\t/**\n\t * 当前日期是否在日期指定范围内<br>\n\t * 起始日期和结束日期可以互换<br>\n\t * 通过includeBegin, includeEnd参数控制日期范围区间是否为开区间，例如：传入参数：includeBegin=true, includeEnd=false，\n\t * 则本方法会判断 date ∈ (beginDate, endDate] 是否成立\n\t *\n\t * @param date         被检查的日期\n\t * @param beginDate    起始日期\n\t * @param endDate      结束日期\n\t * @param includeBegin 时间范围是否包含起始日期\n\t * @param includeEnd   时间范围是否包含结束日期\n\t * @return 是否在范围内\n\t * @author FengBaoheng\n\t * @since 5.8.6\n\t */\n\tpublic static boolean isIn(TemporalAccessor date, TemporalAccessor beginDate, TemporalAccessor endDate,\n\t\t\t\t\t\t\t   boolean includeBegin, boolean includeEnd) {\n\t\tif (date == null || beginDate == null || endDate == null) {\n\t\t\tthrow new IllegalArgumentException(\"参数不可为null\");\n\t\t}\n\n\t\tfinal long thisMills = toEpochMilli(date);\n\t\tfinal long beginMills = toEpochMilli(beginDate);\n\t\tfinal long endMills = toEpochMilli(endDate);\n\t\tfinal long rangeMin = Math.min(beginMills, endMills);\n\t\tfinal long rangeMax = Math.max(beginMills, endMills);\n\n\t\t// 先判断是否满足 date ∈ (beginDate, endDate)\n\t\tboolean isIn = rangeMin < thisMills && thisMills < rangeMax;\n\n\t\t// 若不满足，则再判断是否在时间范围的边界上\n\t\tif (!isIn && includeBegin) {\n\t\t\tisIn = thisMills == rangeMin;\n\t\t}\n\n\t\tif (!isIn && includeEnd) {\n\t\t\tisIn = thisMills == rangeMax;\n\t\t}\n\n\t\treturn isIn;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/TemporalUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport java.time.DayOfWeek;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.time.temporal.Temporal;\nimport java.time.temporal.TemporalAdjusters;\nimport java.time.temporal.TemporalUnit;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * {@link Temporal} 工具类封装\n *\n * @author looly\n * @since 5.4.5\n */\npublic class TemporalUtil {\n\n\t/**\n\t * 获取两个日期的差，如果结束时间早于开始时间，获取结果为负。\n\t * <p>\n\t * 返回结果为{@link Duration}对象，通过调用toXXX方法返回相差单位\n\t *\n\t * @param startTimeInclude 开始时间（包含）\n\t * @param endTimeExclude   结束时间（不包含）\n\t * @return 时间差 {@link Duration}对象\n\t */\n\tpublic static Duration between(Temporal startTimeInclude, Temporal endTimeExclude) {\n\t\treturn Duration.between(startTimeInclude, endTimeExclude);\n\t}\n\n\t/**\n\t * 获取两个日期的差，如果结束时间早于开始时间，获取结果为负。\n\t * <p>\n\t * 返回结果为时间差的long值\n\t *\n\t * @param startTimeInclude 开始时间（包括）\n\t * @param endTimeExclude   结束时间（不包括）\n\t * @param unit             时间差单位\n\t * @return 时间差\n\t */\n\tpublic static long between(Temporal startTimeInclude, Temporal endTimeExclude, ChronoUnit unit) {\n\t\treturn unit.between(startTimeInclude, endTimeExclude);\n\t}\n\n\t/**\n\t * 将 {@link TimeUnit} 转换为 {@link ChronoUnit}.\n\t *\n\t * @param unit 被转换的{@link TimeUnit}单位，如果为{@code null}返回{@code null}\n\t * @return {@link ChronoUnit}\n\t * @since 5.7.16\n\t */\n\tpublic static ChronoUnit toChronoUnit(TimeUnit unit) throws IllegalArgumentException {\n\t\tif (null == unit) {\n\t\t\treturn null;\n\t\t}\n\t\tswitch (unit) {\n\t\t\tcase NANOSECONDS:\n\t\t\t\treturn ChronoUnit.NANOS;\n\t\t\tcase MICROSECONDS:\n\t\t\t\treturn ChronoUnit.MICROS;\n\t\t\tcase MILLISECONDS:\n\t\t\t\treturn ChronoUnit.MILLIS;\n\t\t\tcase SECONDS:\n\t\t\t\treturn ChronoUnit.SECONDS;\n\t\t\tcase MINUTES:\n\t\t\t\treturn ChronoUnit.MINUTES;\n\t\t\tcase HOURS:\n\t\t\t\treturn ChronoUnit.HOURS;\n\t\t\tcase DAYS:\n\t\t\t\treturn ChronoUnit.DAYS;\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalArgumentException(\"Unknown TimeUnit constant\");\n\t\t}\n\t}\n\n\t/**\n\t * 转换 {@link ChronoUnit} 到 {@link TimeUnit}.\n\t *\n\t * @param unit {@link ChronoUnit}，如果为{@code null}返回{@code null}\n\t * @return {@link TimeUnit}\n\t * @throws IllegalArgumentException 如果{@link TimeUnit}没有对应单位抛出\n\t * @since 5.7.16\n\t */\n\tpublic static TimeUnit toTimeUnit(ChronoUnit unit) throws IllegalArgumentException {\n\t\tif (null == unit) {\n\t\t\treturn null;\n\t\t}\n\t\tswitch (unit) {\n\t\t\tcase NANOS:\n\t\t\t\treturn TimeUnit.NANOSECONDS;\n\t\t\tcase MICROS:\n\t\t\t\treturn TimeUnit.MICROSECONDS;\n\t\t\tcase MILLIS:\n\t\t\t\treturn TimeUnit.MILLISECONDS;\n\t\t\tcase SECONDS:\n\t\t\t\treturn TimeUnit.SECONDS;\n\t\t\tcase MINUTES:\n\t\t\t\treturn TimeUnit.MINUTES;\n\t\t\tcase HOURS:\n\t\t\t\treturn TimeUnit.HOURS;\n\t\t\tcase DAYS:\n\t\t\t\treturn TimeUnit.DAYS;\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalArgumentException(\"ChronoUnit cannot be converted to TimeUnit: \" + unit);\n\t\t}\n\t}\n\n\t/**\n\t * 日期偏移,根据field不同加不同值（偏移会修改传入的对象）\n\t *\n\t * @param <T>    日期类型，如LocalDate或LocalDateTime\n\t * @param time   {@link Temporal}\n\t * @param number 偏移量，正数为向后偏移，负数为向前偏移\n\t * @param field  偏移单位，见{@link ChronoUnit}，不能为null\n\t * @return 偏移后的日期时间\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Temporal> T offset(T time, long number, TemporalUnit field) {\n\t\tif (null == time) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn (T) time.plus(number, field);\n\t}\n\n\t/**\n\t * 偏移到指定的周几\n\t *\n\t * @param temporal   日期或者日期时间，为{@code null}返回{@code null}\n\t * @param dayOfWeek  周几\n\t * @param <T>        日期类型，如LocalDate或LocalDateTime\n\t * @param isPrevious 是否向前偏移，{@code true}向前偏移，{@code false}向后偏移。\n\t * @return 偏移后的日期\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Temporal> T offset(T temporal, DayOfWeek dayOfWeek, boolean isPrevious) {\n\t\tif (null == temporal) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (T) temporal.with(isPrevious ? TemporalAdjusters.previous(dayOfWeek) : TemporalAdjusters.next(dayOfWeek));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/TimeInterval.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 计时器<br>\n * 计算某个过程花费的时间，精确到毫秒或纳秒\n *\n * @author Looly\n */\npublic class TimeInterval extends GroupTimeInterval {\n\tprivate static final long serialVersionUID = 1L;\n\tprivate static final String DEFAULT_ID = StrUtil.EMPTY;\n\n\t/**\n\t * 构造，默认使用毫秒计数\n\t */\n\tpublic TimeInterval() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isNano 是否使用纳秒计数，false则使用毫秒\n\t */\n\tpublic TimeInterval(boolean isNano) {\n\t\tsuper(isNano);\n\t\tstart();\n\t}\n\n\t/**\n\t * @return 开始计时并返回当前时间\n\t */\n\tpublic long start() {\n\t\treturn start(DEFAULT_ID);\n\t}\n\n\t/**\n\t * @return 重新计时并返回从开始到当前的持续时间\n\t */\n\tpublic long intervalRestart() {\n\t\treturn intervalRestart(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 重新开始计算时间（重置开始时间）\n\t *\n\t * @return this\n\t * @see #start()\n\t * @since 3.0.1\n\t */\n\tpublic TimeInterval restart() {\n\t\tstart(DEFAULT_ID);\n\t\treturn this;\n\t}\n\n\t//----------------------------------------------------------- Interval\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数）<br>\n\t * 如果使用纳秒计时，返回纳秒差，否则返回毫秒差\n\t *\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic long interval() {\n\t\treturn interval(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数），返回XX天XX小时XX分XX秒XX毫秒\n\t *\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t * @since 4.6.7\n\t */\n\tpublic String intervalPretty() {\n\t\treturn intervalPretty(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔时间（毫秒数）\n\t *\n\t * @return 从开始到当前的间隔时间（毫秒数）\n\t */\n\tpublic long intervalMs() {\n\t\treturn intervalMs(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔秒数，取绝对值\n\t *\n\t * @return 从开始到当前的间隔秒数，取绝对值\n\t */\n\tpublic long intervalSecond() {\n\t\treturn intervalSecond(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔分钟数，取绝对值\n\t *\n\t * @return 从开始到当前的间隔分钟数，取绝对值\n\t */\n\tpublic long intervalMinute() {\n\t\treturn intervalMinute(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔小时数，取绝对值\n\t *\n\t * @return 从开始到当前的间隔小时数，取绝对值\n\t */\n\tpublic long intervalHour() {\n\t\treturn intervalHour(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔天数，取绝对值\n\t *\n\t * @return 从开始到当前的间隔天数，取绝对值\n\t */\n\tpublic long intervalDay() {\n\t\treturn intervalDay(DEFAULT_ID);\n\t}\n\n\t/**\n\t * 从开始到当前的间隔周数，取绝对值\n\t *\n\t * @return 从开始到当前的间隔周数，取绝对值\n\t */\n\tpublic long intervalWeek() {\n\t\treturn intervalWeek(DEFAULT_ID);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/TimeZoneUtil.java",
    "content": "package cn.hutool.core.date;\r\n\r\nimport cn.hutool.core.util.JdkUtil;\r\nimport cn.hutool.core.util.ObjectUtil;\r\nimport cn.hutool.core.util.SystemPropsUtil;\r\n\r\nimport java.time.ZoneId;\r\nimport java.util.TimeZone;\r\n\r\n/**\r\n * 时间时区工具类\r\n *\r\n * @author looly\r\n * @since 5.8.44\r\n */\r\npublic class TimeZoneUtil {\r\n\t/**\r\n\t * A public version of {@link java.util.TimeZone}'s package private {@code GMT_ID} field.\r\n\t */\r\n\tpublic static final String GMT_ID = \"GMT\";\r\n\r\n\t/**\r\n\t * The GMT time zone.\r\n\t */\r\n\tpublic static final TimeZone GMT = getTimeZone(GMT_ID);\r\n\r\n\t/**\r\n\t * Delegates to {@link TimeZone#getTimeZone(String)} after mapping an ID if it's in {@link ZoneId#SHORT_IDS}.\r\n\t * <p>\r\n\t * On Java 25, calling {@link TimeZone#getTimeZone(String)} with an ID in {@link ZoneId#SHORT_IDS} writes a message to {@link System#err} in the form:\r\n\t * </p>\r\n\t *\r\n\t * <pre>\r\n\t * WARNING: Use of the three-letter time zone ID \"the-short-id\" is deprecated and it will be removed in a future release\r\n\t * </pre>\r\n\t * <p>\r\n\t * You can disable mapping from {@link ZoneId#SHORT_IDS} by setting the system property {@code \"TimeZones.mapShortIDs=false\"}.\r\n\t * </p>\r\n\t *\r\n\t * @param id Same as {@link TimeZone#getTimeZone(String)}.\r\n\t * @return Same as {@link TimeZone#getTimeZone(String)}.\r\n\t */\r\n\tpublic static TimeZone getTimeZone(final String id) {\r\n\t\treturn TimeZone.getTimeZone(JdkUtil.IS_AT_LEAST_JDK25 && mapShortIDs() ? ZoneId.SHORT_IDS.getOrDefault(id, id) : id);\r\n\t}\r\n\r\n\tprivate static boolean mapShortIDs() {\r\n\t\treturn SystemPropsUtil.getBoolean(\"TimeZones.mapShortIDs\", true);\r\n\t}\r\n\r\n\t/**\r\n\t * Returns the given TimeZone if non-{@code null}, otherwise {@link TimeZone#getDefault()}.\r\n\t *\r\n\t * @param timeZone a locale or {@code null}.\r\n\t * @return the given locale if non-{@code null}, otherwise {@link TimeZone#getDefault()}.\r\n\t */\r\n\tpublic static TimeZone toTimeZone(final TimeZone timeZone) {\r\n\t\treturn ObjectUtil.defaultIfNull(timeZone, TimeZone::getDefault);\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/Week.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.DayOfWeek;\nimport java.util.Calendar;\n\n/**\n * 星期枚举<br>\n * 与Calendar中的星期int值对应\n *\n * @author Looly\n * @see #SUNDAY\n * @see #MONDAY\n * @see #TUESDAY\n * @see #WEDNESDAY\n * @see #THURSDAY\n * @see #FRIDAY\n * @see #SATURDAY\n */\npublic enum Week {\n\n\t/**\n\t * 周日\n\t */\n\tSUNDAY(Calendar.SUNDAY),\n\t/**\n\t * 周一\n\t */\n\tMONDAY(Calendar.MONDAY),\n\t/**\n\t * 周二\n\t */\n\tTUESDAY(Calendar.TUESDAY),\n\t/**\n\t * 周三\n\t */\n\tWEDNESDAY(Calendar.WEDNESDAY),\n\t/**\n\t * 周四\n\t */\n\tTHURSDAY(Calendar.THURSDAY),\n\t/**\n\t * 周五\n\t */\n\tFRIDAY(Calendar.FRIDAY),\n\t/**\n\t * 周六\n\t */\n\tSATURDAY(Calendar.SATURDAY);\n\n\t// ---------------------------------------------------------------\n\t/**\n\t * Weeks aliases.\n\t */\n\tprivate static final String[] ALIASES = {\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"};\n\tprivate static final Week[] ENUMS = Week.values();\n\n\t/**\n\t * 星期对应{@link Calendar} 中的Week值\n\t */\n\tprivate final int value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 星期对应{@link Calendar} 中的Week值\n\t */\n\tWeek(int value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获得星期对应{@link Calendar} 中的Week值\n\t *\n\t * @return 星期对应 {@link Calendar} 中的Week值\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 获取ISO8601规范的int值，from 1 (Monday) to 7 (Sunday).\n\t *\n\t * @return ISO8601规范的int值\n\t * @since 5.8.0\n\t */\n\tpublic int getIso8601Value() {\n\t\tint iso8601IntValue = getValue() - 1;\n\t\tif (0 == iso8601IntValue) {\n\t\t\tiso8601IntValue = 7;\n\t\t}\n\t\treturn iso8601IntValue;\n\t}\n\n\t/**\n\t * 转换为中文名\n\t *\n\t * @return 星期的中文名\n\t * @since 3.3.0\n\t */\n\tpublic String toChinese() {\n\t\treturn toChinese(\"星期\");\n\t}\n\n\t/**\n\t * 转换为中文名\n\t *\n\t * @param weekNamePre 表示星期的前缀，例如前缀为“星期”，则返回结果为“星期一”；前缀为”周“，结果为“周一”\n\t * @return 星期的中文名\n\t * @since 4.0.11\n\t */\n\tpublic String toChinese(String weekNamePre) {\n\t\tswitch (this) {\n\t\t\tcase SUNDAY:\n\t\t\t\treturn weekNamePre + \"日\";\n\t\t\tcase MONDAY:\n\t\t\t\treturn weekNamePre + \"一\";\n\t\t\tcase TUESDAY:\n\t\t\t\treturn weekNamePre + \"二\";\n\t\t\tcase WEDNESDAY:\n\t\t\t\treturn weekNamePre + \"三\";\n\t\t\tcase THURSDAY:\n\t\t\t\treturn weekNamePre + \"四\";\n\t\t\tcase FRIDAY:\n\t\t\t\treturn weekNamePre + \"五\";\n\t\t\tcase SATURDAY:\n\t\t\t\treturn weekNamePre + \"六\";\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 转换为{@link DayOfWeek}\n\t *\n\t * @return {@link DayOfWeek}\n\t * @since 5.8.0\n\t */\n\tpublic DayOfWeek toJdkDayOfWeek() {\n\t\treturn DayOfWeek.of(getIso8601Value());\n\t}\n\n\t/**\n\t * 将 {@link Calendar}星期相关值转换为Week枚举对象<br>\n\t *\n\t * @param calendarWeekIntValue Calendar中关于Week的int值，1表示Sunday\n\t * @return Week\n\t * @see #SUNDAY\n\t * @see #MONDAY\n\t * @see #TUESDAY\n\t * @see #WEDNESDAY\n\t * @see #THURSDAY\n\t * @see #FRIDAY\n\t * @see #SATURDAY\n\t */\n\tpublic static Week of(int calendarWeekIntValue) {\n\t\tif (calendarWeekIntValue > ENUMS.length || calendarWeekIntValue < 1) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ENUMS[calendarWeekIntValue - 1];\n\t}\n\n\t/**\n\t * 解析别名为Week对象，别名如：sun或者SUNDAY，不区分大小写\n\t *\n\t * @param name 别名值\n\t * @return 周枚举Week，非空\n\t * @throws IllegalArgumentException 如果别名无对应的枚举，抛出此异常\n\t * @since 5.8.0\n\t */\n\tpublic static Week of(String name) throws IllegalArgumentException {\n\t\tAssert.notBlank(name);\n\n\t\t// issue#3637\n\t\tif (StrUtil.startWithAny(name, \"星期\", \"周\")) {\n\t\t\tchar chineseNumber = name.charAt(name.length() - 1);\n\t\t\tswitch (chineseNumber) {\n\t\t\t\tcase '一':\n\t\t\t\t\treturn MONDAY;\n\t\t\t\tcase '二':\n\t\t\t\t\treturn TUESDAY;\n\t\t\t\tcase '三':\n\t\t\t\t\treturn WEDNESDAY;\n\t\t\t\tcase '四':\n\t\t\t\t\treturn THURSDAY;\n\t\t\t\tcase '五':\n\t\t\t\t\treturn FRIDAY;\n\t\t\t\tcase '六':\n\t\t\t\t\treturn SATURDAY;\n\t\t\t\tcase '日':\n\t\t\t\t\treturn SUNDAY;\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Invalid week name: \" + name);\n\t\t}\n\n\t\tWeek of = of(ArrayUtil.indexOfIgnoreCase(ALIASES, name) + 1);\n\t\tif (null == of) {\n\t\t\tof = Week.valueOf(name.toUpperCase());\n\t\t}\n\t\treturn of;\n\t}\n\n\t/**\n\t * 将 {@link DayOfWeek}星期相关值转换为Week枚举对象<br>\n\t *\n\t * @param dayOfWeek DayOfWeek星期值\n\t * @return Week\n\t * @see #SUNDAY\n\t * @see #MONDAY\n\t * @see #TUESDAY\n\t * @see #WEDNESDAY\n\t * @see #THURSDAY\n\t * @see #FRIDAY\n\t * @see #SATURDAY\n\t * @since 5.7.14\n\t */\n\tpublic static Week of(DayOfWeek dayOfWeek) {\n\t\tAssert.notNull(dayOfWeek);\n\t\tint week = dayOfWeek.getValue() + 1;\n\t\tif (8 == week) {\n\t\t\t// 周日\n\t\t\tweek = 1;\n\t\t}\n\t\treturn of(week);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/YearQuarter.java",
    "content": "package cn.hutool.core.date;\n\nimport java.io.Serializable;\nimport java.time.LocalDate;\nimport java.time.YearMonth;\nimport java.time.temporal.ChronoField;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Objects;\n\nimport static java.time.temporal.ChronoField.YEAR;\n\n/**\n * 表示年份与季度\n *\n * @author ZhouXY\n * @since 5.8.37\n */\npublic final class YearQuarter implements Comparable<YearQuarter>, Serializable {\n\tprivate static final long serialVersionUID = 3804145964419489753L;\n\n\t/**\n\t * 年份\n\t */\n\tprivate final int year;\n\t/**\n\t * 季度\n\t */\n\tprivate final Quarter quarter;\n\t/**\n\t * 季度开始日期\n\t */\n\tprivate final LocalDate firstDate;\n\t/**\n\t * 季度结束日期\n\t */\n\tprivate final LocalDate lastDate;\n\n\tprivate YearQuarter(int year, Quarter quarter) {\n\t\tthis.year = year;\n\t\tthis.quarter = quarter;\n\t\tthis.firstDate = quarter.firstMonthDay().atYear(year);\n\t\tthis.lastDate = quarter.lastMonthDay().atYear(year);\n\t}\n\n\t// #region - StaticFactory\n\n\t/**\n\t * 根据指定年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param year    年份\n\t * @param quarter 季度\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(int year, int quarter) {\n\t\tint yearValue = YEAR.checkValidIntValue(year);\n\t\tint quarterValue = Quarter.checkValidIntValue(quarter);\n\t\treturn new YearQuarter(yearValue, Objects.requireNonNull(Quarter.of(quarterValue)));\n\t}\n\n\t/**\n\t * 根据指定年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param year    年份\n\t * @param quarter 季度\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(int year, Quarter quarter) {\n\t\treturn new YearQuarter(YEAR.checkValidIntValue(year), Objects.requireNonNull(quarter));\n\t}\n\n\t/**\n\t * 根据指定日期，判断日期所在的年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param date 日期\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(LocalDate date) {\n\t\tObjects.requireNonNull(date);\n\t\treturn new YearQuarter(date.getYear(), Quarter.fromMonth(date.getMonthValue()));\n\t}\n\n\t/**\n\t * 根据指定日期，判断日期所在的年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param date 日期\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(Date date) {\n\t\tObjects.requireNonNull(date);\n\t\t@SuppressWarnings(\"deprecation\") final int yearValue = YEAR.checkValidIntValue(date.getYear() + 1900L);\n\t\t@SuppressWarnings(\"deprecation\") final int monthValue = date.getMonth() + 1;\n\t\treturn new YearQuarter(yearValue, Quarter.fromMonth(monthValue));\n\t}\n\n\t/**\n\t * 根据指定日期，判断日期所在的年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param date 日期\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(Calendar date) {\n\t\tObjects.requireNonNull(date);\n\t\tfinal int yearValue = ChronoField.YEAR.checkValidIntValue(date.get(Calendar.YEAR));\n\t\tfinal int monthValue = date.get(Calendar.MONTH) + 1;\n\t\treturn new YearQuarter(yearValue, Quarter.fromMonth(monthValue));\n\t}\n\n\t/**\n\t * 根据指定年月，判断其所在的年份与季度，创建 {@code YearQuarter} 实例\n\t *\n\t * @param yearMonth 年月\n\t * @return {@code YearQuarter} 实例\n\t */\n\tpublic static YearQuarter of(YearMonth yearMonth) {\n\t\tObjects.requireNonNull(yearMonth);\n\t\treturn of(yearMonth.getYear(), Quarter.fromMonth(yearMonth.getMonthValue()));\n\t}\n\n\t/**\n\t * 当前年季\n\t *\n\t * @return 当前年季\n\t */\n\n\tpublic static YearQuarter now() {\n\t\treturn of(LocalDate.now());\n\t}\n\n\t// #endregion\n\n\t// #region - Getters\n\n\t/**\n\t * 年份\n\t *\n\t * @return 年份\n\t */\n\tpublic int getYear() {\n\t\treturn this.year;\n\t}\n\n\t/**\n\t * 季度\n\t *\n\t * @return 季度\n\t */\n\tpublic Quarter getQuarter() {\n\t\treturn this.quarter;\n\t}\n\n\t/**\n\t * 季度值。从 1 开始。\n\t *\n\t * @return 季度值\n\t */\n\tpublic int getQuarterValue() {\n\t\treturn this.quarter.getValue();\n\t}\n\n\t/**\n\t * 该季度第一个月\n\t *\n\t * @return {@link YearMonth} 对象\n\t */\n\tpublic YearMonth firstYearMonth() {\n\t\treturn YearMonth.of(this.year, this.quarter.firstMonthValue());\n\t}\n\n\t/**\n\t * 该季度第一个月\n\t *\n\t * @return {@link Month} 对象\n\t */\n\tpublic Month firstMonth() {\n\t\treturn this.quarter.firstMonth();\n\t}\n\n\t/**\n\t * 该季度的第一个月\n\t *\n\t * @return 结果。月份值从 1 开始，1 表示 1月，以此类推。\n\t */\n\tpublic int firstMonthValue() {\n\t\treturn this.quarter.firstMonthValue();\n\t}\n\n\t/**\n\t * 该季度的最后一个月\n\t *\n\t * @return {@link YearMonth} 对象\n\t */\n\tpublic YearMonth lastYearMonth() {\n\t\treturn YearMonth.of(this.year, this.quarter.lastMonthValue());\n\t}\n\n\t/**\n\t * 该季度的最后一个月\n\t *\n\t * @return {@link Month} 对象\n\t */\n\tpublic Month lastMonth() {\n\t\treturn this.quarter.lastMonth();\n\t}\n\n\t/**\n\t * 该季度的最后一个月\n\t *\n\t * @return 结果。月份值从 1 开始，1 表示 1月，以此类推。\n\t */\n\tpublic int lastMonthValue() {\n\t\treturn this.quarter.lastMonthValue();\n\t}\n\n\t/**\n\t * 该季度的第一天\n\t *\n\t * @return {@link LocalDate} 对象\n\t */\n\tpublic LocalDate firstDate() {\n\t\treturn firstDate;\n\t}\n\n\t/**\n\t * 该季度的最后一天\n\t *\n\t * @return {@link LocalDate} 对象\n\t */\n\tpublic LocalDate lastDate() {\n\t\treturn lastDate;\n\t}\n\n\t// #endregion\n\n\t// #region - computes\n\n\t/**\n\t * 添加季度\n\t *\n\t * @param quartersToAdd 要添加的季度数\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter plusQuarters(long quartersToAdd) {\n\t\tif (quartersToAdd == 0L) {\n\t\t\treturn this;\n\t\t}\n\t\tlong quarterCount = this.year * 4L + (this.quarter.getValue() - 1);\n\t\tlong calcQuarters = quarterCount + quartersToAdd; // safe overflow\n\t\tint newYear = YEAR.checkValidIntValue(Math.floorDiv(calcQuarters, 4));\n\t\tint newQuarter = (int) Math.floorMod(calcQuarters, 4) + 1;\n\t\treturn new YearQuarter(newYear, Objects.requireNonNull(Quarter.of(newQuarter)));\n\t}\n\n\t/**\n\t * 减去季度\n\t *\n\t * @param quartersToMinus 要减去的季度数\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter minusQuarters(long quartersToMinus) {\n\t\treturn plusQuarters(-quartersToMinus);\n\t}\n\n\t/**\n\t * 下一个季度\n\t *\n\t * @return 结果\n\t */\n\tpublic YearQuarter nextQuarter() {\n\t\treturn plusQuarters(1L);\n\t}\n\n\t/**\n\t * 上一个季度\n\t *\n\t * @return 结果\n\t */\n\tpublic YearQuarter lastQuarter() {\n\t\treturn minusQuarters(1L);\n\t}\n\n\t/**\n\t * 添加年份\n\t *\n\t * @param yearsToAdd 要添加的年份数\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter plusYears(long yearsToAdd) {\n\t\tif (yearsToAdd == 0L) {\n\t\t\treturn this;\n\t\t}\n\t\tint newYear = YEAR.checkValidIntValue(this.year + yearsToAdd); // safe overflow\n\t\treturn new YearQuarter(newYear, this.quarter);\n\t}\n\n\t/**\n\t * 减去年份\n\t *\n\t * @param yearsToMinus 要减去的年份数\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter minusYears(long yearsToMinus) {\n\t\treturn plusYears(-yearsToMinus);\n\t}\n\n\t/**\n\t * 下一年同季度\n\t *\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter nextYear() {\n\t\treturn plusYears(1L);\n\t}\n\n\t/**\n\t * 上一年同季度\n\t *\n\t * @return 计算结果\n\t */\n\tpublic YearQuarter lastYear() {\n\t\treturn minusYears(1L);\n\t}\n\n\t// #endregion\n\n\t// #region - hashCode & equals\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(year, quarter);\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj)\n\t\t\treturn true;\n\t\tif (obj == null)\n\t\t\treturn false;\n\t\tif (getClass() != obj.getClass())\n\t\t\treturn false;\n\t\tYearQuarter other = (YearQuarter) obj;\n\t\treturn year == other.year && quarter == other.quarter;\n\t}\n\n\t// #endregion\n\n\t// #region - compare\n\n\t@Override\n\tpublic int compareTo(YearQuarter other) {\n\t\tint cmp = (this.year - other.year);\n\t\tif (cmp == 0) {\n\t\t\tcmp = this.quarter.compareTo(other.quarter);\n\t\t}\n\t\treturn cmp;\n\t}\n\n\t/**\n\t * 判断是否在指定年份季度之前\n\t *\n\t * @param other 比较对象\n\t * @return 结果\n\t */\n\tpublic boolean isBefore(YearQuarter other) {\n\t\treturn this.compareTo(other) < 0;\n\t}\n\n\t/**\n\t * 判断是否在指定年份季度之后\n\t *\n\t * @param other 比较对象\n\t * @return 结果\n\t */\n\tpublic boolean isAfter(YearQuarter other) {\n\t\treturn this.compareTo(other) > 0;\n\t}\n\n\t// #endregion\n\n\t// #region - toString\n\n\t/**\n\t * 返回 {@code YearQuarter} 的字符串表示形式，如 \"2024 Q3\"\n\t *\n\t * @return {@code YearQuarter} 的字符串表示形式\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn this.year + \" \" + this.quarter.name();\n\t}\n\n\t// #endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 星座 来自：https://blog.csdn.net/u010758605/article/details/48317881\n *\n * @author looly\n * @since 4.4.3\n */\npublic class Zodiac {\n\n\t/** 星座分隔时间日 */\n\tprivate static final int[] DAY_ARR = new int[] { 20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22 };\n\t/** 星座 */\n\tprivate static final String[] ZODIACS = new String[] { \"摩羯座\", \"水瓶座\", \"双鱼座\", \"白羊座\", \"金牛座\", \"双子座\", \"巨蟹座\", \"狮子座\", \"处女座\", \"天秤座\", \"天蝎座\", \"射手座\", \"摩羯座\" };\n\tprivate static final String[] CHINESE_ZODIACS = new String[] { \"鼠\", \"牛\", \"虎\", \"兔\", \"龙\", \"蛇\", \"马\", \"羊\", \"猴\", \"鸡\", \"狗\", \"猪\" };\n\n\t/**\n\t * 通过生日计算星座\n\t *\n\t * @param date 出生日期\n\t * @return 星座名\n\t */\n\tpublic static String getZodiac(Date date) {\n\t\treturn getZodiac(DateUtil.calendar(date));\n\t}\n\n\t/**\n\t * 通过生日计算星座\n\t *\n\t * @param calendar 出生日期\n\t * @return 星座名\n\t */\n\tpublic static String getZodiac(Calendar calendar) {\n\t\tif (null == calendar) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getZodiac(calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));\n\t}\n\n\t/**\n\t * 通过生日计算星座\n\t *\n\t * @param month 月，从0开始计数\n\t * @param day 天\n\t * @return 星座名\n\t * @since 4.5.0\n\t */\n\tpublic static String getZodiac(Month month, int day) {\n\t\treturn getZodiac(month.getValue(), day);\n\t}\n\n\t/**\n\t * 通过生日计算星座\n\t *\n\t * @param month 月，从0开始计数，见{@link Month#getValue()}\n\t * @param day 天\n\t * @return 星座名\n\t */\n\tpublic static String getZodiac(int month, int day) {\n\t\tAssert.checkBetween(month,\n\t\t\tMonth.JANUARY.getValue(),\n\t\t\tMonth.DECEMBER.getValue(), \"Unsupported month value, must be [0,12]\");\n\t\t// 在分隔日前为前一个星座，否则为后一个星座\n\t\treturn day < DAY_ARR[month] ? ZODIACS[month] : ZODIACS[month + 1];\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------- 生肖\n\t/**\n\t * 通过生日计算生肖，只计算1900年后出生的人\n\t *\n\t * @param date 出生日期（年需农历）\n\t * @return 星座名\n\t */\n\tpublic static String getChineseZodiac(Date date) {\n\t\treturn getChineseZodiac(DateUtil.calendar(date));\n\t}\n\n\t/**\n\t * 通过生日计算生肖，只计算1900年后出生的人\n\t *\n\t * @param calendar 出生日期（年需农历）\n\t * @return 星座名\n\t */\n\tpublic static String getChineseZodiac(Calendar calendar) {\n\t\tif (null == calendar) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getChineseZodiac(calendar.get(Calendar.YEAR));\n\t}\n\n\t/**\n\t * 计算生肖，只计算1900年后出生的人\n\t *\n\t * @param year 农历年\n\t * @return 生肖名\n\t */\n\tpublic static String getChineseZodiac(int year) {\n\t\tif (year < 1900) {\n\t\t\treturn null;\n\t\t}\n\t\treturn CHINESE_ZODIACS[(year - 1900) % CHINESE_ZODIACS.length];\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/ZoneUtil.java",
    "content": "package cn.hutool.core.date;\n\nimport java.time.ZoneId;\nimport java.util.TimeZone;\n\n/**\n * {@link ZoneId}和{@link TimeZone}相关封装\n *\n * @author looly\n * @since 5.7.15\n */\npublic class ZoneUtil {\n\n\t/**\n\t * {@link ZoneId}转换为{@link TimeZone}，{@code null}则返回系统默认值\n\t *\n\t * @param zoneId {@link ZoneId}，{@code null}则返回系统默认值\n\t * @return {@link TimeZone}\n\t */\n\tpublic static TimeZone toTimeZone(ZoneId zoneId) {\n\t\tif (null == zoneId) {\n\t\t\treturn TimeZone.getDefault();\n\t\t}\n\n\t\treturn TimeZone.getTimeZone(zoneId);\n\t}\n\n\t/**\n\t * {@link TimeZone}转换为{@link ZoneId}，{@code null}则返回系统默认值\n\t *\n\t * @param timeZone {@link TimeZone}，{@code null}则返回系统默认值\n\t * @return {@link ZoneId}\n\t */\n\tpublic static ZoneId toZoneId(TimeZone timeZone) {\n\t\tif (null == timeZone) {\n\t\t\treturn ZoneId.systemDefault();\n\t\t}\n\n\t\treturn timeZone.toZoneId();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/ChineseMonth.java",
    "content": "package cn.hutool.core.date.chinese;\n\n/**\n * 农历月份表示\n *\n * @author looly\n * @since 5.4.1\n */\npublic class ChineseMonth {\n\n\tprivate static final String[] MONTH_NAME = {\"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"七\", \"八\", \"九\", \"十\", \"十一\", \"十二\"};\n\tprivate static final String[] MONTH_NAME_TRADITIONAL = {\"正\", \"二\", \"三\", \"四\", \"五\", \"六\", \"七\", \"八\", \"九\", \"寒\", \"冬\", \"腊\"};\n\n\t/**\n\t * 当前农历月份是否为闰月\n\t *\n\t * @param year  农历年\n\t * @param month 农历月\n\t * @return 是否为闰月\n\t * @since 5.4.2\n\t */\n\tpublic static boolean isLeapMonth(int year, int month) {\n\t\treturn month == LunarInfo.leapMonth(year);\n\t}\n\n\t/**\n\t * 获得农历月称呼<br>\n\t * 当为传统表示时，表示为二月，腊月，或者润正月等\n\t * 当为非传统表示时，二月，十二月，或者润一月等\n\t *\n\t * @param isLeapMonth   是否闰月\n\t * @param month         月份，从1开始，如果是闰月，应传入需要显示的月份\n\t * @param isTraditional 是否传统表示，例如一月传统表示为正月\n\t * @return 返回农历月份称呼\n\t */\n\tpublic static String getChineseMonthName(boolean isLeapMonth, int month, boolean isTraditional) {\n\t\treturn (isLeapMonth ? \"闰\" : \"\") + (isTraditional ? MONTH_NAME_TRADITIONAL : MONTH_NAME)[month - 1] + \"月\";\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/GanZhi.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport java.time.LocalDate;\n\n/**\n * 天干地支类\n * 天干地支，简称为干支\n *\n * @author looly\n * @since 5.4.1\n */\npublic class GanZhi {\n\n\t/**\n\t * 十天干：甲（jiǎ）、乙（yǐ）、丙（bǐng）、丁（dīng）、戊（wù）、己（jǐ）、庚（gēng）、辛（xīn）、壬（rén）、癸（guǐ）\n\t * 十二地支：子（zǐ）、丑（chǒu）、寅（yín）、卯（mǎo）、辰（chén）、巳（sì）、午（wǔ）、未（wèi）、申（shēn）、酉（yǒu）、戌（xū）、亥（hài）\n\t * 十二地支对应十二生肖:子-鼠，丑-牛，寅-虎，卯-兔，辰-龙，巳-蛇， 午-马，未-羊，申-猴，酉-鸡，戌-狗，亥-猪\n\t *\n\t * @see <a href=\"https://baike.baidu.com/item/%E5%A4%A9%E5%B9%B2%E5%9C%B0%E6%94%AF/278140\">天干地支：简称，干支</a>\n\t */\n\tprivate static final String[] GAN = new String[]{\"甲\", \"乙\", \"丙\", \"丁\", \"戊\", \"己\", \"庚\", \"辛\", \"壬\", \"癸\"};\n\tprivate static final String[] ZHI = new String[]{\"子\", \"丑\", \"寅\", \"卯\", \"辰\", \"巳\", \"午\", \"未\", \"申\", \"酉\", \"戌\", \"亥\"};\n\n\t/**\n\t * 传入 月日的offset 传回干支, 0=甲子\n\t *\n\t * @param num 月日的offset\n\t * @return 干支\n\t */\n\tpublic static String cyclicalm(int num) {\n\t\treturn (GAN[num % 10] + ZHI[num % 12]);\n\t}\n\n\t/**\n\t * 传入年传回干支\n\t *\n\t * @param year 农历年\n\t * @return 干支\n\t * @since 5.4.7\n\t */\n\tpublic static String getGanzhiOfYear(int year) {\n\t\t// 1864年（1900 - 36）是甲子年，用于计算基准的干支年\n\t\treturn cyclicalm(year - LunarInfo.BASE_YEAR + 36);\n\t}\n\n\t/**\n\t * 获取干支月\n\t *\n\t * @param year  公历年\n\t * @param month 公历月，从1开始\n\t * @param day   公历日\n\t * @return 干支月\n\t * @since 5.4.7\n\t */\n\tpublic static String getGanzhiOfMonth(int year, int month, int day) {\n\t\t//返回当月「节」为几日开始\n\t\tint firstNode = SolarTerms.getTerm(year, (month * 2 - 1));\n\t\t// 依据12节气修正干支月\n\t\tint monthOffset = (year - LunarInfo.BASE_YEAR) * 12 + month + 11;\n\t\tif (day >= firstNode) {\n\t\t\tmonthOffset++;\n\t\t}\n\t\treturn cyclicalm(monthOffset);\n\t}\n\n\t/**\n\t * 获取干支日\n\t *\n\t * @param year  公历年\n\t * @param month 公历月，从1开始\n\t * @param day   公历日\n\t * @return 干支\n\t * @since 5.4.7\n\t */\n\tpublic static String getGanzhiOfDay(int year, int month, int day) {\n\t\t// 与1970-01-01相差天数，不包括当天\n\t\tfinal long days = LocalDate.of(year, month, day).toEpochDay() - 1;\n\t\t//1899-12-21是农历1899年腊月甲子日  41：相差1900-01-31有41天\n\t\treturn cyclicalm((int) (days - LunarInfo.BASE_DAY + 41));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/LunarFestival.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.map.TableMap;\n\nimport java.util.List;\n\n/**\n * 节假日（农历）封装\n *\n * @author looly\n * @since 5.4.1\n */\npublic class LunarFestival {\n\n\t//农历节日  *表示放假日\n\t// 来自：https://baike.baidu.com/item/%E4%B8%AD%E5%9B%BD%E4%BC%A0%E7%BB%9F%E8%8A%82%E6%97%A5/396100\n\tprivate static final TableMap<Pair<Integer, Integer>, String> L_FTV = new TableMap<>(16);\n\n\tstatic {\n\t\t// 节日\n\t\tL_FTV.put(new Pair<>(1, 1), \"春节\");\n\t\tL_FTV.put(new Pair<>(1, 2), \"犬日\");\n\t\tL_FTV.put(new Pair<>(1, 3), \"猪日\");\n\t\tL_FTV.put(new Pair<>(1, 4), \"羊日\");\n\t\tL_FTV.put(new Pair<>(1, 5), \"牛日 破五日\");\n\t\tL_FTV.put(new Pair<>(1, 6), \"马日 送穷日\");\n\t\tL_FTV.put(new Pair<>(1, 7), \"人日 人胜节\");\n\t\tL_FTV.put(new Pair<>(1, 8), \"谷日 八仙日\");\n\t\tL_FTV.put(new Pair<>(1, 9), \"天日 九皇会\");\n\t\tL_FTV.put(new Pair<>(1, 10), \"地日 石头生日\");\n\t\tL_FTV.put(new Pair<>(1, 12), \"火日 老鼠娶媳妇日\");\n\t\tL_FTV.put(new Pair<>(1, 13), \"上（试）灯日 关公升天日\");\n\t\tL_FTV.put(new Pair<>(1, 15), \"元宵节 上元节\");\n\t\tL_FTV.put(new Pair<>(1, 18), \"落灯日\");\n\n\t\t// 二月\n\t\tL_FTV.put(new Pair<>(2, 1), \"中和节 太阳生日\");\n\t\tL_FTV.put(new Pair<>(2, 2), \"龙抬头\");\n\t\tL_FTV.put(new Pair<>(2, 12), \"花朝节\");\n\t\tL_FTV.put(new Pair<>(2, 19), \"观世音圣诞\");\n\n\t\t// 三月\n\t\tL_FTV.put(new Pair<>(3, 3), \"上巳节\");\n\n\t\t// 四月\n\t\tL_FTV.put(new Pair<>(4, 1), \"祭雹神\");\n\t\tL_FTV.put(new Pair<>(4, 4), \"文殊菩萨诞辰\");\n\t\tL_FTV.put(new Pair<>(4, 8), \"佛诞节\");\n\n\t\t// 五月\n\t\tL_FTV.put(new Pair<>(5, 5), \"端午节 端阳节\");\n\n\t\t// 六月\n\t\tL_FTV.put(new Pair<>(6, 6), \"晒衣节 姑姑节\");\n\t\tL_FTV.put(new Pair<>(6, 6), \"天贶节\");\n\t\tL_FTV.put(new Pair<>(6, 24), \"彝族火把节\");\n\n\t\t// 七月\n\t\tL_FTV.put(new Pair<>(7, 7), \"七夕\");\n\t\tL_FTV.put(new Pair<>(7, 14), \"鬼节(南方)\");\n\t\tL_FTV.put(new Pair<>(7, 15), \"盂兰盆节 中元节\");\n\t\tL_FTV.put(new Pair<>(7, 30), \"地藏节\");\n\n\t\t// 八月\n\t\tL_FTV.put(new Pair<>(8, 15), \"中秋节\");\n\n\t\t// 九月\n\t\tL_FTV.put(new Pair<>(9, 9), \"重阳节\");\n\n\t\t// 十月\n\t\tL_FTV.put(new Pair<>(10, 1), \"祭祖节\");\n\t\tL_FTV.put(new Pair<>(10, 15), \"下元节\");\n\n\t\t// 十一月\n\t\tL_FTV.put(new Pair<>(11, 17), \"阿弥陀佛圣诞\");\n\n\t\t// 腊月\n\t\tL_FTV.put(new Pair<>(12, 8), \"腊八节\");\n\t\tL_FTV.put(new Pair<>(12, 16), \"尾牙\");\n\t\tL_FTV.put(new Pair<>(12, 23), \"小年\");\n\t\tL_FTV.put(new Pair<>(12, 30), \"除夕\");\n\t}\n\n\t/**\n\t * 获得节日列表\n\t *\n\t * @param year  年\n\t * @param month 月\n\t * @param day   日\n\t * @return 获得农历节日\n\t * @since 5.4.5\n\t */\n\tpublic static List<String> getFestivals(int year, int month, int day) {\n\t\t// 春节判断，如果12月是小月，则29为除夕，否则30为除夕\n\t\tif (12 == month && 29 == day) {\n\t\t\tif (29 == LunarInfo.monthDays(year, month)) {\n\t\t\t\tday++;\n\t\t\t}\n\t\t}\n\t\treturn getFestivals(month, day);\n\t}\n\n\t/**\n\t * 获得节日列表，此方法无法判断月是否为大月或小月\n\t *\n\t * @param month 月\n\t * @param day   日\n\t * @return 获得农历节日\n\t */\n\tpublic static List<String> getFestivals(int month, int day) {\n\t\treturn L_FTV.getValues(new Pair<>(month, day));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/LunarInfo.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport java.time.LocalDate;\n\n/**\n * 阴历（农历）信息\n *\n * @author looly\n * @since 5.4.1\n */\npublic class LunarInfo {\n\n\t/**\n\t * 1900年\n\t */\n\tpublic static final int BASE_YEAR = 1900;\n\t/**\n\t * 1900-01-31，农历正月初一\n\t */\n\tpublic static final long BASE_DAY = LocalDate.of(BASE_YEAR, 1, 31).toEpochDay();\n\n\t/**\n\t * 此表来自：<a href=\"https://github.com/jjonline/calendar.js/blob/master/calendar.js\">https://github.com/jjonline/calendar.js/blob/master/calendar.js</a>\n\t * 农历表示：\n\t * 1.  表示当年有无闰年，有的话，为闰月的月份，没有的话，为0。\n\t * 2-4.为除了闰月外的正常月份是大月还是小月，1为30天，0为29天。\n\t * 5.  表示闰月是大月还是小月，仅当存在闰月的情况下有意义。\n\t */\n\tprivate static final long[] LUNAR_CODE = new long[]{\n\t\t\t0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909\n\t\t\t0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919\n\t\t\t0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929\n\t\t\t0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939\n\t\t\t0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949\n\t\t\t0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959\n\t\t\t0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969\n\t\t\t0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979\n\t\t\t0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989\n\t\t\t0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999\n\t\t\t0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009\n\t\t\t0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019\n\t\t\t0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029\n\t\t\t0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039\n\t\t\t0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049\n\t\t\t0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059\n\t\t\t0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069\n\t\t\t0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079\n\t\t\t0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089\n\t\t\t0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099\n\t};\n\n\t// 支持的最大年限\n\tpublic static final int MAX_YEAR = BASE_YEAR + LUNAR_CODE.length - 1;\n\n\t/**\n\t * 传回农历 y年的总天数\n\t *\n\t * @param y 年\n\t * @return 总天数\n\t */\n\tpublic static int yearDays(int y) {\n\t\tint i, sum = 348;\n\t\tfor (i = 0x8000; i > 0x8; i >>= 1) {\n\t\t\tif ((getCode(y) & i) != 0) {\n\t\t\t\tsum += 1;\n\t\t\t}\n\t\t}\n\t\treturn (sum + leapDays(y));\n\t}\n\n\t/**\n\t * 传回农历 y年闰月的天数，如果本年无闰月，返回0，区分大小月\n\t *\n\t * @param y 农历年\n\t * @return 闰月的天数\n\t */\n\tpublic static int leapDays(int y) {\n\t\tif (leapMonth(y) != 0) {\n\t\t\treturn (getCode(y) & 0x10000) != 0 ? 30 : 29;\n\t\t}\n\n\t\treturn 0;\n\t}\n\n\t/**\n\t * 传回农历 y年m月的总天数，区分大小月\n\t *\n\t * @param y 年\n\t * @param m 月\n\t * @return 总天数\n\t */\n\tpublic static int monthDays(int y, int m) {\n\t\treturn (getCode(y) & (0x10000 >> m)) == 0 ? 29 : 30;\n\t}\n\n\t/**\n\t * 传回农历 y年闰哪个月 1-12 , 没闰传回 0<br>\n\t * 此方法会返回润N月中的N，如二月、闰二月都返回2\n\t *\n\t * @param y 年\n\t * @return 润的月, 没闰传回 0\n\t */\n\tpublic static int leapMonth(int y) {\n\t\treturn (int) (getCode(y) & 0xf);\n\t}\n\n\t/**\n\t * 获取对应年的农历信息\n\t *\n\t * @param year 年\n\t * @return 农历信息\n\t */\n\tprivate static long getCode(int year) {\n\t\treturn LUNAR_CODE[year - BASE_YEAR];\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/SolarTerms.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport cn.hutool.core.date.ChineseDate;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.LocalDate;\nimport java.util.Date;\n\n/**\n * 24节气相关信息\n *\n * @author looly, zak\n * @since 5.4.1\n */\npublic class SolarTerms {\n\n\t/**\n\t * 1900-2100各年的24节气日期速查表\n\t * 此表来自：https://github.com/jjonline/calendar.js/blob/master/calendar.js\n\t */\n\tprivate static final String[] S_TERM_INFO = new String[]{\n\t\t\t\"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\", \"97bcf97c3598082c95f8c965cc920f\",\n\t\t\t\"97bd0b06bdb0722c965ce1cfcc920f\", \"b027097bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\",\n\t\t\t\"97bcf97c359801ec95f8c965cc920f\", \"97bd0b06bdb0722c965ce1cfcc920f\", \"b027097bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b97bd19801ec9210c965cc920e\", \"97bcf97c359801ec95f8c965cc920f\", \"97bd0b06bdb0722c965ce1cfcc920f\",\n\t\t\t\"b027097bd097c36b0b6fc9274c91aa\", \"9778397bd19801ec9210c965cc920e\", \"97b6b97bd19801ec95f8c965cc920f\",\n\t\t\t\"97bd09801d98082c95f8e1cfcc920f\", \"97bd097bd097c36b0b6fc9210c8dc2\", \"9778397bd197c36c9210c9274c91aa\",\n\t\t\t\"97b6b97bd19801ec95f8c965cc920e\", \"97bd09801d98082c95f8e1cfcc920f\", \"97bd097bd097c36b0b6fc9210c8dc2\",\n\t\t\t\"9778397bd097c36c9210c9274c91aa\", \"97b6b97bd19801ec95f8c965cc920e\", \"97bcf97c3598082c95f8e1cfcc920f\",\n\t\t\t\"97bd097bd097c36b0b6fc9210c8dc2\", \"9778397bd097c36c9210c9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\",\n\t\t\t\"97bcf97c3598082c95f8c965cc920f\", \"97bd097bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b97bd19801ec9210c965cc920e\", \"97bcf97c3598082c95f8c965cc920f\", \"97bd097bd097c35b0b6fc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\", \"97bcf97c359801ec95f8c965cc920f\",\n\t\t\t\"97bd097bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\",\n\t\t\t\"97bcf97c359801ec95f8c965cc920f\", \"97bd097bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b97bd19801ec9210c965cc920e\", \"97bcf97c359801ec95f8c965cc920f\", \"97bd097bd07f595b0b6fc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9210c8dc2\", \"9778397bd19801ec9210c9274c920e\", \"97b6b97bd19801ec95f8c965cc920f\",\n\t\t\t\"97bd07f5307f595b0b0bc920fb0722\", \"7f0e397bd097c36b0b6fc9210c8dc2\", \"9778397bd097c36c9210c9274c920e\",\n\t\t\t\"97b6b97bd19801ec95f8c965cc920f\", \"97bd07f5307f595b0b0bc920fb0722\", \"7f0e397bd097c36b0b6fc9210c8dc2\",\n\t\t\t\"9778397bd097c36c9210c9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\", \"97bd07f1487f595b0b0bc920fb0722\",\n\t\t\t\"7f0e397bd097c36b0b6fc9210c8dc2\", \"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\",\n\t\t\t\"97bcf7f1487f595b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b97bd19801ec9210c965cc920e\", \"97bcf7f1487f595b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\", \"97bcf7f1487f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\", \"97b6b97bd19801ec9210c965cc920e\",\n\t\t\t\"97bcf7f1487f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b97bd19801ec9210c9274c920e\", \"97bcf7f0e47f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b0bc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9210c91aa\", \"97b6b97bd197c36c9210c9274c920e\", \"97bcf7f0e47f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd07f595b0b0bc920fb0722\", \"9778397bd097c36b0b6fc9210c8dc2\", \"9778397bd097c36c9210c9274c920e\",\n\t\t\t\"97b6b7f0e47f531b0723b0b6fb0722\", \"7f0e37f5307f595b0b0bc920fb0722\", \"7f0e397bd097c36b0b6fc9210c8dc2\",\n\t\t\t\"9778397bd097c36b0b70c9274c91aa\", \"97b6b7f0e47f531b0723b0b6fb0721\", \"7f0e37f1487f595b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd097c35b0b6fc9210c8dc2\", \"9778397bd097c36b0b6fc9274c91aa\", \"97b6b7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e27f1487f595b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9274c91aa\", \"97b6b7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd097c35b0b6fc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\", \"97b6b7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b0bc920fb0722\", \"9778397bd097c36b0b6fc9274c91aa\",\n\t\t\t\"97b6b7f0e47f531b0723b0787b0721\", \"7f0e27f0e47f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b0bc920fb0722\",\n\t\t\t\"9778397bd097c36b0b6fc9210c91aa\", \"97b6b7f0e47f149b0723b0787b0721\", \"7f0e27f0e47f531b0723b0b6fb0722\",\n\t\t\t\"7f0e397bd07f595b0b0bc920fb0722\", \"9778397bd097c36b0b6fc9210c8dc2\", \"977837f0e37f149b0723b0787b0721\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0722\", \"7f0e37f5307f595b0b0bc920fb0722\", \"7f0e397bd097c35b0b6fc9210c8dc2\",\n\t\t\t\"977837f0e37f14998082b0787b0721\", \"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e37f1487f595b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd097c35b0b6fc9210c8dc2\", \"977837f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\", \"977837f0e37f14998082b0787b06bd\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e397bd097c35b0b6fc920fb0722\",\n\t\t\t\"977837f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e397bd07f595b0b0bc920fb0722\", \"977837f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b0bc920fb0722\", \"977837f0e37f14998082b0787b06bd\",\n\t\t\t\"7f07e7f0e47f149b0723b0787b0721\", \"7f0e27f0e47f531b0b0bb0b6fb0722\", \"7f0e397bd07f595b0b0bc920fb0722\",\n\t\t\t\"977837f0e37f14998082b0723b06bd\", \"7f07e7f0e37f149b0723b0787b0721\", \"7f0e27f0e47f531b0723b0b6fb0722\",\n\t\t\t\"7f0e397bd07f595b0b0bc920fb0722\", \"977837f0e37f14898082b0723b02d5\", \"7ec967f0e37f14998082b0787b0721\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0722\", \"7f0e37f1487f595b0b0bb0b6fb0722\", \"7f0e37f0e37f14898082b0723b02d5\",\n\t\t\t\"7ec967f0e37f14998082b0787b0721\", \"7f07e7f0e47f531b0723b0b6fb0722\", \"7f0e37f1487f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e37f0e37f14898082b0723b02d5\", \"7ec967f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e37f1487f531b0b0bb0b6fb0722\", \"7f0e37f0e37f14898082b072297c35\", \"7ec967f0e37f14998082b0787b06bd\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e37f0e37f14898082b072297c35\",\n\t\t\t\"7ec967f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\",\n\t\t\t\"7f0e37f0e366aa89801eb072297c35\", \"7ec967f0e37f14998082b0787b06bd\", \"7f07e7f0e47f149b0723b0787b0721\",\n\t\t\t\"7f0e27f1487f531b0b0bb0b6fb0722\", \"7f0e37f0e366aa89801eb072297c35\", \"7ec967f0e37f14998082b0723b06bd\",\n\t\t\t\"7f07e7f0e47f149b0723b0787b0721\", \"7f0e27f0e47f531b0723b0b6fb0722\", \"7f0e37f0e366aa89801eb072297c35\",\n\t\t\t\"7ec967f0e37f14998082b0723b06bd\", \"7f07e7f0e37f14998083b0787b0721\", \"7f0e27f0e47f531b0723b0b6fb0722\",\n\t\t\t\"7f0e37f0e366aa89801eb072297c35\", \"7ec967f0e37f14898082b0723b02d5\", \"7f07e7f0e37f14998082b0787b0721\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0722\", \"7f0e36665b66aa89801e9808297c35\", \"665f67f0e37f14898082b0723b02d5\",\n\t\t\t\"7ec967f0e37f14998082b0787b0721\", \"7f07e7f0e47f531b0723b0b6fb0722\", \"7f0e36665b66a449801e9808297c35\",\n\t\t\t\"665f67f0e37f14898082b0723b02d5\", \"7ec967f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\",\n\t\t\t\"7f0e36665b66a449801e9808297c35\", \"665f67f0e37f14898082b072297c35\", \"7ec967f0e37f14998082b0787b06bd\",\n\t\t\t\"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e26665b66a449801e9808297c35\", \"665f67f0e37f1489801eb072297c35\",\n\t\t\t\"7ec967f0e37f14998082b0787b06bd\", \"7f07e7f0e47f531b0723b0b6fb0721\", \"7f0e27f1487f531b0b0bb0b6fb0722\"};\n\n\t/**\n\t * 24节气\n\t */\n\tprivate static final String[] TERMS = {\n\t\t\t\"小寒\", \"大寒\", \"立春\", \"雨水\", \"惊蛰\", \"春分\",\n\t\t\t\"清明\", \"谷雨\", \"立夏\", \"小满\", \"芒种\", \"夏至\",\n\t\t\t\"小暑\", \"大暑\", \"立秋\", \"处暑\", \"白露\", \"秋分\",\n\t\t\t\"寒露\", \"霜降\", \"立冬\", \"小雪\", \"大雪\", \"冬至\"\n\t};\n\n\t/**\n\t * 传入公历y年获得该年第n个节气的公历日期\n\t *\n\t * @param y  公历年(1900-2100)\n\t * @param n 二十四节气中的第几个节气(1~24)；从n=1(小寒)算起\n\t * @return getTerm(1987,3) -》4;意即1987年2月4日立春\n\t */\n\tpublic static int getTerm(int y, int n) {\n\t\tif (y < 1900 || y > 2100) {\n\t\t\treturn -1;\n\t\t}\n\t\tif (n < 1 || n > 24) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tfinal String _table = S_TERM_INFO[y - 1900];\n\t\tInteger[] _info = new Integer[6];\n\t\tfor (int i = 0; i < 6; i++) {\n\t\t\t_info[i] = Integer.parseInt(_table.substring(i * 5, 5 * (i + 1)), 16);\n\t\t}\n\t\tString[] _calday = new String[24];\n\t\tfor (int i = 0; i < 6; i++) {\n\t\t\t_calday[4 * i] = _info[i].toString().substring(0, 1);\n\t\t\t_calday[4 * i + 1] = _info[i].toString().substring(1, 3);\n\t\t\t_calday[4 * i + 2] = _info[i].toString().substring(3, 4);\n\t\t\t_calday[4 * i + 3] = _info[i].toString().substring(4, 6);\n\t\t}\n\t\treturn NumberUtil.parseInt(_calday[n - 1]);\n\t}\n\n\t/**\n\t * 根据日期获取节气\n\t * @param date 日期\n\t * @return 返回指定日期所处的节气，若不是一个节气则返回空字符串\n\t */\n\tpublic static String getTerm(Date date) {\n\t\tfinal DateTime dt = DateUtil.date(date);\n\t\treturn getTermInternal(dt.year(), dt.month() + 1, dt.dayOfMonth());\n\t}\n\n\n\t/**\n\t * 根据农历日期获取节气\n\t * @param chineseDate 农历日期\n\t * @return 返回指定农历日期所处的节气，若不是一个节气则返回空字符串\n\t */\n\tpublic static String getTerm(ChineseDate chineseDate) {\n\t\treturn chineseDate.getTerm();\n\t}\n\n\t/**\n\t * 根据日期获取节气\n\t * @param date 日期\n\t * @return 返回指定日期所处的节气，若不是一个节气则返回空字符串\n\t */\n\tpublic static String getTerm(LocalDate date) {\n\t\treturn getTermInternal(date.getYear(), date.getMonthValue(), date.getDayOfMonth());\n\t}\n\n\t/**\n\t * 根据年月日获取节气\n\t * @param year 公历年\n\t * @param mouth 公历月，从1开始\n\t * @param day 公历日，从1开始\n\t * @return 返回指定年月日所处的节气，若不是一个节气则返回空字符串\n\t */\n\tpublic static String getTerm(int year, int mouth, int day) {\n\t\treturn getTerm(LocalDate.of(year, mouth, day));\n\t}\n\n\t/**\n\t * 根据年月日获取节气, 内部方法，不对月和日做有效校验\n\t * @param year 公历年\n\t * @param mouth 公历月，从1开始\n\t * @param day 公历日，从1开始\n\t * @return 返回指定年月日所处的节气，若不是一个节气则返回空字符串\n\t */\n\tprivate static String getTermInternal(int year, int mouth, int day) {\n\t\tif (year < 1900 || year > 2100) {\n\t\t\tthrow new IllegalArgumentException(\"只支持1900-2100之间的日期获取节气\");\n\t\t}\n\n\t\tfinal String termTable = S_TERM_INFO[year - 1900];\n\n\t\t// 节气速查表中每5个字符含有4个节气，通过月份直接计算偏移\n\t\tfinal int segment = (mouth + 1) / 2 - 1;\n\t\tfinal int termInfo = Integer.parseInt(termTable.substring(segment * 5, (segment + 1) * 5), 16);\n\t\tfinal String termInfoStr = String.valueOf(termInfo);\n\n\t\tfinal String[] segmentTable = new String[4];\n\t\tsegmentTable[0] = termInfoStr.substring(0, 1);\n\t\tsegmentTable[1] = termInfoStr.substring(1, 3);\n\t\tsegmentTable[2] = termInfoStr.substring(3, 4);\n\t\tsegmentTable[3] = termInfoStr.substring(4, 6);\n\n\t\t// 奇数月份的节气在前2个，偶数月份的节气在后两个\n\t\tfinal int segmentOffset = (mouth & 1) == 1 ? 0 : 2;\n\n\t\tif (day == Integer.parseInt(segmentTable[segmentOffset])) {\n\t\t\treturn TERMS[segment * 4 + segmentOffset];\n\t\t}\n\t\tif (day == Integer.parseInt(segmentTable[segmentOffset + 1])) {\n\t\t\treturn TERMS[segment * 4 + segmentOffset + 1];\n\t\t}\n\t\treturn StrUtil.EMPTY;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/chinese/package-info.java",
    "content": "/**\n * 农历相关类汇总，包括农历月、天干地支、农历节日、24节气等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.date.chinese;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java",
    "content": "package cn.hutool.core.date.format;\n\nimport java.io.Serializable;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\npublic abstract class AbstractDateBasic implements DateBasic, Serializable {\n\tprivate static final long serialVersionUID = 6333136319870641818L;\n\n\t/** The pattern */\n\tprotected final  String pattern;\n\t/** The time zone. */\n\tprotected final TimeZone timeZone;\n\t/** The locale. */\n\tprotected final Locale locale;\n\n\t/**\n\t * 构造，内部使用\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 非空时区{@link TimeZone}\n\t * @param locale 非空{@link Locale} 日期地理位置\n\t */\n\tprotected AbstractDateBasic(final String pattern, final TimeZone timeZone, final Locale locale) {\n\t\tthis.pattern = pattern;\n\t\tthis.timeZone = timeZone;\n\t\tthis.locale = locale;\n\t}\n\n\t// ----------------------------------------------------------------------- Accessors\n\t@Override\n\tpublic String getPattern() {\n\t\treturn pattern;\n\t}\n\n\t@Override\n\tpublic TimeZone getTimeZone() {\n\t\treturn timeZone;\n\t}\n\n\t@Override\n\tpublic Locale getLocale() {\n\t\treturn locale;\n\t}\n\n\t// ----------------------------------------------------------------------- Basics\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof FastDatePrinter == false) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal AbstractDateBasic other = (AbstractDateBasic) obj;\n\t\treturn pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"FastDatePrinter[\" + pattern + \",\" + locale + \",\" + timeZone.getID() + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/DateBasic.java",
    "content": "package cn.hutool.core.date.format;\n\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * 日期基本信息获取接口\n *\n * @author Looly\n * @since 2.16.2\n */\npublic interface DateBasic {\n\n\t/**\n\t * 获得日期格式化或者转换的格式\n\t *\n\t * @return {@link java.text.SimpleDateFormat}兼容的格式\n\t */\n\tString getPattern();\n\n\t/**\n\t * 获得时区\n\t *\n\t * @return {@link TimeZone}\n\t */\n\tTimeZone getTimeZone();\n\n\t/**\n\t * 获得 日期地理位置\n\t *\n\t * @return {@link Locale}\n\t */\n\tLocale getLocale();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/DateParser.java",
    "content": "package cn.hutool.core.date.format;\n\nimport java.text.ParseException;\nimport java.text.ParsePosition;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 日期解析接口，用于解析日期字符串为 {@link Date} 对象<br>\n * Thanks to Apache Commons Lang 3.5\n * @since 2.16.2\n */\npublic interface DateParser extends DateBasic{\n\n\t/**\n\t * 将日期字符串解析并转换为  {@link Date} 对象<br>\n\t * 等价于 {@link java.text.DateFormat#parse(String)}\n\t *\n\t * @param source 日期字符串\n\t * @return {@link Date}\n\t * @throws ParseException 转换异常，被转换的字符串格式错误。\n\t */\n\tDate parse(String source) throws ParseException;\n\n\t/**\n\t * 将日期字符串解析并转换为  {@link Date} 对象<br>\n\t * 等价于 {@link java.text.DateFormat#parse(String, ParsePosition)}\n\t *\n\t * @param source 日期字符串\n\t * @param pos {@link ParsePosition}\n\t * @return {@link Date}\n\t */\n\tDate parse(String source, ParsePosition pos);\n\n\t/**\n\t * 根据给定格式更新{@link Calendar}\n\t * Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed.\n\t * Not all source text needs to be consumed.\n\t * Upon parse failure, ParsePosition error index is updated to the offset of the source text which does not match the supplied format.\n\t *\n\t * @param source 被转换的日期字符串\n\t * @param pos 定义开始转换的位置，转换结束后更新转换到的位置\n\t * @param calendar The calendar into which to set parsed fields.\n\t * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)\n\t * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range.\n\t */\n\tboolean parse(String source, ParsePosition pos, Calendar calendar);\n\n\t/**\n\t * 将日期字符串解析并转换为  {@link Date} 对象<br>\n\t *\n\t * @param source A {@code String} whose beginning should be parsed.\n\t * @return a {@code java.util.Date} object\n\t * @throws ParseException if the beginning of the specified string cannot be parsed.\n\t * @see java.text.DateFormat#parseObject(String)\n\t */\n\tdefault Object parseObject(String source) throws ParseException{\n\t\treturn parse(source);\n\t}\n\n\t/**\n\t * 根据 {@link ParsePosition} 给定将日期字符串解析并转换为  {@link Date} 对象<br>\n\t *\n\t * @param source A {@code String} whose beginning should be parsed.\n\t * @param pos the parse position\n\t * @return a {@code java.util.Date} object\n\t * @see java.text.DateFormat#parseObject(String, ParsePosition)\n\t */\n\tdefault Object parseObject(String source, ParsePosition pos){\n\t\treturn parse(source, pos);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/DatePrinter.java",
    "content": "package cn.hutool.core.date.format;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 日期格式化输出接口<br>\n * Thanks to Apache Commons Lang 3.5\n * @author Looly\n * @since 2.16.2\n */\npublic interface DatePrinter extends DateBasic {\n\n\t/**\n\t * 格式化日期表示的毫秒数\n\t *\n\t * @param millis 日期毫秒数\n\t * @return the formatted string\n\t * @since 2.1\n\t */\n\tString format(long millis);\n\n\t/**\n\t * 使用 {@code GregorianCalendar} 格式化 {@code Date}\n\t *\n\t * @param date 日期 {@link Date}\n\t * @return 格式化后的字符串\n\t */\n\tString format(Date date);\n\n\t/**\n\t * <p>\n\t * Formats a {@code Calendar} object.\n\t * </p>\n\t * 格式化 {@link Calendar}\n\t *\n\t * @param calendar {@link Calendar}\n\t * @return 格式化后的字符串\n\t */\n\tString format(Calendar calendar);\n\n\t/**\n\t * <p>\n\t * Formats a millisecond {@code long} value into the supplied {@code Appendable}.\n\t * </p>\n\t *\n\t * @param millis the millisecond value to format\n\t * @param buf the buffer to format into\n\t * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.\n\t * @return the specified string buffer\n\t */\n\t<B extends Appendable> B format(long millis, B buf);\n\n\t/**\n\t * <p>\n\t * Formats a {@code Date} object into the supplied {@code Appendable} using a {@code GregorianCalendar}.\n\t * </p>\n\t *\n\t * @param date the date to format\n\t * @param buf the buffer to format into\n\t * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.\n\t * @return the specified string buffer\n\t */\n\t<B extends Appendable> B format(Date date, B buf);\n\n\t/**\n\t * <p>\n\t * Formats a {@code Calendar} object into the supplied {@code Appendable}.\n\t * </p>\n\t * The TimeZone set on the Calendar is only used to adjust the time offset. The TimeZone specified during the construction of the Parser will determine the TimeZone used in the formatted string.\n\t *\n\t * @param calendar the calendar to format\n\t * @param buf the buffer to format into\n\t * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.\n\t * @return the specified string buffer\n\t */\n\t<B extends Appendable> B format(Calendar calendar, B buf);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/FastDateFormat.java",
    "content": "package cn.hutool.core.date.format;\n\nimport cn.hutool.core.date.DatePattern;\n\nimport java.text.DateFormat;\nimport java.text.FieldPosition;\nimport java.text.Format;\nimport java.text.ParseException;\nimport java.text.ParsePosition;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * <p>\n * FastDateFormat 是一个线程安全的 {@link java.text.SimpleDateFormat} 实现。\n * </p>\n *\n * <p>\n * 通过以下静态方法获得此对象: <br>\n * {@link #getInstance(String, TimeZone, Locale)}<br>\n * {@link #getDateInstance(int, TimeZone, Locale)}<br>\n * {@link #getTimeInstance(int, TimeZone, Locale)}<br>\n * {@link #getDateTimeInstance(int, int, TimeZone, Locale)}\n * </p>\n *\n * Thanks to Apache Commons Lang 3.5\n * @since 2.16.2\n */\npublic class FastDateFormat extends Format implements DateParser, DatePrinter {\n\tprivate static final long serialVersionUID = 8097890768636183236L;\n\n\t/** FULL locale dependent date or time style. */\n\tpublic static final int FULL = DateFormat.FULL;\n\t/** LONG locale dependent date or time style. */\n\tpublic static final int LONG = DateFormat.LONG;\n\t/** MEDIUM locale dependent date or time style. */\n\tpublic static final int MEDIUM = DateFormat.MEDIUM;\n\t/** SHORT locale dependent date or time style. */\n\tpublic static final int SHORT = DateFormat.SHORT;\n\n\tprivate static final FormatCache<FastDateFormat> CACHE = new FormatCache<FastDateFormat>(){\n\t\t@Override\n\t\tprotected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) {\n\t\t\treturn new FastDateFormat(pattern, timeZone, locale);\n\t\t}\n\t};\n\n\tprivate final FastDatePrinter printer;\n\tprivate final FastDateParser parser;\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 获得 FastDateFormat实例，使用默认格式和地区\n\t *\n\t * @return FastDateFormat\n\t */\n\tpublic static FastDateFormat getInstance() {\n\t\treturn CACHE.getInstance();\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例，使用默认地区<br>\n\t * 支持缓存\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @return FastDateFormat\n\t * @throws IllegalArgumentException 日期格式问题\n\t */\n\tpublic static FastDateFormat getInstance(final String pattern) {\n\t\treturn CACHE.getInstance(pattern, null, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 时区{@link TimeZone}\n\t * @return FastDateFormat\n\t * @throws IllegalArgumentException 日期格式问题\n\t */\n\tpublic static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) {\n\t\treturn CACHE.getInstance(pattern, timeZone, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return FastDateFormat\n\t * @throws IllegalArgumentException 日期格式问题\n\t */\n\tpublic static FastDateFormat getInstance(final String pattern, final Locale locale) {\n\t\treturn CACHE.getInstance(pattern, null, locale);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 时区{@link TimeZone}\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return FastDateFormat\n\t * @throws IllegalArgumentException 日期格式问题\n\t */\n\tpublic static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {\n\t\treturn CACHE.getInstance(pattern, timeZone, locale);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style date style: FULL, LONG, MEDIUM, or SHORT\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateInstance(final int style) {\n\t\treturn CACHE.getDateInstance(style, null, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateInstance(final int style, final Locale locale) {\n\t\treturn CACHE.getDateInstance(style, null, locale);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone 时区{@link TimeZone}\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {\n\t\treturn CACHE.getDateInstance(style, timeZone, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone 时区{@link TimeZone}\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) {\n\t\treturn CACHE.getDateInstance(style, timeZone, locale);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style time style: FULL, LONG, MEDIUM, or SHORT\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getTimeInstance(final int style) {\n\t\treturn CACHE.getTimeInstance(style, null, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getTimeInstance(final int style, final Locale locale) {\n\t\treturn CACHE.getTimeInstance(style, null, locale);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone optional time zone, overrides time zone of formatted time\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) {\n\t\treturn CACHE.getTimeInstance(style, timeZone, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param style time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone optional time zone, overrides time zone of formatted time\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) {\n\t\treturn CACHE.getTimeInstance(style, timeZone, locale);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) {\n\t\treturn CACHE.getDateTimeInstance(dateStyle, timeStyle, null, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) {\n\t\treturn CACHE.getDateTimeInstance(dateStyle, timeStyle, null, locale);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone 时区{@link TimeZone}\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone) {\n\t\treturn getDateTimeInstance(dateStyle, timeStyle, timeZone, null);\n\t}\n\n\t/**\n\t * 获得 FastDateFormat 实例<br>\n\t * 支持缓存\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone 时区{@link TimeZone}\n\t * @param locale {@link Locale} 日期地理位置\n\t * @return 本地化 FastDateFormat\n\t */\n\tpublic static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {\n\t\treturn CACHE.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale);\n\t}\n\n\t// ----------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 非空时区 {@link TimeZone}\n\t * @param locale {@link Locale} 日期地理位置\n\t * @throws NullPointerException if pattern, timeZone, or locale is null.\n\t */\n\tprotected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {\n\t\tthis(pattern, timeZone, locale, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 非空时区 {@link TimeZone}\n\t * @param locale {@link Locale} 日期地理位置\n\t * @param centuryStart The start of the 100 year period to use as the \"default century\" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years\n\t * @throws NullPointerException if pattern, timeZone, or locale is null.\n\t */\n\tprotected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {\n\t\tprinter = new FastDatePrinter(pattern, timeZone, locale);\n\t\tparser = new FastDateParser(pattern, timeZone, locale, centuryStart);\n\t}\n\t// ----------------------------------------------------------------------- Constructor end\n\n\t// ----------------------------------------------------------------------- Format methods\n\t@Override\n\tpublic StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {\n\t\treturn toAppendTo.append(printer.format(obj));\n\t}\n\n\t@Override\n\tpublic String format(final long millis) {\n\t\treturn printer.format(millis);\n\t}\n\n\t@Override\n\tpublic String format(final Date date) {\n\t\treturn printer.format(date);\n\t}\n\n\t@Override\n\tpublic String format(final Calendar calendar) {\n\t\treturn printer.format(calendar);\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(final long millis, final B buf) {\n\t\treturn printer.format(millis, buf);\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(final Date date, final B buf) {\n\t\treturn printer.format(date, buf);\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(final Calendar calendar, final B buf) {\n\t\treturn printer.format(calendar, buf);\n\t}\n\n\t// ----------------------------------------------------------------------- Parsing\n\t@Override\n\tpublic Date parse(final String source) throws ParseException {\n\t\treturn parser.parse(source);\n\t}\n\n\t@Override\n\tpublic Date parse(final String source, final ParsePosition pos) {\n\t\treturn parser.parse(source, pos);\n\t}\n\n\t@Override\n\tpublic boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {\n\t\treturn parser.parse(source, pos, calendar);\n\t}\n\n\t@Override\n\tpublic Object parseObject(final String source, final ParsePosition pos) {\n\t\treturn parser.parseObject(source, pos);\n\t}\n\n\t// ----------------------------------------------------------------------- Accessors\n\t@Override\n\tpublic String getPattern() {\n\t\treturn printer.getPattern();\n\t}\n\n\t@Override\n\tpublic TimeZone getTimeZone() {\n\t\treturn printer.getTimeZone();\n\t}\n\n\t@Override\n\tpublic Locale getLocale() {\n\t\treturn printer.getLocale();\n\t}\n\n\t/**\n\t *估算生成的日期字符串长度<br>\n\t * 实际生成的字符串长度小于或等于此值\n\t *\n\t * @return 日期字符串长度\n\t */\n\tpublic int getMaxLengthEstimate() {\n\t\treturn printer.getMaxLengthEstimate();\n\t}\n\n\t// convert DateTimeFormatter\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 便捷获取 DateTimeFormatter\n\t * 由于 {@link DatePattern} 很大一部分的格式没有提供 {@link DateTimeFormatter},因此这里提供快捷获取方式\n\t * @return DateTimeFormatter\n\t * @author dazer neusoft\n\t * @since 5.6.4\n\t */\n\tpublic DateTimeFormatter getDateTimeFormatter() {\n\t\tDateTimeFormatter formatter = DateTimeFormatter.ofPattern(this.getPattern());\n\t\tif (this.getLocale() != null) {\n\t\t\tformatter = formatter.withLocale(this.getLocale());\n\t\t}\n\t\tif (this.getTimeZone() != null) {\n\t\t\tformatter = formatter.withZone(this.getTimeZone().toZoneId());\n\t\t}\n\t\treturn formatter;\n\t}\n\n\t// Basics\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof FastDateFormat == false) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal FastDateFormat other = (FastDateFormat) obj;\n\t\t// no need to check parser, as it has same invariants as printer\n\t\treturn printer.equals(other.printer);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn printer.hashCode();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"FastDateFormat[\" + printer.getPattern() + \",\" + printer.getLocale() + \",\" + printer.getTimeZone().getID() + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/FastDateParser.java",
    "content": "package cn.hutool.core.date.format;\n\nimport cn.hutool.core.date.TimeZoneUtil;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.text.DateFormatSymbols;\nimport java.text.ParseException;\nimport java.text.ParsePosition;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * {@link java.text.SimpleDateFormat} 的线程安全版本，用于解析日期字符串并转换为 {@link Date} 对象<br>\n * Thanks to Apache Commons Lang 3.5\n *\n * @see FastDatePrinter\n * @since 2.16.2\n */\npublic class FastDateParser extends AbstractDateBasic implements DateParser {\n\tprivate static final long serialVersionUID = -3199383897950947498L;\n\n\tstatic final Locale JAPANESE_IMPERIAL = new Locale(\"ja\", \"JP\", \"JP\");\n\n\t/**\n\t * 世纪：2000年前为19， 之后为20\n\t */\n\tprivate final int century;\n\tprivate final int startYear;\n\n\t// derived fields\n\tprivate transient List<StrategyAndWidth> patterns;\n\n\t// comparator used to sort regex alternatives\n\t// alternatives should be ordered longer first, and shorter last. ('february' before 'feb')\n\t// all entries must be lowercase by locale.\n\tprivate static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();\n\n\t/**\n\t * <p>\n\t * Constructs a new FastDateParser.\n\t * </p>\n\t * <p>\n\t * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached FastDateParser instance.\n\t *\n\t * @param pattern  non-null {@link java.text.SimpleDateFormat} compatible pattern\n\t * @param timeZone non-null time zone to use\n\t * @param locale   non-null locale\n\t */\n\tpublic FastDateParser(String pattern, TimeZone timeZone, Locale locale) {\n\t\tthis(pattern, timeZone, locale, null);\n\t}\n\n\t/**\n\t * <p>\n\t * Constructs a new FastDateParser.\n\t * </p>\n\t *\n\t * @param pattern      non-null {@link java.text.SimpleDateFormat} compatible pattern\n\t * @param timeZone     non-null time zone to use\n\t * @param locale       non-null locale\n\t * @param centuryStart The start of the century for 2 digit year parsing\n\t */\n\tpublic FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {\n\t\tsuper(pattern, timeZone, locale);\n\t\tfinal Calendar definingCalendar = Calendar.getInstance(timeZone, locale);\n\n\t\tint centuryStartYear;\n\t\tif (centuryStart != null) {\n\t\t\tdefiningCalendar.setTime(centuryStart);\n\t\t\tcenturyStartYear = definingCalendar.get(Calendar.YEAR);\n\t\t} else if (locale.equals(JAPANESE_IMPERIAL)) {\n\t\t\tcenturyStartYear = 0;\n\t\t} else {\n\t\t\t// from 80 years ago to 20 years from now\n\t\t\tdefiningCalendar.setTime(new Date());\n\t\t\tcenturyStartYear = definingCalendar.get(Calendar.YEAR) - 80;\n\t\t}\n\t\tcentury = centuryStartYear / 100 * 100;\n\t\tstartYear = centuryStartYear - century;\n\n\t\tinit(definingCalendar);\n\t}\n\n\t/**\n\t * Initialize derived fields from defining fields. This is called from constructor and from readObject (de-serialization)\n\t *\n\t * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser\n\t */\n\tprivate void init(final Calendar definingCalendar) {\n\t\tpatterns = new ArrayList<>();\n\n\t\tfinal StrategyParser fm = new StrategyParser(definingCalendar);\n\t\tfor (; ; ) {\n\t\t\tfinal StrategyAndWidth field = fm.getNextStrategy();\n\t\t\tif (field == null) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tpatterns.add(field);\n\t\t}\n\t}\n\n\t// helper classes to parse the format string\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * Holds strategy and field width\n\t */\n\tprivate static class StrategyAndWidth {\n\t\tfinal Strategy strategy;\n\t\tfinal int width;\n\n\t\tStrategyAndWidth(final Strategy strategy, final int width) {\n\t\t\tthis.strategy = strategy;\n\t\t\tthis.width = width;\n\t\t}\n\n\t\tint getMaxWidth(final ListIterator<StrategyAndWidth> lt) {\n\t\t\tif (!strategy.isNumber() || !lt.hasNext()) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tfinal Strategy nextStrategy = lt.next().strategy;\n\t\t\tlt.previous();\n\t\t\treturn nextStrategy.isNumber() ? width : 0;\n\t\t}\n\t}\n\n\t/**\n\t * Parse format into Strategies\n\t */\n\tprivate class StrategyParser {\n\t\tfinal private Calendar definingCalendar;\n\t\tprivate int currentIdx;\n\n\t\tStrategyParser(final Calendar definingCalendar) {\n\t\t\tthis.definingCalendar = definingCalendar;\n\t\t}\n\n\t\tStrategyAndWidth getNextStrategy() {\n\t\t\tif (currentIdx >= pattern.length()) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tfinal char c = pattern.charAt(currentIdx);\n\t\t\tif (isFormatLetter(c)) {\n\t\t\t\treturn letterPattern(c);\n\t\t\t}\n\t\t\treturn literal();\n\t\t}\n\n\t\tprivate StrategyAndWidth letterPattern(final char c) {\n\t\t\tfinal int begin = currentIdx;\n\t\t\twhile (++currentIdx < pattern.length()) {\n\t\t\t\tif (pattern.charAt(currentIdx) != c) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfinal int width = currentIdx - begin;\n\t\t\treturn new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);\n\t\t}\n\n\t\tprivate StrategyAndWidth literal() {\n\t\t\tboolean activeQuote = false;\n\n\t\t\tfinal StringBuilder sb = new StringBuilder();\n\t\t\twhile (currentIdx < pattern.length()) {\n\t\t\t\tfinal char c = pattern.charAt(currentIdx);\n\t\t\t\tif (!activeQuote && isFormatLetter(c)) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (c == '\\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\\'')) {\n\t\t\t\t\tactiveQuote = !activeQuote;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t++currentIdx;\n\t\t\t\tsb.append(c);\n\t\t\t}\n\n\t\t\tif (activeQuote) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unterminated quote\");\n\t\t\t}\n\n\t\t\tfinal String formatField = sb.toString();\n\t\t\treturn new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());\n\t\t}\n\t}\n\n\tprivate static boolean isFormatLetter(final char c) {\n\t\treturn c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';\n\t}\n\n\t// Serializing\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * Create the object after serialization. This implementation reinitializes the transient properties.\n\t *\n\t * @param in ObjectInputStream from which the object is being deserialized.\n\t * @throws IOException            if there is an IO issue.\n\t * @throws ClassNotFoundException if a class cannot be found.\n\t */\n\tprivate void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {\n\t\tin.defaultReadObject();\n\n\t\tfinal Calendar definingCalendar = Calendar.getInstance(timeZone, locale);\n\t\tinit(definingCalendar);\n\t}\n\n\t@Override\n\tpublic Date parse(String source) throws ParseException {\n\t\tfinal ParsePosition pp = new ParsePosition(0);\n\t\tfinal Date date = parse(source, pp);\n\t\tif (date == null) {\n\t\t\t// Add a note re supported date range\n\t\t\tif (locale.equals(JAPANESE_IMPERIAL)) {\n\t\t\t\tthrow new ParseException(\"(The \" + locale + \" locale does not support dates before 1868 AD)\\n\" +\n\t\t\t\t\t\t\"Unparseable date: \\\"\" + source, pp.getErrorIndex());\n\t\t\t}\n\t\t\tthrow new ParseException(\"Unparseable date: \" + source, pp.getErrorIndex());\n\t\t}\n\t\treturn date;\n\t}\n\n\t@Override\n\tpublic Date parse(String source, ParsePosition pos) {\n\t\t// timing tests indicate getting new instance is 19% faster than cloning\n\t\tfinal Calendar cal = Calendar.getInstance(timeZone, locale);\n\t\tcal.clear();\n\n\t\treturn parse(source, pos, cal) ? cal.getTime() : null;\n\t}\n\n\t@Override\n\tpublic boolean parse(String source, ParsePosition pos, Calendar calendar) {\n\t\tfinal ListIterator<StrategyAndWidth> lt = patterns.listIterator();\n\t\twhile (lt.hasNext()) {\n\t\t\tfinal StrategyAndWidth strategyAndWidth = lt.next();\n\t\t\tfinal int maxWidth = strategyAndWidth.getMaxWidth(lt);\n\t\t\tif (false == strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t// Support for strategies\n\t// -----------------------------------------------------------------------\n\n\tprivate static StringBuilder simpleQuote(final StringBuilder sb, final String value) {\n\t\tfor (int i = 0; i < value.length(); ++i) {\n\t\t\tfinal char c = value.charAt(i);\n\t\t\tswitch (c) {\n\t\t\t\tcase '\\\\':\n\t\t\t\tcase '^':\n\t\t\t\tcase '$':\n\t\t\t\tcase '.':\n\t\t\t\tcase '|':\n\t\t\t\tcase '?':\n\t\t\t\tcase '*':\n\t\t\t\tcase '+':\n\t\t\t\tcase '(':\n\t\t\t\tcase ')':\n\t\t\t\tcase '[':\n\t\t\t\tcase '{':\n\t\t\t\t\tsb.append('\\\\');\n\t\t\t\tdefault:\n\t\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t}\n\t\treturn sb;\n\t}\n\n\t/**\n\t * Get the short and long values displayed for a field\n\t *\n\t * @param cal    The calendar to obtain the short and long values\n\t * @param locale The locale of display names\n\t * @param field  The field of interest\n\t * @param regex  The regular expression to build\n\t * @return The map of string display names to field values\n\t */\n\tprivate static Map<String, Integer> appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) {\n\t\tfinal Map<String, Integer> values = new HashMap<>();\n\n\t\tfinal Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale);\n\t\tfinal TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);\n\t\tfor (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) {\n\t\t\tfinal String key = displayName.getKey().toLowerCase(locale);\n\t\t\tif (sorted.add(key)) {\n\t\t\t\tvalues.put(key, displayName.getValue());\n\t\t\t}\n\t\t}\n\t\tfor (final String symbol : sorted) {\n\t\t\tsimpleQuote(regex, symbol).append('|');\n\t\t}\n\t\treturn values;\n\t}\n\n\t/**\n\t * 使用当前的世纪调整两位数年份为四位数年份\n\t *\n\t * @param twoDigitYear 两位数年份\n\t * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)\n\t */\n\tprivate int adjustYear(final int twoDigitYear) {\n\t\tfinal int trial = century + twoDigitYear;\n\t\treturn twoDigitYear >= startYear ? trial : trial + 100;\n\t}\n\n\t/**\n\t * 单个日期字段的分析策略\n\t */\n\tprivate static abstract class Strategy {\n\t\t/**\n\t\t * Is this field a number? The default implementation returns false.\n\t\t *\n\t\t * @return true, if field is a number\n\t\t */\n\t\tboolean isNumber() {\n\t\t\treturn false;\n\t\t}\n\n\t\tabstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);\n\t}\n\n\t/**\n\t * A strategy to parse a single field from the parsing pattern\n\t */\n\tprivate static abstract class PatternStrategy extends Strategy {\n\n\t\tprivate Pattern pattern;\n\n\t\tvoid createPattern(final StringBuilder regex) {\n\t\t\tcreatePattern(regex.toString());\n\t\t}\n\n\t\tvoid createPattern(final String regex) {\n\t\t\tthis.pattern = Pattern.compile(regex);\n\t\t}\n\n\t\t/**\n\t\t * Is this field a number? The default implementation returns false.\n\t\t *\n\t\t * @return true, if field is a number\n\t\t */\n\t\t@Override\n\t\tboolean isNumber() {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tboolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {\n\t\t\tfinal Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));\n\t\t\tif (!matcher.lookingAt()) {\n\t\t\t\tpos.setErrorIndex(pos.getIndex());\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tpos.setIndex(pos.getIndex() + matcher.end(1));\n\t\t\tsetCalendar(parser, calendar, matcher.group(1));\n\t\t\treturn true;\n\t\t}\n\n\t\tabstract void setCalendar(FastDateParser parser, Calendar cal, String value);\n\t}\n\n\t/**\n\t * Obtain a Strategy given a field from a SimpleDateFormat pattern\n\t *\n\t * @param f                格式\n\t * @param width            长度\n\t * @param definingCalendar The calendar to obtain the short and long values\n\t * @return The Strategy that will handle parsing for the field\n\t */\n\tprivate Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {\n\t\tswitch (f) {\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalArgumentException(\"Format '\" + f + \"' not supported\");\n\t\t\tcase 'D':\n\t\t\t\treturn DAY_OF_YEAR_STRATEGY;\n\t\t\tcase 'E':\n\t\t\t\treturn getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);\n\t\t\tcase 'F':\n\t\t\t\treturn DAY_OF_WEEK_IN_MONTH_STRATEGY;\n\t\t\tcase 'G':\n\t\t\t\treturn getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);\n\t\t\tcase 'H': // Hour in day (0-23)\n\t\t\t\treturn HOUR_OF_DAY_STRATEGY;\n\t\t\tcase 'K': // Hour in am/pm (0-11)\n\t\t\t\treturn HOUR_STRATEGY;\n\t\t\tcase 'M':\n\t\t\t\treturn width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;\n\t\t\tcase 'S':\n\t\t\t\treturn MILLISECOND_STRATEGY;\n\t\t\tcase 'W':\n\t\t\t\treturn WEEK_OF_MONTH_STRATEGY;\n\t\t\tcase 'a':\n\t\t\t\treturn getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);\n\t\t\tcase 'd':\n\t\t\t\treturn DAY_OF_MONTH_STRATEGY;\n\t\t\tcase 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0\n\t\t\t\treturn HOUR12_STRATEGY;\n\t\t\tcase 'k': // Hour in day (1-24), i.e. midnight is 24, not 0\n\t\t\t\treturn HOUR24_OF_DAY_STRATEGY;\n\t\t\tcase 'm':\n\t\t\t\treturn MINUTE_STRATEGY;\n\t\t\tcase 's':\n\t\t\t\treturn SECOND_STRATEGY;\n\t\t\tcase 'u':\n\t\t\t\treturn DAY_OF_WEEK_STRATEGY;\n\t\t\tcase 'w':\n\t\t\t\treturn WEEK_OF_YEAR_STRATEGY;\n\t\t\tcase 'y':\n\t\t\tcase 'Y':\n\t\t\t\treturn width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;\n\t\t\tcase 'X':\n\t\t\t\treturn ISO8601TimeZoneStrategy.getStrategy(width);\n\t\t\tcase 'Z':\n\t\t\t\tif (width == 2) {\n\t\t\t\t\treturn ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;\n\t\t\t\t}\n\t\t\t\t//$FALL-THROUGH$\n\t\t\tcase 'z':\n\t\t\t\treturn getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);\n\t\t}\n\t}\n\n\t@SuppressWarnings(\"unchecked\") // OK because we are creating an array with no entries\n\tprivate static final ConcurrentMap<Locale, Strategy>[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT];\n\n\t/**\n\t * Get a cache of Strategies for a particular field\n\t *\n\t * @param field The Calendar field\n\t * @return a cache of Locale to Strategy\n\t */\n\tprivate static ConcurrentMap<Locale, Strategy> getCache(final int field) {\n\t\tsynchronized (CACHES) {\n\t\t\tif (CACHES[field] == null) {\n\t\t\t\tCACHES[field] = new SafeConcurrentHashMap<>(3);\n\t\t\t}\n\t\t\treturn CACHES[field];\n\t\t}\n\t}\n\n\t/**\n\t * Construct a Strategy that parses a Text field\n\t *\n\t * @param field            The Calendar field\n\t * @param definingCalendar The calendar to obtain the short and long values\n\t * @return a TextStrategy for the field and Locale\n\t */\n\tprivate Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {\n\t\tfinal ConcurrentMap<Locale, Strategy> cache = getCache(field);\n\t\tStrategy strategy = cache.get(locale);\n\t\tif (strategy == null) {\n\t\t\tstrategy = field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);\n\t\t\tfinal Strategy inCache = cache.putIfAbsent(locale, strategy);\n\t\t\tif (inCache != null) {\n\t\t\t\treturn inCache;\n\t\t\t}\n\t\t}\n\t\treturn strategy;\n\t}\n\n\t/**\n\t * A strategy that copies the static or quoted field in the parsing pattern\n\t */\n\tprivate static class CopyQuotedStrategy extends Strategy {\n\n\t\tfinal private String formatField;\n\n\t\t/**\n\t\t * Construct a Strategy that ensures the formatField has literal text\n\t\t *\n\t\t * @param formatField The literal text to match\n\t\t */\n\t\tCopyQuotedStrategy(final String formatField) {\n\t\t\tthis.formatField = formatField;\n\t\t}\n\n\t\t@Override\n\t\tboolean isNumber() {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tboolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {\n\t\t\tfor (int idx = 0; idx < formatField.length(); ++idx) {\n\t\t\t\tfinal int sIdx = idx + pos.getIndex();\n\t\t\t\tif (sIdx == source.length()) {\n\t\t\t\t\tpos.setErrorIndex(sIdx);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (formatField.charAt(idx) != source.charAt(sIdx)) {\n\t\t\t\t\tpos.setErrorIndex(sIdx);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tpos.setIndex(formatField.length() + pos.getIndex());\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t/**\n\t * A strategy that handles a text field in the parsing pattern\n\t */\n\tprivate static class CaseInsensitiveTextStrategy extends PatternStrategy {\n\t\tprivate final int field;\n\t\tfinal Locale locale;\n\t\tprivate final Map<String, Integer> lKeyValues;\n\n\t\t/**\n\t\t * Construct a Strategy that parses a Text field\n\t\t *\n\t\t * @param field            The Calendar field\n\t\t * @param definingCalendar The Calendar to use\n\t\t * @param locale           The Locale to use\n\t\t */\n\t\tCaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {\n\t\t\tthis.field = field;\n\t\t\tthis.locale = locale;\n\n\t\t\tfinal StringBuilder regex = new StringBuilder();\n\t\t\tregex.append(\"((?iu)\");\n\t\t\tlKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);\n\t\t\tregex.setLength(regex.length() - 1);\n\t\t\tregex.append(\")\");\n\t\t\tcreatePattern(regex);\n\t\t}\n\n\t\t@Override\n\t\tvoid setCalendar(final FastDateParser parser, final Calendar cal, final String value) {\n\t\t\tfinal Integer iVal = lKeyValues.get(value.toLowerCase(locale));\n\t\t\tcal.set(field, iVal);\n\t\t}\n\t}\n\n\t/**\n\t * A strategy that handles a number field in the parsing pattern\n\t */\n\tprivate static class NumberStrategy extends Strategy {\n\t\tprivate final int field;\n\n\t\t/**\n\t\t * Construct a Strategy that parses a Number field\n\t\t *\n\t\t * @param field The Calendar field\n\t\t */\n\t\tNumberStrategy(final int field) {\n\t\t\tthis.field = field;\n\t\t}\n\n\t\t@Override\n\t\tboolean isNumber() {\n\t\t\treturn true;\n\t\t}\n\n\t\t@Override\n\t\tboolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {\n\t\t\tint idx = pos.getIndex();\n\t\t\tint last = source.length();\n\n\t\t\tif (maxWidth == 0) {\n\t\t\t\t// if no maxWidth, strip leading white space\n\t\t\t\tfor (; idx < last; ++idx) {\n\t\t\t\t\tfinal char c = source.charAt(idx);\n\t\t\t\t\tif (!Character.isWhitespace(c)) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tpos.setIndex(idx);\n\t\t\t} else {\n\t\t\t\tfinal int end = idx + maxWidth;\n\t\t\t\tif (last > end) {\n\t\t\t\t\tlast = end;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (; idx < last; ++idx) {\n\t\t\t\tfinal char c = source.charAt(idx);\n\t\t\t\tif (!Character.isDigit(c)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (pos.getIndex() == idx) {\n\t\t\t\tpos.setErrorIndex(idx);\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tfinal int value = Integer.parseInt(source.substring(pos.getIndex(), idx));\n\t\t\tpos.setIndex(idx);\n\n\t\t\tcalendar.set(field, modify(parser, value));\n\t\t\treturn true;\n\t\t}\n\n\t\t/**\n\t\t * Make any modifications to parsed integer\n\t\t *\n\t\t * @param parser The parser\n\t\t * @param iValue The parsed integer\n\t\t * @return The modified value\n\t\t */\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue;\n\t\t}\n\n\t}\n\n\tprivate static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {\n\t\t@Override\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue < 100 ? parser.adjustYear(iValue) : iValue;\n\t\t}\n\t};\n\n\t/**\n\t * A strategy that handles a timezone field in the parsing pattern\n\t */\n\tstatic class TimeZoneStrategy extends PatternStrategy {\n\t\tprivate static final String RFC_822_TIME_ZONE = \"[+-]\\\\d{4}\";\n\t\tprivate static final String UTC_TIME_ZONE_WITH_OFFSET = \"[+-]\\\\d{2}:\\\\d{2}\";\n\t\tprivate static final String GMT_OPTION = \"GMT[+-]\\\\d{1,2}:\\\\d{2}\";\n\n\t\tprivate final Locale locale;\n\t\tprivate final Map<String, TzInfo> tzNames = new HashMap<>();\n\n\t\tprivate static class TzInfo {\n\t\t\tTimeZone zone;\n\t\t\tint dstOffset;\n\n\t\t\tTzInfo(final TimeZone tz, final boolean useDst) {\n\t\t\t\tzone = tz;\n\t\t\t\tdstOffset = useDst ? tz.getDSTSavings() : 0;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Index of zone id\n\t\t */\n\t\tprivate static final int ID = 0;\n\n\t\t/**\n\t\t * Tests whether to skip the given time zone, true if TimeZone.getTimeZone().\n\t\t * <p>\n\t\t * On Java 25 and up, skips short IDs if {@code ignoreTimeZoneShortIDs} is true.\n\t\t * </p>\n\t\t * <p>\n\t\t * This method is package private only for testing.\n\t\t * </p>\n\t\t *\n\t\t * @param tzId the ID to test.\n\t\t * @return Whether to skip the given time zone ID.\n\t\t */\n\t\tstatic boolean skipTimeZone(final String tzId) {\n\t\t\treturn TimeZoneUtil.GMT_ID.equalsIgnoreCase(tzId);\n\t\t}\n\n\t\t/**\n\t\t * Construct a Strategy that parses a TimeZone\n\t\t *\n\t\t * @param locale The Locale\n\t\t */\n\t\tTimeZoneStrategy(final Locale locale) {\n\t\t\tthis.locale = locale;\n\n\t\t\tfinal StringBuilder sb = new StringBuilder();\n\t\t\tsb.append(\"((?iu)\" + RFC_822_TIME_ZONE + \"|\" + UTC_TIME_ZONE_WITH_OFFSET + \"|\" + GMT_OPTION);\n\n\t\t\tfinal Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);\n\n\t\t\tfinal String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();\n\t\t\tfor (final String[] zoneNames : zones) {\n\t\t\t\t// offset 0 is the time zone ID and is not localized\n\t\t\t\tfinal String tzId = zoneNames[ID];\n\t\t\t\tif (skipTimeZone(tzId)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfinal TimeZone tz = TimeZoneUtil.getTimeZone(tzId);\n\t\t\t\t// offset 1 is long standard name\n\t\t\t\t// offset 2 is short standard name\n\t\t\t\tfinal TzInfo standard = new TzInfo(tz, false);\n\t\t\t\tTzInfo tzInfo = standard;\n\t\t\t\tfor (int i = 1; i < zoneNames.length; ++i) {\n\t\t\t\t\tswitch (i) {\n\t\t\t\t\t\tcase 3: // offset 3 is long daylight savings (or summertime) name\n\t\t\t\t\t\t\t// offset 4 is the short summertime name\n\t\t\t\t\t\t\ttzInfo = new TzInfo(tz, true);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 5: // offset 5 starts additional names, probably standard time\n\t\t\t\t\t\t\ttzInfo = standard;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (zoneNames[i] != null) {\n\t\t\t\t\t\tfinal String key = zoneNames[i].toLowerCase(locale);\n\t\t\t\t\t\t// ignore the data associated with duplicates supplied in\n\t\t\t\t\t\t// the additional names\n\t\t\t\t\t\tif (sorted.add(key)) {\n\t\t\t\t\t\t\ttzNames.put(key, tzInfo);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// order the regex alternatives with longer strings first, greedy\n\t\t\t// match will ensure longest string will be consumed\n\t\t\tfor (final String zoneName : sorted) {\n\t\t\t\tsimpleQuote(sb.append('|'), zoneName);\n\t\t\t}\n\t\t\tsb.append(\")\");\n\t\t\tcreatePattern(sb);\n\t\t}\n\n\t\t@Override\n\t\tvoid setCalendar(final FastDateParser parser, final Calendar cal, final String value) {\n\t\t\tif (value.charAt(0) == '+' || value.charAt(0) == '-') {\n\t\t\t\tfinal TimeZone tz = TimeZoneUtil.getTimeZone(\"GMT\" + value);\n\t\t\t\tcal.setTimeZone(tz);\n\t\t\t} else if (value.regionMatches(true, 0, \"GMT\", 0, 3)) {\n\t\t\t\tfinal TimeZone tz = TimeZoneUtil.getTimeZone(value.toUpperCase());\n\t\t\t\tcal.setTimeZone(tz);\n\t\t\t} else {\n\t\t\t\tfinal TzInfo tzInfo = tzNames.get(value.toLowerCase(locale));\n\t\t\t\tcal.set(Calendar.DST_OFFSET, tzInfo.dstOffset);\n\t\t\t\t//issue#I1AXIN@Gitee\n//\t\t\t\tcal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());\n\t\t\t\tcal.set(Calendar.ZONE_OFFSET, parser.getTimeZone().getRawOffset());\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate static class ISO8601TimeZoneStrategy extends PatternStrategy {\n\t\t// Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm\n\n\t\t/**\n\t\t * Construct a Strategy that parses a TimeZone\n\t\t *\n\t\t * @param pattern The Pattern\n\t\t */\n\t\tISO8601TimeZoneStrategy(final String pattern) {\n\t\t\tcreatePattern(pattern);\n\t\t}\n\n\t\t@Override\n\t\tvoid setCalendar(final FastDateParser parser, final Calendar cal, final String value) {\n\t\t\tif (Objects.equals(value, \"Z\")) {\n\t\t\t\tcal.setTimeZone(TimeZoneUtil.getTimeZone(\"UTC\"));\n\t\t\t} else {\n\t\t\t\tcal.setTimeZone(TimeZoneUtil.getTimeZone(\"GMT\" + value));\n\t\t\t}\n\t\t}\n\n\t\tprivate static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy(\"(Z|(?:[+-]\\\\d{2}))\");\n\t\tprivate static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy(\"(Z|(?:[+-]\\\\d{2}\\\\d{2}))\");\n\t\tprivate static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy(\"(Z|(?:[+-]\\\\d{2}(?::)\\\\d{2}))\");\n\n\t\t/**\n\t\t * Factory method for ISO8601TimeZoneStrategies.\n\t\t *\n\t\t * @param tokenLen a token indicating the length of the TimeZone String to be formatted.\n\t\t * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException will be thrown.\n\t\t */\n\t\tstatic Strategy getStrategy(final int tokenLen) {\n\t\t\tswitch (tokenLen) {\n\t\t\t\tcase 1:\n\t\t\t\t\treturn ISO_8601_1_STRATEGY;\n\t\t\t\tcase 2:\n\t\t\t\t\treturn ISO_8601_2_STRATEGY;\n\t\t\t\tcase 3:\n\t\t\t\t\treturn ISO_8601_3_STRATEGY;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new IllegalArgumentException(\"invalid number of X\");\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {\n\t\t@Override\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue - 1;\n\t\t}\n\t};\n\tprivate static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);\n\tprivate static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);\n\tprivate static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);\n\tprivate static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);\n\tprivate static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);\n\tprivate static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {\n\t\t@Override\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue != 7 ? iValue + 1 : Calendar.SUNDAY;\n\t\t}\n\t};\n\tprivate static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);\n\tprivate static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);\n\tprivate static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {\n\t\t@Override\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue == 24 ? 0 : iValue;\n\t\t}\n\t};\n\tprivate static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {\n\t\t@Override\n\t\tint modify(final FastDateParser parser, final int iValue) {\n\t\t\treturn iValue == 12 ? 0 : iValue;\n\t\t}\n\t};\n\tprivate static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);\n\tprivate static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);\n\tprivate static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);\n\tprivate static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java",
    "content": "package cn.hutool.core.date.format;\n\nimport cn.hutool.core.date.DateException;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.text.DateFormatSymbols;\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * {@link java.text.SimpleDateFormat} 的线程安全版本，用于将 {@link Date} 格式化输出<br>\n * Thanks to Apache Commons Lang 3.5\n *\n * @see FastDateParser\n */\npublic class FastDatePrinter extends AbstractDateBasic implements DatePrinter {\n\tprivate static final long serialVersionUID = -6305750172255764887L;\n\n\t/**\n\t * 规则列表.\n\t */\n\tprivate transient Rule[] rules;\n\t/**\n\t * 估算最大长度.\n\t */\n\tprivate transient int mMaxLengthEstimate;\n\n\t// Constructor\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 构造，内部使用<br>\n\t *\n\t * @param pattern  使用{@link java.text.SimpleDateFormat} 相同的日期格式\n\t * @param timeZone 非空时区{@link TimeZone}\n\t * @param locale   非空{@link Locale} 日期地理位置\n\t */\n\tpublic FastDatePrinter(String pattern, TimeZone timeZone, Locale locale) {\n\t\tsuper(pattern, timeZone, locale);\n\t\tinit();\n\t}\n\n\t/**\n\t * 初始化\n\t */\n\tprivate void init() {\n\t\tfinal List<Rule> rulesList = parsePattern();\n\t\trules = rulesList.toArray(new Rule[0]);\n\n\t\tint len = 0;\n\t\tfor (int i = rules.length; --i >= 0; ) {\n\t\t\tlen += rules[i].estimateLength();\n\t\t}\n\n\t\tmMaxLengthEstimate = len;\n\t}\n\n\t// Parse the pattern\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * <p>\n\t * Returns a list of Rules given a pattern.\n\t * </p>\n\t *\n\t * @return a {@code List} of Rule objects\n\t * @throws IllegalArgumentException if pattern is invalid\n\t */\n\tprotected List<Rule> parsePattern() {\n\t\tfinal DateFormatSymbols symbols = new DateFormatSymbols(locale);\n\t\tfinal List<Rule> rules = new ArrayList<>();\n\n\t\tfinal String[] ERAs = symbols.getEras();\n\t\tfinal String[] months = symbols.getMonths();\n\t\tfinal String[] shortMonths = symbols.getShortMonths();\n\t\tfinal String[] weekdays = symbols.getWeekdays();\n\t\tfinal String[] shortWeekdays = symbols.getShortWeekdays();\n\t\tfinal String[] AmPmStrings = symbols.getAmPmStrings();\n\n\t\tfinal int length = pattern.length();\n\t\tfinal int[] indexRef = new int[1];\n\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tindexRef[0] = i;\n\t\t\tfinal String token = parseToken(pattern, indexRef);\n\t\t\ti = indexRef[0];\n\n\t\t\tfinal int tokenLen = token.length();\n\t\t\tif (tokenLen == 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tRule rule;\n\t\t\tfinal char c = token.charAt(0);\n\n\t\t\tswitch (c) {\n\t\t\t\tcase 'G': // era designator (text)\n\t\t\t\t\trule = new TextField(Calendar.ERA, ERAs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'y': // year (number)\n\t\t\t\tcase 'Y': // week year\n\t\t\t\t\tif (tokenLen == 2) {\n\t\t\t\t\t\trule = TwoDigitYearField.INSTANCE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4));\n\t\t\t\t\t}\n\t\t\t\t\tif (c == 'Y') {\n\t\t\t\t\t\trule = new WeekYear((NumberRule) rule);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'M': // month in year (text and number)\n\t\t\t\t\tif (tokenLen >= 4) {\n\t\t\t\t\t\trule = new TextField(Calendar.MONTH, months);\n\t\t\t\t\t} else if (tokenLen == 3) {\n\t\t\t\t\t\trule = new TextField(Calendar.MONTH, shortMonths);\n\t\t\t\t\t} else if (tokenLen == 2) {\n\t\t\t\t\t\trule = TwoDigitMonthField.INSTANCE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trule = UnpaddedMonthField.INSTANCE;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'd': // day in month (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'h': // hour in am/pm (number, 1..12)\n\t\t\t\t\trule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'H': // hour in day (number, 0..23)\n\t\t\t\t\trule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'm': // minute in hour (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.MINUTE, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 's': // second in minute (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.SECOND, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'S': // millisecond (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.MILLISECOND, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'E': // day in week (text)\n\t\t\t\t\trule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'u': // day in week (number)\n\t\t\t\t\trule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'D': // day in year (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'F': // day of week in month (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'w': // week in year (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'W': // week in month (number)\n\t\t\t\t\trule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'a': // am/pm marker (text)\n\t\t\t\t\trule = new TextField(Calendar.AM_PM, AmPmStrings);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'k': // hour in day (1..24)\n\t\t\t\t\trule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'K': // hour in am/pm (0..11)\n\t\t\t\t\trule = selectNumberRule(Calendar.HOUR, tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'X': // ISO 8601\n\t\t\t\t\trule = Iso8601_Rule.getRule(tokenLen);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'z': // time zone (text)\n\t\t\t\t\tif (tokenLen >= 4) {\n\t\t\t\t\t\trule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG);\n\t\t\t\t\t} else {\n\t\t\t\t\t\trule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Z': // time zone (value)\n\t\t\t\t\tif (tokenLen == 1) {\n\t\t\t\t\t\trule = TimeZoneNumberRule.INSTANCE_NO_COLON;\n\t\t\t\t\t} else if (tokenLen == 2) {\n\t\t\t\t\t\trule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trule = TimeZoneNumberRule.INSTANCE_COLON;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase '\\'': // literal text\n\t\t\t\t\tfinal String sub = token.substring(1);\n\t\t\t\t\tif (sub.length() == 1) {\n\t\t\t\t\t\trule = new CharacterLiteral(sub.charAt(0));\n\t\t\t\t\t} else {\n\t\t\t\t\t\trule = new StringLiteral(sub);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new IllegalArgumentException(\"Illegal pattern component: \" + token);\n\t\t\t}\n\n\t\t\trules.add(rule);\n\t\t}\n\n\t\treturn rules;\n\t}\n\n\t/**\n\t * <p>\n\t * Performs the parsing of tokens.\n\t * </p>\n\t *\n\t * @param pattern  the pattern\n\t * @param indexRef index references\n\t * @return parsed token\n\t */\n\tprotected String parseToken(String pattern, int[] indexRef) {\n\t\tfinal StringBuilder buf = new StringBuilder();\n\n\t\tint i = indexRef[0];\n\t\tfinal int length = pattern.length();\n\n\t\tchar c = pattern.charAt(i);\n\t\tif (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {\n\t\t\t// Scan a run of the same character, which indicates a time\n\t\t\t// pattern.\n\t\t\tbuf.append(c);\n\n\t\t\twhile (i + 1 < length) {\n\t\t\t\tfinal char peek = pattern.charAt(i + 1);\n\t\t\t\tif (peek == c) {\n\t\t\t\t\tbuf.append(c);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// This will identify token as text.\n\t\t\tbuf.append('\\'');\n\n\t\t\tboolean inLiteral = false;\n\n\t\t\tfor (; i < length; i++) {\n\t\t\t\tc = pattern.charAt(i);\n\n\t\t\t\tif (c == '\\'') {\n\t\t\t\t\tif (i + 1 < length && pattern.charAt(i + 1) == '\\'') {\n\t\t\t\t\t\t// '' is treated as escaped '\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tbuf.append(c);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tinLiteral = !inLiteral;\n\t\t\t\t\t}\n\t\t\t\t} else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {\n\t\t\t\t\ti--;\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\tbuf.append(c);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tindexRef[0] = i;\n\t\treturn buf.toString();\n\t}\n\n\t/**\n\t * <p>\n\t * Gets an appropriate rule for the padding required.\n\t * </p>\n\t *\n\t * @param field   the field to get a rule for\n\t * @param padding the padding required\n\t * @return a new rule with the correct padding\n\t */\n\tprotected NumberRule selectNumberRule(int field, int padding) {\n\t\tswitch (padding) {\n\t\t\tcase 1:\n\t\t\t\treturn new UnpaddedNumberField(field);\n\t\t\tcase 2:\n\t\t\t\treturn new TwoDigitNumberField(field);\n\t\t\tdefault:\n\t\t\t\treturn new PaddedNumberField(field, padding);\n\t\t}\n\t}\n\n\t// Format methods\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * <p>\n\t * Formats a {@code Date}, {@code Calendar} or {@code Long} (milliseconds) object.\n\t * </p>\n\t *\n\t * @param obj the object to format\n\t * @return The formatted value.\n\t */\n\tString format(Object obj) {\n\t\tif (obj instanceof Date) {\n\t\t\treturn format((Date) obj);\n\t\t} else if (obj instanceof Calendar) {\n\t\t\treturn format((Calendar) obj);\n\t\t} else if (obj instanceof Long) {\n\t\t\treturn format(((Long) obj).longValue());\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"Unknown class: \" + (obj == null ? \"<null>\" : obj.getClass().getName()));\n\t\t}\n\t}\n\n\t@Override\n\tpublic String format(long millis) {\n\t\tfinal Calendar c = Calendar.getInstance(timeZone, locale);\n\t\tc.setTimeInMillis(millis);\n\t\treturn applyRulesToString(c);\n\t}\n\n\t@Override\n\tpublic String format(Date date) {\n\t\tfinal Calendar c = Calendar.getInstance(timeZone, locale);\n\t\tc.setTime(date);\n\t\treturn applyRulesToString(c);\n\t}\n\n\t@Override\n\tpublic String format(Calendar calendar) {\n\t\treturn format(calendar, new StringBuilder(mMaxLengthEstimate)).toString();\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(long millis, B buf) {\n\t\tfinal Calendar c = Calendar.getInstance(timeZone, locale);\n\t\tc.setTimeInMillis(millis);\n\t\treturn applyRules(c, buf);\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(Date date, B buf) {\n\t\tfinal Calendar c = Calendar.getInstance(timeZone, locale);\n\t\tc.setTime(date);\n\t\treturn applyRules(c, buf);\n\t}\n\n\t@Override\n\tpublic <B extends Appendable> B format(Calendar calendar, B buf) {\n\t\t// do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored\n\t\tif (!calendar.getTimeZone().equals(timeZone)) {\n\t\t\tcalendar = (Calendar) calendar.clone();\n\t\t\tcalendar.setTimeZone(timeZone);\n\t\t}\n\t\treturn applyRules(calendar, buf);\n\t}\n\n\t/**\n\t * Creates a String representation of the given Calendar by applying the rules of this printer to it.\n\t *\n\t * @param c the Calender to apply the rules to.\n\t * @return a String representation of the given Calendar.\n\t */\n\tprivate String applyRulesToString(Calendar c) {\n\t\treturn applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();\n\t}\n\n\t/**\n\t * <p>\n\t * Performs the formatting by applying the rules to the specified calendar.\n\t * </p>\n\t *\n\t * @param calendar the calendar to format\n\t * @param buf      the buffer to format into\n\t * @param <B>      the Appendable class type, usually StringBuilder or StringBuffer.\n\t * @return the specified string buffer\n\t */\n\tprivate <B extends Appendable> B applyRules(Calendar calendar, B buf) {\n\t\ttry {\n\t\t\tfor (final Rule rule : this.rules) {\n\t\t\t\trule.appendTo(buf, calendar);\n\t\t\t}\n\t\t} catch (final IOException e) {\n\t\t\tthrow new DateException(e);\n\t\t}\n\t\treturn buf;\n\t}\n\n\t/**\n\t * 估算生成的日期字符串长度<br>\n\t * 实际生成的字符串长度小于或等于此值\n\t *\n\t * @return 日期字符串长度\n\t */\n\tpublic int getMaxLengthEstimate() {\n\t\treturn mMaxLengthEstimate;\n\t}\n\n\t// Serializing\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * Create the object after serialization. This implementation reinitializes the transient properties.\n\t *\n\t * @param in ObjectInputStream from which the object is being deserialized.\n\t * @throws IOException            if there is an IO issue.\n\t * @throws ClassNotFoundException if a class cannot be found.\n\t */\n\tprivate void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {\n\t\tin.defaultReadObject();\n\t\tinit();\n\t}\n\n\t/**\n\t * Appends two digits to the given buffer.\n\t *\n\t * @param buffer the buffer to append to.\n\t * @param value  the value to append digits from.\n\t */\n\tprivate static void appendDigits(Appendable buffer, int value) throws IOException {\n\t\tbuffer.append((char) (value / 10 + '0'));\n\t\tbuffer.append((char) (value % 10 + '0'));\n\t}\n\n\tprivate static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3\n\n\t/**\n\t * Appends all digits to the given buffer.\n\t *\n\t * @param buffer the buffer to append to.\n\t * @param value  the value to append digits from.\n\t */\n\tprivate static void appendFullDigits(Appendable buffer, int value, int minFieldWidth) throws IOException {\n\t\t// specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array\n\t\t// see LANG-1248\n\t\tif (value < 10000) {\n\t\t\t// less memory allocation path works for four digits or less\n\n\t\t\tint nDigits = 4;\n\t\t\tif (value < 1000) {\n\t\t\t\t--nDigits;\n\t\t\t\tif (value < 100) {\n\t\t\t\t\t--nDigits;\n\t\t\t\t\tif (value < 10) {\n\t\t\t\t\t\t--nDigits;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// left zero pad\n\t\t\tfor (int i = minFieldWidth - nDigits; i > 0; --i) {\n\t\t\t\tbuffer.append('0');\n\t\t\t}\n\n\t\t\tswitch (nDigits) {\n\t\t\t\tcase 4:\n\t\t\t\t\tbuffer.append((char) (value / 1000 + '0'));\n\t\t\t\t\tvalue %= 1000;\n\t\t\t\tcase 3:\n\t\t\t\t\tif (value >= 100) {\n\t\t\t\t\t\tbuffer.append((char) (value / 100 + '0'));\n\t\t\t\t\t\tvalue %= 100;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuffer.append('0');\n\t\t\t\t\t}\n\t\t\t\tcase 2:\n\t\t\t\t\tif (value >= 10) {\n\t\t\t\t\t\tbuffer.append((char) (value / 10 + '0'));\n\t\t\t\t\t\tvalue %= 10;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuffer.append('0');\n\t\t\t\t\t}\n\t\t\t\tcase 1:\n\t\t\t\t\tbuffer.append((char) (value + '0'));\n\t\t\t}\n\t\t} else {\n\t\t\t// more memory allocation path works for any digits\n\n\t\t\t// build up decimal representation in reverse\n\t\t\tfinal char[] work = new char[MAX_DIGITS];\n\t\t\tint digit = 0;\n\t\t\twhile (value != 0) {\n\t\t\t\twork[digit++] = (char) (value % 10 + '0');\n\t\t\t\tvalue = value / 10;\n\t\t\t}\n\n\t\t\t// pad with zeros\n\t\t\twhile (digit < minFieldWidth) {\n\t\t\t\tbuffer.append('0');\n\t\t\t\t--minFieldWidth;\n\t\t\t}\n\n\t\t\t// reverse\n\t\t\twhile (--digit >= 0) {\n\t\t\t\tbuffer.append(work[digit]);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Rules\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 规则\n\t */\n\tprivate interface Rule {\n\t\t/**\n\t\t * Returns the estimated length of the result.\n\t\t *\n\t\t * @return the estimated length\n\t\t */\n\t\tint estimateLength();\n\n\t\t/**\n\t\t * Appends the value of the specified calendar to the output buffer based on the rule implementation.\n\t\t *\n\t\t * @param buf      the output buffer\n\t\t * @param calendar calendar to be appended\n\t\t * @throws IOException if an I/O error occurs\n\t\t */\n\t\tvoid appendTo(Appendable buf, Calendar calendar) throws IOException;\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class defining a numeric rule.\n\t * </p>\n\t */\n\tprivate interface NumberRule extends Rule {\n\t\t/**\n\t\t * Appends the specified value to the output buffer based on the rule implementation.\n\t\t *\n\t\t * @param buffer the output buffer\n\t\t * @param value  the value to be appended\n\t\t * @throws IOException if an I/O error occurs\n\t\t */\n\t\tvoid appendTo(Appendable buffer, int value) throws IOException;\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a constant single character.\n\t * </p>\n\t */\n\tprivate static class CharacterLiteral implements Rule {\n\t\tprivate final char mValue;\n\n\t\t/**\n\t\t * Constructs a new instance of {@code CharacterLiteral} to hold the specified value.\n\t\t *\n\t\t * @param value the character literal\n\t\t */\n\t\tCharacterLiteral(final char value) {\n\t\t\tmValue = value;\n\t\t}\n\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 1;\n\t\t}\n\n\t\t@Override\n\t\tpublic void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {\n\t\t\tbuffer.append(mValue);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a constant string.\n\t * </p>\n\t */\n\tprivate static class StringLiteral implements Rule {\n\t\tprivate final String mValue;\n\n\t\t/**\n\t\t * Constructs a new instance of {@code StringLiteral} to hold the specified value.\n\t\t *\n\t\t * @param value the string literal\n\t\t */\n\t\tStringLiteral(String value) {\n\t\t\tmValue = value;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mValue.length();\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tbuffer.append(mValue);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output one of a set of values.\n\t * </p>\n\t */\n\tprivate static class TextField implements Rule {\n\t\tprivate final int mField;\n\t\tprivate final String[] mValues;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TextField} with the specified field and values.\n\t\t *\n\t\t * @param field  the field\n\t\t * @param values the field values\n\t\t */\n\t\tTextField(int field, String[] values) {\n\t\t\tmField = field;\n\t\t\tmValues = values;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\tint max = 0;\n\t\t\tfor (int i = mValues.length; --i >= 0; ) {\n\t\t\t\tfinal int len = mValues[i].length();\n\t\t\t\tif (len > max) {\n\t\t\t\t\tmax = len;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn max;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tbuffer.append(mValues[calendar.get(mField)]);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output an unpadded number.\n\t * </p>\n\t */\n\tprivate static class UnpaddedNumberField implements NumberRule {\n\t\tprivate final int mField;\n\n\t\t/**\n\t\t * Constructs an instance of {@code UnpadedNumberField} with the specified field.\n\t\t *\n\t\t * @param field the field\n\t\t */\n\t\tUnpaddedNumberField(int field) {\n\t\t\tmField = field;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 4;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(mField));\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tif (value < 10) {\n\t\t\t\tbuffer.append((char) (value + '0'));\n\t\t\t} else if (value < 100) {\n\t\t\t\tappendDigits(buffer, value);\n\t\t\t} else {\n\t\t\t\tappendFullDigits(buffer, value, 1);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output an unpadded month.\n\t * </p>\n\t */\n\tprivate static class UnpaddedMonthField implements NumberRule {\n\t\tstatic final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();\n\n\t\t/**\n\t\t * Constructs an instance of {@code UnpaddedMonthField}.\n\t\t */\n\t\tUnpaddedMonthField() {\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 2;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(Calendar.MONTH) + 1);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tif (value < 10) {\n\t\t\t\tbuffer.append((char) (value + '0'));\n\t\t\t} else {\n\t\t\t\tappendDigits(buffer, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a padded number.\n\t * </p>\n\t */\n\tprivate static class PaddedNumberField implements NumberRule {\n\t\tprivate final int mField;\n\t\tprivate final int mSize;\n\n\t\t/**\n\t\t * Constructs an instance of {@code PaddedNumberField}.\n\t\t *\n\t\t * @param field the field\n\t\t * @param size  size of the output field\n\t\t */\n\t\tPaddedNumberField(int field, int size) {\n\t\t\tif (size < 3) {\n\t\t\t\t// Should use UnpaddedNumberField or TwoDigitNumberField.\n\t\t\t\tthrow new IllegalArgumentException();\n\t\t\t}\n\t\t\tmField = field;\n\t\t\tmSize = size;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mSize;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(mField));\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tappendFullDigits(buffer, value, mSize);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a two digit number.\n\t * </p>\n\t */\n\tprivate static class TwoDigitNumberField implements NumberRule {\n\t\tprivate final int mField;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TwoDigitNumberField} with the specified field.\n\t\t *\n\t\t * @param field the field\n\t\t */\n\t\tTwoDigitNumberField(int field) {\n\t\t\tmField = field;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 2;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(mField));\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tif (value < 100) {\n\t\t\t\tappendDigits(buffer, value);\n\t\t\t} else {\n\t\t\t\tappendFullDigits(buffer, value, 2);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a two digit year.\n\t * </p>\n\t */\n\tprivate static class TwoDigitYearField implements NumberRule {\n\t\tstatic final TwoDigitYearField INSTANCE = new TwoDigitYearField();\n\n\t\t/**\n\t\t * Constructs an instance of {@code TwoDigitYearField}.\n\t\t */\n\t\tTwoDigitYearField() {\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 2;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(Calendar.YEAR) % 100);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tappendDigits(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a two digit month.\n\t * </p>\n\t */\n\tprivate static class TwoDigitMonthField implements NumberRule {\n\t\tstatic final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();\n\n\t\t/**\n\t\t * Constructs an instance of {@code TwoDigitMonthField}.\n\t\t */\n\t\tTwoDigitMonthField() {\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 2;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tappendTo(buffer, calendar.get(Calendar.MONTH) + 1);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic final void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tappendDigits(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output the twelve hour field.\n\t * </p>\n\t */\n\tprivate static class TwelveHourField implements NumberRule {\n\t\tprivate final NumberRule mRule;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TwelveHourField} with the specified {@code NumberRule}.\n\t\t *\n\t\t * @param rule the rule\n\t\t */\n\t\tTwelveHourField(final NumberRule rule) {\n\t\t\tmRule = rule;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mRule.estimateLength();\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tint value = calendar.get(Calendar.HOUR);\n\t\t\tif (value == 0) {\n\t\t\t\tvalue = calendar.getLeastMaximum(Calendar.HOUR) + 1;\n\t\t\t}\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output the twenty four hour field.\n\t * </p>\n\t */\n\tprivate static class TwentyFourHourField implements NumberRule {\n\t\tprivate final NumberRule mRule;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TwentyFourHourField} with the specified {@code NumberRule}.\n\t\t *\n\t\t * @param rule the rule\n\t\t */\n\t\tTwentyFourHourField(NumberRule rule) {\n\t\t\tmRule = rule;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mRule.estimateLength();\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tint value = calendar.get(Calendar.HOUR_OF_DAY);\n\t\t\tif (value == 0) {\n\t\t\t\tvalue = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;\n\t\t\t}\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output the numeric day in week.\n\t * </p>\n\t */\n\tprivate static class DayInWeekField implements NumberRule {\n\t\tprivate final NumberRule mRule;\n\n\t\tDayInWeekField(NumberRule rule) {\n\t\t\tmRule = rule;\n\t\t}\n\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mRule.estimateLength();\n\t\t}\n\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tfinal int value = calendar.get(Calendar.DAY_OF_WEEK);\n\t\t\tmRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7);\n\t\t}\n\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output the numeric day in week.\n\t * </p>\n\t */\n\tprivate static class WeekYear implements NumberRule {\n\t\tprivate final NumberRule mRule;\n\n\t\tWeekYear(final NumberRule rule) {\n\t\t\tmRule = rule;\n\t\t}\n\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn mRule.estimateLength();\n\t\t}\n\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tint weekYear = calendar.getWeekYear();\n\t\t\tif (mRule instanceof TwoDigitYearField) {\n\t\t\t\t// issue#3641\n\t\t\t\tweekYear %= 100;\n\t\t\t}\n\t\t\tmRule.appendTo(buffer, weekYear);\n\t\t}\n\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, int value) throws IOException {\n\t\t\tmRule.appendTo(buffer, value);\n\t\t}\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\tprivate static final ConcurrentMap<TimeZoneDisplayKey, String> C_TIME_ZONE_DISPLAY_CACHE = new SafeConcurrentHashMap<>(7);\n\n\t/**\n\t * <p>\n\t * Gets the time zone display name, using a cache for performance.\n\t * </p>\n\t *\n\t * @param tz       the zone to query\n\t * @param daylight true if daylight savings\n\t * @param style    the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}\n\t * @param locale   the locale to use\n\t * @return the textual name of the time zone\n\t */\n\tstatic String getTimeZoneDisplay(TimeZone tz, boolean daylight, int style, Locale locale) {\n\t\tfinal TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);\n\t\tString value = C_TIME_ZONE_DISPLAY_CACHE.get(key);\n\t\tif (value == null) {\n\t\t\t// This is a very slow call, so cache the results.\n\t\t\tvalue = tz.getDisplayName(daylight, style, locale);\n\t\t\tfinal String prior = C_TIME_ZONE_DISPLAY_CACHE.putIfAbsent(key, value);\n\t\t\tif (prior != null) {\n\t\t\t\tvalue = prior;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a time zone name.\n\t * </p>\n\t */\n\tprivate static class TimeZoneNameRule implements Rule {\n\t\tprivate final Locale mLocale;\n\t\tprivate final int mStyle;\n\t\tprivate final String mStandard;\n\t\tprivate final String mDaylight;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TimeZoneNameRule} with the specified properties.\n\t\t *\n\t\t * @param timeZone the time zone\n\t\t * @param locale   the locale\n\t\t * @param style    the style\n\t\t */\n\t\tTimeZoneNameRule(TimeZone timeZone, Locale locale, int style) {\n\t\t\tmLocale = locale;\n\t\t\tmStyle = style;\n\n\t\t\tmStandard = getTimeZoneDisplay(timeZone, false, style, locale);\n\t\t\tmDaylight = getTimeZoneDisplay(timeZone, true, style, locale);\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\t// We have no access to the Calendar object that will be passed to\n\t\t\t// appendTo so base estimate on the TimeZone passed to the\n\t\t\t// constructor\n\t\t\treturn Math.max(mStandard.length(), mDaylight.length());\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\t\t\tfinal TimeZone zone = calendar.getTimeZone();\n\t\t\tif (calendar.get(Calendar.DST_OFFSET) != 0) {\n\t\t\t\tbuffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));\n\t\t\t} else {\n\t\t\t\tbuffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}.\n\t * </p>\n\t */\n\tprivate static class TimeZoneNumberRule implements Rule {\n\t\tstatic final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);\n\t\tstatic final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);\n\n\t\tfinal boolean mColon;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.\n\t\t *\n\t\t * @param colon add colon between HH and MM in the output if {@code true}\n\t\t */\n\t\tTimeZoneNumberRule(boolean colon) {\n\t\t\tmColon = colon;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn 5;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(Appendable buffer, Calendar calendar) throws IOException {\n\n\t\t\tint offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);\n\n\t\t\tif (offset < 0) {\n\t\t\t\tbuffer.append('-');\n\t\t\t\toffset = -offset;\n\t\t\t} else {\n\t\t\t\tbuffer.append('+');\n\t\t\t}\n\n\t\t\tfinal int hours = offset / (60 * 60 * 1000);\n\t\t\tappendDigits(buffer, hours);\n\n\t\t\tif (mColon) {\n\t\t\t\tbuffer.append(':');\n\t\t\t}\n\n\t\t\tfinal int minutes = offset / (60 * 1000) - 60 * hours;\n\t\t\tappendDigits(buffer, minutes);\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}.\n\t * </p>\n\t */\n\tprivate static class Iso8601_Rule implements Rule {\n\n\t\t// Sign TwoDigitHours or Z\n\t\tstatic final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);\n\t\t// Sign TwoDigitHours Minutes or Z\n\t\tstatic final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);\n\t\t// Sign TwoDigitHours : Minutes or Z\n\t\tstatic final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);\n\n\t\t/**\n\t\t * Factory method for Iso8601_Rules.\n\t\t *\n\t\t * @param tokenLen a token indicating the length of the TimeZone String to be formatted.\n\t\t * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such rule exists, an IllegalArgumentException will be thrown.\n\t\t */\n\t\tstatic Iso8601_Rule getRule(int tokenLen) {\n\t\t\tswitch (tokenLen) {\n\t\t\t\tcase 1:\n\t\t\t\t\treturn Iso8601_Rule.ISO8601_HOURS;\n\t\t\t\tcase 2:\n\t\t\t\t\treturn Iso8601_Rule.ISO8601_HOURS_MINUTES;\n\t\t\t\tcase 3:\n\t\t\t\t\treturn Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new IllegalArgumentException(\"invalid number of X\");\n\t\t\t}\n\t\t}\n\n\t\tfinal int length;\n\n\t\t/**\n\t\t * Constructs an instance of {@code Iso8601_Rule} with the specified properties.\n\t\t *\n\t\t * @param length The number of characters in output (unless Z is output)\n\t\t */\n\t\tIso8601_Rule(int length) {\n\t\t\tthis.length = length;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int estimateLength() {\n\t\t\treturn length;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {\n\t\t\tint offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);\n\t\t\tif (offset == 0) {\n\t\t\t\tbuffer.append(\"Z\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (offset < 0) {\n\t\t\t\tbuffer.append('-');\n\t\t\t\toffset = -offset;\n\t\t\t} else {\n\t\t\t\tbuffer.append('+');\n\t\t\t}\n\n\t\t\tfinal int hours = offset / (60 * 60 * 1000);\n\t\t\tappendDigits(buffer, hours);\n\n\t\t\tif (length < 5) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (length == 6) {\n\t\t\t\tbuffer.append(':');\n\t\t\t}\n\n\t\t\tfinal int minutes = offset / (60 * 1000) - 60 * hours;\n\t\t\tappendDigits(buffer, minutes);\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------\n\n\t/**\n\t * <p>\n\t * Inner class that acts as a compound key for time zone names.\n\t * </p>\n\t */\n\tprivate static class TimeZoneDisplayKey {\n\t\tprivate final TimeZone mTimeZone;\n\t\tprivate final int mStyle;\n\t\tprivate final Locale mLocale;\n\n\t\t/**\n\t\t * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.\n\t\t *\n\t\t * @param timeZone the time zone\n\t\t * @param daylight adjust the style for daylight saving time if {@code true}\n\t\t * @param style    the timezone style\n\t\t * @param locale   the timezone locale\n\t\t */\n\t\tTimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale) {\n\t\t\tmTimeZone = timeZone;\n\t\t\tif (daylight) {\n\t\t\t\tmStyle = style | 0x80000000;\n\t\t\t} else {\n\t\t\t\tmStyle = style;\n\t\t\t}\n\t\t\tmLocale = locale;\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn (mStyle * 31 + mLocale.hashCode()) * 31 + mTimeZone.hashCode();\n\t\t}\n\n\t\t/**\n\t\t * {@inheritDoc}\n\t\t */\n\t\t@Override\n\t\tpublic boolean equals(final Object obj) {\n\t\t\tif (this == obj) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (obj instanceof TimeZoneDisplayKey) {\n\t\t\t\tfinal TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;\n\t\t\t\treturn mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/FormatCache.java",
    "content": "package cn.hutool.core.date.format;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Tuple;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.text.DateFormat;\nimport java.text.Format;\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * 日期格式化器缓存<br>\n * Thanks to Apache Commons Lang 3.5\n *\n * @since 2.16.2\n */\nabstract class FormatCache<F extends Format> {\n\n\t/**\n\t * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG\n\t */\n\tstatic final int NONE = -1;\n\n\tprivate final ConcurrentMap<Tuple, F> cInstanceCache = new SafeConcurrentHashMap<>(7);\n\n\tprivate static final ConcurrentMap<Tuple, String> C_DATE_TIME_INSTANCE_CACHE = new SafeConcurrentHashMap<>(7);\n\n\t/**\n\t * 使用默认的pattern、timezone和locale获得缓存中的实例\n\t *\n\t * @return a date/time formatter\n\t */\n\tpublic F getInstance() {\n\t\treturn getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, null, null);\n\t}\n\n\t/**\n\t * 使用 pattern, time zone and locale 获得对应的 格式化器\n\t *\n\t * @param pattern  非空日期格式，使用与 {@link java.text.SimpleDateFormat}相同格式\n\t * @param timeZone 时区，默认当前时区\n\t * @param locale   地区，默认使用当前地区\n\t * @return 格式化器\n\t * @throws IllegalArgumentException pattern 无效或{@code null}\n\t */\n\tpublic F getInstance(final String pattern, TimeZone timeZone, Locale locale) {\n\t\tAssert.notBlank(pattern, \"pattern must not be blank\");\n\t\tif (timeZone == null) {\n\t\t\ttimeZone = TimeZone.getDefault();\n\t\t}\n\t\tif (locale == null) {\n\t\t\tlocale = Locale.getDefault();\n\t\t}\n\t\tfinal Tuple key = new Tuple(pattern, timeZone, locale);\n\t\tF format = cInstanceCache.get(key);\n\t\tif (format == null) {\n\t\t\tformat = createInstance(pattern, timeZone, locale);\n\t\t\tfinal F previousValue = cInstanceCache.putIfAbsent(key, format);\n\t\t\tif (previousValue != null) {\n\t\t\t\t// another thread snuck in and did the same work\n\t\t\t\t// we should return the instance that is in ConcurrentMap\n\t\t\t\tformat = previousValue;\n\t\t\t}\n\t\t}\n\t\treturn format;\n\t}\n\n\t/**\n\t * 创建格式化器\n\t *\n\t * @param pattern  非空日期格式，使用与 {@link java.text.SimpleDateFormat}相同格式\n\t * @param timeZone 时区，默认当前时区\n\t * @param locale   地区，默认使用当前地区\n\t * @return 格式化器\n\t * @throws IllegalArgumentException pattern 无效或{@code null}\n\t */\n\tabstract protected F createInstance(String pattern, TimeZone timeZone, Locale locale);\n\n\t/**\n\t * <p>\n\t * Gets a date/time formatter instance using the specified style, time zone and locale.\n\t * </p>\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format\n\t * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale\n\t * @param locale    optional locale, overrides system locale\n\t * @return a localized standard date/time formatter\n\t * @throws IllegalArgumentException if the Locale has no date/time pattern defined\n\t */\n\t// This must remain private, see LANG-884\n\tF getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {\n\t\tif (locale == null) {\n\t\t\tlocale = Locale.getDefault();\n\t\t}\n\t\tfinal String pattern = getPatternForStyle(dateStyle, timeStyle, locale);\n\t\treturn getInstance(pattern, timeZone, locale);\n\t}\n\n\t/**\n\t * <p>\n\t * Gets a date formatter instance using the specified style, time zone and locale.\n\t * </p>\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale\n\t * @param locale    optional locale, overrides system locale\n\t * @return a localized standard date/time formatter\n\t * @throws IllegalArgumentException if the Locale has no date/time pattern defined\n\t */\n\t// package protected, for access from FastDateFormat; do not make public or protected\n\tF getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {\n\t\treturn getDateTimeInstance(dateStyle, null, timeZone, locale);\n\t}\n\n\t/**\n\t * <p>\n\t * Gets a time formatter instance using the specified style, time zone and locale.\n\t * </p>\n\t *\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT\n\t * @param timeZone  optional time zone, overrides time zone of formatted date, null means use default Locale\n\t * @param locale    optional locale, overrides system locale\n\t * @return a localized standard date/time formatter\n\t * @throws IllegalArgumentException if the Locale has no date/time pattern defined\n\t */\n\t// package protected, for access from FastDateFormat; do not make public or protected\n\tF getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {\n\t\treturn getDateTimeInstance(null, timeStyle, timeZone, locale);\n\t}\n\n\t/**\n\t * <p>\n\t * Gets a date/time format for the specified styles and locale.\n\t * </p>\n\t *\n\t * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format\n\t * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format\n\t * @param locale    The non-null locale of the desired format\n\t * @return a localized standard date/time format\n\t * @throws IllegalArgumentException if the Locale has no date/time pattern defined\n\t */\n\t// package protected, for access from test code; do not make public or protected\n\tstatic String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {\n\t\tfinal Tuple key = new Tuple(dateStyle, timeStyle, locale);\n\n\t\tString pattern = C_DATE_TIME_INSTANCE_CACHE.get(key);\n\t\tif (pattern == null) {\n\t\t\ttry {\n\t\t\t\tDateFormat formatter;\n\t\t\t\tif (dateStyle == null) {\n\t\t\t\t\tformatter = DateFormat.getTimeInstance(timeStyle, locale);\n\t\t\t\t} else if (timeStyle == null) {\n\t\t\t\t\tformatter = DateFormat.getDateInstance(dateStyle, locale);\n\t\t\t\t} else {\n\t\t\t\t\tformatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);\n\t\t\t\t}\n\t\t\t\tpattern = ((SimpleDateFormat) formatter).toPattern();\n\t\t\t\tfinal String previous = C_DATE_TIME_INSTANCE_CACHE.putIfAbsent(key, pattern);\n\t\t\t\tif (previous != null) {\n\t\t\t\t\t// even though it doesn't matter if another thread put the pattern\n\t\t\t\t\t// it's still good practice to return the String instance that is\n\t\t\t\t\t// actually in the ConcurrentMap\n\t\t\t\t\tpattern = previous;\n\t\t\t\t}\n\t\t\t} catch (final ClassCastException ex) {\n\t\t\t\tthrow new IllegalArgumentException(\"No date time pattern for locale: \" + locale);\n\t\t\t}\n\t\t}\n\t\treturn pattern;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/GlobalCustomFormat.java",
    "content": "package cn.hutool.core.date.format;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * 全局自定义格式<br>\n * 用于定义用户指定的日期格式和输出日期的关系\n *\n * @author looly\n * @since 5.7.2\n */\npublic class GlobalCustomFormat {\n\n\t/**\n\t * 格式：秒时间戳（Unix时间戳）\n\t */\n\tpublic static final String FORMAT_SECONDS = \"#sss\";\n\t/**\n\t * 格式：毫秒时间戳\n\t */\n\tpublic static final String FORMAT_MILLISECONDS = \"#SSS\";\n\n\tprivate static final Map<CharSequence, Function<Date, String>> formatterMap;\n\tprivate static final Map<CharSequence, Function<CharSequence, Date>> parserMap;\n\n\tstatic {\n\t\tformatterMap = new SafeConcurrentHashMap<>();\n\t\tparserMap = new SafeConcurrentHashMap<>();\n\n\t\t// Hutool预设的几种自定义格式\n\t\tputFormatter(FORMAT_SECONDS, (date) -> String.valueOf(Math.floorDiv(date.getTime(), 1000)));\n\t\tputParser(FORMAT_SECONDS, (dateStr) -> DateUtil.date(Math.multiplyExact(Long.parseLong(dateStr.toString()), 1000)));\n\n\t\tputFormatter(FORMAT_MILLISECONDS, (date) -> String.valueOf(date.getTime()));\n\t\tputParser(FORMAT_MILLISECONDS, (dateStr) -> DateUtil.date(Long.parseLong(dateStr.toString())));\n\t}\n\n\t/**\n\t * 加入日期格式化规则\n\t *\n\t * @param format 格式\n\t * @param func   格式化函数\n\t */\n\tpublic static void putFormatter(String format, Function<Date, String> func) {\n\t\tAssert.notNull(format, \"Format must be not null !\");\n\t\tAssert.notNull(func, \"Function must be not null !\");\n\t\tformatterMap.put(format, func);\n\t}\n\n\t/**\n\t * 加入日期解析规则\n\t *\n\t * @param format 格式\n\t * @param func   解析函数\n\t */\n\tpublic static void putParser(String format, Function<CharSequence, Date> func) {\n\t\tAssert.notNull(format, \"Format must be not null !\");\n\t\tAssert.notNull(func, \"Function must be not null !\");\n\t\tparserMap.put(format, func);\n\t}\n\n\t/**\n\t * 检查指定格式是否为自定义格式\n\t *\n\t * @param format 格式\n\t * @return 是否为自定义格式\n\t */\n\tpublic static boolean isCustomFormat(String format) {\n\t\treturn formatterMap.containsKey(format);\n\t}\n\n\t/**\n\t * 使用自定义格式格式化日期\n\t *\n\t * @param date   日期\n\t * @param format 自定义格式\n\t * @return 格式化后的日期\n\t */\n\tpublic static String format(Date date, CharSequence format) {\n\t\tif (null != formatterMap) {\n\t\t\tfinal Function<Date, String> func = formatterMap.get(format);\n\t\t\tif (null != func) {\n\t\t\t\treturn func.apply(date);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 使用自定义格式格式化日期\n\t *\n\t * @param temporalAccessor 日期\n\t * @param format           自定义格式\n\t * @return 格式化后的日期\n\t */\n\tpublic static String format(TemporalAccessor temporalAccessor, CharSequence format) {\n\t\treturn format(DateUtil.date(temporalAccessor), format);\n\t}\n\n\t/**\n\t * 使用自定义格式解析日期\n\t *\n\t * @param dateStr 日期字符串\n\t * @param format  自定义格式\n\t * @return 格式化后的日期\n\t */\n\tpublic static Date parse(CharSequence dateStr, String format) {\n\t\tif (null != parserMap) {\n\t\t\tfinal Function<CharSequence, Date> func = parserMap.get(format);\n\t\t\tif (null != func) {\n\t\t\t\treturn func.apply(dateStr);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/format/package-info.java",
    "content": "/**\n * 提供线程安全的日期格式的格式化和解析实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.date.format;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/date/package-info.java",
    "content": "/**\n * 日期封装，日期的核心为DateTime类，DateUtil提供日期操作的入口\n *\n * @author looly\n *\n */\npackage cn.hutool.core.date;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/CheckedUtil.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.lang.func.*;\n\nimport java.util.Objects;\n\n/**\n * 方便的执行会抛出受检查类型异常的方法调用或者代码段\n * <p>\n * 该工具通过函数式的方式将那些需要抛出受检查异常的表达式或者代码段转化成一个 cn.hutool.core.lang.func.Func* 对象\n * </p>\n * <p>\n * {@code\n * <pre>\n *      //代码中如果遇到一个方法调用声明了受检查异常那么我们的代码就必须这样写\n *         Map<String, String> describedObject = null;\n *         try {\n *             describe = BeanUtils.describe(new Object());\n *         } catch (IllegalAccessException e) {\n *             throw new RuntimeException(e);\n *         } catch (InvocationTargetException e) {\n *             throw new RuntimeException(e);\n *         } catch (NoSuchMethodException e) {\n *             throw new RuntimeException(e);\n *         }\n *         // use describedObject ...\n *\n *       //上面的代码增加了异常块使得代码不那么流畅，现在可以这样写：\n *       Map<String, String> describedObject = CheckedUtil.uncheck(BeanUtils::describe).call(new Object());\n *       // use describedObject ...\n *\n *       CheckedUtil.uncheck 方法接受任意可以转化成 cn.hutool.core.lang.func.Func* 函数式接口的 Lambda 表达式。返回对应的函数式对象。\n *       上述代码可以理解为：\n *        Func0<Object, Map<String, String>> aFunc = CheckedUtil.uncheck(BeanUtils::describe);\n *        Map<String, String> describedObject = aFunc.call(传入参数);\n *        该aFunc对象代表的就是BeanUtils::describe这个表达式，且在内部转化了检查类型异常，不需要代码里面显示处理。\n *\n *\n * </pre>\n * }\n *\n * @author conder\n * @since 5.7.19\n */\npublic class CheckedUtil {\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression Lambda表达式\n\t * @param <P>        运行时传入的参数类型\n\t * @param <R>        最终返回的数据类型\n\t * @return {@link FuncRt}\n\t */\n\tpublic static <P, R> FuncRt<P, R> uncheck(Func<P, R> expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func0 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression 运行时传入的参数类型\n\t * @param <R>        最终返回的数据类型\n\t * @return {@link Func0Rt}\n\t */\n\tpublic static <R> Func0Rt<R> uncheck(Func0<R> expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func1 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression 运行时传入的参数类型\n\t * @param <P>        运行时传入的参数类型\n\t * @param <R>        最终返回的数据类型\n\t * @return {@link Func1Rt}\n\t */\n\tpublic static <P, R> Func1Rt<P, R> uncheck(Func1<P, R> expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression 运行时传入的参数类型\n\t * @param <P>        运行时传入的参数类型\n\t * @return {@link VoidFuncRt}\n\t */\n\tpublic static <P> VoidFuncRt<P> uncheck(VoidFunc<P> expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression 运行时传入的参数类型\n\t * @return {@link VoidFunc0Rt}\n\t */\n\tpublic static VoidFunc0Rt uncheck(VoidFunc0 expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc1 的Lambda表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression 运行时传入的参数类型\n\t * @param <P>        运行时传入的参数类型\n\t * @return {@link VoidFunc1Rt}\n\t */\n\tpublic static <P> VoidFunc1Rt<P> uncheck(VoidFunc1<P> expression) {\n\t\treturn uncheck(expression, RuntimeException::new);\n\t}\n\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func的Lambda表达式，和一个可以把Exception转化成RuntimeExceptionde的表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @param <P>         运行时传入的参数类型\n\t * @param <R>         最终返回的数据类型\n\t * @return {@link FuncRt}\n\t */\n\tpublic static <P, R> FuncRt<P, R> uncheck(Func<P, R> expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn t -> {\n\t\t\ttry {\n\t\t\t\treturn expression.call(t);\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func0的Lambda表达式，和一个可以把Exception转化成RuntimeExceptionde的表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @param <R>         最终返回的数据类型\n\t * @return {@link Func0Rt}\n\t */\n\tpublic static <R> Func0Rt<R> uncheck(Func0<R> expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn () -> {\n\t\t\ttry {\n\t\t\t\treturn expression.call();\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.Func1的Lambda表达式，和一个可以把Exception转化成RuntimeExceptionde的表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @param <P>         运行时传入的参数类型\n\t * @param <R>         最终返回的数据类型\n\t * @return {@link Func1Rt}\n\t */\n\tpublic static <P, R> Func1Rt<P, R> uncheck(Func1<P, R> expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn t -> {\n\t\t\ttry {\n\t\t\t\treturn expression.call(t);\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc的Lambda表达式，和一个可以把Exception转化成RuntimeExceptionde的表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @param <P>         运行时传入的参数类型\n\t * @return {@link VoidFuncRt}\n\t */\n\tpublic static <P> VoidFuncRt<P> uncheck(VoidFunc<P> expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn t -> {\n\t\t\ttry {\n\t\t\t\texpression.call(t);\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0的Lambda表达式，和一个RuntimeException，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression Lambda表达式\n\t * @param rte        期望抛出的运行时异常\n\t * @return {@link VoidFunc0Rt}\n\t */\n\tpublic static VoidFunc0Rt uncheck(VoidFunc0 expression, RuntimeException rte) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn () -> {\n\t\t\ttry {\n\t\t\t\texpression.call();\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rte == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\trte.initCause(e);\n\t\t\t\t\tthrow rte;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc0的Lambda表达式，和一个可以把Exception转化成RuntimeExceptionde的表达式，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @return {@link VoidFunc0Rt}\n\t */\n\tpublic static VoidFunc0Rt uncheck(VoidFunc0 expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn () -> {\n\t\t\ttry {\n\t\t\t\texpression.call();\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 接收一个可以转化成 cn.hutool.core.lang.func.VoidFunc1的Lambda表达式，和一个RuntimeException，当执行表达式抛出任何异常的时候，都会转化成运行时异常\n\t * 如此一来，代码中就不用显示的try-catch转化成运行时异常\n\t *\n\t * @param expression  Lambda表达式\n\t * @param rteSupplier 转化运行时异常的表达式\n\t * @param <P>         运行时传入的参数类型\n\t * @return {@link VoidFunc1Rt}\n\t */\n\tpublic static <P> VoidFunc1Rt<P> uncheck(VoidFunc1<P> expression, Supplier1<RuntimeException, Exception> rteSupplier) {\n\t\tObjects.requireNonNull(expression, \"expression can not be null\");\n\t\treturn t -> {\n\t\t\ttry {\n\t\t\t\texpression.call(t);\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (rteSupplier == null) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} else {\n\t\t\t\t\tthrow rteSupplier.get(e);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tpublic interface FuncRt<P, R> extends Func<P, R> {\n\t\t@SuppressWarnings(\"unchecked\")\n\t\t@Override\n\t\tR call(P... parameters) throws RuntimeException;\n\t}\n\n\tpublic interface Func0Rt<R> extends Func0<R> {\n\t\t@Override\n\t\tR call() throws RuntimeException;\n\t}\n\n\tpublic interface Func1Rt<P, R> extends Func1<P, R> {\n\t\t@Override\n\t\tR call(P parameter) throws RuntimeException;\n\t}\n\n\tpublic interface VoidFuncRt<P> extends VoidFunc<P> {\n\t\t@SuppressWarnings(\"unchecked\")\n\t\t@Override\n\t\tvoid call(P... parameters) throws RuntimeException;\n\t}\n\n\tpublic interface VoidFunc0Rt extends VoidFunc0 {\n\t\t@Override\n\t\tvoid call() throws RuntimeException;\n\t}\n\n\tpublic interface VoidFunc1Rt<P> extends VoidFunc1<P> {\n\t\t@Override\n\t\tvoid call(P parameter) throws RuntimeException;\n\t}\n\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/DependencyException.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 依赖异常\n *\n * @author xiaoleilu\n * @since 4.0.10\n */\npublic class DependencyException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic DependencyException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic DependencyException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic DependencyException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic DependencyException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic DependencyException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic DependencyException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.PrintStream;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.UndeclaredThrowableException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 异常工具类\n *\n * @author Looly\n */\npublic class ExceptionUtil {\n\n\t/**\n\t * 获得完整消息，包括异常名，消息格式为：{SimpleClassName}: {ThrowableMessage}\n\t *\n\t * @param e 异常\n\t * @return 完整消息\n\t */\n\tpublic static String getMessage(Throwable e) {\n\t\tif (null == e) {\n\t\t\treturn StrUtil.NULL;\n\t\t}\n\t\treturn StrUtil.format(\"{}: {}\", e.getClass().getSimpleName(), e.getMessage());\n\t}\n\n\t/**\n\t * 获得消息，调用异常类的getMessage方法\n\t *\n\t * @param e 异常\n\t * @return 消息\n\t */\n\tpublic static String getSimpleMessage(Throwable e) {\n\t\treturn (null == e) ? StrUtil.NULL : e.getMessage();\n\t}\n\n\t/**\n\t * 使用运行时异常包装编译异常<br>\n\t * <p>\n\t * 如果传入参数已经是运行时异常，则直接返回，不再额外包装\n\t *\n\t * @param throwable 异常\n\t * @return 运行时异常\n\t */\n\tpublic static RuntimeException wrapRuntime(Throwable throwable) {\n\t\tif (throwable instanceof RuntimeException) {\n\t\t\treturn (RuntimeException) throwable;\n\t\t}\n\t\treturn new RuntimeException(throwable);\n\t}\n\n\t/**\n\t * 将指定的消息包装为运行时异常\n\t *\n\t * @param message 异常消息\n\t * @return 运行时异常\n\t * @since 5.5.2\n\t */\n\tpublic static RuntimeException wrapRuntime(String message) {\n\t\treturn new RuntimeException(message);\n\t}\n\n\t/**\n\t * 包装一个异常\n\t *\n\t * @param <T>           被包装的异常类型\n\t * @param throwable     异常\n\t * @param wrapThrowable 包装后的异常类\n\t * @return 包装后的异常\n\t * @since 3.3.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Throwable> T wrap(Throwable throwable, Class<T> wrapThrowable) {\n\t\tif (wrapThrowable.isInstance(throwable)) {\n\t\t\treturn (T) throwable;\n\t\t}\n\t\treturn ReflectUtil.newInstance(wrapThrowable, throwable);\n\t}\n\n\t/**\n\t * 包装异常并重新抛出此异常<br>\n\t * {@link RuntimeException} 和{@link Error} 直接抛出，其它检查异常包装为{@link UndeclaredThrowableException} 后抛出\n\t *\n\t * @param throwable 异常\n\t */\n\tpublic static void wrapAndThrow(Throwable throwable) {\n\t\tif (throwable instanceof RuntimeException) {\n\t\t\tthrow (RuntimeException) throwable;\n\t\t}\n\t\tif (throwable instanceof Error) {\n\t\t\tthrow (Error) throwable;\n\t\t}\n\t\tthrow new UndeclaredThrowableException(throwable);\n\t}\n\n\t/**\n\t * 将消息包装为运行时异常并抛出\n\t *\n\t * @param message 异常消息\n\t * @since 5.5.2\n\t */\n\tpublic static void wrapRuntimeAndThrow(String message) {\n\t\tthrow new RuntimeException(message);\n\t}\n\n\t/**\n\t * 剥离反射引发的InvocationTargetException、UndeclaredThrowableException中间异常，返回业务本身的异常\n\t *\n\t * @param wrapped 包装的异常\n\t * @return 剥离后的异常\n\t */\n\tpublic static Throwable unwrap(Throwable wrapped) {\n\t\tThrowable unwrapped = wrapped;\n\t\twhile (true) {\n\t\t\tif (unwrapped instanceof InvocationTargetException) {\n\t\t\t\tunwrapped = ((InvocationTargetException) unwrapped).getTargetException();\n\t\t\t} else if (unwrapped instanceof UndeclaredThrowableException) {\n\t\t\t\tunwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable();\n\t\t\t} else {\n\t\t\t\treturn unwrapped;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取当前栈信息\n\t *\n\t * @return 当前栈信息\n\t */\n\tpublic static StackTraceElement[] getStackElements() {\n\t\t// return (new Throwable()).getStackTrace();\n\t\treturn Thread.currentThread().getStackTrace();\n\t}\n\n\t/**\n\t * 获取指定层的堆栈信息\n\t *\n\t * @param i 层数\n\t * @return 指定层的堆栈信息\n\t * @since 4.1.4\n\t */\n\tpublic static StackTraceElement getStackElement(int i) {\n\t\treturn Thread.currentThread().getStackTrace()[i];\n\t}\n\n\t/**\n\t * 获取指定层的堆栈信息\n\t *\n\t * @param fqcn 指定类名为基础\n\t * @param i 指定类名的类堆栈相对层数\n\t * @return 指定层的堆栈信息\n\t * @since 5.6.6\n\t */\n\tpublic static StackTraceElement getStackElement(String fqcn, int i) {\n\t\tfinal StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace();\n\t\tfinal int index = ArrayUtil.matchIndex((ele) -> StrUtil.equals(fqcn, ele.getClassName()), stackTraceArray);\n\t\tif(index > 0){\n\t\t\treturn stackTraceArray[index + i];\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取入口堆栈信息\n\t *\n\t * @return 入口堆栈信息\n\t * @since 4.1.4\n\t */\n\tpublic static StackTraceElement getRootStackElement() {\n\t\tfinal StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();\n\t\treturn Thread.currentThread().getStackTrace()[stackElements.length - 1];\n\t}\n\n\t/**\n\t * 堆栈转为单行完整字符串\n\t *\n\t * @param throwable 异常对象\n\t * @return 堆栈转为的字符串\n\t */\n\tpublic static String stacktraceToOneLineString(Throwable throwable) {\n\t\treturn stacktraceToOneLineString(throwable, 3000);\n\t}\n\n\t/**\n\t * 堆栈转为单行完整字符串\n\t *\n\t * @param throwable 异常对象\n\t * @param limit     限制最大长度\n\t * @return 堆栈转为的字符串\n\t */\n\tpublic static String stacktraceToOneLineString(Throwable throwable, int limit) {\n\t\tMap<Character, String> replaceCharToStrMap = new HashMap<>();\n\t\treplaceCharToStrMap.put(StrUtil.C_CR, StrUtil.SPACE);\n\t\treplaceCharToStrMap.put(StrUtil.C_LF, StrUtil.SPACE);\n\t\treplaceCharToStrMap.put(StrUtil.C_TAB, StrUtil.SPACE);\n\n\t\treturn stacktraceToString(throwable, limit, replaceCharToStrMap);\n\t}\n\n\t/**\n\t * 堆栈转为完整字符串\n\t *\n\t * @param throwable 异常对象\n\t * @return 堆栈转为的字符串\n\t */\n\tpublic static String stacktraceToString(Throwable throwable) {\n\t\treturn stacktraceToString(throwable, 3000);\n\t}\n\n\t/**\n\t * 堆栈转为完整字符串\n\t *\n\t * @param throwable 异常对象\n\t * @param limit     限制最大长度\n\t * @return 堆栈转为的字符串\n\t */\n\tpublic static String stacktraceToString(Throwable throwable, int limit) {\n\t\treturn stacktraceToString(throwable, limit, null);\n\t}\n\n\t/**\n\t * 堆栈转为完整字符串\n\t *\n\t * @param throwable           异常对象\n\t * @param limit               限制最大长度，&lt;0表示不限制长度\n\t * @param replaceCharToStrMap 替换字符为指定字符串\n\t * @return 堆栈转为的字符串\n\t */\n\tpublic static String stacktraceToString(Throwable throwable, int limit, Map<Character, String> replaceCharToStrMap) {\n\t\tfinal FastByteArrayOutputStream baos = new FastByteArrayOutputStream();\n\t\tthrowable.printStackTrace(new PrintStream(baos));\n\n\t\tfinal String exceptionStr = baos.toString();\n\t\tfinal int length = exceptionStr.length();\n\t\tif (limit < 0 || limit > length) {\n\t\t\tlimit = length;\n\t\t}\n\n\t\tif (MapUtil.isNotEmpty(replaceCharToStrMap)) {\n\t\t\tfinal StringBuilder sb = StrUtil.builder();\n\t\t\tchar c;\n\t\t\tString value;\n\t\t\tfor (int i = 0; i < limit; i++) {\n\t\t\t\tc = exceptionStr.charAt(i);\n\t\t\t\tvalue = replaceCharToStrMap.get(c);\n\t\t\t\tif (null != value) {\n\t\t\t\t\tsb.append(value);\n\t\t\t\t} else {\n\t\t\t\t\tsb.append(c);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sb.toString();\n\t\t} else {\n\t\t\tif(limit == length){\n\t\t\t\treturn exceptionStr;\n\t\t\t}\n\t\t\treturn StrUtil.subPre(exceptionStr, limit);\n\t\t}\n\t}\n\n\t/**\n\t * 判断是否由指定异常类引起\n\t *\n\t * @param throwable    异常\n\t * @param causeClasses 定义的引起异常的类\n\t * @return 是否由指定异常类引起\n\t * @since 4.1.13\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static boolean isCausedBy(Throwable throwable, Class<? extends Exception>... causeClasses) {\n\t\treturn null != getCausedBy(throwable, causeClasses);\n\t}\n\n\t/**\n\t * 获取由指定异常类引起的异常\n\t *\n\t * @param throwable    异常\n\t * @param causeClasses 定义的引起异常的类\n\t * @return 是否由指定异常类引起\n\t * @since 4.1.13\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static Throwable getCausedBy(Throwable throwable, Class<? extends Exception>... causeClasses) {\n\t\tThrowable cause = throwable;\n\t\twhile (cause != null) {\n\t\t\tfor (Class<? extends Exception> causeClass : causeClasses) {\n\t\t\t\tif (causeClass.isInstance(cause)) {\n\t\t\t\t\treturn cause;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcause = cause.getCause();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 判断指定异常是否来自或者包含指定异常\n\t *\n\t * @param throwable      异常\n\t * @param exceptionClass 定义的引起异常的类\n\t * @return true 来自或者包含\n\t * @since 4.3.2\n\t */\n\tpublic static boolean isFromOrSuppressedThrowable(Throwable throwable, Class<? extends Throwable> exceptionClass) {\n\t\treturn convertFromOrSuppressedThrowable(throwable, exceptionClass, true) != null;\n\t}\n\n\t/**\n\t * 判断指定异常是否来自或者包含指定异常\n\t *\n\t * @param throwable      异常\n\t * @param exceptionClass 定义的引起异常的类\n\t * @param checkCause     判断cause\n\t * @return true 来自或者包含\n\t * @since 4.4.1\n\t */\n\tpublic static boolean isFromOrSuppressedThrowable(Throwable throwable, Class<? extends Throwable> exceptionClass, boolean checkCause) {\n\t\treturn convertFromOrSuppressedThrowable(throwable, exceptionClass, checkCause) != null;\n\t}\n\n\t/**\n\t * 转化指定异常为来自或者包含指定异常\n\t *\n\t * @param <T>            异常类型\n\t * @param throwable      异常\n\t * @param exceptionClass 定义的引起异常的类\n\t * @return 结果为null 不是来自或者包含\n\t * @since 4.3.2\n\t */\n\tpublic static <T extends Throwable> T convertFromOrSuppressedThrowable(Throwable throwable, Class<T> exceptionClass) {\n\t\treturn convertFromOrSuppressedThrowable(throwable, exceptionClass, true);\n\t}\n\n\t/**\n\t * 转化指定异常为来自或者包含指定异常\n\t *\n\t * @param <T>            异常类型\n\t * @param throwable      异常\n\t * @param exceptionClass 定义的引起异常的类\n\t * @param checkCause     判断cause\n\t * @return 结果为null 不是来自或者包含\n\t * @since 4.4.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Throwable> T convertFromOrSuppressedThrowable(Throwable throwable, Class<T> exceptionClass, boolean checkCause) {\n\t\tif (throwable == null || exceptionClass == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (exceptionClass.isAssignableFrom(throwable.getClass())) {\n\t\t\treturn (T) throwable;\n\t\t}\n\t\tif (checkCause) {\n\t\t\tThrowable cause = throwable.getCause();\n\t\t\tif (cause != null && exceptionClass.isAssignableFrom(cause.getClass())) {\n\t\t\t\treturn (T) cause;\n\t\t\t}\n\t\t}\n\t\tThrowable[] throwables = throwable.getSuppressed();\n\t\tif (ArrayUtil.isNotEmpty(throwables)) {\n\t\t\tfor (Throwable throwable1 : throwables) {\n\t\t\t\tif (exceptionClass.isAssignableFrom(throwable1.getClass())) {\n\t\t\t\t\treturn (T) throwable1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取异常链上所有异常的集合，如果{@link Throwable} 对象没有cause，返回只有一个节点的List<br>\n\t * 如果传入null，返回空集合\n\t *\n\t * <p>\n\t * 此方法来自Apache-Commons-Lang3\n\t * </p>\n\t *\n\t * @param throwable 异常对象，可以为null\n\t * @return 异常链中所有异常集合\n\t * @since 4.6.2\n\t */\n\tpublic static List<Throwable> getThrowableList(Throwable throwable) {\n\t\tfinal List<Throwable> list = new ArrayList<>();\n\t\twhile (throwable != null && false == list.contains(throwable)) {\n\t\t\tlist.add(throwable);\n\t\t\tthrowable = throwable.getCause();\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 获取异常链中最尾端的异常，即异常最早发生的异常对象。<br>\n\t * 此方法通过调用{@link Throwable#getCause()} 直到没有cause为止，如果异常本身没有cause，返回异常本身<br>\n\t * 传入null返回也为null\n\t *\n\t * <p>\n\t * 此方法来自Apache-Commons-Lang3\n\t * </p>\n\t *\n\t * @param throwable 异常对象，可能为null\n\t * @return 最尾端异常，传入null参数返回也为null\n\t */\n\tpublic static Throwable getRootCause(final Throwable throwable) {\n\t\tfinal List<Throwable> list = getThrowableList(throwable);\n\t\treturn list.size() < 1 ? null : list.get(list.size() - 1);\n\t}\n\n\t/**\n\t * 获取异常链中最尾端的异常的消息，消息格式为：{SimpleClassName}: {ThrowableMessage}\n\t *\n\t * @param th 异常\n\t * @return 消息\n\t * @since 4.6.2\n\t */\n\tpublic static String getRootCauseMessage(final Throwable th) {\n\t\treturn getMessage(getRootCause(th));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/InvocationTargetRuntimeException.java",
    "content": "package cn.hutool.core.exceptions;\n\n/**\n * InvocationTargetException的运行时异常\n *\n * @author looly\n * @since 5.8.1\n */\npublic class InvocationTargetRuntimeException extends UtilException {\n\n\tpublic InvocationTargetRuntimeException(Throwable e) {\n\t\tsuper(e);\n\t}\n\n\tpublic InvocationTargetRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic InvocationTargetRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(messageTemplate, params);\n\t}\n\n\tpublic InvocationTargetRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic InvocationTargetRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic InvocationTargetRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(throwable, messageTemplate, params);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/NotInitedException.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 未初始化异常\n *\n * @author xiaoleilu\n */\npublic class NotInitedException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic NotInitedException(Throwable e) {\n\t\tsuper(e);\n\t}\n\n\tpublic NotInitedException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic NotInitedException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic NotInitedException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic NotInitedException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic NotInitedException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/StatefulException.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 带有状态码的异常\n *\n * @author xiaoleilu\n */\npublic class StatefulException extends RuntimeException {\n\tprivate static final long serialVersionUID = 6057602589533840889L;\n\n\t// 异常状态码\n\tprivate int status;\n\n\tpublic StatefulException() {\n\t}\n\n\tpublic StatefulException(String msg) {\n\t\tsuper(msg);\n\t}\n\n\tpublic StatefulException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic StatefulException(Throwable throwable) {\n\t\tsuper(throwable);\n\t}\n\n\tpublic StatefulException(String msg, Throwable throwable) {\n\t\tsuper(msg, throwable);\n\t}\n\n\tpublic StatefulException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic StatefulException(int status, String msg) {\n\t\tsuper(msg);\n\t\tthis.status = status;\n\t}\n\n\tpublic StatefulException(int status, Throwable throwable) {\n\t\tsuper(throwable);\n\t\tthis.status = status;\n\t}\n\n\tpublic StatefulException(int status, String msg, Throwable throwable) {\n\t\tsuper(msg, throwable);\n\t\tthis.status = status;\n\t}\n\n\t/**\n\t * @return 获得异常状态码\n\t */\n\tpublic int getStatus() {\n\t\treturn status;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/UtilException.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 工具类异常\n *\n * @author xiaoleilu\n */\npublic class UtilException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic UtilException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic UtilException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic UtilException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic UtilException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic UtilException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic UtilException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/ValidateException.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 验证异常\n *\n * @author xiaoleilu\n */\npublic class ValidateException extends StatefulException {\n\tprivate static final long serialVersionUID = 6057602589533840889L;\n\n\tpublic ValidateException() {\n\t}\n\n\tpublic ValidateException(String msg) {\n\t\tsuper(msg);\n\t}\n\n\tpublic ValidateException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ValidateException(Throwable throwable) {\n\t\tsuper(throwable);\n\t}\n\n\tpublic ValidateException(String msg, Throwable throwable) {\n\t\tsuper(msg, throwable);\n\t}\n\n\tpublic ValidateException(int status, String msg) {\n\t\tsuper(status, msg);\n\t}\n\n\tpublic ValidateException(int status, Throwable throwable) {\n\t\tsuper(status, throwable);\n\t}\n\n\tpublic ValidateException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic ValidateException(int status, String msg, Throwable throwable) {\n\t\tsuper(status, msg, throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/exceptions/package-info.java",
    "content": "/**\n * 特殊异常封装，同时提供异常工具ExceptionUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.exceptions;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\n\n/**\n * 数组类型的Get接口\n * @author Looly\n *\n */\npublic interface ArrayTypeGetter {\n\t/*-------------------------- 数组类型 start -------------------------------*/\n\n\t/**\n\t * 获取Object型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tString[] getObjs(String key);\n\n\t/**\n\t * 获取String型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tString[] getStrs(String key);\n\n\t/**\n\t * 获取Integer型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tInteger[] getInts(String key);\n\n\t/**\n\t * 获取Short型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tShort[] getShorts(String key);\n\n\t/**\n\t * 获取Boolean型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tBoolean[] getBools(String key);\n\n\t/**\n\t * 获取Long型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tLong[] getLongs(String key);\n\n\t/**\n\t * 获取Character型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tCharacter[] getChars(String key);\n\n\t/**\n\t * 获取Double型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tDouble[] getDoubles(String key);\n\n\t/**\n\t * 获取Byte型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tByte[] getBytes(String key);\n\n\t/**\n\t * 获取BigInteger型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tBigInteger[] getBigIntegers(String key);\n\n\t/**\n\t * 获取BigDecimal型属性值数组\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tBigDecimal[] getBigDecimals(String key);\n\t/*-------------------------- 数组类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Date;\n\n/**\n * 基本类型的getter接口<br>\n * 提供一个统一的接口定义返回不同类型的值（基本类型）<br>\n *\n * @author Looly\n * @param <K> key类型\n */\npublic interface BasicTypeGetter<K> {\n\t/*-------------------------- 基本类型 start -------------------------------*/\n\n\t/**\n\t * 获取Object属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tObject getObj(K key);\n\n\t/**\n\t * 获取字符串型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tString getStr(K key);\n\n\t/**\n\t * 获取int型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tInteger getInt(K key);\n\n\t/**\n\t * 获取short型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tShort getShort(K key);\n\n\t/**\n\t * 获取boolean型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tBoolean getBool(K key);\n\n\t/**\n\t * 获取long型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tLong getLong(K key);\n\n\t/**\n\t * 获取char型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tCharacter getChar(K key);\n\n\t/**\n\t * 获取float型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tFloat getFloat(K key);\n\n\t/**\n\t * 获取double型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tDouble getDouble(K key);\n\n\t/**\n\t * 获取byte型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tByte getByte(K key);\n\n\t/**\n\t * 获取BigDecimal型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tBigDecimal getBigDecimal(K key);\n\n\t/**\n\t * 获取BigInteger型属性值\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tBigInteger getBigInteger(K key);\n\n\t/**\n\t * 获得Enum类型的值\n\t *\n\t * @param <E> 枚举类型\n\t * @param clazz Enum的Class\n\t * @param key KEY\n\t * @return Enum类型的值，无则返回Null\n\t */\n\t<E extends Enum<E>> E getEnum(Class<E> clazz, K key);\n\n\t/**\n\t * 获取Date类型值\n\t *\n\t * @param key 属性名\n\t * @return Date类型属性值\n\t */\n\tDate getDate(K key);\n\t/*-------------------------- 基本类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\n\n/**\n * 基于分组的Get接口\n * @author Looly\n *\n */\npublic interface GroupedTypeGetter {\n\t/*-------------------------- 基本类型 start -------------------------------*/\n\t/**\n\t * 获取字符串型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tString getStrByGroup(String key, String group);\n\n\t/**\n\t * 获取int型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tInteger getIntByGroup(String key, String group);\n\n\t/**\n\t * 获取short型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tShort getShortByGroup(String key, String group);\n\n\t/**\n\t * 获取boolean型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tBoolean getBoolByGroup(String key, String group);\n\n\t/**\n\t * 获取Long型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tLong getLongByGroup(String key, String group);\n\n\t/**\n\t * 获取char型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tCharacter getCharByGroup(String key, String group);\n\n\t/**\n\t * 获取double型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tDouble getDoubleByGroup(String key, String group);\n\n\t/**\n\t * 获取byte型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tByte getByteByGroup(String key, String group);\n\n\t/**\n\t * 获取BigDecimal型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tBigDecimal getBigDecimalByGroup(String key, String group);\n\n\t/**\n\t * 获取BigInteger型属性值<br>\n\t *\n\t * @param key 属性名\n\t * @param group 分组\n\t * @return 属性值\n\t */\n\tBigInteger getBigIntegerByGroup(String key, String group);\n\t/*-------------------------- 基本类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/ListTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.List;\n\n/**\n * 列表类型的Get接口\n * @author Looly\n *\n */\npublic interface ListTypeGetter {\n\t/*-------------------------- List类型 start -------------------------------*/\n\t/**\n\t * 获取Object型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Object> getObjList(String key);\n\n\t/**\n\t * 获取String型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<String> getStrList(String key);\n\n\t/**\n\t * 获取Integer型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Integer> getIntList(String key);\n\n\t/**\n\t * 获取Short型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Short> getShortList(String key);\n\n\t/**\n\t * 获取Boolean型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Boolean> getBoolList(String key);\n\n\t/**\n\t * 获取Long型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Long> getLongList(String key);\n\n\t/**\n\t * 获取Character型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Character> getCharList(String key);\n\n\t/**\n\t * 获取Double型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Double> getDoubleList(String key);\n\n\t/**\n\t * 获取Byte型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<Byte> getByteList(String key);\n\n\t/**\n\t * 获取BigDecimal型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<BigDecimal> getBigDecimalList(String key);\n\n\t/**\n\t * 获取BigInteger型属性值列表\n\t *\n\t * @param key 属性名\n\t * @return 属性值列表\n\t */\n\tList<BigInteger> getBigIntegerList(String key);\n\t/*-------------------------- List类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\n\n/**\n * 可选默认值的数组类型的Get接口\n * 提供一个统一的接口定义返回不同类型的值（基本类型）<br>\n * 如果值不存在或获取错误，返回默认值\n *\n * @author Looly\n * @since 4.0.2\n *\n */\npublic interface OptArrayTypeGetter {\n\t/*-------------------------- 数组类型 start -------------------------------*/\n\n\t/**\n\t * 获取Object型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tObject[] getObjs(String key, Object[] defaultValue);\n\n\t/**\n\t * 获取String型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tString[] getStrs(String key, String[] defaultValue);\n\n\t/**\n\t * 获取Integer型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tInteger[] getInts(String key, Integer[] defaultValue);\n\n\t/**\n\t * 获取Short型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tShort[] getShorts(String key, Short[] defaultValue);\n\n\t/**\n\t * 获取Boolean型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tBoolean[] getBools(String key, Boolean[] defaultValue);\n\n\t/**\n\t * 获取Long型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tLong[] getLongs(String key, Long[] defaultValue);\n\n\t/**\n\t * 获取Character型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tCharacter[] getChars(String key, Character[] defaultValue);\n\n\t/**\n\t * 获取Double型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tDouble[] getDoubles(String key, Double[] defaultValue);\n\n\t/**\n\t * 获取Byte型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tByte[] getBytes(String key, Byte[] defaultValue);\n\n\t/**\n\t * 获取BigInteger型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tBigInteger[] getBigIntegers(String key, BigInteger[] defaultValue);\n\n\t/**\n\t * 获取BigDecimal型属性值数组\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认数组值\n\t * @return 属性值列表\n\t */\n\tBigDecimal[] getBigDecimals(String key, BigDecimal[] defaultValue);\n\t/*-------------------------- 数组类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Date;\n\n/**\n * 可选默认值的基本类型的getter接口<br>\n * 提供一个统一的接口定义返回不同类型的值（基本类型）<br>\n * 如果值不存在或获取错误，返回默认值\n * @author Looly\n */\npublic interface OptBasicTypeGetter<K> {\n\t/*-------------------------- 基本类型 start -------------------------------*/\n\n\t/**\n\t * 获取Object属性值\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tObject getObj(K key, Object defaultValue);\n\n\t/**\n\t * 获取字符串型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tString getStr(K key, String defaultValue);\n\n\t/**\n\t * 获取int型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tInteger getInt(K key, Integer defaultValue);\n\n\t/**\n\t * 获取short型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tShort getShort(K key, Short defaultValue);\n\n\t/**\n\t * 获取boolean型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tBoolean getBool(K key, Boolean defaultValue);\n\n\t/**\n\t * 获取Long型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tLong getLong(K key, Long defaultValue);\n\n\t/**\n\t * 获取char型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tCharacter getChar(K key, Character defaultValue);\n\n\t/**\n\t * 获取float型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tFloat getFloat(K key, Float defaultValue);\n\n\t/**\n\t * 获取double型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tDouble getDouble(K key, Double defaultValue);\n\n\t/**\n\t * 获取byte型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tByte getByte(K key, Byte defaultValue);\n\n\t/**\n\t * 获取BigDecimal型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tBigDecimal getBigDecimal(K key, BigDecimal defaultValue);\n\n\t/**\n\t * 获取BigInteger型属性值<br>\n\t * 若获得的值为不可见字符，使用默认值\n\t *\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值，无对应值返回defaultValue\n\t */\n\tBigInteger getBigInteger(K key, BigInteger defaultValue);\n\n\t/**\n\t * 获得Enum类型的值\n\t *\n\t * @param <E> 枚举类型\n\t * @param clazz Enum的Class\n\t * @param key KEY\n\t * @param defaultValue 默认值\n\t * @return Enum类型的值，无则返回Null\n\t */\n\t<E extends Enum<E>> E getEnum(Class<E> clazz, K key, E defaultValue);\n\n\t/**\n\t * 获取Date类型值\n\t * @param key 属性名\n\t * @param defaultValue 默认值\n\t * @return Date类型属性值\n\t */\n\tDate getDate(K key, Date defaultValue);\n\t/*-------------------------- 基本类型 end -------------------------------*/\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport cn.hutool.core.convert.Convert;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Date;\n\n/**\n * 基本类型的getter接口抽象实现，所有类型的值获取都是通过将getObj获得的值转换而来<br>\n * 用户只需实现getObj方法即可，其他类型将会从Object结果中转换\n * 在不提供默认值的情况下， 如果值不存在或获取错误，返回null<br>\n *\n * @author Looly\n */\npublic interface OptNullBasicTypeFromObjectGetter<K> extends OptNullBasicTypeGetter<K> {\n\t@Override\n\tdefault String getStr(K key, String defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toStr(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Integer getInt(K key, Integer defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toInt(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Short getShort(K key, Short defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toShort(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Boolean getBool(K key, Boolean defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toBool(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Long getLong(K key, Long defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toLong(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Character getChar(K key, Character defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toChar(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Float getFloat(K key, Float defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toFloat(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Double getDouble(K key, Double defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toDouble(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Byte getByte(K key, Byte defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toByte(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault BigDecimal getBigDecimal(K key, BigDecimal defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toBigDecimal(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault BigInteger getBigInteger(K key, BigInteger defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toBigInteger(obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault <E extends Enum<E>> E getEnum(Class<E> clazz, K key, E defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toEnum(clazz, obj, defaultValue);\n\t}\n\n\t@Override\n\tdefault Date getDate(K key, Date defaultValue) {\n\t\tfinal Object obj = getObj(key);\n\t\tif (null == obj) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Convert.toDate(obj, defaultValue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport cn.hutool.core.convert.Convert;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Date;\n\n/**\n * 基本类型的getter接口抽象实现，所有类型的值获取都是通过将String转换而来<br>\n * 用户只需实现getStr方法即可，其他类型将会从String结果中转换 在不提供默认值的情况下， 如果值不存在或获取错误，返回null<br>\n *\n * @author Looly\n */\npublic interface OptNullBasicTypeFromStringGetter<K> extends OptNullBasicTypeGetter<K> {\n\t@Override\n\tdefault Object getObj(K key, Object defaultValue) {\n\t\treturn getStr(key, null == defaultValue ? null : defaultValue.toString());\n\t}\n\n\t@Override\n\tdefault Integer getInt(K key, Integer defaultValue) {\n\t\treturn Convert.toInt(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Short getShort(K key, Short defaultValue) {\n\t\treturn Convert.toShort(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Boolean getBool(K key, Boolean defaultValue) {\n\t\treturn Convert.toBool(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Long getLong(K key, Long defaultValue) {\n\t\treturn Convert.toLong(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Character getChar(K key, Character defaultValue) {\n\t\treturn Convert.toChar(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Float getFloat(K key, Float defaultValue) {\n\t\treturn Convert.toFloat(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Double getDouble(K key, Double defaultValue) {\n\t\treturn Convert.toDouble(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Byte getByte(K key, Byte defaultValue) {\n\t\treturn Convert.toByte(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault BigDecimal getBigDecimal(K key, BigDecimal defaultValue) {\n\t\treturn Convert.toBigDecimal(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault BigInteger getBigInteger(K key, BigInteger defaultValue) {\n\t\treturn Convert.toBigInteger(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault <E extends Enum<E>> E getEnum(Class<E> clazz, K key, E defaultValue) {\n\t\treturn Convert.toEnum(clazz, getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tdefault Date getDate(K key, Date defaultValue) {\n\t\treturn Convert.toDate(getStr(key), defaultValue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java",
    "content": "package cn.hutool.core.getter;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Date;\n\n/**\n * 基本类型的getter接口抽象实现<br>\n * 提供一个统一的接口定义返回不同类型的值（基本类型）<br>\n * 在不提供默认值的情况下， 如果值不存在或获取错误，返回null<br>\n * 用户只需实现{@link OptBasicTypeGetter}接口即可\n * @author Looly\n */\npublic interface OptNullBasicTypeGetter<K> extends BasicTypeGetter<K>, OptBasicTypeGetter<K>{\n\t@Override\n\tdefault Object getObj(K key) {\n\t\treturn getObj(key, null);\n\t}\n\n\t/**\n\t * 获取字符串型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault String getStr(K key){\n\t\treturn this.getStr(key, null);\n\t}\n\n\t/**\n\t * 获取int型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Integer getInt(K key) {\n\t\treturn this.getInt(key, null);\n\t}\n\n\t/**\n\t * 获取short型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Short getShort(K key){\n\t\treturn this.getShort(key, null);\n\t}\n\n\t/**\n\t * 获取boolean型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Boolean getBool(K key){\n\t\treturn this.getBool(key, null);\n\t}\n\n\t/**\n\t * 获取long型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Long getLong(K key){\n\t\treturn this.getLong(key, null);\n\t}\n\n\t/**\n\t * 获取char型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Character getChar(K key){\n\t\treturn this.getChar(key, null);\n\t}\n\n\t/**\n\t * 获取float型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Float getFloat(K key){\n\t\treturn this.getFloat(key, null);\n\t}\n\n\t/**\n\t * 获取double型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Double getDouble(K key){\n\t\treturn this.getDouble(key, null);\n\t}\n\n\t/**\n\t * 获取byte型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Byte getByte(K key){\n\t\treturn this.getByte(key, null);\n\t}\n\n\t/**\n\t * 获取BigDecimal型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault BigDecimal getBigDecimal(K key){\n\t\treturn this.getBigDecimal(key, null);\n\t}\n\n\t/**\n\t * 获取BigInteger型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault BigInteger getBigInteger(K key){\n\t\treturn this.getBigInteger(key, null);\n\t}\n\n\t/**\n\t * 获取Enum型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param clazz Enum 的 Class\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault <E extends Enum<E>> E getEnum(Class<E> clazz, K key) {\n\t\treturn this.getEnum(clazz, key, null);\n\t}\n\n\t/**\n\t * 获取Date型属性值<br>\n\t * 无值或获取错误返回null\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\t@Override\n\tdefault Date getDate(K key) {\n\t\treturn this.getDate(key, null);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/getter/package-info.java",
    "content": "/**\n * getXXX方法的接口和抽象实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.getter;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/BackgroundRemoval.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileTypeUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.imageio.ImageIO;\nimport javax.swing.ImageIcon;\nimport java.awt.Color;\nimport java.awt.Graphics;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * <p>图片背景识别处理、背景替换、背景设置为矢量图</p>\n * <p>根据一定规则算出图片背景色的RGB值，进行替换</p>\n * <p>2020-05-21 16:36</p>\n *\n * @author Dai Yuanchuan\n **/\npublic class BackgroundRemoval {\n\n\t/**\n\t * 目前暂时支持的图片类型数组\n\t * 其他格式的不保证结果\n\t */\n\tpublic static String[] IMAGES_TYPE = {\"jpg\", \"png\"};\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param inputPath  要处理图片的路径\n\t * @param outputPath 输出图片的路径\n\t * @param tolerance  容差值[根据图片的主题色,加入容差值,值的范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(String inputPath, String outputPath, int tolerance) {\n\t\treturn backgroundRemoval(new File(inputPath), new File(outputPath), tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param input     需要进行操作的图片\n\t * @param output    最后输出的文件\n\t * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(File input, File output, int tolerance) {\n\t\treturn backgroundRemoval(input, output, null, tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param input     需要进行操作的图片\n\t * @param output    最后输出的文件\n\t * @param override  指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(File input, File output, Color override, int tolerance) {\n\t\tif (fileTypeValidation(input, IMAGES_TYPE)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\t// 获取图片左上、中上、右上、右中、右下、下中、左下、左中、8个像素点rgb的16进制值\n\t\t\tBufferedImage bufferedImage = ImageIO.read(input);\n\t\t\t// 图片输出的格式为 png\n\t\t\treturn ImageIO.write(backgroundRemoval(bufferedImage, override, tolerance), \"png\", output);\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param bufferedImage 需要进行处理的图片流\n\t * @param override      指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance     容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理好的图片流\n\t */\n\tpublic static BufferedImage backgroundRemoval(BufferedImage bufferedImage, Color override, int tolerance) {\n\t\t// 容差值 最大255 最小0\n\t\ttolerance = Math.min(255, Math.max(tolerance, 0));\n\t\t// 绘制icon\n\t\tImageIcon imageIcon = new ImageIcon(bufferedImage);\n\t\tBufferedImage image = new BufferedImage(imageIcon.getIconWidth(), imageIcon.getIconHeight(),\n\t\t\t\tBufferedImage.TYPE_4BYTE_ABGR);\n\t\t// 绘图工具\n\t\tGraphics graphics = image.getGraphics();\n\t\tgraphics.drawImage(imageIcon.getImage(), 0, 0, imageIcon.getImageObserver());\n\t\t// 需要删除的RGB元素\n\t\tString[] removeRgb = getRemoveRgb(bufferedImage);\n\t\t// 获取图片的大概主色调\n\t\tString mainColor = getMainColor(bufferedImage);\n\t\tint alpha = 0;\n\t\tfor (int y = image.getMinY(); y < image.getHeight(); y++) {\n\t\t\tfor (int x = image.getMinX(); x < image.getWidth(); x++) {\n\t\t\t\t// 获取像素的16进制\n\t\t\t\tint rgb = image.getRGB(x, y);\n\t\t\t\tString hex = ImgUtil.toHex((rgb & 0xff0000) >> 16, (rgb & 0xff00) >> 8, (rgb & 0xff));\n\t\t\t\tboolean isTrue = ArrayUtil.contains(removeRgb, hex) ||\n\t\t\t\t\t\tareColorsWithinTolerance(hexToRgb(mainColor), new Color(Integer.parseInt(hex.substring(1), 16)), tolerance);\n\t\t\t\tif (isTrue) {\n\t\t\t\t\trgb = override == null ? ((alpha + 1) << 24) | (rgb & 0x00ffffff) : override.getRGB();\n\t\t\t\t}\n\t\t\t\timage.setRGB(x, y, rgb);\n\t\t\t}\n\t\t}\n\t\tgraphics.drawImage(image, 0, 0, imageIcon.getImageObserver());\n\t\treturn image;\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param outputStream 需要进行处理的图片字节数组流\n\t * @param override     指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance    容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理好的图片流\n\t */\n\tpublic static BufferedImage backgroundRemoval(ByteArrayOutputStream outputStream, Color override, int tolerance) {\n\t\ttry {\n\t\t\treturn backgroundRemoval(ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray())), override, tolerance);\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 获取要删除的 RGB 元素\n\t * 分别获取图片左上、中上、右上、右中、右下、下中、左下、左中、8个像素点rgb的16进制值\n\t *\n\t * @param image 图片流\n\t * @return String数组 包含 各个位置的rgb数值\n\t */\n\tprivate static String[] getRemoveRgb(BufferedImage image) {\n\t\t// 获取图片流的宽和高\n\t\tint width = image.getWidth() - 1;\n\t\tint height = image.getHeight() - 1;\n\t\t// 左上\n\t\tint leftUpPixel = image.getRGB(1, 1);\n\t\tString leftUp = ImgUtil.toHex((leftUpPixel & 0xff0000) >> 16, (leftUpPixel & 0xff00) >> 8, (leftUpPixel & 0xff));\n\t\t// 上中\n\t\tint upMiddlePixel = image.getRGB(width / 2, 1);\n\t\tString upMiddle = ImgUtil.toHex((upMiddlePixel & 0xff0000) >> 16, (upMiddlePixel & 0xff00) >> 8, (upMiddlePixel & 0xff));\n\t\t// 右上\n\t\tint rightUpPixel = image.getRGB(width, 1);\n\t\tString rightUp = ImgUtil.toHex((rightUpPixel & 0xff0000) >> 16, (rightUpPixel & 0xff00) >> 8, (rightUpPixel & 0xff));\n\t\t// 右中\n\t\tint rightMiddlePixel = image.getRGB(width, height / 2);\n\t\tString rightMiddle = ImgUtil.toHex((rightMiddlePixel & 0xff0000) >> 16, (rightMiddlePixel & 0xff00) >> 8, (rightMiddlePixel & 0xff));\n\t\t// 右下\n\t\tint lowerRightPixel = image.getRGB(width, height);\n\t\tString lowerRight = ImgUtil.toHex((lowerRightPixel & 0xff0000) >> 16, (lowerRightPixel & 0xff00) >> 8, (lowerRightPixel & 0xff));\n\t\t// 下中\n\t\tint lowerMiddlePixel = image.getRGB(width / 2, height);\n\t\tString lowerMiddle = ImgUtil.toHex((lowerMiddlePixel & 0xff0000) >> 16, (lowerMiddlePixel & 0xff00) >> 8, (lowerMiddlePixel & 0xff));\n\t\t// 左下\n\t\tint leftLowerPixel = image.getRGB(1, height);\n\t\tString leftLower = ImgUtil.toHex((leftLowerPixel & 0xff0000) >> 16, (leftLowerPixel & 0xff00) >> 8, (leftLowerPixel & 0xff));\n\t\t// 左中\n\t\tint leftMiddlePixel = image.getRGB(1, height / 2);\n\t\tString leftMiddle = ImgUtil.toHex((leftMiddlePixel & 0xff0000) >> 16, (leftMiddlePixel & 0xff00) >> 8, (leftMiddlePixel & 0xff));\n\t\t// 需要删除的RGB元素\n\t\treturn new String[]{leftUp, upMiddle, rightUp, rightMiddle, lowerRight, lowerMiddle, leftLower, leftMiddle};\n\t}\n\n\t/**\n\t * 十六进制颜色码转RGB颜色值\n\t *\n\t * @param hex 十六进制颜色码\n\t * @return 返回 RGB颜色值\n\t */\n\tpublic static Color hexToRgb(String hex) {\n\t\treturn new Color(Integer.parseInt(hex.substring(1), 16));\n\t}\n\n\n\t/**\n\t * 判断颜色是否在容差范围内\n\t * 对比两个颜色的相似度，判断这个相似度是否小于 tolerance 容差值\n\t *\n\t * @param color1    颜色1\n\t * @param color2    颜色2\n\t * @param tolerance 容差值\n\t * @return 返回true:两个颜色在容差值之内 false: 不在\n\t */\n\tpublic static boolean areColorsWithinTolerance(Color color1, Color color2, int tolerance) {\n\t\treturn areColorsWithinTolerance(color1, color2, new Color(tolerance, tolerance, tolerance));\n\t}\n\n\t/**\n\t * 判断颜色是否在容差范围内\n\t * 对比两个颜色的相似度，判断这个相似度是否小于 tolerance 容差值\n\t *\n\t * @param color1    颜色1\n\t * @param color2    颜色2\n\t * @param tolerance 容差色值\n\t * @return 返回true:两个颜色在容差值之内 false: 不在\n\t */\n\tpublic static boolean areColorsWithinTolerance(Color color1, Color color2, Color tolerance) {\n\t\treturn (color1.getRed() - color2.getRed() < tolerance.getRed() && color1\n\t\t\t\t.getRed() - color2.getRed() > -tolerance.getRed())\n\t\t\t\t&& (color1.getBlue() - color2.getBlue() < tolerance\n\t\t\t\t.getBlue() && color1.getBlue() - color2.getBlue() > -tolerance\n\t\t\t\t.getBlue())\n\t\t\t\t&& (color1.getGreen() - color2.getGreen() < tolerance\n\t\t\t\t.getGreen() && color1.getGreen()\n\t\t\t\t- color2.getGreen() > -tolerance.getGreen());\n\t}\n\n\t/**\n\t * 获取图片大概的主题色\n\t * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值\n\t *\n\t * @param input 图片文件路径\n\t * @return 返回一个图片的大概的色值 一个16进制的颜色码\n\t */\n\tpublic static String getMainColor(String input) {\n\t\treturn getMainColor(new File(input));\n\t}\n\n\t/**\n\t * 获取图片大概的主题色\n\t * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值\n\t *\n\t * @param input 图片文件\n\t * @return 返回一个图片的大概的色值 一个16进制的颜色码\n\t */\n\tpublic static String getMainColor(File input) {\n\t\ttry {\n\t\t\treturn getMainColor(ImageIO.read(input));\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\treturn \"\";\n\t}\n\n\t/**\n\t * 获取图片大概的主题色\n\t * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值\n\t *\n\t * @param bufferedImage 图片流\n\t * @return 返回一个图片的大概的色值 一个16进制的颜色码\n\t */\n\tpublic static String getMainColor(BufferedImage bufferedImage) {\n\t\tif (bufferedImage == null) {\n\t\t\tthrow new IllegalArgumentException(\"图片流是空的\");\n\t\t}\n\n\t\t// 存储图片的所有RGB元素\n\t\tList<String> list = new ArrayList<>();\n\t\tfor (int y = bufferedImage.getMinY(); y < bufferedImage.getHeight(); y++) {\n\t\t\tfor (int x = bufferedImage.getMinX(); x < bufferedImage.getWidth(); x++) {\n\t\t\t\tint pixel = bufferedImage.getRGB(x, y);\n\t\t\t\tlist.add(((pixel & 0xff0000) >> 16) + \"-\" + ((pixel & 0xff00) >> 8) + \"-\" + (pixel & 0xff));\n\t\t\t}\n\t\t}\n\n\t\tfinal Map<String, Integer> map = new HashMap<>(list.size(), 1);\n\t\tfor (String string : list) {\n\t\t\tInteger integer = map.get(string);\n\t\t\tif (integer == null) {\n\t\t\t\tinteger = 1;\n\t\t\t} else {\n\t\t\t\tinteger++;\n\t\t\t}\n\t\t\tmap.put(string, integer);\n\t\t}\n\t\tString max = StrUtil.EMPTY;\n\t\tlong num = 0;\n\t\tfor (Map.Entry<String, Integer> entry : map.entrySet()) {\n\t\t\tString key = entry.getKey();\n\t\t\tInteger temp = entry.getValue();\n\t\t\tif (StrUtil.isBlank(max) || temp > num) {\n\t\t\t\tmax = key;\n\t\t\t\tnum = temp;\n\t\t\t}\n\t\t}\n\t\tString[] strings = max.split(\"-\");\n\t\t// rgb 的数量只有3个\n\t\tint rgbLength = 3;\n\t\tif (strings.length == rgbLength) {\n\t\t\treturn ImgUtil.toHex(Integer.parseInt(strings[0]), Integer.parseInt(strings[1]),\n\t\t\t\t\tInteger.parseInt(strings[2]));\n\t\t}\n\t\treturn StrUtil.EMPTY;\n\t}\n\n\t// -------------------------------------------------------------------------- private\n\n\t/**\n\t * 文件类型验证\n\t * 根据给定文件类型数据，验证给定文件类型.\n\t *\n\t * @param input      需要进行验证的文件\n\t * @param imagesType 文件包含的类型数组\n\t * @return 返回布尔值 false:给定文件的文件类型在文件数组中  true:给定文件的文件类型 不在给定数组中。\n\t */\n\tprivate static boolean fileTypeValidation(File input, String[] imagesType) {\n\t\tif (!input.exists()) {\n\t\t\tthrow new IllegalArgumentException(\"给定文件为空\");\n\t\t}\n\t\t// 获取图片类型\n\t\tString type = FileTypeUtil.getType(input);\n\t\t// 类型对比\n\t\tif (!ArrayUtil.contains(imagesType, type)) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"文件类型{}不支持\", type));\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/ColorUtil.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.awt.Color;\nimport java.awt.image.BufferedImage;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Random;\n\n/**\n * 颜色工具类\n *\n * @since 5.8.7\n */\npublic class ColorUtil {\n\n\t/**\n\t * RGB颜色范围上限\n\t */\n\tprivate static final int RGB_COLOR_BOUND = 256;\n\n\t/**\n\t * Color对象转16进制表示，例如#fcf6d6\n\t *\n\t * @param color {@link Color}\n\t * @return 16进制的颜色值，例如#fcf6d6\n\t * @since 4.1.14\n\t */\n\tpublic static String toHex(Color color) {\n\t\treturn toHex(color.getRed(), color.getGreen(), color.getBlue());\n\t}\n\n\t/**\n\t * RGB颜色值转换成十六进制颜色码\n\t *\n\t * @param r 红(R)\n\t * @param g 绿(G)\n\t * @param b 蓝(B)\n\t * @return 返回字符串形式的 十六进制颜色码 如\n\t */\n\tpublic static String toHex(int r, int g, int b) {\n\t\t// rgb 小于 255\n\t\tif (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {\n\t\t\tthrow new IllegalArgumentException(\"RGB must be 0~255!\");\n\t\t}\n\t\treturn String.format(\"#%02X%02X%02X\", r, g, b);\n\t}\n\n\t/**\n\t * 将颜色值转换成具体的颜色类型 汇集了常用的颜色集，支持以下几种形式：\n\t *\n\t * <pre>\n\t * 1. 颜色的英文名（大小写皆可）\n\t * 2. 16进制表示，例如：#fcf6d6或者$fcf6d6\n\t * 3. RGB形式，例如：13,148,252\n\t * </pre>\n\t * <p>\n\t * 方法来自：com.lnwazg.kit\n\t *\n\t * @param colorName 颜色的英文名，16进制表示或RGB表示\n\t * @return {@link Color}\n\t * @since 4.1.14\n\t */\n\tpublic static Color getColor(String colorName) {\n\t\tif (StrUtil.isBlank(colorName)) {\n\t\t\treturn null;\n\t\t}\n\t\tcolorName = colorName.toUpperCase();\n\n\t\tif (\"BLACK\".equals(colorName)) {\n\t\t\treturn Color.BLACK;\n\t\t} else if (\"WHITE\".equals(colorName)) {\n\t\t\treturn Color.WHITE;\n\t\t} else if (\"LIGHTGRAY\".equals(colorName) || \"LIGHT_GRAY\".equals(colorName)) {\n\t\t\treturn Color.LIGHT_GRAY;\n\t\t} else if (\"GRAY\".equals(colorName)) {\n\t\t\treturn Color.GRAY;\n\t\t} else if (\"DARKGRAY\".equals(colorName) || \"DARK_GRAY\".equals(colorName)) {\n\t\t\treturn Color.DARK_GRAY;\n\t\t} else if (\"RED\".equals(colorName)) {\n\t\t\treturn Color.RED;\n\t\t} else if (\"PINK\".equals(colorName)) {\n\t\t\treturn Color.PINK;\n\t\t} else if (\"ORANGE\".equals(colorName)) {\n\t\t\treturn Color.ORANGE;\n\t\t} else if (\"YELLOW\".equals(colorName)) {\n\t\t\treturn Color.YELLOW;\n\t\t} else if (\"GREEN\".equals(colorName)) {\n\t\t\treturn Color.GREEN;\n\t\t} else if (\"MAGENTA\".equals(colorName)) {\n\t\t\treturn Color.MAGENTA;\n\t\t} else if (\"CYAN\".equals(colorName)) {\n\t\t\treturn Color.CYAN;\n\t\t} else if (\"BLUE\".equals(colorName)) {\n\t\t\treturn Color.BLUE;\n\t\t} else if (\"DARKGOLD\".equals(colorName) || \"DARK_GOLD\".equals(colorName)) {\n\t\t\t// 暗金色\n\t\t\treturn hexToColor(\"#9e7e67\");\n\t\t} else if (\"LIGHTGOLD\".equals(colorName) || \"LIGHT_GOLD\".equals(colorName)) {\n\t\t\t// 亮金色\n\t\t\treturn hexToColor(\"#ac9c85\");\n\t\t} else if (StrUtil.startWith(colorName, '#')) {\n\t\t\treturn hexToColor(colorName);\n\t\t} else if (StrUtil.startWith(colorName, '$')) {\n\t\t\t// 由于#在URL传输中无法传输，因此用$代替#\n\t\t\treturn hexToColor(\"#\" + colorName.substring(1));\n\t\t} else {\n\t\t\t// rgb值\n\t\t\tfinal List<String> rgb = StrUtil.split(colorName, ',');\n\t\t\tif (3 == rgb.size()) {\n\t\t\t\tfinal Integer r = Convert.toInt(rgb.get(0));\n\t\t\t\tfinal Integer g = Convert.toInt(rgb.get(1));\n\t\t\t\tfinal Integer b = Convert.toInt(rgb.get(2));\n\t\t\t\tif (!ArrayUtil.hasNull(r, g, b)) {\n\t\t\t\t\treturn new Color(r, g, b);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取一个RGB值对应的颜色\n\t *\n\t * @param rgb RGB值\n\t * @return {@link Color}\n\t * @since 4.1.14\n\t */\n\tpublic static Color getColor(int rgb) {\n\t\treturn new Color(rgb);\n\t}\n\n\t/**\n\t * 16进制的颜色值转换为Color对象，例如#fcf6d6\n\t *\n\t * @param hex 16进制的颜色值，例如#fcf6d6\n\t * @return {@link Color}\n\t * @since 4.1.14\n\t */\n\tpublic static Color hexToColor(String hex) {\n\t\treturn getColor(Integer.parseInt(StrUtil.removePrefix(hex, \"#\"), 16));\n\t}\n\n\t/**\n\t * 叠加颜色\n\t * @param color1 颜色1\n\t * @param color2 颜色2\n\t * @return 叠加后的颜色\n\t */\n\tpublic static Color add(Color color1, Color color2) {\n\t\tdouble r1 = color1.getRed();\n\t\tdouble g1 = color1.getGreen();\n\t\tdouble b1 = color1.getBlue();\n\t\tdouble a1 = color1.getAlpha();\n\t\tdouble r2 = color2.getRed();\n\t\tdouble g2 = color2.getGreen();\n\t\tdouble b2 = color2.getBlue();\n\t\tdouble a2 = color2.getAlpha();\n\t\tint r = (int) ((r1 * a1 / 255 + r2 * a2 / 255) / (a1 / 255 + a2 / 255));\n\t\tint g = (int) ((g1 * a1 / 255 + g2 * a2 / 255) / (a1 / 255 + a2 / 255));\n\t\tint b = (int) ((b1 * a1 / 255 + b2 * a2 / 255) / (a1 / 255 + a2 / 255));\n\t\treturn new Color(r, g, b);\n\t}\n\n\t/**\n\t * 生成随机颜色，与指定颜色有一定的区分度\n\t *\n\t * @param compareColor 比较颜色\n\t * @param minDistance 最小色差，按三维坐标计算的距离值\n\t * @return 随机颜色\n\t * @since 5.8.30\n\t */\n\tpublic static Color randomColor(Color compareColor,int minDistance) {\n\t\t// 注意minDistance太大会增加循环次数，保证至少1/3的概率生成成功\n\t\tAssert.isTrue(minDistance < maxDistance(compareColor) / 3 * 2,\n\t\t\t\t\"minDistance is too large, there are too few remaining colors!\");\n\t\tColor color = randomColor();\n\t\twhile (computeColorDistance(compareColor,color) < minDistance) {\n\t\t\tcolor = randomColor();\n\t\t}\n\t\treturn color;\n\t}\n\n\t/**\n\t * 生成随机颜色\n\t *\n\t * @return 随机颜色\n\t * @since 3.1.2\n\t */\n\tpublic static Color randomColor() {\n\t\treturn randomColor(null);\n\t}\n\n\t/**\n\t * 计算给定点与其他点之间的最大可能距离。\n\t *\n\t * @param color 指定颜色\n\t * @return 其余颜色与color的最大距离\n\t */\n\tpublic static int maxDistance(final Color color) {\n\t\tif (null == color) {\n\t\t\t// (0,0,0)到(256,256,256)的距离约等于442.336\n\t\t\treturn 443;\n\t\t}\n\t\tfinal int maxX = RGB_COLOR_BOUND - 2 * color.getRed();\n\t\tfinal int maxY = RGB_COLOR_BOUND - 2 * color.getGreen();\n\t\tfinal int maxZ = RGB_COLOR_BOUND - 2 * color.getBlue();\n\t\treturn (int)Math.sqrt(maxX * maxX + maxY * maxY + maxZ * maxZ);\n\t}\n\n\t/**\n\t * 计算两个颜色之间的色差，按三维坐标距离计算\n\t *\n\t * @param color1 颜色1\n\t * @param color2 颜色2\n\t * @return 色差，按三维坐标距离值\n\t * @since 5.8.30\n\t */\n\tpublic static int computeColorDistance(Color color1, Color color2) {\n\t\tif (null == color1 || null == color2) {\n\t\t\t// (0,0,0)到(256,256,256)的距离约等于442.336\n\t\t\treturn 443;\n\t\t}\n\t\treturn (int) Math.sqrt(Math.pow(color1.getRed() - color2.getRed(), 2)\n\t\t\t\t+ Math.pow(color1.getGreen() - color2.getGreen(), 2)\n\t\t\t\t+ Math.pow(color1.getBlue() - color2.getBlue(), 2));\n\t}\n\n\t/**\n\t * 生成随机颜色\n\t *\n\t * @param random 随机对象 {@link Random}\n\t * @return 随机颜色\n\t * @since 3.1.2\n\t */\n\tpublic static Color randomColor(Random random) {\n\t\tif (null == random) {\n\t\t\trandom = RandomUtil.getRandom();\n\t\t}\n\t\treturn new Color(random.nextInt(RGB_COLOR_BOUND), random.nextInt(RGB_COLOR_BOUND), random.nextInt(RGB_COLOR_BOUND));\n\t}\n\n\t/**\n\t * 获取给定图片的主色调，背景填充用\n\t *\n\t * @param image      {@link BufferedImage}\n\t * @param rgbFilters 过滤多种颜色\n\t * @return {@link String} #ffffff\n\t * @since 5.6.7\n\t */\n\tpublic static String getMainColor(BufferedImage image, int[]... rgbFilters) {\n\t\tint r, g, b;\n\t\tMap<String, Long> countMap = new HashMap<>();\n\t\tint width = image.getWidth();\n\t\tint height = image.getHeight();\n\t\tint minx = image.getMinX();\n\t\tint miny = image.getMinY();\n\t\tfor (int i = minx; i < width; i++) {\n\t\t\tfor (int j = miny; j < height; j++) {\n\t\t\t\tint pixel = image.getRGB(i, j);\n\t\t\t\tr = (pixel & 0xff0000) >> 16;\n\t\t\t\tg = (pixel & 0xff00) >> 8;\n\t\t\t\tb = (pixel & 0xff);\n\t\t\t\tif (matchFilters(r, g, b, rgbFilters)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcountMap.merge(r + \"-\" + g + \"-\" + b, 1L, Long::sum);\n\t\t\t}\n\t\t}\n\t\tString maxColor = null;\n\t\tlong maxCount = 0;\n\t\tfor (Map.Entry<String, Long> entry : countMap.entrySet()) {\n\t\t\tString key = entry.getKey();\n\t\t\tLong count = entry.getValue();\n\t\t\tif (count > maxCount) {\n\t\t\t\tmaxColor = key;\n\t\t\t\tmaxCount = count;\n\t\t\t}\n\t\t}\n\t\tfinal String[] splitRgbStr = StrUtil.splitToArray(maxColor, '-');\n\t\tString rHex = Integer.toHexString(Integer.parseInt(splitRgbStr[0]));\n\t\tString gHex = Integer.toHexString(Integer.parseInt(splitRgbStr[1]));\n\t\tString bHex = Integer.toHexString(Integer.parseInt(splitRgbStr[2]));\n\t\trHex = rHex.length() == 1 ? \"0\" + rHex : rHex;\n\t\tgHex = gHex.length() == 1 ? \"0\" + gHex : gHex;\n\t\tbHex = bHex.length() == 1 ? \"0\" + bHex : bHex;\n\t\treturn \"#\" + rHex + gHex + bHex;\n\t}\n\n\t/**\n\t * 给定RGB是否匹配过滤器中任何一个RGB颜色\n\t *\n\t * @param r          R\n\t * @param g          G\n\t * @param b          B\n\t * @param rgbFilters 颜色过滤器\n\t * @return 是否匹配\n\t */\n\tprivate static boolean matchFilters(int r, int g, int b, int[]... rgbFilters) {\n\t\tif (ArrayUtil.isNotEmpty(rgbFilters)) {\n\t\t\tfor (int[] rgbFilter : rgbFilters) {\n\t\t\t\tif (r == rgbFilter[0] && g == rgbFilter[1] && b == rgbFilter[2]) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/FontUtil.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.IORuntimeException;\n\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.FontFormatException;\nimport java.awt.FontMetrics;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * AWT中字体相关工具类\n *\n * @author looly\n * @since 5.3.6\n */\npublic class FontUtil {\n\n\t/**\n\t * 创建默认字体\n\t *\n\t * @return 默认字体\n\t */\n\tpublic static Font createFont() {\n\t\treturn new Font(null);\n\t}\n\n\t/**\n\t * 创建SansSerif字体\n\t *\n\t * @param size 字体大小\n\t * @return 字体\n\t */\n\tpublic static Font createSansSerifFont(int size) {\n\t\treturn createFont(Font.SANS_SERIF, size);\n\t}\n\n\t/**\n\t * 创建指定名称的字体\n\t *\n\t * @param name 字体名称\n\t * @param size 字体大小\n\t * @return 字体\n\t */\n\tpublic static Font createFont(String name, int size) {\n\t\treturn new Font(name, Font.PLAIN, size);\n\t}\n\n\t/**\n\t * 根据文件创建字体<br>\n\t * 首先尝试创建{@link Font#TRUETYPE_FONT}字体，此类字体无效则创建{@link Font#TYPE1_FONT}\n\t *\n\t * @param fontFile 字体文件\n\t * @return {@link Font}\n\t */\n\tpublic static Font createFont(File fontFile) {\n\t\ttry {\n\t\t\treturn Font.createFont(Font.TRUETYPE_FONT, fontFile);\n\t\t} catch (FontFormatException e) {\n\t\t\t// True Type字体无效时使用Type1字体\n\t\t\ttry {\n\t\t\t\treturn Font.createFont(Font.TYPE1_FONT, fontFile);\n\t\t\t} catch (Exception e1) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 根据文件创建字体<br>\n\t * 首先尝试创建{@link Font#TRUETYPE_FONT}字体，此类字体无效则创建{@link Font#TYPE1_FONT}\n\t *\n\t * @param fontStream 字体流\n\t * @return {@link Font}\n\t */\n\tpublic static Font createFont(InputStream fontStream) {\n\t\ttry {\n\t\t\treturn Font.createFont(Font.TRUETYPE_FONT, fontStream);\n\t\t} catch (FontFormatException e) {\n\t\t\t// True Type字体无效时使用Type1字体\n\t\t\ttry {\n\t\t\t\treturn Font.createFont(Font.TYPE1_FONT, fontStream);\n\t\t\t} catch (Exception e1) {\n\t\t\t\tthrow new UtilException(e1);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得字体对应字符串的长宽信息\n\t *\n\t * @param metrics {@link FontMetrics}\n\t * @param str  字符串\n\t * @return 长宽信息\n\t */\n\tpublic static Dimension getDimension(FontMetrics metrics, String str) {\n\t\tfinal int width = metrics.stringWidth(str);\n\t\tfinal int height = metrics.getAscent() - metrics.getLeading() - metrics.getDescent();\n\n\t\treturn new Dimension(width, height);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/GraphicsUtil.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.awt.AlphaComposite;\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.FontMetrics;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.Point;\nimport java.awt.Rectangle;\nimport java.awt.RenderingHints;\nimport java.awt.image.BufferedImage;\n\n/**\n * {@link Graphics}相关工具类\n *\n * @author looly\n * @since 4.5.2\n */\npublic class GraphicsUtil {\n\n\t/**\n\t * 创建{@link Graphics2D}\n\t *\n\t * @param image {@link BufferedImage}\n\t * @param color {@link Color}背景颜色以及当前画笔颜色，{@code null}表示不设置背景色\n\t * @return {@link Graphics2D}\n\t * @since 4.5.2\n\t */\n\tpublic static Graphics2D createGraphics(BufferedImage image, Color color) {\n\t\tfinal Graphics2D g = image.createGraphics();\n\n\t\tif (null != color) {\n\t\t\t// 填充背景\n\t\t\tg.setColor(color);\n\t\t\tg.fillRect(0, 0, image.getWidth(), image.getHeight());\n\t\t}\n\n\t\treturn g;\n\t}\n\n\t/**\n\t * 获取文字居中高度的Y坐标（距离上边距距离）<br>\n\t * 此方法依赖FontMetrics，如果获取失败，默认为背景高度的1/3\n\t *\n\t * @param g                {@link Graphics2D}画笔\n\t * @param backgroundHeight 背景高度\n\t * @return 最小高度，-1表示无法获取\n\t * @since 4.5.17\n\t */\n\tpublic static int getCenterY(Graphics g, int backgroundHeight) {\n\t\t// 获取允许文字最小高度\n\t\tFontMetrics metrics = null;\n\t\ttry {\n\t\t\tmetrics = g.getFontMetrics();\n\t\t} catch (Exception e) {\n\t\t\t// 此处报告bug某些情况下会抛出IndexOutOfBoundsException，在此做容错处理\n\t\t}\n\t\tint y;\n\t\tif (null != metrics) {\n\t\t\ty = (backgroundHeight - metrics.getHeight()) / 2 + metrics.getAscent();\n\t\t} else {\n\t\t\ty = backgroundHeight / 3;\n\t\t}\n\t\treturn y;\n\t}\n\n\t/**\n\t * 绘制字符串，使用随机颜色，默认抗锯齿\n\t *\n\t * @param g      {@link Graphics}画笔\n\t * @param str    字符串\n\t * @param font   字体\n\t * @param width  字符串总宽度\n\t * @param height 字符串背景高度\n\t * @return 画笔对象\n\t * @since 4.5.10\n\t */\n\tpublic static Graphics drawStringColourful(Graphics g, String str, Font font, int width, int height) {\n\t\treturn drawString(g, str, font, null, width, height, null, 0);\n\t}\n\n\t/**\n\t * 绘制字符串，使用随机颜色，默认抗锯齿\n\t *\n\t * @param g      {@link Graphics}画笔\n\t * @param str    字符串\n\t * @param font   字体\n\t * @param width  字符串总宽度\n\t * @param height 字符串背景高度\n\t * @param compareColor 用于比对的颜色\n\t * @param minColorDistance 随机生成的颜色与对比颜色的最小色差，小于此值则重新生成颜色\n\t * @return 画笔对象\n\t * @since 5.8.30\n\t */\n\tpublic static Graphics drawStringColourful(Graphics g, String str, Font font, int width, int height, Color compareColor, int minColorDistance) {\n\t\treturn drawString(g, str, font, null, width, height, compareColor, minColorDistance);\n\t}\n\n\t/**\n\t * 绘制字符串，使用随机颜色，并且与背景颜色保持一定色差，默认抗锯齿\n\t *\n\t * @param g      {@link Graphics}画笔\n\t * @param str    字符串\n\t * @param font   字体\n\t * @param width  字符串总宽度\n\t * @param height 字符串背景高度\n\t * @param backgroundColor 背景颜色\n\t * @return 画笔对象\n\t * @since 4.5.10\n\t */\n\tpublic static Graphics drawStringColourful(Graphics g, String str, Font font, int width, int height, Color backgroundColor) {\n\t\t// 默认色差为最大色差的1/2\n\t\treturn drawString(g, str, font, null, width, height, backgroundColor, ColorUtil.maxDistance(backgroundColor) / 2);\n\t}\n\n\t/**\n\t * 绘制字符串，默认抗锯齿\n\t *\n\t * @param g      {@link Graphics}画笔\n\t * @param str    字符串\n\t * @param font   字体\n\t * @param color  字体颜色，{@code null} 表示使用随机颜色（每个字符单独随机）\n\t * @param width  字符串背景的宽度\n\t * @param height 字符串背景的高度\n\t * @return 画笔对象\n\t * @since 4.5.10\n\t */\n\tpublic static Graphics drawString(Graphics g, String str, Font font, Color color, int width, int height) {\n\t\treturn drawString(g, str, font, color, width, height, null, 0);\n\t}\n\n\t/**\n\t * 绘制字符串，默认抗锯齿\n\t *\n\t * @param g      {@link Graphics}画笔\n\t * @param str    字符串\n\t * @param font   字体\n\t * @param color  字体颜色，{@code null} 表示使用随机颜色（每个字符单独随机）\n\t * @param width  字符串背景的宽度\n\t * @param height 字符串背景的高度\n\t * @param compareColor 用于比对的颜色\n\t * @param minColorDistance 随机生成的颜色与对比颜色的最小色差，小于此值则重新生成颜色\n\t * @return 画笔对象\n\t * @since 5.8.30\n\t */\n\tpublic static Graphics drawString(Graphics g, String str, Font font, Color color, int width, int height, Color compareColor, int minColorDistance) {\n\t\t// 抗锯齿\n\t\tif (g instanceof Graphics2D) {\n\t\t\t((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\t\t}\n\t\t// 创建字体\n\t\tg.setFont(font);\n\n\t\t// 文字高度（必须在设置字体后调用）\n\t\tint midY = getCenterY(g, height);\n\t\tif (null != color) {\n\t\t\tg.setColor(color);\n\t\t}\n\n\t\tfinal int len = str.length();\n\t\tint charWidth = width / len;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tif (null == color) {\n\t\t\t\t// 产生随机的颜色值，让输出的每个字符的颜色值都将不同。\n\t\t\t\tif (null != compareColor && minColorDistance > 0) {\n\t\t\t\t\tg.setColor(ImgUtil.randomColor(compareColor,minColorDistance));\n\t\t\t\t}else {\n\t\t\t\t\tg.setColor(ImgUtil.randomColor());\n\t\t\t\t}\n\t\t\t}\n\t\t\tg.drawString(String.valueOf(str.charAt(i)), i * charWidth, midY);\n\t\t}\n\t\treturn g;\n\t}\n\n\t/**\n\t * 绘制字符串，默认抗锯齿。<br>\n\t * 此方法定义一个矩形区域和坐标，文字基于这个区域中间偏移x,y绘制。\n\t *\n\t * @param g         {@link Graphics}画笔\n\t * @param str       字符串\n\t * @param font      字体，字体大小决定了在背景中绘制的大小\n\t * @param color     字体颜色，{@code null} 表示使用黑色\n\t * @param rectangle 字符串绘制坐标和大小，此对象定义了绘制字符串的区域大小和偏移位置\n\t * @return 画笔对象\n\t * @since 4.5.10\n\t */\n\tpublic static Graphics drawString(Graphics g, String str, Font font, Color color, Rectangle rectangle) {\n\t\t// 背景长宽\n\t\tfinal int backgroundWidth = rectangle.width;\n\t\tfinal int backgroundHeight = rectangle.height;\n\n\t\t//获取字符串本身的长宽\n\t\tDimension dimension;\n\t\ttry {\n\t\t\tdimension = FontUtil.getDimension(g.getFontMetrics(font), str);\n\t\t} catch (Exception e) {\n\t\t\t// 此处报告bug某些情况下会抛出IndexOutOfBoundsException，在此做容错处理\n\t\t\tdimension = new Dimension(backgroundWidth / 3, backgroundHeight / 3);\n\t\t}\n\n\t\trectangle.setSize(dimension.width, dimension.height);\n\t\tfinal Point point = ImgUtil.getPointBaseCentre(rectangle, backgroundWidth, backgroundHeight);\n\n\t\treturn drawString(g, str, font, color, point);\n\t}\n\n\t/**\n\t * 绘制字符串，默认抗锯齿\n\t *\n\t * @param g     {@link Graphics}画笔\n\t * @param str   字符串\n\t * @param font  字体，字体大小决定了在背景中绘制的大小\n\t * @param color 字体颜色，{@code null} 表示使用黑色\n\t * @param point 绘制字符串的位置坐标\n\t * @return 画笔对象\n\t * @since 5.3.6\n\t */\n\tpublic static Graphics drawString(Graphics g, String str, Font font, Color color, Point point) {\n\t\t// 抗锯齿\n\t\tif (g instanceof Graphics2D) {\n\t\t\t((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\t\t}\n\n\t\tg.setFont(font);\n\t\tg.setColor(ObjectUtil.defaultIfNull(color, Color.BLACK));\n\t\tg.drawString(str, point.x, point.y);\n\n\t\treturn g;\n\t}\n\n\t/**\n\t * 绘制图片\n\t *\n\t * @param g     画笔\n\t * @param img   要绘制的图片\n\t * @param point 绘制的位置，基于左上角\n\t * @return 画笔对象\n\t */\n\tpublic static Graphics drawImg(Graphics g, Image img, Point point) {\n\t\treturn drawImg(g, img,\n\t\t\t\tnew Rectangle(point.x, point.y, img.getWidth(null), img.getHeight(null)));\n\t}\n\n\t/**\n\t * 绘制图片\n\t *\n\t * @param g         画笔\n\t * @param img       要绘制的图片\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height,，基于左上角\n\t * @return 画笔对象\n\t */\n\tpublic static Graphics drawImg(Graphics g, Image img, Rectangle rectangle) {\n\t\tg.drawImage(img, rectangle.x, rectangle.y, rectangle.width, rectangle.height, null); // 绘制切割后的图\n\t\treturn g;\n\t}\n\n\t/**\n\t * 设置画笔透明度\n\t *\n\t * @param g     画笔\n\t * @param alpha 透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 画笔\n\t */\n\tpublic static Graphics2D setAlpha(Graphics2D g, float alpha) {\n\t\tg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));\n\t\treturn g;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/Img.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.imageio.ImageIO;\nimport javax.imageio.stream.ImageInputStream;\nimport javax.imageio.stream.ImageOutputStream;\nimport java.awt.*;\nimport java.awt.color.ColorSpace;\nimport java.awt.geom.AffineTransform;\nimport java.awt.geom.Ellipse2D;\nimport java.awt.geom.RoundRectangle2D;\nimport java.awt.image.BufferedImage;\nimport java.awt.image.CropImageFilter;\nimport java.awt.image.ImageFilter;\nimport java.io.*;\nimport java.net.URL;\nimport java.nio.file.Path;\n\n/**\n * 图像编辑器\n *\n * @author looly\n * @since 4.1.5\n */\npublic class Img implements Flushable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final BufferedImage srcImage;\n\tprivate Image targetImage;\n\t/**\n\t * 目标图片文件格式，用于写出\n\t */\n\tprivate String targetImageType;\n\t/**\n\t * 计算x,y坐标的时候是否从中心做为原始坐标开始计算\n\t */\n\tprivate boolean positionBaseCentre = true;\n\t/**\n\t * 图片输出质量，用于压缩\n\t */\n\tprivate float quality = -1;\n\t/**\n\t * 图片背景色\n\t */\n\tprivate Color backgroundColor;\n\n\t/**\n\t * 从Path读取图片并开始处理\n\t *\n\t * @param imagePath 图片文件路径\n\t * @return Img\n\t */\n\tpublic static Img from(Path imagePath) {\n\t\treturn from(imagePath.toFile());\n\t}\n\n\t/**\n\t * 从文件读取图片并开始处理\n\t *\n\t * @param imageFile 图片文件\n\t * @return Img\n\t */\n\tpublic static Img from(File imageFile) {\n\t\treturn new Img(ImgUtil.read(imageFile));\n\t}\n\n\t/**\n\t * 从资源对象中读取图片并开始处理\n\t *\n\t * @param resource 图片资源对象\n\t * @return Img\n\t * @since 4.4.1\n\t */\n\tpublic static Img from(Resource resource) {\n\t\treturn from(resource.getStream());\n\t}\n\n\t/**\n\t * 从流读取图片并开始处理\n\t *\n\t * @param in 图片流\n\t * @return Img\n\t */\n\tpublic static Img from(InputStream in) {\n\t\treturn new Img(ImgUtil.read(in));\n\t}\n\n\t/**\n\t * 从ImageInputStream取图片并开始处理\n\t *\n\t * @param imageStream 图片流\n\t * @return Img\n\t */\n\tpublic static Img from(ImageInputStream imageStream) {\n\t\treturn new Img(ImgUtil.read(imageStream));\n\t}\n\n\t/**\n\t * 从URL取图片并开始处理\n\t *\n\t * @param imageUrl 图片URL\n\t * @return Img\n\t */\n\tpublic static Img from(URL imageUrl) {\n\t\treturn new Img(ImgUtil.read(imageUrl));\n\t}\n\n\t/**\n\t * 从Image取图片并开始处理\n\t *\n\t * @param image 图片\n\t * @return Img\n\t */\n\tpublic static Img from(Image image) {\n\t\treturn new Img(ImgUtil.castToBufferedImage(image, ImgUtil.IMAGE_TYPE_JPG));\n\t}\n\n\t/**\n\t * 构造，目标图片类型取决于来源图片类型\n\t *\n\t * @param srcImage 来源图片\n\t */\n\tpublic Img(BufferedImage srcImage) {\n\t\tthis(srcImage, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param srcImage        来源图片\n\t * @param targetImageType 目标图片类型，null则读取来源图片类型\n\t * @since 5.0.7\n\t */\n\tpublic Img(BufferedImage srcImage, String targetImageType) {\n\t\tthis.srcImage = srcImage;\n\t\tif (null == targetImageType) {\n\t\t\tif (srcImage.getType() == BufferedImage.TYPE_INT_ARGB\n\t\t\t\t\t|| srcImage.getType() == BufferedImage.TYPE_INT_ARGB_PRE\n\t\t\t\t\t|| srcImage.getType() == BufferedImage.TYPE_4BYTE_ABGR\n\t\t\t\t\t|| srcImage.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE\n\t\t\t) {\n\t\t\t\ttargetImageType = ImgUtil.IMAGE_TYPE_PNG;\n\t\t\t} else {\n\t\t\t\ttargetImageType = ImgUtil.IMAGE_TYPE_JPG;\n\t\t\t}\n\t\t}\n\t\tthis.targetImageType = targetImageType;\n\t}\n\n\t/**\n\t * 设置目标图片文件格式，用于写出\n\t *\n\t * @param imgType 图片格式\n\t * @return this\n\t * @see ImgUtil#IMAGE_TYPE_JPG\n\t * @see ImgUtil#IMAGE_TYPE_PNG\n\t */\n\tpublic Img setTargetImageType(String imgType) {\n\t\tthis.targetImageType = imgType;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 计算x,y坐标的时候是否从中心做为原始坐标开始计算\n\t *\n\t * @param positionBaseCentre 是否从中心做为原始坐标开始计算\n\t * @return this\n\t * @since 4.1.15\n\t */\n\tpublic Img setPositionBaseCentre(boolean positionBaseCentre) {\n\t\tthis.positionBaseCentre = positionBaseCentre;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置图片输出质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t *\n\t * @param quality 质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t * @return this\n\t * @since 4.3.2\n\t */\n\tpublic Img setQuality(double quality) {\n\t\treturn setQuality((float) quality);\n\t}\n\n\t/**\n\t * 设置图片输出质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t *\n\t * @param quality 质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t * @return this\n\t * @since 4.3.2\n\t */\n\tpublic Img setQuality(float quality) {\n\t\tif (quality > 0 && quality < 1) {\n\t\t\tthis.quality = quality;\n\t\t} else {\n\t\t\tthis.quality = 1;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置图片的背景色\n\t *\n\t * @param backgroundColor{@link Color} 背景色\n\t * @return this\n\t */\n\tpublic Img setBackgroundColor(Color backgroundColor) {\n\t\tthis.backgroundColor = backgroundColor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）\n\t *\n\t * @param scale 缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @return this\n\t */\n\tpublic Img scale(float scale) {\n\t\tif (scale < 0) {\n\t\t\t// 自动修正负数\n\t\t\tscale = -scale;\n\t\t}\n\t\tfinal Image srcImg = getValidSrcImg();\n\n\t\t// PNG图片特殊处理\n\t\tif (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {\n\t\t\t// 修正float转double导致的精度丢失\n\t\t\tfinal double scaleDouble = NumberUtil.toDouble(scale);\n\t\t\tthis.targetImage = ImgUtil.transform(AffineTransform.getScaleInstance(scaleDouble, scaleDouble),\n\t\t\t\t\tImgUtil.toBufferedImage(srcImg, this.targetImageType));\n\t\t} else {\n\t\t\t// 缩放后的图片宽\n\t\t\tfinal int width = NumberUtil.mul((Number) srcImg.getWidth(null), scale).intValue();\n\t\t\t// 缩放后的图片高\n\t\t\tfinal int height = NumberUtil.mul((Number) srcImg.getHeight(null), scale).intValue();\n\t\t\tscale(width, height);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 缩放图像（按长宽缩放）<br>\n\t * 注意：目标长宽与原图不成比例会变形\n\t *\n\t * @param width  目标宽度\n\t * @param height 目标高度\n\t * @return this\n\t */\n\tpublic Img scale(int width, int height) {\n\t\treturn scale(width, height, Image.SCALE_SMOOTH);\n\t}\n\n\t/**\n\t * 缩放图像（按长宽缩放）<br>\n\t * 注意：目标长宽与原图不成比例会变形\n\t *\n\t * @param width     目标宽度\n\t * @param height    目标高度\n\t * @param scaleType 缩放类型，可选{@link Image#SCALE_SMOOTH}平滑模式或{@link Image#SCALE_DEFAULT}默认模式\n\t * @return this\n\t * @since 5.7.18\n\t */\n\tpublic Img scale(int width, int height, int scaleType) {\n\t\tfinal Image srcImg = getValidSrcImg();\n\n\t\tfinal int srcHeight = srcImg.getHeight(null);\n\t\tfinal int srcWidth = srcImg.getWidth(null);\n\t\tif (srcHeight == height && srcWidth == width) {\n\t\t\t// 源与目标长宽一致返回原图\n\t\t\tthis.targetImage = srcImg;\n\t\t\treturn this;\n\t\t}\n\n\t\tif (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {\n\t\t\t// png特殊处理，借助AffineTransform可以实现透明度保留\n\t\t\tfinal double sx = NumberUtil.div(width, srcWidth);// 宽度缩放比\n\t\t\tfinal double sy = NumberUtil.div(height, srcHeight); // 高度缩放比\n\t\t\tthis.targetImage = ImgUtil.transform(AffineTransform.getScaleInstance(sx, sy),\n\t\t\t\t\tImgUtil.toBufferedImage(srcImg, this.targetImageType));\n\t\t} else {\n\t\t\tthis.targetImage = srcImg.getScaledInstance(width, height, scaleType);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 等比缩放图像，此方法按照按照给定的长宽等比缩放图片，按照长宽缩放比最多的一边等比缩放，空白部分填充背景色<br>\n\t * 缩放后默认为jpeg格式\n\t *\n\t * @param width      缩放后的宽度\n\t * @param height     缩放后的高度\n\t * @param fixedColor 比例不对时补充的颜色，不补充为{@code null}\n\t * @return this\n\t */\n\tpublic Img scale(int width, int height, Color fixedColor) {\n\t\tImage srcImage = getValidSrcImg();\n\t\tint srcHeight = srcImage.getHeight(null);\n\t\tint srcWidth = srcImage.getWidth(null);\n\t\tdouble heightRatio = NumberUtil.div(height, srcHeight);\n\t\tdouble widthRatio = NumberUtil.div(width, srcWidth);\n\n\t\t// 浮点数之间的等值判断,基本数据类型不能用==比较,包装数据类型不能用equals来判断。\n\t\tif (NumberUtil.equals(heightRatio, widthRatio)) {\n\t\t\t// 长宽都按照相同比例缩放时，返回缩放后的图片\n\t\t\tscale(width, height);\n\t\t} else if (widthRatio < heightRatio) {\n\t\t\t// 宽缩放比例多就按照宽缩放\n\t\t\tscale(width, (int) (srcHeight * widthRatio));\n\t\t} else {\n\t\t\t// 否则按照高缩放\n\t\t\tscale((int) (srcWidth * heightRatio), height);\n\t\t}\n\n\t\t// 获取缩放后的新的宽和高\n\t\tsrcImage = getValidSrcImg();\n\t\tsrcHeight = srcImage.getHeight(null);\n\t\tsrcWidth = srcImage.getWidth(null);\n\n\t\tfinal BufferedImage image = new BufferedImage(width, height, getTypeInt());\n\t\tGraphics2D g = image.createGraphics();\n\n\t\t// 设置背景\n\t\tif (null != fixedColor) {\n\t\t\tg.setBackground(fixedColor);\n\t\t\tg.clearRect(0, 0, width, height);\n\t\t}\n\n\t\t// 在中间贴图\n\t\tg.drawImage(srcImage, (width - srcWidth) / 2, (height - srcHeight) / 2, srcWidth, srcHeight, fixedColor, null);\n\n\t\tg.dispose();\n\t\tthis.targetImage = image;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)\n\t *\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height\n\t * @return this\n\t */\n\tpublic Img cut(Rectangle rectangle) {\n\t\tfinal Image srcImage = getValidSrcImg();\n\t\tfixRectangle(rectangle, srcImage.getWidth(null), srcImage.getHeight(null));\n\n\t\tfinal ImageFilter cropFilter = new CropImageFilter(rectangle.x, rectangle.y, rectangle.width, rectangle.height);\n\t\tthis.targetImage = ImgUtil.filter(cropFilter, srcImage);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 图像切割为圆形(按指定起点坐标和半径切割)，填充满整个图片（直径取长宽最小值）\n\t *\n\t * @param x 原图的x坐标起始位置\n\t * @param y 原图的y坐标起始位置\n\t * @return this\n\t * @since 4.1.15\n\t */\n\tpublic Img cut(int x, int y) {\n\t\treturn cut(x, y, -1);\n\t}\n\n\t/**\n\t * 图像切割为圆形(按指定起点坐标和半径切割)\n\t *\n\t * @param x      原图的x坐标起始位置\n\t * @param y      原图的y坐标起始位置\n\t * @param radius 半径，小于0表示填充满整个图片（直径取长宽最小值）\n\t * @return this\n\t * @since 4.1.15\n\t */\n\tpublic Img cut(int x, int y, int radius) {\n\t\tfinal Image srcImage = getValidSrcImg();\n\t\tfinal int width = srcImage.getWidth(null);\n\t\tfinal int height = srcImage.getHeight(null);\n\n\t\t// 计算直径\n\t\tfinal int diameter = radius > 0 ? radius * 2 : Math.min(width, height);\n\t\tfinal BufferedImage targetImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB);\n\t\tfinal Graphics2D g = targetImage.createGraphics();\n\t\tg.setClip(new Ellipse2D.Double(0, 0, diameter, diameter));\n\n\t\tif (this.positionBaseCentre) {\n\t\t\tx = x - width / 2 + diameter / 2;\n\t\t\ty = y - height / 2 + diameter / 2;\n\t\t}\n\t\tg.drawImage(srcImage, x, y, null);\n\t\tg.dispose();\n\t\tthis.targetImage = targetImage;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 图片圆角处理\n\t *\n\t * @param arc 圆角弧度，0~1，为长宽占比\n\t * @return this\n\t * @since 4.5.3\n\t */\n\tpublic Img round(double arc) {\n\t\tfinal Image srcImage = getValidSrcImg();\n\t\tfinal int width = srcImage.getWidth(null);\n\t\tfinal int height = srcImage.getHeight(null);\n\n\t\t// 通过弧度占比计算弧度\n\t\tarc = NumberUtil.mul(arc, Math.min(width, height));\n\n\t\tfinal BufferedImage targetImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);\n\t\tfinal Graphics2D g2 = targetImage.createGraphics();\n\t\tg2.setComposite(AlphaComposite.Src);\n\t\t// 抗锯齿\n\t\tg2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\t\tg2.fill(new RoundRectangle2D.Double(0, 0, width, height, arc, arc));\n\t\tg2.setComposite(AlphaComposite.SrcAtop);\n\t\tg2.drawImage(srcImage, 0, 0, null);\n\t\tg2.dispose();\n\t\tthis.targetImage = targetImage;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 彩色转为灰度\n\t *\n\t * @return this\n\t */\n\tpublic Img gray() {\n\t\tthis.targetImage = ImgUtil.colorConvert(ColorSpace.getInstance(ColorSpace.CS_GRAY), getValidSrcBufferedImg());\n\t\treturn this;\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片\n\t *\n\t * @return this\n\t */\n\tpublic Img binary() {\n\t\tthis.targetImage = ImgUtil.copyImage(getValidSrcImg(), BufferedImage.TYPE_BYTE_BINARY);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法只在给定位置写出一个水印字符串\n\t *\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息\n\t * @param x         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 处理后的图像\n\t */\n\tpublic Img pressText(String pressText, Color color, Font font, int x, int y, float alpha) {\n\t\treturn pressText(pressText, color, font, new Point(x, y), alpha);\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法只在给定位置写出一个水印字符串\n\t *\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息\n\t * @param point     绘制字符串的位置坐标\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 处理后的图像\n\t */\n\tpublic Img pressText(String pressText, Color color, Font font, Point point, float alpha) {\n\t\tfinal BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);\n\n\t\tif (null == font) {\n\t\t\t// 默认字体\n\t\t\tfont = FontUtil.createSansSerifFont((int) (targetImage.getHeight() * 0.75));\n\t\t}\n\n\t\tfinal Graphics2D g = targetImage.createGraphics();\n\t\t// 透明度\n\t\tg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));\n\n\t\t// 绘制\n\t\tif (positionBaseCentre) {\n\t\t\t// 基于中心绘制\n\t\t\tGraphicsUtil.drawString(g, pressText, font, color,\n\t\t\t\t\tnew Rectangle(point.x, point.y, targetImage.getWidth(), targetImage.getHeight()));\n\t\t} else {\n\t\t\t// 基于左上角绘制\n\t\t\tGraphicsUtil.drawString(g, pressText, font, color, point);\n\t\t}\n\n\t\t// 收笔\n\t\tg.dispose();\n\t\tthis.targetImage = targetImage;\n\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 给图片添加全屏文字水印\n\t *\n\t * @param pressText  水印文字，文件间的间隔使用尾部添加空格方式实现\n\t * @param color      水印的字体颜色\n\t * @param font       {@link Font} 字体相关信息\n\t * @param lineHeight 行高\n\t * @param degree     旋转角度，（单位：弧度），以圆点（0,0）为圆心，正代表顺时针，负代表逆时针\n\t * @param alpha      透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 处理后的图像\n\t * @since 5.8.0\n\t */\n\tpublic Img pressTextFull(String pressText, Color color, Font font, int lineHeight, int degree, float alpha) {\n\t\tfinal BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);\n\n\t\tif (null == font) {\n\t\t\t// 默认字体\n\t\t\tfont = FontUtil.createSansSerifFont((int) (targetImage.getHeight() * 0.75));\n\t\t}\n\t\tfinal int targetHeight = targetImage.getHeight();\n\t\tfinal int targetWidth = targetImage.getWidth();\n\n\t\t// 创建画笔，并设置透明度和角度\n\t\tfinal Graphics2D g = targetImage.createGraphics();\n\t\tg.setColor(color);\n\t\t// 基于图片中心旋转\n\t\tg.rotate(Math.toRadians(degree), targetWidth >> 1, targetHeight >> 1);\n\t\tg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));\n\n\t\t//获取字符串本身的长宽\n\t\tDimension dimension;\n\t\ttry {\n\t\t\tdimension = FontUtil.getDimension(g.getFontMetrics(font), pressText);\n\t\t} catch (Exception e) {\n\t\t\t// 此处报告bug某些情况下会抛出IndexOutOfBoundsException，在此做容错处理\n\t\t\tdimension = new Dimension(targetWidth / 3, targetHeight / 3);\n\t\t}\n\t\tfinal int intervalHeight = dimension.height * lineHeight;\n\t\t// 在画笔按照画布中心旋转后，达到45度时，上下左右会出现空白区，此处各延申长款的1.5倍实现全覆盖\n\t\tint y = -targetHeight >> 1;\n\t\twhile (y < targetHeight * 1.5) {\n\t\t\tint x = -targetWidth >> 1;\n\t\t\twhile (x < targetWidth * 1.5) {\n\t\t\t\tGraphicsUtil.drawString(g, pressText, font, color, new Point(x, y));\n\t\t\t\tx += dimension.width;\n\t\t\t}\n\t\t\ty += intervalHeight;\n\t\t}\n\t\tg.dispose();\n\n\t\tthis.targetImage = targetImage;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 给图片添加图片水印\n\t *\n\t * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha    透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return this\n\t */\n\tpublic Img pressImage(Image pressImg, int x, int y, float alpha) {\n\t\tfinal int pressImgWidth = pressImg.getWidth(null);\n\t\tfinal int pressImgHeight = pressImg.getHeight(null);\n\t\treturn pressImage(pressImg, new Rectangle(x, y, pressImgWidth, pressImgHeight), alpha);\n\t}\n\n\t/**\n\t * 给图片添加图片水印\n\t *\n\t * @param pressImg  水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height，x,y从背景图片中心计算\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return this\n\t * @since 4.1.14\n\t */\n\tpublic Img pressImage(Image pressImg, Rectangle rectangle, float alpha) {\n\t\tfinal Image targetImg = getValidSrcImg();\n\n\t\tthis.targetImage = draw(ImgUtil.toBufferedImage(targetImg, this.targetImageType), pressImg, rectangle, alpha);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 来自：http://blog.51cto.com/cping1982/130066\n\t *\n\t * @param degree 旋转角度\n\t * @return 旋转后的图片\n\t * @since 3.2.2\n\t */\n\tpublic Img rotate(int degree) {\n\t\tfinal Image image = getValidSrcImg();\n\t\tint width = image.getWidth(null);\n\t\tint height = image.getHeight(null);\n\t\tfinal Rectangle rectangle = calcRotatedSize(width, height, degree);\n\t\tfinal BufferedImage targetImg = new BufferedImage(rectangle.width, rectangle.height, getTypeInt());\n\t\tGraphics2D graphics2d = targetImg.createGraphics();\n\t\t// 抗锯齿\n\t\tgraphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);\n\t\t// 从中心旋转\n\t\tgraphics2d.translate((rectangle.width - width) / 2D, (rectangle.height - height) / 2D);\n\t\tgraphics2d.rotate(Math.toRadians(degree), width / 2D, height / 2D);\n\t\tgraphics2d.drawImage(image, 0, 0, null);\n\t\tgraphics2d.dispose();\n\t\tthis.targetImage = targetImg;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 水平翻转图像\n\t *\n\t * @return this\n\t */\n\tpublic Img flip() {\n\t\tfinal Image image = getValidSrcImg();\n\t\tint width = image.getWidth(null);\n\t\tint height = image.getHeight(null);\n\t\tfinal BufferedImage targetImg = new BufferedImage(width, height, getTypeInt());\n\t\tGraphics2D graphics2d = targetImg.createGraphics();\n\t\tgraphics2d.drawImage(image, 0, 0, width, height, width, 0, 0, height, null);\n\t\tgraphics2d.dispose();\n\n\t\tthis.targetImage = targetImg;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 描边，此方法为向内描边，会覆盖图片相应的位置\n\t *\n\t * @param color 描边颜色，默认黑色\n\t * @param width 边框粗细\n\t * @return this\n\t * @since 5.4.1\n\t */\n\tpublic Img stroke(Color color, float width) {\n\t\treturn stroke(color, new BasicStroke(width));\n\t}\n\n\t/**\n\t * 描边，此方法为向内描边，会覆盖图片相应的位置\n\t *\n\t * @param color  描边颜色，默认黑色\n\t * @param stroke 描边属性，包括粗细、线条类型等，见{@link BasicStroke}\n\t * @return this\n\t * @since 5.4.1\n\t */\n\tpublic Img stroke(Color color, Stroke stroke) {\n\t\tfinal BufferedImage image = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);\n\t\tint width = image.getWidth(null);\n\t\tint height = image.getHeight(null);\n\t\tGraphics2D g = image.createGraphics();\n\n\t\tg.setColor(ObjectUtil.defaultIfNull(color, Color.BLACK));\n\t\tif (null != stroke) {\n\t\t\tg.setStroke(stroke);\n\t\t}\n\n\t\tg.drawRect(0, 0, width - 1, height - 1);\n\n\t\tg.dispose();\n\t\tthis.targetImage = image;\n\n\t\treturn this;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------------- Write\n\n\t/**\n\t * 获取处理过的图片\n\t *\n\t * @return 处理过的图片\n\t */\n\tpublic Image getImg() {\n\t\treturn getValidSrcImg();\n\t}\n\n\t/**\n\t * 写出图像为结果设置格式<br>\n\t * 结果类型设定见{@link #setTargetImageType(String)}\n\t *\n\t * @param out 写出到的目标流\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean write(OutputStream out) throws IORuntimeException {\n\t\treturn write(ImgUtil.getImageOutputStream(out));\n\t}\n\n\t/**\n\t * 写出图像为结果设置格式<br>\n\t * 结果类型设定见{@link #setTargetImageType(String)}\n\t *\n\t * @param targetImageStream 写出到的目标流\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean write(ImageOutputStream targetImageStream) throws IORuntimeException {\n\t\tAssert.notBlank(this.targetImageType, \"Target image type is blank !\");\n\t\tAssert.notNull(targetImageStream, \"Target output stream is null !\");\n\n\t\tfinal Image targetImage = (null == this.targetImage) ? this.srcImage : this.targetImage;\n\t\tAssert.notNull(targetImage, \"Target image is null !\");\n\n\t\treturn ImgUtil.write(targetImage, this.targetImageType, targetImageStream, this.quality, this.backgroundColor);\n\t}\n\n\t/**\n\t * 写出图像为目标文件扩展名对应的格式\n\t *\n\t * @param targetFile 目标文件\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean write(File targetFile) throws IORuntimeException {\n\t\tfinal String formatName = FileUtil.extName(targetFile);\n\t\tif (StrUtil.isNotBlank(formatName)) {\n\t\t\tthis.targetImageType = formatName;\n\t\t}\n\n\t\tif (targetFile.exists()) {\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\ttargetFile.delete();\n\t\t}\n\n\t\tImageOutputStream out = null;\n\t\ttry {\n\t\t\tout = ImgUtil.getImageOutputStream(targetFile);\n\t\t\treturn write(out);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void flush() {\n\t\tImgUtil.flush(this.srcImage);\n\t\tImgUtil.flush(this.targetImage);\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 将图片绘制在背景上\n\t *\n\t * @param backgroundImg 背景图片\n\t * @param img           要绘制的图片\n\t * @param rectangle     矩形对象，表示矩形区域的x，y，width，height，x,y从背景图片中心计算（如果positionBaseCentre为true）\n\t * @param alpha         透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 绘制后的背景\n\t */\n\tprivate BufferedImage draw(BufferedImage backgroundImg, Image img, Rectangle rectangle, float alpha) {\n\t\tfinal Graphics2D g = backgroundImg.createGraphics();\n\t\tGraphicsUtil.setAlpha(g, alpha);\n\n\t\tfixRectangle(rectangle, backgroundImg.getWidth(), backgroundImg.getHeight());\n\t\tGraphicsUtil.drawImg(g, img, rectangle);\n\n\t\tg.dispose();\n\t\treturn backgroundImg;\n\t}\n\n\t/**\n\t * 获取int类型的图片类型\n\t *\n\t * @return 图片类型\n\t * @see BufferedImage#TYPE_INT_ARGB\n\t * @see BufferedImage#TYPE_INT_RGB\n\t */\n\tprivate int getTypeInt() {\n\t\t//noinspection SwitchStatementWithTooFewBranches\n\t\tswitch (this.targetImageType) {\n\t\t\tcase ImgUtil.IMAGE_TYPE_PNG:\n\t\t\t\treturn BufferedImage.TYPE_INT_ARGB;\n\t\t\tdefault:\n\t\t\t\treturn BufferedImage.TYPE_INT_RGB;\n\t\t}\n\t}\n\n\t/**\n\t * 获取有效的源图片，首先检查上一次处理的结果图片，如无则使用用户传入的源图片\n\t *\n\t * @return 有效的源图片\n\t */\n\tprivate Image getValidSrcImg() {\n\t\treturn ObjectUtil.defaultIfNull(this.targetImage, this.srcImage);\n\t}\n\n\t/**\n\t * 获取有效的源{@link BufferedImage}图片，首先检查上一次处理的结果图片，如无则使用用户传入的源图片\n\t *\n\t * @return 有效的源图片\n\t * @since 5.7.8\n\t */\n\tprivate BufferedImage getValidSrcBufferedImg() {\n\t\treturn ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);\n\t}\n\n\t/**\n\t * 修正矩形框位置，如果{@link Img#setPositionBaseCentre(boolean)} 设为{@code true}，<br>\n\t * 则坐标修正为基于图形中心，否则基于左上角\n\t *\n\t * @param rectangle  矩形\n\t * @param baseWidth  参考宽\n\t * @param baseHeight 参考高\n\t * @return 修正后的{@link Rectangle}\n\t * @since 4.1.15\n\t */\n\tprivate Rectangle fixRectangle(Rectangle rectangle, int baseWidth, int baseHeight) {\n\t\tif (this.positionBaseCentre) {\n\t\t\tfinal Point pointBaseCentre = ImgUtil.getPointBaseCentre(rectangle, baseWidth, baseHeight);\n\t\t\t// 修正图片位置从背景的中心计算\n\t\t\trectangle.setLocation(pointBaseCentre.x, pointBaseCentre.y);\n\t\t}\n\t\treturn rectangle;\n\t}\n\n\t/**\n\t * 计算旋转后的图片尺寸\n\t *\n\t * @param width  宽度\n\t * @param height 高度\n\t * @param degree 旋转角度\n\t * @return 计算后目标尺寸\n\t * @since 4.1.20\n\t */\n\tprivate static Rectangle calcRotatedSize(int width, int height, int degree) {\n\t\tif (degree < 0) {\n\t\t\t// 负数角度转换为正数角度\n\t\t\tdegree += 360;\n\t\t}\n\t\tif (degree >= 90) {\n\t\t\tif (degree / 90 % 2 == 1) {\n\t\t\t\tint temp = height;\n\t\t\t\t//noinspection SuspiciousNameCombination\n\t\t\t\theight = width;\n\t\t\t\twidth = temp;\n\t\t\t}\n\t\t\tdegree = degree % 90;\n\t\t}\n\t\tdouble r = Math.sqrt(height * height + width * width) / 2;\n\t\tdouble len = 2 * Math.sin(Math.toRadians(degree) / 2) * r;\n\t\tdouble angel_alpha = (Math.PI - Math.toRadians(degree)) / 2;\n\t\tdouble angel_dalta_width = Math.atan((double) height / width);\n\t\tdouble angel_dalta_height = Math.atan((double) width / height);\n\t\tint len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));\n\t\tint len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));\n\t\tint des_width = width + len_dalta_width * 2;\n\t\tint des_height = height + len_dalta_height * 2;\n\n\t\treturn new Rectangle(des_width, des_height);\n\t}\n\t// ---------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport javax.imageio.*;\nimport javax.imageio.stream.ImageInputStream;\nimport javax.imageio.stream.ImageOutputStream;\nimport javax.swing.ImageIcon;\nimport java.awt.*;\nimport java.awt.color.ColorSpace;\nimport java.awt.font.FontRenderContext;\nimport java.awt.geom.AffineTransform;\nimport java.awt.geom.Rectangle2D;\nimport java.awt.image.*;\nimport java.io.*;\nimport java.net.URL;\nimport java.util.Iterator;\nimport java.util.Random;\n\n/**\n * 图片处理工具类：<br>\n * 功能：缩放图像、切割图像、旋转、图像类型转换、彩色转黑白、文字水印、图片水印等 <br>\n * 参考：http://blog.csdn.net/zhangzhikaixinya/article/details/8459400\n *\n * @author Looly\n */\npublic class ImgUtil {\n\n\t// region ----- [const] image type\n\t/**\n\t * 图形交换格式：GIF\n\t */\n\tpublic static final String IMAGE_TYPE_GIF = \"gif\";\n\t/**\n\t * 联合照片专家组：JPG\n\t */\n\tpublic static final String IMAGE_TYPE_JPG = \"jpg\";\n\t/**\n\t * 联合照片专家组：JPEG\n\t */\n\tpublic static final String IMAGE_TYPE_JPEG = \"jpeg\";\n\t/**\n\t * 英文Bitmap（位图）的简写，它是Windows操作系统中的标准图像文件格式：BMP\n\t */\n\tpublic static final String IMAGE_TYPE_BMP = \"bmp\";\n\t/**\n\t * 可移植网络图形：PNG\n\t */\n\tpublic static final String IMAGE_TYPE_PNG = \"png\";\n\t/**\n\t * Photoshop的专用格式：PSD\n\t */\n\tpublic static final String IMAGE_TYPE_PSD = \"psd\";\n\t// endregion\n\n\t// ---------------------------------------------------------------------------------------------------------------------- scale\n\n\t/**\n\t * 缩放图像（按比例缩放），目标文件的扩展名决定目标文件类型\n\t *\n\t * @param srcImageFile  源图像文件\n\t * @param destImageFile 缩放后的图像文件，扩展名决定目标类型\n\t * @param scale         缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t */\n\tpublic static void scale(File srcImageFile, File destImageFile, float scale) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tscale(image, destImageFile, scale);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像来源流\n\t * @param destStream 缩放后的图像写出到的流\n\t * @param scale      缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @since 3.0.9\n\t */\n\tpublic static void scale(InputStream srcStream, OutputStream destStream, float scale) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tscale(image, destStream, scale);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像来源流\n\t * @param destStream 缩放后的图像写出到的流\n\t * @param scale      缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @since 3.1.0\n\t */\n\tpublic static void scale(ImageInputStream srcStream, ImageOutputStream destStream, float scale) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tscale(image, destStream, scale);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcImg   源图像来源流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destFile 缩放后的图像写出到的流\n\t * @param scale    缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void scale(Image srcImg, File destFile, float scale) throws IORuntimeException {\n\t\tImg.from(srcImg).setTargetImageType(FileUtil.extName(destFile)).scale(scale).write(destFile);\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcImg 源图像来源流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out    缩放后的图像写出到的流\n\t * @param scale  缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void scale(Image srcImg, OutputStream out, float scale) throws IORuntimeException {\n\t\tscale(srcImg, getImageOutputStream(out), scale);\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcImg          源图像来源流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 缩放后的图像写出到的流\n\t * @param scale           缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.0\n\t */\n\tpublic static void scale(Image srcImg, ImageOutputStream destImageStream, float scale) throws IORuntimeException {\n\t\twriteJpg(scale(srcImg, scale), destImageStream);\n\t}\n\n\t/**\n\t * 缩放图像（按比例缩放）\n\t *\n\t * @param srcImg 源图像来源流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param scale  缩放比例。比例大于1时为放大，小于1大于0为缩小\n\t * @return {@link Image}\n\t * @since 3.1.0\n\t */\n\tpublic static Image scale(Image srcImg, float scale) {\n\t\treturn Img.from(srcImg).scale(scale).getImg();\n\t}\n\n\t/**\n\t * 缩放图像（按长宽缩放）<br>\n\t * 注意：目标长宽与原图不成比例会变形\n\t *\n\t * @param srcImg 源图像来源流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param width  目标宽度\n\t * @param height 目标高度\n\t * @return {@link Image}\n\t * @since 3.1.0\n\t */\n\tpublic static Image scale(Image srcImg, int width, int height) {\n\t\treturn Img.from(srcImg).scale(width, height).getImg();\n\t}\n\n\t/**\n\t * 缩放图像（按高度和宽度缩放）<br>\n\t * 缩放后默认格式与源图片相同，无法识别原图片默认JPG\n\t *\n\t * @param srcImageFile  源图像文件地址\n\t * @param destImageFile 缩放后的图像地址\n\t * @param width         缩放后的宽度\n\t * @param height        缩放后的高度\n\t * @param fixedColor    补充的颜色，不补充为{@code null}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void scale(File srcImageFile, File destImageFile, int width, int height, Color fixedColor) throws IORuntimeException {\n\t\tImg img = null;\n\t\ttry {\n\t\t\timg = Img.from(srcImageFile);\n\t\t\timg.setTargetImageType(FileUtil.extName(destImageFile))\n\t\t\t\t.scale(width, height, fixedColor)//\n\t\t\t\t.write(destImageFile);\n\t\t} finally {\n\t\t\tIoUtil.flush(img);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按高度和宽度缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 缩放后的图像目标流\n\t * @param width      缩放后的宽度\n\t * @param height     缩放后的高度\n\t * @param fixedColor 比例不对时补充的颜色，不补充为{@code null}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void scale(InputStream srcStream, OutputStream destStream, int width, int height, Color fixedColor) throws IORuntimeException {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tscale(image, getImageOutputStream(destStream), width, height, fixedColor);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按高度和宽度缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 缩放后的图像目标流\n\t * @param width      缩放后的宽度\n\t * @param height     缩放后的高度\n\t * @param fixedColor 比例不对时补充的颜色，不补充为{@code null}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void scale(ImageInputStream srcStream, ImageOutputStream destStream, int width, int height, Color fixedColor) throws IORuntimeException {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tscale(image, destStream, width, height, fixedColor);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 缩放图像（按高度和宽度缩放）<br>\n\t * 缩放后默认为jpeg格式，此方法并不关闭流\n\t *\n\t * @param srcImage        源图像\n\t * @param destImageStream 缩放后的图像目标流\n\t * @param width           缩放后的宽度\n\t * @param height          缩放后的高度\n\t * @param fixedColor      比例不对时补充的颜色，不补充为{@code null}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void scale(Image srcImage, ImageOutputStream destImageStream, int width, int height, Color fixedColor) throws IORuntimeException {\n\t\twriteJpg(scale(srcImage, width, height, fixedColor), destImageStream);\n\t}\n\n\t/**\n\t * 缩放图像（按高度和宽度缩放）<br>\n\t * 缩放后默认为jpeg格式\n\t *\n\t * @param srcImage   源图像\n\t * @param width      缩放后的宽度\n\t * @param height     缩放后的高度\n\t * @param fixedColor 比例不对时补充的颜色，不补充为{@code null}\n\t * @return {@link Image}\n\t */\n\tpublic static Image scale(Image srcImage, int width, int height, Color fixedColor) {\n\t\treturn Img.from(srcImage).scale(width, height, fixedColor).getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- cut\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)\n\t *\n\t * @param srcImgFile  源图像文件\n\t * @param destImgFile 切片后的图像文件\n\t * @param rectangle   矩形对象，表示矩形区域的x，y，width，height\n\t * @since 3.1.0\n\t */\n\tpublic static void cut(File srcImgFile, File destImgFile, Rectangle rectangle) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImgFile);\n\t\t\tcut(image, destImgFile, rectangle);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 切片后的图像输出流\n\t * @param rectangle  矩形对象，表示矩形区域的x，y，width，height\n\t * @since 3.1.0\n\t */\n\tpublic static void cut(InputStream srcStream, OutputStream destStream, Rectangle rectangle) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tcut(image, destStream, rectangle);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 切片后的图像输出流\n\t * @param rectangle  矩形对象，表示矩形区域的x，y，width，height\n\t * @since 3.1.0\n\t */\n\tpublic static void cut(ImageInputStream srcStream, ImageOutputStream destStream, Rectangle rectangle) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tcut(image, destStream, rectangle);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，此方法并不关闭流\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destFile  输出的文件\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void cut(Image srcImage, File destFile, Rectangle rectangle) throws IORuntimeException {\n\t\twrite(cut(srcImage, rectangle), destFile);\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，此方法并不关闭流\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out       切片后的图像输出流\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.0\n\t */\n\tpublic static void cut(Image srcImage, OutputStream out, Rectangle rectangle) throws IORuntimeException {\n\t\tcut(srcImage, getImageOutputStream(out), rectangle);\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，此方法并不关闭流\n\t *\n\t * @param srcImage        源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 切片后的图像输出流\n\t * @param rectangle       矩形对象，表示矩形区域的x，y，width，height\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.0\n\t */\n\tpublic static void cut(Image srcImage, ImageOutputStream destImageStream, Rectangle rectangle) throws IORuntimeException {\n\t\twriteJpg(cut(srcImage, rectangle), destImageStream);\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height\n\t * @return {@link BufferedImage}\n\t * @since 3.1.0\n\t */\n\tpublic static Image cut(Image srcImage, Rectangle rectangle) {\n\t\treturn Img.from(srcImage).setPositionBaseCentre(false).cut(rectangle).getImg();\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)，填充满整个图片（直径取长宽最小值）\n\t *\n\t * @param srcImage 源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param x        原图的x坐标起始位置\n\t * @param y        原图的y坐标起始位置\n\t * @return {@link Image}\n\t * @since 4.1.15\n\t */\n\tpublic static Image cut(Image srcImage, int x, int y) {\n\t\treturn cut(srcImage, x, y, -1);\n\t}\n\n\t/**\n\t * 图像切割(按指定起点坐标和宽高切割)\n\t *\n\t * @param srcImage 源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param x        原图的x坐标起始位置\n\t * @param y        原图的y坐标起始位置\n\t * @param radius   半径，小于0表示填充满整个图片（直径取长宽最小值）\n\t * @return {@link Image}\n\t * @since 4.1.15\n\t */\n\tpublic static Image cut(Image srcImage, int x, int y, int radius) {\n\t\treturn Img.from(srcImage).cut(x, y, radius).getImg();\n\t}\n\n\t/**\n\t * 图像切片（指定切片的宽度和高度）\n\t *\n\t * @param srcImageFile 源图像\n\t * @param descDir      切片目标文件夹\n\t * @param destWidth    目标切片宽度。默认200\n\t * @param destHeight   目标切片高度。默认150\n\t */\n\tpublic static void slice(File srcImageFile, File descDir, int destWidth, int destHeight) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tslice(image, descDir, destWidth, destHeight);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像切片（指定切片的宽度和高度）\n\t *\n\t * @param srcImage   源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param descDir    切片目标文件夹\n\t * @param destWidth  目标切片宽度。默认200\n\t * @param destHeight 目标切片高度。默认150\n\t */\n\tpublic static void slice(Image srcImage, File descDir, int destWidth, int destHeight) {\n\t\tif (destWidth <= 0) {\n\t\t\tdestWidth = 200; // 切片宽度\n\t\t}\n\t\tif (destHeight <= 0) {\n\t\t\tdestHeight = 150; // 切片高度\n\t\t}\n\t\tint srcWidth = srcImage.getWidth(null); // 源图宽度\n\t\tint srcHeight = srcImage.getHeight(null); // 源图高度\n\n\t\tif (srcWidth < destWidth) {\n\t\t\tdestWidth = srcWidth;\n\t\t}\n\t\tif (srcHeight < destHeight) {\n\t\t\tdestHeight = srcHeight;\n\t\t}\n\n\t\tint cols; // 切片横向数量\n\t\tint rows; // 切片纵向数量\n\t\t// 计算切片的横向和纵向数量\n\t\tif (srcWidth % destWidth == 0) {\n\t\t\tcols = srcWidth / destWidth;\n\t\t} else {\n\t\t\tcols = (int) Math.floor((double) srcWidth / destWidth) + 1;\n\t\t}\n\t\tif (srcHeight % destHeight == 0) {\n\t\t\trows = srcHeight / destHeight;\n\t\t} else {\n\t\t\trows = (int) Math.floor((double) srcHeight / destHeight) + 1;\n\t\t}\n\t\t// 循环建立切片\n\t\tImage tag;\n\t\tfor (int i = 0; i < rows; i++) {\n\t\t\tfor (int j = 0; j < cols; j++) {\n\t\t\t\t// 四个参数分别为图像起点坐标和宽高\n\t\t\t\t// 即: CropImageFilter(int x,int y,int width,int height)\n\t\t\t\ttag = cut(srcImage, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight));\n\t\t\t\t// 输出为文件\n\t\t\t\twrite(tag, FileUtil.file(descDir, \"_r\" + i + \"_c\" + j + \".jpg\"));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 图像切割（指定切片的行数和列数）\n\t *\n\t * @param srcImageFile 源图像文件\n\t * @param destDir      切片目标文件夹\n\t * @param rows         目标切片行数。默认2，必须是范围 [1, 20] 之内\n\t * @param cols         目标切片列数。默认2，必须是范围 [1, 20] 之内\n\t */\n\tpublic static void sliceByRowsAndCols(File srcImageFile, File destDir, int rows, int cols) {\n\t\tsliceByRowsAndCols(srcImageFile, destDir, IMAGE_TYPE_JPEG, rows, cols);\n\t}\n\n\t/**\n\t * 图像切割（指定切片的行数和列数）\n\t *\n\t * @param srcImageFile 源图像文件\n\t * @param destDir      切片目标文件夹\n\t * @param format       目标文件格式\n\t * @param rows         目标切片行数。默认2，必须是范围 [1, 20] 之内\n\t * @param cols         目标切片列数。默认2，必须是范围 [1, 20] 之内\n\t */\n\tpublic static void sliceByRowsAndCols(File srcImageFile, File destDir, String format, int rows, int cols) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tsliceByRowsAndCols(image, destDir, format, rows, cols);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像切割（指定切片的行数和列数），默认RGB模式\n\t *\n\t * @param srcImage 源图像，如果非{@link BufferedImage}，则默认使用RGB模式\n\t * @param destDir  切片目标文件夹\n\t * @param rows     目标切片行数。默认2，必须是范围 [1, 20] 之内\n\t * @param cols     目标切片列数。默认2，必须是范围 [1, 20] 之内\n\t */\n\tpublic static void sliceByRowsAndCols(Image srcImage, File destDir, int rows, int cols) {\n\t\tsliceByRowsAndCols(srcImage, destDir, IMAGE_TYPE_JPEG, rows, cols);\n\t}\n\n\t/**\n\t * 图像切割（指定切片的行数和列数），默认RGB模式\n\t *\n\t * @param srcImage 源图像，如果非{@link BufferedImage}，则默认使用RGB模式\n\t * @param destDir  切片目标文件夹\n\t * @param format   目标文件格式\n\t * @param rows     目标切片行数。默认2，必须是范围 [1, 20] 之内\n\t * @param cols     目标切片列数。默认2，必须是范围 [1, 20] 之内\n\t * @since 5.8.6\n\t */\n\tpublic static void sliceByRowsAndCols(Image srcImage, File destDir, String format, int rows, int cols) {\n\t\tif (false == destDir.exists()) {\n\t\t\tFileUtil.mkdir(destDir);\n\t\t} else if (false == destDir.isDirectory()) {\n\t\t\tthrow new IllegalArgumentException(\"Destination Dir must be a Directory !\");\n\t\t}\n\n\t\tif (rows <= 0 || rows > 20) {\n\t\t\trows = 2; // 切片行数\n\t\t}\n\t\tif (cols <= 0 || cols > 20) {\n\t\t\tcols = 2; // 切片列数\n\t\t}\n\t\t// 读取源图像\n\t\tint srcWidth = srcImage.getWidth(null); // 源图宽度\n\t\tint srcHeight = srcImage.getHeight(null); // 源图高度\n\n\t\tint destWidth = NumberUtil.partValue(srcWidth, cols); // 每张切片的宽度\n\t\tint destHeight = NumberUtil.partValue(srcHeight, rows); // 每张切片的高度\n\n\t\t// 循环建立切片\n\t\tImage tag;\n\t\tfor (int i = 0; i < rows; i++) {\n\t\t\tfor (int j = 0; j < cols; j++) {\n\t\t\t\ttag = cut(srcImage, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight));\n\t\t\t\t// 输出为文件\n\t\t\t\twrite(tag, new File(destDir, \"_r\" + i + \"_c\" + j + \".\" + format));\n\t\t\t}\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- convert\n\n\t/**\n\t * 图像类型转换：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG\n\t *\n\t * @param srcImageFile  源图像文件\n\t * @param destImageFile 目标图像文件\n\t */\n\tpublic static void convert(File srcImageFile, File destImageFile) {\n\t\tAssert.notNull(srcImageFile);\n\t\tAssert.notNull(destImageFile);\n\t\tAssert.isFalse(srcImageFile.equals(destImageFile), \"Src file is equals to dest file!\");\n\n\t\tfinal String srcExtName = FileUtil.extName(srcImageFile);\n\t\tfinal String destExtName = FileUtil.extName(destImageFile);\n\t\tif (StrUtil.equalsIgnoreCase(srcExtName, destExtName)) {\n\t\t\t// 扩展名相同直接复制文件\n\t\t\tFileUtil.copy(srcImageFile, destImageFile, true);\n\t\t}\n\n\t\tImg img = null;\n\t\ttry {\n\t\t\timg = Img.from(srcImageFile);\n\t\t\timg.write(destImageFile);\n\t\t} finally {\n\t\t\tIoUtil.flush(img);\n\t\t}\n\t}\n\n\t/**\n\t * 图像类型转换：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param formatName 包含格式非正式名称的 String：如JPG、JPEG、GIF等\n\t * @param destStream 目标图像输出流\n\t * @since 3.0.9\n\t */\n\tpublic static void convert(InputStream srcStream, String formatName, OutputStream destStream) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\twrite(image, formatName, getImageOutputStream(destStream));\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 图像类型转换：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage        源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param formatName      包含格式非正式名称的 String：如JPG、JPEG、GIF等\n\t * @param destImageStream 目标图像输出流\n\t * @since 4.1.14\n\t */\n\tpublic static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream) {\n\t\tImg.from(srcImage).setTargetImageType(formatName).write(destImageStream);\n\t}\n\n\t/**\n\t * 图像类型转换：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage        源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param formatName      包含格式非正式名称的 String：如JPG、JPEG、GIF等\n\t * @param destImageStream 目标图像输出流\n\t * @param isSrcPng        源图片是否为PNG格式（参数无效）\n\t * @since 4.1.14\n\t */\n\t@Deprecated\n\tpublic static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream, boolean isSrcPng) {\n\t\tconvert(srcImage, formatName, destImageStream);\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- grey\n\n\t/**\n\t * 彩色转为黑白\n\t *\n\t * @param srcImageFile  源图像地址\n\t * @param destImageFile 目标图像地址\n\t */\n\tpublic static void gray(File srcImageFile, File destImageFile) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tgray(image, destImageFile);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @since 3.0.9\n\t */\n\tpublic static void gray(InputStream srcStream, OutputStream destStream) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tgray(image, destStream);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @since 3.0.9\n\t */\n\tpublic static void gray(ImageInputStream srcStream, ImageOutputStream destStream) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tgray(image, destStream);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param outFile  目标文件\n\t * @since 3.2.2\n\t */\n\tpublic static void gray(Image srcImage, File outFile) {\n\t\twrite(gray(srcImage), outFile);\n\t}\n\n\t/**\n\t * 彩色转为黑白<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out      目标图像流\n\t * @since 3.2.2\n\t */\n\tpublic static void gray(Image srcImage, OutputStream out) {\n\t\tgray(srcImage, getImageOutputStream(out));\n\t}\n\n\t/**\n\t * 彩色转为黑白<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage        源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 目标图像流\n\t * @throws IORuntimeException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic static void gray(Image srcImage, ImageOutputStream destImageStream) throws IORuntimeException {\n\t\twriteJpg(gray(srcImage), destImageStream);\n\t}\n\n\t/**\n\t * 彩色转为黑白\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @return {@link Image}灰度后的图片\n\t * @since 3.1.0\n\t */\n\tpublic static Image gray(Image srcImage) {\n\t\treturn Img.from(srcImage).gray().getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- binary\n\n\t/**\n\t * 彩色转为黑白二值化图片，根据目标文件扩展名确定转换后的格式\n\t *\n\t * @param srcImageFile  源图像地址\n\t * @param destImageFile 目标图像地址\n\t */\n\tpublic static void binary(File srcImageFile, File destImageFile) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tbinary(image, destImageFile);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param imageType  图片格式(扩展名)\n\t * @since 4.0.5\n\t */\n\tpublic static void binary(InputStream srcStream, OutputStream destStream, String imageType) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tbinary(image, getImageOutputStream(destStream), imageType);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白黑白二值化图片<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param imageType  图片格式(扩展名)\n\t * @since 4.0.5\n\t */\n\tpublic static void binary(ImageInputStream srcStream, ImageOutputStream destStream, String imageType) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tbinary(image, destStream, imageType);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片，根据目标文件扩展名确定转换后的格式\n\t *\n\t * @param srcImage 源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param outFile  目标文件\n\t * @since 4.0.5\n\t */\n\tpublic static void binary(Image srcImage, File outFile) {\n\t\twrite(binary(srcImage), outFile);\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片<br>\n\t * 此方法并不关闭流，输出JPG格式\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out       目标图像流\n\t * @param imageType 图片格式(扩展名)\n\t * @since 4.0.5\n\t */\n\tpublic static void binary(Image srcImage, OutputStream out, String imageType) {\n\t\tbinary(srcImage, getImageOutputStream(out), imageType);\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片<br>\n\t * 此方法并不关闭流，输出JPG格式\n\t *\n\t * @param srcImage        源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 目标图像流\n\t * @param imageType       图片格式(扩展名)\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.5\n\t */\n\tpublic static void binary(Image srcImage, ImageOutputStream destImageStream, String imageType) throws IORuntimeException {\n\t\twrite(binary(srcImage), imageType, destImageStream);\n\t}\n\n\t/**\n\t * 彩色转为黑白二值化图片\n\t *\n\t * @param srcImage 源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @return {@link Image}二值化后的图片\n\t * @since 4.0.5\n\t */\n\tpublic static Image binary(Image srcImage) {\n\t\treturn Img.from(srcImage).binary().getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- press\n\n\t/**\n\t * 给图片添加文字水印\n\t *\n\t * @param imageFile 源图像文件\n\t * @param destFile  目标图像文件\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t */\n\tpublic static void pressText(File imageFile, File destFile, String pressText, Color color, Font font, int x, int y, float alpha) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(imageFile);\n\t\t\tpressText(image, destFile, pressText, color, font, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param pressText  水印文字\n\t * @param color      水印的字体颜色\n\t * @param font       {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha      透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t */\n\tpublic static void pressText(InputStream srcStream, OutputStream destStream, String pressText, Color color, Font font, int x, int y, float alpha) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tpressText(image, getImageOutputStream(destStream), pressText, color, font, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param pressText  水印文字\n\t * @param color      水印的字体颜色\n\t * @param font       {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha      透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t */\n\tpublic static void pressText(ImageInputStream srcStream, ImageOutputStream destStream, String pressText, Color color, Font font, int x, int y, float alpha) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tpressText(image, destStream, pressText, color, font, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destFile  目标流\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void pressText(Image srcImage, File destFile, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException {\n\t\twrite(pressText(srcImage, pressText, color, font, x, y, alpha), destFile);\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param to        目标流\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void pressText(Image srcImage, OutputStream to, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException {\n\t\tpressText(srcImage, getImageOutputStream(to), pressText, color, font, x, y, alpha);\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage        源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 目标图像流\n\t * @param pressText       水印文字\n\t * @param color           水印的字体颜色\n\t * @param font            {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x               修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y               修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha           透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void pressText(Image srcImage, ImageOutputStream destImageStream, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException {\n\t\twriteJpg(pressText(srcImage, pressText, color, font, x, y, alpha), destImageStream);\n\t}\n\n\t/**\n\t * 给图片添加文字水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage  源图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param pressText 水印文字\n\t * @param color     水印的字体颜色\n\t * @param font      {@link Font} 字体相关信息，如果默认则为{@code null}\n\t * @param x         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y         修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 处理后的图像\n\t * @since 3.2.2\n\t */\n\tpublic static Image pressText(Image srcImage, String pressText, Color color, Font font, int x, int y, float alpha) {\n\t\treturn Img.from(srcImage).pressText(pressText, color, font, x, y, alpha).getImg();\n\t}\n\n\t/**\n\t * 给图片添加图片水印\n\t *\n\t * @param srcImageFile  源图像文件\n\t * @param destImageFile 目标图像文件\n\t * @param pressImg      水印图片\n\t * @param x             修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y             修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha         透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t */\n\tpublic static void pressImage(File srcImageFile, File destImageFile, Image pressImg, int x, int y, float alpha) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcImageFile);\n\t\t\tpressImage(image, destImageFile, pressImg, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param pressImg   水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha      透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t */\n\tpublic static void pressImage(InputStream srcStream, OutputStream destStream, Image pressImg, int x, int y, float alpha) {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tpressImage(image, getImageOutputStream(destStream), pressImg, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param destStream 目标图像流\n\t * @param pressImg   水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y          修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha      透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void pressImage(ImageInputStream srcStream, ImageOutputStream destStream, Image pressImg, int x, int y, float alpha) throws IORuntimeException {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(srcStream);\n\t\t\tpressImage(image, destStream, pressImg, x, y, alpha);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param outFile  写出文件\n\t * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha    透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void pressImage(Image srcImage, File outFile, Image pressImg, int x, int y, float alpha) throws IORuntimeException {\n\t\twrite(pressImage(srcImage, pressImg, x, y, alpha), outFile);\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out      目标图像流\n\t * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha    透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void pressImage(Image srcImage, OutputStream out, Image pressImg, int x, int y, float alpha) throws IORuntimeException {\n\t\tpressImage(srcImage, getImageOutputStream(out), pressImg, x, y, alpha);\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage        源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param destImageStream 目标图像流\n\t * @param pressImg        水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x               修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y               修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha           透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void pressImage(Image srcImage, ImageOutputStream destImageStream, Image pressImg, int x, int y, float alpha) throws IORuntimeException {\n\t\twriteJpg(pressImage(srcImage, pressImg, x, y, alpha), destImageStream);\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage 源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param x        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param y        修正值。 默认在中间，偏移量相对于中间偏移\n\t * @param alpha    透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 结果图片\n\t */\n\tpublic static Image pressImage(Image srcImage, Image pressImg, int x, int y, float alpha) {\n\t\treturn Img.from(srcImage).pressImage(pressImg, x, y, alpha).getImg();\n\t}\n\n\t/**\n\t * 给图片添加图片水印<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcImage  源图像流，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param pressImg  水印图片，可以使用{@link ImageIO#read(File)}方法读取文件\n\t * @param rectangle 矩形对象，表示矩形区域的x，y，width，height，x,y从背景图片中心计算\n\t * @param alpha     透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字\n\t * @return 结果图片\n\t * @since 4.1.14\n\t */\n\tpublic static Image pressImage(Image srcImage, Image pressImg, Rectangle rectangle, float alpha) {\n\t\treturn Img.from(srcImage).pressImage(pressImg, rectangle, alpha).getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- rotate\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 此方法不会关闭输出流\n\t *\n\t * @param imageFile 被旋转图像文件\n\t * @param degree    旋转角度\n\t * @param outFile   输出文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void rotate(File imageFile, int degree, File outFile) throws IORuntimeException {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(imageFile);\n\t\t\trotate(image, degree, outFile);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 此方法不会关闭输出流\n\t *\n\t * @param image   目标图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param degree  旋转角度\n\t * @param outFile 输出文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void rotate(Image image, int degree, File outFile) throws IORuntimeException {\n\t\twrite(rotate(image, degree), outFile);\n\t}\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 此方法不会关闭输出流\n\t *\n\t * @param image  目标图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param degree 旋转角度\n\t * @param out    输出流\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void rotate(Image image, int degree, OutputStream out) throws IORuntimeException {\n\t\twriteJpg(rotate(image, degree), getImageOutputStream(out));\n\t}\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 此方法不会关闭输出流，输出格式为JPG\n\t *\n\t * @param image  图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param degree 旋转角度\n\t * @param out    输出图像流\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void rotate(Image image, int degree, ImageOutputStream out) throws IORuntimeException {\n\t\twriteJpg(rotate(image, degree), out);\n\t}\n\n\t/**\n\t * 旋转图片为指定角度<br>\n\t * 来自：<a href=\"http://blog.51cto.com/cping1982/130066\">http://blog.51cto.com/cping1982/130066</a>\n\t *\n\t * @param image  图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param degree 旋转角度\n\t * @return 旋转后的图片\n\t * @since 3.2.2\n\t */\n\tpublic static Image rotate(Image image, int degree) {\n\t\treturn Img.from(image).rotate(degree).getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- flip\n\n\t/**\n\t * 水平翻转图像\n\t *\n\t * @param imageFile 图像文件\n\t * @param outFile   输出文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void flip(File imageFile, File outFile) throws IORuntimeException {\n\t\tBufferedImage image = null;\n\t\ttry {\n\t\t\timage = read(imageFile);\n\t\t\tflip(image, outFile);\n\t\t} finally {\n\t\t\tflush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 水平翻转图像\n\t *\n\t * @param image   图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param outFile 输出文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void flip(Image image, File outFile) throws IORuntimeException {\n\t\twrite(flip(image), outFile);\n\t}\n\n\t/**\n\t * 水平翻转图像\n\t *\n\t * @param image 图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out   输出\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void flip(Image image, OutputStream out) throws IORuntimeException {\n\t\tflip(image, getImageOutputStream(out));\n\t}\n\n\t/**\n\t * 水平翻转图像，写出格式为JPG\n\t *\n\t * @param image 图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @param out   输出\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static void flip(Image image, ImageOutputStream out) throws IORuntimeException {\n\t\twriteJpg(flip(image), out);\n\t}\n\n\t/**\n\t * 水平翻转图像\n\t *\n\t * @param image 图像，使用结束后需手动调用{@link #flush(Image)}释放资源\n\t * @return 翻转后的图片\n\t * @since 3.2.2\n\t */\n\tpublic static Image flip(Image image) {\n\t\treturn Img.from(image).flip().getImg();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- compress\n\n\t/**\n\t * 压缩图像，输出图像只支持jpg文件\n\t *\n\t * @param imageFile 图像文件\n\t * @param outFile   输出文件，只支持jpg文件\n\t * @param quality   压缩比例，必须为0~1\n\t * @throws IORuntimeException IO异常\n\t * @since 4.3.2\n\t */\n\tpublic static void compress(File imageFile, File outFile, float quality) throws IORuntimeException {\n\t\tImg img = null;\n\t\ttry {\n\t\t\timg = Img.from(imageFile);\n\t\t\timg.setQuality(quality).write(outFile);\n\t\t} finally {\n\t\t\tIoUtil.flush(img);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------- other\n\n\t/**\n\t * {@link Image} 转 {@link RenderedImage}<br>\n\t * 首先尝试强转，否则新建一个{@link BufferedImage}后重新绘制，使用 {@link BufferedImage#TYPE_INT_RGB} 模式。\n\t *\n\t * @param img {@link Image}\n\t * @return {@link BufferedImage}\n\t * @since 4.3.2\n\t * @deprecated 改用 {@link #castToRenderedImage(Image, String)}\n\t */\n\t@Deprecated\n\tpublic static RenderedImage toRenderedImage(Image img) {\n\t\treturn castToRenderedImage(img, IMAGE_TYPE_JPG);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 首先尝试强转，否则新建一个{@link BufferedImage}后重新绘制，使用 {@link BufferedImage#TYPE_INT_RGB} 模式\n\t *\n\t * @param img {@link Image}\n\t * @return {@link BufferedImage}\n\t * @deprecated 改用 {@link #castToBufferedImage(Image, String)}\n\t */\n\t@Deprecated\n\tpublic static BufferedImage toBufferedImage(Image img) {\n\t\treturn castToBufferedImage(img, IMAGE_TYPE_JPG);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link RenderedImage}<br>\n\t * 首先尝试强转，否则新建一个{@link BufferedImage}后重新绘制，使用 {@link BufferedImage#TYPE_INT_RGB} 模式。\n\t *\n\t * @param img       {@link Image}\n\t * @param imageType 目标图片类型，例如jpg或png等\n\t * @return {@link BufferedImage}\n\t * @since 4.3.2\n\t */\n\tpublic static RenderedImage castToRenderedImage(final Image img, final String imageType) {\n\t\tif (img instanceof RenderedImage) {\n\t\t\treturn (RenderedImage) img;\n\t\t}\n\n\t\treturn toBufferedImage(img, imageType);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 首先尝试强转，否则新建一个{@link BufferedImage}后重新绘制，使用 imageType 模式\n\t *\n\t * @param img       {@link Image}\n\t * @param imageType 目标图片类型，例如jpg或png等\n\t * @return {@link BufferedImage}\n\t */\n\tpublic static BufferedImage castToBufferedImage(final Image img, final String imageType) {\n\t\tif (img instanceof BufferedImage) {\n\t\t\treturn (BufferedImage) img;\n\t\t}\n\n\t\treturn toBufferedImage(img, imageType);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 如果源图片的RGB模式与目标模式一致，则直接转换，否则重新绘制<br>\n\t * 默认的，png图片使用 {@link BufferedImage#TYPE_INT_ARGB}模式，其它使用 {@link BufferedImage#TYPE_INT_RGB} 模式\n\t *\n\t * @param image     {@link Image}\n\t * @param imageType 目标图片类型，例如jpg或png等\n\t * @return {@link BufferedImage}\n\t * @since 4.3.2\n\t */\n\tpublic static BufferedImage toBufferedImage(Image image, String imageType) {\n\t\treturn toBufferedImage(image, imageType, null);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 如果源图片的RGB模式与目标模式一致，则直接转换，否则重新绘制<br>\n\t * 默认的，png图片使用 {@link BufferedImage#TYPE_INT_ARGB}模式，其它使用 {@link BufferedImage#TYPE_INT_RGB} 模式\n\t *\n\t * @param image           {@link Image}\n\t * @param imageType       目标图片类型，例如jpg或png等\n\t * @param backgroundColor 背景色{@link Color}\n\t * @return {@link BufferedImage}\n\t * @since 4.3.2\n\t */\n\tpublic static BufferedImage toBufferedImage(Image image, String imageType, Color backgroundColor) {\n\t\tfinal int type = IMAGE_TYPE_PNG.equalsIgnoreCase(imageType)\n\t\t\t? BufferedImage.TYPE_INT_ARGB\n\t\t\t: BufferedImage.TYPE_INT_RGB;\n\t\treturn toBufferedImage(image, type, backgroundColor);\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 如果源图片的RGB模式与目标模式一致，则直接转换，否则重新绘制\n\t *\n\t * @param image     {@link Image}\n\t * @param imageType 目标图片类型，{@link BufferedImage}中的常量，例如黑白等\n\t * @return {@link BufferedImage}\n\t * @since 5.4.7\n\t */\n\tpublic static BufferedImage toBufferedImage(Image image, int imageType) {\n\t\tBufferedImage bufferedImage;\n\t\tif (image instanceof BufferedImage) {\n\t\t\tbufferedImage = (BufferedImage) image;\n\t\t\tif (imageType != bufferedImage.getType()) {\n\t\t\t\tbufferedImage = copyImage(image, imageType);\n\t\t\t}\n\t\t\treturn bufferedImage;\n\t\t}\n\n\t\tbufferedImage = copyImage(image, imageType);\n\t\treturn bufferedImage;\n\t}\n\n\t/**\n\t * {@link Image} 转 {@link BufferedImage}<br>\n\t * 如果源图片的RGB模式与目标模式一致，则直接转换，否则重新绘制\n\t *\n\t * @param image           {@link Image}\n\t * @param imageType       目标图片类型，{@link BufferedImage}中的常量，例如黑白等\n\t * @param backgroundColor 背景色{@link Color}\n\t * @return {@link BufferedImage}\n\t * @since 5.4.7\n\t */\n\tpublic static BufferedImage toBufferedImage(Image image, int imageType, Color backgroundColor) {\n\t\tBufferedImage bufferedImage;\n\t\tif (image instanceof BufferedImage) {\n\t\t\tbufferedImage = (BufferedImage) image;\n\t\t\tif (imageType != bufferedImage.getType()) {\n\t\t\t\tbufferedImage = copyImage(image, imageType, backgroundColor);\n\t\t\t}\n\t\t\treturn bufferedImage;\n\t\t}\n\n\t\tbufferedImage = copyImage(image, imageType, backgroundColor);\n\t\treturn bufferedImage;\n\t}\n\n\t/**\n\t * 将已有Image复制新的一份出来\n\t *\n\t * @param img       {@link Image}\n\t * @param imageType 目标图片类型，{@link BufferedImage}中的常量，例如黑白等\n\t * @return {@link BufferedImage}\n\t * @see BufferedImage#TYPE_INT_RGB\n\t * @see BufferedImage#TYPE_INT_ARGB\n\t * @see BufferedImage#TYPE_INT_ARGB_PRE\n\t * @see BufferedImage#TYPE_INT_BGR\n\t * @see BufferedImage#TYPE_3BYTE_BGR\n\t * @see BufferedImage#TYPE_4BYTE_ABGR\n\t * @see BufferedImage#TYPE_4BYTE_ABGR_PRE\n\t * @see BufferedImage#TYPE_BYTE_GRAY\n\t * @see BufferedImage#TYPE_USHORT_GRAY\n\t * @see BufferedImage#TYPE_BYTE_BINARY\n\t * @see BufferedImage#TYPE_BYTE_INDEXED\n\t * @see BufferedImage#TYPE_USHORT_565_RGB\n\t * @see BufferedImage#TYPE_USHORT_555_RGB\n\t */\n\tpublic static BufferedImage copyImage(Image img, int imageType) {\n\t\treturn copyImage(img, imageType, null);\n\t}\n\n\t/**\n\t * 将已有Image复制新的一份出来\n\t *\n\t * @param img             {@link Image}\n\t * @param imageType       目标图片类型，{@link BufferedImage}中的常量，例如黑白等\n\t * @param backgroundColor 背景色，{@code null} 表示默认背景色（黑色或者透明）\n\t * @return {@link BufferedImage}\n\t * @see BufferedImage#TYPE_INT_RGB\n\t * @see BufferedImage#TYPE_INT_ARGB\n\t * @see BufferedImage#TYPE_INT_ARGB_PRE\n\t * @see BufferedImage#TYPE_INT_BGR\n\t * @see BufferedImage#TYPE_3BYTE_BGR\n\t * @see BufferedImage#TYPE_4BYTE_ABGR\n\t * @see BufferedImage#TYPE_4BYTE_ABGR_PRE\n\t * @see BufferedImage#TYPE_BYTE_GRAY\n\t * @see BufferedImage#TYPE_USHORT_GRAY\n\t * @see BufferedImage#TYPE_BYTE_BINARY\n\t * @see BufferedImage#TYPE_BYTE_INDEXED\n\t * @see BufferedImage#TYPE_USHORT_565_RGB\n\t * @see BufferedImage#TYPE_USHORT_555_RGB\n\t * @since 4.5.17\n\t */\n\tpublic static BufferedImage copyImage(Image img, int imageType, Color backgroundColor) {\n\t\t// ensures that all the pixels loaded\n\t\t// issue#1821@Github\n\t\timg = new ImageIcon(img).getImage();\n\n\t\tfinal BufferedImage bimage = new BufferedImage(\n\t\t\timg.getWidth(null), img.getHeight(null), imageType);\n\t\tfinal Graphics2D bGr = GraphicsUtil.createGraphics(bimage, backgroundColor);\n\t\ttry {\n\t\t\tbGr.drawImage(img, 0, 0, null);\n\t\t} finally {\n\t\t\tbGr.dispose();\n\t\t}\n\n\t\treturn bimage;\n\t}\n\n\t/**\n\t * 创建与当前设备颜色模式兼容的 {@link BufferedImage}\n\t *\n\t * @param width        宽度\n\t * @param height       高度\n\t * @param transparency 透明模式，见 {@link java.awt.Transparency}\n\t * @return {@link BufferedImage}\n\t * @since 5.7.13\n\t */\n\tpublic static BufferedImage createCompatibleImage(int width, int height, int transparency) throws HeadlessException {\n\t\tGraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();\n\t\tGraphicsDevice gs = ge.getDefaultScreenDevice();\n\t\tGraphicsConfiguration gc = gs.getDefaultConfiguration();\n\t\treturn gc.createCompatibleImage(width, height, transparency);\n\t}\n\n\t/**\n\t * 将Base64编码的图像信息转为 {@link BufferedImage}\n\t *\n\t * @param base64 图像的Base64表示\n\t * @return {@link BufferedImage}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedImage toImage(String base64) throws IORuntimeException {\n\t\treturn toImage(Base64.decode(base64));\n\t}\n\n\t/**\n\t * 将的图像bytes转为 {@link BufferedImage}\n\t *\n\t * @param imageBytes 图像bytes\n\t * @return {@link BufferedImage}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedImage toImage(byte[] imageBytes) throws IORuntimeException {\n\t\treturn read(new ByteArrayInputStream(imageBytes));\n\t}\n\n\t/**\n\t * 将图片对象转换为InputStream形式\n\t *\n\t * @param image     图片对象\n\t * @param imageType 图片类型\n\t * @return Base64的字符串表现形式\n\t * @since 4.2.4\n\t */\n\tpublic static ByteArrayInputStream toStream(Image image, String imageType) {\n\t\treturn IoUtil.toStream(toBytes(image, imageType));\n\t}\n\n\t/**\n\t * 将图片对象转换为Base64的Data URI形式，格式为：data:image/[imageType];base64,[data]\n\t *\n\t * @param image     图片对象\n\t * @param imageType 图片类型\n\t * @return Base64的字符串表现形式\n\t * @since 5.3.6\n\t */\n\tpublic static String toBase64DataUri(Image image, String imageType) {\n\t\treturn URLUtil.getDataUri(\n\t\t\t\"image/\" + imageType, \"base64\",\n\t\t\ttoBase64(image, imageType));\n\t}\n\n\t/**\n\t * 将图片对象转换为Base64形式\n\t *\n\t * @param image     图片对象\n\t * @param imageType 图片类型\n\t * @return Base64的字符串表现形式\n\t * @since 4.1.8\n\t */\n\tpublic static String toBase64(Image image, String imageType) {\n\t\treturn Base64.encode(toBytes(image, imageType));\n\t}\n\n\t/**\n\t * 将图片对象转换为bytes形式\n\t *\n\t * @param image     图片对象\n\t * @param imageType 图片类型\n\t * @return Base64的字符串表现形式\n\t * @since 5.2.4\n\t */\n\tpublic static byte[] toBytes(Image image, String imageType) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\twrite(image, imageType, out);\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 根据文字创建PNG图片\n\t *\n\t * @param str             文字\n\t * @param font            字体{@link Font}\n\t * @param backgroundColor 背景颜色，默认透明\n\t * @param fontColor       字体颜色，默认黑色\n\t * @param out             图片输出地\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void createImage(String str, Font font, Color backgroundColor, Color fontColor, ImageOutputStream out) throws IORuntimeException {\n\t\twritePng(createImage(str, font, backgroundColor, fontColor, BufferedImage.TYPE_INT_ARGB), out);\n\t}\n\n\t/**\n\t * 根据文字创建透明背景的PNG图片\n\t *\n\t * @param str       文字\n\t * @param font      字体{@link Font}\n\t * @param fontColor 字体颜色，默认黑色\n\t * @param out       图片输出地\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void createTransparentImage(String str, Font font, Color fontColor, ImageOutputStream out) throws IORuntimeException {\n\t\twritePng(createImage(str, font, null, fontColor, BufferedImage.TYPE_INT_ARGB), out);\n\t}\n\n\t/**\n\t * 根据文字创建图片\n\t *\n\t * @param str             文字\n\t * @param font            字体{@link Font}\n\t * @param backgroundColor 背景颜色，默认透明\n\t * @param fontColor       字体颜色，默认黑色\n\t * @param imageType       图片类型，见：{@link BufferedImage}\n\t * @return 图片\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedImage createImage(String str, Font font, Color backgroundColor, Color fontColor, int imageType) throws IORuntimeException {\n\t\t// 获取font的样式应用在str上的整个矩形\n\t\tfinal Rectangle2D r = getRectangle(str, font);\n\t\t// 获取单个字符的高度\n\t\tint unitHeight = (int) Math.floor(r.getHeight());\n\t\t// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度\n\t\tint width = (int) Math.round(r.getWidth()) + 1;\n\t\t// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度\n\t\tint height = unitHeight + 3;\n\n\t\t// 创建图片\n\t\tBufferedImage image = new BufferedImage(width, height, imageType);\n\t\tGraphics g = image.getGraphics();\n\t\tif (null != backgroundColor) {\n\t\t\t// 先用背景色填充整张图片,也就是背景\n\t\t\tg.setColor(backgroundColor);\n\t\t\tg.fillRect(0, 0, width, height);\n\t\t}\n\n\t\tg.setColor(ObjectUtil.defaultIfNull(fontColor, Color.BLACK));\n\t\tg.setFont(font);// 设置画笔字体\n\t\tg.drawString(str, 0, font.getSize());// 画出字符串\n\t\tg.dispose();\n\n\t\treturn image;\n\t}\n\n\t/**\n\t * 获取font的样式应用在str上的整个矩形\n\t *\n\t * @param str  字符串，必须非空\n\t * @param font 字体，必须非空\n\t * @return {@link Rectangle2D}\n\t * @since 5.3.3\n\t */\n\tpublic static Rectangle2D getRectangle(String str, Font font) {\n\t\treturn font.getStringBounds(str,\n\t\t\tnew FontRenderContext(AffineTransform.getScaleInstance(1, 1),\n\t\t\t\tfalse,\n\t\t\t\tfalse));\n\t}\n\n\t/**\n\t * 根据文件创建字体<br>\n\t * 首先尝试创建{@link Font#TRUETYPE_FONT}字体，此类字体无效则创建{@link Font#TYPE1_FONT}\n\t *\n\t * @param fontFile 字体文件\n\t * @return {@link Font}\n\t * @since 3.0.9\n\t */\n\tpublic static Font createFont(File fontFile) {\n\t\treturn FontUtil.createFont(fontFile);\n\t}\n\n\t/**\n\t * 根据文件创建字体<br>\n\t * 首先尝试创建{@link Font#TRUETYPE_FONT}字体，此类字体无效则创建{@link Font#TYPE1_FONT}\n\t *\n\t * @param fontStream 字体流\n\t * @return {@link Font}\n\t * @since 3.0.9\n\t */\n\tpublic static Font createFont(InputStream fontStream) {\n\t\treturn FontUtil.createFont(fontStream);\n\t}\n\n\t/**\n\t * 创建{@link Graphics2D}\n\t *\n\t * @param image {@link BufferedImage}\n\t * @param color {@link Color}背景颜色以及当前画笔颜色\n\t * @return {@link Graphics2D}\n\t * @see GraphicsUtil#createGraphics(BufferedImage, Color)\n\t * @since 3.2.3\n\t */\n\tpublic static Graphics2D createGraphics(BufferedImage image, Color color) {\n\t\treturn GraphicsUtil.createGraphics(image, color);\n\t}\n\n\t/**\n\t * 写出图像为JPG格式\n\t *\n\t * @param image           {@link Image}\n\t * @param destImageStream 写出到的目标流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void writeJpg(Image image, ImageOutputStream destImageStream) throws IORuntimeException {\n\t\twrite(image, IMAGE_TYPE_JPG, destImageStream);\n\t}\n\n\t/**\n\t * 写出图像为PNG格式\n\t *\n\t * @param image           {@link Image}\n\t * @param destImageStream 写出到的目标流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void writePng(Image image, ImageOutputStream destImageStream) throws IORuntimeException {\n\t\twrite(image, IMAGE_TYPE_PNG, destImageStream);\n\t}\n\n\t/**\n\t * 写出图像为JPG格式\n\t *\n\t * @param image {@link Image}\n\t * @param out   写出到的目标流\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.10\n\t */\n\tpublic static void writeJpg(Image image, OutputStream out) throws IORuntimeException {\n\t\twrite(image, IMAGE_TYPE_JPG, out);\n\t}\n\n\t/**\n\t * 写出图像为PNG格式\n\t *\n\t * @param image {@link Image}\n\t * @param out   写出到的目标流\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.10\n\t */\n\tpublic static void writePng(Image image, OutputStream out) throws IORuntimeException {\n\t\twrite(image, IMAGE_TYPE_PNG, out);\n\t}\n\n\t/**\n\t * 按照目标格式写出图像：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param srcStream  源图像流\n\t * @param formatName 包含格式非正式名称的 String：如JPG、JPEG、GIF等\n\t * @param destStream 目标图像输出流\n\t * @since 5.0.0\n\t */\n\tpublic static void write(ImageInputStream srcStream, String formatName, ImageOutputStream destStream) {\n\t\twrite(read(srcStream), formatName, destStream);\n\t}\n\n\t/**\n\t * 写出图像：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param image     {@link Image}\n\t * @param imageType 图片类型（图片扩展名）\n\t * @param out       写出到的目标流\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static void write(Image image, String imageType, OutputStream out) throws IORuntimeException {\n\t\twrite(image, imageType, getImageOutputStream(out));\n\t}\n\n\t/**\n\t * 写出图像为指定格式：GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG<br>\n\t * 此方法并不关闭流\n\t *\n\t * @param image           {@link Image}\n\t * @param imageType       图片类型（图片扩展名）\n\t * @param destImageStream 写出到的目标流\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static boolean write(Image image, String imageType, ImageOutputStream destImageStream) throws IORuntimeException {\n\t\treturn write(image, imageType, destImageStream, 1);\n\t}\n\n\t/**\n\t * 写出图像为指定格式\n\t *\n\t * @param image           {@link Image}\n\t * @param imageType       图片类型（图片扩展名）\n\t * @param destImageStream 写出到的目标流\n\t * @param quality         质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t * @since 4.3.2\n\t */\n\tpublic static boolean write(Image image, String imageType, ImageOutputStream destImageStream, float quality) throws IORuntimeException {\n\t\treturn write(image, imageType, destImageStream, quality, null);\n\t}\n\n\t/**\n\t * 写出图像为指定格式\n\t *\n\t * @param image           {@link Image}\n\t * @param imageType       图片类型（图片扩展名）\n\t * @param destImageStream 写出到的目标流\n\t * @param quality         质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t * @param backgroundColor 背景色{@link Color}\n\t * @return 是否成功写出，如果返回false表示未找到合适的Writer\n\t * @throws IORuntimeException IO异常\n\t * @since 4.3.2\n\t */\n\tpublic static boolean write(Image image, String imageType, ImageOutputStream destImageStream, float quality, Color backgroundColor) throws IORuntimeException {\n\t\tif (StrUtil.isBlank(imageType)) {\n\t\t\timageType = IMAGE_TYPE_JPG;\n\t\t}\n\n\t\tfinal BufferedImage bufferedImage = toBufferedImage(image, imageType, backgroundColor);\n\t\tfinal ImageWriter writer = getWriter(bufferedImage, imageType);\n\t\treturn write(bufferedImage, writer, destImageStream, quality);\n\t}\n\n\t/**\n\t * 写出图像为目标文件扩展名对应的格式\n\t *\n\t * @param image      {@link Image}\n\t * @param targetFile 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.0\n\t */\n\tpublic static void write(Image image, File targetFile) throws IORuntimeException {\n\t\tFileUtil.touch(targetFile);\n\t\tImageOutputStream out = null;\n\t\ttry {\n\t\t\tout = getImageOutputStream(targetFile);\n\t\t\twrite(image, FileUtil.extName(targetFile), out);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t}\n\n\t/**\n\t * 通过{@link ImageWriter}写出图片到输出流\n\t *\n\t * @param image   图片\n\t * @param writer  {@link ImageWriter}\n\t * @param output  输出的Image流{@link ImageOutputStream}\n\t * @param quality 质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩\n\t * @return 是否成功写出\n\t * @since 4.3.2\n\t */\n\tpublic static boolean write(Image image, ImageWriter writer, ImageOutputStream output, float quality) {\n\t\tif (writer == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\twriter.setOutput(output);\n\t\tfinal RenderedImage renderedImage = castToRenderedImage(image, IMAGE_TYPE_JPG);\n\t\t// 设置质量\n\t\tImageWriteParam imgWriteParams = null;\n\t\tif (quality > 0 && quality < 1) {\n\t\t\timgWriteParams = writer.getDefaultWriteParam();\n\t\t\tif (imgWriteParams.canWriteCompressed()) {\n\t\t\t\timgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);\n\t\t\t\timgWriteParams.setCompressionQuality(quality);\n\t\t\t\tfinal ColorModel colorModel = renderedImage.getColorModel();// ColorModel.getRGBdefault();\n\t\t\t\timgWriteParams.setDestinationType(new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16)));\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tif (null != imgWriteParams) {\n\t\t\t\twriter.write(null, new IIOImage(renderedImage, null, null), imgWriteParams);\n\t\t\t} else {\n\t\t\t\twriter.write(renderedImage);\n\t\t\t}\n\t\t\toutput.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\twriter.dispose();\n\t\t\t// issue#IAPZG7\n\t\t\t// FileCacheImageOutputStream会产生临时文件，此处关闭清除\n\t\t\tIoUtil.close(output);\n\t\t\t// issue#ID6VNJ\n\t\t\tflush(image);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 获得{@link ImageReader}\n\t *\n\t * @param type 图片文件类型，例如 \"jpeg\" 或 \"tiff\"\n\t * @return {@link ImageReader}\n\t */\n\tpublic static ImageReader getReader(String type) {\n\t\tfinal Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName(type);\n\t\tif (iterator.hasNext()) {\n\t\t\treturn iterator.next();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 从文件中读取图片，请使用绝对路径，使用相对路径会相对于ClassPath\n\t *\n\t * @param imageFilePath 图片文件路径\n\t * @return 图片\n\t * @since 4.1.15\n\t */\n\tpublic static BufferedImage read(String imageFilePath) {\n\t\treturn read(FileUtil.file(imageFilePath));\n\t}\n\n\t/**\n\t * 从文件中读取图片\n\t *\n\t * @param imageFile 图片文件\n\t * @return 图片\n\t * @since 3.2.2\n\t */\n\tpublic static BufferedImage read(File imageFile) {\n\t\tBufferedImage result;\n\t\ttry {\n\t\t\tresult = ImageIO.read(imageFile);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type of file [\" + imageFile.getName() + \"] is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从URL中获取或读取图片对象\n\t *\n\t * @param url URL\n\t * @return {@link Image}\n\t * @since 5.5.8\n\t */\n\tpublic static Image getImage(URL url) {\n\t\treturn Toolkit.getDefaultToolkit().getImage(url);\n\t}\n\n\t/**\n\t * 从{@link Resource}中读取图片\n\t *\n\t * @param resource 图片资源\n\t * @return 图片\n\t * @since 4.4.1\n\t */\n\tpublic static BufferedImage read(Resource resource) {\n\t\treturn read(resource.getStream());\n\t}\n\n\t/**\n\t * 从流中读取图片\n\t *\n\t * @param imageStream 图片文件\n\t * @return 图片\n\t * @since 3.2.2\n\t */\n\tpublic static BufferedImage read(InputStream imageStream) {\n\t\tBufferedImage result;\n\t\ttry {\n\t\t\tresult = ImageIO.read(imageStream);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从图片流中读取图片\n\t *\n\t * @param imageStream 图片文件\n\t * @return 图片\n\t * @since 3.2.2\n\t */\n\tpublic static BufferedImage read(ImageInputStream imageStream) {\n\t\tBufferedImage result;\n\t\ttry {\n\t\t\tresult = ImageIO.read(imageStream);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从URL中读取图片\n\t *\n\t * @param imageUrl 图片文件\n\t * @return 图片\n\t * @since 3.2.2\n\t */\n\tpublic static BufferedImage read(URL imageUrl) {\n\t\tBufferedImage result;\n\t\ttry {\n\t\t\tresult = ImageIO.read(imageUrl);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type of [\" + imageUrl + \"] is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取{@link ImageOutputStream}\n\t *\n\t * @param out {@link OutputStream}\n\t * @return {@link ImageOutputStream}\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static ImageOutputStream getImageOutputStream(OutputStream out) throws IORuntimeException {\n\t\tImageOutputStream result;\n\t\ttry {\n\t\t\tresult = ImageIO.createImageOutputStream(out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取{@link ImageOutputStream}\n\t *\n\t * @param outFile {@link File}\n\t * @return {@link ImageOutputStream}\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static ImageOutputStream getImageOutputStream(File outFile) throws IORuntimeException {\n\t\tImageOutputStream result;\n\t\ttry {\n\t\t\tresult = ImageIO.createImageOutputStream(outFile);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type of file [\" + outFile.getName() + \"] is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取{@link ImageInputStream}\n\t *\n\t * @param in {@link InputStream}\n\t * @return {@link ImageInputStream}\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static ImageInputStream getImageInputStream(InputStream in) throws IORuntimeException {\n\t\tImageOutputStream result;\n\t\ttry {\n\t\t\tresult = ImageIO.createImageOutputStream(in);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null == result) {\n\t\t\tthrow new IllegalArgumentException(\"Image type is not supported!\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 根据给定的Image对象和格式获取对应的{@link ImageWriter}，如果未找到合适的Writer，返回null\n\t *\n\t * @param img        {@link Image}\n\t * @param formatName 图片格式，例如\"jpg\"、\"png\"\n\t * @return {@link ImageWriter}\n\t * @since 4.3.2\n\t */\n\tpublic static ImageWriter getWriter(Image img, String formatName) {\n\t\tfinal ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(toBufferedImage(img, formatName));\n\t\tfinal Iterator<ImageWriter> iter = ImageIO.getImageWriters(type, formatName);\n\t\treturn iter.hasNext() ? iter.next() : null;\n\t}\n\n\t/**\n\t * 根据给定的图片格式或者扩展名获取{@link ImageWriter}，如果未找到合适的Writer，返回null\n\t *\n\t * @param formatName 图片格式或扩展名，例如\"jpg\"、\"png\"\n\t * @return {@link ImageWriter}\n\t * @since 4.3.2\n\t */\n\tpublic static ImageWriter getWriter(String formatName) {\n\t\tImageWriter writer = null;\n\t\tIterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(formatName);\n\t\tif (iter.hasNext()) {\n\t\t\twriter = iter.next();\n\t\t}\n\t\tif (null == writer) {\n\t\t\t// 尝试扩展名获取\n\t\t\titer = ImageIO.getImageWritersBySuffix(formatName);\n\t\t\tif (iter.hasNext()) {\n\t\t\t\twriter = iter.next();\n\t\t\t}\n\t\t}\n\t\treturn writer;\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------------- Color\n\n\t/**\n\t * Color对象转16进制表示，例如#fcf6d6\n\t *\n\t * @param color {@link Color}\n\t * @return 16进制的颜色值，例如#fcf6d6\n\t * @see ColorUtil#toHex(Color)\n\t * @since 4.1.14\n\t */\n\tpublic static String toHex(Color color) {\n\t\treturn ColorUtil.toHex(color);\n\t}\n\n\t/**\n\t * RGB颜色值转换成十六进制颜色码\n\t *\n\t * @param r 红(R)\n\t * @param g 绿(G)\n\t * @param b 蓝(B)\n\t * @return 返回字符串形式的 十六进制颜色码\n\t * @see ColorUtil#toHex(int, int, int)\n\t */\n\tpublic static String toHex(int r, int g, int b) {\n\t\treturn ColorUtil.toHex(r, g, b);\n\t}\n\n\t/**\n\t * 16进制的颜色值转换为Color对象，例如#fcf6d6\n\t *\n\t * @param hex 16进制的颜色值，例如#fcf6d6\n\t * @return {@link Color}\n\t * @since 4.1.14\n\t */\n\tpublic static Color hexToColor(String hex) {\n\t\treturn ColorUtil.hexToColor(hex);\n\t}\n\n\t/**\n\t * 获取一个RGB值对应的颜色\n\t *\n\t * @param rgb RGB值\n\t * @return {@link Color}\n\t * @see ColorUtil#getColor(int)\n\t * @since 4.1.14\n\t */\n\tpublic static Color getColor(int rgb) {\n\t\treturn ColorUtil.getColor(rgb);\n\t}\n\n\t/**\n\t * 将颜色值转换成具体的颜色类型 汇集了常用的颜色集，支持以下几种形式：\n\t *\n\t * <pre>\n\t * 1. 颜色的英文名（大小写皆可）\n\t * 2. 16进制表示，例如：#fcf6d6或者$fcf6d6\n\t * 3. RGB形式，例如：13,148,252\n\t * </pre>\n\t * <p>\n\t * 方法来自：com.lnwazg.kit\n\t *\n\t * @param colorName 颜色的英文名，16进制表示或RGB表示\n\t * @return {@link Color}\n\t * @see ColorUtil#getColor(String)\n\t * @since 4.1.14\n\t */\n\tpublic static Color getColor(String colorName) {\n\t\treturn ColorUtil.getColor(colorName);\n\t}\n\n\t/**\n\t * 生成随机颜色\n\t *\n\t * @return 随机颜色\n\t * @see ColorUtil#randomColor()\n\t * @since 3.1.2\n\t */\n\tpublic static Color randomColor() {\n\t\treturn ColorUtil.randomColor();\n\t}\n\n\t/**\n\t * 生成随机颜色，与指定颜色有一定的区分度\n\t *\n\t * @param compareColor 比较颜色\n\t * @param minDistance 最小色差，按三维坐标计算的距离值\n\t * @return 与指定颜色有一定的区分度的随机颜色\n\t * @since 5.8.30\n\t */\n\tpublic static Color randomColor(Color compareColor,int minDistance) {\n\t\treturn ColorUtil.randomColor(compareColor,minDistance);\n\t}\n\n\t/**\n\t * 生成随机颜色，与指定颜色有一定的区分度\n\t *\n\t * @param compareColor 比较颜色\n\t * @return 与指定颜色有一定的区分度的随机颜色，默认是最大可能的三维距离的一半\n\t * @since 5.8.30\n\t */\n\tpublic static Color randomColor(Color compareColor) {\n\t\treturn ColorUtil.randomColor(compareColor,ColorUtil.maxDistance(compareColor) / 2);\n\t}\n\n\t/**\n\t * 生成随机颜色\n\t *\n\t * @param random 随机对象 {@link Random}\n\t * @return 随机颜色\n\t * @see ColorUtil#randomColor(Random)\n\t * @since 3.1.2\n\t */\n\tpublic static Color randomColor(Random random) {\n\t\treturn ColorUtil.randomColor(random);\n\t}\n\n\t/**\n\t * 获得修正后的矩形坐标位置，变为以背景中心为基准坐标（即x,y == 0,0时，处于背景正中）\n\t *\n\t * @param rectangle        矩形\n\t * @param backgroundWidth  参考宽（背景宽）\n\t * @param backgroundHeight 参考高（背景高）\n\t * @return 修正后的{@link Point}\n\t * @since 5.3.6\n\t */\n\tpublic static Point getPointBaseCentre(Rectangle rectangle, int backgroundWidth, int backgroundHeight) {\n\t\treturn new Point(\n\t\t\trectangle.x + (Math.abs(backgroundWidth - rectangle.width) / 2), //\n\t\t\trectangle.y + (Math.abs(backgroundHeight - rectangle.height) / 2)//\n\t\t);\n\t}\n\n\t/**\n\t * 获取给定图片的主色调，背景填充用\n\t *\n\t * @param image      {@link BufferedImage}\n\t * @param rgbFilters 过滤多种颜色\n\t * @return {@link String} #ffffff\n\t * @since 5.6.7\n\t */\n\tpublic static String getMainColor(BufferedImage image, int[]... rgbFilters) {\n\t\treturn ColorUtil.getMainColor(image, rgbFilters);\n\t}\n\t// ------------------------------------------------------------------------------------------------------ 背景图换算\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param inputPath  要处理图片的路径\n\t * @param outputPath 输出图片的路径\n\t * @param tolerance  容差值[根据图片的主题色,加入容差值,值的范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(String inputPath, String outputPath, int tolerance) {\n\t\treturn BackgroundRemoval.backgroundRemoval(inputPath, outputPath, tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param input     需要进行操作的图片\n\t * @param output    最后输出的文件\n\t * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(File input, File output, int tolerance) {\n\t\treturn BackgroundRemoval.backgroundRemoval(input, output, tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param input     需要进行操作的图片\n\t * @param output    最后输出的文件\n\t * @param override  指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理结果 true:图片处理完成 false:图片处理失败\n\t */\n\tpublic static boolean backgroundRemoval(File input, File output, Color override, int tolerance) {\n\t\treturn BackgroundRemoval.backgroundRemoval(input, output, override, tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param bufferedImage 需要进行处理的图片流\n\t * @param override      指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance     容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理好的图片流\n\t */\n\tpublic static BufferedImage backgroundRemoval(BufferedImage bufferedImage, Color override, int tolerance) {\n\t\treturn BackgroundRemoval.backgroundRemoval(bufferedImage, override, tolerance);\n\t}\n\n\t/**\n\t * 背景移除\n\t * 图片去底工具\n\t * 将 \"纯色背景的图片\" 还原成 \"透明背景的图片\"\n\t * 将纯色背景的图片转成矢量图\n\t * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色\n\t * 再加入一定的容差值,然后将所有像素点与该颜色进行比较\n\t * 发现相同则将颜色不透明度设置为0,使颜色完全透明.\n\t *\n\t * @param outputStream 需要进行处理的图片字节数组流\n\t * @param override     指定替换成的背景颜色 为null时背景为透明\n\t * @param tolerance    容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]\n\t * @return 返回处理好的图片流\n\t */\n\tpublic static BufferedImage backgroundRemoval(ByteArrayOutputStream outputStream, Color override, int tolerance) {\n\t\treturn BackgroundRemoval.backgroundRemoval(outputStream, override, tolerance);\n\t}\n\n\t/**\n\t * 图片颜色转换<br>\n\t * 可以使用灰度 (gray)等\n\t *\n\t * @param colorSpace 颜色模式，如灰度等\n\t * @param image      被转换的图片\n\t * @return 转换后的图片\n\t * @since 5.7.8\n\t */\n\tpublic static BufferedImage colorConvert(ColorSpace colorSpace, BufferedImage image) {\n\t\treturn filter(new ColorConvertOp(colorSpace, null), image);\n\t}\n\n\t/**\n\t * 转换图片<br>\n\t * 可以使用一系列平移 (translation)、缩放 (scale)、翻转 (flip)、旋转 (rotation) 和错切 (shear) 来构造仿射变换。\n\t *\n\t * @param xform 2D仿射变换，它执行从 2D 坐标到其他 2D 坐标的线性映射，保留了线的“直线性”和“平行性”。\n\t * @param image 被转换的图片\n\t * @return 转换后的图片\n\t * @since 5.7.8\n\t */\n\tpublic static BufferedImage transform(AffineTransform xform, BufferedImage image) {\n\t\treturn filter(new AffineTransformOp(xform, null), image);\n\t}\n\n\t/**\n\t * 图片过滤转换\n\t *\n\t * @param op    过滤操作实现，如二维转换可传入{@link AffineTransformOp}\n\t * @param image 原始图片\n\t * @return 过滤后的图片\n\t * @since 5.7.8\n\t */\n\tpublic static BufferedImage filter(BufferedImageOp op, BufferedImage image) {\n\t\treturn op.filter(image, null);\n\t}\n\n\t/**\n\t * 图片滤镜，借助 {@link ImageFilter}实现，实现不同的图片滤镜\n\t *\n\t * @param filter 滤镜实现\n\t * @param image  图片\n\t * @return 滤镜后的图片\n\t * @since 5.7.8\n\t */\n\tpublic static Image filter(ImageFilter filter, Image image) {\n\t\treturn Toolkit.getDefaultToolkit().createImage(\n\t\t\tnew FilteredImageSource(image.getSource(), filter));\n\t}\n\n\t/**\n\t * 刷新和释放{@link Image} 资源\n\t *\n\t * @param image {@link Image}\n\t */\n\tpublic static void flush(Image image) {\n\t\tif (null != image) {\n\t\t\timage.flush();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/LabColor.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.awt.Color;\nimport java.awt.color.ColorSpace;\n\n/**\n * 表示以 LAB 形式存储的颜色。<br>\n * <ul>\n *     <li>L: 亮度</li>\n *     <li>a: 正数代表红色，负端代表绿色</li>\n *     <li>b: 正数代表黄色，负端代表蓝色</li>\n * </ul>\n *\n * @author Tom Xin\n * @since 5.8.7\n */\npublic class LabColor {\n\n\tprivate static final ColorSpace XYZ_COLOR_SPACE = ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);\n\n\t/**\n\t * L: 亮度\n\t */\n\tprivate final double l;\n\t/**\n\t * A: 正数代表红色，负端代表绿色\n\t */\n\tprivate final double a;\n\t/**\n\t * B: 正数代表黄色，负端代表蓝色\n\t */\n\tprivate final double b;\n\n\tpublic LabColor(Integer rgb) {\n\t\tthis((rgb != null) ? new Color(rgb) : null);\n\t}\n\n\tpublic LabColor(Color color) {\n\t\tAssert.notNull(color, \"Color must not be null\");\n\t\tfinal float[] lab = fromXyz(color.getColorComponents(XYZ_COLOR_SPACE, null));\n\t\tthis.l = lab[0];\n\t\tthis.a = lab[1];\n\t\tthis.b = lab[2];\n\t}\n\n\t/**\n\t * 获取颜色差\n\t * @param other 其他Lab颜色\n\t * @return 颜色差\n\t */\n\t// See https://en.wikipedia.org/wiki/Color_difference#CIE94\n\tpublic double getDistance(LabColor other) {\n\t\tdouble c1 = Math.sqrt(this.a * this.a + this.b * this.b);\n\t\tdouble deltaC = c1 - Math.sqrt(other.a * other.a + other.b * other.b);\n\t\tdouble deltaA = this.a - other.a;\n\t\tdouble deltaB = this.b - other.b;\n\t\tdouble deltaH = Math.sqrt(Math.max(0.0, deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));\n\t\treturn Math.sqrt(Math.max(0.0, Math.pow((this.l - other.l), 2)\n\t\t\t\t+ Math.pow(deltaC / (1 + 0.045 * c1), 2) + Math.pow(deltaH / (1 + 0.015 * c1), 2.0)));\n\t}\n\n\tprivate float[] fromXyz(float[] xyz) {\n\t\treturn fromXyz(xyz[0], xyz[1], xyz[2]);\n\t}\n\n\t/**\n\t * 从xyz换算<br>\n\t * L=116f(y)-16<br>\n\t * a=500[f(x/0.982)-f(y)]<br>\n\t * b=200[f(y)-f(z/1.183 )]<br>\n\t * 其中： f(x)=7.787x+0.138, x<0.008856; f(x)=(x)1/3,x>0.008856\n\t *\n\t * @param x X\n\t * @param y Y\n\t * @param z Z\n\t * @return Lab\n\t */\n\tprivate static float[] fromXyz(float x, float y, float z) {\n\t\tfinal double l = (f(y) - 16.0) * 116.0;\n\t\tfinal double a = (f(x) - f(y)) * 500.0;\n\t\tfinal double b = (f(y) - f(z)) * 200.0;\n\t\treturn new float[]{(float) l, (float) a, (float) b};\n\t}\n\n\tprivate static double f(double t) {\n\t\treturn (t > (216.0 / 24389.0)) ? Math.cbrt(t) : (1.0 / 3.0) * Math.pow(29.0 / 6.0, 2) * t + (4.0 / 29.0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/ScaleType.java",
    "content": "package cn.hutool.core.img;\n\nimport java.awt.Image;\n\n/**\n * 图片缩略算法类型\n *\n * @author looly\n * @since 4.5.8\n */\npublic enum ScaleType {\n\n\t/** 默认 */\n\tDEFAULT(Image.SCALE_DEFAULT),\n\t/** 快速 */\n\tFAST(Image.SCALE_FAST),\n\t/** 平滑 */\n\tSMOOTH(Image.SCALE_SMOOTH),\n\t/** 使用 ReplicateScaleFilter 类中包含的图像缩放算法 */\n\tREPLICATE(Image.SCALE_REPLICATE),\n\t/** Area Averaging算法 */\n\tAREA_AVERAGING(Image.SCALE_AREA_AVERAGING);\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 缩放方式\n\t * @see Image#SCALE_DEFAULT\n\t * @see Image#SCALE_FAST\n\t * @see Image#SCALE_SMOOTH\n\t * @see Image#SCALE_REPLICATE\n\t * @see Image#SCALE_AREA_AVERAGING\n\t */\n\tScaleType(int value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final int value;\n\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/gif/AnimatedGifEncoder.java",
    "content": "package cn.hutool.core.img.gif;\n\nimport java.awt.Color;\nimport java.awt.Graphics2D;\nimport java.awt.image.BufferedImage;\nimport java.awt.image.DataBufferByte;\nimport java.io.BufferedOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\n/**\n * 动态GIF动画生成器，可生成一个或多个帧的GIF。\n *\n * <pre>\n * Example:\n *    AnimatedGifEncoder e = new AnimatedGifEncoder();\n *    e.start(outputFileName);\n *    e.setDelay(1000);   // 1 frame per sec\n *    e.addFrame(image1);\n *    e.addFrame(image2);\n *    e.finish();\n * </pre>\n * <p>\n * 来自：https://github.com/rtyley/animated-gif-lib-for-java\n *\n * @author Kevin Weiner, FM Software\n * @version 1.03 November 2003\n * @since 5.3.8\n */\npublic class AnimatedGifEncoder {\n\n\tprotected int width; // image size\n\tprotected int height;\n\tprotected Color transparent = null; // transparent color if given\n\tprotected boolean transparentExactMatch = false; // transparent color will be found by looking for the closest color\n\t// or for the exact color if transparentExactMatch == true\n\tprotected Color background = null;  // background color if given\n\tprotected int transIndex; // transparent index in color table\n\tprotected int repeat = -1; // no repeat\n\tprotected int delay = 0; // frame delay (hundredths)\n\tprotected boolean started = false; // ready to output frames\n\tprotected OutputStream out;\n\tprotected BufferedImage image; // current frame\n\tprotected byte[] pixels; // BGR byte array from frame\n\tprotected byte[] indexedPixels; // converted frame indexed to palette\n\tprotected int colorDepth; // number of bit planes\n\tprotected byte[] colorTab; // RGB palette\n\tprotected boolean[] usedEntry = new boolean[256]; // active palette entries\n\tprotected int palSize = 7; // color table size (bits-1)\n\tprotected int dispose = -1; // disposal code (-1 = use default)\n\tprotected boolean closeStream = false; // close stream when finished\n\tprotected boolean firstFrame = true;\n\tprotected boolean sizeSet = false; // if false, get size from first frame\n\tprotected int sample = 10; // default sample interval for quantizer\n\n\t/**\n\t * 设置每一帧的间隔时间\n\t * Sets the delay time between each frame, or changes it\n\t * for subsequent frames (applies to last frame added).\n\t *\n\t * @param ms 间隔时间，单位毫秒\n\t */\n\tpublic void setDelay(int ms) {\n\t\tdelay = Math.round(ms / 10.0f);\n\t}\n\n\t/**\n\t * Sets the GIF frame disposal code for the last added frame\n\t * and any subsequent frames.  Default is 0 if no transparent\n\t * color has been set, otherwise 2.\n\t *\n\t * @param code int disposal code.\n\t */\n\tpublic void setDispose(int code) {\n\t\tif (code >= 0) {\n\t\t\tdispose = code;\n\t\t}\n\t}\n\n\t/**\n\t * Sets the number of times the set of GIF frames\n\t * should be played.  Default is 1; 0 means play\n\t * indefinitely.  Must be invoked before the first\n\t * image is added.\n\t *\n\t * @param iter int number of iterations.\n\t */\n\tpublic void setRepeat(int iter) {\n\t\tif (iter >= 0) {\n\t\t\trepeat = iter;\n\t\t}\n\t}\n\n\t/**\n\t * Sets the transparent color for the last added frame\n\t * and any subsequent frames.\n\t * Since all colors are subject to modification\n\t * in the quantization process, the color in the final\n\t * palette for each frame closest to the given color\n\t * becomes the transparent color for that frame.\n\t * May be set to null to indicate no transparent color.\n\t *\n\t * @param c Color to be treated as transparent on display.\n\t */\n\tpublic void setTransparent(Color c) {\n\t\tsetTransparent(c, false);\n\t}\n\n\t/**\n\t * Sets the transparent color for the last added frame\n\t * and any subsequent frames.\n\t * Since all colors are subject to modification\n\t * in the quantization process, the color in the final\n\t * palette for each frame closest to the given color\n\t * becomes the transparent color for that frame.\n\t * If exactMatch is set to true, transparent color index\n\t * is search with exact match, and not looking for the\n\t * closest one.\n\t * May be set to null to indicate no transparent color.\n\t *\n\t * @param c          Color to be treated as transparent on display.\n\t * @param exactMatch If exactMatch is set to true, transparent color index is search with exact match\n\t */\n\tpublic void setTransparent(Color c, boolean exactMatch) {\n\t\ttransparent = c;\n\t\ttransparentExactMatch = exactMatch;\n\t}\n\n\n\t/**\n\t * Sets the background color for the last added frame\n\t * and any subsequent frames.\n\t * Since all colors are subject to modification\n\t * in the quantization process, the color in the final\n\t * palette for each frame closest to the given color\n\t * becomes the background color for that frame.\n\t * May be set to null to indicate no background color\n\t * which will default to black.\n\t *\n\t * @param c Color to be treated as background on display.\n\t */\n\tpublic void setBackground(Color c) {\n\t\tbackground = c;\n\t}\n\n\t/**\n\t * Adds next GIF frame.  The frame is not written immediately, but is\n\t * actually deferred until the next frame is received so that timing\n\t * data can be inserted.  Invoking {@code finish()} flushes all\n\t * frames.  If {@code setSize} was not invoked, the size of the\n\t * first image is used for all subsequent frames.\n\t *\n\t * @param im BufferedImage containing frame to write.\n\t * @return true if successful.\n\t */\n\tpublic boolean addFrame(BufferedImage im) {\n\t\tif ((im == null) || !started) {\n\t\t\treturn false;\n\t\t}\n\t\tboolean ok = true;\n\t\ttry {\n\t\t\tif (!sizeSet) {\n\t\t\t\t// use first frame's size\n\t\t\t\tsetSize(im.getWidth(), im.getHeight());\n\t\t\t}\n\t\t\timage = im;\n\t\t\tgetImagePixels(); // convert to correct format if necessary\n\t\t\tanalyzePixels(); // build color table & map pixels\n\t\t\tif (firstFrame) {\n\t\t\t\twriteLSD(); // logical screen descriptior\n\t\t\t\twritePalette(); // global color table\n\t\t\t\tif (repeat >= 0) {\n\t\t\t\t\t// use NS app extension to indicate reps\n\t\t\t\t\twriteNetscapeExt();\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteGraphicCtrlExt(); // write graphic control extension\n\t\t\twriteImageDesc(); // image descriptor\n\t\t\tif (!firstFrame) {\n\t\t\t\twritePalette(); // local color table\n\t\t\t}\n\t\t\twritePixels(); // encode and write pixel data\n\t\t\tfirstFrame = false;\n\t\t} catch (IOException e) {\n\t\t\tok = false;\n\t\t}\n\n\t\treturn ok;\n\t}\n\n\t/**\n\t * Flushes any pending data and closes output file.\n\t * If writing to an OutputStream, the stream is not\n\t * closed.\n\t *\n\t * @return is ok\n\t */\n\tpublic boolean finish() {\n\t\tif (!started) return false;\n\t\tboolean ok = true;\n\t\tstarted = false;\n\t\ttry {\n\t\t\tout.write(0x3b); // gif trailer\n\t\t\tout.flush();\n\t\t\tif (closeStream) {\n\t\t\t\tout.close();\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tok = false;\n\t\t}\n\n\t\t// reset for subsequent use\n\t\ttransIndex = 0;\n\t\tout = null;\n\t\timage = null;\n\t\tpixels = null;\n\t\tindexedPixels = null;\n\t\tcolorTab = null;\n\t\tcloseStream = false;\n\t\tfirstFrame = true;\n\n\t\treturn ok;\n\t}\n\n\t/**\n\t * Sets frame rate in frames per second.  Equivalent to\n\t * {@code setDelay(1000/fps)}.\n\t *\n\t * @param fps float frame rate (frames per second)\n\t */\n\tpublic void setFrameRate(float fps) {\n\t\tif (fps != 0f) {\n\t\t\tdelay = Math.round(100f / fps);\n\t\t}\n\t}\n\n\t/**\n\t * Sets quality of color quantization (conversion of images\n\t * to the maximum 256 colors allowed by the GIF specification).\n\t * Lower values (minimum = 1) produce better colors, but slow\n\t * processing significantly.  10 is the default, and produces\n\t * good color mapping at reasonable speeds.  Values greater\n\t * than 20 do not yield significant improvements in speed.\n\t *\n\t * @param quality int greater than 0.\n\t */\n\tpublic void setQuality(int quality) {\n\t\tif (quality < 1) quality = 1;\n\t\tsample = quality;\n\t}\n\n\t/**\n\t * Sets the GIF frame size.  The default size is the\n\t * size of the first frame added if this method is\n\t * not invoked.\n\t *\n\t * @param w int frame width.\n\t * @param h int frame width.\n\t */\n\tpublic void setSize(int w, int h) {\n\t\tif (started && !firstFrame) return;\n\t\twidth = w;\n\t\theight = h;\n\t\tif (width < 1) width = 320;\n\t\tif (height < 1) height = 240;\n\t\tsizeSet = true;\n\t}\n\n\t/**\n\t * Initiates GIF file creation on the given stream.  The stream\n\t * is not closed automatically.\n\t *\n\t * @param os OutputStream on which GIF images are written.\n\t * @return false if initial write failed.\n\t */\n\tpublic boolean start(OutputStream os) {\n\t\tif (os == null) return false;\n\t\tboolean ok = true;\n\t\tcloseStream = false;\n\t\tout = os;\n\t\ttry {\n\t\t\twriteString(\"GIF89a\"); // header\n\t\t} catch (IOException e) {\n\t\t\tok = false;\n\t\t}\n\t\treturn started = ok;\n\t}\n\n\t/**\n\t * Initiates writing of a GIF file with the specified name.\n\t *\n\t * @param file String containing output file name.\n\t * @return false if open or initial write failed.\n\t */\n\tpublic boolean start(String file) {\n\t\tboolean ok;\n\t\ttry {\n\t\t\tout = new BufferedOutputStream(Files.newOutputStream(Paths.get(file)));\n\t\t\tok = start(out);\n\t\t\tcloseStream = true;\n\t\t} catch (IOException e) {\n\t\t\tok = false;\n\t\t}\n\t\treturn started = ok;\n\t}\n\n\tpublic boolean isStarted() {\n\t\treturn started;\n\t}\n\n\t/**\n\t * Analyzes image colors and creates color map.\n\t */\n\tprotected void analyzePixels() {\n\t\tint len = pixels.length;\n\t\tint nPix = len / 3;\n\t\tindexedPixels = new byte[nPix];\n\t\tNeuQuant nq = new NeuQuant(pixels, len, sample);\n\t\t// initialize quantizer\n\t\tcolorTab = nq.process(); // create reduced palette\n\t\t// convert map from BGR to RGB\n\t\tfor (int i = 0; i < colorTab.length; i += 3) {\n\t\t\tbyte temp = colorTab[i];\n\t\t\tcolorTab[i] = colorTab[i + 2];\n\t\t\tcolorTab[i + 2] = temp;\n\t\t\tusedEntry[i / 3] = false;\n\t\t}\n\t\t// map image pixels to new palette\n\t\tint k = 0;\n\t\tfor (int i = 0; i < nPix; i++) {\n\t\t\tint index =\n\t\t\t\t\tnq.map(pixels[k++] & 0xff,\n\t\t\t\t\t\t\tpixels[k++] & 0xff,\n\t\t\t\t\t\t\tpixels[k++] & 0xff);\n\t\t\tusedEntry[index] = true;\n\t\t\tindexedPixels[i] = (byte) index;\n\t\t}\n\t\tpixels = null;\n\t\tcolorDepth = 8;\n\t\tpalSize = 7;\n\t\t// get closest match to transparent color if specified\n\t\tif (transparent != null) {\n\t\t\ttransIndex = transparentExactMatch ? findExact(transparent) : findClosest(transparent);\n\t\t}\n\t}\n\n\t/**\n\t * Returns index of palette color closest to c\n\t *\n\t * @param c Color\n\t * @return index\n\t */\n\tprotected int findClosest(Color c) {\n\t\tif (colorTab == null) return -1;\n\t\tint r = c.getRed();\n\t\tint g = c.getGreen();\n\t\tint b = c.getBlue();\n\t\tint minpos = 0;\n\t\tint dmin = 256 * 256 * 256;\n\t\tint len = colorTab.length;\n\t\tfor (int i = 0; i < len; ) {\n\t\t\tint dr = r - (colorTab[i++] & 0xff);\n\t\t\tint dg = g - (colorTab[i++] & 0xff);\n\t\t\tint db = b - (colorTab[i] & 0xff);\n\t\t\tint d = dr * dr + dg * dg + db * db;\n\t\t\tint index = i / 3;\n\t\t\tif (usedEntry[index] && (d < dmin)) {\n\t\t\t\tdmin = d;\n\t\t\t\tminpos = index;\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t\treturn minpos;\n\t}\n\n\t/**\n\t * Returns true if the exact matching color is existing, and used in the color palette, otherwise, return false.\n\t * This method has to be called before finishing the image,\n\t * because after finished the palette is destroyed and it will always return false.\n\t *\n\t * @param c 颜色\n\t * @return 颜色是否存在\n\t */\n\tboolean isColorUsed(Color c) {\n\t\treturn findExact(c) != -1;\n\t}\n\n\t/**\n\t * Returns index of palette exactly matching to color c or -1 if there is no exact matching.\n\t *\n\t * @param c Color\n\t * @return index\n\t */\n\tprotected int findExact(Color c) {\n\t\tif (colorTab == null) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tint r = c.getRed();\n\t\tint g = c.getGreen();\n\t\tint b = c.getBlue();\n\t\tint len = colorTab.length / 3;\n\t\tfor (int index = 0; index < len; ++index) {\n\t\t\tint i = index * 3;\n\t\t\t// If the entry is used in colorTab, then check if it is the same exact color we're looking for\n\t\t\tif (usedEntry[index] && r == (colorTab[i] & 0xff) && g == (colorTab[i + 1] & 0xff) && b == (colorTab[i + 2] & 0xff)) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * Extracts image pixels into byte array \"pixels\"\n\t */\n\tprotected void getImagePixels() {\n\t\tint w = image.getWidth();\n\t\tint h = image.getHeight();\n\t\tint type = image.getType();\n\t\tif ((w != width)\n\t\t\t\t|| (h != height)\n\t\t\t\t|| (type != BufferedImage.TYPE_3BYTE_BGR)) {\n\t\t\t// create new image with right size/format\n\t\t\tBufferedImage temp =\n\t\t\t\t\tnew BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);\n\t\t\tGraphics2D g = temp.createGraphics();\n\t\t\tg.setColor(background);\n\t\t\tg.fillRect(0, 0, width, height);\n\t\t\tg.drawImage(image, 0, 0, null);\n\t\t\timage = temp;\n\t\t}\n\t\tpixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();\n\t}\n\n\t/**\n\t * Writes Graphic Control Extension\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeGraphicCtrlExt() throws IOException {\n\t\tout.write(0x21); // extension introducer\n\t\tout.write(0xf9); // GCE label\n\t\tout.write(4); // data block size\n\t\tint transp, disp;\n\t\tif (transparent == null) {\n\t\t\ttransp = 0;\n\t\t\tdisp = 0; // dispose = no action\n\t\t} else {\n\t\t\ttransp = 1;\n\t\t\tdisp = 2; // force clear if using transparent color\n\t\t}\n\t\tif (dispose >= 0) {\n\t\t\tdisp = dispose & 7; // user override\n\t\t}\n\t\tdisp <<= 2;\n\n\t\t// packed fields\n\t\t//noinspection PointlessBitwiseExpression\n\t\tout.write(0 | // 1:3 reserved\n\t\t\t\tdisp | // 4:6 disposal\n\t\t\t\t0 | // 7   user input - 0 = none\n\t\t\t\ttransp); // 8   transparency flag\n\n\t\twriteShort(delay); // delay x 1/100 sec\n\t\tout.write(transIndex); // transparent color index\n\t\tout.write(0); // block terminator\n\t}\n\n\t/**\n\t * Writes Image Descriptor\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeImageDesc() throws IOException {\n\t\tout.write(0x2c); // image separator\n\t\twriteShort(0); // image position x,y = 0,0\n\t\twriteShort(0);\n\t\twriteShort(width); // image size\n\t\twriteShort(height);\n\t\t// packed fields\n\t\tif (firstFrame) {\n\t\t\t// no LCT  - GCT is used for first (or only) frame\n\t\t\tout.write(0);\n\t\t} else {\n\t\t\t// specify normal LCT\n\t\t\t//noinspection PointlessBitwiseExpression\n\t\t\tout.write(0x80 | // 1 local color table  1=yes\n\t\t\t\t\t0 | // 2 interlace - 0=no\n\t\t\t\t\t0 | // 3 sorted - 0=no\n\t\t\t\t\t0 | // 4-5 reserved\n\t\t\t\t\tpalSize); // 6-8 size of color table\n\t\t}\n\t}\n\n\t/**\n\t * Writes Logical Screen Descriptor\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeLSD() throws IOException {\n\t\t// logical screen size\n\t\twriteShort(width);\n\t\twriteShort(height);\n\t\t// packed fields\n\t\t//noinspection PointlessBitwiseExpression\n\t\tout.write((0x80 | // 1   : global color table flag = 1 (gct used)\n\t\t\t\t0x70 | // 2-4 : color resolution = 7\n\t\t\t\t0x00 | // 5   : gct sort flag = 0\n\t\t\t\tpalSize)); // 6-8 : gct size\n\n\t\tout.write(0); // background color index\n\t\tout.write(0); // pixel aspect ratio - assume 1:1\n\t}\n\n\t/**\n\t * Writes Netscape application extension to define\n\t * repeat count.\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeNetscapeExt() throws IOException {\n\t\tout.write(0x21); // extension introducer\n\t\tout.write(0xff); // app extension label\n\t\tout.write(11); // block size\n\t\twriteString(\"NETSCAPE\" + \"2.0\"); // app id + auth code\n\t\tout.write(3); // sub-block size\n\t\tout.write(1); // loop sub-block id\n\t\twriteShort(repeat); // loop count (extra iterations, 0=repeat forever)\n\t\tout.write(0); // block terminator\n\t}\n\n\t/**\n\t * Writes color table\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writePalette() throws IOException {\n\t\tout.write(colorTab, 0, colorTab.length);\n\t\tint n = (3 * 256) - colorTab.length;\n\t\tfor (int i = 0; i < n; i++) {\n\t\t\tout.write(0);\n\t\t}\n\t}\n\n\t/**\n\t * Encodes and writes pixel data\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprotected void writePixels() throws IOException {\n\t\tLZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);\n\t\tencoder.encode(out);\n\t}\n\n\t/**\n\t * Write 16-bit value to output stream, LSB first\n\t *\n\t * @param value 16-bit value\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeShort(int value) throws IOException {\n\t\tout.write(value & 0xff);\n\t\tout.write((value >> 8) & 0xff);\n\t}\n\n\t/**\n\t * Writes string to output stream\n\t *\n\t * @param s String\n\t * @throws IOException IO异常\n\t */\n\tprotected void writeString(String s) throws IOException {\n\t\tfor (int i = 0; i < s.length(); i++) {\n\t\t\tout.write((byte) s.charAt(i));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/gif/GifDecoder.java",
    "content": "package cn.hutool.core.img.gif;\n\nimport cn.hutool.core.io.IoUtil;\n\nimport java.awt.AlphaComposite;\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.Graphics2D;\nimport java.awt.Rectangle;\nimport java.awt.image.BufferedImage;\nimport java.awt.image.DataBufferInt;\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\n\n/**\n * GIF文件解析\n * Class GifDecoder - Decodes a GIF file into one or more frames.\n * <p>\n * Example:\n *\n * <pre>\n * {@code\n *    GifDecoder d = new GifDecoder();\n *    d.read(\"sample.gif\");\n *    int n = d.getFrameCount();\n *    for (int i = 0; i < n; i++) {\n *       BufferedImage frame = d.getFrame(i);  // frame i\n *       int t = d.getDelay(i);  // display duration of frame in milliseconds\n *       // do something with frame\n *    }\n * }\n * </pre>\n * <p>\n * 来自：https://github.com/rtyley/animated-gif-lib-for-java\n *\n * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.\n */\npublic class GifDecoder {\n\n\t/**\n\t * File read status: No errors.\n\t */\n\tpublic static final int STATUS_OK = 0;\n\n\t/**\n\t * File read status: Error decoding file (may be partially decoded)\n\t */\n\tpublic static final int STATUS_FORMAT_ERROR = 1;\n\n\t/**\n\t * File read status: Unable to open source.\n\t */\n\tpublic static final int STATUS_OPEN_ERROR = 2;\n\n\tprotected BufferedInputStream in;\n\tprotected int status;\n\n\tprotected int width; // full image width\n\tprotected int height; // full image height\n\tprotected boolean gctFlag; // global color table used\n\tprotected int gctSize; // size of global color table\n\tprotected int loopCount = 1; // iterations; 0 = repeat forever\n\n\tprotected int[] gct; // global color table\n\tprotected int[] lct; // local color table\n\tprotected int[] act; // active color table\n\n\tprotected int bgIndex; // background color index\n\tprotected int bgColor; // background color\n\tprotected int lastBgColor; // previous bg color\n\tprotected int pixelAspect; // pixel aspect ratio\n\n\tprotected boolean lctFlag; // local color table flag\n\tprotected boolean interlace; // interlace flag\n\tprotected int lctSize; // local color table size\n\n\tprotected int ix, iy, iw, ih; // current image rectangle\n\tprotected Rectangle lastRect; // last image rect\n\tprotected BufferedImage image; // current frame\n\tprotected BufferedImage lastImage; // previous frame\n\n\tprotected byte[] block = new byte[256]; // current data block\n\tprotected int blockSize = 0; // block size\n\n\t// last graphic control extension info\n\tprotected int dispose = 0;\n\t// 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev\n\tprotected int lastDispose = 0;\n\tprotected boolean transparency = false; // use transparent color\n\tprotected int delay = 0; // delay in milliseconds\n\tprotected int transIndex; // transparent color index\n\n\tprotected static final int MAX_STACK_SIZE = 4096;\n\t// max decoder pixel stack size\n\n\t// LZW decoder working arrays\n\tprotected short[] prefix;\n\tprotected byte[] suffix;\n\tprotected byte[] pixelStack;\n\tprotected byte[] pixels;\n\n\tprotected ArrayList<GifFrame> frames; // frames read from current file\n\tprotected int frameCount;\n\n\tstatic class GifFrame {\n\t\tpublic GifFrame(BufferedImage im, int del) {\n\t\t\timage = im;\n\t\t\tdelay = del;\n\t\t}\n\n\t\tpublic BufferedImage image;\n\t\tpublic int delay;\n\t}\n\n\t/**\n\t * Gets display duration for specified frame.\n\t *\n\t * @param n int index of frame\n\t * @return delay in milliseconds\n\t */\n\tpublic int getDelay(int n) {\n\t\t//\n\t\tdelay = -1;\n\t\tif ((n >= 0) && (n < frameCount)) {\n\t\t\tdelay = frames.get(n).delay;\n\t\t}\n\t\treturn delay;\n\t}\n\n\t/**\n\t * Gets the number of frames read from file.\n\t *\n\t * @return frame count\n\t */\n\tpublic int getFrameCount() {\n\t\treturn frameCount;\n\t}\n\n\t/**\n\t * Gets the first (or only) image read.\n\t *\n\t * @return BufferedImage containing first frame, or null if none.\n\t */\n\tpublic BufferedImage getImage() {\n\t\treturn getFrame(0);\n\t}\n\n\t/**\n\t * Gets the \"Netscape\" iteration count, if any.\n\t * A count of 0 means repeat indefinitiely.\n\t *\n\t * @return iteration count if one was specified, else 1.\n\t */\n\tpublic int getLoopCount() {\n\t\treturn loopCount;\n\t}\n\n\t/**\n\t * Creates new frame image from current data (and previous\n\t * frames as specified by their disposition codes).\n\t */\n\tprotected void setPixels() {\n\t\t// expose destination image's pixels as int array\n\t\tint[] dest =\n\t\t\t\t((DataBufferInt) image.getRaster().getDataBuffer()).getData();\n\n\t\t// fill in starting image contents based on last image's dispose code\n\t\tif (lastDispose > 0) {\n\t\t\tif (lastDispose == 3) {\n\t\t\t\t// use image before last\n\t\t\t\tint n = frameCount - 2;\n\t\t\t\tif (n > 0) {\n\t\t\t\t\tlastImage = getFrame(n - 1);\n\t\t\t\t} else {\n\t\t\t\t\tlastImage = null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (lastImage != null) {\n\t\t\t\tint[] prev =\n\t\t\t\t\t\t((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();\n\t\t\t\tSystem.arraycopy(prev, 0, dest, 0, width * height);\n\t\t\t\t// copy pixels\n\n\t\t\t\tif (lastDispose == 2) {\n\t\t\t\t\t// fill last image rect area with background color\n\t\t\t\t\tGraphics2D g = image.createGraphics();\n\t\t\t\t\tColor c;\n\t\t\t\t\tif (transparency) {\n\t\t\t\t\t\tc = new Color(0, 0, 0, 0);    // assume background is transparent\n\t\t\t\t\t} else {\n\t\t\t\t\t\tc = new Color(lastBgColor); // use given background color\n\t\t\t\t\t}\n\t\t\t\t\tg.setColor(c);\n\t\t\t\t\tg.setComposite(AlphaComposite.Src); // replace area\n\t\t\t\t\tg.fill(lastRect);\n\t\t\t\t\tg.dispose();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// copy each source line to the appropriate place in the destination\n\t\tint pass = 1;\n\t\tint inc = 8;\n\t\tint iline = 0;\n\t\tfor (int i = 0; i < ih; i++) {\n\t\t\tint line = i;\n\t\t\tif (interlace) {\n\t\t\t\tif (iline >= ih) {\n\t\t\t\t\tpass++;\n\t\t\t\t\tswitch (pass) {\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tiline = 4;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\tiline = 2;\n\t\t\t\t\t\t\tinc = 4;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 4:\n\t\t\t\t\t\t\tiline = 1;\n\t\t\t\t\t\t\tinc = 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tline = iline;\n\t\t\t\tiline += inc;\n\t\t\t}\n\t\t\tline += iy;\n\t\t\tif (line < height) {\n\t\t\t\tint k = line * width;\n\t\t\t\tint dx = k + ix; // start of line in dest\n\t\t\t\tint dlim = dx + iw; // end of dest line\n\t\t\t\tif ((k + width) < dlim) {\n\t\t\t\t\tdlim = k + width; // past dest edge\n\t\t\t\t}\n\t\t\t\tint sx = i * iw; // start of line in source\n\t\t\t\twhile (dx < dlim) {\n\t\t\t\t\t// map color and insert in destination\n\t\t\t\t\tint index = ((int) pixels[sx++]) & 0xff;\n\t\t\t\t\tint c = act[index];\n\t\t\t\t\tif (c != 0) {\n\t\t\t\t\t\tdest[dx] = c;\n\t\t\t\t\t}\n\t\t\t\t\tdx++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Gets the image contents of frame n.\n\t *\n\t * @param n frame\n\t * @return BufferedImage\n\t */\n\tpublic BufferedImage getFrame(int n) {\n\t\tBufferedImage im = null;\n\t\tif ((n >= 0) && (n < frameCount)) {\n\t\t\tim = frames.get(n).image;\n\t\t}\n\t\treturn im;\n\t}\n\n\t/**\n\t * Gets image size.\n\t *\n\t * @return GIF image dimensions\n\t */\n\tpublic Dimension getFrameSize() {\n\t\treturn new Dimension(width, height);\n\t}\n\n\t/**\n\t * Reads GIF image from stream\n\t *\n\t * @param is BufferedInputStream containing GIF file.\n\t * @return read status code (0 = no errors)\n\t */\n\tpublic int read(BufferedInputStream is) {\n\t\tinit();\n\t\tif (is != null) {\n\t\t\tin = is;\n\t\t\treadHeader();\n\t\t\tif (false == err()) {\n\t\t\t\treadContents();\n\t\t\t\tif (frameCount < 0) {\n\t\t\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tstatus = STATUS_OPEN_ERROR;\n\t\t}\n\t\tIoUtil.close(is);\n\t\treturn status;\n\t}\n\n\t/**\n\t * Reads GIF image from stream\n\t *\n\t * @param is InputStream containing GIF file.\n\t * @return read status code (0 = no errors)\n\t */\n\tpublic int read(InputStream is) {\n\t\tinit();\n\t\tif (is != null) {\n\t\t\tif (!(is instanceof BufferedInputStream))\n\t\t\t\tis = new BufferedInputStream(is);\n\t\t\tin = (BufferedInputStream) is;\n\t\t\treadHeader();\n\t\t\tif (!err()) {\n\t\t\t\treadContents();\n\t\t\t\tif (frameCount < 0) {\n\t\t\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tstatus = STATUS_OPEN_ERROR;\n\t\t}\n\t\tIoUtil.close(is);\n\t\treturn status;\n\t}\n\n\t/**\n\t * Reads GIF file from specified file/URL source\n\t * (URL assumed if name contains \":/\" or \"file:\")\n\t *\n\t * @param name String containing source\n\t * @return read status code (0 = no errors)\n\t */\n\tpublic int read(String name) {\n\t\tstatus = STATUS_OK;\n\t\ttry {\n\t\t\tname = name.trim().toLowerCase();\n\t\t\tif ((name.contains(\"file:\")) ||\n\t\t\t\t\t(name.indexOf(\":/\") > 0)) {\n\t\t\t\tURL url = new URL(name);\n\t\t\t\tin = new BufferedInputStream(url.openStream());\n\t\t\t} else {\n\t\t\t\tin = new BufferedInputStream(Files.newInputStream(Paths.get(name)));\n\t\t\t}\n\t\t\tstatus = read(in);\n\t\t} catch (IOException e) {\n\t\t\tstatus = STATUS_OPEN_ERROR;\n\t\t}\n\n\t\treturn status;\n\t}\n\n\t/**\n\t * Decodes LZW image data into pixel array.\n\t * Adapted from John Cristy's ImageMagick.\n\t */\n\tprotected void decodeImageData() {\n\t\tint NullCode = -1;\n\t\tint npix = iw * ih;\n\t\tint available,\n\t\t\t\tclear,\n\t\t\t\tcode_mask,\n\t\t\t\tcode_size,\n\t\t\t\tend_of_information,\n\t\t\t\tin_code,\n\t\t\t\told_code,\n\t\t\t\tbits,\n\t\t\t\tcode,\n\t\t\t\tcount,\n\t\t\t\ti,\n\t\t\t\tdatum,\n\t\t\t\tdata_size,\n\t\t\t\tfirst,\n\t\t\t\ttop,\n\t\t\t\tbi,\n\t\t\t\tpi;\n\n\t\tif ((pixels == null) || (pixels.length < npix)) {\n\t\t\tpixels = new byte[npix]; // allocate new pixel array\n\t\t}\n\t\tif (prefix == null) prefix = new short[MAX_STACK_SIZE];\n\t\tif (suffix == null) suffix = new byte[MAX_STACK_SIZE];\n\t\tif (pixelStack == null) pixelStack = new byte[MAX_STACK_SIZE + 1];\n\n\t\t//  Initialize GIF data stream decoder.\n\n\t\tdata_size = read();\n\t\tclear = 1 << data_size;\n\t\tend_of_information = clear + 1;\n\t\tavailable = clear + 2;\n\t\told_code = NullCode;\n\t\tcode_size = data_size + 1;\n\t\tcode_mask = (1 << code_size) - 1;\n\t\tfor (code = 0; code < clear; code++) {\n\t\t\tprefix[code] = 0;\n\t\t\tsuffix[code] = (byte) code;\n\t\t}\n\n\t\t//  Decode GIF pixel stream.\n\n\t\tdatum = bits = count = first = top = pi = bi = 0;\n\n\t\tfor (i = 0; i < npix; ) {\n\t\t\tif (top == 0) {\n\t\t\t\tif (bits < code_size) {\n\t\t\t\t\t//  Load bytes until there are enough bits for a code.\n\t\t\t\t\tif (count == 0) {\n\t\t\t\t\t\t// Read a new data block.\n\t\t\t\t\t\tcount = readBlock();\n\t\t\t\t\t\tif (count <= 0)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tbi = 0;\n\t\t\t\t\t}\n\t\t\t\t\tdatum += (((int) block[bi]) & 0xff) << bits;\n\t\t\t\t\tbits += 8;\n\t\t\t\t\tbi++;\n\t\t\t\t\tcount--;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t//  Get the next code.\n\n\t\t\t\tcode = datum & code_mask;\n\t\t\t\tdatum >>= code_size;\n\t\t\t\tbits -= code_size;\n\n\t\t\t\t//  Interpret the code\n\n\t\t\t\tif ((code > available) || (code == end_of_information))\n\t\t\t\t\tbreak;\n\t\t\t\tif (code == clear) {\n\t\t\t\t\t//  Reset decoder.\n\t\t\t\t\tcode_size = data_size + 1;\n\t\t\t\t\tcode_mask = (1 << code_size) - 1;\n\t\t\t\t\tavailable = clear + 2;\n\t\t\t\t\told_code = NullCode;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (old_code == NullCode) {\n\t\t\t\t\tpixelStack[top++] = suffix[code];\n\t\t\t\t\told_code = code;\n\t\t\t\t\tfirst = code;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tin_code = code;\n\t\t\t\tif (code == available) {\n\t\t\t\t\tpixelStack[top++] = (byte) first;\n\t\t\t\t\tcode = old_code;\n\t\t\t\t}\n\t\t\t\twhile (code > clear) {\n\t\t\t\t\tpixelStack[top++] = suffix[code];\n\t\t\t\t\tcode = prefix[code];\n\t\t\t\t}\n\t\t\t\tfirst = ((int) suffix[code]) & 0xff;\n\n\t\t\t\t//  Add a new string to the string table,\n\n\t\t\t\tif (available >= MAX_STACK_SIZE) {\n\t\t\t\t\tpixelStack[top++] = (byte) first;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tpixelStack[top++] = (byte) first;\n\t\t\t\tprefix[available] = (short) old_code;\n\t\t\t\tsuffix[available] = (byte) first;\n\t\t\t\tavailable++;\n\t\t\t\tif (((available & code_mask) == 0)\n\t\t\t\t\t\t&& (available < MAX_STACK_SIZE)) {\n\t\t\t\t\tcode_size++;\n\t\t\t\t\tcode_mask += available;\n\t\t\t\t}\n\t\t\t\told_code = in_code;\n\t\t\t}\n\n\t\t\t//  Pop a pixel off the pixel stack.\n\n\t\t\ttop--;\n\t\t\tpixels[pi++] = pixelStack[top];\n\t\t\ti++;\n\t\t}\n\n\t\tfor (i = pi; i < npix; i++) {\n\t\t\tpixels[i] = 0; // clear missing pixels\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns true if an error was encountered during reading/decoding\n\t *\n\t * @return true if an error was encountered during reading/decoding\n\t */\n\tprotected boolean err() {\n\t\treturn status != STATUS_OK;\n\t}\n\n\t/**\n\t * Initializes or re-initializes reader\n\t */\n\tprotected void init() {\n\t\tstatus = STATUS_OK;\n\t\tframeCount = 0;\n\t\tframes = new ArrayList<>();\n\t\tgct = null;\n\t\tlct = null;\n\t}\n\n\t/**\n\t * Reads a single byte from the input stream.\n\t *\n\t * @return single byte\n\t */\n\tprotected int read() {\n\t\tint curByte = 0;\n\t\ttry {\n\t\t\tcurByte = in.read();\n\t\t} catch (IOException e) {\n\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t}\n\t\treturn curByte;\n\t}\n\n\t/**\n\t * Reads next variable length block from input.\n\t *\n\t * @return number of bytes stored in \"buffer\"\n\t */\n\tprotected int readBlock() {\n\t\tblockSize = read();\n\t\tint n = 0;\n\t\tif (blockSize > 0) {\n\t\t\ttry {\n\t\t\t\tint count;\n\t\t\t\twhile (n < blockSize) {\n\t\t\t\t\tcount = in.read(block, n, blockSize - n);\n\t\t\t\t\tif (count == -1)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tn += count;\n\t\t\t\t}\n\t\t\t} catch (IOException e) {\n\t\t\t\t//ignore\n\t\t\t}\n\n\t\t\tif (n < blockSize) {\n\t\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t\t}\n\t\t}\n\t\treturn n;\n\t}\n\n\t/**\n\t * Reads color table as 256 RGB integer values\n\t *\n\t * @param ncolors int number of colors to read\n\t * @return int array containing 256 colors (packed ARGB with full alpha)\n\t */\n\tprotected int[] readColorTable(int ncolors) {\n\t\tint nbytes = 3 * ncolors;\n\t\tint[] tab = null;\n\t\tbyte[] c = new byte[nbytes];\n\t\tint n = 0;\n\t\ttry {\n\t\t\tn = in.read(c);\n\t\t} catch (IOException e) {\n\t\t\t//ignore\n\t\t}\n\t\tif (n < nbytes) {\n\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t} else {\n\t\t\ttab = new int[256]; // max size to avoid bounds checks\n\t\t\tint i = 0;\n\t\t\tint j = 0;\n\t\t\twhile (i < ncolors) {\n\t\t\t\tint r = ((int) c[j++]) & 0xff;\n\t\t\t\tint g = ((int) c[j++]) & 0xff;\n\t\t\t\tint b = ((int) c[j++]) & 0xff;\n\t\t\t\ttab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;\n\t\t\t}\n\t\t}\n\t\treturn tab;\n\t}\n\n\t/**\n\t * Main file parser.  Reads GIF content blocks.\n\t */\n\tprotected void readContents() {\n\t\t// read GIF file content blocks\n\t\tboolean done = false;\n\t\twhile (!(done || err())) {\n\t\t\tint code = read();\n\t\t\tswitch (code) {\n\n\t\t\t\tcase 0x2C: // image separator\n\t\t\t\t\treadImage();\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 0x21: // extension\n\t\t\t\t\tcode = read();\n\t\t\t\t\tswitch (code) {\n\t\t\t\t\t\tcase 0xf9: // graphics control extension\n\t\t\t\t\t\t\treadGraphicControlExt();\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase 0xff: // application extension\n\t\t\t\t\t\t\treadBlock();\n\t\t\t\t\t\t\tfinal StringBuilder app = new StringBuilder();\n\t\t\t\t\t\t\tfor (int i = 0; i < 11; i++) {\n\t\t\t\t\t\t\t\tapp.append((char) block[i]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (\"NETSCAPE2.0\".contentEquals(app)) {\n\t\t\t\t\t\t\t\treadNetscapeExt();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tskip(); // don't care\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault: // uninteresting extension\n\t\t\t\t\t\t\tskip();\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 0x3b: // terminator\n\t\t\t\t\tdone = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 0x00: // bad byte, but keep going and see what happens\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reads Graphics Control Extension values\n\t */\n\tprotected void readGraphicControlExt() {\n\t\tread(); // block size\n\t\tint packed = read(); // packed fields\n\t\tdispose = (packed & 0x1c) >> 2; // disposal method\n\t\tif (dispose == 0) {\n\t\t\tdispose = 1; // elect to keep old image if discretionary\n\t\t}\n\t\ttransparency = (packed & 1) != 0;\n\t\tdelay = readShort() * 10; // delay in milliseconds\n\t\ttransIndex = read(); // transparent color index\n\t\tread(); // block terminator\n\t}\n\n\t/**\n\t * Reads GIF file header information.\n\t */\n\tprotected void readHeader() {\n\t\tfinal StringBuilder id = new StringBuilder();\n\t\tfor (int i = 0; i < 6; i++) {\n\t\t\tid.append((char) read());\n\t\t}\n\t\tif (false == id.toString().startsWith(\"GIF\")) {\n\t\t\tstatus = STATUS_FORMAT_ERROR;\n\t\t\treturn;\n\t\t}\n\n\t\treadLSD();\n\t\tif (gctFlag && !err()) {\n\t\t\tgct = readColorTable(gctSize);\n\t\t\tbgColor = gct[bgIndex];\n\t\t}\n\t}\n\n\t/**\n\t * Reads next frame image\n\t */\n\tprotected void readImage() {\n\t\tix = readShort(); // (sub)image position & size\n\t\tiy = readShort();\n\t\tiw = readShort();\n\t\tih = readShort();\n\n\t\tint packed = read();\n\t\tlctFlag = (packed & 0x80) != 0; // 1 - local color table flag\n\t\tinterlace = (packed & 0x40) != 0; // 2 - interlace flag\n\t\t// 3 - sort flag\n\t\t// 4-5 - reserved\n\t\tlctSize = 2 << (packed & 7); // 6-8 - local color table size\n\n\t\tif (lctFlag) {\n\t\t\tlct = readColorTable(lctSize); // read table\n\t\t\tact = lct; // make local table active\n\t\t} else {\n\t\t\tact = gct; // make global table active\n\t\t\tif (bgIndex == transIndex)\n\t\t\t\tbgColor = 0;\n\t\t}\n\t\tint save = 0;\n\t\tif (transparency) {\n\t\t\tsave = act[transIndex];\n\t\t\tact[transIndex] = 0; // set transparent color if specified\n\t\t}\n\n\t\tif (act == null) {\n\t\t\tstatus = STATUS_FORMAT_ERROR; // no color table defined\n\t\t}\n\n\t\tif (err()) return;\n\n\t\tdecodeImageData(); // decode pixel data\n\t\tskip();\n\n\t\tif (err()) return;\n\n\t\tframeCount++;\n\n\t\t// create new image to receive frame data\n\t\timage =\n\t\t\t\tnew BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);\n\n\t\tsetPixels(); // transfer pixel data to image\n\n\t\tframes.add(new GifFrame(image, delay)); // add image to frame list\n\n\t\tif (transparency) {\n\t\t\tact[transIndex] = save;\n\t\t}\n\t\tresetFrame();\n\n\t}\n\n\t/**\n\t * Reads Logical Screen Descriptor\n\t */\n\tprotected void readLSD() {\n\n\t\t// logical screen size\n\t\twidth = readShort();\n\t\theight = readShort();\n\n\t\t// packed fields\n\t\tint packed = read();\n\t\tgctFlag = (packed & 0x80) != 0; // 1   : global color table flag\n\t\t// 2-4 : color resolution\n\t\t// 5   : gct sort flag\n\t\tgctSize = 2 << (packed & 7); // 6-8 : gct size\n\n\t\tbgIndex = read(); // background color index\n\t\tpixelAspect = read(); // pixel aspect ratio\n\t}\n\n\t/**\n\t * Reads Netscape extenstion to obtain iteration count\n\t */\n\tprotected void readNetscapeExt() {\n\t\tdo {\n\t\t\treadBlock();\n\t\t\tif (block[0] == 1) {\n\t\t\t\t// loop count sub-block\n\t\t\t\tint b1 = ((int) block[1]) & 0xff;\n\t\t\t\tint b2 = ((int) block[2]) & 0xff;\n\t\t\t\tloopCount = (b2 << 8) | b1;\n\t\t\t}\n\t\t} while ((blockSize > 0) && !err());\n\t}\n\n\t/**\n\t * Reads next 16-bit value, LSB first\n\t *\n\t * @return next 16-bit value\n\t */\n\tprotected int readShort() {\n\t\t// read 16-bit value, LSB first\n\t\treturn read() | (read() << 8);\n\t}\n\n\t/**\n\t * Resets frame state for reading next image.\n\t */\n\tprotected void resetFrame() {\n\t\tlastDispose = dispose;\n\t\tlastRect = new Rectangle(ix, iy, iw, ih);\n\t\tlastImage = image;\n\t\tlastBgColor = bgColor;\n\t\tlct = null;\n\t}\n\n\t/**\n\t * Skips variable length blocks up to and including\n\t * next zero length block.\n\t */\n\tprotected void skip() {\n\t\tdo {\n\t\t\treadBlock();\n\t\t} while ((blockSize > 0) && !err());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/gif/LZWEncoder.java",
    "content": "package cn.hutool.core.img.gif;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n//==============================================================================\n//  Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott.\n//  K Weiner 12/00\n\nclass LZWEncoder {\n\n\tprivate static final int EOF = -1;\n\n\tprivate final int imgW;\n\tprivate final int imgH;\n\tprivate final byte[] pixAry;\n\tprivate final int initCodeSize;\n\n\tprivate int remaining;\n\tprivate int curPixel;\n\n\t// GIFCOMPR.C       - GIF Image compression routines\n\t//\n\t// Lempel-Ziv compression based on 'compress'.  GIF modifications by\n\t// David Rowley (mgardi@watdcsu.waterloo.edu)\n\n\t// General DEFINEs\n\n\tstatic final int BITS = 12;\n\n\tstatic final int HSIZE = 5003; // 80% occupancy\n\n\t// GIF Image compression - modified 'compress'\n\t//\n\t// Based on: compress.c - File compression ala IEEE Computer, June 1984.\n\t//\n\t// By Authors:  Spencer W. Thomas      (decvax!harpo!utah-cs!utah-gr!thomas)\n\t//              Jim McKie              (decvax!mcvax!jim)\n\t//              Steve Davies           (decvax!vax135!petsd!peora!srd)\n\t//              Ken Turkowski          (decvax!decwrl!turtlevax!ken)\n\t//              James A. Woods         (decvax!ihnp4!ames!jaw)\n\t//              Joe Orost              (decvax!vax135!petsd!joe)\n\n\tint n_bits; // number of bits/code\n\tint maxbits = BITS; // user settable max # bits/code\n\tint maxcode; // maximum code, given n_bits\n\tint maxmaxcode = 1 << BITS; // should NEVER generate this code\n\n\tint[] htab = new int[HSIZE];\n\tint[] codetab = new int[HSIZE];\n\n\tint hsize = HSIZE; // for dynamic table sizing\n\n\tint free_ent = 0; // first unused entry\n\n\t// block compression parameters -- after all codes are used up,\n\t// and compression rate changes, start over.\n\tboolean clear_flg = false;\n\n\t// Algorithm:  use open addressing double hashing (no chaining) on the\n\t// prefix code / next character combination.  We do a variant of Knuth's\n\t// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime\n\t// secondary probe.  Here, the modular division first probe is gives way\n\t// to a faster exclusive-or manipulation.  Also do block compression with\n\t// an adaptive reset, whereby the code table is cleared when the compression\n\t// ratio decreases, but after the table fills.  The variable-length output\n\t// codes are re-sized at this point, and a special CLEAR code is generated\n\t// for the decompressor.  Late addition:  construct the table according to\n\t// file size for noticeable speed improvement on small files.  Please direct\n\t// questions about this implementation to ames!jaw.\n\n\tint g_init_bits;\n\n\tint ClearCode;\n\tint EOFCode;\n\n\t// output\n\t//\n\t// Output the given code.\n\t// Inputs:\n\t//      code:   A n_bits-bit integer.  If == -1, then EOF.  This assumes\n\t//              that n_bits =< wordsize - 1.\n\t// Outputs:\n\t//      Outputs code to the file.\n\t// Assumptions:\n\t//      Chars are 8 bits long.\n\t// Algorithm:\n\t//      Maintain a BITS character long buffer (so that 8 codes will\n\t// fit in it exactly).  Use the VAX insv instruction to insert each\n\t// code in turn.  When the buffer fills up empty it and start over.\n\n\tint cur_accum = 0;\n\tint cur_bits = 0;\n\n\tfinal int[] masks =\n\t\t{\n\t\t\t0x0000,\n\t\t\t0x0001,\n\t\t\t0x0003,\n\t\t\t0x0007,\n\t\t\t0x000F,\n\t\t\t0x001F,\n\t\t\t0x003F,\n\t\t\t0x007F,\n\t\t\t0x00FF,\n\t\t\t0x01FF,\n\t\t\t0x03FF,\n\t\t\t0x07FF,\n\t\t\t0x0FFF,\n\t\t\t0x1FFF,\n\t\t\t0x3FFF,\n\t\t\t0x7FFF,\n\t\t\t0xFFFF };\n\n\t// Number of characters so far in this 'packet'\n\tint a_count;\n\n\t// Define the storage for the packet accumulator\n\tbyte[] accum = new byte[256];\n\n\t//----------------------------------------------------------------------------\n\tLZWEncoder(int width, int height, byte[] pixels, int color_depth) {\n\t\timgW = width;\n\t\timgH = height;\n\t\tpixAry = pixels;\n\t\tinitCodeSize = Math.max(2, color_depth);\n\t}\n\n\t// Add a character to the end of the current packet, and if it is 254\n\t// characters, flush the packet to disk.\n\tvoid char_out(byte c, OutputStream outs) throws IOException {\n\t\taccum[a_count++] = c;\n\t\tif (a_count >= 254)\n\t\t\tflush_char(outs);\n\t}\n\n\t// Clear out the hash table\n\n\t// table clear for block compress\n\tvoid cl_block(OutputStream outs) throws IOException {\n\t\tcl_hash(hsize);\n\t\tfree_ent = ClearCode + 2;\n\t\tclear_flg = true;\n\n\t\toutput(ClearCode, outs);\n\t}\n\n\t// reset code table\n\tvoid cl_hash(int hsize) {\n\t\tfor (int i = 0; i < hsize; ++i)\n\t\t\thtab[i] = -1;\n\t}\n\n\tvoid compress(int init_bits, OutputStream outs) throws IOException {\n\t\tint fcode;\n\t\tint i /* = 0 */;\n\t\tint c;\n\t\tint ent;\n\t\tint disp;\n\t\tint hsize_reg;\n\t\tint hshift;\n\n\t\t// Set up the globals:  g_init_bits - initial number of bits\n\t\tg_init_bits = init_bits;\n\n\t\t// Set up the necessary values\n\t\tclear_flg = false;\n\t\tn_bits = g_init_bits;\n\t\tmaxcode = MAXCODE(n_bits);\n\n\t\tClearCode = 1 << (init_bits - 1);\n\t\tEOFCode = ClearCode + 1;\n\t\tfree_ent = ClearCode + 2;\n\n\t\ta_count = 0; // clear packet\n\n\t\tent = nextPixel();\n\n\t\thshift = 0;\n\t\tfor (fcode = hsize; fcode < 65536; fcode *= 2)\n\t\t\t++hshift;\n\t\thshift = 8 - hshift; // set hash code range bound\n\n\t\thsize_reg = hsize;\n\t\tcl_hash(hsize_reg); // clear hash table\n\n\t\toutput(ClearCode, outs);\n\n\t\touter_loop : while ((c = nextPixel()) != EOF) {\n\t\t\tfcode = (c << maxbits) + ent;\n\t\t\ti = (c << hshift) ^ ent; // xor hashing\n\n\t\t\tif (htab[i] == fcode) {\n\t\t\t\tent = codetab[i];\n\t\t\t\tcontinue;\n\t\t\t} else if (htab[i] >= 0) // non-empty slot\n\t\t\t\t{\n\t\t\t\tdisp = hsize_reg - i; // secondary hash (after G. Knott)\n\t\t\t\tif (i == 0)\n\t\t\t\t\tdisp = 1;\n\t\t\t\tdo {\n\t\t\t\t\tif ((i -= disp) < 0)\n\t\t\t\t\t\ti += hsize_reg;\n\n\t\t\t\t\tif (htab[i] == fcode) {\n\t\t\t\t\t\tent = codetab[i];\n\t\t\t\t\t\tcontinue outer_loop;\n\t\t\t\t\t}\n\t\t\t\t} while (htab[i] >= 0);\n\t\t\t}\n\t\t\toutput(ent, outs);\n\t\t\tent = c;\n\t\t\tif (free_ent < maxmaxcode) {\n\t\t\t\tcodetab[i] = free_ent++; // code -> hashtable\n\t\t\t\thtab[i] = fcode;\n\t\t\t} else\n\t\t\t\tcl_block(outs);\n\t\t}\n\t\t// Put out the final code.\n\t\toutput(ent, outs);\n\t\toutput(EOFCode, outs);\n\t}\n\n\t//----------------------------------------------------------------------------\n\tvoid encode(OutputStream os) throws IOException {\n\t\tos.write(initCodeSize); // write \"initial code size\" byte\n\n\t\tremaining = imgW * imgH; // reset navigation variables\n\t\tcurPixel = 0;\n\n\t\tcompress(initCodeSize + 1, os); // compress and write the pixel data\n\n\t\tos.write(0); // write block terminator\n\t}\n\n\t// Flush the packet to disk, and reset the accumulator\n\tvoid flush_char(OutputStream outs) throws IOException {\n\t\tif (a_count > 0) {\n\t\t\touts.write(a_count);\n\t\t\touts.write(accum, 0, a_count);\n\t\t\ta_count = 0;\n\t\t}\n\t}\n\n\tfinal int MAXCODE(int n_bits) {\n\t\treturn (1 << n_bits) - 1;\n\t}\n\n\t//----------------------------------------------------------------------------\n\t// Return the next pixel from the image\n\t//----------------------------------------------------------------------------\n\tprivate int nextPixel() {\n\t\tif (remaining == 0)\n\t\t\treturn EOF;\n\n\t\t--remaining;\n\n\t\tbyte pix = pixAry[curPixel++];\n\n\t\treturn pix & 0xff;\n\t}\n\n\tvoid output(int code, OutputStream outs) throws IOException {\n\t\tcur_accum &= masks[cur_bits];\n\n\t\tif (cur_bits > 0)\n\t\t\tcur_accum |= (code << cur_bits);\n\t\telse\n\t\t\tcur_accum = code;\n\n\t\tcur_bits += n_bits;\n\n\t\twhile (cur_bits >= 8) {\n\t\t\tchar_out((byte) (cur_accum & 0xff), outs);\n\t\t\tcur_accum >>= 8;\n\t\t\tcur_bits -= 8;\n\t\t}\n\n\t\t// If the next entry is going to be too big for the code size,\n\t\t// then increase it, if possible.\n\t\tif (free_ent > maxcode || clear_flg) {\n\t\t\tif (clear_flg) {\n\t\t\t\tmaxcode = MAXCODE(n_bits = g_init_bits);\n\t\t\t\tclear_flg = false;\n\t\t\t} else {\n\t\t\t\t++n_bits;\n\t\t\t\tif (n_bits == maxbits)\n\t\t\t\t\tmaxcode = maxmaxcode;\n\t\t\t\telse\n\t\t\t\t\tmaxcode = MAXCODE(n_bits);\n\t\t\t}\n\t\t}\n\n\t\tif (code == EOFCode) {\n\t\t\t// At EOF, write the rest of the buffer.\n\t\t\twhile (cur_bits > 0) {\n\t\t\t\tchar_out((byte) (cur_accum & 0xff), outs);\n\t\t\t\tcur_accum >>= 8;\n\t\t\t\tcur_bits -= 8;\n\t\t\t}\n\n\t\t\tflush_char(outs);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/gif/NeuQuant.java",
    "content": "package cn.hutool.core.img.gif;\n\n/* NeuQuant Neural-Net Quantization Algorithm\n * ------------------------------------------\n *\n * Copyright (c) 1994 Anthony Dekker\n *\n * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.\n * See \"Kohonen neural networks for optimal colour quantization\"\n * in \"Network: Computation in Neural Systems\" Vol. 5 (1994) pp 351-367.\n * for a discussion of the algorithm.\n *\n * Any party obtaining a copy of these files from the author, directly or\n * indirectly, is granted, free of charge, a full and unrestricted irrevocable,\n * world-wide, paid up, royalty-free, nonexclusive right and license to deal\n * in this software and documentation files (the \"Software\"), including without\n * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,\n * and/or sell copies of the Software, and to permit persons who receive\n * copies from any such party to do so, with the only requirement being\n * that this copyright notice remain intact.\n */\n\n// Ported to Java 12/00 K Weiner\n\n/**\n * NeuQuant Neural-Net Quantization Algorithm\n *\n * @author Dekker\n */\npublic class NeuQuant {\n\n\tprotected static final int NETSIZE = 256; /* number of colours used */\n\n\t/* four primes near 500 - assume no image has a length so large */\n\t/* that it is divisible by all four primes */\n\tprotected static final int PRIME1 = 499;\n\tprotected static final int PRIME2 = 491;\n\tprotected static final int PRIME3 = 487;\n\tprotected static final int PRIME4 = 503;\n\n\tprotected static final int MINPICTUREBYTES = (3 * PRIME4);\n\t/* minimum size for input image */\n\n\t/* Program Skeleton\n\t   ----------------\n\t   [select samplefac in range 1..30]\n\t   [read image from input file]\n\t   pic = (unsigned char*) malloc(3*width*height);\n\t   initnet(pic,3*width*height,samplefac);\n\t   learn();\n\t   unbiasnet();\n\t   [write output image header, using writecolourmap(f)]\n\t   inxbuild();\n\t   write output image using inxsearch(b,g,r)      */\n\n\t/* Network Definitions\n\t   ------------------- */\n\n\tprotected static final int MAXNETPOS = (NETSIZE - 1);\n\tprotected static final int NETBIASSHIFT = 4; /* bias for colour values */\n\tprotected static final int NCYCLES = 100; /* no. of learning cycles */\n\n\t/* defs for freq and bias */\n\tprotected static final int INTBIASSHIFT = 16; /* bias for fractions */\n\tprotected static final int INTBIAS = (1 << INTBIASSHIFT);\n\tprotected static final int GAMMASHIFT = 10; /* gamma = 1024 */\n\tprotected static final int GAMMA = (1 << GAMMASHIFT);\n\tprotected static final int BETASHIFT = 10;\n\tprotected static final int BETA = (INTBIAS >> BETASHIFT); /* beta = 1/1024 */\n\tprotected static final int BETAGAMMA =\n\t\t\t(INTBIAS << (GAMMASHIFT - BETASHIFT));\n\n\t/* defs for decreasing radius factor */\n\tprotected static final int INITRAD = (NETSIZE >> 3); /* for 256 cols, radius starts */\n\tprotected static final int RADIUSBIASSHIFT = 6; /* at 32.0 biased by 6 bits */\n\tprotected static final int RADIUSBIAS = (1 << RADIUSBIASSHIFT);\n\tprotected static final int INITRADIUS = (INITRAD * RADIUSBIAS); /* and decreases by a */\n\tprotected static final int RADIUSDEC = 30; /* factor of 1/30 each cycle */\n\n\t/* defs for decreasing alpha factor */\n\tprotected static final int ALPHABIASSHIFT = 10; /* alpha starts at 1.0 */\n\tprotected static final int INITALPHA = (1 << ALPHABIASSHIFT);\n\n\tprotected int alphadec; /* biased by 10 bits */\n\n\t/* radbias and alpharadbias used for radpower calculation */\n\tprotected static final int RADBIASSHIFT = 8;\n\tprotected static final int RADBIAS = (1 << RADBIASSHIFT);\n\tprotected static final int ALPHARADBSHIFT = (ALPHABIASSHIFT + RADBIASSHIFT);\n\tprotected static final int ALPHARADBIAS = (1 << ALPHARADBSHIFT);\n\n\t/* Types and Global Variables\n\t-------------------------- */\n\n\tprotected byte[] thepicture; /* the input image itself */\n\tprotected int lengthcount; /* lengthcount = H*W*3 */\n\n\tprotected int samplefac; /* sampling factor 1..30 */\n\n\t//   typedef int pixel[4];                /* BGRc */\n\tprotected int[][] network; /* the network itself - [netsize][4] */\n\n\tprotected int[] netindex = new int[256];\n\t/* for network lookup - really 256 */\n\n\tprotected int[] bias = new int[NETSIZE];\n\t/* bias and freq arrays for learning */\n\tprotected int[] freq = new int[NETSIZE];\n\tprotected int[] radpower = new int[INITRAD];\n\t/* radpower for precomputation */\n\n\t/* Initialise network in range (0,0,0) to (255,255,255) and set parameters\n\t   ----------------------------------------------------------------------- */\n\tpublic NeuQuant(byte[] thepic, int len, int sample) {\n\n\t\tint i;\n\t\tint[] p;\n\n\t\tthepicture = thepic;\n\t\tlengthcount = len;\n\t\tsamplefac = sample;\n\n\t\tnetwork = new int[NETSIZE][];\n\t\tfor (i = 0; i < NETSIZE; i++) {\n\t\t\tnetwork[i] = new int[4];\n\t\t\tp = network[i];\n\t\t\tp[0] = p[1] = p[2] = (i << (NETBIASSHIFT + 8)) / NETSIZE;\n\t\t\tfreq[i] = INTBIAS / NETSIZE; /* 1/netsize */\n\t\t\tbias[i] = 0;\n\t\t}\n\t}\n\n\tpublic byte[] colorMap() {\n\t\tbyte[] map = new byte[3 * NETSIZE];\n\t\tint[] index = new int[NETSIZE];\n\t\tfor (int i = 0; i < NETSIZE; i++)\n\t\t\tindex[network[i][3]] = i;\n\t\tint k = 0;\n\t\tfor (int i = 0; i < NETSIZE; i++) {\n\t\t\tint j = index[i];\n\t\t\tmap[k++] = (byte) (network[j][0]);\n\t\t\tmap[k++] = (byte) (network[j][1]);\n\t\t\tmap[k++] = (byte) (network[j][2]);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/* Insertion sort of network and building of netindex[0..255] (to do after unbias)\n\t   ------------------------------------------------------------------------------- */\n\tpublic void inxbuild() {\n\n\t\tint i, j, smallpos, smallval;\n\t\tint[] p;\n\t\tint[] q;\n\t\tint previouscol, startpos;\n\n\t\tpreviouscol = 0;\n\t\tstartpos = 0;\n\t\tfor (i = 0; i < NETSIZE; i++) {\n\t\t\tp = network[i];\n\t\t\tsmallpos = i;\n\t\t\tsmallval = p[1]; /* index on g */\n\t\t\t/* find smallest in i..netsize-1 */\n\t\t\tfor (j = i + 1; j < NETSIZE; j++) {\n\t\t\t\tq = network[j];\n\t\t\t\tif (q[1] < smallval) { /* index on g */\n\t\t\t\t\tsmallpos = j;\n\t\t\t\t\tsmallval = q[1]; /* index on g */\n\t\t\t\t}\n\t\t\t}\n\t\t\tq = network[smallpos];\n\t\t\t/* swap p (i) and q (smallpos) entries */\n\t\t\tif (i != smallpos) {\n\t\t\t\tj = q[0];\n\t\t\t\tq[0] = p[0];\n\t\t\t\tp[0] = j;\n\t\t\t\tj = q[1];\n\t\t\t\tq[1] = p[1];\n\t\t\t\tp[1] = j;\n\t\t\t\tj = q[2];\n\t\t\t\tq[2] = p[2];\n\t\t\t\tp[2] = j;\n\t\t\t\tj = q[3];\n\t\t\t\tq[3] = p[3];\n\t\t\t\tp[3] = j;\n\t\t\t}\n\t\t\t/* smallval entry is now in position i */\n\t\t\tif (smallval != previouscol) {\n\t\t\t\tnetindex[previouscol] = (startpos + i) >> 1;\n\t\t\t\tfor (j = previouscol + 1; j < smallval; j++)\n\t\t\t\t\tnetindex[j] = i;\n\t\t\t\tpreviouscol = smallval;\n\t\t\t\tstartpos = i;\n\t\t\t}\n\t\t}\n\t\tnetindex[previouscol] = (startpos + MAXNETPOS) >> 1;\n\t\tfor (j = previouscol + 1; j < 256; j++)\n\t\t\tnetindex[j] = MAXNETPOS; /* really 256 */\n\t}\n\n\t/* Main Learning Loop\n\t   ------------------ */\n\tpublic void learn() {\n\n\t\tint i, j, b, g, r;\n\t\tint radius, rad, alpha, step, delta, samplepixels;\n\t\tbyte[] p;\n\t\tint pix, lim;\n\n\t\tif (lengthcount < MINPICTUREBYTES)\n\t\t\tsamplefac = 1;\n\t\talphadec = 30 + ((samplefac - 1) / 3);\n\t\tp = thepicture;\n\t\tpix = 0;\n\t\tlim = lengthcount;\n\t\tsamplepixels = lengthcount / (3 * samplefac);\n\t\tdelta = samplepixels / NCYCLES;\n\t\talpha = INITALPHA;\n\t\tradius = INITRADIUS;\n\n\t\trad = radius >> RADIUSBIASSHIFT;\n\t\tfor (i = 0; i < rad; i++)\n\t\t\tradpower[i] =\n\t\t\t\t\talpha * (((rad * rad - i * i) * RADBIAS) / (rad * rad));\n\n\t\t//fprintf(stderr,\"beginning 1D learning: initial radius=%d\\n\", rad);\n\n\t\tif (lengthcount < MINPICTUREBYTES)\n\t\t\tstep = 3;\n\t\telse if ((lengthcount % PRIME1) != 0)\n\t\t\tstep = 3 * PRIME1;\n\t\telse {\n\t\t\tif ((lengthcount % PRIME2) != 0)\n\t\t\t\tstep = 3 * PRIME2;\n\t\t\telse {\n\t\t\t\tif ((lengthcount % PRIME3) != 0)\n\t\t\t\t\tstep = 3 * PRIME3;\n\t\t\t\telse\n\t\t\t\t\tstep = 3 * PRIME4;\n\t\t\t}\n\t\t}\n\n\t\ti = 0;\n\t\twhile (i < samplepixels) {\n\t\t\tb = (p[pix] & 0xff) << NETBIASSHIFT;\n\t\t\tg = (p[pix + 1] & 0xff) << NETBIASSHIFT;\n\t\t\tr = (p[pix + 2] & 0xff) << NETBIASSHIFT;\n\t\t\tj = contest(b, g, r);\n\n\t\t\taltersingle(alpha, j, b, g, r);\n\t\t\tif (rad != 0)\n\t\t\t\talterneigh(rad, j, b, g, r); /* alter neighbours */\n\n\t\t\tpix += step;\n\t\t\tif (pix >= lim)\n\t\t\t\tpix -= lengthcount;\n\n\t\t\ti++;\n\t\t\tif (delta == 0)\n\t\t\t\tdelta = 1;\n\t\t\tif (i % delta == 0) {\n\t\t\t\talpha -= alpha / alphadec;\n\t\t\t\tradius -= radius / RADIUSDEC;\n\t\t\t\trad = radius >> RADIUSBIASSHIFT;\n\t\t\t\tif (rad <= 1)\n\t\t\t\t\trad = 0;\n\t\t\t\tfor (j = 0; j < rad; j++)\n\t\t\t\t\tradpower[j] =\n\t\t\t\t\t\t\talpha * (((rad * rad - j * j) * RADBIAS) / (rad * rad));\n\t\t\t}\n\t\t}\n\t\t//fprintf(stderr,\"finished 1D learning: final alpha=%f !\\n\",((float)alpha)/initalpha);\n\t}\n\n\t/* Search for BGR values 0..255 (after net is unbiased) and return colour index\n\t   ---------------------------------------------------------------------------- */\n\tpublic int map(int b, int g, int r) {\n\n\t\tint i, j, dist, a, bestd;\n\t\tint[] p;\n\t\tint best;\n\n\t\tbestd = 1000; /* biggest possible dist is 256*3 */\n\t\tbest = -1;\n\t\ti = netindex[g]; /* index on g */\n\t\tj = i - 1; /* start at netindex[g] and work outwards */\n\n\t\twhile ((i < NETSIZE) || (j >= 0)) {\n\t\t\tif (i < NETSIZE) {\n\t\t\t\tp = network[i];\n\t\t\t\tdist = p[1] - g; /* inx key */\n\t\t\t\tif (dist >= bestd)\n\t\t\t\t\ti = NETSIZE; /* stop iter */\n\t\t\t\telse {\n\t\t\t\t\ti++;\n\t\t\t\t\tif (dist < 0)\n\t\t\t\t\t\tdist = -dist;\n\t\t\t\t\ta = p[0] - b;\n\t\t\t\t\tif (a < 0)\n\t\t\t\t\t\ta = -a;\n\t\t\t\t\tdist += a;\n\t\t\t\t\tif (dist < bestd) {\n\t\t\t\t\t\ta = p[2] - r;\n\t\t\t\t\t\tif (a < 0)\n\t\t\t\t\t\t\ta = -a;\n\t\t\t\t\t\tdist += a;\n\t\t\t\t\t\tif (dist < bestd) {\n\t\t\t\t\t\t\tbestd = dist;\n\t\t\t\t\t\t\tbest = p[3];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (j >= 0) {\n\t\t\t\tp = network[j];\n\t\t\t\tdist = g - p[1]; /* inx key - reverse dif */\n\t\t\t\tif (dist >= bestd)\n\t\t\t\t\tj = -1; /* stop iter */\n\t\t\t\telse {\n\t\t\t\t\tj--;\n\t\t\t\t\tif (dist < 0)\n\t\t\t\t\t\tdist = -dist;\n\t\t\t\t\ta = p[0] - b;\n\t\t\t\t\tif (a < 0)\n\t\t\t\t\t\ta = -a;\n\t\t\t\t\tdist += a;\n\t\t\t\t\tif (dist < bestd) {\n\t\t\t\t\t\ta = p[2] - r;\n\t\t\t\t\t\tif (a < 0)\n\t\t\t\t\t\t\ta = -a;\n\t\t\t\t\t\tdist += a;\n\t\t\t\t\t\tif (dist < bestd) {\n\t\t\t\t\t\t\tbestd = dist;\n\t\t\t\t\t\t\tbest = p[3];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (best);\n\t}\n\n\tpublic byte[] process() {\n\t\tlearn();\n\t\tunbiasnet();\n\t\tinxbuild();\n\t\treturn colorMap();\n\t}\n\n\t/* Unbias network to give byte values 0..255 and record position i to prepare for sort\n\t   ----------------------------------------------------------------------------------- */\n\tpublic void unbiasnet() {\n\t\tfor (int i = 0; i < NETSIZE; i++) {\n\t\t\tnetwork[i][0] >>= NETBIASSHIFT;\n\t\t\tnetwork[i][1] >>= NETBIASSHIFT;\n\t\t\tnetwork[i][2] >>= NETBIASSHIFT;\n\t\t\tnetwork[i][3] = i; /* record colour no */\n\t\t}\n\t}\n\n\t/* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|]\n\t   --------------------------------------------------------------------------------- */\n\tprotected void alterneigh(int rad, int i, int b, int g, int r) {\n\n\t\tint j, k, lo, hi, a, m;\n\t\tint[] p;\n\n\t\tlo = i - rad;\n\t\tif (lo < -1)\n\t\t\tlo = -1;\n\t\thi = i + rad;\n\t\tif (hi > NETSIZE)\n\t\t\thi = NETSIZE;\n\n\t\tj = i + 1;\n\t\tk = i - 1;\n\t\tm = 1;\n\t\twhile ((j < hi) || (k > lo)) {\n\t\t\ta = radpower[m++];\n\t\t\tif (j < hi) {\n\t\t\t\tp = network[j++];\n\t\t\t\ttry {\n\t\t\t\t\tp[0] -= (a * (p[0] - b)) / ALPHARADBIAS;\n\t\t\t\t\tp[1] -= (a * (p[1] - g)) / ALPHARADBIAS;\n\t\t\t\t\tp[2] -= (a * (p[2] - r)) / ALPHARADBIAS;\n\t\t\t\t} catch (Exception ignored) {\n\t\t\t\t} // prevents 1.3 miscompilation\n\t\t\t}\n\t\t\tif (k > lo) {\n\t\t\t\tp = network[k--];\n\t\t\t\ttry {\n\t\t\t\t\tp[0] -= (a * (p[0] - b)) / ALPHARADBIAS;\n\t\t\t\t\tp[1] -= (a * (p[1] - g)) / ALPHARADBIAS;\n\t\t\t\t\tp[2] -= (a * (p[2] - r)) / ALPHARADBIAS;\n\t\t\t\t} catch (Exception ignored) {\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Move neuron i towards biased (b,g,r) by factor alpha\n\t   ---------------------------------------------------- */\n\tprotected void altersingle(int alpha, int i, int b, int g, int r) {\n\n\t\t/* alter hit neuron */\n\t\tint[] n = network[i];\n\t\tn[0] -= (alpha * (n[0] - b)) / INITALPHA;\n\t\tn[1] -= (alpha * (n[1] - g)) / INITALPHA;\n\t\tn[2] -= (alpha * (n[2] - r)) / INITALPHA;\n\t}\n\n\t/* Search for biased BGR values\n\t   ---------------------------- */\n\tprotected int contest(int b, int g, int r) {\n\n\t\t/* finds closest neuron (min dist) and updates freq */\n\t\t/* finds best neuron (min dist-bias) and returns position */\n\t\t/* for frequently chosen neurons, freq[i] is high and bias[i] is negative */\n\t\t/* bias[i] = gamma*((1/netsize)-freq[i]) */\n\n\t\tint i, dist, a, biasdist, betafreq;\n\t\tint bestpos, bestbiaspos, bestd, bestbiasd;\n\t\tint[] n;\n\n\t\tbestd = ~(1 << 31);\n\t\tbestbiasd = bestd;\n\t\tbestpos = -1;\n\t\tbestbiaspos = bestpos;\n\n\t\tfor (i = 0; i < NETSIZE; i++) {\n\t\t\tn = network[i];\n\t\t\tdist = n[0] - b;\n\t\t\tif (dist < 0)\n\t\t\t\tdist = -dist;\n\t\t\ta = n[1] - g;\n\t\t\tif (a < 0)\n\t\t\t\ta = -a;\n\t\t\tdist += a;\n\t\t\ta = n[2] - r;\n\t\t\tif (a < 0)\n\t\t\t\ta = -a;\n\t\t\tdist += a;\n\t\t\tif (dist < bestd) {\n\t\t\t\tbestd = dist;\n\t\t\t\tbestpos = i;\n\t\t\t}\n\t\t\tbiasdist = dist - ((bias[i]) >> (INTBIASSHIFT - NETBIASSHIFT));\n\t\t\tif (biasdist < bestbiasd) {\n\t\t\t\tbestbiasd = biasdist;\n\t\t\t\tbestbiaspos = i;\n\t\t\t}\n\t\t\tbetafreq = (freq[i] >> BETASHIFT);\n\t\t\tfreq[i] -= betafreq;\n\t\t\tbias[i] += (betafreq << GAMMASHIFT);\n\t\t}\n\t\tfreq[bestpos] += BETA;\n\t\tbias[bestpos] -= BETAGAMMA;\n\t\treturn (bestbiaspos);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/gif/package-info.java",
    "content": "/**\n * GIF处理，来自：https://github.com/rtyley/animated-gif-lib-for-java\n *\n * @author looly\n *\n */\npackage cn.hutool.core.img.gif;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/img/package-info.java",
    "content": "/**\n * 图像处理相关工具类封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.img;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/AppendableWriter.java",
    "content": "package cn.hutool.core.io;\n\nimport java.io.Closeable;\nimport java.io.Flushable;\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.nio.CharBuffer;\n\n/**\n * 同时继承{@link Writer}和实现{@link Appendable}的聚合类，用于适配两种接口操作\n * 实现来自：jodd\n *\n * @author looly，jodd\n * @since 5.7.0\n */\npublic class AppendableWriter extends Writer implements Appendable {\n\n\tprivate final Appendable appendable;\n\tprivate final boolean flushable;\n\tprivate boolean closed;\n\n\tpublic AppendableWriter(final Appendable appendable) {\n\t\tthis.appendable = appendable;\n\t\tthis.flushable = appendable instanceof Flushable;\n\t\tthis.closed = false;\n\t}\n\n\t@Override\n\tpublic void write(final char[] cbuf, final int off, final int len) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(CharBuffer.wrap(cbuf), off, off + len);\n\t}\n\n\t@Override\n\tpublic void write(final int c) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append((char) c);\n\t}\n\n\t@Override\n\tpublic Writer append(final char c) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(c);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Writer append(final CharSequence csq, final int start, final int end) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(csq, start, end);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Writer append(final CharSequence csq) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(csq);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void write(final String str, final int off, final int len) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(str, off, off + len);\n\t}\n\n\t@Override\n\tpublic void write(final String str) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(str);\n\t}\n\n\t@Override\n\tpublic void write(final char[] cbuf) throws IOException {\n\t\tcheckNotClosed();\n\t\tappendable.append(CharBuffer.wrap(cbuf));\n\t}\n\n\t@Override\n\tpublic void flush() throws IOException {\n\t\tcheckNotClosed();\n\t\tif (flushable) {\n\t\t\t((Flushable) appendable).flush();\n\t\t}\n\t}\n\n\t/**\n\t * 检查Writer是否已经被关闭\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void checkNotClosed() throws IOException {\n\t\tif (closed) {\n\t\t\tthrow new IOException(\"Writer is closed!\" + this);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tif (false == closed) {\n\t\t\tflush();\n\t\t\tif (appendable instanceof Closeable) {\n\t\t\t\t((Closeable) appendable).close();\n\t\t\t}\n\t\t\tclosed = true;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/BOMInputStream.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PushbackInputStream;\n\n/**\n * 读取带BOM头的流内容，{@code getCharset()}方法调用后会得到BOM头的编码，且会去除BOM头<br>\n * BOM定义：http://www.unicode.org/unicode/faq/utf_bom.html<br>\n * <ul>\n * <li>00 00 FE FF = UTF-32, big-endian</li>\n * <li>FF FE 00 00 = UTF-32, little-endian</li>\n * <li>EF BB BF = UTF-8</li>\n * <li>FE FF = UTF-16, big-endian</li>\n * <li>FF FE = UTF-16, little-endian</li>\n * </ul>\n * 使用： <br>\n * <code>\n * String enc = \"UTF-8\"; // or NULL to use systemdefault<br>\n * FileInputStream fis = new FileInputStream(file); <br>\n * BOMInputStream uin = new BOMInputStream(fis, enc); <br>\n * enc = uin.getCharset(); // check and skip possible BOM bytes\n * </code>\n * <br><br>\n * 参考： http://akini.mbnet.fi/java/unicodereader/UnicodeInputStream.java.txt\n *\n * @author looly\n */\npublic class BOMInputStream extends InputStream {\n\n\tprivate final PushbackInputStream in;\n\tprivate boolean isInited = false;\n\tprivate final String defaultCharset;\n\tprivate String charset;\n\n\tprivate static final int BOM_SIZE = 4;\n\n\t// ----------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t * @param in 流\n\t */\n\tpublic BOMInputStream(InputStream in) {\n\t\tthis(in, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param in 流\n\t * @param defaultCharset 默认编码\n\t */\n\tpublic BOMInputStream(InputStream in, String defaultCharset) {\n\t\tthis.in = new PushbackInputStream(in, BOM_SIZE);\n\t\tthis.defaultCharset = defaultCharset;\n\t}\n\t// ----------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获取默认编码\n\t *\n\t * @return 默认编码\n\t */\n\tpublic String getDefaultCharset() {\n\t\treturn defaultCharset;\n\t}\n\n\t/**\n\t * 获取BOM头中的编码\n\t *\n\t * @return 编码\n\t */\n\tpublic String getCharset() {\n\t\tif (false == isInited) {\n\t\t\ttry {\n\t\t\t\tinit();\n\t\t\t} catch (IOException ex) {\n\t\t\t\tthrow new IORuntimeException(ex);\n\t\t\t}\n\t\t}\n\t\treturn charset;\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tisInited = true;\n\t\tin.close();\n\t}\n\n\t@Override\n\tpublic int read() throws IOException {\n\t\tisInited = true;\n\t\treturn in.read();\n\t}\n\n\t/**\n\t * Read-ahead four bytes and check for BOM marks. <br>\n\t * Extra bytes are unread back to the stream, only BOM bytes are skipped.\n\t * @throws IOException 读取引起的异常\n\t */\n\tprotected void init() throws IOException {\n\t\tif (isInited) {\n\t\t\treturn;\n\t\t}\n\n\t\tbyte[] bom = new byte[BOM_SIZE];\n\t\tint n, unread;\n\t\tn = in.read(bom, 0, bom.length);\n\n\t\tif ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) {\n\t\t\tcharset = \"UTF-32BE\";\n\t\t\tunread = n - 4;\n\t\t} else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) {\n\t\t\tcharset = \"UTF-32LE\";\n\t\t\tunread = n - 4;\n\t\t} else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) {\n\t\t\tcharset = \"UTF-8\";\n\t\t\tunread = n - 3;\n\t\t} else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) {\n\t\t\tcharset = \"UTF-16BE\";\n\t\t\tunread = n - 2;\n\t\t} else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) {\n\t\t\tcharset = \"UTF-16LE\";\n\t\t\tunread = n - 2;\n\t\t} else {\n\t\t\t// Unicode BOM mark not found, unread all bytes\n\t\t\tcharset = defaultCharset;\n\t\t\tunread = n;\n\t\t}\n\n\t\tif (unread > 0) {\n\t\t\tin.unread(bom, (n - unread), unread);\n\t\t}\n\n\t\tisInited = true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/BomReader.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.io.UnsupportedEncodingException;\n\n/**\n * 读取带BOM头的流内容的Reader，如果非bom的流或无法识别的编码，则默认UTF-8<br>\n * BOM定义：http://www.unicode.org/unicode/faq/utf_bom.html\n *\n * <ul>\n * <li>00 00 FE FF = UTF-32, big-endian</li>\n * <li>FF FE 00 00 = UTF-32, little-endian</li>\n * <li>EF BB BF = UTF-8</li>\n * <li>FE FF = UTF-16, big-endian</li>\n * <li>FF FE = UTF-16, little-endian</li>\n * </ul>\n * 使用： <br>\n * <code>\n * FileInputStream fis = new FileInputStream(file); <br>\n * BomReader uin = new BomReader(fis); <br>\n * </code>\n *\n * @author looly\n * @since 5.7.14\n */\npublic class BomReader extends Reader {\n\n\tprivate InputStreamReader reader;\n\n\t/**\n\t * 构造\n\t *\n\t * @param in 流\n\t */\n\tpublic BomReader(InputStream in) {\n\t\tAssert.notNull(in, \"InputStream must be not null!\");\n\t\tfinal BOMInputStream bin = (in instanceof BOMInputStream) ? (BOMInputStream) in : new BOMInputStream(in);\n\t\ttry {\n\t\t\tthis.reader = new InputStreamReader(bin, bin.getCharset());\n\t\t} catch (UnsupportedEncodingException ignore) {\n\t\t}\n\t}\n\n\t@Override\n\tpublic int read(char[] cbuf, int off, int len) throws IOException {\n\t\treturn reader.read(cbuf, off, len);\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\treader.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.Charset;\nimport java.util.Arrays;\n\n/**\n * {@link ByteBuffer} 工具类<br>\n * 此工具来自于 t-io 项目以及其它项目的相关部分收集<br>\n * ByteBuffer的相关介绍见：https://www.cnblogs.com/ruber/p/6857159.html\n *\n * @author tanyaowu, looly\n * @since 4.0.0\n */\npublic class BufferUtil {\n\n\t/**\n\t * 拷贝到一个新的ByteBuffer\n\t *\n\t * @param src   源ByteBuffer\n\t * @param start 起始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的ByteBuffer\n\t */\n\tpublic static ByteBuffer copy(ByteBuffer src, int start, int end) {\n\t\treturn ByteBuffer.wrap(Arrays.copyOfRange(src.array(), start, end));\n\t}\n\n\t/**\n\t * 拷贝ByteBuffer\n\t *\n\t * @param src  源ByteBuffer\n\t * @param dest 目标ByteBuffer\n\t * @return 目标ByteBuffer\n\t */\n\tpublic static ByteBuffer copy(ByteBuffer src, ByteBuffer dest) {\n\t\treturn copy(src, dest, Math.min(src.limit(), dest.remaining()));\n\t}\n\n\t/**\n\t * 拷贝ByteBuffer\n\t *\n\t * @param src    源ByteBuffer\n\t * @param dest   目标ByteBuffer\n\t * @param length 长度\n\t * @return 目标ByteBuffer\n\t */\n\tpublic static ByteBuffer copy(ByteBuffer src, ByteBuffer dest, int length) {\n\t\treturn copy(src, src.position(), dest, dest.position(), length);\n\t}\n\n\t/**\n\t * 拷贝ByteBuffer\n\t *\n\t * @param src       源ByteBuffer\n\t * @param srcStart  源开始的位置\n\t * @param dest      目标ByteBuffer\n\t * @param destStart 目标开始的位置\n\t * @param length    长度\n\t * @return 目标ByteBuffer\n\t */\n\tpublic static ByteBuffer copy(ByteBuffer src, int srcStart, ByteBuffer dest, int destStart, int length) {\n\t\tSystem.arraycopy(src.array(), srcStart, dest.array(), destStart, length);\n\t\treturn dest;\n\t}\n\n\t/**\n\t * 读取剩余部分并转为UTF-8编码字符串\n\t *\n\t * @param buffer ByteBuffer\n\t * @return 字符串\n\t * @since 4.5.0\n\t */\n\tpublic static String readUtf8Str(ByteBuffer buffer) {\n\t\treturn readStr(buffer, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 读取剩余部分并转为字符串\n\t *\n\t * @param buffer  ByteBuffer\n\t * @param charset 编码\n\t * @return 字符串\n\t * @since 4.5.0\n\t */\n\tpublic static String readStr(ByteBuffer buffer, Charset charset) {\n\t\treturn StrUtil.str(readBytes(buffer), charset);\n\t}\n\n\t/**\n\t * 读取剩余部分bytes<br>\n\t *\n\t * @param buffer ByteBuffer\n\t * @return bytes\n\t */\n\tpublic static byte[] readBytes(ByteBuffer buffer) {\n\t\tfinal int remaining = buffer.remaining();\n\t\tbyte[] ab = new byte[remaining];\n\t\tbuffer.get(ab);\n\t\treturn ab;\n\t}\n\n\t/**\n\t * 读取指定长度的bytes<br>\n\t * 如果长度不足，则读取剩余部分，此时buffer必须为读模式\n\t *\n\t * @param buffer    ByteBuffer\n\t * @param maxLength 最大长度\n\t * @return bytes\n\t */\n\tpublic static byte[] readBytes(ByteBuffer buffer, int maxLength) {\n\t\tfinal int remaining = buffer.remaining();\n\t\tif (maxLength > remaining) {\n\t\t\tmaxLength = remaining;\n\t\t}\n\t\tbyte[] ab = new byte[maxLength];\n\t\tbuffer.get(ab);\n\t\treturn ab;\n\t}\n\n\t/**\n\t * 读取指定区间的数据\n\t *\n\t * @param buffer {@link ByteBuffer}\n\t * @param start  开始位置\n\t * @param end    结束位置\n\t * @return bytes\n\t */\n\tpublic static byte[] readBytes(ByteBuffer buffer, int start, int end) {\n\t\tbyte[] bs = new byte[end - start];\n\t\tSystem.arraycopy(buffer.array(), start, bs, 0, bs.length);\n\t\treturn bs;\n\t}\n\n\t/**\n\t * 一行的末尾位置，查找位置时位移ByteBuffer到结束位置\n\t *\n\t * @param buffer {@link ByteBuffer}\n\t * @return 末尾位置，未找到或达到最大长度返回-1\n\t */\n\tpublic static int lineEnd(ByteBuffer buffer) {\n\t\treturn lineEnd(buffer, buffer.remaining());\n\t}\n\n\t/**\n\t * 一行的末尾位置，查找位置时位移ByteBuffer到结束位置<br>\n\t * 支持的换行符如下：\n\t *\n\t * <pre>\n\t * 1. \\r\\n\n\t * 2. \\n\n\t * </pre>\n\t *\n\t * @param buffer    {@link ByteBuffer}\n\t * @param maxLength 读取最大长度\n\t * @return 末尾位置，未找到或达到最大长度返回-1\n\t */\n\tpublic static int lineEnd(ByteBuffer buffer, int maxLength) {\n\t\tint primitivePosition = buffer.position();\n\t\tboolean canEnd = false;\n\t\tint charIndex = primitivePosition;\n\t\tbyte b;\n\t\twhile (buffer.hasRemaining()) {\n\t\t\tb = buffer.get();\n\t\t\tcharIndex++;\n\t\t\tif (b == StrUtil.C_CR) {\n\t\t\t\tcanEnd = true;\n\t\t\t} else if (b == StrUtil.C_LF) {\n\t\t\t\treturn canEnd ? charIndex - 2 : charIndex - 1;\n\t\t\t} else {\n\t\t\t\t// 只有\\r无法确认换行\n\t\t\t\tcanEnd = false;\n\t\t\t}\n\n\t\t\tif (charIndex - primitivePosition > maxLength) {\n\t\t\t\t// 查找到尽头，未找到，还原位置\n\t\t\t\tbuffer.position(primitivePosition);\n\t\t\t\tthrow new IndexOutOfBoundsException(StrUtil.format(\"Position is out of maxLength: {}\", maxLength));\n\t\t\t}\n\t\t}\n\n\t\t// 查找到buffer尽头，未找到，还原位置\n\t\tbuffer.position(primitivePosition);\n\t\t// 读到结束位置\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 读取一行，如果buffer中最后一部分并非完整一行，则返回null<br>\n\t * 支持的换行符如下：\n\t *\n\t * <pre>\n\t * 1. \\r\\n\n\t * 2. \\n\n\t * </pre>\n\t *\n\t * @param buffer  ByteBuffer\n\t * @param charset 编码\n\t * @return 一行\n\t */\n\tpublic static String readLine(ByteBuffer buffer, Charset charset) {\n\t\tfinal int startPosition = buffer.position();\n\t\tfinal int endPosition = lineEnd(buffer);\n\n\t\tif (endPosition > startPosition) {\n\t\t\tbyte[] bs = readBytes(buffer, startPosition, endPosition);\n\t\t\treturn StrUtil.str(bs, charset);\n\t\t} else if (endPosition == startPosition) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 创建新Buffer\n\t *\n\t * @param data 数据\n\t * @return {@link ByteBuffer}\n\t * @since 4.5.0\n\t */\n\tpublic static ByteBuffer create(byte[] data) {\n\t\treturn ByteBuffer.wrap(data);\n\t}\n\n\t/**\n\t * 从字符串创建新Buffer\n\t *\n\t * @param data    数据\n\t * @param charset 编码\n\t * @return {@link ByteBuffer}\n\t * @since 4.5.0\n\t */\n\tpublic static ByteBuffer create(CharSequence data, Charset charset) {\n\t\treturn create(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 从字符串创建新Buffer，使用UTF-8编码\n\t *\n\t * @param data 数据\n\t * @return {@link ByteBuffer}\n\t * @since 4.5.0\n\t */\n\tpublic static ByteBuffer createUtf8(CharSequence data) {\n\t\treturn create(StrUtil.utf8Bytes(data));\n\t}\n\n\t/**\n\t * 创建{@link CharBuffer}\n\t *\n\t * @param capacity 容量\n\t * @return {@link CharBuffer}\n\t * @since 5.5.7\n\t */\n\tpublic static CharBuffer createCharBuffer(int capacity) {\n\t\treturn CharBuffer.allocate(capacity);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/CharsetDetector.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.CharacterCodingException;\nimport java.nio.charset.Charset;\nimport java.nio.charset.CharsetDecoder;\n\n/**\n * 编码探测器\n *\n * @author looly\n * @since 5.4.7\n */\npublic class CharsetDetector {\n\n\t/**\n\t * 默认的参与测试的编码\n\t */\n\tprivate static final Charset[] DEFAULT_CHARSETS;\n\n\tstatic {\n\t\tString[] names = {\n\t\t\t\t\"UTF-8\",\n\t\t\t\t\"GBK\",\n\t\t\t\t\"GB2312\",\n\t\t\t\t\"GB18030\",\n\t\t\t\t\"UTF-16BE\",\n\t\t\t\t\"UTF-16LE\",\n\t\t\t\t\"UTF-16\",\n\t\t\t\t\"BIG5\",\n\t\t\t\t\"UNICODE\",\n\t\t\t\t\"US-ASCII\"};\n\t\tDEFAULT_CHARSETS = Convert.convert(Charset[].class, names);\n\t}\n\n\t/**\n\t * 探测文件编码\n\t *\n\t * @param file     文件\n\t * @param charsets 需要测试用的编码，null或空使用默认的编码数组\n\t * @return 编码\n\t * @since 5.6.7\n\t */\n\tpublic static Charset detect(File file, Charset... charsets) {\n\t\treturn detect(FileUtil.getInputStream(file), charsets);\n\t}\n\n\t/**\n\t * 探测编码<br>\n\t * 注意：此方法会读取流的一部分，然后关闭流，如重复使用流，请使用支持reset方法的流\n\t *\n\t * @param in       流，使用后关闭此流\n\t * @param charsets 需要测试用的编码，null或空使用默认的编码数组\n\t * @return 编码\n\t */\n\tpublic static Charset detect(InputStream in, Charset... charsets) {\n\t\treturn detect(IoUtil.DEFAULT_LARGE_BUFFER_SIZE, in, charsets);\n\t}\n\n\t/**\n\t * 探测编码<br>\n\t * 注意：此方法会读取流的一部分，然后关闭流，如重复使用流，请使用支持reset方法的流\n\t *\n\t * @param bufferSize 自定义缓存大小，即每次检查的长度\n\t * @param in         流，使用后关闭此流\n\t * @param charsets   需要测试用的编码，null或空使用默认的编码数组\n\t * @return 编码\n\t * @since 5.7.10\n\t */\n\tpublic static Charset detect(int bufferSize, InputStream in, Charset... charsets) {\n\t\tif (ArrayUtil.isEmpty(charsets)) {\n\t\t\tcharsets = DEFAULT_CHARSETS;\n\t\t}\n\n\t\tfinal byte[] buffer = new byte[bufferSize];\n\t\ttry {\n\t\t\twhile (in.read(buffer) > -1) {\n\t\t\t\tfor (Charset charset : charsets) {\n\t\t\t\t\tfinal CharsetDecoder decoder = charset.newDecoder();\n\t\t\t\t\tif (identify(buffer, decoder)) {\n\t\t\t\t\t\treturn charset;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 通过try的方式测试指定bytes是否可以被解码，从而判断是否为指定编码\n\t *\n\t * @param bytes   测试的bytes\n\t * @param decoder 解码器\n\t * @return 是否是指定编码\n\t */\n\tprivate static boolean identify(byte[] bytes, CharsetDecoder decoder) {\n\t\ttry {\n\t\t\tdecoder.decode(ByteBuffer.wrap(bytes));\n\t\t} catch (CharacterCodingException e) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 基于快速缓冲FastByteBuffer的OutputStream，随着数据的增长自动扩充缓冲区\n * <p>\n * 可以通过{@link #toByteArray()}和 {@link #toString()}来获取数据\n * <p>\n * {@link #close()}方法无任何效果，当流被关闭后不会抛出IOException\n * <p>\n * 这种设计避免重新分配内存块而是分配新增的缓冲区，缓冲区不会被GC，数据也不会被拷贝到其他缓冲区。\n *\n * @author biezhi\n */\npublic class FastByteArrayOutputStream extends OutputStream {\n\n\tprivate final FastByteBuffer buffer;\n\n\t/**\n\t * 构造\n\t */\n\tpublic FastByteArrayOutputStream() {\n\t\tthis(1024);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param size 预估大小\n\t */\n\tpublic FastByteArrayOutputStream(int size) {\n\t\tbuffer = new FastByteBuffer(size);\n\t}\n\n\t@Override\n\tpublic void write(byte[] b, int off, int len) {\n\t\tbuffer.append(b, off, len);\n\t}\n\n\t@Override\n\tpublic void write(int b) {\n\t\tbuffer.append((byte) b);\n\t}\n\n\tpublic int size() {\n\t\treturn buffer.size();\n\t}\n\n\t/**\n\t * 此方法无任何效果，当流被关闭后不会抛出IOException\n\t */\n\t@Override\n\tpublic void close() {\n\t\t// nop\n\t}\n\n\tpublic void reset() {\n\t\tbuffer.reset();\n\t}\n\n\t/**\n\t * 写出\n\t * @param out 输出流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void writeTo(OutputStream out) throws IORuntimeException {\n\t\tfinal int index = buffer.index();\n\t\tif(index < 0){\n\t\t\t// 无数据写出\n\t\t\treturn;\n\t\t}\n\t\tbyte[] buf;\n\t\ttry {\n\t\t\tfor (int i = 0; i < index; i++) {\n\t\t\t\tbuf = buffer.array(i);\n\t\t\t\tout.write(buf);\n\t\t\t}\n\t\t\tout.write(buffer.array(index), 0, buffer.offset());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\n\t/**\n\t * 转为Byte数组\n\t * @return Byte数组\n\t */\n\tpublic byte[] toByteArray() {\n\t\treturn buffer.toArray();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn toString(CharsetUtil.defaultCharset());\n\t}\n\n\t/**\n\t * 转为字符串\n\t * @param charsetName 编码\n\t * @return 字符串\n\t */\n\tpublic String toString(String charsetName) {\n\t\treturn toString(CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 转为字符串\n\t * @param charset 编码,null表示默认编码\n\t * @return 字符串\n\t */\n\tpublic String toString(Charset charset) {\n\t\treturn new String(toByteArray(),\n\t\t\t\tObjectUtil.defaultIfNull(charset, () -> CharsetUtil.defaultCharset()));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FastByteBuffer.java",
    "content": "package cn.hutool.core.io;\n\n/**\n * 代码移植自<a href=\"https://github.com/biezhi/blade\">blade</a><br>\n * 快速缓冲，将数据存放在缓冲集中，取代以往的单一数组\n *\n * @author biezhi, looly\n * @since 1.0\n */\npublic class FastByteBuffer {\n\n\t/**\n\t * 缓冲集\n\t */\n\tprivate byte[][] buffers = new byte[16][];\n\t/**\n\t * 缓冲数\n\t */\n\tprivate int buffersCount;\n\t/**\n\t * 当前缓冲索引\n\t */\n\tprivate int currentBufferIndex = -1;\n\t/**\n\t * 当前缓冲\n\t */\n\tprivate byte[] currentBuffer;\n\t/**\n\t * 当前缓冲偏移量\n\t */\n\tprivate int offset;\n\t/**\n\t * 缓冲字节数\n\t */\n\tprivate int size;\n\n\t/**\n\t * 一个缓冲区的最小字节数\n\t */\n\tprivate final int minChunkLen;\n\n\tpublic FastByteBuffer() {\n\t\tthis(1024);\n\t}\n\n\tpublic FastByteBuffer(int size) {\n\t\tif(size <= 0){\n\t\t\tsize = 1024;\n\t\t}\n\t\tthis.minChunkLen = Math.abs(size);\n\t}\n\n\t/**\n\t * 分配下一个缓冲区，不会小于1024\n\t *\n\t * @param newSize 理想缓冲区字节数\n\t */\n\tprivate void needNewBuffer(int newSize) {\n\t\tint delta = newSize - size;\n\t\tint newBufferSize = Math.max(minChunkLen, delta);\n\n\t\tcurrentBufferIndex++;\n\t\tcurrentBuffer = new byte[newBufferSize];\n\t\toffset = 0;\n\n\t\t// add buffer\n\t\tif (currentBufferIndex >= buffers.length) {\n\t\t\tint newLen = buffers.length << 1;\n\t\t\tbyte[][] newBuffers = new byte[newLen][];\n\t\t\tSystem.arraycopy(buffers, 0, newBuffers, 0, buffers.length);\n\t\t\tbuffers = newBuffers;\n\t\t}\n\t\tbuffers[currentBufferIndex] = currentBuffer;\n\t\tbuffersCount++;\n\t}\n\n\t/**\n\t * 向快速缓冲加入数据\n\t *\n\t * @param array 数据\n\t * @param off 偏移量\n\t * @param len 字节数\n\t * @return 快速缓冲自身 @see FastByteBuffer\n\t */\n\tpublic FastByteBuffer append(byte[] array, int off, int len) {\n\t\tint end = off + len;\n\t\tif ((off < 0) || (len < 0) || (end > array.length)) {\n\t\t\tthrow new IndexOutOfBoundsException();\n\t\t}\n\t\tif (len == 0) {\n\t\t\treturn this;\n\t\t}\n\t\tint newSize = size + len;\n\t\tint remaining = len;\n\n\t\tif (currentBuffer != null) {\n\t\t\t// first try to fill current buffer\n\t\t\tint part = Math.min(remaining, currentBuffer.length - offset);\n\t\t\tSystem.arraycopy(array, end - remaining, currentBuffer, offset, part);\n\t\t\tremaining -= part;\n\t\t\toffset += part;\n\t\t\tsize += part;\n\t\t}\n\n\t\tif (remaining > 0) {\n\t\t\t// still some data left\n\t\t\t// ask for new buffer\n\t\t\tneedNewBuffer(newSize);\n\n\t\t\t// then copy remaining\n\t\t\t// but this time we are sure that it will fit\n\t\t\tint part = Math.min(remaining, currentBuffer.length - offset);\n\t\t\tSystem.arraycopy(array, end - remaining, currentBuffer, offset, part);\n\t\t\toffset += part;\n\t\t\tsize += part;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 向快速缓冲加入数据\n\t *\n\t * @param array 数据\n\t *\n\t * @return 快速缓冲自身 @see FastByteBuffer\n\t */\n\tpublic FastByteBuffer append(byte[] array) {\n\t\treturn append(array, 0, array.length);\n\t}\n\n\t/**\n\t * 向快速缓冲加入一个字节\n\t *\n\t * @param element 一个字节的数据\n\t * @return 快速缓冲自身 @see FastByteBuffer\n\t */\n\tpublic FastByteBuffer append(byte element) {\n\t\tif ((currentBuffer == null) || (offset == currentBuffer.length)) {\n\t\t\tneedNewBuffer(size + 1);\n\t\t}\n\n\t\tcurrentBuffer[offset] = element;\n\t\toffset++;\n\t\tsize++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将另一个快速缓冲加入到自身\n\t *\n\t * @param buff 快速缓冲\n\t * @return 快速缓冲自身 @see FastByteBuffer\n\t */\n\tpublic FastByteBuffer append(FastByteBuffer buff) {\n\t\tif (buff.size == 0) {\n\t\t\treturn this;\n\t\t}\n\t\tfor (int i = 0; i < buff.currentBufferIndex; i++) {\n\t\t\tappend(buff.buffers[i]);\n\t\t}\n\t\tappend(buff.currentBuffer, 0, buff.offset);\n\t\treturn this;\n\t}\n\n\tpublic int size() {\n\t\treturn size;\n\t}\n\n\tpublic boolean isEmpty() {\n\t\treturn size == 0;\n\t}\n\n\t/**\n\t * 当前缓冲位于缓冲区的索引位\n\t *\n\t * @return {@link #currentBufferIndex}\n\t */\n\tpublic int index() {\n\t\treturn currentBufferIndex;\n\t}\n\n\tpublic int offset() {\n\t\treturn offset;\n\t}\n\n\t/**\n\t * 根据索引位返回缓冲集中的缓冲\n\t *\n\t * @param index 索引位\n\t * @return 缓冲\n\t */\n\tpublic byte[] array(int index) {\n\t\treturn buffers[index];\n\t}\n\n\tpublic void reset() {\n\t\tsize = 0;\n\t\toffset = 0;\n\t\tcurrentBufferIndex = -1;\n\t\tcurrentBuffer = null;\n\t\tbuffersCount = 0;\n\t}\n\n\t/**\n\t * 返回快速缓冲中的数据\n\t *\n\t * @return 快速缓冲中的数据\n\t */\n\tpublic byte[] toArray() {\n\t\tint pos = 0;\n\t\tbyte[] array = new byte[size];\n\n\t\tif (currentBufferIndex == -1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = 0; i < currentBufferIndex; i++) {\n\t\t\tint len = buffers[i].length;\n\t\t\tSystem.arraycopy(buffers[i], 0, array, pos, len);\n\t\t\tpos += len;\n\t\t}\n\n\t\tSystem.arraycopy(buffers[currentBufferIndex], 0, array, pos, offset);\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 返回快速缓冲中的数据\n\t *\n\t * @param start 逻辑起始位置\n\t * @param len 逻辑字节长\n\t * @return 快速缓冲中的数据\n\t */\n\tpublic byte[] toArray(int start, int len) {\n\t\tint remaining = len;\n\t\tint pos = 0;\n\t\tbyte[] array = new byte[len];\n\n\t\tif (len == 0) {\n\t\t\treturn array;\n\t\t}\n\n\t\tint i = 0;\n\t\twhile (start >= buffers[i].length) {\n\t\t\tstart -= buffers[i].length;\n\t\t\ti++;\n\t\t}\n\n\t\twhile (i < buffersCount) {\n\t\t\tbyte[] buf = buffers[i];\n\t\t\tint c = Math.min(buf.length - start, remaining);\n\t\t\tSystem.arraycopy(buf, start, array, pos, c);\n\t\t\tpos += c;\n\t\t\tremaining -= c;\n\t\t\tif (remaining == 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tstart = 0;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 根据索引位返回一个字节\n\t *\n\t * @param index 索引位\n\t * @return 一个字节\n\t */\n\tpublic byte get(int index) {\n\t\tif ((index >= size) || (index < 0)) {\n\t\t\tthrow new IndexOutOfBoundsException();\n\t\t}\n\t\tint ndx = 0;\n\t\twhile (true) {\n\t\t\tbyte[] b = buffers[ndx];\n\t\t\tif (index < b.length) {\n\t\t\t\treturn b[index];\n\t\t\t}\n\t\t\tndx++;\n\t\t\tindex -= b.length;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FastStringWriter.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.text.StrBuilder;\n\nimport java.io.Writer;\n\n/**\n * 借助{@link StrBuilder} 提供快读的字符串写出，相比jdk的StringWriter非线程安全，速度更快。\n *\n * @author looly\n * @since 5.3.3\n */\npublic final class FastStringWriter extends Writer {\n\n\tprivate final StrBuilder builder;\n\n\t/**\n\t * 构造\n\t */\n\tpublic FastStringWriter() {\n\t\tthis(StrBuilder.DEFAULT_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialSize 初始容量\n\t */\n\tpublic FastStringWriter(int initialSize) {\n\t\tif (initialSize < 0) {\n\t\t\tinitialSize = StrBuilder.DEFAULT_CAPACITY;\n\t\t}\n\t\tthis.builder = new StrBuilder(initialSize);\n\t}\n\n\n\t@Override\n\tpublic void write(final int c) {\n\t\tthis.builder.append((char) c);\n\t}\n\n\n\t@Override\n\tpublic void write(final String str) {\n\t\tthis.builder.append(str);\n\t}\n\n\n\t@Override\n\tpublic void write(final String str, final int off, final int len) {\n\t\tthis.builder.append(str, off, off + len);\n\t}\n\n\n\t@Override\n\tpublic void write(final char[] cbuf) {\n\t\tthis.builder.append(cbuf, 0, cbuf.length);\n\t}\n\n\n\t@Override\n\tpublic void write(final char[] cbuf, final int off, final int len) {\n\t\tif ((off < 0) || (off > cbuf.length) || (len < 0) ||\n\t\t\t\t((off + len) > cbuf.length) || ((off + len) < 0)) {\n\t\t\tthrow new IndexOutOfBoundsException();\n\t\t} else if (len == 0) {\n\t\t\treturn;\n\t\t}\n\t\tthis.builder.append(cbuf, off, len);\n\t}\n\n\n\t@Override\n\tpublic void flush() {\n\t\t// Nothing to be flushed\n\t}\n\n\n\t@Override\n\tpublic void close() {\n\t\t// Nothing to be closed\n\t}\n\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FileMagicNumber.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjUtil;\n\nimport java.math.BigInteger;\nimport java.util.Arrays;\nimport java.util.Objects;\n\n/**\n * 文件类型魔数封装\n *\n * @author CherryRum\n * @since 5.8.12\n */\npublic enum FileMagicNumber {\n\tUNKNOWN(null, null) {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn false;\n\t\t}\n\t},\n\t//image start---------------------------------------------\n\tJPEG(\"image/jpeg\", \"jpg\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0xff)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0xd8)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xff);\n\t\t}\n\t},\n\tJXR(\"image/vnd.ms-photo\", \"jxr\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\t//file magic number https://www.iana.org/assignments/media-types/image/jxr\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xbc);\n\t\t}\n\t},\n\tAPNG(\"image/apng\", \"apng\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal boolean b = bytes.length > 8\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x89)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x50)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4e)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x47)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x0d)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x0a)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x1a)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x0a);\n\n\t\t\tif (b) {\n\t\t\t\tint i = 8;\n\t\t\t\twhile (i < bytes.length) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfinal int dataLength = new BigInteger(1, Arrays.copyOfRange(bytes, i, i + 4)).intValue();\n\t\t\t\t\t\ti += 4;\n\t\t\t\t\t\tfinal byte[] bytes1 = Arrays.copyOfRange(bytes, i, i + 4);\n\t\t\t\t\t\tfinal String chunkType = new String(bytes1);\n\t\t\t\t\t\ti += 4;\n\t\t\t\t\t\tif (Objects.equals(chunkType, \"IDAT\") || Objects.equals(chunkType, \"IEND\")) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t} else if (Objects.equals(chunkType, \"acTL\")) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti += dataLength + 4;\n\t\t\t\t\t} catch (final Exception e) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\tPNG(\"image/png\", \"png\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x89)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x50)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4e)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x47);\n\t\t}\n\t},\n\tGIF(\"image/gif\", \"gif\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x47)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x46);\n\t\t}\n\t},\n\tBMP(\"image/bmp\", \"bmp\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 1\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x42)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4d);\n\t\t}\n\t},\n\tTIFF(\"image/tiff\", \"tiff\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 4) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x2a)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x00);\n\t\t\tfinal boolean flag2 = (Objects.equals(bytes[0], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x2a));\n\t\t\treturn flag1 || flag2;\n\n\t\t}\n\t},\n\n\tDWG(\"image/vnd.dwg\", \"dwg\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 10\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x43)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x31)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x30);\n\t\t}\n\t},\n\n\tWEBP(\"image/webp\", \"webp\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 11\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x57)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x45)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x42)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x50);\n\t\t}\n\t},\n\tPSD(\"image/vnd.adobe.photoshop\", \"psd\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x38)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x42)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x50)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x53);\n\t\t}\n\t},\n\tICO(\"image/x-icon\", \"ico\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x01)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x00);\n\t\t}\n\t},\n\tXCF(\"image/x-xcf\", \"xcf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 9\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x67)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x6d)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x20)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x78)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x63)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x20)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x76);\n\t\t}\n\t},\n\t//image end-----------------------------------------------\n\n\t//audio start---------------------------------------------\n\n\tWAV(\"audio/x-wav\", \"wav\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 11\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x57)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x56)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x45);\n\t\t}\n\t},\n\tMIDI(\"audio/midi\", \"midi\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x68)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x64);\n\t\t}\n\t},\n\tMP3(\"audio/mpeg\", \"mp3\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 2) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x49) && Objects.equals(bytes[1], (byte) 0x44) && Objects.equals(bytes[2], (byte) 0x33);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xFB);\n\t\t\tfinal boolean flag3 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF3);\n\t\t\tfinal boolean flag4 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF2);\n\t\t\treturn flag1 || flag2 || flag3 || flag4;\n\t\t}\n\t},\n\tOGG(\"audio/ogg\", \"ogg\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x67)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x67)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x53);\n\t\t}\n\t},\n\tFLAC(\"audio/x-flac\", \"flac\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x43);\n\t\t}\n\t},\n\tM4A(\"audio/mp4\", \"m4a\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn (bytes.length > 10\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x34)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x41))\n\t\t\t\t\t|| (Objects.equals(bytes[0], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x34)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x20));\n\t\t}\n\t},\n\tAAC(\"audio/aac\", \"aac\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF1);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[0], (byte) 0xFF) && Objects.equals(bytes[1], (byte) 0xF9);\n\t\t\treturn flag1 || flag2;\n\t\t}\n\t},\n\tAMR(\"audio/amr\", \"amr\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\t//single-channel\n\t\t\tif (bytes.length < 11) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x23)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x21)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x0A);\n\t\t\t//multi-channel:\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[0], (byte) 0x23)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x21)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x5F)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x43)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x31)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x2e)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x30)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x0a);\n\t\t\treturn flag1 || flag2;\n\t\t}\n\t},\n\tAC3(\"audio/ac3\", \"ac3\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x0b)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x77);\n\t\t}\n\t},\n\tAIFF(\"audio/x-aiff\", \"aiff\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 11\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x46);\n\t\t}\n\t},\n\t//audio end-----------------------------------------------\n\n\t//font start---------------------------------------------\n\t// The existing registration \"application/font-woff\" is deprecated in favor of \"font/woff\".\n\tWOFF(\"font/woff\", \"woff\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 8) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x77)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[4], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x01)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x00);\n\t\t\tfinal boolean flag3 = Objects.equals(bytes[4], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x4f);\n\t\t\tfinal boolean flag4 = Objects.equals(bytes[4], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x75)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x65);\n\t\t\treturn flag1 && (flag2 || flag3 || flag4);\n\t\t}\n\t},\n\tWOFF2(\"font/woff2\", \"woff2\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 8) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x77)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x32);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[4], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x01)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x00);\n\t\t\tfinal boolean flag3 = Objects.equals(bytes[4], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x4f);\n\t\t\tfinal boolean flag4 = Objects.equals(bytes[4], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x75)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x65);\n\t\t\treturn flag1 && (flag2 || flag3 || flag4);\n\t\t}\n\t},\n\tTTF(\"font/ttf\", \"ttf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x01)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x00);\n\t\t}\n\t},\n\tOTF(\"font/otf\", \"otf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x54)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x00);\n\t\t}\n\t},\n\n\t//font end-----------------------------------------------\n\n\t//archive start-----------------------------------------\n\tEPUB(\"application/epub+zip\", \"epub\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 58\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x50) && Objects.equals(bytes[1], (byte) 0x4b)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x03) && Objects.equals(bytes[3], (byte) 0x04)\n\t\t\t\t\t&& Objects.equals(bytes[30], (byte) 0x6d) && Objects.equals(bytes[31], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[32], (byte) 0x6d) && Objects.equals(bytes[33], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[34], (byte) 0x74) && Objects.equals(bytes[35], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[36], (byte) 0x70) && Objects.equals(bytes[37], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[38], (byte) 0x61) && Objects.equals(bytes[39], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[40], (byte) 0x70) && Objects.equals(bytes[41], (byte) 0x6c)\n\t\t\t\t\t&& Objects.equals(bytes[42], (byte) 0x69) && Objects.equals(bytes[43], (byte) 0x63)\n\t\t\t\t\t&& Objects.equals(bytes[44], (byte) 0x61) && Objects.equals(bytes[45], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[46], (byte) 0x69) && Objects.equals(bytes[47], (byte) 0x6f)\n\t\t\t\t\t&& Objects.equals(bytes[48], (byte) 0x6e) && Objects.equals(bytes[49], (byte) 0x2f)\n\t\t\t\t\t&& Objects.equals(bytes[50], (byte) 0x65) && Objects.equals(bytes[51], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[52], (byte) 0x75) && Objects.equals(bytes[53], (byte) 0x62)\n\t\t\t\t\t&& Objects.equals(bytes[54], (byte) 0x2b) && Objects.equals(bytes[55], (byte) 0x7a)\n\t\t\t\t\t&& Objects.equals(bytes[56], (byte) 0x69) && Objects.equals(bytes[57], (byte) 0x70);\n\t\t}\n\t},\n\tZIP(\"application/zip\", \"zip\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 4) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x50) && Objects.equals(bytes[1], (byte) 0x4b);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[2], (byte) 0x03) || Objects.equals(bytes[2], (byte) 0x05) || Objects.equals(bytes[2], (byte) 0x07);\n\t\t\tfinal boolean flag3 = Objects.equals(bytes[3], (byte) 0x04) || Objects.equals(bytes[3], (byte) 0x06) || Objects.equals(bytes[3], (byte) 0x08);\n\t\t\treturn flag1 && flag2 && flag3;\n\t\t}\n\t},\n\tTAR(\"application/x-tar\", \"tar\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 261\n\t\t\t\t\t&& Objects.equals(bytes[257], (byte) 0x75)\n\t\t\t\t\t&& Objects.equals(bytes[258], (byte) 0x73)\n\t\t\t\t\t&& Objects.equals(bytes[259], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[260], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[261], (byte) 0x72);\n\t\t}\n\t},\n\tRAR(\"application/x-rar-compressed\", \"rar\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 6\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x21)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x1a)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x07)\n\t\t\t\t\t&& (Objects.equals(bytes[6], (byte) 0x00) || Objects.equals(bytes[6], (byte) 0x01));\n\t\t}\n\t},\n\tGZ(\"application/gzip\", \"gz\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x1f)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x8b)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x08);\n\t\t}\n\t},\n\tBZ2(\"application/x-bzip2\", \"bz2\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x42)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x5a)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x68);\n\t\t}\n\t},\n\tSevenZ(\"application/x-7z-compressed\", \"7z\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 6\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x37)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x7a)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xbc)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0xaf)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x27)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x1c)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x00);\n\t\t}\n\t},\n\tPDF(\"application/pdf\", \"pdf\") {\n\t\t@Override\n\t\tpublic boolean match(byte[] bytes) {\n\t\t\t//去除bom头并且跳过三个字节\n\t\t\tif (bytes.length > 3 && Objects.equals(bytes[0], (byte) 0xEF)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0xBB) && Objects.equals(bytes[2], (byte) 0xBF)) {\n\t\t\t\tbytes = Arrays.copyOfRange(bytes, 3, bytes.length);\n\t\t\t}\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x25)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x50)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x44)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46);\n\t\t}\n\t},\n\tEXE(\"application/x-msdownload\", \"exe\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 1\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x5a);\n\t\t}\n\t},\n\tSWF(\"application/x-shockwave-flash\", \"swf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 2\n\t\t\t\t\t&& (Objects.equals(bytes[0], 0x43) || Objects.equals(bytes[0], (byte) 0x46))\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x57)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x53);\n\t\t}\n\t},\n\tRTF(\"application/rtf\", \"rtf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x7b)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x5c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x66);\n\t\t}\n\t},\n\tNES(\"application/x-nintendo-nes-rom\", \"nes\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4e)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x45)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x53)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x1a);\n\t\t}\n\t},\n\tCRX(\"application/x-google-chrome-extension\", \"crx\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x43)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x32)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x34);\n\t\t}\n\t},\n\tCAB(\"application/vnd.ms-cab-compressed\", \"cab\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 4) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[0], (byte) 0x4d) && Objects.equals(bytes[1], (byte) 0x53)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x43) && Objects.equals(bytes[3], (byte) 0x46);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[0], (byte) 0x49) && Objects.equals(bytes[1], (byte) 0x53)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x63) && Objects.equals(bytes[3], (byte) 0x28);\n\t\t\treturn flag1 || flag2;\n\t\t}\n\t},\n\tPS(\"application/postscript\", \"ps\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 1\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x25)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x21);\n\t\t}\n\t},\n\tXZ(\"application/x-xz\", \"xz\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 5\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0xFD)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x37)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x7A)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x58)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x5A)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x00);\n\t\t}\n\t},\n\tSQLITE(\"application/x-sqlite3\", \"sqlite\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 15\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x53) && Objects.equals(bytes[1], (byte) 0x51)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4c) && Objects.equals(bytes[3], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x74) && Objects.equals(bytes[5], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x20) && Objects.equals(bytes[7], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x6f) && Objects.equals(bytes[9], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x6d) && Objects.equals(bytes[11], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[12], (byte) 0x74) && Objects.equals(bytes[13], (byte) 0x20)\n\t\t\t\t\t&& Objects.equals(bytes[14], (byte) 0x33) && Objects.equals(bytes[15], (byte) 0x00);\n\t\t}\n\t},\n\tDEB(\"application/x-deb\", \"deb\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 20\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x21) && Objects.equals(bytes[1], (byte) 0x3c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x61) && Objects.equals(bytes[3], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x63) && Objects.equals(bytes[5], (byte) 0x68)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x3e) && Objects.equals(bytes[7], (byte) 0x0a)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x64) && Objects.equals(bytes[9], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x62) && Objects.equals(bytes[11], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[12], (byte) 0x61) && Objects.equals(bytes[13], (byte) 0x6e)\n\t\t\t\t\t&& Objects.equals(bytes[14], (byte) 0x2d) && Objects.equals(bytes[15], (byte) 0x62)\n\t\t\t\t\t&& Objects.equals(bytes[16], (byte) 0x69) && Objects.equals(bytes[17], (byte) 0x6e)\n\t\t\t\t\t&& Objects.equals(bytes[18], (byte) 0x61) && Objects.equals(bytes[19], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[20], (byte) 0x79);\n\t\t}\n\t},\n\tAR(\"application/x-unix-archive\", \"ar\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 6\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x21)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x3c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x63)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x68)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x3e);\n\t\t}\n\t},\n\tLZOP(\"application/x-lzop\", \"lzo\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 7\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x89)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x5a)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x4f)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x0d)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x0a)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x1a);\n\t\t}\n\t},\n\tLZ(\"application/x-lzip\", \"lz\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x4c)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x5a)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x50);\n\t\t}\n\t},\n\tELF(\"application/x-executable\", \"elf\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 52\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x7f)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x45)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4c)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46);\n\t\t}\n\t},\n\tLZ4(\"application/x-lz4\", \"lz4\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x04)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x22)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x18);\n\t\t}\n\t},\n\t//https://github.com/madler/brotli/blob/master/br-format-v3.txt,brotli 没有固定的file magic number,所以此处只是参考\n\tBR(\"application/x-brotli\", \"br\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0xce)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0xb2)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xcf)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x81);\n\t\t}\n\t},\n\tDCM(\"application/x-dicom\", \"dcm\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 128\n\t\t\t\t\t&& Objects.equals(bytes[128], (byte) 0x44)\n\t\t\t\t\t&& Objects.equals(bytes[129], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[130], (byte) 0x43)\n\t\t\t\t\t&& Objects.equals(bytes[131], (byte) 0x4d);\n\t\t}\n\t},\n\tRPM(\"application/x-rpm\", \"rpm\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0xed)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0xab)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xee)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0xdb);\n\t\t}\n\t},\n\tZSTD(\"application/x-zstd\", \"zst\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal int length = bytes.length;\n\t\t\tif (length < 5) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal byte[] buf1 = new byte[]{(byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28};\n\t\t\tfinal boolean flag1 = ArrayUtil.contains(buf1, bytes[0])\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0xb5)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x2f)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0xfd);\n\t\t\tif (flag1) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif ((bytes[0] & 0xF0) == 0x50) {\n\t\t\t\treturn bytes[1] == 0x2A && bytes[2] == 0x4D && bytes[3] == 0x18;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\t//archive end------------------------------------------------------------\n\t//video start------------------------------------------------------------\n\tMP4(\"video/mp4\", \"mp4\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 13) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x53)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x4e)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x56);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x73)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x6f)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x6d);\n\t\t\treturn flag1 || flag2;\n\t\t}\n\t},\n\tAVI(\"video/x-msvideo\", \"avi\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 11\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x41)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x56)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x49)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x20);\n\t\t}\n\t},\n\tWMV(\"video/x-ms-wmv\", \"wmv\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 9\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x30)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x26)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xb2)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x75)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x8e)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0xcf)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x11)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0xa6)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0xd9);\n\t\t}\n\t},\n\tM4V(\"video/x-m4v\", \"m4v\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 12) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x4d)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x34)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x56)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x20);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x6d)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x34)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x32);\n\t\t\treturn flag1 || flag2;\n\t\t}\n\t},\n\tFLV(\"video/x-flv\", \"flv\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x46)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x4c)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x56)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x01);\n\t\t}\n\t},\n\tMKV(\"video/x-matroska\", \"mkv\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\t//0x42 0x82 0x88 0x6d 0x61 0x74 0x72 0x6f 0x73 0x6b 0x61\n\t\t\tfinal boolean flag1 = bytes.length > 11\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x1a)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x45)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xdf)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0xa3);\n\n\t\t\tif (flag1) {\n\t\t\t\t//此处需要判断是否是'\\x42\\x82\\x88matroska'，算法类似kmp判断\n\t\t\t\tfinal byte[] bytes1 = {(byte) 0x42, (byte) 0x82, (byte) 0x88, (byte) 0x6d, (byte) 0x61, (byte) 0x74, (byte) 0x72, (byte) 0x6f, (byte) 0x73, (byte) 0x6b, (byte) 0x61};\n\t\t\t\tfinal int index = FileMagicNumber.indexOf(bytes, bytes1);\n\t\t\t\treturn index > 0;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\n\tWEBM(\"video/webm\", \"webm\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal boolean flag1 = bytes.length > 8\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x1a)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x45)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0xdf)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0xa3);\n\t\t\tif (flag1) {\n\t\t\t\t//此处需要判断是否是'\\x42\\x82\\x88webm'，算法类似kmp判断\n\t\t\t\tfinal byte[] bytes1 = {(byte) 0x42, (byte) 0x82, (byte) 0x88, (byte) 0x77, (byte) 0x65, (byte) 0x62, (byte) 0x6d};\n\t\t\t\tfinal int index = FileMagicNumber.indexOf(bytes, bytes1);\n\t\t\t\treturn index > 0;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\t//此文件签名非常复杂，只判断常见的几种\n\tMOV(\"video/quicktime\", \"mov\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 12) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal boolean flag1 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x71)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x20)\n\t\t\t\t\t&& Objects.equals(bytes[11], (byte) 0x20);\n\t\t\tfinal boolean flag2 = Objects.equals(bytes[4], (byte) 0x6D)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x6F)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x6F)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x76);\n\t\t\tfinal boolean flag3 = Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x72)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x65);\n\t\t\tfinal boolean flag4 = Objects.equals(bytes[4], (byte) 0x6D)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x64)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x74);\n\t\t\tfinal boolean flag5 = Objects.equals(bytes[4], (byte) 0x77)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x64)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x65);\n\t\t\tfinal boolean flag6 = Objects.equals(bytes[4], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x6E)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x6F)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x74);\n\t\t\tfinal boolean flag7 = Objects.equals(bytes[4], (byte) 0x73)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x6B)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x69)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70);\n\t\t\treturn flag1 || flag2 || flag3 || flag4 || flag5 || flag6 || flag7;\n\t\t}\n\t},\n\tMPEG(\"video/mpeg\", \"mpg\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 3\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x01)\n\t\t\t\t\t&& (bytes[3] >= (byte) 0xb0 && bytes[3] <= (byte) 0xbf);\n\t\t}\n\t},\n\tRMVB(\"video/vnd.rn-realvideo\", \"rmvb\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 4\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x2E)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x52)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x4D)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x46);\n\t\t}\n\t},\n\tM3GP(\"video/3gpp\", \"3gp\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 10\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x66)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x74)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x70)\n\t\t\t\t\t&& Objects.equals(bytes[8], (byte) 0x33)\n\t\t\t\t\t&& Objects.equals(bytes[9], (byte) 0x67)\n\t\t\t\t\t&& Objects.equals(bytes[10], (byte) 0x70);\n\t\t}\n\t},\n\t//video end ---------------------------------------------------------------\n\t//document start ----------------------------------------------------------\n\tDOC(\"application/msword\", \"doc\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1};\n\t\t\tfinal boolean flag1 = bytes.length > 515 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1);\n\t\t\tif (flag1) {\n\t\t\t\tfinal byte[] byte2 = new byte[]{(byte) 0xec, (byte) 0xa5, (byte) 0xc1, (byte) 0x00};\n\t\t\t\tfinal boolean flag2 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 516), byte2);\n\t\t\t\tfinal byte[] byte3 = new byte[]{(byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x4d, (byte) 0x53, (byte) 0x57, (byte) 0x6f, (byte) 0x72, (byte) 0x64\n\t\t\t\t\t\t, (byte) 0x44, (byte) 0x6f, (byte) 0x63, (byte) 0x00, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x57, (byte) 0x6f, (byte) 0x72, (byte) 0x64,\n\t\t\t\t\t\t(byte) 0x2e, (byte) 0x44, (byte) 0x6f, (byte) 0x63, (byte) 0x75, (byte) 0x6d, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x2e, (byte) 0x38, (byte) 0x00,\n\t\t\t\t\t\t(byte) 0xf4, (byte) 0x39, (byte) 0xb2, (byte) 0x71};\n\t\t\t\tfinal byte[] range = Arrays.copyOfRange(bytes, 2075, 2142);\n\t\t\t\tfinal boolean flag3 = bytes.length > 2142 && FileMagicNumber.indexOf(range, byte3) > 0;\n\t\t\t\treturn flag2 || flag3;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\n\tXLS(\"application/vnd.ms-excel\", \"xls\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1};\n\t\t\tfinal boolean flag1 = bytes.length > 520 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1);\n\t\t\tif (flag1) {\n\t\t\t\tfinal byte[] byte2 = new byte[]{(byte) 0xfd, (byte) 0xff, (byte) 0xff, (byte) 0xff};\n\t\t\t\tfinal boolean flag2 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 516), byte2) && (bytes[518] == 0x00 || bytes[518] == 0x02);\n\t\t\t\tfinal byte[] byte3 = new byte[]{(byte) 0x09, (byte) 0x08, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x06, (byte) 0x05, (byte) 0x00};\n\t\t\t\tfinal boolean flag3 = Arrays.equals(Arrays.copyOfRange(bytes, 512, 520), byte3);\n\t\t\t\tfinal byte[] byte4 = new byte[]{(byte) 0xe2, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x5c, (byte) 0x00, (byte) 0x70, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x43, (byte) 0x61, (byte) 0x6c, (byte) 0x63};\n\t\t\t\tfinal boolean flag4 = bytes.length > 2095 && Arrays.equals(Arrays.copyOfRange(bytes, 1568, 2095), byte4);\n\t\t\t\treturn flag2 || flag3 || flag4;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t},\n\tPPT(\"application/vnd.ms-powerpoint\", \"ppt\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xd0, (byte) 0xcf, (byte) 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, (byte) 0x1a, (byte) 0xe1};\n\t\t\tfinal boolean flag1 = bytes.length > 524 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte1);\n\t\t\tif (flag1) {\n\t\t\t\tfinal byte[] byte2 = new byte[]{(byte) 0xa0, (byte) 0x46, (byte) 0x1d, (byte) 0xf0};\n\t\t\t\tfinal byte[] byteRange = Arrays.copyOfRange(bytes, 512, 516);\n\t\t\t\tfinal boolean flag2 = Arrays.equals(byteRange, byte2);\n\t\t\t\tfinal byte[] byte3 = new byte[]{(byte) 0x00, (byte) 0x6e, (byte) 0x1e, (byte) 0xf0};\n\t\t\t\tfinal boolean flag3 = Arrays.equals(byteRange, byte3);\n\t\t\t\tfinal byte[] byte4 = new byte[]{(byte) 0x0f, (byte) 0x00, (byte) 0xe8, (byte) 0x03};\n\t\t\t\tfinal boolean flag4 = Arrays.equals(byteRange, byte4);\n\t\t\t\tfinal byte[] byte5 = new byte[]{(byte) 0xfd, (byte) 0xff, (byte) 0xff, (byte) 0xff};\n\t\t\t\tfinal boolean flag5 = Arrays.equals(byteRange, byte5) && bytes[522] == 0x00 && bytes[523] == 0x00;\n\t\t\t\tfinal byte[] byte6 = new byte[]{(byte) 0x00, (byte) 0xb9, (byte) 0x29, (byte) 0xe8, (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n\t\t\t\t\t\t(byte) 0x4d, (byte) 0x53, (byte) 0x20, (byte) 0x50, (byte) 0x6f, (byte) 0x77, (byte) 0x65, (byte) 0x72, (byte) 0x50, (byte)\n\t\t\t\t\t\t0x6f, (byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x20, (byte) 0x39, (byte) 0x37};\n\t\t\t\tfinal boolean flag6 = bytes.length > 2096 && Arrays.equals(Arrays.copyOfRange(bytes, 2072, 2096), byte6);\n\t\t\t\treturn flag2 || flag3 || flag4 || flag5 || flag6;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t},\n\tDOCX(\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\", \"docx\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn Objects.equals(FileMagicNumber.matchDocument(bytes), DOCX);\n\t\t}\n\t},\n\tPPTX(\"application/vnd.openxmlformats-officedocument.presentationml.presentation\", \"pptx\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn Objects.equals(FileMagicNumber.matchDocument(bytes), PPTX);\n\t\t}\n\t},\n\tXLSX(\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", \"xlsx\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn Objects.equals(FileMagicNumber.matchDocument(bytes), XLSX);\n\t\t}\n\t},\n\n\t//document end ------------------------------------------------------------\n\t//other start -------------------------------------------------------------\n\tWASM(\"application/wasm\", \"wasm\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 7\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x61)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x73)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x6D)\n\t\t\t\t\t&& Objects.equals(bytes[4], (byte) 0x01)\n\t\t\t\t\t&& Objects.equals(bytes[5], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[6], (byte) 0x00)\n\t\t\t\t\t&& Objects.equals(bytes[7], (byte) 0x00);\n\t\t}\n\t},\n\t// https://source.android.com/devices/tech/dalvik/dex-format#dex-file-magic\n\tDEX(\"application/vnd.android.dex\", \"dex\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 36\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x64)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x78)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x0A)\n\t\t\t\t\t&& Objects.equals(bytes[36], (byte) 0x70);\n\t\t}\n\t},\n\tDEY(\"application/vnd.android.dey\", \"dey\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\treturn bytes.length > 100\n\t\t\t\t\t&& Objects.equals(bytes[0], (byte) 0x64)\n\t\t\t\t\t&& Objects.equals(bytes[1], (byte) 0x65)\n\t\t\t\t\t&& Objects.equals(bytes[2], (byte) 0x79)\n\t\t\t\t\t&& Objects.equals(bytes[3], (byte) 0x0A) &&\n\t\t\t\t\tDEX.match(Arrays.copyOfRange(bytes, 40, 100));\n\t\t}\n\t},\n\tEML(\"message/rfc822\", \"eml\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tif (bytes.length < 8) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x20, (byte) 0x20, (byte) 0x20};\n\t\t\tfinal byte[] byte2 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x20, (byte) 0x3F, (byte) 0x3F, (byte) 0x3F};\n\t\t\tfinal byte[] byte3 = new byte[]{(byte) 0x46, (byte) 0x72, (byte) 0x6F, (byte) 0x6D, (byte) 0x3A, (byte) 0x20};\n\t\t\tfinal byte[] byte4 = new byte[]{(byte) 0x52, (byte) 0x65, (byte) 0x74, (byte) 0x75, (byte) 0x72, (byte) 0x6E, (byte) 0x2D, (byte) 0x50, (byte) 0x61, (byte) 0x74, (byte) 0x68, (byte) 0x3A, (byte) 0x20};\n\t\t\treturn Arrays.equals(Arrays.copyOfRange(bytes, 0, 7), byte1)\n\t\t\t\t\t|| Arrays.equals(Arrays.copyOfRange(bytes, 0, 8), byte2)\n\t\t\t\t\t|| Arrays.equals(Arrays.copyOfRange(bytes, 0, 6), byte3)\n\t\t\t\t\t|| bytes.length > 13 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 13), byte4);\n\t\t}\n\t},\n\tMDB(\"application/vnd.ms-access\", \"mdb\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x53, (byte) 0x74, (byte) 0x61, (byte) 0x6E, (byte) 0x64,\n\t\t\t\t\t(byte) 0x61, (byte) 0x72, (byte) 0x64, (byte) 0x20, (byte) 0x4A, (byte) 0x65, (byte) 0x74, (byte) 0x20, (byte) 0x44, (byte) 0x42};\n\t\t\treturn bytes.length > 18 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 18), byte1);\n\t\t}\n\t},\n\t//CHM  49 54 53 46\n\tCHM(\"application/vnd.ms-htmlhelp\", \"chm\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x49, (byte) 0x54, (byte) 0x53, (byte) 0x46};\n\t\t\treturn bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1);\n\t\t}\n\t},\n\t//class CA FE BA BE\n\tCLASS(\"application/java-vm\", \"class\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE};\n\t\t\treturn bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1);\n\t\t}\n\t},\n\t//torrent 64 38 3A 61 6E 6E 6F 75 6E 63 65\n\tTORRENT(\"application/x-bittorrent\", \"torrent\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x64, (byte) 0x38, (byte) 0x3A, (byte) 0x61, (byte) 0x6E, (byte) 0x6E, (byte) 0x6F, (byte) 0x75, (byte) 0x6E, (byte) 0x63, (byte) 0x65};\n\t\t\treturn bytes.length > 11 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 11), byte1);\n\t\t}\n\t},\n\tWPD(\"application/vnd.wordperfect\", \"wpd\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xFF, (byte) 0x57, (byte) 0x50, (byte) 0x43};\n\t\t\treturn bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1);\n\t\t}\n\t},\n\tDBX(\"\", \"dbx\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0xCF, (byte) 0xAD, (byte) 0x12, (byte) 0xFE};\n\t\t\treturn bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1);\n\t\t}\n\t},\n\tPST(\"application/vnd.ms-outlook-pst\", \"pst\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x21, (byte) 0x42, (byte) 0x44, (byte) 0x4E};\n\t\t\treturn bytes.length > 4 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 4), byte1);\n\t\t}\n\t},\n\tRAM(\"audio/x-pn-realaudio\", \"ram\") {\n\t\t@Override\n\t\tpublic boolean match(final byte[] bytes) {\n\t\t\tfinal byte[] byte1 = new byte[]{(byte) 0x2E, (byte) 0x72, (byte) 0x61, (byte) 0xFD, (byte) 0x00};\n\t\t\treturn bytes.length > 5 && Arrays.equals(Arrays.copyOfRange(bytes, 0, 5), byte1);\n\t\t}\n\t}\n\t//other end ---------------------------------------------------------------\n\t;\n\tprivate final String mimeType;\n\tprivate final String extension;\n\n\tFileMagicNumber(final String mimeType, final String extension) {\n\t\tthis.mimeType = mimeType;\n\t\tthis.extension = extension;\n\t}\n\n\t/**\n\t * 根据给定的bytes，获取对应识别到的{@code FileMagicNumber}\n\t *\n\t * @param bytes bytes魔数\n\t * @return {@code FileMagicNumber}\n\t */\n\tpublic static FileMagicNumber getMagicNumber(final byte[] bytes) {\n\t\tif(ObjUtil.isNull(bytes)){\n\t\t\treturn UNKNOWN;\n\t\t}\n\n\t\tfinal FileMagicNumber number = Arrays.stream(values())\n\t\t\t\t.filter(fileMagicNumber -> fileMagicNumber.match(bytes))\n\t\t\t\t.findFirst()\n\t\t\t\t.orElse(UNKNOWN);\n\t\tif (number.equals(FileMagicNumber.ZIP)) {\n\t\t\tfinal FileMagicNumber fn = FileMagicNumber.matchDocument(bytes);\n\t\t\treturn fn == UNKNOWN ? ZIP : fn;\n\t\t}\n\t\treturn number;\n\t}\n\n\tpublic String getMimeType() {\n\t\treturn mimeType;\n\t}\n\n\tpublic String getExtension() {\n\t\treturn extension;\n\t}\n\n\tprivate static int indexOf(final byte[] array, final byte[] target) {\n\t\tif (array == null || target == null || array.length < target.length) {\n\t\t\treturn -1;\n\t\t}\n\t\tif (target.length == 0) {\n\t\t\treturn 0;\n\t\t} else {\n\t\t\tlabel1:\n\t\t\tfor (int i = 0; i < array.length - target.length + 1; ++i) {\n\t\t\t\tfor (int j = 0; j < target.length; ++j) {\n\t\t\t\t\tif (array[i + j] != target[j]) {\n\t\t\t\t\t\tcontinue label1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn i;\n\t\t\t}\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\t//处理 Open XML 类型的文件\n\tprivate static boolean compareBytes(final byte[] buf, final byte[] slice, final int startOffset) {\n\t\tfinal int sl = slice.length;\n\t\tif (startOffset + sl > buf.length) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal byte[] sub = Arrays.copyOfRange(buf, startOffset, startOffset + sl);\n\t\treturn Arrays.equals(sub, slice);\n\t}\n\n\tprivate static FileMagicNumber matchOpenXmlMime(final byte[] bytes, final int offset) {\n\t\tfinal byte[] word = new byte[]{'w', 'o', 'r', 'd', '/'};\n\t\tfinal byte[] ppt = new byte[]{'p', 'p', 't', '/'};\n\t\tfinal byte[] xl = new byte[]{'x', 'l', '/'};\n\t\tif (FileMagicNumber.compareBytes(bytes, word, offset)) {\n\t\t\treturn FileMagicNumber.DOCX;\n\t\t}\n\t\tif (FileMagicNumber.compareBytes(bytes, ppt, offset)) {\n\t\t\treturn FileMagicNumber.PPTX;\n\t\t}\n\t\tif (FileMagicNumber.compareBytes(bytes, xl, offset)) {\n\t\t\treturn FileMagicNumber.XLSX;\n\t\t}\n\t\treturn FileMagicNumber.UNKNOWN;\n\t}\n\n\tprivate static FileMagicNumber matchDocument(final byte[] bytes) {\n\t\tfinal FileMagicNumber fileMagicNumber = FileMagicNumber.matchOpenXmlMime(bytes, (byte) 0x1e);\n\t\tif (false == fileMagicNumber.equals(UNKNOWN)) {\n\t\t\treturn fileMagicNumber;\n\t\t}\n\t\tfinal byte[] bytes1 = new byte[]{0x5B, 0x43, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x5F, 0x54, 0x79, 0x70, 0x65, 0x73, 0x5D, 0x2E, 0x78, 0x6D, 0x6C};\n\t\tfinal byte[] bytes2 = new byte[]{0x5F, 0x72, 0x65, 0x6C, 0x73, 0x2F, 0x2E, 0x72, 0x65, 0x6C, 0x73};\n\t\tfinal byte[] bytes3 = new byte[]{0x64, 0x6F, 0x63, 0x50, 0x72, 0x6F, 0x70, 0x73};\n\t\tfinal boolean flag1 = FileMagicNumber.compareBytes(bytes, bytes1, (byte) 0x1e);\n\t\tfinal boolean flag2 = FileMagicNumber.compareBytes(bytes, bytes2, (byte) 0x1e);\n\t\tfinal boolean flag3 = FileMagicNumber.compareBytes(bytes, bytes3, (byte) 0x1e);\n\t\tif (false == (flag1 || flag2 || flag3)) {\n\t\t\treturn UNKNOWN;\n\t\t}\n\t\tint index = 0;\n\t\tfor (int i = 0; i < 4; i++) {\n\t\t\tindex = searchSignature(bytes, index + 4, 6000);\n\t\t\tif (index == -1) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfinal FileMagicNumber fn = FileMagicNumber.matchOpenXmlMime(bytes, index + 30);\n\t\t\tif (false == fn.equals(UNKNOWN)) {\n\t\t\t\treturn fn;\n\t\t\t}\n\t\t}\n\t\treturn UNKNOWN;\n\t}\n\n\tprivate static int searchSignature(final byte[] bytes, final int start, final int rangeNum) {\n\t\tfinal byte[] signature = new byte[]{0x50, 0x4B, 0x03, 0x04};\n\t\tfinal int length = bytes.length;\n\t\tint end = start + rangeNum;\n\t\tif (end > length) {\n\t\t\tend = length;\n\t\t}\n\t\tfinal int index = FileMagicNumber.indexOf(Arrays.copyOfRange(bytes, start, end), signature);\n\t\treturn (index == -1)\n\t\t\t\t? -1\n\t\t\t\t: (start + index);\n\t}\n\n\tpublic abstract boolean match(byte[] bytes);\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.concurrent.ConcurrentSkipListMap;\n\n/**\n * 文件类型判断工具类\n *\n * <p>此工具根据文件的前几位bytes猜测文件类型，对于文本、zip判断不准确，对于视频、图片类型判断准确</p>\n *\n * <p>需要注意的是，xlsx、docx等Office2007格式，全部识别为zip，因为新版采用了OpenXML格式，这些格式本质上是XML文件打包为zip</p>\n *\n * @author Looly\n */\npublic class FileTypeUtil {\n\n\tprivate static final Map<String, String> FILE_TYPE_MAP = new ConcurrentSkipListMap<>();\n\n\t/**\n\t * 增加文件类型映射<br>\n\t * 如果已经存在将覆盖之前的映射\n\t *\n\t * @param fileStreamHexHead 文件流头部Hex信息\n\t * @param extName           文件扩展名\n\t * @return 之前已经存在的文件扩展名\n\t */\n\tpublic static String putFileType(String fileStreamHexHead, String extName) {\n\t\treturn FILE_TYPE_MAP.put(fileStreamHexHead, extName);\n\t}\n\n\t/**\n\t * 移除文件类型映射\n\t *\n\t * @param fileStreamHexHead 文件流头部Hex信息\n\t * @return 移除的文件扩展名\n\t */\n\tpublic static String removeFileType(String fileStreamHexHead) {\n\t\treturn FILE_TYPE_MAP.remove(fileStreamHexHead);\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t *\n\t * @param fileStreamHexHead 文件流头部16进制字符串\n\t * @return 文件类型，未找到为{@code null}\n\t */\n\tpublic static String getType(String fileStreamHexHead) {\n\t\tif(StrUtil.isBlank(fileStreamHexHead)){\n\t\t\treturn null;\n\t\t}\n\t\tif (MapUtil.isNotEmpty(FILE_TYPE_MAP)) {\n\t\t\tfor (final Entry<String, String> fileTypeEntry : FILE_TYPE_MAP.entrySet()) {\n\t\t\t\tif (StrUtil.startWithIgnoreCase(fileStreamHexHead, fileTypeEntry.getKey())) {\n\t\t\t\t\treturn fileTypeEntry.getValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tbyte[] bytes = HexUtil.decodeHex(fileStreamHexHead);\n\t\treturn FileMagicNumber.getMagicNumber(bytes).getExtension();\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t *\n\t * @param in           文件流\n\t * @param fileHeadSize 自定义读取文件头部的大小\n\t * @return 文件类型，未找到为{@code null}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String getType(InputStream in, int fileHeadSize) throws IORuntimeException {\n\t\treturn getType((IoUtil.readHex(in, fileHeadSize, false)));\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型<br>\n\t * 注意此方法会读取头部一些bytes，造成此流接下来读取时缺少部分bytes<br>\n\t * 因此如果想复用此流，流需支持{@link InputStream#reset()}方法。\n\t *\n\t * @param in      {@link InputStream}\n\t * @param isExact 是否精确匹配，如果为false，使用前64个bytes匹配，如果为true，使用前8192bytes匹配\n\t * @return 类型，文件的扩展名，提供的in为{@code null}或未找到为{@code null}\n\t * @throws IORuntimeException 读取流引起的异常\n\t */\n\tpublic static String getType(InputStream in, boolean isExact) throws IORuntimeException {\n\t\tif (null == in) {\n\t\t\treturn null;\n\t\t}\n\t\treturn isExact\n\t\t\t? getType(IoUtil.readHex8192Upper(in))\n\t\t\t: getType(IoUtil.readHex64Upper(in));\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型<br>\n\t * 注意此方法会读取头部64个bytes，造成此流接下来读取时缺少部分bytes<br>\n\t * 因此如果想复用此流，流需支持{@link InputStream#reset()}方法。\n\t *\n\t * @param in {@link InputStream}\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException 读取流引起的异常\n\t */\n\tpublic static String getType(InputStream in) throws IORuntimeException {\n\t\treturn getType(in, false);\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t * 注意此方法会读取头部64个bytes，造成此流接下来读取时缺少部分bytes<br>\n\t * 因此如果想复用此流，流需支持{@link InputStream#reset()}方法。\n\t *\n\t * <pre>\n\t *     1、无法识别类型默认按照扩展名识别\n\t *     2、xls、doc、msi头信息无法区分，按照扩展名区分\n\t *     3、zip可能为docx、xlsx、pptx、jar、war、ofd头信息无法区分，按照扩展名区分\n\t * </pre>\n\t *\n\t * @param in       {@link InputStream}\n\t * @param filename 文件名\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException 读取流引起的异常\n\t */\n\tpublic static String getType(InputStream in, String filename) throws IORuntimeException {\n\t\treturn getType(in, filename, false);\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t * 注意此方法会读取头部一些bytes，造成此流接下来读取时缺少部分bytes<br>\n\t * 因此如果想复用此流，流需支持{@link InputStream#reset()}方法。\n\t *\n\t * <pre>\n\t *     1、无法识别类型默认按照扩展名识别\n\t *     2、xls、doc、msi头信息无法区分，按照扩展名区分\n\t *     3、zip可能为docx、xlsx、pptx、jar、war、ofd头信息无法区分，按照扩展名区分\n\t * </pre>\n\t *\n\t * @param in       {@link InputStream}\n\t * @param filename 文件名\n\t * @param isExact  是否精确匹配，如果为false，使用前64个bytes匹配，如果为true，使用前8192bytes匹配\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException 读取流引起的异常\n\t */\n\tpublic static String getType(InputStream in, String filename, boolean isExact) throws IORuntimeException {\n\t\tString typeName = getType(in, isExact);\n\t\tif (null == typeName) {\n\t\t\t// 未成功识别类型，扩展名辅助识别\n\t\t\ttypeName = FileUtil.extName(filename);\n\t\t} else if (\"zip\".equals(typeName)) {\n\t\t\t// zip可能为docx、xlsx、pptx、jar、war、ofd等格式，扩展名辅助判断\n\t\t\tfinal String extName = FileUtil.extName(filename);\n\t\t\tif (\"docx\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"docx\";\n\t\t\t} else if (\"xlsx\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"xlsx\";\n\t\t\t} else if (\"pptx\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"pptx\";\n\t\t\t} else if (\"jar\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"jar\";\n\t\t\t} else if (\"war\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"war\";\n\t\t\t} else if (\"ofd\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"ofd\";\n\t\t\t} else if (\"apk\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"apk\";\n\t\t\t}\n\t\t} else if (\"jar\".equals(typeName)) {\n\t\t\t// wps编辑过的.xlsx文件与.jar的开头相同,通过扩展名判断\n\t\t\tfinal String extName = FileUtil.extName(filename);\n\t\t\tif (\"xlsx\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"xlsx\";\n\t\t\t} else if (\"docx\".equalsIgnoreCase(extName)) {\n\t\t\t\t// issue#I47JGH\n\t\t\t\ttypeName = \"docx\";\n\t\t\t} else if (\"pptx\".equalsIgnoreCase(extName)) {\n\t\t\t\t// issue#I5A0GO\n\t\t\t\ttypeName = \"pptx\";\n\t\t\t} else if (\"zip\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"zip\";\n\t\t\t} else if (\"apk\".equalsIgnoreCase(extName)) {\n\t\t\t\ttypeName = \"apk\";\n\t\t\t}\n\t\t}\n\t\treturn typeName;\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t *\n\t * <pre>\n\t *     1、无法识别类型默认按照扩展名识别\n\t *     2、xls、doc、msi头信息无法区分，按照扩展名区分\n\t *     3、zip可能为jar、war头信息无法区分，按照扩展名区分\n\t * </pre>\n\t *\n\t * @param file    文件 {@link File}\n\t * @param isExact 是否精确匹配，如果为false，使用前64个bytes匹配，如果为true，使用前8192bytes匹配\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException 读取文件引起的异常\n\t */\n\tpublic static String getType(File file, boolean isExact) throws IORuntimeException {\n\t\tif (false == FileUtil.isFile(file)) {\n\t\t\tthrow new IllegalArgumentException(\"Not a regular file!\");\n\t\t}\n\t\tFileInputStream in = null;\n\t\ttry {\n\t\t\tin = IoUtil.toStream(file);\n\t\t\treturn getType(in, file.getName(), isExact);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t *\n\t * <pre>\n\t *     1、无法识别类型默认按照扩展名识别\n\t *     2、xls、doc、msi头信息无法区分，按照扩展名区分\n\t *     3、zip可能为jar、war头信息无法区分，按照扩展名区分\n\t * </pre>\n\t *\n\t * @param file 文件 {@link File}\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException 读取文件引起的异常\n\t */\n\tpublic static String getType(File file) throws IORuntimeException {\n\t\treturn getType(file, false);\n\t}\n\n\t/**\n\t * 通过路径获得文件类型\n\t *\n\t * @param path    路径，绝对路径或相对ClassPath的路径\n\t * @param isExact 是否精确匹配，如果为false，使用前64个bytes匹配，如果为true，使用前8192bytes匹配\n\t * @return 类型\n\t * @throws IORuntimeException 读取文件引起的异常\n\t */\n\tpublic static String getTypeByPath(String path, boolean isExact) throws IORuntimeException {\n\t\treturn getType(FileUtil.file(path), isExact);\n\t}\n\n\t/**\n\t * 通过路径获得文件类型\n\t *\n\t * @param path 路径，绝对路径或相对ClassPath的路径\n\t * @return 类型\n\t * @throws IORuntimeException 读取文件引起的异常\n\t */\n\tpublic static String getTypeByPath(String path) throws IORuntimeException {\n\t\treturn getTypeByPath(path, false);\n\t}\n\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.file.FileReader;\nimport cn.hutool.core.io.file.*;\nimport cn.hutool.core.io.file.FileWriter;\nimport cn.hutool.core.io.file.FileReader.ReaderHandler;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.io.unit.DataSizeUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.*;\n\nimport java.io.*;\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.nio.charset.Charset;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.jar.JarFile;\nimport java.util.regex.Pattern;\nimport java.util.zip.CRC32;\nimport java.util.zip.Checksum;\n\n/**\n * 文件工具类\n *\n * @author looly\n */\npublic class FileUtil extends PathUtil {\n\n\t/**\n\t * Class文件扩展名\n\t */\n\tpublic static final String CLASS_EXT = FileNameUtil.EXT_CLASS;\n\t/**\n\t * Jar文件扩展名\n\t */\n\tpublic static final String JAR_FILE_EXT = FileNameUtil.EXT_JAR;\n\t/**\n\t * 在Jar中的路径jar的扩展名形式\n\t */\n\tpublic static final String JAR_PATH_EXT = \".jar!\";\n\t/**\n\t * 当Path为文件形式时, path会加入一个表示文件的前缀\n\t */\n\tpublic static final String PATH_FILE_PRE = URLUtil.FILE_URL_PREFIX;\n\t/**\n\t * 文件路径分隔符<br>\n\t * 在Unix和Linux下 是{@code '/'}; 在Windows下是 {@code '\\'}\n\t */\n\tpublic static final String FILE_SEPARATOR = File.separator;\n\t/**\n\t * 多个PATH之间的分隔符<br>\n\t * 在Unix和Linux下 是{@code ':'}; 在Windows下是 {@code ';'}\n\t */\n\tpublic static final String PATH_SEPARATOR = File.pathSeparator;\n\t/**\n\t * 绝对路径判断正则\n\t */\n\tprivate static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile(\"^[a-zA-Z]:([/\\\\\\\\].*)?\", Pattern.DOTALL);\n\n\t/**\n\t * windows的共享文件夹开头\n\t */\n\tprivate static final String SMB_PATH_PREFIX = \"\\\\\\\\\";\n\n\t/**\n\t * 是否为Windows环境\n\t *\n\t * @return 是否为Windows环境\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isWindows() {\n\t\treturn FileNameUtil.WINDOWS_SEPARATOR == File.separatorChar;\n\t}\n\n\t/**\n\t * 列出指定路径下的目录和文件<br>\n\t * 给定的绝对路径不能是压缩包中的路径\n\t *\n\t * @param path 目录绝对路径或者相对路径\n\t * @return 文件列表（包含目录）\n\t */\n\tpublic static File[] ls(String path) {\n\t\tif (path == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tFile file = file(path);\n\t\tif (file.isDirectory()) {\n\t\t\treturn file.listFiles();\n\t\t}\n\t\tthrow new IORuntimeException(StrUtil.format(\"Path [{}] is not directory!\", path));\n\t}\n\n\t/**\n\t * 文件是否为空<br>\n\t * 目录：里面没有文件时为空 文件：文件大小为0时为空\n\t *\n\t * @param file 文件\n\t * @return 是否为空，当提供非目录时，返回false\n\t */\n\tpublic static boolean isEmpty(File file) {\n\t\tif (null == file || false == file.exists()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (file.isDirectory()) {\n\t\t\tString[] subFiles = file.list();\n\t\t\treturn ArrayUtil.isEmpty(subFiles);\n\t\t} else if (file.isFile()) {\n\t\t\treturn file.length() <= 0;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * 目录是否为空\n\t *\n\t * @param file 目录\n\t * @return 是否为空，当提供非目录时，返回false\n\t */\n\tpublic static boolean isNotEmpty(File file) {\n\t\treturn false == isEmpty(file);\n\t}\n\n\t/**\n\t * 目录是否为空\n\t *\n\t * @param dir 目录\n\t * @return 是否为空\n\t */\n\tpublic static boolean isDirEmpty(File dir) {\n\t\treturn isDirEmpty(dir.toPath());\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供file为文件，直接返回过滤结果\n\t *\n\t * @param path       当前遍历文件或目录的路径\n\t * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录\n\t * @return 文件列表\n\t * @since 3.2.0\n\t */\n\tpublic static List<File> loopFiles(String path, FileFilter fileFilter) {\n\t\treturn loopFiles(file(path), fileFilter);\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供file为文件，直接返回过滤结果\n\t *\n\t * @param file       当前遍历文件或目录\n\t * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录\n\t * @return 文件列表\n\t */\n\tpublic static List<File> loopFiles(File file, FileFilter fileFilter) {\n\t\treturn loopFiles(file, -1, fileFilter);\n\t}\n\n\t/**\n\t * 递归遍历目录并处理目录下的文件，可以处理目录或文件：\n\t * <ul>\n\t *     <li>非目录则直接调用{@link Consumer}处理</li>\n\t *     <li>目录则递归调用此方法处理</li>\n\t * </ul>\n\t *\n\t * @param file     文件或目录，文件直接处理\n\t * @param consumer 文件处理器，只会处理文件\n\t * @since 5.5.2\n\t */\n\tpublic static void walkFiles(File file, Consumer<File> consumer) {\n\t\tif (file.isDirectory()) {\n\t\t\tfinal File[] subFiles = file.listFiles();\n\t\t\tif (ArrayUtil.isNotEmpty(subFiles)) {\n\t\t\t\tfor (File tmp : subFiles) {\n\t\t\t\t\twalkFiles(tmp, consumer);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconsumer.accept(file);\n\t\t}\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供file为文件，直接返回过滤结果\n\t *\n\t * @param file       当前遍历文件或目录\n\t * @param maxDepth   遍历最大深度，-1表示遍历到没有目录为止\n\t * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件\n\t * @return 文件列表\n\t * @since 4.6.3\n\t */\n\tpublic static List<File> loopFiles(File file, int maxDepth, FileFilter fileFilter) {\n\t\treturn loopFiles(file.toPath(), maxDepth, fileFilter);\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果用户传入相对路径，则是相对classpath的路径<br>\n\t * 如：\"test/aaa\"表示\"${classpath}/test/aaa\"\n\t *\n\t * @param path 相对ClassPath的目录或者绝对路径目录\n\t * @return 文件列表\n\t * @since 3.2.0\n\t */\n\tpublic static List<File> loopFiles(String path) {\n\t\treturn loopFiles(file(path));\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件\n\t *\n\t * @param file 当前遍历文件\n\t * @return 文件列表\n\t */\n\tpublic static List<File> loopFiles(File file) {\n\t\treturn loopFiles(file, null);\n\t}\n\n\t/**\n\t * 获得指定目录下所有文件<br>\n\t * 不会扫描子目录<br>\n\t * 如果用户传入相对路径，则是相对classpath的路径<br>\n\t * 如：\"test/aaa\"表示\"${classpath}/test/aaa\"\n\t *\n\t * @param path 相对ClassPath的目录或者绝对路径目录\n\t * @return 文件路径列表（如果是jar中的文件，则给定类似.jar!/xxx/xxx的路径）\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> listFileNames(String path) throws IORuntimeException {\n\t\tif (path == null) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tpath = getAbsolutePath(path);\n\t\tint index = path.lastIndexOf(FileUtil.JAR_PATH_EXT);\n\t\tif (index < 0) {\n\t\t\t// 普通目录\n\t\t\tfinal List<String> paths = new ArrayList<>();\n\t\t\tfinal File[] files = ls(path);\n\t\t\tfor (File file : files) {\n\t\t\t\tif (file.isFile()) {\n\t\t\t\t\tpaths.add(file.getName());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn paths;\n\t\t}\n\n\t\t// jar文件中的路径\n\t\tindex = index + FileUtil.JAR_FILE_EXT.length();\n\t\tJarFile jarFile = null;\n\t\ttry {\n\t\t\tjarFile = new JarFile(path.substring(0, index));\n\t\t\t// 防止出现jar!/cn/hutool/这类路径导致文件找不到\n\t\t\treturn ZipUtil.listFileNames(jarFile, StrUtil.removePrefix(path.substring(index + 1), \"/\"));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(StrUtil.format(\"Can not read file path of [{}]\", path), e);\n\t\t} finally {\n\t\t\tIoUtil.close(jarFile);\n\t\t}\n\t}\n\n\t/**\n\t * 创建File对象，相当于调用new File()，不做任何处理\n\t *\n\t * @param path 文件路径，相对路径表示相对项目路径\n\t * @return File\n\t * @since 4.1.4\n\t */\n\tpublic static File newFile(String path) {\n\t\treturn new File(path);\n\t}\n\n\t/**\n\t * 创建File对象，自动识别相对或绝对路径，相对路径将自动从ClassPath下寻找\n\t *\n\t * @param path 相对ClassPath的目录或者绝对路径目录\n\t * @return File\n\t */\n\tpublic static File file(String path) {\n\t\tif (null == path) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new File(getAbsolutePath(path));\n\t}\n\n\t/**\n\t * 创建File对象<br>\n\t * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/\n\t *\n\t * @param parent 父目录\n\t * @param path   文件路径\n\t * @return File\n\t */\n\tpublic static File file(String parent, String path) {\n\t\treturn file(new File(parent), path);\n\t}\n\n\t/**\n\t * 创建File对象<br>\n\t * 根据的路径构建文件，在Win下直接构建，在Linux下拆分路径单独构建\n\t * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/\n\t *\n\t * @param parent 父文件对象\n\t * @param path   文件路径\n\t * @return File\n\t */\n\tpublic static File file(File parent, String path) {\n\t\tif (StrUtil.isBlank(path)) {\n\t\t\tthrow new NullPointerException(\"File path is blank!\");\n\t\t}\n\t\treturn checkSlip(parent, buildFile(parent, path));\n\t}\n\n\t/**\n\t * 通过多层目录参数创建文件<br>\n\t * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/\n\t *\n\t * @param directory 父目录\n\t * @param names     元素名（多层目录名），由外到内依次传入\n\t * @return the file 文件\n\t * @since 4.0.6\n\t */\n\tpublic static File file(File directory, String... names) {\n\t\tAssert.notNull(directory, \"directory must not be null\");\n\t\tif (ArrayUtil.isEmpty(names)) {\n\t\t\treturn directory;\n\t\t}\n\n\t\tFile file = directory;\n\t\tfor (String name : names) {\n\t\t\tif (null != name) {\n\t\t\t\tfile = file(file, name);\n\t\t\t}\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 通过多层目录创建文件\n\t * <p>\n\t * 元素名（多层目录名）\n\t *\n\t * @param names 多层文件的文件名，由外到内依次传入\n\t * @return the file 文件\n\t * @since 4.0.6\n\t */\n\tpublic static File file(String... names) {\n\t\tif (ArrayUtil.isEmpty(names)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tFile file = null;\n\t\tfor (String name : names) {\n\t\t\tif (file == null) {\n\t\t\t\tfile = file(name);\n\t\t\t} else {\n\t\t\t\tfile = file(file, name);\n\t\t\t}\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 创建File对象\n\t *\n\t * @param uri 文件URI\n\t * @return File\n\t */\n\tpublic static File file(URI uri) {\n\t\tif (uri == null) {\n\t\t\tthrow new NullPointerException(\"File uri is null!\");\n\t\t}\n\t\treturn new File(uri);\n\t}\n\n\t/**\n\t * 创建File对象\n\t *\n\t * @param url 文件URL\n\t * @return File\n\t */\n\tpublic static File file(URL url) {\n\t\treturn new File(URLUtil.toURI(url));\n\t}\n\n\t/**\n\t * 获取临时文件路径（绝对路径）\n\t *\n\t * @return 临时文件路径\n\t * @since 4.0.6\n\t */\n\tpublic static String getTmpDirPath() {\n\t\treturn System.getProperty(\"java.io.tmpdir\");\n\t}\n\n\t/**\n\t * 获取临时文件目录\n\t *\n\t * @return 临时文件目录\n\t * @since 4.0.6\n\t */\n\tpublic static File getTmpDir() {\n\t\treturn file(getTmpDirPath());\n\t}\n\n\t/**\n\t * 获取用户路径（绝对路径）\n\t *\n\t * @return 用户路径\n\t * @since 4.0.6\n\t */\n\tpublic static String getUserHomePath() {\n\t\treturn System.getProperty(\"user.home\");\n\t}\n\n\t/**\n\t * 获取用户目录\n\t *\n\t * @return 用户目录\n\t * @since 4.0.6\n\t */\n\tpublic static File getUserHomeDir() {\n\t\treturn file(getUserHomePath());\n\t}\n\n\t/**\n\t * 判断文件是否存在，如果path为null，则返回false\n\t *\n\t * @param path 文件路径\n\t * @return 如果存在返回true\n\t */\n\tpublic static boolean exist(String path) {\n\t\treturn (null != path) && file(path).exists();\n\t}\n\n\t/**\n\t * 判断文件是否存在，如果file为null，则返回false\n\t *\n\t * @param file 文件\n\t * @return 如果存在返回true\n\t */\n\tpublic static boolean exist(File file) {\n\t\treturn (null != file) && file.exists();\n\t}\n\n\t/**\n\t * 是否存在匹配文件\n\t *\n\t * @param directory 文件夹路径\n\t * @param regexp    文件夹中所包含文件名的正则表达式\n\t * @return 如果存在匹配文件返回true\n\t */\n\tpublic static boolean exist(String directory, String regexp) {\n\t\tfinal File file = new File(directory);\n\t\tif (false == file.exists()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String[] fileList = file.list();\n\t\tif (fileList == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (String fileName : fileList) {\n\t\t\tif (fileName.matches(regexp)) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 指定文件最后修改时间\n\t *\n\t * @param file 文件\n\t * @return 最后修改时间\n\t */\n\tpublic static Date lastModifiedTime(File file) {\n\t\tif (false == exist(file)) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn new Date(file.lastModified());\n\t}\n\n\t/**\n\t * 指定路径文件最后修改时间\n\t *\n\t * @param path 绝对路径\n\t * @return 最后修改时间\n\t */\n\tpublic static Date lastModifiedTime(String path) {\n\t\treturn lastModifiedTime(new File(path));\n\t}\n\n\t/**\n\t * 计算目录或文件的总大小<br>\n\t * 当给定对象为文件时，直接调用 {@link File#length()}<br>\n\t * 当给定对象为目录时，遍历目录下的所有文件和目录，递归计算其大小，求和返回<br>\n\t * 此方法不包括目录本身的占用空间大小。\n\t *\n\t * @param file 目录或文件,null或者文件不存在返回0\n\t * @return 总大小，bytes长度\n\t */\n\tpublic static long size(File file) {\n\t\treturn size(file, false);\n\t}\n\n\t/**\n\t * 计算目录或文件的总大小<br>\n\t * 当给定对象为文件时，直接调用 {@link File#length()}<br>\n\t * 当给定对象为目录时，遍历目录下的所有文件和目录，递归计算其大小，求和返回\n\t *\n\t * @param file           目录或文件,null或者文件不存在返回0\n\t * @param includeDirSize 是否包括每层目录本身的大小\n\t * @return 总大小，bytes长度\n\t * @since 5.7.21\n\t */\n\tpublic static long size(File file, boolean includeDirSize) {\n\t\tif (null == file || false == file.exists() || isSymlink(file)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (file.isDirectory()) {\n\t\t\tlong size = includeDirSize ? file.length() : 0;\n\t\t\tFile[] subFiles = file.listFiles();\n\t\t\tif (ArrayUtil.isEmpty(subFiles)) {\n\t\t\t\treturn 0L;// empty directory\n\t\t\t}\n\t\t\tfor (File subFile : subFiles) {\n\t\t\t\tsize += size(subFile, includeDirSize);\n\t\t\t}\n\t\t\treturn size;\n\t\t} else {\n\t\t\treturn file.length();\n\t\t}\n\t}\n\n\t/**\n\t * 计算文件的总行数<br>\n\t * 参考：https://stackoverflow.com/questions/453018/number-of-lines-in-a-file-in-java\n\t *\n\t * @param file 文件\n\t * @return 该文件总行数\n\t * @since 5.7.22\n\t */\n\tpublic static int getTotalLines(File file) {\n\t\treturn getTotalLines(file, 1024);\n\t}\n\n\t/**\n\t * 计算文件的总行数<br>\n\t * 参考：https://stackoverflow.com/questions/453018/number-of-lines-in-a-file-in-java<br>\n\t * 最后一行如果末尾带有换行符，则被当作为新行\n\t *\n\t * @param file       文件\n\t * @param bufferSize 缓存大小，小于1则使用默认的1024\n\t * @return 该文件总行数\n\t * @since 5.8.28\n\t */\n\tpublic static int getTotalLines(File file, int bufferSize) {\n\t\treturn getTotalLines(file, bufferSize, true);\n\t}\n\n\t/**\n\t * 计算文件的总行数<br>\n\t * 参考：https://stackoverflow.com/questions/453018/number-of-lines-in-a-file-in-java\n\t *\n\t * @param file       文件\n\t * @param bufferSize 缓存大小，小于1则使用默认的1024\n\t * @param lastLineSeparatorAsNewLine 是否将最后一行分隔符作为新行，Linux下要求最后一行必须带有换行符，不算一行，此处用户选择\n\t * @return 该文件总行数\n\t * @since 5.8.37\n\t */\n\tpublic static int getTotalLines(File file, int bufferSize, boolean lastLineSeparatorAsNewLine) {\n\t\tif (false == isFile(file)) {\n\t\t\tthrow new IORuntimeException(\"Input must be a File\");\n\t\t}\n\t\tif (bufferSize < 1) {\n\t\t\tbufferSize = 1024;\n\t\t}\n\t\ttry (InputStream is = getInputStream(file)) {\n\t\t\tbyte[] chars = new byte[bufferSize];\n\t\t\tint readChars = is.read(chars);\n\t\t\tif (readChars == -1) {\n\t\t\t\t// 空文件，返回0\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\t// 起始行为1\n\t\t\t// 如果只有一行，无换行符，则读取结束后返回1\n\t\t\t// 如果多行，最后一行无换行符，最后一行需要单独计数\n\t\t\t// 如果多行，最后一行有换行符，则空行算作一行\n\t\t\tint count = 1;\n\t\t\tbyte pre;\n\t\t\tbyte c = 0;\n\t\t\twhile (readChars == bufferSize) {\n\t\t\t\tfor (int i = 0; i < bufferSize; i++) {\n\t\t\t\t\tpre = c;\n\t\t\t\t\tc = chars[i];\n\t\t\t\t\t// 换行符兼容MAC\n\t\t\t\t\tif (c == CharUtil.LF || pre == CharUtil.CR) {\n\t\t\t\t\t\t++count;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treadChars = is.read(chars);\n\t\t\t}\n\n\t\t\t// count remaining characters\n\t\t\twhile (readChars != -1) {\n\t\t\t\tfor (int i = 0; i < readChars; i++) {\n\t\t\t\t\tpre = c;\n\t\t\t\t\tc = chars[i];\n\t\t\t\t\tif (c == CharUtil.LF || pre == CharUtil.CR) {\n\t\t\t\t\t\t++count;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treadChars = is.read(chars);\n\t\t\t}\n\n\t\t\tif(lastLineSeparatorAsNewLine){\n\t\t\t\t// 最后一个字符为\\r，则单独计数行\n\t\t\t\tif(c == CharUtil.CR){\n\t\t\t\t\t++count;\n\t\t\t\t}\n\t\t\t}else{\n\t\t\t\t// 最后一个字符为\\n，则可选是否算作新行单独计数行\n\t\t\t\tif(c == CharUtil.LF){\n\t\t\t\t\t--count;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn count;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 给定文件或目录的最后修改时间是否晚于给定时间\n\t *\n\t * @param file      文件或目录\n\t * @param reference 参照文件\n\t * @return 是否晚于给定时间\n\t */\n\tpublic static boolean newerThan(File file, File reference) {\n\t\tif (null == reference || false == reference.exists()) {\n\t\t\treturn true;// 文件一定比一个不存在的文件新\n\t\t}\n\t\treturn newerThan(file, reference.lastModified());\n\t}\n\n\t/**\n\t * 给定文件或目录的最后修改时间是否晚于给定时间\n\t *\n\t * @param file       文件或目录\n\t * @param timeMillis 做为对比的时间\n\t * @return 是否晚于给定时间\n\t */\n\tpublic static boolean newerThan(File file, long timeMillis) {\n\t\tif (null == file || false == file.exists()) {\n\t\t\treturn false;// 不存在的文件一定比任何时间旧\n\t\t}\n\t\treturn file.lastModified() > timeMillis;\n\t}\n\n\t/**\n\t * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型\n\t *\n\t * @param path 相对ClassPath的目录或者绝对路径目录，使用POSIX风格\n\t * @return 文件，若路径为null，返回null\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File touch(String path) throws IORuntimeException {\n\t\tif (path == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn touch(file(path));\n\t}\n\n\t/**\n\t * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型\n\t *\n\t * @param file 文件对象\n\t * @return 文件，若路径为null，返回null\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File touch(File file) throws IORuntimeException {\n\t\tif (null == file) {\n\t\t\treturn null;\n\t\t}\n\t\tif (false == file.exists()) {\n\t\t\tmkParentDirs(file);\n\t\t\ttry {\n\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\tfile.createNewFile();\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型\n\t *\n\t * @param parent 父文件对象\n\t * @param path   文件路径\n\t * @return File\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File touch(File parent, String path) throws IORuntimeException {\n\t\treturn touch(file(parent, path));\n\t}\n\n\t/**\n\t * 创建文件及其父目录，如果这个文件存在，直接返回这个文件<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型\n\t *\n\t * @param parent 父文件对象\n\t * @param path   文件路径\n\t * @return File\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File touch(String parent, String path) throws IORuntimeException {\n\t\treturn touch(file(parent, path));\n\t}\n\n\t/**\n\t * 创建所给文件或目录的父目录\n\t *\n\t * @param file 文件或目录\n\t * @return 父目录\n\t */\n\tpublic static File mkParentDirs(File file) {\n\t\tif (null == file) {\n\t\t\treturn null;\n\t\t}\n\t\treturn mkdir(getParent(file, 1));\n\t}\n\n\t/**\n\t * 创建父文件夹，如果存在直接返回此文件夹\n\t *\n\t * @param path 文件夹路径，使用POSIX格式，无论哪个平台\n\t * @return 创建的目录\n\t */\n\tpublic static File mkParentDirs(String path) {\n\t\tif (path == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn mkParentDirs(file(path));\n\t}\n\n\t/**\n\t * 删除文件或者文件夹<br>\n\t * 路径如果为相对路径，会转换为ClassPath路径！ 注意：删除文件夹时不会判断文件夹是否为空，如果不空则递归删除子文件或文件夹<br>\n\t * 某个文件删除失败会终止删除操作\n\t *\n\t * @param fullFileOrDirPath 文件或者目录的路径\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static boolean del(String fullFileOrDirPath) throws IORuntimeException {\n\t\treturn del(file(fullFileOrDirPath));\n\t}\n\n\t/**\n\t * 删除文件或者文件夹<br>\n\t * 注意：删除文件夹时不会判断文件夹是否为空，如果不空则递归删除子文件或文件夹<br>\n\t * 某个文件删除失败会终止删除操作\n\t *\n\t * <p>\n\t * 从5.7.6开始，删除文件使用{@link Files#delete(Path)}代替 {@link File#delete()}<br>\n\t * 因为前者遇到文件被占用等原因时，抛出异常，而非返回false，异常会指明具体的失败原因。\n\t * </p>\n\t *\n\t * @param file 文件对象\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t * @see Files#delete(Path)\n\t */\n\tpublic static boolean del(File file) throws IORuntimeException {\n\t\tif (file == null || false == file.exists()) {\n\t\t\t// 如果文件不存在或已被删除，此处返回true表示删除成功\n\t\t\treturn true;\n\t\t}\n\n\t\tif (file.isDirectory()) {\n\t\t\t// 清空目录下所有文件和目录\n\t\t\tboolean isOk = clean(file);\n\t\t\tif (false == isOk) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// 删除文件或清空后的目录\n\t\tfinal Path path = file.toPath();\n\t\ttry {\n\t\t\tdelFile(path);\n\t\t} catch (DirectoryNotEmptyException e) {\n\t\t\t// 遍历清空目录没有成功，此时补充删除一次（可能存在部分软链）\n\t\t\tdel(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 清空文件夹<br>\n\t * 注意：清空文件夹时不会判断文件夹是否为空，如果不空则递归删除子文件或文件夹<br>\n\t * 某个文件删除失败会终止删除操作\n\t *\n\t * @param dirPath 文件夹路径\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.8\n\t */\n\tpublic static boolean clean(String dirPath) throws IORuntimeException {\n\t\treturn clean(file(dirPath));\n\t}\n\n\t/**\n\t * 清空文件夹<br>\n\t * 注意：清空文件夹时不会判断文件夹是否为空，如果不空则递归删除子文件或文件夹<br>\n\t * 某个文件删除失败会终止删除操作\n\t *\n\t * @param directory 文件夹\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t * @since 3.0.6\n\t */\n\tpublic static boolean clean(File directory) throws IORuntimeException {\n\t\tif (directory == null || directory.exists() == false || false == directory.isDirectory()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfinal File[] files = directory.listFiles();\n\t\tif (null != files) {\n\t\t\tfor (File childFile : files) {\n\t\t\t\tif (false == del(childFile)) {\n\t\t\t\t\t// 删除一个出错则本次删除任务失败\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 清理空文件夹<br>\n\t * 此方法用于递归删除空的文件夹，不删除文件<br>\n\t * 如果传入的文件夹本身就是空的，删除这个文件夹\n\t *\n\t * @param directory 文件夹\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.5\n\t */\n\tpublic static boolean cleanEmpty(File directory) throws IORuntimeException {\n\t\tif (directory == null || false == directory.exists() || false == directory.isDirectory()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tFile[] files = directory.listFiles();\n\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\t// 空文件夹则删除之\n\t\t\treturn directory.delete();\n\t\t}\n\n\t\tfor (File childFile : files) {\n\t\t\tcleanEmpty(childFile);\n\t\t}\n\n\t\t// 当前目录清除完毕，需要再次判断当前文件夹，空文件夹则删除之\n\t\tString[] fileNames = directory.list();\n\t\tif (ArrayUtil.isEmpty(fileNames)) {\n\t\t\treturn directory.delete();\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 创建文件夹，如果存在直接返回此文件夹<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型\n\t *\n\t * @param dirPath 文件夹路径，使用POSIX格式，无论哪个平台\n\t * @return 创建的目录\n\t */\n\tpublic static File mkdir(String dirPath) {\n\t\tif (dirPath == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal File dir = file(dirPath);\n\t\treturn mkdir(dir);\n\t}\n\n\t/**\n\t * 创建文件夹，会递归自动创建其不存在的父文件夹，如果存在直接返回此文件夹<br>\n\t * 此方法不对File对象类型做判断，如果File不存在，无法判断其类型<br>\n\t *\n\t * @param dir 目录\n\t * @return 创建的目录\n\t */\n\tpublic static File mkdir(File dir) {\n\t\tif (dir == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (false == dir.exists()) {\n\t\t\tmkdirsSafely(dir, 5, 1);\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * 安全地级联创建目录 (确保并发环境下能创建成功)\n\t *\n\t * <pre>\n\t *     并发环境下，假设 test 目录不存在，如果线程A mkdirs \"test/A\" 目录，线程B mkdirs \"test/B\"目录，\n\t *     其中一个线程可能会失败，进而导致以下代码抛出 FileNotFoundException 异常\n\t *\n\t *     file.getParentFile().mkdirs(); // 父目录正在被另一个线程创建中，返回 false\n\t *     file.createNewFile(); // 抛出 IO 异常，因为该线程无法感知到父目录已被创建\n\t * </pre>\n\t *\n\t * @param dir         待创建的目录\n\t * @param tryCount    最大尝试次数\n\t * @param sleepMillis 线程等待的毫秒数\n\t * @return true表示创建成功，false表示创建失败\n\t * @author z8g\n\t * @since 5.7.21\n\t */\n\tpublic static boolean mkdirsSafely(File dir, int tryCount, long sleepMillis) {\n\t\tif (dir == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (dir.isDirectory()) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (int i = 1; i <= tryCount; i++) { // 高并发场景下，可以看到 i 处于 1 ~ 3 之间\n\t\t\t// 如果文件已存在，也会返回 false，所以该值不能作为是否能创建的依据，因此不对其进行处理\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\tdir.mkdirs();\n\t\t\tif (dir.exists()) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tThreadUtil.sleep(sleepMillis);\n\t\t}\n\t\treturn dir.exists();\n\t}\n\n\t/**\n\t * 创建临时文件<br>\n\t * 创建后的文件名为 prefix[Random.tmp\n\t *\n\t * @param dir 临时文件创建的所在目录\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File createTempFile(File dir) throws IORuntimeException {\n\t\treturn createTempFile(\"hutool\", null, dir, true);\n\t}\n\n\t/**\n\t * 在默认临时文件目录下创建临时文件，创建后的文件名为 prefix[Random].tmp。\n\t * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。\n\t * 在 UNIX 系统上，此属性的默认值通常是 {@code \"tmp\"} 或 {@code \"vartmp\"}；\n\t * 在 Microsoft Windows 系统上，它通常是 {@code \"C:\\\\WINNT\\\\TEMP\"}。\n\t * 调用 Java 虚拟机时，可以为该系统属性赋予不同的值，但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。\n\t *\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.22\n\t */\n\tpublic static File createTempFile() throws IORuntimeException {\n\t\treturn createTempFile(\"hutool\", null, null, true);\n\t}\n\n\t/**\n\t * 在默认临时文件目录下创建临时文件，创建后的文件名为 prefix[Random].suffix。\n\t * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。\n\t * 在 UNIX 系统上，此属性的默认值通常是 {@code \"tmp\"} 或 {@code \"vartmp\"}；\n\t * 在 Microsoft Windows 系统上，它通常是 {@code \"C:\\\\WINNT\\\\TEMP\"}。\n\t * 调用 Java 虚拟机时，可以为该系统属性赋予不同的值，但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。\n\t *\n\t * @param suffix    后缀，如果null则使用默认.tmp\n\t * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.22\n\t */\n\tpublic static File createTempFile(String suffix, boolean isReCreat) throws IORuntimeException {\n\t\treturn createTempFile(\"hutool\", suffix, null, isReCreat);\n\t}\n\n\t/**\n\t * 在默认临时文件目录下创建临时文件，创建后的文件名为 prefix[Random].suffix。\n\t * 默认临时文件目录由系统属性 {@code java.io.tmpdir} 指定。\n\t * 在 UNIX 系统上，此属性的默认值通常是 {@code \"tmp\"} 或 {@code \"vartmp\"}；\n\t * 在 Microsoft Windows 系统上，它通常是 {@code \"C:\\\\WINNT\\\\TEMP\"}。\n\t * 调用 Java 虚拟机时，可以为该系统属性赋予不同的值，但不保证对该属性的编程更改对该方法使用的临时目录有任何影响。\n\t *\n\t * @param prefix    前缀，至少3个字符\n\t * @param suffix    后缀，如果null则使用默认.tmp\n\t * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.22\n\t */\n\tpublic static File createTempFile(String prefix, String suffix, boolean isReCreat) throws IORuntimeException {\n\t\treturn createTempFile(prefix, suffix, null, isReCreat);\n\t}\n\n\t/**\n\t * 创建临时文件<br>\n\t * 创建后的文件名为 prefix[Random].tmp\n\t *\n\t * @param dir       临时文件创建的所在目录\n\t * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File createTempFile(File dir, boolean isReCreat) throws IORuntimeException {\n\t\treturn createTempFile(\"hutool\", null, dir, isReCreat);\n\t}\n\n\t/**\n\t * 创建临时文件<br>\n\t * 创建后的文件名为 prefix[Random].suffix From com.jodd.io.FileUtil\n\t *\n\t * @param prefix    前缀，至少3个字符\n\t * @param suffix    后缀，如果null则使用默认.tmp\n\t * @param dir       临时文件创建的所在目录\n\t * @param isReCreat 是否重新创建文件（删掉原来的，创建新的）\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File createTempFile(String prefix, String suffix, File dir, boolean isReCreat) throws IORuntimeException {\n\t\tint exceptionsCount = 0;\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\t// https://github.com/chinabugotech/hutool/issues/3103\n\t\t\t\t//File file = File.createTempFile(prefix, suffix, mkdir(dir)).getCanonicalFile();\n\t\t\t\tfinal File file = PathUtil.createTempFile(prefix, suffix, null == dir ? null : dir.toPath()).toFile().getCanonicalFile();\n\t\t\t\tif (isReCreat) {\n\t\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\t\tfile.delete();\n\t\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\t\tfile.createNewFile();\n\t\t\t\t}\n\t\t\t\treturn file;\n\t\t\t} catch (IOException ioex) { // fixes java.io.WinNTFileSystem.createFileExclusively access denied\n\t\t\t\tif (++exceptionsCount >= 50) {\n\t\t\t\t\tthrow new IORuntimeException(ioex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 通过JDK7+的 Files#copy(Path, Path, CopyOption...) 方法拷贝文件\n\t *\n\t * @param src     源文件路径\n\t * @param dest    目标文件或目录路径，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return File\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File copyFile(String src, String dest, StandardCopyOption... options) throws IORuntimeException {\n\t\tAssert.notBlank(src, \"Source File path is blank !\");\n\t\tAssert.notBlank(dest, \"Destination File path is blank !\");\n\t\treturn copyFile(Paths.get(src), Paths.get(dest), options).toFile();\n\t}\n\n\t/**\n\t * 通过JDK7+的 Files#copy(InputStream, Path, CopyOption...) 方法拷贝文件\n\t *\n\t * @param src     源文件\n\t * @param dest    目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.27\n\t */\n\tpublic static File copyFile(Resource src, File dest, StandardCopyOption... options) throws IORuntimeException {\n\t\t// check\n\t\tAssert.notNull(src, \"Source File is null !\");\n\t\tAssert.notNull(dest, \"Destination File or directiory is null !\");\n\t\treturn copyFile(src, dest.toPath(), options).toFile();\n\t}\n\n\t/**\n\t * 通过JDK7+的 Files#copy(InputStream, Path, CopyOption...) 方法拷贝文件\n\t *\n\t * @param src     源文件流，使用后不关闭\n\t * @param dest    目标文件，不存在自动创建\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.27\n\t */\n\tpublic static File copyFile(InputStream src, File dest, StandardCopyOption... options) throws IORuntimeException {\n\t\t// check\n\t\tAssert.notNull(src, \"Source File is null !\");\n\t\tAssert.notNull(dest, \"Destination File or directiory is null !\");\n\t\treturn copyFile(src, dest.toPath(), options).toFile();\n\t}\n\n\t/**\n\t * 通过JDK7+的 Files#copy(Path, Path, CopyOption...) 方法拷贝文件\n\t *\n\t * @param src     源文件\n\t * @param dest    目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File copyFile(File src, File dest, StandardCopyOption... options) throws IORuntimeException {\n\t\t// check\n\t\tAssert.notNull(src, \"Source File is null !\");\n\t\tif (false == src.exists()) {\n\t\t\tthrow new IORuntimeException(\"File not exist: \" + src);\n\t\t}\n\t\tAssert.notNull(dest, \"Destination File or directiory is null !\");\n\t\tif (equals(src, dest)) {\n\t\t\tthrow new IORuntimeException(\"Files '{}' and '{}' are equal\", src, dest);\n\t\t}\n\t\treturn copyFile(src.toPath(), dest.toPath(), options).toFile();\n\t}\n\n\t/**\n\t * 复制文件或目录<br>\n\t * 如果目标文件为目录，则将源文件以相同文件名拷贝到目标目录\n\t *\n\t * @param srcPath    源文件或目录\n\t * @param destPath   目标文件或目录，目标不存在会自动创建（目录、文件都创建）\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标目录或文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File copy(String srcPath, String destPath, boolean isOverride) throws IORuntimeException {\n\t\treturn copy(file(srcPath), file(destPath), isOverride);\n\t}\n\n\t/**\n\t * 复制文件或目录<br>\n\t * 情况如下：\n\t *\n\t * <pre>\n\t * 1、src和dest都为目录，则将src目录及其目录下所有文件目录拷贝到dest下\n\t * 2、src和dest都为文件，直接复制，名字为dest\n\t * 3、src为文件，dest为目录，将src拷贝到dest目录下\n\t * </pre>\n\t *\n\t * @param src        源文件\n\t * @param dest       目标文件或目录，目标不存在会自动创建（目录、文件都创建）\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标目录或文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File copy(File src, File dest, boolean isOverride) throws IORuntimeException {\n\t\treturn FileCopier.create(src, dest).setOverride(isOverride).copy();\n\t}\n\n\t/**\n\t * 复制文件或目录<br>\n\t * 情况如下：\n\t *\n\t * <pre>\n\t * 1、src和dest都为目录，则将src下所有文件目录拷贝到dest下\n\t * 2、src和dest都为文件，直接复制，名字为dest\n\t * 3、src为文件，dest为目录，将src拷贝到dest目录下\n\t * </pre>\n\t *\n\t * @param src        源文件\n\t * @param dest       目标文件或目录，目标不存在会自动创建（目录、文件都创建）\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标目录或文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File copyContent(File src, File dest, boolean isOverride) throws IORuntimeException {\n\t\treturn FileCopier.create(src, dest).setCopyContentIfDir(true).setOverride(isOverride).copy();\n\t}\n\n\t/**\n\t * 复制文件或目录<br>\n\t * 情况如下：\n\t *\n\t * <pre>\n\t * 1、src和dest都为目录，则将src下所有文件（不包括子目录）拷贝到dest下\n\t * 2、src和dest都为文件，直接复制，名字为dest\n\t * 3、src为文件，dest为目录，将src拷贝到dest目录下\n\t * </pre>\n\t *\n\t * @param src        源文件\n\t * @param dest       目标文件或目录，目标不存在会自动创建（目录、文件都创建）\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标目录或文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.1.5\n\t */\n\tpublic static File copyFilesFromDir(File src, File dest, boolean isOverride) throws IORuntimeException {\n\t\treturn FileCopier.create(src, dest).setCopyContentIfDir(true).setOnlyCopyFile(true).setOverride(isOverride).copy();\n\t}\n\n\t/**\n\t * 移动文件或者目录\n\t *\n\t * @param src        源文件或者目录\n\t * @param target     目标文件或者目录\n\t * @param isOverride 是否覆盖目标，只有目标为文件才覆盖\n\t * @throws IORuntimeException IO异常\n\t * @see PathUtil#move(Path, Path, boolean)\n\t */\n\tpublic static void move(File src, File target, boolean isOverride) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Src file must be not null!\");\n\t\tAssert.notNull(target, \"target file must be not null!\");\n\t\tmove(src.toPath(), target.toPath(), isOverride);\n\t}\n\n\t/**\n\t * 移动文件或者目录\n\t *\n\t * @param src        源文件或者目录\n\t * @param target     目标文件或者目录\n\t * @param isOverride 是否覆盖目标，只有目标为文件才覆盖\n\t * @throws IORuntimeException IO异常\n\t * @see PathUtil#moveContent(Path, Path, boolean)\n\t * @since 5.7.9\n\t */\n\tpublic static void moveContent(File src, File target, boolean isOverride) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Src file must be not null!\");\n\t\tAssert.notNull(target, \"target file must be not null!\");\n\t\tmoveContent(src.toPath(), target.toPath(), isOverride);\n\t}\n\n\t/**\n\t * 修改文件或目录的文件名，不变更路径，只是简单修改文件名，不保留扩展名。<br>\n\t *\n\t * <pre>\n\t * FileUtil.rename(file, \"aaa.png\", true) xx/xx.png =》xx/aaa.png\n\t * </pre>\n\t *\n\t * @param file       被修改的文件\n\t * @param newName    新的文件名，如需扩展名，需自行在此参数加上，原文件名的扩展名不会被保留\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标文件\n\t * @since 5.3.6\n\t */\n\tpublic static File rename(File file, String newName, boolean isOverride) {\n\t\treturn rename(file, newName, false, isOverride);\n\t}\n\n\t/**\n\t * 修改文件或目录的文件名，不变更路径，只是简单修改文件名<br>\n\t * 重命名有两种模式：<br>\n\t * 1、isRetainExt为true时，保留原扩展名：\n\t *\n\t * <pre>\n\t * FileUtil.rename(file, \"aaa\", true) xx/xx.png =》xx/aaa.png\n\t * </pre>\n\t *\n\t * <p>\n\t * 2、isRetainExt为false时，不保留原扩展名，需要在newName中\n\t *\n\t * <pre>\n\t * FileUtil.rename(file, \"aaa.jpg\", false) xx/xx.png =》xx/aaa.jpg\n\t * </pre>\n\t *\n\t * @param file        被修改的文件\n\t * @param newName     新的文件名，可选是否包括扩展名\n\t * @param isRetainExt 是否保留原文件的扩展名，如果保留，则newName不需要加扩展名\n\t * @param isOverride  是否覆盖目标文件\n\t * @return 目标文件\n\t * @see PathUtil#rename(Path, String, boolean)\n\t * @since 3.0.9\n\t */\n\tpublic static File rename(File file, String newName, boolean isRetainExt, boolean isOverride) {\n\t\tif (isRetainExt) {\n\t\t\tfinal String extName = FileUtil.extName(file);\n\t\t\tif (StrUtil.isNotBlank(extName)) {\n\t\t\t\tnewName = newName.concat(\".\").concat(extName);\n\t\t\t}\n\t\t}\n\t\treturn rename(file.toPath(), newName, isOverride).toFile();\n\t}\n\n\t/**\n\t * 获取规范的绝对路径\n\t *\n\t * @param file 文件\n\t * @return 规范绝对路径，如果传入file为null，返回null\n\t * @since 4.1.4\n\t */\n\tpublic static String getCanonicalPath(File file) {\n\t\tif (null == file) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn file.getCanonicalPath();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取绝对路径<br>\n\t * 此方法不会判定给定路径是否有效（文件或目录存在）\n\t *\n\t * @param path      相对路径\n\t * @param baseClass 相对路径所相对的类\n\t * @return 绝对路径\n\t */\n\tpublic static String getAbsolutePath(String path, Class<?> baseClass) {\n\t\tString normalPath;\n\t\tif (path == null) {\n\t\t\tnormalPath = StrUtil.EMPTY;\n\t\t} else {\n\t\t\tnormalPath = normalize(path);\n\t\t\tif (isAbsolutePath(normalPath)) {\n\t\t\t\t// 给定的路径已经是绝对路径了\n\t\t\t\treturn normalPath;\n\t\t\t}\n\t\t}\n\n\t\t// 相对于ClassPath路径\n\t\tfinal URL url = ResourceUtil.getResource(normalPath, baseClass);\n\t\tif (null != url) {\n\t\t\t// 对于jar中文件包含file:前缀，需要去掉此类前缀，在此做标准化，since 3.0.8 解决中文或空格路径被编码的问题\n\t\t\treturn FileUtil.normalize(URLUtil.getDecodedPath(url));\n\t\t}\n\n\t\t// 如果资源不存在，则返回一个拼接的资源绝对路径\n\t\tfinal String classPath = ClassUtil.getClassPath();\n\t\tif (null == classPath) {\n\t\t\t// throw new NullPointerException(\"ClassPath is null !\");\n\t\t\t// 在jar运行模式中，ClassPath有可能获取不到，此时返回原始相对路径（此时获取的文件为相对工作目录）\n\t\t\treturn normalPath;\n\t\t}\n\n\t\t// 资源不存在的情况下使用标准化路径有问题，使用原始路径拼接后标准化路径\n\t\treturn normalize(classPath.concat(Objects.requireNonNull(normalPath)));\n\t}\n\n\t/**\n\t * 获取绝对路径，相对于ClassPath的目录<br>\n\t * 如果给定就是绝对路径，则返回原路径，原路径把所有\\替换为/<br>\n\t * 兼容Spring风格的路径表示，例如：classpath:config/example.setting也会被识别后转换\n\t *\n\t * @param path 相对路径\n\t * @return 绝对路径\n\t */\n\tpublic static String getAbsolutePath(String path) {\n\t\treturn getAbsolutePath(path, null);\n\t}\n\n\t/**\n\t * 获取标准的绝对路径\n\t *\n\t * @param file 文件\n\t * @return 绝对路径\n\t */\n\tpublic static String getAbsolutePath(File file) {\n\t\tif (file == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\treturn file.getCanonicalPath();\n\t\t} catch (IOException e) {\n\t\t\treturn file.getAbsolutePath();\n\t\t}\n\t}\n\n\t/**\n\t * 给定路径已经是绝对路径<br>\n\t * 此方法并没有针对路径做标准化，建议先执行{@link #normalize(String)}方法标准化路径后判断<br>\n\t * 绝对路径判断条件是：\n\t * <ul>\n\t *     <li>以/开头的路径</li>\n\t *     <li>满足类似于 c:/xxxxx，其中祖母随意，不区分大小写</li>\n\t *     <li>满足类似于 d:\\xxxxx，其中祖母随意，不区分大小写</li>\n\t *     <li>满足windows SMB协议格式，如: \\\\192.168.254.1\\Share</li>\n\t * </ul>\n\t *\n\t * @param path 需要检查的Path\n\t * @return 是否已经是绝对路径\n\t */\n\tpublic static boolean isAbsolutePath(String path) {\n\t\tif (StrUtil.isEmpty(path)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 给定的路径已经是绝对路径了\n\t\treturn StrUtil.C_SLASH == path.charAt(0) || path.startsWith(SMB_PATH_PREFIX) || ReUtil.isMatch(PATTERN_PATH_ABSOLUTE, path);\n\t}\n\n\t/**\n\t * 判断是否为目录，如果path为null，则返回false\n\t *\n\t * @param path 文件路径\n\t * @return 如果为目录true\n\t */\n\tpublic static boolean isDirectory(String path) {\n\t\treturn (null != path) && file(path).isDirectory();\n\t}\n\n\t/**\n\t * 判断是否为目录，如果file为null，则返回false\n\t *\n\t * @param file 文件\n\t * @return 如果为目录true\n\t */\n\tpublic static boolean isDirectory(File file) {\n\t\treturn (null != file) && file.isDirectory();\n\t}\n\n\t/**\n\t * 判断是否为文件，如果path为null，则返回false\n\t *\n\t * @param path 文件路径\n\t * @return 如果为文件true\n\t */\n\tpublic static boolean isFile(String path) {\n\t\treturn (null != path) && file(path).isFile();\n\t}\n\n\t/**\n\t * 判断是否为文件，如果file为null，则返回false\n\t *\n\t * @param file 文件\n\t * @return 如果为文件true\n\t */\n\tpublic static boolean isFile(File file) {\n\t\treturn (null != file) && file.isFile();\n\t}\n\n\t/**\n\t * 检查两个文件是否是同一个文件<br>\n\t * 所谓文件相同，是指File对象是否指向同一个文件或文件夹\n\t *\n\t * @param file1 文件1\n\t * @param file2 文件2\n\t * @return 是否相同\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static boolean equals(File file1, File file2) throws IORuntimeException {\n\t\tAssert.notNull(file1);\n\t\tAssert.notNull(file2);\n\t\tif (false == file1.exists() || false == file2.exists()) {\n\t\t\t// 两个文件都不存在判断其路径是否相同， 对于一个存在一个不存在的情况，一定不相同\n\t\t\treturn false == file1.exists()//\n\t\t\t\t&& false == file2.exists()//\n\t\t\t\t&& pathEquals(file1, file2);\n\t\t}\n\t\treturn equals(file1.toPath(), file2.toPath());\n\t}\n\n\t/**\n\t * 比较两个文件内容是否相同<br>\n\t * 首先比较长度，长度一致再比较内容<br>\n\t * 此方法来自Apache Commons io\n\t *\n\t * @param file1 文件1\n\t * @param file2 文件2\n\t * @return 两个文件内容一致返回true，否则false\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static boolean contentEquals(File file1, File file2) throws IORuntimeException {\n\t\tboolean file1Exists = file1.exists();\n\t\tif (file1Exists != file2.exists()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (false == file1Exists) {\n\t\t\t// 两个文件都不存在，返回true\n\t\t\treturn true;\n\t\t}\n\n\t\tif (file1.isDirectory() || file2.isDirectory()) {\n\t\t\t// 不比较目录\n\t\t\tthrow new IORuntimeException(\"Can't compare directories, only files\");\n\t\t}\n\n\t\tif (file1.length() != file2.length()) {\n\t\t\t// 文件长度不同\n\t\t\treturn false;\n\t\t}\n\n\t\tif (equals(file1, file2)) {\n\t\t\t// 同一个文件\n\t\t\treturn true;\n\t\t}\n\n\t\tInputStream input1 = null;\n\t\tInputStream input2 = null;\n\t\ttry {\n\t\t\tinput1 = getInputStream(file1);\n\t\t\tinput2 = getInputStream(file2);\n\t\t\treturn IoUtil.contentEquals(input1, input2);\n\n\t\t} finally {\n\t\t\tIoUtil.close(input1);\n\t\t\tIoUtil.close(input2);\n\t\t}\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 比较两个文件内容是否相同<br>\n\t * 首先比较长度，长度一致再比较内容，比较内容采用按行读取，每行比较<br>\n\t * 此方法来自Apache Commons io\n\t *\n\t * @param file1   文件1\n\t * @param file2   文件2\n\t * @param charset 编码，null表示使用平台默认编码 两个文件内容一致返回true，否则false\n\t * @return 是否相同\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static boolean contentEqualsIgnoreEOL(File file1, File file2, Charset charset) throws IORuntimeException {\n\t\tboolean file1Exists = file1.exists();\n\t\tif (file1Exists != file2.exists()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!file1Exists) {\n\t\t\t// 两个文件都不存在，返回true\n\t\t\treturn true;\n\t\t}\n\n\t\tif (file1.isDirectory() || file2.isDirectory()) {\n\t\t\t// 不比较目录\n\t\t\tthrow new IORuntimeException(\"Can't compare directories, only files\");\n\t\t}\n\n\t\tif (equals(file1, file2)) {\n\t\t\t// 同一个文件\n\t\t\treturn true;\n\t\t}\n\n\t\tReader input1 = null;\n\t\tReader input2 = null;\n\t\ttry {\n\t\t\tinput1 = getReader(file1, charset);\n\t\t\tinput2 = getReader(file2, charset);\n\t\t\treturn IoUtil.contentEqualsIgnoreEOL(input1, input2);\n\t\t} finally {\n\t\t\tIoUtil.close(input1);\n\t\t\tIoUtil.close(input2);\n\t\t}\n\t}\n\n\t/**\n\t * 文件路径是否相同<br>\n\t * 取两个文件的绝对路径比较，在Windows下忽略大小写，在Linux下不忽略。\n\t *\n\t * @param file1 文件1\n\t * @param file2 文件2\n\t * @return 文件路径是否相同\n\t * @since 3.0.9\n\t */\n\tpublic static boolean pathEquals(File file1, File file2) {\n\t\tif (isWindows()) {\n\t\t\t// Windows环境\n\t\t\ttry {\n\t\t\t\tif (StrUtil.equalsIgnoreCase(file1.getCanonicalPath(), file2.getCanonicalPath())) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (StrUtil.equalsIgnoreCase(file1.getAbsolutePath(), file2.getAbsolutePath())) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 类Unix环境\n\t\t\ttry {\n\t\t\t\tif (StrUtil.equals(file1.getCanonicalPath(), file2.getCanonicalPath())) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tif (StrUtil.equals(file1.getAbsolutePath(), file2.getAbsolutePath())) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获得最后一个文件路径分隔符的位置\n\t *\n\t * @param filePath 文件路径\n\t * @return 最后一个文件路径分隔符的位置\n\t */\n\tpublic static int lastIndexOfSeparator(String filePath) {\n\t\tif (StrUtil.isNotEmpty(filePath)) {\n\t\t\tint i = filePath.length();\n\t\t\tchar c;\n\t\t\twhile (--i >= 0) {\n\t\t\t\tc = filePath.charAt(i);\n\t\t\t\tif (CharUtil.isFileSeparator(c)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 判断文件是否被改动<br>\n\t * 如果文件对象为 null 或者文件不存在，被视为改动\n\t *\n\t * @param file           文件对象\n\t * @param lastModifyTime 上次的改动时间\n\t * @return 是否被改动\n\t * @deprecated 拼写错误，请使用{@link #isModified(File, long)}\n\t */\n\t@Deprecated\n\tpublic static boolean isModifed(File file, long lastModifyTime) {\n\t\treturn isModified(file, lastModifyTime);\n\t}\n\n\n\t/**\n\t * 判断文件是否被改动<br>\n\t * 如果文件对象为 null 或者文件不存在，被视为改动\n\t *\n\t * @param file           文件对象\n\t * @param lastModifyTime 上次的改动时间\n\t * @return 是否被改动\n\t */\n\tpublic static boolean isModified(File file, long lastModifyTime) {\n\t\tif (null == file || false == file.exists()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn file.lastModified() != lastModifyTime;\n\t}\n\n\t/**\n\t * 修复路径<br>\n\t * 如果原路径尾部有分隔符，则保留为标准分隔符（/），否则不保留\n\t * <ol>\n\t * <li>1. 统一用 /</li>\n\t * <li>2. 多个 / 转换为一个 /</li>\n\t * <li>3. 去除左边空格</li>\n\t * <li>4. .. 和 . 转换为绝对路径，当..多于已有路径时，直接返回根路径</li>\n\t * </ol>\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * \"/foo//\" =》 \"/foo/\"\n\t * \"/foo/./\" =》 \"/foo/\"\n\t * \"/foo/../bar\" =》 \"/bar\"\n\t * \"/foo/../bar/\" =》 \"/bar/\"\n\t * \"/foo/../bar/../baz\" =》 \"/baz\"\n\t * \"/../\" =》 \"/\"\n\t * \"foo/bar/..\" =》 \"foo\"\n\t * \"foo/../bar\" =》 \"bar\"\n\t * \"foo/../../bar\" =》 \"bar\"\n\t * \"//server/foo/../bar\" =》 \"/server/bar\"\n\t * \"//server/../bar\" =》 \"/bar\"\n\t * \"C:\\\\foo\\\\..\\\\bar\" =》 \"C:/bar\"\n\t * \"C:\\\\..\\\\bar\" =》 \"C:/bar\"\n\t * \"~/foo/../bar/\" =》 \"~/bar/\"\n\t * \"~/../bar\" =》 普通用户运行是'bar的home目录'，ROOT用户运行是'/bar'\n\t * </pre>\n\t *\n\t * @param path 原路径\n\t * @return 修复后的路径\n\t */\n\tpublic static String normalize(String path) {\n\t\tif (path == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t//兼容Windows下的共享目录路径（原始路径如果以\\\\开头，则保留这种路径）\n\t\tif (path.startsWith(SMB_PATH_PREFIX)) {\n\t\t\treturn path;\n\t\t}\n\n\t\t// 兼容Spring风格的ClassPath路径，去除前缀，不区分大小写\n\t\tString pathToUse = StrUtil.removePrefixIgnoreCase(path, URLUtil.CLASSPATH_URL_PREFIX);\n\t\t// 去除file:前缀\n\t\tpathToUse = StrUtil.removePrefixIgnoreCase(pathToUse, URLUtil.FILE_URL_PREFIX);\n\n\t\t// 识别home目录形式，并转换为绝对路径\n\t\tif (StrUtil.startWith(pathToUse, '~')) {\n\t\t\tpathToUse = getUserHomePath() + pathToUse.substring(1);\n\t\t}\n\n\t\t// 统一使用斜杠\n\t\tpathToUse = pathToUse.replaceAll(\"[/\\\\\\\\]+\", StrUtil.SLASH);\n\t\t// 去除开头空白符，末尾空白符合法，不去除\n\t\tpathToUse = StrUtil.trimStart(pathToUse);\n\t\t// issue#IAB65V 去除尾部的换行符\n\t\tpathToUse = StrUtil.trim(pathToUse, 1, (c)->c == '\\n' || c == '\\r');\n\n\t\tString prefix = StrUtil.EMPTY;\n\t\tint prefixIndex = pathToUse.indexOf(StrUtil.COLON);\n\t\tif (prefixIndex > -1) {\n\t\t\t// 可能Windows风格路径\n\t\t\tprefix = pathToUse.substring(0, prefixIndex + 1);\n\t\t\tif (StrUtil.startWith(prefix, StrUtil.C_SLASH)) {\n\t\t\t\t// 去除类似于/C:这类路径开头的斜杠\n\t\t\t\tprefix = prefix.substring(1);\n\t\t\t}\n\t\t\tif (false == prefix.contains(StrUtil.SLASH)) {\n\t\t\t\tpathToUse = pathToUse.substring(prefixIndex + 1);\n\t\t\t} else {\n\t\t\t\t// 如果前缀中包含/,说明非Windows风格path\n\t\t\t\tprefix = StrUtil.EMPTY;\n\t\t\t}\n\t\t}\n\t\tif (pathToUse.startsWith(StrUtil.SLASH)) {\n\t\t\tprefix += StrUtil.SLASH;\n\t\t\tpathToUse = pathToUse.substring(1);\n\t\t}\n\n\t\tList<String> pathList = StrUtil.split(pathToUse, StrUtil.C_SLASH);\n\n\t\tList<String> pathElements = new LinkedList<>();\n\t\tint tops = 0;\n\t\tString element;\n\t\tfor (int i = pathList.size() - 1; i >= 0; i--) {\n\t\t\telement = pathList.get(i);\n\t\t\t// 只处理非.的目录，即只处理非当前目录\n\t\t\tif (false == StrUtil.DOT.equals(element)) {\n\t\t\t\tif (StrUtil.DOUBLE_DOT.equals(element)) {\n\t\t\t\t\ttops++;\n\t\t\t\t} else {\n\t\t\t\t\tif (tops > 0) {\n\t\t\t\t\t\t// 有上级目录标记时按照个数依次跳过\n\t\t\t\t\t\ttops--;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Normal path element found.\n\t\t\t\t\t\tpathElements.add(0, element);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// issue#1703@Github\n\t\tif (tops > 0 && StrUtil.isEmpty(prefix)) {\n\t\t\t// 只有相对路径补充开头的..，绝对路径直接忽略之\n\t\t\twhile (tops-- > 0) {\n\t\t\t\t//遍历完节点发现还有上级标注（即开头有一个或多个..），补充之\n\t\t\t\t// Normal path element found.\n\t\t\t\tpathElements.add(0, StrUtil.DOUBLE_DOT);\n\t\t\t}\n\t\t}\n\n\t\treturn prefix + CollUtil.join(pathElements, StrUtil.SLASH);\n\t}\n\n\t/**\n\t * 获得相对子路径\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * dirPath: d:/aaa/bbb    filePath: d:/aaa/bbb/ccc     =》    ccc\n\t * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/ccc.txt     =》    ccc.txt\n\t * </pre>\n\t *\n\t * @param rootDir 绝对父路径\n\t * @param file    文件\n\t * @return 相对子路径\n\t */\n\tpublic static String subPath(String rootDir, File file) {\n\t\ttry {\n\t\t\treturn subPath(rootDir, file.getCanonicalPath());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得相对子路径，忽略大小写\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * dirPath: d:/aaa/bbb    filePath: d:/aaa/bbb/ccc     =》    ccc\n\t * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/ccc.txt     =》    ccc.txt\n\t * dirPath: d:/Aaa/bbb    filePath: d:/aaa/bbb/     =》    \"\"\n\t * </pre>\n\t *\n\t * @param dirPath  父路径\n\t * @param filePath 文件路径\n\t * @return 相对子路径\n\t */\n\tpublic static String subPath(String dirPath, String filePath) {\n\t\tif (StrUtil.isNotEmpty(dirPath) && StrUtil.isNotEmpty(filePath)) {\n\n\t\t\tdirPath = StrUtil.removeSuffix(normalize(dirPath), \"/\");\n\t\t\tfilePath = normalize(filePath);\n\n\t\t\tfinal String result = StrUtil.removePrefixIgnoreCase(filePath, dirPath);\n\t\t\treturn StrUtil.removePrefix(result, \"/\");\n\t\t}\n\t\treturn filePath;\n\t}\n\n\t// -------------------------------------------------------------------------------------------- name start\n\n\t/**\n\t * 返回文件名\n\t *\n\t * @param file 文件\n\t * @return 文件名\n\t * @see FileNameUtil#getName(File)\n\t * @since 4.1.13\n\t */\n\tpublic static String getName(File file) {\n\t\treturn FileNameUtil.getName(file);\n\t}\n\n\t/**\n\t * 返回文件名<br>\n\t * <pre>\n\t * \"d:/test/aaa\" 返回 \"aaa\"\n\t * \"/test/aaa.jpg\" 返回 \"aaa.jpg\"\n\t * </pre>\n\t *\n\t * @param filePath 文件\n\t * @return 文件名\n\t * @see FileNameUtil#getName(String)\n\t * @since 4.1.13\n\t */\n\tpublic static String getName(String filePath) {\n\t\treturn FileNameUtil.getName(filePath);\n\t}\n\n\t/**\n\t * 获取文件后缀名，扩展名不带“.”\n\t *\n\t * @param file 文件\n\t * @return 扩展名\n\t * @see FileNameUtil#getSuffix(File)\n\t * @since 5.3.8\n\t */\n\tpublic static String getSuffix(File file) {\n\t\treturn FileNameUtil.getSuffix(file);\n\t}\n\n\t/**\n\t * 获得文件后缀名，扩展名不带“.”\n\t *\n\t * @param fileName 文件名\n\t * @return 扩展名\n\t * @see FileNameUtil#getSuffix(String)\n\t * @since 5.3.8\n\t */\n\tpublic static String getSuffix(String fileName) {\n\t\treturn FileNameUtil.getSuffix(fileName);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param file 文件\n\t * @return 主文件名\n\t * @see FileNameUtil#getPrefix(File)\n\t * @since 5.3.8\n\t */\n\tpublic static String getPrefix(File file) {\n\t\treturn FileNameUtil.getPrefix(file);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param fileName 完整文件名\n\t * @return 主文件名\n\t * @see FileNameUtil#getPrefix(String)\n\t * @since 5.3.8\n\t */\n\tpublic static String getPrefix(String fileName) {\n\t\treturn FileNameUtil.getPrefix(fileName);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param file 文件\n\t * @return 主文件名\n\t * @see FileNameUtil#mainName(File)\n\t */\n\tpublic static String mainName(File file) {\n\t\treturn FileNameUtil.mainName(file);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param fileName 完整文件名\n\t * @return 主文件名\n\t * @see FileNameUtil#mainName(String)\n\t */\n\tpublic static String mainName(String fileName) {\n\t\treturn FileNameUtil.mainName(fileName);\n\t}\n\n\t/**\n\t * 获取文件扩展名（后缀名），扩展名不带“.”\n\t *\n\t * @param file 文件\n\t * @return 扩展名\n\t * @see FileNameUtil#extName(File)\n\t */\n\tpublic static String extName(File file) {\n\t\treturn FileNameUtil.extName(file);\n\t}\n\n\t/**\n\t * 获得文件的扩展名（后缀名），扩展名不带“.”\n\t *\n\t * @param fileName 文件名\n\t * @return 扩展名\n\t * @see FileNameUtil#extName(String)\n\t */\n\tpublic static String extName(String fileName) {\n\t\treturn FileNameUtil.extName(fileName);\n\t}\n\t// -------------------------------------------------------------------------------------------- name end\n\n\t/**\n\t * 判断文件路径是否有指定后缀，忽略大小写<br>\n\t * 常用语判断扩展名\n\t *\n\t * @param file   文件或目录\n\t * @param suffix 后缀\n\t * @return 是否有指定后缀\n\t */\n\tpublic static boolean pathEndsWith(File file, String suffix) {\n\t\treturn file.getPath().toLowerCase().endsWith(suffix);\n\t}\n\n\t/**\n\t * 根据文件流的头部信息获得文件类型\n\t *\n\t * <pre>\n\t *      1、无法识别类型默认按照扩展名识别\n\t *      2、xls、doc、msi头信息无法区分，按照扩展名区分\n\t *      3、zip可能为docx、xlsx、pptx、jar、war头信息无法区分，按照扩展名区分\n\t * </pre>\n\t *\n\t * @param file 文件 {@link File}\n\t * @return 类型，文件的扩展名，未找到为{@code null}\n\t * @throws IORuntimeException IO异常\n\t * @see FileTypeUtil#getType(File)\n\t */\n\tpublic static String getType(File file) throws IORuntimeException {\n\t\treturn FileTypeUtil.getType(file);\n\t}\n\n\t// -------------------------------------------------------------------------------------------- in start\n\n\t/**\n\t * 获得输入流\n\t *\n\t * @param file 文件\n\t * @return 输入流\n\t * @throws IORuntimeException 文件未找到\n\t */\n\tpublic static BufferedInputStream getInputStream(File file) throws IORuntimeException {\n\t\treturn IoUtil.toBuffered(IoUtil.toStream(file));\n\t}\n\n\t/**\n\t * 获得输入流\n\t *\n\t * @param path 文件路径\n\t * @return 输入流\n\t * @throws IORuntimeException 文件未找到\n\t */\n\tpublic static BufferedInputStream getInputStream(String path) throws IORuntimeException {\n\t\treturn getInputStream(file(path));\n\t}\n\n\t/**\n\t * 获得BOM输入流，用于处理带BOM头的文件\n\t *\n\t * @param file 文件\n\t * @return 输入流\n\t * @throws IORuntimeException 文件未找到\n\t */\n\tpublic static BOMInputStream getBOMInputStream(File file) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn new BOMInputStream(Files.newInputStream(file.toPath()));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取带BOM头的文件为Reader\n\t *\n\t * @param file 文件\n\t * @return BufferedReader对象\n\t * @since 5.5.8\n\t */\n\tpublic static BufferedReader getBOMReader(File file) {\n\t\treturn IoUtil.getReader(getBOMInputStream(file));\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param file 文件\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedReader getUtf8Reader(File file) throws IORuntimeException {\n\t\treturn getReader(file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param path 文件路径\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedReader getUtf8Reader(String path) throws IORuntimeException {\n\t\treturn getReader(path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param file        文件\n\t * @param charsetName 字符集\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #getReader(File, Charset)}\n\t */\n\t@Deprecated\n\tpublic static BufferedReader getReader(File file, String charsetName) throws IORuntimeException {\n\t\treturn IoUtil.getReader(getInputStream(file), CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedReader getReader(File file, Charset charset) throws IORuntimeException {\n\t\treturn IoUtil.getReader(getInputStream(file), charset);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param path        绝对路径\n\t * @param charsetName 字符集\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #getReader(String, Charset)}\n\t */\n\t@Deprecated\n\tpublic static BufferedReader getReader(String path, String charsetName) throws IORuntimeException {\n\t\treturn getReader(path, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param path    绝对路径\n\t * @param charset 字符集\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedReader getReader(String path, Charset charset) throws IORuntimeException {\n\t\treturn getReader(file(path), charset);\n\t}\n\n\t// -------------------------------------------------------------------------------------------- in end\n\n\t/**\n\t * 读取文件所有数据<br>\n\t * 文件的长度不能超过Integer.MAX_VALUE\n\t *\n\t * @param file 文件\n\t * @return 字节码\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static byte[] readBytes(File file) throws IORuntimeException {\n\t\treturn FileReader.create(file).readBytes();\n\t}\n\n\t/**\n\t * 读取文件所有数据<br>\n\t * 文件的长度不能超过Integer.MAX_VALUE\n\t *\n\t * @param filePath 文件路径\n\t * @return 字节码\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.0\n\t */\n\tpublic static byte[] readBytes(String filePath) throws IORuntimeException {\n\t\treturn readBytes(file(filePath));\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param file 文件\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readUtf8String(File file) throws IORuntimeException {\n\t\treturn readString(file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param path 文件路径\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readUtf8String(String path) throws IORuntimeException {\n\t\treturn readString(path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param file        文件\n\t * @param charsetName 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readString(File, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String readString(File file, String charsetName) throws IORuntimeException {\n\t\treturn readString(file, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readString(File file, Charset charset) throws IORuntimeException {\n\t\treturn FileReader.create(file, charset).readString();\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param path        文件路径\n\t * @param charsetName 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readString(String, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String readString(String path, String charsetName) throws IORuntimeException {\n\t\treturn readString(path, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readString(String path, Charset charset) throws IORuntimeException {\n\t\treturn readString(file(path), charset);\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param url         文件URL\n\t * @param charsetName 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readString(URL, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String readString(URL url, String charsetName) throws IORuntimeException {\n\t\treturn readString(url, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @param url     文件URL\n\t * @param charset 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.10\n\t */\n\tpublic static String readString(URL url, Charset charset) throws IORuntimeException {\n\t\tif (url == null) {\n\t\t\tthrow new NullPointerException(\"Empty url provided!\");\n\t\t}\n\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = url.openStream();\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 从文件中读取每一行的UTF-8编码数据\n\t *\n\t * @param <T>        集合类型\n\t * @param path       文件路径\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T extends Collection<String>> T readUtf8Lines(String path, T collection) throws IORuntimeException {\n\t\treturn readLines(path, CharsetUtil.CHARSET_UTF_8, collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>        集合类型\n\t * @param path       文件路径\n\t * @param charset    字符集\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(String path, String charset, T collection) throws IORuntimeException {\n\t\treturn readLines(file(path), charset, collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>        集合类型\n\t * @param path       文件路径\n\t * @param charset    字符集\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(String path, Charset charset, T collection) throws IORuntimeException {\n\t\treturn readLines(file(path), charset, collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据，数据编码为UTF-8\n\t *\n\t * @param <T>        集合类型\n\t * @param file       文件路径\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T extends Collection<String>> T readUtf8Lines(File file, T collection) throws IORuntimeException {\n\t\treturn readLines(file, CharsetUtil.CHARSET_UTF_8, collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>        集合类型\n\t * @param file       文件路径\n\t * @param charset    字符集\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(File file, String charset, T collection) throws IORuntimeException {\n\t\treturn FileReader.create(file, CharsetUtil.charset(charset)).readLines(collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>        集合类型\n\t * @param file       文件路径\n\t * @param charset    字符集\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(File file, Charset charset, T collection) throws IORuntimeException {\n\t\treturn FileReader.create(file, charset).readLines(collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据，编码为UTF-8\n\t *\n\t * @param <T>        集合类型\n\t * @param url        文件的URL\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readUtf8Lines(URL url, T collection) throws IORuntimeException {\n\t\treturn readLines(url, CharsetUtil.CHARSET_UTF_8, collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>         集合类型\n\t * @param url         文件的URL\n\t * @param charsetName 字符集\n\t * @param collection  集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readLines(URL, Charset, Collection)}\n\t */\n\t@Deprecated\n\tpublic static <T extends Collection<String>> T readLines(URL url, String charsetName, T collection) throws IORuntimeException {\n\t\treturn readLines(url, CharsetUtil.charset(charsetName), collection);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T>        集合类型\n\t * @param url        文件的URL\n\t * @param charset    字符集\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T extends Collection<String>> T readLines(URL url, Charset charset, T collection) throws IORuntimeException {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = url.openStream();\n\t\t\treturn IoUtil.readLines(in, charset, collection);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param url 文件的URL\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readUtf8Lines(URL url) throws IORuntimeException {\n\t\treturn readLines(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param url         文件的URL\n\t * @param charsetName 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readLines(URL, Charset)}\n\t */\n\t@Deprecated\n\tpublic static List<String> readLines(URL url, String charsetName) throws IORuntimeException {\n\t\treturn readLines(url, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param url     文件的URL\n\t * @param charset 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readLines(URL url, Charset charset) throws IORuntimeException {\n\t\treturn readLines(url, charset, new ArrayList<>());\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据，编码为UTF-8\n\t *\n\t * @param path 文件路径\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static List<String> readUtf8Lines(String path) throws IORuntimeException {\n\t\treturn readLines(path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readLines(String path, String charset) throws IORuntimeException {\n\t\treturn readLines(path, charset, new ArrayList<>());\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static List<String> readLines(String path, Charset charset) throws IORuntimeException {\n\t\treturn readLines(path, charset, new ArrayList<>());\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param file 文件\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static List<String> readUtf8Lines(File file) throws IORuntimeException {\n\t\treturn readLines(file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param file   文件\n\t * @param filter 过滤器\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static List<String> readUtf8Lines(File file, Predicate<String> filter) throws IORuntimeException {\n\t\treturn readLines(file, CharsetUtil.CHARSET_UTF_8, filter);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readLines(File file, String charset) throws IORuntimeException {\n\t\treturn readLines(file, charset, new ArrayList<>());\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readLines(File file, Charset charset) throws IORuntimeException {\n\t\treturn readLines(file, charset, new ArrayList<>());\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @param filter  过滤器\n\t * @return 文件中的每行内容的集合List\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> readLines(File file, Charset charset, Predicate<String> filter) throws IORuntimeException {\n\t\tfinal List<String> result = new ArrayList<>();\n\t\treadLines(file, charset, (LineHandler) line -> {\n\t\t\tif (filter.test(line)) {\n\t\t\t\tresult.add(line);\n\t\t\t}\n\t\t});\n\t\treturn result;\n\t}\n\n\t/**\n\t * 按行处理文件内容，编码为UTF-8\n\t *\n\t * @param file        文件\n\t * @param lineHandler {@link LineHandler}行处理器\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void readUtf8Lines(File file, LineHandler lineHandler) throws IORuntimeException {\n\t\treadLines(file, CharsetUtil.CHARSET_UTF_8, lineHandler);\n\t}\n\n\t/**\n\t * 按行处理文件内容\n\t *\n\t * @param file        文件\n\t * @param charset     编码\n\t * @param lineHandler {@link LineHandler}行处理器\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void readLines(File file, Charset charset, LineHandler lineHandler) throws IORuntimeException {\n\t\tFileReader.create(file, charset).readLines(lineHandler);\n\t}\n\n\t/**\n\t * 按行处理文件内容\n\t *\n\t * @param file        {@link RandomAccessFile}文件\n\t * @param charset     编码\n\t * @param lineHandler {@link LineHandler}行处理器\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.2\n\t */\n\tpublic static void readLines(RandomAccessFile file, Charset charset, LineHandler lineHandler) {\n\t\tString line;\n\t\ttry {\n\t\t\twhile ((line = file.readLine()) != null) {\n\t\t\t\tlineHandler.handle(CharsetUtil.convert(line, CharsetUtil.CHARSET_ISO_8859_1, charset));\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 单行处理文件内容\n\t *\n\t * @param file        {@link RandomAccessFile}文件\n\t * @param charset     编码\n\t * @param lineHandler {@link LineHandler}行处理器\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.2\n\t */\n\tpublic static void readLine(RandomAccessFile file, Charset charset, LineHandler lineHandler) {\n\t\tfinal String line = readLine(file, charset);\n\t\tif (null != line) {\n\t\t\tlineHandler.handle(line);\n\t\t}\n\t}\n\n\t/**\n\t * 单行处理文件内容\n\t *\n\t * @param file    {@link RandomAccessFile}文件\n\t * @param charset 编码\n\t * @return 行内容\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.18\n\t */\n\tpublic static String readLine(RandomAccessFile file, Charset charset) {\n\t\tString line;\n\t\ttry {\n\t\t\tline = file.readLine();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tif (null != line) {\n\t\t\treturn CharsetUtil.convert(line, CharsetUtil.CHARSET_ISO_8859_1, charset);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T>           集合类型\n\t * @param readerHandler Reader处理类\n\t * @param path          文件的绝对路径\n\t * @return 从文件中load出的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T> T loadUtf8(String path, ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\treturn load(path, CharsetUtil.CHARSET_UTF_8, readerHandler);\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T>           集合类型\n\t * @param readerHandler Reader处理类\n\t * @param path          文件的绝对路径\n\t * @param charset       字符集\n\t * @return 从文件中load出的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T> T load(String path, String charset, ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\treturn FileReader.create(file(path), CharsetUtil.charset(charset)).read(readerHandler);\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T>           集合类型\n\t * @param readerHandler Reader处理类\n\t * @param path          文件的绝对路径\n\t * @param charset       字符集\n\t * @return 从文件中load出的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T> T load(String path, Charset charset, ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\treturn FileReader.create(file(path), charset).read(readerHandler);\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T>           集合类型\n\t * @param readerHandler Reader处理类\n\t * @param file          文件\n\t * @return 从文件中load出的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T> T loadUtf8(File file, ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\treturn load(file, CharsetUtil.CHARSET_UTF_8, readerHandler);\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T>           集合类型\n\t * @param readerHandler Reader处理类\n\t * @param file          文件\n\t * @param charset       字符集\n\t * @return 从文件中load出的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static <T> T load(File file, Charset charset, ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\treturn FileReader.create(file, charset).read(readerHandler);\n\t}\n\n\t// -------------------------------------------------------------------------------------------- out start\n\n\t/**\n\t * 获得一个输出流对象\n\t *\n\t * @param file 文件\n\t * @return 输出流对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedOutputStream getOutputStream(File file) throws IORuntimeException {\n\t\tfinal OutputStream out;\n\t\ttry {\n\t\t\tout = Files.newOutputStream(touch(file).toPath());\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn IoUtil.toBuffered(out);\n\t}\n\n\t/**\n\t * 获得一个输出流对象\n\t *\n\t * @param path 输出到的文件路径，绝对路径\n\t * @return 输出流对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedOutputStream getOutputStream(String path) throws IORuntimeException {\n\t\treturn getOutputStream(touch(path));\n\t}\n\n\t/**\n\t * 获得一个带缓存的写入对象\n\t *\n\t * @param path        输出路径，绝对路径\n\t * @param charsetName 字符集\n\t * @param isAppend    是否追加\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #getWriter(String, Charset, boolean)}\n\t */\n\t@Deprecated\n\tpublic static BufferedWriter getWriter(String path, String charsetName, boolean isAppend) throws IORuntimeException {\n\t\treturn getWriter(path, Charset.forName(charsetName), isAppend);\n\t}\n\n\t/**\n\t * 获得一个带缓存的写入对象\n\t *\n\t * @param path     输出路径，绝对路径\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedWriter getWriter(String path, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn getWriter(touch(path), charset, isAppend);\n\t}\n\n\t/**\n\t * 获得一个带缓存的写入对象\n\t *\n\t * @param file        输出文件\n\t * @param charsetName 字符集\n\t * @param isAppend    是否追加\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #getWriter(File, Charset, boolean)}\n\t */\n\t@Deprecated\n\tpublic static BufferedWriter getWriter(File file, String charsetName, boolean isAppend) throws IORuntimeException {\n\t\treturn getWriter(file, Charset.forName(charsetName), isAppend);\n\t}\n\n\t/**\n\t * 获得一个带缓存的写入对象\n\t *\n\t * @param file     输出文件\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BufferedWriter getWriter(File file, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(file, charset).getWriter(isAppend);\n\t}\n\n\t/**\n\t * 获得一个打印写入对象，可以有print\n\t *\n\t * @param path     输出路径，绝对路径\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 打印对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static PrintWriter getPrintWriter(String path, String charset, boolean isAppend) throws IORuntimeException {\n\t\treturn new PrintWriter(getWriter(path, charset, isAppend));\n\t}\n\n\t/**\n\t * 获得一个打印写入对象，可以有print\n\t *\n\t * @param path     输出路径，绝对路径\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 打印对象\n\t * @throws IORuntimeException IO异常\n\t * @since 4.1.1\n\t */\n\tpublic static PrintWriter getPrintWriter(String path, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn new PrintWriter(getWriter(path, charset, isAppend));\n\t}\n\n\t/**\n\t * 获得一个打印写入对象，可以有print\n\t *\n\t * @param file     文件\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 打印对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static PrintWriter getPrintWriter(File file, String charset, boolean isAppend) throws IORuntimeException {\n\t\treturn new PrintWriter(getWriter(file, charset, isAppend));\n\t}\n\n\t/**\n\t * 获得一个打印写入对象，可以有print\n\t *\n\t * @param file     文件\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 打印对象\n\t * @throws IORuntimeException IO异常\n\t * @since 5.4.3\n\t */\n\tpublic static PrintWriter getPrintWriter(File file, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn new PrintWriter(getWriter(file, charset, isAppend));\n\t}\n\n\t/**\n\t * 获取当前系统的换行分隔符\n\t *\n\t * <pre>\n\t * Windows: \\r\\n\n\t * Mac: \\r\n\t * Linux: \\n\n\t * </pre>\n\t *\n\t * @return 换行符\n\t * @since 4.0.5\n\t */\n\tpublic static String getLineSeparator() {\n\t\treturn System.lineSeparator();\n\t\t// return System.getProperty(\"line.separator\");\n\t}\n\n\t// -------------------------------------------------------------------------------------------- out end\n\n\t/**\n\t * 将String写入文件，覆盖模式，字符集为UTF-8\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeUtf8String(String content, String path) throws IORuntimeException {\n\t\treturn writeString(content, path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式，字符集为UTF-8\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeUtf8String(String content, File file) throws IORuntimeException {\n\t\treturn writeString(content, file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeString(String content, String path, String charset) throws IORuntimeException {\n\t\treturn writeString(content, touch(path), charset);\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeString(String content, String path, Charset charset) throws IORuntimeException {\n\t\treturn writeString(content, touch(path), charset);\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 被写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeString(String content, File file, String charset) throws IORuntimeException {\n\t\treturn FileWriter.create(file, CharsetUtil.charset(charset)).write(content);\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 被写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeString(String content, File file, Charset charset) throws IORuntimeException {\n\t\treturn FileWriter.create(file, charset).write(content);\n\t}\n\n\t/**\n\t * 将String写入文件，UTF-8编码追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static File appendUtf8String(String content, String path) throws IORuntimeException {\n\t\treturn appendString(content, path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将String写入文件，追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File appendString(String content, String path, String charset) throws IORuntimeException {\n\t\treturn appendString(content, touch(path), charset);\n\t}\n\n\t/**\n\t * 将String写入文件，追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param path    文件路径\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File appendString(String content, String path, Charset charset) throws IORuntimeException {\n\t\treturn appendString(content, touch(path), charset);\n\t}\n\n\t/**\n\t * 将String写入文件，UTF-8编码追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static File appendUtf8String(String content, File file) throws IORuntimeException {\n\t\treturn appendString(content, file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将String写入文件，追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File appendString(String content, File file, String charset) throws IORuntimeException {\n\t\treturn FileWriter.create(file, CharsetUtil.charset(charset)).append(content);\n\t}\n\n\t/**\n\t * 将String写入文件，追加模式\n\t *\n\t * @param content 写入的内容\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File appendString(String content, File file, Charset charset) throws IORuntimeException {\n\t\treturn FileWriter.create(file, charset).append(content);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式，编码为UTF-8\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @param path 绝对路径\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.0\n\t */\n\tpublic static <T> File writeUtf8Lines(Collection<T> list, String path) throws IORuntimeException {\n\t\treturn writeLines(list, path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式，编码为UTF-8\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @param file 绝对路径\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.0\n\t */\n\tpublic static <T> File writeUtf8Lines(Collection<T> list, File file) throws IORuntimeException {\n\t\treturn writeLines(list, file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param path    绝对路径\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, String path, String charset) throws IORuntimeException {\n\t\treturn writeLines(list, path, charset, false);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param path    绝对路径\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, String path, Charset charset) throws IORuntimeException {\n\t\treturn writeLines(list, path, charset, false);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.2.0\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, File file, String charset) throws IORuntimeException {\n\t\treturn writeLines(list, file, charset, false);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.2.0\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, File file, Charset charset) throws IORuntimeException {\n\t\treturn writeLines(list, file, charset, false);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @param file 文件\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static <T> File appendUtf8Lines(Collection<T> list, File file) throws IORuntimeException {\n\t\treturn appendLines(list, file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @param path 文件路径\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static <T> File appendUtf8Lines(Collection<T> list, String path) throws IORuntimeException {\n\t\treturn appendLines(list, path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param path    绝对路径\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File appendLines(Collection<T> list, String path, String charset) throws IORuntimeException {\n\t\treturn writeLines(list, path, charset, true);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static <T> File appendLines(Collection<T> list, File file, String charset) throws IORuntimeException {\n\t\treturn writeLines(list, file, charset, true);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param path    绝对路径\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File appendLines(Collection<T> list, String path, Charset charset) throws IORuntimeException {\n\t\treturn writeLines(list, path, charset, true);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式，策略为：\n\t * <ul>\n\t *     <li>当文件为空，从开头追加，尾部不加空行</li>\n\t *     <li>当有内容，换行追加，尾部不加空行</li>\n\t *     <li>当有内容，并末尾有空行，依旧换行追加</li>\n\t * </ul>\n\t *\n\t * @param <T>     集合元素类型\n\t * @param list    列表\n\t * @param file    文件\n\t * @param charset 字符集\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static <T> File appendLines(Collection<T> list, File file, Charset charset) throws IORuntimeException {\n\t\treturn writeLines(list, file, charset, true);\n\t}\n\n\t/**\n\t * 将列表写入文件\n\t *\n\t * @param <T>      集合元素类型\n\t * @param list     列表\n\t * @param path     文件路径\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, String path, String charset, boolean isAppend) throws IORuntimeException {\n\t\treturn writeLines(list, file(path), charset, isAppend);\n\t}\n\n\t/**\n\t * 将列表写入文件\n\t *\n\t * @param <T>      集合元素类型\n\t * @param list     列表\n\t * @param path     文件路径\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, String path, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn writeLines(list, file(path), charset, isAppend);\n\t}\n\n\t/**\n\t * 将列表写入文件\n\t *\n\t * @param <T>      集合元素类型\n\t * @param list     列表\n\t * @param file     文件\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, File file, String charset, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(file, CharsetUtil.charset(charset)).writeLines(list, isAppend);\n\t}\n\n\t/**\n\t * 将列表写入文件\n\t *\n\t * @param <T>      集合元素类型\n\t * @param list     列表\n\t * @param file     文件\n\t * @param charset  字符集\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T> File writeLines(Collection<T> list, File file, Charset charset, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(file, charset).writeLines(list, isAppend);\n\t}\n\n\t/**\n\t * 将Map写入文件，每个键值对为一行，一行中键与值之间使用kvSeparator分隔\n\t *\n\t * @param map         Map\n\t * @param file        文件\n\t * @param kvSeparator 键和值之间的分隔符，如果传入null使用默认分隔符\" = \"\n\t * @param isAppend    是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.5\n\t */\n\tpublic static File writeUtf8Map(Map<?, ?> map, File file, String kvSeparator, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(file, CharsetUtil.CHARSET_UTF_8).writeMap(map, kvSeparator, isAppend);\n\t}\n\n\t/**\n\t * 将Map写入文件，每个键值对为一行，一行中键与值之间使用kvSeparator分隔\n\t *\n\t * @param map         Map\n\t * @param file        文件\n\t * @param charset     字符集编码\n\t * @param kvSeparator 键和值之间的分隔符，如果传入null使用默认分隔符\" = \"\n\t * @param isAppend    是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.5\n\t */\n\tpublic static File writeMap(Map<?, ?> map, File file, Charset charset, String kvSeparator, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(file, charset).writeMap(map, kvSeparator, isAppend);\n\t}\n\n\t/**\n\t * 写数据到文件中<br>\n\t * 文件路径如果是相对路径，则相对ClassPath\n\t *\n\t * @param data 数据\n\t * @param path 相对ClassPath的目录或者绝对路径目录\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeBytes(byte[] data, String path) throws IORuntimeException {\n\t\treturn writeBytes(data, touch(path));\n\t}\n\n\t/**\n\t * 写数据到文件中\n\t *\n\t * @param dest 目标文件\n\t * @param data 数据\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeBytes(byte[] data, File dest) throws IORuntimeException {\n\t\treturn writeBytes(data, dest, 0, data.length, false);\n\t}\n\n\t/**\n\t * 写入数据到文件\n\t *\n\t * @param data     数据\n\t * @param dest     目标文件\n\t * @param off      数据开始位置\n\t * @param len      数据长度\n\t * @param isAppend 是否追加模式\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeBytes(byte[] data, File dest, int off, int len, boolean isAppend) throws IORuntimeException {\n\t\treturn FileWriter.create(dest).write(data, off, len, isAppend);\n\t}\n\n\t/**\n\t * 将流的内容写入文件<br>\n\t * 此方法会自动关闭输入流\n\t *\n\t * @param dest 目标文件\n\t * @param in   输入流\n\t * @return dest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeFromStream(InputStream in, File dest) throws IORuntimeException {\n\t\treturn writeFromStream(in, dest, true);\n\t}\n\n\t/**\n\t * 将流的内容写入文件\n\t *\n\t * @param dest      目标文件\n\t * @param in        输入流\n\t * @param isCloseIn 是否关闭输入流\n\t * @return dest\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.6\n\t */\n\tpublic static File writeFromStream(InputStream in, File dest, boolean isCloseIn) throws IORuntimeException {\n\t\treturn FileWriter.create(dest).writeFromStream(in, isCloseIn);\n\t}\n\n\t/**\n\t * 将流的内容写入文件<br>\n\t * 此方法会自动关闭输入流\n\t *\n\t * @param in           输入流\n\t * @param fullFilePath 文件绝对路径\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static File writeFromStream(InputStream in, String fullFilePath) throws IORuntimeException {\n\t\treturn writeFromStream(in, touch(fullFilePath));\n\t}\n\n\t/**\n\t * 将文件写入流中，此方法不会关闭输出流\n\t *\n\t * @param file 文件\n\t * @param out  流\n\t * @return 写出的流byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long writeToStream(File file, OutputStream out) throws IORuntimeException {\n\t\treturn FileReader.create(file).writeToStream(out);\n\t}\n\n\t/**\n\t * 将路径对应文件写入流中，此方法不会关闭输出流\n\t *\n\t * @param fullFilePath 文件绝对路径\n\t * @param out          输出流\n\t * @return 写出的流byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long writeToStream(String fullFilePath, OutputStream out) throws IORuntimeException {\n\t\treturn writeToStream(touch(fullFilePath), out);\n\t}\n\n\t/**\n\t * 可读的文件大小\n\t *\n\t * @param file 文件\n\t * @return 大小\n\t */\n\tpublic static String readableFileSize(File file) {\n\t\treturn readableFileSize(file.length());\n\t}\n\n\t/**\n\t * 可读的文件大小<br>\n\t * 参考 <a href=\"http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc\">http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc</a>\n\t *\n\t * @param size Long类型大小\n\t * @return 大小\n\t * @see DataSizeUtil#format(long)\n\t */\n\tpublic static String readableFileSize(long size) {\n\t\treturn DataSizeUtil.format(size);\n\t}\n\n\t/**\n\t * 转换文件编码<br>\n\t * 此方法用于转换文件编码，读取的文件实际编码必须与指定的srcCharset编码一致，否则导致乱码\n\t *\n\t * @param file        文件\n\t * @param srcCharset  原文件的编码，必须与文件内容的编码保持一致\n\t * @param destCharset 转码后的编码\n\t * @return 被转换编码的文件\n\t * @see CharsetUtil#convert(File, Charset, Charset)\n\t * @since 3.1.0\n\t */\n\tpublic static File convertCharset(File file, Charset srcCharset, Charset destCharset) {\n\t\treturn CharsetUtil.convert(file, srcCharset, destCharset);\n\t}\n\n\t/**\n\t * 转换换行符<br>\n\t * 将给定文件的换行符转换为指定换行符\n\t *\n\t * @param file          文件\n\t * @param charset       编码\n\t * @param lineSeparator 换行符枚举{@link LineSeparator}\n\t * @return 被修改的文件\n\t * @since 3.1.0\n\t */\n\tpublic static File convertLineSeparator(File file, Charset charset, LineSeparator lineSeparator) {\n\t\tfinal List<String> lines = readLines(file, charset);\n\t\treturn FileWriter.create(file, charset).writeLines(lines, lineSeparator, false);\n\t}\n\n\t/**\n\t * 清除文件名中的在Windows下不支持的非法字符，包括： \\ / : * ? \" &lt; &gt; |\n\t *\n\t * @param fileName 文件名（必须不包括路径，否则路径符将被替换）\n\t * @return 清理后的文件名\n\t * @see FileNameUtil#cleanInvalid(String)\n\t * @since 3.3.1\n\t */\n\tpublic static String cleanInvalid(String fileName) {\n\t\treturn FileNameUtil.cleanInvalid(fileName);\n\t}\n\n\t/**\n\t * 文件名中是否包含在Windows下不支持的非法字符，包括： \\ / : * ? \" &lt; &gt; |\n\t *\n\t * @param fileName 文件名（必须不包括路径，否则路径符将被替换）\n\t * @return 是否包含非法字符\n\t * @see FileNameUtil#containsInvalid(String)\n\t * @since 3.3.1\n\t */\n\tpublic static boolean containsInvalid(String fileName) {\n\t\treturn FileNameUtil.containsInvalid(fileName);\n\t}\n\n\t/**\n\t * 计算文件CRC32校验码\n\t *\n\t * @param file 文件，不能为目录\n\t * @return CRC32值\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static long checksumCRC32(File file) throws IORuntimeException {\n\t\treturn checksum(file, new CRC32()).getValue();\n\t}\n\n\t/**\n\t * 计算文件校验码\n\t *\n\t * @param file     文件，不能为目录\n\t * @param checksum {@link Checksum}\n\t * @return Checksum\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static Checksum checksum(File file, Checksum checksum) throws IORuntimeException {\n\t\tAssert.notNull(file, \"File is null !\");\n\t\tif (file.isDirectory()) {\n\t\t\tthrow new IllegalArgumentException(\"Checksums can't be computed on directories\");\n\t\t}\n\t\ttry {\n\t\t\treturn IoUtil.checksum(Files.newInputStream(file.toPath()), checksum);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Web项目下的web root路径<br>\n\t * 原理是首先获取ClassPath路径，由于在web项目中ClassPath位于 WEB-INF/classes/下，故向上获取两级目录即可。\n\t *\n\t * @return web root路径\n\t * @since 4.0.13\n\t */\n\tpublic static File getWebRoot() {\n\t\tfinal String classPath = ClassUtil.getClassPath();\n\t\tif (StrUtil.isNotBlank(classPath)) {\n\t\t\treturn getParent(file(classPath), 2);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定层级的父路径\n\t *\n\t * <pre>\n\t * getParent(\"d:/aaa/bbb/cc/ddd\", 0) -》 \"d:/aaa/bbb/cc/ddd\"\n\t * getParent(\"d:/aaa/bbb/cc/ddd\", 2) -》 \"d:/aaa/bbb\"\n\t * getParent(\"d:/aaa/bbb/cc/ddd\", 4) -》 \"d:/\"\n\t * getParent(\"d:/aaa/bbb/cc/ddd\", 5) -》 null\n\t * </pre>\n\t *\n\t * @param filePath 目录或文件路径\n\t * @param level    层级\n\t * @return 路径File，如果不存在返回null\n\t * @since 4.1.2\n\t */\n\tpublic static String getParent(String filePath, int level) {\n\t\tfinal File parent = getParent(file(filePath), level);\n\t\ttry {\n\t\t\treturn null == parent ? null : parent.getCanonicalPath();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定层级的父路径\n\t *\n\t * <pre>\n\t * getParent(file(\"d:/aaa/bbb/cc/ddd\", 0)) -》 \"d:/aaa/bbb/cc/ddd\"\n\t * getParent(file(\"d:/aaa/bbb/cc/ddd\", 2)) -》 \"d:/aaa/bbb\"\n\t * getParent(file(\"d:/aaa/bbb/cc/ddd\", 4)) -》 \"d:/\"\n\t * getParent(file(\"d:/aaa/bbb/cc/ddd\", 5)) -》 null\n\t * </pre>\n\t *\n\t * @param file  目录或文件\n\t * @param level 层级\n\t * @return 路径File，如果不存在返回null\n\t * @since 4.1.2\n\t */\n\tpublic static File getParent(File file, int level) {\n\t\tif (level < 1 || null == file) {\n\t\t\treturn file;\n\t\t}\n\n\t\tFile parentFile;\n\t\ttry {\n\t\t\tparentFile = file.getCanonicalFile().getParentFile();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tif (1 == level) {\n\t\t\treturn parentFile;\n\t\t}\n\t\treturn getParent(parentFile, level - 1);\n\t}\n\n\t/**\n\t * 检查父完整路径是否为自路径的前半部分，如果不是说明不是子路径，可能存在slip注入。\n\t * <p>\n\t * 见http://blog.nsfocus.net/zip-slip-2/\n\t *\n\t * @param parentFile 父文件或目录\n\t * @param file       子文件或目录\n\t * @return 子文件或目录\n\t * @throws IllegalArgumentException 检查创建的子文件不在父目录中抛出此异常\n\t */\n\tpublic static File checkSlip(File parentFile, File file) throws IllegalArgumentException {\n\t\tif (null != parentFile && null != file) {\n\t\t\tif (false == isSub(parentFile, file)) {\n\t\t\t\tthrow new IllegalArgumentException(\"New file is outside of the parent dir: \" + file.getName());\n\t\t\t}\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 根据文件扩展名获得MimeType\n\t *\n\t * @param filePath 文件路径或文件名\n\t * @return MimeType\n\t * @since 4.1.15\n\t */\n\tpublic static String getMimeType(String filePath) {\n\t\tif (StrUtil.isBlank(filePath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 补充一些常用的mimeType\n\t\tif (StrUtil.endWithIgnoreCase(filePath, \".css\")) {\n\t\t\treturn \"text/css\";\n\t\t} else if (StrUtil.endWithIgnoreCase(filePath, \".js\")) {\n\t\t\treturn \"application/x-javascript\";\n\t\t} else if (StrUtil.endWithIgnoreCase(filePath, \".rar\")) {\n\t\t\treturn \"application/x-rar-compressed\";\n\t\t} else if (StrUtil.endWithIgnoreCase(filePath, \".7z\")) {\n\t\t\treturn \"application/x-7z-compressed\";\n\t\t} else if (StrUtil.endWithIgnoreCase(filePath, \".wgt\")) {\n\t\t\treturn \"application/widget\";\n\t\t} else if (StrUtil.endWithIgnoreCase(filePath, \".webp\")) {\n\t\t\t// JDK8不支持\n\t\t\treturn \"image/webp\";\n\t\t}\n\n\t\tString contentType = URLConnection.getFileNameMap().getContentTypeFor(filePath);\n\t\tif (null == contentType) {\n\t\t\tcontentType = getMimeType(Paths.get(filePath));\n\t\t}\n\n\t\treturn contentType;\n\t}\n\n\t/**\n\t * 判断是否为符号链接文件\n\t *\n\t * @param file 被检查的文件\n\t * @return 是否为符号链接文件\n\t * @since 4.4.2\n\t */\n\tpublic static boolean isSymlink(File file) {\n\t\treturn isSymlink(file.toPath());\n\t}\n\n\t/**\n\t * 判断给定的目录是否为给定文件或文件夹的子目录\n\t *\n\t * @param parent 父目录\n\t * @param sub    子目录\n\t * @return 子目录是否为父目录的子目录\n\t * @since 4.5.4\n\t */\n\tpublic static boolean isSub(File parent, File sub) {\n\t\tAssert.notNull(parent);\n\t\tAssert.notNull(sub);\n\t\treturn isSub(parent.toPath(), sub.toPath());\n\t}\n\n\t/**\n\t * 创建{@link RandomAccessFile}\n\t *\n\t * @param path 文件Path\n\t * @param mode 模式，见{@link FileMode}\n\t * @return {@link RandomAccessFile}\n\t * @since 4.5.2\n\t */\n\tpublic static RandomAccessFile createRandomAccessFile(Path path, FileMode mode) {\n\t\treturn createRandomAccessFile(path.toFile(), mode);\n\t}\n\n\t/**\n\t * 创建{@link RandomAccessFile}\n\t *\n\t * @param file 文件\n\t * @param mode 模式，见{@link FileMode}\n\t * @return {@link RandomAccessFile}\n\t * @since 4.5.2\n\t */\n\tpublic static RandomAccessFile createRandomAccessFile(File file, FileMode mode) {\n\t\ttry {\n\t\t\treturn new RandomAccessFile(file, mode.name());\n\t\t} catch (FileNotFoundException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 文件内容跟随器，实现类似Linux下\"tail -f\"命令功能<br>\n\t * 此方法会阻塞当前线程\n\t *\n\t * @param file    文件\n\t * @param handler 行处理器\n\t */\n\tpublic static void tail(File file, LineHandler handler) {\n\t\ttail(file, CharsetUtil.CHARSET_UTF_8, handler);\n\t}\n\n\t/**\n\t * 文件内容跟随器，实现类似Linux下\"tail -f\"命令功能<br>\n\t * 此方法会阻塞当前线程\n\t *\n\t * @param file    文件\n\t * @param charset 编码\n\t * @param handler 行处理器\n\t */\n\tpublic static void tail(File file, Charset charset, LineHandler handler) {\n\t\tnew Tailer(file, charset, handler).start();\n\t}\n\n\t/**\n\t * 文件内容跟随器，实现类似Linux下\"tail -f\"命令功能<br>\n\t * 此方法会阻塞当前线程\n\t *\n\t * @param file    文件\n\t * @param charset 编码\n\t */\n\tpublic static void tail(File file, Charset charset) {\n\t\ttail(file, charset, Tailer.CONSOLE_HANDLER);\n\t}\n\n\t/**\n\t * 根据压缩包中的路径构建目录结构，在Win下直接构建，在Linux下拆分路径单独构建\n\t *\n\t * @param outFile  最外部路径\n\t * @param fileName 文件名，可以包含路径\n\t * @return 文件或目录\n\t * @since 5.0.5\n\t */\n\tprivate static File buildFile(File outFile, String fileName) {\n\t\t// 替换Windows路径分隔符为Linux路径分隔符，便于统一处理\n\t\tfileName = fileName.replace('\\\\', '/');\n\t\tif (false == isWindows()\n\t\t\t// 检查文件名中是否包含\"/\"，不考虑以\"/\"结尾的情况\n\t\t\t&& fileName.lastIndexOf(CharUtil.SLASH, fileName.length() - 2) > 0) {\n\t\t\t// 在Linux下多层目录创建存在问题，/会被当成文件名的一部分，此处做处理\n\t\t\t// 使用/拆分路径（zip中无\\），级联创建父目录\n\t\t\tfinal List<String> pathParts = StrUtil.split(fileName, '/', false, true);\n\t\t\tfinal int lastPartIndex = pathParts.size() - 1;//目录个数\n\t\t\tfor (int i = 0; i < lastPartIndex; i++) {\n\t\t\t\t//由于路径拆分，slip不检查，在最后一步检查\n\t\t\t\toutFile = new File(outFile, pathParts.get(i));\n\t\t\t}\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\toutFile.mkdirs();\n\t\t\t// 最后一个部分如果非空，作为文件名\n\t\t\tfileName = pathParts.get(lastPartIndex);\n\t\t}\n\t\treturn new File(outFile, fileName);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/IORuntimeException.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * IO运行时异常，常用于对IOException的包装\n *\n * @author xiaoleilu\n */\npublic class IORuntimeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic IORuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic IORuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic IORuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic IORuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic IORuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n\n\t/**\n\t * 导致这个异常的异常是否是指定类型的异常\n\t *\n\t * @param clazz 异常类\n\t * @return 是否为指定类型异常\n\t */\n\tpublic boolean causeInstanceOf(Class<? extends Throwable> clazz) {\n\t\tfinal Throwable cause = this.getCause();\n\t\treturn null != clazz && clazz.isInstance(cause);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.collection.LineIter;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.copy.ReaderWriterCopier;\nimport cn.hutool.core.io.copy.StreamCopier;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.Flushable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.ObjectOutputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PushbackInputStream;\nimport java.io.PushbackReader;\nimport java.io.Reader;\nimport java.io.Serializable;\nimport java.io.UnsupportedEncodingException;\nimport java.io.Writer;\nimport java.nio.CharBuffer;\nimport java.nio.channels.FileChannel;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.Objects;\nimport java.util.zip.CRC32;\nimport java.util.zip.CheckedInputStream;\nimport java.util.zip.Checksum;\n\n/**\n * IO工具类<br>\n * IO工具类只是辅助流的读写，并不负责关闭流。原因是流可能被多次读写，读写关闭后容易造成问题。\n *\n * @author xiaoleilu\n */\npublic class IoUtil extends NioUtil {\n\n\t// -------------------------------------------------------------------------------------- Copy start\n\n\t/**\n\t * 将Reader中的内容复制到Writer中 使用默认缓存大小，拷贝后不关闭Reader\n\t *\n\t * @param reader Reader\n\t * @param writer Writer\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(Reader reader, Writer writer) throws IORuntimeException {\n\t\treturn copy(reader, writer, DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 将Reader中的内容复制到Writer中，拷贝后不关闭Reader\n\t *\n\t * @param reader     Reader\n\t * @param writer     Writer\n\t * @param bufferSize 缓存大小\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(Reader reader, Writer writer, int bufferSize) throws IORuntimeException {\n\t\treturn copy(reader, writer, bufferSize, null);\n\t}\n\n\t/**\n\t * 将Reader中的内容复制到Writer中，拷贝后不关闭Reader\n\t *\n\t * @param reader         Reader\n\t * @param writer         Writer\n\t * @param bufferSize     缓存大小\n\t * @param streamProgress 进度处理器\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(Reader reader, Writer writer, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn copy(reader, writer, bufferSize, -1, streamProgress);\n\t}\n\n\t/**\n\t * 将Reader中的内容复制到Writer中，拷贝后不关闭Reader\n\t *\n\t * @param reader         Reader\n\t * @param writer         Writer\n\t * @param bufferSize     缓存大小\n\t * @param count          最大长度\n\t * @param streamProgress 进度处理器\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(Reader reader, Writer writer, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn new ReaderWriterCopier(bufferSize, count, streamProgress).copy(reader, writer);\n\t}\n\n\t/**\n\t * 拷贝流，使用默认Buffer大小，拷贝后不关闭流\n\t *\n\t * @param in  输入流\n\t * @param out 输出流\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(InputStream in, OutputStream out) throws IORuntimeException {\n\t\treturn copy(in, out, DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 拷贝流，拷贝后不关闭流\n\t *\n\t * @param in         输入流\n\t * @param out        输出流\n\t * @param bufferSize 缓存大小\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(InputStream in, OutputStream out, int bufferSize) throws IORuntimeException {\n\t\treturn copy(in, out, bufferSize, null);\n\t}\n\n\t/**\n\t * 拷贝流，拷贝后不关闭流\n\t *\n\t * @param in             输入流\n\t * @param out            输出流\n\t * @param bufferSize     缓存大小\n\t * @param streamProgress 进度条\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn copy(in, out, bufferSize, -1, streamProgress);\n\t}\n\n\t/**\n\t * 拷贝流，拷贝后不关闭流\n\t *\n\t * @param in             输入流\n\t * @param out            输出流\n\t * @param bufferSize     缓存大小\n\t * @param count          总拷贝长度\n\t * @param streamProgress 进度条\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static long copy(InputStream in, OutputStream out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn new StreamCopier(bufferSize, count, streamProgress).copy(in, out);\n\t}\n\n\t/**\n\t * 拷贝文件流，使用NIO\n\t *\n\t * @param in  输入\n\t * @param out 输出\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(FileInputStream in, FileOutputStream out) throws IORuntimeException {\n\t\tAssert.notNull(in, \"FileInputStream is null!\");\n\t\tAssert.notNull(out, \"FileOutputStream is null!\");\n\n\t\tFileChannel inChannel = null;\n\t\tFileChannel outChannel = null;\n\t\ttry {\n\t\t\tinChannel = in.getChannel();\n\t\t\toutChannel = out.getChannel();\n\t\t\treturn copy(inChannel, outChannel);\n\t\t} finally {\n\t\t\tclose(outChannel);\n\t\t\tclose(inChannel);\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------- Copy end\n\n\t// -------------------------------------------------------------------------------------- getReader and getWriter start\n\n\t/**\n\t * 获得一个文件读取器，默认使用UTF-8编码\n\t *\n\t * @param in 输入流\n\t * @return BufferedReader对象\n\t * @since 5.1.6\n\t */\n\tpublic static BufferedReader getUtf8Reader(InputStream in) {\n\t\treturn getReader(in, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param in          输入流\n\t * @param charsetName 字符集名称\n\t * @return BufferedReader对象\n\t * @deprecated 请使用 {@link #getReader(InputStream, Charset)}\n\t */\n\t@Deprecated\n\tpublic static BufferedReader getReader(InputStream in, String charsetName) {\n\t\treturn getReader(in, Charset.forName(charsetName));\n\t}\n\n\t/**\n\t * 从{@link BOMInputStream}中获取Reader\n\t *\n\t * @param in {@link BOMInputStream}\n\t * @return {@link BufferedReader}\n\t * @since 5.5.8\n\t */\n\tpublic static BufferedReader getReader(BOMInputStream in) {\n\t\treturn getReader(in, in.getCharset());\n\t}\n\n\t/**\n\t * 从{@link InputStream}中获取{@link BomReader}\n\t *\n\t * @param in {@link InputStream}\n\t * @return {@link BomReader}\n\t * @since 5.7.14\n\t */\n\tpublic static BomReader getBomReader(InputStream in) {\n\t\treturn new BomReader(in);\n\t}\n\n\t/**\n\t * 获得一个Reader\n\t *\n\t * @param in      输入流\n\t * @param charset 字符集\n\t * @return BufferedReader对象\n\t */\n\tpublic static BufferedReader getReader(InputStream in, Charset charset) {\n\t\tif (null == in) {\n\t\t\treturn null;\n\t\t}\n\n\t\tInputStreamReader reader;\n\t\tif (null == charset) {\n\t\t\treader = new InputStreamReader(in);\n\t\t} else {\n\t\t\treader = new InputStreamReader(in, charset);\n\t\t}\n\n\t\treturn new BufferedReader(reader);\n\t}\n\n\t/**\n\t * 获得{@link BufferedReader}<br>\n\t * 如果是{@link BufferedReader}强转返回，否则新建。如果提供的Reader为null返回null\n\t *\n\t * @param reader 普通Reader，如果为null返回null\n\t * @return {@link BufferedReader} or null\n\t * @since 3.0.9\n\t */\n\tpublic static BufferedReader getReader(Reader reader) {\n\t\tif (null == reader) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);\n\t}\n\n\t/**\n\t * 获得{@link PushbackReader}<br>\n\t * 如果是{@link PushbackReader}强转返回，否则新建\n\t *\n\t * @param reader       普通Reader\n\t * @param pushBackSize 推后的byte数\n\t * @return {@link PushbackReader}\n\t * @since 3.1.0\n\t */\n\tpublic static PushbackReader getPushBackReader(Reader reader, int pushBackSize) {\n\t\treturn (reader instanceof PushbackReader) ? (PushbackReader) reader : new PushbackReader(reader, pushBackSize);\n\t}\n\n\t/**\n\t * 获得一个Writer，默认编码UTF-8\n\t *\n\t * @param out 输入流\n\t * @return OutputStreamWriter对象\n\t * @since 5.1.6\n\t */\n\tpublic static OutputStreamWriter getUtf8Writer(OutputStream out) {\n\t\treturn getWriter(out, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得一个Writer\n\t *\n\t * @param out         输入流\n\t * @param charsetName 字符集\n\t * @return OutputStreamWriter对象\n\t * @deprecated 请使用 {@link #getWriter(OutputStream, Charset)}\n\t */\n\t@Deprecated\n\tpublic static OutputStreamWriter getWriter(OutputStream out, String charsetName) {\n\t\treturn getWriter(out, Charset.forName(charsetName));\n\t}\n\n\t/**\n\t * 获得一个Writer\n\t *\n\t * @param out     输入流\n\t * @param charset 字符集\n\t * @return OutputStreamWriter对象\n\t */\n\tpublic static OutputStreamWriter getWriter(OutputStream out, Charset charset) {\n\t\tif (null == out) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (null == charset) {\n\t\t\treturn new OutputStreamWriter(out);\n\t\t} else {\n\t\t\treturn new OutputStreamWriter(out, charset);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------- getReader and getWriter end\n\n\t// -------------------------------------------------------------------------------------- read start\n\n\t/**\n\t * 从流中读取UTF8编码的内容\n\t *\n\t * @param in 输入流\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @since 5.4.4\n\t */\n\tpublic static String readUtf8(InputStream in) throws IORuntimeException {\n\t\treturn read(in, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从流中读取内容，读取完成后关闭流\n\t *\n\t * @param in          输入流\n\t * @param charsetName 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #read(InputStream, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String read(InputStream in, String charsetName) throws IORuntimeException {\n\t\tfinal FastByteArrayOutputStream out = read(in);\n\t\treturn StrUtil.isBlank(charsetName) ? out.toString() : out.toString(charsetName);\n\t}\n\n\t/**\n\t * 从流中读取内容，读取完毕后关闭流\n\t *\n\t * @param in      输入流，读取完毕后关闭流\n\t * @param charset 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String read(InputStream in, Charset charset) throws IORuntimeException {\n\t\treturn StrUtil.str(readBytes(in), charset);\n\t}\n\n\t/**\n\t * 从流中读取内容，读到输出流中，读取完毕后关闭流\n\t *\n\t * @param in 输入流\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static FastByteArrayOutputStream read(InputStream in) throws IORuntimeException {\n\t\treturn read(in, true);\n\t}\n\n\t/**\n\t * 从流中读取内容，读到输出流中，读取完毕后可选是否关闭流\n\t *\n\t * @param in      输入流\n\t * @param isClose 读取完毕后是否关闭流\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.3\n\t */\n\tpublic static FastByteArrayOutputStream read(InputStream in, boolean isClose) throws IORuntimeException {\n\t\tfinal FastByteArrayOutputStream out;\n\t\tif (in instanceof FileInputStream) {\n\t\t\t// 文件流的长度是可预见的，此时直接读取效率更高\n\t\t\ttry {\n\t\t\t\tout = new FastByteArrayOutputStream(in.available());\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t} else {\n\t\t\tout = new FastByteArrayOutputStream();\n\t\t}\n\t\ttry {\n\t\t\tcopy(in, out);\n\t\t} finally {\n\t\t\tif (isClose) {\n\t\t\t\tclose(in);\n\t\t\t}\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * 从Reader中读取String，读取完毕后关闭Reader\n\t *\n\t * @param reader Reader\n\t * @return String\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String read(Reader reader) throws IORuntimeException {\n\t\treturn read(reader, true);\n\t}\n\n\t/**\n\t * 从{@link Reader}中读取String\n\t *\n\t * @param reader  {@link Reader}\n\t * @param isClose 是否关闭{@link Reader}\n\t * @return String\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String read(Reader reader, boolean isClose) throws IORuntimeException {\n\t\tfinal StringBuilder builder = StrUtil.builder();\n\t\tfinal CharBuffer buffer = CharBuffer.allocate(DEFAULT_BUFFER_SIZE);\n\t\ttry {\n\t\t\twhile (-1 != reader.read(buffer)) {\n\t\t\t\tbuilder.append(buffer.flip());\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isClose) {\n\t\t\t\tIoUtil.close(reader);\n\t\t\t}\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 从流中读取bytes，读取完毕后关闭流\n\t *\n\t * @param in {@link InputStream}\n\t * @return bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static byte[] readBytes(InputStream in) throws IORuntimeException {\n\t\treturn readBytes(in, true);\n\t}\n\n\t/**\n\t * 从流中读取bytes\n\t *\n\t * @param in      {@link InputStream}\n\t * @param isClose 是否关闭输入流\n\t * @return bytes\n\t * @throws IORuntimeException IO异常\n\t * @since 5.0.4\n\t */\n\tpublic static byte[] readBytes(InputStream in, boolean isClose) throws IORuntimeException {\n\t\treturn read(in, isClose).toByteArray();\n\t}\n\n\t/**\n\t * 读取指定长度的byte数组，不关闭流\n\t *\n\t * @param in     {@link InputStream}，为{@code null}返回{@code null}\n\t * @param length 长度，小于等于0返回空byte数组\n\t * @return bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static byte[] readBytes(InputStream in, int length) throws IORuntimeException {\n\t\tif (null == in) {\n\t\t\treturn null;\n\t\t}\n\t\tif (length <= 0) {\n\t\t\treturn new byte[0];\n\t\t}\n\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream(length);\n\t\tcopy(in, out, DEFAULT_BUFFER_SIZE, length, null);\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 读取16进制字符串\n\t *\n\t * @param in          {@link InputStream}\n\t * @param length      长度\n\t * @param toLowerCase true 传换成小写格式 ， false 传换成大写格式\n\t * @return 16进制字符串\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readHex(InputStream in, int length, boolean toLowerCase) throws IORuntimeException {\n\t\treturn HexUtil.encodeHexStr(readBytes(in, length), toLowerCase);\n\t}\n\n\t/**\n\t * 从流中读取前64个byte并转换为16进制，字母部分使用大写\n\t *\n\t * @param in {@link InputStream}\n\t * @return 16进制字符串\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readHex64Upper(InputStream in) throws IORuntimeException {\n\t\treturn readHex(in, 64, false);\n\t}\n\n\t/**\n\t * 从流中读取前8192个byte并转换为16进制，字母部分使用大写\n\t *\n\t * @param in {@link InputStream}\n\t * @return 16进制字符串\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readHex8192Upper(InputStream in) throws IORuntimeException {\n\t\ttry {\n\t\t\tint i = in.available();\n\t\t\treturn readHex(in, Math.min(8192, i), false);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 从流中读取前64个byte并转换为16进制，字母部分使用小写\n\t *\n\t * @param in {@link InputStream}\n\t * @return 16进制字符串\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readHex64Lower(InputStream in) throws IORuntimeException {\n\t\treturn readHex(in, 64, true);\n\t}\n\n\t/**\n\t * 从流中读取对象，即对象的反序列化\n\t *\n\t * <p>\n\t * 注意！！！ 此方法不会检查反序列化安全，可能存在反序列化漏洞风险！！！\n\t * </p>\n\t *\n\t * @param <T> 读取对象的类型\n\t * @param in  输入流\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t * @throws UtilException      ClassNotFoundException包装\n\t */\n\tpublic static <T> T readObj(InputStream in) throws IORuntimeException, UtilException {\n\t\treturn readObj(in, null);\n\t}\n\n\t/**\n\t * 从流中读取对象，即对象的反序列化，读取后不关闭流\n\t *\n\t * <p>\n\t * 注意！！！ 此方法不会检查反序列化安全，可能存在反序列化漏洞风险！！！\n\t * </p>\n\t *\n\t * @param <T>   读取对象的类型\n\t * @param in    输入流\n\t * @param clazz 读取对象类型\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t * @throws UtilException      ClassNotFoundException包装\n\t */\n\tpublic static <T> T readObj(InputStream in, Class<T> clazz) throws IORuntimeException, UtilException {\n\t\ttry {\n\t\t\treturn readObj((in instanceof ValidateObjectInputStream) ?\n\t\t\t\t\t\t\t(ValidateObjectInputStream) in : new ValidateObjectInputStream(in),\n\t\t\t\t\tclazz);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 从流中读取对象，即对象的反序列化，读取后不关闭流\n\t *\n\t * <p>\n\t * 此方法使用了{@link ValidateObjectInputStream}中的黑白名单方式过滤类，用于避免反序列化漏洞<br>\n\t * 通过构造{@link ValidateObjectInputStream}，调用{@link ValidateObjectInputStream#accept(Class[])}\n\t * 或者{@link ValidateObjectInputStream#refuse(Class[])}方法添加可以被序列化的类或者禁止序列化的类。\n\t * </p>\n\t *\n\t * @param <T>   读取对象的类型\n\t * @param in    输入流，使用{@link ValidateObjectInputStream}中的黑白名单方式过滤类，用于避免反序列化漏洞\n\t * @param clazz 读取对象类型\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t * @throws UtilException      ClassNotFoundException包装\n\t */\n\tpublic static <T> T readObj(ValidateObjectInputStream in, Class<T> clazz) throws IORuntimeException, UtilException {\n\t\tif (in == null) {\n\t\t\tthrow new IllegalArgumentException(\"The InputStream must not be null\");\n\t\t}\n\t\tif(null != clazz){\n\t\t\tin.accept(clazz);\n\t\t}\n\t\ttry {\n\t\t\t//noinspection unchecked\n\t\t\treturn (T) in.readObject();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 从流中读取内容，使用UTF-8编码\n\t *\n\t * @param <T>        集合类型\n\t * @param in         输入流\n\t * @param collection 返回集合\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readUtf8Lines(InputStream in, T collection) throws IORuntimeException {\n\t\treturn readLines(in, CharsetUtil.CHARSET_UTF_8, collection);\n\t}\n\n\t/**\n\t * 从流中读取内容\n\t *\n\t * @param <T>         集合类型\n\t * @param in          输入流\n\t * @param charsetName 字符集\n\t * @param collection  返回集合\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #readLines(InputStream, Charset, Collection)}\n\t */\n\t@Deprecated\n\tpublic static <T extends Collection<String>> T readLines(InputStream in, String charsetName, T collection) throws IORuntimeException {\n\t\treturn readLines(in, CharsetUtil.charset(charsetName), collection);\n\t}\n\n\t/**\n\t * 从流中读取内容\n\t *\n\t * @param <T>        集合类型\n\t * @param in         输入流\n\t * @param charset    字符集\n\t * @param collection 返回集合\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(InputStream in, Charset charset, T collection) throws IORuntimeException {\n\t\treturn readLines(getReader(in, charset), collection);\n\t}\n\n\t/**\n\t * 从Reader中读取内容\n\t *\n\t * @param <T>        集合类型\n\t * @param reader     {@link Reader}\n\t * @param collection 返回集合\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static <T extends Collection<String>> T readLines(Reader reader, T collection) throws IORuntimeException {\n\t\treadLines(reader, (LineHandler) collection::add);\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 按行读取UTF-8编码数据，针对每行的数据做处理\n\t *\n\t * @param in          {@link InputStream}\n\t * @param lineHandler 行处理接口，实现handle方法用于编辑一行的数据后入到指定地方\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static void readUtf8Lines(InputStream in, LineHandler lineHandler) throws IORuntimeException {\n\t\treadLines(in, CharsetUtil.CHARSET_UTF_8, lineHandler);\n\t}\n\n\t/**\n\t * 按行读取数据，针对每行的数据做处理\n\t *\n\t * @param in          {@link InputStream}\n\t * @param charset     {@link Charset}编码\n\t * @param lineHandler 行处理接口，实现handle方法用于编辑一行的数据后入到指定地方\n\t * @throws IORuntimeException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic static void readLines(InputStream in, Charset charset, LineHandler lineHandler) throws IORuntimeException {\n\t\treadLines(getReader(in, charset), lineHandler);\n\t}\n\n\t/**\n\t * 按行读取数据，针对每行的数据做处理<br>\n\t * {@link Reader}自带编码定义，因此读取数据的编码跟随其编码。<br>\n\t * 此方法不会关闭流，除非抛出异常\n\t *\n\t * @param reader      {@link Reader}\n\t * @param lineHandler 行处理接口，实现handle方法用于编辑一行的数据后入到指定地方\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void readLines(Reader reader, LineHandler lineHandler) throws IORuntimeException {\n\t\tAssert.notNull(reader);\n\t\tAssert.notNull(lineHandler);\n\n\t\tfor (String line : lineIter(reader)) {\n\t\t\tlineHandler.handle(line);\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------- read end\n\n\t/**\n\t * String 转为流\n\t *\n\t * @param content     内容\n\t * @param charsetName 编码\n\t * @return 字节流\n\t * @deprecated 请使用 {@link #toStream(String, Charset)}\n\t */\n\t@Deprecated\n\tpublic static ByteArrayInputStream toStream(String content, String charsetName) {\n\t\treturn toStream(content, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * String 转为流\n\t *\n\t * @param content 内容\n\t * @param charset 编码\n\t * @return 字节流\n\t */\n\tpublic static ByteArrayInputStream toStream(String content, Charset charset) {\n\t\tif (content == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn toStream(StrUtil.bytes(content, charset));\n\t}\n\n\t/**\n\t * String 转为UTF-8编码的字节流流\n\t *\n\t * @param content 内容\n\t * @return 字节流\n\t * @since 4.5.1\n\t */\n\tpublic static ByteArrayInputStream toUtf8Stream(String content) {\n\t\treturn toStream(content, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 文件转为{@link FileInputStream}\n\t *\n\t * @param file 文件\n\t * @return {@link FileInputStream}\n\t */\n\tpublic static FileInputStream toStream(File file) {\n\t\ttry {\n\t\t\treturn new FileInputStream(file);\n\t\t} catch (FileNotFoundException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * byte[] 转为{@link ByteArrayInputStream}\n\t *\n\t * @param content 内容bytes\n\t * @return 字节流\n\t * @since 4.1.8\n\t */\n\tpublic static ByteArrayInputStream toStream(byte[] content) {\n\t\tif (content == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new ByteArrayInputStream(content);\n\t}\n\n\t/**\n\t * {@link ByteArrayOutputStream}转为{@link ByteArrayInputStream}\n\t *\n\t * @param out {@link ByteArrayOutputStream}\n\t * @return 字节流\n\t * @since 5.3.6\n\t */\n\tpublic static ByteArrayInputStream toStream(ByteArrayOutputStream out) {\n\t\tif (out == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new ByteArrayInputStream(out.toByteArray());\n\t}\n\n\t/**\n\t * 转换为{@link BufferedInputStream}\n\t *\n\t * @param in {@link InputStream}\n\t * @return {@link BufferedInputStream}\n\t * @since 4.0.10\n\t */\n\tpublic static BufferedInputStream toBuffered(InputStream in) {\n\t\tAssert.notNull(in, \"InputStream must be not null!\");\n\t\treturn (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedInputStream}\n\t *\n\t * @param in         {@link InputStream}\n\t * @param bufferSize buffer size\n\t * @return {@link BufferedInputStream}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedInputStream toBuffered(InputStream in, int bufferSize) {\n\t\tAssert.notNull(in, \"InputStream must be not null!\");\n\t\treturn (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in, bufferSize);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedOutputStream}\n\t *\n\t * @param out {@link OutputStream}\n\t * @return {@link BufferedOutputStream}\n\t * @since 4.0.10\n\t */\n\tpublic static BufferedOutputStream toBuffered(OutputStream out) {\n\t\tAssert.notNull(out, \"OutputStream must be not null!\");\n\t\treturn (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedOutputStream}\n\t *\n\t * @param out        {@link OutputStream}\n\t * @param bufferSize buffer size\n\t * @return {@link BufferedOutputStream}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedOutputStream toBuffered(OutputStream out, int bufferSize) {\n\t\tAssert.notNull(out, \"OutputStream must be not null!\");\n\t\treturn (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out, bufferSize);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedReader}\n\t *\n\t * @param reader {@link Reader}\n\t * @return {@link BufferedReader}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedReader toBuffered(Reader reader) {\n\t\tAssert.notNull(reader, \"Reader must be not null!\");\n\t\treturn (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedReader}\n\t *\n\t * @param reader     {@link Reader}\n\t * @param bufferSize buffer size\n\t * @return {@link BufferedReader}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedReader toBuffered(Reader reader, int bufferSize) {\n\t\tAssert.notNull(reader, \"Reader must be not null!\");\n\t\treturn (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader, bufferSize);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedWriter}\n\t *\n\t * @param writer {@link Writer}\n\t * @return {@link BufferedWriter}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedWriter toBuffered(Writer writer) {\n\t\tAssert.notNull(writer, \"Writer must be not null!\");\n\t\treturn (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer);\n\t}\n\n\t/**\n\t * 转换为{@link BufferedWriter}\n\t *\n\t * @param writer     {@link Writer}\n\t * @param bufferSize buffer size\n\t * @return {@link BufferedWriter}\n\t * @since 5.6.1\n\t */\n\tpublic static BufferedWriter toBuffered(Writer writer, int bufferSize) {\n\t\tAssert.notNull(writer, \"Writer must be not null!\");\n\t\treturn (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer, bufferSize);\n\t}\n\n\t/**\n\t * 将{@link InputStream}转换为支持mark标记的流<br>\n\t * 若原流支持mark标记，则返回原流，否则使用{@link BufferedInputStream} 包装之\n\t *\n\t * @param in 流\n\t * @return {@link InputStream}\n\t * @since 4.0.9\n\t */\n\tpublic static InputStream toMarkSupportStream(InputStream in) {\n\t\tif (null == in) {\n\t\t\treturn null;\n\t\t}\n\t\tif (false == in.markSupported()) {\n\t\t\treturn new BufferedInputStream(in);\n\t\t}\n\t\treturn in;\n\t}\n\n\t/**\n\t * 转换为{@link PushbackInputStream}<br>\n\t * 如果传入的输入流已经是{@link PushbackInputStream}，强转返回，否则新建一个\n\t *\n\t * @param in           {@link InputStream}\n\t * @param pushBackSize 推后的byte数\n\t * @return {@link PushbackInputStream}\n\t * @since 3.1.0\n\t */\n\tpublic static PushbackInputStream toPushbackStream(InputStream in, int pushBackSize) {\n\t\treturn (in instanceof PushbackInputStream) ? (PushbackInputStream) in : new PushbackInputStream(in, pushBackSize);\n\t}\n\n\t/**\n\t * 将指定{@link InputStream} 转换为{@link InputStream#available()}方法可用的流。<br>\n\t * 在Socket通信流中，服务端未返回数据情况下{@link InputStream#available()}方法始终为{@code 0}<br>\n\t * 因此，在读取前需要调用{@link InputStream#read()}读取一个字节（未返回会阻塞），一旦读取到了，{@link InputStream#available()}方法就正常了。<br>\n\t * 需要注意的是，在网络流中，是按照块来传输的，所以 {@link InputStream#available()} 读取到的并非最终长度，而是此次块的长度。<br>\n\t * 此方法返回对象的规则为：\n\t *\n\t * <ul>\n\t *     <li>FileInputStream 返回原对象，因为文件流的available方法本身可用</li>\n\t *     <li>其它InputStream 返回PushbackInputStream</li>\n\t * </ul>\n\t *\n\t * @param in 被转换的流\n\t * @return 转换后的流，可能为{@link PushbackInputStream}\n\t * @since 5.5.3\n\t */\n\tpublic static InputStream toAvailableStream(InputStream in) {\n\t\tif (in instanceof FileInputStream) {\n\t\t\t// FileInputStream本身支持available方法。\n\t\t\treturn in;\n\t\t}\n\n\t\tfinal PushbackInputStream pushbackInputStream = toPushbackStream(in, 1);\n\t\ttry {\n\t\t\tfinal int available = pushbackInputStream.available();\n\t\t\tif (available <= 0) {\n\t\t\t\t//此操作会阻塞，直到有数据被读到\n\t\t\t\tint b = pushbackInputStream.read();\n\t\t\t\tpushbackInputStream.unread(b);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn pushbackInputStream;\n\t}\n\n\t/**\n\t * 将byte[]写到流中\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 写入完毕是否关闭输出流\n\t * @param content    写入的内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void write(OutputStream out, boolean isCloseOut, byte[] content) throws IORuntimeException {\n\t\ttry {\n\t\t\tout.write(content);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isCloseOut) {\n\t\t\t\tclose(out);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将多部分内容写到流中，自动转换为UTF-8字符串\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 写入完毕是否关闭输出流\n\t * @param contents   写入的内容，调用toString()方法，不包括不会自动换行\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.1\n\t */\n\tpublic static void writeUtf8(OutputStream out, boolean isCloseOut, Object... contents) throws IORuntimeException {\n\t\twrite(out, CharsetUtil.CHARSET_UTF_8, isCloseOut, contents);\n\t}\n\n\t/**\n\t * 将多部分内容写到流中，自动转换为字符串\n\t *\n\t * @param out         输出流\n\t * @param charsetName 写出的内容的字符集\n\t * @param isCloseOut  写入完毕是否关闭输出流\n\t * @param contents    写入的内容，调用toString()方法，不包括不会自动换行\n\t * @throws IORuntimeException IO异常\n\t * @deprecated 请使用 {@link #write(OutputStream, Charset, boolean, Object...)}\n\t */\n\t@Deprecated\n\tpublic static void write(OutputStream out, String charsetName, boolean isCloseOut, Object... contents) throws IORuntimeException {\n\t\twrite(out, CharsetUtil.charset(charsetName), isCloseOut, contents);\n\t}\n\n\t/**\n\t * 将多部分内容写到流中，自动转换为字符串\n\t *\n\t * @param out        输出流\n\t * @param charset    写出的内容的字符集\n\t * @param isCloseOut 写入完毕是否关闭输出流\n\t * @param contents   写入的内容，调用toString()方法，不包括不会自动换行\n\t * @throws IORuntimeException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic static void write(OutputStream out, Charset charset, boolean isCloseOut, Object... contents) throws IORuntimeException {\n\t\tOutputStreamWriter osw = null;\n\t\ttry {\n\t\t\tosw = getWriter(out, charset);\n\t\t\tfor (Object content : contents) {\n\t\t\t\tif (content != null) {\n\t\t\t\t\tosw.write(Convert.toStr(content, StrUtil.EMPTY));\n\t\t\t\t}\n\t\t\t}\n\t\t\tosw.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isCloseOut) {\n\t\t\t\tclose(osw);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将多部分内容写到流中\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 写入完毕是否关闭输出流\n\t * @param obj        写入的对象内容\n\t * @throws IORuntimeException IO异常\n\t * @since 5.3.3\n\t */\n\tpublic static void writeObj(OutputStream out, boolean isCloseOut, Serializable obj) throws IORuntimeException {\n\t\twriteObjects(out, isCloseOut, obj);\n\t}\n\n\t/**\n\t * 将多部分内容写到流中\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 写入完毕是否关闭输出流\n\t * @param contents   写入的内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static void writeObjects(OutputStream out, boolean isCloseOut, Serializable... contents) throws IORuntimeException {\n\t\tObjectOutputStream osw = null;\n\t\ttry {\n\t\t\tif(ArrayUtil.isEmpty( contents)){\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tosw = out instanceof ObjectOutputStream ? (ObjectOutputStream) out : new ObjectOutputStream(out);\n\t\t\tfor (Object content : contents) {\n\t\t\t\tif (content != null) {\n\t\t\t\t\tosw.writeObject(content);\n\t\t\t\t}\n\t\t\t}\n\t\t\tosw.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isCloseOut) {\n\t\t\t\tclose(osw);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从缓存中刷出数据\n\t *\n\t * @param flushable {@link Flushable}\n\t * @since 4.2.2\n\t */\n\tpublic static void flush(Flushable flushable) {\n\t\tif (null != flushable) {\n\t\t\ttry {\n\t\t\t\tflushable.flush();\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 静默刷出\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 关闭<br>\n\t * 关闭失败不会抛出异常\n\t *\n\t * @param closeable 被关闭的对象\n\t */\n\tpublic static void close(Closeable closeable) {\n\t\tif (null != closeable) {\n\t\t\ttry {\n\t\t\t\tcloseable.close();\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 静默关闭\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 尝试关闭指定对象<br>\n\t * 判断对象如果实现了{@link AutoCloseable}，则调用之\n\t *\n\t * @param obj 可关闭对象\n\t * @since 4.3.2\n\t * @deprecated 拼写错误，请使用{@link #closeIfPossible(Object)}\n\t */\n\t@Deprecated\n\tpublic static void closeIfPosible(Object obj) {\n\t\tcloseIfPossible( obj);\n\t}\n\n\t/**\n\t * 尝试关闭指定对象<br>\n\t * 判断对象如果实现了{@link AutoCloseable}，则调用之\n\t *\n\t * @param obj 可关闭对象\n\t * @since 5.8.41\n\t */\n\tpublic static void closeIfPossible(Object obj) {\n\t\tif (obj instanceof AutoCloseable) {\n\t\t\tclose((AutoCloseable) obj);\n\t\t}\n\t}\n\n\t/**\n\t * 对比两个流内容是否相同<br>\n\t * 内部会转换流为 {@link BufferedInputStream}\n\t *\n\t * @param input1 第一个流\n\t * @param input2 第二个流\n\t * @return 两个流的内容一致返回true，否则false\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static boolean contentEquals(InputStream input1, InputStream input2) throws IORuntimeException {\n\t\tif (false == (input1 instanceof BufferedInputStream)) {\n\t\t\tinput1 = new BufferedInputStream(input1);\n\t\t}\n\t\tif (false == (input2 instanceof BufferedInputStream)) {\n\t\t\tinput2 = new BufferedInputStream(input2);\n\t\t}\n\n\t\ttry {\n\t\t\tint ch = input1.read();\n\t\t\twhile (EOF != ch) {\n\t\t\t\tint ch2 = input2.read();\n\t\t\t\tif (ch != ch2) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tch = input1.read();\n\t\t\t}\n\n\t\t\tint ch2 = input2.read();\n\t\t\treturn ch2 == EOF;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 对比两个Reader的内容是否一致<br>\n\t * 内部会转换流为 {@link BufferedInputStream}\n\t *\n\t * @param input1 第一个reader\n\t * @param input2 第二个reader\n\t * @return 两个流的内容一致返回true，否则false\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static boolean contentEquals(Reader input1, Reader input2) throws IORuntimeException {\n\t\tinput1 = getReader(input1);\n\t\tinput2 = getReader(input2);\n\n\t\ttry {\n\t\t\tint ch = input1.read();\n\t\t\twhile (EOF != ch) {\n\t\t\t\tint ch2 = input2.read();\n\t\t\t\tif (ch != ch2) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tch = input1.read();\n\t\t\t}\n\n\t\t\tint ch2 = input2.read();\n\t\t\treturn ch2 == EOF;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 对比两个流内容是否相同，忽略EOL字符<br>\n\t * 内部会转换流为 {@link BufferedInputStream}\n\t *\n\t * @param input1 第一个流\n\t * @param input2 第二个流\n\t * @return 两个流的内容一致返回true，否则false\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static boolean contentEqualsIgnoreEOL(Reader input1, Reader input2) throws IORuntimeException {\n\t\tfinal BufferedReader br1 = getReader(input1);\n\t\tfinal BufferedReader br2 = getReader(input2);\n\n\t\ttry {\n\t\t\tString line1 = br1.readLine();\n\t\t\tString line2 = br2.readLine();\n\t\t\twhile (line1 != null && line1.equals(line2)) {\n\t\t\t\tline1 = br1.readLine();\n\t\t\t\tline2 = br2.readLine();\n\t\t\t}\n\t\t\treturn Objects.equals(line1, line2);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 计算流CRC32校验码，计算后关闭流\n\t *\n\t * @param in 文件，不能为目录\n\t * @return CRC32值\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic static long checksumCRC32(InputStream in) throws IORuntimeException {\n\t\treturn checksum(in, new CRC32()).getValue();\n\t}\n\n\t/**\n\t * 计算流的校验码，计算后关闭流\n\t *\n\t * @param in       流\n\t * @param checksum {@link Checksum}\n\t * @return Checksum\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.10\n\t */\n\tpublic static Checksum checksum(InputStream in, Checksum checksum) throws IORuntimeException {\n\t\tAssert.notNull(in, \"InputStream is null !\");\n\t\tif (null == checksum) {\n\t\t\tchecksum = new CRC32();\n\t\t}\n\t\ttry {\n\t\t\tin = new CheckedInputStream(in, checksum);\n\t\t\tIoUtil.copy(in, new NullOutputStream());\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t\treturn checksum;\n\t}\n\n\t/**\n\t * 计算流的校验码，计算后关闭流\n\t *\n\t * @param in       流\n\t * @param checksum {@link Checksum}\n\t * @return Checksum\n\t * @throws IORuntimeException IO异常\n\t * @since 5.4.0\n\t */\n\tpublic static long checksumValue(InputStream in, Checksum checksum) {\n\t\treturn checksum(in, checksum).getValue();\n\t}\n\n\t/**\n\t * 返回行遍历器\n\t * <pre>\n\t * LineIterator it = null;\n\t * try {\n\t * \tit = IoUtil.lineIter(reader);\n\t * \twhile (it.hasNext()) {\n\t * \t\tString line = it.nextLine();\n\t * \t\t// do something with line\n\t *    }\n\t * } finally {\n\t * \t\tit.close();\n\t * }\n\t * </pre>\n\t *\n\t * @param reader {@link Reader}\n\t * @return {@link LineIter}\n\t * @since 5.6.1\n\t */\n\tpublic static LineIter lineIter(Reader reader) {\n\t\treturn new LineIter(reader);\n\t}\n\n\t/**\n\t * 返回行遍历器\n\t * <pre>\n\t * LineIterator it = null;\n\t * try {\n\t * \tit = IoUtil.lineIter(in, CharsetUtil.CHARSET_UTF_8);\n\t * \twhile (it.hasNext()) {\n\t * \t\tString line = it.nextLine();\n\t * \t\t// do something with line\n\t *    }\n\t * } finally {\n\t * \t\tit.close();\n\t * }\n\t * </pre>\n\t *\n\t * @param in      {@link InputStream}\n\t * @param charset 编码\n\t * @return {@link LineIter}\n\t * @since 5.6.1\n\t */\n\tpublic static LineIter lineIter(InputStream in, Charset charset) {\n\t\treturn new LineIter(in, charset);\n\t}\n\n\t/**\n\t * {@link ByteArrayOutputStream} 转换为String\n\t * @param out {@link ByteArrayOutputStream}\n\t * @param charset 编码\n\t * @return 字符串\n\t * @since 5.7.17\n\t */\n\tpublic static String toStr(ByteArrayOutputStream out, Charset charset){\n\t\ttry {\n\t\t\treturn out.toString(charset.name());\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/LimitedInputStream.java",
    "content": "package cn.hutool.core.io;\n\nimport java.io.FilterInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * 限制读取最大长度的{@link FilterInputStream} 实现<br>\n * 来自：https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java\n *\n * @author jadx\n */\npublic class LimitedInputStream extends FilterInputStream {\n\n\tprivate final long maxSize;\n\tprivate long currentPos;\n\n\t/**\n\t * 构造\n\t * @param in {@link InputStream}\n\t * @param maxSize 限制最大读取量，单位byte\n\t */\n\tpublic LimitedInputStream(InputStream in, long maxSize) {\n\t\tsuper(in);\n\t\tthis.maxSize = maxSize;\n\t}\n\n\t@Override\n\tpublic int read() throws IOException {\n\t\tfinal int data = super.read();\n\t\tif (data != -1) {\n\t\t\tcurrentPos++;\n\t\t\tcheckPos();\n\t\t}\n\t\treturn data;\n\t}\n\n\t@Override\n\tpublic int read(byte[] b, int off, int len) throws IOException {\n\t\tfinal int count = super.read(b, off, len);\n\t\tif (count > 0) {\n\t\t\tcurrentPos += count;\n\t\t\tcheckPos();\n\t\t}\n\t\treturn count;\n\t}\n\n\t@Override\n\tpublic long skip(long n) throws IOException {\n\t\tfinal long skipped = super.skip(n);\n\t\tif (skipped != 0) {\n\t\t\tcurrentPos += skipped;\n\t\t\tcheckPos();\n\t\t}\n\t\treturn skipped;\n\t}\n\n\tprivate void checkPos() {\n\t\tif (currentPos > maxSize) {\n\t\t\tthrow new IllegalStateException(\"Read limit exceeded\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/LineHandler.java",
    "content": "package cn.hutool.core.io;\n\n/**\n * 行处理器\n * @author Looly\n *\n */\n@FunctionalInterface\npublic interface LineHandler {\n\t/**\n\t * 处理一行数据，可以编辑后存入指定地方\n\t * @param line 行\n\t */\n\tvoid handle(String line);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/ManifestUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.net.JarURLConnection;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.util.jar.JarFile;\nimport java.util.jar.Manifest;\n\n/**\n * Jar包中manifest.mf文件获取和解析工具类\n * 来自Jodd\n *\n * @author looly, jodd\n * @since 5.7.0\n */\npublic class ManifestUtil {\n\tprivate static final String[] MANIFEST_NAMES = {\"Manifest.mf\", \"manifest.mf\", \"MANIFEST.MF\"};\n\n\t/**\n\t * 根据 class 获取 所在 jar 包文件的 Manifest<br>\n\t * 此方法主要利用class定位jar包，如引入hutool-all，则传入hutool中任意一个类即可获取这个jar的Manifest信息<br>\n\t * 如果这个类不在jar包中，返回{@code null}\n\t *\n\t * @param cls 类\n\t * @return Manifest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Manifest getManifest(Class<?> cls) throws IORuntimeException {\n\t\tURL url = ResourceUtil.getResource(null, cls);\n\t\tURLConnection connection;\n\t\ttry {\n\t\t\tconnection = url.openConnection();\n\t\t}catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (connection instanceof JarURLConnection) {\n\t\t\tJarURLConnection conn = (JarURLConnection) connection;\n\t\t\treturn getManifest(conn);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取 jar 包文件或项目目录下的 Manifest\n\t *\n\t * @param classpathItem 文件路径\n\t * @return Manifest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Manifest getManifest(File classpathItem) throws IORuntimeException{\n\t\tManifest manifest = null;\n\n\t\tif (classpathItem.isFile()) {\n\t\t\ttry (JarFile jarFile = new JarFile(classpathItem)){\n\t\t\t\tmanifest = getManifest(jarFile);\n\t\t\t} catch (final IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t} else {\n\t\t\tfinal File metaDir = new File(classpathItem, \"META-INF\");\n\t\t\tFile manifestFile = null;\n\t\t\tif (metaDir.isDirectory()) {\n\t\t\t\tfor (final String name : MANIFEST_NAMES) {\n\t\t\t\t\tfinal File mFile = new File(metaDir, name);\n\t\t\t\t\tif (mFile.isFile()) {\n\t\t\t\t\t\tmanifestFile = mFile;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (null != manifestFile) {\n\t\t\t\ttry(FileInputStream fis = new FileInputStream(manifestFile)){\n\t\t\t\t\tmanifest = new Manifest(fis);\n\t\t\t\t} catch (final IOException e) {\n\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn manifest;\n\t}\n\n\t/**\n\t * 根据 {@link JarURLConnection} 获取 jar 包文件的 Manifest\n\t *\n\t * @param connection {@link JarURLConnection}\n\t * @return Manifest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Manifest getManifest(JarURLConnection connection) throws IORuntimeException{\n\t\tfinal JarFile jarFile;\n\t\ttry {\n\t\t\tjarFile = connection.getJarFile();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn getManifest(jarFile);\n\t}\n\n\t/**\n\t * 根据 {@link JarURLConnection} 获取 jar 包文件的 Manifest\n\t *\n\t * @param jarFile {@link JarURLConnection}\n\t * @return Manifest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Manifest getManifest(JarFile jarFile) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn jarFile.getManifest();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.copy.ChannelCopier;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.MappedByteBuffer;\nimport java.nio.channels.Channels;\nimport java.nio.channels.FileChannel;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.channels.WritableByteChannel;\nimport java.nio.charset.Charset;\n\n/**\n * NIO相关工具封装，主要针对Channel读写、拷贝等封装\n *\n * @author looly\n * @since 5.5.3\n */\npublic class NioUtil {\n\n\t/**\n\t * 默认缓存大小 8192\n\t */\n\tpublic static final int DEFAULT_BUFFER_SIZE = 2 << 12;\n\t/**\n\t * 默认中等缓存大小 16384\n\t */\n\tpublic static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13;\n\t/**\n\t * 默认大缓存大小 32768\n\t */\n\tpublic static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14;\n\n\t/**\n\t * 数据流末尾\n\t */\n\tpublic static final int EOF = -1;\n\n\t/**\n\t * 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java<br>\n\t * 本方法不会关闭流\n\t *\n\t * @param in             输入流\n\t * @param out            输出流\n\t * @param bufferSize     缓存大小\n\t * @param streamProgress 进度条\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn copyByNIO(in, out, bufferSize, -1, streamProgress);\n\t}\n\n\t/**\n\t * 拷贝流<br>\n\t * 本方法不会关闭流\n\t *\n\t * @param in             输入流\n\t * @param out            输出流\n\t * @param bufferSize     缓存大小\n\t * @param count          最大长度\n\t * @param streamProgress 进度条\n\t * @return 传输的byte数\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static long copyByNIO(InputStream in, OutputStream out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException {\n\t\tfinal long copySize = copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, count, streamProgress);\n\t\tIoUtil.flush(out);\n\t\treturn copySize;\n\t}\n\n\t/**\n\t * 拷贝文件Channel，使用NIO，拷贝后不会关闭channel\n\t *\n\t * @param inChannel  {@link FileChannel}\n\t * @param outChannel {@link FileChannel}\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.3\n\t */\n\tpublic static long copy(FileChannel inChannel, FileChannel outChannel) throws IORuntimeException {\n\t\tAssert.notNull(inChannel, \"In channel is null!\");\n\t\tAssert.notNull(outChannel, \"Out channel is null!\");\n\n\t\ttry {\n\t\t\treturn copySafely(inChannel, outChannel);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 文件拷贝实现\n\t *\n\t * <pre>\n\t * FileChannel#transferTo 或 FileChannel#transferFrom 的实现是平台相关的，需要确保低版本平台的兼容性\n\t * 例如 android 7以下平台在使用 ZipInputStream 解压文件的过程中，\n\t * 通过 FileChannel#transferFrom 传输到文件时，其返回值可能小于 totalBytes，不处理将导致文件内容缺失\n\t *\n\t * // 错误写法，dstChannel.transferFrom 返回值小于 zipEntry.getSize()，导致解压后文件内容缺失\n\t * try (InputStream srcStream = zipFile.getInputStream(zipEntry);\n\t * \t\tReadableByteChannel srcChannel = Channels.newChannel(srcStream);\n\t * \t\tFileOutputStream fos = new FileOutputStream(saveFile);\n\t * \t\tFileChannel dstChannel = fos.getChannel()) {\n\t * \t\tdstChannel.transferFrom(srcChannel, 0, zipEntry.getSize());\n\t *  }\n\t * </pre>\n\t *\n\t * @param inChannel  输入通道\n\t * @param outChannel 输出通道\n\t * @return 输入通道的字节数\n\t * @throws IOException 发生IO错误\n\t * @link http://androidxref.com/6.0.1_r10/xref/libcore/luni/src/main/java/java/nio/FileChannelImpl.java\n\t * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/java/sun/nio/ch/FileChannelImpl.java\n\t * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/native/FileChannelImpl.c\n\t * @author z8g\n\t * @since 5.7.21\n\t */\n\tprivate static long copySafely(FileChannel inChannel, FileChannel outChannel) throws IOException {\n\t\tfinal long totalBytes = inChannel.size();\n\t\tfor (long pos = 0, remaining = totalBytes; remaining > 0; ) { // 确保文件内容不会缺失\n\t\t\tfinal long writeBytes = inChannel.transferTo(pos, remaining, outChannel); // 实际传输的字节数\n\t\t\tpos += writeBytes;\n\t\t\tremaining -= writeBytes;\n\t\t}\n\t\treturn totalBytes;\n\t}\n\n\t/**\n\t * 拷贝流，使用NIO，不会关闭channel\n\t *\n\t * @param in  {@link ReadableByteChannel}\n\t * @param out {@link WritableByteChannel}\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.0\n\t */\n\tpublic static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException {\n\t\treturn copy(in, out, DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 拷贝流，使用NIO，不会关闭channel\n\t *\n\t * @param in         {@link ReadableByteChannel}\n\t * @param out        {@link WritableByteChannel}\n\t * @param bufferSize 缓冲大小，如果小于等于0，使用默认\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.0\n\t */\n\tpublic static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException {\n\t\treturn copy(in, out, bufferSize, null);\n\t}\n\n\t/**\n\t * 拷贝流，使用NIO，不会关闭channel\n\t *\n\t * @param in             {@link ReadableByteChannel}\n\t * @param out            {@link WritableByteChannel}\n\t * @param bufferSize     缓冲大小，如果小于等于0，使用默认\n\t * @param streamProgress {@link StreamProgress}进度处理器\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn copy(in, out, bufferSize, -1, streamProgress);\n\t}\n\n\t/**\n\t * 拷贝流，使用NIO，不会关闭channel\n\t *\n\t * @param in             {@link ReadableByteChannel}\n\t * @param out            {@link WritableByteChannel}\n\t * @param bufferSize     缓冲大小，如果小于等于0，使用默认\n\t * @param count          读取总长度\n\t * @param streamProgress {@link StreamProgress}进度处理器\n\t * @return 拷贝的字节数\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, long count, StreamProgress streamProgress) throws IORuntimeException {\n\t\treturn new ChannelCopier(bufferSize, count, streamProgress).copy(in, out);\n\t}\n\n\t/**\n\t * 从流中读取内容，读取完毕后并不关闭流\n\t *\n\t * @param channel 可读通道，读取完毕后并不关闭通道\n\t * @param charset 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.0\n\t */\n\tpublic static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException {\n\t\tFastByteArrayOutputStream out = read(channel);\n\t\treturn null == charset ? out.toString() : out.toString(charset);\n\t}\n\n\t/**\n\t * 从流中读取内容，读到输出流中\n\t *\n\t * @param channel 可读通道，读取完毕后并不关闭通道\n\t * @return 输出流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException {\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream();\n\t\tcopy(channel, Channels.newChannel(out));\n\t\treturn out;\n\t}\n\n\t/**\n\t * 从FileChannel中读取UTF-8编码内容\n\t *\n\t * @param fileChannel 文件管道\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String readUtf8(FileChannel fileChannel) throws IORuntimeException {\n\t\treturn read(fileChannel, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从FileChannel中读取内容，读取完毕后并不关闭Channel\n\t *\n\t * @param fileChannel 文件管道\n\t * @param charsetName 字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException {\n\t\treturn read(fileChannel, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 从FileChannel中读取内容\n\t *\n\t * @param fileChannel 文件管道\n\t * @param charset     字符集\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException {\n\t\tMappedByteBuffer buffer;\n\t\ttry {\n\t\t\tbuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn StrUtil.str(buffer, charset);\n\t}\n\n\t/**\n\t * 关闭<br>\n\t * 关闭失败不会抛出异常\n\t *\n\t * @param closeable 被关闭的对象\n\t */\n\tpublic static void close(AutoCloseable closeable) {\n\t\tif (null != closeable) {\n\t\t\ttry {\n\t\t\t\tcloseable.close();\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 静默关闭\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/NullOutputStream.java",
    "content": "package cn.hutool.core.io;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * 此OutputStream写出数据到<b>/dev/null</b>，即忽略所有数据<br>\n * 来自 Apache Commons io\n *\n * @author looly\n * @since 4.0.6\n */\npublic class NullOutputStream extends OutputStream {\n\n\t/**\n\t * 单例\n\t */\n\tpublic static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream();\n\n\t/**\n\t * 什么也不做，写出到<code>/dev/null</code>.\n\t *\n\t * @param b 写出的数据\n\t * @param off 开始位置\n\t * @param len 长度\n\t */\n\t@Override\n\tpublic void write(byte[] b, int off, int len) {\n\t\t// to /dev/null\n\t}\n\n\t/**\n\t * 什么也不做，写出到 <code>/dev/null</code>.\n\t *\n\t * @param b 写出的数据\n\t */\n\t@Override\n\tpublic void write(int b) {\n\t\t// to /dev/null\n\t}\n\n\t/**\n\t * 什么也不做，写出到 <code>/dev/null</code>.\n\t *\n\t * @param b 写出的数据\n\t * @throws IOException 不抛出\n\t */\n\t@Override\n\tpublic void write(byte[] b) throws IOException {\n\t\t// to /dev/null\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/StreamProgress.java",
    "content": "package cn.hutool.core.io;\n\n/**\n * Stream进度条<br>\n * 提供流拷贝进度监测，如开始、结束触发，以及进度回调。<br>\n * 注意进度回调的{@code total}参数为总大小，某些场景下无总大小的标记，则此值应为-1或者{@link Long#MAX_VALUE}，表示此参数无效。\n *\n * @author Looly\n */\npublic interface StreamProgress {\n\n\t/**\n\t * 开始\n\t */\n\tvoid start();\n\n\t/**\n\t * 进行中\n\t *\n\t * @param total        总大小，如果未知为 -1或者{@link Long#MAX_VALUE}\n\t * @param progressSize 已经进行的大小\n\t */\n\tvoid progress(long total, long progressSize);\n\n\t/**\n\t * 结束\n\t */\n\tvoid finish();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.collection.CollUtil;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InvalidClassException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectStreamClass;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * 带有类验证的对象流，用于避免反序列化漏洞<br>\n * 详细见：https://xz.aliyun.com/t/41/\n *\n * @author looly\n * @since 5.2.6\n */\npublic class ValidateObjectInputStream extends ObjectInputStream {\n\n\tprivate Set<String> whiteClassSet;\n\tprivate Set<String> blackClassSet;\n\n\t/**\n\t * 构造\n\t *\n\t * @param inputStream 流\n\t * @param acceptClasses 白名单的类\n\t * @throws IOException IO异常\n\t */\n\tpublic ValidateObjectInputStream(InputStream inputStream, Class<?>... acceptClasses) throws IOException {\n\t\tsuper(inputStream);\n\t\taccept(acceptClasses);\n\t}\n\n\t/**\n\t * 禁止反序列化的类，用于反序列化验证\n\t *\n\t * @param refuseClasses 禁止反序列化的类\n\t * @since 5.3.5\n\t */\n\tpublic void refuse(Class<?>... refuseClasses) {\n\t\tif(null == this.blackClassSet){\n\t\t\tthis.blackClassSet = new HashSet<>();\n\t\t}\n\t\tfor (Class<?> acceptClass : refuseClasses) {\n\t\t\tthis.blackClassSet.add(acceptClass.getName());\n\t\t}\n\t}\n\n\t/**\n\t * 接受反序列化的类，用于反序列化验证\n\t *\n\t * @param acceptClasses 接受反序列化的类\n\t */\n\tpublic void accept(Class<?>... acceptClasses) {\n\t\tif(null == this.whiteClassSet){\n\t\t\tthis.whiteClassSet = new HashSet<>();\n\t\t}\n\t\tfor (Class<?> acceptClass : acceptClasses) {\n\t\t\tthis.whiteClassSet.add(acceptClass.getName());\n\t\t}\n\t}\n\n\t/**\n\t * 只允许反序列化SerialObject class\n\t */\n\t@Override\n\tprotected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {\n\t\tvalidateClassName(desc.getName());\n\t\treturn super.resolveClass(desc);\n\t}\n\n\t/**\n\t * 验证反序列化的类是否合法\n\t * @param className 类名\n\t * @throws InvalidClassException 非法类\n\t */\n\tprivate void validateClassName(String className) throws InvalidClassException {\n\t\t// 黑名单\n\t\tif(CollUtil.isNotEmpty(this.blackClassSet)){\n\t\t\tif(this.blackClassSet.contains(className)){\n\t\t\t\tthrow new InvalidClassException(\"Unauthorized deserialization attempt by black list\", className);\n\t\t\t}\n\t\t}\n\n\t\tif(CollUtil.isEmpty(this.whiteClassSet)){\n\t\t\treturn;\n\t\t}\n\t\tif(className.startsWith(\"java.\")){\n\t\t\t// java中的类默认在白名单中\n\t\t\treturn;\n\t\t}\n\t\tif(this.whiteClassSet.contains(className)){\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new InvalidClassException(\"Unauthorized deserialization attempt\", className);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC16.java",
    "content": "package cn.hutool.core.io.checksum;\n\nimport cn.hutool.core.io.checksum.crc16.CRC16Checksum;\nimport cn.hutool.core.io.checksum.crc16.CRC16IBM;\n\nimport java.io.Serializable;\nimport java.util.zip.Checksum;\n\n/**\n * CRC16 循环冗余校验码（Cyclic Redundancy Check）实现，默认IBM算法\n *\n * @author looly\n * @since 4.4.1\n */\npublic class CRC16 implements Checksum, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final CRC16Checksum crc16;\n\n\tpublic CRC16() {\n\t\tthis(new CRC16IBM());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param crc16Checksum {@link CRC16Checksum} 实现\n\t */\n\tpublic CRC16(CRC16Checksum crc16Checksum) {\n\t\tthis.crc16 = crc16Checksum;\n\t}\n\n\t/**\n\t * 获取16进制的CRC16值\n\t *\n\t * @return 16进制的CRC16值\n\t * @since 5.7.22\n\t */\n\tpublic String getHexValue() {\n\t\treturn this.crc16.getHexValue();\n\t}\n\n\t/**\n\t * 获取16进制的CRC16值\n\t *\n\t * @param isPadding 不足4位时，是否填充0以满足位数\n\t * @return 16进制的CRC16值，4位\n\t * @since 5.7.22\n\t */\n\tpublic String getHexValue(boolean isPadding) {\n\t\treturn crc16.getHexValue(isPadding);\n\t}\n\n\t@Override\n\tpublic long getValue() {\n\t\treturn crc16.getValue();\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\tcrc16.reset();\n\t}\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tcrc16.update(b, off, len);\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\tcrc16.update(b);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC8.java",
    "content": "package cn.hutool.core.io.checksum;\n\nimport java.io.Serializable;\nimport java.util.zip.Checksum;\n\n/**\n * CRC8 循环冗余校验码（Cyclic Redundancy Check）实现<br>\n * 代码来自：https://github.com/BBSc0der\n * \n * @author Bolek,Looly\n * @since 4.4.1\n */\npublic class CRC8 implements Checksum, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final short init;\n\tprivate final short[] crcTable = new short[256];\n\tprivate short value;\n\n\t/**\n\t * 构造<br>\n\t * \n\t * @param polynomial Polynomial, typically one of the POLYNOMIAL_* constants.\n\t * @param init Initial value, typically either 0xff or zero.\n\t */\n\tpublic CRC8(int polynomial, short init) {\n\t\tthis.value = this.init = init;\n\t\tfor (int dividend = 0; dividend < 256; dividend++) {\n\t\t\tint remainder = dividend;// << 8;\n\t\t\tfor (int bit = 0; bit < 8; ++bit) {\n\t\t\t\tif ((remainder & 0x01) != 0) {\n\t\t\t\t\tremainder = (remainder >>> 1) ^ polynomial;\n\t\t\t\t} else {\n\t\t\t\t\tremainder >>>= 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcrcTable[dividend] = (short) remainder;\n\t\t}\n\t}\n\n\t@Override\n\tpublic void update(byte[] buffer, int offset, int len) {\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tint data = buffer[offset + i] ^ value;\n\t\t\tvalue = (short) (crcTable[data & 0xff] ^ (value << 8));\n\t\t}\n\t}\n\n\t/**\n\t * Updates the current checksum with the specified array of bytes. Equivalent to calling <code>update(buffer, 0, buffer.length)</code>.\n\t * \n\t * @param buffer the byte array to update the checksum with\n\t */\n\tpublic void update(byte[] buffer) {\n\t\tupdate(buffer, 0, buffer.length);\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\tupdate(new byte[] { (byte) b }, 0, 1);\n\t}\n\n\t@Override\n\tpublic long getValue() {\n\t\treturn value & 0xff;\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\tvalue = init;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Ansi.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_ANSI\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16Ansi extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xa001;\n\n\t@Override\n\tpublic void reset() {\n\t\tthis.wCRCin = 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\tint hi = wCRCin >> 8;\n\t\thi ^= b;\n\t\twCRCin = hi;\n\n\t\tfor (int i = 0; i < 8; i++) {\n\t\t\tint flag = wCRCin & 0x0001;\n\t\t\twCRCin = wCRCin >> 1;\n\t\t\tif (flag == 1) {\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITT.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_CCITT：多项式x16+x12+x5+1（0x1021），初始值0x0000，低位在前，高位在后，结果与0x0000异或\n * 0x8408是0x1021按位颠倒后的结果。\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16CCITT extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0x8408;\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16CCITTFalse.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_CCITT_FALSE：多项式x16+x12+x5+1（0x1021），初始值0xFFFF，低位在后，高位在前，结果与0x0000异或\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16CCITTFalse extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0x1021;\n\n\t@Override\n\tpublic void reset() {\n\t\tthis.wCRCin = 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin &= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\tfor (int i = 0; i < 8; i++) {\n\t\t\tboolean bit = ((b >> (7 - i) & 1) == 1);\n\t\t\tboolean c15 = ((wCRCin >> 15 & 1) == 1);\n\t\t\twCRCin <<= 1;\n\t\t\tif (c15 ^ bit)\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Checksum.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.zip.Checksum;\n\n/**\n * CRC16 Checksum，用于提供多种CRC16算法的通用实现<br>\n * 通过继承此类，重写update和reset完成相应算法。\n *\n * @author looly\n * @since 5.3.10\n */\npublic abstract class CRC16Checksum implements Checksum, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * CRC16 Checksum 结果值\n\t */\n\tprotected int wCRCin;\n\n\tpublic CRC16Checksum(){\n\t\treset();\n\t}\n\n\t@Override\n\tpublic long getValue() {\n\t\treturn wCRCin;\n\t}\n\n\t/**\n\t * 获取16进制的CRC16值\n\t *\n\t * @return 16进制的CRC16值\n\t */\n\tpublic String getHexValue(){\n\t\treturn getHexValue(false);\n\t}\n\n\t/**\n\t * 获取16进制的CRC16值\n\t * @param isPadding 不足4位时，是否填充0以满足位数\n\t * @return 16进制的CRC16值，4位\n\t */\n\tpublic String getHexValue(boolean isPadding){\n\t\tString hex = HexUtil.toHex(getValue());\n\t\tif(isPadding){\n\t\t\thex = StrUtil.padPre(hex, 4, '0');\n\t\t}\n\n\t\treturn hex;\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\twCRCin = 0x0000;\n\t}\n\n\t/**\n\t * 计算全部字节\n\t * @param b 字节\n\t */\n\tpublic void update(byte[] b){\n\t\tupdate(b, 0, b.length);\n\t}\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tfor (int i = off; i < off + len; i++)\n\t\t\tupdate(b[i]);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16DNP.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_DNP：多项式x16+x13+x12+x11+x10+x8+x6+x5+x2+1（0x3D65），初始值0x0000，低位在前，高位在后，结果与0xFFFF异或\n * 0xA6BC是0x3D65按位颠倒后的结果\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16DNP extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xA6BC;\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin ^= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16IBM.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_IBM：多项式x16+x15+x2+1（0x8005），初始值0x0000，低位在前，高位在后，结果与0x0000异或\n * 0xA001是0x8005按位颠倒后的结果\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16IBM extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xa001;\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Maxim.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_MAXIM：多项式x16+x15+x2+1（0x8005），初始值0x0000，低位在前，高位在后，结果与0xFFFF异或\n * 0xA001是0x8005按位颠倒后的结果\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16Maxim extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xa001;\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin ^= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16Modbus.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC-16 (Modbus)\n * CRC16_MODBUS：多项式x16+x15+x2+1（0x8005），初始值0xFFFF，低位在前，高位在后，结果与0x0000异或\n * 0xA001是0x8005按位颠倒后的结果\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16Modbus extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xa001;\n\n\t@Override\n\tpublic void reset(){\n\t\tthis.wCRCin = 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16USB.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_USB：多项式x16+x15+x2+1（0x8005），初始值0xFFFF，低位在前，高位在后，结果与0xFFFF异或\n * 0xA001是0x8005按位颠倒后的结果\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16USB extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0xa001;\n\n\t@Override\n\tpublic void reset(){\n\t\tthis.wCRCin = 0xFFFF;\n\t}\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin ^= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16X25.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC16_X25：多项式x16+x12+x5+1（0x1021），初始值0xffff，低位在前，高位在后，结果与0xFFFF异或\n * 0x8408是0x1021按位颠倒后的结果。\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16X25 extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int WC_POLY = 0x8408;\n\n\t@Override\n\tpublic void reset(){\n\t\tthis.wCRCin = 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin ^= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\twCRCin ^= (b & 0x00ff);\n\t\tfor (int j = 0; j < 8; j++) {\n\t\t\tif ((wCRCin & 0x0001) != 0) {\n\t\t\t\twCRCin >>= 1;\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t\t} else {\n\t\t\t\twCRCin >>= 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/CRC16XModem.java",
    "content": "package cn.hutool.core.io.checksum.crc16;\n\n/**\n * CRC-CCITT (XModem)\n * CRC16_XMODEM：多项式x16+x12+x5+1（0x1021），初始值0x0000，低位在后，高位在前，结果与0x0000异或\n *\n * @author looly\n * @since 5.3.10\n */\npublic class CRC16XModem extends CRC16Checksum{\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 0001 0000 0010 0001 (0, 5, 12)\n\tprivate static final int WC_POLY = 0x1021;\n\n\t@Override\n\tpublic void update(byte[] b, int off, int len) {\n\t\tsuper.update(b, off, len);\n\t\twCRCin &= 0xffff;\n\t}\n\n\t@Override\n\tpublic void update(int b) {\n\t\tfor (int i = 0; i < 8; i++) {\n\t\t\tboolean bit = ((b >> (7 - i) & 1) == 1);\n\t\t\tboolean c15 = ((wCRCin >> 15 & 1) == 1);\n\t\t\twCRCin <<= 1;\n\t\t\tif (c15 ^ bit)\n\t\t\t\twCRCin ^= WC_POLY;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/crc16/package-info.java",
    "content": "/**\n * CRC16相关算法封装为Checksum\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.checksum.crc16;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/checksum/package-info.java",
    "content": "/**\n * IO校验相关库和工具\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.checksum;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/copy/ChannelCopier.java",
    "content": "package cn.hutool.core.io.copy;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.channels.WritableByteChannel;\n\n/**\n * {@link ReadableByteChannel} 向 {@link WritableByteChannel} 拷贝\n *\n * @author looly\n * @since 5.7.8\n */\npublic class ChannelCopier extends IoCopier<ReadableByteChannel, WritableByteChannel> {\n\n\t/**\n\t * 构造\n\t */\n\tpublic ChannelCopier() {\n\t\tthis(IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t */\n\tpublic ChannelCopier(int bufferSize) {\n\t\tthis(bufferSize, -1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t */\n\tpublic ChannelCopier(int bufferSize, long count) {\n\t\tthis(bufferSize, count, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t * @param progress   进度条\n\t */\n\tpublic ChannelCopier(int bufferSize, long count, StreamProgress progress) {\n\t\tsuper(bufferSize, count, progress);\n\t}\n\n\t@Override\n\tpublic long copy(ReadableByteChannel source, WritableByteChannel target) {\n\t\tAssert.notNull(source, \"InputStream is null !\");\n\t\tAssert.notNull(target, \"OutputStream is null !\");\n\n\t\tfinal StreamProgress progress = this.progress;\n\t\tif (null != progress) {\n\t\t\tprogress.start();\n\t\t}\n\t\tfinal long size;\n\t\ttry {\n\t\t\tsize = doCopy(source, target, ByteBuffer.allocate(bufferSize(this.count)), progress);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null != progress) {\n\t\t\tprogress.finish();\n\t\t}\n\t\treturn size;\n\t}\n\n\t/**\n\t * 执行拷贝，如果限制最大长度，则按照最大长度读取，否则一直读取直到遇到-1\n\t *\n\t * @param source   {@link InputStream}\n\t * @param target   {@link OutputStream}\n\t * @param buffer   缓存\n\t * @param progress 进度条\n\t * @return 拷贝总长度\n\t * @throws IOException IO异常\n\t */\n\tprivate long doCopy(ReadableByteChannel source, WritableByteChannel target, ByteBuffer buffer, StreamProgress progress) throws IOException {\n\t\tlong numToRead = this.count > 0 ? this.count : Long.MAX_VALUE;\n\t\tlong total = 0;\n\n\t\tint read;\n\t\twhile (numToRead > 0) {\n\t\t\tread = source.read(buffer);\n\t\t\tif (read < 0) {\n\t\t\t\t// 提前读取到末尾\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tbuffer.flip();// 写转读\n\t\t\ttarget.write(buffer);\n\t\t\tbuffer.clear();\n\n\t\t\tnumToRead -= read;\n\t\t\ttotal += read;\n\t\t\tif (null != progress) {\n\t\t\t\tprogress.progress(this.count, total);\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/copy/IoCopier.java",
    "content": "package cn.hutool.core.io.copy;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\n\n/**\n * IO拷贝抽象，可自定义包括缓存、进度条等信息<br>\n * 此对象非线程安全\n *\n * @param <S> 拷贝源类型，如InputStream、Reader等\n * @param <T> 拷贝目标类型，如OutputStream、Writer等\n * @author looly\n * @since 5.7.8\n */\npublic abstract class IoCopier<S, T> {\n\n\tprotected final int bufferSize;\n\t/**\n\t * 拷贝总数\n\t */\n\tprotected final long count;\n\n\t/**\n\t * 进度条\n\t */\n\tprotected StreamProgress progress;\n\n\t/**\n\t * 是否每次写出一个buffer内容就执行flush\n\t */\n\tprotected boolean flushEveryBuffer;\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小，&lt; 0 表示默认{@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t * @param count      拷贝总数，-1表示无限制\n\t * @param progress   进度条\n\t */\n\tpublic IoCopier(int bufferSize, long count, StreamProgress progress) {\n\t\tthis.bufferSize = bufferSize > 0 ? bufferSize : IoUtil.DEFAULT_BUFFER_SIZE;\n\t\tthis.count = count <= 0 ? Long.MAX_VALUE : count;\n\t\tthis.progress = progress;\n\t}\n\n\t/**\n\t * 执行拷贝\n\t *\n\t * @param source 拷贝源，如InputStream、Reader等\n\t * @param target 拷贝目标，如OutputStream、Writer等\n\t * @return 拷贝的实际长度\n\t */\n\tpublic abstract long copy(S source, T target);\n\n\t/**\n\t * 缓存大小，取默认缓存和目标长度最小值\n\t *\n\t * @param count 目标长度\n\t * @return 缓存大小\n\t */\n\tprotected int bufferSize(long count) {\n\t\treturn (int) Math.min(this.bufferSize, count);\n\t}\n\n\t/**\n\t * 设置是否每次写出一个buffer内容就执行flush\n\t *\n\t * @param flushEveryBuffer 是否每次写出一个buffer内容就执行flush\n\t * @return this\n\t * @since 5.7.18\n\t */\n\tpublic IoCopier<S, T> setFlushEveryBuffer(boolean flushEveryBuffer){\n\t\tthis.flushEveryBuffer = flushEveryBuffer;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/copy/ReaderWriterCopier.java",
    "content": "package cn.hutool.core.io.copy;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Reader;\nimport java.io.Writer;\n\n/**\n * {@link Reader} 向 {@link Writer} 拷贝\n *\n * @author looly\n * @since 5.7.8\n */\npublic class ReaderWriterCopier extends IoCopier<Reader, Writer> {\n\n\t/**\n\t * 构造\n\t */\n\tpublic ReaderWriterCopier() {\n\t\tthis(IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t */\n\tpublic ReaderWriterCopier(int bufferSize) {\n\t\tthis(bufferSize, -1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t */\n\tpublic ReaderWriterCopier(int bufferSize, long count) {\n\t\tthis(bufferSize, count, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t * @param progress   进度条\n\t */\n\tpublic ReaderWriterCopier(int bufferSize, long count, StreamProgress progress) {\n\t\tsuper(bufferSize, count, progress);\n\t}\n\n\t@Override\n\tpublic long copy(Reader source, Writer target) {\n\t\tAssert.notNull(source, \"InputStream is null !\");\n\t\tAssert.notNull(target, \"OutputStream is null !\");\n\n\t\tfinal StreamProgress progress = this.progress;\n\t\tif (null != progress) {\n\t\t\tprogress.start();\n\t\t}\n\t\tfinal long size;\n\t\ttry {\n\t\t\tsize = doCopy(source, target, new char[bufferSize(this.count)], progress);\n\t\t\ttarget.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null != progress) {\n\t\t\tprogress.finish();\n\t\t}\n\t\treturn size;\n\t}\n\n\t/**\n\t * 执行拷贝，如果限制最大长度，则按照最大长度读取，否则一直读取直到遇到-1\n\t *\n\t * @param source   {@link InputStream}\n\t * @param target   {@link OutputStream}\n\t * @param buffer   缓存\n\t * @param progress 进度条\n\t * @return 拷贝总长度\n\t * @throws IOException IO异常\n\t */\n\tprivate long doCopy(Reader source, Writer target, char[] buffer, StreamProgress progress) throws IOException {\n\t\tlong numToRead = this.count > 0 ? this.count : Long.MAX_VALUE;\n\t\tlong total = 0;\n\n\t\tint read;\n\t\twhile (numToRead > 0) {\n\t\t\tread = source.read(buffer, 0, bufferSize(numToRead));\n\t\t\tif (read < 0) {\n\t\t\t\t// 提前读取到末尾\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttarget.write(buffer, 0, read);\n\t\t\tif(flushEveryBuffer){\n\t\t\t\ttarget.flush();\n\t\t\t}\n\n\t\t\tnumToRead -= read;\n\t\t\ttotal += read;\n\t\t\tif (null != progress) {\n\t\t\t\tprogress.progress(this.count, total);\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/copy/StreamCopier.java",
    "content": "package cn.hutool.core.io.copy;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * {@link InputStream} 向 {@link OutputStream} 拷贝\n *\n * @author looly\n * @since 5.7.8\n */\npublic class StreamCopier extends IoCopier<InputStream, OutputStream> {\n\n\t/**\n\t * 构造\n\t */\n\tpublic StreamCopier() {\n\t\tthis(IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t */\n\tpublic StreamCopier(int bufferSize) {\n\t\tthis(bufferSize, -1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t */\n\tpublic StreamCopier(int bufferSize, long count) {\n\t\tthis(bufferSize, count, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bufferSize 缓存大小\n\t * @param count      拷贝总数\n\t * @param progress   进度条\n\t */\n\tpublic StreamCopier(int bufferSize, long count, StreamProgress progress) {\n\t\tsuper(bufferSize, count, progress);\n\t}\n\n\t@Override\n\tpublic long copy(InputStream source, OutputStream target) {\n\t\tAssert.notNull(source, \"InputStream is null !\");\n\t\tAssert.notNull(target, \"OutputStream is null !\");\n\n\t\tfinal StreamProgress progress = this.progress;\n\t\tif (null != progress) {\n\t\t\tprogress.start();\n\t\t}\n\t\tfinal long size;\n\t\ttry {\n\t\t\tsize = doCopy(source, target, new byte[bufferSize(this.count)], progress);\n\t\t\ttarget.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tif (null != progress) {\n\t\t\tprogress.finish();\n\t\t}\n\n\t\treturn size;\n\t}\n\n\t/**\n\t * 执行拷贝，如果限制最大长度，则按照最大长度读取，否则一直读取直到遇到-1\n\t *\n\t * @param source   {@link InputStream}\n\t * @param target   {@link OutputStream}\n\t * @param buffer   缓存\n\t * @param progress 进度条\n\t * @return 拷贝总长度\n\t * @throws IOException IO异常\n\t */\n\tprivate long doCopy(InputStream source, OutputStream target, byte[] buffer, StreamProgress progress) throws IOException {\n\t\tlong numToRead = this.count > 0 ? this.count : Long.MAX_VALUE;\n\t\tlong total = 0;\n\n\t\tint read;\n\t\twhile (numToRead > 0) {\n\t\t\tread = source.read(buffer, 0, bufferSize(numToRead));\n\t\t\tif (read < 0) {\n\t\t\t\t// 提前读取到末尾\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttarget.write(buffer, 0, read);\n\t\t\tif(flushEveryBuffer){\n\t\t\t\ttarget.flush();\n\t\t\t}\n\n\t\t\tnumToRead -= read;\n\t\t\ttotal += read;\n\t\t\tif (null != progress) {\n\t\t\t\tprogress.progress(this.count, total);\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/copy/package-info.java",
    "content": "/**\n * IO流拷贝相关封装相关封装\n *\n * @author looly\n * @since 5.7.8\n */\npackage cn.hutool.core.io.copy;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileAppender.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.thread.lock.LockUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.File;\nimport java.io.PrintWriter;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.locks.Lock;\n\n/**\n * 文件追加器<br>\n * 持有一个文件，在内存中积累一定量的数据后统一追加到文件<br>\n * 此类只有在写入文件时打开文件，并在写入结束后关闭之。因此此类不需要关闭<br>\n * 在调用append方法后会缓存于内存，只有超过容量后才会一次性写入文件，因此内存中随时有剩余未写入文件的内容，在最后必须调用flush方法将剩余内容刷入文件\n *\n * @author looly\n * @since 3.1.2\n */\npublic class FileAppender implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final FileWriter writer;\n\t/**\n\t * 内存中持有的字符串数\n\t */\n\tprivate final int capacity;\n\t/**\n\t * 追加内容是否为新行\n\t */\n\tprivate final boolean isNewLineMode;\n\t/**\n\t * 数据行缓存\n\t */\n\tprivate final List<String> list;\n\t/**\n\t * 写出锁，用于保护写出线程安全\n\t */\n\tprivate final Lock lock;\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile      目标文件\n\t * @param capacity      当行数积累多少条时刷入到文件\n\t * @param isNewLineMode 追加内容是否为新行\n\t */\n\tpublic FileAppender(File destFile, int capacity, boolean isNewLineMode) {\n\t\tthis(destFile, CharsetUtil.CHARSET_UTF_8, capacity, isNewLineMode);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile      目标文件\n\t * @param charset       编码\n\t * @param capacity      当行数积累多少条时刷入到文件\n\t * @param isNewLineMode 追加内容是否为新行\n\t */\n\tpublic FileAppender(File destFile, Charset charset, int capacity, boolean isNewLineMode) {\n\t\tthis(destFile, charset, capacity, isNewLineMode, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile      目标文件\n\t * @param charset       编码\n\t * @param capacity      当行数积累多少条时刷入到文件\n\t * @param isNewLineMode 追加内容是否为新行\n\t * @param lock          是否加锁，添加则使用给定锁保护写出，保证线程安全，{@code null}则表示无锁\n\t */\n\tpublic FileAppender(File destFile, Charset charset, int capacity, boolean isNewLineMode, Lock lock) {\n\t\tthis.capacity = capacity;\n\t\tthis.list = new ArrayList<>(capacity);\n\t\tthis.isNewLineMode = isNewLineMode;\n\t\tthis.writer = FileWriter.create(destFile, charset);\n\t\tthis.lock = ObjectUtil.defaultIfNull(lock, LockUtil::getNoLock);\n\t}\n\n\t/**\n\t * 追加\n\t *\n\t * @param line 行\n\t * @return this\n\t */\n\tpublic FileAppender append(String line) {\n\t\tif (list.size() >= capacity) {\n\t\t\tflush();\n\t\t}\n\n\t\tthis.lock.lock();\n\t\ttry{\n\t\t\tlist.add(line);\n\t\t} finally {\n\t\t\tthis.lock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 刷入到文件\n\t *\n\t * @return this\n\t */\n\tpublic FileAppender flush() {\n\t\tthis.lock.lock();\n\t\ttry{\n\t\t\ttry (PrintWriter pw = writer.getPrintWriter(true)) {\n\t\t\t\tfor (String str : list) {\n\t\t\t\t\tpw.print(str);\n\t\t\t\t\tif (isNewLineMode) {\n\t\t\t\t\t\tpw.println();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tlist.clear();\n\t\t} finally {\n\t\t\tthis.lock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.copier.SrcToDestCopier;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.Files;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\n\n/**\n * 文件拷贝器<br>\n * 支持以下几种情况：\n * <pre>\n * 1、文件复制到文件\n * 2、文件复制到目录\n * 3、目录复制到目录\n * 4、目录下的文件和目录复制到另一个目录\n * </pre>\n *\n * @author Looly\n * @since 3.0.9\n */\npublic class FileCopier extends SrcToDestCopier<File, FileCopier>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 是否覆盖目标文件 */\n\tprivate boolean isOverride;\n\t/** 是否拷贝所有属性 */\n\tprivate boolean isCopyAttributes;\n\t/** 当拷贝来源是目录时是否只拷贝目录下的内容 */\n\tprivate boolean isCopyContentIfDir;\n\t/** 当拷贝来源是目录时是否只拷贝文件而忽略子目录 */\n\tprivate boolean isOnlyCopyFile;\n\n\t//-------------------------------------------------------------------------------------------------------- static method start\n\t/**\n\t * 新建一个文件复制器\n\t * @param srcPath 源文件路径（相对ClassPath路径或绝对路径）\n\t * @param destPath 目标文件路径（相对ClassPath路径或绝对路径）\n\t * @return this\n\t */\n\tpublic static FileCopier create(String srcPath, String destPath) {\n\t\treturn new FileCopier(FileUtil.file(srcPath), FileUtil.file(destPath));\n\t}\n\n\t/**\n\t * 新建一个文件复制器\n\t * @param src 源文件\n\t * @param dest 目标文件\n\t * @return this\n\t */\n\tpublic static FileCopier create(File src, File dest) {\n\t\treturn new FileCopier(src, dest);\n\t}\n\t//-------------------------------------------------------------------------------------------------------- static method end\n\n\t//-------------------------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t * @param src 源文件\n\t * @param dest 目标文件\n\t */\n\tpublic FileCopier(File src, File dest) {\n\t\tthis.src = src;\n\t\tthis.dest = dest;\n\t}\n\t//-------------------------------------------------------------------------------------------------------- Constructor end\n\n\t//-------------------------------------------------------------------------------------------------------- Getters and Setters start\n\t/**\n\t * 是否覆盖目标文件\n\t * @return 是否覆盖目标文件\n\t */\n\tpublic boolean isOverride() {\n\t\treturn isOverride;\n\t}\n\t/**\n\t * 设置是否覆盖目标文件\n\t * @param isOverride 是否覆盖目标文件\n\t * @return this\n\t */\n\tpublic FileCopier setOverride(boolean isOverride) {\n\t\tthis.isOverride = isOverride;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否拷贝所有属性\n\t * @return 是否拷贝所有属性\n\t */\n\tpublic boolean isCopyAttributes() {\n\t\treturn isCopyAttributes;\n\t}\n\t/**\n\t * 设置是否拷贝所有属性\n\t * @param isCopyAttributes 是否拷贝所有属性\n\t * @return this\n\t */\n\tpublic FileCopier setCopyAttributes(boolean isCopyAttributes) {\n\t\tthis.isCopyAttributes = isCopyAttributes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 当拷贝来源是目录时是否只拷贝目录下的内容\n\t * @return 当拷贝来源是目录时是否只拷贝目录下的内容\n\t */\n\tpublic boolean isCopyContentIfDir() {\n\t\treturn isCopyContentIfDir;\n\t}\n\n\t/**\n\t * 当拷贝来源是目录时是否只拷贝目录下的内容\n\t * @param isCopyContentIfDir 是否只拷贝目录下的内容\n\t * @return this\n\t */\n\tpublic FileCopier setCopyContentIfDir(boolean isCopyContentIfDir) {\n\t\tthis.isCopyContentIfDir = isCopyContentIfDir;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 当拷贝来源是目录时是否只拷贝文件而忽略子目录\n\t *\n\t * @return 当拷贝来源是目录时是否只拷贝文件而忽略子目录\n\t * @since 4.1.5\n\t */\n\tpublic boolean isOnlyCopyFile() {\n\t\treturn isOnlyCopyFile;\n\t}\n\n\t/**\n\t * 设置当拷贝来源是目录时是否只拷贝文件而忽略子目录\n\t *\n\t * @param isOnlyCopyFile 当拷贝来源是目录时是否只拷贝文件而忽略子目录\n\t * @return this\n\t * @since 4.1.5\n\t */\n\tpublic FileCopier setOnlyCopyFile(boolean isOnlyCopyFile) {\n\t\tthis.isOnlyCopyFile = isOnlyCopyFile;\n\t\treturn this;\n\t}\n\t//-------------------------------------------------------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 执行拷贝<br>\n\t * 拷贝规则为：\n\t * <pre>\n\t * 1、源为文件，目标为已存在目录，则拷贝到目录下，文件名不变\n\t * 2、源为文件，目标为不存在路径，则目标以文件对待（自动创建父级目录）比如：/dest/aaa，如果aaa不存在，则aaa被当作文件名\n\t * 3、源为文件，目标是一个已存在的文件，则当{@link #setOverride(boolean)}设为true时会被覆盖，默认不覆盖\n\t * 4、源为目录，目标为已存在目录，当{@link #setCopyContentIfDir(boolean)}为true时，只拷贝目录中的内容到目标目录中，否则整个源目录连同其目录拷贝到目标目录中\n\t * 5、源为目录，目标为不存在路径，则自动创建目标为新目录，然后按照规则4复制\n\t * 6、源为目录，目标为文件，抛出IO异常\n\t * 7、源路径和目标路径相同时，抛出IO异常\n\t * </pre>\n\t *\n\t * @return 拷贝后目标的文件或目录\n\t * @throws IORuntimeException IO异常\n\t */\n\t@Override\n\tpublic File copy() throws IORuntimeException{\n\t\tfinal File src = this.src;\n\t\tFile dest = this.dest;\n\t\t// check\n\t\tAssert.notNull(src, \"Source File is null !\");\n\t\tif (false == src.exists()) {\n\t\t\tthrow new IORuntimeException(\"File not exist: \" + src);\n\t\t}\n\t\tAssert.notNull(dest, \"Destination File or directiory is null !\");\n\t\tif (FileUtil.equals(src, dest)) {\n\t\t\tthrow new IORuntimeException(\"Files '{}' and '{}' are equal\", src, dest);\n\t\t}\n\n\t\tif (src.isDirectory()) {// 复制目录\n\t\t\tif(dest.exists() && false == dest.isDirectory()) {\n\t\t\t\t//源为目录，目标为文件，抛出IO异常\n\t\t\t\tthrow new IORuntimeException(\"Src is a directory but dest is a file!\");\n\t\t\t}\n\t\t\tif(FileUtil.isSub(src, dest)) {\n\t\t\t\tthrow new IORuntimeException(\"Dest is a sub directory of src !\");\n\t\t\t}\n\n\t\t\tfinal File subTarget = isCopyContentIfDir ? dest : FileUtil.mkdir(FileUtil.file(dest, src.getName()));\n\t\t\tinternalCopyDirContent(src, subTarget);\n\t\t} else {// 复制文件\n\t\t\tdest = internalCopyFile(src, dest);\n\t\t}\n\t\treturn dest;\n\t}\n\n\t//----------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 拷贝目录内容，只用于内部，不做任何安全检查<br>\n\t * 拷贝内容的意思为源目录下的所有文件和目录拷贝到另一个目录下，而不拷贝源目录本身\n\t *\n\t * @param src 源目录\n\t * @param dest 目标目录\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void internalCopyDirContent(File src, File dest) throws IORuntimeException {\n\t\tif (null != copyFilter && false == copyFilter.accept(src)) {\n\t\t\t//被过滤的目录跳过\n\t\t\treturn;\n\t\t}\n\n\t\tif (false == dest.exists()) {\n\t\t\t//目标为不存在路径，创建为目录\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\tdest.mkdirs();\n\t\t} else if (false == dest.isDirectory()) {\n\t\t\tthrow new IORuntimeException(StrUtil.format(\"Src [{}] is a directory but dest [{}] is a file!\", src.getPath(), dest.getPath()));\n\t\t}\n\n\t\tfinal String[] files = src.list();\n\t\tif(ArrayUtil.isNotEmpty(files)){\n\t\t\tFile srcFile;\n\t\t\tFile destFile;\n\t\t\tfor (String file : files) {\n\t\t\t\tsrcFile = new File(src, file);\n\t\t\t\tdestFile = this.isOnlyCopyFile ? dest : new File(dest, file);\n\t\t\t\t// 递归复制\n\t\t\t\tif (srcFile.isDirectory()) {\n\t\t\t\t\tinternalCopyDirContent(srcFile, destFile);\n\t\t\t\t} else {\n\t\t\t\t\tinternalCopyFile(srcFile, destFile);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 拷贝文件，只用于内部，不做任何安全检查<br>\n\t * 情况如下：\n\t * <pre>\n\t * 1、如果目标是一个不存在的路径，则目标以文件对待（自动创建父级目录）比如：/dest/aaa，如果aaa不存在，则aaa被当作文件名\n\t * 2、如果目标是一个已存在的目录，则文件拷贝到此目录下，文件名与原文件名一致\n\t * </pre>\n\t *\n\t * @param src  源文件，必须为文件\n\t * @param dest 目标文件，如果非覆盖模式必须为目录\n\t * @return 目标的目录或文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate File internalCopyFile(File src, File dest) throws IORuntimeException {\n\t\tif (null != copyFilter && false == copyFilter.accept(src)) {\n\t\t\t//被过滤的文件跳过\n\t\t\treturn src;\n\t\t}\n\n\t\t// 如果已经存在目标文件，切为不覆盖模式，跳过之\n\t\tif (dest.exists()) {\n\t\t\tif(dest.isDirectory()) {\n\t\t\t\t//目标为目录，目录下创建同名文件\n\t\t\t\tdest = new File(dest, src.getName());\n\t\t\t}\n\n\t\t\tif(dest.exists() && false == isOverride) {\n\t\t\t\t//非覆盖模式跳过\n\t\t\t\treturn src;\n\t\t\t}\n\t\t}else {\n\t\t\t//路径不存在则创建父目录\n\t\t\tFileUtil.mkParentDirs(dest);\n\t\t}\n\n\t\tfinal ArrayList<CopyOption> optionList = new ArrayList<>(2);\n\t\tif(isOverride) {\n\t\t\toptionList.add(StandardCopyOption.REPLACE_EXISTING);\n\t\t}\n\t\tif(isCopyAttributes) {\n\t\t\toptionList.add(StandardCopyOption.COPY_ATTRIBUTES);\n\t\t}\n\n\t\ttry {\n\t\t\tFiles.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[0]));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn dest;\n\t}\n\t//----------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileMode.java",
    "content": "package cn.hutool.core.io.file;\n\n/**\n * 文件读写模式，常用于RandomAccessFile\n *\n * @author looly\n * @since 4.5.2\n */\npublic enum FileMode {\n\t/** 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 */\n\tr,\n\t/** 打开以便读取和写入。 */\n\trw,\n\t/** 打开以便读取和写入。相对于 \"rw\"，\"rws\" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。 */\n\trws,\n\t/** 打开以便读取和写入，相对于 \"rw\"，\"rwd\" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。 */\n\trwd\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.util.regex.Pattern;\n\n/**\n * 文件名相关工具类\n *\n * @author looly\n * @since 5.4.1\n */\npublic class FileNameUtil {\n\n\t/**\n\t * .java文件扩展名\n\t */\n\tpublic static final String EXT_JAVA = \".java\";\n\t/**\n\t * .class文件扩展名\n\t */\n\tpublic static final String EXT_CLASS = \".class\";\n\t/**\n\t * .jar文件扩展名\n\t */\n\tpublic static final String EXT_JAR = \".jar\";\n\n\t/**\n\t * 类Unix路径分隔符\n\t */\n\tpublic static final char UNIX_SEPARATOR = CharUtil.SLASH;\n\t/**\n\t * Windows路径分隔符\n\t */\n\tpublic static final char WINDOWS_SEPARATOR = CharUtil.BACKSLASH;\n\n\t/**\n\t * Windows下文件名中的无效字符\n\t */\n\tprivate static final Pattern FILE_NAME_INVALID_PATTERN_WIN = Pattern.compile(\"[\\\\\\\\/:*?\\\"<>|\\r\\n]\");\n\n\t/**\n\t * 特殊后缀\n\t */\n\tprivate static final CharSequence[] SPECIAL_SUFFIX = {\"tar.bz2\", \"tar.Z\", \"tar.gz\", \"tar.xz\"};\n\n\n\t// -------------------------------------------------------------------------------------------- name start\n\n\t/**\n\t * 返回文件名\n\t *\n\t * @param file 文件\n\t * @return 文件名\n\t * @since 4.1.13\n\t */\n\tpublic static String getName(File file) {\n\t\treturn (null != file) ? file.getName() : null;\n\t}\n\n\t/**\n\t * 返回文件名<br>\n\t * <pre>\n\t * \"d:/test/aaa\" 返回 \"aaa\"\n\t * \"/test/aaa.jpg\" 返回 \"aaa.jpg\"\n\t * </pre>\n\t *\n\t * @param filePath 文件\n\t * @return 文件名\n\t * @since 4.1.13\n\t */\n\tpublic static String getName(String filePath) {\n\t\tif (null == filePath) {\n\t\t\treturn null;\n\t\t}\n\t\tint len = filePath.length();\n\t\tif (0 == len) {\n\t\t\treturn filePath;\n\t\t}\n\t\tif (CharUtil.isFileSeparator(filePath.charAt(len - 1))) {\n\t\t\t// 以分隔符结尾的去掉结尾分隔符\n\t\t\tlen--;\n\t\t}\n\n\t\tint begin = 0;\n\t\tchar c;\n\t\tfor (int i = len - 1; i > -1; i--) {\n\t\t\tc = filePath.charAt(i);\n\t\t\tif (CharUtil.isFileSeparator(c)) {\n\t\t\t\t// 查找最后一个路径分隔符（/或者\\）\n\t\t\t\tbegin = i + 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn filePath.substring(begin, len);\n\t}\n\n\t/**\n\t * 获取文件后缀名，扩展名不带“.”\n\t *\n\t * @param file 文件\n\t * @return 扩展名\n\t * @see #extName(File)\n\t * @since 5.3.8\n\t */\n\tpublic static String getSuffix(File file) {\n\t\treturn extName(file);\n\t}\n\n\t/**\n\t * 获得文件后缀名，扩展名不带“.”\n\t *\n\t * @param fileName 文件名\n\t * @return 扩展名\n\t * @see #extName(String)\n\t * @since 5.3.8\n\t */\n\tpublic static String getSuffix(String fileName) {\n\t\treturn extName(fileName);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param file 文件\n\t * @return 主文件名\n\t * @see #mainName(File)\n\t * @since 5.3.8\n\t */\n\tpublic static String getPrefix(File file) {\n\t\treturn mainName(file);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param fileName 完整文件名\n\t * @return 主文件名\n\t * @see #mainName(String)\n\t * @since 5.3.8\n\t */\n\tpublic static String getPrefix(String fileName) {\n\t\treturn mainName(fileName);\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param file 文件\n\t * @return 主文件名\n\t */\n\tpublic static String mainName(File file) {\n\t\tif (file.isDirectory()) {\n\t\t\treturn file.getName();\n\t\t}\n\t\treturn mainName(file.getName());\n\t}\n\n\t/**\n\t * 返回主文件名\n\t *\n\t * @param fileName 完整文件名\n\t * @return 主文件名\n\t */\n\tpublic static String mainName(String fileName) {\n\t\tif (null == fileName) {\n\t\t\treturn null;\n\t\t}\n\t\tint len = fileName.length();\n\t\tif (0 == len) {\n\t\t\treturn fileName;\n\t\t}\n\n\t\t//issue#2642，多级扩展名的主文件名\n\t\tfor (final CharSequence specialSuffix : SPECIAL_SUFFIX) {\n\t\t\tif(StrUtil.endWith(fileName, \".\" + specialSuffix)){\n\t\t\t\treturn StrUtil.subPre(fileName, len - specialSuffix.length() - 1);\n\t\t\t}\n\t\t}\n\n\t\tif (CharUtil.isFileSeparator(fileName.charAt(len - 1))) {\n\t\t\tlen--;\n\t\t}\n\n\t\tint begin = 0;\n\t\tint end = len;\n\t\tchar c;\n\t\tfor (int i = len - 1; i >= 0; i--) {\n\t\t\tc = fileName.charAt(i);\n\t\t\tif (len == end && CharUtil.DOT == c) {\n\t\t\t\t// 查找最后一个文件名和扩展名的分隔符：.\n\t\t\t\tend = i;\n\t\t\t}\n\t\t\t// 查找最后一个路径分隔符（/或者\\），如果这个分隔符在.之后，则继续查找，否则结束\n\t\t\tif (CharUtil.isFileSeparator(c)) {\n\t\t\t\tbegin = i + 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn fileName.substring(begin, end);\n\t}\n\n\t/**\n\t * 获取文件扩展名（后缀名），扩展名不带“.”\n\t *\n\t * @param file 文件\n\t * @return 扩展名\n\t */\n\tpublic static String extName(File file) {\n\t\tif (null == file) {\n\t\t\treturn null;\n\t\t}\n\t\tif (file.isDirectory()) {\n\t\t\treturn null;\n\t\t}\n\t\treturn extName(file.getName());\n\t}\n\n\t/**\n\t * 获得文件的扩展名（后缀名），扩展名不带“.”\n\t *\n\t * @param fileName 文件名\n\t * @return 扩展名\n\t */\n\tpublic static String extName(String fileName) {\n\t\tif (fileName == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int index = fileName.lastIndexOf(StrUtil.DOT);\n\t\tif (index == -1) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t} else {\n\t\t\t// issue#I4W5FS@Gitee\n\t\t\tfinal int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT);\n\t\t\tfinal String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1);\n\t\t\tif (StrUtil.equalsAny(substr, SPECIAL_SUFFIX)) {\n\t\t\t\treturn substr;\n\t\t\t}\n\n\t\t\tfinal String ext = fileName.substring(index + 1);\n\t\t\t// 扩展名中不能包含路径相关的符号\n\t\t\treturn StrUtil.containsAny(ext, UNIX_SEPARATOR, WINDOWS_SEPARATOR) ? StrUtil.EMPTY : ext;\n\t\t}\n\t}\n\n\t/**\n\t * 清除文件名中的在Windows下不支持的非法字符，包括： \\ / : * ? \" &lt; &gt; |\n\t *\n\t * @param fileName 文件名（必须不包括路径，否则路径符将被替换）\n\t * @return 清理后的文件名\n\t * @since 3.3.1\n\t */\n\tpublic static String cleanInvalid(String fileName) {\n\t\treturn StrUtil.isBlank(fileName) ? fileName : ReUtil.delAll(FILE_NAME_INVALID_PATTERN_WIN, fileName);\n\t}\n\n\t/**\n\t * 文件名中是否包含在Windows下不支持的非法字符，包括： \\ / : * ? \" &lt; &gt; |\n\t *\n\t * @param fileName 文件名（必须不包括路径，否则路径符将被替换）\n\t * @return 是否包含非法字符\n\t * @since 3.3.1\n\t */\n\tpublic static boolean containsInvalid(String fileName) {\n\t\treturn (false == StrUtil.isBlank(fileName)) && ReUtil.contains(FILE_NAME_INVALID_PATTERN_WIN, fileName);\n\t}\n\n\t/**\n\t * 根据文件名检查文件类型，忽略大小写\n\t *\n\t * @param fileName 文件名，例如hutool.png\n\t * @param extNames 被检查的扩展名数组，同一文件类型可能有多种扩展名，扩展名不带“.”\n\t * @return 是否是指定扩展名的类型\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isType(String fileName, String... extNames) {\n\t\treturn StrUtil.equalsAnyIgnoreCase(extName(fileName), extNames);\n\t}\n\t// -------------------------------------------------------------------------------------------- name end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileReader.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.LineHandler;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * 文件读取器\n *\n * @author Looly\n *\n */\npublic class FileReader extends FileWrapper {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建 FileReader\n\t * @param file 文件\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t * @return FileReader\n\t */\n\tpublic static FileReader create(File file, Charset charset){\n\t\treturn new FileReader(file, charset);\n\t}\n\n\t/**\n\t * 创建 FileReader, 编码：{@link FileWrapper#DEFAULT_CHARSET}\n\t * @param file 文件\n\t * @return FileReader\n\t */\n\tpublic static FileReader create(File file){\n\t\treturn new FileReader(file);\n\t}\n\n\t// ------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t * @param file 文件\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t */\n\tpublic FileReader(File file, Charset charset) {\n\t\tsuper(file, charset);\n\t\tcheckFile();\n\t}\n\n\t/**\n\t * 构造\n\t * @param file 文件\n\t * @param charset 编码，使用 {@link CharsetUtil#charset(String)}\n\t */\n\tpublic FileReader(File file, String charset) {\n\t\tthis(file, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 构造\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t */\n\tpublic FileReader(String filePath, Charset charset) {\n\t\tthis(FileUtil.file(filePath), charset);\n\t}\n\n\t/**\n\t * 构造\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t * @param charset 编码，使用 {@link CharsetUtil#charset(String)}\n\t */\n\tpublic FileReader(String filePath, String charset) {\n\t\tthis(FileUtil.file(filePath), CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 编码使用 {@link FileWrapper#DEFAULT_CHARSET}\n\t * @param file 文件\n\t */\n\tpublic FileReader(File file) {\n\t\tthis(file, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 编码使用 {@link FileWrapper#DEFAULT_CHARSET}\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t */\n\tpublic FileReader(String filePath) {\n\t\tthis(filePath, DEFAULT_CHARSET);\n\t}\n\t// ------------------------------------------------------- Constructor end\n\n\t/**\n\t * 读取文件所有数据<br>\n\t * 文件的长度不能超过 {@link Integer#MAX_VALUE}\n\t *\n\t * @return 字节码\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic byte[] readBytes() throws IORuntimeException {\n\t\tlong len = file.length();\n\t\tif (len >= Integer.MAX_VALUE) {\n\t\t\tthrow new IORuntimeException(\"File is larger then max array size\");\n\t\t}\n\n\t\tbyte[] bytes = new byte[(int) len];\n\t\tFileInputStream in = null;\n\t\tint readLength;\n\t\ttry {\n\t\t\tin = new FileInputStream(file);\n\t\t\treadLength = in.read(bytes);\n\t\t\tif(readLength < len){\n\t\t\t\tthrow new IOException(StrUtil.format(\"File length is [{}] but read [{}]!\", len, readLength));\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\n\t\treturn bytes;\n\t}\n\n\t/**\n\t * 读取文件内容\n\t *\n\t * @return 内容\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic String readString() throws IORuntimeException{\n\t\treturn new String(readBytes(), this.charset);\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @param <T> 集合类型\n\t * @param collection 集合\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic <T extends Collection<String>> T readLines(T collection) throws IORuntimeException {\n\t\tBufferedReader reader = null;\n\t\ttry {\n\t\t\treader = FileUtil.getReader(file, charset);\n\t\t\tString line;\n\t\t\twhile (true) {\n\t\t\t\tline = reader.readLine();\n\t\t\t\tif (line == null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcollection.add(line);\n\t\t\t}\n\t\t\treturn collection;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t/**\n\t * 按照行处理文件内容\n\t *\n\t * @param lineHandler 行处理器\n\t * @throws IORuntimeException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic void readLines(LineHandler lineHandler) throws IORuntimeException{\n\t\tBufferedReader reader = null;\n\t\ttry {\n\t\t\treader = FileUtil.getReader(file, charset);\n\t\t\tIoUtil.readLines(reader, lineHandler);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t/**\n\t * 从文件中读取每一行数据\n\t *\n\t * @return 文件中的每行内容的集合\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic List<String> readLines() throws IORuntimeException {\n\t\treturn readLines(new ArrayList<>());\n\t}\n\n\t/**\n\t * 按照给定的readerHandler读取文件中的数据\n\t *\n\t * @param <T> 读取的结果对象类型\n\t * @param readerHandler Reader处理类\n\t * @return 从文件中read出的数据\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic <T> T read(ReaderHandler<T> readerHandler) throws IORuntimeException {\n\t\tBufferedReader reader = null;\n\t\tT result;\n\t\ttry {\n\t\t\treader = FileUtil.getReader(this.file, charset);\n\t\t\tresult = readerHandler.handle(reader);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic BufferedReader getReader() throws IORuntimeException {\n\t\treturn IoUtil.getReader(getInputStream(), this.charset);\n\t}\n\n\t/**\n\t * 获得输入流\n\t *\n\t * @return 输入流\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic BufferedInputStream getInputStream() throws IORuntimeException {\n\t\ttry {\n\t\t\treturn new BufferedInputStream(new FileInputStream(this.file));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将文件写入流中，此方法不会关闭比输出流\n\t *\n\t * @param out 流\n\t * @return 写出的流byte数\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic long writeToStream(OutputStream out) throws IORuntimeException {\n\t\treturn writeToStream(out, false);\n\t}\n\n\t/**\n\t * 将文件写入流中\n\t *\n\t * @param out 流\n\t * @param isCloseOut 是否关闭输出流\n\t * @return 写出的流byte数\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.2\n\t */\n\tpublic long writeToStream(OutputStream out, boolean isCloseOut) throws IORuntimeException {\n\t\ttry (FileInputStream in = new FileInputStream(this.file)){\n\t\t\treturn IoUtil.copy(in, out);\n\t\t}catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally{\n\t\t\tif(isCloseOut){\n\t\t\t\tIoUtil.close(out);\n\t\t\t}\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------- Interface start\n\t/**\n\t * Reader处理接口\n\t *\n\t * @author Luxiaolei\n\t *\n\t * @param <T> Reader处理返回结果类型\n\t */\n\tpublic interface ReaderHandler<T> {\n\t\tT handle(BufferedReader reader) throws IOException;\n\t}\n\t// -------------------------------------------------------------------------- Interface end\n\n\t/**\n\t * 检查文件\n\t *\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void checkFile() throws IORuntimeException {\n\t\tif (false == file.exists()) {\n\t\t\tthrow new IORuntimeException(\"File not exist: \" + file);\n\t\t}\n\t\tif (false == file.isFile()) {\n\t\t\tthrow new IORuntimeException(\"Not a file:\" + file);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileSystemUtil.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.charset.Charset;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\n\n/**\n * {@link FileSystem}相关工具类封装<br>\n * 参考：https://blog.csdn.net/j16421881/article/details/78858690\n *\n * @author looly\n * @since 5.7.15\n */\npublic class FileSystemUtil {\n\n\t/**\n\t * 创建 {@link FileSystem}\n\t *\n\t * @param path 文件路径，可以是目录或Zip文件等\n\t * @return {@link FileSystem}\n\t */\n\tpublic static FileSystem create(String path) {\n\t\ttry {\n\t\t\treturn FileSystems.newFileSystem(\n\t\t\t\t\tPaths.get(path).toUri(),\n\t\t\t\t\tMapUtil.of(\"create\", \"true\"));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建 Zip的{@link FileSystem}，默认UTF-8编码\n\t *\n\t * @param path 文件路径，可以是目录或Zip文件等\n\t * @return {@link FileSystem}\n\t */\n\tpublic static FileSystem createZip(String path) {\n\t\treturn createZip(path, null);\n\t}\n\n\t/**\n\t * 创建 Zip的{@link FileSystem}\n\t *\n\t * @param path 文件路径，可以是目录或Zip文件等\n\t * @param charset 编码\n\t * @return {@link FileSystem}\n\t */\n\tpublic static FileSystem createZip(String path, Charset charset) {\n\t\tif(null == charset){\n\t\t\tcharset = CharsetUtil.CHARSET_UTF_8;\n\t\t}\n\t\tfinal HashMap<String, String> env = new HashMap<>();\n\t\tenv.put(\"create\", \"true\");\n\t\tenv.put(\"encoding\", charset.name());\n\n\t\ttry {\n\t\t\treturn FileSystems.newFileSystem(\n\t\t\t\t\tURI.create(\"jar:\" + Paths.get(path).toUri()), env);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取目录的根路径，或Zip文件中的根路径\n\t *\n\t * @param fileSystem {@link FileSystem}\n\t * @return 根 {@link Path}\n\t */\n\tpublic static Path getRoot(FileSystem fileSystem) {\n\t\treturn fileSystem.getPath(StrUtil.SLASH);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileWrapper.java",
    "content": "package cn.hutool.core.io.file;\n\nimport java.io.File;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\n/**\n * 文件包装器，扩展文件对象\n *\n * @author Looly\n *\n */\npublic class FileWrapper implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected File file;\n\tprotected Charset charset;\n\n\t/** 默认编码：UTF-8 */\n\tpublic static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;\n\n\t// ------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t * @param file 文件\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t */\n\tpublic FileWrapper(File file, Charset charset) {\n\t\tthis.file = file;\n\t\tthis.charset = charset;\n\t}\n\t// ------------------------------------------------------- Constructor end\n\n\t// ------------------------------------------------------- Setters and Getters start start\n\t/**\n\t * 获得文件\n\t * @return 文件\n\t */\n\tpublic File getFile() {\n\t\treturn file;\n\t}\n\n\t/**\n\t * 设置文件\n\t * @param file 文件\n\t * @return 自身\n\t */\n\tpublic FileWrapper setFile(File file) {\n\t\tthis.file = file;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得字符集编码\n\t * @return 编码\n\t */\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\t/**\n\t * 设置字符集编码\n\t * @param charset 编码\n\t * @return 自身\n\t */\n\tpublic FileWrapper setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\t// ------------------------------------------------------- Setters and Getters start end\n\n\t/**\n\t * 可读的文件大小\n\t * @return 大小\n\t */\n\tpublic String readableFileSize() {\n\t\treturn FileUtil.readableFileSize(file.length());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/FileWriter.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedOutputStream;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * 文件写入器\n *\n * @author Looly\n */\npublic class FileWriter extends FileWrapper {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建 FileWriter\n\t *\n\t * @param file    文件\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t * @return FileWriter\n\t */\n\tpublic static FileWriter create(File file, Charset charset) {\n\t\treturn new FileWriter(file, charset);\n\t}\n\n\t/**\n\t * 创建 FileWriter, 编码：{@link FileWrapper#DEFAULT_CHARSET}\n\t *\n\t * @param file 文件\n\t * @return FileWriter\n\t */\n\tpublic static FileWriter create(File file) {\n\t\treturn new FileWriter(file);\n\t}\n\n\t// ------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param file    文件\n\t * @param charset 编码，使用 {@link CharsetUtil}\n\t */\n\tpublic FileWriter(File file, Charset charset) {\n\t\tsuper(file, charset);\n\t\tcheckFile();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file    文件\n\t * @param charset 编码，使用 {@link CharsetUtil#charset(String)}\n\t */\n\tpublic FileWriter(File file, String charset) {\n\t\tthis(file, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t * @param charset  编码，使用 {@link CharsetUtil}\n\t */\n\tpublic FileWriter(String filePath, Charset charset) {\n\t\tthis(FileUtil.file(filePath), charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t * @param charset  编码，使用 {@link CharsetUtil#charset(String)}\n\t */\n\tpublic FileWriter(String filePath, String charset) {\n\t\tthis(FileUtil.file(filePath), CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 编码使用 {@link FileWrapper#DEFAULT_CHARSET}\n\t *\n\t * @param file 文件\n\t */\n\tpublic FileWriter(File file) {\n\t\tthis(file, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 编码使用 {@link FileWrapper#DEFAULT_CHARSET}\n\t *\n\t * @param filePath 文件路径，相对路径会被转换为相对于ClassPath的路径\n\t */\n\tpublic FileWriter(String filePath) {\n\t\tthis(filePath, DEFAULT_CHARSET);\n\t}\n\t// ------------------------------------------------------- Constructor end\n\n\t/**\n\t * 将String写入文件\n\t *\n\t * @param content  写入的内容\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File write(String content, boolean isAppend) throws IORuntimeException {\n\t\tBufferedWriter writer = null;\n\t\ttry {\n\t\t\twriter = getWriter(isAppend);\n\t\t\twriter.write(content);\n\t\t\twriter.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 将String写入文件，覆盖模式\n\t *\n\t * @param content 写入的内容\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File write(String content) throws IORuntimeException {\n\t\treturn write(content, false);\n\t}\n\n\t/**\n\t * 将String写入文件，追加模式\n\t *\n\t * @param content 写入的内容\n\t * @return 写入的文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File append(String content) throws IORuntimeException {\n\t\treturn write(content, true);\n\t}\n\n\t/**\n\t * 将列表写入文件，覆盖模式，最后一行末尾不追加换行符\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic <T> File writeLines(Iterable<T> list) throws IORuntimeException {\n\t\treturn writeLines(list, false);\n\t}\n\n\t/**\n\t * 将列表写入文件，追加模式，最后一行末尾不追加换行符\n\t *\n\t * @param <T>  集合元素类型\n\t * @param list 列表\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic <T> File appendLines(Iterable<T> list) throws IORuntimeException {\n\t\treturn writeLines(list, true);\n\t}\n\n\t/**\n\t * 将列表写入文件，最后一行末尾不追加换行符\n\t *\n\t * @param <T>      集合元素类型\n\t * @param list     列表\n\t * @param isAppend 是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic <T> File writeLines(Iterable<T> list, boolean isAppend) throws IORuntimeException {\n\t\treturn writeLines(list, null, isAppend);\n\t}\n\n\t/**\n\t * 将列表写入文件，最后一行末尾不追加换行符\n\t *\n\t * @param <T>           集合元素类型\n\t * @param list          列表\n\t * @param lineSeparator 换行符枚举（Windows、Mac或Linux换行符）\n\t * @param isAppend      是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.0\n\t */\n\tpublic <T> File writeLines(Iterable<T> list, LineSeparator lineSeparator, boolean isAppend) throws IORuntimeException {\n\t\treturn writeLines(list, lineSeparator, isAppend, false);\n\t}\n\n\t/**\n\t * 将列表写入文件\n\t *\n\t * @param <T>           集合元素类型\n\t * @param list          列表\n\t * @param lineSeparator 换行符枚举（Windows、Mac或Linux换行符）\n\t * @param isAppend      是否追加\n\t * @param appendLineSeparator 是否在最后一行末尾追加换行符，Linux下要求最后一行必须带有换行符\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.37\n\t */\n\tpublic <T> File writeLines(Iterable<T> list, LineSeparator lineSeparator, boolean isAppend, boolean appendLineSeparator) throws IORuntimeException {\n\t\ttry (PrintWriter writer = getPrintWriter(isAppend)) {\n\t\t\tboolean isFirst = true;\n\t\t\tfor (T t : list) {\n\t\t\t\tif (null != t) {\n\t\t\t\t\tif(isFirst){\n\t\t\t\t\t\tisFirst = false;\n\t\t\t\t\t\tif(isAppend && FileUtil.isNotEmpty(this.file)){\n\t\t\t\t\t\t\t// 追加模式下且文件非空，补充换行符\n\t\t\t\t\t\t\tprintNewLine(writer, lineSeparator);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else{\n\t\t\t\t\t\tprintNewLine(writer, lineSeparator);\n\t\t\t\t\t}\n\t\t\t\t\twriter.print(t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(appendLineSeparator){\n\t\t\t\tprintNewLine(writer, lineSeparator);\n\t\t\t}\n\t\t\twriter.flush();\n\t\t}\n\t\treturn this.file;\n\t}\n\n\t/**\n\t * 将Map写入文件，每个键值对为一行，一行中键与值之间使用kvSeparator分隔\n\t *\n\t * @param map         Map\n\t * @param kvSeparator 键和值之间的分隔符，如果传入null使用默认分隔符\" = \"\n\t * @param isAppend    是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.5\n\t */\n\tpublic File writeMap(Map<?, ?> map, String kvSeparator, boolean isAppend) throws IORuntimeException {\n\t\treturn writeMap(map, null, kvSeparator, isAppend);\n\t}\n\n\t/**\n\t * 将Map写入文件，每个键值对为一行，一行中键与值之间使用kvSeparator分隔\n\t *\n\t * @param map           Map\n\t * @param lineSeparator 换行符枚举（Windows、Mac或Linux换行符）\n\t * @param kvSeparator   键和值之间的分隔符，如果传入null使用默认分隔符\" = \"\n\t * @param isAppend      是否追加\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.5\n\t */\n\tpublic File writeMap(Map<?, ?> map, LineSeparator lineSeparator, String kvSeparator, boolean isAppend) throws IORuntimeException {\n\t\tif (null == kvSeparator) {\n\t\t\tkvSeparator = \" = \";\n\t\t}\n\t\ttry (PrintWriter writer = getPrintWriter(isAppend)) {\n\t\t\tfor (Entry<?, ?> entry : map.entrySet()) {\n\t\t\t\tif (null != entry) {\n\t\t\t\t\twriter.print(StrUtil.format(\"{}{}{}\", entry.getKey(), kvSeparator, entry.getValue()));\n\t\t\t\t\tprintNewLine(writer, lineSeparator);\n\t\t\t\t\twriter.flush();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this.file;\n\t}\n\n\t/**\n\t * 写入数据到文件\n\t *\n\t * @param data 数据\n\t * @param off  数据开始位置\n\t * @param len  数据长度\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File write(byte[] data, int off, int len) throws IORuntimeException {\n\t\treturn write(data, off, len, false);\n\t}\n\n\t/**\n\t * 追加数据到文件\n\t *\n\t * @param data 数据\n\t * @param off  数据开始位置\n\t * @param len  数据长度\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File append(byte[] data, int off, int len) throws IORuntimeException {\n\t\treturn write(data, off, len, true);\n\t}\n\n\t/**\n\t * 写入数据到文件\n\t *\n\t * @param data     数据\n\t * @param off      数据开始位置\n\t * @param len      数据长度\n\t * @param isAppend 是否追加模式\n\t * @return 目标文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File write(byte[] data, int off, int len, boolean isAppend) throws IORuntimeException {\n\t\ttry (FileOutputStream out = new FileOutputStream(FileUtil.touch(file), isAppend)) {\n\t\t\tout.write(data, off, len);\n\t\t\tout.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 将流的内容写入文件<br>\n\t * 此方法会自动关闭输入流\n\t *\n\t * @param in 输入流，不关闭\n\t * @return dest\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic File writeFromStream(InputStream in) throws IORuntimeException {\n\t\treturn writeFromStream(in, true);\n\t}\n\n\t/**\n\t * 将流的内容写入文件\n\t *\n\t * @param in        输入流，不关闭\n\t * @param isCloseIn 是否关闭输入流\n\t * @return dest\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.2\n\t */\n\tpublic File writeFromStream(InputStream in, boolean isCloseIn) throws IORuntimeException {\n\t\tOutputStream out = null;\n\t\ttry {\n\t\t\tout = Files.newOutputStream(FileUtil.touch(file).toPath());\n\t\t\tIoUtil.copy(in, out);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tif (isCloseIn) {\n\t\t\t\tIoUtil.close(in);\n\t\t\t}\n\t\t}\n\t\treturn file;\n\t}\n\n\t/**\n\t * 获得一个输出流对象\n\t *\n\t * @return 输出流对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic BufferedOutputStream getOutputStream() throws IORuntimeException {\n\t\ttry {\n\t\t\treturn new BufferedOutputStream(Files.newOutputStream(FileUtil.touch(file).toPath()));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得一个带缓存的写入对象\n\t *\n\t * @param isAppend 是否追加\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic BufferedWriter getWriter(boolean isAppend) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn new BufferedWriter(new OutputStreamWriter(new FileOutputStream(FileUtil.touch(file), isAppend), charset));\n\t\t} catch (Exception e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得一个打印写入对象，可以有print\n\t *\n\t * @param isAppend 是否追加\n\t * @return 打印对象\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic PrintWriter getPrintWriter(boolean isAppend) throws IORuntimeException {\n\t\treturn new PrintWriter(getWriter(isAppend));\n\t}\n\n\t/**\n\t * 检查文件\n\t *\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void checkFile() throws IORuntimeException {\n\t\tAssert.notNull(file, \"File to write content is null !\");\n\t\tif (this.file.exists() && false == file.isFile()) {\n\t\t\tthrow new IORuntimeException(\"File [{}] is not a file !\", this.file.getAbsoluteFile());\n\t\t}\n\t}\n\n\t/**\n\t * 打印新行\n\t *\n\t * @param writer        Writer\n\t * @param lineSeparator 换行符枚举\n\t * @since 4.0.5\n\t */\n\tprivate void printNewLine(PrintWriter writer, LineSeparator lineSeparator) {\n\t\tif (null == lineSeparator) {\n\t\t\t//默认换行符\n\t\t\twriter.println();\n\t\t} else {\n\t\t\t//自定义换行符\n\t\t\twriter.print(lineSeparator.getValue());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.LineHandler;\nimport cn.hutool.core.io.watch.SimpleWatcher;\n\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\n\n/**\n * 行处理的Watcher实现\n *\n * @author looly\n * @since 4.5.2\n */\npublic class LineReadWatcher extends SimpleWatcher implements Runnable {\n\n\tprivate final RandomAccessFile randomAccessFile;\n\tprivate final Charset charset;\n\tprivate final LineHandler lineHandler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param randomAccessFile {@link RandomAccessFile}\n\t * @param charset 编码\n\t * @param lineHandler 行处理器{@link LineHandler}实现\n\t */\n\tpublic LineReadWatcher(RandomAccessFile randomAccessFile, Charset charset, LineHandler lineHandler) {\n\t\tthis.randomAccessFile = randomAccessFile;\n\t\tthis.charset = charset;\n\t\tthis.lineHandler = lineHandler;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\tonModify(null, null);\n\t}\n\n\t@Override\n\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t\tfinal RandomAccessFile randomAccessFile = this.randomAccessFile;\n\t\tfinal Charset charset = this.charset;\n\t\tfinal LineHandler lineHandler = this.lineHandler;\n\n\t\ttry {\n\t\t\tfinal long currentLength = randomAccessFile.length();\n\t\t\tfinal long position = randomAccessFile.getFilePointer();\n\t\t\tif (position == currentLength) {\n\t\t\t\t// 内容长度不变时忽略此次事件\n\t\t\t\treturn;\n\t\t\t} else if (currentLength < position) {\n\t\t\t\t// 如果内容变短或变0，说明文件做了删改或清空，回到内容末尾或0\n\t\t\t\trandomAccessFile.seek(currentLength);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 读取行\n\t\t\tFileUtil.readLines(randomAccessFile, charset, lineHandler);\n\n\t\t\t// 记录当前读到的位置\n\t\t\trandomAccessFile.seek(currentLength);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/LineSeparator.java",
    "content": "package cn.hutool.core.io.file;\n\n/**\n * 换行符枚举<br>\n * 换行符包括：\n * <pre>\n * Mac系统换行符：\"\\r\"\n * Linux系统换行符：\"\\n\"\n * Windows系统换行符：\"\\r\\n\"\n * </pre>\n *\n * @see #MAC\n * @see #LINUX\n * @see #WINDOWS\n * @author Looly\n * @since 3.1.0\n */\npublic enum LineSeparator {\n\t/** Mac系统换行符：\"\\r\" */\n\tMAC(\"\\r\"),\n\t/** Linux系统换行符：\"\\n\" */\n\tLINUX(\"\\n\"),\n\t/** Windows系统换行符：\"\\r\\n\" */\n\tWINDOWS(\"\\r\\n\");\n\n\tprivate final String value;\n\n\tLineSeparator(String lineSeparator) {\n\t\tthis.value = lineSeparator;\n\t}\n\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/PathMover.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.file.visitor.MoveVisitor;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.nio.file.*;\n\n/**\n * 文件移动封装\n *\n * @author looly\n * @since 5.8.14\n */\npublic class PathMover {\n\n\t/**\n\t * 创建文件或目录移动器\n\t *\n\t * @param src        源文件或目录\n\t * @param target     目标文件或目录\n\t * @param isOverride 是否覆盖目标文件\n\t * @return {@code PathMover}\n\t */\n\tpublic static PathMover of(final Path src, final Path target, final boolean isOverride) {\n\t\treturn of(src, target, isOverride ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{});\n\t}\n\n\t/**\n\t * 创建文件或目录移动器\n\t *\n\t * @param src     源文件或目录\n\t * @param target  目标文件或目录\n\t * @param options 移动参数\n\t * @return {@code PathMover}\n\t */\n\tpublic static PathMover of(final Path src, final Path target, final CopyOption[] options) {\n\t\treturn new PathMover(src, target, options);\n\t}\n\n\tprivate final Path src;\n\tprivate final Path target;\n\tprivate final CopyOption[] options;\n\n\t/**\n\t * 构造\n\t *\n\t * @param src     源文件或目录，不能为{@code null}且必须存在\n\t * @param target  目标文件或目录\n\t * @param options 移动参数\n\t */\n\tpublic PathMover(final Path src, final Path target, final CopyOption[] options) {\n\t\tAssert.notNull(target, \"Src path must be not null !\");\n\t\tif(false == PathUtil.exists(src, false)){\n\t\t\tthrow new IllegalArgumentException(\"Src path is not exist!\");\n\t\t}\n\t\tthis.src = src;\n\t\tthis.target = Assert.notNull(target, \"Target path must be not null !\");\n\t\tthis.options = ObjUtil.defaultIfNull(options, () -> new CopyOption[]{});\n\t}\n\n\t/**\n\t * 移动文件或目录到目标中，例如：\n\t * <ul>\n\t *     <li>如果src和target为同一文件或目录，直接返回target。</li>\n\t *     <li>如果src为文件，target为目录，则移动到目标目录下，存在同名文件则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为文件，则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为不存在的路径，则重命名源文件到目标指定的文件，如move(\"/a/b\", \"/c/d\"), d不存在，则b变成d。</li>\n\t *     <li>如果src为目录，target为文件，抛出{@link IllegalArgumentException}</li>\n\t *     <li>如果src为目录，target为目录，则将源目录及其内容移动到目标路径目录中，如move(\"/a/b\", \"/c/d\")，结果为\"/c/d/b\"</li>\n\t *     <li>如果src为目录，target为不存在的路径，则重命名src到target，如move(\"/a/b\", \"/c/d\")，结果为\"/c/d/\"，相当于b重命名为d</li>\n\t * </ul>\n\t *\n\t * @return 目标文件Path\n\t */\n\tpublic Path move() {\n\t\tfinal Path src = this.src;\n\t\tPath target = this.target;\n\t\tfinal CopyOption[] options = this.options;\n\n\t\tif (PathUtil.isSub(src, target)) {\n\t\t\tif(Files.exists(target) && PathUtil.equals(src, target)){\n\t\t\t\t// issue#2845，当用户传入目标路径与源路径一致时，直接返回，否则会导致删除风险。\n\t\t\t\treturn target;\n\t\t\t}\n\n\t\t\t// 当用户将文件夹拷贝到其子文件夹时，报错\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Target [{}] is sub path of src [{}]!\", target, src));\n\t\t}\n\n\t\tif (PathUtil.isDirectory(target)) {\n\t\t\t// 创建子路径的情况，1是目标是目录，需要移动到目录下，2是目标不能存在，自动创建目录\n\t\t\ttarget = target.resolve(src.getFileName());\n\t\t}\n\n\t\t// 自动创建目标的父目录\n\t\tPathUtil.mkParentDirs(target);\n\t\ttry {\n\t\t\treturn Files.move(src, target, options);\n\t\t} catch (final IOException e) {\n\t\t\tif (e instanceof FileAlreadyExistsException || e instanceof AccessDeniedException) {\n\t\t\t\t// issue#I4QV0L@Gitee issue#I95CLT@Gitee\n\t\t\t\t// FileAlreadyExistsException 目标已存在\n\t\t\t\t// AccessDeniedException 目标已存在且只读\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t\t// 移动失败，可能是跨分区移动导致的，采用递归移动方式\n\t\t\twalkMove(src, target, options);\n\t\t\t// 移动后删除空目录\n\t\t\tPathUtil.del(src);\n\t\t\treturn target;\n\t\t}\n\t}\n\n\t/**\n\t * 移动文件或目录内容到目标中，例如：\n\t * <ul>\n\t *     <li>如果src为文件，target为目录，则移动到目标目录下，存在同名文件则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为文件，则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为不存在的路径，则重命名源文件到目标指定的文件，如moveContent(\"/a/b\", \"/c/d\"), d不存在，则b变成d。</li>\n\t *     <li>如果src为目录，target为文件，抛出{@link IllegalArgumentException}</li>\n\t *     <li>如果src为目录，target为目录，则将源目录下的内容移动到目标路径目录中，源目录不删除。</li>\n\t *     <li>如果src为目录，target为不存在的路径，则创建目标路径为目录，将源目录下的内容移动到目标路径目录中，源目录不删除。</li>\n\t * </ul>\n\t *\n\t * @return 目标文件Path\n\t */\n\tpublic Path moveContent() {\n\t\tfinal Path src = this.src;\n\t\tfinal Path target = this.target;\n\t\tif (PathUtil.isExistsAndNotDirectory(target, false)) {\n\t\t\t// 文件移动调用move方法\n\t\t\treturn move();\n\t\t}\n\n\t\t// issue#2893 target 不存在导致NoSuchFileException\n\t\tif (Files.exists(target) && PathUtil.equals(src, target)) {\n\t\t\t// issue#2845，当用户传入目标路径与源路径一致时，直接返回，否则会导致删除风险。\n\t\t\treturn target;\n\t\t}\n\n\t\tfinal CopyOption[] options = this.options;\n\n\t\t// 自动创建目标的父目录\n\t\tPathUtil.mkParentDirs(target);\n\n\t\t// 移动失败，可能是跨分区移动导致的，采用递归移动方式\n\t\twalkMove(src, target, options);\n\t\treturn target;\n\t}\n\n\t/**\n\t * 递归移动\n\t *\n\t * @param src     源目录\n\t * @param target  目标目录\n\t * @param options 移动参数\n\t */\n\tprivate static void walkMove(final Path src, final Path target, final CopyOption... options) {\n\t\ttry {\n\t\t\t// 移动源目录下的内容而不删除目录\n\t\t\tFiles.walkFileTree(src, new MoveVisitor(src, target, options));\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.file.visitor.CopyVisitor;\nimport cn.hutool.core.io.file.visitor.DelVisitor;\nimport cn.hutool.core.io.resource.FileResource;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * NIO中Path对象操作封装\n *\n * @author looly\n * @since 5.4.1\n */\npublic class PathUtil {\n\t/**\n\t * 目录是否为空\n\t *\n\t * @param dirPath 目录\n\t * @return 是否为空\n\t * @throws IORuntimeException IOException\n\t */\n\tpublic static boolean isDirEmpty(Path dirPath) {\n\t\ttry (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath)) {\n\t\t\treturn false == dirStream.iterator().hasNext();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供path为文件，直接返回过滤结果\n\t *\n\t * @param path       当前遍历文件或目录\n\t * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件\n\t * @return 文件列表\n\t * @since 5.4.1\n\t */\n\tpublic static List<File> loopFiles(Path path, FileFilter fileFilter) {\n\t\treturn loopFiles(path, -1, fileFilter);\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供path为文件，直接返回过滤结果\n\t *\n\t * @param path       当前遍历文件或目录\n\t * @param maxDepth   遍历最大深度，-1表示遍历到没有目录为止\n\t * @param fileFilter 文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件\n\t * @return 文件列表\n\t * @since 5.4.1\n\t */\n\tpublic static List<File> loopFiles(Path path, int maxDepth, FileFilter fileFilter) {\n\t\treturn loopFiles(path, maxDepth, false, fileFilter);\n\t}\n\n\t/**\n\t * 递归遍历目录以及子目录中的所有文件<br>\n\t * 如果提供path为文件，直接返回过滤结果\n\t *\n\t * @param path          当前遍历文件或目录\n\t * @param maxDepth      遍历最大深度，-1表示遍历到没有目录为止\n\t * @param isFollowLinks 是否跟踪软链（快捷方式）\n\t * @param fileFilter    文件过滤规则对象，选择要保留的文件，只对文件有效，不过滤目录，null表示接收全部文件\n\t * @return 文件列表\n\t * @since 5.4.1\n\t */\n\tpublic static List<File> loopFiles(final Path path, final int maxDepth, final boolean isFollowLinks, final FileFilter fileFilter) {\n\t\tfinal List<File> fileList = new ArrayList<>();\n\n\t\tif (!exists(path, isFollowLinks)) {\n\t\t\treturn fileList;\n\t\t} else if (!isDirectory(path, isFollowLinks)) {\n\t\t\tfinal File file = path.toFile();\n\t\t\tif (null == fileFilter || fileFilter.accept(file)) {\n\t\t\t\tfileList.add(file);\n\t\t\t}\n\t\t\treturn fileList;\n\t\t}\n\n\t\twalkFiles(path, maxDepth, isFollowLinks, new SimpleFileVisitor<Path>() {\n\n\t\t\t@Override\n\t\t\tpublic FileVisitResult visitFile(final Path path, final BasicFileAttributes attrs) {\n\t\t\t\tfinal File file = path.toFile();\n\t\t\t\tif (null == fileFilter || fileFilter.accept(file)) {\n\t\t\t\t\tfileList.add(file);\n\t\t\t\t}\n\t\t\t\treturn FileVisitResult.CONTINUE;\n\t\t\t}\n\t\t});\n\n\t\treturn fileList;\n\t}\n\n\t/**\n\t * 遍历指定path下的文件并做处理\n\t *\n\t * @param start   起始路径，必须为目录\n\t * @param visitor {@link FileVisitor} 接口，用于自定义在访问文件时，访问目录前后等节点做的操作\n\t * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor)\n\t * @since 5.5.2\n\t */\n\tpublic static void walkFiles(Path start, FileVisitor<? super Path> visitor) {\n\t\twalkFiles(start, -1, visitor);\n\t}\n\n\t/**\n\t * 遍历指定path下的文件并做处理\n\t *\n\t * @param start    起始路径，必须为目录\n\t * @param maxDepth 最大遍历深度，-1表示不限制深度\n\t * @param visitor  {@link FileVisitor} 接口，用于自定义在访问文件时，访问目录前后等节点做的操作\n\t * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor)\n\t * @since 4.6.3\n\t */\n\tpublic static void walkFiles(Path start, int maxDepth, FileVisitor<? super Path> visitor) {\n\t\twalkFiles(start, maxDepth, false, visitor);\n\t}\n\n\t/**\n\t * 遍历指定path下的文件并做处理\n\t *\n\t * @param start         起始路径，必须为目录\n\t * @param maxDepth      最大遍历深度，-1表示不限制深度\n\t * @param visitor       {@link FileVisitor} 接口，用于自定义在访问文件时，访问目录前后等节点做的操作\n\t * @param isFollowLinks 是否追踪到软链对应的真实地址\n\t * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor)\n\t * @since 5.8.23\n\t */\n\tpublic static void walkFiles(final Path start, int maxDepth, final boolean isFollowLinks, final FileVisitor<? super Path> visitor) {\n\t\tif (maxDepth < 0) {\n\t\t\t// < 0 表示遍历到最底层\n\t\t\tmaxDepth = Integer.MAX_VALUE;\n\t\t}\n\n\t\ttry {\n\t\t\tFiles.walkFileTree(start, getFileVisitOption(isFollowLinks), maxDepth, visitor);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 删除文件或者文件夹，不追踪软链<br>\n\t * 注意：删除文件夹时不会判断文件夹是否为空，如果不空则递归删除子文件或文件夹<br>\n\t * 某个文件删除失败会终止删除操作\n\t *\n\t * @param path 文件对象\n\t * @return 成功与否\n\t * @throws IORuntimeException IO异常\n\t * @since 4.4.2\n\t */\n\tpublic static boolean del(Path path) throws IORuntimeException {\n\t\tif (null == path || Files.notExists(path)) {\n\t\t\treturn true;\n\t\t}\n\n\t\ttry {\n\t\t\tif (isDirectory(path)) {\n\t\t\t\tFiles.walkFileTree(path, DelVisitor.INSTANCE);\n\t\t\t} else {\n\t\t\t\tdelFile(path);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 通过JDK7+的 {@link Files#copy(InputStream, Path, CopyOption...)} 方法拷贝文件\n\t *\n\t * @param src     源文件流\n\t * @param target  目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标Path\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.27\n\t */\n\tpublic static Path copyFile(Resource src, Path target, CopyOption... options) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Source is null !\");\n\t\tif(src instanceof FileResource){\n\t\t\treturn copyFile(((FileResource) src).getFile().toPath(), target, options);\n\t\t}\n\t\ttry(InputStream stream = src.getStream()){\n\t\t\treturn copyFile(stream, target, options);\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 通过JDK7+的 {@link Files#copy(InputStream, Path, CopyOption...)} 方法拷贝文件\n\t *\n\t * @param src     源文件流，使用后不闭流\n\t * @param target  目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标Path\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.27\n\t */\n\tpublic static Path copyFile(InputStream src, Path target, CopyOption... options) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Source is null !\");\n\t\tAssert.notNull(target, \"Destination File or directory is null !\");\n\n\t\t// 创建级联父目录\n\t\tmkParentDirs(target);\n\n\t\ttry {\n\t\t\tFiles.copy(src, target, options);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn target;\n\t}\n\n\t/**\n\t * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件<br>\n\t * 此方法不支持递归拷贝目录，如果src传入是目录，只会在目标目录中创建空目录\n\t *\n\t * @param src     源文件路径，如果为目录只在目标中创建新目录\n\t * @param dest    目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标Path\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Path copyFile(Path src, Path dest, StandardCopyOption... options) throws IORuntimeException {\n\t\treturn copyFile(src, dest, (CopyOption[]) options);\n\t}\n\n\t/**\n\t * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件<br>\n\t * 此方法不支持递归拷贝目录，如果src传入是目录，只会在目标目录中创建空目录\n\t *\n\t * @param src     源文件路径，如果为目录只在目标中创建新目录\n\t * @param target  目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return 目标Path\n\t * @throws IORuntimeException IO异常\n\t * @since 5.4.1\n\t */\n\tpublic static Path copyFile(Path src, Path target, CopyOption... options) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Source File is null !\");\n\t\tAssert.notNull(target, \"Destination File or directory is null !\");\n\n\t\tfinal Path targetPath = isDirectory(target) ? target.resolve(src.getFileName()) : target;\n\t\t// 创建级联父目录\n\t\tmkParentDirs(targetPath);\n\t\ttry {\n\t\t\treturn Files.copy(src, targetPath, options);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 拷贝文件或目录，拷贝规则为：\n\t *\n\t * <ul>\n\t *     <li>源文件为目录，目标也为目录或不存在，则拷贝整个目录到目标目录下</li>\n\t *     <li>源文件为文件，目标为目录或不存在，则拷贝文件到目标目录下</li>\n\t *     <li>源文件为文件，目标也为文件，则在{@link StandardCopyOption#REPLACE_EXISTING}情况下覆盖之</li>\n\t * </ul>\n\t *\n\t * @param src     源文件路径，如果为目录会在目标中创建新目录\n\t * @param target  目标文件或目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return Path\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.1\n\t */\n\tpublic static Path copy(Path src, Path target, CopyOption... options) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Src path must be not null !\");\n\t\tAssert.notNull(target, \"Target path must be not null !\");\n\n\t\tif (isDirectory(src)) {\n\t\t\treturn copyContent(src, target.resolve(src.getFileName()), options);\n\t\t}\n\t\treturn copyFile(src, target, options);\n\t}\n\n\t/**\n\t * 拷贝目录下的所有文件或目录到目标目录中，此方法不支持文件对文件的拷贝。\n\t * <ul>\n\t *     <li>源文件为目录，目标也为目录或不存在，则拷贝目录下所有文件和目录到目标目录下</li>\n\t *     <li>源文件为文件，目标为目录或不存在，则拷贝文件到目标目录下</li>\n\t * </ul>\n\t *\n\t * @param src     源文件路径，如果为目录只在目标中创建新目录\n\t * @param target  目标目录，如果为目录使用与源文件相同的文件名\n\t * @param options {@link StandardCopyOption}\n\t * @return Path\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.1\n\t */\n\tpublic static Path copyContent(Path src, Path target, CopyOption... options) throws IORuntimeException {\n\t\tAssert.notNull(src, \"Src path must be not null !\");\n\t\tAssert.notNull(target, \"Target path must be not null !\");\n\n\t\ttry {\n\t\t\tFiles.walkFileTree(src, new CopyVisitor(src, target, options));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn target;\n\t}\n\n\t/**\n\t * 判断是否为目录，如果file为null，则返回false<br>\n\t * 此方法不会追踪到软链对应的真实地址，即软链被当作文件\n\t *\n\t * @param path {@link Path}\n\t * @return 如果为目录true\n\t * @since 5.5.1\n\t */\n\tpublic static boolean isDirectory(Path path) {\n\t\treturn isDirectory(path, false);\n\t}\n\n\t/**\n\t * 判断是否为目录，如果file为null，则返回false\n\t *\n\t * @param path          {@link Path}\n\t * @param isFollowLinks 是否追踪到软链对应的真实地址\n\t * @return 如果为目录true\n\t * @since 3.1.0\n\t */\n\tpublic static boolean isDirectory(Path path, boolean isFollowLinks) {\n\t\tif (null == path) {\n\t\t\treturn false;\n\t\t}\n\t\treturn Files.isDirectory(path, getLinkOptions(isFollowLinks));\n\t}\n\n\t/**\n\t * 获取指定位置的子路径部分，支持负数，例如index为-1表示从后数第一个节点位置\n\t *\n\t * @param path  路径\n\t * @param index 路径节点位置，支持负数（负数从后向前计数）\n\t * @return 获取的子路径\n\t * @since 3.1.2\n\t */\n\tpublic static Path getPathEle(Path path, int index) {\n\t\treturn subPath(path, index, index == -1 ? path.getNameCount() : index + 1);\n\t}\n\n\t/**\n\t * 获取指定位置的最后一个子路径部分\n\t *\n\t * @param path 路径\n\t * @return 获取的最后一个子路径\n\t * @since 3.1.2\n\t */\n\tpublic static Path getLastPathEle(Path path) {\n\t\treturn getPathEle(path, path.getNameCount() - 1);\n\t}\n\n\t/**\n\t * 获取指定位置的子路径部分，支持负数，例如起始为-1表示从后数第一个节点位置\n\t *\n\t * @param path      路径\n\t * @param fromIndex 起始路径节点（包括）\n\t * @param toIndex   结束路径节点（不包括）\n\t * @return 获取的子路径\n\t * @since 3.1.2\n\t */\n\tpublic static Path subPath(Path path, int fromIndex, int toIndex) {\n\t\tif (null == path) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int len = path.getNameCount();\n\n\t\tif (fromIndex < 0) {\n\t\t\tfromIndex = len + fromIndex;\n\t\t\tif (fromIndex < 0) {\n\t\t\t\tfromIndex = 0;\n\t\t\t}\n\t\t} else if (fromIndex > len) {\n\t\t\tfromIndex = len;\n\t\t}\n\n\t\tif (toIndex < 0) {\n\t\t\ttoIndex = len + toIndex;\n\t\t\tif (toIndex < 0) {\n\t\t\t\ttoIndex = len;\n\t\t\t}\n\t\t} else if (toIndex > len) {\n\t\t\ttoIndex = len;\n\t\t}\n\n\t\tif (toIndex < fromIndex) {\n\t\t\tint tmp = fromIndex;\n\t\t\tfromIndex = toIndex;\n\t\t\ttoIndex = tmp;\n\t\t}\n\n\t\tif (fromIndex == toIndex) {\n\t\t\treturn null;\n\t\t}\n\t\treturn path.subpath(fromIndex, toIndex);\n\t}\n\n\t/**\n\t * 获取文件属性\n\t *\n\t * @param path          文件路径{@link Path}\n\t * @param isFollowLinks 是否跟踪到软链对应的真实路径\n\t * @return {@link BasicFileAttributes}\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static BasicFileAttributes getAttributes(Path path, boolean isFollowLinks) throws IORuntimeException {\n\t\tif (null == path) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\treturn Files.readAttributes(path, BasicFileAttributes.class, getLinkOptions(isFollowLinks));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得输入流\n\t *\n\t * @param path Path\n\t * @return 输入流\n\t * @throws IORuntimeException 文件未找到\n\t * @since 4.0.0\n\t */\n\tpublic static BufferedInputStream getInputStream(Path path) throws IORuntimeException {\n\t\tfinal InputStream in;\n\t\ttry {\n\t\t\tin = Files.newInputStream(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn IoUtil.toBuffered(in);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param path 文件Path\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.0\n\t */\n\tpublic static BufferedReader getUtf8Reader(Path path) throws IORuntimeException {\n\t\treturn getReader(path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得一个文件读取器\n\t *\n\t * @param path    文件Path\n\t * @param charset 字符集\n\t * @return BufferedReader对象\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.0\n\t */\n\tpublic static BufferedReader getReader(Path path, Charset charset) throws IORuntimeException {\n\t\treturn IoUtil.getReader(getInputStream(path), charset);\n\t}\n\n\t/**\n\t * 读取文件的所有内容为byte数组\n\t *\n\t * @param path 文件\n\t * @return byte数组\n\t * @since 5.5.4\n\t */\n\tpublic static byte[] readBytes(Path path) {\n\t\ttry {\n\t\t\treturn Files.readAllBytes(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得输出流\n\t *\n\t * @param path Path\n\t * @return 输入流\n\t * @throws IORuntimeException 文件未找到\n\t * @since 5.4.1\n\t */\n\tpublic static BufferedOutputStream getOutputStream(Path path) throws IORuntimeException {\n\t\tfinal OutputStream in;\n\t\ttry {\n\t\t\tin = Files.newOutputStream(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn IoUtil.toBuffered(in);\n\t}\n\n\t/**\n\t * 修改文件或目录的文件名，不变更路径，只是简单修改文件名<br>\n\t *\n\t * <pre>\n\t * FileUtil.rename(file, \"aaa.jpg\", false) xx/xx.png =》xx/aaa.jpg\n\t * </pre>\n\t *\n\t * @param path       被修改的文件\n\t * @param newName    新的文件名，包括扩展名\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标文件Path\n\t * @since 5.4.1\n\t */\n\tpublic static Path rename(Path path, String newName, boolean isOverride) {\n\t\treturn move(path, path.resolveSibling(newName), isOverride);\n\t}\n\n\t/**\n\t * 移动文件或目录到目标中，例如：\n\t * <ul>\n\t *     <li>如果src和target为同一文件或目录，直接返回target。</li>\n\t *     <li>如果src为文件，target为目录，则移动到目标目录下，存在同名文件则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为文件，则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为不存在的路径，则重命名源文件到目标指定的文件，如moveContent(\"/a/b\", \"/c/d\"), d不存在，则b变成d。</li>\n\t *     <li>如果src为目录，target为文件，抛出{@link IllegalArgumentException}</li>\n\t *     <li>如果src为目录，target为目录，则将源目录及其内容移动到目标路径目录中，如move(\"/a/b\", \"/c/d\")，结果为\"/c/d/b\"</li>\n\t *     <li>如果src为目录，target为不存在的路径，则重命名src到target，如move(\"/a/b\", \"/c/d\")，结果为\"/c/d/\"，相当于b重命名为d</li>\n\t * </ul>\n\t *\n\t * @param src        源文件或目录路径\n\t * @param target     目标路径，如果为目录，则移动到此目录下\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标文件Path\n\t */\n\tpublic static Path move(Path src, Path target, boolean isOverride) {\n\t\treturn PathMover.of(src, target, isOverride).move();\n\t}\n\n\t/**\n\t * 移动文件或目录内容到目标中，例如：\n\t * <ul>\n\t *     <li>如果src为文件，target为目录，则移动到目标目录下，存在同名文件则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为文件，则按照是否覆盖参数执行。</li>\n\t *     <li>如果src为文件，target为不存在的路径，则重命名源文件到目标指定的文件，如moveContent(\"/a/b\", \"/c/d\"), d不存在，则b变成d。</li>\n\t *     <li>如果src为目录，target为文件，抛出{@link IllegalArgumentException}</li>\n\t *     <li>如果src为目录，target为目录，则将源目录下的内容移动到目标路径目录中，源目录不删除。</li>\n\t *     <li>如果src为目录，target为不存在的路径，则创建目标路径为目录，将源目录下的内容移动到目标路径目录中，源目录不删除。</li>\n\t * </ul>\n\t *\n\t * @param src        源文件或目录路径\n\t * @param target     目标路径，如果为目录，则移动到此目录下\n\t * @param isOverride 是否覆盖目标文件\n\t * @return 目标文件Path\n\t */\n\tpublic static Path moveContent(Path src, Path target, boolean isOverride) {\n\t\treturn PathMover.of(src, target, isOverride).moveContent();\n\t}\n\n\t/**\n\t * 检查两个文件是否是同一个文件<br>\n\t * 所谓文件相同，是指Path对象是否指向同一个文件或文件夹\n\t *\n\t * @param file1 文件1\n\t * @param file2 文件2\n\t * @return 是否相同\n\t * @throws IORuntimeException IO异常\n\t * @see Files#isSameFile(Path, Path)\n\t * @since 5.4.1\n\t */\n\tpublic static boolean equals(Path file1, Path file2) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn Files.isSameFile(file1, file2);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 判断是否为文件，如果file为null，则返回false\n\t *\n\t * @param path          文件\n\t * @param isFollowLinks 是否跟踪软链（快捷方式）\n\t * @return 如果为文件true\n\t * @see Files#isRegularFile(Path, LinkOption...)\n\t */\n\tpublic static boolean isFile(Path path, boolean isFollowLinks) {\n\t\tif (null == path) {\n\t\t\treturn false;\n\t\t}\n\t\treturn Files.isRegularFile(path, getLinkOptions(isFollowLinks));\n\t}\n\n\t/**\n\t * 判断是否为符号链接文件\n\t *\n\t * @param path 被检查的文件\n\t * @return 是否为符号链接文件\n\t * @since 4.4.2\n\t */\n\tpublic static boolean isSymlink(Path path) {\n\t\treturn Files.isSymbolicLink(path);\n\t}\n\n\t/**\n\t * 判断文件或目录是否存在\n\t *\n\t * @param path          文件\n\t * @param isFollowLinks 是否跟踪软链（快捷方式）\n\t * @return 是否存在\n\t * @since 5.5.3\n\t */\n\tpublic static boolean exists(Path path, boolean isFollowLinks) {\n\t\treturn Files.exists(path, getLinkOptions(isFollowLinks));\n\t}\n\n\t/**\n\t * 判断是否存在且为非目录\n\t * <ul>\n\t *     <li>如果path为{@code null}，返回{@code false}</li>\n\t *     <li>如果path不存在，返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param path          {@link Path}\n\t * @param isFollowLinks 是否追踪到软链对应的真实地址\n\t * @return 如果为目录true\n\t * @since 5.8.14\n\t */\n\tpublic static boolean isExistsAndNotDirectory(final Path path, final boolean isFollowLinks) {\n\t\treturn exists(path, isFollowLinks) && false == isDirectory(path, isFollowLinks);\n\t}\n\n\t/**\n\t * 判断给定的目录是否为给定文件或文件夹的子目录\n\t *\n\t * @param parent 父目录\n\t * @param sub    子目录\n\t * @return 子目录是否为父目录的子目录\n\t * @since 5.5.5\n\t */\n\tpublic static boolean isSub(Path parent, Path sub) {\n\t\treturn toAbsNormal(sub).startsWith(toAbsNormal(parent));\n\t}\n\n\t/**\n\t * 将Path路径转换为标准的绝对路径\n\t *\n\t * @param path 文件或目录Path\n\t * @return 转换后的Path\n\t * @since 5.5.5\n\t */\n\tpublic static Path toAbsNormal(Path path) {\n\t\tAssert.notNull(path);\n\t\treturn path.toAbsolutePath().normalize();\n\t}\n\n\t/**\n\t * 获得文件的MimeType\n\t *\n\t * @param file 文件\n\t * @return MimeType\n\t * @see Files#probeContentType(Path)\n\t * @since 5.5.5\n\t */\n\tpublic static String getMimeType(Path file) {\n\t\ttry {\n\t\t\treturn Files.probeContentType(file);\n\t\t} catch (IOException ignore) {\n\t\t\t// issue#3179，使用OpenJDK可能抛出NoSuchFileException，此处返回null\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 创建所给目录及其父目录\n\t *\n\t * @param dir 目录\n\t * @return 目录\n\t * @since 5.5.7\n\t */\n\tpublic static Path mkdir(Path dir) {\n\t\tif (null != dir && false == exists(dir, false)) {\n\t\t\ttry {\n\t\t\t\tFiles.createDirectories(dir);\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * 创建所给文件或目录的父目录\n\t *\n\t * @param path 文件或目录\n\t * @return 父目录\n\t * @since 5.5.7\n\t */\n\tpublic static Path mkParentDirs(Path path) {\n\t\treturn mkdir(path.getParent());\n\t}\n\n\t/**\n\t * 获取{@link Path}文件名\n\t *\n\t * @param path {@link Path}\n\t * @return 文件名\n\t * @since 5.7.15\n\t */\n\tpublic static String getName(Path path) {\n\t\tif (null == path) {\n\t\t\treturn null;\n\t\t}\n\t\treturn path.getFileName().toString();\n\t}\n\n\t/**\n\t * 创建临时文件<br>\n\t * 创建后的文件名为 prefix[Random].suffix From com.jodd.io.FileUtil\n\t *\n\t * @param prefix    前缀，至少3个字符\n\t * @param suffix    后缀，如果null则使用默认.tmp\n\t * @param dir       临时文件创建的所在目录\n\t * @return 临时文件\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static Path createTempFile(final String prefix, final String suffix, final Path dir) throws IORuntimeException {\n\t\tint exceptionsCount = 0;\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tif(null == dir){\n\t\t\t\t\treturn Files.createTempFile(prefix, suffix);\n\t\t\t\t}else{\n\t\t\t\t\treturn Files.createTempFile(mkdir(dir), prefix, suffix);\n\t\t\t\t}\n\t\t\t} catch (final IOException ioex) { // fixes java.io.WinNTFileSystem.createFileExclusively access denied\n\t\t\t\tif (++exceptionsCount >= 50) {\n\t\t\t\t\tthrow new IORuntimeException(ioex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 删除文件或空目录，不追踪软链\n\t *\n\t * @param path 文件对象\n\t * @throws IOException IO异常\n\t * @since 5.7.7\n\t */\n\tprotected static void delFile(Path path) throws IOException {\n\t\ttry {\n\t\t\tFiles.delete(path);\n\t\t} catch (AccessDeniedException e) {\n\t\t\t// 可能遇到只读文件，无法删除.使用 file 方法删除\n\t\t\tif (false == path.toFile().delete()) {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 构建是否追踪软链的选项\n\t *\n\t * @param isFollowLinks 是否追踪软链\n\t * @return 选项\n\t * @since 5.8.23\n\t */\n\tpublic static LinkOption[] getLinkOptions(final boolean isFollowLinks) {\n\t\treturn isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS};\n\t}\n\n\t/**\n\t * 构建是否追踪软链的选项\n\t *\n\t * @param isFollowLinks 是否追踪软链\n\t * @return 选项\n\t * @since 5.8.23\n\t */\n\tpublic static Set<FileVisitOption> getFileVisitOption(final boolean isFollowLinks) {\n\t\treturn isFollowLinks ? EnumSet.of(FileVisitOption.FOLLOW_LINKS) :\n\t\t\tEnumSet.noneOf(FileVisitOption.class);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/Tailer.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.date.DateUnit;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.LineHandler;\nimport cn.hutool.core.io.watch.SimpleWatcher;\nimport cn.hutool.core.io.watch.WatchKind;\nimport cn.hutool.core.io.watch.WatchMonitor;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\nimport java.util.Stack;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 文件内容跟随器，实现类似Linux下\"tail -f\"命令功能\n *\n * @author looly\n * @since 4.5.2\n */\npublic class Tailer implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final LineHandler CONSOLE_HANDLER = new ConsoleLineHandler();\n\n\t/** 编码 */\n\tprivate final Charset charset;\n\t/** 行处理器 */\n\tprivate final LineHandler lineHandler;\n\t/** 初始读取的行数 */\n\tprivate final int initReadLine;\n\t/** 定时任务检查间隔时长 */\n\tprivate final long period;\n\n\tprivate final String filePath;\n\tprivate final RandomAccessFile randomAccessFile;\n\tprivate final ScheduledExecutorService executorService;\n\tprivate WatchMonitor fileDeleteWatchMonitor;\n\tprivate boolean stopOnDelete;\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param file 文件\n\t * @param lineHandler 行处理器\n\t */\n\tpublic Tailer(File file, LineHandler lineHandler) {\n\t\tthis(file, lineHandler, 0);\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param file 文件\n\t * @param lineHandler 行处理器\n\t * @param initReadLine 启动时预读取的行数，1表示一行\n\t */\n\tpublic Tailer(File file, LineHandler lineHandler, int initReadLine) {\n\t\tthis(file, CharsetUtil.CHARSET_UTF_8, lineHandler, initReadLine, DateUnit.SECOND.getMillis());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 文件\n\t * @param charset 编码\n\t * @param lineHandler 行处理器\n\t */\n\tpublic Tailer(File file, Charset charset, LineHandler lineHandler) {\n\t\tthis(file, charset, lineHandler, 0, DateUnit.SECOND.getMillis());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 文件\n\t * @param charset 编码\n\t * @param lineHandler 行处理器\n\t * @param initReadLine 启动时预读取的行数，1表示一行\n\t * @param period 检查间隔\n\t */\n\tpublic Tailer(File file, Charset charset, LineHandler lineHandler, int initReadLine, long period) {\n\t\tcheckFile(file);\n\t\tthis.charset = charset;\n\t\tthis.lineHandler = lineHandler;\n\t\tthis.period = period;\n\t\tthis.initReadLine = initReadLine;\n\t\tthis.randomAccessFile = FileUtil.createRandomAccessFile(file, FileMode.r);\n\t\tthis.executorService = Executors.newSingleThreadScheduledExecutor();\n\t\tthis.filePath=file.getAbsolutePath();\n\t}\n\n\t/**\n\t * 设置删除文件后是否退出并抛出异常\n\t *\n\t * @param stopOnDelete 删除文件后是否退出并抛出异常\n\t */\n\tpublic void setStopOnDelete(final boolean stopOnDelete) {\n\t\tthis.stopOnDelete = stopOnDelete;\n\t}\n\n\t/**\n\t * 开始监听\n\t */\n\tpublic void start() {\n\t\tstart(false);\n\t}\n\n\t/**\n\t * 开始监听\n\t *\n\t * @param async 是否异步执行\n\t */\n\tpublic void start(boolean async) {\n\t\t// 初始读取\n\t\ttry {\n\t\t\tthis.readTail();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tfinal LineReadWatcher lineReadWatcher = new LineReadWatcher(this.randomAccessFile, this.charset, this.lineHandler);\n\t\tfinal ScheduledFuture<?> scheduledFuture = this.executorService.scheduleAtFixedRate(//\n\t\t\t\tlineReadWatcher, //\n\t\t\t\t0, //\n\t\t\t\tthis.period, TimeUnit.MILLISECONDS//\n\t\t);\n\n\t\t// 监听删除\n\t\tif(stopOnDelete){\n\t\t\tfileDeleteWatchMonitor = WatchMonitor.create(this.filePath, WatchKind.DELETE.getValue());\n\t\t\tfileDeleteWatchMonitor.setWatcher(new SimpleWatcher(){\n\t\t\t\t@Override\n\t\t\t\tpublic void onDelete(final WatchEvent<?> event, final Path currentPath) {\n\t\t\t\t\tsuper.onDelete(event, currentPath);\n\t\t\t\t\tstop();\n\t\t\t\t\tthrow new IORuntimeException(\"{} has been deleted\", filePath);\n\t\t\t\t}\n\t\t\t});\n\t\t\tfileDeleteWatchMonitor.start();\n\t\t}\n\n\t\tif (false == async) {\n\t\t\ttry {\n\t\t\t\tscheduledFuture.get();\n\t\t\t} catch (ExecutionException e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\t// ignore and exist\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 结束，此方法需在异步模式或\n\t */\n\tpublic void stop(){\n\t\ttry{\n\t\t\tthis.executorService.shutdown();\n\t\t}finally {\n\t\t\tIoUtil.close(this.randomAccessFile);\n\t\t\tIoUtil.close(this.fileDeleteWatchMonitor);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 预读取行\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void readTail() throws IOException {\n\t\tfinal long len = this.randomAccessFile.length();\n\n\t\tif (initReadLine > 0) {\n\t\t\tStack<String> stack = new Stack<>();\n\n\t\t\tlong start = this.randomAccessFile.getFilePointer();\n\t\t\tlong nextEnd = (len - 1) < 0 ? 0 : len - 1;\n\t\t\tthis.randomAccessFile.seek(nextEnd);\n\t\t\tint c;\n\t\t\tint currentLine = 0;\n\t\t\twhile (nextEnd > start) {\n\t\t\t\t// 满\n\t\t\t\tif (currentLine >= initReadLine) {\n\t\t\t\t\t// issue#IA77ML initReadLine是行数，从1开始，currentLine是行号，从0开始\n\t\t\t\t\t// 因此行号0表示一行，所以currentLine == initReadLine表示读取完毕\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tc = this.randomAccessFile.read();\n\t\t\t\tif (c == CharUtil.LF || c == CharUtil.CR) {\n\t\t\t\t\t// FileUtil.readLine(this.randomAccessFile, this.charset, this.lineHandler);\n\t\t\t\t\tfinal String line = FileUtil.readLine(this.randomAccessFile, this.charset);\n\t\t\t\t\tif(null != line) {\n\t\t\t\t\t\tstack.push(line);\n\t\t\t\t\t}\n\t\t\t\t\tcurrentLine++;\n\t\t\t\t\tnextEnd--;\n\t\t\t\t}\n\t\t\t\tnextEnd--;\n\t\t\t\tthis.randomAccessFile.seek(nextEnd);\n\t\t\t\tif (nextEnd == 0) {\n\t\t\t\t\t// 当文件指针退至文件开始处，输出第一行\n\t\t\t\t\t// FileUtil.readLine(this.randomAccessFile, this.charset, this.lineHandler);\n\t\t\t\t\tfinal String line = FileUtil.readLine(this.randomAccessFile, this.charset);\n\t\t\t\t\tif(null != line) {\n\t\t\t\t\t\tstack.push(line);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 输出缓存栈中的内容\n\t\t\twhile (false == stack.isEmpty()) {\n\t\t\t\tthis.lineHandler.handle(stack.pop());\n\t\t\t}\n\t\t}\n\n\t\t// 将指针置于末尾\n\t\ttry {\n\t\t\tthis.randomAccessFile.seek(len);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 检查文件有效性\n\t *\n\t * @param file 文件\n\t */\n\tprivate static void checkFile(File file) {\n\t\tif (false == file.exists()) {\n\t\t\tthrow new UtilException(\"File [{}] not exist !\", file.getAbsolutePath());\n\t\t}\n\t\tif (false == file.isFile()) {\n\t\t\tthrow new UtilException(\"Path [{}] is not a file !\", file.getAbsolutePath());\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------------------- Private method end\n\n\t/**\n\t * 命令行打印的行处理器\n\t *\n\t * @author looly\n\t * @since 4.5.2\n\t */\n\tpublic static class ConsoleLineHandler implements LineHandler {\n\t\t@Override\n\t\tpublic void handle(String line) {\n\t\t\tConsole.log(line);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/package-info.java",
    "content": "/**\n * 对文件读写的封装，包括文件拷贝、文件读取、文件写出、行处理等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.file;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java",
    "content": "package cn.hutool.core.io.file.visitor;\n\nimport cn.hutool.core.io.file.PathUtil;\n\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\n/**\n * 文件拷贝的FileVisitor实现，用于递归遍历拷贝目录，此类非线程安全<br>\n * 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。\n *\n * @author looly\n * @since 5.5.1\n */\npublic class CopyVisitor extends SimpleFileVisitor<Path> {\n\n\t/**\n\t * 源Path，或基准路径，用于计算被拷贝文件的相对路径\n\t */\n\tprivate final Path source;\n\tprivate final Path target;\n\tprivate final CopyOption[] copyOptions;\n\n\t/**\n\t * 标记目标目录是否创建，省略每次判断目标是否存在\n\t */\n\tprivate boolean isTargetCreated;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source      源Path，或基准路径，用于计算被拷贝文件的相对路径\n\t * @param target      目标Path\n\t * @param copyOptions 拷贝选项，如跳过已存在等\n\t */\n\tpublic CopyVisitor(Path source, Path target, CopyOption... copyOptions) {\n\t\tif (PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)) {\n\t\t\tthrow new IllegalArgumentException(\"Target must be a directory\");\n\t\t}\n\t\tthis.source = source;\n\t\tthis.target = target;\n\t\tthis.copyOptions = copyOptions;\n\t}\n\n\t@Override\n\tpublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {\n\t\tinitTargetDir();\n\t\t// 将当前目录相对于源路径转换为相对于目标路径\n\t\tfinal Path targetDir = resolveTarget(dir);\n\n\t\t// 在目录不存在的情况下，copy方法会创建新目录\n\t\ttry {\n\t\t\tFiles.copy(dir, targetDir, copyOptions);\n\t\t} catch (FileAlreadyExistsException e) {\n\t\t\tif (false == Files.isDirectory(targetDir)) {\n\t\t\t\t// 目标文件存在抛出异常，目录忽略\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t@Override\n\tpublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n\t\t\tthrows IOException {\n\t\tinitTargetDir();\n\t\t// 如果目标存在，无论目录还是文件都抛出FileAlreadyExistsException异常，此处不做特别处理\n\t\tFiles.copy(file, resolveTarget(file), copyOptions);\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t/**\n\t * 根据源文件或目录路径，拼接生成目标的文件或目录路径<br>\n\t * 原理是首先截取源路径，得到相对路径，再和目标路径拼接\n\t *\n\t * <p>\n\t * 如：源路径是 /opt/test/，需要拷贝的文件是 /opt/test/a/a.txt，得到相对路径 a/a.txt<br>\n\t * 目标路径是/home/，则得到最终目标路径是 /home/a/a.txt\n\t * </p>\n\t *\n\t * @param file 需要拷贝的文件或目录Path\n\t * @return 目标Path\n\t */\n\tprivate Path resolveTarget(Path file) {\n\t\treturn target.resolve(source.relativize(file));\n\t}\n\n\t/**\n\t * 初始化目标文件或目录\n\t */\n\tprivate void initTargetDir() {\n\t\tif (false == this.isTargetCreated) {\n\t\t\tPathUtil.mkdir(this.target);\n\t\t\tthis.isTargetCreated = true;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/visitor/DelVisitor.java",
    "content": "package cn.hutool.core.io.file.visitor;\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\n/**\n * 删除操作的FileVisitor实现，用于递归遍历删除文件夹\n *\n * @author looly\n * @since 5.5.1\n */\npublic class DelVisitor extends SimpleFileVisitor<Path> {\n\n\tpublic static DelVisitor INSTANCE = new DelVisitor();\n\n\t@Override\n\tpublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n\t\tFiles.delete(file);\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t/**\n\t * 访问目录结束后删除目录，当执行此方法时，子文件或目录都已访问（删除）完毕<br>\n\t * 理论上当执行到此方法时，目录下已经被清空了\n\t *\n\t * @param dir 目录\n\t * @param e   异常\n\t * @return {@link FileVisitResult}\n\t * @throws IOException IO异常\n\t */\n\t@Override\n\tpublic FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {\n\t\tif (e == null) {\n\t\t\tFiles.delete(dir);\n\t\t\treturn FileVisitResult.CONTINUE;\n\t\t} else {\n\t\t\tthrow e;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/visitor/MoveVisitor.java",
    "content": "package cn.hutool.core.io.file.visitor;\n\nimport cn.hutool.core.io.file.PathUtil;\n\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\n/**\n * 文件移动操作的FileVisitor实现，用于递归遍历移动目录和文件，此类非线程安全<br>\n * 此类在遍历源目录并移动过程中会自动创建目标目录中不存在的上级目录。\n *\n * @author looly\n * @since 5.7.7\n */\npublic class MoveVisitor extends SimpleFileVisitor<Path> {\n\n\tprivate final Path source;\n\tprivate final Path target;\n\tprivate boolean isTargetCreated;\n\tprivate final CopyOption[] copyOptions;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 源Path\n\t * @param target 目标Path\n\t * @param copyOptions 拷贝（移动）选项\n\t */\n\tpublic MoveVisitor(Path source, Path target, CopyOption... copyOptions) {\n\t\tif(PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)){\n\t\t\tthrow new IllegalArgumentException(\"Target must be a directory\");\n\t\t}\n\t\tthis.source = source;\n\t\tthis.target = target;\n\t\tthis.copyOptions = copyOptions;\n\t}\n\n\t@Override\n\tpublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)\n\t\t\tthrows IOException {\n\t\tinitTarget();\n\t\t// 将当前目录相对于源路径转换为相对于目标路径\n\t\tfinal Path targetDir = target.resolve(source.relativize(dir));\n\t\tif(false == Files.exists(targetDir)){\n\t\t\tFiles.createDirectories(targetDir);\n\t\t} else if(false == Files.isDirectory(targetDir)){\n\t\t\tthrow new FileAlreadyExistsException(targetDir.toString());\n\t\t}\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t@Override\n\tpublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n\t\t\tthrows IOException {\n\t\tinitTarget();\n\t\tFiles.move(file, target.resolve(source.relativize(file)), copyOptions);\n\t\treturn FileVisitResult.CONTINUE;\n\t}\n\n\t/**\n\t * 初始化目标文件或目录\n\t */\n\tprivate void initTarget(){\n\t\tif(false == this.isTargetCreated){\n\t\t\tPathUtil.mkdir(this.target);\n\t\t\tthis.isTargetCreated = true;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/file/visitor/package-info.java",
    "content": "/**\n * FileVisitor功能性实现，包括递归删除、拷贝等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.file.visitor;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/package-info.java",
    "content": "/**\n * IO相关封装和工具类，包括Inputstream和OutputStream实现类，工具包括流工具IoUtil、文件工具FileUtil和Buffer工具BufferUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/BytesResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.net.URL;\nimport java.nio.charset.Charset;\n\n/**\n * 基于byte[]的资源获取器<br>\n * 注意：此对象中getUrl方法始终返回null\n *\n * @author looly\n * @since 4.0.9\n */\npublic class BytesResource implements Resource, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final byte[] bytes;\n\tprivate final String name;\n\n\t/**\n\t * 构造\n\t *\n\t * @param bytes 字节数组\n\t */\n\tpublic BytesResource(byte[] bytes) {\n\t\tthis(bytes, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param bytes 字节数组\n\t * @param name 资源名称\n\t */\n\tpublic BytesResource(byte[] bytes, String name) {\n\t\tthis.bytes = bytes;\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn new ByteArrayInputStream(this.bytes);\n\t}\n\n\t@Override\n\tpublic String readStr(Charset charset) throws IORuntimeException {\n\t\treturn StrUtil.str(this.bytes, charset);\n\t}\n\n\t@Override\n\tpublic byte[] readBytes() throws IORuntimeException {\n\t\treturn this.bytes;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.io.StringReader;\nimport java.net.URL;\nimport java.nio.charset.Charset;\n\n/**\n * {@link CharSequence}资源，字符串做为资源\n *\n * @author looly\n * @since 5.5.2\n */\npublic class CharSequenceResource implements Resource, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final CharSequence data;\n\tprivate final CharSequence name;\n\tprivate final Charset charset;\n\n\t/**\n\t * 构造，使用UTF8编码\n\t *\n\t * @param data 资源数据\n\t */\n\tpublic CharSequenceResource(CharSequence data) {\n\t\tthis(data, null);\n\t}\n\n\t/**\n\t * 构造，使用UTF8编码\n\t *\n\t * @param data 资源数据\n\t * @param name 资源名称\n\t */\n\tpublic CharSequenceResource(CharSequence data, String name) {\n\t\tthis(data, name, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param data 资源数据\n\t * @param name 资源名称\n\t * @param charset 编码\n\t */\n\tpublic CharSequenceResource(CharSequence data, CharSequence name, Charset charset) {\n\t\tthis.data = data;\n\t\tthis.name = name;\n\t\tthis.charset = charset;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn StrUtil.str(this.name);\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn new ByteArrayInputStream(readBytes());\n\t}\n\n\t@Override\n\tpublic BufferedReader getReader(Charset charset) {\n\t\treturn IoUtil.getReader(new StringReader(this.data.toString()));\n\t}\n\n\t@Override\n\tpublic String readStr(Charset charset) throws IORuntimeException {\n\t\treturn this.data.toString();\n\t}\n\n\t@Override\n\tpublic byte[] readBytes() throws IORuntimeException {\n\t\treturn this.data.toString().getBytes(this.charset);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.net.URL;\n\n/**\n * ClassPath单一资源访问类<br>\n * 传入路径path必须为相对路径，如果传入绝对路径，Linux路径会去掉开头的“/”，而Windows路径会直接报错。<br>\n * 传入的path所指向的资源必须存在，否则报错\n *\n * @author Looly\n *\n */\npublic class ClassPathResource extends UrlResource {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String path;\n\tprivate final ClassLoader classLoader;\n\tprivate final Class<?> clazz;\n\n\t// -------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param path 相对于ClassPath的路径\n\t */\n\tpublic ClassPathResource(String path) {\n\t\tthis(path, null, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path 相对于ClassPath的路径\n\t * @param classLoader {@link ClassLoader}\n\t */\n\tpublic ClassPathResource(String path, ClassLoader classLoader) {\n\t\tthis(path, classLoader, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path 相对于给定Class的路径\n\t * @param clazz {@link Class} 用于定位路径\n\t */\n\tpublic ClassPathResource(String path, Class<?> clazz) {\n\t\tthis(path, null, clazz);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pathBaseClassLoader 相对路径\n\t * @param classLoader {@link ClassLoader}\n\t * @param clazz {@link Class} 用于定位路径\n\t */\n\tpublic ClassPathResource(String pathBaseClassLoader, ClassLoader classLoader, Class<?> clazz) {\n\t\tsuper((URL) null);\n\t\tAssert.notNull(pathBaseClassLoader, \"Path must not be null\");\n\n\t\tfinal String path = normalizePath(pathBaseClassLoader);\n\t\tthis.path = path;\n\t\tthis.name = StrUtil.isBlank(path) ? null : FileUtil.getName(path);\n\n\t\tthis.classLoader = ObjectUtil.defaultIfNull(classLoader, ClassUtil::getClassLoader);\n\t\tthis.clazz = clazz;\n\t\tinitUrl();\n\t}\n\t// -------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获得Path\n\t *\n\t * @return path\n\t */\n\tpublic final String getPath() {\n\t\treturn this.path;\n\t}\n\n\t/**\n\t * 获得绝对路径Path<br>\n\t * 对于不存在的资源，返回拼接后的绝对路径\n\t *\n\t * @return 绝对路径path\n\t */\n\tpublic final String getAbsolutePath() {\n\t\tif (FileUtil.isAbsolutePath(this.path)) {\n\t\t\treturn this.path;\n\t\t}\n\t\t// url在初始化的时候已经断言，此处始终不为null\n\t\treturn FileUtil.normalize(URLUtil.getDecodedPath(this.url));\n\t}\n\n\t/**\n\t * 获得 {@link ClassLoader}\n\t *\n\t * @return {@link ClassLoader}\n\t */\n\tpublic final ClassLoader getClassLoader() {\n\t\treturn this.classLoader;\n\t}\n\n\t/**\n\t * 根据给定资源初始化URL\n\t */\n\tprivate void initUrl() {\n\t\tif (null != this.clazz) {\n\t\t\tsuper.url = this.clazz.getResource(this.path);\n\t\t} else if (null != this.classLoader) {\n\t\t\tsuper.url = this.classLoader.getResource(this.path);\n\t\t} else {\n\t\t\tsuper.url = ClassLoader.getSystemResource(this.path);\n\t\t}\n\t\tif (null == super.url) {\n\t\t\tthrow new NoResourceException(\"Resource of path [{}] not exist!\", this.path);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn (null == this.path) ? super.toString() : \"classpath:\" + this.path;\n\t}\n\n\t/**\n\t * 标准化Path格式\n\t *\n\t * @param path Path\n\t * @return 标准化后的path\n\t */\n\tprivate String normalizePath(String path) {\n\t\t// 标准化路径\n\t\tpath = FileUtil.normalize(path);\n\t\tpath = StrUtil.removePrefix(path, StrUtil.SLASH);\n\n\t\tAssert.isFalse(FileUtil.isAbsolutePath(path), \"Path [{}] must be a relative path !\", path);\n\t\treturn path;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/FileObjectResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport javax.tools.FileObject;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.nio.charset.Charset;\n\n/**\n * {@link FileObject} 资源包装\n *\n * @author looly\n * @since 5.5.2\n */\npublic class FileObjectResource implements Resource {\n\n\tprivate final FileObject fileObject;\n\n\t/**\n\t * 构造\n\t *\n\t * @param fileObject {@link FileObject}\n\t */\n\tpublic FileObjectResource(FileObject fileObject) {\n\t\tthis.fileObject = fileObject;\n\t}\n\n\t/**\n\t * 获取原始的{@link FileObject}\n\t *\n\t * @return {@link FileObject}\n\t */\n\tpublic FileObject getFileObject() {\n\t\treturn this.fileObject;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.fileObject.getName();\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\ttry {\n\t\t\treturn this.fileObject.toUri().toURL();\n\t\t} catch (MalformedURLException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\ttry {\n\t\t\treturn this.fileObject.openInputStream();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic BufferedReader getReader(Charset charset) {\n\t\ttry {\n\t\t\treturn IoUtil.getReader(this.fileObject.openReader(false));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/FileResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.net.URL;\nimport java.nio.file.Path;\n\n/**\n * 文件资源访问对象，支持{@link Path} 和 {@link File} 访问\n *\n * @author looly\n */\npublic class FileResource implements Resource, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final File file;\n\tprivate final long lastModified;\n\tprivate final String name;\n\n\t// ----------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param path 文件绝对路径或相对ClassPath路径，但是这个路径不能指向一个jar包中的文件\n\t */\n\tpublic FileResource(String path) {\n\t\tthis(FileUtil.file(path));\n\t}\n\n\t/**\n\t * 构造，文件名使用文件本身的名字，带扩展名\n\t *\n\t * @param path 文件\n\t * @since 4.4.1\n\t */\n\tpublic FileResource(Path path) {\n\t\tthis(path.toFile());\n\t}\n\n\t/**\n\t * 构造，文件名使用文件本身的名字，带扩展名\n\t *\n\t * @param file 文件\n\t */\n\tpublic FileResource(File file) {\n\t\tthis(file, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 文件\n\t * @param fileName 文件名，带扩展名，如果为null获取文件本身的文件名\n\t */\n\tpublic FileResource(File file, String fileName) {\n\t\tAssert.notNull(file, \"File must be not null !\");\n\t\tthis.file = file;\n\t\tthis.lastModified = file.lastModified();\n\t\tthis.name = ObjectUtil.defaultIfNull(fileName, file::getName);\n\t}\n\n\t// ----------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t@Override\n\tpublic URL getUrl(){\n\t\treturn URLUtil.getURL(this.file);\n\t}\n\n\t@Override\n\tpublic InputStream getStream() throws NoResourceException {\n\t\tif (!this.file.exists()) {\n\t\t\tthrow new NoResourceException(\"File [{}] not exist!\", this.file.getAbsolutePath());\n\t\t}\n\t\treturn FileUtil.getInputStream(this.file);\n\t}\n\n\t/**\n\t * 获取文件\n\t *\n\t * @return 文件\n\t */\n\tpublic File getFile() {\n\t\treturn this.file;\n\t}\n\n\t@Override\n\tpublic boolean isModified() {\n\t\treturn this.lastModified != file.lastModified();\n\t}\n\n\t/**\n\t * 返回路径\n\t * @return 返回URL路径\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn this.file.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.net.URL;\n\n/**\n * 基于{@link InputStream}的资源获取器<br>\n * 注意：此对象中getUrl方法始终返回null\n *\n * @author looly\n * @since 4.0.9\n */\npublic class InputStreamResource implements Resource, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final InputStream in;\n\tprivate final String name;\n\n\t/**\n\t * 构造\n\t *\n\t * @param in {@link InputStream}\n\t */\n\tpublic InputStreamResource(InputStream in) {\n\t\tthis(in, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param in {@link InputStream}\n\t * @param name 资源名称\n\t */\n\tpublic InputStreamResource(InputStream in, String name) {\n\t\tthis.in = in;\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn this.in;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport java.io.File;\nimport java.util.Collection;\n\n/**\n * 多文件组合资源<br>\n * 此资源为一个利用游标自循环资源，只有调用{@link #next()} 方法才会获取下一个资源，使用完毕后调用{@link #reset()}方法重置游标\n *\n * @author looly\n *\n */\npublic class MultiFileResource extends MultiResource{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param files 文件资源列表\n\t */\n\tpublic MultiFileResource(Collection<File> files) {\n\t\tadd(files);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param files 文件资源列表\n\t */\n\tpublic MultiFileResource(File... files) {\n\t\tadd(files);\n\t}\n\n\t/**\n\t * 增加文件资源\n\t *\n\t * @param files 文件资源\n\t * @return this\n\t */\n\tpublic MultiFileResource add(File... files) {\n\t\tfor (File file : files) {\n\t\t\tthis.add(new FileResource(file));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加文件资源\n\t *\n\t * @param files 文件资源\n\t * @return this\n\t */\n\tpublic MultiFileResource add(Collection<File> files) {\n\t\tfor (File file : files) {\n\t\t\tthis.add(new FileResource(file));\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic MultiFileResource add(Resource resource) {\n\t\treturn (MultiFileResource)super.add(resource);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/MultiResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IORuntimeException;\n\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.ConcurrentModificationException;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * 多资源组合资源<br>\n * 此资源为一个利用游标自循环资源，只有调用{@link #next()} 方法才会获取下一个资源，使用完毕后调用{@link #reset()}方法重置游标\n *\n * @author looly\n * @since 4.1.0\n */\npublic class MultiResource implements Resource, Iterable<Resource>, Iterator<Resource>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final List<Resource> resources;\n\t/**\n\t * 游标\n\t */\n\tprivate int cursor = -1;\n\n\t/**\n\t * 构造\n\t *\n\t * @param resources 资源数组\n\t */\n\tpublic MultiResource(Resource... resources) {\n\t\tthis(CollUtil.newArrayList(resources));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param resources 资源列表\n\t */\n\tpublic MultiResource(Collection<Resource> resources) {\n\t\tif(resources instanceof List) {\n\t\t\tthis.resources = (List<Resource>)resources;\n\t\t}else {\n\t\t\tthis.resources = CollUtil.newArrayList(resources);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn resources.get(getValidCursor()).getName();\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn resources.get(getValidCursor()).getUrl();\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn resources.get(getValidCursor()).getStream();\n\t}\n\n\t@Override\n\tpublic boolean isModified() {\n\t\treturn resources.get(getValidCursor()).isModified();\n\t}\n\n\t@Override\n\tpublic BufferedReader getReader(Charset charset) {\n\t\treturn resources.get(getValidCursor()).getReader(charset);\n\t}\n\n\t@Override\n\tpublic String readStr(Charset charset) throws IORuntimeException {\n\t\treturn resources.get(getValidCursor()).readStr(charset);\n\t}\n\n\t@Override\n\tpublic String readUtf8Str() throws IORuntimeException {\n\t\treturn resources.get(getValidCursor()).readUtf8Str();\n\t}\n\n\t@Override\n\tpublic byte[] readBytes() throws IORuntimeException {\n\t\treturn resources.get(getValidCursor()).readBytes();\n\t}\n\n\t@Override\n\tpublic Iterator<Resource> iterator() {\n\t\treturn resources.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn getValidCursor() < resources.size();\n\t}\n\n\t@Override\n\tpublic synchronized Resource next() {\n\t\tif (!hasNext()) {\n\t\t\tthrow new ConcurrentModificationException();\n\t\t}\n\t\tthis.cursor++;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tthis.resources.remove(getValidCursor());\n\t}\n\n\t/**\n\t * 重置游标\n\t */\n\tpublic synchronized void reset() {\n\t\tthis.cursor = -1;\n\t}\n\n\t/**\n\t * 增加资源\n\t * @param resource 资源\n\t * @return this\n\t */\n\tpublic MultiResource add(Resource resource) {\n\t\tthis.resources.add(resource);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取当前有效游标位置的资源\n\t *\n\t * @return 资源\n\t */\n\tprivate int getValidCursor() {\n\t\treturn Math.max(cursor, 0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/NoResourceException.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 资源文件或资源不存在异常\n *\n * @author xiaoleilu\n * @since 4.0.2\n */\npublic class NoResourceException extends IORuntimeException {\n\tprivate static final long serialVersionUID = -623254467603299129L;\n\n\tpublic NoResourceException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic NoResourceException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic NoResourceException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic NoResourceException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic NoResourceException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n\n\t/**\n\t * 导致这个异常的异常是否是指定类型的异常\n\t *\n\t * @param clazz 异常类\n\t * @return 是否为指定类型异常\n\t */\n\t@Override\n\tpublic boolean causeInstanceOf(Class<? extends Throwable> clazz) {\n\t\tfinal Throwable cause = this.getCause();\n\t\treturn clazz.isInstance(cause);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.URL;\nimport java.nio.charset.Charset;\n\n/**\n * 资源接口定义<br>\n * <p>资源是数据表示的统称，我们可以将任意的数据封装为一个资源，然后读取其内容。</p>\n * <p>资源可以是文件、URL、ClassPath中的文件亦或者jar(zip)包中的文件。</p>\n * <p>\n *     提供资源接口的意义在于，我们可以使用一个方法接收任意类型的数据，从而处理数据，\n *     无需专门针对File、InputStream等写多个重载方法，同时也为更好的扩展提供了可能。\n * </p>\n * <p>使用非常简单，假设我们需要从classpath中读取一个xml，我们不用关心这个文件在目录中还是在jar中：</p>\n * <pre>\n *     Resource resource = new ClassPathResource(\"test.xml\");\n *     String xmlStr = resource.readUtf8Str();\n * </pre>\n * <p>同样，我们可以自己实现Resource接口，按照业务需要从任意位置读取数据，比如从数据库中。</p>\n *\n * @author looly\n * @since 3.2.1\n */\npublic interface Resource {\n\n\t/**\n\t * 获取资源名，例如文件资源的资源名为文件名\n\t *\n\t * @return 资源名\n\t * @since 4.0.13\n\t */\n\tString getName();\n\n\t/**\n\t * 获得解析后的{@link URL}，无对应URL的返回{@code null}\n\t *\n\t * @return 解析后的{@link URL}\n\t */\n\tURL getUrl();\n\n\t/**\n\t * 获得 {@link InputStream}\n\t *\n\t * @return {@link InputStream}\n\t */\n\tInputStream getStream();\n\n\t/**\n\t * 检查资源是否变更<br>\n\t * 一般用于文件类资源，检查文件是否被修改过。\n\t *\n\t * @return 是否变更\n\t * @since 5.7.21\n\t */\n\tdefault boolean isModified(){\n\t\treturn false;\n\t}\n\n\t/**\n\t * 将资源内容写出到流，不关闭输出流，但是关闭资源流\n\t *\n\t * @param out 输出流\n\t * @throws IORuntimeException IO异常\n\t * @since 5.3.5\n\t */\n\tdefault void writeTo(OutputStream out) throws IORuntimeException {\n\t\ttry (InputStream in = getStream()) {\n\t\t\tIoUtil.copy(in, out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得Reader\n\t *\n\t * @param charset 编码\n\t * @return {@link BufferedReader}\n\t */\n\tdefault BufferedReader getReader(Charset charset) {\n\t\treturn IoUtil.getReader(getStream(), charset);\n\t}\n\n\t/**\n\t * 读取资源内容，读取完毕后会关闭流<br>\n\t * 关闭流并不影响下一次读取\n\t *\n\t * @param charset 编码\n\t * @return 读取资源内容\n\t * @throws IORuntimeException 包装{@link IOException}\n\t */\n\tdefault String readStr(Charset charset) throws IORuntimeException {\n\t\treturn IoUtil.read(getReader(charset));\n\t}\n\n\t/**\n\t * 读取资源内容，读取完毕后会关闭流<br>\n\t * 关闭流并不影响下一次读取\n\t *\n\t * @return 读取资源内容\n\t * @throws IORuntimeException 包装IOException\n\t */\n\tdefault String readUtf8Str() throws IORuntimeException {\n\t\treturn readStr(CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 读取资源内容，读取完毕后会关闭流<br>\n\t * 关闭流并不影响下一次读取\n\t *\n\t * @return 读取资源内容\n\t * @throws IORuntimeException 包装IOException\n\t */\n\tdefault byte[] readBytes() throws IORuntimeException {\n\t\treturn IoUtil.readBytes(getStream());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.collection.EnumerationIter;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.util.Enumeration;\nimport java.util.List;\n\n/**\n * Resource资源工具类\n *\n * @author Looly\n */\npublic class ResourceUtil {\n\n\t/**\n\t * 读取Classpath下的资源为字符串，使用UTF-8编码\n\t *\n\t * @param resource 资源路径，使用相对ClassPath的路径\n\t * @return 资源内容\n\t * @since 3.1.1\n\t */\n\tpublic static String readUtf8Str(String resource) {\n\t\treturn getResourceObj(resource).readUtf8Str();\n\t}\n\n\t/**\n\t * 读取Classpath下的资源为字符串\n\t *\n\t * @param resource 可以是绝对路径，也可以是相对路径（相对ClassPath）\n\t * @param charset  编码\n\t * @return 资源内容\n\t * @since 3.1.1\n\t */\n\tpublic static String readStr(String resource, Charset charset) {\n\t\treturn getResourceObj(resource).readStr(charset);\n\t}\n\n\t/**\n\t * 读取Classpath下的资源为byte[]\n\t *\n\t * @param resource 可以是绝对路径，也可以是相对路径（相对ClassPath）\n\t * @return 资源内容\n\t * @since 4.5.19\n\t */\n\tpublic static byte[] readBytes(String resource) {\n\t\treturn getResourceObj(resource).readBytes();\n\t}\n\n\t/**\n\t * 从ClassPath资源中获取{@link InputStream}\n\t *\n\t * @param resource ClassPath资源\n\t * @return {@link InputStream}\n\t * @throws NoResourceException 资源不存在异常\n\t * @since 3.1.2\n\t */\n\tpublic static InputStream getStream(String resource) throws NoResourceException {\n\t\treturn getResourceObj(resource).getStream();\n\t}\n\n\t/**\n\t * 从ClassPath资源中获取{@link InputStream}，当资源不存在时返回null\n\t *\n\t * @param resource ClassPath资源\n\t * @return {@link InputStream}\n\t * @since 4.0.3\n\t */\n\tpublic static InputStream getStreamSafe(String resource) {\n\t\ttry {\n\t\t\treturn getResourceObj(resource).getStream();\n\t\t} catch (NoResourceException e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 从ClassPath资源中获取{@link BufferedReader}\n\t *\n\t * @param resource ClassPath资源\n\t * @return {@link InputStream}\n\t * @since 5.3.6\n\t */\n\tpublic static BufferedReader getUtf8Reader(String resource) {\n\t\treturn getReader(resource, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 从ClassPath资源中获取{@link BufferedReader}\n\t *\n\t * @param resource ClassPath资源\n\t * @param charset  编码\n\t * @return {@link InputStream}\n\t * @since 3.1.2\n\t */\n\tpublic static BufferedReader getReader(String resource, Charset charset) {\n\t\treturn getResourceObj(resource).getReader(charset);\n\t}\n\n\t/**\n\t * 获得资源的URL<br>\n\t * 路径用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a/db.config\n\t * spring/xml/test.xml\n\t * </pre>\n\t *\n\t * @param resource 资源（相对Classpath的路径）\n\t * @return 资源URL\n\t */\n\tpublic static URL getResource(String resource) throws IORuntimeException {\n\t\treturn getResource(resource, null);\n\t}\n\n\t/**\n\t * 获取指定路径下的资源列表<br>\n\t * 路径格式必须为目录格式,用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a\n\t * spring/xml\n\t * </pre>\n\t *\n\t * @param resource 资源路径\n\t * @return 资源列表\n\t */\n\tpublic static List<URL> getResources(String resource) {\n\t\treturn getResources(resource, null);\n\t}\n\n\t/**\n\t * 获取指定路径下的资源列表<br>\n\t * 路径格式必须为目录格式,用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a\n\t * spring/xml\n\t * </pre>\n\t *\n\t * @param resource 资源路径\n\t * @param filter   过滤器，用于过滤不需要的资源，{@code null}表示不过滤，保留所有元素\n\t * @return 资源列表\n\t */\n\tpublic static List<URL> getResources(String resource, Filter<URL> filter) {\n\t\treturn IterUtil.filterToList(getResourceIter(resource), filter);\n\t}\n\n\t/**\n\t * 获取指定路径下的资源Iterator<br>\n\t * 路径格式必须为目录格式,用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a\n\t * spring/xml\n\t * </pre>\n\t *\n\t * @param resource 资源路径\n\t * @return 资源列表\n\t * @since 4.1.5\n\t */\n\tpublic static EnumerationIter<URL> getResourceIter(String resource) {\n\t\treturn getResourceIter(resource, null);\n\t}\n\n\t/**\n\t * 获取指定路径下的资源Iterator<br>\n\t * 路径格式必须为目录格式,用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a\n\t * spring/xml\n\t * </pre>\n\t *\n\t * @param resource 资源路径\n\t * @param classLoader {@link ClassLoader}\n\t * @return 资源列表\n\t * @since 4.1.5\n\t */\n\tpublic static EnumerationIter<URL> getResourceIter(String resource, ClassLoader classLoader) {\n\t\tfinal Enumeration<URL> resources;\n\t\ttry {\n\t\t\tresources = ObjUtil.defaultIfNull(classLoader, ClassLoaderUtil::getClassLoader).getResources(resource);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn new EnumerationIter<>(resources);\n\t}\n\n\t/**\n\t * 获得资源相对路径对应的URL\n\t *\n\t * @param resource  资源相对路径，{@code null}和\"\"都表示classpath根路径\n\t * @param baseClass 基准Class，获得的相对路径相对于此Class所在路径，如果为{@code null}则相对ClassPath\n\t * @return {@link URL}\n\t */\n\tpublic static URL getResource(String resource, Class<?> baseClass) {\n\t\tresource = StrUtil.nullToEmpty(resource);\n\t\treturn (null != baseClass) ? baseClass.getResource(resource) : ClassLoaderUtil.getClassLoader().getResource(resource);\n\t}\n\n\t/**\n\t * 获取{@link Resource} 资源对象<br>\n\t * 如果提供路径为绝对路径或路径以file:开头，返回{@link FileResource}，否则返回{@link ClassPathResource}\n\t *\n\t * @param path 路径，可以是绝对路径，也可以是相对路径（相对ClassPath）\n\t * @return {@link Resource} 资源对象\n\t * @since 3.2.1\n\t */\n\tpublic static Resource getResourceObj(String path) {\n\t\tif (StrUtil.isNotBlank(path)) {\n\t\t\tif (path.startsWith(URLUtil.FILE_URL_PREFIX) || FileUtil.isAbsolutePath(path)) {\n\t\t\t\treturn new FileResource(path);\n\t\t\t}\n\t\t}\n\t\treturn new ClassPathResource(path);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/StringResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * 字符串资源，字符串做为资源\n *\n * @author looly\n * @since 4.1.0\n * @see CharSequenceResource\n */\npublic class StringResource extends CharSequenceResource {\n\tprivate static final long serialVersionUID = 1L;\n\n\n\t/**\n\t * 构造，使用UTF8编码\n\t *\n\t * @param data 资源数据\n\t */\n\tpublic StringResource(String data) {\n\t\tsuper(data, null);\n\t}\n\n\t/**\n\t * 构造，使用UTF8编码\n\t *\n\t * @param data 资源数据\n\t * @param name 资源名称\n\t */\n\tpublic StringResource(String data, String name) {\n\t\tsuper(data, name, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param data 资源数据\n\t * @param name 资源名称\n\t * @param charset 编码\n\t */\n\tpublic StringResource(String data, String name, Charset charset) {\n\t\tsuper(data, name, charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/UrlResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.*;\nimport java.net.URI;\nimport java.net.URL;\n\n/**\n * URL资源访问类\n * @author Looly\n *\n */\npublic class UrlResource implements Resource, Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected URL url;\n\tprivate long lastModified = 0;\n\tprotected String name;\n\n\t//-------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t * @param uri URI\n\t * @since 5.7.21\n\t */\n\tpublic UrlResource(URI uri) {\n\t\tthis(URLUtil.url(uri), null);\n\t}\n\n\t/**\n\t * 构造\n\t * @param url URL\n\t */\n\tpublic UrlResource(URL url) {\n\t\tthis(url, null);\n\t}\n\n\t/**\n\t * 构造\n\t * @param url URL，允许为空\n\t * @param name 资源名称\n\t */\n\tpublic UrlResource(URL url, String name) {\n\t\tthis.url = url;\n\t\tif(null != url && URLUtil.URL_PROTOCOL_FILE.equals(url.getProtocol())){\n\t\t\tthis.lastModified = FileUtil.file(url).lastModified();\n\t\t}\n\t\tthis.name = ObjectUtil.defaultIfNull(name, () -> (null != url ? FileUtil.getName(url.getPath()) : null));\n\t}\n\n\t/**\n\t * 构造\n\t * @param file 文件路径\n\t * @deprecated Please use {@link FileResource}\n\t */\n\t@Deprecated\n\tpublic UrlResource(File file) {\n\t\tthis.url = URLUtil.getURL(file);\n\t}\n\t//-------------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t@Override\n\tpublic URL getUrl(){\n\t\treturn this.url;\n\t}\n\n\t@Override\n\tpublic InputStream getStream() throws NoResourceException{\n\t\tif(null == this.url){\n\t\t\tthrow new NoResourceException(\"Resource URL is null!\");\n\t\t}\n\t\treturn URLUtil.getStream(url);\n\t}\n\n\t@Override\n\tpublic boolean isModified() {\n\t\t// lastModified == 0表示此资源非文件资源\n\t\treturn (0 != this.lastModified) && this.lastModified != getFile().lastModified();\n\t}\n\n\t/**\n\t * 获得File\n\t * @return {@link File}\n\t */\n\tpublic File getFile(){\n\t\treturn FileUtil.file(this.url);\n\t}\n\n\t/**\n\t * 返回路径\n\t * @return 返回URL路径\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn (null == this.url) ? \"null\" : this.url.toString();\n\t}\n\n\t/**\n\t * 获取资源长度\n\t *\n\t * @return 资源长度\n\t * @since 5.8.21\n\t */\n\tpublic long size() {\n\t\treturn URLUtil.size(this.url);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/VfsResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ReflectUtil;\n\nimport java.io.InputStream;\nimport java.lang.reflect.Method;\nimport java.net.URL;\n\n/**\n * VFS资源封装<br>\n * 支持VFS 3.x on JBoss AS 6+，JBoss AS 7 and WildFly 8+<br>\n * 参考：org.springframework.core.io.VfsUtils\n *\n * @author looly, Spring\n * @since 5.7.21\n */\npublic class VfsResource implements Resource {\n\tprivate static final String VFS3_PKG = \"org.jboss.vfs.\";\n\n\tprivate static final Method VIRTUAL_FILE_METHOD_EXISTS;\n\tprivate static final Method VIRTUAL_FILE_METHOD_GET_INPUT_STREAM;\n\tprivate static final Method VIRTUAL_FILE_METHOD_GET_SIZE;\n\tprivate static final Method VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED;\n\tprivate static final Method VIRTUAL_FILE_METHOD_TO_URL;\n\tprivate static final Method VIRTUAL_FILE_METHOD_GET_NAME;\n\n\tstatic {\n\t\tClass<?> virtualFile = ClassLoaderUtil.loadClass(VFS3_PKG + \"VirtualFile\");\n\t\ttry {\n\t\t\tVIRTUAL_FILE_METHOD_EXISTS = virtualFile.getMethod(\"exists\");\n\t\t\tVIRTUAL_FILE_METHOD_GET_INPUT_STREAM = virtualFile.getMethod(\"openStream\");\n\t\t\tVIRTUAL_FILE_METHOD_GET_SIZE = virtualFile.getMethod(\"getSize\");\n\t\t\tVIRTUAL_FILE_METHOD_GET_LAST_MODIFIED = virtualFile.getMethod(\"getLastModified\");\n\t\t\tVIRTUAL_FILE_METHOD_TO_URL = virtualFile.getMethod(\"toURL\");\n\t\t\tVIRTUAL_FILE_METHOD_GET_NAME = virtualFile.getMethod(\"getName\");\n\t\t} catch (NoSuchMethodException ex) {\n\t\t\tthrow new IllegalStateException(\"Could not detect JBoss VFS infrastructure\", ex);\n\t\t}\n\t}\n\n\t/**\n\t * org.jboss.vfs.VirtualFile实例对象\n\t */\n\tprivate final Object virtualFile;\n\tprivate final long lastModified;\n\n\t/**\n\t * 构造\n\t *\n\t * @param resource org.jboss.vfs.VirtualFile实例对象\n\t */\n\tpublic VfsResource(Object resource) {\n\t\tAssert.notNull(resource, \"VirtualFile must not be null\");\n\t\tthis.virtualFile = resource;\n\t\tthis.lastModified = getLastModified();\n\t}\n\n\t/**\n\t * VFS文件是否存在\n\t *\n\t * @return 文件是否存在\n\t */\n\tpublic boolean exists() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_EXISTS);\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_NAME);\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_TO_URL);\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_INPUT_STREAM);\n\t}\n\n\t@Override\n\tpublic boolean isModified() {\n\t\treturn this.lastModified != getLastModified();\n\t}\n\n\t/**\n\t * 获得VFS文件最后修改时间\n\t *\n\t * @return 最后修改时间\n\t */\n\tpublic long getLastModified() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED);\n\t}\n\n\t/**\n\t * 获取VFS文件大小\n\t *\n\t * @return VFS文件大小\n\t */\n\tpublic long size() {\n\t\treturn ReflectUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_SIZE);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/WebAppResource.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport java.io.File;\n\nimport cn.hutool.core.io.FileUtil;\n\n/**\n * Web root资源访问对象\n *\n * @author looly\n * @since 4.1.11\n */\npublic class WebAppResource extends FileResource {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param path 相对于Web root的路径\n\t */\n\tpublic WebAppResource(String path) {\n\t\tsuper(new File(FileUtil.getWebRoot(), path));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/resource/package-info.java",
    "content": "/**\n * 针对ClassPath和文件中资源读取的封装，主要入口为工具类ResourceUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.resource;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/unit/DataSize.java",
    "content": "package cn.hutool.core.io.unit;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.math.BigDecimal;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 数据大小，可以将类似于'12MB'表示转换为bytes长度的数字\n * <p>\n * 此类来自于：Spring-framework\n *\n * <pre>\n *     byte        1B     1\n *     kilobyte    1KB    1,024\n *     megabyte    1MB    1,048,576\n *     gigabyte    1GB    1,073,741,824\n *     terabyte    1TB    1,099,511,627,776\n * </pre>\n *\n * @author Sam Brannen,Stephane Nicoll\n * @since 5.3.10\n */\npublic final class DataSize implements Comparable<DataSize> {\n\n\t/**\n\t * The pattern for parsing.\n\t */\n\tprivate static final Pattern PATTERN = Pattern.compile(\"^([+-]?\\\\d+(\\\\.\\\\d+)?)([a-zA-Z]{0,3})$\");\n\n\t/**\n\t * Bytes per Kilobyte(KB).\n\t */\n\tprivate static final long BYTES_PER_KB = 1024;\n\n\t/**\n\t * Bytes per Megabyte(MB).\n\t */\n\tprivate static final long BYTES_PER_MB = BYTES_PER_KB * 1024;\n\n\t/**\n\t * Bytes per Gigabyte(GB).\n\t */\n\tprivate static final long BYTES_PER_GB = BYTES_PER_MB * 1024;\n\n\t/**\n\t * Bytes per Terabyte(TB).\n\t */\n\tprivate static final long BYTES_PER_TB = BYTES_PER_GB * 1024;\n\n\n\t/**\n\t * bytes长度\n\t */\n\tprivate final long bytes;\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param bytes 长度\n\t */\n\tprivate DataSize(long bytes) {\n\t\tthis.bytes = bytes;\n\t}\n\n\n\t/**\n\t * 获得对应bytes的DataSize\n\t *\n\t * @param bytes bytes大小，可正可负\n\t * @return this\n\t */\n\tpublic static DataSize ofBytes(long bytes) {\n\t\treturn new DataSize(bytes);\n\t}\n\n\t/**\n\t * 获得对应kilobytes的DataSize\n\t *\n\t * @param kilobytes kilobytes大小，可正可负\n\t * @return a DataSize\n\t */\n\tpublic static DataSize ofKilobytes(long kilobytes) {\n\t\treturn new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB));\n\t}\n\n\t/**\n\t * 获得对应megabytes的DataSize\n\t *\n\t * @param megabytes megabytes大小，可正可负\n\t * @return a DataSize\n\t */\n\tpublic static DataSize ofMegabytes(long megabytes) {\n\t\treturn new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB));\n\t}\n\n\t/**\n\t * 获得对应gigabytes的DataSize\n\t *\n\t * @param gigabytes gigabytes大小，可正可负\n\t * @return a DataSize\n\t */\n\tpublic static DataSize ofGigabytes(long gigabytes) {\n\t\treturn new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB));\n\t}\n\n\t/**\n\t * 获得对应terabytes的DataSize\n\t *\n\t * @param terabytes terabytes大小，可正可负\n\t * @return a DataSize\n\t */\n\tpublic static DataSize ofTerabytes(long terabytes) {\n\t\treturn new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB));\n\t}\n\n\t/**\n\t * 获得指定{@link DataUnit}对应的DataSize\n\t *\n\t * @param amount 大小\n\t * @param unit 数据大小单位，null表示默认的BYTES\n\t * @return DataSize\n\t */\n\tpublic static DataSize of(long amount, DataUnit unit) {\n\t\tif(null == unit){\n\t\t\tunit = DataUnit.BYTES;\n\t\t}\n\t\treturn new DataSize(Math.multiplyExact(amount, unit.size().toBytes()));\n\t}\n\n\t/**\n\t * 获得指定{@link DataUnit}对应的DataSize\n\t *\n\t * @param amount 大小\n\t * @param unit 数据大小单位，null表示默认的BYTES\n\t * @return DataSize\n\t * @since 5.4.5\n\t */\n\tpublic static DataSize of(BigDecimal amount, DataUnit unit) {\n\t\tif(null == unit){\n\t\t\tunit = DataUnit.BYTES;\n\t\t}\n\t\treturn new DataSize(amount.multiply(new BigDecimal(unit.size().toBytes())).longValue());\n\t}\n\n\t/**\n\t * 获取指定数据大小文本对应的DataSize对象，如果无单位指定，默认获取{@link DataUnit#BYTES}\n\t * <p>\n\t * 例如：\n\t * <pre>\n\t * \"12KB\" -- parses as \"12 kilobytes\"\n\t * \"5MB\"  -- parses as \"5 megabytes\"\n\t * \"20\"   -- parses as \"20 bytes\"\n\t * </pre>\n\t *\n\t * @param text the text to parse\n\t * @return the parsed DataSize\n\t * @see #parse(CharSequence, DataUnit)\n\t */\n\tpublic static DataSize parse(CharSequence text) {\n\t\treturn parse(text, null);\n\t}\n\n\t/**\n\t * Obtain a DataSize from a text string such as {@code 12MB} using\n\t * the specified default {@link DataUnit} if no unit is specified.\n\t * <p>\n\t * The string starts with a number followed optionally by a unit matching one of the\n\t * supported {@linkplain DataUnit suffixes}.\n\t * <p>\n\t * Examples:\n\t * <pre>\n\t * \"12KB\" -- parses as \"12 kilobytes\"\n\t * \"5MB\"  -- parses as \"5 megabytes\"\n\t * \"20\"   -- parses as \"20 kilobytes\" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})\n\t * </pre>\n\t *\n\t * @param text the text to parse\n\t * @param defaultUnit 默认的数据单位\n\t * @return the parsed DataSize\n\t */\n\tpublic static DataSize parse(CharSequence text, DataUnit defaultUnit) {\n\t\tAssert.notNull(text, \"Text must not be null\");\n\t\ttry {\n\t\t\tfinal Matcher matcher = PATTERN.matcher(StrUtil.cleanBlank(text));\n\t\t\tAssert.state(matcher.matches(), \"Does not match data size pattern\");\n\n\t\t\tfinal DataUnit unit = determineDataUnit(matcher.group(3), defaultUnit);\n\t\t\treturn DataSize.of(new BigDecimal(matcher.group(1)), unit);\n\t\t} catch (Exception ex) {\n\t\t\tthrow new IllegalArgumentException(\"'\" + text + \"' is not a valid data size\", ex);\n\t\t}\n\t}\n\n\t/**\n\t * 决定数据单位，后缀不识别时使用默认单位\n\t * @param suffix 后缀\n\t * @param defaultUnit 默认单位\n\t * @return {@link DataUnit}\n\t */\n\tprivate static DataUnit determineDataUnit(String suffix, DataUnit defaultUnit) {\n\t\tDataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES);\n\t\treturn (StrUtil.isNotEmpty(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse);\n\t}\n\n\t/**\n\t * 是否为负数，不包括0\n\t *\n\t * @return 负数返回true，否则false\n\t */\n\tpublic boolean isNegative() {\n\t\treturn this.bytes < 0;\n\t}\n\n\t/**\n\t * 返回bytes大小\n\t *\n\t * @return bytes大小\n\t */\n\tpublic long toBytes() {\n\t\treturn this.bytes;\n\t}\n\n\t/**\n\t * 返回KB大小\n\t *\n\t * @return KB大小\n\t */\n\tpublic long toKilobytes() {\n\t\treturn this.bytes / BYTES_PER_KB;\n\t}\n\n\t/**\n\t * 返回MB大小\n\t *\n\t * @return MB大小\n\t */\n\tpublic long toMegabytes() {\n\t\treturn this.bytes / BYTES_PER_MB;\n\t}\n\n\t/**\n\t * 返回GB大小\n\t *\n\t * @return GB大小\n\t *\n\t */\n\tpublic long toGigabytes() {\n\t\treturn this.bytes / BYTES_PER_GB;\n\t}\n\n\t/**\n\t * 返回TB大小\n\t *\n\t * @return TB大小\n\t */\n\tpublic long toTerabytes() {\n\t\treturn this.bytes / BYTES_PER_TB;\n\t}\n\n\t@Override\n\tpublic int compareTo(DataSize other) {\n\t\treturn Long.compare(this.bytes, other.bytes);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn String.format(\"%dB\", this.bytes);\n\t}\n\n\n\t@Override\n\tpublic boolean equals(Object other) {\n\t\tif (this == other) {\n\t\t\treturn true;\n\t\t}\n\t\tif (other == null || getClass() != other.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tDataSize otherSize = (DataSize) other;\n\t\treturn (this.bytes == otherSize.bytes);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Long.hashCode(this.bytes);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/unit/DataSizeUtil.java",
    "content": "package cn.hutool.core.io.unit;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.text.DecimalFormat;\n\n/**\n * 数据大小工具类\n *\n * @author looly\n * @since 5.3.10\n */\npublic class DataSizeUtil {\n\n\t/**\n\t * 解析数据大小字符串，转换为bytes大小\n\t *\n\t * @param text 数据大小字符串，类似于：12KB, 5MB等\n\t * @return bytes大小\n\t */\n\tpublic static long parse(String text) {\n\t\treturn DataSize.parse(text).toBytes();\n\t}\n\n\t/**\n\t * 可读的文件大小<br>\n\t * 参考 http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc\n\t *\n\t * @param size Long类型大小\n\t * @return 大小\n\t */\n\tpublic static String format(long size) {\n\t\tif (size <= 0) {\n\t\t\treturn \"0\";\n\t\t}\n\t\tint digitGroups = Math.min(DataUnit.UNIT_NAMES.length-1, (int) (Math.log10(size) / Math.log10(1024)));\n\t\treturn new DecimalFormat(\"#,##0.##\")\n\t\t\t\t.format(size / Math.pow(1024, digitGroups)) + \" \" + DataUnit.UNIT_NAMES[digitGroups];\n\t}\n\n\t/**\n\t * 根据单位，将文件大小转换为对应单位的大小\n\t *\n\t * @param size 文件大小\n\t * @param fileDataUnit 单位\n\t * @return 大小\n\t * @since 5.8.34\n\t */\n\tpublic static String format(Long size, DataUnit fileDataUnit){\n\t\tif (size <= 0) {\n\t\t\treturn \"0\";\n\t\t}\n\t\tint digitGroups = ArrayUtil.indexOf(DataUnit.UNIT_NAMES,fileDataUnit.getSuffix());\n\t\treturn new DecimalFormat(\"##0.##\").format(size / Math.pow(1024, digitGroups)) + \" \" + DataUnit.UNIT_NAMES[digitGroups];\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/unit/DataUnit.java",
    "content": "package cn.hutool.core.io.unit;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 数据单位封装<p>\n * 此类来自于：Spring-framework\n *\n * <pre>\n *     BYTES      1B      2^0     1\n *     KILOBYTES  1KB     2^10    1,024\n *     MEGABYTES  1MB     2^20    1,048,576\n *     GIGABYTES  1GB     2^30    1,073,741,824\n *     TERABYTES  1TB     2^40    1,099,511,627,776\n * </pre>\n *\n * @author Sam Brannen，Stephane Nicoll\n * @since 5.3.10\n */\npublic enum DataUnit {\n\n\t/**\n\t * Bytes, 后缀表示为： {@code B}.\n\t */\n\tBYTES(\"B\", DataSize.ofBytes(1)),\n\n\t/**\n\t * Kilobytes, 后缀表示为： {@code KB}.\n\t */\n\tKILOBYTES(\"KB\", DataSize.ofKilobytes(1)),\n\n\t/**\n\t * Megabytes, 后缀表示为： {@code MB}.\n\t */\n\tMEGABYTES(\"MB\", DataSize.ofMegabytes(1)),\n\n\t/**\n\t * Gigabytes, 后缀表示为： {@code GB}.\n\t */\n\tGIGABYTES(\"GB\", DataSize.ofGigabytes(1)),\n\n\t/**\n\t * Terabytes, 后缀表示为： {@code TB}.\n\t */\n\tTERABYTES(\"TB\", DataSize.ofTerabytes(1));\n\n\t/**\n\t * 单位后缀\n\t */\n\tpublic static final String[] UNIT_NAMES = new String[]{\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\"};\n\n\tprivate final String suffix;\n\tprivate final DataSize size;\n\n\n\tDataUnit(String suffix, DataSize size) {\n\t\tthis.suffix = suffix;\n\t\tthis.size = size;\n\t}\n\n\t/**\n\t * 单位后缀\n\t *\n\t * @return 单位后缀\n\t * @since 5.8.34\n\t */\n\tpublic String getSuffix() {\n\t\treturn this.suffix;\n\t}\n\n\tDataSize size() {\n\t\treturn this.size;\n\t}\n\n\t/**\n\t * 通过后缀返回对应的 DataUnit\n\t *\n\t * @param suffix 单位后缀，如KB、GB、GiB等\n\t * @return 匹配到的{@code DataUnit}\n\t * @throws IllegalArgumentException 后缀无法识别报错\n\t */\n\tpublic static DataUnit fromSuffix(String suffix) {\n\t\t// issue#ICXXVF 兼容KiB、MiB、GiB\n\t\tif(StrUtil.length(suffix) == 3 && CharUtil.equals(suffix.charAt(1), 'i', true)){\n\t\t\tsuffix = new String(new char[]{suffix.charAt(0), suffix.charAt(2)});\n\t\t}\n\n\t\tfor (DataUnit candidate : values()) {\n\t\t\t// 支持类似于 3MB，3M，3m等写法\n\t\t\tif (StrUtil.startWithIgnoreCase(candidate.suffix, suffix)) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t}\n\t\tthrow new IllegalArgumentException(\"Unknown data unit suffix '\" + suffix + \"'\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/unit/package-info.java",
    "content": "/**\n * 数据单位相关封装，包括DataUnit数据单位和DataSize数据大小\n *\n * @author looly\n * @since 5.3.10\n */\npackage cn.hutool.core.io.unit;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport cn.hutool.core.io.watch.watchers.IgnoreWatcher;\n\n/**\n * 空白WatchListener<br>\n * 用户继承此类后实现需要监听的方法\n * @author Looly\n *\n */\npublic class SimpleWatcher extends IgnoreWatcher{\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchAction.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\n\n/**\n * 监听事件处理函数接口\n *\n * @author looly\n * @since 5.4.0\n */\n@FunctionalInterface\npublic interface WatchAction {\n\n\t/**\n\t * 事件处理，通过实现此方法处理各种事件。\n\t *\n\t * 事件可以调用 {@link WatchEvent#kind()}获取，对应事件见{@link WatchKind}\n\t *\n\t * @param event       事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tvoid doAction(WatchEvent<?> event, Path currentPath);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchException.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 监听异常\n * @author Looly\n *\n */\npublic class WatchException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8068509879445395353L;\n\n\tpublic WatchException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic WatchException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic WatchException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic WatchException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic WatchException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchKind.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport java.nio.file.StandardWatchEventKinds;\nimport java.nio.file.WatchEvent;\n\n/**\n * 监听事件类型枚举，包括：\n *\n * <pre>\n *      1. 事件丢失 OVERFLOW -》StandardWatchEventKinds.OVERFLOW\n *      2. 修改事件 MODIFY   -》StandardWatchEventKinds.ENTRY_MODIFY\n *      3. 创建事件 CREATE   -》StandardWatchEventKinds.ENTRY_CREATE\n *      4. 删除事件 DELETE   -》StandardWatchEventKinds.ENTRY_DELETE\n * </pre>\n *\n * @author loolly\n * @since 5.1.0\n */\npublic enum WatchKind {\n\n\t/**\n\t * 事件丢失\n\t */\n\tOVERFLOW(StandardWatchEventKinds.OVERFLOW),\n\t/**\n\t * 修改事件\n\t */\n\tMODIFY(StandardWatchEventKinds.ENTRY_MODIFY),\n\t/**\n\t * 创建事件\n\t */\n\tCREATE(StandardWatchEventKinds.ENTRY_CREATE),\n\t/**\n\t * 删除事件\n\t */\n\tDELETE(StandardWatchEventKinds.ENTRY_DELETE);\n\n\t/**\n\t * 全部事件\n\t */\n\tpublic static final WatchEvent.Kind<?>[] ALL = {//\n\t\t\tOVERFLOW.getValue(),      //事件丢失\n\t\t\tMODIFY.getValue(), //修改\n\t\t\tCREATE.getValue(),  //创建\n\t\t\tDELETE.getValue()   //删除\n\t};\n\n\tprivate final WatchEvent.Kind<?> value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 事件类型\n\t */\n\tWatchKind(WatchEvent.Kind<?> value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取枚举对应的事件类型\n\t *\n\t * @return 事件类型值\n\t */\n\tpublic WatchEvent.Kind<?> getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.watch.watchers.WatcherChain;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.WatchEvent;\nimport java.nio.file.WatchService;\n\n/**\n * 路径监听器\n *\n * <p>\n * 监听器可监听目录或文件<br>\n * 如果监听的Path不存在，则递归创建空目录然后监听此空目录<br>\n * 递归监听目录时，并不会监听新创建的目录\n *\n * @author Looly\n */\npublic class WatchMonitor extends WatchServer {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 事件丢失\n\t */\n\tpublic static final WatchEvent.Kind<?> OVERFLOW = WatchKind.OVERFLOW.getValue();\n\t/**\n\t * 修改事件\n\t */\n\tpublic static final WatchEvent.Kind<?> ENTRY_MODIFY = WatchKind.MODIFY.getValue();\n\t/**\n\t * 创建事件\n\t */\n\tpublic static final WatchEvent.Kind<?> ENTRY_CREATE = WatchKind.CREATE.getValue();\n\t/**\n\t * 删除事件\n\t */\n\tpublic static final WatchEvent.Kind<?> ENTRY_DELETE = WatchKind.DELETE.getValue();\n\t/**\n\t * 全部事件\n\t */\n\tpublic static final WatchEvent.Kind<?>[] EVENTS_ALL = WatchKind.ALL;\n\n\t/**\n\t * 监听路径，必须为目录\n\t */\n\tprivate Path path;\n\t/**\n\t * 递归目录的最大深度，当小于1时不递归下层目录\n\t */\n\tprivate int maxDepth;\n\t/**\n\t * 监听的文件，对于单文件监听不为空\n\t */\n\tprivate Path filePath;\n\n\t/**\n\t * 监听器\n\t */\n\tprivate Watcher watcher;\n\t//------------------------------------------------------ Static method start\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param url    URL\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URL url, WatchEvent.Kind<?>... events) {\n\t\treturn create(url, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param url      URL\n\t * @param events   监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URL url, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(URLUtil.toURI(url), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param uri    URI\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URI uri, WatchEvent.Kind<?>... events) {\n\t\treturn create(uri, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param uri      URI\n\t * @param events   监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URI uri, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(Paths.get(uri), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param file   文件\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(File file, WatchEvent.Kind<?>... events) {\n\t\treturn create(file, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param file     文件\n\t * @param events   监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(File file, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(file.toPath(), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path   路径\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(String path, WatchEvent.Kind<?>... events) {\n\t\treturn create(path, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path     路径\n\t * @param events   监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(String path, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(Paths.get(path), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path   路径\n\t * @param events 监听事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(Path path, WatchEvent.Kind<?>... events) {\n\t\treturn create(path, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path     路径\n\t * @param events   监听事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(Path path, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn new WatchMonitor(path, maxDepth, events);\n\t}\n\n\t//--------- createAll\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param uri     URI\n\t * @param watcher {@link Watcher}\n\t * @return WatchMonitor\n\t */\n\tpublic static WatchMonitor createAll(URI uri, Watcher watcher) {\n\t\treturn createAll(Paths.get(uri), watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param url     URL\n\t * @param watcher {@link Watcher}\n\t * @return WatchMonitor\n\t */\n\tpublic static WatchMonitor createAll(URL url, Watcher watcher) {\n\t\ttry {\n\t\t\treturn createAll(Paths.get(url.toURI()), watcher);\n\t\t} catch (URISyntaxException e) {\n\t\t\tthrow new WatchException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param file    被监听文件\n\t * @param watcher {@link Watcher}\n\t * @return WatchMonitor\n\t */\n\tpublic static WatchMonitor createAll(File file, Watcher watcher) {\n\t\treturn createAll(file.toPath(), watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path    路径\n\t * @param watcher {@link Watcher}\n\t * @return WatchMonitor\n\t */\n\tpublic static WatchMonitor createAll(String path, Watcher watcher) {\n\t\treturn createAll(Paths.get(path), watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path    路径\n\t * @param watcher {@link Watcher}\n\t * @return WatchMonitor\n\t */\n\tpublic static WatchMonitor createAll(Path path, Watcher watcher) {\n\t\tfinal WatchMonitor watchMonitor = create(path, EVENTS_ALL);\n\t\twatchMonitor.setWatcher(watcher);\n\t\treturn watchMonitor;\n\t}\n\t//------------------------------------------------------ Static method end\n\n\t//------------------------------------------------------ Constructor method start\n\n\t/**\n\t * 构造\n\t *\n\t * @param file   文件\n\t * @param events 监听的事件列表\n\t */\n\tpublic WatchMonitor(File file, WatchEvent.Kind<?>... events) {\n\t\tthis(file.toPath(), events);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path   字符串路径\n\t * @param events 监听的事件列表\n\t */\n\tpublic WatchMonitor(String path, WatchEvent.Kind<?>... events) {\n\t\tthis(Paths.get(path), events);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path   字符串路径\n\t * @param events 监听事件列表\n\t */\n\tpublic WatchMonitor(Path path, WatchEvent.Kind<?>... events) {\n\t\tthis(path, 0, events);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 例如设置：\n\t * <pre>\n\t * maxDepth &lt;= 1 表示只监听当前目录\n\t * maxDepth = 2 表示监听当前目录以及下层目录\n\t * maxDepth = 3 表示监听当前目录以及下两层\n\t * </pre>\n\t *\n\t * @param path     字符串路径\n\t * @param maxDepth 递归目录的最大深度，当小于2时不递归下层目录\n\t * @param events   监听事件列表\n\t */\n\tpublic WatchMonitor(Path path, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\tthis.path = path;\n\t\tthis.maxDepth = maxDepth;\n\t\tthis.events = events;\n\t\tthis.init();\n\t}\n\t//------------------------------------------------------ Constructor method end\n\n\t/**\n\t * 初始化<br>\n\t * 初始化包括：\n\t * <pre>\n\t * 1、解析传入的路径，判断其为目录还是文件\n\t * 2、创建{@link WatchService} 对象\n\t * </pre>\n\t *\n\t * @throws WatchException 监听异常，IO异常时抛出此异常\n\t */\n\t@Override\n\tpublic void init() throws WatchException {\n\t\t//获取目录或文件路径\n\t\tif (false == Files.exists(this.path, LinkOption.NOFOLLOW_LINKS)) {\n\t\t\t// 不存在的路径\n\t\t\tfinal Path lastPathEle = FileUtil.getLastPathEle(this.path);\n\t\t\tif (null != lastPathEle) {\n\t\t\t\tfinal String lastPathEleStr = lastPathEle.toString();\n\t\t\t\t//带有点表示有扩展名，按照未创建的文件对待。Linux下.d的为目录，排除之\n\t\t\t\tif (StrUtil.contains(lastPathEleStr, StrUtil.C_DOT) && false == StrUtil.endWithIgnoreCase(lastPathEleStr, \".d\")) {\n\t\t\t\t\tthis.filePath = this.path;\n\t\t\t\t\tthis.path = this.filePath.getParent();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t//创建不存在的目录或父目录\n\t\t\ttry {\n\t\t\t\tFiles.createDirectories(this.path);\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t} else if (Files.isRegularFile(this.path, LinkOption.NOFOLLOW_LINKS)) {\n\t\t\t// 文件路径\n\t\t\tthis.filePath = this.path;\n\t\t\tthis.path = this.filePath.getParent();\n\t\t}\n\n\t\tsuper.init();\n\t}\n\n\t/**\n\t * 设置监听<br>\n\t * 多个监听请使用{@link WatcherChain}\n\t *\n\t * @param watcher 监听\n\t * @return WatchMonitor\n\t */\n\tpublic WatchMonitor setWatcher(Watcher watcher) {\n\t\tthis.watcher = watcher;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\twatch();\n\t}\n\n\t/**\n\t * 开始监听事件，阻塞当前进程\n\t */\n\tpublic void watch() {\n\t\twatch(this.watcher);\n\t}\n\n\t/**\n\t * 开始监听事件，阻塞当前进程\n\t *\n\t * @param watcher 监听\n\t * @throws WatchException 监听异常，如果监听关闭抛出此异常\n\t */\n\tpublic void watch(Watcher watcher) throws WatchException {\n\t\tif (isClosed) {\n\t\t\tthrow new WatchException(\"Watch Monitor is closed !\");\n\t\t}\n\n\t\t// 按照层级注册路径及其子路径\n\t\tregisterPath();\n//\t\tlog.debug(\"Start watching path: [{}]\", this.path);\n\n\t\twhile (false == isClosed) {\n\t\t\tdoTakeAndWatch(watcher);\n\t\t}\n\t}\n\n\t/**\n\t * 当监听目录时，监听目录的最大深度<br>\n\t * 当设置值为1（或小于1）时，表示不递归监听子目录<br>\n\t * 例如设置：\n\t * <pre>\n\t * maxDepth &lt;= 1 表示只监听当前目录\n\t * maxDepth = 2 表示监听当前目录以及下层目录\n\t * maxDepth = 3 表示监听当前目录以及下层\n\t * </pre>\n\t *\n\t * @param maxDepth 最大深度，当设置值为1（或小于1）时，表示不递归监听子目录，监听所有子目录请传{@link Integer#MAX_VALUE}\n\t * @return this\n\t */\n\tpublic WatchMonitor setMaxDepth(int maxDepth) {\n\t\tthis.maxDepth = maxDepth;\n\t\treturn this;\n\t}\n\n\t//------------------------------------------------------ private method start\n\n\t/**\n\t * 执行事件获取并处理\n\t *\n\t * @param watcher {@link Watcher}\n\t */\n\tprivate void doTakeAndWatch(Watcher watcher) {\n\t\tsuper.watch(watcher, watchEvent -> null == filePath || filePath.endsWith(watchEvent.context().toString()));\n\t}\n\n\t/**\n\t * 注册监听路径\n\t */\n\tprivate void registerPath() {\n\t\tregisterPath(this.path, (null != this.filePath) ? 0 : this.maxDepth);\n\t}\n\t//------------------------------------------------------ private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchServer.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.nio.file.AccessDeniedException;\nimport java.nio.file.ClosedWatchServiceException;\nimport java.nio.file.FileSystems;\nimport java.nio.file.FileVisitOption;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.WatchEvent;\nimport java.nio.file.WatchKey;\nimport java.nio.file.WatchService;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 文件监听服务，此服务可以同时监听多个路径。\n *\n * @author loolly\n * @since 5.1.0\n */\npublic class WatchServer extends Thread implements Closeable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 监听服务\n\t */\n\tprivate WatchService watchService;\n\t/**\n\t * 监听事件列表\n\t */\n\tprotected WatchEvent.Kind<?>[] events;\n\t/**\n\t * 监听选项，例如监听频率等\n\t */\n\tprivate WatchEvent.Modifier[] modifiers;\n\t/**\n\t * 监听是否已经关闭\n\t */\n\tprotected boolean isClosed;\n\t/**\n\t * WatchKey 和 Path的对应表\n\t */\n\tprivate final Map<WatchKey, Path> watchKeyPathMap = new HashMap<>();\n\n\t/**\n\t * 初始化<br>\n\t * 初始化包括：\n\t * <pre>\n\t * 1、解析传入的路径，判断其为目录还是文件\n\t * 2、创建{@link WatchService} 对象\n\t * </pre>\n\t *\n\t * @throws WatchException 监听异常，IO异常时抛出此异常\n\t */\n\tpublic void init() throws WatchException {\n\t\t//初始化监听\n\t\ttry {\n\t\t\twatchService = FileSystems.getDefault().newWatchService();\n\t\t} catch (IOException e) {\n\t\t\tthrow new WatchException(e);\n\t\t}\n\n\t\tisClosed = false;\n\t}\n\n\t/**\n\t * 设置监听选项，例如监听频率等，可设置项包括：\n\t *\n\t * <pre>\n\t * 1、com.sun.nio.file.StandardWatchEventKinds\n\t * 2、com.sun.nio.file.SensitivityWatchEventModifier\n\t * </pre>\n\t *\n\t * @param modifiers 监听选项，例如监听频率等\n\t */\n\tpublic void setModifiers(WatchEvent.Modifier[] modifiers) {\n\t\tthis.modifiers = modifiers;\n\t}\n\n\t/**\n\t * 将指定路径加入到监听中\n\t *\n\t * @param path     路径\n\t * @param maxDepth 递归下层目录的最大深度\n\t */\n\tpublic void registerPath(Path path, int maxDepth) {\n\t\tfinal WatchEvent.Kind<?>[] kinds = ArrayUtil.defaultIfEmpty(this.events, WatchKind.ALL);\n\n\t\ttry {\n\t\t\tfinal WatchKey key;\n\t\t\tif (ArrayUtil.isEmpty(this.modifiers)) {\n\t\t\t\tkey = path.register(this.watchService, kinds);\n\t\t\t} else {\n\t\t\t\tkey = path.register(this.watchService, kinds, this.modifiers);\n\t\t\t}\n\t\t\twatchKeyPathMap.put(key, path);\n\n\t\t\t// 递归注册下一层层级的目录\n\t\t\tif (maxDepth > 1) {\n\t\t\t\t//遍历所有子目录并加入监听\n\t\t\t\tFiles.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), maxDepth, new SimpleFileVisitor<Path>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {\n\t\t\t\t\t\tregisterPath(dir, 0);//继续添加目录\n\t\t\t\t\t\treturn super.postVisitDirectory(dir, exc);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tif (false == (e instanceof AccessDeniedException)) {\n\t\t\t\tthrow new WatchException(e);\n\t\t\t}\n\n\t\t\t//对于禁止访问的目录，跳过监听\n\t\t}\n\t}\n\n\t/**\n\t * 执行事件获取并处理\n\t *\n\t * @param action      监听回调函数，实现此函数接口用于处理WatchEvent事件\n\t * @param watchFilter 监听过滤接口，通过实现此接口过滤掉不需要监听的情况，null表示不过滤\n\t * @since 5.4.0\n\t */\n\tpublic void watch(WatchAction action, Filter<WatchEvent<?>> watchFilter) {\n\t\tWatchKey wk;\n\t\ttry {\n\t\t\twk = watchService.take();\n\t\t} catch (InterruptedException | ClosedWatchServiceException e) {\n\t\t\t// 用户中断\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\tfinal Path currentPath = watchKeyPathMap.get(wk);\n\n\t\tfor (WatchEvent<?> event : wk.pollEvents()) {\n\t\t\t// 如果监听文件，检查当前事件是否与所监听文件关联\n\t\t\tif (null != watchFilter && false == watchFilter.accept(event)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\taction.doAction(event, currentPath);\n\t\t}\n\n\t\twk.reset();\n\t}\n\n\t/**\n\t * 执行事件获取并处理\n\t *\n\t * @param watcher     {@link Watcher}\n\t * @param watchFilter 监听过滤接口，通过实现此接口过滤掉不需要监听的情况，null表示不过滤\n\t */\n\tpublic void watch(Watcher watcher, Filter<WatchEvent<?>> watchFilter) {\n\t\twatch((event, currentPath) -> {\n\t\t\tfinal WatchEvent.Kind<?> kind = event.kind();\n\n\t\t\tif (kind == WatchKind.CREATE.getValue()) {\n\t\t\t\twatcher.onCreate(event, currentPath);\n\t\t\t} else if (kind == WatchKind.MODIFY.getValue()) {\n\t\t\t\twatcher.onModify(event, currentPath);\n\t\t\t} else if (kind == WatchKind.DELETE.getValue()) {\n\t\t\t\twatcher.onDelete(event, currentPath);\n\t\t\t} else if (kind == WatchKind.OVERFLOW.getValue()) {\n\t\t\t\twatcher.onOverflow(event, currentPath);\n\t\t\t}\n\t\t}, watchFilter);\n\t}\n\n\t/**\n\t * 通过 path 获取 watchKey\n\t *\n\t * @param path path\n\t * @return 如果不存在则返回 null\n\t */\n\tpublic WatchKey getWatchKey(Path path) {\n\t\tfor (Map.Entry<WatchKey, Path> entry : watchKeyPathMap.entrySet()) {\n\t\t\tif (ObjectUtil.equals(path, entry.getValue())) {\n\t\t\t\treturn entry.getKey();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 关闭监听\n\t */\n\t@Override\n\tpublic void close() {\n\t\tisClosed = true;\n\t\tIoUtil.close(watchService);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/WatchUtil.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.*;\n\n/**\n * 监听工具类<br>\n * 主要负责文件监听器的快捷创建\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class WatchUtil {\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param url URL\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URL url, WatchEvent.Kind<?>... events) {\n\t\treturn create(url, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param url URL\n\t * @param events 监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URL url, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(URLUtil.toURI(url), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param uri URI\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URI uri, WatchEvent.Kind<?>... events) {\n\t\treturn create(uri, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param uri URI\n\t * @param events 监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(URI uri, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(Paths.get(uri), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param file 文件\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(File file, WatchEvent.Kind<?>... events) {\n\t\treturn create(file, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param file 文件\n\t * @param events 监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(File file, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(file.toPath(), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path 路径\n\t * @param events 监听的事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(String path, WatchEvent.Kind<?>... events) {\n\t\treturn create(path, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path 路径\n\t * @param events 监听的事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(String path, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn create(Paths.get(path), maxDepth, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path 路径\n\t * @param events 监听事件列表\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(Path path, WatchEvent.Kind<?>... events) {\n\t\treturn create(path, 0, events);\n\t}\n\n\t/**\n\t * 创建并初始化监听\n\t *\n\t * @param path 路径\n\t * @param events 监听事件列表\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @return 监听对象\n\t */\n\tpublic static WatchMonitor create(Path path, int maxDepth, WatchEvent.Kind<?>... events) {\n\t\treturn new WatchMonitor(path, maxDepth, events);\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------- createAll\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param url URL\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(URL url, Watcher watcher) {\n\t\treturn createAll(url, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param url URL\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(URL url, int maxDepth, Watcher watcher) {\n\t\treturn createAll(URLUtil.toURI(url), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param uri URI\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(URI uri, Watcher watcher) {\n\t\treturn createAll(uri, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param uri URI\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(URI uri, int maxDepth, Watcher watcher) {\n\t\treturn createAll(Paths.get(uri), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param file 被监听文件\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(File file, Watcher watcher) {\n\t\treturn createAll(file, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param file 被监听文件\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(File file, int maxDepth, Watcher watcher) {\n\t\treturn createAll(file.toPath(), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path 路径\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(String path, Watcher watcher) {\n\t\treturn createAll(path, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path 路径\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(String path, int maxDepth, Watcher watcher) {\n\t\treturn createAll(Paths.get(path), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path 路径\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(Path path, Watcher watcher) {\n\t\treturn createAll(path, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听所有事件\n\t *\n\t * @param path 路径\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t */\n\tpublic static WatchMonitor createAll(Path path, int maxDepth, Watcher watcher) {\n\t\tfinal WatchMonitor watchMonitor = create(path, maxDepth, WatchMonitor.EVENTS_ALL);\n\t\twatchMonitor.setWatcher(watcher);\n\t\treturn watchMonitor;\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------- createModify\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param url URL\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(URL url, Watcher watcher) {\n\t\treturn createModify(url, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param url URL\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(URL url, int maxDepth, Watcher watcher) {\n\t\treturn createModify(URLUtil.toURI(url), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param uri URI\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(URI uri, Watcher watcher) {\n\t\treturn createModify(uri, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param uri URI\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(URI uri, int maxDepth, Watcher watcher) {\n\t\treturn createModify(Paths.get(uri), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param file 被监听文件\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(File file, Watcher watcher) {\n\t\treturn createModify(file, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param file 被监听文件\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(File file, int maxDepth, Watcher watcher) {\n\t\treturn createModify(file.toPath(), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param path 路径\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(String path, Watcher watcher) {\n\t\treturn createModify(path, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param path 路径\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(String path, int maxDepth, Watcher watcher) {\n\t\treturn createModify(Paths.get(path), maxDepth, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param path 路径\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(Path path, Watcher watcher) {\n\t\treturn createModify(path, 0, watcher);\n\t}\n\n\t/**\n\t * 创建并初始化监听，监听修改事件\n\t *\n\t * @param path 路径\n\t * @param maxDepth 当监听目录时，监听目录的最大深度，当设置值为1（或小于1）时，表示不递归监听子目录\n\t * @param watcher {@link Watcher}\n\t * @return {@link WatchMonitor}\n\t * @since 4.5.2\n\t */\n\tpublic static WatchMonitor createModify(Path path, int maxDepth, Watcher watcher) {\n\t\tfinal WatchMonitor watchMonitor = create(path, maxDepth, WatchMonitor.ENTRY_MODIFY);\n\t\twatchMonitor.setWatcher(watcher);\n\t\treturn watchMonitor;\n\t}\n\n\t/**\n\t * 注册Watchable对象到WatchService服务\n\t *\n\t * @param watchable 可注册对象\n\t * @param watcher WatchService对象\n\t * @param events 监听事件\n\t * @return {@link WatchKey}\n\t * @since 4.6.9\n\t */\n\tpublic static WatchKey register(Watchable watchable, WatchService watcher, WatchEvent.Kind<?>... events){\n\t\ttry {\n\t\t\treturn watchable.register(watcher, events);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/Watcher.java",
    "content": "package cn.hutool.core.io.watch;\n\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\n\n/**\n * 观察者（监视器）\n *\n * @author Looly\n */\npublic interface Watcher {\n\t/**\n\t * 文件创建时执行的方法\n\t *\n\t * @param event       事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tvoid onCreate(WatchEvent<?> event, Path currentPath);\n\n\t/**\n\t * 文件修改时执行的方法<br>\n\t * 文件修改可能触发多次\n\t *\n\t * @param event       事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tvoid onModify(WatchEvent<?> event, Path currentPath);\n\n\t/**\n\t * 文件删除时执行的方法\n\t *\n\t * @param event       事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tvoid onDelete(WatchEvent<?> event, Path currentPath);\n\n\t/**\n\t * 事件丢失或出错时执行的方法\n\t *\n\t * @param event       事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tvoid onOverflow(WatchEvent<?> event, Path currentPath);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/package-info.java",
    "content": "/**\n * 基于JDK7+ WatchService的文件和目录监听封装，支持多级目录\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.watch;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java",
    "content": "package cn.hutool.core.io.watch.watchers;\n\nimport cn.hutool.core.collection.ConcurrentHashSet;\nimport cn.hutool.core.io.watch.Watcher;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.thread.ThreadUtil;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.WatchEvent;\nimport java.nio.file.WatchService;\nimport java.util.Set;\n\n/**\n * 延迟观察者<br>\n * 使用此观察者通过定义一定的延迟时间，解决{@link WatchService}多个modify的问题<br>\n * 在监听目录或文件时，如果这个文件有修改操作，会多次触发modify方法。<br>\n * 此类通过维护一个Set将短时间内相同文件多次modify的事件合并处理触发，从而避免以上问题。<br>\n * 注意：延迟只针对modify事件，其它事件无效\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class DelayWatcher implements Watcher {\n\n\t/** Path集合。此集合用于去重在指定delay内多次触发的文件Path */\n\tprivate final Set<Path> eventSet = new ConcurrentHashSet<>();\n\t/** 实际处理 */\n\tprivate final Watcher watcher;\n\t/** 延迟，单位毫秒 */\n\tprivate final long delay;\n\n\t//---------------------------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t * @param watcher 实际处理触发事件的监视器{@link Watcher}，不可以是{@link DelayWatcher}\n\t * @param delay 延迟时间，单位毫秒\n\t */\n\tpublic DelayWatcher(Watcher watcher, long delay) {\n\t\tAssert.notNull(watcher);\n\t\tif(watcher instanceof DelayWatcher) {\n\t\t\tthrow new IllegalArgumentException(\"Watcher must not be a DelayWatcher\");\n\t\t}\n\t\tthis.watcher = watcher;\n\t\tthis.delay = delay;\n\t}\n\t//---------------------------------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t\tif(this.delay < 1) {\n\t\t\tthis.watcher.onModify(event, currentPath);\n\t\t}else {\n\t\t\tonDelayModify(event, currentPath);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onCreate(WatchEvent<?> event, Path currentPath) {\n\t\twatcher.onCreate(event, currentPath);\n\t}\n\n\t@Override\n\tpublic void onDelete(WatchEvent<?> event, Path currentPath) {\n\t\twatcher.onDelete(event, currentPath);\n\t}\n\n\t@Override\n\tpublic void onOverflow(WatchEvent<?> event, Path currentPath) {\n\t\twatcher.onOverflow(event, currentPath);\n\t}\n\n\t//---------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 触发延迟修改\n\t * @param event 事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tprivate void onDelayModify(WatchEvent<?> event, Path currentPath) {\n\t\tPath eventPath = Paths.get(currentPath.toString(), event.context().toString());\n\t\tif(eventSet.contains(eventPath)) {\n\t\t\t//此事件已经被触发过，后续事件忽略，等待统一处理。\n\t\t\treturn;\n\t\t}\n\n\t\t//事件第一次触发，此时标记事件，并启动处理线程延迟处理，处理结束后会删除标记\n\t\teventSet.add(eventPath);\n\t\tstartHandleModifyThread(event, currentPath);\n\t}\n\n\t/**\n\t * 开启处理线程\n\t *\n\t * @param event 事件\n\t * @param currentPath 事件发生的当前Path路径\n\t */\n\tprivate void startHandleModifyThread(final WatchEvent<?> event, final Path currentPath) {\n\t\tThreadUtil.execute(() -> {\n\t\t\tThreadUtil.sleep(delay);\n\t\t\teventSet.remove(Paths.get(currentPath.toString(), event.context().toString()));\n\t\t\twatcher.onModify(event, currentPath);\n\t\t});\n\t}\n\t//---------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java",
    "content": "package cn.hutool.core.io.watch.watchers;\n\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\n\nimport cn.hutool.core.io.watch.Watcher;\n\n/**\n * 跳过所有事件处理Watcher<br>\n * 用户继承此类后实现需要监听的方法\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class IgnoreWatcher implements Watcher {\n\n\t@Override\n\tpublic void onCreate(WatchEvent<?> event, Path currentPath) {\n\t}\n\n\t@Override\n\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t}\n\n\t@Override\n\tpublic void onDelete(WatchEvent<?> event, Path currentPath) {\n\t}\n\n\t@Override\n\tpublic void onOverflow(WatchEvent<?> event, Path currentPath) {\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java",
    "content": "package cn.hutool.core.io.watch.watchers;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.watch.Watcher;\nimport cn.hutool.core.lang.Chain;\n\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * 观察者链<br>\n * 用于加入多个观察者\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class WatcherChain implements Watcher, Chain<Watcher, WatcherChain>{\n\n\t/** 观察者列表 */\n\tfinal private List<Watcher> chain;\n\n\t/**\n\t * 创建观察者链{@link WatcherChain}\n\t * @param watchers  观察者列表\n\t * @return {@link WatcherChain}\n\t */\n\tpublic static WatcherChain create(Watcher... watchers) {\n\t\treturn new WatcherChain(watchers);\n\t}\n\n\t/**\n\t * 构造\n\t * @param watchers 观察者列表\n\t */\n\tpublic WatcherChain(Watcher... watchers) {\n\t\tchain = CollUtil.newArrayList(watchers);\n\t}\n\n\t@Override\n\tpublic void onCreate(WatchEvent<?> event, Path currentPath) {\n\t\tfor (Watcher watcher : chain) {\n\t\t\twatcher.onCreate(event, currentPath);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t\tfor (Watcher watcher : chain) {\n\t\t\twatcher.onModify(event, currentPath);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onDelete(WatchEvent<?> event, Path currentPath) {\n\t\tfor (Watcher watcher : chain) {\n\t\t\twatcher.onDelete(event, currentPath);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onOverflow(WatchEvent<?> event, Path currentPath) {\n\t\tfor (Watcher watcher : chain) {\n\t\t\twatcher.onOverflow(event, currentPath);\n\t\t}\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Iterator<Watcher> iterator() {\n\t\treturn this.chain.iterator();\n\t}\n\n\t@Override\n\tpublic WatcherChain addChain(Watcher element) {\n\t\tthis.chain.add(element);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java",
    "content": "/**\n * 文件监听中的观察者实现类，包括延迟处理、处理链等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.io.watch.watchers;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Assert.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n/**\n * 断言<br>\n * 断言某些对象或值是否符合规定，否则抛出异常。经常用于做变量检查\n *\n * @author Looly\n */\npublic class Assert {\n\n\tprivate static final String TEMPLATE_VALUE_MUST_BE_BETWEEN_AND = \"The value must be between {} and {}.\";\n\n\n\t/**\n\t * 断言是否为真，如果为 {@code false} 抛出给定的异常<br>\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isTrue(i &gt; 0, IllegalArgumentException::new);\n\t * </pre>\n\t *\n\t * @param <X>        异常类型\n\t * @param expression 布尔值\n\t * @param supplier   指定断言不通过时抛出的异常\n\t * @throws X if expression is {@code false}\n\t */\n\tpublic static <X extends Throwable> void isTrue(boolean expression, Supplier<? extends X> supplier) throws X {\n\t\tif (false == expression) {\n\t\t\tthrow supplier.get();\n\t\t}\n\t}\n\n\t/**\n\t * 断言是否为真，如果为 {@code false} 抛出 {@code IllegalArgumentException} 异常<br>\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isTrue(i &gt; 0, \"The value must be greater than zero\");\n\t * </pre>\n\t *\n\t * @param expression       布尔值\n\t * @param errorMsgTemplate 错误抛出异常附带的消息模板，变量用{}代替\n\t * @param params           参数列表\n\t * @throws IllegalArgumentException if expression is {@code false}\n\t */\n\tpublic static void isTrue(boolean expression, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tisTrue(expression, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言是否为真，如果为 {@code false} 抛出 {@code IllegalArgumentException} 异常<br>\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isTrue(i &gt; 0);\n\t * </pre>\n\t *\n\t * @param expression 布尔值\n\t * @throws IllegalArgumentException if expression is {@code false}\n\t */\n\tpublic static void isTrue(boolean expression) throws IllegalArgumentException {\n\t\tisTrue(expression, \"[Assertion failed] - this expression must be true\");\n\t}\n\n\t/**\n\t * 断言是否为假，如果为 {@code true} 抛出指定类型异常<br>\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t *  Assert.isFalse(i &gt; 0, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <X>           异常类型\n\t * @param expression    布尔值\n\t * @param errorSupplier 指定断言不通过时抛出的异常\n\t * @throws X if expression is {@code false}\n\t * @since 5.4.5\n\t */\n\tpublic static <X extends Throwable> void isFalse(boolean expression, Supplier<X> errorSupplier) throws X {\n\t\tif (expression) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t}\n\n\t/**\n\t * 断言是否为假，如果为 {@code true} 抛出 {@code IllegalArgumentException} 异常<br>\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isFalse(i &lt; 0, \"The value must not be negative\");\n\t * </pre>\n\t *\n\t * @param expression       布尔值\n\t * @param errorMsgTemplate 错误抛出异常附带的消息模板，变量用{}代替\n\t * @param params           参数列表\n\t * @throws IllegalArgumentException if expression is {@code false}\n\t */\n\tpublic static void isFalse(boolean expression, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tisFalse(expression, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言是否为假，如果为 {@code true} 抛出 {@code IllegalArgumentException} 异常<br>\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isFalse(i &lt; 0);\n\t * </pre>\n\t *\n\t * @param expression 布尔值\n\t * @throws IllegalArgumentException if expression is {@code false}\n\t */\n\tpublic static void isFalse(boolean expression) throws IllegalArgumentException {\n\t\tisFalse(expression, \"[Assertion failed] - this expression must be false\");\n\t}\n\n\t/**\n\t * 断言对象是否为{@code null} ，如果不为{@code null} 抛出指定类型异常\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.isNull(value, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <X>           异常类型\n\t * @param object        被检查的对象\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @throws X if the object is not {@code null}\n\t * @since 5.4.5\n\t */\n\tpublic static <X extends Throwable> void isNull(Object object, Supplier<X> errorSupplier) throws X {\n\t\tif (null != object) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t}\n\n\t/**\n\t * 断言对象是否为{@code null} ，如果不为{@code null} 抛出{@link IllegalArgumentException} 异常\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isNull(value, \"The value must be null\");\n\t * </pre>\n\t *\n\t * @param object           被检查的对象\n\t * @param errorMsgTemplate 消息模板，变量使用{}表示\n\t * @param params           参数列表\n\t * @throws IllegalArgumentException if the object is not {@code null}\n\t */\n\tpublic static void isNull(Object object, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tisNull(object, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言对象是否为{@code null} ，如果不为{@code null} 抛出{@link IllegalArgumentException} 异常\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isNull(value);\n\t * </pre>\n\t *\n\t * @param object 被检查对象\n\t * @throws IllegalArgumentException if the object is not {@code null}\n\t */\n\tpublic static void isNull(Object object) throws IllegalArgumentException {\n\t\tisNull(object, \"[Assertion failed] - the object argument must be null\");\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------- Check not null\n\n\t/**\n\t * 断言对象是否不为{@code null} ，如果为{@code null} 抛出指定类型异常\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.notNull(clazz, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <T>           被检查对象泛型类型\n\t * @param <X>           异常类型\n\t * @param object        被检查对象\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 被检查后的对象\n\t * @throws X if the object is {@code null}\n\t * @since 5.4.5\n\t */\n\tpublic static <T, X extends Throwable> T notNull(T object, Supplier<X> errorSupplier) throws X {\n\t\tif (null == object) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn object;\n\t}\n\n\t/**\n\t * 断言对象是否不为{@code null} ，如果为{@code null} 抛出{@link IllegalArgumentException} 异常 Assert that an object is not {@code null} .\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notNull(clazz, \"The class must not be null\");\n\t * </pre>\n\t *\n\t * @param <T>              被检查对象泛型类型\n\t * @param object           被检查对象\n\t * @param errorMsgTemplate 错误消息模板，变量使用{}表示\n\t * @param params           参数\n\t * @return 被检查后的对象\n\t * @throws IllegalArgumentException if the object is {@code null}\n\t */\n\tpublic static <T> T notNull(T object, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notNull(object, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言对象是否不为{@code null} ，如果为{@code null} 抛出{@link IllegalArgumentException} 异常\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notNull(clazz);\n\t * </pre>\n\t *\n\t * @param <T>    被检查对象类型\n\t * @param object 被检查对象\n\t * @return 非空对象\n\t * @throws IllegalArgumentException if the object is {@code null}\n\t */\n\tpublic static <T> T notNull(T object) throws IllegalArgumentException {\n\t\treturn notNull(object, \"[Assertion failed] - this argument is required; it must not be null\");\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------- Check empty\n\n\t/**\n\t * 检查给定字符串是否为空，为空抛出自定义异常，并使用指定的函数获取错误信息返回。\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(name, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <X>           异常类型\n\t * @param <T>           字符串类型\n\t * @param text          被检查字符串\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 非空字符串\n\t * @throws X 被检查字符串为空抛出此异常\n\t * @see StrUtil#isNotEmpty(CharSequence)\n\t * @since 5.4.5\n\t */\n\tpublic static <T extends CharSequence, X extends Throwable> T notEmpty(T text, Supplier<X> errorSupplier) throws X {\n\t\tif (StrUtil.isEmpty(text)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn text;\n\t}\n\n\t/**\n\t * 检查给定字符串是否为空，为空抛出 {@link IllegalArgumentException}\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(name, \"Name must not be empty\");\n\t * </pre>\n\t *\n\t * @param <T>              字符串类型\n\t * @param text             被检查字符串\n\t * @param errorMsgTemplate 错误消息模板，变量使用{}表示\n\t * @param params           参数\n\t * @return 非空字符串\n\t * @throws IllegalArgumentException 被检查字符串为空\n\t * @see StrUtil#isNotEmpty(CharSequence)\n\t */\n\tpublic static <T extends CharSequence> T notEmpty(T text, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notEmpty(text, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 检查给定字符串是否为空，为空抛出 {@link IllegalArgumentException}\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(name);\n\t * </pre>\n\t *\n\t * @param <T>  字符串类型\n\t * @param text 被检查字符串\n\t * @return 被检查的字符串\n\t * @throws IllegalArgumentException 被检查字符串为空\n\t * @see StrUtil#isNotEmpty(CharSequence)\n\t */\n\tpublic static <T extends CharSequence> T notEmpty(T text) throws IllegalArgumentException {\n\t\treturn notEmpty(text, \"[Assertion failed] - this String argument must have length; it must not be null or empty\");\n\t}\n\n\t/**\n\t * 检查给定字符串是否为空白（null、空串或只包含空白符），为空抛出自定义异常。\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.notBlank(name, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <X>              异常类型\n\t * @param <T>              字符串类型\n\t * @param text             被检查字符串\n\t * @param errorMsgSupplier 错误抛出异常附带的消息生产接口\n\t * @return 非空字符串\n\t * @throws X 被检查字符串为空白\n\t * @see StrUtil#isNotBlank(CharSequence)\n\t */\n\tpublic static <T extends CharSequence, X extends Throwable> T notBlank(T text, Supplier<X> errorMsgSupplier) throws X {\n\t\tif (StrUtil.isBlank(text)) {\n\t\t\tthrow errorMsgSupplier.get();\n\t\t}\n\t\treturn text;\n\t}\n\n\t/**\n\t * 检查给定字符串是否为空白（null、空串或只包含空白符），为空抛出 {@link IllegalArgumentException}\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notBlank(name, \"Name must not be blank\");\n\t * </pre>\n\t *\n\t * @param <T>              字符串类型\n\t * @param text             被检查字符串\n\t * @param errorMsgTemplate 错误消息模板，变量使用{}表示\n\t * @param params           参数\n\t * @return 非空字符串\n\t * @throws IllegalArgumentException 被检查字符串为空白\n\t * @see StrUtil#isNotBlank(CharSequence)\n\t */\n\tpublic static <T extends CharSequence> T notBlank(T text, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notBlank(text, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 检查给定字符串是否为空白（null、空串或只包含空白符），为空抛出 {@link IllegalArgumentException}\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notBlank(name);\n\t * </pre>\n\t *\n\t * @param <T>  字符串类型\n\t * @param text 被检查字符串\n\t * @return 非空字符串\n\t * @throws IllegalArgumentException 被检查字符串为空白\n\t * @see StrUtil#isNotBlank(CharSequence)\n\t */\n\tpublic static <T extends CharSequence> T notBlank(T text) throws IllegalArgumentException {\n\t\treturn notBlank(text, \"[Assertion failed] - this String argument must have text; it must not be null, empty, or blank\");\n\t}\n\n\t/**\n\t * 断言给定字符串是否不被另一个字符串包含（即是否为子串）\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.notContain(name, \"rod\", ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return \");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <T>           字符串类型\n\t * @param <X>           异常类型\n\t * @param textToSearch  被搜索的字符串\n\t * @param substring     被检查的子串\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 被检查的子串\n\t * @throws X 非子串抛出异常\n\t * @see StrUtil#contains(CharSequence, CharSequence)\n\t * @since 5.4.5\n\t */\n\tpublic static <T extends CharSequence, X extends Throwable> T notContain(CharSequence textToSearch, T substring, Supplier<X> errorSupplier) throws X {\n\t\tif (StrUtil.contains(textToSearch, substring)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn substring;\n\t}\n\n\t/**\n\t * 断言给定字符串是否不被另一个字符串包含（即是否为子串）\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notContain(name, \"rod\", \"Name must not contain 'rod'\");\n\t * </pre>\n\t *\n\t * @param textToSearch     被搜索的字符串\n\t * @param substring        被检查的子串\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 被检查的子串\n\t * @throws IllegalArgumentException 非子串抛出异常\n\t */\n\tpublic static String notContain(String textToSearch, String substring, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notContain(textToSearch, substring, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定字符串是否不被另一个字符串包含（即是否为子串）\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notContain(name, \"rod\");\n\t * </pre>\n\t *\n\t * @param textToSearch 被搜索的字符串\n\t * @param substring    被检查的子串\n\t * @return 被检查的子串\n\t * @throws IllegalArgumentException 非子串抛出异常\n\t */\n\tpublic static String notContain(String textToSearch, String substring) throws IllegalArgumentException {\n\t\treturn notContain(textToSearch, substring, \"[Assertion failed] - this String argument must not contain the substring [{}]\", substring);\n\t}\n\n\t/**\n\t * 断言给定数组是否包含元素，数组必须不为 {@code null} 且至少包含一个元素\n\t * 并使用指定的函数获取错误信息返回\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(array, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <T>           数组元素类型\n\t * @param <X>           异常类型\n\t * @param array         被检查的数组\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 被检查的数组\n\t * @throws X if the object array is {@code null} or has no elements\n\t * @see ArrayUtil#isNotEmpty(Object[])\n\t * @since 5.4.5\n\t */\n\tpublic static <T, X extends Throwable> T[] notEmpty(T[] array, Supplier<X> errorSupplier) throws X {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 断言给定数组是否包含元素，数组必须不为 {@code null} 且至少包含一个元素\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(array, \"The array must have elements\");\n\t * </pre>\n\t *\n\t * @param <T>              数组元素类型\n\t * @param array            被检查的数组\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 被检查的数组\n\t * @throws IllegalArgumentException if the object array is {@code null} or has no elements\n\t */\n\tpublic static <T> T[] notEmpty(T[] array, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notEmpty(array, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定数组是否包含元素，数组必须不为 {@code null} 且至少包含一个元素\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(array, \"The array must have elements\");\n\t * </pre>\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被检查的数组\n\t * @return 被检查的数组\n\t * @throws IllegalArgumentException if the object array is {@code null} or has no elements\n\t */\n\tpublic static <T> T[] notEmpty(T[] array) throws IllegalArgumentException {\n\t\treturn notEmpty(array, \"[Assertion failed] - this array must not be empty: it must contain at least 1 element\");\n\t}\n\n\t/**\n\t * 断言给定数组是否不包含{@code null}元素，如果数组为空或 {@code null}将被认为不包含\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.noNullElements(array, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return \");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <T>           数组元素类型\n\t * @param <X>           异常类型\n\t * @param array         被检查的数组\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 被检查的数组\n\t * @throws X if the object array contains a {@code null} element\n\t * @see ArrayUtil#hasNull(Object[])\n\t * @since 5.4.5\n\t */\n\tpublic static <T, X extends Throwable> T[] noNullElements(T[] array, Supplier<X> errorSupplier) throws X {\n\t\tif (ArrayUtil.hasNull(array)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 断言给定数组是否不包含{@code null}元素，如果数组为空或 {@code null}将被认为不包含\n\t *\n\t * <pre class=\"code\">\n\t * Assert.noNullElements(array, \"The array must not have null elements\");\n\t * </pre>\n\t *\n\t * @param <T>              数组元素类型\n\t * @param array            被检查的数组\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 被检查的数组\n\t * @throws IllegalArgumentException if the object array contains a {@code null} element\n\t */\n\tpublic static <T> T[] noNullElements(T[] array, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn noNullElements(array, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定数组是否不包含{@code null}元素，如果数组为空或 {@code null}将被认为不包含\n\t *\n\t * <pre class=\"code\">\n\t * Assert.noNullElements(array);\n\t * </pre>\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被检查的数组\n\t * @return 被检查的数组\n\t * @throws IllegalArgumentException if the object array contains a {@code null} element\n\t */\n\tpublic static <T> T[] noNullElements(T[] array) throws IllegalArgumentException {\n\t\treturn noNullElements(array, \"[Assertion failed] - this array must not contain any null elements\");\n\t}\n\n\t/**\n\t * 断言给定集合为空\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.empty(collection, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <E>           集合元素类型\n\t * @param <T>           集合类型\n\t * @param <X>           异常类型\n\t * @param collection    被检查的集合\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @throws X if the collection is not {@code null} or has elements\n\t * @see CollUtil#isEmpty(Iterable)\n\t * @since 5.8.39\n\t */\n\tpublic static <E, T extends Iterable<E>, X extends Throwable> void empty(T collection, Supplier<X> errorSupplier) throws X {\n\t\tif (CollUtil.isNotEmpty(collection)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t}\n\n\n\t/**\n\t * 断言给定集合为空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.empty(collection, \"Collection must have no elements\");\n\t * </pre>\n\t *\n\t * @param <E>              集合元素类型\n\t * @param <T>              集合类型\n\t * @param collection       被检查的集合\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @throws IllegalArgumentException if the collection is not {@code null} or has elements\n\t * @since 5.8.39\n\t */\n\tpublic static <E, T extends Iterable<E>> void empty(T collection, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tempty(collection, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定集合为空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.empty(collection);\n\t * </pre>\n\t *\n\t * @param <E>        集合元素类型\n\t * @param <T>        集合类型\n\t * @param collection 被检查的集合\n\t * @throws IllegalArgumentException if the collection is not {@code null} or has elements\n\t * @since 5.8.39\n\t */\n\tpublic static <E, T extends Iterable<E>> void empty(T collection) throws IllegalArgumentException {\n\t\tempty(collection, \"[Assertion failed] - this collection must be empty\");\n\t}\n\n\t/**\n\t * 断言给定集合非空\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(collection, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <E>           集合元素类型\n\t * @param <T>           集合类型\n\t * @param <X>           异常类型\n\t * @param collection    被检查的集合\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 非空集合\n\t * @throws X if the collection is {@code null} or has no elements\n\t * @see CollUtil#isNotEmpty(Iterable)\n\t * @since 5.4.5\n\t */\n\tpublic static <E, T extends Iterable<E>, X extends Throwable> T notEmpty(T collection, Supplier<X> errorSupplier) throws X {\n\t\tif (CollUtil.isEmpty(collection)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 断言给定集合非空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(collection, \"Collection must have elements\");\n\t * </pre>\n\t *\n\t * @param <E>              集合元素类型\n\t * @param <T>              集合类型\n\t * @param collection       被检查的集合\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 非空集合\n\t * @throws IllegalArgumentException if the collection is {@code null} or has no elements\n\t */\n\tpublic static <E, T extends Iterable<E>> T notEmpty(T collection, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notEmpty(collection, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定集合非空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(collection);\n\t * </pre>\n\t *\n\t * @param <E>        集合元素类型\n\t * @param <T>        集合类型\n\t * @param collection 被检查的集合\n\t * @return 被检查集合\n\t * @throws IllegalArgumentException if the collection is {@code null} or has no elements\n\t */\n\tpublic static <E, T extends Iterable<E>> T notEmpty(T collection) throws IllegalArgumentException {\n\t\treturn notEmpty(collection, \"[Assertion failed] - this collection must not be empty: it must contain at least 1 element\");\n\t}\n\n\t/**\n\t * 断言给定Map非空\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(map, ()-&gt;{\n\t *      // to query relation message\n\t *      return new IllegalArgumentException(\"relation message to return\");\n\t *  });\n\t * </pre>\n\t *\n\t * @param <K>           Key类型\n\t * @param <V>           Value类型\n\t * @param <T>           Map类型\n\t * @param <X>           异常类型\n\t * @param map           被检查的Map\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 被检查的Map\n\t * @throws X if the map is {@code null} or has no entries\n\t * @see MapUtil#isNotEmpty(Map)\n\t * @since 5.4.5\n\t */\n\tpublic static <K, V, T extends Map<K, V>, X extends Throwable> T notEmpty(T map, Supplier<X> errorSupplier) throws X {\n\t\tif (MapUtil.isEmpty(map)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 断言给定Map非空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(map, \"Map must have entries\");\n\t * </pre>\n\t *\n\t * @param <K>              Key类型\n\t * @param <V>              Value类型\n\t * @param <T>              Map类型\n\t * @param map              被检查的Map\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 被检查的Map\n\t * @throws IllegalArgumentException if the map is {@code null} or has no entries\n\t */\n\tpublic static <K, V, T extends Map<K, V>> T notEmpty(T map, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\treturn notEmpty(map, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言给定Map非空\n\t *\n\t * <pre class=\"code\">\n\t * Assert.notEmpty(map, \"Map must have entries\");\n\t * </pre>\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @param <T> Map类型\n\t * @param map 被检查的Map\n\t * @return 被检查的Map\n\t * @throws IllegalArgumentException if the map is {@code null} or has no entries\n\t */\n\tpublic static <K, V, T extends Map<K, V>> T notEmpty(T map) throws IllegalArgumentException {\n\t\treturn notEmpty(map, \"[Assertion failed] - this map must not be empty; it must contain at least one entry\");\n\t}\n\n\t/**\n\t * 断言给定对象是否是给定类的实例\n\t *\n\t * <pre class=\"code\">\n\t * Assert.instanceOf(Foo.class, foo);\n\t * </pre>\n\t *\n\t * @param <T>  被检查对象泛型类型\n\t * @param type 被检查对象匹配的类型\n\t * @param obj  被检查对象\n\t * @return 被检查的对象\n\t * @throws IllegalArgumentException if the object is not an instance of clazz\n\t * @see Class#isInstance(Object)\n\t */\n\tpublic static <T> T isInstanceOf(Class<?> type, T obj) {\n\t\treturn isInstanceOf(type, obj, \"Object [{}] is not instanceof [{}]\", obj, type);\n\t}\n\n\t/**\n\t * 断言给定对象是否是给定类的实例\n\t *\n\t * <pre class=\"code\">\n\t * Assert.instanceOf(Foo.class, foo, \"foo must be an instance of class Foo\");\n\t * </pre>\n\t *\n\t * @param <T>              被检查对象泛型类型\n\t * @param type             被检查对象匹配的类型\n\t * @param obj              被检查对象\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 被检查对象\n\t * @throws IllegalArgumentException if the object is not an instance of clazz\n\t * @see Class#isInstance(Object)\n\t */\n\tpublic static <T> T isInstanceOf(Class<?> type, T obj, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tnotNull(type, \"Type to check against must not be null\");\n\t\tif (false == type.isInstance(obj)) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params));\n\t\t}\n\t\treturn obj;\n\t}\n\n\t/**\n\t * 断言 {@code superType.isAssignableFrom(subType)} 是否为 {@code true}.\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isAssignable(Number.class, myClass);\n\t * </pre>\n\t *\n\t * @param superType 需要检查的父类或接口\n\t * @param subType   需要检查的子类\n\t * @throws IllegalArgumentException 如果子类非继承父类，抛出此异常\n\t */\n\tpublic static void isAssignable(Class<?> superType, Class<?> subType) throws IllegalArgumentException {\n\t\tisAssignable(superType, subType, \"{} is not assignable to {})\", subType, superType);\n\t}\n\n\t/**\n\t * 断言 {@code superType.isAssignableFrom(subType)} 是否为 {@code true}.\n\t *\n\t * <pre class=\"code\">\n\t * Assert.isAssignable(Number.class, myClass, \"myClass must can be assignable to class Number\");\n\t * </pre>\n\t *\n\t * @param superType        需要检查的父类或接口\n\t * @param subType          需要检查的子类\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @throws IllegalArgumentException 如果子类非继承父类，抛出此异常\n\t */\n\tpublic static void isAssignable(Class<?> superType, Class<?> subType, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tnotNull(superType, \"Type to check against must not be null\");\n\t\tif (subType == null || !superType.isAssignableFrom(subType)) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params));\n\t\t}\n\t}\n\n\t/**\n\t * 检查boolean表达式，当检查结果为false时抛出 {@code IllegalStateException}。\n\t * 并使用指定的函数获取错误信息返回\n\t * <pre class=\"code\">\n\t * Assert.state(id == null, ()-&gt;{\n\t *      // to query relation message\n\t *      return \"relation message to return \";\n\t *  });\n\t * </pre>\n\t *\n\t * @param expression       boolean 表达式\n\t * @param errorMsgSupplier 错误抛出异常附带的消息生产接口\n\t * @throws IllegalStateException 表达式为 {@code false} 抛出此异常\n\t */\n\tpublic static void state(boolean expression, Supplier<String> errorMsgSupplier) throws IllegalStateException {\n\t\tif (false == expression) {\n\t\t\tthrow new IllegalStateException(errorMsgSupplier.get());\n\t\t}\n\t}\n\n\t/**\n\t * 检查boolean表达式，当检查结果为false时抛出 {@code IllegalStateException}。\n\t *\n\t * <pre class=\"code\">\n\t * Assert.state(id == null, \"The id property must not already be initialized\");\n\t * </pre>\n\t *\n\t * @param expression       boolean 表达式\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @throws IllegalStateException 表达式为 {@code false} 抛出此异常\n\t */\n\tpublic static void state(boolean expression, String errorMsgTemplate, Object... params) throws IllegalStateException {\n\t\tif (false == expression) {\n\t\t\tthrow new IllegalStateException(StrUtil.format(errorMsgTemplate, params));\n\t\t}\n\t}\n\n\t/**\n\t * 检查boolean表达式，当检查结果为false时抛出 {@code IllegalStateException}。\n\t *\n\t * <pre class=\"code\">\n\t * Assert.state(id == null);\n\t * </pre>\n\t *\n\t * @param expression boolean 表达式\n\t * @throws IllegalStateException 表达式为 {@code false} 抛出此异常\n\t */\n\tpublic static void state(boolean expression) throws IllegalStateException {\n\t\tstate(expression, \"[Assertion failed] - this state invariant must be true\");\n\t}\n\n\t/**\n\t * 检查下标（数组、集合、字符串）是否符合要求，下标必须满足：\n\t *\n\t * <pre>\n\t * 0 &le; index &lt; size\n\t * </pre>\n\t *\n\t * @param index 下标\n\t * @param size  长度\n\t * @return 检查后的下标\n\t * @throws IllegalArgumentException  如果size &lt; 0 抛出此异常\n\t * @throws IndexOutOfBoundsException 如果index &lt; 0或者 index &ge; size 抛出此异常\n\t * @since 4.1.9\n\t */\n\tpublic static int checkIndex(int index, int size) throws IllegalArgumentException, IndexOutOfBoundsException {\n\t\treturn checkIndex(index, size, \"[Assertion failed]\");\n\t}\n\n\t/**\n\t * 检查下标（数组、集合、字符串）是否符合要求，下标必须满足：\n\t *\n\t * <pre>\n\t * 0 &le; index &lt; size\n\t * </pre>\n\t *\n\t * @param index            下标\n\t * @param size             长度\n\t * @param errorMsgTemplate 异常时的消息模板\n\t * @param params           参数列表\n\t * @return 检查后的下标\n\t * @throws IllegalArgumentException  如果size &lt; 0 抛出此异常\n\t * @throws IndexOutOfBoundsException 如果index &lt; 0或者 index &ge; size 抛出此异常\n\t * @since 4.1.9\n\t */\n\tpublic static int checkIndex(int index, int size, String errorMsgTemplate, Object... params) throws IllegalArgumentException, IndexOutOfBoundsException {\n\t\tif (index < 0 || index >= size) {\n\t\t\tthrow new IndexOutOfBoundsException(badIndexMsg(index, size, errorMsgTemplate, params));\n\t\t}\n\t\treturn index;\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param <X>           异常类型\n\t * @param value         值\n\t * @param min           最小值（包含）\n\t * @param max           最大值（包含）\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 经过检查后的值\n\t * @throws X if value is out of bound\n\t * @since 5.7.15\n\t */\n\tpublic static <X extends Throwable> int checkBetween(int value, int min, int max, Supplier<? extends X> errorSupplier) throws X {\n\t\tif (value < min || value > max) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value            值\n\t * @param min              最小值（包含）\n\t * @param max              最大值（包含）\n\t * @param errorMsgTemplate 异常信息模板，类似于\"aa{}bb{}cc\"\n\t * @param params           异常信息参数，用于替换\"{}\"占位符\n\t * @return 经过检查后的值\n\t * @since 5.7.15\n\t */\n\tpublic static int checkBetween(int value, int min, int max, String errorMsgTemplate, Object... params) {\n\t\treturn checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value 值\n\t * @param min   最小值（包含）\n\t * @param max   最大值（包含）\n\t * @return 检查后的长度值\n\t * @since 4.1.10\n\t */\n\tpublic static int checkBetween(int value, int min, int max) {\n\t\treturn checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param <X>           异常类型\n\t * @param value         值\n\t * @param min           最小值（包含）\n\t * @param max           最大值（包含）\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 经过检查后的值\n\t * @throws X if value is out of bound\n\t * @since 5.7.15\n\t */\n\tpublic static <X extends Throwable> long checkBetween(long value, long min, long max, Supplier<? extends X> errorSupplier) throws X {\n\t\tif (value < min || value > max) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value            值\n\t * @param min              最小值（包含）\n\t * @param max              最大值（包含）\n\t * @param errorMsgTemplate 异常信息模板，类似于\"aa{}bb{}cc\"\n\t * @param params           异常信息参数，用于替换\"{}\"占位符\n\t * @return 经过检查后的值\n\t * @since 5.7.15\n\t */\n\tpublic static long checkBetween(long value, long min, long max, String errorMsgTemplate, Object... params) {\n\t\treturn checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value 值\n\t * @param min   最小值（包含）\n\t * @param max   最大值（包含）\n\t * @return 检查后的长度值\n\t * @since 4.1.10\n\t */\n\tpublic static long checkBetween(long value, long min, long max) {\n\t\treturn checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param <X>           异常类型\n\t * @param value         值\n\t * @param min           最小值（包含）\n\t * @param max           最大值（包含）\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @return 经过检查后的值\n\t * @throws X if value is out of bound\n\t * @since 5.7.15\n\t */\n\tpublic static <X extends Throwable> double checkBetween(double value, double min, double max, Supplier<? extends X> errorSupplier) throws X {\n\t\tif (value < min || value > max) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value            值\n\t * @param min              最小值（包含）\n\t * @param max              最大值（包含）\n\t * @param errorMsgTemplate 异常信息模板，类似于\"aa{}bb{}cc\"\n\t * @param params           异常信息参数，用于替换\"{}\"占位符\n\t * @return 经过检查后的值\n\t * @since 5.7.15\n\t */\n\tpublic static double checkBetween(double value, double min, double max, String errorMsgTemplate, Object... params) {\n\t\treturn checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value 值\n\t * @param min   最小值（包含）\n\t * @param max   最大值（包含）\n\t * @return 检查后的长度值\n\t * @since 4.1.10\n\t */\n\tpublic static double checkBetween(double value, double min, double max) {\n\t\treturn checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value 值\n\t * @param min   最小值（包含）\n\t * @param max   最大值（包含）\n\t * @return 检查后的长度值\n\t * @since 4.1.10\n\t */\n\tpublic static Number checkBetween(Number value, Number min, Number max) {\n\t\tnotNull(value);\n\t\tnotNull(min);\n\t\tnotNull(max);\n\t\tdouble valueDouble = value.doubleValue();\n\t\tdouble minDouble = min.doubleValue();\n\t\tdouble maxDouble = max.doubleValue();\n\t\tif (valueDouble < minDouble || valueDouble > maxDouble) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max));\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 断言两个对象是否不相等,如果两个对象相等 抛出IllegalArgumentException 异常\n\t * <pre class=\"code\">\n\t *   Assert.notEquals(obj1,obj2);\n\t * </pre>\n\t *\n\t * @param obj1 对象1\n\t * @param obj2 对象2\n\t * @throws IllegalArgumentException obj1 must be not equals obj2\n\t */\n\tpublic static void notEquals(Object obj1, Object obj2) {\n\t\tnotEquals(obj1, obj2, \"({}) must be not equals ({})\", obj1, obj2);\n\t}\n\n\t/**\n\t * 断言两个对象是否不相等,如果两个对象相等 抛出IllegalArgumentException 异常\n\t * <pre class=\"code\">\n\t *   Assert.notEquals(obj1,obj2,\"obj1 must be not equals obj2\");\n\t * </pre>\n\t *\n\t * @param obj1             对象1\n\t * @param obj2             对象2\n\t * @param errorMsgTemplate 异常信息模板，类似于\"aa{}bb{}cc\"\n\t * @param params           异常信息参数，用于替换\"{}\"占位符\n\t * @throws IllegalArgumentException obj1 must be not equals obj2\n\t */\n\tpublic static void notEquals(Object obj1, Object obj2, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tnotEquals(obj1, obj2, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言两个对象是否不相等,如果两个对象相等,抛出指定类型异常,并使用指定的函数获取错误信息返回\n\t *\n\t * @param obj1          对象1\n\t * @param obj2          对象2\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @param <X>           异常类型\n\t * @throws X obj1 must be not equals obj2\n\t */\n\tpublic static <X extends Throwable> void notEquals(Object obj1, Object obj2, Supplier<X> errorSupplier) throws X {\n\t\tif (ObjectUtil.equals(obj1, obj2)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t}\n\t// ----------------------------------------------------------------------------------------------------------- Check not equals\n\n\t/**\n\t * 断言两个对象是否相等,如果两个对象不相等 抛出IllegalArgumentException 异常\n\t * <pre class=\"code\">\n\t *   Assert.isEquals(obj1,obj2);\n\t * </pre>\n\t *\n\t * @param obj1 对象1\n\t * @param obj2 对象2\n\t * @throws IllegalArgumentException obj1 must be equals obj2\n\t */\n\tpublic static void equals(Object obj1, Object obj2) {\n\t\tequals(obj1, obj2, \"({}) must be equals ({})\", obj1, obj2);\n\t}\n\n\t/**\n\t * 断言两个对象是否相等,如果两个对象不相等 抛出IllegalArgumentException 异常\n\t * <pre class=\"code\">\n\t *   Assert.isEquals(obj1,obj2,\"obj1 must be equals obj2\");\n\t * </pre>\n\t *\n\t * @param obj1             对象1\n\t * @param obj2             对象2\n\t * @param errorMsgTemplate 异常信息模板，类似于\"aa{}bb{}cc\"\n\t * @param params           异常信息参数，用于替换\"{}\"占位符\n\t * @throws IllegalArgumentException obj1 must be equals obj2\n\t */\n\tpublic static void equals(Object obj1, Object obj2, String errorMsgTemplate, Object... params) throws IllegalArgumentException {\n\t\tequals(obj1, obj2, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));\n\t}\n\n\t/**\n\t * 断言两个对象是否相等,如果两个对象不相等,抛出指定类型异常,并使用指定的函数获取错误信息返回\n\t *\n\t * @param obj1          对象1\n\t * @param obj2          对象2\n\t * @param errorSupplier 错误抛出异常附带的消息生产接口\n\t * @param <X>           异常类型\n\t * @throws X obj1 must be equals obj2\n\t */\n\tpublic static <X extends Throwable> void equals(Object obj1, Object obj2, Supplier<X> errorSupplier) throws X {\n\t\tif (ObjectUtil.notEqual(obj1, obj2)) {\n\t\t\tthrow errorSupplier.get();\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------- Check is equals\n\n\t// -------------------------------------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 错误的下标时显示的消息\n\t *\n\t * @param index  下标\n\t * @param size   长度\n\t * @param desc   异常时的消息模板\n\t * @param params 参数列表\n\t * @return 消息\n\t */\n\tprivate static String badIndexMsg(int index, int size, String desc, Object... params) {\n\t\tif (index < 0) {\n\t\t\treturn StrUtil.format(\"{} ({}) must not be negative\", StrUtil.format(desc, params), index);\n\t\t} else if (size < 0) {\n\t\t\tthrow new IllegalArgumentException(\"negative size: \" + size);\n\t\t} else { // index >= size\n\t\t\treturn StrUtil.format(\"{} ({}) must be less than size ({})\", StrUtil.format(desc, params), index, size);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Chain.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 责任链接口\n * @author Looly\n *\n * @param <E> 元素类型\n * @param <T> 目标类类型，用于返回this对象\n */\npublic interface Chain<E, T> extends Iterable<E>{\n\t/**\n\t * 加入责任链\n\t * @param element 责任链新的环节元素\n\t * @return this\n\t */\n\tT addChain(E element);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ClassScanner.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.EnumerationIter;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.*;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.lang.annotation.Annotation;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.jar.JarEntry;\nimport java.util.jar.JarFile;\n\n/**\n * 类扫描器\n *\n * @author looly\n * @since 4.6.9\n */\npublic class ClassScanner implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 包名\n\t */\n\tprivate final String packageName;\n\t/**\n\t * 包名，最后跟一个点，表示包名，避免在检查前缀时的歧义<br>\n\t * 如果包名指定为空，不跟点\n\t */\n\tprivate final String packageNameWithDot;\n\t/**\n\t * 包路径，用于文件中对路径操作\n\t */\n\tprivate final String packageDirName;\n\t/**\n\t * 包路径，用于jar中对路径操作，在Linux下与packageDirName一致\n\t */\n\tprivate final String packagePath;\n\t/**\n\t * 过滤器\n\t */\n\tprivate final Filter<Class<?>> classFilter;\n\t/**\n\t * 编码\n\t */\n\tprivate final Charset charset;\n\t/**\n\t * 类加载器\n\t */\n\tprivate ClassLoader classLoader;\n\t/**\n\t * 是否初始化类\n\t */\n\tprivate boolean initialize;\n\t/**\n\t * 扫描结果集\n\t */\n\tprivate final Set<Class<?>> classes = new HashSet<>();\n\n\t/**\n\t * 忽略loadClass时的错误\n\t */\n\tprivate boolean ignoreLoadError = false;\n\n\t/**\n\t * 获取加载错误的类名列表\n\t */\n\tprivate final Set<String> classesOfLoadError = new HashSet<>();\n\n\t/**\n\t * 扫描指定包路径下所有包含指定注解的类，包括其他加载的jar或者类\n\t *\n\t * @param packageName     包路径\n\t * @param annotationClass 注解类\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanAllPackageByAnnotation(String packageName, Class<? extends Annotation> annotationClass) {\n\t\treturn scanAllPackage(packageName, clazz -> clazz.isAnnotationPresent(annotationClass));\n\t}\n\n\t/**\n\t * 扫描指定包路径下所有包含指定注解的类<br>\n\t * 如果classpath下已经有类，不再扫描其他加载的jar或者类\n\t *\n\t * @param packageName     包路径\n\t * @param annotationClass 注解类\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackageByAnnotation(String packageName, Class<? extends Annotation> annotationClass) {\n\t\treturn scanPackage(packageName, clazz -> clazz.isAnnotationPresent(annotationClass));\n\t}\n\n\t/**\n\t * 扫描指定包路径下所有指定类或接口的子类或实现类，不包括指定父类本身，包括其他加载的jar或者类\n\t *\n\t * @param packageName 包路径\n\t * @param superClass  父类或接口（不包括）\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanAllPackageBySuper(String packageName, Class<?> superClass) {\n\t\treturn scanAllPackage(packageName, clazz -> superClass.isAssignableFrom(clazz) && !superClass.equals(clazz));\n\t}\n\n\t/**\n\t * 扫描指定包路径下所有指定类或接口的子类或实现类，不包括指定父类本身<br>\n\t * 如果classpath下已经有类，不再扫描其他加载的jar或者类\n\t *\n\t * @param packageName 包路径\n\t * @param superClass  父类或接口（不包括）\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackageBySuper(String packageName, Class<?> superClass) {\n\t\treturn scanPackage(packageName, clazz -> superClass.isAssignableFrom(clazz) && !superClass.equals(clazz));\n\t}\n\n\t/**\n\t * 扫描该包路径下所有class文件，包括其他加载的jar或者类\n\t *\n\t * @return 类集合\n\t * @since 5.7.5\n\t */\n\tpublic static Set<Class<?>> scanAllPackage() {\n\t\treturn scanAllPackage(StrUtil.EMPTY, null);\n\t}\n\n\t/**\n\t * 扫描classpath下所有class文件，如果classpath下已经有类，不再扫描其他加载的jar或者类\n\t *\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackage() {\n\t\treturn scanPackage(StrUtil.EMPTY, null);\n\t}\n\n\t/**\n\t * 扫描该包路径下所有class文件\n\t *\n\t * @param packageName 包路径 com | com. | com.abs | com.abs.\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackage(String packageName) {\n\t\treturn scanPackage(packageName, null);\n\t}\n\n\t/**\n\t * 扫描包路径下和所有在classpath中加载的类，满足class过滤器条件的所有class文件，<br>\n\t * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException<br>\n\t * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理<br>\n\t *\n\t * @param packageName 包路径 com | com. | com.abs | com.abs.\n\t * @param classFilter class过滤器，过滤掉不需要的class\n\t * @return 类集合\n\t * @since 5.7.5\n\t */\n\tpublic static Set<Class<?>> scanAllPackage(String packageName, Filter<Class<?>> classFilter) {\n\t\treturn new ClassScanner(packageName, classFilter).scan(true);\n\t}\n\n\t/**\n\t * 扫描包路径下满足class过滤器条件的所有class文件，<br>\n\t * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException<br>\n\t * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理<br>\n\t *\n\t * @param packageName 包路径 com | com. | com.abs | com.abs.\n\t * @param classFilter class过滤器，过滤掉不需要的class\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackage(String packageName, Filter<Class<?>> classFilter) {\n\t\treturn new ClassScanner(packageName, classFilter).scan();\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t */\n\tpublic ClassScanner() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param packageName 包名，所有包传入\"\"或者null\n\t */\n\tpublic ClassScanner(String packageName) {\n\t\tthis(packageName, null);\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param packageName 包名，所有包传入\"\"或者null\n\t * @param classFilter 过滤器，无需传入null\n\t */\n\tpublic ClassScanner(String packageName, Filter<Class<?>> classFilter) {\n\t\tthis(packageName, classFilter, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param packageName 包名，所有包传入\"\"或者null\n\t * @param classFilter 过滤器，无需传入null\n\t * @param charset     编码\n\t */\n\tpublic ClassScanner(String packageName, Filter<Class<?>> classFilter, Charset charset) {\n\t\tpackageName = StrUtil.nullToEmpty(packageName);\n\t\tthis.packageName = packageName;\n\t\tthis.packageNameWithDot = StrUtil.addSuffixIfNot(packageName, StrUtil.DOT);\n\t\tthis.packageDirName = packageName.replace(CharUtil.DOT, File.separatorChar);\n\t\tthis.packagePath = packageName.replace(CharUtil.DOT, CharUtil.SLASH);\n\t\tthis.classFilter = classFilter;\n\t\tthis.charset = charset;\n\t}\n\n\t/**\n\t * 设置是否忽略loadClass时的错误\n\t *\n\t * @param ignoreLoadError 忽略loadClass时的错误\n\t * @return this\n\t */\n\tpublic ClassScanner setIgnoreLoadError(boolean ignoreLoadError) {\n\t\tthis.ignoreLoadError = ignoreLoadError;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 扫描包路径下满足class过滤器条件的所有class文件<br>\n\t * 此方法首先扫描指定包名下的资源目录，如果未扫描到，则扫描整个classpath中所有加载的类\n\t *\n\t * @return 类集合\n\t */\n\tpublic Set<Class<?>> scan() {\n\t\treturn scan(false);\n\t}\n\n\t/**\n\t * 扫描包路径下满足class过滤器条件的所有class文件\n\t *\n\t * @param forceScanJavaClassPaths 是否强制扫描其他位于classpath关联jar中的类\n\t * @return 类集合\n\t * @since 5.7.5\n\t */\n\tpublic Set<Class<?>> scan(boolean forceScanJavaClassPaths) {\n\n\t\t//多次扫描时,清理上次扫描历史\n\t\tthis.classes.clear();\n\t\tthis.classesOfLoadError.clear();\n\n\t\tfor (URL url : ResourceUtil.getResourceIter(this.packagePath, this.classLoader)) {\n\t\t\tswitch (url.getProtocol()) {\n\t\t\t\tcase \"file\":\n\t\t\t\t\tscanFile(new File(URLUtil.decode(url.getFile(), this.charset.name())), null);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"jar\":\n\t\t\t\t\tscanJar(URLUtil.getJarFile(url));\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// classpath下未找到，则扫描其他jar包下的类\n\t\tif (forceScanJavaClassPaths || CollUtil.isEmpty(this.classes)) {\n\t\t\tscanJavaClassPaths();\n\t\t}\n\n\t\treturn Collections.unmodifiableSet(this.classes);\n\t}\n\n\t/**\n\t * 设置是否在扫描到类时初始化类\n\t *\n\t * @param initialize 是否初始化类\n\t */\n\tpublic void setInitialize(boolean initialize) {\n\t\tthis.initialize = initialize;\n\t}\n\n\t/**\n\t * 设置自定义的类加载器\n\t *\n\t * @param classLoader 类加载器\n\t * @since 4.6.9\n\t */\n\tpublic void setClassLoader(ClassLoader classLoader) {\n\t\tthis.classLoader = classLoader;\n\t}\n\n\t/**\n\t * 忽略加载错误扫描后，可以获得之前扫描时加载错误的类名字集合\n\t * @return 加载错误的类名字集合\n\t */\n\tpublic Set<String> getClassesOfLoadError() {\n\t\treturn Collections.unmodifiableSet(this.classesOfLoadError);\n\t}\n\n\t// --------------------------------------------------------------------------------------------------- Private method start\n\n\t@Override\n\tprotected Object clone() throws CloneNotSupportedException {\n\t\treturn super.clone();\n\t}\n\n\t/**\n\t * 扫描Java指定的ClassPath路径\n\t */\n\tprivate void scanJavaClassPaths() {\n\t\tfinal String[] javaClassPaths = ClassUtil.getJavaClassPaths();\n\t\tfor (String classPath : javaClassPaths) {\n\t\t\t// bug修复，由于路径中空格和中文导致的Jar找不到\n\t\t\tclassPath = URLUtil.decode(classPath, CharsetUtil.systemCharsetName());\n\n\t\t\tscanFile(new File(classPath), null);\n\t\t}\n\t}\n\n\t/**\n\t * 扫描文件或目录中的类\n\t *\n\t * @param file    文件或目录\n\t * @param rootDir 包名对应classpath绝对路径\n\t */\n\tprivate void scanFile(File file, String rootDir) {\n\t\tif (file.isFile()) {\n\t\t\tfinal String fileName = file.getAbsolutePath();\n\t\t\tif (fileName.endsWith(FileUtil.CLASS_EXT)) {\n\t\t\t\tfinal String className = fileName//\n\t\t\t\t\t\t// 8为classes长度，fileName.length() - 6为\".class\"的长度\n\t\t\t\t\t\t.substring(rootDir.length(), fileName.length() - 6)//\n\t\t\t\t\t\t.replace(File.separatorChar, CharUtil.DOT);//\n\t\t\t\t//加入满足条件的类\n\t\t\t\taddIfAccept(className);\n\t\t\t} else if (fileName.endsWith(FileUtil.JAR_FILE_EXT)) {\n\t\t\t\ttry {\n\t\t\t\t\tscanJar(new JarFile(file));\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (file.isDirectory()) {\n\t\t\tfinal File[] files = file.listFiles();\n\t\t\tif (null != files) {\n\t\t\t\tfor (File subFile : files) {\n\t\t\t\t\tscanFile(subFile, (null == rootDir) ? subPathBeforePackage(file) : rootDir);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 扫描jar包，扫描结束后关闭jar文件\n\t *\n\t * @param jar jar包\n\t */\n\tprivate void scanJar(JarFile jar) {\n\t\ttry{\n\t\t\tString name;\n\t\t\tfor (JarEntry entry : new EnumerationIter<>(jar.entries())) {\n\t\t\t\tname = StrUtil.removePrefix(entry.getName(), StrUtil.SLASH);\n\t\t\t\tif (StrUtil.isEmpty(packagePath) || name.startsWith(this.packagePath)) {\n\t\t\t\t\tif (name.endsWith(FileUtil.CLASS_EXT) && false == entry.isDirectory()) {\n\t\t\t\t\t\tfinal String className = name//\n\t\t\t\t\t\t\t.substring(0, name.length() - 6)//\n\t\t\t\t\t\t\t.replace(CharUtil.SLASH, CharUtil.DOT);//\n\t\t\t\t\t\taddIfAccept(loadClass(className));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(jar);\n\t\t}\n\t}\n\n\t/**\n\t * 加载类\n\t *\n\t * @param className 类名\n\t * @return 加载的类\n\t */\n\tprotected Class<?> loadClass(String className) {\n\t\tClassLoader loader = this.classLoader;\n\t\tif (null == loader) {\n\t\t\tloader = ClassLoaderUtil.getClassLoader();\n\t\t\tthis.classLoader = loader;\n\t\t}\n\n\t\tClass<?> clazz = null;\n\t\ttry {\n\t\t\tclazz = Class.forName(className, this.initialize, loader);\n\t\t} catch (NoClassDefFoundError | ClassNotFoundException e) {\n\t\t\t// 由于依赖库导致的类无法加载，直接跳过此类\n\t\t\tclassesOfLoadError.add(className);\n\t\t} catch (UnsupportedClassVersionError e) {\n\t\t\t// 版本导致的不兼容的类，跳过\n\t\t\tclassesOfLoadError.add(className);\n\t\t} catch (Throwable e) {\n\t\t\tif (false == this.ignoreLoadError) {\n\t\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t\t} else {\n\t\t\t\tclassesOfLoadError.add(className);\n\t\t\t}\n\t\t}\n\t\treturn clazz;\n\t}\n\n\t/**\n\t * 通过过滤器，是否满足接受此类的条件\n\t *\n\t * @param className 类名\n\t */\n\tprivate void addIfAccept(String className) {\n\t\tif (StrUtil.isBlank(className)) {\n\t\t\treturn;\n\t\t}\n\t\tint classLen = className.length();\n\t\tint packageLen = this.packageName.length();\n\t\tif (classLen == packageLen) {\n\t\t\t//类名和包名长度一致，用户可能传入的包名是类名\n\t\t\tif (className.equals(this.packageName)) {\n\t\t\t\taddIfAccept(loadClass(className));\n\t\t\t}\n\t\t} else if (classLen > packageLen) {\n\t\t\t//检查类名是否以指定包名为前缀，包名后加.（避免类似于cn.hutool.A和cn.hutool.ATest这类类名引起的歧义）\n\t\t\tif (\".\".equals(this.packageNameWithDot) || className.startsWith(this.packageNameWithDot)) {\n\t\t\t\taddIfAccept(loadClass(className));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 通过过滤器，是否满足接受此类的条件\n\t *\n\t * @param clazz 类\n\t */\n\tprivate void addIfAccept(Class<?> clazz) {\n\t\tif (null != clazz) {\n\t\t\tFilter<Class<?>> classFilter = this.classFilter;\n\t\t\tif (classFilter == null || classFilter.accept(clazz)) {\n\t\t\t\tthis.classes.add(clazz);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 截取文件绝对路径中包名之前的部分\n\t *\n\t * @param file 文件\n\t * @return 包名之前的部分\n\t */\n\tprivate String subPathBeforePackage(File file) {\n\t\tString filePath = file.getAbsolutePath();\n\t\tif (StrUtil.isNotEmpty(this.packageDirName)) {\n\t\t\tfilePath = StrUtil.subBefore(filePath, this.packageDirName, true);\n\t\t}\n\t\treturn StrUtil.addSuffixIfNot(filePath, File.separator);\n\t}\n\t// --------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ConsistentHash.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.lang.hash.Hash32;\nimport cn.hutool.core.util.HashUtil;\n\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n/**\n * 一致性Hash算法\n * 算法详解：http://blog.csdn.net/sparkliang/article/details/5279393\n * 算法实现：https://weblogs.java.net/blog/2007/11/27/consistent-hashing\n * @author xiaoleilu\n *\n * @param <T>\t节点类型\n */\npublic class ConsistentHash<T> implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** Hash计算对象，用于自定义hash算法 */\n\tHash32<Object> hashFunc;\n\t/** 复制的节点个数 */\n\tprivate final int numberOfReplicas;\n\t/** 一致性Hash环 */\n\tprivate final SortedMap<Integer, T> circle = new TreeMap<>();\n\n\t/**\n\t * 构造，使用Java默认的Hash算法\n\t * @param numberOfReplicas 复制的节点个数，增加每个节点的复制节点有利于负载均衡\n\t * @param nodes 节点对象\n\t */\n\tpublic ConsistentHash(int numberOfReplicas, Collection<T> nodes) {\n\t\tthis.numberOfReplicas = numberOfReplicas;\n\t\tthis.hashFunc = key -> {\n\t\t\t//默认使用FNV1hash算法\n\t\t\treturn HashUtil.fnvHash(key.toString());\n\t\t};\n\t\t//初始化节点\n\t\tfor (T node : nodes) {\n\t\t\tadd(node);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t * @param hashFunc hash算法对象\n\t * @param numberOfReplicas 复制的节点个数，增加每个节点的复制节点有利于负载均衡\n\t * @param nodes 节点对象\n\t */\n\tpublic ConsistentHash(Hash32<Object> hashFunc, int numberOfReplicas, Collection<T> nodes) {\n\t\tthis.numberOfReplicas = numberOfReplicas;\n\t\tthis.hashFunc = hashFunc;\n\t\t//初始化节点\n\t\tfor (T node : nodes) {\n\t\t\tadd(node);\n\t\t}\n\t}\n\n\t/**\n\t * 增加节点<br>\n\t * 每增加一个节点，就会在闭环上增加给定复制节点数<br>\n\t * 例如复制节点数是2，则每调用此方法一次，增加两个虚拟节点，这两个节点指向同一Node\n\t * 由于hash算法会调用node的toString方法，故按照toString去重\n\t * @param node 节点对象\n\t */\n\tpublic void add(T node) {\n\t\tfor (int i = 0; i < numberOfReplicas; i++) {\n\t\t\tcircle.put(hashFunc.hash32(node.toString() + i), node);\n\t\t}\n\t}\n\n\t/**\n\t * 移除节点的同时移除相应的虚拟节点\n\t * @param node 节点对象\n\t */\n\tpublic void remove(T node) {\n\t\tfor (int i = 0; i < numberOfReplicas; i++) {\n\t\t\tcircle.remove(hashFunc.hash32(node.toString() + i));\n\t\t}\n\t}\n\n\t/**\n\t * 获得一个最近的顺时针节点\n\t * @param key 为给定键取Hash，取得顺时针方向上最近的一个虚拟节点对应的实际节点\n\t * @return 节点对象\n\t */\n\tpublic T get(Object key) {\n\t\tif (circle.isEmpty()) {\n\t\t\treturn null;\n\t\t}\n\t\tint hash = hashFunc.hash32(key);\n\t\tif (false == circle.containsKey(hash)) {\n\t\t\tSortedMap<Integer, T> tailMap = circle.tailMap(hash);\t//返回此映射的部分视图，其键大于等于 hash\n\t\t\thash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();\n\t\t}\n\t\t//正好命中\n\t\treturn circle.get(hash);\n\t}\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Console.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Scanner;\n\nimport static java.lang.System.err;\nimport static java.lang.System.out;\n\n/**\n * 命令行（控制台）工具方法类<br>\n * 此类主要针对{@link System#out} 和 {@link System#err} 做封装。\n *\n * @author Looly\n */\n\npublic class Console {\n\n\tprivate static final String TEMPLATE_VAR = \"{}\";\n\n\t// --------------------------------------------------------------------------------- Log\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志\n\t */\n\tpublic static void log() {\n\t\tout.println();\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志<br>\n\t * 如果传入打印对象为{@link Throwable}对象，那么同时打印堆栈\n\t *\n\t * @param obj 要打印的对象\n\t */\n\tpublic static void log(Object obj) {\n\t\tif (obj instanceof Throwable) {\n\t\t\tfinal Throwable e = (Throwable) obj;\n\t\t\tlog(e, e.getMessage());\n\t\t} else {\n\t\t\tlog(TEMPLATE_VAR, obj);\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志<br>\n\t * 如果传入打印对象为{@link Throwable}对象，那么同时打印堆栈\n\t *\n\t * @param obj1      第一个要打印的对象\n\t * @param otherObjs 其它要打印的对象\n\t * @since 5.4.3\n\t */\n\tpublic static void log(Object obj1, Object... otherObjs) {\n\t\tif (ArrayUtil.isEmpty(otherObjs)) {\n\t\t\tlog(obj1);\n\t\t} else {\n\t\t\tlog(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1));\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志<br>\n\t * 当传入template无\"{}\"时，被认为非模板，直接打印多个参数以空格分隔\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t */\n\tpublic static void log(String template, Object... values) {\n\t\tif (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) {\n\t\t\tlogInternal(template, values);\n\t\t} else {\n\t\t\tlogInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template));\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志\n\t *\n\t * @param t        异常对象\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t */\n\tpublic static void log(Throwable t, String template, Object... values) {\n\t\tout.println(StrUtil.format(template, values));\n\t\tif (null != t) {\n\t\t\t//noinspection CallToPrintStackTrace\n\t\t\tt.printStackTrace(out);\n\t\t\tout.flush();\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t * @since 5.4.3\n\t */\n\tprivate static void logInternal(String template, Object... values) {\n\t\tlog(null, template, values);\n\t}\n\n\t// --------------------------------------------------------------------------------- print\n\n\t/**\n\t * 打印表格到控制台\n\t *\n\t * @param consoleTable 控制台表格\n\t * @since 5.4.5\n\t */\n\tpublic static void table(ConsoleTable consoleTable) {\n\t\tprint(consoleTable.toString());\n\t}\n\n\t/**\n\t * 同 System.out.print()方法，打印控制台日志\n\t *\n\t * @param obj 要打印的对象\n\t * @since 3.3.1\n\t */\n\tpublic static void print(Object obj) {\n\t\tprint(TEMPLATE_VAR, obj);\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志<br>\n\t * 如果传入打印对象为{@link Throwable}对象，那么同时打印堆栈\n\t *\n\t * @param obj1      第一个要打印的对象\n\t * @param otherObjs 其它要打印的对象\n\t * @since 5.4.3\n\t */\n\tpublic static void print(Object obj1, Object... otherObjs) {\n\t\tif (ArrayUtil.isEmpty(otherObjs)) {\n\t\t\tprint(obj1);\n\t\t} else {\n\t\t\tprint(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1));\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.print()方法，打印控制台日志\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t * @since 3.3.1\n\t */\n\tpublic static void print(String template, Object... values) {\n\t\tif (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) {\n\t\t\tprintInternal(template, values);\n\t\t} else {\n\t\t\tprintInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template));\n\t\t}\n\t}\n\n\t/**\n\t * 打印进度条\n\t *\n\t * @param showChar 进度条提示字符，例如“#”\n\t * @param len      打印长度\n\t * @since 4.5.6\n\t */\n\tpublic static void printProgress(char showChar, int len) {\n\t\tprint(\"{}{}\", CharUtil.CR, StrUtil.repeat(showChar, len));\n\t}\n\n\t/**\n\t * 打印进度条\n\t *\n\t * @param showChar 进度条提示字符，例如“#”\n\t * @param totalLen 总长度\n\t * @param rate     总长度所占比取值0~1\n\t * @since 4.5.6\n\t */\n\tpublic static void printProgress(char showChar, int totalLen, double rate) {\n\t\tAssert.isTrue(rate >= 0 && rate <= 1, \"Rate must between 0 and 1 (both include)\");\n\t\tprintProgress(showChar, (int) (totalLen * rate));\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t * @since 5.4.3\n\t */\n\tprivate static void printInternal(String template, Object... values) {\n\t\tout.print(StrUtil.format(template, values));\n\t}\n\n\t// --------------------------------------------------------------------------------- Error\n\n\t/**\n\t * 同 System.err.println()方法，打印控制台日志\n\t */\n\tpublic static void error() {\n\t\terr.println();\n\t}\n\n\t/**\n\t * 同 System.err.println()方法，打印控制台日志\n\t *\n\t * @param obj 要打印的对象\n\t */\n\tpublic static void error(Object obj) {\n\t\tif (obj instanceof Throwable) {\n\t\t\tThrowable e = (Throwable) obj;\n\t\t\terror(e, e.getMessage());\n\t\t} else {\n\t\t\terror(TEMPLATE_VAR, obj);\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.out.println()方法，打印控制台日志<br>\n\t * 如果传入打印对象为{@link Throwable}对象，那么同时打印堆栈\n\t *\n\t * @param obj1      第一个要打印的对象\n\t * @param otherObjs 其它要打印的对象\n\t * @since 5.4.3\n\t */\n\tpublic static void error(Object obj1, Object... otherObjs) {\n\t\tif (ArrayUtil.isEmpty(otherObjs)) {\n\t\t\terror(obj1);\n\t\t} else {\n\t\t\terror(buildTemplateSplitBySpace(otherObjs.length + 1), ArrayUtil.insert(otherObjs, 0, obj1));\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.err.println()方法，打印控制台日志\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t */\n\tpublic static void error(String template, Object... values) {\n\t\tif (ArrayUtil.isEmpty(values) || StrUtil.contains(template, TEMPLATE_VAR)) {\n\t\t\terrorInternal(template, values);\n\t\t} else {\n\t\t\terrorInternal(buildTemplateSplitBySpace(values.length + 1), ArrayUtil.insert(values, 0, template));\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.err.println()方法，打印控制台日志\n\t *\n\t * @param t        异常对象\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t */\n\tpublic static void error(Throwable t, String template, Object... values) {\n\t\terr.println(StrUtil.format(template, values));\n\t\tif (null != t) {\n\t\t\tt.printStackTrace(err);\n\t\t\terr.flush();\n\t\t}\n\t}\n\n\t/**\n\t * 同 System.err.println()方法，打印控制台日志\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示\n\t * @param values   值\n\t */\n\tprivate static void errorInternal(String template, Object... values) {\n\t\terror(null, template, values);\n\t}\n\n\t// --------------------------------------------------------------------------------- in\n\n\t/**\n\t * 创建从控制台读取内容的{@link Scanner}\n\t *\n\t * @return {@link Scanner}\n\t * @since 3.3.1\n\t */\n\tpublic static Scanner scanner() {\n\t\treturn new Scanner(System.in);\n\t}\n\n\t/**\n\t * 读取用户输入的内容（在控制台敲回车前的内容）\n\t *\n\t * @return 用户输入的内容\n\t * @since 3.3.1\n\t */\n\tpublic static String input() {\n\t\treturn scanner().nextLine();\n\t}\n\n\t// --------------------------------------------------------------------------------- console lineNumber\n\n\t/**\n\t * 返回当前位置+行号 (不支持Lambda、内部类、递归内使用)\n\t *\n\t * @return 返回当前行号\n\t * @author dahuoyzs\n\t * @since 5.2.5\n\t */\n\tpublic static String where() {\n\t\tfinal StackTraceElement stackTraceElement = new Throwable().getStackTrace()[1];\n\t\tfinal String className = stackTraceElement.getClassName();\n\t\tfinal String methodName = stackTraceElement.getMethodName();\n\t\tfinal String fileName = stackTraceElement.getFileName();\n\t\tfinal Integer lineNumber = stackTraceElement.getLineNumber();\n\t\treturn String.format(\"%s.%s(%s:%s)\", className, methodName, fileName, lineNumber);\n\t}\n\n\t/**\n\t * 返回当前行号 (不支持Lambda、内部类、递归内使用)\n\t *\n\t * @return 返回当前行号\n\t * @since 5.2.5\n\t */\n\tpublic static Integer lineNumber() {\n\t\treturn new Throwable().getStackTrace()[1].getLineNumber();\n\t}\n\n\t/**\n\t * 构建空格分隔的模板，类似于\"{} {} {} {}\"\n\t *\n\t * @param count 变量数量\n\t * @return 模板\n\t */\n\tprivate static String buildTemplateSplitBySpace(int count) {\n\t\treturn StrUtil.repeatAndJoin(TEMPLATE_VAR, count, StrUtil.SPACE);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ConsoleTable.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 控制台打印表格工具\n *\n * @author 孙宇\n * @since 5.4.4\n */\npublic class ConsoleTable {\n\n\tprivate static final char ROW_LINE = '－';\n\tprivate static final char COLUMN_LINE = '|';\n\tprivate static final char CORNER = '+';\n\tprivate static final char SPACE = '\\u3000';\n\tprivate static final char LF = CharUtil.LF;\n\n\tprivate boolean isSBCMode = true;\n\n\t/**\n\t * 创建ConsoleTable对象\n\t *\n\t * @return ConsoleTable\n\t * @since 5.4.5\n\t */\n\tpublic static ConsoleTable create() {\n\t\treturn new ConsoleTable();\n\t}\n\n\t/**\n\t * 表格头信息\n\t */\n\tprivate final List<List<String>> headerList = new ArrayList<>();\n\t/**\n\t * 表格体信息\n\t */\n\tprivate final List<List<String>> bodyList = new ArrayList<>();\n\t/**\n\t * 每列最大字符个数\n\t */\n\tprivate List<Integer> columnCharNumber;\n\n\t/**\n\t * 设置是否使用全角模式<br>\n\t * 当包含中文字符时，输出的表格可能无法对齐，因此当设置为全角模式时，全部字符转为全角。\n\t *\n\t * @param isSBCMode 是否全角模式\n\t * @return this\n\t * @since 5.8.0\n\t */\n\tpublic ConsoleTable setSBCMode(boolean isSBCMode) {\n\t\tthis.isSBCMode = isSBCMode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加头信息\n\t *\n\t * @param titles 列名\n\t * @return 自身对象\n\t */\n\tpublic ConsoleTable addHeader(String... titles) {\n\t\tif (columnCharNumber == null) {\n\t\t\tcolumnCharNumber = new ArrayList<>(Collections.nCopies(titles.length, 0));\n\t\t}\n\t\tList<String> l = new ArrayList<>();\n\t\tfillColumns(l, titles);\n\t\theaderList.add(l);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加体信息\n\t *\n\t * @param values 列值\n\t * @return 自身对象\n\t */\n\tpublic ConsoleTable addBody(String... values) {\n\t\tList<String> l = new ArrayList<>();\n\t\tbodyList.add(l);\n\t\tfillColumns(l, values);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 填充表格头或者体\n\t *\n\t * @param l       被填充列表\n\t * @param columns 填充内容\n\t */\n\tprivate void fillColumns(List<String> l, String[] columns) {\n\t\tfor (int i = 0; i < columns.length; i++) {\n\t\t\tString column = StrUtil.toString(columns[i]);\n\t\t\tif (isSBCMode) {\n\t\t\t\tcolumn = Convert.toSBC(column);\n\t\t\t}\n\t\t\tl.add(column);\n\t\t\tint width = column.length();\n\t\t\tif (width > columnCharNumber.get(i)) {\n\t\t\t\tcolumnCharNumber.set(i, width);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取表格字符串\n\t *\n\t * @return 表格字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfillBorder(sb);\n\t\tfillRows(sb, headerList);\n\t\tfillBorder(sb);\n\t\tfillRows(sb, bodyList);\n\t\tfillBorder(sb);\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 填充表头或者表体信息（多行）\n\t *\n\t * @param sb   内容\n\t * @param list 表头列表或者表体列表\n\t */\n\tprivate void fillRows(StringBuilder sb, List<List<String>> list) {\n\t\tfor (List<String> row : list) {\n\t\t\tsb.append(COLUMN_LINE);\n\t\t\tfillRow(sb, row);\n\t\t\tsb.append(LF);\n\t\t}\n\t}\n\n\t/**\n\t * 填充一行数据\n\t *\n\t * @param sb  内容\n\t * @param row 一行数据\n\t */\n\tprivate void fillRow(StringBuilder sb, List<String> row) {\n\t\tfinal int size = row.size();\n\t\tString value;\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tvalue = row.get(i);\n\t\t\tsb.append(SPACE);\n\t\t\tsb.append(value);\n\t\t\tfinal int length = value.length();\n\t\t\tfinal int sbcCount = sbcCount(value);\n\t\t\tif(sbcCount % 2 == 1){\n\t\t\t\tsb.append(CharUtil.SPACE);\n\t\t\t}\n\t\t\tsb.append(SPACE);\n\t\t\tint maxLength = columnCharNumber.get(i);\n\t\t\tfor (int j = 0; j < (maxLength - length + (sbcCount / 2)); j++) {\n\t\t\t\tsb.append(SPACE);\n\t\t\t}\n\t\t\tsb.append(COLUMN_LINE);\n\t\t}\n\t}\n\n\t/**\n\t * 拼装边框\n\t *\n\t * @param sb StringBuilder\n\t */\n\tprivate void fillBorder(StringBuilder sb) {\n\t\tsb.append(CORNER);\n\t\tfor (Integer width : columnCharNumber) {\n\t\t\tsb.append(StrUtil.repeat(ROW_LINE, width + 2));\n\t\t\tsb.append(CORNER);\n\t\t}\n\t\tsb.append(LF);\n\t}\n\n\t/**\n\t * 打印到控制台\n\t */\n\tpublic void print() {\n\t\tConsole.print(toString());\n\t}\n\n\t/**\n\t * 半角字符数量\n\t *\n\t * @param value 字符串\n\t * @return 填充空格数量\n\t */\n\tprivate int sbcCount(String value) {\n\t\tint count = 0;\n\t\tfor (int i = 0; i < value.length(); i++) {\n\t\t\tif (value.charAt(i) < '\\177') {\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\n\t\treturn count;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 片段默认实现\n *\n * @param <T> 数字类型，用于表示位置index\n * @author looly\n * @since 5.5.3\n */\npublic class DefaultSegment<T extends Number> implements Segment<T> {\n\n\tprotected T startIndex;\n\tprotected T endIndex;\n\n\t/**\n\t * 构造\n\t * @param startIndex 起始位置\n\t * @param endIndex 结束位置\n\t */\n\tpublic DefaultSegment(T startIndex, T endIndex) {\n\t\tthis.startIndex = startIndex;\n\t\tthis.endIndex = endIndex;\n\t}\n\n\t@Override\n\tpublic T getStartIndex() {\n\t\treturn this.startIndex;\n\t}\n\n\t@Override\n\tpublic T getEndIndex() {\n\t\treturn this.endIndex;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Dict.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.getter.BasicTypeGetter;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.lang.func.LambdaUtil;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.sql.Time;\nimport java.sql.Timestamp;\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\n/**\n * 字典对象，扩充了HashMap中的方法\n *\n * @author loolly\n */\npublic class Dict extends LinkedHashMap<String, Object> implements BasicTypeGetter<String> {\n\tprivate static final long serialVersionUID = 6135423866861206530L;\n\n\tstatic final float DEFAULT_LOAD_FACTOR = 0.75f;\n\tstatic final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16\n\n\t/**\n\t * 是否大小写不敏感\n\t */\n\tprivate boolean caseInsensitive;\n\n\t// --------------------------------------------------------------- Static method start\n\n\t/**\n\t * 创建Dict\n\t *\n\t * @return Dict\n\t */\n\tpublic static Dict create() {\n\t\treturn new Dict();\n\t}\n\n\t/**\n\t * 将PO对象转为Dict\n\t *\n\t * @param <T>  Bean类型\n\t * @param bean Bean对象\n\t * @return Vo\n\t */\n\tpublic static <T> Dict parse(T bean) {\n\t\treturn create().parseBean(bean);\n\t}\n\n\t/**\n\t * 根据给定的Pair数组创建Dict对象\n\t *\n\t * @param pairs 键值对\n\t * @return Dict\n\t * @since 5.4.1\n\t */\n\t@SafeVarargs\n\tpublic static Dict of(Pair<String, Object>... pairs) {\n\t\tfinal Dict dict = create();\n\t\tfor (Pair<String, Object> pair : pairs) {\n\t\t\tdict.put(pair.getKey(), pair.getValue());\n\t\t}\n\t\treturn dict;\n\t}\n\n\t/**\n\t * 根据给定的键值对数组创建Dict对象，传入参数必须为key,value,key,value...\n\t *\n\t * <p>奇数参数必须为key，key最后会转换为String类型。</p>\n\t * <p>偶数参数必须为value，可以为任意类型。</p>\n\t *\n\t * <pre>\n\t * Dict dict = Dict.of(\n\t * \t\"RED\", \"#FF0000\",\n\t * \t\"GREEN\", \"#00FF00\",\n\t * \t\"BLUE\", \"#0000FF\"\n\t * );\n\t * </pre>\n\t *\n\t * @param keysAndValues 键值对列表，必须奇数参数为key，偶数参数为value\n\t * @return Dict\n\t * @since 5.4.1\n\t */\n\tpublic static Dict of(Object... keysAndValues) {\n\t\tfinal Dict dict = create();\n\n\t\tString key = null;\n\t\tfor (int i = 0; i < keysAndValues.length; i++) {\n\t\t\tif (i % 2 == 0) {\n\t\t\t\tkey = Convert.toStr(keysAndValues[i]);\n\t\t\t} else {\n\t\t\t\tdict.put(key, keysAndValues[i]);\n\t\t\t}\n\t\t}\n\n\t\treturn dict;\n\t}\n\t// --------------------------------------------------------------- Static method end\n\n\t// --------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic Dict() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic Dict(boolean caseInsensitive) {\n\t\tthis(DEFAULT_INITIAL_CAPACITY, caseInsensitive);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t */\n\tpublic Dict(int initialCapacity) {\n\t\tthis(initialCapacity, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic Dict(int initialCapacity, boolean caseInsensitive) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR, caseInsensitive);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param loadFactor      容量增长因子，0~1，即达到容量的百分之多少时扩容\n\t */\n\tpublic Dict(int initialCapacity, float loadFactor) {\n\t\tthis(initialCapacity, loadFactor, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param loadFactor      容量增长因子，0~1，即达到容量的百分之多少时扩容\n\t * @param caseInsensitive 是否大小写不敏感\n\t * @since 4.5.16\n\t */\n\tpublic Dict(int initialCapacity, float loadFactor, boolean caseInsensitive) {\n\t\tsuper(initialCapacity, loadFactor);\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic Dict(Map<String, Object> m) {\n\t\tsuper((null == m) ? new HashMap<>() : m);\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 转换为Bean对象\n\t *\n\t * @param <T>  Bean类型\n\t * @param bean Bean\n\t * @return Bean\n\t */\n\tpublic <T> T toBean(T bean) {\n\t\treturn toBean(bean, false);\n\t}\n\n\t/**\n\t * 转换为Bean对象\n\t *\n\t * @param <T>  Bean类型\n\t * @param bean Bean\n\t * @return Bean\n\t * @since 3.3.1\n\t */\n\tpublic <T> T toBeanIgnoreCase(T bean) {\n\t\tBeanUtil.fillBeanWithMapIgnoreCase(this, bean, false);\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 转换为Bean对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param bean          Bean\n\t * @param isToCamelCase 是否转换为驼峰模式\n\t * @return Bean\n\t */\n\tpublic <T> T toBean(T bean, boolean isToCamelCase) {\n\t\tBeanUtil.fillBeanWithMap(this, bean, isToCamelCase, false);\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 转换为Bean对象,并使用驼峰法模式转换\n\t *\n\t * @param <T>  Bean类型\n\t * @param bean Bean\n\t * @return Bean\n\t */\n\tpublic <T> T toBeanWithCamelCase(T bean) {\n\t\tBeanUtil.fillBeanWithMap(this, bean, true, false);\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 填充Value Object对象\n\t *\n\t * @param <T>   Bean类型\n\t * @param clazz Value Object（或者POJO）的类\n\t * @return vo\n\t */\n\tpublic <T> T toBean(Class<T> clazz) {\n\t\treturn BeanUtil.toBean(this, clazz);\n\t}\n\n\t/**\n\t * 填充Value Object对象，忽略大小写\n\t *\n\t * @param <T>   Bean类型\n\t * @param clazz Value Object（或者POJO）的类\n\t * @return vo\n\t */\n\tpublic <T> T toBeanIgnoreCase(Class<T> clazz) {\n\t\treturn BeanUtil.toBeanIgnoreCase(this, clazz, false);\n\t}\n\n\t/**\n\t * 将值对象转换为Dict<br>\n\t * 类名会被当作表名，小写第一个字母\n\t *\n\t * @param <T>  Bean类型\n\t * @param bean 值对象\n\t * @return 自己\n\t */\n\tpublic <T> Dict parseBean(T bean) {\n\t\tAssert.notNull(bean, \"Bean class must be not null\");\n\t\tthis.putAll(BeanUtil.beanToMap(bean));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将值对象转换为Dict<br>\n\t * 类名会被当作表名，小写第一个字母\n\t *\n\t * @param <T>               Bean类型\n\t * @param bean              值对象\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue   是否忽略值为空的字段\n\t * @return 自己\n\t */\n\tpublic <T> Dict parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\tAssert.notNull(bean, \"Bean class must be not null\");\n\t\tthis.putAll(BeanUtil.beanToMap(bean, isToUnderlineCase, ignoreNullValue));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 与给定实体对比并去除相同的部分<br>\n\t * 此方法用于在更新操作时避免所有字段被更新，跳过不需要更新的字段 version from 2.0.0\n\t *\n\t * @param <T>          字典对象类型\n\t * @param dict         字典对象\n\t * @param withoutNames 不需要去除的字段名\n\t */\n\tpublic <T extends Dict> void removeEqual(T dict, String... withoutNames) {\n\t\tHashSet<String> withoutSet = CollUtil.newHashSet(withoutNames);\n\t\tfor (Map.Entry<String, Object> entry : dict.entrySet()) {\n\t\t\tif (withoutSet.contains(entry.getKey())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfinal Object value = this.get(entry.getKey());\n\t\t\tif (Objects.equals(value, entry.getValue())) {\n\t\t\t\tthis.remove(entry.getKey());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 过滤Map保留指定键值对，如果键不存在跳过\n\t *\n\t * @param keys 键列表\n\t * @return Dict 结果\n\t * @since 4.0.10\n\t */\n\tpublic Dict filter(String... keys) {\n\t\tfinal Dict result = new Dict(keys.length, 1);\n\n\t\tfor (String key : keys) {\n\t\t\tif (this.containsKey(key)) {\n\t\t\t\tresult.put(key, this.get(key));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t// -------------------------------------------------------------------- Set start\n\n\t/**\n\t * 设置列\n\t *\n\t * @param attr  属性\n\t * @param value 值\n\t * @return 本身\n\t */\n\tpublic Dict set(String attr, Object value) {\n\t\tthis.put(attr, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置列，当键或值为null时忽略\n\t *\n\t * @param attr  属性\n\t * @param value 值\n\t * @return 本身\n\t */\n\tpublic Dict setIgnoreNull(String attr, Object value) {\n\t\tif (null != attr && null != value) {\n\t\t\tset(attr, value);\n\t\t}\n\t\treturn this;\n\t}\n\t// -------------------------------------------------------------------- Set end\n\n\t// -------------------------------------------------------------------- Get start\n\n\t@Override\n\tpublic Object getObj(String key) {\n\t\treturn super.get(key);\n\t}\n\n\t/**\n\t * 获得特定类型值\n\t *\n\t * @param <T>  值类型\n\t * @param attr 字段名\n\t * @return 字段值\n\t * @since 4.6.3\n\t */\n\tpublic <T> T getBean(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * 获得特定类型值\n\t *\n\t * @param <T>          值类型\n\t * @param attr         字段名\n\t * @param defaultValue 默认值\n\t * @return 字段值\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T get(String attr, T defaultValue) {\n\t\tfinal Object result = get(attr);\n\t\treturn (T) (result != null ? result : defaultValue);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic String getStr(String attr) {\n\t\treturn Convert.toStr(get(attr), null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic Integer getInt(String attr) {\n\t\treturn Convert.toInt(get(attr), null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic Long getLong(String attr) {\n\t\treturn Convert.toLong(get(attr), null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic Float getFloat(String attr) {\n\t\treturn Convert.toFloat(get(attr), null);\n\t}\n\n\t@Override\n\tpublic Short getShort(String attr) {\n\t\treturn Convert.toShort(get(attr), null);\n\t}\n\n\t@Override\n\tpublic Character getChar(String attr) {\n\t\treturn Convert.toChar(get(attr), null);\n\t}\n\n\t@Override\n\tpublic Double getDouble(String attr) {\n\t\treturn Convert.toDouble(get(attr), null);\n\t}\n\n\t@Override\n\tpublic Byte getByte(String attr) {\n\t\treturn Convert.toByte(get(attr), null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic Boolean getBool(String attr) {\n\t\treturn Convert.toBool(get(attr), null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic BigDecimal getBigDecimal(String attr) {\n\t\treturn Convert.toBigDecimal(get(attr));\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic BigInteger getBigInteger(String attr) {\n\t\treturn Convert.toBigInteger(get(attr));\n\t}\n\n\t@Override\n\tpublic <E extends Enum<E>> E getEnum(Class<E> clazz, String key) {\n\t\treturn Convert.toEnum(clazz, get(key));\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\tpublic byte[] getBytes(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\t@Override\n\tpublic Date getDate(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\tpublic Time getTime(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\tpublic Timestamp getTimestamp(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * @param attr 字段名\n\t * @return 字段值\n\t */\n\tpublic Number getNumber(String attr) {\n\t\treturn get(attr, null);\n\t}\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param <T>        目标类型\n\t * @param expression 表达式\n\t * @return 对象\n\t * @see BeanPath#get(Object)\n\t * @since 5.7.14\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getByPath(String expression) {\n\t\treturn (T) BeanPath.create(expression).get(this);\n\t}\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t * <p>\n\t * 获取表达式对应值后转换为对应类型的值\n\t *\n\t * @param <T>        返回值类型\n\t * @param expression 表达式\n\t * @param resultType 返回值类型\n\t * @return 对象\n\t * @see BeanPath#get(Object)\n\t * @since 5.7.14\n\t */\n\tpublic <T> T getByPath(String expression, Class<T> resultType) {\n\t\treturn Convert.convert(resultType, getByPath(expression));\n\t}\n\t// -------------------------------------------------------------------- Get end\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn super.containsKey(customKey((String) key));\n\t}\n\n\t@Override\n\tpublic Object get(Object key) {\n\t\treturn super.get(customKey((String) key));\n\t}\n\n\t@Override\n\tpublic Object put(String key, Object value) {\n\t\treturn super.put(customKey(key), value);\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends String, ?> m) {\n\t\tm.forEach(this::put);\n\t}\n\n\t@Override\n\tpublic Dict clone() {\n\t\treturn (Dict) super.clone();\n\t}\n\n\t@Override\n\tpublic Object remove(Object key) {\n\t\treturn super.remove(customKey((String) key));\n\t}\n\n\t@Override\n\tpublic boolean remove(Object key, Object value) {\n\t\treturn super.remove(customKey((String) key), value);\n\t}\n\n\t@Override\n\tpublic boolean replace(String key, Object oldValue, Object newValue) {\n\t\treturn super.replace(customKey(key), oldValue, newValue);\n\t}\n\n\t@Override\n\tpublic Object replace(String key, Object value) {\n\t\treturn super.replace(customKey(key), value);\n\t}\n\n\t//---------------------------------------------------------------------------- Override default methods start\n\t@Override\n\tpublic Object getOrDefault(Object key, Object defaultValue) {\n\t\treturn super.getOrDefault(customKey((String) key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Object computeIfPresent(final String key, final BiFunction<? super String, ? super Object, ?> remappingFunction) {\n\t\treturn super.computeIfPresent(customKey(key), remappingFunction);\n\t}\n\n\t@Override\n\tpublic Object compute(final String key, final BiFunction<? super String, ? super Object, ?> remappingFunction) {\n\t\treturn super.compute(customKey(key), remappingFunction);\n\t}\n\n\t@Override\n\tpublic Object merge(final String key, final Object value, final BiFunction<? super Object, ? super Object, ?> remappingFunction) {\n\t\treturn super.merge(customKey(key), value, remappingFunction);\n\t}\n\n\t@Override\n\tpublic Object putIfAbsent(String key, Object value) {\n\t\treturn super.putIfAbsent(customKey(key), value);\n\t}\n\n\t@Override\n\tpublic Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) {\n\t\treturn super.computeIfAbsent(customKey(key), mappingFunction);\n\t}\n\n\t//---------------------------------------------------------------------------- Override default methods end\n\n\t/**\n\t * 将Key转为小写\n\t *\n\t * @param key KEY\n\t * @return 小写KEY\n\t */\n\tprotected String customKey(String key) {\n\t\tif (this.caseInsensitive && null != key) {\n\t\t\tkey = key.toLowerCase();\n\t\t}\n\t\treturn key;\n\t}\n\n\t/**\n\t * 通过lambda批量设置值<br>\n\t * 实际使用时，可以使用getXXX的方法引用来完成键值对的赋值：\n\t * <pre>\n\t *     User user = GenericBuilder.of(User::new).with(User::setUsername, \"hutool\").build();\n\t *     Dict.create().setFields(user::getNickname, user::getUsername);\n\t * </pre>\n\t *\n\t * @param fields lambda,不能为空\n\t * @return this\n\t * @since 5.7.23\n\t */\n\tpublic Dict setFields(Func0<?>... fields) {\n\t\tArrays.stream(fields).forEach(f -> set(LambdaUtil.getFieldName(f), f.callWithRuntimeException()));\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Editor.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 编辑器接口，常用于对于集合中的元素做统一编辑<br>\n * 此编辑器两个作用：\n *\n * <pre>\n * 1、如果返回值为{@code null}，表示此值被抛弃\n * 2、对对象做修改\n * </pre>\n *\n * @param <T> 被编辑对象类型\n * @author Looly\n */\n@FunctionalInterface\npublic interface Editor<T> {\n\t/**\n\t * 修改过滤后的结果\n\t *\n\t * @param t 被过滤的对象\n\t * @return 修改后的对象，如果被过滤返回{@code null}\n\t */\n\tT edit(T t);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/EnumItem.java",
    "content": "package cn.hutool.core.lang;\n\nimport java.io.Serializable;\n\n/**\n * 枚举元素通用接口，在自定义枚举上实现此接口可以用于数据转换<br>\n * 数据库保存时建议保存 intVal()而非ordinal()防备需求变更<br>\n *\n * @param <E> Enum类型\n * @author nierjia\n * @since 5.4.2\n */\npublic interface EnumItem<E extends EnumItem<E>> extends Serializable {\n\n\tString name();\n\n\t/**\n\t * 在中文语境下，多数时间枚举会配合一个中文说明\n\t *\n\t * @return enum名\n\t */\n\tdefault String text() {\n\t\treturn name();\n\t}\n\n\tint intVal();\n\n\t/**\n\t * 获取所有枚举对象\n\t *\n\t * @return 枚举对象数组\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault E[] items() {\n\t\treturn (E[]) this.getClass().getEnumConstants();\n\t}\n\n\t/**\n\t * 通过int类型值查找兄弟其他枚举\n\t *\n\t * @param intVal int值\n\t * @return Enum\n\t */\n\tdefault E fromInt(Integer intVal) {\n\t\tif (intVal == null) {\n\t\t\treturn null;\n\t\t}\n\t\tE[] vs = items();\n\t\tfor (E enumItem : vs) {\n\t\t\tif (enumItem.intVal() == intVal) {\n\t\t\t\treturn enumItem;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 通过String类型的值转换，根据实现可以用name/text\n\t *\n\t * @param strVal String值\n\t * @return Enum\n\t */\n\tdefault E fromStr(String strVal) {\n\t\tif (strVal == null) {\n\t\t\treturn null;\n\t\t}\n\t\tE[] vs = items();\n\t\tfor (E enumItem : vs) {\n\t\t\tif (strVal.equalsIgnoreCase(enumItem.name())) {\n\t\t\t\treturn enumItem;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\n}\n\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Filter.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 过滤器接口\n *\n * @author Looly\n */\n@FunctionalInterface\npublic interface Filter<T> {\n\t/**\n\t * 是否接受对象\n\t *\n\t * @param t 检查的对象\n\t * @return 是否接受对象\n\t */\n\tboolean accept(T t);\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/JarClassLoader.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.List;\n\n/**\n * 外部Jar的类加载器\n *\n * @author Looly\n */\npublic class JarClassLoader extends URLClassLoader {\n\n\t/**\n\t * 加载Jar到ClassPath\n\t *\n\t * @param dir jar文件或所在目录\n\t * @return JarClassLoader\n\t */\n\tpublic static JarClassLoader load(File dir) {\n\t\tfinal JarClassLoader loader = new JarClassLoader();\n\t\tloader.addJar(dir);//查找加载所有jar\n\t\tloader.addURL(dir);//查找加载所有class\n\t\treturn loader;\n\t}\n\n\t/**\n\t * 加载Jar到ClassPath\n\t *\n\t * @param jarFile jar文件或所在目录\n\t * @return JarClassLoader\n\t */\n\tpublic static JarClassLoader loadJar(File jarFile) {\n\t\tfinal JarClassLoader loader = new JarClassLoader();\n\t\tloader.addJar(jarFile);\n\t\treturn loader;\n\t}\n\n\t/**\n\t * 加载Jar文件到指定loader中\n\t *\n\t * @param loader  {@link URLClassLoader}\n\t * @param jarFile 被加载的jar\n\t * @throws UtilException IO异常包装和执行异常\n\t */\n\tpublic static void loadJar(URLClassLoader loader, File jarFile) throws UtilException {\n\t\ttry {\n\t\t\tfinal Method method = ClassUtil.getDeclaredMethod(URLClassLoader.class, \"addURL\", URL.class);\n\t\t\tif (null != method) {\n\t\t\t\tmethod.setAccessible(true);\n\t\t\t\tfinal List<File> jars = loopJar(jarFile);\n\t\t\t\tfor (File jar : jars) {\n\t\t\t\t\tReflectUtil.invoke(loader, method, jar.toURI().toURL());\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 加载Jar文件到System ClassLoader中\n\t *\n\t * @param jarFile 被加载的jar\n\t * @return System ClassLoader\n\t */\n\tpublic static URLClassLoader loadJarToSystemClassLoader(File jarFile) {\n\t\tURLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();\n\t\tloadJar(urlClassLoader, jarFile);\n\t\treturn urlClassLoader;\n\t}\n\n\t// ------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic JarClassLoader() {\n\t\tthis(new URL[]{});\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param urls 被加载的URL\n\t */\n\tpublic JarClassLoader(URL[] urls) {\n\t\tsuper(urls, ClassUtil.getClassLoader());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param urls        被加载的URL\n\t * @param classLoader 类加载器\n\t */\n\tpublic JarClassLoader(URL[] urls, ClassLoader classLoader) {\n\t\tsuper(urls, classLoader);\n\t}\n\t// ------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 加载Jar文件，或者加载目录\n\t *\n\t * @param jarFileOrDir jar文件或者jar文件所在目录\n\t * @return this\n\t */\n\tpublic JarClassLoader addJar(File jarFileOrDir) {\n\t\tif (isJarFile(jarFileOrDir)) {\n\t\t\treturn addURL(jarFileOrDir);\n\t\t}\n\t\tfinal List<File> jars = loopJar(jarFileOrDir);\n\t\tfor (File jar : jars) {\n\t\t\taddURL(jar);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void addURL(URL url) {\n\t\tsuper.addURL(url);\n\t}\n\n\t/**\n\t * 增加class所在目录或文件<br>\n\t * 如果为目录，此目录用于搜索class文件，如果为文件，需为jar文件\n\t *\n\t * @param dir 目录\n\t * @return this\n\t * @since 4.4.2\n\t */\n\tpublic JarClassLoader addURL(File dir) {\n\t\tsuper.addURL(URLUtil.getURL(dir));\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 递归获得Jar文件\n\t *\n\t * @param file jar文件或者包含jar文件的目录\n\t * @return jar文件列表\n\t */\n\tprivate static List<File> loopJar(File file) {\n\t\treturn FileUtil.loopFiles(file, JarClassLoader::isJarFile);\n\t}\n\n\t/**\n\t * 是否为jar文件\n\t *\n\t * @param file 文件\n\t * @return 是否为jar文件\n\t * @since 4.4.2\n\t */\n\tprivate static boolean isJarFile(File file) {\n\t\tif (false == FileUtil.isFile(file)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn file.getPath().toLowerCase().endsWith(\".jar\");\n\t}\n\t// ------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Matcher.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 匹配接口\n *\n * @param <T> 匹配的对象类型\n * @author Looly\n */\n@FunctionalInterface\npublic interface Matcher<T> {\n\t/**\n\t * 给定对象是否匹配\n\t *\n\t * @param t 对象\n\t * @return 是否匹配\n\t */\n\tboolean match(T t);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.RuntimeUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.net.NetworkInterface;\nimport java.nio.ByteBuffer;\nimport java.util.Enumeration;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * MongoDB ID生成策略实现<br>\n * ObjectId由以下几部分组成：\n *\n * <pre>\n * 1. Time 时间戳。\n * 2. Machine 所在主机的唯一标识符，一般是机器主机名的散列值。\n * 3. PID 进程ID。确保同一机器中不冲突\n * 4. INC 自增计数器。确保同一秒内产生objectId的唯一性。\n * </pre>\n *\n * <table summary=\"\" border=\"1\">\n *     <tr>\n *         <td>时间戳</td>\n *         <td>机器ID</td>\n *         <td>进程ID</td>\n *         <td>自增计数器</td>\n *     </tr>\n *     <tr>\n *         <td>4</td>\n *         <td>3</td>\n *         <td>2</td>\n *         <td>3</td>\n *     </tr>\n * </table>\n *\n * 参考：http://blog.csdn.net/qxc1281/article/details/54021882\n *\n * @author looly\n * @since 4.0.0\n *\n */\npublic class ObjectId {\n\n\t/** 线程安全的下一个随机数,每次生成自增+1 */\n\tprivate static final AtomicInteger NEXT_INC = new AtomicInteger(RandomUtil.randomInt());\n\t/** 机器信息 */\n\tprivate static final int MACHINE = getMachinePiece() | getProcessPiece();\n\n\t/**\n\t * 给定的字符串是否为有效的ObjectId\n\t *\n\t * @param s 字符串\n\t * @return 是否为有效的ObjectId\n\t */\n\tpublic static boolean isValid(String s) {\n\t\tif (s == null) {\n\t\t\treturn false;\n\t\t}\n\t\ts = StrUtil.removeAll(s, \"-\");\n\t\tfinal int len = s.length();\n\t\tif (len != 24) {\n\t\t\treturn false;\n\t\t}\n\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = s.charAt(i);\n\t\t\tif (c >= '0' && c <= '9') {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c >= 'a' && c <= 'f') {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c >= 'A' && c <= 'F') {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 获取一个objectId的bytes表现形式\n\t *\n\t * @return objectId\n\t * @since 4.1.15\n\t */\n\tpublic static byte[] nextBytes() {\n\t\tfinal ByteBuffer bb = ByteBuffer.wrap(new byte[12]);\n\t\tbb.putInt((int) DateUtil.currentSeconds());// 4位\n\t\tbb.putInt(MACHINE);// 4位\n\t\tbb.putInt(NEXT_INC.getAndIncrement());// 4位\n\n\t\treturn bb.array();\n\t}\n\n\t/**\n\t * 获取一个objectId用下划线分割\n\t *\n\t * @return objectId\n\t */\n\tpublic static String next() {\n\t\treturn next(false);\n\t}\n\n\t/**\n\t * 获取一个objectId\n\t *\n\t * @param withHyphen 是否包含分隔符\n\t * @return objectId\n\t */\n\tpublic static String next(boolean withHyphen) {\n\t\tbyte[] array = nextBytes();\n\t\tfinal StringBuilder buf = new StringBuilder(withHyphen ? 26 : 24);\n\t\tint t;\n\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\tif (withHyphen && i % 4 == 0 && i != 0) {\n\t\t\t\tbuf.append(\"-\");\n\t\t\t}\n\t\t\tt = array[i] & 0xff;\n\t\t\tif (t < 16) {\n\t\t\t\tbuf.append('0');\n\t\t\t}\n\t\t\tbuf.append(Integer.toHexString(t));\n\n\t\t}\n\t\treturn buf.toString();\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 获取机器码片段\n\t *\n\t * @return 机器码片段\n\t */\n\tprivate static int getMachinePiece() {\n\t\t// 机器码\n\t\tint machinePiece;\n\t\ttry {\n\t\t\tStringBuilder netSb = new StringBuilder();\n\t\t\t// 返回机器所有的网络接口\n\t\t\tEnumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();\n\t\t\t// 遍历网络接口\n\t\t\twhile (e.hasMoreElements()) {\n\t\t\t\tNetworkInterface ni = e.nextElement();\n\t\t\t\t// 网络接口信息\n\t\t\t\tnetSb.append(ni.toString());\n\t\t\t}\n\t\t\t// 保留后两位\n\t\t\tmachinePiece = netSb.toString().hashCode() << 16;\n\t\t} catch (Throwable e) {\n\t\t\t// 出问题随机生成,保留后两位\n\t\t\tmachinePiece = (RandomUtil.randomInt()) << 16;\n\t\t}\n\t\treturn machinePiece;\n\t}\n\n\t/**\n\t * 获取进程码片段\n\t *\n\t * @return 进程码片段\n\t */\n\tprivate static int getProcessPiece() {\n\t\t// 进程码\n\t\t// 因为静态变量类加载可能相同,所以要获取进程ID + 加载对象的ID值\n\t\tfinal int processPiece;\n\t\t// 进程ID初始化\n\t\tint processId;\n\t\ttry {\n\t\t\tprocessId = RuntimeUtil.getPid();\n\t\t} catch (Throwable t) {\n\t\t\tprocessId = RandomUtil.randomInt();\n\t\t}\n\n\t\tfinal ClassLoader loader = ClassLoaderUtil.getClassLoader();\n\t\t// 返回对象哈希码,无论是否重写hashCode方法\n\t\tint loaderId = (loader != null) ? System.identityHashCode(loader) : 0;\n\n\t\t// 进程ID + 对象加载ID\n\t\t// 保留前2位\n\t\tfinal String processSb = Integer.toHexString(processId) + Integer.toHexString(loaderId);\n\t\tprocessPiece = processSb.hashCode() & 0xFFFF;\n\n\t\treturn processPiece;\n\t}\n\t// ----------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Opt.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.lang.func.VoidFunc0;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Collection;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\n/**\n * 复制jdk16中的Optional，以及自己进行了一点调整和新增，比jdk8中的Optional多了几个实用的函数<br>\n * 详细见：<a href=\"https://gitee.com/chinabugotech/hutool/pulls/426\">https://gitee.com/chinabugotech/hutool/pulls/426</a>\n *\n * @param <T> 包裹里元素的类型\n * @author VampireAchao\n * @see java.util.Optional\n */\npublic class Opt<T> {\n\t/**\n\t * 一个空的{@code Opt}\n\t */\n\tprivate static final Opt<?> EMPTY = new Opt<>(null);\n\n\t/**\n\t * 返回一个空的{@code Opt}\n\t *\n\t * @param <T> 包裹里元素的类型\n\t * @return Opt\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Opt<T> empty() {\n\t\treturn (Opt<T>) EMPTY;\n\t}\n\n\t/**\n\t * 返回一个包裹里元素不可能为空的{@code Opt}\n\t *\n\t * @param value 包裹里的元素\n\t * @param <T>   包裹里元素的类型\n\t * @return 一个包裹里元素不可能为空的 {@code Opt}\n\t * @throws NullPointerException 如果传入的元素为空，抛出 {@code NPE}\n\t */\n\tpublic static <T> Opt<T> of(T value) {\n\t\treturn new Opt<>(Objects.requireNonNull(value));\n\t}\n\n\t/**\n\t * 返回一个包裹里元素可能为空的{@code Opt}\n\t *\n\t * @param value 传入需要包裹的元素\n\t * @param <T>   包裹里元素的类型\n\t * @return 一个包裹里元素可能为空的 {@code Opt}\n\t */\n\tpublic static <T> Opt<T> ofNullable(T value) {\n\t\treturn value == null ? empty()\n\t\t\t\t: new Opt<>(value);\n\t}\n\n\t/**\n\t * 返回一个包裹里元素可能为空的{@code Opt}，额外判断了空字符串的情况\n\t *\n\t * @param value 传入需要包裹的元素\n\t * @param <T>   包裹里元素的类型\n\t * @return 一个包裹里元素可能为空，或者为空字符串的 {@code Opt}\n\t */\n\tpublic static <T> Opt<T> ofBlankAble(T value) {\n\t\treturn StrUtil.isBlankIfStr(value) ? empty() : new Opt<>(value);\n\t}\n\n\t/**\n\t * 返回一个包裹里{@code List}集合可能为空的{@code Opt}，额外判断了集合内元素为空的情况\n\t *\n\t * @param <T>   包裹里元素的类型\n\t * @param <R>   集合值类型\n\t * @param value 传入需要包裹的元素，支持CharSequence、Map、Iterable、Iterator、Array类型\n\t * @return 一个包裹里元素可能为空的 {@code Opt}\n\t * @since 5.7.17\n\t */\n\tpublic static <T, R extends Collection<T>> Opt<R> ofEmptyAble(R value) {\n\t\treturn ObjectUtil.isEmpty(value) ? empty() : new Opt<>(value);\n\t}\n\n\t/**\n\t * @param supplier 操作\n\t * @param <T>      类型\n\t * @return 操作执行后的值\n\t */\n\tpublic static <T> Opt<T> ofTry(Func0<T> supplier) {\n\t\ttry {\n\t\t\treturn Opt.ofNullable(supplier.call());\n\t\t} catch (Exception e) {\n\t\t\tfinal Opt<T> empty = new Opt<>(null);\n\t\t\tempty.exception = e;\n\t\t\treturn empty;\n\t\t}\n\t}\n\n\t/**\n\t * 包裹里实际的元素\n\t */\n\tprivate final T value;\n\tprivate Exception exception;\n\n\t/**\n\t * {@code Opt}的构造函数\n\t *\n\t * @param value 包裹里的元素\n\t */\n\tprivate Opt(T value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 返回包裹里的元素，取不到则为{@code null}，注意！！！此处和{@link java.util.Optional#get()}不同的一点是本方法并不会抛出{@code NoSuchElementException}\n\t * 如果元素为空，则返回{@code null}，如果需要一个绝对不能为{@code null}的值，则使用{@link #orElseThrow()}\n\t *\n\t * <p>\n\t * 如果需要一个绝对不能为 {@code null}的值，则使用{@link #orElseThrow()}\n\t * 做此处修改的原因是，有时候我们确实需要返回一个null给前端，并且这样的时候并不少见\n\t * 而使用 {@code .orElse(null)}需要写整整12个字符，用{@code .get()}就只需要6个啦\n\t *\n\t * @return 包裹里的元素，有可能为{@code null}\n\t */\n\tpublic T get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 判断包裹里元素的值是否不存在，不存在为 {@code true}，否则为{@code false}\n\t *\n\t * @return 包裹里元素的值不存在 则为 {@code true}，否则为{@code false}\n\t * @since 11 这是jdk11{@link java.util.Optional}中的新函数\n\t */\n\tpublic boolean isEmpty() {\n\t\treturn value == null;\n\t}\n\n\t/**\n\t * 获取异常<br>\n\t * 当调用 {@link #ofTry(Func0)}时，异常信息不会抛出，而是保存，调用此方法获取抛出的异常\n\t *\n\t * @return 异常\n\t * @since 5.7.17\n\t */\n\tpublic Exception getException() {\n\t\treturn this.exception;\n\t}\n\n\t/**\n\t * 是否失败<br>\n\t * 当调用 {@link #ofTry(Func0)}时，抛出异常则表示失败\n\t *\n\t * @return 是否失败\n\t * @since 5.7.17\n\t */\n\tpublic boolean isFail() {\n\t\treturn null != this.exception;\n\t}\n\n\t/**\n\t * 如果包裹内容失败了，则执行传入的操作({@link Consumer#accept})\n\t *\n\t * <p> 例如执行有异常就打印结果\n\t * <pre>{@code\n\t *     Opt.ofTry(() -> 1 / 0).ifFail(Console::log);\n\t * }</pre>\n\t *\n\t * @param action 你想要执行的操作\n\t * @return this\n\t * @throws NullPointerException 如果包裹里的值存在，但你传入的操作为{@code null}时抛出\n\t */\n\tpublic Opt<T> ifFail(final Consumer<? super Throwable> action) throws NullPointerException {\n\t\tObjects.requireNonNull(action, \"action is null\");\n\n\t\tif (isFail()) {\n\t\t\taction.accept(this.exception);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 判断包裹里元素的值是否存在，存在为 {@code true}，否则为{@code false}\n\t *\n\t * @return 包裹里元素的值存在为 {@code true}，否则为{@code false}\n\t */\n\tpublic boolean isPresent() {\n\t\treturn value != null;\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的操作({@link Consumer#accept})\n\t *\n\t * <p> 例如如果值存在就打印结果\n\t * <pre>{@code\n\t * Opt.ofNullable(\"Hello Hutool!\").ifPresent(Console::log);\n\t * }</pre>\n\t *\n\t * @param action 你想要执行的操作\n\t * @return this\n\t * @throws NullPointerException 如果包裹里的值存在，但你传入的操作为{@code null}时抛出\n\t */\n\tpublic Opt<T> ifPresent(Consumer<? super T> action) {\n\t\tif (isPresent()) {\n\t\t\taction.accept(value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的值存在时的操作({@link Consumer#accept})\n\t * 否则执行传入的值不存在时的操作({@link VoidFunc0}中的{@link VoidFunc0#call()})\n\t *\n\t * <p>\n\t * 例如值存在就打印对应的值，不存在则用{@code Console.error}打印另一句字符串\n\t * <pre>{@code\n\t * Opt.ofNullable(\"Hello Hutool!\").ifPresentOrElse(Console::log, () -> Console.error(\"Ops!Something is wrong!\"));\n\t * }</pre>\n\t *\n\t * @param action      包裹里的值存在时的操作\n\t * @param emptyAction 包裹里的值不存在时的操作\n\t * @return this;\n\t * @throws NullPointerException 如果包裹里的值存在时，执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null}，则抛出{@code NPE}\n\t */\n\tpublic Opt<T> ifPresentOrElse(Consumer<? super T> action, VoidFunc0 emptyAction) {\n\t\tif (isPresent()) {\n\t\t\taction.accept(value);\n\t\t} else {\n\t\t\temptyAction.callWithRuntimeException();\n\t\t}\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的值存在时的操作({@link Function#apply(Object)})支持链式调用、转换为其他类型\n\t * 否则执行传入的值不存在时的操作({@link VoidFunc0}中的{@link VoidFunc0#call()})\n\t *\n\t * <p>\n\t * 如果值存在就转换为大写，否则用{@code Console.error}打印另一句字符串\n\t * <pre>{@code\n\t * String hutool = Opt.ofBlankAble(\"hutool\").mapOrElse(String::toUpperCase, () -> Console.log(\"yes\")).mapOrElse(String::intern, () -> Console.log(\"Value is not present~\")).get();\n\t * }</pre>\n\t *\n\t * @param <U>         map后新的类型\n\t * @param mapper      包裹里的值存在时的操作\n\t * @param emptyAction 包裹里的值不存在时的操作\n\t * @return 新的类型的Opt\n\t * @throws NullPointerException 如果包裹里的值存在时，执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null}，则抛出{@code NPE}\n\t */\n\tpublic <U> Opt<U> mapOrElse(Function<? super T, ? extends U> mapper, VoidFunc0 emptyAction) {\n\t\tif (isPresent()) {\n\t\t\treturn ofNullable(mapper.apply(value));\n\t\t} else {\n\t\t\temptyAction.callWithRuntimeException();\n\t\t\treturn empty();\n\t\t}\n\t}\n\n\t/**\n\t * 判断包裹里的值存在并且与给定的条件是否满足 ({@link Predicate#test}执行结果是否为true)\n\t * 如果满足条件则返回本身\n\t * 不满足条件或者元素本身为空时返回一个返回一个空的{@code Opt}\n\t *\n\t * @param predicate 给定的条件\n\t * @return 如果满足条件则返回本身, 不满足条件或者元素本身为空时返回一个空的{@code Opt}\n\t * @throws NullPointerException 如果给定的条件为 {@code null}，抛出{@code NPE}\n\t */\n\tpublic Opt<T> filter(Predicate<? super T> predicate) {\n\t\tObjects.requireNonNull(predicate);\n\t\tif (isEmpty()) {\n\t\t\treturn this;\n\t\t} else {\n\t\t\treturn predicate.test(value) ? this : empty();\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回一个包裹了该操作返回值的{@code Opt}\n\t * 如果不存在，返回一个空的{@code Opt}\n\t *\n\t * @param mapper 值存在时执行的操作\n\t * @param <U>    操作返回值的类型\n\t * @return 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回一个包裹了该操作返回值的{@code Opt}，\n\t * 如果不存在，返回一个空的{@code Opt}\n\t * @throws NullPointerException 如果给定的操作为 {@code null}，抛出 {@code NPE}\n\t */\n\tpublic <U> Opt<U> map(Function<? super T, ? extends U> mapper) {\n\t\tObjects.requireNonNull(mapper);\n\t\tif (isEmpty()) {\n\t\t\treturn empty();\n\t\t} else {\n\t\t\treturn Opt.ofNullable(mapper.apply(value));\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回该操作返回值\n\t * 如果不存在，返回一个空的{@code Opt}\n\t * 和 {@link Opt#map}的区别为 传入的操作返回值必须为 Opt\n\t *\n\t * @param mapper 值存在时执行的操作\n\t * @param <U>    操作返回值的类型\n\t * @return 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回该操作返回值\n\t * 如果不存在，返回一个空的{@code Opt}\n\t * @throws NullPointerException 如果给定的操作为 {@code null}或者给定的操作执行结果为 {@code null}，抛出 {@code NPE}\n\t */\n\tpublic <U> Opt<U> flatMap(Function<? super T, ? extends Opt<? extends U>> mapper) {\n\t\tObjects.requireNonNull(mapper);\n\t\tif (isEmpty()) {\n\t\t\treturn empty();\n\t\t} else {\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tfinal Opt<U> r = (Opt<U>) mapper.apply(value);\n\t\t\treturn Objects.requireNonNull(r);\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回该操作返回值\n\t * 如果不存在，返回一个空的{@code Opt}\n\t * 和 {@link Opt#map}的区别为 传入的操作返回值必须为 {@link Optional}\n\t *\n\t * @param mapper 值存在时执行的操作\n\t * @param <U>    操作返回值的类型\n\t * @return 如果包裹里的值存在，就执行传入的操作({@link Function#apply})并返回该操作返回值\n\t * 如果不存在，返回一个空的{@code Opt}\n\t * @throws NullPointerException 如果给定的操作为 {@code null}或者给定的操作执行结果为 {@code null}，抛出 {@code NPE}\n\t * @see Optional#flatMap(Function)\n\t * @since 5.7.16\n\t */\n\tpublic <U> Opt<U> flattedMap(Function<? super T, ? extends Optional<? extends U>> mapper) {\n\t\tObjects.requireNonNull(mapper);\n\t\tif (isEmpty()) {\n\t\t\treturn empty();\n\t\t} else {\n\t\t\treturn ofNullable(mapper.apply(value).orElse(null));\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里元素的值存在，就执行对应的操作，并返回本身\n\t * 如果不存在，返回一个空的{@code Opt}\n\t *\n\t * <p>属于 {@link #ifPresent}的链式拓展\n\t *\n\t * @param action 值存在时执行的操作\n\t * @return this\n\t * @throws NullPointerException 如果值存在，并且传入的操作为 {@code null}\n\t * @author VampireAchao\n\t */\n\tpublic Opt<T> peek(Consumer<T> action) throws NullPointerException {\n\t\tObjects.requireNonNull(action);\n\t\tif (isEmpty()) {\n\t\t\treturn Opt.empty();\n\t\t}\n\t\taction.accept(value);\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 如果包裹里元素的值存在，就执行对应的操作集，并返回本身\n\t * 如果不存在，返回一个空的{@code Opt}\n\t *\n\t * <p>属于 {@link #ifPresent}的链式拓展\n\t * <p>属于 {@link #peek(Consumer)}的动态拓展\n\t *\n\t * @param actions 值存在时执行的操作，动态参数，可传入数组，当数组为一个空数组时并不会抛出 {@code NPE}\n\t * @return this\n\t * @throws NullPointerException 如果值存在，并且传入的操作集中的元素为 {@code null}\n\t * @author VampireAchao\n\t */\n\t@SafeVarargs\n\tpublic final Opt<T> peeks(Consumer<T>... actions) throws NullPointerException {\n\t\t// 第三个参数 (opts, opt) -> null其实并不会执行到该函数式接口所以直接返回了个null\n\t\treturn Stream.of(actions).reduce(this, Opt<T>::peek, (opts, opt) -> null);\n\t}\n\n\t/**\n\t * 如果包裹里元素的值存在，就返回本身，如果不存在，则使用传入的操作执行后获得的 {@code Opt}\n\t *\n\t * @param supplier 不存在时的操作\n\t * @return 如果包裹里元素的值存在，就返回本身，如果不存在，则使用传入的函数执行后获得的 {@code Opt}\n\t * @throws NullPointerException 如果传入的操作为空，或者传入的操作执行后返回值为空，则抛出 {@code NPE}\n\t */\n\tpublic Opt<T> or(Supplier<? extends Opt<? extends T>> supplier) {\n\t\tObjects.requireNonNull(supplier);\n\t\tif (isPresent()) {\n\t\t\treturn this;\n\t\t} else {\n\t\t\t@SuppressWarnings(\"unchecked\") final Opt<T> r = (Opt<T>) supplier.get();\n\t\t\treturn Objects.requireNonNull(r);\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里元素的值存在，就返回一个包含该元素的 {@link Stream},\n\t * 否则返回一个空元素的 {@link Stream}\n\t *\n\t * <p> 该方法能将 Opt 中的元素传递给 {@link Stream}\n\t * <pre>{@code\n\t *     Stream<Opt<T>> os = ..\n\t *     Stream<T> s = os.flatMap(Opt::stream)\n\t * }</pre>\n\t *\n\t * @return 返回一个包含该元素的 {@link Stream}或空的 {@link Stream}\n\t */\n\tpublic Stream<T> stream() {\n\t\tif (isEmpty()) {\n\t\t\treturn Stream.empty();\n\t\t} else {\n\t\t\treturn Stream.of(value);\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里元素的值存在，则返回该值，否则返回传入的{@code other}\n\t *\n\t * @param other 元素为空时返回的值，有可能为 {@code null}.\n\t * @return 如果包裹里元素的值存在，则返回该值，否则返回传入的{@code other}\n\t */\n\tpublic T orElse(T other) {\n\t\treturn isPresent() ? value : other;\n\t}\n\n\t/**\n\t * 异常则返回另一个可选值\n\t *\n\t * @param other 可选值\n\t * @return 如果未发生异常，则返回该值，否则返回传入的{@code other}\n\t * @since 5.7.17\n\t */\n\tpublic T exceptionOrElse(T other) {\n\t\treturn isFail() ? other : value;\n\t}\n\n\t/**\n\t * 如果包裹里元素的值存在，则返回该值，否则返回传入的操作执行后的返回值\n\t *\n\t * @param supplier 值不存在时需要执行的操作，返回一个类型与 包裹里元素类型 相同的元素\n\t * @return 如果包裹里元素的值存在，则返回该值，否则返回传入的操作执行后的返回值\n\t * @throws NullPointerException 如果之不存在，并且传入的操作为空，则抛出 {@code NPE}\n\t */\n\tpublic T orElseGet(Supplier<? extends T> supplier) {\n\t\treturn isPresent() ? value : supplier.get();\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，则返回该值，否则抛出 {@code NoSuchElementException}\n\t *\n\t * @return 返回一个不为 {@code null} 的包裹里的值\n\t * @throws NoSuchElementException 如果包裹里的值不存在则抛出该异常\n\t */\n\tpublic T orElseThrow() {\n\t\treturn orElseThrow(NoSuchElementException::new, \"No value present\");\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，则返回该值，否则执行传入的操作，获取异常类型的返回值并抛出\n\t * <p>往往是一个包含无参构造器的异常 例如传入{@code IllegalStateException::new}\n\t *\n\t * @param <X>               异常类型\n\t * @param exceptionSupplier 值不存在时执行的操作，返回值继承 {@link Throwable}\n\t * @return 包裹里不能为空的值\n\t * @throws X                    如果值不存在\n\t * @throws NullPointerException 如果值不存在并且 传入的操作为 {@code null}或者操作执行后的返回值为{@code null}\n\t */\n\tpublic <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {\n\t\tif (isPresent()) {\n\t\t\treturn value;\n\t\t} else {\n\t\t\tthrow exceptionSupplier.get();\n\t\t}\n\t}\n\n\t/**\n\t * 如果包裹里的值存在，则返回该值，否则执行传入的操作，获取异常类型的返回值并抛出\n\t *\n\t * <p>往往是一个包含 自定义消息 构造器的异常 例如\n\t * <pre>{@code\n\t * \t\tOpt.ofNullable(null).orElseThrow(IllegalStateException::new, \"Ops!Something is wrong!\");\n\t * }</pre>\n\t *\n\t * @param <X>               异常类型\n\t * @param exceptionFunction 值不存在时执行的操作，返回值继承 {@link Throwable}\n\t * @param message           作为传入操作执行时的参数，一般作为异常自定义提示语\n\t * @return 包裹里不能为空的值\n\t * @throws X                    如果值不存在\n\t * @throws NullPointerException 如果值不存在并且 传入的操作为 {@code null}或者操作执行后的返回值为{@code null}\n\t * @author VampireAchao\n\t */\n\tpublic <X extends Throwable> T orElseThrow(Function<String, ? extends X> exceptionFunction, String message) throws X {\n\t\tif (isPresent()) {\n\t\t\treturn value;\n\t\t} else {\n\t\t\tthrow exceptionFunction.apply(message);\n\t\t}\n\t}\n\n\t/**\n\t * 转换为 {@link Optional}对象\n\t *\n\t * @return {@link Optional}对象\n\t * @since 5.7.16\n\t */\n\tpublic Optional<T> toOptional() {\n\t\treturn Optional.ofNullable(this.value);\n\t}\n\n\t/**\n\t * 判断传入参数是否与 {@code Opt}相等\n\t * 在以下情况下返回true\n\t * <ul>\n\t * <li>它也是一个 {@code Opt} 并且\n\t * <li>它们包裹住的元素都为空 或者\n\t * <li>它们包裹住的元素之间相互 {@code equals()}\n\t * </ul>\n\t *\n\t * @param obj 一个要用来判断是否相等的参数\n\t * @return 如果传入的参数也是一个 {@code Opt}并且它们包裹住的元素都为空\n\t * 或者它们包裹住的元素之间相互 {@code equals()} 就返回{@code true}\n\t * 否则返回 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (!(obj instanceof Opt)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal Opt<?> other = (Opt<?>) obj;\n\t\treturn Objects.equals(value, other.value);\n\t}\n\n\t/**\n\t * 如果包裹内元素为空，则返回0，否则返回元素的 {@code hashcode}\n\t *\n\t * @return 如果包裹内元素为空，则返回0，否则返回元素的 {@code hashcode}\n\t */\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hashCode(value);\n\t}\n\n\t/**\n\t * 返回包裹内元素调用{@code toString()}的结果，不存在则返回{@code null}\n\t *\n\t * @return 包裹内元素调用{@code toString()}的结果，不存在则返回{@code null}\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.toStringOrNull(this.value);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Pair.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.clone.CloneSupport;\n\nimport java.io.Serializable;\nimport java.util.Objects;\n\n/**\n * 键值对对象，只能在构造时传入键值\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 4.1.5\n */\npublic class Pair<K, V> extends CloneSupport<Pair<K, V>> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected K key;\n\tprotected V value;\n\n\t/**\n\t * 构建{@code Pair}对象\n\t *\n\t * @param <K>   键类型\n\t * @param <V>   值类型\n\t * @param key   键\n\t * @param value 值\n\t * @return {@code Pair}\n\t * @since 5.4.3\n\t */\n\tpublic static <K, V> Pair<K, V> of(K key, V value) {\n\t\treturn new Pair<>(key, value);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param key   键\n\t * @param value 值\n\t */\n\tpublic Pair(K key, V value) {\n\t\tthis.key = key;\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取键\n\t *\n\t * @return 键\n\t */\n\tpublic K getKey() {\n\t\treturn this.key;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @return 值\n\t */\n\tpublic V getValue() {\n\t\treturn this.value;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Pair [key=\" + key + \", value=\" + value + \"]\";\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o)\n\t\t\treturn true;\n\t\tif (o instanceof Pair) {\n\t\t\tPair<?, ?> pair = (Pair<?, ?>) o;\n\t\t\treturn Objects.equals(getKey(), pair.getKey()) &&\n\t\t\t\t\tObjects.equals(getValue(), pair.getValue());\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\t//copy from 1.8 HashMap.Node\n\t\treturn Objects.hashCode(key) ^ Objects.hashCode(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java",
    "content": "package cn.hutool.core.lang;\n\nimport java.io.Serializable;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * {@link ParameterizedType} 接口实现，用于重新定义泛型类型\n *\n * @author looly\n * @since 4.5.7\n */\npublic class ParameterizedTypeImpl implements ParameterizedType, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Type[] actualTypeArguments;\n\tprivate final Type ownerType;\n\tprivate final Type rawType;\n\n\t/**\n\t * 构造\n\t *\n\t * @param actualTypeArguments 实际的泛型参数类型\n\t * @param ownerType 拥有者类型\n\t * @param rawType 原始类型\n\t */\n\tpublic ParameterizedTypeImpl(Type[] actualTypeArguments, Type ownerType, Type rawType) {\n\t\tthis.actualTypeArguments = actualTypeArguments;\n\t\tthis.ownerType = ownerType;\n\t\tthis.rawType = rawType;\n\t}\n\n\t@Override\n\tpublic Type[] getActualTypeArguments() {\n\t\treturn actualTypeArguments;\n\t}\n\n\t@Override\n\tpublic Type getOwnerType() {\n\t\treturn ownerType;\n\t}\n\n\t@Override\n\tpublic Type getRawType() {\n\t\treturn rawType;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder buf = new StringBuilder();\n\n\t\tfinal Type useOwner = this.ownerType;\n\t\tfinal Class<?> raw = (Class<?>) this.rawType;\n\t\tif (useOwner == null) {\n\t\t\tbuf.append(raw.getName());\n\t\t} else {\n\t\t\tif (useOwner instanceof Class<?>) {\n\t\t\t\tbuf.append(((Class<?>) useOwner).getName());\n\t\t\t} else {\n\t\t\t\tbuf.append(useOwner.toString());\n\t\t\t}\n\t\t\tbuf.append('.').append(raw.getSimpleName());\n\t\t}\n\n\t\tappendAllTo(buf.append('<'), \", \", this.actualTypeArguments).append('>');\n\t\treturn buf.toString();\n\t}\n\n\t/**\n\t * 追加 {@code types} 到 {@code buf}，使用 {@code sep} 分隔\n\t *\n\t * @param buf 目标\n\t * @param sep 分隔符\n\t * @param types 加入的类型\n\t * @return {@code buf}\n\t */\n\tprivate static StringBuilder appendAllTo(final StringBuilder buf, final String sep, final Type... types) {\n\t\tif (ArrayUtil.isNotEmpty(types)) {\n\t\t\tboolean isFirst = true;\n\t\t\tfor (Type type : types) {\n\t\t\t\tif (isFirst) {\n\t\t\t\t\tisFirst = false;\n\t\t\t\t} else {\n\t\t\t\t\tbuf.append(sep);\n\t\t\t\t}\n\n\t\t\t\tString typeStr;\n\t\t\t\tif(type instanceof Class) {\n\t\t\t\t\ttypeStr = ((Class<?>)type).getName();\n\t\t\t\t}else {\n\t\t\t\t\ttypeStr = StrUtil.toString(type);\n\t\t\t\t}\n\n\t\t\t\tbuf.append(typeStr);\n\t\t\t}\n\t\t}\n\t\treturn buf;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\nimport java.util.regex.Pattern;\n\n/**\n * 常用正则表达式集合，更多正则见:<br>\n * <a href=\"https://any86.github.io/any-rule/\">https://any86.github.io/any-rule/</a>\n *\n * @author Looly\n */\npublic class PatternPool {\n\n\t/**\n\t * 英文字母 、数字和下划线\n\t */\n\tpublic final static Pattern GENERAL = Pattern.compile(RegexPool.GENERAL);\n\t/**\n\t * 数字\n\t */\n\tpublic final static Pattern NUMBERS = Pattern.compile(RegexPool.NUMBERS);\n\t/**\n\t * 字母\n\t */\n\tpublic final static Pattern WORD = Pattern.compile(RegexPool.WORD);\n\t/**\n\t * 单个中文汉字\n\t */\n\tpublic final static Pattern CHINESE = Pattern.compile(RegexPool.CHINESE);\n\t/**\n\t * 中文汉字\n\t */\n\tpublic final static Pattern CHINESES = Pattern.compile(RegexPool.CHINESES);\n\t/**\n\t * 分组\n\t */\n\tpublic final static Pattern GROUP_VAR = Pattern.compile(RegexPool.GROUP_VAR);\n\t/**\n\t * IP v4\n\t */\n\tpublic final static Pattern IPV4 = Pattern.compile(RegexPool.IPV4);\n\t/**\n\t * IP v6\n\t */\n\tpublic final static Pattern IPV6 = Pattern.compile(RegexPool.IPV6);\n\t/**\n\t * 货币\n\t */\n\tpublic final static Pattern MONEY = Pattern.compile(RegexPool.MONEY);\n\t/**\n\t * 邮件，符合RFC 5322规范，正则来自：<a href=\"http://emailregex.com/\">http://emailregex.com/</a><br>\n\t * <a href=\"https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/44317754\">https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/44317754</a>\n\t * 注意email 要宽松一点。比如 jetz.chong@hutool.cn、jetz-chong@ hutool.cn、jetz_chong@hutool.cn、dazhi.duan@hutool.cn 宽松一点把，都算是正常的邮箱\n\t */\n\tpublic final static Pattern EMAIL = Pattern.compile(RegexPool.EMAIL, Pattern.CASE_INSENSITIVE);\n\n\t/**\n\t * 规则同EMAIL，添加了对中文的支持\n\t */\n\tpublic final static Pattern EMAIL_WITH_CHINESE = Pattern.compile(RegexPool.EMAIL_WITH_CHINESE,Pattern.CASE_INSENSITIVE);\n\t/**\n\t * 移动电话\n\t */\n\tpublic final static Pattern MOBILE = Pattern.compile(RegexPool.MOBILE);\n\t/**\n\t * 中国香港移动电话\n\t * eg: 中国香港： +852 5100 4810， 三位区域码+10位数字, 中国香港手机号码8位数\n\t * eg: 中国大陆： +86  180 4953 1399，2位区域码标示+13位数字\n\t * 中国大陆 +86 Mainland China\n\t * 中国香港 +852 Hong Kong\n\t * 中国澳门 +853 Macao\n\t * 中国台湾 +886 Taiwan\n\t */\n\tpublic final static Pattern MOBILE_HK = Pattern.compile(RegexPool.MOBILE_HK);\n\t/**\n\t * 中国台湾移动电话\n\t * eg: 中国台湾： +886 09 60 000000， 三位区域码+号码以数字09开头 + 8位数字, 中国台湾手机号码10位数\n\t * 中国台湾 +886 Taiwan 国际域名缩写：TW\n\t */\n\tpublic final static Pattern MOBILE_TW = Pattern.compile(RegexPool.MOBILE_TW);\n\t/**\n\t * 中国澳门移动电话\n\t * eg: 中国台湾： +853 68 00000， 三位区域码 +号码以数字6开头 + 7位数字, 中国台湾手机号码8位数\n\t * 中国澳门 +853 Macao 国际域名缩写：MO\n\t */\n\tpublic final static Pattern MOBILE_MO = Pattern.compile(RegexPool.MOBILE_MO);\n\t/**\n\t * 座机号码\n\t */\n\tpublic final static Pattern TEL = Pattern.compile(RegexPool.TEL);\n\t/**\n\t * 座机号码+400+800电话\n\t */\n\tpublic final static Pattern TEL_400_800 = Pattern.compile(RegexPool.TEL_400_800);\n\t/**\n\t * 18位身份证号码\n\t */\n\tpublic final static Pattern CITIZEN_ID = Pattern.compile(RegexPool.CITIZEN_ID);\n\t/**\n\t * 邮编，兼容港澳台\n\t */\n\tpublic final static Pattern ZIP_CODE = Pattern.compile(RegexPool.ZIP_CODE);\n\t/**\n\t * 生日\n\t */\n\tpublic final static Pattern BIRTHDAY = Pattern.compile(RegexPool.BIRTHDAY);\n\t/**\n\t * URL\n\t */\n\tpublic final static Pattern URL = Pattern.compile(RegexPool.URL);\n\t/**\n\t * Http URL\n\t */\n\tpublic final static Pattern URL_HTTP = Pattern.compile(RegexPool.URL_HTTP, Pattern.CASE_INSENSITIVE);\n\t/**\n\t * 中文字、英文字母、数字和下划线\n\t */\n\tpublic final static Pattern GENERAL_WITH_CHINESE = Pattern.compile(RegexPool.GENERAL_WITH_CHINESE);\n\t/**\n\t * UUID\n\t */\n\tpublic final static Pattern UUID = Pattern.compile(RegexPool.UUID, Pattern.CASE_INSENSITIVE);\n\t/**\n\t * 不带横线的UUID\n\t */\n\tpublic final static Pattern UUID_SIMPLE = Pattern.compile(RegexPool.UUID_SIMPLE);\n\t/**\n\t * MAC地址正则\n\t */\n\tpublic static final Pattern MAC_ADDRESS = Pattern.compile(RegexPool.MAC_ADDRESS, Pattern.CASE_INSENSITIVE);\n\t/**\n\t * 16进制字符串\n\t */\n\tpublic static final Pattern HEX = Pattern.compile(RegexPool.HEX);\n\t/**\n\t * 时间正则\n\t */\n\tpublic static final Pattern TIME = Pattern.compile(RegexPool.TIME);\n\t/**\n\t * 中国车牌号码（兼容新能源车牌）\n\t */\n\tpublic final static Pattern PLATE_NUMBER = Pattern.compile(RegexPool.PLATE_NUMBER);\n\n\t/**\n\t * 统一社会信用代码\n\t * <pre>\n\t * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n\t * 第二部分：机构类别代码1位 (数字或大写英文字母)\n\t * 第三部分：登记管理机关行政区划码6位 (数字)\n\t * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n\t * 第五部分：校验码1位 (数字或大写英文字母)\n\t * </pre>\n\t */\n\tpublic static final Pattern CREDIT_CODE = Pattern.compile(RegexPool.CREDIT_CODE);\n\t/**\n\t * 车架号（车辆识别代号由世界制造厂识别代号(WMI、车辆说明部分(VDS)车辆指示部分(VIS)三部分组成，共 17 位字码。）<br>\n\t * 别名：车辆识别代号、车辆识别码、车架号、十七位码<br>\n\t * 标准号：GB 16735-2019<br>\n\t * 标准官方地址：https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E2EBF667F8C032B1EDFD6DF9C1114E02\n\t * 对年产量大于或等于1 000 辆的完整车辆和/或非完整车辆制造厂：\n\t * <pre>\n\t *   第一部分为世界制造厂识别代号(WMI)，3位\n\t *   第二部分为车辆说明部分(VDS)，     6位\n\t *   第三部分为车辆指示部分(VIS)，     8位\n\t * </pre>\n\t *\n\t * 对年产量小于 1 000 辆的完整车辆和/或非完整车辆制造厂：\n\t * <pre>\n\t *   第一部分为世界制造广识别代号(WMI),3位;\n\t *   第二部分为车辆说明部分(VDS)，6位;\n\t *   第三部分的三、四、五位与第一部分的三位字码起构成世界制造厂识别代号(WMI),其余五位为车辆指示部分(VIS)，8位。\n\t * </pre>\n\t *\n\t * <pre>\n\t *   eg:LDC613P23A1305189\n\t *   eg:LSJA24U62JG269225\n\t *   eg:LBV5S3102ESJ25655\n\t * </pre>\n\t */\n\tpublic static final Pattern CAR_VIN = Pattern.compile(RegexPool.CAR_VIN);\n\t/**\n\t * 驾驶证  别名：驾驶证档案编号、行驶证编号\n\t * eg:430101758218\n\t * 12位数字字符串\n\t * 仅限：中国驾驶证档案编号\n\t */\n\tpublic static final Pattern CAR_DRIVING_LICENCE = Pattern.compile(RegexPool.CAR_DRIVING_LICENCE);\n\t/**\n\t * 中文姓名\n\t * 总结中国人姓名：2-60位，只能是中文和 ·\n\t */\n\tpublic static final Pattern CHINESE_NAME = Pattern.compile(RegexPool.CHINESE_NAME);\n\n\t// -------------------------------------------------------------------------------------------------------------------------------------------------------------------\n\t/**\n\t * Pattern池\n\t */\n\tprivate static final WeakKeyValueConcurrentMap<RegexWithFlag, Pattern> POOL = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 先从Pattern池中查找正则对应的{@link Pattern}，找不到则编译正则表达式并入池。\n\t *\n\t * @param regex 正则表达式\n\t * @return {@link Pattern}\n\t */\n\tpublic static Pattern get(String regex) {\n\t\treturn get(regex, 0);\n\t}\n\n\t/**\n\t * 先从Pattern池中查找正则对应的{@link Pattern}，找不到则编译正则表达式并入池。\n\t *\n\t * @param regex 正则表达式\n\t * @param flags 正则标识位集合 {@link Pattern}\n\t * @return {@link Pattern}\n\t */\n\tpublic static Pattern get(String regex, int flags) {\n\t\tfinal RegexWithFlag regexWithFlag = new RegexWithFlag(regex, flags);\n\t\treturn POOL.computeIfAbsent(regexWithFlag, (key)-> Pattern.compile(regex, flags));\n\t}\n\n\t/**\n\t * 移除缓存\n\t *\n\t * @param regex 正则\n\t * @param flags 标识\n\t * @return 移除的{@link Pattern}，可能为{@code null}\n\t */\n\tpublic static Pattern remove(String regex, int flags) {\n\t\treturn POOL.remove(new RegexWithFlag(regex, flags));\n\t}\n\n\t/**\n\t * 清空缓存池\n\t */\n\tpublic static void clear() {\n\t\tPOOL.clear();\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------------------------------------\n\n\t/**\n\t * 正则表达式和正则标识位的包装\n\t *\n\t * @author Looly\n\t */\n\tprivate static class RegexWithFlag {\n\t\tprivate final String regex;\n\t\tprivate final int flag;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param regex 正则\n\t\t * @param flag  标识\n\t\t */\n\t\tpublic RegexWithFlag(String regex, int flag) {\n\t\t\tthis.regex = regex;\n\t\t\tthis.flag = flag;\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\tfinal int prime = 31;\n\t\t\tint result = 1;\n\t\t\tresult = prime * result + flag;\n\t\t\tresult = prime * result + ((regex == null) ? 0 : regex.hashCode());\n\t\t\treturn result;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object obj) {\n\t\t\tif (this == obj) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (obj == null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (getClass() != obj.getClass()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tRegexWithFlag other = (RegexWithFlag) obj;\n\t\t\tif (flag != other.flag) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (regex == null) {\n\t\t\t\treturn other.regex == null;\n\t\t\t} else {\n\t\t\t\treturn regex.equals(other.regex);\n\t\t\t}\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Pid.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.lang.management.ManagementFactory;\n\n/**\n * 进程ID单例封装<br>\n * 第一次访问时调用{@link ManagementFactory#getRuntimeMXBean()}获取PID信息，之后直接使用缓存值\n *\n * @author looly\n * @since 5.8.0\n */\npublic enum Pid {\n\tINSTANCE;\n\n\tprivate final int pid;\n\n\tPid() {\n\t\tthis.pid = getPid();\n\t}\n\n\t/**\n\t * 获取PID值\n\t *\n\t * @return pid\n\t */\n\tpublic int get() {\n\t\treturn this.pid;\n\t}\n\n\t/**\n\t * 获取当前进程ID，首先获取进程名称，读取@前的ID值，如果不存在，则读取进程名的hash值\n\t *\n\t * @return 进程ID\n\t * @throws UtilException 进程名称为空\n\t */\n\tprivate static int getPid() throws UtilException {\n\t\tfinal String processName = ManagementFactory.getRuntimeMXBean().getName();\n\t\tif (StrUtil.isBlank(processName)) {\n\t\t\tthrow new UtilException(\"Process name is blank!\");\n\t\t}\n\t\tfinal int atIndex = processName.indexOf('@');\n\t\tif (atIndex > 0) {\n\t\t\treturn Integer.parseInt(processName.substring(0, atIndex));\n\t\t} else {\n\t\t\treturn processName.hashCode();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Range.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.thread.lock.NoLock;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\n\n/**\n * 范围生成器。根据给定的初始值、结束值和步进生成一个步进列表生成器<br>\n * 由于用户自行实现{@link Stepper}来定义步进，因此Range本身无法判定边界（是否达到end），需在step实现边界判定逻辑。\n *\n * <p>\n * 此类使用{@link ReentrantReadWriteLock}保证线程安全\n * </p>\n *\n * @param <T> 生成范围对象的类型\n * @author Looly\n */\npublic class Range<T> implements Iterable<T>, Iterator<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 锁保证线程安全\n\t */\n\tprivate Lock lock = new ReentrantLock();\n\t/**\n\t * 起始对象\n\t */\n\tprivate final T start;\n\t/**\n\t * 结束对象\n\t */\n\tprivate final T end;\n\t/**\n\t * 下一个对象\n\t */\n\tprivate T next;\n\t/**\n\t * 步进\n\t */\n\tprivate final Stepper<T> stepper;\n\t/**\n\t * 索引\n\t */\n\tprivate int index = 0;\n\t/**\n\t * 是否包含第一个元素\n\t */\n\tprivate final boolean includeStart;\n\t/**\n\t * 是否包含最后一个元素\n\t */\n\tprivate final boolean includeEnd;\n\n\t/**\n\t * 构造\n\t *\n\t * @param start   起始对象（包括）\n\t * @param stepper 步进\n\t */\n\tpublic Range(T start, Stepper<T> stepper) {\n\t\tthis(start, null, stepper);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param start   起始对象（包含）\n\t * @param end     结束对象（包含）\n\t * @param stepper 步进\n\t */\n\tpublic Range(T start, T end, Stepper<T> stepper) {\n\t\tthis(start, end, stepper, true, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param start          起始对象\n\t * @param end            结束对象\n\t * @param stepper        步进\n\t * @param isIncludeStart 是否包含第一个元素\n\t * @param isIncludeEnd   是否包含最后一个元素\n\t */\n\tpublic Range(T start, T end, Stepper<T> stepper, boolean isIncludeStart, boolean isIncludeEnd) {\n\t\tAssert.notNull(start, \"First element must be not null!\");\n\t\tthis.start = start;\n\t\tthis.end = end;\n\t\tthis.stepper = stepper;\n\t\tthis.next = safeStep(this.start);\n\t\tthis.includeStart = isIncludeStart;\n\t\tthis.includeEnd = isIncludeEnd;\n\t}\n\n\t/**\n\t * 禁用锁，调用此方法后不再使用锁保护\n\t *\n\t * @return this\n\t * @since 4.3.1\n\t */\n\tpublic Range<T> disableLock() {\n\t\tthis.lock = new NoLock();\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tif (0 == this.index && this.includeStart) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (null == this.next) {\n\t\t\t\treturn false;\n\t\t\t} else if (false == includeEnd && this.next.equals(this.end)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic T next() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tif (false == this.hasNext()) {\n\t\t\t\tthrow new NoSuchElementException(\"Has no next range!\");\n\t\t\t}\n\t\t\treturn nextUncheck();\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获取下一个元素，并将下下个元素准备好\n\t */\n\tprivate T nextUncheck() {\n\t\tT current;\n\t\tif(0 == this.index){\n\t\t\tcurrent = start;\n\t\t\tif(false == this.includeStart){\n\t\t\t\t// 获取下一组元素\n\t\t\t\tindex ++;\n\t\t\t\treturn nextUncheck();\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent = next;\n\t\t\tthis.next = safeStep(this.next);\n\t\t}\n\n\t\tindex++;\n\t\treturn current;\n\t}\n\n\t/**\n\t * 不抛异常的获取下一步进的元素，如果获取失败返回{@code null}\n\t *\n\t * @param base  上一个元素\n\t * @return 下一步进\n\t */\n\tprivate T safeStep(T base) {\n\t\tfinal int index = this.index;\n\t\tT next = null;\n\t\ttry {\n\t\t\tnext = stepper.step(base, this.end, index);\n\t\t} catch (Exception e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn next;\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tthrow new UnsupportedOperationException(\"Can not remove ranged element!\");\n\t}\n\n\t@Override\n\tpublic Iterator<T> iterator() {\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置Range\n\t *\n\t * @return this\n\t */\n\tpublic Range<T> reset() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tthis.index = 0;\n\t\t\tthis.next = safeStep(this.start);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 步进接口，此接口用于实现如何对一个对象按照指定步进增加步进<br>\n\t * 步进接口可以定义以下逻辑：\n\t *\n\t * <pre>\n\t * 1、步进规则，即对象如何做步进\n\t * 2、步进大小，通过实现此接口，在实现类中定义一个对象属性，可灵活定义步进大小\n\t * 3、限制range个数，通过实现此接口，在实现类中定义一个对象属性，可灵活定义limit，限制range个数\n\t * </pre>\n\t *\n\t * @param <T> 需要增加步进的对象\n\t * @author Looly\n\t */\n\t@FunctionalInterface\n\tpublic interface Stepper<T> {\n\t\t/**\n\t\t * 增加步进<br>\n\t\t * 增加步进后的返回值如果为{@code null}则表示步进结束<br>\n\t\t * 用户需根据end参数自行定义边界，当达到边界时返回null表示结束，否则Range中边界对象无效，会导致无限循环\n\t\t *\n\t\t * @param current 上一次增加步进后的基础对象\n\t\t * @param end     结束对象\n\t\t * @param index   当前索引（步进到第几个元素），从0开始计数\n\t\t * @return 增加步进后的对象\n\t\t */\n\t\tT step(T current, T end, int index);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/RegexPool.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 常用正则表达式字符串池\n *\n * @author looly\n * @since 5.7.3\n */\npublic interface RegexPool {\n\t/**\n\t * 英文字母 、数字和下划线\n\t */\n\tString GENERAL = \"^\\\\w+$\";\n\t/**\n\t * 数字\n\t */\n\tString NUMBERS = \"\\\\d+\";\n\t/**\n\t * 字母\n\t */\n\tString WORD = \"[a-zA-Z]+\";\n\t/**\n\t * 单个中文汉字<br>\n\t * 参照维基百科汉字Unicode范围(https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97 页面右侧)\n\t */\n\tString CHINESE = \"[\\u2E80-\\u2EFF\\u2F00-\\u2FDF\\u31C0-\\u31EF\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uD840\\uDC00-\\uD869\\uDEDF\\uD869\\uDF00-\\uD86D\\uDF3F\\uD86D\\uDF40-\\uD86E\\uDC1F\\uD86E\\uDC20-\\uD873\\uDEAF\\uD87E\\uDC00-\\uD87E\\uDE1F]\";\n\t/**\n\t * 中文汉字\n\t */\n\tString CHINESES = CHINESE + \"+\";\n\t/**\n\t * 分组\n\t */\n\tString GROUP_VAR = \"\\\\$(\\\\d+)\";\n\t/**\n\t * IP v4<br>\n\t * 采用分组方式便于解析地址的每一个段\n\t */\n\t//String IPV4 = \"\\\\b((?!\\\\d\\\\d\\\\d)\\\\d+|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.((?!\\\\d\\\\d\\\\d)\\\\d+|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.((?!\\\\d\\\\d\\\\d)\\\\d+|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.((?!\\\\d\\\\d\\\\d)\\\\d+|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\b\";\n\tString IPV4 = \"^(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)\\\\.(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)\\\\.(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)\\\\.(25[0-5]|2[0-4]\\\\d|[0-1]?\\\\d?\\\\d)$\";\n\t/**\n\t * IP v6\n\t */\n\tString IPV6 = \"(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\\\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\\\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\";\n\t/**\n\t * 货币\n\t */\n\tString MONEY = \"^(\\\\d+(?:\\\\.\\\\d+)?)$\";\n\t/**\n\t * 邮件，符合RFC 5322规范，正则来自：http://emailregex.com/\n\t * What is the maximum length of a valid email address? https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/44317754\n\t * 注意email 要宽松一点。比如 jetz.chong@hutool.cn、jetz-chong@ hutool.cn、jetz_chong@hutool.cn、dazhi.duan@hutool.cn 宽松一点把，都算是正常的邮箱\n\t */\n\tString EMAIL = \"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\\\"(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21\\\\x23-\\\\x5b\\\\x5d-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])*\\\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21-\\\\x5a\\\\x53-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])+)])\";\n\n\t/**\n\t * 规则同EMAIL，添加了对中文的支持\n\t */\n\tString EMAIL_WITH_CHINESE = \"(?:[a-z0-9\\\\u4e00-\\\\u9fa5!#$%&'*+/=?^_`{|}~-]+(?:\\\\.[a-z0-9\\\\u4e00-\\\\u9fa5!#$%&'*+/=?^_`{|}~-]+)*|\\\"(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21\\\\x23-\\\\x5b\\\\x5d-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])*\\\")@(?:(?:[a-z0-9\\\\u4e00-\\\\u9fa5](?:[a-z0-9\\\\u4e00-\\\\u9fa5-]*[a-z0-9\\\\u4e00-\\\\u9fa5])?\\\\.)+[a-z0-9\\\\u4e00-\\\\u9fa5](?:[a-z0-9\\\\u4e00-\\\\u9fa5-]*[a-z0-9\\\\u4e00-\\\\u9fa5])?|\\\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9\\\\u4e00-\\\\u9fa5-]*[a-z0-9\\\\u4e00-\\\\u9fa5]:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21-\\\\x5a\\\\x53-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])+)])\";\n\t/**\n\t * 移动电话\n\t * eg: 中国大陆： +86  180 4953 1399，2位区域码标示+11位数字\n\t * 中国大陆 +86 Mainland China\n\t */\n\tString MOBILE = \"(?:0|86|\\\\+86)?1[3-9]\\\\d{9}\";\n\t/**\n\t * 中国香港移动电话\n\t * eg: 中国香港： +852 5100 4810， 三位区域码+10位数字, 中国香港手机号码8位数\n\t */\n\tString MOBILE_HK = \"(?:0|852|\\\\+852)?\\\\d{8}\";\n\t/**\n\t * 中国台湾移动电话\n\t * eg: 中国台湾： +886 09 60 000000， 三位区域码+号码以数字09开头 + 8位数字, 中国台湾手机号码10位数\n\t * 中国台湾 +886 Taiwan 国际域名缩写：TW\n\t */\n\tString MOBILE_TW = \"(?:0|886|\\\\+886)?(?:|-)09\\\\d{8}\";\n\t/**\n\t * 中国澳门移动电话\n\t * eg: 中国澳门： +853 68 00000， 三位区域码 +号码以数字6开头 + 7位数字, 中国澳门手机号码8位数\n\t * 中国澳门 +853 Macao 国际域名缩写：MO\n\t */\n\tString MOBILE_MO = \"(?:0|853|\\\\+853)?(?:|-)6\\\\d{7}\";\n\t/**\n\t * 座机号码<br>\n\t * pr#387@Gitee\n\t */\n\tString TEL = \"(010|02\\\\d|0[3-9]\\\\d{2})-?(\\\\d{6,8})\";\n\t/**\n\t * 座机号码+400+800电话\n\t */\n\tString TEL_400_800 = \"0\\\\d{2,3}[\\\\- ]?[0-9]\\\\d{6,7}|[48]00[\\\\- ]?[0-9]\\\\d{2}[\\\\- ]?\\\\d{4}\";\n\t/**\n\t * 18位身份证号码\n\t */\n\tString CITIZEN_ID = \"[1-9]\\\\d{5}[1-2]\\\\d{3}((0\\\\d)|(1[0-2]))(([012]\\\\d)|3[0-1])\\\\d{3}(\\\\d|X|x)\";\n\t/**\n\t * 邮编，兼容港澳台\n\t */\n\tString ZIP_CODE = \"^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[0-7]|6[0-7]|7[0-5]|8[0-9]|9[0-8])\\\\d{4}|99907[78]$\";\n\t/**\n\t * 生日\n\t */\n\tString BIRTHDAY = \"^(\\\\d{2,4})([/\\\\-.年]?)(\\\\d{1,2})([/\\\\-.月]?)(\\\\d{1,2})日?$\";\n\t/**\n\t * URI<br>\n\t * 定义见：https://www.ietf.org/rfc/rfc3986.html#appendix-B\n\t */\n\tString URI = \"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\\\?([^#]*))?(#(.*))?\";\n\t/**\n\t * URL\n\t */\n\tString URL = \"[a-zA-Z]+://[\\\\w-+&@#/%?=~_|!:,.;]*[\\\\w-+&@#/%=~_|]\";\n\t/**\n\t * Http URL（来自：http://urlregex.com/）<br>\n\t * 此正则同时支持FTP、File等协议的URL\n\t */\n\tString URL_HTTP = \"(https?|ftp|file)://[\\\\w-+&@#/%?=~_|!:,.;]*[\\\\w-+&@#/%=~_|]\";\n\t/**\n\t * 中文字、英文字母、数字和下划线\n\t */\n\tString GENERAL_WITH_CHINESE = \"^[\\u4E00-\\u9FFF\\\\w]+$\";\n\t/**\n\t * UUID\n\t */\n\tString UUID = \"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$\";\n\t/**\n\t * 不带横线的UUID\n\t */\n\tString UUID_SIMPLE = \"^[0-9a-fA-F]{32}$\";\n\t/**\n\t * MAC地址正则\n\t */\n\t//String MAC_ADDRESS = \"((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|0x(\\\\d{12}).+ETHER\";\n\tString MAC_ADDRESS = \"((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|((?:[a-fA-F0-9]{1,4}[.]){2}[a-fA-F0-9]{1,4})|[a-fA-F0-9]{12}|0x(\\\\d{12}).+ETHER\";\n\t/**\n\t * 16进制字符串\n\t */\n\tString HEX = \"^[a-fA-F0-9]+$\";\n\t/**\n\t * 时间正则\n\t */\n\tString TIME = \"\\\\d{1,2}[:时]\\\\d{1,2}([:分]\\\\d{1,2})?秒?\";\n\t/**\n\t * 中国车牌号码（兼容新能源车牌）\n\t */\n\tString PLATE_NUMBER =\n\t\t\t//https://gitee.com/chinabugotech/hutool/issues/I1B77H?from=project-issue\n\t\t\t\"^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[ABCDEFGHJK])|([ABCDEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4})))|\" +\n\t\t\t\t\t//https://gitee.com/chinabugotech/hutool/issues/I1BJHE?from=project-issue\n\t\t\t\t\t\"([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]\\\\d{3}\\\\d{1,3}[领])|\" +\n\t\t\t\t\t\"([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$\";\n\n\t/**\n\t * 统一社会信用代码\n\t * <pre>\n\t * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n\t * 第二部分：机构类别代码1位 (数字或大写英文字母)\n\t * 第三部分：登记管理机关行政区划码6位 (数字)\n\t * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n\t * 第五部分：校验码1位 (数字或大写英文字母)\n\t * </pre>\n\t */\n\tString CREDIT_CODE = \"^[0-9A-HJ-NPQRTUWXY]{2}\\\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$\";\n\t/**\n\t * 车架号（车辆识别代号由世界制造厂识别代号(WMI、车辆说明部分(VDS)车辆指示部分(VIS)三部分组成，共 17 位字码。）<br>\n\t * 别名：车辆识别代号、车辆识别码、车架号、十七位码<br>\n\t * 标准号：GB 16735-2019<br>\n\t * 标准官方地址：https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E2EBF667F8C032B1EDFD6DF9C1114E02\n\t * 对年产量大于或等于1 000 辆的完整车辆和/或非完整车辆制造厂：\n\t * <pre>\n\t *   第一部分为世界制造厂识别代号(WMI)，3位\n\t *   第二部分为车辆说明部分(VDS)，     6位\n\t *   第三部分为车辆指示部分(VIS)，     8位\n\t * </pre>\n\t *\n\t * 对年产量小于 1 000 辆的完整车辆和/或非完整车辆制造厂：\n\t * <pre>\n\t *   第一部分为世界制造广识别代号(WMI),3位;\n\t *   第二部分为车辆说明部分(VDS)，6位;\n\t *   第三部分的三、四、五位与第一部分的三位字码起构成世界制造厂识别代号(WMI),其余五位为车辆指示部分(VIS)，8位。\n\t * </pre>\n\t *\n\t * <pre>\n\t *   eg:LDC613P23A1305189\n\t *   eg:LSJA24U62JG269225\n\t *   eg:LBV5S3102ESJ25655\n\t * </pre>\n\t */\n\tString CAR_VIN = \"^[A-HJ-NPR-Z0-9]{8}[X0-9]([A-HJ-NPR-Z0-9]{3}\\\\d{5}|[A-HJ-NPR-Z0-9]{5}\\\\d{3})$\";\n\t/**\n\t * 驾驶证  别名：驾驶证档案编号、行驶证编号\n\t * eg:430101758218\n\t * 12位数字字符串\n\t * 仅限：中国驾驶证档案编号\n\t */\n\tString CAR_DRIVING_LICENCE = \"^[0-9]{12}$\";\n\t/**\n\t * 中文姓名\n\t * 维吾尔族姓名里面的点是 · 输入法中文状态下，键盘左上角数字1前面的那个符号；<br>\n\t * 错误字符：{@code ．.。．.}<br>\n\t * 正确维吾尔族姓名：\n\t * <pre>\n\t * 霍加阿卜杜拉·麦提喀斯木\n\t * 玛合萨提别克·哈斯木别克\n\t * 阿布都热依木江·艾斯卡尔\n\t * 阿卜杜尼亚孜·毛力尼亚孜\n\t * </pre>\n\t * <pre>\n\t * ----------\n\t * 错误示例：孟  伟                reason: 有空格\n\t * 错误示例：连逍遥0               reason: 数字\n\t * 错误示例：依帕古丽-艾则孜        reason: 特殊符号\n\t * 错误示例：牙力空.买提萨力        reason: 新疆人的点不对\n\t * 错误示例：王建鹏2002-3-2        reason: 有数字、特殊符号\n\t * 错误示例：雷金默(雷皓添）        reason: 有括号\n\t * 错误示例：翟冬:亮               reason: 有特殊符号\n\t * 错误示例：李                   reason: 少于2位\n\t * ----------\n\t * </pre>\n\t * 总结中文姓名：2-60位，只能是中文和维吾尔族的点·\n\t * 放宽汉字范围：如生僻姓名 刘欣䶮yǎn\n\t */\n\tString CHINESE_NAME = \"^[\\u3400-\\u9FFF·]{2,60}$\";\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Replacer.java",
    "content": "package cn.hutool.core.lang;\n\n/**\n * 替换器<br>\n * 通过实现此接口完成指定类型对象的替换操作，替换后的目标类型依旧为指定类型\n *\n * @author looly\n *\n * @param <T> 被替换操作的类型\n * @since 4.1.5\n */\n@FunctionalInterface\npublic interface Replacer<T> {\n\n\t/**\n\t * 替换指定类型为目标类型\n\t *\n\t * @param t 被替换的对象\n\t * @return 替代后的对象\n\t */\n\tT replace(T t);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ResourceClassLoader.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.security.SecureClassLoader;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 资源类加载器，可以加载任意类型的资源类\n *\n * @param <T> {@link Resource}接口实现类\n * @author looly, lzpeng\n * @since 5.5.2\n */\npublic class ResourceClassLoader<T extends Resource> extends SecureClassLoader {\n\n\tprivate final Map<String, T> resourceMap;\n\t/**\n\t * 缓存已经加载的类\n\t */\n\tprivate final Map<String, Class<?>> cacheClassMap;\n\n\t/**\n\t * 构造\n\t *\n\t * @param parentClassLoader 父类加载器，null表示默认当前上下文加载器\n\t * @param resourceMap       资源map\n\t */\n\tpublic ResourceClassLoader(ClassLoader parentClassLoader, Map<String, T> resourceMap) {\n\t\tsuper(ObjectUtil.defaultIfNull(parentClassLoader, ClassLoaderUtil::getClassLoader));\n\t\tthis.resourceMap = ObjectUtil.defaultIfNull(resourceMap, () -> new HashMap<>());\n\t\tthis.cacheClassMap = new HashMap<>();\n\t}\n\n\t/**\n\t * 增加需要加载的类资源\n\t *\n\t * @param resource 资源，可以是文件、流或者字符串\n\t * @return this\n\t */\n\tpublic ResourceClassLoader<T> addResource(T resource) {\n\t\tthis.resourceMap.put(resource.getName(), resource);\n\t\treturn this;\n\t}\n\n\t@Override\n\tprotected Class<?> findClass(String name) throws ClassNotFoundException {\n\t\tfinal Class<?> clazz = cacheClassMap.computeIfAbsent(name, this::defineByName);\n\t\tif (clazz == null) {\n\t\t\treturn super.findClass(name);\n\t\t}\n\t\treturn clazz;\n\t}\n\n\t/**\n\t * 从给定资源中读取class的二进制流，然后生成类<br>\n\t * 如果这个类资源不存在，返回{@code null}\n\t *\n\t * @param name 类名\n\t * @return 定义的类\n\t */\n\tprivate Class<?> defineByName(String name) {\n\t\tfinal Resource resource = resourceMap.get(name);\n\t\tif (null != resource) {\n\t\t\tfinal byte[] bytes = resource.readBytes();\n\t\t\treturn defineClass(name, bytes, 0, bytes.length);\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Segment.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.NumberUtil;\n\nimport java.lang.reflect.Type;\n\n/**\n * 片段表示，用于表示文本、集合等数据结构的一个区间。\n * @param <T> 数字类型，用于表示位置index\n *\n * @author looly\n * @since 5.5.3\n */\npublic interface Segment<T extends Number> {\n\n\t/**\n\t * 获取起始位置\n\t *\n\t * @return 起始位置\n\t */\n\tT getStartIndex();\n\n\t/**\n\t * 获取结束位置\n\t *\n\t * @return 结束位置\n\t */\n\tT getEndIndex();\n\n\t/**\n\t * 片段长度，默认计算方法为abs({@link #getEndIndex()} - {@link #getEndIndex()})\n\t *\n\t * @return 片段长度\n\t */\n\tdefault T length(){\n\t\tfinal T start = Assert.notNull(getStartIndex(), \"Start index must be not null!\");\n\t\tfinal T end = Assert.notNull(getEndIndex(), \"End index must be not null!\");\n\t\treturn Convert.convert((Type) start.getClass(), NumberUtil.sub(end, start).abs());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/SimpleCache.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.TransIter;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutableObj;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\nimport java.io.Serializable;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.WeakHashMap;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.function.Predicate;\n\n/**\n * 简单缓存，无超时实现，默认使用{@link WeakKeyValueConcurrentMap}实现缓存自动清理\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n */\npublic class SimpleCache<K, V> implements Iterable<Map.Entry<K, V>>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 池\n\t */\n\tprivate final Map<Mutable<K>, V> rawMap;\n\t// 乐观读写锁\n\tprivate final ReadWriteLock lock = new ReentrantReadWriteLock();\n\t/**\n\t * 写的时候每个key一把锁，降低锁的粒度\n\t */\n\tprotected final Map<K, Lock> keyLockMap = new SafeConcurrentHashMap<>();\n\n\t/**\n\t * 构造，默认使用{@link WeakHashMap}实现缓存自动清理\n\t */\n\tpublic SimpleCache() {\n\t\tthis(new WeakKeyValueConcurrentMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t * <p>\n\t * 通过自定义Map初始化，可以自定义缓存实现。<br>\n\t * 比如使用{@link WeakHashMap}则会自动清理key，使用HashMap则不会清理<br>\n\t * 同时，传入的Map对象也可以自带初始化的键值对，防止在get时创建\n\t * </p>\n\t *\n\t * @param initMap 初始Map，用于定义Map类型\n\t */\n\tpublic SimpleCache(Map<Mutable<K>, V> initMap) {\n\t\tthis.rawMap = initMap;\n\t}\n\n\t/**\n\t * 从缓存池中查找值\n\t *\n\t * @param key 键\n\t * @return 值\n\t */\n\tpublic V get(K key) {\n\t\tlock.readLock().lock();\n\t\ttry {\n\t\t\treturn rawMap.get(MutableObj.of(key));\n\t\t} finally {\n\t\t\tlock.readLock().unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t *\n\t * @param key      键\n\t * @param supplier 如果不存在回调方法，用于生产值对象\n\t * @return 值对象\n\t */\n\tpublic V get(K key, Func0<V> supplier) {\n\t\treturn get(key, null, supplier);\n\t}\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t *\n\t * @param key            键\n\t * @param validPredicate 检查结果对象是否可用，如是否断开连接等\n\t * @param supplier       如果不存在回调方法或结果不可用，用于生产值对象\n\t * @return 值对象\n\t * @since 5.7.9\n\t */\n\tpublic V get(K key, Predicate<V> validPredicate, Func0<V> supplier) {\n\t\tV v = get(key);\n\t\tif((null != validPredicate && null != v && false == validPredicate.test(v))){\n\t\t\tv = null;\n\t\t}\n\t\tif (null == v && null != supplier) {\n\t\t\t//每个key单独获取一把锁，降低锁的粒度提高并发能力，see pr#1385@Github\n\t\t\tfinal Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock());\n\t\t\tkeyLock.lock();\n\t\t\ttry {\n\t\t\t\t// 双重检查，防止在竞争锁的过程中已经有其它线程写入\n\t\t\t\tv = get(key);\n\t\t\t\tif (null == v || (null != validPredicate && false == validPredicate.test(v))) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tv = supplier.call();\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t\t\t\t}\n\t\t\t\t\tput(key, v);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tkeyLock.unlock();\n\t\t\t\tkeyLockMap.remove(key);\n\t\t\t}\n\t\t}\n\n\t\treturn v;\n\t}\n\n\t/**\n\t * 放入缓存\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @return 值\n\t */\n\tpublic V put(K key, V value) {\n\t\t// 独占写锁\n\t\tlock.writeLock().lock();\n\t\ttry {\n\t\t\trawMap.put(MutableObj.of(key), value);\n\t\t} finally {\n\t\t\tlock.writeLock().unlock();\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 移除缓存\n\t *\n\t * @param key 键\n\t * @return 移除的值\n\t */\n\tpublic V remove(K key) {\n\t\t// 独占写锁\n\t\tlock.writeLock().lock();\n\t\ttry {\n\t\t\treturn rawMap.remove(MutableObj.of(key));\n\t\t} finally {\n\t\t\tlock.writeLock().unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 清空缓存池\n\t */\n\tpublic void clear() {\n\t\t// 独占写锁\n\t\tlock.writeLock().lock();\n\t\ttry {\n\t\t\tthis.rawMap.clear();\n\t\t} finally {\n\t\t\tlock.writeLock().unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic Iterator<Map.Entry<K, V>> iterator() {\n\t\treturn new TransIter<>(this.rawMap.entrySet().iterator(), (entry)-> new Map.Entry<K, V>() {\n\t\t\t@Override\n\t\t\tpublic K getKey() {\n\t\t\t\treturn entry.getKey().get();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic V getValue() {\n\t\t\t\treturn entry.getValue();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic V setValue(V value) {\n\t\t\t\treturn entry.setValue(value);\n\t\t\t}\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Singleton.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * 单例类<br>\n * 提供单例对象的统一管理，当调用get方法时，如果对象池中存在此对象，返回此对象，否则创建新对象返回<br>\n *\n * @author loolly\n */\npublic final class Singleton {\n\n\tprivate static final SafeConcurrentHashMap<String, Object> POOL = new SafeConcurrentHashMap<>();\n\n\tprivate Singleton() {\n\t}\n\n\t/**\n\t * 获得指定类的单例对象<br>\n\t * 对象存在于池中返回，否则创建，每次调用此方法获得的对象为同一个对象<br>\n\t * 注意：单例针对的是类和参数，也就是说只有类、参数一致才会返回同一个对象\n\t *\n\t * @param <T>    单例对象类型\n\t * @param clazz  类\n\t * @param params 构造方法参数\n\t * @return 单例对象\n\t */\n\tpublic static <T> T get(Class<T> clazz, Object... params) {\n\t\tAssert.notNull(clazz, \"Class must be not null !\");\n\t\tfinal String key = buildKey(clazz.getName(), params);\n\t\treturn get(key, () -> ReflectUtil.newInstance(clazz, params));\n\t}\n\n\t/**\n\t * 获得指定类的单例对象<br>\n\t * 对象存在于池中返回，否则创建，每次调用此方法获得的对象为同一个对象<br>\n\t *\n\t * @param <T>      单例对象类型\n\t * @param key      自定义键\n\t * @param supplier 单例对象的创建函数\n\t * @return 单例对象\n\t * @since 5.3.3\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T get(String key, Func0<T> supplier) {\n\t\treturn (T) POOL.computeIfAbsent(key, (k)-> supplier.callWithRuntimeException());\n\t}\n\n\t/**\n\t * 获得指定类的单例对象<br>\n\t * 对象存在于池中返回，否则创建，每次调用此方法获得的对象为同一个对象<br>\n\t *\n\t * @param <T>       单例对象类型\n\t * @param className 类名\n\t * @param params    构造参数\n\t * @return 单例对象\n\t */\n\tpublic static <T> T get(String className, Object... params) {\n\t\tAssert.notBlank(className, \"Class name must be not blank !\");\n\t\tfinal Class<T> clazz = ClassUtil.loadClass(className);\n\t\treturn get(clazz, params);\n\t}\n\n\t/**\n\t * 将已有对象放入单例中，其Class做为键\n\t *\n\t * @param obj 对象\n\t * @since 4.0.7\n\t */\n\tpublic static void put(Object obj) {\n\t\tAssert.notNull(obj, \"Bean object must be not null !\");\n\t\tput(obj.getClass().getName(), obj);\n\t}\n\n\t/**\n\t * 将已有对象放入单例中，key做为键\n\t *\n\t * @param key 键\n\t * @param obj 对象\n\t * @since 5.3.3\n\t */\n\tpublic static void put(String key, Object obj) {\n\t\tPOOL.put(key, obj);\n\t}\n\n\t/**\n\t * 判断某个类的对象是否存在\n\t *\n\t * @param clazz 类\n\t * @param params 构造参数\n\t * @return 是否存在\n\t */\n\tpublic static boolean exists(Class<?> clazz, Object... params){\n\t\tif (null != clazz){\n\t\t\tfinal String key = buildKey(clazz.getName(), params);\n\t\t\treturn POOL.containsKey(key);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取单例池中存在的所有类\n\t *\n\t * @return 非重复的类集合\n\t */\n\tpublic static Set<Class<?>> getExistClass(){\n\t\treturn POOL.values().stream().map(Object::getClass).collect(Collectors.toSet());\n\t}\n\n\t/**\n\t * 移除指定Singleton对象\n\t *\n\t * @param clazz 类\n\t */\n\tpublic static void remove(Class<?> clazz) {\n\t\tif (null != clazz) {\n\t\t\tremove(clazz.getName());\n\t\t}\n\t}\n\n\t/**\n\t * 移除指定Singleton对象\n\t *\n\t * @param key 键\n\t */\n\tpublic static void remove(String key) {\n\t\tPOOL.remove(key);\n\t}\n\n\t/**\n\t * 清除所有Singleton对象\n\t */\n\tpublic static void destroy() {\n\t\tPOOL.clear();\n\t}\n\n\t// ------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 构建key\n\t *\n\t * @param className 类名\n\t * @param params    参数列表\n\t * @return key\n\t */\n\tprivate static String buildKey(String className, Object... params) {\n\t\tif (ArrayUtil.isEmpty(params)) {\n\t\t\treturn className;\n\t\t}\n\t\treturn StrUtil.format(\"{}#{}\", className, ArrayUtil.join(params, \"_\"));\n\t}\n\t// ------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Snowflake.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.date.SystemClock;\nimport cn.hutool.core.lang.id.IdConstants;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.Date;\n\n/**\n * Twitter的Snowflake 算法<br>\n * 分布式系统中，有一些需要使用全局唯一ID的场景，有些时候我们希望能使用一种简单一些的ID，并且希望ID能够按照时间有序生成。\n *\n * <p>\n * snowflake的结构如下(每部分用-分开):<br>\n *\n * <pre>\n * 符号位（1bit）- 时间戳相对值（41bit）- 数据中心标志（5bit）- 机器标志（5bit）- 递增序号（12bit）\n * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n * </pre>\n * <p>\n * 第一位为未使用(符号位表示正数)，接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>\n * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点）<br>\n * 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号）\n * <p>\n * 并且可以通过生成的id反推出生成时间,datacenterId和workerId\n * <p>\n * 参考：http://www.cnblogs.com/relucent/p/4955340.html<br>\n * 关于长度是18还是19的问题见：https://blog.csdn.net/unifirst/article/details/80408050\n *\n * @author Looly\n * @since 3.0.1\n */\npublic class Snowflake implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\n\t/**\n\t * 默认的起始时间，为Thu, 04 Nov 2010 01:42:54 GMT\n\t */\n\tpublic static long DEFAULT_TWEPOCH = 1288834974657L;\n\t/**\n\t * 默认回拨时间，2S\n\t */\n\tpublic static long DEFAULT_TIME_OFFSET = 2000L;\n\n\tprivate static final long WORKER_ID_BITS = 5L;\n\t// 最大支持机器节点数0~31，一共32个\n\t@SuppressWarnings({\"PointlessBitwiseExpression\", \"FieldCanBeLocal\"})\n\tpublic static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);\n\tprivate static final long DATA_CENTER_ID_BITS = 5L;\n\t// 最大支持数据中心节点数0~31，一共32个\n\t@SuppressWarnings({\"PointlessBitwiseExpression\", \"FieldCanBeLocal\"})\n\tpublic static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);\n\t// 序列号12位（表示只允许workId的范围为：0-4095）\n\tprivate static final long SEQUENCE_BITS = 12L;\n\t// 机器节点左移12位\n\tprivate static final long WORKER_ID_SHIFT = SEQUENCE_BITS;\n\t// 数据中心节点左移17位\n\tprivate static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;\n\t// 时间毫秒数左移22位\n\tprivate static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;\n\t// 序列掩码，用于限定序列最大值不能超过4095\n\tprivate static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095\n\n\t/**\n\t * 初始化时间点\n\t */\n\tprivate final long twepoch;\n\tprivate final long workerId;\n\tprivate final long dataCenterId;\n\tprivate final boolean useSystemClock;\n\t/**\n\t * 允许的时钟回拨毫秒数\n\t */\n\tprivate final long timeOffset;\n\t/**\n\t * 当在低频模式下时，序号始终为0，导致生成ID始终为偶数<br>\n\t * 此属性用于限定一个随机上限，在不同毫秒下生成序号时，给定一个随机数，避免偶数问题。<br>\n\t * 注意次数必须小于{@link #SEQUENCE_MASK}，{@code 0}表示不使用随机数。<br>\n\t * 这个上限不包括值本身。\n\t */\n\tprivate final long randomSequenceLimit;\n\n\t/**\n\t * 自增序号，当高频模式下时，同一毫秒内生成N个ID，则这个序号在同一毫秒下，自增以避免ID重复。\n\t */\n\tprivate long sequence = 0L;\n\tprivate long lastTimestamp = -1L;\n\n\t/**\n\t * 构造，使用自动生成的工作节点ID和数据中心ID\n\t */\n\tpublic Snowflake() {\n\t\tthis(IdConstants.DEFAULT_WORKER_ID);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param workerId 终端ID\n\t */\n\tpublic Snowflake(long workerId) {\n\t\tthis(workerId, IdConstants.DEFAULT_DATACENTER_ID);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param workerId     终端ID\n\t * @param dataCenterId 数据中心ID\n\t */\n\tpublic Snowflake(long workerId, long dataCenterId) {\n\t\tthis(workerId, dataCenterId, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param workerId         终端ID\n\t * @param dataCenterId     数据中心ID\n\t * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳\n\t */\n\tpublic Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {\n\t\tthis(null, workerId, dataCenterId, isUseSystemClock);\n\t}\n\n\t/**\n\t * @param epochDate        初始化时间起点（null表示默认起始日期）,后期修改会导致id重复,如果要修改连workerId dataCenterId，慎用\n\t * @param workerId         工作机器节点id\n\t * @param dataCenterId     数据中心id\n\t * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳\n\t * @since 5.1.3\n\t */\n\tpublic Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {\n\t\tthis(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);\n\t}\n\n\t/**\n\t * @param epochDate        初始化时间起点（null表示默认起始日期）,后期修改会导致id重复,如果要修改连workerId dataCenterId，慎用\n\t * @param workerId         工作机器节点id\n\t * @param dataCenterId     数据中心id\n\t * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳\n\t * @param timeOffset       允许时间回拨的毫秒数\n\t * @since 5.8.0\n\t */\n\tpublic Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {\n\t\tthis(epochDate, workerId, dataCenterId, isUseSystemClock, timeOffset, 0);\n\t}\n\n\t/**\n\t * @param epochDate           初始化时间起点（null表示默认起始日期）,后期修改会导致id重复,如果要修改连workerId dataCenterId，慎用\n\t * @param workerId            工作机器节点id\n\t * @param dataCenterId        数据中心id\n\t * @param isUseSystemClock    是否使用{@link SystemClock} 获取当前时间戳\n\t * @param timeOffset          允许时间回拨的毫秒数\n\t * @param randomSequenceLimit 限定一个随机上限，在不同毫秒下生成序号时，给定一个随机数，避免偶数问题，0表示无随机，上限不包括值本身。\n\t * @since 5.8.0\n\t */\n\tpublic Snowflake(Date epochDate, long workerId, long dataCenterId,\n\t\t\t\t\t boolean isUseSystemClock, long timeOffset, long randomSequenceLimit) {\n\t\tthis.twepoch = (null != epochDate) ? epochDate.getTime() : DEFAULT_TWEPOCH;\n\t\tthis.workerId = Assert.checkBetween(workerId, 0, MAX_WORKER_ID);\n\t\tthis.dataCenterId = Assert.checkBetween(dataCenterId, 0, MAX_DATA_CENTER_ID);\n\t\tthis.useSystemClock = isUseSystemClock;\n\t\tthis.timeOffset = timeOffset;\n\t\tthis.randomSequenceLimit = Assert.checkBetween(randomSequenceLimit, 0, SEQUENCE_MASK);\n\t}\n\n\t/**\n\t * 根据Snowflake的ID，获取机器id\n\t *\n\t * @param id snowflake算法生成的id\n\t * @return 所属机器的id\n\t */\n\tpublic long getWorkerId(long id) {\n\t\treturn id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);\n\t}\n\n\t/**\n\t * 根据Snowflake的ID，获取数据中心id\n\t *\n\t * @param id snowflake算法生成的id\n\t * @return 所属数据中心\n\t */\n\tpublic long getDataCenterId(long id) {\n\t\treturn id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);\n\t}\n\n\t/**\n\t * 根据Snowflake的ID，获取生成时间\n\t *\n\t * @param id snowflake算法生成的id\n\t * @return 生成的时间\n\t */\n\tpublic long getGenerateDateTime(long id) {\n\t\treturn (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;\n\t}\n\n\t/**\n\t * 根据传入时间戳-计算ID起终点\n\t *\n\t * @param timestampStart 开始时间戳\n\t * @param timestampEnd   结束时间戳\n\t * @return key-ID起点，Value-ID终点\n\t * @since 5.8.23\n\t */\n\tpublic Pair<Long, Long> getIdScopeByTimestamp(long timestampStart, long timestampEnd) {\n\t\treturn getIdScopeByTimestamp(timestampStart, timestampEnd, true);\n\t}\n\n\t/**\n\t * 根据传入时间戳-计算ID起终点 Gitee/issues/I60M14\n\t *\n\t * @param timestampStart        开始时间戳\n\t * @param timestampEnd          结束时间戳\n\t * @param ignoreCenterAndWorker 是否忽略数据中心和机器节点的占位，忽略后可获得分布式环境全局可信赖的起终点。\n\t * @return key-ID起点，Value-ID终点\n\t * @since 5.8.23\n\t */\n\tpublic Pair<Long, Long> getIdScopeByTimestamp(long timestampStart, long timestampEnd, boolean ignoreCenterAndWorker) {\n\t\tlong startTimeMinId = (timestampStart - twepoch) << TIMESTAMP_LEFT_SHIFT;\n\t\tlong endTimeMinId = (timestampEnd - twepoch) << TIMESTAMP_LEFT_SHIFT;\n\t\tif (ignoreCenterAndWorker) {\n\t\t\tlong endId = endTimeMinId | ~(-1 << TIMESTAMP_LEFT_SHIFT);\n\t\t\treturn Pair.of(startTimeMinId, endId);\n\t\t} else {\n\t\t\tlong startId = startTimeMinId | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT);\n\t\t\tlong endId = endTimeMinId | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | SEQUENCE_MASK;\n\t\t\treturn Pair.of(startId, endId);\n\t\t}\n\t}\n\n\t/**\n\t * 下一个ID\n\t *\n\t * @return ID\n\t */\n\tpublic synchronized long nextId() {\n\t\tlong timestamp = genTime();\n\t\tif (timestamp < this.lastTimestamp) {\n\t\t\tif (this.lastTimestamp - timestamp < timeOffset) {\n\t\t\t\t// 容忍指定的回拨，避免NTP校时造成的异常\n\t\t\t\ttimestamp = lastTimestamp;\n\t\t\t} else {\n\t\t\t\t// 如果服务器时间有问题(时钟后退) 报错。\n\t\t\t\tthrow new IllegalStateException(StrUtil.format(\"Clock moved backwards. Refusing to generate id for {}ms\", lastTimestamp - timestamp));\n\t\t\t}\n\t\t}\n\n\t\tif (timestamp == this.lastTimestamp) {\n\t\t\tfinal long sequence = (this.sequence + 1) & SEQUENCE_MASK;\n\t\t\tif (sequence == 0) {\n\t\t\t\ttimestamp = tilNextMillis(lastTimestamp);\n\t\t\t}\n\t\t\tthis.sequence = sequence;\n\t\t} else {\n\t\t\t// issue#I51EJY\n\t\t\tif (randomSequenceLimit > 1) {\n\t\t\t\tsequence = RandomUtil.randomLong(randomSequenceLimit);\n\t\t\t} else {\n\t\t\t\tsequence = 0L;\n\t\t\t}\n\t\t}\n\n\t\tlastTimestamp = timestamp;\n\n\t\treturn ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)\n\t\t\t\t| (dataCenterId << DATA_CENTER_ID_SHIFT)\n\t\t\t\t| (workerId << WORKER_ID_SHIFT)\n\t\t\t\t| sequence;\n\t}\n\n\t/**\n\t * 下一个ID（字符串形式）\n\t *\n\t * @return ID 字符串形式\n\t */\n\tpublic String nextIdStr() {\n\t\treturn Long.toString(nextId());\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 循环等待下一个时间\n\t *\n\t * @param lastTimestamp 上次记录的时间\n\t * @return 下一个时间\n\t */\n\tprivate long tilNextMillis(long lastTimestamp) {\n\t\tlong timestamp = genTime();\n\t\t// 循环直到操作系统时间戳变化\n\t\twhile (timestamp == lastTimestamp) {\n\t\t\ttimestamp = genTime();\n\t\t}\n\t\tif (timestamp < lastTimestamp) {\n\t\t\t// 如果发现新的时间戳比上次记录的时间戳数值小，说明操作系统时间发生了倒退，报错\n\t\t\tthrow new IllegalStateException(\n\t\t\t\t\tStrUtil.format(\"Clock moved backwards. Refusing to generate id for {}ms\", lastTimestamp - timestamp));\n\t\t}\n\t\treturn timestamp;\n\t}\n\n\t/**\n\t * 生成时间戳\n\t *\n\t * @return 时间戳\n\t */\n\tprivate long genTime() {\n\t\treturn this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Tuple.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.clone.CloneSupport;\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Spliterator;\nimport java.util.Spliterators;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\n/**\n * 不可变数组类型（元组），用于多值返回<br>\n * 多值可以支持每个元素值类型不同\n *\n * @author Looly\n */\npublic class Tuple extends CloneSupport<Tuple> implements Iterable<Object>, Serializable {\n\tprivate static final long serialVersionUID = -7689304393482182157L;\n\n\tprivate final Object[] members;\n\tprivate int hashCode;\n\tprivate boolean cacheHash;\n\n\t/**\n\t * 构造\n\t *\n\t * @param members 成员数组\n\t */\n\tpublic Tuple(Object... members) {\n\t\t// defensive copy，保证 Tuple 的不可变性，防止外部修改传入数组影响内部状态\n\t\tthis.members = members.clone();\n\t}\n\n\t/**\n\t * 获取指定位置元素\n\t *\n\t * @param <T>   返回对象类型\n\t * @param index 位置\n\t * @return 元素\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T get(int index) {\n\t\treturn (T) members[index];\n\t}\n\n\t/**\n\t * 获得所有元素\n\t *\n\t * @return 获得所有元素的副本\n\t */\n\tpublic Object[] getMembers() {\n\t\t// 返回副本而非内部数组引用，防止外部修改破坏 Tuple 的不可变性\n\t\treturn this.members.clone();\n\t}\n\n\t/**\n\t * 将元组转换成列表\n\t *\n\t * @return 转换得到的列表\n\t * @since 5.6.6\n\t */\n\tpublic final List<Object> toList() {\n\t\treturn ListUtil.toList(this.members);\n\t}\n\n\t/**\n\t * 缓存Hash值，当为true时，此对象的hash值只被计算一次，常用于Tuple中的值不变时使用。\n\t * 注意：当为true时，member变更对象后，hash值不会变更。\n\t *\n\t * @param cacheHash 是否缓存hash值\n\t * @return this\n\t * @since 5.2.1\n\t */\n\tpublic Tuple setCacheHash(boolean cacheHash) {\n\t\tthis.cacheHash = cacheHash;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 得到元组的大小\n\t *\n\t * @return 元组的大小\n\t * @since 5.6.6\n\t */\n\tpublic int size() {\n\t\treturn this.members.length;\n\t}\n\n\t/**\n\t * 判断元组中是否包含某元素\n\t *\n\t * @param value 需要判定的元素\n\t * @return 是否包含\n\t * @since 5.6.6\n\t */\n\tpublic boolean contains(Object value) {\n\t\treturn ArrayUtil.contains(this.members, value);\n\t}\n\n\t/**\n\t * 将元组转成流\n\t *\n\t * @return 流\n\t * @since 5.6.6\n\t */\n\tpublic final Stream<Object> stream() {\n\t\treturn Arrays.stream(this.members);\n\t}\n\n\t/**\n\t * 将元组转成并行流\n\t *\n\t * @return 流\n\t * @since 5.6.6\n\t */\n\tpublic final Stream<Object> parallelStream() {\n\t\treturn StreamSupport.stream(spliterator(), true);\n\t}\n\n\t/**\n\t * 截取元组指定部分\n\t *\n\t * @param start 起始位置（包括）\n\t * @param end   终止位置（不包括）\n\t * @return 截取得到的元组\n\t * @since 5.6.6\n\t */\n\tpublic final Tuple sub(final int start, final int end) {\n\t\treturn new Tuple(ArrayUtil.sub(this.members, start, end));\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tif (this.cacheHash && 0 != this.hashCode) {\n\t\t\treturn this.hashCode;\n\t\t}\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + Arrays.deepHashCode(members);\n\t\tif (this.cacheHash) {\n\t\t\tthis.hashCode = result;\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tTuple other = (Tuple) obj;\n\t\treturn false != Arrays.deepEquals(members, other.members);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn Arrays.toString(members);\n\t}\n\n\t@Override\n\tpublic Iterator<Object> iterator() {\n\t\treturn new ArrayIter<>(members);\n\t}\n\n\t@Override\n\tpublic final Spliterator<Object> spliterator() {\n\t\treturn Spliterators.spliterator(this.members, Spliterator.ORDERED);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/TypeReference.java",
    "content": "package cn.hutool.core.lang;\n\nimport java.lang.reflect.Type;\n\nimport cn.hutool.core.util.TypeUtil;\n\n/**\n * Type类型参考<br>\n * 通过构建一个类型参考子类，可以获取其泛型参数中的Type类型。例如：\n *\n * <pre>\n * TypeReference&lt;List&lt;String&gt;&gt; list = new TypeReference&lt;List&lt;String&gt;&gt;() {};\n * Type t = tr.getType();\n * </pre>\n *\n * 此类无法应用于通配符泛型参数（wildcard parameters），比如：{@code Class<?>} 或者 {@code List? extends CharSequence>}\n *\n * <p>\n * 此类参考FastJSON的TypeReference实现\n *\n * @author looly\n *\n * @param <T> 需要自定义的参考类型\n * @since 4.2.2\n */\npublic abstract class TypeReference<T> implements Type {\n\n\t/** 泛型参数 */\n\tprivate final Type type;\n\n\t/**\n\t * 构造\n\t */\n\tpublic TypeReference() {\n\t\tthis.type = TypeUtil.getTypeArgument(getClass());\n\t}\n\n\t/**\n\t * 获取用户定义的泛型参数\n\t *\n\t * @return 泛型参数\n\t */\n\tpublic Type getType() {\n\t\treturn this.type;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.type.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/UUID.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.Random;\n\n/**\n * 提供通用唯一识别码（universally unique identifier）（UUID）实现，UUID表示一个128位的值。<br>\n * 此类拷贝自java.util.UUID，用于生成不带-的UUID字符串\n *\n * <p>\n * 这些通用标识符具有不同的变体。此类的方法用于操作 Leach-Salz 变体，不过构造方法允许创建任何 UUID 变体（将在下面进行描述）。\n * <p>\n * 变体 2 (Leach-Salz) UUID 的布局如下： long 型数据的最高有效位由以下无符号字段组成：\n *\n * <pre>\n * 0xFFFFFFFF00000000 time_low\n * 0x00000000FFFF0000 time_mid\n * 0x000000000000F000 version\n * 0x0000000000000FFF time_hi\n * </pre>\n * <p>\n * long 型数据的最低有效位由以下无符号字段组成：\n *\n * <pre>\n * 0xC000000000000000 variant\n * 0x3FFF000000000000 clock_seq\n * 0x0000FFFFFFFFFFFF node\n * </pre>\n *\n * <p>\n * variant 字段包含一个表示 UUID 布局的值。以上描述的位布局仅在 UUID 的 variant 值为 2（表示 Leach-Salz 变体）时才有效。 *\n * <p>\n * version 字段保存描述此 UUID 类型的值。有 4 种不同的基本 UUID 类型：基于时间的 UUID、DCE 安全 UUID、基于名称的 UUID 和随机生成的 UUID。<br>\n * 这些类型的 version 值分别为 1、2、3 和 4。\n *\n * @since 4.1.11\n */\npublic final class UUID implements java.io.Serializable, Comparable<UUID> {\n\tprivate static final long serialVersionUID = -1185015143654744140L;\n\n\t/**\n\t * {@link SecureRandom} 的单例\n\t *\n\t * @author looly\n\t */\n\tprivate static class Holder {\n\t\tstatic final SecureRandom NUMBER_GENERATOR = RandomUtil.getSecureRandom();\n\t}\n\n\t/**\n\t * 此UUID的最高64有效位\n\t */\n\tprivate final long mostSigBits;\n\n\t/**\n\t * 此UUID的最低64有效位\n\t */\n\tprivate final long leastSigBits;\n\n\t/**\n\t * 私有构造\n\t *\n\t * @param data 数据\n\t */\n\tprivate UUID(byte[] data) {\n\t\tlong msb = 0;\n\t\tlong lsb = 0;\n\t\tassert data.length == 16 : \"data must be 16 bytes in length\";\n\t\tfor (int i = 0; i < 8; i++) {\n\t\t\tmsb = (msb << 8) | (data[i] & 0xff);\n\t\t}\n\t\tfor (int i = 8; i < 16; i++) {\n\t\t\tlsb = (lsb << 8) | (data[i] & 0xff);\n\t\t}\n\t\tthis.mostSigBits = msb;\n\t\tthis.leastSigBits = lsb;\n\t}\n\n\t/**\n\t * 使用指定的数据构造新的 UUID。\n\t *\n\t * @param mostSigBits  用于 {@code UUID} 的最高有效 64 位\n\t * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位\n\t */\n\tpublic UUID(long mostSigBits, long leastSigBits) {\n\t\tthis.mostSigBits = mostSigBits;\n\t\tthis.leastSigBits = leastSigBits;\n\t}\n\n\t/**\n\t * 获取类型 4（伪随机生成的）UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。\n\t *\n\t * @return 随机生成的 {@code UUID}\n\t */\n\tpublic static UUID fastUUID() {\n\t\treturn randomUUID(false);\n\t}\n\n\t/**\n\t * 获取类型 4（伪随机生成的）UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。\n\t *\n\t * @return 随机生成的 {@code UUID}\n\t */\n\tpublic static UUID randomUUID() {\n\t\treturn randomUUID(true);\n\t}\n\n\t/**\n\t * 获取类型 4（伪随机生成的）UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。\n\t *\n\t * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码，否则可以得到更好的性能\n\t * @return 随机生成的 {@code UUID}\n\t */\n\tpublic static UUID randomUUID(boolean isSecure) {\n\t\tfinal Random ng = isSecure ? Holder.NUMBER_GENERATOR : RandomUtil.getRandom();\n\n\t\tfinal byte[] randomBytes = new byte[16];\n\t\tng.nextBytes(randomBytes);\n\n\t\trandomBytes[6] &= 0x0f; /* clear version */\n\t\trandomBytes[6] |= 0x40; /* set to version 4 */\n\t\trandomBytes[8] &= 0x3f; /* clear variant */\n\t\trandomBytes[8] |= 0x80; /* set to IETF variant */\n\n\t\treturn new UUID(randomBytes);\n\t}\n\n\t/**\n\t * 根据指定的字节数组获取类型 3（基于名称的）UUID 的静态工厂。\n\t *\n\t * @param name 用于构造 UUID 的字节数组。\n\t * @return 根据指定数组生成的 {@code UUID}\n\t */\n\tpublic static UUID nameUUIDFromBytes(byte[] name) {\n\t\tMessageDigest md;\n\t\ttry {\n\t\t\tmd = MessageDigest.getInstance(\"MD5\");\n\t\t} catch (NoSuchAlgorithmException nsae) {\n\t\t\tthrow new InternalError(\"MD5 not supported\");\n\t\t}\n\t\tbyte[] md5Bytes = md.digest(name);\n\t\tmd5Bytes[6] &= 0x0f; /* clear version */\n\t\tmd5Bytes[6] |= 0x30; /* set to version 3 */\n\t\tmd5Bytes[8] &= 0x3f; /* clear variant */\n\t\tmd5Bytes[8] |= 0x80; /* set to IETF variant */\n\t\treturn new UUID(md5Bytes);\n\t}\n\n\t/**\n\t * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。\n\t *\n\t * @param name 指定 {@code UUID} 字符串\n\t * @return 具有指定值的 {@code UUID}\n\t * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常\n\t */\n\tpublic static UUID fromString(String name) {\n\t\tString[] components = name.split(\"-\");\n\t\tif (components.length != 5) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid UUID string: \" + name);\n\t\t}\n\t\tfor (int i = 0; i < 5; i++) {\n\t\t\tcomponents[i] = \"0x\" + components[i];\n\t\t}\n\n\t\tlong mostSigBits = Long.decode(components[0]);\n\t\tmostSigBits <<= 16;\n\t\tmostSigBits |= Long.decode(components[1]);\n\t\tmostSigBits <<= 16;\n\t\tmostSigBits |= Long.decode(components[2]);\n\n\t\tlong leastSigBits = Long.decode(components[3]);\n\t\tleastSigBits <<= 48;\n\t\tleastSigBits |= Long.decode(components[4]);\n\n\t\treturn new UUID(mostSigBits, leastSigBits);\n\t}\n\n\t/**\n\t * 返回此 UUID 的 128 位值中的最低有效 64 位。\n\t *\n\t * @return 此 UUID 的 128 位值中的最低有效 64 位。\n\t */\n\tpublic long getLeastSignificantBits() {\n\t\treturn leastSigBits;\n\t}\n\n\t/**\n\t * 返回此 UUID 的 128 位值中的最高有效 64 位。\n\t *\n\t * @return 此 UUID 的 128 位值中最高有效 64 位。\n\t */\n\tpublic long getMostSignificantBits() {\n\t\treturn mostSigBits;\n\t}\n\n\t/**\n\t * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。\n\t * <p>\n\t * 版本号具有以下含意:\n\t * <ul>\n\t * <li>1 基于时间的 UUID\n\t * <li>2 DCE 安全 UUID\n\t * <li>3 基于名称的 UUID\n\t * <li>4 随机生成的 UUID\n\t * </ul>\n\t *\n\t * @return 此 {@code UUID} 的版本号\n\t */\n\tpublic int version() {\n\t\t// Version is bits masked by 0x000000000000F000 in MS long\n\t\treturn (int) ((mostSigBits >> 12) & 0x0f);\n\t}\n\n\t/**\n\t * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。\n\t * <p>\n\t * 变体号具有以下含意：\n\t * <ul>\n\t * <li>0 为 NCS 向后兼容保留\n\t * <li>2 <a href=\"http://www.ietf.org/rfc/rfc4122.txt\">IETF&nbsp;RFC&nbsp;4122</a>(Leach-Salz), 用于此类\n\t * <li>6 保留，微软向后兼容\n\t * <li>7 保留供以后定义使用\n\t * </ul>\n\t *\n\t * @return 此 {@code UUID} 相关联的变体号\n\t */\n\tpublic int variant() {\n\t\t// This field is composed of a varying number of bits.\n\t\t// 0 - - Reserved for NCS backward compatibility\n\t\t// 1 0 - The IETF aka Leach-Salz variant (used by this class)\n\t\t// 1 1 0 Reserved, Microsoft backward compatibility\n\t\t// 1 1 1 Reserved for future definition.\n\t\treturn (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));\n\t}\n\n\t/**\n\t * 与此 UUID 相关联的时间戳值。\n\t *\n\t * <p>\n\t * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br>\n\t * 所得到的时间戳以 100 毫微秒为单位，从 UTC（通用协调时间） 1582 年 10 月 15 日零时开始。\n\t *\n\t * <p>\n\t * 时间戳值仅在在基于时间的 UUID（其 version 类型为 1）中才有意义。<br>\n\t * 如果此 {@code UUID} 不是基于时间的 UUID，则此方法抛出 UnsupportedOperationException。\n\t *\n\t * @return 时间戳值\n\t * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。\n\t */\n\tpublic long timestamp() throws UnsupportedOperationException {\n\t\tcheckTimeBase();\n\t\treturn (mostSigBits & 0x0FFFL) << 48//\n\t\t\t\t| ((mostSigBits >> 16) & 0x0FFFFL) << 32//\n\t\t\t\t| mostSigBits >>> 32;\n\t}\n\n\t/**\n\t * 与此 UUID 相关联的时钟序列值。\n\t *\n\t * <p>\n\t * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。\n\t * <p>\n\t * {@code clockSequence} 值仅在基于时间的 UUID（其 version 类型为 1）中才有意义。 如果此 UUID 不是基于时间的 UUID，则此方法抛出 UnsupportedOperationException。\n\t *\n\t * @return 此 {@code UUID} 的时钟序列\n\t * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1\n\t */\n\tpublic int clockSequence() throws UnsupportedOperationException {\n\t\tcheckTimeBase();\n\t\treturn (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);\n\t}\n\n\t/**\n\t * 与此 UUID 相关的节点值。\n\t *\n\t * <p>\n\t * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址，该地址用于生成此 UUID 以保证空间唯一性。\n\t * <p>\n\t * 节点值仅在基于时间的 UUID（其 version 类型为 1）中才有意义。<br>\n\t * 如果此 UUID 不是基于时间的 UUID，则此方法抛出 UnsupportedOperationException。\n\t *\n\t * @return 此 {@code UUID} 的节点值\n\t * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1\n\t */\n\tpublic long node() throws UnsupportedOperationException {\n\t\tcheckTimeBase();\n\t\treturn leastSigBits & 0x0000FFFFFFFFFFFFL;\n\t}\n\n\t// Object Inherited Methods\n\n\t/**\n\t * 返回此{@code UUID} 的字符串表现形式。\n\t *\n\t * <p>\n\t * UUID 的字符串表示形式由此 BNF 描述：\n\t *\n\t * <pre>\n\t * {@code\n\t * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>\n\t * time_low               = 4*<hexOctet>\n\t * time_mid               = 2*<hexOctet>\n\t * time_high_and_version  = 2*<hexOctet>\n\t * variant_and_sequence   = 2*<hexOctet>\n\t * node                   = 6*<hexOctet>\n\t * hexOctet               = <hexDigit><hexDigit>\n\t * hexDigit               = [0-9a-fA-F]\n\t * }\n\t * </pre>\n\t *\n\t * @return 此{@code UUID} 的字符串表现形式\n\t * @see #toString(boolean)\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn toString(false);\n\t}\n\n\t/**\n\t * 返回此{@code UUID} 的字符串表现形式。\n\t *\n\t * <p>\n\t * UUID 的字符串表示形式由此 BNF 描述：\n\t *\n\t * <pre>\n\t * {@code\n\t * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>\n\t * time_low               = 4*<hexOctet>\n\t * time_mid               = 2*<hexOctet>\n\t * time_high_and_version  = 2*<hexOctet>\n\t * variant_and_sequence   = 2*<hexOctet>\n\t * node                   = 6*<hexOctet>\n\t * hexOctet               = <hexDigit><hexDigit>\n\t * hexDigit               = [0-9a-fA-F]\n\t * }\n\t * </pre>\n\t *\n\t * @param isSimple 是否简单模式，简单模式为不带'-'的UUID字符串\n\t * @return 此{@code UUID} 的字符串表现形式\n\t */\n\tpublic String toString(boolean isSimple) {\n\t\tfinal StringBuilder builder = StrUtil.builder(isSimple ? 32 : 36);\n\t\t// time_low\n\t\tbuilder.append(digits(mostSigBits >> 32, 8));\n\t\tif (false == isSimple) {\n\t\t\tbuilder.append('-');\n\t\t}\n\t\t// time_mid\n\t\tbuilder.append(digits(mostSigBits >> 16, 4));\n\t\tif (false == isSimple) {\n\t\t\tbuilder.append('-');\n\t\t}\n\t\t// time_high_and_version\n\t\tbuilder.append(digits(mostSigBits, 4));\n\t\tif (false == isSimple) {\n\t\t\tbuilder.append('-');\n\t\t}\n\t\t// variant_and_sequence\n\t\tbuilder.append(digits(leastSigBits >> 48, 4));\n\t\tif (false == isSimple) {\n\t\t\tbuilder.append('-');\n\t\t}\n\t\t// node\n\t\tbuilder.append(digits(leastSigBits, 12));\n\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 返回此 UUID 的哈希码。\n\t *\n\t * @return UUID 的哈希码值。\n\t */\n\t@Override\n\tpublic int hashCode() {\n\t\tlong hilo = mostSigBits ^ leastSigBits;\n\t\treturn ((int) (hilo >> 32)) ^ (int) hilo;\n\t}\n\n\t/**\n\t * 将此对象与指定对象比较。\n\t * <p>\n\t * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值（每一位均相同）时，结果才为 {@code true}。\n\t *\n\t * @param obj 要与之比较的对象\n\t * @return 如果对象相同，则返回 {@code true}；否则返回 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif ((null == obj) || (obj.getClass() != UUID.class)) {\n\t\t\treturn false;\n\t\t}\n\t\tUUID id = (UUID) obj;\n\t\treturn (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);\n\t}\n\n\t// Comparison Operations\n\n\t/**\n\t * 将此 UUID 与指定的 UUID 比较。\n\t *\n\t * <p>\n\t * 如果两个 UUID 不同，且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段，则第一个 UUID 大于第二个 UUID。\n\t *\n\t * @param val 与此 UUID 比较的 UUID\n\t * @return 在此 UUID 小于、等于或大于 val 时，分别返回 -1、0 或 1。\n\t */\n\t@Override\n\tpublic int compareTo(UUID val) {\n\t\t// The ordering is intentionally set up so that the UUIDs\n\t\t// can simply be numerically compared as two numbers\n\t\tint compare = Long.compare(this.mostSigBits, val.mostSigBits);\n\t\tif(0 == compare){\n\t\t\tcompare = Long.compare(this.leastSigBits, val.leastSigBits);\n\t\t}\n\t\treturn compare;\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 返回指定数字对应的hex值\n\t *\n\t * @param val    值\n\t * @param digits 位\n\t * @return 值\n\t */\n\tprivate static String digits(long val, int digits) {\n\t\tlong hi = 1L << (digits * 4);\n\t\treturn Long.toHexString(hi | (val & (hi - 1))).substring(1);\n\t}\n\n\t/**\n\t * 检查是否为time-based版本UUID\n\t */\n\tprivate void checkTimeBase() {\n\t\tif (version() != 1) {\n\t\t\tthrow new UnsupportedOperationException(\"Not a time-based UUID\");\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Validator.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.core.util.CreditCodeUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.IdcardUtil;\n\nimport java.math.BigDecimal;\nimport java.net.MalformedURLException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 字段验证器（验证器），分两种类型的验证：\n *\n * <ul>\n *     <li>isXXX：通过返回boolean值判断是否满足给定格式。</li>\n *     <li>validateXXX：通过抛出异常{@link ValidateException}检查是否满足给定格式。</li>\n * </ul>\n * <p>\n * 主要验证字段非空、是否为满足指定格式等（如是否为Email、电话等）\n *\n * @author Looly\n */\npublic class Validator {\n\n\t/**\n\t * 英文字母 、数字和下划线\n\t */\n\tpublic final static Pattern GENERAL = PatternPool.GENERAL;\n\t/**\n\t * 数字\n\t */\n\tpublic final static Pattern NUMBERS = PatternPool.NUMBERS;\n\t/**\n\t * 分组\n\t */\n\tpublic final static Pattern GROUP_VAR = PatternPool.GROUP_VAR;\n\t/**\n\t * IP v4\n\t */\n\tpublic final static Pattern IPV4 = PatternPool.IPV4;\n\t/**\n\t * IP v6\n\t */\n\tpublic final static Pattern IPV6 = PatternPool.IPV6;\n\t/**\n\t * 货币\n\t */\n\tpublic final static Pattern MONEY = PatternPool.MONEY;\n\t/**\n\t * 邮件\n\t */\n\tpublic final static Pattern EMAIL = PatternPool.EMAIL;\n\n\t/**\n\t * 邮件（包含中文）\n\t */\n\tpublic final static Pattern EMAIL_WITH_CHINESE = PatternPool.EMAIL_WITH_CHINESE;\n\n\t/**\n\t * 移动电话\n\t */\n\tpublic final static Pattern MOBILE = PatternPool.MOBILE;\n\n\t/**\n\t * 身份证号码\n\t */\n\tpublic final static Pattern CITIZEN_ID = PatternPool.CITIZEN_ID;\n\n\t/**\n\t * 邮编\n\t */\n\tpublic final static Pattern ZIP_CODE = PatternPool.ZIP_CODE;\n\t/**\n\t * 生日\n\t */\n\tpublic final static Pattern BIRTHDAY = PatternPool.BIRTHDAY;\n\t/**\n\t * URL\n\t */\n\tpublic final static Pattern URL = PatternPool.URL;\n\t/**\n\t * Http URL\n\t */\n\tpublic final static Pattern URL_HTTP = PatternPool.URL_HTTP;\n\t/**\n\t * 中文字、英文字母、数字和下划线\n\t */\n\tpublic final static Pattern GENERAL_WITH_CHINESE = PatternPool.GENERAL_WITH_CHINESE;\n\t/**\n\t * UUID\n\t */\n\tpublic final static Pattern UUID = PatternPool.UUID;\n\t/**\n\t * 不带横线的UUID\n\t */\n\tpublic final static Pattern UUID_SIMPLE = PatternPool.UUID_SIMPLE;\n\t/**\n\t * 中国车牌号码\n\t */\n\tpublic final static Pattern PLATE_NUMBER = PatternPool.PLATE_NUMBER;\n\t/**\n\t * 车架号;别名：车辆识别代号 车辆识别码；十七位码\n\t */\n\tpublic final static Pattern CAR_VIN = PatternPool.CAR_VIN;\n\t/**\n\t * 驾驶证  别名：驾驶证档案编号、行驶证编号；12位数字字符串；仅限：中国驾驶证档案编号\n\t */\n\tpublic final static Pattern CAR_DRIVING_LICENCE = PatternPool.CAR_DRIVING_LICENCE;\n\n\t/**\n\t * 给定值是否为{@code true}\n\t *\n\t * @param value 值\n\t * @return 是否为<code>true</code>\n\t * @since 4.4.5\n\t */\n\tpublic static boolean isTrue(boolean value) {\n\t\treturn value;\n\t}\n\n\t/**\n\t * 给定值是否不为{@code false}\n\t *\n\t * @param value 值\n\t * @return 是否不为<code>false</code>\n\t * @since 4.4.5\n\t */\n\tpublic static boolean isFalse(boolean value) {\n\t\treturn false == value;\n\t}\n\n\t/**\n\t * 检查指定值是否为{@code true}\n\t *\n\t * @param value            值\n\t * @param errorMsgTemplate 错误消息内容模板（变量使用{}表示）\n\t * @param params           模板中变量替换后的值\n\t * @return 检查过后的值\n\t * @throws ValidateException 检查不满足条件抛出的异常\n\t * @since 4.4.5\n\t */\n\tpublic static boolean validateTrue(boolean value, String errorMsgTemplate, Object... params) throws ValidateException {\n\t\tif (isFalse(value)) {\n\t\t\tthrow new ValidateException(errorMsgTemplate, params);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查指定值是否为{@code false}\n\t *\n\t * @param value            值\n\t * @param errorMsgTemplate 错误消息内容模板（变量使用{}表示）\n\t * @param params           模板中变量替换后的值\n\t * @return 检查过后的值\n\t * @throws ValidateException 检查不满足条件抛出的异常\n\t * @since 4.4.5\n\t */\n\tpublic static boolean validateFalse(boolean value, String errorMsgTemplate, Object... params) throws ValidateException {\n\t\tif (isTrue(value)) {\n\t\t\tthrow new ValidateException(errorMsgTemplate, params);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 给定值是否为{@code null}\n\t *\n\t * @param value 值\n\t * @return 是否为<code>null</code>\n\t */\n\tpublic static boolean isNull(Object value) {\n\t\treturn null == value;\n\t}\n\n\t/**\n\t * 给定值是否不为{@code null}\n\t *\n\t * @param value 值\n\t * @return 是否不为<code>null</code>\n\t */\n\tpublic static boolean isNotNull(Object value) {\n\t\treturn null != value;\n\t}\n\n\t/**\n\t * 检查指定值是否为{@code null}\n\t *\n\t * @param <T>              被检查的对象类型\n\t * @param value            值\n\t * @param errorMsgTemplate 错误消息内容模板（变量使用{}表示）\n\t * @param params           模板中变量替换后的值\n\t * @return 检查过后的值\n\t * @throws ValidateException 检查不满足条件抛出的异常\n\t * @since 4.4.5\n\t */\n\tpublic static <T> T validateNull(T value, String errorMsgTemplate, Object... params) throws ValidateException {\n\t\tif (isNotNull(value)) {\n\t\t\tthrow new ValidateException(errorMsgTemplate, params);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 检查指定值是否非{@code null}\n\t *\n\t * @param <T>              被检查的对象类型\n\t * @param value            值\n\t * @param errorMsgTemplate 错误消息内容模板（变量使用{}表示）\n\t * @param params           模板中变量替换后的值\n\t * @return 检查过后的值\n\t * @throws ValidateException 检查不满足条件抛出的异常\n\t */\n\tpublic static <T> T validateNotNull(T value, String errorMsgTemplate, Object... params) throws ValidateException {\n\t\tif (isNull(value)) {\n\t\t\tthrow new ValidateException(errorMsgTemplate, params);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为空<br>\n\t * 对于String类型判定是否为empty(null 或 \"\")<br>\n\t *\n\t * @param value 值\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Object value) {\n\t\treturn (null == value || (value instanceof String && StrUtil.isEmpty((String) value)));\n\t}\n\n\t/**\n\t * 验证是否为非空<br>\n\t * 对于String类型判定是否为empty(null 或 \"\")<br>\n\t *\n\t * @param value 值\n\t * @return 是否为空\n\t */\n\tpublic static boolean isNotEmpty(Object value) {\n\t\treturn false == isEmpty(value);\n\t}\n\n\t/**\n\t * 验证是否为空，非空时抛出异常<br>\n\t * 对于String类型判定是否为empty(null 或 \"\")<br>\n\t *\n\t * @param <T>      值类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值，验证通过返回此值，空值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T> T validateEmpty(T value, String errorMsg) throws ValidateException {\n\t\tif (isNotEmpty(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为非空，为空时抛出异常<br>\n\t * 对于String类型判定是否为empty(null 或 \"\")<br>\n\t *\n\t * @param <T>      值类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值，验证通过返回此值，非空值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T> T validateNotEmpty(T value, String errorMsg) throws ValidateException {\n\t\tif (isEmpty(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否相等<br>\n\t * 当两值都为null返回true\n\t *\n\t * @param t1 对象1\n\t * @param t2 对象2\n\t * @return 当两值都为null或相等返回true\n\t */\n\tpublic static boolean equal(Object t1, Object t2) {\n\t\treturn ObjectUtil.equal(t1, t2);\n\t}\n\n\t/**\n\t * 验证是否相等，不相等抛出异常<br>\n\t *\n\t * @param t1       对象1\n\t * @param t2       对象2\n\t * @param errorMsg 错误信息\n\t * @return 相同值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static Object validateEqual(Object t1, Object t2, String errorMsg) throws ValidateException {\n\t\tif (false == equal(t1, t2)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn t1;\n\t}\n\n\t/**\n\t * 验证是否不等，相等抛出异常<br>\n\t *\n\t * @param t1       对象1\n\t * @param t2       对象2\n\t * @param errorMsg 错误信息\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static void validateNotEqual(Object t1, Object t2, String errorMsg) throws ValidateException {\n\t\tif (equal(t1, t2)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t}\n\n\t/**\n\t * 验证是否非空且与指定值相等<br>\n\t * 当数据为空时抛出验证异常<br>\n\t * 当两值不等时抛出异常\n\t *\n\t * @param t1       对象1\n\t * @param t2       对象2\n\t * @param errorMsg 错误信息\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static void validateNotEmptyAndEqual(Object t1, Object t2, String errorMsg) throws ValidateException {\n\t\tvalidateNotEmpty(t1, errorMsg);\n\t\tvalidateEqual(t1, t2, errorMsg);\n\t}\n\n\t/**\n\t * 验证是否非空且与指定值相等<br>\n\t * 当数据为空时抛出验证异常<br>\n\t * 当两值相等时抛出异常\n\t *\n\t * @param t1       对象1\n\t * @param t2       对象2\n\t * @param errorMsg 错误信息\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static void validateNotEmptyAndNotEqual(Object t1, Object t2, String errorMsg) throws ValidateException {\n\t\tvalidateNotEmpty(t1, errorMsg);\n\t\tvalidateNotEqual(t1, t2, errorMsg);\n\t}\n\n\t/**\n\t * 通过正则表达式验证<br>\n\t * 不符合正则抛出{@link ValidateException} 异常\n\t *\n\t * @param <T>      字符串类型\n\t * @param regex    正则\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateMatchRegex(String regex, T value, String errorMsg) throws ValidateException {\n\t\tif (false == isMatchRegex(regex, value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 通过正则表达式验证\n\t *\n\t * @param pattern 正则模式\n\t * @param value   值\n\t * @return 是否匹配正则\n\t */\n\tpublic static boolean isMatchRegex(Pattern pattern, CharSequence value) {\n\t\treturn ReUtil.isMatch(pattern, value);\n\t}\n\n\t/**\n\t * 通过正则表达式验证\n\t *\n\t * @param regex 正则\n\t * @param value 值\n\t * @return 是否匹配正则\n\t */\n\tpublic static boolean isMatchRegex(String regex, CharSequence value) {\n\t\treturn ReUtil.isMatch(regex, value);\n\t}\n\n\t/**\n\t * 验证是否为英文字母 、数字和下划线\n\t *\n\t * @param value 值\n\t * @return 是否为英文字母 、数字和下划线\n\t */\n\tpublic static boolean isGeneral(CharSequence value) {\n\t\treturn isMatchRegex(GENERAL, value);\n\t}\n\n\t/**\n\t * 验证是否为英文字母 、数字和下划线\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateGeneral(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isGeneral(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为给定长度范围的英文字母 、数字和下划线\n\t *\n\t * @param value 值\n\t * @param min   最小长度，负数自动识别为0\n\t * @param max   最大长度，0或负数表示不限制最大长度\n\t * @return 是否为给定长度范围的英文字母 、数字和下划线\n\t */\n\tpublic static boolean isGeneral(CharSequence value, int min, int max) {\n\t\tif (min < 0) {\n\t\t\tmin = 0;\n\t\t}\n\t\tString reg = \"^\\\\w{\" + min + \",\" + max + \"}$\";\n\t\tif (max <= 0) {\n\t\t\treg = \"^\\\\w{\" + min + \",}$\";\n\t\t}\n\t\treturn isMatchRegex(reg, value);\n\t}\n\n\t/**\n\t * 验证是否为给定长度范围的英文字母 、数字和下划线\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param min      最小长度，负数自动识别为0\n\t * @param max      最大长度，0或负数表示不限制最大长度\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateGeneral(T value, int min, int max, String errorMsg) throws ValidateException {\n\t\tif (false == isGeneral(value, min, max)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为给定最小长度的英文字母 、数字和下划线\n\t *\n\t * @param value 值\n\t * @param min   最小长度，负数自动识别为0\n\t * @return 是否为给定最小长度的英文字母 、数字和下划线\n\t */\n\tpublic static boolean isGeneral(CharSequence value, int min) {\n\t\treturn isGeneral(value, min, 0);\n\t}\n\n\t/**\n\t * 验证是否为给定最小长度的英文字母 、数字和下划线\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param min      最小长度，负数自动识别为0\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateGeneral(T value, int min, String errorMsg) throws ValidateException {\n\t\treturn validateGeneral(value, min, 0, errorMsg);\n\t}\n\n\t/**\n\t * 判断字符串是否全部为字母组成，包括大写和小写字母和汉字\n\t *\n\t * @param value 值\n\t * @return 是否全部为字母组成，包括大写和小写字母和汉字\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isLetter(CharSequence value) {\n\t\treturn StrUtil.isAllCharMatch(value, Character::isLetter);\n\t}\n\n\t/**\n\t * 验证是否全部为字母组成，包括大写和小写字母和汉字\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 3.3.0\n\t */\n\tpublic static <T extends CharSequence> T validateLetter(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isLetter(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 判断字符串是否全部为大写字母\n\t *\n\t * @param value 值\n\t * @return 是否全部为大写字母\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isUpperCase(CharSequence value) {\n\t\treturn StrUtil.isAllCharMatch(value, Character::isUpperCase);\n\t}\n\n\t/**\n\t * 验证字符串是否全部为大写字母\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 3.3.0\n\t */\n\tpublic static <T extends CharSequence> T validateUpperCase(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isUpperCase(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 判断字符串是否全部为小写字母\n\t *\n\t * @param value 值\n\t * @return 是否全部为小写字母\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isLowerCase(CharSequence value) {\n\t\treturn StrUtil.isAllCharMatch(value, Character::isLowerCase);\n\t}\n\n\t/**\n\t * 验证字符串是否全部为小写字母\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 3.3.0\n\t */\n\tpublic static <T extends CharSequence> T validateLowerCase(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isLowerCase(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证该字符串是否是数字\n\t *\n\t * @param value 字符串内容\n\t * @return 是否是数字\n\t */\n\tpublic static boolean isNumber(CharSequence value) {\n\t\treturn NumberUtil.isNumber(value);\n\t}\n\n\t/**\n\t * 是否包含数字\n\t *\n\t * @param value 当前字符串\n\t * @return boolean 是否存在数字\n\t * @since 5.6.5\n\t */\n\tpublic static boolean hasNumber(CharSequence value) {\n\t\treturn ReUtil.contains(PatternPool.NUMBERS, value);\n\t}\n\n\t/**\n\t * 验证是否为数字\n\t *\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static String validateNumber(String value, String errorMsg) throws ValidateException {\n\t\tif (false == isNumber(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证该字符串是否是字母（包括大写和小写字母）\n\t *\n\t * @param value 字符串内容\n\t * @return 是否是字母（包括大写和小写字母）\n\t * @since 4.1.8\n\t */\n\tpublic static boolean isWord(CharSequence value) {\n\t\treturn isMatchRegex(PatternPool.WORD, value);\n\t}\n\n\t/**\n\t * 验证是否为字母（包括大写和小写字母）\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 4.1.8\n\t */\n\tpublic static <T extends CharSequence> T validateWord(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isWord(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为货币\n\t *\n\t * @param value 值\n\t * @return 是否为货币\n\t */\n\tpublic static boolean isMoney(CharSequence value) {\n\t\treturn isMatchRegex(MONEY, value);\n\t}\n\n\t/**\n\t * 验证是否为货币\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateMoney(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isMoney(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\n\t}\n\n\t/**\n\t * 验证是否为邮政编码（中国）\n\t *\n\t * @param value 值\n\t * @return 是否为邮政编码（中国）\n\t */\n\tpublic static boolean isZipCode(CharSequence value) {\n\t\treturn isMatchRegex(ZIP_CODE, value);\n\t}\n\n\t/**\n\t * 验证是否为邮政编码（中国）\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateZipCode(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isZipCode(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为可用邮箱地址\n\t *\n\t * @param value 值\n\t * @return true为可用邮箱地址\n\t */\n\tpublic static boolean isEmail(CharSequence value) {\n\t\treturn isMatchRegex(EMAIL, value);\n\t}\n\n\t/**\n\t * 验证是否为可用邮箱地址（兼容中文邮箱地址）\n\t *\n\t * @param value 值\n\t * @param includChinese 包含中文标识\n\t * @return true为可用邮箱地址\n\t */\n\tpublic static boolean isEmail(CharSequence value,boolean includChinese) {\n\t\tif (includChinese){\n\t\t\treturn isMatchRegex(EMAIL_WITH_CHINESE, value);\n\t\t}\n\t\treturn isEmail(value);\n\t}\n\n\n\t/**\n\t * 验证是否为可用邮箱地址\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateEmail(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isEmail(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为手机号码（中国）\n\t *\n\t * @param value 值\n\t * @return 是否为手机号码（中国）\n\t */\n\tpublic static boolean isMobile(CharSequence value) {\n\t\treturn isMatchRegex(MOBILE, value);\n\t}\n\n\t/**\n\t * 验证是否为手机号码（中国）\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateMobile(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isMobile(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为身份证号码（支持18位、15位和港澳台的10位）\n\t *\n\t * @param value 身份证号，支持18位、15位和港澳台的10位\n\t * @return 是否为有效身份证号码\n\t */\n\tpublic static boolean isCitizenId(CharSequence value) {\n\t\treturn IdcardUtil.isValidCard(String.valueOf(value));\n\t}\n\n\t/**\n\t * 验证是否为身份证号码（支持18位、15位和港澳台的10位）\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateCitizenIdNumber(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isCitizenId(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为生日\n\t *\n\t * @param year  年，从1900年开始计算\n\t * @param month 月，从1开始计数\n\t * @param day   日，从1开始计数\n\t * @return 是否为生日\n\t */\n\tpublic static boolean isBirthday(int year, int month, int day) {\n\t\t// 验证年\n\t\tint thisYear = DateUtil.thisYear();\n\t\tif (year < 1900 || year > thisYear) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 验证月\n\t\tif (month < 1 || month > 12) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 验证日\n\t\tif (day < 1 || day > 31) {\n\t\t\treturn false;\n\t\t}\n\t\t// 检查几个特殊月的最大天数\n\t\tif (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (month == 2) {\n\t\t\t// 在2月，非闰年最大28，闰年最大29\n\t\t\treturn day < 29 || (day == 29 && DateUtil.isLeapYear(year));\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 验证是否为生日<br>\n\t * 只支持以下几种格式：\n\t * <ul>\n\t * <li>yyyyMMdd</li>\n\t * <li>yyyy-MM-dd</li>\n\t * <li>yyyy/MM/dd</li>\n\t * <li>yyyy.MM.dd</li>\n\t * <li>yyyy年MM月dd日</li>\n\t * </ul>\n\t *\n\t * @param value 值\n\t * @return 是否为生日\n\t */\n\tpublic static boolean isBirthday(CharSequence value) {\n\t\tfinal Matcher matcher = BIRTHDAY.matcher(value);\n\t\tif (matcher.find()) {\n\t\t\tint year = Integer.parseInt(matcher.group(1));\n\t\t\tint month = Integer.parseInt(matcher.group(3));\n\t\t\tint day = Integer.parseInt(matcher.group(5));\n\t\t\treturn isBirthday(year, month, day);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 验证验证是否为生日\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateBirthday(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isBirthday(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为IPV4地址\n\t *\n\t * @param value 值\n\t * @return 是否为IPV4地址\n\t */\n\tpublic static boolean isIpv4(CharSequence value) {\n\t\treturn isMatchRegex(IPV4, value);\n\t}\n\n\t/**\n\t * 验证是否为IPV4地址\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateIpv4(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isIpv4(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为IPV6地址\n\t *\n\t * @param value 值\n\t * @return 是否为IPV6地址\n\t */\n\tpublic static boolean isIpv6(CharSequence value) {\n\t\treturn isMatchRegex(IPV6, value);\n\t}\n\n\t/**\n\t * 验证是否为IPV6地址\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateIpv6(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isIpv6(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为MAC地址\n\t *\n\t * @param value 值\n\t * @return 是否为MAC地址\n\t * @since 4.1.3\n\t */\n\tpublic static boolean isMac(CharSequence value) {\n\t\treturn isMatchRegex(PatternPool.MAC_ADDRESS, value);\n\t}\n\n\t/**\n\t * 验证是否为MAC地址\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 4.1.3\n\t */\n\tpublic static <T extends CharSequence> T validateMac(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isMac(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为中国车牌号\n\t *\n\t * @param value 值\n\t * @return 是否为中国车牌号\n\t * @since 3.0.6\n\t */\n\tpublic static boolean isPlateNumber(CharSequence value) {\n\t\treturn isMatchRegex(PLATE_NUMBER, value);\n\t}\n\n\t/**\n\t * 验证是否为中国车牌号\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 3.0.6\n\t */\n\tpublic static <T extends CharSequence> T validatePlateNumber(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isPlateNumber(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为URL\n\t *\n\t * @param value 值\n\t * @return 是否为URL\n\t */\n\tpublic static boolean isUrl(CharSequence value) {\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tnew java.net.URL(StrUtil.str(value));\n\t\t} catch (MalformedURLException e) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 验证是否为URL\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateUrl(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isUrl(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否都为汉字\n\t *\n\t * @param value 值\n\t * @return 是否为汉字\n\t */\n\tpublic static boolean isChinese(CharSequence value) {\n\t\treturn isMatchRegex(PatternPool.CHINESES, value);\n\t}\n\n\t/**\n\t * 验证是否包含汉字\n\t *\n\t * @param value 值\n\t * @return 是否包含汉字\n\t * @since 5.2.1\n\t */\n\tpublic static boolean hasChinese(CharSequence value) {\n\t\treturn ReUtil.contains(ReUtil.RE_CHINESES, value);\n\t}\n\n\t/**\n\t * 验证是否为汉字\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    表单值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateChinese(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isChinese(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为中文字、英文字母、数字和下划线\n\t *\n\t * @param value 值\n\t * @return 是否为中文字、英文字母、数字和下划线\n\t */\n\tpublic static boolean isGeneralWithChinese(CharSequence value) {\n\t\treturn isMatchRegex(GENERAL_WITH_CHINESE, value);\n\t}\n\n\t/**\n\t * 验证是否为中文字、英文字母、数字和下划线\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateGeneralWithChinese(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isGeneralWithChinese(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为UUID<br>\n\t * 包括带横线标准格式和不带横线的简单模式\n\t *\n\t * @param value 值\n\t * @return 是否为UUID\n\t */\n\tpublic static boolean isUUID(CharSequence value) {\n\t\treturn isMatchRegex(UUID, value) || isMatchRegex(UUID_SIMPLE, value);\n\t}\n\n\t/**\n\t * 验证是否为UUID<br>\n\t * 包括带横线标准格式和不带横线的简单模式\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t */\n\tpublic static <T extends CharSequence> T validateUUID(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isUUID(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为Hex（16进制）字符串\n\t *\n\t * @param value 值\n\t * @return 是否为Hex（16进制）字符串\n\t * @since 4.3.3\n\t */\n\tpublic static boolean isHex(CharSequence value) {\n\t\treturn isMatchRegex(PatternPool.HEX, value);\n\t}\n\n\t/**\n\t * 验证是否为Hex（16进制）字符串\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @since 4.3.3\n\t */\n\tpublic static <T extends CharSequence> T validateHex(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isHex(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 检查给定的数字是否在指定范围内\n\t *\n\t * @param value 值\n\t * @param min   最小值（包含）\n\t * @param max   最大值（包含）\n\t * @return 是否满足\n\t * @since 4.1.10\n\t */\n\tpublic static boolean isBetween(Number value, Number min, Number max) {\n\t\tAssert.notNull(value);\n\t\tAssert.notNull(min);\n\t\tAssert.notNull(max);\n\t\t// 通过 NumberUtil 转换为 BigDecimal，使用 BigDecimal 进行比较以保留精度\n\t\tBigDecimal valBd = NumberUtil.toBigDecimal(value);\n\t\tBigDecimal minBd = NumberUtil.toBigDecimal(min);\n\t\tBigDecimal maxBd = NumberUtil.toBigDecimal(max);\n\t\treturn valBd.compareTo(minBd) >= 0 && valBd.compareTo(maxBd) <= 0;\n\t}\n\n\t/**\n\t * 检查给定的数字是否在指定范围内\n\t *\n\t * @param value    值\n\t * @param min      最小值（包含）\n\t * @param max      最大值（包含）\n\t * @param errorMsg 验证错误的信息\n\t * @throws ValidateException 验证异常\n\t * @since 4.1.10\n\t */\n\tpublic static void validateBetween(Number value, Number min, Number max, String errorMsg) throws ValidateException {\n\t\tif (false == isBetween(value, min, max)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t}\n\n\t/**\n\t * 是否是有效的统一社会信用代码\n\t * <pre>\n\t * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n\t * 第二部分：机构类别代码1位 (数字或大写英文字母)\n\t * 第三部分：登记管理机关行政区划码6位 (数字)\n\t * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n\t * 第五部分：校验码1位 (数字或大写英文字母)\n\t * </pre>\n\t *\n\t * @param creditCode 统一社会信用代码\n\t * @return 校验结果\n\t * @since 5.2.4\n\t */\n\tpublic static boolean isCreditCode(CharSequence creditCode) {\n\t\treturn CreditCodeUtil.isCreditCode(creditCode);\n\t}\n\n\t/**\n\t * 验证是否为车架号；别名：行驶证编号 车辆识别代号 车辆识别码\n\t *\n\t * @param value 值，17位车架号；形如：LSJA24U62JG269225、LDC613P23A1305189\n\t * @return 是否为车架号\n\t * @author dazer and ourslook\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isCarVin(CharSequence value) {\n\t\treturn isMatchRegex(CAR_VIN, value);\n\t}\n\n\t/**\n\t * 验证是否为车架号；别名：行驶证编号 车辆识别代号 车辆识别码\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @author dazer and ourslook\n\t * @since 5.6.3\n\t */\n\tpublic static <T extends CharSequence> T validateCarVin(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isCarVin(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 验证是否为驾驶证  别名：驾驶证档案编号、行驶证编号\n\t * 仅限：中国驾驶证档案编号\n\t *\n\t * @param value 值，12位数字字符串,eg:430101758218\n\t * @return 是否为档案编号\n\t * @author dazer and ourslook\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isCarDrivingLicence(CharSequence value) {\n\t\treturn isMatchRegex(CAR_DRIVING_LICENCE, value);\n\t}\n\n\n\t/**\n\t * 是否是中文姓名\n\t * 维吾尔族姓名里面的点是 · 输入法中文状态下，键盘左上角数字1前面的那个符号；<br>\n\t * 错误字符：{@code ．.。．.}<br>\n\t * 正确维吾尔族姓名：\n\t * <pre>\n\t * 霍加阿卜杜拉·麦提喀斯木\n\t * 玛合萨提别克·哈斯木别克\n\t * 阿布都热依木江·艾斯卡尔\n\t * 阿卜杜尼亚孜·毛力尼亚孜\n\t * </pre>\n\t * <pre>\n\t * ----------\n\t * 错误示例：孟  伟                reason: 有空格\n\t * 错误示例：连逍遥0               reason: 数字\n\t * 错误示例：依帕古丽-艾则孜        reason: 特殊符号\n\t * 错误示例：牙力空.买提萨力        reason: 新疆人的点不对\n\t * 错误示例：王建鹏2002-3-2        reason: 有数字、特殊符号\n\t * 错误示例：雷金默(雷皓添）        reason: 有括号\n\t * 错误示例：翟冬:亮               reason: 有特殊符号\n\t * 错误示例：李                   reason: 少于2位\n\t * ----------\n\t * </pre>\n\t * 总结中文姓名：2-60位，只能是中文和 ·\n\t *\n\t * @param value 中文姓名\n\t * @return 是否是正确的中文姓名\n\t * @author dazer\n\t * @since 5.8.0.M3\n\t */\n\tpublic static boolean isChineseName(CharSequence value) {\n\t\treturn isMatchRegex(PatternPool.CHINESE_NAME, value);\n\t}\n\n\n\t/**\n\t * 验证是否为驾驶证  别名：驾驶证档案编号、行驶证编号\n\t *\n\t * @param <T>      字符串类型\n\t * @param value    值\n\t * @param errorMsg 验证错误的信息\n\t * @return 验证后的值\n\t * @throws ValidateException 验证异常\n\t * @author dazer and ourslook\n\t * @since 5.6.3\n\t */\n\tpublic static <T extends CharSequence> T validateCarDrivingLicence(T value, String errorMsg) throws ValidateException {\n\t\tif (false == isCarDrivingLicence(value)) {\n\t\t\tthrow new ValidateException(errorMsg);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 检查给定的index是否超出长度限制，默认检查超出倍数（10倍），此方法主要用于内部，检查包括：\n\t * <ul>\n\t *     <li>数组调用setOrPadding时，最多允许padding的长度</li>\n\t *     <li>List调用setOrPadding时，最多允许padding的长度</li>\n\t *     <li>JSONArray调用setOrPadding时，最多允许padding的长度</li>\n\t * </ul>\n\t *\n\t * @param index 索引\n\t * @param size  数组、列表长度\n\t * @since 5.8.22\n\t */\n\tpublic static void checkIndexLimit(final int index, final int size) {\n\t\t// issue#3286, 增加安全检查，最多增加10倍\n\t\tif (index > (size + 1) * 10) {\n\t\t\tthrow new ValidateException(\"Index [{}] is too large for size: [{}]\", index, size);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/Version.java",
    "content": "/*\n * Copyright (c) 2024. looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          https://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.lang;\n\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.util.CharUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 字符串版本表示，用于解析版本号的不同部分并比较大小。<br>\n * 来自：java.lang.module.ModuleDescriptor.Version\n *\n * @author Looly\n */\npublic class Version implements Comparable<Version>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 解析版本字符串为Version对象\n\t *\n\t * @param v 版本字符串\n\t * @return The resulting {@code Version}\n\t * @throws IllegalArgumentException 如果 {@code v} 为 {@code null}或 \"\"或无法解析的字符串，抛出此异常\n\t */\n\tpublic static Version of(final String v) {\n\t\treturn new Version(v);\n\t}\n\n\tprivate final String version;\n\n\tprivate final List<Object> sequence;\n\tprivate final List<Object> pre;\n\tprivate final List<Object> build;\n\n\t/**\n\t * 版本对象，格式：tok+ ( '-' tok+)? ( '+' tok+)?，版本之间使用'.'或'-'分隔，版本号可能包含'+'<br>\n\t * 数字部分按照大小比较，字符串按照字典顺序比较。\n\t *\n\t * <ol>\n\t *     <li>sequence: 主版本号</li>\n\t *     <li>pre: 次版本号</li>\n\t *     <li>build: 构建版本</li>\n\t * </ol>\n\t *\n\t * @param v 版本字符串\n\t */\n\tpublic Version(final String v) {\n\t\tAssert.notNull(v, \"Null version string\");\n\t\tfinal int n = v.length();\n\n\t\tthis.version = v;\n\t\tthis.sequence = new ArrayList<>(4);\n\t\tthis.pre = new ArrayList<>(2);\n\t\tthis.build = new ArrayList<>(2);\n\n\t\tif (n == 0){\n\t\t\treturn;\n\t\t}\n\n\t\tint i = 0;\n\t\tchar c = v.charAt(i);\n\t\t// 不检查开头字符为数字，字母按照字典顺序的数字对待\n\n\t\tfinal List<Object> sequence = this.sequence;\n\t\tfinal List<Object> pre = this.pre;\n\t\tfinal List<Object> build = this.build;\n\n\t\t// 解析主版本\n\t\ti = takeNumber(v, i, sequence);\n\n\t\twhile (i < n) {\n\t\t\tc = v.charAt(i);\n\t\t\tif (c == '.') {\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c == '-' || c == '+') {\n\t\t\t\ti++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (CharUtil.isNumber(c)){\n\t\t\t\ti = takeNumber(v, i, sequence);\n\t\t\t}else{\n\t\t\t\ti = takeString(v, i, sequence);\n\t\t\t}\n\t\t}\n\n\t\tif (c == '-' && i >= n){\n\t\t\treturn;\n\t\t}\n\n\t\t// 解析次版本\n\t\twhile (i < n) {\n\t\t\tc = v.charAt(i);\n\t\t\tif (c >= '0' && c <= '9')\n\t\t\t\ti = takeNumber(v, i, pre);\n\t\t\telse\n\t\t\t\ti = takeString(v, i, pre);\n\t\t\tif (i >= n){\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tc = v.charAt(i);\n\t\t\tif (c == '.' || c == '-') {\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c == '+') {\n\t\t\t\ti++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (c == '+' && i >= n){\n\t\t\treturn;\n\t\t}\n\n\t\t// 解析build版本\n\t\twhile (i < n) {\n\t\t\tc = v.charAt(i);\n\t\t\tif (c >= '0' && c <= '9') {\n\t\t\t\ti = takeNumber(v, i, build);\n\t\t\t}else {\n\t\t\t\ti = takeString(v, i, build);\n\t\t\t}\n\t\t\tif (i >= n){\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tc = v.charAt(i);\n\t\t\tif (c == '.' || c == '-' || c == '+') {\n\t\t\t\ti++;\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic int compareTo(final Version that) {\n\t\tint c = compareTokens(this.sequence, that.sequence);\n\t\tif (c != 0) {\n\t\t\treturn c;\n\t\t}\n\t\tif (this.pre.isEmpty()) {\n\t\t\tif (!that.pre.isEmpty()) {\n\t\t\t\treturn +1;\n\t\t\t}\n\t\t} else {\n\t\t\tif (that.pre.isEmpty()) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t\tc = compareTokens(this.pre, that.pre);\n\t\tif (c != 0) {\n\t\t\treturn c;\n\t\t}\n\t\treturn compareTokens(this.build, that.build);\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object ob) {\n\t\tif (!(ob instanceof Version)){\n\t\t\treturn false;\n\t\t}\n\t\treturn compareTo((Version) ob) == 0;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn version.hashCode();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn version;\n\t}\n\n\t// region ----- private methods\n\t/**\n\t * 获取字符串中从位置i开始的数字，并加入到acc中<br>\n\t * 如 a123b，则从1开始，解析到acc中为[1, 2, 3]\n\t *\n\t * @param s 字符串\n\t * @param i 位置\n\t * @param acc 数字列表\n\t * @return 结束位置（不包含）\n\t */\n\tprivate static int takeNumber(final String s, int i, final List<Object> acc) {\n\t\tchar c = s.charAt(i);\n\t\tint d = (c - '0');\n\t\tfinal int n = s.length();\n\t\twhile (++i < n) {\n\t\t\tc = s.charAt(i);\n\t\t\tif (CharUtil.isNumber(c)) {\n\t\t\t\td = d * 10 + (c - '0');\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tacc.add(d);\n\t\treturn i;\n\t}\n\n\t// Take a string token starting at position i\n\t// Append it to the given list\n\t// Return the index of the first character not taken\n\t// Requires: s.charAt(i) is not '.'\n\t//\n\n\t/**\n\t * 获取字符串中从位置i开始的字符串，并加入到acc中<br>\n\t * 字符串结束的位置为'.'、'-'、'+'和数字\n\t *\n\t * @param s 版本字符串\n\t * @param i 开始位置\n\t * @param acc 字符串列表\n\t * @return 结束位置（不包含）\n\t */\n\tprivate static int takeString(final String s, int i, final List<Object> acc) {\n\t\tfinal int b = i;\n\t\tfinal int n = s.length();\n\t\twhile (++i < n) {\n\t\t\tfinal char c = s.charAt(i);\n\t\t\tif (c != '.' && c != '-' && c != '+' && !(c >= '0' && c <= '9')){\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tacc.add(s.substring(b, i));\n\t\treturn i;\n\t}\n\n\t/**\n\t * 比较节点\n\t * @param ts1 节点1\n\t * @param ts2 节点2\n\t * @return 比较结果\n\t */\n\tprivate int compareTokens(final List<Object> ts1, final List<Object> ts2) {\n\t\tfinal int n = Math.min(ts1.size(), ts2.size());\n\t\tfor (int i = 0; i < n; i++) {\n\t\t\tfinal Object o1 = ts1.get(i);\n\t\t\tfinal Object o2 = ts2.get(i);\n\t\t\tif ((o1 instanceof Integer && o2 instanceof Integer)\n\t\t\t\t|| (o1 instanceof String && o2 instanceof String)) {\n\t\t\t\tfinal int c = CompareUtil.compare(o1, o2, null);\n\t\t\t\tif (c == 0){\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn c;\n\t\t\t}\n\t\t\t// Types differ, so convert number to string form\n\t\t\tfinal int c = o1.toString().compareTo(o2.toString());\n\t\t\tif (c == 0){\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn c;\n\t\t}\n\t\tfinal List<Object> rest = ts1.size() > ts2.size() ? ts1 : ts2;\n\t\tfinal int e = rest.size();\n\t\tfor (int i = n; i < e; i++) {\n\t\t\tfinal Object o = rest.get(i);\n\t\t\tif (o instanceof Integer && ((Integer) o) == 0){\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn ts1.size() - ts2.size();\n\t\t}\n\t\treturn 0;\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/WeightListRandom.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * 动态按权重随机的随机池，底层是list实现。\n *\n * @param <E> 元素类型\n * @author 王叶峰\n * @since 5.8.33\n */\npublic class WeightListRandom<E> implements Serializable {\n\tprivate static final long serialVersionUID = 6902006276975764032L;\n\n\t/**\n\t * 随机元素池\n\t */\n\tprivate final ArrayList<EWeight<E>> randomPool;\n\n\t/**\n\t * 构造\n\t */\n\tpublic WeightListRandom() {\n\t\trandomPool = new ArrayList<>();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param poolSize 容量\n\t */\n\tpublic WeightListRandom(int poolSize) {\n\t\trandomPool = new ArrayList<>(poolSize);\n\t}\n\n\t/**\n\t * 添加元素\n\t *\n\t * @param e      元素\n\t * @param weight 权重\n\t */\n\tpublic void add(E e, double weight) {\n\t\tAssert.isTrue(weight > 0, \"权重必须大于0！\");\n\t\trandomPool.add(new EWeight<>(e, sumWeight() + weight));\n\t}\n\n\t/**\n\t * 移除元素\n\t *\n\t * @param e 元素\n\t * @return 是否移除成功\n\t */\n\tpublic boolean remove(E e) {\n\t\tboolean removed = false;\n\t\tdouble weight = 0;\n\t\tint i = 0;\n\t\tIterator<EWeight<E>> iterator = randomPool.iterator();\n\t\twhile (iterator.hasNext()) {\n\t\t\tEWeight<E> ew = iterator.next();\n\t\t\tif (!removed && ew.e.equals(e)) {\n\t\t\t\titerator.remove();\n\t\t\t\tweight = ew.sumWeight - (i == 0 ? 0 : randomPool.get(i - 1).sumWeight);// 权重=当前权重-上一个权重\n\t\t\t\tremoved = true;\n\t\t\t}\n\t\t\tif (removed) {\n\t\t\t\t// 重新计算后续权重\n\t\t\t\tew.sumWeight -= weight;\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t\treturn removed;\n\t}\n\n\t/**\n\t * 随机出一个元素\n\t *\n\t * @return 随机池的一个元素\n\t */\n\tpublic E next() {\n\t\tcheckEmptyPool();\n\n\t\tif (randomPool.size() == 1) {\n\t\t\treturn randomPool.get(0).e;\n\t\t}\n\t\tThreadLocalRandom random = RandomUtil.getRandom();\n\t\tdouble randVal = random.nextDouble() * sumWeight();\n\t\treturn binarySearch(randVal);\n\t}\n\n\t/**\n\t * 按照给定的总权重随机出一个元素\n\t *\n\t * @param weight 总权重\n\t * @return 随机池的一个元素或者null\n\t */\n\tpublic E randomByWeight(double weight) {\n\t\tAssert.isTrue(weight >= sumWeight(), \"权重必须大于当前总权重！\");\n\t\tThreadLocalRandom random = RandomUtil.getRandom();\n\t\tdouble randVal = random.nextDouble() * sumWeight();\n\t\tif (randVal > sumWeight()) {\n\t\t\treturn null;\n\t\t}\n\t\treturn binarySearch(randVal);\n\t}\n\n\t/**\n\t * 判断随机池是否为空\n\t *\n\t * @return 是否为空\n\t */\n\tpublic boolean isEmpty() {\n\t\treturn randomPool.isEmpty();\n\t}\n\n\tprivate static class EWeight<E> {\n\t\tfinal E e;\n\t\tdouble sumWeight;\n\n\t\tpublic EWeight(E e, double sumWeight) {\n\t\t\tthis.e = e;\n\t\t\tthis.sumWeight = sumWeight;\n\t\t}\n\t}\n\n\t/**\n\t * 二分查找小于等于key的最大值的元素\n\t *\n\t * @param key 目标值\n\t * @return 随机池的一个元素或者null 当key大于所有元素的总权重时，返回null\n\t */\n\tprivate E binarySearch(double key) {\n\t\tint low = 0;\n\t\tint high = randomPool.size() - 1;\n\n\t\twhile (low <= high) {\n\t\t\tint mid = (low + high) >>> 1;\n\t\t\tdouble midVal = randomPool.get(mid).sumWeight;\n\n\t\t\tif (midVal < key) {\n\t\t\t\tlow = mid + 1;\n\t\t\t} else if (midVal > key) {\n\t\t\t\thigh = mid - 1;\n\t\t\t} else {\n\t\t\t\treturn randomPool.get(mid).e;\n\t\t\t}\n\t\t}\n\t\treturn randomPool.get(low).e;\n\t}\n\n\tprivate double sumWeight() {\n\t\tif (randomPool.isEmpty()) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn randomPool.get(randomPool.size() - 1).sumWeight;\n\t}\n\n\tprivate void checkEmptyPool() {\n\t\tif (isEmpty()) {\n\t\t\tthrow new IllegalArgumentException(\"随机池为空！\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/WeightRandom.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.io.Serializable;\nimport java.util.Random;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n/**\n * 权重随机算法实现<br>\n * <p>\n * 平时，经常会遇到权重随机算法，从不同权重的N个元素中随机选择一个，并使得总体选择结果是按照权重分布的。如广告投放、负载均衡等。\n * </p>\n * <p>\n * 如有4个元素A、B、C、D，权重分别为1、2、3、4，随机结果中A:B:C:D的比例要为1:2:3:4。<br>\n * </p>\n * 总体思路：累加每个元素的权重A(1)-B(3)-C(6)-D(10)，则4个元素的的权重管辖区间分别为[0,1)、[1,3)、[3,6)、[6,10)。<br>\n * 然后随机出一个[0,10)之间的随机数。落在哪个区间，则该区间之后的元素即为按权重命中的元素。<br>\n *\n * <p>\n * 参考博客：https://www.cnblogs.com/waterystone/p/5708063.html\n * <p>\n *\n * @param <T> 权重随机获取的对象类型\n * @author looly\n * @since 3.3.0\n */\npublic class WeightRandom<T> implements Serializable {\n\tprivate static final long serialVersionUID = -8244697995702786499L;\n\n\tprivate final TreeMap<Double, T> weightMap;\n\n\n\t/**\n\t * 创建权重随机获取器\n\t *\n\t * @param <T> 权重随机获取的对象类型\n\t * @return {@link WeightRandom}\n\t */\n\tpublic static <T> WeightRandom<T> create() {\n\t\treturn new WeightRandom<>();\n\t}\n\n\t// ---------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic WeightRandom() {\n\t\tweightMap = new TreeMap<>();\n\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param weightObj 带有权重的对象\n\t */\n\tpublic WeightRandom(WeightObj<T> weightObj) {\n\t\tthis();\n\t\tif(null != weightObj) {\n\t\t\tadd(weightObj);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param weightObjs 带有权重的对象\n\t */\n\tpublic WeightRandom(Iterable<WeightObj<T>> weightObjs) {\n\t\tthis();\n\t\tif(CollUtil.isNotEmpty(weightObjs)) {\n\t\t\tfor (WeightObj<T> weightObj : weightObjs) {\n\t\t\t\tadd(weightObj);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param weightObjs 带有权重的对象\n\t */\n\tpublic WeightRandom(WeightObj<T>[] weightObjs) {\n\t\tthis();\n\t\tfor (WeightObj<T> weightObj : weightObjs) {\n\t\t\tadd(weightObj);\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 增加对象\n\t *\n\t * @param obj 对象\n\t * @param weight 权重\n\t * @return this\n\t */\n\tpublic WeightRandom<T> add(T obj, double weight) {\n\t\treturn add(new WeightObj<>(obj, weight));\n\t}\n\n\t/**\n\t * 增加对象权重\n\t *\n\t * @param weightObj 权重对象\n\t * @return this\n\t */\n\tpublic WeightRandom<T> add(WeightObj<T> weightObj) {\n\t\tif(null != weightObj) {\n\t\t\tfinal double weight = weightObj.getWeight();\n\t\t\tif(weightObj.getWeight() > 0) {\n\t\t\t\tdouble lastWeight = (this.weightMap.size() == 0) ? 0 : this.weightMap.lastKey();\n\t\t\t\tthis.weightMap.put(weight + lastWeight, weightObj.getObj());// 权重累加\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空权重表\n\t *\n\t * @return this\n\t */\n\tpublic WeightRandom<T> clear() {\n\t\tif(null != this.weightMap) {\n\t\t\tthis.weightMap.clear();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 下一个随机对象\n\t *\n\t * @return 随机对象\n\t */\n\tpublic T next() {\n\t\tif(MapUtil.isEmpty(this.weightMap)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Random random = RandomUtil.getRandom();\n\t\tfinal double randomWeight = this.weightMap.lastKey() * random.nextDouble();\n\t\tfinal SortedMap<Double, T> tailMap = this.weightMap.tailMap(randomWeight, false);\n\t\treturn this.weightMap.get(tailMap.firstKey());\n\t}\n\n\t/**\n\t * 带有权重的对象包装\n\t *\n\t * @author looly\n\t *\n\t * @param <T> 对象类型\n\t */\n\tpublic static class WeightObj<T> {\n\t\t/** 对象 */\n\t\tprivate T obj;\n\t\t/** 权重 */\n\t\tprivate final double weight;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param obj 对象\n\t\t * @param weight 权重\n\t\t */\n\t\tpublic WeightObj(T obj, double weight) {\n\t\t\tthis.obj = obj;\n\t\t\tthis.weight = weight;\n\t\t}\n\n\t\t/**\n\t\t * 获取对象\n\t\t *\n\t\t * @return 对象\n\t\t */\n\t\tpublic T getObj() {\n\t\t\treturn obj;\n\t\t}\n\n\t\t/**\n\t\t * 设置对象\n\t\t *\n\t\t * @param obj 对象\n\t\t */\n\t\tpublic void setObj(T obj) {\n\t\t\tthis.obj = obj;\n\t\t}\n\n\t\t/**\n\t\t * 获取权重\n\t\t *\n\t\t * @return 权重\n\t\t */\n\t\tpublic double getWeight() {\n\t\t\treturn weight;\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\tfinal int prime = 31;\n\t\t\tint result = 1;\n\t\t\tresult = prime * result + ((obj == null) ? 0 : obj.hashCode());\n\t\t\tlong temp;\n\t\t\ttemp = Double.doubleToLongBits(weight);\n\t\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\t\treturn result;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object obj) {\n\t\t\tif (this == obj) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (obj == null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (getClass() != obj.getClass()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tWeightObj<?> other = (WeightObj<?>) obj;\n\t\t\tif (this.obj == null) {\n\t\t\t\tif (other.obj != null) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else if (!this.obj.equals(other.obj)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn Double.doubleToLongBits(weight) == Double.doubleToLongBits(other.weight);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/Ansi8BitColor.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * ANSI 8-bit前景或背景色（即8位编码，共256种颜色（2^8） ）<br>\n * <ul>\n *     <li>0-7：                        标准颜色（同ESC [ 30–37 m）</li>\n *     <li>8-15：                       高强度颜色（同ESC [ 90–97 m）</li>\n *     <li>16-231（6 × 6 × 6 共 216色）： 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)</li>\n *     <li>232-255：                    从黑到白的24阶灰度色</li>\n * </ul>\n *\n * <p>来自Spring Boot</p>\n *\n * @author Toshiaki Maki, Phillip Webb\n * @see #foreground(int)\n * @see #background(int)\n * @since 5.8.0\n */\npublic final class Ansi8BitColor implements AnsiElement {\n\n\tprivate static final String PREFIX_FORE = \"38;5;\";\n\tprivate static final String PREFIX_BACK = \"48;5;\";\n\n\t/**\n\t * 前景色ANSI颜色实例\n\t *\n\t * @param code 颜色代码(0-255)\n\t * @return 前景色ANSI颜色实例\n\t */\n\tpublic static Ansi8BitColor foreground(int code) {\n\t\treturn new Ansi8BitColor(PREFIX_FORE, code);\n\t}\n\n\t/**\n\t * 背景色ANSI颜色实例\n\t *\n\t * @param code 颜色代码(0-255)\n\t * @return 背景色ANSI颜色实例\n\t */\n\tpublic static Ansi8BitColor background(int code) {\n\t\treturn new Ansi8BitColor(PREFIX_BACK, code);\n\t}\n\n\tprivate final String prefix;\n\tprivate final int code;\n\n\t/**\n\t * 构造\n\t *\n\t * @param prefix 前缀\n\t * @param code   颜色代码(0-255)\n\t * @throws IllegalArgumentException 颜色代码不在0~255范围内\n\t */\n\tprivate Ansi8BitColor(String prefix, int code) {\n\t\tAssert.isTrue(code >= 0 && code <= 255, \"Code must be between 0 and 255\");\n\t\tthis.prefix = prefix;\n\t\tthis.code = code;\n\t}\n\n\t/**\n\t * 获取颜色代码(0-255)\n\t *\n\t * @return 颜色代码(0 - 255)\n\t */\n\t@Override\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null || getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tAnsi8BitColor other = (Ansi8BitColor) obj;\n\t\treturn this.prefix.equals(other.prefix) && this.code == other.code;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn this.prefix.hashCode() * 31 + this.code;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.prefix + this.code;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiBackground.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * ANSI背景颜色枚举\n *\n * <p>来自Spring Boot</p>\n *\n * @author Phillip Webb, Geoffrey Chandler\n * @since 5.8.0\n */\npublic enum AnsiBackground implements AnsiElement {\n\n\t/**\n\t * 默认背景色\n\t */\n\tDEFAULT(49),\n\n\t/**\n\t * 黑色\n\t */\n\tBLACK(40),\n\n\t/**\n\t * 红\n\t */\n\tRED(41),\n\n\t/**\n\t * 绿\n\t */\n\tGREEN(42),\n\n\t/**\n\t * 黄\n\t */\n\tYELLOW(43),\n\n\t/**\n\t * 蓝\n\t */\n\tBLUE(44),\n\n\t/**\n\t * 品红\n\t */\n\tMAGENTA(45),\n\n\t/**\n\t * 青\n\t */\n\tCYAN(46),\n\n\t/**\n\t * 白\n\t */\n\tWHITE(47),\n\n\t/**\n\t * 亮黑\n\t */\n\tBRIGHT_BLACK(100),\n\n\t/**\n\t * 亮红\n\t */\n\tBRIGHT_RED(101),\n\n\t/**\n\t * 亮绿\n\t */\n\tBRIGHT_GREEN(102),\n\n\t/**\n\t * 亮黄\n\t */\n\tBRIGHT_YELLOW(103),\n\n\t/**\n\t * 亮蓝\n\t */\n\tBRIGHT_BLUE(104),\n\n\t/**\n\t * 亮品红\n\t */\n\tBRIGHT_MAGENTA(105),\n\n\t/**\n\t * 亮青\n\t */\n\tBRIGHT_CYAN(106),\n\n\t/**\n\t * 亮白\n\t */\n\tBRIGHT_WHITE(107);\n\n\tprivate final int code;\n\n\tAnsiBackground(int code) {\n\t\tthis.code = code;\n\t}\n\n\t/**\n\t * 获取ANSI颜色代码\n\t *\n\t * @return 颜色代码\n\t */\n\t@Override\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.toString(this.code);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiColor.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * ANSI标准颜色\n *\n * <p>来自Spring Boot</p>\n *\n * @author Phillip Webb, Geoffrey Chandler\n * @since 5.8.0\n */\npublic enum AnsiColor implements AnsiElement {\n\n\t/**\n\t * 默认前景色\n\t */\n\tDEFAULT(39),\n\n\t/**\n\t * 黑\n\t */\n\tBLACK(30),\n\n\t/**\n\t * 红\n\t */\n\tRED(31),\n\n\t/**\n\t * 绿\n\t */\n\tGREEN(32),\n\n\t/**\n\t * 黄\n\t */\n\tYELLOW(33),\n\n\t/**\n\t * 蓝\n\t */\n\tBLUE(34),\n\n\t/**\n\t * 品红\n\t */\n\tMAGENTA(35),\n\n\t/**\n\t * 青\n\t */\n\tCYAN(36),\n\n\t/**\n\t * 白\n\t */\n\tWHITE(37),\n\n\t/**\n\t * 亮黑\n\t */\n\tBRIGHT_BLACK(90),\n\n\t/**\n\t * 亮红\n\t */\n\tBRIGHT_RED(91),\n\n\t/**\n\t * 亮绿\n\t */\n\tBRIGHT_GREEN(92),\n\n\t/**\n\t * 亮黄\n\t */\n\tBRIGHT_YELLOW(93),\n\n\t/**\n\t * 亮蓝\n\t */\n\tBRIGHT_BLUE(94),\n\n\t/**\n\t * 亮品红\n\t */\n\tBRIGHT_MAGENTA(95),\n\n\t/**\n\t * 亮青\n\t */\n\tBRIGHT_CYAN(96),\n\n\t/**\n\t * 亮白\n\t */\n\tBRIGHT_WHITE(97);\n\n\tprivate final int code;\n\n\tAnsiColor(int code) {\n\t\tthis.code = code;\n\t}\n\n\t/**\n\t * 获取ANSI颜色代码\n\t *\n\t * @return 颜色代码\n\t */\n\t@Override\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.toString(this.code);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiColorWrapper.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Objects;\n\n/**\n * ANSI 颜色包装类\n *\n * @author TomXin\n * @since 5.8.6\n */\npublic class AnsiColorWrapper {\n\n\tprivate final int code;\n\n\tprivate final AnsiColors.BitDepth bitDepth;\n\n\t/**\n\t * 创建指定位深度的 {@code AnsiColorWrapper} 实例\n\t *\n\t * @param code     颜色编码，位深度为4bit时，code取值范围[30~37]，[90~97]。位深度为8bit时，code取值范围[0~255]\n\t * @param bitDepth 位深度\n\t */\n\tpublic AnsiColorWrapper(int code, AnsiColors.BitDepth bitDepth) {\n\t\tif (bitDepth == AnsiColors.BitDepth.FOUR) {\n\t\t\tAssert.isTrue((30 <= code && code <= 37) || (90 <= code && code <= 97), \"The value of 4 bit color only supported [30~37],[90~97].\");\n\t\t}\n\t\tAssert.isTrue((0 <= code && code <= 255), \"The value of 8 bit color only supported [0~255].\");\n\t\tthis.code = code;\n\t\tthis.bitDepth = bitDepth;\n\t}\n\n\t/**\n\t * 转换为 {@link AnsiElement} 实例\n\t *\n\t * @param foreOrBack 区分前景还是背景\n\t * @return {@link AnsiElement} 实例\n\t */\n\tpublic AnsiElement toAnsiElement(ForeOrBack foreOrBack) {\n\t\tif (bitDepth == AnsiColors.BitDepth.FOUR) {\n\t\t\tif (foreOrBack == ForeOrBack.FORE) {\n\t\t\t\tfor (AnsiColor item : AnsiColor.values()) {\n\t\t\t\t\tif (item.getCode() == this.code) {\n\t\t\t\t\t\treturn item;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"No matched AnsiColor instance,code={}\", this.code));\n\t\t\t}\n\t\t\tfor (AnsiBackground item : AnsiBackground.values()) {\n\t\t\t\tif (item.getCode() == this.code + 10) {\n\t\t\t\t\treturn item;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"No matched AnsiBackground instance,code={}\", this.code));\n\t\t}\n\t\tif (foreOrBack == ForeOrBack.FORE) {\n\t\t\treturn Ansi8BitColor.foreground(this.code);\n\t\t}\n\t\treturn Ansi8BitColor.background(this.code);\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tAnsiColorWrapper that = (AnsiColorWrapper) o;\n\t\treturn this.code == that.code && this.bitDepth == that.bitDepth;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(this.code, this.bitDepth);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiColors.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.img.LabColor;\n\nimport java.awt.Color;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n *\n * 在 {@link Color AWT Colors} 的上下文中使用 {@link AnsiColor} 的实用程序\n * <p>来自Spring Boot</p>\n *\n * @author Craig Burke,Ruben Dijkstra,Phillip Webb,Michael Simons,Tom Xin\n * @since 5.8.6\n */\npublic final class AnsiColors {\n\n\tprivate static final Map<AnsiColorWrapper, LabColor> ANSI_COLOR_MAP;\n\n\t/**\n\t * @see AnsiColor#BRIGHT_WHITE\n\t */\n\tprivate static final int CODE_OF_4_BIT_ANSI_COLOR_BRIGHT_WHITE = 97;\n\n\tstatic {\n\t\tMap<AnsiColorWrapper, LabColor> colorMap = new LinkedHashMap<>(16, 1);\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BLACK.getCode(), BitDepth.FOUR), new LabColor(0x000000));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.RED.getCode(), BitDepth.FOUR), new LabColor(0xAA0000));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.GREEN.getCode(), BitDepth.FOUR), new LabColor(0x00AA00));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.YELLOW.getCode(), BitDepth.FOUR), new LabColor(0xAA5500));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BLUE.getCode(), BitDepth.FOUR), new LabColor(0x0000AA));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.MAGENTA.getCode(), BitDepth.FOUR), new LabColor(0xAA00AA));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.CYAN.getCode(), BitDepth.FOUR), new LabColor(0x00AAAA));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.WHITE.getCode(), BitDepth.FOUR), new LabColor(0xAAAAAA));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_BLACK.getCode(), BitDepth.FOUR), new LabColor(0x555555));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_RED.getCode(), BitDepth.FOUR), new LabColor(0xFF5555));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_GREEN.getCode(), BitDepth.FOUR), new LabColor(0x55FF00));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_YELLOW.getCode(), BitDepth.FOUR), new LabColor(0xFFFF55));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_BLUE.getCode(), BitDepth.FOUR), new LabColor(0x5555FF));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_MAGENTA.getCode(), BitDepth.FOUR), new LabColor(0xFF55FF));\n\t\tcolorMap.put(new AnsiColorWrapper(AnsiColor.BRIGHT_CYAN.getCode(), BitDepth.FOUR), new LabColor(0x55FFFF));\n\t\tcolorMap.put(new AnsiColorWrapper(CODE_OF_4_BIT_ANSI_COLOR_BRIGHT_WHITE, BitDepth.FOUR), new LabColor(0xFFFFFF));\n\t\tANSI_COLOR_MAP = Collections.unmodifiableMap(colorMap);\n\t}\n\n\tprivate static final int[] ANSI_8BIT_COLOR_CODE_LOOKUP = new int[] { 0x000000, 0x800000, 0x008000, 0x808000,\n\t\t\t0x000080, 0x800080, 0x008080, 0xc0c0c0, 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff,\n\t\t\t0x00ffff, 0xffffff, 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f,\n\t\t\t0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff,\n\t\t\t0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, 0x00d700, 0x00d75f, 0x00d787, 0x00d7af,\n\t\t\t0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f,\n\t\t\t0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff,\n\t\t\t0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf,\n\t\t\t0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f,\n\t\t\t0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff,\n\t\t\t0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af,\n\t\t\t0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 0x87d700, 0x87d75f,\n\t\t\t0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff,\n\t\t\t0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf,\n\t\t\t0xaf5fd7, 0xaf5fff, 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f,\n\t\t\t0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff,\n\t\t\t0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, 0xd70000, 0xd7005f, 0xd70087, 0xd700af,\n\t\t\t0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f,\n\t\t\t0xd78787, 0xd787af, 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff,\n\t\t\t0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf,\n\t\t\t0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f,\n\t\t\t0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff,\n\t\t\t0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af,\n\t\t\t0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, 0x080808, 0x121212,\n\t\t\t0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 0x585858, 0x626262, 0x6c6c6c, 0x767676,\n\t\t\t0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada,\n\t\t\t0xe4e4e4, 0xeeeeee };\n\n\tprivate final Map<AnsiColorWrapper, LabColor> lookup;\n\n\t/**\n\t * 创建具有指定位深度的新 {@code AnsiColors} 实例。\n\t * @param bitDepth 所需的位深度\n\t */\n\tpublic AnsiColors(BitDepth bitDepth) {\n\t\tthis.lookup = getLookup(bitDepth);\n\t}\n\n\tprivate Map<AnsiColorWrapper, LabColor> getLookup(BitDepth bitDepth) {\n\t\tif (bitDepth == BitDepth.EIGHT) {\n\t\t\tfinal Map<AnsiColorWrapper, LabColor> lookup = new LinkedHashMap<>(256, 1);\n\t\t\tfor (int i = 0; i < ANSI_8BIT_COLOR_CODE_LOOKUP.length; i++) {\n\t\t\t\tlookup.put(new AnsiColorWrapper(i,BitDepth.EIGHT), new LabColor(ANSI_8BIT_COLOR_CODE_LOOKUP[i]));\n\t\t\t}\n\t\t\treturn Collections.unmodifiableMap(lookup);\n\t\t}\n\t\treturn ANSI_COLOR_MAP;\n\t}\n\n\t/**\n\t * 找到最接近给定 AWT {@link Color} 的 {@link AnsiColorWrapper ANSI 颜色包装} 实例。\n\t * @param color AWT 颜色\n\t * @return 最接近指定 ANSI 颜色的 {@link AnsiColorWrapper ANSI 颜色包装} 实例\n\t */\n\tpublic AnsiColorWrapper findClosest(Color color) {\n\t\treturn findClosest(new LabColor(color));\n\t}\n\n\tprivate AnsiColorWrapper findClosest(LabColor color) {\n\t\tAnsiColorWrapper closest = null;\n\t\tdouble closestDistance = Float.MAX_VALUE;\n\t\tfor (Map.Entry<AnsiColorWrapper, LabColor> entry : this.lookup.entrySet()) {\n\t\t\tdouble candidateDistance = color.getDistance(entry.getValue());\n\t\t\tif (closest == null || candidateDistance < closestDistance) {\n\t\t\t\tclosestDistance = candidateDistance;\n\t\t\t\tclosest = entry.getKey();\n\t\t\t}\n\t\t}\n\t\treturn closest;\n\t}\n\n\t/**\n\t * 此类支持的位深度。\n\t */\n\tpublic enum BitDepth {\n\n\t\t/**\n\t\t * 4位 (16色).\n\t\t * @see AnsiColor\n\t\t * @see AnsiBackground\n\t\t */\n\t\tFOUR(4),\n\n\t\t/**\n\t\t * 8位 (256色).\n\t\t * @see Ansi8BitColor\n\t\t */\n\t\tEIGHT(8);\n\n\t\tprivate final int bits;\n\n\t\tBitDepth(int bits) {\n\t\t\tthis.bits = bits;\n\t\t}\n\n\t\tpublic static BitDepth of(int bits) {\n\t\t\tfor (BitDepth candidate : values()) {\n\t\t\t\tif (candidate.bits == bits) {\n\t\t\t\t\treturn candidate;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Unsupported ANSI bit depth '\" + bits + \"'\");\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiElement.java",
    "content": "package cn.hutool.core.lang.ansi;\n\n/**\n * ANSI可转义节点接口，实现为ANSI颜色等\n *\n * <p>来自Spring Boot</p>\n *\n * @author Phillip Webb\n */\npublic interface AnsiElement {\n\n\t/**\n\t * @return ANSI转义编码\n\t */\n\t@Override\n\tString toString();\n\n\t/**\n\t * 获取ANSI代码，默认返回-1\n\t * @return ANSI代码\n\t * @since 5.8.7\n\t */\n\tdefault int getCode(){\n\t\treturn -1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiEncoder.java",
    "content": "package cn.hutool.core.lang.ansi;\n\n/**\n * 生成ANSI格式的编码输出\n *\n * @author Phillip Webb\n * @since 1.0.0\n */\npublic abstract class AnsiEncoder {\n\n\tprivate static final String ENCODE_JOIN = \";\";\n\tprivate static final String ENCODE_START = \"\\033[\";\n\tprivate static final String ENCODE_END = \"m\";\n\tprivate static final String RESET = \"0;\" + AnsiColor.DEFAULT;\n\n\t/**\n\t * 创建ANSI字符串，参数中的{@link AnsiElement}会被转换为编码形式。\n\t *\n\t * @param elements 节点数组\n\t * @return ANSI字符串\n\t */\n\tpublic static String encode(Object... elements) {\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tbuildEnabled(sb, elements);\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 追加需要需转义的节点\n\t *\n\t * @param sb       {@link StringBuilder}\n\t * @param elements 节点列表\n\t */\n\tprivate static void buildEnabled(StringBuilder sb, Object[] elements) {\n\t\tboolean writingAnsi = false;\n\t\tboolean containsEncoding = false;\n\t\tfor (Object element : elements) {\n\t\t\tif (null == element) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (element instanceof AnsiElement) {\n\t\t\t\tcontainsEncoding = true;\n\t\t\t\tif (writingAnsi) {\n\t\t\t\t\tsb.append(ENCODE_JOIN);\n\t\t\t\t} else {\n\t\t\t\t\tsb.append(ENCODE_START);\n\t\t\t\t\twritingAnsi = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (writingAnsi) {\n\t\t\t\t\tsb.append(ENCODE_END);\n\t\t\t\t\twritingAnsi = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsb.append(element);\n\t\t}\n\n\t\t// 恢复默认\n\t\tif (containsEncoding) {\n\t\t\tsb.append(writingAnsi ? ENCODE_JOIN : ENCODE_START);\n\t\t\tsb.append(RESET);\n\t\t\tsb.append(ENCODE_END);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/AnsiStyle.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * ANSI文本样式风格枚举\n *\n * <p>来自Spring Boot</p>\n *\n * @author Phillip Webb\n * @since 5.8.0\n */\npublic enum AnsiStyle implements AnsiElement {\n\n\t/**\n\t * 重置/正常\n\t */\n\tNORMAL(0),\n\n\t/**\n\t * 粗体或增加强度\n\t */\n\tBOLD(1),\n\n\t/**\n\t * 弱化（降低强度）\n\t */\n\tFAINT(2),\n\n\t/**\n\t * 斜体\n\t */\n\tITALIC(3),\n\n\t/**\n\t * 下划线\n\t */\n\tUNDERLINE(4);\n\n\tprivate final int code;\n\n\tAnsiStyle(int code) {\n\t\tthis.code = code;\n\t}\n\n\t/**\n\t * 获取ANSI文本样式风格代码\n\t *\n\t * @return 文本样式风格代码\n\t */\n\t@Override\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.toString(this.code);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/ForeOrBack.java",
    "content": "package cn.hutool.core.lang.ansi;\n\n/**\n * 区分前景还是背景\n */\npublic enum ForeOrBack{\n\n\t/**\n\t * 前景\n\t */\n\tFORE,\n\t/**\n\t * 背景\n\t */\n\tBACK,\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ansi/package-info.java",
    "content": "/**\n * 命令行终端中ANSI 转义序列相关封装，如ANSI颜色等\n *\n * @author spring, looly\n */\npackage cn.hutool.core.lang.ansi;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/caller/Caller.java",
    "content": "package cn.hutool.core.lang.caller;\n\n/**\n * 调用者接口<br>\n * 可以通过此接口的实现类方法获取调用者、多级调用者以及判断是否被调用\n *\n * @author Looly\n *\n */\npublic interface Caller {\n\t/**\n\t * 获得调用者\n\t *\n\t * @return 调用者\n\t */\n\tClass<?> getCaller();\n\n\t/**\n\t * 获得调用者的调用者\n\t *\n\t * @return 调用者的调用者\n\t */\n\tClass<?> getCallerCaller();\n\n\t/**\n\t * 获得调用者，指定第几级调用者 调用者层级关系：\n\t *\n\t * <pre>\n\t * 0 {@link CallerUtil}\n\t * 1 调用{@link CallerUtil}中方法的类\n\t * 2 调用者的调用者\n\t * ...\n\t * </pre>\n\t *\n\t * @param depth 层级。0表示{@link CallerUtil}本身，1表示调用{@link CallerUtil}的类，2表示调用者的调用者，依次类推\n\t * @return 第几级调用者\n\t */\n\tClass<?> getCaller(int depth);\n\n\t/**\n\t * 是否被指定类调用\n\t *\n\t * @param clazz 调用者类\n\t * @return 是否被调用\n\t */\n\tboolean isCalledBy(Class<?> clazz);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java",
    "content": "package cn.hutool.core.lang.caller;\n\n/**\n * 调用者。可以通过此类的方法获取调用者、多级调用者以及判断是否被调用\n *\n * @author Looly\n * @since 4.1.6\n */\npublic class CallerUtil {\n\tprivate static final Caller INSTANCE;\n\tstatic {\n\t\tINSTANCE = tryCreateCaller();\n\t}\n\n\t/**\n\t * 获得调用者\n\t *\n\t * @return 调用者\n\t */\n\tpublic static Class<?> getCaller() {\n\t\treturn INSTANCE.getCaller();\n\t}\n\n\t/**\n\t * 获得调用者的调用者\n\t *\n\t * @return 调用者的调用者\n\t */\n\tpublic static Class<?> getCallerCaller() {\n\t\treturn INSTANCE.getCallerCaller();\n\t}\n\n\t/**\n\t * 获得调用者，指定第几级调用者<br>\n\t * 调用者层级关系：\n\t *\n\t * <pre>\n\t * 0 CallerUtil\n\t * 1 调用CallerUtil中方法的类\n\t * 2 调用者的调用者\n\t * ...\n\t * </pre>\n\t *\n\t * @param depth 层级。0表示CallerUtil本身，1表示调用CallerUtil的类，2表示调用者的调用者，依次类推\n\t * @return 第几级调用者\n\t */\n\tpublic static Class<?> getCaller(int depth) {\n\t\treturn INSTANCE.getCaller(depth);\n\t}\n\n\t/**\n\t * 是否被指定类调用\n\t *\n\t * @param clazz 调用者类\n\t * @return 是否被调用\n\t */\n\tpublic static boolean isCalledBy(Class<?> clazz) {\n\t\treturn INSTANCE.isCalledBy(clazz);\n\t}\n\n\t/**\n\t * 获取调用此方法的方法名\n\t *\n\t * @param isFullName 是否返回全名，全名包括方法所在类的全路径名\n\t * @return 调用此方法的方法名\n\t * @since 5.2.4\n\t */\n\tpublic static String getCallerMethodName(boolean isFullName){\n\t\tfinal StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[2];\n\t\tfinal String methodName = stackTraceElement.getMethodName();\n\t\tif(false == isFullName){\n\t\t\treturn methodName;\n\t\t}\n\n\t\treturn stackTraceElement.getClassName() + \".\" + methodName;\n\t}\n\n\t/**\n\t * 尝试创建{@link Caller}实现\n\t *\n\t * @return {@link Caller}实现\n\t */\n\tprivate static Caller tryCreateCaller() {\n\t\tCaller caller;\n\t\ttry {\n\t\t\tcaller = new SecurityManagerCaller();\n\t\t\tif(null != caller.getCaller() && null != caller.getCallerCaller()) {\n\t\t\t\treturn caller;\n\t\t\t}\n\t\t} catch (Throwable e) {\n\t\t\t//ignore\n\t\t}\n\n\t\tcaller = new StackTraceCaller();\n\t\treturn caller;\n\t}\n\t// ---------------------------------------------------------------------------------------------- static interface and class\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java",
    "content": "package cn.hutool.core.lang.caller;\n\nimport java.io.Serializable;\n\nimport cn.hutool.core.util.ArrayUtil;\n\n/**\n * {@link SecurityManager} 方式获取调用者\n *\n * @author Looly\n */\npublic class SecurityManagerCaller extends SecurityManager implements Caller, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int OFFSET = 1;\n\n\t@Override\n\tpublic Class<?> getCaller() {\n\t\tfinal Class<?>[] context = getClassContext();\n\t\tif (null != context && (OFFSET + 1) < context.length) {\n\t\t\treturn context[OFFSET + 1];\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic Class<?> getCallerCaller() {\n\t\tfinal Class<?>[] context = getClassContext();\n\t\tif (null != context && (OFFSET + 2) < context.length) {\n\t\t\treturn context[OFFSET + 2];\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic Class<?> getCaller(int depth) {\n\t\tfinal Class<?>[] context = getClassContext();\n\t\tif (null != context && (OFFSET + depth) < context.length) {\n\t\t\treturn context[OFFSET + depth];\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic boolean isCalledBy(Class<?> clazz) {\n\t\tfinal Class<?>[] classes = getClassContext();\n\t\tif(ArrayUtil.isNotEmpty(classes)) {\n\t\t\tfor (Class<?> contextClass : classes) {\n\t\t\t\tif (contextClass.equals(clazz)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java",
    "content": "package cn.hutool.core.lang.caller;\n\nimport java.io.Serializable;\n\nimport cn.hutool.core.exceptions.UtilException;\n\n/**\n * 通过StackTrace方式获取调用者。此方式效率最低，不推荐使用\n *\n * @author Looly\n */\npublic class StackTraceCaller implements Caller, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\tprivate static final int OFFSET = 2;\n\n\t@Override\n\tpublic Class<?> getCaller() {\n\t\tfinal StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\n\t\tif (OFFSET + 1 >= stackTrace.length) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal String className = stackTrace[OFFSET + 1].getClassName();\n\t\ttry {\n\t\t\treturn Class.forName(className);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new UtilException(e, \"[{}] not found!\", className);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Class<?> getCallerCaller() {\n\t\tfinal StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\n\t\tif (OFFSET + 2 >= stackTrace.length) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal String className = stackTrace[OFFSET + 2].getClassName();\n\t\ttry {\n\t\t\treturn Class.forName(className);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new UtilException(e, \"[{}] not found!\", className);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Class<?> getCaller(int depth) {\n\t\tfinal StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\n\t\tif (OFFSET + depth >= stackTrace.length) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal String className = stackTrace[OFFSET + depth].getClassName();\n\t\ttry {\n\t\t\treturn Class.forName(className);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new UtilException(e, \"[{}] not found!\", className);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean isCalledBy(Class<?> clazz) {\n\t\tfinal StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\n\t\tfor (final StackTraceElement element : stackTrace) {\n\t\t\tif (element.getClassName().equals(clazz.getName())) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/caller/package-info.java",
    "content": "/**\n * 调用者接口及实现。可以通过此类的方法获取调用者、多级调用者以及判断是否被调用\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.caller;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/copier/Copier.java",
    "content": "package cn.hutool.core.lang.copier;\n\n/**\n * 拷贝接口\n * @author Looly\n *\n * @param <T> 拷贝目标类型\n */\n@FunctionalInterface\npublic interface Copier<T> {\n\t/**\n\t * 执行拷贝\n\t * @return 拷贝的目标\n\t */\n\tT copy();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java",
    "content": "package cn.hutool.core.lang.copier;\n\nimport java.io.Serializable;\n\nimport cn.hutool.core.lang.Filter;\n\n/**\n * 复制器抽象类<br>\n * 抽象复制器抽象了一个对象复制到另一个对象，通过实现{@link #copy()}方法实现复制逻辑。<br>\n *\n * @author Looly\n *\n * @param <T> 拷贝的对象\n * @param <C> 本类的类型。用于set方法返回本对象，方便流式编程\n * @since 3.0.9\n */\npublic abstract class SrcToDestCopier<T, C extends SrcToDestCopier<T, C>> implements Copier<T>, Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 源 */\n\tprotected T src;\n\t/** 目标 */\n\tprotected T dest;\n\t/** 拷贝过滤器，可以过滤掉不需要拷贝的源 */\n\tprotected Filter<T> copyFilter;\n\n\t//-------------------------------------------------------------------------------------------------------- Getters and Setters start\n\t/**\n\t * 获取源\n\t * @return 源\n\t */\n\tpublic T getSrc() {\n\t\treturn src;\n\t}\n\t/**\n\t * 设置源\n\t *\n\t * @param src 源\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic C setSrc(T src) {\n\t\tthis.src = src;\n\t\treturn (C)this;\n\t}\n\n\t/**\n\t * 获得目标\n\t *\n\t * @return 目标\n\t */\n\tpublic T getDest() {\n\t\treturn dest;\n\t}\n\t/**\n\t * 设置目标\n\t *\n\t * @param dest 目标\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic C setDest(T dest) {\n\t\tthis.dest = dest;\n\t\treturn (C)this;\n\t}\n\n\t/**\n\t * 获得过滤器\n\t * @return 过滤器\n\t */\n\tpublic Filter<T> getCopyFilter() {\n\t\treturn copyFilter;\n\t}\n\t/**\n\t * 设置过滤器\n\t *\n\t * @param copyFilter 过滤器\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic C setCopyFilter(Filter<T> copyFilter) {\n\t\tthis.copyFilter = copyFilter;\n\t\treturn (C)this;\n\t}\n\t//-------------------------------------------------------------------------------------------------------- Getters and Setters end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/copier/package-info.java",
    "content": "/**\n * 拷贝抽象实现，通过抽象拷贝，可以实现文件、流、Buffer之间的拷贝实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.copier;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Consumer3.java",
    "content": "package cn.hutool.core.lang.func;\n\n/**\n * 3参数Consumer\n *\n * @param <P1> 参数一类型\n * @param <P2> 参数二类型\n * @param <P3> 参数三类型\n * @author TomXin\n * @since 5.7.22\n */\n@FunctionalInterface\npublic interface Consumer3<P1, P2, P3> {\n\n\t/**\n\t * 接收参数方法\n\t *\n\t * @param p1 参数一\n\t * @param p2 参数二\n\t * @param p3 参数三\n\t */\n\tvoid accept(P1 p1, P2 p2, P3 p3);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Func.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @param <P> 参数类型\n * @param <R> 返回值类型\n * @since 3.1.0\n */\n@FunctionalInterface\npublic interface Func<P, R> extends Serializable {\n\t/**\n\t * 执行函数\n\t *\n\t * @param parameters 参数列表\n\t * @return 函数执行结果\n\t * @throws Exception 自定义异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tR call(P... parameters) throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @param parameters 参数列表\n\t * @return 函数执行结果\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault R callWithRuntimeException(P... parameters){\n\t\ttry {\n\t\t\treturn call(parameters);\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Func0.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 无参数的函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @param <R> 返回值类型\n * @since 4.5.2\n */\n@FunctionalInterface\npublic interface Func0<R> extends Serializable {\n\t/**\n\t * 执行函数\n\t *\n\t * @return 函数执行结果\n\t * @throws Exception 自定义异常\n\t */\n\tR call() throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @return 函数执行结果\n\t * @since 5.3.6\n\t */\n\tdefault R callWithRuntimeException(){\n\t\ttry {\n\t\t\treturn call();\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Func1.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 只有一个参数的函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @param <P> 参数类型\n * @param <R> 返回值类型\n * @since 4.2.2\n */\n@FunctionalInterface\npublic interface Func1<P, R> extends Serializable {\n\n\t/**\n\t * 执行函数\n\t *\n\t * @param parameter 参数\n\t * @return 函数执行结果\n\t * @throws Exception 自定义异常\n\t */\n\tR call(P parameter) throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @param parameter 参数\n\t * @return 函数执行结果\n\t * @since 5.3.6\n\t */\n\tdefault R callWithRuntimeException(P parameter){\n\t\ttry {\n\t\t\treturn call(parameter);\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/LambdaUtil.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.invoke.MethodHandleInfo;\nimport java.lang.invoke.SerializedLambda;\n\n/**\n * Lambda相关工具类\n *\n * @author looly, Scen\n * @since 5.6.3\n */\npublic class LambdaUtil {\n\n\tprivate static final WeakKeyValueConcurrentMap<String, SerializedLambda> cache = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 通过对象的方法或类的静态方法引用，获取lambda实现类\n\t * 传入lambda无参数但含有返回值的情况能够匹配到此方法：\n\t * <ul>\n\t * <li>引用特定对象的实例方法：<pre>{@code\n\t * MyTeacher myTeacher = new MyTeacher();\n\t * Class<MyTeacher> supplierClass = LambdaUtil.getRealClass(myTeacher::getAge);\n\t * assertEquals(MyTeacher.class, supplierClass);\n\t * }</pre></li>\n\t * <li>引用静态无参方法：<pre>{@code\n\t * Class<MyTeacher> staticSupplierClass = LambdaUtil.getRealClass(MyTeacher::takeAge);\n\t * assertEquals(MyTeacher.class, staticSupplierClass);\n\t * }</pre></li>\n\t * </ul>\n\t * 在以下场景无法获取到正确类型\n\t * <pre>{@code\n\t * // 枚举测试，只能获取到枚举类型\n\t * Class<Enum<?>> enumSupplierClass = LambdaUtil.getRealClass(LambdaUtil.LambdaKindEnum.REF_NONE::ordinal);\n\t * assertEquals(Enum.class, enumSupplierClass);\n\t * // 调用父类方法，只能获取到父类类型\n\t * Class<Entity<?>> superSupplierClass = LambdaUtil.getRealClass(myTeacher::getId);\n\t * assertEquals(Entity.class, superSupplierClass);\n\t * // 引用父类静态带参方法，只能获取到父类类型\n\t * Class<Entity<?>> staticSuperFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeId);\n\t * assertEquals(Entity.class, staticSuperFunctionClass);\n\t * }</pre>\n\t *\n\t * @param func lambda\n\t * @param <R>  类型\n\t * @return lambda实现类\n\t * @throws IllegalArgumentException 如果是不支持的方法引用，抛出该异常，见{@link LambdaUtil#checkLambdaTypeCanGetClass}\n\t * @since 5.8.0\n\t * @author VampireAchao\n\t */\n\tpublic static <R> Class<R> getRealClass(Func0<?> func) {\n\t\tfinal SerializedLambda lambda = resolve(func);\n\t\tcheckLambdaTypeCanGetClass(lambda.getImplMethodKind());\n\t\treturn ClassUtil.loadClass(lambda.getImplClass());\n\t}\n\n\t/**\n\t * 解析lambda表达式,加了缓存。\n\t * 该缓存可能会在任意不定的时间被清除\n\t *\n\t * @param <T>  Lambda类型\n\t * @param func 需要解析的 lambda 对象（无参方法）\n\t * @return 返回解析后的结果\n\t */\n\tpublic static <T> SerializedLambda resolve(Func1<T, ?> func) {\n\t\treturn _resolve(func);\n\t}\n\n\t/**\n\t * 解析lambda表达式,加了缓存。\n\t * 该缓存可能会在任意不定的时间被清除\n\t *\n\t * @param <R>  Lambda返回类型\n\t * @param func 需要解析的 lambda 对象（无参方法）\n\t * @return 返回解析后的结果\n\t * @since 5.7.23\n\t */\n\tpublic static <R> SerializedLambda resolve(Func0<R> func) {\n\t\treturn _resolve(func);\n\t}\n\n\t/**\n\t * 获取lambda表达式函数（方法）名称\n\t *\n\t * @param <P>  Lambda参数类型\n\t * @param func 函数（无参方法）\n\t * @return 函数名称\n\t */\n\tpublic static <P> String getMethodName(Func1<P, ?> func) {\n\t\treturn resolve(func).getImplMethodName();\n\t}\n\n\t/**\n\t * 获取lambda表达式函数（方法）名称\n\t *\n\t * @param <R>  Lambda返回类型\n\t * @param func 函数（无参方法）\n\t * @return 函数名称\n\t * @since 5.7.23\n\t */\n\tpublic static <R> String getMethodName(Func0<R> func) {\n\t\treturn resolve(func).getImplMethodName();\n\t}\n\n\t/**\n\t * 通过对象的方法或类的静态方法引用，然后根据{@link SerializedLambda#getInstantiatedMethodType()}获取lambda实现类<br>\n\t * 传入lambda有参数且含有返回值的情况能够匹配到此方法：\n\t * <ul>\n\t * <li>引用特定类型的任意对象的实例方法：<pre>{@code\n\t * Class<MyTeacher> functionClass = LambdaUtil.getRealClass(MyTeacher::getAge);\n\t * assertEquals(MyTeacher.class, functionClass);\n\t * }</pre></li>\n\t * <li>引用静态带参方法：<pre>{@code\n\t * Class<MyTeacher> staticFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeAgeBy);\n\t * assertEquals(MyTeacher.class, staticFunctionClass);\n\t * }</pre></li>\n\t * </ul>\n\t *\n\t * @param func lambda\n\t * @param <P>  方法调用方类型\n\t * @param <R>  返回值类型\n\t * @return lambda实现类\n\t * @throws IllegalArgumentException 如果是不支持的方法引用，抛出该异常，见{@link LambdaUtil#checkLambdaTypeCanGetClass}\n\t * @since 5.8.0\n\t * @author VampireAchao\n\t */\n\tpublic static <P, R> Class<P> getRealClass(Func1<P, R> func) {\n\t\tfinal SerializedLambda lambda = resolve(func);\n\t\tcheckLambdaTypeCanGetClass(lambda.getImplMethodKind());\n\t\tfinal String instantiatedMethodType = lambda.getInstantiatedMethodType();\n\t\treturn ClassUtil.loadClass(StrUtil.sub(instantiatedMethodType, 2, StrUtil.indexOf(instantiatedMethodType, ';')));\n\t}\n\n\t/**\n\t * 获取lambda表达式Getter或Setter函数（方法）对应的字段名称，规则如下：\n\t * <ul>\n\t *     <li>getXxxx获取为xxxx，如getName得到name。</li>\n\t *     <li>setXxxx获取为xxxx，如setName得到name。</li>\n\t *     <li>isXxxx获取为xxxx，如isName得到name。</li>\n\t *     <li>其它不满足规则的方法名抛出{@link IllegalArgumentException}</li>\n\t * </ul>\n\t *\n\t * @param <T>  Lambda类型\n\t * @param func 函数（无参方法）\n\t * @return 方法名称\n\t * @throws IllegalArgumentException 非Getter或Setter方法\n\t * @since 5.7.10\n\t */\n\tpublic static <T> String getFieldName(Func1<T, ?> func) throws IllegalArgumentException {\n\t\treturn BeanUtil.getFieldName(getMethodName(func));\n\t}\n\n\t/**\n\t * 获取lambda表达式Getter或Setter函数（方法）对应的字段名称，规则如下：\n\t * <ul>\n\t *     <li>getXxxx获取为xxxx，如getName得到name。</li>\n\t *     <li>setXxxx获取为xxxx，如setName得到name。</li>\n\t *     <li>isXxxx获取为xxxx，如isName得到name。</li>\n\t *     <li>其它不满足规则的方法名抛出{@link IllegalArgumentException}</li>\n\t * </ul>\n\t *\n\t * @param <T>  Lambda类型\n\t * @param func 函数（无参方法）\n\t * @return 方法名称\n\t * @throws IllegalArgumentException 非Getter或Setter方法\n\t * @since 5.7.23\n\t */\n\tpublic static <T> String getFieldName(Func0<T> func) throws IllegalArgumentException {\n\t\treturn BeanUtil.getFieldName(getMethodName(func));\n\t}\n\n\t//region Private methods\n\t/**\n\t * 检查是否为支持的类型\n\t *\n\t * @param implMethodKind 支持的lambda类型\n\t * @throws IllegalArgumentException 如果是不支持的方法引用，抛出该异常\n\t */\n\tprivate static void checkLambdaTypeCanGetClass(int implMethodKind) {\n\t\tif (implMethodKind != MethodHandleInfo.REF_invokeVirtual &&\n\t\t\t\timplMethodKind != MethodHandleInfo.REF_invokeStatic) {\n\t\t\tthrow new IllegalArgumentException(\"该lambda不是合适的方法引用\");\n\t\t}\n\t}\n\n\t/**\n\t * 解析lambda表达式,加了缓存。\n\t * 该缓存可能会在任意不定的时间被清除。\n\t *\n\t * <p>\n\t * 通过反射调用实现序列化接口函数对象的writeReplace方法，从而拿到{@link SerializedLambda}<br>\n\t * 该对象中包含了lambda表达式的所有信息。\n\t * </p>\n\t *\n\t * @param func 需要解析的 lambda 对象\n\t * @return 返回解析后的结果\n\t */\n\tprivate static SerializedLambda _resolve(Serializable func) {\n\t\treturn cache.computeIfAbsent(func.getClass().getName(), (key) -> ReflectUtil.invoke(func, \"writeReplace\"));\n\t}\n\t//endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Supplier1.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport java.util.function.Supplier;\n\n/**\n * 1参数Supplier\n *\n * @param <T>  目标   类型\n * @param <P1> 参数一 类型\n * @author TomXin\n * @since 5.7.21\n */\n@FunctionalInterface\npublic interface Supplier1<T, P1> {\n\t/**\n\t * 生成实例的方法\n\t *\n\t * @param p1 参数一\n\t * @return 目标对象\n\t */\n\tT get(P1 p1);\n\n\t/**\n\t * 将带有参数的Supplier转换为无参{@link Supplier}\n\t *\n\t * @param p1 参数1\n\t * @return {@link Supplier}\n\t */\n\tdefault Supplier<T> toSupplier(P1 p1) {\n\t\treturn () -> get(p1);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Supplier2.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport java.util.function.Supplier;\n\n/**\n * 两个参数的Supplier\n *\n * @param <T>  目标   类型\n * @param <P1> 参数一 类型\n * @param <P2> 参数二 类型\n * @author TomXin\n * @since 5.7.21\n */\n@FunctionalInterface\npublic interface Supplier2<T, P1, P2> {\n\n\t/**\n\t * 生成实例的方法\n\t *\n\t * @param p1 参数一\n\t * @param p2 参数二\n\t * @return 目标对象\n\t */\n\tT get(P1 p1, P2 p2);\n\n\t/**\n\t * 将带有参数的Supplier转换为无参{@link Supplier}\n\t *\n\t * @param p1 参数1\n\t * @param p2 参数2\n\t * @return {@link Supplier}\n\t */\n\tdefault Supplier<T> toSupplier(P1 p1, P2 p2) {\n\t\treturn () -> get(p1, p2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Supplier3.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport java.util.function.Supplier;\n\n/**\n * 3参数Supplier\n *\n * @param <T>  目标类型\n * @param <P1> 参数一类型\n * @param <P2> 参数二类型\n * @param <P3> 参数三类型\n * @author TomXin\n * @since 5.7.21\n */\n@FunctionalInterface\npublic interface Supplier3<T, P1, P2, P3> {\n\n\t/**\n\t * 生成实例的方法\n\t *\n\t * @param p1 参数一\n\t * @param p2 参数二\n\t * @param p3 参数三\n\t * @return 目标对象\n\t */\n\tT get(P1 p1, P2 p2, P3 p3);\n\n\t/**\n\t * 将带有参数的Supplier转换为无参{@link Supplier}\n\t *\n\t * @param p1 参数1\n\t * @param p2 参数2\n\t * @param p3 参数3\n\t * @return {@link Supplier}\n\t */\n\tdefault Supplier<T> toSupplier(P1 p1, P2 p2, P3 p3) {\n\t\treturn () -> get(p1, p2, p3);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Supplier4.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport java.util.function.Supplier;\n\n/**\n * 4参数Supplier\n *\n * @param <T>  目标   类型\n * @param <P1> 参数一 类型\n * @param <P2> 参数二 类型\n * @param <P3> 参数三 类型\n * @param <P4> 参数四 类型\n * @author TomXin\n * @since 5.7.21\n */\n@FunctionalInterface\npublic interface Supplier4<T, P1, P2, P3, P4> {\n\n\t/**\n\t * 生成实例的方法\n\t *\n\t * @param p1 参数一\n\t * @param p2 参数二\n\t * @param p3 参数三\n\t * @param p4 参数四\n\t * @return 目标对象\n\t */\n\tT get(P1 p1, P2 p2, P3 p3, P4 p4);\n\n\t/**\n\t * 将带有参数的Supplier转换为无参{@link Supplier}\n\t *\n\t * @param p1 参数1\n\t * @param p2 参数2\n\t * @param p3 参数3\n\t * @param p4 参数4\n\t * @return {@link Supplier}\n\t */\n\tdefault Supplier<T> toSupplier(P1 p1, P2 p2, P3 p3, P4 p4) {\n\t\treturn () -> get(p1, p2, p3, p4);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/Supplier5.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport java.util.function.Supplier;\n\n/**\n * 5参数Supplier\n *\n * @param <T>  目标   类型\n * @param <P1> 参数一 类型\n * @param <P2> 参数二 类型\n * @param <P3> 参数三 类型\n * @param <P4> 参数四 类型\n * @param <P5> 参数五 类型\n * @author TomXin\n * @since 5.7.21\n */\n@FunctionalInterface\npublic interface Supplier5<T, P1, P2, P3, P4, P5> {\n\n\t/**\n\t * 生成实例的方法\n\t *\n\t * @param p1 参数一\n\t * @param p2 参数二\n\t * @param p3 参数三\n\t * @param p4 参数四\n\t * @param p5 参数五\n\t * @return 目标对象\n\t */\n\tT get(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5);\n\n\t/**\n\t * 将带有参数的Supplier转换为无参{@link Supplier}\n\t *\n\t * @param p1 参数1\n\t * @param p2 参数2\n\t * @param p3 参数3\n\t * @param p4 参数4\n\t * @param p5 参数5\n\t * @return {@link Supplier}\n\t */\n\tdefault Supplier<T> toSupplier(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) {\n\t\treturn () -> get(p1, p2, p3, p4, p5);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @param <P> 参数类型\n * @since 3.1.0\n */\n@FunctionalInterface\npublic interface VoidFunc<P> extends Serializable {\n\n\t/**\n\t * 执行函数\n\t *\n\t * @param parameters 参数列表\n\t * @throws Exception 自定义异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tvoid call(P... parameters) throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @param parameters 参数列表\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault void callWithRuntimeException(P... parameters){\n\t\ttry {\n\t\t\tcall(parameters);\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc0.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @since 3.2.3\n */\n@FunctionalInterface\npublic interface VoidFunc0 extends Serializable {\n\n\t/**\n\t * 执行函数\n\t *\n\t * @throws Exception 自定义异常\n\t */\n\tvoid call() throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @since 5.3.6\n\t */\n\tdefault void callWithRuntimeException(){\n\t\ttry {\n\t\t\tcall();\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc1.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\n\nimport java.io.Serializable;\n\n/**\n * 函数对象<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author Looly\n *\n * @since 3.2.3\n */\n@FunctionalInterface\npublic interface VoidFunc1<P> extends Serializable {\n\n\t/**\n\t * 执行函数\n\t *\n\t * @param parameter 参数\n\t * @throws Exception 自定义异常\n\t */\n\tvoid call(P parameter) throws Exception;\n\n\t/**\n\t * 执行函数，异常包装为RuntimeException\n\t *\n\t * @param parameter 参数\n\t * @since 5.3.6\n\t */\n\tdefault void callWithRuntimeException(P parameter){\n\t\ttry {\n\t\t\tcall(parameter);\n\t\t} catch (Exception e) {\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/func/package-info.java",
    "content": "/**\n * 函数封装<br>\n * 接口灵感来自于<a href=\"http://actframework.org/\">ActFramework</a><br>\n * 一个函数接口代表一个一个函数，用于包装一个函数为对象<br>\n * 在JDK8之前，Java的函数并不能作为参数传递，也不能作为返回值存在，此接口用于将一个函数包装成为一个对象，从而传递对象\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.func;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/Generator.java",
    "content": "package cn.hutool.core.lang.generator;\n\n/**\n * 生成器泛型接口<br>\n * 通过实现此接口可以自定义生成对象的策略\n *\n * @param <T> 生成对象类型\n * @since 5.4.3\n */\npublic interface Generator<T> {\n\n\t/**\n\t * 生成新的对象\n\t *\n\t * @return 新的对象\n\t */\n\tT next();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/ObjectGenerator.java",
    "content": "package cn.hutool.core.lang.generator;\n\nimport cn.hutool.core.util.ReflectUtil;\n\n/**\n * 对象生成器，通过指定对象的Class类型，调用next方法时生成新的对象。\n *\n * @param <T> 对象类型\n * @author looly\n * @since 5.4.3\n */\npublic class ObjectGenerator<T> implements Generator<T> {\n\n\tprivate final Class<T> clazz;\n\n\t/**\n\t * 构造\n\t * @param clazz 对象类型\n\t */\n\tpublic ObjectGenerator(Class<T> clazz) {\n\t\tthis.clazz = clazz;\n\t}\n\n\t@Override\n\tpublic T next() {\n\t\treturn ReflectUtil.newInstanceIfPossible(this.clazz);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/ObjectIdGenerator.java",
    "content": "package cn.hutool.core.lang.generator;\n\nimport cn.hutool.core.lang.ObjectId;\n\n/**\n * ObjectId生成器\n *\n * @author looly\n * @since 5.4.3\n */\npublic class ObjectIdGenerator implements Generator<String> {\n\t@Override\n\tpublic String next() {\n\t\treturn ObjectId.next();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/SnowflakeGenerator.java",
    "content": "package cn.hutool.core.lang.generator;\n\nimport cn.hutool.core.lang.Snowflake;\n\n/**\n * Snowflake生成器<br>\n * 注意，默认此生成器必须单例使用，否则会有重复<br>\n * 默认构造的终端ID和数据中心ID都为0，不适用于分布式环境。\n *\n * @author looly\n * @since 5.4.3\n */\npublic class SnowflakeGenerator implements Generator<Long> {\n\n\tprivate final Snowflake snowflake;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SnowflakeGenerator() {\n\t\tthis(0, 0);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param workerId     终端ID\n\t * @param dataCenterId 数据中心ID\n\t */\n\tpublic SnowflakeGenerator(long workerId, long dataCenterId) {\n\t\tsnowflake = new Snowflake(workerId, dataCenterId);\n\t}\n\n\t@Override\n\tpublic Long next() {\n\t\treturn this.snowflake.nextId();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/UUIDGenerator.java",
    "content": "package cn.hutool.core.lang.generator;\n\nimport cn.hutool.core.util.IdUtil;\n\n/**\n * UUID生成器\n *\n * @author looly\n * @since 5.4.3\n */\npublic class UUIDGenerator implements Generator<String> {\n\t@Override\n\tpublic String next() {\n\t\treturn IdUtil.fastUUID();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/generator/package-info.java",
    "content": "/**\n * 提供生成器接口及相关封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.generator;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/CityHash.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.util.ByteUtil;\n\nimport java.util.Arrays;\n\n/**\n * Google发布的Hash计算算法：CityHash64 与 CityHash128。<br>\n * 它们分别根据字串计算 64 和 128 位的散列值。这些算法不适用于加密，但适合用在散列表等处。\n *\n * <p>\n * 代码来自：https://github.com/rolandhe/string-tools<br>\n * 原始算法：https://github.com/google/cityhash\n *\n * @author hexiufeng\n * @since 5.2.5\n */\npublic class CityHash {\n\n\t// Some primes between 2^63 and 2^64 for various uses.\n\tprivate static final long k0 = 0xc3a5c85c97cb3127L;\n\tprivate static final long k1 = 0xb492b66fbe98f273L;\n\tprivate static final long k2 = 0x9ae16a3b2f90404fL;\n\tprivate static final long kMul = 0x9ddfea08eb382d69L;\n\n\t// Magic numbers for 32-bit hashing.  Copied from Murmur3.\n\tprivate static final int c1 = 0xcc9e2d51;\n\tprivate static final int c2 = 0x1b873593;\n\n\n\t/**\n\t * 计算32位City Hash值\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t */\n\tpublic static int hash32(byte[] data) {\n\t\tint len = data.length;\n\t\tif (len <= 24) {\n\t\t\treturn len <= 12 ?\n\t\t\t\t\t(len <= 4 ? hash32Len0to4(data) : hash32Len5to12(data)) :\n\t\t\t\t\thash32Len13to24(data);\n\t\t}\n\n\t\t// len > 24\n\t\tint h = len, g = c1 * len, f = g;\n\t\tint a0 = rotate32(fetch32(data, len - 4) * c1, 17) * c2;\n\t\tint a1 = rotate32(fetch32(data, len - 8) * c1, 17) * c2;\n\t\tint a2 = rotate32(fetch32(data, len - 16) * c1, 17) * c2;\n\t\tint a3 = rotate32(fetch32(data, len - 12) * c1, 17) * c2;\n\t\tint a4 = rotate32(fetch32(data, len - 20) * c1, 17) * c2;\n\t\th ^= a0;\n\t\th = rotate32(h, 19);\n\t\th = h * 5 + 0xe6546b64;\n\t\th ^= a2;\n\t\th = rotate32(h, 19);\n\t\th = h * 5 + 0xe6546b64;\n\t\tg ^= a1;\n\t\tg = rotate32(g, 19);\n\t\tg = g * 5 + 0xe6546b64;\n\t\tg ^= a3;\n\t\tg = rotate32(g, 19);\n\t\tg = g * 5 + 0xe6546b64;\n\t\tf += a4;\n\t\tf = rotate32(f, 19);\n\t\tf = f * 5 + 0xe6546b64;\n\t\tint iters = (len - 1) / 20;\n\n\t\tint pos = 0;\n\t\tdo {\n\t\t\ta0 = rotate32(fetch32(data, pos) * c1, 17) * c2;\n\t\t\ta1 = fetch32(data, pos + 4);\n\t\t\ta2 = rotate32(fetch32(data, pos + 8) * c1, 17) * c2;\n\t\t\ta3 = rotate32(fetch32(data, pos + 12) * c1, 17) * c2;\n\t\t\ta4 = fetch32(data, pos + 16);\n\t\t\th ^= a0;\n\t\t\th = rotate32(h, 18);\n\t\t\th = h * 5 + 0xe6546b64;\n\t\t\tf += a1;\n\t\t\tf = rotate32(f, 19);\n\t\t\tf = f * c1;\n\t\t\tg += a2;\n\t\t\tg = rotate32(g, 18);\n\t\t\tg = g * 5 + 0xe6546b64;\n\t\t\th ^= a3 + a1;\n\t\t\th = rotate32(h, 19);\n\t\t\th = h * 5 + 0xe6546b64;\n\t\t\tg ^= a4;\n\t\t\tg = Integer.reverseBytes(g) * 5;\n\t\t\th += a4 * 5;\n\t\t\th = Integer.reverseBytes(h);\n\t\t\tf += a0;\n\t\t\tint swapValue = f;\n\t\t\tf = g;\n\t\t\tg = h;\n\t\t\th = swapValue;\n\n\t\t\tpos += 20;\n\t\t} while (--iters != 0);\n\n\t\tg = rotate32(g, 11) * c1;\n\t\tg = rotate32(g, 17) * c1;\n\t\tf = rotate32(f, 11) * c1;\n\t\tf = rotate32(f, 17) * c1;\n\t\th = rotate32(h + g, 19);\n\t\th = h * 5 + 0xe6546b64;\n\t\th = rotate32(h, 17) * c1;\n\t\th = rotate32(h + f, 19);\n\t\th = h * 5 + 0xe6546b64;\n\t\th = rotate32(h, 17) * c1;\n\t\treturn h;\n\t}\n\n\t/**\n\t * 计算64位City Hash值\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t */\n\tpublic static long hash64(byte[] data) {\n\t\tint len = data.length;\n\t\tif (len <= 32) {\n\t\t\tif (len <= 16) {\n\t\t\t\treturn hashLen0to16(data);\n\t\t\t} else {\n\t\t\t\treturn hashLen17to32(data);\n\t\t\t}\n\t\t} else if (len <= 64) {\n\t\t\treturn hashLen33to64(data);\n\t\t}\n\n\t\t// For strings over 64 bytes we hash the end first, and then as we\n\t\t// loop we keep 56 bytes of state: v, w, x, y, and z.\n\t\tlong x = fetch64(data, len - 40);\n\t\tlong y = fetch64(data, len - 16) + fetch64(data, len - 56);\n\t\tlong z = hashLen16(fetch64(data, len - 48) + len, fetch64(data, len - 24));\n\t\tNumber128 v = weakHashLen32WithSeeds(data, len - 64, len, z);\n\t\tNumber128 w = weakHashLen32WithSeeds(data, len - 32, y + k1, x);\n\t\tx = x * k1 + fetch64(data, 0);\n\n\t\t// Decrease len to the nearest multiple of 64, and operate on 64-byte chunks.\n\t\tlen = (len - 1) & ~63;\n\t\tint pos = 0;\n\t\tdo {\n\t\t\tx = rotate64(x + y + v.getLowValue() + fetch64(data, pos + 8), 37) * k1;\n\t\t\ty = rotate64(y + v.getHighValue() + fetch64(data, pos + 48), 42) * k1;\n\t\t\tx ^= w.getHighValue();\n\t\t\ty += v.getLowValue() + fetch64(data, pos + 40);\n\t\t\tz = rotate64(z + w.getLowValue(), 33) * k1;\n\t\t\tv = weakHashLen32WithSeeds(data, pos, v.getHighValue() * k1, x + w.getLowValue());\n\t\t\tw = weakHashLen32WithSeeds(data, pos + 32, z + w.getHighValue(), y + fetch64(data, pos + 16));\n\t\t\t// swap z,x value\n\t\t\tlong swapValue = x;\n\t\t\tx = z;\n\t\t\tz = swapValue;\n\t\t\tpos += 64;\n\t\t\tlen -= 64;\n\t\t} while (len != 0);\n\t\treturn hashLen16(hashLen16(v.getLowValue(), w.getLowValue()) + shiftMix(y) * k1 + z,\n\t\t\t\thashLen16(v.getHighValue(), w.getHighValue()) + x);\n\t}\n\n\t/**\n\t * 计算64位City Hash值\n\t *\n\t * @param data  数据\n\t * @param seed0 种子1\n\t * @param seed1 种子2\n\t * @return hash值\n\t */\n\tpublic static long hash64(byte[] data, long seed0, long seed1) {\n\t\treturn hashLen16(hash64(data) - seed0, seed1);\n\t}\n\n\t/**\n\t * 计算64位City Hash值，种子1使用默认的{@link #k2}\n\t *\n\t * @param data 数据\n\t * @param seed 种子2\n\t * @return hash值\n\t */\n\tpublic static long hash64(byte[] data, long seed) {\n\t\treturn hash64(data, k2, seed);\n\t}\n\n\t/**\n\t * 计算128位City Hash值\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t */\n\tpublic static Number128 hash128(byte[] data) {\n\t\tint len = data.length;\n\t\treturn len >= 16 ?\n\t\t\t\thash128(data, 16,\n\t\t\t\t\t\tnew Number128(fetch64(data, 0), fetch64(data, 8) + k0)) :\n\t\t\t\thash128(data, 0, new Number128(k0, k1));\n\t}\n\n\t/**\n\t * 计算128位City Hash值\n\t *\n\t * @param data 数据\n\t * @param seed 种子\n\t * @return hash值\n\t */\n\tpublic static Number128 hash128(byte[] data, Number128 seed) {\n\t\treturn hash128(data, 0, seed);\n\t}\n\n\t//------------------------------------------------------------------------------------------------------- Private method start\n\tprivate static Number128 hash128(final byte[] byteArray, int start, final Number128 seed) {\n\t\tint len = byteArray.length - start;\n\n\t\tif (len < 128) {\n\t\t\treturn cityMurmur(Arrays.copyOfRange(byteArray, start, byteArray.length), seed);\n\t\t}\n\n\t\t// We expect len >= 128 to be the common case.  Keep 56 bytes of state:\n\t\t// v, w, x, y, and z.\n\t\tNumber128 v = new Number128(0L, 0L);\n\t\tNumber128 w = new Number128(0L, 0L);\n\t\tlong x = seed.getLowValue();\n\t\tlong y = seed.getHighValue();\n\t\tlong z = len * k1;\n\t\tv.setLowValue(rotate64(y ^ k1, 49) * k1 + fetch64(byteArray, start));\n\t\tv.setHighValue(rotate64(v.getLowValue(), 42) * k1 + fetch64(byteArray, start + 8));\n\t\tw.setLowValue(rotate64(y + z, 35) * k1 + x);\n\t\tw.setHighValue(rotate64(x + fetch64(byteArray, start + 88), 53) * k1);\n\n\t\t// This is the same inner loop as CityHash64(), manually unrolled.\n\t\tint pos = start;\n\t\tdo {\n\t\t\tx = rotate64(x + y + v.getLowValue() + fetch64(byteArray, pos + 8), 37) * k1;\n\t\t\ty = rotate64(y + v.getHighValue() + fetch64(byteArray, pos + 48), 42) * k1;\n\t\t\tx ^= w.getHighValue();\n\t\t\ty += v.getLowValue() + fetch64(byteArray, pos + 40);\n\t\t\tz = rotate64(z + w.getLowValue(), 33) * k1;\n\t\t\tv = weakHashLen32WithSeeds(byteArray, pos, v.getHighValue() * k1, x + w.getLowValue());\n\t\t\tw = weakHashLen32WithSeeds(byteArray, pos + 32, z + w.getHighValue(), y + fetch64(byteArray, pos + 16));\n\n\t\t\tlong swapValue = x;\n\t\t\tx = z;\n\t\t\tz = swapValue;\n\t\t\tpos += 64;\n\t\t\tx = rotate64(x + y + v.getLowValue() + fetch64(byteArray, pos + 8), 37) * k1;\n\t\t\ty = rotate64(y + v.getHighValue() + fetch64(byteArray, pos + 48), 42) * k1;\n\t\t\tx ^= w.getHighValue();\n\t\t\ty += v.getLowValue() + fetch64(byteArray, pos + 40);\n\t\t\tz = rotate64(z + w.getLowValue(), 33) * k1;\n\t\t\tv = weakHashLen32WithSeeds(byteArray, pos, v.getHighValue() * k1, x + w.getLowValue());\n\t\t\tw = weakHashLen32WithSeeds(byteArray, pos + 32, z + w.getHighValue(), y + fetch64(byteArray, pos + 16));\n\t\t\tswapValue = x;\n\t\t\tx = z;\n\t\t\tz = swapValue;\n\t\t\tpos += 64;\n\t\t\tlen -= 128;\n\t\t} while (len >= 128);\n\t\tx += rotate64(v.getLowValue() + z, 49) * k0;\n\t\ty = y * k0 + rotate64(w.getHighValue(), 37);\n\t\tz = z * k0 + rotate64(w.getLowValue(), 27);\n\t\tw.setLowValue(w.getLowValue() * 9);\n\t\tv.setLowValue(v.getLowValue() * k0);\n\n\t\t// If 0 < len < 128, hash up to 4 chunks of 32 bytes each from the end of s.\n\t\tfor (int tail_done = 0; tail_done < len; ) {\n\t\t\ttail_done += 32;\n\t\t\ty = rotate64(x + y, 42) * k0 + v.getHighValue();\n\t\t\tw.setLowValue(w.getLowValue() + fetch64(byteArray, pos + len - tail_done + 16));\n\t\t\tx = x * k0 + w.getLowValue();\n\t\t\tz += w.getHighValue() + fetch64(byteArray, pos + len - tail_done);\n\t\t\tw.setHighValue(w.getHighValue() + v.getLowValue());\n\t\t\tv = weakHashLen32WithSeeds(byteArray, pos + len - tail_done, v.getLowValue() + z, v.getHighValue());\n\t\t\tv.setLowValue(v.getLowValue() * k0);\n\t\t}\n\t\t// At this point our 56 bytes of state should contain more than\n\t\t// enough information for a strong 128-bit hash.  We use two\n\t\t// different 56-byte-to-8-byte hashes to get a 16-byte final result.\n\t\tx = hashLen16(x, v.getLowValue());\n\t\ty = hashLen16(y + z, w.getLowValue());\n\t\treturn new Number128(hashLen16(x + v.getHighValue(), w.getHighValue()) + y,\n\t\t\t\thashLen16(x + w.getHighValue(), y + v.getHighValue()));\n\n\t}\n\n\tprivate static int hash32Len0to4(final byte[] byteArray) {\n\t\tint b = 0;\n\t\tint c = 9;\n\t\tint len = byteArray.length;\n\t\tfor (int v : byteArray) {\n\t\t\tb = b * c1 + v;\n\t\t\tc ^= b;\n\t\t}\n\t\treturn fmix(mur(b, mur(len, c)));\n\t}\n\n\tprivate static int hash32Len5to12(final byte[] byteArray) {\n\t\tint len = byteArray.length;\n\t\tint a = len, b = len * 5, c = 9, d = b;\n\t\ta += fetch32(byteArray, 0);\n\t\tb += fetch32(byteArray, len - 4);\n\t\tc += fetch32(byteArray, ((len >>> 1) & 4));\n\t\treturn fmix(mur(c, mur(b, mur(a, d))));\n\t}\n\n\tprivate static int hash32Len13to24(byte[] byteArray) {\n\t\tint len = byteArray.length;\n\t\tint a = fetch32(byteArray, (len >>> 1) - 4);\n\t\tint b = fetch32(byteArray, 4);\n\t\tint c = fetch32(byteArray, len - 8);\n\t\tint d = fetch32(byteArray, (len >>> 1));\n\t\tint e = fetch32(byteArray, 0);\n\t\tint f = fetch32(byteArray, len - 4);\n\t\t@SuppressWarnings(\"UnnecessaryLocalVariable\")\n\t\tint h = len;\n\n\t\treturn fmix(mur(f, mur(e, mur(d, mur(c, mur(b, mur(a, h)))))));\n\t}\n\n\tprivate static long hashLen0to16(byte[] byteArray) {\n\t\tint len = byteArray.length;\n\t\tif (len >= 8) {\n\t\t\tlong mul = k2 + len * 2L;\n\t\t\tlong a = fetch64(byteArray, 0) + k2;\n\t\t\tlong b = fetch64(byteArray, len - 8);\n\t\t\tlong c = rotate64(b, 37) * mul + a;\n\t\t\tlong d = (rotate64(a, 25) + b) * mul;\n\t\t\treturn hashLen16(c, d, mul);\n\t\t}\n\t\tif (len >= 4) {\n\t\t\tlong mul = k2 + len * 2;\n\t\t\tlong a = fetch32(byteArray, 0) & 0xffffffffL;\n\t\t\treturn hashLen16(len + (a << 3), fetch32(byteArray, len - 4) & 0xffffffffL, mul);\n\t\t}\n\t\tif (len > 0) {\n\t\t\tint a = byteArray[0] & 0xff;\n\t\t\tint b = byteArray[len >>> 1] & 0xff;\n\t\t\tint c = byteArray[len - 1] & 0xff;\n\t\t\tint y = a + (b << 8);\n\t\t\tint z = len + (c << 2);\n\t\t\treturn shiftMix(y * k2 ^ z * k0) * k2;\n\t\t}\n\t\treturn k2;\n\t}\n\n\t// This probably works well for 16-byte strings as well, but it may be overkill in that case.\n\tprivate static long hashLen17to32(byte[] byteArray) {\n\t\tint len = byteArray.length;\n\t\tlong mul = k2 + len * 2L;\n\t\tlong a = fetch64(byteArray, 0) * k1;\n\t\tlong b = fetch64(byteArray, 8);\n\t\tlong c = fetch64(byteArray, len - 8) * mul;\n\t\tlong d = fetch64(byteArray, len - 16) * k2;\n\t\treturn hashLen16(rotate64(a + b, 43) + rotate64(c, 30) + d,\n\t\t\t\ta + rotate64(b + k2, 18) + c, mul);\n\t}\n\n\tprivate static long hashLen33to64(byte[] byteArray) {\n\t\tint len = byteArray.length;\n\t\tlong mul = k2 + len * 2L;\n\t\tlong a = fetch64(byteArray, 0) * k2;\n\t\tlong b = fetch64(byteArray, 8);\n\t\tlong c = fetch64(byteArray, len - 24);\n\t\tlong d = fetch64(byteArray, len - 32);\n\t\tlong e = fetch64(byteArray, 16) * k2;\n\t\tlong f = fetch64(byteArray, 24) * 9;\n\t\tlong g = fetch64(byteArray, len - 8);\n\t\tlong h = fetch64(byteArray, len - 16) * mul;\n\t\tlong u = rotate64(a + g, 43) + (rotate64(b, 30) + c) * 9;\n\t\tlong v = ((a + g) ^ d) + f + 1;\n\t\tlong w = Long.reverseBytes((u + v) * mul) + h;\n\t\tlong x = rotate64(e + f, 42) + c;\n\t\tlong y = (Long.reverseBytes((v + w) * mul) + g) * mul;\n\t\tlong z = e + f + c;\n\t\ta = Long.reverseBytes((x + z) * mul + y) + b;\n\t\tb = shiftMix((z + a) * mul + d + h) * mul;\n\t\treturn b + x;\n\t}\n\n\tprivate static long fetch64(byte[] byteArray, int start) {\n\t\treturn ByteUtil.bytesToLong(byteArray, start, ByteUtil.CPU_ENDIAN);\n\t}\n\n\tprivate static int fetch32(byte[] byteArray, final int start) {\n\t\treturn ByteUtil.bytesToInt(byteArray, start, ByteUtil.CPU_ENDIAN);\n\t}\n\n\tprivate static long rotate64(long val, int shift) {\n\t\t// Avoid shifting by 64: doing so yields an undefined result.\n\t\treturn shift == 0 ? val : ((val >>> shift) | (val << (64 - shift)));\n\t}\n\n\tprivate static int rotate32(int val, int shift) {\n\t\t// Avoid shifting by 32: doing so yields an undefined result.\n\t\treturn shift == 0 ? val : ((val >>> shift) | (val << (32 - shift)));\n\t}\n\n\tprivate static long hashLen16(long u, long v, long mul) {\n\t\t// Murmur-inspired hashing.\n\t\tlong a = (u ^ v) * mul;\n\t\ta ^= (a >>> 47);\n\t\tlong b = (v ^ a) * mul;\n\t\tb ^= (b >>> 47);\n\t\tb *= mul;\n\t\treturn b;\n\t}\n\n\tprivate static long hashLen16(long u, long v) {\n\t\treturn hash128to64(new Number128(u, v));\n\t}\n\n\tprivate static long hash128to64(final Number128 number128) {\n\t\t// Murmur-inspired hashing.\n\t\tlong a = (number128.getLowValue() ^ number128.getHighValue()) * kMul;\n\t\ta ^= (a >>> 47);\n\t\tlong b = (number128.getHighValue() ^ a) * kMul;\n\t\tb ^= (b >>> 47);\n\t\tb *= kMul;\n\t\treturn b;\n\t}\n\n\tprivate static long shiftMix(long val) {\n\t\treturn val ^ (val >>> 47);\n\t}\n\n\tprivate static int fmix(int h) {\n\t\th ^= h >>> 16;\n\t\th *= 0x85ebca6b;\n\t\th ^= h >>> 13;\n\t\th *= 0xc2b2ae35;\n\t\th ^= h >>> 16;\n\t\treturn h;\n\t}\n\n\tprivate static int mur(int a, int h) {\n\t\t// Helper from Murmur3 for combining two 32-bit values.\n\t\ta *= c1;\n\t\ta = rotate32(a, 17);\n\t\ta *= c2;\n\t\th ^= a;\n\t\th = rotate32(h, 19);\n\t\treturn h * 5 + 0xe6546b64;\n\t}\n\n\tprivate static Number128 weakHashLen32WithSeeds(\n\t\t\tlong w, long x, long y, long z, long a, long b) {\n\t\ta += w;\n\t\tb = rotate64(b + a + z, 21);\n\t\tlong c = a;\n\t\ta += x;\n\t\ta += y;\n\t\tb += rotate64(a, 44);\n\t\treturn new Number128(a + z, b + c);\n\t}\n\n\t// Return a 16-byte hash for s[0] ... s[31], a, and b.  Quick and dirty.\n\tprivate static Number128 weakHashLen32WithSeeds(\n\t\t\tbyte[] byteArray, int start, long a, long b) {\n\t\treturn weakHashLen32WithSeeds(fetch64(byteArray, start),\n\t\t\t\tfetch64(byteArray, start + 8),\n\t\t\t\tfetch64(byteArray, start + 16),\n\t\t\t\tfetch64(byteArray, start + 24),\n\t\t\t\ta,\n\t\t\t\tb);\n\t}\n\n\tprivate static Number128 cityMurmur(final byte[] byteArray, Number128 seed) {\n\t\tint len = byteArray.length;\n\t\tlong a = seed.getLowValue();\n\t\tlong b = seed.getHighValue();\n\t\tlong c;\n\t\tlong d;\n\t\tint l = len - 16;\n\t\tif (l <= 0) {  // len <= 16\n\t\t\ta = shiftMix(a * k1) * k1;\n\t\t\tc = b * k1 + hashLen0to16(byteArray);\n\t\t\td = shiftMix(a + (len >= 8 ? fetch64(byteArray, 0) : c));\n\t\t} else {  // len > 16\n\t\t\tc = hashLen16(fetch64(byteArray, len - 8) + k1, a);\n\t\t\td = hashLen16(b + len, c + fetch64(byteArray, len - 16));\n\t\t\ta += d;\n\t\t\tint pos = 0;\n\t\t\tdo {\n\t\t\t\ta ^= shiftMix(fetch64(byteArray, pos) * k1) * k1;\n\t\t\t\ta *= k1;\n\t\t\t\tb ^= a;\n\t\t\t\tc ^= shiftMix(fetch64(byteArray, pos + 8) * k1) * k1;\n\t\t\t\tc *= k1;\n\t\t\t\td ^= c;\n\t\t\t\tpos += 16;\n\t\t\t\tl -= 16;\n\t\t\t} while (l > 0);\n\t\t}\n\t\ta = hashLen16(a, c);\n\t\tb = hashLen16(d, b);\n\t\treturn new Number128(a ^ b, hashLen16(b, a));\n\t}\n\t//------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/Hash.java",
    "content": "package cn.hutool.core.lang.hash;\n\n/**\n * Hash计算接口\n *\n * @param <T> 被计算hash的对象类型\n * @author looly\n * @since 5.7.15\n */\n@FunctionalInterface\npublic interface Hash<T> {\n\t/**\n\t * 计算Hash值\n\t *\n\t * @param t 对象\n\t * @return hash\n\t */\n\tNumber hash(T t);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/Hash128.java",
    "content": "package cn.hutool.core.lang.hash;\n\n/**\n * Hash计算接口\n *\n * @param <T> 被计算hash的对象类型\n * @author looly\n * @since 5.2.5\n */\n@FunctionalInterface\npublic interface Hash128<T> extends Hash<T>{\n\n\t/**\n\t * 计算Hash值\n\t *\n\t * @param t 对象\n\t * @return hash\n\t */\n\tNumber128 hash128(T t);\n\n\t@Override\n\tdefault Number hash(T t){\n\t\treturn hash128(t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/Hash32.java",
    "content": "package cn.hutool.core.lang.hash;\n\n/**\n * Hash计算接口\n *\n * @param <T> 被计算hash的对象类型\n * @author looly\n * @since 5.2.5\n */\n@FunctionalInterface\npublic interface Hash32<T> extends Hash<T>{\n\t/**\n\t * 计算Hash值\n\t *\n\t * @param t 对象\n\t * @return hash\n\t */\n\tint hash32(T t);\n\n\t@Override\n\tdefault Number hash(T t){\n\t\treturn hash32(t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/Hash64.java",
    "content": "package cn.hutool.core.lang.hash;\n\n/**\n * Hash计算接口\n *\n * @param <T> 被计算hash的对象类型\n * @author looly\n * @since 5.2.5\n */\n@FunctionalInterface\npublic interface Hash64<T> extends Hash<T>{\n\t/**\n\t * 计算Hash值\n\t *\n\t * @param t 对象\n\t * @return hash\n\t */\n\tlong hash64(T t);\n\n\t@Override\n\tdefault Number hash(T t){\n\t\treturn hash64(t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/KetamaHash.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\n/**\n * Ketama算法，用于在一致性Hash中快速定位服务器位置\n *\n * @author looly\n * @since 5.7.20\n */\npublic class KetamaHash implements Hash64<String>, Hash32<String> {\n\n\t@Override\n\tpublic long hash64(String key) {\n\t\tbyte[] bKey = md5(key);\n\t\treturn ((long) (bKey[3] & 0xFF) << 24)\n\t\t\t\t| ((long) (bKey[2] & 0xFF) << 16)\n\t\t\t\t| ((long) (bKey[1] & 0xFF) << 8)\n\t\t\t\t| (bKey[0] & 0xFF);\n\t}\n\n\t@Override\n\tpublic int hash32(String key) {\n\t\treturn (int) (hash64(key) & 0xffffffffL);\n\t}\n\n\t@Override\n\tpublic Number hash(String key) {\n\t\treturn hash64(key);\n\t}\n\n\t/**\n\t * 计算MD5值，使用UTF-8编码\n\t *\n\t * @param key 被计算的键\n\t * @return MD5值\n\t */\n\tprivate static byte[] md5(String key) {\n\t\tfinal MessageDigest md5;\n\t\ttry {\n\t\t\tmd5 = MessageDigest.getInstance(\"MD5\");\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new UtilException(\"MD5 algorithm not suooport!\", e);\n\t\t}\n\t\treturn md5.digest(StrUtil.utf8Bytes(key));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/MetroHash.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.util.ByteUtil;\n\nimport java.nio.ByteOrder;\nimport java.util.Arrays;\n\n/**\n * Apache 发布的MetroHash算法，是一组用于非加密用例的最先进的哈希函数。\n * 除了卓越的性能外，他们还以算法生成而著称。\n *\n * <p>\n * 官方实现：https://github.com/jandrewrogers/MetroHash\n * 官方文档：http://www.jandrewrogers.com/2015/05/27/metrohash/\n * Go语言实现：https://github.com/linvon/cuckoo-filter/blob/main/vendor/github.com/dgryski/go-metro/\n * @author li\n */\npublic class MetroHash {\n\n\t/**\n\t * hash64 种子加盐\n\t */\n\tprivate final static long k0_64 = 0xD6D018F5;\n\tprivate final static long k1_64 = 0xA2AA033B;\n\tprivate final static long k2_64 = 0x62992FC1;\n\tprivate final static long k3_64 = 0x30BC5B29;\n\n\t/**\n\t * hash128 种子加盐\n\t */\n\tprivate final static long k0_128 = 0xC83A91E1;\n\tprivate final static long k1_128 = 0x8648DBDB;\n\tprivate final static long k2_128 = 0x7BDEC03B;\n\tprivate final static long k3_128 = 0x2F5870A5;\n\n\tpublic static long hash64(byte[] data) {\n\t\treturn hash64(data, 1337);\n\t}\n\n\tpublic static Number128 hash128(byte[] data) {\n\t\treturn hash128(data, 1337);\n\t}\n\n\tpublic static long hash64(byte[] data, long seed) {\n\t\tbyte[] buffer = data;\n\t\tlong hash = (seed + k2_64) * k0_64;\n\n\t\tlong v0, v1, v2, v3;\n\t\tv0 = hash;\n\t\tv1 = hash;\n\t\tv2 = hash;\n\t\tv3 = hash;\n\n\t\tif (buffer.length >= 32) {\n\n\t\t\twhile (buffer.length >= 32) {\n\t\t\t\tv0 += littleEndian64(buffer, 0) * k0_64;\n\t\t\t\tv0 = rotateLeft64(v0, -29) + v2;\n\t\t\t\tv1 += littleEndian64(buffer, 8) * k1_64;\n\t\t\t\tv1 = rotateLeft64(v1, -29) + v3;\n\t\t\t\tv2 += littleEndian64(buffer, 24) * k2_64;\n\t\t\t\tv2 = rotateLeft64(v2, -29) + v0;\n\t\t\t\tv3 += littleEndian64(buffer, 32) * k3_64;\n\t\t\t\tv3 = rotateLeft64(v3, -29) + v1;\n\t\t\t\tbuffer = Arrays.copyOfRange(buffer, 32, buffer.length);\n\t\t\t}\n\n\t\t\tv2 ^= rotateLeft64(((v0 + v3) * k0_64) + v1, -37) * k1_64;\n\t\t\tv3 ^= rotateLeft64(((v1 + v2) * k1_64) + v0, -37) * k0_64;\n\t\t\tv0 ^= rotateLeft64(((v0 + v2) * k0_64) + v3, -37) * k1_64;\n\t\t\tv1 ^= rotateLeft64(((v1 + v3) * k1_64) + v2, -37) * k0_64;\n\t\t\thash += v0 ^ v1;\n\t\t}\n\n\t\tif (buffer.length >= 16) {\n\t\t\tv0 = hash + littleEndian64(buffer, 0) * k2_64;\n\t\t\tv0 = rotateLeft64(v0, -29) * k3_64;\n\t\t\tv1 = hash + littleEndian64(buffer, 8) * k2_64;\n\t\t\tv1 = rotateLeft64(v1, -29) * k3_64;\n\t\t\tv0 ^= rotateLeft64(v0 * k0_64, -21) + v1;\n\t\t\tv1 ^= rotateLeft64(v1 * k3_64, -21) + v0;\n\t\t\thash += v1;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 16, buffer.length);\n\t\t}\n\n\t\tif (buffer.length >= 8) {\n\t\t\thash += littleEndian64(buffer, 0) * k3_64;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\thash ^= rotateLeft64(hash, -55) * k1_64;\n\t\t}\n\n\t\tif (buffer.length >= 4) {\n\t\t\thash += (long) littleEndian32(Arrays.copyOfRange(buffer, 0, 4)) * k3_64;\n\t\t\thash ^= rotateLeft64(hash, -26) * k1_64;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 4, buffer.length);\n\t\t}\n\n\t\tif (buffer.length >= 2) {\n\t\t\thash += (long) littleEndian16(Arrays.copyOfRange(buffer, 0, 2)) * k3_64;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 2, buffer.length);\n\t\t\thash ^= rotateLeft64(hash, -48) * k1_64;\n\t\t}\n\n\t\tif (buffer.length >= 1) {\n\t\t\thash += (long) buffer[0] * k3_64;\n\t\t\thash ^= rotateLeft64(hash, -38) * k1_64;\n\t\t}\n\n\t\thash ^= rotateLeft64(hash, -28);\n\t\thash *= k0_64;\n\t\thash ^= rotateLeft64(hash, -29);\n\n\t\treturn hash;\n\t}\n\n\tpublic static Number128 hash128(byte[] data, long seed) {\n\t\tbyte[] buffer = data;\n\n\t\tlong v0, v1, v2, v3;\n\n\t\tv0 = (seed - k0_128) * k3_128;\n\t\tv1 = (seed + k1_128) * k2_128;\n\n\t\tif (buffer.length >= 32) {\n\t\t\tv2 = (seed + k0_128) * k2_128;\n\t\t\tv3 = (seed - k1_128) * k3_128;\n\n\t\t\twhile (buffer.length >= 32) {\n\t\t\t\tv0 += littleEndian64(buffer, 0) * k0_128;\n\t\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\t\tv0 = rotateRight(v0, 29) + v2;\n\t\t\t\tv1 += littleEndian64(buffer, 0) * k1_128;\n\t\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\t\tv1 = rotateRight(v1, 29) + v3;\n\t\t\t\tv2 += littleEndian64(buffer, 0) * k2_128;\n\t\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\t\tv2 = rotateRight(v2, 29) + v0;\n\t\t\t\tv3 = littleEndian64(buffer, 0) * k3_128;\n\t\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\t\tv3 = rotateRight(v3, 29) + v1;\n\t\t\t}\n\n\t\t\tv2 ^= rotateRight(((v0 + v3) * k0_128) + v1, 21) * k1_128;\n\t\t\tv3 ^= rotateRight(((v1 + v2) * k1_128) + v0, 21) * k0_128;\n\t\t\tv0 ^= rotateRight(((v0 + v2) * k0_128) + v3, 21) * k1_128;\n\t\t\tv1 ^= rotateRight(((v1 + v3) * k1_128) + v2, 21) * k0_128;\n\t\t}\n\n\t\tif (buffer.length >= 16) {\n\t\t\tv0 += littleEndian64(buffer, 0) * k2_128;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\tv0 = rotateRight(v0, 33) * k3_128;\n\t\t\tv1 += littleEndian64(buffer, 0) * k2_128;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\tv1 = rotateRight(v1, 33) * k3_128;\n\t\t\tv0 ^= rotateRight((v0 * k2_128) + v1, 45) + k1_128;\n\t\t\tv1 ^= rotateRight((v1 * k3_128) + v0, 45) + k0_128;\n\t\t}\n\n\t\tif (buffer.length >= 8) {\n\t\t\tv0 += littleEndian64(buffer, 0) * k2_128;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 8, buffer.length);\n\t\t\tv0 = rotateRight(v0, 33) * k3_128;\n\t\t\tv0 ^= rotateRight((v0 * k2_128) + v1, 27) * k1_128;\n\t\t}\n\n\t\tif (buffer.length >= 4) {\n\t\t\tv1 += (long) littleEndian32(buffer) * k2_128;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 4, buffer.length);\n\t\t\tv1 = rotateRight(v1, 33) * k3_128;\n\t\t\tv1 ^= rotateRight((v1 * k3_128) + v0, 46) * k0_128;\n\t\t}\n\n\t\tif (buffer.length >= 2) {\n\t\t\tv0 += (long) littleEndian16(buffer) * k2_128;\n\t\t\tbuffer = Arrays.copyOfRange(buffer, 2, buffer.length);\n\t\t\tv0 = rotateRight(v0, 33) * k3_128;\n\t\t\tv0 ^= rotateRight((v0 * k2_128) * v1, 22) * k1_128;\n\t\t}\n\n\t\tif (buffer.length >= 1) {\n\t\t\tv1 += (long) buffer[0] * k2_128;\n\t\t\tv1 = rotateRight(v1, 33) * k3_128;\n\t\t\tv1 ^= rotateRight((v1 * k3_128) + v0, 58) * k0_128;\n\t\t}\n\n\t\tv0 += rotateRight((v0 * k0_128) + v1, 13);\n\t\tv1 += rotateRight((v1 * k1_128) + v0, 37);\n\t\tv0 += rotateRight((v0 * k2_128) + v1, 13);\n\t\tv1 += rotateRight((v1 * k3_128) + v0, 37);\n\n\t\treturn new Number128(v0, v1);\n\t}\n\n\n\tprivate static long littleEndian64(byte[] b, int start) {\n\t\treturn ByteUtil.bytesToLong(b, start, ByteOrder.LITTLE_ENDIAN);\n\t}\n\n\tprivate static int littleEndian32(byte[] b) {\n\t\treturn (int) b[0] | (int) b[1] << 8 | (int) b[2] << 16 | (int) b[3] << 24;\n\t}\n\n\tprivate static int littleEndian16(byte[] b) {\n\t\treturn ByteUtil.bytesToShort(b, ByteOrder.LITTLE_ENDIAN);\n\t}\n\n\tprivate static long rotateLeft64(long x, int k) {\n\t\tint n = 64;\n\t\tint s = k & (n - 1);\n\t\treturn x << s | x >> (n - s);\n\t}\n\n\tprivate static long rotateRight(long val, int shift) {\n\t\treturn (val >> shift) | (val << (64 - shift));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/MurmurHash.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.util.ByteUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.nio.ByteOrder;\nimport java.nio.charset.Charset;\n\n/**\n * Murmur3 32bit、64bit、128bit 哈希算法实现<br>\n * 此算法来自于：https://github.com/xlturing/Simhash4J/blob/master/src/main/java/bee/simhash/main/Murmur3.java\n *\n * <p>\n * 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#94 <br>\n * 128-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#255\n * </p>\n *\n * @author looly, Simhash4J\n * @since 4.3.3\n */\npublic class MurmurHash implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// Constants for 32 bit variant\n\tprivate static final int C1_32 = 0xcc9e2d51;\n\tprivate static final int C2_32 = 0x1b873593;\n\tprivate static final int R1_32 = 15;\n\tprivate static final int R2_32 = 13;\n\tprivate static final int M_32 = 5;\n\tprivate static final int N_32 = 0xe6546b64;\n\n\t// Constants for 128 bit variant\n\tprivate static final long C1 = 0x87c37b91114253d5L;\n\tprivate static final long C2 = 0x4cf5ad432745937fL;\n\tprivate static final int R1 = 31;\n\tprivate static final int R2 = 27;\n\tprivate static final int R3 = 33;\n\tprivate static final int M = 5;\n\tprivate static final int N1 = 0x52dce729;\n\tprivate static final int N2 = 0x38495ab5;\n\n\tprivate static final int DEFAULT_SEED = 0;\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\tprivate static final ByteOrder DEFAULT_ORDER = ByteOrder.LITTLE_ENDIAN;\n\n\t/**\n\t * Murmur3 32-bit Hash值计算\n\t *\n\t * @param data 数据\n\t * @return Hash值\n\t */\n\tpublic static int hash32(CharSequence data) {\n\t\treturn hash32(StrUtil.bytes(data, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * Murmur3 32-bit Hash值计算\n\t *\n\t * @param data 数据\n\t * @return Hash值\n\t */\n\tpublic static int hash32(byte[] data) {\n\t\treturn hash32(data, data.length, DEFAULT_SEED);\n\t}\n\n\t/**\n\t * Murmur3 32-bit Hash值计算\n\t *\n\t * @param data   数据\n\t * @param length 长度\n\t * @param seed   种子，默认0\n\t * @return Hash值\n\t */\n\tpublic static int hash32(byte[] data, int length, int seed) {\n\t\treturn hash32(data, 0, length, seed);\n\t}\n\n\t/**\n\t * Murmur3 32-bit Hash值计算\n\t *\n\t * @param data   数据\n\t * @param offset 数据开始位置\n\t * @param length 长度\n\t * @param seed   种子，默认0\n\t * @return Hash值\n\t */\n\tpublic static int hash32(byte[] data, int offset, int length, int seed) {\n\t\tint hash = seed;\n\t\tfinal int nblocks = length >> 2;\n\n\t\t// body\n\t\tfor (int i = 0; i < nblocks; i++) {\n\t\t\tfinal int i4 = offset  + (i << 2);\n\t\t\tfinal int k = ByteUtil.bytesToInt(data, i4, DEFAULT_ORDER);\n\t\t\t// mix functions\n\t\t\thash = mix32(k, hash);\n\t\t}\n\n\t\t// tail\n\t\tfinal int idx = offset + (nblocks << 2);\n\t\tint k1 = 0;\n\t\tswitch (offset + length - idx) {\n\t\t\tcase 3:\n\t\t\t\tk1 ^= (data[idx + 2] & 0xff) << 16;\n\t\t\tcase 2:\n\t\t\t\tk1 ^= (data[idx + 1] & 0xff) << 8;\n\t\t\tcase 1:\n\t\t\t\tk1 ^= (data[idx] & 0xff);\n\n\t\t\t\t// mix functions\n\t\t\t\tk1 *= C1_32;\n\t\t\t\tk1 = Integer.rotateLeft(k1, R1_32);\n\t\t\t\tk1 *= C2_32;\n\t\t\t\thash ^= k1;\n\t\t}\n\n\t\t// finalization\n\t\thash ^= length;\n\t\treturn fmix32(hash);\n\t}\n\n\t/**\n\t * Murmur3 64-bit Hash值计算\n\t *\n\t * @param data 数据\n\t * @return Hash值\n\t */\n\tpublic static long hash64(CharSequence data) {\n\t\treturn hash64(StrUtil.bytes(data, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * Murmur3 64-bit 算法<br>\n\t * This is essentially MSB 8 bytes of Murmur3 128-bit variant.\n\t *\n\t * @param data 数据\n\t * @return Hash值\n\t */\n\tpublic static long hash64(byte[] data) {\n\t\treturn hash64(data, data.length, DEFAULT_SEED);\n\t}\n\n\t/**\n\t * 类Murmur3 64-bit 算法 <br>\n\t * This is essentially MSB 8 bytes of Murmur3 128-bit variant.\n\t *\n\t * @param data   数据\n\t * @param length 长度\n\t * @param seed   种子，默认0\n\t * @return Hash值\n\t */\n\tpublic static long hash64(byte[] data, int length, int seed) {\n\t\tlong hash = seed;\n\t\tfinal int nblocks = length >> 3;\n\n\t\t// body\n\t\tfor (int i = 0; i < nblocks; i++) {\n\t\t\tfinal int i8 = i << 3;\n\t\t\tlong k = ByteUtil.bytesToLong(data, i8, DEFAULT_ORDER);\n\n\t\t\t// mix functions\n\t\t\tk *= C1;\n\t\t\tk = Long.rotateLeft(k, R1);\n\t\t\tk *= C2;\n\t\t\thash ^= k;\n\t\t\thash = Long.rotateLeft(hash, R2) * M + N1;\n\t\t}\n\n\t\t// tail\n\t\tlong k1 = 0;\n\t\tint tailStart = nblocks << 3;\n\t\tswitch (length - tailStart) {\n\t\t\tcase 7:\n\t\t\t\tk1 ^= ((long) data[tailStart + 6] & 0xff) << 48;\n\t\t\tcase 6:\n\t\t\t\tk1 ^= ((long) data[tailStart + 5] & 0xff) << 40;\n\t\t\tcase 5:\n\t\t\t\tk1 ^= ((long) data[tailStart + 4] & 0xff) << 32;\n\t\t\tcase 4:\n\t\t\t\tk1 ^= ((long) data[tailStart + 3] & 0xff) << 24;\n\t\t\tcase 3:\n\t\t\t\tk1 ^= ((long) data[tailStart + 2] & 0xff) << 16;\n\t\t\tcase 2:\n\t\t\t\tk1 ^= ((long) data[tailStart + 1] & 0xff) << 8;\n\t\t\tcase 1:\n\t\t\t\tk1 ^= ((long) data[tailStart] & 0xff);\n\t\t\t\tk1 *= C1;\n\t\t\t\tk1 = Long.rotateLeft(k1, R1);\n\t\t\t\tk1 *= C2;\n\t\t\t\thash ^= k1;\n\t\t}\n\n\t\t// finalization\n\t\thash ^= length;\n\t\thash = fmix64(hash);\n\n\t\treturn hash;\n\t}\n\n\t/**\n\t * Murmur3 128-bit Hash值计算\n\t *\n\t * @param data 数据\n\t * @return Hash值 (2 longs)\n\t */\n\tpublic static long[] hash128(CharSequence data) {\n\t\treturn hash128(StrUtil.bytes(data, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * Murmur3 128-bit 算法.\n\t *\n\t * @param data -数据\n\t * @return Hash值 (2 longs)\n\t */\n\tpublic static long[] hash128(byte[] data) {\n\t\treturn hash128(data, data.length, DEFAULT_SEED);\n\t}\n\n\t/**\n\t * Murmur3 128-bit variant.\n\t *\n\t * @param data   数据\n\t * @param length 长度\n\t * @param seed   种子，默认0\n\t * @return Hash值(2 longs)\n\t */\n\tpublic static long[] hash128(byte[] data, int length, int seed) {\n\t\treturn hash128(data, 0, length, seed);\n\t}\n\n\t/**\n\t * Murmur3 128-bit variant.\n\t *\n\t * @param data   数据\n\t * @param offset 数据开始位置\n\t * @param length 长度\n\t * @param seed   种子，默认0\n\t * @return Hash值(2 longs)\n\t */\n\tpublic static long[] hash128(byte[] data, int offset, int length, int seed) {\n\t\t// 避免负数的种子\n\t\tseed &= 0xffffffffL;\n\n\t\tlong h1 = seed;\n\t\tlong h2 = seed;\n\t\tfinal int nblocks = length >> 4;\n\n\t\t// body\n\t\tfor (int i = 0; i < nblocks; i++) {\n\t\t\tfinal int i16 = offset + (i << 4);\n\t\t\tlong k1 = ByteUtil.bytesToLong(data, i16, DEFAULT_ORDER);\n\t\t\tlong k2 = ByteUtil.bytesToLong(data, i16 + 8, DEFAULT_ORDER);\n\n\t\t\t// mix functions for k1\n\t\t\tk1 *= C1;\n\t\t\tk1 = Long.rotateLeft(k1, R1);\n\t\t\tk1 *= C2;\n\t\t\th1 ^= k1;\n\t\t\th1 = Long.rotateLeft(h1, R2);\n\t\t\th1 += h2;\n\t\t\th1 = h1 * M + N1;\n\n\t\t\t// mix functions for k2\n\t\t\tk2 *= C2;\n\t\t\tk2 = Long.rotateLeft(k2, R3);\n\t\t\tk2 *= C1;\n\t\t\th2 ^= k2;\n\t\t\th2 = Long.rotateLeft(h2, R1);\n\t\t\th2 += h1;\n\t\t\th2 = h2 * M + N2;\n\t\t}\n\n\t\t// tail\n\t\tlong k1 = 0;\n\t\tlong k2 = 0;\n\t\tfinal int tailStart = offset + (nblocks << 4);\n\t\tswitch (offset + length - tailStart) {\n\t\t\tcase 15:\n\t\t\t\tk2 ^= (long) (data[tailStart + 14] & 0xff) << 48;\n\t\t\tcase 14:\n\t\t\t\tk2 ^= (long) (data[tailStart + 13] & 0xff) << 40;\n\t\t\tcase 13:\n\t\t\t\tk2 ^= (long) (data[tailStart + 12] & 0xff) << 32;\n\t\t\tcase 12:\n\t\t\t\tk2 ^= (long) (data[tailStart + 11] & 0xff) << 24;\n\t\t\tcase 11:\n\t\t\t\tk2 ^= (long) (data[tailStart + 10] & 0xff) << 16;\n\t\t\tcase 10:\n\t\t\t\tk2 ^= (long) (data[tailStart + 9] & 0xff) << 8;\n\t\t\tcase 9:\n\t\t\t\tk2 ^= data[tailStart + 8] & 0xff;\n\t\t\t\tk2 *= C2;\n\t\t\t\tk2 = Long.rotateLeft(k2, R3);\n\t\t\t\tk2 *= C1;\n\t\t\t\th2 ^= k2;\n\n\t\t\tcase 8:\n\t\t\t\tk1 ^= (long) (data[tailStart + 7] & 0xff) << 56;\n\t\t\tcase 7:\n\t\t\t\tk1 ^= (long) (data[tailStart + 6] & 0xff) << 48;\n\t\t\tcase 6:\n\t\t\t\tk1 ^= (long) (data[tailStart + 5] & 0xff) << 40;\n\t\t\tcase 5:\n\t\t\t\tk1 ^= (long) (data[tailStart + 4] & 0xff) << 32;\n\t\t\tcase 4:\n\t\t\t\tk1 ^= (long) (data[tailStart + 3] & 0xff) << 24;\n\t\t\tcase 3:\n\t\t\t\tk1 ^= (long) (data[tailStart + 2] & 0xff) << 16;\n\t\t\tcase 2:\n\t\t\t\tk1 ^= (long) (data[tailStart + 1] & 0xff) << 8;\n\t\t\tcase 1:\n\t\t\t\tk1 ^= data[tailStart] & 0xff;\n\t\t\t\tk1 *= C1;\n\t\t\t\tk1 = Long.rotateLeft(k1, R1);\n\t\t\t\tk1 *= C2;\n\t\t\t\th1 ^= k1;\n\t\t}\n\n\t\t// finalization\n\t\th1 ^= length;\n\t\th2 ^= length;\n\n\t\th1 += h2;\n\t\th2 += h1;\n\n\t\th1 = fmix64(h1);\n\t\th2 = fmix64(h2);\n\n\t\th1 += h2;\n\t\th2 += h1;\n\n\t\treturn new long[]{h1, h2};\n\t}\n\n\tprivate static int mix32(int k, int hash) {\n\t\tk *= C1_32;\n\t\tk = Integer.rotateLeft(k, R1_32);\n\t\tk *= C2_32;\n\t\thash ^= k;\n\t\treturn Integer.rotateLeft(hash, R2_32) * M_32 + N_32;\n\t}\n\n\tprivate static int fmix32(int hash) {\n\t\thash ^= (hash >>> 16);\n\t\thash *= 0x85ebca6b;\n\t\thash ^= (hash >>> 13);\n\t\thash *= 0xc2b2ae35;\n\t\thash ^= (hash >>> 16);\n\t\treturn hash;\n\t}\n\n\tprivate static long fmix64(long h) {\n\t\th ^= (h >>> 33);\n\t\th *= 0xff51afd7ed558ccdL;\n\t\th ^= (h >>> 33);\n\t\th *= 0xc4ceb9fe1a85ec53L;\n\t\th ^= (h >>> 33);\n\t\treturn h;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/Number128.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport java.util.Objects;\n\n/**\n * 128位数字表示，分高位和低位\n *\n * @author hexiufeng\n * @since 5.2.5\n */\npublic class Number128 extends Number {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate long lowValue;\n\tprivate long highValue;\n\n\t/**\n\t * 构造\n\t *\n\t * @param lowValue  低位\n\t * @param highValue 高位\n\t */\n\tpublic Number128(long lowValue, long highValue) {\n\t\tthis.lowValue = lowValue;\n\t\tthis.highValue = highValue;\n\t}\n\n\t/**\n\t * 获取低位值\n\t *\n\t * @return 地位值\n\t */\n\tpublic long getLowValue() {\n\t\treturn lowValue;\n\t}\n\n\t/**\n\t * 设置低位值\n\t *\n\t * @param lowValue 低位值\n\t */\n\tpublic void setLowValue(long lowValue) {\n\t\tthis.lowValue = lowValue;\n\t}\n\n\t/**\n\t * 获取高位值\n\t *\n\t * @return 高位值\n\t */\n\tpublic long getHighValue() {\n\t\treturn highValue;\n\t}\n\n\t/**\n\t * 设置高位值\n\t *\n\t * @param hiValue 高位值\n\t */\n\tpublic void setHighValue(long hiValue) {\n\t\tthis.highValue = hiValue;\n\t}\n\n\t/**\n\t * 获取高低位数组，long[0]：低位，long[1]：高位\n\t *\n\t * @return 高低位数组，long[0]：低位，long[1]：高位\n\t */\n\tpublic long[] getLongArray() {\n\t\treturn new long[]{lowValue, highValue};\n\t}\n\n\t@Override\n\tpublic int intValue() {\n\t\treturn (int) longValue();\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn this.lowValue;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn longValue();\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn longValue();\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object o) {\n\t\tif (this == o) return true;\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal Number128 number128 = (Number128) o;\n\t\treturn lowValue == number128.lowValue && highValue == number128.highValue;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(lowValue, highValue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/hash/package-info.java",
    "content": "/**\n * 提供Hash算法的封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.hash;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/id/IdConstants.java",
    "content": "package cn.hutool.core.lang.id;\n\nimport cn.hutool.core.lang.Snowflake;\nimport cn.hutool.core.util.IdUtil;\n\n/**\n * ID相关常量\n *\n * @author Looly\n * @since 5.8.28\n */\npublic class IdConstants {\n\t/**\n\t * 默认的数据中心ID。\n\t * <p>此常量通过调用{@link IdUtil#getDataCenterId(long)}方法，传入{@link Snowflake#MAX_DATA_CENTER_ID}作为参数，\n\t * 来获取一个默认的数据中心ID。它在系统中作为一个全局配置使用，标识系统默认运行在一个最大数据中心ID限定的环境中。</p>\n\t *\n\t * @see IdUtil#getDataCenterId(long)\n\t * @see Snowflake#MAX_DATA_CENTER_ID\n\t */\n\tpublic static final long DEFAULT_DATACENTER_ID = IdUtil.getDataCenterId(Snowflake.MAX_DATA_CENTER_ID);\n\n\t/**\n\t * 默认的Worker ID生成。\n\t * <p>这个静态常量是通过调用IdUtil的getWorkerId方法，使用默认的数据中心ID和Snowflake算法允许的最大Worker ID来获取的。</p>\n\t *\n\t * @see IdUtil#getWorkerId(long, long) 获取Worker ID的具体实现方法\n\t * @see Snowflake#MAX_WORKER_ID Snowflake算法中定义的最大Worker ID\n\t */\n\tpublic static final long DEFAULT_WORKER_ID = IdUtil.getWorkerId(DEFAULT_DATACENTER_ID, Snowflake.MAX_WORKER_ID);\n\n\t/**\n\t * 默认的Snowflake单例，使用默认的Worker ID和数据中心ID。<br>\n\t * 传入{@link #DEFAULT_WORKER_ID}和{@link #DEFAULT_DATACENTER_ID}作为参数。<br>\n\t * 此单例对象保证在同一JVM实例中获取ID唯一，唯一性使用进程ID和MAC地址保证。\n\t */\n\tpublic static final Snowflake DEFAULT_SNOWFLAKE = new Snowflake(DEFAULT_WORKER_ID, DEFAULT_DATACENTER_ID);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/id/NanoId.java",
    "content": "package cn.hutool.core.lang.id;\n\nimport cn.hutool.core.util.RandomUtil;\n\nimport java.security.SecureRandom;\nimport java.util.Random;\n\n/**\n * NanoId，一个小型、安全、对 URL友好的唯一字符串 ID 生成器，特点：\n *\n * <ul>\n *     <li>安全：它使用加密、强大的随机 API，并保证符号的正确分配</li>\n *     <li>体积小：只有 258 bytes 大小（压缩后）、无依赖</li>\n *     <li>紧凑：它使用比 UUID (A-Za-z0-9_~)更多的符号</li>\n * </ul>\n *\n * <p>\n * 此实现的逻辑基于JavaScript的NanoId实现，见：https://github.com/ai/nanoid\n *\n * @author David Klebanoff\n */\npublic class NanoId {\n\n\t/**\n\t * 默认随机数生成器，使用{@link SecureRandom}确保健壮性\n\t */\n\tprivate static final SecureRandom DEFAULT_NUMBER_GENERATOR = RandomUtil.getSecureRandom();\n\n\t/**\n\t * 默认随机字母表，使用URL安全的Base64字符\n\t */\n\tprivate static final char[] DEFAULT_ALPHABET =\n\t\t\t\"_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\".toCharArray();\n\n\t/**\n\t * 默认长度\n\t */\n\tpublic static final int DEFAULT_SIZE = 21;\n\n\t/**\n\t * 生成伪随机的NanoId字符串，长度为默认的{@link #DEFAULT_SIZE}，使用密码安全的伪随机生成器\n\t *\n\t * @return 伪随机的NanoId字符串\n\t */\n\tpublic static String randomNanoId() {\n\t\treturn randomNanoId(DEFAULT_SIZE);\n\t}\n\n\t/**\n\t * 生成伪随机的NanoId字符串\n\t *\n\t * @param size ID长度\n\t * @return 伪随机的NanoId字符串\n\t */\n\tpublic static String randomNanoId(int size) {\n\t\treturn randomNanoId(null, null, size);\n\t}\n\n\t/**\n\t * 生成伪随机的NanoId字符串\n\t *\n\t * @param random   随机数生成器\n\t * @param alphabet 随机字母表\n\t * @param size     ID长度\n\t * @return 伪随机的NanoId字符串\n\t */\n\tpublic static String randomNanoId(Random random, char[] alphabet, int size) {\n\t\tif (random == null) {\n\t\t\trandom = DEFAULT_NUMBER_GENERATOR;\n\t\t}\n\n\t\tif (alphabet == null) {\n\t\t\talphabet = DEFAULT_ALPHABET;\n\t\t}\n\n\t\tif (alphabet.length == 0 || alphabet.length >= 256) {\n\t\t\tthrow new IllegalArgumentException(\"Alphabet must contain between 1 and 255 symbols.\");\n\t\t}\n\n\t\tif (size <= 0) {\n\t\t\tthrow new IllegalArgumentException(\"Size must be greater than zero.\");\n\t\t}\n\n\t\tfinal int mask = (2 << (int) Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1;\n\t\tfinal int step = (int) Math.ceil(1.6 * mask * size / alphabet.length);\n\n\t\tfinal StringBuilder idBuilder = new StringBuilder();\n\n\t\twhile (true) {\n\t\t\tfinal byte[] bytes = new byte[step];\n\t\t\trandom.nextBytes(bytes);\n\t\t\tfor (int i = 0; i < step; i++) {\n\t\t\t\tfinal int alphabetIndex = bytes[i] & mask;\n\t\t\t\tif (alphabetIndex < alphabet.length) {\n\t\t\t\t\tidBuilder.append(alphabet[alphabetIndex]);\n\t\t\t\t\tif (idBuilder.length() == size) {\n\t\t\t\t\t\treturn idBuilder.toString();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/id/package-info.java",
    "content": "/**\n * 提供各种ID生成\n *\n * @author looly\n * @since 5.7.5\n */\npackage cn.hutool.core.lang.id;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/intern/InternUtil.java",
    "content": "package cn.hutool.core.lang.intern;\n\n/**\n * 规范化对象生成工具\n *\n * @author looly\n * @since 5.4.3\n */\npublic class InternUtil {\n\n\t/**\n\t * 创建WeakHshMap实现的字符串规范化器\n\t *\n\t * @param <T> 规范对象的类型\n\t * @return {@link Interner}\n\t */\n\tpublic static <T> Interner<T> createWeakInterner(){\n\t\treturn new WeakInterner<>();\n\t}\n\n\t/**\n\t * 创建JDK默认实现的字符串规范化器\n\t *\n\t * @return {@link Interner}\n\t * @see String#intern()\n\t */\n\tpublic static Interner<String> createJdkInterner(){\n\t\treturn new JdkStringInterner();\n\t}\n\n\t/**\n\t * 创建字符串规范化器\n\t *\n\t * @param isWeak 是否创建使用WeakHashMap实现的Interner\n\t * @return {@link Interner}\n\t */\n\tpublic static Interner<String> createStringInterner(boolean isWeak){\n\t\treturn isWeak ? createWeakInterner() : createJdkInterner();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/intern/Interner.java",
    "content": "package cn.hutool.core.lang.intern;\n\n/**\n * 规范化表示形式封装<br>\n * 所谓规范化，即当两个对象equals时，规范化的对象则可以实现==<br>\n * 此包中的相关封装类似于 {@link String#intern()}\n *\n * @param <T> 规范化的对象类型\n * @author looly\n * @since 5.4.3\n */\npublic interface Interner<T> {\n\n\t/**\n\t * 返回指定对象对应的规范化对象，sample对象可能有多个，但是这些对象如果都equals，则返回的是同一个对象\n\t *\n\t * @param sample 对象\n\t * @return 样例对象\n\t */\n\tT intern(T sample);\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/intern/JdkStringInterner.java",
    "content": "package cn.hutool.core.lang.intern;\n\n/**\n * JDK中默认的字符串规范化实现\n *\n * @author looly\n * @since 5.4.3\n */\npublic class JdkStringInterner implements Interner<String>{\n\t@Override\n\tpublic String intern(String sample) {\n\t\tif(null == sample){\n\t\t\treturn null;\n\t\t}\n\t\treturn sample.intern();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/intern/WeakInterner.java",
    "content": "package cn.hutool.core.lang.intern;\n\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\nimport java.lang.ref.WeakReference;\n\n/**\n * 使用WeakHashMap(线程安全)存储对象的规范化对象，注意此对象需单例使用！<br>\n *\n * @author looly\n * @since 5.4.3\n */\npublic class WeakInterner<T> implements Interner<T>{\n\n\tprivate final WeakKeyValueConcurrentMap<T, WeakReference<T>> cache = new WeakKeyValueConcurrentMap<>();\n\n\tpublic T intern(T sample) {\n\t\tif (sample == null) {\n\t\t\treturn null;\n\t\t}\n\t\tT val;\n\t\tdo {\n\t\t\tval = this.cache.computeIfAbsent(sample, WeakReference::new).get();\n\t\t} while (val == null);\n\t\treturn val;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/intern/package-info.java",
    "content": "/**\n * 规范化表示形式封装<br>\n * 所谓规范化，即当两个对象equals时，规范化的对象则可以实现==<br>\n * 此包中的相关封装类似于 String#intern()\n *\n * @author looly\n */\npackage cn.hutool.core.lang.intern;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java",
    "content": "package cn.hutool.core.lang.loader;\n\nimport java.io.Serializable;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/**\n * 原子引用加载器<br>\n * 使用{@link AtomicReference} 实懒加载，过程如下\n * <pre>\n * 1. 检查引用中是否有加载好的对象，有则返回\n * 2. 如果没有则初始化一个对象，并再次比较引用中是否有其它线程加载好的对象，无则加入，有则返回已有的\n * </pre>\n *\n * 当对象未被创建，对象的初始化操作在多线程情况下可能会被调用多次（多次创建对象），但是总是返回同一对象\n *\n * @author looly\n *\n * @param <T> 被加载对象类型\n */\npublic abstract class AtomicLoader<T> implements Loader<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 被加载对象的引用 */\n\tprivate final AtomicReference<T> reference = new AtomicReference<>();\n\n\t/**\n\t * 获取一个对象，第一次调用此方法时初始化对象然后返回，之后调用此方法直接返回原对象\n\t */\n\t@Override\n\tpublic T get() {\n\t\tT result = reference.get();\n\n\t\tif (result == null) {\n\t\t\tresult = init();\n\t\t\tif (false == reference.compareAndSet(null, result)) {\n\t\t\t\t// 其它线程已经创建好此对象\n\t\t\t\tresult = reference.get();\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 初始化被加载的对象<br>\n\t * 如果对象从未被加载过，调用此方法初始化加载对象，此方法只被调用一次\n\t *\n\t * @return 被加载的对象\n\t */\n\tprotected abstract T init();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java",
    "content": "package cn.hutool.core.lang.loader;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * 函数式懒加载加载器<br>\n * 传入用于生成对象的函数，在对象需要使用时调用生成对象，然后抛弃此生成对象的函数。<br>\n * 此加载器常用于对象比较庞大而不一定被使用的情况，用于减少启动时资源占用问题<br>\n * 继承自{@link LazyLoader}，如何实现多线程安全，由LazyLoader完成。\n *\n * @param <T> 被加载对象类型\n * @author Mr.Po\n * @see cn.hutool.core.lang.loader.LazyLoader\n * @since 5.6.1\n */\npublic class LazyFunLoader<T> extends LazyLoader<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 用于生成对象的函数\n\t */\n\tprivate Supplier<T> supplier;\n\n\t/**\n\t * 静态工厂方法，提供语义性与编码便利性\n\t * @param supplier 用于生成对象的函数\n\t * @param <T> 对象类型\n\t * @return 函数式懒加载加载器对象\n\t * @since 5.8.0\n\t */\n\tpublic static <T> LazyFunLoader<T> on(final Supplier<T> supplier) {\n\t\tAssert.notNull(supplier, \"supplier must be not null!\");\n\t\treturn new LazyFunLoader<>(supplier);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param supplier 用于生成对象的函数\n\t */\n\tpublic LazyFunLoader(Supplier<T> supplier) {\n\t\tAssert.notNull(supplier);\n\t\tthis.supplier = supplier;\n\t}\n\n\t@Override\n\tprotected T init() {\n\t\tT t = this.supplier.get();\n\t\tthis.supplier = null;\n\t\treturn t;\n\t}\n\n\t/**\n\t * 是否已经初始化\n\t *\n\t * @return 是/否\n\t */\n\tpublic boolean isInitialize() {\n\t\treturn this.supplier == null;\n\t}\n\n\t/**\n\t * 如果已经初始化，就执行传入函数\n\t *\n\t * @param consumer 待执行函数\n\t */\n\tpublic void ifInitialized(Consumer<T> consumer) {\n\t\tAssert.notNull(consumer);\n\n\t\t//\t已经初始化\n\t\tif (this.isInitialize()) {\n\t\t\tconsumer.accept(this.get());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java",
    "content": "package cn.hutool.core.lang.loader;\n\nimport java.io.Serializable;\n\n/**\n * 懒加载加载器<br>\n * 在load方法被调用前，对象未被加载，直到被调用后才开始加载<br>\n * 此加载器常用于对象比较庞大而不一定被使用的情况，用于减少启动时资源占用问题<br>\n * 此加载器使用双重检查（Double-Check）方式检查对象是否被加载，避免多线程下重复加载或加载丢失问题\n *\n * @author looly\n *\n * @param <T> 被加载对象类型\n */\npublic abstract class LazyLoader<T> implements Loader<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 被加载对象 */\n\tprivate volatile T object;\n\n\t/**\n\t * 获取一个对象，第一次调用此方法时初始化对象然后返回，之后调用此方法直接返回原对象\n\t */\n\t@Override\n\tpublic T get() {\n\t\tT result = object;\n\t\tif (result == null) {\n\t\t\tsynchronized (this) {\n\t\t\t\tresult = object;\n\t\t\t\tif (result == null) {\n\t\t\t\t\tobject = result = init();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 初始化被加载的对象<br>\n\t * 如果对象从未被加载过，调用此方法初始化加载对象，此方法只被调用一次\n\t *\n\t * @return 被加载的对象\n\t */\n\tprotected abstract T init();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/loader/Loader.java",
    "content": "package cn.hutool.core.lang.loader;\n\n/**\n * 对象加载抽象接口<br>\n * 通过实现此接口自定义实现对象的加载方式，例如懒加载机制、多线程加载等\n *\n * @author looly\n *\n * @param <T> 对象类型\n */\n@FunctionalInterface\npublic interface Loader<T> {\n\n\t/**\n\t * 获取一个准备好的对象<br>\n\t * 通过准备逻辑准备好被加载的对象，然后返回。在准备完毕之前此方法应该被阻塞\n\t *\n\t * @return 加载完毕的对象\n\t */\n\tT get();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/loader/package-info.java",
    "content": "/**\n * 加载器的抽象接口和实现，包括懒加载的实现等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.loader;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/Mutable.java",
    "content": "package cn.hutool.core.lang.mutable;\n\n/**\n * 提供可变值类型接口\n *\n * @param <T> 值得类型\n * @since 3.0.1\n */\npublic interface Mutable<T> {\n\n\t/**\n\t * 获得原始值\n\t * @return 原始值\n\t */\n\tT get();\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tvoid set(T value);\n\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport java.io.Serializable;\n\n/**\n * 可变 {@code boolean} 类型\n *\n * @see Boolean\n * @since 3.0.1\n */\npublic class MutableBool implements Comparable<MutableBool>, Mutable<Boolean>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate boolean value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableBool() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableBool(final boolean value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 转为Boolean错误\n\t */\n\tpublic MutableBool(final String value) throws NumberFormatException {\n\t\tthis.value = Boolean.parseBoolean(value);\n\t}\n\n\t@Override\n\tpublic Boolean get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final boolean value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Boolean value) {\n\t\tthis.value = value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 MutableBool</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableBool) {\n\t\t\treturn value == ((MutableBool) obj).value;\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn value ? Boolean.TRUE.hashCode() : Boolean.FALSE.hashCode();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 MutableBool 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableBool other) {\n\t\treturn Boolean.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 {@code byte} 类型\n *\n * @see Byte\n * @since 3.0.1\n */\npublic class MutableByte extends Number implements Comparable<MutableByte>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate byte value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableByte() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableByte(final byte value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableByte(final Number value) {\n\t\tthis(value.byteValue());\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 转为Byte错误\n\t */\n\tpublic MutableByte(final String value) throws NumberFormatException {\n\t\tthis.value = Byte.parseByte(value);\n\t}\n\n\t@Override\n\tpublic Byte get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final byte value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.byteValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 值+1\n\t * @return this\n\t */\n\tpublic MutableByte increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t * @return this\n\t */\n\tpublic MutableByte decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableByte add(final byte operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableByte add(final Number operand) {\n\t\tthis.value += operand.byteValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableByte subtract(final byte operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableByte subtract(final Number operand) {\n\t\tthis.value -= operand.byteValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic byte byteValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic int intValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 MutableByte</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableByte) {\n\t\t\treturn value == ((MutableByte) obj).byteValue();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 MutableByte 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableByte other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 {@code double} 类型\n *\n * @see Double\n * @since 3.0.1\n */\npublic class MutableDouble extends Number implements Comparable<MutableDouble>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate double value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableDouble() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableDouble(final double value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableDouble(final Number value) {\n\t\tthis(value.doubleValue());\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 数字转换错误\n\t */\n\tpublic MutableDouble(final String value) throws NumberFormatException {\n\t\tthis.value = Double.parseDouble(value);\n\t}\n\n\t@Override\n\tpublic Double get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final double value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.doubleValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 值+1\n\t * @return this\n\t */\n\tpublic MutableDouble increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t * @return this\n\t */\n\tpublic MutableDouble decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableDouble add(final double operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t */\n\tpublic MutableDouble add(final Number operand) {\n\t\tthis.value += operand.doubleValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableDouble subtract(final double operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t */\n\tpublic MutableDouble subtract(final Number operand) {\n\t\tthis.value -= operand.doubleValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic int intValue() {\n\t\treturn (int) value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn (long) value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn (float) value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 {@code MutableDouble}</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableDouble) {\n\t\t\treturn (Double.doubleToLongBits(((MutableDouble)obj).value) == Double.doubleToLongBits(value));\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal long bits = Double.doubleToLongBits(value);\n\t\treturn (int) (bits ^ bits >>> 32);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 {@code MutableDouble} 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableDouble other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 <code>float</code> 类型\n *\n * @see Float\n * @since 3.0.1\n */\npublic class MutableFloat extends Number implements Comparable<MutableFloat>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate float value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableFloat() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableFloat(final float value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableFloat(final Number value) {\n\t\tthis(value.floatValue());\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 数字转换错误\n\t */\n\tpublic MutableFloat(final String value) throws NumberFormatException {\n\t\tthis.value = Float.parseFloat(value);\n\t}\n\n\t@Override\n\tpublic Float get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final float value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.floatValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 值+1\n\t * @return this\n\t */\n\tpublic MutableFloat increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t * @return this\n\t */\n\tpublic MutableFloat decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableFloat add(final float operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableFloat add(final Number operand) {\n\t\tthis.value += operand.floatValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableFloat subtract(final float operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableFloat subtract(final Number operand) {\n\t\tthis.value -= operand.floatValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic int intValue() {\n\t\treturn (int) value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn (long) value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 {@link MutableFloat}</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 <code>false</code>\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableFloat) {\n\t\t\treturn (Float.floatToIntBits(((MutableFloat)obj).value) == Float.floatToIntBits(value));\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Float.floatToIntBits(value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 {@link MutableFloat} 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableFloat other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 <code>int</code> 类型\n *\n * @see Integer\n * @since 3.0.1\n */\npublic class MutableInt extends Number implements Comparable<MutableInt>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate int value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableInt() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableInt(final int value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableInt(final Number value) {\n\t\tthis(value.intValue());\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 数字转换错误\n\t */\n\tpublic MutableInt(final String value) throws NumberFormatException {\n\t\tthis.value = Integer.parseInt(value);\n\t}\n\n\t@Override\n\tpublic Integer get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final int value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.intValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 值+1\n\t * @return this\n\t */\n\tpublic MutableInt increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t * @return this\n\t */\n\tpublic MutableInt decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableInt add(final int operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableInt add(final Number operand) {\n\t\tthis.value += operand.intValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableInt subtract(final int operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableInt subtract(final Number operand) {\n\t\tthis.value -= operand.intValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic int intValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 MutableInt</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableInt) {\n\t\t\treturn value == ((MutableInt) obj).intValue();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn this.value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 MutableInt 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableInt other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 {@code long} 类型\n *\n * @see Long\n * @since 3.0.1\n */\npublic class MutableLong extends Number implements Comparable<MutableLong>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate long value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableLong() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tpublic MutableLong(final long value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tpublic MutableLong(final Number value) {\n\t\tthis(value.longValue());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param value String值\n\t * @throws NumberFormatException 数字转换错误\n\t */\n\tpublic MutableLong(final String value) throws NumberFormatException {\n\t\tthis.value = Long.parseLong(value);\n\t}\n\n\t@Override\n\tpublic Long get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t *\n\t * @param value 值\n\t */\n\tpublic void set(final long value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.longValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 值+1\n\t *\n\t * @return this\n\t */\n\tpublic MutableLong increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t *\n\t * @return this\n\t */\n\tpublic MutableLong decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 增加值\n\t *\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableLong add(final long operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t *\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableLong add(final Number operand) {\n\t\tthis.value += operand.longValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableLong subtract(final long operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableLong subtract(final Number operand) {\n\t\tthis.value -= operand.longValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic int intValue() {\n\t\treturn (int) value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 MutableLong</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回{@code true}，否则 {@code false}\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableLong) {\n\t\t\treturn value == ((MutableLong) obj).longValue();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn (int) (value ^ (value >>> 32));\n\t}\n\n\t// -----------------------------------------------------------------------\n\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 MutableLong 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableLong other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.ObjUtil;\n\nimport java.io.Serializable;\n\n/**\n * 可变{@code Object}\n *\n * @param <T> 可变的类型\n * @since 3.0.1\n */\npublic class MutableObj<T> implements Mutable<T>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构建MutableObj\n\t * @param value 被包装的值\n\t * @param <T> 值类型\n\t * @return MutableObj\n\t * @since 5.8.0\n\t */\n\tpublic static <T> MutableObj<T> of(T value){\n\t\treturn new MutableObj<>(value);\n\t}\n\n\tprivate T value;\n\n\t/**\n\t * 构造，空值\n\t */\n\tpublic MutableObj() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tpublic MutableObj(final T value) {\n\t\tthis.value = value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic T get() {\n\t\treturn this.value;\n\t}\n\n\t@Override\n\tpublic void set(final T value) {\n\t\tthis.value = value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (this.getClass() == obj.getClass()) {\n\t\t\tfinal MutableObj<?> that = (MutableObj<?>) obj;\n\t\t\treturn ObjUtil.equals(this.value, that.value);\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn value == null ? 0 : value.hashCode();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn value == null ? \"null\" : value.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutablePair.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.lang.Pair;\n\n/**\n * 可变{@link Pair}实现，可以修改键和值\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 5.7.16\n */\npublic class MutablePair<K, V> extends Pair<K, V> implements Mutable<Pair<K, V>>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t * @param key   键\n\t * @param value 值\n\t */\n\tpublic MutablePair(K key, V value) {\n\t\tsuper(key, value);\n\t}\n\n\t/**\n\t * 设置键\n\t *\n\t * @param key 新键\n\t * @return this\n\t */\n\tpublic MutablePair<K, V> setKey(K key) {\n\t\tthis.key = key;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置值\n\t *\n\t * @param value 新值\n\t * @return this\n\t */\n\tpublic MutablePair<K, V> setValue(V value) {\n\t\tthis.value = value;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Pair<K, V> get() {\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void set(Pair<K, V> pair) {\n\t\tthis.key = pair.getKey();\n\t\tthis.value = pair.getValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java",
    "content": "package cn.hutool.core.lang.mutable;\n\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 可变 <code>short</code> 类型\n *\n * @see Short\n * @since 3.0.1\n */\npublic class MutableShort extends Number implements Comparable<MutableShort>, Mutable<Number> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate short value;\n\n\t/**\n\t * 构造，默认值0\n\t */\n\tpublic MutableShort() {\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableShort(final short value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造\n\t * @param value 值\n\t */\n\tpublic MutableShort(final Number value) {\n\t\tthis(value.shortValue());\n\t}\n\n\t/**\n\t * 构造\n\t * @param value String值\n\t * @throws NumberFormatException 转为Short错误\n\t */\n\tpublic MutableShort(final String value) throws NumberFormatException {\n\t\tthis.value = Short.parseShort(value);\n\t}\n\n\t@Override\n\tpublic Short get() {\n\t\treturn this.value;\n\t}\n\n\t/**\n\t * 设置值\n\t * @param value 值\n\t */\n\tpublic void set(final short value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void set(final Number value) {\n\t\tthis.value = value.shortValue();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 值+1\n\t * @return this\n\t */\n\tpublic MutableShort increment() {\n\t\tvalue++;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 值减一\n\t * @return this\n\t */\n\tpublic MutableShort decrement() {\n\t\tvalue--;\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值\n\t * @return this\n\t */\n\tpublic MutableShort add(final short operand) {\n\t\tthis.value += operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加值\n\t * @param operand 被增加的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableShort add(final Number operand) {\n\t\tthis.value += operand.shortValue();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值\n\t * @return this\n\t */\n\tpublic MutableShort subtract(final short operand) {\n\t\tthis.value -= operand;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 减去值\n\t *\n\t * @param operand 被减的值，非空\n\t * @return this\n\t * @throws NullPointerException if the object is null\n\t */\n\tpublic MutableShort subtract(final Number operand) {\n\t\tthis.value -= operand.shortValue();\n\t\treturn this;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic short shortValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic int intValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic long longValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic float floatValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic double doubleValue() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 相等需同时满足如下条件：\n\t * <ol>\n\t * \t<li>非空</li>\n\t * \t<li>类型为 {@link MutableShort}</li>\n\t * \t<li>值相等</li>\n\t * </ol>\n\t *\n\t * @param obj 比对的对象\n\t * @return 相同返回<code>true</code>，否则 <code>false</code>\n\t */\n\t@Override\n\tpublic boolean equals(final Object obj) {\n\t\tif (obj instanceof MutableShort) {\n\t\t\treturn value == ((MutableShort) obj).shortValue();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn value;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t/**\n\t * 比较\n\t *\n\t * @param other 其它 {@link MutableShort} 对象\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t */\n\t@Override\n\tpublic int compareTo(final MutableShort other) {\n\t\treturn NumberUtil.compare(this.value, other.value);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t@Override\n\tpublic String toString() {\n\t\treturn String.valueOf(value);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/mutable/package-info.java",
    "content": "/**\n * 提供可变值对象的封装，用于封装int、long等不可变值，使其可变\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang.mutable;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/package-info.java",
    "content": "/**\n * 语言特性包，包括大量便捷的数据结构，例如验证器Validator，分布式ID生成器Snowflake等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.lang;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/PhantomObj.java",
    "content": "package cn.hutool.core.lang.ref;\n\nimport cn.hutool.core.util.ObjUtil;\n\nimport java.lang.ref.PhantomReference;\nimport java.lang.ref.ReferenceQueue;\nimport java.util.Objects;\n\n/**\n * 虚引用对象，在GC时发现虚引用对象，会将{@link PhantomReference}插入{@link ReferenceQueue}。 <br>\n * 此时对象未被真正回收，要等到{@link ReferenceQueue}被真正处理后才会被回收。\n *\n * @param <T> 键类型\n */\npublic class PhantomObj<T> extends PhantomReference<T> implements Ref<T>{\n\tprivate final int hashCode;\n\n\t/**\n\t * 构造\n\t *\n\t * @param obj   原始对象\n\t * @param queue {@link ReferenceQueue}\n\t */\n\tpublic PhantomObj(final T obj, final ReferenceQueue<? super T> queue) {\n\t\tsuper(obj, queue);\n\t\thashCode = Objects.hashCode(obj);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn hashCode;\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object other) {\n\t\tif (other == this) {\n\t\t\treturn true;\n\t\t} else if (other instanceof PhantomObj) {\n\t\t\treturn ObjUtil.equals(((PhantomObj<?>) other).get(), get());\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/Ref.java",
    "content": "package cn.hutool.core.lang.ref;\n\n/**\n * 针对{@link java.lang.ref.Reference}的接口定义，用于扩展功能<br>\n * 例如提供自定义的无需回收对象\n *\n * @param <T> 对象类型\n */\n@FunctionalInterface\npublic interface Ref<T> {\n\n\t/**\n\t * 获取引用的原始对象\n\t *\n\t * @return 原始对象\n\t */\n\tT get();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/ReferenceType.java",
    "content": "package cn.hutool.core.lang.ref;\n\nimport java.lang.ref.PhantomReference;\nimport java.lang.ref.ReferenceQueue;\n\n/**\n * 引用类型\n *\n * @author Looly\n */\npublic enum ReferenceType {\n\t/**\n\t * 强引用，不回收\n\t */\n\tSTRONG,\n\t/**\n\t * 软引用，在GC报告内存不足时会被GC回收\n\t */\n\tSOFT,\n\t/**\n\t * 弱引用，在GC时发现弱引用会回收其对象\n\t */\n\tWEAK,\n\t/**\n\t * 虚引用，在GC时发现虚引用对象，会将{@link PhantomReference}插入{@link ReferenceQueue}。 <br>\n\t * 此时对象未被真正回收，要等到{@link ReferenceQueue}被真正处理后才会被回收。\n\t */\n\tPHANTOM\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/SoftObj.java",
    "content": "package cn.hutool.core.lang.ref;\n\nimport java.lang.ref.ReferenceQueue;\nimport java.lang.ref.SoftReference;\nimport java.util.Objects;\n\n/**\n * 软引用对象，在GC报告内存不足时会被GC回收\n *\n * @param <T> 键类型\n */\npublic class SoftObj<T> extends SoftReference<T> implements Ref<T>{\n\tprivate final int hashCode;\n\n\t/**\n\t * 构造\n\t *\n\t * @param obj   原始对象\n\t * @param queue {@link ReferenceQueue}\n\t */\n\tpublic SoftObj(final T obj, final ReferenceQueue<? super T> queue) {\n\t\tsuper(obj, queue);\n\t\thashCode = Objects.hashCode(obj);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn hashCode;\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object other) {\n\t\tif (other == this) {\n\t\t\treturn true;\n\t\t} else if (other instanceof SoftObj) {\n\t\t\treturn Objects.equals(((SoftObj<?>) other).get(), get());\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/StrongObj.java",
    "content": "package cn.hutool.core.lang.ref;\n\nimport java.util.Objects;\n\n/**\n * 弱引用对象，在GC时发现弱引用会回收其对象\n *\n * @param <T> 键类型\n */\npublic class StrongObj<T> implements Ref<T> {\n\n\tprivate final T obj;\n\n\t/**\n\t * 构造\n\t *\n\t * @param obj 原始对象\n\t */\n\tpublic StrongObj(final T obj) {\n\t\tthis.obj = obj;\n\t}\n\n\t@Override\n\tpublic T get() {\n\t\treturn this.obj;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hashCode(obj);\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object other) {\n\t\tif (other == this) {\n\t\t\treturn true;\n\t\t} else if (other instanceof StrongObj) {\n\t\t\treturn Objects.equals(((StrongObj<?>) other).get(), get());\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/WeakObj.java",
    "content": "package cn.hutool.core.lang.ref;\n\nimport cn.hutool.core.util.ObjUtil;\n\nimport java.lang.ref.ReferenceQueue;\nimport java.lang.ref.WeakReference;\nimport java.util.Objects;\n\n/**\n * 弱引用对象，在GC时发现弱引用会回收其对象\n *\n * @param <T> 键类型\n */\npublic class WeakObj<T> extends WeakReference<T> implements Ref<T>{\n\tprivate final int hashCode;\n\n\t/**\n\t * 构造\n\t *\n\t * @param obj   原始对象\n\t * @param queue {@link ReferenceQueue}\n\t */\n\tpublic WeakObj(final T obj, final ReferenceQueue<? super T> queue) {\n\t\tsuper(obj, queue);\n\t\thashCode = Objects.hashCode(obj);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn hashCode;\n\t}\n\n\t@Override\n\tpublic boolean equals(final Object other) {\n\t\tif (other == this) {\n\t\t\treturn true;\n\t\t} else if (other instanceof WeakObj) {\n\t\t\treturn ObjUtil.equals(((WeakObj<?>) other).get(), get());\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/ref/package-info.java",
    "content": "/**\n * 引用工具封装，主要针对{@link java.lang.ref.Reference} 工具化封装<br>\n * 主要封装包括：\n * <pre>\n * 1. {@link java.lang.ref.SoftReference} 软引用，在GC报告内存不足时会被GC回收\n * 2. {@link java.lang.ref.WeakReference} 弱引用，在GC时发现弱引用会回收其对象\n * 3. {@link java.lang.ref.PhantomReference} 虚引用，在GC时发现虚引用对象，会将{@link java.lang.ref.PhantomReference}插入{@link java.lang.ref.ReferenceQueue}。 此时对象未被真正回收，要等到{@link java.lang.ref.ReferenceQueue}被真正处理后才会被回收。\n * </pre>\n */\npackage cn.hutool.core.lang.ref;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/reflect/ActualTypeMapperPool.java",
    "content": "package cn.hutool.core.lang.reflect;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.TypeUtil;\n\nimport java.lang.reflect.GenericArrayType;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.lang.reflect.TypeVariable;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 泛型变量和泛型实际类型映射关系缓存\n *\n * @author looly\n * @since 5.4.2\n */\npublic class ActualTypeMapperPool {\n\n\tprivate static final WeakKeyValueConcurrentMap<Type, Map<Type, Type>> CACHE = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 获取泛型变量和泛型实际类型的对应关系Map\n\t *\n\t * @param type 被解析的包含泛型参数的类\n\t * @return 泛型对应关系Map\n\t */\n\tpublic static Map<Type, Type> get(Type type) {\n\t\treturn CACHE.computeIfAbsent(type, (key) -> createTypeMap(type));\n\t}\n\n\t/**\n\t * 获取泛型变量名（字符串）和泛型实际类型的对应关系Map\n\t *\n\t * @param type 被解析的包含泛型参数的类\n\t * @return 泛型对应关系Map\n\t * @since 5.7.16\n\t */\n\tpublic static Map<String, Type> getStrKeyMap(Type type) {\n\t\treturn Convert.toMap(String.class, Type.class, get(type));\n\t}\n\n\t/**\n\t * 获得泛型变量对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t *\n\t * @param type         类\n\t * @param typeVariable 泛型变量，例如T等\n\t * @return 实际类型，可能为Class等\n\t */\n\tpublic static Type getActualType(Type type, TypeVariable<?> typeVariable) {\n\t\tfinal Map<Type, Type> typeTypeMap = get(type);\n\t\tType result = typeTypeMap.get(typeVariable);\n\t\twhile (result instanceof TypeVariable) {\n\t\t\tresult = typeTypeMap.get(result);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获得泛型变量对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t *\n\t * @param type             类\n\t * @param genericArrayType 泛型数组类型\n\t * @return 实际类型，可能为Class等\n\t * @since 5.8.37\n\t */\n\tpublic static Type getActualType(Type type, GenericArrayType genericArrayType) {\n\t\tfinal Map<Type, Type> typeTypeMap = get(type);\n\t\tType actualType = typeTypeMap.get(genericArrayType);\n\n\t\tif (actualType == null) {\n\t\t\tType componentType = typeTypeMap.get(genericArrayType.getGenericComponentType());\n\t\t\tif (!(componentType instanceof Class<?>)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tactualType = ArrayUtil.getArrayType((Class<?>) componentType);\n\t\t\ttypeTypeMap.put(genericArrayType, actualType);\n\t\t}\n\n\t\treturn actualType;\n\t}\n\n\t/**\n\t * 获取指定泛型变量对应的真实类型<br>\n\t * 由于子类中泛型参数实现和父类（接口）中泛型定义位置是一一对应的，因此可以通过对应关系找到泛型实现类型<br>\n\t *\n\t * @param type          真实类型所在类，此类中记录了泛型参数对应的实际类型\n\t * @param typeVariables 泛型变量，需要的实际类型对应的泛型参数\n\t * @return 给定泛型参数对应的实际类型，如果无对应类型，对应位置返回null\n\t */\n\tpublic static Type[] getActualTypes(Type type, Type... typeVariables) {\n\t\t// 查找方法定义所在类或接口中此泛型参数的位置\n\t\tfinal Type[] result = new Type[typeVariables.length];\n\t\tfor (int i = 0; i < typeVariables.length; i++) {\n\t\t\tresult[i] = (typeVariables[i] instanceof TypeVariable)\n\t\t\t\t? getActualType(type, (TypeVariable<?>) typeVariables[i])\n\t\t\t\t: typeVariables[i];\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 创建类中所有的泛型变量和泛型实际类型的对应关系Map\n\t *\n\t * @param type 被解析的包含泛型参数的类\n\t * @return 泛型对应关系Map\n\t */\n\tprivate static Map<Type, Type> createTypeMap(Type type) {\n\t\tfinal Map<Type, Type> typeMap = new HashMap<>();\n\n\t\t// 按继承层级寻找泛型变量和实际类型的对应关系\n\t\t// 在类中，对应关系分为两类：\n\t\t// 1. 父类定义变量，子类标注实际类型\n\t\t// 2. 父类定义变量，子类继承这个变量，让子类的子类去标注，以此类推\n\t\t// 此方法中我们将每一层级的对应关系全部加入到Map中，查找实际类型的时候，根据传入的泛型变量，\n\t\t// 找到对应关系，如果对应的是继承的泛型变量，则递归继续找，直到找到实际或返回null为止。\n\t\t// 如果传入的非Class，例如TypeReference，获取到泛型参数中实际的泛型对象类，继续按照类处理\n\t\twhile (null != type) {\n\t\t\tfinal ParameterizedType parameterizedType = TypeUtil.toParameterizedType(type);\n\t\t\tif (null == parameterizedType) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tfinal Type[] typeArguments = parameterizedType.getActualTypeArguments();\n\t\t\tfinal Class<?> rawType = (Class<?>) parameterizedType.getRawType();\n\t\t\tfinal Type[] typeParameters = rawType.getTypeParameters();\n\n\t\t\tType value;\n\t\t\tfor (int i = 0; i < typeParameters.length; i++) {\n\t\t\t\tvalue = typeArguments[i];\n\t\t\t\t// 跳过泛型变量对应泛型变量的情况\n\t\t\t\tif (false == value instanceof TypeVariable) {\n\t\t\t\t\ttypeMap.put(typeParameters[i], value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttype = rawType;\n\t\t}\n\n\t\treturn typeMap;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/reflect/LookupFactory.java",
    "content": "package cn.hutool.core.lang.reflect;\n\nimport cn.hutool.core.exceptions.UtilException;\n\nimport java.lang.invoke.MethodHandles;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * {@link MethodHandles.Lookup}工厂，用于创建{@link MethodHandles.Lookup}对象<br>\n * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial\n * 时会出现权限不够问题，抛出\"no private access for invokespecial\"异常，因此针对JDK8及JDK9+分别封装lookup方法。\n *\n * 参考：\n * <p><a href=\"https://blog.csdn.net/u013202238/article/details/108687086\">https://blog.csdn.net/u013202238/article/details/108687086</a></p>\n *\n * @author looly\n * @since 5.7.7\n */\npublic class LookupFactory {\n\n\tprivate static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED\n\t\t\t| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;\n\n\tprivate static Constructor<MethodHandles.Lookup> java8LookupConstructor;\n\tprivate static Method privateLookupInMethod;\n\n\tstatic {\n\t\t//先查询jdk9 开始提供的java.lang.invoke.MethodHandles.privateLookupIn方法,\n\t\t//如果没有说明是jdk8的版本.(不考虑jdk8以下版本)\n\t\ttry {\n\t\t\t//noinspection JavaReflectionMemberAccess\n\t\t\tprivateLookupInMethod = MethodHandles.class.getMethod(\"privateLookupIn\", Class.class, MethodHandles.Lookup.class);\n\t\t} catch (NoSuchMethodException ignore) {\n\t\t\t//ignore\n\t\t}\n\n\t\t//jdk8\n\t\t//这种方式其实也适用于jdk9及以上的版本,但是上面优先,可以避免 jdk9 反射警告\n\t\tif (privateLookupInMethod == null) {\n\t\t\ttry {\n\t\t\t\tjava8LookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);\n\t\t\t\tjava8LookupConstructor.setAccessible(true);\n\t\t\t} catch (NoSuchMethodException e) {\n\t\t\t\t//可能是jdk8 以下版本\n\t\t\t\tthrow new IllegalStateException(\n\t\t\t\t\t\t\"There is neither 'privateLookupIn(Class, Lookup)' nor 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.\", e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial\n\t * 时会出现权限不够问题，抛出\"no private access for invokespecial\"异常，因此针对JDK8及JDK9+分别封装lookup方法。\n\t *\n\t * @param callerClass 被调用的类或接口\n\t * @return {@link MethodHandles.Lookup}\n\t */\n\tpublic static MethodHandles.Lookup lookup(Class<?> callerClass) {\n\t\t//使用反射,因为当前jdk可能不是java9或以上版本\n\t\tif (privateLookupInMethod != null) {\n\t\t\ttry {\n\t\t\t\treturn (MethodHandles.Lookup) privateLookupInMethod.invoke(MethodHandles.class, callerClass, MethodHandles.lookup());\n\t\t\t} catch (IllegalAccessException | InvocationTargetException e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t\t//jdk 8\n\t\ttry {\n\t\t\treturn java8LookupConstructor.newInstance(callerClass, ALLOWED_MODES);\n\t\t} catch (Exception e) {\n\t\t\tthrow new IllegalStateException(\"no 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.\", e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/reflect/MethodHandleUtil.java",
    "content": "package cn.hutool.core.lang.reflect;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\nimport java.lang.reflect.Method;\n\n/**\n * 方法句柄{@link MethodHandle}封装工具类<br>\n * 方法句柄是一个有类型的，可以直接执行的指向底层方法、构造器、field等的引用，可以简单理解为函数指针，它是一种更加底层的查找、调整和调用方法的机制。\n * 参考：\n * <ul>\n *     <li>https://stackoverflow.com/questions/22614746/how-do-i-invoke-java-8-default-methods-reflectively</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.7\n */\npublic class MethodHandleUtil {\n\n\t/**\n\t * jdk8中如果直接调用{@link MethodHandles#lookup()}获取到的{@link MethodHandles.Lookup}在调用findSpecial和unreflectSpecial\n\t * 时会出现权限不够问题，抛出\"no private access for invokespecial\"异常，因此针对JDK8及JDK9+分别封装lookup方法。\n\t *\n\t * @param callerClass 被调用的类或接口\n\t * @return {@link MethodHandles.Lookup}\n\t */\n\tpublic static MethodHandles.Lookup lookup(Class<?> callerClass) {\n\t\treturn LookupFactory.lookup(callerClass);\n\t}\n\n\t/**\n\t * 查找指定方法的方法句柄<br>\n\t * 此方法只会查找：\n\t * <ul>\n\t *     <li>当前类的方法（包括构造方法和private方法）</li>\n\t *     <li>父类的方法（包括构造方法和private方法）</li>\n\t *     <li>当前类的static方法</li>\n\t * </ul>\n\t *\n\t * @param callerClass 方法所在类或接口\n\t * @param name        方法名称，{@code null}或者空则查找构造方法\n\t * @param type        返回类型和参数类型\n\t * @return 方法句柄 {@link MethodHandle}，{@code null}表示未找到方法\n\t */\n\tpublic static MethodHandle findMethod(Class<?> callerClass, String name, MethodType type) {\n\t\tif (StrUtil.isBlank(name)) {\n\t\t\treturn findConstructor(callerClass, type);\n\t\t}\n\n\t\tMethodHandle handle = null;\n\t\tfinal MethodHandles.Lookup lookup = lookup(callerClass);\n\t\ttry {\n\t\t\thandle = lookup.findVirtual(callerClass, name, type);\n\t\t} catch (IllegalAccessException | NoSuchMethodException ignore) {\n\t\t\t//ignore\n\t\t}\n\n\t\t// static方法\n\t\tif (null == handle) {\n\t\t\ttry {\n\t\t\t\thandle = lookup.findStatic(callerClass, name, type);\n\t\t\t} catch (IllegalAccessException | NoSuchMethodException ignore) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\n\t\t// 特殊方法，包括构造方法、私有方法等\n\t\tif (null == handle) {\n\t\t\ttry {\n\t\t\t\thandle = lookup.findSpecial(callerClass, name, type, callerClass);\n\t\t\t} catch (NoSuchMethodException ignore) {\n\t\t\t\t//ignore\n\t\t\t} catch (IllegalAccessException e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\n\t\treturn handle;\n\t}\n\n\t/**\n\t * 查找指定的构造方法\n\t *\n\t * @param callerClass 类\n\t * @param args        参数\n\t * @return 构造方法句柄\n\t */\n\tpublic static MethodHandle findConstructor(Class<?> callerClass, Class<?>... args) {\n\t\treturn findConstructor(callerClass, MethodType.methodType(void.class, args));\n\t}\n\n\t/**\n\t * 查找指定的构造方法\n\t *\n\t * @param callerClass 类\n\t * @param type        参数类型，此处返回类型应为void.class\n\t * @return 构造方法句柄\n\t */\n\tpublic static MethodHandle findConstructor(Class<?> callerClass, MethodType type) {\n\t\tfinal MethodHandles.Lookup lookup = lookup(callerClass);\n\t\ttry {\n\t\t\treturn lookup.findConstructor(callerClass, type);\n\t\t} catch (NoSuchMethodException e) {\n\t\t\treturn null;\n\t\t} catch (IllegalAccessException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行接口或对象中的特殊方法（private、static等）<br>\n\t *\n\t * <pre class=\"code\">\n\t *     interface Duck {\n\t *         default String quack() {\n\t *             return \"Quack\";\n\t *         }\n\t *     }\n\t *\n\t *     Duck duck = (Duck) Proxy.newProxyInstance(\n\t *         ClassLoaderUtil.getClassLoader(),\n\t *         new Class[] { Duck.class },\n\t *         MethodHandleUtil::invokeDefault);\n\t * </pre>\n\t *\n\t * @param <T>        返回结果类型\n\t * @param obj        接口的子对象或代理对象\n\t * @param methodName 方法名称\n\t * @param args       参数\n\t * @return 结果\n\t */\n\tpublic static <T> T invokeSpecial(Object obj, String methodName, Object... args) {\n\t\tAssert.notNull(obj, \"Object to get method must be not null!\");\n\t\tAssert.notBlank(methodName, \"Method name must be not blank!\");\n\n\t\tfinal Method method = ReflectUtil.getMethodOfObj(obj, methodName, args);\n\t\tif (null == method) {\n\t\t\tthrow new UtilException(\"No such method: [{}] from [{}]\", methodName, obj.getClass());\n\t\t}\n\t\treturn invokeSpecial(obj, method, args);\n\t}\n\n\t/**\n\t * 执行接口或对象中的方法\n\t *\n\t * @param <T>    返回结果类型\n\t * @param obj    接口的子对象或代理对象\n\t * @param method 方法\n\t * @param args   参数\n\t * @return 结果\n\t */\n\tpublic static <T> T invoke(Object obj, Method method, Object... args) {\n\t\treturn invoke(false, obj, method, args);\n\t}\n\n\t/**\n\t * 执行接口或对象中的特殊方法（private、static等）<br>\n\t *\n\t * <pre class=\"code\">\n\t *     interface Duck {\n\t *         default String quack() {\n\t *             return \"Quack\";\n\t *         }\n\t *     }\n\t *\n\t *     Duck duck = (Duck) Proxy.newProxyInstance(\n\t *         ClassLoaderUtil.getClassLoader(),\n\t *         new Class[] { Duck.class },\n\t *         MethodHandleUtil::invoke);\n\t * </pre>\n\t *\n\t * @param <T>    返回结果类型\n\t * @param obj    接口的子对象或代理对象\n\t * @param method 方法\n\t * @param args   参数\n\t * @return 结果\n\t */\n\tpublic static <T> T invokeSpecial(Object obj, Method method, Object... args) {\n\t\treturn invoke(true, obj, method, args);\n\t}\n\n\t/**\n\t * 执行接口或对象中的方法<br>\n\t *\n\t * <pre class=\"code\">\n\t *     interface Duck {\n\t *         default String quack() {\n\t *             return \"Quack\";\n\t *         }\n\t *     }\n\t *\n\t *     Duck duck = (Duck) Proxy.newProxyInstance(\n\t *         ClassLoaderUtil.getClassLoader(),\n\t *         new Class[] { Duck.class },\n\t *         MethodHandleUtil::invoke);\n\t * </pre>\n\t *\n\t * @param <T>       返回结果类型\n\t * @param isSpecial 是否为特殊方法（private、static等）\n\t * @param obj       接口的子对象或代理对象\n\t * @param method    方法\n\t * @param args      参数\n\t * @return 结果\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T invoke(boolean isSpecial, Object obj, Method method, Object... args) {\n\t\tAssert.notNull(method, \"Method must be not null!\");\n\t\tfinal Class<?> declaringClass = method.getDeclaringClass();\n\t\tfinal MethodHandles.Lookup lookup = lookup(declaringClass);\n\t\ttry {\n\t\t\tMethodHandle handle = isSpecial ? lookup.unreflectSpecial(method, declaringClass)\n\t\t\t\t\t: lookup.unreflect(method);\n\t\t\tif (null != obj) {\n\t\t\t\thandle = handle.bindTo(obj);\n\t\t\t}\n\t\t\treturn (T) handle.invokeWithArguments(args);\n\t\t} catch (Throwable e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/reflect/package-info.java",
    "content": "/**\n * 提供反射相关功能对象和类\n *\n * @author looly\n * @since 5.4.2\n */\npackage cn.hutool.core.lang.reflect;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/Node.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.comparator.CompareUtil;\n\nimport java.io.Serializable;\n\n/**\n * 节点接口，提供节点相关的的方法定义\n *\n * @param <T> ID类型\n * @author looly\n * @since 5.2.4\n */\npublic interface Node<T> extends Comparable<Node<T>>, Serializable {\n\n\t/**\n\t * 获取ID\n\t *\n\t * @return ID\n\t */\n\tT getId();\n\n\t/**\n\t * 设置ID\n\t *\n\t * @param id ID\n\t * @return this\n\t */\n\tNode<T> setId(T id);\n\n\t/**\n\t * 获取父节点ID\n\t *\n\t * @return 父节点ID\n\t */\n\tT getParentId();\n\n\t/**\n\t * 设置父节点ID\n\t *\n\t * @param parentId 父节点ID\n\t * @return this\n\t */\n\tNode<T> setParentId(T parentId);\n\n\t/**\n\t * 获取节点标签名称\n\t *\n\t * @return 节点标签名称\n\t */\n\tCharSequence getName();\n\n\t/**\n\t * 设置节点标签名称\n\t *\n\t * @param name 节点标签名称\n\t * @return this\n\t */\n\tNode<T> setName(CharSequence name);\n\n\t/**\n\t * 获取权重\n\t *\n\t * @return 权重\n\t */\n\tComparable<?> getWeight();\n\n\t/**\n\t * 设置权重\n\t *\n\t * @param weight 权重\n\t * @return this\n\t */\n\tNode<T> setWeight(Comparable<?> weight);\n\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\", \"NullableProblems\"})\n\t@Override\n\tdefault int compareTo(Node node) {\n\t\tif(null == node){\n\t\t\treturn 1;\n\t\t}\n\t\tfinal Comparable weight = this.getWeight();\n\t\tfinal Comparable weightOther = node.getWeight();\n\t\treturn CompareUtil.compare(weight, weightOther);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/Tree.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * 通过转换器将你的实体转化为TreeNodeMap节点实体 属性都存在此处,属性有序，可支持排序\n *\n * @param <T> ID类型\n * @author liangbaikai\n * @since 5.2.1\n */\npublic class Tree<T> extends LinkedHashMap<String, Object> implements Node<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final TreeNodeConfig treeNodeConfig;\n\tprivate Tree<T> parent;\n\n\tpublic Tree() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param treeNodeConfig TreeNode配置\n\t */\n\tpublic Tree(TreeNodeConfig treeNodeConfig) {\n\t\tthis.treeNodeConfig = ObjectUtil.defaultIfNull(\n\t\t\t\ttreeNodeConfig, TreeNodeConfig.DEFAULT_CONFIG);\n\t}\n\n\t/**\n\t * 获取节点配置\n\t *\n\t * @return 节点配置\n\t * @since 5.7.2\n\t */\n\tpublic TreeNodeConfig getConfig() {\n\t\treturn this.treeNodeConfig;\n\t}\n\n\t/**\n\t * 获取父节点\n\t *\n\t * @return 父节点\n\t * @since 5.2.4\n\t */\n\tpublic Tree<T> getParent() {\n\t\treturn parent;\n\t}\n\n\t/**\n\t * 获取ID对应的节点，如果有多个ID相同的节点，只返回第一个。<br>\n\t * 此方法只查找此节点及子节点，采用广度优先遍历。\n\t *\n\t * @param id ID\n\t * @return 节点\n\t * @since 5.2.4\n\t */\n\tpublic Tree<T> getNode(T id) {\n\t\treturn TreeUtil.getNode(this, id);\n\t}\n\n\t/**\n\t * 获取所有父节点名称列表\n\t *\n\t * <p>\n\t * 比如有个人在研发1部，他上面有研发部，接着上面有技术中心<br>\n\t * 返回结果就是：[研发一部, 研发中心, 技术中心]\n\t *\n\t * @param id                 节点ID\n\t * @param includeCurrentNode 是否包含当前节点的名称\n\t * @return 所有父节点名称列表\n\t * @since 5.2.4\n\t */\n\tpublic List<CharSequence> getParentsName(T id, boolean includeCurrentNode) {\n\t\treturn TreeUtil.getParentsName(getNode(id), includeCurrentNode);\n\t}\n\n\t/**\n\t * 获取所有父节点名称列表\n\t *\n\t * <p>\n\t * 比如有个人在研发1部，他上面有研发部，接着上面有技术中心<br>\n\t * 返回结果就是：[研发一部, 研发中心, 技术中心]\n\t *\n\t * @param includeCurrentNode 是否包含当前节点的名称\n\t * @return 所有父节点名称列表\n\t * @since 5.2.4\n\t */\n\tpublic List<CharSequence> getParentsName(boolean includeCurrentNode) {\n\t\treturn TreeUtil.getParentsName(this, includeCurrentNode);\n\t}\n\n\t/**\n\t * 设置父节点\n\t *\n\t * @param parent 父节点\n\t * @return this\n\t * @since 5.2.4\n\t */\n\tpublic Tree<T> setParent(Tree<T> parent) {\n\t\tthis.parent = parent;\n\t\tif (null != parent) {\n\t\t\tthis.setParentId(parent.getId());\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T getId() {\n\t\treturn (T) this.get(treeNodeConfig.getIdKey());\n\t}\n\n\t@Override\n\tpublic Tree<T> setId(T id) {\n\t\tthis.put(treeNodeConfig.getIdKey(), id);\n\t\treturn this;\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T getParentId() {\n\t\treturn (T) this.get(treeNodeConfig.getParentIdKey());\n\t}\n\n\t@Override\n\tpublic Tree<T> setParentId(T parentId) {\n\t\tthis.put(treeNodeConfig.getParentIdKey(), parentId);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic CharSequence getName() {\n\t\treturn (CharSequence) this.get(treeNodeConfig.getNameKey());\n\t}\n\n\t@Override\n\tpublic Tree<T> setName(CharSequence name) {\n\t\tthis.put(treeNodeConfig.getNameKey(), name);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Comparable<?> getWeight() {\n\t\treturn (Comparable<?>) this.get(treeNodeConfig.getWeightKey());\n\t}\n\n\t@Override\n\tpublic Tree<T> setWeight(Comparable<?> weight) {\n\t\tthis.put(treeNodeConfig.getWeightKey(), weight);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取所有子节点\n\t *\n\t * @return 所有子节点\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<Tree<T>> getChildren() {\n\t\treturn (List<Tree<T>>) this.get(treeNodeConfig.getChildrenKey());\n\t}\n\n\t/**\n\t * 是否有子节点，无子节点则此为叶子节点\n\t *\n\t * @return 是否有子节点\n\t * @since 5.7.17\n\t */\n\tpublic boolean hasChild() {\n\t\treturn CollUtil.isNotEmpty(getChildren());\n\t}\n\n\t/**\n\t * 递归树并处理子树下的节点：\n\t *\n\t * @param consumer 节点处理器\n\t * @since 5.7.16\n\t */\n\tpublic void walk(Consumer<Tree<T>> consumer) {\n\t\tconsumer.accept(this);\n\t\tfinal List<Tree<T>> children = getChildren();\n\t\tif (CollUtil.isNotEmpty(children)) {\n\t\t\tchildren.forEach((tree) -> tree.walk(consumer));\n\t\t}\n\t}\n\n\t/**\n\t * 递归过滤并生成新的树<br>\n\t * 通过{@link Filter}指定的过滤规则，本节点或子节点满足过滤条件，则保留当前节点，否则抛弃节点及其子节点\n\t *\n\t * @param filter 节点过滤规则函数，只需处理本级节点本身即可\n\t * @return 过滤后的节点，{@code null} 表示不满足过滤要求，丢弃之\n\t * @see #filter(Filter)\n\t * @since 5.7.17\n\t */\n\tpublic Tree<T> filterNew(Filter<Tree<T>> filter) {\n\t\treturn cloneTree().filter(filter);\n\t}\n\n\t/**\n\t * 递归过滤当前树，注意此方法会修改当前树<br>\n\t * 通过{@link Filter}指定的过滤规则，本节点或子节点满足过滤条件，则保留当前节点及其所有子节点，否则抛弃节点及其子节点\n\t *\n\t * @param filter 节点过滤规则函数，只需处理本级节点本身即可\n\t * @return 过滤后的节点，{@code null} 表示不满足过滤要求，丢弃之\n\t * @see #filterNew(Filter)\n\t * @since 5.7.17\n\t */\n\tpublic Tree<T> filter(Filter<Tree<T>> filter) {\n\t\tif(filter.accept(this)){\n\t\t\t// 本节点满足，则包括所有子节点都保留\n\t\t\treturn this;\n\t\t}\n\n\t\tfinal List<Tree<T>> children = getChildren();\n\t\tif (CollUtil.isNotEmpty(children)) {\n\t\t\t// 递归过滤子节点\n\t\t\tfinal List<Tree<T>> filteredChildren = new ArrayList<>(children.size());\n\t\t\tTree<T> filteredChild;\n\t\t\tfor (Tree<T> child : children) {\n\t\t\t\tfilteredChild = child.filter(filter);\n\t\t\t\tif (null != filteredChild) {\n\t\t\t\t\tfilteredChildren.add(filteredChild);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(CollUtil.isNotEmpty(filteredChildren)){\n\t\t\t\t// 子节点有符合过滤条件的节点，则本节点保留\n\t\t\t\treturn this.setChildren(filteredChildren);\n\t\t\t} else {\n\t\t\t\tthis.setChildren(null);\n\t\t\t}\n\t\t}\n\n\t\t// 子节点都不符合过滤条件，检查本节点\n\t\treturn null;\n\t}\n\n\t/**\n\t * 设置子节点，设置后会覆盖所有原有子节点\n\t *\n\t * @param children 子节点列表，如果为{@code null}表示移除子节点\n\t * @return this\n\t */\n\tpublic Tree<T> setChildren(List<Tree<T>> children) {\n\t\tif(null == children){\n\t\t\tthis.remove(treeNodeConfig.getChildrenKey());\n\t\t}\n\t\tthis.put(treeNodeConfig.getChildrenKey(), children);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加子节点，同时关联子节点的父节点为当前节点\n\t *\n\t * @param children 子节点列表\n\t * @return this\n\t * @since 5.6.7\n\t */\n\t@SafeVarargs\n\tpublic final Tree<T> addChildren(Tree<T>... children) {\n\t\tif (ArrayUtil.isNotEmpty(children)) {\n\t\t\tList<Tree<T>> childrenList = this.getChildren();\n\t\t\tif (null == childrenList) {\n\t\t\t\tchildrenList = new ArrayList<>();\n\t\t\t\tsetChildren(childrenList);\n\t\t\t}\n\t\t\tfor (Tree<T> child : children) {\n\t\t\t\tchild.setParent(this);\n\t\t\t\tchildrenList.add(child);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 扩展属性\n\t *\n\t * @param key   键\n\t * @param value 扩展值\n\t */\n\tpublic void putExtra(String key, Object value) {\n\t\tAssert.notEmpty(key, \"Key must be not empty !\");\n\t\tthis.put(key, value);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringWriter stringWriter = new StringWriter();\n\t\tprintTree(this, new PrintWriter(stringWriter), 0);\n\t\treturn stringWriter.toString();\n\t}\n\n\t/**\n\t * 递归克隆当前节点（即克隆整个树，保留字段值）<br>\n\t * 注意，此方法只会克隆节点，节点属性如果是引用类型，不会克隆\n\t *\n\t * @return 新的节点\n\t * @since 5.7.17\n\t */\n\tpublic Tree<T> cloneTree() {\n\t\tfinal Tree<T> result = ObjectUtil.clone(this);\n\t\tresult.setChildren(cloneChildren(result));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 递归复制子节点\n\t *\n\t * @param parent 新的父节点\n\t * @return 新的子节点列表\n\t */\n\tprivate List<Tree<T>> cloneChildren(final Tree<T> parent) {\n\t\tfinal List<Tree<T>> children = getChildren();\n\t\tif (null == children) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal List<Tree<T>> newChildren = new ArrayList<>(children.size());\n\t\tchildren.forEach((t) -> {\n\t\t\tnewChildren.add(t.cloneTree().setParent(parent));\n\t\t});\n\t\treturn newChildren;\n\t}\n\n\t/**\n\t * 打印\n\t *\n\t * @param tree   树\n\t * @param writer Writer\n\t * @param intent 缩进量\n\t */\n\tprivate static void printTree(Tree<?> tree, PrintWriter writer, int intent) {\n\t\twriter.println(StrUtil.format(\"{}{}[{}]\", StrUtil.repeat(CharUtil.SPACE, intent), tree.getName(), tree.getId()));\n\t\twriter.flush();\n\n\t\tfinal List<? extends Tree<?>> children = tree.getChildren();\n\t\tif (CollUtil.isNotEmpty(children)) {\n\t\t\tfor (Tree<?> child : children) {\n\t\t\t\tprintTree(child, writer, intent + 2);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.tree.parser.NodeParser;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * 树构建器\n *\n * @param <E> ID类型\n */\npublic class TreeBuilder<E> implements Builder<Tree<E>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Tree<E> root;\n\tprivate final Map<E, Tree<E>> idTreeMap;\n\tprivate boolean isBuild;\n\n\t/**\n\t * 创建Tree构建器\n\t *\n\t * @param rootId 根节点ID\n\t * @param <T>    ID类型\n\t * @return TreeBuilder\n\t */\n\tpublic static <T> TreeBuilder<T> of(T rootId) {\n\t\treturn of(rootId, null);\n\t}\n\n\t/**\n\t * 创建Tree构建器\n\t *\n\t * @param rootId 根节点ID\n\t * @param config 配置\n\t * @param <T>    ID类型\n\t * @return TreeBuilder\n\t */\n\tpublic static <T> TreeBuilder<T> of(T rootId, TreeNodeConfig config) {\n\t\treturn new TreeBuilder<>(rootId, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootId 根节点ID\n\t * @param config 配置\n\t */\n\tpublic TreeBuilder(E rootId, TreeNodeConfig config) {\n\t\tthis(new Tree<E>(config).setId(rootId));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootNode 根节点\n\t * @since 5.8.33\n\t */\n\tpublic TreeBuilder(Tree<E> rootNode) {\n\t\tthis.root = rootNode;\n\t\tthis.idTreeMap = new LinkedHashMap<>();\n\t}\n\n\t/**\n\t * 设置ID\n\t *\n\t * @param id ID\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic TreeBuilder<E> setId(E id) {\n\t\tthis.root.setId(id);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置父节点ID\n\t *\n\t * @param parentId 父节点ID\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic TreeBuilder<E> setParentId(E parentId) {\n\t\tthis.root.setParentId(parentId);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置节点标签名称\n\t *\n\t * @param name 节点标签名称\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic TreeBuilder<E> setName(CharSequence name) {\n\t\tthis.root.setName(name);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置权重\n\t *\n\t * @param weight 权重\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic TreeBuilder<E> setWeight(Comparable<?> weight) {\n\t\tthis.root.setWeight(weight);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 扩展属性\n\t *\n\t * @param key   键\n\t * @param value 扩展值\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic TreeBuilder<E> putExtra(String key, Object value) {\n\t\tAssert.notEmpty(key, \"Key must be not empty !\");\n\t\tthis.root.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加节点列表，增加的节点是不带子节点的\n\t *\n\t * @param map 节点列表\n\t * @return this\n\t */\n\tpublic TreeBuilder<E> append(Map<E, Tree<E>> map) {\n\t\tcheckBuilt();\n\n\t\tthis.idTreeMap.putAll(map);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加节点列表，增加的节点是不带子节点的\n\t *\n\t * @param trees 节点列表\n\t * @return this\n\t */\n\tpublic TreeBuilder<E> append(Iterable<Tree<E>> trees) {\n\t\tcheckBuilt();\n\n\t\tfor (Tree<E> tree : trees) {\n\t\t\tthis.idTreeMap.put(tree.getId(), tree);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加节点列表，增加的节点是不带子节点的\n\t *\n\t * @param list       Bean列表\n\t * @param <T>        Bean类型\n\t * @param nodeParser 节点转换器，用于定义一个Bean如何转换为Tree节点\n\t * @return this\n\t */\n\tpublic <T> TreeBuilder<E> append(List<T> list, NodeParser<T, E> nodeParser) {\n\t\tcheckBuilt();\n\n\t\tfinal TreeNodeConfig config = this.root.getConfig();\n\t\tfinal E rootId = this.root.getId();\n\t\tfinal Map<E, Tree<E>> map = this.idTreeMap;\n\t\tTree<E> node;\n\t\tfor (T t : list) {\n\t\t\tnode = new Tree<>(config);\n\t\t\tnodeParser.parse(t, node);\n\t\t\tif (null != rootId && false == rootId.getClass().equals(node.getId().getClass())) {\n\t\t\t\tthrow new IllegalArgumentException(\"rootId type is node.getId().getClass()!\");\n\t\t\t}\n\t\t\t// issue#IAUSHR 如果指定根节点存在，直接复用\n\t\t\tif(Objects.equals(rootId, node.getId())){\n\t\t\t\tthis.root = node;\n\t\t\t}else {\n\t\t\t\t//非根节点\n\t\t\t\tmap.put(node.getId(), node);\n\t\t\t}\n\n\t\t}\n\t\t// this.idTreeMap重复put\n\t\t// return append(map);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加节点列表，增加的节点是不带子节点的\n\t *\n\t * @param <T>        Bean类型\n\t * @param list       Bean列表\n\t * @param rootId     根ID\n\t * @param nodeParser 节点转换器，用于定义一个Bean如何转换为Tree节点\n\t * @return this\n\t * @since 5.8.6\n\t * @deprecated rootId参数可以不提供，在root节点中直接获取，请使用{@link #append(List, NodeParser)}\n\t */\n\t@Deprecated\n\tpublic <T> TreeBuilder<E> append(List<T> list, E rootId, NodeParser<T, E> nodeParser) {\n\t\tcheckBuilt();\n\n\t\tfinal TreeNodeConfig config = this.root.getConfig();\n\t\tfinal Map<E, Tree<E>> map = this.idTreeMap;\n\t\tTree<E> node;\n\t\tfor (T t : list) {\n\t\t\tnode = new Tree<>(config);\n\t\t\tnodeParser.parse(t, node);\n\t\t\tif (null != rootId && false == rootId.getClass().equals(node.getId().getClass())) {\n\t\t\t\tthrow new IllegalArgumentException(\"rootId type is node.getId().getClass()!\");\n\t\t\t}\n\t\t\t// issue#IAUSHR 如果指定根节点存在，直接复用\n\t\t\tif(Objects.equals(rootId, node.getId())){\n\t\t\t\tthis.root = node;\n\t\t\t}else {\n\t\t\t\t//非根节点\n\t\t\t\tmap.put(node.getId(), node);\n\t\t\t}\n\n\t\t}\n\t\t// this.idTreeMap重复put\n\t\t// return append(map);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置Builder，实现复用\n\t *\n\t * @return this\n\t */\n\tpublic TreeBuilder<E> reset() {\n\t\tthis.idTreeMap.clear();\n\t\tthis.root.setChildren(null);\n\t\tthis.isBuild = false;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Tree<E> build() {\n\t\tcheckBuilt();\n\n\t\tbuildFromMap();\n\t\tcutTree();\n\n\t\tthis.isBuild = true;\n\t\tthis.idTreeMap.clear();\n\n\t\treturn root;\n\t}\n\n\t/**\n\t * 构建树列表，没有顶层节点，例如：\n\t *\n\t * <pre>\n\t * -用户管理\n\t *  -用户管理\n\t *    +用户添加\n\t * - 部门管理\n\t *  -部门管理\n\t *    +部门添加\n\t * </pre>\n\t *\n\t * @return 树列表\n\t */\n\tpublic List<Tree<E>> buildList() {\n\t\tif (isBuild) {\n\t\t\t// 已经构建过了\n\t\t\treturn this.root.getChildren();\n\t\t}\n\t\treturn build().getChildren();\n\t}\n\n\t/**\n\t * 开始构建\n\t */\n\tprivate void buildFromMap() {\n\t\tif (MapUtil.isEmpty(this.idTreeMap)) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal Map<E, Tree<E>> eTreeMap = MapUtil.sortByValue(this.idTreeMap, false);\n\t\tE parentId;\n\t\tfor (Tree<E> node : eTreeMap.values()) {\n\t\t\tif (null == node) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparentId = node.getParentId();\n\t\t\tif (ObjectUtil.equals(this.root.getId(), parentId)) {\n\t\t\t\tthis.root.addChildren(node);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfinal Tree<E> parentNode = eTreeMap.get(parentId);\n\t\t\tif (null != parentNode) {\n\t\t\t\tparentNode.addChildren(node);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 树剪枝\n\t */\n\tprivate void cutTree() {\n\t\tfinal TreeNodeConfig config = this.root.getConfig();\n\t\tfinal Integer deep = config.getDeep();\n\t\tif (null == deep || deep < 0) {\n\t\t\treturn;\n\t\t}\n\t\tcutTree(this.root, 0, deep);\n\t}\n\n\t/**\n\t * 树剪枝叶\n\t *\n\t * @param tree        节点\n\t * @param currentDepp 当前层级\n\t * @param maxDeep     最大层级\n\t */\n\tprivate void cutTree(Tree<E> tree, int currentDepp, int maxDeep) {\n\t\tif (null == tree) {\n\t\t\treturn;\n\t\t}\n\t\tif (currentDepp == maxDeep) {\n\t\t\t// 剪枝\n\t\t\ttree.setChildren(null);\n\t\t\treturn;\n\t\t}\n\n\t\tfinal List<Tree<E>> children = tree.getChildren();\n\t\tif (CollUtil.isNotEmpty(children)) {\n\t\t\tfor (Tree<E> child : children) {\n\t\t\t\tcutTree(child, currentDepp + 1, maxDeep);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 检查是否已经构建\n\t */\n\tprivate void checkBuilt() {\n\t\tAssert.isFalse(isBuild, \"Current tree has been built.\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeNode.java",
    "content": "package cn.hutool.core.lang.tree;\n\n\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * 树节点 每个属性都可以在{@link TreeNodeConfig}中被重命名<br>\n * 在你的项目里它可以是部门实体、地区实体等任意类树节点实体\n * 类树节点实体: 包含key，父Key.不限于这些属性的可以构造成一颗树的实体对象\n *\n * @param <T> ID类型\n * @author liangbaikai\n */\npublic class TreeNode<T> implements Node<T> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * ID\n\t */\n\tprivate T id;\n\n\t/**\n\t * 父节点ID\n\t */\n\tprivate T parentId;\n\n\t/**\n\t * 名称\n\t */\n\tprivate CharSequence name;\n\n\t/**\n\t * 顺序 越小优先级越高 默认0\n\t */\n\tprivate Comparable<?> weight = 0;\n\n\t/**\n\t * 扩展字段\n\t */\n\tprivate Map<String, Object> extra;\n\n\n\t/**\n\t * 空构造\n\t */\n\tpublic TreeNode() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param id       ID\n\t * @param parentId 父节点ID\n\t * @param name     名称\n\t * @param weight   权重\n\t */\n\tpublic TreeNode(T id, T parentId, String name, Comparable<?> weight) {\n\t\tthis.id = id;\n\t\tthis.parentId = parentId;\n\t\tthis.name = name;\n\t\tif (weight != null) {\n\t\t\tthis.weight = weight;\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic T getId() {\n\t\treturn id;\n\t}\n\n\t@Override\n\tpublic TreeNode<T> setId(T id) {\n\t\tthis.id = id;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic T getParentId() {\n\t\treturn this.parentId;\n\t}\n\n\t@Override\n\tpublic TreeNode<T> setParentId(T parentId) {\n\t\tthis.parentId = parentId;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic CharSequence getName() {\n\t\treturn name;\n\t}\n\n\t@Override\n\tpublic TreeNode<T> setName(CharSequence name) {\n\t\tthis.name = name;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Comparable<?> getWeight() {\n\t\treturn weight;\n\t}\n\n\t@Override\n\tpublic TreeNode<T> setWeight(Comparable<?> weight) {\n\t\tthis.weight = weight;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取扩展字段\n\t *\n\t * @return 扩展字段Map\n\t * @since 5.2.5\n\t */\n\tpublic Map<String, Object> getExtra() {\n\t\treturn extra;\n\t}\n\n\t/**\n\t * 设置扩展字段\n\t *\n\t * @param extra 扩展字段\n\t * @return this\n\t * @since 5.2.5\n\t */\n\tpublic TreeNode<T> setExtra(Map<String, Object> extra) {\n\t\tthis.extra = extra;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tTreeNode<?> treeNode = (TreeNode<?>) o;\n\t\treturn Objects.equals(id, treeNode.id);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(id);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeNodeConfig.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport java.io.Serializable;\n\n/**\n * 树配置属性相关\n *\n * @author liangbaikai\n */\npublic class TreeNodeConfig implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 默认属性配置对象\n\t */\n\tpublic static TreeNodeConfig DEFAULT_CONFIG = new TreeNodeConfig();\n\n\t// 属性名配置字段\n\tprivate String idKey = \"id\";\n\tprivate String parentIdKey = \"parentId\";\n\tprivate String weightKey = \"weight\";\n\tprivate String nameKey = \"name\";\n\tprivate String childrenKey = \"children\";\n\t// 可以配置递归深度 从0开始计算 默认此配置为空,即不限制\n\tprivate Integer deep;\n\n\n\t/**\n\t * 获取ID对应的名称\n\t *\n\t * @return ID对应的名称\n\t */\n\tpublic String getIdKey() {\n\t\treturn this.idKey;\n\t}\n\n\t/**\n\t * 设置ID对应的名称\n\t *\n\t * @param idKey ID对应的名称\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setIdKey(String idKey) {\n\t\tthis.idKey = idKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取权重对应的名称\n\t *\n\t * @return 权重对应的名称\n\t */\n\tpublic String getWeightKey() {\n\t\treturn this.weightKey;\n\t}\n\n\t/**\n\t * 设置权重对应的名称\n\t *\n\t * @param weightKey 权重对应的名称\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setWeightKey(String weightKey) {\n\t\tthis.weightKey = weightKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取节点名对应的名称\n\t *\n\t * @return 节点名对应的名称\n\t */\n\tpublic String getNameKey() {\n\t\treturn this.nameKey;\n\t}\n\n\t/**\n\t * 设置节点名对应的名称\n\t *\n\t * @param nameKey 节点名对应的名称\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setNameKey(String nameKey) {\n\t\tthis.nameKey = nameKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取子点对应的名称\n\t *\n\t * @return 子点对应的名称\n\t */\n\tpublic String getChildrenKey() {\n\t\treturn this.childrenKey;\n\t}\n\n\t/**\n\t * 设置子点对应的名称\n\t *\n\t * @param childrenKey 子点对应的名称\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setChildrenKey(String childrenKey) {\n\t\tthis.childrenKey = childrenKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取父节点ID对应的名称\n\t *\n\t * @return 父点对应的名称\n\t */\n\tpublic String getParentIdKey() {\n\t\treturn this.parentIdKey;\n\t}\n\n\n\t/**\n\t * 设置父点对应的名称\n\t *\n\t * @param parentIdKey 父点对应的名称\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setParentIdKey(String parentIdKey) {\n\t\tthis.parentIdKey = parentIdKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取递归深度\n\t *\n\t * @return 递归深度\n\t */\n\tpublic Integer getDeep() {\n\t\treturn this.deep;\n\t}\n\n\t/**\n\t * 设置递归深度\n\t *\n\t * @param deep 递归深度\n\t * @return this\n\t */\n\tpublic TreeNodeConfig setDeep(Integer deep) {\n\t\tthis.deep = deep;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeUtil.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.lang.tree.parser.DefaultNodeParser;\nimport cn.hutool.core.lang.tree.parser.NodeParser;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\n/**\n * 树工具类\n *\n * @author liangbaikai\n */\npublic class TreeUtil {\n\n\t/**\n\t * 构建单root节点树\n\t *\n\t * @param list 源数据集合\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static Tree<Integer> buildSingle(List<TreeNode<Integer>> list) {\n\t\treturn buildSingle(list, 0);\n\t}\n\n\t/**\n\t * 树构建\n\t *\n\t * @param list 源数据集合\n\t * @return List\n\t */\n\tpublic static List<Tree<Integer>> build(List<TreeNode<Integer>> list) {\n\t\treturn build(list, 0);\n\t}\n\n\t/**\n\t * 构建单root节点树<br>\n\t * 它会生成一个以指定ID为ID的空的节点，然后逐级增加子节点。\n\t *\n\t * @param <E>      ID类型\n\t * @param list     源数据集合\n\t * @param parentId 最顶层父id值 一般为 0 之类\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static <E> Tree<E> buildSingle(List<TreeNode<E>> list, E parentId) {\n\t\treturn buildSingle(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, new DefaultNodeParser<>());\n\t}\n\n\t/**\n\t * 树构建\n\t *\n\t * @param <E>      ID类型\n\t * @param list     源数据集合\n\t * @param parentId 最顶层父id值 一般为 0 之类\n\t * @return List\n\t */\n\tpublic static <E> List<Tree<E>> build(List<TreeNode<E>> list, E parentId) {\n\t\treturn build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, new DefaultNodeParser<>());\n\t}\n\n\t/**\n\t * 构建单root节点树<br>\n\t * 它会生成一个以指定ID为ID的空的节点，然后逐级增加子节点。\n\t *\n\t * @param <T>        转换的实体 为数据源里的对象类型\n\t * @param <E>        ID类型\n\t * @param list       源数据集合\n\t * @param parentId   最顶层父id值 一般为 0 之类\n\t * @param nodeParser 转换器\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static <T, E> Tree<E> buildSingle(List<T> list, E parentId, NodeParser<T, E> nodeParser) {\n\t\treturn buildSingle(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);\n\t}\n\n\t/**\n\t * 树构建\n\t *\n\t * @param <T>        转换的实体 为数据源里的对象类型\n\t * @param <E>        ID类型\n\t * @param list       源数据集合\n\t * @param parentId   最顶层父id值 一般为 0 之类\n\t * @param nodeParser 转换器\n\t * @return List\n\t */\n\tpublic static <T, E> List<Tree<E>> build(List<T> list, E parentId, NodeParser<T, E> nodeParser) {\n\t\treturn build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser);\n\t}\n\n\t/**\n\t * 树构建\n\t *\n\t * @param <T>            转换的实体 为数据源里的对象类型\n\t * @param <E>            ID类型\n\t * @param list           源数据集合\n\t * @param rootId         最顶层父id值 一般为 0 之类\n\t * @param treeNodeConfig 配置\n\t * @param nodeParser     转换器\n\t * @return List\n\t */\n\tpublic static <T, E> List<Tree<E>> build(List<T> list, E rootId, TreeNodeConfig treeNodeConfig, NodeParser<T, E> nodeParser) {\n\t\treturn buildSingle(list, rootId, treeNodeConfig, nodeParser).getChildren();\n\t}\n\n\t/**\n\t * 构建单root节点树<br>\n\t * 它会生成一个以指定ID为ID的空的节点，然后逐级增加子节点。\n\t *\n\t * @param <T>            转换的实体 为数据源里的对象类型\n\t * @param <E>            ID类型\n\t * @param list           源数据集合\n\t * @param rootId         最顶层父id值 一般为 0 之类\n\t * @param treeNodeConfig 配置\n\t * @param nodeParser     转换器\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static <T, E> Tree<E> buildSingle(List<T> list, E rootId, TreeNodeConfig treeNodeConfig, NodeParser<T, E> nodeParser) {\n\t\treturn TreeBuilder.of(rootId, treeNodeConfig)\n\t\t\t\t.append(list, nodeParser).build();\n\t}\n\n\t/**\n\t * 树构建，按照权重排序\n\t *\n\t * @param <E>    ID类型\n\t * @param map    源数据Map\n\t * @param rootId 最顶层父id值 一般为 0 之类\n\t * @return List\n\t * @since 5.6.7\n\t */\n\tpublic static <E> List<Tree<E>> build(Map<E, Tree<E>> map, E rootId) {\n\t\treturn buildSingle(map, rootId).getChildren();\n\t}\n\n\t/**\n\t * 单点树构建，按照权重排序<br>\n\t * 它会生成一个以指定ID为ID的空的节点，然后逐级增加子节点。\n\t *\n\t * @param <E>    ID类型\n\t * @param map    源数据Map\n\t * @param rootId 根节点id值 一般为 0 之类\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static <E> Tree<E> buildSingle(Map<E, Tree<E>> map, E rootId) {\n\t\tfinal Tree<E> tree = IterUtil.getFirstNoneNull(map.values());\n\t\tif (null != tree) {\n\t\t\tfinal TreeNodeConfig config = tree.getConfig();\n\t\t\treturn TreeBuilder.of(rootId, config)\n\t\t\t\t\t.append(map)\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn createEmptyNode(rootId);\n\t}\n\n\t/**\n\t * 获取ID对应的节点，如果有多个ID相同的节点，只返回第一个。<br>\n\t * 此方法只查找此节点及子节点，采用递归深度优先遍历。\n\t *\n\t * @param <T>  ID类型\n\t * @param node 节点\n\t * @param id   ID\n\t * @return 节点\n\t * @since 5.2.4\n\t */\n\tpublic static <T> Tree<T> getNode(Tree<T> node, T id) {\n\t\tif (ObjectUtil.equal(id, node.getId())) {\n\t\t\treturn node;\n\t\t}\n\n\t\tfinal List<Tree<T>> children = node.getChildren();\n\t\tif (null == children) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 查找子节点\n\t\tTree<T> childNode;\n\t\tfor (Tree<T> child : children) {\n\t\t\tchildNode = child.getNode(id);\n\t\t\tif (null != childNode) {\n\t\t\t\treturn childNode;\n\t\t\t}\n\t\t}\n\n\t\t// 未找到节点\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取所有父节点名称列表\n\t *\n\t * <p>\n\t * 比如有个人在研发1部，他上面有研发部，接着上面有技术中心<br>\n\t * 返回结果就是：[研发一部, 研发中心, 技术中心]\n\t *\n\t * @param <T>                节点ID类型\n\t * @param node               节点\n\t * @param includeCurrentNode 是否包含当前节点的名称\n\t * @return 所有父节点名称列表，node为null返回空List\n\t * @since 5.2.4\n\t */\n\tpublic static <T> List<CharSequence> getParentsName(Tree<T> node, boolean includeCurrentNode) {\n\t\tfinal List<CharSequence> result = new ArrayList<>();\n\t\tif (null == node) {\n\t\t\treturn result;\n\t\t}\n\n\t\tif (includeCurrentNode) {\n\t\t\tresult.add(node.getName());\n\t\t}\n\n\t\tTree<T> parent = node.getParent();\n\t\tCharSequence name;\n\t\twhile (null != parent) {\n\t\t\tname = parent.getName();\n\t\t\tparent = parent.getParent();\n\t\t\tif(null != name || null != parent){\n\t\t\t\t// issue#I795IN，根节点的null不加入\n\t\t\t\tresult.add(name);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t *  获取所有父节点ID列表\n\t *\n\t * <p>\n\t * 比如有个人在研发1部，他上面有研发部，接着上面有技术中心<br>\n\t * 返回结果就是：[研发部, 技术中心]\n\t *\n\t * @param <T>                节点ID类型\n\t * @param node               节点\n\t * @param includeCurrentNode 是否包含当前节点的名称\n\t * @return 所有父节点ID列表，node为null返回空List\n\t * @since 5.8.22\n\t */\n\tpublic static <T> List<T> getParentsId(Tree<T> node, boolean includeCurrentNode) {\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tif (null == node) {\n\t\t\treturn result;\n\t\t}\n\n\t\tif (includeCurrentNode) {\n\t\t\tresult.add(node.getId());\n\t\t}\n\n\t\tTree<T> parent = node.getParent();\n\t\tT id;\n\t\twhile (null != parent) {\n\t\t\tid = parent.getId();\n\t\t\tparent = parent.getParent();\n\t\t\tif(null != id || null != parent){\n\t\t\t\t// issue#I795IN，根节点的null不加入\n\t\t\t\tresult.add(id);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 创建空Tree的节点\n\t *\n\t * @param id  节点ID\n\t * @param <E> 节点ID类型\n\t * @return {@link Tree}\n\t * @since 5.7.2\n\t */\n\tpublic static <E> Tree<E> createEmptyNode(E id) {\n\t\treturn new Tree<E>().setId(id);\n\t}\n\n\t/**\n\t * 函数式构建树状结构(无需继承Tree类)\n\t *\n\t * @param nodes\t\t\t需要构建树集合\n\t * @param rootId\t\t根节点ID\n\t * @param idFunc\t\t获取节点ID函数\n\t * @param parentIdFunc\t获取节点父ID函数\n\t * @param setChildFunc\t设置孩子集合函数\n\t * @param <T>\t\t\t节点ID类型\n\t * @param <E>\t\t\t节点类型\n\t * @return List\n\t */\n\tpublic static <T, E> List<E> build(List<E> nodes, T rootId,\n\t\t\t\t\t\t\t\t\t   Function<E, T> idFunc,\n\t\t\t\t\t\t\t\t\t   Function<E, T> parentIdFunc,\n\t\t\t\t\t\t\t\t\t   BiConsumer<E, List<E>> setChildFunc) {\n\t\tList<E> rootList = nodes.stream().filter(tree -> parentIdFunc.apply(tree).equals(rootId)).collect(Collectors.toList());\n\t\tSet<T> filterOperated = new HashSet<>(rootList.size() + nodes.size());\n\t\t//对每个根节点都封装它的孩子节点\n\t\trootList.forEach(root -> setChildren(root, nodes, filterOperated, idFunc, parentIdFunc, setChildFunc));\n\t\treturn rootList;\n\t}\n\n\t/**\n\t * 封装孩子节点\n\t *\n\t * @param root\t\t\t\t根节点\n\t * @param nodes\t\t\t\t节点集合\n\t * @param filterOperated\t过滤操作Map\n\t * @param idFunc\t\t\t获取节点ID函数\n\t * @param parentIdFunc\t\t获取节点父ID函数\n\t * @param setChildFunc\t\t设置孩子集合函数\n\t * @param <T>\t\t\t\t节点ID类型\n\t * @param <E>\t\t\t\t节点类型\n\t */\n\tprivate static <T, E> void setChildren(E root, List<E> nodes, Set<T> filterOperated, Function<E, T> idFunc, Function<E, T> parentIdFunc, BiConsumer<E, List<E>> setChildFunc) {\n\t\tList<E> children = new ArrayList<>();\n\t\tnodes.stream()\n\t\t\t//过滤出未操作过的节点\n\t\t\t.filter(body -> !filterOperated.contains(idFunc.apply(body)))\n\t\t\t//过滤出孩子节点\n\t\t\t.filter(body -> Objects.equals(idFunc.apply(root), parentIdFunc.apply(body)))\n\t\t\t.forEach(body -> {\n\t\t\t\tfilterOperated.add(idFunc.apply(body));\n\t\t\t\tchildren.add(body);\n\t\t\t\t//递归 对每个孩子节点执行同样操作\n\t\t\t\tsetChildren(body, nodes, filterOperated, idFunc, parentIdFunc, setChildFunc);\n\t\t\t});\n\t\tsetChildFunc.accept(root, children);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/package-info.java",
    "content": "/**\n * 提供通用树生成，特点：<p>\n * 1、每个字段可自定义<br>\n * 2、支持排序 树深度配置,自定义转换器等<br>\n * 3、支持额外属性扩展<br>\n * 4、贴心 许多属性,特性都有默认值处理<br>\n * 5、使用简单 可一行代码生成树<br>\n * 6、代码简洁轻量无额外依赖\n * </p>\n *\n * @author liangbaikai（https://gitee.com/liangbaikai00/）\n * @since 5.2.1\n */\npackage cn.hutool.core.lang.tree;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/parser/DefaultNodeParser.java",
    "content": "package cn.hutool.core.lang.tree.parser;\n\nimport cn.hutool.core.lang.tree.TreeNode;\nimport cn.hutool.core.lang.tree.Tree;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.util.Map;\n\n/**\n * 默认的简单转换器\n *\n * @param <T> ID类型\n * @author liangbaikai\n */\npublic class DefaultNodeParser<T> implements NodeParser<TreeNode<T>, T> {\n\n\t@Override\n\tpublic void parse(TreeNode<T> treeNode, Tree<T> tree) {\n\t\ttree.setId(treeNode.getId());\n\t\ttree.setParentId(treeNode.getParentId());\n\t\ttree.setWeight(treeNode.getWeight());\n\t\ttree.setName(treeNode.getName());\n\n\t\t//扩展字段\n\t\tfinal Map<String, Object> extra = treeNode.getExtra();\n\t\tif(MapUtil.isNotEmpty(extra)){\n\t\t\textra.forEach(tree::putExtra);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/lang/tree/parser/NodeParser.java",
    "content": "package cn.hutool.core.lang.tree.parser;\n\nimport cn.hutool.core.lang.tree.Tree;\n\n/**\n * 树节点解析器 可以参考{@link DefaultNodeParser}\n *\n * @param <T> 转换的实体 为数据源里的对象类型\n * @author liangbaikai\n */\n@FunctionalInterface\npublic interface NodeParser<T, E> {\n\t/**\n\t * @param object   源数据实体\n\t * @param treeNode 树节点实体\n\t */\n\tvoid parse(T object, Tree<E> treeNode);\n}\n\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/AbsEntry.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Map;\n\n/**\n * 抽象的{@link Map.Entry}实现，来自Guava<br>\n * 实现了默认的{@link #equals(Object)}、{@link #hashCode()}、{@link #toString()}方法。<br>\n * 默认{@link #setValue(Object)}抛出异常。\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Guava\n * @since 5.7.23\n */\npublic abstract class AbsEntry<K, V> implements Map.Entry<K, V> {\n\n\t@Override\n\tpublic V setValue(V value) {\n\t\tthrow new UnsupportedOperationException(\"Entry is read only.\");\n\t}\n\n\t@Override\n\tpublic boolean equals(Object object) {\n\t\tif (object instanceof Map.Entry) {\n\t\t\tfinal Map.Entry<?, ?> that = (Map.Entry<?, ?>) object;\n\t\t\treturn ObjectUtil.equals(this.getKey(), that.getKey())\n\t\t\t\t\t&& ObjectUtil.equals(this.getValue(), that.getValue());\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\t//copy from 1.8 HashMap.Node\n\t\tK k = getKey();\n\t\tV v = getValue();\n\t\treturn ((k == null) ? 0 : k.hashCode()) ^ ((v == null) ? 0 : v.hashCode());\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getKey() + \"=\" + getValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/BiMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\n/**\n * 双向Map<br>\n * 互换键值对不检查值是否有重复，如果有则后加入的元素替换先加入的元素<br>\n * 值的顺序在HashMap中不确定，所以谁覆盖谁也不确定，在有序的Map中按照先后顺序覆盖，保留最后的值<br>\n * 它与TableMap的区别是，BiMap维护两个Map实现高效的正向和反向查找\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 5.2.6\n */\npublic class BiMap<K, V> extends MapWrapper<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Map<V, K> inverse;\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw 被包装的Map\n\t */\n\tpublic BiMap(Map<K, V> raw) {\n\t\tsuper(raw);\n\t}\n\n\t@Override\n\tpublic V put(K key, V value) {\n\t\tfinal V oldValue = super.put(key, value);\n\t\tif (null != this.inverse) {\n\t\t\tif(null != oldValue){\n\t\t\t\t// issue#I88R5M\n\t\t\t\t// 如果put的key相同，value不同，需要在inverse中移除旧的关联\n\t\t\t\tthis.inverse.remove(oldValue);\n\t\t\t}\n\t\t\tthis.inverse.put(value, key);\n\t\t}\n\t\treturn oldValue;\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends K, ? extends V> m) {\n\t\tsuper.putAll(m);\n\t\tif (null != this.inverse) {\n\t\t\tm.forEach((key, value) -> this.inverse.put(value, key));\n\t\t}\n\t}\n\n\t@Override\n\tpublic V remove(Object key) {\n\t\tfinal V v = super.remove(key);\n\t\tif (null != this.inverse && null != v) {\n\t\t\tthis.inverse.remove(v);\n\t\t}\n\t\treturn v;\n\t}\n\n\t@Override\n\tpublic boolean remove(Object key, Object value) {\n\t\treturn super.remove(key, value) && null != this.inverse && this.inverse.remove(value, key);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tsuper.clear();\n\t\tthis.inverse = null;\n\t}\n\n\t/**\n\t * 获取反向Map\n\t *\n\t * @return 反向Map\n\t */\n\tpublic Map<V, K> getInverse() {\n\t\tif (null == this.inverse) {\n\t\t\tinverse = MapUtil.inverse(getRaw());\n\t\t}\n\t\treturn this.inverse;\n\t}\n\n\t/**\n\t * 根据值获得键\n\t *\n\t * @param value 值\n\t * @return 键\n\t */\n\tpublic K getKey(V value) {\n\t\treturn getInverse().get(value);\n\t}\n\n\t@Override\n\tpublic V putIfAbsent(K key, V value) {\n\t\tfinal V oldValue = super.putIfAbsent(key, value);\n\t\t// 只有当oldValue为null时(即key之前不存在),才更新反向Map\n\t\tif (null == oldValue && null != this.inverse) {\n\t\t\tthis.inverse.put(value, key);\n\t\t}\n\t\treturn oldValue;\n\t}\n\n\t@Override\n\tpublic V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {\n\t\tfinal V result = super.computeIfAbsent(key, mappingFunction);\n\t\tresetInverseMap();\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tfinal V result = super.computeIfPresent(key, remappingFunction);\n\t\tresetInverseMap();\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tfinal V result = super.compute(key, remappingFunction);\n\t\tresetInverseMap();\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {\n\t\tfinal V result = super.merge(key, value, remappingFunction);\n\t\tresetInverseMap();\n\t\treturn result;\n\t}\n\n\t/**\n\t * 重置反转的Map，如果反转map为空，则不操作。\n\t */\n\tprivate void resetInverseMap() {\n\t\tif (null != this.inverse) {\n\t\t\tinverse = null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 驼峰Key风格的LinkedHashMap<br>\n * 对KEY转换为驼峰，get(\"int_value\")和get(\"intValue\")获得的值相同，put进入的值也会被覆盖\n *\n * @author Looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 4.0.7\n */\npublic class CamelCaseLinkedMap<K, V> extends CamelCaseMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic CamelCaseLinkedMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic CamelCaseLinkedMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic CamelCaseLinkedMap(Map<? extends K, ? extends V> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m Map，数据会被默认拷贝到一个新的LinkedHashMap中\n\t */\n\tpublic CamelCaseLinkedMap(float loadFactor, Map<? extends K, ? extends V> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子\n\t */\n\tpublic CamelCaseLinkedMap(int initialCapacity, float loadFactor) {\n\t\tsuper(new LinkedHashMap<>(initialCapacity, loadFactor));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * 驼峰Key风格的Map<br>\n * 对KEY转换为驼峰，get(\"int_value\")和get(\"intValue\")获得的值相同，put进入的值也会被覆盖\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 4.0.7\n */\npublic class CamelCaseMap<K, V> extends FuncKeyMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic CamelCaseMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic CamelCaseMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic CamelCaseMap(Map<? extends K, ? extends V> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m          初始Map，数据会被默认拷贝到一个新的HashMap中\n\t */\n\tpublic CamelCaseMap(float loadFactor, Map<? extends K, ? extends V> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor      加载因子\n\t */\n\tpublic CamelCaseMap(int initialCapacity, float loadFactor) {\n\t\tthis(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 注意此构造将传入的Map作为被包装的Map，针对任何修改，传入的Map都会被同样修改。\n\t *\n\t * @param emptyMapBuilder Map构造器，必须构造空的Map\n\t */\n\tCamelCaseMap(MapBuilder<K, V> emptyMapBuilder) {\n\t\t// issue#I5VRHW@Gitee 使Function可以被序列化\n\t\tsuper(emptyMapBuilder.build(), (Function<Object, K> & Serializable)(key) -> {\n\t\t\tif (key instanceof CharSequence) {\n\t\t\t\tkey = StrUtil.toCamelCase(key.toString());\n\t\t\t}\n\t\t\t//noinspection unchecked\n\t\t\treturn (K) key;\n\t\t});\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 忽略大小写的LinkedHashMap<br>\n * 对KEY忽略大小写，get(\"Value\")和get(\"value\")获得的值相同，put进入的值也会被覆盖\n *\n * @author Looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 3.3.1\n */\npublic class CaseInsensitiveLinkedMap<K, V> extends CaseInsensitiveMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic CaseInsensitiveLinkedMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic CaseInsensitiveLinkedMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic CaseInsensitiveLinkedMap(Map<? extends K, ? extends V> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m Map\n\t * @since 3.1.2\n\t */\n\tpublic CaseInsensitiveLinkedMap(float loadFactor, Map<? extends K, ? extends V> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子\n\t */\n\tpublic CaseInsensitiveLinkedMap(int initialCapacity, float loadFactor) {\n\t\tsuper(MapBuilder.create(new LinkedHashMap<>(initialCapacity, loadFactor)));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.io.Serializable;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * 忽略大小写的Map<br>\n * 对KEY忽略大小写，get(\"Value\")和get(\"value\")获得的值相同，put进入的值也会被覆盖\n *\n * @author Looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 3.0.2\n */\npublic class CaseInsensitiveMap<K, V> extends FuncKeyMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t//------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic CaseInsensitiveMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic CaseInsensitiveMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 注意此构造将传入的Map所有值复制到当前map中，不修改传入map。\n\t *\n\t * @param m 被包装的自定义Map创建器\n\t */\n\tpublic CaseInsensitiveMap(Map<? extends K, ? extends V> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m Map\n\t * @since 3.1.2\n\t */\n\tpublic CaseInsensitiveMap(float loadFactor, Map<? extends K, ? extends V> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子\n\t */\n\tpublic CaseInsensitiveMap(int initialCapacity, float loadFactor) {\n\t\tthis(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 注意此构造将传入的Map作为被包装的Map，针对任何修改，传入的Map都会被同样修改。\n\t *\n\t * @param emptyMapBuilder 被包装的自定义Map创建器\n\t */\n\tCaseInsensitiveMap(MapBuilder<K, V> emptyMapBuilder) {\n\t\t// issue#I5VRHW@Gitee 使Function可以被序列化\n\t\tsuper(emptyMapBuilder.build(), (Function<Object, K> & Serializable)(key)->{\n\t\t\tif (key instanceof CharSequence) {\n\t\t\t\tkey = key.toString().toLowerCase();\n\t\t\t}\n\t\t\t//noinspection unchecked\n\t\t\treturn (K) key;\n\t\t});\n\t}\n\t//------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveTreeMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n/**\n * 忽略大小写的{@link TreeMap}<br>\n * 对KEY忽略大小写，get(\"Value\")和get(\"value\")获得的值相同，put进入的值也会被覆盖\n *\n * @author Looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 3.3.1\n */\npublic class CaseInsensitiveTreeMap<K, V> extends CaseInsensitiveMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic CaseInsensitiveTreeMap() {\n\t\tthis((Comparator<? super K>) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t * @since 3.1.2\n\t */\n\tpublic CaseInsensitiveTreeMap(Map<? extends K, ? extends V> m) {\n\t\tthis();\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map，初始Map，键值对会被复制到新的TreeMap中\n\t * @since 3.1.2\n\t */\n\tpublic CaseInsensitiveTreeMap(SortedMap<? extends K, ? extends V> m) {\n\t\tsuper(MapBuilder.create(new TreeMap<K, V>(m)));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param comparator 比较器，{@code null}表示使用默认比较器\n\t */\n\tpublic CaseInsensitiveTreeMap(Comparator<? super K> comparator) {\n\t\tsuper(MapBuilder.create(new TreeMap<>(comparator)));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Map;\n\n/**\n * 自定义键的Map，默认HashMap实现\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 4.0.7\n */\npublic abstract class CustomKeyMap<K, V> extends TransMap<K, V> {\n\tprivate static final long serialVersionUID = 4043263744224569870L;\n\n\t/**\n\t * 构造<br>\n\t * 通过传入一个Map从而确定Map的类型，子类需创建一个空的Map，而非传入一个已有Map，否则值可能会被修改\n\t *\n\t * @param emptyMap Map 被包装的Map，必须为空Map，否则自定义key会无效\n\t * @since 3.1.2\n\t */\n\tpublic CustomKeyMap(Map<K, V> emptyMap) {\n\t\tsuper(emptyMap);\n\t}\n\n\t@Override\n\tprotected V customValue(Object value) {\n\t\t//noinspection unchecked\n\t\treturn (V)value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * 固定大小的{@link LinkedHashMap} 实现<br>\n * 注意此类非线程安全，由于{@link #get(Object)}操作会修改链表的顺序结构，因此也不可以使用读写锁。\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n */\npublic class FixedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {\n\tprivate static final long serialVersionUID = -629171177321416095L;\n\n\t/**\n\t * 容量，超过此容量自动删除末尾元素\n\t */\n\tprivate int capacity;\n\t/**\n\t * 移除监听\n\t */\n\tprivate Consumer<Map.Entry<K, V>> removeListener;\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 容量，实际初始容量比容量大1\n\t */\n\tpublic FixedLinkedHashMap(int capacity) {\n\t\tsuper(capacity + 1, 1.0f, true);\n\t\tthis.capacity = capacity;\n\t}\n\n\t/**\n\t * 获取容量\n\t *\n\t * @return 容量\n\t */\n\tpublic int getCapacity() {\n\t\treturn this.capacity;\n\t}\n\n\t/**\n\t * 设置容量\n\t *\n\t * @param capacity 容量\n\t */\n\tpublic void setCapacity(int capacity) {\n\t\tthis.capacity = capacity;\n\t}\n\n\t/**\n\t * 设置自定义移除监听\n\t *\n\t * @param removeListener 移除监听\n\t */\n\tpublic void setRemoveListener(final Consumer<Map.Entry<K, V>> removeListener) {\n\t\tthis.removeListener = removeListener;\n\t}\n\n\t@Override\n\tprotected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {\n\t\t//当链表元素大于容量时，移除最老（最久未被使用）的元素\n\t\tif (size() > this.capacity) {\n\t\t\tif (null != removeListener) {\n\t\t\t\t// 自定义监听\n\t\t\t\tremoveListener.accept(eldest);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/ForestMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\n\n/**\n * 基于多个{@link TreeEntry}构成的、彼此平行的树结构构成的森林集合。\n *\n * @param <K> key类型\n * @param <V> value类型\n * @author huangchengxing\n * @see TreeEntry\n */\npublic interface ForestMap<K, V> extends Map<K, TreeEntry<K, V>> {\n\n\t// ===================== Map接口方法的重定义 =====================\n\n\t/**\n\t * 添加一个节点，效果等同于 {@code putNode(key, node.getValue())}\n\t * <ul>\n\t *     <li>若key对应节点不存在，则以传入的键值创建一个新的节点；</li>\n\t *     <li>若key对应节点存在，则将该节点的值替换为{@code node}指定的值；</li>\n\t * </ul>\n\t *\n\t * @param key  节点的key值\n\t * @param node 节点\n\t * @return 节点，若key已有对应节点，则返回具有旧值的节点，否则返回null\n\t * @see #putNode(Object, Object)\n\t */\n\t@Override\n\tdefault TreeEntry<K, V> put(K key, TreeEntry<K, V> node) {\n\t\treturn putNode(key, node.getValue());\n\t}\n\n\t/**\n\t * 批量添加节点，若节点具有父节点或者子节点，则一并在当前实例中引入该关系\n\t *\n\t * @param treeEntryMap 节点集合\n\t */\n\t@Override\n\tdefault void putAll(Map<? extends K, ? extends TreeEntry<K, V>> treeEntryMap) {\n\t\tif (CollUtil.isEmpty(treeEntryMap)) {\n\t\t\treturn;\n\t\t}\n\t\ttreeEntryMap.forEach((k, v) -> {\n\t\t\tif (v.hasParent()) {\n\t\t\t\tfinal TreeEntry<K, V> parent = v.getDeclaredParent();\n\t\t\t\tputLinkedNodes(parent.getKey(), parent.getValue(), v.getKey(), v.getValue());\n\t\t\t} else {\n\t\t\t\tputNode(v.getKey(), v.getValue());\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 将指定节点从当前{@link Map}中删除\n\t * <ul>\n\t *     <li>若存在父节点或子节点，则将其断开其与父节点或子节点的引用关系；</li>\n\t *     <li>\n\t *         若同时存在父节点或子节点，则会在删除后将让子节点直接成为父节点的子节点，比如：<br>\n\t *         现有引用关系 a -&gt; b -&gt; c，删除 b 后，将有 a -&gt; c\n\t *     </li>\n\t * </ul>\n\t *\n\t * @param key 节点的key\n\t * @return 删除的节点，若key没有对应节点，则返回null\n\t */\n\t@Override\n\tTreeEntry<K, V> remove(Object key);\n\n\t/**\n\t * 将当前集合清空，并清除全部节点间的引用关系\n\t */\n\t@Override\n\tvoid clear();\n\n\t// ===================== 节点操作 =====================\n\n\t/**\n\t * 批量添加节点\n\t *\n\t * @param <C>                集合类型\n\t * @param values             要添加的值\n\t * @param keyGenerator       从值中获取key的方法\n\t * @param parentKeyGenerator 从值中获取父节点key的方法\n\t * @param ignoreNullNode     是否获取到的key为null的子节点/父节点\n\t */\n\tdefault <C extends Collection<V>> void putAllNode(\n\t\t\tC values, Function<V, K> keyGenerator, Function<V, K> parentKeyGenerator, boolean ignoreNullNode) {\n\t\tif (CollUtil.isEmpty(values)) {\n\t\t\treturn;\n\t\t}\n\t\tvalues.forEach(v -> {\n\t\t\tfinal K key = keyGenerator.apply(v);\n\t\t\tfinal K parentKey = parentKeyGenerator.apply(v);\n\n\t\t\t// 不忽略keu为null节点\n\t\t\tfinal boolean hasKey = ObjectUtil.isNotNull(key);\n\t\t\tfinal boolean hasParentKey = ObjectUtil.isNotNull(parentKey);\n\t\t\tif (!ignoreNullNode || (hasKey && hasParentKey)) {\n\t\t\t\tlinkNodes(parentKey, key);\n\t\t\t\tget(key).setValue(v);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 父子节点的key都为null\n\t\t\tif (!hasKey && !hasParentKey) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 父节点key为null\n\t\t\tif (hasKey) {\n\t\t\t\tputNode(key, v);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 子节点key为null\n\t\t\tputNode(parentKey, null);\n\t\t});\n\t}\n\n\t/**\n\t * 添加一个节点\n\t * <ul>\n\t *     <li>若key对应节点不存在，则以传入的键值创建一个新的节点；</li>\n\t *     <li>若key对应节点存在，则将该节点的值替换为{@code node}指定的值；</li>\n\t * </ul>\n\t *\n\t * @param key   节点的key\n\t * @param value 节点的value\n\t * @return 节点，若key已有对应节点，则返回具有旧值的节点，否则返回null\n\t */\n\tTreeEntry<K, V> putNode(K key, V value);\n\n\t/**\n\t * 同时添加父子节点：\n\t * <ul>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点不存在，则会根据键值创建一个对应的节点；</li>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点存在，则会更新对应节点的值；</li>\n\t * </ul>\n\t * 该操作等同于：\n\t * <pre>{@code\n\t *     putNode(parentKey, parentValue);\n\t *     putNode(childKey, childValue);\n\t *     linkNodes(parentKey, childKey);\n\t * }</pre>\n\t *\n\t * @param parentKey   父节点的key\n\t * @param parentValue 父节点的value\n\t * @param childKey    子节点的key\n\t * @param childValue  子节点的值\n\t */\n\tdefault void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) {\n\t\tputNode(parentKey, parentValue);\n\t\tputNode(childKey, childValue);\n\t\tlinkNodes(parentKey, childKey);\n\t}\n\n\t/**\n\t * 添加子节点，并为子节点指定父节点：\n\t * <ul>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点不存在，则会根据键值创建一个对应的节点；</li>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点存在，则会更新对应节点的值；</li>\n\t * </ul>\n\t *\n\t * @param parentKey  父节点的key\n\t * @param childKey   子节点的key\n\t * @param childValue 子节点的值\n\t */\n\tvoid putLinkedNodes(K parentKey, K childKey, V childValue);\n\n\t/**\n\t * 为集合中的指定的节点建立父子关系\n\t *\n\t * @param parentKey 父节点的key\n\t * @param childKey  子节点的key\n\t */\n\tdefault void linkNodes(K parentKey, K childKey) {\n\t\tlinkNodes(parentKey, childKey, null);\n\t}\n\n\t/**\n\t * 为集合中的指定的节点建立父子关系\n\t *\n\t * @param parentKey 父节点的key\n\t * @param childKey  子节点的key\n\t * @param consumer  对父节点和子节点的操作，允许为null\n\t */\n\tvoid linkNodes(K parentKey, K childKey, BiConsumer<TreeEntry<K, V>, TreeEntry<K, V>> consumer);\n\n\t/**\n\t * 若{@code parentKey}或{@code childKey}对应节点都存在，则移除指定该父节点与其直接关联的指定子节点间的引用关系\n\t *\n\t * @param parentKey 父节点的key\n\t * @param childKey  子节点\n\t */\n\tvoid unlinkNode(K parentKey, K childKey);\n\n\t// ===================== 父节点相关方法 =====================\n\n\t/**\n\t * 获取指定节点所在树结构的全部树节点 <br>\n\t * 比如：存在 a -&gt; b -&gt; c 的关系，则输入 a/b/c 都将返回 a, b, c\n\t *\n\t * @param key 指定节点的key\n\t * @return 节点\n\t */\n\tdefault Set<TreeEntry<K, V>> getTreeNodes(K key) {\n\t\tfinal TreeEntry<K, V> target = get(key);\n\t\tif (ObjectUtil.isNull(target)) {\n\t\t\treturn Collections.emptySet();\n\t\t}\n\t\tfinal Set<TreeEntry<K, V>> results = CollUtil.newLinkedHashSet(target.getRoot());\n\t\tCollUtil.addAll(results, target.getRoot().getChildren().values());\n\t\treturn results;\n\t}\n\n\t/**\n\t * 获取以指定节点作为叶子节点的树结构，然后获取该树结构的根节点 <br>\n\t * 比如：存在 a -&gt; b -&gt; c 的关系，则输入 a/b/c 都将返回 a\n\t *\n\t * @param key 指定节点的key\n\t * @return 节点\n\t */\n\tdefault TreeEntry<K, V> getRootNode(K key) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(TreeEntry::getRoot)\n\t\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 获取指定节点的直接父节点 <br>\n\t * 比如：若存在 a -&gt; b -&gt; c 的关系，此时输入 a 将返回 null，输入 b 将返回 a，输入 c 将返回 b\n\t *\n\t * @param key 指定节点的key\n\t * @return 节点\n\t */\n\tdefault TreeEntry<K, V> getDeclaredParentNode(K key) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(TreeEntry::getDeclaredParent)\n\t\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 获取以指定节点作为叶子节点的树结构，然后获取该树结构中指定节点的指定父节点\n\t *\n\t * @param key       指定节点的key\n\t * @param parentKey 指定父节点key\n\t * @return 节点\n\t */\n\tdefault TreeEntry<K, V> getParentNode(K key, K parentKey) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(t -> t.getParent(parentKey))\n\t\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 获取以指定节点作为叶子节点的树结构，然后确认该树结构中当前节点是否存在指定父节点\n\t *\n\t * @param key       指定节点的key\n\t * @param parentKey 指定父节点的key\n\t * @return 是否\n\t */\n\tdefault boolean containsParentNode(K key, K parentKey) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(m -> m.containsParent(parentKey))\n\t\t\t\t.orElse(false);\n\t}\n\n\t/**\n\t * 获取指定节点的值\n\t *\n\t * @param key 节点的key\n\t * @return 节点值，若节点不存在，或节点值为null都将返回null\n\t */\n\tdefault V getNodeValue(K key) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t.map(TreeEntry::getValue)\n\t\t\t.get();\n\t}\n\n\t// ===================== 子节点相关方法 =====================\n\n\t/**\n\t * 判断以该父节点作为根节点的树结构中是否具有指定子节点\n\t *\n\t * @param parentKey 父节点\n\t * @param childKey  子节点\n\t * @return 是否\n\t */\n\tdefault boolean containsChildNode(K parentKey, K childKey) {\n\t\treturn Opt.ofNullable(get(parentKey))\n\t\t\t\t.map(m -> m.containsChild(childKey))\n\t\t\t\t.orElse(false);\n\t}\n\n\t/**\n\t * 获取指定父节点直接关联的子节点 <br>\n\t * 比如：若存在 a -&gt; b -&gt; c 的关系，此时输入 b 将返回 c，输入 a 将返回 b\n\t *\n\t * @param key key\n\t * @return 节点\n\t */\n\tdefault Collection<TreeEntry<K, V>> getDeclaredChildNodes(K key) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(TreeEntry::getDeclaredChildren)\n\t\t\t\t.map(Map::values)\n\t\t\t\t.orElseGet(Collections::emptyList);\n\t}\n\n\t/**\n\t * 获取指定父节点的全部子节点 <br>\n\t * 比如：若存在 a -&gt; b -&gt; c 的关系，此时输入 b 将返回 c，输入 a 将返回 b，c\n\t *\n\t * @param key key\n\t * @return 该节点的全部子节点\n\t */\n\tdefault Collection<TreeEntry<K, V>> getChildNodes(K key) {\n\t\treturn Opt.ofNullable(get(key))\n\t\t\t\t.map(TreeEntry::getChildren)\n\t\t\t\t.map(Map::values)\n\t\t\t\t.orElseGet(Collections::emptyList);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/FuncKeyMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * 自定义函数Key风格的Map\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.6.0\n */\npublic class FuncKeyMap<K, V> extends CustomKeyMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Function<Object, K> keyFunc;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造<br>\n\t * 注意提供的Map中不能有键值对，否则可能导致自定义key失效\n\t *\n\t * @param emptyMap       Map，提供的空map\n\t * @param keyFunc 自定义KEY的函数\n\t */\n\tpublic FuncKeyMap(Map<K, V> emptyMap, Function<Object, K> keyFunc) {\n\t\tsuper(emptyMap);\n\t\tthis.keyFunc = keyFunc;\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 根据函数自定义键\n\t *\n\t * @param key KEY\n\t * @return 驼峰Key\n\t */\n\t@Override\n\tprotected K customKey(Object key) {\n\t\tif (null != this.keyFunc) {\n\t\t\treturn keyFunc.apply(key);\n\t\t}\n\t\t//noinspection unchecked\n\t\treturn (K)key;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/FuncMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * 自定义键值函数风格的Map\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.0\n */\npublic class FuncMap<K, V> extends TransMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Function<Object, K> keyFunc;\n\tprivate final Function<Object, V> valueFunc;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造<br>\n\t * 注意提供的Map中不能有键值对，否则可能导致自定义key失效\n\t *\n\t * @param mapFactory  Map，提供的空map\n\t * @param keyFunc   自定义KEY的函数\n\t * @param valueFunc 自定义value函数\n\t */\n\tpublic FuncMap(Supplier<Map<K, V>> mapFactory, Function<Object, K> keyFunc, Function<Object, V> valueFunc) {\n\t\tthis(mapFactory.get(), keyFunc, valueFunc);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 注意提供的Map中不能有键值对，否则可能导致自定义key失效\n\t *\n\t * @param emptyMap  Map，提供的空map\n\t * @param keyFunc   自定义KEY的函数\n\t * @param valueFunc 自定义value函数\n\t */\n\tpublic FuncMap(Map<K, V> emptyMap, Function<Object, K> keyFunc, Function<Object, V> valueFunc) {\n\t\tsuper(emptyMap);\n\t\tthis.keyFunc = keyFunc;\n\t\tthis.valueFunc = valueFunc;\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 根据函数自定义键\n\t *\n\t * @param key KEY\n\t * @return 驼峰Key\n\t */\n\t@Override\n\tprotected K customKey(Object key) {\n\t\tif (null != this.keyFunc) {\n\t\t\treturn keyFunc.apply(key);\n\t\t}\n\t\t//noinspection unchecked\n\t\treturn (K) key;\n\t}\n\n\t@Override\n\tprotected V customValue(Object value) {\n\t\tif (null != this.valueFunc) {\n\t\t\treturn valueFunc.apply(value);\n\t\t}\n\t\t//noinspection unchecked\n\t\treturn (V) value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/LinkedForestMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiPredicate;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\n/**\n * {@link ForestMap}的基本实现。\n *\n * <p>该集合可以被视为以{@link TreeEntryNode#getKey()}作为key，{@link TreeEntryNode}实例作为value的{@link LinkedHashMap}。<br>\n * 使用时，将每一对键与值对视为一个{@link TreeEntryNode}节点，节点的id即为{@link TreeEntryNode#getKey()}，\n * 任何情况下使用相同的key都将会访问到同一个节点。<br>\n *\n * <p>节点通过key形成父子关系，并最终构成多叉树结构，多组平行的多叉树将在当前集合中构成森林。\n * 使用者可以通过{@link ForestMap}本身的方法来对森林进行操作或访问，\n * 也可以在获取到{@link TreeEntry}后，使用节点本身的方法对数进行操作或访问。\n *\n * @param <K> key类型\n * @author huangchengxing\n */\npublic class LinkedForestMap<K, V> implements ForestMap<K, V> {\n\n\t/**\n\t * 节点集合\n\t */\n\tprivate final Map<K, TreeEntryNode<K, V>> nodes;\n\n\t/**\n\t * 当指定节点已经与其他节点构成了父子关系，是否允许将该节点的父节点强制替换为指定节点\n\t */\n\tprivate final boolean allowOverrideParent;\n\n\t/**\n\t * 构建{@link LinkedForestMap}\n\t *\n\t * @param allowOverrideParent 当指定节点已经与其他节点构成了父子关系，是否允许将该节点的父节点强制替换为指定节点\n\t */\n\tpublic LinkedForestMap(boolean allowOverrideParent) {\n\t\tthis.allowOverrideParent = allowOverrideParent;\n\t\tthis.nodes = new LinkedHashMap<>();\n\t}\n\n\t// ====================== Map接口实现 ======================\n\n\t/**\n\t * 获取当前实例中的节点个数\n\t *\n\t * @return 节点个数\n\t */\n\t@Override\n\tpublic int size() {\n\t\treturn nodes.size();\n\t}\n\n\t/**\n\t * 当前实例是否为空\n\t *\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn nodes.isEmpty();\n\t}\n\n\t/**\n\t * 当前实例中是否存在key对应的节点\n\t *\n\t * @param key key\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn nodes.containsKey(key);\n\t}\n\n\t/**\n\t * 当前实例中是否存在对应的{@link TreeEntry}实例\n\t *\n\t * @param value {@link TreeEntry}实例\n\t * @return 是否\n\t */\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\treturn nodes.containsValue(value);\n\t}\n\n\t/**\n\t * 获取key对应的节点\n\t *\n\t * @param key key\n\t * @return 节点\n\t */\n\t@Override\n\tpublic TreeEntry<K, V> get(Object key) {\n\t\treturn nodes.get(key);\n\t}\n\n\t/**\n\t * 将指定节点从当前{@link Map}中删除\n\t * <ul>\n\t *     <li>若存在父节点或子节点，则将其断开其与父节点或子节点的引用关系；</li>\n\t *     <li>\n\t *         若同时存在父节点或子节点，则会在删除后将让子节点直接成为父节点的子节点，比如：<br>\n\t *         现有引用关系 a -&gt; b -&gt; c，删除 b 后，将有 a -&gt; c\n\t *     </li>\n\t * </ul>\n\t *\n\t * @param key 节点的key\n\t * @return 删除的且引用关系已经改变的节点，若key没有对应节点，则返回null\n\t */\n\t@Override\n\tpublic TreeEntry<K, V> remove(Object key) {\n\t\tfinal TreeEntryNode<K, V> target = nodes.remove(key);\n\t\tif (ObjectUtil.isNull(target)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 若存在父节点：\n\t\t// 1.将该目标从父节点的子节点中移除\n\t\t// 2.将目标的子节点直接将目标的父节点作为父节点\n\t\tif (target.hasParent()) {\n\t\t\tfinal TreeEntryNode<K, V> parent = target.getDeclaredParent();\n\t\t\tfinal Map<K, TreeEntry<K, V>> targetChildren = target.getChildren();\n\t\t\tparent.removeDeclaredChild(target.getKey());\n\t\t\ttarget.clear();\n\t\t\ttargetChildren.forEach((k, c) -> parent.addChild((TreeEntryNode<K, V>) c));\n\t\t}\n\t\treturn target;\n\t}\n\n\t/**\n\t * 将当前集合清空，并清除全部节点间的引用关系\n\t */\n\t@Override\n\tpublic void clear() {\n\t\tnodes.values().forEach(TreeEntryNode::clear);\n\t\tnodes.clear();\n\t}\n\n\t/**\n\t * 返回当前实例中全部的key组成的{@link Set}集合\n\t *\n\t * @return 集合\n\t */\n\t@Override\n\tpublic Set<K> keySet() {\n\t\treturn nodes.keySet();\n\t}\n\n\t/**\n\t * 返回当前实例中全部{@link TreeEntry}组成的{@link Collection}集合\n\t *\n\t * @return 集合\n\t */\n\t@Override\n\tpublic Collection<TreeEntry<K, V>> values() {\n\t\treturn new ArrayList<>(nodes.values());\n\t}\n\n\t/**\n\t * 由key与{@link TreeEntry}组成的键值对实体的{@link Set}集合。\n\t * 注意，返回集合中{@link Map.Entry#setValue(Object)}不支持调用。\n\t *\n\t * @return 集合\n\t */\n\t@Override\n\tpublic Set<Map.Entry<K, TreeEntry<K, V>>> entrySet() {\n\t\treturn nodes.entrySet().stream()\n\t\t\t\t.map(this::wrap)\n\t\t\t\t.collect(Collectors.toSet());\n\t}\n\n\t/**\n\t * 将{@link TreeEntryNode}包装为{@link EntryNodeWrapper}\n\t */\n\tprivate Map.Entry<K, TreeEntry<K, V>> wrap(Map.Entry<K, TreeEntryNode<K, V>> nodeEntry) {\n\t\treturn new EntryNodeWrapper<>(nodeEntry.getValue());\n\t}\n\n\t// ====================== ForestMap接口实现 ======================\n\n\t/**\n\t * 添加一个节点\n\t * <ul>\n\t *     <li>若key对应节点不存在，则以传入的键值创建一个新的节点；</li>\n\t *     <li>若key对应节点存在，则将该节点的值替换为{@code node}指定的值；</li>\n\t * </ul>\n\t *\n\t * @param key   节点的key\n\t * @param value 节点的value\n\t * @return 节点，若key已有对应节点，则返回具有旧值的节点，否则返回null\n\t */\n\t@Override\n\tpublic TreeEntryNode<K, V> putNode(K key, V value) {\n\t\tTreeEntryNode<K, V> target = nodes.get(key);\n\t\tif (ObjectUtil.isNotNull(target)) {\n\t\t\tfinal V oldVal = target.getValue();\n\t\t\ttarget.setValue(value);\n\t\t\treturn target.copy(oldVal);\n\t\t}\n\t\ttarget = new TreeEntryNode<>(null, key, value);\n\t\tnodes.put(key, target);\n\t\treturn null;\n\t}\n\n\t/**\n\t * 同时添加父子节点：\n\t * <ul>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点不存在，则会根据键值创建一个对应的节点；</li>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点存在，则会更新对应节点的值；</li>\n\t * </ul>\n\t * 该操作等同于：\n\t * <pre>\n\t *     TreeEntry&lt;K, V&gt;  parent = putNode(parentKey, parentValue);\n\t *     TreeEntry&lt;K, V&gt;  child = putNode(childKey, childValue);\n\t *     linkNodes(parentKey, childKey);\n\t * </pre>\n\t *\n\t * @param parentKey   父节点的key\n\t * @param parentValue 父节点的value\n\t * @param childKey    子节点的key\n\t * @param childValue  子节点的值\n\t */\n\t@Override\n\tpublic void putLinkedNodes(K parentKey, V parentValue, K childKey, V childValue) {\n\t\tlinkNodes(parentKey, childKey, (parent, child) -> {\n\t\t\tparent.setValue(parentValue);\n\t\t\tchild.setValue(childValue);\n\t\t});\n\t}\n\n\t/**\n\t * 添加子节点，并为子节点指定父节点：\n\t * <ul>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点不存在，则会根据键值创建一个对应的节点；</li>\n\t *     <li>若{@code parentKey}或{@code childKey}对应的节点存在，则会更新对应节点的值；</li>\n\t * </ul>\n\t *\n\t * @param parentKey  父节点的key\n\t * @param childKey   子节点的key\n\t * @param childValue 子节点的值\n\t */\n\t@Override\n\tpublic void putLinkedNodes(K parentKey, K childKey, V childValue) {\n\t\tlinkNodes(parentKey, childKey, (parent, child) -> child.setValue(childValue));\n\t}\n\n\t/**\n\t * 为指定的节点建立父子关系，若{@code parentKey}或{@code childKey}对应节点不存在，则会创建一个对应的值为null的空节点\n\t *\n\t * @param parentKey 父节点的key\n\t * @param childKey  子节点的key\n\t * @param consumer  对父节点和子节点的操作，允许为null\n\t */\n\t@Override\n\tpublic void linkNodes(K parentKey, K childKey, BiConsumer<TreeEntry<K, V>, TreeEntry<K, V>> consumer) {\n\t\tconsumer = ObjectUtil.defaultIfNull(consumer, (parent, child) -> {\n\t\t});\n\t\tfinal TreeEntryNode<K, V> parentNode = nodes.computeIfAbsent(parentKey, t -> new TreeEntryNode<>(null, t));\n\t\tTreeEntryNode<K, V> childNode = nodes.get(childKey);\n\n\t\t// 1.子节点不存在\n\t\tif (ObjectUtil.isNull(childNode)) {\n\t\t\tchildNode = new TreeEntryNode<>(parentNode, childKey);\n\t\t\tconsumer.accept(parentNode, childNode);\n\t\t\tnodes.put(childKey, childNode);\n\t\t\treturn;\n\t\t}\n\n\t\t// 2.子节点存在，且已经是该父节点的子节点了\n\t\tif (ObjectUtil.equals(parentNode, childNode.getDeclaredParent())) {\n\t\t\tconsumer.accept(parentNode, childNode);\n\t\t\treturn;\n\t\t}\n\n\t\t// 3.子节点存在，但是未与其他节点构成父子关系\n\t\tif (false == childNode.hasParent()) {\n\t\t\tparentNode.addChild(childNode);\n\t\t}\n\t\t// 4.子节点存在，且已经与其他节点构成父子关系，但是允许子节点直接修改其父节点\n\t\telse if (allowOverrideParent) {\n\t\t\tchildNode.getDeclaredParent().removeDeclaredChild(childNode.getKey());\n\t\t\tparentNode.addChild(childNode);\n\t\t}\n\t\t// 5.子节点存在，且已经与其他节点构成父子关系，但是不允许子节点直接修改其父节点\n\t\telse {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\n\t\t\t\t\t\"[{}] has been used as child of [{}], can not be overwrite as child of [{}]\",\n\t\t\t\t\tchildNode.getKey(), childNode.getDeclaredParent().getKey(), parentKey\n\t\t\t));\n\t\t}\n\t\tconsumer.accept(parentNode, childNode);\n\t}\n\n\t/**\n\t * 移除指定父节点与其直接关联的子节点间的引用关系，但是不会将该节点从集合中删除\n\t *\n\t * @param parentKey 父节点的key\n\t * @param childKey  子节点\n\t */\n\t@Override\n\tpublic void unlinkNode(K parentKey, K childKey) {\n\t\tfinal TreeEntryNode<K, V> childNode = nodes.get(childKey);\n\t\tif (ObjectUtil.isNull(childNode)) {\n\t\t\treturn;\n\t\t}\n\t\tif (childNode.hasParent()) {\n\t\t\tchildNode.getDeclaredParent().removeDeclaredChild(childNode.getKey());\n\t\t}\n\t}\n\n\t/**\n\t * 树节点\n\t *\n\t * @param <K> key类型\n\t * @author huangchengxing\n\t */\n\tpublic static class TreeEntryNode<K, V> implements TreeEntry<K, V> {\n\n\t\t/**\n\t\t * 根节点\n\t\t */\n\t\tprivate TreeEntryNode<K, V> root;\n\n\t\t/**\n\t\t * 父节点\n\t\t */\n\t\tprivate TreeEntryNode<K, V> parent;\n\n\t\t/**\n\t\t * 权重，表示到根节点的距离\n\t\t */\n\t\tprivate int weight;\n\n\t\t/**\n\t\t * 子节点\n\t\t */\n\t\tprivate final Map<K, TreeEntryNode<K, V>> children;\n\n\t\t/**\n\t\t * key\n\t\t */\n\t\tprivate final K key;\n\n\t\t/**\n\t\t * 值\n\t\t */\n\t\tprivate V value;\n\n\t\t/**\n\t\t * 创建一个节点\n\t\t *\n\t\t * @param parent 节点的父节点\n\t\t * @param key    节点的key\n\t\t */\n\t\tpublic TreeEntryNode(TreeEntryNode<K, V> parent, K key) {\n\t\t\tthis(parent, key, null);\n\t\t}\n\n\t\t/**\n\t\t * 创建一个节点\n\t\t *\n\t\t * @param parent 节点的父节点\n\t\t * @param key    节点的key\n\t\t * @param value  节点的value\n\t\t */\n\t\tpublic TreeEntryNode(TreeEntryNode<K, V> parent, K key, V value) {\n\t\t\tthis.parent = parent;\n\t\t\tthis.key = key;\n\t\t\tthis.value = value;\n\t\t\tthis.children = new LinkedHashMap<>();\n\t\t\tif (ObjectUtil.isNull(parent)) {\n\t\t\t\tthis.root = this;\n\t\t\t\tthis.weight = 0;\n\t\t\t} else {\n\t\t\t\tparent.addChild(this);\n\t\t\t\tthis.weight = parent.weight + 1;\n\t\t\t\tthis.root = parent.root;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * 获取当前节点的key\n\t\t *\n\t\t * @return 节点的key\n\t\t */\n\t\t@Override\n\t\tpublic K getKey() {\n\t\t\treturn key;\n\t\t}\n\n\t\t/**\n\t\t * 获取当前节点与根节点的距离\n\t\t *\n\t\t * @return 当前节点与根节点的距离\n\t\t */\n\t\t@Override\n\t\tpublic int getWeight() {\n\t\t\treturn weight;\n\t\t}\n\n\t\t/**\n\t\t * 获取节点的value\n\t\t *\n\t\t * @return 节点的value\n\t\t */\n\t\t@Override\n\t\tpublic V getValue() {\n\t\t\treturn value;\n\t\t}\n\n\t\t/**\n\t\t * 设置节点的value\n\t\t *\n\t\t * @param value 节点的value\n\t\t * @return 节点的旧value\n\t\t */\n\t\t@Override\n\t\tpublic V setValue(V value) {\n\t\t\tfinal V oldVal = getValue();\n\t\t\tthis.value = value;\n\t\t\treturn oldVal;\n\t\t}\n\n\t\t// ================== 父节点的操作 ==================\n\n\t\t/**\n\t\t * 从当前节点开始，向上递归当前节点的父节点\n\t\t *\n\t\t * @param includeCurrent 是否处理当前节点\n\t\t * @param consumer       对节点的操作\n\t\t * @param breakTraverse  是否终止遍历\n\t\t * @return 遍历到的最后一个节点\n\t\t */\n\t\tTreeEntryNode<K, V> traverseParentNodes(\n\t\t\t\tboolean includeCurrent, Consumer<TreeEntryNode<K, V>> consumer, Predicate<TreeEntryNode<K, V>> breakTraverse) {\n\t\t\tbreakTraverse = ObjectUtil.defaultIfNull(breakTraverse, a -> n -> false);\n\t\t\tTreeEntryNode<K, V> curr = includeCurrent ? this : this.parent;\n\t\t\twhile (ObjectUtil.isNotNull(curr)) {\n\t\t\t\tconsumer.accept(curr);\n\t\t\t\tif (breakTraverse.test(curr)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcurr = curr.parent;\n\t\t\t}\n\t\t\treturn curr;\n\t\t}\n\n\t\t/**\n\t\t * 当前节点是否为根节点\n\t\t *\n\t\t * @return 当前节点是否为根节点\n\t\t */\n\t\tpublic boolean isRoot() {\n\t\t\treturn getRoot() == this;\n\t\t}\n\n\t\t/**\n\t\t * 获取以当前节点作为叶子节点的树结构，然后获取该树结构的根节点\n\t\t *\n\t\t * @return 根节点\n\t\t */\n\t\t@Override\n\t\tpublic TreeEntryNode<K, V> getRoot() {\n\t\t\tif (ObjectUtil.isNotNull(this.root)) {\n\t\t\t\treturn this.root;\n\t\t\t} else {\n\t\t\t\tthis.root = traverseParentNodes(true, p -> {\n\t\t\t\t}, p -> !p.hasParent());\n\t\t\t}\n\t\t\treturn this.root;\n\t\t}\n\n\t\t/**\n\t\t * 获取当前节点直接关联的父节点\n\t\t *\n\t\t * @return 父节点，当节点不存在对应父节点时返回null\n\t\t */\n\t\t@Override\n\t\tpublic TreeEntryNode<K, V> getDeclaredParent() {\n\t\t\treturn parent;\n\t\t}\n\n\t\t/**\n\t\t * 获取以当前节点作为叶子节点的树结构，然后获取该树结构中当前节点的指定父节点\n\t\t *\n\t\t * @param key 指定父节点的key\n\t\t * @return 指定父节点，当不存在时返回null\n\t\t */\n\t\t@Override\n\t\tpublic TreeEntryNode<K, V> getParent(K key) {\n\t\t\treturn traverseParentNodes(false, p -> {\n\t\t\t}, p -> p.equalsKey(key));\n\t\t}\n\n\t\t/**\n\t\t * 获取以当前节点作为根节点的树结构，然后遍历所有节点\n\t\t *\n\t\t * @param includeSelf  是否处理当前节点\n\t\t * @param nodeConsumer 对节点的处理\n\t\t */\n\t\t@Override\n\t\tpublic void forEachChild(boolean includeSelf, Consumer<TreeEntry<K, V>> nodeConsumer) {\n\t\t\ttraverseChildNodes(includeSelf, (index, child) -> nodeConsumer.accept(child), null);\n\t\t}\n\n\t\t/**\n\t\t * 指定key与当前节点的key是否相等\n\t\t *\n\t\t * @param key 要比较的key\n\t\t * @return 是否key一致\n\t\t */\n\t\tpublic boolean equalsKey(K key) {\n\t\t\treturn ObjectUtil.equal(getKey(), key);\n\t\t}\n\n\t\t// ================== 子节点的操作 ==================\n\n\t\t/**\n\t\t * 从当前节点开始，按广度优先向下遍历当前节点的所有子节点\n\t\t *\n\t\t * @param includeCurrent 是否包含当前节点\n\t\t * @param consumer       对节点与节点和当前节点的距离的操作，当{@code includeCurrent}为false时下标从1开始，否则从0开始\n\t\t * @param breakTraverse  是否终止遍历，为null时默认总是返回{@code true}\n\t\t * @return 遍历到的最后一个节点\n\t\t */\n\t\tTreeEntryNode<K, V> traverseChildNodes(\n\t\t\t\tboolean includeCurrent, BiConsumer<Integer, TreeEntryNode<K, V>> consumer, BiPredicate<Integer, TreeEntryNode<K, V>> breakTraverse) {\n\t\t\tbreakTraverse = ObjectUtil.defaultIfNull(breakTraverse, (i, n) -> false);\n\t\t\tfinal Deque<List<TreeEntryNode<K, V>>> keyNodeDeque = CollUtil.newLinkedList(CollUtil.newArrayList(this));\n\t\t\tboolean needProcess = includeCurrent;\n\t\t\tint index = includeCurrent ? 0 : 1;\n\t\t\tTreeEntryNode<K, V> lastNode = null;\n\t\t\twhile (!keyNodeDeque.isEmpty()) {\n\t\t\t\tfinal List<TreeEntryNode<K, V>> curr = keyNodeDeque.removeFirst();\n\t\t\t\tfinal List<TreeEntryNode<K, V>> next = new ArrayList<>();\n\t\t\t\tfor (final TreeEntryNode<K, V> node : curr) {\n\t\t\t\t\tif (needProcess) {\n\t\t\t\t\t\tconsumer.accept(index, node);\n\t\t\t\t\t\tif (breakTraverse.test(index, node)) {\n\t\t\t\t\t\t\treturn node;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tneedProcess = true;\n\t\t\t\t\t}\n\t\t\t\t\tCollUtil.addAll(next, node.children.values());\n\t\t\t\t}\n\t\t\t\tif (!next.isEmpty()) {\n\t\t\t\t\tkeyNodeDeque.addLast(next);\n\t\t\t\t}\n\t\t\t\tlastNode = CollUtil.getLast(next);\n\t\t\t\tindex++;\n\t\t\t}\n\t\t\treturn lastNode;\n\t\t}\n\n\n\t\t/**\n\t\t * 添加子节点\n\t\t *\n\t\t * @param child 子节点\n\t\t * @throws IllegalArgumentException 当要添加的子节点已经是其自身父节点时抛出\n\t\t */\n\t\tvoid addChild(TreeEntryNode<K, V> child) {\n\t\t\tif (containsChild(child.key)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 检查循环引用\n\t\t\ttraverseParentNodes(true, s -> Assert.notEquals(\n\t\t\t\t\ts.key, child.key,\n\t\t\t\t\t\"circular reference between [{}] and [{}]!\",\n\t\t\t\t\ts.key, this.key\n\t\t\t), null);\n\n\t\t\t// 调整该节点的信息\n\t\t\tchild.parent = this;\n\t\t\tchild.traverseChildNodes(true, (i, c) -> {\n\t\t\t\tc.root = getRoot();\n\t\t\t\tc.weight = i + getWeight() + 1;\n\t\t\t}, null);\n\n\t\t\t// 将该节点添加为当前节点的子节点\n\t\t\tchildren.put(child.key, child);\n\t\t}\n\n\t\t/**\n\t\t * 移除子节点\n\t\t *\n\t\t * @param key 子节点\n\t\t */\n\t\tvoid removeDeclaredChild(K key) {\n\t\t\tfinal TreeEntryNode<K, V> child = children.get(key);\n\t\t\tif (ObjectUtil.isNull(child)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 断开该节点与其父节点的关系\n\t\t\tthis.children.remove(key);\n\n\t\t\t// 重置子节点及其下属节点的相关属性\n\t\t\tchild.parent = null;\n\t\t\tchild.traverseChildNodes(true, (i, c) -> {\n\t\t\t\tc.root = child;\n\t\t\t\tc.weight = i;\n\t\t\t}, null);\n\t\t}\n\n\t\t/**\n\t\t * 获取以当前节点作为根节点的树结构，然后获取该树结构中的当前节点的指定子节点\n\t\t *\n\t\t * @param key 指定子节点的key\n\t\t * @return 节点\n\t\t */\n\t\t@Override\n\t\tpublic TreeEntryNode<K, V> getChild(K key) {\n\t\t\treturn traverseChildNodes(false, (i, c) -> {\n\t\t\t}, (i, c) -> c.equalsKey(key));\n\t\t}\n\n\t\t/**\n\t\t * 获取当前节点直接关联的子节点\n\t\t *\n\t\t * @return 节点\n\t\t */\n\t\t@Override\n\t\tpublic Map<K, TreeEntry<K, V>> getDeclaredChildren() {\n\t\t\treturn new LinkedHashMap<>(this.children);\n\t\t}\n\n\t\t/**\n\t\t * 获取以当前节点作为根节点的树结构，然后按广度优先获取该树结构中的当前节点的全部子节点\n\t\t *\n\t\t * @return 节点\n\t\t */\n\t\t@Override\n\t\tpublic Map<K, TreeEntry<K, V>> getChildren() {\n\t\t\tfinal Map<K, TreeEntry<K, V>> childrenMap = new LinkedHashMap<>();\n\t\t\ttraverseChildNodes(false, (i, c) -> childrenMap.put(c.getKey(), c), null);\n\t\t\treturn childrenMap;\n\t\t}\n\n\t\t/**\n\t\t * 移除对子节点、父节点与根节点的全部引用\n\t\t */\n\t\tvoid clear() {\n\t\t\tthis.root = null;\n\t\t\tthis.children.clear();\n\t\t\tthis.parent = null;\n\t\t}\n\n\t\t/**\n\t\t * 比较目标对象与当前{@link TreeEntry}是否相等。<br>\n\t\t * 默认只要{@link TreeEntry#getKey()}的返回值相同，即认为两者相等\n\t\t *\n\t\t * @param o 目标对象\n\t\t * @return 是否\n\t\t */\n\t\t@Override\n\t\tpublic boolean equals(Object o) {\n\t\t\tif (this == o) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (o == null || this.getClass().equals(o.getClass()) || ClassUtil.isAssignable(this.getClass(), o.getClass())) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfinal TreeEntry<?, ?> treeEntry = (TreeEntry<?, ?>) o;\n\t\t\treturn ObjectUtil.equals(this.getKey(), treeEntry.getKey());\n\t\t}\n\n\t\t/**\n\t\t * 返回当前{@link TreeEntry}的哈希值。<br>\n\t\t * 默认总是返回{@link TreeEntry#getKey()}的哈希值\n\t\t *\n\t\t * @return 哈希值\n\t\t */\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn Objects.hash(getKey());\n\t\t}\n\n\t\t/**\n\t\t * 复制一个当前节点\n\t\t *\n\t\t * @param value 复制的节点的值\n\t\t * @return 节点\n\t\t */\n\t\tTreeEntryNode<K, V> copy(V value) {\n\t\t\tTreeEntryNode<K, V> copiedNode = new TreeEntryNode<>(this.parent, this.key, ObjectUtil.defaultIfNull(value, this.value));\n\t\t\tcopiedNode.children.putAll(children);\n\t\t\treturn copiedNode;\n\t\t}\n\n\t}\n\n\t/**\n\t * {@link java.util.Map.Entry}包装类\n\t *\n\t * @param <K> key类型\n\t * @param <V> value类型\n\t * @param <N> 包装的{@link TreeEntry}类型\n\t * @see #entrySet()\n\t * @see #values()\n\t */\n\tpublic static class EntryNodeWrapper<K, V, N extends TreeEntry<K, V>> implements Map.Entry<K, TreeEntry<K, V>> {\n\t\tprivate final N entryNode;\n\n\t\tEntryNodeWrapper(N entryNode) {\n\t\t\tthis.entryNode = entryNode;\n\t\t}\n\n\t\t@Override\n\t\tpublic K getKey() {\n\t\t\treturn entryNode.getKey();\n\t\t}\n\n\t\t@Override\n\t\tpublic TreeEntry<K, V> getValue() {\n\t\t\treturn entryNode;\n\t\t}\n\n\t\t@Override\n\t\tpublic TreeEntry<K, V> setValue(TreeEntry<K, V> value) {\n\t\t\tthrow new UnsupportedOperationException();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java",
    "content": "package cn.hutool.core.map;\n\n\nimport cn.hutool.core.builder.Builder;\n\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n/**\n * Map创建类\n *\n * @param <K> Key类型\n * @param <V> Value类型\n * @since 3.1.1\n */\npublic class MapBuilder<K, V> implements Builder<Map<K, V>> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Map<K, V> map;\n\n\t/**\n\t * 创建Builder，默认HashMap实现\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @return MapBuilder\n\t * @since 5.3.0\n\t */\n\tpublic static <K, V> MapBuilder<K, V> create() {\n\t\treturn create(false);\n\t}\n\n\t/**\n\t * 创建Builder\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param isLinked true创建LinkedHashMap，false创建HashMap\n\t * @return MapBuilder\n\t * @since 5.3.0\n\t */\n\tpublic static <K, V> MapBuilder<K, V> create(boolean isLinked) {\n\t\treturn create(MapUtil.newHashMap(isLinked));\n\t}\n\n\t/**\n\t * 创建Builder\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @param map Map实体类\n\t * @return MapBuilder\n\t * @since 3.2.3\n\t */\n\tpublic static <K, V> MapBuilder<K, V> create(Map<K, V> map) {\n\t\treturn new MapBuilder<>(map);\n\t}\n\n\t/**\n\t * 链式Map创建类\n\t *\n\t * @param map 要使用的Map实现类\n\t */\n\tpublic MapBuilder(Map<K, V> map) {\n\t\tthis.map = map;\n\t}\n\n\t/**\n\t * 链式Map创建\n\t *\n\t * @param k Key类型\n\t * @param v Value类型\n\t * @return 当前类\n\t */\n\tpublic MapBuilder<K, V> put(K k, V v) {\n\t\tmap.put(k, v);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 链式Map创建\n\t *\n\t * @param condition put条件\n\t * @param k         Key类型\n\t * @param v         Value类型\n\t * @return 当前类\n\t * @since 5.7.5\n\t */\n\tpublic MapBuilder<K, V> put(boolean condition, K k, V v) {\n\t\tif (condition) {\n\t\t\tput(k, v);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 链式Map创建\n\t *\n\t * @param condition put条件\n\t * @param k         Key类型\n\t * @param supplier  Value类型结果提供方\n\t * @return 当前类\n\t * @since 5.7.5\n\t */\n\tpublic MapBuilder<K, V> put(boolean condition, K k, Supplier<V> supplier) {\n\t\tif (condition) {\n\t\t\tput(k, supplier.get());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 链式Map创建\n\t *\n\t * @param map 合并map\n\t * @return 当前类\n\t */\n\tpublic MapBuilder<K, V> putAll(Map<K, V> map) {\n\t\tthis.map.putAll(map);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空Map\n\t *\n\t * @return this\n\t * @since 5.7.23\n\t */\n\tpublic MapBuilder<K, V> clear() {\n\t\tthis.map.clear();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建后的map\n\t *\n\t * @return 创建后的map\n\t */\n\tpublic Map<K, V> map() {\n\t\treturn map;\n\t}\n\n\t/**\n\t * 创建后的map\n\t *\n\t * @return 创建后的map\n\t * @since 3.3.0\n\t */\n\t@Override\n\tpublic Map<K, V> build() {\n\t\treturn map();\n\t}\n\n\t/**\n\t * 将map转成字符串\n\t *\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @return 连接字符串\n\t */\n\tpublic String join(String separator, final String keyValueSeparator) {\n\t\treturn MapUtil.join(this.map, separator, keyValueSeparator);\n\t}\n\n\t/**\n\t * 将map转成字符串\n\t *\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @return 连接后的字符串\n\t */\n\tpublic String joinIgnoreNull(String separator, final String keyValueSeparator) {\n\t\treturn MapUtil.joinIgnoreNull(this.map, separator, keyValueSeparator);\n\t}\n\n\t/**\n\t * 将map转成字符串\n\t *\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @return 连接后的字符串\n\t */\n\tpublic String join(String separator, final String keyValueSeparator, boolean isIgnoreNull) {\n\t\treturn MapUtil.join(this.map, separator, keyValueSeparator, isIgnoreNull);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/MapProxy.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.getter.OptNullBasicTypeFromObjectGetter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.BooleanUtil;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Map代理，提供各种getXXX方法，并提供默认值支持\n *\n * @author looly\n * @since 3.2.0\n */\npublic class MapProxy implements Map<Object, Object>, OptNullBasicTypeFromObjectGetter<Object>, InvocationHandler, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@SuppressWarnings(\"rawtypes\")\n\tMap map;\n\n\t/**\n\t * 创建代理Map<br>\n\t * 此类对Map做一次包装，提供各种getXXX方法\n\t *\n\t * @param map 被代理的Map\n\t * @return {@link MapProxy}\n\t */\n\tpublic static MapProxy create(Map<?, ?> map) {\n\t\treturn (map instanceof MapProxy) ? (MapProxy) map : new MapProxy(map);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param map 被代理的Map\n\t */\n\tpublic MapProxy(Map<?, ?> map) {\n\t\tthis.map = map;\n\t}\n\n\t@Override\n\tpublic Object getObj(Object key, Object defaultValue) {\n\t\tfinal Object value = map.get(key);\n\t\treturn null != value ? value : defaultValue;\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn map.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn map.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn map.containsKey(key);\n\t}\n\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\treturn map.containsValue(value);\n\t}\n\n\t@Override\n\tpublic Object get(Object key) {\n\t\treturn map.get(key);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic Object put(Object key, Object value) {\n\t\treturn map.put(key, value);\n\t}\n\n\t@Override\n\tpublic Object remove(Object key) {\n\t\treturn map.remove(key);\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"NullableProblems\"})\n\t@Override\n\tpublic void putAll(Map<?, ?> m) {\n\t\tmap.putAll(m);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tmap.clear();\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"NullableProblems\"})\n\t@Override\n\tpublic Set<Object> keySet() {\n\t\treturn map.keySet();\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"NullableProblems\"})\n\t@Override\n\tpublic Collection<Object> values() {\n\t\treturn map.values();\n\t}\n\n\t@SuppressWarnings({\"unchecked\", \"NullableProblems\"})\n\t@Override\n\tpublic Set<Entry<Object, Object>> entrySet() {\n\t\treturn map.entrySet();\n\t}\n\n\t@Override\n\tpublic Object invoke(Object proxy, Method method, Object[] args) {\n\t\tfinal Class<?>[] parameterTypes = method.getParameterTypes();\n\t\tif (ArrayUtil.isEmpty(parameterTypes)) {\n\t\t\tfinal Class<?> returnType = method.getReturnType();\n\t\t\tif (void.class != returnType) {\n\t\t\t\t// 匹配Getter\n\t\t\t\tfinal String methodName = method.getName();\n\t\t\t\tString fieldName = null;\n\t\t\t\tif (methodName.startsWith(\"get\")) {\n\t\t\t\t\t// 匹配getXXX\n\t\t\t\t\tfieldName = StrUtil.removePreAndLowerFirst(methodName, 3);\n\t\t\t\t} else if (BooleanUtil.isBoolean(returnType) && methodName.startsWith(\"is\")) {\n\t\t\t\t\t// 匹配isXXX\n\t\t\t\t\tfieldName = StrUtil.removePreAndLowerFirst(methodName, 2);\n\t\t\t\t}else if (\"hashCode\".equals(methodName)) {\n\t\t\t\t\treturn this.hashCode();\n\t\t\t\t} else if (\"toString\".equals(methodName)) {\n\t\t\t\t\treturn this.toString();\n\t\t\t\t}\n\n\t\t\t\tif (StrUtil.isNotBlank(fieldName)) {\n\t\t\t\t\tif (false == this.containsKey(fieldName)) {\n\t\t\t\t\t\t// 驼峰不存在转下划线尝试\n\t\t\t\t\t\tfieldName = StrUtil.toUnderlineCase(fieldName);\n\t\t\t\t\t}\n\t\t\t\t\treturn Convert.convert(method.getGenericReturnType(), this.get(fieldName));\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else if (1 == parameterTypes.length) {\n\t\t\t// 匹配Setter\n\t\t\tfinal String methodName = method.getName();\n\t\t\tif (methodName.startsWith(\"set\")) {\n\t\t\t\tfinal String fieldName = StrUtil.removePreAndLowerFirst(methodName, 3);\n\t\t\t\tif (StrUtil.isNotBlank(fieldName)) {\n\t\t\t\t\tthis.put(fieldName, args[0]);\n\t\t\t\t\tfinal Class<?> returnType = method.getReturnType();\n\t\t\t\t\tif(returnType.isInstance(proxy)){\n\t\t\t\t\t\treturn proxy;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (\"equals\".equals(methodName)) {\n\t\t\t\treturn this.equals(args[0]);\n\t\t\t}\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(method.toGenericString());\n\t}\n\n\t/**\n\t * 将Map代理为指定接口的动态代理对象\n\t *\n\t * @param <T> 代理的Bean类型\n\t * @param interfaceClass 接口\n\t * @return 代理对象\n\t * @since 4.5.2\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T toProxyBean(Class<T> interfaceClass) {\n\t\treturn (T) Proxy.newProxyInstance(ClassLoaderUtil.getClassLoader(), new Class<?>[]{interfaceClass}, this);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.*;\nimport cn.hutool.core.stream.CollectorUtil;\nimport cn.hutool.core.util.*;\n\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * Map相关工具类\n *\n * @author Looly\n * @since 3.1.1\n */\npublic class MapUtil {\n\n\t/**\n\t * 默认初始大小\n\t */\n\tpublic static final int DEFAULT_INITIAL_CAPACITY = 16;\n\t/**\n\t * 默认增长因子，当Map的size达到 容量*增长因子时，开始扩充Map\n\t */\n\tpublic static final float DEFAULT_LOAD_FACTOR = 0.75f;\n\n\t/**\n\t * Map是否为空\n\t *\n\t * @param map 集合\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Map<?, ?> map) {\n\t\treturn null == map || map.isEmpty();\n\t}\n\n\t/**\n\t * Map是否为非空\n\t *\n\t * @param map 集合\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(Map<?, ?> map) {\n\t\treturn null != map && false == map.isEmpty();\n\t}\n\n\t/**\n\t * 如果提供的集合为{@code null}，返回一个不可变的默认空集合，否则返回原集合<br>\n\t * 空集合使用{@link Collections#emptyMap()}\n\t *\n\t * @param <K> 键类型\n\t * @param <V> 值类型\n\t * @param set 提供的集合，可能为null\n\t * @return 原集合，若为null返回空集合\n\t * @since 4.6.3\n\t */\n\tpublic static <K, V> Map<K, V> emptyIfNull(Map<K, V> set) {\n\t\treturn (null == set) ? Collections.emptyMap() : set;\n\t}\n\n\t/**\n\t * 如果给定Map为空，返回默认Map\n\t *\n\t * @param <T>        集合类型\n\t * @param <K>        键类型\n\t * @param <V>        值类型\n\t * @param map        Map\n\t * @param defaultMap 默认Map\n\t * @return 非空（empty）的原Map或默认Map\n\t * @since 4.6.9\n\t */\n\tpublic static <T extends Map<K, V>, K, V> T defaultIfEmpty(T map, T defaultMap) {\n\t\treturn isEmpty(map) ? defaultMap : map;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- new HashMap\n\n\t/**\n\t * 新建一个HashMap\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @return HashMap对象\n\t */\n\tpublic static <K, V> HashMap<K, V> newHashMap() {\n\t\treturn new HashMap<>();\n\t}\n\n\t/**\n\t * 新建一个HashMap\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param size     初始大小，由于默认负载因子0.75，传入的size会实际初始大小为size / 0.75 + 1\n\t * @param isLinked Map的Key是否有序，有序返回 {@link LinkedHashMap}，否则返回 {@link HashMap}\n\t * @return HashMap对象\n\t * @since 3.0.4\n\t */\n\tpublic static <K, V> HashMap<K, V> newHashMap(int size, boolean isLinked) {\n\t\tfinal int initialCapacity = (int) (size / DEFAULT_LOAD_FACTOR) + 1;\n\t\treturn isLinked ? new LinkedHashMap<>(initialCapacity) : new HashMap<>(initialCapacity);\n\t}\n\n\t/**\n\t * 新建一个HashMap\n\t *\n\t * @param <K>  Key类型\n\t * @param <V>  Value类型\n\t * @param size 初始大小，由于默认负载因子0.75，传入的size会实际初始大小为size / 0.75 + 1\n\t * @return HashMap对象\n\t */\n\tpublic static <K, V> HashMap<K, V> newHashMap(int size) {\n\t\treturn newHashMap(size, false);\n\t}\n\n\t/**\n\t * 新建一个HashMap\n\t *\n\t * @param <K>      Key类型\n\t * @param <V>      Value类型\n\t * @param isLinked Map的Key是否有序，有序返回 {@link LinkedHashMap}，否则返回 {@link HashMap}\n\t * @return HashMap对象\n\t */\n\tpublic static <K, V> HashMap<K, V> newHashMap(boolean isLinked) {\n\t\treturn newHashMap(DEFAULT_INITIAL_CAPACITY, isLinked);\n\t}\n\n\t/**\n\t * 新建TreeMap，Key有序的Map\n\t *\n\t * @param <K>        key的类型\n\t * @param <V>        value的类型\n\t * @param comparator Key比较器\n\t * @return TreeMap\n\t * @since 3.2.3\n\t */\n\tpublic static <K, V> TreeMap<K, V> newTreeMap(Comparator<? super K> comparator) {\n\t\treturn new TreeMap<>(comparator);\n\t}\n\n\t/**\n\t * 新建TreeMap，Key有序的Map\n\t *\n\t * @param <K>        key的类型\n\t * @param <V>        value的类型\n\t * @param map        Map\n\t * @param comparator Key比较器\n\t * @return TreeMap\n\t * @since 3.2.3\n\t */\n\tpublic static <K, V> TreeMap<K, V> newTreeMap(Map<K, V> map, Comparator<? super K> comparator) {\n\t\tfinal TreeMap<K, V> treeMap = new TreeMap<>(comparator);\n\t\tif (false == isEmpty(map)) {\n\t\t\ttreeMap.putAll(map);\n\t\t}\n\t\treturn treeMap;\n\t}\n\n\t/**\n\t * 创建键不重复Map\n\t *\n\t * @param <K>  key的类型\n\t * @param <V>  value的类型\n\t * @param size 初始容量\n\t * @return {@link IdentityHashMap}\n\t * @since 4.5.7\n\t */\n\tpublic static <K, V> Map<K, V> newIdentityMap(int size) {\n\t\treturn new IdentityHashMap<>(size);\n\t}\n\n\t/**\n\t * 新建一个初始容量为{@link MapUtil#DEFAULT_INITIAL_CAPACITY} 的ConcurrentHashMap\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @return ConcurrentHashMap\n\t */\n\tpublic static <K, V> ConcurrentHashMap<K, V> newConcurrentHashMap() {\n\t\treturn new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 新建一个ConcurrentHashMap\n\t *\n\t * @param size 初始容量，当传入的容量小于等于0时，容量为{@link MapUtil#DEFAULT_INITIAL_CAPACITY}\n\t * @param <K>  key的类型\n\t * @param <V>  value的类型\n\t * @return ConcurrentHashMap\n\t */\n\tpublic static <K, V> ConcurrentHashMap<K, V> newConcurrentHashMap(int size) {\n\t\tfinal int initCapacity = size <= 0 ? DEFAULT_INITIAL_CAPACITY : size;\n\t\treturn new ConcurrentHashMap<>(initCapacity);\n\t}\n\n\t/**\n\t * 传入一个Map将其转化为ConcurrentHashMap类型\n\t *\n\t * @param map map\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @return ConcurrentHashMap\n\t */\n\tpublic static <K, V> ConcurrentHashMap<K, V> newConcurrentHashMap(Map<K, V> map) {\n\t\tif (isEmpty(map)) {\n\t\t\treturn new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY);\n\t\t}\n\t\treturn new ConcurrentHashMap<>(map);\n\t}\n\n\t/**\n\t * 创建Map<br>\n\t * 传入抽象Map{@link AbstractMap}和{@link Map}类将默认创建{@link HashMap}\n\t *\n\t * @param <K>     map键类型\n\t * @param <V>     map值类型\n\t * @param mapType map类型\n\t * @return {@link Map}实例\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> createMap(Class<?> mapType) {\n\t\tif (null == mapType || mapType.isAssignableFrom(AbstractMap.class)) {\n\t\t\treturn new HashMap<>();\n\t\t} else {\n\t\t\ttry {\n\t\t\t\treturn (Map<K, V>) ReflectUtil.newInstance(mapType);\n\t\t\t} catch (UtilException e) {\n\t\t\t\t// 不支持的map类型，返回默认的HashMap\n\t\t\t\treturn new HashMap<>();\n\t\t\t}\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- value of\n\n\t/**\n\t * 将单一键值对转换为Map\n\t *\n\t * @param <K>   键类型\n\t * @param <V>   值类型\n\t * @param key   键\n\t * @param value 值\n\t * @return {@link HashMap}\n\t */\n\tpublic static <K, V> HashMap<K, V> of(K key, V value) {\n\t\treturn of(key, value, false);\n\t}\n\n\t/**\n\t * 将单一键值对转换为Map\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param key     键\n\t * @param value   值\n\t * @param isOrder 是否有序\n\t * @return {@link HashMap}\n\t */\n\tpublic static <K, V> HashMap<K, V> of(K key, V value, boolean isOrder) {\n\t\tfinal HashMap<K, V> map = newHashMap(isOrder);\n\t\tmap.put(key, value);\n\t\treturn map;\n\t}\n\n\t/**\n\t * 根据给定的Pair数组创建Map对象\n\t *\n\t * @param <K>   键类型\n\t * @param <V>   值类型\n\t * @param pairs 键值对\n\t * @return Map\n\t * @since 5.4.1\n\t * @deprecated 方法容易歧义，请使用 {@code #ofEntries(Entry[])}\n\t */\n\t@SafeVarargs\n\t@Deprecated\n\tpublic static <K, V> Map<K, V> of(Pair<K, V>... pairs) {\n\t\tfinal Map<K, V> map = new HashMap<>();\n\t\tfor (Pair<K, V> pair : pairs) {\n\t\t\tmap.put(pair.getKey(), pair.getValue());\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 根据给定的Pair数组创建Map对象\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param entries 键值对\n\t * @return Map\n\t * @see #entry(Object, Object)\n\t * @since 5.8.0\n\t */\n\t@SafeVarargs\n\tpublic static <K, V> Map<K, V> ofEntries(Map.Entry<K, V>... entries) {\n\t\tfinal Map<K, V> map = new HashMap<>();\n\t\tfor (Map.Entry<K, V> pair : entries) {\n\t\t\tmap.put(pair.getKey(), pair.getValue());\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 将数组转换为Map（HashMap），支持数组元素类型为：\n\t *\n\t * <pre>\n\t * Map.Entry\n\t * 长度大于1的数组（取前两个值），如果不满足跳过此元素\n\t * Iterable 长度也必须大于1（取前两个值），如果不满足跳过此元素\n\t * Iterator 长度也必须大于1（取前两个值），如果不满足跳过此元素\n\t * </pre>\n\t *\n\t * <pre>\n\t * Map&lt;Object, Object&gt; colorMap = MapUtil.of(new String[][] {\n\t *    { \"RED\", \"#FF0000\" },\n\t *    { \"GREEN\", \"#00FF00\" },\n\t *    { \"BLUE\", \"#0000FF\" }\n\t * });\n\t * </pre>\n\t * <p>\n\t * 参考：commons-lang\n\t *\n\t * @param array 数组。元素类型为Map.Entry、数组、Iterable、Iterator\n\t * @return {@link HashMap}\n\t * @since 3.0.8\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static HashMap<Object, Object> of(Object[] array) {\n\t\tif (array == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal HashMap<Object, Object> map = new HashMap<>((int) (array.length * 1.5));\n\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\tfinal Object object = array[i];\n\t\t\tif (object instanceof Map.Entry) {\n\t\t\t\tMap.Entry entry = (Map.Entry) object;\n\t\t\t\tmap.put(entry.getKey(), entry.getValue());\n\t\t\t} else if (object instanceof Object[]) {\n\t\t\t\tfinal Object[] entry = (Object[]) object;\n\t\t\t\tif (entry.length > 1) {\n\t\t\t\t\tmap.put(entry[0], entry[1]);\n\t\t\t\t}\n\t\t\t} else if (object instanceof Iterable) {\n\t\t\t\tfinal Iterator iter = ((Iterable) object).iterator();\n\t\t\t\tif (iter.hasNext()) {\n\t\t\t\t\tfinal Object key = iter.next();\n\t\t\t\t\tif (iter.hasNext()) {\n\t\t\t\t\t\tfinal Object value = iter.next();\n\t\t\t\t\t\tmap.put(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (object instanceof Iterator) {\n\t\t\t\tfinal Iterator iter = ((Iterator) object);\n\t\t\t\tif (iter.hasNext()) {\n\t\t\t\t\tfinal Object key = iter.next();\n\t\t\t\t\tif (iter.hasNext()) {\n\t\t\t\t\t\tfinal Object value = iter.next();\n\t\t\t\t\t\tmap.put(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Array element {}, '{}', is not type of Map.Entry or Array or Iterable or Iterator\", i, object));\n\t\t\t}\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 行转列，合并相同的键，值合并为列表<br>\n\t * 将Map列表中相同key的值组成列表做为Map的value<br>\n\t * 是{@link #toMapList(Map)}的逆方法<br>\n\t * 比如传入数据：\n\t *\n\t * <pre>\n\t * [\n\t *  {a: 1, b: 1, c: 1}\n\t *  {a: 2, b: 2}\n\t *  {a: 3, b: 3}\n\t *  {a: 4}\n\t * ]\n\t * </pre>\n\t * <p>\n\t * 结果是：\n\t *\n\t * <pre>\n\t * {\n\t *   a: [1,2,3,4]\n\t *   b: [1,2,3,]\n\t *   c: [1]\n\t * }\n\t * </pre>\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param mapList Map列表\n\t * @return Map\n\t */\n\tpublic static <K, V> Map<K, List<V>> toListMap(Iterable<? extends Map<K, V>> mapList) {\n\t\tfinal HashMap<K, List<V>> resultMap = new HashMap<>();\n\t\tif (CollUtil.isEmpty(mapList)) {\n\t\t\treturn resultMap;\n\t\t}\n\n\t\tSet<Entry<K, V>> entrySet;\n\t\tfor (Map<K, V> map : mapList) {\n\t\t\tentrySet = map.entrySet();\n\t\t\tK key;\n\t\t\tList<V> valueList;\n\t\t\tfor (Entry<K, V> entry : entrySet) {\n\t\t\t\tkey = entry.getKey();\n\t\t\t\tvalueList = resultMap.get(key);\n\t\t\t\tif (null == valueList) {\n\t\t\t\t\tvalueList = CollUtil.newArrayList(entry.getValue());\n\t\t\t\t\tresultMap.put(key, valueList);\n\t\t\t\t} else {\n\t\t\t\t\tvalueList.add(entry.getValue());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn resultMap;\n\t}\n\n\t/**\n\t * 列转行。将Map中值列表分别按照其位置与key组成新的map。<br>\n\t * 是{@link #toListMap(Iterable)}的逆方法<br>\n\t * 比如传入数据：\n\t *\n\t * <pre>\n\t * {\n\t *   a: [1,2,3,4]\n\t *   b: [1,2,3,]\n\t *   c: [1]\n\t * }\n\t * </pre>\n\t * <p>\n\t * 结果是：\n\t *\n\t * <pre>\n\t * [\n\t *  {a: 1, b: 1, c: 1}\n\t *  {a: 2, b: 2}\n\t *  {a: 3, b: 3}\n\t *  {a: 4}\n\t * ]\n\t * </pre>\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param listMap 列表Map\n\t * @return Map列表\n\t */\n\tpublic static <K, V> List<Map<K, V>> toMapList(Map<K, ? extends Iterable<V>> listMap) {\n\t\tfinal List<Map<K, V>> resultList = new ArrayList<>();\n\t\tif (isEmpty(listMap)) {\n\t\t\treturn resultList;\n\t\t}\n\n\t\tboolean isEnd;// 是否结束。标准是元素列表已耗尽\n\t\tint index = 0;// 值索引\n\t\tMap<K, V> map;\n\t\tdo {\n\t\t\tisEnd = true;\n\t\t\tmap = new HashMap<>();\n\t\t\tList<V> vList;\n\t\t\tint vListSize;\n\t\t\tfor (Entry<K, ? extends Iterable<V>> entry : listMap.entrySet()) {\n\t\t\t\tvList = CollUtil.newArrayList(entry.getValue());\n\t\t\t\tvListSize = vList.size();\n\t\t\t\tif (index < vListSize) {\n\t\t\t\t\tmap.put(entry.getKey(), vList.get(index));\n\t\t\t\t\tif (index != vListSize - 1) {\n\t\t\t\t\t\t// 当值列表中还有更多值（非最后一个），继续循环\n\t\t\t\t\t\tisEnd = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (false == map.isEmpty()) {\n\t\t\t\tresultList.add(map);\n\t\t\t}\n\t\t\tindex++;\n\t\t} while (false == isEnd);\n\n\t\treturn resultList;\n\t}\n\n\t/**\n\t * 根据给定的entry列表，根据entry的key进行分组;\n\t *\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @param entries entry列表\n\t * @return entries\n\t */\n\tpublic static <K, V> Map<K, List<V>> grouping(Iterable<Map.Entry<K, V>> entries) {\n\t\tfinal Map<K, List<V>> map = new HashMap<>();\n\t\tif (CollUtil.isEmpty(entries)) {\n\t\t\treturn map;\n\t\t}\n\t\tfor (final Map.Entry<K, V> pair : entries) {\n\t\t\tfinal List<V> values = map.computeIfAbsent(pair.getKey(), k -> new ArrayList<>());\n\t\t\tvalues.add(pair.getValue());\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 将已知Map转换为key为驼峰风格的Map<br>\n\t * 如果KEY为非String类型，保留原值\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @param map 原Map\n\t * @return 驼峰风格Map\n\t * @since 3.3.1\n\t */\n\tpublic static <K, V> Map<K, V> toCamelCaseMap(Map<K, V> map) {\n\t\treturn (map instanceof LinkedHashMap) ? new CamelCaseLinkedMap<>(map) : new CamelCaseMap<>(map);\n\t}\n\n\t/**\n\t * 将键值对转换为二维数组，第一维是key，第二维是value\n\t *\n\t * @param map map\n\t * @return 数组\n\t * @since 4.1.9\n\t */\n\tpublic static Object[][] toObjectArray(Map<?, ?> map) {\n\t\tif (map == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Object[][] result = new Object[map.size()][2];\n\t\tif (map.isEmpty()) {\n\t\t\treturn result;\n\t\t}\n\t\tint index = 0;\n\t\tfor (Entry<?, ?> entry : map.entrySet()) {\n\t\t\tresult[index][0] = entry.getKey();\n\t\t\tresult[index][1] = entry.getValue();\n\t\t\tindex++;\n\t\t}\n\t\treturn result;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- join\n\n\t/**\n\t * 将map转成字符串\n\t *\n\t * @param <K>               键类型\n\t * @param <V>               值类型\n\t * @param map               Map\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 连接字符串\n\t * @since 3.1.1\n\t */\n\tpublic static <K, V> String join(Map<K, V> map, String separator, String keyValueSeparator, String... otherParams) {\n\t\treturn join(map, separator, keyValueSeparator, false, otherParams);\n\t}\n\n\t/**\n\t * 根据参数排序后拼接为字符串，常用于签名\n\t *\n\t * @param params            参数\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 签名字符串\n\t * @since 5.0.4\n\t */\n\tpublic static String sortJoin(Map<?, ?> params, String separator, String keyValueSeparator, boolean isIgnoreNull,\n\t\t\t\t\t\t\t\t  String... otherParams) {\n\t\treturn join(sort(params), separator, keyValueSeparator, isIgnoreNull, otherParams);\n\t}\n\n\t/**\n\t * 将map转成字符串，忽略null的键和值\n\t *\n\t * @param <K>               键类型\n\t * @param <V>               值类型\n\t * @param map               Map\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 连接后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static <K, V> String joinIgnoreNull(Map<K, V> map, String separator, String keyValueSeparator, String... otherParams) {\n\t\treturn join(map, separator, keyValueSeparator, true, otherParams);\n\t}\n\n\t/**\n\t * 将map转成字符串\n\t *\n\t * @param <K>               键类型\n\t * @param <V>               值类型\n\t * @param map               Map，为空返回otherParams拼接\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 连接后的字符串，map和otherParams为空返回\"\"\n\t * @since 3.1.1\n\t */\n\tpublic static <K, V> String join(Map<K, V> map, String separator, String keyValueSeparator, boolean isIgnoreNull, String... otherParams) {\n\t\tfinal StringBuilder strBuilder = StrUtil.builder();\n\t\tboolean isFirst = true;\n\t\tif (isNotEmpty(map)) {\n\t\t\tfor (Entry<K, V> entry : map.entrySet()) {\n\t\t\t\tif (false == isIgnoreNull || entry.getKey() != null && entry.getValue() != null) {\n\t\t\t\t\tif (isFirst) {\n\t\t\t\t\t\tisFirst = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstrBuilder.append(separator);\n\t\t\t\t\t}\n\t\t\t\t\tstrBuilder.append(Convert.toStr(entry.getKey())).append(keyValueSeparator).append(Convert.toStr(entry.getValue()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// 补充其它字符串到末尾，默认无分隔符\n\t\tif (ArrayUtil.isNotEmpty(otherParams)) {\n\t\t\tfor (String otherParam : otherParams) {\n\t\t\t\tstrBuilder.append(otherParam);\n\t\t\t}\n\t\t}\n\t\treturn strBuilder.toString();\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- filter\n\n\t/**\n\t * 编辑Map<br>\n\t * 编辑过程通过传入的Editor实现来返回需要的元素内容，这个Editor实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，如果返回{@code null}表示这个元素对象抛弃\n\t * 2、修改元素对象，返回集合中为修改后的对象\n\t * </pre>\n\t *\n\t * @param <K>    Key类型\n\t * @param <V>    Value类型\n\t * @param map    Map\n\t * @param editor 编辑器接口\n\t * @return 编辑后的Map\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> edit(Map<K, V> map, Editor<Entry<K, V>> editor) {\n\t\tif (null == map || null == editor) {\n\t\t\treturn map;\n\t\t}\n\n\t\tMap<K, V> map2 = ReflectUtil.newInstanceIfPossible(map.getClass());\n\t\tif (null == map2) {\n\t\t\tmap2 = new HashMap<>(map.size(), 1f);\n\t\t}\n\t\tif (isEmpty(map)) {\n\t\t\treturn map2;\n\t\t}\n\n\t\t// issue#3162@Github，在构造中put值，会导致新建map带有值内容，此处清空\n\t\tif (false == map2.isEmpty()) {\n\t\t\tmap2.clear();\n\t\t}\n\n\t\tEntry<K, V> modified;\n\t\tfor (Entry<K, V> entry : map.entrySet()) {\n\t\t\tmodified = editor.edit(entry);\n\t\t\tif (null != modified) {\n\t\t\t\tmap2.put(modified.getKey(), modified.getValue());\n\t\t\t}\n\t\t}\n\t\treturn map2;\n\t}\n\n\t/**\n\t * 过滤<br>\n\t * 过滤过程通过传入的Editor实现来返回需要的元素内容，这个Filter实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，如果返回null表示这个元素对象抛弃\n\t * </pre>\n\t *\n\t * @param <K>    Key类型\n\t * @param <V>    Value类型\n\t * @param map    Map\n\t * @param filter 过滤器接口，{@code null}返回原Map\n\t * @return 过滤后的Map\n\t * @since 3.1.0\n\t */\n\tpublic static <K, V> Map<K, V> filter(Map<K, V> map, Filter<Entry<K, V>> filter) {\n\t\tif (null == map || null == filter) {\n\t\t\treturn map;\n\t\t}\n\t\treturn edit(map, t -> filter.accept(t) ? t : null);\n\t}\n\n\n\t/**\n\t * 通过biFunction自定义一个规则，此规则将原Map中的元素转换成新的元素，生成新的Map返回<br>\n\t * 变更过程通过传入的 {@link BiFunction} 实现来返回一个值可以为不同类型的 {@link Map}\n\t *\n\t * @param map        原有的map\n\t * @param biFunction {@code lambda}，参数包含{@code key},{@code value}，返回值会作为新的{@code value}\n\t * @param <K>        {@code key}的类型\n\t * @param <V>        {@code value}的类型\n\t * @param <R>        新的，修改后的{@code value}的类型\n\t * @return 值可以为不同类型的 {@link Map}\n\t * @since 5.8.0\n\t */\n\tpublic static <K, V, R> Map<K, R> map(Map<K, V> map, BiFunction<K, V, R> biFunction) {\n\t\tif (null == map || null == biFunction) {\n\t\t\treturn MapUtil.newHashMap();\n\t\t}\n\t\treturn map.entrySet().stream().collect(CollectorUtil.toMap(Map.Entry::getKey, m -> biFunction.apply(m.getKey(), m.getValue()), (l, r) -> l));\n\t}\n\n\t/**\n\t * 过滤Map保留指定键值对，如果键不存在跳过\n\t *\n\t * @param <K>  Key类型\n\t * @param <V>  Value类型\n\t * @param map  原始Map\n\t * @param keys 键列表，{@code null}返回原Map\n\t * @return Map 结果，结果的Map类型与原Map保持一致\n\t * @since 4.0.10\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> filter(Map<K, V> map, K... keys) {\n\t\tif (null == map || null == keys) {\n\t\t\treturn map;\n\t\t}\n\n\t\tMap<K, V> map2 = ReflectUtil.newInstanceIfPossible(map.getClass());\n\t\tif (null == map2) {\n\t\t\tmap2 = new HashMap<>(map.size(), 1f);\n\t\t}\n\t\tif (isEmpty(map)) {\n\t\t\treturn map2;\n\t\t}\n\n\t\t// issue#3162@Github，在构造中put值，会导致新建map带有值内容，此处清空\n\t\tif (false == map2.isEmpty()) {\n\t\t\tmap2.clear();\n\t\t}\n\n\t\tfor (K key : keys) {\n\t\t\tif (map.containsKey(key)) {\n\t\t\t\tmap2.put(key, map.get(key));\n\t\t\t}\n\t\t}\n\t\treturn map2;\n\t}\n\n\t/**\n\t * Map的键和值互换\n\t * 互换键值对不检查值是否有重复，如果有则后加入的元素替换先加入的元素<br>\n\t * 值的顺序在HashMap中不确定，所以谁覆盖谁也不确定，在有序的Map中按照先后顺序覆盖，保留最后的值\n\t *\n\t * @param <T> 键和值类型\n\t * @param map Map对象，键值类型必须一致\n\t * @return 互换后的Map\n\t * @see #inverse(Map)\n\t * @since 3.2.2\n\t */\n\tpublic static <T> Map<T, T> reverse(Map<T, T> map) {\n\t\treturn edit(map, t -> new Entry<T, T>() {\n\n\t\t\t@Override\n\t\t\tpublic T getKey() {\n\t\t\t\treturn t.getValue();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic T getValue() {\n\t\t\t\treturn t.getKey();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic T setValue(T value) {\n\t\t\t\tthrow new UnsupportedOperationException(\"Unsupported setValue method !\");\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Map的键和值互换<br>\n\t * 互换键值对不检查值是否有重复，如果有则后加入的元素替换先加入的元素<br>\n\t * 值的顺序在HashMap中不确定，所以谁覆盖谁也不确定，在有序的Map中按照先后顺序覆盖，保留最后的值\n\t *\n\t * @param <K> 键和值类型\n\t * @param <V> 键和值类型\n\t * @param map Map对象\n\t * @return 互换后的Map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V> Map<V, K> inverse(Map<K, V> map) {\n\t\tfinal Map<V, K> result = createMap(map.getClass());\n\t\tmap.forEach((key, value) -> result.put(value, key));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 排序已有Map，Key有序的Map，使用默认Key排序方式（字母顺序）\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @param map Map\n\t * @return TreeMap\n\t * @see #newTreeMap(Map, Comparator)\n\t * @since 4.0.1\n\t */\n\tpublic static <K, V> TreeMap<K, V> sort(Map<K, V> map) {\n\t\treturn sort(map, null);\n\t}\n\n\t/**\n\t * 排序已有Map，Key有序的Map\n\t *\n\t * @param <K>        key的类型\n\t * @param <V>        value的类型\n\t * @param map        Map，为null返回null\n\t * @param comparator Key比较器\n\t * @return TreeMap，map为null返回null\n\t * @see #newTreeMap(Map, Comparator)\n\t * @since 4.0.1\n\t */\n\tpublic static <K, V> TreeMap<K, V> sort(Map<K, V> map, Comparator<? super K> comparator) {\n\t\tif (null == map) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (map instanceof TreeMap) {\n\t\t\t// 已经是可排序Map，此时只有比较器一致才返回原map\n\t\t\tTreeMap<K, V> result = (TreeMap<K, V>) map;\n\t\t\tif (null == comparator || comparator.equals(result.comparator())) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\n\t\treturn newTreeMap(map, comparator);\n\t}\n\n\t/**\n\t * 按照值排序，可选是否倒序\n\t *\n\t * @param map    需要对值排序的map\n\t * @param <K>    键类型\n\t * @param <V>    值类型\n\t * @param isDesc 是否倒序\n\t * @return 排序后新的Map\n\t * @since 5.5.8\n\t */\n\tpublic static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map, boolean isDesc) {\n\t\tMap<K, V> result = new LinkedHashMap<>();\n\t\tComparator<Entry<K, V>> entryComparator = Entry.comparingByValue();\n\t\tif (isDesc) {\n\t\t\tentryComparator = entryComparator.reversed();\n\t\t}\n\t\tmap.entrySet().stream().sorted(entryComparator).forEachOrdered(e -> result.put(e.getKey(), e.getValue()));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 创建代理Map<br>\n\t * {@link MapProxy}对Map做一次包装，提供各种getXXX方法\n\t *\n\t * @param map 被代理的Map\n\t * @return {@link MapProxy}\n\t * @since 3.2.0\n\t */\n\tpublic static MapProxy createProxy(Map<?, ?> map) {\n\t\treturn MapProxy.create(map);\n\t}\n\n\t/**\n\t * 创建Map包装类MapWrapper<br>\n\t * {@link MapWrapper}对Map做一次包装\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @param map 被代理的Map\n\t * @return {@link MapWrapper}\n\t * @since 4.5.4\n\t */\n\tpublic static <K, V> MapWrapper<K, V> wrap(Map<K, V> map) {\n\t\treturn new MapWrapper<>(map);\n\t}\n\n\t/**\n\t * 将对应Map转换为不可修改的Map\n\t *\n\t * @param map Map\n\t * @param <K> 键类型\n\t * @param <V> 值类型\n\t * @return 不修改Map\n\t * @since 5.2.6\n\t */\n\tpublic static <K, V> Map<K, V> unmodifiable(Map<K, V> map) {\n\t\treturn Collections.unmodifiableMap(map);\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- builder\n\n\t/**\n\t * 创建链接调用map\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @return map创建类\n\t */\n\tpublic static <K, V> MapBuilder<K, V> builder() {\n\t\treturn builder(new HashMap<>());\n\t}\n\n\t/**\n\t * 创建链接调用map\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @param map 实际使用的map\n\t * @return map创建类\n\t */\n\tpublic static <K, V> MapBuilder<K, V> builder(Map<K, V> map) {\n\t\treturn new MapBuilder<>(map);\n\t}\n\n\t/**\n\t * 创建链接调用map\n\t *\n\t * @param <K> Key类型\n\t * @param <V> Value类型\n\t * @param k   key\n\t * @param v   value\n\t * @return map创建类\n\t */\n\tpublic static <K, V> MapBuilder<K, V> builder(K k, V v) {\n\t\treturn (builder(new HashMap<K, V>())).put(k, v);\n\t}\n\n\t/**\n\t * 获取Map的部分key生成新的Map\n\t *\n\t * @param <K>  Key类型\n\t * @param <V>  Value类型\n\t * @param map  Map\n\t * @param keys 键列表\n\t * @return 新Map，只包含指定的key\n\t * @since 4.0.6\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> getAny(Map<K, V> map, final K... keys) {\n\t\treturn filter(map, entry -> ArrayUtil.contains(keys, entry.getKey()));\n\t}\n\n\t/**\n\t * 去掉Map中指定key的键值对，修改原Map\n\t *\n\t * @param <K>  Key类型\n\t * @param <V>  Value类型\n\t * @param map  Map\n\t * @param keys 键列表\n\t * @return 修改后的key\n\t * @since 5.0.5\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> removeAny(Map<K, V> map, final K... keys) {\n\t\tfor (K key : keys) {\n\t\t\tmap.remove(key);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为字符串\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static String getStr(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, String.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为字符串\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static String getStr(Map<?, ?> map, Object key, String defaultValue) {\n\t\treturn get(map, key, String.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Integer\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Integer getInt(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Integer.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Integer\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Integer getInt(Map<?, ?> map, Object key, Integer defaultValue) {\n\t\treturn get(map, key, Integer.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Double\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Double getDouble(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Double.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Double\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Double getDouble(Map<?, ?> map, Object key, Double defaultValue) {\n\t\treturn get(map, key, Double.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Float\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Float getFloat(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Float.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Float\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Float getFloat(Map<?, ?> map, Object key, Float defaultValue) {\n\t\treturn get(map, key, Float.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Short\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Short getShort(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Short.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Short\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Short getShort(Map<?, ?> map, Object key, Short defaultValue) {\n\t\treturn get(map, key, Short.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Bool\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Boolean getBool(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Boolean.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Bool\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Boolean getBool(Map<?, ?> map, Object key, Boolean defaultValue) {\n\t\treturn get(map, key, Boolean.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Character\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Character getChar(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Character.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Character\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Character getChar(Map<?, ?> map, Object key, Character defaultValue) {\n\t\treturn get(map, key, Character.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Long\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static Long getLong(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Long.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为Long\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static Long getLong(Map<?, ?> map, Object key, Long defaultValue) {\n\t\treturn get(map, key, Long.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为{@link Date}\n\t *\n\t * @param map Map\n\t * @param key 键\n\t * @return 值\n\t * @since 4.1.2\n\t */\n\tpublic static Date getDate(Map<?, ?> map, Object key) {\n\t\treturn get(map, key, Date.class);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为{@link Date}\n\t *\n\t * @param map          Map\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 4.1.2\n\t */\n\tpublic static Date getDate(Map<?, ?> map, Object key, Date defaultValue) {\n\t\treturn get(map, key, Date.class, defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型\n\t *\n\t * @param <T>  目标值类型\n\t * @param map  Map\n\t * @param key  键\n\t * @param type 值类型\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\tpublic static <T> T get(Map<?, ?> map, Object key, Class<T> type) {\n\t\treturn get(map, key, type, null);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型\n\t *\n\t * @param <T>          目标值类型\n\t * @param map          Map\n\t * @param key          键\n\t * @param type         值类型\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static <T> T get(Map<?, ?> map, Object key, Class<T> type, T defaultValue) {\n\t\treturn null == map ? defaultValue : Convert.convert(type, map.get(key), defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型，此方法在转换失败后不抛异常，返回null。\n\t *\n\t * @param <T>          目标值类型\n\t * @param map          Map\n\t * @param key          键\n\t * @param type         值类型\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.5.3\n\t */\n\tpublic static <T> T getQuietly(Map<?, ?> map, Object key, Class<T> type, T defaultValue) {\n\t\treturn null == map ? defaultValue : Convert.convertQuietly(type, map.get(key), defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型\n\t *\n\t * @param <T>  目标值类型\n\t * @param map  Map\n\t * @param key  键\n\t * @param type 值类型\n\t * @return 值\n\t * @since 4.5.12\n\t */\n\tpublic static <T> T get(Map<?, ?> map, Object key, TypeReference<T> type) {\n\t\treturn get(map, key, type, null);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型\n\t *\n\t * @param <T>          目标值类型\n\t * @param map          Map\n\t * @param key          键\n\t * @param type         值类型\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.3.11\n\t */\n\tpublic static <T> T get(Map<?, ?> map, Object key, TypeReference<T> type, T defaultValue) {\n\t\treturn null == map ? defaultValue : Convert.convert(type, map.get(key), defaultValue);\n\t}\n\n\t/**\n\t * 获取Map指定key的值，并转换为指定类型，转换失败后返回null，不抛异常\n\t *\n\t * @param <T>          目标值类型\n\t * @param map          Map\n\t * @param key          键\n\t * @param type         值类型\n\t * @param defaultValue 默认值\n\t * @return 值\n\t * @since 5.5.3\n\t */\n\tpublic static <T> T getQuietly(Map<?, ?> map, Object key, TypeReference<T> type, T defaultValue) {\n\t\treturn null == map ? defaultValue : Convert.convertQuietly(type, map.get(key), defaultValue);\n\t}\n\n\t/**\n\t * 重命名键<br>\n\t * 实现方式为移除然后重新put，当旧的key不存在直接返回<br>\n\t * 当新的key存在，抛出{@link IllegalArgumentException} 异常\n\t *\n\t * @param <K>    key的类型\n\t * @param <V>    value的类型\n\t * @param map    Map\n\t * @param oldKey 原键\n\t * @param newKey 新键\n\t * @return map\n\t * @throws IllegalArgumentException 新key存在抛出此异常\n\t * @since 4.5.16\n\t */\n\tpublic static <K, V> Map<K, V> renameKey(Map<K, V> map, K oldKey, K newKey) {\n\t\tif (isNotEmpty(map) && map.containsKey(oldKey)) {\n\t\t\tif (map.containsKey(newKey)) {\n\t\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"The key '{}' exist !\", newKey));\n\t\t\t}\n\t\t\tmap.put(newKey, map.remove(oldKey));\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 去除Map中值为{@code null}的键值对<br>\n\t * 注意：此方法在传入的Map上直接修改。\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @param map Map\n\t * @return map\n\t * @since 4.6.5\n\t */\n\tpublic static <K, V> Map<K, V> removeNullValue(Map<K, V> map) {\n\t\tif (isEmpty(map)) {\n\t\t\treturn map;\n\t\t}\n\n\t\tfinal Iterator<Entry<K, V>> iter = map.entrySet().iterator();\n\t\tEntry<K, V> entry;\n\t\twhile (iter.hasNext()) {\n\t\t\tentry = iter.next();\n\t\t\tif (null == entry.getValue()) {\n\t\t\t\titer.remove();\n\t\t\t}\n\t\t}\n\n\t\treturn map;\n\t}\n\n\t/**\n\t * 去除Map中值为指定值的键值对<br>\n\t * 注意：此方法在传入的Map上直接修改。\n\t *\n\t * @param <K>   key的类型\n\t * @param <V>   value的类型\n\t * @param map   Map\n\t * @param value 给定值\n\t * @return map\n\t * @since 5.8.41\n\t */\n\tpublic static <K, V> Map<K, V> removeByValue(final Map<K, V> map, final V value) {\n\t\treturn removeIf(map, entry -> ObjUtil.equals(value, entry.getValue()));\n\t}\n\n\t/**\n\t * 去除Map中值为{@code null}的键值对<br>\n\t * 注意：此方法在传入的Map上直接修改。\n\t *\n\t * @param <K>       key的类型\n\t * @param <V>       value的类型\n\t * @param map       Map\n\t * @param predicate 移除条件，当{@link Predicate#test(Object)}为{@code true}时移除\n\t * @return map\n\t * @since 5.8.41\n\t */\n\tpublic static <K, V> Map<K, V> removeIf(final Map<K, V> map, final Predicate<Entry<K, V>> predicate) {\n\t\tif (isEmpty(map)) {\n\t\t\treturn map;\n\t\t}\n\t\tmap.entrySet().removeIf(predicate);\n\t\treturn map;\n\t}\n\n\t/**\n\t * 返回一个空Map\n\t *\n\t * @param <K> 键类型\n\t * @param <V> 值类型\n\t * @return 空Map\n\t * @see Collections#emptyMap()\n\t * @since 5.3.1\n\t */\n\tpublic static <K, V> Map<K, V> empty() {\n\t\treturn Collections.emptyMap();\n\t}\n\n\t/**\n\t * 根据传入的Map类型不同，返回对应类型的空Map，支持类型包括：\n\t *\n\t * <pre>\n\t *     1. NavigableMap\n\t *     2. SortedMap\n\t *     3. Map\n\t * </pre>\n\t *\n\t * @param <K>      键类型\n\t * @param <V>      值类型\n\t * @param <T>      Map类型\n\t * @param mapClass Map类型，null返回默认的Map\n\t * @return 空Map\n\t * @since 5.3.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V, T extends Map<K, V>> T empty(Class<?> mapClass) {\n\t\tif (null == mapClass) {\n\t\t\treturn (T) Collections.emptyMap();\n\t\t}\n\t\tif (NavigableMap.class == mapClass) {\n\t\t\treturn (T) Collections.emptyNavigableMap();\n\t\t} else if (SortedMap.class == mapClass) {\n\t\t\treturn (T) Collections.emptySortedMap();\n\t\t} else if (Map.class == mapClass) {\n\t\t\treturn (T) Collections.emptyMap();\n\t\t}\n\n\t\t// 不支持空集合的集合类型\n\t\tthrow new IllegalArgumentException(StrUtil.format(\"[{}] is not support to get empty!\", mapClass));\n\t}\n\n\t/**\n\t * 清除一个或多个Map集合内的元素，每个Map调用clear()方法\n\t *\n\t * @param maps 一个或多个Map\n\t */\n\tpublic static void clear(Map<?, ?>... maps) {\n\t\tfor (Map<?, ?> map : maps) {\n\t\t\tif (isNotEmpty(map)) {\n\t\t\t\tmap.clear();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从Map中获取指定键列表对应的值列表<br>\n\t * 如果key在map中不存在或key对应值为null，则返回值列表对应位置的值也为null\n\t *\n\t * @param <K>  键类型\n\t * @param <V>  值类型\n\t * @param map  {@link Map}\n\t * @param keys 键列表\n\t * @return 值列表\n\t * @since 5.7.20\n\t */\n\tpublic static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, Iterator<K> keys) {\n\t\tfinal ArrayList<V> values = new ArrayList<>();\n\t\twhile (keys.hasNext()) {\n\t\t\tvalues.add(map.get(keys.next()));\n\t\t}\n\t\treturn values;\n\t}\n\n\t/**\n\t * 将键和值转换为{@link AbstractMap.SimpleImmutableEntry}<br>\n\t * 返回的Entry不可变\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @param <K>   键类型\n\t * @param <V>   值类型\n\t * @return {@link AbstractMap.SimpleImmutableEntry}\n\t * @since 5.8.0\n\t */\n\tpublic static <K, V> Map.Entry<K, V> entry(K key, V value) {\n\t\treturn entry(key, value, true);\n\t}\n\n\t/**\n\t * 将键和值转换为{@link AbstractMap.SimpleEntry} 或者 {@link AbstractMap.SimpleImmutableEntry}\n\t *\n\t * @param key         键\n\t * @param value       值\n\t * @param <K>         键类型\n\t * @param <V>         值类型\n\t * @param isImmutable 是否不可变Entry\n\t * @return {@link AbstractMap.SimpleEntry} 或者 {@link AbstractMap.SimpleImmutableEntry}\n\t * @since 5.8.0\n\t */\n\tpublic static <K, V> Map.Entry<K, V> entry(K key, V value, boolean isImmutable) {\n\t\treturn isImmutable ?\n\t\t\tnew AbstractMap.SimpleImmutableEntry<>(key, value) :\n\t\t\tnew AbstractMap.SimpleEntry<>(key, value);\n\t}\n\n\t/**\n\t * 如果 key 对应的 value 不存在，则使用获取 mappingFunction 重新计算后的值，并保存为该 key 的 value，否则返回 value。<br>\n\t * 方法来自Dubbo，解决使用ConcurrentHashMap.computeIfAbsent导致的死循环问题。（issues#2349）<br>\n\t * A temporary workaround for Java 8 specific performance issue JDK-8161372 .<br>\n\t * This class should be removed once we drop Java 8 support.<br>\n\t * 参考：https://github.com/apache/dubbo/blob/3.2/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ConcurrentHashMapUtils.java\n\t *\n\t * @param <K>             键类型\n\t * @param <V>             值类型\n\t * @param map             Map\n\t * @param key             键\n\t * @param mappingFunction 值不存在时值的生成函数\n\t * @return 值\n\t * @see <a href=\"https://bugs.openjdk.java.net/browse/JDK-8161372\">https://bugs.openjdk.java.net/browse/JDK-8161372</a>\n\t */\n\tpublic static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<? super K, ? extends V> mappingFunction) {\n\t\tif (JdkUtil.IS_JDK8) {\n\t\t\treturn computeIfAbsentForJdk8(map, key, mappingFunction);\n\t\t} else {\n\t\t\treturn map.computeIfAbsent(key, mappingFunction);\n\t\t}\n\t}\n\n\t/**\n\t * 如果 key 对应的 value 不存在，则使用获取 mappingFunction 重新计算后的值，并保存为该 key 的 value，否则返回 value。<br>\n\t * 解决使用ConcurrentHashMap.computeIfAbsent导致的死循环问题。（issues#2349）<br>\n\t * A temporary workaround for Java 8 specific performance issue JDK-8161372 .<br>\n\t * This class should be removed once we drop Java 8 support.\n\t *\n\t * <p>\n\t * 注意此方法只能用于JDK8\n\t * </p>\n\t *\n\t * @param <K>             键类型\n\t * @param <V>             值类型\n\t * @param map             Map，一般用于线程安全的Map\n\t * @param key             键\n\t * @param mappingFunction 值计算函数\n\t * @return 值\n\t * @see <a href=\"https://bugs.openjdk.java.net/browse/JDK-8161372\">https://bugs.openjdk.java.net/browse/JDK-8161372</a>\n\t */\n\tpublic static <K, V> V computeIfAbsentForJdk8(final Map<K, V> map, final K key, final Function<? super K, ? extends V> mappingFunction) {\n\t\tV value = map.get(key);\n\t\tif (null == value) {\n\t\t\tvalue = mappingFunction.apply(key);\n\t\t\tfinal V res = map.putIfAbsent(key, value);\n\t\t\tif (null != res) {\n\t\t\t\t// issues#I6RVMY\n\t\t\t\t// 如果旧值存在，说明其他线程已经赋值成功，putIfAbsent没有执行，返回旧值\n\t\t\t\treturn res;\n\t\t\t}\n\t\t\t// 如果旧值不存在，说明赋值成功，返回当前值\n\n\t\t\t// Dubbo的解决方式，判空后调用依旧无法解决死循环问题\n\t\t\t// 见：Issue2349Test\n\t\t\t//value = map.computeIfAbsent(key, mappingFunction);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 将一个Map按照固定大小拆分成多个子Map\n\t *\n\t * @param <K>  键类型\n\t * @param <V>  值类型\n\t * @param map  Map\n\t * @param size 子Map的大小\n\t * @return 子Map列表\n\t * @since 5.8.26\n\t */\n\tpublic static <K, V> List<Map<K, V>> partition(Map<K, V> map, int size) {\n\t\tAssert.notNull(map);\n\t\tif (size <= 0) {\n\t\t\tthrow new IllegalArgumentException(\"Size must be greater than 0\");\n\t\t}\n\t\tList<Map<K, V>> list = new ArrayList<>();\n\t\tIterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();\n\t\twhile (iterator.hasNext()) {\n\t\t\tMap<K, V> subMap = new HashMap<>(size);\n\t\t\tfor (int i = 0; i < size && iterator.hasNext(); i++) {\n\t\t\t\tMap.Entry<K, V> entry = iterator.next();\n\t\t\t\tsubMap.put(entry.getKey(), entry.getValue());\n\t\t\t}\n\t\t\tlist.add(subMap);\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 将多层级Map处理为一个层级Map类型\n\t *\n\t * @param map 入参Map\n\t * @param <K> 键类型\n\t * @param <V> 值类型\n\t * @return 单层级Map返回值\n\t * @since 5.8.40\n\t */\n\tpublic static <K, V> Map<K, V> flatten(final Map<K, V> map) {\n\t\treturn flatten(map, new HashMap<>());\n\t}\n\n\t/**\n\t * 递归调用将多层级Map处理为一个层级Map类型\n\t *\n\t * @param map     入参Map\n\t * @param flatMap 单层级Map返回值\n\t * @param <K>     键类型\n\t * @param <V>     值类型\n\t * @return 单层级Map返回值\n\t * @since 5.8.40\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> flatten(final Map<K, V> map, Map<K, V> flatMap) {\n\t\tAssert.notNull(map);\n\t\tif (null == flatMap) {\n\t\t\tflatMap = new HashMap<>();\n\t\t}\n\n\t\tMap<K, V> finalFlatMap = flatMap;\n\t\tmap.forEach((k, v) -> {\n\t\t\t// 避免嵌套循环\n\t\t\tif (v instanceof Map && v != map) {\n\t\t\t\tflatten((Map<K, V>) v, finalFlatMap);\n\t\t\t} else {\n\t\t\t\tfinalFlatMap.put(k, v);\n\t\t\t}\n\t\t});\n\n\t\treturn flatMap;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * Map包装类，通过包装一个已有Map实现特定功能。例如自定义Key的规则或Value规则\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 4.3.3\n */\npublic class MapWrapper<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, Serializable, Cloneable {\n\tprivate static final long serialVersionUID = -7524578042008586382L;\n\n\t/**\n\t * 默认增长因子\n\t */\n\tprotected static final float DEFAULT_LOAD_FACTOR = 0.75f;\n\t/**\n\t * 默认初始大小\n\t */\n\tprotected static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16\n\n\tprivate Map<K, V> raw;\n\n\t/**\n\t * 构造<br>\n\t * 通过传入一个Map从而确定Map的类型，子类需创建一个空的Map，而非传入一个已有Map，否则值可能会被修改\n\t *\n\t * @param mapFactory 空Map创建工厂\n\t * @since 5.8.0\n\t */\n\tpublic MapWrapper(Supplier<Map<K, V>> mapFactory) {\n\t\tthis(mapFactory.get());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw 被包装的Map\n\t */\n\tpublic MapWrapper(Map<K, V> raw) {\n\t\tthis.raw = raw;\n\t}\n\n\t/**\n\t * 获取原始的Map\n\t *\n\t * @return Map\n\t */\n\tpublic Map<K, V> getRaw() {\n\t\treturn this.raw;\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn raw.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn raw.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn raw.containsKey(key);\n\t}\n\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\treturn raw.containsValue(value);\n\t}\n\n\t@Override\n\tpublic V get(Object key) {\n\t\treturn raw.get(key);\n\t}\n\n\t@Override\n\tpublic V put(K key, V value) {\n\t\treturn raw.put(key, value);\n\t}\n\n\t@Override\n\tpublic V remove(Object key) {\n\t\treturn raw.remove(key);\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends K, ? extends V> m) {\n\t\traw.putAll(m);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\traw.clear();\n\t}\n\n\t@Override\n\tpublic Collection<V> values() {\n\t\treturn raw.values();\n\t}\n\n\t@Override\n\tpublic Set<K> keySet() {\n\t\treturn raw.keySet();\n\t}\n\n\t@Override\n\tpublic Set<Entry<K, V>> entrySet() {\n\t\treturn raw.entrySet();\n\t}\n\n\t@Override\n\tpublic Iterator<Entry<K, V>> iterator() {\n\t\treturn this.entrySet().iterator();\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tMapWrapper<?, ?> that = (MapWrapper<?, ?>) o;\n\t\treturn Objects.equals(raw, that.raw);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(raw);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn raw.toString();\n\t}\n\n\n\t@Override\n\tpublic void forEach(BiConsumer<? super K, ? super V> action) {\n\t\traw.forEach(action);\n\t}\n\n\t@Override\n\tpublic void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {\n\t\traw.replaceAll(function);\n\t}\n\n\t@Override\n\tpublic V putIfAbsent(K key, V value) {\n\t\treturn raw.putIfAbsent(key, value);\n\t}\n\n\t@Override\n\tpublic boolean remove(Object key, Object value) {\n\t\treturn raw.remove(key, value);\n\t}\n\n\t@Override\n\tpublic boolean replace(K key, V oldValue, V newValue) {\n\t\treturn raw.replace(key, oldValue, newValue);\n\t}\n\n\t@Override\n\tpublic V replace(K key, V value) {\n\t\treturn raw.replace(key, value);\n\t}\n\n\t@Override\n\tpublic V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {\n\t\treturn raw.computeIfAbsent(key, mappingFunction);\n\t}\n\n\t// 重写默认方法的意义在于，如果被包装的Map自定义了这些默认方法，包装类就可以保持这些行为的一致性\n\t//---------------------------------------------------------------------------- Override default methods start\n\t@Override\n\tpublic V getOrDefault(Object key, V defaultValue) {\n\t\treturn raw.getOrDefault(key, defaultValue);\n\t}\n\n\t@Override\n\tpublic V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\treturn raw.computeIfPresent(key, remappingFunction);\n\t}\n\n\t@Override\n\tpublic V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\treturn raw.compute(key, remappingFunction);\n\t}\n\n\t@Override\n\tpublic V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {\n\t\treturn raw.merge(key, value, remappingFunction);\n\t}\n\n\t@Override\n\tpublic MapWrapper<K, V> clone() throws CloneNotSupportedException {\n\t\t@SuppressWarnings(\"unchecked\") final MapWrapper<K, V> clone = (MapWrapper<K, V>) super.clone();\n\t\tclone.raw = ObjectUtil.clone(raw);\n\t\treturn clone;\n\t}\n\n\t//---------------------------------------------------------------------------- Override default methods end\n\n\t// region 序列化与反序列化重写\n\tprivate void writeObject(final ObjectOutputStream out) throws IOException {\n\t\tout.defaultWriteObject();\n\t\tout.writeObject(this.raw);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tprivate void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {\n\t\tin.defaultReadObject();\n\t\traw = (Map<K, V>) in.readObject();\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/ReferenceConcurrentMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReferenceUtil;\n\nimport java.io.Serializable;\nimport java.lang.ref.Reference;\nimport java.lang.ref.ReferenceQueue;\nimport java.lang.ref.SoftReference;\nimport java.lang.ref.WeakReference;\nimport java.util.AbstractMap;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\n/**\n * 线程安全的ReferenceMap实现<br>\n * 参考：jdk.management.resource.internal.WeakKeyConcurrentHashMap\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 5.8.0\n * @deprecated 请使用{@link cn.hutool.core.map.reference.ReferenceConcurrentMap}\n */\npublic class ReferenceConcurrentMap<K, V> implements ConcurrentMap<K, V>, Iterable<Map.Entry<K, V>>, Serializable {\n\n\tfinal ConcurrentMap<Reference<K>, V> raw;\n\tprivate final ReferenceQueue<K> lastQueue;\n\tprivate final ReferenceUtil.ReferenceType keyType;\n\t/**\n\t * 回收监听\n\t */\n\tprivate BiConsumer<Reference<? extends K>, V> purgeListener;\n\n\t// region 构造\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw           {@link ConcurrentMap}实现\n\t * @param referenceType Reference类型\n\t */\n\tpublic ReferenceConcurrentMap(ConcurrentMap<Reference<K>, V> raw, ReferenceUtil.ReferenceType referenceType) {\n\t\tthis.raw = raw;\n\t\tthis.keyType = referenceType;\n\t\tlastQueue = new ReferenceQueue<>();\n\t}\n\t// endregion\n\n\t/**\n\t * 设置对象回收清除监听\n\t *\n\t * @param purgeListener 监听函数\n\t */\n\tpublic void setPurgeListener(BiConsumer<Reference<? extends K>, V> purgeListener) {\n\t\tthis.purgeListener = purgeListener;\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn 0 == size();\n\t}\n\n\t@Override\n\tpublic V get(Object key) {\n\t\tthis.purgeStaleKeys();\n\t\t//noinspection unchecked\n\t\treturn this.raw.get(ofKey((K) key, null));\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\tthis.purgeStaleKeys();\n\t\t//noinspection unchecked\n\t\treturn this.raw.containsKey(ofKey((K) key, null));\n\t}\n\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.containsValue(value);\n\t}\n\n\t@Override\n\tpublic V put(K key, V value) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.put(ofKey(key, this.lastQueue), value);\n\t}\n\n\t@Override\n\tpublic V putIfAbsent(K key, V value) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.putIfAbsent(ofKey(key, this.lastQueue), value);\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends K, ? extends V> m) {\n\t\tm.forEach(this::put);\n\t}\n\n\t@Override\n\tpublic V replace(K key, V value) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.replace(ofKey(key, this.lastQueue), value);\n\t}\n\n\t@Override\n\tpublic boolean replace(K key, V oldValue, V newValue) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.replace(ofKey(key, this.lastQueue), oldValue, newValue);\n\t}\n\n\t@Override\n\tpublic void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {\n\t\tthis.purgeStaleKeys();\n\t\tthis.raw.replaceAll((kWeakKey, value) -> function.apply(kWeakKey.get(), value));\n\t}\n\n\t@Override\n\tpublic V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.computeIfAbsent(ofKey(key, this.lastQueue), kWeakKey -> mappingFunction.apply(key));\n\t}\n\n\t@Override\n\tpublic V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.computeIfPresent(ofKey(key, this.lastQueue), (kWeakKey, value) -> remappingFunction.apply(key, value));\n\t}\n\n\t/**\n\t * 从缓存中获得对象，当对象不在缓存中或已经过期返回Func0回调产生的对象\n\t *\n\t * @param key      键\n\t * @param supplier 如果不存在回调方法，用于生产值对象\n\t * @return 值对象\n\t */\n\tpublic V computeIfAbsent(K key, Func0<? extends V> supplier) {\n\t\treturn computeIfAbsent(key, (keyParam) -> supplier.callWithRuntimeException());\n\t}\n\n\t@Override\n\tpublic V remove(Object key) {\n\t\tthis.purgeStaleKeys();\n\t\t//noinspection unchecked\n\t\treturn this.raw.remove(ofKey((K) key, null));\n\t}\n\n\t@Override\n\tpublic boolean remove(Object key, Object value) {\n\t\tthis.purgeStaleKeys();\n\t\t//noinspection unchecked\n\t\treturn this.raw.remove(ofKey((K) key, null), value);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tthis.raw.clear();\n\t\t//noinspection StatementWithEmptyBody\n\t\twhile (lastQueue.poll() != null) ;\n\t}\n\n\t@Override\n\tpublic Set<K> keySet() {\n\t\tthis.purgeStaleKeys();\n\t\t// TODO 非高效方式的set转换，应该返回一个view\n\t\tfinal Collection<K> trans = CollUtil.trans(this.raw.keySet(), (reference) -> null == reference ? null : reference.get());\n\t\treturn new HashSet<>(trans);\n\t}\n\n\t@Override\n\tpublic Collection<V> values() {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.values();\n\t}\n\n\t@Override\n\tpublic Set<Entry<K, V>> entrySet() {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.entrySet().stream()\n\t\t\t\t.map(entry -> new AbstractMap.SimpleImmutableEntry<>(entry.getKey().get(), entry.getValue()))\n\t\t\t\t.collect(Collectors.toSet());\n\t}\n\n\t@Override\n\tpublic void forEach(BiConsumer<? super K, ? super V> action) {\n\t\tthis.purgeStaleKeys();\n\t\tthis.raw.forEach((key, value)-> action.accept(key.get(), value));\n\t}\n\n\t@Override\n\tpublic Iterator<Entry<K, V>> iterator() {\n\t\treturn entrySet().iterator();\n\t}\n\n\t@Override\n\tpublic V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.compute(ofKey(key, this.lastQueue), (kWeakKey, value) -> remappingFunction.apply(key, value));\n\t}\n\n\t@Override\n\tpublic V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {\n\t\tthis.purgeStaleKeys();\n\t\treturn this.raw.merge(ofKey(key, this.lastQueue), value, remappingFunction);\n\t}\n\n\t/**\n\t * 清除被回收的键\n\t */\n\tprivate void purgeStaleKeys() {\n\t\tReference<? extends K> reference;\n\t\tV value;\n\t\twhile ((reference = this.lastQueue.poll()) != null) {\n\t\t\tvalue = this.raw.remove(reference);\n\t\t\tif (null != purgeListener) {\n\t\t\t\tpurgeListener.accept(reference, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 根据Reference类型构建key对应的{@link Reference}\n\t *\n\t * @param key   键\n\t * @param queue {@link ReferenceQueue}\n\t * @return {@link Reference}\n\t */\n\tprivate Reference<K> ofKey(K key, ReferenceQueue<? super K> queue) {\n\t\tswitch (keyType) {\n\t\t\tcase WEAK:\n\t\t\t\treturn new WeakKey<>(key, queue);\n\t\t\tcase SOFT:\n\t\t\t\treturn new SoftKey<>(key, queue);\n\t\t}\n\t\tthrow new IllegalArgumentException(\"Unsupported key type: \" + keyType);\n\t}\n\n\t/**\n\t * 弱键\n\t *\n\t * @param <K> 键类型\n\t */\n\tprivate static class WeakKey<K> extends WeakReference<K> {\n\t\tprivate final int hashCode;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param key   原始Key，不能为{@code null}\n\t\t * @param queue {@link ReferenceQueue}\n\t\t */\n\t\tWeakKey(K key, ReferenceQueue<? super K> queue) {\n\t\t\tsuper(key, queue);\n\t\t\thashCode = key.hashCode();\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn hashCode;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object other) {\n\t\t\tif (other == this) {\n\t\t\t\treturn true;\n\t\t\t} else if (other instanceof WeakKey) {\n\t\t\t\treturn ObjectUtil.equals(((WeakKey<?>) other).get(), get());\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 弱键\n\t *\n\t * @param <K> 键类型\n\t */\n\tprivate static class SoftKey<K> extends SoftReference<K> {\n\t\tprivate final int hashCode;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param key   原始Key，不能为{@code null}\n\t\t * @param queue {@link ReferenceQueue}\n\t\t */\n\t\tSoftKey(K key, ReferenceQueue<? super K> queue) {\n\t\t\tsuper(key, queue);\n\t\t\thashCode = key.hashCode();\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn hashCode;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object other) {\n\t\t\tif (other == this) {\n\t\t\t\treturn true;\n\t\t\t} else if (other instanceof SoftKey) {\n\t\t\t\treturn ObjectUtil.equals(((SoftKey<?>) other).get(), get());\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/SafeConcurrentHashMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.JdkUtil;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Function;\n\n/**\n * 安全的ConcurrentHashMap实现<br>\n * 此类用于解决在JDK8中调用{@link ConcurrentHashMap#computeIfAbsent(Object, Function)}可能造成的死循环问题。<br>\n * 方法来自Dubbo，见：issues#2349<br>\n * <p>\n * 相关bug见：@see <a href=\"https://bugs.openjdk.java.net/browse/JDK-8161372\">https://bugs.openjdk.java.net/browse/JDK-8161372</a>\n *\n * @param <K> 键类型\n * @param <V> 值类型\n */\npublic class SafeConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// region == 构造 ==\n\n\t/**\n\t * 构造，默认初始大小（16）\n\t */\n\tpublic SafeConcurrentHashMap() {\n\t\tsuper();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 预估初始大小\n\t */\n\tpublic SafeConcurrentHashMap(int initialCapacity) {\n\t\tsuper(initialCapacity);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m 初始键值对\n\t */\n\tpublic SafeConcurrentHashMap(Map<? extends K, ? extends V> m) {\n\t\tsuper(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param loadFactor      增长系数\n\t */\n\tpublic SafeConcurrentHashMap(int initialCapacity, float loadFactor) {\n\t\tsuper(initialCapacity, loadFactor);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity  初始容量\n\t * @param loadFactor       增长系数\n\t * @param concurrencyLevel 并发级别，即Segment的个数\n\t */\n\tpublic SafeConcurrentHashMap(int initialCapacity,\n\t\t\t\t\t\t\t\t float loadFactor, int concurrencyLevel) {\n\t\tsuper(initialCapacity, loadFactor, concurrencyLevel);\n\t}\n\t// endregion == 构造 ==\n\n\t@Override\n\tpublic V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {\n\t\tif (JdkUtil.IS_JDK8) {\n\t\t\treturn MapUtil.computeIfAbsentForJdk8(this, key, mappingFunction);\n\t\t} else {\n\t\t\treturn super.computeIfAbsent(key, mappingFunction);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/TableMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\n\n/**\n * 可重复键和值的Map<br>\n * 通过键值单独建立List方式，使键值对一一对应，实现正向和反向两种查找<br>\n * 无论是正向还是反向，都是遍历列表查找过程，相比标准的HashMap要慢，数据越多越慢\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n */\npublic class TableMap<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int DEFAULT_CAPACITY = 10;\n\n\tprivate final List<K> keys;\n\tprivate final List<V> values;\n\n\t/**\n\t * 构造\n\t */\n\tpublic TableMap() {\n\t\tthis(DEFAULT_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param size 初始容量\n\t */\n\tpublic TableMap(int size) {\n\t\tthis.keys = new ArrayList<>(size);\n\t\tthis.values = new ArrayList<>(size);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param keys   键列表\n\t * @param values 值列表\n\t */\n\tpublic TableMap(K[] keys, V[] values) {\n\t\tthis.keys = CollUtil.toList(keys);\n\t\tthis.values = CollUtil.toList(values);\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn keys.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn CollUtil.isEmpty(keys);\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\t//noinspection SuspiciousMethodCalls\n\t\treturn keys.contains(key);\n\t}\n\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\t//noinspection SuspiciousMethodCalls\n\t\treturn values.contains(value);\n\t}\n\n\t@Override\n\tpublic V get(Object key) {\n\t\t//noinspection SuspiciousMethodCalls\n\t\tfinal int index = keys.indexOf(key);\n\t\tif (index > -1) {\n\t\t\treturn values.get(index);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据value获得对应的key，只返回找到的第一个value对应的key值\n\t *\n\t * @param value 值\n\t * @return 键\n\t * @since 5.3.3\n\t */\n\tpublic K getKey(V value) {\n\t\tfinal int index = values.indexOf(value);\n\t\tif (index > -1) {\n\t\t\treturn keys.get(index);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定key对应的所有值\n\t *\n\t * @param key 键\n\t * @return 值列表\n\t * @since 5.2.5\n\t */\n\tpublic List<V> getValues(K key) {\n\t\treturn CollUtil.getAny(\n\t\t\t\tthis.values,\n\t\t\t\tListUtil.indexOfAll(this.keys, (ele) -> ObjectUtil.equal(ele, key))\n\t\t);\n\t}\n\n\t/**\n\t * 获取指定value对应的所有key\n\t *\n\t * @param value 值\n\t * @return 值列表\n\t * @since 5.2.5\n\t */\n\tpublic List<K> getKeys(V value) {\n\t\treturn CollUtil.getAny(\n\t\t\t\tthis.keys,\n\t\t\t\tListUtil.indexOfAll(this.values, (ele) -> ObjectUtil.equal(ele, value))\n\t\t);\n\t}\n\n\t@Override\n\tpublic V put(K key, V value) {\n\t\tkeys.add(key);\n\t\tvalues.add(value);\n\t\treturn null;\n\t}\n\n\t/**\n\t * 移除指定的所有键和对应的所有值\n\t *\n\t * @param key 键\n\t * @return 最后一个移除的值\n\t */\n\t@Override\n\tpublic V remove(Object key) {\n\t\tV lastValue = null;\n\t\tint index;\n\t\t//noinspection SuspiciousMethodCalls\n\t\twhile ((index = keys.indexOf(key)) > -1) {\n\t\t\tlastValue = removeByIndex(index);\n\t\t}\n\t\treturn lastValue;\n\t}\n\n\t/**\n\t * 移除指定位置的键值对\n\t *\n\t * @param index 位置，不能越界\n\t * @return 移除的值\n\t */\n\tpublic V removeByIndex(final int index) {\n\t\tkeys.remove(index);\n\t\treturn values.remove(index);\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends K, ? extends V> m) {\n\t\tfor (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {\n\t\t\tthis.put(entry.getKey(), entry.getValue());\n\t\t}\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tkeys.clear();\n\t\tvalues.clear();\n\t}\n\n\t@Override\n\tpublic Set<K> keySet() {\n\t\treturn new HashSet<>(this.keys);\n\t}\n\n\t/**\n\t * 获取所有键，可重复，不可修改\n\t *\n\t * @return 键列表\n\t * @since 5.8.0\n\t */\n\tpublic List<K> keys() {\n\t\treturn Collections.unmodifiableList(this.keys);\n\t}\n\n\t@Override\n\tpublic Collection<V> values() {\n\t\treturn Collections.unmodifiableList(this.values);\n\t}\n\n\t@Override\n\tpublic Set<Map.Entry<K, V>> entrySet() {\n\t\tfinal Set<Map.Entry<K, V>> hashSet = new LinkedHashSet<>();\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\thashSet.add(MapUtil.entry(keys.get(i), values.get(i)));\n\t\t}\n\t\treturn hashSet;\n\t}\n\n\t@Override\n\tpublic Iterator<Map.Entry<K, V>> iterator() {\n\t\treturn new Iterator<Map.Entry<K, V>>() {\n\t\t\tprivate final Iterator<K> keysIter = keys.iterator();\n\t\t\tprivate final Iterator<V> valuesIter = values.iterator();\n\n\t\t\t@Override\n\t\t\tpublic boolean hasNext() {\n\t\t\t\treturn keysIter.hasNext() && valuesIter.hasNext();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Map.Entry<K, V> next() {\n\t\t\t\treturn MapUtil.entry(keysIter.next(), valuesIter.next());\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void remove() {\n\t\t\t\tkeysIter.remove();\n\t\t\t\tvaluesIter.remove();\n\t\t\t}\n\t\t};\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"TableMap{\" +\n\t\t\t\t\"keys=\" + keys +\n\t\t\t\t\", values=\" + values +\n\t\t\t\t'}';\n\t}\n\n\t@Override\n\tpublic void forEach(final BiConsumer<? super K, ? super V> action) {\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\taction.accept(keys.get(i), values.get(i));\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean remove(final Object key, final Object value) {\n\t\tboolean removed = false;\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tif (ObjUtil.equals(key, keys.get(i)) && ObjUtil.equals(value, values.get(i))) {\n\t\t\t\tremoveByIndex(i);\n\t\t\t\tremoved = true;\n\t\t\t\t// 移除当前元素，下个元素前移\n\t\t\t\ti--;\n\t\t\t}\n\t\t}\n\t\treturn removed;\n\t}\n\n\t@Override\n\tpublic void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tfinal V newValue = function.apply(keys.get(i), values.get(i));\n\t\t\tvalues.set(i, newValue);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean replace(final K key, final V oldValue, final V newValue) {\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tif (ObjUtil.equals(key, keys.get(i)) && ObjUtil.equals(oldValue, values.get(i))) {\n\t\t\t\tvalues.set(i, newValue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 替换指定key的所有值为指定值\n\t *\n\t * @param key   指定的key\n\t * @param value 替换的值\n\t * @return 最后替换的值\n\t */\n\t@Override\n\tpublic V replace(final K key, final V value) {\n\t\tV lastValue = null;\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tif (ObjUtil.equals(key, keys.get(i))) {\n\t\t\t\tlastValue = values.set(i, value);\n\t\t\t}\n\t\t}\n\t\treturn lastValue;\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tif(null == remappingFunction){\n\t\t\treturn null;\n\t\t}\n\n\t\tV lastValue = null;\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tif (ObjUtil.equals(key, keys.get(i))) {\n\t\t\t\tfinal V newValue = remappingFunction.apply(key, values.get(i));\n\t\t\t\tif(null != newValue){\n\t\t\t\t\tlastValue = values.set(i, newValue);\n\t\t\t\t} else{\n\t\t\t\t\tremoveByIndex(i);\n\t\t\t\t\t// 移除当前元素，下个元素前移\n\t\t\t\t\ti--;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn lastValue;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/TolerantMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * 一个可以提供默认值的Map\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author pantao, looly\n */\npublic class TolerantMap<K, V> extends MapWrapper<K, V> {\n\tprivate static final long serialVersionUID = -4158133823263496197L;\n\n\tprivate final V defaultValue;\n\n\t/**\n\t * 构造\n\t *\n\t * @param defaultValue 默认值\n\t */\n\tpublic TolerantMap(V defaultValue) {\n\t\tthis(new HashMap<>(), defaultValue);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param loadFactor      增长因子\n\t * @param defaultValue    默认值\n\t */\n\tpublic TolerantMap(int initialCapacity, float loadFactor, V defaultValue) {\n\t\tthis(new HashMap<>(initialCapacity, loadFactor), defaultValue);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t * @param defaultValue    默认值\n\t */\n\tpublic TolerantMap(int initialCapacity, V defaultValue) {\n\t\tthis(new HashMap<>(initialCapacity), defaultValue);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param map          Map实现\n\t * @param defaultValue 默认值\n\t */\n\tpublic TolerantMap(Map<K, V> map, V defaultValue) {\n\t\tsuper(map);\n\t\tthis.defaultValue = defaultValue;\n\t}\n\n\t/**\n\t * 构建TolerantMap\n\t *\n\t * @param map          map实现\n\t * @param defaultValue 默认值\n\t * @param <K>          键类型\n\t * @param <V>          值类型\n\t * @return TolerantMap\n\t */\n\tpublic static <K, V> TolerantMap<K, V> of(Map<K, V> map, V defaultValue) {\n\t\treturn new TolerantMap<>(map, defaultValue);\n\t}\n\n\t@Override\n\tpublic V get(Object key) {\n\t\treturn getOrDefault(key, defaultValue);\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (false == super.equals(o)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal TolerantMap<?, ?> that = (TolerantMap<?, ?>) o;\n\t\treturn getRaw().equals(that.getRaw())\n\t\t\t\t&& Objects.equals(defaultValue, that.defaultValue);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(getRaw(), defaultValue);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"TolerantMap{\" + \"map=\" + getRaw() + \", defaultValue=\" + defaultValue + '}';\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/TransMap.java",
    "content": "package cn.hutool.core.map;\n\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * 自定义键和值转换的的Map<br>\n * 继承此类后，通过实现{@link #customKey(Object)}和{@link #customValue(Object)}，按照给定规则加入到map或获取值。\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.0\n */\npublic abstract class TransMap<K, V> extends MapWrapper<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造<br>\n\t * 通过传入一个Map从而确定Map的类型，子类需创建一个空的Map，而非传入一个已有Map，否则值可能会被修改\n\t *\n\t * @param mapFactory 空Map创建工厂\n\t * @since 5.8.0\n\t */\n\tpublic TransMap(Supplier<Map<K, V>> mapFactory) {\n\t\tsuper(mapFactory);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 通过传入一个Map从而确定Map的类型，子类需创建一个空的Map，而非传入一个已有Map，否则值可能会被修改\n\t *\n\t * @param emptyMap Map 被包装的Map，必须为空Map，否则自定义key会无效\n\t * @since 3.1.2\n\t */\n\tpublic TransMap(Map<K, V> emptyMap) {\n\t\tsuper(emptyMap);\n\t}\n\n\t@Override\n\tpublic V get(Object key) {\n\t\treturn super.get(customKey(key));\n\t}\n\n\t@Override\n\tpublic V put(K key, V value) {\n\t\treturn super.put(customKey(key), customValue(value));\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends K, ? extends V> m) {\n\t\tm.forEach(this::put);\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn super.containsKey(customKey(key));\n\t}\n\n\t@Override\n\tpublic V remove(Object key) {\n\t\treturn super.remove(customKey(key));\n\t}\n\n\t@Override\n\tpublic boolean remove(Object key, Object value) {\n\t\treturn super.remove(customKey(key), customValue(value));\n\t}\n\n\t@Override\n\tpublic boolean replace(K key, V oldValue, V newValue) {\n\t\treturn super.replace(customKey(key), customValue(oldValue), customValue(newValue));\n\t}\n\n\t@Override\n\tpublic V replace(K key, V value) {\n\t\treturn super.replace(customKey(key), customValue(value));\n\t}\n\n\t//---------------------------------------------------------------------------- Override default methods start\n\t@Override\n\tpublic V getOrDefault(Object key, V defaultValue) {\n\t\treturn super.getOrDefault(customKey(key), customValue(defaultValue));\n\t}\n\n\t@Override\n\tpublic V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\treturn super.computeIfPresent(customKey(key), (k, v) -> remappingFunction.apply(k, customValue(v)));\n\t}\n\n\t@Override\n\tpublic V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\treturn super.compute(customKey(key), (k, v) -> remappingFunction.apply(k, customValue(v)));\n\t}\n\n\t@Override\n\tpublic V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {\n\t\treturn super.merge(customKey(key), customValue(value), remappingFunction);\n\t}\n\n\t@Override\n\tpublic V putIfAbsent(K key, V value) {\n\t\treturn super.putIfAbsent(customKey(key), customValue(value));\n\t}\n\n\t@Override\n\tpublic V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {\n\t\treturn super.computeIfAbsent(customKey(key), k -> customValue(mappingFunction.apply(k)));\n\t}\n\t//---------------------------------------------------------------------------- Override default methods end\n\n\t/**\n\t * 自定义键\n\t *\n\t * @param key KEY\n\t * @return 自定义KEY\n\t */\n\tprotected abstract K customKey(Object key);\n\n\t/**\n\t * 自定义值\n\t *\n\t * @param value 值\n\t * @return 自定义值\n\t */\n\tprotected abstract V customValue(Object value);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/TreeEntry.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * 允许拥有一个父节点与多个子节点的{@link Map.Entry}实现，\n * 表示一个以key作为唯一标识，并且可以挂载一个对应值的树节点，\n * 提供一些基于该节点对其所在树结构进行访问的方法\n *\n * @param <V> 节点的key类型\n * @param <K> 节点的value类型\n * @author huangchengxing\n * @see ForestMap\n */\npublic interface TreeEntry<K, V> extends Map.Entry<K, V> {\n\n\t// ===================== Entry方法的重定义 =====================\n\n\t/**\n\t * 比较目标对象与当前{@link TreeEntry}是否相等。<br>\n\t * 默认只要{@link TreeEntry#getKey()}的返回值相同，即认为两者相等\n\t *\n\t * @param o 目标对象\n\t * @return 是否\n\t */\n\t@Override\n\tboolean equals(Object o);\n\n\t/**\n\t * 返回当前{@link TreeEntry}的哈希值。<br>\n\t * 默认总是返回{@link TreeEntry#getKey()}的哈希值\n\t *\n\t * @return 哈希值\n\t */\n\t@Override\n\tint hashCode();\n\n\t// ===================== 父节点相关方法 =====================\n\n\t/**\n\t * 获取以当前节点作为叶子节点的树结构，然后获取当前节点与根节点的距离\n\t *\n\t * @return 当前节点与根节点的距离\n\t */\n\tint getWeight();\n\n\t/**\n\t * 获取以当前节点作为叶子节点的树结构，然后获取该树结构的根节点\n\t *\n\t * @return 根节点\n\t */\n\tTreeEntry<K, V> getRoot();\n\n\t/**\n\t * 当前节点是否存在直接关联的父节点\n\t *\n\t * @return 是否\n\t */\n\tdefault boolean hasParent() {\n\t\treturn ObjectUtil.isNotNull(getDeclaredParent());\n\t}\n\n\t/**\n\t * 获取当前节点直接关联的父节点\n\t *\n\t * @return 父节点，当节点不存在对应父节点时返回null\n\t */\n\tTreeEntry<K, V> getDeclaredParent();\n\n\t/**\n\t * 获取以当前节点作为叶子节点的树结构，然后获取该树结构中当前节点的指定父节点\n\t *\n\t * @param key 指定父节点的key\n\t * @return 指定父节点，当不存在时返回null\n\t */\n\tTreeEntry<K, V> getParent(K key);\n\n\t/**\n\t * 获取以当前节点作为叶子节点的树结构，然后确认该树结构中当前节点是否存在指定父节点\n\t *\n\t * @param key 指定父节点的key\n\t * @return 是否\n\t */\n\tdefault boolean containsParent(K key) {\n\t\treturn ObjectUtil.isNotNull(getParent(key));\n\t}\n\n\t// ===================== 子节点相关方法 =====================\n\n\t/**\n\t * 获取以当前节点作为根节点的树结构，然后遍历所有节点\n\t *\n\t * @param includeSelf 是否处理当前节点\n\t * @param nodeConsumer 对节点的处理\n\t */\n\tvoid forEachChild(boolean includeSelf, Consumer<TreeEntry<K, V>> nodeConsumer);\n\n\t/**\n\t * 获取当前节点直接关联的子节点\n\t *\n\t * @return 节点\n\t */\n\tMap<K, TreeEntry<K, V>> getDeclaredChildren();\n\n\t/**\n\t * 获取以当前节点作为根节点的树结构，然后获取该树结构中的当前节点的全部子节点\n\t *\n\t * @return 节点\n\t */\n\tMap<K, TreeEntry<K, V>> getChildren();\n\n\t/**\n\t * 当前节点是否有子节点\n\t *\n\t * @return 是否\n\t */\n\tdefault boolean hasChildren() {\n\t\treturn CollUtil.isNotEmpty(getDeclaredChildren());\n\t}\n\n\t/**\n\t * 获取以当前节点作为根节点的树结构，然后获取该树结构中的当前节点的指定子节点\n\t *\n\t * @param key 指定子节点的key\n\t * @return 节点\n\t */\n\tTreeEntry<K, V> getChild(K key);\n\n\t/**\n\t * 获取以当前节点作为根节点的树结构，然后确认该树结构中当前节点是否存在指定子节点\n\t *\n\t * @param key 指定子节点的key\n\t * @return 是否\n\t */\n\tdefault boolean containsChild(K key) {\n\t\treturn ObjectUtil.isNotNull(getChild(key));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/WeakConcurrentMap.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.ReferenceUtil;\n\nimport java.lang.ref.Reference;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * 线程安全的WeakMap实现<br>\n * 参考：jdk.management.resource.internal.WeakKeyConcurrentHashMap\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 5.8.0\n * @deprecated 请使用{@link cn.hutool.core.map.reference.WeakKeyConcurrentMap}\n */\npublic class WeakConcurrentMap<K, V> extends ReferenceConcurrentMap<K, V> {\n\n\t/**\n\t * 构造\n\t */\n\tpublic WeakConcurrentMap() {\n\t\tthis(new SafeConcurrentHashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link ConcurrentMap}实现\n\t */\n\tpublic WeakConcurrentMap(ConcurrentMap<Reference<K>, V> raw) {\n\t\tsuper(raw, ReferenceUtil.ReferenceType.WEAK);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/AbsCollValueMap.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.map.MapWrapper;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 值作为集合的Map实现，通过调用putValue可以在相同key时加入多个值，多个值用集合表示\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @param <C> 集合类型\n * @author looly\n * @since 5.7.4\n */\npublic abstract class AbsCollValueMap<K, V, C extends Collection<V>> extends MapWrapper<K, C> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 默认集合初始大小\n\t */\n\tprotected static final int DEFAULT_COLLECTION_INITIAL_CAPACITY = 3;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic AbsCollValueMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic AbsCollValueMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic AbsCollValueMap(Map<? extends K, C> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m          Map\n\t */\n\tpublic AbsCollValueMap(float loadFactor, Map<? extends K, C> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor      加载因子\n\t */\n\tpublic AbsCollValueMap(int initialCapacity, float loadFactor) {\n\t\tsuper(new HashMap<>(initialCapacity, loadFactor));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 放入所有value\n\t *\n\t * @param m valueMap\n\t * @since 5.7.4\n\t */\n\tpublic void putAllValues(Map<? extends K, ? extends Collection<V>> m) {\n\t\tif (null != m) {\n\t\t\tm.forEach((key, valueColl) -> {\n\t\t\t\tif (null != valueColl) {\n\t\t\t\t\tvalueColl.forEach((value) -> putValue(key, value));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * 放入Value<br>\n\t * 如果键对应值列表有值，加入，否则创建一个新列表后加入\n\t *\n\t * @param key   键\n\t * @param value 值\n\t */\n\tpublic void putValue(K key, V value) {\n\t\tC collection = this.get(key);\n\t\tif (null == collection) {\n\t\t\tcollection = createCollection();\n\t\t\tthis.put(key, collection);\n\t\t}\n\t\tcollection.add(value);\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @param key   键\n\t * @param index 第几个值的索引，越界返回null\n\t * @return 值或null\n\t */\n\tpublic V get(K key, int index) {\n\t\tfinal Collection<V> collection = get(key);\n\t\treturn CollUtil.get(collection, index);\n\t}\n\n\t/**\n\t * 移除value集合中的某个值\n\t *\n\t * @param key   键\n\t * @param value 集合中的某个值\n\t * @return 是否删除成功\n\t */\n\tpublic boolean removeValue(K key, V value) {\n\t\tC collection = this.get(key);\n\t\treturn null != collection && collection.remove(value);\n\t}\n\n\t/**\n\t * 移除value集合中的某些值\n\t *\n\t * @param key   键\n\t * @param values 集合中的某些值\n\t * @return 是否删除成功\n\t */\n\tpublic boolean removeValues(K key, Collection<V> values) {\n\t\tC collection = this.get(key);\n\t\treturn null != collection && collection.removeAll(values);\n\t}\n\n\t/**\n\t * 创建集合<br>\n\t * 此方法用于创建在putValue后追加值所在的集合，子类实现此方法创建不同类型的集合\n\t *\n\t * @return {@link Collection}\n\t */\n\tprotected abstract C createCollection();\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/AbsTable.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.TransIter;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Serializable;\nimport java.util.AbstractCollection;\nimport java.util.AbstractSet;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * 抽象{@link Table}接口实现<br>\n * 默认实现了：\n * <ul>\n *     <li>{@link #equals(Object)}</li>\n *     <li>{@link #hashCode()}</li>\n *     <li>{@link #toString()}</li>\n *     <li>{@link #values()}</li>\n *     <li>{@link #cellSet()}</li>\n *     <li>{@link #iterator()}</li>\n * </ul>\n *\n * @param <R> 行类型\n * @param <C> 列类型\n * @param <V> 值类型\n * @author Guava, Looly\n * @since 5.7.23\n */\npublic abstract class AbsTable<R, C, V> implements Table<R, C, V> {\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (obj == this) {\n\t\t\treturn true;\n\t\t} else if (obj instanceof Table) {\n\t\t\tfinal Table<?, ?, ?> that = (Table<?, ?, ?>) obj;\n\t\t\treturn this.cellSet().equals(that.cellSet());\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn cellSet().hashCode();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn rowMap().toString();\n\t}\n\n\t//region values\n\t@Override\n\tpublic Collection<V> values() {\n\t\tCollection<V> result = values;\n\t\treturn (result == null) ? values = new Values() : result;\n\t}\n\n\tprivate Collection<V> values;\n\tprivate class Values extends AbstractCollection<V> {\n\t\t@Override\n\t\tpublic Iterator<V> iterator() {\n\t\t\treturn new TransIter<>(cellSet().iterator(), Cell::getValue);\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean contains(Object o) {\n\t\t\t//noinspection unchecked\n\t\t\treturn containsValue((V) o);\n\t\t}\n\n\t\t@Override\n\t\tpublic void clear() {\n\t\t\tAbsTable.this.clear();\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn AbsTable.this.size();\n\t\t}\n\t}\n\t//endregion\n\n\t//region cellSet\n\t@Override\n\tpublic Set<Cell<R, C, V>> cellSet() {\n\t\tSet<Cell<R, C, V>> result = cellSet;\n\t\treturn (result == null) ? cellSet = new CellSet() : result;\n\t}\n\n\tprivate Set<Cell<R, C, V>> cellSet;\n\n\tprivate class CellSet extends AbstractSet<Cell<R, C, V>> {\n\t\t@Override\n\t\tpublic boolean contains(Object o) {\n\t\t\tif (o instanceof Cell) {\n\t\t\t\t@SuppressWarnings(\"unchecked\") final Cell<R, C, V> cell = (Cell<R, C, V>) o;\n\t\t\t\tMap<C, V> row = getRow(cell.getRowKey());\n\t\t\t\tif (null != row) {\n\t\t\t\t\treturn ObjectUtil.equals(row.get(cell.getColumnKey()), cell.getValue());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean remove(Object o) {\n\t\t\tif (contains(o)) {\n\t\t\t\t@SuppressWarnings(\"unchecked\") final Cell<R, C, V> cell = (Cell<R, C, V>) o;\n\t\t\t\tAbsTable.this.remove(cell.getRowKey(), cell.getColumnKey());\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic void clear() {\n\t\t\tAbsTable.this.clear();\n\t\t}\n\n\t\t@Override\n\t\tpublic Iterator<Table.Cell<R, C, V>> iterator() {\n\t\t\treturn new AbsTable<R, C, V>.CellIterator();\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn AbsTable.this.size();\n\t\t}\n\t}\n\t//endregion\n\n\t//region iterator\n\t@Override\n\tpublic Iterator<Cell<R, C, V>> iterator() {\n\t\treturn new CellIterator();\n\t}\n\n\t/**\n\t * 基于{@link Cell}的{@link Iterator}实现\n\t */\n\tprivate class CellIterator implements Iterator<Cell<R, C, V>> {\n\t\tfinal Iterator<Map.Entry<R, Map<C, V>>> rowIterator = rowMap().entrySet().iterator();\n\t\tMap.Entry<R, Map<C, V>> rowEntry;\n\t\tIterator<Map.Entry<C, V>> columnIterator = IterUtil.empty();\n\n\t\t@Override\n\t\tpublic boolean hasNext() {\n\t\t\treturn rowIterator.hasNext() || columnIterator.hasNext();\n\t\t}\n\n\t\t@Override\n\t\tpublic Cell<R, C, V> next() {\n\t\t\tif (false == columnIterator.hasNext()) {\n\t\t\t\trowEntry = rowIterator.next();\n\t\t\t\tcolumnIterator = rowEntry.getValue().entrySet().iterator();\n\t\t\t}\n\t\t\tfinal Map.Entry<C, V> columnEntry = columnIterator.next();\n\t\t\treturn new SimpleCell<>(rowEntry.getKey(), columnEntry.getKey(), columnEntry.getValue());\n\t\t}\n\n\t\t@Override\n\t\tpublic void remove() {\n\t\t\tcolumnIterator.remove();\n\t\t\tif (rowEntry.getValue().isEmpty()) {\n\t\t\t\trowIterator.remove();\n\t\t\t}\n\t\t}\n\t}\n\t//endregion\n\n\t/**\n\t * 简单{@link Cell} 实现\n\t *\n\t * @param <R> 行类型\n\t * @param <C> 列类型\n\t * @param <V> 值类型\n\t */\n\tprivate static class SimpleCell<R, C, V> implements Cell<R, C, V>, Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate final R rowKey;\n\t\tprivate final C columnKey;\n\t\tprivate final V value;\n\n\t\tSimpleCell(R rowKey, C columnKey, V value) {\n\t\t\tthis.rowKey = rowKey;\n\t\t\tthis.columnKey = columnKey;\n\t\t\tthis.value = value;\n\t\t}\n\n\t\t@Override\n\t\tpublic R getRowKey() {\n\t\t\treturn rowKey;\n\t\t}\n\n\t\t@Override\n\t\tpublic C getColumnKey() {\n\t\t\treturn columnKey;\n\t\t}\n\n\t\t@Override\n\t\tpublic V getValue() {\n\t\t\treturn value;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object obj) {\n\t\t\tif (obj == this) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (obj instanceof Cell) {\n\t\t\t\tCell<?, ?, ?> other = (Cell<?, ?, ?>) obj;\n\t\t\t\treturn ObjectUtil.equal(rowKey, other.getRowKey())\n\t\t\t\t\t\t&& ObjectUtil.equal(columnKey, other.getColumnKey())\n\t\t\t\t\t\t&& ObjectUtil.equal(value, other.getValue());\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn Objects.hash(rowKey, columnKey, value);\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"(\" + rowKey + \",\" + columnKey + \")=\" + value;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport cn.hutool.core.lang.func.Func0;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 值作为集合的Map实现，通过调用putValue可以在相同key时加入多个值，多个值用集合表示<br>\n * 此类可以通过传入函数自定义集合类型的创建规则\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author looly\n * @since 4.3.3\n */\npublic class CollectionValueMap<K, V> extends AbsCollValueMap<K, V, Collection<V>> {\n\tprivate static final long serialVersionUID = 9012989578038102983L;\n\n\tprivate final Func0<Collection<V>> collectionCreateFunc;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic CollectionValueMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic CollectionValueMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic CollectionValueMap(Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m          Map\n\t */\n\tpublic CollectionValueMap(float loadFactor, Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(loadFactor, m, ArrayList::new);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor      加载因子\n\t */\n\tpublic CollectionValueMap(int initialCapacity, float loadFactor) {\n\t\tthis(initialCapacity, loadFactor, ArrayList::new);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor           加载因子\n\t * @param m                    Map\n\t * @param collectionCreateFunc Map中值的集合创建函数\n\t * @since 5.7.4\n\t */\n\tpublic CollectionValueMap(float loadFactor, Map<? extends K, ? extends Collection<V>> m, Func0<Collection<V>> collectionCreateFunc) {\n\t\tthis(m.size(), loadFactor, collectionCreateFunc);\n\t\tthis.putAll(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity      初始大小\n\t * @param loadFactor           加载因子\n\t * @param collectionCreateFunc Map中值的集合创建函数\n\t * @since 5.7.4\n\t */\n\tpublic CollectionValueMap(int initialCapacity, float loadFactor, Func0<Collection<V>> collectionCreateFunc) {\n\t\tsuper(new HashMap<>(initialCapacity, loadFactor));\n\t\tthis.collectionCreateFunc = collectionCreateFunc;\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tprotected Collection<V> createCollection() {\n\t\treturn collectionCreateFunc.callWithRuntimeException();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/ListValueMap.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 值作为集合List的Map实现，通过调用putValue可以在相同key时加入多个值，多个值用集合表示\n *\n * @author looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 4.3.3\n */\npublic class ListValueMap<K, V> extends AbsCollValueMap<K, V, List<V>> {\n\tprivate static final long serialVersionUID = 6044017508487827899L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic ListValueMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic ListValueMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic ListValueMap(Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m Map\n\t */\n\tpublic ListValueMap(float loadFactor, Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAllValues(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子\n\t */\n\tpublic ListValueMap(int initialCapacity, float loadFactor) {\n\t\tsuper(new HashMap<>(initialCapacity, loadFactor));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tprotected List<V> createCollection() {\n\t\treturn new ArrayList<>(DEFAULT_COLLECTION_INITIAL_CAPACITY);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/RowKeyTable.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.collection.ComputeIter;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.TransIter;\nimport cn.hutool.core.map.AbsEntry;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.util.AbstractMap;\nimport java.util.AbstractSet;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 将行的键作为主键的{@link Table}实现<br>\n * 此结构为: 行=(列=值)\n *\n * @param <R> 行类型\n * @param <C> 列类型\n * @param <V> 值类型\n * @author Guava, Looly\n * @since 5.7.23\n */\npublic class RowKeyTable<R, C, V> extends AbsTable<R, C, V> {\n\n\tfinal Map<R, Map<C, V>> raw;\n\t/**\n\t * 列的Map创建器，用于定义Table中Value对应Map类型\n\t */\n\tfinal Builder<? extends Map<C, V>> columnBuilder;\n\n\t//region 构造\n\n\t/**\n\t * 构造\n\t */\n\tpublic RowKeyTable() {\n\t\tthis(new HashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isLinked 是否有序，有序则使用{@link java.util.LinkedHashMap}作为原始Map\n\t * @since 5.8.0\n\t */\n\tpublic RowKeyTable(boolean isLinked) {\n\t\tthis(MapUtil.newHashMap(isLinked), () -> MapUtil.newHashMap(isLinked));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw 原始Map\n\t */\n\tpublic RowKeyTable(Map<R, Map<C, V>> raw) {\n\t\tthis(raw, HashMap::new);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw              原始Map\n\t * @param columnMapBuilder 列的map创建器\n\t */\n\tpublic RowKeyTable(Map<R, Map<C, V>> raw, Builder<? extends Map<C, V>> columnMapBuilder) {\n\t\tthis.raw = raw;\n\t\tthis.columnBuilder = null == columnMapBuilder ? HashMap::new : columnMapBuilder;\n\t}\n\t//endregion\n\n\t@Override\n\tpublic Map<R, Map<C, V>> rowMap() {\n\t\treturn raw;\n\t}\n\n\t@Override\n\tpublic V put(R rowKey, C columnKey, V value) {\n\t\treturn raw.computeIfAbsent(rowKey, (key) -> columnBuilder.build()).put(columnKey, value);\n\t}\n\n\t@Override\n\tpublic V remove(R rowKey, C columnKey) {\n\t\tfinal Map<C, V> map = getRow(rowKey);\n\t\tif (null == map) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal V value = map.remove(columnKey);\n\t\tif (map.isEmpty()) {\n\t\t\traw.remove(rowKey);\n\t\t}\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn raw.isEmpty();\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tthis.raw.clear();\n\t}\n\n\t@Override\n\tpublic boolean containsColumn(C columnKey) {\n\t\tif (columnKey == null) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (Map<C, V> map : raw.values()) {\n\t\t\tif (null != map && map.containsKey(columnKey)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t//region columnMap\n\t@Override\n\tpublic Map<C, Map<R, V>> columnMap() {\n\t\tMap<C, Map<R, V>> result = columnMap;\n\t\treturn (result == null) ? columnMap = new ColumnMap() : result;\n\t}\n\n\tprivate Map<C, Map<R, V>> columnMap;\n\n\tprivate class ColumnMap extends AbstractMap<C, Map<R, V>> {\n\t\t@Override\n\t\tpublic Set<Entry<C, Map<R, V>>> entrySet() {\n\t\t\treturn new ColumnMapEntrySet();\n\t\t}\n\t}\n\n\tprivate class ColumnMapEntrySet extends AbstractSet<Map.Entry<C, Map<R, V>>> {\n\t\tprivate final Set<C> columnKeySet = columnKeySet();\n\n\t\t@Override\n\t\tpublic Iterator<Map.Entry<C, Map<R, V>>> iterator() {\n\t\t\treturn new TransIter<>(columnKeySet.iterator(),\n\t\t\t\t\tc -> MapUtil.entry(c, getColumn(c)));\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn columnKeySet.size();\n\t\t}\n\t}\n\t//endregion\n\n\n\t//region columnKeySet\n\t@Override\n\tpublic Set<C> columnKeySet() {\n\t\tSet<C> result = columnKeySet;\n\t\treturn (result == null) ? columnKeySet = new ColumnKeySet() : result;\n\t}\n\n\tprivate Set<C> columnKeySet;\n\n\tprivate class ColumnKeySet extends AbstractSet<C> {\n\n\t\t@Override\n\t\tpublic Iterator<C> iterator() {\n\t\t\treturn new ColumnKeyIterator();\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn IterUtil.size(iterator());\n\t\t}\n\t}\n\n\tprivate class ColumnKeyIterator extends ComputeIter<C> {\n\t\tfinal Map<C, V> seen = columnBuilder.build();\n\t\tfinal Iterator<Map<C, V>> mapIterator = raw.values().iterator();\n\t\tIterator<Map.Entry<C, V>> entryIterator = IterUtil.empty();\n\n\t\t@Override\n\t\tprotected C computeNext() {\n\t\t\twhile (true) {\n\t\t\t\tif (entryIterator.hasNext()) {\n\t\t\t\t\tMap.Entry<C, V> entry = entryIterator.next();\n\t\t\t\t\tif (false == seen.containsKey(entry.getKey())) {\n\t\t\t\t\t\tseen.put(entry.getKey(), entry.getValue());\n\t\t\t\t\t\treturn entry.getKey();\n\t\t\t\t\t}\n\t\t\t\t} else if (mapIterator.hasNext()) {\n\t\t\t\t\tentryIterator = mapIterator.next().entrySet().iterator();\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t//endregion\n\n\t//region getColumn\n\t@Override\n\tpublic List<C> columnKeys() {\n\t\tfinal Collection<Map<C, V>> values = this.raw.values();\n\t\tfinal List<C> result = new ArrayList<>(values.size() * 16);\n\t\tfor (Map<C, V> map : values) {\n\t\t\tmap.forEach((key, value)->{result.add(key);});\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic Map<R, V> getColumn(C columnKey) {\n\t\treturn new Column(columnKey);\n\t}\n\n\tprivate class Column extends AbstractMap<R, V> {\n\t\tfinal C columnKey;\n\n\t\tColumn(C columnKey) {\n\t\t\tthis.columnKey = columnKey;\n\t\t}\n\n\t\t@Override\n\t\tpublic Set<Entry<R, V>> entrySet() {\n\t\t\treturn new EntrySet();\n\t\t}\n\n\t\tprivate class EntrySet extends AbstractSet<Map.Entry<R, V>> {\n\n\t\t\t@Override\n\t\t\tpublic Iterator<Map.Entry<R, V>> iterator() {\n\t\t\t\treturn new EntrySetIterator();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic int size() {\n\t\t\t\tint size = 0;\n\t\t\t\tfor (Map<C, V> map : raw.values()) {\n\t\t\t\t\tif (map.containsKey(columnKey)) {\n\t\t\t\t\t\tsize++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn size;\n\t\t\t}\n\t\t}\n\n\t\tprivate class EntrySetIterator extends ComputeIter<Entry<R, V>> {\n\t\t\tfinal Iterator<Entry<R, Map<C, V>>> iterator = raw.entrySet().iterator();\n\n\t\t\t@Override\n\t\t\tprotected Entry<R, V> computeNext() {\n\t\t\t\twhile (iterator.hasNext()) {\n\t\t\t\t\tfinal Entry<R, Map<C, V>> entry = iterator.next();\n\t\t\t\t\tif (entry.getValue().containsKey(columnKey)) {\n\t\t\t\t\t\treturn new AbsEntry<R, V>() {\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic R getKey() {\n\t\t\t\t\t\t\t\treturn entry.getKey();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic V getValue() {\n\t\t\t\t\t\t\t\treturn entry.getValue().get(columnKey);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic V setValue(V value) {\n\t\t\t\t\t\t\t\treturn entry.getValue().put(columnKey, value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t}\n\t//endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/SetValueMap.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 值作为集合Set（LinkedHashSet）的Map实现，通过调用putValue可以在相同key时加入多个值，多个值用集合表示\n *\n * @author looly\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @since 4.3.3\n */\npublic class SetValueMap<K, V> extends AbsCollValueMap<K, V, Set<V>> {\n\tprivate static final long serialVersionUID = 6044017508487827899L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic SetValueMap() {\n\t\tthis(DEFAULT_INITIAL_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t */\n\tpublic SetValueMap(int initialCapacity) {\n\t\tthis(initialCapacity, DEFAULT_LOAD_FACTOR);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param m Map\n\t */\n\tpublic SetValueMap(Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(DEFAULT_LOAD_FACTOR, m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param loadFactor 加载因子\n\t * @param m Map\n\t */\n\tpublic SetValueMap(float loadFactor, Map<? extends K, ? extends Collection<V>> m) {\n\t\tthis(m.size(), loadFactor);\n\t\tthis.putAllValues(m);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param loadFactor 加载因子\n\t */\n\tpublic SetValueMap(int initialCapacity, float loadFactor) {\n\t\tsuper(new HashMap<>(initialCapacity, loadFactor));\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tprotected Set<V> createCollection() {\n\t\treturn new LinkedHashSet<>(DEFAULT_COLLECTION_INITIAL_CAPACITY);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/Table.java",
    "content": "package cn.hutool.core.map.multi;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.lang.func.Consumer3;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 表格数据结构定义<br>\n * 此结构类似于Guava的Table接口，使用两个键映射到一个值，类似于表格结构。\n *\n * @param <R> 行键类型\n * @param <C> 列键类型\n * @param <V> 值类型\n * @since 5.7.23\n */\npublic interface Table<R, C, V> extends Iterable<Table.Cell<R, C, V>> {\n\n\t/**\n\t * 是否包含指定行列的映射<br>\n\t * 行和列任意一个不存在都会返回{@code false}，如果行和列都存在，值为{@code null}，也会返回{@code true}\n\t *\n\t * @param rowKey    行键\n\t * @param columnKey 列键\n\t * @return 是否包含映射\n\t */\n\tdefault boolean contains(R rowKey, C columnKey) {\n\t\treturn Opt.ofNullable(getRow(rowKey)).map((map) -> map.containsKey(columnKey))\n\t\t\t.orElse(false);\n\t}\n\n\t//region Row\n\n\t/**\n\t * 行是否存在\n\t *\n\t * @param rowKey 行键\n\t * @return 行是否存在\n\t */\n\tdefault boolean containsRow(R rowKey) {\n\t\treturn Opt.ofNullable(rowMap()).map((map) -> map.containsKey(rowKey)).get();\n\t}\n\n\t/**\n\t * 获取行\n\t *\n\t * @param rowKey 行键\n\t * @return 行映射，返回的键为列键，值为表格的值\n\t */\n\tdefault Map<C, V> getRow(R rowKey) {\n\t\treturn Opt.ofNullable(rowMap()).map((map) -> map.get(rowKey)).get();\n\t}\n\n\t/**\n\t * 返回所有行的key，行的key不可重复\n\t *\n\t * @return 行键\n\t */\n\tdefault Set<R> rowKeySet() {\n\t\treturn Opt.ofNullable(rowMap()).map(Map::keySet).get();\n\t}\n\n\t/**\n\t * 返回行列对应的Map\n\t *\n\t * @return map，键为行键，值为列和值的对应map\n\t */\n\tMap<R, Map<C, V>> rowMap();\n\t//endregion\n\n\t//region Column\n\n\t/**\n\t * 列是否存在\n\t *\n\t * @param columnKey 列键\n\t * @return 列是否存在\n\t */\n\tdefault boolean containsColumn(C columnKey) {\n\t\treturn Opt.ofNullable(columnMap()).map((map) -> map.containsKey(columnKey)).get();\n\t}\n\n\t/**\n\t * 获取列\n\t *\n\t * @param columnKey 列键\n\t * @return 列映射，返回的键为行键，值为表格的值\n\t */\n\tdefault Map<R, V> getColumn(C columnKey) {\n\t\treturn Opt.ofNullable(columnMap()).map((map) -> map.get(columnKey)).get();\n\t}\n\n\t/**\n\t * 返回所有列的key，列的key不可重复\n\t *\n\t * @return 列set\n\t */\n\tdefault Set<C> columnKeySet() {\n\t\treturn Opt.ofNullable(columnMap()).map(Map::keySet).get();\n\t}\n\n\t/**\n\t * 返回所有列的key，列的key如果实现Map是可重复key，则返回对应不去重的List。\n\t *\n\t * @return 列set\n\t * @since 5.8.0\n\t */\n\tdefault List<C> columnKeys() {\n\t\tfinal Map<C, Map<R, V>> columnMap = columnMap();\n\t\tif(MapUtil.isEmpty(columnMap)){\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal List<C> result = new ArrayList<>(columnMap.size());\n\t\tfor (Map.Entry<C, Map<R, V>> cMapEntry : columnMap.entrySet()) {\n\t\t\tresult.add(cMapEntry.getKey());\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 返回列-行对应的map\n\t *\n\t * @return map，键为列键，值为行和值的对应map\n\t */\n\tMap<C, Map<R, V>> columnMap();\n\t//endregion\n\n\t//region value\n\n\t/**\n\t * 指定值是否存在\n\t *\n\t * @param value 值\n\t * @return 值\n\t */\n\tdefault boolean containsValue(V value){\n\t\tfinal Collection<Map<C, V>> rows = Opt.ofNullable(rowMap()).map(Map::values).get();\n\t\tif(null != rows){\n\t\t\tfor (Map<C, V> row : rows) {\n\t\t\t\tif (row.containsValue(value)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取指定值\n\t *\n\t * @param rowKey    行键\n\t * @param columnKey 列键\n\t * @return 值，如果值不存在，返回{@code null}\n\t */\n\tdefault V get(R rowKey, C columnKey) {\n\t\treturn Opt.ofNullable(getRow(rowKey)).map((map) -> map.get(columnKey)).get();\n\t}\n\n\t/**\n\t * 所有行列值的集合\n\t *\n\t * @return 值的集合\n\t */\n\tCollection<V> values();\n\t//endregion\n\n\t/**\n\t * 所有单元格集合\n\t *\n\t * @return 单元格集合\n\t */\n\tSet<Cell<R, C, V>> cellSet();\n\n\t/**\n\t * 为表格指定行列赋值，如果不存在，创建之，存在则替换之，返回原值\n\t *\n\t * @param rowKey    行键\n\t * @param columnKey 列键\n\t * @param value     值\n\t * @return 原值，不存在返回{@code null}\n\t */\n\tV put(R rowKey, C columnKey, V value);\n\n\t/**\n\t * 批量加入\n\t *\n\t * @param table 其他table\n\t */\n\tdefault void putAll(Table<? extends R, ? extends C, ? extends V> table){\n\t\tif (null != table) {\n\t\t\tfor (Table.Cell<? extends R, ? extends C, ? extends V> cell : table.cellSet()) {\n\t\t\t\tput(cell.getRowKey(), cell.getColumnKey(), cell.getValue());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 移除指定值\n\t *\n\t * @param rowKey    行键\n\t * @param columnKey 列键\n\t * @return 移除的值，如果值不存在，返回{@code null}\n\t */\n\tV remove(R rowKey, C columnKey);\n\n\t/**\n\t * 表格是否为空\n\t *\n\t * @return 是否为空\n\t */\n\tboolean isEmpty();\n\n\t/**\n\t * 表格大小，一般为单元格的个数\n\t *\n\t * @return 表格大小\n\t */\n\tdefault int size(){\n\t\tfinal Map<R, Map<C, V>> rowMap = rowMap();\n\t\tif(MapUtil.isEmpty(rowMap)){\n\t\t\treturn 0;\n\t\t}\n\t\tint size = 0;\n\t\tfor (Map<C, V> map : rowMap.values()) {\n\t\t\tsize += map.size();\n\t\t}\n\t\treturn size;\n\t}\n\n\t/**\n\t * 清空表格\n\t */\n\tvoid clear();\n\n\t/**\n\t * 遍历表格的单元格，处理值\n\t *\n\t * @param consumer 单元格值处理器\n\t */\n\tdefault void forEach(Consumer3<? super R, ? super C, ? super V> consumer) {\n\t\tfor (Cell<R, C, V> cell : this) {\n\t\t\tconsumer.accept(cell.getRowKey(), cell.getColumnKey(), cell.getValue());\n\t\t}\n\t}\n\n\t/**\n\t * 单元格，用于表示一个单元格的行、列和值\n\t *\n\t * @param <R> 行键类型\n\t * @param <C> 列键类型\n\t * @param <V> 值类型\n\t */\n\tinterface Cell<R, C, V> {\n\t\t/**\n\t\t * 获取行键\n\t\t *\n\t\t * @return 行键\n\t\t */\n\t\tR getRowKey();\n\n\t\t/**\n\t\t * 获取列键\n\t\t *\n\t\t * @return 列键\n\t\t */\n\t\tC getColumnKey();\n\n\t\t/**\n\t\t * 获取值\n\t\t *\n\t\t * @return 值\n\t\t */\n\t\tV getValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java",
    "content": "/**\n * 多参数类型的Map实现，包括集合类型值的Map和Table\n *\n * @author looly\n *\n */\npackage cn.hutool.core.map.multi;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/package-info.java",
    "content": "/**\n * Map相关封装，提供特殊Map实现以及Map工具MapUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.map;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/reference/ReferenceConcurrentMap.java",
    "content": "/*\n * Copyright (c) 2013-2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.map.reference;\n\nimport cn.hutool.core.lang.ref.Ref;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ReferenceUtil;\n\nimport java.io.Serializable;\nimport java.lang.ref.Reference;\nimport java.lang.ref.ReferenceQueue;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\n/**\n * 线程安全的ReferenceMap实现\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.41\n */\npublic abstract class ReferenceConcurrentMap<K, V> implements ConcurrentMap<K, V>, Iterable<Map.Entry<K, V>>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 键值对引用\n\t */\n\tfinal ConcurrentMap<Ref<K>, Ref<V>> raw;\n\t/**\n\t * 因无强链接而清除的键队列\n\t */\n\tprivate final ReferenceQueue<K> lastKeyQueue;\n\t/**\n\t * 因无强链接而清除的值队列\n\t */\n\tprivate final ReferenceQueue<V> lastValueQueue;\n\t/**\n\t * 回收监听\n\t */\n\tprivate BiConsumer<Ref<? extends K>, Ref<? extends V>> purgeListener;\n\n\t// region 构造\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link ConcurrentMap}实现\n\t */\n\tpublic ReferenceConcurrentMap(final ConcurrentMap<Ref<K>, Ref<V>> raw) {\n\t\tthis.raw = raw;\n\t\tlastKeyQueue = new ReferenceQueue<>();\n\t\tlastValueQueue = new ReferenceQueue<>();\n\t}\n\t// endregion\n\n\t/**\n\t * 设置对象回收清除监听\n\t *\n\t * @param purgeListener 监听函数\n\t */\n\tpublic void setPurgeListener(final BiConsumer<Ref<? extends K>, Ref<? extends V>> purgeListener) {\n\t\tthis.purgeListener = purgeListener;\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\tthis.purgeStale();\n\t\treturn this.raw.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\tthis.purgeStale();\n\t\treturn this.raw.isEmpty();\n\t}\n\n\t@Override\n\tpublic V get(final Object key) {\n\t\tthis.purgeStale();\n\t\treturn unwrap(this.raw.get(wrapKey(key)));\n\t}\n\n\t@Override\n\tpublic boolean containsKey(final Object key) {\n\t\tthis.purgeStale();\n\t\treturn this.raw.containsKey(wrapKey(key));\n\t}\n\n\t@Override\n\tpublic boolean containsValue(final Object value) {\n\t\tthis.purgeStale();\n\t\treturn this.raw.containsValue(wrapValue(value));\n\t}\n\n\t@Override\n\tpublic V put(final K key, final V value) {\n\t\tthis.purgeStale();\n\t\tfinal Ref<V> vReference = this.raw.put(wrapKey(key), wrapValue(value));\n\t\treturn unwrap(vReference);\n\t}\n\n\t@Override\n\tpublic V putIfAbsent(final K key, final V value) {\n\t\tthis.purgeStale();\n\t\tfinal Ref<V> vReference = this.raw.putIfAbsent(wrapKey(key), wrapValue(value));\n\t\treturn unwrap(vReference);\n\t}\n\n\t@Override\n\tpublic void putAll(final Map<? extends K, ? extends V> m) {\n\t\tm.forEach(this::put);\n\t}\n\n\t@Override\n\tpublic V replace(final K key, final V value) {\n\t\tthis.purgeStale();\n\t\tfinal Ref<V> vReference = this.raw.replace(wrapKey(key), wrapValue(value));\n\t\treturn unwrap(vReference);\n\t}\n\n\t@Override\n\tpublic boolean replace(final K key, final V oldValue, final V newValue) {\n\t\tthis.purgeStale();\n\t\treturn this.raw.replace(wrapKey(key), wrapValue(oldValue), wrapValue(newValue));\n\t}\n\n\t@Override\n\tpublic void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {\n\t\tthis.purgeStale();\n\t\tthis.raw.replaceAll((rKey, rValue) -> wrapValue(function.apply(unwrap(rKey), unwrap(rValue))));\n\t}\n\n\t@Override\n\tpublic V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {\n\t\tV result = null;\n\t\twhile(null == result){\n\t\t\tthis.purgeStale();\n\t\t\tfinal Ref<V> vReference = this.raw.computeIfAbsent(wrapKey(key),\n\t\t\t\tkReference -> wrapValue(mappingFunction.apply(unwrap(kReference))));\n\n\t\t\t// issue#IA5GMH 如果vReference在此时被GC回收，则unwrap后为null，需要循环计算\n\t\t\t// 但是当用户提供的值本身为null，则直接返回之\n\t\t\tif(NullRef.NULL == vReference){\n\t\t\t\t// 用户提供的值本身为null\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tresult = unwrap(vReference);\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tV result = null;\n\t\twhile(null == result){\n\t\t\tthis.purgeStale();\n\t\t\tfinal Ref<V> vReference = this.raw.computeIfPresent(wrapKey(key),\n\t\t\t\t(kReference, vReference1) -> wrapValue(remappingFunction.apply(unwrap(kReference), unwrap(vReference1))));\n\n\t\t\t// issue#IA5GMH 如果vReference在此时被GC回收，则unwrap后为null，需要循环计算\n\t\t\t// 但是当用户提供的值本身为null，则直接返回之\n\t\t\tif(NullRef.NULL == vReference){\n\t\t\t\t// 用户提供的值本身为null\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tresult = unwrap(vReference);\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic V remove(final Object key) {\n\t\tthis.purgeStale();\n\t\treturn unwrap(this.raw.remove(wrapKey(key)));\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic boolean remove(final Object key, final Object value) {\n\t\tthis.purgeStale();\n\t\treturn this.raw.remove(wrapKey((K) key, null), value);\n\t}\n\n\t@SuppressWarnings(\"StatementWithEmptyBody\")\n\t@Override\n\tpublic void clear() {\n\t\tthis.raw.clear();\n\t\twhile (lastKeyQueue.poll() != null) ;\n\t\twhile (lastValueQueue.poll() != null) ;\n\t}\n\n\t@Override\n\tpublic Set<K> keySet() {\n\t\tthis.purgeStale();\n\t\tfinal Set<Ref<K>> referenceSet = this.raw.keySet();\n\t\treturn new AbstractSet<K>() {\n\t\t\t@Override\n\t\t\tpublic Iterator<K> iterator() {\n\t\t\t\tfinal Iterator<Ref<K>> referenceIter = referenceSet.iterator();\n\t\t\t\treturn new Iterator<K>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic boolean hasNext() {\n\t\t\t\t\t\treturn referenceIter.hasNext();\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic K next() {\n\t\t\t\t\t\treturn unwrap(referenceIter.next());\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic int size() {\n\t\t\t\treturn referenceSet.size();\n\t\t\t}\n\t\t};\n\t}\n\n\t@Override\n\tpublic Collection<V> values() {\n\t\tthis.purgeStale();\n\t\tfinal Collection<Ref<V>> referenceValues = this.raw.values();\n\t\treturn new AbstractCollection<V>() {\n\t\t\t@Override\n\t\t\tpublic Iterator<V> iterator() {\n\t\t\t\tfinal Iterator<Ref<V>> referenceIter = referenceValues.iterator();\n\t\t\t\treturn new Iterator<V>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic boolean hasNext() {\n\t\t\t\t\t\treturn referenceIter.hasNext();\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic V next() {\n\t\t\t\t\t\treturn unwrap(referenceIter.next());\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic int size() {\n\t\t\t\treturn referenceValues.size();\n\t\t\t}\n\t\t};\n\t}\n\n\t@Override\n\tpublic Set<Entry<K, V>> entrySet() {\n\t\tthis.purgeStale();\n\t\tfinal Set<Entry<Ref<K>, Ref<V>>> referenceEntrySet = this.raw.entrySet();\n\t\treturn new AbstractSet<Entry<K, V>>() {\n\t\t\t@Override\n\t\t\tpublic Iterator<Entry<K, V>> iterator() {\n\t\t\t\tfinal Iterator<Entry<Ref<K>, Ref<V>>> referenceIter = referenceEntrySet.iterator();\n\t\t\t\treturn new Iterator<Entry<K, V>>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic boolean hasNext() {\n\t\t\t\t\t\treturn referenceIter.hasNext();\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic Entry<K, V> next() {\n\t\t\t\t\t\tfinal Entry<Ref<K>, Ref<V>> next = referenceIter.next();\n\t\t\t\t\t\treturn new Entry<K, V>() {\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic K getKey() {\n\t\t\t\t\t\t\t\treturn unwrap(next.getKey());\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic V getValue() {\n\t\t\t\t\t\t\t\treturn unwrap(next.getValue());\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic V setValue(final V value) {\n\t\t\t\t\t\t\t\treturn unwrap(next.setValue(wrapValue(value)));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic int size() {\n\t\t\t\treturn referenceEntrySet.size();\n\t\t\t}\n\t\t};\n\t}\n\n\t@Override\n\tpublic void forEach(final BiConsumer<? super K, ? super V> action) {\n\t\tthis.purgeStale();\n\t\tthis.raw.forEach((key, rValue) -> action.accept(key.get(), unwrap(rValue)));\n\t}\n\n\t@Override\n\tpublic Iterator<Entry<K, V>> iterator() {\n\t\treturn entrySet().iterator();\n\t}\n\n\t@Override\n\tpublic V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {\n\t\tthis.purgeStale();\n\t\treturn unwrap(this.raw.compute(wrapKey(key),\n\t\t\t(kReference, vReference) -> wrapValue(remappingFunction.apply(unwrap(kReference), unwrap(vReference)))));\n\t}\n\n\t@Override\n\tpublic V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {\n\t\tthis.purgeStale();\n\t\treturn unwrap(this.raw.merge(wrapKey(key), wrapValue(value),\n\t\t\t(vReference, vReference2) -> wrapValue(remappingFunction.apply(unwrap(vReference), unwrap(vReference2)))));\n\t}\n\n\t/**\n\t * 清除被回收的键和值\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate void purgeStale() {\n\t\tRef<? extends K> key;\n\t\tRef<? extends V> value;\n\n\t\t// 清除无效key对应键值对\n\t\twhile ((key = (Ref<? extends K>) this.lastKeyQueue.poll()) != null) {\n\t\t\tvalue = this.raw.remove(key);\n\t\t\tif (null != purgeListener) {\n\t\t\t\tpurgeListener.accept(key, value);\n\t\t\t}\n\t\t}\n\n\t\t// 清除无效value对应的键值对\n\t\twhile ((value = (Ref<? extends V>) this.lastValueQueue.poll()) != null) {\n\t\t\tMapUtil.removeByValue(this.raw, (Ref<V>) value);\n\t\t\tif (null != purgeListener) {\n\t\t\t\tpurgeListener.accept(null, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 根据Reference类型构建key对应的{@link Reference}\n\t *\n\t * @param key   键\n\t * @param queue {@link ReferenceQueue}\n\t * @return {@link Reference}\n\t */\n\tabstract Ref<K> wrapKey(final K key, final ReferenceQueue<? super K> queue);\n\n\t/**\n\t * 根据Reference类型构建value对应的{@link Reference}\n\t *\n\t * @param value 值\n\t * @param queue {@link ReferenceQueue}\n\t * @return {@link Reference}\n\t */\n\tabstract Ref<V> wrapValue(final V value, final ReferenceQueue<? super V> queue);\n\n\t/**\n\t * 根据Reference类型构建key对应的{@link Reference}\n\t *\n\t * @param key 键\n\t * @return {@link Reference}\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate Ref<K> wrapKey(final Object key) {\n\t\treturn wrapKey((K) key, this.lastKeyQueue);\n\t}\n\n\t/**\n\t * 根据Reference类型构建value对应的{@link Reference}\n\t *\n\t * @param value 键\n\t * @return {@link Reference}\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate Ref<V> wrapValue(final Object value) {\n\t\tif(null == value){\n\t\t\treturn (Ref<V>) NullRef.NULL;\n\t\t}\n\t\treturn wrapValue((V) value, this.lastValueQueue);\n\t}\n\n\t/**\n\t * 去包装对象\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 对象\n\t * @return 值\n\t */\n\tprivate static <T> T unwrap(final Ref<T> obj) {\n\t\treturn ReferenceUtil.get(obj);\n\t}\n\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static class NullRef implements Ref {\n\t\tpublic static final Object NULL = new NullRef();\n\n\t\t@Override\n\t\tpublic Object get() {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/reference/SoftConcurrentMap.java",
    "content": "/*\n * Copyright (c) 2013-2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.map.reference;\n\nimport cn.hutool.core.lang.ref.Ref;\nimport cn.hutool.core.lang.ref.SoftObj;\n\nimport java.lang.ref.ReferenceQueue;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * 线程安全的SoftMap实现<br>\n * 键和值都为Soft引用，即，在GC报告内存不足时会被GC回收\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.41\n */\npublic class SoftConcurrentMap<K, V> extends ReferenceConcurrentMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SoftConcurrentMap() {\n\t\tthis(new ConcurrentHashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link ConcurrentMap}实现\n\t */\n\tpublic SoftConcurrentMap(final ConcurrentMap<Ref<K>, Ref<V>> raw) {\n\t\tsuper(raw);\n\t}\n\n\t@Override\n\tRef<K> wrapKey(final K key, final ReferenceQueue<? super K> queue) {\n\t\treturn new SoftObj<>(key, queue);\n\t}\n\n\t@Override\n\tRef<V> wrapValue(final V value, final ReferenceQueue<? super V> queue) {\n\t\treturn new SoftObj<>(value, queue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/reference/WeakKeyConcurrentMap.java",
    "content": "/*\n * Copyright (c) 2013-2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.map.reference;\n\nimport cn.hutool.core.lang.ref.Ref;\nimport cn.hutool.core.lang.ref.StrongObj;\nimport cn.hutool.core.lang.ref.WeakObj;\n\nimport java.lang.ref.ReferenceQueue;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * 线程安全的WeakMap实现<br>\n * 键为Weak引用，即，在GC时发现弱引用会回收其对象\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.41\n */\npublic class WeakKeyConcurrentMap<K, V> extends ReferenceConcurrentMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t */\n\tpublic WeakKeyConcurrentMap() {\n\t\tthis(new ConcurrentHashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link ConcurrentMap}实现\n\t */\n\tpublic WeakKeyConcurrentMap(final ConcurrentMap<Ref<K>, Ref<V>> raw) {\n\t\tsuper(raw);\n\t}\n\n\t@Override\n\tRef<K> wrapKey(final K key, final ReferenceQueue<? super K> queue) {\n\t\treturn new WeakObj<>(key, queue);\n\t}\n\n\t@Override\n\tRef<V> wrapValue(final V value, final ReferenceQueue<? super V> queue) {\n\t\treturn new StrongObj<>(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/reference/WeakKeyValueConcurrentMap.java",
    "content": "/*\n * Copyright (c) 2013-2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.map.reference;\n\nimport cn.hutool.core.lang.ref.Ref;\nimport cn.hutool.core.lang.ref.WeakObj;\n\nimport java.lang.ref.ReferenceQueue;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * 线程安全的WeakMap实现<br>\n * 键和值都为Weak引用，即，在GC时发现弱引用会回收其对象\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly\n * @since 5.8.41\n */\npublic class WeakKeyValueConcurrentMap<K, V> extends ReferenceConcurrentMap<K, V> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t */\n\tpublic WeakKeyValueConcurrentMap() {\n\t\tthis(new ConcurrentHashMap<>());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link ConcurrentMap}实现\n\t */\n\tpublic WeakKeyValueConcurrentMap(final ConcurrentMap<Ref<K>, Ref<V>> raw) {\n\t\tsuper(raw);\n\t}\n\n\t@Override\n\tRef<K> wrapKey(final K key, final ReferenceQueue<? super K> queue) {\n\t\treturn new WeakObj<>(key, queue);\n\t}\n\n\t@Override\n\tRef<V> wrapValue(final V value, final ReferenceQueue<? super V> queue) {\n\t\treturn new WeakObj<>(value, queue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/map/reference/package-info.java",
    "content": "/*\n * Copyright (c) 2013-2025 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * 弱引用Map实现\n *\n * @author Looly\n * @since 5.8.41\n */\npackage cn.hutool.core.map.reference;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/Arrangement.java",
    "content": "package cn.hutool.core.math;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * 排列A(n, m)<br>\n * 排列组合相关类 参考：http://cgs1999.iteye.com/blog/2327664\n *\n * @author looly\n * @since 4.0.7\n */\npublic class Arrangement implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String[] datas;\n\n\t/**\n\t * 构造\n\t *\n\t * @param datas 用于排列的数据\n\t */\n\tpublic Arrangement(String[] datas) {\n\t\tthis.datas = datas;\n\t}\n\n\t/**\n\t * 计算排列数，即A(n, n) = n!\n\t *\n\t * @param n 总数\n\t * @return 排列数\n\t */\n\tpublic static long count(int n) {\n\t\treturn count(n, n);\n\t}\n\n\t/**\n\t * 计算排列数，即A(n, m) = n!/(n-m)!\n\t *\n\t * @param n 总数\n\t * @param m 选择的个数\n\t * @return 排列数\n\t */\n\tpublic static long count(int n, int m) {\n\t\tif (m < 0 || m > n) {\n\t\t\tthrow new IllegalArgumentException(\"n >= 0 && m >= 0 && m <= n required\");\n\t\t}\n\t\tif (m == 0) {\n\t\t\treturn 1;\n\t\t}\n\t\tlong result = 1;\n\t\t// 从 n 到 n-m+1 逐个乘\n\t\tfor (int i = 0; i < m; i++) {\n\t\t\tlong next = result * (n - i);\n\t\t\t// 溢出检测\n\t\t\tif (next < result) {\n\t\t\t\tthrow new ArithmeticException(\"Overflow computing A(\" + n + \",\" + m + \")\");\n\t\t\t}\n\t\t\tresult = next;\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算排列总数，即A(n, 1) + A(n, 2) + A(n, 3)...\n\t *\n\t * @param n 总数\n\t * @return 排列数\n\t */\n\tpublic static long countAll(int n) {\n\t\tlong total = 0;\n\t\tfor (int i = 1; i <= n; i++) {\n\t\t\ttotal += count(n, i);\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * 全排列选择（列表全部参与排列）\n\t *\n\t * @return 所有排列列表\n\t */\n\tpublic List<String[]> select() {\n\t\treturn select(this.datas.length);\n\t}\n\n\t/**\n\t * 从当前数据中选择 m 个元素，生成所有「不重复」的排列（Permutation）。\n\t *\n\t * <p>\n\t * 说明：\n\t * <ul>\n\t *     <li>不允许重复选择同一个元素（即经典排列 A(n, m)）</li>\n\t *     <li>结果中不会出现 [\"1\",\"1\"] 这种重复元素的情况</li>\n\t *     <li>顺序敏感，因此 [\"1\",\"2\"] 与 [\"2\",\"1\"] 都会包含</li>\n\t * </ul>\n\t *\n\t * 数量公式：\n\t * <pre>\n\t * A(n, m) = n! / (n - m)!\n\t * </pre>\n\t *\n\t * 举例：\n\t * <pre>\n\t * datas = [\"1\",\"2\",\"3\"]\n\t * m = 2\n\t * 输出：\n\t * [\"1\",\"2\"]\n\t * [\"1\",\"3\"]\n\t * [\"2\",\"1\"]\n\t * [\"2\",\"3\"]\n\t * [\"3\",\"1\"]\n\t * [\"3\",\"2\"]\n\t * 共 6 个（A(3,2)=6）\n\t * </pre>\n\t *\n\t * @param m 选择的元素个数\n\t * @return 所有长度为 m 的不重复排列列表\n\t */\n\n\tpublic List<String[]> select(int m) {\n\t\tif (m < 0 || m > datas.length) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\tif (m == 0) {\n\t\t\t// A(n,0) = 1，唯一一个空排列\n\t\t\treturn Collections.singletonList(new String[0]);\n\t\t}\n\n\t\tlong estimated = count(datas.length, m);\n\t\tint capacity = estimated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) estimated;\n\n\t\tList<String[]> result = new ArrayList<>(capacity);\n\t\tboolean[] visited = new boolean[datas.length];\n\t\tdfs(new String[m], 0, visited, result);\n\t\treturn result;\n\t}\n\n\n\t/**\n\t * 生成当前数据的全部不重复排列（长度为 1 至 n 的所有排列）。\n\t *\n\t * <p>\n\t * 说明：\n\t * <ul>\n\t *     <li>不允许重复选择元素（无 [\"1\",\"1\"]，无 [\"2\",\"2\",\"3\"] 这种）</li>\n\t *     <li>包含所有长度 m=1..n 的排列</li>\n\t *     <li>总数量为 A(n,1) + A(n,2) + ... + A(n,n)</li>\n\t * </ul>\n\t *\n\t * 举例（datas = [\"1\",\"2\",\"3\"]）：\n\t * <pre>\n\t * m=1: [\"1\"], [\"2\"], [\"3\"]                             → 3 个\n\t * m=2: [\"1\",\"2\"], [\"1\",\"3\"], [\"2\",\"1\"], ...            → 6 个\n\t * m=3: [\"1\",\"2\",\"3\"], [\"1\",\"3\",\"2\"], [\"2\",\"1\",\"3\"], ...→ 6 个\n\t *\n\t * 总共：3 + 6 + 6 = 15\n\t * </pre>\n\t *\n\t * @return 所有不重复排列列表\n\t */\n\n\tpublic List<String[]> selectAll() {\n\t\tList<String[]> result = new ArrayList<>();\n\t\tfor (int m = 1; m <= datas.length; m++) {\n\t\t\tresult.addAll(select(m));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 排列选择<br>\n\t * 排列方式为先从数据数组中取出一个元素，再把剩余的元素作为新的基数，依次列推，直到选择到足够的元素\n\t *\n\t * @param datas 选择的基数\n\t * @param resultList 前面（resultIndex-1）个的排列结果\n\t * @param resultIndex 选择索引，从0开始\n\t * @param result 最终结果\n\t */\n\tprivate void select(String[] datas, String[] resultList, int resultIndex, List<String[]> result) {\n\t\tif (resultIndex >= resultList.length) { // 全部选择完时，输出排列结果\n\t\t\tif (false == result.contains(resultList)) {\n\t\t\t\tresult.add(Arrays.copyOf(resultList, resultList.length));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// 递归选择下一个\n\t\tfor (int i = 0; i < datas.length; i++) {\n\t\t\tresultList[resultIndex] = datas[i];\n\t\t\tselect(ArrayUtil.remove(datas, i), resultList, resultIndex + 1, result);\n\t\t}\n\t}\n\n\t/**\n\t * 返回一个排列的迭代器\n\t *\n\t * @param m 选择的元素个数\n\t * @return 排列迭代器\n\t */\n\tpublic Iterable<String[]> iterate(int m) {\n\t\treturn () -> new ArrangementIterator(datas, m);\n\t}\n\n\n\t/**\n\t * 排列迭代器\n\t *\n\t * @author CherryRum\n\t */\n\tprivate static class ArrangementIterator implements Iterator<String[]> {\n\n\t\tprivate final String[] datas;\n\t\tprivate final int n;\n\t\tprivate final int m;\n\t\tprivate final boolean[] visited;\n\t\tprivate final String[] buffer;\n\n\t\t// 每一层记录当前尝试的下标，-1表示还未尝试\n\t\tprivate final int[] indices;\n\t\tprivate int depth;\n\t\tprivate boolean end;\n\n\t\t// 预取下一个元素\n\t\tprivate String[] nextItem;\n\t\tprivate boolean nextPrepared;\n\n\t\tArrangementIterator(String[] datas, int m) {\n\t\t\tthis.datas = datas;\n\t\t\tthis.n = datas.length;\n\t\t\tthis.m = m;\n\t\t\tthis.visited = new boolean[n];\n\t\t\tthis.nextItem = null;\n\t\t\tthis.nextPrepared = false;\n\n\t\t\tif (m < 0 || m > n) {\n\t\t\t\t// 无效或无解，直接结束\n\t\t\t\tthis.indices = new int[Math.max(1, m)];\n\t\t\t\tthis.buffer = new String[Math.max(1, m)];\n\t\t\t\tthis.depth = -1;\n\t\t\t\tthis.end = true;\n\t\t\t} else if (m == 0) {\n\t\t\t\t// m == 0: 只返回一个空数组\n\t\t\t\tthis.indices = new int[0];\n\t\t\t\tthis.buffer = new String[0];\n\t\t\t\tthis.depth = 0;\n\t\t\t\tthis.end = false;\n\t\t\t} else {\n\t\t\t\tthis.indices = new int[m];\n\t\t\t\tArrays.fill(this.indices, -1);\n\t\t\t\tthis.buffer = new String[m];\n\t\t\t\tthis.depth = 0;\n\t\t\t\tthis.end = false;\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasNext() {\n\t\t\tif (end) return false;\n\t\t\tif (nextPrepared) return nextItem != null;\n\t\t\tprepareNext();\n\t\t\treturn nextItem != null;\n\t\t}\n\n\t\t@Override\n\t\tpublic String[] next() {\n\t\t\tif (end && !nextPrepared) {\n\t\t\t\tthrow new NoSuchElementException();\n\t\t\t}\n\t\t\tif (!nextPrepared) {\n\t\t\t\tprepareNext();\n\t\t\t}\n\t\t\tif (nextItem == null) {\n\t\t\t\tthrow new NoSuchElementException();\n\t\t\t}\n\t\t\tString[] ret = nextItem;\n\t\t\t// 清除预取缓存，下一次需要重新准备\n\t\t\tnextItem = null;\n\t\t\tnextPrepared = false;\n\t\t\t// 如果m == 0，该项是唯一项，迭代结束\n\t\t\tif (m == 0) {\n\t\t\t\tend = true;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\n\t\t/**\n\t\t * 将状态推进到下一个可返回的排列并把它放入 nextItem。\n\t\t * 如果无更多排列，则将 end=true 并把 nextItem 置为 null。\n\t\t */\n\t\tprivate void prepareNext() {\n\t\t\t// 已经准备过或已结束\n\t\t\tif (nextPrepared || end) {\n\t\t\t\tnextPrepared = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// special-case m == 0\n\t\t\tif (m == 0) {\n\t\t\t\tnextItem = new String[0];\n\t\t\t\tnextPrepared = true;\n\t\t\t\t// do not set end here; end will be set after returning this element in next()\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 非递归模拟DFS，直到找到一个可返回的排列或穷尽\n\t\t\twhile (depth >= 0) {\n\t\t\t\tint start = indices[depth] + 1;\n\t\t\t\tboolean found = false;\n\t\t\t\tfor (int i = start; i < n; i++) {\n\t\t\t\t\tif (!visited[i]) {\n\t\t\t\t\t\t// 如果当前层之前有选过一个元素，要先取消之前选中的 visited\n\t\t\t\t\t\tif (indices[depth] != -1) {\n\t\t\t\t\t\t\tvisited[indices[depth]] = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindices[depth] = i;\n\t\t\t\t\t\tvisited[i] = true;\n\t\t\t\t\t\tbuffer[depth] = datas[i];\n\t\t\t\t\t\tfound = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!found) {\n\t\t\t\t\t// 本层没有可用元素，回溯\n\t\t\t\t\tif (indices[depth] != -1) {\n\t\t\t\t\t\tvisited[indices[depth]] = false;\n\t\t\t\t\t\tindices[depth] = -1;\n\t\t\t\t\t}\n\t\t\t\t\tdepth--;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 若已达到输出深度，准备输出（但不抛出）\n\t\t\t\tif (depth == m - 1) {\n\t\t\t\t\tnextItem = Arrays.copyOf(buffer, m);\n\t\t\t\t\t// 取消当前visited，为下一次在同一层寻找下一个候选做准备\n\t\t\t\t\tvisited[indices[depth]] = false;\n\t\t\t\t\t// 保持 depth 不变（下一次 prepare 会从 indices[depth]+1 开始寻找）\n\t\t\t\t\tnextPrepared = true;\n\t\t\t\t\treturn;\n\t\t\t\t} else {\n\t\t\t\t\t// 向下一层深入：初始化下一层为-1并继续循环\n\t\t\t\t\tdepth++;\n\t\t\t\t\tif (depth < m) {\n\t\t\t\t\t\tindices[depth] = -1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 若循环结束，说明已经穷尽所有可能\n\t\t\tend = true;\n\t\t\tnextItem = null;\n\t\t\tnextPrepared = true;\n\t\t}\n\t}\n\n\t/**\n\t * 核心递归方法（回溯算法）\n\t * * @param current 当前构建的排列数组\n\t *\n\t * @param depth   当前递归深度（填到了第几个位置）\n\t * @param visited 标记数组，记录哪些索引已经被使用了\n\t * @param result  结果集\n\t */\n\tprivate void dfs(String[] current, int depth, boolean[] visited, List<String[]> result) {\n\t\tif (depth == current.length) {\n\t\t\tresult.add(Arrays.copyOf(current, current.length));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (int i = 0; i < datas.length; i++) {\n\t\t\tif (!visited[i]) {\n\t\t\t\tvisited[i] = true;\n\t\t\t\tcurrent[depth] = datas[i];\n\n\t\t\t\tdfs(current, depth + 1, visited, result);\n\t\t\t\tvisited[i] = false;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/BitStatusUtil.java",
    "content": "package cn.hutool.core.math;\n\n/**\n * 通过位运算表示状态的工具类<br>\n * 参数必须是 `偶数` 且 `大于等于0`！\n *\n * 工具实现见博客：https://blog.starxg.com/2020/11/bit-status/\n *\n * @author huangxingguang，senssic\n * @since 5.6.6\n */\npublic class BitStatusUtil {\n\n\t/**\n\t * 增加状态\n\t *\n\t * @param states 原状态\n\t * @param stat   要添加的状态\n\t * @return 新的状态值\n\t */\n\tpublic static int add(int states, int stat) {\n\t\tcheck(states, stat);\n\t\treturn states | stat;\n\t}\n\n\t/**\n\t * 判断是否含有状态\n\t *\n\t * @param states 原状态\n\t * @param stat   要判断的状态\n\t * @return true：有\n\t */\n\tpublic static boolean has(int states, int stat) {\n\t\tcheck(states, stat);\n\t\treturn (states & stat) == stat;\n\t}\n\n\t/**\n\t * 删除一个状态\n\t *\n\t * @param states 原状态\n\t * @param stat   要删除的状态\n\t * @return 新的状态值\n\t */\n\tpublic static int remove(int states, int stat) {\n\t\tcheck(states, stat);\n\t\tif (has(states, stat)) {\n\t\t\treturn states ^ stat;\n\t\t}\n\t\treturn states;\n\t}\n\n\t/**\n\t * 清空状态就是0\n\t *\n\t * @return 0\n\t */\n\tpublic static int clear() {\n\t\treturn 0;\n\t}\n\n\t/**\n\t * 检查\n\t * <ul>\n\t *     <li>必须大于0</li>\n\t *     <li>必须为偶数</li>\n\t * </ul>\n\t *\n\t * @param args 被检查的状态\n\t */\n\tprivate static void check(int... args) {\n\t\tfor (int arg : args) {\n\t\t\tif (arg < 0) {\n\t\t\t\tthrow new IllegalArgumentException(arg + \" 必须大于等于0\");\n\t\t\t}\n\t\t\tif ((arg & 1) == 1) {\n\t\t\t\tthrow new IllegalArgumentException(arg + \" 不是偶数\");\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/Calculator.java",
    "content": "package cn.hutool.core.math;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.math.BigDecimal;\nimport java.util.Collections;\nimport java.util.Stack;\n\n/**\n * 数学表达式计算工具类<br>\n * 见：https://github.com/chinabugotech/hutool/issues/1090#issuecomment-693750140\n *\n * @author trainliang, looly\n * @since 5.4.3\n */\npublic class Calculator {\n\tprivate final Stack<String> postfixStack = new Stack<>();// 后缀式栈\n\tprivate final int[] operatPriority = new int[]{0, 3, 2, 1, -1, 1, 0, 2};// 运用运算符ASCII码-40做索引的运算符优先级\n\n\t/**\n\t * 计算表达式的值\n\t *\n\t * @param expression 表达式\n\t * @return 计算结果\n\t */\n\tpublic static double conversion(String expression) {\n\t\treturn (new Calculator()).calculate(expression);\n\t}\n\n\t/**\n\t * 按照给定的表达式计算\n\t *\n\t * @param expression 要计算的表达式例如:5+12*(3+5)/7\n\t * @return 计算结果\n\t */\n\tpublic double calculate(String expression) {\n\t\tprepare(transform(expression));\n\n\t\tfinal Stack<String> resultStack = new Stack<>();\n\t\tCollections.reverse(postfixStack);// 将后缀式栈反转\n\t\tString firstValue, secondValue, currentOp;// 参与计算的第一个值，第二个值和算术运算符\n\t\twhile (false == postfixStack.isEmpty()) {\n\t\t\tcurrentOp = postfixStack.pop();\n\t\t\tif (false == isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中\n\t\t\t\tcurrentOp = currentOp.replace(\"~\", \"-\");\n\t\t\t\tresultStack.push(currentOp);\n\t\t\t} else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算\n\t\t\t\tsecondValue = resultStack.pop();\n\t\t\t\tfirstValue = resultStack.pop();\n\n\t\t\t\t// 将负数标记符改为负号\n\t\t\t\tfirstValue = firstValue.replace(\"~\", \"-\");\n\t\t\t\tsecondValue = secondValue.replace(\"~\", \"-\");\n\n\t\t\t\tfinal BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0));\n\t\t\t\tresultStack.push(tempResult.toString());\n\t\t\t}\n\t\t}\n\n\t\t// 当结果集中有多个数字时，可能是省略*，类似(1+2)3\n\t\treturn NumberUtil.mul(resultStack.toArray(new String[0])).doubleValue();\n\t\t//return Double.parseDouble(resultStack.pop());\n\t}\n\n\t/**\n\t * 数据准备阶段将表达式转换成为后缀式栈\n\t *\n\t * @param expression 表达式\n\t */\n\tprivate void prepare(String expression) {\n\t\tfinal Stack<Character> opStack = new Stack<>();\n\t\topStack.push(',');// 运算符放入栈底元素逗号，此符号优先级最低\n\t\tfinal char[] arr = expression.toCharArray();\n\t\tint currentIndex = 0;// 当前字符的位置\n\t\tint count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值\n\t\tchar currentOp, peekOp;// 当前操作符和栈顶操作符\n\t\tfor (int i = 0; i < arr.length; i++) {\n\t\t\tcurrentOp = arr[i];\n\t\t\tif (isOperator(currentOp)) {// 如果当前字符是运算符\n\t\t\t\tif (count > 0) {\n\t\t\t\t\tpostfixStack.push(new String(arr, currentIndex, count));// 取两个运算符之间的数字\n\t\t\t\t}\n\t\t\t\tpeekOp = opStack.peek();\n\t\t\t\tif (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号\n\t\t\t\t\twhile (opStack.peek() != '(') {\n\t\t\t\t\t\tpostfixStack.push(String.valueOf(opStack.pop()));\n\t\t\t\t\t}\n\t\t\t\t\topStack.pop();\n\t\t\t\t} else {\n\t\t\t\t\twhile (currentOp != '(' && peekOp != ',' && compare(currentOp, peekOp)) {\n\t\t\t\t\t\tpostfixStack.push(String.valueOf(opStack.pop()));\n\t\t\t\t\t\tpeekOp = opStack.peek();\n\t\t\t\t\t}\n\t\t\t\t\topStack.push(currentOp);\n\t\t\t\t}\n\t\t\t\tcount = 0;\n\t\t\t\tcurrentIndex = i + 1;\n\t\t\t} else {\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\t//新增防止数组越界\n\t\tif (count > 1 || (count == 1 && currentIndex < arr.length && !isOperator(arr[currentIndex]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中\n\t\t\tpostfixStack.push(new String(arr, currentIndex, count));\n\t\t}\n\n\t\twhile (opStack.peek() != ',') {\n\t\t\tpostfixStack.push(String.valueOf(opStack.pop()));// 将操作符栈中的剩余的元素添加到后缀式栈中\n\t\t}\n\t}\n\n\t/**\n\t * 判断是否为算术符号\n\t *\n\t * @param c 字符\n\t * @return 是否为算术符号\n\t */\n\tprivate boolean isOperator(char c) {\n\t\treturn c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')' || c == '%';\n\t}\n\n\t/**\n\t * 利用ASCII码-40做下标去算术符号优先级\n\t *\n\t * @param cur  下标\n\t * @param peek peek\n\t * @return 优先级，如果cur高或相等，返回true，否则false\n\t */\n\tprivate boolean compare(char cur, char peek) {// 如果是peek优先级高于cur，返回true，默认都是peek优先级要低\n\t\tfinal int offset = 40;\n\t\tif(cur  == '%'){\n\t\t\t// %优先级最高\n\t\t\tcur = 47;\n\t\t}\n\t\tif(peek  == '%'){\n\t\t\t// %优先级最高\n\t\t\tpeek = 47;\n\t\t}\n\n\t\treturn operatPriority[peek - offset] >= operatPriority[cur - offset];\n\t}\n\n\t/**\n\t * 按照给定的算术运算符做计算\n\t *\n\t * @param firstValue  第一个值\n\t * @param secondValue 第二个值\n\t * @param currentOp   算数符，只支持'+'、'-'、'*'、'/'、'%'\n\t * @return 结果\n\t */\n\tprivate BigDecimal calculate(String firstValue, String secondValue, char currentOp) {\n\t\tfinal BigDecimal first = NumberUtil.toBigDecimal(firstValue);\n\t\tfinal BigDecimal second = NumberUtil.toBigDecimal(secondValue);\n\n\t\t//添加除零检查并提供明确错误信息\n\t\tif ((currentOp == '/' || currentOp == '%') && second.compareTo(BigDecimal.ZERO) == 0) {\n\t\t\tthrow new ArithmeticException(\"Division by zero: cannot divide \" + firstValue + \" by zero\");\n\t\t}\n\n\t\tfinal BigDecimal result;\n\t\tswitch (currentOp) {\n\t\t\tcase '+':\n\t\t\t\tresult = NumberUtil.add(firstValue, secondValue);\n\t\t\t\tbreak;\n\t\t\tcase '-':\n\t\t\t\tresult = NumberUtil.sub(firstValue, secondValue);\n\t\t\t\tbreak;\n\t\t\tcase '*':\n\t\t\t\tresult = NumberUtil.mul(firstValue, secondValue);\n\t\t\t\tbreak;\n\t\t\tcase '/':\n\t\t\t\tresult = NumberUtil.div(firstValue, secondValue);\n\t\t\t\tbreak;\n\t\t\tcase '%':\n\t\t\t\tresult = NumberUtil.toBigDecimal(firstValue).remainder(NumberUtil.toBigDecimal(secondValue));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalStateException(\"Unexpected value: \" + currentOp);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将表达式中的一元负号转换为内部标记（~），便于后续解析。\n\t * 规则说明：\n\t *  - 科学计数法整体识别为数字，e/E 后的 + 或 - 属于指数符号，不参与一元符号折叠。\n\t *  - 一元 + / - 仅在表达式开头或运算符、左括号之后生效；可折叠连续符号，如 --3、+-3 -> ~3。\n\t * 示例：\n\t *  - 输入：-2+-1*(-3E-2)-(-1)\n\t *  - 输出：~2+~1*(~3E~2)-(~1)\n\t */\n\tprivate static String transform(String expression) {\n\t\texpression = StrUtil.cleanBlank(expression);\n\t\texpression = StrUtil.removeSuffix(expression, \"=\");\n\t\tfinal char[] arr = expression.toCharArray();\n\n\t\tfinal StringBuilder out = new StringBuilder(arr.length);\n\t\tfor (int i = 0; i < arr.length; i++) {\n\t\t\tchar c = arr[i];\n\n\t\t\t// 把x或X当作 *\n\t\t\tif (CharUtil.equals(c, 'x', true)) {\n\t\t\t\tout.append('*');\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 若是'+'或'-'，需要判断是指数符号、二元运算符还是一元运算符序列\n\t\t\tif (c == '+' || c == '-') {\n\t\t\t\t// 如果前一个已写入的字符为'e'或'E'，则视作科学计数法的符号\n\t\t\t\tint outLen = out.length();\n\t\t\t\tif (outLen > 0) {\n\t\t\t\t\tchar prevOut = out.charAt(outLen - 1);\n\t\t\t\t\tif (prevOut == 'e' || prevOut == 'E') {\n\t\t\t\t\t\t// 在e/E 后：\n\t\t\t\t\t\t// '+' 可以安全丢弃（1e+3 == 1e3）\n\t\t\t\t\t\t// '-' 必须保留但不能被当作二元运算符，故用'~'临时替代，后续再还原为'-'\n\t\t\t\t\t\tif (c == '-') {\n\t\t\t\t\t\t\tout.append('~');\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 查找前一个非空字符（原串中的），用于判断是否为一元上下文\n\t\t\t\tint j = i - 1;\n\t\t\t\twhile (j >= 0 && Character.isWhitespace(arr[j])) j--;\n\t\t\t\tboolean unaryContext = (j < 0) || isPrevCharOperatorOrLeftParen(arr[j]);\n\n\t\t\t\tif (unaryContext) {\n\t\t\t\t\t// 收集连续的一系列 + 或 -（例如 --+ - -> 合并为一个净符号）\n\t\t\t\t\tint k = i;\n\t\t\t\t\tint minusCount = 0;\n\t\t\t\t\twhile (k < arr.length && (arr[k] == '+' || arr[k] == '-')) {\n\t\t\t\t\t\tif (arr[k] == '-') minusCount++;\n\t\t\t\t\t\tk++;\n\t\t\t\t\t}\n\t\t\t\t\tboolean netNegative = (minusCount % 2 == 1);\n\t\t\t\t\tif (netNegative) {\n\t\t\t\t\t\t// 用~标记一元负号（与原实现保持兼容）\n\t\t\t\t\t\tout.append('~');\n\t\t\t\t\t}\n\t\t\t\t\ti = k - 1;\n\t\t\t\t} else {\n\t\t\t\t\t//二元运算符，直接写入 + 或 -\n\t\t\t\t\tout.append(c);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t//其它字符（包括数字、字母、括号、e、E、小数点等）直接追加\n\t\t\tout.append(c);\n\t\t}\n\n\t\t// 特殊处理：如果开头为 \"~(\"，原实现会将其转为 \"0~(\" 形式改为以0开始的负括号处理\n\t\tfinal String result = out.toString();\n\t\tfinal char[] resArr = result.toCharArray();\n\t\tif (resArr.length >= 2 && resArr[0] == '~' && resArr[1] == '(') {\n\t\t\tresArr[0] = '-';\n\t\t\treturn \"0\" + new String(resArr);\n\t\t} else {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\t/**\n\t * 判断给定位置前一个非空字符是否为运算符或左括号（用于判定是否为一元上下文）\n\t */\n\tprivate static boolean isPrevCharOperatorOrLeftParen(char c) {\n\t\treturn c == '%' || c == '+' || c == '-' || c == '*' || c == '/' || c == '(';\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/Combination.java",
    "content": "package cn.hutool.core.math;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 组合，即C(n, m)<br>\n * 排列组合相关类 参考：http://cgs1999.iteye.com/blog/2327664\n *\n * @author looly\n * @since 4.0.6\n */\npublic class Combination implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String[] datas;\n\n\t/**\n\t * 组合，即C(n, m)<br>\n\t * 排列组合相关类 参考：http://cgs1999.iteye.com/blog/2327664\n\t *\n\t * @param datas 用于组合的数据\n\t */\n\tpublic Combination(String[] datas) {\n\t\tthis.datas = datas;\n\t}\n\n\t/**\n\t * 计算组合数，即C(n, m) = n!/((n-m)! * m!)\n\t * <p>注意：此方法内部使用 BigInteger 修复了旧版 factorial 的计算错误，\n\t * 但最终仍以 long 返回，因此当结果超过 long 范围时仍会溢出。</p>\n\t * <p>建议使用 {@link #countBig(int, int)} 获取精确结果，或使用\n\t * {@link #countSafe(int, int)} 获取安全 long 版本。</p>\n\t * @param n 总数\n\t * @param m 选择的个数\n\t * @return 组合数\n\t */\n\t@Deprecated\n\tpublic static long count(int n, int m) {\n\t\tBigInteger big = countBig(n, m);\n\t\treturn big.longValue();\n\t}\n\n\t/**\n\t * 计算组合数 C(n, m) 的 BigInteger 精确版本。\n\t * 使用逐步累乘除法（非阶乘）保证不溢出、性能好。\n\t * <p>\n\t * 数学定义：\n\t * C(n, m) = n! / (m! (n - m)!)\n\t * <p>\n\t * 优化方式：\n\t * 1. 利用对称性 m = min(m, n-m)\n\t * 2. 每一步先乘 BigInteger，再除以当前 i，保证数值不暴涨\n\t *\n\t * @param n 总数 n（必须 大于等于 0）\n\t * @param m 取出 m（必须 大于等于 0）\n\t * @return C(n, m) 的 BigInteger 精确值；当 m 大于 n 时返回 BigInteger.ZERO\n\t */\n\tpublic static BigInteger countBig(int n, int m) {\n\t\tif (n < 0 || m < 0) {\n\t\t\tthrow new IllegalArgumentException(\"n and m must be non-negative. got n=\" + n + \", m=\" + m);\n\t\t}\n\t\tif (m > n) {\n\t\t\treturn BigInteger.ZERO;\n\t\t}\n\t\tif (m == 0 || n == m) {\n\t\t\treturn BigInteger.ONE;\n\t\t}\n\t\t// 使用对称性：C(n, m) = C(n, n-m)\n\t\tm = Math.min(m, n - m);\n\t\tBigInteger result = BigInteger.ONE;\n\t\t// 从 1 → m 累乘\n\t\tfor (int i = 1; i <= m; i++) {\n\t\t\tint numerator = n - m + i;\n\t\t\tresult = result.multiply(BigInteger.valueOf(numerator))\n\t\t\t\t.divide(BigInteger.valueOf(i));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 安全组合数 long 版本。\n\t *\n\t * @param n 总数 n（必须 大于等于 0）\n\t * @param m 取出 m（必须 大于等于 0）\n\t *          <p>若结果超出 long 范围，会抛 ArithmeticException，而非溢出。</p>\n\t * @return C(n, m) 的 long 精确值；当 m 大于 n 时返回 0L\n\t */\n\tpublic static long countSafe(int n, int m) {\n\t\tBigInteger big = countBig(n, m);\n\t\treturn big.longValueExact();\n\t}\n\n\n\t/**\n\t * 计算组合总数，即C(n, 1) + C(n, 2) + C(n, 3)...\n\t *\n\t * @param n 总数\n\t * @return 组合数\n\t */\n\tpublic static long countAll(int n) {\n\t\tif (n < 0 || n > 63) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"countAll must have n >= 0 and n <= 63, but got n={}\", n));\n\t\t}\n\t\treturn n == 63 ? Long.MAX_VALUE : (1L << n) - 1;\n\t}\n\n\t/**\n\t * 组合选择（从列表中选择m个组合）\n\t *\n\t * @param m 选择个数\n\t * @return 组合结果\n\t */\n\tpublic List<String[]> select(int m) {\n\t\tfinal List<String[]> result = new ArrayList<>((int) count(this.datas.length, m));\n\t\tselect(0, new String[m], 0, result);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 全组合\n\t *\n\t * @return 全排列结果\n\t */\n\tpublic List<String[]> selectAll() {\n\t\tfinal List<String[]> result = new ArrayList<>((int) countAll(this.datas.length));\n\t\tfor (int i = 1; i <= this.datas.length; i++) {\n\t\t\tresult.addAll(select(i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 组合选择\n\t *\n\t * @param dataIndex   待选开始索引\n\t * @param resultList  前面（resultIndex-1）个的组合结果\n\t * @param resultIndex 选择索引，从0开始\n\t * @param result      结果集\n\t */\n\tprivate void select(int dataIndex, String[] resultList, int resultIndex, List<String[]> result) {\n\t\tint resultLen = resultList.length;\n\t\tint resultCount = resultIndex + 1;\n\t\tif (resultCount > resultLen) { // 全部选择完时，输出组合结果\n\t\t\tresult.add(Arrays.copyOf(resultList, resultList.length));\n\t\t\treturn;\n\t\t}\n\n\t\t// 递归选择下一个\n\t\tfor (int i = dataIndex; i < datas.length + resultCount - resultLen; i++) {\n\t\t\tresultList[resultIndex] = datas[i];\n\t\t\tselect(i + 1, resultList, resultIndex + 1, result);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/MathUtil.java",
    "content": "package cn.hutool.core.math;\n\nimport java.util.List;\n\n/**\n * 数学相关方法工具类<br>\n * 此工具类与{@link cn.hutool.core.util.NumberUtil}属于一类工具，NumberUtil偏向于简单数学计算的封装，MathUtil偏向复杂数学计算\n *\n * @author looly\n * @since 4.0.7\n */\npublic class MathUtil {\n\n\t//--------------------------------------------------------------------------------------------- Arrangement\n\t/**\n\t * 计算排列数，即A(n, m) = n!/(n-m)!\n\t *\n\t * @param n 总数\n\t * @param m 选择的个数\n\t * @return 排列数\n\t */\n\tpublic static long arrangementCount(int n, int m) {\n\t\treturn Arrangement.count(n, m);\n\t}\n\n\t/**\n\t * 计算排列数，即A(n, n) = n!\n\t *\n\t * @param n 总数\n\t * @return 排列数\n\t */\n\tpublic static long arrangementCount(int n) {\n\t\treturn Arrangement.count(n);\n\t}\n\n\t/**\n\t * 排列选择（从列表中选择n个排列）\n\t *\n\t * @param datas 待选列表\n\t * @param m 选择个数\n\t * @return 所有排列列表\n\t */\n\tpublic static List<String[]> arrangementSelect(String[] datas, int m) {\n\t\treturn new Arrangement(datas).select(m);\n\t}\n\n\t/**\n\t * 全排列选择（列表全部参与排列）\n\t *\n\t * @param datas 待选列表\n\t * @return 所有排列列表\n\t */\n\tpublic static List<String[]> arrangementSelect(String[] datas) {\n\t\treturn new Arrangement(datas).select();\n\t}\n\n\t//--------------------------------------------------------------------------------------------- Combination\n\t/**\n\t * 计算组合数，即C(n, m) = n!/((n-m)! * m!)\n\t *\n\t * @param n 总数\n\t * @param m 选择的个数\n\t * @return 组合数\n\t */\n\tpublic static long combinationCount(int n, int m) {\n\t\treturn Combination.count(n, m);\n\t}\n\n\t/**\n\t * 组合选择（从列表中选择n个组合）\n\t *\n\t * @param datas 待选列表\n\t * @param m 选择个数\n\t * @return 所有组合列表\n\t */\n\tpublic static List<String[]> combinationSelect(String[] datas, int m) {\n\t\treturn new Combination(datas).select(m);\n\t}\n\n\t/**\n\t * 金额元转换为分\n\t *\n\t * @param yuan 金额，单位元\n\t * @return 金额，单位分\n\t * @since 5.7.11\n\t */\n\tpublic static long yuanToCent(double yuan) {\n\t\treturn new Money(yuan).getCent();\n\t}\n\n\t/**\n\t * 金额分转换为元\n\t *\n\t * @param cent 金额，单位分\n\t * @return 金额，单位元\n\t * @since 5.7.11\n\t */\n\tpublic static double centToYuan(long cent) {\n\t\tlong yuan = cent / 100;\n\t\tint centPart = (int) (cent % 100);\n\t\treturn new Money(yuan, centPart).getAmount().doubleValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/Money.java",
    "content": "package cn.hutool.core.math;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.Serializable;\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.Currency;\n\n/**\n * 单币种货币类，处理货币算术、币种和取整。\n * <p>\n * 感谢提供此方法的用户：https://github.com/chinabugotech/hutool/issues/605\n *\n * <p>\n * 货币类中封装了货币金额和币种。目前金额在内部是long类型表示，\n * 单位是所属币种的最小货币单位（对人民币是分）。\n *\n * <p>\n * 目前，货币实现了以下主要功能：<br>\n * <ul>\n *   <li>支持货币对象与double(float)/long(int)/String/BigDecimal之间相互转换。\n *   <li>货币类在运算中提供与JDK中的BigDecimal类似的运算接口，\n *       BigDecimal的运算接口支持任意指定精度的运算功能，能够支持各种\n *       可能的财务规则。\n *   <li>货币类在运算中也提供一组简单运算接口，使用这组运算接口，则在\n *       精度处理上使用缺省的处理规则。\n *   <li>推荐使用Money，不建议直接使用BigDecimal的原因之一在于，\n *       使用BigDecimal，同样金额和币种的货币使用BigDecimal存在多种可能\n *       的表示，例如：new BigDecimal(\"10.5\")与new BigDecimal(\"10.50\")\n *       不相等，因为scale不等。使得Money类，同样金额和币种的货币只有\n *       一种表示方式，new Money(\"10.5\")和new Money(\"10.50\")应该是相等的。\n *   <li>不推荐直接使用BigDecimal的另一原因在于， BigDecimal是Immutable，\n *       一旦创建就不可更改，对BigDecimal进行任意运算都会生成一个新的\n *       BigDecimal对象，因此对于大批量统计的性能不够满意。Money类是\n *       mutable的，对大批量统计提供较好的支持。\n *   <li>提供基本的格式化功能。\n *   <li>Money类中不包含与业务相关的统计功能和格式化功能。业务相关的功能\n *       建议使用utility类来实现。\n *   <li>Money类实现了Serializable接口，支持作为远程调用的参数和返回值。\n *   <li>Money类实现了equals和hashCode方法。\n * </ul>\n *\n * @author ddatsh\n * @since 5.0.4\n */\n\npublic class Money implements Serializable, Comparable<Money> {\n\tprivate static final long serialVersionUID = -1004117971993390293L;\n\n\t/**\n\t * 缺省的币种代码，为CNY（人民币）。\n\t */\n\tpublic static final String DEFAULT_CURRENCY_CODE = \"CNY\";\n\n\t/**\n\t * 缺省的取整模式，为{@link RoundingMode#HALF_EVEN}\n\t * （四舍五入，当小数为0.5时，则取最近的偶数）。\n\t */\n\tpublic static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_EVEN;\n\n\t/**\n\t * 一组可能的元/分换算比例。\n\t *\n\t * <p>\n\t * 此处，“分”是指货币的最小单位，“元”是货币的最常用单位，\n\t * 不同的币种有不同的元/分换算比例，如人民币是100，而日元为1。\n\t */\n\tprivate static final int[] CENT_FACTORS = new int[]{1, 10, 100, 1000};\n\n\t/**\n\t * 金额，以分为单位。\n\t */\n\tprivate long cent;\n\n\t/**\n\t * 币种。\n\t */\n\tprivate final Currency currency;\n\n\t// 构造器 ====================================================\n\n\t/**\n\t * 缺省构造器。\n\t *\n\t * <p>\n\t * 创建一个具有缺省金额（0）和缺省币种的货币对象。\n\t */\n\tpublic Money() {\n\t\tthis(0);\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code yuan}元{@code cent}分和缺省币种的货币对象。\n\t *\n\t * @param yuan 金额元数，0的情况下表示元的部分从分中截取\n\t * @param cent 金额分数。\n\t */\n\tpublic Money(long yuan, int cent) {\n\t\tthis(yuan, cent, Currency.getInstance(DEFAULT_CURRENCY_CODE));\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code yuan}元{@code cent}分和指定币种的货币对象。\n\t *\n\t * @param yuan     金额元数，0的情况下表示元的部分从分中截取\n\t * @param cent     金额分数。\n\t * @param currency 货币单位\n\t */\n\tpublic Money(long yuan, int cent, Currency currency) {\n\t\tthis.currency = currency;\n\n\t\tif(0 == yuan) {\n\t\t\tthis.cent = cent;\n\t\t} else{\n\t\t\tthis.cent = (yuan * getCentFactor()) + (cent % getCentFactor());\n\t\t}\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}元和缺省币种的货币对象。\n\t *\n\t * @param amount 金额，以元为单位。\n\t */\n\tpublic Money(String amount) {\n\t\tthis(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE));\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}元和指定币种{@code currency}的货币对象。\n\t *\n\t * @param amount   金额，以元为单位。\n\t * @param currency 币种。\n\t */\n\tpublic Money(String amount, Currency currency) {\n\t\tthis(new BigDecimal(amount), currency);\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}元和指定币种{@code currency}的货币对象。\n\t * 如果金额不能转换为整数分，则使用指定的取整模式{@code roundingMode}取整。\n\t *\n\t * @param amount       金额，以元为单位。\n\t * @param currency     币种。\n\t * @param roundingMode 取整模式。\n\t */\n\tpublic Money(String amount, Currency currency, RoundingMode roundingMode) {\n\t\tthis(new BigDecimal(amount), currency, roundingMode);\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有参数{@code amount}指定金额和缺省币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用四舍五入方式取整。\n\t *\n\t * <p>\n\t * 注意：由于double类型运算中存在误差，使用四舍五入方式取整的\n\t * 结果并不确定，因此，应尽量避免使用double类型创建货币类型。\n\t * 例：\n\t * {@code\n\t * assertEquals(999, Math.round(9.995 * 100));\n\t * assertEquals(1000, Math.round(999.5));\n\t * money = new Money((9.995));\n\t * assertEquals(999, money.getCent());\n\t * money = new Money(10.005);\n\t * assertEquals(1001, money.getCent());\n\t * }\n\t *\n\t * @param amount 金额，以元为单位。\n\t */\n\tpublic Money(double amount) {\n\t\tthis(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE));\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}和指定币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用四舍五入方式取整。\n\t *\n\t * <p>\n\t * 注意：由于double类型运算中存在误差，使用四舍五入方式取整的\n\t * 结果并不确定，因此，应尽量避免使用double类型创建货币类型。\n\t * 例：\n\t * {@code\n\t * assertEquals(999, Math.round(9.995 * 100));\n\t * assertEquals(1000, Math.round(999.5));\n\t * money = new Money((9.995));\n\t * assertEquals(999, money.getCent());\n\t * money = new Money(10.005);\n\t * assertEquals(1001, money.getCent());\n\t * }\n\t *\n\t * @param amount   金额，以元为单位。\n\t * @param currency 币种。\n\t */\n\tpublic Money(double amount, Currency currency) {\n\t\tthis.currency = currency;\n\t\tthis.cent = Math.round(amount * getCentFactor());\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}和缺省币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用缺省取整模式{@code DEFAULT_ROUNDING_MODE}取整。\n\t *\n\t * @param amount 金额，以元为单位。\n\t */\n\tpublic Money(BigDecimal amount) {\n\t\tthis(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE));\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有参数{@code amount}指定金额和缺省币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用指定的取整模式{@code roundingMode}取整。\n\t *\n\t * @param amount       金额，以元为单位。\n\t * @param roundingMode 取整模式\n\t */\n\tpublic Money(BigDecimal amount, RoundingMode roundingMode) {\n\t\tthis(amount, Currency.getInstance(DEFAULT_CURRENCY_CODE), roundingMode);\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}和指定币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用缺省的取整模式{@code DEFAULT_ROUNDING_MODE}进行取整。\n\t *\n\t * @param amount   金额，以元为单位。\n\t * @param currency 币种\n\t */\n\tpublic Money(BigDecimal amount, Currency currency) {\n\t\tthis(amount, currency, DEFAULT_ROUNDING_MODE);\n\t}\n\n\t/**\n\t * 构造器。\n\t *\n\t * <p>\n\t * 创建一个具有金额{@code amount}和指定币种的货币对象。\n\t * 如果金额不能转换为整数分，则使用指定的取整模式{@code roundingMode}取整。\n\t *\n\t * @param amount       金额，以元为单位。\n\t * @param currency     币种。\n\t * @param roundingMode 取整模式。\n\t */\n\tpublic Money(BigDecimal amount, Currency currency, RoundingMode roundingMode) {\n\t\tthis.currency = currency;\n\t\tthis.cent = rounding(amount.movePointRight(currency.getDefaultFractionDigits()),\n\t\t\t\troundingMode);\n\t}\n\n\t// Bean方法 ====================================================\n\n\t/**\n\t * 获取本货币对象代表的金额数。\n\t *\n\t * @return 金额数，以元为单位。\n\t */\n\tpublic BigDecimal getAmount() {\n\t\treturn BigDecimal.valueOf(cent, currency.getDefaultFractionDigits());\n\t}\n\n\t/**\n\t * 设置本货币对象代表的金额数。\n\t *\n\t * @param amount 金额数，以元为单位。\n\t */\n\tpublic void setAmount(BigDecimal amount) {\n\t\tif (amount != null) {\n\t\t\tcent = rounding(amount.movePointRight(currency.getDefaultFractionDigits()), DEFAULT_ROUNDING_MODE);\n\t\t}\n\t}\n\n\t/**\n\t * 获取本货币对象代表的金额数。\n\t *\n\t * @return 金额数，以分为单位。\n\t */\n\tpublic long getCent() {\n\t\treturn cent;\n\t}\n\n\t/**\n\t * 获取本货币对象代表的币种。\n\t *\n\t * @return 本货币对象所代表的币种。\n\t */\n\tpublic Currency getCurrency() {\n\t\treturn currency;\n\t}\n\n\t/**\n\t * 获取本货币币种的元/分换算比率。\n\t *\n\t * @return 本货币币种的元/分换算比率。\n\t */\n\tpublic int getCentFactor() {\n\t\treturn CENT_FACTORS[currency.getDefaultFractionDigits()];\n\t}\n\n\t// 基本对象方法 ===================================================\n\n\t/**\n\t * 判断本货币对象与另一对象是否相等。\n\t * <p>\n\t * 货币对象与另一对象相等的充分必要条件是：<br>\n\t * <ul>\n\t *  <li>另一对象也属货币对象类。\n\t *  <li>金额相同。\n\t *  <li>币种相同。\n\t * </ul>\n\t *\n\t * @param other 待比较的另一对象。\n\t * @return {@code true}表示相等，{@code false}表示不相等。\n\t */\n\t@Override\n\tpublic boolean equals(Object other) {\n\t\treturn (other instanceof Money) && equals((Money) other);\n\t}\n\n\t/**\n\t * 判断本货币对象与另一货币对象是否相等。\n\t * <p>\n\t * 货币对象与另一货币对象相等的充分必要条件是：<br>\n\t * <ul>\n\t *  <li>金额相同。\n\t *  <li>币种相同。\n\t * </ul>\n\t *\n\t * @param other 待比较的另一货币对象。\n\t * @return {@code true}表示相等，{@code false}表示不相等。\n\t */\n\tpublic boolean equals(Money other) {\n\t\treturn currency.equals(other.currency) && (cent == other.cent);\n\t}\n\n\t/**\n\t * 计算本货币对象的杂凑值。\n\t *\n\t * @return 本货币对象的杂凑值。\n\t */\n\t@Override\n\tpublic int hashCode() {\n\t\treturn (int) (cent ^ (cent >>> 32));\n\t}\n\n\t/**\n\t * 货币比较。\n\t *\n\t * <p>\n\t * 比较本货币对象与另一货币对象的大小。\n\t * 如果待比较的两个货币对象的币种不同，则抛出{@code java.lang.IllegalArgumentException}。\n\t * 如果本货币对象的金额少于待比较货币对象，则返回-1。\n\t * 如果本货币对象的金额等于待比较货币对象，则返回0。\n\t * 如果本货币对象的金额大于待比较货币对象，则返回1。\n\t *\n\t * @param other 另一对象。\n\t * @return -1表示小于，0表示等于，1表示大于。\n\t * @throws IllegalArgumentException 待比较货币对象与本货币对象的币种不同。\n\t */\n\t@Override\n\tpublic int compareTo(Money other) {\n\t\tassertSameCurrencyAs(other);\n\t\treturn Long.compare(cent, other.cent);\n\t}\n\n\t/**\n\t * 货币比较。\n\t *\n\t * <p>\n\t * 判断本货币对象是否大于另一货币对象。\n\t * 如果待比较的两个货币对象的币种不同，则抛出{@code java.lang.IllegalArgumentException}。\n\t * 如果本货币对象的金额大于待比较货币对象，则返回true，否则返回false。\n\t *\n\t * @param other 另一对象。\n\t * @return true表示大于，false表示不大于（小于等于）。\n\t * @throws IllegalArgumentException 待比较货币对象与本货币对象的币种不同。\n\t */\n\tpublic boolean greaterThan(Money other) {\n\t\treturn compareTo(other) > 0;\n\t}\n\n\t// 货币算术 ==========================================\n\n\t/**\n\t * 货币加法。\n\t *\n\t * <p>\n\t * 如果两货币币种相同，则返回一个新的相同币种的货币对象，其金额为\n\t * 两货币对象金额之和，本货币对象的值不变。\n\t * 如果两货币对象币种不同，抛出{@code java.lang.IllegalArgumentException}。\n\t *\n\t * @param other 作为加数的货币对象。\n\t * @return 相加后的结果。\n\t * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。\n\t */\n\tpublic Money add(Money other) {\n\t\tassertSameCurrencyAs(other);\n\n\t\treturn newMoneyWithSameCurrency(cent + other.cent);\n\t}\n\n\t/**\n\t * 货币累加。\n\t *\n\t * <p>\n\t * 如果两货币币种相同，则本货币对象的金额等于两货币对象金额之和，并返回本货币对象的引用。\n\t * 如果两货币对象币种不同，抛出{@code java.lang.IllegalArgumentException}。\n\t *\n\t * @param other 作为加数的货币对象。\n\t * @return 累加后的本货币对象。\n\t * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。\n\t */\n\tpublic Money addTo(Money other) {\n\t\tassertSameCurrencyAs(other);\n\n\t\tthis.cent += other.cent;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币减法。\n\t *\n\t * <p>\n\t * 如果两货币币种相同，则返回一个新的相同币种的货币对象，其金额为\n\t * 本货币对象的金额减去参数货币对象的金额。本货币对象的值不变。\n\t * 如果两货币币种不同，抛出{@code java.lang.IllegalArgumentException}。\n\t *\n\t * @param other 作为减数的货币对象。\n\t * @return 相减后的结果。\n\t * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。\n\t */\n\tpublic Money subtract(Money other) {\n\t\tassertSameCurrencyAs(other);\n\n\t\treturn newMoneyWithSameCurrency(cent - other.cent);\n\t}\n\n\t/**\n\t * 货币累减。\n\t *\n\t * <p>\n\t * 如果两货币币种相同，则本货币对象的金额等于两货币对象金额之差，并返回本货币对象的引用。\n\t * 如果两货币币种不同，抛出{@code java.lang.IllegalArgumentException}。\n\t *\n\t * @param other 作为减数的货币对象。\n\t * @return 累减后的本货币对象。\n\t * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。\n\t */\n\tpublic Money subtractFrom(Money other) {\n\t\tassertSameCurrencyAs(other);\n\n\t\tthis.cent -= other.cent;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币乘法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额乘以乘数。\n\t * 本货币对象的值不变。\n\t *\n\t * @param val 乘数\n\t * @return 乘法后的结果。\n\t */\n\tpublic Money multiply(long val) {\n\t\treturn newMoneyWithSameCurrency(cent * val);\n\t}\n\n\t/**\n\t * 货币累乘。\n\t *\n\t * <p>\n\t * 本货币对象金额乘以乘数，并返回本货币对象。\n\t *\n\t * @param val 乘数\n\t * @return 累乘后的本货币对象。\n\t */\n\tpublic Money multiplyBy(long val) {\n\t\tthis.cent *= val;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币乘法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额乘以乘数。\n\t * 本货币对象的值不变。如果相乘后的金额不能转换为整数分，则四舍五入。\n\t *\n\t * @param val 乘数\n\t * @return 相乘后的结果。\n\t */\n\tpublic Money multiply(double val) {\n\t\treturn newMoneyWithSameCurrency(Math.round(cent * val));\n\t}\n\n\t/**\n\t * 货币累乘。\n\t *\n\t * <p>\n\t * 本货币对象金额乘以乘数，并返回本货币对象。\n\t * 如果相乘后的金额不能转换为整数分，则使用四舍五入。\n\t *\n\t * @param val 乘数\n\t * @return 累乘后的本货币对象。\n\t */\n\tpublic Money multiplyBy(double val) {\n\t\tthis.cent = Math.round(this.cent * val);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币乘法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额乘以乘数。\n\t * 本货币对象的值不变。如果相乘后的金额不能转换为整数分，使用缺省的取整模式\n\t * {@code DEFUALT_ROUNDING_MODE}进行取整。\n\t *\n\t * @param val 乘数\n\t * @return 相乘后的结果。\n\t */\n\tpublic Money multiply(BigDecimal val) {\n\t\treturn multiply(val, DEFAULT_ROUNDING_MODE);\n\t}\n\n\t/**\n\t * 货币累乘。\n\t *\n\t * <p>\n\t * 本货币对象金额乘以乘数，并返回本货币对象。\n\t * 如果相乘后的金额不能转换为整数分，使用缺省的取整方式\n\t * {@code DEFUALT_ROUNDING_MODE}进行取整。\n\t *\n\t * @param val 乘数\n\t * @return 累乘后的结果。\n\t */\n\tpublic Money multiplyBy(BigDecimal val) {\n\t\treturn multiplyBy(val, DEFAULT_ROUNDING_MODE);\n\t}\n\n\t/**\n\t * 货币乘法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额乘以乘数。\n\t * 本货币对象的值不变。如果相乘后的金额不能转换为整数分，使用指定的取整方式\n\t * {@code roundingMode}进行取整。\n\t *\n\t * @param val          乘数\n\t * @param roundingMode 取整方式\n\t * @return 相乘后的结果。\n\t */\n\tpublic Money multiply(BigDecimal val, RoundingMode roundingMode) {\n\t\tBigDecimal newCent = BigDecimal.valueOf(cent).multiply(val);\n\n\t\treturn newMoneyWithSameCurrency(rounding(newCent, roundingMode));\n\t}\n\n\t/**\n\t * 货币累乘。\n\t *\n\t * <p>\n\t * 本货币对象金额乘以乘数，并返回本货币对象。\n\t * 如果相乘后的金额不能转换为整数分，使用指定的取整方式\n\t * {@code roundingMode}进行取整。\n\t *\n\t * @param val          乘数\n\t * @param roundingMode 取整方式\n\t * @return 累乘后的结果。\n\t */\n\tpublic Money multiplyBy(BigDecimal val, RoundingMode roundingMode) {\n\t\tBigDecimal newCent = BigDecimal.valueOf(cent).multiply(val);\n\n\t\tthis.cent = rounding(newCent, roundingMode);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币除法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额除以除数。\n\t * 本货币对象的值不变。如果相除后的金额不能转换为整数分，使用四舍五入方式取整。\n\t *\n\t * @param val 除数\n\t * @return 相除后的结果。\n\t */\n\tpublic Money divide(double val) {\n\t\tif (val == 0) {\n\t\t\tthrow new ArithmeticException(\"Division by zero\");\n\t\t}\n\t\treturn newMoneyWithSameCurrency(Math.round(cent / val));\n\t}\n\n\t/**\n\t * 货币累除。\n\t *\n\t * <p>\n\t * 本货币对象金额除以除数，并返回本货币对象。\n\t * 如果相除后的金额不能转换为整数分，使用四舍五入方式取整。\n\t *\n\t * @param val 除数\n\t * @return 累除后的结果。\n\t */\n\tpublic Money divideBy(double val) {\n\n\t\tif (val == 0) {\n\t\t\tthrow new ArithmeticException(\"Division by zero\");\n\t\t}\t\tthis.cent = Math.round(this.cent / val);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币除法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额除以除数。\n\t * 本货币对象的值不变。如果相除后的金额不能转换为整数分，使用缺省的取整模式\n\t * {@code DEFAULT_ROUNDING_MODE}进行取整。\n\t *\n\t * @param val 除数\n\t * @return 相除后的结果。\n\t */\n\tpublic Money divide(BigDecimal val) {\n\t\treturn divide(val, DEFAULT_ROUNDING_MODE);\n\t}\n\n\t/**\n\t * 货币除法。\n\t *\n\t * <p>\n\t * 返回一个新的货币对象，币种与本货币对象相同，金额为本货币对象的金额除以除数。\n\t * 本货币对象的值不变。如果相除后的金额不能转换为整数分，使用指定的取整模式\n\t * {@code roundingMode}进行取整。\n\t *\n\t * @param val          除数\n\t * @param roundingMode 取整\n\t * @return 相除后的结果。\n\t */\n\tpublic Money divide(BigDecimal val, RoundingMode roundingMode) {\n\t\tBigDecimal newCent = BigDecimal.valueOf(cent).divide(val, roundingMode);\n\n\t\treturn newMoneyWithSameCurrency(newCent.longValue());\n\t}\n\n\t/**\n\t * 货币累除。\n\t *\n\t * <p>\n\t * 本货币对象金额除以除数，并返回本货币对象。\n\t * 如果相除后的金额不能转换为整数分，使用缺省的取整模式\n\t * {@code DEFAULT_ROUNDING_MODE}进行取整。\n\t *\n\t * @param val 除数\n\t * @return 累除后的结果。\n\t */\n\tpublic Money divideBy(BigDecimal val) {\n\t\treturn divideBy(val, DEFAULT_ROUNDING_MODE);\n\t}\n\n\t/**\n\t * 货币累除。\n\t *\n\t * <p>\n\t * 本货币对象金额除以除数，并返回本货币对象。\n\t * 如果相除后的金额不能转换为整数分，使用指定的取整模式\n\t * {@code roundingMode}进行取整。\n\t *\n\t * @param val 除数\n\t * @param roundingMode 保留小数方式\n\t * @return 累除后的结果。\n\t */\n\tpublic Money divideBy(BigDecimal val, RoundingMode roundingMode) {\n\t\tBigDecimal newCent = BigDecimal.valueOf(cent).divide(val, roundingMode);\n\n\t\tthis.cent = newCent.longValue();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 货币分配。\n\t *\n\t * <p>\n\t * 将本货币对象尽可能平均分配成{@code targets}份。\n\t * 如果不能平均分配尽，则将零头放到开始的若干份中。分配\n\t * 运算能够确保不会丢失金额零头。\n\t *\n\t * @param targets 待分配的份数\n\t * @return 货币对象数组，数组的长度与分配份数相同，数组元素\n\t * 从大到小排列，所有货币对象的金额最多只相差1分。\n\t */\n\tpublic Money[] allocate(int targets) {\n\t\tMoney[] results = new Money[targets];\n\n\t\tMoney lowResult = newMoneyWithSameCurrency(cent / targets);\n\t\tMoney highResult = newMoneyWithSameCurrency(lowResult.cent + 1);\n\n\t\tint remainder = (int) (cent % targets);\n\n\t\tfor (int i = 0; i < remainder; i++) {\n\t\t\tresults[i] = highResult;\n\t\t}\n\n\t\tfor (int i = remainder; i < targets; i++) {\n\t\t\tresults[i] = lowResult;\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * 货币分配。\n\t *\n\t * <p>\n\t * 将本货币对象按照规定的比例分配成若干份。分配所剩的零头\n\t * 从第一份开始顺序分配。分配运算确保不会丢失金额零头。\n\t *\n\t * @param ratios 分配比例数组，每一个比例是一个长整型，代表\n\t *               相对于总数的相对数。\n\t * @return 货币对象数组，数组的长度与分配比例数组的长度相同。\n\t */\n\tpublic Money[] allocate(long[] ratios) {\n\t\tMoney[] results = new Money[ratios.length];\n\n\t\tlong total = 0;\n\n\t\tfor (long element : ratios) {\n\t\t\ttotal += element;\n\t\t}\n\n\t\tlong remainder = cent;\n\n\t\tfor (int i = 0; i < results.length; i++) {\n\t\t\tresults[i] = newMoneyWithSameCurrency((cent * ratios[i]) / total);\n\t\t\tremainder -= results[i].cent;\n\t\t}\n\n\t\tfor (int i = 0; i < remainder; i++) {\n\t\t\tresults[i].cent++;\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// 格式化方法 =================================================\n\n\t/**\n\t * 生成本对象的缺省字符串表示\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn getAmount().toString();\n\t}\n\n\t// 内部方法 ===================================================\n\n\t/**\n\t * 断言本货币对象与另一货币对象是否具有相同的币种。\n\t *\n\t * <p>\n\t * 如果本货币对象与另一货币对象具有相同的币种，则方法返回。\n\t * 否则抛出运行时异常{@code java.lang.IllegalArgumentException}。\n\t *\n\t * @param other 另一货币对象\n\t * @throws IllegalArgumentException 如果本货币对象与另一货币对象币种不同。\n\t */\n\tprotected void assertSameCurrencyAs(Money other) {\n\t\tif (!currency.equals(other.currency)) {\n\t\t\tthrow new IllegalArgumentException(\"Money math currency mismatch.\");\n\t\t}\n\t}\n\n\t/**\n\t * 对BigDecimal型的值按指定取整方式取整。\n\t *\n\t * @param val          待取整的BigDecimal值\n\t * @param roundingMode 取整方式\n\t * @return 取整后的long型值\n\t */\n\tprotected long rounding(BigDecimal val, RoundingMode roundingMode) {\n\t\treturn val.setScale(0, roundingMode).longValue();\n\t}\n\n\t/**\n\t * 创建一个币种相同，具有指定金额的货币对象。\n\t *\n\t * @param cent 金额，以分为单位\n\t * @return 一个新建的币种相同，具有指定金额的货币对象\n\t */\n\tprotected Money newMoneyWithSameCurrency(long cent) {\n\t\tMoney money = new Money(0, currency);\n\n\t\tmoney.cent = cent;\n\n\t\treturn money;\n\t}\n\n\t// 调试方式 ==================================================\n\n\t/**\n\t * 生成本对象内部变量的字符串表示，用于调试。\n\t *\n\t * @return 本对象内部变量的字符串表示。\n\t */\n\tpublic String dump() {\n\t\treturn StrUtil.builder()\n\t\t\t\t.append(\"cent = \")\n\t\t\t\t.append(this.cent)\n\t\t\t\t.append(File.separatorChar)\n\t\t\t\t.append(\"currency = \")\n\t\t\t\t.append(this.currency)\n\t\t\t\t.toString();\n\t}\n\n\t/**\n\t * 设置货币的分值。\n\t *\n\t * @param cent 分值\n\t */\n\tpublic void setCent(long cent) {\n\t\tthis.cent = cent;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/math/package-info.java",
    "content": "/**\n * 提供数学计算相关封装，包括排列组合等，入口为MathUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.math;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/DefaultTrustManager.java",
    "content": "package cn.hutool.core.net;\n\nimport javax.net.ssl.SSLEngine;\nimport javax.net.ssl.X509ExtendedTrustManager;\nimport java.net.Socket;\nimport java.security.cert.X509Certificate;\n\n/**\n * 默认信任管理器，默认信任所有客户端和服务端证书<br>\n * 继承{@link X509ExtendedTrustManager}的原因见：https://blog.csdn.net/ghaohao/article/details/79454913\n *\n * @author Looly\n * @since 5.5.7\n */\npublic class DefaultTrustManager extends X509ExtendedTrustManager {\n\n\t/**\n\t * 默认的全局单例默认信任管理器，默认信任所有客户端和服务端证书\n\t * @since 5.7.8\n\t */\n\tpublic static DefaultTrustManager INSTANCE = new DefaultTrustManager();\n\n\t@Override\n\tpublic X509Certificate[] getAcceptedIssuers() {\n\t\treturn new X509Certificate[0];\n\t}\n\n\t@Override\n\tpublic void checkClientTrusted(X509Certificate[] chain, String authType) {\n\t}\n\n\t@Override\n\tpublic void checkServerTrusted(X509Certificate[] chain, String authType) {\n\t}\n\n\t@Override\n\tpublic void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) {\n\t}\n\n\t@Override\n\tpublic void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) {\n\t}\n\n\t@Override\n\tpublic void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) {\n\t}\n\n\t@Override\n\tpublic void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) {\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/FormUrlencoded.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.codec.PercentCodec;\n\n/**\n * application/x-www-form-urlencoded，遵循W3C HTML Form content types规范，如空格须转+，+须被编码<br>\n * 规范见：https://url.spec.whatwg.org/#urlencoded-serializing\n *\n * @since 5.7.16\n */\npublic class FormUrlencoded {\n\n\t/**\n\t * query中的value，默认除\"-\", \"_\", \".\", \"*\"外都编码<br>\n\t * 这个类似于JDK提供的{@link java.net.URLEncoder}\n\t */\n\tpublic static final PercentCodec ALL = PercentCodec.of(RFC3986.UNRESERVED)\n\t\t\t.removeSafe('~').addSafe('*').setEncodeSpaceAsPlus(true);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/Ipv4Util.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.lang.Validator;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\n\n/**\n * IPV4地址工具类\n *\n * <p>pr自：https://gitee.com/loolly/hutool/pulls/161</p>\n *\n * @author ZhuKun\n * @since 5.4.1\n */\npublic class Ipv4Util {\n\n\tpublic static final String LOCAL_IP = \"127.0.0.1\";\n\n\t/**\n\t * IP段的分割符\n\t */\n\tpublic static final String IP_SPLIT_MARK = \"-\";\n\n\t/**\n\t * IP与掩码的分割符\n\t */\n\tpublic static final String IP_MASK_SPLIT_MARK = StrUtil.SLASH;\n\n\t/**\n\t * 最大掩码位\n\t */\n\tpublic static final int IP_MASK_MAX = 32;\n\n\t/**\n\t * 格式化IP段\n\t *\n\t * @param ip   IP地址\n\t * @param mask 掩码\n\t * @return 返回xxx.xxx.xxx.xxx/mask的格式\n\t */\n\tpublic static String formatIpBlock(String ip, String mask) {\n\t\treturn ip + IP_MASK_SPLIT_MARK + getMaskBitByMask(mask);\n\t}\n\n\t/**\n\t * 智能转换IP地址集合\n\t *\n\t * @param ipRange IP段，支持X.X.X.X-X.X.X.X或X.X.X.X/X\n\t * @param isAll   true:全量地址，false:可用地址；仅在ipRange为X.X.X.X/X时才生效\n\t * @return IP集\n\t */\n\tpublic static List<String> list(String ipRange, boolean isAll) {\n\t\tif (ipRange.contains(IP_SPLIT_MARK)) {\n\t\t\t// X.X.X.X-X.X.X.X\n\t\t\tfinal String[] range = StrUtil.splitToArray(ipRange, IP_SPLIT_MARK);\n\t\t\treturn list(range[0], range[1]);\n\t\t} else if (ipRange.contains(IP_MASK_SPLIT_MARK)) {\n\t\t\t// X.X.X.X/X\n\t\t\tfinal String[] param = StrUtil.splitToArray(ipRange, IP_MASK_SPLIT_MARK);\n\t\t\treturn list(param[0], Integer.parseInt(param[1]), isAll);\n\t\t} else {\n\t\t\treturn ListUtil.toList(ipRange);\n\t\t}\n\t}\n\n\t/**\n\t * 根据IP地址、子网掩码获取IP地址区间\n\t *\n\t * @param ip      IP地址\n\t * @param maskBit 掩码位，例如24、32\n\t * @param isAll   true:全量地址，false:可用地址\n\t * @return 区间地址\n\t */\n\tpublic static List<String> list(String ip, int maskBit, boolean isAll) {\n\t\tif (maskBit == IP_MASK_MAX) {\n\t\t\tfinal List<String> list = new ArrayList<>();\n\t\t\tif (isAll) {\n\t\t\t\tlist.add(ip);\n\t\t\t}\n\t\t\treturn list;\n\t\t}\n\n\t\tString startIp = getBeginIpStr(ip, maskBit);\n\t\tString endIp = getEndIpStr(ip, maskBit);\n\t\tif (isAll) {\n\t\t\treturn list(startIp, endIp);\n\t\t}\n\n\t\tint lastDotIndex = startIp.lastIndexOf(CharUtil.DOT) + 1;\n\t\tstartIp = StrUtil.subPre(startIp, lastDotIndex) +\n\t\t\t\t(Integer.parseInt(Objects.requireNonNull(StrUtil.subSuf(startIp, lastDotIndex))) + 1);\n\t\tlastDotIndex = endIp.lastIndexOf(CharUtil.DOT) + 1;\n\t\tendIp = StrUtil.subPre(endIp, lastDotIndex) +\n\t\t\t\t(Integer.parseInt(Objects.requireNonNull(StrUtil.subSuf(endIp, lastDotIndex))) - 1);\n\t\treturn list(startIp, endIp);\n\t}\n\n\t/**\n\t * 得到IP地址区间\n\t *\n\t * @param ipFrom 开始IP\n\t * @param ipTo   结束IP\n\t * @return 区间地址\n\t */\n\tpublic static List<String> list(String ipFrom, String ipTo) {\n\t\t// 确定ip数量\n\t\tfinal int count = countByIpRange(ipFrom, ipTo);\n\t\tfinal int[] from = Convert.convert(int[].class, StrUtil.splitToArray(ipFrom, CharUtil.DOT));\n\t\tfinal int[] to = Convert.convert(int[].class, StrUtil.splitToArray(ipTo, CharUtil.DOT));\n\n\t\tfinal List<String> ips = new ArrayList<>(count);\n\t\t// 是否是循环的第一个值\n\t\tboolean aIsStart = true, bIsStart = true, cIsStart = true;\n\t\t// 是否是循环的最后一个值\n\t\tboolean aIsEnd, bIsEnd, cIsEnd;\n\t\t// 循环的结束值\n\t\tint aEnd = to[0], bEnd, cEnd, dEnd;\n\t\tfor (int a = from[0]; a <= aEnd; a++) {\n\t\t\taIsEnd = (a == aEnd);\n\t\t\t// 本次循环的结束结束值\n\t\t\tbEnd = aIsEnd ? to[1] : 255;\n\t\t\tfor (int b = (aIsStart ? from[1] : 0); b <= bEnd; b++) {\n\t\t\t\t// 在上一个循环是最后值的基础上进行判断\n\t\t\t\tbIsEnd = aIsEnd && (b == bEnd);\n\t\t\t\tcEnd = bIsEnd ? to[2] : 255;\n\t\t\t\tfor (int c = (bIsStart ? from[2] : 0); c <= cEnd; c++) {\n\t\t\t\t\t// 在之前循环是最后值的基础上进行判断\n\t\t\t\t\tcIsEnd = bIsEnd && (c == cEnd);\n\t\t\t\t\tdEnd = cIsEnd ? to[3] : 255;\n\t\t\t\t\tfor (int d = (cIsStart ? from[3] : 0); d <= dEnd; d++) {\n\t\t\t\t\t\tips.add(a + \".\" + b + \".\" + c + \".\" + d);\n\t\t\t\t\t}\n\t\t\t\t\tcIsStart = false;\n\t\t\t\t}\n\t\t\t\tbIsStart = false;\n\t\t\t}\n\t\t\taIsStart = false;\n\t\t}\n\t\treturn ips;\n\t}\n\n\t/**\n\t * 根据long值获取ip v4地址：xx.xx.xx.xx\n\t *\n\t * @param longIP IP的long表示形式\n\t * @return IP V4 地址\n\t */\n\tpublic static String longToIpv4(long longIP) {\n\t\tfinal StringBuilder sb = StrUtil.builder();\n\t\t// 直接右移24位\n\t\tsb.append(longIP >> 24 & 0xFF);\n\t\tsb.append(CharUtil.DOT);\n\t\t// 将高8位置0，然后右移16位\n\t\tsb.append(longIP >> 16 & 0xFF);\n\t\tsb.append(CharUtil.DOT);\n\t\tsb.append(longIP >> 8 & 0xFF);\n\t\tsb.append(CharUtil.DOT);\n\t\tsb.append(longIP & 0xFF);\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 根据ip地址(xxx.xxx.xxx.xxx)计算出long型的数据\n\t * 方法别名：inet_aton\n\t *\n\t * @param strIP IP V4 地址\n\t * @return long值\n\t */\n\tpublic static long ipv4ToLong(String strIP) {\n\t\tfinal Matcher matcher = PatternPool.IPV4.matcher(strIP);\n\t\tif (matcher.matches()) {\n\t\t\treturn matchAddress(matcher);\n\t\t}\n//\t\tValidator.validateIpv4(strIP, \"Invalid IPv4 address!\");\n//\t\tfinal long[] ip = Convert.convert(long[].class, StrUtil.split(strIP, CharUtil.DOT));\n//\t\treturn (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];\n\t\tthrow new IllegalArgumentException(\"Invalid IPv4 address!\");\n\t}\n\n\t/**\n\t * 根据ip地址(xxx.xxx.xxx.xxx)计算出long型的数据, 如果格式不正确返回 defaultValue\n\t * @param strIP IP V4 地址\n\t * @param defaultValue 默认值\n\t * @return long值\n\t */\n\tpublic static long ipv4ToLong(String strIP, long defaultValue) {\n\t\treturn Validator.isIpv4(strIP) ? ipv4ToLong(strIP) : defaultValue;\n\t}\n\n\t/**\n\t * 根据 ip/掩码位 计算IP段的起始IP（字符串型）\n\t * 方法别名：inet_ntoa\n\t *\n\t * @param ip      给定的IP，如218.240.38.69\n\t * @param maskBit 给定的掩码位，如30\n\t * @return 起始IP的字符串表示\n\t */\n\tpublic static String getBeginIpStr(String ip, int maskBit) {\n\t\treturn longToIpv4(getBeginIpLong(ip, maskBit));\n\t}\n\n\t/**\n\t * 根据 ip/掩码位 计算IP段的起始IP（Long型）\n\t *\n\t * @param ip      给定的IP，如218.240.38.69\n\t * @param maskBit 给定的掩码位，如30\n\t * @return 起始IP的长整型表示\n\t */\n\tpublic static Long getBeginIpLong(String ip, int maskBit) {\n\t\treturn ipv4ToLong(ip) & ipv4ToLong(getMaskByMaskBit(maskBit));\n\t}\n\n\t/**\n\t * 根据 ip/掩码位 计算IP段的终止IP（字符串型）\n\t *\n\t * @param ip      给定的IP，如218.240.38.69\n\t * @param maskBit 给定的掩码位，如30\n\t * @return 终止IP的字符串表示\n\t */\n\tpublic static String getEndIpStr(String ip, int maskBit) {\n\t\treturn longToIpv4(getEndIpLong(ip, maskBit));\n\t}\n\n\t/**\n\t * 根据子网掩码转换为掩码位\n\t *\n\t * @param mask 掩码的点分十进制表示，例如 255.255.255.0\n\t * @return 掩码位，例如 24\n\t * @throws IllegalArgumentException 子网掩码非法\n\t */\n\tpublic static int getMaskBitByMask(String mask) {\n\t\tInteger maskBit = MaskBit.getMaskBit(mask);\n\t\tif (maskBit == null) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid netmask \" + mask);\n\t\t}\n\t\treturn maskBit;\n\t}\n\n\t/**\n\t * 计算子网大小\n\t *\n\t * @param maskBit 掩码位\n\t * @param isAll   true:全量地址，false:可用地址\n\t * @return 地址总数\n\t */\n\tpublic static int countByMaskBit(int maskBit, boolean isAll) {\n\t\t//如果是可用地址的情况，掩码位小于等于0或大于等于32，则可用地址为0\n\t\tif ((false == isAll) && (maskBit <= 0 || maskBit >= 32)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal int count = (int) Math.pow(2, 32 - maskBit);\n\t\treturn isAll ? count : count - 2;\n\t}\n\n\t/**\n\t * 根据掩码位获取掩码\n\t *\n\t * @param maskBit 掩码位\n\t * @return 掩码\n\t */\n\tpublic static String getMaskByMaskBit(int maskBit) {\n\t\treturn MaskBit.get(maskBit);\n\t}\n\n\t/**\n\t * 根据开始IP与结束IP计算掩码\n\t *\n\t * @param fromIp 开始IP\n\t * @param toIp   结束IP\n\t * @return 掩码x.x.x.x\n\t */\n\tpublic static String getMaskByIpRange(String fromIp, String toIp) {\n\t\tlong toIpLong = ipv4ToLong(toIp);\n\t\tlong fromIpLong = ipv4ToLong(fromIp);\n\t\tAssert.isTrue(fromIpLong < toIpLong, \"to IP must be greater than from IP!\");\n\n\t\tString[] fromIpSplit = StrUtil.splitToArray(fromIp, CharUtil.DOT);\n\t\tString[] toIpSplit = StrUtil.splitToArray(toIp, CharUtil.DOT);\n\t\tStringBuilder mask = new StringBuilder();\n\t\tfor (int i = 0; i < toIpSplit.length; i++) {\n\t\t\tmask.append(255 - Integer.parseInt(toIpSplit[i]) + Integer.parseInt(fromIpSplit[i])).append(CharUtil.DOT);\n\t\t}\n\t\treturn mask.substring(0, mask.length() - 1);\n\t}\n\n\t/**\n\t * 计算IP区间有多少个IP\n\t *\n\t * @param fromIp 开始IP\n\t * @param toIp   结束IP\n\t * @return IP数量\n\t */\n\tpublic static int countByIpRange(String fromIp, String toIp) {\n\t\tlong toIpLong = ipv4ToLong(toIp);\n\t\tlong fromIpLong = ipv4ToLong(fromIp);\n\t\tif (fromIpLong > toIpLong) {\n\t\t\tthrow new IllegalArgumentException(\"to IP must be greater than from IP!\");\n\t\t}\n\t\tint count = 1;\n\t\tint[] fromIpSplit = StrUtil.split(fromIp, CharUtil.DOT).stream().mapToInt(Integer::parseInt).toArray();\n\t\tint[] toIpSplit = StrUtil.split(toIp, CharUtil.DOT).stream().mapToInt(Integer::parseInt).toArray();\n\t\tfor (int i = fromIpSplit.length - 1; i >= 0; i--) {\n\t\t\tcount += (toIpSplit[i] - fromIpSplit[i]) * Math.pow(256, fromIpSplit.length - i - 1);\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 判断掩码是否合法\n\t *\n\t * @param mask 掩码的点分十进制表示，例如 255.255.255.0\n\t * @return true：掩码合法；false：掩码不合法\n\t */\n\tpublic static boolean isMaskValid(String mask) {\n\t\treturn MaskBit.getMaskBit(mask) != null;\n\t}\n\n\t/**\n\t * 判断掩码位是否合法\n\t *\n\t * @param maskBit 掩码位，例如 24\n\t * @return true：掩码位合法；false：掩码位不合法\n\t */\n\tpublic static boolean isMaskBitValid(int maskBit) {\n\t\treturn MaskBit.get(maskBit) != null;\n\t}\n\n\t/**\n\t * 判定是否为内网IPv4<br>\n\t * 私有IP：\n\t * <pre>\n\t * A类 10.0.0.0-10.255.255.255\n\t * B类 172.16.0.0-172.31.255.255\n\t * C类 192.168.0.0-192.168.255.255\n\t * </pre>\n\t * 当然，还有127这个网段是环回地址\n\t *\n\t * @param ipAddress IP地址\n\t * @return 是否为内网IP\n\t * @since 5.7.18\n\t */\n\tpublic static boolean isInnerIP(String ipAddress) {\n\t\tboolean isInnerIp;\n\t\tlong ipNum = ipv4ToLong(ipAddress);\n\n\t\tlong aBegin = ipv4ToLong(\"10.0.0.0\");\n\t\tlong aEnd = ipv4ToLong(\"10.255.255.255\");\n\n\t\tlong bBegin = ipv4ToLong(\"172.16.0.0\");\n\t\tlong bEnd = ipv4ToLong(\"172.31.255.255\");\n\n\t\tlong cBegin = ipv4ToLong(\"192.168.0.0\");\n\t\tlong cEnd = ipv4ToLong(\"192.168.255.255\");\n\n\t\tisInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || LOCAL_IP.equals(ipAddress);\n\t\treturn isInnerIp;\n\t}\n\n\t/**\n\t * 检测指定 IP 地址是否匹配通配符 wildcard\n\t *\n\t * @param wildcard   通配符，如 192.168.*.1\n\t * @param ipAddress 待检测的 IP 地址\n\t * @return 是否匹配\n\t */\n\tpublic static boolean matches(String wildcard, String ipAddress) {\n\t\tif (false == ReUtil.isMatch(PatternPool.IPV4, ipAddress)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String[] wildcardSegments = wildcard.split(\"\\\\.\");\n\t\tfinal String[] ipSegments = ipAddress.split(\"\\\\.\");\n\n\t\tif (wildcardSegments.length != ipSegments.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < wildcardSegments.length; i++) {\n\t\t\tif (false == \"*\".equals(wildcardSegments[i])\n\t\t\t\t&& false == wildcardSegments[i].equals(ipSegments[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t//-------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 根据 ip/掩码位 计算IP段的终止IP（Long型）\n\t * 注：此接口返回负数，请使用转成字符串后再转Long型\n\t *\n\t * @param ip      给定的IP，如218.240.38.69\n\t * @param maskBit 给定的掩码位，如30\n\t * @return 终止IP的长整型表示\n\t */\n\tpublic static Long getEndIpLong(String ip, int maskBit) {\n\t\treturn getBeginIpLong(ip, maskBit)\n\t\t\t\t+ (0xffffffffL & ~ipv4ToLong(getMaskByMaskBit(maskBit)));\n\t}\n\n\t/**\n\t * 将匹配到的Ipv4地址的4个分组分别处理\n\t *\n\t * @param matcher 匹配到的Ipv4正则\n\t * @return ipv4对应long\n\t */\n\tprivate static long matchAddress(Matcher matcher) {\n\t\tlong addr = 0;\n\t\tfor (int i = 1; i <= 4; ++i) {\n\t\t\taddr |= Long.parseLong(matcher.group(i)) << 8 * (4 - i);\n\t\t}\n\t\treturn addr;\n\t}\n\n\t/**\n\t * 指定IP的long是否在指定范围内\n\t *\n\t * @param userIp 用户IP\n\t * @param begin  开始IP\n\t * @param end    结束IP\n\t * @return 是否在范围内\n\t */\n\tprivate static boolean isInner(long userIp, long begin, long end) {\n\t\treturn (userIp >= begin) && (userIp <= end);\n\t}\n\t//-------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/LocalPortGenerater.java",
    "content": "package cn.hutool.core.net;\n\nimport java.io.Serializable;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 本地端口生成器（LocalPortGenerator）。\n * <p>\n * 当前类名中“Generater”为拼写错误（正确应为 Generator），为保持兼容性暂未更改。\n *   该问题将在后续大版本中以重命名方式修复，并保留旧类名的弃用（@Deprecated）兼容层。\n * <p>\n *\n * 用于从指定起点开始递增探测一个当前“可用”的本地端口。探测通过短暂绑定\n * {@link java.net.ServerSocket}（以及可选 UDP DatagramSocket）完成，但不会真正占用端口。\n * <p>注意：</p>\n * <ul>\n *   <li>该方法执行的是端口“探测”，非“分配”，返回端口不保证实际使用时仍然可用。</li>\n *   <li>存在 TOCTOU（检测到使用之间）竞态，多线程下可能返回同一端口。</li>\n *   <li>UDP 探测可能导致误判（TCP 可用但 UDP 被占用）。</li>\n *   <li>不适合作为生产级端口分配策略，推荐使用 {@code new ServerSocket(0)}。</li>\n * </ul>\n *\n * <p>未来版本计划：</p>\n * <ul>\n *   <li>修复类名拼写问题：将“Generater”更名为 “Generator”。</li>\n *   <li>提供真正可靠的端口获取实现（绑定即占用，避免竞态）。</li>\n *   <li>优化探测策略，减少不必要的 UDP 检测。</li>\n *   <li>提供更安全的随机端口生成 API。</li>\n * </ul>\n * @author looly\n * @since 4.0.3\n */\n\n\npublic class LocalPortGenerater  implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 备选的本地端口 */\n\tprivate final AtomicInteger alternativePort;\n\n\t/**\n\t * 构造\n\t *\n\t * @param beginPort 起始端口号\n\t */\n\tpublic LocalPortGenerater(int beginPort) {\n\t\talternativePort = new AtomicInteger(beginPort);\n\t}\n\n\t/**\n\t * 生成一个本地端口，用于远程端口映射\n\t *\n\t * @return 未被使用的本地端口\n\t */\n\tpublic int generate() {\n\t\tint validPort = alternativePort.get();\n\t\t// 获取可用端口\n\t\twhile (false == NetUtil.isUsableLocalPort(validPort)) {\n\t\t\tvalidPort = alternativePort.incrementAndGet();\n\t\t}\n\t\treturn validPort;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/MaskBit.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.map.BiMap;\nimport java.util.HashMap;\n\n/**\n * 掩码位和掩码之间的Map对应\n *\n * @since 5.4.1\n * @author looly\n */\npublic class MaskBit {\n\n\t/**\n\t * 掩码位与掩码的点分十进制的双向对应关系\n\t */\n\tprivate static final BiMap<Integer, String> MASK_BIT_MAP;\n\tstatic {\n\t\tMASK_BIT_MAP = new BiMap<>(new HashMap<>(32));\n\t\tMASK_BIT_MAP.put(1, \"128.0.0.0\");\n\t\tMASK_BIT_MAP.put(2, \"192.0.0.0\");\n\t\tMASK_BIT_MAP.put(3, \"224.0.0.0\");\n\t\tMASK_BIT_MAP.put(4, \"240.0.0.0\");\n\t\tMASK_BIT_MAP.put(5, \"248.0.0.0\");\n\t\tMASK_BIT_MAP.put(6, \"252.0.0.0\");\n\t\tMASK_BIT_MAP.put(7, \"254.0.0.0\");\n\t\tMASK_BIT_MAP.put(8, \"255.0.0.0\");\n\t\tMASK_BIT_MAP.put(9, \"255.128.0.0\");\n\t\tMASK_BIT_MAP.put(10, \"255.192.0.0\");\n\t\tMASK_BIT_MAP.put(11, \"255.224.0.0\");\n\t\tMASK_BIT_MAP.put(12, \"255.240.0.0\");\n\t\tMASK_BIT_MAP.put(13, \"255.248.0.0\");\n\t\tMASK_BIT_MAP.put(14, \"255.252.0.0\");\n\t\tMASK_BIT_MAP.put(15, \"255.254.0.0\");\n\t\tMASK_BIT_MAP.put(16, \"255.255.0.0\");\n\t\tMASK_BIT_MAP.put(17, \"255.255.128.0\");\n\t\tMASK_BIT_MAP.put(18, \"255.255.192.0\");\n\t\tMASK_BIT_MAP.put(19, \"255.255.224.0\");\n\t\tMASK_BIT_MAP.put(20, \"255.255.240.0\");\n\t\tMASK_BIT_MAP.put(21, \"255.255.248.0\");\n\t\tMASK_BIT_MAP.put(22, \"255.255.252.0\");\n\t\tMASK_BIT_MAP.put(23, \"255.255.254.0\");\n\t\tMASK_BIT_MAP.put(24, \"255.255.255.0\");\n\t\tMASK_BIT_MAP.put(25, \"255.255.255.128\");\n\t\tMASK_BIT_MAP.put(26, \"255.255.255.192\");\n\t\tMASK_BIT_MAP.put(27, \"255.255.255.224\");\n\t\tMASK_BIT_MAP.put(28, \"255.255.255.240\");\n\t\tMASK_BIT_MAP.put(29, \"255.255.255.248\");\n\t\tMASK_BIT_MAP.put(30, \"255.255.255.252\");\n\t\tMASK_BIT_MAP.put(31, \"255.255.255.254\");\n\t\tMASK_BIT_MAP.put(32, \"255.255.255.255\");\n\t}\n\n\t/**\n\t * 根据掩码位获取掩码\n\t *\n\t * @param maskBit 掩码位\n\t * @return 掩码\n\t */\n\tpublic static String get(int maskBit) {\n\t\treturn MASK_BIT_MAP.get(maskBit);\n\t}\n\n\t/**\n\t * 根据掩码获取掩码位\n\t *\n\t * @param mask 掩码的点分十进制表示，如 255.255.255.0\n\t *\n\t * @return 掩码位，如 24；如果掩码不合法，则返回null\n\t * @since 5.6.5\n\t */\n\tpublic static Integer getMaskBit(String mask) {\n\t\treturn MASK_BIT_MAP.getKey(mask);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.EnumerationIter;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.JNDIUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attribute;\nimport javax.naming.directory.Attributes;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.math.BigInteger;\nimport java.net.*;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\nimport java.util.*;\n\n/**\n * 网络相关工具\n *\n * @author looly\n */\npublic class NetUtil {\n\n\t/**\n\t * 本地IPv4地址\n\t */\n\tpublic final static String LOCAL_IP = Ipv4Util.LOCAL_IP;\n\n\t/**\n\t * 本地主机名称\n\t */\n\tpublic static String localhostName;\n\n\t/**\n\t * 默认最小端口，1024\n\t */\n\tpublic static final int PORT_RANGE_MIN = 1024;\n\t/**\n\t * 默认最大端口，65535\n\t */\n\tpublic static final int PORT_RANGE_MAX = 0xFFFF;\n\n\t/**\n\t * 根据long值获取ip v4地址\n\t *\n\t * @param longIP IP的long表示形式\n\t * @return IP V4 地址\n\t * @see Ipv4Util#longToIpv4(long)\n\t */\n\tpublic static String longToIpv4(long longIP) {\n\t\treturn Ipv4Util.longToIpv4(longIP);\n\t}\n\n\t/**\n\t * 根据ip地址计算出long型的数据\n\t *\n\t * @param strIP IP V4 地址\n\t * @return long值\n\t * @see Ipv4Util#ipv4ToLong(String)\n\t */\n\tpublic static long ipv4ToLong(String strIP) {\n\t\treturn Ipv4Util.ipv4ToLong(strIP);\n\t}\n\n\t/**\n\t * 将IPv6地址字符串转为大整数\n\t *\n\t * @param ipv6Str 字符串\n\t * @return 大整数, 如发生异常返回 null\n\t * @since 5.5.7\n\t * @deprecated 拼写错误，请使用{@link #ipv6ToBigInteger(String)}\n\t */\n\t@Deprecated\n\tpublic static BigInteger ipv6ToBitInteger(String ipv6Str) {\n\t\treturn ipv6ToBigInteger(ipv6Str);\n\t}\n\n\t/**\n\t * 将IPv6地址字符串转为大整数\n\t *\n\t * @param ipv6Str 字符串\n\t * @return 大整数, 如发生异常返回 null\n\t * @since 5.5.7\n\t */\n\tpublic static BigInteger ipv6ToBigInteger(String ipv6Str) {\n\t\ttry {\n\t\t\tInetAddress address = InetAddress.getByName(ipv6Str);\n\t\t\tif (address instanceof Inet6Address) {\n\t\t\t\treturn new BigInteger(1, address.getAddress());\n\t\t\t}\n\t\t} catch (UnknownHostException ignore) {\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将大整数转换成ipv6字符串\n\t *\n\t * @param bigInteger 大整数\n\t * @return IPv6字符串, 如发生异常返回 null\n\t * @since 5.5.7\n\t */\n\tpublic static String bigIntegerToIPv6(BigInteger bigInteger) {\n\t\t// 确保 BigInteger 在 IPv6 地址范围内\n\t\tif (bigInteger.compareTo(BigInteger.ZERO) < 0 || bigInteger.compareTo(new BigInteger(\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\", 16)) > 0) {\n\t\t\tthrow new IllegalArgumentException(\"BigInteger value is out of IPv6 range\");\n\t\t}\n\n\t\t// 将 BigInteger 转换为 16 字节的字节数组\n\t\tbyte[] bytes = bigInteger.toByteArray();\n\t\tif (bytes.length > 16) {\n\t\t\t// 如果字节数组长度大于 16，去掉前导零\n\t\t\tint offset = bytes[0] == 0 ? 1 : 0;\n\t\t\tfinal byte[] newBytes = new byte[16];\n\t\t\tSystem.arraycopy(bytes, offset, newBytes, 0, 16);\n\t\t\tbytes = newBytes;\n\t\t} else if (bytes.length < 16) {\n\t\t\t// 如果字节数组长度小于 16，前面补零\n\t\t\tbyte[] paddedBytes = new byte[16];\n\t\t\tSystem.arraycopy(bytes, 0, paddedBytes, 16 - bytes.length, bytes.length);\n\t\t\tbytes = paddedBytes;\n\t\t}\n\n\t\t// 将字节数组转换为 IPv6 地址字符串\n\t\ttry {\n\t\t\treturn Inet6Address.getByAddress(bytes).getHostAddress();\n\t\t} catch (UnknownHostException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 检测本地端口可用性<br>\n\t * 来自org.springframework.util.SocketUtils\n\t *\n\t * @param port 被检测的端口\n\t * @return 是否可用\n\t */\n\tpublic static boolean isUsableLocalPort(int port) {\n\t\tif (false == isValidPort(port)) {\n\t\t\t// 给定的IP未在指定端口范围中\n\t\t\treturn false;\n\t\t}\n\n\t\t// issue#765@Github, 某些绑定非127.0.0.1的端口无法被检测到\n\t\ttry (ServerSocket ss = new ServerSocket(port)) {\n\t\t\tss.setReuseAddress(true);\n\t\t} catch (IOException ignored) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry (DatagramSocket ds = new DatagramSocket(port)) {\n\t\t\tds.setReuseAddress(true);\n\t\t} catch (IOException ignored) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 是否为有效的端口<br>\n\t * 此方法并不检查端口是否被占用\n\t *\n\t * @param port 端口号\n\t * @return 是否有效\n\t */\n\tpublic static boolean isValidPort(int port) {\n\t\t// 有效端口是0～65535\n\t\treturn port >= 0 && port <= PORT_RANGE_MAX;\n\t}\n\n\t/**\n\t * 查找1024~65535范围内的可用端口<br>\n\t * 此方法只检测给定范围内的随机一个端口，检测65535-1024次<br>\n\t * 来自org.springframework.util.SocketUtils\n\t *\n\t * @return 可用的端口\n\t * @since 4.5.4\n\t */\n\tpublic static int getUsableLocalPort() {\n\t\treturn getUsableLocalPort(PORT_RANGE_MIN);\n\t}\n\n\t/**\n\t * 查找指定范围内的可用端口，最大值为65535<br>\n\t * 此方法只检测给定范围内的随机一个端口，检测65535-minPort次<br>\n\t * 来自org.springframework.util.SocketUtils\n\t *\n\t * @param minPort 端口最小值（包含）\n\t * @return 可用的端口\n\t * @since 4.5.4\n\t */\n\tpublic static int getUsableLocalPort(int minPort) {\n\t\treturn getUsableLocalPort(minPort, PORT_RANGE_MAX);\n\t}\n\n\t/**\n\t * 查找指定范围内的可用端口<br>\n\t * 此方法只检测给定范围内的随机一个端口，检测maxPort-minPort次<br>\n\t * 来自org.springframework.util.SocketUtils\n\t *\n\t * @param minPort 端口最小值（包含）\n\t * @param maxPort 端口最大值（包含）\n\t * @return 可用的端口\n\t * @since 4.5.4\n\t */\n\tpublic static int getUsableLocalPort(int minPort, int maxPort) {\n\t\tfinal int maxPortExclude = maxPort + 1;\n\t\tint randomPort;\n\t\tfor (int i = minPort; i < maxPortExclude; i++) {\n\t\t\trandomPort = RandomUtil.randomInt(minPort, maxPortExclude);\n\t\t\tif (isUsableLocalPort(randomPort)) {\n\t\t\t\treturn randomPort;\n\t\t\t}\n\t\t}\n\n\t\tthrow new UtilException(\"Could not find an available port in the range [{}, {}] after {} attempts\", minPort, maxPort, maxPort - minPort);\n\t}\n\n\t/**\n\t * 获取多个本地可用端口<br>\n\t * 来自org.springframework.util.SocketUtils\n\t *\n\t * @param numRequested 尝试次数\n\t * @param minPort      端口最小值（包含）\n\t * @param maxPort      端口最大值（包含）\n\t * @return 可用的端口\n\t * @since 4.5.4\n\t */\n\tpublic static TreeSet<Integer> getUsableLocalPorts(int numRequested, int minPort, int maxPort) {\n\t\tfinal TreeSet<Integer> availablePorts = new TreeSet<>();\n\t\tint attemptCount = 0;\n\t\twhile ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) {\n\t\t\tavailablePorts.add(getUsableLocalPort(minPort, maxPort));\n\t\t}\n\n\t\tif (availablePorts.size() != numRequested) {\n\t\t\tthrow new UtilException(\"Could not find {} available  ports in the range [{}, {}]\", numRequested, minPort, maxPort);\n\t\t}\n\n\t\treturn availablePorts;\n\t}\n\n\t/**\n\t * 判定是否为内网IPv4<br>\n\t * 私有IP：\n\t * <pre>\n\t * A类 10.0.0.0-10.255.255.255\n\t * B类 172.16.0.0-172.31.255.255\n\t * C类 192.168.0.0-192.168.255.255\n\t * </pre>\n\t * 当然，还有127这个网段是环回地址\n\t *\n\t * @param ipAddress IP地址\n\t * @return 是否为内网IP\n\t * @see Ipv4Util#isInnerIP(String)\n\t */\n\tpublic static boolean isInnerIP(String ipAddress) {\n\t\treturn Ipv4Util.isInnerIP(ipAddress);\n\t}\n\n\t/**\n\t * 相对URL转换为绝对URL\n\t *\n\t * @param absoluteBasePath 基准路径，绝对\n\t * @param relativePath     相对路径\n\t * @return 绝对URL\n\t */\n\tpublic static String toAbsoluteUrl(String absoluteBasePath, String relativePath) {\n\t\ttry {\n\t\t\tURL absoluteUrl = new URL(absoluteBasePath);\n\t\t\treturn new URL(absoluteUrl, relativePath).toString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"To absolute url [{}] base [{}] error!\", relativePath, absoluteBasePath);\n\t\t}\n\t}\n\n\t/**\n\t * 隐藏掉IP地址的最后一部分为 * 代替\n\t *\n\t * @param ip IP地址\n\t * @return 隐藏部分后的IP\n\t */\n\tpublic static String hideIpPart(String ip) {\n\t\treturn StrUtil.builder(ip.length()).append(ip, 0, ip.lastIndexOf(\".\") + 1).append(\"*\").toString();\n\t}\n\n\t/**\n\t * 隐藏掉IP地址的最后一部分为 * 代替\n\t *\n\t * @param ip IP地址\n\t * @return 隐藏部分后的IP\n\t */\n\tpublic static String hideIpPart(long ip) {\n\t\treturn hideIpPart(longToIpv4(ip));\n\t}\n\n\t/**\n\t * 构建InetSocketAddress<br>\n\t * 当host中包含端口时（用“：”隔开），使用host中的端口，否则使用默认端口<br>\n\t * 给定host为空时使用本地host（127.0.0.1）\n\t *\n\t * @param host        Host\n\t * @param defaultPort 默认端口\n\t * @return InetSocketAddress\n\t */\n\tpublic static InetSocketAddress buildInetSocketAddress(String host, int defaultPort) {\n\t\tif (StrUtil.isBlank(host)) {\n\t\t\thost = LOCAL_IP;\n\t\t}\n\n\t\tString destHost;\n\t\tint port;\n\t\tint index = host.indexOf(\":\");\n\t\tif (index != -1) {\n\t\t\t// host:port形式\n\t\t\tdestHost = host.substring(0, index);\n\t\t\tport = Integer.parseInt(host.substring(index + 1));\n\t\t} else {\n\t\t\tdestHost = host;\n\t\t\tport = defaultPort;\n\t\t}\n\n\t\treturn new InetSocketAddress(destHost, port);\n\t}\n\n\t/**\n\t * 通过域名得到IP\n\t *\n\t * @param hostName HOST\n\t * @return ip address or hostName if UnknownHostException\n\t */\n\tpublic static String getIpByHost(String hostName) {\n\t\ttry {\n\t\t\treturn InetAddress.getByName(hostName).getHostAddress();\n\t\t} catch (UnknownHostException e) {\n\t\t\treturn hostName;\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定名称的网卡信息\n\t *\n\t * @param name 网络接口名，例如Linux下默认是eth0\n\t * @return 网卡，未找到返回{@code null}\n\t * @since 5.0.7\n\t */\n\tpublic static NetworkInterface getNetworkInterface(String name) {\n\t\tEnumeration<NetworkInterface> networkInterfaces;\n\t\ttry {\n\t\t\tnetworkInterfaces = NetworkInterface.getNetworkInterfaces();\n\t\t} catch (SocketException e) {\n\t\t\treturn null;\n\t\t}\n\n\t\tNetworkInterface netInterface;\n\t\twhile (networkInterfaces.hasMoreElements()) {\n\t\t\tnetInterface = networkInterfaces.nextElement();\n\t\t\tif (null != netInterface && name.equals(netInterface.getName())) {\n\t\t\t\treturn netInterface;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取本机所有网卡\n\t *\n\t * @return 所有网卡，异常返回{@code null}\n\t * @since 3.0.1\n\t */\n\tpublic static Collection<NetworkInterface> getNetworkInterfaces() {\n\t\tEnumeration<NetworkInterface> networkInterfaces;\n\t\ttry {\n\t\t\tnetworkInterfaces = NetworkInterface.getNetworkInterfaces();\n\t\t} catch (SocketException e) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn CollUtil.addAll(new ArrayList<>(), networkInterfaces);\n\t}\n\n\t/**\n\t * 获得本机的IPv4地址列表<br>\n\t * 返回的IP列表有序，按照系统设备顺序\n\t *\n\t * @return IP地址列表 {@link LinkedHashSet}\n\t */\n\tpublic static LinkedHashSet<String> localIpv4s() {\n\t\tfinal LinkedHashSet<InetAddress> localAddressList = localAddressList(t -> t instanceof Inet4Address);\n\n\t\treturn toIpList(localAddressList);\n\t}\n\n\t/**\n\t * 获得本机的IPv6地址列表<br>\n\t * 返回的IP列表有序，按照系统设备顺序\n\t *\n\t * @return IP地址列表 {@link LinkedHashSet}\n\t * @since 4.5.17\n\t */\n\tpublic static LinkedHashSet<String> localIpv6s() {\n\t\tfinal LinkedHashSet<InetAddress> localAddressList = localAddressList(t -> t instanceof Inet6Address);\n\n\t\treturn toIpList(localAddressList);\n\t}\n\n\t/**\n\t * 地址列表转换为IP地址列表\n\t *\n\t * @param addressList 地址{@link Inet4Address} 列表\n\t * @return IP地址字符串列表\n\t * @since 4.5.17\n\t */\n\tpublic static LinkedHashSet<String> toIpList(Set<InetAddress> addressList) {\n\t\tfinal LinkedHashSet<String> ipSet = new LinkedHashSet<>();\n\t\tfor (InetAddress address : addressList) {\n\t\t\tipSet.add(address.getHostAddress());\n\t\t}\n\n\t\treturn ipSet;\n\t}\n\n\t/**\n\t * 获得本机的IP地址列表（包括Ipv4和Ipv6）<br>\n\t * 返回的IP列表有序，按照系统设备顺序\n\t *\n\t * @return IP地址列表 {@link LinkedHashSet}\n\t */\n\tpublic static LinkedHashSet<String> localIps() {\n\t\tfinal LinkedHashSet<InetAddress> localAddressList = localAddressList(null);\n\t\treturn toIpList(localAddressList);\n\t}\n\n\t/**\n\t * 获取所有满足过滤条件的本地IP地址对象\n\t *\n\t * @param addressFilter 过滤器，null表示不过滤，获取所有地址\n\t * @return 过滤后的地址对象列表\n\t * @since 4.5.17\n\t */\n\tpublic static LinkedHashSet<InetAddress> localAddressList(Filter<InetAddress> addressFilter) {\n\t\treturn localAddressList(null, addressFilter);\n\t}\n\n\t/**\n\t * 获取所有满足过滤条件的本地IP地址对象\n\t *\n\t * @param addressFilter          过滤器，null表示不过滤，获取所有地址\n\t * @param networkInterfaceFilter 过滤器，null表示不过滤，获取所有网卡\n\t * @return 过滤后的地址对象列表\n\t */\n\tpublic static LinkedHashSet<InetAddress> localAddressList(Filter<NetworkInterface> networkInterfaceFilter, Filter<InetAddress> addressFilter) {\n\t\tEnumeration<NetworkInterface> networkInterfaces;\n\t\ttry {\n\t\t\tnetworkInterfaces = NetworkInterface.getNetworkInterfaces();\n\t\t} catch (SocketException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\n\t\tif (networkInterfaces == null) {\n\t\t\tthrow new UtilException(\"Get network interface error!\");\n\t\t}\n\n\t\tfinal LinkedHashSet<InetAddress> ipSet = new LinkedHashSet<>();\n\n\t\twhile (networkInterfaces.hasMoreElements()) {\n\t\t\tfinal NetworkInterface networkInterface = networkInterfaces.nextElement();\n\t\t\tif (networkInterfaceFilter != null && false == networkInterfaceFilter.accept(networkInterface)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfinal Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();\n\t\t\twhile (inetAddresses.hasMoreElements()) {\n\t\t\t\tfinal InetAddress inetAddress = inetAddresses.nextElement();\n\t\t\t\tif (inetAddress != null && (null == addressFilter || addressFilter.accept(inetAddress))) {\n\t\t\t\t\tipSet.add(inetAddress);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn ipSet;\n\t}\n\n\t/**\n\t * 获取本机网卡IP地址，这个地址为所有网卡中非回路地址的第一个<br>\n\t * 如果获取失败调用 {@link InetAddress#getLocalHost()}方法获取。<br>\n\t * 此方法不会抛出异常，获取失败将返回{@code null}<br>\n\t * <p>\n\t * 参考：http://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java\n\t *\n\t * @return 本机网卡IP地址，获取失败返回{@code null}\n\t * @since 3.0.7\n\t */\n\tpublic static String getLocalhostStr() {\n\t\tInetAddress localhost = getLocalhost();\n\t\tif (null != localhost) {\n\t\t\treturn localhost.getHostAddress();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取本机网卡IP地址，规则如下：\n\t *\n\t * <pre>\n\t * 1. 查找所有网卡地址，必须非回路（loopback）地址、非局域网地址（siteLocal）、IPv4地址\n\t * 2. 如果无满足要求的地址，调用 {@link InetAddress#getLocalHost()} 获取地址\n\t * </pre>\n\t * <p>\n\t * 此方法不会抛出异常，获取失败将返回{@code null}<br>\n\t * <p>\n\t * 见：https://github.com/chinabugotech/hutool/issues/428\n\t *\n\t * @return 本机网卡IP地址，获取失败返回{@code null}\n\t * @since 3.0.1\n\t */\n\tpublic static InetAddress getLocalhost() {\n\t\tfinal LinkedHashSet<InetAddress> localAddressList = localAddressList(address -> {\n\t\t\t// 非loopback地址，指127.*.*.*的地址\n\t\t\treturn false == address.isLoopbackAddress()\n\t\t\t\t// 需为IPV4地址\n\t\t\t\t&& address instanceof Inet4Address;\n\t\t});\n\n\t\tif (CollUtil.isNotEmpty(localAddressList)) {\n\t\t\tInetAddress address2 = null;\n\t\t\tfor (InetAddress inetAddress : localAddressList) {\n\t\t\t\tif (false == inetAddress.isSiteLocalAddress()) {\n\t\t\t\t\t// 非地区本地地址，指10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255\n\t\t\t\t\treturn inetAddress;\n\t\t\t\t} else if (null == address2) {\n\t\t\t\t\taddress2 = inetAddress;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (null != address2) {\n\t\t\t\treturn address2;\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\treturn InetAddress.getLocalHost();\n\t\t} catch (UnknownHostException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得本机MAC地址\n\t *\n\t * @return 本机MAC地址\n\t */\n\tpublic static String getLocalMacAddress() {\n\t\treturn getMacAddress(getLocalhost());\n\t}\n\n\t/**\n\t * 获得指定地址信息中的MAC地址，使用分隔符“-”\n\t *\n\t * @param inetAddress {@link InetAddress}\n\t * @return MAC地址，用-分隔\n\t */\n\tpublic static String getMacAddress(InetAddress inetAddress) {\n\t\treturn getMacAddress(inetAddress, \"-\");\n\t}\n\n\t/**\n\t * 获得指定地址信息中的MAC地址\n\t *\n\t * @param inetAddress {@link InetAddress}\n\t * @param separator   分隔符，推荐使用“-”或者“:”\n\t * @return MAC地址，用-分隔\n\t */\n\tpublic static String getMacAddress(InetAddress inetAddress, String separator) {\n\t\tif (null == inetAddress) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal byte[] mac = getHardwareAddress(inetAddress);\n\t\tif (null != mac) {\n\t\t\tfinal StringBuilder sb = new StringBuilder();\n\t\t\tString s;\n\t\t\tfor (int i = 0; i < mac.length; i++) {\n\t\t\t\tif (i != 0) {\n\t\t\t\t\tsb.append(separator);\n\t\t\t\t}\n\t\t\t\t// 字节转换为整数\n\t\t\t\ts = Integer.toHexString(mac[i] & 0xFF);\n\t\t\t\tsb.append(s.length() == 1 ? 0 + s : s);\n\t\t\t}\n\t\t\treturn sb.toString();\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得指定地址信息中的硬件地址\n\t *\n\t * @param inetAddress {@link InetAddress}\n\t * @return 硬件地址\n\t * @since 5.7.3\n\t */\n\tpublic static byte[] getHardwareAddress(InetAddress inetAddress) {\n\t\tif (null == inetAddress) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tfinal NetworkInterface networkInterface = NetworkInterface.getByInetAddress(inetAddress);\n\t\t\tif (null != networkInterface) {\n\t\t\t\treturn networkInterface.getHardwareAddress();\n\t\t\t}\n\t\t} catch (SocketException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得本机物理地址\n\t *\n\t * @return 本机物理地址\n\t * @since 5.7.3\n\t */\n\tpublic static byte[] getLocalHardwareAddress() {\n\t\treturn getHardwareAddress(getLocalhost());\n\t}\n\n\t/**\n\t * 获取主机名称，一次获取会缓存名称<br>\n\t * 注意此方法会触发反向DNS解析，导致阻塞，阻塞时间取决于网络！\n\t *\n\t * @return 主机名称\n\t * @since 5.4.4\n\t */\n\tpublic static String getLocalHostName() {\n\t\tif (StrUtil.isNotBlank(localhostName)) {\n\t\t\treturn localhostName;\n\t\t}\n\n\t\tfinal InetAddress localhost = getLocalhost();\n\n\t\tif (null != localhost) {\n\t\t\tString name = localhost.getHostName();\n\t\t\tif (StrUtil.isEmpty(name)) {\n\t\t\t\tname = localhost.getHostAddress();\n\t\t\t}\n\t\t\tlocalhostName = name;\n\t\t}\n\n\t\treturn localhostName;\n\t}\n\n\t/**\n\t * 创建 {@link InetSocketAddress}\n\t *\n\t * @param host 域名或IP地址，空表示任意地址\n\t * @param port 端口，0表示系统分配临时端口\n\t * @return {@link InetSocketAddress}\n\t * @since 3.3.0\n\t */\n\tpublic static InetSocketAddress createAddress(String host, int port) {\n\t\tif (StrUtil.isBlank(host)) {\n\t\t\treturn new InetSocketAddress(port);\n\t\t}\n\t\treturn new InetSocketAddress(host, port);\n\t}\n\n\t/**\n\t * 简易的使用Socket发送数据\n\t *\n\t * @param host    Server主机\n\t * @param port    Server端口\n\t * @param isBlock 是否阻塞方式\n\t * @param data    需要发送的数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.3.0\n\t */\n\tpublic static void netCat(String host, int port, boolean isBlock, ByteBuffer data) throws IORuntimeException {\n\t\ttry (SocketChannel channel = SocketChannel.open(createAddress(host, port))) {\n\t\t\tchannel.configureBlocking(isBlock);\n\t\t\tchannel.write(data);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 使用普通Socket发送数据\n\t *\n\t * @param host Server主机\n\t * @param port Server端口\n\t * @param data 数据\n\t * @throws IORuntimeException IO异常\n\t * @since 3.3.0\n\t */\n\tpublic static void netCat(String host, int port, byte[] data) throws IORuntimeException {\n\t\tOutputStream out = null;\n\t\ttry (Socket socket = new Socket(host, port)) {\n\t\t\tout = socket.getOutputStream();\n\t\t\tout.write(data);\n\t\t\tout.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t}\n\n\t/**\n\t * 是否在CIDR规则配置范围内<br>\n\t * 方法来自：【成都】小邓\n\t *\n\t * @param ip   需要验证的IP\n\t * @param cidr CIDR规则\n\t * @return 是否在范围内\n\t * @since 4.0.6\n\t */\n\tpublic static boolean isInRange(String ip, String cidr) {\n\t\tfinal int maskSplitMarkIndex = cidr.lastIndexOf(Ipv4Util.IP_MASK_SPLIT_MARK);\n\t\tif (maskSplitMarkIndex < 0) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid cidr: \" + cidr);\n\t\t}\n\n\t\tfinal long mask = (-1L << 32 - Integer.parseInt(cidr.substring(maskSplitMarkIndex + 1)));\n\t\tlong cidrIpAddr = ipv4ToLong(cidr.substring(0, maskSplitMarkIndex));\n\n\t\treturn (ipv4ToLong(ip) & mask) == (cidrIpAddr & mask);\n\t}\n\n\t/**\n\t * Unicode域名转puny code\n\t *\n\t * @param unicode Unicode域名\n\t * @return puny code\n\t * @since 4.1.22\n\t */\n\tpublic static String idnToASCII(String unicode) {\n\t\treturn IDN.toASCII(unicode);\n\t}\n\n\t/**\n\t * 从多级反向代理中获得第一个非unknown IP地址\n\t *\n\t * @param ip 获得的IP地址\n\t * @return 第一个非unknown IP地址\n\t * @since 4.4.1\n\t */\n\tpublic static String getMultistageReverseProxyIp(String ip) {\n\t\t// 多级反向代理检测\n\t\tif (ip != null && StrUtil.indexOf(ip, ',') > 0) {\n\t\t\tfinal List<String> ips = StrUtil.splitTrim(ip, ',');\n\t\t\tfor (final String subIp : ips) {\n\t\t\t\tif (false == isUnknown(subIp)) {\n\t\t\t\t\tip = subIp;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn ip;\n\t}\n\n\t/**\n\t * 检测给定字符串是否为未知，多用于检测HTTP请求相关<br>\n\t *\n\t * @param checkString 被检测的字符串\n\t * @return 是否未知\n\t * @since 5.2.6\n\t */\n\tpublic static boolean isUnknown(String checkString) {\n\t\treturn StrUtil.isBlank(checkString) || \"unknown\".equalsIgnoreCase(checkString);\n\t}\n\n\t/**\n\t * 检测IP地址是否能ping通\n\t *\n\t * @param ip IP地址\n\t * @return 返回是否ping通\n\t */\n\tpublic static boolean ping(String ip) {\n\t\treturn ping(ip, 200);\n\t}\n\n\t/**\n\t * 检测IP地址是否能ping通\n\t *\n\t * @param ip      IP地址\n\t * @param timeout 检测超时（毫秒）\n\t * @return 是否ping通\n\t */\n\tpublic static boolean ping(String ip, int timeout) {\n\t\ttry {\n\t\t\treturn InetAddress.getByName(ip).isReachable(timeout); // 当返回值是true时，说明host是可用的，false则不可。\n\t\t} catch (Exception ex) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 解析Cookie信息\n\t *\n\t * @param cookieStr Cookie字符串\n\t * @return cookie字符串\n\t * @since 5.2.6\n\t */\n\tpublic static List<HttpCookie> parseCookies(String cookieStr) {\n\t\tif (StrUtil.isBlank(cookieStr)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\treturn HttpCookie.parse(cookieStr);\n\t}\n\n\t/**\n\t * 检查远程端口是否开启\n\t *\n\t * @param address 远程地址\n\t * @param timeout 检测超时\n\t * @return 远程端口是否开启\n\t * @since 5.3.2\n\t */\n\tpublic static boolean isOpen(InetSocketAddress address, int timeout) {\n\t\ttry (Socket sc = new Socket()) {\n\t\t\tsc.connect(address, timeout);\n\t\t\treturn true;\n\t\t} catch (Exception e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 设置全局验证\n\t *\n\t * @param user 用户名\n\t * @param pass 密码，考虑安全，此处不使用String\n\t * @since 5.7.2\n\t */\n\tpublic static void setGlobalAuthenticator(String user, char[] pass) {\n\t\tsetGlobalAuthenticator(new UserPassAuthenticator(user, pass));\n\t}\n\n\t/**\n\t * 设置全局验证\n\t *\n\t * @param authenticator 验证器\n\t * @since 5.7.2\n\t */\n\tpublic static void setGlobalAuthenticator(Authenticator authenticator) {\n\t\tAuthenticator.setDefault(authenticator);\n\t}\n\n\t/**\n\t * 获取DNS信息，如TXT信息：\n\t *\n\t * <pre class=\"code\">\n\t *     NetUtil.attrNames(\"hutool.cn\", \"TXT\")\n\t * </pre>\n\t *\n\t * @param hostName  主机域名\n\t * @param attrNames 属性\n\t * @return DNS信息\n\t * @since 5.7.7\n\t */\n\tpublic static List<String> getDnsInfo(String hostName, String... attrNames) {\n\t\tfinal String uri = StrUtil.addPrefixIfNot(hostName, \"dns:\");\n\t\tfinal Attributes attributes = JNDIUtil.getAttributes(uri, attrNames);\n\n\t\tfinal List<String> infos = new ArrayList<>();\n\t\tfor (Attribute attribute : new EnumerationIter<>(attributes.getAll())) {\n\t\t\ttry {\n\t\t\t\tinfos.add((String) attribute.get());\n\t\t\t} catch (NamingException ignore) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\t\treturn infos;\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Private method start\n\n\t// ----------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/PassAuth.java",
    "content": "package cn.hutool.core.net;\n\nimport java.net.Authenticator;\nimport java.net.PasswordAuthentication;\n\n/**\n * 账号密码形式的{@link Authenticator} 实现。\n *\n * @author looly\n * @since 5.5.3\n */\npublic class PassAuth extends Authenticator {\n\n\t/**\n\t * 创建账号密码形式的{@link Authenticator} 实现。\n\t *\n\t * @param user 用户名\n\t * @param pass 密码\n\t * @return PassAuth\n\t */\n\tpublic static PassAuth of(String user, char[] pass) {\n\t\treturn new PassAuth(user, pass);\n\t}\n\n\tprivate final PasswordAuthentication auth;\n\n\t/**\n\t * 构造\n\t *\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic PassAuth(String user, char[] pass) {\n\t\tauth = new PasswordAuthentication(user, pass);\n\t}\n\n\t@Override\n\tprotected PasswordAuthentication getPasswordAuthentication() {\n\t\treturn auth;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/ProxySocketFactory.java",
    "content": "package cn.hutool.core.net;\n\nimport javax.net.SocketFactory;\nimport java.io.IOException;\nimport java.net.*;\n\n/**\n * 代理Socket工厂，用于创建代理Socket<br>\n * 来自commons-net的DefaultSocketFactory\n *\n * @author commons-net, looly\n * @since 5.8.23\n */\npublic class ProxySocketFactory extends SocketFactory {\n\n\t/**\n\t * 创建代理SocketFactory\n\t * @param proxy 代理对象\n\t * @return {@code ProxySocketFactory}\n\t */\n\tpublic static ProxySocketFactory of(final Proxy proxy) {\n\t\treturn new ProxySocketFactory(proxy);\n\t}\n\n\tprivate final Proxy proxy;\n\n\t/**\n\t * 构造\n\t *\n\t * @param proxy Socket代理\n\t */\n\tpublic ProxySocketFactory(final Proxy proxy) {\n\t\tthis.proxy = proxy;\n\t}\n\n\t@Override\n\tpublic Socket createSocket() {\n\t\tif (proxy != null) {\n\t\t\treturn new Socket(proxy);\n\t\t}\n\t\treturn new Socket();\n\t}\n\n\t@Override\n\tpublic Socket createSocket(final InetAddress address, final int port) throws IOException {\n\t\tif (proxy != null) {\n\t\t\tfinal Socket s = new Socket(proxy);\n\t\t\ts.connect(new InetSocketAddress(address, port));\n\t\t\treturn s;\n\t\t}\n\t\treturn new Socket(address, port);\n\t}\n\n\t@Override\n\tpublic Socket createSocket(final InetAddress address, final int port, final InetAddress localAddr, final int localPort) throws IOException {\n\t\tif (proxy != null) {\n\t\t\tfinal Socket s = new Socket(proxy);\n\t\t\ts.bind(new InetSocketAddress(localAddr, localPort));\n\t\t\ts.connect(new InetSocketAddress(address, port));\n\t\t\treturn s;\n\t\t}\n\t\treturn new Socket(address, port, localAddr, localPort);\n\t}\n\n\t@Override\n\tpublic Socket createSocket(final String host, final int port) throws IOException {\n\t\tif (proxy != null) {\n\t\t\tfinal Socket s = new Socket(proxy);\n\t\t\ts.connect(new InetSocketAddress(host, port));\n\t\t\treturn s;\n\t\t}\n\t\treturn new Socket(host, port);\n\t}\n\n\t@Override\n\tpublic Socket createSocket(final String host, final int port, final InetAddress localAddr, final int localPort) throws IOException {\n\t\tif (proxy != null) {\n\t\t\tfinal Socket s = new Socket(proxy);\n\t\t\ts.bind(new InetSocketAddress(localAddr, localPort));\n\t\t\ts.connect(new InetSocketAddress(host, port));\n\t\t\treturn s;\n\t\t}\n\t\treturn new Socket(host, port, localAddr, localPort);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/RFC3986.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.codec.PercentCodec;\n\n/**\n * <a href=\"https://www.ietf.org/rfc/rfc3986.html\">RFC3986</a> 编码实现<br>\n * 定义见：<a href=\"https://www.ietf.org/rfc/rfc3986.html#appendix-A\">https://www.ietf.org/rfc/rfc3986.html#appendix-A</a>\n *\n * @author looly\n * @since 5.7.16\n */\npublic class RFC3986 {\n\n\t/**\n\t * gen-delims = \":\" / \"/\" / \"?\" / \"#\" / \"[\" / \"]\" / \"@\"\n\t */\n\tpublic static final PercentCodec GEN_DELIMS = PercentCodec.of(\":/?#[]@\");\n\n\t/**\n\t * sub-delims = \"!\" / \"$\" / \"{@code &}\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t */\n\tpublic static final PercentCodec SUB_DELIMS = PercentCodec.of(\"!$&'()*+,;=\");\n\n\t/**\n\t * reserved = gen-delims / sub-delims<br>\n\t * see：<a href=\"https://www.ietf.org/rfc/rfc3986.html#section-2.2\">https://www.ietf.org/rfc/rfc3986.html#section-2.2</a>\n\t */\n\tpublic static final PercentCodec RESERVED = GEN_DELIMS.orNew(SUB_DELIMS);\n\n\t/**\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"<br>\n\t * see: <a href=\"https://www.ietf.org/rfc/rfc3986.html#section-2.3\">https://www.ietf.org/rfc/rfc3986.html#section-2.3</a>\n\t */\n\tpublic static final PercentCodec UNRESERVED = PercentCodec.of(unreservedChars());\n\n\t/**\n\t * pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t */\n\tpublic static final PercentCodec PCHAR = UNRESERVED.orNew(SUB_DELIMS).or(PercentCodec.of(\":@\"));\n\n\t/**\n\t * segment  = pchar<br>\n\t * see: <a href=\"https://www.ietf.org/rfc/rfc3986.html#section-3.3\">https://www.ietf.org/rfc/rfc3986.html#section-3.3</a>\n\t */\n\tpublic static final PercentCodec SEGMENT = PCHAR;\n\t/**\n\t * segment-nz-nc  = SEGMENT ; non-zero-length segment without any colon \":\"\n\t */\n\tpublic static final PercentCodec SEGMENT_NZ_NC = PercentCodec.of(SEGMENT).removeSafe(':');\n\n\t/**\n\t * path = segment / \"/\"\n\t */\n\tpublic static final PercentCodec PATH = SEGMENT.orNew(PercentCodec.of(\"/\"));\n\n\t/**\n\t * query = pchar / \"/\" / \"?\"\n\t */\n\tpublic static final PercentCodec QUERY = PCHAR.orNew(PercentCodec.of(\"/?\"));\n\n\t/**\n\t * fragment     = pchar / \"/\" / \"?\"\n\t */\n\tpublic static final PercentCodec FRAGMENT = QUERY;\n\n\t/**\n\t * query中的value<br>\n\t * value不能包含\"{@code &}\"，可以包含 \"=\"\n\t */\n\tpublic static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(QUERY).removeSafe('&');\n\n\t/**\n\t * query中的value编码器，严格模式，value中不能包含任何分隔符。\n\t */\n\tpublic static final PercentCodec QUERY_PARAM_VALUE_STRICT = UNRESERVED;\n\n\t/**\n\t * query中的key<br>\n\t * key不能包含\"{@code &}\" 和 \"=\"\n\t */\n\tpublic static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY_PARAM_VALUE).removeSafe('=');\n\n\t/**\n\t * query中的key编码器，严格模式，key中不能包含任何分隔符。\n\t */\n\tpublic static final PercentCodec QUERY_PARAM_NAME_STRICT = UNRESERVED;\n\n\t/**\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t *\n\t * @return unreserved字符\n\t */\n\tprivate static StringBuilder unreservedChars() {\n\t\tStringBuilder sb = new StringBuilder();\n\n\t\t// ALPHA\n\t\tfor (char c = 'A'; c <= 'Z'; c++) {\n\t\t\tsb.append(c);\n\t\t}\n\t\tfor (char c = 'a'; c <= 'z'; c++) {\n\t\t\tsb.append(c);\n\t\t}\n\n\t\t// DIGIT\n\t\tfor (char c = '0'; c <= '9'; c++) {\n\t\t\tsb.append(c);\n\t\t}\n\n\t\t// \"-\" / \".\" / \"_\" / \"~\"\n\t\tsb.append(\"_.-~\");\n\n\t\treturn sb;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/SSLContextBuilder.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport java.security.GeneralSecurityException;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\n\n/**\n * {@link SSLContext}构建器，可以自定义：<br>\n * <ul>\n *     <li>协议（protocol），默认TLS</li>\n *     <li>{@link KeyManager}，默认空</li>\n *     <li>{@link TrustManager}，默认{@link DefaultTrustManager}，即信任全部</li>\n *     <li>{@link SecureRandom}</li>\n * </ul>\n * <p>\n * 构建后可获得{@link SSLContext}，通过调用{@link SSLContext#getSocketFactory()}获取{@link javax.net.ssl.SSLSocketFactory}\n *\n * @author Looly\n * @since 5.5.2\n */\npublic class SSLContextBuilder implements SSLProtocols, Builder<SSLContext> {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tprivate String protocol = TLS;\n\tprivate KeyManager[] keyManagers;\n\tprivate TrustManager[] trustManagers = {DefaultTrustManager.INSTANCE};\n\tprivate SecureRandom secureRandom = new SecureRandom();\n\n\n\t/**\n\t * 创建 SSLContextBuilder\n\t *\n\t * @return SSLContextBuilder\n\t */\n\tpublic static SSLContextBuilder create() {\n\t\treturn new SSLContextBuilder();\n\t}\n\n\t/**\n\t * 设置协议。例如TLS等\n\t *\n\t * @param protocol 协议\n\t * @return 自身\n\t */\n\tpublic SSLContextBuilder setProtocol(String protocol) {\n\t\tif (StrUtil.isNotBlank(protocol)) {\n\t\t\tthis.protocol = protocol;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置信任信息\n\t *\n\t * @param trustManagers TrustManager列表\n\t * @return 自身\n\t */\n\tpublic SSLContextBuilder setTrustManagers(TrustManager... trustManagers) {\n\t\tif (ArrayUtil.isNotEmpty(trustManagers)) {\n\t\t\tthis.trustManagers = trustManagers;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置 JSSE key managers\n\t *\n\t * @param keyManagers JSSE key managers\n\t * @return 自身\n\t */\n\tpublic SSLContextBuilder setKeyManagers(KeyManager... keyManagers) {\n\t\tif (ArrayUtil.isNotEmpty(keyManagers)) {\n\t\t\tthis.keyManagers = keyManagers;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置 SecureRandom\n\t *\n\t * @param secureRandom SecureRandom\n\t * @return 自己\n\t */\n\tpublic SSLContextBuilder setSecureRandom(SecureRandom secureRandom) {\n\t\tif (null != secureRandom) {\n\t\t\tthis.secureRandom = secureRandom;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建{@link SSLContext}\n\t *\n\t * @return {@link SSLContext}\n\t */\n\t@Override\n\tpublic SSLContext build() {\n\t\treturn buildQuietly();\n\t}\n\n\t/**\n\t * 构建{@link SSLContext}需要处理异常\n\t *\n\t * @return {@link SSLContext}\n\t * @throws NoSuchAlgorithmException 无此算法异常\n\t * @throws KeyManagementException   密钥管理异常\n\t * @since 5.7.22\n\t */\n\tpublic SSLContext buildChecked() throws NoSuchAlgorithmException, KeyManagementException {\n\t\tSSLContext sslContext = SSLContext.getInstance(protocol);\n\t\tsslContext.init(this.keyManagers, this.trustManagers, this.secureRandom);\n\t\treturn sslContext;\n\t}\n\n\t/**\n\t * 构建{@link SSLContext}\n\t *\n\t * @return {@link SSLContext}\n\t * @throws IORuntimeException 包装 GeneralSecurityException异常\n\t */\n\tpublic SSLContext buildQuietly() throws IORuntimeException {\n\t\ttry {\n\t\t\treturn buildChecked();\n\t\t} catch (GeneralSecurityException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/SSLProtocols.java",
    "content": "package cn.hutool.core.net;\n\n/**\n * SSL或TLS协议\n *\n * @author looly\n * @since 5.7.8\n */\npublic interface SSLProtocols {\n\n\t/**\n\t * Supports some version of SSL; may support other versions\n\t */\n\tString SSL = \"SSL\";\n\t/**\n\t * Supports SSL version 2 or later; may support other versions\n\t */\n\tString SSLv2 = \"SSLv2\";\n\t/**\n\t * Supports SSL version 3; may support other versions\n\t */\n\tString SSLv3 = \"SSLv3\";\n\n\t/**\n\t * Supports some version of TLS; may support other versions\n\t */\n\tString TLS = \"TLS\";\n\t/**\n\t * Supports RFC 2246: TLS version 1.0 ; may support other versions\n\t */\n\tString TLSv1 = \"TLSv1\";\n\t/**\n\t * Supports RFC 4346: TLS version 1.1 ; may support other versions\n\t */\n\tString TLSv11 = \"TLSv1.1\";\n\t/**\n\t * Supports RFC 5246: TLS version 1.2 ; may support other versions\n\t */\n\tString TLSv12 = \"TLSv1.2\";\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/SSLUtil.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.io.IORuntimeException;\n\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\n\n/**\n * SSL(Secure Sockets Layer 安全套接字协议)相关工具封装\n *\n * @author looly\n * @since 5.5.2\n */\npublic class SSLUtil {\n\n\t/**\n\t * 创建{@link SSLContext}，默认信任全部\n\t *\n\t * @param protocol     SSL协议，例如TLS等\n\t * @return {@link SSLContext}\n\t * @throws IORuntimeException 包装 GeneralSecurityException异常\n\t * @since 5.7.8\n\t */\n\tpublic static SSLContext createSSLContext(String protocol) throws IORuntimeException{\n\t\treturn SSLContextBuilder.create().setProtocol(protocol).build();\n\t}\n\n\t/**\n\t * 创建{@link SSLContext}\n\t *\n\t * @param protocol     SSL协议，例如TLS等\n\t * @param keyManager   密钥管理器,{@code null}表示无\n\t * @param trustManager 信任管理器, {@code null}表示无\n\t * @return {@link SSLContext}\n\t * @throws IORuntimeException 包装 GeneralSecurityException异常\n\t */\n\tpublic static SSLContext createSSLContext(String protocol, KeyManager keyManager, TrustManager trustManager)\n\t\t\tthrows IORuntimeException {\n\t\treturn createSSLContext(protocol,\n\t\t\t\tkeyManager == null ? null : new KeyManager[]{keyManager},\n\t\t\t\ttrustManager == null ? null : new TrustManager[]{trustManager});\n\t}\n\n\t/**\n\t * 创建和初始化{@link SSLContext}\n\t *\n\t * @param protocol      SSL协议，例如TLS等\n\t * @param keyManagers   密钥管理器,{@code null}表示无\n\t * @param trustManagers 信任管理器, {@code null}表示无\n\t * @return {@link SSLContext}\n\t * @throws IORuntimeException 包装 GeneralSecurityException异常\n\t */\n\tpublic static SSLContext createSSLContext(String protocol, KeyManager[] keyManagers, TrustManager[] trustManagers) throws IORuntimeException {\n\t\treturn SSLContextBuilder.create()\n\t\t\t\t.setProtocol(protocol)\n\t\t\t\t.setKeyManagers(keyManagers)\n\t\t\t\t.setTrustManagers(trustManagers).build();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/URLDecoder.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\n\n/**\n * URL解码，数据内容的类型是 application/x-www-form-urlencoded。\n *\n * <pre>\n * 1. 将%20转换为空格 ;\n * 2. 将\"%xy\"转换为文本形式,xy是两位16进制的数值;\n * 3. 跳过不符合规范的%形式，直接输出\n * </pre>\n *\n * @author looly\n */\npublic class URLDecoder implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final byte ESCAPE_CHAR = '%';\n\n\t/**\n\t * 解码，不对+解码\n\t *\n\t * <ol>\n\t *     <li>将%20转换为空格</li>\n\t *     <li>将 \"%xy\"转换为文本形式,xy是两位16进制的数值</li>\n\t *     <li>跳过不符合规范的%形式，直接输出</li>\n\t * </ol>\n\t *\n\t * @param str     包含URL编码后的字符串\n\t * @param charset 编码\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decodeForPath(String str, Charset charset) {\n\t\treturn decode(str, charset, false);\n\t}\n\n\t/**\n\t * 解码<br>\n\t * 规则见：https://url.spec.whatwg.org/#urlencoded-parsing\n\t * <pre>\n\t *   1. 将+和%20转换为空格(\" \");\n\t *   2. 将\"%xy\"转换为文本形式,xy是两位16进制的数值;\n\t *   3. 跳过不符合规范的%形式，直接输出\n\t * </pre>\n\t *\n\t * @param str     包含URL编码后的字符串\n\t * @param charset 编码\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decode(String str, Charset charset) {\n\t\treturn decode(str, charset, true);\n\t}\n\n\t/**\n\t * 解码\n\t * <pre>\n\t *   1. 将%20转换为空格 ;\n\t *   2. 将\"%xy\"转换为文本形式,xy是两位16进制的数值;\n\t *   3. 跳过不符合规范的%形式，直接输出\n\t * </pre>\n\t *\n\t * @param str           包含URL编码后的字符串\n\t * @param isPlusToSpace 是否+转换为空格\n\t * @param charset       编码，{@code null}表示不做编码\n\t * @return 解码后的字符串\n\t */\n\tpublic static String decode(String str, Charset charset, boolean isPlusToSpace) {\n\t\tif(null == str || null == charset){\n\t\t\treturn str;\n\t\t}\n\n\t\tfinal int length = str.length();\n\t\tif(0 == length){\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal StringBuilder result = new StringBuilder(length / 3);\n\n\t\tint begin = 0;\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif(ESCAPE_CHAR == c || CharUtil.isHexChar(c)){\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 遇到非需要处理的字符跳过\n\t\t\t// 处理之前的hex字符\n\t\t\tif(i > begin){\n\t\t\t\tresult.append(decodeSub(str, begin, i, charset, isPlusToSpace));\n\t\t\t}\n\n\t\t\t// 非Hex字符，忽略本字符\n\t\t\tif('+' == c && isPlusToSpace){\n\t\t\t\tc = CharUtil.SPACE;\n\t\t\t}\n\n\t\t\t// 非Hex字符，忽略本字符\n\t\t\tresult.append(c);\n\t\t\tbegin = i + 1;\n\t\t}\n\n\t\t// 处理剩余字符\n\t\tif(begin < length){\n\t\t\tresult.append(decodeSub(str, begin, length, charset, isPlusToSpace));\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 解码\n\t * <pre>\n\t *   1. 将+和%20转换为空格 ;\n\t *   2. 将\"%xy\"转换为文本形式,xy是两位16进制的数值;\n\t *   3. 跳过不符合规范的%形式，直接输出\n\t * </pre>\n\t *\n\t * @param bytes url编码的bytes\n\t * @return 解码后的bytes\n\t */\n\tpublic static byte[] decode(byte[] bytes) {\n\t\treturn decode(bytes, true);\n\t}\n\n\t/**\n\t * 解码\n\t * <pre>\n\t *   1. 将%20转换为空格 ;\n\t *   2. 将\"%xy\"转换为文本形式,xy是两位16进制的数值;\n\t *   3. 跳过不符合规范的%形式，直接输出\n\t * </pre>\n\t *\n\t * @param bytes         url编码的bytes\n\t * @param isPlusToSpace 是否+转换为空格\n\t * @return 解码后的bytes\n\t * @since 5.6.3\n\t */\n\tpublic static byte[] decode(byte[] bytes, boolean isPlusToSpace) {\n\t\tif (bytes == null) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);\n\t\tint b;\n\t\tfor (int i = 0; i < bytes.length; i++) {\n\t\t\tb = bytes[i];\n\t\t\tif (b == '+') {\n\t\t\t\tbuffer.write(isPlusToSpace ? CharUtil.SPACE : b);\n\t\t\t} else if (b == ESCAPE_CHAR) {\n\t\t\t\tif (i + 1 < bytes.length) {\n\t\t\t\t\tfinal int u = CharUtil.digit16(bytes[i + 1]);\n\t\t\t\t\tif (u >= 0 && i + 2 < bytes.length) {\n\t\t\t\t\t\tfinal int l = CharUtil.digit16(bytes[i + 2]);\n\t\t\t\t\t\tif (l >= 0) {\n\t\t\t\t\t\t\tbuffer.write((char) ((u << 4) + l));\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// 跳过不符合规范的%形式\n\t\t\t\tbuffer.write(b);\n\t\t\t} else {\n\t\t\t\tbuffer.write(b);\n\t\t\t}\n\t\t}\n\t\treturn buffer.toByteArray();\n\t}\n\n\t/**\n\t * 解码子串\n\t *\n\t * @param str 字符串\n\t * @param begin 开始位置（包含）\n\t * @param end 结束位置（不包含）\n\t * @param charset 编码\n\t * @param isPlusToSpace 是否+转换为空格\n\t * @return 解码后的字符串\n\t */\n\tprivate static String decodeSub(final String str, final int begin, final int end,\n\t\t\t\t\t\t\t\t\tfinal Charset charset, final boolean isPlusToSpace){\n\t\treturn new String(decode(\n\t\t\t// 截取需要decode的部分\n\t\t\tstr.substring(begin, end).getBytes(CharsetUtil.CHARSET_ISO_8859_1), isPlusToSpace\n\t\t), charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/URLEncodeUtil.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * URL编码工具<br>\n * TODO 在6.x中移除此工具（无法很好区分URL编码和www-form编码）\n *\n * @since 5.7.13\n * @author looly\n */\npublic class URLEncodeUtil {\n\t/**\n\t * 编码URL，默认使用UTF-8编码<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。\n\t *\n\t * @param url URL\n\t * @return 编码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t */\n\tpublic static String encodeAll(String url) {\n\t\treturn encodeAll(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 编码URL<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。\n\t *\n\t * @param url     URL\n\t * @param charset 编码，为null表示不编码\n\t * @return 编码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t */\n\tpublic static String encodeAll(String url, Charset charset) throws UtilException {\n\t\treturn RFC3986.UNRESERVED.encode(url, charset);\n\t}\n\n\t/**\n\t * 编码URL，默认使用UTF-8编码<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于URL自动编码，类似于浏览器中键入地址自动编码，对于像类似于“/”的字符不再编码\n\t *\n\t * @param url URL\n\t * @return 编码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t * @since 3.1.2\n\t */\n\tpublic static String encode(String url) throws UtilException {\n\t\treturn encode(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 编码字符为 application/x-www-form-urlencoded<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于URL自动编码，类似于浏览器中键入地址自动编码，对于像类似于“/”的字符不再编码\n\t *\n\t * @param url     被编码内容\n\t * @param charset 编码\n\t * @return 编码后的字符\n\t * @since 4.4.1\n\t */\n\tpublic static String encode(String url, Charset charset) {\n\t\treturn RFC3986.PATH.encode(url, charset);\n\t}\n\n\t/**\n\t * 编码URL，默认使用UTF-8编码<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于POST请求中的请求体自动编码，转义大部分特殊字符\n\t *\n\t * @param url URL\n\t * @return 编码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t * @since 3.1.2\n\t */\n\tpublic static String encodeQuery(String url) throws UtilException {\n\t\treturn encodeQuery(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 编码字符为URL中查询语句<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于POST请求中的请求体自动编码，转义大部分特殊字符\n\t *\n\t * @param url     被编码内容\n\t * @param charset 编码\n\t * @return 编码后的字符\n\t * @since 4.4.1\n\t */\n\tpublic static String encodeQuery(String url, Charset charset) {\n\t\treturn RFC3986.QUERY.encode(url, charset);\n\t}\n\n\t/**\n\t * 编码URL，默认使用UTF-8编码<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于URL的Segment中自动编码，转义大部分特殊字符\n\t *\n\t * <pre>\n\t * pchar = unreserved（不处理） / pct-encoded / sub-delims（子分隔符） / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * @param url URL\n\t * @return 编码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t * @since 5.6.5\n\t */\n\tpublic static String encodePathSegment(String url) throws UtilException {\n\t\treturn encodePathSegment(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 编码字符为URL中查询语句<br>\n\t * 将需要转换的内容（ASCII码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>\n\t * 此方法用于URL的Segment中自动编码，转义大部分特殊字符\n\t *\n\t * <pre>\n\t * pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * @param url     被编码内容\n\t * @param charset 编码\n\t * @return 编码后的字符\n\t * @since 5.6.5\n\t */\n\tpublic static String encodePathSegment(String url, Charset charset) {\n\t\tif (StrUtil.isEmpty(url)) {\n\t\t\treturn url;\n\t\t}\n\t\treturn RFC3986.SEGMENT.encode(url, charset);\n\t}\n\n\t/**\n\t * 编码URL，默认使用UTF-8编码<br>\n\t * URL的Fragment URLEncoder<br>\n\t * 默认的编码器针对Fragment，定义如下：\n\t *\n\t * <pre>\n\t * fragment    = *( pchar / \"/\" / \"?\" )\n\t * pchar       = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims  = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 具体见：https://datatracker.ietf.org/doc/html/rfc3986#section-3.5\n\t *\n\t * @param url     被编码内容\n\t * @return 编码后的字符\n\t * @since 5.7.13\n\t */\n\tpublic static String encodeFragment(String url) throws UtilException {\n\t\treturn encodeFragment(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * URL的Fragment URLEncoder<br>\n\t * 默认的编码器针对Fragment，定义如下：\n\t *\n\t * <pre>\n\t * fragment    = *( pchar / \"/\" / \"?\" )\n\t * pchar       = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims  = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 具体见：https://datatracker.ietf.org/doc/html/rfc3986#section-3.5\n\t *\n\t * @param url     被编码内容\n\t * @param charset 编码\n\t * @return 编码后的字符\n\t * @since 5.7.13\n\t */\n\tpublic static String encodeFragment(String url, Charset charset) {\n\t\tif (StrUtil.isEmpty(url)) {\n\t\t\treturn url;\n\t\t}\n\t\treturn RFC3986.FRAGMENT.encode(url, charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.BitSet;\n\n/**\n * URL编码，数据内容的类型是 application/x-www-form-urlencoded。\n * TODO 6.x移除此类，使用PercentCodec代替（无法很好区分URL编码和www-form编码）\n *\n * <pre>\n * 1.字符\"a\"-\"z\"，\"A\"-\"Z\"，\"0\"-\"9\"，\".\"，\"-\"，\"*\"，和\"_\" 都不会被编码;\n * 2.将空格转换为%20 ;\n * 3.将非文本内容转换成\"%xy\"的形式,xy是两位16进制的数值;\n * </pre>\n *\n * @author looly\n * @see cn.hutool.core.codec.PercentCodec\n * @deprecated 此类中的方法并不规范，请使用 {@link RFC3986}\n */\n@Deprecated\npublic class URLEncoder implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// --------------------------------------------------------------------------------------------- Static method start\n\t/**\n\t * 默认URLEncoder<br>\n\t * 默认的编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * default = pchar / \"/\"\n\t * pchar = unreserved（不处理） / pct-encoded / sub-delims（子分隔符） / \":\" / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t */\n\tpublic static final URLEncoder DEFAULT = createDefault();\n\n\t/**\n\t * URL的Path的每一个Segment URLEncoder<br>\n\t * 默认的编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * pchar = unreserved / pct-encoded / sub-delims / \":\"（非空segment不包含:） / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 定义见：https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3\n\t */\n\tpublic static final URLEncoder PATH_SEGMENT = createPathSegment();\n\n\t/**\n\t * URL的Fragment URLEncoder<br>\n\t * 默认的编码器针对Fragment，定义如下：\n\t *\n\t * <pre>\n\t * fragment    = *( pchar / \"/\" / \"?\" )\n\t * pchar       = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims  = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 具体见：https://datatracker.ietf.org/doc/html/rfc3986#section-3.5\n\t * @since 5.7.13\n\t */\n\tpublic static final URLEncoder FRAGMENT = createFragment();\n\n\t/**\n\t * 用于查询语句的URLEncoder<br>\n\t * 编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * 0x20 ' ' =》 '+'\n\t * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is\n\t * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' Also '=' and '&amp;' 不编码\n\t * 其它编码为 %nn 形式\n\t * </pre>\n\t * <p>\n\t * 详细见：https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm\n\t */\n\tpublic static final URLEncoder QUERY = createQuery();\n\n\t/**\n\t * 全编码的URLEncoder<br>\n\t * <pre>\n\t *  0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is\n\t *  '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' 不编码\n\t *  其它编码为 %nn 形式\n\t * </pre>\n\t */\n\tpublic static final URLEncoder ALL = createAll();\n\n\t/**\n\t * 创建默认URLEncoder<br>\n\t * 默认的编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * default = pchar / \"/\"\n\t * pchar = unreserved（不处理） / pct-encoded / sub-delims（子分隔符） / \":\" / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * @return URLEncoder\n\t */\n\tpublic static URLEncoder createDefault() {\n\t\tfinal URLEncoder encoder = new URLEncoder();\n\t\tencoder.addSafeCharacter('-');\n\t\tencoder.addSafeCharacter('.');\n\t\tencoder.addSafeCharacter('_');\n\t\tencoder.addSafeCharacter('~');\n\n\t\t// Add the sub-delims\n\t\taddSubDelims(encoder);\n\n\t\t// Add the remaining literals\n\t\tencoder.addSafeCharacter(':');\n\t\tencoder.addSafeCharacter('@');\n\n\t\t// Add '/' so it isn't encoded when we encode a path\n\t\tencoder.addSafeCharacter('/');\n\n\t\treturn encoder;\n\t}\n\n\t/**\n\t * URL的Path的每一个Segment URLEncoder<br>\n\t * 默认的编码器针对URI路径的每一段编码，定义如下：\n\t *\n\t * <pre>\n\t * pchar = unreserved / pct-encoded / sub-delims / \":\"（非空segment不包含:） / \"@\"\n\t * unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 定义见：https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3\n\t *\n\t * @return URLEncoder\n\t */\n\tpublic static URLEncoder createPathSegment() {\n\t\tfinal URLEncoder encoder = new URLEncoder();\n\n\t\t// unreserved\n\t\tencoder.addSafeCharacter('-');\n\t\tencoder.addSafeCharacter('.');\n\t\tencoder.addSafeCharacter('_');\n\t\tencoder.addSafeCharacter('~');\n\n\t\t// Add the sub-delims\n\t\taddSubDelims(encoder);\n\n\t\t// Add the remaining literals\n\t\t//non-zero-length segment without any colon \":\"\n\t\t//encoder.addSafeCharacter(':');\n\t\tencoder.addSafeCharacter('@');\n\n\t\treturn encoder;\n\t}\n\n\t/**\n\t * URL的Fragment URLEncoder<br>\n\t * 默认的编码器针对Fragment，定义如下：\n\t *\n\t * <pre>\n\t * fragment    = *( pchar / \"/\" / \"?\" )\n\t * pchar       = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n\t * unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n\t * sub-delims  = \"!\" / \"$\" / \"&amp;\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * </pre>\n\t *\n\t * 具体见：https://datatracker.ietf.org/doc/html/rfc3986#section-3.5\n\t *\n\t * @return URLEncoder\n\t * @since 5.7.13\n\t */\n\tpublic static URLEncoder createFragment() {\n\t\tfinal URLEncoder encoder = new URLEncoder();\n\t\tencoder.addSafeCharacter('-');\n\t\tencoder.addSafeCharacter('.');\n\t\tencoder.addSafeCharacter('_');\n\t\tencoder.addSafeCharacter('~');\n\n\t\t// Add the sub-delims\n\t\taddSubDelims(encoder);\n\n\t\t// Add the remaining literals\n\t\tencoder.addSafeCharacter(':');\n\t\tencoder.addSafeCharacter('@');\n\n\t\tencoder.addSafeCharacter('/');\n\t\tencoder.addSafeCharacter('?');\n\n\t\treturn encoder;\n\t}\n\n\t/**\n\t * 创建用于查询语句的URLEncoder<br>\n\t * 编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * 0x20 ' ' =》 '+'\n\t * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is\n\t * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' Also '=' and '&amp;' 不编码\n\t * 其它编码为 %nn 形式\n\t * </pre>\n\t * <p>\n\t * 详细见：https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm\n\t *\n\t * @return URLEncoder\n\t */\n\tpublic static URLEncoder createQuery() {\n\t\tfinal URLEncoder encoder = new URLEncoder();\n\t\t// Special encoding for space\n\t\tencoder.setEncodeSpaceAsPlus(true);\n\t\t// Alpha and digit are safe by default\n\t\t// Add the other permitted characters\n\t\tencoder.addSafeCharacter('*');\n\t\tencoder.addSafeCharacter('-');\n\t\tencoder.addSafeCharacter('.');\n\t\tencoder.addSafeCharacter('_');\n\n\t\tencoder.addSafeCharacter('=');\n\t\tencoder.addSafeCharacter('&');\n\n\t\treturn encoder;\n\t}\n\n\t/**\n\t * 创建URLEncoder<br>\n\t * 编码器针对URI路径编码，定义如下：\n\t *\n\t * <pre>\n\t * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is\n\t * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' 不编码\n\t * 其它编码为 %nn 形式\n\t * </pre>\n\t * <p>\n\t * 详细见：https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm\n\t *\n\t * @return URLEncoder\n\t */\n\tpublic static URLEncoder createAll() {\n\t\tfinal URLEncoder encoder = new URLEncoder();\n\t\tencoder.addSafeCharacter('*');\n\t\tencoder.addSafeCharacter('-');\n\t\tencoder.addSafeCharacter('.');\n\t\tencoder.addSafeCharacter('_');\n\n\t\treturn encoder;\n\t}\n\t// --------------------------------------------------------------------------------------------- Static method end\n\n\t/**\n\t * 存放安全编码\n\t */\n\tprivate final BitSet safeCharacters;\n\t/**\n\t * 是否编码空格为+\n\t */\n\tprivate boolean encodeSpaceAsPlus = false;\n\n\t/**\n\t * 构造<br>\n\t * [a-zA-Z0-9]默认不被编码\n\t */\n\tpublic URLEncoder() {\n\t\tthis(new BitSet(256));\n\n\t\t// unreserved\n\t\taddAlpha();\n\t\taddDigit();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param safeCharacters 安全字符，安全字符不被编码\n\t */\n\tprivate URLEncoder(BitSet safeCharacters) {\n\t\tthis.safeCharacters = safeCharacters;\n\t}\n\n\t/**\n\t * 增加安全字符<br>\n\t * 安全字符不被编码\n\t *\n\t * @param c 字符\n\t */\n\tpublic void addSafeCharacter(char c) {\n\t\tsafeCharacters.set(c);\n\t}\n\n\t/**\n\t * 移除安全字符<br>\n\t * 安全字符不被编码\n\t *\n\t * @param c 字符\n\t */\n\tpublic void removeSafeCharacter(char c) {\n\t\tsafeCharacters.clear(c);\n\t}\n\n\t/**\n\t * 是否将空格编码为+\n\t *\n\t * @param encodeSpaceAsPlus 是否将空格编码为+\n\t */\n\tpublic void setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) {\n\t\tthis.encodeSpaceAsPlus = encodeSpaceAsPlus;\n\t}\n\n\t/**\n\t * 将URL中的字符串编码为%形式\n\t *\n\t * @param path    需要编码的字符串\n\t * @param charset 编码, {@code null}返回原字符串，表示不编码\n\t * @return 编码后的字符串\n\t */\n\tpublic String encode(String path, Charset charset) {\n\t\tif (null == charset || StrUtil.isEmpty(path)) {\n\t\t\treturn path;\n\t\t}\n\n\t\tfinal StringBuilder rewrittenPath = new StringBuilder(path.length());\n\t\tByteArrayOutputStream buf = new ByteArrayOutputStream();\n\t\tOutputStreamWriter writer = new OutputStreamWriter(buf, charset);\n\n\t\tint c;\n\t\tfor (int i = 0; i < path.length(); i++) {\n\t\t\tc = path.charAt(i);\n\t\t\tif (safeCharacters.get(c)) {\n\t\t\t\trewrittenPath.append((char) c);\n\t\t\t} else if (encodeSpaceAsPlus && c == CharUtil.SPACE) {\n\t\t\t\t// 对于空格单独处理\n\t\t\t\trewrittenPath.append('+');\n\t\t\t} else {\n\t\t\t\t// convert to external encoding before hex conversion\n\t\t\t\ttry {\n\t\t\t\t\twriter.write((char) c);\n\t\t\t\t\twriter.flush();\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tbuf.reset();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tbyte[] ba = buf.toByteArray();\n\t\t\t\tfor (byte toEncode : ba) {\n\t\t\t\t\t// Converting each byte in the buffer\n\t\t\t\t\trewrittenPath.append('%');\n\t\t\t\t\tHexUtil.appendHex(rewrittenPath, toEncode, false);\n\t\t\t\t}\n\t\t\t\tbuf.reset();\n\t\t\t}\n\t\t}\n\t\treturn rewrittenPath.toString();\n\t}\n\n\t/**\n\t * 增加安全字符[a-z][A-Z]\n\t */\n\tprivate void addAlpha() {\n\t\tfor (char i = 'a'; i <= 'z'; i++) {\n\t\t\taddSafeCharacter(i);\n\t\t}\n\t\tfor (char i = 'A'; i <= 'Z'; i++) {\n\t\t\taddSafeCharacter(i);\n\t\t}\n\t}\n\n\t/**\n\t * 增加数字1-9\n\t */\n\tprivate void addDigit() {\n\t\tfor (char i = '0'; i <= '9'; i++) {\n\t\t\taddSafeCharacter(i);\n\t\t}\n\t}\n\n\n\t/**\n\t * 增加sub-delims<br>\n\t * sub-delims  = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \") / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t * 定义见：https://datatracker.ietf.org/doc/html/rfc3986#section-2.2\n\t */\n\tprivate static void addSubDelims(URLEncoder encoder){\n\t\t// Add the sub-delims\n\t\tencoder.addSafeCharacter('!');\n\t\tencoder.addSafeCharacter('$');\n\t\tencoder.addSafeCharacter('&');\n\t\tencoder.addSafeCharacter('\\'');\n\t\tencoder.addSafeCharacter('(');\n\t\tencoder.addSafeCharacter(')');\n\t\tencoder.addSafeCharacter('*');\n\t\tencoder.addSafeCharacter('+');\n\t\tencoder.addSafeCharacter(',');\n\t\tencoder.addSafeCharacter(';');\n\t\tencoder.addSafeCharacter('=');\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/UserPassAuthenticator.java",
    "content": "package cn.hutool.core.net;\n\nimport java.net.Authenticator;\nimport java.net.PasswordAuthentication;\n\n/**\n * 账号密码形式的{@link Authenticator}\n *\n * @author looly\n * @since 5.7.2\n */\npublic class UserPassAuthenticator extends Authenticator {\n\n\tprivate final String user;\n\tprivate final char[] pass;\n\n\t/**\n\t * 构造\n\t *\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic UserPassAuthenticator(String user, char[] pass) {\n\t\tthis.user = user;\n\t\tthis.pass = pass;\n\t}\n\n\t@Override\n\tprotected PasswordAuthentication getPasswordAuthentication() {\n\t\treturn new PasswordAuthentication(this.user, this.pass);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java",
    "content": "package cn.hutool.core.net.multipart;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.map.multi.ListValueMap;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * HttpRequest解析器<br>\n * 来自Jodd\n *\n * @author jodd.org\n */\npublic class MultipartFormData {\n\n\t/** 请求参数 */\n\tprivate final ListValueMap<String, String> requestParameters = new ListValueMap<>();\n\t/** 请求文件 */\n\tprivate final ListValueMap<String, UploadFile> requestFiles = new ListValueMap<>();\n\t/** 上传选项 */\n\tprivate final UploadSetting setting;\n\n\t/** 是否解析完毕 */\n\tprivate boolean loaded;\n\n\t// --------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic MultipartFormData() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param uploadSetting 上传设定\n\t */\n\tpublic MultipartFormData(UploadSetting uploadSetting) {\n\t\tthis.setting = uploadSetting == null ? new UploadSetting() : uploadSetting;\n\t}\n\t// --------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 提取上传的文件和表单数据\n\t *\n\t * @param inputStream HttpRequest流\n\t * @param charset 编码\n\t * @throws IOException IO异常\n\t */\n\tpublic void parseRequestStream(InputStream inputStream, Charset charset) throws IOException {\n\t\tsetLoaded();\n\n\t\tMultipartRequestInputStream input = new MultipartRequestInputStream(inputStream);\n\t\tinput.readBoundary();\n\t\twhile (true) {\n\t\t\tUploadFileHeader header = input.readDataHeader(charset);\n\t\t\tif (header == null) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (header.isFile == true) {\n\t\t\t\t// 文件类型的表单项\n\t\t\t\tString fileName = header.fileName;\n\t\t\t\tif (fileName.length() > 0 && header.contentType.contains(\"application/x-macbinary\")) {\n\t\t\t\t\tinput.skipBytes(128);\n\t\t\t\t}\n\t\t\t\tfinal UploadFile newFile = new UploadFile(header, setting);\n\t\t\t\tif(newFile.processStream(input)){\n\t\t\t\t\tputFile(header.formFieldName, newFile);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 标准表单项\n\t\t\t\tputParameter(header.formFieldName, input.readString(charset));\n\t\t\t}\n\n\t\t\tinput.skipBytes(1);\n\t\t\tinput.mark(1);\n\n\t\t\t// read byte, but may be end of stream\n\t\t\tint nextByte = input.read();\n\t\t\tif (nextByte == -1 || nextByte == '-') {\n\t\t\t\tinput.reset();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tinput.reset();\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------- parameters\n\t/**\n\t * 返回单一参数值，如果有多个只返回第一个\n\t *\n\t * @param paramName 参数名\n\t * @return null未找到，否则返回值\n\t */\n\tpublic String getParam(String paramName) {\n\t\tfinal List<String> values = getListParam(paramName);\n\t\tif (CollUtil.isNotEmpty(values)) {\n\t\t\treturn values.get(0);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @return 获得参数名集合\n\t */\n\tpublic Set<String> getParamNames() {\n\t\treturn requestParameters.keySet();\n\t}\n\n\t/**\n\t * 获得数组表单值\n\t *\n\t * @param paramName 参数名\n\t * @return 数组表单值\n\t */\n\tpublic String[] getArrayParam(String paramName) {\n\t\tfinal List<String> listParam = getListParam(paramName);\n\t\tif(null != listParam){\n\t\t\treturn listParam.toArray(new String[0]);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得集合表单值\n\t *\n\t * @param paramName 参数名\n\t * @return 数组表单值\n\t * @since 5.3.0\n\t */\n\tpublic List<String> getListParam(String paramName) {\n\t\treturn requestParameters.get(paramName);\n\t}\n\n\t/**\n\t * 获取所有属性的集合\n\t *\n\t * @return 所有属性的集合\n\t */\n\tpublic Map<String, String[]> getParamMap() {\n\t\treturn Convert.toMap(String.class, String[].class, getParamListMap());\n\t}\n\n\t/**\n\t * 获取所有属性的集合\n\t *\n\t * @return 所有属性的集合\n\t */\n\tpublic ListValueMap<String, String> getParamListMap() {\n\t\treturn this.requestParameters;\n\t}\n\n\t// --------------------------------------------------------------------------- Files parameters\n\t/**\n\t * 获取上传的文件\n\t *\n\t * @param paramName 文件参数名称\n\t * @return 上传的文件， 如果无为null\n\t */\n\tpublic UploadFile getFile(String paramName) {\n\t\tUploadFile[] values = getFiles(paramName);\n\t\tif ((values != null) && (values.length > 0)) {\n\t\t\treturn values[0];\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得某个属性名的所有文件<br>\n\t * 当表单中两个文件使用同一个name的时候\n\t *\n\t * @param paramName 属性名\n\t * @return 上传的文件列表\n\t */\n\tpublic UploadFile[] getFiles(String paramName) {\n\t\tfinal List<UploadFile> fileList = getFileList(paramName);\n\t\tif(null != fileList){\n\t\t\treturn fileList.toArray(new UploadFile[0]);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得某个属性名的所有文件<br>\n\t * 当表单中两个文件使用同一个name的时候\n\t *\n\t * @param paramName 属性名\n\t * @return 上传的文件列表\n\t * @since 5.3.0\n\t */\n\tpublic List<UploadFile> getFileList(String paramName) {\n\t\treturn requestFiles.get(paramName);\n\t}\n\n\t/**\n\t * 获取上传的文件属性名集合\n\t *\n\t * @return 上传的文件属性名集合\n\t */\n\tpublic Set<String> getFileParamNames() {\n\t\treturn requestFiles.keySet();\n\t}\n\n\t/**\n\t * 获取文件映射\n\t *\n\t * @return 文件映射\n\t */\n\tpublic Map<String, UploadFile[]> getFileMap() {\n\t\treturn Convert.toMap(String.class, UploadFile[].class, getFileListValueMap());\n\t}\n\n\t/**\n\t * 获取文件映射\n\t *\n\t * @return 文件映射\n\t */\n\tpublic ListValueMap<String, UploadFile> getFileListValueMap() {\n\t\treturn this.requestFiles;\n\t}\n\n\t// --------------------------------------------------------------------------- Load\n\t/**\n\t * 是否已被解析\n\t *\n\t * @return 如果流已被解析返回true\n\t */\n\tpublic boolean isLoaded() {\n\t\treturn loaded;\n\t}\n\n\t// ---------------------------------------------------------------- Private method start\n\t/**\n\t * 加入上传文件\n\t *\n\t * @param name 参数名\n\t * @param uploadFile 文件\n\t */\n\tprivate void putFile(String name, UploadFile uploadFile) {\n\t\tthis.requestFiles.putValue(name, uploadFile);\n\t}\n\n\t/**\n\t * 加入普通参数\n\t *\n\t * @param name 参数名\n\t * @param value 参数值\n\t */\n\tprivate void putParameter(String name, String value) {\n\t\tthis.requestParameters.putValue(name, value);\n\t}\n\n\t/**\n\t * 设置使输入流为解析状态，如果已解析，则抛出异常\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void setLoaded() throws IOException {\n\t\tif (loaded == true) {\n\t\t\tthrow new IOException(\"Multi-part request already parsed.\");\n\t\t}\n\t\tloaded = true;\n\t}\n\t// ---------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java",
    "content": "package cn.hutool.core.net.multipart;\n\nimport cn.hutool.core.io.FastByteArrayOutputStream;\n\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Http请求解析流，提供了专门针对带文件的form表单的解析<br>\n * 来自Jodd\n *\n * @author jodd.org\n */\npublic class MultipartRequestInputStream extends BufferedInputStream {\n\n\tpublic MultipartRequestInputStream(InputStream in) {\n\t\tsuper(in);\n\t}\n\n\t/**\n\t * 读取byte字节流，在末尾抛出异常\n\t *\n\t * @return byte\n\t * @throws IOException 读取异常\n\t */\n\tpublic byte readByte() throws IOException {\n\t\tint i = super.read();\n\t\tif (i == -1) {\n\t\t\tthrow new IOException(\"End of HTTP request stream reached\");\n\t\t}\n\t\treturn (byte) i;\n\t}\n\n\t/**\n\t * 跳过指定位数的 bytes.\n\t *\n\t * @param i 跳过的byte数\n\t * @throws IOException IO异常\n\t */\n\tpublic void skipBytes(long i) throws IOException {\n\t\tlong len = super.skip(i);\n\t\tif (len != i) {\n\t\t\tthrow new IOException(\"Unable to skip data in HTTP request\");\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------- boundary\n\n\t/**\n\t * part部分边界\n\t */\n\tprotected byte[] boundary;\n\n\t/**\n\t * 输入流中读取边界\n\t *\n\t * @return 边界\n\t * @throws IOException 读取异常\n\t */\n\tpublic byte[] readBoundary() throws IOException {\n\t\tByteArrayOutputStream boundaryOutput = new ByteArrayOutputStream(1024);\n\t\tbyte b;\n\t\t// skip optional whitespaces\n\t\t//noinspection StatementWithEmptyBody\n\t\twhile ((b = readByte()) <= ' ') {\n\t\t}\n\t\tboundaryOutput.write(b);\n\n\t\t// now read boundary chars\n\t\twhile ((b = readByte()) != '\\r') {\n\t\t\tboundaryOutput.write(b);\n\t\t}\n\t\tif (boundaryOutput.size() == 0) {\n\t\t\tthrow new IOException(\"Problems with parsing request: invalid boundary\");\n\t\t}\n\t\tskipBytes(1);\n\t\tboundary = new byte[boundaryOutput.size() + 2];\n\t\tSystem.arraycopy(boundaryOutput.toByteArray(), 0, boundary, 2, boundary.length - 2);\n\t\tboundary[0] = '\\r';\n\t\tboundary[1] = '\\n';\n\t\treturn boundary;\n\t}\n\n\t// ---------------------------------------------------------------- data header\n\n\tprotected UploadFileHeader lastHeader;\n\n\tpublic UploadFileHeader getLastHeader() {\n\t\treturn lastHeader;\n\t}\n\n\t/**\n\t * 从流中读取文件头部信息， 如果达到末尾则返回null\n\t *\n\t * @param encoding 字符集\n\t * @return 头部信息， 如果达到末尾则返回null\n\t * @throws IOException 读取异常\n\t */\n\tpublic UploadFileHeader readDataHeader(Charset encoding) throws IOException {\n\t\tString dataHeader = readDataHeaderString(encoding);\n\t\tif (dataHeader != null) {\n\t\t\tlastHeader = new UploadFileHeader(dataHeader);\n\t\t} else {\n\t\t\tlastHeader = null;\n\t\t}\n\t\treturn lastHeader;\n\t}\n\n\t/**\n\t * 读取数据头信息字符串\n\t *\n\t * @param charset 编码\n\t * @return 数据头信息字符串\n\t * @throws IOException IO异常\n\t */\n\tprotected String readDataHeaderString(Charset charset) throws IOException {\n\t\tByteArrayOutputStream data = new ByteArrayOutputStream();\n\t\tbyte b;\n\t\twhile (true) {\n\t\t\t// end marker byte on offset +0 and +2 must be 13\n\t\t\tif ((b = readByte()) != '\\r') {\n\t\t\t\tdata.write(b);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tmark(4);\n\t\t\tskipBytes(1);\n\t\t\tint i = read();\n\t\t\tif (i == -1) {\n\t\t\t\t// reached end of stream\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (i == '\\r') {\n\t\t\t\treset();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\treset();\n\t\t\tdata.write(b);\n\t\t}\n\t\tskipBytes(3);\n\t\treturn charset == null ? data.toString() : data.toString(charset.name());\n\t}\n\t// ---------------------------------------------------------------- copy\n\n\t/**\n\t * 读取字节流，直到下一个boundary\n\t *\n\t * @param charset 编码，null表示系统默认编码\n\t * @return 读取的字符串\n\t * @throws IOException 读取异常\n\t */\n\tpublic String readString(Charset charset) throws IOException {\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream();\n\t\tcopy(out);\n\t\treturn out.toString(charset);\n\t}\n\n\t/**\n\t * 字节流复制到out，直到下一个boundary\n\t *\n\t * @param out 输出流\n\t * @return 复制的字节数\n\t * @throws IOException 读取异常\n\t */\n\tpublic long copy(OutputStream out) throws IOException {\n\t\tlong count = 0;\n\t\twhile (true) {\n\t\t\tbyte b = readByte();\n\t\t\tif (isBoundary(b)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tout.write(b);\n\t\t\tcount++;\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 复制字节流到out， 大于maxBytes或者文件末尾停止\n\t *\n\t * @param out   输出流\n\t * @param limit 最大字节数\n\t * @return 复制的字节数\n\t * @throws IOException 读取异常\n\t */\n\tpublic long copy(OutputStream out, long limit) throws IOException {\n\t\tlong count = 0;\n\t\twhile (true) {\n\t\t\tbyte b = readByte();\n\t\t\tif (isBoundary(b)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tout.write(b);\n\t\t\tcount++;\n\t\t\tif (count > limit) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 跳过边界表示\n\t *\n\t * @return 跳过的字节数\n\t * @throws IOException 读取异常\n\t */\n\tpublic long skipToBoundary() throws IOException {\n\t\tlong count = 0;\n\t\twhile (true) {\n\t\t\tbyte b = readByte();\n\t\t\tcount++;\n\t\t\tif (isBoundary(b)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * @param b byte\n\t * @return 是否为边界的标志\n\t * @throws IOException 读取异常\n\t */\n\tpublic boolean isBoundary(byte b) throws IOException {\n\t\tint boundaryLen = boundary.length;\n\t\tmark(boundaryLen + 1);\n\t\tint bpos = 0;\n\t\twhile (b == boundary[bpos]) {\n\t\t\tb = readByte();\n\t\t\tbpos++;\n\t\t\tif (bpos == boundaryLen) {\n\t\t\t\treturn true; // boundary found!\n\t\t\t}\n\t\t}\n\t\treset();\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFile.java",
    "content": "package cn.hutool.core.net.multipart;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedOutputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.NoSuchFileException;\n\n/**\n * 上传的文件对象\n *\n * @author xiaoleilu\n */\npublic class UploadFile {\n\n\tprivate static final String TMP_FILE_PREFIX = \"hutool-\";\n\tprivate static final String TMP_FILE_SUFFIX = \".upload.tmp\";\n\n\tprivate final UploadFileHeader header;\n\tprivate final UploadSetting setting;\n\n\tprivate long size = -1;\n\n\t// 文件流（小文件位于内存中）\n\tprivate byte[] data;\n\t// 临时文件（大文件位于临时文件夹中）\n\tprivate File tempFile;\n\n\t/**\n\t * 构造\n\t *\n\t * @param header  头部信息\n\t * @param setting 上传设置\n\t */\n\tpublic UploadFile(UploadFileHeader header, UploadSetting setting) {\n\t\tthis.header = header;\n\t\tthis.setting = setting;\n\t}\n\n\t// ---------------------------------------------------------------- operations\n\n\t/**\n\t * 从磁盘或者内存中删除这个文件\n\t */\n\tpublic void delete() {\n\t\tif (tempFile != null) {\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\ttempFile.delete();\n\t\t}\n\t\tif (data != null) {\n\t\t\tdata = null;\n\t\t}\n\t}\n\n\t/**\n\t * 将上传的文件写入指定的目标文件路径，自动创建文件<br>\n\t * 写入后原临时文件会被删除\n\t *\n\t * @param destPath 目标文件路径\n\t * @return 目标文件\n\t * @throws IOException IO异常\n\t */\n\tpublic File write(String destPath) throws IOException {\n\t\tif (data != null || tempFile != null) {\n\t\t\treturn write(FileUtil.file(destPath));\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 将上传的文件写入目标文件<br>\n\t * 写入后原临时文件会被删除\n\t *\n\t * @param destination 目标文件\n\t * @return 目标文件\n\t * @throws IOException IO异常\n\t */\n\tpublic File write(File destination) throws IOException {\n\t\tassertValid();\n\n\t\tif (destination.isDirectory() == true) {\n\t\t\tdestination = new File(destination, this.header.getFileName());\n\t\t}\n\t\tif (data != null) {\n\t\t\t// 内存中\n\t\t\tFileUtil.writeBytes(data, destination);\n\t\t\tdata = null;\n\t\t} else {\n\t\t\t// 临时文件\n\t\t\tif(null == this.tempFile){\n\t\t\t\tthrow new NullPointerException(\"Temp file is null !\");\n\t\t\t}\n\t\t\tif(false == this.tempFile.exists()){\n\t\t\t\tthrow new NoSuchFileException(\"Temp file: [\" + this.tempFile.getAbsolutePath() + \"] not exist!\");\n\t\t\t}\n\n\t\t\tFileUtil.move(tempFile, destination, true);\n\t\t}\n\t\treturn destination;\n\t}\n\n\t/**\n\t * @return 获得文件字节流\n\t * @throws IOException IO异常\n\t */\n\tpublic byte[] getFileContent() throws IOException {\n\t\tassertValid();\n\n\t\tif (data != null) {\n\t\t\treturn data;\n\t\t}\n\t\tif (tempFile != null) {\n\t\t\treturn FileUtil.readBytes(tempFile);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @return 获得文件流\n\t * @throws IOException IO异常\n\t */\n\tpublic InputStream getFileInputStream() throws IOException {\n\t\tassertValid();\n\n\t\tif (data != null) {\n\t\t\treturn IoUtil.toBuffered(IoUtil.toStream(this.data));\n\t\t}\n\t\tif (tempFile != null) {\n\t\t\treturn IoUtil.toBuffered(IoUtil.toStream(this.tempFile));\n\t\t}\n\t\treturn null;\n\t}\n\n\t// ---------------------------------------------------------------- header\n\n\t/**\n\t * @return 上传文件头部信息\n\t */\n\tpublic UploadFileHeader getHeader() {\n\t\treturn header;\n\t}\n\n\t/**\n\t * @return 文件名\n\t */\n\tpublic String getFileName() {\n\t\treturn header == null ? null : header.getFileName();\n\t}\n\n\t// ---------------------------------------------------------------- properties\n\n\t/**\n\t * @return 上传文件的大小，&gt; 0 表示未上传\n\t */\n\tpublic long size() {\n\t\treturn size;\n\t}\n\n\t/**\n\t * @return 是否上传成功\n\t */\n\tpublic boolean isUploaded() {\n\t\treturn size > 0;\n\t}\n\n\t/**\n\t * @return 文件是否在内存中\n\t */\n\tpublic boolean isInMemory() {\n\t\treturn data != null;\n\t}\n\n\t// ---------------------------------------------------------------- process\n\n\t/**\n\t * 处理上传表单流，提取出文件\n\t *\n\t * @param input 上传表单的流\n\t * @return 是否成功\n\t * @throws IOException IO异常\n\t */\n\tprotected boolean processStream(MultipartRequestInputStream input) throws IOException {\n\t\tif (!isAllowedExtension()) {\n\t\t\t// 非允许的扩展名\n\t\t\tsize = input.skipToBoundary();\n\t\t\treturn false;\n\t\t}\n\t\tsize = 0;\n\n\t\t// 处理内存文件\n\t\tint memoryThreshold = setting.memoryThreshold;\n\t\tif (memoryThreshold > 0) {\n\t\t\tfinal ByteArrayOutputStream baos = new ByteArrayOutputStream(memoryThreshold);\n\t\t\tfinal long written = input.copy(baos, memoryThreshold);\n\t\t\tdata = baos.toByteArray();\n\t\t\tif (written <= memoryThreshold) {\n\t\t\t\t// 文件存放于内存\n\t\t\t\tsize = data.length;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 处理硬盘文件\n\t\ttempFile = FileUtil.createTempFile(TMP_FILE_PREFIX, TMP_FILE_SUFFIX, FileUtil.touch(setting.tmpUploadPath), false);\n\t\tfinal BufferedOutputStream out = FileUtil.getOutputStream(this.tempFile);\n\t\tif (data != null) {\n\t\t\tsize = data.length;\n\t\t\tout.write(data);\n\t\t\tdata = null; // not needed anymore\n\t\t}\n\t\tfinal long maxFileSize = setting.maxFileSize;\n\t\ttry {\n\t\t\tif (maxFileSize == -1) {\n\t\t\t\tsize += input.copy(out);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tsize += input.copy(out, maxFileSize - size + 1); // one more byte to detect larger files\n\t\t\tif (size > maxFileSize) {\n\t\t\t\t// 超出上传大小限制\n\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\ttempFile.delete();\n\t\t\t\ttempFile = null;\n\t\t\t\tinput.skipToBoundary();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t\treturn true;\n\t}\n\n\t// ---------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * @return 是否为允许的扩展名\n\t */\n\tprivate boolean isAllowedExtension() {\n\t\tfinal String[] exts = setting.fileExts;\n\t\tboolean isAllow = setting.isAllowFileExts;\n\t\tif (exts == null || exts.length == 0) {\n\t\t\t// 如果给定扩展名列表为空，当允许扩展名时全部允许，否则全部禁止\n\t\t\treturn isAllow;\n\t\t}\n\n\t\tfinal String fileNameExt = FileUtil.extName(this.getFileName());\n\t\tfor (String fileExtension : setting.fileExts) {\n\t\t\tif (fileNameExt.equalsIgnoreCase(fileExtension)) {\n\t\t\t\treturn isAllow;\n\t\t\t}\n\t\t}\n\n\t\t// 未匹配到扩展名，如果为允许列表，返回false， 否则true\n\t\treturn !isAllow;\n\t}\n\n\t/**\n\t * 断言是否文件流可用\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void assertValid() throws IOException {\n\t\tif (false == isUploaded()) {\n\t\t\tthrow new IOException(StrUtil.format(\"File [{}] upload fail\", getFileName()));\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java",
    "content": "package cn.hutool.core.net.multipart;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 上传的文件的头部信息<br>\n * 来自Jodd\n *\n * @author jodd.org\n */\npublic class UploadFileHeader {\n\n\t// String dataHeader;\n\tString formFieldName;\n\n\tString formFileName;\n\tString path;\n\tString fileName;\n\n\tboolean isFile;\n\tString contentType;\n\tString mimeType;\n\tString mimeSubtype;\n\tString contentDisposition;\n\n\tUploadFileHeader(String dataHeader) {\n\t\tprocessHeaderString(dataHeader);\n\t}\n\n\t// ---------------------------------------------------------------- public interface\n\n\t/**\n\t * Returns {@code true} if uploaded data are correctly marked as a file.<br>\n\t * This is true if header contains string 'filename'.\n\t *\n\t * @return 是否为文件\n\t */\n\tpublic boolean isFile() {\n\t\treturn isFile;\n\t}\n\n\t/**\n\t * 返回表单字段名\n\t *\n\t * @return 表单字段名\n\t */\n\tpublic String getFormFieldName() {\n\t\treturn formFieldName;\n\t}\n\n\t/**\n\t * 返回表单中的文件名，来自客户端传入\n\t *\n\t * @return 表单文件名\n\t */\n\tpublic String getFormFileName() {\n\t\treturn formFileName;\n\t}\n\n\t/**\n\t * 获取文件名，不包括路径\n\t *\n\t * @return 文件名\n\t */\n\tpublic String getFileName() {\n\t\treturn fileName;\n\t}\n\n\t/**\n\t * Returns uploaded content type. It is usually in the following form:<br>\n\t * mime_type/mime_subtype.\n\t *\n\t * @return content type\n\t * @see #getMimeType()\n\t * @see #getMimeSubtype()\n\t */\n\tpublic String getContentType() {\n\t\treturn contentType;\n\t}\n\n\t/**\n\t * Returns file types MIME.\n\t *\n\t * @return types MIME\n\t */\n\tpublic String getMimeType() {\n\t\treturn mimeType;\n\t}\n\n\t/**\n\t * Returns file sub type MIME.\n\t *\n\t * @return sub type MIME\n\t */\n\tpublic String getMimeSubtype() {\n\t\treturn mimeSubtype;\n\t}\n\n\t/**\n\t * Returns content disposition. Usually it is 'form-data'.\n\t *\n\t * @return content disposition\n\t */\n\tpublic String getContentDisposition() {\n\t\treturn contentDisposition;\n\t}\n\n\t// ---------------------------------------------------------------- Private Method\n\n\t/**\n\t * 获得头信息字符串字符串中指定的值\n\t *\n\t * @param dataHeader 头信息\n\t * @param fieldName  字段名\n\t * @return 字段值\n\t */\n\tprivate String getDataFieldValue(String dataHeader, String fieldName) {\n\t\tString value = null;\n\t\tString token = StrUtil.format(\"{}=\\\"\", fieldName);\n\t\tint pos = dataHeader.indexOf(token);\n\t\tif (pos > 0) {\n\t\t\tint start = pos + token.length();\n\t\t\tint end = dataHeader.indexOf('\"', start);\n\t\t\tif ((start > 0) && (end > 0)) {\n\t\t\t\tvalue = dataHeader.substring(start, end);\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 头信息中获得content type\n\t *\n\t * @param dataHeader data header string\n\t * @return content type or an empty string if no content type defined\n\t */\n\tprivate String getContentType(String dataHeader) {\n\t\tString token = \"Content-Type:\";\n\t\tint start = dataHeader.indexOf(token);\n\t\tif (start == -1) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tstart += token.length();\n\t\treturn dataHeader.substring(start);\n\t}\n\n\tprivate String getContentDisposition(String dataHeader) {\n\t\tint start = dataHeader.indexOf(':') + 1;\n\t\tint end = dataHeader.indexOf(';');\n\t\treturn dataHeader.substring(start, end);\n\t}\n\n\tprivate String getMimeType(String ContentType) {\n\t\tint pos = ContentType.indexOf('/');\n\t\tif (pos == -1) {\n\t\t\treturn ContentType;\n\t\t}\n\t\treturn ContentType.substring(1, pos);\n\t}\n\n\tprivate String getMimeSubtype(String ContentType) {\n\t\tint start = ContentType.indexOf('/');\n\t\tif (start == -1) {\n\t\t\treturn ContentType;\n\t\t}\n\t\tstart++;\n\t\treturn ContentType.substring(start);\n\t}\n\n\t/**\n\t * 处理头字符串，使之转化为字段\n\t *\n\t * @param dataHeader 头字符串\n\t */\n\tprivate void processHeaderString(String dataHeader) {\n\t\tisFile = dataHeader.indexOf(\"filename\") > 0;\n\t\tformFieldName = getDataFieldValue(dataHeader, \"name\");\n\t\tif (isFile) {\n\t\t\tformFileName = getDataFieldValue(dataHeader, \"filename\");\n\t\t\tif (formFileName == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (formFileName.length() == 0) {\n\t\t\t\tpath = StrUtil.EMPTY;\n\t\t\t\tfileName = StrUtil.EMPTY;\n\t\t\t}\n\t\t\tint ls = FileUtil.lastIndexOfSeparator(formFileName);\n\t\t\tif (ls == -1) {\n\t\t\t\tpath = StrUtil.EMPTY;\n\t\t\t\tfileName = formFileName;\n\t\t\t} else {\n\t\t\t\tpath = formFileName.substring(0, ls);\n\t\t\t\tfileName = formFileName.substring(ls);\n\t\t\t}\n\t\t\tif (fileName.length() > 0) {\n\t\t\t\tthis.contentType = getContentType(dataHeader);\n\t\t\t\tmimeType = getMimeType(contentType);\n\t\t\t\tmimeSubtype = getMimeSubtype(contentType);\n\t\t\t\tcontentDisposition = getContentDisposition(dataHeader);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java",
    "content": "package cn.hutool.core.net.multipart;\n\n/**\n * 上传文件设定文件\n *\n * @author xiaoleilu\n *\n */\npublic class UploadSetting {\n\n\t/** 最大文件大小，默认无限制 */\n\tprotected long maxFileSize = -1;\n\t/** 文件保存到内存的边界 */\n\tprotected int memoryThreshold = 8192;\n\t/** 临时文件目录 */\n\tprotected String tmpUploadPath;\n\t/** 文件扩展名限定 */\n\tprotected String[] fileExts;\n\t/** 扩展名是允许列表还是禁止列表 */\n\tprotected boolean isAllowFileExts = true;\n\n\tpublic UploadSetting() {\n\t}\n\n\t// ---------------------------------------------------------------------- Setters and Getters start\n\t/**\n\t * @return 获得最大文件大小，-1表示无限制\n\t */\n\tpublic long getMaxFileSize() {\n\t\treturn maxFileSize;\n\t}\n\n\t/**\n\t * 设定最大文件大小，-1表示无限制\n\t *\n\t * @param maxFileSize 最大文件大小\n\t */\n\tpublic void setMaxFileSize(long maxFileSize) {\n\t\tthis.maxFileSize = maxFileSize;\n\t}\n\n\t/**\n\t * @return 文件保存到内存的边界\n\t */\n\tpublic int getMemoryThreshold() {\n\t\treturn memoryThreshold;\n\t}\n\n\t/**\n\t * 设定文件保存到内存的边界<br>\n\t * 如果文件大小小于这个边界，将保存于内存中，否则保存至临时目录中\n\t *\n\t * @param memoryThreshold 文件保存到内存的边界\n\t */\n\tpublic void setMemoryThreshold(int memoryThreshold) {\n\t\tthis.memoryThreshold = memoryThreshold;\n\t}\n\n\t/**\n\t * @return 上传文件的临时目录，若为空，使用系统目录\n\t */\n\tpublic String getTmpUploadPath() {\n\t\treturn tmpUploadPath;\n\t}\n\n\t/**\n\t * 设定上传文件的临时目录，null表示使用系统临时目录\n\t *\n\t * @param tmpUploadPath 临时目录，绝对路径\n\t */\n\tpublic void setTmpUploadPath(String tmpUploadPath) {\n\t\tthis.tmpUploadPath = tmpUploadPath;\n\t}\n\n\t/**\n\t * @return 文件扩展名限定列表\n\t */\n\tpublic String[] getFileExts() {\n\t\treturn fileExts;\n\t}\n\n\t/**\n\t * 设定文件扩展名限定里列表<br>\n\t * 禁止列表还是允许列表取决于isAllowFileExts\n\t *\n\t * @param fileExts 文件扩展名列表\n\t */\n\tpublic void setFileExts(String[] fileExts) {\n\t\tthis.fileExts = fileExts;\n\t}\n\n\t/**\n\t * 是否允许文件扩展名<br>\n\t *\n\t * @return 若true表示只允许列表里的扩展名，否则是禁止列表里的扩展名\n\t */\n\tpublic boolean isAllowFileExts() {\n\t\treturn isAllowFileExts;\n\t}\n\n\t/**\n\t * 设定是否允许扩展名\n\t *\n\t * @param isAllowFileExts 若true表示只允许列表里的扩展名，否则是禁止列表里的扩展名\n\t */\n\tpublic void setAllowFileExts(boolean isAllowFileExts) {\n\t\tthis.isAllowFileExts = isAllowFileExts;\n\t}\n\t// ---------------------------------------------------------------------- Setters and Getters end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/multipart/package-info.java",
    "content": "/**\n * 文件上传封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.net.multipart;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/package-info.java",
    "content": "/**\n * 网络相关工具\n *\n * @author looly\n *\n */\npackage cn.hutool.core.net;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java",
    "content": "package cn.hutool.core.net.url;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.RFC3986;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.net.URLStreamHandler;\nimport java.nio.charset.Charset;\n\n/**\n * URL 生成器，格式形如：\n * <pre>\n * [scheme:]scheme-specific-part[#fragment]\n * [scheme:][//authority][path][?query][#fragment]\n * [scheme:][//host:port][path][?query][#fragment]\n * </pre>\n *\n * @author looly\n * @see <a href=\"https://en.wikipedia.org/wiki/Uniform_Resource_Identifier\">Uniform Resource Identifier</a>\n * @since 5.3.1\n */\npublic final class UrlBuilder implements Builder<String> {\n\tprivate static final long serialVersionUID = 1L;\n\tprivate static final String DEFAULT_SCHEME = \"http\";\n\n\t/**\n\t * 协议，例如http\n\t */\n\tprivate String scheme;\n\t/**\n\t * 主机，例如127.0.0.1\n\t */\n\tprivate String host;\n\t/**\n\t * 端口，默认-1\n\t */\n\tprivate int port = -1;\n\t/**\n\t * 路径，例如/aa/bb/cc\n\t */\n\tprivate UrlPath path;\n\t/**\n\t * 查询语句，例如a=1&amp;b=2\n\t */\n\tprivate UrlQuery query;\n\t/**\n\t * 标识符，例如#后边的部分\n\t */\n\tprivate String fragment;\n\n\t/**\n\t * 编码，用于URLEncode和URLDecode\n\t */\n\tprivate Charset charset;\n\t/**\n\t * 是否需要编码`%`<br>\n\t * 区别对待，如果是，则生成URL时需要重新全部编码，否则跳过所有`%`\n\t */\n\tprivate boolean needEncodePercent;\n\n\t/**\n\t * 使用URI构建UrlBuilder\n\t *\n\t * @param uri     URI\n\t * @param charset 编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(URI uri, Charset charset) {\n\t\treturn of(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getRawQuery(), uri.getFragment(), charset);\n\t}\n\n\t/**\n\t * 使用URL字符串构建UrlBuilder，当传入的URL没有协议时，按照http协议对待<br>\n\t * 此方法不对URL编码\n\t *\n\t * @param httpUrl URL字符串\n\t * @return UrlBuilder\n\t * @since 5.4.3\n\t */\n\tpublic static UrlBuilder ofHttpWithoutEncode(String httpUrl) {\n\t\treturn ofHttp(httpUrl, null);\n\t}\n\n\t/**\n\t * 使用URL字符串构建UrlBuilder，当传入的URL没有协议时，按照http协议对待，编码默认使用UTF-8\n\t *\n\t * @param httpUrl URL字符串\n\t * @return UrlBuilder\n\t * @since 5.6.3\n\t */\n\tpublic static UrlBuilder ofHttp(String httpUrl) {\n\t\treturn ofHttp(httpUrl, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 使用URL字符串构建UrlBuilder，当传入的URL没有协议时，按照http协议对待。\n\t *\n\t * @param httpUrl URL字符串\n\t * @param charset 编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder ofHttp(String httpUrl, Charset charset) {\n\t\tAssert.notBlank(httpUrl, \"Http url must be not blank!\");\n\t\thttpUrl = StrUtil.trimStart(httpUrl);\n\t\t// issue#I66CIR\n\t\tif(false == StrUtil.startWithAnyIgnoreCase(httpUrl, \"http://\", \"https://\")){\n\t\t\thttpUrl = \"http://\" + httpUrl;\n\t\t}\n\t\treturn of(URLUtil.toUrlForHttp(httpUrl), charset);\n\t}\n\n\t/**\n\t * 使用URL字符串构建UrlBuilder，默认使用UTF-8编码\n\t *\n\t * @param url URL字符串\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(String url) {\n\t\treturn of(url, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 使用URL字符串构建UrlBuilder\n\t *\n\t * @param url     URL字符串\n\t * @param charset 编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(String url, Charset charset) {\n\t\tAssert.notBlank(url, \"Url must be not blank!\");\n\t\treturn of(URLUtil.url(StrUtil.trim(url)), charset);\n\t}\n\n\t/**\n\t * 使用URL构建UrlBuilder\n\t *\n\t * @param url     URL\n\t * @param charset 编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(URL url, Charset charset) {\n\t\treturn of(url.getProtocol(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef(), charset);\n\t}\n\n\t/**\n\t * 构建UrlBuilder\n\t *\n\t * @param scheme   协议，默认http\n\t * @param host     主机，例如127.0.0.1\n\t * @param port     端口，-1表示默认端口\n\t * @param path     路径，例如/aa/bb/cc\n\t * @param query    查询，例如a=1&amp;b=2\n\t * @param fragment 标识符例如#后边的部分\n\t * @param charset  编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(String scheme, String host, int port, String path, String query, String fragment, Charset charset) {\n\t\treturn of(scheme, host, port,\n\t\t\t\tUrlPath.of(path, charset),\n\t\t\t\tUrlQuery.of(query, charset, false), fragment, charset);\n\t}\n\n\t/**\n\t * 构建UrlBuilder\n\t *\n\t * @param scheme   协议，默认http\n\t * @param host     主机，例如127.0.0.1\n\t * @param port     端口，-1表示默认端口\n\t * @param path     路径，例如/aa/bb/cc\n\t * @param query    查询，例如a=1&amp;b=2\n\t * @param fragment 标识符例如#后边的部分\n\t * @param charset  编码，用于URLEncode和URLDecode\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) {\n\t\treturn new UrlBuilder(scheme, host, port, path, query, fragment, charset);\n\t}\n\n\t/**\n\t * 创建空的UrlBuilder\n\t *\n\t * @return UrlBuilder\n\t * @deprecated 请使用 {@link #of()}\n\t */\n\t@Deprecated\n\tpublic static UrlBuilder create() {\n\t\treturn new UrlBuilder();\n\t}\n\n\t/**\n\t * 创建空的UrlBuilder\n\t *\n\t * @return UrlBuilder\n\t */\n\tpublic static UrlBuilder of() {\n\t\treturn new UrlBuilder();\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic UrlBuilder() {\n\t\tthis.charset = CharsetUtil.CHARSET_UTF_8;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param scheme   协议，默认http\n\t * @param host     主机，例如127.0.0.1\n\t * @param port     端口，-1表示默认端口\n\t * @param path     路径，例如/aa/bb/cc\n\t * @param query    查询，例如a=1&amp;b=2\n\t * @param fragment 标识符例如#后边的部分\n\t * @param charset  编码，用于URLEncode和URLDecode，{@code null}表示不编码\n\t */\n\tpublic UrlBuilder(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) {\n\t\tthis.charset = charset;\n\t\tthis.scheme = scheme;\n\t\tthis.host = host;\n\t\tthis.port = port;\n\t\tthis.path = path;\n\t\tthis.query = query;\n\t\tthis.setFragment(fragment);\n\t\t// 编码非空情况下做解码\n\t\tthis.needEncodePercent = null != charset;\n\t}\n\n\t/**\n\t * 获取协议，例如http\n\t *\n\t * @return 协议，例如http\n\t */\n\tpublic String getScheme() {\n\t\treturn scheme;\n\t}\n\n\t/**\n\t * 获取协议，例如http，如果用户未定义协议，使用默认的http协议\n\t *\n\t * @return 协议，例如http\n\t */\n\tpublic String getSchemeWithDefault() {\n\t\treturn StrUtil.emptyToDefault(this.scheme, DEFAULT_SCHEME);\n\t}\n\n\t/**\n\t * 设置协议，例如http\n\t *\n\t * @param scheme 协议，例如http\n\t * @return this\n\t */\n\tpublic UrlBuilder setScheme(String scheme) {\n\t\tthis.scheme = scheme;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 主机，例如127.0.0.1\n\t *\n\t * @return 主机，例如127.0.0.1\n\t */\n\tpublic String getHost() {\n\t\treturn host;\n\t}\n\n\t/**\n\t * 设置主机，例如127.0.0.1\n\t *\n\t * @param host 主机，例如127.0.0.1\n\t * @return this\n\t */\n\tpublic UrlBuilder setHost(String host) {\n\t\tthis.host = host;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取端口，默认-1\n\t *\n\t * @return 端口，默认-1\n\t */\n\tpublic int getPort() {\n\t\treturn port;\n\t}\n\n\t/**\n\t * 获取端口，如果未自定义返回协议默认端口\n\t *\n\t * @return 端口\n\t */\n\tpublic int getPortWithDefault() {\n\t\tint port = getPort();\n\t\tif (port > 0) {\n\t\t\treturn port;\n\t\t}\n\t\tURL url = this.toURL();\n\t\treturn url.getDefaultPort();\n\t}\n\n\t/**\n\t * 设置端口，默认-1\n\t *\n\t * @param port 端口，默认-1\n\t * @return this\n\t */\n\tpublic UrlBuilder setPort(int port) {\n\t\tthis.port = port;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得authority部分\n\t *\n\t * @return authority部分\n\t */\n\tpublic String getAuthority() {\n\t\treturn (port < 0) ? host : host + \":\" + port;\n\t}\n\n\t/**\n\t * 获取路径，例如/aa/bb/cc\n\t *\n\t * @return 路径，例如/aa/bb/cc\n\t */\n\tpublic UrlPath getPath() {\n\t\treturn path;\n\t}\n\n\t/**\n\t * 获得路径，例如/aa/bb/cc\n\t *\n\t * @return 路径，例如/aa/bb/cc\n\t */\n\tpublic String getPathStr() {\n\t\treturn null == this.path ? StrUtil.SLASH : this.path.build(charset, this.needEncodePercent);\n\t}\n\n\t/**\n\t * 是否path的末尾加 /\n\t *\n\t * @param withEngTag 是否path的末尾加 /\n\t * @return this\n\t * @since 5.8.5\n\t */\n\tpublic UrlBuilder setWithEndTag(boolean withEngTag) {\n\t\tif (null == this.path) {\n\t\t\tthis.path = new UrlPath();\n\t\t}\n\n\t\tthis.path.setWithEndTag(withEngTag);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置路径，例如/aa/bb/cc，将覆盖之前所有的path相关设置\n\t *\n\t * @param path 路径，例如/aa/bb/cc\n\t * @return this\n\t */\n\tpublic UrlBuilder setPath(UrlPath path) {\n\t\tthis.path = path;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加路径，在现有路径基础上追加路径\n\t *\n\t * @param path 路径，例如aaa/bbb/ccc\n\t * @return this\n\t */\n\tpublic UrlBuilder addPath(CharSequence path) {\n\t\tUrlPath.of(path, this.charset).getSegments().forEach(this::addPathSegment);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加路径节点，路径节点中的\"/\"会被转义为\"%2F\"\n\t *\n\t * @param segment 路径节点\n\t * @return this\n\t * @since 5.7.16\n\t */\n\tpublic UrlBuilder addPathSegment(CharSequence segment) {\n\t\tif (StrUtil.isEmpty(segment)) {\n\t\t\treturn this;\n\t\t}\n\t\tif (null == this.path) {\n\t\t\tthis.path = new UrlPath();\n\t\t}\n\t\tthis.path.add(segment);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加path节点\n\t *\n\t * @param path path节点\n\t * @return this\n\t * @deprecated 方法重复，请使用{@link #addPath(CharSequence)}\n\t */\n\t@Deprecated\n\tpublic UrlBuilder appendPath(CharSequence path) {\n\t\treturn addPath(path);\n\t}\n\n\t/**\n\t * 获取查询语句，例如a=1&amp;b=2<br>\n\t * 可能为{@code null}\n\t *\n\t * @return 查询语句，例如a=1&amp;b=2，可能为{@code null}\n\t */\n\tpublic UrlQuery getQuery() {\n\t\treturn query;\n\t}\n\n\t/**\n\t * 获取查询语句，例如a=1&amp;b=2\n\t *\n\t * @return 查询语句，例如a=1&amp;b=2\n\t */\n\tpublic String getQueryStr() {\n\t\treturn null == this.query ? null : this.query.build(this.charset, this.needEncodePercent);\n\t}\n\n\t/**\n\t * 设置查询语句，例如a=1&amp;b=2，将覆盖之前所有的query相关设置\n\t *\n\t * @param query 查询语句，例如a=1&amp;b=2\n\t * @return this\n\t */\n\tpublic UrlBuilder setQuery(UrlQuery query) {\n\t\tthis.query = query;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加查询项，支持重复键\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @return this\n\t */\n\tpublic UrlBuilder addQuery(String key, Object value) {\n\t\tif (StrUtil.isEmpty(key)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tif (this.query == null) {\n\t\t\tthis.query = new UrlQuery();\n\t\t}\n\t\tthis.query.add(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取标识符，#后边的部分\n\t *\n\t * @return 标识符，例如#后边的部分\n\t */\n\tpublic String getFragment() {\n\t\treturn fragment;\n\t}\n\n\t/**\n\t * 获取标识符，#后边的部分\n\t *\n\t * @return 标识符，例如#后边的部分\n\t */\n\tpublic String getFragmentEncoded() {\n\t\tfinal char[] safeChars = this.needEncodePercent ? null : new char[]{'%'};\n\t\treturn RFC3986.FRAGMENT.encode(this.fragment, this.charset, safeChars);\n\t}\n\n\t/**\n\t * 设置标识符，例如#后边的部分\n\t *\n\t * @param fragment 标识符，例如#后边的部分\n\t * @return this\n\t */\n\tpublic UrlBuilder setFragment(String fragment) {\n\t\tif (StrUtil.isEmpty(fragment)) {\n\t\t\tthis.fragment = null;\n\t\t}\n\t\tthis.fragment = StrUtil.removePrefix(fragment, \"#\");\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取编码，用于URLEncode和URLDecode\n\t *\n\t * @return 编码\n\t */\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\t/**\n\t * 设置编码，用于URLEncode和URLDecode\n\t *\n\t * @param charset 编码\n\t * @return this\n\t */\n\tpublic UrlBuilder setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建URL字符串\n\t *\n\t * @return URL字符串\n\t */\n\t@Override\n\tpublic String build() {\n\t\treturn toURL().toString();\n\t}\n\n\t/**\n\t * 转换为{@link URL} 对象\n\t *\n\t * @return {@link URL}\n\t */\n\tpublic URL toURL() {\n\t\treturn toURL(null);\n\t}\n\n\t/**\n\t * 转换为{@link URL} 对象\n\t *\n\t * @param handler {@link URLStreamHandler}，null表示默认\n\t * @return {@link URL}\n\t */\n\tpublic URL toURL(URLStreamHandler handler) {\n\t\tfinal StringBuilder fileBuilder = new StringBuilder();\n\n\t\t// path\n\t\tfileBuilder.append(getPathStr());\n\n\t\t// query\n\t\tfinal String query = getQueryStr();\n\t\tif (StrUtil.isNotBlank(query)) {\n\t\t\tfileBuilder.append('?').append(query);\n\t\t}\n\n\t\t// fragment\n\t\tif (StrUtil.isNotBlank(this.fragment)) {\n\t\t\tfileBuilder.append('#').append(getFragmentEncoded());\n\t\t}\n\n\t\ttry {\n\t\t\treturn new URL(getSchemeWithDefault(), host, port, fileBuilder.toString(), handler);\n\t\t} catch (MalformedURLException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 转换为URI\n\t *\n\t * @return URI\n\t */\n\tpublic URI toURI() {\n\t\ttry {\n\t\t\treturn toURL().toURI();\n\t\t} catch (URISyntaxException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn build();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java",
    "content": "package cn.hutool.core.net.url;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.RFC3986;\nimport cn.hutool.core.net.URLDecoder;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * URL中Path部分的封装\n *\n * @author looly\n * @since 5.3.1\n */\npublic class UrlPath {\n\n\tprivate List<String> segments;\n\tprivate boolean withEngTag;\n\n\t/**\n\t * 构建UrlPath\n\t *\n\t * @param pathStr 初始化的路径字符串\n\t * @param charset decode用的编码，null表示不做decode\n\t * @return UrlPath\n\t */\n\tpublic static UrlPath of(CharSequence pathStr, Charset charset) {\n\t\tfinal UrlPath urlPath = new UrlPath();\n\t\turlPath.parse(pathStr, charset);\n\t\treturn urlPath;\n\t}\n\n\t/**\n\t * 是否path的末尾加 /\n\t *\n\t * @param withEngTag 是否path的末尾加 /\n\t * @return this\n\t */\n\tpublic UrlPath setWithEndTag(boolean withEngTag) {\n\t\tthis.withEngTag = withEngTag;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取path的节点列表\n\t *\n\t * @return 节点列表\n\t */\n\tpublic List<String> getSegments() {\n\t\treturn ObjectUtil.defaultIfNull(this.segments, ListUtil::empty);\n\t}\n\n\t/**\n\t * 获得指定节点\n\t *\n\t * @param index 节点位置\n\t * @return 节点，无节点或者越界返回null\n\t */\n\tpublic String getSegment(int index) {\n\t\tif (null == this.segments || index >= this.segments.size()) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.segments.get(index);\n\t}\n\n\t/**\n\t * 添加到path最后面\n\t *\n\t * @param segment Path节点\n\t * @return this\n\t */\n\tpublic UrlPath add(CharSequence segment) {\n\t\taddInternal(fixPath(segment), false);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加到path最前面\n\t *\n\t * @param segment Path节点\n\t * @return this\n\t */\n\tpublic UrlPath addBefore(CharSequence segment) {\n\t\taddInternal(fixPath(segment), true);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 解析path\n\t *\n\t * @param path    路径，类似于aaa/bb/ccc或/aaa/bbb/ccc\n\t * @param charset decode编码，null表示不解码\n\t * @return this\n\t */\n\tpublic UrlPath parse(CharSequence path, Charset charset) {\n\t\tif (StrUtil.isNotEmpty(path)) {\n\t\t\t// 原URL中以/结尾，则这个规则需保留，issue#I1G44J@Gitee\n\t\t\tif(StrUtil.endWith(path, CharUtil.SLASH)){\n\t\t\t\tthis.withEngTag = true;\n\t\t\t}\n\n\t\t\tpath = fixPath(path);\n\t\t\tif(StrUtil.isNotEmpty(path)){\n\t\t\t\tfinal List<String> split = StrUtil.split(path, '/');\n\t\t\t\tfor (String seg : split) {\n\t\t\t\t\taddInternal(URLDecoder.decodeForPath(seg, charset), false);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建path，前面带'/'<br>\n\t * <pre>\n\t *     path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty\n\t * </pre>\n\t *\n\t * @param charset encode编码，null表示不做encode\n\t * @return 如果没有任何内容，则返回空字符串\"\"\n\t */\n\tpublic String build(Charset charset) {\n\t\treturn build(charset, true);\n\t}\n\n\t/**\n\t * 构建path，前面带'/'<br>\n\t * <pre>\n\t *     path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty\n\t * </pre>\n\t *\n\t * @param charset encode编码，null表示不做encode\n\t * @param encodePercent 是否编码`%`\n\t * @return 如果没有任何内容，则返回空字符串\"\"\n\t * @since 5.8.0\n\t */\n\tpublic String build(Charset charset, boolean encodePercent) {\n\t\tif (CollUtil.isEmpty(this.segments)) {\n\t\t\t// 没有节点的path取决于是否末尾追加/，如果不追加返回空串，否则返回/\n\t\t\treturn withEngTag ? StrUtil.SLASH : StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal char[] safeChars = encodePercent ? null : new char[]{'%'};\n\t\tfinal StringBuilder builder = new StringBuilder();\n\t\tfor (final String segment : segments) {\n\t\t\t// https://www.ietf.org/rfc/rfc3986.html#section-3.3\n\t\t\t// 此处Path中是允许有`:`的，之前理解有误，应该是相对URI的第一个segment中不允许有`:`\n\t\t\tbuilder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars));\n\t\t}\n\n\t\tif(withEngTag){\n\t\t\tif (StrUtil.isEmpty(builder)) {\n\t\t\t\t// 空白追加是保证以/开头\n\t\t\t\tbuilder.append(CharUtil.SLASH);\n\t\t\t}else if (false == StrUtil.endWith(builder, CharUtil.SLASH)) {\n\t\t\t\t// 尾部没有/则追加，否则不追加\n\t\t\t\tbuilder.append(CharUtil.SLASH);\n\t\t\t}\n\t\t}\n\n\t\treturn builder.toString();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn build(null);\n\t}\n\n\t/**\n\t * 增加节点\n\t *\n\t * @param segment 节点\n\t * @param before  是否在前面添加\n\t */\n\tprivate void addInternal(CharSequence segment, boolean before) {\n\t\tif (this.segments == null) {\n\t\t\tthis.segments = new LinkedList<>();\n\t\t}\n\n\t\tfinal String seg = StrUtil.str(segment);\n\t\tif (before) {\n\t\t\tthis.segments.add(0, seg);\n\t\t} else {\n\t\t\tthis.segments.add(seg);\n\t\t}\n\t}\n\n\t/**\n\t * 修正路径，包括去掉前后的/，去掉空白符\n\t *\n\t * @param path 节点或路径path\n\t * @return 修正后的路径\n\t */\n\tprivate static String fixPath(CharSequence path) {\n\t\tAssert.notNull(path, \"Path segment must be not null!\");\n\t\tif (\"/\".contentEquals(path)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tString segmentStr = StrUtil.trim(path);\n\t\tsegmentStr = StrUtil.removePrefix(segmentStr, StrUtil.SLASH);\n\t\tsegmentStr = StrUtil.removeSuffix(segmentStr, StrUtil.SLASH);\n\t\tsegmentStr = StrUtil.trim(segmentStr);\n\t\treturn segmentStr;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java",
    "content": "package cn.hutool.core.net.url;\n\nimport cn.hutool.core.codec.PercentCodec;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.TableMap;\nimport cn.hutool.core.net.FormUrlencoded;\nimport cn.hutool.core.net.RFC3986;\nimport cn.hutool.core.net.URLDecoder;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * URL中查询字符串部分的封装，类似于：\n * <pre>\n *   key1=v1&amp;key2=&amp;key3=v3\n * </pre>\n * 查询封装分为解析查询字符串和构建查询字符串，解析可通过charset为null来自定义是否decode编码后的内容，<br>\n * 构建则通过charset是否为null是否encode参数键值对\n *\n * @author looly\n * @since 5.3.1\n */\npublic class UrlQuery {\n\n\tprivate final TableMap<CharSequence, CharSequence> query;\n\t/**\n\t * 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t */\n\tprivate final boolean isFormUrlEncoded;\n\t/**\n\t * 是否严格模式，严格模式下，query的name和value中均不允许有分隔符。\n\t */\n\tprivate boolean isStrict;\n\n\t/**\n\t * 构建UrlQuery\n\t *\n\t * @param queryMap 初始化的查询键值对\n\t * @return UrlQuery\n\t */\n\tpublic static UrlQuery of(Map<? extends CharSequence, ?> queryMap) {\n\t\treturn new UrlQuery(queryMap);\n\t}\n\n\t/**\n\t * 构建UrlQuery\n\t *\n\t * @param queryMap         初始化的查询键值对\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @return UrlQuery\n\t */\n\tpublic static UrlQuery of(Map<? extends CharSequence, ?> queryMap, boolean isFormUrlEncoded) {\n\t\treturn new UrlQuery(queryMap, isFormUrlEncoded);\n\t}\n\n\t/**\n\t * 构建UrlQuery\n\t *\n\t * @param queryStr 初始化的查询字符串\n\t * @param charset  decode用的编码，null表示不做decode\n\t * @return UrlQuery\n\t */\n\tpublic static UrlQuery of(String queryStr, Charset charset) {\n\t\treturn of(queryStr, charset, true);\n\t}\n\n\t/**\n\t * 构建UrlQuery\n\t *\n\t * @param queryStr       初始化的查询字符串\n\t * @param charset        decode用的编码，null表示不做decode\n\t * @param autoRemovePath 是否自动去除path部分，{@code true}则自动去除第一个?前的内容\n\t * @return UrlQuery\n\t * @since 5.5.8\n\t */\n\tpublic static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath) {\n\t\treturn of(queryStr, charset, autoRemovePath, false);\n\t}\n\n\t/**\n\t * 构建UrlQuery\n\t *\n\t * @param queryStr         初始化的查询字符串\n\t * @param charset          decode用的编码，null表示不做decode\n\t * @param autoRemovePath   是否自动去除path部分，{@code true}则自动去除第一个?前的内容\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @return UrlQuery\n\t * @since 5.7.16\n\t */\n\tpublic static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath, boolean isFormUrlEncoded) {\n\t\treturn new UrlQuery(isFormUrlEncoded).parse(queryStr, charset, autoRemovePath);\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic UrlQuery() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @since 5.7.16\n\t */\n\tpublic UrlQuery(boolean isFormUrlEncoded) {\n\t\tthis(null, isFormUrlEncoded);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param queryMap 初始化的查询键值对\n\t */\n\tpublic UrlQuery(Map<? extends CharSequence, ?> queryMap) {\n\t\tthis(queryMap, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param queryMap         初始化的查询键值对\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @since 5.7.16\n\t */\n\tpublic UrlQuery(Map<? extends CharSequence, ?> queryMap, boolean isFormUrlEncoded) {\n\t\tif (MapUtil.isNotEmpty(queryMap)) {\n\t\t\tquery = new TableMap<>(queryMap.size());\n\t\t\taddAll(queryMap);\n\t\t} else {\n\t\t\tquery = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);\n\t\t}\n\t\tthis.isFormUrlEncoded = isFormUrlEncoded;\n\t}\n\n\t/**\n\t * 设置是否严格模式\n\t *\n\t * @param strict 是否严格模式\n\t * @return this\n\t * @since 5.8.20\n\t */\n\tpublic UrlQuery setStrict(final boolean strict) {\n\t\tisStrict = strict;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加键值对\n\t *\n\t * @param key   键\n\t * @param value 值，集合和数组转换为逗号分隔形式\n\t * @return this\n\t */\n\tpublic UrlQuery add(CharSequence key, Object value) {\n\t\tthis.query.put(key, toStr(value));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 批量增加键值对\n\t *\n\t * @param queryMap query中的键值对\n\t * @return this\n\t */\n\tpublic UrlQuery addAll(Map<? extends CharSequence, ?> queryMap) {\n\t\tif (MapUtil.isNotEmpty(queryMap)) {\n\t\t\tqueryMap.forEach(this::add);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除键及对应所有的值\n\t *\n\t * @param key 键\n\t * @return this\n\t * @since 5.8.30\n\t */\n\tpublic UrlQuery remove(CharSequence key) {\n\t\tthis.query.remove(key);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 解析URL中的查询字符串\n\t *\n\t * @param queryStr 查询字符串，类似于key1=v1&amp;key2=&amp;key3=v3\n\t * @param charset  decode编码，null表示不做decode\n\t * @return this\n\t */\n\tpublic UrlQuery parse(String queryStr, Charset charset) {\n\t\treturn parse(queryStr, charset, true);\n\t}\n\n\t/**\n\t * 解析URL中的查询字符串\n\t *\n\t * @param queryStr       查询字符串，类似于key1=v1&amp;key2=&amp;key3=v3\n\t * @param charset        decode编码，null表示不做decode\n\t * @param autoRemovePath 是否自动去除path部分，{@code true}则自动去除第一个?前的内容\n\t * @return this\n\t * @since 5.5.8\n\t */\n\tpublic UrlQuery parse(String queryStr, Charset charset, boolean autoRemovePath) {\n\t\tif (StrUtil.isBlank(queryStr)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tif (autoRemovePath) {\n\t\t\t// 去掉Path部分\n\t\t\tint pathEndPos = queryStr.indexOf('?');\n\t\t\tif (pathEndPos > -1) {\n\t\t\t\tqueryStr = StrUtil.subSuf(queryStr, pathEndPos + 1);\n\t\t\t\tif (StrUtil.isBlank(queryStr)) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t} else if(StrUtil.startWith(queryStr, \"http://\") || StrUtil.startWith(queryStr, \"https://\")){\n\t\t\t\t// issue#IBRVE4 用户传入只有URL，没有param部分，返回空\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\treturn doParse(queryStr, charset);\n\t}\n\n\t/**\n\t * 获得查询的Map\n\t *\n\t * @return 查询的Map，只读\n\t */\n\tpublic Map<CharSequence, CharSequence> getQueryMap() {\n\t\treturn MapUtil.unmodifiable(this.query);\n\t}\n\n\t/**\n\t * 获取查询值\n\t *\n\t * @param key 键\n\t * @return 值\n\t */\n\tpublic CharSequence get(CharSequence key) {\n\t\tif (MapUtil.isEmpty(this.query)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.query.get(key);\n\t}\n\n\t/**\n\t * 构建URL查询字符串，即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>\n\t * 对于{@code null}处理规则如下：\n\t * <ul>\n\t *     <li>如果key为{@code null}，则这个键值对忽略</li>\n\t *     <li>如果value为{@code null}，只保留key，如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>\n\t * </ul>\n\t *\n\t * @param charset encode编码，null表示不做encode编码\n\t * @return URL查询字符串\n\t */\n\tpublic String build(Charset charset) {\n\t\treturn build(charset, true);\n\t}\n\n\t/**\n\t * 构建URL查询字符串，即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>\n\t * 对于{@code null}处理规则如下：\n\t * <ul>\n\t *     <li>如果key为{@code null}，则这个键值对忽略</li>\n\t *     <li>如果value为{@code null}，只保留key，如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>\n\t * </ul>\n\t *\n\t * @param charset       encode编码，null表示不做encode编码\n\t * @param encodePercent 是否编码`%`\n\t * @return URL查询字符串\n\t */\n\tpublic String build(Charset charset, boolean encodePercent) {\n\t\tif (isFormUrlEncoded) {\n\t\t\treturn build(FormUrlencoded.ALL, FormUrlencoded.ALL, charset, encodePercent);\n\t\t}\n\n\t\tif (isStrict) {\n\t\t\treturn build(RFC3986.QUERY_PARAM_NAME_STRICT, RFC3986.QUERY_PARAM_VALUE_STRICT, charset, encodePercent);\n\t\t}\n\t\treturn build(RFC3986.QUERY_PARAM_NAME, RFC3986.QUERY_PARAM_VALUE, charset, encodePercent);\n\t}\n\n\t/**\n\t * 构建URL查询字符串，即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>\n\t * 对于{@code null}处理规则如下：\n\t * <ul>\n\t *     <li>如果key为{@code null}，则这个键值对忽略</li>\n\t *     <li>如果value为{@code null}，只保留key，如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>\n\t * </ul>\n\t *\n\t * @param keyCoder   键值对中键的编码器\n\t * @param valueCoder 键值对中值的编码器\n\t * @param charset    encode编码，null表示不做encode编码\n\t * @return URL查询字符串\n\t * @since 5.7.16\n\t */\n\tpublic String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset) {\n\t\treturn build(keyCoder, valueCoder, charset, true);\n\t}\n\n\t/**\n\t * 构建URL查询字符串，即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>\n\t * 对于{@code null}处理规则如下：\n\t * <ul>\n\t *     <li>如果key为{@code null}，则这个键值对忽略</li>\n\t *     <li>如果value为{@code null}，只保留key，如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>\n\t * </ul>\n\t *\n\t * @param keyCoder      键值对中键的编码器\n\t * @param valueCoder    键值对中值的编码器\n\t * @param charset       encode编码，null表示不做encode编码\n\t * @param encodePercent 是否编码`%`\n\t * @return URL查询字符串\n\t * @since 5.8.0\n\t */\n\tpublic String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset, boolean encodePercent) {\n\t\tif (MapUtil.isEmpty(this.query)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal char[] safeChars = encodePercent ? null : new char[]{'%'};\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tCharSequence name;\n\t\tCharSequence value;\n\t\tfor (Map.Entry<CharSequence, CharSequence> entry : this.query) {\n\t\t\tname = entry.getKey();\n\t\t\tif (null != name) {\n\t\t\t\tif (sb.length() > 0) {\n\t\t\t\t\tsb.append(\"&\");\n\t\t\t\t}\n\t\t\t\tsb.append(keyCoder.encode(name, charset, safeChars));\n\t\t\t\tvalue = entry.getValue();\n\t\t\t\tif (null != value) {\n\t\t\t\t\tsb.append(\"=\").append(valueCoder.encode(value, charset, safeChars));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 生成查询字符串，类似于aaa=111&amp;bbb=222<br>\n\t * 此方法不对任何特殊字符编码，仅用于输出显示\n\t *\n\t * @return 查询字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn build(null);\n\t}\n\n\t/**\n\t * 解析URL中的查询字符串<br>\n\t * 规则见：https://url.spec.whatwg.org/#urlencoded-parsing\n\t *\n\t * @param queryStr 查询字符串，类似于key1=v1&amp;key2=&amp;key3=v3\n\t * @param charset  decode编码，null表示不做decode\n\t * @return this\n\t * @since 5.5.8\n\t */\n\tprivate UrlQuery doParse(String queryStr, Charset charset) {\n\t\tfinal int len = queryStr.length();\n\t\tString name = null;\n\t\tint pos = 0; // 未处理字符开始位置\n\t\tint i; // 未处理字符结束位置\n\t\tchar c; // 当前字符\n\t\tfor (i = 0; i < len; i++) {\n\t\t\tc = queryStr.charAt(i);\n\t\t\tswitch (c) {\n\t\t\t\tcase '='://键和值的分界符\n\t\t\t\t\tif (null == name) {\n\t\t\t\t\t\t// name可以是\"\"\n\t\t\t\t\t\tname = queryStr.substring(pos, i);\n\t\t\t\t\t\t// 开始位置从分节符后开始\n\t\t\t\t\t\tpos = i + 1;\n\t\t\t\t\t}\n\t\t\t\t\t// 当=不作为分界符时，按照普通字符对待\n\t\t\t\t\tbreak;\n\t\t\t\tcase '&'://键值对之间的分界符\n\t\t\t\t\taddParam(name, queryStr.substring(pos, i), charset);\n\t\t\t\t\tname = null;\n\t\t\t\t\tif (i + 4 < len && \"amp;\".equals(queryStr.substring(i + 1, i + 5))) {\n\t\t\t\t\t\t// issue#850@Github，\"&amp;\"转义为\"&\"\n\t\t\t\t\t\ti += 4;\n\t\t\t\t\t}\n\t\t\t\t\t// 开始位置从分节符后开始\n\t\t\t\t\tpos = i + 1;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// 处理结尾\n\t\taddParam(name, queryStr.substring(pos, i), charset);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 对象转换为字符串，用于URL的Query中\n\t *\n\t * @param value 值\n\t * @return 字符串\n\t */\n\tprivate static String toStr(Object value) {\n\t\tString result;\n\t\tif (value instanceof Iterable) {\n\t\t\tresult = CollUtil.join((Iterable<?>) value, \",\");\n\t\t} else if (value instanceof Iterator) {\n\t\t\tresult = IterUtil.join((Iterator<?>) value, \",\");\n\t\t} else {\n\t\t\tresult = Convert.toStr(value);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将键值对加入到值为List类型的Map中,，情况如下：\n\t * <pre>\n\t *     1、key和value都不为null，类似于 \"a=1\"或者\"=1\"，直接put\n\t *     2、key不为null，value为null，类似于 \"a=\"，值传\"\"\n\t *     3、key为null，value不为null，类似于 \"1\"\n\t *     4、key和value都为null，忽略之，比如&&\n\t * </pre>\n\t *\n\t * @param key     key，为null则value作为key\n\t * @param value   value，为null且key不为null时传入\"\"\n\t * @param charset 编码\n\t */\n\tprivate void addParam(String key, String value, Charset charset) {\n\t\tif (null != key) {\n\t\t\tfinal String actualKey = URLDecoder.decode(key, charset, isFormUrlEncoded);\n\t\t\tthis.query.put(actualKey, StrUtil.nullToEmpty(URLDecoder.decode(value, charset, isFormUrlEncoded)));\n\t\t} else if (null != value) {\n\t\t\t// name为空，value作为name，value赋值null\n\t\t\tthis.query.put(URLDecoder.decode(value, charset, isFormUrlEncoded), null);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/net/url/package-info.java",
    "content": "/**\n * URL相关工具\n *\n * @author looly\n * @since 5.3.1\n */\npackage cn.hutool.core.net.url;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/package-info.java",
    "content": "/**\n * Hutool核心方法及数据结构包\n *\n * @author looly\n *\n */\npackage cn.hutool.core;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java",
    "content": "package cn.hutool.core.stream;\n\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.StringJoiner;\nimport java.util.function.BiConsumer;\nimport java.util.function.BinaryOperator;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collector;\nimport java.util.stream.Collectors;\n\n/**\n * 可变的汇聚操作{@link Collector} 相关工具封装\n *\n * @author looly, VampireAchao\n * @since 5.6.7\n */\npublic class CollectorUtil {\n\n\t/**\n\t * 说明已包含IDENTITY_FINISH特征 为 Characteristics.IDENTITY_FINISH 的缩写\n\t */\n\tpublic static final Set<Collector.Characteristics> CH_ID\n\t\t= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));\n\t/**\n\t * 说明不包含IDENTITY_FINISH特征\n\t */\n\tpublic static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();\n\n\t/**\n\t * 提供任意对象的Join操作的{@link Collector}实现，对象默认调用toString方法\n\t *\n\t * @param delimiter 分隔符\n\t * @param <T>       对象类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T> Collector<T, ?, String> joining(CharSequence delimiter) {\n\t\treturn joining(delimiter, Object::toString);\n\t}\n\n\t/**\n\t * 提供任意对象的Join操作的{@link Collector}实现\n\t *\n\t * @param delimiter    分隔符\n\t * @param toStringFunc 自定义指定对象转换为字符串的方法\n\t * @param <T>          对象类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T> Collector<T, ?, String> joining(CharSequence delimiter,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  Function<T, ? extends CharSequence> toStringFunc) {\n\t\treturn joining(delimiter, StrUtil.EMPTY, StrUtil.EMPTY, toStringFunc);\n\t}\n\n\t/**\n\t * 提供任意对象的Join操作的{@link Collector}实现\n\t *\n\t * @param delimiter    分隔符\n\t * @param prefix       前缀\n\t * @param suffix       后缀\n\t * @param toStringFunc 自定义指定对象转换为字符串的方法\n\t * @param <T>          对象类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T> Collector<T, ?, String> joining(CharSequence delimiter,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  CharSequence prefix,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  CharSequence suffix,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  Function<T, ? extends CharSequence> toStringFunc) {\n\t\treturn new SimpleCollector<>(\n\t\t\t() -> new StringJoiner(delimiter, prefix, suffix),\n\t\t\t(joiner, ele) -> joiner.add(toStringFunc.apply(ele)),\n\t\t\tStringJoiner::merge,\n\t\t\tStringJoiner::toString,\n\t\t\tCollections.emptySet()\n\t\t);\n\t}\n\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现，可指定map类型\n\t *\n\t * @param classifier 分组依据\n\t * @param mapFactory 提供的map\n\t * @param downstream 下游操作\n\t * @param <T>        实体类型\n\t * @param <K>        实体中的分组依据对应类型，也是Map中key的类型\n\t * @param <D>        下游操作对应返回类型，也是Map中value的类型\n\t * @param <A>        下游操作在进行中间操作时对应类型\n\t * @param <M>        最后返回结果Map类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t  Supplier<M> mapFactory,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t  Collector<? super T, A, D> downstream) {\n\t\tfinal Supplier<A> downstreamSupplier = downstream.supplier();\n\t\tfinal BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();\n\t\tfinal BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {\n\t\t\tfinal K key = Opt.ofNullable(t).map(classifier).orElse(null);\n\t\t\tfinal A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());\n\t\t\tdownstreamAccumulator.accept(container, t);\n\t\t};\n\t\tfinal BinaryOperator<Map<K, A>> merger = mapMerger(downstream.combiner());\n\t\t@SuppressWarnings(\"unchecked\") final Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;\n\n\t\tif (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {\n\t\t\treturn new SimpleCollector<>(mangledFactory, accumulator, merger, CH_ID);\n\t\t} else {\n\t\t\t@SuppressWarnings(\"unchecked\") final Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();\n\t\t\tfinal Function<Map<K, A>, M> finisher = intermediate -> {\n\t\t\t\tintermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));\n\t\t\t\t@SuppressWarnings(\"unchecked\") final M castResult = (M) intermediate;\n\t\t\t\treturn castResult;\n\t\t\t};\n\t\t\treturn new SimpleCollector<>(mangledFactory, accumulator, merger, finisher, CH_NOID);\n\t\t}\n\t}\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现\n\t *\n\t * @param classifier 分组依据\n\t * @param downstream 下游操作\n\t * @param <T>        实体类型\n\t * @param <K>        实体中的分组依据对应类型，也是Map中key的类型\n\t * @param <D>        下游操作对应返回类型，也是Map中value的类型\n\t * @param <A>        下游操作在进行中间操作时对应类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K, A, D>\n\tCollector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,\n\t\t\t\t\t\t\t\t\t\t  Collector<? super T, A, D> downstream) {\n\t\treturn groupingBy(classifier, HashMap::new, downstream);\n\t}\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现\n\t *\n\t * @param classifier 分组依据\n\t * @param <T>        实体类型\n\t * @param <K>        实体中的分组依据对应类型，也是Map中key的类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K> Collector<T, ?, Map<K, List<T>>>\n\tgroupingBy(Function<? super T, ? extends K> classifier) {\n\t\treturn groupingBy(classifier, Collectors.toList());\n\t}\n\n\t/**\n\t * 对null友好的 toMap 操作的 {@link Collector}实现，默认使用HashMap\n\t *\n\t * @param keyMapper     指定map中的key\n\t * @param valueMapper   指定map中的value\n\t * @param mergeFunction 合并前对value进行的操作\n\t * @param <T>           实体类型\n\t * @param <K>           map中key的类型\n\t * @param <U>           map中value的类型\n\t * @return 对null友好的 toMap 操作的 {@link Collector}实现\n\t */\n\tpublic static <T, K, U>\n\tCollector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,\n\t\t\t\t\t\t\t\t\t Function<? super T, ? extends U> valueMapper,\n\t\t\t\t\t\t\t\t\t BinaryOperator<U> mergeFunction) {\n\t\treturn toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);\n\t}\n\n\t/**\n\t * 对null友好的 toMap 操作的 {@link Collector}实现\n\t *\n\t * @param keyMapper     指定map中的key\n\t * @param valueMapper   指定map中的value\n\t * @param mergeFunction 合并前对value进行的操作\n\t * @param mapSupplier   最终需要的map类型\n\t * @param <T>           实体类型\n\t * @param <K>           map中key的类型\n\t * @param <U>           map中value的类型\n\t * @param <M>           map的类型\n\t * @return 对null友好的 toMap 操作的 {@link Collector}实现\n\t */\n\tpublic static <T, K, U, M extends Map<K, U>>\n\tCollector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,\n\t\t\t\t\t\t\t Function<? super T, ? extends U> valueMapper,\n\t\t\t\t\t\t\t BinaryOperator<U> mergeFunction,\n\t\t\t\t\t\t\t Supplier<M> mapSupplier) {\n\t\tBiConsumer<M, T> accumulator\n\t\t\t= (map, element) -> map.put(Opt.ofNullable(element).map(keyMapper).get(), Opt.ofNullable(element).map(valueMapper).get());\n\t\treturn new SimpleCollector<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);\n\t}\n\n\t/**\n\t * 用户合并map的BinaryOperator，传入合并前需要对value进行的操作\n\t *\n\t * @param mergeFunction 合并前需要对value进行的操作\n\t * @param <K>           key的类型\n\t * @param <V>           value的类型\n\t * @param <M>           map\n\t * @return 用户合并map的BinaryOperator\n\t */\n\tpublic static <K, V, M extends Map<K, V>> BinaryOperator<M> mapMerger(BinaryOperator<V> mergeFunction) {\n\t\treturn (m1, m2) -> {\n\t\t\tfor (Map.Entry<K, V> e : m2.entrySet()) {\n\t\t\t\tm1.merge(e.getKey(), e.getValue(), mergeFunction);\n\t\t\t}\n\t\t\treturn m1;\n\t\t};\n\t}\n\n\t/**\n\t * 聚合这种数据类型:{@code Collection<Map<K,V>> => Map<K,List<V>>}\n\t * 其中key相同的value，会累加到List中\n\t *\n\t * @param <K> key的类型\n\t * @param <V> value的类型\n\t * @return 聚合后的map\n\t * @since 5.8.5\n\t */\n\tpublic static <K, V> Collector<Map<K, V>, ?, Map<K, List<V>>> reduceListMap() {\n\t\treturn reduceListMap(HashMap::new);\n\t}\n\n\t/**\n\t * 聚合这种数据类型:{@code Collection<Map<K,V>> => Map<K,List<V>>}\n\t * 其中key相同的value，会累加到List中\n\t *\n\t * @param mapSupplier 可自定义map的类型如concurrentHashMap等\n\t * @param <K>         key的类型\n\t * @param <V>         value的类型\n\t * @param <R>         返回值的类型\n\t * @return 聚合后的map\n\t * @since 5.8.5\n\t */\n\tpublic static <K, V, R extends Map<K, List<V>>> Collector<Map<K, V>, ?, R> reduceListMap(final Supplier<R> mapSupplier) {\n\t\treturn Collectors.reducing(mapSupplier.get(), value -> {\n\t\t\t\tfinal R result = mapSupplier.get();\n\t\t\t\tvalue.forEach((k, v) -> result.computeIfAbsent(k, i -> new ArrayList<>()).add(v));\n\t\t\t\treturn result;\n\t\t\t}, (l, r) -> {\n\t\t\t\tfinal R resultMap = mapSupplier.get();\n\t\t\t\tresultMap.putAll(l);\n\t\t\t\tr.forEach((k, v) -> resultMap.computeIfAbsent(k, i -> new ArrayList<>()).addAll(v));\n\t\t\t\treturn resultMap;\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现，\n\t * 对集合分组，然后对分组后的值集合进行映射\n\t *\n\t * @param classifier       分组依据\n\t * @param valueMapper      值映射方法\n\t * @param valueCollFactory 值集合的工厂方法\n\t * @param mapFactory       Map集合的工厂方法\n\t * @param <T>              元素类型\n\t * @param <K>              键类型\n\t * @param <R>              值类型\n\t * @param <C>              值集合类型\n\t * @param <M>              返回的Map集合类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K, R, C extends Collection<R>, M extends Map<K, C>> Collector<T, ?, M> groupingBy(\n\t\tfinal Function<? super T, ? extends K> classifier,\n\t\tfinal Function<? super T, ? extends R> valueMapper,\n\t\tfinal Supplier<C> valueCollFactory,\n\t\tfinal Supplier<M> mapFactory) {\n\t\treturn groupingBy(classifier, mapFactory, Collectors.mapping(\n\t\t\tvalueMapper, Collectors.toCollection(valueCollFactory)\n\t\t));\n\t}\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现，\n\t * 对集合分组，然后对分组后的值集合进行映射\n\t *\n\t * @param classifier       分组依据\n\t * @param valueMapper      值映射方法\n\t * @param valueCollFactory 值集合的工厂方法\n\t * @param <T>              元素类型\n\t * @param <K>              键类型\n\t * @param <R>              值类型\n\t * @param <C>              值集合类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K, R, C extends Collection<R>> Collector<T, ?, Map<K, C>> groupingBy(\n\t\tfinal Function<? super T, ? extends K> classifier,\n\t\tfinal Function<? super T, ? extends R> valueMapper,\n\t\tfinal Supplier<C> valueCollFactory) {\n\t\treturn groupingBy(classifier, valueMapper, valueCollFactory, HashMap::new);\n\t}\n\n\t/**\n\t * 提供对null值友好的groupingBy操作的{@link Collector}实现，\n\t * 对集合分组，然后对分组后的值集合进行映射\n\t *\n\t * @param classifier  分组依据\n\t * @param valueMapper 值映射方法\n\t * @param <T>         元素类型\n\t * @param <K>         键类型\n\t * @param <R>         值类型\n\t * @return {@link Collector}\n\t */\n\tpublic static <T, K, R> Collector<T, ?, Map<K, List<R>>> groupingBy(\n\t\tfinal Function<? super T, ? extends K> classifier,\n\t\tfinal Function<? super T, ? extends R> valueMapper) {\n\t\treturn groupingBy(classifier, valueMapper, ArrayList::new, HashMap::new);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/stream/SimpleCollector.java",
    "content": "package cn.hutool.core.stream;\n\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.BinaryOperator;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collector;\n\n/**\n * 简单{@link Collector}接口实现\n *\n * @param <T> 输入数据类型\n * @param <A> 累积结果的容器类型\n * @param <R> 数据结果类型\n * @since 5.6.7\n */\npublic class SimpleCollector<T, A, R> implements Collector<T, A, R> {\n\t/**\n\t * 创建新的结果容器，容器类型为A\n\t */\n\tprivate final Supplier<A> supplier;\n\t/**\n\t * 将输入元素合并到结果容器中\n\t */\n\tprivate final BiConsumer<A, T> accumulator;\n\t/**\n\t * 合并两个结果容器（并行流使用，将多个线程产生的结果容器合并）\n\t */\n\tprivate final BinaryOperator<A> combiner;\n\t/**\n\t * 将结果容器转换成最终的表示\n\t */\n\tprivate final Function<A, R> finisher;\n\t/**\n\t * 特征值枚举，见{@link Characteristics}\n\t * <ul>\n\t *     <li>CONCURRENT：     表示结果容器只有一个（即使是在并行流的情况下）。\n\t *     只有在并行流且收集器不具备此特性的情况下，combiner()返回的lambda表达式才会执行（中间结果容器只有一个就无需合并）。\n\t *     设置此特性时意味着多个线程可以对同一个结果容器调用，因此结果容器必须是线程安全的。</li>\n\t *     <li>UNORDERED：      表示流中的元素无序</li>\n\t *     <li>IDENTITY_FINISH：表示中间结果容器类型与最终结果类型一致。设置此特性时finisher()方法不会被调用</li>\n\t * </ul>\n\t */\n\tprivate final Set<Characteristics> characteristics;\n\n\t/**\n\t * 构造\n\t *\n\t * @param supplier        创建新的结果容器函数\n\t * @param accumulator     将输入元素合并到结果容器中函数\n\t * @param combiner        合并两个结果容器函数（并行流使用，将多个线程产生的结果容器合并）\n\t * @param finisher        将结果容器转换成最终的表示函数\n\t * @param characteristics 特征值枚举\n\t */\n\tpublic SimpleCollector(Supplier<A> supplier,\n\t\t\t\t\t\t   BiConsumer<A, T> accumulator,\n\t\t\t\t\t\t   BinaryOperator<A> combiner,\n\t\t\t\t\t\t   Function<A, R> finisher,\n\t\t\t\t\t\t   Set<Characteristics> characteristics) {\n\t\tthis.supplier = supplier;\n\t\tthis.accumulator = accumulator;\n\t\tthis.combiner = combiner;\n\t\tthis.finisher = finisher;\n\t\tthis.characteristics = characteristics;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param supplier        创建新的结果容器函数\n\t * @param accumulator     将输入元素合并到结果容器中函数\n\t * @param combiner        合并两个结果容器函数（并行流使用，将多个线程产生的结果容器合并）\n\t * @param characteristics 特征值枚举\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic SimpleCollector(Supplier<A> supplier,\n\t\t\t\t\t\t   BiConsumer<A, T> accumulator,\n\t\t\t\t\t\t   BinaryOperator<A> combiner,\n\t\t\t\t\t\t   Set<Characteristics> characteristics) {\n\t\tthis(supplier, accumulator, combiner, i -> (R) i, characteristics);\n\t}\n\n\t@Override\n\tpublic BiConsumer<A, T> accumulator() {\n\t\treturn accumulator;\n\t}\n\n\t@Override\n\tpublic Supplier<A> supplier() {\n\t\treturn supplier;\n\t}\n\n\t@Override\n\tpublic BinaryOperator<A> combiner() {\n\t\treturn combiner;\n\t}\n\n\t@Override\n\tpublic Function<A, R> finisher() {\n\t\treturn finisher;\n\t}\n\n\t@Override\n\tpublic Set<Characteristics> characteristics() {\n\t\treturn characteristics;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/stream/StreamUtil.java",
    "content": "package cn.hutool.core.stream;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.Spliterators;\nimport java.util.function.Function;\nimport java.util.function.UnaryOperator;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\n/**\n * {@link Stream} 工具类\n *\n * @author looly\n * @since 5.6.7\n */\npublic class StreamUtil {\n\n\t@SafeVarargs\n\tpublic static <T> Stream<T> of(T... array) {\n\t\tAssert.notNull(array, \"Array must be not null!\");\n\t\treturn Stream.of(array);\n\t}\n\n\t/**\n\t * {@link Iterable}转换为{@link Stream}，默认非并行\n\t *\n\t * @param iterable 集合\n\t * @param <T>      集合元素类型\n\t * @return {@link Stream}\n\t */\n\tpublic static <T> Stream<T> of(Iterable<T> iterable) {\n\t\treturn of(iterable, false);\n\t}\n\n\t/**\n\t * {@link Iterable}转换为{@link Stream}\n\t *\n\t * @param iterable 集合\n\t * @param parallel 是否并行\n\t * @param <T>      集合元素类型\n\t * @return {@link Stream}\n\t */\n\tpublic static <T> Stream<T> of(Iterable<T> iterable, boolean parallel) {\n\t\tAssert.notNull(iterable, \"Iterable must be not null!\");\n\n\t\treturn iterable instanceof Collection ?\n\t\t\t\tparallel ? ((Collection<T>) iterable).parallelStream() : ((Collection<T>) iterable).stream() :\n\t\t\t\tStreamSupport.stream(iterable.spliterator(), parallel);\n\t}\n\n\t/**\n\t * {@link Iterator} 转换为 {@link Stream}\n\t * @param iterator 迭代器\n\t * @param <T> 集合元素类型\n\t * @return {@link Stream}\n\t * @throws IllegalArgumentException 如果iterator为null，抛出该异常\n\t */\n\tpublic static <T> Stream<T> of(Iterator<T> iterator) {\n\t\treturn of(iterator, false);\n\t}\n\n\t/**\n\t * {@link Iterator} 转换为 {@link Stream}\n\t * @param iterator 迭代器\n\t * @param parallel 是否并行\n\t * @param <T> 集合元素类型\n\t * @return {@link Stream}\n\t * @throws IllegalArgumentException 如果iterator为null，抛出该异常\n\t */\n\tpublic static <T> Stream<T> of(Iterator<T> iterator, boolean parallel) {\n\t\tAssert.notNull(iterator, \"iterator must not be null!\");\n\t\treturn StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), parallel);\n\t}\n\n\t/**\n\t * 按行读取文件为{@link Stream}\n\t *\n\t * @param file 文件\n\t * @return {@link Stream}\n\t */\n\tpublic static Stream<String> of(File file) {\n\t\treturn of(file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 按行读取文件为{@link Stream}\n\t *\n\t * @param path 路径\n\t * @return {@link Stream}\n\t */\n\tpublic static Stream<String> of(Path path) {\n\t\treturn of(path, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 按行读取文件为{@link Stream}\n\t *\n\t * @param file    文件\n\t * @param charset 编码\n\t * @return {@link Stream}\n\t */\n\tpublic static Stream<String> of(File file, Charset charset) {\n\t\tAssert.notNull(file, \"File must be not null!\");\n\t\treturn of(file.toPath(), charset);\n\t}\n\n\t/**\n\t * 按行读取文件为{@link Stream}\n\t *\n\t * @param path    路径\n\t * @param charset 编码\n\t * @return {@link Stream}\n\t */\n\tpublic static Stream<String> of(Path path, Charset charset) {\n\t\ttry {\n\t\t\treturn Files.lines(path, charset);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 通过函数创建Stream\n\t *\n\t * @param seed           初始值\n\t * @param elementCreator 递进函数，每次调用此函数获取下一个值\n\t * @param limit          限制个数\n\t * @param <T>            创建元素类型\n\t * @return {@link Stream}\n\t */\n\tpublic static <T> Stream<T> of(T seed, UnaryOperator<T> elementCreator, int limit) {\n\t\treturn Stream.iterate(seed, elementCreator).limit(limit);\n\t}\n\n\t/**\n\t * 将Stream中所有元素以指定分隔符，合并为一个字符串，对象默认调用toString方法\n\t *\n\t * @param stream    {@link Stream}\n\t * @param delimiter 分隔符\n\t * @param <T>       元素类型\n\t * @return 字符串\n\t */\n\tpublic static <T> String join(Stream<T> stream, CharSequence delimiter) {\n\t\treturn stream.collect(CollectorUtil.joining(delimiter));\n\t}\n\n\t/**\n\t * 将Stream中所有元素以指定分隔符，合并为一个字符串\n\t *\n\t * @param stream       {@link Stream}\n\t * @param delimiter    分隔符\n\t * @param toStringFunc 元素转换为字符串的函数\n\t * @param <T>          元素类型\n\t * @return 字符串\n\t */\n\tpublic static <T> String join(Stream<T> stream, CharSequence delimiter,\n\t\t\t\t\t\t\t\t  Function<T, ? extends CharSequence> toStringFunc) {\n\t\treturn stream.collect(CollectorUtil.joining(delimiter, toStringFunc));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/stream/package-info.java",
    "content": "/**\n * Java8的stream相关封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.stream;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/DesktopUtil.java",
    "content": "package cn.hutool.core.swing;\n\nimport java.awt.Desktop;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.URLUtil;\n\n/**\n * 桌面相关工具（平台相关）<br>\n * Desktop 类允许 Java 应用程序启动已在本机桌面上注册的关联应用程序，以处理 URI 或文件。\n *\n * @author looly\n * @since 4.5.7\n */\npublic class DesktopUtil {\n\n\t/**\n\t * 获得{@link Desktop}\n\t *\n\t * @return {@link Desktop}\n\t */\n\tpublic static Desktop getDsktop() {\n\t\treturn Desktop.getDesktop();\n\t}\n\n\t/**\n\t * 使用平台默认浏览器打开指定URL地址\n\t *\n\t * @param url URL地址\n\t */\n\tpublic static void browse(String url) {\n\t\tbrowse(URLUtil.toURI(url));\n\t}\n\n\t/**\n\t * 使用平台默认浏览器打开指定URI地址\n\t *\n\t * @param uri URI地址\n\t * @since 4.6.3\n\t */\n\tpublic static void browse(URI uri) {\n\t\tfinal Desktop dsktop = getDsktop();\n\t\ttry {\n\t\t\tdsktop.browse(uri);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 启动关联应用程序来打开文件\n\t *\n\t * @param file URL地址\n\t */\n\tpublic static void open(File file) {\n\t\tfinal Desktop dsktop = getDsktop();\n\t\ttry {\n\t\t\tdsktop.open(file);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 启动关联编辑器应用程序并打开用于编辑的文件\n\t *\n\t * @param file 文件\n\t */\n\tpublic static void edit(File file) {\n\t\tfinal Desktop dsktop = getDsktop();\n\t\ttry {\n\t\t\tdsktop.edit(file);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 使用关联应用程序的打印命令, 用本机桌面打印设备来打印文件\n\t *\n\t * @param file 文件\n\t */\n\tpublic static void print(File file) {\n\t\tfinal Desktop dsktop = getDsktop();\n\t\ttry {\n\t\t\tdsktop.print(file);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 使用平台默认浏览器打开指定URL地址\n\t *\n\t * @param mailAddress 邮件地址\n\t */\n\tpublic static void mail(String mailAddress) {\n\t\tfinal Desktop dsktop = getDsktop();\n\t\ttry {\n\t\t\tdsktop.mail(URLUtil.toURI(mailAddress));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/RobotUtil.java",
    "content": "package cn.hutool.core.swing;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.swing.clipboard.ClipboardUtil;\n\nimport java.awt.AWTException;\nimport java.awt.Rectangle;\nimport java.awt.Robot;\nimport java.awt.event.InputEvent;\nimport java.awt.event.KeyEvent;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\n\n/**\n * {@link Robot} 封装工具类，提供截屏等工具\n *\n * @author looly\n * @since 4.1.14\n */\npublic class RobotUtil {\n\n\tprivate static final Robot ROBOT;\n\tprivate static int delay;\n\n\tstatic {\n\t\ttry {\n\t\t\tROBOT = new Robot();\n\t\t} catch (AWTException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取 Robot 单例实例\n\t *\n\t * @return {@link Robot}单例对象\n\t * @since 5.7.6\n\t */\n\tpublic static Robot getRobot() {\n\t\treturn ROBOT;\n\t}\n\n\t/**\n\t * 设置默认的延迟时间<br>\n\t * 当按键执行完后的等待时间，也可以用ThreadUtil.sleep方法代替\n\t *\n\t * @param delayMillis 等待毫秒数\n\t * @since 4.5.7\n\t */\n\tpublic static void setDelay(int delayMillis) {\n\t\tdelay = delayMillis;\n\t}\n\n\t/**\n\t * 获取全局默认的延迟时间\n\t *\n\t * @return 全局默认的延迟时间\n\t * @since 5.7.6\n\t */\n\tpublic static int getDelay() {\n\t\treturn delay;\n\t}\n\n\t/**\n\t * 模拟鼠标移动\n\t *\n\t * @param x 移动到的x坐标\n\t * @param y 移动到的y坐标\n\t * @since 4.5.7\n\t */\n\tpublic static void mouseMove(int x, int y) {\n\t\tROBOT.mouseMove(x, y);\n\t}\n\n\t/**\n\t * 模拟单击<br>\n\t * 鼠标单击包括鼠标左键的按下和释放\n\t *\n\t * @since 4.5.7\n\t */\n\tpublic static void click() {\n\t\tROBOT.mousePress(InputEvent.BUTTON1_MASK);\n\t\tROBOT.mouseRelease(InputEvent.BUTTON1_MASK);\n\t\tdelay();\n\t}\n\n\t/**\n\t * 模拟右键单击<br>\n\t * 鼠标单击包括鼠标右键的按下和释放\n\t *\n\t * @since 4.5.7\n\t */\n\tpublic static void rightClick() {\n\t\tROBOT.mousePress(InputEvent.BUTTON3_MASK);\n\t\tROBOT.mouseRelease(InputEvent.BUTTON3_MASK);\n\t\tdelay();\n\t}\n\n\t/**\n\t * 模拟鼠标滚轮滚动\n\t *\n\t * @param wheelAmt 滚动数，负数表示向前滚动，正数向后滚动\n\t * @since 4.5.7\n\t */\n\tpublic static void mouseWheel(int wheelAmt) {\n\t\tROBOT.mouseWheel(wheelAmt);\n\t\tdelay();\n\t}\n\n\t/**\n\t * 模拟键盘点击<br>\n\t * 包括键盘的按下和释放\n\t *\n\t * @param keyCodes 按键码列表，见{@link java.awt.event.KeyEvent}\n\t * @since 4.5.7\n\t */\n\tpublic static void keyClick(int... keyCodes) {\n\t\tfor (int keyCode : keyCodes) {\n\t\t\tROBOT.keyPress(keyCode);\n\t\t\tROBOT.keyRelease(keyCode);\n\t\t}\n\t\tdelay();\n\t}\n\n\t/**\n\t * 打印输出指定字符串（借助剪贴板）\n\t *\n\t * @param str 字符串\n\t */\n\tpublic static void keyPressString(String str) {\n\t\tClipboardUtil.setStr(str);\n\t\tkeyPressWithCtrl(KeyEvent.VK_V);// 粘贴\n\t\tdelay();\n\t}\n\n\t/**\n\t * shift+ 按键\n\t *\n\t * @param key 按键\n\t */\n\tpublic static void keyPressWithShift(int key) {\n\t\tROBOT.keyPress(KeyEvent.VK_SHIFT);\n\t\tROBOT.keyPress(key);\n\t\tROBOT.keyRelease(key);\n\t\tROBOT.keyRelease(KeyEvent.VK_SHIFT);\n\t\tdelay();\n\t}\n\n\t/**\n\t * ctrl+ 按键\n\t *\n\t * @param key 按键\n\t */\n\tpublic static void keyPressWithCtrl(int key) {\n\t\tROBOT.keyPress(KeyEvent.VK_CONTROL);\n\t\tROBOT.keyPress(key);\n\t\tROBOT.keyRelease(key);\n\t\tROBOT.keyRelease(KeyEvent.VK_CONTROL);\n\t\tdelay();\n\t}\n\n\t/**\n\t * alt+ 按键\n\t *\n\t * @param key 按键\n\t */\n\tpublic static void keyPressWithAlt(int key) {\n\t\tROBOT.keyPress(KeyEvent.VK_ALT);\n\t\tROBOT.keyPress(key);\n\t\tROBOT.keyRelease(key);\n\t\tROBOT.keyRelease(KeyEvent.VK_ALT);\n\t\tdelay();\n\t}\n\n\t/**\n\t * 截取全屏\n\t *\n\t * @return 截屏的图片\n\t */\n\tpublic static BufferedImage captureScreen() {\n\t\treturn captureScreen(ScreenUtil.getRectangle());\n\t}\n\n\t/**\n\t * 截取全屏到文件\n\t *\n\t * @param outFile 写出到的文件\n\t * @return 写出到的文件\n\t */\n\tpublic static File captureScreen(File outFile) {\n\t\tImgUtil.write(captureScreen(), outFile);\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 截屏\n\t *\n\t * @param screenRect 截屏的矩形区域\n\t * @return 截屏的图片\n\t */\n\tpublic static BufferedImage captureScreen(Rectangle screenRect) {\n\t\treturn ROBOT.createScreenCapture(screenRect);\n\t}\n\n\t/**\n\t * 截屏\n\t *\n\t * @param screenRect 截屏的矩形区域\n\t * @param outFile 写出到的文件\n\t * @return 写出到的文件\n\t */\n\tpublic static File captureScreen(Rectangle screenRect, File outFile) {\n\t\tImgUtil.write(captureScreen(screenRect), outFile);\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 等待指定毫秒数\n\t */\n\tpublic static void delay() {\n\t\tif (delay > 0) {\n\t\t\tROBOT.delay(delay);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/ScreenUtil.java",
    "content": "package cn.hutool.core.swing;\n\nimport java.awt.Dimension;\nimport java.awt.Rectangle;\nimport java.awt.Toolkit;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\n\n/**\n * 屏幕相关（当前显示设置）工具类\n *\n * @author looly\n * @since 4.1.14\n */\npublic class ScreenUtil {\n\tpublic static Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();\n\n\t/**\n\t * 获取屏幕宽度\n\t *\n\t * @return 屏幕宽度\n\t */\n\tpublic static int getWidth() {\n\t\treturn (int) dimension.getWidth();\n\t}\n\n\t/**\n\t * 获取屏幕高度\n\t *\n\t * @return 屏幕高度\n\t */\n\tpublic static int getHeight() {\n\t\treturn (int) dimension.getHeight();\n\t}\n\n\t/**\n\t * 获取屏幕的矩形\n\t * @return 屏幕的矩形\n\t */\n\tpublic static Rectangle getRectangle() {\n\t\treturn new Rectangle(getWidth(), getHeight());\n\t}\n\n\t//-------------------------------------------------------------------------------------------- 截屏\n\t/**\n\t * 截取全屏\n\t *\n\t * @return 截屏的图片\n\t * @see RobotUtil#captureScreen()\n\t */\n\tpublic static BufferedImage captureScreen() {\n\t\treturn RobotUtil.captureScreen();\n\t}\n\n\t/**\n\t * 截取全屏到文件\n\t *\n\t * @param outFile 写出到的文件\n\t * @return 写出到的文件\n\t * @see RobotUtil#captureScreen(File)\n\t */\n\tpublic static File captureScreen(File outFile) {\n\t\treturn RobotUtil.captureScreen(outFile);\n\t}\n\n\t/**\n\t * 截屏\n\t *\n\t * @param screenRect 截屏的矩形区域\n\t * @return 截屏的图片\n\t * @see RobotUtil#captureScreen(Rectangle)\n\t */\n\tpublic static BufferedImage captureScreen(Rectangle screenRect) {\n\t\treturn RobotUtil.captureScreen(screenRect);\n\t}\n\n\t/**\n\t * 截屏\n\t *\n\t * @param screenRect 截屏的矩形区域\n\t * @param outFile 写出到的文件\n\t * @return 写出到的文件\n\t * @see RobotUtil#captureScreen(Rectangle, File)\n\t */\n\tpublic static File captureScreen(Rectangle screenRect, File outFile) {\n\t\treturn RobotUtil.captureScreen(screenRect, outFile);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardListener.java",
    "content": "package cn.hutool.core.swing.clipboard;\n\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.Transferable;\n\n/**\n * 剪贴板监听事件处理接口<br>\n * 用户通过实现此接口，实现监听剪贴板内容变化\n *\n * @author looly\n *@since 4.5.6\n */\npublic interface ClipboardListener {\n\t/**\n\t * 剪贴板变动触发的事件方法<br>\n\t * 在此事件中对剪贴板设置值无效，如若修改，需返回修改内容\n\t *\n\t * @param clipboard 剪贴板对象\n\t * @param contents 内容\n\t * @return 如果对剪贴板内容做修改，则返回修改的内容，{@code null}表示保留原内容\n\t */\n\tTransferable onChange(Clipboard clipboard, Transferable contents);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardMonitor.java",
    "content": "package cn.hutool.core.swing.clipboard;\n\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.ClipboardOwner;\nimport java.awt.datatransfer.Transferable;\nimport java.io.Closeable;\nimport java.util.LinkedHashSet;\nimport java.util.Set;\n\n/**\n * 剪贴板监听\n *\n * @author looly\n * @since 4.5.6\n */\npublic enum ClipboardMonitor implements ClipboardOwner, Runnable, Closeable {\n\tINSTANCE;\n\n\t/** 默认重试此时：10 */\n\tpublic static final int DEFAULT_TRY_COUNT = 10;\n\t/** 默认重试等待：100 */\n\tpublic static final long DEFAULT_DELAY = 100;\n\n\t/** 重试次数 */\n\tprivate int tryCount;\n\t/** 重试等待 */\n\tprivate long delay;\n\t/** 系统剪贴板对象 */\n\tprivate final Clipboard clipboard;\n\t/** 监听事件处理 */\n\tprivate final Set<ClipboardListener> listenerSet = new LinkedHashSet<>();\n\t/** 是否正在监听 */\n\tprivate boolean isRunning;\n\n\t// ---------------------------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，尝试获取剪贴板内容的次数为10，第二次之后延迟100毫秒\n\t */\n\tClipboardMonitor() {\n\t\tthis(DEFAULT_TRY_COUNT, DEFAULT_DELAY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tryCount 尝试获取剪贴板内容的次数\n\t * @param delay 响应延迟，当从第二次开始，延迟一定毫秒数等待剪贴板可以获取，当tryCount小于2时无效\n\t */\n\tClipboardMonitor(int tryCount, long delay) {\n\t\tthis(tryCount, delay, ClipboardUtil.getClipboard());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tryCount 尝试获取剪贴板内容的次数\n\t * @param delay 响应延迟，当从第二次开始，延迟一定毫秒数等待剪贴板可以获取，当tryCount小于2时无效\n\t * @param clipboard 剪贴板对象\n\t */\n\tClipboardMonitor(int tryCount, long delay, Clipboard clipboard) {\n\t\tthis.tryCount = tryCount;\n\t\tthis.delay = delay;\n\t\tthis.clipboard = clipboard;\n\t}\n\t// ---------------------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 设置重试次数\n\t *\n\t * @param tryCount 重试次数\n\t * @return this\n\t */\n\tpublic ClipboardMonitor setTryCount(int tryCount) {\n\t\tthis.tryCount = tryCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置重试等待\n\t *\n\t * @param delay 重试等待\n\t * @return this\n\t */\n\tpublic ClipboardMonitor setDelay(long delay) {\n\t\tthis.delay = delay;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置 监听事件处理\n\t *\n\t * @param listener 监听事件处理\n\t * @return this\n\t */\n\tpublic ClipboardMonitor addListener(ClipboardListener listener) {\n\t\tthis.listenerSet.add(listener);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 去除指定监听\n\t *\n\t * @param listener 监听\n\t * @return this\n\t */\n\tpublic ClipboardMonitor removeListener(ClipboardListener listener) {\n\t\tthis.listenerSet.remove(listener);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空监听\n\t *\n\t * @return this\n\t */\n\tpublic ClipboardMonitor clearListener() {\n\t\tthis.listenerSet.clear();\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void lostOwnership(Clipboard clipboard, Transferable contents) {\n\t\tTransferable newContents;\n\t\ttry {\n\t\t\tnewContents = tryGetContent(clipboard);\n\t\t} catch (InterruptedException e) {\n\t\t\t// 中断后结束简体\n\t\t\treturn;\n\t\t}\n\n\t\tTransferable transferable = null;\n\t\tfor (ClipboardListener listener : listenerSet) {\n\t\t\ttry {\n\t\t\t\ttransferable = listener.onChange(clipboard, ObjectUtil.defaultIfNull(transferable, newContents));\n\t\t\t} catch (Throwable e) {\n\t\t\t\t// 忽略事件处理异常，保证所有监听正常执行\n\t\t\t}\n\t\t}\n\n\t\tif (isRunning) {\n\t\t\t// 继续监听\n\t\t\tclipboard.setContents(ObjectUtil.defaultIfNull(transferable, ObjectUtil.defaultIfNull(newContents, contents)), this);\n\t\t}\n\t}\n\n\t@Override\n\tpublic synchronized void run() {\n\t\tif(false == isRunning) {\n\t\t\tfinal Clipboard clipboard = this.clipboard;\n\t\t\tclipboard.setContents(clipboard.getContents(null), this);\n\t\t\tisRunning = true;\n\t\t}\n\t}\n\n\t/**\n\t * 开始监听\n\t *\n\t * @param sync 是否阻塞\n\t */\n\tpublic void listen(boolean sync) {\n\t\trun();\n\n\t\tif (sync) {\n\t\t\tThreadUtil.sync(this);\n\t\t}\n\t}\n\n\t/**\n\t * 关闭（停止）监听\n\t */\n\t@Override\n\tpublic void close() {\n\t\tthis.isRunning = false;\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 尝试获取剪贴板内容\n\t *\n\t * @param clipboard 剪贴板\n\t * @return 剪贴板内容，{@code null} 表示未获取到\n\t * @throws InterruptedException 线程中断\n\t */\n\tprivate Transferable tryGetContent(Clipboard clipboard) throws InterruptedException {\n\t\tTransferable newContents = null;\n\t\tfor (int i = 0; i < this.tryCount; i++) {\n\t\t\tif (this.delay > 0 && i > 0) {\n\t\t\t\t// 第一次获取不等待，只有从第二次获取时才开始等待\n\t\t\t\t//noinspection BusyWait\n\t\t\t\tThread.sleep(this.delay);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tnewContents = clipboard.getContents(null);\n\t\t\t} catch (IllegalStateException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t\tif (null != newContents) {\n\t\t\t\treturn newContents;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardUtil.java",
    "content": "package cn.hutool.core.swing.clipboard;\n\nimport java.awt.Image;\nimport java.awt.Toolkit;\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.ClipboardOwner;\nimport java.awt.datatransfer.DataFlavor;\nimport java.awt.datatransfer.StringSelection;\nimport java.awt.datatransfer.Transferable;\nimport java.awt.datatransfer.UnsupportedFlavorException;\nimport java.io.IOException;\n\nimport cn.hutool.core.exceptions.UtilException;\n\n/**\n * 系统剪贴板工具类\n *\n * @author looly\n * @since 3.2.0\n */\npublic class ClipboardUtil {\n\n\t/**\n\t * 获取系统剪贴板\n\t *\n\t * @return {@link Clipboard}\n\t */\n\tpublic static Clipboard getClipboard() {\n\t\treturn Toolkit.getDefaultToolkit().getSystemClipboard();\n\t}\n\n\t/**\n\t * 设置内容到剪贴板\n\t *\n\t * @param contents 内容\n\t */\n\tpublic static void set(Transferable contents) {\n\t\tset(contents, null);\n\t}\n\n\t/**\n\t * 设置内容到剪贴板\n\t *\n\t * @param contents 内容\n\t * @param owner 所有者\n\t */\n\tpublic static void set(Transferable contents, ClipboardOwner owner) {\n\t\tgetClipboard().setContents(contents, owner);\n\t}\n\n\t/**\n\t * 获取剪贴板内容\n\t *\n\t * @param flavor 数据元信息，标识数据类型\n\t * @return 剪贴板内容，类型根据flavor不同而不同\n\t */\n\tpublic static Object get(DataFlavor flavor) {\n\t\treturn get(getClipboard().getContents(null), flavor);\n\t}\n\n\t/**\n\t * 获取剪贴板内容\n\t *\n\t * @param content {@link Transferable}\n\t * @param flavor 数据元信息，标识数据类型\n\t * @return 剪贴板内容，类型根据flavor不同而不同\n\t */\n\tpublic static Object get(Transferable content, DataFlavor flavor) {\n\t\tif (null != content && content.isDataFlavorSupported(flavor)) {\n\t\t\ttry {\n\t\t\t\treturn content.getTransferData(flavor);\n\t\t\t} catch (UnsupportedFlavorException | IOException e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 设置字符串文本到剪贴板\n\t *\n\t * @param text 字符串文本\n\t */\n\tpublic static void setStr(String text) {\n\t\tset(new StringSelection(text));\n\t}\n\n\t/**\n\t * 从剪贴板获取文本\n\t *\n\t * @return 文本\n\t */\n\tpublic static String getStr() {\n\t\treturn (String) get(DataFlavor.stringFlavor);\n\t}\n\n\t/**\n\t * 从剪贴板的{@link Transferable}获取文本\n\t *\n\t * @param content {@link Transferable}\n\t * @return 文本\n\t * @since 4.5.6\n\t */\n\tpublic static String getStr(Transferable content) {\n\t\treturn (String) get(content, DataFlavor.stringFlavor);\n\t}\n\n\t/**\n\t * 设置图片到剪贴板\n\t *\n\t * @param image 图像\n\t */\n\tpublic static void setImage(Image image) {\n\t\tset(new ImageSelection(image), null);\n\t}\n\n\t/**\n\t * 从剪贴板获取图片\n\t *\n\t * @return 图片{@link Image}\n\t */\n\tpublic static Image getImage() {\n\t\treturn (Image) get(DataFlavor.imageFlavor);\n\t}\n\n\t/**\n\t * 从剪贴板的{@link Transferable}获取图片\n\t *\n\t * @param content  {@link Transferable}\n\t * @return 图片\n\t * @since 4.5.6\n\t */\n\tpublic static Image getImage(Transferable content) {\n\t\treturn (Image) get(content, DataFlavor.imageFlavor);\n\t}\n\n\t/**\n\t * 监听剪贴板修改事件\n\t *\n\t * @param listener 监听处理接口\n\t * @since 4.5.6\n\t * @see ClipboardMonitor#listen(boolean)\n\t */\n\tpublic static void listen(ClipboardListener listener) {\n\t\tlisten(listener, true);\n\t}\n\n\t/**\n\t * 监听剪贴板修改事件\n\t *\n\t * @param listener 监听处理接口\n\t * @param sync 是否同步阻塞\n\t * @since 4.5.6\n\t * @see ClipboardMonitor#listen(boolean)\n\t */\n\tpublic static void listen(ClipboardListener listener, boolean sync) {\n\t\tlisten(ClipboardMonitor.DEFAULT_TRY_COUNT, ClipboardMonitor.DEFAULT_DELAY, listener, sync);\n\t}\n\n\t/**\n\t * 监听剪贴板修改事件\n\t *\n\t * @param tryCount 尝试获取剪贴板内容的次数\n\t * @param delay 响应延迟，当从第二次开始，延迟一定毫秒数等待剪贴板可以获取\n\t * @param listener 监听处理接口\n\t * @param sync 是否同步阻塞\n\t * @since 4.5.6\n\t * @see ClipboardMonitor#listen(boolean)\n\t */\n\tpublic static void listen(int tryCount, long delay, ClipboardListener listener, boolean sync) {\n\t\tClipboardMonitor.INSTANCE//\n\t\t\t\t.setTryCount(tryCount)//\n\t\t\t\t.setDelay(delay)//\n\t\t\t\t.addListener(listener)//\n\t\t\t\t.listen(sync);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ImageSelection.java",
    "content": "package cn.hutool.core.swing.clipboard;\n\nimport java.awt.Image;\nimport java.awt.datatransfer.DataFlavor;\nimport java.awt.datatransfer.Transferable;\nimport java.awt.datatransfer.UnsupportedFlavorException;\nimport java.io.Serializable;\n\n/**\n * 图片转换器，用于将图片对象转换为剪贴板支持的对象<br>\n * 此对象也用于将图像文件和{@link DataFlavor#imageFlavor} 元信息对应\n *\n * @author looly\n * @since 4.5.6\n */\npublic class ImageSelection implements Transferable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Image image;\n\n\t/**\n\t * 构造\n\t *\n\t * @param image 图片\n\t */\n\tpublic ImageSelection(Image image) {\n\t\tthis.image = image;\n\t}\n\n\t/**\n\t * 获取元数据类型信息\n\t *\n\t * @return 元数据类型列表\n\t */\n\t@Override\n\tpublic DataFlavor[] getTransferDataFlavors() {\n\t\treturn new DataFlavor[] { DataFlavor.imageFlavor };\n\t}\n\n\t/**\n\t * 是否支持指定元数据类型\n\t *\n\t * @param flavor 元数据类型\n\t * @return 是否支持\n\t */\n\t@Override\n\tpublic boolean isDataFlavorSupported(DataFlavor flavor) {\n\t\treturn DataFlavor.imageFlavor.equals(flavor);\n\t}\n\n\t/**\n\t * 获取图片\n\t *\n\t * @param flavor 元数据类型\n\t * @return 转换后的对象\n\t */\n\t@Override\n\tpublic Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {\n\t\tif (false == DataFlavor.imageFlavor.equals(flavor)) {\n\t\t\tthrow new UnsupportedFlavorException(flavor);\n\t\t}\n\t\treturn image;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/StrClipboardListener.java",
    "content": "package cn.hutool.core.swing.clipboard;\n\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.DataFlavor;\nimport java.awt.datatransfer.Transferable;\nimport java.io.Serializable;\n\n/**\n * 剪贴板字符串内容监听\n *\n * @author looly\n * @since 4.5.7\n */\npublic abstract class StrClipboardListener implements ClipboardListener, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tpublic Transferable onChange(Clipboard clipboard, Transferable contents) {\n\t\tif (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {\n\t\t\treturn onChange(clipboard, ClipboardUtil.getStr(contents));\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 剪贴板变动触发的事件方法<br>\n\t * 在此事件中对剪贴板设置值无效，如若修改，需返回修改内容\n\t *\n\t * @param clipboard 剪贴板对象\n\t * @param contents 内容\n\t * @return 如果对剪贴板内容做修改，则返回修改的内容，{@code null}表示保留原内容\n\t */\n\tpublic abstract Transferable onChange(Clipboard clipboard, String contents);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/clipboard/package-info.java",
    "content": "/**\n * 剪贴板相关的工具，包括剪贴板监听等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.swing.clipboard;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/swing/package-info.java",
    "content": "/**\n * Swing和awt相关封装\n *\n * @author looly\n *\n */\npackage cn.hutool.core.swing;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/ASCIIStrCache.java",
    "content": "package cn.hutool.core.text;\n\n/**\n * ASCII字符对应的字符串缓存\n *\n * @author looly\n * @since 4.0.1\n *\n */\npublic class ASCIIStrCache {\n\n\tprivate static final int ASCII_LENGTH = 128;\n\tprivate static final String[] CACHE = new String[ASCII_LENGTH];\n\tstatic {\n\t\tfor (char c = 0; c < ASCII_LENGTH; c++) {\n\t\t\tCACHE[c] = String.valueOf(c);\n\t\t}\n\t}\n\n\t/**\n\t * 字符转为字符串<br>\n\t * 如果为ASCII字符，使用缓存\n\t *\n\t * @param c 字符\n\t * @return 字符串\n\t */\n\tpublic static String toString(char c) {\n\t\treturn c < ASCII_LENGTH ? CACHE[c] : String.valueOf(c);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/AntPathMatcher.java",
    "content": "package cn.hutool.core.text;\n\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Ant风格的路径匹配器。<br>\n * 来自Spring-core和Ant\n *\n * <p>匹配URL的规则如下：<br>\n * <ul>\n * <li>{@code ?} 匹配单个字符</li>\n * <li>{@code *} 匹配0个或多个字符</li>\n * <li>{@code **} 0个或多个路径中的<em>目录节点</em></li>\n * <li>{@code {hutool:[a-z]+}} 匹配以\"hutool\"命名的正则 {@code [a-z]+}</li>\n * </ul>\n *\n * <p>例子: </p>\n * <ul>\n * <li>{@code com/t?st.jsp} &mdash; 匹配 {@code com/test.jsp} 或 {@code com/tast.jsp} 或 {@code com/txst.jsp}</li>\n * <li>{@code com/*.jsp} &mdash; 匹配{@code com}目录下全部 {@code .jsp}文件</li>\n * <li>{@code com/&#42;&#42;/test.jsp} &mdash; 匹配{@code com}目录下全部 {@code test.jsp}文件</li>\n * <li>{@code cn/hutool/&#42;&#42;/*.jsp} &mdash; 匹配{@code cn/hutool}路径下全部{@code .jsp} 文件</li>\n * <li>{@code org/&#42;&#42;/servlet/bla.jsp} &mdash; 匹配{@code cn/hutool/servlet/bla.jsp} 或{@code cn/hutool/testing/servlet/bla.jsp} 或 {@code org/servlet/bla.jsp}</li>\n * <li>{@code com/{filename:\\\\w+}.jsp} 匹配 {@code com/test.jsp} 并将 {@code test} 关联到 {@code filename} 变量</li>\n * </ul>\n *\n * <p><strong>注意:</strong> 表达式和路径必须都为绝对路径或都为相对路径。\n *\n * @author Alef Arendsen, Juergen Hoeller, Rob Harrop, Arjen Poutsma, Rossen Stoyanchev, Sam Brannen, Vladislav Kisel\n * @since 5.7.22\n */\npublic class AntPathMatcher {\n\n\t/**\n\t * Default path separator: \"/\".\n\t */\n\tpublic static final String DEFAULT_PATH_SEPARATOR = StrUtil.SLASH;\n\n\tprivate static final int CACHE_TURNOFF_THRESHOLD = 65536;\n\n\tprivate static final Pattern VARIABLE_PATTERN = Pattern.compile(\"\\\\{[^/]+?}\");\n\n\tprivate static final char[] WILDCARD_CHARS = {'*', '?', '{'};\n\n\tprivate String pathSeparator;\n\n\tprivate PathSeparatorPatternCache pathSeparatorPatternCache;\n\n\tprivate boolean caseSensitive = true;\n\n\tprivate boolean trimTokens = false;\n\n\tprivate volatile Boolean cachePatterns;\n\n\tprivate final Map<String, String[]> tokenizedPatternCache = new SafeConcurrentHashMap<>(256);\n\n\tprivate final Map<String, AntPathStringMatcher> stringMatcherCache = new SafeConcurrentHashMap<>(256);\n\n\n\t/**\n\t * 使用 {@link #DEFAULT_PATH_SEPARATOR} 作为分隔符构造\n\t */\n\tpublic AntPathMatcher() {\n\t\tthis(DEFAULT_PATH_SEPARATOR);\n\t}\n\n\t/**\n\t * 使用自定义的分隔符构造\n\t *\n\t * @param pathSeparator the path separator to use, must not be {@code null}.\n\t * @since 4.1\n\t */\n\tpublic AntPathMatcher(String pathSeparator) {\n\t\tif (null == pathSeparator) {\n\t\t\tpathSeparator = DEFAULT_PATH_SEPARATOR;\n\t\t}\n\t\tsetPathSeparator(pathSeparator);\n\t}\n\n\n\t/**\n\t * 设置路径分隔符\n\t *\n\t * @param pathSeparator 分隔符，{@code null}表示使用默认分隔符{@link #DEFAULT_PATH_SEPARATOR}\n\t * @return this\n\t */\n\tpublic AntPathMatcher setPathSeparator(String pathSeparator) {\n\t\tif (null == pathSeparator) {\n\t\t\tpathSeparator = DEFAULT_PATH_SEPARATOR;\n\t\t}\n\t\tthis.pathSeparator = pathSeparator;\n\t\tthis.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否大小写敏感，默认为{@code true}\n\t *\n\t * @param caseSensitive 是否大小写敏感\n\t * @return this\n\t */\n\tpublic AntPathMatcher setCaseSensitive(boolean caseSensitive) {\n\t\tthis.caseSensitive = caseSensitive;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否去除路径节点两边的空白符，默认为{@code false}\n\t *\n\t * @param trimTokens 是否去除路径节点两边的空白符\n\t * @return this\n\t */\n\tpublic AntPathMatcher setTrimTokens(boolean trimTokens) {\n\t\tthis.trimTokens = trimTokens;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Specify whether to cache parsed pattern metadata for patterns passed\n\t * into this matcher's {@link #match} method. A value of {@code true}\n\t * activates an unlimited pattern cache; a value of {@code false} turns\n\t * the pattern cache off completely.\n\t * <p>Default is for the cache to be on, but with the variant to automatically\n\t * turn it off when encountering too many patterns to cache at runtime\n\t * (the threshold is 65536), assuming that arbitrary permutations of patterns\n\t * are coming in, with little chance for encountering a recurring pattern.\n\t *\n\t * @param cachePatterns 是否缓存表达式\n\t * @return this\n\t * @see #getStringMatcher(String)\n\t */\n\tpublic AntPathMatcher setCachePatterns(boolean cachePatterns) {\n\t\tthis.cachePatterns = cachePatterns;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 判断给定路径是否是表达式\n\t *\n\t * @param path 路径\n\t * @return 是否为表达式\n\t */\n\tpublic boolean isPattern(String path) {\n\t\tif (path == null) {\n\t\t\treturn false;\n\t\t}\n\t\tboolean uriVar = false;\n\t\tfinal int length = path.length();\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = path.charAt(i);\n\t\t\t// 含有通配符\n\t\t\tif (c == '*' || c == '?') {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (c == CharPool.DELIM_START) {\n\t\t\t\turiVar = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c == CharPool.DELIM_END && uriVar) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 给定路径是否匹配表达式\n\t *\n\t * @param pattern 表达式\n\t * @param path    路径\n\t * @return 是否匹配\n\t */\n\tpublic boolean match(String pattern, String path) {\n\t\treturn doMatch(pattern, path, true, null);\n\t}\n\n\t/**\n\t * 前置部分匹配\n\t *\n\t * @param pattern 表达式\n\t * @param path    路径\n\t * @return 是否匹配\n\t */\n\tpublic boolean matchStart(String pattern, String path) {\n\t\treturn doMatch(pattern, path, false, null);\n\t}\n\n\t/**\n\t * 执行匹配，判断给定的{@code path}是否匹配{@code pattern}\n\t *\n\t * @param pattern              表达式\n\t * @param path                 路径\n\t * @param fullMatch            是否全匹配。{@code true} 表示全路径匹配，{@code false}表示只匹配开始\n\t * @param uriTemplateVariables 变量映射\n\t * @return {@code true} 表示提供的 {@code path} 匹配, {@code false} 表示不匹配\n\t */\n\tprotected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {\n\t\tif (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String[] pattDirs = tokenizePattern(pattern);\n\t\tif (fullMatch && this.caseSensitive && false == isPotentialMatch(path, pattDirs)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String[] pathDirs = tokenizePath(path);\n\t\tint pattIdxStart = 0;\n\t\tint pattIdxEnd = pattDirs.length - 1;\n\t\tint pathIdxStart = 0;\n\t\tint pathIdxEnd = pathDirs.length - 1;\n\n\t\t// Match all elements up to the first **\n\t\twhile (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\n\t\t\tString pattDir = pattDirs[pattIdxStart];\n\t\t\tif (\"**\".equals(pattDir)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (notMatchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tpattIdxStart++;\n\t\t\tpathIdxStart++;\n\t\t}\n\n\t\tif (pathIdxStart > pathIdxEnd) {\n\t\t\t// Path is exhausted, only match if rest of pattern is * or **'s\n\t\t\tif (pattIdxStart > pattIdxEnd) {\n\t\t\t\treturn (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));\n\t\t\t}\n\t\t\tif (false == fullMatch) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals(\"*\") && path.endsWith(this.pathSeparator)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tfor (int i = pattIdxStart; i <= pattIdxEnd; i++) {\n\t\t\t\tif (false == pattDirs[i].equals(\"**\")) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} else if (pattIdxStart > pattIdxEnd) {\n\t\t\t// String not exhausted, but pattern is. Failure.\n\t\t\treturn false;\n\t\t} else if (false == fullMatch && \"**\".equals(pattDirs[pattIdxStart])) {\n\t\t\t// Path start definitely matches due to \"**\" part in pattern.\n\t\t\treturn true;\n\t\t}\n\n\t\t// up to last '**'\n\t\twhile (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\n\t\t\tString pattDir = pattDirs[pattIdxEnd];\n\t\t\tif (pattDir.equals(\"**\")) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (notMatchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tpattIdxEnd--;\n\t\t\tpathIdxEnd--;\n\t\t}\n\t\tif (pathIdxStart > pathIdxEnd) {\n\t\t\t// String is exhausted\n\t\t\tfor (int i = pattIdxStart; i <= pattIdxEnd; i++) {\n\t\t\t\tif (false == pattDirs[i].equals(\"**\")) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\twhile (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {\n\t\t\tint patIdxTmp = -1;\n\t\t\tfor (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {\n\t\t\t\tif (pattDirs[i].equals(\"**\")) {\n\t\t\t\t\tpatIdxTmp = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (patIdxTmp == pattIdxStart + 1) {\n\t\t\t\t// '**/**' situation, so skip one\n\t\t\t\tpattIdxStart++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Find the pattern between padIdxStart & padIdxTmp in str between\n\t\t\t// strIdxStart & strIdxEnd\n\t\t\tint patLength = (patIdxTmp - pattIdxStart - 1);\n\t\t\tint strLength = (pathIdxEnd - pathIdxStart + 1);\n\t\t\tint foundIdx = -1;\n\n\t\t\tstrLoop:\n\t\t\tfor (int i = 0; i <= strLength - patLength; i++) {\n\t\t\t\tfor (int j = 0; j < patLength; j++) {\n\t\t\t\t\tString subPat = pattDirs[pattIdxStart + j + 1];\n\t\t\t\t\tString subStr = pathDirs[pathIdxStart + i + j];\n\t\t\t\t\tif (notMatchStrings(subPat, subStr, uriTemplateVariables)) {\n\t\t\t\t\t\tcontinue strLoop;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfoundIdx = pathIdxStart + i;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (foundIdx == -1) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tpattIdxStart = patIdxTmp;\n\t\t\tpathIdxStart = foundIdx + patLength;\n\t\t}\n\n\t\tfor (int i = pattIdxStart; i <= pattIdxEnd; i++) {\n\t\t\tif (false == pattDirs[i].equals(\"**\")) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate boolean isPotentialMatch(String path, String[] pattDirs) {\n\t\tif (!this.trimTokens) {\n\t\t\tint pos = 0;\n\t\t\tfor (String pattDir : pattDirs) {\n\t\t\t\tint skipped = skipSeparator(path, pos, this.pathSeparator);\n\t\t\t\tpos += skipped;\n\t\t\t\tskipped = skipSegment(path, pos, pattDir);\n\t\t\t\tif (skipped < pattDir.length()) {\n\t\t\t\t\treturn (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0))));\n\t\t\t\t}\n\t\t\t\tpos += skipped;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tprivate int skipSegment(String path, int pos, String prefix) {\n\t\tint skipped = 0;\n\t\tfor (int i = 0; i < prefix.length(); i++) {\n\t\t\tchar c = prefix.charAt(i);\n\t\t\tif (isWildcardChar(c)) {\n\t\t\t\treturn skipped;\n\t\t\t}\n\t\t\tint currPos = pos + skipped;\n\t\t\tif (currPos >= path.length()) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tif (c == path.charAt(currPos)) {\n\t\t\t\tskipped++;\n\t\t\t}\n\t\t}\n\t\treturn skipped;\n\t}\n\n\tprivate int skipSeparator(String path, int pos, String separator) {\n\t\tint skipped = 0;\n\t\twhile (path.startsWith(separator, pos + skipped)) {\n\t\t\tskipped += separator.length();\n\t\t}\n\t\treturn skipped;\n\t}\n\n\tprivate boolean isWildcardChar(char c) {\n\t\tfor (char candidate : WILDCARD_CHARS) {\n\t\t\tif (c == candidate) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Tokenize the given path pattern into parts, based on this matcher's settings.\n\t * <p>Performs caching based on {@link #setCachePatterns}, delegating to\n\t * {@link #tokenizePath(String)} for the actual tokenization algorithm.\n\t *\n\t * @param pattern the pattern to tokenize\n\t * @return the tokenized pattern parts\n\t */\n\tprotected String[] tokenizePattern(String pattern) {\n\t\tString[] tokenized = null;\n\t\tBoolean cachePatterns = this.cachePatterns;\n\t\tif (cachePatterns == null || cachePatterns) {\n\t\t\ttokenized = this.tokenizedPatternCache.get(pattern);\n\t\t}\n\t\tif (tokenized == null) {\n\t\t\ttokenized = tokenizePath(pattern);\n\t\t\tif (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {\n\t\t\t\t// Try to adapt to the runtime situation that we're encountering:\n\t\t\t\t// There are obviously too many different patterns coming in here...\n\t\t\t\t// So let's turn off the cache since the patterns are unlikely to be reoccurring.\n\t\t\t\tdeactivatePatternCache();\n\t\t\t\treturn tokenized;\n\t\t\t}\n\t\t\tif (cachePatterns == null || cachePatterns) {\n\t\t\t\tthis.tokenizedPatternCache.put(pattern, tokenized);\n\t\t\t}\n\t\t}\n\t\treturn tokenized;\n\t}\n\n\tprivate void deactivatePatternCache() {\n\t\tthis.cachePatterns = false;\n\t\tthis.tokenizedPatternCache.clear();\n\t\tthis.stringMatcherCache.clear();\n\t}\n\n\t/**\n\t * Tokenize the given path into parts, based on this matcher's settings.\n\t *\n\t * @param path the path to tokenize\n\t * @return the tokenized path parts\n\t */\n\tprotected String[] tokenizePath(String path) {\n\t\treturn StrSplitter.splitToArray(path, this.pathSeparator, 0, this.trimTokens, true);\n\t}\n\n\t/**\n\t * Test whether or not a string matches against a pattern.\n\t *\n\t * @param pattern the pattern to match against (never {@code null})\n\t * @param str     the String which must be matched against the pattern (never {@code null})\n\t * @return {@code true} if the string matches against the pattern, or {@code false} otherwise\n\t */\n\tprivate boolean notMatchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) {\n\t\treturn false == getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);\n\t}\n\n\t/**\n\t * Build or retrieve an {@link AntPathStringMatcher} for the given pattern.\n\t * <p>The default implementation checks this AntPathMatcher's internal cache\n\t * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance\n\t * if no cached copy is found.\n\t * <p>When encountering too many patterns to cache at runtime (the threshold is 65536),\n\t * it turns the default cache off, assuming that arbitrary permutations of patterns\n\t * are coming in, with little chance for encountering a recurring pattern.\n\t * <p>This method may be overridden to implement a custom cache strategy.\n\t *\n\t * @param pattern the pattern to match against (never {@code null})\n\t * @return a corresponding AntPathStringMatcher (never {@code null})\n\t * @see #setCachePatterns\n\t */\n\tprotected AntPathStringMatcher getStringMatcher(String pattern) {\n\t\tAntPathStringMatcher matcher = null;\n\t\tBoolean cachePatterns = this.cachePatterns;\n\t\tif (cachePatterns == null || cachePatterns) {\n\t\t\tmatcher = this.stringMatcherCache.get(pattern);\n\t\t}\n\t\tif (matcher == null) {\n\t\t\tmatcher = new AntPathStringMatcher(pattern, this.caseSensitive);\n\t\t\tif (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {\n\t\t\t\t// Try to adapt to the runtime situation that we're encountering:\n\t\t\t\t// There are obviously too many different patterns coming in here...\n\t\t\t\t// So let's turn off the cache since the patterns are unlikely to be reoccurring.\n\t\t\t\tdeactivatePatternCache();\n\t\t\t\treturn matcher;\n\t\t\t}\n\t\t\tif (cachePatterns == null || cachePatterns) {\n\t\t\t\tthis.stringMatcherCache.put(pattern, matcher);\n\t\t\t}\n\t\t}\n\t\treturn matcher;\n\t}\n\n\t/**\n\t * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul>\n\t * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} &rarr; ''</li>\n\t * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} &rarr; '{@code cvs/commit}'</li>\n\t * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} &rarr; '{@code commit.html}'</li>\n\t * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} &rarr; '{@code cvs/commit}'</li>\n\t * <li>'{@code /docs/**\\/*.html}' and '{@code /docs/cvs/commit.html} &rarr; '{@code cvs/commit.html}'</li>\n\t * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} &rarr; '{@code docs/cvs/commit.html}'</li>\n\t * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} &rarr; '{@code /docs/cvs/commit.html}'</li>\n\t * <li>'{@code *}' and '{@code /docs/cvs/commit.html} &rarr; '{@code /docs/cvs/commit.html}'</li> </ul>\n\t * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but\n\t * does <strong>not</strong> enforce this.\n\t *\n\t * @param pattern 表达式\n\t * @param path    路径\n\t * @return 表达式匹配到的部分\n\t */\n\tpublic String extractPathWithinPattern(String pattern, String path) {\n\t\tString[] patternParts = tokenizePath(pattern);\n\t\tString[] pathParts = tokenizePath(path);\n\t\tStringBuilder builder = new StringBuilder();\n\t\tboolean pathStarted = false;\n\n\t\tfor (int segment = 0; segment < patternParts.length; segment++) {\n\t\t\tString patternPart = patternParts[segment];\n\t\t\tif (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) {\n\t\t\t\tfor (; segment < pathParts.length; segment++) {\n\t\t\t\t\tif (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) {\n\t\t\t\t\t\tbuilder.append(this.pathSeparator);\n\t\t\t\t\t}\n\t\t\t\t\tbuilder.append(pathParts[segment]);\n\t\t\t\t\tpathStarted = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn builder.toString();\n\t}\n\n\tpublic Map<String, String> extractUriTemplateVariables(String pattern, String path) {\n\t\tMap<String, String> variables = new LinkedHashMap<>();\n\t\tboolean result = doMatch(pattern, path, true, variables);\n\t\tif (!result) {\n\t\t\tthrow new IllegalStateException(\"Pattern \\\"\" + pattern + \"\\\" is not a match for \\\"\" + path + \"\\\"\");\n\t\t}\n\t\treturn variables;\n\t}\n\n\t/**\n\t * Combine two patterns into a new pattern.\n\t * <p>This implementation simply concatenates the two patterns, unless\n\t * the first pattern contains a file extension match (e.g., {@code *.html}).\n\t * In that case, the second pattern will be merged into the first. Otherwise,\n\t * an {@code IllegalArgumentException} will be thrown.\n\t * <p>Examples</p>\n\t * <table border=\"1\" summary=\"\">\n\t * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr>\n\t * <tr><td>{@code null}</td><td>{@code null}</td><td>&nbsp;</td></tr>\n\t * <tr><td>/hotels</td><td>{@code null}</td><td>/hotels</td></tr>\n\t * <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr>\n\t * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr>\n\t * <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr>\n\t * <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr>\n\t * <tr><td>/hotels/&#42;&#42;</td><td>/bookings</td><td>/hotels/&#42;&#42;/bookings</td></tr>\n\t * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>\n\t * <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>\n\t * <tr><td>/hotels/&#42;&#42;</td><td>{hotel}</td><td>/hotels/&#42;&#42;/{hotel}</td></tr>\n\t * <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr>\n\t * <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr>\n\t * <tr><td>/*.html</td><td>/*.txt</td><td>{@code IllegalArgumentException}</td></tr>\n\t * </table>\n\t *\n\t * @param pattern1 the first pattern\n\t * @param pattern2 the second pattern\n\t * @return the combination of the two patterns\n\t * @throws IllegalArgumentException if the two patterns cannot be combined\n\t */\n\tpublic String combine(String pattern1, String pattern2) {\n\t\tif (StrUtil.isEmpty(pattern1) && StrUtil.isEmpty(pattern2)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tif (StrUtil.isEmpty(pattern1)) {\n\t\t\treturn pattern2;\n\t\t}\n\t\tif (StrUtil.isEmpty(pattern2)) {\n\t\t\treturn pattern1;\n\t\t}\n\n\t\tboolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1);\n\t\tif (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) {\n\t\t\t// /* + /hotel -> /hotel ; \"/*.*\" + \"/*.html\" -> /*.html\n\t\t\t// However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar\n\t\t\treturn pattern2;\n\t\t}\n\n\t\t// /hotels/* + /booking -> /hotels/booking\n\t\t// /hotels/* + booking -> /hotels/booking\n\t\tif (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) {\n\t\t\treturn concat(pattern1.substring(0, pattern1.length() - 2), pattern2);\n\t\t}\n\n\t\t// /hotels/** + /booking -> /hotels/**/booking\n\t\t// /hotels/** + booking -> /hotels/**/booking\n\t\tif (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) {\n\t\t\treturn concat(pattern1, pattern2);\n\t\t}\n\n\t\tint starDotPos1 = pattern1.indexOf(\"*.\");\n\t\tif (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(\".\")) {\n\t\t\t// simply concatenate the two patterns\n\t\t\treturn concat(pattern1, pattern2);\n\t\t}\n\n\t\tString ext1 = pattern1.substring(starDotPos1 + 1);\n\t\tint dotPos2 = pattern2.indexOf('.');\n\t\tString file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2));\n\t\tString ext2 = (dotPos2 == -1 ? \"\" : pattern2.substring(dotPos2));\n\t\tboolean ext1All = (ext1.equals(\".*\") || ext1.isEmpty());\n\t\tboolean ext2All = (ext2.equals(\".*\") || ext2.isEmpty());\n\t\tif (!ext1All && !ext2All) {\n\t\t\tthrow new IllegalArgumentException(\"Cannot combine patterns: \" + pattern1 + \" vs \" + pattern2);\n\t\t}\n\t\tString ext = (ext1All ? ext2 : ext1);\n\t\treturn file2 + ext;\n\t}\n\n\tprivate String concat(String path1, String path2) {\n\t\tboolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator);\n\t\tboolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator);\n\n\t\tif (path1EndsWithSeparator && path2StartsWithSeparator) {\n\t\t\treturn path1 + path2.substring(1);\n\t\t} else if (path1EndsWithSeparator || path2StartsWithSeparator) {\n\t\t\treturn path1 + path2;\n\t\t} else {\n\t\t\treturn path1 + this.pathSeparator + path2;\n\t\t}\n\t}\n\n\t/**\n\t * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of\n\t * explicitness.\n\t * <p>This {@code Comparator} will {@linkplain List#sort(Comparator) sort}\n\t * a list so that more specific patterns (without URI templates or wild cards) come before\n\t * generic patterns. So given a list with the following patterns, the returned comparator\n\t * will sort this list so that the order will be as indicated.\n\t * <ol>\n\t * <li>{@code /hotels/new}</li>\n\t * <li>{@code /hotels/{hotel}}</li>\n\t * <li>{@code /hotels/*}</li>\n\t * </ol>\n\t * <p>The full path given as parameter is used to test for exact matches. So when the given path\n\t * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}.\n\t *\n\t * @param path the full path to use for comparison\n\t * @return a comparator capable of sorting patterns in order of explicitness\n\t */\n\tpublic Comparator<String> getPatternComparator(String path) {\n\t\treturn new AntPatternComparator(path);\n\t}\n\n\n\t/**\n\t * Tests whether or not a string matches against a pattern via a {@link Pattern}.\n\t * <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and\n\t * only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>.\n\t */\n\tprotected static class AntPathStringMatcher {\n\n\t\tprivate static final Pattern GLOB_PATTERN = Pattern.compile(\"\\\\?|\\\\*|\\\\{((?:\\\\{[^/]+?}|[^/{}]|\\\\\\\\[{}])+?)}\");\n\n\t\tprivate static final String DEFAULT_VARIABLE_PATTERN = \"((?s).*)\";\n\n\t\tprivate final String rawPattern;\n\n\t\tprivate final boolean caseSensitive;\n\n\t\tprivate final boolean exactMatch;\n\n\t\tprivate final Pattern pattern;\n\n\t\tprivate final List<String> variableNames = new ArrayList<>();\n\n\t\tpublic AntPathStringMatcher(String pattern, boolean caseSensitive) {\n\t\t\tthis.rawPattern = pattern;\n\t\t\tthis.caseSensitive = caseSensitive;\n\t\t\tStringBuilder patternBuilder = new StringBuilder();\n\t\t\tMatcher matcher = GLOB_PATTERN.matcher(pattern);\n\t\t\tint end = 0;\n\t\t\twhile (matcher.find()) {\n\t\t\t\tpatternBuilder.append(quote(pattern, end, matcher.start()));\n\t\t\t\tString match = matcher.group();\n\t\t\t\tif (\"?\".equals(match)) {\n\t\t\t\t\tpatternBuilder.append('.');\n\t\t\t\t} else if (\"*\".equals(match)) {\n\t\t\t\t\tpatternBuilder.append(\".*\");\n\t\t\t\t} else if (match.startsWith(\"{\") && match.endsWith(\"}\")) {\n\t\t\t\t\tint colonIdx = match.indexOf(':');\n\t\t\t\t\tif (colonIdx == -1) {\n\t\t\t\t\t\tpatternBuilder.append(DEFAULT_VARIABLE_PATTERN);\n\t\t\t\t\t\tthis.variableNames.add(matcher.group(1));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tString variablePattern = match.substring(colonIdx + 1, match.length() - 1);\n\t\t\t\t\t\tpatternBuilder.append('(');\n\t\t\t\t\t\tpatternBuilder.append(variablePattern);\n\t\t\t\t\t\tpatternBuilder.append(')');\n\t\t\t\t\t\tString variableName = match.substring(1, colonIdx);\n\t\t\t\t\t\tthis.variableNames.add(variableName);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tend = matcher.end();\n\t\t\t}\n\t\t\t// No glob pattern was found, this is an exact String match\n\t\t\tif (end == 0) {\n\t\t\t\tthis.exactMatch = true;\n\t\t\t\tthis.pattern = null;\n\t\t\t} else {\n\t\t\t\tthis.exactMatch = false;\n\t\t\t\tpatternBuilder.append(quote(pattern, end, pattern.length()));\n\t\t\t\tthis.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) :\n\t\t\t\t\t\tPattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));\n\t\t\t}\n\t\t}\n\n\t\tprivate String quote(String s, int start, int end) {\n\t\t\tif (start == end) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\treturn Pattern.quote(s.substring(start, end));\n\t\t}\n\n\t\t/**\n\t\t * Main entry point.\n\t\t *\n\t\t * @param str                  Str\n\t\t * @param uriTemplateVariables uri template vars\n\t\t * @return {@code true} if the string matches against the pattern, or {@code false} otherwise.\n\t\t */\n\t\tpublic boolean matchStrings(String str, Map<String, String> uriTemplateVariables) {\n\t\t\tif (this.exactMatch) {\n\t\t\t\treturn this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str);\n\t\t\t} else if (this.pattern != null) {\n\t\t\t\tMatcher matcher = this.pattern.matcher(str);\n\t\t\t\tif (matcher.matches()) {\n\t\t\t\t\tif (uriTemplateVariables != null) {\n\t\t\t\t\t\tif (this.variableNames.size() != matcher.groupCount()) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"The number of capturing groups in the pattern segment \" +\n\t\t\t\t\t\t\t\t\tthis.pattern + \" does not match the number of URI template variables it defines, \" +\n\t\t\t\t\t\t\t\t\t\"which can occur if capturing groups are used in a URI template regex. \" +\n\t\t\t\t\t\t\t\t\t\"Use non-capturing groups instead.\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (int i = 1; i <= matcher.groupCount(); i++) {\n\t\t\t\t\t\t\tString name = this.variableNames.get(i - 1);\n\t\t\t\t\t\t\tif (name.startsWith(\"*\")) {\n\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"Capturing patterns (\" + name + \") are not \" +\n\t\t\t\t\t\t\t\t\t\t\"supported by the AntPathMatcher. Use the PathPatternParser instead.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tString value = matcher.group(i);\n\t\t\t\t\t\t\turiTemplateVariables.put(name, value);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t}\n\n\n\t/**\n\t * The default {@link Comparator} implementation returned by\n\t * {@link #getPatternComparator(String)}.\n\t * <p>In order, the most \"generic\" pattern is determined by the following:\n\t * <ul>\n\t * <li>if it's null or a capture all pattern (i.e. it is equal to \"/**\")</li>\n\t * <li>if the other pattern is an actual match</li>\n\t * <li>if it's a catch-all pattern (i.e. it ends with \"**\"</li>\n\t * <li>if it's got more \"*\" than the other pattern</li>\n\t * <li>if it's got more \"{foo}\" than the other pattern</li>\n\t * <li>if it's shorter than the other pattern</li>\n\t * </ul>\n\t */\n\tprotected static class AntPatternComparator implements Comparator<String> {\n\n\t\tprivate final String path;\n\n\t\tpublic AntPatternComparator(String path) {\n\t\t\tthis.path = path;\n\t\t}\n\n\t\t/**\n\t\t * Compare two patterns to determine which should match first, i.e. which\n\t\t * is the most specific regarding the current path.\n\t\t *\n\t\t * @param pattern1 表达式1\n\t\t * @param pattern2 表达式2\n\t\t * @return a negative integer, zero, or a positive integer as pattern1 is\n\t\t * more specific, equally specific, or less specific than pattern2.\n\t\t */\n\t\t@Override\n\t\tpublic int compare(String pattern1, String pattern2) {\n\t\t\tPatternInfo info1 = new PatternInfo(pattern1);\n\t\t\tPatternInfo info2 = new PatternInfo(pattern2);\n\n\t\t\tif (info1.isLeastSpecific() && info2.isLeastSpecific()) {\n\t\t\t\treturn 0;\n\t\t\t} else if (info1.isLeastSpecific()) {\n\t\t\t\treturn 1;\n\t\t\t} else if (info2.isLeastSpecific()) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tboolean pattern1EqualsPath = pattern1.equals(this.path);\n\t\t\tboolean pattern2EqualsPath = pattern2.equals(this.path);\n\t\t\tif (pattern1EqualsPath && pattern2EqualsPath) {\n\t\t\t\treturn 0;\n\t\t\t} else if (pattern1EqualsPath) {\n\t\t\t\treturn -1;\n\t\t\t} else if (pattern2EqualsPath) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif (info1.isPrefixPattern() && info2.isPrefixPattern()) {\n\t\t\t\treturn info2.getLength() - info1.getLength();\n\t\t\t} else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {\n\t\t\t\treturn 1;\n\t\t\t} else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif (info1.getTotalCount() != info2.getTotalCount()) {\n\t\t\t\treturn info1.getTotalCount() - info2.getTotalCount();\n\t\t\t}\n\n\t\t\tif (info1.getLength() != info2.getLength()) {\n\t\t\t\treturn info2.getLength() - info1.getLength();\n\t\t\t}\n\n\t\t\tif (info1.getSingleWildcards() < info2.getSingleWildcards()) {\n\t\t\t\treturn -1;\n\t\t\t} else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif (info1.getUriVars() < info2.getUriVars()) {\n\t\t\t\treturn -1;\n\t\t\t} else if (info2.getUriVars() < info1.getUriVars()) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t}\n\n\n\t\t/**\n\t\t * Value class that holds information about the pattern, e.g. number of\n\t\t * occurrences of \"*\", \"**\", and \"{\" pattern elements.\n\t\t */\n\t\tprivate static class PatternInfo {\n\n\t\t\tprivate final String pattern;\n\t\t\tprivate int uriVars;\n\t\t\tprivate int singleWildcards;\n\t\t\tprivate int doubleWildcards;\n\t\t\tprivate boolean catchAllPattern;\n\t\t\tprivate boolean prefixPattern;\n\t\t\tprivate Integer length;\n\n\t\t\tpublic PatternInfo(String pattern) {\n\t\t\t\tthis.pattern = pattern;\n\t\t\t\tif (this.pattern != null) {\n\t\t\t\t\tinitCounters();\n\t\t\t\t\tthis.catchAllPattern = this.pattern.equals(\"/**\");\n\t\t\t\t\tthis.prefixPattern = !this.catchAllPattern && this.pattern.endsWith(\"/**\");\n\t\t\t\t}\n\t\t\t\tif (this.uriVars == 0) {\n\t\t\t\t\tthis.length = (this.pattern != null ? this.pattern.length() : 0);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tprotected void initCounters() {\n\t\t\t\tint pos = 0;\n\t\t\t\tif (this.pattern != null) {\n\t\t\t\t\twhile (pos < this.pattern.length()) {\n\t\t\t\t\t\tif (this.pattern.charAt(pos) == '{') {\n\t\t\t\t\t\t\tthis.uriVars++;\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t} else if (this.pattern.charAt(pos) == '*') {\n\t\t\t\t\t\t\tif (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') {\n\t\t\t\t\t\t\t\tthis.doubleWildcards++;\n\t\t\t\t\t\t\t\tpos += 2;\n\t\t\t\t\t\t\t} else if (pos > 0 && !this.pattern.substring(pos - 1).equals(\".*\")) {\n\t\t\t\t\t\t\t\tthis.singleWildcards++;\n\t\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpublic int getUriVars() {\n\t\t\t\treturn this.uriVars;\n\t\t\t}\n\n\t\t\tpublic int getSingleWildcards() {\n\t\t\t\treturn this.singleWildcards;\n\t\t\t}\n\n\t\t\tpublic int getDoubleWildcards() {\n\t\t\t\treturn this.doubleWildcards;\n\t\t\t}\n\n\t\t\tpublic boolean isLeastSpecific() {\n\t\t\t\treturn (this.pattern == null || this.catchAllPattern);\n\t\t\t}\n\n\t\t\tpublic boolean isPrefixPattern() {\n\t\t\t\treturn this.prefixPattern;\n\t\t\t}\n\n\t\t\tpublic int getTotalCount() {\n\t\t\t\treturn this.uriVars + this.singleWildcards + (2 * this.doubleWildcards);\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Returns the length of the given pattern, where template variables are considered to be 1 long.\n\t\t\t *\n\t\t\t * @return 长度\n\t\t\t */\n\t\t\tpublic int getLength() {\n\t\t\t\tif (this.length == null) {\n\t\t\t\t\tthis.length = (this.pattern != null ?\n\t\t\t\t\t\t\tVARIABLE_PATTERN.matcher(this.pattern).replaceAll(\"#\").length() : 0);\n\t\t\t\t}\n\t\t\t\treturn this.length;\n\t\t\t}\n\t\t}\n\t}\n\n\n\t/**\n\t * A simple cache for patterns that depend on the configured path separator.\n\t */\n\tprivate static class PathSeparatorPatternCache {\n\n\t\tprivate final String endsOnWildCard;\n\n\t\tprivate final String endsOnDoubleWildCard;\n\n\t\tpublic PathSeparatorPatternCache(String pathSeparator) {\n\t\t\tthis.endsOnWildCard = pathSeparator + \"*\";\n\t\t\tthis.endsOnDoubleWildCard = pathSeparator + \"**\";\n\t\t}\n\n\t\tpublic String getEndsOnWildCard() {\n\t\t\treturn this.endsOnWildCard;\n\t\t}\n\n\t\tpublic String getEndsOnDoubleWildCard() {\n\t\t\treturn this.endsOnDoubleWildCard;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/CharPool.java",
    "content": "package cn.hutool.core.text;\n\n/**\n * 常用字符常量\n * @see StrPool\n * @author looly\n * @since 5.6.3\n */\npublic interface CharPool {\n\t/**\n\t * 字符常量：空格符 {@code ' '}\n\t */\n\tchar SPACE = ' ';\n\t/**\n\t * 字符常量：制表符 {@code '\\t'}\n\t */\n\tchar TAB = '\t';\n\t/**\n\t * 字符常量：点 {@code '.'}\n\t */\n\tchar DOT = '.';\n\t/**\n\t * 字符常量：斜杠 {@code '/'}\n\t */\n\tchar SLASH = '/';\n\t/**\n\t * 字符常量：反斜杠 {@code '\\\\'}\n\t */\n\tchar BACKSLASH = '\\\\';\n\t/**\n\t * 字符常量：回车符 {@code '\\r'}\n\t */\n\tchar CR = '\\r';\n\t/**\n\t * 字符常量：换行符 {@code '\\n'}\n\t */\n\tchar LF = '\\n';\n\t/**\n\t * 字符常量：减号（连接符） {@code '-'}\n\t */\n\tchar DASHED = '-';\n\t/**\n\t * 字符常量：下划线 {@code '_'}\n\t */\n\tchar UNDERLINE = '_';\n\t/**\n\t * 字符常量：逗号 {@code ','}\n\t */\n\tchar COMMA = ',';\n\t/**\n\t * 字符常量：花括号（左） <code>'{'</code>\n\t */\n\tchar DELIM_START = '{';\n\t/**\n\t * 字符常量：花括号（右） <code>'}'</code>\n\t */\n\tchar DELIM_END = '}';\n\t/**\n\t * 字符常量：中括号（左） {@code '['}\n\t */\n\tchar BRACKET_START = '[';\n\t/**\n\t * 字符常量：中括号（右） {@code ']'}\n\t */\n\tchar BRACKET_END = ']';\n\t/**\n\t * 字符常量：双引号 {@code '\"'}\n\t */\n\tchar DOUBLE_QUOTES = '\"';\n\t/**\n\t * 字符常量：单引号 {@code '\\''}\n\t */\n\tchar SINGLE_QUOTE = '\\'';\n\t/**\n\t * 字符常量：与 {@code '&'}\n\t */\n\tchar AMP = '&';\n\t/**\n\t * 字符常量：冒号 {@code ':'}\n\t */\n\tchar COLON = ':';\n\t/**\n\t * 字符常量：艾特 {@code '@'}\n\t */\n\tchar AT = '@';\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/CharSequenceUtil.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.comparator.VersionComparator;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.Matcher;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.text.finder.CharFinder;\nimport cn.hutool.core.text.finder.Finder;\nimport cn.hutool.core.text.finder.StrFinder;\nimport cn.hutool.core.util.*;\n\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\nimport java.text.MessageFormat;\nimport java.text.Normalizer;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * {@link CharSequence} 相关工具类封装\n *\n * @author looly\n * @since 5.5.3\n */\npublic class CharSequenceUtil {\n\n\t/**\n\t * 索引值：{@code -1}\n\t */\n\tpublic static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND;\n\n\t/**\n\t * 字符串常量：{@code \"null\"} <br>\n\t * 注意：{@code \"null\" != null}\n\t */\n\tpublic static final String NULL = \"null\";\n\n\t/**\n\t * 字符串常量：空字符串 {@code \"\"}\n\t */\n\tpublic static final String EMPTY = \"\";\n\n\t/**\n\t * 字符串常量：空格符 {@code \" \"}\n\t */\n\tpublic static final String SPACE = \" \";\n\n\t/**\n\t * <p>字符串是否为空白，空白的定义如下：</p>\n\t * <ol>\n\t *     <li>{@code null}</li>\n\t *     <li>空字符串：{@code \"\"}</li>\n\t *     <li>空格、全角空格、制表符、换行符，等不可见字符</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isBlank(null)     // true}</li>\n\t *     <li>{@code CharSequenceUtil.isBlank(\"\")       // true}</li>\n\t *     <li>{@code CharSequenceUtil.isBlank(\" \\t\\n\")  // true}</li>\n\t *     <li>{@code CharSequenceUtil.isBlank(\"abc\")    // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isEmpty(CharSequence)} 的区别是：\n\t * 该方法会校验空白字符，且性能相对于 {@link #isEmpty(CharSequence)} 略慢。</p>\n\t * <br>\n\t *\n\t * <p>建议：</p>\n\t * <ul>\n\t *     <li>该方法建议仅对于客户端（或第三方接口）传入的参数使用该方法。</li>\n\t *     <li>需要同时校验多个字符串时，建议采用 {@link #hasBlank(CharSequence...)} 或 {@link #isAllBlank(CharSequence...)}</li>\n\t * </ul>\n\t *\n\t * @param str 被检测的字符串\n\t * @return 若为空白，则返回 true\n\t * @see #isEmpty(CharSequence)\n\t */\n\tpublic static boolean isBlank(CharSequence str) {\n\t\tfinal int length;\n\t\tif ((str == null) || ((length = str.length()) == 0)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\t// 只要有一个非空字符即为非空字符串\n\t\t\tif (false == CharUtil.isBlankChar(str.charAt(i))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * <p>字符串是否为非空白，非空白的定义如下： </p>\n\t * <ol>\n\t *     <li>不为 {@code null}</li>\n\t *     <li>不为空字符串：{@code \"\"}</li>\n\t *     <li>不为空格、全角空格、制表符、换行符，等不可见字符</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isNotBlank(null)     // false}</li>\n\t *     <li>{@code CharSequenceUtil.isNotBlank(\"\")       // false}</li>\n\t *     <li>{@code CharSequenceUtil.isNotBlank(\" \\t\\n\")  // false}</li>\n\t *     <li>{@code CharSequenceUtil.isNotBlank(\"abc\")    // true}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isNotEmpty(CharSequence)} 的区别是：\n\t * 该方法会校验空白字符，且性能相对于 {@link #isNotEmpty(CharSequence)} 略慢。</p>\n\t * <p>建议：仅对于客户端（或第三方接口）传入的参数使用该方法。</p>\n\t *\n\t * @param str 被检测的字符串\n\t * @return 是否为非空\n\t * @see #isBlank(CharSequence)\n\t */\n\tpublic static boolean isNotBlank(CharSequence str) {\n\t\treturn !isBlank(str);\n\t}\n\n\t/**\n\t * <p>指定字符串数组中，是否包含空字符串。</p>\n\t * <p>如果指定的字符串数组的长度为 0，或者其中的任意一个元素是空字符串，则返回 true。</p>\n\t * <br>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.hasBlank()                  // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasBlank(\"\", null, \" \")     // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasBlank(\"123\", \" \")        // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasBlank(\"123\", \"abc\")      // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isAllBlank(CharSequence...)} 的区别在于：</p>\n\t * <ul>\n\t *     <li>hasBlank(CharSequence...)            等价于 {@code isBlank(...) || isBlank(...) || ...}</li>\n\t *     <li>{@link #isAllBlank(CharSequence...)} 等价于 {@code isBlank(...) && isBlank(...) && ...}</li>\n\t * </ul>\n\t *\n\t * @param strs 字符串列表\n\t * @return 是否包含空字符串\n\t */\n\tpublic static boolean hasBlank(CharSequence... strs) {\n\t\tif (ArrayUtil.isEmpty(strs)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (CharSequence str : strs) {\n\t\t\tif (isBlank(str)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * <p>指定字符串数组中的元素，是否全部为空字符串。</p>\n\t * <p>如果指定的字符串数组的长度为 0，或者所有元素都是空字符串，则返回 true。</p>\n\t * <br>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isAllBlank()                  // true}</li>\n\t *     <li>{@code CharSequenceUtil.isAllBlank(\"\", null, \" \")     // true}</li>\n\t *     <li>{@code CharSequenceUtil.isAllBlank(\"123\", \" \")        // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllBlank(\"123\", \"abc\")      // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #hasBlank(CharSequence...)} 的区别在于：</p>\n\t * <ul>\n\t *     <li>{@link #hasBlank(CharSequence...)}   等价于 {@code isBlank(...) || isBlank(...) || ...}</li>\n\t *     <li>isAllBlank(CharSequence...)          等价于 {@code isBlank(...) && isBlank(...) && ...}</li>\n\t * </ul>\n\t *\n\t * @param strs 字符串列表\n\t * @return 所有字符串是否为空白\n\t */\n\tpublic static boolean isAllBlank(CharSequence... strs) {\n\t\tif (ArrayUtil.isEmpty(strs)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (CharSequence str : strs) {\n\t\t\tif (isNotBlank(str)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * <p>字符串是否为空，空的定义如下：</p>\n\t * <ol>\n\t *     <li>{@code null}</li>\n\t *     <li>空字符串：{@code \"\"}</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isEmpty(null)     // true}</li>\n\t *     <li>{@code CharSequenceUtil.isEmpty(\"\")       // true}</li>\n\t *     <li>{@code CharSequenceUtil.isEmpty(\" \\t\\n\")  // false}</li>\n\t *     <li>{@code CharSequenceUtil.isEmpty(\"abc\")    // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isBlank(CharSequence)} 的区别是：该方法不校验空白字符。</p>\n\t * <p>建议：</p>\n\t * <ul>\n\t *     <li>该方法建议用于工具类或任何可以预期的方法参数的校验中。</li>\n\t *     <li>需要同时校验多个字符串时，建议采用 {@link #hasEmpty(CharSequence...)} 或 {@link #isAllEmpty(CharSequence...)}</li>\n\t * </ul>\n\t *\n\t * @param str 被检测的字符串\n\t * @return 是否为空\n\t * @see #isBlank(CharSequence)\n\t */\n\tpublic static boolean isEmpty(CharSequence str) {\n\t\treturn str == null || str.length() == 0;\n\t}\n\n\t/**\n\t * <p>字符串是否为非空白，非空白的定义如下： </p>\n\t * <ol>\n\t *     <li>不为 {@code null}</li>\n\t *     <li>不为空字符串：{@code \"\"}</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isNotEmpty(null)     // false}</li>\n\t *     <li>{@code CharSequenceUtil.isNotEmpty(\"\")       // false}</li>\n\t *     <li>{@code CharSequenceUtil.isNotEmpty(\" \\t\\n\")  // true}</li>\n\t *     <li>{@code CharSequenceUtil.isNotEmpty(\"abc\")    // true}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isNotBlank(CharSequence)} 的区别是：该方法不校验空白字符。</p>\n\t * <p>建议：该方法建议用于工具类或任何可以预期的方法参数的校验中。</p>\n\t *\n\t * @param str 被检测的字符串\n\t * @return 是否为非空\n\t * @see #isEmpty(CharSequence)\n\t */\n\tpublic static boolean isNotEmpty(CharSequence str) {\n\t\treturn !isEmpty(str);\n\t}\n\n\t/**\n\t * 当给定字符串为null时，转换为Empty\n\t *\n\t * @param str 被检查的字符串\n\t * @return 原字符串或者空串\n\t * @see #nullToEmpty(CharSequence)\n\t * @since 4.6.3\n\t */\n\tpublic static String emptyIfNull(CharSequence str) {\n\t\treturn nullToEmpty(str);\n\t}\n\n\t/**\n\t * 当给定字符串为null时，转换为Empty\n\t *\n\t * @param str 被转换的字符串\n\t * @return 转换后的字符串\n\t */\n\tpublic static String nullToEmpty(CharSequence str) {\n\t\treturn nullToDefault(str, EMPTY);\n\t}\n\n\t/**\n\t * 如果字符串是 {@code null}，则返回指定默认字符串，否则返回字符串本身。\n\t *\n\t * <pre>\n\t * nullToDefault(null, &quot;default&quot;)  = &quot;default&quot;\n\t * nullToDefault(&quot;&quot;, &quot;default&quot;)    = &quot;&quot;\n\t * nullToDefault(&quot;  &quot;, &quot;default&quot;)  = &quot;  &quot;\n\t * nullToDefault(&quot;bat&quot;, &quot;default&quot;) = &quot;bat&quot;\n\t * </pre>\n\t *\n\t * @param str        要转换的字符串\n\t * @param defaultStr 默认字符串\n\t * @return 字符串本身或指定的默认字符串\n\t */\n\tpublic static String nullToDefault(CharSequence str, String defaultStr) {\n\t\treturn (str == null) ? defaultStr : str.toString();\n\t}\n\n\t/**\n\t * 如果字符串是{@code null}或者&quot;&quot;，则返回指定默认字符串，否则返回字符串本身。\n\t *\n\t * <pre>\n\t * emptyToDefault(null, &quot;default&quot;)  = &quot;default&quot;\n\t * emptyToDefault(&quot;&quot;, &quot;default&quot;)    = &quot;default&quot;\n\t * emptyToDefault(&quot;  &quot;, &quot;default&quot;)  = &quot;  &quot;\n\t * emptyToDefault(&quot;bat&quot;, &quot;default&quot;) = &quot;bat&quot;\n\t * </pre>\n\t *\n\t * @param str        要转换的字符串\n\t * @param defaultStr 默认字符串\n\t * @return 字符串本身或指定的默认字符串\n\t * @since 4.1.0\n\t */\n\tpublic static String emptyToDefault(CharSequence str, String defaultStr) {\n\t\treturn isEmpty(str) ? defaultStr : str.toString();\n\t}\n\n\t/**\n\t * 如果字符串是{@code null}或者&quot;&quot;或者空白，则返回指定默认字符串，否则返回字符串本身。\n\t *\n\t * <pre>\n\t * blankToDefault(null, &quot;default&quot;)  = &quot;default&quot;\n\t * blankToDefault(&quot;&quot;, &quot;default&quot;)    = &quot;default&quot;\n\t * blankToDefault(&quot;  &quot;, &quot;default&quot;)  = &quot;default&quot;\n\t * blankToDefault(&quot;bat&quot;, &quot;default&quot;) = &quot;bat&quot;\n\t * </pre>\n\t *\n\t * @param str        要转换的字符串\n\t * @param defaultStr 默认字符串\n\t * @return 字符串本身或指定的默认字符串\n\t * @since 4.1.0\n\t */\n\tpublic static String blankToDefault(CharSequence str, String defaultStr) {\n\t\treturn isBlank(str) ? defaultStr : str.toString();\n\t}\n\n\t/**\n\t * 当给定字符串为空字符串时，转换为{@code null}\n\t *\n\t * @param str 被转换的字符串\n\t * @return 转换后的字符串\n\t */\n\tpublic static String emptyToNull(CharSequence str) {\n\t\treturn isEmpty(str) ? null : str.toString();\n\t}\n\n\t/**\n\t * <p>是否包含空字符串。</p>\n\t * <p>如果指定的字符串数组的长度为 0，或者其中的任意一个元素是空字符串，则返回 true。</p>\n\t * <br>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.hasEmpty()                  // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasEmpty(\"\", null)          // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasEmpty(\"123\", \"\")         // true}</li>\n\t *     <li>{@code CharSequenceUtil.hasEmpty(\"123\", \"abc\")      // false}</li>\n\t *     <li>{@code CharSequenceUtil.hasEmpty(\" \", \"\\t\", \"\\n\")   // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于：</p>\n\t * <ul>\n\t *     <li>hasEmpty(CharSequence...)            等价于 {@code isEmpty(...) || isEmpty(...) || ...}</li>\n\t *     <li>{@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}</li>\n\t * </ul>\n\t *\n\t * @param strs 字符串列表\n\t * @return 是否包含空字符串\n\t */\n\tpublic static boolean hasEmpty(CharSequence... strs) {\n\t\tif (ArrayUtil.isEmpty(strs)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (CharSequence str : strs) {\n\t\t\tif (isEmpty(str)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * <p>指定字符串数组中的元素，是否全部为空字符串。</p>\n\t * <p>如果指定的字符串数组的长度为 0，或者所有元素都是空字符串，则返回 true。</p>\n\t * <br>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isAllEmpty()                  // true}</li>\n\t *     <li>{@code CharSequenceUtil.isAllEmpty(\"\", null)          // true}</li>\n\t *     <li>{@code CharSequenceUtil.isAllEmpty(\"123\", \"\")         // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllEmpty(\"123\", \"abc\")      // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllEmpty(\" \", \"\\t\", \"\\n\")   // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #hasEmpty(CharSequence...)} 的区别在于：</p>\n\t * <ul>\n\t *     <li>{@link #hasEmpty(CharSequence...)}   等价于 {@code isEmpty(...) || isEmpty(...) || ...}</li>\n\t *     <li>isAllEmpty(CharSequence...)          等价于 {@code isEmpty(...) && isEmpty(...) && ...}</li>\n\t * </ul>\n\t *\n\t * @param strs 字符串列表\n\t * @return 所有字符串是否为空白\n\t */\n\tpublic static boolean isAllEmpty(CharSequence... strs) {\n\t\tif (ArrayUtil.isEmpty(strs)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (CharSequence str : strs) {\n\t\t\tif (isNotEmpty(str)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * <p>指定字符串数组中的元素，是否都不为空字符串。</p>\n\t * <p>如果指定的字符串数组的长度不为 0，或者所有元素都不是空字符串，则返回 true。</p>\n\t * <br>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code CharSequenceUtil.isAllNotEmpty()                  // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllNotEmpty(\"\", null)          // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllNotEmpty(\"123\", \"\")         // false}</li>\n\t *     <li>{@code CharSequenceUtil.isAllNotEmpty(\"123\", \"abc\")      // true}</li>\n\t *     <li>{@code CharSequenceUtil.isAllNotEmpty(\" \", \"\\t\", \"\\n\")   // true}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于：</p>\n\t * <ul>\n\t *     <li>{@link #isAllEmpty(CharSequence...)}    等价于 {@code isEmpty(...) && isEmpty(...) && ...}</li>\n\t *     <li>isAllNotEmpty(CharSequence...)          等价于 {@code !isEmpty(...) && !isEmpty(...) && ...}</li>\n\t * </ul>\n\t *\n\t * @param args 字符串数组\n\t * @return 所有字符串是否都不为为空白\n\t * @since 5.3.6\n\t */\n\tpublic static boolean isAllNotEmpty(CharSequence... args) {\n\t\treturn false == hasEmpty(args);\n\t}\n\n\t/**\n\t * 是否全都不为{@code null}或空对象或空白符的对象，通过{@link #hasBlank(CharSequence...)} 判断元素\n\t *\n\t * @param args 被检查的对象,一个或者多个\n\t * @return 是否都不为空\n\t * @since 5.3.6\n\t */\n\tpublic static boolean isAllNotBlank(CharSequence... args) {\n\t\treturn false == hasBlank(args);\n\t}\n\n\t/**\n\t * 检查字符串是否为null、“null”、“undefined”\n\t *\n\t * @param str 被检查的字符串\n\t * @return 是否为null、“null”、“undefined”\n\t * @since 4.0.10\n\t */\n\tpublic static boolean isNullOrUndefined(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn true;\n\t\t}\n\t\treturn isNullOrUndefinedStr(str);\n\t}\n\n\t/**\n\t * 检查字符串是否为null、“”、“null”、“undefined”\n\t *\n\t * @param str 被检查的字符串\n\t * @return 是否为null、“”、“null”、“undefined”\n\t * @since 4.0.10\n\t */\n\tpublic static boolean isEmptyOrUndefined(CharSequence str) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn isNullOrUndefinedStr(str);\n\t}\n\n\t/**\n\t * 检查字符串是否为null、空白串、“null”、“undefined”\n\t *\n\t * @param str 被检查的字符串\n\t * @return 是否为null、空白串、“null”、“undefined”\n\t * @since 4.0.10\n\t */\n\tpublic static boolean isBlankOrUndefined(CharSequence str) {\n\t\tif (isBlank(str)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn isNullOrUndefinedStr(str);\n\t}\n\n\t/**\n\t * 是否为“null”、“undefined”，不做空指针检查\n\t *\n\t * @param str 字符串\n\t * @return 是否为“null”、“undefined”\n\t */\n\tprivate static boolean isNullOrUndefinedStr(CharSequence str) {\n\t\tString strString = str.toString().trim();\n\t\treturn NULL.equals(strString) || \"undefined\".equals(strString);\n\t}\n\n\t// ------------------------------------------------------------------------ Trim\n\n\t/**\n\t * 除去字符串头尾部的空白，如果字符串是{@code null}，依然返回{@code null}。\n\t *\n\t * <p>\n\t * 注意，和{@link String#trim()}不同，此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白， 因而可以除去英文字符集之外的其它空白，如中文空格。\n\t *\n\t * <pre>\n\t * trim(null)          = null\n\t * trim(&quot;&quot;)            = &quot;&quot;\n\t * trim(&quot;     &quot;)       = &quot;&quot;\n\t * trim(&quot;abc&quot;)         = &quot;abc&quot;\n\t * trim(&quot;    abc    &quot;) = &quot;abc&quot;\n\t * </pre>\n\t *\n\t * @param str 要处理的字符串\n\t * @return 除去头尾空白的字符串，如果原字串为{@code null}，则返回{@code null}\n\t */\n\tpublic static String trim(CharSequence str) {\n\t\treturn (null == str) ? null : trim(str, 0);\n\t}\n\n\t/**\n\t * 除去字符串头尾部的空白，如果字符串是{@code null}，返回{@code \"\"}。\n\t *\n\t * <pre>\n\t * CharSequenceUtil.trimToEmpty(null)          = \"\"\n\t * CharSequenceUtil.trimToEmpty(\"\")            = \"\"\n\t * CharSequenceUtil.trimToEmpty(\"     \")       = \"\"\n\t * CharSequenceUtil.trimToEmpty(\"abc\")         = \"abc\"\n\t * CharSequenceUtil.trimToEmpty(\"    abc    \") = \"abc\"\n\t * </pre>\n\t *\n\t * @param str 字符串\n\t * @return 去除两边空白符后的字符串, 如果为null返回\"\"\n\t * @since 3.1.1\n\t */\n\tpublic static String trimToEmpty(CharSequence str) {\n\t\treturn str == null ? EMPTY : trim(str);\n\t}\n\n\t/**\n\t * 除去字符串头尾部的空白，如果字符串是{@code null}或者\"\"，返回{@code null}。\n\t *\n\t * <pre>\n\t * CharSequenceUtil.trimToNull(null)          = null\n\t * CharSequenceUtil.trimToNull(\"\")            = null\n\t * CharSequenceUtil.trimToNull(\"     \")       = null\n\t * CharSequenceUtil.trimToNull(\"abc\")         = \"abc\"\n\t * CharSequenceUtil.trimToEmpty(\"    abc    \") = \"abc\"\n\t * </pre>\n\t *\n\t * @param str 字符串\n\t * @return 去除两边空白符后的字符串, 如果为空返回null\n\t * @since 3.2.1\n\t */\n\tpublic static String trimToNull(CharSequence str) {\n\t\tfinal String trimStr = trim(str);\n\t\treturn EMPTY.equals(trimStr) ? null : trimStr;\n\t}\n\n\t/**\n\t * 除去字符串头部的空白，如果字符串是{@code null}，则返回{@code null}。\n\t *\n\t * <p>\n\t * 注意，和{@link String#trim()}不同，此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白， 因而可以除去英文字符集之外的其它空白，如中文空格。\n\t *\n\t * <pre>\n\t * trimStart(null)         = null\n\t * trimStart(&quot;&quot;)           = &quot;&quot;\n\t * trimStart(&quot;abc&quot;)        = &quot;abc&quot;\n\t * trimStart(&quot;  abc&quot;)      = &quot;abc&quot;\n\t * trimStart(&quot;abc  &quot;)      = &quot;abc  &quot;\n\t * trimStart(&quot; abc &quot;)      = &quot;abc &quot;\n\t * </pre>\n\t *\n\t * @param str 要处理的字符串\n\t * @return 除去空白的字符串，如果原字串为{@code null}或结果字符串为{@code \"\"}，则返回 {@code null}\n\t */\n\tpublic static String trimStart(CharSequence str) {\n\t\treturn trim(str, -1);\n\t}\n\n\t/**\n\t * 除去字符串尾部的空白，如果字符串是{@code null}，则返回{@code null}。\n\t *\n\t * <p>\n\t * 注意，和{@link String#trim()}不同，此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白， 因而可以除去英文字符集之外的其它空白，如中文空格。\n\t *\n\t * <pre>\n\t * trimEnd(null)       = null\n\t * trimEnd(&quot;&quot;)         = &quot;&quot;\n\t * trimEnd(&quot;abc&quot;)      = &quot;abc&quot;\n\t * trimEnd(&quot;  abc&quot;)    = &quot;  abc&quot;\n\t * trimEnd(&quot;abc  &quot;)    = &quot;abc&quot;\n\t * trimEnd(&quot; abc &quot;)    = &quot; abc&quot;\n\t * </pre>\n\t *\n\t * @param str 要处理的字符串\n\t * @return 除去空白的字符串，如果原字串为{@code null}或结果字符串为{@code \"\"}，则返回 {@code null}\n\t */\n\tpublic static String trimEnd(CharSequence str) {\n\t\treturn trim(str, 1);\n\t}\n\n\t/**\n\t * 除去字符串头尾部的空白符，如果字符串是{@code null}，依然返回{@code null}。\n\t *\n\t * @param str  要处理的字符串\n\t * @param mode {@code -1}表示trimStart，{@code 0}表示trim全部， {@code 1}表示trimEnd\n\t * @return 除去指定字符后的的字符串，如果原字串为{@code null}，则返回{@code null}\n\t */\n\tpublic static String trim(CharSequence str, int mode) {\n\t\treturn trim(str, mode, CharUtil::isBlankChar);\n\t}\n\n\t/**\n\t * 按照断言，除去字符串头尾部的断言为真的字符，如果字符串是{@code null}，依然返回{@code null}。\n\t *\n\t * @param str       要处理的字符串\n\t * @param mode      {@code -1}表示trimStart，{@code 0}表示trim全部， {@code 1}表示trimEnd\n\t * @param predicate 断言是否过掉字符，返回{@code true}表述过滤掉，{@code false}表示不过滤\n\t * @return 除去指定字符后的的字符串，如果原字串为{@code null}，则返回{@code null}\n\t * @since 5.7.4\n\t */\n\tpublic static String trim(CharSequence str, int mode, Predicate<Character> predicate) {\n\t\tString result;\n\t\tif (str == null) {\n\t\t\tresult = null;\n\t\t} else {\n\t\t\tint length = str.length();\n\t\t\tint start = 0;\n\t\t\tint end = length;// 扫描字符串头部\n\t\t\tif (mode <= 0) {\n\t\t\t\twhile ((start < end) && (predicate.test(str.charAt(start)))) {\n\t\t\t\t\tstart++;\n\t\t\t\t}\n\t\t\t}// 扫描字符串尾部\n\t\t\tif (mode >= 0) {\n\t\t\t\twhile ((start < end) && (predicate.test(str.charAt(end - 1)))) {\n\t\t\t\t\tend--;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ((start > 0) || (end < length)) {\n\t\t\t\tresult = str.toString().substring(start, end);\n\t\t\t} else {\n\t\t\t\tresult = str.toString();\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// ------------------------------------------------------------------------ startWith\n\n\t/**\n\t * 字符串是否以给定字符开始\n\t *\n\t * @param str 字符串\n\t * @param c   字符\n\t * @return 是否开始\n\t */\n\tpublic static boolean startWith(CharSequence str, char c) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn c == str.charAt(0);\n\t}\n\n\t/**\n\t * 是否以指定字符串开头<br>\n\t * 如果给定的字符串和开头字符串都为null则返回true，否则任意一个值为null返回false\n\t *\n\t * @param str        被监测字符串\n\t * @param prefix     开头字符串\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 是否以指定字符串开头\n\t * @since 5.4.3\n\t */\n\tpublic static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) {\n\t\treturn startWith(str, prefix, ignoreCase, false);\n\t}\n\n\t/**\n\t * 是否以指定字符串开头<br>\n\t * 如果给定的字符串和开头字符串都为null则返回true，否则任意一个值为null返回false<br>\n\t * <pre>\n\t *     CharSequenceUtil.startWith(\"123\", \"123\", false, true);   -- false\n\t *     CharSequenceUtil.startWith(\"ABCDEF\", \"abc\", true, true); -- true\n\t *     CharSequenceUtil.startWith(\"abc\", \"abc\", true, true);    -- false\n\t * </pre>\n\t *\n\t * @param str          被监测字符串\n\t * @param prefix       开头字符串\n\t * @param ignoreCase   是否忽略大小写\n\t * @param ignoreEquals 是否忽略字符串相等的情况\n\t * @return 是否以指定字符串开头\n\t * @since 5.4.3\n\t */\n\tpublic static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) {\n\t\tif (null == str || null == prefix) {\n\t\t\tif (ignoreEquals) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn null == str && null == prefix;\n\t\t}\n\n\t\tboolean isStartWith = str.toString()\n\t\t\t.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());\n\n\t\tif (isStartWith) {\n\t\t\treturn (false == ignoreEquals) || (false == equals(str, prefix, ignoreCase));\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否以指定字符串开头\n\t *\n\t * @param str    被监测字符串\n\t * @param prefix 开头字符串\n\t * @return 是否以指定字符串开头\n\t */\n\tpublic static boolean startWith(CharSequence str, CharSequence prefix) {\n\t\treturn startWith(str, prefix, false);\n\t}\n\n\t/**\n\t * 是否以指定字符串开头，忽略相等字符串的情况\n\t *\n\t * @param str    被监测字符串\n\t * @param prefix 开头字符串\n\t * @return 是否以指定字符串开头并且两个字符串不相等\n\t */\n\tpublic static boolean startWithIgnoreEquals(CharSequence str, CharSequence prefix) {\n\t\treturn startWith(str, prefix, false, true);\n\t}\n\n\t/**\n\t * 是否以指定字符串开头，忽略大小写\n\t *\n\t * @param str    被监测字符串\n\t * @param prefix 开头字符串\n\t * @return 是否以指定字符串开头\n\t */\n\tpublic static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) {\n\t\treturn startWith(str, prefix, true);\n\t}\n\n\t/**\n\t * 给定字符串是否以任何一个字符串开始<br>\n\t * 给定字符串和数组为空都返回false\n\t *\n\t * @param str      给定字符串\n\t * @param prefixes 需要检测的开始字符串\n\t * @return 给定字符串是否以任何一个字符串开始\n\t * @since 3.0.6\n\t */\n\tpublic static boolean startWithAny(CharSequence str, CharSequence... prefixes) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (CharSequence prefix : prefixes) {\n\t\t\tif (startWith(str, prefix, false)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 给定字符串是否以任何一个字符串开始（忽略大小写）<br>\n\t * 给定字符串和数组为空都返回false\n\t *\n\t * @param str      给定字符串\n\t * @param prefixes 需要检测的开始字符串\n\t * @return 给定字符串是否以任何一个字符串开始\n\t * @since 5.8.1\n\t */\n\tpublic static boolean startWithAnyIgnoreCase(final CharSequence str, final CharSequence... prefixes) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (final CharSequence prefix : prefixes) {\n\t\t\tif (startWith(str, prefix, true)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t// ------------------------------------------------------------------------ endWith\n\n\t/**\n\t * 字符串是否以给定字符结尾\n\t *\n\t * @param str 字符串\n\t * @param c   字符\n\t * @return 是否结尾\n\t */\n\tpublic static boolean endWith(CharSequence str, char c) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn c == str.charAt(str.length() - 1);\n\t}\n\n\t/**\n\t * 是否以指定字符串结尾<br>\n\t * 如果给定的字符串和开头字符串都为null则返回true，否则任意一个值为null返回false\n\t *\n\t * @param str        被监测字符串\n\t * @param suffix     结尾字符串\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 是否以指定字符串结尾\n\t */\n\tpublic static boolean endWith(CharSequence str, CharSequence suffix, boolean ignoreCase) {\n\t\treturn endWith(str, suffix, ignoreCase, false);\n\t}\n\n\t/**\n\t * 是否以指定字符串结尾<br>\n\t * 如果给定的字符串和开头字符串都为null则返回true，否则任意一个值为null返回false\n\t *\n\t * @param str          被监测字符串\n\t * @param suffix       结尾字符串\n\t * @param ignoreCase   是否忽略大小写\n\t * @param ignoreEquals 是否忽略字符串相等的情况\n\t * @return 是否以指定字符串结尾\n\t * @since 5.8.0\n\t */\n\tpublic static boolean endWith(CharSequence str, CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) {\n\t\tif (null == str || null == suffix) {\n\t\t\tif (ignoreEquals) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn null == str && null == suffix;\n\t\t}\n\n\t\tfinal int strOffset = str.length() - suffix.length();\n\t\tboolean isEndWith = str.toString()\n\t\t\t.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());\n\n\t\tif (isEndWith) {\n\t\t\treturn (false == ignoreEquals) || (false == equals(str, suffix, ignoreCase));\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否以指定字符串结尾\n\t *\n\t * @param str    被监测字符串\n\t * @param suffix 结尾字符串\n\t * @return 是否以指定字符串结尾\n\t */\n\tpublic static boolean endWith(CharSequence str, CharSequence suffix) {\n\t\treturn endWith(str, suffix, false);\n\t}\n\n\t/**\n\t * 是否以指定字符串结尾，忽略大小写\n\t *\n\t * @param str    被监测字符串\n\t * @param suffix 结尾字符串\n\t * @return 是否以指定字符串结尾\n\t */\n\tpublic static boolean endWithIgnoreCase(CharSequence str, CharSequence suffix) {\n\t\treturn endWith(str, suffix, true);\n\t}\n\n\t/**\n\t * 给定字符串是否以任何一个字符串结尾<br>\n\t * 给定字符串和数组为空都返回false\n\t *\n\t * @param str      给定字符串\n\t * @param suffixes 需要检测的结尾字符串\n\t * @return 给定字符串是否以任何一个字符串结尾\n\t * @since 3.0.6\n\t */\n\tpublic static boolean endWithAny(CharSequence str, CharSequence... suffixes) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (CharSequence suffix : suffixes) {\n\t\t\tif (endWith(str, suffix, false)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 给定字符串是否以任何一个字符串结尾（忽略大小写）<br>\n\t * 给定字符串和数组为空都返回false\n\t *\n\t * @param str      给定字符串\n\t * @param suffixes 需要检测的结尾字符串\n\t * @return 给定字符串是否以任何一个字符串结尾\n\t * @since 5.5.9\n\t */\n\tpublic static boolean endWithAnyIgnoreCase(CharSequence str, CharSequence... suffixes) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (CharSequence suffix : suffixes) {\n\t\t\tif (endWith(str, suffix, true)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t// ------------------------------------------------------------------------ contains\n\n\t/**\n\t * 指定字符是否在字符串中出现过\n\t *\n\t * @param str        字符串\n\t * @param searchChar 被查找的字符\n\t * @return 是否包含\n\t * @since 3.1.2\n\t */\n\tpublic static boolean contains(CharSequence str, char searchChar) {\n\t\treturn indexOf(str, searchChar) > -1;\n\t}\n\n\t/**\n\t * 指定字符串是否在字符串中出现过\n\t *\n\t * @param str       字符串\n\t * @param searchStr 被查找的字符串\n\t * @return 是否包含\n\t * @since 5.1.1\n\t */\n\tpublic static boolean contains(CharSequence str, CharSequence searchStr) {\n\t\tif (null == str || null == searchStr) {\n\t\t\treturn false;\n\t\t}\n\t\treturn str.toString().contains(searchStr);\n\t}\n\n\t/**\n\t * 查找指定字符串是否包含指定字符串列表中的任意一个字符串\n\t *\n\t * @param str      指定字符串\n\t * @param testStrs 需要检查的字符串数组\n\t * @return 是否包含任意一个字符串\n\t * @since 3.2.0\n\t */\n\tpublic static boolean containsAny(CharSequence str, CharSequence... testStrs) {\n\t\treturn null != getContainsStr(str, testStrs);\n\t}\n\n\t/**\n\t * 查找指定字符串是否包含指定字符列表中的任意一个字符\n\t *\n\t * @param str       指定字符串\n\t * @param testChars 需要检查的字符数组\n\t * @return 是否包含任意一个字符\n\t * @since 4.1.11\n\t */\n\tpublic static boolean containsAny(CharSequence str, char... testChars) {\n\t\tif (false == isEmpty(str)) {\n\t\t\tint len = str.length();\n\t\t\tfor (int i = 0; i < len; i++) {\n\t\t\t\tif (ArrayUtil.contains(testChars, str.charAt(i))) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 检查指定字符串中是否只包含给定的字符<br>\n\t * 这里的containsOnly并不是必须全部给定的testChars都需要有，而是一个子集。testChars是个限定集合，检查字符串中的字符是否在这个限定集合中。<br>\n\t * <pre>{@code\n\t *   StrUtil.containsOnly(\"asdas\", 'a', 'd', 's','l');   --> true\n\t * }</pre>\n\t *\n\t * @param str       字符串\n\t * @param testChars 检查的字符\n\t * @return 字符串含有非检查的字符，返回false\n\t * @since 4.4.1\n\t */\n\tpublic static boolean containsOnly(CharSequence str, char... testChars) {\n\t\tif (false == isEmpty(str)) {\n\t\t\tint len = str.length();\n\t\t\tfor (int i = 0; i < len; i++) {\n\t\t\t\tif (false == ArrayUtil.contains(testChars, str.charAt(i))) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查指定字符串中是否含给定的所有字符串\n\t *\n\t * @param str       字符串\n\t * @param testChars 检查的字符\n\t * @return 字符串含有非检查的字符，返回false\n\t * @since 4.4.1\n\t */\n\tpublic static boolean containsAll(CharSequence str, CharSequence... testChars) {\n\t\tif (isBlank(str) || ArrayUtil.isEmpty(testChars)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (CharSequence testChar : testChars) {\n\t\t\tif (false == contains(str, testChar)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 给定字符串是否包含空白符（空白符包括空格、制表符、全角空格和不间断空格）<br>\n\t * 如果给定字符串为null或者\"\"，则返回false\n\t *\n\t * @param str 字符串\n\t * @return 是否包含空白符\n\t * @since 4.0.8\n\t */\n\tpublic static boolean containsBlank(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal int length = str.length();\n\t\tif (0 == length) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < length; i += 1) {\n\t\t\tif (CharUtil.isBlankChar(str.charAt(i))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 查找指定字符串是否包含指定字符串列表中的任意一个字符串，如果包含返回找到的第一个字符串\n\t *\n\t * @param str      指定字符串\n\t * @param testStrs 需要检查的字符串数组\n\t * @return 被包含的第一个字符串\n\t * @since 3.2.0\n\t */\n\tpublic static String getContainsStr(CharSequence str, CharSequence... testStrs) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) {\n\t\t\treturn null;\n\t\t}\n\t\tfor (CharSequence checkStr : testStrs) {\n\t\t\tif (null != checkStr && str.toString().contains(checkStr)) {\n\t\t\t\treturn checkStr.toString();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 是否包含特定字符，忽略大小写，如果给定两个参数都为{@code null}，返回true\n\t *\n\t * @param str     被检测字符串\n\t * @param testStr 被测试是否包含的字符串\n\t * @return 是否包含\n\t */\n\tpublic static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) {\n\t\tif (null == str) {\n\t\t\t// 如果被监测字符串和\n\t\t\treturn null == testStr;\n\t\t}\n\t\treturn indexOfIgnoreCase(str, testStr) > -1;\n\t}\n\n\t/**\n\t * 查找指定字符串是否包含指定字符串列表中的任意一个字符串<br>\n\t * 忽略大小写\n\t *\n\t * @param str      指定字符串\n\t * @param testStrs 需要检查的字符串数组\n\t * @return 是否包含任意一个字符串\n\t * @since 3.2.0\n\t */\n\tpublic static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) {\n\t\treturn null != getContainsStrIgnoreCase(str, testStrs);\n\t}\n\n\t/**\n\t * 查找指定字符串是否包含指定字符串列表中的任意一个字符串，如果包含返回找到的第一个字符串<br>\n\t * 忽略大小写\n\t *\n\t * @param str      指定字符串\n\t * @param testStrs 需要检查的字符串数组\n\t * @return 被包含的第一个字符串\n\t * @since 3.2.0\n\t */\n\tpublic static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) {\n\t\t\treturn null;\n\t\t}\n\t\tfor (CharSequence testStr : testStrs) {\n\t\t\tif (containsIgnoreCase(str, testStr)) {\n\t\t\t\treturn testStr.toString();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t// ------------------------------------------------------------------------ indexOf\n\n\t/**\n\t * 指定范围内查找指定字符\n\t *\n\t * @param str        字符串\n\t * @param searchChar 被查找的字符\n\t * @return 位置\n\t */\n\tpublic static int indexOf(CharSequence str, char searchChar) {\n\t\treturn indexOf(str, searchChar, 0);\n\t}\n\n\t/**\n\t * 指定范围内查找指定字符\n\t *\n\t * @param str        字符串\n\t * @param searchChar 被查找的字符\n\t * @param start      起始位置，如果小于0，从0开始查找\n\t * @return 位置\n\t */\n\tpublic static int indexOf(CharSequence str, char searchChar, int start) {\n\t\tif (str instanceof String) {\n\t\t\treturn ((String) str).indexOf(searchChar, start);\n\t\t} else {\n\t\t\treturn indexOf(str, searchChar, start, -1);\n\t\t}\n\t}\n\n\t/**\n\t * 指定范围内查找指定字符\n\t *\n\t * @param text       字符串\n\t * @param searchChar 被查找的字符\n\t * @param start      起始位置，如果小于0，从0开始查找\n\t * @param end        终止位置，如果超过str.length()则默认查找到字符串末尾\n\t * @return 位置\n\t */\n\tpublic static int indexOf(CharSequence text, char searchChar, int start, int end) {\n\t\tif (isEmpty(text)) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\treturn new CharFinder(searchChar).setText(text).setEndIndex(end).start(start);\n\t}\n\n\t/**\n\t * 指定范围内查找字符串，忽略大小写<br>\n\t *\n\t * <pre>\n\t * CharSequenceUtil.indexOfIgnoreCase(null, *, *)          = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(*, null, *)          = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"\", \"\", 0)           = 0\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"A\", 0)  = 0\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 0)  = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"AB\", 0) = 1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 3)  = 5\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 9)  = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", -1) = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"\", 2)   = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"abc\", \"\", 9)        = -1\n\t * </pre>\n\t *\n\t * @param str       字符串\n\t * @param searchStr 需要查找位置的字符串\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) {\n\t\treturn indexOfIgnoreCase(str, searchStr, 0);\n\t}\n\n\t/**\n\t * 指定范围内查找字符串\n\t *\n\t * <pre>\n\t * CharSequenceUtil.indexOfIgnoreCase(null, *, *)          = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(*, null, *)          = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"\", \"\", 0)           = 0\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"A\", 0)  = 0\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 0)  = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"AB\", 0) = 1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 3)  = 5\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 9)  = -1\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", -1) = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"aabaabaa\", \"\", 2)   = 2\n\t * CharSequenceUtil.indexOfIgnoreCase(\"abc\", \"\", 9)        = -1\n\t * </pre>\n\t *\n\t * @param str       字符串\n\t * @param searchStr 需要查找位置的字符串\n\t * @param fromIndex 起始位置\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) {\n\t\treturn indexOf(str, searchStr, fromIndex, true);\n\t}\n\n\t/**\n\t * 指定范围内查找字符串\n\t *\n\t * @param text       字符串，空则返回-1\n\t * @param searchStr  需要查找位置的字符串，空则返回-1\n\t * @param from       起始位置（包含）\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int indexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) {\n\t\tif (isEmpty(text) || isEmpty(searchStr)) {\n\t\t\tif (CharSequenceUtil.equals(text, searchStr)) {\n\t\t\t\treturn 0;\n\t\t\t} else {\n\t\t\t\treturn INDEX_NOT_FOUND;\n\t\t\t}\n\t\t}\n\t\treturn new StrFinder(searchStr, ignoreCase).setText(text).start(from);\n\t}\n\n\t/**\n\t * 指定范围内查找字符串，忽略大小写\n\t *\n\t * @param str       字符串\n\t * @param searchStr 需要查找位置的字符串\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr) {\n\t\treturn lastIndexOfIgnoreCase(str, searchStr, str.length());\n\t}\n\n\t/**\n\t * 指定范围内查找字符串，忽略大小写<br>\n\t * fromIndex 为搜索起始位置，从后往前计数\n\t *\n\t * @param str       字符串\n\t * @param searchStr 需要查找位置的字符串\n\t * @param fromIndex 起始位置，从后往前计数\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr, int fromIndex) {\n\t\treturn lastIndexOf(str, searchStr, fromIndex, true);\n\t}\n\n\t/**\n\t * 指定范围内查找字符串<br>\n\t * fromIndex 为搜索起始位置，从后往前计数\n\t *\n\t * @param text       字符串\n\t * @param searchStr  需要查找位置的字符串\n\t * @param from       起始位置，从后往前计数\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 位置\n\t * @since 3.2.1\n\t */\n\tpublic static int lastIndexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) {\n\t\tif (isEmpty(text) || isEmpty(searchStr)) {\n\t\t\tif (CharSequenceUtil.equals(text, searchStr)) {\n\t\t\t\treturn 0;\n\t\t\t} else {\n\t\t\t\treturn INDEX_NOT_FOUND;\n\t\t\t}\n\t\t}\n\t\treturn new StrFinder(searchStr, ignoreCase)\n\t\t\t.setText(text).setNegative(true).start(from);\n\t}\n\n\t/**\n\t * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。\n\t *\n\t * <p>\n\t * 如果 str=null 或 searchStr=null 或 ordinal&le;0 则返回-1<br>\n\t * 此方法来自：Apache-Commons-Lang\n\t * <p>\n\t * 例子（*代表任意字符）：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.ordinalIndexOf(null, *, *)          = -1\n\t * CharSequenceUtil.ordinalIndexOf(*, null, *)          = -1\n\t * CharSequenceUtil.ordinalIndexOf(\"\", \"\", *)           = 0\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"a\", 1)  = 0\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"a\", 2)  = 1\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"b\", 1)  = 2\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"b\", 2)  = 5\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"ab\", 1) = 1\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"ab\", 2) = 4\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"\", 1)   = 0\n\t * CharSequenceUtil.ordinalIndexOf(\"aabaabaa\", \"\", 2)   = 0\n\t * </pre>\n\t *\n\t * @param str       被检查的字符串，可以为null\n\t * @param searchStr 被查找的字符串，可以为null\n\t * @param ordinal   第几次出现的位置\n\t * @return 查找到的位置\n\t * @since 3.2.3\n\t */\n\tpublic static int ordinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal) {\n\t\tif (str == null || searchStr == null || ordinal <= 0) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\tif (searchStr.length() == 0) {\n\t\t\treturn 0;\n\t\t}\n\t\tint found = 0;\n\t\tint index = INDEX_NOT_FOUND;\n\t\tdo {\n\t\t\tindex = indexOf(str, searchStr, index + 1, false);\n\t\t\tif (index < 0) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t\tfound++;\n\t\t} while (found < ordinal);\n\t\treturn index;\n\t}\n\n\t// ------------------------------------------------------------------------ remove\n\n\t/**\n\t * 移除字符串中所有给定字符串<br>\n\t * 例：removeAll(\"aa-bb-cc-dd\", \"-\") =》 aabbccdd\n\t *\n\t * @param str         字符串\n\t * @param strToRemove 被移除的字符串\n\t * @return 移除后的字符串\n\t */\n\tpublic static String removeAll(CharSequence str, CharSequence strToRemove) {\n\t\t// strToRemove如果为空， 也不用继续后面的逻辑\n\t\tif (isEmpty(str) || isEmpty(strToRemove)) {\n\t\t\treturn str(str);\n\t\t}\n\t\treturn str.toString().replace(strToRemove, EMPTY);\n\t}\n\n\t/**\n\t * 移除字符串中所有给定字符串，当某个字符串出现多次，则全部移除<br>\n\t * 例：removeAny(\"aa-bb-cc-dd\", \"a\", \"b\") =》 --cc-dd\n\t *\n\t * @param str          字符串\n\t * @param strsToRemove 被移除的字符串\n\t * @return 移除后的字符串\n\t * @since 5.3.8\n\t */\n\tpublic static String removeAny(CharSequence str, CharSequence... strsToRemove) {\n\t\tString result = str(str);\n\t\tif (isNotEmpty(str)) {\n\t\t\tfor (CharSequence strToRemove : strsToRemove) {\n\t\t\t\tresult = removeAll(result, strToRemove);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 去除字符串中指定的多个字符，如有多个则全部去除\n\t *\n\t * @param str   字符串\n\t * @param chars 字符列表\n\t * @return 去除后的字符\n\t * @since 4.2.2\n\t */\n\tpublic static String removeAll(CharSequence str, char... chars) {\n\t\tif (null == str || ArrayUtil.isEmpty(chars)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal int len = str.length();\n\t\tif (0 == len) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal StringBuilder builder = new StringBuilder(len);\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif (false == ArrayUtil.contains(chars, c)) {\n\t\t\t\tbuilder.append(c);\n\t\t\t}\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 去除所有换行符，包括：\n\t *\n\t * <pre>\n\t * 1. \\r\n\t * 1. \\n\n\t * </pre>\n\t *\n\t * @param str 字符串\n\t * @return 处理后的字符串\n\t * @since 4.2.2\n\t */\n\tpublic static String removeAllLineBreaks(CharSequence str) {\n\t\treturn removeAll(str, CharUtil.CR, CharUtil.LF);\n\t}\n\n\t/**\n\t * 去掉首部指定长度的字符串并将剩余字符串首字母小写<br>\n\t * 例如：str=setName, preLength=3 =》 return name\n\t *\n\t * @param str       被处理的字符串\n\t * @param preLength 去掉的长度\n\t * @return 处理后的字符串，不符合规范返回null\n\t */\n\tpublic static String removePreAndLowerFirst(CharSequence str, int preLength) {\n\t\tif (str == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (str.length() > preLength) {\n\t\t\tchar first = Character.toLowerCase(str.charAt(preLength));\n\t\t\tif (str.length() > preLength + 1) {\n\t\t\t\treturn first + str.toString().substring(preLength + 1);\n\t\t\t}\n\t\t\treturn String.valueOf(first);\n\t\t} else {\n\t\t\treturn str.toString();\n\t\t}\n\t}\n\n\t/**\n\t * 去掉首部指定长度的字符串并将剩余字符串首字母小写<br>\n\t * 例如：str=setName, prefix=set =》 return name\n\t *\n\t * @param str    被处理的字符串\n\t * @param prefix 前缀\n\t * @return 处理后的字符串，不符合规范返回null\n\t */\n\tpublic static String removePreAndLowerFirst(CharSequence str, CharSequence prefix) {\n\t\treturn lowerFirst(removePrefix(str, prefix));\n\t}\n\n\t/**\n\t * 去掉指定前缀\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @return 切掉后的字符串，若前缀不是 preffix， 返回原字符串\n\t */\n\tpublic static String removePrefix(CharSequence str, CharSequence prefix) {\n\t\tif (isEmpty(str) || isEmpty(prefix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tif (str2.startsWith(prefix.toString())) {\n\t\t\treturn subSuf(str2, prefix.length());// 截取后半段\n\t\t}\n\t\treturn str2;\n\t}\n\n\t/**\n\t * 去掉指定所有前缀，如：\n\t * <pre>{@code\n\t *     str=abcdef, prefix=ab => return cdef\n\t *     str=ababcdef, prefix=ab => return cdef\n\t *     str=ababcdef, prefix=\"\" => return ababcdef\n\t *     str=ababcdef, prefix=null => return ababcdef\n\t * }</pre>\n\t *\n\t * @param str    字符串，空返回原字符串\n\t * @param prefix 前缀，空返回原字符串\n\t * @return 去掉所有前缀的字符串，若前缀不是 preffix， 返回原字符串\n\t * @since 5.8.30\n\t */\n\tpublic static String removeAllPrefix(CharSequence str, CharSequence prefix) {\n\t\tif (isEmpty(str) || isEmpty(prefix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String prefixStr = prefix.toString();\n\t\tfinal int prefixLength = prefixStr.length();\n\n\t\tfinal String str2 = str.toString();\n\t\tint toIndex = 0;\n\t\twhile (str2.startsWith(prefixStr, toIndex)) {\n\t\t\ttoIndex += prefixLength;\n\t\t}\n\t\treturn subSuf(str2, toIndex);\n\t}\n\n\t/**\n\t * 忽略大小写去掉指定前缀\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @return 切掉后的字符串，若前缀不是 prefix， 返回原字符串\n\t */\n\tpublic static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) {\n\t\tif (isEmpty(str) || isEmpty(prefix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tif (startWithIgnoreCase(str, prefix)) {\n\t\t\treturn subSuf(str2, prefix.length());// 截取后半段\n\t\t}\n\t\treturn str2;\n\t}\n\n\t/**\n\t * 去掉指定后缀\n\t *\n\t * @param str    字符串\n\t * @param suffix 后缀\n\t * @return 切掉后的字符串，若后缀不是 suffix， 返回原字符串\n\t */\n\tpublic static String removeSuffix(CharSequence str, CharSequence suffix) {\n\t\tif (isEmpty(str) || isEmpty(suffix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tif (str2.endsWith(suffix.toString())) {\n\t\t\treturn subPre(str2, str2.length() - suffix.length());// 截取前半段\n\t\t}\n\t\treturn str2;\n\t}\n\n\t/**\n\t * 去掉指定所有后缀，如：\n\t * <pre>{@code\n\t *     str=11abab, suffix=ab => return 11\n\t *     str=11ab, suffix=ab => return 11\n\t *     str=11ab, suffix=\"\" => return 11ab\n\t *     str=11ab, suffix=null => return 11ab\n\t * }</pre>\n\t *\n\t * @param str    字符串，空返回原字符串\n\t * @param suffix 后缀字符串，空返回原字符串\n\t * @return 去掉所有后缀的字符串，若后缀不是 suffix， 返回原字符串\n\t * @since 5.8.30\n\t */\n\tpublic static String removeAllSuffix(CharSequence str, CharSequence suffix) {\n\t\tif (isEmpty(str) || isEmpty(suffix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String suffixStr = suffix.toString();\n\t\tfinal int suffixLength = suffixStr.length();\n\n\t\tfinal String str2 = str.toString();\n\t\tint toIndex = str2.length();\n\t\twhile (str2.startsWith(suffixStr, toIndex - suffixLength)) {\n\t\t\ttoIndex -= suffixLength;\n\t\t}\n\t\treturn subPre(str2, toIndex);\n\t}\n\n\n\t/**\n\t * 去掉指定后缀，并小写首字母\n\t *\n\t * @param str    字符串\n\t * @param suffix 后缀\n\t * @return 切掉后的字符串，若后缀不是 suffix， 返回原字符串\n\t */\n\tpublic static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) {\n\t\treturn lowerFirst(removeSuffix(str, suffix));\n\t}\n\n\t/**\n\t * 忽略大小写去掉指定后缀\n\t *\n\t * @param str    字符串\n\t * @param suffix 后缀\n\t * @return 切掉后的字符串，若后缀不是 suffix， 返回原字符串\n\t */\n\tpublic static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) {\n\t\tif (isEmpty(str) || isEmpty(suffix)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tif (endWithIgnoreCase(str, suffix)) {\n\t\t\treturn subPre(str2, str2.length() - suffix.length());\n\t\t}\n\t\treturn str2;\n\t}\n\n\t/**\n\t * 清理空白字符\n\t *\n\t * @param str 被清理的字符串\n\t * @return 清理后的字符串\n\t */\n\tpublic static String cleanBlank(CharSequence str) {\n\t\treturn filter(str, c -> false == CharUtil.isBlankChar(c));\n\t}\n\n\t// ------------------------------------------------------------------------ strip\n\n\t/**\n\t * 去除两边的指定字符串，忽略大小写\n\t *\n\t * @param str            被处理的字符串\n\t * @param prefixOrSuffix 前缀或后缀\n\t * @return 处理后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String stripIgnoreCase(final CharSequence str, final CharSequence prefixOrSuffix) {\n\t\treturn stripIgnoreCase(str, prefixOrSuffix, prefixOrSuffix);\n\t}\n\n\t/**\n\t * 去除两边的指定字符串，忽略大小写\n\t *\n\t * @param str    被处理的字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 处理后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String stripIgnoreCase(final CharSequence str, final CharSequence prefix, final CharSequence suffix) {\n\t\treturn strip(str, prefix, suffix, true);\n\t}\n\n\t/**\n\t * 去除两边的指定字符串\n\t *\n\t * @param str            被处理的字符串\n\t * @param prefixOrSuffix 前缀或后缀\n\t * @return 处理后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String strip(CharSequence str, CharSequence prefixOrSuffix) {\n\t\tif (equals(str, prefixOrSuffix)) {\n\t\t\t// 对于去除相同字符的情况单独处理\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn strip(str, prefixOrSuffix, prefixOrSuffix);\n\t}\n\n\t/**\n\t * 去除两边的指定字符串<br>\n\t * 两边字符如果存在，则去除，不存在不做处理\n\t * <pre>{@code\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"b\"  -> \"aa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", null, null  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"\"  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"b\"  -> \"aaa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", null, \"b\"  -> \"aaa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"\"  -> \"aa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", null  -> \"aa_STRIPPED_bbb\"\n\t *\n\t * \"a\", \"a\", \"a\"  -> \"\"\n\t * }\n\t * </pre>\n\t *\n\t * @param str    被处理的字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 处理后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String strip(CharSequence str, CharSequence prefix, CharSequence suffix) {\n\t\treturn strip(str, prefix, suffix, false);\n\t}\n\n\t/**\n\t * 去除两边的指定字符串<br>\n\t * 两边字符如果存在，则去除，不存在不做处理\n\t * <pre>{@code\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"b\"  -> \"aa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", null, null  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"\"  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"b\"  -> \"aaa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", null, \"b\"  -> \"aaa_STRIPPED_bb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"\"  -> \"aa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", null  -> \"aa_STRIPPED_bbb\"\n\t *\n\t * \"a\", \"a\", \"a\"  -> \"\"\n\t * }\n\t * </pre>\n\t *\n\t * @param str        被处理的字符串\n\t * @param prefix     前缀\n\t * @param suffix     后缀\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 处理后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String strip(CharSequence str, CharSequence prefix, CharSequence suffix, boolean ignoreCase) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tint from = 0;\n\t\tint to = str2.length();\n\n\t\tif (startWith(str2, prefix, ignoreCase)) {\n\t\t\tfrom = prefix.length();\n\t\t\tif (from == to) {\n\t\t\t\t// \"a\", \"a\", \"a\"  -> \"\"\n\t\t\t\treturn EMPTY;\n\t\t\t}\n\t\t}\n\t\tif (endWith(str2, suffix, ignoreCase)) {\n\t\t\tto -= suffix.length();\n\t\t\tif (from == to) {\n\t\t\t\t// \"a\", \"a\", \"a\"  -> \"\"\n\t\t\t\treturn EMPTY;\n\t\t\t} else if (to < from) {\n\t\t\t\t// pre去除后和suffix有重叠，如 (\"aba\", \"ab\", \"ba\") -> \"a\"\n\t\t\t\tto += suffix.length();\n\t\t\t}\n\t\t}\n\n\t\treturn str2.substring(from, to);\n\t}\n\n\t/**\n\t * 去除两边<u><b>所有</b></u>的指定字符串\n\t *\n\t * <pre>{@code\n\t * \"aaa_STRIPPED_bbb\", \"a\"  -> \"_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"b\"  -> \"_STRIPPED_\"\n\t * \"aaa_STRIPPED_bbb\", \"\"  -> \"aaa_STRIPPED_bbb\"\n\t * }\n\t * </pre>\n\t *\n\t * @param str            被处理的字符串\n\t * @param prefixOrSuffix 前缀或后缀\n\t * @return 处理后的字符串\n\t * @since 5.8.30\n\t */\n\tpublic static String stripAll(final CharSequence str, final CharSequence prefixOrSuffix) {\n\t\tif (equals(str, prefixOrSuffix)) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn stripAll(str, prefixOrSuffix, prefixOrSuffix);\n\t}\n\n\t/**\n\t * 去除两边<u><b>所有</b></u>的指定字符串\n\t *\n\t * <pre>{@code\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"b\"  -> \"_STRIPPED_\"\n\t * \"aaa_STRIPPED_bbb\", null, null  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"\"  -> \"aaa_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"\", \"b\"  -> \"aaa_STRIPPED_\"\n\t * \"aaa_STRIPPED_bbb\", null, \"b\"  -> \"aaa_STRIPPED_\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", \"\"  -> \"_STRIPPED_bbb\"\n\t * \"aaa_STRIPPED_bbb\", \"a\", null  -> \"_STRIPPED_bbb\"\n\t *\n\t * // special test\n\t * \"aaaaaabbb\", \"aaa\", null  -> \"bbb\"\n\t * \"aaaaaaabbb\", \"aa\", null  -> \"abbb\"\n\t *\n\t * \"aaaaaaaaa\", \"aaa\", \"aa\"  -> \"\"\n\t * \"a\", \"a\", \"a\"  -> \"\"\n\t * }\n\t * </pre>\n\t *\n\t * @param str    被处理的字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 处理后的字符串\n\t * @since 5.8.30\n\t */\n\tpublic static String stripAll(final CharSequence str, final CharSequence prefix, final CharSequence suffix) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal String prefixStr = emptyIfNull(prefix);\n\t\tfinal String suffixStr = emptyIfNull(suffix);\n\n\t\tfinal String str2 = str.toString();\n\t\tint from = 0;\n\t\tint to = str2.length();\n\n\t\tif (!prefixStr.isEmpty()) {\n\t\t\twhile (str2.startsWith(prefixStr, from)) {\n\t\t\t\tfrom += prefix.length();\n\t\t\t\tif (from == to) {\n\t\t\t\t\t// \"a\", \"a\", \"a\"  -> \"\"\n\t\t\t\t\treturn EMPTY;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!suffixStr.isEmpty()) {\n\t\t\twhile (str2.startsWith(suffixStr, to - suffixStr.length())) {\n\t\t\t\tto -= suffixStr.length();\n\t\t\t\tif (from == to) {\n\t\t\t\t\t// \"a\", \"a\", \"a\"  -> \"\"\n\t\t\t\t\treturn EMPTY;\n\t\t\t\t} else if (to < from) {\n\t\t\t\t\t// pre去除后和suffix有重叠，如 (\"aba\", \"ab\", \"ba\") -> \"a\"\n\t\t\t\t\tto += suffixStr.length();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn str2.substring(from, to);\n\t}\n\n\t// ------------------------------------------------------------------------ add\n\n\t/**\n\t * 如果给定字符串不是以prefix开头的，在开头补充 prefix\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @return 补充后的字符串\n\t * @see #prependIfMissing(CharSequence, CharSequence, CharSequence...)\n\t */\n\tpublic static String addPrefixIfNot(CharSequence str, CharSequence prefix) {\n\t\treturn prependIfMissing(str, prefix, prefix);\n\t}\n\n\t/**\n\t * 如果给定字符串不是以suffix结尾的，在尾部补充 suffix\n\t *\n\t * @param str    字符串\n\t * @param suffix 后缀\n\t * @return 补充后的字符串\n\t * @see #appendIfMissing(CharSequence, CharSequence, CharSequence...)\n\t */\n\tpublic static String addSuffixIfNot(CharSequence str, CharSequence suffix) {\n\t\treturn appendIfMissing(str, suffix, suffix);\n\t}\n\n\t// ------------------------------------------------------------------------ split\n\n\t/**\n\t * 切分字符串为long数组\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符\n\t * @return 切分后long数组\n\t * @since 4.0.6\n\t */\n\tpublic static long[] splitToLong(CharSequence str, char separator) {\n\t\treturn Convert.convert(long[].class, splitTrim(str, separator));\n\t}\n\n\t/**\n\t * 切分字符串为long数组\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符串\n\t * @return 切分后long数组\n\t * @since 4.0.6\n\t */\n\tpublic static long[] splitToLong(CharSequence str, CharSequence separator) {\n\t\treturn Convert.convert(long[].class, splitTrim(str, separator));\n\t}\n\n\t/**\n\t * 切分字符串为int数组\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符\n\t * @return 切分后long数组\n\t * @since 4.0.6\n\t */\n\tpublic static int[] splitToInt(CharSequence str, char separator) {\n\t\treturn Convert.convert(int[].class, splitTrim(str, separator));\n\t}\n\n\t/**\n\t * 切分字符串为int数组\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符串\n\t * @return 切分后long数组\n\t * @since 4.0.6\n\t */\n\tpublic static int[] splitToInt(CharSequence str, CharSequence separator) {\n\t\treturn Convert.convert(int[].class, splitTrim(str, separator));\n\t}\n\n\t/**\n\t * 切分字符串<br>\n\t * a#b#c =》 [a,b,c] <br>\n\t * a##b#c =》 [a,\"\",b,c]\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @return 切分后的集合\n\t */\n\tpublic static List<String> split(CharSequence str, char separator) {\n\t\treturn split(str, separator, 0);\n\t}\n\n\t/**\n\t * 切分字符串，如果分隔符不存在则返回原字符串\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符\n\t * @return 字符串\n\t * @since 5.6.7\n\t */\n\tpublic static String[] splitToArray(CharSequence str, CharSequence separator) {\n\t\tif (str == null) {\n\t\t\treturn new String[]{};\n\t\t}\n\n\t\treturn StrSplitter.splitToArray(str.toString(), str(separator), 0, false, false);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @return 切分后的数组\n\t */\n\tpublic static String[] splitToArray(CharSequence str, char separator) {\n\t\treturn splitToArray(str, separator, 0);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param text      被切分的字符串\n\t * @param separator 分隔符字符\n\t * @param limit     限制分片数\n\t * @return 切分后的数组\n\t */\n\tpublic static String[] splitToArray(CharSequence text, char separator, int limit) {\n\t\tAssert.notNull(text, \"Text must be not null!\");\n\t\treturn StrSplitter.splitToArray(text.toString(), separator, limit, false, false);\n\t}\n\n\t/**\n\t * 切分字符串，不去除切分后每个元素两边的空白符，不去除空白项\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @param limit     限制分片数，-1不限制\n\t * @return 切分后的集合\n\t */\n\tpublic static List<String> split(CharSequence str, char separator, int limit) {\n\t\treturn split(str, separator, limit, false, false);\n\t}\n\n\t/**\n\t * 切分字符串，去除切分后每个元素两边的空白符，去除空白项\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @return 切分后的集合\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, char separator) {\n\t\treturn splitTrim(str, separator, -1);\n\t}\n\n\t/**\n\t * 切分字符串，去除切分后每个元素两边的空白符，去除空白项\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @return 切分后的集合\n\t * @since 3.2.0\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, CharSequence separator) {\n\t\treturn splitTrim(str, separator, -1);\n\t}\n\n\t/**\n\t * 切分字符串，去除切分后每个元素两边的空白符，去除空白项\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @param limit     限制分片数，-1不限制\n\t * @return 切分后的集合\n\t * @since 3.1.0\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, char separator, int limit) {\n\t\treturn split(str, separator, limit, true, true);\n\t}\n\n\t/**\n\t * 切分字符串，去除切分后每个元素两边的空白符，去除空白项\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符字符\n\t * @param limit     限制分片数，-1不限制\n\t * @return 切分后的集合\n\t * @since 3.2.0\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, CharSequence separator, int limit) {\n\t\treturn split(str, separator, limit, true, true);\n\t}\n\n\t/**\n\t * 切分字符串，不限制分片数量\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, 0, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn StrSplitter.split(str, separator, limit, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param <R>         切分后元素类型\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param ignoreEmpty 是否忽略空串\n\t * @param mapping     切分后的字符串元素的转换方法\n\t * @return 切分后的集合，元素类型是经过 mapping 转换后的\n\t * @since 5.7.14\n\t */\n\tpublic static <R> List<R> split(CharSequence str, char separator, int limit, boolean ignoreEmpty, Function<String, R> mapping) {\n\t\treturn StrSplitter.split(str, separator, limit, ignoreEmpty, mapping);\n\t}\n\n\t/**\n\t * 切分字符串，如果分隔符不存在则返回原字符串\n\t *\n\t * @param str       被切分的字符串\n\t * @param separator 分隔符\n\t * @return 字符串\n\t * @since 5.7.1\n\t */\n\tpublic static List<String> split(CharSequence str, CharSequence separator) {\n\t\treturn split(str, separator, false, false);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 5.6.7\n\t */\n\tpublic static List<String> split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, 0, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.0\n\t */\n\tpublic static List<String> split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\tfinal String separatorStr = (null == separator) ? null : separator.toString();\n\t\treturn StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 根据给定长度，将给定字符串截取为多个部分\n\t *\n\t * @param str 字符串\n\t * @param len 每一个小节的长度\n\t * @return 截取后的字符串数组\n\t * @see StrSplitter#splitByLength(CharSequence, int)\n\t */\n\tpublic static String[] split(CharSequence str, int len) {\n\t\treturn StrSplitter.splitByLength(str, len);\n\t}\n\n\t/**\n\t * 将字符串切分为N等份\n\t *\n\t * @param str        字符串\n\t * @param partLength 每等份的长度\n\t * @return 切分后的数组\n\t * @since 3.0.6\n\t */\n\tpublic static String[] cut(CharSequence str, int partLength) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tint len = str.length();\n\t\tif (len < partLength) {\n\t\t\treturn new String[]{str.toString()};\n\t\t}\n\t\tint part = NumberUtil.count(len, partLength);\n\t\tfinal String[] array = new String[part];\n\n\t\tfinal String str2 = str.toString();\n\t\tfor (int i = 0; i < part; i++) {\n\t\t\tarray[i] = str2.substring(i * partLength, (i == part - 1) ? len : (partLength + i * partLength));\n\t\t}\n\t\treturn array;\n\t}\n\n\t// ------------------------------------------------------------------------ sub\n\n\t/**\n\t * 改进JDK subString<br>\n\t * index从0开始计算，最后一个字符为-1<br>\n\t * 如果from和to位置一样，返回 \"\" <br>\n\t * 如果from或to为负数，则按照length从后向前数位置，如果绝对值大于字符串长度，则from归到0，to归到length<br>\n\t * 如果经过修正的index中from大于to，则互换from和to example: <br>\n\t * abcdefgh 2 3 =》 c <br>\n\t * abcdefgh 2 -3 =》 cde <br>\n\t *\n\t * @param str              String\n\t * @param fromIndexInclude 开始的index（包括）\n\t * @param toIndexExclude   结束的index（不包括）\n\t * @return 字串\n\t */\n\tpublic static String sub(CharSequence str, int fromIndexInclude, int toIndexExclude) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tint len = str.length();\n\n\t\tif (fromIndexInclude < 0) {\n\t\t\tfromIndexInclude = len + fromIndexInclude;\n\t\t\tif (fromIndexInclude < 0) {\n\t\t\t\tfromIndexInclude = 0;\n\t\t\t}\n\t\t} else if (fromIndexInclude > len) {\n\t\t\tfromIndexInclude = len;\n\t\t}\n\n\t\tif (toIndexExclude < 0) {\n\t\t\ttoIndexExclude = len + toIndexExclude;\n\t\t\tif (toIndexExclude < 0) {\n\t\t\t\ttoIndexExclude = len;\n\t\t\t}\n\t\t} else if (toIndexExclude > len) {\n\t\t\ttoIndexExclude = len;\n\t\t}\n\n\t\tif (toIndexExclude < fromIndexInclude) {\n\t\t\tint tmp = fromIndexInclude;\n\t\t\tfromIndexInclude = toIndexExclude;\n\t\t\ttoIndexExclude = tmp;\n\t\t}\n\n\t\tif (fromIndexInclude == toIndexExclude) {\n\t\t\treturn EMPTY;\n\t\t}\n\n\t\treturn str.toString().substring(fromIndexInclude, toIndexExclude);\n\t}\n\n\t/**\n\t * 通过CodePoint截取字符串，可以截断Emoji\n\t *\n\t * @param str       String\n\t * @param fromIndex 开始的index（包括）\n\t * @param toIndex   结束的index（不包括）\n\t * @return 字串\n\t */\n\tpublic static String subByCodePoint(CharSequence str, int fromIndex, int toIndex) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tif (fromIndex < 0 || fromIndex > toIndex) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\n\t\tif (fromIndex == toIndex) {\n\t\t\treturn EMPTY;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tfinal int subLen = toIndex - fromIndex;\n\t\tstr.toString().codePoints().skip(fromIndex).limit(subLen).forEach(v -> sb.append(Character.toChars(v)));\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 截取部分字符串，这里一个汉字的长度认为是2\n\t *\n\t * @param str    字符串\n\t * @param len    bytes切割到的位置（包含）\n\t * @param suffix 切割后加上后缀\n\t * @return 切割后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static String subPreGbk(CharSequence str, int len, CharSequence suffix) {\n\t\treturn subPreGbk(str, len, true) + suffix;\n\t}\n\n\t/**\n\t * 截取部分字符串，这里一个汉字的长度认为是2<br>\n\t * 可以自定义halfUp，如len为10，如果截取后最后一个字符是半个字符，{@code true}表示保留，则长度是11，否则长度9\n\t *\n\t * @param str    字符串\n\t * @param len    bytes切割到的位置（包含）\n\t * @param halfUp 遇到截取一半的GBK字符，是否保留。\n\t * @return 切割后的字符串\n\t * @since 5.7.17\n\t */\n\tpublic static String subPreGbk(CharSequence str, int len, boolean halfUp) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tint counterOfDoubleByte = 0;\n\t\tfinal byte[] b = bytes(str, CharsetUtil.CHARSET_GBK);\n\t\tif (b.length <= len) {\n\t\t\treturn str.toString();\n\t\t}\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tif (b[i] < 0) {\n\t\t\t\tcounterOfDoubleByte++;\n\t\t\t}\n\t\t}\n\n\t\tif (counterOfDoubleByte % 2 != 0) {\n\t\t\tif (halfUp) {\n\t\t\t\tlen += 1;\n\t\t\t} else {\n\t\t\t\tlen -= 1;\n\t\t\t}\n\t\t}\n\t\treturn new String(b, 0, len, CharsetUtil.CHARSET_GBK);\n\t}\n\n\t/**\n\t * 切割指定位置之前部分的字符串\n\t *\n\t * @param string         字符串\n\t * @param toIndexExclude 切割到的位置（不包括）\n\t * @return 切割后的剩余的前半部分字符串\n\t */\n\tpublic static String subPre(CharSequence string, int toIndexExclude) {\n\t\treturn sub(string, 0, toIndexExclude);\n\t}\n\n\t/**\n\t * 切割指定位置之后部分的字符串\n\t *\n\t * @param string    字符串\n\t * @param fromIndex 切割开始的位置（包括）\n\t * @return 切割后后剩余的后半部分字符串\n\t */\n\tpublic static String subSuf(CharSequence string, int fromIndex) {\n\t\tif (isEmpty(string)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn sub(string, fromIndex, string.length());\n\t}\n\n\t/**\n\t * 切割指定长度的后部分的字符串\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subSufByLength(\"abcde\", 3)      =    \"cde\"\n\t * CharSequenceUtil.subSufByLength(\"abcde\", 0)      =    \"\"\n\t * CharSequenceUtil.subSufByLength(\"abcde\", -5)     =    \"\"\n\t * CharSequenceUtil.subSufByLength(\"abcde\", -1)     =    \"\"\n\t * CharSequenceUtil.subSufByLength(\"abcde\", 5)       =    \"abcde\"\n\t * CharSequenceUtil.subSufByLength(\"abcde\", 10)     =    \"abcde\"\n\t * CharSequenceUtil.subSufByLength(null, 3)               =    null\n\t * </pre>\n\t *\n\t * @param string 字符串\n\t * @param length 切割长度\n\t * @return 切割后后剩余的后半部分字符串\n\t * @since 4.0.1\n\t */\n\tpublic static String subSufByLength(CharSequence string, int length) {\n\t\tif (isEmpty(string)) {\n\t\t\treturn null;\n\t\t}\n\t\tif (length <= 0) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn sub(string, -length, string.length());\n\t}\n\n\t/**\n\t * 截取字符串,从指定位置开始,截取指定长度的字符串<br>\n\t * 如果fromIndex为正数，则向后截取指定length长度，如果为负数，则向前截取length长度。\n\t *\n\t * @param input     原始字符串\n\t * @param fromIndex 开始的index,包括\n\t * @param length    要截取的长度\n\t * @return 截取后的字符串\n\t * @author weibaohui\n\t */\n\tpublic static String subWithLength(String input, int fromIndex, int length) {\n\t\tfinal int toIndex;\n\t\tif (fromIndex < 0) {\n\t\t\ttoIndex = fromIndex - length;\n\t\t} else {\n\t\t\ttoIndex = fromIndex + length;\n\t\t}\n\t\treturn sub(input, fromIndex, toIndex);\n\t}\n\n\t/**\n\t * 截取分隔字符串之前的字符串，不包括分隔字符串<br>\n\t * 如果给定的字符串为空串（null或\"\"）或者分隔字符串为null，返回原字符串<br>\n\t * 如果分隔字符串为空串\"\"，则返回空串，如果分隔字符串未找到，返回原字符串，举例如下：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBefore(null, *, false)      = null\n\t * CharSequenceUtil.subBefore(\"\", *, false)        = \"\"\n\t * CharSequenceUtil.subBefore(\"abc\", \"a\", false)   = \"\"\n\t * CharSequenceUtil.subBefore(\"abcba\", \"b\", false) = \"a\"\n\t * CharSequenceUtil.subBefore(\"abc\", \"c\", false)   = \"ab\"\n\t * CharSequenceUtil.subBefore(\"abc\", \"d\", false)   = \"abc\"\n\t * CharSequenceUtil.subBefore(\"abc\", \"\", false)    = \"\"\n\t * CharSequenceUtil.subBefore(\"abc\", null, false)  = \"abc\"\n\t * </pre>\n\t *\n\t * @param string          被查找的字符串\n\t * @param separator       分隔字符串（不包括）\n\t * @param isLastSeparator 是否查找最后一个分隔字符串（多次出现分隔字符串时选取最后一个），true为选取最后一个\n\t * @return 切割后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) {\n\t\tif (isEmpty(string) || separator == null) {\n\t\t\treturn null == string ? null : string.toString();\n\t\t}\n\n\t\tfinal String str = string.toString();\n\t\tfinal String sep = separator.toString();\n\t\tif (sep.isEmpty()) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tfinal int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);\n\t\tif (INDEX_NOT_FOUND == pos) {\n\t\t\treturn str;\n\t\t}\n\t\tif (0 == pos) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.substring(0, pos);\n\t}\n\n\t/**\n\t * 截取分隔字符串之前的字符串，不包括分隔字符串<br>\n\t * 如果给定的字符串为空串（null或\"\"）或者分隔字符串为null，返回原字符串<br>\n\t * 如果分隔字符串未找到，返回原字符串，举例如下：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBefore(null, *, false)      = null\n\t * CharSequenceUtil.subBefore(\"\", *, false)        = \"\"\n\t * CharSequenceUtil.subBefore(\"abc\", 'a', false)   = \"\"\n\t * CharSequenceUtil.subBefore(\"abcba\", 'b', false) = \"a\"\n\t * CharSequenceUtil.subBefore(\"abc\", 'c', false)   = \"ab\"\n\t * CharSequenceUtil.subBefore(\"abc\", 'd', false)   = \"abc\"\n\t * </pre>\n\t *\n\t * @param string          被查找的字符串\n\t * @param separator       分隔字符串（不包括）\n\t * @param isLastSeparator 是否查找最后一个分隔字符串（多次出现分隔字符串时选取最后一个），true为选取最后一个\n\t * @return 切割后的字符串\n\t * @since 4.1.15\n\t */\n\tpublic static String subBefore(CharSequence string, char separator, boolean isLastSeparator) {\n\t\tif (isEmpty(string)) {\n\t\t\treturn null == string ? null : EMPTY;\n\t\t}\n\n\t\tfinal String str = string.toString();\n\t\tfinal int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator);\n\t\tif (INDEX_NOT_FOUND == pos) {\n\t\t\treturn str;\n\t\t}\n\t\tif (0 == pos) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.substring(0, pos);\n\t}\n\n\t/**\n\t * 截取分隔字符串之后的字符串，不包括分隔字符串<br>\n\t * 如果给定的字符串为空串（null或\"\"），返回原字符串<br>\n\t * 如果分隔字符串为空串（null或\"\"），则返回空串，如果分隔字符串未找到，返回空串，举例如下：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subAfter(null, *, false)      = null\n\t * CharSequenceUtil.subAfter(\"\", *, false)        = \"\"\n\t * CharSequenceUtil.subAfter(*, null, false)      = \"\"\n\t * CharSequenceUtil.subAfter(\"abc\", \"a\", false)   = \"bc\"\n\t * CharSequenceUtil.subAfter(\"abcba\", \"b\", false) = \"cba\"\n\t * CharSequenceUtil.subAfter(\"abc\", \"c\", false)   = \"\"\n\t * CharSequenceUtil.subAfter(\"abc\", \"d\", false)   = \"\"\n\t * CharSequenceUtil.subAfter(\"abc\", \"\", false)    = \"abc\"\n\t * </pre>\n\t *\n\t * @param string          被查找的字符串\n\t * @param separator       分隔字符串（不包括）\n\t * @param isLastSeparator 是否查找最后一个分隔字符串（多次出现分隔字符串时选取最后一个），true为选取最后一个\n\t * @return 切割后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) {\n\t\tif (isEmpty(string)) {\n\t\t\treturn null == string ? null : EMPTY;\n\t\t}\n\t\tif (separator == null) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tfinal String str = string.toString();\n\t\tfinal String sep = separator.toString();\n\t\tfinal int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);\n\t\tif (INDEX_NOT_FOUND == pos || (string.length() - 1) == pos) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.substring(pos + separator.length());\n\t}\n\n\t/**\n\t * 截取分隔字符串之后的字符串，不包括分隔字符串<br>\n\t * 如果给定的字符串为空串（null或\"\"），返回原字符串<br>\n\t * 如果分隔字符串为空串（null或\"\"），则返回空串，如果分隔字符串未找到，返回空串，举例如下：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subAfter(null, *, false)      = null\n\t * CharSequenceUtil.subAfter(\"\", *, false)        = \"\"\n\t * CharSequenceUtil.subAfter(\"abc\", 'a', false)   = \"bc\"\n\t * CharSequenceUtil.subAfter(\"abcba\", 'b', false) = \"cba\"\n\t * CharSequenceUtil.subAfter(\"abc\", 'c', false)   = \"\"\n\t * CharSequenceUtil.subAfter(\"abc\", 'd', false)   = \"\"\n\t * </pre>\n\t *\n\t * @param string          被查找的字符串\n\t * @param separator       分隔字符串（不包括）\n\t * @param isLastSeparator 是否查找最后一个分隔字符串（多次出现分隔字符串时选取最后一个），true为选取最后一个\n\t * @return 切割后的字符串\n\t * @since 4.1.15\n\t */\n\tpublic static String subAfter(CharSequence string, char separator, boolean isLastSeparator) {\n\t\tif (isEmpty(string)) {\n\t\t\treturn null == string ? null : EMPTY;\n\t\t}\n\t\tfinal String str = string.toString();\n\t\tfinal int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator);\n\t\tif (INDEX_NOT_FOUND == pos) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.substring(pos + 1);\n\t}\n\n\t/**\n\t * 截取指定字符串中间部分，不包括标识字符串<br>\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBetween(\"wx[b]yz\", \"[\", \"]\") = \"b\"\n\t * CharSequenceUtil.subBetween(null, *, *)          = null\n\t * CharSequenceUtil.subBetween(*, null, *)          = null\n\t * CharSequenceUtil.subBetween(*, *, null)          = null\n\t * CharSequenceUtil.subBetween(\"\", \"\", \"\")          = \"\"\n\t * CharSequenceUtil.subBetween(\"\", \"\", \"]\")         = null\n\t * CharSequenceUtil.subBetween(\"\", \"[\", \"]\")        = null\n\t * CharSequenceUtil.subBetween(\"yabcz\", \"\", \"\")     = \"\"\n\t * CharSequenceUtil.subBetween(\"yabcz\", \"y\", \"z\")   = \"abc\"\n\t * CharSequenceUtil.subBetween(\"yabczyabcz\", \"y\", \"z\")   = \"abc\"\n\t * </pre>\n\t *\n\t * @param str    被切割的字符串\n\t * @param before 截取开始的字符串标识\n\t * @param after  截取到的字符串标识\n\t * @return 截取后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static String subBetween(CharSequence str, CharSequence before, CharSequence after) {\n\t\tif (str == null || before == null || after == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\tfinal String before2 = before.toString();\n\t\tfinal String after2 = after.toString();\n\n\t\tfinal int start = str2.indexOf(before2);\n\t\tif (start != INDEX_NOT_FOUND) {\n\t\t\tfinal int end = str2.indexOf(after2, start + before2.length());\n\t\t\tif (end != INDEX_NOT_FOUND) {\n\t\t\t\treturn str2.substring(start + before2.length(), end);\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 截取指定字符串中间部分，不包括标识字符串<br>\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBetween(null, *)            = null\n\t * CharSequenceUtil.subBetween(\"\", \"\")             = \"\"\n\t * CharSequenceUtil.subBetween(\"\", \"tag\")          = null\n\t * CharSequenceUtil.subBetween(\"tagabctag\", null)  = null\n\t * CharSequenceUtil.subBetween(\"tagabctag\", \"\")    = \"\"\n\t * CharSequenceUtil.subBetween(\"tagabctag\", \"tag\") = \"abc\"\n\t * </pre>\n\t *\n\t * @param str            被切割的字符串\n\t * @param beforeAndAfter 截取开始和结束的字符串标识\n\t * @return 截取后的字符串\n\t * @since 3.1.1\n\t */\n\tpublic static String subBetween(CharSequence str, CharSequence beforeAndAfter) {\n\t\treturn subBetween(str, beforeAndAfter, beforeAndAfter);\n\t}\n\n\t/**\n\t * 截取指定字符串多段中间部分，不包括标识字符串<br>\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBetweenAll(\"wx[b]y[z]\", \"[\", \"]\") \t\t= [\"b\",\"z\"]\n\t * CharSequenceUtil.subBetweenAll(null, *, *)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(*, null, *)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(*, *, null)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"\", \"\", \"\")          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"\", \"\", \"]\")         \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"\", \"[\", \"]\")        \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"yabcz\", \"\", \"\")     \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"yabcz\", \"y\", \"z\")   \t\t\t= [\"abc\"]\n\t * CharSequenceUtil.subBetweenAll(\"yabczyabcz\", \"y\", \"z\")   \t\t= [\"abc\",\"abc\"]\n\t * CharSequenceUtil.subBetweenAll(\"[yabc[zy]abcz]\", \"[\", \"]\");   = [\"zy\"]           重叠时只截取内部，\n\t * </pre>\n\t *\n\t * @param str    被切割的字符串\n\t * @param prefix 截取开始的字符串标识\n\t * @param suffix 截取到的字符串标识\n\t * @return 截取后的字符串\n\t * @author dahuoyzs\n\t * @since 5.2.5\n\t */\n\tpublic static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) {\n\t\tif (hasEmpty(str, prefix, suffix) ||\n\t\t\t// 不包含起始字符串，则肯定没有子串\n\t\t\tfalse == contains(str, prefix)) {\n\t\t\treturn new String[0];\n\t\t}\n\n\t\tfinal List<String> result = new LinkedList<>();\n\t\tfinal String[] split = splitToArray(str, prefix);\n\t\tif (prefix.equals(suffix)) {\n\t\t\t// 前后缀字符相同，单独处理\n\t\t\tfor (int i = 1, length = split.length - 1; i < length; i += 2) {\n\t\t\t\tresult.add(split[i]);\n\t\t\t}\n\t\t} else {\n\t\t\tint suffixIndex;\n\t\t\tString fragment;\n\t\t\tfor (int i = 1; i < split.length; i++) {\n\t\t\t\tfragment = split[i];\n\t\t\t\tsuffixIndex = fragment.indexOf(suffix.toString());\n\t\t\t\tif (suffixIndex > 0) {\n\t\t\t\t\tresult.add(fragment.substring(0, suffixIndex));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result.toArray(new String[0]);\n\t}\n\n\t/**\n\t * 截取指定字符串多段中间部分，不包括标识字符串<br>\n\t * <p>\n\t * 栗子：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.subBetweenAll(null, *)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(*, null)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(*, *)          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"\", \"\")          \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"\", \"#\")         \t\t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"gotanks\", \"\")     \t\t= []\n\t * CharSequenceUtil.subBetweenAll(\"#gotanks#\", \"#\")   \t\t= [\"gotanks\"]\n\t * CharSequenceUtil.subBetweenAll(\"#hello# #world#!\", \"#\")   = [\"hello\", \"world\"]\n\t * CharSequenceUtil.subBetweenAll(\"#hello# world#!\", \"#\");   = [\"hello\"]\n\t * </pre>\n\t *\n\t * @param str             被切割的字符串\n\t * @param prefixAndSuffix 截取开始和结束的字符串标识\n\t * @return 截取后的字符串\n\t * @author gotanks\n\t * @since 5.5.0\n\t */\n\tpublic static String[] subBetweenAll(CharSequence str, CharSequence prefixAndSuffix) {\n\t\treturn subBetweenAll(str, prefixAndSuffix, prefixAndSuffix);\n\t}\n\n\t// ------------------------------------------------------------------------ repeat\n\n\t/**\n\t * 重复某个字符\n\t *\n\t * <pre>\n\t * CharSequenceUtil.repeat('e', 0)  = \"\"\n\t * CharSequenceUtil.repeat('e', 3)  = \"eee\"\n\t * CharSequenceUtil.repeat('e', -2) = \"\"\n\t * </pre>\n\t *\n\t * @param c     被重复的字符\n\t * @param count 重复的数目，如果小于等于0则返回\"\"\n\t * @return 重复字符字符串\n\t */\n\tpublic static String repeat(char c, int count) {\n\t\tif (count <= 0) {\n\t\t\treturn EMPTY;\n\t\t}\n\n\t\tchar[] result = new char[count];\n\t\tArrays.fill(result, c);\n\t\treturn new String(result);\n\t}\n\n\t/**\n\t * 重复某个字符串\n\t *\n\t * @param str   被重复的字符\n\t * @param count 重复的数目\n\t * @return 重复字符字符串\n\t */\n\tpublic static String repeat(CharSequence str, int count) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (count <= 0 || str.length() == 0) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tif (count == 1) {\n\t\t\treturn str.toString();\n\t\t}\n\n\t\t// 检查\n\t\tfinal int len = str.length();\n\t\tfinal long longSize = (long) len * (long) count;\n\t\tfinal int size = (int) longSize;\n\t\tif (size != longSize) {\n\t\t\tthrow new ArrayIndexOutOfBoundsException(\"Required String length is too large: \" + longSize);\n\t\t}\n\n\t\tfinal char[] array = new char[size];\n\t\tstr.toString().getChars(0, len, array, 0);\n\t\tint n;\n\t\tfor (n = len; n < size - n; n <<= 1) {// n <<= 1相当于n *2\n\t\t\tSystem.arraycopy(array, 0, array, n, n);\n\t\t}\n\t\tSystem.arraycopy(array, 0, array, n, size - n);\n\t\treturn new String(array);\n\t}\n\n\t/**\n\t * 重复某个字符串到指定长度\n\t *\n\t * @param str    被重复的字符\n\t * @param padLen 指定长度\n\t * @return 重复字符字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String repeatByLength(CharSequence str, int padLen) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (padLen <= 0) {\n\t\t\treturn CharSequenceUtil.EMPTY;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen == padLen) {\n\t\t\treturn str.toString();\n\t\t} else if (strLen > padLen) {\n\t\t\treturn subPre(str, padLen);\n\t\t}\n\n\t\t// 重复，直到达到指定长度\n\t\tfinal char[] padding = new char[padLen];\n\t\tfor (int i = 0; i < padLen; i++) {\n\t\t\tpadding[i] = str.charAt(i % strLen);\n\t\t}\n\t\treturn new String(padding);\n\t}\n\n\t/**\n\t * 重复某个字符串并通过分界符连接\n\t *\n\t * <pre>\n\t * CharSequenceUtil.repeatAndJoin(\"?\", 5, \",\")   = \"?,?,?,?,?\"\n\t * CharSequenceUtil.repeatAndJoin(\"?\", 0, \",\")   = \"\"\n\t * CharSequenceUtil.repeatAndJoin(\"?\", 5, null) = \"?????\"\n\t * </pre>\n\t *\n\t * @param str       被重复的字符串\n\t * @param count     数量\n\t * @param delimiter 分界符\n\t * @return 连接后的字符串\n\t * @since 4.0.1\n\t */\n\tpublic static String repeatAndJoin(CharSequence str, int count, CharSequence delimiter) {\n\t\tif (count <= 0) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tfinal StringBuilder builder = new StringBuilder(str.length() * count);\n\t\tbuilder.append(str);\n\t\tcount--;\n\n\t\tfinal boolean isAppendDelimiter = isNotEmpty(delimiter);\n\t\twhile (count-- > 0) {\n\t\t\tif (isAppendDelimiter) {\n\t\t\t\tbuilder.append(delimiter);\n\t\t\t}\n\t\t\tbuilder.append(str);\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ equals\n\n\t/**\n\t * 比较两个字符串（大小写敏感）。\n\t *\n\t * <pre>\n\t * equals(null, null)   = true\n\t * equals(null, &quot;abc&quot;)  = false\n\t * equals(&quot;abc&quot;, null)  = false\n\t * equals(&quot;abc&quot;, &quot;abc&quot;) = true\n\t * equals(&quot;abc&quot;, &quot;ABC&quot;) = false\n\t * </pre>\n\t *\n\t * @param str1 要比较的字符串1\n\t * @param str2 要比较的字符串2\n\t * @return 如果两个字符串相同，或者都是{@code null}，则返回{@code true}\n\t */\n\tpublic static boolean equals(CharSequence str1, CharSequence str2) {\n\t\treturn equals(str1, str2, false);\n\t}\n\n\t/**\n\t * 比较两个字符串（大小写不敏感）。\n\t *\n\t * <pre>\n\t * equalsIgnoreCase(null, null)   = true\n\t * equalsIgnoreCase(null, &quot;abc&quot;)  = false\n\t * equalsIgnoreCase(&quot;abc&quot;, null)  = false\n\t * equalsIgnoreCase(&quot;abc&quot;, &quot;abc&quot;) = true\n\t * equalsIgnoreCase(&quot;abc&quot;, &quot;ABC&quot;) = true\n\t * </pre>\n\t *\n\t * @param str1 要比较的字符串1\n\t * @param str2 要比较的字符串2\n\t * @return 如果两个字符串相同，或者都是{@code null}，则返回{@code true}\n\t */\n\tpublic static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) {\n\t\treturn equals(str1, str2, true);\n\t}\n\n\t/**\n\t * 比较两个字符串是否相等，规则如下\n\t * <ul>\n\t *     <li>str1和str2都为{@code null}</li>\n\t *     <li>忽略大小写使用{@link String#equalsIgnoreCase(String)}判断相等</li>\n\t *     <li>不忽略大小写使用{@link String#contentEquals(CharSequence)}判断相等</li>\n\t * </ul>\n\t *\n\t * @param str1       要比较的字符串1\n\t * @param str2       要比较的字符串2\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 如果两个字符串相同，或者都是{@code null}，则返回{@code true}\n\t * @since 3.2.0\n\t */\n\tpublic static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) {\n\t\tif (null == str1) {\n\t\t\t// 只有两个都为null才判断相等\n\t\t\treturn str2 == null;\n\t\t}\n\t\tif (null == str2) {\n\t\t\t// 字符串2空，字符串1非空，直接false\n\t\t\treturn false;\n\t\t}\n\n\t\tif (ignoreCase) {\n\t\t\treturn str1.toString().equalsIgnoreCase(str2.toString());\n\t\t} else {\n\t\t\treturn str1.toString().contentEquals(str2);\n\t\t}\n\t}\n\n\t/**\n\t * 给定字符串是否与提供的中任一字符串相同（忽略大小写），相同则返回{@code true}，没有相同的返回{@code false}<br>\n\t * 如果参与比对的字符串列表为空，返回{@code false}\n\t *\n\t * @param str1 给定需要检查的字符串\n\t * @param strs 需要参与比对的字符串列表\n\t * @return 是否相同\n\t * @since 4.3.2\n\t */\n\tpublic static boolean equalsAnyIgnoreCase(CharSequence str1, CharSequence... strs) {\n\t\treturn equalsAny(str1, true, strs);\n\t}\n\n\t/**\n\t * 给定字符串是否与提供的中任一字符串相同，相同则返回{@code true}，没有相同的返回{@code false}<br>\n\t * 如果参与比对的字符串列表为空，返回{@code false}\n\t *\n\t * @param str1 给定需要检查的字符串\n\t * @param strs 需要参与比对的字符串列表\n\t * @return 是否相同\n\t * @since 4.3.2\n\t */\n\tpublic static boolean equalsAny(CharSequence str1, CharSequence... strs) {\n\t\treturn equalsAny(str1, false, strs);\n\t}\n\n\t/**\n\t * 给定字符串是否与提供的中任一字符串相同，相同则返回{@code true}，没有相同的返回{@code false}<br>\n\t * 如果参与比对的字符串列表为空，返回{@code false}\n\t *\n\t * @param str1       给定需要检查的字符串\n\t * @param ignoreCase 是否忽略大小写\n\t * @param strs       需要参与比对的字符串列表\n\t * @return 是否相同\n\t * @since 4.3.2\n\t */\n\tpublic static boolean equalsAny(CharSequence str1, boolean ignoreCase, CharSequence... strs) {\n\t\tif (ArrayUtil.isEmpty(strs)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (CharSequence str : strs) {\n\t\t\tif (equals(str1, str, ignoreCase)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 字符串指定位置的字符是否与给定字符相同<br>\n\t * 如果字符串为null，返回false<br>\n\t * 如果给定的位置大于字符串长度，返回false<br>\n\t * 如果给定的位置小于0，返回false\n\t *\n\t * @param str      字符串\n\t * @param position 位置\n\t * @param c        需要对比的字符\n\t * @return 字符串指定位置的字符是否与给定字符相同\n\t * @since 3.3.1\n\t */\n\tpublic static boolean equalsCharAt(CharSequence str, int position, char c) {\n\t\tif (null == str || position < 0) {\n\t\t\treturn false;\n\t\t}\n\t\treturn str.length() > position && c == str.charAt(position);\n\t}\n\n\t/**\n\t * 截取第一个字串的部分字符，与第二个字符串比较（长度一致），判断截取的子串是否相同<br>\n\t * 任意一个字符串为null返回false\n\t *\n\t * @param str1       第一个字符串\n\t * @param start1     第一个字符串开始的位置\n\t * @param str2       第二个字符串\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 子串是否相同\n\t * @since 3.2.1\n\t */\n\tpublic static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, boolean ignoreCase) {\n\t\treturn isSubEquals(str1, start1, str2, 0, str2.length(), ignoreCase);\n\t}\n\n\t/**\n\t * 截取两个字符串的不同部分（长度一致），判断截取的子串是否相同<br>\n\t * 任意一个字符串为null返回false\n\t *\n\t * @param str1       第一个字符串\n\t * @param start1     第一个字符串开始的位置\n\t * @param str2       第二个字符串\n\t * @param start2     第二个字符串开始的位置\n\t * @param length     截取长度\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 子串是否相同\n\t * @since 3.2.1\n\t */\n\tpublic static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) {\n\t\tif (null == str1 || null == str2) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length);\n\t}\n\n\t// ------------------------------------------------------------------------ format\n\n\t/**\n\t * 格式化文本, {} 表示占位符<br>\n\t * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>\n\t * 如果想输出 {} 使用 \\\\转义 { 即可，如果想输出 {} 之前的 \\ 使用双转义符 \\\\\\\\ 即可<br>\n\t * 例：<br>\n\t * 通常使用：format(\"this is {} for {}\", \"a\", \"b\") =》 this is a for b<br>\n\t * 转义{}： format(\"this is \\\\{} for {}\", \"a\", \"b\") =》 this is {} for a<br>\n\t * 转义\\： format(\"this is \\\\\\\\{} for {}\", \"a\", \"b\") =》 this is \\a for b<br>\n\t *\n\t * @param template 文本模板，被替换的部分用 {} 表示，如果模板为null，返回\"null\"\n\t * @param params   参数值\n\t * @return 格式化后的文本，如果模板为null，返回\"null\"\n\t */\n\tpublic static String format(CharSequence template, Object... params) {\n\t\tif (null == template) {\n\t\t\treturn NULL;\n\t\t}\n\t\tif (ArrayUtil.isEmpty(params) || isBlank(template)) {\n\t\t\treturn template.toString();\n\t\t}\n\t\treturn StrFormatter.format(template.toString(), params);\n\t}\n\n\t/**\n\t * 有序的格式化文本，使用{number}做为占位符<br>\n\t * 通常使用：format(\"this is {0} for {1}\", \"a\", \"b\") =》 this is a for b<br>\n\t *\n\t * @param pattern   文本格式\n\t * @param arguments 参数\n\t * @return 格式化后的文本\n\t */\n\tpublic static String indexedFormat(CharSequence pattern, Object... arguments) {\n\t\treturn MessageFormat.format(pattern.toString(), arguments);\n\t}\n\t// ------------------------------------------------------------------------ bytes\n\n\t/**\n\t * 编码字符串，编码为UTF-8\n\t *\n\t * @param str 字符串\n\t * @return 编码后的字节码\n\t */\n\tpublic static byte[] utf8Bytes(CharSequence str) {\n\t\treturn bytes(str, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 编码字符串<br>\n\t * 使用系统默认编码\n\t *\n\t * @param str 字符串\n\t * @return 编码后的字节码\n\t */\n\tpublic static byte[] bytes(CharSequence str) {\n\t\treturn bytes(str, Charset.defaultCharset());\n\t}\n\n\t/**\n\t * 编码字符串\n\t *\n\t * @param str     字符串\n\t * @param charset 字符集，如果此字段为空，则解码的结果取决于平台\n\t * @return 编码后的字节码\n\t */\n\tpublic static byte[] bytes(CharSequence str, String charset) {\n\t\treturn bytes(str, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset));\n\t}\n\n\t/**\n\t * 编码字符串\n\t *\n\t * @param str     字符串\n\t * @param charset 字符集，如果此字段为空，则解码的结果取决于平台\n\t * @return 编码后的字节码\n\t */\n\tpublic static byte[] bytes(CharSequence str, Charset charset) {\n\t\tif (str == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (null == charset) {\n\t\t\treturn str.toString().getBytes();\n\t\t}\n\t\treturn str.toString().getBytes(charset);\n\t}\n\n\t/**\n\t * 字符串转换为byteBuffer\n\t *\n\t * @param str     字符串\n\t * @param charset 编码\n\t * @return byteBuffer\n\t */\n\tpublic static ByteBuffer byteBuffer(CharSequence str, String charset) {\n\t\treturn ByteBuffer.wrap(bytes(str, charset));\n\t}\n\n\t// ------------------------------------------------------------------------ wrap\n\n\t/**\n\t * 包装指定字符串<br>\n\t * 当前缀和后缀一致时使用此方法\n\t *\n\t * @param str             被包装的字符串\n\t * @param prefixAndSuffix 前缀和后缀\n\t * @return 包装后的字符串\n\t * @since 3.1.0\n\t */\n\tpublic static String wrap(CharSequence str, CharSequence prefixAndSuffix) {\n\t\treturn wrap(str, prefixAndSuffix, prefixAndSuffix);\n\t}\n\n\t/**\n\t * 包装指定字符串\n\t *\n\t * @param str    被包装的字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 包装后的字符串\n\t */\n\tpublic static String wrap(CharSequence str, CharSequence prefix, CharSequence suffix) {\n\t\treturn nullToEmpty(prefix).concat(nullToEmpty(str)).concat(nullToEmpty(suffix));\n\t}\n\n\t/**\n\t * 使用单个字符包装多个字符串\n\t *\n\t * @param prefixAndSuffix 前缀和后缀\n\t * @param strs            多个字符串\n\t * @return 包装的字符串数组\n\t * @since 5.4.1\n\t */\n\tpublic static String[] wrapAllWithPair(CharSequence prefixAndSuffix, CharSequence... strs) {\n\t\treturn wrapAll(prefixAndSuffix, prefixAndSuffix, strs);\n\t}\n\n\t/**\n\t * 包装多个字符串\n\t *\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @param strs   多个字符串\n\t * @return 包装的字符串数组\n\t * @since 4.0.7\n\t */\n\tpublic static String[] wrapAll(CharSequence prefix, CharSequence suffix, CharSequence... strs) {\n\t\tfinal String[] results = new String[strs.length];\n\t\tfor (int i = 0; i < strs.length; i++) {\n\t\t\tresults[i] = wrap(strs[i], prefix, suffix);\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * 包装指定字符串，如果前缀或后缀已经包含对应的字符串，则不再包装\n\t *\n\t * @param str    被包装的字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 包装后的字符串\n\t */\n\tpublic static String wrapIfMissing(CharSequence str, CharSequence prefix, CharSequence suffix) {\n\t\tint len = 0;\n\t\tif (isNotEmpty(str)) {\n\t\t\tlen += str.length();\n\t\t}\n\t\tif (isNotEmpty(prefix)) {\n\t\t\tlen += prefix.length();\n\t\t}\n\t\tif (isNotEmpty(suffix)) {\n\t\t\tlen += suffix.length();\n\t\t}\n\t\tStringBuilder sb = new StringBuilder(len);\n\t\tif (isNotEmpty(prefix) && false == startWith(str, prefix)) {\n\t\t\tsb.append(prefix);\n\t\t}\n\t\tif (isNotEmpty(str)) {\n\t\t\tsb.append(str);\n\t\t}\n\t\tif (isNotEmpty(suffix) && false == endWith(str, suffix)) {\n\t\t\tsb.append(suffix);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 使用成对的字符包装多个字符串，如果已经包装，则不再包装\n\t *\n\t * @param prefixAndSuffix 前缀和后缀\n\t * @param strs            多个字符串\n\t * @return 包装的字符串数组\n\t * @since 5.4.1\n\t */\n\tpublic static String[] wrapAllWithPairIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) {\n\t\treturn wrapAllIfMissing(prefixAndSuffix, prefixAndSuffix, strs);\n\t}\n\n\t/**\n\t * 包装多个字符串，如果已经包装，则不再包装\n\t *\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @param strs   多个字符串\n\t * @return 包装的字符串数组\n\t * @since 4.0.7\n\t */\n\tpublic static String[] wrapAllIfMissing(CharSequence prefix, CharSequence suffix, CharSequence... strs) {\n\t\tfinal String[] results = new String[strs.length];\n\t\tfor (int i = 0; i < strs.length; i++) {\n\t\t\tresults[i] = wrapIfMissing(strs[i], prefix, suffix);\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * 去掉字符包装，如果未被包装则返回原字符串\n\t *\n\t * @param str    字符串\n\t * @param prefix 前置字符串\n\t * @param suffix 后置字符串\n\t * @return 去掉包装字符的字符串\n\t * @since 4.0.1\n\t */\n\tpublic static String unWrap(CharSequence str, String prefix, String suffix) {\n\t\tif (isWrap(str, prefix, suffix)) {\n\t\t\treturn sub(str, prefix.length(), str.length() - suffix.length());\n\t\t}\n\t\treturn str.toString();\n\t}\n\n\t/**\n\t * 去掉字符包装，如果未被包装则返回原字符串\n\t *\n\t * @param str    字符串\n\t * @param prefix 前置字符\n\t * @param suffix 后置字符\n\t * @return 去掉包装字符的字符串\n\t * @since 4.0.1\n\t */\n\tpublic static String unWrap(CharSequence str, char prefix, char suffix) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tif (str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix) {\n\t\t\treturn sub(str, 1, str.length() - 1);\n\t\t}\n\t\treturn str.toString();\n\t}\n\n\t/**\n\t * 去掉字符包装，如果未被包装则返回原字符串\n\t *\n\t * @param str             字符串\n\t * @param prefixAndSuffix 前置和后置字符\n\t * @return 去掉包装字符的字符串\n\t * @since 4.0.1\n\t */\n\tpublic static String unWrap(CharSequence str, char prefixAndSuffix) {\n\t\treturn unWrap(str, prefixAndSuffix, prefixAndSuffix);\n\t}\n\n\t/**\n\t * 指定字符串是否被包装\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 是否被包装\n\t */\n\tpublic static boolean isWrap(CharSequence str, String prefix, String suffix) {\n\t\tif (ArrayUtil.hasNull(str, prefix, suffix)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal String str2 = str.toString();\n\t\treturn str2.startsWith(prefix) && str2.endsWith(suffix);\n\t}\n\n\t/**\n\t * 指定字符串是否被同一字符包装（前后都有这些字符串）\n\t *\n\t * @param str     字符串\n\t * @param wrapper 包装字符串\n\t * @return 是否被包装\n\t */\n\tpublic static boolean isWrap(CharSequence str, String wrapper) {\n\t\treturn isWrap(str, wrapper, wrapper);\n\t}\n\n\t/**\n\t * 指定字符串是否被同一字符包装（前后都有这些字符串）\n\t *\n\t * @param str     字符串\n\t * @param wrapper 包装字符\n\t * @return 是否被包装\n\t */\n\tpublic static boolean isWrap(CharSequence str, char wrapper) {\n\t\treturn isWrap(str, wrapper, wrapper);\n\t}\n\n\t/**\n\t * 指定字符串是否被包装\n\t *\n\t * @param str        字符串\n\t * @param prefixChar 前缀\n\t * @param suffixChar 后缀\n\t * @return 是否被包装\n\t */\n\tpublic static boolean isWrap(CharSequence str, char prefixChar, char suffixChar) {\n\t\tif (null == str || str.length() < 2) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn str.charAt(0) == prefixChar && str.charAt(str.length() - 1) == suffixChar;\n\t}\n\n\t// ------------------------------------------------------------------------ pad\n\n\t/**\n\t * 补充字符串以满足指定长度，如果提供的字符串大于指定长度，截断之\n\t * 同：leftPad (org.apache.commons.lang3.leftPad)\n\t *\n\t * <pre>\n\t * CharSequenceUtil.padPre(null, *, *);//null\n\t * CharSequenceUtil.padPre(\"1\", 3, \"ABC\");//\"AB1\"\n\t * CharSequenceUtil.padPre(\"123\", 2, \"ABC\");//\"12\"\n\t * CharSequenceUtil.padPre(\"1039\", -1, \"0\");//\"103\"\n\t * </pre>\n\t *\n\t * @param str    字符串\n\t * @param length 长度\n\t * @param padStr 补充的字符\n\t * @return 补充后的字符串\n\t */\n\tpublic static String padPre(CharSequence str, int length, CharSequence padStr) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen == length) {\n\t\t\treturn str.toString();\n\t\t} else if (strLen > length) {\n\t\t\t//如果提供的字符串大于指定长度，截断之\n\t\t\treturn subPre(str, length);\n\t\t}\n\n\t\treturn repeatByLength(padStr, length - strLen).concat(str.toString());\n\t}\n\n\t/**\n\t * 补充字符串以满足最小长度，如果提供的字符串大于指定长度，截断之\n\t * 同：leftPad (org.apache.commons.lang3.leftPad)\n\t *\n\t * <pre>\n\t * CharSequenceUtil.padPre(null, *, *);//null\n\t * CharSequenceUtil.padPre(\"1\", 3, '0');//\"001\"\n\t * CharSequenceUtil.padPre(\"123\", 2, '0');//\"12\"\n\t * </pre>\n\t *\n\t * @param str     字符串\n\t * @param length  长度\n\t * @param padChar 补充的字符\n\t * @return 补充后的字符串\n\t */\n\tpublic static String padPre(CharSequence str, int length, char padChar) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen == length) {\n\t\t\treturn str.toString();\n\t\t} else if (strLen > length) {\n\t\t\t//如果提供的字符串大于指定长度，截断之\n\t\t\treturn subPre(str, length);\n\t\t}\n\n\t\treturn repeat(padChar, length - strLen).concat(str.toString());\n\t}\n\n\t/**\n\t * 补充字符串以满足最小长度，如果提供的字符串大于指定长度，截断之\n\t *\n\t * <pre>\n\t * CharSequenceUtil.padAfter(null, *, *);//null\n\t * CharSequenceUtil.padAfter(\"1\", 3, '0');//\"100\"\n\t * CharSequenceUtil.padAfter(\"123\", 2, '0');//\"23\"\n\t * CharSequenceUtil.padAfter(\"123\", -1, '0')//\"\" 空串\n\t * </pre>\n\t *\n\t * @param str     字符串，如果为{@code null}，直接返回null\n\t * @param length  长度\n\t * @param padChar 补充的字符\n\t * @return 补充后的字符串\n\t */\n\tpublic static String padAfter(CharSequence str, int length, char padChar) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen == length) {\n\t\t\treturn str.toString();\n\t\t} else if (strLen > length) {\n\t\t\t//如果提供的字符串大于指定长度，截断之\n\t\t\treturn sub(str, strLen - length, strLen);\n\t\t}\n\n\t\treturn str.toString().concat(repeat(padChar, length - strLen));\n\t}\n\n\t/**\n\t * 补充字符串以满足最小长度\n\t *\n\t * <pre>\n\t * CharSequenceUtil.padAfter(null, *, *);//null\n\t * CharSequenceUtil.padAfter(\"1\", 3, \"ABC\");//\"1AB\"\n\t * CharSequenceUtil.padAfter(\"123\", 2, \"ABC\");//\"23\"\n\t * </pre>\n\t *\n\t * @param str    字符串，如果为{@code null}，直接返回null\n\t * @param length 长度\n\t * @param padStr 补充的字符\n\t * @return 补充后的字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String padAfter(CharSequence str, int length, CharSequence padStr) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen == length) {\n\t\t\treturn str.toString();\n\t\t} else if (strLen > length) {\n\t\t\t//如果提供的字符串大于指定长度，截断之\n\t\t\treturn subSufByLength(str, length);\n\t\t}\n\n\t\treturn str.toString().concat(repeatByLength(padStr, length - strLen));\n\t}\n\n\t// ------------------------------------------------------------------------ center\n\n\t/**\n\t * 居中字符串，两边补充空格符，如果指定长度小于字符串，则返回原字符串\n\t *\n\t * <pre>\n\t * CharSequenceUtil.center(null, *)   = null\n\t * CharSequenceUtil.center(\"\", 4)     = \"    \"\n\t * CharSequenceUtil.center(\"ab\", -1)  = \"ab\"\n\t * CharSequenceUtil.center(\"ab\", 4)   = \" ab \"\n\t * CharSequenceUtil.center(\"abcd\", 2) = \"abcd\"\n\t * CharSequenceUtil.center(\"a\", 4)    = \" a  \"\n\t * </pre>\n\t *\n\t * @param str  字符串\n\t * @param size 指定长度\n\t * @return 补充后的字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String center(CharSequence str, final int size) {\n\t\treturn center(str, size, CharUtil.SPACE);\n\t}\n\n\t/**\n\t * 居中字符串，两边补充指定字符，如果指定长度小于字符串，则返回原字符串\n\t *\n\t * <pre>\n\t * CharSequenceUtil.center(null, *, *)     = null\n\t * CharSequenceUtil.center(\"\", 4, ' ')     = \"    \"\n\t * CharSequenceUtil.center(\"ab\", -1, ' ')  = \"ab\"\n\t * CharSequenceUtil.center(\"ab\", 4, ' ')   = \" ab \"\n\t * CharSequenceUtil.center(\"abcd\", 2, ' ') = \"abcd\"\n\t * CharSequenceUtil.center(\"a\", 4, ' ')    = \" a  \"\n\t * CharSequenceUtil.center(\"a\", 4, 'y')   = \"yayy\"\n\t * CharSequenceUtil.center(\"abc\", 7, ' ')   = \"  abc  \"\n\t * </pre>\n\t *\n\t * @param str     字符串\n\t * @param size    指定长度\n\t * @param padChar 两边补充的字符\n\t * @return 补充后的字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String center(CharSequence str, final int size, char padChar) {\n\t\tif (str == null || size <= 0) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tfinal int pads = size - strLen;\n\t\tif (pads <= 0) {\n\t\t\treturn str.toString();\n\t\t}\n\t\tstr = padPre(str, strLen + pads / 2, padChar);\n\t\tstr = padAfter(str, size, padChar);\n\t\treturn str.toString();\n\t}\n\n\t/**\n\t * 居中字符串，两边补充指定字符串，如果指定长度小于字符串，则返回原字符串\n\t *\n\t * <pre>\n\t * CharSequenceUtil.center(null, *, *)     = null\n\t * CharSequenceUtil.center(\"\", 4, \" \")     = \"    \"\n\t * CharSequenceUtil.center(\"ab\", -1, \" \")  = \"ab\"\n\t * CharSequenceUtil.center(\"ab\", 4, \" \")   = \" ab \"\n\t * CharSequenceUtil.center(\"abcd\", 2, \" \") = \"abcd\"\n\t * CharSequenceUtil.center(\"a\", 4, \" \")    = \" a  \"\n\t * CharSequenceUtil.center(\"a\", 4, \"yz\")   = \"yayz\"\n\t * CharSequenceUtil.center(\"abc\", 7, null) = \"  abc  \"\n\t * CharSequenceUtil.center(\"abc\", 7, \"\")   = \"  abc  \"\n\t * </pre>\n\t *\n\t * @param str    字符串\n\t * @param size   指定长度\n\t * @param padStr 两边补充的字符串\n\t * @return 补充后的字符串\n\t */\n\tpublic static String center(CharSequence str, final int size, CharSequence padStr) {\n\t\tif (str == null || size <= 0) {\n\t\t\treturn str(str);\n\t\t}\n\t\tif (isEmpty(padStr)) {\n\t\t\tpadStr = SPACE;\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tfinal int pads = size - strLen;\n\t\tif (pads <= 0) {\n\t\t\treturn str.toString();\n\t\t}\n\t\tstr = padPre(str, strLen + pads / 2, padStr);\n\t\tstr = padAfter(str, size, padStr);\n\t\treturn str.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ str\n\n\t/**\n\t * {@link CharSequence} 转为字符串，null安全\n\t *\n\t * @param cs {@link CharSequence}\n\t * @return 字符串\n\t */\n\tpublic static String str(CharSequence cs) {\n\t\treturn null == cs ? null : cs.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ count\n\n\t/**\n\t * 统计指定内容中包含指定字符串的数量<br>\n\t * 参数为 {@code null} 或者 \"\" 返回 {@code 0}.\n\t *\n\t * <pre>\n\t * CharSequenceUtil.count(null, *)       = 0\n\t * CharSequenceUtil.count(\"\", *)         = 0\n\t * CharSequenceUtil.count(\"abba\", null)  = 0\n\t * CharSequenceUtil.count(\"abba\", \"\")    = 0\n\t * CharSequenceUtil.count(\"abba\", \"a\")   = 2\n\t * CharSequenceUtil.count(\"abba\", \"ab\")  = 1\n\t * CharSequenceUtil.count(\"abba\", \"xxx\") = 0\n\t * </pre>\n\t *\n\t * @param content      被查找的字符串\n\t * @param strForSearch 需要查找的字符串\n\t * @return 查找到的个数\n\t */\n\tpublic static int count(CharSequence content, CharSequence strForSearch) {\n\t\tif (hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tint count = 0;\n\t\tint idx = 0;\n\t\tfinal String content2 = content.toString();\n\t\tfinal String strForSearch2 = strForSearch.toString();\n\t\twhile ((idx = content2.indexOf(strForSearch2, idx)) > -1) {\n\t\t\tcount++;\n\t\t\tidx += strForSearch.length();\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 统计指定内容中包含指定字符的数量\n\t *\n\t * @param content       内容\n\t * @param charForSearch 被统计的字符\n\t * @return 包含数量\n\t */\n\tpublic static int count(CharSequence content, char charForSearch) {\n\t\tint count = 0;\n\t\tif (isEmpty(content)) {\n\t\t\treturn 0;\n\t\t}\n\t\tint contentLength = content.length();\n\t\tfor (int i = 0; i < contentLength; i++) {\n\t\t\tif (charForSearch == content.charAt(i)) {\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t// ------------------------------------------------------------------------ compare\n\n\t/**\n\t * 比较两个字符串，用于排序\n\t *\n\t * <pre>\n\t * CharSequenceUtil.compare(null, null, *)     = 0\n\t * CharSequenceUtil.compare(null , \"a\", true)  &lt; 0\n\t * CharSequenceUtil.compare(null , \"a\", false) &gt; 0\n\t * CharSequenceUtil.compare(\"a\", null, true)   &gt; 0\n\t * CharSequenceUtil.compare(\"a\", null, false)  &lt; 0\n\t * CharSequenceUtil.compare(\"abc\", \"abc\", *)   = 0\n\t * CharSequenceUtil.compare(\"a\", \"b\", *)       &lt; 0\n\t * CharSequenceUtil.compare(\"b\", \"a\", *)       &gt; 0\n\t * CharSequenceUtil.compare(\"a\", \"B\", *)       &gt; 0\n\t * CharSequenceUtil.compare(\"ab\", \"abc\", *)    &lt; 0\n\t * </pre>\n\t *\n\t * @param str1       字符串1\n\t * @param str2       字符串2\n\t * @param nullIsLess {@code null} 值是否排在前（null是否小于非空值）\n\t * @return 排序值。负数：str1 &lt; str2，正数：str1 &gt; str2, 0：str1 == str2\n\t */\n\tpublic static int compare(final CharSequence str1, final CharSequence str2, final boolean nullIsLess) {\n\t\tif (str1 == str2) {\n\t\t\treturn 0;\n\t\t}\n\t\tif (str1 == null) {\n\t\t\treturn nullIsLess ? -1 : 1;\n\t\t}\n\t\tif (str2 == null) {\n\t\t\treturn nullIsLess ? 1 : -1;\n\t\t}\n\t\treturn str1.toString().compareTo(str2.toString());\n\t}\n\n\t/**\n\t * 比较两个字符串，用于排序，大小写不敏感\n\t *\n\t * <pre>\n\t * CharSequenceUtil.compareIgnoreCase(null, null, *)     = 0\n\t * CharSequenceUtil.compareIgnoreCase(null , \"a\", true)  &lt; 0\n\t * CharSequenceUtil.compareIgnoreCase(null , \"a\", false) &gt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"a\", null, true)   &gt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"a\", null, false)  &lt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"abc\", \"abc\", *)   = 0\n\t * CharSequenceUtil.compareIgnoreCase(\"abc\", \"ABC\", *)   = 0\n\t * CharSequenceUtil.compareIgnoreCase(\"a\", \"b\", *)       &lt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"b\", \"a\", *)       &gt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"a\", \"B\", *)       &lt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"A\", \"b\", *)       &lt; 0\n\t * CharSequenceUtil.compareIgnoreCase(\"ab\", \"abc\", *)    &lt; 0\n\t * </pre>\n\t *\n\t * @param str1       字符串1\n\t * @param str2       字符串2\n\t * @param nullIsLess {@code null} 值是否排在前（null是否小于非空值）\n\t * @return 排序值。负数：str1 &lt; str2，正数：str1 &gt; str2, 0：str1 == str2\n\t */\n\tpublic static int compareIgnoreCase(CharSequence str1, CharSequence str2, boolean nullIsLess) {\n\t\tif (str1 == str2) {\n\t\t\treturn 0;\n\t\t}\n\t\tif (str1 == null) {\n\t\t\treturn nullIsLess ? -1 : 1;\n\t\t}\n\t\tif (str2 == null) {\n\t\t\treturn nullIsLess ? 1 : -1;\n\t\t}\n\t\treturn str1.toString().compareToIgnoreCase(str2.toString());\n\t}\n\n\t/**\n\t * 比较两个版本<br>\n\t * null版本排在最小：即：\n\t *\n\t * <pre>\n\t * CharSequenceUtil.compareVersion(null, \"v1\") &lt; 0\n\t * CharSequenceUtil.compareVersion(\"v1\", \"v1\")  = 0\n\t * CharSequenceUtil.compareVersion(null, null)   = 0\n\t * CharSequenceUtil.compareVersion(\"v1\", null) &gt; 0\n\t * CharSequenceUtil.compareVersion(\"1.0.0\", \"1.0.2\") &lt; 0\n\t * CharSequenceUtil.compareVersion(\"1.0.2\", \"1.0.2a\") &lt; 0\n\t * CharSequenceUtil.compareVersion(\"1.13.0\", \"1.12.1c\") &gt; 0\n\t * CharSequenceUtil.compareVersion(\"V0.0.20170102\", \"V0.0.20170101\") &gt; 0\n\t * </pre>\n\t *\n\t * @param version1 版本1\n\t * @param version2 版本2\n\t * @return 排序值。负数：version1 &lt; version2，正数：version1 &gt; version2, 0：version1 == version2\n\t * @since 4.0.2\n\t */\n\tpublic static int compareVersion(CharSequence version1, CharSequence version2) {\n\t\treturn VersionComparator.INSTANCE.compare(str(version1), str(version2));\n\t}\n\n\t// ------------------------------------------------------------------------ append and prepend\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为结尾，则在尾部添加结尾字符串<br>\n\t * 不忽略大小写\n\t *\n\t * @param str      被检查的字符串\n\t * @param suffix   需要添加到结尾的字符串\n\t * @param suffixes 需要额外检查的结尾字符串，如果以这些中的一个为结尾，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String appendIfMissing(CharSequence str, CharSequence suffix, CharSequence... suffixes) {\n\t\treturn appendIfMissing(str, suffix, false, suffixes);\n\t}\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为结尾，则在尾部添加结尾字符串<br>\n\t * 忽略大小写\n\t *\n\t * @param str      被检查的字符串\n\t * @param suffix   需要添加到结尾的字符串\n\t * @param suffixes 需要额外检查的结尾字符串，如果以这些中的一个为结尾，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String appendIfMissingIgnoreCase(CharSequence str, CharSequence suffix, CharSequence... suffixes) {\n\t\treturn appendIfMissing(str, suffix, true, suffixes);\n\t}\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为结尾，则在尾部添加结尾字符串\n\t *\n\t * @param str          被检查的字符串\n\t * @param suffix       需要添加到结尾的字符串，不参与检查匹配\n\t * @param ignoreCase   检查结尾时是否忽略大小写\n\t * @param testSuffixes 需要额外检查的结尾字符串，如果以这些中的一个为结尾，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String appendIfMissing(CharSequence str, CharSequence suffix, boolean ignoreCase, CharSequence... testSuffixes) {\n\t\tif (str == null || isEmpty(suffix) || endWith(str, suffix, ignoreCase)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tif (ArrayUtil.isNotEmpty(testSuffixes)) {\n\t\t\tfor (final CharSequence testSuffix : testSuffixes) {\n\t\t\t\tif (endWith(str, testSuffix, ignoreCase)) {\n\t\t\t\t\treturn str.toString();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn str.toString().concat(suffix.toString());\n\t}\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为开头，则在首部添加起始字符串<br>\n\t * 不忽略大小写\n\t *\n\t * @param str      被检查的字符串\n\t * @param prefix   需要添加到首部的字符串\n\t * @param prefixes 需要额外检查的首部字符串，如果以这些中的一个为起始，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String prependIfMissing(CharSequence str, CharSequence prefix, CharSequence... prefixes) {\n\t\treturn prependIfMissing(str, prefix, false, prefixes);\n\t}\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为开头，则在首部添加起始字符串<br>\n\t * 忽略大小写\n\t *\n\t * @param str      被检查的字符串\n\t * @param prefix   需要添加到首部的字符串\n\t * @param prefixes 需要额外检查的首部字符串，如果以这些中的一个为起始，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String prependIfMissingIgnoreCase(CharSequence str, CharSequence prefix, CharSequence... prefixes) {\n\t\treturn prependIfMissing(str, prefix, true, prefixes);\n\t}\n\n\t/**\n\t * 如果给定字符串不是以给定的一个或多个字符串为开头，则在首部添加起始字符串\n\t *\n\t * @param str        被检查的字符串\n\t * @param prefix     需要添加到首部的字符串\n\t * @param ignoreCase 检查结尾时是否忽略大小写\n\t * @param prefixes   需要额外检查的首部字符串，如果以这些中的一个为起始，则不再添加\n\t * @return 如果已经结尾，返回原字符串，否则返回添加结尾的字符串\n\t * @since 3.0.7\n\t */\n\tpublic static String prependIfMissing(CharSequence str, CharSequence prefix, boolean ignoreCase, CharSequence... prefixes) {\n\t\tif (str == null || isEmpty(prefix) || startWith(str, prefix, ignoreCase)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tif (prefixes != null) {\n\t\t\tfor (final CharSequence s : prefixes) {\n\t\t\t\tif (startWith(str, s, ignoreCase)) {\n\t\t\t\t\treturn str.toString();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn prefix.toString().concat(str.toString());\n\t}\n\n\t// ------------------------------------------------------------------------ replace\n\n\t/**\n\t * 替换字符串中的指定字符串，忽略大小写\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacement 被替换的字符串\n\t * @return 替换后的字符串\n\t * @since 4.0.3\n\t */\n\tpublic static String replaceIgnoreCase(CharSequence str, CharSequence searchStr, CharSequence replacement) {\n\t\treturn replace(str, 0, searchStr, replacement, true);\n\t}\n\n\t/**\n\t * 替换字符串中的指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacement 被替换的字符串\n\t * @return 替换后的字符串\n\t * @since 4.0.3\n\t */\n\tpublic static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement) {\n\t\treturn replace(str, 0, searchStr, replacement, false);\n\t}\n\n\t/**\n\t * 替换字符串中的指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacement 被替换的字符串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 替换后的字符串\n\t * @since 4.0.3\n\t */\n\tpublic static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) {\n\t\treturn replace(str, 0, searchStr, replacement, ignoreCase);\n\t}\n\n\t/**\n\t * 替换字符串中的指定字符串\n\t *\n\t * @param str         字符串\n\t * @param fromIndex   开始位置（包括）\n\t * @param searchStr   被查找的字符串\n\t * @param replacement 被替换的字符串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 替换后的字符串\n\t * @since 4.0.3\n\t */\n\tpublic static String replace(CharSequence str, int fromIndex, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) {\n\t\tif (isEmpty(str) || isEmpty(searchStr)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tif (null == replacement) {\n\t\t\treplacement = EMPTY;\n\t\t}\n\n\t\tfinal int strLength = str.length();\n\t\tfinal int searchStrLength = searchStr.length();\n\t\tif (strLength < searchStrLength) {\n\t\t\t// issue#I4M16G@Gitee\n\t\t\treturn str(str);\n\t\t}\n\n\t\tif (fromIndex > strLength) {\n\t\t\treturn str(str);\n\t\t} else if (fromIndex < 0) {\n\t\t\tfromIndex = 0;\n\t\t}\n\n\t\tfinal StringBuilder result = new StringBuilder(strLength - searchStrLength + replacement.length());\n\t\tif (0 != fromIndex) {\n\t\t\tresult.append(str.subSequence(0, fromIndex));\n\t\t}\n\n\t\tint preIndex = fromIndex;\n\t\tint index;\n\t\twhile ((index = indexOf(str, searchStr, preIndex, ignoreCase)) > -1) {\n\t\t\tresult.append(str.subSequence(preIndex, index));\n\t\t\tresult.append(replacement);\n\t\t\tpreIndex = index + searchStrLength;\n\t\t}\n\n\t\tif (preIndex < strLength) {\n\t\t\t// 结尾部分\n\t\t\tresult.append(str.subSequence(preIndex, strLength));\n\t\t}\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 替换指定字符串的指定区间内字符为固定字符<br>\n\t * 此方法使用{@link String#codePoints()}完成拆分替换\n\t *\n\t * @param str          字符串\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @param replacedChar 被替换的字符\n\t * @return 替换后的字符串\n\t * @since 3.2.1\n\t * @deprecated 歧义，请使用{@link #replaceByCodePoint(CharSequence, int, int, char)}\n\t */\n\t@Deprecated\n\tpublic static String replace(CharSequence str, int startInclude, int endExclude, char replacedChar) {\n\t\treturn replaceByCodePoint(str, startInclude, endExclude, replacedChar);\n\t}\n\n\t/**\n\t * 替换指定字符串的指定区间内字符为固定字符<br>\n\t * 此方法使用{@link String#codePoints()}完成拆分替换\n\t *\n\t * @param str          字符串\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @param replacedChar 被替换的字符\n\t * @return 替换后的字符串\n\t * @since 5.8.27\n\t */\n\tpublic static String replaceByCodePoint(CharSequence str, int startInclude, int endExclude, char replacedChar) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal String originalStr = str(str);\n\t\tint[] strCodePoints = originalStr.codePoints().toArray();\n\t\tfinal int strLength = strCodePoints.length;\n\t\tif (startInclude > strLength) {\n\t\t\treturn originalStr;\n\t\t}\n\t\tif (endExclude > strLength) {\n\t\t\tendExclude = strLength;\n\t\t}\n\t\tif (startInclude > endExclude) {\n\t\t\t// 如果起始位置大于结束位置，不替换\n\t\t\treturn originalStr;\n\t\t}\n\n\t\tfinal StringBuilder stringBuilder = new StringBuilder();\n\t\tfor (int i = 0; i < strLength; i++) {\n\t\t\tif (i >= startInclude && i < endExclude) {\n\t\t\t\tstringBuilder.append(replacedChar);\n\t\t\t} else {\n\t\t\t\tstringBuilder.append(new String(strCodePoints, i, 1));\n\t\t\t}\n\t\t}\n\t\treturn stringBuilder.toString();\n\t}\n\n\t/**\n\t * 替换指定字符串的指定区间内字符为指定字符串，字符串只重复一次<br>\n\t * 此方法使用{@link String#codePoints()}完成拆分替换\n\t *\n\t * @param str          字符串\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @param replacedStr  被替换的字符串\n\t * @return 替换后的字符串\n\t * @since 3.2.1\n\t * @deprecated 歧义，请使用{@link #replaceByCodePoint(CharSequence, int, int, CharSequence)}\n\t */\n\t@Deprecated\n\tpublic static String replace(CharSequence str, int startInclude, int endExclude, CharSequence replacedStr) {\n\t\treturn replaceByCodePoint(str, startInclude, endExclude, replacedStr);\n\t}\n\n\t/**\n\t * 替换指定字符串的指定区间内字符为指定字符串，字符串只重复一次<br>\n\t * 此方法使用{@link String#codePoints()}完成拆分替换\n\t *\n\t * @param str          字符串\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @param replacedStr  被替换的字符串\n\t * @return 替换后的字符串\n\t * @since 5.8.27\n\t */\n\tpublic static String replaceByCodePoint(CharSequence str, int startInclude, int endExclude, CharSequence replacedStr) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal String originalStr = str(str);\n\t\tint[] strCodePoints = originalStr.codePoints().toArray();\n\t\tfinal int strLength = strCodePoints.length;\n\t\tif (startInclude > strLength) {\n\t\t\treturn originalStr;\n\t\t}\n\t\tif (endExclude > strLength) {\n\t\t\tendExclude = strLength;\n\t\t}\n\t\tif (startInclude > endExclude) {\n\t\t\t// 如果起始位置大于结束位置，不替换\n\t\t\treturn originalStr;\n\t\t}\n\n\t\tfinal StringBuilder stringBuilder = new StringBuilder();\n\t\tfor (int i = 0; i < startInclude; i++) {\n\t\t\tstringBuilder.append(new String(strCodePoints, i, 1));\n\t\t}\n\t\tstringBuilder.append(replacedStr);\n\t\tfor (int i = endExclude; i < strLength; i++) {\n\t\t\tstringBuilder.append(new String(strCodePoints, i, 1));\n\t\t}\n\t\treturn stringBuilder.toString();\n\t}\n\n\t/**\n\t * 替换所有正则匹配的文本，并使用自定义函数决定如何替换<br>\n\t * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分，然后经过重新处理、组装变成新的内容放回原位。\n\t * <pre class=\"code\">\n\t *     replace(this.content, \"(\\\\d+)\", parameters -&gt; \"-\" + parameters.group(1) + \"-\")\n\t *     // 结果为：\"ZZZaaabbbccc中文-1234-\"\n\t * </pre>\n\t *\n\t * @param str        要替换的字符串\n\t * @param pattern    用于匹配的正则式\n\t * @param replaceFun 决定如何替换的函数\n\t * @return 替换后的字符串\n\t * @see ReUtil#replaceAll(CharSequence, java.util.regex.Pattern, Func1)\n\t * @since 4.2.2\n\t */\n\tpublic static String replace(CharSequence str, java.util.regex.Pattern pattern, Func1<java.util.regex.Matcher, String> replaceFun) {\n\t\treturn ReUtil.replaceAll(str, pattern, replaceFun);\n\t}\n\n\t/**\n\t * 替换所有正则匹配的文本，并使用自定义函数决定如何替换\n\t *\n\t * @param str        要替换的字符串\n\t * @param regex      用于匹配的正则式\n\t * @param replaceFun 决定如何替换的函数\n\t * @return 替换后的字符串\n\t * @see ReUtil#replaceAll(CharSequence, String, Func1)\n\t * @since 4.2.2\n\t */\n\tpublic static String replace(CharSequence str, String regex, Func1<java.util.regex.Matcher, String> replaceFun) {\n\t\treturn ReUtil.replaceAll(str, regex, replaceFun);\n\t}\n\n\t/**\n\t * 替换字符串中最后一个指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacedStr 被替换的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String replaceLast(CharSequence str, CharSequence searchStr, CharSequence replacedStr) {\n\t\treturn replaceLast(str, searchStr, replacedStr, false);\n\t}\n\n\t/**\n\t * 替换字符串中最后一个指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacedStr 被替换的字符串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 替换后的字符串\n\t */\n\tpublic static String replaceLast(CharSequence str, CharSequence searchStr, CharSequence replacedStr, boolean ignoreCase) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tint lastIndex = lastIndexOf(str, searchStr, str.length(), ignoreCase);\n\t\tif (INDEX_NOT_FOUND == lastIndex) {\n\t\t\treturn str(str);\n\t\t}\n\t\treturn replace(str, lastIndex, searchStr, replacedStr, ignoreCase);\n\t}\n\n\t/**\n\t * 替换字符串中第一个指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacedStr 被替换的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String replaceFirst(CharSequence str, CharSequence searchStr, CharSequence replacedStr) {\n\t\treturn replaceFirst(str, searchStr, replacedStr, false);\n\t}\n\n\t/**\n\t * 替换字符串中第一个指定字符串\n\t *\n\t * @param str         字符串\n\t * @param searchStr   被查找的字符串\n\t * @param replacedStr 被替换的字符串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 替换后的字符串\n\t */\n\tpublic static String replaceFirst(CharSequence str, CharSequence searchStr, CharSequence replacedStr, boolean ignoreCase) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tint startInclude = indexOf(str, searchStr, 0, ignoreCase);\n\t\tif (INDEX_NOT_FOUND == startInclude) {\n\t\t\treturn str(str);\n\t\t}\n\t\treturn replaceByCodePoint(str, startInclude, startInclude + searchStr.length(), replacedStr);\n\t}\n\n\t/**\n\t * 替换指定字符串的指定区间内字符为\"*\"\n\t * 俗称：脱敏功能，后面其他功能，可以见：DesensitizedUtil(脱敏工具类)\n\t *\n\t * <pre>\n\t * CharSequenceUtil.hide(null,*,*)=null\n\t * CharSequenceUtil.hide(\"\",0,*)=\"\"\n\t * CharSequenceUtil.hide(\"jackduan@163.com\",-1,4)   ****duan@163.com\n\t * CharSequenceUtil.hide(\"jackduan@163.com\",2,3)    ja*kduan@163.com\n\t * CharSequenceUtil.hide(\"jackduan@163.com\",3,2)    jackduan@163.com\n\t * CharSequenceUtil.hide(\"jackduan@163.com\",16,16)  jackduan@163.com\n\t * CharSequenceUtil.hide(\"jackduan@163.com\",16,17)  jackduan@163.com\n\t * </pre>\n\t *\n\t * @param str          字符串\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @return 替换后的字符串\n\t * @since 4.1.14\n\t */\n\tpublic static String hide(CharSequence str, int startInclude, int endExclude) {\n\t\treturn replaceByCodePoint(str, startInclude, endExclude, '*');\n\t}\n\n\t/**\n\t * 脱敏，使用默认的脱敏策略\n\t *\n\t * <pre>\n\t * CharSequenceUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.USER_ID)) =  \"0\"\n\t * CharSequenceUtil.desensitized(\"段正淳\", DesensitizedUtil.DesensitizedType.CHINESE_NAME)) = \"段**\"\n\t * CharSequenceUtil.desensitized(\"51343620000320711X\", DesensitizedUtil.DesensitizedType.ID_CARD)) = \"5***************1X\"\n\t * CharSequenceUtil.desensitized(\"09157518479\", DesensitizedUtil.DesensitizedType.FIXED_PHONE)) = \"0915*****79\"\n\t * CharSequenceUtil.desensitized(\"18049531999\", DesensitizedUtil.DesensitizedType.MOBILE_PHONE)) = \"180****1999\"\n\t * CharSequenceUtil.desensitized(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.DesensitizedType.ADDRESS)) = \"北京市海淀区马********\"\n\t * CharSequenceUtil.desensitized(\"duandazhi-jack@gmail.com.cn\", DesensitizedUtil.DesensitizedType.EMAIL)) = \"d*************@gmail.com.cn\"\n\t * CharSequenceUtil.desensitized(\"1234567890\", DesensitizedUtil.DesensitizedType.PASSWORD)) = \"**********\"\n\t * CharSequenceUtil.desensitized(\"苏D40000\", DesensitizedUtil.DesensitizedType.CAR_LICENSE)) = \"苏D4***0\"\n\t * CharSequenceUtil.desensitized(\"11011111222233333256\", DesensitizedType.BANK_CARD)) = \"1101 **** **** **** 3256\"\n\t * </pre>\n\t *\n\t * @param str              字符串\n\t * @param desensitizedType 脱敏类型;可以脱敏：用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码\n\t * @return 脱敏之后的字符串\n\t * @author dazer and neusoft and qiaomu\n\t * @see DesensitizedUtil 如果需要自定义，脱敏规则，请使用该工具类；\n\t * @since 5.6.2\n\t */\n\tpublic static String desensitized(CharSequence str, DesensitizedUtil.DesensitizedType desensitizedType) {\n\t\treturn DesensitizedUtil.desensitized(str, desensitizedType);\n\t}\n\n\t/**\n\t * 替换字符字符数组中所有的字符为replacedStr<br>\n\t * 提供的chars为所有需要被替换的字符，例如：\"\\r\\n\"，则\"\\r\"和\"\\n\"都会被替换，哪怕他们单独存在\n\t *\n\t * @param str         被检查的字符串\n\t * @param chars       需要替换的字符列表，用一个字符串表示这个字符列表\n\t * @param replacedStr 替换成的字符串\n\t * @return 新字符串\n\t * @since 3.2.2\n\t */\n\tpublic static String replaceChars(CharSequence str, String chars, CharSequence replacedStr) {\n\t\tif (isEmpty(str) || isEmpty(chars)) {\n\t\t\treturn str(str);\n\t\t}\n\t\treturn replaceChars(str, chars.toCharArray(), replacedStr);\n\t}\n\n\t/**\n\t * 替换字符字符数组中所有的字符为replacedStr\n\t *\n\t * @param str         被检查的字符串\n\t * @param chars       需要替换的字符列表\n\t * @param replacedStr 替换成的字符串\n\t * @return 新字符串\n\t * @since 3.2.2\n\t */\n\tpublic static String replaceChars(CharSequence str, char[] chars, CharSequence replacedStr) {\n\t\tif (isEmpty(str) || ArrayUtil.isEmpty(chars)) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tfinal Set<Character> set = new HashSet<>(chars.length);\n\t\tfor (char c : chars) {\n\t\t\tset.add(c);\n\t\t}\n\t\tint strLen = str.length();\n\t\tfinal StringBuilder builder = new StringBuilder();\n\t\tchar c;\n\t\tfor (int i = 0; i < strLen; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tbuilder.append(set.contains(c) ? replacedStr : c);\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ length\n\n\t/**\n\t * 获取字符串的长度，如果为null返回0\n\t *\n\t * @param cs a 字符串\n\t * @return 字符串的长度，如果为null返回0\n\t * @since 4.3.2\n\t */\n\tpublic static int length(CharSequence cs) {\n\t\treturn cs == null ? 0 : cs.length();\n\t}\n\n\t/**\n\t * 给定字符串转为bytes后的byte数（byte长度）\n\t *\n\t * @param cs      字符串\n\t * @param charset 编码\n\t * @return byte长度\n\t * @since 4.5.2\n\t */\n\tpublic static int byteLength(CharSequence cs, Charset charset) {\n\t\treturn cs == null ? 0 : cs.toString().getBytes(charset).length;\n\t}\n\n\t/**\n\t * 给定字符串数组的总长度<br>\n\t * null字符长度定义为0\n\t *\n\t * @param strs 字符串数组\n\t * @return 总长度\n\t * @since 4.0.1\n\t */\n\tpublic static int totalLength(CharSequence... strs) {\n\t\tint totalLength = 0;\n\t\tfor (CharSequence str : strs) {\n\t\t\ttotalLength += (null == str ? 0 : str.length());\n\t\t}\n\t\treturn totalLength;\n\t}\n\n\t/**\n\t * 限制字符串长度，如果超过指定长度，截取指定长度并在末尾加\"...\"\n\t *\n\t * @param string 字符串\n\t * @param length 最大长度\n\t * @return 切割后的剩余的前半部分字符串+\"...\"\n\t * @since 4.0.10\n\t */\n\tpublic static String maxLength(CharSequence string, int length) {\n\t\tAssert.isTrue(length > 0);\n\t\tif (null == string) {\n\t\t\treturn null;\n\t\t}\n\t\tif (string.length() <= length) {\n\t\t\treturn string.toString();\n\t\t}\n\t\treturn sub(string, 0, length) + \"...\";\n\t}\n\n\t// ------------------------------------------------------------------------ firstXXX\n\n\t/**\n\t * 返回第一个非{@code null} 元素\n\t *\n\t * @param strs 多个元素\n\t * @param <T>  元素类型\n\t * @return 第一个非空元素，如果给定的数组为空或者都为空，返回{@code null}\n\t * @since 5.4.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends CharSequence> T firstNonNull(T... strs) {\n\t\treturn ArrayUtil.firstNonNull(strs);\n\t}\n\n\t/**\n\t * 返回第一个非empty 元素\n\t *\n\t * @param strs 多个元素\n\t * @param <T>  元素类型\n\t * @return 第一个非空元素，如果给定的数组为空或者都为空，返回{@code null}\n\t * @see #isNotEmpty(CharSequence)\n\t * @since 5.4.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends CharSequence> T firstNonEmpty(T... strs) {\n\t\treturn ArrayUtil.firstMatch(CharSequenceUtil::isNotEmpty, strs);\n\t}\n\n\t/**\n\t * 返回第一个非blank 元素\n\t *\n\t * @param strs 多个元素\n\t * @param <T>  元素类型\n\t * @return 第一个非空元素，如果给定的数组为空或者都为空，返回{@code null}\n\t * @see #isNotBlank(CharSequence)\n\t * @since 5.4.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends CharSequence> T firstNonBlank(T... strs) {\n\t\treturn ArrayUtil.firstMatch(CharSequenceUtil::isNotBlank, strs);\n\t}\n\n\t// ------------------------------------------------------------------------ lower and upper\n\n\t/**\n\t * 将字符串转为小写\n\t *\n\t * @param str 被转的字符串\n\t * @return 转换后的字符串\n\t * @see String#toLowerCase()\n\t * @since 5.8.38\n\t * @deprecated 拼写错误，请使用 {@link #toLowerCase(CharSequence)}\n\t */\n\t@Deprecated\n\tpublic static String toLoweCase(final CharSequence str) {\n\t\treturn toLowerCase(str);\n\t}\n\n\t/**\n\t * 将字符串转为小写\n\t *\n\t * @param str 被转的字符串\n\t * @return 转换后的字符串\n\t * @see String#toLowerCase()\n\t * @since 5.8.39\n\t */\n\tpublic static String toLowerCase(final CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (0 == str.length()) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.toString().toLowerCase();\n\t}\n\n\t/**\n\t * 将字符串转为大写\n\t *\n\t * @param str 被转的字符串\n\t * @return 转换后的字符串\n\t * @see String#toUpperCase()\n\t * @since 5.8.38\n\t */\n\tpublic static String toUpperCase(final CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (0 == str.length()) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\treturn str.toString().toUpperCase();\n\t}\n\n\t/**\n\t * 原字符串首字母大写并在其首部添加指定字符串 例如：str=name, preString=get =》 return getName\n\t *\n\t * @param str       被处理的字符串\n\t * @param preString 添加的首部\n\t * @return 处理后的字符串\n\t */\n\tpublic static String upperFirstAndAddPre(CharSequence str, String preString) {\n\t\tif (str == null || preString == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn preString + upperFirst(str);\n\t}\n\n\t/**\n\t * 大写首字母<br>\n\t * 例如：str = name, return Name\n\t *\n\t * @param str 字符串\n\t * @return 字符串\n\t */\n\tpublic static String upperFirst(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (str.length() > 0) {\n\t\t\tchar firstChar = str.charAt(0);\n\t\t\tif (Character.isLowerCase(firstChar)) {\n\t\t\t\treturn Character.toUpperCase(firstChar) + subSuf(str, 1);\n\t\t\t}\n\t\t}\n\t\treturn str.toString();\n\t}\n\n\t/**\n\t * 小写首字母<br>\n\t * 例如：str = Name, return name\n\t *\n\t * @param str 字符串\n\t * @return 字符串\n\t */\n\tpublic static String lowerFirst(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tif (str.length() > 0) {\n\t\t\tchar firstChar = str.charAt(0);\n\t\t\tif (Character.isUpperCase(firstChar)) {\n\t\t\t\treturn Character.toLowerCase(firstChar) + subSuf(str, 1);\n\t\t\t}\n\t\t}\n\t\treturn str.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ filter\n\n\t/**\n\t * 过滤字符串\n\t *\n\t * @param str    字符串\n\t * @param filter 过滤器，{@link Filter#accept(Object)}返回为{@code true}的保留字符\n\t * @return 过滤后的字符串\n\t * @since 5.4.0\n\t */\n\tpublic static String filter(CharSequence str, final Filter<Character> filter) {\n\t\tif (str == null || filter == null) {\n\t\t\treturn str(str);\n\t\t}\n\n\t\tint len = str.length();\n\t\tfinal StringBuilder sb = new StringBuilder(len);\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif (filter.accept(c)) {\n\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ case\n\n\t/**\n\t * 给定字符串中的字母是否全部为大写，判断依据如下：\n\t *\n\t * <pre>\n\t * 1. 大写字母包括A-Z\n\t * 2. 其它非字母的Unicode符都算作大写\n\t * </pre>\n\t *\n\t * @param str 被检查的字符串\n\t * @return 是否全部为大写\n\t * @since 4.2.2\n\t */\n\tpublic static boolean isUpperCase(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal int len = str.length();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tif (Character.isLowerCase(str.charAt(i))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 给定字符串中的字母是否全部为小写，判断依据如下：\n\t *\n\t * <pre>\n\t * 1. 小写字母包括a-z\n\t * 2. 其它非字母的Unicode符都算作小写\n\t * </pre>\n\t *\n\t * @param str 被检查的字符串\n\t * @return 是否全部为小写\n\t * @since 4.2.2\n\t */\n\tpublic static boolean isLowerCase(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal int len = str.length();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tif (Character.isUpperCase(str.charAt(i))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 切换给定字符串中的大小写。大写转小写，小写转大写。\n\t *\n\t * <pre>\n\t * CharSequenceUtil.swapCase(null)                 = null\n\t * CharSequenceUtil.swapCase(\"\")                   = \"\"\n\t * CharSequenceUtil.swapCase(\"The dog has a BONE\") = \"tHE DOG HAS A bone\"\n\t * </pre>\n\t *\n\t * @param str 字符串\n\t * @return 交换后的字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String swapCase(final String str) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str;\n\t\t}\n\n\t\tfinal char[] buffer = str.toCharArray();\n\n\t\tfor (int i = 0; i < buffer.length; i++) {\n\t\t\tfinal char ch = buffer[i];\n\t\t\tif (Character.isUpperCase(ch)) {\n\t\t\t\tbuffer[i] = Character.toLowerCase(ch);\n\t\t\t} else if (Character.isTitleCase(ch)) {\n\t\t\t\tbuffer[i] = Character.toLowerCase(ch);\n\t\t\t} else if (Character.isLowerCase(ch)) {\n\t\t\t\tbuffer[i] = Character.toUpperCase(ch);\n\t\t\t}\n\t\t}\n\t\treturn new String(buffer);\n\t}\n\n\t/**\n\t * 将驼峰式命名的字符串转换为下划线方式。如果转换前的驼峰式命名的字符串为空，则返回空字符串。<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * HelloWorld=》hello_world\n\t * Hello_World=》hello_world\n\t * HelloWorld_test=》hello_world_test\n\t * </pre>\n\t *\n\t * @param str 转换前的驼峰式命名的字符串，也可以为下划线形式\n\t * @return 转换后下划线方式命名的字符串\n\t * @see NamingCase#toUnderlineCase(CharSequence)\n\t */\n\tpublic static String toUnderlineCase(CharSequence str) {\n\t\treturn NamingCase.toUnderlineCase(str);\n\t}\n\n\t/**\n\t * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空，则返回空字符串。<br>\n\t *\n\t * @param str    转换前的驼峰式命名的字符串，也可以为符号连接形式\n\t * @param symbol 连接符\n\t * @return 转换后符号连接方式命名的字符串\n\t * @see NamingCase#toSymbolCase(CharSequence, char)\n\t * @since 4.0.10\n\t */\n\tpublic static String toSymbolCase(CharSequence str, char symbol) {\n\t\treturn NamingCase.toSymbolCase(str, symbol);\n\t}\n\n\t/**\n\t * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。<br>\n\t * 例如：hello_world=》helloWorld\n\t *\n\t * @param name 转换前的下划线大写方式命名的字符串\n\t * @return 转换后的驼峰式命名的字符串\n\t * @see NamingCase#toCamelCase(CharSequence)\n\t */\n\tpublic static String toCamelCase(CharSequence name) {\n\t\treturn NamingCase.toCamelCase(name);\n\t}\n\n\t/**\n\t * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。<br>\n\t * 例如：hello_world=》helloWorld; hello-world=》helloWorld\n\t *\n\t * @param name   转换前的下划线大写方式命名的字符串\n\t * @param symbol 连接符\n\t * @return 转换后的驼峰式命名的字符串\n\t * @see NamingCase#toCamelCase(CharSequence, char)\n\t */\n\tpublic static String toCamelCase(CharSequence name, char symbol) {\n\t\treturn NamingCase.toCamelCase(name, symbol);\n\t}\n\n\t// ------------------------------------------------------------------------ isSurround\n\n\t/**\n\t * 给定字符串是否被字符包围\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 是否包围，空串不包围\n\t */\n\tpublic static boolean isSurround(CharSequence str, CharSequence prefix, CharSequence suffix) {\n\t\tif (CharSequenceUtil.isBlank(str)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (str.length() < (prefix.length() + suffix.length())) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String str2 = str.toString();\n\t\treturn str2.startsWith(prefix.toString()) && str2.endsWith(suffix.toString());\n\t}\n\n\t/**\n\t * 给定字符串是否被字符包围\n\t *\n\t * @param str    字符串\n\t * @param prefix 前缀\n\t * @param suffix 后缀\n\t * @return 是否包围，空串不包围\n\t */\n\tpublic static boolean isSurround(CharSequence str, char prefix, char suffix) {\n\t\tif (CharSequenceUtil.isBlank(str)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (str.length() < 2) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix;\n\t}\n\n\t// ------------------------------------------------------------------------ builder\n\n\t/**\n\t * 创建StringBuilder对象\n\t *\n\t * @param strs 初始字符串列表\n\t * @return StringBuilder对象\n\t */\n\tpublic static StringBuilder builder(CharSequence... strs) {\n\t\treturn builder(Function.identity(), strs);\n\t}\n\n\t/**\n\t * 创建StringBuilder对象\n\t *\n\t * @param strEditor 编辑器，用于对每个字符串进行编辑\n\t * @param strs      待处理的字符串列表\n\t * @return StringBuilder对象\n\t * @since 5.8.42\n\t */\n\tpublic static StringBuilder builder(Function<CharSequence, CharSequence> strEditor, final CharSequence... strs) {\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tfor (final CharSequence str : strs) {\n\t\t\tsb.append(strEditor.apply(str));\n\t\t}\n\t\treturn sb;\n\t}\n\n\t/**\n\t * 创建StrBuilder对象\n\t *\n\t * @param strs 初始字符串列表\n\t * @return StrBuilder对象\n\t */\n\tpublic static StrBuilder strBuilder(CharSequence... strs) {\n\t\treturn StrBuilder.create(strs);\n\t}\n\n\t// ------------------------------------------------------------------------ getter and setter\n\n\t/**\n\t * 获得set或get或is方法对应的标准属性名<br>\n\t * 例如：setName 返回 name\n\t *\n\t * <pre>\n\t * getName =》name\n\t * setName =》name\n\t * isName  =》name\n\t * </pre>\n\t *\n\t * @param getOrSetMethodName Get或Set方法名\n\t * @return 如果是set或get方法名，返回field， 否则null\n\t */\n\tpublic static String getGeneralField(CharSequence getOrSetMethodName) {\n\t\tfinal String getOrSetMethodNameStr = getOrSetMethodName.toString();\n\t\tif (getOrSetMethodNameStr.startsWith(\"get\") || getOrSetMethodNameStr.startsWith(\"set\")) {\n\t\t\treturn removePreAndLowerFirst(getOrSetMethodName, 3);\n\t\t} else if (getOrSetMethodNameStr.startsWith(\"is\")) {\n\t\t\treturn removePreAndLowerFirst(getOrSetMethodName, 2);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 生成set方法名<br>\n\t * 例如：name 返回 setName\n\t *\n\t * @param fieldName 属性名\n\t * @return setXxx\n\t */\n\tpublic static String genSetter(CharSequence fieldName) {\n\t\treturn upperFirstAndAddPre(fieldName, \"set\");\n\t}\n\n\t/**\n\t * 生成get方法名\n\t *\n\t * @param fieldName 属性名\n\t * @return getXxx\n\t */\n\tpublic static String genGetter(CharSequence fieldName) {\n\t\treturn upperFirstAndAddPre(fieldName, \"get\");\n\t}\n\n\t// ------------------------------------------------------------------------ other\n\n\t/**\n\t * 连接多个字符串为一个\n\t *\n\t * @param isNullToEmpty 是否null转为\"\"\n\t * @param strs          字符串数组\n\t * @return 连接后的字符串\n\t * @since 4.1.0\n\t */\n\tpublic static String concat(boolean isNullToEmpty, CharSequence... strs) {\n\t\tfinal StrBuilder sb = new StrBuilder();\n\t\tfor (CharSequence str : strs) {\n\t\t\tsb.append(isNullToEmpty ? nullToEmpty(str) : str);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 将给定字符串，变成 \"xxx...xxx\" 形式的字符串\n\t *\n\t * <ul>\n\t *     <li>abcdefgh  9 -》 abcdefgh</li>\n\t *     <li>abcdefgh  8 -》 abcdefgh</li>\n\t *     <li>abcdefgh  7 -》 ab...gh</li>\n\t *     <li>abcdefgh  6 -》 ab...h</li>\n\t *     <li>abcdefgh  5 -》 a...h</li>\n\t *     <li>abcdefgh  4 -》 a..h</li>\n\t *     <li>abcdefgh  3 -》 a.h</li>\n\t *     <li>abcdefgh  2 -》 a.</li>\n\t *     <li>abcdefgh  1 -》 a</li>\n\t *     <li>abcdefgh  0 -》 abcdefgh</li>\n\t *     <li>abcdefgh -1 -》 abcdefgh</li>\n\t * </ul>\n\t *\n\t * @param str       字符串\n\t * @param maxLength 结果的最大长度\n\t * @return 截取后的字符串\n\t */\n\tpublic static String brief(CharSequence str, int maxLength) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int strLength = str.length();\n\t\tif (maxLength <= 0 || strLength <= maxLength) {\n\t\t\treturn str.toString();\n\t\t}\n\n\t\t// since 5.7.5，特殊长度\n\t\tswitch (maxLength) {\n\t\t\tcase 1:\n\t\t\t\treturn String.valueOf(str.charAt(0));\n\t\t\tcase 2:\n\t\t\t\treturn str.charAt(0) + \".\";\n\t\t\tcase 3:\n\t\t\t\treturn str.charAt(0) + \".\" + str.charAt(strLength - 1);\n\t\t\tcase 4:\n\t\t\t\treturn str.charAt(0) + \"..\" + str.charAt(strLength - 1);\n\t\t}\n\n\t\tfinal int suffixLength = (maxLength - 3) / 2;\n\t\tfinal int preLength = suffixLength + (maxLength - 3) % 2; // suffixLength 或 suffixLength + 1\n\t\tfinal String str2 = str.toString();\n\t\treturn format(\"{}...{}\",\n\t\t\tstr2.substring(0, preLength),\n\t\t\tstr2.substring(strLength - suffixLength));\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将多个对象转换为字符串\n\t *\n\t * @param conjunction 分隔符 {@link StrPool#COMMA}\n\t * @param objs        数组\n\t * @return 连接后的字符串\n\t * @see ArrayUtil#join(Object, CharSequence)\n\t */\n\tpublic static String join(CharSequence conjunction, Object... objs) {\n\t\treturn ArrayUtil.join(objs, conjunction);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将多个对象转换为字符串\n\t *\n\t * @param <T>         元素类型\n\t * @param conjunction 分隔符 {@link StrPool#COMMA}\n\t * @param iterable    集合\n\t * @return 连接后的字符串\n\t * @see CollUtil#join(Iterable, CharSequence)\n\t * @since 5.6.6\n\t */\n\tpublic static <T> String join(CharSequence conjunction, Iterable<T> iterable) {\n\t\treturn CollUtil.join(iterable, conjunction);\n\t}\n\n\t/**\n\t * 字符串的每一个字符是否都与定义的匹配器匹配\n\t *\n\t * @param value   字符串\n\t * @param matcher 匹配器\n\t * @return 是否全部匹配\n\t * @since 3.2.3\n\t */\n\tpublic static boolean isAllCharMatch(CharSequence value, Matcher<Character> matcher) {\n\t\tif (CharSequenceUtil.isBlank(value)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (int i = value.length(); --i >= 0; ) {\n\t\t\tif (false == matcher.match(value.charAt(i))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查字符串是否都为数字组成\n\t *\n\t * @param str 字符串\n\t * @return 是否都为数字组成\n\t * @since 5.7.3\n\t */\n\tpublic static boolean isNumeric(CharSequence str) {\n\t\treturn isAllCharMatch(str, Character::isDigit);\n\t}\n\n\t/**\n\t * 循环位移指定位置的字符串为指定距离<br>\n\t * 当moveLength大于0向右位移，小于0向左位移，0不位移<br>\n\t * 当moveLength大于字符串长度时采取循环位移策略，即位移到头后从头（尾）位移，例如长度为10，位移13则表示位移3\n\t *\n\t * @param str          字符串\n\t * @param startInclude 起始位置（包括）\n\t * @param endExclude   结束位置（不包括）\n\t * @param moveLength   移动距离，负数表示左移，正数为右移\n\t * @return 位移后的字符串\n\t * @since 4.0.7\n\t */\n\tpublic static String move(CharSequence str, int startInclude, int endExclude, int moveLength) {\n\t\tif (isEmpty(str)) {\n\t\t\treturn str(str);\n\t\t}\n\t\tfinal int len = str.length();\n\t\t// 参数校验\n\t\tif (startInclude < 0 || endExclude > len || startInclude > endExclude) {\n\t\t\tthrow new IndexOutOfBoundsException(\"Invalid range: [\" + startInclude + \", \" + endExclude + \")\");\n\t\t}\n\n\t\tif (Math.abs(moveLength) > len) {\n\t\t\t// 循环位移，当越界时循环\n\t\t\tmoveLength = moveLength % len;\n\t\t}\n\t\t// 分离“移动块”和“剩余轨道”\n\t\tfinal String block = str.subSequence(startInclude, endExclude).toString();\n\t\tfinal String rest = new StringBuilder(str).delete(startInclude, endExclude).toString();\n\n\t\tfinal int restLen = rest.length();\n\t\tif (restLen == 0) {\n\t\t\treturn str.toString(); // 全选时位移无意义\n\t\t}\n\t\t// 计算循环周期：剩余字符数 + 1（代表块可以存在的不同位置点）\n\t\tfinal int totalPositions = restLen + 1;\n\t\t// 计算移动后的新位置 (处理正负数)\n\t\tfinal int newPos = (startInclude + moveLength % totalPositions + totalPositions) % totalPositions;\n\t\t// 重新组装\n\t\treturn rest.substring(0, newPos) + // 放入新位置前的剩余字符\n\t\t\tblock +                     // 放入整体块\n\t\t\trest.substring(newPos); // 放入剩下的剩余字符\n\t}\n\n\t/**\n\t * 检查给定字符串的所有字符是否都一样\n\t *\n\t * @param str 字符出啊\n\t * @return 给定字符串的所有字符是否都一样\n\t * @since 5.7.3\n\t */\n\tpublic static boolean isCharEquals(CharSequence str) {\n\t\tAssert.notEmpty(str, \"Str to check must be not empty!\");\n\t\treturn count(str, str.charAt(0)) == str.length();\n\t}\n\n\t/**\n\t * 对字符串归一化处理，如 \"Á\" 可以使用 \"u00C1\"或 \"u0041u0301\"表示，实际测试中两个字符串并不equals<br>\n\t * 因此使用此方法归一为一种表示形式，默认按照W3C通常建议的，在NFC中交换文本。\n\t *\n\t * @param str 归一化的字符串\n\t * @return 归一化后的字符串\n\t * @see Normalizer#normalize(CharSequence, Normalizer.Form)\n\t * @since 5.7.16\n\t */\n\tpublic static String normalize(CharSequence str) {\n\t\treturn Normalizer.normalize(str, Normalizer.Form.NFC);\n\t}\n\n\t/**\n\t * 在给定字符串末尾填充指定字符，以达到给定长度<br>\n\t * 如果字符串本身的长度大于等于length，返回原字符串\n\t *\n\t * @param str       字符串\n\t * @param fixedChar 补充的字符\n\t * @param length    补充到的长度\n\t * @return 补充后的字符串\n\t * @since 5.8.0\n\t */\n\tpublic static String fixLength(CharSequence str, char fixedChar, int length) {\n\t\tfinal int fixedLength = length - str.length();\n\t\tif (fixedLength <= 0) {\n\t\t\treturn str.toString();\n\t\t}\n\t\treturn str + repeat(fixedChar, fixedLength);\n\t}\n\n\t/**\n\t * <p>指定字符串数组中，是否包含空字符串。</p>\n\t * <p>如果传入参数对象不是为空,则返回false。如果字符串包含字母,不区分大小写,则返回true</p>\n\t *\n\t * @param str 对象\n\t * @return 如果为字符串, 是否有字母\n\t */\n\tpublic static boolean hasLetter(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\tif (CharUtil.isLetter(str.charAt(i))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 字符串1和字符串2的公共前缀\n\t *\n\t * @param str1 字符串1\n\t * @param str2 字符串2\n\t * @return 字符串1和字符串2的公共前缀\n\t */\n\tpublic static CharSequence commonPrefix(CharSequence str1, CharSequence str2) {\n\t\tif (isEmpty(str1) || isEmpty(str2)) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tfinal int minLength = Math.min(str1.length(), str2.length());\n\n\t\tint index = 0;\n\n\t\tfor (; index < minLength; index++) {\n\n\t\t\tif (str1.charAt(index) != str2.charAt(index)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn str1.subSequence(0, index);\n\t}\n\n\t/**\n\t * 字符串1和字符串2的公共后缀\n\t *\n\t * @param str1 字符串1\n\t * @param str2 字符串2\n\t * @return 字符串1和字符串2的公共后缀\n\t */\n\tpublic static CharSequence commonSuffix(CharSequence str1, CharSequence str2) {\n\t\tif (isEmpty(str1) || isEmpty(str2)) {\n\t\t\treturn EMPTY;\n\t\t}\n\t\tint str1Index = str1.length() - 1;\n\t\tint str2Index = str2.length() - 1;\n\t\tfor (; str1Index >= 0 && str2Index >= 0; str1Index--, str2Index--) {\n\t\t\tif (str1.charAt(str1Index) != str2.charAt(str2Index)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn str1.subSequence(str1Index + 1, str1.length());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/NamingCase.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 命名规则封装，主要是针对驼峰风格命名、连接符命名等的封装\n *\n * @author looly\n * @since 5.7.10\n */\npublic class NamingCase {\n\n\t/**\n\t * 将驼峰式命名的字符串转换为下划线方式，又称SnakeCase、underScoreCase。<br>\n\t * 如果转换前的驼峰式命名的字符串为空，则返回空字符串。<br>\n\t * 规则为：\n\t * <ul>\n\t *     <li>单字之间以下划线隔开</li>\n\t *     <li>每个单字的首字母亦用小写字母</li>\n\t * </ul>\n\t * 例如：\n\t *\n\t * <pre>\n\t * HelloWorld=》hello_world\n\t * Hello_World=》hello_world\n\t * HelloWorld_test=》hello_world_test\n\t * </pre>\n\t *\n\t * @param str 转换前的驼峰式命名的字符串，也可以为下划线形式\n\t * @return 转换后下划线方式命名的字符串\n\t */\n\tpublic static String toUnderlineCase(CharSequence str) {\n\t\treturn toSymbolCase(str, CharUtil.UNDERLINE);\n\t}\n\n\t/**\n\t * 将驼峰式命名的字符串转换为短横连接方式。<br>\n\t * 如果转换前的驼峰式命名的字符串为空，则返回空字符串。<br>\n\t * 规则为：\n\t * <ul>\n\t *     <li>单字之间横线线隔开</li>\n\t *     <li>每个单字的首字母亦用小写字母</li>\n\t * </ul>\n\t * 例如：\n\t *\n\t * <pre>\n\t * HelloWorld=》hello-world\n\t * Hello_World=》hello-world\n\t * HelloWorld_test=》hello-world-test\n\t * </pre>\n\t *\n\t * @param str 转换前的驼峰式命名的字符串，也可以为下划线形式\n\t * @return 转换后下划线方式命名的字符串\n\t */\n\tpublic static String toKebabCase(CharSequence str) {\n\t\treturn toSymbolCase(str, CharUtil.DASHED);\n\t}\n\n\t/**\n\t * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空，则返回空字符串。\n\t *\n\t * @param str    转换前的驼峰式命名的字符串，也可以为符号连接形式\n\t * @param symbol 连接符\n\t * @return 转换后符号连接方式命名的字符串\n\t * @since 4.0.10\n\t */\n\tpublic static String toSymbolCase(CharSequence str, char symbol) {\n\t\tif (str == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal int length = str.length();\n\t\tfinal StrBuilder sb = new StrBuilder();\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif (Character.isUpperCase(c)) {\n\t\t\t\tfinal Character preChar = (i > 0) ? str.charAt(i - 1) : null;\n\t\t\t\tfinal Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null;\n\n\t\t\t\tif (null != preChar) {\n\t\t\t\t\tif (symbol == preChar) {\n\t\t\t\t\t\t// 前一个为分隔符\n\t\t\t\t\t\tif (null == nextChar || Character.isLowerCase(nextChar)) {\n\t\t\t\t\t\t\t//普通首字母大写，如_Abb -> _abb\n\t\t\t\t\t\t\tc = Character.toLowerCase(c);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t//后一个为大写，按照专有名词对待，如_AB -> _AB\n\t\t\t\t\t} else if (Character.isLowerCase(preChar)) {\n\t\t\t\t\t\t// 前一个为小写\n\t\t\t\t\t\tsb.append(symbol);\n\t\t\t\t\t\tif (null == nextChar || Character.isLowerCase(nextChar) || CharUtil.isNumber(nextChar)) {\n\t\t\t\t\t\t\t//普通首字母大写，如aBcc -> a_bcc\n\t\t\t\t\t\t\tc = Character.toLowerCase(c);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// 后一个为大写，按照专有名词对待，如aBC -> a_BC\n\t\t\t\t\t} else {\n\t\t\t\t\t\t//前一个为大写\n\t\t\t\t\t\tif (null != nextChar && Character.isLowerCase(nextChar)) {\n\t\t\t\t\t\t\t// 普通首字母大写，如ABcc -> A_bcc\n\t\t\t\t\t\t\tsb.append(symbol);\n\t\t\t\t\t\t\tc = Character.toLowerCase(c);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// 后一个为大写，按照专有名词对待，如ABC -> ABC\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// 首字母，需要根据后一个判断是否转为小写\n\t\t\t\t\tif (null == nextChar || Character.isLowerCase(nextChar)) {\n\t\t\t\t\t\t// 普通首字母大写，如Abc -> abc\n\t\t\t\t\t\tc = Character.toLowerCase(c);\n\t\t\t\t\t}\n\t\t\t\t\t// 后一个为大写，按照专有名词对待，如ABC -> ABC\n\t\t\t\t}\n\t\t\t}\n\t\t\tsb.append(c);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 将下划线方式命名的字符串转换为帕斯卡式。<br>\n\t * 规则为：\n\t * <ul>\n\t *     <li>单字之间不以空格或任何连接符断开</li>\n\t *     <li>第一个单字首字母采用大写字母</li>\n\t *     <li>后续单字的首字母亦用大写字母</li>\n\t * </ul>\n\t * 如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。<br>\n\t * 例如：hello_world=》HelloWorld\n\t *\n\t * @param name 转换前的下划线大写方式命名的字符串\n\t * @return 转换后的驼峰式命名的字符串\n\t */\n\tpublic static String toPascalCase(CharSequence name) {\n\t\treturn StrUtil.upperFirst(toCamelCase(name));\n\t}\n\n\t/**\n\t * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。<br>\n\t * 规则为：\n\t * <ul>\n\t *     <li>单字之间不以空格或任何连接符断开</li>\n\t *     <li>第一个单字首字母采用小写字母</li>\n\t *     <li>后续单字的首字母亦用大写字母</li>\n\t * </ul>\n\t * 例如：hello_world=》helloWorld\n\t *\n\t * @param name 转换前的下划线大写方式命名的字符串\n\t * @return 转换后的驼峰式命名的字符串\n\t */\n\tpublic static String toCamelCase(CharSequence name) {\n\t\treturn toCamelCase(name, CharUtil.UNDERLINE);\n\t}\n\n\t/**\n\t * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。\n\t *\n\t * @param name   转换前的自定义方式命名的字符串\n\t * @param symbol 原字符串中的连接符连接符\n\t * @return 转换后的驼峰式命名的字符串\n\t * @since 5.7.17\n\t */\n\tpublic static String toCamelCase(CharSequence name, char symbol) {\n\t\treturn toCamelCase(name, symbol, true);\n\t}\n\n\t/**\n\t * 将连接符方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空，则返回空字符串。\n\t *\n\t * @param name             转换前的自定义方式命名的字符串\n\t * @param symbol           原字符串中的连接符连接符\n\t * @param otherCharToLower 其他非连接符后的字符是否需要转为小写\n\t * @return 转换后的驼峰式命名的字符串\n\t */\n\tpublic static String toCamelCase(CharSequence name, char symbol, boolean otherCharToLower) {\n\t\tif (null == name) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal String name2 = name.toString();\n\t\tif (StrUtil.contains(name2, symbol)) {\n\t\t\tfinal int length = name2.length();\n\t\t\tfinal StringBuilder sb = new StringBuilder(length);\n\t\t\tboolean upperCase = false;\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\tchar c = name2.charAt(i);\n\n\t\t\t\tif (c == symbol) {\n\t\t\t\t\tupperCase = true;\n\t\t\t\t} else if (upperCase) {\n\t\t\t\t\tsb.append(Character.toUpperCase(c));\n\t\t\t\t\tupperCase = false;\n\t\t\t\t} else {\n\t\t\t\t\tsb.append(otherCharToLower ? Character.toLowerCase(c) : c);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sb.toString();\n\t\t} else {\n\t\t\treturn name2;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/PasswdStrength.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 检测密码强度<br>\n * 来自：https://github.com/venshine/CheckPasswordStrength\n *\n * @author venshine\n * @since 5.7.3\n */\npublic class PasswdStrength {\n\n\t/**\n\t * 密码等级枚举\n\t */\n\tpublic enum PASSWD_LEVEL {\n\t\tEASY, MIDIUM, STRONG, VERY_STRONG, EXTREMELY_STRONG\n\t}\n\n\t/**\n\t * 字符类型枚举\n\t */\n\tpublic enum CHAR_TYPE {\n\t\tNUM, SMALL_LETTER, CAPITAL_LETTER, OTHER_CHAR\n\t}\n\n\t/**\n\t * 简单密码字典\n\t */\n\tprivate final static String[] DICTIONARY = {\"password\", \"abc123\", \"iloveyou\", \"adobe123\", \"123123\", \"sunshine\",\n\t\t\"1314520\", \"a1b2c3\", \"123qwe\", \"aaa111\", \"qweasd\", \"admin\", \"passwd\"};\n\n\t/**\n\t * 数字长度\n\t */\n\tprivate final static int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999,\n\t\tInteger.MAX_VALUE};\n\n\t/**\n\t * 检查密码的健壮性\n\t *\n\t * @param passwd 密码\n\t * @return strength level\n\t */\n\tpublic static int check(String passwd) {\n\t\tif (null == passwd) {\n\t\t\tthrow new IllegalArgumentException(\"password is empty\");\n\t\t}\n\t\tint len = passwd.length();\n\t\tint level = 0;\n\n\t\t// increase points\n\t\tif (countLetter(passwd, CHAR_TYPE.NUM) > 0) {\n\t\t\tlevel++;\n\t\t}\n\t\tif (countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0) {\n\t\t\tlevel++;\n\t\t}\n\t\tif (len > 4 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0) {\n\t\t\tlevel++;\n\t\t}\n\t\tif (len > 6 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 4 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 || countLetter(passwd, CHAR_TYPE.NUM) > 0\n\t\t\t&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0\n\t\t\t&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2\n\t\t\t|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3\n\t\t\t|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2\n\t\t\t|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 || countLetter(passwd, CHAR_TYPE.NUM) >= 2\n\t\t\t&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2\n\t\t\t|| countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2\n\t\t\t&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 10 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2\n\t\t\t&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 3) {\n\t\t\tlevel++;\n\t\t}\n\t\tif (countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 6) {\n\t\t\tlevel++;\n\t\t}\n\n\t\tif (len > 12) {\n\t\t\tlevel++;\n\t\t\tif (len >= 16) {\n\t\t\t\tlevel++;\n\t\t\t}\n\t\t}\n\n\t\t// 判断passwd是否为连续字母（a-z/A-Z）的完整子串\n\t\tif (\"abcdefghijklmnopqrstuvwxyz\".contains(passwd) || \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\".contains(passwd)) {\n\t\t\tlevel--;\n\t\t}\n\t\t// 判断passwd是否为键盘连续序列的完整子串\n\t\tif (\"qwertyuiop\".contains(passwd) || \"asdfghjkl\".contains(passwd) || \"zxcvbnm\".contains(passwd)) {\n\t\t\tlevel--;\n\t\t}\n\t\t// 判断passwd是否为纯数字弱密码（升序或降序）的完整子串\n\t\tif (StrUtil.isNumeric(passwd) && (\"01234567890\".contains(passwd) || \"09876543210\".contains(passwd))) {\n\t\t\tlevel--;\n\t\t}\n\n\t\tif (countLetter(passwd, CHAR_TYPE.NUM) == len || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) == len\n\t\t\t|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) == len) {\n\t\t\tlevel--;\n\t\t}\n\n\t\tif (len % 2 == 0) { // aaabbb\n\t\t\tString part1 = passwd.substring(0, len / 2);\n\t\t\tString part2 = passwd.substring(len / 2);\n\t\t\tif (part1.equals(part2)) {\n\t\t\t\tlevel--;\n\t\t\t}\n\t\t\tif (StrUtil.isCharEquals(part1) && StrUtil.isCharEquals(part2)) {\n\t\t\t\tlevel--;\n\t\t\t}\n\t\t}\n\t\tif (len % 3 == 0) { // ababab\n\t\t\tString part1 = passwd.substring(0, len / 3);\n\t\t\tString part2 = passwd.substring(len / 3, len / 3 * 2);\n\t\t\tString part3 = passwd.substring(len / 3 * 2);\n\t\t\tif (part1.equals(part2) && part2.equals(part3)) {\n\t\t\t\tlevel--;\n\t\t\t}\n\t\t}\n\n\t\tif (StrUtil.isNumeric(passwd) && len >= 6 && len <= 8) { // 19881010 or 881010\n\t\t\tint year = 0;\n\t\t\tif (len == 8 || len == 6) {\n\t\t\t\tyear = Integer.parseInt(passwd.substring(0, len - 4));\n\t\t\t}\n\t\t\tint size = sizeOfInt(year);\n\t\t\tint month = Integer.parseInt(passwd.substring(size, size + 2));\n\t\t\tint day = Integer.parseInt(passwd.substring(size + 2, len));\n\t\t\tif (year >= 1950 && year < 2050 && month >= 1 && month <= 12 && day >= 1 && day <= 31) {\n\t\t\t\tlevel--;\n\t\t\t}\n\t\t}\n\n\t\t// 检测密码是否为简单密码字典中的弱密码或包含字典弱密码片段\n\t\tfor (String s : DICTIONARY) {\n\t\t\tif (passwd.equals(s) || s.contains(passwd)) {\n\t\t\t\tlevel--;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (len <= 6) {\n\t\t\tlevel--;\n\t\t\tif (len <= 4) {\n\t\t\t\tlevel--;\n\t\t\t\tif (len <= 3) {\n\t\t\t\t\tlevel = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (StrUtil.isCharEquals(passwd)) {\n\t\t\tlevel = 0;\n\t\t}\n\n\t\tif (level < 0) {\n\t\t\tlevel = 0;\n\t\t}\n\n\t\treturn level;\n\t}\n\n\t/**\n\t * 获取密码强度等级, 包括 easy, medium, strong, very strong, extremely strong\n\t *\n\t * @param passwd 密码\n\t * @return 密码等级枚举\n\t */\n\tpublic static PASSWD_LEVEL getLevel(String passwd) {\n\t\tint level = check(passwd);\n\t\tswitch (level) {\n\t\t\tcase 0:\n\t\t\tcase 1:\n\t\t\tcase 2:\n\t\t\tcase 3:\n\t\t\t\treturn PASSWD_LEVEL.EASY;\n\t\t\tcase 4:\n\t\t\tcase 5:\n\t\t\tcase 6:\n\t\t\t\treturn PASSWD_LEVEL.MIDIUM;\n\t\t\tcase 7:\n\t\t\tcase 8:\n\t\t\tcase 9:\n\t\t\t\treturn PASSWD_LEVEL.STRONG;\n\t\t\tcase 10:\n\t\t\tcase 11:\n\t\t\tcase 12:\n\t\t\t\treturn PASSWD_LEVEL.VERY_STRONG;\n\t\t\tdefault:\n\t\t\t\treturn PASSWD_LEVEL.EXTREMELY_STRONG;\n\t\t}\n\t}\n\n\t/**\n\t * 检查字符类型，包括数字、大写字母、小写字母及其他字符\n\t *\n\t * @param c 字符\n\t * @return 类型\n\t */\n\tprivate static CHAR_TYPE checkCharacterType(char c) {\n\t\tif (c >= 48 && c <= 57) {\n\t\t\treturn CHAR_TYPE.NUM;\n\t\t}\n\t\tif (c >= 65 && c <= 90) {\n\t\t\treturn CHAR_TYPE.CAPITAL_LETTER;\n\t\t}\n\t\tif (c >= 97 && c <= 122) {\n\t\t\treturn CHAR_TYPE.SMALL_LETTER;\n\t\t}\n\t\treturn CHAR_TYPE.OTHER_CHAR;\n\t}\n\n\t/**\n\t * 计算密码中指定字符类型的数量\n\t *\n\t * @param passwd 密码\n\t * @param type   类型\n\t * @return 数量\n\t */\n\tprivate static int countLetter(String passwd, CHAR_TYPE type) {\n\t\tint count = 0;\n\t\tif (null != passwd) {\n\t\t\tfinal int length = passwd.length();\n\t\t\tif (length > 0) {\n\t\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\t\tif (checkCharacterType(passwd.charAt(i)) == type) {\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * calculate the size of an integer number\n\t *\n\t * @param x 值\n\t * @return 数字长度\n\t */\n\tprivate static int sizeOfInt(int x) {\n\t\tfor (int i = 0; ; i++)\n\t\t\tif (x <= SIZE_TABLE[i]) {\n\t\t\t\treturn i + 1;\n\t\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/Simhash.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.lang.hash.MurmurHash;\n\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.locks.StampedLock;\n\n/**\n * <p>\n * Simhash是一种局部敏感hash，用于海量文本去重。<br>\n * 算法实现来自：https://github.com/xlturing/Simhash4J\n * </p>\n *\n * <p>\n * 局部敏感hash定义：假定两个字符串具有一定的相似性，在hash之后，仍然能保持这种相似性，就称之为局部敏感hash。\n * </p>\n *\n * @author Looly, litaoxiao\n * @since 4.3.3\n */\npublic class Simhash {\n\n\tprivate final int bitNum = 64;\n\t/** 存储段数，默认按照4段进行simhash存储 */\n\tprivate final int fracCount;\n\tprivate final int fracBitNum;\n\t/** 汉明距离的衡量标准，小于此距离标准表示相似 */\n\tprivate final int hammingThresh;\n\n\t/** 按照分段存储simhash，查找更快速 */\n\tprivate final List<Map<String, List<Long>>> storage;\n\tprivate final StampedLock lock = new StampedLock();\n\n\t/**\n\t * 构造\n\t */\n\tpublic Simhash() {\n\t\tthis(4, 3);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param fracCount 存储段数\n\t * @param hammingThresh 汉明距离的衡量标准\n\t */\n\tpublic Simhash(int fracCount, int hammingThresh) {\n\t\tthis.fracCount = fracCount;\n\t\tthis.fracBitNum = bitNum / fracCount;\n\t\tthis.hammingThresh = hammingThresh;\n\t\tthis.storage = new ArrayList<>(fracCount);\n\t\tfor (int i = 0; i < fracCount; i++) {\n\t\t\tstorage.add(new HashMap<>());\n\t\t}\n\t}\n\n\t/**\n\t * 指定文本计算simhash值\n\t *\n\t * @param segList 分词的词列表\n\t * @return Hash值\n\t */\n\tpublic long hash(Collection<? extends CharSequence> segList) {\n\t\tfinal int bitNum = this.bitNum;\n\t\t// 按照词语的hash值，计算simHashWeight(低位对齐)\n\t\tfinal int[] weight = new int[bitNum];\n\t\tlong wordHash;\n\t\tfor (CharSequence seg : segList) {\n\t\t\twordHash = MurmurHash.hash64(seg);\n\t\t\tfor (int i = 0; i < bitNum; i++) {\n\t\t\t\tif (((wordHash >> i) & 1) == 1)\n\t\t\t\t\tweight[i] += 1;\n\t\t\t\telse\n\t\t\t\t\tweight[i] -= 1;\n\t\t\t}\n\t\t}\n\n\t\t// 计算得到Simhash值\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < bitNum; i++) {\n\t\t\tsb.append((weight[i] > 0) ? 1 : 0);\n\t\t}\n\n\t\treturn new BigInteger(sb.toString(), 2).longValue();\n\t}\n\n\t/**\n\t * 判断文本是否与已存储的数据重复\n\t *\n\t * @param segList 文本分词后的结果\n\t * @return 是否重复\n\t */\n\tpublic boolean equals(Collection<? extends CharSequence> segList) {\n\t\tlong simhash = hash(segList);\n\t\tfinal List<String> fracList = splitSimhash(simhash);\n\t\tfinal int hammingThresh = this.hammingThresh;\n\n\t\tString frac;\n\t\tMap<String, List<Long>> fracMap;\n\t\tfinal long stamp = this.lock.readLock();\n\t\ttry {\n\t\t\tfor (int i = 0; i < fracCount; i++) {\n\t\t\t\tfrac = fracList.get(i);\n\t\t\t\tfracMap = storage.get(i);\n\t\t\t\tif (fracMap.containsKey(frac)) {\n\t\t\t\t\tfor (Long simhash2 : fracMap.get(frac)) {\n\t\t\t\t\t\t// 当汉明距离小于标准时相似\n\t\t\t\t\t\tif (hamming(simhash, simhash2) < hammingThresh) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.lock.unlockRead(stamp);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 按照(frac, 《simhash, content》)索引进行存储\n\t *\n\t * @param simhash Simhash值\n\t */\n\tpublic void store(Long simhash) {\n\t\tfinal int fracCount = this.fracCount;\n\t\tfinal List<Map<String, List<Long>>> storage = this.storage;\n\t\tfinal List<String> lFrac = splitSimhash(simhash);\n\n\t\tString frac;\n\t\tMap<String, List<Long>> fracMap;\n\t\tfinal long stamp = this.lock.writeLock();\n\t\ttry {\n\t\t\tfor (int i = 0; i < fracCount; i++) {\n\t\t\t\tfrac = lFrac.get(i);\n\t\t\t\tfracMap = storage.get(i);\n\t\t\t\tif (fracMap.containsKey(frac)) {\n\t\t\t\t\tfracMap.get(frac).add(simhash);\n\t\t\t\t} else {\n\t\t\t\t\tfinal List<Long> ls = new ArrayList<>();\n\t\t\t\t\tls.add(simhash);\n\t\t\t\t\tfracMap.put(frac, ls);\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.lock.unlockWrite(stamp);\n\t\t}\n\t}\n\n\t//------------------------------------------------------------------------------------------------------ Private method start\n\t/**\n\t * 计算汉明距离\n\t *\n\t * @param s1 值1\n\t * @param s2 值2\n\t * @return 汉明距离\n\t */\n\tprivate int hamming(Long s1, Long s2) {\n\t\tfinal int bitNum = this.bitNum;\n\t\tint dis = 0;\n\t\tfor (int i = 0; i < bitNum; i++) {\n\t\t\tif ((s1 >> i & 1) != (s2 >> i & 1))\n\t\t\t\tdis++;\n\t\t}\n\t\treturn dis;\n\t}\n\n\t/**\n\t * 将simhash分成n段\n\t *\n\t * @param simhash Simhash值\n\t * @return N段Simhash\n\t */\n\tprivate List<String> splitSimhash(Long simhash) {\n\t\tfinal int bitNum = this.bitNum;\n\t\tfinal int fracBitNum = this.fracBitNum;\n\n\t\tfinal List<String> ls = new ArrayList<>();\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < bitNum; i++) {\n\t\t\tsb.append(simhash >> i & 1);\n\t\t\tif ((i + 1) % fracBitNum == 0) {\n\t\t\t\tls.add(sb.toString());\n\t\t\t\tsb.setLength(0);\n\t\t\t}\n\t\t}\n\t\treturn ls;\n\t}\n\t//------------------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\n\n/**\n * 可复用的字符串生成器，非线程安全<br>\n *  TODO 6.x移除此类，java8的StringBuilder非常完善了，无需重写。\n *\n * @author Looly\n * @since 4.0.0\n */\npublic class StrBuilder implements CharSequence, Appendable, Serializable {\n\tprivate static final long serialVersionUID = 6341229705927508451L;\n\n\t/**\n\t * 默认容量\n\t */\n\tpublic static final int DEFAULT_CAPACITY = 16;\n\n\t/**\n\t * 存放的字符数组\n\t */\n\tprivate char[] value;\n\t/**\n\t * 当前指针位置，或者叫做已经加入的字符数，此位置总在最后一个字符之后\n\t */\n\tprivate int position;\n\n\t/**\n\t * 创建字符串构建器\n\t *\n\t * @return this\n\t */\n\tpublic static StrBuilder create() {\n\t\treturn new StrBuilder();\n\t}\n\n\t/**\n\t * 创建字符串构建器\n\t *\n\t * @param initialCapacity 初始容量\n\t * @return this\n\t */\n\tpublic static StrBuilder create(int initialCapacity) {\n\t\treturn new StrBuilder(initialCapacity);\n\t}\n\n\t/**\n\t * 创建字符串构建器\n\t *\n\t * @param strs 初始字符串\n\t * @return this\n\t * @since 4.0.1\n\t */\n\tpublic static StrBuilder create(CharSequence... strs) {\n\t\treturn new StrBuilder(strs);\n\t}\n\n\t// ------------------------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic StrBuilder() {\n\t\tthis(DEFAULT_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 初始容量\n\t */\n\tpublic StrBuilder(int initialCapacity) {\n\t\tvalue = new char[initialCapacity];\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param strs 初始字符串\n\t * @since 4.0.1\n\t */\n\tpublic StrBuilder(CharSequence... strs) {\n\t\tthis(ArrayUtil.isEmpty(strs) ? DEFAULT_CAPACITY : (totalLength(strs) + DEFAULT_CAPACITY));\n\t\tfor (CharSequence str : strs) {\n\t\t\tappend(str);\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------------------ Constructor end\n\n\t// ------------------------------------------------------------------------------------ Append\n\n\t/**\n\t * 追加对象，对象会被转换为字符串\n\t *\n\t * @param obj 对象\n\t * @return this\n\t */\n\tpublic StrBuilder append(Object obj) {\n\t\treturn insert(this.position, obj);\n\t}\n\n\t/**\n\t * 追加一个字符\n\t *\n\t * @param c 字符\n\t * @return this\n\t */\n\t@Override\n\tpublic StrBuilder append(char c) {\n\t\treturn insert(this.position, c);\n\t}\n\n\t/**\n\t * 追加一个字符数组\n\t *\n\t * @param src 字符数组\n\t * @return this\n\t */\n\tpublic StrBuilder append(char[] src) {\n\t\tif (ArrayUtil.isEmpty(src)) {\n\t\t\treturn this;\n\t\t}\n\t\treturn append(src, 0, src.length);\n\t}\n\n\t/**\n\t * 追加一个字符数组\n\t *\n\t * @param src    字符数组\n\t * @param srcPos 开始位置（包括）\n\t * @param length 长度\n\t * @return this\n\t */\n\tpublic StrBuilder append(char[] src, int srcPos, int length) {\n\t\treturn insert(this.position, src, srcPos, length);\n\t}\n\n\t@Override\n\tpublic StrBuilder append(CharSequence csq) {\n\t\treturn insert(this.position, csq);\n\t}\n\n\t@Override\n\tpublic StrBuilder append(CharSequence csq, int start, int end) {\n\t\treturn insert(this.position, csq, start, end);\n\t}\n\n\t// ------------------------------------------------------------------------------------ Insert\n\n\t/**\n\t * 追加对象，对象会被转换为字符串\n\t *\n\t * @param index 插入位置\n\t * @param obj   对象\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, Object obj) {\n\t\tif (obj instanceof CharSequence) {\n\t\t\treturn insert(index, (CharSequence) obj);\n\t\t}\n\t\treturn insert(index, Convert.toStr(obj));\n\t}\n\n\t/**\n\t * 插入指定字符\n\t *\n\t * @param index 位置\n\t * @param c     字符\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, char c) {\n\t\tif(index < 0){\n\t\t\tindex = this.position + index;\n\t\t}\n\t\tif ((index < 0)) {\n\t\t\tthrow new StringIndexOutOfBoundsException(index);\n\t\t}\n\n\t\tmoveDataAfterIndex(index, 1);\n\t\tvalue[index] = c;\n\t\tthis.position = Math.max(this.position, index) + 1;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定位置插入数据<br>\n\t * 如果插入位置为当前位置，则定义为追加<br>\n\t * 如果插入位置大于当前位置，则中间部分补充空格\n\t *\n\t * @param index 插入位置\n\t * @param src   源数组\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, char[] src) {\n\t\tif (ArrayUtil.isEmpty(src)) {\n\t\t\treturn this;\n\t\t}\n\t\treturn insert(index, src, 0, src.length);\n\t}\n\n\t/**\n\t * 指定位置插入数据<br>\n\t * 如果插入位置为当前位置，则定义为追加<br>\n\t * 如果插入位置大于当前位置，则中间部分补充空格\n\t *\n\t * @param index  插入位置\n\t * @param src    源数组\n\t * @param srcPos 位置\n\t * @param length 长度\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, char[] src, int srcPos, int length) {\n\t\tif (ArrayUtil.isEmpty(src) || srcPos > src.length || length <= 0) {\n\t\t\treturn this;\n\t\t}\n\t\tif(index < 0){\n\t\t\tindex = this.position + index;\n\t\t}\n\t\tif ((index < 0)) {\n\t\t\tthrow new StringIndexOutOfBoundsException(index);\n\t\t}\n\n\t\tif (srcPos < 0) {\n\t\t\tsrcPos = 0;\n\t\t} else if (srcPos + length > src.length) {\n\t\t\t// 长度越界，只截取最大长度\n\t\t\tlength = src.length - srcPos;\n\t\t}\n\n\t\tmoveDataAfterIndex(index, length);\n\t\t// 插入数据\n\t\tSystem.arraycopy(src, srcPos, value, index, length);\n\t\tthis.position = Math.max(this.position, index) + length;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定位置插入字符串的某个部分<br>\n\t * 如果插入位置为当前位置，则定义为追加<br>\n\t * 如果插入位置大于当前位置，则中间部分补充空格\n\t *\n\t * @param index 位置\n\t * @param csq   字符串\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, CharSequence csq) {\n\t\tif(index < 0){\n\t\t\tindex = this.position + index;\n\t\t}\n\t\tif ((index < 0)) {\n\t\t\tthrow new StringIndexOutOfBoundsException(index);\n\t\t}\n\n\t\tif (null == csq) {\n\t\t\tcsq = StrUtil.EMPTY;\n\t\t}\n\t\tint len = csq.length();\n\t\tmoveDataAfterIndex(index, csq.length());\n\t\tif (csq instanceof String) {\n\t\t\t((String) csq).getChars(0, len, this.value, index);\n\t\t} else if (csq instanceof StringBuilder) {\n\t\t\t((StringBuilder) csq).getChars(0, len, this.value, index);\n\t\t} else if (csq instanceof StringBuffer) {\n\t\t\t((StringBuffer) csq).getChars(0, len, this.value, index);\n\t\t} else if (csq instanceof StrBuilder) {\n\t\t\t((StrBuilder) csq).getChars(0, len, this.value, index);\n\t\t} else {\n\t\t\tfor (int i = 0, j = index; i < len; i++, j++) {\n\t\t\t\tthis.value[j] = csq.charAt(i);\n\t\t\t}\n\t\t}\n\t\tthis.position = Math.max(this.position, index) + len;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定位置插入字符串的某个部分<br>\n\t * 如果插入位置为当前位置，则定义为追加<br>\n\t * 如果插入位置大于当前位置，则中间部分补充空格\n\t *\n\t * @param index 位置\n\t * @param csq   字符串\n\t * @param start 字符串开始位置（包括）\n\t * @param end   字符串结束位置（不包括）\n\t * @return this\n\t */\n\tpublic StrBuilder insert(int index, CharSequence csq, int start, int end) {\n\t\tif (csq == null) {\n\t\t\tcsq = \"null\";\n\t\t}\n\t\tfinal int csqLen = csq.length();\n\t\tif (start > csqLen) {\n\t\t\treturn this;\n\t\t}\n\t\tif (start < 0) {\n\t\t\tstart = 0;\n\t\t}\n\t\tif (end > csqLen) {\n\t\t\tend = csqLen;\n\t\t}\n\t\tif (start >= end) {\n\t\t\treturn this;\n\t\t}\n\t\tif(index < 0){\n\t\t\tindex = this.position + index;\n\t\t}\n\t\tif ((index < 0)) {\n\t\t\tthrow new StringIndexOutOfBoundsException(index);\n\t\t}\n\n\t\tfinal int length = end - start;\n\t\tmoveDataAfterIndex(index, length);\n\t\tfor (int i = start, j = index; i < end; i++, j++) {\n\t\t\tvalue[j] = csq.charAt(i);\n\t\t}\n\t\tthis.position = Math.max(this.position, index) + length;\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------------------------------------------ Others\n\n\t/**\n\t * 将指定段的字符列表写出到目标字符数组中\n\t *\n\t * @param srcBegin 起始位置（包括）\n\t * @param srcEnd   结束位置（不包括）\n\t * @param dst      目标数组\n\t * @param dstBegin 目标起始位置（包括）\n\t * @return this\n\t */\n\tpublic StrBuilder getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) {\n\t\tif (srcBegin < 0) {\n\t\t\tsrcBegin = 0;\n\t\t}\n\t\tif (srcEnd < 0) {\n\t\t\tsrcEnd = 0;\n\t\t} else if (srcEnd > this.position) {\n\t\t\tsrcEnd = this.position;\n\t\t}\n\t\tif (srcBegin > srcEnd) {\n\t\t\tthrow new StringIndexOutOfBoundsException(\"srcBegin > srcEnd\");\n\t\t}\n\t\tSystem.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否有内容\n\t *\n\t * @return 是否有内容\n\t */\n\tpublic boolean hasContent() {\n\t\treturn position > 0;\n\t}\n\n\t/**\n\t * 是否为空\n\t *\n\t * @return 是否为空\n\t */\n\tpublic boolean isEmpty() {\n\t\treturn position == 0;\n\t}\n\n\t/**\n\t * 删除全部字符，位置归零\n\t *\n\t * @return this\n\t */\n\tpublic StrBuilder clear() {\n\t\treturn reset();\n\t}\n\n\t/**\n\t * 删除全部字符，位置归零\n\t *\n\t * @return this\n\t */\n\tpublic StrBuilder reset() {\n\t\tthis.position = 0;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 删除到指定位置<br>\n\t * 如果新位置小于等于0，则删除全部\n\t *\n\t * @param newPosition 新的位置，不包括这个位置\n\t * @return this\n\t */\n\tpublic StrBuilder delTo(int newPosition) {\n\t\tif (newPosition < 0) {\n\t\t\tnewPosition = 0;\n\t\t}\n\t\treturn del(newPosition, this.position);\n\t}\n\n\t/**\n\t * 删除指定长度的字符，规则如下：\n\t *\n\t * <pre>\n\t * 1、end大于等于最大长度，结束按照最大长度计算，相当于删除start之后虽有部分（性能最好）\n\t * 2、end小于start时，抛出StringIndexOutOfBoundsException\n\t * 3、start小于0 按照0处理\n\t * 4、start等于end不处理\n\t * 5、start和end都位于长度区间内，删除这段内容（内存拷贝）\n\t * </pre>\n\t *\n\t * @param start 开始位置，负数按照0处理（包括）\n\t * @param end   结束位置，超出最大长度按照最大长度处理（不包括）\n\t * @return this\n\t * @throws StringIndexOutOfBoundsException 当start &gt; end抛出此异常\n\t */\n\tpublic StrBuilder del(int start, int end) throws StringIndexOutOfBoundsException {\n\t\tif (start < 0) {\n\t\t\tstart = 0;\n\t\t}\n\n\t\tif (end >= this.position) {\n\t\t\t// end在边界及以外，相当于删除后半部分\n\t\t\tthis.position = start;\n\t\t\treturn this;\n\t\t} else if (end < 0) {\n\t\t\t// start和end都为0的情况下表示删除全部\n\t\t\tend = 0;\n\t\t}\n\n\t\tint len = end - start;\n\t\t// 截取中间部分，需要将后半部分复制到删除的开始位置\n\t\tif (len > 0) {\n\t\t\tSystem.arraycopy(value, start + len, value, start, this.position - end);\n\t\t\tthis.position -= len;\n\t\t} else if (len < 0) {\n\t\t\tthrow new StringIndexOutOfBoundsException(\"Start is greater than End.\");\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 生成字符串\n\t *\n\t * @param isReset 是否重置，重置后相当于空的构建器\n\t * @return 生成的字符串\n\t */\n\tpublic String toString(boolean isReset) {\n\t\tif (position > 0) {\n\t\t\tfinal String s = new String(value, 0, position);\n\t\t\tif (isReset) {\n\t\t\t\treset();\n\t\t\t}\n\t\t\treturn s;\n\t\t}\n\t\treturn StrUtil.EMPTY;\n\t}\n\n\t/**\n\t * 重置并返回生成的字符串\n\t *\n\t * @return 字符串\n\t */\n\tpublic String toStringAndReset() {\n\t\treturn toString(true);\n\t}\n\n\t/**\n\t * 生成字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn toString(false);\n\t}\n\n\t@Override\n\tpublic int length() {\n\t\treturn this.position;\n\t}\n\n\t@Override\n\tpublic char charAt(int index) {\n\t\tif(index < 0){\n\t\t\tindex = this.position + index;\n\t\t}\n\t\tif ((index < 0) || (index >= this.position)) {\n\t\t\tthrow new StringIndexOutOfBoundsException(index);\n\t\t}\n\t\treturn this.value[index];\n\t}\n\n\t@Override\n\tpublic CharSequence subSequence(int start, int end) {\n\t\treturn subString(start, end);\n\t}\n\n\t/**\n\t * 返回自定段的字符串\n\t *\n\t * @param start 开始位置（包括）\n\t * @return this\n\t */\n\tpublic String subString(int start) {\n\t\treturn subString(start, this.position);\n\t}\n\n\t/**\n\t * 返回自定段的字符串\n\t *\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return this\n\t */\n\tpublic String subString(int start, int end) {\n\t\treturn new String(this.value, start, end - start);\n\t}\n\n\t// ------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 指定位置之后的数据后移指定长度\n\t *\n\t * @param index  位置\n\t * @param length 位移长度\n\t */\n\tprivate void moveDataAfterIndex(int index, int length) {\n\t\tensureCapacity(Math.max(this.position, index) + length);\n\t\tif (index < this.position) {\n\t\t\t// 插入位置在已有数据范围内，后移插入位置之后的数据\n\t\t\tSystem.arraycopy(this.value, index, this.value, index + length, this.position - index);\n\t\t} else if (index > this.position) {\n\t\t\t// 插入位置超出范围，则当前位置到index清除为空格\n\t\t\tArrays.fill(this.value, this.position, index, StrUtil.C_SPACE);\n\t\t}\n\t\t// 不位移\n\t}\n\n\t/**\n\t * 确认容量是否够用，不够用则扩展容量\n\t *\n\t * @param minimumCapacity 最小容量\n\t */\n\tprivate void ensureCapacity(int minimumCapacity) {\n\t\t// overflow-conscious code\n\t\tif (minimumCapacity - value.length > 0) {\n\t\t\texpandCapacity(minimumCapacity);\n\t\t}\n\t}\n\n\t/**\n\t * 扩展容量<br>\n\t * 首先对容量进行二倍扩展，如果小于最小容量，则扩展为最小容量\n\t *\n\t * @param minimumCapacity 需要扩展的最小容量\n\t */\n\tprivate void expandCapacity(int minimumCapacity) {\n\t\tint newCapacity = (value.length << 1) + 2;\n\t\t// overflow-conscious code\n\t\tif (newCapacity - minimumCapacity < 0) {\n\t\t\tnewCapacity = minimumCapacity;\n\t\t}\n\t\tif (newCapacity < 0) {\n\t\t\tthrow new OutOfMemoryError(\"Capacity is too long and max than Integer.MAX\");\n\t\t}\n\t\tvalue = Arrays.copyOf(value, newCapacity);\n\t}\n\n\t/**\n\t * 给定字符串数组的总长度<br>\n\t * null字符长度定义为0\n\t *\n\t * @param strs 字符串数组\n\t * @return 总长度\n\t * @since 4.0.1\n\t */\n\tprivate static int totalLength(CharSequence... strs) {\n\t\tint totalLength = 0;\n\t\tfor (CharSequence str : strs) {\n\t\t\ttotalLength += (null == str ? 0 : str.length());\n\t\t}\n\t\treturn totalLength;\n\t}\n\t// ------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Map;\n\n/**\n * 字符串格式化工具\n *\n * @author Looly\n */\npublic class StrFormatter {\n\n\t/**\n\t * 格式化字符串<br>\n\t * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>\n\t * 如果想输出 {} 使用 \\\\转义 { 即可，如果想输出 {} 之前的 \\ 使用双转义符 \\\\\\\\ 即可<br>\n\t * 例：<br>\n\t * 通常使用：format(\"this is {} for {}\", \"a\", \"b\") =》 this is a for b<br>\n\t * 转义{}： format(\"this is \\\\{} for {}\", \"a\", \"b\") =》 this is \\{} for a<br>\n\t * 转义\\： format(\"this is \\\\\\\\{} for {}\", \"a\", \"b\") =》 this is \\a for b<br>\n\t *\n\t * @param strPattern 字符串模板\n\t * @param argArray   参数列表\n\t * @return 结果\n\t */\n\tpublic static String format(String strPattern, Object... argArray) {\n\t\treturn formatWith(strPattern, StrUtil.EMPTY_JSON, argArray);\n\t}\n\n\t/**\n\t * 格式化字符串<br>\n\t * 此方法只是简单将指定占位符 按照顺序替换为参数<br>\n\t * 如果想输出占位符使用 \\\\转义即可，如果想输出占位符之前的 \\ 使用双转义符 \\\\\\\\ 即可<br>\n\t * 例：<br>\n\t * 通常使用：format(\"this is {} for {}\", \"{}\", \"a\", \"b\") =》 this is a for b<br>\n\t * 转义{}： format(\"this is \\\\{} for {}\", \"{}\", \"a\", \"b\") =》 this is {} for a<br>\n\t * 转义\\： format(\"this is \\\\\\\\{} for {}\", \"{}\", \"a\", \"b\") =》 this is \\a for b<br>\n\t *\n\t * @param strPattern  字符串模板\n\t * @param placeHolder 占位符，例如{}\n\t * @param argArray    参数列表\n\t * @return 结果\n\t * @since 5.7.14\n\t */\n\tpublic static String formatWith(String strPattern, String placeHolder, Object... argArray) {\n\t\tif (StrUtil.isBlank(strPattern) || StrUtil.isBlank(placeHolder) || ArrayUtil.isEmpty(argArray)) {\n\t\t\treturn strPattern;\n\t\t}\n\t\tfinal int strPatternLength = strPattern.length();\n\t\tfinal int placeHolderLength = placeHolder.length();\n\n\t\t// 初始化定义好的长度以获得更好的性能\n\t\tfinal StringBuilder sbuf = new StringBuilder(strPatternLength + 50);\n\n\t\tint handledPosition = 0;// 记录已经处理到的位置\n\t\tint delimIndex;// 占位符所在位置\n\t\tfor (int argIndex = 0; argIndex < argArray.length; argIndex++) {\n\t\t\tdelimIndex = strPattern.indexOf(placeHolder, handledPosition);\n\t\t\tif (delimIndex == -1) {// 剩余部分无占位符\n\t\t\t\tif (handledPosition == 0) { // 不带占位符的模板直接返回\n\t\t\t\t\treturn strPattern;\n\t\t\t\t}\n\t\t\t\t// 字符串模板剩余部分不再包含占位符，加入剩余部分后返回结果\n\t\t\t\tsbuf.append(strPattern, handledPosition, strPatternLength);\n\t\t\t\treturn sbuf.toString();\n\t\t\t}\n\n\t\t\t// 转义符\n\t\t\tif (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == StrUtil.C_BACKSLASH) {// 转义符\n\t\t\t\tif (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == StrUtil.C_BACKSLASH) {// 双转义符\n\t\t\t\t\t// 转义符之前还有一个转义符，占位符依旧有效\n\t\t\t\t\tsbuf.append(strPattern, handledPosition, delimIndex - 1);\n\t\t\t\t\tsbuf.append(StrUtil.utf8Str(argArray[argIndex]));\n\t\t\t\t\thandledPosition = delimIndex + placeHolderLength;\n\t\t\t\t} else {\n\t\t\t\t\t// 占位符被转义\n\t\t\t\t\targIndex--;\n\t\t\t\t\tsbuf.append(strPattern, handledPosition, delimIndex - 1);\n\t\t\t\t\tsbuf.append(placeHolder.charAt(0));\n\t\t\t\t\thandledPosition = delimIndex + 1;\n\t\t\t\t}\n\t\t\t} else {// 正常占位符\n\t\t\t\tsbuf.append(strPattern, handledPosition, delimIndex);\n\t\t\t\tsbuf.append(StrUtil.utf8Str(argArray[argIndex]));\n\t\t\t\thandledPosition = delimIndex + placeHolderLength;\n\t\t\t}\n\t\t}\n\n\t\t// 加入最后一个占位符后所有的字符\n\t\tsbuf.append(strPattern, handledPosition, strPatternLength);\n\n\t\treturn sbuf.toString();\n\t}\n\n\t/**\n\t * 格式化文本，使用 {varName} 占位<br>\n\t * map = {a: \"aValue\", b: \"bValue\"} format(\"{a} and {b}\", map) ---=》 aValue and bValue\n\t *\n\t * @param template   文本模板，被替换的部分用 {key} 表示\n\t * @param map        参数值对\n\t * @param ignoreNull 是否忽略 {@code null} 值，忽略则 {@code null} 值对应的变量不被替换，否则替换为\"\"\n\t * @return 格式化后的文本\n\t * @since 5.7.10\n\t */\n\tpublic static String format(CharSequence template, Map<?, ?> map, boolean ignoreNull) {\n\t\tif (null == template) {\n\t\t\treturn null;\n\t\t}\n\t\tif (null == map || map.isEmpty()) {\n\t\t\treturn template.toString();\n\t\t}\n\n\t\tString template2 = template.toString();\n\t\tString value;\n\t\tfor (Map.Entry<?, ?> entry : map.entrySet()) {\n\t\t\tvalue = StrUtil.utf8Str(entry.getValue());\n\t\t\tif (null == value && ignoreNull) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttemplate2 = StrUtil.replace(template2, \"{\" + entry.getKey() + \"}\", value);\n\t\t}\n\t\treturn template2;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrJoiner.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.Iterator;\nimport java.util.function.Function;\n\n/**\n * 字符串连接器（拼接器），通过给定的字符串和多个元素，拼接为一个字符串<br>\n * 相较于{@link java.util.StringJoiner}提供更加灵活的配置，包括：\n * <ul>\n *     <li>支持任意Appendable接口实现</li>\n *     <li>支持每个元素单独wrap</li>\n *     <li>支持自定义null的处理逻辑</li>\n *     <li>支持自定义默认结果</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.2\n */\npublic class StrJoiner implements Appendable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Appendable appendable;\n\tprivate CharSequence delimiter;\n\tprivate CharSequence prefix;\n\tprivate CharSequence suffix;\n\t// 前缀和后缀是否包装每个元素，true表示包装每个元素，false包装整个字符串\n\tprivate boolean wrapElement;\n\t// null元素处理逻辑\n\tprivate NullMode nullMode = NullMode.NULL_STRING;\n\t// 当结果为空时默认返回的拼接结果\n\tprivate String emptyResult = StrUtil.EMPTY;\n\n\t// appendable中是否包含内容，用于判断增加内容时，是否首先加入分隔符\n\tprivate boolean hasContent;\n\n\t/**\n\t * 根据已有StrJoiner配置新建一个新的StrJoiner\n\t *\n\t * @param joiner 已有StrJoiner\n\t * @return 新的StrJoiner，配置相同\n\t * @since 5.7.12\n\t */\n\tpublic static StrJoiner of(StrJoiner joiner) {\n\t\tStrJoiner joinerNew = new StrJoiner(joiner.delimiter, joiner.prefix, joiner.suffix);\n\t\tjoinerNew.wrapElement = joiner.wrapElement;\n\t\tjoinerNew.nullMode = joiner.nullMode;\n\t\tjoinerNew.emptyResult = joiner.emptyResult;\n\n\t\treturn joinerNew;\n\t}\n\n\t/**\n\t * 使用指定分隔符创建StrJoiner\n\t *\n\t * @param delimiter 分隔符\n\t * @return StrJoiner\n\t */\n\tpublic static StrJoiner of(CharSequence delimiter) {\n\t\treturn new StrJoiner(delimiter);\n\t}\n\n\t/**\n\t * 使用指定分隔符创建StrJoiner\n\t *\n\t * @param delimiter 分隔符\n\t * @param prefix    前缀\n\t * @param suffix    后缀\n\t * @return StrJoiner\n\t */\n\tpublic static StrJoiner of(CharSequence delimiter, CharSequence prefix, CharSequence suffix) {\n\t\treturn new StrJoiner(delimiter, prefix, suffix);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param delimiter 分隔符，{@code null}表示无连接符，直接拼接\n\t */\n\tpublic StrJoiner(CharSequence delimiter) {\n\t\tthis(null, delimiter);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param appendable 字符串追加器，拼接的字符串都将加入到此，{@code null}使用默认{@link StringBuilder}\n\t * @param delimiter  分隔符，{@code null}表示无连接符，直接拼接\n\t */\n\tpublic StrJoiner(Appendable appendable, CharSequence delimiter) {\n\t\tthis(appendable, delimiter, null, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param delimiter 分隔符，{@code null}表示无连接符，直接拼接\n\t * @param prefix    前缀\n\t * @param suffix    后缀\n\t */\n\tpublic StrJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) {\n\t\tthis(null, delimiter, prefix, suffix);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param appendable 字符串追加器，拼接的字符串都将加入到此，{@code null}使用默认{@link StringBuilder}\n\t * @param delimiter  分隔符，{@code null}表示无连接符，直接拼接\n\t * @param prefix     前缀\n\t * @param suffix     后缀\n\t */\n\tpublic StrJoiner(Appendable appendable, CharSequence delimiter,\n\t\t\t\t\t CharSequence prefix, CharSequence suffix) {\n\t\tif (null != appendable) {\n\t\t\tthis.appendable = appendable;\n\t\t\tcheckHasContent(appendable);\n\t\t}\n\n\t\tthis.delimiter = delimiter;\n\t\tthis.prefix = prefix;\n\t\tthis.suffix = suffix;\n\t}\n\n\t/**\n\t * 设置分隔符\n\t *\n\t * @param delimiter 分隔符\n\t * @return this\n\t */\n\tpublic StrJoiner setDelimiter(CharSequence delimiter) {\n\t\tthis.delimiter = delimiter;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置前缀\n\t *\n\t * @param prefix 前缀\n\t * @return this\n\t */\n\tpublic StrJoiner setPrefix(CharSequence prefix) {\n\t\tthis.prefix = prefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置后缀\n\t *\n\t * @param suffix 后缀\n\t * @return this\n\t */\n\tpublic StrJoiner setSuffix(CharSequence suffix) {\n\t\tthis.suffix = suffix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置前缀和后缀是否包装每个元素\n\t *\n\t * @param wrapElement true表示包装每个元素，false包装整个字符串\n\t * @return this\n\t */\n\tpublic StrJoiner setWrapElement(boolean wrapElement) {\n\t\tthis.wrapElement = wrapElement;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置{@code null}元素处理逻辑\n\t *\n\t * @param nullMode 逻辑枚举，可选忽略、转换为\"\"或转换为null字符串\n\t * @return this\n\t */\n\tpublic StrJoiner setNullMode(NullMode nullMode) {\n\t\tthis.nullMode = nullMode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置当没有任何元素加入时，默认返回的字符串，默认\"\"\n\t *\n\t * @param emptyResult 默认字符串\n\t * @return this\n\t */\n\tpublic StrJoiner setEmptyResult(String emptyResult) {\n\t\tthis.emptyResult = emptyResult;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加对象到拼接器中\n\t *\n\t * @param obj 对象，支持数组、集合等\n\t * @return this\n\t */\n\tpublic StrJoiner append(Object obj) {\n\t\tif (null == obj) {\n\t\t\tappend((CharSequence) null);\n\t\t} else if (ArrayUtil.isArray(obj)) {\n\t\t\tappend(new ArrayIter<>(obj));\n\t\t} else if (obj instanceof Iterator) {\n\t\t\tappend((Iterator<?>) obj);\n\t\t} else if (obj instanceof Iterable) {\n\t\t\tappend(((Iterable<?>) obj).iterator());\n\t\t} else {\n\t\t\tappend(ObjectUtil.toString(obj));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加数组中的元素到拼接器中\n\t *\n\t * @param <T>   元素类型\n\t * @param array 元素数组\n\t * @return this\n\t */\n\tpublic <T> StrJoiner append(T[] array) {\n\t\tif (null == array) {\n\t\t\treturn this;\n\t\t}\n\t\treturn append(new ArrayIter<>(array));\n\t}\n\n\t/**\n\t * 追加{@link Iterator}中的元素到拼接器中\n\t *\n\t * @param <T>      元素类型\n\t * @param iterator 元素列表\n\t * @return this\n\t */\n\tpublic <T> StrJoiner append(Iterator<T> iterator) {\n\t\tif (null != iterator) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\tappend(iterator.next());\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加数组中的元素到拼接器中\n\t *\n\t * @param <T>       元素类型\n\t * @param array     元素数组\n\t * @param toStrFunc 元素对象转换为字符串的函数\n\t * @return this\n\t */\n\tpublic <T> StrJoiner append(T[] array, Function<T, ? extends CharSequence> toStrFunc) {\n\t\treturn append((Iterator<T>) new ArrayIter<>(array), toStrFunc);\n\t}\n\n\t/**\n\t * 追加{@link Iterator}中的元素到拼接器中\n\t *\n\t * @param <E>       元素类型\n\t * @param iterable  元素列表\n\t * @param toStrFunc 元素对象转换为字符串的函数\n\t * @return this\n\t */\n\tpublic <E> StrJoiner append(Iterable<E> iterable, Function<? super E, ? extends CharSequence> toStrFunc) {\n\t\treturn append(IterUtil.getIter(iterable), toStrFunc);\n\t}\n\n\t/**\n\t * 追加{@link Iterator}中的元素到拼接器中\n\t *\n\t * @param <E>       元素类型\n\t * @param iterator  元素列表\n\t * @param toStrFunc 元素对象转换为字符串的函数\n\t * @return this\n\t */\n\tpublic <E> StrJoiner append(Iterator<E> iterator, Function<? super E, ? extends CharSequence> toStrFunc) {\n\t\tif (null != iterator) {\n\t\t\twhile (iterator.hasNext()) {\n\t\t\t\tappend(toStrFunc.apply(iterator.next()));\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic StrJoiner append(CharSequence csq) {\n\t\treturn append(csq, 0, StrUtil.length(csq));\n\t}\n\n\t@Override\n\tpublic StrJoiner append(CharSequence csq, int startInclude, int endExclude) {\n\t\tif (null == csq) {\n\t\t\tswitch (this.nullMode) {\n\t\t\t\tcase IGNORE:\n\t\t\t\t\treturn this;\n\t\t\t\tcase TO_EMPTY:\n\t\t\t\t\tcsq = StrUtil.EMPTY;\n\t\t\t\t\tbreak;\n\t\t\t\tcase NULL_STRING:\n\t\t\t\t\tcsq = StrUtil.NULL;\n\t\t\t\t\tendExclude = StrUtil.NULL.length();\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tfinal Appendable appendable = prepare();\n\t\t\tif (wrapElement && StrUtil.isNotEmpty(this.prefix)) {\n\t\t\t\tappendable.append(prefix);\n\t\t\t}\n\t\t\tappendable.append(csq, startInclude, endExclude);\n\t\t\tif (wrapElement && StrUtil.isNotEmpty(this.suffix)) {\n\t\t\t\tappendable.append(suffix);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic StrJoiner append(char c) {\n\t\treturn append(String.valueOf(c));\n\t}\n\n\t/**\n\t * 合并一个StrJoiner 到当前的StrJoiner<br>\n\t * 合并规则为，在尾部直接追加，当存在{@link #prefix}时，如果{@link #wrapElement}为{@code false}，则去除之。\n\t *\n\t * @param strJoiner 其他的StrJoiner\n\t * @return this\n\t * @since 5.7.22\n\t */\n\tpublic StrJoiner merge(StrJoiner strJoiner){\n\t\tif(null != strJoiner && null != strJoiner.appendable){\n\t\t\tfinal String otherStr = strJoiner.toString();\n\t\t\tif(strJoiner.wrapElement){\n\t\t\t\tthis.append(otherStr);\n\t\t\t}else{\n\t\t\t\tthis.append(otherStr, this.prefix.length(), otherStr.length());\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 长度<br>\n\t * 长度计算方式为prefix + suffix + content<br>\n\t * 此方法结果与toString().length()一致。\n\t *\n\t * @return 长度，如果结果为{@code null}，返回-1\n\t * @since 5.7.22\n\t */\n\tpublic int length() {\n\t\treturn (this.appendable != null ? this.appendable.toString().length() + StrUtil.length(suffix) :\n\t\t\t\tnull == this.emptyResult ? -1 : emptyResult.length());\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tif (null == this.appendable) {\n\t\t\treturn emptyResult;\n\t\t}\n\n\t\tString result = this.appendable.toString();\n\t\tif (false == wrapElement && StrUtil.isNotEmpty(this.suffix)) {\n\t\t\tresult += this.suffix;\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * {@code null}处理的模式\n\t */\n\tpublic enum NullMode {\n\t\t/**\n\t\t * 忽略{@code null}，即null元素不加入拼接的字符串\n\t\t */\n\t\tIGNORE,\n\t\t/**\n\t\t * {@code null}转为\"\"\n\t\t */\n\t\tTO_EMPTY,\n\t\t/**\n\t\t * {@code null}转为null字符串\n\t\t */\n\t\tNULL_STRING\n\t}\n\n\t/**\n\t * 准备连接器，如果连接器非空，追加元素，否则初始化前缀\n\t *\n\t * @return {@link Appendable}\n\t * @throws IOException IO异常\n\t */\n\tprivate Appendable prepare() throws IOException {\n\t\tif (hasContent) {\n\t\t\tthis.appendable.append(delimiter);\n\t\t} else {\n\t\t\tif (null == this.appendable) {\n\t\t\t\tthis.appendable = new StringBuilder();\n\t\t\t}\n\t\t\tif (false == wrapElement && StrUtil.isNotEmpty(this.prefix)) {\n\t\t\t\tthis.appendable.append(this.prefix);\n\t\t\t}\n\t\t\tthis.hasContent = true;\n\t\t}\n\t\treturn this.appendable;\n\t}\n\n\t/**\n\t * 检查用户传入的{@link Appendable} 是否已经存在内容，而且不能以分隔符结尾\n\t *\n\t * @param appendable {@link Appendable}\n\t */\n\tprivate void checkHasContent(Appendable appendable) {\n\t\tif (appendable instanceof CharSequence) {\n\t\t\tfinal CharSequence charSequence = (CharSequence) appendable;\n\t\t\tif (charSequence.length() > 0 && StrUtil.endWith(charSequence, delimiter)) {\n\t\t\t\tthis.hasContent = true;\n\t\t\t}\n\t\t} else {\n\t\t\tfinal String initStr = appendable.toString();\n\t\t\tif (StrUtil.isNotEmpty(initStr) && false == StrUtil.endWith(initStr, delimiter)) {\n\t\t\t\tthis.hasContent = true;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrMatcher.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 字符串模式匹配，使用${XXXXX}作为变量，例如：\n *\n * <pre>\n *     pattern: ${name}-${age}-${gender}-${country}-${province}-${city}-${status}\n *     text:    \"小明-19-男-中国-河南-郑州-已婚\"\n *     result:  {name=小明, age=19, gender=男, country=中国, province=河南, city=郑州, status=已婚}\n * </pre>\n *\n * @author looly\n * @since 5.6.0\n */\npublic class StrMatcher {\n\n\tList<String> patterns;\n\n\t/**\n\t * 构造\n\t *\n\t * @param pattern 模式，变量用${XXX}占位\n\t */\n\tpublic StrMatcher(String pattern) {\n\t\tthis.patterns = parse(pattern);\n\t}\n\n\t/**\n\t * 匹配并提取匹配到的内容\n\t *\n\t * @param text 被匹配的文本\n\t * @return 匹配的map，key为变量名，value为匹配到的值\n\t */\n\tpublic Map<String, String> match(String text) {\n\t\tfinal HashMap<String, String> result = MapUtil.newHashMap(true);\n\t\tint from = 0;\n\t\tString key = null;\n\t\tint to;\n\t\tfor (String part : patterns) {\n\t\t\tif (StrUtil.isWrap(part, \"${\", \"}\")) {\n\t\t\t\t// 禁止连续变量，例如${a}${b}\n\t\t\t\tif (key != null) {\n\t\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\"Consecutive variables like ${a}${b} are not supported\");\n\t\t\t\t}\n\t\t\t\t// 变量\n\t\t\t\tkey = StrUtil.sub(part, 2, part.length() - 1);\n\t\t\t} else {\n\t\t\t\tto = text.indexOf(part, from);\n\t\t\t\tif (to < 0) {\n\t\t\t\t\t//普通字符串未匹配到，说明整个模式不能匹配，返回空\n\t\t\t\t\treturn MapUtil.empty();\n\t\t\t\t}\n\t\t\t\tif (null != key && to > from) {\n\t\t\t\t\t// 变量对应部分有内容\n\t\t\t\t\tresult.put(key, text.substring(from, to));\n\t\t\t\t}\n\t\t\t\t// 下一个起始点是普通字符串的末尾\n\t\t\t\tfrom = to + part.length();\n\t\t\t\tkey = null;\n\t\t\t}\n\t\t}\n\n\t\tif (null != key && from < text.length()) {\n\t\t\t// 变量对应部分有内容\n\t\t\tresult.put(key, text.substring(from));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 解析表达式\n\t *\n\t * @param pattern 表达式，使用${XXXX}作为变量占位符\n\t * @return 表达式\n\t */\n\tprivate static List<String> parse(String pattern) {\n\t\tList<String> patterns = new ArrayList<>();\n\t\tfinal int length = pattern.length();\n\t\tchar c = 0;\n\t\tchar pre;\n\t\tboolean inVar = false;\n\t\tStringBuilder part = StrUtil.builder();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tpre = c;\n\t\t\tc = pattern.charAt(i);\n\t\t\tif (inVar) {\n\t\t\t\tpart.append(c);\n\t\t\t\tif ('}' == c) {\n\t\t\t\t\t// 变量结束\n\t\t\t\t\tinVar = false;\n\t\t\t\t\tpatterns.add(part.toString());\n\t\t\t\t\tpart.setLength(0);\n\t\t\t\t}\n\t\t\t} else if ('{' == c && '$' == pre) {\n\t\t\t\t// 变量开始\n\t\t\t\tinVar = true;\n\t\t\t\tfinal String preText = part.substring(0, part.length() - 1);\n\t\t\t\tif (StrUtil.isNotEmpty(preText)) {\n\t\t\t\t\tpatterns.add(preText);\n\t\t\t\t}\n\t\t\t\tpart.setLength(0);\n\t\t\t\tpart.append(pre).append(c);\n\t\t\t} else {\n\t\t\t\t// 普通字符\n\t\t\t\tpart.append(c);\n\t\t\t}\n\t\t}\n\n\t\tif (part.length() > 0) {\n\t\t\tpatterns.add(part.toString());\n\t\t}\n\t\treturn patterns;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrPool.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.XmlUtil;\n\n/**\n * 常用字符串常量定义\n * @see CharPool\n *\n * @author looly\n * @since 5.6.3\n */\npublic interface StrPool {\n\n\t/**\n\t * 字符常量：空格符 {@code ' '}\n\t */\n\tchar C_SPACE = CharPool.SPACE;\n\n\t/**\n\t * 字符常量：制表符 {@code '\\t'}\n\t */\n\tchar C_TAB = CharPool.TAB;\n\n\t/**\n\t * 字符常量：点 {@code '.'}\n\t */\n\tchar C_DOT = CharPool.DOT;\n\n\t/**\n\t * 字符常量：斜杠 {@code '/'}\n\t */\n\tchar C_SLASH = CharPool.SLASH;\n\n\t/**\n\t * 字符常量：反斜杠 {@code '\\\\'}\n\t */\n\tchar C_BACKSLASH = CharPool.BACKSLASH;\n\n\t/**\n\t * 字符常量：回车符 {@code '\\r'}\n\t */\n\tchar C_CR = CharPool.CR;\n\n\t/**\n\t * 字符常量：换行符 {@code '\\n'}\n\t */\n\tchar C_LF = CharPool.LF;\n\n\t/**\n\t * 字符常量：下划线 {@code '_'}\n\t */\n\tchar C_UNDERLINE = CharPool.UNDERLINE;\n\n\t/**\n\t * 字符常量：逗号 {@code ','}\n\t */\n\tchar C_COMMA = CharPool.COMMA;\n\n\t/**\n\t * 字符常量：花括号（左） <code>'{'</code>\n\t */\n\tchar C_DELIM_START = CharPool.DELIM_START;\n\n\t/**\n\t * 字符常量：花括号（右） <code>'}'</code>\n\t */\n\tchar C_DELIM_END = CharPool.DELIM_END;\n\n\t/**\n\t * 字符常量：中括号（左） {@code '['}\n\t */\n\tchar C_BRACKET_START = CharPool.BRACKET_START;\n\n\t/**\n\t * 字符常量：中括号（右） {@code ']'}\n\t */\n\tchar C_BRACKET_END = CharPool.BRACKET_END;\n\n\t/**\n\t * 字符常量：冒号 {@code ':'}\n\t */\n\tchar C_COLON = CharPool.COLON;\n\n\t/**\n\t * 字符常量：艾特 {@code '@'}\n\t */\n\tchar C_AT = CharPool.AT;\n\n\t/**\n\t * 字符串常量：制表符 {@code \"\\t\"}\n\t */\n\tString TAB = \"\t\";\n\n\t/**\n\t * 字符串常量：点 {@code \".\"}\n\t */\n\tString DOT = \".\";\n\n\t/**\n\t * 字符串常量：双点 {@code \"..\"} <br>\n\t * 用途：作为指向上级文件夹的路径，如：{@code \"../path\"}\n\t */\n\tString DOUBLE_DOT = \"..\";\n\n\t/**\n\t * 字符串常量：斜杠 {@code \"/\"}\n\t */\n\tString SLASH = \"/\";\n\n\t/**\n\t * 字符串常量：反斜杠 {@code \"\\\\\"}\n\t */\n\tString BACKSLASH = \"\\\\\";\n\n\t/**\n\t * 字符串常量：回车符 {@code \"\\r\"} <br>\n\t * 解释：该字符常用于表示 Linux 系统和 MacOS 系统下的文本换行\n\t */\n\tString CR = \"\\r\";\n\n\t/**\n\t * 字符串常量：换行符 {@code \"\\n\"}\n\t */\n\tString LF = \"\\n\";\n\n\t/**\n\t * 字符串常量：Windows 换行 {@code \"\\r\\n\"} <br>\n\t * 解释：该字符串常用于表示 Windows 系统下的文本换行\n\t */\n\tString CRLF = \"\\r\\n\";\n\n\t/**\n\t * 字符串常量：下划线 {@code \"_\"}\n\t */\n\tString UNDERLINE = \"_\";\n\n\t/**\n\t * 字符串常量：减号（连接符） {@code \"-\"}\n\t */\n\tString DASHED = \"-\";\n\n\t/**\n\t * 字符串常量：逗号 {@code \",\"}\n\t */\n\tString COMMA = \",\";\n\n\t/**\n\t * 字符串常量：花括号（左） <code>\"{\"</code>\n\t */\n\tString DELIM_START = \"{\";\n\n\t/**\n\t * 字符串常量：花括号（右） <code>\"}\"</code>\n\t */\n\tString DELIM_END = \"}\";\n\n\t/**\n\t * 字符串常量：中括号（左） {@code \"[\"}\n\t */\n\tString BRACKET_START = \"[\";\n\n\t/**\n\t * 字符串常量：中括号（右） {@code \"]\"}\n\t */\n\tString BRACKET_END = \"]\";\n\n\t/**\n\t * 字符串常量：冒号 {@code \":\"}\n\t */\n\tString COLON = \":\";\n\n\t/**\n\t * 字符串常量：艾特 {@code \"@\"}\n\t */\n\tString AT = \"@\";\n\n\n\t/**\n\t * 字符串常量：HTML 不间断空格转义 {@code \"&nbsp;\" -> \" \"}\n\t */\n\tString HTML_NBSP = XmlUtil.NBSP;\n\n\t/**\n\t * 字符串常量：HTML And 符转义 {@code \"&amp;\" -> \"&\"}\n\t */\n\tString HTML_AMP = XmlUtil.AMP;\n\n\t/**\n\t * 字符串常量：HTML 双引号转义 {@code \"&quot;\" -> \"\\\"\"}\n\t */\n\tString HTML_QUOTE = XmlUtil.QUOTE;\n\n\t/**\n\t * 字符串常量：HTML 单引号转义 {@code \"&apos\" -> \"'\"}\n\t */\n\tString HTML_APOS = XmlUtil.APOS;\n\n\t/**\n\t * 字符串常量：HTML 小于号转义 {@code \"&lt;\" -> \"<\"}\n\t */\n\tString HTML_LT = XmlUtil.LT;\n\n\t/**\n\t * 字符串常量：HTML 大于号转义 {@code \"&gt;\" -> \">\"}\n\t */\n\tString HTML_GT = XmlUtil.GT;\n\n\t/**\n\t * 字符串常量：空 JSON {@code \"{}\"}\n\t */\n\tString EMPTY_JSON = \"{}\";\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/StrSplitter.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.text.finder.CharFinder;\nimport cn.hutool.core.text.finder.CharMatcherFinder;\nimport cn.hutool.core.text.finder.LengthFinder;\nimport cn.hutool.core.text.finder.PatternFinder;\nimport cn.hutool.core.text.finder.StrFinder;\nimport cn.hutool.core.text.split.SplitIter;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\n\n/**\n * 字符串切分器，封装统一的字符串分割静态方法\n * @author Looly\n * @since 5.7.0\n */\npublic class StrSplitter {\n\n\t//---------------------------------------------------------------------------------------------- Split by char\n\n\t/**\n\t * 切分字符串路径，仅支持Unix分界符：/\n\t *\n\t * @param str 被切分的字符串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> splitPath(CharSequence str) {\n\t\treturn splitPath(str, 0);\n\t}\n\n\t/**\n\t * 切分字符串路径，仅支持Unix分界符：/\n\t *\n\t * @param str 被切分的字符串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitPathToArray(CharSequence str) {\n\t\treturn toArray(splitPath(str));\n\t}\n\n\t/**\n\t * 切分字符串路径，仅支持Unix分界符：/\n\t *\n\t * @param str   被切分的字符串\n\t * @param limit 限制分片数\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> splitPath(CharSequence str, int limit) {\n\t\treturn split(str, StrUtil.C_SLASH, limit, true, true);\n\t}\n\n\t/**\n\t * 切分字符串路径，仅支持Unix分界符：/\n\t *\n\t * @param str   被切分的字符串\n\t * @param limit 限制分片数\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitPathToArray(CharSequence str, int limit) {\n\t\treturn toArray(splitPath(str, limit));\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, char separator, boolean ignoreEmpty) {\n\t\treturn split(str, separator, 0, true, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, 0, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串，大小写敏感，去除每个元素两边空白符\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, char separator, int limit, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, true, ignoreEmpty, false);\n\t}\n\n\t/**\n\t * 切分字符串，大小写敏感\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, isTrim, ignoreEmpty, false);\n\t}\n\n\t/**\n\t * 切分字符串，大小写敏感\n\t *\n\t * @param <R>         切分后的元素类型\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param ignoreEmpty 是否忽略空串\n\t * @param mapping     切分后的字符串元素的转换方法\n\t * @return 切分后的集合，元素类型是经过 mapping 转换后的\n\t * @since 5.7.14\n\t */\n\tpublic static <R> List<R> split(CharSequence str, char separator, int limit, boolean ignoreEmpty, Function<String, R> mapping) {\n\t\treturn split(str, separator, limit, ignoreEmpty, false, mapping);\n\t}\n\n\t/**\n\t * 切分字符串，忽略大小写\n\t *\n\t * @param text        被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitIgnoreCase(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(text, separator, limit, isTrim, ignoreEmpty, true);\n\t}\n\n\t/**\n\t * 切分字符串\n\t *\n\t * @param text        被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 切分后的集合\n\t */\n\tpublic static List<String> split(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) {\n\t\treturn split(text, separator, limit, ignoreEmpty, ignoreCase, trimFunc(isTrim));\n\t}\n\n\t/**\n\t * 切分字符串<br>\n\t * 如果提供的字符串为{@code null}，则返回一个空的{@link ArrayList}<br>\n\t * 如果提供的字符串为\"\"，则当ignoreEmpty时返回空的{@link ArrayList}，否则返回只有一个\"\"元素的{@link ArrayList}\n\t *\n\t * @param <R>         切分后的元素类型\n\t * @param text        被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，-1不限制\n\t * @param ignoreEmpty 是否忽略空串\n\t * @param ignoreCase  是否忽略大小写\n\t * @param mapping     切分后的字符串元素的转换方法\n\t * @return 切分后的集合，元素类型是经过 mapping 转换后的\n\t * @since 5.7.14\n\t */\n\tpublic static <R> List<R> split(CharSequence text, char separator, int limit, boolean ignoreEmpty,\n\t\t\t\t\t\t\t\t\tboolean ignoreCase, Function<String, R> mapping) {\n\t\tif (null == text) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tfinal SplitIter splitIter = new SplitIter(text, new CharFinder(separator, ignoreCase), limit, ignoreEmpty);\n\t\treturn splitIter.toList(mapping);\n\t}\n\n\t/**\n\t * 切分字符串为字符串数组\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitToArray(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn toArray(split(str, separator, limit, isTrim, ignoreEmpty));\n\t}\n\n\t//---------------------------------------------------------------------------------------------- Split by String\n\n\t/**\n\t * 切分字符串，不忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, String separator, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, -1, isTrim, ignoreEmpty, false);\n\t}\n\n\t/**\n\t * 切分字符串，去除每个元素两边空格，忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, String separator, boolean ignoreEmpty) {\n\t\treturn split(str, separator, true, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串，不忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param limit       限制分片数，小于等于0表示无限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, isTrim, ignoreEmpty, false);\n\t}\n\n\t/**\n\t * 切分字符串，去除每个元素两边空格，忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param limit       限制分片数\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitTrim(CharSequence str, String separator, int limit, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, true, ignoreEmpty);\n\t}\n\n\t/**\n\t * 切分字符串，忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param limit       限制分片数\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitIgnoreCase(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, isTrim, ignoreEmpty, true);\n\t}\n\n\t/**\n\t * 切分字符串，去除每个元素两边空格，忽略大小写\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param limit       限制分片数\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> splitTrimIgnoreCase(CharSequence str, String separator, int limit, boolean ignoreEmpty) {\n\t\treturn split(str, separator, limit, true, ignoreEmpty, true);\n\t}\n\n\t/**\n\t * 切分字符串<br>\n\t * 如果为空字符串或者null 则返回空集合\n\t *\n\t * @param text        被切分的字符串\n\t * @param separator   分隔符字符串\n\t * @param limit       限制分片数，小于等于0表示无限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @param ignoreCase  是否忽略大小写\n\t * @return 切分后的集合\n\t * @since 3.2.1\n\t */\n\tpublic static List<String> split(CharSequence text, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) {\n\t\tif (null == text) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tfinal SplitIter splitIter = new SplitIter(text, new StrFinder(separator, ignoreCase), limit, ignoreEmpty);\n\t\treturn splitIter.toList(isTrim);\n\t}\n\n\t/**\n\t * 切分字符串为字符串数组\n\t *\n\t * @param str         被切分的字符串\n\t * @param separator   分隔符字符\n\t * @param limit       限制分片数，小于等于0表示无限制\n\t * @param isTrim      是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty 是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitToArray(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn toArray(split(str, separator, limit, isTrim, ignoreEmpty));\n\t}\n\n\t//---------------------------------------------------------------------------------------------- Split by Whitespace\n\n\t/**\n\t * 使用空白符切分字符串<br>\n\t * 切分后的字符串两边不包含空白符，空串或空白符串并不做为元素之一<br>\n\t * 如果为空字符串或者null 则返回空集合\n\t *\n\t * @param text  被切分的字符串\n\t * @param limit 限制分片数\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(CharSequence text, int limit) {\n\t\tif (null == text) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tfinal SplitIter splitIter = new SplitIter(text, new CharMatcherFinder(CharUtil::isBlankChar), limit, true);\n\t\treturn splitIter.toList(false);\n\t}\n\n\t/**\n\t * 切分字符串为字符串数组\n\t *\n\t * @param str   被切分的字符串\n\t * @param limit 限制分片数\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitToArray(String str, int limit) {\n\t\treturn toArray(split(str, limit));\n\t}\n\t//---------------------------------------------------------------------------------------------- Split by regex\n\n\t/**\n\t * 通过正则切分字符串\n\t *\n\t * @param text           字符串\n\t * @param separatorRegex 分隔符正则\n\t * @param limit          限制分片数\n\t * @param isTrim         是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty    是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> splitByRegex(String text, String separatorRegex, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\tfinal Pattern pattern = PatternPool.get(separatorRegex);\n\t\treturn split(text, pattern, limit, isTrim, ignoreEmpty);\n\t}\n\n\t/**\n\t * 通过正则切分字符串<br>\n\t * 如果为空字符串或者null 则返回空集合\n\t *\n\t * @param text             字符串\n\t * @param separatorPattern 分隔符正则{@link Pattern}\n\t * @param limit            限制分片数\n\t * @param isTrim           是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty      是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static List<String> split(String text, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\tif (null == text) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tfinal SplitIter splitIter = new SplitIter(text, new PatternFinder(separatorPattern), limit, ignoreEmpty);\n\t\treturn splitIter.toList(isTrim);\n\t}\n\n\t/**\n\t * 通过正则切分字符串为字符串数组\n\t *\n\t * @param str              被切分的字符串\n\t * @param separatorPattern 分隔符正则{@link Pattern}\n\t * @param limit            限制分片数\n\t * @param isTrim           是否去除切分字符串后每个元素两边的空格\n\t * @param ignoreEmpty      是否忽略空串\n\t * @return 切分后的集合\n\t * @since 3.0.8\n\t */\n\tpublic static String[] splitToArray(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) {\n\t\treturn toArray(split(str, separatorPattern, limit, isTrim, ignoreEmpty));\n\t}\n\t//---------------------------------------------------------------------------------------------- Split by length\n\n\t/**\n\t * 根据给定长度，将给定字符串截取为多个部分\n\t *\n\t * @param text 字符串\n\t * @param len  每一个小节的长度\n\t * @return 截取后的字符串数组\n\t */\n\tpublic static String[] splitByLength(CharSequence text, int len) {\n\t\tif (null == text) {\n\t\t\treturn new String[0];\n\t\t}\n\t\tSplitIter splitIter = new SplitIter(text, new LengthFinder(len), -1, false);\n\t\treturn splitIter.toArray(false);\n\t}\n\t//---------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * List转Array\n\t *\n\t * @param list List\n\t * @return Array\n\t */\n\tprivate static String[] toArray(List<String> list) {\n\t\treturn list.toArray(new String[0]);\n\t}\n\n\t/**\n\t * Trim函数\n\t *\n\t * @param isTrim 是否trim\n\t * @return {@link Function}\n\t */\n\tprivate static Function<String, String> trimFunc(boolean isTrim) {\n\t\treturn (str) -> isTrim ? StrUtil.trim(str) : str;\n\t}\n\t//---------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/TextSimilarity.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 文本相似度计算<br>\n * 工具类提供者：【杭州】fineliving\n *\n * @author fanqun\n * @since 3.2.3\n **/\npublic class TextSimilarity {\n\n\t/**\n\t * 利用莱文斯坦距离(Levenshtein distance)算法计算相似度，两个都是空串相似度为1，被认为是相同的串<br>\n\t * 比较方法为：\n\t * <ul>\n\t *     <li>只比较两个字符串字母、数字、汉字部分，其他符号去除</li>\n\t *     <li>计算出两个字符串最大子串，除以最长的字符串，结果即为相似度</li>\n\t * </ul>\n\t *\n\t * @param strA 字符串1\n\t * @param strB 字符串2\n\t * @return 相似度\n\t */\n\tpublic static double similar(String strA, String strB) {\n\t\tString newStrA, newStrB;\n\t\tif (strA.length() < strB.length()) {\n\t\t\tnewStrA = removeSign(strB);\n\t\t\tnewStrB = removeSign(strA);\n\t\t} else {\n\t\t\tnewStrA = removeSign(strA);\n\t\t\tnewStrB = removeSign(strB);\n\t\t}\n\n\t\t// 用较大的字符串长度作为分母，相似子串作为分子计算出字串相似度\n\t\tint temp = Math.max(newStrA.length(), newStrB.length());\n\t\tif(0 == temp) {\n\t\t\t// 两个都是空串相似度为1，被认为是相同的串\n\t\t\treturn 1;\n\t\t}\n\n\t\tfinal int commonLength = longestCommonSubstringLength(newStrA, newStrB);\n\t\treturn NumberUtil.div(commonLength, temp);\n\t}\n\n\t/**\n\t * 利用莱文斯坦距离(Levenshtein distance)算法计算相似度百分比\n\t *\n\t * @param strA 字符串1\n\t * @param strB 字符串2\n\t * @param scale 保留小数\n\t * @return 百分比\n\t */\n\tpublic static String similar(String strA, String strB, int scale) {\n\t\treturn NumberUtil.formatPercent(similar(strA, strB), scale);\n\t}\n\n\t/**\n\t * 最长公共子串，采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。<br>\n\t * 算法解析见：https://leetcode-cn.com/problems/longest-common-subsequence/solution/zui-chang-gong-gong-zi-xu-lie-by-leetcod-y7u0/\n\t *\n\t * @param strA 字符串1\n\t * @param strB 字符串2\n\t * @return 最长公共子串\n\t */\n\tpublic static String longestCommonSubstring(String strA, String strB) {\n\t\t// 初始化矩阵数据,matrix[0][0]的值为0， 如果字符数组chars_strA和chars_strB的对应位相同，则matrix[i][j]的值为左上角的值加1，\n\t\t// 否则，matrix[i][j]的值等于左上方最近两个位置的较大值， 矩阵中其余各点的值为0.\n\t\tfinal int[][] matrix = generateMatrix(strA, strB);\n\n\t\tint m = strA.length();\n\t\tint n = strB.length();\n\t\t// 矩阵中，如果matrix[m][n]的值不等于matrix[m-1][n]的值也不等于matrix[m][n-1]的值，\n\t\t// 则matrix[m][n]对应的字符为相似字符元，并将其存入result数组中。\n\t\tchar[] result = new char[matrix[m][n]];\n\t\tint currentIndex = result.length - 1;\n\t\twhile (matrix[m][n] != 0) {\n\t\t\tif (matrix[m][n] == matrix[m][n - 1]) {\n\t\t\t\tn--;\n\t\t\t} else if (matrix[m][n] == matrix[m - 1][n]) {\n\t\t\t\tm--;\n\t\t\t} else {\n\t\t\t\tresult[currentIndex] = strA.charAt(m - 1);\n\t\t\t\tcurrentIndex--;\n\t\t\t\tn--;\n\t\t\t\tm--;\n\t\t\t}\n\t\t}\n\t\treturn new String(result);\n\t}\n\n\t// --------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 将字符串的所有数据依次写成一行，去除无意义字符串\n\t *\n\t * @param str 字符串\n\t * @return 处理后的字符串\n\t */\n\tprivate static String removeSign(String str) {\n\t\tint length = str.length();\n\t\tStringBuilder sb = StrUtil.builder(length);\n\t\t// 遍历字符串str,如果是汉字数字或字母，则追加到ab上面\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif(isValidChar(c)) {\n\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 判断字符是否为汉字，数字和字母， 因为对符号进行相似度比较没有实际意义，故符号不加入考虑范围。\n\t *\n\t * @param charValue 字符\n\t * @return true表示为非汉字，数字和字母，false反之\n\t */\n\tprivate static boolean isValidChar(char charValue) {\n\t\treturn (charValue >= 0x4E00 && charValue <= 0X9FFF) || //\n\t\t\t\t(charValue >= 'a' && charValue <= 'z') || //\n\t\t\t\t(charValue >= 'A' && charValue <= 'Z') || //\n\t\t\t\t(charValue >= '0' && charValue <= '9');\n\t}\n\n\t/**\n\t * 求公共子串，采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。\n\t *\n\t * @param strA 字符串1\n\t * @param strB 字符串2\n\t * @return 公共子串\n\t */\n\tprivate static int longestCommonSubstringLength(String strA, String strB) {\n\t\tfinal int m = strA.length();\n\t\tfinal int n = strB.length();\n\t\treturn generateMatrix(strA, strB)[m][n];\n\t}\n\n\t/**\n\t * 求公共子串，采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。\n\t *\n\t * @param strA 字符串1\n\t * @param strB 字符串2\n\t * @return 公共串矩阵\n\t */\n\tprivate static int[][] generateMatrix(String strA, String strB) {\n\t\tint m = strA.length();\n\t\tint n = strB.length();\n\n\t\t// 初始化矩阵数据,matrix[0][0]的值为0， 如果字符数组chars_strA和chars_strB的对应位相同，则matrix[i][j]的值为左上角的值加1，\n\t\t// 否则，matrix[i][j]的值等于左上方最近两个位置的较大值， 矩阵中其余各点的值为0.\n\t\tfinal int[][] matrix = new int[m + 1][n + 1];\n\t\tfor (int i = 1; i <= m; i++) {\n\t\t\tfor (int j = 1; j <= n; j++) {\n\t\t\t\tif (strA.charAt(i - 1) == strB.charAt(j - 1)) {\n\t\t\t\t\tmatrix[i][j] = matrix[i - 1][j - 1] + 1;\n\t\t\t\t} else {\n\t\t\t\t\tmatrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn matrix;\n\t}\n\t// --------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/UnicodeUtil.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 提供Unicode字符串和普通字符串之间的转换\n *\n * @author 兜兜毛毛, looly\n * @since 4.0.0\n */\npublic class UnicodeUtil {\n\n\t/**\n\t * Unicode字符串转为普通字符串<br>\n\t * Unicode字符串的表现方式为：\\\\uXXXX\n\t *\n\t * @param unicode Unicode字符串\n\t * @return 普通字符串\n\t */\n\tpublic static String toString(String unicode) {\n\t\tif (StrUtil.isBlank(unicode)) {\n\t\t\treturn unicode;\n\t\t}\n\n\t\tfinal int len = unicode.length();\n\t\tStringBuilder sb = new StringBuilder(len);\n\t\tint i;\n\t\tint pos = 0;\n\t\twhile ((i = StrUtil.indexOfIgnoreCase(unicode, \"\\\\u\", pos)) != -1) {\n\t\t\tsb.append(unicode, pos, i);//写入Unicode符之前的部分\n\t\t\tpos = i;\n\t\t\tif (i + 5 < len) {\n\t\t\t\tchar c;\n\t\t\t\ttry {\n\t\t\t\t\tc = (char) Integer.parseInt(unicode.substring(i + 2, i + 6), 16);\n\t\t\t\t\tsb.append(c);\n\t\t\t\t\tpos = i + 6;//跳过整个Unicode符\n\t\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t\t//非法Unicode符，跳过\n\t\t\t\t\tsb.append(unicode, pos, i + 2);//写入\"\\\\u\"\n\t\t\t\t\tpos = i + 2;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t//非Unicode符，结束\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (pos < len) {\n\t\t\tsb.append(unicode, pos, len);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 字符编码为Unicode形式\n\t *\n\t * @param c 被编码的字符\n\t * @return Unicode字符串\n\t * @since 5.6.2\n\t * @see HexUtil#toUnicodeHex(char)\n\t */\n\tpublic static String toUnicode(char c) {\n\t\treturn HexUtil.toUnicodeHex(c);\n\t}\n\n\t/**\n\t * 字符编码为Unicode形式\n\t *\n\t * @param c 被编码的字符\n\t * @return Unicode字符串\n\t * @since 5.6.2\n\t * @see HexUtil#toUnicodeHex(int)\n\t */\n\tpublic static String toUnicode(int c) {\n\t\treturn HexUtil.toUnicodeHex(c);\n\t}\n\n\t/**\n\t * 字符串编码为Unicode形式\n\t *\n\t * @param str 被编码的字符串\n\t * @return Unicode字符串\n\t */\n\tpublic static String toUnicode(String str) {\n\t\treturn toUnicode(str, true);\n\t}\n\n\t/**\n\t * 字符串编码为Unicode形式\n\t *\n\t * @param str         被编码的字符串\n\t * @param isSkipAscii 是否跳过ASCII字符（只跳过可见字符）\n\t * @return Unicode字符串\n\t */\n\tpublic static String toUnicode(String str, boolean isSkipAscii) {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\treturn str;\n\t\t}\n\n\t\tfinal int len = str.length();\n\t\tfinal StringBuilder unicode = new StringBuilder(str.length() * 6);\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tif (isSkipAscii && CharUtil.isAsciiPrintable(c)) {\n\t\t\t\tunicode.append(c);\n\t\t\t} else {\n\t\t\t\tunicode.append(HexUtil.toUnicodeHex(c));\n\t\t\t}\n\t\t}\n\t\treturn unicode.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvBaseReader.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.File;\nimport java.io.Reader;\nimport java.io.Serializable;\nimport java.io.StringReader;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * CSV文件读取器基础类，提供灵活的文件、路径中的CSV读取，一次构造可多次调用读取不同数据，参考：FastCSV\n *\n * @author Looly\n * @since 5.0.4\n */\npublic class CsvBaseReader implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 默认编码\n\t */\n\tprotected static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\n\tprivate final CsvReadConfig config;\n\n\t//--------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，使用默认配置项\n\t */\n\tpublic CsvBaseReader() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 配置项\n\t */\n\tpublic CsvBaseReader(CsvReadConfig config) {\n\t\tthis.config = ObjectUtil.defaultIfNull(config, CsvReadConfig::defaultConfig);\n\t}\n\t//--------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 设置字段分隔符，默认逗号','\n\t *\n\t * @param fieldSeparator 字段分隔符，默认逗号','\n\t */\n\tpublic void setFieldSeparator(char fieldSeparator) {\n\t\tthis.config.setFieldSeparator(fieldSeparator);\n\t}\n\n\t/**\n\t * 设置 文本分隔符，文本包装符，默认双引号'\"'\n\t *\n\t * @param textDelimiter 文本分隔符，文本包装符，默认双引号'\"'\n\t */\n\tpublic void setTextDelimiter(char textDelimiter) {\n\t\tthis.config.setTextDelimiter(textDelimiter);\n\t}\n\n\t/**\n\t * 设置是否首行做为标题行，默认false\n\t *\n\t * @param containsHeader 是否首行做为标题行，默认false\n\t */\n\tpublic void setContainsHeader(boolean containsHeader) {\n\t\tthis.config.setContainsHeader(containsHeader);\n\t}\n\n\t/**\n\t * 设置是否跳过空白行，默认true\n\t *\n\t * @param skipEmptyRows 是否跳过空白行，默认true\n\t */\n\tpublic void setSkipEmptyRows(boolean skipEmptyRows) {\n\t\tthis.config.setSkipEmptyRows(skipEmptyRows);\n\t}\n\n\t/**\n\t * 设置每行字段个数不同时是否抛出异常，默认false\n\t *\n\t * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常，默认false\n\t */\n\tpublic void setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) {\n\t\tthis.config.setErrorOnDifferentFieldCount(errorOnDifferentFieldCount);\n\t}\n\n\t/**\n\t * 读取CSV文件，默认UTF-8编码\n\t *\n\t * @param file CSV文件\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(File file) throws IORuntimeException {\n\t\treturn read(file, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 从字符串中读取CSV数据\n\t *\n\t * @param csvStr CSV字符串\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t */\n\tpublic CsvData readFromStr(String csvStr) {\n\t\treturn read(new StringReader(csvStr));\n\t}\n\n\t/**\n\t * 从字符串中读取CSV数据\n\t *\n\t * @param csvStr     CSV字符串\n\t * @param rowHandler 行处理器，用于一行一行的处理数据\n\t */\n\tpublic void readFromStr(String csvStr, CsvRowHandler rowHandler) {\n\t\tread(parse(new StringReader(csvStr)), true, rowHandler);\n\t}\n\n\n\t/**\n\t * 读取CSV文件\n\t *\n\t * @param file    CSV文件\n\t * @param charset 文件编码，默认系统编码\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(File file, Charset charset) throws IORuntimeException {\n\t\treturn read(Objects.requireNonNull(file.toPath(), \"file must not be null\"), charset);\n\t}\n\n\t/**\n\t * 读取CSV文件，默认UTF-8编码\n\t *\n\t * @param path CSV文件\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(Path path) throws IORuntimeException {\n\t\treturn read(path, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 读取CSV文件\n\t *\n\t * @param path    CSV文件\n\t * @param charset 文件编码，默认系统编码\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(Path path, Charset charset) throws IORuntimeException {\n\t\tAssert.notNull(path, \"path must not be null\");\n\t\treturn read(FileUtil.getReader(path, charset));\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据，读取后关闭Reader\n\t *\n\t * @param reader Reader\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(Reader reader) throws IORuntimeException {\n\t\treturn read(reader, true);\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据\n\t *\n\t * @param reader Reader\n\t * @param close 读取结束是否关闭Reader\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read(Reader reader, boolean close) throws IORuntimeException {\n\t\tfinal CsvParser csvParser = parse(reader);\n\t\tfinal List<CsvRow> rows = new ArrayList<>();\n\t\tread(csvParser, close, rows::add);\n\t\tfinal List<String> header = config.headerLineNo > -1 ? csvParser.getHeader() : null;\n\n\t\treturn new CsvData(header, rows);\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据，结果为Map，读取后关闭Reader。<br>\n\t * 此方法默认识别首行为标题行。\n\t *\n\t * @param reader Reader\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic List<Map<String, String>> readMapList(Reader reader) throws IORuntimeException {\n\t\t// 此方法必须包含标题\n\t\tthis.config.setContainsHeader(true);\n\n\t\tfinal List<Map<String, String>> result = new ArrayList<>();\n\t\tread(reader, (row) -> result.add(row.getFieldMap()));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据并转换为Bean列表，读取后关闭Reader。<br>\n\t * 此方法默认识别首行为标题行。\n\t *\n\t * @param <T>    Bean类型\n\t * @param reader Reader\n\t * @param clazz  Bean类型\n\t * @return Bean列表\n\t */\n\tpublic <T> List<T> read(Reader reader, Class<T> clazz) {\n\t\t// 此方法必须包含标题\n\t\tthis.config.setContainsHeader(true);\n\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tread(reader, (row) -> result.add(row.toBean(clazz)));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从字符串中读取CSV数据并转换为Bean列表，读取后关闭Reader。<br>\n\t * 此方法默认识别首行为标题行。\n\t *\n\t * @param <T>    Bean类型\n\t * @param csvStr csv字符串\n\t * @param clazz  Bean类型\n\t * @return Bean列表\n\t */\n\tpublic <T> List<T> read(String csvStr, Class<T> clazz) {\n\t\t// 此方法必须包含标题\n\t\tthis.config.setContainsHeader(true);\n\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tread(new StringReader(csvStr), (row) -> result.add(row.toBean(clazz)));\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据，读取后关闭Reader\n\t *\n\t * @param reader     Reader\n\t * @param rowHandler 行处理器，用于一行一行的处理数据\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void read(Reader reader, CsvRowHandler rowHandler) throws IORuntimeException {\n\t\tread(reader, true, rowHandler);\n\t}\n\n\t/**\n\t * 从Reader中读取CSV数据，读取后关闭Reader\n\t *\n\t * @param reader     Reader\n\t * @param close     读取结束是否关闭Reader\n\t * @param rowHandler 行处理器，用于一行一行的处理数据\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void read(Reader reader, boolean close, CsvRowHandler rowHandler) throws IORuntimeException {\n\t\tread(parse(reader), close, rowHandler);\n\t}\n\n\t//--------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 读取CSV数据，读取后关闭Parser\n\t *\n\t * @param csvParser  CSV解析器\n\t * @param close      读取结束是否关闭{@link CsvParser}\n\t * @param rowHandler 行处理器，用于一行一行的处理数据\n\t * @throws IORuntimeException IO异常\n\t * @since 5.0.4\n\t */\n\tprivate void read(CsvParser csvParser, boolean close, CsvRowHandler rowHandler) throws IORuntimeException {\n\t\ttry {\n\t\t\twhile (csvParser.hasNext()) {\n\t\t\t\trowHandler.handle(csvParser.next());\n\t\t\t}\n\t\t} finally {\n\t\t\tif(close){\n\t\t\t\tIoUtil.close(csvParser);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 构建 {@link CsvParser}\n\t *\n\t * @param reader Reader\n\t * @return CsvParser\n\t * @throws IORuntimeException IO异常\n\t */\n\tprotected CsvParser parse(Reader reader) throws IORuntimeException {\n\t\treturn new CsvParser(reader, this.config);\n\t}\n\t//--------------------------------------------------------------------------------------------- Private method start\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvConfig.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.util.CharUtil;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * CSV基础配置项，此配置项可用于读取和写出CSV，定义了包括字段分隔符、文本包装符等符号\n *\n * @param <T> 继承子类类型，用于this返回\n * @author looly\n * @since 4.0.5\n */\n@SuppressWarnings(\"unchecked\")\npublic class CsvConfig<T extends CsvConfig<T>> implements Serializable {\n\tprivate static final long serialVersionUID = -8069578249066158459L;\n\n\t/**\n\t * 字段分隔符，默认逗号','\n\t */\n\tprotected char fieldSeparator = CharUtil.COMMA;\n\t/**\n\t * 文本包装符，默认双引号'\"'\n\t */\n\tprotected char textDelimiter = CharUtil.DOUBLE_QUOTES;\n\t/**\n\t * 注释符号，用于区分注释行，默认'#'\n\t */\n\tprotected Character commentCharacter = '#';\n\t/**\n\t * 标题别名\n\t */\n\tprotected Map<String, String> headerAlias = new LinkedHashMap<>();\n\n\t/**\n\t * 设置字段分隔符，默认逗号','\n\t *\n\t * @param fieldSeparator 字段分隔符，默认逗号','\n\t * @return this\n\t */\n\tpublic T setFieldSeparator(final char fieldSeparator) {\n\t\tthis.fieldSeparator = fieldSeparator;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置 文本分隔符，文本包装符，默认双引号'\"'\n\t *\n\t * @param textDelimiter 文本分隔符，文本包装符，默认双引号'\"'\n\t * @return this\n\t */\n\tpublic T setTextDelimiter(char textDelimiter) {\n\t\tthis.textDelimiter = textDelimiter;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置注释无效<br>\n\t * 当写出CSV时，{@link CsvWriter#writeComment(String)}将抛出异常<br>\n\t * 当读取CSV时，注释行按照正常行读取\n\t *\n\t * @return this\n\t * @since 5.7.14\n\t */\n\tpublic T disableComment() {\n\t\treturn setCommentCharacter(null);\n\t}\n\n\t/**\n\t * 设置 注释符号，用于区分注释行，{@code null}表示忽略注释\n\t *\n\t * @param commentCharacter 注释符号，用于区分注释行\n\t * @return this\n\t * @since 5.5.7\n\t */\n\tpublic T setCommentCharacter(Character commentCharacter) {\n\t\tthis.commentCharacter = commentCharacter;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置标题行的别名Map\n\t *\n\t * @param headerAlias 别名Map\n\t * @return this\n\t * @since 5.7.10\n\t */\n\tpublic T setHeaderAlias(Map<String, String> headerAlias) {\n\t\tthis.headerAlias = headerAlias;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 增加标题别名\n\t *\n\t * @param header 标题\n\t * @param alias  别名\n\t * @return this\n\t * @since 5.7.10\n\t */\n\tpublic T addHeaderAlias(String header, String alias) {\n\t\tthis.headerAlias.put(header, alias);\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 去除标题别名\n\t *\n\t * @param header 标题\n\t * @return this\n\t * @since 5.7.10\n\t */\n\tpublic T removeHeaderAlias(String header) {\n\t\tthis.headerAlias.remove(header);\n\t\treturn (T) this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvData.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * CSV数据，包括头部信息和行数据，参考：FastCSV\n *\n * @author Looly\n */\npublic class CsvData implements Iterable<CsvRow>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final List<String> header;\n\tprivate final List<CsvRow> rows;\n\n\t/**\n\t * 构造\n\t *\n\t * @param header 头信息, 可以为null\n\t * @param rows 行\n\t */\n\tpublic CsvData(final List<String> header, final List<CsvRow> rows) {\n\t\tthis.header = header;\n\t\tthis.rows = rows;\n\t}\n\n\t/**\n\t * 总行数\n\t *\n\t * @return 总行数\n\t */\n\tpublic int getRowCount() {\n\t\treturn this.rows.size();\n\t}\n\n\t/**\n\t * 获取头信息列表，如果无头信息为{@code Null}，返回列表为只读列表\n\t *\n\t * @return the header row - might be {@code null} if no header exists\n\t */\n\tpublic List<String> getHeader() {\n\t\tif(null == this.header){\n\t\t\treturn null;\n\t\t}\n\t\treturn Collections.unmodifiableList(this.header);\n\t}\n\n\t/**\n\t * 获取指定行，从0开始\n\t *\n\t * @param index 行号\n\t * @return 行数据\n\t * @throws IndexOutOfBoundsException if index is out of range\n\t */\n\tpublic CsvRow getRow(final int index) {\n\t\treturn this.rows.get(index);\n\t}\n\n\t/**\n\t * 获取所有行\n\t *\n\t * @return 所有行\n\t */\n\tpublic List<CsvRow> getRows() {\n\t\treturn this.rows;\n\t}\n\n\t@Override\n\tpublic Iterator<CsvRow> iterator() {\n\t\treturn this.rows.iterator();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CsvData{\" +\n\t\t\t\t\"header=\" + header +\n\t\t\t\t\", rows=\" + rows +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java",
    "content": "/*\n * Copyright (c) 2013-2024 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.text.csv;\n\nimport cn.hutool.core.collection.ComputeIter;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * CSV行解析器，参考：FastCSV\n *\n * @author Looly\n */\npublic final class CsvParser extends ComputeIter<CsvRow> implements Closeable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int DEFAULT_ROW_CAPACITY = 10;\n\n\tprivate final CsvReadConfig config;\n\tprivate final CsvTokener tokener;\n\t/**\n\t * 前一个特殊分界字符\n\t */\n\tprivate int preChar = -1;\n\t/**\n\t * 是否在引号包装内\n\t */\n\tprivate boolean inQuotes;\n\t/**\n\t * 当前读取字段\n\t */\n\tprivate final StringBuilder currentField = new StringBuilder(512);\n\n\t/**\n\t * 标题行\n\t */\n\tprivate CsvRow header;\n\t/**\n\t * 当前行号\n\t */\n\tprivate long lineNo = -1;\n\t/**\n\t * 引号内的行数\n\t */\n\tprivate long inQuotesLineCount;\n\t/**\n\t * 第一行字段数，用于检查每行字段数是否一致\n\t */\n\tprivate int firstLineFieldCount = -1;\n\t/**\n\t * 最大字段数量，用于初始化行，减少扩容\n\t */\n\tprivate int maxFieldCount;\n\t/**\n\t * 是否读取结束\n\t */\n\tprivate boolean finished;\n\n\t/**\n\t * CSV解析器\n\t *\n\t * @param reader Reader\n\t * @param config 配置，null则为默认配置\n\t */\n\tpublic CsvParser(final Reader reader, final CsvReadConfig config) {\n\t\tthis.config = ObjUtil.defaultIfNull(config, CsvReadConfig::defaultConfig);\n\t\tthis.tokener = new CsvTokener(reader);\n\t}\n\n\t/**\n\t * 获取头部字段列表，如果headerLineNo &lt; 0，抛出异常\n\t *\n\t * @return 头部列表\n\t * @throws IllegalStateException 如果不解析头部或者没有调用nextRow()方法\n\t */\n\tpublic List<String> getHeader() {\n\t\tif (config.headerLineNo < 0) {\n\t\t\tthrow new IllegalStateException(\"No header available - header parsing is disabled\");\n\t\t}\n\t\tif (lineNo < config.beginLineNo) {\n\t\t\tthrow new IllegalStateException(\"No header available - call nextRow() first\");\n\t\t}\n\t\treturn header.getRawList();\n\t}\n\n\t@Override\n\tprotected CsvRow computeNext() {\n\t\treturn nextRow();\n\t}\n\n\t/**\n\t * 读取下一行数据\n\t *\n\t * @return CsvRow，{@code null}表示读取结束\n\t * @throws IORuntimeException IO读取异常\n\t */\n\tpublic CsvRow nextRow() throws IORuntimeException {\n\t\tList<String> currentFields;\n\t\tint fieldCount;\n\t\twhile (!finished) {\n\t\t\tcurrentFields = readLine();\n\t\t\tfieldCount = currentFields.size();\n\t\t\tif (fieldCount < 1) {\n\t\t\t\t// 空List表示读取结束\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// 读取范围校验\n\t\t\tif (lineNo < config.beginLineNo) {\n\t\t\t\t// 未达到读取起始行，继续\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (lineNo > config.endLineNo) {\n\t\t\t\t// 超出结束行，读取结束\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// 跳过空行\n\t\t\tif (config.skipEmptyRows && fieldCount == 1 && currentFields.get(0).isEmpty()) {\n\t\t\t\t// [\"\"]表示空行\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 检查每行的字段数是否一致\n\t\t\tif (config.errorOnDifferentFieldCount) {\n\t\t\t\tif (firstLineFieldCount < 0) {\n\t\t\t\t\tfirstLineFieldCount = fieldCount;\n\t\t\t\t} else if (fieldCount != firstLineFieldCount) {\n\t\t\t\t\tthrow new IORuntimeException(String.format(\"Line %d has %d fields, but first line has %d fields\", lineNo, fieldCount, firstLineFieldCount));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 记录最大字段数\n\t\t\tif (fieldCount > maxFieldCount) {\n\t\t\t\tmaxFieldCount = fieldCount;\n\t\t\t}\n\n\t\t\t//初始化标题\n\t\t\tif (lineNo == config.headerLineNo && null == header) {\n\t\t\t\tinitHeader(currentFields);\n\t\t\t\t// 作为标题行后，此行跳过，下一行做为第一行\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\treturn new CsvRow(lineNo, null == header ? null : header.headerMap, currentFields);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 当前行做为标题行\n\t *\n\t * @param currentFields 当前行字段列表\n\t */\n\tprivate void initHeader(final List<String> currentFields) {\n\t\tfinal Map<String, Integer> localHeaderMap = new LinkedHashMap<>(currentFields.size());\n\t\tfor (int i = 0; i < currentFields.size(); i++) {\n\t\t\tString field = currentFields.get(i);\n\t\t\tif (MapUtil.isNotEmpty(this.config.headerAlias)) {\n\t\t\t\t// 自定义别名\n\t\t\t\tfield = ObjUtil.defaultIfNull(this.config.headerAlias.get(field), field);\n\t\t\t}\n\t\t\tif (StrUtil.isNotEmpty(field) && !localHeaderMap.containsKey(field)) {\n\t\t\t\tlocalHeaderMap.put(field, i);\n\t\t\t}\n\t\t}\n\n\t\theader = new CsvRow(this.lineNo, Collections.unmodifiableMap(localHeaderMap), Collections.unmodifiableList(currentFields));\n\t}\n\n\t/**\n\t * 读取一行数据，如果读取结束，返回size为0的List<br>\n\t * 空行是size为1的List，唯一元素是\"\"\n\t *\n\t * <p>\n\t * 行号要考虑注释行和引号包装的内容中的换行\n\t * </p>\n\t *\n\t * @return 一行数据\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate List<String> readLine() throws IORuntimeException {\n\t\t// 矫正行号\n\t\t// 当一行内容包含多行数据时，记录首行行号，但是读取下一行时，需要把多行内容的行数加上\n\t\tif (inQuotesLineCount > 0) {\n\t\t\tthis.lineNo += this.inQuotesLineCount;\n\t\t\tthis.inQuotesLineCount = 0;\n\t\t}\n\n\t\tfinal List<String> currentFields = new ArrayList<>(maxFieldCount > 0 ? maxFieldCount : DEFAULT_ROW_CAPACITY);\n\n\t\tfinal StringBuilder currentField = this.currentField;\n\t\tint preChar = this.preChar;//前一个特殊分界字符\n\t\tboolean inComment = false;\n\n\t\tint c;\n\t\twhile (true) {\n\t\t\tc = tokener.next();\n\t\t\tif(c < 0){\n\t\t\t\tif (currentField.length() > 0 || preChar == config.fieldSeparator) {\n\t\t\t\t\tif(this.inQuotes){\n\t\t\t\t\t\t// 未闭合的文本包装，在末尾补充包装符\n\t\t\t\t\t\tcurrentField.append(config.textDelimiter);\n\t\t\t\t\t}\n\n\t\t\t\t\t//剩余部分作为一个字段\n\t\t\t\t\taddField(currentFields, currentField.toString());\n\t\t\t\t\tcurrentField.setLength(0);\n\t\t\t\t}\n\t\t\t\t// 读取结束\n\t\t\t\tthis.finished = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// 注释行标记\n\t\t\tif (preChar < 0 || preChar == CharUtil.CR || preChar == CharUtil.LF) {\n\t\t\t\t// 判断行首字符为指定注释字符的注释开始，直到遇到换行符\n\t\t\t\t// 行首分两种，1是preChar < 0表示文本开始，2是换行符后紧跟就是下一行的开始\n\t\t\t\t// issue#IA8WE0 如果注释符出现在包装符内，被认为是普通字符\n\t\t\t\tif (!inQuotes && null != this.config.commentCharacter && c == this.config.commentCharacter) {\n\t\t\t\t\tinComment = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 注释行处理\n\t\t\tif (inComment) {\n\t\t\t\tif (c == CharUtil.CR || c == CharUtil.LF) {\n\t\t\t\t\t// 注释行以换行符为结尾\n\t\t\t\t\tlineNo++;\n\t\t\t\t\tinComment = false;\n\t\t\t\t}\n\t\t\t\t// 跳过注释行中的任何字符\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (inQuotes) {\n\t\t\t\t//引号内，作为内容，直到引号结束\n\t\t\t\tif (c == config.textDelimiter) {\n\t\t\t\t\t// issue#IB5UQ8 文本包装符转义\n\t\t\t\t\tfinal int next = tokener.next();\n\t\t\t\t\tif(next != config.textDelimiter){\n\t\t\t\t\t\t// 包装结束\n\t\t\t\t\t\tinQuotes = false;\n\t\t\t\t\t\ttokener.back();\n\t\t\t\t\t}\n\t\t\t\t\t// https://datatracker.ietf.org/doc/html/rfc4180#section-2 跳过转义符，只保留被转义的包装符\n\t\t\t\t} else {\n\t\t\t\t\t// 字段内容中新行\n\t\t\t\t\tif (isLineEnd(c, preChar)) {\n\t\t\t\t\t\tinQuotesLineCount++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// 普通字段字符\n\t\t\t\tcurrentField.append((char)c);\n\t\t\t} else {\n\t\t\t\t// 非引号内\n\t\t\t\tif (c == config.fieldSeparator) {\n\t\t\t\t\t//一个字段结束\n\t\t\t\t\taddField(currentFields, currentField.toString());\n\t\t\t\t\tcurrentField.setLength(0);\n\t\t\t\t} else if (c == config.textDelimiter && isFieldBegin(preChar)) {\n\t\t\t\t\t// 引号开始且出现在字段开头\n\t\t\t\t\tinQuotes = true;\n\t\t\t\t\tcurrentField.append((char)c);\n\t\t\t\t} else if (c == CharUtil.CR) {\n\t\t\t\t\t// \\r\n\t\t\t\t\taddField(currentFields, currentField.toString());\n\t\t\t\t\tcurrentField.setLength(0);\n\t\t\t\t\tpreChar = c;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (c == CharUtil.LF) {\n\t\t\t\t\t// \\n\n\t\t\t\t\tif (preChar != CharUtil.CR) {\n\t\t\t\t\t\taddField(currentFields, currentField.toString());\n\t\t\t\t\t\tcurrentField.setLength(0);\n\t\t\t\t\t\tpreChar = c;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t// 前一个字符是\\r，已经处理过这个字段了，此处直接跳过\n\t\t\t\t} else {\n\t\t\t\t\tcurrentField.append((char)c);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreChar = c;\n\t\t}\n\n\t\t// restore fields\n\t\tthis.preChar = preChar;\n\n\t\tlineNo++;\n\t\treturn currentFields;\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\ttokener.close();\n\t}\n\n\t/**\n\t * 将字段加入字段列表并自动去包装和去转义\n\t *\n\t * @param currentFields 当前的字段列表（即为行）\n\t * @param field         字段\n\t */\n\tprivate void addField(final List<String> currentFields, String field) {\n\t\tfinal char textDelimiter = this.config.textDelimiter;\n\n\t\t// 忽略多余引号后的换行符\n\t\tfield = StrUtil.trim(field, 1, (c-> c == CharUtil.LF || c == CharUtil.CR));\n\n\t\tif(StrUtil.isWrap(field, textDelimiter)){\n\t\t\tfield = StrUtil.sub(field, 1, field.length() - 1);\n\t\t}\n\n\t\tif (this.config.trimField) {\n\t\t\t// issue#I49M0C@Gitee\n\t\t\tfield = StrUtil.trim(field);\n\t\t}\n\t\tcurrentFields.add(field);\n\t}\n\n\t/**\n\t * 是否行结束符\n\t *\n\t * @param c       符号\n\t * @param preChar 前一个字符\n\t * @return 是否结束\n\t * @since 5.7.4\n\t */\n\tprivate boolean isLineEnd(final int c, final int preChar) {\n\t\treturn (c == CharUtil.CR || c == CharUtil.LF) && preChar != CharUtil.CR;\n\t}\n\n\t/**\n\t * 通过前一个字符，判断是否字段开始，几种情况：\n\t * <ul>\n\t *     <li>正文开头，无前字符</li>\n\t *     <li>字段分隔符，即上个字段结束</li>\n\t *     <li>换行符，即新行开始</li>\n\t * </ul>\n\t *\n\t * @param preChar 前字符\n\t * @return 是否字段开始\n\t */\n\tprivate boolean isFieldBegin(final int preChar) {\n\t\treturn preChar == -1\n\t\t\t|| preChar == config.fieldSeparator\n\t\t\t|| preChar == CharUtil.LF\n\t\t\t|| preChar == CharUtil.CR;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport java.io.Serializable;\n\n/**\n * CSV读取配置项\n *\n * @author looly\n *\n */\npublic class CsvReadConfig extends CsvConfig<CsvReadConfig> implements Serializable {\n\tprivate static final long serialVersionUID = 5396453565371560052L;\n\n\t/** 指定标题行号，-1表示无标题行 */\n\tprotected long headerLineNo = -1;\n\t/** 是否跳过空白行，默认true */\n\tprotected boolean skipEmptyRows = true;\n\t/** 每行字段个数不同时是否抛出异常，默认false */\n\tprotected boolean errorOnDifferentFieldCount;\n\t/** 定义开始的行（包括），此处为原始文件行号 */\n\tprotected long beginLineNo;\n\t/** 结束的行（包括），此处为原始文件行号 */\n\tprotected long endLineNo = Long.MAX_VALUE-1;\n\t/** 每个字段是否去除两边空白符 */\n\tprotected boolean trimField;\n\n\t/**\n\t * 默认配置\n\t *\n\t * @return 默认配置\n\t */\n\tpublic static CsvReadConfig defaultConfig() {\n\t\treturn new CsvReadConfig();\n\t}\n\n\t/**\n\t * 设置是否首行做为标题行，默认false<br>\n\t * 当设置为{@code true}时，默认标题行号是{@link #beginLineNo}，{@code false}为-1，表示无行号\n\t *\n\t * @param containsHeader 是否首行做为标题行，默认false\n\t * @return this\n\t * @see #setHeaderLineNo(long)\n\t */\n\tpublic CsvReadConfig setContainsHeader(boolean containsHeader) {\n\t\treturn setHeaderLineNo(containsHeader ? beginLineNo : -1);\n\t}\n\n\t/**\n\t * 设置标题行行号，默认-1，表示无标题行<br>\n\t *\n\t * @param headerLineNo 标题行行号，-1表示无标题行\n\t * @return this\n\t * @since 5.7.23\n\t */\n\tpublic CsvReadConfig setHeaderLineNo(long headerLineNo) {\n\t\tthis.headerLineNo = headerLineNo;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否跳过空白行，默认true\n\t *\n\t * @param skipEmptyRows 是否跳过空白行，默认true\n\t * @return this\n\t */\n\tpublic CsvReadConfig setSkipEmptyRows(boolean skipEmptyRows) {\n\t\tthis.skipEmptyRows = skipEmptyRows;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置每行字段个数不同时是否抛出异常，默认false\n\t *\n\t * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常，默认false\n\t * @return this\n\t */\n\tpublic CsvReadConfig setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) {\n\t\tthis.errorOnDifferentFieldCount = errorOnDifferentFieldCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置开始的行（包括），默认0，此处为原始文件行号\n\t *\n\t * @param beginLineNo 开始的行号（包括）\n\t * @return this\n\t * @since 5.7.4\n\t */\n\tpublic CsvReadConfig setBeginLineNo(long beginLineNo) {\n\t\tthis.beginLineNo = beginLineNo;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置结束的行（包括），默认不限制，此处为原始文件行号\n\t *\n\t * @param endLineNo 结束的行号（包括）\n\t * @return this\n\t * @since 5.7.4\n\t */\n\tpublic CsvReadConfig setEndLineNo(long endLineNo) {\n\t\tthis.endLineNo = endLineNo;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置每个字段是否去除两边空白符<br>\n\t * 如果字段以{@link #textDelimiter}包围，则保留两边空格\n\t *\n\t * @param trimField 去除两边空白符\n\t * @return this\n\t * @since 5.7.13\n\t */\n\tpublic CsvReadConfig setTrimField(boolean trimField) {\n\t\tthis.trimField = trimField;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReader.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.util.Iterator;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\n/**\n * CSV文件读取器，参考：FastCSV\n *\n * @author Looly\n * @since 4.0.1\n */\npublic class CsvReader extends CsvBaseReader implements Iterable<CsvRow>, Closeable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Reader reader;\n\n\t//--------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，使用默认配置项\n\t */\n\tpublic CsvReader() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 配置项\n\t */\n\tpublic CsvReader(CsvReadConfig config) {\n\t\tthis((Reader) null, config);\n\t}\n\n\t/**\n\t * 构造，默认{@link #DEFAULT_CHARSET}编码\n\t *\n\t * @param file   CSV文件路径，null表示不设置路径\n\t * @param config 配置项，null表示默认配置\n\t * @since 5.0.4\n\t */\n\tpublic CsvReader(File file, CsvReadConfig config) {\n\t\tthis(file, DEFAULT_CHARSET, config);\n\t}\n\n\t/**\n\t * 构造，默认{@link #DEFAULT_CHARSET}编码\n\t *\n\t * @param path   CSV文件路径，null表示不设置路径\n\t * @param config 配置项，null表示默认配置\n\t * @since 5.0.4\n\t */\n\tpublic CsvReader(Path path, CsvReadConfig config) {\n\t\tthis(path, DEFAULT_CHARSET, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file    CSV文件路径，null表示不设置路径\n\t * @param charset 编码\n\t * @param config  配置项，null表示默认配置\n\t * @since 5.0.4\n\t */\n\tpublic CsvReader(File file, Charset charset, CsvReadConfig config) {\n\t\tthis(FileUtil.getReader(file, charset), config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path    CSV文件路径，null表示不设置路径\n\t * @param charset 编码\n\t * @param config  配置项，null表示默认配置\n\t * @since 5.0.4\n\t */\n\tpublic CsvReader(Path path, Charset charset, CsvReadConfig config) {\n\t\tthis(FileUtil.getReader(path, charset), config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param reader {@link Reader}，null表示不设置默认reader\n\t * @param config 配置项，null表示默认配置\n\t * @since 5.0.4\n\t */\n\tpublic CsvReader(Reader reader, CsvReadConfig config) {\n\t\tsuper(config);\n\t\tthis.reader = reader;\n\t}\n\t//--------------------------------------------------------------------------------------------- Constructor end\n\t/**\n\t * 读取CSV文件，此方法只能调用一次<br>\n\t * 调用此方法的前提是构造中传入文件路径或Reader\n\t *\n\t * @return {@link CsvData}，包含数据列表和行信息\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvData read() throws IORuntimeException {\n\t\treturn read(this.reader, false);\n\t}\n\n\t/**\n\t * 读取CSV数据，此方法只能调用一次<br>\n\t * 调用此方法的前提是构造中传入文件路径或Reader\n\t *\n\t * @param rowHandler 行处理器，用于一行一行的处理数据\n\t * @throws IORuntimeException IO异常\n\t * @since 5.0.4\n\t */\n\tpublic void read(CsvRowHandler rowHandler) throws IORuntimeException {\n\t\tread(this.reader, false, rowHandler);\n\t}\n\n\t/**\n\t * 根据Reader创建{@link Stream}，以便使用stream方式读取csv行\n\t *\n\t * @return {@link Stream}\n\t * @since 5.7.14\n\t */\n\tpublic Stream<CsvRow> stream() {\n\t\treturn StreamSupport.stream(spliterator(), false)\n\t\t\t\t.onClose(() -> {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tclose();\n\t\t\t\t\t} catch (final IOException e) {\n\t\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t\t}\n\t\t\t\t});\n\t}\n\n\t@Override\n\tpublic Iterator<CsvRow> iterator() {\n\t\treturn parse(this.reader);\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tIoUtil.close(this.reader);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRow.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.ListIterator;\nimport java.util.Map;\n\n/**\n * CSV中一行的表示\n *\n * @author Looly\n */\npublic final class CsvRow implements List<String> {\n\n\t/** 原始行号 */\n\tprivate final long originalLineNumber;\n\n\tfinal Map<String, Integer> headerMap;\n\tfinal List<String> fields;\n\n\t/**\n\t * 构造\n\t *\n\t * @param originalLineNumber 对应文件中的第几行\n\t * @param headerMap 标题Map\n\t * @param fields 数据列表\n\t */\n\tpublic CsvRow(long originalLineNumber, Map<String, Integer> headerMap, List<String> fields) {\n\t\tAssert.notNull(fields, \"fields must be not null!\");\n\t\tthis.originalLineNumber = originalLineNumber;\n\t\tthis.headerMap = headerMap;\n\t\tthis.fields = fields;\n\t}\n\n\t/**\n\t * 获取原始行号，多行情况下为首行行号。忽略注释行\n\t *\n\t * @return the original line number 行号\n\t */\n\tpublic long getOriginalLineNumber() {\n\t\treturn originalLineNumber;\n\t}\n\n\t/**\n\t * 获取标题对应的字段内容\n\t *\n\t * @param name 标题名\n\t * @return 字段值，null表示无此字段值\n\t * @throws IllegalStateException CSV文件无标题行抛出此异常\n\t */\n\tpublic String getByName(String name) {\n\t\tAssert.notNull(this.headerMap, \"No header available!\");\n\n\t\tfinal Integer col = headerMap.get(name);\n\t\tif (col != null) {\n\t\t\treturn get(col);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取本行所有字段值列表\n\t *\n\t * @return 字段值列表\n\t */\n\tpublic List<String> getRawList() {\n\t\treturn fields;\n\t}\n\n\t/**\n\t * 获取标题与字段值对应的Map\n\t *\n\t * @return 标题与字段值对应的Map\n\t * @throws IllegalStateException CSV文件无标题行抛出此异常\n\t */\n\tpublic Map<String, String> getFieldMap() {\n\t\tif (headerMap == null) {\n\t\t\tthrow new IllegalStateException(\"No header available\");\n\t\t}\n\n\t\tfinal Map<String, String> fieldMap = new LinkedHashMap<>(headerMap.size(), 1);\n\t\tString key;\n\t\tInteger col;\n\t\tString val;\n\t\tfor (final Map.Entry<String, Integer> header : headerMap.entrySet()) {\n\t\t\tkey = header.getKey();\n\t\t\tcol = headerMap.get(key);\n\t\t\tval = null == col ? null : get(col);\n\t\t\tfieldMap.put(key, val);\n\t\t}\n\n\t\treturn fieldMap;\n\t}\n\n\t/**\n\t * 一行数据转换为Bean对象\n\t *\n\t * @param <T> Bean类型\n\t * @param clazz bean类\n\t * @return Bean\n\t * @since 5.3.6\n\t */\n\tpublic <T> T toBean(Class<T> clazz){\n\t\treturn BeanUtil.toBeanIgnoreError(getFieldMap(), clazz);\n\t}\n\n\t/**\n\t * 获取字段格式\n\t *\n\t * @return 字段格式\n\t */\n\tpublic int getFieldCount() {\n\t\treturn fields.size();\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn this.fields.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn this.fields.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean contains(Object o) {\n\t\treturn this.fields.contains(o);\n\t}\n\n\t@Override\n\tpublic Iterator<String> iterator() {\n\t\treturn this.fields.iterator();\n\t}\n\n\t@Override\n\tpublic Object[] toArray() {\n\t\treturn this.fields.toArray();\n\t}\n\n\t@Override\n\tpublic <T> T[] toArray(T[] a) {\n\t\t//noinspection SuspiciousToArrayCall\n\t\treturn this.fields.toArray(a);\n\t}\n\n\t@Override\n\tpublic boolean add(String e) {\n\t\treturn this.fields.add(e);\n\t}\n\n\t@Override\n\tpublic boolean remove(Object o) {\n\t\treturn this.fields.remove(o);\n\t}\n\n\t@Override\n\tpublic boolean containsAll(Collection<?> c) {\n\t\treturn this.fields.containsAll(c);\n\t}\n\n\t@Override\n\tpublic boolean addAll(Collection<? extends String> c) {\n\t\treturn this.fields.addAll(c);\n\t}\n\n\t@Override\n\tpublic boolean addAll(int index, Collection<? extends String> c) {\n\t\treturn this.fields.addAll(index, c);\n\t}\n\n\t@Override\n\tpublic boolean removeAll(Collection<?> c) {\n\t\treturn this.fields.removeAll(c);\n\t}\n\n\t@Override\n\tpublic boolean retainAll(Collection<?> c) {\n\t\treturn this.fields.retainAll(c);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tthis.fields.clear();\n\t}\n\n\t@Override\n\tpublic String get(int index) {\n\t\treturn index >= fields.size() ? null : fields.get(index);\n\t}\n\n\t@Override\n\tpublic String set(int index, String element) {\n\t\treturn this.fields.set(index, element);\n\t}\n\n\t@Override\n\tpublic void add(int index, String element) {\n\t\tthis.fields.add(index, element);\n\t}\n\n\t@Override\n\tpublic String remove(int index) {\n\t\treturn this.fields.remove(index);\n\t}\n\n\t@Override\n\tpublic int indexOf(Object o) {\n\t\treturn this.fields.indexOf(o);\n\t}\n\n\t@Override\n\tpublic int lastIndexOf(Object o) {\n\t\treturn this.fields.lastIndexOf(o);\n\t}\n\n\t@Override\n\tpublic ListIterator<String> listIterator() {\n\t\treturn this.fields.listIterator();\n\t}\n\n\t@Override\n\tpublic ListIterator<String> listIterator(int index) {\n\t\treturn this.fields.listIterator(index);\n\t}\n\n\t@Override\n\tpublic List<String> subList(int fromIndex, int toIndex) {\n\t\treturn this.fields.subList(fromIndex, toIndex);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder sb = new StringBuilder(\"CsvRow{\");\n\t\tsb.append(\"originalLineNumber=\");\n\t\tsb.append(originalLineNumber);\n\t\tsb.append(\", \");\n\n\t\tsb.append(\"fields=\");\n\t\tif (headerMap != null) {\n\t\t\tsb.append('{');\n\t\t\tfor (final Iterator<Map.Entry<String, String>> it = getFieldMap().entrySet().iterator(); it.hasNext();) {\n\n\t\t\t\tfinal Map.Entry<String, String> entry = it.next();\n\t\t\t\tsb.append(entry.getKey());\n\t\t\t\tsb.append('=');\n\t\t\t\tif (entry.getValue() != null) {\n\t\t\t\t\tsb.append(entry.getValue());\n\t\t\t\t}\n\t\t\t\tif (it.hasNext()) {\n\t\t\t\t\tsb.append(\", \");\n\t\t\t\t}\n\t\t\t}\n\t\t\tsb.append('}');\n\t\t} else {\n\t\t\tsb.append(fields.toString());\n\t\t}\n\n\t\tsb.append('}');\n\t\treturn sb.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRowHandler.java",
    "content": "package cn.hutool.core.text.csv;\n\n/**\n * CSV的行处理器，实现此接口用于按照行处理数据\n *\n * @author Looly\n * @since 5.0.4\n */\n@FunctionalInterface\npublic interface CsvRowHandler {\n\n\t/**\n\t * 处理行数据\n\t *\n\t * @param row 行数据\n\t */\n\tvoid handle(CsvRow row);\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvTokener.java",
    "content": "/*\n * Copyright (c) 2024 Hutool Team and hutool.cn\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.Reader;\n\n/**\n * CSV解析器，用于解析CSV文件\n *\n * @author looly\n * @since 5.8.0\n */\npublic class CsvTokener implements Closeable {\n\n\tprivate final Reader raw;\n\t/**\n\t * 在Reader的位置（解析到第几个字符）\n\t */\n\tprivate long index;\n\t/**\n\t * 前一个字符\n\t */\n\tprivate int prev;\n\t/**\n\t * 是否使用前一个字符\n\t */\n\tprivate boolean usePrev;\n\n\t/**\n\t * 构造\n\t *\n\t * @param reader {@link Reader}\n\t */\n\tpublic CsvTokener(final Reader reader) {\n\t\tthis.raw = IoUtil.toBuffered(reader);\n\t}\n\n\t/**\n\t * 读取下一个字符，并记录位置\n\t *\n\t * @return 下一个字符\n\t */\n\tpublic int next() {\n\t\tif(this.usePrev){\n\t\t\tthis.usePrev = false;\n\t\t}else{\n\t\t\ttry {\n\t\t\t\tthis.prev = this.raw.read();\n\t\t\t} catch (final IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\tthis.index++;\n\t\treturn this.prev;\n\t}\n\n\t/**\n\t * 将标记回退到第一个字符\n\t *\n\t * @throws IllegalStateException 当多次调用back时，抛出此异常\n\t */\n\tpublic void back() throws IllegalStateException {\n\t\tif (this.usePrev || this.index <= 0) {\n\t\t\tthrow new IllegalStateException(\"Stepping back two steps is not supported\");\n\t\t}\n\t\tthis.index --;\n\t\tthis.usePrev = true;\n\t}\n\n\t/**\n\t * 获取当前位置\n\t *\n\t * @return 位置\n\t */\n\tpublic long getIndex() {\n\t\treturn this.index;\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tIoUtil.close(this.raw);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvUtil.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport java.io.File;\nimport java.io.Reader;\nimport java.io.Writer;\nimport java.nio.charset.Charset;\n\n/**\n * CSV工具\n *\n * @author looly\n * @since 4.0.5\n */\npublic class CsvUtil {\n\n\t//----------------------------------------------------------------------------------------------------------- Reader\n\n\t/**\n\t * 获取CSV读取器，调用此方法创建的Reader须自行指定读取的资源\n\t *\n\t * @param config 配置, 允许为空.\n\t * @return {@link CsvReader}\n\t */\n\tpublic static CsvReader getReader(CsvReadConfig config) {\n\t\treturn new CsvReader(config);\n\t}\n\n\t/**\n\t * 获取CSV读取器，调用此方法创建的Reader须自行指定读取的资源\n\t *\n\t * @return {@link CsvReader}\n\t */\n\tpublic static CsvReader getReader() {\n\t\treturn new CsvReader();\n\t}\n\n\t/**\n\t * 获取CSV读取器\n\t *\n\t * @param reader {@link Reader}\n\t * @param config 配置, {@code null}表示默认配置\n\t * @return {@link CsvReader}\n\t * @since 5.7.14\n\t */\n\tpublic static CsvReader getReader(Reader reader, CsvReadConfig config) {\n\t\treturn new CsvReader(reader, config);\n\t}\n\n\t/**\n\t * 获取CSV读取器\n\t *\n\t * @param reader {@link Reader}\n\t * @return {@link CsvReader}\n\t * @since 5.7.14\n\t */\n\tpublic static CsvReader getReader(Reader reader) {\n\t\treturn getReader(reader, null);\n\t}\n\n\t//----------------------------------------------------------------------------------------------------------- Writer\n\n\t/**\n\t * 获取CSV生成器（写出器），使用默认配置，覆盖已有文件（如果存在）\n\t *\n\t * @param filePath File CSV文件路径\n\t * @param charset  编码\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(String filePath, Charset charset) {\n\t\treturn new CsvWriter(filePath, charset);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器），使用默认配置，覆盖已有文件（如果存在）\n\t *\n\t * @param file    File CSV文件\n\t * @param charset 编码\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(File file, Charset charset) {\n\t\treturn new CsvWriter(file, charset);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器），使用默认配置\n\t *\n\t * @param filePath File CSV文件路径\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(String filePath, Charset charset, boolean isAppend) {\n\t\treturn new CsvWriter(filePath, charset, isAppend);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器），使用默认配置\n\t *\n\t * @param file     File CSV文件\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(File file, Charset charset, boolean isAppend) {\n\t\treturn new CsvWriter(file, charset, isAppend);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器）\n\t *\n\t * @param file     File CSV文件\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t * @param config   写出配置，null则使用默认配置\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(File file, Charset charset, boolean isAppend, CsvWriteConfig config) {\n\t\treturn new CsvWriter(file, charset, isAppend, config);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器）\n\t *\n\t * @param writer Writer\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(Writer writer) {\n\t\treturn new CsvWriter(writer);\n\t}\n\n\t/**\n\t * 获取CSV生成器（写出器）\n\t *\n\t * @param writer Writer\n\t * @param config 写出配置，null则使用默认配置\n\t * @return {@link CsvWriter}\n\t */\n\tpublic static CsvWriter getWriter(Writer writer, CsvWriteConfig config) {\n\t\treturn new CsvWriter(writer, config);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.util.CharUtil;\n\nimport java.io.Serializable;\n\n/**\n * CSV写出配置项\n *\n * @author looly\n */\npublic class CsvWriteConfig extends CsvConfig<CsvWriteConfig> implements Serializable {\n\tprivate static final long serialVersionUID = 5396453565371560052L;\n\n\t/**\n\t * 是否始终使用文本分隔符，文本包装符，默认false，按需添加\n\t */\n\tprotected boolean alwaysDelimitText;\n\t/**\n\t * 换行符\n\t */\n\tprotected char[] lineDelimiter = {CharUtil.CR, CharUtil.LF};\n\n\t/**\n\t * 文件末尾是否添加换行符<br>\n\t * 按照https://datatracker.ietf.org/doc/html/rfc4180#section-2 规范，末尾换行符可有可无。\n\t */\n\tprotected boolean endingLineBreak;\n\n\t/**\n\t * 默认配置\n\t *\n\t * @return 默认配置\n\t */\n\tpublic static CsvWriteConfig defaultConfig() {\n\t\treturn new CsvWriteConfig();\n\t}\n\n\t/**\n\t * 设置是否始终使用文本分隔符，文本包装符，默认false，按需添加\n\t *\n\t * @param alwaysDelimitText 是否始终使用文本分隔符，文本包装符，默认false，按需添加\n\t * @return this\n\t */\n\tpublic CsvWriteConfig setAlwaysDelimitText(boolean alwaysDelimitText) {\n\t\tthis.alwaysDelimitText = alwaysDelimitText;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置换行符\n\t *\n\t * @param lineDelimiter 换行符\n\t * @return this\n\t */\n\tpublic CsvWriteConfig setLineDelimiter(char[] lineDelimiter) {\n\t\tthis.lineDelimiter = lineDelimiter;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 文件末尾是否添加换行符<br>\n\t * 按照https://datatracker.ietf.org/doc/html/rfc4180#section-2 规范，末尾换行符可有可无。\n\t *\n\t * @param endingLineBreak 文件末尾是否添加换行符\n\t * @return this\n\t */\n\tpublic CsvWriteConfig setEndingLineBreak(boolean endingLineBreak) {\n\t\tthis.endingLineBreak = endingLineBreak;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriter.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.BufferedWriter;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.Flushable;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * CSV数据写出器\n *\n * @author Looly\n * @since 4.0.5\n */\npublic final class CsvWriter implements Closeable, Flushable, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 写出器\n\t */\n\tprivate final Writer writer;\n\t/**\n\t * 写出配置\n\t */\n\tprivate final CsvWriteConfig config;\n\t/**\n\t * 是否处于新行开始\n\t */\n\tprivate boolean newline = true;\n\t/**\n\t * 是否首行，即CSV开始的位置，当初始化时默认为true，一旦写入内容，为false\n\t */\n\tprivate boolean isFirstLine = true;\n\n\t// --------------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，覆盖已有文件（如果存在），默认编码UTF-8\n\t *\n\t * @param filePath File CSV文件路径\n\t */\n\tpublic CsvWriter(String filePath) {\n\t\tthis(FileUtil.file(filePath));\n\t}\n\n\t/**\n\t * 构造，覆盖已有文件（如果存在），默认编码UTF-8\n\t *\n\t * @param file File CSV文件\n\t */\n\tpublic CsvWriter(File file) {\n\t\tthis(file, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 构造，覆盖已有文件（如果存在）\n\t *\n\t * @param filePath File CSV文件路径\n\t * @param charset  编码\n\t */\n\tpublic CsvWriter(String filePath, Charset charset) {\n\t\tthis(FileUtil.file(filePath), charset);\n\t}\n\n\t/**\n\t * 构造，覆盖已有文件（如果存在）\n\t *\n\t * @param file    File CSV文件\n\t * @param charset 编码\n\t */\n\tpublic CsvWriter(File file, Charset charset) {\n\t\tthis(file, charset, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param filePath File CSV文件路径\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t */\n\tpublic CsvWriter(String filePath, Charset charset, boolean isAppend) {\n\t\tthis(FileUtil.file(filePath), charset, isAppend);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file     CSV文件\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t */\n\tpublic CsvWriter(File file, Charset charset, boolean isAppend) {\n\t\tthis(file, charset, isAppend, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param filePath CSV文件路径\n\t * @param charset  编码\n\t * @param isAppend 是否追加\n\t * @param config   写出配置，null则使用默认配置\n\t */\n\tpublic CsvWriter(String filePath, Charset charset, boolean isAppend, CsvWriteConfig config) {\n\t\tthis(FileUtil.file(filePath), charset, isAppend, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file     CSV文件\n\t * @param charset  编码\n\t * @param isAppend 是否追加，append模式下，endingLineBreak自动设置为true\n\t * @param config   写出配置，null则使用默认配置\n\t */\n\tpublic CsvWriter(File file, Charset charset, boolean isAppend, CsvWriteConfig config) {\n\t\tthis(FileUtil.getWriter(file, charset, isAppend), isAppend?(config==null?CsvWriteConfig.defaultConfig().setEndingLineBreak(true):config.setEndingLineBreak(true)):config);\n\t}\n\n\t/**\n\t * 构造，使用默认配置\n\t *\n\t * @param writer {@link Writer}\n\t */\n\tpublic CsvWriter(Writer writer) {\n\t\tthis(writer, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param writer Writer\n\t * @param config 写出配置，null则使用默认配置\n\t */\n\tpublic CsvWriter(Writer writer, CsvWriteConfig config) {\n\t\tthis.writer = (writer instanceof BufferedWriter) ? writer : new BufferedWriter(writer);\n\t\tthis.config = ObjectUtil.defaultIfNull(config, CsvWriteConfig::defaultConfig);\n\t}\n\t// --------------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 设置是否始终使用文本分隔符，文本包装符，默认false，按需添加\n\t *\n\t * @param alwaysDelimitText 是否始终使用文本分隔符，文本包装符，默认false，按需添加\n\t * @return this\n\t */\n\tpublic CsvWriter setAlwaysDelimitText(boolean alwaysDelimitText) {\n\t\tthis.config.setAlwaysDelimitText(alwaysDelimitText);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置换行符\n\t *\n\t * @param lineDelimiter 换行符\n\t * @return this\n\t */\n\tpublic CsvWriter setLineDelimiter(char[] lineDelimiter) {\n\t\tthis.config.setLineDelimiter(lineDelimiter);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将多行写出到Writer\n\t *\n\t * @param lines 多行数据\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvWriter write(String[]... lines) throws IORuntimeException {\n\t\treturn write(new ArrayIter<>(lines));\n\t}\n\n\t/**\n\t * 将多行写出到Writer\n\t *\n\t * @param lines 多行数据，每行数据可以是集合或者数组\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvWriter write(Iterable<?> lines) throws IORuntimeException {\n\t\tif (CollUtil.isNotEmpty(lines)) {\n\t\t\tfor (Object values : lines) {\n\t\t\t\tappendLine(Convert.toStrArray(values));\n\t\t\t}\n\t\t\tflush();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将一个 CsvData 集合写出到Writer\n\t *\n\t * @param csvData CsvData\n\t * @return this\n\t * @since 5.7.4\n\t */\n\tpublic CsvWriter write(CsvData csvData) {\n\t\tif (csvData != null) {\n\t\t\t// 1、写header\n\t\t\tfinal List<String> header = csvData.getHeader();\n\t\t\tif (CollUtil.isNotEmpty(header)) {\n\t\t\t\tthis.writeHeaderLine(header.toArray(new String[0]));\n\t\t\t}\n\t\t\t// 2、写内容\n\t\t\tthis.write(csvData.getRows());\n\t\t\tflush();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将一个Bean集合写出到Writer，并自动生成表头\n\t *\n\t * @param beans Bean集合\n\t * @return this\n\t */\n\tpublic CsvWriter writeBeans(Iterable<?> beans) {\n\t\treturn writeBeans(beans, (String[]) null);\n\t}\n\n\t/**\n\t * 将一个Bean集合写出到Writer，并自动生成表头\n\t *\n\t * @param beans Bean集合\n\t * @param properties Bean 中指定的可以导出的属性\n\t * @return this\n\t */\n\tpublic CsvWriter writeBeans(Iterable<?> beans, String... properties) {\n\t\treturn writeBeans(beans, true, properties);\n\t}\n\n\t/**\n\t * 将一个Bean集合写出到Writer，并自动生成表头\n\t *\n\t * @param beans Bean集合\n\t * @param writeHeaderLine 是否写出表头，即Bean的字段名称列表作为首行\n\t * @param properties Bean 中指定的可以导出的属性\n\t * @return this\n\t * @since 5.8.29\n\t */\n\t@SuppressWarnings(\"resource\")\n\tpublic CsvWriter writeBeans(Iterable<?> beans, boolean writeHeaderLine, String... properties) {\n\t\tif (CollUtil.isNotEmpty(beans)) {\n\t\t\tboolean isFirst = writeHeaderLine;\n\t\t\tMap<String, Object> map;\n\t\t\tfor (Object bean : beans) {\n\t\t\t\tmap = BeanUtil.beanToMap(bean, properties);\n\t\t\t\tif (isFirst) {\n\t\t\t\t\twriteHeaderLine(map.keySet().toArray(new String[0]));\n\t\t\t\t\tisFirst = false;\n\t\t\t\t}\n\t\t\t\twriteLine(Convert.toStrArray(map.values()));\n\t\t\t}\n\t\t\tflush();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出一行头部行，支持标题别名\n\t *\n\t * @param fields 字段列表 ({@code null} 值会被做为空值追加\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.10\n\t */\n\tpublic CsvWriter writeHeaderLine(String... fields) throws IORuntimeException {\n\t\tfinal Map<String, String> headerAlias = this.config.headerAlias;\n\t\tif (MapUtil.isNotEmpty(headerAlias)) {\n\t\t\t// 标题别名替换\n\t\t\tString alias;\n\t\t\tfor (int i = 0; i < fields.length; i++) {\n\t\t\t\talias = headerAlias.get(fields[i]);\n\t\t\t\tif (null != alias) {\n\t\t\t\t\tfields[i] = alias;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn writeLine(fields);\n\t}\n\n\t/**\n\t * 写出一行\n\t *\n\t * @param fields 字段列表 ({@code null} 值会被做为空值追加)\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.7\n\t */\n\tpublic CsvWriter writeLine(String... fields) throws IORuntimeException {\n\t\tif (ArrayUtil.isEmpty(fields)) {\n\t\t\treturn writeLine();\n\t\t}\n\t\tappendLine(fields);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加新行（换行）\n\t *\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CsvWriter writeLine() throws IORuntimeException {\n\t\ttry {\n\t\t\twriter.write(config.lineDelimiter);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tnewline = true;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出一行注释，注释符号可自定义<br>\n\t * 如果注释符不存在，则抛出异常\n\t *\n\t * @param comment 注释内容\n\t * @return this\n\t * @see CsvConfig#commentCharacter\n\t * @since 5.5.7\n\t */\n\tpublic CsvWriter writeComment(String comment) {\n\t\tAssert.notNull(this.config.commentCharacter, \"Comment is disable!\");\n\t\ttry {\n\t\t\tif(isFirstLine){\n\t\t\t\t// 首行不补换行符\n\t\t\t\tisFirstLine = false;\n\t\t\t}else {\n\t\t\t\twriter.write(config.lineDelimiter);\n\t\t\t}\n\t\t\twriter.write(this.config.commentCharacter);\n\t\t\twriter.write(comment);\n\t\t\tnewline = true;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@SuppressWarnings(\"resource\")\n\t@Override\n\tpublic void close() {\n\t\tif(this.config.endingLineBreak){\n\t\t\t//https://gitee.com/chinabugotech/hutool/issues/I75K5G\n\t\t\twriteLine();\n\t\t}\n\t\tIoUtil.close(this.writer);\n\t}\n\n\t@Override\n\tpublic void flush() throws IORuntimeException {\n\t\ttry {\n\t\t\twriter.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 追加一行，末尾会自动换行，但是追加前不会换行\n\t *\n\t * @param fields 字段列表 ({@code null} 值会被做为空值追加)\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void appendLine(String... fields) throws IORuntimeException {\n\t\ttry {\n\t\t\tdoAppendLine(fields);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 追加一行，末尾会自动换行，但是追加前不会换行\n\t *\n\t * @param fields 字段列表 ({@code null} 值会被做为空值追加)\n\t * @throws IOException IO异常\n\t */\n\tprivate void doAppendLine(String... fields) throws IOException {\n\t\tif (null != fields) {\n\t\t\tif(isFirstLine){\n\t\t\t\t// 首行不补换行符\n\t\t\t\tisFirstLine = false;\n\t\t\t}else {\n\t\t\t\twriter.write(config.lineDelimiter);\n\t\t\t}\n\t\t\tfor (String field : fields) {\n\t\t\t\tappendField(field);\n\t\t\t}\n\t\t\tnewline = true;\n\t\t}\n\t}\n\n\t/**\n\t * 在当前行追加字段值，自动添加字段分隔符，如果有必要，自动包装字段\n\t *\n\t * @param value 字段值，{@code null} 会被做为空串写出\n\t * @throws IOException IO异常\n\t */\n\tprivate void appendField(final String value) throws IOException {\n\t\tboolean alwaysDelimitText = config.alwaysDelimitText;\n\t\tchar textDelimiter = config.textDelimiter;\n\t\tchar fieldSeparator = config.fieldSeparator;\n\n\t\tif (false == newline) {\n\t\t\twriter.write(fieldSeparator);\n\t\t} else {\n\t\t\tnewline = false;\n\t\t}\n\n\t\tif (null == value) {\n\t\t\tif (alwaysDelimitText) {\n\t\t\t\twriter.write(new char[]{textDelimiter, textDelimiter});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tfinal char[] valueChars = value.toCharArray();\n\t\tboolean needsTextDelimiter = alwaysDelimitText;\n\t\tboolean containsTextDelimiter = false;\n\n\t\tfor (final char c : valueChars) {\n\t\t\tif (c == textDelimiter) {\n\t\t\t\t// 字段值中存在包装符\n\t\t\t\tcontainsTextDelimiter = needsTextDelimiter = true;\n\t\t\t\tbreak;\n\t\t\t} else if (c == fieldSeparator || c == CharUtil.LF || c == CharUtil.CR) {\n\t\t\t\t// 包含分隔符或换行符需要包装符包装\n\t\t\t\tneedsTextDelimiter = true;\n\t\t\t}\n\t\t}\n\n\t\t// 包装符开始\n\t\tif (needsTextDelimiter) {\n\t\t\twriter.write(textDelimiter);\n\t\t}\n\n\t\t// 正文\n\t\tif (containsTextDelimiter) {\n\t\t\tfor (final char c : valueChars) {\n\t\t\t\t// 转义文本包装符\n\t\t\t\tif (c == textDelimiter) {\n\t\t\t\t\twriter.write(textDelimiter);\n\t\t\t\t}\n\t\t\t\twriter.write(c);\n\t\t\t}\n\t\t} else {\n\t\t\twriter.write(valueChars);\n\t\t}\n\n\t\t// 包装符结尾\n\t\tif (needsTextDelimiter) {\n\t\t\twriter.write(textDelimiter);\n\t\t}\n\t}\n\t// --------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/csv/package-info.java",
    "content": "/**\n * 提供CSV文件读写的封装，入口为CsvUtil<br>\n * 规范见：https://datatracker.ietf.org/doc/html/rfc4180\n *\n * @author looly\n *\n */\npackage cn.hutool.core.text.csv;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Escape.java",
    "content": "package cn.hutool.core.text.escape;\n\nimport cn.hutool.core.text.replacer.LookupReplacer;\nimport cn.hutool.core.text.replacer.ReplacerChain;\n\n/**\n * HTML4的ESCAPE\n * 参考：Commons Lang3\n *\n * @author looly\n *\n */\npublic class Html4Escape extends ReplacerChain {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * HTML转义字符<br>\n\t * HTML转义相比XML，并不转义单引号<br>\n\t * 见：https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents\n\t */\n\tprotected static final String[][] BASIC_ESCAPE = { //\n\t\t{\"\\\"\", \"&quot;\"}, // \" - double-quote\n\t\t{\"&\", \"&amp;\"}, // & - ampersand\n\t\t{\"<\", \"&lt;\"}, // < - less-than\n\t\t{\">\", \"&gt;\"}, // > - greater-than\n\t};\n\n\tprotected static final String[][] ISO8859_1_ESCAPE = { //\n\t\t\t{ \"\\u00A0\", \"&nbsp;\" }, // non-breaking space\n\t\t\t{ \"\\u00A1\", \"&iexcl;\" }, // inverted exclamation mark\n\t\t\t{ \"\\u00A2\", \"&cent;\" }, // cent sign\n\t\t\t{ \"\\u00A3\", \"&pound;\" }, // pound sign\n\t\t\t{ \"\\u00A4\", \"&curren;\" }, // currency sign\n\t\t\t{ \"\\u00A5\", \"&yen;\" }, // yen sign = yuan sign\n\t\t\t{ \"\\u00A6\", \"&brvbar;\" }, // broken bar = broken vertical bar\n\t\t\t{ \"\\u00A7\", \"&sect;\" }, // section sign\n\t\t\t{ \"\\u00A8\", \"&uml;\" }, // diaeresis = spacing diaeresis\n\t\t\t{ \"\\u00A9\", \"&copy;\" }, // � - copyright sign\n\t\t\t{ \"\\u00AA\", \"&ordf;\" }, // feminine ordinal indicator\n\t\t\t{ \"\\u00AB\", \"&laquo;\" }, // left-pointing double angle quotation mark = left pointing guillemet\n\t\t\t{ \"\\u00AC\", \"&not;\" }, // not sign\n\t\t\t{ \"\\u00AD\", \"&shy;\" }, // soft hyphen = discretionary hyphen\n\t\t\t{ \"\\u00AE\", \"&reg;\" }, // � - registered trademark sign\n\t\t\t{ \"\\u00AF\", \"&macr;\" }, // macron = spacing macron = overline = APL overbar\n\t\t\t{ \"\\u00B0\", \"&deg;\" }, // degree sign\n\t\t\t{ \"\\u00B1\", \"&plusmn;\" }, // plus-minus sign = plus-or-minus sign\n\t\t\t{ \"\\u00B2\", \"&sup2;\" }, // superscript two = superscript digit two = squared\n\t\t\t{ \"\\u00B3\", \"&sup3;\" }, // superscript three = superscript digit three = cubed\n\t\t\t{ \"\\u00B4\", \"&acute;\" }, // acute accent = spacing acute\n\t\t\t{ \"\\u00B5\", \"&micro;\" }, // micro sign\n\t\t\t{ \"\\u00B6\", \"&para;\" }, // pilcrow sign = paragraph sign\n\t\t\t{ \"\\u00B7\", \"&middot;\" }, // middle dot = Georgian comma = Greek middle dot\n\t\t\t{ \"\\u00B8\", \"&cedil;\" }, // cedilla = spacing cedilla\n\t\t\t{ \"\\u00B9\", \"&sup1;\" }, // superscript one = superscript digit one\n\t\t\t{ \"\\u00BA\", \"&ordm;\" }, // masculine ordinal indicator\n\t\t\t{ \"\\u00BB\", \"&raquo;\" }, // right-pointing double angle quotation mark = right pointing guillemet\n\t\t\t{ \"\\u00BC\", \"&frac14;\" }, // vulgar fraction one quarter = fraction one quarter\n\t\t\t{ \"\\u00BD\", \"&frac12;\" }, // vulgar fraction one half = fraction one half\n\t\t\t{ \"\\u00BE\", \"&frac34;\" }, // vulgar fraction three quarters = fraction three quarters\n\t\t\t{ \"\\u00BF\", \"&iquest;\" }, // inverted question mark = turned question mark\n\t\t\t{ \"\\u00C0\", \"&Agrave;\" }, // � - uppercase A, grave accent\n\t\t\t{ \"\\u00C1\", \"&Aacute;\" }, // � - uppercase A, acute accent\n\t\t\t{ \"\\u00C2\", \"&Acirc;\" }, // � - uppercase A, circumflex accent\n\t\t\t{ \"\\u00C3\", \"&Atilde;\" }, // � - uppercase A, tilde\n\t\t\t{ \"\\u00C4\", \"&Auml;\" }, // � - uppercase A, umlaut\n\t\t\t{ \"\\u00C5\", \"&Aring;\" }, // � - uppercase A, ring\n\t\t\t{ \"\\u00C6\", \"&AElig;\" }, // � - uppercase AE\n\t\t\t{ \"\\u00C7\", \"&Ccedil;\" }, // � - uppercase C, cedilla\n\t\t\t{ \"\\u00C8\", \"&Egrave;\" }, // � - uppercase E, grave accent\n\t\t\t{ \"\\u00C9\", \"&Eacute;\" }, // � - uppercase E, acute accent\n\t\t\t{ \"\\u00CA\", \"&Ecirc;\" }, // � - uppercase E, circumflex accent\n\t\t\t{ \"\\u00CB\", \"&Euml;\" }, // � - uppercase E, umlaut\n\t\t\t{ \"\\u00CC\", \"&Igrave;\" }, // � - uppercase I, grave accent\n\t\t\t{ \"\\u00CD\", \"&Iacute;\" }, // � - uppercase I, acute accent\n\t\t\t{ \"\\u00CE\", \"&Icirc;\" }, // � - uppercase I, circumflex accent\n\t\t\t{ \"\\u00CF\", \"&Iuml;\" }, // � - uppercase I, umlaut\n\t\t\t{ \"\\u00D0\", \"&ETH;\" }, // � - uppercase Eth, Icelandic\n\t\t\t{ \"\\u00D1\", \"&Ntilde;\" }, // � - uppercase N, tilde\n\t\t\t{ \"\\u00D2\", \"&Ograve;\" }, // � - uppercase O, grave accent\n\t\t\t{ \"\\u00D3\", \"&Oacute;\" }, // � - uppercase O, acute accent\n\t\t\t{ \"\\u00D4\", \"&Ocirc;\" }, // � - uppercase O, circumflex accent\n\t\t\t{ \"\\u00D5\", \"&Otilde;\" }, // � - uppercase O, tilde\n\t\t\t{ \"\\u00D6\", \"&Ouml;\" }, // � - uppercase O, umlaut\n\t\t\t{ \"\\u00D7\", \"&times;\" }, // multiplication sign\n\t\t\t{ \"\\u00D8\", \"&Oslash;\" }, // � - uppercase O, slash\n\t\t\t{ \"\\u00D9\", \"&Ugrave;\" }, // � - uppercase U, grave accent\n\t\t\t{ \"\\u00DA\", \"&Uacute;\" }, // � - uppercase U, acute accent\n\t\t\t{ \"\\u00DB\", \"&Ucirc;\" }, // � - uppercase U, circumflex accent\n\t\t\t{ \"\\u00DC\", \"&Uuml;\" }, // � - uppercase U, umlaut\n\t\t\t{ \"\\u00DD\", \"&Yacute;\" }, // � - uppercase Y, acute accent\n\t\t\t{ \"\\u00DE\", \"&THORN;\" }, // � - uppercase THORN, Icelandic\n\t\t\t{ \"\\u00DF\", \"&szlig;\" }, // � - lowercase sharps, German\n\t\t\t{ \"\\u00E0\", \"&agrave;\" }, // � - lowercase a, grave accent\n\t\t\t{ \"\\u00E1\", \"&aacute;\" }, // � - lowercase a, acute accent\n\t\t\t{ \"\\u00E2\", \"&acirc;\" }, // � - lowercase a, circumflex accent\n\t\t\t{ \"\\u00E3\", \"&atilde;\" }, // � - lowercase a, tilde\n\t\t\t{ \"\\u00E4\", \"&auml;\" }, // � - lowercase a, umlaut\n\t\t\t{ \"\\u00E5\", \"&aring;\" }, // � - lowercase a, ring\n\t\t\t{ \"\\u00E6\", \"&aelig;\" }, // � - lowercase ae\n\t\t\t{ \"\\u00E7\", \"&ccedil;\" }, // � - lowercase c, cedilla\n\t\t\t{ \"\\u00E8\", \"&egrave;\" }, // � - lowercase e, grave accent\n\t\t\t{ \"\\u00E9\", \"&eacute;\" }, // � - lowercase e, acute accent\n\t\t\t{ \"\\u00EA\", \"&ecirc;\" }, // � - lowercase e, circumflex accent\n\t\t\t{ \"\\u00EB\", \"&euml;\" }, // � - lowercase e, umlaut\n\t\t\t{ \"\\u00EC\", \"&igrave;\" }, // � - lowercase i, grave accent\n\t\t\t{ \"\\u00ED\", \"&iacute;\" }, // � - lowercase i, acute accent\n\t\t\t{ \"\\u00EE\", \"&icirc;\" }, // � - lowercase i, circumflex accent\n\t\t\t{ \"\\u00EF\", \"&iuml;\" }, // � - lowercase i, umlaut\n\t\t\t{ \"\\u00F0\", \"&eth;\" }, // � - lowercase eth, Icelandic\n\t\t\t{ \"\\u00F1\", \"&ntilde;\" }, // � - lowercase n, tilde\n\t\t\t{ \"\\u00F2\", \"&ograve;\" }, // � - lowercase o, grave accent\n\t\t\t{ \"\\u00F3\", \"&oacute;\" }, // � - lowercase o, acute accent\n\t\t\t{ \"\\u00F4\", \"&ocirc;\" }, // � - lowercase o, circumflex accent\n\t\t\t{ \"\\u00F5\", \"&otilde;\" }, // � - lowercase o, tilde\n\t\t\t{ \"\\u00F6\", \"&ouml;\" }, // � - lowercase o, umlaut\n\t\t\t{ \"\\u00F7\", \"&divide;\" }, // division sign\n\t\t\t{ \"\\u00F8\", \"&oslash;\" }, // � - lowercase o, slash\n\t\t\t{ \"\\u00F9\", \"&ugrave;\" }, // � - lowercase u, grave accent\n\t\t\t{ \"\\u00FA\", \"&uacute;\" }, // � - lowercase u, acute accent\n\t\t\t{ \"\\u00FB\", \"&ucirc;\" }, // � - lowercase u, circumflex accent\n\t\t\t{ \"\\u00FC\", \"&uuml;\" }, // � - lowercase u, umlaut\n\t\t\t{ \"\\u00FD\", \"&yacute;\" }, // � - lowercase y, acute accent\n\t\t\t{ \"\\u00FE\", \"&thorn;\" }, // � - lowercase thorn, Icelandic\n\t\t\t{ \"\\u00FF\", \"&yuml;\" }, // � - lowercase y, umlaut\n\t};\n\n\tprotected static final String[][] HTML40_EXTENDED_ESCAPE = {\n\t\t\t// <!-- Latin Extended-B -->\n\t\t\t{ \"\\u0192\", \"&fnof;\" }, // latin small f with hook = function= florin, U+0192 ISOtech -->\n\t\t\t// <!-- Greek -->\n\t\t\t{ \"\\u0391\", \"&Alpha;\" }, // greek capital letter alpha, U+0391 -->\n\t\t\t{ \"\\u0392\", \"&Beta;\" }, // greek capital letter beta, U+0392 -->\n\t\t\t{ \"\\u0393\", \"&Gamma;\" }, // greek capital letter gamma,U+0393 ISOgrk3 -->\n\t\t\t{ \"\\u0394\", \"&Delta;\" }, // greek capital letter delta,U+0394 ISOgrk3 -->\n\t\t\t{ \"\\u0395\", \"&Epsilon;\" }, // greek capital letter epsilon, U+0395 -->\n\t\t\t{ \"\\u0396\", \"&Zeta;\" }, // greek capital letter zeta, U+0396 -->\n\t\t\t{ \"\\u0397\", \"&Eta;\" }, // greek capital letter eta, U+0397 -->\n\t\t\t{ \"\\u0398\", \"&Theta;\" }, // greek capital letter theta,U+0398 ISOgrk3 -->\n\t\t\t{ \"\\u0399\", \"&Iota;\" }, // greek capital letter iota, U+0399 -->\n\t\t\t{ \"\\u039A\", \"&Kappa;\" }, // greek capital letter kappa, U+039A -->\n\t\t\t{ \"\\u039B\", \"&Lambda;\" }, // greek capital letter lambda,U+039B ISOgrk3 -->\n\t\t\t{ \"\\u039C\", \"&Mu;\" }, // greek capital letter mu, U+039C -->\n\t\t\t{ \"\\u039D\", \"&Nu;\" }, // greek capital letter nu, U+039D -->\n\t\t\t{ \"\\u039E\", \"&Xi;\" }, // greek capital letter xi, U+039E ISOgrk3 -->\n\t\t\t{ \"\\u039F\", \"&Omicron;\" }, // greek capital letter omicron, U+039F -->\n\t\t\t{ \"\\u03A0\", \"&Pi;\" }, // greek capital letter pi, U+03A0 ISOgrk3 -->\n\t\t\t{ \"\\u03A1\", \"&Rho;\" }, // greek capital letter rho, U+03A1 -->\n\t\t\t// <!-- there is no Sigmaf, and no U+03A2 character either -->\n\t\t\t{ \"\\u03A3\", \"&Sigma;\" }, // greek capital letter sigma,U+03A3 ISOgrk3 -->\n\t\t\t{ \"\\u03A4\", \"&Tau;\" }, // greek capital letter tau, U+03A4 -->\n\t\t\t{ \"\\u03A5\", \"&Upsilon;\" }, // greek capital letter upsilon,U+03A5 ISOgrk3 -->\n\t\t\t{ \"\\u03A6\", \"&Phi;\" }, // greek capital letter phi,U+03A6 ISOgrk3 -->\n\t\t\t{ \"\\u03A7\", \"&Chi;\" }, // greek capital letter chi, U+03A7 -->\n\t\t\t{ \"\\u03A8\", \"&Psi;\" }, // greek capital letter psi,U+03A8 ISOgrk3 -->\n\t\t\t{ \"\\u03A9\", \"&Omega;\" }, // greek capital letter omega,U+03A9 ISOgrk3 -->\n\t\t\t{ \"\\u03B1\", \"&alpha;\" }, // greek small letter alpha,U+03B1 ISOgrk3 -->\n\t\t\t{ \"\\u03B2\", \"&beta;\" }, // greek small letter beta, U+03B2 ISOgrk3 -->\n\t\t\t{ \"\\u03B3\", \"&gamma;\" }, // greek small letter gamma,U+03B3 ISOgrk3 -->\n\t\t\t{ \"\\u03B4\", \"&delta;\" }, // greek small letter delta,U+03B4 ISOgrk3 -->\n\t\t\t{ \"\\u03B5\", \"&epsilon;\" }, // greek small letter epsilon,U+03B5 ISOgrk3 -->\n\t\t\t{ \"\\u03B6\", \"&zeta;\" }, // greek small letter zeta, U+03B6 ISOgrk3 -->\n\t\t\t{ \"\\u03B7\", \"&eta;\" }, // greek small letter eta, U+03B7 ISOgrk3 -->\n\t\t\t{ \"\\u03B8\", \"&theta;\" }, // greek small letter theta,U+03B8 ISOgrk3 -->\n\t\t\t{ \"\\u03B9\", \"&iota;\" }, // greek small letter iota, U+03B9 ISOgrk3 -->\n\t\t\t{ \"\\u03BA\", \"&kappa;\" }, // greek small letter kappa,U+03BA ISOgrk3 -->\n\t\t\t{ \"\\u03BB\", \"&lambda;\" }, // greek small letter lambda,U+03BB ISOgrk3 -->\n\t\t\t{ \"\\u03BC\", \"&mu;\" }, // greek small letter mu, U+03BC ISOgrk3 -->\n\t\t\t{ \"\\u03BD\", \"&nu;\" }, // greek small letter nu, U+03BD ISOgrk3 -->\n\t\t\t{ \"\\u03BE\", \"&xi;\" }, // greek small letter xi, U+03BE ISOgrk3 -->\n\t\t\t{ \"\\u03BF\", \"&omicron;\" }, // greek small letter omicron, U+03BF NEW -->\n\t\t\t{ \"\\u03C0\", \"&pi;\" }, // greek small letter pi, U+03C0 ISOgrk3 -->\n\t\t\t{ \"\\u03C1\", \"&rho;\" }, // greek small letter rho, U+03C1 ISOgrk3 -->\n\t\t\t{ \"\\u03C2\", \"&sigmaf;\" }, // greek small letter final sigma,U+03C2 ISOgrk3 -->\n\t\t\t{ \"\\u03C3\", \"&sigma;\" }, // greek small letter sigma,U+03C3 ISOgrk3 -->\n\t\t\t{ \"\\u03C4\", \"&tau;\" }, // greek small letter tau, U+03C4 ISOgrk3 -->\n\t\t\t{ \"\\u03C5\", \"&upsilon;\" }, // greek small letter upsilon,U+03C5 ISOgrk3 -->\n\t\t\t{ \"\\u03C6\", \"&phi;\" }, // greek small letter phi, U+03C6 ISOgrk3 -->\n\t\t\t{ \"\\u03C7\", \"&chi;\" }, // greek small letter chi, U+03C7 ISOgrk3 -->\n\t\t\t{ \"\\u03C8\", \"&psi;\" }, // greek small letter psi, U+03C8 ISOgrk3 -->\n\t\t\t{ \"\\u03C9\", \"&omega;\" }, // greek small letter omega,U+03C9 ISOgrk3 -->\n\t\t\t{ \"\\u03D1\", \"&thetasym;\" }, // greek small letter theta symbol,U+03D1 NEW -->\n\t\t\t{ \"\\u03D2\", \"&upsih;\" }, // greek upsilon with hook symbol,U+03D2 NEW -->\n\t\t\t{ \"\\u03D6\", \"&piv;\" }, // greek pi symbol, U+03D6 ISOgrk3 -->\n\t\t\t// <!-- General Punctuation -->\n\t\t\t{ \"\\u2022\", \"&bull;\" }, // bullet = black small circle,U+2022 ISOpub -->\n\t\t\t// <!-- bullet is NOT the same as bullet operator, U+2219 -->\n\t\t\t{ \"\\u2026\", \"&hellip;\" }, // horizontal ellipsis = three dot leader,U+2026 ISOpub -->\n\t\t\t{ \"\\u2032\", \"&prime;\" }, // prime = minutes = feet, U+2032 ISOtech -->\n\t\t\t{ \"\\u2033\", \"&Prime;\" }, // double prime = seconds = inches,U+2033 ISOtech -->\n\t\t\t{ \"\\u203E\", \"&oline;\" }, // overline = spacing overscore,U+203E NEW -->\n\t\t\t{ \"\\u2044\", \"&frasl;\" }, // fraction slash, U+2044 NEW -->\n\t\t\t// <!-- Letterlike Symbols -->\n\t\t\t{ \"\\u2118\", \"&weierp;\" }, // script capital P = power set= Weierstrass p, U+2118 ISOamso -->\n\t\t\t{ \"\\u2111\", \"&image;\" }, // blackletter capital I = imaginary part,U+2111 ISOamso -->\n\t\t\t{ \"\\u211C\", \"&real;\" }, // blackletter capital R = real part symbol,U+211C ISOamso -->\n\t\t\t{ \"\\u2122\", \"&trade;\" }, // trade mark sign, U+2122 ISOnum -->\n\t\t\t{ \"\\u2135\", \"&alefsym;\" }, // alef symbol = first transfinite cardinal,U+2135 NEW -->\n\t\t\t// <!-- alef symbol is NOT the same as hebrew letter alef,U+05D0 although the\n\t\t\t// same glyph could be used to depict both characters -->\n\t\t\t// <!-- Arrows -->\n\t\t\t{ \"\\u2190\", \"&larr;\" }, // leftwards arrow, U+2190 ISOnum -->\n\t\t\t{ \"\\u2191\", \"&uarr;\" }, // upwards arrow, U+2191 ISOnum-->\n\t\t\t{ \"\\u2192\", \"&rarr;\" }, // rightwards arrow, U+2192 ISOnum -->\n\t\t\t{ \"\\u2193\", \"&darr;\" }, // downwards arrow, U+2193 ISOnum -->\n\t\t\t{ \"\\u2194\", \"&harr;\" }, // left right arrow, U+2194 ISOamsa -->\n\t\t\t{ \"\\u21B5\", \"&crarr;\" }, // downwards arrow with corner leftwards= carriage return, U+21B5 NEW -->\n\t\t\t{ \"\\u21D0\", \"&lArr;\" }, // leftwards double arrow, U+21D0 ISOtech -->\n\t\t\t// <!-- ISO 10646 does not say that lArr is the same as the 'is implied by'\n\t\t\t// arrow but also does not have any other character for that function.\n\t\t\t// So ? lArr canbe used for 'is implied by' as ISOtech suggests -->\n\t\t\t{ \"\\u21D1\", \"&uArr;\" }, // upwards double arrow, U+21D1 ISOamsa -->\n\t\t\t{ \"\\u21D2\", \"&rArr;\" }, // rightwards double arrow,U+21D2 ISOtech -->\n\t\t\t// <!-- ISO 10646 does not say this is the 'implies' character but does not\n\t\t\t// have another character with this function so ?rArr can be used for\n\t\t\t// 'implies' as ISOtech suggests -->\n\t\t\t{ \"\\u21D3\", \"&dArr;\" }, // downwards double arrow, U+21D3 ISOamsa -->\n\t\t\t{ \"\\u21D4\", \"&hArr;\" }, // left right double arrow,U+21D4 ISOamsa -->\n\t\t\t// <!-- Mathematical Operators -->\n\t\t\t{ \"\\u2200\", \"&forall;\" }, // for all, U+2200 ISOtech -->\n\t\t\t{ \"\\u2202\", \"&part;\" }, // partial differential, U+2202 ISOtech -->\n\t\t\t{ \"\\u2203\", \"&exist;\" }, // there exists, U+2203 ISOtech -->\n\t\t\t{ \"\\u2205\", \"&empty;\" }, // empty set = null set = diameter,U+2205 ISOamso -->\n\t\t\t{ \"\\u2207\", \"&nabla;\" }, // nabla = backward difference,U+2207 ISOtech -->\n\t\t\t{ \"\\u2208\", \"&isin;\" }, // element of, U+2208 ISOtech -->\n\t\t\t{ \"\\u2209\", \"&notin;\" }, // not an element of, U+2209 ISOtech -->\n\t\t\t{ \"\\u220B\", \"&ni;\" }, // contains as member, U+220B ISOtech -->\n\t\t\t// <!-- should there be a more memorable name than 'ni'? -->\n\t\t\t{ \"\\u220F\", \"&prod;\" }, // n-ary product = product sign,U+220F ISOamsb -->\n\t\t\t// <!-- prod is NOT the same character as U+03A0 'greek capital letter pi'\n\t\t\t// though the same glyph might be used for both -->\n\t\t\t{ \"\\u2211\", \"&sum;\" }, // n-ary summation, U+2211 ISOamsb -->\n\t\t\t// <!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'\n\t\t\t// though the same glyph might be used for both -->\n\t\t\t{ \"\\u2212\", \"&minus;\" }, // minus sign, U+2212 ISOtech -->\n\t\t\t{ \"\\u2217\", \"&lowast;\" }, // asterisk operator, U+2217 ISOtech -->\n\t\t\t{ \"\\u221A\", \"&radic;\" }, // square root = radical sign,U+221A ISOtech -->\n\t\t\t{ \"\\u221D\", \"&prop;\" }, // proportional to, U+221D ISOtech -->\n\t\t\t{ \"\\u221E\", \"&infin;\" }, // infinity, U+221E ISOtech -->\n\t\t\t{ \"\\u2220\", \"&ang;\" }, // angle, U+2220 ISOamso -->\n\t\t\t{ \"\\u2227\", \"&and;\" }, // logical and = wedge, U+2227 ISOtech -->\n\t\t\t{ \"\\u2228\", \"&or;\" }, // logical or = vee, U+2228 ISOtech -->\n\t\t\t{ \"\\u2229\", \"&cap;\" }, // intersection = cap, U+2229 ISOtech -->\n\t\t\t{ \"\\u222A\", \"&cup;\" }, // union = cup, U+222A ISOtech -->\n\t\t\t{ \"\\u222B\", \"&int;\" }, // integral, U+222B ISOtech -->\n\t\t\t{ \"\\u2234\", \"&there4;\" }, // therefore, U+2234 ISOtech -->\n\t\t\t{ \"\\u223C\", \"&sim;\" }, // tilde operator = varies with = similar to,U+223C ISOtech -->\n\t\t\t// <!-- tilde operator is NOT the same character as the tilde, U+007E,although\n\t\t\t// the same glyph might be used to represent both -->\n\t\t\t{ \"\\u2245\", \"&cong;\" }, // approximately equal to, U+2245 ISOtech -->\n\t\t\t{ \"\\u2248\", \"&asymp;\" }, // almost equal to = asymptotic to,U+2248 ISOamsr -->\n\t\t\t{ \"\\u2260\", \"&ne;\" }, // not equal to, U+2260 ISOtech -->\n\t\t\t{ \"\\u2261\", \"&equiv;\" }, // identical to, U+2261 ISOtech -->\n\t\t\t{ \"\\u2264\", \"&le;\" }, // less-than or equal to, U+2264 ISOtech -->\n\t\t\t{ \"\\u2265\", \"&ge;\" }, // greater-than or equal to,U+2265 ISOtech -->\n\t\t\t{ \"\\u2282\", \"&sub;\" }, // subset of, U+2282 ISOtech -->\n\t\t\t{ \"\\u2283\", \"&sup;\" }, // superset of, U+2283 ISOtech -->\n\t\t\t// <!-- note that nsup, 'not a superset of, U+2283' is not covered by the\n\t\t\t// Symbol font encoding and is not included. Should it be, for symmetry?\n\t\t\t// It is in ISOamsn --> <!ENTITY nsub\", \"8836\"},\n\t\t\t// not a subset of, U+2284 ISOamsn -->\n\t\t\t{ \"\\u2286\", \"&sube;\" }, // subset of or equal to, U+2286 ISOtech -->\n\t\t\t{ \"\\u2287\", \"&supe;\" }, // superset of or equal to,U+2287 ISOtech -->\n\t\t\t{ \"\\u2295\", \"&oplus;\" }, // circled plus = direct sum,U+2295 ISOamsb -->\n\t\t\t{ \"\\u2297\", \"&otimes;\" }, // circled times = vector product,U+2297 ISOamsb -->\n\t\t\t{ \"\\u22A5\", \"&perp;\" }, // up tack = orthogonal to = perpendicular,U+22A5 ISOtech -->\n\t\t\t{ \"\\u22C5\", \"&sdot;\" }, // dot operator, U+22C5 ISOamsb -->\n\t\t\t// <!-- dot operator is NOT the same character as U+00B7 middle dot -->\n\t\t\t// <!-- Miscellaneous Technical -->\n\t\t\t{ \"\\u2308\", \"&lceil;\" }, // left ceiling = apl upstile,U+2308 ISOamsc -->\n\t\t\t{ \"\\u2309\", \"&rceil;\" }, // right ceiling, U+2309 ISOamsc -->\n\t\t\t{ \"\\u230A\", \"&lfloor;\" }, // left floor = apl downstile,U+230A ISOamsc -->\n\t\t\t{ \"\\u230B\", \"&rfloor;\" }, // right floor, U+230B ISOamsc -->\n\t\t\t{ \"\\u2329\", \"&lang;\" }, // left-pointing angle bracket = bra,U+2329 ISOtech -->\n\t\t\t// <!-- lang is NOT the same character as U+003C 'less than' or U+2039 'single left-pointing angle quotation\n\t\t\t// mark' -->\n\t\t\t{ \"\\u232A\", \"&rang;\" }, // right-pointing angle bracket = ket,U+232A ISOtech -->\n\t\t\t// <!-- rang is NOT the same character as U+003E 'greater than' or U+203A\n\t\t\t// 'single right-pointing angle quotation mark' -->\n\t\t\t// <!-- Geometric Shapes -->\n\t\t\t{ \"\\u25CA\", \"&loz;\" }, // lozenge, U+25CA ISOpub -->\n\t\t\t// <!-- Miscellaneous Symbols -->\n\t\t\t{ \"\\u2660\", \"&spades;\" }, // black spade suit, U+2660 ISOpub -->\n\t\t\t// <!-- black here seems to mean filled as opposed to hollow -->\n\t\t\t{ \"\\u2663\", \"&clubs;\" }, // black club suit = shamrock,U+2663 ISOpub -->\n\t\t\t{ \"\\u2665\", \"&hearts;\" }, // black heart suit = valentine,U+2665 ISOpub -->\n\t\t\t{ \"\\u2666\", \"&diams;\" }, // black diamond suit, U+2666 ISOpub -->\n\n\t\t\t// <!-- Latin Extended-A -->\n\t\t\t{ \"\\u0152\", \"&OElig;\" }, // -- latin capital ligature OE,U+0152 ISOlat2 -->\n\t\t\t{ \"\\u0153\", \"&oelig;\" }, // -- latin small ligature oe, U+0153 ISOlat2 -->\n\t\t\t// <!-- ligature is a misnomer, this is a separate character in some languages -->\n\t\t\t{ \"\\u0160\", \"&Scaron;\" }, // -- latin capital letter S with caron,U+0160 ISOlat2 -->\n\t\t\t{ \"\\u0161\", \"&scaron;\" }, // -- latin small letter s with caron,U+0161 ISOlat2 -->\n\t\t\t{ \"\\u0178\", \"&Yuml;\" }, // -- latin capital letter Y with diaeresis,U+0178 ISOlat2 -->\n\t\t\t// <!-- Spacing Modifier Letters -->\n\t\t\t{ \"\\u02C6\", \"&circ;\" }, // -- modifier letter circumflex accent,U+02C6 ISOpub -->\n\t\t\t{ \"\\u02DC\", \"&tilde;\" }, // small tilde, U+02DC ISOdia -->\n\t\t\t// <!-- General Punctuation -->\n\t\t\t{ \"\\u2002\", \"&ensp;\" }, // en space, U+2002 ISOpub -->\n\t\t\t{ \"\\u2003\", \"&emsp;\" }, // em space, U+2003 ISOpub -->\n\t\t\t{ \"\\u2009\", \"&thinsp;\" }, // thin space, U+2009 ISOpub -->\n\t\t\t{ \"\\u200C\", \"&zwnj;\" }, // zero width non-joiner,U+200C NEW RFC 2070 -->\n\t\t\t{ \"\\u200D\", \"&zwj;\" }, // zero width joiner, U+200D NEW RFC 2070 -->\n\t\t\t{ \"\\u200E\", \"&lrm;\" }, // left-to-right mark, U+200E NEW RFC 2070 -->\n\t\t\t{ \"\\u200F\", \"&rlm;\" }, // right-to-left mark, U+200F NEW RFC 2070 -->\n\t\t\t{ \"\\u2013\", \"&ndash;\" }, // en dash, U+2013 ISOpub -->\n\t\t\t{ \"\\u2014\", \"&mdash;\" }, // em dash, U+2014 ISOpub -->\n\t\t\t{ \"\\u2018\", \"&lsquo;\" }, // left single quotation mark,U+2018 ISOnum -->\n\t\t\t{ \"\\u2019\", \"&rsquo;\" }, // right single quotation mark,U+2019 ISOnum -->\n\t\t\t{ \"\\u201A\", \"&sbquo;\" }, // single low-9 quotation mark, U+201A NEW -->\n\t\t\t{ \"\\u201C\", \"&ldquo;\" }, // left double quotation mark,U+201C ISOnum -->\n\t\t\t{ \"\\u201D\", \"&rdquo;\" }, // right double quotation mark,U+201D ISOnum -->\n\t\t\t{ \"\\u201E\", \"&bdquo;\" }, // double low-9 quotation mark, U+201E NEW -->\n\t\t\t{ \"\\u2020\", \"&dagger;\" }, // dagger, U+2020 ISOpub -->\n\t\t\t{ \"\\u2021\", \"&Dagger;\" }, // double dagger, U+2021 ISOpub -->\n\t\t\t{ \"\\u2030\", \"&permil;\" }, // per mille sign, U+2030 ISOtech -->\n\t\t\t{ \"\\u2039\", \"&lsaquo;\" }, // single left-pointing angle quotation mark,U+2039 ISO proposed -->\n\t\t\t// <!-- lsaquo is proposed but not yet ISO standardized -->\n\t\t\t{ \"\\u203A\", \"&rsaquo;\" }, // single right-pointing angle quotation mark,U+203A ISO proposed -->\n\t\t\t// <!-- rsaquo is proposed but not yet ISO standardized -->\n\t\t\t{ \"\\u20AC\", \"&euro;\" }, // -- euro sign, U+20AC NEW -->\n\t};\n\n\tpublic Html4Escape() {\n\t\tsuper();\n\t\taddChain(new LookupReplacer(BASIC_ESCAPE));\n\t\taddChain(new LookupReplacer(ISO8859_1_ESCAPE));\n\t\taddChain(new LookupReplacer(HTML40_EXTENDED_ESCAPE));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java",
    "content": "package cn.hutool.core.text.escape;\n\nimport cn.hutool.core.text.replacer.LookupReplacer;\n\n/**\n * HTML4的UNESCAPE\n *\n * @author looly\n *\n */\npublic class Html4Unescape extends XmlUnescape {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected static final String[][] ISO8859_1_UNESCAPE  = InternalEscapeUtil.invert(Html4Escape.ISO8859_1_ESCAPE);\n\tprotected static final String[][] HTML40_EXTENDED_UNESCAPE  = InternalEscapeUtil.invert(Html4Escape.HTML40_EXTENDED_ESCAPE);\n\n\tpublic Html4Unescape() {\n\t\tsuper();\n\t\taddChain(new LookupReplacer(ISO8859_1_UNESCAPE));\n\t\taddChain(new LookupReplacer(HTML40_EXTENDED_UNESCAPE));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java",
    "content": "package cn.hutool.core.text.escape;\n\n/**\n * 内部Escape工具类\n * @author looly\n *\n */\nclass InternalEscapeUtil {\n\n\t/**\n\t * 将数组中的0和1位置的值互换，即键值转换\n\t *\n\t * @param array String[][] 被转换的数组\n\t * @return String[][] 转换后的数组\n\t */\n\tpublic static String[][] invert(final String[][] array) {\n\t\tfinal String[][] newarray = new String[array.length][2];\n\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\tnewarray[i][0] = array[i][1];\n\t\t\tnewarray[i][1] = array[i][0];\n\t\t}\n\t\treturn newarray;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java",
    "content": "package cn.hutool.core.text.escape;\n\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.text.replacer.StrReplacer;\nimport cn.hutool.core.util.CharUtil;\n\n/**\n * 形如&#39;的反转义器\n *\n * @author looly\n *\n */\npublic class NumericEntityUnescaper extends StrReplacer {\n\tprivate static final long serialVersionUID = 1L;\n\n\t@Override\n\tprotected int replace(CharSequence str, int pos, StrBuilder out) {\n\t\tfinal int len = str.length();\n\t\t// 检查以确保以&#开头\n\t\tif (str.charAt(pos) == '&' && pos < len - 2 && str.charAt(pos + 1) == '#') {\n\t\t\tint start = pos + 2;\n\t\t\tboolean isHex = false;\n\t\t\tfinal char firstChar = str.charAt(start);\n\t\t\tif (firstChar == 'x' || firstChar == 'X') {\n\t\t\t\tstart++;\n\t\t\t\tisHex = true;\n\t\t\t}\n\n\t\t\t// 确保&#后还有数字\n\t\t\tif (start == len) {\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\tint end = start;\n\t\t\twhile (end < len && CharUtil.isHexChar(str.charAt(end))) {\n\t\t\t\tend++;\n\t\t\t}\n\t\t\tfinal boolean isSemiNext = (end != len) && (str.charAt(end) == ';');\n\t\t\tif (isSemiNext) {\n\t\t\t\tint entityValue;\n\t\t\t\ttry {\n\t\t\t\t\tif (isHex) {\n\t\t\t\t\t\tentityValue = Integer.parseInt(str.subSequence(start, end).toString(), 16);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tentityValue = Integer.parseInt(str.subSequence(start, end).toString(), 10);\n\t\t\t\t\t}\n\t\t\t\t} catch (final NumberFormatException nfe) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tout.append((char)entityValue);\n\t\t\t\treturn 2 + end - start + (isHex ? 1 : 0) + 1;\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/XmlEscape.java",
    "content": "package cn.hutool.core.text.escape;\n\nimport cn.hutool.core.text.replacer.LookupReplacer;\nimport cn.hutool.core.text.replacer.ReplacerChain;\n\n/**\n * XML特殊字符转义<br>\n * 见：https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents<br>\n *\n * <pre>\n * \t &amp; (ampersand) 替换为 &amp;amp;\n * \t &lt; (less than) 替换为 &amp;lt;\n * \t &gt; (greater than) 替换为 &amp;gt;\n * \t &quot; (double quote) 替换为 &amp;quot;\n * \t ' (single quote / apostrophe) 替换为 &amp;apos;\n * </pre>\n *\n * @author looly\n * @since 5.7.2\n */\npublic class XmlEscape extends ReplacerChain {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected static final String[][] BASIC_ESCAPE = { //\n\t\t\t{\"'\", \"&apos;\"}, // \" - single-quote\n\t\t\t{\"\\\"\", \"&quot;\"}, // \" - double-quote\n\t\t\t{\"&\", \"&amp;\"}, // & - ampersand\n\t\t\t{\"<\", \"&lt;\"}, // < - less-than\n\t\t\t{\">\", \"&gt;\"}, // > - greater-than\n\t};\n\n\t/**\n\t * 构造\n\t */\n\tpublic XmlEscape() {\n\t\taddChain(new LookupReplacer(BASIC_ESCAPE));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/XmlUnescape.java",
    "content": "package cn.hutool.core.text.escape;\n\nimport cn.hutool.core.text.replacer.LookupReplacer;\nimport cn.hutool.core.text.replacer.ReplacerChain;\n\n/**\n * XML的UNESCAPE\n *\n * @author looly\n * @since 5.7.2\n */\npublic class XmlUnescape extends ReplacerChain {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected static final String[][] BASIC_UNESCAPE  = InternalEscapeUtil.invert(XmlEscape.BASIC_ESCAPE);\n\n\t/**\n\t * 构造\n\t */\n\tpublic XmlUnescape() {\n\t\taddChain(new LookupReplacer(BASIC_UNESCAPE));\n\t\taddChain(new NumericEntityUnescaper());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/escape/package-info.java",
    "content": "/**\n * 提供各种转义和反转义实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.text.escape;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/CharFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.NumberUtil;\n\n/**\n * 字符查找器<br>\n * 查找指定字符在字符串中的位置信息\n *\n * @author looly\n * @since 5.7.14\n */\npublic class CharFinder extends TextFinder {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final char c;\n\tprivate final boolean caseInsensitive;\n\n\t/**\n\t * 构造，不忽略字符大小写\n\t *\n\t * @param c 被查找的字符\n\t */\n\tpublic CharFinder(char c) {\n\t\tthis(c, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param c               被查找的字符\n\t * @param caseInsensitive 是否忽略大小写\n\t */\n\tpublic CharFinder(char c, boolean caseInsensitive) {\n\t\tthis.c = c;\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic int start(int from) {\n\t\tAssert.notNull(this.text, \"Text to find must be not null!\");\n\t\tfinal int limit = getValidEndIndex();\n\t\tif(negative){\n\t\t\tfor (int i = from; i > limit; i--) {\n\t\t\t\tif (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t} else{\n\t\t\tfor (int i = from; i < limit; i++) {\n\t\t\t\tif (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t@Override\n\tpublic int end(int start) {\n\t\tif (start < 0) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn start + 1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/CharMatcherFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Matcher;\n\n/**\n * 字符匹配查找器<br>\n * 查找满足指定{@link Matcher} 匹配的字符所在位置，此类长用于查找某一类字符，如数字等\n *\n * @since 5.7.14\n * @author looly\n */\npublic class CharMatcherFinder extends TextFinder {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Matcher<Character> matcher;\n\n\t/**\n\t * 构造\n\t * @param matcher 被查找的字符匹配器\n\t */\n\tpublic CharMatcherFinder(Matcher<Character> matcher) {\n\t\tthis.matcher = matcher;\n\t}\n\n\t@Override\n\tpublic int start(int from) {\n\t\tAssert.notNull(this.text, \"Text to find must be not null!\");\n\t\tfinal int limit = getValidEndIndex();\n\t\tif(negative){\n\t\t\tfor (int i = from; i > limit; i--) {\n\t\t\t\tif(matcher.match(text.charAt(i))){\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = from; i < limit; i++) {\n\t\t\t\tif(matcher.match(text.charAt(i))){\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t@Override\n\tpublic int end(int start) {\n\t\tif(start < 0){\n\t\t\treturn -1;\n\t\t}\n\t\treturn start + 1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/Finder.java",
    "content": "package cn.hutool.core.text.finder;\n\n/**\n * 字符串查找接口，通过调用{@link #start(int)}查找开始位置，再调用{@link #end(int)}找结束位置\n *\n * @author looly\n * @since 5.7.14\n */\npublic interface Finder {\n\n\tint INDEX_NOT_FOUND = -1;\n\n\t/**\n\t * 返回开始位置，即起始字符位置（包含），未找到返回-1\n\t *\n\t * @param from 查找的开始位置（包含）\n\t * @return 起始字符位置，未找到返回-1\n\t */\n\tint start(int from);\n\n\t/**\n\t * 返回结束位置，即最后一个字符后的位置（不包含）\n\t *\n\t * @param start 找到的起始位置\n\t * @return 结束位置，未找到返回-1\n\t */\n\tint end(int start);\n\n\t/**\n\t * 复位查找器，用于重用对象\n\t * @return this\n\t */\n\tdefault Finder reset(){\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/LengthFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport cn.hutool.core.lang.Assert;\n\n/**\n * 固定长度查找器<br>\n * 给定一个长度，查找的位置为from + length，一般用于分段截取\n *\n * @since 5.7.14\n * @author looly\n */\npublic class LengthFinder extends TextFinder {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final int length;\n\n\t/**\n\t * 构造\n\t * @param length 长度\n\t */\n\tpublic LengthFinder(int length) {\n\t\tAssert.isTrue(length > 0, \"Length must be great than 0\");\n\t\tthis.length = length;\n\t}\n\n\t@Override\n\tpublic int start(int from) {\n\t\tAssert.notNull(this.text, \"Text to find must be not null!\");\n\t\tfinal int limit = getValidEndIndex();\n\t\tint result;\n\t\tif(negative){\n\t\t\tresult = from - length;\n\t\t\tif(result > limit){\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} else {\n\t\t\tresult = from + length;\n\t\t\tif(result < limit){\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n\n\t@Override\n\tpublic int end(int start) {\n\t\treturn start;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/PatternFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 正则查找器<br>\n * 通过传入正则表达式，查找指定字符串中匹配正则的开始和结束位置\n *\n * @author looly\n * @since 5.7.14\n */\npublic class PatternFinder extends TextFinder {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Pattern pattern;\n\tprivate Matcher matcher;\n\n\t/**\n\t * 构造\n\t *\n\t * @param regex           被查找的正则表达式\n\t * @param caseInsensitive 是否忽略大小写\n\t */\n\tpublic PatternFinder(String regex, boolean caseInsensitive) {\n\t\tthis(Pattern.compile(regex, caseInsensitive ? Pattern.CASE_INSENSITIVE : 0));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pattern 被查找的正则{@link Pattern}\n\t */\n\tpublic PatternFinder(Pattern pattern) {\n\t\tthis.pattern = pattern;\n\t}\n\n\t@Override\n\tpublic TextFinder setText(CharSequence text) {\n\t\tthis.matcher = pattern.matcher(text);\n\t\treturn super.setText(text);\n\t}\n\n\t@Override\n\tpublic TextFinder setNegative(boolean negative) {\n\t\tthrow new UnsupportedOperationException(\"Negative is invalid for Pattern!\");\n\t}\n\n\t@Override\n\tpublic int start(int from) {\n\t\tif (matcher.find(from)) {\n\t\t\tfinal int end = matcher.end();\n\t\t\t// 只有匹配到的字符串结尾在limit范围内，才算找到\n\t\t\tif(end <= getValidEndIndex()){\n\t\t\t\tfinal int start = matcher.start();\n\t\t\t\tif(start == end){\n\t\t\t\t\t// issue#3421，如果匹配空串，按照未匹配对待，避免死循环\n\t\t\t\t\treturn INDEX_NOT_FOUND;\n\t\t\t\t}\n\n\t\t\t\treturn start;\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t@Override\n\tpublic int end(int start) {\n\t\tfinal int end = matcher.end();\n\t\tfinal int limit;\n\t\tif(endIndex < 0){\n\t\t\tlimit = text.length();\n\t\t}else{\n\t\t\tlimit = Math.min(endIndex, text.length());\n\t\t}\n\t\treturn end <= limit ? end : INDEX_NOT_FOUND;\n\t}\n\n\t@Override\n\tpublic PatternFinder reset() {\n\t\tthis.matcher.reset();\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/StrFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.text.CharSequenceUtil;\n\n/**\n * 字符串查找器\n *\n * @author looly\n * @since 5.7.14\n */\npublic class StrFinder extends TextFinder {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final CharSequence strToFind;\n\tprivate final boolean caseInsensitive;\n\n\t/**\n\t * 构造\n\t *\n\t * @param strToFind       被查找的字符串\n\t * @param caseInsensitive 是否忽略大小写\n\t */\n\tpublic StrFinder(CharSequence strToFind, boolean caseInsensitive) {\n\t\tAssert.notEmpty(strToFind);\n\t\tthis.strToFind = strToFind;\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic int start(int from) {\n\t\tAssert.notNull(this.text, \"Text to find must be not null!\");\n\t\tfinal int subLen = strToFind.length();\n\n\t\tif (from < 0) {\n\t\t\tfrom = 0;\n\t\t}\n\t\tint endLimit = getValidEndIndex();\n\t\tif (negative) {\n\t\t\tfor (int i = from; i > endLimit; i--) {\n\t\t\t\tif (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tendLimit = endLimit - subLen + 1;\n\t\t\tfor (int i = from; i < endLimit; i++) {\n\t\t\t\tif (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t@Override\n\tpublic int end(int start) {\n\t\tif (start < 0) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn start + strToFind.length();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/TextFinder.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.Serializable;\n\n/**\n * 文本查找抽象类\n *\n * @author looly\n * @since 5.7.14\n */\npublic abstract class TextFinder implements Finder, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected CharSequence text;\n\tprotected int endIndex = -1;\n\tprotected boolean negative;\n\n\t/**\n\t * 设置被查找的文本\n\t *\n\t * @param text 文本\n\t * @return this\n\t */\n\tpublic TextFinder setText(CharSequence text) {\n\t\tthis.text = Assert.notNull(text, \"Text must be not null!\");\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置查找的结束位置<br>\n\t * 如果从前向后查找，结束位置最大为text.length()<br>\n\t * 如果从后向前，结束位置为-1\n\t *\n\t * @param endIndex 结束位置（不包括）\n\t * @return this\n\t */\n\tpublic TextFinder setEndIndex(int endIndex) {\n\t\tthis.endIndex = endIndex;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否反向查找，{@code true}表示从后向前查找\n\t *\n\t * @param negative 结束位置（不包括）\n\t * @return this\n\t */\n\tpublic TextFinder setNegative(boolean negative) {\n\t\tthis.negative = negative;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取有效结束位置<br>\n\t * 如果{@link #endIndex}小于0，在反向模式下是开头（-1），正向模式是结尾（text.length()）\n\t *\n\t * @return 有效结束位置\n\t */\n\tprotected int getValidEndIndex() {\n\t\tif(negative && -1 == endIndex){\n\t\t\t// 反向查找模式下，-1表示0前面的位置，即字符串反向末尾的位置\n\t\t\treturn -1;\n\t\t}\n\t\tfinal int limit;\n\t\tif (endIndex < 0) {\n\t\t\tlimit = endIndex + text.length() + 1;\n\t\t} else {\n\t\t\tlimit = Math.min(endIndex, text.length());\n\t\t}\n\t\treturn limit;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/finder/package-info.java",
    "content": "/**\n * 文本查找实现，包括：\n * <ul>\n *     <li>查找文本中的字符（正向、反向）</li>\n *     <li>查找文本中的匹配字符（正向、反向）</li>\n *     <li>查找文本中的字符串（正向、反向）</li>\n *     <li>查找文本中匹配正则的字符串（正向）</li>\n * </ul>\n *\n * @author looly\n *\n */\npackage cn.hutool.core.text.finder;\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/package-info.java",
    "content": "/**\n * 提供文本相关操作的封装，还包括Unicode工具UnicodeUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.text;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java",
    "content": "package cn.hutool.core.text.replacer;\n\nimport cn.hutool.core.text.StrBuilder;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 查找替换器，通过查找指定关键字，替换对应的值\n *\n * @author looly\n * @since 4.1.5\n */\npublic class LookupReplacer extends StrReplacer {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Map<String, String> lookupMap;\n\tprivate final Set<Character> prefixSet;\n\tprivate final int minLength;\n\tprivate final int maxLength;\n\n\t/**\n\t * 构造\n\t *\n\t * @param lookup 被查找的键值对\n\t */\n\tpublic LookupReplacer(String[]... lookup) {\n\t\tthis.lookupMap = new HashMap<>();\n\t\tthis.prefixSet = new HashSet<>();\n\n\t\tint minLength = Integer.MAX_VALUE;\n\t\tint maxLength = 0;\n\t\tString key;\n\t\tint keySize;\n\t\tfor (String[] pair : lookup) {\n\t\t\tkey = pair[0];\n\t\t\tlookupMap.put(key, pair[1]);\n\t\t\tthis.prefixSet.add(key.charAt(0));\n\t\t\tkeySize = key.length();\n\t\t\tif (keySize > maxLength) {\n\t\t\t\tmaxLength = keySize;\n\t\t\t}\n\t\t\tif (keySize < minLength) {\n\t\t\t\tminLength = keySize;\n\t\t\t}\n\t\t}\n\t\tthis.maxLength = maxLength;\n\t\tthis.minLength = minLength;\n\t}\n\n\t@Override\n\tprotected int replace(CharSequence str, int pos, StrBuilder out) {\n\t\tif (prefixSet.contains(str.charAt(pos))) {\n\t\t\tint max = this.maxLength;\n\t\t\tif (pos + this.maxLength > str.length()) {\n\t\t\t\tmax = str.length() - pos;\n\t\t\t}\n\t\t\tCharSequence subSeq;\n\t\t\tString result;\n\t\t\tfor (int i = max; i >= this.minLength; i--) {\n\t\t\t\tsubSeq = str.subSequence(pos, pos + i);\n\t\t\t\tresult = lookupMap.get(subSeq.toString());\n\t\t\t\tif(null != result) {\n\t\t\t\t\tout.append(result);\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java",
    "content": "package cn.hutool.core.text.replacer;\n\nimport cn.hutool.core.lang.Chain;\nimport cn.hutool.core.text.StrBuilder;\n\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * 字符串替换链，用于组合多个字符串替换逻辑\n *\n * @author looly\n * @since 4.1.5\n */\npublic class ReplacerChain extends StrReplacer implements Chain<StrReplacer, ReplacerChain> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final List<StrReplacer> replacers = new LinkedList<>();\n\n\t/**\n\t * 构造\n\t *\n\t * @param strReplacers 字符串替换器\n\t */\n\tpublic ReplacerChain(StrReplacer... strReplacers) {\n\t\tfor (StrReplacer strReplacer : strReplacers) {\n\t\t\taddChain(strReplacer);\n\t\t}\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Iterator<StrReplacer> iterator() {\n\t\treturn replacers.iterator();\n\t}\n\n\t@Override\n\tpublic ReplacerChain addChain(StrReplacer element) {\n\t\treplacers.add(element);\n\t\treturn this;\n\t}\n\n\t@Override\n\tprotected int replace(CharSequence str, int pos, StrBuilder out) {\n\t\tint consumed = 0;\n\t\tfor (StrReplacer strReplacer : replacers) {\n\t\t\tconsumed = strReplacer.replace(str, pos, out);\n\t\t\tif (0 != consumed) {\n\t\t\t\treturn consumed;\n\t\t\t}\n\t\t}\n\t\treturn consumed;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java",
    "content": "package cn.hutool.core.text.replacer;\n\nimport cn.hutool.core.lang.Replacer;\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\n\n/**\n * 抽象字符串替换类<br>\n * 通过实现replace方法实现局部替换逻辑\n *\n * @author looly\n * @since 4.1.5\n */\npublic abstract class StrReplacer implements Replacer<CharSequence>, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 抽象的字符串替换方法，通过传入原字符串和当前位置，执行替换逻辑，返回处理或替换的字符串长度部分。\n\t *\n\t * @param str 被处理的字符串\n\t * @param pos 当前位置\n\t * @param out 输出\n\t * @return 处理的原字符串长度，0表示跳过此字符\n\t */\n\tprotected abstract int replace(CharSequence str, int pos, StrBuilder out);\n\n\t@Override\n\tpublic CharSequence replace(CharSequence t) {\n\t\tif(StrUtil.isEmpty(t)){\n\t\t\treturn t;\n\t\t}\n\t\tfinal int len = t.length();\n\t\tfinal StrBuilder builder = StrBuilder.create(len);\n\t\tint pos = 0;//当前位置\n\t\tint consumed;//处理过的字符数\n\t\twhile (pos < len) {\n\t\t\tconsumed = replace(t, pos, builder);\n\t\t\tif (0 == consumed) {\n\t\t\t\t//0表示未处理或替换任何字符，原样输出本字符并从下一个字符继续\n\t\t\t\tbuilder.append(t.charAt(pos));\n\t\t\t\tpos++;\n\t\t\t}\n\t\t\tpos += consumed;\n\t\t}\n\t\treturn builder;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/replacer/package-info.java",
    "content": "/**\n * 文本替换类抽象及实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.text.replacer;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/text/split/SplitIter.java",
    "content": "package cn.hutool.core.text.split;\n\nimport cn.hutool.core.collection.ComputeIter;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.text.finder.TextFinder;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\n\n/**\n * 字符串切分迭代器<br>\n * 此迭代器是字符串切分的懒模式实现，实例化后不完成切分，只有调用{@link #hasNext()}或遍历时才完成切分<br>\n * 此迭代器非线程安全\n *\n * @author looly\n * @since 5.7.14\n */\npublic class SplitIter extends ComputeIter<String> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String text;\n\tprivate final TextFinder finder;\n\tprivate final int limit;\n\tprivate final boolean ignoreEmpty;\n\n\t/**\n\t * 上一次的结束位置\n\t */\n\tprivate int offset;\n\t/**\n\t * 计数器，用于判断是否超过limit\n\t */\n\tprivate int count;\n\n\t/**\n\t * 构造\n\t *\n\t * @param text            文本，不能为{@code null}\n\t * @param separatorFinder 分隔符匹配器\n\t * @param limit           限制数量，小于等于0表示无限制\n\t * @param ignoreEmpty     是否忽略\"\"\n\t */\n\tpublic SplitIter(CharSequence text, TextFinder separatorFinder, int limit, boolean ignoreEmpty) {\n\t\tAssert.notNull(text, \"Text must be not null!\");\n\t\tthis.text = text.toString();\n\t\tthis.finder = separatorFinder.setText(text);\n\t\tthis.limit = limit > 0 ? limit : Integer.MAX_VALUE;\n\t\tthis.ignoreEmpty = ignoreEmpty;\n\t}\n\n\t@Override\n\tprotected String computeNext() {\n\t\t// 达到数量上限或末尾，结束\n\t\tif (count >= limit || offset > text.length()) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 达到数量上限\n\t\tif (count == (limit - 1)) {\n\t\t\t// 当到达限制次数时，最后一个元素为剩余部分\n\t\t\tif (ignoreEmpty && offset == text.length()) {\n\t\t\t\t// 最后一个是空串\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// 结尾整个作为一个元素\n\t\t\tcount++;\n\t\t\treturn text.substring(offset);\n\t\t}\n\n\t\tString result = null;\n\t\tint start;\n\t\tdo {\n\t\t\tstart = finder.start(offset);\n\t\t\t// 无分隔符，结束\n\t\t\tif (start < 0) {\n\t\t\t\t// 如果不再有分隔符，但是遗留了字符，则单独作为一个段\n\t\t\t\tif (offset <= text.length()) {\n\t\t\t\t\tresult = text.substring(offset);\n\t\t\t\t\tif (!ignoreEmpty || !result.isEmpty()) {\n\t\t\t\t\t\t// 返回非空串\n\t\t\t\t\t\toffset = Integer.MAX_VALUE;\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// 找到新的分隔符位置\n\t\t\tresult = text.substring(offset, start);\n\t\t\toffset = finder.end(start);\n\t\t} while (ignoreEmpty && result.isEmpty()); // 空串则继续循环\n\n\t\tcount++;\n\t\treturn result;\n\t}\n\n\t/**\n\t * 重置\n\t */\n\tpublic void reset() {\n\t\tthis.finder.reset();\n\t\tthis.offset = 0;\n\t\tthis.count = 0;\n\t\tresetState();\n\t}\n\n\t/**\n\t * 获取切分后的对象数组\n\t *\n\t * @param trim 是否去除元素两边空格\n\t * @return 切分后的列表\n\t */\n\tpublic String[] toArray(boolean trim) {\n\t\treturn toList(trim).toArray(new String[0]);\n\t}\n\n\t/**\n\t * 获取切分后的对象列表\n\t *\n\t * @param trim 是否去除元素两边空格\n\t * @return 切分后的列表\n\t */\n\tpublic List<String> toList(boolean trim) {\n\t\treturn toList((str) -> trim ? StrUtil.trim(str) : str);\n\t}\n\n\t/**\n\t * 获取切分后的对象列表\n\t *\n\t * @param <T>     元素类型\n\t * @param mapping 字符串映射函数\n\t * @return 切分后的列表\n\t */\n\tpublic <T> List<T> toList(Function<String, T> mapping) {\n\t\tfinal List<T> result = new ArrayList<>();\n\t\twhile (this.hasNext()) {\n\t\t\tfinal T apply = mapping.apply(this.next());\n\t\t\tif (ignoreEmpty && StrUtil.isEmptyIfStr(apply)) {\n\t\t\t\t// 对于mapping之后依旧是String的情况，ignoreEmpty依旧有效\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tresult.add(apply);\n\t\t}\n\t\tif (result.isEmpty()) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/AsyncUtil.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.lang.reflect.UndeclaredThrowableException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutionException;\n\n/**\n * {@link CompletableFuture}异步工具类<br>\n * {@link CompletableFuture} 是 Future 的改进，可以通过传入回调对象，在任务完成后调用之\n *\n * @author achao1441470436@gmail.com\n * @since 5.7.17\n */\npublic class AsyncUtil {\n\n\t/**\n\t * 等待所有任务执行完毕，包裹了异常\n\t *\n\t * @param tasks 并行任务\n\t * @throws UndeclaredThrowableException 未受检异常\n\t */\n\tpublic static void waitAll(CompletableFuture<?>... tasks) {\n\t\ttry {\n\t\t\tCompletableFuture.allOf(tasks).get();\n\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\tthrow new ThreadException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 等待任意一个任务执行完毕，包裹了异常\n\t *\n\t * @param <T>  任务返回值类型\n\t * @param tasks 并行任务\n\t * @return 执行结束的任务返回值\n\t * @throws UndeclaredThrowableException 未受检异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T waitAny(CompletableFuture<?>... tasks) {\n\t\ttry {\n\t\t\treturn (T) CompletableFuture.anyOf(tasks).get();\n\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\tthrow new ThreadException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取异步任务结果，包裹了异常\n\t *\n\t * @param <T>  任务返回值类型\n\t * @param task 异步任务\n\t * @return 任务返回值\n\t * @throws RuntimeException 未受检异常\n\t */\n\tpublic static <T> T get(CompletableFuture<T> task) {\n\t\ttry {\n\t\t\treturn task.get();\n\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\tthrow new ThreadException(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/BlockPolicy.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.RejectedExecutionHandler;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.function.Consumer;\n\n/**\n * 当任务队列过长时处于阻塞状态，直到添加到队列中\n * 如果阻塞过程中被中断，就会抛出{@link InterruptedException}异常<br>\n * 有时候在线程池内访问第三方接口，只希望固定并发数去访问，并且不希望丢弃任务时使用此策略，队列满的时候会处于阻塞状态(例如刷库的场景)\n *\n * @author luozongle\n * @since 5.8.0\n */\npublic class BlockPolicy implements RejectedExecutionHandler {\n\n\t/**\n\t * 线程池关闭时，为避免任务丢失，留下处理方法\n\t * 如果需要由调用方来运行，可以{@code new BlockPolicy(Runnable::run)}\n\t */\n\tprivate final Consumer<Runnable> handlerwhenshutdown;\n\n\t/**\n\t * 构造\n\t *\n\t * @param handlerwhenshutdown 线程池关闭后的执行策略\n\t */\n\tpublic BlockPolicy(final Consumer<Runnable> handlerwhenshutdown) {\n\t\tthis.handlerwhenshutdown = handlerwhenshutdown;\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic BlockPolicy() {\n\t\tthis(null);\n\t}\n\n\t@Override\n\tpublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) {\n\t\t// 线程池未关闭时，阻塞等待\n\t\tif (false == e.isShutdown()) {\n\t\t\ttry {\n\t\t\t\te.getQueue().put(r);\n\t\t\t} catch (InterruptedException ex) {\n\t\t\t\tthrow new RejectedExecutionException(\"Task \" + r + \" rejected from \" + e);\n\t\t\t}\n\t\t} else if (null != handlerwhenshutdown) {\n\t\t\t// 当设置了关闭时候的处理\n\t\t\thandlerwhenshutdown.accept(r);\n\t\t}\n\n\t\t// 线程池关闭后，丢弃任务\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.date.TimeInterval;\n\nimport java.io.Closeable;\nimport java.io.IOException;\n\n/**\n * 高并发测试工具类\n *\n * <pre>\n * ps:\n * //模拟1000个线程并发\n * ConcurrencyTester ct = new ConcurrencyTester(1000);\n * ct.test(() -&gt; {\n *      // 需要并发测试的业务代码\n * });\n *\n * Console.log(ct.getInterval());\n * ct.close();\n * </pre>\n *\n * @author kwer\n */\npublic class ConcurrencyTester implements Closeable {\n\tprivate final SyncFinisher sf;\n\tprivate final TimeInterval timeInterval;\n\tprivate long interval;\n\n\t/**\n\t * 构造\n\t * @param threadSize 线程数\n\t */\n\tpublic ConcurrencyTester(int threadSize) {\n\t\tthis.sf = new SyncFinisher(threadSize);\n\t\tthis.timeInterval = new TimeInterval();\n\t}\n\n\t/**\n\t * 执行测试<br>\n\t * 执行测试后不会关闭线程池，可以调用{@link #close()}释放线程池\n\t *\n\t * @param runnable 要测试的内容\n\t * @return this\n\t */\n\tpublic ConcurrencyTester test(Runnable runnable) {\n\t\tthis.sf.clearWorker();\n\n\t\ttimeInterval.start();\n\t\tthis.sf\n\t\t\t\t.addRepeatWorker(runnable)\n\t\t\t\t.setBeginAtSameTime(true)\n\t\t\t\t.start();\n\n\t\tthis.interval = timeInterval.interval();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置测试器，重置包括：\n\t *\n\t * <ul>\n\t *     <li>清空worker</li>\n\t *     <li>重置计时器</li>\n\t * </ul>\n\t *\n\t * @return this\n\t * @since 5.7.2\n\t */\n\tpublic ConcurrencyTester reset(){\n\t\tthis.sf.clearWorker();\n\t\tthis.timeInterval.restart();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取执行时间\n\t *\n\t * @return 执行时间，单位毫秒\n\t */\n\tpublic long getInterval() {\n\t\treturn this.interval;\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tthis.sf.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.AbstractExecutorService;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\n/**\n * ExecutorService代理\n *\n * @author loolly\n */\npublic class DelegatedExecutorService extends AbstractExecutorService {\n\tprivate final ExecutorService e;\n\n\t/**\n\t * 构造\n\t *\n\t * @param executor {@link ExecutorService}\n\t */\n\tpublic DelegatedExecutorService(ExecutorService executor) {\n\t\tAssert.notNull(executor, \"executor must be not null !\");\n\t\te = executor;\n\t}\n\n\t@Override\n\tpublic void execute(Runnable command) {\n\t\te.execute(command);\n\t}\n\n\t@Override\n\tpublic void shutdown() {\n\t\te.shutdown();\n\t}\n\n\t@Override\n\tpublic List<Runnable> shutdownNow() {\n\t\treturn e.shutdownNow();\n\t}\n\n\t@Override\n\tpublic boolean isShutdown() {\n\t\treturn e.isShutdown();\n\t}\n\n\t@Override\n\tpublic boolean isTerminated() {\n\t\treturn e.isTerminated();\n\t}\n\n\t@Override\n\tpublic boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {\n\t\treturn e.awaitTermination(timeout, unit);\n\t}\n\n\t@Override\n\tpublic Future<?> submit(Runnable task) {\n\t\treturn e.submit(task);\n\t}\n\n\t@Override\n\tpublic <T> Future<T> submit(Callable<T> task) {\n\t\treturn e.submit(task);\n\t}\n\n\t@Override\n\tpublic <T> Future<T> submit(Runnable task, T result) {\n\t\treturn e.submit(task, result);\n\t}\n\n\t@Override\n\tpublic <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {\n\t\treturn e.invokeAll(tasks);\n\t}\n\n\t@Override\n\tpublic <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)\n\t\t\tthrows InterruptedException {\n\t\treturn e.invokeAll(tasks, timeout, unit);\n\t}\n\n\t@Override\n\tpublic <T> T invokeAny(Collection<? extends Callable<T>> tasks)\n\t\t\tthrows InterruptedException, ExecutionException {\n\t\treturn e.invokeAny(tasks);\n\t}\n\n\t@Override\n\tpublic <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)\n\t\t\tthrows InterruptedException, ExecutionException, TimeoutException {\n\t\treturn e.invokeAny(tasks, timeout, unit);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.RejectedExecutionHandler;\nimport java.util.concurrent.SynchronousQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * {@link ThreadPoolExecutor} 建造者\n *\n * <pre>\n *     1. 如果池中任务数 &lt; corePoolSize     -》 放入立即执行\n *     2. 如果池中任务数 &gt; corePoolSize     -》 放入队列等待\n *     3. 队列满                              -》 新建线程立即执行\n *     4. 执行中的线程 &gt; maxPoolSize        -》 触发handler（RejectedExecutionHandler）异常\n * </pre>\n *\n * @author looly\n * @since 4.1.9\n */\npublic class ExecutorBuilder implements Builder<ThreadPoolExecutor> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 默认的等待队列容量 */\n\tpublic static final int DEFAULT_QUEUE_CAPACITY = 1024;\n\n\t/**\n\t * 初始池大小\n\t */\n\tprivate int corePoolSize;\n\t/**\n\t * 最大池大小（允许同时执行的最大线程数）\n\t */\n\tprivate int maxPoolSize = Integer.MAX_VALUE;\n\t/**\n\t * 线程存活时间，即当池中线程多于初始大小时，多出的线程保留的时长\n\t */\n\tprivate long keepAliveTime = TimeUnit.SECONDS.toNanos(60);\n\t/**\n\t * 队列，用于存放未执行的线程\n\t */\n\tprivate BlockingQueue<Runnable> workQueue;\n\t/**\n\t * 线程工厂，用于自定义线程创建\n\t */\n\tprivate ThreadFactory threadFactory;\n\t/**\n\t * 当线程阻塞（block）时的异常处理器，所谓线程阻塞即线程池和等待队列已满，无法处理线程时采取的策略\n\t */\n\tprivate RejectedExecutionHandler handler;\n\t/**\n\t * 线程执行超时后是否回收线程\n\t */\n\tprivate Boolean allowCoreThreadTimeOut;\n\n\t/**\n\t * 设置初始池大小，默认0\n\t *\n\t * @param corePoolSize 初始池大小\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setCorePoolSize(int corePoolSize) {\n\t\tthis.corePoolSize = corePoolSize;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置最大池大小（允许同时执行的最大线程数）\n\t *\n\t * @param maxPoolSize 最大池大小（允许同时执行的最大线程数）\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setMaxPoolSize(int maxPoolSize) {\n\t\tthis.maxPoolSize = maxPoolSize;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置线程存活时间，即当池中线程多于初始大小时，多出的线程保留的时长\n\t *\n\t * @param keepAliveTime 线程存活时间\n\t * @param unit          单位\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setKeepAliveTime(long keepAliveTime, TimeUnit unit) {\n\t\treturn setKeepAliveTime(unit.toNanos(keepAliveTime));\n\t}\n\n\t/**\n\t * 设置线程存活时间，即当池中线程多于初始大小时，多出的线程保留的时长，单位纳秒\n\t *\n\t * @param keepAliveTime 线程存活时间，单位纳秒\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setKeepAliveTime(long keepAliveTime) {\n\t\tthis.keepAliveTime = keepAliveTime;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置队列，用于存在未执行的线程<br>\n\t * 可选队列有：\n\t *\n\t * <pre>\n\t * 1. {@link SynchronousQueue}    它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t * 2. {@link LinkedBlockingQueue} 默认无界队列，当运行线程大于corePoolSize时始终放入此队列，此时maxPoolSize无效。\n\t *                        当构造LinkedBlockingQueue对象时传入参数，变为有界队列，队列满时，运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t * 3. {@link ArrayBlockingQueue}  有界队列，相对无界队列有利于控制队列大小，队列满时，运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t * </pre>\n\t *\n\t * @param workQueue 队列\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setWorkQueue(BlockingQueue<Runnable> workQueue) {\n\t\tthis.workQueue = workQueue;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 使用{@link ArrayBlockingQueue} 做为等待队列<br>\n\t * 有界队列，相对无界队列有利于控制队列大小，队列满时，运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t *\n\t * @param capacity 队列容量\n\t * @return this\n\t * @since 5.1.4\n\t */\n\tpublic ExecutorBuilder useArrayBlockingQueue(int capacity) {\n\t\treturn setWorkQueue(new ArrayBlockingQueue<>(capacity));\n\t}\n\n\t/**\n\t * 使用{@link SynchronousQueue} 做为等待队列（非公平策略）<br>\n\t * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t *\n\t * @return this\n\t * @since 4.1.11\n\t */\n\tpublic ExecutorBuilder useSynchronousQueue() {\n\t\treturn useSynchronousQueue(false);\n\t}\n\n\t/**\n\t * 使用{@link SynchronousQueue} 做为等待队列<br>\n\t * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程，否则触发异常策略\n\t *\n\t * @param fair 是否使用公平访问策略\n\t * @return this\n\t * @since 4.5.0\n\t */\n\tpublic ExecutorBuilder useSynchronousQueue(boolean fair) {\n\t\treturn setWorkQueue(new SynchronousQueue<>(fair));\n\t}\n\n\t/**\n\t * 设置线程工厂，用于自定义线程创建\n\t *\n\t * @param threadFactory 线程工厂\n\t * @return this\n\t * @see ThreadFactoryBuilder\n\t */\n\tpublic ExecutorBuilder setThreadFactory(ThreadFactory threadFactory) {\n\t\tthis.threadFactory = threadFactory;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置当线程阻塞（block）时的异常处理器，所谓线程阻塞即线程池和等待队列已满，无法处理线程时采取的策略\n\t * <p>\n\t * 此处可以使用JDK预定义的几种策略，见{@link RejectPolicy}枚举\n\t *\n\t * @param handler {@link RejectedExecutionHandler}\n\t * @return this\n\t * @see RejectPolicy\n\t */\n\tpublic ExecutorBuilder setHandler(RejectedExecutionHandler handler) {\n\t\tthis.handler = handler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置线程执行超时后是否回收线程\n\t *\n\t * @param allowCoreThreadTimeOut 线程执行超时后是否回收线程\n\t * @return this\n\t */\n\tpublic ExecutorBuilder setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {\n\t\tthis.allowCoreThreadTimeOut = allowCoreThreadTimeOut;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建ExecutorBuilder，开始构建\n\t *\n\t * @return this\n\t */\n\tpublic static ExecutorBuilder create() {\n\t\treturn new ExecutorBuilder();\n\t}\n\n\t/**\n\t * 构建ThreadPoolExecutor\n\t */\n\t@Override\n\tpublic ThreadPoolExecutor build() {\n\t\treturn build(this);\n\t}\n\n\t/**\n\t * 创建有回收关闭功能的ExecutorService\n\t *\n\t * @return 创建有回收关闭功能的ExecutorService\n\t * @since 5.1.4\n\t */\n\tpublic ExecutorService buildFinalizable() {\n\t\treturn new FinalizableDelegatedExecutorService(build());\n\t}\n\n\t/**\n\t * 构建ThreadPoolExecutor\n\t *\n\t * @param builder this\n\t * @return {@link ThreadPoolExecutor}\n\t */\n\tprivate static ThreadPoolExecutor build(ExecutorBuilder builder) {\n\t\tfinal int corePoolSize = builder.corePoolSize;\n\t\tfinal int maxPoolSize = builder.maxPoolSize;\n\t\tfinal long keepAliveTime = builder.keepAliveTime;\n\t\tfinal BlockingQueue<Runnable> workQueue;\n\t\tif (null != builder.workQueue) {\n\t\t\tworkQueue = builder.workQueue;\n\t\t} else {\n\t\t\t// corePoolSize为0则要使用SynchronousQueue避免无限阻塞\n\t\t\tworkQueue = (corePoolSize <= 0) ? new SynchronousQueue<>() : new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);\n\t\t}\n\t\tfinal ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory();\n\t\tRejectedExecutionHandler handler = ObjectUtil.defaultIfNull(builder.handler, RejectPolicy.ABORT.getValue());\n\n\t\tfinal ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(//\n\t\t\t\tcorePoolSize, //\n\t\t\t\tmaxPoolSize, //\n\t\t\t\tkeepAliveTime, TimeUnit.NANOSECONDS, //\n\t\t\t\tworkQueue, //\n\t\t\t\tthreadFactory, //\n\t\t\t\thandler//\n\t\t);\n\t\tif (null != builder.allowCoreThreadTimeOut) {\n\t\t\tthreadPoolExecutor.allowCoreThreadTimeOut(builder.allowCoreThreadTimeOut);\n\t\t}\n\t\treturn threadPoolExecutor;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.concurrent.ExecutorService;\n\n/**\n * 保证ExecutorService在对象回收时正常结束\n *\n * @author loolly\n */\npublic class FinalizableDelegatedExecutorService extends DelegatedExecutorService {\n\n\t/**\n\t * 构造\n\t *\n\t * @param executor {@link ExecutorService}\n\t */\n\tFinalizableDelegatedExecutorService(ExecutorService executor) {\n\t\tsuper(executor);\n\t}\n\n\t@Override\n\tprotected void finalize() {\n\t\tsuper.shutdown();\n\t}\n}"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\n\nimport cn.hutool.core.exceptions.UtilException;\n\n/**\n * 全局公共线程池<br>\n * 此线程池是一个无限线程池，即加入的线程不等待任何线程，直接执行\n *\n * @author Looly\n *\n */\npublic class GlobalThreadPool {\n\tprivate static ExecutorService executor;\n\n\tprivate GlobalThreadPool() {\n\t}\n\n\tstatic {\n\t\tinit();\n\t}\n\n\t/**\n\t * 初始化全局线程池\n\t */\n\tsynchronized public static void init() {\n\t\tif (null != executor) {\n\t\t\texecutor.shutdownNow();\n\t\t}\n\t\texecutor = ExecutorBuilder.create().useSynchronousQueue().build();\n\t}\n\n\t/**\n\t * 关闭公共线程池\n\t *\n\t * @param isNow 是否立即关闭而不等待正在执行的线程\n\t */\n\tsynchronized public static void shutdown(boolean isNow) {\n\t\tif (null != executor) {\n\t\t\tif (isNow) {\n\t\t\t\texecutor.shutdownNow();\n\t\t\t} else {\n\t\t\t\texecutor.shutdown();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获得 {@link ExecutorService}\n\t *\n\t * @return {@link ExecutorService}\n\t */\n\tpublic static ExecutorService getExecutor() {\n\t\treturn executor;\n\t}\n\n\t/**\n\t * 直接在公共线程池中执行线程\n\t *\n\t * @param runnable 可运行对象\n\t */\n\tpublic static void execute(Runnable runnable) {\n\t\ttry {\n\t\t\texecutor.execute(runnable);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Exception when running task!\");\n\t\t}\n\t}\n\n\t/**\n\t * 执行有返回值的异步方法<br>\n\t * Future代表一个异步执行的操作，通过get()方法可以获得操作的结果，如果异步操作还没有完成，则，get()会使当前线程阻塞\n\t *\n\t * @param <T> 执行的Task\n\t * @param task {@link Callable}\n\t * @return Future\n\t */\n\tpublic static <T> Future<T> submit(Callable<T> task) {\n\t\treturn executor.submit(task);\n\t}\n\n\t/**\n\t * 执行有返回值的异步方法<br>\n\t * Future代表一个异步执行的操作，通过get()方法可以获得操作的结果，如果异步操作还没有完成，则，get()会使当前线程阻塞\n\t *\n\t * @param runnable 可运行对象\n\t * @return {@link Future}\n\t * @since 3.0.5\n\t */\n\tpublic static Future<?> submit(Runnable runnable) {\n\t\treturn executor.submit(runnable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.lang.Thread.UncaughtExceptionHandler;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 线程创建工厂类，此工厂可选配置：\n *\n * <pre>\n * 1. 自定义线程命名前缀\n * 2. 自定义是否守护线程\n * </pre>\n *\n * @author looly\n * @since 4.0.0\n */\npublic class NamedThreadFactory implements ThreadFactory {\n\n\t/** 命名前缀 */\n\tprivate final String prefix;\n\t/** 线程组 */\n\tprivate final ThreadGroup group;\n\t/** 线程组 */\n\tprivate final AtomicInteger threadNumber = new AtomicInteger(1);\n\t/** 是否守护线程 */\n\tprivate final boolean isDaemon;\n\t/** 无法捕获的异常统一处理 */\n\tprivate final UncaughtExceptionHandler handler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param prefix 线程名前缀\n\t * @param isDaemon 是否守护线程\n\t */\n\tpublic NamedThreadFactory(String prefix, boolean isDaemon) {\n\t\tthis(prefix, null, isDaemon);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param prefix 线程名前缀\n\t * @param threadGroup 线程组，可以为null\n\t * @param isDaemon 是否守护线程\n\t */\n\tpublic NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon) {\n\t\tthis(prefix, threadGroup, isDaemon, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param prefix 线程名前缀\n\t * @param threadGroup 线程组，可以为null\n\t * @param isDaemon 是否守护线程\n\t * @param handler 未捕获异常处理\n\t */\n\tpublic NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon, UncaughtExceptionHandler handler) {\n\t\tthis.prefix = StrUtil.isBlank(prefix) ? \"Hutool\" : prefix;\n\t\tif (null == threadGroup) {\n\t\t\tthreadGroup = ThreadUtil.currentThreadGroup();\n\t\t}\n\t\tthis.group = threadGroup;\n\t\tthis.isDaemon = isDaemon;\n\t\tthis.handler = handler;\n\t}\n\n\t@Override\n\tpublic Thread newThread(Runnable r) {\n\t\tfinal Thread t = new Thread(this.group, r, StrUtil.format(\"{}{}\", prefix, threadNumber.getAndIncrement()));\n\n\t\t//守护线程\n\t\tif (false == t.isDaemon()) {\n\t\t\tif (isDaemon) {\n\t\t\t\t// 原线程为非守护则设置为守护\n\t\t\t\tt.setDaemon(true);\n\t\t\t}\n\t\t} else if (false == isDaemon) {\n\t\t\t// 原线程为守护则还原为非守护\n\t\t\tt.setDaemon(false);\n\t\t}\n\t\t//异常处理\n\t\tif(null != this.handler) {\n\t\t\tt.setUncaughtExceptionHandler(handler);\n\t\t}\n\t\t//优先级\n\t\tif (Thread.NORM_PRIORITY != t.getPriority()) {\n\t\t\t// 标准优先级\n\t\t\tt.setPriority(Thread.NORM_PRIORITY);\n\t\t}\n\t\treturn t;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/RecyclableBatchThreadPoolExecutor.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * 可召回批处理线程池执行器\n * <pre>\n * 1.数据分批并行处理\n * 2.主线程、线程池混合执行批处理任务，主线程空闲时会尝试召回线程池队列中的任务执行\n * 3.线程安全，可用同时执行多个任务，线程池满载时，效率与单线程模式相当，无阻塞风险，无脑提交任务即可\n * </pre>\n *\n * 适用场景：\n * <pre>\n * 1.批量处理数据且需要同步结束的场景，能一定程度上提高吞吐量、防止任务堆积 {@link #process(List, int, Function)}\n * 2.普通查询接口加速 {@link #processByWarp(Warp[])}\n * </pre>\n *\n * @author likuan\n */\npublic class RecyclableBatchThreadPoolExecutor {\n\n\tprivate final ExecutorService executor;\n\n\t/**\n\t * 构造\n\t *\n\t * @param poolSize 线程池大小\n\t */\n\tpublic RecyclableBatchThreadPoolExecutor(int poolSize){\n\t\tthis(poolSize,\"recyclable-batch-pool-\");\n\t}\n\n\t/**\n\t * 建议的构造方法\n\t * <pre>\n\t * 1.使用无界队列，主线程会召回队列中的任务执行，不会有任务堆积，无需考虑拒绝策略\n\t * 2.假如在web场景中请求量过大导致oom，不使用此工具也会有同样的结果，甚至更严重，应该对请求做限制或做其他优化\n\t * </pre>\n\t *\n\t * @param poolSize 线程池大小\n\t * @param threadPoolPrefix 线程名前缀\n\t */\n\tpublic RecyclableBatchThreadPoolExecutor(int poolSize, String threadPoolPrefix){\n\t\tAtomicInteger threadNumber = new AtomicInteger(1);\n\t\tThreadFactory threadFactory = r -> {\n\t\t\tThread t = new Thread(r, threadPoolPrefix + threadNumber.getAndIncrement());\n\t\t\tif (t.isDaemon()) {\n\t\t\t\tt.setDaemon(false);\n\t\t\t}\n\t\t\tif (t.getPriority() != Thread.NORM_PRIORITY) {\n\t\t\t\tt.setPriority(Thread.NORM_PRIORITY);\n\t\t\t}\n\t\t\treturn t;\n\t\t};\n\t\tthis.executor = new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),threadFactory);\n\t}\n\n\t/**\n\t * 自定义线程池，一般不需要使用\n\t * @param executor 线程池\n\t */\n\tpublic RecyclableBatchThreadPoolExecutor(ExecutorService executor){\n\t\tthis.executor = executor;\n\t}\n\n\t/**\n\t * 关闭线程池\n\t */\n\tpublic void shutdown(){\n\t\texecutor.shutdown();\n\t}\n\n\t/**\n\t * 获取线程池\n\t * @return ExecutorService\n\t */\n\tpublic ExecutorService getExecutor(){\n\t\treturn executor;\n\t}\n\n\t/**\n\t * 分批次处理数据\n\t * <pre>\n\t * 1.所有批次执行完成后会过滤null并返回合并结果，保持输入数据顺序，不需要结果{@link Function}返回null即可\n\t * 2.{@link Function}需自行处理异常、保证线程安全\n\t * 3.原始数据在分片后可能被外部修改，导致批次数据不一致，如有必要，传参之前进行数据拷贝\n\t * 4.主线程会参与处理批次数据，如果要异步执行任务请使用普通线程池\n\t * </pre>\n\t *\n\t * @param <T> 输入数据类型\n\t * @param <R> 输出数据类型\n\t * @param data 待处理数据集合\n\t * @param batchSize 每批次数据量\n\t * @param processor 单条数据处理函数\n\t * @return 处理结果集合\n\t */\n\tpublic <T,R> List<R> process(List<T> data, int batchSize, Function<T,R> processor) {\n\t\tif (batchSize < 1) {\n\t\t\tthrow new IllegalArgumentException(\"batchSize >= 1\");\n\t\t}\n\t\tList<List<T>> batches = splitData(data, batchSize);\n\t\tint batchCount = batches.size();\n\t\tint minusOne = batchCount - 1;\n\t\tArrayDeque<IdempotentTask<R>> taskQueue = new ArrayDeque<>(minusOne);\n\t\tMap<Integer,Future<TaskResult<R>>> futuresMap = new HashMap<>();\n\t\t// 提交前 batchCount-1 批任务\n\t\tfor (int i = 0 ; i < minusOne ; i++) {\n\t\t\tfinal int index = i;\n\t\t\tIdempotentTask<R> task = new IdempotentTask<>(i,() -> processBatch(batches.get(index), processor));\n\t\t\ttaskQueue.add(task);\n\t\t\tfuturesMap.put(i,executor.submit(task));\n\t\t}\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<R>[] resultArr = new ArrayList[batchCount];\n\t\t// 处理最后一批\n\t\tresultArr[minusOne] = processBatch(batches.get(minusOne), processor);\n\t\t// 处理剩余任务\n\t\tprocessRemainingTasks(taskQueue, futuresMap,resultArr);\n\t\t//排序、过滤null\n\t\treturn Stream.of(resultArr)\n\t\t\t\t.filter(Objects::nonNull)\n\t\t\t\t.flatMap(List::stream)\n\t\t\t\t.collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 处理剩余任务并收集结果\n\t * @param taskQueue 任务队列\n\t * @param futuresMap 异步任务映射\n\t * @param resultArr 结果存储数组\n\t */\n\tprivate <R> void processRemainingTasks(Queue<IdempotentTask<R>> taskQueue, Map<Integer,Future<TaskResult<R>>> futuresMap, List<R>[] resultArr) {\n\t\t// 主消费未执行任务\n\t\tIdempotentTask<R> task;\n\t\twhile ((task = taskQueue.poll()) != null) {\n\t\t\ttry {\n\t\t\t\tTaskResult<R> call = task.call();\n\t\t\t\tif (call.effective) {\n\t\t\t\t\t// 取消被主线程执行任务\n\t\t\t\t\tFuture<TaskResult<R>> future = futuresMap.remove(task.index);\n\t\t\t\t\tfuture.cancel(false);\n\t\t\t\t\t//加入结果集\n\t\t\t\t\tresultArr[task.index] = call.result;\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 不处理异常\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t}\n\t\tfuturesMap.forEach((index,future)->{\n\t\t\ttry {\n\t\t\t\tTaskResult<R> taskResult = future.get();\n\t\t\t\tif(taskResult.effective){\n\t\t\t\t\tresultArr[index] = taskResult.result;\n\t\t\t\t}\n\t\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 幂等任务包装类，确保任务只执行一次\n\t */\n\tprivate static class IdempotentTask<R> implements Callable<TaskResult<R>> {\n\n\t\tprivate final int index;\n\t\tprivate final Callable<List<R>> delegate;\n\t\tprivate final AtomicBoolean executed = new AtomicBoolean(false);\n\n\t\tIdempotentTask(int index,Callable<List<R>> delegate) {\n\t\t\tthis.index = index;\n\t\t\tthis.delegate = delegate;\n\t\t}\n\n\t\t@Override\n\t\tpublic TaskResult<R> call() throws Exception {\n\t\t\tif (executed.compareAndSet(false, true)) {\n\t\t\t\treturn new TaskResult<>(delegate.call(), true);\n\t\t\t}\n\t\t\treturn new TaskResult<>(null, false);\n\t\t}\n\t}\n\n\t/**\n\t * 结果包装类，标记结果有效性\n\t */\n\tprivate static class TaskResult<R>{\n\t\tprivate final List<R> result;\n\t\tprivate final boolean effective;\n\t\tTaskResult(List<R> result, boolean effective){\n\t\t\tthis.result = result;\n\t\t\tthis.effective = effective;\n\t\t}\n\t}\n\n\t/**\n\t * 数据分片方法\n\t * @param data 原始数据\n\t * @param batchSize 每批次数据量\n\t * @return 分片后的二维集合\n\t */\n\tprivate static <T> List<List<T>> splitData(List<T> data, int batchSize) {\n\t\tint batchCount = (data.size() + batchSize - 1) / batchSize;\n\t\treturn new AbstractList<List<T>>() {\n\t\t\t@Override\n\t\t\tpublic List<T> get(int index) {\n\t\t\t\tint from = index * batchSize;\n\t\t\t\tint to = Math.min((index + 1) * batchSize, data.size());\n\t\t\t\treturn data.subList(from, to);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic int size() {\n\t\t\t\treturn batchCount;\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * 单批次数据处理\n\t * @param batch 单批次数据\n\t * @param processor 处理函数\n\t * @return 处理结果\n\t */\n\tprivate static <T,R> List<R> processBatch(List<T> batch, Function<T,R> processor) {\n\t\treturn batch.stream().map(processor).filter(Objects::nonNull).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 处理Warp数组\n\t *\n\t * <pre>{@code\n\t * Warp<String> warp1 = Warp.of(this::select1);\n\t * Warp<List<String>> warp2 = Warp.of(this::select2);\n\t * executor.processByWarp(warp1, warp2);\n\t * String r1 = warp1.get();\n\t * List<String> r2 = warp2.get();\n\t * }</pre>\n\t *\n\t * @param warps Warp数组\n\t * @return Warp集合,此方法返回结果为空的不会被过滤\n\t */\n\tpublic List<Warp<?>> processByWarp(Warp<?>... warps) {\n\t\treturn processByWarp(Arrays.asList(warps));\n\t}\n\n\t/**\n\t * 处理Warp集合\n\t * @param warps Warp集合\n\t * @return Warp集合,此方法返回结果为空的不会被过滤\n\t */\n\tpublic List<Warp<?>> processByWarp(List<Warp<?>> warps) {\n\t\treturn process(warps, 1, Warp::execute);\n\t}\n\n\t/**\n\t * 处理逻辑包装类\n\t * @param <R> 结果类型\n\t */\n\tpublic static class Warp<R>{\n\n\t\tprivate Warp(Supplier<R> supplier){\n\t\t\tObjects.requireNonNull(supplier);\n\t\t\tthis.supplier = supplier;\n\t\t}\n\n\t\t/**\n\t\t * 创建Warp\n\t\t * @param supplier 执行逻辑\n\t\t * @return Warp\n\t\t * @param <R> 结果类型\n\t\t */\n\t\tpublic static <R> Warp<R> of(Supplier<R> supplier){\n\t\t\treturn new Warp<>(supplier);\n\t\t}\n\n\t\tprivate final Supplier<R> supplier;\n\t\tprivate R result;\n\n\t\t/**\n\t\t * 获取结果\n\t\t * @return 结果\n\t\t */\n\t\tpublic R get() {\n\t\t\treturn result;\n\t\t}\n\n\t\t/**\n\t\t * 执行\n\t\t * @return this\n\t\t */\n\t\tpublic Warp<R> execute() {\n\t\t\tresult = supplier.get();\n\t\t\treturn this;\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/RejectPolicy.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.concurrent.RejectedExecutionHandler;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * 线程拒绝策略枚举\n *\n * <p>\n * 如果设置了maxSize, 当总线程数达到上限, 会调用RejectedExecutionHandler进行处理，此枚举为JDK预定义的几种策略枚举表示\n *\n * @author looly\n * @since 4.1.13\n */\npublic enum RejectPolicy {\n\n\t/** 处理程序遭到拒绝将抛出RejectedExecutionException */\n\tABORT(new ThreadPoolExecutor.AbortPolicy()),\n\t/** 放弃当前任务 */\n\tDISCARD(new ThreadPoolExecutor.DiscardPolicy()),\n\t/** 如果执行程序尚未关闭，则位于工作队列头部的任务将被删除，然后重试执行程序（如果再次失败，则重复此过程） */\n\tDISCARD_OLDEST(new ThreadPoolExecutor.DiscardOldestPolicy()),\n\t/** 由主线程来直接执行 */\n\tCALLER_RUNS(new ThreadPoolExecutor.CallerRunsPolicy()),\n\t/** 当任务队列过长时处于阻塞状态，直到添加到队列中，固定并发数去访问，并且不希望丢弃任务时使用此策略 */\n\tBLOCK(new BlockPolicy());\n\n\tprivate final RejectedExecutionHandler value;\n\n\tRejectPolicy(RejectedExecutionHandler handler) {\n\t\tthis.value = handler;\n\t}\n\n\t/**\n\t * 获取RejectedExecutionHandler枚举值\n\t *\n\t * @return RejectedExecutionHandler\n\t */\n\tpublic RejectedExecutionHandler getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java",
    "content": "package cn.hutool.core.thread;\n\nimport java.util.concurrent.Semaphore;\n\n/**\n * 带有信号量控制的{@link Runnable} 接口抽象实现\n *\n * <p>\n * 通过设置信号量，可以限制可以访问某些资源（物理或逻辑的）线程数目。<br>\n * 例如：设置信号量为2，表示最多有两个线程可以同时执行方法逻辑，其余线程等待，直到此线程逻辑执行完毕\n * </p>\n *\n * @author looly\n * @since 4.4.5\n */\npublic class SemaphoreRunnable implements Runnable {\n\n\t/** 实际执行的逻辑 */\n\tprivate final Runnable runnable;\n\t/** 信号量 */\n\tprivate final Semaphore semaphore;\n\n\t/**\n\t * 构造\n\t *\n\t * @param runnable 实际执行的线程逻辑\n\t * @param semaphore 信号量，多个线程必须共享同一信号量\n\t */\n\tpublic SemaphoreRunnable(Runnable runnable, Semaphore semaphore) {\n\t\tthis.runnable = runnable;\n\t\tthis.semaphore = semaphore;\n\t}\n\n\t/**\n\t * 获得信号量\n\t *\n\t * @return {@link Semaphore}\n\t * @since 5.3.6\n\t */\n\tpublic Semaphore getSemaphore(){\n\t\treturn this.semaphore;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\tif (null != this.semaphore) {\n\t\t\ttry{\n\t\t\t\tsemaphore.acquire();\n\t\t\t\ttry {\n\t\t\t\t\tthis.runnable.run();\n\t\t\t\t} finally {\n\t\t\t\t\tsemaphore.release();\n\t\t\t\t}\n\t\t\t}catch (InterruptedException e) {\n\t\t\t\tThread.currentThread().interrupt();\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/SyncFinisher.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.exceptions.UtilException;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.LinkedHashSet;\nimport java.util.Set;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\n\n/**\n * 线程同步结束器<br>\n * 在完成一组正在其他线程中执行的操作之前，它允许一个或多个线程一直等待。\n *\n * <pre>\n * ps:\n * //模拟1000个线程并发\n * SyncFinisher sf = new SyncFinisher(1000);\n * sf.addWorker(() -&gt; {\n *      // 需要并发测试的业务代码\n * });\n * sf.start()\n * </pre>\n *\n * @author Looly\n * @since 4.1.15\n */\npublic class SyncFinisher implements Closeable {\n\n\tprivate final Set<Worker> workers;\n\tprivate final int threadSize;\n\tprivate ExecutorService executorService;\n\n\tprivate boolean isBeginAtSameTime;\n\t/**\n\t * 启动同步器，用于保证所有worker线程同时开始\n\t */\n\tprivate final CountDownLatch beginLatch;\n\t/**\n\t * 结束同步器，用于等待所有worker线程同时结束\n\t */\n\tprivate CountDownLatch endLatch;\n\t/**\n\t * 异常处理\n\t */\n\tprivate Thread.UncaughtExceptionHandler exceptionHandler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param threadSize 线程数\n\t */\n\tpublic SyncFinisher(int threadSize) {\n\t\tthis.beginLatch = new CountDownLatch(1);\n\t\tthis.threadSize = threadSize;\n\t\tthis.workers = new LinkedHashSet<>();\n\t}\n\n\t/**\n\t * 设置自定义线程池，默认为{@link ExecutorBuilder}创建的线程池\n\t *\n\t * @param executorService 线程池\n\t * @return this\n\t * @since 5.8.33\n\t */\n\tpublic SyncFinisher setExecutorService(ExecutorService executorService) {\n\t\tthis.executorService = executorService;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否所有worker线程同时开始\n\t *\n\t * @param isBeginAtSameTime 是否所有worker线程同时开始\n\t * @return this\n\t */\n\tpublic SyncFinisher setBeginAtSameTime(boolean isBeginAtSameTime) {\n\t\tthis.isBeginAtSameTime = isBeginAtSameTime;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置异常处理\n\t *\n\t * @param exceptionHandler 异常处理器\n\t * @return this\n\t * @since 5.8.19\n\t */\n\tpublic SyncFinisher setExceptionHandler(final Thread.UncaughtExceptionHandler exceptionHandler) {\n\t\tthis.exceptionHandler = exceptionHandler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加定义的线程数同等数量的worker\n\t *\n\t * @param runnable 工作线程\n\t * @return this\n\t */\n\tpublic SyncFinisher addRepeatWorker(final Runnable runnable) {\n\t\tfor (int i = 0; i < this.threadSize; i++) {\n\t\t\taddWorker(new Worker() {\n\t\t\t\t@Override\n\t\t\t\tpublic void work() {\n\t\t\t\t\trunnable.run();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加工作线程\n\t *\n\t * @param runnable 工作线程\n\t * @return this\n\t */\n\tpublic SyncFinisher addWorker(final Runnable runnable) {\n\t\treturn addWorker(new Worker() {\n\t\t\t@Override\n\t\t\tpublic void work() {\n\t\t\t\trunnable.run();\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 增加工作线程\n\t *\n\t * @param worker 工作线程\n\t * @return this\n\t */\n\tsynchronized public SyncFinisher addWorker(Worker worker) {\n\t\tworkers.add(worker);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 开始工作<br>\n\t * 执行此方法后如果不再重复使用此对象，需调用{@link #stop()}关闭回收资源。\n\t */\n\tpublic void start() {\n\t\tstart(true);\n\t}\n\n\t/**\n\t * 开始工作<br>\n\t * 执行此方法后如果不再重复使用此对象，需调用{@link #stop()}关闭回收资源。\n\t *\n\t * @param sync 是否阻塞等待\n\t * @since 4.5.8\n\t */\n\tpublic void start(boolean sync) {\n\t\tendLatch = new CountDownLatch(workers.size());\n\n\t\tif (null == this.executorService || this.executorService.isShutdown()) {\n\t\t\tthis.executorService = buildExecutor();\n\t\t}\n\t\tfor (Worker worker : workers) {\n\t\t\tif(null != this.exceptionHandler){\n\t\t\t\texecutorService.execute(worker);\n\t\t\t} else{\n\t\t\t\texecutorService.submit(worker);\n\t\t\t}\n\t\t}\n\t\t// 保证所有worker同时开始\n\t\tthis.beginLatch.countDown();\n\n\t\tif (sync) {\n\t\t\ttry {\n\t\t\t\tthis.endLatch.await();\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 结束线程池。此方法执行两种情况：\n\t * <ol>\n\t *     <li>执行start(true)后，调用此方法结束线程池回收资源</li>\n\t *     <li>执行start(false)后，用户自行判断结束点执行此方法</li>\n\t * </ol>\n\t *\n\t * @since 5.6.6\n\t */\n\tpublic void stop() {\n\t\tif (null != this.executorService) {\n\t\t\tthis.executorService.shutdown();\n\t\t\tthis.executorService = null;\n\t\t}\n\n\t\tclearWorker();\n\t}\n\n\t/**\n\t * 立即结束线程池所有线程。此方法执行两种情况：\n\t * <ol>\n\t *     <li>执行start(true)后，调用此方法结束线程池回收资源</li>\n\t *     <li>执行start(false)后，用户自行判断结束点执行此方法</li>\n\t * </ol>\n\t *\n\t * @since 5.8.11\n\t */\n\tpublic void stopNow() {\n\t\tif (null != this.executorService) {\n\t\t\tthis.executorService.shutdownNow();\n\t\t\tthis.executorService = null;\n\t\t}\n\n\t\tclearWorker();\n\t}\n\n\t/**\n\t * 清空工作线程对象\n\t */\n\tpublic void clearWorker() {\n\t\tworkers.clear();\n\t}\n\n\t/**\n\t * 剩余任务数\n\t *\n\t * @return 剩余任务数\n\t */\n\tpublic long count() {\n\t\treturn endLatch.getCount();\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tstop();\n\t}\n\n\t/**\n\t * 工作者，为一个线程\n\t *\n\t * @author xiaoleilu\n\t */\n\tpublic abstract class Worker implements Runnable {\n\n\t\t@Override\n\t\tpublic void run() {\n\t\t\tif (isBeginAtSameTime) {\n\t\t\t\ttry {\n\t\t\t\t\tbeginLatch.await();\n\t\t\t\t} catch (InterruptedException e) {\n\t\t\t\t\tthrow new UtilException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttry {\n\t\t\t\twork();\n\t\t\t} finally {\n\t\t\t\tendLatch.countDown();\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * 任务内容\n\t\t */\n\t\tpublic abstract void work();\n\t}\n\n\t/**\n\t * 构建线程池，加入了自定义的异常处理\n\t *\n\t * @return {@link ExecutorService}\n\t */\n\tprivate ExecutorService buildExecutor() {\n\t\treturn ExecutorBuilder.create()\n\t\t\t.setCorePoolSize(threadSize)\n\t\t\t.setThreadFactory(new NamedThreadFactory(\"hutool-\", null, false, exceptionHandler))\n\t\t\t.build();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/ThreadException.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 工具类异常\n *\n * @author looly\n * @since 5.7.17\n */\npublic class ThreadException extends RuntimeException {\n\tprivate static final long serialVersionUID = 5253124428623713216L;\n\n\tpublic ThreadException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic ThreadException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic ThreadException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ThreadException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic ThreadException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic ThreadException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.lang.Thread.UncaughtExceptionHandler;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicLong;\n\n/**\n * ThreadFactory创建器<br>\n * 参考：Guava的ThreadFactoryBuilder\n *\n * @author looly\n * @since 4.1.9\n */\npublic class ThreadFactoryBuilder implements Builder<ThreadFactory> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 用于线程创建的线程工厂类\n\t */\n\tprivate ThreadFactory backingThreadFactory;\n\t/**\n\t * 线程名的前缀\n\t */\n\tprivate String namePrefix;\n\t/**\n\t * 是否守护线程，默认false\n\t */\n\tprivate Boolean daemon;\n\t/**\n\t * 线程优先级\n\t */\n\tprivate Integer priority;\n\t/**\n\t * 未捕获异常处理器\n\t */\n\tprivate UncaughtExceptionHandler uncaughtExceptionHandler;\n\n\t/**\n\t * 创建{@code ThreadFactoryBuilder}\n\t *\n\t * @return {@code ThreadFactoryBuilder}\n\t */\n\tpublic static ThreadFactoryBuilder create() {\n\t\treturn new ThreadFactoryBuilder();\n\t}\n\n\t/**\n\t * 设置用于创建基础线程的线程工厂\n\t *\n\t * @param backingThreadFactory 用于创建基础线程的线程工厂\n\t * @return this\n\t */\n\tpublic ThreadFactoryBuilder setThreadFactory(ThreadFactory backingThreadFactory) {\n\t\tthis.backingThreadFactory = backingThreadFactory;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置线程名前缀，例如设置前缀为hutool-thread-，则线程名为hutool-thread-1之类。\n\t *\n\t * @param namePrefix 线程名前缀\n\t * @return this\n\t */\n\tpublic ThreadFactoryBuilder setNamePrefix(String namePrefix) {\n\t\tthis.namePrefix = namePrefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否守护线程\n\t *\n\t * @param daemon 是否守护线程\n\t * @return this\n\t */\n\tpublic ThreadFactoryBuilder setDaemon(boolean daemon) {\n\t\tthis.daemon = daemon;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置线程优先级\n\t *\n\t * @param priority 优先级\n\t * @return this\n\t * @see Thread#MIN_PRIORITY\n\t * @see Thread#NORM_PRIORITY\n\t * @see Thread#MAX_PRIORITY\n\t */\n\tpublic ThreadFactoryBuilder setPriority(int priority) {\n\t\tif (priority < Thread.MIN_PRIORITY) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Thread priority ({}) must be >= {}\", priority, Thread.MIN_PRIORITY));\n\t\t}\n\t\tif (priority > Thread.MAX_PRIORITY) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Thread priority ({}) must be <= {}\", priority, Thread.MAX_PRIORITY));\n\t\t}\n\t\tthis.priority = priority;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置未捕获异常的处理方式\n\t *\n\t * @param uncaughtExceptionHandler {@link UncaughtExceptionHandler}\n\t * @return this\n\t */\n\tpublic ThreadFactoryBuilder setUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) {\n\t\tthis.uncaughtExceptionHandler = uncaughtExceptionHandler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建{@link ThreadFactory}\n\t *\n\t * @return {@link ThreadFactory}\n\t */\n\t@Override\n\tpublic ThreadFactory build() {\n\t\treturn build(this);\n\t}\n\n\t/**\n\t * 构建\n\t *\n\t * @param builder {@code ThreadFactoryBuilder}\n\t * @return {@link ThreadFactory}\n\t */\n\tprivate static ThreadFactory build(ThreadFactoryBuilder builder) {\n\t\tfinal ThreadFactory backingThreadFactory = (null != builder.backingThreadFactory)//\n\t\t\t\t? builder.backingThreadFactory //\n\t\t\t\t: Executors.defaultThreadFactory();\n\t\tfinal String namePrefix = builder.namePrefix;\n\t\tfinal Boolean daemon = builder.daemon;\n\t\tfinal Integer priority = builder.priority;\n\t\tfinal UncaughtExceptionHandler handler = builder.uncaughtExceptionHandler;\n\t\tfinal AtomicLong count = (null == namePrefix) ? null : new AtomicLong();\n\t\treturn r -> {\n\t\t\tfinal Thread thread = backingThreadFactory.newThread(r);\n\t\t\tif (null != namePrefix) {\n\t\t\t\tthread.setName(namePrefix + count.getAndIncrement());\n\t\t\t}\n\t\t\tif (null != daemon) {\n\t\t\t\tthread.setDaemon(daemon);\n\t\t\t}\n\t\t\tif (null != priority) {\n\t\t\t\tthread.setPriority(priority);\n\t\t\t}\n\t\t\tif (null != handler) {\n\t\t\t\tthread.setUncaughtExceptionHandler(handler);\n\t\t\t}\n\t\t\treturn thread;\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.RuntimeUtil;\n\nimport java.io.IOException;\nimport java.lang.Thread.UncaughtExceptionHandler;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.CompletionService;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorCompletionService;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.RejectedExecutionHandler;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Supplier;\n\n/**\n * 线程池工具\n *\n * @author luxiaolei\n */\npublic class ThreadUtil {\n\t/** 主线程名称 **/\n\tpublic static final String MAIN_THREAD_NAME = \"main\";\n\n\t/**\n\t * 新建一个线程池，默认的策略如下：\n\t * <pre>\n\t *    1. 初始线程数为corePoolSize指定的大小\n\t *    2. 没有最大线程数限制\n\t *    3. 默认使用LinkedBlockingQueue，默认队列大小为1024\n\t * </pre>\n\t *\n\t * @param corePoolSize 同时执行的线程数大小\n\t * @return ExecutorService\n\t */\n\tpublic static ThreadPoolExecutor newExecutor(int corePoolSize) {\n\t\tExecutorBuilder builder = ExecutorBuilder.create();\n\t\tif (corePoolSize > 0) {\n\t\t\tbuilder.setCorePoolSize(corePoolSize);\n\t\t}\n\t\treturn builder.build();\n\t}\n\n\t/**\n\t * 获得一个新的线程池，默认的策略如下：\n\t * <pre>\n\t *    1. 初始线程数为 0\n\t *    2. 最大线程数为Integer.MAX_VALUE\n\t *    3. 使用SynchronousQueue\n\t *    4. 任务直接提交给线程而不保持它们\n\t * </pre>\n\t *\n\t * @return ExecutorService\n\t */\n\tpublic static ThreadPoolExecutor newExecutor() {\n\t\treturn ExecutorBuilder.create().useSynchronousQueue().build();\n\t}\n\n\t/**\n\t * 获得一个新的线程池，只有单个线程，策略如下：\n\t * <pre>\n\t *    1. 初始线程数为 1\n\t *    2. 最大线程数为 1\n\t *    3. 默认使用LinkedBlockingQueue，默认队列大小为1024\n\t *    4. 同时只允许一个线程工作，剩余放入队列等待，等待数超过1024报错\n\t * </pre>\n\t *\n\t * @return ExecutorService\n\t */\n\tpublic static ExecutorService newSingleExecutor() {\n\t\treturn ExecutorBuilder.create()//\n\t\t\t\t.setCorePoolSize(1)//\n\t\t\t\t.setMaxPoolSize(1)//\n\t\t\t\t.setKeepAliveTime(0)//\n\t\t\t\t.buildFinalizable();\n\t}\n\n\t/**\n\t * 获得一个新的线程池<br>\n\t * 如果maximumPoolSize &gt;= corePoolSize，在没有新任务加入的情况下，多出的线程将最多保留60s\n\t *\n\t * @param corePoolSize    初始线程池大小\n\t * @param maximumPoolSize 最大线程池大小\n\t * @return {@link ThreadPoolExecutor}\n\t */\n\tpublic static ThreadPoolExecutor newExecutor(int corePoolSize, int maximumPoolSize) {\n\t\treturn ExecutorBuilder.create()\n\t\t\t\t.setCorePoolSize(corePoolSize)\n\t\t\t\t.setMaxPoolSize(maximumPoolSize)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 获得一个新的线程池，并指定最大任务队列大小<br>\n\t * 如果maximumPoolSize &gt;= corePoolSize，在没有新任务加入的情况下，多出的线程将最多保留60s\n\t *\n\t * @param corePoolSize     初始线程池大小\n\t * @param maximumPoolSize  最大线程池大小\n\t * @param maximumQueueSize 最大任务队列大小\n\t * @return {@link ThreadPoolExecutor}\n\t * @since 5.4.1\n\t */\n\tpublic static ThreadPoolExecutor newExecutor(int corePoolSize, int maximumPoolSize, int maximumQueueSize) {\n\t\treturn ExecutorBuilder.create()\n\t\t\t\t.setCorePoolSize(corePoolSize)\n\t\t\t\t.setMaxPoolSize(maximumPoolSize)\n\t\t\t\t.setWorkQueue(new LinkedBlockingQueue<>(maximumQueueSize))\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 获得一个新的线程池<br>\n\t * 传入阻塞系数，线程池的大小计算公式为：CPU可用核心数 / (1 - 阻塞因子)<br>\n\t * Blocking Coefficient(阻塞系数) = 阻塞时间／（阻塞时间+使用CPU的时间）<br>\n\t * 计算密集型任务的阻塞系数为0，而IO密集型任务的阻塞系数则接近于1。\n\t * <p>\n\t * see: <a href=\"http://blog.csdn.net/partner4java/article/details/9417663\">http://blog.csdn.net/partner4java/article/details/9417663</a>\n\t *\n\t * @param blockingCoefficient 阻塞系数，阻塞因子介于0~1之间的数，阻塞因子越大，线程池中的线程数越多。\n\t * @return {@link ThreadPoolExecutor}\n\t * @since 3.0.6\n\t */\n\tpublic static ThreadPoolExecutor newExecutorByBlockingCoefficient(float blockingCoefficient) {\n\t\tif (blockingCoefficient >= 1 || blockingCoefficient < 0) {\n\t\t\tthrow new IllegalArgumentException(\"[blockingCoefficient] must between 0 and 1, or equals 0.\");\n\t\t}\n\n\t\t// 最佳的线程数 = CPU可用核心数 / (1 - 阻塞系数)\n\t\tint poolSize = (int) (RuntimeUtil.getProcessorCount() / (1 - blockingCoefficient));\n\t\treturn ExecutorBuilder.create().setCorePoolSize(poolSize).setMaxPoolSize(poolSize).setKeepAliveTime(0L).build();\n\t}\n\n\t/**\n\t * 获取一个新的线程池，默认的策略如下<br>\n\t * <pre>\n\t *     1. 核心线程数与最大线程数为nThreads指定的大小\n\t *     2. 默认使用LinkedBlockingQueue，默认队列大小为1024\n\t *     3. 如果isBlocked为{@code true}，当执行拒绝策略的时候会处于阻塞状态，直到能添加到队列中或者被{@link Thread#interrupt()}中断\n\t * </pre>\n\t *\n\t * @param nThreads         线程池大小\n\t * @param threadNamePrefix 线程名称前缀\n\t * @param isBlocked        是否使用{@link BlockPolicy}策略\n\t * @return ExecutorService\n\t * @author luozongle\n\t * @since 5.8.0\n\t */\n\tpublic static ThreadPoolExecutor newFixedExecutor(int nThreads, String threadNamePrefix, boolean isBlocked) {\n\t\treturn newFixedExecutor(nThreads, 1024, threadNamePrefix, isBlocked);\n\t}\n\n\t/**\n\t * 获取一个新的线程池，默认的策略如下<br>\n\t * <pre>\n\t *     1. 核心线程数与最大线程数为nThreads指定的大小\n\t *     2. 默认使用LinkedBlockingQueue\n\t *     3. 如果isBlocked为{@code true}，当执行拒绝策略的时候会处于阻塞状态，直到能添加到队列中或者被{@link Thread#interrupt()}中断\n\t * </pre>\n\t *\n\t * @param nThreads         线程池大小\n\t * @param maximumQueueSize 队列大小\n\t * @param threadNamePrefix 线程名称前缀\n\t * @param isBlocked        是否使用{@link BlockPolicy}策略\n\t * @return ExecutorService\n\t * @author luozongle\n\t * @since 5.8.0\n\t */\n\tpublic static ThreadPoolExecutor newFixedExecutor(int nThreads, int maximumQueueSize, String threadNamePrefix, boolean isBlocked) {\n\t\treturn newFixedExecutor(nThreads, maximumQueueSize, threadNamePrefix,\n\t\t\t\t(isBlocked ? RejectPolicy.BLOCK : RejectPolicy.ABORT).getValue());\n\t}\n\n\t/**\n\t * 获得一个新的线程池，默认策略如下<br>\n\t * <pre>\n\t *     1. 核心线程数与最大线程数为nThreads指定的大小\n\t *     2. 默认使用LinkedBlockingQueue\n\t * </pre>\n\t *\n\t * @param nThreads         线程池大小\n\t * @param maximumQueueSize 队列大小\n\t * @param threadNamePrefix 线程名称前缀\n\t * @param handler          拒绝策略\n\t * @return ExecutorService\n\t * @author luozongle\n\t * @since 5.8.0\n\t */\n\tpublic static ThreadPoolExecutor newFixedExecutor(int nThreads,\n\t\t\t\t\t\t\t\t\t\t\t\t   int maximumQueueSize,\n\t\t\t\t\t\t\t\t\t\t\t\t   String threadNamePrefix,\n\t\t\t\t\t\t\t\t\t\t\t\t   RejectedExecutionHandler handler) {\n\t\treturn ExecutorBuilder.create()\n\t\t\t\t.setCorePoolSize(nThreads).setMaxPoolSize(nThreads)\n\t\t\t\t.setWorkQueue(new LinkedBlockingQueue<>(maximumQueueSize))\n\t\t\t\t.setThreadFactory(createThreadFactory(threadNamePrefix))\n\t\t\t\t.setHandler(handler)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 直接在公共线程池中执行线程\n\t *\n\t * @param runnable 可运行对象\n\t */\n\tpublic static void execute(Runnable runnable) {\n\t\tGlobalThreadPool.execute(runnable);\n\t}\n\n\t/**\n\t * 执行异步方法\n\t *\n\t * @param runnable 需要执行的方法体\n\t * @param isDaemon 是否守护线程。守护线程会在主线程结束后自动结束\n\t * @return 执行的方法体\n\t */\n\tpublic static Runnable execAsync(Runnable runnable, boolean isDaemon) {\n\t\tThread thread = new Thread(runnable);\n\t\tthread.setDaemon(isDaemon);\n\t\tthread.start();\n\n\t\treturn runnable;\n\t}\n\n\t/**\n\t * 执行有返回值的异步方法<br>\n\t * Future代表一个异步执行的操作，通过get()方法可以获得操作的结果，如果异步操作还没有完成，则，get()会使当前线程阻塞\n\t *\n\t * @param <T>  回调对象类型\n\t * @param task {@link Callable}\n\t * @return Future\n\t */\n\tpublic static <T> Future<T> execAsync(Callable<T> task) {\n\t\treturn GlobalThreadPool.submit(task);\n\t}\n\n\t/**\n\t * 执行有返回值的异步方法<br>\n\t * Future代表一个异步执行的操作，通过get()方法可以获得操作的结果，如果异步操作还没有完成，则，get()会使当前线程阻塞\n\t *\n\t * @param runnable 可运行对象\n\t * @return {@link Future}\n\t * @since 3.0.5\n\t */\n\tpublic static Future<?> execAsync(Runnable runnable) {\n\t\treturn GlobalThreadPool.submit(runnable);\n\t}\n\n\t/**\n\t * 新建一个CompletionService，调用其submit方法可以异步执行多个任务，最后调用take方法按照完成的顺序获得其结果。<br>\n\t * 若未完成，则会阻塞\n\t *\n\t * @param <T> 回调对象类型\n\t * @return CompletionService\n\t */\n\tpublic static <T> CompletionService<T> newCompletionService() {\n\t\treturn new ExecutorCompletionService<>(GlobalThreadPool.getExecutor());\n\t}\n\n\t/**\n\t * 新建一个CompletionService，调用其submit方法可以异步执行多个任务，最后调用take方法按照完成的顺序获得其结果。<br>\n\t * 若未完成，则会阻塞\n\t *\n\t * @param <T>      回调对象类型\n\t * @param executor 执行器 {@link ExecutorService}\n\t * @return CompletionService\n\t */\n\tpublic static <T> CompletionService<T> newCompletionService(ExecutorService executor) {\n\t\treturn new ExecutorCompletionService<>(executor);\n\t}\n\n\t/**\n\t * 新建一个CountDownLatch，一个同步辅助类，在完成一组正在其他线程中执行的操作之前，它允许一个或多个线程一直等待。\n\t *\n\t * @param threadCount 线程数量\n\t * @return CountDownLatch\n\t */\n\tpublic static CountDownLatch newCountDownLatch(int threadCount) {\n\t\treturn new CountDownLatch(threadCount);\n\t}\n\n\t/**\n\t * 创建新线程，非守护线程，正常优先级，线程组与当前线程的线程组一致\n\t *\n\t * @param runnable {@link Runnable}\n\t * @param name     线程名\n\t * @return {@link Thread}\n\t * @since 3.1.2\n\t */\n\tpublic static Thread newThread(Runnable runnable, String name) {\n\t\tfinal Thread t = newThread(runnable, name, false);\n\t\tif (t.getPriority() != Thread.NORM_PRIORITY) {\n\t\t\tt.setPriority(Thread.NORM_PRIORITY);\n\t\t}\n\t\treturn t;\n\t}\n\n\t/**\n\t * 创建新线程\n\t *\n\t * @param runnable {@link Runnable}\n\t * @param name     线程名\n\t * @param isDaemon 是否守护线程\n\t * @return {@link Thread}\n\t * @since 4.1.2\n\t */\n\tpublic static Thread newThread(Runnable runnable, String name, boolean isDaemon) {\n\t\tfinal Thread t = new Thread(null, runnable, name);\n\t\tt.setDaemon(isDaemon);\n\t\treturn t;\n\t}\n\n\t/**\n\t * 挂起当前线程\n\t *\n\t * @param timeout  挂起的时长\n\t * @param timeUnit 时长单位\n\t * @return 被中断返回false，否则true\n\t */\n\tpublic static boolean sleep(Number timeout, TimeUnit timeUnit) {\n\t\ttry {\n\t\t\ttimeUnit.sleep(timeout.longValue());\n\t\t} catch (InterruptedException e) {\n            // 重新标记线程为中断状态（恢复中断信息），让后续代码能感知到“线程曾被中断过”\n            Thread.currentThread().interrupt();\n            return false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 挂起当前线程\n\t *\n\t * @param millis 挂起的毫秒数\n\t * @return 被中断返回false，否则true\n\t */\n\tpublic static boolean sleep(Number millis) {\n\t\tif (millis == null) {\n\t\t\treturn true;\n\t\t}\n\t\treturn sleep(millis.longValue());\n\t}\n\n\t/**\n\t * 挂起当前线程\n\t *\n\t * @param millis 挂起的毫秒数\n\t * @return 被中断返回false，否则true\n\t * @since 5.3.2\n\t */\n\tpublic static boolean sleep(long millis) {\n\t\tif (millis > 0) {\n\t\t\ttry {\n\t\t\t\tThread.sleep(millis);\n\t\t\t} catch (InterruptedException e) {\n                // 重新标记线程为中断状态（恢复中断信息），让后续代码能感知到“线程曾被中断过”\n                Thread.currentThread().interrupt();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 考虑{@link Thread#sleep(long)}方法有可能时间不足给定毫秒数，此方法保证sleep时间不小于给定的毫秒数\n\t *\n\t * @param millis 给定的sleep时间\n\t * @return 被中断返回false，否则true\n\t * @see ThreadUtil#sleep(Number)\n\t */\n\tpublic static boolean safeSleep(Number millis) {\n\t\tif (millis == null) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn safeSleep(millis.longValue());\n\t}\n\n\t/**\n\t * 考虑{@link Thread#sleep(long)}方法有可能时间不足给定毫秒数，此方法保证sleep时间不小于给定的毫秒数\n\t *\n\t * @param millis 给定的sleep时间\n\t * @return 被中断返回false，否则true\n\t * @see ThreadUtil#sleep(Number)\n\t * @since 5.3.2\n\t */\n\tpublic static boolean safeSleep(long millis) {\n\t\tlong done = 0;\n\t\tlong before;\n\t\t// done表示实际花费的时间，确保实际花费时间大于应该sleep的时间\n\t\twhile (done < millis) {\n\t\t\tbefore = System.nanoTime();\n\t\t\tif (!sleep(millis - done)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// done始终为正\n\t\t\tdone += (System.nanoTime() - before) / 1_000_000;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * @return 获得堆栈列表\n\t */\n\tpublic static StackTraceElement[] getStackTrace() {\n\t\treturn Thread.currentThread().getStackTrace();\n\t}\n\n\t/**\n\t * 获得堆栈项\n\t *\n\t * @param i 第几个堆栈项\n\t * @return 堆栈项\n\t */\n\tpublic static StackTraceElement getStackTraceElement(int i) {\n\t\tStackTraceElement[] stackTrace = getStackTrace();\n\t\tif (i < 0) {\n\t\t\ti += stackTrace.length;\n\t\t}\n\t\treturn stackTrace[i];\n\t}\n\n\t/**\n\t * 创建本地线程对象\n\t *\n\t * @param <T>           持有对象类型\n\t * @param isInheritable 是否为子线程提供从父线程那里继承的值\n\t * @return 本地线程\n\t */\n\tpublic static <T> ThreadLocal<T> createThreadLocal(boolean isInheritable) {\n\t\tif (isInheritable) {\n\t\t\treturn new InheritableThreadLocal<>();\n\t\t} else {\n\t\t\treturn new ThreadLocal<>();\n\t\t}\n\t}\n\n\t/**\n\t * 创建本地线程对象\n\t *\n\t * @param <T>      持有对象类型\n\t * @param supplier 初始化线程对象函数\n\t * @return 本地线程\n\t * @see ThreadLocal#withInitial(Supplier)\n\t * @since 5.6.7\n\t */\n\tpublic static <T> ThreadLocal<T> createThreadLocal(Supplier<? extends T> supplier) {\n\t\treturn ThreadLocal.withInitial(supplier);\n\t}\n\n\t/**\n\t * 创建ThreadFactoryBuilder\n\t *\n\t * @return ThreadFactoryBuilder\n\t * @see ThreadFactoryBuilder#build()\n\t * @since 4.1.13\n\t */\n\tpublic static ThreadFactoryBuilder createThreadFactoryBuilder() {\n\t\treturn ThreadFactoryBuilder.create();\n\t}\n\n\t/**\n\t * 创建自定义线程名称前缀的{@link ThreadFactory}\n\t *\n\t * @param threadNamePrefix 线程名称前缀\n\t * @return {@link ThreadFactory}\n\t * @see ThreadFactoryBuilder#build()\n\t * @since 5.8.0\n\t */\n\tpublic static ThreadFactory createThreadFactory(String threadNamePrefix) {\n\t\treturn ThreadFactoryBuilder.create().setNamePrefix(threadNamePrefix).build();\n\t}\n\n\t/**\n\t * 结束线程，调用此方法后，线程将抛出 {@link InterruptedException}异常\n\t *\n\t * @param thread 线程\n\t * @param isJoin 是否等待结束\n\t */\n\tpublic static void interrupt(Thread thread, boolean isJoin) {\n\t\tif (null != thread && false == thread.isInterrupted()) {\n\t\t\tthread.interrupt();\n\t\t\tif (isJoin) {\n\t\t\t\twaitForDie(thread);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 等待当前线程结束. 调用 {@link Thread#join()} 并忽略 {@link InterruptedException}\n\t */\n\tpublic static void waitForDie() {\n\t\twaitForDie(Thread.currentThread());\n\t}\n\n\t/**\n\t * 等待线程结束. 调用 {@link Thread#join()} 并忽略 {@link InterruptedException}\n\t *\n\t * @param thread 线程\n\t */\n\tpublic static void waitForDie(Thread thread) {\n\t\tif (null == thread) {\n\t\t\treturn;\n\t\t}\n\n\t\tboolean dead = false;\n\t\tdo {\n\t\t\ttry {\n\t\t\t\tthread.join();\n\t\t\t\tdead = true;\n\t\t\t} catch (InterruptedException e) {\n                // 重新标记线程为中断状态（恢复中断信息），让后续代码能感知到“线程曾被中断过”\n                Thread.currentThread().interrupt();\n\t\t\t}\n\t\t} while (false == dead);\n\t}\n\n\t/**\n\t * 获取JVM中与当前线程同组的所有线程<br>\n\t *\n\t * @return 线程对象数组\n\t */\n\tpublic static Thread[] getThreads() {\n\t\treturn getThreads(Thread.currentThread().getThreadGroup().getParent());\n\t}\n\n\t/**\n\t * 获取JVM中与当前线程同组的所有线程<br>\n\t * 使用数组二次拷贝方式，防止在线程列表获取过程中线程终止<br>\n\t * from Voovan\n\t *\n\t * @param group 线程组\n\t * @return 线程对象数组\n\t */\n\tpublic static Thread[] getThreads(ThreadGroup group) {\n\t\tfinal Thread[] slackList = new Thread[group.activeCount() * 2];\n\t\tfinal int actualSize = group.enumerate(slackList);\n\t\tfinal Thread[] result = new Thread[actualSize];\n\t\tSystem.arraycopy(slackList, 0, result, 0, actualSize);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取进程的主线程<br>\n\t * from Voovan\n\t *\n\t * @return 进程的主线程\n\t */\n\tpublic static Thread getMainThread() {\n\t\tfor (Thread thread : getThreads()) {\n\t\t\tif (1 == thread.getId() || MAIN_THREAD_NAME.equals(thread.getName())) {\n\t\t\t\treturn thread;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取当前线程的线程组\n\t *\n\t * @return 线程组\n\t * @since 3.1.2\n\t */\n\tpublic static ThreadGroup currentThreadGroup() {\n\t\tfinal SecurityManager s = System.getSecurityManager();\n\t\treturn (null != s) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();\n\t}\n\n\t/**\n\t * 创建线程工厂\n\t *\n\t * @param prefix   线程名前缀\n\t * @param isDaemon 是否守护线程\n\t * @return {@link ThreadFactory}\n\t * @since 4.0.0\n\t */\n\tpublic static ThreadFactory newNamedThreadFactory(String prefix, boolean isDaemon) {\n\t\treturn new NamedThreadFactory(prefix, isDaemon);\n\t}\n\n\t/**\n\t * 创建线程工厂\n\t *\n\t * @param prefix      线程名前缀\n\t * @param threadGroup 线程组，可以为null\n\t * @param isDaemon    是否守护线程\n\t * @return {@link ThreadFactory}\n\t * @since 4.0.0\n\t */\n\tpublic static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon) {\n\t\treturn new NamedThreadFactory(prefix, threadGroup, isDaemon);\n\t}\n\n\t/**\n\t * 创建线程工厂\n\t *\n\t * @param prefix      线程名前缀\n\t * @param threadGroup 线程组，可以为null\n\t * @param isDaemon    是否守护线程\n\t * @param handler     未捕获异常处理\n\t * @return {@link ThreadFactory}\n\t * @since 4.0.0\n\t */\n\tpublic static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon, UncaughtExceptionHandler handler) {\n\t\treturn new NamedThreadFactory(prefix, threadGroup, isDaemon, handler);\n\t}\n\n\t/**\n\t * 阻塞当前线程，保证在main方法中执行不被退出\n\t *\n\t * @param obj 对象所在线程\n\t * @since 4.5.6\n\t */\n\t@SuppressWarnings(\"SynchronizationOnLocalVariableOrMethodParameter\")\n\tpublic static void sync(Object obj) {\n\t\tsynchronized (obj) {\n\t\t\ttry {\n\t\t\t\tobj.wait();\n\t\t\t} catch (InterruptedException e) {\n                // 重新标记线程为中断状态（恢复中断信息），让后续代码能感知到“线程曾被中断过”\n                Thread.currentThread().interrupt();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 并发测试<br>\n\t * 此方法用于测试多线程下执行某些逻辑的并发性能<br>\n\t * 调用此方法会导致当前线程阻塞。<br>\n\t * 结束后可调用{@link ConcurrencyTester#getInterval()} 方法获取执行时间\n\t *\n\t * @param threadSize 并发线程数\n\t * @param runnable   执行的逻辑实现\n\t * @return {@link ConcurrencyTester}\n\t * @since 4.5.8\n\t */\n\tpublic static ConcurrencyTester concurrencyTest(int threadSize, Runnable runnable) {\n        try (ConcurrencyTester tester = new ConcurrencyTester(threadSize)) {\n            return tester.test(runnable);\n        } catch (IOException e) {\n            throw new IORuntimeException(e);\n        }\n    }\n\n\t/**\n\t * 创建{@link ScheduledThreadPoolExecutor}\n\t *\n\t * @param corePoolSize 初始线程池大小\n\t * @return {@link ScheduledThreadPoolExecutor}\n\t * @since 5.5.8\n\t */\n\tpublic static ScheduledThreadPoolExecutor createScheduledExecutor(int corePoolSize) {\n\t\treturn new ScheduledThreadPoolExecutor(corePoolSize);\n\t}\n\n\t/**\n\t * 开始执行一个定时任务，执行方式分fixedRate模式和fixedDelay模式。<br>\n\t * 注意：此方法的延迟和周期的单位均为毫秒。\n\t *\n\t * <ul>\n\t *     <li>fixedRate 模式：以固定的频率执行。每period的时刻检查，如果上个任务完成，启动下个任务，否则等待上个任务结束后立即启动。</li>\n\t *     <li>fixedDelay模式：以固定的延时执行。上次任务结束后等待period再执行下个任务。</li>\n\t * </ul>\n\t *\n\t * @param executor              定时任务线程池，{@code null}新建一个默认线程池\n\t * @param command               需要定时执行的逻辑\n\t * @param initialDelay          初始延迟，单位毫秒\n\t * @param period                执行周期，单位毫秒\n\t * @param fixedRateOrFixedDelay {@code true}表示fixedRate模式，{@code false}表示fixedDelay模式\n\t * @return {@link ScheduledThreadPoolExecutor}\n\t * @since 5.5.8\n\t */\n\tpublic static ScheduledThreadPoolExecutor schedule(ScheduledThreadPoolExecutor executor,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   Runnable command,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   long initialDelay,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   long period,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   boolean fixedRateOrFixedDelay) {\n\t\treturn schedule(executor, command, initialDelay, period, TimeUnit.MILLISECONDS, fixedRateOrFixedDelay);\n\t}\n\n\t/**\n\t * 开始执行一个定时任务，执行方式分fixedRate模式和fixedDelay模式。\n\t *\n\t * <ul>\n\t *     <li>fixedRate 模式：以固定的频率执行。每period的时刻检查，如果上个任务完成，启动下个任务，否则等待上个任务结束后立即启动。</li>\n\t *     <li>fixedDelay模式：以固定的延时执行。上次任务结束后等待period再执行下个任务。</li>\n\t * </ul>\n\t *\n\t * @param executor              定时任务线程池，{@code null}新建一个默认线程池\n\t * @param command               需要定时执行的逻辑\n\t * @param initialDelay          初始延迟\n\t * @param period                执行周期\n\t * @param timeUnit              时间单位\n\t * @param fixedRateOrFixedDelay {@code true}表示fixedRate模式，{@code false}表示fixedDelay模式\n\t * @return {@link ScheduledThreadPoolExecutor}\n\t * @since 5.6.5\n\t */\n\tpublic static ScheduledThreadPoolExecutor schedule(ScheduledThreadPoolExecutor executor,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   Runnable command,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   long initialDelay,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   long period,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   TimeUnit timeUnit,\n\t\t\t\t\t\t\t\t\t\t\t\t\t   boolean fixedRateOrFixedDelay) {\n\t\tif (null == executor) {\n\t\t\texecutor = createScheduledExecutor(2);\n\t\t}\n\t\tif (fixedRateOrFixedDelay) {\n\t\t\texecutor.scheduleAtFixedRate(command, initialDelay, period, timeUnit);\n\t\t} else {\n\t\t\texecutor.scheduleWithFixedDelay(command, initialDelay, period, timeUnit);\n\t\t}\n\n\t\treturn executor;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/lock/LockUtil.java",
    "content": "package cn.hutool.core.thread.lock;\n\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.concurrent.locks.StampedLock;\n\n/**\n * 锁相关工具\n *\n * @author looly\n * @since 5.2.5\n */\npublic class LockUtil {\n\n\tprivate static final NoLock NO_LOCK = new NoLock();\n\n\t/**\n\t * 创建{@link StampedLock}锁\n\t *\n\t * @return {@link StampedLock}锁\n\t */\n\tpublic static StampedLock createStampLock() {\n\t\treturn new StampedLock();\n\t}\n\n\t/**\n\t * 创建{@link ReentrantReadWriteLock}锁\n\t *\n\t * @param fair 是否公平锁\n\t * @return {@link ReentrantReadWriteLock}锁\n\t */\n\tpublic static ReentrantReadWriteLock createReadWriteLock(boolean fair) {\n\t\treturn new ReentrantReadWriteLock(fair);\n\t}\n\n\t/**\n\t * 获取单例的无锁对象\n\t *\n\t * @return {@link NoLock}\n\t */\n\tpublic static NoLock getNoLock(){\n\t\treturn NO_LOCK;\n\t}\n\n\t/**\n\t * 创建分段锁（强引用），使用 ReentrantLock\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @return 分段锁实例\n\t */\n\tpublic static SegmentLock<Lock> createSegmentLock(int segments) {\n\t\treturn SegmentLock.lock(segments);\n\t}\n\n\t/**\n\t * 创建分段读写锁（强引用），使用 ReentrantReadWriteLock\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @return 分段读写锁实例\n\t */\n\tpublic static SegmentLock<ReadWriteLock> createSegmentReadWriteLock(int segments) {\n\t\treturn SegmentLock.readWriteLock(segments);\n\t}\n\n\t/**\n\t * 创建分段信号量（强引用）\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param permits 每个信号量的许可数\n\t * @return 分段信号量实例\n\t */\n\tpublic static SegmentLock<Semaphore> createSegmentSemaphore(int segments, int permits) {\n\t\treturn SegmentLock.semaphore(segments, permits);\n\t}\n\n\t/**\n\t * 创建弱引用分段锁，使用 ReentrantLock，懒加载\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @return 弱引用分段锁实例\n\t */\n\tpublic static SegmentLock<Lock> createLazySegmentLock(int segments) {\n\t\treturn SegmentLock.lazyWeakLock(segments);\n\t}\n\n\t/**\n\t * 根据 key 获取分段锁（强引用）\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param key 用于映射分段的 key\n\t * @return 对应的 Lock 实例\n\t */\n\tpublic static Lock getSegmentLock(int segments, Object key) {\n\t\treturn SegmentLock.lock(segments).get(key);\n\t}\n\n\t/**\n\t * 根据 key 获取分段读锁（强引用）\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param key 用于映射分段的 key\n\t * @return 对应的读锁实例\n\t */\n\tpublic static Lock getSegmentReadLock(int segments, Object key) {\n\t\treturn SegmentLock.readWriteLock(segments).get(key).readLock();\n\t}\n\n\t/**\n\t * 根据 key 获取分段写锁（强引用）\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param key 用于映射分段的 key\n\t * @return 对应的写锁实例\n\t */\n\tpublic static Lock getSegmentWriteLock(int segments, Object key) {\n\t\treturn SegmentLock.readWriteLock(segments).get(key).writeLock();\n\t}\n\n\t/**\n\t * 根据 key 获取分段信号量（强引用）\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param permits 每个信号量的许可数\n\t * @param key 用于映射分段的 key\n\t * @return 对应的 Semaphore 实例\n\t */\n\tpublic static Semaphore getSegmentSemaphore(int segments, int permits, Object key) {\n\t\treturn SegmentLock.semaphore(segments, permits).get(key);\n\t}\n\n\t/**\n\t * 根据 key 获取弱引用分段锁，懒加载\n\t *\n\t * @param segments 分段数量，必须大于 0\n\t * @param key 用于映射分段的 key\n\t * @return 对应的 Lock 实例\n\t */\n\tpublic static Lock getLazySegmentLock(int segments, Object key) {\n\t\treturn SegmentLock.lazyWeakLock(segments).get(key);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/lock/NoLock.java",
    "content": "package cn.hutool.core.thread.lock;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\n\n/**\n * 无锁实现\n *\n * @author looly\n *@since 4.3.1\n */\npublic class NoLock implements Lock{\n\n\tpublic static NoLock INSTANCE = new NoLock();\n\n\t@Override\n\tpublic void lock() {\n\t}\n\n\t@Override\n\tpublic void lockInterruptibly() {\n\t}\n\n\t@Override\n\tpublic boolean tryLock() {\n\t\treturn true;\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean tryLock(long time, TimeUnit unit) {\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic void unlock() {\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Condition newCondition() {\n\t\tthrow new UnsupportedOperationException(\"NoLock`s newCondition method is unsupported\");\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/lock/NoReadWriteLock.java",
    "content": "package cn.hutool.core.thread.lock;\n\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\n\n/**\n * 无锁的读写锁实现\n *\n * @author looly\n * @since 5.8.0\n */\npublic class NoReadWriteLock implements ReadWriteLock {\n\t@Override\n\tpublic Lock readLock() {\n\t\treturn NoLock.INSTANCE;\n\t}\n\n\t@Override\n\tpublic Lock writeLock() {\n\t\treturn NoLock.INSTANCE;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/lock/SegmentLock.java",
    "content": "package cn.hutool.core.thread.lock;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\n\nimport java.lang.ref.Reference;\nimport java.lang.ref.ReferenceQueue;\nimport java.lang.ref.WeakReference;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReferenceArray;\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.function.Supplier;\n\n/**\n * 分段锁工具类，支持 Lock、Semaphore 和 ReadWriteLock 的分段实现。\n * <p>\n * 通过将锁分成多个段（segments），不同的操作可以并发使用不同的段，避免所有线程竞争同一把锁。\n * 相等的 key 保证映射到同一段锁（如 key1.equals(key2) 时，get(key1) 和 get(key2) 返回相同对象）。\n * 但不同 key 可能因哈希冲突映射到同一段，段数越少冲突概率越高。\n * <p>\n * 支持两种实现：\n * <ul>\n *     <li>强引用：创建时初始化所有段，内存占用稳定。</li>\n *     <li>弱引用：懒加载，首次使用时创建段，未使用时可被垃圾回收，适合大量段但使用较少的场景。</li>\n * </ul>\n *\n * @param <L> 锁类型\n * @author Guava,dakuo\n * @since 5.8.38\n */\npublic abstract class SegmentLock<L> {\n\n\t/** 当段数大于此阈值时，使用 ConcurrentMap 替代大数组以节省内存（适用于懒加载场景） */\n\tprivate static final int LARGE_LAZY_CUTOFF = 1024;\n\n\tprivate SegmentLock() {}\n\n\t/**\n\t * 根据 key 获取对应的锁段，保证相同 key 返回相同对象。\n\t *\n\t * @param key 非空 key\n\t * @return 对应的锁段\n\t */\n\tpublic abstract L get(Object key);\n\n\t/**\n\t * 根据索引获取锁段，索引范围为 [0, size())。\n\t *\n\t * @param index 索引\n\t * @return 指定索引的锁段\n\t */\n\tpublic abstract L getAt(int index);\n\n\t/**\n\t * 计算 key 对应的段索引。\n\t *\n\t * @param key 非空 key\n\t * @return 段索引\n\t */\n\tabstract int indexFor(Object key);\n\n\t/**\n\t * 获取总段数。\n\t *\n\t * @return 段数\n\t */\n\tpublic abstract int size();\n\n\t/**\n\t * 批量获取多个 key 对应的锁段列表，按索引升序排列，避免死锁。\n\t *\n\t * @param keys 非空 key 集合\n\t * @return 锁段列表（可能有重复）\n\t */\n\tpublic Iterable<L> bulkGet(Iterable<?> keys) {\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<Object> result = (List<Object>) CollUtil.newArrayList(keys);\n\t\tif (CollUtil.isEmpty(result)) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\tint[] stripes = new int[result.size()];\n\t\tfor (int i = 0; i < result.size(); i++) {\n\t\t\tstripes[i] = indexFor(result.get(i));\n\t\t}\n\t\tArrays.sort(stripes);\n\t\tint previousStripe = stripes[0];\n\t\tresult.set(0, getAt(previousStripe));\n\t\tfor (int i = 1; i < result.size(); i++) {\n\t\t\tint currentStripe = stripes[i];\n\t\t\tif (currentStripe == previousStripe) {\n\t\t\t\tresult.set(i, result.get(i - 1));\n\t\t\t} else {\n\t\t\t\tresult.set(i, getAt(currentStripe));\n\t\t\t\tpreviousStripe = currentStripe;\n\t\t\t}\n\t\t}\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<L> asStripes = (List<L>) result;\n\t\treturn Collections.unmodifiableList(asStripes);\n\t}\n\n\t// 静态工厂方法\n\n\t/**\n\t * 创建强引用的分段锁，所有段在创建时初始化。\n\t *\n\t * @param stripes 段数\n\t * @param supplier 锁提供者\n\t * @param <L> 锁类型\n\t * @return 分段锁实例\n\t */\n\tpublic static <L> SegmentLock<L> custom(int stripes, Supplier<L> supplier) {\n\t\treturn new CompactSegmentLock<>(stripes, supplier);\n\t}\n\n\t/**\n\t * 创建强引用的可重入锁分段实例。\n\t *\n\t * @param stripes 段数\n\t * @return 分段锁实例\n\t */\n\tpublic static SegmentLock<Lock> lock(int stripes) {\n\t\treturn custom(stripes, PaddedLock::new);\n\t}\n\n\t/**\n\t * 创建弱引用的可重入锁分段实例，懒加载。\n\t *\n\t * @param stripes 段数\n\t * @return 分段锁实例\n\t */\n\tpublic static SegmentLock<Lock> lazyWeakLock(int stripes) {\n\t\treturn lazyWeakCustom(stripes, () -> new ReentrantLock(false));\n\t}\n\n\t/**\n\t * 创建弱引用的分段锁，懒加载。\n\t *\n\t * @param stripes 段数\n\t * @param supplier 锁提供者\n\t * @param <L> 锁类型\n\t * @return 分段锁实例\n\t */\n\tprivate static <L> SegmentLock<L> lazyWeakCustom(int stripes, Supplier<L> supplier) {\n\t\treturn stripes < LARGE_LAZY_CUTOFF\n\t\t\t? new SmallLazySegmentLock<>(stripes, supplier)\n\t\t\t: new LargeLazySegmentLock<>(stripes, supplier);\n\t}\n\n\t/**\n\t * 创建强引用的信号量分段实例。\n\t *\n\t * @param stripes 段数\n\t * @param permits 每个信号量的许可数\n\t * @return 分段信号量实例\n\t */\n\tpublic static SegmentLock<Semaphore> semaphore(int stripes, int permits) {\n\t\treturn custom(stripes, () -> new PaddedSemaphore(permits));\n\t}\n\n\t/**\n\t * 创建弱引用的信号量分段实例，懒加载。\n\t *\n\t * @param stripes 段数\n\t * @param permits 每个信号量的许可数\n\t * @return 分段信号量实例\n\t */\n\tpublic static SegmentLock<Semaphore> lazyWeakSemaphore(int stripes, int permits) {\n\t\treturn lazyWeakCustom(stripes, () -> new Semaphore(permits, false));\n\t}\n\n\t/**\n\t * 创建强引用的读写锁分段实例。\n\t *\n\t * @param stripes 段数\n\t * @return 分段读写锁实例\n\t */\n\tpublic static SegmentLock<ReadWriteLock> readWriteLock(int stripes) {\n\t\treturn custom(stripes, ReentrantReadWriteLock::new);\n\t}\n\n\t/**\n\t * 创建弱引用的读写锁分段实例，懒加载。\n\t *\n\t * @param stripes 段数\n\t * @return 分段读写锁实例\n\t */\n\tpublic static SegmentLock<ReadWriteLock> lazyWeakReadWriteLock(int stripes) {\n\t\treturn lazyWeakCustom(stripes, WeakSafeReadWriteLock::new);\n\t}\n\n\t// 内部实现类\n\n\t/**\n\t * 弱引用安全的读写锁实现，确保读锁和写锁持有对自身的强引用。\n\t */\n\tprivate static final class WeakSafeReadWriteLock implements ReadWriteLock {\n\t\tprivate final ReadWriteLock delegate;\n\n\t\tWeakSafeReadWriteLock() {\n\t\t\tthis.delegate = new ReentrantReadWriteLock();\n\t\t}\n\n\t\t@Override\n\t\tpublic Lock readLock() {\n\t\t\treturn new WeakSafeLock(delegate.readLock(), this);\n\t\t}\n\n\t\t@Override\n\t\tpublic Lock writeLock() {\n\t\t\treturn new WeakSafeLock(delegate.writeLock(), this);\n\t\t}\n\t}\n\n\t/**\n\t * 弱引用安全的锁包装类，确保持有强引用。\n\t */\n\tprivate static final class WeakSafeLock implements Lock {\n\t\tprivate final Lock delegate;\n\t\tprivate final WeakSafeReadWriteLock strongReference;\n\n\t\tWeakSafeLock(Lock delegate, WeakSafeReadWriteLock strongReference) {\n\t\t\tthis.delegate = delegate;\n\t\t\tthis.strongReference = strongReference;\n\t\t}\n\n\t\t@Override\n\t\tpublic void lock() {\n\t\t\tdelegate.lock();\n\t\t}\n\n\t\t@Override\n\t\tpublic void lockInterruptibly() throws InterruptedException {\n\t\t\tdelegate.lockInterruptibly();\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean tryLock() {\n\t\t\treturn delegate.tryLock();\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean tryLock(long time, java.util.concurrent.TimeUnit unit) throws InterruptedException {\n\t\t\treturn delegate.tryLock(time, unit);\n\t\t}\n\n\t\t@Override\n\t\tpublic void unlock() {\n\t\t\tdelegate.unlock();\n\t\t}\n\n\t\t@Override\n\t\tpublic Condition newCondition() {\n\t\t\treturn new WeakSafeCondition(delegate.newCondition(), strongReference);\n\t\t}\n\t}\n\n\t/**\n\t * 弱引用安全的条件包装类。\n\t */\n\t@SuppressWarnings(\"FieldCanBeLocal\")\n\tprivate static final class WeakSafeCondition implements Condition {\n\t\tprivate final Condition delegate;\n\n\t\t/** 防止垃圾回收 */\n\t\tprivate final WeakSafeReadWriteLock strongReference;\n\n\t\tWeakSafeCondition(Condition delegate, WeakSafeReadWriteLock strongReference) {\n\t\t\tthis.delegate = delegate;\n\t\t\tthis.strongReference = strongReference;\n\t\t}\n\n\t\t@Override\n\t\tpublic void await() throws InterruptedException {\n\t\t\tdelegate.await();\n\t\t}\n\n\t\t@Override\n\t\tpublic void awaitUninterruptibly() {\n\t\t\tdelegate.awaitUninterruptibly();\n\t\t}\n\n\t\t@Override\n\t\tpublic long awaitNanos(long nanosTimeout) throws InterruptedException {\n\t\t\treturn delegate.awaitNanos(nanosTimeout);\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean await(long time, TimeUnit unit) throws InterruptedException {\n\t\t\treturn delegate.await(time, unit);\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean awaitUntil(Date deadline) throws InterruptedException {\n\t\t\treturn delegate.awaitUntil(deadline);\n\t\t}\n\n\t\t@Override\n\t\tpublic void signal() {\n\t\t\tdelegate.signal();\n\t\t}\n\n\t\t@Override\n\t\tpublic void signalAll() {\n\t\t\tdelegate.signalAll();\n\t\t}\n\t}\n\n\t/**\n\t * 抽象基类，确保段数为 2 的幂。\n\t */\n\tprivate abstract static class PowerOfTwoSegmentLock<L> extends SegmentLock<L> {\n\t\tfinal int mask;\n\n\t\tPowerOfTwoSegmentLock(int stripes) {\n\t\t\tAssert.isTrue(stripes > 0, \"Segment count must be positive\");\n\t\t\tthis.mask = stripes > Integer.MAX_VALUE / 2 ? ALL_SET : ceilToPowerOfTwo(stripes) - 1;\n\t\t}\n\n\t\t@Override\n\t\tfinal int indexFor(Object key) {\n\t\t\tint hash = smear(key.hashCode());\n\t\t\treturn hash & mask;\n\t\t}\n\n\t\t@Override\n\t\tpublic final L get(Object key) {\n\t\t\treturn getAt(indexFor(key));\n\t\t}\n\t}\n\n\t/**\n\t * 强引用实现，使用固定数组存储段。\n\t */\n\tprivate static class CompactSegmentLock<L> extends PowerOfTwoSegmentLock<L> {\n\t\tprivate final Object[] array;\n\n\t\tCompactSegmentLock(int stripes, Supplier<L> supplier) {\n\t\t\tsuper(stripes);\n\t\t\tAssert.isTrue(stripes <= Integer.MAX_VALUE / 2, \"Segment count must be <= 2^30\");\n\t\t\tthis.array = new Object[mask + 1];\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tarray[i] = supplier.get();\n\t\t\t}\n\t\t}\n\n\t\t@SuppressWarnings(\"unchecked\")\n\t\t@Override\n\t\tpublic L getAt(int index) {\n\t\t\tif (index < 0 || index >= array.length) {\n\t\t\t\tthrow new IllegalArgumentException(\"Index \" + index + \" out of bounds for size \" + array.length);\n\t\t\t}\n\t\t\treturn (L) array[index];\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn array.length;\n\t\t}\n\t}\n\n\t/**\n\t * 小规模弱引用实现，使用 AtomicReferenceArray 存储段。\n\t */\n\tprivate static class SmallLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {\n\t\tfinal AtomicReferenceArray<ArrayReference<? extends L>> locks;\n\t\tfinal Supplier<L> supplier;\n\t\tfinal int size;\n\t\tfinal ReferenceQueue<L> queue = new ReferenceQueue<>();\n\n\t\tSmallLazySegmentLock(int stripes, Supplier<L> supplier) {\n\t\t\tsuper(stripes);\n\t\t\tthis.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;\n\t\t\tthis.locks = new AtomicReferenceArray<>(size);\n\t\t\tthis.supplier = supplier;\n\t\t}\n\n\t\t@Override\n\t\tpublic L getAt(int index) {\n\t\t\tif (size != Integer.MAX_VALUE) {\n\t\t\t\tAssert.isTrue(index >= 0 && index < size, \"Index out of bounds\");\n\t\t\t}\n\t\t\tArrayReference<? extends L> existingRef = locks.get(index);\n\t\t\tL existing = existingRef == null ? null : existingRef.get();\n\t\t\tif (existing != null) {\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tL created = supplier.get();\n\t\t\tArrayReference<L> newRef = new ArrayReference<>(created, index, queue);\n\t\t\twhile (!locks.compareAndSet(index, existingRef, newRef)) {\n\t\t\t\texistingRef = locks.get(index);\n\t\t\t\texisting = existingRef == null ? null : existingRef.get();\n\t\t\t\tif (existing != null) {\n\t\t\t\t\treturn existing;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdrainQueue();\n\t\t\treturn created;\n\t\t}\n\n\t\tprivate void drainQueue() {\n\t\t\tReference<? extends L> ref;\n\t\t\twhile ((ref = queue.poll()) != null) {\n\t\t\t\tArrayReference<? extends L> arrayRef = (ArrayReference<? extends L>) ref;\n\t\t\t\tlocks.compareAndSet(arrayRef.index, arrayRef, null);\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn size;\n\t\t}\n\n\t\tprivate static final class ArrayReference<L> extends WeakReference<L> {\n\t\t\tfinal int index;\n\n\t\t\tArrayReference(L referent, int index, ReferenceQueue<L> queue) {\n\t\t\t\tsuper(referent, queue);\n\t\t\t\tthis.index = index;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 大规模弱引用实现，使用 ConcurrentMap 存储段。\n\t */\n\tprivate static class LargeLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {\n\t\tfinal ConcurrentMap<Integer, L> locks;\n\t\tfinal Supplier<L> supplier;\n\t\tfinal int size;\n\n\t\tLargeLazySegmentLock(int stripes, Supplier<L> supplier) {\n\t\t\tsuper(stripes);\n\t\t\tthis.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;\n\t\t\tthis.locks = new ConcurrentHashMap<>();\n\t\t\tthis.supplier = supplier;\n\t\t}\n\n\t\t@Override\n\t\tpublic L getAt(int index) {\n\t\t\tif (size != Integer.MAX_VALUE) {\n\t\t\t\tAssert.isTrue(index >= 0 && index < size, \"Index out of bounds\");\n\t\t\t}\n\t\t\tL existing = locks.get(index);\n\t\t\tif (existing != null) {\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tL created = supplier.get();\n\t\t\texisting = locks.putIfAbsent(index, created);\n\t\t\treturn existing != null ? existing : created;\n\t\t}\n\n\t\t@Override\n\t\tpublic int size() {\n\t\t\treturn size;\n\t\t}\n\t}\n\n\tprivate static final int ALL_SET = ~0;\n\n\tprivate static int ceilToPowerOfTwo(int x) {\n\t\treturn 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));\n\t}\n\n\tprivate static int smear(int hashCode) {\n\t\thashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);\n\t\treturn hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);\n\t}\n\n\t/**\n\t * 填充锁，避免缓存行干扰。\n\t */\n\tprivate static class PaddedLock extends ReentrantLock {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tlong unused1;\n\t\tlong unused2;\n\t\tlong unused3;\n\n\t\tPaddedLock() {\n\t\t\tsuper(false);\n\t\t}\n\t}\n\n\t/**\n\t * 填充信号量，避免缓存行干扰。\n\t */\n\tprivate static class PaddedSemaphore extends Semaphore {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tlong unused1;\n\t\tlong unused2;\n\t\tlong unused3;\n\n\t\tPaddedSemaphore(int permits) {\n\t\t\tsuper(permits, false);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/lock/package-info.java",
    "content": "/**\n * 锁的实现\n *\n * @author looly\n *\n */\npackage cn.hutool.core.thread.lock;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/package-info.java",
    "content": "/**\n * 提供线程及高并发封装，入口为ThreadUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.core.thread;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java",
    "content": "package cn.hutool.core.thread.threadlocal;\n\n/**\n * 带有Name标识的 {@link InheritableThreadLocal}，调用toString返回name\n *\n * @param <T> 值类型\n * @author looly\n * @since 4.1.4\n */\npublic class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T> {\n\n\tprivate final String name;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 名字\n\t */\n\tpublic NamedInheritableThreadLocal(String name) {\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.name;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java",
    "content": "package cn.hutool.core.thread.threadlocal;\n\n/**\n * 带有Name标识的 {@link ThreadLocal}，调用toString返回name\n *\n * @param <T> 值类型\n * @author looly\n * @since 4.1.4\n */\npublic class NamedThreadLocal<T> extends ThreadLocal<T> {\n\n\tprivate final String name;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 名字\n\t */\n\tpublic NamedThreadLocal(String name) {\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.name;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java",
    "content": "/**\n *\n * ThreadLocal相关封装\n * @author looly\n *\n */\npackage cn.hutool.core.thread.threadlocal;"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.collection.UniqueKeySet;\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.*;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.text.StrJoiner;\n\nimport java.lang.reflect.Array;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\n/**\n * 数组工具类\n *\n * @author Looly\n */\npublic class ArrayUtil extends PrimitiveArrayUtil {\n\n\t// ---------------------------------------------------------------------- isEmpty\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static <T> boolean isEmpty(T[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 如果给定数组为空，返回默认数组\n\t *\n\t * @param <T>          数组元素类型\n\t * @param array        数组\n\t * @param defaultArray 默认数组\n\t * @return 非空（empty）的原数组或默认数组\n\t * @since 4.6.9\n\t */\n\tpublic static <T> T[] defaultIfEmpty(T[] array, T[] defaultArray) {\n\t\treturn isEmpty(array) ? defaultArray : array;\n\t}\n\n\t/**\n\t * 数组是否为空<br>\n\t * 此方法会匹配单一对象，如果此对象为{@code null}则返回true<br>\n\t * 如果此对象为非数组，理解为此对象为数组的第一个元素，则返回false<br>\n\t * 如果此对象为数组对象，数组长度大于0情况下返回false，否则返回true\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(Object array) {\n\t\tif (array != null) {\n\t\t\tif (isArray(array)) {\n\t\t\t\treturn 0 == Array.getLength(array);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t// ---------------------------------------------------------------------- isNotEmpty\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static <T> boolean isNotEmpty(T[] array) {\n\t\treturn (null != array && array.length != 0);\n\t}\n\n\t/**\n\t * 数组是否为非空<br>\n\t * 此方法会匹配单一对象，如果此对象为{@code null}则返回false<br>\n\t * 如果此对象为非数组，理解为此对象为数组的第一个元素，则返回true<br>\n\t * 如果此对象为数组对象，数组长度大于0情况下返回true，否则返回false\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(Object array) {\n\t\treturn !isEmpty(array);\n\t}\n\n\t/**\n\t * 是否包含{@code null}元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被检查的数组\n\t * @return 是否包含{@code null}元素\n\t * @since 3.0.7\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> boolean hasNull(T... array) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (T element : array) {\n\t\t\t\tif (ObjectUtil.isNull(element)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn array == null;\n\t}\n\n\t/**\n\t * 多个字段是否全为null\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被检查的数组\n\t * @return 多个字段是否全为null\n\t * @author dahuoyzs\n\t * @since 5.4.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> boolean isAllNull(T... array) {\n\t\treturn null == firstNonNull(array);\n\t}\n\n\t/**\n\t * 返回数组中第一个非空元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 非空元素，如果不存在非空元素或数组为空，返回{@code null}\n\t * @since 3.0.7\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T firstNonNull(T... array) {\n\t\treturn firstMatch(ObjectUtil::isNotNull, array);\n\t}\n\n\t/**\n\t * 返回数组中第一个匹配规则的值\n\t *\n\t * @param <T>     数组元素类型\n\t * @param matcher 匹配接口，实现此接口自定义匹配规则\n\t * @param array   数组\n\t * @return 匹配元素，如果不存在匹配元素或数组为空，返回 {@code null}\n\t * @since 3.0.7\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T firstMatch(Matcher<T> matcher, T... array) {\n\t\tfinal int index = matchIndex(matcher, array);\n\t\tif (index < 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn array[index];\n\t}\n\n\t/**\n\t * 返回数组中第一个匹配规则的值的位置\n\t *\n\t * @param <T>     数组元素类型\n\t * @param matcher 匹配接口，实现此接口自定义匹配规则\n\t * @param array   数组\n\t * @return 匹配到元素的位置，-1表示未匹配到\n\t * @since 5.6.6\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> int matchIndex(Matcher<T> matcher, T... array) {\n\t\treturn matchIndex(matcher, 0, array);\n\t}\n\n\t/**\n\t * 返回数组中第一个匹配规则的值的位置\n\t *\n\t * @param <T>               数组元素类型\n\t * @param matcher           匹配接口，实现此接口自定义匹配规则\n\t * @param beginIndexInclude 检索开始的位置\n\t * @param array             数组\n\t * @return 匹配到元素的位置，-1表示未匹配到\n\t * @since 5.7.3\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> int matchIndex(Matcher<T> matcher, int beginIndexInclude, T... array) {\n\t\tAssert.notNull(matcher, \"Matcher must be not null !\");\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = beginIndexInclude; i < array.length; i++) {\n\t\t\t\tif (matcher.match(array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 新建一个空数组\n\t *\n\t * @param <T>           数组元素类型\n\t * @param componentType 元素类型\n\t * @param newSize       大小\n\t * @return 空数组\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T[] newArray(Class<?> componentType, int newSize) {\n\t\treturn (T[]) Array.newInstance(componentType, newSize);\n\t}\n\n\t/**\n\t * 新建一个空数组\n\t *\n\t * @param newSize 大小\n\t * @return 空数组\n\t * @since 3.3.0\n\t */\n\tpublic static Object[] newArray(int newSize) {\n\t\treturn new Object[newSize];\n\t}\n\n\t/**\n\t * 获取数组对象的元素类型\n\t *\n\t * @param array 数组对象\n\t * @return 元素类型\n\t * @since 3.2.2\n\t */\n\tpublic static Class<?> getComponentType(Object array) {\n\t\treturn null == array ? null : array.getClass().getComponentType();\n\t}\n\n\t/**\n\t * 获取数组对象的元素类型\n\t *\n\t * @param arrayClass 数组类\n\t * @return 元素类型\n\t * @since 3.2.2\n\t */\n\tpublic static Class<?> getComponentType(Class<?> arrayClass) {\n\t\treturn null == arrayClass ? null : arrayClass.getComponentType();\n\t}\n\n\t/**\n\t * 根据数组元素类型，获取数组的类型<br>\n\t * 方法是通过创建一个空数组从而获取其类型\n\t *\n\t * @param componentType 数组元素类型\n\t * @return 数组类型\n\t * @since 3.2.2\n\t */\n\tpublic static Class<?> getArrayType(Class<?> componentType) {\n\t\treturn Array.newInstance(componentType, 0).getClass();\n\t}\n\n\t/**\n\t * 强转数组类型<br>\n\t * 强制转换的前提是数组元素类型可被强制转换<br>\n\t * 强制转换后会生成一个新数组\n\t *\n\t * @param type     数组类型或数组元素类型\n\t * @param arrayObj 原数组\n\t * @return 转换后的数组类型\n\t * @throws NullPointerException     提供参数为空\n\t * @throws IllegalArgumentException 参数arrayObj不是数组\n\t * @since 3.0.6\n\t */\n\tpublic static Object[] cast(Class<?> type, Object arrayObj) throws NullPointerException, IllegalArgumentException {\n\t\tif (null == arrayObj) {\n\t\t\tthrow new NullPointerException(\"Argument [arrayObj] is null !\");\n\t\t}\n\t\tif (false == arrayObj.getClass().isArray()) {\n\t\t\tthrow new IllegalArgumentException(\"Argument [arrayObj] is not array !\");\n\t\t}\n\t\tif (null == type) {\n\t\t\treturn (Object[]) arrayObj;\n\t\t}\n\n\t\tfinal Class<?> componentType = type.isArray() ? type.getComponentType() : type;\n\t\tfinal Object[] array = (Object[]) arrayObj;\n\t\tfinal Object[] result = ArrayUtil.newArray(componentType, array.length);\n\t\tSystem.arraycopy(array, 0, result, 0, array.length);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将新元素添加到已有数组中<br>\n\t * 添加新元素会生成一个新的数组，不影响原数组\n\t *\n\t * @param <T>         数组元素类型\n\t * @param buffer      已有数组\n\t * @param newElements 新元素\n\t * @return 新数组\n\t */\n\t@SafeVarargs\n\tpublic static <T> T[] append(T[] buffer, T... newElements) {\n\t\tif (isEmpty(buffer)) {\n\t\t\treturn newElements;\n\t\t}\n\t\treturn insert(buffer, buffer.length, newElements);\n\t}\n\n\t/**\n\t * 将新元素添加到已有数组中<br>\n\t * 添加新元素会生成一个新的数组，不影响原数组\n\t *\n\t * @param <T>         数组元素类型\n\t * @param array       已有数组\n\t * @param newElements 新元素\n\t * @return 新数组\n\t */\n\t@SafeVarargs\n\tpublic static <T> Object append(Object array, T... newElements) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn newElements;\n\t\t}\n\t\treturn insert(array, length(array), newElements);\n\t}\n\n\t/**\n\t * 将元素值设置为数组的某个位置，当给定的index大于数组长度，则追加\n\t *\n\t * @param <T>    数组元素类型\n\t * @param buffer 已有数组\n\t * @param index  位置，大于长度追加，否则替换\n\t * @param value  新值\n\t * @return 新数组或原有数组\n\t * @since 4.1.2\n\t */\n\tpublic static <T> T[] setOrAppend(T[] buffer, int index, T value) {\n\t\tif (index < buffer.length) {\n\t\t\tArray.set(buffer, index, value);\n\t\t\treturn buffer;\n\t\t} else {\n\t\t\tif (ArrayUtil.isEmpty(buffer)) {\n\t\t\t\t// issue#I5APJE\n\t\t\t\t// 可变长类型在buffer为空的情况下，类型会被擦除，导致报错，此处修正\n\t\t\t\tfinal T[] values = newArray(value.getClass(), 1);\n\t\t\t\tvalues[0] = value;\n\t\t\t\treturn append(buffer, values);\n\t\t\t}\n\t\t\treturn append(buffer, value);\n\t\t}\n\t}\n\n\t/**\n\t * 将元素值设置为数组的某个位置，当给定的index大于数组长度，则追加<br>\n\t * 替换时返回原数组，追加时返回新数组\n\t *\n\t * @param array 已有数组\n\t * @param index 位置，大于长度追加，否则替换\n\t * @param value 新值\n\t * @return 新数组或原有数组\n\t * @since 4.1.2\n\t */\n\tpublic static Object setOrAppend(Object array, int index, Object value) {\n\t\tif (index < length(array)) {\n\t\t\tArray.set(array, index, value);\n\t\t\treturn array;\n\t\t} else {\n\t\t\treturn append(array, value);\n\t\t}\n\t}\n\n\t/**\n\t * 将新元素插入到到已有数组中的某个位置<br>\n\t * 添加新元素会生成一个新数组或原有数组<br>\n\t * 如果插入位置为为负数，那么生成一个由插入元素顺序加已有数组顺序的新数组\n\t *\n\t * @param <T>    数组元素类型\n\t * @param buffer 已有数组\n\t * @param index  位置，大于长度追加，否则替换，&lt;0表示从头部追加\n\t * @param values 新值\n\t * @return 新数组或原有数组\n\t * @since 5.7.23\n\t */\n\t@SuppressWarnings({\"unchecked\"})\n\tpublic static <T> T[] replace(T[] buffer, int index, T... values) {\n\t\tif (isEmpty(values)) {\n\t\t\treturn buffer;\n\t\t}\n\t\tif (isEmpty(buffer)) {\n\t\t\treturn values;\n\t\t}\n\t\tif (index < 0) {\n\t\t\t// 从头部追加\n\t\t\treturn insert(buffer, 0, values);\n\t\t}\n\t\tif (index >= buffer.length) {\n\t\t\t// 超出长度，尾部追加\n\t\t\treturn append(buffer, values);\n\t\t}\n\n\t\tif (buffer.length >= values.length + index) {\n\t\t\tSystem.arraycopy(values, 0, buffer, index, values.length);\n\t\t\treturn buffer;\n\t\t}\n\n\t\t// 替换长度大于原数组长度，新建数组\n\t\tint newArrayLength = index + values.length;\n\t\tfinal T[] result = newArray(buffer.getClass().getComponentType(), newArrayLength);\n\t\tSystem.arraycopy(buffer, 0, result, 0, index);\n\t\tSystem.arraycopy(values, 0, result, index, values.length);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将新元素插入到到已有数组中的某个位置<br>\n\t * 添加新元素会生成一个新的数组，不影响原数组<br>\n\t * 如果插入位置为为负数，从原数组从后向前计数，若大于原数组长度，则空白处用null填充\n\t *\n\t * @param <T>         数组元素类型\n\t * @param buffer      已有数组\n\t * @param index       插入位置，此位置为对应此位置元素之前的空档\n\t * @param newElements 新元素\n\t * @return 新数组\n\t * @since 4.0.8\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T[] insert(T[] buffer, int index, T... newElements) {\n\t\treturn (T[]) insert((Object) buffer, index, newElements);\n\t}\n\n\t/**\n\t * 将新元素插入到到已有数组中的某个位置<br>\n\t * 添加新元素会生成一个新的数组，不影响原数组<br>\n\t * 如果插入位置为为负数，从原数组从后向前计数，若大于原数组长度，则空白处用null填充\n\t *\n\t * @param <T>         数组元素类型\n\t * @param array       已有数组\n\t * @param index       插入位置，此位置为对应此位置元素之前的空档\n\t * @param newElements 新元素\n\t * @return 新数组\n\t * @since 4.0.8\n\t */\n\t@SuppressWarnings({\"unchecked\", \"SuspiciousSystemArraycopy\"})\n\tpublic static <T> Object insert(Object array, int index, T... newElements) {\n\t\tif (isEmpty(newElements)) {\n\t\t\treturn array;\n\t\t}\n\t\tif (isEmpty(array)) {\n\t\t\treturn newElements;\n\t\t}\n\n\t\tfinal int len = length(array);\n\t\tif (index < 0) {\n\t\t\tindex = (index % len) + len;\n\t\t}\n\n\t\t// 已有数组的元素类型\n\t\tfinal Class<?> originComponentType = array.getClass().getComponentType();\n\t\tObject newEleArr = newElements;\n\t\t// 如果 已有数组的元素类型是 原始类型，则需要转换 新元素数组 为该类型，避免ArrayStoreException\n\t\tif (originComponentType.isPrimitive()) {\n\t\t\tnewEleArr = Convert.convert(array.getClass(), newElements);\n\t\t}\n\t\tfinal Object result = Array.newInstance(originComponentType, Math.max(len, index) + newElements.length);\n\t\tSystem.arraycopy(array, 0, result, 0, Math.min(len, index));\n\t\tSystem.arraycopy(newEleArr, 0, result, index, newElements.length);\n\t\tif (index < len) {\n\t\t\tSystem.arraycopy(array, index, result, index + newElements.length, len - index);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 生成一个新的重新设置大小的数组<br>\n\t * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，缩小则截断\n\t *\n\t * @param <T>           数组元素类型\n\t * @param data          原数组\n\t * @param newSize       新的数组大小\n\t * @param componentType 数组元素类型\n\t * @return 调整后的新数组\n\t */\n\tpublic static <T> T[] resize(T[] data, int newSize, Class<?> componentType) {\n\t\tif (newSize < 0) {\n\t\t\treturn data;\n\t\t}\n\n\t\tfinal T[] newArray = newArray(componentType, newSize);\n\t\tif (newSize > 0 && isNotEmpty(data)) {\n\t\t\tSystem.arraycopy(data, 0, newArray, 0, Math.min(data.length, newSize));\n\t\t}\n\t\treturn newArray;\n\t}\n\n\t/**\n\t * 生成一个新的重新设置大小的数组<br>\n\t * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，其它位置补充0，缩小则截断\n\t *\n\t * @param array   原数组\n\t * @param newSize 新的数组大小\n\t * @return 调整后的新数组\n\t * @since 4.6.7\n\t */\n\tpublic static Object resize(Object array, int newSize) {\n\t\tif (newSize < 0) {\n\t\t\treturn array;\n\t\t}\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = length(array);\n\t\tfinal Object newArray = Array.newInstance(array.getClass().getComponentType(), newSize);\n\t\tif (newSize > 0 && isNotEmpty(array)) {\n\t\t\t//noinspection SuspiciousSystemArraycopy\n\t\t\tSystem.arraycopy(array, 0, newArray, 0, Math.min(length, newSize));\n\t\t}\n\t\treturn newArray;\n\t}\n\n\t/**\n\t * 生成一个新的重新设置大小的数组<br>\n\t * 新数组的类型为原数组的类型，调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，缩小则截断\n\t *\n\t * @param <T>     数组元素类型\n\t * @param buffer  原数组\n\t * @param newSize 新的数组大小\n\t * @return 调整后的新数组\n\t */\n\tpublic static <T> T[] resize(T[] buffer, int newSize) {\n\t\treturn resize(buffer, newSize, buffer.getClass().getComponentType());\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param <T>    数组元素类型\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t */\n\t@SafeVarargs\n\tpublic static <T> T[] addAll(T[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\tint length = 0;\n\t\tfor (final T[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\tfinal T[] result = newArray(arrays.getClass().getComponentType().getComponentType(), length);\n\n\t\tlength = 0;\n\t\tfor (final T[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 包装 {@link System#arraycopy(Object, int, Object, int, int)}<br>\n\t * 数组复制\n\t *\n\t * @param src     源数组\n\t * @param srcPos  源数组开始位置\n\t * @param dest    目标数组\n\t * @param destPos 目标数组开始位置\n\t * @param length  拷贝数组长度\n\t * @return 目标数组\n\t * @since 3.0.6\n\t */\n\tpublic static Object copy(Object src, int srcPos, Object dest, int destPos, int length) {\n\t\t//noinspection SuspiciousSystemArraycopy\n\t\tif (null == src || null == dest) {\n\t\t\tthrow new NullPointerException(\"Source array and destination array must not be null\");\n\t\t}\n\t\tSystem.arraycopy(src, srcPos, dest, destPos, length);\n\t\treturn dest;\n\t}\n\n\t/**\n\t * 包装 {@link System#arraycopy(Object, int, Object, int, int)}<br>\n\t * 数组复制，缘数组和目标数组都是从位置0开始复制\n\t *\n\t * @param src    源数组\n\t * @param dest   目标数组\n\t * @param length 拷贝数组长度\n\t * @return 目标数组\n\t * @since 3.0.6\n\t */\n\tpublic static Object copy(Object src, Object dest, int length) {\n\t\t//noinspection SuspiciousSystemArraycopy\n\t\tif (null == src || null == dest) {\n\t\t\tthrow new NullPointerException(\"Source array and destination array must not be null\");\n\t\t}\n\t\tSystem.arraycopy(src, 0, dest, 0, length);\n\t\treturn dest;\n\t}\n\n\t/**\n\t * 克隆数组\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被克隆的数组\n\t * @return 新数组\n\t */\n\tpublic static <T> T[] clone(T[] array) {\n\t\tif (array == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn array.clone();\n\t}\n\n\t/**\n\t * 克隆数组，如果非数组返回{@code null}\n\t *\n\t * @param <T> 数组元素类型\n\t * @param obj 数组对象\n\t * @return 克隆后的数组对象\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T clone(final T obj) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tif (isArray(obj)) {\n\t\t\tfinal Object result;\n\t\t\tfinal Class<?> componentType = obj.getClass().getComponentType();\n\t\t\tif (componentType.isPrimitive()) {// 原始类型\n\t\t\t\tint length = Array.getLength(obj);\n\t\t\t\tresult = Array.newInstance(componentType, length);\n\t\t\t\twhile (length-- > 0) {\n\t\t\t\t\tArray.set(result, length, Array.get(obj, length));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult = ((Object[]) obj).clone();\n\t\t\t}\n\t\t\treturn (T) result;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 编辑数组<br>\n\t * 编辑过程通过传入的Editor实现来返回需要的元素内容，这个Editor实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，如果返回{@code null}表示这个元素对象抛弃\n\t * 2、修改元素对象，返回集合中为修改后的对象\n\t * </pre>\n\t * <p>\n\t *\n\t * @param <T>    数组元素类型\n\t * @param array  数组\n\t * @param editor 编辑器接口，{@code null}返回原集合\n\t * @return 编辑后的数组\n\t * @since 5.3.3\n\t */\n\tpublic static <T> T[] edit(T[] array, Editor<T> editor) {\n\t\tif (null == editor) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfinal ArrayList<T> list = new ArrayList<>(array.length);\n\t\tT modified;\n\t\tfor (T t : array) {\n\t\t\tmodified = editor.edit(t);\n\t\t\tif (null != modified) {\n\t\t\t\tlist.add(modified);\n\t\t\t}\n\t\t}\n\t\tfinal T[] result = newArray(array.getClass().getComponentType(), list.size());\n\t\treturn list.toArray(result);\n\t}\n\n\t/**\n\t * 过滤<br>\n\t * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容，这个Filter实现可以实现以下功能：\n\t *\n\t * <pre>\n\t * 1、过滤出需要的对象，{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中\n\t * </pre>\n\t *\n\t * @param <T>    数组元素类型\n\t * @param array  数组\n\t * @param filter 过滤器接口，用于定义过滤规则，{@code null}返回原集合\n\t * @return 过滤后的数组\n\t * @since 3.2.1\n\t */\n\tpublic static <T> T[] filter(T[] array, Filter<T> filter) {\n\t\tif (null == array || null == filter) {\n\t\t\treturn array;\n\t\t}\n\t\treturn edit(array, t -> filter.accept(t) ? t : null);\n\t}\n\n\t/**\n\t * 去除{@code null} 元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 处理后的数组\n\t * @since 3.2.2\n\t */\n\tpublic static <T> T[] removeNull(T[] array) {\n\t\treturn edit(array, t -> {\n\t\t\t// 返回null便不加入集合\n\t\t\treturn t;\n\t\t});\n\t}\n\n\t/**\n\t * 去除{@code null}或者\"\" 元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 处理后的数组\n\t * @since 3.2.2\n\t */\n\tpublic static <T extends CharSequence> T[] removeEmpty(T[] array) {\n\t\treturn filter(array, StrUtil::isNotEmpty);\n\t}\n\n\t/**\n\t * 去除{@code null}或者\"\"或者空白字符串 元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 处理后的数组\n\t * @since 3.2.2\n\t */\n\tpublic static <T extends CharSequence> T[] removeBlank(T[] array) {\n\t\treturn filter(array, StrUtil::isNotBlank);\n\t}\n\n\t/**\n\t * 数组元素中的null转换为\"\"\n\t *\n\t * @param array 数组\n\t * @return 新数组\n\t * @since 3.2.1\n\t */\n\tpublic static String[] nullToEmpty(String[] array) {\n\t\treturn edit(array, t -> null == t ? StrUtil.EMPTY : t);\n\t}\n\n\t/**\n\t * 映射键值（参考Python的zip()函数）<br>\n\t * 例如：<br>\n\t * keys = [a,b,c,d]<br>\n\t * values = [1,2,3,4]<br>\n\t * 则得到的Map是 {a=1, b=2, c=3, d=4}<br>\n\t * 如果两个数组长度不同，则只对应最短部分\n\t *\n\t * @param <K>     Key类型\n\t * @param <V>     Value类型\n\t * @param keys    键列表\n\t * @param values  值列表\n\t * @param isOrder 是否有序\n\t * @return Map\n\t * @since 3.0.4\n\t */\n\tpublic static <K, V> Map<K, V> zip(K[] keys, V[] values, boolean isOrder) {\n\t\tif (isEmpty(keys) || isEmpty(values)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal int size = Math.min(keys.length, values.length);\n\t\tfinal Map<K, V> map = MapUtil.newHashMap(size, isOrder);\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tmap.put(keys[i], values[i]);\n\t\t}\n\n\t\treturn map;\n\t}\n\n\t/**\n\t * 映射键值（参考Python的zip()函数），返回Map无序<br>\n\t * 例如：<br>\n\t * keys = [a,b,c,d]<br>\n\t * values = [1,2,3,4]<br>\n\t * 则得到的Map是 {a=1, b=2, c=3, d=4}<br>\n\t * 如果两个数组长度不同，则只对应最短部分\n\t *\n\t * @param <K>    Key类型\n\t * @param <V>    Value类型\n\t * @param keys   键列表\n\t * @param values 值列表\n\t * @return Map\n\t */\n\tpublic static <K, V> Map<K, V> zip(K[] keys, V[] values) {\n\t\treturn zip(keys, values, false);\n\t}\n\n\t// ------------------------------------------------------------------- indexOf and lastIndexOf and contains\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param <T>               数组类型\n\t * @param array             数组\n\t * @param value             被检查的元素\n\t * @param beginIndexInclude 检索开始的位置\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static <T> int indexOf(T[] array, Object value, int beginIndexInclude) {\n\t\treturn matchIndex((obj) -> ObjectUtil.equal(value, obj), beginIndexInclude, array);\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param <T>   数组类型\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static <T> int indexOf(T[] array, Object value) {\n\t\treturn matchIndex((obj) -> ObjectUtil.equal(value, obj), array);\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，忽略大小写，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.1.2\n\t */\n\tpublic static int indexOfIgnoreCase(CharSequence[] array, CharSequence value) {\n\t\tif (null != array) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (StrUtil.equalsIgnoreCase(array[i], value)) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param <T>   数组类型\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static <T> int lastIndexOf(T[] array, Object value) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\treturn lastIndexOf(array, value, array.length - 1);\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param <T>        数组类型\n\t * @param array      数组\n\t * @param value      被检查的元素\n\t * @param endInclude 查找方式为从后向前查找，查找的数组结束位置，一般为array.length-1\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 5.7.3\n\t */\n\tpublic static <T> int lastIndexOf(T[] array, Object value, int endInclude) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = endInclude; i >= 0; i--) {\n\t\t\t\tif (ObjectUtil.equal(value, array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t */\n\tpublic static <T> boolean contains(T[] array, T value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含指定元素中的任意一个\n\t *\n\t * @param <T>    数组元素类型\n\t * @param array  数组\n\t * @param values 被检查的多个元素\n\t * @return 是否包含指定元素中的任意一个\n\t * @since 4.1.20\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> boolean containsAny(T[] array, T... values) {\n\t\tfor (T value : values) {\n\t\t\tif (contains(array, value)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 数组中是否包含指定元素中的全部\n\t *\n\t * @param <T>    数组元素类型\n\t * @param array  数组\n\t * @param values 被检查的多个元素\n\t * @return 是否包含指定元素中的全部\n\t * @since 5.4.7\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> boolean containsAll(T[] array, T... values) {\n\t\tfor (T value : values) {\n\t\t\tif (false == contains(array, value)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 数组中是否包含元素，忽略大小写\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.1.2\n\t */\n\tpublic static boolean containsIgnoreCase(CharSequence[] array, CharSequence value) {\n\t\treturn indexOfIgnoreCase(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t// ------------------------------------------------------------------- Wrap and unwrap\n\n\t/**\n\t * 包装数组对象\n\t *\n\t * @param obj 对象，可以是对象数组或者基本类型数组\n\t * @return 包装类型数组或对象数组\n\t * @throws UtilException 对象为非数组\n\t */\n\tpublic static Object[] wrap(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tif (isArray(obj)) {\n\t\t\ttry {\n\t\t\t\treturn (Object[]) obj;\n\t\t\t} catch (Exception e) {\n\t\t\t\tfinal String className = obj.getClass().getComponentType().getName();\n\t\t\t\tswitch (className) {\n\t\t\t\t\tcase \"long\":\n\t\t\t\t\t\treturn wrap((long[]) obj);\n\t\t\t\t\tcase \"int\":\n\t\t\t\t\t\treturn wrap((int[]) obj);\n\t\t\t\t\tcase \"short\":\n\t\t\t\t\t\treturn wrap((short[]) obj);\n\t\t\t\t\tcase \"char\":\n\t\t\t\t\t\treturn wrap((char[]) obj);\n\t\t\t\t\tcase \"byte\":\n\t\t\t\t\t\treturn wrap((byte[]) obj);\n\t\t\t\t\tcase \"boolean\":\n\t\t\t\t\t\treturn wrap((boolean[]) obj);\n\t\t\t\t\tcase \"float\":\n\t\t\t\t\t\treturn wrap((float[]) obj);\n\t\t\t\t\tcase \"double\":\n\t\t\t\t\t\treturn wrap((double[]) obj);\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow new UtilException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthrow new UtilException(StrUtil.format(\"[{}] is not Array!\", obj.getClass()));\n\t}\n\n\t/**\n\t * 对象是否为数组对象\n\t *\n\t * @param obj 对象\n\t * @return 是否为数组对象，如果为{@code null} 返回false\n\t */\n\tpublic static boolean isArray(Object obj) {\n\t\treturn null != obj && obj.getClass().isArray();\n\t}\n\n\t/**\n\t * 获取数组对象中指定index的值，支持负数，例如-1表示倒数第一个值<br>\n\t * 如果数组下标越界，返回null\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组对象\n\t * @param index 下标，支持负数\n\t * @return 值\n\t * @since 4.0.6\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T get(Object array, int index) {\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (index < 0) {\n\t\t\tindex += Array.getLength(array);\n\t\t}\n\t\ttry {\n\t\t\treturn (T) Array.get(array, index);\n\t\t} catch (ArrayIndexOutOfBoundsException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 获取数组中指定多个下标元素值，组成新数组\n\t *\n\t * @param <T>     数组元素类型\n\t * @param array   数组，如果提供为{@code null}则返回{@code null}\n\t * @param indexes 下标列表\n\t * @return 结果\n\t */\n\tpublic static <T> T[] getAny(Object array, int... indexes) {\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\t\tif (null == indexes) {\n\t\t\treturn newArray(array.getClass().getComponentType(), 0);\n\t\t}\n\n\t\tfinal T[] result = newArray(array.getClass().getComponentType(), indexes.length);\n\t\tfor (int i = 0; i < indexes.length; i++) {\n\t\t\tresult[i] = ArrayUtil.get(array, indexes[i]);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.2.2\n\t */\n\tpublic static <T> T[] sub(T[] array, int start, int end) {\n\t\tint length = length(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn newArray(array.getClass().getComponentType(), 0);\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn newArray(array.getClass().getComponentType(), 0);\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @since 4.0.6\n\t */\n\tpublic static Object[] sub(Object array, int start, int end) {\n\t\treturn sub(array, start, end, 1);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @param step  步进\n\t * @return 新的数组\n\t * @since 4.0.6\n\t */\n\tpublic static Object[] sub(Object array, int start, int end, int step) {\n\t\tint length = length(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new Object[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new Object[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\n\t\tif (step <= 1) {\n\t\t\tstep = 1;\n\t\t}\n\n\t\tfinal ArrayList<Object> list = new ArrayList<>();\n\t\tfor (int i = start; i < end; i += step) {\n\t\t\tlist.add(get(array, i));\n\t\t}\n\n\t\treturn list.toArray();\n\t}\n\n\t/**\n\t * 数组或集合转String\n\t *\n\t * @param obj 集合或数组对象\n\t * @return 数组字符串，与集合转字符串格式相同\n\t */\n\tpublic static String toString(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (obj instanceof long[]) {\n\t\t\treturn Arrays.toString((long[]) obj);\n\t\t} else if (obj instanceof int[]) {\n\t\t\treturn Arrays.toString((int[]) obj);\n\t\t} else if (obj instanceof short[]) {\n\t\t\treturn Arrays.toString((short[]) obj);\n\t\t} else if (obj instanceof char[]) {\n\t\t\treturn Arrays.toString((char[]) obj);\n\t\t} else if (obj instanceof byte[]) {\n\t\t\treturn Arrays.toString((byte[]) obj);\n\t\t} else if (obj instanceof boolean[]) {\n\t\t\treturn Arrays.toString((boolean[]) obj);\n\t\t} else if (obj instanceof float[]) {\n\t\t\treturn Arrays.toString((float[]) obj);\n\t\t} else if (obj instanceof double[]) {\n\t\t\treturn Arrays.toString((double[]) obj);\n\t\t} else if (ArrayUtil.isArray(obj)) {\n\t\t\t// 对象数组\n\t\t\ttry {\n\t\t\t\treturn Arrays.deepToString((Object[]) obj);\n\t\t\t} catch (Exception ignore) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\n\t\treturn obj.toString();\n\t}\n\n\t/**\n\t * 获取数组长度<br>\n\t * 如果参数为{@code null}，返回0\n\t *\n\t * <pre>\n\t * ArrayUtil.length(null)            = 0\n\t * ArrayUtil.length([])              = 0\n\t * ArrayUtil.length([null])          = 1\n\t * ArrayUtil.length([true, false])   = 2\n\t * ArrayUtil.length([1, 2, 3])       = 3\n\t * ArrayUtil.length([\"a\", \"b\", \"c\"]) = 3\n\t * </pre>\n\t *\n\t * @param array 数组对象\n\t * @return 数组长度\n\t * @throws IllegalArgumentException 如果参数不为数组，抛出此异常\n\t * @see Array#getLength(Object)\n\t * @since 3.0.8\n\t */\n\tpublic static int length(Object array) throws IllegalArgumentException {\n\t\tif (null == array) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Array.getLength(array);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将数组转换为字符串\n\t *\n\t * @param <T>         被处理的集合\n\t * @param array       数组\n\t * @param conjunction 分隔符\n\t * @return 连接后的字符串\n\t */\n\tpublic static <T> String join(T[] array, CharSequence conjunction) {\n\t\treturn join(array, conjunction, null, null);\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将数组转换为字符串\n\t *\n\t * @param <T>       被处理的集合\n\t * @param array     数组\n\t * @param delimiter 分隔符\n\t * @param prefix    每个元素添加的前缀，null表示不添加\n\t * @param suffix    每个元素添加的后缀，null表示不添加\n\t * @return 连接后的字符串\n\t * @since 4.0.10\n\t */\n\tpublic static <T> String join(T[] array, CharSequence delimiter, String prefix, String suffix) {\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn StrJoiner.of(delimiter, prefix, suffix)\n\t\t\t\t// 每个元素都添加前后缀\n\t\t\t\t.setWrapElement(true)\n\t\t\t\t.append(array)\n\t\t\t\t.toString();\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将数组转换为字符串\n\t *\n\t * @param <T>         被处理的集合\n\t * @param array       数组\n\t * @param conjunction 分隔符\n\t * @param editor      每个元素的编辑器，null表示不编辑\n\t * @return 连接后的字符串\n\t * @since 5.3.3\n\t */\n\tpublic static <T> String join(T[] array, CharSequence conjunction, Editor<T> editor) {\n\t\treturn StrJoiner.of(conjunction).append(array, (t) -> String.valueOf(editor.edit(t))).toString();\n\t}\n\n\t/**\n\t * 以 conjunction 为分隔符将数组转换为字符串\n\t *\n\t * @param array       数组\n\t * @param conjunction 分隔符\n\t * @return 连接后的字符串\n\t */\n\tpublic static String join(Object array, CharSequence conjunction) {\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\t\tif (false == isArray(array)) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"[{}] is not a Array!\", array.getClass()));\n\t\t}\n\n\t\treturn StrJoiner.of(conjunction).append(array).toString();\n\t}\n\n\t/**\n\t * {@link ByteBuffer} 转byte数组\n\t *\n\t * @param bytebuffer {@link ByteBuffer}\n\t * @return byte数组\n\t * @since 3.0.1\n\t */\n\tpublic static byte[] toArray(ByteBuffer bytebuffer) {\n\t\tif (bytebuffer.hasArray()) {\n\t\t\treturn Arrays.copyOfRange(bytebuffer.array(), bytebuffer.position(), bytebuffer.limit());\n\t\t} else {\n\t\t\tint oldPosition = bytebuffer.position();\n\t\t\tbytebuffer.position(0);\n\t\t\tint size = bytebuffer.limit();\n\t\t\tbyte[] buffers = new byte[size];\n\t\t\tbytebuffer.get(buffers);\n\t\t\tbytebuffer.position(oldPosition);\n\t\t\treturn buffers;\n\t\t}\n\t}\n\n\t/**\n\t * 将集合转为数组\n\t *\n\t * @param <T>           数组元素类型\n\t * @param iterator      {@link Iterator}\n\t * @param componentType 集合元素类型\n\t * @return 数组\n\t * @since 3.0.9\n\t */\n\tpublic static <T> T[] toArray(Iterator<T> iterator, Class<T> componentType) {\n\t\treturn toArray(CollUtil.newArrayList(iterator), componentType);\n\t}\n\n\t/**\n\t * 将集合转为数组\n\t *\n\t * @param <T>           数组元素类型\n\t * @param iterable      {@link Iterable}\n\t * @param componentType 集合元素类型\n\t * @return 数组\n\t * @since 3.0.9\n\t */\n\tpublic static <T> T[] toArray(Iterable<T> iterable, Class<T> componentType) {\n\t\treturn toArray(CollectionUtil.toCollection(iterable), componentType);\n\t}\n\n\t/**\n\t * 将集合转为数组\n\t *\n\t * @param <T>           数组元素类型\n\t * @param collection    集合\n\t * @param componentType 集合元素类型\n\t * @return 数组\n\t * @since 3.0.9\n\t */\n\tpublic static <T> T[] toArray(Collection<T> collection, Class<T> componentType) {\n\t\treturn collection.toArray(newArray(componentType, 0));\n\t}\n\n\t// ---------------------------------------------------------------------- remove\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T[] remove(T[] array, int index) throws IllegalArgumentException {\n\t\treturn (T[]) remove((Object) array, index);\n\t}\n\n\t// ---------------------------------------------------------------------- removeEle\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param <T>     数组元素类型\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static <T> T[] removeEle(T[] array, T element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t// ---------------------------------------------------------------------- Reverse array\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param <T>                 数组元素类型\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 开始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static <T> T[] reverse(T[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\tT tmp;\n\t\twhile (j > i) {\n\t\t\ttmp = array[j];\n\t\t\tarray[j] = array[i];\n\t\t\tarray[i] = tmp;\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static <T> T[] reverse(T[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------ min and max\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static <T extends Comparable<? super T>> T min(T[] numberArray) {\n\t\treturn min(numberArray, null);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @param comparator  比较器，null按照默认比较\n\t * @return 最小值\n\t * @since 5.3.4\n\t */\n\tpublic static <T extends Comparable<? super T>> T min(T[] numberArray, Comparator<T> comparator) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tT min = numberArray[0];\n\t\tfor (T t : numberArray) {\n\t\t\tif (CompareUtil.compare(min, t, comparator) > 0) {\n\t\t\t\tmin = t;\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static <T extends Comparable<? super T>> T max(T[] numberArray) {\n\t\treturn max(numberArray, null);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @param comparator  比较器，null表示默认比较器\n\t * @return 最大值\n\t * @since 5.3.4\n\t */\n\tpublic static <T extends Comparable<? super T>> T max(T[] numberArray, Comparator<T> comparator) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tT max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (CompareUtil.compare(max, numberArray[i], comparator) < 0) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t// 使用Fisher–Yates洗牌算法，以线性时间复杂度打乱数组顺序\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param <T>   元素类型\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T> T[] shuffle(T[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param <T>    元素类型\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T> T[] shuffle(T[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param <T>    元素类型\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static <T> T[] swap(T[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Array must not empty !\");\n\t\t}\n\t\tT tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组对象\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static Object swap(Object array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Array must not empty !\");\n\t\t}\n\t\tObject tmp = get(array, index1);\n\t\tArray.set(array, index1, Array.get(array, index2));\n\t\tArray.set(array, index2, tmp);\n\t\treturn array;\n\t}\n\n\t/**\n\t * 计算{@code null}或空元素对象的个数，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param args 被检查的对象,一个或者多个\n\t * @return 存在{@code null}的数量\n\t * @since 4.5.18\n\t */\n\tpublic static int emptyCount(Object... args) {\n\t\tint count = 0;\n\t\tif (isNotEmpty(args)) {\n\t\t\tfor (Object element : args) {\n\t\t\t\tif (ObjectUtil.isEmpty(element)) {\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count;\n\t}\n\n\t/**\n\t * 是否存在{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param args 被检查对象\n\t * @return 是否存在\n\t * @since 4.5.18\n\t */\n\tpublic static boolean hasEmpty(Object... args) {\n\t\tif (isNotEmpty(args)) {\n\t\t\tfor (Object element : args) {\n\t\t\t\tif (ObjectUtil.isEmpty(element)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否全都为{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param args 被检查的对象,一个或者多个\n\t * @return 是否都为空\n\t * @since 4.5.18\n\t */\n\tpublic static boolean isAllEmpty(Object... args) {\n\t\tfor (Object obj : args) {\n\t\t\tif (false == ObjectUtil.isEmpty(obj)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 是否全都不为{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param args 被检查的对象,一个或者多个\n\t * @return 是否都不为空\n\t * @since 4.5.18\n\t */\n\tpublic static boolean isAllNotEmpty(Object... args) {\n\t\treturn false == hasEmpty(args);\n\t}\n\n\t/**\n\t * 多个字段是否全部不为null\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 被检查的数组\n\t * @return 多个字段是否全部不为null\n\t * @since 5.4.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> boolean isAllNotNull(T... array) {\n\t\treturn false == hasNull(array);\n\t}\n\n\t/**\n\t * 去重数组中的元素，去重后生成新的数组，原数组不变<br>\n\t * 此方法通过{@link LinkedHashSet} 去重\n\t *\n\t * @param <T>   数组元素类型\n\t * @param array 数组\n\t * @return 去重后的数组\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T[] distinct(T[] array) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfinal Set<T> set = new LinkedHashSet<>(array.length, 1);\n\t\tCollections.addAll(set, array);\n\t\treturn toArray(set, (Class<T>) getComponentType(array));\n\t}\n\n\t/**\n\t * 去重数组中的元素，去重后生成新的数组，原数组不变<br>\n\t * 此方法通过{@link LinkedHashSet} 去重\n\t *\n\t * @param <T>             数组元素类型\n\t * @param <K>             唯一键类型\n\t * @param array           数组\n\t * @param uniqueGenerator 唯一键生成器\n\t * @param override        是否覆盖模式，如果为{@code true}，加入的新值会覆盖相同key的旧值，否则会忽略新加值\n\t * @return 去重后的数组\n\t * @since 5.8.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T, K> T[] distinct(T[] array, Function<T, K> uniqueGenerator, boolean override) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfinal UniqueKeySet<K, T> set = new UniqueKeySet<>(true, uniqueGenerator);\n\t\tif (override) {\n\t\t\tCollections.addAll(set, array);\n\t\t} else {\n\t\t\tfor (T t : array) {\n\t\t\t\tset.addIfAbsent(t);\n\t\t\t}\n\t\t}\n\t\treturn toArray(set, (Class<T>) getComponentType(array));\n\t}\n\n\t/**\n\t * 按照指定规则，将一种类型的数组转换为另一种类型\n\t *\n\t * @param array               被转换的数组\n\t * @param targetComponentType 目标的元素类型\n\t * @param func                转换规则函数\n\t * @param <T>                 原数组类型\n\t * @param <R>                 目标数组类型\n\t * @return 转换后的数组\n\t * @since 5.4.2\n\t */\n\tpublic static <T, R> R[] map(T[] array, Class<R> targetComponentType, Function<? super T, ? extends R> func) {\n\t\tfinal R[] result = newArray(targetComponentType, array.length);\n\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\tresult[i] = func.apply(array[i]);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 按照指定规则，将一种类型的数组转换为另一种类型\n\t *\n\t * @param array               被转换的数组\n\t * @param targetComponentType 目标的元素类型\n\t * @param func                转换规则函数\n\t * @param <T>                 原数组类型\n\t * @param <R>                 目标数组类型\n\t * @return 转换后的数组\n\t * @since 5.5.8\n\t */\n\tpublic static <T, R> R[] map(Object array, Class<R> targetComponentType, Function<? super T, ? extends R> func) {\n\t\tfinal int length = length(array);\n\t\tfinal R[] result = newArray(targetComponentType, length);\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tresult[i] = func.apply(get(array, i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 按照指定规则，将一种类型的数组元素提取后转换为{@link List}\n\t *\n\t * @param array 被转换的数组\n\t * @param func  转换规则函数\n\t * @param <T>   原数组类型\n\t * @param <R>   目标数组类型\n\t * @return 转换后的数组\n\t * @since 5.5.7\n\t */\n\tpublic static <T, R> List<R> map(T[] array, Function<? super T, ? extends R> func) {\n\t\treturn Arrays.stream(array).map(func).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 按照指定规则，将一种类型的数组元素提取后转换为{@link Set}\n\t *\n\t * @param array 被转换的数组\n\t * @param func  转换规则函数\n\t * @param <T>   原数组类型\n\t * @param <R>   目标数组类型\n\t * @return 转换后的数组\n\t * @since 5.8.0\n\t */\n\tpublic static <T, R> Set<R> mapToSet(T[] array, Function<? super T, ? extends R> func) {\n\t\treturn Arrays.stream(array).map(func).collect(Collectors.toSet());\n\t}\n\n\t/**\n\t * 判断两个数组是否相等，判断依据包括数组长度和每个元素都相等。\n\t *\n\t * @param array1 数组1\n\t * @param array2 数组2\n\t * @return 是否相等\n\t * @since 5.4.2\n\t */\n\tpublic static boolean equals(Object array1, Object array2) {\n\t\tif (array1 == array2) {\n\t\t\treturn true;\n\t\t}\n\t\tif (hasNull(array1, array2)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tAssert.isTrue(isArray(array1), \"First is not a Array !\");\n\t\tAssert.isTrue(isArray(array2), \"Second is not a Array !\");\n\n\t\tif (array1 instanceof long[]) {\n\t\t\treturn Arrays.equals((long[]) array1, (long[]) array2);\n\t\t} else if (array1 instanceof int[]) {\n\t\t\treturn Arrays.equals((int[]) array1, (int[]) array2);\n\t\t} else if (array1 instanceof short[]) {\n\t\t\treturn Arrays.equals((short[]) array1, (short[]) array2);\n\t\t} else if (array1 instanceof char[]) {\n\t\t\treturn Arrays.equals((char[]) array1, (char[]) array2);\n\t\t} else if (array1 instanceof byte[]) {\n\t\t\treturn Arrays.equals((byte[]) array1, (byte[]) array2);\n\t\t} else if (array1 instanceof double[]) {\n\t\t\treturn Arrays.equals((double[]) array1, (double[]) array2);\n\t\t} else if (array1 instanceof float[]) {\n\t\t\treturn Arrays.equals((float[]) array1, (float[]) array2);\n\t\t} else if (array1 instanceof boolean[]) {\n\t\t\treturn Arrays.equals((boolean[]) array1, (boolean[]) array2);\n\t\t} else {\n\t\t\t// Not an array of primitives\n\t\t\treturn Arrays.deepEquals((Object[]) array1, (Object[]) array2);\n\t\t}\n\t}\n\n\t/**\n\t * 查找子数组的位置\n\t *\n\t * @param array    数组\n\t * @param subArray 子数组\n\t * @param <T>      数组元素类型\n\t * @return 子数组的开始位置，即子数字第一个元素在数组中的位置\n\t * @since 5.4.8\n\t */\n\tpublic static <T> boolean isSub(T[] array, T[] subArray) {\n\t\treturn indexOfSub(array, subArray) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 查找子数组的位置\n\t *\n\t * @param array    数组\n\t * @param subArray 子数组\n\t * @param <T>      数组元素类型\n\t * @return 子数组的开始位置，即子数字第一个元素在数组中的位置\n\t * @since 5.4.8\n\t */\n\tpublic static <T> int indexOfSub(T[] array, T[] subArray) {\n\t\treturn indexOfSub(array, 0, subArray);\n\t}\n\n\t/**\n\t * 查找子数组的位置\n\t *\n\t * @param array        数组\n\t * @param beginInclude 查找开始的位置（包含）\n\t * @param subArray     子数组\n\t * @param <T>          数组元素类型\n\t * @return 子数组的开始位置，即子数字第一个元素在数组中的位置\n\t * @since 5.4.8\n\t */\n\tpublic static <T> int indexOfSub(T[] array, int beginInclude, T[] subArray) {\n\t\tif (isEmpty(array) || isEmpty(subArray)) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\tif(beginInclude < 0){\n\t\t\tbeginInclude += array.length;\n\t\t}\n\t\tif(beginInclude < 0 || beginInclude > array.length - 1){\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\tif (array.length - beginInclude < subArray.length) {\n\t\t\t// 剩余长度不足\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\n\t\tfor (int i = beginInclude; i <= array.length - subArray.length; i++) {\n\t\t\tboolean found = true;\n\t\t\tfor (int j = 0; j < subArray.length; j++) {\n\t\t\t\tif (ObjUtil.notEqual(array[i + j], subArray[j])) {\n\t\t\t\t\tfound = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (found) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 查找最后一个子数组的开始位置\n\t *\n\t * @param array    数组\n\t * @param subArray 子数组\n\t * @param <T>      数组元素类型\n\t * @return 最后一个子数组的开始位置，即子数字第一个元素在数组中的位置\n\t * @since 5.4.8\n\t */\n\tpublic static <T> int lastIndexOfSub(T[] array, T[] subArray) {\n\t\tif (isEmpty(array) || isEmpty(subArray)) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\treturn lastIndexOfSub(array, array.length - 1, subArray);\n\t}\n\n\t/**\n\t * 查找最后一个子数组的开始位置\n\t *\n\t * @param array      数组\n\t * @param endInclude 查找结束的位置（包含），-1表示最后一位\n\t * @param subArray   子数组\n\t * @param <T>        数组元素类型\n\t * @return 最后一个子数组的开始位置，即子数字第一个元素在数组中的位置\n\t * @since 5.4.8\n\t */\n\tpublic static <T> int lastIndexOfSub(T[] array, int endInclude, T[] subArray) {\n\t\tif (isEmpty(array) || isEmpty(subArray)) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\tif (endInclude < 0) {\n\t\t\tendInclude += array.length;\n\t\t}\n\t\tif (endInclude < 0) {\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\t\tif (endInclude > array.length - 1) {\n\t\t\t// 结束位置超过最大值\n\t\t\tendInclude = array.length - 1;\n\t\t}\n\t\tif (subArray.length - 1 > endInclude) {\n\t\t\t// 剩余长度不足\n\t\t\treturn INDEX_NOT_FOUND;\n\t\t}\n\n\t\tfor (int i = Math.min(array.length - subArray.length, endInclude); i >= 0; i--) {\n\t\t\tboolean found = true;\n\t\t\tfor (int j = 0; j < subArray.length; j++) {\n\t\t\t\tif (ObjUtil.notEqual(array[i + j], subArray[j])) {\n\t\t\t\t\tfound = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (found) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t// O(n)时间复杂度检查数组是否有序\n\n\t/**\n\t * 检查数组是否有序，即comparator.compare(array[i], array[i + 1]) &lt;= 0，若传入空数组或空比较器，则返回false\n\t *\n\t * @param array      数组\n\t * @param comparator 比较器\n\t * @param <T>        数组元素类型\n\t * @return 数组是否有序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T> boolean isSorted(T[] array, Comparator<? super T> comparator) {\n\t\tif (array == null || comparator == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (comparator.compare(array[i], array[i + 1]) > 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i].compareTo(array[i + 1]) &lt;= 0，若传入空数组，则返回false\n\t *\n\t * @param <T>   数组元素类型，该类型需要实现Comparable接口\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T extends Comparable<? super T>> boolean isSorted(T[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\n\t/**\n\t * 检查数组是否升序，即array[i].compareTo(array[i + 1]) &lt;= 0，若传入空数组，则返回false\n\t *\n\t * @param <T>   数组元素类型，该类型需要实现Comparable接口\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T extends Comparable<? super T>> boolean isSortedASC(T[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i].compareTo(array[i + 1]) > 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i].compareTo(array[i + 1]) &gt;= 0，若传入空数组，则返回false\n\t *\n\t * @param <T>   数组元素类型，该类型需要实现Comparable接口\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static <T extends Comparable<? super T>> boolean isSortedDESC(T[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i].compareTo(array[i + 1]) < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/BooleanUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\n\nimport java.util.Set;\n\n/**\n * Boolean类型相关工具类\n *\n * @author looly\n * @since 4.1.16\n */\npublic class BooleanUtil {\n\n\t/** 表示为真的字符串 */\n\tprivate static final Set<String> TRUE_SET = CollUtil.newHashSet(\"true\", \"yes\", \"y\", \"t\", \"ok\", \"correct\", \"success\", \"on\", \"1\", \"是\", \"对\", \"真\", \"對\", \"正确\", \"开\", \"开启\", \"√\", \"☑\");\n\t/** 表示为假的字符串 */\n\tprivate static final Set<String> FALSE_SET = CollUtil.newHashSet(\"false\", \"no\", \"n\", \"f\", \"wrong\", \"fail\", \"off\", \"0\", \"否\", \"错\", \"假\", \"錯\", \"错误\", \"关\", \"关闭\", \"×\", \"☒\");\n\n\t/**\n\t * 取相反值\n\t *\n\t * @param bool Boolean值\n\t * @return 相反的Boolean值\n\t */\n\tpublic static Boolean negate(Boolean bool) {\n\t\tif (bool == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn bool ? Boolean.FALSE : Boolean.TRUE;\n\t}\n\n\t/**\n\t * 检查 {@code Boolean} 值是否为 {@code true}\n\t *\n\t * <pre>\n\t *   BooleanUtil.isTrue(Boolean.TRUE)  = true\n\t *   BooleanUtil.isTrue(Boolean.FALSE) = false\n\t *   BooleanUtil.isTrue(null)          = false\n\t * </pre>\n\t *\n\t * @param bool 被检查的Boolean值\n\t * @return 当值为true且非null时返回{@code true}\n\t */\n\tpublic static boolean isTrue(Boolean bool) {\n\t\treturn Boolean.TRUE.equals(bool);\n\t}\n\n\t/**\n\t * 检查 {@code Boolean} 值是否为 {@code false}\n\t *\n\t * <pre>\n\t *   BooleanUtil.isFalse(Boolean.TRUE)  = false\n\t *   BooleanUtil.isFalse(Boolean.FALSE) = true\n\t *   BooleanUtil.isFalse(null)          = false\n\t * </pre>\n\t *\n\t * @param bool 被检查的Boolean值\n\t * @return 当值为false且非null时返回{@code true}\n\t */\n\tpublic static boolean isFalse(Boolean bool) {\n\t\treturn Boolean.FALSE.equals(bool);\n\t}\n\n\t/**\n\t * 取相反值\n\t *\n\t * @param bool Boolean值\n\t * @return 相反的Boolean值\n\t */\n\tpublic static boolean negate(boolean bool) {\n\t\treturn !bool;\n\t}\n\n\t/**\n\t * 转换字符串为boolean值\n\t *\n\t * @param valueStr 字符串\n\t * @return boolean值\n\t */\n\tpublic static boolean toBoolean(String valueStr) {\n\t\tif (StrUtil.isNotBlank(valueStr)) {\n\t\t\tvalueStr = valueStr.trim().toLowerCase();\n\t\t\treturn TRUE_SET.contains(valueStr);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 转换字符串为boolean值<br>\n\t * 如果为[\"true\", \"yes\", \"y\", \"t\", \"ok\", \"1\", \"on\", \"是\", \"对\", \"真\", \"對\", \"√\"]，返回{@code true}<br>\n\t * 如果为[\"false\", \"no\", \"n\", \"f\", \"0\", \"off\", \"否\", \"错\", \"假\", \"錯\", \"×\"]，返回{@code false}<br>\n\t * 其他情况返回{@code null}\n\t *\n\t * @param valueStr 字符串\n\t * @return boolean值\n\t * @since 5.8.1\n\t */\n\tpublic static Boolean toBooleanObject(String valueStr) {\n\t\tif (StrUtil.isNotBlank(valueStr)) {\n\t\t\tvalueStr = valueStr.trim().toLowerCase();\n\t\t\tif(TRUE_SET.contains(valueStr)){\n\t\t\t\treturn true;\n\t\t\t} else if(FALSE_SET.contains(valueStr)){\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * boolean值转为int\n\t *\n\t * @param value Boolean值\n\t * @return int值\n\t */\n\tpublic static int toInt(boolean value) {\n\t\treturn value ? 1 : 0;\n\t}\n\n\t/**\n\t * boolean值转为Integer\n\t *\n\t * @param value Boolean值\n\t * @return Integer值\n\t */\n\tpublic static Integer toInteger(boolean value) {\n\t\treturn toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为char\n\t *\n\t * @param value Boolean值\n\t * @return char值\n\t */\n\tpublic static char toChar(boolean value) {\n\t\treturn (char) toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为Character\n\t *\n\t * @param value Boolean值\n\t * @return Character值\n\t */\n\tpublic static Character toCharacter(boolean value) {\n\t\treturn toChar(value);\n\t}\n\n\t/**\n\t * boolean值转为byte\n\t *\n\t * @param value Boolean值\n\t * @return byte值\n\t */\n\tpublic static byte toByte(boolean value) {\n\t\treturn (byte) toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为Byte\n\t *\n\t * @param value Boolean值\n\t * @return Byte值\n\t */\n\tpublic static Byte toByteObj(boolean value) {\n\t\treturn toByte(value);\n\t}\n\n\t/**\n\t * boolean值转为long\n\t *\n\t * @param value Boolean值\n\t * @return long值\n\t */\n\tpublic static long toLong(boolean value) {\n\t\treturn toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为Long\n\t *\n\t * @param value Boolean值\n\t * @return Long值\n\t */\n\tpublic static Long toLongObj(boolean value) {\n\t\treturn toLong(value);\n\t}\n\n\t/**\n\t * boolean值转为short\n\t *\n\t * @param value Boolean值\n\t * @return short值\n\t */\n\tpublic static short toShort(boolean value) {\n\t\treturn (short) toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为Short\n\t *\n\t * @param value Boolean值\n\t * @return Short值\n\t */\n\tpublic static Short toShortObj(boolean value) {\n\t\treturn toShort(value);\n\t}\n\n\t/**\n\t * boolean值转为float\n\t *\n\t * @param value Boolean值\n\t * @return float值\n\t */\n\tpublic static float toFloat(boolean value) {\n\t\treturn (float) toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为Float\n\t *\n\t * @param value Boolean值\n\t * @return float值\n\t */\n\tpublic static Float toFloatObj(boolean value) {\n\t\treturn toFloat(value);\n\t}\n\n\t/**\n\t * boolean值转为double\n\t *\n\t * @param value Boolean值\n\t * @return double值\n\t */\n\tpublic static double toDouble(boolean value) {\n\t\treturn toInt(value);\n\t}\n\n\t/**\n\t * boolean值转为double\n\t *\n\t * @param value Boolean值\n\t * @return double值\n\t */\n\tpublic static Double toDoubleObj(boolean value) {\n\t\treturn toDouble(value);\n\t}\n\n\t/**\n\t * 将boolean转换为字符串 {@code 'true'} 或者 {@code 'false'}.\n\t *\n\t * <pre>\n\t *   BooleanUtil.toStringTrueFalse(true)   = \"true\"\n\t *   BooleanUtil.toStringTrueFalse(false)  = \"false\"\n\t * </pre>\n\t *\n\t * @param bool Boolean值\n\t * @return {@code 'true'}, {@code 'false'}\n\t */\n\tpublic static String toStringTrueFalse(boolean bool) {\n\t\treturn toString(bool, \"true\", \"false\");\n\t}\n\n\t/**\n\t * 将boolean转换为字符串 {@code 'on'} 或者 {@code 'off'}.\n\t *\n\t * <pre>\n\t *   BooleanUtil.toStringOnOff(true)   = \"on\"\n\t *   BooleanUtil.toStringOnOff(false)  = \"off\"\n\t * </pre>\n\t *\n\t * @param bool Boolean值\n\t * @return {@code 'on'}, {@code 'off'}\n\t */\n\tpublic static String toStringOnOff(boolean bool) {\n\t\treturn toString(bool, \"on\", \"off\");\n\t}\n\n\t/**\n\t * 将boolean转换为字符串 {@code 'yes'} 或者 {@code 'no'}.\n\t *\n\t * <pre>\n\t *   BooleanUtil.toStringYesNo(true)   = \"yes\"\n\t *   BooleanUtil.toStringYesNo(false)  = \"no\"\n\t * </pre>\n\t *\n\t * @param bool Boolean值\n\t * @return {@code 'yes'}, {@code 'no'}\n\t */\n\tpublic static String toStringYesNo(boolean bool) {\n\t\treturn toString(bool, \"yes\", \"no\");\n\t}\n\n\t/**\n\t * 将boolean转换为字符串\n\t *\n\t * <pre>\n\t *   BooleanUtil.toString(true, \"true\", \"false\")   = \"true\"\n\t *   BooleanUtil.toString(false, \"true\", \"false\")  = \"false\"\n\t * </pre>\n\t *\n\t * @param bool Boolean值\n\t * @param trueString 当值为 {@code true}时返回此字符串, 可能为 {@code null}\n\t * @param falseString 当值为 {@code false}时返回此字符串, 可能为 {@code null}\n\t * @return 结果值\n\t */\n\tpublic static String toString(boolean bool, String trueString, String falseString) {\n\t\treturn bool ? trueString : falseString;\n\t}\n\n\t/**\n\t * 将boolean转换为字符串\n\t *\n\t * <pre>\n\t *   BooleanUtil.toString(true, \"true\", \"false\", null) = \"true\"\n\t *   BooleanUtil.toString(false, \"true\", \"false\", null) = \"false\"\n\t *   BooleanUtil.toString(null, \"true\", \"false\", null) = null\n\t * </pre>\n\t *\n\t * @param bool Boolean值\n\t * @param trueString 当值为 {@code true}时返回此字符串, 可能为 {@code null}\n\t * @param falseString 当值为 {@code false}时返回此字符串, 可能为 {@code null}\n\t * @param nullString 当值为 {@code null}时返回此字符串, 可能为 {@code null}\n\t * @return 结果值\n\t */\n\tpublic static String toString(Boolean bool, String trueString, String falseString, String nullString) {\n\t\tif (bool == null) {\n\t\t\treturn nullString;\n\t\t}\n\t\treturn bool ? trueString : falseString;\n\t}\n\n\t/**\n\t * 对Boolean数组取与\n\t *\n\t * <pre>\n\t *   BooleanUtil.and(true, true)         = true\n\t *   BooleanUtil.and(false, false)       = false\n\t *   BooleanUtil.and(true, false)        = false\n\t *   BooleanUtil.and(true, true, false)  = false\n\t *   BooleanUtil.and(true, true, true)   = true\n\t * </pre>\n\t *\n\t * @param array {@code Boolean}数组\n\t * @return 取与为真返回{@code true}\n\t */\n\tpublic static boolean and(boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty !\");\n\t\t}\n\t\tfor (final boolean element : array) {\n\t\t\tif (false == element) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 对Boolean数组取与\n\t *\n\t * <pre>\n\t *   BooleanUtil.and(Boolean.TRUE, Boolean.TRUE)                 = Boolean.TRUE\n\t *   BooleanUtil.and(Boolean.FALSE, Boolean.FALSE)               = Boolean.FALSE\n\t *   BooleanUtil.and(Boolean.TRUE, Boolean.FALSE)                = Boolean.FALSE\n\t *   BooleanUtil.and(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)   = Boolean.TRUE\n\t *   BooleanUtil.and(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE) = Boolean.FALSE\n\t *   BooleanUtil.and(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE)  = Boolean.FALSE\n\t * </pre>\n\t *\n\t * @param array {@code Boolean}数组\n\t * @return 取与为真返回{@code true}\n\t */\n\tpublic static Boolean andOfWrap(Boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty !\");\n\t\t}\n\n\t\tfor (final Boolean b : array) {\n\t\t\tif(!isTrue(b)){\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 对Boolean数组取或\n\t *\n\t * <pre>\n\t *   BooleanUtil.or(true, true)          = true\n\t *   BooleanUtil.or(false, false)        = false\n\t *   BooleanUtil.or(true, false)         = true\n\t *   BooleanUtil.or(true, true, false)   = true\n\t *   BooleanUtil.or(true, true, true)    = true\n\t *   BooleanUtil.or(false, false, false) = false\n\t * </pre>\n\t *\n\t * @param array {@code Boolean}数组\n\t * @return 取或为真返回{@code true}\n\t */\n\tpublic static boolean or(boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty !\");\n\t\t}\n\t\tfor (final boolean element : array) {\n\t\t\tif (element) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 对Boolean数组取或\n\t *\n\t * <pre>\n\t *   BooleanUtil.or(Boolean.TRUE, Boolean.TRUE)                  = Boolean.TRUE\n\t *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE)                = Boolean.FALSE\n\t *   BooleanUtil.or(Boolean.TRUE, Boolean.FALSE)                 = Boolean.TRUE\n\t *   BooleanUtil.or(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)    = Boolean.TRUE\n\t *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE)  = Boolean.TRUE\n\t *   BooleanUtil.or(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE)   = Boolean.TRUE\n\t *   BooleanUtil.or(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE) = Boolean.FALSE\n\t * </pre>\n\t *\n\t * @param array {@code Boolean}数组\n\t * @return 取或为真返回{@code true}\n\t */\n\tpublic static Boolean orOfWrap(Boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty !\");\n\t\t}\n\n\t\tfor (final Boolean b : array) {\n\t\t\tif(isTrue(b)){\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 对Boolean数组取异或\n\t * 当前实现的语义为：当数组中 true 的数量为奇数时返回 true，数量为偶数时返回 false。\n\t *\n\t * 注意：该方法并不是判断“是否恰好只有一个 true”，\n\t * 不适用于互斥条件校验（例如：多个条件只能有一个成立的场景）。\n\t *\n\t * <pre>\n\t *   BooleanUtil.xor(true, true)   = false\n\t *   BooleanUtil.xor(false, false) = false\n\t *   BooleanUtil.xor(true, false)  = true\n\t *   BooleanUtil.xor(true, true)   = false\n\t *   BooleanUtil.xor(false, false) = false\n\t *   BooleanUtil.xor(true, false)  = true\n\t *   BooleanUtil.xor(true, true, true)   = true\n\t *   BooleanUtil.xor(true, true, false)  = false\n\t * </pre>\n\t * 如需判断“是否恰好只有一个 true”，请使用 {@link #exactlyOneTrue(boolean...)}。\n\t * @param array {@code boolean}数组\n\t * @return 如果异或计算为true返回 {@code true}\n\t */\n\tpublic static boolean xor(boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty\");\n\t\t}\n\n\t\tboolean result = false;\n\t\tfor (final boolean element : array) {\n\t\t\tresult ^= element;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 判断 boolean 数组中是否 恰好只有一个 元素为 true。\n\t * 常用于互斥条件校验场景，例如：\n\t * 多个角色只能同时存在一个\n\t * 多个条件只能命中一个分支\n\t * 与 {@link #xor(boolean...)} 不同，\n\t * 本方法在出现多个 true 时会直接返回 false。\n\t *\n\t * @param array boolean 数组\n\t * @return 当且仅当数组中恰好只有一个 true 时返回 true\n\t */\n\tpublic static boolean exactlyOneTrue(final boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty\");\n\t\t}\n\n\t\tint count = 0;\n\t\tfor (boolean b : array) {\n\t\t\tif (b && ++count > 1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn count == 1;\n\t}\n\n\t/**\n\t * 对Boolean数组取异或\n\t *\n\t * <pre>\n\t *   BooleanUtil.xor(new Boolean[] { Boolean.TRUE, Boolean.TRUE })   = Boolean.FALSE\n\t *   BooleanUtil.xor(new Boolean[] { Boolean.FALSE, Boolean.FALSE }) = Boolean.FALSE\n\t *   BooleanUtil.xor(new Boolean[] { Boolean.TRUE, Boolean.FALSE })  = Boolean.TRUE\n\t * </pre>\n\t *\n\t * @param array {@code Boolean} 数组\n\t * @return 异或为真取{@code true}\n\t */\n\tpublic static Boolean xorOfWrap(Boolean... array) {\n\t\tif (ArrayUtil.isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"The Array must not be empty !\");\n\t\t}\n\t\tfinal boolean[] primitive = Convert.convert(boolean[].class, array);\n\t\treturn xor(primitive);\n\t}\n\n\t/**\n\t * 给定类是否为Boolean或者boolean\n\t *\n\t * @param clazz 类\n\t * @return 是否为Boolean或者boolean\n\t * @since 4.5.2\n\t */\n\tpublic static boolean isBoolean(Class<?> clazz) {\n\t\treturn (clazz == Boolean.class || clazz == boolean.class);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ByteUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.nio.ByteOrder;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.DoubleAdder;\nimport java.util.concurrent.atomic.LongAdder;\n\n/**\n * 对数字和字节进行转换。<br>\n * 假设数据存储是以大端模式存储的：<br>\n * <ul>\n *     <li>byte: 字节类型 占8位二进制 00000000</li>\n *     <li>char: 字符类型 占2个字节 16位二进制 byte[0] byte[1]</li>\n *     <li>int : 整数类型 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]</li>\n *     <li>long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5]</li>\n *     <li>long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7]</li>\n *     <li>float: 浮点数(小数) 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]</li>\n *     <li>double: 双精度浮点数(小数) 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4]byte[5] byte[6] byte[7]</li>\n * </ul>\n * 注：注释来自Hanlp，代码提供来自pr#1492@Github\n *\n * @author looly, hanlp, FULaBUla\n * @since 5.6.3\n */\npublic class ByteUtil {\n\n\tpublic static final ByteOrder DEFAULT_ORDER = ByteOrder.LITTLE_ENDIAN;\n\t/**\n\t * CPU的字节序\n\t */\n\tpublic static final ByteOrder CPU_ENDIAN = \"little\".equals(System.getProperty(\"sun.cpu.endian\")) ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;\n\n\t/**\n\t * int转byte\n\t *\n\t * @param intValue int值\n\t * @return byte值\n\t */\n\tpublic static byte intToByte(int intValue) {\n\t\treturn (byte) intValue;\n\t}\n\n\t/**\n\t * byte转无符号int\n\t *\n\t * @param byteValue byte值\n\t * @return 无符号int值\n\t * @since 3.2.0\n\t */\n\tpublic static int byteToUnsignedInt(byte byteValue) {\n\t\t// Java 总是把 byte 当做有符处理；我们可以通过将其和 0xFF 进行二进制与得到它的无符值\n\t\treturn byteValue & 0xFF;\n\t}\n\n\t/**\n\t * byte数组转short<br>\n\t * 默认以小端序转换\n\t *\n\t * @param bytes byte数组\n\t * @return short值\n\t */\n\tpublic static short bytesToShort(byte[] bytes) {\n\t\treturn bytesToShort(bytes, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * byte数组转short<br>\n\t * 自定义端序\n\t *\n\t * @param bytes     byte数组，长度必须为2\n\t * @param byteOrder 端序\n\t * @return short值\n\t */\n\tpublic static short bytesToShort(final byte[] bytes, final ByteOrder byteOrder) {\n\t\treturn bytesToShort(bytes, 0, byteOrder);\n\t}\n\n\t/**\n\t * byte数组转short<br>\n\t * 自定义端序\n\t *\n\t * @param bytes     byte数组，长度必须大于2\n\t * @param start     开始位置\n\t * @param byteOrder 端序\n\t * @return short值\n\t */\n\tpublic static short bytesToShort(final byte[] bytes, final int start, final ByteOrder byteOrder) {\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\t//小端模式，数据的高字节保存在内存的高地址中，而数据的低字节保存在内存的低地址中\n\t\t\treturn (short) (bytes[start] & 0xff | (bytes[start + 1] & 0xff) << Byte.SIZE);\n\t\t} else {\n\t\t\treturn (short) (bytes[start + 1] & 0xff | (bytes[start] & 0xff) << Byte.SIZE);\n\t\t}\n\t}\n\n\t/**\n\t * short转byte数组<br>\n\t * 默认以小端序转换\n\t *\n\t * @param shortValue short值\n\t * @return byte数组\n\t */\n\tpublic static byte[] shortToBytes(short shortValue) {\n\t\treturn shortToBytes(shortValue, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * short转byte数组<br>\n\t * 自定义端序\n\t *\n\t * @param shortValue short值\n\t * @param byteOrder  端序\n\t * @return byte数组\n\t */\n\tpublic static byte[] shortToBytes(short shortValue, ByteOrder byteOrder) {\n\t\tbyte[] b = new byte[Short.BYTES];\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\tb[0] = (byte) (shortValue & 0xff);\n\t\t\tb[1] = (byte) ((shortValue >> Byte.SIZE) & 0xff);\n\t\t} else {\n\t\t\tb[1] = (byte) (shortValue & 0xff);\n\t\t\tb[0] = (byte) ((shortValue >> Byte.SIZE) & 0xff);\n\t\t}\n\t\treturn b;\n\t}\n\n\t/**\n\t * byte[]转int值<br>\n\t * 默认以小端序转换\n\t *\n\t * @param bytes byte数组\n\t * @return int值\n\t */\n\tpublic static int bytesToInt(byte[] bytes) {\n\t\treturn bytesToInt(bytes, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * byte[]转int值<br>\n\t * 自定义端序\n\t *\n\t * @param bytes     byte数组\n\t * @param byteOrder 端序\n\t * @return int值\n\t */\n\tpublic static int bytesToInt(byte[] bytes, ByteOrder byteOrder) {\n\t\treturn bytesToInt(bytes, 0, byteOrder);\n\t}\n\n\t/**\n\t * byte[]转int值<br>\n\t * 自定义端序\n\t *\n\t * @param bytes     byte数组\n\t * @param start 开始位置（包含）\n\t * @param byteOrder 端序\n\t * @return int值\n\t * @since 5.7.21\n\t */\n\tpublic static int bytesToInt(byte[] bytes, int start, ByteOrder byteOrder) {\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\treturn bytes[start] & 0xFF | //\n\t\t\t\t\t(bytes[1 + start] & 0xFF) << 8 | //\n\t\t\t\t\t(bytes[2 + start] & 0xFF) << 16 | //\n\t\t\t\t\t(bytes[3 + start] & 0xFF) << 24; //\n\t\t} else {\n\t\t\treturn bytes[3 + start] & 0xFF | //\n\t\t\t\t\t(bytes[2 + start] & 0xFF) << 8 | //\n\t\t\t\t\t(bytes[1 + start] & 0xFF) << 16 | //\n\t\t\t\t\t(bytes[start] & 0xFF) << 24; //\n\t\t}\n\n\t}\n\n\t/**\n\t * int转byte数组<br>\n\t * 默认以小端序转换\n\t *\n\t * @param intValue int值\n\t * @return byte数组\n\t */\n\tpublic static byte[] intToBytes(int intValue) {\n\t\treturn intToBytes(intValue, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * int转byte数组<br>\n\t * 自定义端序\n\t *\n\t * @param intValue  int值\n\t * @param byteOrder 端序\n\t * @return byte数组\n\t */\n\tpublic static byte[] intToBytes(int intValue, ByteOrder byteOrder) {\n\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\treturn new byte[]{ //\n\t\t\t\t\t(byte) (intValue & 0xFF), //\n\t\t\t\t\t(byte) ((intValue >> 8) & 0xFF), //\n\t\t\t\t\t(byte) ((intValue >> 16) & 0xFF), //\n\t\t\t\t\t(byte) ((intValue >> 24) & 0xFF) //\n\t\t\t};\n\n\t\t} else {\n\t\t\treturn new byte[]{ //\n\t\t\t\t\t(byte) ((intValue >> 24) & 0xFF), //\n\t\t\t\t\t(byte) ((intValue >> 16) & 0xFF), //\n\t\t\t\t\t(byte) ((intValue >> 8) & 0xFF), //\n\t\t\t\t\t(byte) (intValue & 0xFF) //\n\t\t\t};\n\t\t}\n\n\t}\n\n\t/**\n\t * long转byte数组<br>\n\t * 默认以小端序转换<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param longValue long值\n\t * @return byte数组\n\t */\n\tpublic static byte[] longToBytes(long longValue) {\n\t\treturn longToBytes(longValue, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * long转byte数组<br>\n\t * 自定义端序<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param longValue long值\n\t * @param byteOrder 端序\n\t * @return byte数组\n\t */\n\tpublic static byte[] longToBytes(long longValue, ByteOrder byteOrder) {\n\t\tbyte[] result = new byte[Long.BYTES];\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\tfor (int i = 0; i < result.length; i++) {\n\t\t\t\tresult[i] = (byte) (longValue & 0xFF);\n\t\t\t\tlongValue >>= Byte.SIZE;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = (result.length - 1); i >= 0; i--) {\n\t\t\t\tresult[i] = (byte) (longValue & 0xFF);\n\t\t\t\tlongValue >>= Byte.SIZE;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * byte数组转long<br>\n\t * 默认以小端序转换<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param bytes byte数组\n\t * @return long值\n\t */\n\tpublic static long bytesToLong(byte[] bytes) {\n\t\treturn bytesToLong(bytes, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * byte数组转long<br>\n\t * 自定义端序<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param bytes     byte数组\n\t * @param byteOrder 端序\n\t * @return long值\n\t */\n\tpublic static long bytesToLong(byte[] bytes, ByteOrder byteOrder) {\n\t\treturn bytesToLong(bytes, 0, byteOrder);\n\t}\n\n\t/**\n\t * byte数组转long<br>\n\t * 自定义端序<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param bytes     byte数组\n\t * @param start     计算数组开始位置\n\t * @param byteOrder 端序\n\t * @return long值\n\t * @since 5.7.21\n\t */\n\tpublic static long bytesToLong(byte[] bytes, int start, ByteOrder byteOrder) {\n\t\tlong values = 0;\n\t\tif (ByteOrder.LITTLE_ENDIAN == byteOrder) {\n\t\t\tfor (int i = (Long.BYTES - 1); i >= 0; i--) {\n\t\t\t\tvalues <<= Byte.SIZE;\n\t\t\t\tvalues |= (bytes[i + start] & 0xff);\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = 0; i < Long.BYTES; i++) {\n\t\t\t\tvalues <<= Byte.SIZE;\n\t\t\t\tvalues |= (bytes[i + start] & 0xff);\n\t\t\t}\n\t\t}\n\n\t\treturn values;\n\t}\n\n\t/**\n\t * float转byte数组，默认以小端序转换<br>\n\t *\n\t * @param floatValue float值\n\t * @return byte数组\n\t * @since 5.7.18\n\t */\n\tpublic static byte[] floatToBytes(float floatValue) {\n\t\treturn floatToBytes(floatValue, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * float转byte数组，自定义端序<br>\n\t *\n\t * @param floatValue float值\n\t * @param byteOrder  端序\n\t * @return byte数组\n\t * @since 5.7.18\n\t */\n\tpublic static byte[] floatToBytes(float floatValue, ByteOrder byteOrder) {\n\t\treturn intToBytes(Float.floatToIntBits(floatValue), byteOrder);\n\t}\n\n\t/**\n\t * byte数组转float<br>\n\t * 默认以小端序转换<br>\n\t *\n\t * @param bytes byte数组\n\t * @return float值\n\t * @since 5.7.18\n\t */\n\tpublic static float bytesToFloat(byte[] bytes) {\n\t\treturn bytesToFloat(bytes, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * byte数组转float<br>\n\t * 自定义端序<br>\n\t *\n\t * @param bytes     byte数组\n\t * @param byteOrder 端序\n\t * @return float值\n\t * @since 5.7.18\n\t */\n\tpublic static float bytesToFloat(byte[] bytes, ByteOrder byteOrder) {\n\t\treturn Float.intBitsToFloat(bytesToInt(bytes, byteOrder));\n\t}\n\n\t/**\n\t * double转byte数组<br>\n\t * 默认以小端序转换<br>\n\t *\n\t * @param doubleValue double值\n\t * @return byte数组\n\t */\n\tpublic static byte[] doubleToBytes(double doubleValue) {\n\t\treturn doubleToBytes(doubleValue, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * double转byte数组<br>\n\t * 自定义端序<br>\n\t * from: https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java\n\t *\n\t * @param doubleValue double值\n\t * @param byteOrder   端序\n\t * @return byte数组\n\t */\n\tpublic static byte[] doubleToBytes(double doubleValue, ByteOrder byteOrder) {\n\t\treturn longToBytes(Double.doubleToLongBits(doubleValue), byteOrder);\n\t}\n\n\t/**\n\t * byte数组转Double<br>\n\t * 默认以小端序转换<br>\n\t *\n\t * @param bytes byte数组\n\t * @return long值\n\t */\n\tpublic static double bytesToDouble(byte[] bytes) {\n\t\treturn bytesToDouble(bytes, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * byte数组转double<br>\n\t * 自定义端序<br>\n\t *\n\t * @param bytes     byte数组\n\t * @param byteOrder 端序\n\t * @return long值\n\t */\n\tpublic static double bytesToDouble(byte[] bytes, ByteOrder byteOrder) {\n\t\treturn Double.longBitsToDouble(bytesToLong(bytes, byteOrder));\n\t}\n\n\t/**\n\t * 将{@link Number}转换为\n\t *\n\t * @param number 数字\n\t * @return bytes\n\t */\n\tpublic static byte[] numberToBytes(Number number) {\n\t\treturn numberToBytes(number, DEFAULT_ORDER);\n\t}\n\n\t/**\n\t * 将{@link Number}转换为\n\t *\n\t * @param number    数字\n\t * @param byteOrder 端序\n\t * @return bytes\n\t */\n\tpublic static byte[] numberToBytes(Number number, ByteOrder byteOrder) {\n\t\tif(number instanceof Byte){\n\t\t\treturn new byte[]{number.byteValue()};\n\t\t}else if (number instanceof Double) {\n\t\t\treturn doubleToBytes((Double) number, byteOrder);\n\t\t} else if (number instanceof Long) {\n\t\t\treturn longToBytes((Long) number, byteOrder);\n\t\t} else if (number instanceof Integer) {\n\t\t\treturn intToBytes((Integer) number, byteOrder);\n\t\t} else if (number instanceof Short) {\n\t\t\treturn shortToBytes((Short) number, byteOrder);\n\t\t} else if (number instanceof Float) {\n\t\t\treturn floatToBytes((Float) number, byteOrder);\n\t\t} else {\n\t\t\treturn doubleToBytes(number.doubleValue(), byteOrder);\n\t\t}\n\t}\n\n\t/**\n\t * byte数组转换为指定类型数字\n\t *\n\t * @param <T>         数字类型\n\t * @param bytes       byte数组\n\t * @param targetClass 目标数字类型\n\t * @param byteOrder   端序\n\t * @return 转换后的数字\n\t * @throws IllegalArgumentException 不支持的数字类型，如用户自定义数字类型\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T extends Number> T bytesToNumber(byte[] bytes, Class<T> targetClass, ByteOrder byteOrder) throws IllegalArgumentException {\n\t\tNumber number;\n\t\tif (Byte.class == targetClass) {\n\t\t\tnumber = bytes[0];\n\t\t} else if (Short.class == targetClass) {\n\t\t\tnumber = bytesToShort(bytes, byteOrder);\n\t\t} else if (Integer.class == targetClass) {\n\t\t\tnumber = bytesToInt(bytes, byteOrder);\n\t\t} else if (AtomicInteger.class == targetClass) {\n\t\t\tnumber = new AtomicInteger(bytesToInt(bytes, byteOrder));\n\t\t} else if (Long.class == targetClass) {\n\t\t\tnumber = bytesToLong(bytes, byteOrder);\n\t\t} else if (AtomicLong.class == targetClass) {\n\t\t\tnumber = new AtomicLong(bytesToLong(bytes, byteOrder));\n\t\t} else if (LongAdder.class == targetClass) {\n\t\t\tfinal LongAdder longValue = new LongAdder();\n\t\t\tlongValue.add(bytesToLong(bytes, byteOrder));\n\t\t\tnumber = longValue;\n\t\t} else if (Float.class == targetClass) {\n\t\t\tnumber = bytesToFloat(bytes, byteOrder);\n\t\t} else if (Double.class == targetClass) {\n\t\t\tnumber = bytesToDouble(bytes, byteOrder);\n\t\t} else if (DoubleAdder.class == targetClass) {\n\t\t\tfinal DoubleAdder doubleAdder = new DoubleAdder();\n\t\t\tdoubleAdder.add(bytesToDouble(bytes, byteOrder));\n\t\t\tnumber = doubleAdder;\n\t\t} else if (BigDecimal.class == targetClass) {\n\t\t\tnumber = NumberUtil.toBigDecimal(bytesToDouble(bytes, byteOrder));\n\t\t} else if (BigInteger.class == targetClass) {\n\t\t\tnumber = BigInteger.valueOf(bytesToLong(bytes, byteOrder));\n\t\t} else if (Number.class == targetClass) {\n\t\t\t// 用户没有明确类型具体类型，默认Double\n\t\t\tnumber = bytesToDouble(bytes, byteOrder);\n\t\t} else {\n\t\t\t// 用户自定义类型不支持\n\t\t\tthrow new IllegalArgumentException(\"Unsupported Number type: \" + targetClass.getName());\n\t\t}\n\n\t\treturn (T) number;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.text.ASCIIStrCache;\nimport cn.hutool.core.text.CharPool;\n\n/**\n * 字符工具类<br>\n * 部分工具来自于Apache Commons系列\n *\n * @author looly\n * @since 4.0.1\n */\npublic class CharUtil implements CharPool {\n\n\t/**\n\t * 是否为ASCII字符，ASCII字符位于0~127之间\n\t *\n\t * <pre>\n\t *   CharUtil.isAscii('a')  = true\n\t *   CharUtil.isAscii('A')  = true\n\t *   CharUtil.isAscii('3')  = true\n\t *   CharUtil.isAscii('-')  = true\n\t *   CharUtil.isAscii('\\n') = true\n\t *   CharUtil.isAscii('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符处\n\t * @return true表示为ASCII字符，ASCII字符位于0~127之间\n\t */\n\tpublic static boolean isAscii(char ch) {\n\t\treturn ch < 128;\n\t}\n\n\t/**\n\t * 是否为可见ASCII字符，可见字符位于32~126之间\n\t *\n\t * <pre>\n\t *   CharUtil.isAsciiPrintable('a')  = true\n\t *   CharUtil.isAsciiPrintable('A')  = true\n\t *   CharUtil.isAsciiPrintable('3')  = true\n\t *   CharUtil.isAsciiPrintable('-')  = true\n\t *   CharUtil.isAsciiPrintable('\\n') = false\n\t *   CharUtil.isAsciiPrintable('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符处\n\t * @return true表示为ASCII可见字符，可见字符位于32~126之间\n\t */\n\tpublic static boolean isAsciiPrintable(char ch) {\n\t\treturn ch >= 32 && ch < 127;\n\t}\n\n\t/**\n\t * 是否为ASCII控制符（不可见字符），控制符位于0~31和127\n\t *\n\t * <pre>\n\t *   CharUtil.isAsciiControl('a')  = false\n\t *   CharUtil.isAsciiControl('A')  = false\n\t *   CharUtil.isAsciiControl('3')  = false\n\t *   CharUtil.isAsciiControl('-')  = false\n\t *   CharUtil.isAsciiControl('\\n') = true\n\t *   CharUtil.isAsciiControl('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为控制符，控制符位于0~31和127\n\t */\n\tpublic static boolean isAsciiControl(final char ch) {\n\t\treturn ch < 32 || ch == 127;\n\t}\n\n\t/**\n\t * 判断是否为字母（包括大写字母和小写字母）<br>\n\t * 字母包括A~Z和a~z\n\t *\n\t * <pre>\n\t *   CharUtil.isLetter('a')  = true\n\t *   CharUtil.isLetter('A')  = true\n\t *   CharUtil.isLetter('3')  = false\n\t *   CharUtil.isLetter('-')  = false\n\t *   CharUtil.isLetter('\\n') = false\n\t *   CharUtil.isLetter('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为字母（包括大写字母和小写字母）字母包括A~Z和a~z\n\t */\n\tpublic static boolean isLetter(char ch) {\n\t\treturn isLetterUpper(ch) || isLetterLower(ch);\n\t}\n\n\t/**\n\t * <p>\n\t * 判断是否为大写字母，大写字母包括A~Z\n\t * </p>\n\t *\n\t * <pre>\n\t *   CharUtil.isLetterUpper('a')  = false\n\t *   CharUtil.isLetterUpper('A')  = true\n\t *   CharUtil.isLetterUpper('3')  = false\n\t *   CharUtil.isLetterUpper('-')  = false\n\t *   CharUtil.isLetterUpper('\\n') = false\n\t *   CharUtil.isLetterUpper('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为大写字母，大写字母包括A~Z\n\t */\n\tpublic static boolean isLetterUpper(final char ch) {\n\t\treturn ch >= 'A' && ch <= 'Z';\n\t}\n\n\t/**\n\t * <p>\n\t * 检查字符是否为小写字母，小写字母指a~z\n\t * </p>\n\t *\n\t * <pre>\n\t *   CharUtil.isLetterLower('a')  = true\n\t *   CharUtil.isLetterLower('A')  = false\n\t *   CharUtil.isLetterLower('3')  = false\n\t *   CharUtil.isLetterLower('-')  = false\n\t *   CharUtil.isLetterLower('\\n') = false\n\t *   CharUtil.isLetterLower('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为小写字母，小写字母指a~z\n\t */\n\tpublic static boolean isLetterLower(final char ch) {\n\t\treturn ch >= 'a' && ch <= 'z';\n\t}\n\n\t/**\n\t * <p>\n\t * 检查是否为数字字符，数字字符指0~9\n\t * </p>\n\t *\n\t * <pre>\n\t *   CharUtil.isNumber('a')  = false\n\t *   CharUtil.isNumber('A')  = false\n\t *   CharUtil.isNumber('3')  = true\n\t *   CharUtil.isNumber('-')  = false\n\t *   CharUtil.isNumber('\\n') = false\n\t *   CharUtil.isNumber('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为数字字符，数字字符指0~9\n\t */\n\tpublic static boolean isNumber(char ch) {\n\t\treturn ch >= '0' && ch <= '9';\n\t}\n\n\t/**\n\t * 是否为16进制规范的字符，判断是否为如下字符\n\t * <pre>\n\t * 1. 0~9\n\t * 2. a~f\n\t * 4. A~F\n\t * </pre>\n\t *\n\t * @param c 字符\n\t * @return 是否为16进制规范的字符\n\t * @since 4.1.5\n\t */\n\tpublic static boolean isHexChar(char c) {\n\t\treturn isNumber(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');\n\t}\n\n\t/**\n\t * 是否为字母或数字，包括A~Z、a~z、0~9\n\t *\n\t * <pre>\n\t *   CharUtil.isLetterOrNumber('a')  = true\n\t *   CharUtil.isLetterOrNumber('A')  = true\n\t *   CharUtil.isLetterOrNumber('3')  = true\n\t *   CharUtil.isLetterOrNumber('-')  = false\n\t *   CharUtil.isLetterOrNumber('\\n') = false\n\t *   CharUtil.isLetterOrNumber('&copy;') = false\n\t * </pre>\n\t *\n\t * @param ch 被检查的字符\n\t * @return true表示为字母或数字，包括A~Z、a~z、0~9\n\t */\n\tpublic static boolean isLetterOrNumber(final char ch) {\n\t\treturn isLetter(ch) || isNumber(ch);\n\t}\n\n\t/**\n\t * 字符转为字符串<br>\n\t * 如果为ASCII字符，使用缓存\n\t *\n\t * @param c 字符\n\t * @return 字符串\n\t * @see ASCIIStrCache#toString(char)\n\t */\n\tpublic static String toString(char c) {\n\t\treturn ASCIIStrCache.toString(c);\n\t}\n\n\t/**\n\t * 给定类名是否为字符类，字符类包括：\n\t *\n\t * <pre>\n\t * Character.class\n\t * char.class\n\t * </pre>\n\t *\n\t * @param clazz 被检查的类\n\t * @return true表示为字符类\n\t */\n\tpublic static boolean isCharClass(Class<?> clazz) {\n\t\treturn clazz == Character.class || clazz == char.class;\n\t}\n\n\t/**\n\t * 给定对象对应的类是否为字符类，字符类包括：\n\t *\n\t * <pre>\n\t * Character.class\n\t * char.class\n\t * </pre>\n\t *\n\t * @param value 被检查的对象\n\t * @return true表示为字符类\n\t */\n\tpublic static boolean isChar(Object value) {\n\t\t//noinspection ConstantConditions\n\t\treturn value instanceof Character || value.getClass() == char.class;\n\t}\n\n\t/**\n\t * 是否空白符<br>\n\t * 空白符包括空格、制表符、全角空格和不间断空格<br>\n\t *\n\t * @param c 字符\n\t * @return 是否空白符\n\t * @see Character#isWhitespace(int)\n\t * @see Character#isSpaceChar(int)\n\t * @since 4.0.10\n\t */\n\tpublic static boolean isBlankChar(char c) {\n\t\treturn isBlankChar((int) c);\n\t}\n\n\t/**\n\t * 是否空白符<br>\n\t * 空白符包括空格、制表符、全角空格和不间断空格<br>\n\t *\n\t * @param c 字符\n\t * @return 是否空白符\n\t * @see Character#isWhitespace(int)\n\t * @see Character#isSpaceChar(int)\n\t * @since 4.0.10\n\t */\n\tpublic static boolean isBlankChar(int c) {\n\t\treturn Character.isWhitespace(c)\n\t\t\t|| Character.isSpaceChar(c)\n\t\t\t|| c == '\\ufeff'\n\t\t\t|| c == '\\u202a'\n\t\t\t|| c == '\\u0000'\n\t\t\t// issue#I5UGSQ，Hangul Filler\n\t\t\t|| c == '\\u3164'\n\t\t\t// Braille Pattern Blank\n\t\t\t|| c == '\\u2800'\n\t\t\t// Zero Width Non-Joiner, ZWNJ\n\t\t\t|| c == '\\u200c'\n\t\t\t// MONGOLIAN VOWEL SEPARATOR\n\t\t\t|| c == '\\u180e';\n\t}\n\n\t/**\n\t * 判断是否为emoji表情符<br>\n\t *\n\t * @param c 字符\n\t * @return 是否为emoji\n\t * @since 4.0.8\n\t */\n\tpublic static boolean isEmoji(char c) {\n\t\t//noinspection ConstantConditions\n\t\treturn false == ((c == 0x0) || //\n\t\t\t(c == 0x9) || //\n\t\t\t(c == 0xA) || //\n\t\t\t(c == 0xD) || //\n\t\t\t((c >= 0x20) && (c <= 0xD7FF)) || //\n\t\t\t((c >= 0xE000) && (c <= 0xFFFD)) || //\n\t\t\t((c >= 0x100000) && (c <= 0x10FFFF)));\n\t}\n\n\t/**\n\t * 是否为Windows或者Linux（Unix）文件分隔符<br>\n\t * Windows平台下分隔符为\\，Linux（Unix）为/\n\t *\n\t * @param c 字符\n\t * @return 是否为Windows或者Linux（Unix）文件分隔符\n\t * @since 4.1.11\n\t */\n\tpublic static boolean isFileSeparator(char c) {\n\t\treturn SLASH == c || BACKSLASH == c;\n\t}\n\n\t/**\n\t * 比较两个字符是否相同\n\t *\n\t * @param c1              字符1\n\t * @param c2              字符2\n\t * @param caseInsensitive 是否忽略大小写\n\t * @return 是否相同\n\t * @since 4.0.3\n\t */\n\tpublic static boolean equals(char c1, char c2, boolean caseInsensitive) {\n\t\tif (caseInsensitive) {\n\t\t\treturn Character.toLowerCase(c1) == Character.toLowerCase(c2);\n\t\t}\n\t\treturn c1 == c2;\n\t}\n\n\t/**\n\t * 获取字符类型\n\t *\n\t * @param c 字符\n\t * @return 字符类型\n\t * @since 5.2.3\n\t */\n\tpublic static int getType(int c) {\n\t\treturn Character.getType(c);\n\t}\n\n\t/**\n\t * 获取给定字符的16进制数值\n\t *\n\t * @param b 字符\n\t * @return 16进制字符\n\t * @since 5.3.1\n\t */\n\tpublic static int digit16(int b) {\n\t\treturn Character.digit(b, 16);\n\t}\n\n\t/**\n\t * 将字母、数字转换为带圈的字符：\n\t * <pre>\n\t *     '1' -》 '①'\n\t *     'A' -》 'Ⓐ'\n\t *     'a' -》 'ⓐ'\n\t * </pre>\n\t * <p>\n\t * 获取带圈数字 /封闭式字母数字 ，从1-20,超过1-20报错\n\t *\n\t * @param c 被转换的字符，如果字符不支持转换，返回原字符\n\t * @return 转换后的字符\n\t * @see <a href=\"https://en.wikipedia.org/wiki/List_of_Unicode_characters#Unicode_symbols\">Unicode_symbols</a>\n\t * @see <a href=\"https://en.wikipedia.org/wiki/Enclosed_Alphanumerics\">Alphanumerics</a>\n\t * @since 5.6.2\n\t */\n\tpublic static char toCloseChar(char c) {\n\t\tint result = c;\n\t\tif (c >= '1' && c <= '9') {\n\t\t\tresult = '①' + c - '1';\n\t\t} else if (c >= 'A' && c <= 'Z') {\n\t\t\tresult = 'Ⓐ' + c - 'A';\n\t\t} else if (c >= 'a' && c <= 'z') {\n\t\t\tresult = 'ⓐ' + c - 'a';\n\t\t}\n\t\treturn (char) result;\n\t}\n\n\t/**\n\t * 将[1-20]数字转换为带圈的字符：\n\t * <pre>\n\t *     1 -》 '①'\n\t *     12 -》 '⑫'\n\t *     20 -》 '⑳'\n\t * </pre>\n\t * 也称作：封闭式字符，英文：Enclosed Alphanumerics\n\t *\n\t * @param number 被转换的数字\n\t * @return 转换后的字符\n\t * @author dazer\n\t * @see <a href=\"https://en.wikipedia.org/wiki/List_of_Unicode_characters#Unicode_symbols\">维基百科wikipedia-Unicode_symbols</a>\n\t * @see <a href=\"https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%88%97%E8%A1%A8\">维基百科wikipedia-Unicode字符列表</a>\n\t * @see <a href=\"https://coolsymbol.com/\">coolsymbol</a>\n\t * @see <a href=\"https://baike.baidu.com/item/%E7%89%B9%E6%AE%8A%E5%AD%97%E7%AC%A6/112715?fr=aladdin\">百度百科 特殊字符</a>\n\t * @since 5.6.2\n\t */\n\tpublic static char toCloseByNumber(int number) {\n\t\tif (number < 1 || number > 20) {\n\t\t\tthrow new IllegalArgumentException(\"Number must be [1-20]\");\n\t\t}\n\t\treturn (char) ('①' + number - 1);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/CharsetUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.io.CharsetDetector;\nimport cn.hutool.core.io.FileUtil;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.charset.UnsupportedCharsetException;\n\n/**\n * 字符集工具类\n *\n * @author xiaoleilu\n */\npublic class CharsetUtil {\n\n\t/**\n\t * ISO-8859-1\n\t */\n\tpublic static final String ISO_8859_1 = \"ISO-8859-1\";\n\t/**\n\t * UTF-8\n\t */\n\tpublic static final String UTF_8 = \"UTF-8\";\n\t/**\n\t * GBK\n\t */\n\tpublic static final String GBK = \"GBK\";\n\n\t/**\n\t * ISO-8859-1\n\t */\n\tpublic static final Charset CHARSET_ISO_8859_1 = StandardCharsets.ISO_8859_1;\n\t/**\n\t * UTF-8\n\t */\n\tpublic static final Charset CHARSET_UTF_8 = StandardCharsets.UTF_8;\n\t/**\n\t * GBK\n\t */\n\tpublic static final Charset CHARSET_GBK;\n\n\tstatic {\n\t\t//避免不支持GBK的系统中运行报错 issue#731\n\t\tCharset _CHARSET_GBK = null;\n\t\ttry {\n\t\t\t_CHARSET_GBK = Charset.forName(GBK);\n\t\t} catch (UnsupportedCharsetException e) {\n\t\t\t//ignore\n\t\t}\n\t\tCHARSET_GBK = _CHARSET_GBK;\n\t}\n\n\t/**\n\t * 转换为Charset对象\n\t *\n\t * @param charsetName 字符集，为空则返回默认字符集\n\t * @return Charset\n\t * @throws UnsupportedCharsetException 编码不支持\n\t */\n\tpublic static Charset charset(String charsetName) throws UnsupportedCharsetException {\n\t\treturn StrUtil.isBlank(charsetName) ? Charset.defaultCharset() : Charset.forName(charsetName);\n\t}\n\n\t/**\n\t * 解析字符串编码为Charset对象，解析失败返回系统默认编码\n\t *\n\t * @param charsetName 字符集，为空则返回默认字符集\n\t * @return Charset\n\t * @since 5.2.6\n\t */\n\tpublic static Charset parse(String charsetName) {\n\t\treturn parse(charsetName, Charset.defaultCharset());\n\t}\n\n\t/**\n\t * 解析字符串编码为Charset对象，解析失败返回默认编码\n\t *\n\t * @param charsetName    字符集，为空则返回默认字符集\n\t * @param defaultCharset 解析失败使用的默认编码\n\t * @return Charset\n\t * @since 5.2.6\n\t */\n\tpublic static Charset parse(String charsetName, Charset defaultCharset) {\n\t\tif (StrUtil.isBlank(charsetName)) {\n\t\t\treturn defaultCharset;\n\t\t}\n\n\t\tCharset result;\n\t\ttry {\n\t\t\tresult = Charset.forName(charsetName);\n\t\t} catch (UnsupportedCharsetException e) {\n\t\t\tresult = defaultCharset;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 转换字符串的字符集编码\n\t *\n\t * @param source      字符串\n\t * @param srcCharset  源字符集，默认ISO-8859-1\n\t * @param destCharset 目标字符集，默认UTF-8\n\t * @return 转换后的字符集\n\t */\n\tpublic static String convert(String source, String srcCharset, String destCharset) {\n\t\treturn convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));\n\t}\n\n\t/**\n\t * 转换字符串的字符集编码<br>\n\t * 当以错误的编码读取为字符串时，打印字符串将出现乱码。<br>\n\t * 此方法用于纠正因读取使用编码错误导致的乱码问题。<br>\n\t * 例如，在Servlet请求中客户端用GBK编码了请求参数，我们使用UTF-8读取到的是乱码，此时，使用此方法即可还原原编码的内容\n\t * <pre>\n\t * 客户端 -》 GBK编码 -》 Servlet容器 -》 UTF-8解码 -》 乱码\n\t * 乱码 -》 UTF-8编码 -》 GBK解码 -》 正确内容\n\t * </pre>\n\t *\n\t * @param source      字符串\n\t * @param srcCharset  源字符集，默认ISO-8859-1\n\t * @param destCharset 目标字符集，默认UTF-8\n\t * @return 转换后的字符集\n\t */\n\tpublic static String convert(String source, Charset srcCharset, Charset destCharset) {\n\t\tif (null == srcCharset) {\n\t\t\tsrcCharset = StandardCharsets.ISO_8859_1;\n\t\t}\n\n\t\tif (null == destCharset) {\n\t\t\tdestCharset = StandardCharsets.UTF_8;\n\t\t}\n\n\t\tif (StrUtil.isBlank(source) || srcCharset.equals(destCharset)) {\n\t\t\treturn source;\n\t\t}\n\t\treturn new String(source.getBytes(srcCharset), destCharset);\n\t}\n\n\t/**\n\t * 转换文件编码<br>\n\t * 此方法用于转换文件编码，读取的文件实际编码必须与指定的srcCharset编码一致，否则导致乱码\n\t *\n\t * @param file        文件\n\t * @param srcCharset  原文件的编码，必须与文件内容的编码保持一致\n\t * @param destCharset 转码后的编码\n\t * @return 被转换编码的文件\n\t * @since 3.1.0\n\t */\n\tpublic static File convert(File file, Charset srcCharset, Charset destCharset) {\n\t\tfinal String str = FileUtil.readString(file, srcCharset);\n\t\treturn FileUtil.writeString(str, file, destCharset);\n\t}\n\n\t/**\n\t * 系统字符集编码，如果是Windows，则默认为GBK编码，否则取 {@link CharsetUtil#defaultCharsetName()}\n\t *\n\t * @return 系统字符集编码\n\t * @see CharsetUtil#defaultCharsetName()\n\t * @since 3.1.2\n\t */\n\tpublic static String systemCharsetName() {\n\t\treturn systemCharset().name();\n\t}\n\n\t/**\n\t * 系统字符集编码，如果是Windows，则默认为GBK编码，否则取 {@link CharsetUtil#defaultCharsetName()}\n\t *\n\t * @return 系统字符集编码\n\t * @see CharsetUtil#defaultCharsetName()\n\t * @since 3.1.2\n\t */\n\tpublic static Charset systemCharset() {\n\t\treturn FileUtil.isWindows() ? CHARSET_GBK : defaultCharset();\n\t}\n\n\t/**\n\t * 系统默认字符集编码\n\t *\n\t * @return 系统字符集编码\n\t */\n\tpublic static String defaultCharsetName() {\n\t\treturn defaultCharset().name();\n\t}\n\n\t/**\n\t * 系统默认字符集编码\n\t *\n\t * @return 系统字符集编码\n\t */\n\tpublic static Charset defaultCharset() {\n\t\treturn Charset.defaultCharset();\n\t}\n\n\t/**\n\t * 探测编码<br>\n\t * 注意：此方法会读取流的一部分，然后关闭流，如重复使用流，请使用使用支持reset方法的流\n\t *\n\t * @param in       流，使用后关闭此流\n\t * @param charsets 需要测试用的编码，null或空使用默认的编码数组\n\t * @return 编码\n\t * @see CharsetDetector#detect(InputStream, Charset...)\n\t * @since 5.7.10\n\t */\n\tpublic static Charset defaultCharset(InputStream in, Charset... charsets) {\n\t\treturn CharsetDetector.detect(in, charsets);\n\t}\n\n\t/**\n\t * 探测编码<br>\n\t * 注意：此方法会读取流的一部分，然后关闭流，如重复使用流，请使用使用支持reset方法的流\n\t *\n\t * @param bufferSize 自定义缓存大小，即每次检查的长度\n\t * @param in         流，使用后关闭此流\n\t * @param charsets   需要测试用的编码，null或空使用默认的编码数组\n\t * @return 编码\n\t * @see CharsetDetector#detect(int, InputStream, Charset...)\n\t * @since 5.7.10\n\t */\n\tpublic static Charset defaultCharset(int bufferSize, InputStream in, Charset... charsets) {\n\t\treturn CharsetDetector.detect(bufferSize, in, charsets);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.convert.BasicType;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.JarClassLoader;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.text.CharPool;\n\nimport java.io.File;\nimport java.lang.reflect.Array;\nimport java.security.AccessController;\nimport java.security.PrivilegedAction;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * {@link ClassLoader}工具类\n *\n * @author Looly\n * @since 3.0.9\n */\npublic class ClassLoaderUtil {\n\n\t/**\n\t * 数组类的结尾符: \"[]\"\n\t */\n\tprivate static final String ARRAY_SUFFIX = \"[]\";\n\t/**\n\t * 内部数组类名前缀: \"[\"\n\t */\n\tprivate static final String INTERNAL_ARRAY_PREFIX = \"[\";\n\t/**\n\t * 内部非原始类型类名前缀: \"[L\"\n\t */\n\tprivate static final String NON_PRIMITIVE_ARRAY_PREFIX = \"[L\";\n\t/**\n\t * 包名分界符: '.'\n\t */\n\tprivate static final char PACKAGE_SEPARATOR = StrUtil.C_DOT;\n\t/**\n\t * 内部类分界符: '$'\n\t */\n\tprivate static final char INNER_CLASS_SEPARATOR = '$';\n\n\t/**\n\t * 原始类型名和其class对应表，例如：int =》 int.class\n\t */\n\tprivate static final Map<String, Class<?>> PRIMITIVE_TYPE_NAME_MAP = new SafeConcurrentHashMap<>(32);\n\n\tstatic {\n\t\tfinal List<Class<?>> primitiveTypes = new ArrayList<>(32);\n\t\t// 加入原始类型\n\t\tprimitiveTypes.addAll(BasicType.PRIMITIVE_WRAPPER_MAP.keySet());\n\t\t// 加入原始类型数组类型\n\t\tprimitiveTypes.add(boolean[].class);\n\t\tprimitiveTypes.add(byte[].class);\n\t\tprimitiveTypes.add(char[].class);\n\t\tprimitiveTypes.add(double[].class);\n\t\tprimitiveTypes.add(float[].class);\n\t\tprimitiveTypes.add(int[].class);\n\t\tprimitiveTypes.add(long[].class);\n\t\tprimitiveTypes.add(short[].class);\n\t\tprimitiveTypes.add(void.class);\n\t\tfor (final Class<?> primitiveType : primitiveTypes) {\n\t\t\tPRIMITIVE_TYPE_NAME_MAP.put(primitiveType.getName(), primitiveType);\n\t\t}\n\t}\n\n\t/**\n\t * 获取当前线程的{@link ClassLoader}\n\t *\n\t * @return 当前线程的class loader\n\t * @see Thread#getContextClassLoader()\n\t */\n\tpublic static ClassLoader getContextClassLoader() {\n\t\tif (System.getSecurityManager() == null) {\n\t\t\treturn Thread.currentThread().getContextClassLoader();\n\t\t} else {\n\t\t\t// 绕开权限检查\n\t\t\treturn AccessController.doPrivileged(\n\t\t\t\t\t(PrivilegedAction<ClassLoader>) () -> Thread.currentThread().getContextClassLoader());\n\t\t}\n\t}\n\n\t/**\n\t * 获取系统{@link ClassLoader}\n\t *\n\t * @return 系统{@link ClassLoader}\n\t * @see ClassLoader#getSystemClassLoader()\n\t * @since 5.7.0\n\t */\n\tpublic static ClassLoader getSystemClassLoader() {\n\t\tif (System.getSecurityManager() == null) {\n\t\t\treturn ClassLoader.getSystemClassLoader();\n\t\t} else {\n\t\t\t// 绕开权限检查\n\t\t\treturn AccessController.doPrivileged(\n\t\t\t\t\t(PrivilegedAction<ClassLoader>) ClassLoader::getSystemClassLoader);\n\t\t}\n\t}\n\n\n\t/**\n\t * 获取{@link ClassLoader}<br>\n\t * 获取顺序如下：<br>\n\t *\n\t * <pre>\n\t * 1、获取当前线程的ContextClassLoader\n\t * 2、获取当前类对应的ClassLoader\n\t * 3、获取系统ClassLoader（{@link ClassLoader#getSystemClassLoader()}）\n\t * </pre>\n\t *\n\t * @return 类加载器\n\t */\n\tpublic static ClassLoader getClassLoader() {\n\t\tClassLoader classLoader = getContextClassLoader();\n\t\tif (classLoader == null) {\n\t\t\tclassLoader = ClassLoaderUtil.class.getClassLoader();\n\t\t\tif (null == classLoader) {\n\t\t\t\tclassLoader = getSystemClassLoader();\n\t\t\t}\n\t\t}\n\t\treturn classLoader;\n\t}\n\n\t// ----------------------------------------------------------------------------------- loadClass\n\n\t/**\n\t * 加载类，通过传入类的字符串，返回其对应的类名，使用默认ClassLoader并初始化类（调用static模块内容和初始化static属性）<br>\n\t * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法，支持以下几类类名的加载：\n\t *\n\t * <pre>\n\t * 1、原始类型，例如：int\n\t * 2、数组类型，例如：int[]、Long[]、String[]\n\t * 3、内部类，例如：java.lang.Thread.State会被转为java.lang.Thread$State加载\n\t * </pre>\n\t *\n\t * @param name 类名\n\t * @return 类名对应的类\n\t * @throws UtilException 包装{@link ClassNotFoundException}，没有类名对应的类时抛出此异常\n\t */\n\tpublic static Class<?> loadClass(String name) throws UtilException {\n\t\treturn loadClass(name, true);\n\t}\n\n\t/**\n\t * 加载类，通过传入类的字符串，返回其对应的类名，使用默认ClassLoader<br>\n\t * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法，支持以下几类类名的加载：\n\t *\n\t * <pre>\n\t * 1、原始类型，例如：int\n\t * 2、数组类型，例如：int[]、Long[]、String[]\n\t * 3、内部类，例如：java.lang.Thread.State会被转为java.lang.Thread$State加载\n\t * </pre>\n\t *\n\t * @param name          类名\n\t * @param isInitialized 是否初始化类（调用static模块内容和初始化static属性）\n\t * @return 类名对应的类\n\t * @throws UtilException 包装{@link ClassNotFoundException}，没有类名对应的类时抛出此异常\n\t */\n\tpublic static Class<?> loadClass(String name, boolean isInitialized) throws UtilException {\n\t\treturn loadClass(name, null, isInitialized);\n\t}\n\n\t/**\n\t * 加载类，通过传入类的字符串，返回其对应的类名<br>\n\t * 此方法支持缓存，第一次被加载的类之后会读取缓存中的类<br>\n\t * 加载失败的原因可能是此类不存在或其关联引用类不存在<br>\n\t * 扩展{@link Class#forName(String, boolean, ClassLoader)}方法，支持以下几类类名的加载：\n\t *\n\t * <pre>\n\t * 1、原始类型，例如：int\n\t * 2、数组类型，例如：int[]、Long[]、String[]\n\t * 3、内部类，例如：java.lang.Thread.State会被转为java.lang.Thread$State加载\n\t * </pre>\n\t *\n\t * @param name          类名\n\t * @param classLoader   {@link ClassLoader}，{@code null} 则使用{@link #getClassLoader()}获取\n\t * @param isInitialized 是否初始化类（调用static模块内容和初始化static属性）\n\t * @return 类名对应的类\n\t * @throws UtilException 包装{@link ClassNotFoundException}，没有类名对应的类时抛出此异常\n\t */\n\tpublic static Class<?> loadClass(String name, ClassLoader classLoader, boolean isInitialized) throws UtilException {\n\t\tAssert.notNull(name, \"Name must not be null\");\n\n\t\t// 自动将包名中的\"/\"替换为\".\"\n\t\tname = name.replace(CharPool.SLASH, CharPool.DOT);\n\t\tif(null == classLoader){\n\t\t\tclassLoader = getClassLoader();\n\t\t}\n\n\t\t// 加载原始类型和缓存中的类\n\t\tClass<?> clazz = loadPrimitiveClass(name);\n\t\tif (clazz == null) {\n\t\t\tclazz = doLoadClass(name, classLoader, isInitialized);\n\t\t}\n\t\treturn clazz;\n\t}\n\n\t/**\n\t * 加载原始类型的类。包括原始类型、原始类型数组和void\n\t *\n\t * @param name 原始类型名，比如 int\n\t * @return 原始类型类\n\t */\n\tpublic static Class<?> loadPrimitiveClass(String name) {\n\t\tClass<?> result = null;\n\t\tif (StrUtil.isNotBlank(name)) {\n\t\t\tname = name.trim();\n\t\t\tif (name.length() <= 8) {\n\t\t\t\tresult = PRIMITIVE_TYPE_NAME_MAP.get(name);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 创建新的{@link JarClassLoader}，并使用此Classloader加载目录下的class文件和jar文件\n\t *\n\t * @param jarOrDir jar文件或者包含jar和class文件的目录\n\t * @return {@link JarClassLoader}\n\t * @since 4.4.2\n\t */\n\tpublic static JarClassLoader getJarClassLoader(File jarOrDir) {\n\t\treturn JarClassLoader.load(jarOrDir);\n\t}\n\n\t/**\n\t * 加载外部类\n\t *\n\t * @param jarOrDir jar文件或者包含jar和class文件的目录\n\t * @param name     类名\n\t * @return 类\n\t * @since 4.4.2\n\t */\n\tpublic static Class<?> loadClass(File jarOrDir, String name) {\n\t\ttry {\n\t\t\treturn getJarClassLoader(jarOrDir).loadClass(name);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------- isPresent\n\n\t/**\n\t * 指定类是否被提供，使用默认ClassLoader<br>\n\t * 通过调用{@link #loadClass(String, ClassLoader, boolean)}方法尝试加载指定类名的类，如果加载失败返回false<br>\n\t * 加载失败的原因可能是此类不存在或其关联引用类不存在\n\t *\n\t * @param className 类名\n\t * @return 是否被提供\n\t */\n\tpublic static boolean isPresent(String className) {\n\t\treturn isPresent(className, null);\n\t}\n\n\t/**\n\t * 指定类是否被提供<br>\n\t * 通过调用{@link #loadClass(String, ClassLoader, boolean)}方法尝试加载指定类名的类，如果加载失败返回false<br>\n\t * 加载失败的原因可能是此类不存在或其关联引用类不存在\n\t *\n\t * @param className   类名\n\t * @param classLoader {@link ClassLoader}\n\t * @return 是否被提供\n\t */\n\tpublic static boolean isPresent(String className, ClassLoader classLoader) {\n\t\ttry {\n\t\t\tloadClass(className, classLoader, false);\n\t\t\treturn true;\n\t\t} catch (Throwable ex) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 加载非原始类类，无缓存\n\t * @param name 类名\n\t * @param classLoader {@link ClassLoader}\n\t * @param isInitialized 是否初始化\n\t * @return 类\n\t */\n\tprivate static Class<?> doLoadClass(String name, ClassLoader classLoader, boolean isInitialized){\n\t\tClass<?> clazz;\n\t\tif (name.endsWith(ARRAY_SUFFIX)) {\n\t\t\t// 对象数组\"java.lang.String[]\"风格\n\t\t\tfinal String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length());\n\t\t\tfinal Class<?> elementClass = loadClass(elementClassName, classLoader, isInitialized);\n\t\t\tclazz = Array.newInstance(elementClass, 0).getClass();\n\t\t} else if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(\";\")) {\n\t\t\t// \"[Ljava.lang.String;\" 风格\n\t\t\tfinal String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1);\n\t\t\tfinal Class<?> elementClass = loadClass(elementName, classLoader, isInitialized);\n\t\t\tclazz = Array.newInstance(elementClass, 0).getClass();\n\t\t} else if (name.startsWith(INTERNAL_ARRAY_PREFIX)) {\n\t\t\t// \"[[I\" 或 \"[[Ljava.lang.String;\" 风格\n\t\t\tfinal String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length());\n\t\t\tfinal Class<?> elementClass = loadClass(elementName, classLoader, isInitialized);\n\t\t\tclazz = Array.newInstance(elementClass, 0).getClass();\n\t\t} else {\n\t\t\t// 加载普通类\n\t\t\tif (null == classLoader) {\n\t\t\t\tclassLoader = getClassLoader();\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tclazz = Class.forName(name, isInitialized, classLoader);\n\t\t\t} catch (ClassNotFoundException ex) {\n\t\t\t\t// 尝试获取内部类，例如java.lang.Thread.State =》java.lang.Thread$State\n\t\t\t\tclazz = tryLoadInnerClass(name, classLoader, isInitialized);\n\t\t\t\tif (null == clazz) {\n\t\t\t\t\tthrow new UtilException(ex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn clazz;\n\t}\n\n\t/**\n\t * 尝试转换并加载内部类，例如java.lang.Thread.State =》java.lang.Thread$State\n\t *\n\t * @param name          类名\n\t * @param classLoader   {@link ClassLoader}，{@code null} 则使用系统默认ClassLoader\n\t * @param isInitialized 是否初始化类（调用static模块内容和初始化static属性）\n\t * @return 类名对应的类\n\t * @since 4.1.20\n\t */\n\tprivate static Class<?> tryLoadInnerClass(String name, ClassLoader classLoader, boolean isInitialized) {\n\t\t// 尝试获取内部类，例如java.lang.Thread.State =》java.lang.Thread$State\n\t\tfinal int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR);\n\t\tif (lastDotIndex > 0) {// 类与内部类的分隔符不能在第一位，因此>0\n\t\t\tfinal String innerClassName = name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1);\n\t\t\ttry {\n\t\t\t\treturn Class.forName(innerClassName, isInitialized, classLoader);\n\t\t\t} catch (ClassNotFoundException ex2) {\n\t\t\t\t// 尝试获取内部类失败时，忽略之。\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\t// ----------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ClassUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.bean.NullWrapperBean;\nimport cn.hutool.core.convert.BasicType;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.ClassScanner;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.Singleton;\n\nimport java.io.IOException;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Modifier;\nimport java.lang.reflect.Type;\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Date;\nimport java.util.Enumeration;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Set;\n\n/**\n * 类工具类 <br>\n *\n * @author xiaoleilu\n */\npublic class ClassUtil {\n\n\t/**\n\t * {@code null}安全的获取对象类型\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 对象，如果为{@code null} 返回{@code null}\n\t * @return 对象类型，提供对象如果为{@code null} 返回{@code null}\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Class<T> getClass(T obj) {\n\t\treturn ((null == obj) ? null : (Class<T>) obj.getClass());\n\t}\n\n\t/**\n\t * 获得外围类<br>\n\t * 返回定义此类或匿名类所在的类，如果类本身是在包中定义的，返回{@code null}\n\t *\n\t * @param clazz 类\n\t * @return 外围类\n\t * @since 4.5.7\n\t */\n\tpublic static Class<?> getEnclosingClass(Class<?> clazz) {\n\t\treturn null == clazz ? null : clazz.getEnclosingClass();\n\t}\n\n\t/**\n\t * 是否为顶层类，即定义在包中的类，而非定义在类中的内部类\n\t *\n\t * @param clazz 类\n\t * @return 是否为顶层类\n\t * @since 4.5.7\n\t */\n\tpublic static boolean isTopLevelClass(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn false;\n\t\t}\n\t\treturn null == getEnclosingClass(clazz);\n\t}\n\n\t/**\n\t * 获取类名\n\t *\n\t * @param obj      获取类名对象\n\t * @param isSimple 是否简单类名，如果为true，返回不带包名的类名\n\t * @return 类名\n\t * @since 3.0.7\n\t */\n\tpublic static String getClassName(Object obj, boolean isSimple) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Class<?> clazz = obj.getClass();\n\t\treturn getClassName(clazz, isSimple);\n\t}\n\n\t/**\n\t * 获取类名<br>\n\t * 类名并不包含“.class”这个扩展名<br>\n\t * 例如：ClassUtil这个类<br>\n\t *\n\t * <pre>\n\t * isSimple为false: \"com.xiaoleilu.hutool.util.ClassUtil\"\n\t * isSimple为true: \"ClassUtil\"\n\t * </pre>\n\t *\n\t * @param clazz    类\n\t * @param isSimple 是否简单类名，如果为true，返回不带包名的类名\n\t * @return 类名\n\t * @since 3.0.7\n\t */\n\tpublic static String getClassName(Class<?> clazz, boolean isSimple) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\treturn isSimple ? clazz.getSimpleName() : clazz.getName();\n\t}\n\n\t/**\n\t * 获取完整类名的短格式如：<br>\n\t * cn.hutool.core.util.StrUtil -》c.h.c.u.StrUtil\n\t *\n\t * @param className 类名\n\t * @return 短格式类名\n\t * @since 4.1.9\n\t */\n\tpublic static String getShortClassName(String className) {\n\t\tfinal List<String> packages = StrUtil.split(className, CharUtil.DOT);\n\t\tif (null == packages || packages.size() < 2) {\n\t\t\treturn className;\n\t\t}\n\n\t\tfinal int size = packages.size();\n\t\tfinal StringBuilder result = StrUtil.builder();\n\t\tresult.append(packages.get(0).charAt(0));\n\t\tfor (int i = 1; i < size - 1; i++) {\n\t\t\tresult.append(CharUtil.DOT).append(packages.get(i).charAt(0));\n\t\t}\n\t\tresult.append(CharUtil.DOT).append(packages.get(size - 1));\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 获得对象数组的类数组\n\t *\n\t * @param objects 对象数组，如果数组中存在{@code null}元素，则此元素被认为是Object类型\n\t * @return 类数组\n\t */\n\tpublic static Class<?>[] getClasses(Object... objects) {\n\t\tClass<?>[] classes = new Class<?>[objects.length];\n\t\tObject obj;\n\t\tfor (int i = 0; i < objects.length; i++) {\n\t\t\tobj = objects[i];\n\t\t\tif (obj instanceof NullWrapperBean) {\n\t\t\t\t// 自定义null值的参数类型\n\t\t\t\tclasses[i] = ((NullWrapperBean<?>) obj).getWrappedClass();\n\t\t\t} else if (null == obj) {\n\t\t\t\tclasses[i] = Object.class;\n\t\t\t} else {\n\t\t\t\tclasses[i] = obj.getClass();\n\t\t\t}\n\t\t}\n\t\treturn classes;\n\t}\n\n\t/**\n\t * 指定类是否与给定的类名相同\n\t *\n\t * @param clazz      类\n\t * @param className  类名，可以是全类名（包含包名），也可以是简单类名（不包含包名）\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 指定类是否与给定的类名相同\n\t * @since 3.0.7\n\t */\n\tpublic static boolean equals(Class<?> clazz, String className, boolean ignoreCase) {\n\t\tif (null == clazz || StrUtil.isBlank(className)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (ignoreCase) {\n\t\t\treturn className.equalsIgnoreCase(clazz.getName()) || className.equalsIgnoreCase(clazz.getSimpleName());\n\t\t} else {\n\t\t\treturn className.equals(clazz.getName()) || className.equals(clazz.getSimpleName());\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Scan classes\n\n\t/**\n\t * 扫描指定包路径下所有包含指定注解的类\n\t *\n\t * @param packageName     包路径\n\t * @param annotationClass 注解类\n\t * @return 类集合\n\t * @see ClassScanner#scanPackageByAnnotation(String, Class)\n\t */\n\tpublic static Set<Class<?>> scanPackageByAnnotation(String packageName, final Class<? extends Annotation> annotationClass) {\n\t\treturn ClassScanner.scanPackageByAnnotation(packageName, annotationClass);\n\t}\n\n\t/**\n\t * 扫描指定包路径下所有指定类或接口的子类或实现类\n\t *\n\t * @param packageName 包路径\n\t * @param superClass  父类或接口\n\t * @return 类集合\n\t * @see ClassScanner#scanPackageBySuper(String, Class)\n\t */\n\tpublic static Set<Class<?>> scanPackageBySuper(String packageName, final Class<?> superClass) {\n\t\treturn ClassScanner.scanPackageBySuper(packageName, superClass);\n\t}\n\n\t/**\n\t * 扫描该包路径下所有class文件\n\t *\n\t * @return 类集合\n\t * @see ClassScanner#scanPackage()\n\t */\n\tpublic static Set<Class<?>> scanPackage() {\n\t\treturn ClassScanner.scanPackage();\n\t}\n\n\t/**\n\t * 扫描该包路径下所有class文件\n\t *\n\t * @param packageName 包路径 com | com. | com.abs | com.abs.\n\t * @return 类集合\n\t * @see ClassScanner#scanPackage(String)\n\t */\n\tpublic static Set<Class<?>> scanPackage(String packageName) {\n\t\treturn ClassScanner.scanPackage(packageName);\n\t}\n\n\t/**\n\t * 扫描包路径下满足class过滤器条件的所有class文件，<br>\n\t * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException<br>\n\t * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理,有可能是一个不完善的地方，以后需要进行修改<br>\n\t *\n\t * @param packageName 包路径 com | com. | com.abs | com.abs.\n\t * @param classFilter class过滤器，过滤掉不需要的class\n\t * @return 类集合\n\t */\n\tpublic static Set<Class<?>> scanPackage(String packageName, Filter<Class<?>> classFilter) {\n\t\treturn ClassScanner.scanPackage(packageName, classFilter);\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Method\n\n\t/**\n\t * 获得指定类中的Public方法名<br>\n\t * 去重重载的方法\n\t *\n\t * @param clazz 类\n\t * @return 方法名Set\n\t */\n\tpublic static Set<String> getPublicMethodNames(Class<?> clazz) {\n\t\treturn ReflectUtil.getPublicMethodNames(clazz);\n\t}\n\n\t/**\n\t * 获得本类及其父类所有Public方法\n\t *\n\t * @param clazz 查找方法的类\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static Method[] getPublicMethods(Class<?> clazz) {\n\t\treturn ReflectUtil.getPublicMethods(clazz);\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表\n\t *\n\t * @param clazz  查找方法的类\n\t * @param filter 过滤器\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, Filter<Method> filter) {\n\t\treturn ReflectUtil.getPublicMethods(clazz, filter);\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表\n\t *\n\t * @param clazz          查找方法的类\n\t * @param excludeMethods 不包括的方法\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, Method... excludeMethods) {\n\t\treturn ReflectUtil.getPublicMethods(clazz, excludeMethods);\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表\n\t *\n\t * @param clazz              查找方法的类\n\t * @param excludeMethodNames 不包括的方法名列表\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, String... excludeMethodNames) {\n\t\treturn ReflectUtil.getPublicMethods(clazz, excludeMethodNames);\n\t}\n\n\t/**\n\t * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回{@code null}\n\t *\n\t * @param clazz      类\n\t * @param methodName 方法名\n\t * @param paramTypes 参数类型\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t */\n\tpublic static Method getPublicMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) throws SecurityException {\n\t\treturn ReflectUtil.getPublicMethod(clazz, methodName, paramTypes);\n\t}\n\n\t/**\n\t * 获得指定类中的Public方法名<br>\n\t * 去重重载的方法\n\t *\n\t * @param clazz 类\n\t * @return 方法名Set\n\t */\n\tpublic static Set<String> getDeclaredMethodNames(Class<?> clazz) {\n\t\treturn ReflectUtil.getMethodNames(clazz);\n\t}\n\n\t/**\n\t * 获得声明的所有方法，包括本类及其父类和接口的所有方法和Object类的方法\n\t *\n\t * @param clazz 类\n\t * @return 方法数组\n\t */\n\tpublic static Method[] getDeclaredMethods(Class<?> clazz) {\n\t\treturn ReflectUtil.getMethods(clazz);\n\t}\n\n\t/**\n\t * 查找指定对象中的所有方法（包括非public方法），也包括父对象和Object类的方法\n\t *\n\t * @param obj        被查找的对象\n\t * @param methodName 方法名\n\t * @param args       参数\n\t * @return 方法\n\t * @throws SecurityException 无访问权限抛出异常\n\t */\n\tpublic static Method getDeclaredMethodOfObj(Object obj, String methodName, Object... args) throws SecurityException {\n\t\treturn getDeclaredMethod(obj.getClass(), methodName, getClasses(args));\n\t}\n\n\t/**\n\t * 查找指定类中的所有方法（包括非public方法），也包括父类和Object类的方法 找不到方法会返回{@code null}\n\t *\n\t * @param clazz          被查找的类\n\t * @param methodName     方法名\n\t * @param parameterTypes 参数类型\n\t * @return 方法\n\t * @throws SecurityException 无访问权限抛出异常\n\t */\n\tpublic static Method getDeclaredMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws SecurityException {\n\t\treturn ReflectUtil.getMethod(clazz, methodName, parameterTypes);\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Field\n\n\t/**\n\t * 查找指定类中的所有字段（包括非public字段）， 字段不存在则返回{@code null}\n\t *\n\t * @param clazz     被查找字段的类\n\t * @param fieldName 字段名\n\t * @return 字段\n\t * @throws SecurityException 安全异常\n\t */\n\tpublic static Field getDeclaredField(Class<?> clazz, String fieldName) throws SecurityException {\n\t\tif (null == clazz || StrUtil.isBlank(fieldName)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn clazz.getDeclaredField(fieldName);\n\t\t} catch (NoSuchFieldException e) {\n\t\t\t// e.printStackTrace();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 查找指定类中的所有字段（包括非public字段)\n\t *\n\t * @param clazz 被查找字段的类\n\t * @return 字段\n\t * @throws SecurityException 安全异常\n\t */\n\tpublic static Field[] getDeclaredFields(Class<?> clazz) throws SecurityException {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\treturn clazz.getDeclaredFields();\n\t}\n\n\t// ----------------------------------------------------------------------------------------- Classpath\n\n\t/**\n\t * 获得ClassPath，不解码路径中的特殊字符（例如空格和中文）\n\t *\n\t * @return ClassPath集合\n\t */\n\tpublic static Set<String> getClassPathResources() {\n\t\treturn getClassPathResources(false);\n\t}\n\n\t/**\n\t * 获得ClassPath\n\t *\n\t * @param isDecode 是否解码路径中的特殊字符（例如空格和中文）\n\t * @return ClassPath集合\n\t * @since 4.0.11\n\t */\n\tpublic static Set<String> getClassPathResources(boolean isDecode) {\n\t\treturn getClassPaths(StrUtil.EMPTY, isDecode);\n\t}\n\n\t/**\n\t * 获得ClassPath，不解码路径中的特殊字符（例如空格和中文）\n\t *\n\t * @param packageName 包名称\n\t * @return ClassPath路径字符串集合\n\t */\n\tpublic static Set<String> getClassPaths(String packageName) {\n\t\treturn getClassPaths(packageName, false);\n\t}\n\n\t/**\n\t * 获得ClassPath\n\t *\n\t * @param packageName 包名称\n\t * @param isDecode    是否解码路径中的特殊字符（例如空格和中文）\n\t * @return ClassPath路径字符串集合\n\t * @since 4.0.11\n\t */\n\tpublic static Set<String> getClassPaths(String packageName, boolean isDecode) {\n\t\tString packagePath = packageName.replace(StrUtil.DOT, StrUtil.SLASH);\n\t\tEnumeration<URL> resources;\n\t\ttry {\n\t\t\tresources = getClassLoader().getResources(packagePath);\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e, \"Loading classPath [{}] error!\", packagePath);\n\t\t}\n\t\tfinal Set<String> paths = new HashSet<>();\n\t\tString path;\n\t\twhile (resources.hasMoreElements()) {\n\t\t\tpath = resources.nextElement().getPath();\n\t\t\tpaths.add(isDecode ? URLUtil.decode(path, CharsetUtil.systemCharsetName()) : path);\n\t\t}\n\t\treturn paths;\n\t}\n\n\t/**\n\t * 获得ClassPath，将编码后的中文路径解码为原字符<br>\n\t * 这个ClassPath路径会文件路径被标准化处理\n\t *\n\t * @return ClassPath\n\t */\n\tpublic static String getClassPath() {\n\t\treturn getClassPath(false);\n\t}\n\n\t/**\n\t * 获得ClassPath，这个ClassPath路径会文件路径被标准化处理\n\t *\n\t * @param isEncoded 是否编码路径中的中文\n\t * @return ClassPath\n\t * @since 3.2.1\n\t */\n\tpublic static String getClassPath(boolean isEncoded) {\n\t\tfinal URL classPathURL = getClassPathURL();\n\t\tString url = isEncoded ? classPathURL.getPath() : URLUtil.getDecodedPath(classPathURL);\n\t\treturn FileUtil.normalize(url);\n\t}\n\n\t/**\n\t * 获得ClassPath URL\n\t *\n\t * @return ClassPath URL\n\t */\n\tpublic static URL getClassPathURL() {\n\t\treturn getResourceURL(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 获得资源的URL<br>\n\t * 路径用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a/db.config\n\t * spring/xml/test.xml\n\t * </pre>\n\t *\n\t * @param resource 资源（相对Classpath的路径）\n\t * @return 资源URL\n\t * @see ResourceUtil#getResource(String)\n\t */\n\tpublic static URL getResourceURL(String resource) throws IORuntimeException {\n\t\treturn ResourceUtil.getResource(resource);\n\t}\n\n\t/**\n\t * 获取指定路径下的资源列表<br>\n\t * 路径格式必须为目录格式,用/分隔，例如:\n\t *\n\t * <pre>\n\t * config/a\n\t * spring/xml\n\t * </pre>\n\t *\n\t * @param resource 资源路径\n\t * @return 资源列表\n\t * @see ResourceUtil#getResources(String)\n\t */\n\tpublic static List<URL> getResources(String resource) {\n\t\treturn ResourceUtil.getResources(resource);\n\t}\n\n\t/**\n\t * 获得资源相对路径对应的URL\n\t *\n\t * @param resource  资源相对路径\n\t * @param baseClass 基准Class，获得的相对路径相对于此Class所在路径，如果为{@code null}则相对ClassPath\n\t * @return {@link URL}\n\t * @see ResourceUtil#getResource(String, Class)\n\t */\n\tpublic static URL getResourceUrl(String resource, Class<?> baseClass) {\n\t\treturn ResourceUtil.getResource(resource, baseClass);\n\t}\n\n\t/**\n\t * @return 获得Java ClassPath路径，不包括 jre\n\t */\n\tpublic static String[] getJavaClassPaths() {\n\t\treturn System.getProperty(\"java.class.path\").split(System.getProperty(\"path.separator\"));\n\t}\n\n\t/**\n\t * 获取当前线程的{@link ClassLoader}\n\t *\n\t * @return 当前线程的class loader\n\t * @see ClassLoaderUtil#getClassLoader()\n\t */\n\tpublic static ClassLoader getContextClassLoader() {\n\t\treturn ClassLoaderUtil.getContextClassLoader();\n\t}\n\n\t/**\n\t * 获取{@link ClassLoader}<br>\n\t * 获取顺序如下：<br>\n\t *\n\t * <pre>\n\t * 1、获取当前线程的ContextClassLoader\n\t * 2、获取{@link ClassLoaderUtil}类对应的ClassLoader\n\t * 3、获取系统ClassLoader（{@link ClassLoader#getSystemClassLoader()}）\n\t * </pre>\n\t *\n\t * @return 类加载器\n\t */\n\tpublic static ClassLoader getClassLoader() {\n\t\treturn ClassLoaderUtil.getClassLoader();\n\t}\n\n\t/**\n\t * 比较判断types1和types2两组类，如果types1中所有的类都与types2对应位置的类相同，或者是其父类或接口，则返回{@code true}\n\t *\n\t * @param types1 类组1\n\t * @param types2 类组2\n\t * @return 是否相同、父类或接口\n\t */\n\tpublic static boolean isAllAssignableFrom(Class<?>[] types1, Class<?>[] types2) {\n\t\tif (ArrayUtil.isEmpty(types1) && ArrayUtil.isEmpty(types2)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (null == types1 || null == types2) {\n\t\t\t// 任何一个为null不相等（之前已判断两个都为null的情况）\n\t\t\treturn false;\n\t\t}\n\t\tif (types1.length != types2.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\tClass<?> type1;\n\t\tClass<?> type2;\n\t\tfor (int i = 0; i < types1.length; i++) {\n\t\t\ttype1 = types1[i];\n\t\t\ttype2 = types2[i];\n\t\t\tif (isBasicType(type1) && isBasicType(type2)) {\n\t\t\t\t// 原始类型和包装类型存在不一致情况\n\t\t\t\tif (BasicType.unWrap(type1) != BasicType.unWrap(type2)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else if (false == type1.isAssignableFrom(type2)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 加载类\n\t *\n\t * @param <T>           对象类型\n\t * @param className     类名\n\t * @param isInitialized 是否初始化\n\t * @return 类\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Class<T> loadClass(String className, boolean isInitialized) {\n\t\treturn (Class<T>) ClassLoaderUtil.loadClass(className, isInitialized);\n\t}\n\n\t/**\n\t * 加载类并初始化\n\t *\n\t * @param <T>       对象类型\n\t * @param className 类名\n\t * @return 类\n\t */\n\tpublic static <T> Class<T> loadClass(String className) {\n\t\treturn loadClass(className, true);\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------- Invoke start\n\n\t/**\n\t * 执行方法<br>\n\t * 可执行Private方法，也可执行static方法<br>\n\t * 执行非static方法时，必须满足对象有默认构造方法<br>\n\t * 非单例模式，如果是非静态方法，每次创建一个新对象\n\t *\n\t * @param <T>                     对象类型\n\t * @param classNameWithMethodName 类名和方法名表达式，类名与方法名用{@code .}或{@code #}连接 例如：com.xiaoleilu.hutool.StrUtil.isEmpty 或 com.xiaoleilu.hutool.StrUtil#isEmpty\n\t * @param args                    参数，必须严格对应指定方法的参数类型和数量\n\t * @return 返回结果\n\t */\n\tpublic static <T> T invoke(String classNameWithMethodName, Object[] args) {\n\t\treturn invoke(classNameWithMethodName, false, args);\n\t}\n\n\t/**\n\t * 执行方法<br>\n\t * 可执行Private方法，也可执行static方法<br>\n\t * 执行非static方法时，必须满足对象有默认构造方法<br>\n\t *\n\t * @param <T>                     对象类型\n\t * @param classNameWithMethodName 类名和方法名表达式，例如：com.xiaoleilu.hutool.StrUtil#isEmpty或com.xiaoleilu.hutool.StrUtil.isEmpty\n\t * @param isSingleton             是否为单例对象，如果此参数为false，每次执行方法时创建一个新对象\n\t * @param args                    参数，必须严格对应指定方法的参数类型和数量\n\t * @return 返回结果\n\t */\n\tpublic static <T> T invoke(String classNameWithMethodName, boolean isSingleton, Object... args) {\n\t\tif (StrUtil.isBlank(classNameWithMethodName)) {\n\t\t\tthrow new UtilException(\"Blank classNameDotMethodName!\");\n\t\t}\n\n\t\tint splitIndex = classNameWithMethodName.lastIndexOf('#');\n\t\tif (splitIndex <= 0) {\n\t\t\tsplitIndex = classNameWithMethodName.lastIndexOf('.');\n\t\t}\n\t\tif (splitIndex <= 0) {\n\t\t\tthrow new UtilException(\"Invalid classNameWithMethodName [{}]!\", classNameWithMethodName);\n\t\t}\n\n\t\tfinal String className = classNameWithMethodName.substring(0, splitIndex);\n\t\tfinal String methodName = classNameWithMethodName.substring(splitIndex + 1);\n\n\t\treturn invoke(className, methodName, isSingleton, args);\n\t}\n\n\t/**\n\t * 执行方法<br>\n\t * 可执行Private方法，也可执行static方法<br>\n\t * 执行非static方法时，必须满足对象有默认构造方法<br>\n\t * 非单例模式，如果是非静态方法，每次创建一个新对象\n\t *\n\t * @param <T>        对象类型\n\t * @param className  类名，完整类路径\n\t * @param methodName 方法名\n\t * @param args       参数，必须严格对应指定方法的参数类型和数量\n\t * @return 返回结果\n\t */\n\tpublic static <T> T invoke(String className, String methodName, Object[] args) {\n\t\treturn invoke(className, methodName, false, args);\n\t}\n\n\t/**\n\t * 执行方法<br>\n\t * 可执行Private方法，也可执行static方法<br>\n\t * 执行非static方法时，必须满足对象有默认构造方法<br>\n\t *\n\t * @param <T>         对象类型\n\t * @param className   类名，完整类路径\n\t * @param methodName  方法名\n\t * @param isSingleton 是否为单例对象，如果此参数为false，每次执行方法时创建一个新对象\n\t * @param args        参数，必须严格对应指定方法的参数类型和数量\n\t * @return 返回结果\n\t */\n\tpublic static <T> T invoke(String className, String methodName, boolean isSingleton, Object... args) {\n\t\tClass<Object> clazz = loadClass(className);\n\t\ttry {\n\t\t\tfinal Method method = getDeclaredMethod(clazz, methodName, getClasses(args));\n\t\t\tif (null == method) {\n\t\t\t\tthrow new NoSuchMethodException(StrUtil.format(\"No such method: [{}]\", methodName));\n\t\t\t}\n\t\t\tif (isStatic(method)) {\n\t\t\t\treturn ReflectUtil.invoke(null, method, args);\n\t\t\t} else {\n\t\t\t\treturn ReflectUtil.invoke(isSingleton ? Singleton.get(clazz) : clazz.newInstance(), method, args);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------- Invoke end\n\n\t/**\n\t * 是否为包装类型\n\t *\n\t * @param clazz 类\n\t * @return 是否为包装类型\n\t */\n\tpublic static boolean isPrimitiveWrapper(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn false;\n\t\t}\n\t\treturn BasicType.WRAPPER_PRIMITIVE_MAP.containsKey(clazz);\n\t}\n\n\t/**\n\t * 是否为基本类型（包括包装类和原始类）\n\t *\n\t * @param clazz 类\n\t * @return 是否为基本类型\n\t */\n\tpublic static boolean isBasicType(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn false;\n\t\t}\n\t\treturn (clazz.isPrimitive() || isPrimitiveWrapper(clazz));\n\t}\n\n\t/**\n\t * 是否简单值类型或简单值类型的数组<br>\n\t * 包括：原始类型,、String、other CharSequence, a Number, a Date, a URI, a URL, a Locale or a Class及其数组\n\t *\n\t * @param clazz 属性类\n\t * @return 是否简单值类型或简单值类型的数组\n\t */\n\tpublic static boolean isSimpleTypeOrArray(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn false;\n\t\t}\n\t\treturn isSimpleValueType(clazz) || (clazz.isArray() && isSimpleValueType(clazz.getComponentType()));\n\t}\n\n\t/**\n\t * 是否为简单值类型<br>\n\t * 包括：\n\t * <pre>\n\t *     原始类型\n\t *     String、other CharSequence\n\t *     Number\n\t *     Date\n\t *     URI\n\t *     URL\n\t *     Locale\n\t *     Class\n\t * </pre>\n\t *\n\t * @param clazz 类\n\t * @return 是否为简单值类型\n\t */\n\tpublic static boolean isSimpleValueType(Class<?> clazz) {\n\t\treturn isBasicType(clazz) //\n\t\t\t\t|| clazz.isEnum() //\n\t\t\t\t|| CharSequence.class.isAssignableFrom(clazz) //\n\t\t\t\t|| Number.class.isAssignableFrom(clazz) //\n\t\t\t\t|| Date.class.isAssignableFrom(clazz) //\n\t\t\t\t|| clazz.equals(URI.class) //\n\t\t\t\t|| clazz.equals(URL.class) //\n\t\t\t\t|| clazz.equals(Locale.class) //\n\t\t\t\t|| clazz.equals(Class.class)//\n\t\t\t\t// jdk8 date object\n\t\t\t\t|| TemporalAccessor.class.isAssignableFrom(clazz); //\n\t}\n\n\t/**\n\t * 检查目标类是否可以从原类转化<br>\n\t * 转化包括：<br>\n\t * 1、原类是对象，目标类型是原类型实现的接口<br>\n\t * 2、目标类型是原类型的父类<br>\n\t * 3、两者是原始类型或者包装类型（相互转换）\n\t *\n\t * @param targetType 目标类型\n\t * @param sourceType 原类型\n\t * @return 是否可转化\n\t */\n\tpublic static boolean isAssignable(Class<?> targetType, Class<?> sourceType) {\n\t\tif (null == targetType || null == sourceType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 对象类型\n\t\tif (targetType.isAssignableFrom(sourceType)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// 基本类型\n\t\tif (targetType.isPrimitive()) {\n\t\t\t// 原始类型\n\t\t\tClass<?> resolvedPrimitive = BasicType.WRAPPER_PRIMITIVE_MAP.get(sourceType);\n\t\t\treturn targetType.equals(resolvedPrimitive);\n\t\t} else {\n\t\t\t// 包装类型\n\t\t\tClass<?> resolvedWrapper = BasicType.PRIMITIVE_WRAPPER_MAP.get(sourceType);\n\t\t\treturn resolvedWrapper != null && targetType.isAssignableFrom(resolvedWrapper);\n\t\t}\n\t}\n\n\t/**\n\t * 指定类是否为Public\n\t *\n\t * @param clazz 类\n\t * @return 是否为public\n\t */\n\tpublic static boolean isPublic(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\tthrow new NullPointerException(\"Class to provided is null.\");\n\t\t}\n\t\treturn Modifier.isPublic(clazz.getModifiers());\n\t}\n\n\t/**\n\t * 指定方法是否为Public\n\t *\n\t * @param method 方法\n\t * @return 是否为public\n\t */\n\tpublic static boolean isPublic(Method method) {\n\t\tAssert.notNull(method, \"Method to provided is null.\");\n\t\treturn Modifier.isPublic(method.getModifiers());\n\t}\n\n\t/**\n\t * 指定类是否为非public\n\t *\n\t * @param clazz 类\n\t * @return 是否为非public\n\t */\n\tpublic static boolean isNotPublic(Class<?> clazz) {\n\t\treturn false == isPublic(clazz);\n\t}\n\n\t/**\n\t * 指定方法是否为非public\n\t *\n\t * @param method 方法\n\t * @return 是否为非public\n\t */\n\tpublic static boolean isNotPublic(Method method) {\n\t\treturn false == isPublic(method);\n\t}\n\n\t/**\n\t * 是否为静态方法\n\t *\n\t * @param method 方法\n\t * @return 是否为静态方法\n\t */\n\tpublic static boolean isStatic(Method method) {\n\t\tAssert.notNull(method, \"Method to provided is null.\");\n\t\treturn Modifier.isStatic(method.getModifiers());\n\t}\n\n\t/**\n\t * 设置方法为可访问\n\t *\n\t * @param method 方法\n\t * @return 方法\n\t */\n\tpublic static Method setAccessible(Method method) {\n\t\tif (null != method && false == method.isAccessible()) {\n\t\t\tmethod.setAccessible(true);\n\t\t}\n\t\treturn method;\n\t}\n\n\t/**\n\t * 是否为抽象类\n\t *\n\t * @param clazz 类\n\t * @return 是否为抽象类\n\t */\n\tpublic static boolean isAbstract(Class<?> clazz) {\n\t\treturn Modifier.isAbstract(clazz.getModifiers());\n\t}\n\n\t/**\n\t * 是否为标准的类<br>\n\t * 这个类必须：\n\t *\n\t * <pre>\n\t * 1、非接口\n\t * 2、非抽象类\n\t * 3、非Enum枚举\n\t * 4、非数组\n\t * 5、非注解\n\t * 6、非原始类型（int, long等）\n\t * </pre>\n\t *\n\t * @param clazz 类\n\t * @return 是否为标准类\n\t */\n\tpublic static boolean isNormalClass(Class<?> clazz) {\n\t\treturn null != clazz //\n\t\t\t\t&& false == clazz.isInterface() //\n\t\t\t\t&& false == isAbstract(clazz) //\n\t\t\t\t&& false == clazz.isEnum() //\n\t\t\t\t&& false == clazz.isArray() //\n\t\t\t\t&& false == clazz.isAnnotation() //\n\t\t\t\t&& false == clazz.isSynthetic() //\n\t\t\t\t&& false == clazz.isPrimitive()//\n \t\t\t\t// issue#3965 String有isEmpty方法，但是不能被当作bean\n\t\t\t\t&& clazz != String.class;//\n\t}\n\n\t/**\n\t * 判断类是否为枚举类型\n\t *\n\t * @param clazz 类\n\t * @return 是否为枚举类型\n\t * @since 3.2.0\n\t */\n\tpublic static boolean isEnum(Class<?> clazz) {\n\t\treturn null != clazz && clazz.isEnum();\n\t}\n\n\t/**\n\t * 获得给定类的第一个泛型参数\n\t *\n\t * @param clazz 被检查的类，必须是已经确定泛型类型的类\n\t * @return {@link Class}\n\t */\n\tpublic static Class<?> getTypeArgument(Class<?> clazz) {\n\t\treturn getTypeArgument(clazz, 0);\n\t}\n\n\t/**\n\t * 获得给定类的泛型参数\n\t *\n\t * @param clazz 被检查的类，必须是已经确定泛型类型的类\n\t * @param index 泛型类型的索引号，即第几个泛型类型\n\t * @return {@link Class}\n\t */\n\tpublic static Class<?> getTypeArgument(Class<?> clazz, int index) {\n\t\tfinal Type argumentType = TypeUtil.getTypeArgument(clazz, index);\n\t\treturn TypeUtil.getClass(argumentType);\n\t}\n\n\t/**\n\t * 获得给定类所在包的名称<br>\n\t * 例如：<br>\n\t * com.xiaoleilu.hutool.util.ClassUtil =》 com.xiaoleilu.hutool.util\n\t *\n\t * @param clazz 类\n\t * @return 包名\n\t */\n\tpublic static String getPackage(Class<?> clazz) {\n\t\tif (clazz == null) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tfinal String className = clazz.getName();\n\t\tint packageEndIndex = className.lastIndexOf(StrUtil.DOT);\n\t\tif (packageEndIndex == -1) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn className.substring(0, packageEndIndex);\n\t}\n\n\t/**\n\t * 获得给定类所在包的路径<br>\n\t * 例如：<br>\n\t * com.xiaoleilu.hutool.util.ClassUtil =》 com/xiaoleilu/hutool/util\n\t *\n\t * @param clazz 类\n\t * @return 包名\n\t */\n\tpublic static String getPackagePath(Class<?> clazz) {\n\t\treturn getPackage(clazz).replace(StrUtil.C_DOT, StrUtil.C_SLASH);\n\t}\n\n\t/**\n\t * 获取指定类型分的默认值<br>\n\t * 默认值规则为：\n\t *\n\t * <pre>\n\t * 1、如果为原始类型，返回0\n\t * 2、非原始类型返回{@code null}\n\t * </pre>\n\t *\n\t * @param clazz 类\n\t * @return 默认值\n\t * @since 3.0.8\n\t */\n\tpublic static Object getDefaultValue(Class<?> clazz) {\n\t\t// 原始类型\n\t\tif (clazz.isPrimitive()) {\n\t\t\treturn getPrimitiveDefaultValue(clazz);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定原始类型分的默认值<br>\n\t * 默认值规则为：\n\t *\n\t * <pre>\n\t * 1、如果为原始类型，返回0\n\t * 2、非原始类型返回{@code null}\n\t * </pre>\n\t *\n\t * @param clazz 类\n\t * @return 默认值\n\t * @since 5.8.0\n\t */\n\tpublic static Object getPrimitiveDefaultValue(Class<?> clazz) {\n\t\tif (long.class == clazz) {\n\t\t\treturn 0L;\n\t\t} else if (int.class == clazz) {\n\t\t\treturn 0;\n\t\t} else if (short.class == clazz) {\n\t\t\treturn (short) 0;\n\t\t} else if (char.class == clazz) {\n\t\t\treturn (char) 0;\n\t\t} else if (byte.class == clazz) {\n\t\t\treturn (byte) 0;\n\t\t} else if (double.class == clazz) {\n\t\t\treturn 0D;\n\t\t} else if (float.class == clazz) {\n\t\t\treturn 0f;\n\t\t} else if (boolean.class == clazz) {\n\t\t\treturn false;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得默认值列表\n\t *\n\t * @param classes 值类型\n\t * @return 默认值列表\n\t * @since 3.0.9\n\t */\n\tpublic static Object[] getDefaultValues(Class<?>... classes) {\n\t\tfinal Object[] values = new Object[classes.length];\n\t\tfor (int i = 0; i < classes.length; i++) {\n\t\t\tvalues[i] = getDefaultValue(classes[i]);\n\t\t}\n\t\treturn values;\n\t}\n\n\t/**\n\t * 是否为JDK中定义的类或接口，判断依据：\n\t *\n\t * <pre>\n\t * 1、以java.、javax.开头的包名\n\t * 2、ClassLoader为null\n\t * </pre>\n\t *\n\t * @param clazz 被检查的类\n\t * @return 是否为JDK中定义的类或接口\n\t * @since 4.6.5\n\t */\n\tpublic static boolean isJdkClass(Class<?> clazz) {\n\t\tfinal Package objectPackage = clazz.getPackage();\n\t\tif (null == objectPackage) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal String objectPackageName = objectPackage.getName();\n\t\treturn objectPackageName.startsWith(\"java.\") //\n\t\t\t\t|| objectPackageName.startsWith(\"javax.\") //\n\t\t\t\t|| clazz.getClassLoader() == null;\n\t}\n\n\t/**\n\t * 获取class类路径URL, 不管是否在jar包中都会返回文件夹的路径<br>\n\t * class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>\n\t * jdk中的类不能使用此方法\n\t *\n\t * @param clazz 类\n\t * @return URL\n\t * @since 5.2.4\n\t */\n\tpublic static URL getLocation(Class<?> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\treturn clazz.getProtectionDomain().getCodeSource().getLocation();\n\t}\n\n\t/**\n\t * 获取class类路径, 不管是否在jar包中都会返回文件夹的路径<br>\n\t * class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>\n\t * jdk中的类不能使用此方法\n\t *\n\t * @param clazz 类\n\t * @return class路径\n\t * @since 5.2.4\n\t */\n\tpublic static String getLocationPath(Class<?> clazz) {\n\t\tfinal URL location = getLocation(clazz);\n\t\tif (null == location) {\n\t\t\treturn null;\n\t\t}\n\t\treturn location.getPath();\n\t}\n\n\t/**\n\t * 是否为抽象类或接口\n\t *\n\t * @param clazz 类\n\t * @return 是否为抽象类或接口\n\t * @since 5.8.2\n\t */\n\tpublic static boolean isAbstractOrInterface(Class<?> clazz) {\n\t\treturn isAbstract(clazz) || isInterface(clazz);\n\t}\n\n\t/**\n\t * 是否为接口\n\t *\n\t * @param clazz 类\n\t * @return 是否为接口\n\t * @since 5.8.2\n\t */\n\tpublic static boolean isInterface(Class<?> clazz) {\n\t\treturn clazz.isInterface();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/CoordinateUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport java.io.Serializable;\nimport java.util.Objects;\n\n/**\n * 坐标系转换相关工具类，主流坐标系包括：<br>\n * <ul>\n *     <li>WGS84坐标系：即地球坐标系，中国外谷歌地图</li>\n *     <li>GCJ02坐标系：即火星坐标系，高德、腾讯、阿里等使用</li>\n *     <li>BD09坐标系：即百度坐标系，GCJ02坐标系经加密后的坐标系。百度、搜狗等使用</li>\n * </ul>\n * <p>\n * 坐标转换相关参考: https://tool.lu/coordinate/<br>\n * 参考：https://github.com/JourWon/coordinate-transform\n *\n * @author hongzhe.qin(qin462328037at163.com), looly\n * @since 5.7.16\n */\npublic class CoordinateUtil {\n\n\t/**\n\t * 坐标转换参数：(火星坐标系与百度坐标系转换的中间量)\n\t */\n\tpublic static final double X_PI = 3.1415926535897932384626433832795 * 3000.0 / 180.0;\n\n\t/**\n\t * 坐标转换参数：π\n\t */\n\tpublic static final double PI = 3.1415926535897932384626433832795D;\n\n\t/**\n\t * 地球半径（Krasovsky 1940）\n\t */\n\tpublic static final double RADIUS = 6378245.0D;\n\n\t/**\n\t * 修正参数（偏率ee）\n\t */\n\tpublic static final double CORRECTION_PARAM = 0.00669342162296594323D;\n\n\t/**\n\t * 判断坐标是否在国外<br>\n\t * 火星坐标系 (GCJ-02)只对国内有效，国外无需转换\n\t *\n\t * @param lng 经度\n\t * @param lat 纬度\n\t * @return 坐标是否在国外\n\t */\n\tpublic static boolean outOfChina(double lng, double lat) {\n\t\treturn (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271);\n\t}\n\n\t//----------------------------------------------------------------------------------- WGS84\n\t/**\n\t * WGS84 转换为 火星坐标系 (GCJ-02)\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @return 火星坐标 (GCJ-02)\n\t */\n\tpublic static Coordinate wgs84ToGcj02(double lng, double lat) {\n\t\treturn new Coordinate(lng, lat).offset(offset(lng, lat, true));\n\t}\n\n\t/**\n\t * WGS84 坐标转为 百度坐标系 (BD-09) 坐标\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @return bd09 坐标\n\t */\n\tpublic static Coordinate wgs84ToBd09(double lng, double lat) {\n\t\tfinal Coordinate gcj02 = wgs84ToGcj02(lng, lat);\n\t\treturn gcj02ToBd09(gcj02.lng, gcj02.lat);\n\t}\n\n\t//----------------------------------------------------------------------------------- GCJ-02\n\t/**\n\t * 火星坐标系 (GCJ-02) 转换为 WGS84\n\t *\n\t * @param lng 经度坐标\n\t * @param lat 纬度坐标\n\t * @return WGS84 坐标\n\t */\n\tpublic static Coordinate gcj02ToWgs84(double lng, double lat) {\n\t\treturn new Coordinate(lng, lat).offset(offset(lng, lat, false));\n\t}\n\n\t/**\n\t * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @return BD-09 坐标\n\t */\n\tpublic static Coordinate gcj02ToBd09(double lng, double lat) {\n\t\tdouble z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI);\n\t\tdouble theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI);\n\t\tdouble bd_lng = z * Math.cos(theta) + 0.0065;\n\t\tdouble bd_lat = z * Math.sin(theta) + 0.006;\n\t\treturn new Coordinate(bd_lng, bd_lat);\n\t}\n\n\t//----------------------------------------------------------------------------------- BD-09\n\t/**\n\t * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换\n\t * 即 百度 转 谷歌、高德\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @return GCJ-02 坐标\n\t */\n\tpublic static Coordinate bd09ToGcj02(double lng, double lat) {\n\t\tdouble x = lng - 0.0065;\n\t\tdouble y = lat - 0.006;\n\t\tdouble z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);\n\t\tdouble theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);\n\t\tdouble gg_lng = z * Math.cos(theta);\n\t\tdouble gg_lat = z * Math.sin(theta);\n\t\treturn new Coordinate(gg_lng, gg_lat);\n\t}\n\n\t/**\n\t * 百度坐标系 (BD-09) 与 WGS84 的转换\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @return WGS84坐标\n\t */\n\tpublic static Coordinate bd09toWgs84(double lng, double lat) {\n\t\tfinal Coordinate gcj02 = bd09ToGcj02(lng, lat);\n\t\treturn gcj02ToWgs84(gcj02.lng, gcj02.lat);\n\t}\n\n\t/**\n        * WGS84 坐标转为 墨卡托投影\n        *\n\t* @param lng 经度值\n\t* @param lat 纬度值\n\t* @return 墨卡托投影\n\t*/\n\tpublic static Coordinate wgs84ToMercator(double lng, double lat) {\n\t\tdouble x = lng * 20037508.342789244 / 180;\n\t\tdouble y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);\n\t\ty = y * 20037508.342789244 / 180;\n\t\treturn new Coordinate(x, y);\n\t}\n\n\t/**\n\t* 墨卡托投影 转为 WGS84 坐标\n\t*\n\t* @param mercatorX 墨卡托X坐标\n\t* @param mercatorY 墨卡托Y坐标\n\t* @return WGS84 坐标\n\t*/\n\tpublic static Coordinate mercatorToWgs84(double mercatorX, double mercatorY) {\n\t\tdouble x = mercatorX / 20037508.342789244 * 180;\n\t\tdouble y = mercatorY / 20037508.342789244 * 180;\n\t\ty = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);\n\t\treturn new Coordinate(x, y);\n\t}\n\t\n\t//----------------------------------------------------------------------------------- Private methods begin\n\n\t/**\n\t * WGS84 与 火星坐标系 (GCJ-02)转换的偏移算法（非精确）\n\t *\n\t * @param lng 经度值\n\t * @param lat 纬度值\n\t * @param isPlus 是否正向偏移：WGS84转GCJ-02使用正向，否则使用反向\n\t * @return 偏移坐标\n\t */\n\tprivate static Coordinate offset(double lng, double lat, boolean isPlus) {\n\t\tdouble dlng = transLng(lng - 105.0, lat - 35.0);\n\t\tdouble dlat = transLat(lng - 105.0, lat - 35.0);\n\n\t\tdouble magic = Math.sin(lat / 180.0 * PI);\n\t\tmagic = 1 - CORRECTION_PARAM * magic * magic;\n\t\tfinal double sqrtMagic = Math.sqrt(magic);\n\n\t\tdlng = (dlng * 180.0) / (RADIUS / sqrtMagic * Math.cos(lat / 180.0 * PI) * PI);\n\t\tdlat = (dlat * 180.0) / ((RADIUS * (1 - CORRECTION_PARAM)) / (magic * sqrtMagic) * PI);\n\n\t\tif(false == isPlus){\n\t\t\tdlng = - dlng;\n\t\t\tdlat = - dlat;\n\t\t}\n\n\t\treturn new Coordinate(dlng, dlat);\n\t}\n\n\t/**\n\t * 计算经度坐标\n\t *\n\t * @param lng 经度坐标\n\t * @param lat 纬度坐标\n\t * @return ret 计算完成后的\n\t */\n\tprivate static double transLng(double lng, double lat) {\n\t\tdouble ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));\n\t\tret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;\n\t\tret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;\n\t\tret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;\n\t\treturn ret;\n\t}\n\n\t/**\n\t * 计算纬度坐标\n\t *\n\t * @param lng 经度\n\t * @param lat 纬度\n\t * @return ret 计算完成后的\n\t */\n\tprivate static double transLat(double lng, double lat) {\n\t\tdouble ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat\n\t\t\t\t+ 0.2 * Math.sqrt(Math.abs(lng));\n\t\tret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;\n\t\tret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;\n\t\tret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;\n\t\treturn ret;\n\t}\n\t//----------------------------------------------------------------------------------- Private methods end\n\n\t/**\n\t * 坐标经纬度\n\t *\n\t * @author looly\n\t */\n\tpublic static class Coordinate implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t/**\n\t\t * 经度\n\t\t */\n\t\tprivate double lng;\n\t\t/**\n\t\t * 纬度\n\t\t */\n\t\tprivate double lat;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param lng 经度\n\t\t * @param lat 纬度\n\t\t */\n\t\tpublic Coordinate(double lng, double lat) {\n\t\t\tthis.lng = lng;\n\t\t\tthis.lat = lat;\n\t\t}\n\n\t\t/**\n\t\t * 获取经度\n\t\t *\n\t\t * @return 经度\n\t\t */\n\t\tpublic double getLng() {\n\t\t\treturn lng;\n\t\t}\n\n\t\t/**\n\t\t * 设置经度\n\t\t *\n\t\t * @param lng 经度\n\t\t * @return this\n\t\t */\n\t\tpublic Coordinate setLng(double lng) {\n\t\t\tthis.lng = lng;\n\t\t\treturn this;\n\t\t}\n\n\t\t/**\n\t\t * 获取纬度\n\t\t *\n\t\t * @return 纬度\n\t\t */\n\t\tpublic double getLat() {\n\t\t\treturn lat;\n\t\t}\n\n\t\t/**\n\t\t * 设置纬度\n\t\t *\n\t\t * @param lat 纬度\n\t\t * @return this\n\t\t */\n\t\tpublic Coordinate setLat(double lat) {\n\t\t\tthis.lat = lat;\n\t\t\treturn this;\n\t\t}\n\n\t\t/**\n\t\t * 当前坐标偏移指定坐标\n\t\t *\n\t\t * @param offset 偏移量\n\t\t * @return this\n\t\t */\n\t\tpublic Coordinate offset(Coordinate offset){\n\t\t\tthis.lng += offset.lng;\n\t\t\tthis.lat += offset.lat;\n\t\t\treturn this;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object o) {\n\t\t\tif (this == o) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tCoordinate that = (Coordinate) o;\n\t\t\treturn Double.compare(that.lng, lng) == 0 && Double.compare(that.lat, lat) == 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\treturn Objects.hash(lng, lat);\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"Coordinate{\" +\n\t\t\t\t\t\"lng=\" + lng +\n\t\t\t\t\t\", lat=\" + lat +\n\t\t\t\t\t'}';\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/CreditCodeUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\n/**\n * 统一社会信用代码（GB32100-2015）工具类<br>\n * 标准见：https://www.cods.org.cn/c/2020-10-29/12575.html\n *\n * <pre>\n * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n * 第二部分：机构类别代码1位 (数字或大写英文字母)\n * 第三部分：登记管理机关行政区划码6位 (数字)\n * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n * 第五部分：校验码1位 (数字或大写英文字母)\n * </pre>\n *\n * @author looly\n * @since 5.2.4\n */\npublic class CreditCodeUtil {\n\n\tpublic static final Pattern CREDIT_CODE_PATTERN = PatternPool.CREDIT_CODE;\n\n\t/**\n\t * 加权因子\n\t */\n\tprivate static final int[] WEIGHT = {1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28};\n\t/**\n\t * 代码字符集\n\t */\n\tprivate static final char[] BASE_CODE_ARRAY = \"0123456789ABCDEFGHJKLMNPQRTUWXY\".toCharArray();\n\tprivate static final Map<Character, Integer> CODE_INDEX_MAP;\n\n\tstatic {\n\t\tCODE_INDEX_MAP = new SafeConcurrentHashMap<>(BASE_CODE_ARRAY.length);\n\t\tfor (int i = 0; i < BASE_CODE_ARRAY.length; i++) {\n\t\t\tCODE_INDEX_MAP.put(BASE_CODE_ARRAY[i], i);\n\t\t}\n\t}\n\n\t/**\n\t * 正则校验统一社会信用代码（18位）\n\t *\n\t * <pre>\n\t * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n\t * 第二部分：机构类别代码1位 (数字或大写英文字母)\n\t * 第三部分：登记管理机关行政区划码6位 (数字)\n\t * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n\t * 第五部分：校验码1位 (数字或大写英文字母)\n\t * </pre>\n\t *\n\t * @param creditCode 统一社会信用代码\n\t * @return 校验结果\n\t */\n\tpublic static boolean isCreditCodeSimple(CharSequence creditCode) {\n\t\tif (StrUtil.isBlank(creditCode)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ReUtil.isMatch(CREDIT_CODE_PATTERN, creditCode);\n\t}\n\n\t/**\n\t * 是否是有效的统一社会信用代码\n\t * <pre>\n\t * 第一部分：登记管理部门代码1位 (数字或大写英文字母)\n\t * 第二部分：机构类别代码1位 (数字或大写英文字母)\n\t * 第三部分：登记管理机关行政区划码6位 (数字)\n\t * 第四部分：主体标识码（组织机构代码）9位 (数字或大写英文字母)\n\t * 第五部分：校验码1位 (数字或大写英文字母)\n\t * </pre>\n\t *\n\t * @param creditCode 统一社会信用代码\n\t * @return 校验结果\n\t */\n\tpublic static boolean isCreditCode(CharSequence creditCode) {\n\t\tif (false == isCreditCodeSimple(creditCode)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal int parityBit = getParityBit(creditCode);\n\t\tif (parityBit < 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn creditCode.charAt(17) == BASE_CODE_ARRAY[parityBit];\n\t}\n\n\t/**\n\t * 获取一个随机的统一社会信用代码\n\t *\n\t * @return 统一社会信用代码\n\t */\n\tpublic static String randomCreditCode() {\n\t\tfinal StringBuilder buf = new StringBuilder(18);\n\n\n\t\t//\n\t\tfor (int i = 0; i < 2; i++) {\n\t\t\tint num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);\n\t\t\tbuf.append(Character.toUpperCase(BASE_CODE_ARRAY[num]));\n\t\t}\n\t\tfor (int i = 2; i < 8; i++) {\n\t\t\tint num = RandomUtil.randomInt(10);\n\t\t\tbuf.append(BASE_CODE_ARRAY[num]);\n\t\t}\n\t\tfor (int i = 8; i < 17; i++) {\n\t\t\tint num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);\n\t\t\tbuf.append(BASE_CODE_ARRAY[num]);\n\t\t}\n\n\t\tfinal String code = buf.toString();\n\t\treturn code + BASE_CODE_ARRAY[getParityBit(code)];\n\t}\n\n\t/**\n\t * 获取校验位的值\n\t *\n\t * @param creditCode 统一社会信息代码\n\t * @return 获取校验位的值，-1表示获取错误\n\t */\n\tprivate static int getParityBit(CharSequence creditCode) {\n\t\tint sum = 0;\n\t\tInteger codeIndex;\n\t\tfor (int i = 0; i < 17; i++) {\n\t\t\tcodeIndex = CODE_INDEX_MAP.get(creditCode.charAt(i));\n\t\t\tif (null == codeIndex) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tsum += codeIndex * WEIGHT[i];\n\t\t}\n\t\tfinal int result = 31 - sum % 31;\n\t\treturn result == 31 ? 0 : result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/DesensitizedUtil.java",
    "content": "package cn.hutool.core.util;\n\n/**\n * 脱敏工具类，支持以下类型信息的脱敏自动处理：\n *\n * <ul>\n *     <li>用户ID</li>\n *     <li>中文名</li>\n *     <li>身份证</li>\n *     <li>座机号</li>\n *     <li>手机号</li>\n *     <li>地址</li>\n *     <li>电子邮件</li>\n *     <li>密码</li>\n *     <li>车牌</li>\n *     <li>银行卡号</li>\n * </ul>\n *\n * @author dazer and neusoft and qiaomu\n * @since 5.6.2\n */\npublic class DesensitizedUtil {\n\n\t/**\n\t * 支持的脱敏类型枚举\n\t *\n\t * @author dazer and neusoft and qiaomu\n\t */\n\tpublic enum DesensitizedType {\n\t\t/**\n\t\t * 用户id\n\t\t */\n\t\tUSER_ID,\n\t\t/**\n\t\t * 中文名\n\t\t */\n\t\tCHINESE_NAME,\n\t\t/**\n\t\t * 身份证号\n\t\t */\n\t\tID_CARD,\n\t\t/**\n\t\t * 座机号\n\t\t */\n\t\tFIXED_PHONE,\n\t\t/**\n\t\t * 手机号\n\t\t */\n\t\tMOBILE_PHONE,\n\t\t/**\n\t\t * 地址\n\t\t */\n\t\tADDRESS,\n\t\t/**\n\t\t * 电子邮件\n\t\t */\n\t\tEMAIL,\n\t\t/**\n\t\t * 密码\n\t\t */\n\t\tPASSWORD,\n\t\t/**\n\t\t * 中国大陆车牌，包含普通车辆、新能源车辆\n\t\t */\n\t\tCAR_LICENSE,\n\t\t/**\n\t\t * 银行卡\n\t\t */\n\t\tBANK_CARD,\n\t\t/**\n\t\t * IPv4地址\n\t\t */\n\t\tIPV4,\n\t\t/**\n\t\t * IPv6地址\n\t\t */\n\t\tIPV6,\n\t\t/**\n\t\t * 护照号\n\t\t */\n\t\tPASSPORT,\n\t\t/**\n\t\t * 统一社会信用代码\n\t\t */\n\t\tCREDIT_CODE,\n\t\t/**\n\t\t * 定义了一个first_mask的规则，只显示第一个字符。\n\t\t */\n\t\tFIRST_MASK,\n\t\t/**\n\t\t * 清空为null\n\t\t */\n\t\tCLEAR_TO_NULL,\n\t\t/**\n\t\t * 清空为\"\"\n\t\t */\n\t\tCLEAR_TO_EMPTY\n\t}\n\n\t/**\n\t * 脱敏，使用默认的脱敏策略\n\t * <pre>\n\t * DesensitizedUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.USER_ID)) =  \"0\"\n\t * DesensitizedUtil.desensitized(\"段正淳\", DesensitizedUtil.DesensitizedType.CHINESE_NAME)) = \"段**\"\n\t * DesensitizedUtil.desensitized(\"51343620000320711X\", DesensitizedUtil.DesensitizedType.ID_CARD)) = \"5***************1X\"\n\t * DesensitizedUtil.desensitized(\"09157518479\", DesensitizedUtil.DesensitizedType.FIXED_PHONE)) = \"0915*****79\"\n\t * DesensitizedUtil.desensitized(\"18049531999\", DesensitizedUtil.DesensitizedType.MOBILE_PHONE)) = \"180****1999\"\n\t * DesensitizedUtil.desensitized(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.DesensitizedType.ADDRESS)) = \"北京市海淀区马********\"\n\t * DesensitizedUtil.desensitized(\"duandazhi-jack@gmail.com.cn\", DesensitizedUtil.DesensitizedType.EMAIL)) = \"d*************@gmail.com.cn\"\n\t * DesensitizedUtil.desensitized(\"1234567890\", DesensitizedUtil.DesensitizedType.PASSWORD)) = \"**********\"\n\t * DesensitizedUtil.desensitized(\"苏D40000\", DesensitizedUtil.DesensitizedType.CAR_LICENSE)) = \"苏D4***0\"\n\t * DesensitizedUtil.desensitized(\"11011111222233333256\", DesensitizedUtil.DesensitizedType.BANK_CARD)) = \"1101 **** **** **** 3256\"\n\t * DesensitizedUtil.desensitized(\"192.168.1.1\", DesensitizedUtil.DesensitizedType.IPV4)) = \"192.*.*.*\"\n\t * </pre>\n\t *\n\t * @param str              字符串\n\t * @param desensitizedType 脱敏类型;可以脱敏：用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码\n\t * @return 脱敏之后的字符串\n\t * @author dazer and neusoft and qiaomu\n\t * @since 5.6.2\n\t */\n\tpublic static String desensitized(CharSequence str, DesensitizedUtil.DesensitizedType desensitizedType) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tString newStr = String.valueOf(str);\n\t\tswitch (desensitizedType) {\n\t\t\tcase USER_ID:\n\t\t\t\tnewStr = String.valueOf(userId());\n\t\t\t\tbreak;\n\t\t\tcase CHINESE_NAME:\n\t\t\t\tnewStr = chineseName(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase ID_CARD:\n\t\t\t\tnewStr = idCardNum(String.valueOf(str), 1, 2);\n\t\t\t\tbreak;\n\t\t\tcase FIXED_PHONE:\n\t\t\t\tnewStr = fixedPhone(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase MOBILE_PHONE:\n\t\t\t\tnewStr = mobilePhone(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase ADDRESS:\n\t\t\t\tnewStr = address(String.valueOf(str), 8);\n\t\t\t\tbreak;\n\t\t\tcase EMAIL:\n\t\t\t\tnewStr = email(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase PASSWORD:\n\t\t\t\tnewStr = password(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase CAR_LICENSE:\n\t\t\t\tnewStr = carLicense(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase BANK_CARD:\n\t\t\t\tnewStr = bankCard(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase IPV4:\n\t\t\t\tnewStr = ipv4(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase IPV6:\n\t\t\t\tnewStr = ipv6(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase PASSPORT:\n\t\t\t\tnewStr = passport(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase CREDIT_CODE:\n\t\t\t\tnewStr = creditCode(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase FIRST_MASK:\n\t\t\t\tnewStr = firstMask(String.valueOf(str));\n\t\t\t\tbreak;\n\t\t\tcase CLEAR_TO_EMPTY:\n\t\t\t\tnewStr = clear();\n\t\t\t\tbreak;\n\t\t\tcase CLEAR_TO_NULL:\n\t\t\t\tnewStr = clearToNull();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t}\n\t\treturn newStr;\n\t}\n\n\t/**\n\t * 清空为空字符串\n\t *\n\t * @return 清空后的值\n\t * @since 5.8.22\n\t */\n\tpublic static String clear() {\n\t\treturn StrUtil.EMPTY;\n\t}\n\n\t/**\n\t * 清空为{@code null}\n\t *\n\t * @return 清空后的值(null)\n\t * @since 5.8.22\n\t */\n\tpublic static String clearToNull() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 【用户id】不对外提供userId\n\t *\n\t * @return 脱敏后的主键\n\t */\n\tpublic static Long userId() {\n\t\treturn 0L;\n\t}\n\n\t/**\n\t * 定义了一个first_mask的规则，只显示第一个字符。<br>\n\t * 脱敏前：123456789；脱敏后：1********。\n\t *\n\t * @param str 字符串\n\t * @return 脱敏后的字符串\n\t */\n\tpublic static String firstMask(String str) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn StrUtil.hide(str, 1, str.length());\n\t}\n\n\t/**\n\t * 【中文姓名】只显示第一个汉字，其他隐藏为2个星号，比如：李**\n\t *\n\t * @param fullName 姓名\n\t * @return 脱敏后的姓名\n\t */\n\tpublic static String chineseName(String fullName) {\n\t\treturn firstMask(fullName);\n\t}\n\n\t/**\n\t * 【身份证号】前1位 和后2位\n\t *\n\t * @param idCardNum 身份证\n\t * @param front     保留：前面的front位数；从1开始\n\t * @param end       保留：后面的end位数；从1开始\n\t * @return 脱敏后的身份证\n\t */\n\tpublic static String idCardNum(String idCardNum, int front, int end) {\n\t\t//身份证不能为空\n\t\tif (StrUtil.isBlank(idCardNum)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\t//需要截取的长度不能大于身份证号长度\n\t\tif ((front + end) > idCardNum.length()) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\t//需要截取的不能小于0\n\t\tif (front < 0 || end < 0) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn StrUtil.hide(idCardNum, front, idCardNum.length() - end);\n\t}\n\n\t/**\n\t * 【固定电话 前四位，后两位\n\t *\n\t * @param num 固定电话\n\t * @return 脱敏后的固定电话；\n\t */\n\tpublic static String fixedPhone(String num) {\n\t\tif (StrUtil.isBlank(num)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn StrUtil.hide(num, 4, num.length() - 2);\n\t}\n\n\t/**\n\t * 【手机号码】前三位，后4位，其他隐藏，比如135****2210\n\t *\n\t * @param num 移动电话；\n\t * @return 脱敏后的移动电话；\n\t */\n\tpublic static String mobilePhone(String num) {\n\t\tif (StrUtil.isBlank(num)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn StrUtil.hide(num, 3, num.length() - 4);\n\t}\n\n\t/**\n\t * 【地址】只显示到地区，不显示详细地址，比如：北京市海淀区****\n\t *\n\t * @param address       家庭住址\n\t * @param sensitiveSize 敏感信息长度\n\t * @return 脱敏后的家庭地址\n\t */\n\tpublic static String address(String address, int sensitiveSize) {\n\t\tif (StrUtil.isBlank(address)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tint length = address.length();\n\t\treturn StrUtil.hide(address, length - sensitiveSize, length);\n\t}\n\n\t/**\n\t * 【电子邮箱】邮箱前缀仅显示第一个字母，前缀其他隐藏，用星号代替，@及后面的地址显示，比如：d**@126.com\n\t *\n\t * @param email 邮箱\n\t * @return 脱敏后的邮箱\n\t */\n\tpublic static String email(String email) {\n\t\tif (StrUtil.isBlank(email)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tint index = StrUtil.indexOf(email, '@');\n\t\tif (index <= 1) {\n\t\t\treturn email;\n\t\t}\n\t\treturn StrUtil.hide(email, 1, index);\n\t}\n\n\t/**\n\t * 【密码】密码的全部字符都用*代替，比如：******\n\t *\n\t * @param password 密码\n\t * @return 脱敏后的密码\n\t */\n\tpublic static String password(String password) {\n\t\tif (StrUtil.isBlank(password)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\treturn StrUtil.repeat('*', password.length());\n\t}\n\n\t/**\n\t * 【中国车牌】车牌中间用*代替\n\t * eg1：null       -》 \"\"\n\t * eg1：\"\"         -》 \"\"\n\t * eg3：苏D40000   -》 苏D4***0\n\t * eg4：陕A12345D  -》 陕A1****D\n\t * eg5：京A123     -》 京A123     如果是错误的车牌，不处理\n\t *\n\t * @param carLicense 完整的车牌号\n\t * @return 脱敏后的车牌\n\t */\n\tpublic static String carLicense(String carLicense) {\n\t\tif (StrUtil.isBlank(carLicense)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\t// 普通车牌\n\t\tif (carLicense.length() == 7) {\n\t\t\tcarLicense = StrUtil.hide(carLicense, 3, 6);\n\t\t} else if (carLicense.length() == 8) {\n\t\t\t// 新能源车牌\n\t\t\tcarLicense = StrUtil.hide(carLicense, 3, 7);\n\t\t}\n\t\treturn carLicense;\n\t}\n\n\t/**\n\t * 【银行卡号脱敏】由于银行卡号长度不定，所以只展示前4位，后面的位数根据卡号决定展示1-4位\n\t * 例如：\n\t * <pre>{@code\n\t *      1. \"1234 2222 3333 4444 6789 9\"    ->   \"1234 **** **** **** **** 9\"\n\t *      2. \"1234 2222 3333 4444 6789 91\"   ->   \"1234 **** **** **** **** 91\"\n\t *      3. \"1234 2222 3333 4444 678\"       ->    \"1234 **** **** **** 678\"\n\t *      4. \"1234 2222 3333 4444 6789\"      ->    \"1234 **** **** **** 6789\"\n\t *  }</pre>\n\t *\n\t * @param bankCardNo 银行卡号\n\t * @return 脱敏之后的银行卡号\n\t */\n\tpublic static String bankCard(String bankCardNo) {\n\t\tif (StrUtil.isBlank(bankCardNo)) {\n\t\t\treturn bankCardNo;\n\t\t}\n\t\tbankCardNo = StrUtil.cleanBlank(bankCardNo);\n\t\tif (bankCardNo.length() < 9) {\n\t\t\treturn bankCardNo;\n\t\t}\n\n\t\tfinal int length = bankCardNo.length();\n\t\tfinal int endLength = length % 4 == 0 ? 4 : length % 4;\n\t\tfinal int midLength = length - 4 - endLength;\n\n\t\tfinal StringBuilder buf = new StringBuilder();\n\t\tbuf.append(bankCardNo, 0, 4);\n\t\tfor (int i = 0; i < midLength; ++i) {\n\t\t\tif (i % 4 == 0) {\n\t\t\t\tbuf.append(CharUtil.SPACE);\n\t\t\t}\n\t\t\tbuf.append('*');\n\t\t}\n\t\tbuf.append(CharUtil.SPACE).append(bankCardNo, length - endLength, length);\n\t\treturn buf.toString();\n\t}\n\n\t/**\n\t * IPv4脱敏，如：脱敏前：192.0.2.1；脱敏后：192.*.*.*。\n\t *\n\t * @param ipv4 IPv4地址\n\t * @return 脱敏后的地址\n\t */\n\tpublic static String ipv4(String ipv4) {\n\t\treturn StrUtil.subBefore(ipv4, '.', false) + \".*.*.*\";\n\t}\n\n\t/**\n\t * IPv6脱敏，如：脱敏前：2001:0db8:86a3:08d3:1319:8a2e:0370:7344；脱敏后：2001:*:*:*:*:*:*:*\n\t *\n\t * @param ipv6 IPv6地址\n\t * @return 脱敏后的地址\n\t */\n\tpublic static String ipv6(String ipv6) {\n\t\treturn StrUtil.subBefore(ipv6, ':', false) + \":*:*:*:*:*:*:*\";\n\t}\n\n\t/**\n\t * 护照号脱敏\n\t * 规则：前2后2，长度不足时保留最小有效信息\n\t * 示例：PJ1234567 → PJ*****67\n\t *\n\t * @param passport 护照号\n\t * @return 脱敏后的护照号\n\t */\n\tpublic static String passport(String passport) {\n\t\tif (StrUtil.isBlank(passport)) return passport;\n\t\tfinal int length = passport.length();\n\t\tif (length <= 2) return StrUtil.hide(passport, 0, length);\n\t\treturn StrUtil.hide(passport, 2, length - 2);\n\t}\n\n\t/**\n\t * 统一社会信用代码脱敏\n\t * 规则：前4后4，长度不足时保留最小有效信息\n\t * 统一社会信用代码由18位数字或大写英文字母组成\n\t * 示例：91110108MA01ABCDE7   → 9111**********CDE7\n\t *\n\t * @param code 统一社会信用代码\n\t * @return 脱敏后的统一社会信用代码\n\t */\n\tpublic static String creditCode(String code) {\n\t\tif (StrUtil.isBlank(code)) return code;\n\t\tfinal int length = code.length();\n\t\tif (length <= 4) return StrUtil.hide(code, 0, length);\n\t\treturn StrUtil.hide(code, 4, length - 4);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/EnumUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.func.LambdaUtil;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.lang.reflect.Field;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * 枚举工具类\n *\n * @author looly\n * @since 3.3.0\n */\npublic class EnumUtil {\n\n\tprivate static final Map<Class<?>, Enum<?>[]> CACHE = new ConcurrentHashMap<>();\n\n\t/**\n\t * 清空缓存，重新加载枚举类\n\t */\n\tpublic static void clearCache() {\n\t\tCACHE.clear();\n\t}\n\n\t/**\n\t * 指定类是否为Enum类\n\t *\n\t * @param clazz 类\n\t * @return 是否为Enum类\n\t */\n\tpublic static boolean isEnum(Class<?> clazz) {\n\t\treturn Assert.notNull(clazz).isEnum();\n\t}\n\n\t/**\n\t * 指定类是否为Enum类\n\t *\n\t * @param obj 类\n\t * @return 是否为Enum类\n\t */\n\tpublic static boolean isEnum(Object obj) {\n\t\treturn Assert.notNull(obj).getClass().isEnum();\n\t}\n\n\t/**\n\t * Enum对象转String，调用{@link Enum#name()} 方法\n\t *\n\t * @param e Enum\n\t * @return name值\n\t * @since 4.1.13\n\t */\n\tpublic static String toString(Enum<?> e) {\n\t\treturn null != e ? e.name() : null;\n\t}\n\n\t/**\n\t * 获取给定位置的枚举值\n\t *\n\t * @param <E>       枚举类型泛型\n\t * @param enumClass 枚举类\n\t * @param index     枚举索引\n\t * @return 枚举值，null表示无此对应枚举\n\t * @since 5.1.6\n\t */\n\tpublic static <E extends Enum<E>> E getEnumAt(Class<E> enumClass, int index) {\n\t\tif (null == enumClass) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal E[] enumConstants = getEnums(enumClass);\n\t\tif (index < 0) {\n\t\t\tindex = enumConstants.length + index;\n\t\t}\n\n\t\treturn index >= 0 && index < enumConstants.length ? enumConstants[index] : null;\n\t}\n\n\t/**\n\t * 字符串转枚举，调用{@link Enum#valueOf(Class, String)}\n\t *\n\t * @param <E>       枚举类型泛型\n\t * @param enumClass 枚举类\n\t * @param value     值\n\t * @return 枚举值\n\t * @since 4.1.13\n\t */\n\tpublic static <E extends Enum<E>> E fromString(Class<E> enumClass, String value) {\n\t\tif (null == enumClass || StrUtil.isBlank(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn Enum.valueOf(enumClass, value);\n\t}\n\n\t/**\n\t * 字符串转枚举，调用{@link Enum#valueOf(Class, String)}<br>\n\t * 如果无枚举值，返回默认值\n\t *\n\t * @param <E>          枚举类型泛型\n\t * @param enumClass    枚举类\n\t * @param value        值\n\t * @param defaultValue 无对应枚举值返回的默认值\n\t * @return 枚举值\n\t * @since 4.5.18\n\t */\n\tpublic static <E extends Enum<E>> E fromString(Class<E> enumClass, String value, E defaultValue) {\n\t\treturn ObjectUtil.defaultIfNull(fromStringQuietly(enumClass, value), defaultValue);\n\t}\n\n\t/**\n\t * 字符串转枚举，调用{@link Enum#valueOf(Class, String)}，转换失败返回{@code null} 而非报错\n\t *\n\t * @param <E>       枚举类型泛型\n\t * @param enumClass 枚举类\n\t * @param value     值\n\t * @return 枚举值\n\t * @since 4.5.18\n\t */\n\tpublic static <E extends Enum<E>> E fromStringQuietly(Class<E> enumClass, String value) {\n\t\ttry {\n\t\t\treturn fromString(enumClass, value);\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 模糊匹配转换为枚举，给定一个值，匹配枚举中定义的所有字段名（包括name属性），一旦匹配到返回这个枚举对象，否则返回null\n\t *\n\t * @param <E>       枚举类型\n\t * @param enumClass 枚举类\n\t * @param value     值\n\t * @return 匹配到的枚举对象，未匹配到返回null\n\t */\n\tpublic static <E extends Enum<E>> E likeValueOf(Class<E> enumClass, Object value) {\n\t\tif (null == enumClass || null == value) {\n\t\t\treturn null;\n\t\t}\n\t\tif (value instanceof CharSequence) {\n\t\t\tvalue = value.toString().trim();\n\t\t}\n\n\t\tfinal Field[] fields = ReflectUtil.getFields(enumClass);\n\t\tfinal E[] enums = getEnums(enumClass);\n\t\tString fieldName;\n\t\tfor (Field field : fields) {\n\t\t\tfieldName = field.getName();\n\t\t\tif (field.getType().isEnum() || \"ENUM$VALUES\".equals(fieldName) || \"ordinal\".equals(fieldName)) {\n\t\t\t\t// 跳过一些特殊字段\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (E enumObj : enums) {\n\t\t\t\tif (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) {\n\t\t\t\t\treturn enumObj;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 枚举类中所有枚举对象的name列表\n\t *\n\t * @param clazz 枚举类\n\t * @return name列表\n\t */\n\tpublic static List<String> getNames(Class<? extends Enum<?>> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Enum<?>[] enums = getEnums2(clazz);\n\t\tif (null == enums) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal List<String> list = new ArrayList<>(enums.length);\n\t\tfor (Enum<?> e : enums) {\n\t\t\tlist.add(e.name());\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 获得枚举类中各枚举对象下指定字段的值\n\t *\n\t * @param clazz     枚举类\n\t * @param fieldName 字段名，最终调用getXXX方法\n\t * @return 字段值列表\n\t */\n\tpublic static List<Object> getFieldValues(Class<? extends Enum<?>> clazz, String fieldName) {\n\t\tif (null == clazz || StrUtil.isBlank(fieldName)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Enum<?>[] enums = getEnums2(clazz);\n\t\tif (null == enums) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal List<Object> list = new ArrayList<>(enums.length);\n\t\tfor (Enum<?> e : enums) {\n\t\t\tlist.add(ReflectUtil.getFieldValue(e, fieldName));\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 获得枚举类中所有的字段名<br>\n\t * 除用户自定义的字段名，也包括“name”字段，例如：\n\t *\n\t * <pre>\n\t *   EnumUtil.getFieldNames(Color.class) == [\"name\", \"index\"]\n\t * </pre>\n\t *\n\t * @param clazz 枚举类\n\t * @return 字段名列表\n\t * @since 4.1.20\n\t */\n\tpublic static List<String> getFieldNames(Class<? extends Enum<?>> clazz) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal List<String> names = new ArrayList<>();\n\t\tfinal Field[] fields = ReflectUtil.getFields(clazz);\n\t\tString name;\n\t\tfor (Field field : fields) {\n\t\t\tname = field.getName();\n\t\t\tif (field.getType().isEnum() || name.contains(\"$VALUES\") || \"ordinal\".equals(name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (false == names.contains(name)) {\n\t\t\t\tnames.add(name);\n\t\t\t}\n\t\t}\n\t\treturn names;\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code null}\n\t *\n\t * @param enumClass 枚举类\n\t * @param predicate 条件\n\t * @param <E>       枚举类型\n\t * @return 对应枚举 ，获取不到时为 {@code null}\n\t * @since 5.8.0\n\t */\n\tpublic static <E extends Enum<E>> E getBy(Class<E> enumClass, Predicate<? super E> predicate) {\n\t\treturn getBy(enumClass, predicate, null);\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code defaultEnum}\n\t * 通过缓存减少反射带来的影响\n\t *\n\t * @param enumClass   枚举类\n\t * @param predicate   条件\n\t * @param defaultEnum 获取不到时的默认枚举值\n\t * @param <E>         枚举类型\n\t * @return 对应枚举 ，获取不到时为 {@code defaultEnum}\n\t */\n\tpublic static <E extends Enum<E>> E getBy(Class<E> enumClass, Predicate<? super E> predicate, E defaultEnum) {\n\t\tif (null == enumClass || null == predicate) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn Arrays.stream(getEnums(enumClass))\n\t\t\t.filter(predicate).findFirst().orElse(defaultEnum);\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code null}\n\t * <p>\n\t * {@link LambdaUtil#getRealClass(Func1)}} 是相对耗时的\n\t * 如果枚举值比较多,那么{@link EnumUtil#getBy(Func1, Object)} 方法\n\t * 大部分时间都是被{@link LambdaUtil#getRealClass(Func1)}}所消耗的\n\t * <br>\n\t * 如果可以在编码过程中可以提供对应的枚举类 该方法与枚举的{@code Enum.values()}方法是差不多的。\n\t *\n\t * @param enumClass 枚举类， 为{@code null}返回{@code null}\n\t * @param condition 条件字段，为{@code null}返回{@code null}\n\t * @param value     条件字段值\n\t * @param <E>       枚举类型\n\t * @param <C>       字段类型\n\t * @return 对应枚举 ，获取不到时为 {@code null}\n\t */\n\tpublic static <E extends Enum<E>, C> E getBy(Class<E> enumClass, Func1<E, C> condition, C value) {\n\t\tif (null == condition) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getBy(enumClass, constant -> ObjUtil.equals(condition.callWithRuntimeException(constant), value));\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code defaultEnum}\n\t *\n\t * @param enumClass   枚举类， 为{@code null}返回{@code null}\n\t * @param condition   条件字段，为{@code null}返回{@code null}\n\t * @param value       条件字段值\n\t * @param defaultEnum 获取不到时的默认枚举值\n\t * @param <E>         枚举类型\n\t * @param <C>         字段类型\n\t * @return 对应枚举 ，获取不到时为 {@code defaultEnum}\n\t */\n\tpublic static <E extends Enum<E>, C> E getBy(Class<E> enumClass, Func1<E, C> condition, C value, E defaultEnum) {\n\t\treturn ObjectUtil.defaultIfNull(getBy(enumClass, condition, value), defaultEnum);\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code null}\n\t *\n\t * @param condition 条件字段，为{@code null}返回{@code null}\n\t * @param value     条件字段值\n\t * @param <E>       枚举类型\n\t * @param <C>       字段类型\n\t * @return 对应枚举 ，获取不到时为 {@code null}\n\t */\n\tpublic static <E extends Enum<E>, C> E getBy(Func1<E, C> condition, C value) {\n\t\tif (null == condition) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Class<E> implClass = LambdaUtil.getRealClass(condition);\n\t\treturn getBy(implClass, condition, value);\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举，获取不到时为 {@code defaultEnum}\n\t *\n\t * @param <E>         枚举类型\n\t * @param <C>         字段类型\n\t * @param condition   条件字段\n\t * @param value       条件字段值\n\t * @param defaultEnum 条件找不到则返回结果使用这个\n\t * @return 对应枚举 ，获取不到时为 {@code null}\n\t * @since 5.8.8\n\t */\n\tpublic static <E extends Enum<E>, C> E getBy(Func1<E, C> condition, C value, E defaultEnum) {\n\t\treturn ObjectUtil.defaultIfNull(getBy(condition, value), defaultEnum);\n\t}\n\n\t/**\n\t * 通过 某字段对应值 获取 枚举中另一字段值，获取不到时为 {@code null}\n\t *\n\t * @param field     你想要获取的字段，{@code null}返回{@code null}\n\t * @param condition 条件字段，{@code null}返回{@code null}\n\t * @param value     条件字段值\n\t * @param <E>       枚举类型\n\t * @param <F>       想要获取的字段类型\n\t * @param <C>       条件字段类型\n\t * @return 对应枚举中另一字段值 ，获取不到时为 {@code null}\n\t * @since 5.8.0\n\t */\n\tpublic static <E extends Enum<E>, F, C> F getFieldBy(Func1<E, F> field, Function<E, C> condition, C value) {\n\t\tif (null == field || null == condition) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Class<E> implClass = LambdaUtil.getRealClass(field);\n\t\treturn Arrays.stream(getEnums(implClass))\n\t\t\t// 过滤\n\t\t\t.filter(constant -> ObjUtil.equals(condition.apply(constant), value))\n\t\t\t// 获取第一个并转换为结果\n\t\t\t.findFirst()\n\t\t\t.map(field::callWithRuntimeException)\n\t\t\t.orElse(null);\n\t}\n\n\t/**\n\t * 获取枚举字符串值和枚举对象的Map对应，使用LinkedHashMap保证有序<br>\n\t * 结果中键为枚举名，值为枚举对象\n\t *\n\t * @param <E>       枚举类型\n\t * @param enumClass 枚举类\n\t * @return 枚举字符串值和枚举对象的Map对应，使用LinkedHashMap保证有序\n\t * @since 4.0.2\n\t */\n\tpublic static <E extends Enum<E>> LinkedHashMap<String, E> getEnumMap(final Class<E> enumClass) {\n\t\tif (null == enumClass) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal LinkedHashMap<String, E> map = new LinkedHashMap<>();\n\t\tfor (final E e : getEnums(enumClass)) {\n\t\t\tmap.put(e.name(), e);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 获得枚举名对应指定字段值的Map<br>\n\t * 键为枚举名，值为字段值\n\t *\n\t * @param clazz     枚举类\n\t * @param fieldName 字段名，最终调用getXXX方法\n\t * @return 枚举名对应指定字段值的Map\n\t */\n\tpublic static Map<String, Object> getNameFieldMap(Class<? extends Enum<?>> clazz, String fieldName) {\n\t\tif (null == clazz || StrUtil.isBlank(fieldName)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Enum<?>[] enums = getEnums2(clazz);\n\t\tif (null == enums) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Map<String, Object> map = MapUtil.newHashMap(enums.length, true);\n\t\tfor (Enum<?> e : enums) {\n\t\t\tmap.put(e.name(), ReflectUtil.getFieldValue(e, fieldName));\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 判断某个值是存在枚举中\n\t *\n\t * @param <E>       枚举类型\n\t * @param enumClass 枚举类\n\t * @param val       需要查找的值\n\t * @return 是否存在\n\t */\n\tpublic static <E extends Enum<E>> boolean contains(final Class<E> enumClass, String val) {\n\t\tfinal LinkedHashMap<String, E> enumMap = getEnumMap(enumClass);\n\t\tif (CollUtil.isEmpty(enumMap)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn enumMap.containsKey(val);\n\t}\n\n\t/**\n\t * 判断某个值是不存在枚举中\n\t *\n\t * @param <E>       枚举类型\n\t * @param enumClass 枚举类\n\t * @param val       需要查找的值\n\t * @return 是否不存在\n\t */\n\tpublic static <E extends Enum<E>> boolean notContains(final Class<E> enumClass, String val) {\n\t\treturn false == contains(enumClass, val);\n\t}\n\n\t/**\n\t * 忽略大小检查某个枚举值是否匹配指定值\n\t *\n\t * @param e   枚举值\n\t * @param val 需要判断的值\n\t * @return 是非匹配\n\t */\n\tpublic static boolean equalsIgnoreCase(final Enum<?> e, String val) {\n\t\treturn StrUtil.equalsIgnoreCase(toString(e), val);\n\t}\n\n\t/**\n\t * 检查某个枚举值是否匹配指定值\n\t *\n\t * @param e   枚举值\n\t * @param val 需要判断的值\n\t * @return 是非匹配\n\t */\n\tpublic static boolean equals(final Enum<?> e, String val) {\n\t\treturn StrUtil.equals(toString(e), val);\n\t}\n\n\t/**\n\t * 获取枚举类中的枚举值，调用过的枚举值会缓存，下次调用会从缓存中获取\n\t *\n\t * @param <E>       枚举类型\n\t * @param enumClass 枚举类\n\t * @return 枚举类中的枚举值\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static <E extends Enum<E>> E[] getEnums(final Class<E> enumClass) {\n\t\treturn (E[]) getEnums2(enumClass);\n\t}\n\n\t/**\n\t * 获取枚举类中的枚举值，调用过的枚举值会缓存，下次调用会从缓存中获取\n\t *\n\t * @param enumClass 枚举类\n\t * @return 枚举类中的枚举值\n\t */\n\tprivate static Enum<?>[] getEnums2(final Class<? extends Enum<?>> enumClass) {\n\t\tif (null == enumClass) {\n\t\t\treturn null;\n\t\t}\n\t\t// fix issue#IDQYJK: 避免 ConcurrentHashMap.computeIfAbsent 在枚举类静态初始化时\n\t\t// 递归调用导致 IllegalStateException: Recursive update 的问题\n\t\t// 使用 get + putIfAbsent 替代 computeIfAbsent，安全支持递归场景\n\t\tEnum<?>[] enums = CACHE.get(enumClass);\n\t\tif (null == enums) {\n\t\t\tenums = enumClass.getEnumConstants();\n\t\t\tif (null != enums) {\n\t\t\t\tCACHE.putIfAbsent(enumClass, enums);\n\t\t\t}\n\t\t}\n\t\treturn enums;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.text.escape.Html4Escape;\nimport cn.hutool.core.text.escape.Html4Unescape;\nimport cn.hutool.core.text.escape.XmlEscape;\nimport cn.hutool.core.text.escape.XmlUnescape;\n\n/**\n * 转义和反转义工具类Escape / Unescape<br>\n * escape采用ISO Latin字符集对指定的字符串进行编码。<br>\n * 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。\n * TODO 7.x迁移到core.text.escape包下\n *\n * @author xiaoleilu\n */\npublic class EscapeUtil {\n\n\t/**\n\t * 不转义的符号编码\n\t */\n\tprivate static final String NOT_ESCAPE_CHARS = \"*@-_+./\";\n\tprivate static final Filter<Character> JS_ESCAPE_FILTER = c -> !(\n\t\tCharacter.isDigit(c)\n\t\t\t|| Character.isLowerCase(c)\n\t\t\t|| Character.isUpperCase(c)\n\t\t\t|| StrUtil.contains(NOT_ESCAPE_CHARS, c)\n\t);\n\n\t/**\n\t * 转义XML中的特殊字符<br>\n\t * <pre>\n\t * \t &amp; (ampersand) 替换为 &amp;amp;\n\t * \t &lt; (less than) 替换为 &amp;lt;\n\t * \t &gt; (greater than) 替换为 &amp;gt;\n\t * \t &quot; (double quote) 替换为 &amp;quot;\n\t * \t ' (single quote / apostrophe) 替换为 &amp;apos;\n\t * </pre>\n\t *\n\t * @param xml XML文本\n\t * @return 转义后的文本\n\t * @since 5.7.2\n\t */\n\tpublic static String escapeXml(CharSequence xml) {\n\t\tXmlEscape escape = new XmlEscape();\n\t\treturn escape.replace(xml).toString();\n\t}\n\n\t/**\n\t * 反转义XML中的特殊字符\n\t *\n\t * @param xml XML文本\n\t * @return 转义后的文本\n\t * @since 5.7.2\n\t */\n\tpublic static String unescapeXml(CharSequence xml) {\n\t\tXmlUnescape unescape = new XmlUnescape();\n\t\treturn unescape.replace(xml).toString();\n\t}\n\n\t/**\n\t * 转义HTML4中的特殊字符\n\t *\n\t * @param html HTML文本\n\t * @return 转义后的文本\n\t * @since 4.1.5\n\t */\n\tpublic static String escapeHtml4(CharSequence html) {\n\t\tHtml4Escape escape = new Html4Escape();\n\t\treturn escape.replace(html).toString();\n\t}\n\n\t/**\n\t * 反转义HTML4中的特殊字符\n\t *\n\t * @param html HTML文本\n\t * @return 转义后的文本\n\t * @since 4.1.5\n\t */\n\tpublic static String unescapeHtml4(CharSequence html) {\n\t\tHtml4Unescape unescape = new Html4Unescape();\n\t\treturn unescape.replace(html).toString();\n\t}\n\n\t/**\n\t * Escape编码（Unicode）（等同于JS的escape()方法）<br>\n\t * 该方法不会对 ASCII 字母和数字进行编码，也不会对下面这些 ASCII 标点符号进行编码： * @ - _ + . / <br>\n\t * 其他所有的字符都会被转义序列替换。\n\t *\n\t * @param content 被转义的内容\n\t * @return 编码后的字符串\n\t */\n\tpublic static String escape(CharSequence content) {\n\t\treturn escape(content, JS_ESCAPE_FILTER);\n\t}\n\n\t/**\n\t * Escape编码（Unicode）<br>\n\t * 该方法不会对 ASCII 字母和数字进行编码。其他所有的字符都会被转义序列替换。\n\t *\n\t * @param content 被转义的内容\n\t * @return 编码后的字符串\n\t */\n\tpublic static String escapeAll(CharSequence content) {\n\t\treturn escape(content, c -> true);\n\t}\n\n\t/**\n\t * Escape编码（Unicode）<br>\n\t * 该方法不会对 ASCII 字母和数字进行编码。其他所有的字符都会被转义序列替换。\n\t *\n\t * @param content 被转义的内容\n\t * @param filter  编码过滤器，对于过滤器中accept为false的字符不做编码\n\t * @return 编码后的字符串\n\t */\n\tpublic static String escape(CharSequence content, Filter<Character> filter) {\n\t\tif (StrUtil.isEmpty(content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal StringBuilder tmp = new StringBuilder(content.length() * 6);\n\t\tchar c;\n\t\tfor (int i = 0; i < content.length(); i++) {\n\t\t\tc = content.charAt(i);\n\t\t\tif (!filter.accept(c)) {\n\t\t\t\ttmp.append(c);\n\t\t\t} else if (c < 256) {\n\t\t\t\ttmp.append(\"%\");\n\t\t\t\tif (c < 16) {\n\t\t\t\t\ttmp.append(\"0\");\n\t\t\t\t}\n\t\t\t\ttmp.append(Integer.toString(c, 16));\n\t\t\t} else {\n\t\t\t\ttmp.append(\"%u\");\n\t\t\t\tif(c <= 0xfff){\n\t\t\t\t\t// issue#I49JU8@Gitee\n\t\t\t\t\ttmp.append(\"0\");\n\t\t\t\t}\n\t\t\t\ttmp.append(Integer.toString(c, 16));\n\t\t\t}\n\t\t}\n\t\treturn tmp.toString();\n\t}\n\n\t/**\n\t * Escape解码支持两种转义格式的解码：\n\t * <ul>\n\t *     <li>%XX - 两位十六进制数字，用于表示ASCII字符（0-255）</li>\n\t *     <li>%uXXXX - 四位十六进制数字，用于表示Unicode字符</li>\n\t * </ul>\n\t * <p>\n\t * 对于不完整的转义序列，本方法会将其原样保留而不抛出异常：\n\t * <ul>\n\t *     <li>字符串末尾的单独\"%\"字符会被原样保留</li>\n\t *     <li>\"%u\"后面不足4位十六进制数字时，整个不完整序列会被原样保留</li>\n\t *     <li>\"%\"后面不足2位十六进制数字时（非%u格式），整个不完整序列会被原样保留</li>\n\t * </ul>\n\t * 例如：\n\t * <pre>\n\t * unescape(\"test%\")      = \"test%\"     // 末尾的%被保留\n\t * unescape(\"test%u12\")   = \"test%u12\"  // 不足4位，原样保留\n\t * unescape(\"test%2\")     = \"test%2\"    // 不足2位，原样保留\n\t * unescape(\"test%20\")    = \"test \"     // 正常解码空格\n\t * unescape(\"test%u4E2D\") = \"test中\"    // 正常解码中文字符\n\t * </pre>\n\t *\n\t * @param content 被转义的内容\n\t * @return 解码后的字符串\n\t */\n\tpublic static String unescape(final String content) {\n\t\tif (StrUtil.isBlank(content)) {\n\t\t\treturn content;\n\t\t}\n\n\t\tfinal int len = content.length();\n\t\tfinal StringBuilder tmp = new StringBuilder(len);\n\t\tint lastPos = 0;\n\t\tint pos;\n\t\tchar ch;\n\t\twhile (lastPos < len) {\n\t\t\tpos = content.indexOf(\"%\", lastPos);\n\t\t\tif (pos == lastPos) {\n\t\t\t\tif (pos + 1 < len && content.charAt(pos + 1) == 'u') {\n\t\t\t\t\tif (pos + 6 <= len) {\n\t\t\t\t\t\tch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);\n\t\t\t\t\t\ttmp.append(ch);\n\t\t\t\t\t\tlastPos = pos + 6;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough characters, append as-is\n\t\t\t\t\t\ttmp.append(content.substring(pos));\n\t\t\t\t\t\tlastPos = len;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Check if there's enough characters for hex escape (%XX)\n\t\t\t\t\tif (pos + 3 <= len) {\n\t\t\t\t\t\tch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);\n\t\t\t\t\t\ttmp.append(ch);\n\t\t\t\t\t\tlastPos = pos + 3;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough characters, append as-is\n\t\t\t\t\t\ttmp.append(content.substring(pos));\n\t\t\t\t\t\tlastPos = len;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (pos == -1) {\n\t\t\t\t\ttmp.append(content.substring(lastPos));\n\t\t\t\t\tlastPos = len;\n\t\t\t\t} else {\n\t\t\t\t\ttmp.append(content, lastPos, pos);\n\t\t\t\t\tlastPos = pos;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn tmp.toString();\n\t}\n\n\t/**\n\t * 安全的unescape文本，当文本不是被escape的时候，返回原文。\n\t *\n\t * @param content 内容\n\t * @return 解码后的字符串，如果解码失败返回原字符串\n\t */\n\tpublic static String safeUnescape(String content) {\n\t\ttry {\n\t\t\treturn unescape(content);\n\t\t} catch (Exception e) {\n\t\t\t// Ignore Exception\n\t\t}\n\t\treturn content;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/HashUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.hash.CityHash;\nimport cn.hutool.core.lang.hash.MetroHash;\nimport cn.hutool.core.lang.hash.MurmurHash;\nimport cn.hutool.core.lang.hash.Number128;\n\n/**\n * Hash算法大全<br>\n * 推荐使用FNV1算法\n *\n * @author Goodzzp, Looly\n */\npublic class HashUtil {\n\n\t/**\n\t * 加法hash\n\t *\n\t * @param key   字符串\n\t * @param prime 一个质数\n\t * @return hash结果\n\t */\n\tpublic static int additiveHash(String key, int prime) {\n\t\tint hash, i;\n\t\tfor (hash = key.length(), i = 0; i < key.length(); i++) {\n\t\t\thash += key.charAt(i);\n\t\t}\n\t\treturn hash % prime;\n\t}\n\n\t/**\n\t * 旋转hash\n\t *\n\t * @param key   输入字符串\n\t * @param prime 质数\n\t * @return hash值\n\t */\n\tpublic static int rotatingHash(String key, int prime) {\n\t\tint hash, i;\n\t\tfor (hash = key.length(), i = 0; i < key.length(); ++i) {\n\t\t\thash = (hash << 4) ^ (hash >> 28) ^ key.charAt(i);\n\t\t}\n\n\t\t// 使用：hash = (hash ^ (hash>>10) ^ (hash>>20)) & mask;\n\t\t// 替代：hash %= prime;\n\t\t// return (hash ^ (hash>>10) ^ (hash>>20));\n\t\treturn hash % prime;\n\t}\n\n\t/**\n\t * 一次一个hash\n\t *\n\t * @param key 输入字符串\n\t * @return 输出hash值\n\t */\n\tpublic static int oneByOneHash(String key) {\n\t\tint hash, i;\n\t\tfor (hash = 0, i = 0; i < key.length(); ++i) {\n\t\t\thash += key.charAt(i);\n\t\t\thash += (hash << 10);\n\t\t\thash ^= (hash >> 6);\n\t\t}\n\t\thash += (hash << 3);\n\t\thash ^= (hash >> 11);\n\t\thash += (hash << 15);\n\t\t// return (hash & M_MASK);\n\t\treturn hash;\n\t}\n\n\t/**\n\t * Bernstein's hash\n\t *\n\t * @param key 输入字节数组\n\t * @return 结果hash\n\t */\n\tpublic static int bernstein(String key) {\n\t\tint hash = 0;\n\t\tint i;\n\t\tfor (i = 0; i < key.length(); ++i) {\n\t\t\thash = 33 * hash + key.charAt(i);\n\t\t}\n\t\treturn hash;\n\t}\n\n\t/**\n\t * Universal Hashing\n\t *\n\t * @param key  字节数组\n\t * @param mask 掩码\n\t * @param tab  tab\n\t * @return hash值\n\t */\n\tpublic static int universal(char[] key, int mask, int[] tab) {\n\t\tint hash = key.length, i, len = key.length;\n\t\tfor (i = 0; i < (len << 3); i += 8) {\n\t\t\tchar k = key[i >> 3];\n\t\t\tif ((k & 0x01) == 0) {\n\t\t\t\thash ^= tab[i];\n\t\t\t}\n\t\t\tif ((k & 0x02) == 0) {\n\t\t\t\thash ^= tab[i + 1];\n\t\t\t}\n\t\t\tif ((k & 0x04) == 0) {\n\t\t\t\thash ^= tab[i + 2];\n\t\t\t}\n\t\t\tif ((k & 0x08) == 0) {\n\t\t\t\thash ^= tab[i + 3];\n\t\t\t}\n\t\t\tif ((k & 0x10) == 0) {\n\t\t\t\thash ^= tab[i + 4];\n\t\t\t}\n\t\t\tif ((k & 0x20) == 0) {\n\t\t\t\thash ^= tab[i + 5];\n\t\t\t}\n\t\t\tif ((k & 0x40) == 0) {\n\t\t\t\thash ^= tab[i + 6];\n\t\t\t}\n\t\t\tif ((k & 0x80) == 0) {\n\t\t\t\thash ^= tab[i + 7];\n\t\t\t}\n\t\t}\n\t\treturn (hash & mask);\n\t}\n\n\t/**\n\t * Zobrist Hashing\n\t *\n\t * @param key  字节数组\n\t * @param mask 掩码\n\t * @param tab  tab\n\t * @return hash值\n\t */\n\tpublic static int zobrist(char[] key, int mask, int[][] tab) {\n\t\tint hash, i;\n\t\tfor (hash = key.length, i = 0; i < key.length; ++i) {\n\t\t\thash ^= tab[i][key[i]];\n\t\t}\n\t\treturn (hash & mask);\n\t}\n\n\t/**\n\t * 改进的32位FNV算法1\n\t *\n\t * @param data 数组\n\t * @return hash结果\n\t */\n\tpublic static int fnvHash(byte[] data) {\n\t\tfinal int p = 16777619;\n\t\tint hash = (int) 2166136261L;\n\t\tfor (byte b : data) {\n\t\t\thash = (hash ^ b) * p;\n\t\t}\n\t\thash += hash << 13;\n\t\thash ^= hash >> 7;\n\t\thash += hash << 3;\n\t\thash ^= hash >> 17;\n\t\thash += hash << 5;\n\t\treturn Math.abs(hash);\n\t}\n\n\t/**\n\t * 改进的32位FNV算法1\n\t *\n\t * @param data 字符串\n\t * @return hash结果\n\t */\n\tpublic static int fnvHash(String data) {\n\t\tfinal int p = 16777619;\n\t\tint hash = (int) 2166136261L;\n\t\tfor (int i = 0; i < data.length(); i++) {\n\t\t\thash = (hash ^ data.charAt(i)) * p;\n\t\t}\n\t\thash += hash << 13;\n\t\thash ^= hash >> 7;\n\t\thash += hash << 3;\n\t\thash ^= hash >> 17;\n\t\thash += hash << 5;\n\t\treturn Math.abs(hash);\n\t}\n\n\t/**\n\t * Thomas Wang的算法，整数hash\n\t *\n\t * @param key 整数\n\t * @return hash值\n\t */\n\tpublic static int intHash(int key) {\n\t\tkey += ~(key << 15);\n\t\tkey ^= (key >>> 10);\n\t\tkey += (key << 3);\n\t\tkey ^= (key >>> 6);\n\t\tkey += ~(key << 11);\n\t\tkey ^= (key >>> 16);\n\t\treturn key;\n\t}\n\n\t/**\n\t * RS算法hash\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int rsHash(String str) {\n\t\tint b = 378551;\n\t\tint a = 63689;\n\t\tint hash = 0;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = hash * a + str.charAt(i);\n\t\t\ta = a * b;\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * JS算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int jsHash(String str) {\n\t\tint hash = 1315423911;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash ^= ((hash << 5) + str.charAt(i) + (hash >> 2));\n\t\t}\n\n\t\treturn Math.abs(hash) & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * PJW算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int pjwHash(String str) {\n\t\tint bitsInUnsignedInt = 32;\n\t\tint threeQuarters = (bitsInUnsignedInt * 3) / 4;\n\t\tint oneEighth = bitsInUnsignedInt / 8;\n\t\tint highBits = 0xFFFFFFFF << (bitsInUnsignedInt - oneEighth);\n\t\tint hash = 0;\n\t\tint test;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = (hash << oneEighth) + str.charAt(i);\n\n\t\t\tif ((test = hash & highBits) != 0) {\n\t\t\t\thash = ((hash ^ (test >> threeQuarters)) & (~highBits));\n\t\t\t}\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * ELF算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int elfHash(String str) {\n\t\tint hash = 0;\n\t\tint x;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = (hash << 4) + str.charAt(i);\n\t\t\tif ((x = (int) (hash & 0xF0000000L)) != 0) {\n\t\t\t\thash ^= (x >> 24);\n\t\t\t\thash &= ~x;\n\t\t\t}\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * BKDR算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int bkdrHash(String str) {\n\t\tint seed = 131; // 31 131 1313 13131 131313 etc..\n\t\tint hash = 0;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = (hash * seed) + str.charAt(i);\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * SDBM算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int sdbmHash(String str) {\n\t\tint hash = 0;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = str.charAt(i) + (hash << 6) + (hash << 16) - hash;\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * DJB算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int djbHash(String str) {\n\t\tint hash = 5381;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = ((hash << 5) + hash) + str.charAt(i);\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * DEK算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int dekHash(String str) {\n\t\tint hash = str.length();\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash = ((hash << 5) ^ (hash >> 27)) ^ str.charAt(i);\n\t\t}\n\n\t\treturn hash & 0x7FFFFFFF;\n\t}\n\n\t/**\n\t * AP算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int apHash(String str) {\n\t\tint hash = 0;\n\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\thash ^= ((i & 1) == 0) ? ((hash << 7) ^ str.charAt(i) ^ (hash >> 3)) : (~((hash << 11) ^ str.charAt(i) ^ (hash >> 5)));\n\t\t}\n\n\t\t// return (hash & 0x7FFFFFFF);\n\t\treturn hash;\n\t}\n\n\t/**\n\t * TianL Hash算法\n\t *\n\t * @param str 字符串\n\t * @return Hash值\n\t */\n\tpublic static long tianlHash(String str) {\n\t\tlong hash;\n\n\t\tint iLength = str.length();\n\t\tif (iLength == 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (iLength <= 256) {\n\t\t\thash = 16777216L * (iLength - 1);\n\t\t} else {\n\t\t\thash = 4278190080L;\n\t\t}\n\n\t\tint i;\n\n\t\tchar ucChar;\n\n\t\tif (iLength <= 96) {\n\t\t\tfor (i = 1; i <= iLength; i++) {\n\t\t\t\tucChar = str.charAt(i - 1);\n\t\t\t\tif (ucChar <= 'Z' && ucChar >= 'A') {\n\t\t\t\t\tucChar = (char) (ucChar + 32);\n\t\t\t\t}\n\t\t\t\thash += (3L * i * ucChar * ucChar + 5L * i * ucChar + 7L * i + 11 * ucChar) % 16777216;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (i = 1; i <= 96; i++) {\n\t\t\t\tucChar = str.charAt(i + iLength - 96 - 1);\n\t\t\t\tif (ucChar <= 'Z' && ucChar >= 'A') {\n\t\t\t\t\tucChar = (char) (ucChar + 32);\n\t\t\t\t}\n\t\t\t\thash += (3L * i * ucChar * ucChar + 5L * i * ucChar + 7L * i + 11 * ucChar) % 16777216;\n\t\t\t}\n\t\t}\n\t\tif (hash < 0) {\n\t\t\thash *= -1;\n\t\t}\n\t\treturn hash;\n\t}\n\n\t/**\n\t * JAVA自己带的算法\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static int javaDefaultHash(String str) {\n\t\tint h = 0;\n\t\tint off = 0;\n\t\tint len = str.length();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\th = 31 * h + str.charAt(off++);\n\t\t}\n\t\treturn h;\n\t}\n\n\t/**\n\t * 混合hash算法，输出64位的值\n\t *\n\t * @param str 字符串\n\t * @return hash值\n\t */\n\tpublic static long mixHash(String str) {\n\t\tlong hash = str.hashCode();\n\t\thash <<= 32;\n\t\thash |= fnvHash(str);\n\t\treturn hash;\n\t}\n\n\t/**\n\t * 根据对象的内存地址生成相应的Hash值\n\t *\n\t * @param obj 对象\n\t * @return hash值\n\t * @since 4.2.2\n\t */\n\tpublic static int identityHashCode(Object obj) {\n\t\treturn System.identityHashCode(obj);\n\t}\n\n\t/**\n\t * MurmurHash算法32-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 4.3.3\n\t */\n\tpublic static int murmur32(byte[] data) {\n\t\treturn MurmurHash.hash32(data);\n\t}\n\n\t/**\n\t * MurmurHash算法64-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 4.3.3\n\t */\n\tpublic static long murmur64(byte[] data) {\n\t\treturn MurmurHash.hash64(data);\n\t}\n\n\t/**\n\t * MurmurHash算法128-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 4.3.3\n\t */\n\tpublic static long[] murmur128(byte[] data) {\n\t\treturn MurmurHash.hash128(data);\n\t}\n\n\t/**\n\t * CityHash算法32-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 5.2.5\n\t */\n\tpublic static int cityHash32(byte[] data) {\n\t\treturn CityHash.hash32(data);\n\t}\n\n\t/**\n\t * CityHash算法64-bit实现，种子1使用默认的CityHash#k2\n\t *\n\t * @param data 数据\n\t * @param seed 种子2\n\t * @return hash值\n\t * @since 5.2.5\n\t */\n\tpublic static long cityHash64(byte[] data, long seed) {\n\t\treturn CityHash.hash64(data, seed);\n\t}\n\n\t/**\n\t * CityHash算法64-bit实现，种子1使用默认的CityHash#k2\n\t *\n\t * @param data  数据\n\t * @param seed0 种子1\n\t * @param seed1 种子2\n\t * @return hash值\n\t * @since 5.2.5\n\t */\n\tpublic static long cityHash64(byte[] data, long seed0, long seed1) {\n\t\treturn CityHash.hash64(data, seed0, seed1);\n\t}\n\n\t/**\n\t * CityHash算法64-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 5.2.5\n\t */\n\tpublic static long cityHash64(byte[] data) {\n\t\treturn CityHash.hash64(data);\n\t}\n\n\t/**\n\t * CityHash算法128-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t * @since 5.2.5\n\t */\n\tpublic static long[] cityHash128(byte[] data) {\n\t\treturn CityHash.hash128(data).getLongArray();\n\t}\n\n\t/**\n\t * CityHash算法128-bit实现\n\t *\n\t * @param data 数据\n\t * @param seed 种子\n\t * @return hash值，long[0]：低位，long[1]：高位\n\t * @since 5.2.5\n\t */\n\tpublic static long[] cityHash128(byte[] data, Number128 seed) {\n\t\treturn CityHash.hash128(data, seed).getLongArray();\n\t}\n\n\t/**\n\t * MetroHash 算法64-bit实现\n\t *\n\t * @param data 数据\n\t * @param seed 种子\n\t * @return hash值\n\t */\n\tpublic static long metroHash64(byte[] data, long seed) {\n\t\treturn MetroHash.hash64(data, seed);\n\t}\n\n\t/**\n\t * MetroHash 算法64-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值\n\t */\n\tpublic static long metroHash64(byte[] data) {\n\t\treturn MetroHash.hash64(data);\n\t}\n\n\t/**\n\t * MetroHash 算法128-bit实现\n\t *\n\t * @param data 数据\n\t * @param seed 种子\n\t * @return hash值，long[0]：低位，long[1]：高位\n\t */\n\tpublic static long[] metroHash128(byte[] data, long seed) {\n\t\treturn MetroHash.hash128(data, seed).getLongArray();\n\t}\n\n\t/**\n\t * MetroHash 算法128-bit实现\n\t *\n\t * @param data 数据\n\t * @return hash值，long[0]：低位，long[1]：高位\n\t */\n\tpublic static long[] metroHash128(byte[] data) {\n\t\treturn MetroHash.hash128(data).getLongArray();\n\t}\n\n\t/**\n\t * HF Hash算法\n\t *\n\t * @param data 字符串\n\t * @return hash结果\n\t * @since 5.8.0\n\t */\n\tpublic static long hfHash(String data) {\n\t\tint length = data.length();\n\t\tlong hash = 0;\n\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\thash += (long) data.charAt(i) * 3 * i;\n\t\t}\n\n\t\tif (hash < 0) {\n\t\t\thash = -hash;\n\t\t}\n\n\t\treturn hash;\n\t}\n\n\t/**\n\t * HFIP Hash算法\n\t *\n\t * @param data 字符串\n\t * @return hash结果\n\t * @since 5.8.0\n\t */\n\tpublic static long hfIpHash(String data) {\n\t\tint length = data.length();\n\t\tlong hash = 0;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\thash += data.charAt(i % 4) ^ data.charAt(i);\n\t\t}\n\t\treturn hash;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/HexUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.codec.Base16Codec;\nimport cn.hutool.core.exceptions.UtilException;\n\nimport java.awt.Color;\nimport java.math.BigInteger;\nimport java.nio.charset.Charset;\n\n/**\n * 十六进制（简写为hex或下标16）在数学中是一种逢16进1的进位制，一般用数字0到9和字母A到F表示（其中:A~F即10~15）。<br>\n * 例如十进制数57，在二进制写作111001，在16进制写作39。<br>\n * 像java,c这样的语言为了区分十六进制和十进制数值,会在十六进制数的前面加上 0x,比如0x20是十进制的32,而不是十进制的20<br>\n * <p>\n * 参考：https://my.oschina.net/xinxingegeya/blog/287476\n *\n * @author Looly\n */\npublic class HexUtil {\n\n\t/**\n\t * 判断给定字符串是否为16进制数<br>\n\t * 如果是，需要使用对应数字类型对象的{@code decode}方法解码<br>\n\t * 例如：{@code Integer.decode}方法解码int类型的16进制数字\n\t *\n\t * @param value 值\n\t * @return 是否为16进制\n\t */\n\tpublic static boolean isHexNumber(String value) {\n\t\tif (StrUtil.isEmpty(value)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (StrUtil.startWith(value, '-')) {\n\t\t\t// issue#2875\n\t\t\treturn false;\n\t\t}\n\t\tint index = 0;\n\t\tif (value.startsWith(\"0x\", index) || value.startsWith(\"0X\", index)) {\n\t\t\tindex += 2;\n\t\t} else if (value.startsWith(\"#\", index)) {\n\t\t\tindex++;\n\t\t}\n\t\ttry {\n\t\t\tnew BigInteger(value.substring(index), 16);\n\t\t} catch (final NumberFormatException e) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------- encode\n\n\t/**\n\t * 将字节数组转换为十六进制字符数组\n\t *\n\t * @param data byte[]\n\t * @return 十六进制char[]\n\t */\n\tpublic static char[] encodeHex(byte[] data) {\n\t\treturn encodeHex(data, true);\n\t}\n\n\t/**\n\t * 将字节数组转换为十六进制字符数组\n\t *\n\t * @param str     字符串\n\t * @param charset 编码\n\t * @return 十六进制char[]\n\t */\n\tpublic static char[] encodeHex(String str, Charset charset) {\n\t\treturn encodeHex(StrUtil.bytes(str, charset), true);\n\t}\n\n\t/**\n\t * 将字节数组转换为十六进制字符数组\n\t *\n\t * @param data        byte[]\n\t * @param toLowerCase {@code true} 传换成小写格式 ， {@code false} 传换成大写格式\n\t * @return 十六进制char[]\n\t */\n\tpublic static char[] encodeHex(byte[] data, boolean toLowerCase) {\n\t\treturn (toLowerCase ? Base16Codec.CODEC_LOWER : Base16Codec.CODEC_UPPER).encode(data);\n\t}\n\n\t/**\n\t * 将字节数组转换为十六进制字符串\n\t *\n\t * @param data byte[]\n\t * @return 十六进制String\n\t */\n\tpublic static String encodeHexStr(byte[] data) {\n\t\treturn encodeHexStr(data, true);\n\t}\n\n\t/**\n\t * 将字符串转换为十六进制字符串，结果为小写\n\t *\n\t * @param data    需要被编码的字符串\n\t * @param charset 编码\n\t * @return 十六进制String\n\t */\n\tpublic static String encodeHexStr(String data, Charset charset) {\n\t\treturn encodeHexStr(StrUtil.bytes(data, charset), true);\n\t}\n\n\t/**\n\t * 将字符串转换为十六进制字符串，结果为小写，默认编码是UTF-8\n\t *\n\t * @param data 被编码的字符串\n\t * @return 十六进制String\n\t */\n\tpublic static String encodeHexStr(String data) {\n\t\treturn encodeHexStr(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将字节数组转换为十六进制字符串\n\t *\n\t * @param data        byte[]\n\t * @param toLowerCase {@code true} 传换成小写格式 ， {@code false} 传换成大写格式\n\t * @return 十六进制String\n\t */\n\tpublic static String encodeHexStr(byte[] data, boolean toLowerCase) {\n\t\treturn new String(encodeHex(data, toLowerCase));\n\t}\n\n\t// ---------------------------------------------------------------------------------------------------- decode\n\n\t/**\n\t * 将十六进制字符数组转换为字符串，默认编码UTF-8\n\t *\n\t * @param hexStr 十六进制String\n\t * @return 字符串\n\t */\n\tpublic static String decodeHexStr(String hexStr) {\n\t\treturn decodeHexStr(hexStr, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将十六进制字符数组转换为字符串\n\t *\n\t * @param hexStr  十六进制String\n\t * @param charset 编码\n\t * @return 字符串\n\t */\n\tpublic static String decodeHexStr(String hexStr, Charset charset) {\n\t\tif (StrUtil.isEmpty(hexStr)) {\n\t\t\treturn hexStr;\n\t\t}\n\t\treturn StrUtil.str(decodeHex(hexStr), charset);\n\t}\n\n\t/**\n\t * 将十六进制字符数组转换为字符串\n\t *\n\t * @param hexData 十六进制char[]\n\t * @param charset 编码\n\t * @return 字符串\n\t */\n\tpublic static String decodeHexStr(char[] hexData, Charset charset) {\n\t\treturn StrUtil.str(decodeHex(hexData), charset);\n\t}\n\n\t/**\n\t * 将十六进制字符串解码为byte[]\n\t *\n\t * @param hexStr 十六进制String\n\t * @return byte[]\n\t */\n\tpublic static byte[] decodeHex(String hexStr) {\n\t\treturn decodeHex((CharSequence) hexStr);\n\t}\n\n\t/**\n\t * 将十六进制字符数组转换为字节数组\n\t *\n\t * @param hexData 十六进制char[]\n\t * @return byte[]\n\t * @throws RuntimeException 如果源十六进制字符数组是一个奇怪的长度，将抛出运行时异常\n\t */\n\tpublic static byte[] decodeHex(char[] hexData) {\n\t\treturn decodeHex(String.valueOf(hexData));\n\t}\n\n\t/**\n\t * 将十六进制字符数组转换为字节数组\n\t *\n\t * @param hexData 十六进制字符串\n\t * @return byte[]\n\t * @throws UtilException 如果源十六进制字符数组是一个奇怪的长度，将抛出运行时异常\n\t * @since 5.6.6\n\t */\n\tpublic static byte[] decodeHex(CharSequence hexData) {\n\t\treturn Base16Codec.CODEC_LOWER.decode(hexData);\n\t}\n\n\t// ---------------------------------------------------------------------------------------- Color\n\n\t/**\n\t * 将{@link Color}编码为Hex形式\n\t *\n\t * @param color {@link Color}\n\t * @return Hex字符串\n\t * @since 3.0.8\n\t */\n\tpublic static String encodeColor(Color color) {\n\t\treturn encodeColor(color, \"#\");\n\t}\n\n\t/**\n\t * 将{@link Color}编码为Hex形式\n\t *\n\t * @param color  {@link Color}\n\t * @param prefix 前缀字符串，可以是#、0x等\n\t * @return Hex字符串\n\t * @since 3.0.8\n\t */\n\tpublic static String encodeColor(Color color, String prefix) {\n\t\tfinal StringBuilder builder = new StringBuilder(prefix);\n\t\tString colorHex;\n\t\tcolorHex = Integer.toHexString(color.getRed());\n\t\tif (1 == colorHex.length()) {\n\t\t\tbuilder.append('0');\n\t\t}\n\t\tbuilder.append(colorHex);\n\t\tcolorHex = Integer.toHexString(color.getGreen());\n\t\tif (1 == colorHex.length()) {\n\t\t\tbuilder.append('0');\n\t\t}\n\t\tbuilder.append(colorHex);\n\t\tcolorHex = Integer.toHexString(color.getBlue());\n\t\tif (1 == colorHex.length()) {\n\t\t\tbuilder.append('0');\n\t\t}\n\t\tbuilder.append(colorHex);\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 将Hex颜色值转为\n\t *\n\t * @param hexColor 16进制颜色值，可以以#开头，也可以用0x开头\n\t * @return {@link Color}\n\t * @since 3.0.8\n\t */\n\tpublic static Color decodeColor(String hexColor) {\n\t\treturn Color.decode(hexColor);\n\t}\n\n\t/**\n\t * 将指定int值转换为Unicode字符串形式，常用于特殊字符（例如汉字）转Unicode形式<br>\n\t * 转换的字符串如果u后不足4位，则前面用0填充，例如：\n\t *\n\t * <pre>\n\t * '你' =》\\u4f60\n\t * </pre>\n\t *\n\t * @param value int值，也可以是char\n\t * @return Unicode表现形式\n\t */\n\tpublic static String toUnicodeHex(int value) {\n\t\tfinal StringBuilder builder = new StringBuilder(6);\n\n\t\tbuilder.append(\"\\\\u\");\n\t\tString hex = toHex(value);\n\t\tint len = hex.length();\n\t\tif (len < 4) {\n\t\t\tbuilder.append(\"0000\", 0, 4 - len);// 不足4位补0\n\t\t}\n\t\tbuilder.append(hex);\n\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 将指定char值转换为Unicode字符串形式，常用于特殊字符（例如汉字）转Unicode形式<br>\n\t * 转换的字符串如果u后不足4位，则前面用0填充，例如：\n\t *\n\t * <pre>\n\t * '你' =》'\\u4f60'\n\t * </pre>\n\t *\n\t * @param ch char值\n\t * @return Unicode表现形式\n\t * @since 4.0.1\n\t */\n\tpublic static String toUnicodeHex(char ch) {\n\t\treturn Base16Codec.CODEC_LOWER.toUnicodeHex(ch);\n\t}\n\n\t/**\n\t * 转为16进制字符串\n\t *\n\t * @param value int值\n\t * @return 16进制字符串\n\t * @since 4.4.1\n\t */\n\tpublic static String toHex(int value) {\n\t\treturn Integer.toHexString(value);\n\t}\n\n\t/**\n\t * 16进制字符串转为int\n\t *\n\t * @param value 16进制字符串\n\t * @return 16进制字符串int值\n\t * @since 5.7.4\n\t */\n\tpublic static int hexToInt(String value) {\n\t\treturn Integer.parseInt(removeHexPrefix(value), 16);\n\t}\n\n\t/**\n\t * 转为16进制字符串\n\t *\n\t * @param value int值\n\t * @return 16进制字符串\n\t * @since 4.4.1\n\t */\n\tpublic static String toHex(long value) {\n\t\treturn Long.toHexString(value);\n\t}\n\n\t/**\n\t * 16进制字符串转为long\n\t *\n\t * @param value 16进制字符串\n\t * @return long值\n\t * @since 5.7.4\n\t */\n\tpublic static long hexToLong(String value) {\n\t\treturn Long.parseLong(removeHexPrefix(value), 16);\n\t}\n\n\t/**\n\t * 转为16进制字符串\n\t *\n\t * @param value float值\n\t * @return 16进制字符串\n\t */\n\tpublic static String toHex(float value) {\n\t\treturn Integer.toHexString(Float.floatToIntBits(value));\n\t}\n\n\t/**\n\t * 16进制字符串转为float\n\t *\n\t * @param value 16进制字符串\n\t * @return 16进制字符串float值\n\t */\n\tpublic static float hexToFloat(String value) {\n\t\treturn Float.intBitsToFloat(Integer.parseUnsignedInt(removeHexPrefix(value), 16));\n\t}\n\n\t/**\n\t * 转为16进制字符串\n\t *\n\t * @param value double值\n\t * @return 16进制字符串\n\t */\n\tpublic static String toHex(double value) {\n\t\treturn Long.toHexString(Double.doubleToLongBits(value));\n\t}\n\n\t/**\n\t * 16进制字符串转为double\n\t *\n\t * @param value 16进制字符串\n\t * @return 16进制字符串double值\n\t */\n\tpublic static double hexToDouble(String value) {\n\t\treturn Double.longBitsToDouble(Long.parseUnsignedLong(removeHexPrefix(value), 16));\n\t}\n\n\t/**\n\t * 将byte值转为16进制并添加到{@link StringBuilder}中\n\t *\n\t * @param builder     {@link StringBuilder}\n\t * @param b           byte\n\t * @param toLowerCase 是否使用小写\n\t * @since 4.4.1\n\t */\n\tpublic static void appendHex(StringBuilder builder, byte b, boolean toLowerCase) {\n\t\t(toLowerCase ? Base16Codec.CODEC_LOWER : Base16Codec.CODEC_UPPER).appendHex(builder, b);\n\t}\n\n\t/**\n\t * Hex（16进制）字符串转为BigInteger\n\t *\n\t * @param hexStr Hex(16进制字符串)\n\t * @return {@link BigInteger}\n\t * @since 5.2.0\n\t */\n\tpublic static BigInteger toBigInteger(String hexStr) {\n\t\tif (null == hexStr) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new BigInteger(removeHexPrefix(hexStr), 16);\n\t}\n\n\t/**\n\t * 格式化Hex字符串，结果为每2位加一个空格，类似于：\n\t * <pre>\n\t *     e8 8c 67 03 80 cb 22 00 95 26 8f\n\t * </pre>\n\t *\n\t * @param hexStr Hex字符串\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(final String hexStr) {\n\t\treturn format(hexStr, StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 格式化Hex字符串，结果为每2位加一个空格，类似于：\n\t * <pre>\n\t *     e8 8c 67 03 80 cb 22 00 95 26 8f\n\t * </pre>\n\t *\n\t * @param hexStr Hex字符串\n\t * @param prefix 自定义前缀，如0x\n\t * @return 格式化后的字符串\n\t */\n\tpublic static String format(final String hexStr, String prefix) {\n\t\tif (StrUtil.isEmpty(hexStr)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tif (null == prefix) {\n\t\t\tprefix = StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal int length = hexStr.length();\n\t\tfinal StringBuilder builder = StrUtil.builder(length + length / 2 + (length / 2 * prefix.length()));\n\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tif (i % 2 == 0) {\n\t\t\t\tif (i != 0) {\n\t\t\t\t\tbuilder.append(CharUtil.SPACE);\n\t\t\t\t}\n\t\t\t\tbuilder.append(prefix);\n\t\t\t}\n\t\t\tbuilder.append(hexStr.charAt(i));\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 去除十六进制字符串的常见前缀 0x、0X、#\n\t *\n\t * @param hexStr 十六进制字符串\n\t * @return 去除前缀后的字符串\n\t */\n\tprivate static String removeHexPrefix(String hexStr) {\n\t\tif (StrUtil.length(hexStr) > 1) {\n\t\t\tfinal char c0 = hexStr.charAt(0);\n\t\t\tswitch (c0) {\n\t\t\t\tcase '0':\n\t\t\t\t\tif (hexStr.charAt(1) == 'x' || hexStr.charAt(1) == 'X') {\n\t\t\t\t\t\treturn hexStr.substring(2);\n\t\t\t\t\t}\n\t\t\t\tcase '#':\n\t\t\t\t\treturn hexStr.substring(1);\n\t\t\t}\n\t\t}\n\t\treturn hexStr;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.ObjectId;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.lang.Snowflake;\nimport cn.hutool.core.lang.UUID;\nimport cn.hutool.core.lang.id.NanoId;\nimport cn.hutool.core.net.NetUtil;\n\n/**\n * ID生成器工具类，此工具类中主要封装：\n *\n * <pre>\n * 1. 唯一性ID生成器：UUID、ObjectId（MongoDB）、Snowflake\n * </pre>\n *\n * <p>\n * ID相关文章见：http://calvin1978.blogcn.com/articles/uuid.html\n *\n * @author looly\n * @since 4.1.13\n */\npublic class IdUtil {\n\n\t// ------------------------------------------------------------------- UUID\n\n\t/**\n\t * 获取随机UUID\n\t *\n\t * @return 随机UUID\n\t */\n\tpublic static String randomUUID() {\n\t\treturn UUID.randomUUID().toString();\n\t}\n\n\t/**\n\t * 简化的UUID，去掉了横线\n\t *\n\t * @return 简化的UUID，去掉了横线\n\t */\n\tpublic static String simpleUUID() {\n\t\treturn UUID.randomUUID().toString(true);\n\t}\n\n\t/**\n\t * 获取随机UUID，使用性能更好的ThreadLocalRandom生成UUID\n\t *\n\t * @return 随机UUID\n\t * @since 4.1.19\n\t */\n\tpublic static String fastUUID() {\n\t\treturn UUID.fastUUID().toString();\n\t}\n\n\t/**\n\t * 简化的UUID，去掉了横线，使用性能更好的ThreadLocalRandom生成UUID\n\t *\n\t * @return 简化的UUID，去掉了横线\n\t * @since 4.1.19\n\t */\n\tpublic static String fastSimpleUUID() {\n\t\treturn UUID.fastUUID().toString(true);\n\t}\n\n\t/**\n\t * 创建MongoDB ID生成策略实现<br>\n\t * ObjectId由以下几部分组成：\n\t *\n\t * <pre>\n\t * 1. Time 时间戳。\n\t * 2. Machine 所在主机的唯一标识符，一般是机器主机名的散列值。\n\t * 3. PID 进程ID。确保同一机器中不冲突\n\t * 4. INC 自增计数器。确保同一秒内产生objectId的唯一性。\n\t * </pre>\n\t * <p>\n\t * 参考：http://blog.csdn.net/qxc1281/article/details/54021882\n\t *\n\t * @return ObjectId\n\t */\n\tpublic static String objectId() {\n\t\treturn ObjectId.next();\n\t}\n\n\t/**\n\t * 创建Twitter的Snowflake 算法生成器。\n\t * <p>\n\t * 特别注意：此方法调用后会创建独立的{@link Snowflake}对象，每个独立的对象ID不互斥，会导致ID重复，请自行保证单例！\n\t * </p>\n\t * 分布式系统中，有一些需要使用全局唯一ID的场景，有些时候我们希望能使用一种简单一些的ID，并且希望ID能够按照时间有序生成。\n\t *\n\t * <p>\n\t * snowflake的结构如下(每部分用-分开):<br>\n\t *\n\t * <pre>\n\t * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n\t * </pre>\n\t * <p>\n\t * 第一位为未使用，接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>\n\t * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点）<br>\n\t * 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号）\n\t *\n\t * <p>\n\t * 参考：http://www.cnblogs.com/relucent/p/4955340.html\n\t *\n\t * @param workerId     终端ID\n\t * @param datacenterId 数据中心ID\n\t * @return {@link Snowflake}\n\t * @deprecated 此方法容易产生歧义：多个Snowflake实例产生的ID会产生重复，此对象在单台机器上必须单例，请使用{@link #getSnowflake(long, long)}\n\t */\n\t@Deprecated\n\tpublic static Snowflake createSnowflake(long workerId, long datacenterId) {\n\t\treturn new Snowflake(workerId, datacenterId);\n\t}\n\n\t/**\n\t * 获取单例的Twitter的Snowflake 算法生成器对象<br>\n\t * 分布式系统中，有一些需要使用全局唯一ID的场景，有些时候我们希望能使用一种简单一些的ID，并且希望ID能够按照时间有序生成。\n\t *\n\t * <p>\n\t * snowflake的结构如下(每部分用-分开):<br>\n\t *\n\t * <pre>\n\t * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n\t * </pre>\n\t * <p>\n\t * 第一位为未使用，接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>\n\t * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点）<br>\n\t * 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号）\n\t *\n\t * <p>\n\t * 参考：http://www.cnblogs.com/relucent/p/4955340.html\n\t *\n\t * @param workerId     终端ID\n\t * @param datacenterId 数据中心ID\n\t * @return {@link Snowflake}\n\t * @since 4.5.9\n\t */\n\tpublic static Snowflake getSnowflake(long workerId, long datacenterId) {\n\t\treturn Singleton.get(Snowflake.class, workerId, datacenterId);\n\t}\n\n\t/**\n\t * 获取单例的Twitter的Snowflake 算法生成器对象<br>\n\t * 分布式系统中，有一些需要使用全局唯一ID的场景，有些时候我们希望能使用一种简单一些的ID，并且希望ID能够按照时间有序生成。\n\t *\n\t * <p>\n\t * snowflake的结构如下(每部分用-分开):<br>\n\t *\n\t * <pre>\n\t * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n\t * </pre>\n\t * <p>\n\t * 第一位为未使用，接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>\n\t * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点）<br>\n\t * 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号）\n\t *\n\t * <p>\n\t * 参考：http://www.cnblogs.com/relucent/p/4955340.html\n\t *\n\t * @param workerId 终端ID\n\t * @return {@link Snowflake}\n\t * @since 5.7.3\n\t */\n\tpublic static Snowflake getSnowflake(long workerId) {\n\t\treturn Singleton.get(Snowflake.class, workerId);\n\t}\n\n\t/**\n\t * 获取单例的Twitter的Snowflake 算法生成器对象<br>\n\t * 分布式系统中，有一些需要使用全局唯一ID的场景，有些时候我们希望能使用一种简单一些的ID，并且希望ID能够按照时间有序生成。\n\t *\n\t * <p>\n\t * snowflake的结构如下(每部分用-分开):<br>\n\t *\n\t * <pre>\n\t * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n\t * </pre>\n\t * <p>\n\t * 第一位为未使用，接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>\n\t * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点）<br>\n\t * 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号）\n\t *\n\t * <p>\n\t * 参考：http://www.cnblogs.com/relucent/p/4955340.html\n\t *\n\t * @return {@link Snowflake}\n\t * @since 5.7.3\n\t */\n\tpublic static Snowflake getSnowflake() {\n\t\treturn Singleton.get(Snowflake.class);\n\t}\n\n\t/**\n\t * 获取数据中心ID<br>\n\t * 数据中心ID依赖于本地网卡MAC地址。\n\t * <p>\n\t * 此算法来自于mybatis-plus#Sequence\n\t * </p>\n\t *\n\t * @param maxDatacenterId 最大的中心ID\n\t * @return 数据中心ID\n\t * @since 5.7.3\n\t */\n\tpublic static long getDataCenterId(long maxDatacenterId) {\n\t\tAssert.isTrue(maxDatacenterId > 0, \"maxDatacenterId must be > 0\");\n\t\tif(maxDatacenterId == Long.MAX_VALUE){\n\t\t\tmaxDatacenterId -= 1;\n\t\t}\n\t\tlong id = 1L;\n\t\tbyte[] mac = null;\n\t\ttry{\n\t\t\tmac = NetUtil.getLocalHardwareAddress();\n\t\t}catch (UtilException ignore){\n\t\t\t// ignore\n\t\t}\n\t\tif (null != mac) {\n\t\t\tid = ((0x000000FF & (long) mac[mac.length - 2])\n\t\t\t\t\t| (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;\n\t\t\tid = id % (maxDatacenterId + 1);\n\t\t}\n\n\t\treturn id;\n\t}\n\n\t/**\n\t * 获取机器ID，使用进程ID配合数据中心ID生成<br>\n\t * 机器依赖于本进程ID或进程名的Hash值。\n\t *\n\t * <p>\n\t * 此算法来自于mybatis-plus#Sequence\n\t * </p>\n\t *\n\t * @param datacenterId 数据中心ID\n\t * @param maxWorkerId  最大的机器节点ID\n\t * @return ID\n\t * @since 5.7.3\n\t */\n\tpublic static long getWorkerId(long datacenterId, long maxWorkerId) {\n\t\tfinal StringBuilder mpid = new StringBuilder();\n\t\tmpid.append(datacenterId);\n\t\ttry {\n\t\t\tmpid.append(RuntimeUtil.getPid());\n\t\t} catch (UtilException igonre) {\n\t\t\t//ignore\n\t\t}\n\t\t/*\n\t\t * MAC + PID 的 hashcode 获取16个低位\n\t\t */\n\t\treturn (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);\n\t}\n\n\t// ------------------------------------------------------------------- NanoId\n\n\t/**\n\t * 获取随机NanoId\n\t *\n\t * @return 随机NanoId\n\t * @since 5.7.5\n\t */\n\tpublic static String nanoId() {\n\t\treturn NanoId.randomNanoId();\n\t}\n\n\t/**\n\t * 获取随机NanoId\n\t *\n\t * @param size ID中的字符数量\n\t * @return 随机NanoId\n\t * @since 5.7.5\n\t */\n\tpublic static String nanoId(int size) {\n\t\treturn NanoId.randomNanoId(size);\n\t}\n\n\t/**\n\t * 简单获取Snowflake 的 nextId\n\t * 终端ID 数据中心ID 默认为PID和MAC地址生成\n\t *\n\t * @return nextId\n\t * @since 5.7.18\n\t */\n\tpublic static long getSnowflakeNextId() {\n\t\treturn getSnowflake().nextId();\n\t}\n\n\t/**\n\t * 简单获取Snowflake 的 nextId\n\t * 终端ID 数据中心ID 默认为PID和MAC地址生成\n\t *\n\t * @return nextIdStr\n\t * @since 5.7.18\n\t */\n\tpublic static String getSnowflakeNextIdStr() {\n\t\treturn getSnowflake().nextIdStr();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.lang.Validator;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * 身份证相关工具类<br>\n * see <a href=\"https://www.oschina.net/code/snippet_1611_2881\">https://www.oschina.net/code/snippet_1611_2881</a>\n *\n * <p>\n * 本工具并没有对行政区划代码做校验，如有需求，请参阅（2018年10月）：\n * <a href=\"http://www.mca.gov.cn/article/sj/xzqh/2018/201804-12/20181011221630.html\">http://www.mca.gov.cn/article/sj/xzqh/2018/201804-12/20181011221630.html</a>\n * </p>\n *\n * @author Looly\n * @since 3.0.4\n */\npublic class IdcardUtil {\n\n\t/**\n\t * 中国公民身份证号码最小长度。\n\t */\n\tprivate static final int CHINA_ID_MIN_LENGTH = 15;\n\t/**\n\t * 中国公民身份证号码最大长度。\n\t */\n\tprivate static final int CHINA_ID_MAX_LENGTH = 18;\n\t/**\n\t * 每位加权因子\n\t */\n\tprivate static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};\n\t/**\n\t * 省市代码表\n\t */\n\tprivate static final Map<String, String> CITY_CODES = new HashMap<>();\n\t/**\n\t * 台湾身份首字母对应数字\n\t */\n\tprivate static final Map<Character, Integer> TW_FIRST_CODE = new HashMap<>();\n\n\tstatic {\n\t\tCITY_CODES.put(\"11\", \"北京\");\n\t\tCITY_CODES.put(\"12\", \"天津\");\n\t\tCITY_CODES.put(\"13\", \"河北\");\n\t\tCITY_CODES.put(\"14\", \"山西\");\n\t\tCITY_CODES.put(\"15\", \"内蒙古\");\n\t\tCITY_CODES.put(\"21\", \"辽宁\");\n\t\tCITY_CODES.put(\"22\", \"吉林\");\n\t\tCITY_CODES.put(\"23\", \"黑龙江\");\n\t\tCITY_CODES.put(\"31\", \"上海\");\n\t\tCITY_CODES.put(\"32\", \"江苏\");\n\t\tCITY_CODES.put(\"33\", \"浙江\");\n\t\tCITY_CODES.put(\"34\", \"安徽\");\n\t\tCITY_CODES.put(\"35\", \"福建\");\n\t\tCITY_CODES.put(\"36\", \"江西\");\n\t\tCITY_CODES.put(\"37\", \"山东\");\n\t\tCITY_CODES.put(\"41\", \"河南\");\n\t\tCITY_CODES.put(\"42\", \"湖北\");\n\t\tCITY_CODES.put(\"43\", \"湖南\");\n\t\tCITY_CODES.put(\"44\", \"广东\");\n\t\tCITY_CODES.put(\"45\", \"广西\");\n\t\tCITY_CODES.put(\"46\", \"海南\");\n\t\tCITY_CODES.put(\"50\", \"重庆\");\n\t\tCITY_CODES.put(\"51\", \"四川\");\n\t\tCITY_CODES.put(\"52\", \"贵州\");\n\t\tCITY_CODES.put(\"53\", \"云南\");\n\t\tCITY_CODES.put(\"54\", \"西藏\");\n\t\tCITY_CODES.put(\"61\", \"陕西\");\n\t\tCITY_CODES.put(\"62\", \"甘肃\");\n\t\tCITY_CODES.put(\"63\", \"青海\");\n\t\tCITY_CODES.put(\"64\", \"宁夏\");\n\t\tCITY_CODES.put(\"65\", \"新疆\");\n\t\tCITY_CODES.put(\"71\", \"台湾\");\n\t\tCITY_CODES.put(\"81\", \"香港\");\n\t\tCITY_CODES.put(\"82\", \"澳门\");\n\t\t//issue#1277，台湾身份证号码以83开头，但是行政区划为71\n\t\tCITY_CODES.put(\"83\", \"台湾\");\n\t\tCITY_CODES.put(\"91\", \"国外\");\n\n\t\tTW_FIRST_CODE.put('A', 10);\n\t\tTW_FIRST_CODE.put('B', 11);\n\t\tTW_FIRST_CODE.put('C', 12);\n\t\tTW_FIRST_CODE.put('D', 13);\n\t\tTW_FIRST_CODE.put('E', 14);\n\t\tTW_FIRST_CODE.put('F', 15);\n\t\tTW_FIRST_CODE.put('G', 16);\n\t\tTW_FIRST_CODE.put('H', 17);\n\t\tTW_FIRST_CODE.put('J', 18);\n\t\tTW_FIRST_CODE.put('K', 19);\n\t\tTW_FIRST_CODE.put('L', 20);\n\t\tTW_FIRST_CODE.put('M', 21);\n\t\tTW_FIRST_CODE.put('N', 22);\n\t\tTW_FIRST_CODE.put('P', 23);\n\t\tTW_FIRST_CODE.put('Q', 24);\n\t\tTW_FIRST_CODE.put('R', 25);\n\t\tTW_FIRST_CODE.put('S', 26);\n\t\tTW_FIRST_CODE.put('T', 27);\n\t\tTW_FIRST_CODE.put('U', 28);\n\t\tTW_FIRST_CODE.put('V', 29);\n\t\tTW_FIRST_CODE.put('X', 30);\n\t\tTW_FIRST_CODE.put('Y', 31);\n\t\tTW_FIRST_CODE.put('W', 32);\n\t\tTW_FIRST_CODE.put('Z', 33);\n\t\tTW_FIRST_CODE.put('I', 34);\n\t\tTW_FIRST_CODE.put('O', 35);\n\t}\n\n\t/**\n\t * 将15位身份证号码转换为18位\n\t *\n\t * @param idCard 15位身份编码\n\t * @return 18位身份编码\n\t */\n\tpublic static String convert15To18(String idCard) {\n\t\tStringBuilder idCard18;\n\t\tif (idCard.length() != CHINA_ID_MIN_LENGTH) {\n\t\t\treturn null;\n\t\t}\n\t\tif (ReUtil.isMatch(PatternPool.NUMBERS, idCard)) {\n\t\t\t// 获取出生年月日\n\t\t\tString birthday = idCard.substring(6, 12);\n\t\t\tDate birthDate = DateUtil.parse(birthday, \"yyMMdd\");\n\t\t\t// 获取出生年(完全表现形式,如：2010)\n\t\t\tint sYear = DateUtil.year(birthDate);\n\t\t\tif (sYear > 2000) {\n\t\t\t\t// 2000年之后不存在15位身份证号，此处用于修复此问题的判断\n\t\t\t\tsYear -= 100;\n\t\t\t}\n\t\t\tidCard18 = StrUtil.builder().append(idCard, 0, 6).append(sYear).append(idCard.substring(8));\n\t\t\t// 获取校验位\n\t\t\tchar sVal = getCheckCode18(idCard18.toString());\n\t\t\tidCard18.append(sVal);\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t\treturn idCard18.toString();\n\t}\n\n\t/**\n\t * 将18位身份证号码转换为15位\n\t *\n\t * @param  idCard 18位身份编码\n\t * @return 15位身份编码\n\t */\n\tpublic static String convert18To15(String idCard) {\n\t\tif (StrUtil.isNotBlank(idCard) && IdcardUtil.isValidCard18(idCard)) {\n\t\t\treturn idCard.substring(0, 6) + idCard.substring(8, idCard.length() - 1);\n\t\t}\n\t\treturn idCard;\n\t}\n\n\t/**\n\t * 是否有效身份证号，忽略X的大小写<br>\n\t * 如果身份证号码中含有空格始终返回{@code false}\n\t *\n\t * @param idCard 身份证号，支持18位、15位和港澳台的10位\n\t * @return 是否有效\n\t */\n\tpublic static boolean isValidCard(String idCard) {\n\t\tif (StrUtil.isBlank(idCard)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t//idCard = idCard.trim();\n\t\tint length = idCard.length();\n\t\tswitch (length) {\n\t\t\tcase 18:// 18位身份证\n\t\t\t\treturn isValidCard18(idCard);\n\t\t\tcase 15:// 15位身份证\n\t\t\t\treturn isValidCard15(idCard);\n\t\t\tcase 10: {// 10位身份证，港澳台地区\n\t\t\t\tString[] cardVal = isValidCard10(idCard);\n\t\t\t\treturn null != cardVal && \"true\".equals(cardVal[2]);\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * <p>\n\t * 判断18位身份证的合法性\n\t * </p>\n\t * 根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定，公民身份号码是特征组合码，由十七位数字本体码和一位数字校验码组成。<br>\n\t * 排列顺序从左至右依次为：六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。\n\t * <p>\n\t * 顺序码: 表示在同一地址码所标识的区域范围内，对同年、同月、同 日出生的人编定的顺序号，顺序码的奇数分配给男性，偶数分配 给女性。\n\t * </p>\n\t * <ol>\n\t * <li>第1、2位数字表示：所在省份的代码</li>\n\t * <li>第3、4位数字表示：所在城市的代码</li>\n\t * <li>第5、6位数字表示：所在区县的代码</li>\n\t * <li>第7~14位数字表示：出生年、月、日</li>\n\t * <li>第15、16位数字表示：所在地的派出所的代码</li>\n\t * <li>第17位数字表示性别：奇数表示男性，偶数表示女性</li>\n\t * <li>第18位数字是校检码，用来检验身份证的正确性。校检码可以是0~9的数字，有时也用x表示</li>\n\t * </ol>\n\t * <p>\n\t * 第十八位数字(校验码)的计算方法为：\n\t * <ol>\n\t * <li>将前面的身份证号码17位数分别乘以不同的系数。从第一位到第十七位的系数分别为：7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2</li>\n\t * <li>将这17位数字和系数相乘的结果相加</li>\n\t * <li>用加出来和除以11，看余数是多少</li>\n\t * <li>余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2</li>\n\t * <li>通过上面得知如果余数是2，就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10，身份证的最后一位号码就是2</li>\n\t * </ol>\n\t * <ol>\n\t * \t     <li>香港人在大陆的身份证，【810000】开头；同样可以直接获取到 性别、出生日期</li>\n\t * \t     <li>81000019980902013X: 文绎循 男 1998-09-02</li>\n\t * \t     <li>810000201011210153: 辛烨 男 2010-11-21</li>\n\t * \t </ol>\n\t * \t <ol>\n\t *       <li>澳门人在大陆的身份证，【820000】开头；同样可以直接获取到 性别、出生日期</li>\n\t *       <li>820000200009100032: 黄敬杰 男 2000-09-10</li>\n\t *  </ol>\n\t *  <ol>\n\t *     <li>台湾人在大陆的身份证，【830000】开头；同样可以直接获取到 性别、出生日期</li>\n\t *     <li>830000200209060065: 王宜妃 女 2002-09-06</li>\n\t *     <li>830000194609150010: 苏建文 男 1946-09-14</li>\n\t *     <li>83000019810715006X: 刁婉琇 女 1981-07-15</li>\n\t * </ol>\n\t *\n\t * @param idcard 待验证的身份证\n\t * @return 是否有效的18位身份证，忽略x的大小写\n\t */\n\tpublic static boolean isValidCard18(String idcard) {\n\t\treturn isValidCard18(idcard, true);\n\t}\n\n\t/**\n\t * <p>\n\t * 判断18位身份证的合法性\n\t * </p>\n\t * 根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定，公民身份号码是特征组合码，由十七位数字本体码和一位数字校验码组成。<br>\n\t * 排列顺序从左至右依次为：六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。\n\t * <p>\n\t * 顺序码: 表示在同一地址码所标识的区域范围内，对同年、同月、同 日出生的人编定的顺序号，顺序码的奇数分配给男性，偶数分配 给女性。\n\t * </p>\n\t * <ol>\n\t * <li>第1、2位数字表示：所在省份的代码</li>\n\t * <li>第3、4位数字表示：所在城市的代码</li>\n\t * <li>第5、6位数字表示：所在区县的代码</li>\n\t * <li>第7~14位数字表示：出生年、月、日</li>\n\t * <li>第15、16位数字表示：所在地的派出所的代码</li>\n\t * <li>第17位数字表示性别：奇数表示男性，偶数表示女性</li>\n\t * <li>第18位数字是校检码，用来检验身份证的正确性。校检码可以是0~9的数字，有时也用x表示</li>\n\t * </ol>\n\t * <p>\n\t * 第十八位数字(校验码)的计算方法为：\n\t * <ol>\n\t * <li>将前面的身份证号码17位数分别乘以不同的系数。从第一位到第十七位的系数分别为：7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2</li>\n\t * <li>将这17位数字和系数相乘的结果相加</li>\n\t * <li>用加出来和除以11，看余数是多少</li>\n\t * <li>余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2</li>\n\t * <li>通过上面得知如果余数是2，就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10，身份证的最后一位号码就是2</li>\n\t * </ol>\n\t *\n\t * @param idcard     待验证的身份证\n\t * @param ignoreCase 是否忽略大小写。{@code true}则忽略X大小写，否则严格匹配大写。\n\t * @return 是否有效的18位身份证\n\t * @since 5.5.7\n\t */\n\tpublic static boolean isValidCard18(String idcard, boolean ignoreCase) {\n\t\tif (idcard == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (CHINA_ID_MAX_LENGTH != idcard.length()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 截取省份代码。新版外国人永久居留身份证以9开头，第二三位是受理地代码\n\t\tfinal String proCode = idcard.startsWith(\"9\") ? idcard.substring(1, 3): idcard.substring(0, 2);\n\t\tif (null == CITY_CODES.get(proCode)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t//校验生日\n\t\tif (false == Validator.isBirthday(idcard.substring(6, 14))) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 前17位\n\t\tfinal String code17 = idcard.substring(0, 17);\n\t\tif (ReUtil.isMatch(PatternPool.NUMBERS, code17)) {\n\t\t\t// 获取校验位\n\t\t\tchar val = getCheckCode18(code17);\n\t\t\t// 第18位\n\t\t\treturn CharUtil.equals(val, idcard.charAt(17), ignoreCase);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 验证15位身份编码是否合法\n\t *\n\t * @param idcard 身份编码\n\t * @return 是否合法\n\t */\n\tpublic static boolean isValidCard15(String idcard) {\n\t\tif (idcard == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (CHINA_ID_MIN_LENGTH != idcard.length()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (ReUtil.isMatch(PatternPool.NUMBERS, idcard)) {\n\t\t\t// 省份\n\t\t\tString proCode = idcard.substring(0, 2);\n\t\t\tif (null == CITY_CODES.get(proCode)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t//校验生日（两位年份，补充为19XX）\n\t\t\treturn false != Validator.isBirthday(\"19\" + idcard.substring(6, 12));\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 验证10位身份编码是否合法\n\t *\n\t * @param idcard 身份编码\n\t * @return 身份证信息数组\n\t * <p>\n\t * [0] - 台湾、澳门、香港 [1] - 性别(男M,女F,未知N) [2] - 是否合法(合法true,不合法false) 若不是身份证件号码则返回null\n\t * </p>\n\t */\n\tpublic static String[] isValidCard10(String idcard) {\n\t\tif (StrUtil.isBlank(idcard)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// issue#IBP6T1 中文空格替换为英文\n\t\tidcard = StrUtil.replace(idcard, \"（\", \"(\");\n\t\tidcard = StrUtil.replace(idcard, \"）\", \")\");\n\n\t\tString[] info = new String[3];\n\t\tString card = idcard.replaceAll(\"[()]\", \"\");\n\t\tif (card.length() != 8 && card.length() != 9 && idcard.length() != 10) {\n\t\t\treturn null;\n\t\t}\n\t\tif (idcard.matches(\"^[a-zA-Z][0-9]{9}$\")) { // 台湾\n\t\t\tinfo[0] = \"台湾\";\n\t\t\tchar char2 = idcard.charAt(1);\n\t\t\tif ('1' == char2) {\n\t\t\t\tinfo[1] = \"M\";\n\t\t\t} else if ('2' == char2) {\n\t\t\t\tinfo[1] = \"F\";\n\t\t\t} else {\n\t\t\t\tinfo[1] = \"N\";\n\t\t\t\tinfo[2] = \"false\";\n\t\t\t\treturn info;\n\t\t\t}\n\t\t\tinfo[2] = isValidTWCard(idcard) ? \"true\" : \"false\";\n\t\t} else if (idcard.matches(\"^[157][0-9]{6}\\\\(?[0-9A-Z]\\\\)?$\")) { // 澳门\n\t\t\tinfo[0] = \"澳门\";\n\t\t\tinfo[1] = \"N\";\n\t\t\tinfo[2] = \"true\";\n\t\t} else if (idcard.matches(\"^[A-Z]{1,2}[0-9]{6}\\\\(?[0-9A]\\\\)?$\")) { // 香港\n\t\t\tinfo[0] = \"香港\";\n\t\t\tinfo[1] = \"N\";\n\t\t\tinfo[2] = isValidHKCard(idcard) ? \"true\" : \"false\";\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t\treturn info;\n\t}\n\n\t/**\n\t * 验证台湾身份证号码\n\t *\n\t * @param idcard 身份证号码\n\t * @return 验证码是否符合\n\t */\n\tpublic static boolean isValidTWCard(String idcard) {\n\t\tif (null == idcard || idcard.length() != 10) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal Integer iStart = TW_FIRST_CODE.get(idcard.charAt(0));\n\t\tif (null == iStart) {\n\t\t\treturn false;\n\t\t}\n\t\tint sum = iStart / 10 + (iStart % 10) * 9;\n\n\t\tfinal String mid = idcard.substring(1, 9);\n\t\tfinal char[] chars = mid.toCharArray();\n\t\tint iflag = 8;\n\t\tfor (char c : chars) {\n\t\t\tsum += Integer.parseInt(String.valueOf(c)) * iflag;\n\t\t\tiflag--;\n\t\t}\n\n\t\tfinal String end = idcard.substring(9, 10);\n\t\treturn (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.parseInt(end);\n\t}\n\n\t/**\n\t * 验证香港身份证号码(存在Bug，部份特殊身份证无法检查)\n\t * <p>\n\t * 身份证前2位为英文字符，如果只出现一个英文字符则表示第一位是空格，对应数字58 前2位英文字符A-Z分别对应数字10-35 最后一位校验码为0-9的数字加上字符\"A\"，\"A\"代表10\n\t * </p>\n\t * <p>\n\t * 将身份证号码全部转换为数字，分别对应乘9-1相加的总和，整除11则证件号码有效\n\t * </p>\n\t *\n\t * @param idcard 身份证号码\n\t * @return 验证码是否符合\n\t */\n\tpublic static boolean isValidHKCard(String idcard) {\n\t\tif(false == idcard.matches(\"^[A-Z]{1,2}[0-9]{6}\\\\(?[0-9A]\\\\)?$\")){\n\t\t\treturn false;\n\t\t}\n\n\t\tString card = idcard.replaceAll(\"[()]\", \"\");\n\t\tint sum;\n\t\tif (card.length() == 9) {\n\t\t\tsum = (Character.toUpperCase(card.charAt(0)) - 55) * 9 + (Character.toUpperCase(card.charAt(1)) - 55) * 8;\n\t\t\tcard = card.substring(1, 9);\n\t\t} else {\n\t\t\tsum = 522 + (Character.toUpperCase(card.charAt(0)) - 55) * 8;\n\t\t}\n\n\t\t// 首字母A-Z，A表示1，以此类推\n\t\tString mid = card.substring(1, 7);\n\t\tString end = card.substring(7, 8);\n\t\tchar[] chars = mid.toCharArray();\n\t\tint iflag = 7;\n\t\tfor (char c : chars) {\n\t\t\tsum = sum + Integer.parseInt(String.valueOf(c)) * iflag;\n\t\t\tiflag--;\n\t\t}\n\t\tif (\"A\".equalsIgnoreCase(end)) {\n\t\t\tsum += 10;\n\t\t} else {\n\t\t\tsum += Integer.parseInt(end);\n\t\t}\n\t\treturn sum % 11 == 0;\n\t}\n\n\t/**\n\t * 根据身份编号获取生日，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 生日(yyyyMMdd)\n\t * @see #getBirth(String)\n\t */\n\tpublic static String getBirthByIdCard(String idcard) {\n\t\treturn getBirth(idcard);\n\t}\n\n\t/**\n\t * 根据身份编号获取生日，只支持15或18位身份证号码\n\t *\n\t * @param idCard 身份编号\n\t * @return 生日(yyyyMMdd)\n\t */\n\tpublic static String getBirth(String idCard) {\n\t\tAssert.notBlank(idCard, \"id card must be not blank!\");\n\t\tfinal int len = idCard.length();\n\t\tif (len < CHINA_ID_MIN_LENGTH) {\n\t\t\treturn null;\n\t\t} else if (len == CHINA_ID_MIN_LENGTH) {\n\t\t\tidCard = convert15To18(idCard);\n\t\t}\n\n\t\treturn Objects.requireNonNull(idCard).substring(6, 14);\n\t}\n\n\t/**\n\t * 从身份证号码中获取生日日期，只支持15或18位身份证号码\n\t *\n\t * @param idCard 身份证号码\n\t * @return 日期\n\t */\n\tpublic static DateTime getBirthDate(String idCard) {\n\t\tfinal String birthByIdCard = getBirthByIdCard(idCard);\n\t\treturn null == birthByIdCard ? null : DateUtil.parse(birthByIdCard, DatePattern.PURE_DATE_FORMAT);\n\t}\n\n\t/**\n\t * 根据身份编号获取年龄，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 年龄\n\t */\n\tpublic static int getAgeByIdCard(String idcard) {\n\t\treturn getAgeByIdCard(idcard, DateUtil.date());\n\t}\n\n\t/**\n\t * 根据身份编号获取指定日期当时的年龄年龄，只支持15或18位身份证号码\n\t *\n\t * @param idcard        身份编号\n\t * @param dateToCompare 以此日期为界，计算年龄。\n\t * @return 年龄\n\t */\n\tpublic static int getAgeByIdCard(String idcard, Date dateToCompare) {\n\t\tString birth = getBirthByIdCard(idcard);\n\t\treturn DateUtil.age(DateUtil.parse(birth, \"yyyyMMdd\"), dateToCompare);\n\t}\n\n\t/**\n\t * 根据身份编号获取生日年，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 生日(yyyy)\n\t */\n\tpublic static Short getYearByIdCard(String idcard) {\n\t\tfinal int len = idcard.length();\n\t\tif (len < CHINA_ID_MIN_LENGTH) {\n\t\t\treturn null;\n\t\t} else if (len == CHINA_ID_MIN_LENGTH) {\n\t\t\tidcard = convert15To18(idcard);\n\t\t}\n\t\treturn Short.valueOf(Objects.requireNonNull(idcard).substring(6, 10));\n\t}\n\n\t/**\n\t * 根据身份编号获取生日月，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 生日(MM)\n\t */\n\tpublic static Short getMonthByIdCard(String idcard) {\n\t\tfinal int len = idcard.length();\n\t\tif (len < CHINA_ID_MIN_LENGTH) {\n\t\t\treturn null;\n\t\t} else if (len == CHINA_ID_MIN_LENGTH) {\n\t\t\tidcard = convert15To18(idcard);\n\t\t}\n\t\treturn Short.valueOf(Objects.requireNonNull(idcard).substring(10, 12));\n\t}\n\n\t/**\n\t * 根据身份编号获取生日天，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 生日(dd)\n\t */\n\tpublic static Short getDayByIdCard(String idcard) {\n\t\tfinal int len = idcard.length();\n\t\tif (len < CHINA_ID_MIN_LENGTH) {\n\t\t\treturn null;\n\t\t} else if (len == CHINA_ID_MIN_LENGTH) {\n\t\t\tidcard = convert15To18(idcard);\n\t\t}\n\t\treturn Short.valueOf(Objects.requireNonNull(idcard).substring(12, 14));\n\t}\n\n\t/**\n\t * 根据身份编号获取性别，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编号\n\t * @return 性别(1 : 男 ， 0 : 女)\n\t */\n\tpublic static int getGenderByIdCard(String idcard) {\n\t\tAssert.notBlank(idcard);\n\t\tfinal int len = idcard.length();\n\t\tif (!(len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH)) {\n\t\t\tthrow new IllegalArgumentException(\"ID Card length must be 15 or 18\");\n\t\t}\n\n\t\tif (len == CHINA_ID_MIN_LENGTH) {\n\t\t\tidcard = convert15To18(idcard);\n\t\t}\n\t\tchar sCardChar = Objects.requireNonNull(idcard).charAt(16);\n\t\treturn (sCardChar % 2 != 0) ? 1 : 0;\n\t}\n\n\t/**\n\t * 根据身份编号获取户籍省份编码，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编码\n\t * @return 省份编码\n\t * @since 5.7.2\n\t */\n\tpublic static String getProvinceCodeByIdCard(String idcard) {\n\t\tint len = idcard.length();\n\t\tif (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) {\n\t\t\treturn idcard.substring(0, 2);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据身份编号获取户籍省份，只支持15或18位身份证号码\n\t *\n\t * @param idcard 身份编码\n\t * @return 省份名称。\n\t */\n\tpublic static String getProvinceByIdCard(String idcard) {\n\t\tfinal String code = getProvinceCodeByIdCard(idcard);\n\t\tif (StrUtil.isNotBlank(code)) {\n\t\t\treturn CITY_CODES.get(code);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据身份编号获取地市级编码，只支持15或18位身份证号码<br>\n\t * 获取编码为4位\n\t *\n\t * @param idcard 身份编码\n\t * @return 地市级编码\n\t */\n\tpublic static String getCityCodeByIdCard(String idcard) {\n\t\tint len = idcard.length();\n\t\tif (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) {\n\t\t\treturn idcard.substring(0, 4);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据身份编号获取区县级编码，只支持15或18位身份证号码<br>\n\t * 获取编码为6位\n\t *\n\t * @param idcard 身份编码\n\t * @return 地市级编码\n\t * @since 5.8.0\n\t */\n\tpublic static String getDistrictCodeByIdCard(String idcard) {\n\t\tint len = idcard.length();\n\t\tif (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) {\n\t\t\treturn idcard.substring(0, 6);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 隐藏指定位置的几个身份证号数字为“*”\n\t *\n\t * @param idcard       身份证号\n\t * @param startInclude 开始位置（包含）\n\t * @param endExclude   结束位置（不包含）\n\t * @return 隐藏后的身份证号码\n\t * @see StrUtil#hide(CharSequence, int, int)\n\t * @since 3.2.2\n\t */\n\tpublic static String hide(String idcard, int startInclude, int endExclude) {\n\t\treturn StrUtil.hide(idcard, startInclude, endExclude);\n\t}\n\n\t/**\n\t * 获取身份证信息，包括身份、城市代码、生日、性别等\n\t *\n\t * @param idcard 15或18位身份证\n\t * @return {@link Idcard}\n\t * @since 5.4.3\n\t */\n\tpublic static Idcard getIdcardInfo(String idcard) {\n\t\treturn new Idcard(idcard);\n\t}\n\n\t// ----------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 获得18位身份证校验码\n\t *\n\t * @param code17 18位身份证号中的前17位\n\t * @return 第18位\n\t */\n\tprivate static char getCheckCode18(String code17) {\n\t\tint sum = getPowerSum(code17.toCharArray());\n\t\treturn getCheckCode18(sum);\n\t}\n\n\t/**\n\t * 将power和值与11取模获得余数进行校验码判断\n\t *\n\t * @param iSum 加权和\n\t * @return 校验位\n\t */\n\tprivate static char getCheckCode18(int iSum) {\n\t\tswitch (iSum % 11) {\n\t\t\tcase 10:\n\t\t\t\treturn '2';\n\t\t\tcase 9:\n\t\t\t\treturn '3';\n\t\t\tcase 8:\n\t\t\t\treturn '4';\n\t\t\tcase 7:\n\t\t\t\treturn '5';\n\t\t\tcase 6:\n\t\t\t\treturn '6';\n\t\t\tcase 5:\n\t\t\t\treturn '7';\n\t\t\tcase 4:\n\t\t\t\treturn '8';\n\t\t\tcase 3:\n\t\t\t\treturn '9';\n\t\t\tcase 2:\n\t\t\t\treturn 'X';\n\t\t\tcase 1:\n\t\t\t\treturn '0';\n\t\t\tcase 0:\n\t\t\t\treturn '1';\n\t\t\tdefault:\n\t\t\t\treturn StrUtil.C_SPACE;\n\t\t}\n\t}\n\n\t/**\n\t * 将身份证的每位和对应位的加权因子相乘之后，再得到和值\n\t *\n\t * @param iArr 身份证号码的数组\n\t * @return 身份证编码\n\t */\n\tprivate static int getPowerSum(char[] iArr) {\n\t\tint iSum = 0;\n\t\tif (POWER.length == iArr.length) {\n\t\t\tfor (int i = 0; i < iArr.length; i++) {\n\t\t\t\tiSum += Integer.parseInt(String.valueOf(iArr[i])) * POWER[i];\n\t\t\t}\n\t\t}\n\t\treturn iSum;\n\t}\n\t// ----------------------------------------------------------------------------------- Private method end\n\n\t/**\n\t * 身份证信息，包括身份、城市代码、生日、性别等\n\t *\n\t * @author looly\n\t * @since 5.4.3\n\t */\n\tpublic static class Idcard implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate final String provinceCode;\n\t\tprivate final String cityCode;\n\t\tprivate final DateTime birthDate;\n\t\tprivate final Integer gender;\n\t\tprivate final int age;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param idcard 身份证号码\n\t\t */\n\t\tpublic Idcard(String idcard) {\n\t\t\tthis.provinceCode = IdcardUtil.getProvinceCodeByIdCard(idcard);\n\t\t\tthis.cityCode = IdcardUtil.getCityCodeByIdCard(idcard);\n\t\t\tthis.birthDate = IdcardUtil.getBirthDate(idcard);\n\t\t\tthis.gender = IdcardUtil.getGenderByIdCard(idcard);\n\t\t\tthis.age = IdcardUtil.getAgeByIdCard(idcard);\n\t\t}\n\n\t\t/**\n\t\t * 获取省份代码\n\t\t *\n\t\t * @return 省份代码\n\t\t */\n\t\tpublic String getProvinceCode() {\n\t\t\treturn this.provinceCode;\n\t\t}\n\n\t\t/**\n\t\t * 获取省份名称\n\t\t *\n\t\t * @return 省份代码\n\t\t */\n\t\tpublic String getProvince() {\n\t\t\treturn CITY_CODES.get(this.provinceCode);\n\t\t}\n\n\t\t/**\n\t\t * 获取市级编码\n\t\t *\n\t\t * @return 市级编码\n\t\t */\n\t\tpublic String getCityCode() {\n\t\t\treturn this.cityCode;\n\t\t}\n\n\t\t/**\n\t\t * 获得生日日期\n\t\t *\n\t\t * @return 生日日期\n\t\t */\n\t\tpublic DateTime getBirthDate() {\n\t\t\treturn this.birthDate;\n\t\t}\n\n\t\t/**\n\t\t * 获取性别代号，性别(1 : 男 ， 0 : 女)\n\t\t *\n\t\t * @return 性别(1 : 男 ， 0 : 女)\n\t\t */\n\t\tpublic Integer getGender() {\n\t\t\treturn this.gender;\n\t\t}\n\n\t\t/**\n\t\t * 获取年龄\n\t\t *\n\t\t * @return 年龄\n\t\t */\n\t\tpublic int getAge() {\n\t\t\treturn age;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"Idcard{\" +\n\t\t\t\t\t\"provinceCode='\" + provinceCode + '\\'' +\n\t\t\t\t\t\", cityCode='\" + cityCode + '\\'' +\n\t\t\t\t\t\", birthDate=\" + birthDate +\n\t\t\t\t\t\", gender=\" + gender +\n\t\t\t\t\t\", age=\" + age +\n\t\t\t\t\t'}';\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/JAXBUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\n\nimport javax.xml.bind.JAXBContext;\nimport javax.xml.bind.Marshaller;\nimport javax.xml.bind.Unmarshaller;\nimport javax.xml.bind.annotation.XmlElement;\nimport javax.xml.bind.annotation.XmlElementWrapper;\nimport javax.xml.bind.annotation.XmlRootElement;\nimport javax.xml.bind.annotation.XmlTransient;\nimport java.io.File;\nimport java.io.Reader;\nimport java.io.StringWriter;\nimport java.nio.charset.Charset;\n\n/**\n * JAXB（Java Architecture for XML Binding），根据XML Schema产生Java对象，即实现xml和Bean互转。\n * <p>\n * 相关介绍：\n * <ul>\n *   <li>https://www.cnblogs.com/yanghaolie/p/11110991.html</li>\n *   <li>https://my.oschina.net/u/4266515/blog/3330113</li>\n * </ul>\n *\n * @author dazer\n * @see XmlUtil\n * @since 5.7.3\n */\npublic class JAXBUtil {\n\n\t/**\n\t * JavaBean转换成xml\n\t * <p>\n\t * bean上面用的常用注解\n\t *\n\t * @param bean Bean对象\n\t * @return 输出的XML字符串\n\t * @see XmlRootElement {@code @XmlRootElement(name = \"school\")}\n\t * @see XmlElement {@code @XmlElement(name = \"school_name\", required = true)}\n\t * @see XmlElementWrapper {@code @XmlElementWrapper(name=\"schools\")}\n\t * @see XmlTransient JAXB \"有两个名为 \"**\" 的属性,类的两个属性具有相同名称 \"**\"\"解决方案\n\t */\n\tpublic static String beanToXml(Object bean) {\n\t\treturn beanToXml(bean, CharsetUtil.CHARSET_UTF_8, true);\n\t}\n\n\t/**\n\t * JavaBean转换成xml\n\t *\n\t * @param bean    Bean对象\n\t * @param charset 编码 eg: utf-8\n\t * @param format  是否格式化输出eg: true\n\t * @return 输出的XML字符串\n\t */\n\tpublic static String beanToXml(Object bean, Charset charset, boolean format) {\n\t\tStringWriter writer;\n\t\ttry {\n\t\t\tJAXBContext context = JAXBContext.newInstance(bean.getClass());\n\t\t\tMarshaller marshaller = context.createMarshaller();\n\t\t\tmarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, format);\n\t\t\tmarshaller.setProperty(Marshaller.JAXB_ENCODING, charset.name());\n\t\t\twriter = new StringWriter();\n\t\t\tmarshaller.marshal(bean, writer);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(\"convertToXml 错误：\" + e.getMessage(), e);\n\t\t}\n\t\treturn writer.toString();\n\t}\n\n\t/**\n\t * xml转换成JavaBean\n\t *\n\t * @param <T> Bean类型\n\t * @param xml XML字符串\n\t * @param c   Bean类型\n\t * @return bean\n\t */\n\tpublic static <T> T xmlToBean(String xml, Class<T> c) {\n\t\treturn xmlToBean(StrUtil.getReader(xml), c);\n\t}\n\n\t/**\n\t * XML文件转Bean\n\t *\n\t * @param file    文件\n\t * @param charset 编码\n\t * @param c       Bean类\n\t * @param <T>     Bean类型\n\t * @return Bean\n\t */\n\tpublic static <T> T xmlToBean(File file, Charset charset, Class<T> c) {\n\t\treturn xmlToBean(FileUtil.getReader(file, charset), c);\n\t}\n\n\t/**\n\t * 从{@link Reader}中读取XML字符串，并转换为Bean\n\t *\n\t * @param reader {@link Reader}\n\t * @param c      Bean类\n\t * @param <T>    Bean类型\n\t * @return Bean\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T xmlToBean(Reader reader, Class<T> c) {\n\t\ttry {\n\t\t\tJAXBContext context = JAXBContext.newInstance(c);\n\t\t\tUnmarshaller unmarshaller = context.createUnmarshaller();\n\t\t\treturn (T) unmarshaller.unmarshal(reader);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(\"convertToJava2 错误：\" + e.getMessage(), e);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/JNDIUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.map.MapUtil;\n\nimport javax.naming.InitialContext;\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attributes;\nimport javax.naming.directory.InitialDirContext;\nimport java.util.Hashtable;\nimport java.util.Map;\n\n/**\n * JNDI工具类<br>\n * JNDI是Java Naming and Directory Interface（JAVA命名和目录接口）的英文简写，<br>\n * 它是为JAVA应用程序提供命名和目录访问服务的API（Application Programing Interface，应用程序编程接口）。\n *\n * <p>\n * 见：https://blog.csdn.net/u010430304/article/details/54601302\n * </p>\n *\n * @author loolY\n * @since 5.7.7\n */\npublic class JNDIUtil {\n\n\t/**\n\t * 创建{@link InitialDirContext}\n\t *\n\t * @param environment 环境参数，{@code null}表示无参数\n\t * @return {@link InitialDirContext}\n\t */\n\tpublic static InitialDirContext createInitialDirContext(Map<String, String> environment) {\n\t\ttry {\n\t\t\tif (MapUtil.isEmpty(environment)) {\n\t\t\t\treturn new InitialDirContext();\n\t\t\t}\n\t\t\treturn new InitialDirContext(Convert.convert(Hashtable.class, environment));\n\t\t} catch (NamingException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建{@link InitialContext}\n\t *\n\t * @param environment 环境参数，{@code null}表示无参数\n\t * @return {@link InitialContext}\n\t */\n\tpublic static InitialContext createInitialContext(Map<String, String> environment) {\n\t\ttry {\n\t\t\tif (MapUtil.isEmpty(environment)) {\n\t\t\t\treturn new InitialContext();\n\t\t\t}\n\t\t\treturn new InitialContext(Convert.convert(Hashtable.class, environment));\n\t\t} catch (NamingException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定容器环境的对象的属性<br>\n\t * 如获取DNS属性，则URI为类似：dns:hutool.cn\n\t *\n\t * @param uri     URI字符串，格式为[scheme:][name]/[domain]\n\t * @param attrIds 需要获取的属性ID名称\n\t * @return {@link Attributes}\n\t */\n\tpublic static Attributes getAttributes(String uri, String... attrIds) {\n\t\ttry {\n\t\t\treturn createInitialDirContext(null).getAttributes(uri, attrIds);\n\t\t} catch (NamingException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/JdkUtil.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.util;\n\n/**\n * JDK相关工具类，包括判断JDK版本等<br>\n * 工具部分方法来自fastjson2的JDKUtils\n *\n * @author fastjson, looly\n */\npublic class JdkUtil {\n\t/**\n\t * JDK版本\n\t */\n\tpublic static final int JVM_VERSION;\n\t/**\n\t * 是否JDK8<br>\n\t * 由于Hutool基于JDK8编译，当使用JDK版本低于8时，不支持。\n\t */\n\tpublic static final boolean IS_JDK8;\n\t/**\n\t * 是否大于等于JDK17\n\t */\n\tpublic static final boolean IS_AT_LEAST_JDK17;\n\n\t/**\n\t * 是否大于等于JDK25\n\t */\n\tpublic static final boolean IS_AT_LEAST_JDK25;\n\n\t/**\n\t * 是否Android环境\n\t */\n\tpublic static final boolean IS_ANDROID;\n\n\tstatic {\n\t\t// JVM版本\n\t\tJVM_VERSION = _getJvmVersion();\n\t\tIS_JDK8 = 8 == JVM_VERSION;\n\t\tIS_AT_LEAST_JDK17 = JVM_VERSION >= 17;\n\t\tIS_AT_LEAST_JDK25 = JVM_VERSION >= 25;\n\n\t\t// JVM名称\n\t\tfinal String jvmName = _getJvmName();\n\t\tIS_ANDROID = jvmName.equals(\"Dalvik\");\n\t}\n\n\t/**\n\t * 获取JVM名称\n\t *\n\t * @return JVM名称\n\t */\n\tprivate static String _getJvmName() {\n\t\treturn System.getProperty(\"java.vm.name\");\n\t}\n\n\t/**\n\t * 根据{@code java.specification.version}属性值，获取版本号\n\t *\n\t * @return 版本号\n\t */\n\tprivate static int _getJvmVersion() {\n\t\tint jvmVersion = -1;\n\n\t\ttry{\n\t\t\tString javaSpecVer = System.getProperty(\"java.specification.version\");\n\t\t\tif (StrUtil.isNotBlank(javaSpecVer)) {\n\t\t\t\tif (javaSpecVer.startsWith(\"1.\")) {\n\t\t\t\t\tjavaSpecVer = javaSpecVer.substring(2);\n\t\t\t\t}\n\t\t\t\tif (javaSpecVer.indexOf('.') == -1) {\n\t\t\t\t\tjvmVersion = Integer.parseInt(javaSpecVer);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Throwable ignore){\n\t\t\t// 默认JDK8\n\t\t\tjvmVersion = 8;\n\t\t}\n\n\t\treturn jvmVersion;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\n\nimport java.lang.reflect.*;\n\n/**\n * 修饰符工具类\n *\n * @author looly\n * @since 4.0.5\n */\npublic class ModifierUtil {\n\n\t/**\n\t * 修饰符枚举\n\t *\n\t * @author looly\n\t * @since 4.0.5\n\t */\n\tpublic enum ModifierType {\n\t\t/**\n\t\t * public修饰符，所有类都能访问\n\t\t */\n\t\tPUBLIC(Modifier.PUBLIC),\n\t\t/**\n\t\t * private修饰符，只能被自己访问和修改\n\t\t */\n\t\tPRIVATE(Modifier.PRIVATE),\n\t\t/**\n\t\t * protected修饰符，自身、子类及同一个包中类可以访问\n\t\t */\n\t\tPROTECTED(Modifier.PROTECTED),\n\t\t/**\n\t\t * static修饰符，（静态修饰符）指定变量被所有对象共享，即所有实例都可以使用该变量。变量属于这个类\n\t\t */\n\t\tSTATIC(Modifier.STATIC),\n\t\t/**\n\t\t * final修饰符，最终修饰符，指定此变量的值不能变，使用在方法上表示不能被重载\n\t\t */\n\t\tFINAL(Modifier.FINAL),\n\t\t/**\n\t\t * synchronized，同步修饰符，在多个线程中，该修饰符用于在运行前，对他所属的方法加锁，以防止其他线程的访问，运行结束后解锁。\n\t\t */\n\t\tSYNCHRONIZED(Modifier.SYNCHRONIZED),\n\t\t/**\n\t\t * （易失修饰符）指定该变量可以同时被几个线程控制和修改\n\t\t */\n\t\tVOLATILE(Modifier.VOLATILE),\n\t\t/**\n\t\t * （过度修饰符）指定该变量是系统保留，暂无特别作用的临时性变量，序列化时忽略\n\t\t */\n\t\tTRANSIENT(Modifier.TRANSIENT),\n\t\t/**\n\t\t * native，本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。\n\t\t */\n\t\tNATIVE(Modifier.NATIVE),\n\n\t\t/**\n\t\t * abstract，将一个类声明为抽象类，没有实现的方法，需要子类提供方法实现。\n\t\t */\n\t\tABSTRACT(Modifier.ABSTRACT),\n\t\t/**\n\t\t * strictfp，一旦使用了关键字strictfp来声明某个类、接口或者方法时，那么在这个关键字所声明的范围内所有浮点运算都是精确的，符合IEEE-754规范的。\n\t\t */\n\t\tSTRICT(Modifier.STRICT);\n\n\t\t/**\n\t\t * 修饰符枚举对应的int修饰符值\n\t\t */\n\t\tprivate final int value;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param modifier 修饰符int表示，见{@link Modifier}\n\t\t */\n\t\tModifierType(int modifier) {\n\t\t\tthis.value = modifier;\n\t\t}\n\n\t\t/**\n\t\t * 获取修饰符枚举对应的int修饰符值，值见{@link Modifier}\n\t\t *\n\t\t * @return 修饰符枚举对应的int修饰符值\n\t\t */\n\t\tpublic int getValue() {\n\t\t\treturn this.value;\n\t\t}\n\t}\n\n\t/**\n\t * 类是否存在给定修饰符中的<b>任意一个</b><br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code true}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param clazz         类，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasModifier(Class<?> clazz, ModifierType... modifierTypes) {\n\t\tif (null == clazz || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn 0 != (clazz.getModifiers() & modifiersToInt(modifierTypes));\n\t}\n\n\t/**\n\t * 构造是否存在给定修饰符中的<b>任意一个</b><br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code true}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param constructor        构造，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasModifier(Constructor<?> constructor, ModifierType... modifierTypes) {\n\t\tif (null == constructor || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn 0 != (constructor.getModifiers() & modifiersToInt(modifierTypes));\n\t}\n\n\t/**\n\t * 方法是否存在给定修饰符中的<b>任意一个</b><br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code true}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param method        方法，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasModifier(Method method, ModifierType... modifierTypes) {\n\t\tif (null == method || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn 0 != (method.getModifiers() & modifiersToInt(modifierTypes));\n\t}\n\n\t/**\n\t * 字段是否存在给定修饰符中的<b>任意一个</b><br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code true}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param field        构造、字段或方法，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasModifier(Field field, ModifierType... modifierTypes) {\n\t\tif (null == field || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn 0 != (field.getModifiers() & modifiersToInt(modifierTypes));\n\t}\n\n\t// region ----- hasAll\n\n\t/**\n\t * 类中是否同时存在<b>所有</b>给定修饰符<br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code false}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param clazz         类，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否同时存在所有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasAllModifiers(final Class<?> clazz, final ModifierType... modifierTypes) {\n\t\tif (null == clazz || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal int checkedModifiersInt = modifiersToInt(modifierTypes);\n\t\treturn checkedModifiersInt == (clazz.getModifiers() & checkedModifiersInt);\n\t}\n\n\t/**\n\t * 成员中是否同时存在<b>所有</b>给定修饰符<br>\n\t * 如定义修饰符为：{@code public static final}，那么如果传入的modifierTypes为：\n\t * <ul>\n\t *     <li>public、static  返回{@code true}</li>\n\t *     <li>public、abstract返回{@code false}</li>\n\t *     <li>private、abstract返回{@code false}</li>\n\t * </ul>\n\t *\n\t * @param member        构造、字段或方法，如果为{@code null}返回{@code false}\n\t * @param modifierTypes 修饰符枚举，如果为空返回{@code false}\n\t * @return 是否同时存在所有指定修饰符，如果有返回true，否则false，如果提供参数为null返回false\n\t */\n\tpublic static boolean hasAllModifiers(final Member member, final ModifierType... modifierTypes) {\n\t\tif (null == member || ArrayUtil.isEmpty(modifierTypes)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal int checkedModifiersInt = modifiersToInt(modifierTypes);\n\t\treturn checkedModifiersInt == (member.getModifiers() & checkedModifiersInt);\n\t}\n\t// endregion\n\n\t/**\n\t * 是否是Public字段\n\t *\n\t * @param field 字段\n\t * @return 是否是Public\n\t */\n\tpublic static boolean isPublic(Field field) {\n\t\treturn hasModifier(field, ModifierType.PUBLIC);\n\t}\n\n\t/**\n\t * 是否是Public方法\n\t *\n\t * @param method 方法\n\t * @return 是否是Public\n\t */\n\tpublic static boolean isPublic(Method method) {\n\t\treturn hasModifier(method, ModifierType.PUBLIC);\n\t}\n\n\t/**\n\t * 是否是Public类\n\t *\n\t * @param clazz 类\n\t * @return 是否是Public\n\t */\n\tpublic static boolean isPublic(Class<?> clazz) {\n\t\treturn hasModifier(clazz, ModifierType.PUBLIC);\n\t}\n\n\t/**\n\t * 是否是Public构造\n\t *\n\t * @param constructor 构造\n\t * @return 是否是Public\n\t */\n\tpublic static boolean isPublic(Constructor<?> constructor) {\n\t\treturn hasModifier(constructor, ModifierType.PUBLIC);\n\t}\n\n\t/**\n\t * 是否是static字段\n\t *\n\t * @param field 字段\n\t * @return 是否是static\n\t * @since 4.0.8\n\t */\n\tpublic static boolean isStatic(Field field) {\n\t\treturn hasModifier(field, ModifierType.STATIC);\n\t}\n\n\t/**\n\t * 是否是static方法\n\t *\n\t * @param method 方法\n\t * @return 是否是static\n\t * @since 4.0.8\n\t */\n\tpublic static boolean isStatic(Method method) {\n\t\treturn hasModifier(method, ModifierType.STATIC);\n\t}\n\n\t/**\n\t * 是否是static类\n\t *\n\t * @param clazz 类\n\t * @return 是否是static\n\t * @since 4.0.8\n\t */\n\tpublic static boolean isStatic(Class<?> clazz) {\n\t\treturn hasModifier(clazz, ModifierType.STATIC);\n\t}\n\n\t/**\n\t * 是否是合成字段（由java编译器生成的）\n\t *\n\t * @param field 字段\n\t * @return 是否是合成字段\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isSynthetic(Field field) {\n\t\treturn field.isSynthetic();\n\t}\n\n\t/**\n\t * 是否是合成方法（由java编译器生成的）\n\t *\n\t * @param method 方法\n\t * @return 是否是合成方法\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isSynthetic(Method method) {\n\t\treturn method.isSynthetic();\n\t}\n\n\t/**\n\t * 是否是合成类（由java编译器生成的）\n\t *\n\t * @param clazz 类\n\t * @return 是否是合成\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isSynthetic(Class<?> clazz) {\n\t\treturn clazz.isSynthetic();\n\t}\n\n\t/**\n\t * 是否抽象方法\n\t *\n\t * @param method 方法\n\t * @return 是否抽象方法\n\t * @since 5.7.23\n\t */\n\tpublic static boolean isAbstract(Method method) {\n\t\treturn hasModifier(method, ModifierType.ABSTRACT);\n\t}\n\n\t/**\n\t * 设置final的field字段可以被修改\n\t * 只要不会被编译器内联优化的 final 属性就可以通过反射有效的进行修改 --  修改后代码中可使用到新的值;\n\t * <p>以下属性，编译器会内联优化，无法通过反射修改：</p>\n\t * <ul>\n\t *     <li> 基本类型 byte, char, short, int, long, float, double, boolean</li>\n\t *     <li> Literal String 类型(直接双引号字符串)</li>\n\t * </ul>\n\t * <p>以下属性，可以通过反射修改：</p>\n\t * <ul>\n\t *     <li>基本类型的包装类 Byte、Character、Short、Long、Float、Double、Boolean</li>\n\t *     <li>字符串，通过 new String(\"\")实例化</li>\n\t *     <li>自定义java类</li>\n\t * </ul>\n\t * <pre class=\"code\">\n\t * {@code\n\t *      //示例，移除final修饰符\n\t *      class JdbcDialects {private static final List<Number> dialects = new ArrayList<>();}\n\t *      Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);\n\t * \t\tReflectUtil.removeFinalModify(field);\n\t * \t\tReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);\n\t *    }\n\t * </pre>\n\t *\n\t * @param field 被修改的field，不可以为空\n\t * @throws UtilException IllegalAccessException等异常包装\n\t * @author dazer\n\t * @since 5.8.8\n\t */\n\tpublic static void removeFinalModify(Field field) {\n\t\tif (field != null) {\n\t\t\tif (hasModifier(field, ModifierUtil.ModifierType.FINAL)) {\n\t\t\t\t//将字段的访问权限设为true：即去除private修饰符的影响\n\t\t\t\tif (false == field.isAccessible()) {\n\t\t\t\t\tfield.setAccessible(true);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\t//去除final修饰符的影响，将字段设为可修改的\n\t\t\t\t\tfinal Field modifiersField = Field.class.getDeclaredField(\"modifiers\");\n\t\t\t\t\t//Field 的 modifiers 是私有的\n\t\t\t\t\tmodifiersField.setAccessible(true);\n\t\t\t\t\t//& ：位与运算符，按位与；  运算规则：两个数都转为二进制，然后从高位开始比较，如果两个数都为1则为1，否则为0。\n\t\t\t\t\t//~ ：位非运算符，按位取反；运算规则：转成二进制，如果位为0，结果是1，如果位为1，结果是0.\n\t\t\t\t\tmodifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);\n\t\t\t\t} catch (final NoSuchFieldException | IllegalAccessException e) {\n\t\t\t\t\t//内部，工具类，基本不抛出异常\n\t\t\t\t\tthrow new UtilException(e, \"IllegalAccess for {}.{}\", field.getDeclaringClass(), field.getName());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t//-------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 多个修饰符做“或”操作，表示同时存在多个修饰符\n\t *\n\t * @param modifierTypes 修饰符列表，元素不能为空\n\t * @return “或”之后的修饰符\n\t */\n\tprivate static int modifiersToInt(ModifierType... modifierTypes) {\n\t\tint modifier = modifierTypes[0].getValue();\n\t\tfor (int i = 1; i < modifierTypes.length; i++) {\n\t\t\tmodifier |= modifierTypes[i].getValue();\n\t\t}\n\t\treturn modifier;\n\t}\n\t//-------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.math.Calculator;\nimport cn.hutool.core.text.CharSequenceUtil;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.math.RoundingMode;\nimport java.text.DecimalFormat;\nimport java.text.NumberFormat;\nimport java.text.ParseException;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * 数字工具类<br>\n * 对于精确值计算应该使用 {@link BigDecimal}<br>\n * JDK7中<strong>BigDecimal(double val)</strong>构造方法的结果有一定的不可预知性，例如：\n *\n * <pre>\n * new BigDecimal(0.1)\n * </pre>\n * <p>\n * 表示的不是<strong>0.1</strong>而是<strong>0.1000000000000000055511151231257827021181583404541015625</strong>\n *\n * <p>\n * 这是因为0.1无法准确的表示为double。因此应该使用<strong>new BigDecimal(String)</strong>。\n * </p>\n * 相关介绍：\n * <ul>\n * <li>http://www.oschina.net/code/snippet_563112_25237</li>\n * <li>https://github.com/venusdrogon/feilong-core/wiki/one-jdk7-bug-thinking</li>\n * </ul>\n *\n * @author Looly\n */\npublic class NumberUtil {\n\n\t/**\n\t * 默认除法运算精度\n\t */\n\tprivate static final int DEFAULT_DIV_SCALE = 10;\n\n\t/**\n\t * 0-20对应的阶乘，超过20的阶乘会超过Long.MAX_VALUE\n\t */\n\tprivate static final long[] FACTORIALS = new long[]{\n\t\t1L, 1L, 2L, 6L, 24L, 120L, 720L, 5040L, 40320L, 362880L, 3628800L, 39916800L, 479001600L, 6227020800L,\n\t\t87178291200L, 1307674368000L, 20922789888000L, 355687428096000L, 6402373705728000L, 121645100408832000L,\n\t\t2432902008176640000L};\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(float v1, float v2) {\n\t\treturn add(Float.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(float v1, double v2) {\n\t\treturn add(Float.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(double v1, float v2) {\n\t\treturn add(Double.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(double v1, double v2) {\n\t\treturn add(Double.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(long v1, double v2) {\n\t\treturn add(Long.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(double v1, long v2) {\n\t\treturn add(Double.toString(v1), Long.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static double add(long v1, long v2) {\n\t\treturn add(Long.toString(v1), Long.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t * @since 3.1.1\n\t */\n\tpublic static double add(Double v1, Double v2) {\n\t\t//noinspection RedundantCast\n\t\treturn add((Number) v1, (Number) v2).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的加法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * <p>\n\t * 需要注意的是，在不同Locale下，数字的表示形式也是不同的，例如：<br>\n\t * 德国、荷兰、比利时、丹麦、意大利、罗马尼亚和欧洲大多地区使用`,`区分小数<br>\n\t * 也就是说，在这些国家地区，1.20表示120，而非1.2。\n\t * </p>\n\t *\n\t * @param v1 被加数\n\t * @param v2 加数\n\t * @return 和\n\t */\n\tpublic static BigDecimal add(Number v1, Number v2) {\n\t\treturn add(new Number[]{v1, v2});\n\t}\n\n\t/**\n\t * 提供精确的加法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * <p>\n\t * 需要注意的是，在不同Locale下，数字的表示形式也是不同的，例如：<br>\n\t * 德国、荷兰、比利时、丹麦、意大利、罗马尼亚和欧洲大多地区使用`,`区分小数<br>\n\t * 也就是说，在这些国家地区，1.20表示120，而非1.2。\n\t * </p>\n\t *\n\t * @param values 多个被加值\n\t * @return 和\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal add(Number... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tNumber value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (null != value) {\n\t\t\t\tresult = result.add(toBigDecimal(value));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的加法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被加值\n\t * @return 和\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal add(String... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tString value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (StrUtil.isNotBlank(value)) {\n\t\t\t\tresult = result.add(toBigDecimal(value));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的加法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被加值\n\t * @return 和\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal add(BigDecimal... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tBigDecimal value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (null != value) {\n\t\t\t\tresult = result.add(value);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的减法运算\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static double sub(float v1, float v2) {\n\t\treturn sub(Float.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的减法运算\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static double sub(float v1, double v2) {\n\t\treturn sub(Float.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的减法运算\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static double sub(double v1, float v2) {\n\t\treturn sub(Double.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的减法运算\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static double sub(double v1, double v2) {\n\t\treturn sub(Double.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的减法运算\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static double sub(Double v1, Double v2) {\n\t\t//noinspection RedundantCast\n\t\treturn sub((Number) v1, (Number) v2).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的减法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param v1 被减数\n\t * @param v2 减数\n\t * @return 差\n\t */\n\tpublic static BigDecimal sub(Number v1, Number v2) {\n\t\treturn sub(new Number[]{v1, v2});\n\t}\n\n\t/**\n\t * 提供精确的减法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被减值\n\t * @return 差\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal sub(Number... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tNumber value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (null != value) {\n\t\t\t\tresult = result.subtract(toBigDecimal(value));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的减法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被减值\n\t * @return 差\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal sub(String... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tString value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (StrUtil.isNotBlank(value)) {\n\t\t\t\tresult = result.subtract(toBigDecimal(value));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的减法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被减值\n\t * @return 差\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal sub(BigDecimal... values) {\n\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tBigDecimal value = values[0];\n\t\tBigDecimal result = toBigDecimal(value);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tif (null != value) {\n\t\t\t\tresult = result.subtract(value);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的乘法运算\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static double mul(float v1, float v2) {\n\t\treturn mul(Float.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的乘法运算\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static double mul(float v1, double v2) {\n\t\treturn mul(Float.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的乘法运算\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static double mul(double v1, float v2) {\n\t\treturn mul(Double.toString(v1), Float.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的乘法运算\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static double mul(double v1, double v2) {\n\t\treturn mul(Double.toString(v1), Double.toString(v2)).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的乘法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static double mul(Double v1, Double v2) {\n\t\t//noinspection RedundantCast\n\t\treturn mul((Number) v1, (Number) v2).doubleValue();\n\t}\n\n\t/**\n\t * 提供精确的乘法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t */\n\tpublic static BigDecimal mul(Number v1, Number v2) {\n\t\treturn mul(new Number[]{v1, v2});\n\t}\n\n\t/**\n\t * 提供精确的乘法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被乘值\n\t * @return 积\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal mul(Number... values) {\n\t\tif (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tNumber value = values[0];\n\t\tBigDecimal result = toBigDecimal(value.toString());\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tvalue = values[i];\n\t\t\tresult = result.multiply(toBigDecimal(value.toString()));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的乘法运算\n\t *\n\t * @param v1 被乘数\n\t * @param v2 乘数\n\t * @return 积\n\t * @since 3.0.8\n\t */\n\tpublic static BigDecimal mul(String v1, String v2) {\n\t\treturn mul(toBigDecimal(v1), toBigDecimal(v2));\n\t}\n\n\t/**\n\t * 提供精确的乘法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被乘值\n\t * @return 积\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal mul(String... values) {\n\t\tif (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tBigDecimal result = toBigDecimal(values[0]);\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tresult = result.multiply(toBigDecimal(values[i]));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供精确的乘法运算<br>\n\t * 如果传入多个值为null或者空，则返回0\n\t *\n\t * @param values 多个被乘值\n\t * @return 积\n\t * @since 4.0.0\n\t */\n\tpublic static BigDecimal mul(BigDecimal... values) {\n\t\tif (ArrayUtil.isEmpty(values) || ArrayUtil.hasNull(values)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\tBigDecimal result = values[0];\n\t\tfor (int i = 1; i < values.length; i++) {\n\t\t\tresult = result.multiply(values[i]);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, float v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, double v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, float v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, double v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(Double v1, Double v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t * @since 3.1.0\n\t */\n\tpublic static BigDecimal div(Number v1, Number v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t */\n\tpublic static BigDecimal div(String v1, String v2) {\n\t\treturn div(v1, v2, DEFAULT_DIV_SCALE);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, float v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, double v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, float v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, double v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(Double v1, Double v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t * @since 3.1.0\n\t */\n\tpublic static BigDecimal div(Number v1, Number v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度,后面的四舍五入\n\t *\n\t * @param v1    被除数\n\t * @param v2    除数\n\t * @param scale 精确度，如果为负值，取绝对值\n\t * @return 两个参数的商\n\t */\n\tpublic static BigDecimal div(String v1, String v2, int scale) {\n\t\treturn div(v1, v2, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, float v2, int scale, RoundingMode roundingMode) {\n\t\treturn div(Float.toString(v1), Float.toString(v2), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(float v1, double v2, int scale, RoundingMode roundingMode) {\n\t\treturn div(Float.toString(v1), Double.toString(v2), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, float v2, int scale, RoundingMode roundingMode) {\n\t\treturn div(Double.toString(v1), Float.toString(v2), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(double v1, double v2, int scale, RoundingMode roundingMode) {\n\t\treturn div(Double.toString(v1), Double.toString(v2), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static double div(Double v1, Double v2, int scale, RoundingMode roundingMode) {\n\t\t//noinspection RedundantCast\n\t\treturn div((Number) v1, (Number) v2, scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t * @since 3.1.0\n\t */\n\tpublic static BigDecimal div(Number v1, Number v2, int scale, RoundingMode roundingMode) {\n\t\tif (v1 instanceof BigDecimal && v2 instanceof BigDecimal) {\n\t\t\treturn div((BigDecimal) v1, (BigDecimal) v2, scale, roundingMode);\n\t\t}\n\t\treturn div(StrUtil.toStringOrNull(v1), StrUtil.toStringOrNull(v2), scale, roundingMode);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t */\n\tpublic static BigDecimal div(String v1, String v2, int scale, RoundingMode roundingMode) {\n\t\treturn div(toBigDecimal(v1), toBigDecimal(v2), scale, roundingMode);\n\t}\n\n\t/**\n\t * 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale指定精确度\n\t *\n\t * @param v1           被除数\n\t * @param v2           除数\n\t * @param scale        精确度，如果为负值，取绝对值\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 两个参数的商\n\t * @since 3.0.9\n\t */\n\tpublic static BigDecimal div(BigDecimal v1, BigDecimal v2, int scale, RoundingMode roundingMode) {\n\t\tAssert.notNull(v2, \"Divisor must be not null !\");\n\t\tif (null == v1) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\t\tif (scale < 0) {\n\t\t\tscale = -scale;\n\t\t}\n\t\treturn v1.divide(v2, scale, roundingMode);\n\t}\n\n\t/**\n\t * 补充Math.ceilDiv() JDK8中添加了和Math.floorDiv()但却没有ceilDiv()\n\t *\n\t * @param v1 被除数\n\t * @param v2 除数\n\t * @return 两个参数的商\n\t * @since 5.3.3\n\t */\n\tpublic static int ceilDiv(int v1, int v2) {\n\t\treturn (int) Math.ceil((double) v1 / v2);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- round\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 采用四舍五入策略 {@link RoundingMode#HALF_UP}<br>\n\t * 例如保留2位小数：123.456789 =》 123.46\n\t *\n\t * @param v     值\n\t * @param scale 保留小数位数\n\t * @return 新值\n\t */\n\tpublic static BigDecimal round(double v, int scale) {\n\t\treturn round(v, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 采用四舍五入策略 {@link RoundingMode#HALF_UP}<br>\n\t * 例如保留2位小数：123.456789 =》 123.46\n\t *\n\t * @param v     值\n\t * @param scale 保留小数位数\n\t * @return 新值\n\t */\n\tpublic static String roundStr(double v, int scale) {\n\t\treturn round(v, scale).toPlainString();\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 采用四舍五入策略 {@link RoundingMode#HALF_UP}<br>\n\t * 例如保留2位小数：123.456789 =》 123.46\n\t *\n\t * @param numberStr 数字值的字符串表现形式\n\t * @param scale     保留小数位数\n\t * @return 新值\n\t */\n\tpublic static BigDecimal round(String numberStr, int scale) {\n\t\treturn round(numberStr, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 采用四舍五入策略 {@link RoundingMode#HALF_UP}<br>\n\t * 例如保留2位小数：123.456789 =》 123.46\n\t *\n\t * @param number 数字值\n\t * @param scale  保留小数位数\n\t * @return 新值\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal round(BigDecimal number, int scale) {\n\t\treturn round(number, scale, RoundingMode.HALF_UP);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 采用四舍五入策略 {@link RoundingMode#HALF_UP}<br>\n\t * 例如保留2位小数：123.456789 =》 123.46\n\t *\n\t * @param numberStr 数字值的字符串表现形式\n\t * @param scale     保留小数位数\n\t * @return 新值\n\t * @since 3.2.2\n\t */\n\tpublic static String roundStr(String numberStr, int scale) {\n\t\treturn round(numberStr, scale).toPlainString();\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 例如保留四位小数：123.456789 =》 123.4567\n\t *\n\t * @param v            值\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 新值\n\t */\n\tpublic static BigDecimal round(double v, int scale, RoundingMode roundingMode) {\n\t\treturn round(Double.toString(v), scale, roundingMode);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 例如保留四位小数：123.456789 =》 123.4567\n\t *\n\t * @param v            值\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 新值\n\t * @since 3.2.2\n\t */\n\tpublic static String roundStr(double v, int scale, RoundingMode roundingMode) {\n\t\treturn round(v, scale, roundingMode).toPlainString();\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 例如保留四位小数：123.456789 =》 123.4567\n\t *\n\t * @param numberStr    数字值的字符串表现形式\n\t * @param scale        保留小数位数，如果传入小于0，则默认0\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}，如果传入null则默认四舍五入\n\t * @return 新值\n\t */\n\tpublic static BigDecimal round(String numberStr, int scale, RoundingMode roundingMode) {\n\t\tAssert.notBlank(numberStr);\n\t\tif (scale < 0) {\n\t\t\tscale = 0;\n\t\t}\n\t\treturn round(toBigDecimal(numberStr), scale, roundingMode);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 例如保留四位小数：123.456789 =》 123.4567\n\t *\n\t * @param number       数字值\n\t * @param scale        保留小数位数，如果传入小于0，则默认0\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}，如果传入null则默认四舍五入\n\t * @return 新值\n\t */\n\tpublic static BigDecimal round(BigDecimal number, int scale, RoundingMode roundingMode) {\n\t\tif (null == number) {\n\t\t\tnumber = BigDecimal.ZERO;\n\t\t}\n\t\tif (scale < 0) {\n\t\t\tscale = 0;\n\t\t}\n\t\tif (null == roundingMode) {\n\t\t\troundingMode = RoundingMode.HALF_UP;\n\t\t}\n\n\t\treturn number.setScale(scale, roundingMode);\n\t}\n\n\t/**\n\t * 保留固定位数小数<br>\n\t * 例如保留四位小数：123.456789 =》 123.4567\n\t *\n\t * @param numberStr    数字值的字符串表现形式\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 新值\n\t * @since 3.2.2\n\t */\n\tpublic static String roundStr(String numberStr, int scale, RoundingMode roundingMode) {\n\t\treturn round(numberStr, scale, roundingMode).toPlainString();\n\t}\n\n\t/**\n\t * 四舍六入五成双计算法\n\t * <p>\n\t * 四舍六入五成双是一种比较精确比较科学的计数保留法，是一种数字修约规则。\n\t * </p>\n\t *\n\t * <pre>\n\t * 算法规则:\n\t * 四舍六入五考虑，\n\t * 五后非零就进一，\n\t * 五后皆零看奇偶，\n\t * 五前为偶应舍去，\n\t * 五前为奇要进一。\n\t * </pre>\n\t *\n\t * @param number 需要科学计算的数据\n\t * @param scale  保留的小数位\n\t * @return 结果\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal roundHalfEven(Number number, int scale) {\n\t\treturn roundHalfEven(toBigDecimal(number), scale);\n\t}\n\n\t/**\n\t * 四舍六入五成双计算法\n\t * <p>\n\t * 四舍六入五成双是一种比较精确比较科学的计数保留法，是一种数字修约规则。\n\t * </p>\n\t *\n\t * <pre>\n\t * 算法规则:\n\t * 四舍六入五考虑，\n\t * 五后非零就进一，\n\t * 五后皆零看奇偶，\n\t * 五前为偶应舍去，\n\t * 五前为奇要进一。\n\t * </pre>\n\t *\n\t * @param value 需要科学计算的数据\n\t * @param scale 保留的小数位\n\t * @return 结果\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal roundHalfEven(BigDecimal value, int scale) {\n\t\treturn round(value, scale, RoundingMode.HALF_EVEN);\n\t}\n\n\t/**\n\t * 保留固定小数位数，舍去多余位数\n\t *\n\t * @param number 需要科学计算的数据\n\t * @param scale  保留的小数位\n\t * @return 结果\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal roundDown(Number number, int scale) {\n\t\treturn roundDown(toBigDecimal(number), scale);\n\t}\n\n\t/**\n\t * 保留固定小数位数，舍去多余位数\n\t *\n\t * @param value 需要科学计算的数据\n\t * @param scale 保留的小数位\n\t * @return 结果\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal roundDown(BigDecimal value, int scale) {\n\t\treturn round(value, scale, RoundingMode.DOWN);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- decimalFormat\n\n\t/**\n\t * 格式化double<br>\n\t * 对 {@link DecimalFormat} 做封装<br>\n\t *\n\t * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充，# 表示只要有可能就把数字拉上这个位置。<br>\n\t *                <ul>\n\t *                <li>0 =》 取一位整数</li>\n\t *                <li>0.00 =》 取一位整数和两位小数</li>\n\t *                <li>00.000 =》 取两位整数和三位小数</li>\n\t *                <li># =》 取所有整数部分</li>\n\t *                <li>#.##% =》 以百分比方式计数，并取两位小数</li>\n\t *                <li>#.#####E0 =》 显示为科学计数法，并取五位小数</li>\n\t *                <li>,### =》 每三位以逗号进行分隔，例如：299,792,458</li>\n\t *                <li>光速大小为每秒,###米 =》 将格式嵌入文本</li>\n\t *                </ul>\n\t * @param value   值\n\t * @return 格式化后的值\n\t */\n\tpublic static String decimalFormat(String pattern, double value) {\n\t\tAssert.isTrue(isValid(value), \"value is NaN or Infinite!\");\n\t\treturn new DecimalFormat(pattern).format(value);\n\t}\n\n\t/**\n\t * 格式化double<br>\n\t * 对 {@link DecimalFormat} 做封装<br>\n\t *\n\t * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充，# 表示只要有可能就把数字拉上这个位置。<br>\n\t *                <ul>\n\t *                <li>0 =》 取一位整数</li>\n\t *                <li>0.00 =》 取一位整数和两位小数</li>\n\t *                <li>00.000 =》 取两位整数和三位小数</li>\n\t *                <li># =》 取所有整数部分</li>\n\t *                <li>#.##% =》 以百分比方式计数，并取两位小数</li>\n\t *                <li>#.#####E0 =》 显示为科学计数法，并取五位小数</li>\n\t *                <li>,### =》 每三位以逗号进行分隔，例如：299,792,458</li>\n\t *                <li>光速大小为每秒,###米 =》 将格式嵌入文本</li>\n\t *                </ul>\n\t * @param value   值\n\t * @return 格式化后的值\n\t * @since 3.0.5\n\t */\n\tpublic static String decimalFormat(String pattern, long value) {\n\t\treturn new DecimalFormat(pattern).format(value);\n\t}\n\n\t/**\n\t * 格式化double<br>\n\t * 对 {@link DecimalFormat} 做封装<br>\n\t *\n\t * @param pattern 格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充，# 表示只要有可能就把数字拉上这个位置。<br>\n\t *                <ul>\n\t *                <li>0 =》 取一位整数</li>\n\t *                <li>0.00 =》 取一位整数和两位小数</li>\n\t *                <li>00.000 =》 取两位整数和三位小数</li>\n\t *                <li># =》 取所有整数部分</li>\n\t *                <li>#.##% =》 以百分比方式计数，并取两位小数</li>\n\t *                <li>#.#####E0 =》 显示为科学计数法，并取五位小数</li>\n\t *                <li>,### =》 每三位以逗号进行分隔，例如：299,792,458</li>\n\t *                <li>光速大小为每秒,###米 =》 将格式嵌入文本</li>\n\t *                </ul>\n\t * @param value   值，支持BigDecimal、BigInteger、Number等类型\n\t * @return 格式化后的值\n\t * @since 5.1.6\n\t */\n\tpublic static String decimalFormat(String pattern, Object value) {\n\t\treturn decimalFormat(pattern, value, null);\n\t}\n\n\t/**\n\t * 格式化double<br>\n\t * 对 {@link DecimalFormat} 做封装<br>\n\t *\n\t * @param pattern      格式 格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充，# 表示只要有可能就把数字拉上这个位置。<br>\n\t *                     <ul>\n\t *                     <li>0 =》 取一位整数</li>\n\t *                     <li>0.00 =》 取一位整数和两位小数</li>\n\t *                     <li>00.000 =》 取两位整数和三位小数</li>\n\t *                     <li># =》 取所有整数部分</li>\n\t *                     <li>#.##% =》 以百分比方式计数，并取两位小数</li>\n\t *                     <li>#.#####E0 =》 显示为科学计数法，并取五位小数</li>\n\t *                     <li>,### =》 每三位以逗号进行分隔，例如：299,792,458</li>\n\t *                     <li>光速大小为每秒,###米 =》 将格式嵌入文本</li>\n\t *                     </ul>\n\t * @param value        值，支持BigDecimal、BigInteger、Number等类型\n\t * @param roundingMode 保留小数的方式枚举\n\t * @return 格式化后的值\n\t * @since 5.6.5\n\t */\n\tpublic static String decimalFormat(String pattern, Object value, RoundingMode roundingMode) {\n\t\tif (value instanceof Number) {\n\t\t\tAssert.isTrue(isValidNumber((Number) value), \"value is NaN or Infinite!\");\n\t\t}\n\t\tfinal DecimalFormat decimalFormat = new DecimalFormat(pattern);\n\t\tif (null != roundingMode) {\n\t\t\tdecimalFormat.setRoundingMode(roundingMode);\n\t\t}\n\t\treturn decimalFormat.format(value);\n\t}\n\n\t/**\n\t * 格式化金额输出，每三位用逗号分隔\n\t *\n\t * @param value 金额\n\t * @return 格式化后的值\n\t * @since 3.0.9\n\t */\n\tpublic static String decimalFormatMoney(double value) {\n\t\treturn decimalFormat(\",##0.00\", value);\n\t}\n\n\t/**\n\t * 格式化百分比，小数采用四舍五入方式\n\t *\n\t * @param number 值\n\t * @param scale  保留小数位数\n\t * @return 百分比\n\t * @since 3.2.3\n\t */\n\tpublic static String formatPercent(double number, int scale) {\n\t\tfinal NumberFormat format = NumberFormat.getPercentInstance();\n\t\tformat.setMaximumFractionDigits(scale);\n\t\treturn format.format(number);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- isXXX\n\n\t/**\n\t * 是否为数字，支持包括：\n\t *\n\t * <pre>\n\t * 1、10进制\n\t * 2、16进制数字（0x开头）\n\t * 3、科学计数法形式（1234E3）\n\t * 4、类型标识形式（123D）\n\t * 5、正负数标识形式（+123、-234）\n\t * </pre>\n\t *\n\t * @param str 字符串值\n\t * @return 是否为数字\n\t */\n\tpublic static boolean isNumber(CharSequence str) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn false;\n\t\t}\n\t\tchar[] chars = str.toString().toCharArray();\n\t\tint sz = chars.length;\n\t\tboolean hasExp = false;\n\t\tboolean hasDecPoint = false;\n\t\tboolean allowSigns = false;\n\t\tboolean foundDigit = false;\n\t\t// deal with any possible sign up front\n\t\tint start = (chars[0] == '-' || chars[0] == '+') ? 1 : 0;\n\t\tif (sz > start + 1) {\n\t\t\tif (chars[start] == '0' && (chars[start + 1] == 'x' || chars[start + 1] == 'X')) {\n\t\t\t\tint i = start + 2;\n\t\t\t\tif (i == sz) {\n\t\t\t\t\treturn false; // str == \"0x\"\n\t\t\t\t}\n\t\t\t\t// checking hex (it can't be anything else)\n\t\t\t\tfor (; i < chars.length; i++) {\n\t\t\t\t\tif ((chars[i] < '0' || chars[i] > '9') && (chars[i] < 'a' || chars[i] > 'f') && (chars[i] < 'A' || chars[i] > 'F')) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tsz--; // don't want to loop to the last char, check it afterwords\n\t\t// for type qualifiers\n\t\tint i = start;\n\t\t// loop to the next to last char or to the last char if we need another digit to\n\t\t// make a valid number (e.g. chars[0..5] = \"1234E\")\n\t\twhile (i < sz || (i < sz + 1 && allowSigns && !foundDigit)) {\n\t\t\tif (chars[i] >= '0' && chars[i] <= '9') {\n\t\t\t\tfoundDigit = true;\n\t\t\t\tallowSigns = false;\n\n\t\t\t} else if (chars[i] == '.') {\n\t\t\t\tif (hasDecPoint || hasExp) {\n\t\t\t\t\t// two decimal points or dec in exponent\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\thasDecPoint = true;\n\t\t\t} else if (chars[i] == 'e' || chars[i] == 'E') {\n\t\t\t\t// we've already taken care of hex.\n\t\t\t\tif (hasExp) {\n\t\t\t\t\t// two E's\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (false == foundDigit) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\thasExp = true;\n\t\t\t\tallowSigns = true;\n\t\t\t} else if (chars[i] == '+' || chars[i] == '-') {\n\t\t\t\tif (!allowSigns) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tallowSigns = false;\n\t\t\t\tfoundDigit = false; // we need a digit after the E\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t\tif (i < chars.length) {\n\t\t\tif (chars[i] >= '0' && chars[i] <= '9') {\n\t\t\t\t// no type qualifier, OK\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (chars[i] == 'e' || chars[i] == 'E') {\n\t\t\t\t// can't have an E at the last byte\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (chars[i] == '.') {\n\t\t\t\tif (hasDecPoint || hasExp) {\n\t\t\t\t\t// two decimal points or dec in exponent\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t// single trailing decimal point after non-exponent is ok\n\t\t\t\treturn foundDigit;\n\t\t\t}\n\t\t\tif (!allowSigns && (chars[i] == 'd' || chars[i] == 'D' || chars[i] == 'f' || chars[i] == 'F')) {\n\t\t\t\treturn foundDigit;\n\t\t\t}\n\t\t\tif (chars[i] == 'l' || chars[i] == 'L') {\n\t\t\t\t// not allowing L with an exponent\n\t\t\t\treturn foundDigit && !hasExp && !hasDecPoint;\n\t\t\t}\n\t\t\t// last character is illegal\n\t\t\treturn false;\n\t\t}\n\t\t// allowSigns is true iff the val ends in 'E'\n\t\t// found digit it to make sure weird stuff like '.' and '1E-' doesn't pass\n\t\treturn false == allowSigns && foundDigit;\n\t}\n\n\t/**\n\t * 判断String是否是整数<br>\n\t * 支持10进制\n\t *\n\t * @param s String\n\t * @return 是否为整数\n\t */\n\tpublic static boolean isInteger(String s) {\n\t\tif (StrUtil.isBlank(s)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tInteger.parseInt(s);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 判断字符串是否是Long类型<br>\n\t * 支持10进制\n\t *\n\t * @param s String\n\t * @return 是否为{@link Long}类型\n\t * @since 4.0.0\n\t */\n\tpublic static boolean isLong(String s) {\n\t\tif (StrUtil.isBlank(s)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tLong.parseLong(s);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 判断字符串是否是浮点数\n\t *\n\t * @param s String\n\t * @return 是否为{@link Double}类型\n\t */\n\tpublic static boolean isDouble(String s) {\n\t\tif (StrUtil.isBlank(s)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tDouble.parseDouble(s);\n\t\t} catch (NumberFormatException ignore) {\n\t\t\treturn false;\n\t\t}\n\t\treturn s.contains(\".\");\n\t}\n\n\t/**\n\t * 是否是质数（素数）<br>\n\t * 质数表的质数又称素数。指整数在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。\n\t *\n\t * @param n 数字\n\t * @return 是否是质数\n\t */\n\tpublic static boolean isPrimes(int n) {\n\t\tAssert.isTrue(n > 1, \"The number must be > 1\");\n\t\tif (n <= 3) {\n\t\t\treturn true;\n\t\t} else if ((n & 1) == 0) {\n\t\t\treturn false;\n\t\t} else if (n % 3 == 0) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (int i = 5; i <= n / i; i += 6) {\n\t\t\tif (n % i == 0 || n % (i + 2) == 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t// ------------------------------------------------------------------------------------------- generateXXX\n\n\t/**\n\t * 生成不重复随机数 根据给定的最小数字和最大数字，以及随机数的个数，产生指定的不重复的数组\n\t *\n\t * @param begin 最小数字（包含该数）\n\t * @param end   最大数字（不包含该数）\n\t * @param size  指定产生随机数的个数\n\t * @return 随机int数组\n\t */\n\tpublic static int[] generateRandomNumber(int begin, int end, int size) {\n\t\t// 种子你可以随意生成，但不能重复\n\t\tfinal int[] seed = ArrayUtil.range(begin, end);\n\t\treturn generateRandomNumber(begin, end, size, seed);\n\t}\n\n\t/**\n\t * 生成不重复随机数 根据给定的最小数字和最大数字，以及随机数的个数，产生指定的不重复的数组\n\t *\n\t * @param begin 最小数字（包含该数）\n\t * @param end   最大数字（不包含该数）\n\t * @param size  指定产生随机数的个数\n\t * @param seed  种子，用于取随机数的int池\n\t * @return 随机int数组\n\t * @since 5.4.5\n\t */\n\tpublic static int[] generateRandomNumber(int begin, int end, int size, int[] seed) {\n\t\tif (begin > end) {\n\t\t\tint temp = begin;\n\t\t\tbegin = end;\n\t\t\tend = temp;\n\t\t}\n\t\t// 加入逻辑判断，确保begin<end并且size不能大于该表示范围\n\t\tAssert.isTrue((end - begin) >= size, \"Size is larger than range between begin and end!\");\n\t\tAssert.isTrue(seed.length >= size, \"Size is larger than seed size!\");\n\n\t\tfinal int[] ranArr = new int[size];\n\t\t// 数量你可以自己定义。\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\t// 得到一个位置\n\t\t\tint j = RandomUtil.randomInt(seed.length - i);\n\t\t\t// 得到那个位置的数值\n\t\t\tranArr[i] = seed[j];\n\t\t\t// 将最后一个未用的数字放到这里\n\t\t\tseed[j] = seed[seed.length - 1 - i];\n\t\t}\n\t\treturn ranArr;\n\t}\n\n\t/**\n\t * 生成不重复随机数 根据给定的最小数字和最大数字，以及随机数的个数，产生指定的不重复的数组\n\t *\n\t * @param begin 最小数字（包含该数）\n\t * @param end   最大数字（不包含该数）\n\t * @param size  指定产生随机数的个数\n\t * @return 随机int数组\n\t */\n\tpublic static Integer[] generateBySet(int begin, int end, int size) {\n\t\tif (begin > end) {\n\t\t\tint temp = begin;\n\t\t\tbegin = end;\n\t\t\tend = temp;\n\t\t}\n\t\t// 加入逻辑判断，确保begin<end并且size不能大于该表示范围\n\t\tif ((end - begin) < size) {\n\t\t\tthrow new UtilException(\"Size is larger than range between begin and end!\");\n\t\t}\n\n\t\tSet<Integer> set = new HashSet<>(size, 1);\n\t\twhile (set.size() < size) {\n\t\t\tset.add(begin + RandomUtil.randomInt(end - begin));\n\t\t}\n\n\t\treturn set.toArray(new Integer[0]);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- range\n\n\t/**\n\t * 从0开始给定范围内的整数列表，步进为1\n\t *\n\t * @param stop 结束（包含）\n\t * @return 整数列表\n\t * @since 3.3.1\n\t */\n\tpublic static int[] range(int stop) {\n\t\treturn range(0, stop);\n\t}\n\n\t/**\n\t * 给定范围内的整数列表，步进为1\n\t *\n\t * @param start 开始（包含）\n\t * @param stop  结束（包含）\n\t * @return 整数列表\n\t */\n\tpublic static int[] range(int start, int stop) {\n\t\treturn range(start, stop, 1);\n\t}\n\n\t/**\n\t * 给定范围内的整数列表\n\t *\n\t * @param start 开始（包含）\n\t * @param stop  结束（包含）\n\t * @param step  步进\n\t * @return 整数列表\n\t */\n\tpublic static int[] range(int start, int stop, int step) {\n\t\tif (start < stop) {\n\t\t\tstep = Math.abs(step);\n\t\t} else if (start > stop) {\n\t\t\tstep = -Math.abs(step);\n\t\t} else {// start == end\n\t\t\treturn new int[]{start};\n\t\t}\n\n\t\tint size = Math.abs((stop - start) / step) + 1;\n\t\tint[] values = new int[size];\n\t\tint index = 0;\n\t\tfor (int i = start; (step > 0) ? i <= stop : i >= stop; i += step) {\n\t\t\tvalues[index] = i;\n\t\t\tindex++;\n\t\t}\n\t\treturn values;\n\t}\n\n\t/**\n\t * 将给定范围内的整数添加到已有集合中，步进为1\n\t *\n\t * @param start  开始（包含）\n\t * @param stop   结束（包含）\n\t * @param values 集合\n\t * @return 集合\n\t */\n\tpublic static Collection<Integer> appendRange(int start, int stop, Collection<Integer> values) {\n\t\treturn appendRange(start, stop, 1, values);\n\t}\n\n\t/**\n\t * 将给定范围内的整数添加到已有集合中\n\t *\n\t * @param start  开始（包含）\n\t * @param stop   结束（包含）\n\t * @param step   步进\n\t * @param values 集合\n\t * @return 集合\n\t */\n\tpublic static Collection<Integer> appendRange(int start, int stop, int step, Collection<Integer> values) {\n\t\tif (start < stop) {\n\t\t\tstep = Math.abs(step);\n\t\t} else if (start > stop) {\n\t\t\tstep = -Math.abs(step);\n\t\t} else {// start == end\n\t\t\tvalues.add(start);\n\t\t\treturn values;\n\t\t}\n\n\t\tfor (int i = start; (step > 0) ? i <= stop : i >= stop; i += step) {\n\t\t\tvalues.add(i);\n\t\t}\n\t\treturn values;\n\t}\n\n\t// ------------------------------------------------------------------------------------------- others\n\n\t/**\n\t * 计算阶乘\n\t * <p>\n\t * n! = n * (n-1) * ... * 2 * 1\n\t * </p>\n\t *\n\t * @param n 阶乘起始\n\t * @return 结果\n\t * @since 5.6.0\n\t */\n\tpublic static BigInteger factorial(BigInteger n) {\n\t\tif (n.equals(BigInteger.ZERO)) {\n\t\t\treturn BigInteger.ONE;\n\t\t}\n\t\treturn factorial(n, BigInteger.ZERO);\n\t}\n\n\t/**\n\t * 计算范围阶乘\n\t * <p>\n\t * factorial(start, end) = start * (start - 1) * ... * (end + 1)\n\t * </p>\n\t *\n\t * @param start 阶乘起始（包含）\n\t * @param end   阶乘结束，必须小于起始（不包括）\n\t * @return 结果\n\t * @since 5.6.0\n\t */\n\tpublic static BigInteger factorial(BigInteger start, BigInteger end) {\n\t\tAssert.notNull(start, \"Factorial start must be not null!\");\n\t\tAssert.notNull(end, \"Factorial end must be not null!\");\n\t\tif (start.compareTo(BigInteger.ZERO) < 0 || end.compareTo(BigInteger.ZERO) < 0) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Factorial start and end both must be > 0, but got start={}, end={}\", start, end));\n\t\t}\n\n\t\tif (start.equals(BigInteger.ZERO)) {\n\t\t\tstart = BigInteger.ONE;\n\t\t}\n\n\t\tif (end.compareTo(BigInteger.ONE) < 0) {\n\t\t\tend = BigInteger.ONE;\n\t\t}\n\n\t\tBigInteger result = start;\n\t\tend = end.add(BigInteger.ONE);\n\t\twhile (start.compareTo(end) > 0) {\n\t\t\tstart = start.subtract(BigInteger.ONE);\n\t\t\tresult = result.multiply(start);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 计算范围阶乘\n\t * <p>\n\t * factorial(start, end) = start * (start - 1) * ... * (end + 1)\n\t * </p>\n\t *\n\t * @param start 阶乘起始（包含）\n\t * @param end   阶乘结束，必须小于起始（不包括）\n\t * @return 结果\n\t * @since 4.1.0\n\t */\n\tpublic static long factorial(long start, long end) {\n\t\t// 负数没有阶乘\n\t\tif (start < 0 || end < 0) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Factorial start and end both must be >= 0, but got start={}, end={}\", start, end));\n\t\t}\n\t\tif (0L == start || start == end) {\n\t\t\treturn 1L;\n\t\t}\n\t\tif (start < end) {\n\t\t\treturn 0L;\n\t\t}\n\t\treturn factorialMultiplyAndCheck(start, factorial(start - 1, end));\n\t}\n\n\t/**\n\t * 计算范围阶乘中校验中间的计算是否存在溢出，factorial提前做了负数和0的校验，因此这里没有校验数字的正负\n\t *\n\t * @param a 乘数\n\t * @param b 被乘数\n\t * @return 如果 a * b的结果没有溢出直接返回，否则抛出异常\n\t */\n\tprivate static long factorialMultiplyAndCheck(long a, long b) {\n\t\tif (a <= Long.MAX_VALUE / b) {\n\t\t\treturn a * b;\n\t\t}\n\t\tthrow new IllegalArgumentException(StrUtil.format(\"Overflow in multiplication: {} * {}\", a, b));\n\t}\n\n\t/**\n\t * 计算阶乘\n\t * <p>\n\t * n! = n * (n-1) * ... * 2 * 1\n\t * </p>\n\t *\n\t * @param n 阶乘起始\n\t * @return 结果\n\t */\n\tpublic static long factorial(long n) {\n\t\tif (n < 0 || n > 20) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Factorial must have n >= 0 and n <= 20 for n!, but got n = {}\", n));\n\t\t}\n\t\treturn FACTORIALS[(int) n];\n\t}\n\n\t/**\n\t * 平方根算法<br>\n\t * 推荐使用 {@link Math#sqrt(double)}\n\t *\n\t * @param x 值\n\t * @return 平方根\n\t */\n\tpublic static long sqrt(long x) {\n\t\tlong y = 0;\n\t\tlong b = (~Long.MAX_VALUE) >>> 1;\n\t\twhile (b > 0) {\n\t\t\tif (x >= y + b) {\n\t\t\t\tx -= y + b;\n\t\t\t\ty >>= 1;\n\t\t\t\ty += b;\n\t\t\t} else {\n\t\t\t\ty >>= 1;\n\t\t\t}\n\t\t\tb >>= 2;\n\t\t}\n\t\treturn y;\n\t}\n\n\t/**\n\t * 可以用于计算双色球、大乐透注数的方法<br>\n\t * 比如大乐透35选5可以这样调用processMultiple(7,5); 就是数学中的：C75=7*6/2*1\n\t *\n\t * @param selectNum 选中小球个数\n\t * @param minNum    最少要选中多少个小球\n\t * @return 注数\n\t */\n\tpublic static int processMultiple(int selectNum, int minNum) {\n\t\tint result;\n\t\tresult = mathSubNode(selectNum, minNum) / mathNode(selectNum - minNum);\n\t\treturn result;\n\t}\n\n\t/**\n\t * 最大公约数\n\t *\n\t * @param m 第一个值\n\t * @param n 第二个值\n\t * @return 最大公约数\n\t */\n\tpublic static int divisor(int m, int n) {\n\t\twhile (m % n != 0) {\n\t\t\tint temp = m % n;\n\t\t\tm = n;\n\t\t\tn = temp;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/**\n\t * 最小公倍数\n\t *\n\t * @param m 第一个值\n\t * @param n 第二个值\n\t * @return 最小公倍数\n\t */\n\tpublic static int multiple(int m, int n) {\n\t\t// 先计算最大公约数\n\t\tint gcd = divisor(m, n);\n\t\t// 使用长整型避免溢出，再转换回整型\n\t\tlong result = (long) m / gcd * (long) n;\n\t\t// 检查结果是否在int范围内\n\t\tif (result > Integer.MAX_VALUE || result < Integer.MIN_VALUE) {\n\t\t\tthrow new ArithmeticException(\"Integer overflow: \" + m + \" * \" + n + \" / \" + gcd);\n\t\t}\n\t\treturn (int) result;\n\t}\n\n\t/**\n\t * 获得数字对应的二进制字符串\n\t * <ul>\n\t *     <li>Integer/Long：直接使用 JDK 内置方法转换</li>\n\t *     <li>Byte/Short：转换为无符号整数后补充前导零至对应位数（Byte=8位，Short=16位）</li>\n\t *     <li>Float/Double：使用 IEEE 754 标准格式转换，Float=32位，Double=64位</li>\n\t * </ul>\n\t *\n\t * @param number 数字\n\t * @return 二进制字符串\n\t */\n\tpublic static String getBinaryStr(Number number) {\n\t\tAssert.notNull(number, \"Number must be not null!\");\n\n\t\tif (number instanceof Double) {\n\t\t\t// 处理double类型\n\t\t\tlong bits = Double.doubleToLongBits((Double) number);\n\t\t\treturn String.format(\"%64s\", Long.toBinaryString(bits)).replace(' ', '0');\n\t\t} else if (number instanceof Float) {\n\t\t\t// 处理float类型\n\t\t\tint bits = Float.floatToIntBits((Float) number);\n\t\t\treturn String.format(\"%32s\", Integer.toBinaryString(bits)).replace(' ', '0');\n\t\t}else if (number instanceof Long) {\n\t\t\treturn Long.toBinaryString((Long) number);\n\t\t} else if (number instanceof Integer) {\n\t\t\treturn Integer.toBinaryString((Integer) number);\n\t\t} else if (number instanceof Byte) {\n\t\t\t// Byte是8位，补前导0至8位\n\t\t\treturn String.format(\"%8s\", Integer.toBinaryString(number.byteValue() & 0xFF)).replace(' ', '0');\n\t\t} else if (number instanceof Short) {\n\t\t\t// Short是16位，补前导0至16位\n\t\t\treturn String.format(\"%16s\", Integer.toBinaryString(number.shortValue() & 0xFFFF)).replace(' ', '0');\n\t\t} else if (number instanceof BigInteger) {\n\t\t\t// 大数整数类型\n\t\t\treturn ((BigInteger) number).toString(2);\n\t\t} else {\n\t\t\treturn Long.toBinaryString(number.longValue());\n\t\t}\n\t}\n\n\t/**\n\t * 二进制转int\n\t *\n\t * @param binaryStr 二进制字符串\n\t * @return int\n\t */\n\tpublic static int binaryToInt(String binaryStr) {\n\t\treturn Integer.parseInt(binaryStr, 2);\n\t}\n\n\t/**\n\t * 二进制转long\n\t *\n\t * @param binaryStr 二进制字符串\n\t * @return long\n\t */\n\tpublic static long binaryToLong(String binaryStr) {\n\t\treturn Long.parseLong(binaryStr, 2);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- compare\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回小于0的数，x&gt;y返回大于0的数\n\t * @see Character#compare(char, char)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(char x, char y) {\n\t\treturn Character.compare(x, y);\n\t}\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回小于0的数，x&gt;y返回大于0的数\n\t * @see Double#compare(double, double)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(double x, double y) {\n\t\treturn Double.compare(x, y);\n\t}\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回小于0的数，x&gt;y返回大于0的数\n\t * @see Integer#compare(int, int)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(int x, int y) {\n\t\treturn Integer.compare(x, y);\n\t}\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回小于0的数，x&gt;y返回大于0的数\n\t * @see Long#compare(long, long)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(long x, long y) {\n\t\treturn Long.compare(x, y);\n\t}\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回小于0的数，x&gt;y返回大于0的数\n\t * @see Short#compare(short, short)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(short x, short y) {\n\t\treturn Short.compare(x, y);\n\t}\n\n\t/**\n\t * 比较两个值的大小\n\t *\n\t * @param x 第一个值\n\t * @param y 第二个值\n\t * @return x==y返回0，x&lt;y返回-1，x&gt;y返回1\n\t * @see Byte#compare(byte, byte)\n\t * @since 3.0.1\n\t */\n\tpublic static int compare(byte x, byte y) {\n\t\treturn Byte.compare(x, y);\n\t}\n\n\t/**\n\t * 比较大小，参数1 &gt; 参数2 返回true\n\t *\n\t * @param bigNum1 数字1\n\t * @param bigNum2 数字2\n\t * @return 是否大于\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isGreater(BigDecimal bigNum1, BigDecimal bigNum2) {\n\t\tAssert.notNull(bigNum1);\n\t\tAssert.notNull(bigNum2);\n\t\treturn bigNum1.compareTo(bigNum2) > 0;\n\t}\n\n\t/**\n\t * 比较大小，参数1 &gt;= 参数2 返回true\n\t *\n\t * @param bigNum1 数字1\n\t * @param bigNum2 数字2\n\t * @return 是否大于等于\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isGreaterOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) {\n\t\tAssert.notNull(bigNum1);\n\t\tAssert.notNull(bigNum2);\n\t\treturn bigNum1.compareTo(bigNum2) >= 0;\n\t}\n\n\t/**\n\t * 比较大小，参数1 &lt; 参数2 返回true\n\t *\n\t * @param bigNum1 数字1\n\t * @param bigNum2 数字2\n\t * @return 是否小于\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isLess(BigDecimal bigNum1, BigDecimal bigNum2) {\n\t\tAssert.notNull(bigNum1);\n\t\tAssert.notNull(bigNum2);\n\t\treturn bigNum1.compareTo(bigNum2) < 0;\n\t}\n\n\t/**\n\t * 比较大小，参数1&lt;=参数2 返回true\n\t *\n\t * @param bigNum1 数字1\n\t * @param bigNum2 数字2\n\t * @return 是否小于等于\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isLessOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) {\n\t\tAssert.notNull(bigNum1);\n\t\tAssert.notNull(bigNum2);\n\t\treturn bigNum1.compareTo(bigNum2) <= 0;\n\t}\n\n\t/**\n\t * 检查值是否在指定范围内\n\t *\n\t * @param value      值\n\t * @param minInclude 最小值（包含）\n\t * @param maxInclude 最大值（包含）\n\t * @return 经过检查后的值\n\t * @since 5.8.5\n\t */\n\tpublic static boolean isIn(final BigDecimal value, final BigDecimal minInclude, final BigDecimal maxInclude) {\n\t\tAssert.notNull(value);\n\t\tAssert.notNull(minInclude);\n\t\tAssert.notNull(maxInclude);\n\t\treturn isGreaterOrEqual(value, minInclude) && isLessOrEqual(value, maxInclude);\n\t}\n\n\t/**\n\t * 比较大小，值相等 返回true<br>\n\t * 此方法通过调用{@link Double#doubleToLongBits(double)}方法来判断是否相等<br>\n\t * 此方法判断值相等时忽略精度的，即0.00 == 0\n\t *\n\t * @param num1 数字1\n\t * @param num2 数字2\n\t * @return 是否相等\n\t * @since 5.4.2\n\t */\n\tpublic static boolean equals(double num1, double num2) {\n\t\treturn Double.doubleToLongBits(num1) == Double.doubleToLongBits(num2);\n\t}\n\n\t/**\n\t * 比较大小，值相等 返回true<br>\n\t * 此方法通过调用{@link Float#floatToIntBits(float)}方法来判断是否相等<br>\n\t * 此方法判断值相等时忽略精度的，即0.00 == 0\n\t *\n\t * @param num1 数字1\n\t * @param num2 数字2\n\t * @return 是否相等\n\t * @since 5.4.5\n\t */\n\tpublic static boolean equals(float num1, float num2) {\n\t\treturn Float.floatToIntBits(num1) == Float.floatToIntBits(num2);\n\t}\n\n\t/**\n\t * 比较大小，值相等 返回true<br>\n\t * 此方法修复传入long型数据由于没有本类型重载方法,导致数据精度丢失\n\t *\n\t * @param num1 数字1\n\t * @param num2 数字2\n\t * @return 是否相等\n\t * @since 5.7.19\n\t */\n\tpublic static boolean equals(long num1, long num2) {\n\t\treturn num1 == num2;\n\t}\n\n\t/**\n\t * 比较数字值是否相等，相等返回{@code true}<br>\n\t * 需要注意的是{@link BigDecimal}需要特殊处理<br>\n\t * BigDecimal使用compareTo方式判断，因为使用equals方法也判断小数位数，如2.0和2.00就不相等，<br>\n\t * 此方法判断值相等时忽略精度的，即0.00 == 0\n\t *\n\t * <ul>\n\t *     <li>如果用户提供两个Number都是{@link BigDecimal}，则通过调用{@link BigDecimal#compareTo(BigDecimal)}方法来判断是否相等</li>\n\t *     <li>其他情况调用{@link Number#equals(Object)}比较</li>\n\t * </ul>\n\t *\n\t * @param number1 数字1\n\t * @param number2 数字2\n\t * @return 是否相等\n\t * @see Objects#equals(Object, Object)\n\t * @since 5.8.17\n\t */\n\tpublic static boolean equals(final Number number1, final Number number2) {\n\t\tif (number1 instanceof BigDecimal && number2 instanceof BigDecimal) {\n\t\t\t// BigDecimal使用compareTo方式判断，因为使用equals方法也判断小数位数，如2.0和2.00就不相等\n\t\t\treturn equals((BigDecimal) number1, (BigDecimal) number2);\n\t\t}\n\t\treturn Objects.equals(number1, number2);\n\t}\n\n\t/**\n\t * 比较大小，值相等 返回true<br>\n\t * 此方法通过调用{@link BigDecimal#compareTo(BigDecimal)}方法来判断是否相等<br>\n\t * 此方法判断值相等时忽略精度的，即0.00 == 0\n\t *\n\t * @param bigNum1 数字1\n\t * @param bigNum2 数字2\n\t * @return 是否相等\n\t */\n\tpublic static boolean equals(BigDecimal bigNum1, BigDecimal bigNum2) {\n\t\t//noinspection NumberEquality\n\t\tif (bigNum1 == bigNum2) {\n\t\t\t// 如果用户传入同一对象，省略compareTo以提高性能。\n\t\t\treturn true;\n\t\t}\n\t\tif (bigNum1 == null || bigNum2 == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn 0 == bigNum1.compareTo(bigNum2);\n\t}\n\n\t/**\n\t * 比较两个字符是否相同\n\t *\n\t * @param c1         字符1\n\t * @param c2         字符2\n\t * @param ignoreCase 是否忽略大小写\n\t * @return 是否相同\n\t * @see CharUtil#equals(char, char, boolean)\n\t * @since 3.2.1\n\t */\n\tpublic static boolean equals(char c1, char c2, boolean ignoreCase) {\n\t\treturn CharUtil.equals(c1, c2, ignoreCase);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(Comparable[])\n\t * @since 4.0.7\n\t */\n\tpublic static <T extends Comparable<? super T>> T min(T[] numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(long...)\n\t * @since 4.0.7\n\t */\n\tpublic static long min(long... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(int...)\n\t * @since 4.0.7\n\t */\n\tpublic static int min(int... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(short...)\n\t * @since 4.0.7\n\t */\n\tpublic static short min(short... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(double...)\n\t * @since 4.0.7\n\t */\n\tpublic static double min(double... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(float...)\n\t * @since 4.0.7\n\t */\n\tpublic static float min(float... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @see ArrayUtil#min(Comparable[])\n\t * @since 5.0.8\n\t */\n\tpublic static BigDecimal min(BigDecimal... numberArray) {\n\t\treturn ArrayUtil.min(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param <T>         元素类型\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(Comparable[])\n\t * @since 4.0.7\n\t */\n\tpublic static <T extends Comparable<? super T>> T max(T[] numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(long...)\n\t * @since 4.0.7\n\t */\n\tpublic static long max(long... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(int...)\n\t * @since 4.0.7\n\t */\n\tpublic static int max(int... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(short...)\n\t * @since 4.0.7\n\t */\n\tpublic static short max(short... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(double...)\n\t * @since 4.0.7\n\t */\n\tpublic static double max(double... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(float...)\n\t * @since 4.0.7\n\t */\n\tpublic static float max(float... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @see ArrayUtil#max(Comparable[])\n\t * @since 5.0.8\n\t */\n\tpublic static BigDecimal max(BigDecimal... numberArray) {\n\t\treturn ArrayUtil.max(numberArray);\n\t}\n\n\t/**\n\t * 数字转字符串<br>\n\t * 调用{@link Number#toString()}，并去除尾小数点儿后多余的0\n\t *\n\t * @param number       A Number\n\t * @param defaultValue 如果number参数为{@code null}，返回此默认值\n\t * @return A String.\n\t * @since 3.0.9\n\t */\n\tpublic static String toStr(Number number, String defaultValue) {\n\t\treturn (null == number) ? defaultValue : toStr(number);\n\t}\n\n\t/**\n\t * 数字转字符串<br>\n\t * 调用{@link Number#toString()}或 {@link BigDecimal#toPlainString()}，并去除尾小数点儿后多余的0\n\t *\n\t * @param number A Number\n\t * @return A String.\n\t */\n\tpublic static String toStr(Number number) {\n\t\treturn toStr(number, true);\n\t}\n\n\t/**\n\t * 数字转字符串<br>\n\t * 调用{@link Number#toString()}或 {@link BigDecimal#toPlainString()}，并去除尾小数点儿后多余的0\n\t *\n\t * @param number               A Number\n\t * @param isStripTrailingZeros 是否去除末尾多余0，例如5.0返回5\n\t * @return A String.\n\t */\n\tpublic static String toStr(Number number, boolean isStripTrailingZeros) {\n\t\tAssert.notNull(number, \"Number is null !\");\n\n\t\t// BigDecimal单独处理，使用非科学计数法\n\t\tif (number instanceof BigDecimal) {\n\t\t\treturn toStr((BigDecimal) number, isStripTrailingZeros);\n\t\t}\n\t\tAssert.isTrue(isValidNumber(number), \"Number is non-finite!\");\n\n\t\t// 去掉小数点儿后多余的0\n\t\tString string = number.toString();\n\t\tif (isStripTrailingZeros) {\n\t\t\tif (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) {\n\t\t\t\twhile (string.endsWith(\"0\")) {\n\t\t\t\t\tstring = string.substring(0, string.length() - 1);\n\t\t\t\t}\n\t\t\t\tif (string.endsWith(\".\")) {\n\t\t\t\t\tstring = string.substring(0, string.length() - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn string;\n\t}\n\n\t/**\n\t * {@link BigDecimal}数字转字符串<br>\n\t * 调用{@link BigDecimal#toPlainString()}，并去除尾小数点儿后多余的0\n\t *\n\t * @param bigDecimal A {@link BigDecimal}\n\t * @return A String.\n\t * @since 5.4.6\n\t */\n\tpublic static String toStr(BigDecimal bigDecimal) {\n\t\treturn toStr(bigDecimal, true);\n\t}\n\n\t/**\n\t * {@link BigDecimal}数字转字符串<br>\n\t * 调用{@link BigDecimal#toPlainString()}，可选去除尾小数点儿后多余的0\n\t *\n\t * @param bigDecimal           A {@link BigDecimal}\n\t * @param isStripTrailingZeros 是否去除末尾多余0，例如5.0返回5\n\t * @return A String.\n\t * @since 5.4.6\n\t */\n\tpublic static String toStr(BigDecimal bigDecimal, boolean isStripTrailingZeros) {\n\t\tAssert.notNull(bigDecimal, \"BigDecimal is null !\");\n\t\tif (isStripTrailingZeros) {\n\t\t\tbigDecimal = bigDecimal.stripTrailingZeros();\n\t\t}\n\t\treturn bigDecimal.toPlainString();\n\t}\n\n\t/**\n\t * 数字转{@link BigDecimal}<br>\n\t * Float、Double等有精度问题，转换为字符串后再转换<br>\n\t * null转换为0\n\t *\n\t * @param number 数字\n\t * @return {@link BigDecimal}\n\t * @since 4.0.9\n\t */\n\tpublic static BigDecimal toBigDecimal(Number number) {\n\t\tif (null == number) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\t\t// issue#3423@Github of CVE-2023-51080\n\t\tAssert.isTrue(isValidNumber(number), \"Number is invalid!\");\n\n\t\tif (number instanceof BigDecimal) {\n\t\t\treturn (BigDecimal) number;\n\t\t} else if (number instanceof Long) {\n\t\t\treturn new BigDecimal((Long) number);\n\t\t} else if (number instanceof Integer) {\n\t\t\treturn new BigDecimal((Integer) number);\n\t\t} else if (number instanceof BigInteger) {\n\t\t\treturn new BigDecimal((BigInteger) number);\n\t\t}\n\n\t\t// Float、Double等有精度问题，转换为字符串后再转换\n\t\treturn new BigDecimal(number.toString());\n\t}\n\n\t/**\n\t * 数字转{@link BigDecimal}<br>\n\t * null或\"\"或空白符转换为0\n\t *\n\t * @param numberStr 数字字符串\n\t * @return {@link BigDecimal}\n\t * @since 4.0.9\n\t */\n\tpublic static BigDecimal toBigDecimal(String numberStr) {\n\t\tif (StrUtil.isBlank(numberStr)) {\n\t\t\treturn BigDecimal.ZERO;\n\t\t}\n\n\t\ttry {\n\t\t\treturn new BigDecimal(numberStr);\n\t\t} catch (Exception ignore) {\n\t\t\t// 忽略解析错误\n\t\t}\n\n\t\t// 支持类似于 1,234.55 格式的数字\n\t\tfinal Number number = parseNumber(numberStr);\n\t\treturn toBigDecimal(number);\n\t}\n\n\t/**\n\t * 数字转{@link BigInteger}<br>\n\t * null转换为0\n\t *\n\t * @param number 数字\n\t * @return {@link BigInteger}\n\t * @since 5.4.5\n\t */\n\tpublic static BigInteger toBigInteger(Number number) {\n\t\tif (null == number) {\n\t\t\treturn BigInteger.ZERO;\n\t\t}\n\n\t\tif (number instanceof BigInteger) {\n\t\t\treturn (BigInteger) number;\n\t\t} else if (number instanceof Long) {\n\t\t\treturn BigInteger.valueOf((Long) number);\n\t\t}\n\n\t\tAssert.isTrue(isValidNumber(number), \"Number is invalid!\");\n\t\treturn toBigInteger(number.longValue());\n\t}\n\n\t/**\n\t * 数字转{@link BigInteger}<br>\n\t * null或\"\"或空白符转换为0\n\t *\n\t * @param number 数字字符串\n\t * @return {@link BigInteger}\n\t * @since 5.4.5\n\t */\n\tpublic static BigInteger toBigInteger(String number) {\n\t\treturn StrUtil.isBlank(number) ? BigInteger.ZERO : new BigInteger(number);\n\t}\n\n\t/**\n\t * 计算等份个数\n\t *\n\t * @param total 总数\n\t * @param part  每份的个数\n\t * @return 分成了几份\n\t * @since 3.0.6\n\t */\n\tpublic static int count(int total, int part) {\n\t\treturn total == 0 ? 0 : (total - 1) / part + 1;\n\t}\n\n\t/**\n\t * 空转0\n\t *\n\t * @param decimal {@link BigDecimal}，可以为{@code null}\n\t * @return {@link BigDecimal}参数为空时返回0的值\n\t * @since 3.0.9\n\t * @deprecated 请使用 {@link #nullToZero(BigDecimal)}\n\t */\n\t@Deprecated\n\tpublic static BigDecimal null2Zero(BigDecimal decimal) {\n\t\treturn decimal == null ? BigDecimal.ZERO : decimal;\n\t}\n\n\t/**\n\t * 如果给定值为0，返回1，否则返回原值\n\t *\n\t * @param value 值\n\t * @return 1或非0值\n\t * @since 3.1.2\n\t */\n\tpublic static int zero2One(int value) {\n\t\treturn 0 == value ? 1 : value;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static int nullToZero(Integer number) {\n\t\treturn number == null ? 0 : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static long nullToZero(Long number) {\n\t\treturn number == null ? 0L : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static double nullToZero(Double number) {\n\t\treturn number == null ? 0.0 : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static float nullToZero(Float number) {\n\t\treturn number == null ? 0.0f : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static short nullToZero(Short number) {\n\t\treturn number == null ? (short) 0 : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static byte nullToZero(Byte number) {\n\t\treturn number == null ? (byte) 0 : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static BigDecimal nullToZero(BigDecimal number) {\n\t\treturn number == null ? BigDecimal.ZERO : number;\n\t}\n\n\t/**\n\t * 如果给定值为{@code null}，返回0，否则返回原值\n\t *\n\t * @param number 值\n\t * @return 0或非0值\n\t */\n\tpublic static BigInteger nullToZero(BigInteger number) {\n\t\treturn number == null ? BigInteger.ZERO : number;\n\t}\n\n\t/**\n\t * 创建{@link BigInteger}，支持16进制、10进制和8进制，如果传入空白串返回null<br>\n\t * from Apache Common Lang\n\t *\n\t * @param str 数字字符串\n\t * @return {@link BigInteger}\n\t * @since 3.2.1\n\t */\n\tpublic static BigInteger newBigInteger(String str) {\n\t\tstr = StrUtil.trimToNull(str);\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\n\t\tint pos = 0; // 数字字符串位置\n\t\tint radix = 10;\n\t\tboolean negate = false; // 负数与否\n\t\tif (str.startsWith(\"-\")) {\n\t\t\tnegate = true;\n\t\t\tpos = 1;\n\t\t}\n\t\tif (str.startsWith(\"0x\", pos) || str.startsWith(\"0X\", pos)) {\n\t\t\t// hex\n\t\t\tradix = 16;\n\t\t\tpos += 2;\n\t\t} else if (str.startsWith(\"#\", pos)) {\n\t\t\t// alternative hex (allowed by Long/Integer)\n\t\t\tradix = 16;\n\t\t\tpos++;\n\t\t} else if (str.startsWith(\"0\", pos) && str.length() > pos + 1) {\n\t\t\t// octal; so long as there are additional digits\n\t\t\tradix = 8;\n\t\t\tpos++;\n\t\t} // default is to treat as decimal\n\n\t\tif (pos > 0) {\n\t\t\tstr = str.substring(pos);\n\t\t}\n\t\tfinal BigInteger value = new BigInteger(str, radix);\n\t\treturn negate ? value.negate() : value;\n\t}\n\n\t/**\n\t * 判断两个数字是否相邻，例如1和2相邻，1和3不相邻<br>\n\t * 判断方法为做差取绝对值判断是否为1\n\t *\n\t * @param number1 数字1\n\t * @param number2 数字2\n\t * @return 是否相邻\n\t * @since 4.0.7\n\t */\n\tpublic static boolean isBeside(long number1, long number2) {\n\t\treturn Math.abs(number1 - number2) == 1;\n\t}\n\n\t/**\n\t * 判断两个数字是否相邻，例如1和2相邻，1和3不相邻<br>\n\t * 判断方法为做差取绝对值判断是否为1\n\t *\n\t * @param number1 数字1\n\t * @param number2 数字2\n\t * @return 是否相邻\n\t * @since 4.0.7\n\t */\n\tpublic static boolean isBeside(int number1, int number2) {\n\t\treturn Math.abs(number1 - number2) == 1;\n\t}\n\n\t/**\n\t * 把给定的总数平均分成N份，返回每份的个数<br>\n\t * 当除以分数有余数时每份+1\n\t *\n\t * @param total     总数\n\t * @param partCount 份数\n\t * @return 每份的个数\n\t * @since 4.0.7\n\t */\n\tpublic static int partValue(int total, int partCount) {\n\t\treturn partValue(total, partCount, true);\n\t}\n\n\t/**\n\t * 把给定的总数平均分成N份，返回每份的个数<br>\n\t * 如果isPlusOneWhenHasRem为true，则当除以分数有余数时每份+1，否则丢弃余数部分\n\t *\n\t * @param total               总数\n\t * @param partCount           份数\n\t * @param isPlusOneWhenHasRem 在有余数时是否每份+1\n\t * @return 每份的个数\n\t * @since 4.0.7\n\t */\n\tpublic static int partValue(int total, int partCount, boolean isPlusOneWhenHasRem) {\n\t\tint partValue = total / partCount;\n\t\tif (isPlusOneWhenHasRem && total % partCount > 0) {\n\t\t\tpartValue++;\n\t\t}\n\t\treturn partValue;\n\t}\n\n\t/**\n\t * 提供精确的幂运算\n\t *\n\t * @param number 底数\n\t * @param n      指数\n\t * @return 幂的积\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal pow(Number number, int n) {\n\t\treturn pow(toBigDecimal(number), n);\n\t}\n\n\t/**\n\t * 提供精确的幂运算<br>\n\t * 如果n为负数，则返回1/a的-n次方，默认四舍五入，保留两位小数\n\t *\n\t * @param number 底数\n\t * @param n      指数，如果为负数，则返回1/a的-n次方\n\t * @return 幂的积\n\t * @since 4.1.0\n\t */\n\tpublic static BigDecimal pow(BigDecimal number, int n) {\n\t\treturn pow(number, n, 2, RoundingMode.HALF_UP);\n\t}\n\n\n\t/**\n\t * 提供精确的幂运算<br>\n\t * 如果n为负数，则返回1/a的-n次方，默认四舍五入\n\t *\n\t * @param number       底数\n\t * @param n            指数，如果为负数，则返回1/a的-n次方\n\t * @param scale        保留的小数位 (指数为负数时生效)\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode} (指数为负数时生效)\n\t * @return 幂的积\n\t * @since 5.8.41\n\t */\n\tpublic static BigDecimal pow(BigDecimal number, int n, int scale, RoundingMode roundingMode) {\n\t\tif (n < 0) {\n\t\t\t// a的n次方，如果n为负数，则返回1/a的-n次方\n\t\t\treturn BigDecimal.ONE.divide(pow(number, -n), scale, roundingMode);\n\t\t}\n\t\treturn number.pow(n);\n\t}\n\n\n\t/**\n\t * 判断一个整数是否是2的幂\n\t *\n\t * @param n 待验证的整数\n\t * @return 如果n是2的幂返回true, 反之返回false\n\t */\n\tpublic static boolean isPowerOfTwo(long n) {\n\t\treturn (n > 0) && ((n & (n - 1)) == 0);\n\t}\n\n\t/**\n\t * 解析转换数字字符串为int型数字，规则如下：\n\t *\n\t * <pre>\n\t * 1、0x开头的视为16进制数字\n\t * 2、0开头的忽略开头的0\n\t * 3、其它情况按照10进制转换\n\t * 4、空串返回0\n\t * 5、.123形式返回0（按照小于0的小数对待）\n\t * 6、123.56截取小数点之前的数字，忽略小数部分\n\t * </pre>\n\t *\n\t * @param number 数字，支持0x开头、0开头和普通十进制\n\t * @return int\n\t * @throws NumberFormatException 数字格式异常\n\t * @since 4.1.4\n\t */\n\tpublic static int parseInt(String number) throws NumberFormatException {\n\t\tif (StrUtil.isBlank(number)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (StrUtil.startWithIgnoreCase(number, \"0x\")) {\n\t\t\t// 0x04表示16进制数\n\t\t\treturn Integer.parseInt(number.substring(2), 16);\n\t\t}\n\n\t\tif (StrUtil.containsIgnoreCase(number, \"E\")) {\n\t\t\t// 科学计数法忽略支持，科学计数法一般用于表示非常小和非常大的数字，这类数字转换为int后精度丢失，没有意义。\n\t\t\tthrow new NumberFormatException(StrUtil.format(\"Unsupported int format: [{}]\", number));\n\t\t}\n\n\t\ttry {\n\t\t\treturn Integer.parseInt(number);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn parseNumber(number).intValue();\n\t\t}\n\t}\n\n\t/**\n\t * 解析转换数字字符串为long型数字，规则如下：\n\t *\n\t * <pre>\n\t * 1、0x开头的视为16进制数字\n\t * 2、0开头的忽略开头的0\n\t * 3、空串返回0\n\t * 4、其它情况按照10进制转换\n\t * 5、.123形式返回0（按照小于0的小数对待）\n\t * 6、123.56截取小数点之前的数字，忽略小数部分\n\t * </pre>\n\t *\n\t * @param number 数字，支持0x开头、0开头和普通十进制\n\t * @return long\n\t * @since 4.1.4\n\t */\n\tpublic static long parseLong(String number) {\n\t\tif (StrUtil.isBlank(number)) {\n\t\t\treturn 0L;\n\t\t}\n\n\t\tif (number.startsWith(\"0x\")) {\n\t\t\t// 0x04表示16进制数\n\t\t\treturn Long.parseLong(number.substring(2), 16);\n\t\t}\n\n\t\ttry {\n\t\t\treturn Long.parseLong(number);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn parseNumber(number).longValue();\n\t\t}\n\t}\n\n\t/**\n\t * 解析转换数字字符串为long型数字，规则如下：\n\t *\n\t * <pre>\n\t * 1、0开头的忽略开头的0\n\t * 2、空串返回0\n\t * 3、其它情况按照10进制转换\n\t * 4、.123形式返回0.123（按照小于0的小数对待）\n\t * </pre>\n\t *\n\t * @param number 数字，支持0x开头、0开头和普通十进制\n\t * @return long\n\t * @since 5.5.5\n\t */\n\tpublic static float parseFloat(String number) {\n\t\tif (StrUtil.isBlank(number)) {\n\t\t\treturn 0f;\n\t\t}\n\n\t\ttry {\n\t\t\treturn Float.parseFloat(number);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn parseNumber(number).floatValue();\n\t\t}\n\t}\n\n\t/**\n\t * 解析转换数字字符串为long型数字，规则如下：\n\t *\n\t * <pre>\n\t * 1、0开头的忽略开头的0\n\t * 2、空串返回0\n\t * 3、其它情况按照10进制转换\n\t * 4、.123形式返回0.123（按照小于0的小数对待）\n\t * </pre>\n\t *\n\t * @param number 数字，支持0x开头、0开头和普通十进制\n\t * @return long\n\t * @since 5.5.5\n\t */\n\tpublic static double parseDouble(String number) {\n\t\tif (StrUtil.isBlank(number)) {\n\t\t\treturn 0D;\n\t\t}\n\n\t\ttry {\n\t\t\treturn Double.parseDouble(number);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn parseNumber(number).doubleValue();\n\t\t}\n\t}\n\n\t/**\n\t * 将指定字符串转换为{@link Number} 对象<br>\n\t * 此方法不支持科学计数法\n\t *\n\t * <p>\n\t * 需要注意的是，在不同Locale下，数字的表示形式也是不同的，例如：<br>\n\t * 德国、荷兰、比利时、丹麦、意大利、罗马尼亚和欧洲大多地区使用`,`区分小数<br>\n\t * 也就是说，在这些国家地区，1.20表示120，而非1.2。\n\t * </p>\n\t *\n\t * @param numberStr Number字符串\n\t * @return Number对象\n\t * @throws NumberFormatException 包装了{@link ParseException}，当给定的数字字符串无法解析时抛出\n\t * @since 4.1.15\n\t */\n\tpublic static Number parseNumber(String numberStr) throws NumberFormatException {\n\t\tif (StrUtil.startWithIgnoreCase(numberStr, \"0x\")) {\n\t\t\t// 0x04表示16进制数\n\t\t\treturn Long.parseLong(numberStr.substring(2), 16);\n\t\t} else if (StrUtil.startWith(numberStr, '+')) {\n\t\t\t// issue#I79VS7\n\t\t\tnumberStr = StrUtil.subSuf(numberStr, 1);\n\t\t}\n\n\t\t// issue@4197@Github 转为半角\n\t\tnumberStr = Convert.toDBC(numberStr);\n\n\t\t// issue#IDJ1NS@Gitee 处理科学计数法E+格式\n\t\t// NumberFormat对E+格式支持不佳,使用BigDecimal直接解析\n\t\tif (StrUtil.containsIgnoreCase(numberStr, \"e\")) {\n\t\t\ttry {\n\t\t\t\treturn new BigDecimal(numberStr);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t// BigDecimal解析失败,继续使用NumberFormat尝试\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tfinal NumberFormat format = NumberFormat.getInstance();\n\t\t\tif (format instanceof DecimalFormat) {\n\t\t\t\t// issue#1818@Github\n\t\t\t\t// 当字符串数字超出double的长度时，会导致截断，此处使用BigDecimal接收\n\t\t\t\t((DecimalFormat) format).setParseBigDecimal(true);\n\t\t\t}\n\t\t\treturn format.parse(numberStr);\n\t\t} catch (ParseException e) {\n\t\t\tfinal NumberFormatException nfe = new NumberFormatException(e.getMessage());\n\t\t\tnfe.initCause(e);\n\t\t\tthrow nfe;\n\t\t}\n\t}\n\n\t/**\n\t * 解析转换数字字符串为 {@link java.lang.Integer } 规则如下：\n\t *\n\t * <pre>\n\t * 1、0x开头的视为16进制数字\n\t * 2、0开头的忽略开头的0\n\t * 3、其它情况按照10进制转换\n\t * 4、空串返回0\n\t * 5、.123形式返回0（按照小于0的小数对待）\n\t * 6、123.56截取小数点之前的数字，忽略小数部分\n\t * 7、解析失败返回默认值\n\t * </pre>\n\t *\n\t * @param numberStr    数字字符串，支持0x开头、0开头和普通十进制\n\t * @param defaultValue 如果解析失败, 将返回defaultValue, 允许null\n\t * @return Integer\n\t */\n\tpublic static Integer parseInt(String numberStr, Integer defaultValue) {\n\t\tif (CharSequenceUtil.isBlank(numberStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn parseInt(numberStr);\n\t\t} catch (NumberFormatException ignore) {\n\n\t\t}\n\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 解析转换数字字符串为 {@link java.lang.Long } 规则如下：\n\t *\n\t * <pre>\n\t * 1、0x开头的视为16进制数字\n\t * 2、0开头的忽略开头的0\n\t * 3、其它情况按照10进制转换\n\t * 4、空串返回0\n\t * 5、.123形式返回0（按照小于0的小数对待）\n\t * 6、123.56截取小数点之前的数字，忽略小数部分\n\t * 7、解析失败返回默认值\n\t * </pre>\n\t *\n\t * @param numberStr    数字字符串，支持0x开头、0开头和普通十进制\n\t * @param defaultValue 如果解析失败, 将返回defaultValue, 允许null\n\t * @return Long\n\t */\n\tpublic static Long parseLong(String numberStr, Long defaultValue) {\n\t\tif (CharSequenceUtil.isBlank(numberStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn parseLong(numberStr);\n\t\t} catch (NumberFormatException ignore) {\n\n\t\t}\n\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 解析转换数字字符串为 {@link java.lang.Float } 规则如下：\n\t *\n\t * <pre>\n\t * 1、0开头的忽略开头的0\n\t * 2、空串返回0\n\t * 3、其它情况按照10进制转换\n\t * 4、.123形式返回0.123（按照小于0的小数对待）\n\t * </pre>\n\t *\n\t * @param numberStr    数字字符串，支持0x开头、0开头和普通十进制\n\t * @param defaultValue 如果解析失败, 将返回defaultValue, 允许null\n\t * @return Float\n\t */\n\tpublic static Float parseFloat(String numberStr, Float defaultValue) {\n\t\tif (CharSequenceUtil.isBlank(numberStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn parseFloat(numberStr);\n\t\t} catch (NumberFormatException ignore) {\n\n\t\t}\n\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 解析转换数字字符串为 {@link java.lang.Double } 规则如下：\n\t *\n\t * <pre>\n\t * 1、0开头的忽略开头的0\n\t * 2、空串返回0\n\t * 3、其它情况按照10进制转换\n\t * 4、.123形式返回0.123（按照小于0的小数对待）\n\t * </pre>\n\t *\n\t * @param numberStr    数字字符串，支持0x开头、0开头和普通十进制\n\t * @param defaultValue 如果解析失败, 将返回defaultValue, 允许null\n\t * @return Double\n\t */\n\tpublic static Double parseDouble(String numberStr, Double defaultValue) {\n\t\tif (CharSequenceUtil.isBlank(numberStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn parseDouble(numberStr);\n\t\t} catch (NumberFormatException ignore) {\n\n\t\t}\n\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 将指定字符串转换为{@link Number }\n\t * 此方法不支持科学计数法\n\t *\n\t * @param numberStr    Number字符串\n\t * @param defaultValue 如果解析失败, 将返回defaultValue, 允许null\n\t * @return Number对象\n\t */\n\tpublic static Number parseNumber(String numberStr, Number defaultValue) {\n\t\tif (CharSequenceUtil.isBlank(numberStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn parseNumber(numberStr);\n\t\t} catch (NumberFormatException ignore) {\n\n\t\t}\n\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * int值转byte数组，使用大端字节序（高位字节在前，低位字节在后）<br>\n\t * 见：<a href=\"http://www.ruanyifeng.com/blog/2016/11/byte-order.html\">http://www.ruanyifeng.com/blog/2016/11/byte-order.html</a>\n\t *\n\t * @param value 值\n\t * @return byte数组\n\t * @since 4.4.5\n\t */\n\tpublic static byte[] toBytes(int value) {\n\t\tfinal byte[] result = new byte[4];\n\n\t\tresult[0] = (byte) (value >> 24);\n\t\tresult[1] = (byte) (value >> 16);\n\t\tresult[2] = (byte) (value >> 8);\n\t\tresult[3] = (byte) (value /* >> 0 */);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * byte数组转int，使用大端字节序（高位字节在前，低位字节在后）<br>\n\t * 见：<a href=\"http://www.ruanyifeng.com/blog/2016/11/byte-order.html\">http://www.ruanyifeng.com/blog/2016/11/byte-order.html</a>\n\t *\n\t * @param bytes byte数组\n\t * @return int\n\t * @since 4.4.5\n\t */\n\tpublic static int toInt(byte[] bytes) {\n\t\treturn (bytes[0] & 0xff) << 24//\n\t\t\t| (bytes[1] & 0xff) << 16//\n\t\t\t| (bytes[2] & 0xff) << 8//\n\t\t\t| (bytes[3] & 0xff);\n\t}\n\n\t/**\n\t * 以无符号字节数组的形式返回传入值。\n\t *\n\t * @param value 需要转换的值\n\t * @return 无符号bytes\n\t * @since 4.5.0\n\t */\n\tpublic static byte[] toUnsignedByteArray(BigInteger value) {\n\t\tbyte[] bytes = value.toByteArray();\n\n\t\tif (bytes[0] == 0) {\n\t\t\tbyte[] tmp = new byte[bytes.length - 1];\n\t\t\tSystem.arraycopy(bytes, 1, tmp, 0, tmp.length);\n\n\t\t\treturn tmp;\n\t\t}\n\n\t\treturn bytes;\n\t}\n\n\t/**\n\t * 以无符号字节数组的形式返回传入值。\n\t *\n\t * @param length bytes长度\n\t * @param value  需要转换的值\n\t * @return 无符号bytes\n\t * @since 4.5.0\n\t */\n\tpublic static byte[] toUnsignedByteArray(int length, BigInteger value) {\n\t\tbyte[] bytes = value.toByteArray();\n\t\tif (bytes.length == length) {\n\t\t\treturn bytes;\n\t\t}\n\n\t\tint start = bytes[0] == 0 ? 1 : 0;\n\t\tint count = bytes.length - start;\n\n\t\tif (count > length) {\n\t\t\tthrow new IllegalArgumentException(\"standard length exceeded for value\");\n\t\t}\n\n\t\tbyte[] tmp = new byte[length];\n\t\tSystem.arraycopy(bytes, start, tmp, tmp.length - count, count);\n\t\treturn tmp;\n\t}\n\n\t/**\n\t * 无符号bytes转{@link BigInteger}\n\t *\n\t * @param buf buf 无符号bytes\n\t * @return {@link BigInteger}\n\t * @since 4.5.0\n\t */\n\tpublic static BigInteger fromUnsignedByteArray(byte[] buf) {\n\t\treturn new BigInteger(1, buf);\n\t}\n\n\t/**\n\t * 无符号bytes转{@link BigInteger}\n\t *\n\t * @param buf    无符号bytes\n\t * @param off    起始位置\n\t * @param length 长度\n\t * @return {@link BigInteger}\n\t */\n\tpublic static BigInteger fromUnsignedByteArray(byte[] buf, int off, int length) {\n\t\tbyte[] mag = buf;\n\t\tif (off != 0 || length != buf.length) {\n\t\t\tmag = new byte[length];\n\t\t\tSystem.arraycopy(buf, off, mag, 0, length);\n\t\t}\n\t\treturn new BigInteger(1, mag);\n\t}\n\n\t/**\n\t * 检查是否为有效的数字<br>\n\t * 检查Double和Float是否为无限大，或者Not a Number<br>\n\t * 非数字类型和Null将返回false\n\t *\n\t * @param number 被检查类型\n\t * @return 检查结果，非数字类型和Null将返回false\n\t * @since 4.6.7\n\t */\n\tpublic static boolean isValidNumber(Number number) {\n\t\tif (null == number) {\n\t\t\treturn false;\n\t\t}\n\t\tif (number instanceof Double) {\n\t\t\treturn (false == ((Double) number).isInfinite()) && (false == ((Double) number).isNaN());\n\t\t} else if (number instanceof Float) {\n\t\t\treturn (false == ((Float) number).isInfinite()) && (false == ((Float) number).isNaN());\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查是否为有效的数字<br>\n\t * 检查double否为无限大，或者Not a Number（NaN）<br>\n\t *\n\t * @param number 被检查double\n\t * @return 检查结果\n\t * @since 5.7.0\n\t */\n\tpublic static boolean isValid(double number) {\n\t\treturn false == (Double.isNaN(number) || Double.isInfinite(number));\n\t}\n\n\t/**\n\t * 检查是否为有效的数字<br>\n\t * 检查double否为无限大，或者Not a Number（NaN）<br>\n\t *\n\t * @param number 被检查double\n\t * @return 检查结果\n\t * @since 5.7.0\n\t */\n\tpublic static boolean isValid(float number) {\n\t\treturn false == (Float.isNaN(number) || Float.isInfinite(number));\n\t}\n\n\t/**\n\t * 计算数学表达式的值，只支持加减乘除和取余<br>\n\t * 如：\n\t * <pre class=\"code\">\n\t *   calculate(\"(0*1--3)-5/-4-(3*(-2.13))\") -》 10.64\n\t * </pre>\n\t *\n\t * @param expression 数学表达式\n\t * @return 结果\n\t * @since 5.7.6\n\t */\n\tpublic static double calculate(String expression) {\n\t\treturn Calculator.conversion(expression);\n\t}\n\n\t/**\n\t * Number值转换为double<br>\n\t * float强制转换存在精度问题，此方法避免精度丢失\n\t *\n\t * @param value 被转换的float值\n\t * @return double值\n\t * @since 5.7.8\n\t */\n\tpublic static double toDouble(Number value) {\n\t\tif (value instanceof Float) {\n\t\t\treturn Double.parseDouble(value.toString());\n\t\t} else {\n\t\t\treturn value.doubleValue();\n\t\t}\n\t}\n\n\t/**\n\t * 检查是否为奇数<br>\n\t *\n\t * @param num 被判断的数值\n\t * @return 是否是奇数\n\t * @author GuoZG\n\t * @since 5.7.17\n\t */\n\tpublic static boolean isOdd(int num) {\n\t\treturn (num & 1) == 1;\n\t}\n\n\t/**\n\t * 检查是否为偶数<br>\n\t *\n\t * @param num 被判断的数值\n\t * @return 是否是偶数\n\t * @author GuoZG\n\t * @since 5.7.17\n\t */\n\tpublic static boolean isEven(int num) {\n\t\treturn false == isOdd(num);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- Private method start\n\tprivate static int mathSubNode(int selectNum, int minNum) {\n\t\tif (selectNum == minNum) {\n\t\t\treturn 1;\n\t\t} else {\n\t\t\treturn selectNum * mathSubNode(selectNum - 1, minNum);\n\t\t}\n\t}\n\n\tprivate static int mathNode(int selectNum) {\n\t\tif (selectNum == 0) {\n\t\t\treturn 1;\n\t\t} else {\n\t\t\treturn selectNum * mathNode(selectNum - 1);\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ObjUtil.java",
    "content": "package cn.hutool.core.util;\n\n/**\n * 对象工具类，同{@link ObjectUtil}<br>\n * 从6.x开始，将删除ObjectUtil，而使用ObjUtil\n *\n */\npublic class ObjUtil extends ObjectUtil{\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.comparator.CompareUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.lang.reflect.Array;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * 对象工具类，包括判空、克隆、序列化等操作\n *\n * @author Looly\n */\npublic class ObjectUtil {\n\n\t/**\n\t * 比较两个对象是否相等，此方法是 {@link #equal(Object, Object)}的别名方法。<br>\n\t * 相同的条件有两个，满足其一即可：<br>\n\t * <ol>\n\t * <li>obj1 == null &amp;&amp; obj2 == null</li>\n\t * <li>obj1.equals(obj2)</li>\n\t * <li>如果是BigDecimal比较，0 == obj1.compareTo(obj2)</li>\n\t * </ol>\n\t *\n\t * @param obj1 对象1\n\t * @param obj2 对象2\n\t * @return 是否相等\n\t * @see #equal(Object, Object)\n\t * @since 5.4.3\n\t */\n\tpublic static boolean equals(Object obj1, Object obj2) {\n\t\treturn equal(obj1, obj2);\n\t}\n\n\t/**\n\t * 比较两个对象是否相等。<br>\n\t * 相同的条件有两个，满足其一即可：<br>\n\t * <ol>\n\t * <li>obj1 == null &amp;&amp; obj2 == null</li>\n\t * <li>obj1.equals(obj2)</li>\n\t * <li>如果是BigDecimal比较，0 == obj1.compareTo(obj2)</li>\n\t * </ol>\n\t *\n\t * @param obj1 对象1\n\t * @param obj2 对象2\n\t * @return 是否相等\n\t * @see Objects#equals(Object, Object)\n\t */\n\tpublic static boolean equal(Object obj1, Object obj2) {\n\t\tif (obj1 instanceof Number && obj2 instanceof Number) {\n\t\t\treturn NumberUtil.equals((Number) obj1, (Number) obj2);\n\t\t}\n\t\treturn Objects.equals(obj1, obj2);\n\t}\n\n\t/**\n\t * 比较两个对象是否不相等。<br>\n\t *\n\t * @param obj1 对象1\n\t * @param obj2 对象2\n\t * @return 是否不等\n\t * @since 3.0.7\n\t */\n\tpublic static boolean notEqual(Object obj1, Object obj2) {\n\t\treturn false == equal(obj1, obj2);\n\t}\n\n\t/**\n\t * <p>计算对象长度，支持类型包括：\n\t * <ul>\n\t *     <li>{@code null}：默认返回{@code 0}；</li>\n\t *     <li>数组：返回数组长度；</li>\n\t *     <li>{@link CharSequence}：返回{@link CharSequence#length()}；</li>\n\t *     <li>{@link Collection}：返回{@link Collection#size()}；</li>\n\t *     <li>{@link Iterator}或{@link Iterable}：可迭代的元素数量；副作用：{@link Iterator}只能被迭代一次</li>\n\t *     <li>{@link Enumeration}：返回可迭代的元素数量；副作用：{@link Enumeration}只能被迭代一次</li>\n\t * </ul>\n\t *\n\t * @param obj 被计算长度的对象\n\t * @return 长度\n\t */\n\tpublic static int length(Object obj) {\n\t\tif (obj == null) {\n\t\t\treturn 0;\n\t\t}\n\t\tif (obj instanceof CharSequence) {\n\t\t\treturn ((CharSequence) obj).length();\n\t\t}\n\t\tif (obj instanceof Collection) {\n\t\t\treturn ((Collection<?>) obj).size();\n\t\t}\n\t\tif (obj instanceof Map) {\n\t\t\treturn ((Map<?, ?>) obj).size();\n\t\t}\n\n\t\tint count;\n\t\tif (obj instanceof Iterator) {\n\t\t\tfinal Iterator<?> iter = (Iterator<?>) obj;\n\t\t\tcount = 0;\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tcount++;\n\t\t\t\titer.next();\n\t\t\t}\n\t\t\treturn count;\n\t\t}\n\t\tif (obj instanceof Enumeration) {\n\t\t\tfinal Enumeration<?> enumeration = (Enumeration<?>) obj;\n\t\t\tcount = 0;\n\t\t\twhile (enumeration.hasMoreElements()) {\n\t\t\t\tcount++;\n\t\t\t\tenumeration.nextElement();\n\t\t\t}\n\t\t\treturn count;\n\t\t}\n\t\tif (obj.getClass().isArray() == true) {\n\t\t\treturn Array.getLength(obj);\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 对象中是否包含元素<br>\n\t * 支持的对象类型包括：\n\t * <ul>\n\t * <li>CharSequence</li>\n\t * <li>Collection</li>\n\t * <li>Map</li>\n\t * <li>Iterator</li>\n\t * <li>Enumeration</li>\n\t * <li>Array</li>\n\t * </ul>\n\t *\n\t * @param obj     对象\n\t * @param element 元素\n\t * @return 是否包含\n\t */\n\tpublic static boolean contains(Object obj, Object element) {\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (obj instanceof CharSequence) {\n\t\t\tif (!(element instanceof CharSequence)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tString elementStr;\n\t\t\ttry {\n\t\t\t\telementStr = element.toString();\n\t\t\t\t// 检查 toString() 返回 null 的情况\n\t\t\t\tif (elementStr == null) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 如果toString抛异常，认为不包含\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn obj.toString().contains(elementStr);\n\t\t}\n\t\tif (obj instanceof Collection) {\n\t\t\treturn ((Collection<?>) obj).contains(element);\n\t\t}\n\t\tif (obj instanceof Map) {\n\t\t\treturn ((Map<?, ?>) obj).containsValue(element);\n\t\t}\n\n\t\tif (obj instanceof Iterator) {\n\t\t\tfinal Iterator<?> iter = (Iterator<?>) obj;\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tfinal Object o = iter.next();\n\t\t\t\tif (equal(o, element)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (obj instanceof Enumeration) {\n\t\t\tfinal Enumeration<?> enumeration = (Enumeration<?>) obj;\n\t\t\twhile (enumeration.hasMoreElements()) {\n\t\t\t\tfinal Object o = enumeration.nextElement();\n\t\t\t\tif (equal(o, element)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (obj.getClass().isArray() == true) {\n\t\t\tfinal int len = Array.getLength(obj);\n\t\t\tfor (int i = 0; i < len; i++) {\n\t\t\t\tfinal Object o = Array.get(obj, i);\n\t\t\t\tif (equal(o, element)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 检查对象是否为null<br>\n\t * 判断标准为：\n\t *\n\t * <pre>\n\t * 1. == null\n\t * 2. equals(null)\n\t * </pre>\n\t *\n\t * @param obj 对象\n\t * @return 是否为null\n\t */\n\tpublic static boolean isNull(Object obj) {\n\t\t//noinspection ConstantConditions\n\t\treturn null == obj || obj.equals(null);\n\t}\n\n\t/**\n\t * 检查对象是否不为null\n\t * <pre>\n\t * 1. != null\n\t * 2. not equals(null)\n\t * </pre>\n\t *\n\t * @param obj 对象\n\t * @return 是否为非null\n\t */\n\tpublic static boolean isNotNull(Object obj) {\n\t\t//noinspection ConstantConditions\n\t\treturn null != obj && false == obj.equals(null);\n\t}\n\n\t/**\n\t * 判断指定对象是否为空，支持：\n\t *\n\t * <pre>\n\t * 1. CharSequence\n\t * 2. Map\n\t * 3. Iterable\n\t * 4. Iterator\n\t * 5. Array\n\t * </pre>\n\t *\n\t * @param obj 被判断的对象\n\t * @return 是否为空，如果类型不支持，返回false\n\t * @since 4.5.7\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static boolean isEmpty(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (obj instanceof CharSequence) {\n\t\t\treturn StrUtil.isEmpty((CharSequence) obj);\n\t\t} else if (obj instanceof Map) {\n\t\t\treturn MapUtil.isEmpty((Map) obj);\n\t\t} else if (obj instanceof Iterable) {\n\t\t\treturn IterUtil.isEmpty((Iterable) obj);\n\t\t} else if (obj instanceof Iterator) {\n\t\t\treturn IterUtil.isEmpty((Iterator) obj);\n\t\t} else if (ArrayUtil.isArray(obj)) {\n\t\t\treturn ArrayUtil.isEmpty(obj);\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断指定对象是否为非空，支持：\n\t *\n\t * <pre>\n\t * 1. CharSequence\n\t * 2. Map\n\t * 3. Iterable\n\t * 4. Iterator\n\t * 5. Array\n\t * </pre>\n\t *\n\t * @param obj 被判断的对象\n\t * @return 是否为空，如果类型不支持，返回true\n\t * @since 4.5.7\n\t */\n\tpublic static boolean isNotEmpty(Object obj) {\n\t\treturn false == isEmpty(obj);\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null}返回默认值\n\t *\n\t * <pre>\n\t * ObjectUtil.defaultIfNull(null, null)      = null\n\t * ObjectUtil.defaultIfNull(null, \"\")        = \"\"\n\t * ObjectUtil.defaultIfNull(null, \"zz\")      = \"zz\"\n\t * ObjectUtil.defaultIfNull(\"abc\", *)        = \"abc\"\n\t * ObjectUtil.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE\n\t * </pre>\n\t *\n\t * @param <T>          对象类型\n\t * @param object       被检查对象，可能为{@code null}\n\t * @param defaultValue 被检查对象为{@code null}返回的默认值，可以为{@code null}\n\t * @return 被检查对象为{@code null}返回默认值，否则返回原值\n\t * @since 3.0.7\n\t */\n\tpublic static <T> T defaultIfNull(final T object, final T defaultValue) {\n\t\treturn isNull(object) ? defaultValue : object;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null}， 返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param source               被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T> T defaultIfNull(T source, Supplier<? extends T> defaultValueSupplier) {\n\t\tif (isNull(source)) {\n\t\t\treturn defaultValueSupplier.get();\n\t\t}\n\t\treturn source;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null}， 返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param source               被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T> T defaultIfNull(T source, Function<T, ? extends T> defaultValueSupplier) {\n\t\tif (isNull(source)) {\n\t\t\treturn defaultValueSupplier.apply(null);\n\t\t}\n\t\treturn source;\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null} 返回默认值, 如果不为null 返回自定义handle处理后的返回值\n\t *\n\t * @param source       Object 类型对象\n\t * @param handle       非空时自定义的处理方法\n\t * @param defaultValue 默认为空的返回值\n\t * @param <T>          被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @return 处理后的返回值\n\t * @since 5.4.6\n\t * @deprecated 当str为{@code null}时，handle使用了str相关的方法引用会导致空指针问题\n\t */\n\t@Deprecated\n\tpublic static <T> T defaultIfNull(Object source, Supplier<? extends T> handle, final T defaultValue) {\n\t\tif (isNotNull(source)) {\n\t\t\treturn handle.get();\n\t\t}\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null} 返回默认值, 如果不为null 返回自定义handle处理后的返回值\n\t *\n\t * @param <T>          被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @param <R>          被检查的对象类型\n\t * @param source       Object 类型对象\n\t * @param handle       非空时自定义的处理方法\n\t * @param defaultValue 默认为空的返回值\n\t * @return 处理后的返回值\n\t * @since 5.4.6\n\t */\n\tpublic static <T, R> T defaultIfNull(R source, Function<R, ? extends T> handle, final T defaultValue) {\n\t\tif (isNotNull(source)) {\n\t\t\treturn handle.apply(source);\n\t\t}\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 如果指定的对象不为 {@code null},则应用提供的映射函数并返回结果,否则返回 {@code null}。\n\t *\n\t * @param source  要检查的对象\n\t * @param handler 要应用的映射函数\n\t * @param <T>     输入对象的类型\n\t * @param <R>     映射函数的返回类型\n\t * @return 映射函数的结果, 如果输入对象为 null,则返回 null\n\t * @since 5.8.41\n\t */\n\tpublic static <T, R> R apply(final T source, final Function<T, R> handler) {\n\t\treturn defaultIfNull(source, handler, null);\n\t}\n\n\t/**\n\t * 如果指定的对象不为 {@code null},则执行{@link Consumer}处理source，否则不进行操作\n\t *\n\t * @param source   要检查的对象\n\t * @param consumer source处理逻辑\n\t * @param <T>      输入对象的类型\n\t */\n\tpublic static <T> void accept(final T source, final Consumer<T> consumer) {\n\t\tif (null != source) {\n\t\t\tconsumer.accept(source);\n\t\t}\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null}或者\"\"返回默认值, 否则返回自定义handle处理后的返回值\n\t *\n\t * @param str          String 类型\n\t * @param handle       自定义的处理方法\n\t * @param defaultValue 默认为空的返回值\n\t * @param <T>          被检查对象为{@code null}或者 \"\"返回默认值，否则返回自定义handle处理后的返回值\n\t * @return 处理后的返回值\n\t * @since 5.4.6\n\t * @deprecated 当str为{@code null}时，handle使用了str相关的方法引用会导致空指针问题\n\t */\n\t@Deprecated\n\tpublic static <T> T defaultIfEmpty(String str, Supplier<? extends T> handle, final T defaultValue) {\n\t\tif (StrUtil.isNotEmpty(str)) {\n\t\t\treturn handle.get();\n\t\t}\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null}或者\"\"返回默认值, 否则返回自定义handle处理后的返回值\n\t *\n\t * @param str          String 类型\n\t * @param handle       自定义的处理方法\n\t * @param defaultValue 默认为空的返回值\n\t * @param <T>          被检查对象为{@code null}或者 \"\"返回默认值，否则返回自定义handle处理后的返回值\n\t * @return 处理后的返回值\n\t * @since 5.4.6\n\t */\n\tpublic static <T> T defaultIfEmpty(String str, Function<CharSequence, ? extends T> handle, final T defaultValue) {\n\t\tif (StrUtil.isNotEmpty(str)) {\n\t\t\treturn handle.apply(str);\n\t\t}\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null}或者 \"\" 返回默认值\n\t *\n\t * <pre>\n\t * ObjectUtil.defaultIfEmpty(null, null)      = null\n\t * ObjectUtil.defaultIfEmpty(null, \"\")        = \"\"\n\t * ObjectUtil.defaultIfEmpty(\"\", \"zz\")      = \"zz\"\n\t * ObjectUtil.defaultIfEmpty(\" \", \"zz\")      = \" \"\n\t * ObjectUtil.defaultIfEmpty(\"abc\", *)        = \"abc\"\n\t * </pre>\n\t *\n\t * @param <T>          对象类型（必须实现CharSequence接口）\n\t * @param str          被检查对象，可能为{@code null}\n\t * @param defaultValue 被检查对象为{@code null}或者 \"\"返回的默认值，可以为{@code null}或者 \"\"\n\t * @return 被检查对象为{@code null}或者 \"\"返回默认值，否则返回原值\n\t * @since 5.0.4\n\t */\n\tpublic static <T extends CharSequence> T defaultIfEmpty(final T str, final T defaultValue) {\n\t\treturn StrUtil.isEmpty(str) ? defaultValue : str;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null} 或 \"\" 时，返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param str                  被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型（必须实现CharSequence接口）\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T extends CharSequence> T defaultIfEmpty(T str, Supplier<? extends T> defaultValueSupplier) {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\treturn defaultValueSupplier.get();\n\t\t}\n\t\treturn str;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null} 或 \"\" 时，返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param str                  被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型（必须实现CharSequence接口）\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T extends CharSequence> T defaultIfEmpty(T str, Function<T, ? extends T> defaultValueSupplier) {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\treturn defaultValueSupplier.apply(null);\n\t\t}\n\t\treturn str;\n\t}\n\n\t/**\n\t * 如果给定对象为{@code null}或者\"\"或者空白符返回默认值\n\t *\n\t * <pre>\n\t * ObjectUtil.defaultIfBlank(null, null)      = null\n\t * ObjectUtil.defaultIfBlank(null, \"\")        = \"\"\n\t * ObjectUtil.defaultIfBlank(\"\", \"zz\")      = \"zz\"\n\t * ObjectUtil.defaultIfBlank(\" \", \"zz\")      = \"zz\"\n\t * ObjectUtil.defaultIfBlank(\"abc\", *)        = \"abc\"\n\t * </pre>\n\t *\n\t * @param <T>          对象类型（必须实现CharSequence接口）\n\t * @param str          被检查对象，可能为{@code null}\n\t * @param defaultValue 被检查对象为{@code null}或者 \"\"或者空白符返回的默认值，可以为{@code null}或者 \"\"或者空白符\n\t * @return 被检查对象为{@code null}或者 \"\"或者空白符返回默认值，否则返回原值\n\t * @since 5.0.4\n\t */\n\tpublic static <T extends CharSequence> T defaultIfBlank(final T str, final T defaultValue) {\n\t\treturn StrUtil.isBlank(str) ? defaultValue : str;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null} 或 \"\" 或 空白字符串时，返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param str                  被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型（必须实现CharSequence接口）\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T extends CharSequence> T defaultIfBlank(T str, Supplier<? extends T> defaultValueSupplier) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn defaultValueSupplier.get();\n\t\t}\n\t\treturn str;\n\t}\n\n\t/**\n\t * 如果被检查对象为 {@code null} 或 \"\" 或 空白字符串时，返回默认值（由 defaultValueSupplier 提供）；否则直接返回\n\t *\n\t * @param str                  被检查对象\n\t * @param defaultValueSupplier 默认值提供者\n\t * @param <T>                  对象类型（必须实现CharSequence接口）\n\t * @return 被检查对象为{@code null}返回默认值，否则返回自定义handle处理后的返回值\n\t * @throws NullPointerException {@code defaultValueSupplier == null} 时，抛出\n\t * @since 5.7.20\n\t */\n\tpublic static <T extends CharSequence> T defaultIfBlank(T str, Function<T, ? extends T> defaultValueSupplier) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn defaultValueSupplier.apply(null);\n\t\t}\n\t\treturn str;\n\t}\n\n\t/**\n\t * 克隆对象<br>\n\t * 如果对象实现Cloneable接口，调用其clone方法<br>\n\t * 如果实现Serializable接口，执行深度克隆<br>\n\t * 否则返回{@code null}\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 被克隆对象\n\t * @return 克隆后的对象\n\t */\n\tpublic static <T> T clone(T obj) {\n\t\tT result = ArrayUtil.clone(obj);\n\t\tif (null == result) {\n\t\t\tif (obj instanceof Cloneable) {\n\t\t\t\tresult = ReflectUtil.invoke(obj, \"clone\");\n\t\t\t} else {\n\t\t\t\tresult = cloneByStream(obj);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 返回克隆后的对象，如果克隆失败，返回原对象\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 对象\n\t * @return 克隆后或原对象\n\t */\n\tpublic static <T> T cloneIfPossible(final T obj) {\n\t\tT clone = null;\n\t\ttry {\n\t\t\tclone = clone(obj);\n\t\t} catch (Exception e) {\n\t\t\t// pass\n\t\t}\n\t\treturn clone == null ? obj : clone;\n\t}\n\n\t/**\n\t * 序列化后拷贝流的方式克隆<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 被克隆对象\n\t * @return 克隆后的对象\n\t * @throws UtilException IO异常和ClassNotFoundException封装\n\t */\n\tpublic static <T> T cloneByStream(T obj) {\n\t\treturn SerializeUtil.clone(obj);\n\t}\n\n\t/**\n\t * 序列化<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 要被序列化的对象\n\t * @return 序列化后的字节码\n\t */\n\tpublic static <T> byte[] serialize(T obj) {\n\t\treturn SerializeUtil.serialize(obj);\n\t}\n\n\t/**\n\t * 反序列化<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * <p>\n\t * 注意！！！ 此方法不会检查反序列化安全，可能存在反序列化漏洞风险！！！\n\t * </p>\n\t *\n\t * @param <T>   对象类型\n\t * @param bytes 反序列化的字节码\n\t * @param acceptClasses 白名单的类\n\t * @return 反序列化后的对象\n\t */\n\tpublic static <T> T deserialize(byte[] bytes, Class<?>... acceptClasses) {\n\t\treturn SerializeUtil.deserialize(bytes, acceptClasses);\n\t}\n\n\t/**\n\t * 是否为基本类型，包括包装类型和非包装类型\n\t *\n\t * @param object 被检查对象，{@code null}返回{@code false}\n\t * @return 是否为基本类型\n\t * @see ClassUtil#isBasicType(Class)\n\t */\n\tpublic static boolean isBasicType(Object object) {\n\t\tif (null == object) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ClassUtil.isBasicType(object.getClass());\n\t}\n\n\t/**\n\t * 检查是否为有效的数字<br>\n\t * 检查Double和Float是否为无限大，或者Not a Number<br>\n\t * 非数字类型和Null将返回true\n\t *\n\t * @param obj 被检查类型\n\t * @return 检查结果，非数字类型和Null将返回true\n\t */\n\tpublic static boolean isValidIfNumber(Object obj) {\n\t\tif (obj instanceof Number) {\n\t\t\treturn NumberUtil.isValidNumber((Number) obj);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * {@code null}安全的对象比较，{@code null}对象排在末尾\n\t *\n\t * @param <T> 被比较对象类型\n\t * @param c1  对象1，可以为{@code null}\n\t * @param c2  对象2，可以为{@code null}\n\t * @return 比较结果，如果c1 &lt; c2，返回数小于0，c1==c2返回0，c1 &gt; c2 大于0\n\t * @see java.util.Comparator#compare(Object, Object)\n\t * @since 3.0.7\n\t */\n\tpublic static <T extends Comparable<? super T>> int compare(T c1, T c2) {\n\t\treturn CompareUtil.compare(c1, c2);\n\t}\n\n\t/**\n\t * {@code null}安全的对象比较\n\t *\n\t * @param <T>         被比较对象类型\n\t * @param c1          对象1，可以为{@code null}\n\t * @param c2          对象2，可以为{@code null}\n\t * @param nullGreater 当被比较对象为null时是否排在前面\n\t * @return 比较结果，如果c1 &lt; c2，返回数小于0，c1==c2返回0，c1 &gt; c2 大于0\n\t * @see java.util.Comparator#compare(Object, Object)\n\t * @since 3.0.7\n\t */\n\tpublic static <T extends Comparable<? super T>> int compare(T c1, T c2, boolean nullGreater) {\n\t\treturn CompareUtil.compare(c1, c2, nullGreater);\n\t}\n\n\t/**\n\t * 获得给定类的第一个泛型参数\n\t *\n\t * @param obj 被检查的对象\n\t * @return {@link Class}\n\t * @since 3.0.8\n\t */\n\tpublic static Class<?> getTypeArgument(Object obj) {\n\t\treturn getTypeArgument(obj, 0);\n\t}\n\n\t/**\n\t * 获得给定类的第一个泛型参数\n\t *\n\t * @param obj   被检查的对象\n\t * @param index 泛型类型的索引号，即第几个泛型类型\n\t * @return {@link Class}\n\t * @since 3.0.8\n\t */\n\tpublic static Class<?> getTypeArgument(Object obj, int index) {\n\t\treturn ClassUtil.getTypeArgument(obj.getClass(), index);\n\t}\n\n\t/**\n\t * 将Object转为String<br>\n\t * 策略为：\n\t * <pre>\n\t *  1、null转为\"null\"\n\t *  2、调用Convert.toStr(Object)转换\n\t * </pre>\n\t *\n\t * @param obj Bean对象\n\t * @return Bean所有字段转为Map后的字符串\n\t * @since 3.2.0\n\t */\n\tpublic static String toString(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn StrUtil.NULL;\n\t\t}\n\t\tif (obj instanceof Map) {\n\t\t\treturn obj.toString();\n\t\t}\n\n\t\treturn Convert.toStr(obj);\n\t}\n\n\t/**\n\t * 存在多少个{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param objs 被检查的对象,一个或者多个\n\t * @return 存在{@code null}的数量\n\t */\n\tpublic static int emptyCount(Object... objs) {\n\t\treturn ArrayUtil.emptyCount(objs);\n\t}\n\n\t/**\n\t * 是否存在{@code null}对象，通过{@link ObjectUtil#isNull(Object)} 判断元素\n\t *\n\t * @param objs 被检查对象\n\t * @return 是否存在\n\t * @see ArrayUtil#hasNull(Object[])\n\t * @since 5.5.3\n\t */\n\tpublic static boolean hasNull(Object... objs) {\n\t\treturn ArrayUtil.hasNull(objs);\n\t}\n\n\t/**\n\t * 是否存在{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param objs 被检查对象\n\t * @return 是否存在\n\t * @see ArrayUtil#hasEmpty(Object...)\n\t */\n\tpublic static boolean hasEmpty(Object... objs) {\n\t\treturn ArrayUtil.hasEmpty(objs);\n\t}\n\n\t/**\n\t * 是否全都为{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param objs 被检查的对象,一个或者多个\n\t * @return 是否都为空\n\t */\n\tpublic static boolean isAllEmpty(Object... objs) {\n\t\treturn ArrayUtil.isAllEmpty(objs);\n\t}\n\n\t/**\n\t * 是否全都不为{@code null}或空对象，通过{@link ObjectUtil#isEmpty(Object)} 判断元素\n\t *\n\t * @param objs 被检查的对象,一个或者多个\n\t * @return 是否都不为空\n\t */\n\tpublic static boolean isAllNotEmpty(Object... objs) {\n\t\treturn ArrayUtil.isAllNotEmpty(objs);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.DefaultSegment;\nimport cn.hutool.core.lang.Segment;\n\n/**\n * 分页工具类\n *\n * @author xiaoleilu\n */\npublic class PageUtil {\n\n\tprivate static int firstPageNo = 0;\n\n\t/**\n\t * 获得首页的页码，可以为0或者1\n\t *\n\t * @return 首页页码\n\t */\n\tpublic static int getFirstPageNo() {\n\t\treturn firstPageNo;\n\t}\n\n\t/**\n\t * 设置首页页码，可以为0或者1\n\t *\n\t * <pre>\n\t *     当设置为0时，页码0表示第一页，开始位置为0\n\t *     当设置为1时，页码1表示第一页，开始位置为0\n\t * </pre>\n\t *\n\t * @param customFirstPageNo 自定义的首页页码，为0或者1\n\t */\n\tsynchronized public static void setFirstPageNo(int customFirstPageNo) {\n\t\tfirstPageNo = customFirstPageNo;\n\t}\n\n\t/**\n\t * 设置首页页码为1\n\t *\n\t * <pre>\n\t *     当设置为1时，页码1表示第一页，开始位置为0\n\t * </pre>\n\t */\n\tpublic static void setOneAsFirstPageNo() {\n\t\tsetFirstPageNo(1);\n\t}\n\n\t/**\n\t * 将页数和每页条目数转换为开始位置<br>\n\t * 此方法用于不包括结束位置的分页方法<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * 页码：0，每页10 =》 0\n\t * 页码：1，每页10 =》 10\n\t * ……\n\t * </pre>\n\t *\n\t * <p>\n\t * 当{@link #setFirstPageNo(int)}设置为1时：\n\t * <pre>\n\t * 页码：1，每页10 =》 0\n\t * 页码：2，每页10 =》 10\n\t * ……\n\t * </pre>\n\t *\n\t * @param pageNo   页码（从0计数）\n\t * @param pageSize 每页条目数\n\t * @return 开始位置\n\t */\n\tpublic static int getStart(int pageNo, int pageSize) {\n\t\tif (pageNo < firstPageNo) {\n\t\t\tpageNo = firstPageNo;\n\t\t}\n\n\t\tif (pageSize < 1) {\n\t\t\tpageSize = 0;\n\t\t}\n\n\t\treturn (pageNo - firstPageNo) * pageSize;\n\t}\n\n\t/**\n\t * 将页数和每页条目数转换为结束位置<br>\n\t * 此方法用于不包括结束位置的分页方法<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * 页码：0，每页10 =》 9\n\t * 页码：1，每页10 =》 19\n\t * ……\n\t * </pre>\n\t *\n\t * <p>\n\t * 当{@link #setFirstPageNo(int)}设置为1时：\n\t * <pre>\n\t * 页码：1，每页10 =》 10\n\t * 页码：2，每页10 =》 20\n\t * ……\n\t * </pre>\n\t *\n\t * @param pageNo   页码（从0计数）\n\t * @param pageSize 每页条目数\n\t * @return 开始位置\n\t * @since 5.2.5\n\t */\n\tpublic static int getEnd(int pageNo, int pageSize) {\n\t\tfinal int start = getStart(pageNo, pageSize);\n\t\treturn getEndByStart(start, pageSize);\n\t}\n\n\t/**\n\t * 将页数和每页条目数转换为开始位置和结束位置<br>\n\t * 此方法用于包括结束位置的分页方法<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * 页码：0，每页10 =》 [0, 10]\n\t * 页码：1，每页10 =》 [10, 20]\n\t * ……\n\t * </pre>\n\t *\n\t * <p>\n\t * 当{@link #setFirstPageNo(int)}设置为1时：\n\t * <pre>\n\t * 页码：1，每页10 =》 [0, 10]\n\t * 页码：2，每页10 =》 [10, 20]\n\t * ……\n\t * </pre>\n\t *\n\t * @param pageNo   页码（从0计数）\n\t * @param pageSize 每页条目数\n\t * @return 第一个数为开始位置，第二个数为结束位置\n\t */\n\tpublic static int[] transToStartEnd(int pageNo, int pageSize) {\n\t\tfinal int start = getStart(pageNo, pageSize);\n\t\treturn new int[]{start, getEndByStart(start, pageSize)};\n\t}\n\n\t/**\n\t * 将页数和每页条目数转换为开始位置和结束位置<br>\n\t * 此方法用于包括结束位置的分页方法<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * 页码：0，每页10 =》 [0, 10]\n\t * 页码：1，每页10 =》 [10, 20]\n\t * ……\n\t * </pre>\n\t *\n\t * <p>\n\t * 当{@link #setFirstPageNo(int)}设置为1时：\n\t * <pre>\n\t * 页码：1，每页10 =》 [0, 10]\n\t * 页码：2，每页10 =》 [10, 20]\n\t * ……\n\t * </pre>\n\t *\n\t * @param pageNo   页码（从0计数）\n\t * @param pageSize 每页条目数\n\t * @return {@link Segment}\n\t * @since 5.5.3\n\t */\n\tpublic static Segment<Integer> toSegment(int pageNo, int pageSize) {\n\t\tfinal int[] startEnd = transToStartEnd(pageNo, pageSize);\n\t\treturn new DefaultSegment<>(startEnd[0], startEnd[1]);\n\t}\n\n\t/**\n\t * 根据总数计算总页数\n\t *\n\t * @param totalCount 总数\n\t * @param pageSize   每页数\n\t * @return 总页数\n\t */\n\tpublic static int totalPage(int totalCount, int pageSize) {\n\t\treturn totalPage((long) totalCount,pageSize);\n\t}\n\n\t/**\n\t * 根据总数计算总页数\n\t *\n\t * @param totalCount 总数\n\t * @param pageSize   每页数\n\t * @return 总页数\n\t * @since 5.8.5\n\t */\n\tpublic static int totalPage(long totalCount, int pageSize) {\n\t\tif (pageSize == 0) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Math.toIntExact(totalCount % pageSize == 0 ? (totalCount / pageSize) : (totalCount / pageSize + 1));\n\t}\n\n\t/**\n\t * 分页彩虹算法<br>\n\t * 来自：<a href=\"https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java\">\n\t *     https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java</a><br>\n\t * 通过传入的信息，生成一个分页列表显示\n\t *\n\t * @param pageNo       当前页\n\t * @param totalPage    总页数\n\t * @param displayCount 每屏展示的页数\n\t * @return 分页条\n\t */\n\tpublic static int[] rainbow(int pageNo, int totalPage, int displayCount) {\n\t\t// displayCount % 2\n\t\tboolean isEven = (displayCount & 1) == 0;\n\t\tint left = displayCount >> 1;\n\t\tint right = displayCount >> 1;\n\n\t\tint length = displayCount;\n\t\tif (isEven) {\n\t\t\tright++;\n\t\t}\n\t\tif (totalPage < displayCount) {\n\t\t\tlength = totalPage;\n\t\t}\n\t\tint[] result = new int[length];\n\t\tif (totalPage >= displayCount) {\n\t\t\tif (pageNo <= left) {\n\t\t\t\tfor (int i = 0; i < result.length; i++) {\n\t\t\t\t\tresult[i] = i + 1;\n\t\t\t\t}\n\t\t\t} else if (pageNo > totalPage - right) {\n\t\t\t\tfor (int i = 0; i < result.length; i++) {\n\t\t\t\t\tresult[i] = i + totalPage - displayCount + 1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor (int i = 0; i < result.length; i++) {\n\t\t\t\t\tresult[i] = i + pageNo - left + (isEven ? 1 : 0);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor (int i = 0; i < result.length; i++) {\n\t\t\t\tresult[i] = i + 1;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\n\t}\n\n\t/**\n\t * 分页彩虹算法(默认展示10页)<br>\n\t * 来自：<a href=\"https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java\">\n\t *     https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java</a>\n\t *\n\t * @param currentPage 当前页\n\t * @param pageCount   总页数\n\t * @return 分页条\n\t */\n\tpublic static int[] rainbow(int currentPage, int pageCount) {\n\t\treturn rainbow(currentPage, pageCount, 10);\n\t}\n\n\t//------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 根据起始位置获取结束位置\n\t *\n\t * @param start    起始位置\n\t * @param pageSize 每页条目数\n\t * @return 结束位置\n\t */\n\tprivate static int getEndByStart(int start, int pageSize) {\n\t\tif (pageSize < 1) {\n\t\t\tpageSize = 0;\n\t\t}\n\t\treturn start + pageSize;\n\t}\n\n\t//------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/PhoneUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.lang.Validator;\n\n\n/**\n * 电话号码工具类，包括：\n * <ul>\n *     <li>手机号码</li>\n *     <li>400、800号码</li>\n *     <li>座机号码</li>\n * </ul>\n *\n * @author dahuoyzs\n * @since 5.3.11\n */\npublic class PhoneUtil {\n\n\t/**\n\t * 验证是否为手机号码（中国大陆）\n\t *\n\t * @param value 值\n\t * @return 是否为手机号码（中国大陆）\n\t * @since 5.3.11\n\t */\n\tpublic static boolean isMobile(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.MOBILE, value);\n\t}\n\n\t/**\n\t * 验证是否为手机号码（中国香港）\n\t * @param value 手机号码\n\t * @return 是否为中国香港手机号码\n\t * @since 5.6.3\n\t * @author dazer, ourslook\n\t */\n\tpublic static boolean isMobileHk(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.MOBILE_HK, value);\n\t}\n\n\t/**\n\t * 验证是否为手机号码（中国台湾）\n\t * @param value 手机号码\n\t * @return 是否为中国台湾手机号码\n\t * @since 5.6.6\n\t * @author ihao\n\t */\n\tpublic static boolean isMobileTw(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.MOBILE_TW, value);\n\t}\n\n\t/**\n\t * 验证是否为手机号码（中国澳门）\n\t * @param value 手机号码\n\t * @return 是否为中国澳门手机号码\n\t * @since 5.6.6\n\t * @author ihao\n\t */\n\tpublic static boolean isMobileMo(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.MOBILE_MO, value);\n\t}\n\n\t/**\n\t * 验证是否为座机号码（中国大陆）\n\t *\n\t * @param value 值\n\t * @return 是否为座机号码（中国大陆）\n\t * @since 5.3.11\n\t */\n\tpublic static boolean isTel(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.TEL, value);\n\t}\n\n\t/**\n\t * 验证是否为座机号码（中国大陆）+ 400 + 800\n\t *\n\t * @param value 值\n\t * @return 是否为座机号码（中国大陆）\n\t * @since 5.6.3\n\t * @author dazer, ourslook\n\t */\n\tpublic static boolean isTel400800(CharSequence value) {\n\t\treturn Validator.isMatchRegex(PatternPool.TEL_400_800, value);\n\t}\n\n\t/**\n\t * 验证是否为座机号码+手机号码（CharUtil中国）+ 400 + 800电话 + 手机号号码（中国香港）\n\t *\n\t * @param value 值\n\t * @return 是否为座机号码+手机号码（中国大陆）+手机号码（中国香港）+手机号码（中国台湾）+手机号码（中国澳门）\n\t * @since 5.3.11\n\t */\n\tpublic static boolean isPhone(CharSequence value) {\n\t\treturn isMobile(value) || isTel400800(value) || isMobileHk(value) || isMobileTw(value) || isMobileMo(value);\n\t}\n\n\t/**\n\t * 隐藏手机号前7位  替换字符为\"*\"\n\t * 栗子\n\t *\n\t * @param phone 手机号码\n\t * @return 替换后的字符串\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence hideBefore(CharSequence phone) {\n\t\treturn StrUtil.hide(phone, 0, 7);\n\t}\n\n\t/**\n\t * 隐藏手机号中间4位  替换字符为\"*\"\n\t *\n\t * @param phone 手机号码\n\t * @return 替换后的字符串\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence hideBetween(CharSequence phone) {\n\t\treturn StrUtil.hide(phone, 3, 7);\n\t}\n\n\t/**\n\t * 隐藏手机号最后4位  替换字符为\"*\"\n\t *\n\t * @param phone 手机号码\n\t * @return 替换后的字符串\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence hideAfter(CharSequence phone) {\n\t\treturn StrUtil.hide(phone, 7, 11);\n\t}\n\n\t/**\n\t * 获取手机号前3位\n\t *\n\t * @param phone 手机号码\n\t * @return 手机号前3位\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence subBefore(CharSequence phone) {\n\t\treturn StrUtil.sub(phone, 0, 3);\n\t}\n\n\t/**\n\t * 获取手机号中间4位\n\t *\n\t * @param phone 手机号码\n\t * @return 手机号中间4位\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence subBetween(CharSequence phone) {\n\t\treturn StrUtil.sub(phone, 3, 7);\n\t}\n\n\t/**\n\t * 获取手机号后4位\n\t *\n\t * @param phone 手机号码\n\t * @return 手机号后4位\n\t * @since 5.3.11\n\t */\n\tpublic static CharSequence subAfter(CharSequence phone) {\n\t\treturn StrUtil.sub(phone, 7, 11);\n\t}\n\n\t/**\n\t * 获取固话号码中的区号\n\t *\n\t * @param value 完整的固话号码\n\t * @return 固话号码的区号部分\n\t * @since 5.7.7\n\t */\n\tpublic static CharSequence subTelBefore(CharSequence value)\n\t{\n\t\treturn ReUtil.getGroup1(PatternPool.TEL, value);\n\t}\n\n\t/**\n\t * 获取固话号码中的号码\n\t *\n\t * @param value 完整的固话号码\n\t * @return 固话号码的号码部分\n\t * @since 5.7.7\n\t */\n\tpublic static CharSequence subTelAfter(CharSequence value)\n\t{\n\t\treturn ReUtil.get(PatternPool.TEL, value, 2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/PrimitiveArrayUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport java.lang.reflect.Array;\nimport java.util.Arrays;\nimport java.util.Random;\n\n/**\n * 原始类型数组工具类\n *\n * @author looly\n * @since 5.5.2\n */\npublic class PrimitiveArrayUtil {\n\t/**\n\t * 数组中元素未找到的下标，值为-1\n\t */\n\tpublic static final int INDEX_NOT_FOUND = -1;\n\n\t// ---------------------------------------------------------------------- isEmpty\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(long[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(int[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(short[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(char[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(byte[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(double[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(float[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 数组是否为空\n\t *\n\t * @param array 数组\n\t * @return 是否为空\n\t */\n\tpublic static boolean isEmpty(boolean[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t// ---------------------------------------------------------------------- isNotEmpty\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(long[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(int[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(short[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(char[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(byte[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(double[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(float[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t/**\n\t * 数组是否为非空\n\t *\n\t * @param array 数组\n\t * @return 是否为非空\n\t */\n\tpublic static boolean isNotEmpty(boolean[] array) {\n\t\treturn false == isEmpty(array);\n\t}\n\n\t// ---------------------------------------------------------------------- resize\n\n\t/**\n\t * 生成一个新的重新设置大小的数组<br>\n\t * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，其它位置补充0，缩小则截断\n\t *\n\t * @param bytes   原数组\n\t * @param newSize 新的数组大小\n\t * @return 调整后的新数组\n\t * @since 4.6.7\n\t */\n\tpublic static byte[] resize(byte[] bytes, int newSize) {\n\t\tif (newSize < 0) {\n\t\t\treturn bytes;\n\t\t}\n\t\tfinal byte[] newArray = new byte[newSize];\n\t\tif (newSize > 0 && isNotEmpty(bytes)) {\n\t\t\tSystem.arraycopy(bytes, 0, newArray, 0, Math.min(bytes.length, newSize));\n\t\t}\n\t\treturn newArray;\n\t}\n\n\t// ---------------------------------------------------------------------- addAll\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static byte[] addAll(byte[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (byte[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal byte[] result = new byte[length];\n\t\tlength = 0;\n\t\tfor (byte[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static int[] addAll(int[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (int[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal int[] result = new int[length];\n\t\tlength = 0;\n\t\tfor (int[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static long[] addAll(long[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (long[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal long[] result = new long[length];\n\t\tlength = 0;\n\t\tfor (long[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static double[] addAll(double[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (double[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal double[] result = new double[length];\n\t\tlength = 0;\n\t\tfor (double[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static float[] addAll(float[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (float[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal float[] result = new float[length];\n\t\tlength = 0;\n\t\tfor (float[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static char[] addAll(char[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (char[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal char[] result = new char[length];\n\t\tlength = 0;\n\t\tfor (char[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static boolean[] addAll(boolean[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (boolean[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal boolean[] result = new boolean[length];\n\t\tlength = 0;\n\t\tfor (boolean[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将多个数组合并在一起<br>\n\t * 忽略null的数组\n\t *\n\t * @param arrays 数组集合\n\t * @return 合并后的数组\n\t * @since 4.6.9\n\t */\n\tpublic static short[] addAll(short[]... arrays) {\n\t\tif (arrays.length == 1) {\n\t\t\treturn arrays[0];\n\t\t}\n\n\t\t// 计算总长度\n\t\tint length = 0;\n\t\tfor (short[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\n\t\tfinal short[] result = new short[length];\n\t\tlength = 0;\n\t\tfor (short[] array : arrays) {\n\t\t\tif (isNotEmpty(array)) {\n\t\t\t\tSystem.arraycopy(array, 0, result, length, array.length);\n\t\t\t\tlength += array.length;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t// ---------------------------------------------------------------------- range\n\n\t/**\n\t * 生成一个从0开始的数字列表<br>\n\t *\n\t * @param excludedEnd 结束的数字（不包含）\n\t * @return 数字列表\n\t */\n\tpublic static int[] range(int excludedEnd) {\n\t\treturn range(0, excludedEnd, 1);\n\t}\n\n\t/**\n\t * 生成一个数字列表<br>\n\t * 自动判定正序反序\n\t *\n\t * @param includedStart 开始的数字（包含）\n\t * @param excludedEnd   结束的数字（不包含）\n\t * @return 数字列表\n\t */\n\tpublic static int[] range(int includedStart, int excludedEnd) {\n\t\treturn range(includedStart, excludedEnd, 1);\n\t}\n\n\t/**\n\t * 生成一个数字列表<br>\n\t * 自动判定正序反序\n\t *\n\t * @param includedStart 开始的数字（包含）\n\t * @param excludedEnd   结束的数字（不包含）\n\t * @param step          步进\n\t * @return 数字列表\n\t */\n\tpublic static int[] range(int includedStart, int excludedEnd, int step) {\n\t\tif (includedStart > excludedEnd) {\n\t\t\tint tmp = includedStart;\n\t\t\tincludedStart = excludedEnd;\n\t\t\texcludedEnd = tmp;\n\t\t}\n\n\t\tif (step <= 0) {\n\t\t\tstep = 1;\n\t\t}\n\n\t\tint deviation = excludedEnd - includedStart;\n\t\tint length = deviation / step;\n\t\tif (deviation % step != 0) {\n\t\t\tlength += 1;\n\t\t}\n\t\tint[] range = new int[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\trange[i] = includedStart;\n\t\t\tincludedStart += step;\n\t\t}\n\t\treturn range;\n\t}\n\n\t// ---------------------------------------------------------------------- split\n\n\t/**\n\t * 拆分byte数组为几个等份（最后一份按照剩余长度分配空间）\n\t *\n\t * @param array 数组\n\t * @param len   每个小节的长度\n\t * @return 拆分后的数组\n\t */\n\tpublic static byte[][] split(byte[] array, int len) {\n\t\tint amount = array.length / len;\n\t\tfinal int remainder = array.length % len;\n\t\tif (remainder != 0) {\n\t\t\t++amount;\n\t\t}\n\t\tfinal byte[][] arrays = new byte[amount][];\n\t\tbyte[] arr;\n\t\tfor (int i = 0; i < amount; i++) {\n\t\t\tif (i == amount - 1 && remainder != 0) {\n\t\t\t\t// 有剩余，按照实际长度创建\n\t\t\t\tarr = new byte[remainder];\n\t\t\t\tSystem.arraycopy(array, i * len, arr, 0, remainder);\n\t\t\t} else {\n\t\t\t\tarr = new byte[len];\n\t\t\t\tSystem.arraycopy(array, i * len, arr, 0, len);\n\t\t\t}\n\t\t\tarrays[i] = arr;\n\t\t}\n\t\treturn arrays;\n\t}\n\n\t// ---------------------------------------------------------------------- indexOf、LastIndexOf、contains\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(long[] array, long value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(long[] array, long value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(long[] array, long value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(int[] array, int value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(int[] array, int value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(int[] array, int value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(short[] array, short value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(short[] array, short value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(short[] array, short value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(char[] array, char value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(char[] array, char value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(char[] array, char value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(byte[] array, byte value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(byte[] array, byte value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(byte[] array, byte value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(double[] array, double value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (NumberUtil.equals(value, array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(double[] array, double value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (NumberUtil.equals(value, array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(double[] array, double value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(float[] array, float value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (NumberUtil.equals(value, array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(float[] array, float value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (NumberUtil.equals(value, array[i])) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(float[] array, float value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int indexOf(boolean[] array, boolean value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 返回数组中指定元素所在最后的位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 数组中指定元素所在位置，未找到返回{@link #INDEX_NOT_FOUND}\n\t * @since 3.0.7\n\t */\n\tpublic static int lastIndexOf(boolean[] array, boolean value) {\n\t\tif (isNotEmpty(array)) {\n\t\t\tfor (int i = array.length - 1; i >= 0; i--) {\n\t\t\t\tif (value == array[i]) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn INDEX_NOT_FOUND;\n\t}\n\n\t/**\n\t * 数组中是否包含元素\n\t *\n\t * @param array 数组\n\t * @param value 被检查的元素\n\t * @return 是否包含\n\t * @since 3.0.7\n\t */\n\tpublic static boolean contains(boolean[] array, boolean value) {\n\t\treturn indexOf(array, value) > INDEX_NOT_FOUND;\n\t}\n\n\t// ------------------------------------------------------------------- Wrap and unwrap\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Integer[] wrap(int... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Integer[0];\n\t\t}\n\n\t\tfinal Integer[] array = new Integer[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组，null转为0\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static int[] unWrap(Integer... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new int[0];\n\t\t}\n\n\t\tfinal int[] array = new int[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], 0);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Long[] wrap(long... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Long[0];\n\t\t}\n\n\t\tfinal Long[] array = new Long[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static long[] unWrap(Long... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new long[0];\n\t\t}\n\n\t\tfinal long[] array = new long[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], 0L);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Character[] wrap(char... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Character[0];\n\t\t}\n\n\t\tfinal Character[] array = new Character[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static char[] unWrap(Character... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new char[0];\n\t\t}\n\n\t\tchar[] array = new char[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], Character.MIN_VALUE);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Byte[] wrap(byte... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Byte[0];\n\t\t}\n\n\t\tfinal Byte[] array = new Byte[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static byte[] unWrap(Byte... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new byte[0];\n\t\t}\n\n\t\tfinal byte[] array = new byte[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], (byte) 0);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Short[] wrap(short... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Short[0];\n\t\t}\n\n\t\tfinal Short[] array = new Short[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static short[] unWrap(Short... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new short[0];\n\t\t}\n\n\t\tfinal short[] array = new short[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], (short) 0);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Float[] wrap(float... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Float[0];\n\t\t}\n\n\t\tfinal Float[] array = new Float[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static float[] unWrap(Float... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new float[0];\n\t\t}\n\n\t\tfinal float[] array = new float[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], 0F);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Double[] wrap(double... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Double[0];\n\t\t}\n\n\t\tfinal Double[] array = new Double[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static double[] unWrap(Double... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new double[0];\n\t\t}\n\n\t\tfinal double[] array = new double[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], 0D);\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 将原始类型数组包装为包装类型\n\t *\n\t * @param values 原始类型数组\n\t * @return 包装类型数组\n\t */\n\tpublic static Boolean[] wrap(boolean... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new Boolean[0];\n\t\t}\n\n\t\tfinal Boolean[] array = new Boolean[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = values[i];\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 包装类数组转为原始类型数组<br>\n\t * {@code null} 按照 {@code false} 对待\n\t *\n\t * @param values 包装类型数组\n\t * @return 原始类型数组\n\t */\n\tpublic static boolean[] unWrap(Boolean... values) {\n\t\tif (null == values) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal int length = values.length;\n\t\tif (0 == length) {\n\t\t\treturn new boolean[0];\n\t\t}\n\n\t\tfinal boolean[] array = new boolean[length];\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tarray[i] = ObjectUtil.defaultIfNull(values[i], false);\n\t\t}\n\t\treturn array;\n\t}\n\n\t// ------------------------------------------------------------------- sub\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static byte[] sub(byte[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new byte[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new byte[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static int[] sub(int[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new int[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new int[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static long[] sub(long[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new long[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new long[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static short[] sub(short[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new short[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new short[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static char[] sub(char[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new char[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new char[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static double[] sub(double[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new double[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new double[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static float[] sub(float[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new float[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new float[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t/**\n\t * 获取子数组\n\t *\n\t * @param array 数组\n\t * @param start 开始位置（包括）\n\t * @param end   结束位置（不包括）\n\t * @return 新的数组\n\t * @see Arrays#copyOfRange(Object[], int, int)\n\t * @since 4.5.2\n\t */\n\tpublic static boolean[] sub(boolean[] array, int start, int end) {\n\t\tint length = Array.getLength(array);\n\t\tif (start < 0) {\n\t\t\tstart += length;\n\t\t}\n\t\tif (end < 0) {\n\t\t\tend += length;\n\t\t}\n\t\tif (start == length) {\n\t\t\treturn new boolean[0];\n\t\t}\n\t\tif (start > end) {\n\t\t\tint tmp = start;\n\t\t\tstart = end;\n\t\t\tend = tmp;\n\t\t}\n\t\tif (end > length) {\n\t\t\tif (start >= length) {\n\t\t\t\treturn new boolean[0];\n\t\t\t}\n\t\t\tend = length;\n\t\t}\n\t\treturn Arrays.copyOfRange(array, start, end);\n\t}\n\n\t// ------------------------------------------------------------------- remove\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static long[] remove(long[] array, int index) throws IllegalArgumentException {\n\t\treturn (long[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static int[] remove(int[] array, int index) throws IllegalArgumentException {\n\t\treturn (int[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static short[] remove(short[] array, int index) throws IllegalArgumentException {\n\t\treturn (short[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static char[] remove(char[] array, int index) throws IllegalArgumentException {\n\t\treturn (char[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] remove(byte[] array, int index) throws IllegalArgumentException {\n\t\treturn (byte[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static double[] remove(double[] array, int index) throws IllegalArgumentException {\n\t\treturn (double[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static float[] remove(float[] array, int index) throws IllegalArgumentException {\n\t\treturn (float[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static boolean[] remove(boolean[] array, int index) throws IllegalArgumentException {\n\t\treturn (boolean[]) remove((Object) array, index);\n\t}\n\n\t/**\n\t * 移除数组中对应位置的元素<br>\n\t * copy from commons-lang\n\t *\n\t * @param array 数组对象，可以是对象数组，也可以原始类型数组\n\t * @param index 位置，如果位置小于0或者大于长度，返回原数组\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\t@SuppressWarnings(\"SuspiciousSystemArraycopy\")\n\tpublic static Object remove(Object array, int index) throws IllegalArgumentException {\n\t\tif (null == array) {\n\t\t\treturn null;\n\t\t}\n\t\tint length = Array.getLength(array);\n\t\tif (index < 0 || index >= length) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfinal Object result = Array.newInstance(array.getClass().getComponentType(), length - 1);\n\t\tSystem.arraycopy(array, 0, result, 0, index);\n\t\tif (index < length - 1) {\n\t\t\t// 后半部分\n\t\t\tSystem.arraycopy(array, index + 1, result, index, length - index - 1);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// ---------------------------------------------------------------------- removeEle\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static long[] removeEle(long[] array, long element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static int[] removeEle(int[] array, int element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static short[] removeEle(short[] array, short element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static char[] removeEle(char[] array, char element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] removeEle(byte[] array, byte element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static double[] removeEle(double[] array, double element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static float[] removeEle(float[] array, float element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t/**\n\t * 移除数组中指定的元素<br>\n\t * 只会移除匹配到的第一个元素 copy from commons-lang\n\t *\n\t * @param array   数组对象，可以是对象数组，也可以原始类型数组\n\t * @param element 要移除的元素\n\t * @return 去掉指定元素后的新数组或原数组\n\t * @throws IllegalArgumentException 参数对象不为数组对象\n\t * @since 3.0.8\n\t */\n\tpublic static boolean[] removeEle(boolean[] array, boolean element) throws IllegalArgumentException {\n\t\treturn remove(array, indexOf(array, element));\n\t}\n\n\t// ---------------------------------------------------------------------- reverse\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static long[] reverse(long[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static long[] reverse(long[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static int[] reverse(int[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static int[] reverse(int[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static short[] reverse(short[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static short[] reverse(short[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static char[] reverse(char[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static char[] reverse(char[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static byte[] reverse(byte[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static byte[] reverse(byte[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static double[] reverse(double[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static double[] reverse(double[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static float[] reverse(float[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static float[] reverse(float[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array               数组，会变更\n\t * @param startIndexInclusive 起始位置（包含）\n\t * @param endIndexExclusive   结束位置（不包含）\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static boolean[] reverse(boolean[] array, final int startIndexInclusive, final int endIndexExclusive) {\n\t\tif (isEmpty(array)) {\n\t\t\treturn array;\n\t\t}\n\t\tint i = Math.max(startIndexInclusive, 0);\n\t\tint j = Math.min(array.length, endIndexExclusive) - 1;\n\t\twhile (j > i) {\n\t\t\tswap(array, i, j);\n\t\t\tj--;\n\t\t\ti++;\n\t\t}\n\t\treturn array;\n\t}\n\n\t/**\n\t * 反转数组，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 变更后的原数组\n\t * @since 3.0.9\n\t */\n\tpublic static boolean[] reverse(boolean[] array) {\n\t\treturn reverse(array, 0, array.length);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------ min and max\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static long min(long... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tlong min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static int min(int... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tint min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static short min(short... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tshort min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static char min(char... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tchar min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static byte min(byte... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tbyte min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static double min(double... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tdouble min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最小值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最小值\n\t * @since 3.0.9\n\t */\n\tpublic static float min(float... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tfloat min = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (min > numberArray[i]) {\n\t\t\t\tmin = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn min;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static long max(long... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tlong max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static int max(int... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tint max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static short max(short... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tshort max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static char max(char... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tchar max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static byte max(byte... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tbyte max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static double max(double... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tdouble max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t/**\n\t * 取最大值\n\t *\n\t * @param numberArray 数字数组\n\t * @return 最大值\n\t * @since 3.0.9\n\t */\n\tpublic static float max(float... numberArray) {\n\t\tif (isEmpty(numberArray)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tfloat max = numberArray[0];\n\t\tfor (int i = 1; i < numberArray.length; i++) {\n\t\t\tif (max < numberArray[i]) {\n\t\t\t\tmax = numberArray[i];\n\t\t\t}\n\t\t}\n\t\treturn max;\n\t}\n\n\t// ---------------------------------------------------------------------- shuffle\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static int[] shuffle(int[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static int[] shuffle(int[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static long[] shuffle(long[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static long[] shuffle(long[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static double[] shuffle(double[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static double[] shuffle(double[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static float[] shuffle(float[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static float[] shuffle(float[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean[] shuffle(boolean[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean[] shuffle(boolean[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static byte[] shuffle(byte[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static byte[] shuffle(byte[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static char[] shuffle(char[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static char[] shuffle(char[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array 数组，会变更\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static short[] shuffle(short[] array) {\n\t\treturn shuffle(array, RandomUtil.getRandom());\n\t}\n\n\t/**\n\t * 打乱数组顺序，会变更原数组\n\t *\n\t * @param array  数组，会变更\n\t * @param random 随机数生成器\n\t * @return 打乱后的数组\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static short[] shuffle(short[] array, Random random) {\n\t\tif (array == null || random == null || array.length <= 1) {\n\t\t\treturn array;\n\t\t}\n\n\t\tfor (int i = array.length; i > 1; i--) {\n\t\t\tswap(array, i - 1, random.nextInt(i));\n\t\t}\n\n\t\treturn array;\n\t}\n\n\t// ---------------------------------------------------------------------- swap\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static int[] swap(int[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tint tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static long[] swap(long[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tlong tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static double[] swap(double[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tdouble tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static float[] swap(float[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tfloat tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static boolean[] swap(boolean[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tboolean tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static byte[] swap(byte[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tbyte tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static char[] swap(char[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tchar tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t/**\n\t * 交换数组中两个位置的值\n\t *\n\t * @param array  数组\n\t * @param index1 位置1\n\t * @param index2 位置2\n\t * @return 交换后的数组，与传入数组为同一对象\n\t * @since 4.0.7\n\t */\n\tpublic static short[] swap(short[] array, int index1, int index2) {\n\t\tif (isEmpty(array)) {\n\t\t\tthrow new IllegalArgumentException(\"Number array must not empty !\");\n\t\t}\n\t\tshort tmp = array[index1];\n\t\tarray[index1] = array[index2];\n\t\tarray[index2] = tmp;\n\t\treturn array;\n\t}\n\n\t// ---------------------------------------------------------------------- asc and desc\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(byte[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(byte[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(byte[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(short[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(short[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(short[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(char[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(char[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(char[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(int[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(int[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(int[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(long[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(long[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(long[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(double[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(double[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(double[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSorted(float[] array) {\n\t\treturn isSortedASC(array);\n\t}\n\n\t/**\n\t * 检查数组是否升序，即array[i] &lt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否升序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedASC(float[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] > array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 检查数组是否降序，即array[i] &gt;= array[i+1]，若传入空数组，则返回false\n\t *\n\t * @param array 数组\n\t * @return 数组是否降序\n\t * @author FengBaoheng\n\t * @since 5.5.2\n\t */\n\tpublic static boolean isSortedDESC(float[] array) {\n\t\tif (array == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (int i = 0; i < array.length - 1; i++) {\n\t\t\tif (array[i] < array[i + 1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/RadixUtil.java",
    "content": "package cn.hutool.core.util;\n\n/**\n * 进制转换工具类，可以转换为任意进制\n * <p>\n * 把一个十进制整数根据自己定义的进制规则进行转换<br>\n * from：https://gitee.com/loolly/hutool/pulls/260\n * <p>\n * 主要应用一下情况：\n * <ul>\n *     <li>根据ID生成邀请码,并且尽可能的缩短。并且不希望直接猜测出和ID的关联</li>\n *     <li>短连接的生成，根据ID转成短连接，同样不希望被猜测到</li>\n *     <li>数字加密，通过两次不同进制的转换，让有规律的数字看起来没有任何规律</li>\n *     <li>....</li>\n * </ul>\n *\n * @author xl7@qq.com\n * @since 5.5.8\n */\n\npublic class RadixUtil {\n\t/**\n\t * 34进制字符串，不包含 IO 字符\n\t * 对于需要补齐的，自己可以随机填充IO字符\n\t * 26个字母：abcdefghijklmnopqrstuvwxyz\n\t */\n\tpublic final static String RADIXS_34 = \"0123456789ABCDEFGHJKLMNPQRSTUVWXYZ\";\n\t/**\n\t * 打乱后的34进制\n\t */\n\tpublic final static String RADIXS_SHUFFLE_34 = \"H3UM16TDFPSBZJ90CW28QYRE45AXKNGV7L\";\n\n\t/**\n\t * 59进制字符串,不包含 IOl 字符\n\t */\n\tpublic final static String RADIXS_59 = \"0123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ\";\n\t/**\n\t * 打乱后的59进制\n\t */\n\tpublic final static String RADIXS_SHUFFLE_59 = \"vh9wGkfK8YmqbsoENP3764SeCX0dVzrgy1HRtpnTaLjJW2xQiZAcBMUFDu5\";\n\n\t/**\n\t * 把一个整型数值转换成自己定义的进制\n\t * 长度即进制<br>\n\t * <ul>\n\t *   <li>encode(\"AB\",10)  51转换成2进制，A=0;B=1 。 二进制1010，结果 BABA</li>\n\t *   <li>encode(\"VIP\",21)  21转换成3进制，V=0;I=1;P=2 ，三进制210 ,得到结果PIV </li>\n\t * </ul>\n\t *\n\t * @param radixs 自定进制,不要重复，否则转不回来的。\n\t * @param num    要转换的数值\n\t * @return 自定义进制字符串\n\t */\n\tpublic static String encode(String radixs, int num) {\n\t\t//考虑到负数问题\n\t\tlong tmpNum = (num >= 0 ? num : (0x100000000L - (~num + 1)));\n\t\treturn encode(radixs, tmpNum, 32);\n\t}\n\n\t/**\n\t * 把一个长整型数值转换成自己定义的进制\n\t *\n\t * @param radixs 自定进制,不要重复，否则转不回来的。\n\t * @param num    要转换的数值\n\t * @return 自定义进制字符串\n\t */\n\tpublic static String encode(String radixs, long num) {\n\t\tif (num < 0) {\n\t\t\tthrow new RuntimeException(\"暂不支持负数！\");\n\t\t}\n\n\t\treturn encode(radixs, num, 64);\n\t}\n\n\t/**\n\t * 把转换后的进制字符还原成int 值\n\t *\n\t * @param radixs    自定进制,需要和encode的保持一致\n\t * @param encodeStr 需要转换成十进制的字符串\n\t * @return int\n\t */\n\tpublic static int decodeToInt(String radixs, String encodeStr) {\n\t\t//还原负数\n\t\treturn (int) decode(radixs, encodeStr);\n\t}\n\n\t/**\n\t * 把转换后进制的字符还原成long 值\n\t *\n\t * @param radixs    自定进制,需要和encode的保持一致\n\t * @param encodeStr 需要转换成十进制的字符串\n\t * @return long\n\t */\n\tpublic static long decode(String radixs, String encodeStr) {\n\t\tif (radixs == null || radixs.length() < 2) {\n\t\t\tthrow new IllegalArgumentException(\"radixs must contain at least 2 characters\");\n\t\t}\n\t\tif (encodeStr == null || encodeStr.isEmpty()) {\n\t\t\tthrow new IllegalArgumentException(\"encodeStr is null or empty\");\n\t\t}\n\t\t//目标是多少进制\n\t\tint rl = radixs.length();\n\t\tlong res = 0L;\n\n\t\tfor (char c : encodeStr.toCharArray()) {\n\t\t\tint idx = radixs.indexOf(c);\n\t\t\tif (idx < 0) {\n\t\t\t\tthrow new IllegalArgumentException(\"Illegal character '\" + c + \"' for radixs\");\n\t\t\t}\n\t\t\tres = res * rl + idx;\n\t\t}\n\t\treturn res;\n\t}\n\n\t// -------------------------------------------------------------------------------- Private methods\n\tprivate static String encode(String radixs, long num, int maxLength) {\n\t\tif (radixs.length() < 2) {\n\t\t\tthrow new RuntimeException(\"自定义进制最少两个字符哦！\");\n\t\t}\n\t\t//目标是多少进制\n\t\tint rl = radixs.length();\n\t\t//考虑到负数问题\n\t\tlong tmpNum = num;\n\t\t//进制的结果，二进制最小进制转换结果是32个字符\n\t\t//StringBuilder 比较耗时\n\t\tchar[] aa = new char[maxLength];\n\t\t//因为反需字符串比较耗时\n\t\tint i = aa.length;\n\t\tdo {\n\t\t\taa[--i] = radixs.charAt((int) (tmpNum % rl));\n\t\t\ttmpNum /= rl;\n\t\t} while (tmpNum > 0);\n\t\t//去掉前面的字符串，trim比较耗时\n\t\treturn new String(aa, i, aa.length - i);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.date.DateField;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.WeightRandom;\nimport cn.hutool.core.lang.WeightRandom.WeightObj;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * 随机工具类\n *\n * @author xiaoleilu\n */\npublic class RandomUtil {\n\n\t/**\n\t * 用于随机选的数字\n\t */\n\tpublic static final String BASE_NUMBER = \"0123456789\";\n\t/**\n\t * 用于随机选的字符\n\t */\n\tpublic static final String BASE_CHAR = \"abcdefghijklmnopqrstuvwxyz\";\n\t/**\n\t * 用于随机选的字符和数字（小写）\n\t */\n\tpublic static final String BASE_CHAR_NUMBER_LOWER = BASE_CHAR + BASE_NUMBER;\n\t/**\n\t * 用于随机选的字符和数字（包括大写和小写字母）\n\t */\n\tpublic static final String BASE_CHAR_NUMBER = BASE_CHAR.toUpperCase() + BASE_CHAR_NUMBER_LOWER;\n\n\t// region ----- get or create Random\n\n\t/**\n\t * 获取随机数生成器对象<br>\n\t * ThreadLocalRandom是JDK 7之后提供并发产生随机数，能够解决多个线程发生的竞争争夺。\n\t *\n\t * <p>\n\t * 注意：此方法返回的{@link ThreadLocalRandom}不可以在多线程环境下共享对象，否则有重复随机数问题。\n\t * 见：https://www.jianshu.com/p/89dfe990295c\n\t * </p>\n\t *\n\t * @return {@link ThreadLocalRandom}\n\t * @since 3.1.2\n\t */\n\tpublic static ThreadLocalRandom getRandom() {\n\t\treturn ThreadLocalRandom.current();\n\t}\n\n\t/**\n\t * 创建{@link SecureRandom}，类提供加密的强随机数生成器 (RNG)<br>\n\t *\n\t * @param seed 自定义随机种子\n\t * @return {@link SecureRandom}\n\t * @since 4.6.5\n\t */\n\tpublic static SecureRandom createSecureRandom(final byte[] seed) {\n\t\treturn (null == seed) ? new SecureRandom() : new SecureRandom(seed);\n\t}\n\n\t/**\n\t * 获取SHA1PRNG的{@link SecureRandom}，类提供加密的强随机数生成器 (RNG)<br>\n\t * 注意：此方法获取的是伪随机序列发生器PRNG（pseudo-random number generator）\n\t *\n\t * <p>\n\t * 相关说明见：https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom\n\t *\n\t * @return {@link SecureRandom}\n\t * @since 3.1.2\n\t */\n\tpublic static SecureRandom getSecureRandom() {\n\t\treturn getSecureRandom(null);\n\t}\n\n\t/**\n\t * 获取SHA1PRNG的{@link SecureRandom}，类提供加密的强随机数生成器 (RNG)<br>\n\t * 注意：此方法获取的是伪随机序列发生器PRNG（pseudo-random number generator）\n\t *\n\t * <p>\n\t * 相关说明见：https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom\n\t *\n\t * @param seed 随机数种子\n\t * @return {@link SecureRandom}\n\t * @see #createSecureRandom(byte[])\n\t * @since 5.5.2\n\t */\n\tpublic static SecureRandom getSecureRandom(final byte[] seed) {\n\t\treturn createSecureRandom(seed);\n\t}\n\n\t/**\n\t * 获取SHA1PRNG的{@link SecureRandom}，类提供加密的强随机数生成器 (RNG)<br>\n\t * 注意：此方法获取的是伪随机序列发生器PRNG（pseudo-random number generator）,在Linux下噪声生成时可能造成较长时间停顿。<br>\n\t * see: http://ifeve.com/jvm-random-and-entropy-source/\n\t *\n\t * <p>\n\t * 相关说明见：https://stackoverflow.com/questions/137212/how-to-solve-slow-java-securerandom\n\t *\n\t * @param seed 随机数种子\n\t * @return {@link SecureRandom}\n\t * @since 5.5.8\n\t */\n\tpublic static SecureRandom getSHA1PRNGRandom(final byte[] seed) {\n\t\tfinal SecureRandom random;\n\t\ttry {\n\t\t\trandom = SecureRandom.getInstance(\"SHA1PRNG\");\n\t\t} catch (final NoSuchAlgorithmException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\tif (null != seed) {\n\t\t\trandom.setSeed(seed);\n\t\t}\n\t\treturn random;\n\t}\n\n\t/**\n\t * 获取algorithms/providers中提供的强安全随机生成器<br>\n\t * 注意：此方法可能造成阻塞或性能问题\n\t *\n\t * @return {@link SecureRandom}\n\t * @since 5.7.12\n\t */\n\tpublic static SecureRandom getSecureRandomStrong() {\n\t\ttry {\n\t\t\treturn SecureRandom.getInstanceStrong();\n\t\t} catch (final NoSuchAlgorithmException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取随机数产生器\n\t *\n\t * @param isSecure 是否为强随机数生成器 (RNG)\n\t * @return {@link Random}\n\t * @see #getSecureRandom()\n\t * @see #getRandom()\n\t * @since 4.1.15\n\t */\n\tpublic static Random getRandom(final boolean isSecure) {\n\t\treturn isSecure ? getSecureRandom() : getRandom();\n\t}\n\t// endregion\n\n\t/**\n\t * 获得随机Boolean值\n\t *\n\t * @return true or false\n\t * @since 4.5.9\n\t */\n\tpublic static boolean randomBoolean() {\n\t\treturn 0 == randomInt(2);\n\t}\n\n\t/**\n\t * 随机bytes\n\t *\n\t * @param length 长度\n\t * @return bytes\n\t */\n\tpublic static byte[] randomBytes(final int length) {\n\t\tfinal byte[] bytes = new byte[length];\n\t\tgetRandom().nextBytes(bytes);\n\t\treturn bytes;\n\t}\n\n\t// region ----- randomInt\n\n\t/**\n\t * 获得随机数int值\n\t *\n\t * @return 随机数\n\t * @see Random#nextInt()\n\t */\n\tpublic static int randomInt() {\n\t\treturn getRandom().nextInt();\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数 [0,limit)\n\t *\n\t * @param limitExclude 限制随机数的范围，不包括这个数\n\t * @return 随机数\n\t * @see Random#nextInt(int)\n\t */\n\tpublic static int randomInt(final int limitExclude) {\n\t\treturn getRandom().nextInt(limitExclude);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param minInclude 最小数（包含）\n\t * @param maxExclude 最大数（不包含）\n\t * @return 随机数\n\t */\n\tpublic static int randomInt(final int minInclude, final int maxExclude) {\n\t\treturn randomInt(minInclude, maxExclude, true, false);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param min        最小数\n\t * @param max        最大数\n\t * @param includeMin 是否包含最小值\n\t * @param includeMax 是否包含最大值\n\t * @return 随机数\n\t */\n\tpublic static int randomInt(int min, int max, final boolean includeMin, final boolean includeMax) {\n\t\tif (!includeMin) {\n\t\t\tmin++;\n\t\t}\n\t\tif (includeMax) {\n\t\t\tmax++;\n\t\t}\n\t\treturn getRandom().nextInt(min, max);\n\t}\n\n\t/**\n\t * 创建指定长度的随机索引\n\t *\n\t * @param length 长度\n\t * @return 随机索引\n\t * @since 5.2.1\n\t */\n\tpublic static int[] randomInts(final int length) {\n\t\tfinal int[] range = ArrayUtil.range(length);\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tfinal int random = randomInt(i, length);\n\t\t\tArrayUtil.swap(range, i, random);\n\t\t}\n\t\treturn range;\n\t}\n\t// endregion\n\n\t// region ----- randomLong\n\n\t/**\n\t * 获得随机数\n\t *\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextLong()\n\t * @since 3.3.0\n\t */\n\tpublic static long randomLong() {\n\t\treturn getRandom().nextLong();\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数 [0,limit)\n\t *\n\t * @param limitExclude 限制随机数的范围，不包括这个数\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextLong(long)\n\t */\n\tpublic static long randomLong(final long limitExclude) {\n\t\treturn getRandom().nextLong(limitExclude);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数[min, max)\n\t *\n\t * @param minInclude 最小数（包含）\n\t * @param maxExclude 最大数（不包含）\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextLong(long, long)\n\t * @since 3.3.0\n\t */\n\tpublic static long randomLong(final long minInclude, final long maxExclude) {\n\t\treturn randomLong(minInclude, maxExclude, true, false);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param min        最小数\n\t * @param max        最大数\n\t * @param includeMin 是否包含最小值\n\t * @param includeMax 是否包含最大值\n\t * @return 随机数\n\t */\n\tpublic static long randomLong(long min, long max, final boolean includeMin, final boolean includeMax) {\n\t\tif (!includeMin) {\n\t\t\tmin++;\n\t\t}\n\t\tif (includeMax) {\n\t\t\tmax++;\n\t\t}\n\t\treturn getRandom().nextLong(min, max);\n\t}\n\t// endregion\n\n\t// region ----- randomFloat\n\t/**\n\t * 获得随机数[0, 1)\n\t *\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextFloat()\n\t */\n\tpublic static float randomFloat() {\n\t\treturn getRandom().nextFloat();\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数 [0,limit)\n\t *\n\t * @param limitExclude 限制随机数的范围，不包括这个数\n\t * @return 随机数\n\t */\n\tpublic static float randomFloat(final float limitExclude) {\n\t\treturn randomFloat(0, limitExclude);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数[min, max)\n\t *\n\t * @param minInclude 最小数（包含）\n\t * @param maxExclude 最大数（不包含）\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextFloat()\n\t */\n\tpublic static float randomFloat(final float minInclude, final float maxExclude) {\n\t\tif (minInclude == maxExclude) {\n\t\t\treturn minInclude;\n\t\t}\n\n\t\treturn minInclude + ((maxExclude - minInclude) * getRandom().nextFloat());\n\t}\n\t// endregion\n\n\t// region ----- randomDouble\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param minInclude 最小数（包含）\n\t * @param maxExclude 最大数（不包含）\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextDouble(double, double)\n\t * @since 3.3.0\n\t */\n\tpublic static double randomDouble(final double minInclude, final double maxExclude) {\n\t\treturn getRandom().nextDouble(minInclude, maxExclude);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param minInclude   最小数（包含）\n\t * @param maxExclude   最大数（不包含）\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 随机数\n\t * @since 4.0.8\n\t */\n\tpublic static double randomDouble(final double minInclude, final double maxExclude, final int scale,\n\t\t\t\t\t\t\t\t\t  final RoundingMode roundingMode) {\n\t\treturn NumberUtil.round(randomDouble(minInclude, maxExclude), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 获得随机数[0, 1)\n\t *\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextDouble()\n\t * @since 3.3.0\n\t */\n\tpublic static double randomDouble() {\n\t\treturn getRandom().nextDouble();\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 随机数\n\t * @since 4.0.8\n\t */\n\tpublic static double randomDouble(final int scale, final RoundingMode roundingMode) {\n\t\treturn NumberUtil.round(randomDouble(), scale, roundingMode).doubleValue();\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数 [0,limit)\n\t *\n\t * @param limit 限制随机数的范围，不包括这个数\n\t * @return 随机数\n\t * @see ThreadLocalRandom#nextDouble(double)\n\t * @since 3.3.0\n\t */\n\tpublic static double randomDouble(final double limit) {\n\t\treturn getRandom().nextDouble(limit);\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param limit        限制随机数的范围，不包括这个数\n\t * @param scale        保留小数位数\n\t * @param roundingMode 保留小数的模式 {@link RoundingMode}\n\t * @return 随机数\n\t * @since 4.0.8\n\t */\n\tpublic static double randomDouble(final double limit, final int scale, final RoundingMode roundingMode) {\n\t\treturn NumberUtil.round(randomDouble(limit), scale, roundingMode).doubleValue();\n\t}\n\t// endregion\n\n\t// region ----- randomBigDecimal\n\n\t/**\n\t * 获得指定范围内的随机数[0, 1)\n\t *\n\t * @return 随机数\n\t * @since 4.0.9\n\t */\n\tpublic static BigDecimal randomBigDecimal() {\n\t\treturn NumberUtil.toBigDecimal(getRandom().nextDouble());\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数 [0,limit)\n\t *\n\t * @param limit 最大数（不包含）\n\t * @return 随机数\n\t * @since 4.0.9\n\t */\n\tpublic static BigDecimal randomBigDecimal(final BigDecimal limit) {\n\t\treturn NumberUtil.toBigDecimal(getRandom().nextDouble(limit.doubleValue()));\n\t}\n\n\t/**\n\t * 获得指定范围内的随机数\n\t *\n\t * @param minInclude 最小数（包含）\n\t * @param maxExclude 最大数（不包含）\n\t * @return 随机数\n\t * @since 4.0.9\n\t */\n\tpublic static BigDecimal randomBigDecimal(final BigDecimal minInclude, final BigDecimal maxExclude) {\n\t\treturn NumberUtil.toBigDecimal(getRandom().nextDouble(minInclude.doubleValue(), maxExclude.doubleValue()));\n\t}\n\t// endregion\n\n\t// region ----- randomEle\n\n\t/**\n\t * 随机获得列表中的元素\n\t *\n\t * @param <T>  元素类型\n\t * @param list 列表\n\t * @return 随机元素\n\t */\n\tpublic static <T> T randomEle(final List<T> list) {\n\t\treturn randomEle(list, list.size());\n\t}\n\n\t/**\n\t * 随机获得列表中的元素\n\t *\n\t * @param <T>   元素类型\n\t * @param list  列表\n\t * @param limit 限制列表的前N项\n\t * @return 随机元素\n\t */\n\tpublic static <T> T randomEle(final List<T> list, int limit) {\n\t\tif (list.size() < limit) {\n\t\t\tlimit = list.size();\n\t\t}\n\t\treturn list.get(randomInt(limit));\n\t}\n\n\t/**\n\t * 随机获得数组中的元素\n\t *\n\t * @param <T>   元素类型\n\t * @param array 列表\n\t * @return 随机元素\n\t * @since 3.3.0\n\t */\n\tpublic static <T> T randomEle(final T[] array) {\n\t\treturn randomEle(array, array.length);\n\t}\n\n\t/**\n\t * 随机获得数组中的元素\n\t *\n\t * @param <T>   元素类型\n\t * @param array 列表\n\t * @param limit 限制列表的前N项\n\t * @return 随机元素\n\t * @since 3.3.0\n\t */\n\tpublic static <T> T randomEle(final T[] array, int limit) {\n\t\tif (array.length < limit) {\n\t\t\tlimit = array.length;\n\t\t}\n\t\treturn array[randomInt(limit)];\n\t}\n\n\t/**\n\t * 随机获得列表中的一定量元素\n\t *\n\t * @param <T>   元素类型\n\t * @param list  列表\n\t * @param count 随机取出的个数\n\t * @return 随机元素\n\t */\n\tpublic static <T> List<T> randomEles(final List<T> list, final int count) {\n\t\tfinal List<T> result = new ArrayList<>(count);\n\t\tfinal int limit = list.size();\n\t\twhile (result.size() < count) {\n\t\t\tresult.add(randomEle(list, limit));\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 随机获得列表中的一定量的元素，返回List<br>\n\t * 此方法与{@link #randomEles(List, int)} 不同点在于，不会获取重复位置的元素\n\t *\n\t * @param source 列表\n\t * @param count  随机取出的个数\n\t * @param <T>    元素类型\n\t * @return 随机列表\n\t * @since 5.2.1\n\t */\n\tpublic static <T> List<T> randomEleList(final List<T> source, final int count) {\n\t\tif (count >= source.size()) {\n\t\t\treturn ListUtil.toList(source);\n\t\t}\n\t\tfinal int[] randomList = ArrayUtil.sub(randomInts(source.size()), 0, count);\n\t\tfinal List<T> result = new ArrayList<>();\n\t\tfor (final int e : randomList) {\n\t\t\tresult.add(source.get(e));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 随机获得列表中的一定量的不重复元素，返回Set\n\t *\n\t * @param <T>        元素类型\n\t * @param collection 列表\n\t * @param count      随机取出的个数\n\t * @return 随机元素\n\t * @throws IllegalArgumentException 需要的长度大于给定集合非重复总数\n\t */\n\tpublic static <T> Set<T> randomEleSet(final Collection<T> collection, final int count) {\n\t\tfinal ArrayList<T> source = CollUtil.distinct(collection);\n\t\tif (count > source.size()) {\n\t\t\tthrow new IllegalArgumentException(\"Count is larger than collection distinct size !\");\n\t\t}\n\n\t\tfinal Set<T> result = new LinkedHashSet<>(count);\n\t\tfinal int limit = source.size();\n\t\twhile (result.size() < count) {\n\t\t\tresult.add(randomEle(source, limit));\n\t\t}\n\n\t\treturn result;\n\t}\n\t// endregion\n\n\t// region ----- randomString\n\n\t/**\n\t * 获得一个随机的字符串（只包含数字和字符）\n\t *\n\t * @param length 字符串的长度\n\t * @return 随机字符串\n\t */\n\tpublic static String randomString(final int length) {\n\t\treturn randomString(BASE_CHAR_NUMBER, length);\n\t}\n\n\t/**\n\t * 获得一个随机的字符串（只包含数字和小写字母）\n\t *\n\t * @param length 字符串的长度\n\t * @return 随机字符串\n\t * @since 5.8.41\n\t */\n\tpublic static String randomStringLower(final int length) {\n\t\treturn randomString(BASE_CHAR_NUMBER_LOWER, length);\n\t}\n\n\t/**\n\t * 获得一个随机的字符串（只包含数字和大写字符）\n\t *\n\t * @param length 字符串的长度\n\t * @return 随机字符串\n\t * @since 4.0.13\n\t */\n\tpublic static String randomStringUpper(final int length) {\n\t\treturn randomString(BASE_CHAR_NUMBER, length).toUpperCase();\n\t}\n\n\t/**\n\t * 获得一个随机的字符串（只包含数字和字母） 并排除指定字符串\n\t *\n\t * @param length   字符串的长度\n\t * @param elemData 要排除的字符串,如：去重容易混淆的字符串，oO0、lL1、q9Q、pP，区分大小写\n\t * @return 随机字符串\n\t */\n\tpublic static String randomStringWithoutStr(final int length, final String elemData) {\n\t\tString baseStr = BASE_CHAR_NUMBER;\n\t\tbaseStr = StrUtil.removeAll(baseStr, elemData.toCharArray());\n\t\treturn randomString(baseStr, length);\n\t}\n\n\t/**\n\t * 获得一个随机的字符串（只包含数字和小写字母） 并排除指定字符串\n\t *\n\t * @param length   字符串的长度\n\t * @param elemData 要排除的字符串,如：去重容易混淆的字符串，oO0、lL1、q9Q、pP，不区分大小写\n\t * @return 随机字符串\n\t * @since 5.8.28\n\t */\n\tpublic static String randomStringLowerWithoutStr(final int length, final String elemData) {\n\t\tString baseStr = BASE_CHAR_NUMBER_LOWER;\n\t\tbaseStr = StrUtil.removeAll(baseStr, elemData.toLowerCase().toCharArray());\n\t\treturn randomString(baseStr, length);\n\t}\n\n\t/**\n\t * 获得一个只包含数字的字符串\n\t *\n\t * @param length 字符串的长度\n\t * @return 随机字符串\n\t */\n\tpublic static String randomNumbers(final int length) {\n\t\treturn randomString(BASE_NUMBER, length);\n\t}\n\n\t/**\n\t * 获得一个随机的字符串\n\t *\n\t * @param baseString 随机字符选取的样本\n\t * @param length     字符串的长度\n\t * @return 随机字符串\n\t */\n\tpublic static String randomString(final String baseString, int length) {\n\t\tif (StrUtil.isEmpty(baseString)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tif (length < 1) {\n\t\t\tlength = 1;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder(length);\n\t\tfinal int baseLength = baseString.length();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tfinal int number = randomInt(baseLength);\n\t\t\tsb.append(baseString.charAt(number));\n\t\t}\n\t\treturn sb.toString();\n\t}\n\t// endregion\n\n\t// region ---- randomChar\n\n\t/**\n\t * 随机汉字（'\\u4E00'-'\\u9FFF'）\n\t *\n\t * @return 随机的汉字字符\n\t * @since 5.7.15\n\t */\n\t@SuppressWarnings(\"UnnecessaryUnicodeEscape\")\n\tpublic static char randomChinese() {\n\t\treturn (char) randomInt('\\u4E00', '\\u9FFF');\n\t}\n\n\t/**\n\t * 随机数字，数字为0~9单个数字\n\t *\n\t * @return 随机数字字符\n\t * @since 3.1.2\n\t */\n\tpublic static char randomNumber() {\n\t\treturn randomChar(BASE_NUMBER);\n\t}\n\n\t/**\n\t * 随机字母或数字，小写\n\t *\n\t * @return 随机字符\n\t * @since 3.1.2\n\t */\n\tpublic static char randomChar() {\n\t\treturn randomChar(BASE_CHAR_NUMBER);\n\t}\n\n\t/**\n\t * 随机字符\n\t *\n\t * @param baseString 随机字符选取的样本\n\t * @return 随机字符\n\t * @since 3.1.2\n\t */\n\tpublic static char randomChar(final String baseString) {\n\t\treturn baseString.charAt(randomInt(baseString.length()));\n\t}\n\t// endregion\n\n\t// region ----- weightRandom\n\n\t/**\n\t * 带有权重的随机生成器\n\t *\n\t * @param <T>        随机对象类型\n\t * @param weightObjs 带有权重的对象列表\n\t * @return {@link WeightRandom}\n\t * @since 4.0.3\n\t */\n\tpublic static <T> WeightRandom<T> weightRandom(final WeightObj<T>[] weightObjs) {\n\t\treturn new WeightRandom<>(weightObjs);\n\t}\n\n\t/**\n\t * 带有权重的随机生成器\n\t *\n\t * @param <T>        随机对象类型\n\t * @param weightObjs 带有权重的对象列表\n\t * @return {@link WeightRandom}\n\t * @since 4.0.3\n\t */\n\tpublic static <T> WeightRandom<T> weightRandom(final Iterable<WeightObj<T>> weightObjs) {\n\t\treturn new WeightRandom<>(weightObjs);\n\t}\n\t// endregion\n\n\t// region ----- randomDate\n\n\t/**\n\t * 以当天为基准，随机产生一个日期\n\t *\n\t * @param min 偏移最小天，可以为负数表示过去的时间（包含）\n\t * @param max 偏移最大天，可以为负数表示过去的时间（不包含）\n\t * @return 随机日期（随机天，其它时间不变）\n\t * @since 4.0.8\n\t */\n\tpublic static DateTime randomDay(final int min, final int max) {\n\t\treturn randomDate(DateUtil.date(), DateField.DAY_OF_YEAR, min, max);\n\t}\n\n\t/**\n\t * 以给定日期为基准，随机产生一个日期\n\t *\n\t * @param baseDate  基准日期\n\t * @param dateField 偏移的时间字段，例如时、分、秒等\n\t * @param min       偏移最小量，可以为负数表示过去的时间（包含）\n\t * @param max       偏移最大量，可以为负数表示过去的时间（不包含）\n\t * @return 随机日期\n\t * @since 4.5.8\n\t */\n\tpublic static DateTime randomDate(Date baseDate, final DateField dateField, final int min, final int max) {\n\t\tif (null == baseDate) {\n\t\t\tbaseDate = DateUtil.date();\n\t\t}\n\n\t\treturn DateUtil.offset(baseDate, dateField, randomInt(min, max));\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ReUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.comparator.LengthComparator;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.*;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutableObj;\nimport cn.hutool.core.map.MapUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.function.Consumer;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 正则相关工具类<br>\n * 常用正则请见 {@link Validator}\n *\n * @author xiaoleilu\n */\npublic class ReUtil {\n\n\t/**\n\t * 正则表达式匹配中文汉字\n\t */\n\tpublic final static String RE_CHINESE = RegexPool.CHINESE;\n\t/**\n\t * 正则表达式匹配中文字符串\n\t */\n\tpublic final static String RE_CHINESES = RegexPool.CHINESES;\n\n\t/**\n\t * 正则中需要被转义的关键字\n\t */\n\tpublic final static Set<Character> RE_KEYS = CollUtil.newHashSet('$', '(', ')', '*', '+', '.', '[', ']', '?', '\\\\', '^', '{', '}', '|');\n\n\t/**\n\t * 获得匹配的字符串，获得正则中分组0的内容\n\t *\n\t * @param regex   匹配的正则\n\t * @param content 被匹配的内容\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t * @since 3.1.2\n\t */\n\tpublic static String getGroup0(String regex, CharSequence content) {\n\t\treturn get(regex, content, 0);\n\t}\n\n\t/**\n\t * 获得匹配的字符串，获得正则中分组1的内容\n\t *\n\t * @param regex   匹配的正则\n\t * @param content 被匹配的内容\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t * @since 3.1.2\n\t */\n\tpublic static String getGroup1(String regex, CharSequence content) {\n\t\treturn get(regex, content, 1);\n\t}\n\n\t/**\n\t * 获得匹配的字符串\n\t *\n\t * @param regex      匹配的正则\n\t * @param content    被匹配的内容\n\t * @param groupIndex 匹配正则的分组序号\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t */\n\tpublic static String get(String regex, CharSequence content, int groupIndex) {\n\t\tif (null == content || null == regex) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn get(pattern, content, groupIndex);\n\t}\n\n\t/**\n\t * 获得匹配的字符串\n\t *\n\t * @param regex     匹配的正则\n\t * @param content   被匹配的内容\n\t * @param groupName 匹配正则的分组名称\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t */\n\tpublic static String get(String regex, CharSequence content, String groupName) {\n\t\tif (null == content || null == regex) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn get(pattern, content, groupName);\n\t}\n\n\t/**\n\t * 获得匹配的字符串，获得正则中分组0的内容\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被匹配的内容\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t * @since 3.1.2\n\t */\n\tpublic static String getGroup0(Pattern pattern, CharSequence content) {\n\t\treturn get(pattern, content, 0);\n\t}\n\n\t/**\n\t * 获得匹配的字符串，获得正则中分组1的内容\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被匹配的内容\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t * @since 3.1.2\n\t */\n\tpublic static String getGroup1(Pattern pattern, CharSequence content) {\n\t\treturn get(pattern, content, 1);\n\t}\n\n\t/**\n\t * 获得匹配的字符串，对应分组0表示整个匹配内容，1表示第一个括号分组内容，依次类推\n\t *\n\t * @param pattern    编译后的正则模式\n\t * @param content    被匹配的内容\n\t * @param groupIndex 匹配正则的分组序号，0表示整个匹配内容，1表示第一个括号分组内容，依次类推\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t */\n\tpublic static String get(Pattern pattern, CharSequence content, int groupIndex) {\n\t\tif (null == content || null == pattern) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal MutableObj<String> result = new MutableObj<>();\n\t\tget(pattern, content, matcher -> result.set(matcher.group(groupIndex)));\n\t\treturn result.get();\n\t}\n\n\t/**\n\t * 获得匹配的字符串\n\t *\n\t * @param pattern   匹配的正则\n\t * @param content   被匹配的内容\n\t * @param groupName 匹配正则的分组名称\n\t * @return 匹配后得到的字符串，未匹配返回null\n\t * @since 5.7.15\n\t */\n\tpublic static String get(Pattern pattern, CharSequence content, String groupName) {\n\t\tif (null == content || null == pattern || null == groupName) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal MutableObj<String> result = new MutableObj<>();\n\t\tget(pattern, content, matcher -> result.set(matcher.group(groupName)));\n\t\treturn result.get();\n\t}\n\n\t/**\n\t * 在给定字符串中查找给定规则的字符，如果找到则使用{@link Consumer}处理之<br>\n\t * 如果内容中有多个匹配项，则只处理找到的第一个结果。\n\t *\n\t * @param pattern  匹配的正则\n\t * @param content  被匹配的内容\n\t * @param consumer 匹配到的内容处理器\n\t * @since 5.7.15\n\t */\n\tpublic static void get(Pattern pattern, CharSequence content, Consumer<Matcher> consumer) {\n\t\tif (null == content || null == pattern || null == consumer) {\n\t\t\treturn;\n\t\t}\n\t\tfinal Matcher m = pattern.matcher(content);\n\t\tif (m.find()) {\n\t\t\tconsumer.accept(m);\n\t\t}\n\t}\n\n\t/**\n\t * 获得匹配的字符串匹配到的所有分组\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被匹配的内容\n\t * @return 匹配后得到的字符串数组，按照分组顺序依次列出，未匹配到返回空列表，任何一个参数为null返回null\n\t * @since 3.1.0\n\t */\n\tpublic static List<String> getAllGroups(Pattern pattern, CharSequence content) {\n\t\treturn getAllGroups(pattern, content, true);\n\t}\n\n\t/**\n\t * 获得匹配的字符串匹配到的所有分组\n\t *\n\t * @param pattern    编译后的正则模式\n\t * @param content    被匹配的内容\n\t * @param withGroup0 是否包括分组0，此分组表示全匹配的信息\n\t * @return 匹配后得到的字符串数组，按照分组顺序依次列出，未匹配到返回空列表，任何一个参数为null返回null\n\t * @since 4.0.13\n\t */\n\tpublic static List<String> getAllGroups(Pattern pattern, CharSequence content, boolean withGroup0) {\n\t\treturn getAllGroups(pattern, content, withGroup0, false);\n\t}\n\n\t/**\n\t * 获得匹配的字符串匹配到的所有分组\n\t *\n\t * @param pattern    编译后的正则模式\n\t * @param content    被匹配的内容\n\t * @param withGroup0 是否包括分组0，此分组表示全匹配的信息\n\t * @param findAll    是否查找所有匹配到的内容，{@code false}表示只读取第一个匹配到的内容\n\t * @return 匹配后得到的字符串数组，按照分组顺序依次列出，未匹配到返回空列表，任何一个参数为null返回null\n\t * @since 4.0.13\n\t */\n\tpublic static List<String> getAllGroups(Pattern pattern, CharSequence content, boolean withGroup0, boolean findAll) {\n\t\tif (null == content || null == pattern) {\n\t\t\treturn null;\n\t\t}\n\n\t\tArrayList<String> result = new ArrayList<>();\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\twhile (matcher.find()) {\n\t\t\tfinal int startGroup = withGroup0 ? 0 : 1;\n\t\t\tfinal int groupCount = matcher.groupCount();\n\t\t\tfor (int i = startGroup; i <= groupCount; i++) {\n\t\t\t\tresult.add(matcher.group(i));\n\t\t\t}\n\n\t\t\tif (false == findAll) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 根据给定正则查找字符串中的匹配项，返回所有匹配的分组名对应分组值<br>\n\t * <pre>\n\t * pattern: (?&lt;year&gt;\\\\d+)-(?&lt;month&gt;\\\\d+)-(?&lt;day&gt;\\\\d+)\n\t * content: 2021-10-11\n\t * result : year: 2021, month: 10, day: 11\n\t * </pre>\n\t *\n\t * @param pattern 匹配的正则\n\t * @param content 被匹配的内容\n\t * @return 命名捕获组，key为分组名，value为对应值\n\t * @since 5.7.15\n\t */\n\tpublic static Map<String, String> getAllGroupNames(Pattern pattern, CharSequence content) {\n\t\tif (null == content || null == pattern) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Matcher m = pattern.matcher(content);\n\t\tfinal Map<String, String> result = MapUtil.newHashMap(m.groupCount());\n\t\tif (m.find()) {\n\t\t\t// 通过反射获取 namedGroups 方法\n\t\t\tfinal Map<String, Integer> map = ReflectUtil.invoke(pattern, \"namedGroups\");\n\t\t\tmap.forEach((key, value) -> result.put(key, m.group(value)));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从content中匹配出多个值并根据template生成新的字符串<br>\n\t * 例如：<br>\n\t * content 2013年5月 pattern (.*?)年(.*?)月 template： $1-$2 return 2013-5\n\t *\n\t * @param pattern  匹配正则\n\t * @param content  被匹配的内容\n\t * @param template 生成内容模板，变量 $1 表示group1的内容，以此类推\n\t * @return 新字符串\n\t */\n\tpublic static String extractMulti(Pattern pattern, CharSequence content, String template) {\n\t\tif (null == content || null == pattern || null == template) {\n\t\t\treturn null;\n\t\t}\n\n\t\t//提取模板中的编号\n\t\tfinal TreeSet<Integer> varNums = new TreeSet<>((o1, o2) -> ObjectUtil.compare(o2, o1));\n\t\tfinal Matcher matcherForTemplate = PatternPool.GROUP_VAR.matcher(template);\n\t\twhile (matcherForTemplate.find()) {\n\t\t\tvarNums.add(Integer.parseInt(matcherForTemplate.group(1)));\n\t\t}\n\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\tif (matcher.find()) {\n\t\t\tfor (Integer group : varNums) {\n\t\t\t\ttemplate = template.replace(\"$\" + group, matcher.group(group));\n\t\t\t}\n\t\t\treturn template;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 从content中匹配出多个值并根据template生成新的字符串<br>\n\t * 匹配结束后会删除匹配内容之前的内容（包括匹配内容）<br>\n\t * 例如：<br>\n\t * content 2013年5月 pattern (.*?)年(.*?)月 template： $1-$2 return 2013-5\n\t *\n\t * @param regex    匹配正则字符串\n\t * @param content  被匹配的内容\n\t * @param template 生成内容模板，变量 $1 表示group1的内容，以此类推\n\t * @return 按照template拼接后的字符串\n\t */\n\tpublic static String extractMulti(String regex, CharSequence content, String template) {\n\t\tif (null == content || null == regex || null == template) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn extractMulti(pattern, content, template);\n\t}\n\n\t/**\n\t * 从content中匹配出多个值并根据template生成新的字符串<br>\n\t * 匹配结束后会删除匹配内容之前的内容（包括匹配内容）<br>\n\t * 例如：<br>\n\t * content 2013年5月 pattern (.*?)年(.*?)月 template： $1-$2 return 2013-5\n\t *\n\t * @param pattern       匹配正则\n\t * @param contentHolder 被匹配的内容的Holder，value为内容正文，经过这个方法的原文将被去掉匹配之前的内容\n\t * @param template      生成内容模板，变量 $1 表示group1的内容，以此类推\n\t * @return 新字符串\n\t * @since 5.8.0\n\t */\n\tpublic static String extractMultiAndDelPre(Pattern pattern, Mutable<CharSequence> contentHolder, String template) {\n\t\tif (null == contentHolder || null == pattern || null == template) {\n\t\t\treturn null;\n\t\t}\n\n\t\tHashSet<String> varNums = findAll(PatternPool.GROUP_VAR, template, 1, new HashSet<>());\n\n\t\tfinal CharSequence content = contentHolder.get();\n\t\tMatcher matcher = pattern.matcher(content);\n\t\tif (matcher.find()) {\n\t\t\tfor (String var : varNums) {\n\t\t\t\tint group = Integer.parseInt(var);\n\t\t\t\ttemplate = template.replace(\"$\" + var, matcher.group(group));\n\t\t\t}\n\t\t\tcontentHolder.set(StrUtil.sub(content, matcher.end(), content.length()));\n\t\t\treturn template;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 从content中匹配出多个值并根据template生成新的字符串<br>\n\t * 例如：<br>\n\t * content 2013年5月 pattern (.*?)年(.*?)月 template： $1-$2 return 2013-5\n\t *\n\t * @param regex         匹配正则字符串\n\t * @param contentHolder 被匹配的内容的Holder，value为内容正文，经过这个方法的原文将被去掉匹配之前的内容\n\t * @param template      生成内容模板，变量 $1 表示group1的内容，以此类推\n\t * @return 按照template拼接后的字符串\n\t */\n\tpublic static String extractMultiAndDelPre(String regex, Mutable<CharSequence> contentHolder, String template) {\n\t\tif (null == contentHolder || null == regex || null == template) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn extractMultiAndDelPre(pattern, contentHolder, template);\n\t}\n\n\t/**\n\t * 删除匹配的第一个内容\n\t *\n\t * @param regex   正则\n\t * @param content 被匹配的内容\n\t * @return 删除后剩余的内容\n\t */\n\tpublic static String delFirst(String regex, CharSequence content) {\n\t\tif (StrUtil.hasBlank(regex, content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn delFirst(pattern, content);\n\t}\n\n\t/**\n\t * 删除匹配的第一个内容\n\t *\n\t * @param pattern 正则\n\t * @param content 被匹配的内容\n\t * @return 删除后剩余的内容\n\t */\n\tpublic static String delFirst(Pattern pattern, CharSequence content) {\n\t\treturn replaceFirst(pattern, content, StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 替换匹配的第一个内容\n\t *\n\t * @param pattern     正则\n\t * @param content     被匹配的内容\n\t * @param replacement 替换的内容\n\t * @return 替换后剩余的内容\n\t * @since 5.6.5\n\t */\n\tpublic static String replaceFirst(Pattern pattern, CharSequence content, String replacement) {\n\t\tif (null == pattern || StrUtil.isEmpty(content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\treturn pattern.matcher(content).replaceFirst(replacement);\n\t}\n\n\t/**\n\t * 删除匹配的最后一个内容\n\t *\n\t * @param regex 正则\n\t * @param str   被匹配的内容\n\t * @return 删除后剩余的内容\n\t * @since 5.6.5\n\t */\n\tpublic static String delLast(String regex, CharSequence str) {\n\t\tif (StrUtil.hasBlank(regex, str)) {\n\t\t\treturn StrUtil.str(str);\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn delLast(pattern, str);\n\t}\n\n\t/**\n\t * 删除匹配的最后一个内容\n\t *\n\t * @param pattern 正则\n\t * @param str     被匹配的内容\n\t * @return 删除后剩余的内容\n\t * @since 5.6.5\n\t */\n\tpublic static String delLast(Pattern pattern, CharSequence str) {\n\t\tif (null != pattern && StrUtil.isNotEmpty(str)) {\n\t\t\tfinal MatchResult matchResult = lastIndexOf(pattern, str);\n\t\t\tif (null != matchResult) {\n\t\t\t\treturn StrUtil.subPre(str, matchResult.start()) + StrUtil.subSuf(str, matchResult.end());\n\t\t\t}\n\t\t}\n\n\t\treturn StrUtil.str(str);\n\t}\n\n\t/**\n\t * 删除匹配的全部内容\n\t *\n\t * @param regex   正则\n\t * @param content 被匹配的内容\n\t * @return 删除后剩余的内容\n\t */\n\tpublic static String delAll(String regex, CharSequence content) {\n\t\tif (StrUtil.hasEmpty(regex, content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn delAll(pattern, content);\n\t}\n\n\t/**\n\t * 删除匹配的全部内容\n\t *\n\t * @param pattern 正则\n\t * @param content 被匹配的内容\n\t * @return 删除后剩余的内容\n\t */\n\tpublic static String delAll(Pattern pattern, CharSequence content) {\n\t\tif (null == pattern || StrUtil.isEmpty(content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\treturn pattern.matcher(content).replaceAll(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 删除正则匹配到的内容之前的字符 如果没有找到，则返回原文\n\t *\n\t * @param regex   定位正则\n\t * @param content 被查找的内容\n\t * @return 删除前缀后的新内容\n\t */\n\tpublic static String delPre(String regex, CharSequence content) {\n\t\tif (null == content || null == regex) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn delPre(pattern, content);\n\t}\n\n\t/**\n\t * 删除正则匹配到的内容之前的字符 如果没有找到，则返回原文\n\t *\n\t * @param pattern 定位正则模式\n\t * @param content 被查找的内容\n\t * @return 删除前缀后的新内容\n\t */\n\tpublic static String delPre(Pattern pattern, CharSequence content) {\n\t\tif (null == content || null == pattern) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\tif (matcher.find()) {\n\t\t\treturn StrUtil.sub(content, matcher.end(), content.length());\n\t\t}\n\t\treturn StrUtil.str(content);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果，获得匹配的所有结果中正则对应分组0的内容\n\t *\n\t * @param regex   正则\n\t * @param content 被查找的内容\n\t * @return 结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> findAllGroup0(String regex, CharSequence content) {\n\t\treturn findAll(regex, content, 0);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果，获得匹配的所有结果中正则对应分组1的内容\n\t *\n\t * @param regex   正则\n\t * @param content 被查找的内容\n\t * @return 结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> findAllGroup1(String regex, CharSequence content) {\n\t\treturn findAll(regex, content, 1);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果\n\t *\n\t * @param regex   正则\n\t * @param content 被查找的内容\n\t * @param group   正则的分组\n\t * @return 结果列表\n\t * @since 3.0.6\n\t */\n\tpublic static List<String> findAll(String regex, CharSequence content, int group) {\n\t\treturn findAll(regex, content, group, new ArrayList<>());\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果\n\t *\n\t * @param <T>        集合类型\n\t * @param regex      正则\n\t * @param content    被查找的内容\n\t * @param group      正则的分组\n\t * @param collection 返回的集合类型\n\t * @return 结果集\n\t */\n\tpublic static <T extends Collection<String>> T findAll(String regex, CharSequence content, int group, T collection) {\n\t\tif (null == regex) {\n\t\t\treturn collection;\n\t\t}\n\n\t\treturn findAll(PatternPool.get(regex, Pattern.DOTALL), content, group, collection);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果，获得匹配的所有结果中正则对应分组0的内容\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被查找的内容\n\t * @return 结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> findAllGroup0(Pattern pattern, CharSequence content) {\n\t\treturn findAll(pattern, content, 0);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果，获得匹配的所有结果中正则对应分组1的内容\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被查找的内容\n\t * @return 结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> findAllGroup1(Pattern pattern, CharSequence content) {\n\t\treturn findAll(pattern, content, 1);\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被查找的内容\n\t * @param group   正则的分组\n\t * @return 结果列表\n\t * @since 3.0.6\n\t */\n\tpublic static List<String> findAll(Pattern pattern, CharSequence content, int group) {\n\t\treturn findAll(pattern, content, group, new ArrayList<>());\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果\n\t *\n\t * @param <T>        集合类型\n\t * @param pattern    编译后的正则模式\n\t * @param content    被查找的内容\n\t * @param group      正则的分组\n\t * @param collection 返回的集合类型\n\t * @return 结果集\n\t */\n\tpublic static <T extends Collection<String>> T findAll(Pattern pattern, CharSequence content, int group, T collection) {\n\t\tif (null == pattern || null == content) {\n\t\t\treturn null;\n\t\t}\n\t\tAssert.notNull(collection, \"Collection must be not null !\");\n\n\t\tfindAll(pattern, content, (matcher) -> collection.add(matcher.group(group)));\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 取得内容中匹配的所有结果，使用{@link Consumer}完成匹配结果处理\n\t *\n\t * @param pattern  编译后的正则模式\n\t * @param content  被查找的内容\n\t * @param consumer 匹配结果处理函数\n\t * @since 5.7.15\n\t */\n\tpublic static void findAll(Pattern pattern, CharSequence content, Consumer<Matcher> consumer) {\n\t\tif (null == pattern || null == content) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\twhile (matcher.find()) {\n\t\t\tconsumer.accept(matcher);\n\t\t}\n\t}\n\n\t/**\n\t * 计算指定字符串中，匹配pattern的个数\n\t *\n\t * @param regex   正则表达式\n\t * @param content 被查找的内容\n\t * @return 匹配个数\n\t */\n\tpublic static int count(String regex, CharSequence content) {\n\t\tif (null == regex || null == content) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn count(pattern, content);\n\t}\n\n\t/**\n\t * 计算指定字符串中，匹配pattern的个数\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被查找的内容\n\t * @return 匹配个数\n\t */\n\tpublic static int count(Pattern pattern, CharSequence content) {\n\t\tif (null == pattern || null == content) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tint count = 0;\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\twhile (matcher.find()) {\n\t\t\tcount++;\n\t\t}\n\n\t\treturn count;\n\t}\n\n\t/**\n\t * 指定内容中是否有表达式匹配的内容\n\t *\n\t * @param regex   正则表达式\n\t * @param content 被查找的内容\n\t * @return 指定内容中是否有表达式匹配的内容\n\t * @since 3.3.1\n\t */\n\tpublic static boolean contains(String regex, CharSequence content) {\n\t\tif (null == regex || null == content) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn contains(pattern, content);\n\t}\n\n\t/**\n\t * 指定内容中是否有表达式匹配的内容\n\t *\n\t * @param pattern 编译后的正则模式\n\t * @param content 被查找的内容\n\t * @return 指定内容中是否有表达式匹配的内容\n\t * @since 3.3.1\n\t */\n\tpublic static boolean contains(Pattern pattern, CharSequence content) {\n\t\tif (null == pattern || null == content) {\n\t\t\treturn false;\n\t\t}\n\t\treturn pattern.matcher(content).find();\n\t}\n\n\t/**\n\t * 找到指定正则匹配到字符串的开始位置\n\t *\n\t * @param regex   正则\n\t * @param content 字符串\n\t * @return 位置，{@code null}表示未找到\n\t * @since 5.6.5\n\t */\n\tpublic static MatchResult indexOf(String regex, CharSequence content) {\n\t\tif (null == regex || null == content) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn indexOf(pattern, content);\n\t}\n\n\t/**\n\t * 找到指定模式匹配到字符串的开始位置\n\t *\n\t * @param pattern 模式\n\t * @param content 字符串\n\t * @return 位置，{@code null}表示未找到\n\t * @since 5.6.5\n\t */\n\tpublic static MatchResult indexOf(Pattern pattern, CharSequence content) {\n\t\tif (null != pattern && null != content) {\n\t\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\t\tif (matcher.find()) {\n\t\t\t\treturn matcher.toMatchResult();\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 找到指定正则匹配到第一个字符串的位置\n\t *\n\t * @param regex   正则\n\t * @param content 字符串\n\t * @return 位置，{@code null}表示未找到\n\t * @since 5.6.5\n\t */\n\tpublic static MatchResult lastIndexOf(String regex, CharSequence content) {\n\t\tif (null == regex || null == content) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn lastIndexOf(pattern, content);\n\t}\n\n\t/**\n\t * 找到指定模式匹配到最后一个字符串的位置\n\t *\n\t * @param pattern 模式\n\t * @param content 字符串\n\t * @return 位置，{@code null}表示未找到\n\t * @since 5.6.5\n\t */\n\tpublic static MatchResult lastIndexOf(Pattern pattern, CharSequence content) {\n\t\tMatchResult result = null;\n\t\tif (null != pattern && null != content) {\n\t\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\t\twhile (matcher.find()) {\n\t\t\t\tresult = matcher.toMatchResult();\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * 从字符串中获得第一个整数\n\t *\n\t * @param StringWithNumber 带数字的字符串\n\t * @return 整数\n\t */\n\tpublic static Integer getFirstNumber(CharSequence StringWithNumber) {\n\t\treturn Convert.toInt(get(PatternPool.NUMBERS, StringWithNumber, 0), null);\n\t}\n\n\t/**\n\t * 给定内容是否匹配正则\n\t *\n\t * @param regex   正则\n\t * @param content 内容\n\t * @return 正则为null或者\"\"则不检查，返回true，内容为null返回false\n\t */\n\tpublic static boolean isMatch(String regex, CharSequence content) {\n\t\tif (content == null) {\n\t\t\t// 提供null的字符串为不匹配\n\t\t\treturn false;\n\t\t}\n\n\t\tif (StrUtil.isEmpty(regex)) {\n\t\t\t// 正则不存在则为全匹配\n\t\t\treturn true;\n\t\t}\n\n\t\t// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);\n\t\tfinal Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);\n\t\treturn isMatch(pattern, content);\n\t}\n\n\t/**\n\t * 给定内容是否匹配正则\n\t *\n\t * @param pattern 模式\n\t * @param content 内容\n\t * @return 正则为null或者\"\"则不检查，返回true，内容为null返回false\n\t */\n\tpublic static boolean isMatch(Pattern pattern, CharSequence content) {\n\t\tif (content == null || pattern == null) {\n\t\t\t// 提供null的字符串为不匹配\n\t\t\treturn false;\n\t\t}\n\t\treturn pattern.matcher(content).matches();\n\t}\n\n\t/**\n\t * 正则替换指定值<br>\n\t * 通过正则查找到字符串，然后把匹配到的字符串加入到replacementTemplate中，$1表示分组1的字符串\n\t *\n\t * <p>\n\t * 例如：原字符串是：中文1234，我想把1234换成(1234)，则可以：\n\t *\n\t * <pre>\n\t * ReUtil.replaceAll(\"中文1234\", \"(\\\\d+)\", \"($1)\"))\n\t *\n\t * 结果：中文(1234)\n\t * </pre>\n\t *\n\t * @param content             文本\n\t * @param regex               正则\n\t * @param replacementTemplate 替换的文本模板，可以使用$1类似的变量提取正则匹配出的内容\n\t * @return 处理后的文本\n\t */\n\tpublic static String replaceAll(CharSequence content, String regex, String replacementTemplate) {\n\t\tfinal Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);\n\t\treturn replaceAll(content, pattern, replacementTemplate);\n\t}\n\n\t/**\n\t * 正则替换指定值<br>\n\t * 通过正则查找到字符串，然后把匹配到的字符串加入到replacementTemplate中，$1表示分组1的字符串\n\t *\n\t * @param content             文本\n\t * @param pattern             {@link Pattern}\n\t * @param replacementTemplate 替换的文本模板，可以使用$1类似的变量提取正则匹配出的内容\n\t * @return 处理后的文本\n\t * @since 3.0.4\n\t */\n\tpublic static String replaceAll(CharSequence content, Pattern pattern, String replacementTemplate) {\n\t\tif (StrUtil.isEmpty(content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal Matcher matcher = pattern.matcher(content);\n\t\tboolean result = matcher.find();\n\t\tif (result) {\n\t\t\tfinal Set<String> varNums = findAll(PatternPool.GROUP_VAR, replacementTemplate, 1, new TreeSet<>(LengthComparator.INSTANCE.reversed()));\n\t\t\tfinal StringBuffer sb = new StringBuffer();\n\t\t\tdo {\n\t\t\t\tString replacement = replacementTemplate;\n\t\t\t\tfor (final String var : varNums) {\n\t\t\t\t\tfinal int group = Integer.parseInt(var);\n\t\t\t\t\t//replacement = replacement.replace(\"$\" + var, matcher.group(group));\n\t\t\t\t\treplacement = StrUtil.replace(replacement, \"$\" + var, matcher.group(group));\n\t\t\t\t}\n\t\t\t\tmatcher.appendReplacement(sb, escape(replacement));\n\t\t\t\tresult = matcher.find();\n\t\t\t} while (result);\n\t\t\tmatcher.appendTail(sb);\n\t\t\treturn sb.toString();\n\t\t}\n\t\treturn StrUtil.str(content);\n\t}\n\n\t/**\n\t * 替换所有正则匹配的文本，并使用自定义函数决定如何替换<br>\n\t * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分，然后经过重新处理、组装变成新的内容放回原位。\n\t *\n\t * <pre class=\"code\">\n\t *     replaceAll(this.content, \"(\\\\d+)\", parameters -&gt; \"-\" + parameters.group(1) + \"-\")\n\t *     // 结果为：\"ZZZaaabbbccc中文-1234-\"\n\t * </pre>\n\t *\n\t * @param str        要替换的字符串\n\t * @param regex      用于匹配的正则式\n\t * @param replaceFun 决定如何替换的函数\n\t * @return 替换后的文本\n\t * @since 4.2.2\n\t */\n\tpublic static String replaceAll(CharSequence str, String regex, Func1<Matcher, String> replaceFun) {\n\t\treturn replaceAll(str, Pattern.compile(regex), replaceFun);\n\t}\n\n\t/**\n\t * 替换所有正则匹配的文本，并使用自定义函数决定如何替换<br>\n\t * replaceFun可以通过{@link Matcher}提取出匹配到的内容的不同部分，然后经过重新处理、组装变成新的内容放回原位。\n\t *\n\t * <pre class=\"code\">\n\t *     replaceAll(this.content, \"(\\\\d+)\", parameters -&gt; \"-\" + parameters.group(1) + \"-\")\n\t *     // 结果为：\"ZZZaaabbbccc中文-1234-\"\n\t * </pre>\n\t *\n\t * @param str        要替换的字符串\n\t * @param pattern    用于匹配的正则式\n\t * @param replaceFun 决定如何替换的函数,可能被多次调用（当有多个匹配时）\n\t * @return 替换后的字符串\n\t * @since 4.2.2\n\t */\n\tpublic static String replaceAll(CharSequence str, Pattern pattern, Func1<Matcher, String> replaceFun) {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\treturn StrUtil.str(str);\n\t\t}\n\n\t\tfinal Matcher matcher = pattern.matcher(str);\n\t\tfinal StringBuffer buffer = new StringBuffer();\n\t\twhile (matcher.find()) {\n\t\t\ttry {\n\t\t\t\tmatcher.appendReplacement(buffer, replaceFun.call(matcher));\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t\tmatcher.appendTail(buffer);\n\t\treturn buffer.toString();\n\t}\n\n\t/**\n\t * 转义字符，将正则的关键字转义\n\t *\n\t * @param c 字符\n\t * @return 转义后的文本\n\t */\n\tpublic static String escape(char c) {\n\t\tfinal StringBuilder builder = new StringBuilder();\n\t\tif (RE_KEYS.contains(c)) {\n\t\t\tbuilder.append('\\\\');\n\t\t}\n\t\tbuilder.append(c);\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 转义字符串，将正则的关键字转义\n\t *\n\t * @param content 文本\n\t * @return 转义后的文本\n\t */\n\tpublic static String escape(CharSequence content) {\n\t\tif (StrUtil.isBlank(content)) {\n\t\t\treturn StrUtil.str(content);\n\t\t}\n\n\t\tfinal StringBuilder builder = new StringBuilder();\n\t\tint len = content.length();\n\t\tchar current;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tcurrent = content.charAt(i);\n\t\t\tif (RE_KEYS.contains(current)) {\n\t\t\t\tbuilder.append('\\\\');\n\t\t\t}\n\t\t\tbuilder.append(current);\n\t\t}\n\t\treturn builder.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ReferenceUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.ref.Ref;\n\nimport java.lang.ref.*;\n\n/**\n * 引用工具类，主要针对{@link Reference} 工具化封装<br>\n * 主要封装包括：\n * <pre>\n * 1. {@link SoftReference} 软引用，在GC报告内存不足时会被GC回收\n * 2. {@link WeakReference} 弱引用，在GC时发现弱引用会回收其对象\n * 3. {@link PhantomReference} 虚引用，在GC时发现虚引用对象，会将{@link PhantomReference}插入{@link ReferenceQueue}。 此时对象未被真正回收，要等到{@link ReferenceQueue}被真正处理后才会被回收。\n * </pre>\n *\n * @author looly\n * @since 3.1.2\n */\npublic class ReferenceUtil {\n\n\t/**\n\t * 获得引用\n\t *\n\t * @param <T> 被引用对象类型\n\t * @param type 引用类型枚举\n\t * @param referent 被引用对象\n\t * @return {@link Reference}\n\t */\n\tpublic static <T> Reference<T> create(ReferenceType type, T referent) {\n\t\treturn create(type, referent, null);\n\t}\n\n\t/**\n\t * 获得引用\n\t *\n\t * @param <T> 被引用对象类型\n\t * @param type 引用类型枚举\n\t * @param referent 被引用对象\n\t * @param queue 引用队列\n\t * @return {@link Reference}\n\t */\n\tpublic static <T> Reference<T> create(ReferenceType type, T referent, ReferenceQueue<T> queue) {\n\t\tswitch (type) {\n\t\tcase SOFT:\n\t\t\treturn new SoftReference<>(referent, queue);\n\t\tcase WEAK:\n\t\t\treturn new WeakReference<>(referent, queue);\n\t\tcase PHANTOM:\n\t\t\treturn new PhantomReference<>(referent, queue);\n\t\tdefault:\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * {@code null}全的解包获取原始对象\n\t *\n\t * @param <T> 对象类型\n\t * @param obj Reference对象\n\t * @return 原始对象 or {@code null}\n\t * @since 5.8.41\n\t */\n\tpublic static <T> T get(final Reference<T> obj) {\n\t\treturn ObjUtil.apply(obj, Reference::get);\n\t}\n\n\t/**\n\t * {@code null}安全的解包获取原始对象\n\t *\n\t * @param <T> 对象类型\n\t * @param obj Ref对象\n\t * @return 原始对象 or {@code null}\n\t * @since 5.8.41\n\t */\n\tpublic static <T> T get(final Ref<T> obj) {\n\t\treturn ObjUtil.apply(obj, Ref::get);\n\t}\n\n\t/**\n\t * 引用类型\n\t *\n\t * @author looly\n\t *\n\t */\n\tpublic enum ReferenceType {\n\t\t/** 软引用，在GC报告内存不足时会被GC回收 */\n\t\tSOFT,\n\t\t/** 弱引用，在GC时发现弱引用会回收其对象 */\n\t\tWEAK,\n\t\t/**\n\t\t * 虚引用，在GC时发现虚引用对象，会将{@link PhantomReference}插入{@link ReferenceQueue}。 <br>\n\t\t * 此时对象未被真正回收，要等到{@link ReferenceQueue}被真正处理后才会被回收。\n\t\t */\n\t\tPHANTOM\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.bean.NullWrapperBean;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.UniqueKeySet;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.exceptions.InvocationTargetRuntimeException;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.reflect.MethodHandleUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\n\nimport java.lang.reflect.*;\nimport java.util.*;\n\n/**\n * 反射工具类\n *\n * @author Looly\n * @since 3.0.9\n */\npublic class ReflectUtil {\n\n\t/**\n\t * 构造对象缓存\n\t */\n\tprivate static final WeakKeyValueConcurrentMap<Class<?>, Constructor<?>[]> CONSTRUCTORS_CACHE = new WeakKeyValueConcurrentMap<>();\n\t/**\n\t * 字段缓存\n\t */\n\tprivate static final WeakKeyValueConcurrentMap<Class<?>, Field[]> FIELDS_CACHE = new WeakKeyValueConcurrentMap<>();\n\t/**\n\t * 方法缓存\n\t */\n\tprivate static final WeakKeyValueConcurrentMap<Class<?>, Method[]> METHODS_CACHE = new WeakKeyValueConcurrentMap<>();\n\t/**\n\t * 方法查找结果缓存（新增：细粒度缓存，避免重复遍历）\n\t * key: 方法查找键（类+是否忽略大小写+方法名+参数类型）\n\t * value: 查找到的Method\n\t */\n\tprivate static final WeakKeyValueConcurrentMap<MethodLookupKey, Method> METHOD_LOOKUP_CACHE = new WeakKeyValueConcurrentMap<>();\n\n\t// --------------------------------------------------------------------------------------------------------- Constructor\n\n\t/**\n\t * 查找类中的指定参数的构造方法，如果找到构造方法，会自动设置可访问为true\n\t *\n\t * @param <T>            对象类型\n\t * @param clazz          类\n\t * @param parameterTypes 参数类型，只要任何一个参数是指定参数的父类或接口或相等即可，此参数可以不传\n\t * @return 构造方法，如果未找到返回null\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... parameterTypes) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Constructor<?>[] constructors = getConstructors(clazz);\n\t\tClass<?>[] pts;\n\t\tfor (Constructor<?> constructor : constructors) {\n\t\t\tpts = constructor.getParameterTypes();\n\t\t\tif (ClassUtil.isAllAssignableFrom(pts, parameterTypes)) {\n\t\t\t\t// 构造可访问\n\t\t\t\tsetAccessible(constructor);\n\t\t\t\treturn (Constructor<T>) constructor;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得一个类中所有构造列表\n\t *\n\t * @param <T>       构造的对象类型\n\t * @param beanClass 类，非{@code null}\n\t * @return 字段列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> Constructor<T>[] getConstructors(Class<T> beanClass) throws SecurityException {\n\t\tAssert.notNull(beanClass);\n\t\treturn (Constructor<T>[]) CONSTRUCTORS_CACHE.computeIfAbsent(beanClass, (key) -> getConstructorsDirectly(beanClass));\n\t}\n\n\t/**\n\t * 获得一个类中所有构造列表，直接反射获取，无缓存\n\t *\n\t * @param beanClass 类\n\t * @return 字段列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\tpublic static Constructor<?>[] getConstructorsDirectly(Class<?> beanClass) throws SecurityException {\n\t\treturn beanClass.getDeclaredConstructors();\n\t}\n\n\t// --------------------------------------------------------------------------------------------------------- Field\n\n\t/**\n\t * 查找指定类中是否包含指定名称对应的字段，包括所有字段（包括非public字段），也包括父类和Object类的字段\n\t *\n\t * @param beanClass 被查找字段的类,不能为null\n\t * @param name      字段名\n\t * @return 是否包含字段\n\t * @throws SecurityException 安全异常\n\t * @since 4.1.21\n\t */\n\tpublic static boolean hasField(Class<?> beanClass, String name) throws SecurityException {\n\t\treturn null != getField(beanClass, name);\n\t}\n\n\t/**\n\t * 获取字段名，如果存在{@link Alias}注解，读取注解的值作为名称\n\t *\n\t * @param field 字段\n\t * @return 字段名\n\t * @since 5.1.6\n\t */\n\tpublic static String getFieldName(Field field) {\n\t\tif (null == field) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Alias alias = field.getAnnotation(Alias.class);\n\t\tif (null != alias) {\n\t\t\treturn alias.value();\n\t\t}\n\n\t\treturn field.getName();\n\t}\n\n\t/**\n\t * 查找指定类中的指定name的字段（包括非public字段），也包括父类和Object类的字段， 字段不存在则返回{@code null}\n\t *\n\t * @param beanClass 被查找字段的类,不能为null\n\t * @param name      字段名\n\t * @return 字段\n\t * @throws SecurityException 安全异常\n\t */\n\tpublic static Field getField(Class<?> beanClass, String name) throws SecurityException {\n\t\tfinal Field[] fields = getFields(beanClass);\n\t\treturn ArrayUtil.firstMatch((field) -> name.equals(getFieldName(field)), fields);\n\t}\n\n\t/**\n\t * 获取指定类中字段名和字段对应的有序Map，包括其父类中的字段<br>\n\t * 如果子类与父类中存在同名字段，则后者覆盖前者。\n\t *\n\t * @param beanClass 类\n\t * @return 字段名和字段对应的Map，有序\n\t * @since 5.0.7\n\t */\n\tpublic static Map<String, Field> getFieldMap(Class<?> beanClass) {\n\t\tfinal Field[] fields = getFields(beanClass);\n\t\tfinal HashMap<String, Field> map = MapUtil.newHashMap(fields.length, true);\n\t\tfor (Field field : fields) {\n\t\t\tmap.put(field.getName(), field);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 获得一个类中所有字段列表，包括其父类中的字段<br>\n\t * 如果子类与父类中存在同名字段，则这两个字段同时存在，子类字段在前，父类字段在后。\n\t *\n\t * @param beanClass 类\n\t * @return 字段列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\tpublic static Field[] getFields(Class<?> beanClass) throws SecurityException {\n\t\tAssert.notNull(beanClass);\n\t\treturn FIELDS_CACHE.computeIfAbsent(beanClass, (key) -> getFieldsDirectly(beanClass, true));\n\t}\n\n\n\t/**\n\t * 获得一个类中所有满足条件的字段列表，包括其父类中的字段<br>\n\t * 如果子类与父类中存在同名字段，则这两个字段同时存在，子类字段在前，父类字段在后。\n\t *\n\t * @param beanClass   类\n\t * @param fieldFilter field过滤器，过滤掉不需要的field，{@code null}返回原集合\n\t * @return 字段列表\n\t * @throws SecurityException 安全检查异常\n\t * @since 5.7.14\n\t */\n\tpublic static Field[] getFields(Class<?> beanClass, Filter<Field> fieldFilter) throws SecurityException {\n\t\treturn ArrayUtil.filter(getFields(beanClass), fieldFilter);\n\t}\n\n\t/**\n\t * 获得一个类中所有字段列表，直接反射获取，无缓存<br>\n\t * 如果子类与父类中存在同名字段，则这两个字段同时存在，子类字段在前，父类字段在后。\n\t *\n\t * @param beanClass            类\n\t * @param withSuperClassFields 是否包括父类的字段列表\n\t * @return 字段列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\tpublic static Field[] getFieldsDirectly(Class<?> beanClass, boolean withSuperClassFields) throws SecurityException {\n\t\tAssert.notNull(beanClass);\n\n\t\tField[] allFields = null;\n\t\tClass<?> searchType = beanClass;\n\t\tField[] declaredFields;\n\t\twhile (searchType != null) {\n\t\t\tdeclaredFields = searchType.getDeclaredFields();\n\t\t\tif (null == allFields) {\n\t\t\t\tallFields = declaredFields;\n\t\t\t} else {\n\t\t\t\tallFields = ArrayUtil.append(allFields, declaredFields);\n\t\t\t}\n\t\t\tsearchType = withSuperClassFields ? searchType.getSuperclass() : null;\n\t\t}\n\n\t\treturn allFields;\n\t}\n\n\t/**\n\t * 获取字段值\n\t *\n\t * @param obj       对象，如果static字段，此处为类\n\t * @param fieldName 字段名\n\t * @return 字段值\n\t * @throws UtilException 包装IllegalAccessException异常\n\t */\n\tpublic static Object getFieldValue(Object obj, String fieldName) throws UtilException {\n\t\tif (null == obj || StrUtil.isBlank(fieldName)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getFieldValue(obj, getField(obj instanceof Class ? (Class<?>) obj : obj.getClass(), fieldName));\n\t}\n\n\t/**\n\t * 获取静态字段值\n\t *\n\t * @param field 字段\n\t * @return 字段值\n\t * @throws UtilException 包装IllegalAccessException异常\n\t * @since 5.1.0\n\t */\n\tpublic static Object getStaticFieldValue(Field field) throws UtilException {\n\t\treturn getFieldValue(null, field);\n\t}\n\n\t/**\n\t * 获取字段值\n\t *\n\t * @param obj   对象，static字段则此字段为null\n\t * @param field 字段\n\t * @return 字段值\n\t * @throws UtilException 包装IllegalAccessException异常\n\t */\n\tpublic static Object getFieldValue(Object obj, Field field) throws UtilException {\n\t\tif (null == field) {\n\t\t\treturn null;\n\t\t}\n\t\tif (obj instanceof Class) {\n\t\t\t// 静态字段获取时对象为null\n\t\t\tobj = null;\n\t\t}\n\n\t\tsetAccessible(field);\n\t\tObject result;\n\t\ttry {\n\t\t\tresult = field.get(obj);\n\t\t} catch (IllegalAccessException e) {\n\t\t\tthrow new UtilException(e, \"IllegalAccess for {}.{}\", field.getDeclaringClass(), field.getName());\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取所有字段的值\n\t *\n\t * @param obj bean对象，如果是static字段，此处为类class\n\t * @return 字段值数组\n\t * @since 4.1.17\n\t */\n\tpublic static Object[] getFieldsValue(Object obj) {\n\t\treturn getFieldsValue(obj, null);\n\t}\n\n\t/**\n\t * 获取所有字段的值\n\t *\n\t * @param obj    bean对象，如果是static字段，此处为类class\n\t * @param filter 字段过滤器，，{@code null}返回原集合\n\t * @return 字段值数组\n\t * @since 5.8.23\n\t */\n\tpublic static Object[] getFieldsValue(Object obj, Filter<Field> filter) {\n\t\tif (null != obj) {\n\t\t\tfinal Field[] fields = getFields(obj instanceof Class ? (Class<?>) obj : obj.getClass(), filter);\n\t\t\tif (null != fields) {\n\t\t\t\treturn ArrayUtil.map(fields, Object.class, field -> getFieldValue(obj, field));\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 设置字段值<br>\n\t * 若值类型与字段类型不一致，则会尝试通过 {@link Convert} 进行转换<br>\n\t * 若字段类型是原始类型而传入的值是 null，则会将字段设置为对应原始类型的默认值（见 {@link ClassUtil#getDefaultValue(Class)}）\n\t * 如果是final字段，setFieldValue，调用这可以先调用 {@link ReflectUtil#removeFinalModify(Field)}方法去除final修饰符<br>\n\t *\n\t * @param obj       对象,static字段则此处传Class\n\t * @param fieldName 字段名\n\t * @param value     值，当值类型与字段类型不匹配时，会尝试转换\n\t * @throws UtilException 包装IllegalAccessException异常\n\t */\n\tpublic static void setFieldValue(Object obj, String fieldName, Object value) throws UtilException {\n\t\tAssert.notNull(obj);\n\t\tAssert.notBlank(fieldName);\n\n\t\tfinal Field field = getField((obj instanceof Class) ? (Class<?>) obj : obj.getClass(), fieldName);\n\t\tAssert.notNull(field, \"Field [{}] is not exist in [{}]\", fieldName, obj.getClass().getName());\n\t\tsetFieldValue(obj, field, value);\n\t}\n\n\t/**\n\t * 设置字段值<br>\n\t * 若值类型与字段类型不一致，则会尝试通过 {@link Convert} 进行转换<br>\n\t * 若字段类型是原始类型而传入的值是 null，则会将字段设置为对应原始类型的默认值（见 {@link ClassUtil#getDefaultValue(Class)}）<br>\n\t * 如果是final字段，setFieldValue，调用这可以先调用 {@link ReflectUtil#removeFinalModify(Field)}方法去除final修饰符\n\t *\n\t * @param obj   对象，如果是static字段，此参数为null\n\t * @param field 字段\n\t * @param value 值，当值类型与字段类型不匹配时，会尝试转换\n\t * @throws UtilException UtilException 包装IllegalAccessException异常\n\t */\n\tpublic static void setFieldValue(Object obj, Field field, Object value) throws UtilException {\n\t\tAssert.notNull(field, \"Field in [{}] not exist !\", obj);\n\n\t\tfinal Class<?> fieldType = field.getType();\n\t\tif (null != value) {\n\t\t\tif (false == fieldType.isAssignableFrom(value.getClass())) {\n\t\t\t\t//对于类型不同的字段，尝试转换，转换失败则使用原对象类型\n\t\t\t\tfinal Object targetValue = Convert.convert(fieldType, value);\n\t\t\t\tif (null != targetValue) {\n\t\t\t\t\tvalue = targetValue;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 获取null对应默认值，防止原始类型造成空指针问题\n\t\t\tvalue = ClassUtil.getDefaultValue(fieldType);\n\t\t}\n\n\t\tsetAccessible(field);\n\t\ttry {\n\t\t\tfield.set(obj instanceof Class ? null : obj, value);\n\t\t} catch (IllegalAccessException e) {\n\t\t\tthrow new UtilException(e, \"IllegalAccess for {}.{}\", obj, field.getName());\n\t\t}\n\t}\n\n\t/**\n\t * 是否为父类引用字段<br>\n\t * 当字段所在类是对象子类时（对象中定义的非static的class），会自动生成一个以\"this$0\"为名称的字段，指向父类对象\n\t *\n\t * @param field 字段\n\t * @return 是否为父类引用字段\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isOuterClassField(Field field) {\n\t\treturn \"this$0\".equals(field.getName());\n\t}\n\n\t// --------------------------------------------------------------------------------------------------------- method\n\n\t/**\n\t * 获得指定类本类及其父类中的Public方法名<br>\n\t * 去重重载的方法\n\t *\n\t * @param clazz 类\n\t * @return 方法名Set\n\t */\n\tpublic static Set<String> getPublicMethodNames(Class<?> clazz) {\n\t\tfinal HashSet<String> methodSet = new HashSet<>();\n\t\tfinal Method[] methodArray = getPublicMethods(clazz);\n\t\tif (ArrayUtil.isNotEmpty(methodArray)) {\n\t\t\tfor (Method method : methodArray) {\n\t\t\t\tmethodSet.add(method.getName());\n\t\t\t}\n\t\t}\n\t\treturn methodSet;\n\t}\n\n\t/**\n\t * 获得本类及其父类所有Public方法\n\t *\n\t * @param clazz 查找方法的类\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static Method[] getPublicMethods(Class<?> clazz) {\n\t\treturn null == clazz ? null : clazz.getMethods();\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表<br>\n\t * TODO 6.x此方法更改返回Method[]\n\t *\n\t * @param clazz  查找方法的类\n\t * @param filter 过滤器\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, Filter<Method> filter) {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Method[] methods = getPublicMethods(clazz);\n\t\tList<Method> methodList;\n\t\tif (null != filter) {\n\t\t\tmethodList = new ArrayList<>();\n\t\t\tfor (Method method : methods) {\n\t\t\t\tif (filter.accept(method)) {\n\t\t\t\t\tmethodList.add(method);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmethodList = CollUtil.newArrayList(methods);\n\t\t}\n\t\treturn methodList;\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表\n\t *\n\t * @param clazz          查找方法的类\n\t * @param excludeMethods 不包括的方法\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, Method... excludeMethods) {\n\t\tfinal HashSet<Method> excludeMethodSet = CollUtil.newHashSet(excludeMethods);\n\t\treturn getPublicMethods(clazz, method -> false == excludeMethodSet.contains(method));\n\t}\n\n\t/**\n\t * 获得指定类过滤后的Public方法列表\n\t *\n\t * @param clazz              查找方法的类\n\t * @param excludeMethodNames 不包括的方法名列表\n\t * @return 过滤后的方法列表\n\t */\n\tpublic static List<Method> getPublicMethods(Class<?> clazz, String... excludeMethodNames) {\n\t\tfinal HashSet<String> excludeMethodNameSet = CollUtil.newHashSet(excludeMethodNames);\n\t\treturn getPublicMethods(clazz, method -> false == excludeMethodNameSet.contains(method.getName()));\n\t}\n\n\t/**\n\t * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回{@code null}\n\t *\n\t * @param clazz      类\n\t * @param methodName 方法名\n\t * @param paramTypes 参数类型\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t */\n\tpublic static Method getPublicMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) throws SecurityException {\n\t\ttry {\n\t\t\treturn clazz.getMethod(methodName, paramTypes);\n\t\t} catch (NoSuchMethodException ex) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 查找指定对象中的所有方法（包括非public方法），也包括父对象和Object类的方法\n\t *\n\t * <p>\n\t * 此方法为精准获取方法名，即方法名和参数数量和类型必须一致，否则返回{@code null}。\n\t * </p>\n\t *\n\t * @param obj        被查找的对象，如果为{@code null}返回{@code null}\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @param args       参数\n\t * @return 方法\n\t * @throws SecurityException 无访问权限抛出异常\n\t */\n\tpublic static Method getMethodOfObj(Object obj, String methodName, Object... args) throws SecurityException {\n\t\tif (null == obj || StrUtil.isBlank(methodName)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getMethod(obj.getClass(), methodName, ClassUtil.getClasses(args));\n\t}\n\n\t/**\n\t * 忽略大小写查找指定方法，如果找不到对应的方法则返回{@code null}\n\t *\n\t * <p>\n\t * 此方法为精准获取方法名，即方法名和参数数量和类型必须一致，否则返回{@code null}。\n\t * </p>\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @param paramTypes 参数类型，指定参数类型如果是方法的子类也算\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t * @since 3.2.0\n\t */\n\tpublic static Method getMethodIgnoreCase(Class<?> clazz, String methodName, Class<?>... paramTypes) throws SecurityException {\n\t\treturn getMethod(clazz, true, methodName, paramTypes);\n\t}\n\n\t/**\n\t * 查找指定方法 如果找不到对应的方法则返回{@code null}\n\t *\n\t * <p>\n\t * 此方法为精准获取方法名，即方法名和参数数量和类型必须一致，否则返回{@code null}。\n\t * </p>\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @param paramTypes 参数类型，指定参数类型如果是方法的子类也算\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t */\n\tpublic static Method getMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) throws SecurityException {\n\t\treturn getMethod(clazz, false, methodName, paramTypes);\n\t}\n\n\t/**\n\t * 查找指定方法 如果找不到对应的方法则返回{@code null}<br>\n\t * 此方法为精准获取方法名，即方法名和参数数量和类型必须一致，否则返回{@code null}。<br>\n\t * 如果查找的方法有多个同参数类型重载，查找第一个找到的方法\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param ignoreCase 是否忽略大小写\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @param paramTypes 参数类型，指定参数类型如果是方法的子类也算\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t * @since 3.2.0\n\t */\n\tpublic static Method getMethod(Class<?> clazz, boolean ignoreCase, String methodName, Class<?>... paramTypes) throws SecurityException {\n\t\tif (null == clazz || StrUtil.isBlank(methodName)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 优先从细粒度缓存查找\n\t\tfinal MethodLookupKey key = new MethodLookupKey(clazz, ignoreCase, methodName, paramTypes);\n\t\treturn METHOD_LOOKUP_CACHE.computeIfAbsent(key, (lookupKey) -> {\n\t\t\t// 缓存未命中时，执行原有的查找逻辑\n\t\t\tMethod res = null;\n\t\t\tfinal Method[] methods = getMethods(clazz);\n\t\t\tif (ArrayUtil.isNotEmpty(methods)) {\n\t\t\t\tfor (Method method : methods) {\n\t\t\t\t\tif (StrUtil.equals(methodName, method.getName(), ignoreCase)\n\t\t\t\t\t\t&& ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes)\n\t\t\t\t\t\t//排除协变桥接方法，pr#1965@Github\n\t\t\t\t\t\t&& (res == null\n\t\t\t\t\t\t|| res.getReturnType().isAssignableFrom(method.getReturnType()))) {\n\t\t\t\t\t\tres = method;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn res;\n\t\t});\n\t}\n\n\t/**\n\t * 按照方法名查找指定方法名的方法，只返回匹配到的第一个方法，如果找不到对应的方法则返回{@code null}\n\t *\n\t * <p>\n\t * 此方法只检查方法名是否一致，并不检查参数的一致性。\n\t * </p>\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t * @since 4.3.2\n\t */\n\tpublic static Method getMethodByName(Class<?> clazz, String methodName) throws SecurityException {\n\t\treturn getMethodByName(clazz, false, methodName);\n\t}\n\n\t/**\n\t * 按照方法名查找指定方法名的方法，只返回匹配到的第一个方法，如果找不到对应的方法则返回{@code null}\n\t *\n\t * <p>\n\t * 此方法只检查方法名是否一致（忽略大小写），并不检查参数的一致性。\n\t * </p>\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t * @since 4.3.2\n\t */\n\tpublic static Method getMethodByNameIgnoreCase(Class<?> clazz, String methodName) throws SecurityException {\n\t\treturn getMethodByName(clazz, true, methodName);\n\t}\n\n\t/**\n\t * 按照方法名查找指定方法名的方法，只返回匹配到的第一个方法，如果找不到对应的方法则返回{@code null}\n\t *\n\t * <p>\n\t * 此方法只检查方法名是否一致，并不检查参数的一致性。\n\t * </p>\n\t *\n\t * @param clazz      类，如果为{@code null}返回{@code null}\n\t * @param ignoreCase 是否忽略大小写\n\t * @param methodName 方法名，如果为空字符串返回{@code null}\n\t * @return 方法\n\t * @throws SecurityException 无权访问抛出异常\n\t * @since 4.3.2\n\t */\n\tpublic static Method getMethodByName(Class<?> clazz, boolean ignoreCase, String methodName) throws SecurityException {\n\t\tif (null == clazz || StrUtil.isBlank(methodName)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tMethod res = null;\n\t\tfinal Method[] methods = getMethods(clazz);\n\t\tif (ArrayUtil.isNotEmpty(methods)) {\n\t\t\tfor (Method method : methods) {\n\t\t\t\tif (StrUtil.equals(methodName, method.getName(), ignoreCase)\n\t\t\t\t\t//排除协变桥接方法，pr#1965@Github\n\t\t\t\t\t&& (res == null\n\t\t\t\t\t|| res.getReturnType().isAssignableFrom(method.getReturnType()))) {\n\t\t\t\t\tres = method;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * 获得指定类中的方法名<br>\n\t * 去重重载的方法\n\t *\n\t * @param clazz 类\n\t * @return 方法名Set\n\t * @throws SecurityException 安全异常\n\t */\n\tpublic static Set<String> getMethodNames(Class<?> clazz) throws SecurityException {\n\t\tfinal HashSet<String> methodSet = new HashSet<>();\n\t\tfinal Method[] methods = getMethods(clazz);\n\t\tfor (Method method : methods) {\n\t\t\tmethodSet.add(method.getName());\n\t\t}\n\t\treturn methodSet;\n\t}\n\n\t/**\n\t * 获得指定类过滤后的方法列表\n\t *\n\t * @param clazz  查找方法的类\n\t * @param filter 过滤器\n\t * @return 过滤后的方法列表\n\t * @throws SecurityException 安全异常\n\t */\n\tpublic static Method[] getMethods(Class<?> clazz, Filter<Method> filter) throws SecurityException {\n\t\tif (null == clazz) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ArrayUtil.filter(getMethods(clazz), filter);\n\t}\n\n\t/**\n\t * 获得一个类中所有方法列表，包括其父类中的方法\n\t *\n\t * @param beanClass 类，非{@code null}\n\t * @return 方法列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\tpublic static Method[] getMethods(Class<?> beanClass) throws SecurityException {\n\t\tAssert.notNull(beanClass);\n\t\treturn METHODS_CACHE.computeIfAbsent(beanClass,\n\t\t\t(key) -> getMethodsDirectly(beanClass, true, true));\n\t}\n\n\t/**\n\t * 获得一个类中所有方法列表，直接反射获取，无缓存<br>\n\t * 接口获取方法和默认方法，获取的方法包括：\n\t * <ul>\n\t *     <li>本类中的所有方法（包括static方法）</li>\n\t *     <li>父类中的所有方法（包括static方法）</li>\n\t *     <li>Object中（包括static方法）</li>\n\t * </ul>\n\t *\n\t * @param beanClass            类或接口\n\t * @param withSupers           是否包括父类或接口的方法列表\n\t * @param withMethodFromObject 是否包括Object中的方法\n\t * @return 方法列表\n\t * @throws SecurityException 安全检查异常\n\t */\n\tpublic static Method[] getMethodsDirectly(Class<?> beanClass, boolean withSupers, boolean withMethodFromObject) throws SecurityException {\n\t\tAssert.notNull(beanClass);\n\n\t\tif (beanClass.isInterface()) {\n\t\t\t// 对于接口，直接调用Class.getMethods方法获取所有方法，因为接口都是public方法\n\t\t\treturn withSupers ? beanClass.getMethods() : beanClass.getDeclaredMethods();\n\t\t}\n\n\t\tfinal UniqueKeySet<String, Method> result = new UniqueKeySet<>(true, ReflectUtil::getUniqueKey);\n\t\tClass<?> searchType = beanClass;\n\t\twhile (searchType != null) {\n\t\t\tif (false == withMethodFromObject && Object.class == searchType) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tresult.addAllIfAbsent(Arrays.asList(searchType.getDeclaredMethods()));\n\t\t\tresult.addAllIfAbsent(getDefaultMethodsFromInterface(searchType));\n\n\n\t\t\tsearchType = (withSupers && false == searchType.isInterface()) ? searchType.getSuperclass() : null;\n\t\t}\n\n\t\treturn result.toArray(new Method[0]);\n\t}\n\n\t/**\n\t * 是否为equals方法\n\t *\n\t * @param method 方法\n\t * @return 是否为equals方法\n\t */\n\tpublic static boolean isEqualsMethod(Method method) {\n\t\tif (method == null ||\n\t\t\t1 != method.getParameterCount() ||\n\t\t\tfalse == \"equals\".equals(method.getName())) {\n\t\t\treturn false;\n\t\t}\n\t\treturn (method.getParameterTypes()[0] == Object.class);\n\t}\n\n\t/**\n\t * 是否为hashCode方法\n\t *\n\t * @param method 方法\n\t * @return 是否为hashCode方法\n\t */\n\tpublic static boolean isHashCodeMethod(Method method) {\n\t\treturn method != null//\n\t\t\t&& \"hashCode\".equals(method.getName())//\n\t\t\t&& isEmptyParam(method);\n\t}\n\n\t/**\n\t * 是否为toString方法\n\t *\n\t * @param method 方法\n\t * @return 是否为toString方法\n\t */\n\tpublic static boolean isToStringMethod(Method method) {\n\t\treturn method != null//\n\t\t\t&& \"toString\".equals(method.getName())//\n\t\t\t&& isEmptyParam(method);\n\t}\n\n\t/**\n\t * 是否为无参数方法\n\t *\n\t * @param method 方法\n\t * @return 是否为无参数方法\n\t * @since 5.1.1\n\t */\n\tpublic static boolean isEmptyParam(Method method) {\n\t\treturn method.getParameterCount() == 0;\n\t}\n\n\t/**\n\t * 检查给定方法是否为Getter或者Setter方法，规则为：<br>\n\t * <ul>\n\t *     <li>方法参数必须为0个或1个</li>\n\t *     <li>如果是无参方法，则判断是否以“get”或“is”开头</li>\n\t *     <li>如果方法参数1个，则判断是否以“set”开头</li>\n\t * </ul>\n\t *\n\t * @param method 方法\n\t * @return 是否为Getter或者Setter方法\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isGetterOrSetterIgnoreCase(Method method) {\n\t\treturn isGetterOrSetter(method, true);\n\t}\n\n\t/**\n\t * 检查给定方法是否为Getter或者Setter方法，规则为：<br>\n\t * <ul>\n\t *     <li>方法参数必须为0个或1个</li>\n\t *     <li>方法名称不能是getClass</li>\n\t *     <li>如果是无参方法，则判断是否以“get”或“is”开头</li>\n\t *     <li>如果方法参数1个，则判断是否以“set”开头</li>\n\t * </ul>\n\t *\n\t * @param method     方法\n\t * @param ignoreCase 是否忽略方法名的大小写\n\t * @return 是否为Getter或者Setter方法\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isGetterOrSetter(Method method, boolean ignoreCase) {\n\t\tif (null == method) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 参数个数必须为0或1\n\t\tfinal int parameterCount = method.getParameterCount();\n\t\tif (parameterCount > 1) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString name = method.getName();\n\t\t// 跳过getClass这个特殊方法\n\t\tif (\"getClass\".equals(name)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (ignoreCase) {\n\t\t\tname = name.toLowerCase();\n\t\t}\n\t\tswitch (parameterCount) {\n\t\t\tcase 0:\n\t\t\t\treturn name.startsWith(\"get\") || name.startsWith(\"is\");\n\t\t\tcase 1:\n\t\t\t\treturn name.startsWith(\"set\");\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\t// --------------------------------------------------------------------------------------------------------- newInstance\n\n\t/**\n\t * 实例化对象\n\t *\n\t * @param <T>   对象类型\n\t * @param clazz 类名\n\t * @return 对象\n\t * @throws UtilException 包装各类异常\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T newInstance(String clazz) throws UtilException {\n\t\ttry {\n\t\t\treturn (T) Class.forName(clazz).newInstance();\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Instance class [{}] error!\", clazz);\n\t\t}\n\t}\n\n\t/**\n\t * 实例化对象\n\t *\n\t * @param <T>    对象类型\n\t * @param clazz  类\n\t * @param params 构造函数参数\n\t * @return 对象\n\t * @throws UtilException 包装各类异常\n\t */\n\tpublic static <T> T newInstance(Class<T> clazz, Object... params) throws UtilException {\n\t\tif (ArrayUtil.isEmpty(params)) {\n\t\t\tfinal Constructor<T> constructor = getConstructor(clazz);\n\t\t\tif (null == constructor) {\n\t\t\t\tthrow new UtilException(\"No constructor for [{}]\", clazz);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn constructor.newInstance();\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UtilException(e, \"Instance class [{}] error!\", clazz);\n\t\t\t}\n\t\t}\n\n\t\tfinal Class<?>[] paramTypes = ClassUtil.getClasses(params);\n\t\tfinal Constructor<T> constructor = getConstructor(clazz, paramTypes);\n\t\tif (null == constructor) {\n\t\t\tthrow new UtilException(\"No Constructor matched for parameter types: [{}]\", new Object[]{paramTypes});\n\t\t}\n\t\ttry {\n\t\t\treturn constructor.newInstance(params);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Instance class [{}] error!\", clazz);\n\t\t}\n\t}\n\n\t/**\n\t * 尝试遍历并调用此类的所有构造方法，直到构造成功并返回\n\t * <p>\n\t * 对于某些特殊的接口，按照其默认实现实例化，例如：\n\t * <pre>\n\t *     Map       -》 HashMap\n\t *     Collction -》 ArrayList\n\t *     List      -》 ArrayList\n\t *     Set       -》 HashSet\n\t * </pre>\n\t *\n\t * @param <T>  对象类型\n\t * @param type 被构造的类\n\t * @return 构造后的对象，构造失败返回{@code null}\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T newInstanceIfPossible(Class<T> type) {\n\t\tAssert.notNull(type);\n\n\t\t// 原始类型\n\t\tif (type.isPrimitive()) {\n\t\t\treturn (T) ClassUtil.getPrimitiveDefaultValue(type);\n\t\t}\n\n\t\tif (Object.class != type) {\n\t\t\t// 某些特殊接口的实例化按照默认实现进行\n\t\t\tif (type.isAssignableFrom(AbstractMap.class)) {\n\t\t\t\ttype = (Class<T>) HashMap.class;\n\t\t\t} else if (type.isAssignableFrom(List.class)) {\n\t\t\t\ttype = (Class<T>) ArrayList.class;\n\t\t\t} else if (type.isAssignableFrom(Set.class)) {\n\t\t\t\ttype = (Class<T>) HashSet.class;\n\t\t\t} else if (type.isAssignableFrom(Queue.class)) {\n\t\t\t\ttype = (Class<T>) LinkedList.class;\n\t\t\t} else if (type.isAssignableFrom(Deque.class)) {\n\t\t\t\ttype = (Class<T>) LinkedList.class;\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\treturn newInstance(type);\n\t\t} catch (Exception e) {\n\t\t\t// ignore\n\t\t\t// 默认构造不存在的情况下查找其它构造\n\t\t}\n\n\t\t// 枚举\n\t\tif (type.isEnum()) {\n\t\t\treturn type.getEnumConstants()[0];\n\t\t}\n\n\t\t// 数组\n\t\tif (type.isArray()) {\n\t\t\treturn (T) Array.newInstance(type.getComponentType(), 0);\n\t\t}\n\n\t\tfinal Constructor<T>[] constructors = getConstructors(type);\n\t\tClass<?>[] parameterTypes;\n\t\tfor (Constructor<T> constructor : constructors) {\n\t\t\tparameterTypes = constructor.getParameterTypes();\n\t\t\tif (0 == parameterTypes.length) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsetAccessible(constructor);\n\t\t\ttry {\n\t\t\t\treturn constructor.newInstance(ClassUtil.getDefaultValues(parameterTypes));\n\t\t\t} catch (Exception ignore) {\n\t\t\t\t// 构造出错时继续尝试下一种构造方式\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t// --------------------------------------------------------------------------------------------------------- invoke\n\n\t/**\n\t * 执行静态方法\n\t *\n\t * @param <T>    对象类型\n\t * @param method 方法（对象方法或static方法都可）\n\t * @param args   参数对象\n\t * @return 结果\n\t * @throws UtilException 多种异常包装\n\t */\n\tpublic static <T> T invokeStatic(Method method, Object... args) throws UtilException {\n\t\treturn invoke(null, method, args);\n\t}\n\n\t/**\n\t * 执行方法<br>\n\t * 执行前要检查给定参数：\n\t *\n\t * <pre>\n\t * 1. 参数个数是否与方法参数个数一致\n\t * 2. 如果某个参数为null但是方法这个位置的参数为原始类型，则赋予原始类型默认值\n\t * </pre>\n\t *\n\t * @param <T>    返回对象类型\n\t * @param obj    对象，如果执行静态方法，此值为{@code null}\n\t * @param method 方法（对象方法或static方法都可）\n\t * @param args   参数对象\n\t * @return 结果\n\t * @throws UtilException 一些列异常的包装\n\t */\n\tpublic static <T> T invokeWithCheck(Object obj, Method method, Object... args) throws UtilException {\n\t\tfinal Class<?>[] types = method.getParameterTypes();\n\t\tif (null != args) {\n\t\t\tAssert.isTrue(args.length == types.length, \"Params length [{}] is not fit for param length [{}] of method !\", args.length, types.length);\n\t\t\tClass<?> type;\n\t\t\tfor (int i = 0; i < args.length; i++) {\n\t\t\t\ttype = types[i];\n\t\t\t\tif (type.isPrimitive() && null == args[i]) {\n\t\t\t\t\t// 参数是原始类型，而传入参数为null时赋予默认值\n\t\t\t\t\targs[i] = ClassUtil.getDefaultValue(type);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn invoke(obj, method, args);\n\t}\n\n\t/**\n\t * 执行方法\n\t *\n\t * <p>\n\t * 对于用户传入参数会做必要检查，包括：\n\t *\n\t * <pre>\n\t *     1、忽略多余的参数\n\t *     2、参数不够补齐默认值\n\t *     3、传入参数为null，但是目标参数类型为原始类型，做转换\n\t * </pre>\n\t *\n\t * @param <T>    返回对象类型\n\t * @param obj    对象，如果执行静态方法，此值为{@code null}\n\t * @param method 方法（对象方法或static方法都可）\n\t * @param args   参数对象\n\t * @return 结果\n\t * @throws InvocationTargetRuntimeException 目标方法执行异常\n\t * @throws UtilException                    {@link IllegalAccessException}异常的包装\n\t */\n\tpublic static <T> T invoke(Object obj, Method method, Object... args) throws InvocationTargetRuntimeException, UtilException {\n\t\ttry {\n\t\t\treturn invokeRaw(obj, method, args);\n\t\t} catch (InvocationTargetException e) {\n\t\t\tthrow new InvocationTargetRuntimeException(e);\n\t\t} catch (IllegalAccessException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行方法\n\t *\n\t * <p>\n\t * 对于用户传入参数会做必要检查，包括：\n\t *\n\t * <pre>\n\t *     1、忽略多余的参数\n\t *     2、参数不够补齐默认值\n\t *     3、传入参数为null，但是目标参数类型为原始类型，做转换\n\t * </pre>\n\t *\n\t * @param <T>    返回对象类型\n\t * @param obj    对象，如果执行静态方法，此值为{@code null}\n\t * @param method 方法（对象方法或static方法都可）\n\t * @param args   参数对象\n\t * @return 结果\n\t * @throws InvocationTargetException 目标方法执行异常\n\t * @throws IllegalAccessException    访问异常\n\t * @since 5.8.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T invokeRaw(Object obj, Method method, Object... args) throws InvocationTargetException, IllegalAccessException {\n\t\tsetAccessible(method);\n\n\t\t// 检查用户传入参数：\n\t\t// 1、忽略多余的参数\n\t\t// 2、参数不够补齐默认值\n\t\t// 3、通过NullWrapperBean传递的参数,会直接赋值null\n\t\t// 4、传入参数为null，但是目标参数类型为原始类型，做转换\n\t\t// 5、传入参数类型不对应，尝试转换类型\n\t\tfinal Class<?>[] parameterTypes = method.getParameterTypes();\n\t\tfinal Object[] actualArgs = new Object[parameterTypes.length];\n\t\tif (null != args) {\n\t\t\tfor (int i = 0; i < actualArgs.length; i++) {\n\t\t\t\tif (i >= args.length || null == args[i]) {\n\t\t\t\t\t// 越界或者空值\n\t\t\t\t\tactualArgs[i] = ClassUtil.getDefaultValue(parameterTypes[i]);\n\t\t\t\t} else if (args[i] instanceof NullWrapperBean) {\n\t\t\t\t\t//如果是通过NullWrapperBean传递的null参数,直接赋值null\n\t\t\t\t\tactualArgs[i] = null;\n\t\t\t\t} else if (false == parameterTypes[i].isAssignableFrom(args[i].getClass())) {\n\t\t\t\t\t//对于类型不同的字段，尝试转换，转换失败则使用原对象类型\n\t\t\t\t\tfinal Object targetValue = Convert.convertWithCheck(parameterTypes[i], args[i], null, true);\n\t\t\t\t\tif (null != targetValue) {\n\t\t\t\t\t\tactualArgs[i] = targetValue;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tactualArgs[i] = args[i];\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tactualArgs[i] = args[i];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (method.isDefault()) {\n\t\t\t// 当方法是default方法时，尤其对象是代理对象，需使用句柄方式执行\n\t\t\t// 代理对象情况下调用method.invoke会导致循环引用执行，最终栈溢出\n\t\t\treturn MethodHandleUtil.invokeSpecial(obj, method, args);\n\t\t}\n\n\t\treturn (T) method.invoke(ClassUtil.isStatic(method) ? null : obj, actualArgs);\n\t}\n\n\t/**\n\t * 执行对象中指定方法\n\t * 如果需要传递的参数为null,请使用NullWrapperBean来传递,不然会丢失类型信息\n\t *\n\t * @param <T>        返回对象类型\n\t * @param obj        方法所在对象\n\t * @param methodName 方法名\n\t * @param args       参数列表\n\t * @return 执行结果\n\t * @throws UtilException IllegalAccessException等异常包装\n\t * @see NullWrapperBean\n\t * @since 3.1.2\n\t */\n\tpublic static <T> T invoke(Object obj, String methodName, Object... args) throws UtilException {\n\t\tAssert.notNull(obj, \"Object to get method must be not null!\");\n\t\tAssert.notBlank(methodName, \"Method name must be not blank!\");\n\n\t\tfinal Method method = getMethodOfObj(obj, methodName, args);\n\t\tif (null == method) {\n\t\t\tthrow new UtilException(\"No such method: [{}] from [{}]\", methodName, obj.getClass());\n\t\t}\n\t\treturn invoke(obj, method, args);\n\t}\n\n\t/**\n\t * 设置方法为可访问（私有方法可以被外部调用）\n\t *\n\t * @param <T>              AccessibleObject的子类，比如Class、Method、Field等\n\t * @param accessibleObject 可设置访问权限的对象，比如Class、Method、Field等\n\t * @return 被设置可访问的对象\n\t * @since 4.6.8\n\t */\n\tpublic static <T extends AccessibleObject> T setAccessible(T accessibleObject) {\n\t\tif (null != accessibleObject && false == accessibleObject.isAccessible()) {\n\t\t\taccessibleObject.setAccessible(true);\n\t\t}\n\t\treturn accessibleObject;\n\t}\n\n\t/**\n\t * 设置final的field字段可以被修改\n\t * 只要不会被编译器内联优化的 final 属性就可以通过反射有效的进行修改 --  修改后代码中可使用到新的值;\n\t * <p>以下属性，编译器会内联优化，无法通过反射修改：</p>\n\t * <ul>\n\t *     <li> 基本类型 byte, char, short, int, long, float, double, boolean</li>\n\t *     <li> Literal String 类型(直接双引号字符串)</li>\n\t * </ul>\n\t * <p>以下属性，可以通过反射修改：</p>\n\t * <ul>\n\t *     <li>基本类型的包装类 Byte、Character、Short、Long、Float、Double、Boolean</li>\n\t *     <li>字符串，通过 new String(\"\")实例化</li>\n\t *     <li>自定义java类</li>\n\t * </ul>\n\t * <pre class=\"code\">\n\t * {@code\n\t *      //示例，移除final修饰符\n\t *      class JdbcDialects {private static final List<Number> dialects = new ArrayList<>();}\n\t *      Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);\n\t * \t\tReflectUtil.removeFinalModify(field);\n\t * \t\tReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);\n\t *    }\n\t * </pre>\n\t *\n\t * @param field 被修改的field，不可以为空\n\t * @throws UtilException IllegalAccessException等异常包装\n\t * @author dazer\n\t * @since 5.8.8\n\t */\n\tpublic static void removeFinalModify(Field field) {\n\t\tModifierUtil.removeFinalModify(field);\n\t}\n\n\t/**\n\t * 获取方法的唯一键，结构为:\n\t * <pre>\n\t *     返回类型#方法名:参数1类型,参数2类型...\n\t * </pre>\n\t *\n\t * @param method 方法\n\t * @return 方法唯一键\n\t */\n\tprivate static String getUniqueKey(Method method) {\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tsb.append(method.getReturnType().getName()).append('#');\n\t\tsb.append(method.getName());\n\t\tClass<?>[] parameters = method.getParameterTypes();\n\t\tfor (int i = 0; i < parameters.length; i++) {\n\t\t\tif (i == 0) {\n\t\t\t\tsb.append(':');\n\t\t\t} else {\n\t\t\t\tsb.append(',');\n\t\t\t}\n\t\t\tsb.append(parameters[i].getName());\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 获取类对应接口中的非抽象方法（default方法）\n\t *\n\t * @param clazz 类\n\t * @return 方法列表\n\t */\n\tprivate static List<Method> getDefaultMethodsFromInterface(Class<?> clazz) {\n\t\tList<Method> result = new ArrayList<>();\n\t\tfor (Class<?> ifc : clazz.getInterfaces()) {\n\t\t\tfor (Method m : ifc.getMethods()) {\n\t\t\t\tif (false == ModifierUtil.isAbstract(m)) {\n\t\t\t\t\tresult.add(m);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 方法查找键（用于细粒度缓存）\n\t * 用于唯一标识一次方法查找操作\n\t */\n\tprivate static class MethodLookupKey {\n\t\tprivate final Class<?> clazz;\n\t\tprivate final boolean ignoreCase;\n\t\tprivate final String methodName;\n\t\tprivate final Class<?>[] paramTypes;\n\n\t\tpublic MethodLookupKey(Class<?> clazz, boolean ignoreCase, String methodName, Class<?>[] paramTypes) {\n\t\t\tthis.clazz = clazz;\n\t\t\tthis.ignoreCase = ignoreCase;\n\t\t\tthis.methodName = methodName;\n\t\t\tthis.paramTypes = paramTypes;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object o) {\n\t\t\tif (this == o) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tMethodLookupKey that = (MethodLookupKey) o;\n\t\t\treturn ignoreCase == that.ignoreCase &&\n\t\t\t\tObjects.equals(clazz, that.clazz) &&\n\t\t\t\tObjects.equals(methodName, that.methodName) &&\n\t\t\t\tArrays.equals(paramTypes, that.paramTypes);\n\t\t}\n\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\tint result = Objects.hash(clazz, ignoreCase, methodName);\n\t\t\tresult = 31 * result + Arrays.hashCode(paramTypes);\n\t\t\treturn result;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/RuntimeUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Pid;\nimport cn.hutool.core.text.StrBuilder;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Stack;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * 系统运行时工具类，用于执行系统命令的工具\n *\n * @author Looly\n * @since 3.1.1\n */\npublic class RuntimeUtil {\n\n\t/**\n\t * 执行系统命令，使用系统默认编码\n\t *\n\t * @param cmds 命令列表，每个元素代表一条命令\n\t * @return 执行结果\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static String execForStr(String... cmds) throws IORuntimeException {\n\t\treturn execForStr(CharsetUtil.systemCharset(), cmds);\n\t}\n\n\t/**\n\t * 执行系统命令，使用传入的 {@link Charset charset} 编码\n\t *\n\t * @param charset 编码\n\t * @param cmds    命令列表，每个元素代表一条命令\n\t * @return 执行结果\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static String execForStr(Charset charset, String... cmds) throws IORuntimeException {\n\t\treturn getResult(exec(cmds), charset);\n\t}\n\n\t/**\n\t * 执行系统命令，使用系统默认编码\n\t *\n\t * @param cmds 命令列表，每个元素代表一条命令\n\t * @return 执行结果，按行区分\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static List<String> execForLines(String... cmds) throws IORuntimeException {\n\t\treturn execForLines(CharsetUtil.systemCharset(), cmds);\n\t}\n\n\t/**\n\t * 执行系统命令，使用传入的 {@link Charset charset} 编码\n\t *\n\t * @param charset 编码\n\t * @param cmds    命令列表，每个元素代表一条命令\n\t * @return 执行结果，按行区分\n\t * @throws IORuntimeException IO异常\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> execForLines(Charset charset, String... cmds) throws IORuntimeException {\n\t\treturn getResultLines(exec(cmds), charset);\n\t}\n\n\t/**\n\t * 执行命令<br>\n\t * 命令带参数时参数可作为其中一个参数，也可以将命令和参数组合为一个字符串传入\n\t *\n\t * @param cmds 命令\n\t * @return {@link Process}\n\t */\n\tpublic static Process exec(String... cmds) {\n\t\tProcess process;\n\t\ttry {\n\t\t\tprocess = new ProcessBuilder(handleCmds(cmds)).redirectErrorStream(true).start();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn process;\n\t}\n\n\t/**\n\t * 执行命令<br>\n\t * 命令带参数时参数可作为其中一个参数，也可以将命令和参数组合为一个字符串传入\n\t *\n\t * @param envp 环境变量参数，传入形式为key=value，null表示继承系统环境变量\n\t * @param cmds 命令\n\t * @return {@link Process}\n\t * @since 4.1.6\n\t */\n\tpublic static Process exec(String[] envp, String... cmds) {\n\t\treturn exec(envp, null, cmds);\n\t}\n\n\t/**\n\t * 执行命令<br>\n\t * 命令带参数时参数可作为其中一个参数，也可以将命令和参数组合为一个字符串传入\n\t *\n\t * @param envp 环境变量参数，传入形式为key=value，null表示继承系统环境变量\n\t * @param dir  执行命令所在目录（用于相对路径命令执行），null表示使用当前进程执行的目录\n\t * @param cmds 命令\n\t * @return {@link Process}\n\t * @since 4.1.6\n\t */\n\tpublic static Process exec(String[] envp, File dir, String... cmds) {\n\t\ttry {\n\t\t\treturn Runtime.getRuntime().exec(handleCmds(cmds), envp, dir);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------------------- result\n\n\t/**\n\t * 获取命令执行结果，使用系统默认编码，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @return 命令执行结果列表\n\t */\n\tpublic static List<String> getResultLines(Process process) {\n\t\treturn getResultLines(process, CharsetUtil.systemCharset());\n\t}\n\n\t/**\n\t * 获取命令执行结果，使用传入的 {@link Charset charset} 编码，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @param charset 编码\n\t * @return 命令执行结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static List<String> getResultLines(Process process, Charset charset) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = process.getInputStream();\n\t\t\treturn IoUtil.readLines(in, charset, new ArrayList<>());\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t\tdestroy(process);\n\t\t}\n\t}\n\n\t/**\n\t * 获取命令执行结果，使用系统默认编码，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @return 命令执行结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static String getResult(Process process) {\n\t\treturn getResult(process, CharsetUtil.systemCharset());\n\t}\n\n\t/**\n\t * 获取命令执行结果，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @param charset 编码\n\t * @return 命令执行结果列表\n\t * @since 3.1.2\n\t */\n\tpublic static String getResult(Process process, Charset charset) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = process.getInputStream();\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t\tdestroy(process);\n\t\t}\n\t}\n\n\t/**\n\t * 获取命令执行异常结果，使用系统默认编码，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @return 命令执行结果列表\n\t * @since 4.1.21\n\t */\n\tpublic static String getErrorResult(Process process) {\n\t\treturn getErrorResult(process, CharsetUtil.systemCharset());\n\t}\n\n\t/**\n\t * 获取命令执行异常结果，获取后销毁进程\n\t *\n\t * @param process {@link Process} 进程\n\t * @param charset 编码\n\t * @return 命令执行结果列表\n\t * @since 4.1.21\n\t */\n\tpublic static String getErrorResult(Process process, Charset charset) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = process.getErrorStream();\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t\tdestroy(process);\n\t\t}\n\t}\n\n\t/**\n\t * 销毁进程\n\t *\n\t * @param process 进程\n\t * @since 3.1.2\n\t */\n\tpublic static void destroy(Process process) {\n\t\tif (null != process) {\n\t\t\tprocess.destroy();\n\t\t}\n\t}\n\n\t/**\n\t * 增加一个JVM关闭后的钩子，用于在JVM关闭时执行某些操作\n\t *\n\t * @param hook 钩子\n\t * @since 4.0.5\n\t */\n\tpublic static void addShutdownHook(Runnable hook) {\n\t\tRuntime.getRuntime().addShutdownHook((hook instanceof Thread) ? (Thread) hook : new Thread(hook));\n\t}\n\n\t/**\n\t * 获得JVM可用的处理器数量（一般为CPU核心数）\n\t *\n\t * <p>\n\t *     这里做一个特殊的处理,在特殊的CPU上面，会有获取不到CPU数量的情况，所以这里做一个保护;\n\t *     默认给一个7，真实的CPU基本都是偶数，方便区分。\n\t *     如果不做处理，会出现创建线程池时{@link ThreadPoolExecutor}，抛出异常：{@link IllegalArgumentException}\n\t * </p>\n\t *\n\t * @return 可用的处理器数量\n\t * @since 5.3.0\n\t */\n\tpublic static int getProcessorCount() {\n\t\tint cpu = Runtime.getRuntime().availableProcessors();\n\t\tif (cpu <= 0) {\n\t\t\tcpu = 7;\n\t\t}\n\t\treturn cpu;\n\t}\n\n\t/**\n\t * 获得JVM中剩余的内存数，单位byte\n\t *\n\t * @return JVM中剩余的内存数，单位byte\n\t * @since 5.3.0\n\t */\n\tpublic static long getFreeMemory() {\n\t\treturn Runtime.getRuntime().freeMemory();\n\t}\n\n\t/**\n\t * 获得JVM已经从系统中获取到的总共的内存数，单位byte\n\t *\n\t * @return JVM中剩余的内存数，单位byte\n\t * @since 5.3.0\n\t */\n\tpublic static long getTotalMemory() {\n\t\treturn Runtime.getRuntime().totalMemory();\n\t}\n\n\t/**\n\t * 获得JVM中可以从系统中获取的最大的内存数，单位byte，以-Xmx参数为准\n\t *\n\t * @return JVM中剩余的内存数，单位byte\n\t * @since 5.3.0\n\t */\n\tpublic static long getMaxMemory() {\n\t\treturn Runtime.getRuntime().maxMemory();\n\t}\n\n\t/**\n\t * 获得JVM最大可用内存，计算方法为：<br>\n\t * 最大内存-总内存+剩余内存\n\t *\n\t * @return 最大可用内存\n\t */\n\tpublic static long getUsableMemory() {\n\t\treturn getMaxMemory() - getTotalMemory() + getFreeMemory();\n\t}\n\n\t/**\n\t * 获取当前进程ID，首先获取进程名称，读取@前的ID值，如果不存在，则读取进程名的hash值\n\t *\n\t * @return 进程ID\n\t * @throws UtilException 进程名称为空\n\t * @since 5.7.3\n\t */\n\tpublic static int getPid() throws UtilException {\n\t\treturn Pid.INSTANCE.get();\n\t}\n\n\t/**\n\t * 处理命令，多行命令原样返回，单行命令拆分处理\n\t *\n\t * @param cmds 命令\n\t * @return 处理后的命令\n\t */\n\tprivate static String[] handleCmds(String... cmds) {\n\t\tif (ArrayUtil.isEmpty(cmds)) {\n\t\t\tthrow new NullPointerException(\"Command is empty !\");\n\t\t}\n\n\t\t// 单条命令的情况\n\t\tif (1 == cmds.length) {\n\t\t\tfinal String cmd = cmds[0];\n\t\t\tif (StrUtil.isBlank(cmd)) {\n\t\t\t\tthrow new NullPointerException(\"Command is blank !\");\n\t\t\t}\n\t\t\tcmds = cmdSplit(cmd);\n\t\t}\n\t\treturn cmds;\n\t}\n\n\t/**\n\t * 命令分割，使用空格分割，考虑双引号和单引号的情况\n\t *\n\t * @param cmd 命令，如 git commit -m 'test commit'\n\t * @return 分割后的命令\n\t */\n\tprivate static String[] cmdSplit(String cmd) {\n\t\tfinal List<String> cmds = new ArrayList<>();\n\n\t\tfinal int length = cmd.length();\n\t\tfinal Stack<Character> stack = new Stack<>();\n\t\tboolean inWrap = false;\n\t\tfinal StrBuilder cache = StrUtil.strBuilder();\n\n\t\tchar c;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = cmd.charAt(i);\n\t\t\tswitch (c) {\n\t\t\t\tcase CharUtil.SINGLE_QUOTE:\n\t\t\t\tcase CharUtil.DOUBLE_QUOTES:\n\t\t\t\t\tif (inWrap) {\n\t\t\t\t\t\tif (c == stack.peek()) {\n\t\t\t\t\t\t\t//结束包装\n\t\t\t\t\t\t\tstack.pop();\n\t\t\t\t\t\t\tinWrap = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcache.append(c);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstack.push(c);\n\t\t\t\t\t\tcache.append(c);\n\t\t\t\t\t\tinWrap = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase CharUtil.SPACE:\n\t\t\t\t\tif (inWrap) {\n\t\t\t\t\t\t// 处于包装内\n\t\t\t\t\t\tcache.append(c);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcmds.add(cache.toString());\n\t\t\t\t\t\tcache.reset();\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tcache.append(c);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (cache.hasContent()) {\n\t\t\tcmds.add(cache.toString());\n\t\t}\n\n\t\treturn cmds.toArray(new String[0]);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/SerializeUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.ValidateObjectInputStream;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.Serializable;\n\n/**\n * 序列化工具类<br>\n * 注意！此工具类依赖于JDK的序列化机制，某些版本的JDK中可能存在远程注入漏洞。\n *\n * @author looly\n * @since 5.6.3\n */\npublic class SerializeUtil {\n\n\t/**\n\t * 序列化后拷贝流的方式克隆<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 被克隆对象\n\t * @return 克隆后的对象\n\t * @throws UtilException IO异常和ClassNotFoundException封装\n\t */\n\tpublic static <T> T clone(T obj) {\n\t\tif (false == (obj instanceof Serializable)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn deserialize(serialize(obj));\n\t}\n\n\t/**\n\t * 序列化<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * @param <T> 对象类型\n\t * @param obj 要被序列化的对象\n\t * @return 序列化后的字节码\n\t */\n\tpublic static <T> byte[] serialize(T obj) {\n\t\tif (false == (obj instanceof Serializable)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream();\n\t\tIoUtil.writeObjects(byteOut, false, (Serializable) obj);\n\t\treturn byteOut.toByteArray();\n\t}\n\n\t/**\n\t * 反序列化<br>\n\t * 对象必须实现Serializable接口\n\t *\n\t * <p>\n\t * 注意！！！ 此方法不会检查反序列化安全，可能存在反序列化漏洞风险！！！\n\t * </p>\n\t *\n\t * @param <T>   对象类型\n\t * @param bytes 反序列化的字节码\n\t * @param acceptClasses 白名单的类\n\t * @return 反序列化后的对象\n\t */\n\tpublic static <T> T deserialize(byte[] bytes, Class<?>... acceptClasses) {\n\t\ttry {\n\t\t\treturn IoUtil.readObj(new ValidateObjectInputStream(\n\t\t\t\t\tnew ByteArrayInputStream(bytes), acceptClasses));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ServiceLoaderUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.ListUtil;\n\n\nimport java.util.*;\n\n\n\n/**\n * SPI机制中的服务加载工具类，流程如下\n *\n * <pre>\n *     1、创建接口，并创建实现类\n *     2、ClassPath/META-INF/services下创建与接口全限定类名相同的文件\n *     3、文件内容填写实现类的全限定类名\n * </pre>\n * 相关介绍见：https://www.jianshu.com/p/3a3edbcd8f24\n *\n * @author looly\n * @since 5.1.6\n */\npublic class ServiceLoaderUtil {\n\n\t/**\n\t * 加载第一个可用的 Service 实现。\n\t * <p>\n\t * 为兼容 JDK 24+ 中 {@link ServiceLoader} 在加载服务实现时可能抛出的 {@link NoClassDefFoundError}，\n\t * 此方法在调用 {@code hasNext()} 和 {@code next()} 时安全忽略异常。\n\t * 当遇到依赖缺失或配置错误的实现时会自动跳过，并返回第一个可用的非空实例。\n\t * </p>\n\t *\n\t * @param clazz 服务接口类型\n\t * @param <T>   服务实现类型\n\t * @return 第一个可用的非空实现，若无可用实现则返回 {@code null}\n\t * @since 5.8.41 （JDK 24+ 兼容优化）\n\t * @see <a href=\"https://bugs.openjdk.org/browse/JDK-8350481\">JDK-8350481</a>\n\t */\n\tpublic static <T> T loadFirstAvailable(Class<T> clazz) {\n\t\tfinal Iterator<T> iterator = load(clazz).iterator();\n\t\twhile (true) {\n\t\t\tT instance;\n\t\t\ttry {\n\t\t\t\t// 注意：JDK 24+ 下 hasNext() 和 next() 均可能触发 NoClassDefFoundError\n\t\t\t\tif (!iterator.hasNext()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tinstance = iterator.next();\n\t\t\t} catch (ServiceConfigurationError | NoClassDefFoundError e) {\n\t\t\t\t// 安全忽略当前实现，尝试下一个\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (instance != null) {\n\t\t\t\treturn instance;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 加载第一个服务，如果用户定义了多个接口实现类，只获取第一个。\n\t *\n\t * @param <T>   接口类型\n\t * @param clazz 服务接口\n\t * @return 第一个服务接口实现对象，无实现返回{@code null}\n\t */\n\tpublic static <T> T loadFirst(Class<T> clazz) {\n\t\tfinal Iterator<T> iterator = load(clazz).iterator();\n\t\tif (iterator.hasNext()) {\n\t\t\treturn iterator.next();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 加载服务\n\t *\n\t * @param <T>   接口类型\n\t * @param clazz 服务接口\n\t * @return 服务接口实现列表\n\t */\n\tpublic static <T> ServiceLoader<T> load(Class<T> clazz) {\n\t\treturn load(clazz, null);\n\t}\n\n\t/**\n\t * 加载服务\n\t *\n\t * @param <T>    接口类型\n\t * @param clazz  服务接口\n\t * @param loader {@link ClassLoader}\n\t * @return 服务接口实现列表\n\t */\n\tpublic static <T> ServiceLoader<T> load(Class<T> clazz, ClassLoader loader) {\n\t\treturn ServiceLoader.load(clazz, ObjectUtil.defaultIfNull(loader, ClassLoaderUtil::getClassLoader));\n\t}\n\n\t/**\n\t * 加载服务 并已list列表返回\n\t *\n\t * @param <T>   接口类型\n\t * @param clazz 服务接口\n\t * @return 服务接口实现列表\n\t * @since 5.4.2\n\t */\n\tpublic static <T> List<T> loadList(Class<T> clazz) {\n\t\treturn loadList(clazz, null);\n\t}\n\n\t/**\n\t * 加载服务 并已list列表返回\n\t *\n\t * @param <T>    接口类型\n\t * @param clazz  服务接口\n\t * @param loader {@link ClassLoader}\n\t * @return 服务接口实现列表\n\t * @since 5.4.2\n\t */\n\tpublic static <T> List<T> loadList(Class<T> clazz, ClassLoader loader) {\n\t\treturn ListUtil.list(false, load(clazz, loader));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.text.*;\n\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.Charset;\nimport java.nio.charset.CharsetDecoder;\nimport java.nio.charset.CodingErrorAction;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Map;\n\n/**\n * 字符串工具类\n *\n * @author xiaoleilu\n */\npublic class StrUtil extends CharSequenceUtil implements StrPool {\n\n\t// ------------------------------------------------------------------------ Blank\n\n\t/**\n\t * <p>如果对象是字符串是否为空白，空白的定义如下：</p>\n\t * <ol>\n\t *     <li>{@code null}</li>\n\t *     <li>空字符串：{@code \"\"}</li>\n\t *     <li>空格、全角空格、制表符、换行符，等不可见字符</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code StrUtil.isBlankIfStr(null)     // true}</li>\n\t *     <li>{@code StrUtil.isBlankIfStr(\"\")       // true}</li>\n\t *     <li>{@code StrUtil.isBlankIfStr(\" \\t\\n\")  // true}</li>\n\t *     <li>{@code StrUtil.isBlankIfStr(\"abc\")    // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isEmptyIfStr(Object)} 的区别是：\n\t * 该方法会校验空白字符，且性能相对于 {@link #isEmptyIfStr(Object)} 略慢。</p>\n\t *\n\t * @param obj 对象\n\t * @return 如果为字符串是否为空串\n\t * @see StrUtil#isBlank(CharSequence)\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isBlankIfStr(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn true;\n\t\t} else if (obj instanceof CharSequence) {\n\t\t\treturn isBlank((CharSequence) obj);\n\t\t}\n\t\treturn false;\n\t}\n\t// ------------------------------------------------------------------------ Empty\n\n\t/**\n\t * <p>如果对象是字符串是否为空串，空的定义如下：</p><br>\n\t * <ol>\n\t *     <li>{@code null}</li>\n\t *     <li>空字符串：{@code \"\"}</li>\n\t * </ol>\n\t *\n\t * <p>例：</p>\n\t * <ul>\n\t *     <li>{@code StrUtil.isEmptyIfStr(null)     // true}</li>\n\t *     <li>{@code StrUtil.isEmptyIfStr(\"\")       // true}</li>\n\t *     <li>{@code StrUtil.isEmptyIfStr(\" \\t\\n\")  // false}</li>\n\t *     <li>{@code StrUtil.isEmptyIfStr(\"abc\")    // false}</li>\n\t * </ul>\n\t *\n\t * <p>注意：该方法与 {@link #isBlankIfStr(Object)} 的区别是：该方法不校验空白字符。</p>\n\t *\n\t * @param obj 对象\n\t * @return 如果为字符串是否为空串\n\t * @since 3.3.0\n\t */\n\tpublic static boolean isEmptyIfStr(Object obj) {\n\t\tif (null == obj) {\n\t\t\treturn true;\n\t\t} else if (obj instanceof CharSequence) {\n\t\t\treturn 0 == ((CharSequence) obj).length();\n\t\t}\n\t\treturn false;\n\t}\n\n\t// ------------------------------------------------------------------------ Trim\n\n\t/**\n\t * 给定字符串数组全部做去首尾空格\n\t *\n\t * @param strs 字符串数组\n\t */\n\tpublic static void trim(String[] strs) {\n\t\tif (null == strs) {\n\t\t\treturn;\n\t\t}\n\t\tString str;\n\t\tfor (int i = 0; i < strs.length; i++) {\n\t\t\tstr = strs[i];\n\t\t\tif (null != str) {\n\t\t\t\tstrs[i] = trim(str);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将对象转为字符串<br>\n\t *\n\t * <pre>\n\t * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组\n\t * 2、对象数组会调用Arrays.toString方法\n\t * </pre>\n\t *\n\t * @param obj 对象\n\t * @return 字符串\n\t */\n\tpublic static String utf8Str(Object obj) {\n\t\treturn str(obj, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将对象转为字符串\n\t *\n\t * <pre>\n\t * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组\n\t * 2、对象数组会调用Arrays.toString方法\n\t * </pre>\n\t *\n\t * @param obj         对象\n\t * @param charsetName 字符集\n\t * @return 字符串\n\t * @deprecated 请使用 {@link #str(Object, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String str(Object obj, String charsetName) {\n\t\treturn str(obj, Charset.forName(charsetName));\n\t}\n\n\t/**\n\t * 将对象转为字符串\n\t * <pre>\n\t * \t 1、Byte数组和ByteBuffer会被转换为对应字符串的数组\n\t * \t 2、对象数组会调用Arrays.toString方法\n\t * </pre>\n\t *\n\t * @param obj     对象\n\t * @param charset 字符集\n\t * @return 字符串\n\t */\n\tpublic static String str(Object obj, Charset charset) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (obj instanceof String) {\n\t\t\treturn (String) obj;\n\t\t} else if (obj instanceof byte[]) {\n\t\t\treturn str((byte[]) obj, charset);\n\t\t} else if (obj instanceof Byte[]) {\n\t\t\treturn str((Byte[]) obj, charset);\n\t\t} else if (obj instanceof ByteBuffer) {\n\t\t\treturn str((ByteBuffer) obj, charset);\n\t\t} else if (ArrayUtil.isArray(obj)) {\n\t\t\treturn ArrayUtil.toString(obj);\n\t\t}\n\n\t\treturn obj.toString();\n\t}\n\n\t/**\n\t * 将byte数组转为字符串\n\t *\n\t * @param bytes   byte数组\n\t * @param charset 字符集\n\t * @return 字符串\n\t */\n\tpublic static String str(byte[] bytes, String charset) {\n\t\treturn str(bytes, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 解码字节码\n\t *\n\t * @param data    byte数组\n\t * @param charset 字符集，如果此字段为空，则解码的结果取决于平台\n\t * @return 解码后的字符串\n\t */\n\tpublic static String str(byte[] data, Charset charset) {\n\t\tif (data == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (null == charset) {\n\t\t\treturn new String(data);\n\t\t}\n\t\treturn new String(data, charset);\n\t}\n\n\t/**\n\t * 将Byte数组转为字符串\n\t *\n\t * @param bytes   byte数组\n\t * @param charset 字符集\n\t * @return 字符串\n\t */\n\tpublic static String str(Byte[] bytes, String charset) {\n\t\treturn str(bytes, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 解码字节码\n\t *\n\t * @param data    Byte数组\n\t * @param charset 字符集，如果此字段为空，则解码的结果取决于平台\n\t * @return 解码后的字符串\n\t */\n\tpublic static String str(Byte[] data, Charset charset) {\n\t\tif (data == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tbyte[] bytes = new byte[data.length];\n\t\tByte dataByte;\n\t\tfor (int i = 0; i < data.length; i++) {\n\t\t\tdataByte = data[i];\n\t\t\tbytes[i] = (null == dataByte) ? -1 : dataByte;\n\t\t}\n\n\t\treturn str(bytes, charset);\n\t}\n\n\t/**\n\t * 将编码的byteBuffer数据转换为字符串\n\t *\n\t * @param data    数据\n\t * @param charset 字符集，如果为空使用当前系统字符集\n\t * @return 字符串\n\t */\n\tpublic static String str(ByteBuffer data, String charset) {\n\t\tif (data == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn str(data, CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 将编码的byteBuffer数据转换为字符串\n\t *\n\t * @param data    数据\n\t * @param charset 字符集，如果为空使用当前系统字符集\n\t * @return 字符串\n\t */\n\tpublic static String str(ByteBuffer data, Charset charset) {\n\t\tif (null == charset) {\n\t\t\tcharset = Charset.defaultCharset();\n\t\t}\n\t\treturn charset.decode(data.duplicate()).toString();\n\t}\n\n\t/**\n\t * 调用对象的toString方法，null会返回“null”\n\t *\n\t * @param obj 对象\n\t * @return 字符串\n\t * @see String#valueOf(Object)\n\t * @since 4.1.3\n\t */\n\tpublic static String toString(Object obj) {\n\t\treturn String.valueOf(obj);\n\t}\n\n\t/**\n\t * 调用对象的toString方法，null会返回{@code null}\n\t *\n\t * @param obj 对象\n\t * @return 字符串 or {@code null}\n\t * @since 5.7.17\n\t */\n\tpublic static String toStringOrNull(Object obj) {\n\t\treturn null == obj ? null : obj.toString();\n\t}\n\n\t/**\n\t * 调用对象的toString方法，null会返回空字符串 \"\"\n\t *\n\t * @param obj 对象\n\t * @return {@link String }\n\t * @author Junwei Xu\n\t */\n\tpublic static String toStringOrEmpty(Object obj) {\n\t\t// obj为空时, 返回 null 或 \"null\" 都不适用部分场景, 此处返回 \"\" 空字符串\n\t\treturn null == obj ? EMPTY : obj.toString();\n\t}\n\n\t/**\n\t * 创建StringBuilder对象\n\t *\n\t * @return StringBuilder对象\n\t */\n\tpublic static StringBuilder builder() {\n\t\treturn new StringBuilder();\n\t}\n\n\t/**\n\t * 创建StrBuilder对象\n\t *\n\t * @return StrBuilder对象\n\t * @since 4.0.1\n\t */\n\tpublic static StrBuilder strBuilder() {\n\t\treturn StrBuilder.create();\n\t}\n\n\t/**\n\t * 创建StringBuilder对象\n\t *\n\t * @param capacity 初始大小\n\t * @return StringBuilder对象\n\t */\n\tpublic static StringBuilder builder(int capacity) {\n\t\treturn new StringBuilder(capacity);\n\t}\n\n\t/**\n\t * 创建StrBuilder对象\n\t *\n\t * @param capacity 初始大小\n\t * @return StrBuilder对象\n\t * @since 4.0.1\n\t */\n\tpublic static StrBuilder strBuilder(int capacity) {\n\t\treturn StrBuilder.create(capacity);\n\t}\n\n\t/**\n\t * 获得StringReader\n\t *\n\t * @param str 字符串\n\t * @return StringReader\n\t */\n\tpublic static StringReader getReader(CharSequence str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new StringReader(str.toString());\n\t}\n\n\t/**\n\t * 获得StringWriter\n\t *\n\t * @return StringWriter\n\t */\n\tpublic static StringWriter getWriter() {\n\t\treturn new StringWriter();\n\t}\n\n\t/**\n\t * 反转字符串<br>\n\t * 例如：abcd =》dcba\n\t *\n\t * @param str 被反转的字符串\n\t * @return 反转后的字符串\n\t * @since 3.0.9\n\t */\n\tpublic static String reverse(final String str) {\n\t\tif (isBlank(str)) {\n\t\t\treturn str;\n\t\t}\n\t\treturn new String(ArrayUtil.reverse(str.toCharArray()));\n\t}\n\n\t/**\n\t * 反转字符串<br>\n\t * 例如：abcd =》dcba\n\t * <p>\n\t * 该方法按Unicode code point进行反转，支持Unicode字符的正确反转\n\t * 确保复杂字符不会被拆分，如表情符号等多字节字符\n\t * </p>\n\t *\n\t * @param str 被反转的字符串\n\t * @return 反转后的字符串，如果输入为null则返回null\n\t * @since 5.8.43\n\t */\n\tpublic static String reverseByCodePoint(String str) {\n\t\tif (null == str) {\n\t\t\treturn null;\n\t\t}\n\n\t\t//按Unicode code point方式进行反转处理\n\t\tStringBuilder result = new StringBuilder();\n\t\tfor (int i = str.length(); i > 0; ) {\n\t\t\t//获取指定位置前的code point\n\t\t\tint codePoint = str.codePointBefore(i);\n\t\t\t//根据code point的字符数量调整索引位置\n\t\t\ti -= Character.charCount(codePoint);\n\t\t\t//将code point追加到结果中\n\t\t\tresult.appendCodePoint(codePoint);\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t// ------------------------------------------------------------------------ fill\n\n\t/**\n\t * 将已有字符串填充为规定长度，如果已有字符串超过这个长度则返回这个字符串<br>\n\t * 字符填充于字符串前\n\t *\n\t * @param str        被填充的字符串\n\t * @param filledChar 填充的字符\n\t * @param len        填充长度\n\t * @return 填充后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String fillBefore(String str, char filledChar, int len) {\n\t\treturn fill(str, filledChar, len, true);\n\t}\n\n\t/**\n\t * 将已有字符串填充为规定长度，如果已有字符串超过这个长度则返回这个字符串<br>\n\t * 字符填充于字符串后\n\t *\n\t * @param str        被填充的字符串\n\t * @param filledChar 填充的字符\n\t * @param len        填充长度\n\t * @return 填充后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String fillAfter(String str, char filledChar, int len) {\n\t\treturn fill(str, filledChar, len, false);\n\t}\n\n\t/**\n\t * 将已有字符串填充为规定长度，如果已有字符串超过这个长度则返回这个字符串\n\t *\n\t * @param str        被填充的字符串\n\t * @param filledChar 填充的字符\n\t * @param len        填充长度\n\t * @param isPre      是否填充在前\n\t * @return 填充后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String fill(String str, char filledChar, int len, boolean isPre) {\n\t\tif (null == str) {\n\t\t\tstr = \"\";\n\t\t}\n\t\tfinal int strLen = str.length();\n\t\tif (strLen > len) {\n\t\t\treturn str;\n\t\t}\n\n\t\tString filledStr = StrUtil.repeat(filledChar, len - strLen);\n\t\treturn isPre ? filledStr.concat(str) : str.concat(filledStr);\n\t}\n\n\t/**\n\t * 计算两个字符串的相似度\n\t *\n\t * @param str1 字符串1\n\t * @param str2 字符串2\n\t * @return 相似度\n\t * @since 3.2.3\n\t */\n\tpublic static double similar(String str1, String str2) {\n\t\treturn TextSimilarity.similar(str1, str2);\n\t}\n\n\t/**\n\t * 计算两个字符串的相似度百分比\n\t *\n\t * @param str1  字符串1\n\t * @param str2  字符串2\n\t * @param scale 相似度\n\t * @return 相似度百分比\n\t * @since 3.2.3\n\t */\n\tpublic static String similar(String str1, String str2, int scale) {\n\t\treturn TextSimilarity.similar(str1, str2, scale);\n\t}\n\n\t/**\n\t * 生成随机UUID\n\t *\n\t * @return UUID字符串\n\t * @see IdUtil#randomUUID()\n\t * @since 4.0.10\n\t */\n\tpublic static String uuid() {\n\t\treturn IdUtil.randomUUID();\n\t}\n\n\t/**\n\t * 格式化文本，使用 {varName} 占位<br>\n\t * map = {a: \"aValue\", b: \"bValue\"} format(\"{a} and {b}\", map) ---=》 aValue and bValue\n\t *\n\t * @param template 文本模板，被替换的部分用 {key} 表示\n\t * @param map      参数值对\n\t * @return 格式化后的文本\n\t */\n\tpublic static String format(CharSequence template, Map<?, ?> map) {\n\t\treturn format(template, map, true);\n\t}\n\n\t/**\n\t * 格式化文本，使用 {varName} 占位<br>\n\t * map = {a: \"aValue\", b: \"bValue\"} format(\"{a} and {b}\", map) ---=》 aValue and bValue\n\t *\n\t * @param template   文本模板，被替换的部分用 {key} 表示\n\t * @param map        参数值对\n\t * @param ignoreNull 是否忽略 {@code null} 值，忽略则 {@code null} 值对应的变量不被替换，否则替换为\"\"\n\t * @return 格式化后的文本\n\t * @since 5.4.3\n\t */\n\tpublic static String format(CharSequence template, Map<?, ?> map, boolean ignoreNull) {\n\t\treturn StrFormatter.format(template, map, ignoreNull);\n\t}\n\n\t/**\n\t * 截断字符串，使用其按照UTF-8编码为字节后不超过maxBytes长度。截断后自动追加省略号(...)\n\t * 用于存储数据库varchar且编码为UTF-8的字段\n\t *\n\t * @param str      java字符串\n\t * @param maxBytes 最大字节长度\n\t * @return 截断后的字符\n\t */\n\tpublic static String truncateUtf8(String str, int maxBytes) {\n\t\tCharset charset = StandardCharsets.UTF_8;\n\t\t//UTF-8编码单个字符最大长度4\n\t\treturn truncateByByteLength(str, charset, maxBytes, 4, true);\n\t}\n\n\t/**\n\t * 截断字符串，使用其按照指定编码为字节后不超过maxBytes长度<br>\n\t * 此方法用于截取总bytes数不超过指定长度，如果字符出没有超出原样输出，如果超出了，则截取掉超出部分，并可选添加...，\n\t * 但是添加“...”后总长度也不超过限制长度。\n\t *\n\t * @param str            原始字符串\n\t * @param charset        指定编码\n\t * @param maxBytesLength 最大字节数\n\t * @param factor         速算因子，取该编码下单个字符的最大可能字节数\n\t * @param appendDots     截断后是否追加省略号...，如果maxBytesLength小于省略号长度，则不添加...\n\t * @return 截断后的字符串\n\t */\n\tpublic static String truncateByByteLength(String str, Charset charset, int maxBytesLength, int factor,\n\t\t\t\t\t\t\t\t\t\t\t  boolean appendDots) {\n\t\t//字符数*速算因子<=最大字节数\n\t\tif (str == null || str.length() * factor <= maxBytesLength) {\n\t\t\treturn str;\n\t\t}\n\t\tfinal byte[] sba = str.getBytes(charset);\n\t\tif (sba.length <= maxBytesLength) {\n\t\t\treturn str;\n\t\t}\n\t\t//限制字节数\n\t\tfinal int dotsBytesLength = \"...\".getBytes(charset).length;\n\t\tfinal int limitBytes;\n\t\t// issue#IDFTJS 修正截断后追加省略号...导致超出限制长度的问题\n\t\tif (appendDots && maxBytesLength > dotsBytesLength) {\n\t\t\tlimitBytes = maxBytesLength - \"...\".getBytes(charset).length;\n\t\t} else {\n\t\t\tlimitBytes = maxBytesLength;\n\t\t}\n\t\tfinal ByteBuffer bb = ByteBuffer.wrap(sba, 0, limitBytes);\n\t\tfinal CharBuffer cb = CharBuffer.allocate(limitBytes);\n\t\tfinal CharsetDecoder decoder = charset.newDecoder();\n\t\t//忽略被截断的字符\n\t\tdecoder.onMalformedInput(CodingErrorAction.IGNORE);\n\t\tdecoder.decode(bb, cb, true);\n\t\tdecoder.flush(cb);\n\t\tfinal String result = new String(cb.array(), 0, cb.position());\n\t\tif (appendDots && maxBytesLength > dotsBytesLength) {\n\t\t\treturn result + \"...\";\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/SystemPropsUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Console;\n\nimport java.util.Properties;\n\n/**\n * 系统属性工具<br>\n * 此工具用于读取系统属性或环境变量信息，封装包括：\n * <ul>\n *     <li>{@link System#getProperty(String)}</li>\n *     <li>{@link System#getenv(String)}</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.16\n */\npublic class SystemPropsUtil {\n\n\t/** Hutool自定义系统属性：是否解析日期字符串采用严格模式 */\n\tpublic static String HUTOOL_DATE_LENIENT = \"hutool.date.lenient\";\n\n\t/**\n\t * 取得系统属性，如果因为Java安全的限制而失败，则将错误打在Log中，然后返回 defaultValue\n\t *\n\t * @param name         属性名\n\t * @param defaultValue 默认值\n\t * @return 属性值或defaultValue\n\t * @see System#getProperty(String)\n\t * @see System#getenv(String)\n\t */\n\tpublic static String get(String name, String defaultValue) {\n\t\treturn StrUtil.nullToDefault(get(name, false), defaultValue);\n\t}\n\n\t/**\n\t * 取得系统属性，如果因为Java安全的限制而失败，则将错误打在Log中，然后返回 {@code null}\n\t *\n\t * @param name  属性名\n\t * @param quiet 安静模式，不将出错信息打在{@code System.err}中\n\t * @return 属性值或{@code null}\n\t * @see System#getProperty(String)\n\t * @see System#getenv(String)\n\t */\n\tpublic static String get(String name, boolean quiet) {\n\t\tString value = null;\n\t\ttry {\n\t\t\tvalue = System.getProperty(name);\n\t\t} catch (SecurityException e) {\n\t\t\tif (false == quiet) {\n\t\t\t\tConsole.error(\"Caught a SecurityException reading the system property '{}'; \" +\n\t\t\t\t\t\t\"the SystemUtil property value will default to null.\", name);\n\t\t\t}\n\t\t}\n\n\t\tif (null == value) {\n\t\t\ttry {\n\t\t\t\tvalue = System.getenv(name);\n\t\t\t} catch (SecurityException e) {\n\t\t\t\tif (false == quiet) {\n\t\t\t\t\tConsole.error(\"Caught a SecurityException reading the system env '{}'; \" +\n\t\t\t\t\t\t\t\"the SystemUtil env value will default to null.\", name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * 获得System属性\n\t *\n\t * @param key 键\n\t * @return 属性值\n\t * @see System#getProperty(String)\n\t * @see System#getenv(String)\n\t */\n\tpublic static String get(String key) {\n\t\treturn get(key, null);\n\t}\n\n\t/**\n\t * 获得boolean类型值\n\t *\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t */\n\tpublic static boolean getBoolean(String key, boolean defaultValue) {\n\t\tString value = get(key);\n\t\tif (value == null) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\treturn BooleanUtil.toBoolean(value);\n\t}\n\n\t/**\n\t * 获得int类型值\n\t *\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t */\n\tpublic static int getInt(String key, int defaultValue) {\n\t\treturn Convert.toInt(get(key), defaultValue);\n\t}\n\n\t/**\n\t * 获得long类型值\n\t *\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 值\n\t */\n\tpublic static long getLong(String key, long defaultValue) {\n\t\treturn Convert.toLong(get(key), defaultValue);\n\t}\n\n\t/**\n\t * @return 属性列表\n\t */\n\tpublic static Properties getProps() {\n\t\treturn System.getProperties();\n\t}\n\n\t/**\n\t * 设置系统属性，value为{@code null}表示移除此属性\n\t *\n\t * @param key   属性名\n\t * @param value 属性值，{@code null}表示移除此属性\n\t */\n\tpublic static void set(String key, String value) {\n\t\tif (null == value) {\n\t\t\tSystem.clearProperty(key);\n\t\t} else {\n\t\t\tSystem.setProperty(key, value);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/TypeUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.ParameterizedTypeImpl;\nimport cn.hutool.core.lang.reflect.ActualTypeMapperPool;\n\nimport java.lang.reflect.*;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 针对 {@link Type} 的工具类封装<br>\n * 最主要功能包括：\n *\n * <pre>\n * 1. 获取方法的参数和返回值类型（包括Type和Class）\n * 2. 获取泛型参数类型（包括对象的泛型参数或集合元素的泛型类型）\n * </pre>\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class TypeUtil {\n\n\t/**\n\t * 获得Type对应的原始类\n\t *\n\t * @param type {@link Type}\n\t * @return 原始类，如果无法获取原始类，返回{@code null}\n\t */\n\tpublic static Class<?> getClass(Type type) {\n\t\tif (null != type) {\n\t\t\tif (type instanceof Class) {\n\t\t\t\treturn (Class<?>) type;\n\t\t\t} else if (type instanceof ParameterizedType) {\n\t\t\t\treturn (Class<?>) ((ParameterizedType) type).getRawType();\n\t\t\t} else if (type instanceof GenericArrayType) {\n\t\t\t\tfinal Type componentType = ((GenericArrayType) type).getGenericComponentType();\n\t\t\t\tfinal Class<?> componentClass = getClass(componentType);\n\t\t\t\tif (componentClass != null) {\n\t\t\t\t\treturn Array.newInstance(componentClass, 0).getClass();\n\t\t\t\t}\n\t\t\t} else if (type instanceof TypeVariable) {\n\t\t\t\tType[] bounds = ((TypeVariable<?>) type).getBounds();\n\t\t\t\tif (bounds.length == 1) {\n\t\t\t\t\treturn getClass(bounds[0]);\n\t\t\t\t}\n\t\t\t} else if (type instanceof WildcardType) {\n\t\t\t\tfinal Type[] upperBounds = ((WildcardType) type).getUpperBounds();\n\t\t\t\tif (upperBounds.length == 1) {\n\t\t\t\t\treturn getClass(upperBounds[0]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取字段对应的Type类型<br>\n\t * 方法优先获取GenericType，获取不到则获取Type\n\t *\n\t * @param field 字段\n\t * @return {@link Type}，可能为{@code null}\n\t */\n\tpublic static Type getType(Field field) {\n\t\tif (null == field) {\n\t\t\treturn null;\n\t\t}\n\t\treturn field.getGenericType();\n\t}\n\n\t/**\n\t * 获得字段的泛型类型\n\t *\n\t * @param clazz     Bean类\n\t * @param fieldName 字段名\n\t * @return 字段的泛型类型\n\t * @since 5.4.2\n\t */\n\tpublic static Type getFieldType(Class<?> clazz, String fieldName) {\n\t\treturn getType(ReflectUtil.getField(clazz, fieldName));\n\t}\n\n\t/**\n\t * 获得Field对应的原始类\n\t *\n\t * @param field {@link Field}\n\t * @return 原始类，如果无法获取原始类，返回{@code null}\n\t * @since 3.1.2\n\t */\n\tpublic static Class<?> getClass(Field field) {\n\t\treturn null == field ? null : field.getType();\n\t}\n\n\t// ----------------------------------------------------------------------------------- Param Type\n\n\t/**\n\t * 获取方法的第一个参数类型<br>\n\t * 优先获取方法的GenericParameterTypes，如果获取不到，则获取ParameterTypes\n\t *\n\t * @param method 方法\n\t * @return {@link Type}，可能为{@code null}\n\t * @since 3.1.2\n\t */\n\tpublic static Type getFirstParamType(Method method) {\n\t\treturn getParamType(method, 0);\n\t}\n\n\t/**\n\t * 获取方法的第一个参数类\n\t *\n\t * @param method 方法\n\t * @return 第一个参数类型，可能为{@code null}\n\t * @since 3.1.2\n\t */\n\tpublic static Class<?> getFirstParamClass(Method method) {\n\t\treturn getParamClass(method, 0);\n\t}\n\n\t/**\n\t * 获取方法的参数类型<br>\n\t * 优先获取方法的GenericParameterTypes，如果获取不到，则获取ParameterTypes\n\t *\n\t * @param method 方法\n\t * @param index  第几个参数的索引，从0开始计数\n\t * @return {@link Type}，可能为{@code null}\n\t */\n\tpublic static Type getParamType(Method method, int index) {\n\t\tType[] types = getParamTypes(method);\n\t\tif (null != types && types.length > index) {\n\t\t\treturn types[index];\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取方法的参数类\n\t *\n\t * @param method 方法\n\t * @param index  第几个参数的索引，从0开始计数\n\t * @return 参数类，可能为{@code null}\n\t * @since 3.1.2\n\t */\n\tpublic static Class<?> getParamClass(Method method, int index) {\n\t\tClass<?>[] classes = getParamClasses(method);\n\t\tif (null != classes && classes.length > index) {\n\t\t\treturn classes[index];\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取方法的参数类型列表<br>\n\t * 优先获取方法的GenericParameterTypes，如果获取不到，则获取ParameterTypes\n\t *\n\t * @param method 方法\n\t * @return {@link Type}列表，可能为{@code null}\n\t * @see Method#getGenericParameterTypes()\n\t * @see Method#getParameterTypes()\n\t */\n\tpublic static Type[] getParamTypes(Method method) {\n\t\treturn null == method ? null : method.getGenericParameterTypes();\n\t}\n\n\t/**\n\t * 解析方法的参数类型列表<br>\n\t * 依赖jre\\lib\\rt.jar\n\t *\n\t * @param method t方法\n\t * @return 参数类型类列表\n\t * @see Method#getGenericParameterTypes\n\t * @see Method#getParameterTypes\n\t * @since 3.1.2\n\t */\n\tpublic static Class<?>[] getParamClasses(Method method) {\n\t\treturn null == method ? null : method.getParameterTypes();\n\t}\n\n\t// ----------------------------------------------------------------------------------- Return Type\n\n\t/**\n\t * 获取方法的返回值类型<br>\n\t * 获取方法的GenericReturnType\n\t *\n\t * @param method 方法\n\t * @return {@link Type}，可能为{@code null}\n\t * @see Method#getGenericReturnType()\n\t * @see Method#getReturnType()\n\t */\n\tpublic static Type getReturnType(Method method) {\n\t\treturn null == method ? null : method.getGenericReturnType();\n\t}\n\n\t/**\n\t * 解析方法的返回类型类列表\n\t *\n\t * @param method 方法\n\t * @return 返回值类型的类\n\t * @see Method#getGenericReturnType\n\t * @see Method#getReturnType\n\t * @since 3.1.2\n\t */\n\tpublic static Class<?> getReturnClass(Method method) {\n\t\treturn null == method ? null : method.getReturnType();\n\t}\n\n\t// ----------------------------------------------------------------------------------- Type Argument\n\n\t/**\n\t * 获得给定类的第一个泛型参数\n\t *\n\t * @param type 被检查的类型，必须是已经确定泛型类型的类型\n\t * @return {@link Type}，可能为{@code null}\n\t */\n\tpublic static Type getTypeArgument(Type type) {\n\t\treturn getTypeArgument(type, 0);\n\t}\n\n\t/**\n\t * 获得给定类的泛型参数\n\t *\n\t * @param type  被检查的类型，必须是已经确定泛型类型的类\n\t * @param index 泛型类型的索引号，即第几个泛型类型\n\t * @return {@link Type}\n\t */\n\tpublic static Type getTypeArgument(Type type, int index) {\n\t\tfinal Type[] typeArguments = getTypeArguments(type);\n\t\tif (null != typeArguments && typeArguments.length > index) {\n\t\t\treturn typeArguments[index];\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得指定类型中所有泛型参数类型，例如：\n\t *\n\t * <pre>\n\t * class A&lt;T&gt;\n\t * class B extends A&lt;String&gt;\n\t * </pre>\n\t * <p>\n\t * 通过此方法，传入B.class即可得到String\n\t *\n\t * @param type 指定类型\n\t * @return 所有泛型参数类型\n\t */\n\tpublic static Type[] getTypeArguments(Type type) {\n\t\tif (null == type) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal ParameterizedType parameterizedType = toParameterizedType(type);\n\t\treturn (null == parameterizedType) ? null : parameterizedType.getActualTypeArguments();\n\t}\n\n\t/**\n\t * 将{@link Type} 转换为{@link ParameterizedType}<br>\n\t * {@link ParameterizedType}用于获取当前类或父类中泛型参数化后的类型<br>\n\t * 一般用于获取泛型参数具体的参数类型，例如：\n\t *\n\t * <pre>\n\t * class A&lt;T&gt;\n\t * class B extends A&lt;String&gt;\n\t * </pre>\n\t * <p>\n\t * 通过此方法，传入B.class即可得到B{@link ParameterizedType}，从而获取到String\n\t *\n\t * @param type {@link Type}\n\t * @return {@link ParameterizedType}\n\t * @since 4.5.2\n\t */\n\tpublic static ParameterizedType toParameterizedType(final Type type) {\n\t\treturn toParameterizedType(type, 0);\n\t}\n\n\t/**\n\t * 将{@link Type} 转换为{@link ParameterizedType}<br>\n\t * {@link ParameterizedType}用于获取当前类或父类中泛型参数化后的类型<br>\n\t * 一般用于获取泛型参数具体的参数类型，例如：\n\t *\n\t * <pre>{@code\n\t *   class A<T>\n\t *   class B extends A<String>;\n\t * }</pre>\n\t * <p>\n\t * 通过此方法，传入B.class即可得到B对应的{@link ParameterizedType}，从而获取到String\n\t *\n\t * @param type           {@link Type}\n\t * @param interfaceIndex 实现的第几个接口\n\t * @return {@link ParameterizedType}\n\t * @since 4.5.2\n\t */\n\tpublic static ParameterizedType toParameterizedType(final Type type, final int interfaceIndex) {\n\t\tif (type instanceof ParameterizedType) {\n\t\t\treturn (ParameterizedType) type;\n\t\t}\n\n\t\tif (type instanceof Class) {\n\t\t\tfinal ParameterizedType[] generics = getGenerics((Class<?>) type);\n\t\t\tif(generics.length > interfaceIndex){\n\t\t\t\treturn generics[interfaceIndex];\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定类所有泛型父类和泛型接口\n\t *\n\t * @param clazz 类\n\t * @return 泛型父类或接口数组\n\t */\n\tpublic static ParameterizedType[] getGenerics(final Class<?> clazz) {\n\t\tfinal List<ParameterizedType> result = new ArrayList<>();\n\t\t// 泛型父类（父类及祖类优先级高）\n\t\tfinal Type genericSuper = clazz.getGenericSuperclass();\n\t\tif(null != genericSuper && !Object.class.equals(genericSuper)){\n\t\t\tfinal ParameterizedType parameterizedType = toParameterizedType(genericSuper);\n\t\t\tif(null != parameterizedType){\n\t\t\t\tresult.add(parameterizedType);\n\t\t\t}\n\t\t}\n\n\t\t// 泛型接口\n\t\tfinal Type[] genericInterfaces = clazz.getGenericInterfaces();\n\t\tif (ArrayUtil.isNotEmpty(genericInterfaces)) {\n\t\t\tfor (final Type genericInterface : genericInterfaces) {\n\t\t\t\tfinal ParameterizedType parameterizedType = toParameterizedType(genericInterface);\n\t\t\t\tif(null != parameterizedType){\n\t\t\t\t\tresult.add(parameterizedType);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result.toArray(new ParameterizedType[0]);\n\t}\n\n\t/**\n\t * 是否未知类型<br>\n\t * type为null或者{@link TypeVariable} 都视为未知类型\n\t *\n\t * @param type Type类型\n\t * @return 是否未知类型\n\t * @since 4.5.2\n\t */\n\tpublic static boolean isUnknown(Type type) {\n\t\treturn null == type || type instanceof TypeVariable;\n\t}\n\n\t/**\n\t * 指定泛型数组中是否含有泛型变量\n\t *\n\t * @param types 泛型数组\n\t * @return 是否含有泛型变量\n\t * @since 4.5.7\n\t */\n\tpublic static boolean hasTypeVariable(Type... types) {\n\t\tfor (Type type : types) {\n\t\t\tif (type instanceof TypeVariable) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取泛型变量和泛型实际类型的对应关系Map，例如：\n\t *\n\t * <pre>\n\t *     T    cn.hutool.test.User\n\t *     E    java.lang.Integer\n\t * </pre>\n\t *\n\t * @param clazz 被解析的包含泛型参数的类\n\t * @return 泛型对应关系Map\n\t */\n\tpublic static Map<Type, Type> getTypeMap(Class<?> clazz) {\n\t\treturn ActualTypeMapperPool.get(clazz);\n\t}\n\n\t/**\n\t * 获得泛型字段对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t *\n\t * @param type  实际类型明确的类\n\t * @param field 字段\n\t * @return 实际类型，可能为Class等\n\t */\n\tpublic static Type getActualType(Type type, Field field) {\n\t\tif (null == field) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getActualType(ObjectUtil.defaultIfNull(type, field::getDeclaringClass), field.getGenericType());\n\t}\n\n\t/**\n\t * 获得泛型变量对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t * 此方法可以处理：\n\t *\n\t * <pre>\n\t *     1. 泛型化对象，类似于Map&lt;User, Key&lt;Long&gt;&gt;\n\t *     2. 泛型变量，类似于T\n\t * </pre>\n\t *\n\t * @param type         类\n\t * @param typeVariable 泛型变量，例如T等\n\t * @return 实际类型，可能为Class等\n\t */\n\tpublic static Type getActualType(Type type, Type typeVariable) {\n\t\tif (typeVariable instanceof ParameterizedType) {\n\t\t\treturn getActualType(type, (ParameterizedType) typeVariable);\n\t\t}\n\n\t\tif (typeVariable instanceof TypeVariable) {\n\t\t\treturn ActualTypeMapperPool.getActualType(type, (TypeVariable<?>) typeVariable);\n\t\t}\n\t\tif (typeVariable instanceof GenericArrayType) {\n\t\t\t//return ActualTypeMapperPool.getActualType(type, (GenericArrayType) typeVariable);\n\t\t\tfinal Type actualType = ActualTypeMapperPool.getActualType(type, (GenericArrayType) typeVariable);\n\t\t\tif(null != actualType){\n\t\t\t\treturn actualType;\n\t\t\t}\n\t\t}\n\n\t\t// 没有需要替换的泛型变量，原样输出\n\t\treturn typeVariable;\n\t}\n\n\t/**\n\t * 获得泛型变量对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t * 此方法可以处理复杂的泛型化对象，类似于Map&lt;User, Key&lt;Long&gt;&gt;\n\t *\n\t * @param type              类\n\t * @param parameterizedType 泛型变量，例如List&lt;T&gt;等\n\t * @return 实际类型，可能为Class等\n\t */\n\tpublic static Type getActualType(Type type, ParameterizedType parameterizedType) {\n\t\t// 字段类型为泛型参数类型，解析对应泛型类型为真实类型，类似于List<T> a\n\t\tType[] actualTypeArguments = parameterizedType.getActualTypeArguments();\n\n\t\t// 泛型对象中含有未被转换的泛型变量\n\t\tif (TypeUtil.hasTypeVariable(actualTypeArguments)) {\n\t\t\tactualTypeArguments = getActualTypes(type, parameterizedType.getActualTypeArguments());\n\t\t\tif (ArrayUtil.isNotEmpty(actualTypeArguments)) {\n\t\t\t\t// 替换泛型变量为实际类型，例如List<T>变为List<String>\n\t\t\t\tparameterizedType = new ParameterizedTypeImpl(actualTypeArguments, parameterizedType.getOwnerType(), parameterizedType.getRawType());\n\t\t\t}\n\t\t}\n\n\t\treturn parameterizedType;\n\t}\n\n\t/**\n\t * 获得泛型变量对应的泛型实际类型，如果此变量没有对应的实际类型，返回null\n\t *\n\t * @param type          类\n\t * @param typeVariables 泛型变量数组，例如T等\n\t * @return 实际类型数组，可能为Class等\n\t */\n\tpublic static Type[] getActualTypes(Type type, Type... typeVariables) {\n\t\treturn ActualTypeMapperPool.getActualTypes(type, typeVariables);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.URLDecoder;\nimport cn.hutool.core.net.URLEncodeUtil;\nimport cn.hutool.core.net.url.UrlQuery;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.JarURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.net.URLStreamHandler;\nimport java.nio.charset.Charset;\nimport java.util.Map;\nimport java.util.jar.JarFile;\n\n/**\n * URL（Uniform Resource Locator）统一资源定位符相关工具类\n *\n * <p>\n * 统一资源定位符，描述了一台特定服务器上某资源的特定位置。\n * </p>\n * URL组成：\n * <pre>\n *   协议://主机名[:端口]/ 路径/[:参数] [?查询]#Fragment\n *   protocol :// hostname[:port] / path / [:parameters][?query]#fragment\n * </pre>\n *\n * @author xiaoleilu\n */\npublic class URLUtil extends URLEncodeUtil {\n\n\t/**\n\t * 针对ClassPath路径的伪协议前缀（兼容Spring）: \"classpath:\"\n\t */\n\tpublic static final String CLASSPATH_URL_PREFIX = \"classpath:\";\n\t/**\n\t * URL 前缀表示文件: \"file:\"\n\t */\n\tpublic static final String FILE_URL_PREFIX = \"file:\";\n\t/**\n\t * URL 前缀表示jar: \"jar:\"\n\t */\n\tpublic static final String JAR_URL_PREFIX = \"jar:\";\n\t/**\n\t * URL 前缀表示war: \"war:\"\n\t */\n\tpublic static final String WAR_URL_PREFIX = \"war:\";\n\t/**\n\t * URL 协议表示文件: \"file\"\n\t */\n\tpublic static final String URL_PROTOCOL_FILE = \"file\";\n\t/**\n\t * URL 协议表示Jar文件: \"jar\"\n\t */\n\tpublic static final String URL_PROTOCOL_JAR = \"jar\";\n\t/**\n\t * URL 协议表示zip文件: \"zip\"\n\t */\n\tpublic static final String URL_PROTOCOL_ZIP = \"zip\";\n\t/**\n\t * URL 协议表示WebSphere文件: \"wsjar\"\n\t */\n\tpublic static final String URL_PROTOCOL_WSJAR = \"wsjar\";\n\t/**\n\t * URL 协议表示JBoss zip文件: \"vfszip\"\n\t */\n\tpublic static final String URL_PROTOCOL_VFSZIP = \"vfszip\";\n\t/**\n\t * URL 协议表示JBoss文件: \"vfsfile\"\n\t */\n\tpublic static final String URL_PROTOCOL_VFSFILE = \"vfsfile\";\n\t/**\n\t * URL 协议表示JBoss VFS资源: \"vfs\"\n\t */\n\tpublic static final String URL_PROTOCOL_VFS = \"vfs\";\n\t/**\n\t * Jar路径以及内部文件路径的分界符: \"!/\"\n\t */\n\tpublic static final String JAR_URL_SEPARATOR = \"!/\";\n\t/**\n\t * WAR路径及内部文件路径分界符\n\t */\n\tpublic static final String WAR_URL_SEPARATOR = \"*/\";\n\n\t/**\n\t * 将{@link URI}转换为{@link URL}\n\t *\n\t * @param uri {@link URI}\n\t * @return URL对象\n\t * @see URI#toURL()\n\t * @throws UtilException {@link MalformedURLException}包装，URI格式有问题时抛出\n\t * @since 5.7.21\n\t */\n\tpublic static URL url(URI uri) throws UtilException{\n\t\tif(null == uri){\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn uri.toURL();\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 通过一个字符串形式的URL地址创建URL对象\n\t *\n\t * @param url URL\n\t * @return URL对象\n\t */\n\tpublic static URL url(String url) {\n\t\treturn url(url, null);\n\t}\n\n\t/**\n\t * 通过一个字符串形式的URL地址创建URL对象\n\t *\n\t * @param url     URL\n\t * @param handler {@link URLStreamHandler}\n\t * @return URL对象\n\t * @since 4.1.1\n\t */\n\tpublic static URL url(String url, URLStreamHandler handler) {\n\t\tif(null == url){\n\t\t\treturn null;\n\t\t}\n\n\t\t// 兼容Spring的ClassPath路径\n\t\tif (url.startsWith(CLASSPATH_URL_PREFIX)) {\n\t\t\turl = url.substring(CLASSPATH_URL_PREFIX.length());\n\t\t\treturn ClassLoaderUtil.getClassLoader().getResource(url);\n\t\t}\n\n\t\ttry {\n\t\t\treturn new URL(null, url, handler);\n\t\t} catch (MalformedURLException e) {\n\t\t\t// issue#I8PY3Y\n\t\t\tif(e.getMessage().contains(\"Accessing an URL protocol that was not enabled\")){\n\t\t\t\t// Graalvm打包需要手动指定参数开启协议：\n\t\t\t\t// --enable-url-protocols=http\n\t\t\t\t// --enable-url-protocols=https\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\n\t\t\t// 尝试文件路径\n\t\t\ttry {\n\t\t\t\treturn new File(url).toURI().toURL();\n\t\t\t} catch (MalformedURLException ex2) {\n\t\t\t\tthrow new UtilException(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取string协议的URL，类似于string:///xxxxx\n\t *\n\t * @param content 正文\n\t * @return URL\n\t * @since 5.5.2\n\t */\n\tpublic static URI getStringURI(CharSequence content) {\n\t\tif(null == content){\n\t\t\treturn null;\n\t\t}\n\t\tfinal String contentStr = StrUtil.addPrefixIfNot(content, \"string:///\");\n\t\treturn URI.create(contentStr);\n\t}\n\n\t/**\n\t * 将URL字符串转换为URL对象，并做必要验证\n\t *\n\t * @param urlStr URL字符串\n\t * @return URL\n\t * @since 4.1.9\n\t */\n\tpublic static URL toUrlForHttp(String urlStr) {\n\t\treturn toUrlForHttp(urlStr, null);\n\t}\n\n\t/**\n\t * 将URL字符串转换为URL对象，并做必要验证\n\t *\n\t * @param urlStr  URL字符串\n\t * @param handler {@link URLStreamHandler}\n\t * @return URL\n\t * @since 4.1.9\n\t */\n\tpublic static URL toUrlForHttp(String urlStr, URLStreamHandler handler) {\n\t\tAssert.notBlank(urlStr, \"Url is blank !\");\n\t\t// 编码空白符，防止空格引起的请求异常\n\t\turlStr = encodeBlank(urlStr);\n\t\ttry {\n\t\t\treturn new URL(null, urlStr, handler);\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 单独编码URL中的空白符，空白符编码为%20\n\t *\n\t * @param urlStr URL字符串\n\t * @return 编码后的字符串\n\t * @since 4.5.14\n\t */\n\tpublic static String encodeBlank(CharSequence urlStr) {\n\t\tif (urlStr == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tint len = urlStr.length();\n\t\tfinal StringBuilder sb = new StringBuilder(len);\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = urlStr.charAt(i);\n\t\t\tif (CharUtil.isBlankChar(c)) {\n\t\t\t\tsb.append(\"%20\");\n\t\t\t} else {\n\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 获得URL\n\t *\n\t * @param pathBaseClassLoader 相对路径（相对于classes）\n\t * @return URL\n\t * @see ResourceUtil#getResource(String)\n\t */\n\tpublic static URL getURL(String pathBaseClassLoader) {\n\t\treturn ResourceUtil.getResource(pathBaseClassLoader);\n\t}\n\n\t/**\n\t * 获得URL\n\t *\n\t * @param path  相对给定 class所在的路径\n\t * @param clazz 指定class\n\t * @return URL\n\t * @see ResourceUtil#getResource(String, Class)\n\t */\n\tpublic static URL getURL(String path, Class<?> clazz) {\n\t\treturn ResourceUtil.getResource(path, clazz);\n\t}\n\n\t/**\n\t * 获得URL，常用于使用绝对路径时的情况\n\t *\n\t * @param file URL对应的文件对象\n\t * @return URL\n\t * @throws UtilException MalformedURLException\n\t */\n\tpublic static URL getURL(File file) {\n\t\tAssert.notNull(file, \"File is null !\");\n\t\ttry {\n\t\t\treturn file.toURI().toURL();\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new UtilException(e, \"Error occurred when get URL!\");\n\t\t}\n\t}\n\n\t/**\n\t * 获得URL，常用于使用绝对路径时的情况\n\t *\n\t * @param files URL对应的文件对象\n\t * @return URL\n\t * @throws UtilException MalformedURLException\n\t */\n\tpublic static URL[] getURLs(File... files) {\n\t\tfinal URL[] urls = new URL[files.length];\n\t\ttry {\n\t\t\tfor (int i = 0; i < files.length; i++) {\n\t\t\t\turls[i] = files[i].toURI().toURL();\n\t\t\t}\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new UtilException(e, \"Error occurred when get URL!\");\n\t\t}\n\n\t\treturn urls;\n\t}\n\n\t/**\n\t * 获取URL中域名部分，只保留URL中的协议（Protocol）、Host，其它为null。\n\t *\n\t * @param url URL\n\t * @return 域名的URI\n\t * @since 4.6.9\n\t */\n\tpublic static URI getHost(URL url) {\n\t\tif (null == url) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\treturn new URI(url.getProtocol(), url.getHost(), null, null);\n\t\t} catch (URISyntaxException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 补全相对路径\n\t *\n\t * @param baseUrl      基准URL\n\t * @param relativePath 相对URL\n\t * @return 相对路径\n\t * @throws UtilException MalformedURLException\n\t */\n\tpublic static String completeUrl(String baseUrl, String relativePath) {\n\t\tbaseUrl = normalize(baseUrl, false);\n\t\tif (StrUtil.isBlank(baseUrl)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tfinal URL absoluteUrl = new URL(baseUrl);\n\t\t\tfinal URL parseUrl = new URL(absoluteUrl, relativePath);\n\t\t\treturn parseUrl.toString();\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\t//-------------------------------------------------------------------------- decode\n\n\t/**\n\t * 解码URL<br>\n\t * 将%开头的16进制表示的内容解码。\n\t *\n\t * @param url URL\n\t * @return 解码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t * @since 3.1.2\n\t */\n\tpublic static String decode(String url) throws UtilException {\n\t\treturn decode(url, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 解码application/x-www-form-urlencoded字符<br>\n\t * 将%开头的16进制表示的内容解码。<br>\n\t * 规则见：https://url.spec.whatwg.org/#urlencoded-parsing\n\t *\n\t * @param content 被解码内容\n\t * @param charset 编码，null表示不解码\n\t * @return 编码后的字符\n\t * @since 4.4.1\n\t */\n\tpublic static String decode(String content, Charset charset) {\n\t\treturn URLDecoder.decode(content, charset);\n\t}\n\n\t/**\n\t * 解码application/x-www-form-urlencoded字符<br>\n\t * 将%开头的16进制表示的内容解码。\n\t *\n\t * @param content       被解码内容\n\t * @param charset       编码，null表示不解码\n\t * @param isPlusToSpace 是否+转换为空格\n\t * @return 编码后的字符\n\t * @since 5.6.3\n\t */\n\tpublic static String decode(String content, Charset charset, boolean isPlusToSpace) {\n\t\treturn URLDecoder.decode(content, charset, isPlusToSpace);\n\t}\n\n\t/**\n\t * 解码application/x-www-form-urlencoded字符<br>\n\t * 将%开头的16进制表示的内容解码。\n\t *\n\t * @param content URL\n\t * @param charset 编码\n\t * @return 解码后的URL\n\t * @throws UtilException UnsupportedEncodingException\n\t */\n\tpublic static String decode(String content, String charset) throws UtilException {\n\t\treturn decode(content, StrUtil.isEmpty(charset) ? null : CharsetUtil.charset(charset));\n\t}\n\n\t/**\n\t * 获得path部分<br>\n\t *\n\t * @param uriStr URI路径\n\t * @return path\n\t * @throws UtilException 包装URISyntaxException\n\t */\n\tpublic static String getPath(String uriStr) {\n\t\treturn toURI(uriStr).getPath();\n\t}\n\n\t/**\n\t * 从URL对象中获取不被编码的路径Path<br>\n\t * 对于本地路径，URL对象的getPath方法对于包含中文或空格时会被编码，导致本读路径读取错误。<br>\n\t * 此方法将URL转为URI后获取路径用于解决路径被编码的问题\n\t *\n\t * @param url {@link URL}\n\t * @return 路径\n\t * @since 3.0.8\n\t */\n\tpublic static String getDecodedPath(URL url) {\n\t\tif (null == url) {\n\t\t\treturn null;\n\t\t}\n\n\t\tString path = null;\n\t\ttry {\n\t\t\t// URL对象的getPath方法对于包含中文或空格的问题\n\t\t\tpath = toURI(url).getPath();\n\t\t} catch (UtilException e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn (null != path) ? path : url.getPath();\n\t}\n\n\t/**\n\t * 转URL为URI\n\t *\n\t * @param url URL\n\t * @return URI\n\t * @throws UtilException 包装URISyntaxException\n\t */\n\tpublic static URI toURI(URL url) throws UtilException {\n\t\treturn toURI(url, false);\n\t}\n\n\t/**\n\t * 转URL为URI\n\t *\n\t * @param url      URL\n\t * @param isEncode 是否编码参数中的特殊字符（默认UTF-8编码）\n\t * @return URI\n\t * @throws UtilException 包装URISyntaxException\n\t * @since 4.6.9\n\t */\n\tpublic static URI toURI(URL url, boolean isEncode) throws UtilException {\n\t\tif (null == url) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn toURI(url.toString(), isEncode);\n\t}\n\n\t/**\n\t * 转字符串为URI\n\t *\n\t * @param location 字符串路径\n\t * @return URI\n\t * @throws UtilException 包装URISyntaxException\n\t */\n\tpublic static URI toURI(String location) throws UtilException {\n\t\treturn toURI(location, false);\n\t}\n\n\t/**\n\t * 转字符串为URI\n\t *\n\t * @param location 字符串路径\n\t * @param isEncode 是否编码参数中的特殊字符（默认UTF-8编码）\n\t * @return URI\n\t * @throws UtilException 包装URISyntaxException\n\t * @since 4.6.9\n\t */\n\tpublic static URI toURI(String location, boolean isEncode) throws UtilException {\n\t\tif (isEncode) {\n\t\t\tlocation = encode(location);\n\t\t}\n\t\ttry {\n\t\t\treturn new URI(StrUtil.trim(location));\n\t\t} catch (URISyntaxException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 提供的URL是否为文件<br>\n\t * 文件协议包括\"file\", \"vfsfile\" 或 \"vfs\".\n\t *\n\t * @param url {@link URL}\n\t * @return 是否为文件\n\t * @since 3.0.9\n\t */\n\tpublic static boolean isFileURL(URL url) {\n\t\tAssert.notNull(url, \"URL must be not null\");\n\t\tString protocol = url.getProtocol();\n\t\treturn (URL_PROTOCOL_FILE.equals(protocol) || //\n\t\t\t\tURL_PROTOCOL_VFSFILE.equals(protocol) || //\n\t\t\t\tURL_PROTOCOL_VFS.equals(protocol));\n\t}\n\n\t/**\n\t * 提供的URL是否为jar包URL 协议包括： \"jar\", \"zip\", \"vfszip\" 或 \"wsjar\".\n\t *\n\t * @param url {@link URL}\n\t * @return 是否为jar包URL\n\t */\n\tpublic static boolean isJarURL(URL url) {\n\t\tAssert.notNull(url, \"URL must be not null\");\n\t\tfinal String protocol = url.getProtocol();\n\t\treturn (URL_PROTOCOL_JAR.equals(protocol) || //\n\t\t\t\tURL_PROTOCOL_ZIP.equals(protocol) || //\n\t\t\t\tURL_PROTOCOL_VFSZIP.equals(protocol) || //\n\t\t\t\tURL_PROTOCOL_WSJAR.equals(protocol));\n\t}\n\n\t/**\n\t * 提供的URL是否为Jar文件URL 判断依据为file协议且扩展名为.jar\n\t *\n\t * @param url the URL to check\n\t * @return whether the URL has been identified as a JAR file URL\n\t * @since 4.1\n\t */\n\tpublic static boolean isJarFileURL(URL url) {\n\t\tAssert.notNull(url, \"URL must be not null\");\n\t\treturn (URL_PROTOCOL_FILE.equals(url.getProtocol()) && //\n\t\t\t\turl.getPath().toLowerCase().endsWith(FileUtil.JAR_FILE_EXT));\n\t}\n\n\t/**\n\t * 从URL中获取流\n\t *\n\t * @param url {@link URL}\n\t * @return InputStream流\n\t * @since 3.2.1\n\t */\n\tpublic static InputStream getStream(URL url) {\n\t\tAssert.notNull(url, \"URL must be not null\");\n\t\ttry {\n\t\t\treturn url.openStream();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得Reader\n\t *\n\t * @param url     {@link URL}\n\t * @param charset 编码\n\t * @return {@link BufferedReader}\n\t * @since 3.2.1\n\t */\n\tpublic static BufferedReader getReader(URL url, Charset charset) {\n\t\treturn IoUtil.getReader(getStream(url), charset);\n\t}\n\n\t/**\n\t * 从URL中获取JarFile\n\t *\n\t * @param url URL\n\t * @return JarFile\n\t * @since 4.1.5\n\t */\n\tpublic static JarFile getJarFile(URL url) {\n\t\ttry {\n\t\t\tJarURLConnection urlConnection = (JarURLConnection) url.openConnection();\n\t\t\treturn urlConnection.getJarFile();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 标准化URL字符串，包括：\n\t *\n\t * <ol>\n\t *     <li>自动补齐“http://”头</li>\n\t *     <li>去除开头的\\或者/</li>\n\t *     <li>替换\\为/</li>\n\t * </ol>\n\t *\n\t * @param url URL字符串\n\t * @return 标准化后的URL字符串\n\t */\n\tpublic static String normalize(String url) {\n\t\treturn normalize(url, false);\n\t}\n\n\t/**\n\t * 标准化URL字符串，包括：\n\t *\n\t * <ol>\n\t *     <li>自动补齐“http://”头</li>\n\t *     <li>去除开头的\\或者/</li>\n\t *     <li>替换\\为/</li>\n\t * </ol>\n\t *\n\t * @param url          URL字符串\n\t * @param isEncodePath 是否对URL中path部分的中文和特殊字符做转义（不包括 http:, /和域名部分）\n\t * @return 标准化后的URL字符串\n\t * @since 4.4.1\n\t */\n\tpublic static String normalize(String url, boolean isEncodePath) {\n\t\treturn normalize(url, isEncodePath, false);\n\t}\n\n\t/**\n\t * 标准化URL字符串，包括：\n\t *\n\t * <ol>\n\t *     <li>自动补齐“http://”头</li>\n\t *     <li>去除开头的\\或者/</li>\n\t *     <li>替换\\为/</li>\n\t *     <li>如果replaceSlash为true，则替换多个/为一个</li>\n\t * </ol>\n\t *\n\t * @param url          URL字符串\n\t * @param isEncodePath 是否对URL中path部分的中文和特殊字符做转义（不包括 http:, /和域名部分）\n\t * @param replaceSlash 是否替换url body中的 //\n\t * @return 标准化后的URL字符串\n\t * @since 5.5.5\n\t */\n\tpublic static String normalize(String url, boolean isEncodePath, boolean replaceSlash) {\n\t\tif (StrUtil.isBlank(url)) {\n\t\t\treturn url;\n\t\t}\n\t\tfinal int sepIndex = url.indexOf(\"://\");\n\t\tString protocol;\n\t\tString body;\n\t\tif (sepIndex > 0) {\n\t\t\tprotocol = StrUtil.subPre(url, sepIndex + 3);\n\t\t\tbody = StrUtil.subSuf(url, sepIndex + 3);\n\t\t} else {\n\t\t\tprotocol = \"http://\";\n\t\t\tbody = url;\n\t\t}\n\n\t\tfinal int paramsSepIndex = StrUtil.indexOf(body, '?');\n\t\tString params = null;\n\t\tif (paramsSepIndex > 0) {\n\t\t\tparams = StrUtil.subSuf(body, paramsSepIndex);\n\t\t\tbody = StrUtil.subPre(body, paramsSepIndex);\n\t\t}\n\n\t\tif (StrUtil.isNotEmpty(body)) {\n\t\t\t// 去除开头的\\或者/\n\t\t\t//noinspection ConstantConditions\n\t\t\tbody = body.replaceAll(\"^[\\\\\\\\/]+\", StrUtil.EMPTY);\n\t\t\t// 替换\\为/\n\t\t\tbody = body.replace(\"\\\\\", \"/\");\n\t\t\tif (replaceSlash) {\n\t\t\t\t//issue#I25MZL@Gitee，双斜杠在URL中是允许存在的，默认不做替换\n\t\t\t\tbody = body.replaceAll(\"//+\", \"/\");\n\t\t\t}\n\t\t}\n\n\t\tfinal int pathSepIndex = StrUtil.indexOf(body, '/');\n\t\tString domain = body;\n\t\tString path = null;\n\t\tif (pathSepIndex > 0) {\n\t\t\tdomain = StrUtil.subPre(body, pathSepIndex);\n\t\t\tpath = StrUtil.subSuf(body, pathSepIndex);\n\t\t}\n\t\tif (isEncodePath) {\n\t\t\tpath = encode(path);\n\t\t}\n\t\treturn protocol + domain + StrUtil.nullToEmpty(path) + StrUtil.nullToEmpty(params);\n\t}\n\n\t/**\n\t * 将Map形式的Form表单数据转换为Url参数形式<br>\n\t * paramMap中如果key为空（null和\"\"）会被忽略，如果value为null，会被做为空白符（\"\"）<br>\n\t * 会自动url编码键和值\n\t *\n\t * <pre>\n\t * key1=v1&amp;key2=&amp;key3=v3\n\t * </pre>\n\t *\n\t * @param paramMap 表单数据\n\t * @param charset  编码，编码为null表示不编码\n\t * @return url参数\n\t */\n\tpublic static String buildQuery(Map<String, ?> paramMap, Charset charset) {\n\t\treturn UrlQuery.of(paramMap).build(charset);\n\t}\n\n\t/**\n\t * 获取指定URL对应资源的内容长度，对于Http，其长度使用Content-Length头决定。\n\t *\n\t * @param url URL\n\t * @return 内容长度，未知返回-1\n\t * @throws IORuntimeException IO异常\n\t * @since 5.3.4\n\t */\n\tpublic static long getContentLength(URL url) throws IORuntimeException {\n\t\tif (null == url) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tURLConnection conn = null;\n\t\ttry {\n\t\t\tconn = url.openConnection();\n\t\t\treturn conn.getContentLengthLong();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (conn instanceof HttpURLConnection) {\n\t\t\t\t((HttpURLConnection) conn).disconnect();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Data URI Scheme封装，数据格式为Base64。data URI scheme 允许我们使用内联（inline-code）的方式在网页中包含数据，<br>\n\t * 目的是将一些小的数据，直接嵌入到网页中，从而不用再从外部文件载入。常用于将图片嵌入网页。\n\t *\n\t * <p>\n\t * Data URI的格式规范：\n\t * <pre>\n\t *     data:[&lt;mime type&gt;][;charset=&lt;charset&gt;][;&lt;encoding&gt;],&lt;encoded data&gt;\n\t * </pre>\n\t *\n\t * @param mimeType 可选项（null表示无），数据类型（image/png、text/plain等）\n\t * @param data     编码后的数据\n\t * @return Data URI字符串\n\t * @since 5.3.11\n\t */\n\tpublic static String getDataUriBase64(String mimeType, String data) {\n\t\treturn getDataUri(mimeType, null, \"base64\", data);\n\t}\n\n\t/**\n\t * Data URI Scheme封装。data URI scheme 允许我们使用内联（inline-code）的方式在网页中包含数据，<br>\n\t * 目的是将一些小的数据，直接嵌入到网页中，从而不用再从外部文件载入。常用于将图片嵌入网页。\n\t *\n\t * <p>\n\t * Data URI的格式规范：\n\t * <pre>\n\t *     data:[&lt;mime type&gt;][;charset=&lt;charset&gt;][;&lt;encoding&gt;],&lt;encoded data&gt;\n\t * </pre>\n\t *\n\t * @param mimeType 可选项（null表示无），数据类型（image/png、text/plain等）\n\t * @param encoding 数据编码方式（US-ASCII，BASE64等）\n\t * @param data     编码后的数据\n\t * @return Data URI字符串\n\t * @since 5.3.6\n\t */\n\tpublic static String getDataUri(String mimeType, String encoding, String data) {\n\t\treturn getDataUri(mimeType, null, encoding, data);\n\t}\n\n\t/**\n\t * Data URI Scheme封装。data URI scheme 允许我们使用内联（inline-code）的方式在网页中包含数据，<br>\n\t * 目的是将一些小的数据，直接嵌入到网页中，从而不用再从外部文件载入。常用于将图片嵌入网页。\n\t *\n\t * <p>\n\t * Data URI的格式规范：\n\t * <pre>\n\t *     data:[&lt;mime type&gt;][;charset=&lt;charset&gt;][;&lt;encoding&gt;],&lt;encoded data&gt;\n\t * </pre>\n\t *\n\t * @param mimeType 可选项（null表示无），数据类型（image/png、text/plain等）\n\t * @param charset  可选项（null表示无），源文本的字符集编码方式\n\t * @param encoding 数据编码方式（US-ASCII，BASE64等）\n\t * @param data     编码后的数据\n\t * @return Data URI字符串\n\t * @since 5.3.6\n\t */\n\tpublic static String getDataUri(String mimeType, Charset charset, String encoding, String data) {\n\t\tfinal StringBuilder builder = StrUtil.builder(\"data:\");\n\t\tif (StrUtil.isNotBlank(mimeType)) {\n\t\t\tbuilder.append(mimeType);\n\t\t}\n\t\tif (null != charset) {\n\t\t\tbuilder.append(\";charset=\").append(charset.name());\n\t\t}\n\t\tif (StrUtil.isNotBlank(encoding)) {\n\t\t\tbuilder.append(';').append(encoding);\n\t\t}\n\t\tbuilder.append(',').append(data);\n\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 获取URL对应数据长度\n\t * <ul>\n\t *     <li>如果URL为文件，转换为文件获取文件长度。</li>\n\t *     <li>其它情况获取{@link URLConnection#getContentLengthLong()}</li>\n\t * </ul>\n\t *\n\t * @param url URL\n\t * @return 长度\n\t */\n\tpublic static long size(final URL url) {\n\t\tif (URLUtil.isFileURL(url)) {\n\t\t\t// 如果资源以独立文件形式存在，尝试获取文件长度\n\t\t\tfinal File file = FileUtil.file(url);\n\t\t\tfinal long length = file.length();\n\t\t\tif (length == 0L && !file.exists()) {\n\t\t\t\tthrow new IORuntimeException(\"File not exist or size is zero!\");\n\t\t\t}\n\t\t\treturn length;\n\t\t} else {\n\t\t\t// 如果资源打在jar包中或来自网络，使用网络请求长度\n\t\t\t// issue#3226, 来自Spring的AbstractFileResolvingResource\n\t\t\tURLConnection con = null;\n\t\t\ttry {\n\t\t\t\tcon = url.openConnection();\n\t\t\t\tuseCachesIfNecessary(con);\n\t\t\t\tif (con instanceof HttpURLConnection) {\n\t\t\t\t\tfinal HttpURLConnection httpCon = (HttpURLConnection) con;\n\t\t\t\t\thttpCon.setRequestMethod(\"HEAD\");\n\t\t\t\t}\n\t\t\t\treturn con.getContentLengthLong();\n\t\t\t} catch (final IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t} finally {\n\t\t\t\tif (con instanceof HttpURLConnection) {\n\t\t\t\t\t((HttpURLConnection) con).disconnect();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 如果连接为JNLP方式，则打开缓存\n\t *\n\t * @param con {@link URLConnection}\n\t */\n\tpublic static void useCachesIfNecessary(final URLConnection con) {\n\t\tcon.setUseCaches(con.getClass().getSimpleName().startsWith(\"JNLP\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/VersionUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.exceptions.UtilException;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 版本对比工具\n * 对 {@link cn.hutool.core.comparator.VersionComparator} 的封装\n * 最主要功能包括：\n *\n *\n * <pre>\n * 1. 版本表达式匹配\n * 2. 单个版本匹配\n * </pre>\n *\n * @author winlans\n * @see cn.hutool.core.comparator.VersionComparator\n */\npublic class VersionUtil {\n\n\tprivate static final Pattern COMPARE_REG = Pattern.compile(\"^[<>≥≤]=?\");\n\t// 默认多版本分隔符\n\tprivate static final String defaultVersionsDelimiter = \";\";\n\n\t/**\n\t * 是否匹配任意一个版本\n\t *\n\t * @param currentVersion  当前版本\n\t * @param compareVersions 待匹配的版本列表\n\t * @return true 包含待匹配的版本\n\t */\n\tpublic static boolean anyMatch(String currentVersion, Collection<String> compareVersions) {\n\t\treturn matchEl(currentVersion, CollUtil.join(compareVersions, defaultVersionsDelimiter));\n\t}\n\n\t/**\n\t * 是否匹配任意一个版本\n\t *\n\t * @param currentVersion  当前版本\n\t * @param compareVersions 待匹配的版本列表\n\t * @return true 包含待匹配的版本\n\t */\n\tpublic static boolean anyMatch(String currentVersion, String... compareVersions) {\n\t\treturn matchEl(currentVersion, ArrayUtil.join(compareVersions, defaultVersionsDelimiter));\n\t}\n\n\t/**\n\t * 当前版本大于待比较版本\n\t *\n\t * @param currentVersion 当前版本\n\t * @param compareVersion 待比较版本\n\t * @return true  当前版本大于待比较版本\n\t */\n\tpublic static boolean isGreaterThan(String currentVersion, String compareVersion) {\n\t\treturn matchEl(currentVersion, \">\" + compareVersion);\n\t}\n\n\t/**\n\t * 当前版本大于等于待比较版本\n\t *\n\t * @param currentVersion 当前版本\n\t * @param compareVersion 待比较版本\n\t * @return true  当前版本大于等于待比较版本\n\t */\n\tpublic static boolean isGreaterThanOrEqual(String currentVersion, String compareVersion) {\n\t\treturn matchEl(currentVersion, \">=\" + compareVersion);\n\t}\n\n\t/**\n\t * 当前版本小于待比较版本\n\t *\n\t * @param currentVersion 当前版本\n\t * @param compareVersion 待比较版本\n\t * @return true  当前版本小于待比较版本\n\t */\n\tpublic static boolean isLessThan(String currentVersion, String compareVersion) {\n\t\treturn matchEl(currentVersion, \"<\" + compareVersion);\n\t}\n\n\t/**\n\t * 当前版本小于等于待比较版本\n\t *\n\t * @param currentVersion 当前版本\n\t * @param compareVersion 待比较版本\n\t * @return true  当前版本小于等于待比较版本\n\t */\n\tpublic static boolean isLessThanOrEqual(String currentVersion, String compareVersion) {\n\t\treturn matchEl(currentVersion, \"<=\" + compareVersion);\n\t}\n\n\t/**\n\t * 当前版本是否满足版本表达式\n\t * <pre>{@code\n\t *     matchEl(\"1.0.2\", \">=1.0.2\") == true\n\t *     matchEl(\"1.0.2\", \"<1.0.1;1.0.2\") == true\n\t *     matchEl(\"1.0.2\", \"<1.0.2\") == false\n\t *     matchEl(\"1.0.2\", \"1.0.0-1.1.1\") == true\n\t *     matchEl(\"1.0.2\", \"1.0.0-1.1.1\") == true\n\t * }</pre>\n\t *\n\t * @param currentVersion 当前版本\n\t * @param versionEl      版本表达式\n\t * @return true  当前版本是否满足版本表达式\n\t */\n\tpublic static boolean matchEl(String currentVersion, String versionEl) {\n\t\treturn matchEl(currentVersion, versionEl, defaultVersionsDelimiter);\n\t}\n\n\t/**\n\t * 当前版本是否满足版本表达式\n\t * <pre>{@code\n\t *     matchEl(\"1.0.2\", \">=1.0.2\", \";\") == true\n\t *     matchEl(\"1.0.2\", \"<1.0.1,1.0.2\", \",\") == true\n\t *     matchEl(\"1.0.2\", \"<1.0.2\", \";\") == false\n\t *     matchEl(\"1.0.2\", \"1.0.0-1.1.1\", \",\") == true\n\t *     matchEl(\"1.0.2\", \"1.0.1,1.0.2-1.1.1\", \",\") == true\n\t * }</pre>\n\t *\n\t * @param currentVersion    当前版本\n\t * @param versionEl         版本表达式（可以匹配多个条件，使用指定的分隔符（默认;）分隔）,\n\t *                          {@code '-'}表示范围包含左右版本,如果 {@code '-'}的左边没有，表示小于等于某个版本号， 右边表示大于等于某个版本号。\n\t *                          支持比较符号{@code '>'},{@code '<'}, {@code '>='},{@code '<='}，{@code '≤'}，{@code '≥'}\n\t *\n\t *                          <ul>\n\t *                          <li>{@code 1.0.1-1.2.4, 1.9.8} 表示版本号 大于等于{@code 1.0.1}且小于等于{@code 1.2.4} 或 版本{@code 1.9.8}</li>\n\t *                          <li>{@code >=2.0.0, 1.9.8} 表示版本号 大于等于{@code 2.0.0}或 版本{@code 1.9.8}</li>\n\t *                          </ul>\n\t * @param versionsDelimiter 多表达式分隔符\n\t * @return true  当前版本是否满足版本表达式\n\t */\n\tpublic static boolean matchEl(String currentVersion, String versionEl, String versionsDelimiter) {\n\t\tif (StrUtil.isBlank(versionsDelimiter)\n\t\t\t|| StrUtil.equals(\"-\", versionsDelimiter)\n\t\t\t|| ReUtil.isMatch(COMPARE_REG, versionsDelimiter)) {\n\t\t\tthrow new UtilException(\"非法的版本分隔符：\" + versionsDelimiter);\n\t\t}\n\n\t\tif (StrUtil.isBlank(versionEl) || StrUtil.isBlank(currentVersion)) {\n\t\t\treturn false;\n\t\t}\n\t\tString trimmedVersion = StrUtil.trim(currentVersion);\n\n\t\tList<String> els = StrUtil.split(versionEl, versionsDelimiter, true, true);\n\t\tif (CollUtil.isEmpty(els)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (String el : els) {\n\t\t\tel = el.trim();\n\t\t\tMatcher matcher = COMPARE_REG.matcher(el);\n\t\t\tif (matcher.find()) {\n\t\t\t\tString op = matcher.group();\n\t\t\t\tString ver = StrUtil.removePrefix(el, op);\n\t\t\t\tswitch (op) {\n\t\t\t\t\tcase \">=\":\n\t\t\t\t\tcase \"≥\":\n\t\t\t\t\t\tif (StrUtil.compareVersion(trimmedVersion, ver) >= 0) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"<=\":\n\t\t\t\t\tcase \"≤\":\n\t\t\t\t\t\tif (StrUtil.compareVersion(trimmedVersion, ver) <= 0) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"<\":\n\t\t\t\t\t\tif (StrUtil.compareVersion(trimmedVersion, ver) < 0) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \">\":\n\t\t\t\t\t\tif (StrUtil.compareVersion(trimmedVersion, ver) > 0) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else if (StrUtil.contains(el, \"-\")) {\n\t\t\t\tint index = el.indexOf('-');\n\t\t\t\tString left = StrUtil.blankToDefault(StrUtil.trim(el.substring(0, index)), \"\");\n\t\t\t\tString right = StrUtil.blankToDefault(StrUtil.trim(el.substring(index + 1)), \"\");\n\n\t\t\t\tboolean leftMatch = StrUtil.isBlank(left) || StrUtil.compareVersion(left, trimmedVersion) <= 0;\n\t\t\t\tboolean rightMatch = StrUtil.isBlank(right) || StrUtil.compareVersion(right, trimmedVersion) >= 0;\n\t\t\t\tif (leftMatch && rightMatch) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else if (Objects.equals(trimmedVersion, el)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.BiMap;\nimport cn.hutool.core.map.MapUtil;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.ContentHandler;\nimport org.xml.sax.InputSource;\nimport org.xml.sax.SAXException;\nimport org.xml.sax.XMLReader;\nimport org.xml.sax.helpers.DefaultHandler;\n\nimport javax.xml.XMLConstants;\nimport javax.xml.namespace.NamespaceContext;\nimport javax.xml.namespace.QName;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\nimport javax.xml.parsers.SAXParser;\nimport javax.xml.parsers.SAXParserFactory;\nimport javax.xml.transform.OutputKeys;\nimport javax.xml.transform.Result;\nimport javax.xml.transform.Source;\nimport javax.xml.transform.Transformer;\nimport javax.xml.transform.TransformerFactory;\nimport javax.xml.transform.dom.DOMSource;\nimport javax.xml.transform.stream.StreamResult;\nimport javax.xml.xpath.XPath;\nimport javax.xml.xpath.XPathConstants;\nimport javax.xml.xpath.XPathExpressionException;\nimport javax.xml.xpath.XPathFactory;\nimport java.beans.XMLEncoder;\nimport java.io.BufferedInputStream;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Reader;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * XML工具类<br>\n * 此工具使用w3c dom工具，不需要依赖第三方包。<br>\n * 工具类封装了XML文档的创建、读取、写出和部分XML操作\n *\n * @author xiaoleilu\n * @see JAXBUtil\n */\npublic class XmlUtil {\n\n\t/**\n\t * 字符串常量：XML 不间断空格转义 {@code \"&nbsp;\" -> \" \"}\n\t */\n\tpublic static final String NBSP = \"&nbsp;\";\n\n\t/**\n\t * 字符串常量：XML And 符转义 {@code \"&amp;\" -> \"&\"}\n\t */\n\tpublic static final String AMP = \"&amp;\";\n\n\t/**\n\t * 字符串常量：XML 双引号转义 {@code \"&quot;\" -> \"\\\"\"}\n\t */\n\tpublic static final String QUOTE = \"&quot;\";\n\n\t/**\n\t * 字符串常量：XML 单引号转义 {@code \"&apos\" -> \"'\"}\n\t */\n\tpublic static final String APOS = \"&apos;\";\n\n\t/**\n\t * 字符串常量：XML 小于号转义 {@code \"&lt;\" -> \"<\"}\n\t */\n\tpublic static final String LT = \"&lt;\";\n\n\t/**\n\t * 字符串常量：XML 大于号转义 {@code \"&gt;\" -> \">\"}\n\t */\n\tpublic static final String GT = \"&gt;\";\n\n\t/**\n\t * 在XML中无效的字符 正则\n\t */\n\tpublic static final String INVALID_REGEX = \"[\\\\x00-\\\\x08\\\\x0b-\\\\x0c\\\\x0e-\\\\x1f]\";\n\t/**\n\t * 在XML中注释的内容 正则\n\t */\n\tpublic static final String COMMENT_REGEX = \"(?s)<!--.+?-->\";\n\t/**\n\t * XML格式化输出默认缩进量\n\t */\n\tpublic static final int INDENT_DEFAULT = 2;\n\n\t/**\n\t * 默认的DocumentBuilderFactory实现\n\t */\n\tprivate static String defaultDocumentBuilderFactory = \"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl\";\n\n\t/**\n\t * 是否打开命名空间支持\n\t */\n\tprivate static boolean namespaceAware = true;\n\t/**\n\t * Sax读取器工厂缓存\n\t */\n\tprivate static SAXParserFactory factory;\n\n\t/**\n\t * 禁用默认的DocumentBuilderFactory，禁用后如果有第三方的实现（如oracle的xdb包中的xmlparse），将会自动加载实现。\n\t */\n\tsynchronized public static void disableDefaultDocumentBuilderFactory() {\n\t\tdefaultDocumentBuilderFactory = null;\n\t}\n\n\t/**\n\t * 设置是否打开命名空间支持，默认打开\n\t *\n\t * @param isNamespaceAware 是否命名空间支持\n\t * @since 5.3.1\n\t */\n\tsynchronized public static void setNamespaceAware(boolean isNamespaceAware) {\n\t\tnamespaceAware = isNamespaceAware;\n\t}\n\n\t// -------------------------------------------------------------------------------------- Read\n\n\t/**\n\t * 读取解析XML文件\n\t *\n\t * @param file XML文件\n\t * @return XML文档对象\n\t */\n\tpublic static Document readXML(File file) {\n\t\tAssert.notNull(file, \"Xml file is null !\");\n\t\tif (false == file.exists()) {\n\t\t\tthrow new UtilException(\"File [{}] not a exist!\", file.getAbsolutePath());\n\t\t}\n\t\tif (false == file.isFile()) {\n\t\t\tthrow new UtilException(\"[{}] not a file!\", file.getAbsolutePath());\n\t\t}\n\n\t\ttry {\n\t\t\tfile = file.getCanonicalFile();\n\t\t} catch (IOException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treturn readXML(in);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 读取解析XML文件<br>\n\t * 如果给定内容以“&lt;”开头，表示这是一个XML内容，直接读取，否则按照路径处理<br>\n\t * 路径可以为相对路径，也可以是绝对路径，相对路径相对于ClassPath\n\t *\n\t * @param pathOrContent 内容或路径\n\t * @return XML文档对象\n\t * @since 3.0.9\n\t */\n\tpublic static Document readXML(String pathOrContent) {\n\t\tif (StrUtil.startWith(pathOrContent, '<')) {\n\t\t\treturn parseXml(pathOrContent);\n\t\t}\n\t\treturn readXML(FileUtil.file(pathOrContent));\n\t}\n\n\t/**\n\t * 读取解析XML文件<br>\n\t * 编码在XML中定义\n\t *\n\t * @param inputStream XML流\n\t * @return XML文档对象\n\t * @throws UtilException IO异常或转换异常\n\t * @since 3.0.9\n\t */\n\tpublic static Document readXML(InputStream inputStream) throws UtilException {\n\t\treturn readXML(new InputSource(inputStream));\n\t}\n\n\t/**\n\t * 读取解析XML文件\n\t *\n\t * @param reader XML流\n\t * @return XML文档对象\n\t * @throws UtilException IO异常或转换异常\n\t * @since 3.0.9\n\t */\n\tpublic static Document readXML(Reader reader) throws UtilException {\n\t\treturn readXML(new InputSource(reader));\n\t}\n\n\t/**\n\t * 读取解析XML文件<br>\n\t * 编码在XML中定义\n\t *\n\t * @param source {@link InputSource}\n\t * @return XML文档对象\n\t * @since 3.0.9\n\t */\n\tpublic static Document readXML(InputSource source) {\n\t\tfinal DocumentBuilder builder = createDocumentBuilder();\n\t\ttry {\n\t\t\treturn builder.parse(source);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Parse XML from stream error!\");\n\t\t}\n\t}\n\n\t/**\n\t * 使用Sax方式读取指定的XML<br>\n\t * 如果用户传入的contentHandler为{@link DefaultHandler}，则其接口都会被处理\n\t *\n\t * @param file           XML源文件,使用后自动关闭\n\t * @param contentHandler XML流处理器，用于按照Element处理xml\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(File file, ContentHandler contentHandler) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treadBySax(new InputSource(in), contentHandler);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 使用Sax方式读取指定的XML<br>\n\t * 如果用户传入的contentHandler为{@link DefaultHandler}，则其接口都会被处理\n\t *\n\t * @param reader         XML源Reader,使用后自动关闭\n\t * @param contentHandler XML流处理器，用于按照Element处理xml\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(Reader reader, ContentHandler contentHandler) {\n\t\ttry {\n\t\t\treadBySax(new InputSource(reader), contentHandler);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t/**\n\t * 使用Sax方式读取指定的XML<br>\n\t * 如果用户传入的contentHandler为{@link DefaultHandler}，则其接口都会被处理\n\t *\n\t * @param source         XML源流,使用后自动关闭\n\t * @param contentHandler XML流处理器，用于按照Element处理xml\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(InputStream source, ContentHandler contentHandler) {\n\t\ttry {\n\t\t\treadBySax(new InputSource(source), contentHandler);\n\t\t} finally {\n\t\t\tIoUtil.close(source);\n\t\t}\n\t}\n\n\t/**\n\t * 使用Sax方式读取指定的XML<br>\n\t * 如果用户传入的contentHandler为{@link DefaultHandler}，则其接口都会被处理\n\t *\n\t * @param source         XML源，可以是文件、流、路径等\n\t * @param contentHandler XML流处理器，用于按照Element处理xml\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(InputSource source, ContentHandler contentHandler) {\n\t\t// 1.获取解析工厂\n\t\tif (null == factory) {\n\t\t\tfactory = SAXParserFactory.newInstance();\n\t\t\tfactory.setValidating(false);\n\t\t\tfactory.setNamespaceAware(namespaceAware);\n\n\t\t\t// https://blog.spoock.com/2018/10/23/java-xxe/\n\t\t\ttry {\n\t\t\t\tfactory.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n\t\t\t\tfactory.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n\t\t\t\tfactory.setFeature(\"http://xml.org/sax/features/external-parameter-entities\", false);\n\t\t\t\tfactory.setFeature(\"http://apache.org/xml/features/nonvalidating/load-external-dtd\", false);\n\t\t\t} catch (final Exception ignore) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\t// 2.从解析工厂获取解析器\n\t\tfinal SAXParser parse;\n\t\tXMLReader reader;\n\t\ttry {\n\t\t\tparse = factory.newSAXParser();\n\t\t\tif (contentHandler instanceof DefaultHandler) {\n\t\t\t\tparse.parse(source, (DefaultHandler) contentHandler);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 3.得到解读器\n\t\t\treader = parse.getXMLReader();\n\t\t\t// 防止XEE攻击，见：https://www.jianshu.com/p/1a857905b22c\n\t\t\t// https://blog.spoock.com/2018/10/23/java-xxe/\n\t\t\treader.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n\t\t\t//  忽略外部DTD\n\t\t\treader.setFeature(\"http://apache.org/xml/features/nonvalidating/load-external-dtd\", false);\n\t\t\t// 不包括外部一般实体。\n\t\t\treader.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n\t\t\t// 不包含外部参数实体或外部DTD子集。\n\t\t\treader.setFeature(\"http://xml.org/sax/features/external-parameter-entities\", false);\n\n\t\t\treader.setContentHandler(contentHandler);\n\t\t\treader.parse(source);\n\t\t} catch (ParserConfigurationException | SAXException e) {\n\t\t\tthrow new UtilException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将String类型的XML转换为XML文档\n\t *\n\t * @param xmlStr XML字符串\n\t * @return XML文档\n\t */\n\tpublic static Document parseXml(String xmlStr) {\n\t\tif (StrUtil.isBlank(xmlStr)) {\n\t\t\tthrow new IllegalArgumentException(\"XML content string is empty !\");\n\t\t}\n\t\txmlStr = cleanInvalid(xmlStr);\n\t\treturn readXML(StrUtil.getReader(xmlStr));\n\t}\n\n\t// -------------------------------------------------------------------------------------- Write\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8<br>\n\t * 默认非格式化输出，若想格式化请使用{@link #format(Document)}\n\t *\n\t * @param doc XML文档\n\t * @return XML字符串\n\t * @since 5.4.5\n\t */\n\tpublic static String toStr(Node doc) {\n\t\treturn toStr(doc, false);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8<br>\n\t * 默认非格式化输出，若想格式化请使用{@link #format(Document)}\n\t *\n\t * @param doc XML文档\n\t * @return XML字符串\n\t */\n\tpublic static String toStr(Document doc) {\n\t\treturn toStr((Node) doc);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8\n\t *\n\t * @param doc      XML文档\n\t * @param isPretty 是否格式化输出\n\t * @return XML字符串\n\t * @since 5.4.5\n\t */\n\tpublic static String toStr(Node doc, boolean isPretty) {\n\t\treturn toStr(doc, CharsetUtil.UTF_8, isPretty);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8\n\t *\n\t * @param doc      XML文档\n\t * @param isPretty 是否格式化输出\n\t * @return XML字符串\n\t * @since 3.0.9\n\t */\n\tpublic static String toStr(Document doc, boolean isPretty) {\n\t\treturn toStr((Node) doc, isPretty);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8\n\t *\n\t * @param doc      XML文档\n\t * @param charset  编码\n\t * @param isPretty 是否格式化输出\n\t * @return XML字符串\n\t * @since 5.4.5\n\t */\n\tpublic static String toStr(Node doc, String charset, boolean isPretty) {\n\t\treturn toStr(doc, charset, isPretty, false);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8\n\t *\n\t * @param doc      XML文档\n\t * @param charset  编码\n\t * @param isPretty 是否格式化输出\n\t * @return XML字符串\n\t * @since 3.0.9\n\t */\n\tpublic static String toStr(Document doc, String charset, boolean isPretty) {\n\t\treturn toStr((Node) doc, charset, isPretty);\n\t}\n\n\t/**\n\t * 将XML文档转换为String<br>\n\t * 字符编码使用XML文档中的编码，获取不到则使用UTF-8\n\t *\n\t * @param doc                XML文档\n\t * @param charset            编码\n\t * @param isPretty           是否格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @return XML字符串\n\t * @since 5.1.2\n\t */\n\tpublic static String toStr(Node doc, String charset, boolean isPretty, boolean omitXmlDeclaration) {\n\t\tfinal StringWriter writer = StrUtil.getWriter();\n\t\ttry {\n\t\t\twrite(doc, writer, charset, isPretty ? INDENT_DEFAULT : 0, omitXmlDeclaration);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Trans xml document to string error!\");\n\t\t}\n\t\treturn writer.toString();\n\t}\n\n\t/**\n\t * 格式化XML输出\n\t *\n\t * @param doc {@link Document} XML文档\n\t * @return 格式化后的XML字符串\n\t * @since 4.4.5\n\t */\n\tpublic static String format(Document doc) {\n\t\treturn toStr(doc, true);\n\t}\n\n\t/**\n\t * 格式化XML输出\n\t *\n\t * @param xmlStr XML字符串\n\t * @return 格式化后的XML字符串\n\t * @since 4.4.5\n\t */\n\tpublic static String format(String xmlStr) {\n\t\treturn format(parseXml(xmlStr));\n\t}\n\n\t/**\n\t * 将XML文档写入到文件<br>\n\t * 使用Document中的编码\n\t *\n\t * @param doc          XML文档\n\t * @param absolutePath 文件绝对路径，不存在会自动创建\n\t */\n\tpublic static void toFile(Document doc, String absolutePath) {\n\t\ttoFile(doc, absolutePath, null);\n\t}\n\n\t/**\n\t * 将XML文档写入到文件<br>\n\t *\n\t * @param doc         XML文档\n\t * @param path        文件路径绝对路径或相对ClassPath路径，不存在会自动创建\n\t * @param charsetName 自定义XML文件的编码，如果为{@code null} 读取XML文档中的编码，否则默认UTF-8\n\t */\n\tpublic static void toFile(Document doc, String path, String charsetName) {\n\t\tif (StrUtil.isBlank(charsetName)) {\n\t\t\tcharsetName = doc.getXmlEncoding();\n\t\t}\n\t\tif (StrUtil.isBlank(charsetName)) {\n\t\t\tcharsetName = CharsetUtil.UTF_8;\n\t\t}\n\n\t\tBufferedWriter writer = null;\n\t\ttry {\n\t\t\twriter = FileUtil.getWriter(path, CharsetUtil.charset(charsetName), false);\n\t\t\twrite(doc, writer, charsetName, INDENT_DEFAULT);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 将XML文档写出\n\t *\n\t * @param node    {@link Node} XML文档节点或文档本身\n\t * @param writer  写出的Writer，Writer决定了输出XML的编码\n\t * @param charset 编码\n\t * @param indent  格式化输出中缩进量，小于1表示不格式化输出\n\t * @since 3.0.9\n\t */\n\tpublic static void write(Node node, Writer writer, String charset, int indent) {\n\t\ttransform(new DOMSource(node), new StreamResult(writer), charset, indent);\n\t}\n\n\t/**\n\t * 将XML文档写出\n\t *\n\t * @param node               {@link Node} XML文档节点或文档本身\n\t * @param writer             写出的Writer，Writer决定了输出XML的编码\n\t * @param charset            编码\n\t * @param indent             格式化输出中缩进量，小于1表示不格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @since 5.1.2\n\t */\n\tpublic static void write(Node node, Writer writer, String charset, int indent, boolean omitXmlDeclaration) {\n\t\ttransform(new DOMSource(node), new StreamResult(writer), charset, indent, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将XML文档写出\n\t *\n\t * @param node    {@link Node} XML文档节点或文档本身\n\t * @param out     写出的Writer，Writer决定了输出XML的编码\n\t * @param charset 编码\n\t * @param indent  格式化输出中缩进量，小于1表示不格式化输出\n\t * @since 4.0.8\n\t */\n\tpublic static void write(Node node, OutputStream out, String charset, int indent) {\n\t\ttransform(new DOMSource(node), new StreamResult(out), charset, indent);\n\t}\n\n\t/**\n\t * 将XML文档写出\n\t *\n\t * @param node               {@link Node} XML文档节点或文档本身\n\t * @param out                写出的Writer，Writer决定了输出XML的编码\n\t * @param charset            编码\n\t * @param indent             格式化输出中缩进量，小于1表示不格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @since 5.1.2\n\t */\n\tpublic static void write(Node node, OutputStream out, String charset, int indent, boolean omitXmlDeclaration) {\n\t\ttransform(new DOMSource(node), new StreamResult(out), charset, indent, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将XML文档写出<br>\n\t * 格式化输出逻辑参考：https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java\n\t *\n\t * @param source  源\n\t * @param result  目标\n\t * @param charset 编码\n\t * @param indent  格式化输出中缩进量，小于1表示不格式化输出\n\t * @since 4.0.9\n\t */\n\tpublic static void transform(Source source, Result result, String charset, int indent) {\n\t\ttransform(source, result, charset, indent, false);\n\t}\n\n\t/**\n\t * 将XML文档写出<br>\n\t * 格式化输出逻辑参考：https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java\n\t *\n\t * @param source             源\n\t * @param result             目标\n\t * @param charset            编码\n\t * @param indent             格式化输出中缩进量，小于1表示不格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @since 5.1.2\n\t */\n\tpublic static void transform(Source source, Result result, String charset, int indent, boolean omitXmlDeclaration) {\n\t\tfinal TransformerFactory factory = TransformerFactory.newInstance();\n\t\ttry {\n\t\t\tfinal Transformer xformer = factory.newTransformer();\n\t\t\tif (indent > 0) {\n\t\t\t\txformer.setOutputProperty(OutputKeys.INDENT, \"yes\");\n\t\t\t\t//fix issue#1232@Github\n\t\t\t\txformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, \"yes\");\n\t\t\t\txformer.setOutputProperty(\"{http://xml.apache.org/xslt}indent-amount\", String.valueOf(indent));\n\t\t\t}\n\t\t\tif (StrUtil.isNotBlank(charset)) {\n\t\t\t\txformer.setOutputProperty(OutputKeys.ENCODING, charset);\n\t\t\t}\n\t\t\tif (omitXmlDeclaration) {\n\t\t\t\txformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, \"yes\");\n\t\t\t}\n\t\t\txformer.transform(source, result);\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Trans xml document to string error!\");\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------- Create\n\n\t/**\n\t * 创建XML文档<br>\n\t * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码\n\t *\n\t * @return XML文档\n\t * @since 4.0.8\n\t */\n\tpublic static Document createXml() {\n\t\treturn createDocumentBuilder().newDocument();\n\t}\n\n\t/**\n\t * 创建 DocumentBuilder\n\t *\n\t * @return DocumentBuilder\n\t * @since 4.1.2\n\t */\n\tpublic static DocumentBuilder createDocumentBuilder() {\n\t\tDocumentBuilder builder;\n\t\ttry {\n\t\t\tbuilder = createDocumentBuilderFactory().newDocumentBuilder();\n\t\t} catch (Exception e) {\n\t\t\tthrow new UtilException(e, \"Create xml document error!\");\n\t\t}\n\t\treturn builder;\n\t}\n\n\t/**\n\t * 创建{@link DocumentBuilderFactory}\n\t * <p>\n\t * 默认使用\"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl\"<br>\n\t * 如果使用第三方实现，请调用{@link #disableDefaultDocumentBuilderFactory()}\n\t * </p>\n\t *\n\t * @return {@link DocumentBuilderFactory}\n\t */\n\tpublic static DocumentBuilderFactory createDocumentBuilderFactory() {\n\t\tfinal DocumentBuilderFactory factory;\n\t\tif (StrUtil.isNotEmpty(defaultDocumentBuilderFactory)) {\n\t\t\tfactory = DocumentBuilderFactory.newInstance(defaultDocumentBuilderFactory, null);\n\t\t} else {\n\t\t\tfactory = DocumentBuilderFactory.newInstance();\n\t\t}\n\t\t// 默认打开NamespaceAware，getElementsByTagNameNS可以使用命名空间\n\t\tfactory.setNamespaceAware(namespaceAware);\n\t\treturn disableXXE(factory);\n\t}\n\n\t/**\n\t * 创建XML文档<br>\n\t * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码\n\t *\n\t * @param rootElementName 根节点名称\n\t * @return XML文档\n\t */\n\tpublic static Document createXml(String rootElementName) {\n\t\treturn createXml(rootElementName, null);\n\t}\n\n\t/**\n\t * 创建XML文档<br>\n\t * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码\n\t *\n\t * @param rootElementName 根节点名称\n\t * @param namespace       命名空间，无则传null\n\t * @return XML文档\n\t * @since 5.0.4\n\t */\n\tpublic static Document createXml(String rootElementName, String namespace) {\n\t\tfinal Document doc = createXml();\n\t\tdoc.appendChild(null == namespace ? doc.createElement(rootElementName) : doc.createElementNS(namespace, rootElementName));\n\t\treturn doc;\n\t}\n\n\t// -------------------------------------------------------------------------------------- Function\n\n\t/**\n\t * 获得XML文档根节点\n\t *\n\t * @param doc {@link Document}\n\t * @return 根节点\n\t * @see Document#getDocumentElement()\n\t * @since 3.0.8\n\t */\n\tpublic static Element getRootElement(Document doc) {\n\t\treturn (null == doc) ? null : doc.getDocumentElement();\n\t}\n\n\t/**\n\t * 获取节点所在的Document\n\t *\n\t * @param node 节点\n\t * @return {@link Document}\n\t * @since 5.3.0\n\t */\n\tpublic static Document getOwnerDocument(Node node) {\n\t\treturn (node instanceof Document) ? (Document) node : node.getOwnerDocument();\n\t}\n\n\t/**\n\t * 去除XML文本中的无效字符\n\t *\n\t * @param xmlContent XML文本\n\t * @return 当传入为null时返回null\n\t */\n\tpublic static String cleanInvalid(String xmlContent) {\n\t\tif (xmlContent == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn xmlContent.replaceAll(INVALID_REGEX, \"\");\n\t}\n\n\t/**\n\t * 去除XML文本中的注释内容\n\t *\n\t * @param xmlContent XML文本\n\t * @return 当传入为null时返回null\n\t * @since 5.4.5\n\t */\n\tpublic static String cleanComment(String xmlContent) {\n\t\tif (xmlContent == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn xmlContent.replaceAll(COMMENT_REGEX, StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 根据节点名获得子节点列表\n\t *\n\t * @param element 节点\n\t * @param tagName 节点名，如果节点名为空（null或blank），返回所有子节点\n\t * @return 节点列表\n\t */\n\tpublic static List<Element> getElements(Element element, String tagName) {\n\t\tfinal NodeList nodeList = StrUtil.isBlank(tagName) ? element.getChildNodes() : element.getElementsByTagName(tagName);\n\t\treturn transElements(element, nodeList);\n\t}\n\n\t/**\n\t * 根据节点名获得第一个子节点\n\t *\n\t * @param element 节点\n\t * @param tagName 节点名\n\t * @return 节点\n\t */\n\tpublic static Element getElement(Element element, String tagName) {\n\t\tfinal NodeList nodeList = element.getElementsByTagName(tagName);\n\t\tfinal int length = nodeList.getLength();\n\t\tif (length < 1) {\n\t\t\treturn null;\n\t\t}\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tElement childEle = (Element) nodeList.item(i);\n\t\t\tif (childEle == null || childEle.getParentNode() == element) {\n\t\t\t\treturn childEle;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据节点名获得第一个子节点\n\t *\n\t * @param element 节点\n\t * @param tagName 节点名\n\t * @return 节点中的值\n\t */\n\tpublic static String elementText(Element element, String tagName) {\n\t\tElement child = getElement(element, tagName);\n\t\treturn child == null ? null : child.getTextContent();\n\t}\n\n\t/**\n\t * 根据节点名获得第一个子节点\n\t *\n\t * @param element      节点\n\t * @param tagName      节点名\n\t * @param defaultValue 默认值\n\t * @return 节点中的值\n\t */\n\tpublic static String elementText(Element element, String tagName, String defaultValue) {\n\t\tElement child = getElement(element, tagName);\n\t\treturn child == null ? defaultValue : child.getTextContent();\n\t}\n\n\t/**\n\t * 将NodeList转换为Element列表\n\t *\n\t * @param nodeList NodeList\n\t * @return Element列表\n\t */\n\tpublic static List<Element> transElements(NodeList nodeList) {\n\t\treturn transElements(null, nodeList);\n\t}\n\n\t/**\n\t * 将NodeList转换为Element列表<br>\n\t * 非Element节点将被忽略\n\t *\n\t * @param parentEle 父节点，如果指定将返回此节点的所有直接子节点，null返回所有就节点\n\t * @param nodeList  NodeList\n\t * @return Element列表\n\t */\n\tpublic static List<Element> transElements(Element parentEle, NodeList nodeList) {\n\t\tint length = nodeList.getLength();\n\t\tfinal ArrayList<Element> elements = new ArrayList<>(length);\n\t\tNode node;\n\t\tElement element;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tnode = nodeList.item(i);\n\t\t\tif (Node.ELEMENT_NODE == node.getNodeType()) {\n\t\t\t\telement = (Element) nodeList.item(i);\n\t\t\t\tif (parentEle == null || element.getParentNode() == parentEle) {\n\t\t\t\t\telements.add(element);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn elements;\n\t}\n\n\t/**\n\t * 将可序列化的对象转换为XML写入文件，已经存在的文件将被覆盖<br>\n\t * Writes serializable object to a XML file. Existing file will be overwritten\n\t *\n\t * @param dest 目标文件\n\t * @param bean 对象\n\t */\n\tpublic static void writeObjectAsXml(File dest, Object bean) {\n\t\tXMLEncoder xmlenc = null;\n\t\ttry {\n\t\t\txmlenc = new XMLEncoder(FileUtil.getOutputStream(dest));\n\t\t\txmlenc.writeObject(bean);\n\t\t} finally {\n\t\t\t// 关闭XMLEncoder会相应关闭OutputStream\n\t\t\tIoUtil.close(xmlenc);\n\t\t}\n\t}\n\n\t/**\n\t * 创建XPath<br>\n\t * Xpath相关文章：https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html\n\t *\n\t * @return {@link XPath}\n\t * @since 3.2.0\n\t */\n\tpublic static XPath createXPath() {\n\t\treturn XPathFactory.newInstance().newXPath();\n\t}\n\n\t/**\n\t * 通过XPath方式读取XML节点等信息<br>\n\t * Xpath相关文章：https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html\n\t *\n\t * @param expression XPath表达式\n\t * @param source     资源，可以是Docunent、Node节点等\n\t * @return 匹配返回类型的值\n\t * @since 4.0.9\n\t */\n\tpublic static Element getElementByXPath(String expression, Object source) {\n\t\treturn (Element) getNodeByXPath(expression, source);\n\t}\n\n\t/**\n\t * 通过XPath方式读取XML的NodeList<br>\n\t * Xpath相关文章：https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html\n\t *\n\t * @param expression XPath表达式\n\t * @param source     资源，可以是Docunent、Node节点等\n\t * @return NodeList\n\t * @since 4.0.9\n\t */\n\tpublic static NodeList getNodeListByXPath(String expression, Object source) {\n\t\treturn (NodeList) getByXPath(expression, source, XPathConstants.NODESET);\n\t}\n\n\t/**\n\t * 通过XPath方式读取XML节点等信息<br>\n\t * Xpath相关文章：https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html\n\t *\n\t * @param expression XPath表达式\n\t * @param source     资源，可以是Docunent、Node节点等\n\t * @return 匹配返回类型的值\n\t * @since 4.0.9\n\t */\n\tpublic static Node getNodeByXPath(String expression, Object source) {\n\t\treturn (Node) getByXPath(expression, source, XPathConstants.NODE);\n\t}\n\n\t/**\n\t * 通过XPath方式读取XML节点等信息<br>\n\t * Xpath相关文章：https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html\n\t *\n\t * @param expression XPath表达式\n\t * @param source     资源，可以是Docunent、Node节点等\n\t * @param returnType 返回类型，{@link javax.xml.xpath.XPathConstants}\n\t * @return 匹配返回类型的值\n\t * @since 3.2.0\n\t */\n\tpublic static Object getByXPath(String expression, Object source, QName returnType) {\n\t\tNamespaceContext nsContext = null;\n\t\tif (source instanceof Node) {\n\t\t\tnsContext = new UniversalNamespaceCache((Node) source, false);\n\t\t}\n\t\treturn getByXPath(expression, source, returnType, nsContext);\n\t}\n\n\t/**\n\t * 通过XPath方式读取XML节点等信息<br>\n\t * Xpath相关文章：<br>\n\t * https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html<br>\n\t * https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/\n\t *\n\t * @param expression XPath表达式\n\t * @param source     资源，可以是Docunent、Node节点等\n\t * @param returnType 返回类型，{@link javax.xml.xpath.XPathConstants}\n\t * @param nsContext  {@link NamespaceContext}\n\t * @return 匹配返回类型的值\n\t * @since 5.3.1\n\t */\n\tpublic static Object getByXPath(String expression, Object source, QName returnType, NamespaceContext nsContext) {\n\t\tfinal XPath xPath = createXPath();\n\t\tif (null != nsContext) {\n\t\t\txPath.setNamespaceContext(nsContext);\n\t\t}\n\t\ttry {\n\t\t\tif (source instanceof InputSource) {\n\t\t\t\treturn xPath.evaluate(expression, (InputSource) source, returnType);\n\t\t\t} else {\n\t\t\t\treturn xPath.evaluate(expression, source, returnType);\n\t\t\t}\n\t\t} catch (XPathExpressionException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 转义XML特殊字符:\n\t *\n\t * <pre>\n\t * &amp; (ampersand) 替换为 &amp;amp;\n\t * &lt; (小于) 替换为 &amp;lt;\n\t * &gt; (大于) 替换为 &amp;gt;\n\t * &quot; (双引号) 替换为 &amp;quot;\n\t * ' (单引号) 替换为 &amp;apos;\n\t * </pre>\n\t *\n\t * @param string 被替换的字符串\n\t * @return 替换后的字符串\n\t * @since 4.0.8\n\t */\n\tpublic static String escape(String string) {\n\t\treturn EscapeUtil.escapeXml(string);\n\t}\n\n\t/**\n\t * 反转义XML特殊字符:\n\t *\n\t * @param string 被替换的字符串\n\t * @return 替换后的字符串\n\t * @see EscapeUtil#unescape(String)\n\t * @since 5.0.6\n\t */\n\tpublic static String unescape(String string) {\n\t\treturn EscapeUtil.unescapeXml(string);\n\t}\n\n\t/**\n\t * XML格式字符串转换为Map\n\t *\n\t * @param xmlStr XML字符串\n\t * @return XML数据转换后的Map\n\t * @since 4.0.8\n\t */\n\tpublic static Map<String, Object> xmlToMap(String xmlStr) {\n\t\treturn xmlToMap(xmlStr, new HashMap<>());\n\t}\n\n\t/**\n\t * XML转Java Bean\n\t *\n\t * @param <T>  bean类型\n\t * @param node XML节点\n\t * @param bean bean类\n\t * @return bean\n\t * @see JAXBUtil#xmlToBean(String, Class)\n\t * @since 5.2.4\n\t */\n\tpublic static <T> T xmlToBean(Node node, Class<T> bean) {\n\t\treturn xmlToBean(node, bean, null);\n\t}\n\n\t/**\n\t * XML转Java Bean\n\t *\n\t * @param <T>         bean类型\n\t * @param node        XML节点\n\t * @param bean        bean类\n\t * @param copyOptions Bean转换选项，可选是否忽略错误等\n\t * @return bean\n\t * @see JAXBUtil#xmlToBean(String, Class)\n\t * @since 5.8.30\n\t */\n\tpublic static <T> T xmlToBean(Node node, Class<T> bean, CopyOptions copyOptions) {\n\t\tfinal Map<String, Object> map = xmlToMap(node);\n\t\tif (null != map && map.size() == 1) {\n\t\t\tfinal String simpleName = bean.getSimpleName();\n\t\t\tfinal String nodeName = CollUtil.getFirst(map.keySet());\n\t\t\tif (simpleName.equalsIgnoreCase(nodeName)) {\n\t\t\t\t// 只有key和bean的名称匹配时才做单一对象转换\n\t\t\t\treturn BeanUtil.toBean(map.get(nodeName), bean, copyOptions);\n\t\t\t}\n\t\t}\n\t\treturn BeanUtil.toBean(map, bean, copyOptions);\n\t}\n\n\t/**\n\t * XML格式字符串转换为Map\n\t *\n\t * @param node XML节点\n\t * @return XML数据转换后的Map\n\t * @since 4.0.8\n\t */\n\tpublic static Map<String, Object> xmlToMap(Node node) {\n\t\treturn xmlToMap(node, new HashMap<>());\n\t}\n\n\t/**\n\t * XML格式字符串转换为Map<br>\n\t * 只支持第一级别的XML，不支持多级XML\n\t *\n\t * @param xmlStr XML字符串\n\t * @param result 结果Map类型\n\t * @return XML数据转换后的Map\n\t * @since 4.0.8\n\t */\n\tpublic static Map<String, Object> xmlToMap(String xmlStr, Map<String, Object> result) {\n\t\tfinal Document doc = parseXml(xmlStr);\n\t\tfinal Element root = getRootElement(doc);\n\t\troot.normalize();\n\n\t\treturn xmlToMap(root, result);\n\t}\n\n\t/**\n\t * XML节点转换为Map\n\t *\n\t * @param node   XML节点\n\t * @param result 结果Map类型\n\t * @return XML数据转换后的Map\n\t * @since 4.0.8\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static Map<String, Object> xmlToMap(Node node, Map<String, Object> result) {\n\t\tif (null == result) {\n\t\t\tresult = new HashMap<>();\n\t\t}\n\t\tfinal NodeList nodeList = node.getChildNodes();\n\t\tfinal int length = nodeList.getLength();\n\t\tNode childNode;\n\t\tElement childEle;\n\t\tfor (int i = 0; i < length; ++i) {\n\t\t\tchildNode = nodeList.item(i);\n\t\t\tif (false == isElement(childNode)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tchildEle = (Element) childNode;\n\t\t\tfinal Object value = result.get(childEle.getNodeName());\n\t\t\tObject newValue;\n\t\t\tif (childEle.hasChildNodes()) {\n\t\t\t\t// 子节点继续递归遍历\n\t\t\t\tfinal Map<String, Object> map = xmlToMap(childEle);\n\t\t\t\tif (MapUtil.isNotEmpty(map)) {\n\t\t\t\t\tnewValue = map;\n\t\t\t\t} else {\n\t\t\t\t\tnewValue = childEle.getTextContent();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewValue = childEle.getTextContent();\n\t\t\t}\n\n\n\t\t\tif (null != newValue) {\n\t\t\t\tif (null != value) {\n\t\t\t\t\tif (value instanceof List) {\n\t\t\t\t\t\t((List<Object>) value).add(newValue);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult.put(childEle.getNodeName(), CollUtil.newArrayList(value, newValue));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tresult.put(childEle.getNodeName(), newValue);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data Map类型数据\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.1.2\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data) {\n\t\treturn toStr(mapToXml(data, \"xml\"));\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data               Map类型数据\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.1.2\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, boolean omitXmlDeclaration) {\n\t\treturn toStr(mapToXml(data, \"xml\"), CharsetUtil.UTF_8, false, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data     Map类型数据\n\t * @param rootName 根节点名\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 4.0.8\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, String rootName) {\n\t\treturn toStr(mapToXml(data, rootName));\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data      Map类型数据\n\t * @param rootName  根节点名\n\t * @param namespace 命名空间，可以为null\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.0.4\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, String rootName, String namespace) {\n\t\treturn toStr(mapToXml(data, rootName, namespace));\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data               Map类型数据\n\t * @param rootName           根节点名\n\t * @param namespace          命名空间，可以为null\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.1.2\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, String rootName, String namespace, boolean omitXmlDeclaration) {\n\t\treturn toStr(mapToXml(data, rootName, namespace), CharsetUtil.UTF_8, false, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data               Map类型数据\n\t * @param rootName           根节点名\n\t * @param namespace          命名空间，可以为null\n\t * @param isPretty           是否格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.1.2\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, String rootName, String namespace, boolean isPretty, boolean omitXmlDeclaration) {\n\t\treturn toStr(mapToXml(data, rootName, namespace), CharsetUtil.UTF_8, isPretty, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将Map转换为XML格式的字符串\n\t *\n\t * @param data               Map类型数据\n\t * @param rootName           根节点名\n\t * @param namespace          命名空间，可以为null\n\t * @param charset            编码\n\t * @param isPretty           是否格式化输出\n\t * @param omitXmlDeclaration 是否忽略 xml Declaration\n\t * @return XML格式的字符串\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.1.2\n\t */\n\tpublic static String mapToXmlStr(Map<?, ?> data, String rootName, String namespace, String charset, boolean isPretty, boolean omitXmlDeclaration) {\n\t\treturn toStr(mapToXml(data, rootName, namespace), charset, isPretty, omitXmlDeclaration);\n\t}\n\n\t/**\n\t * 将Map转换为XML\n\t *\n\t * @param data     Map类型数据\n\t * @param rootName 根节点名\n\t * @return XML\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 4.0.9\n\t */\n\tpublic static Document mapToXml(Map<?, ?> data, String rootName) {\n\t\treturn mapToXml(data, rootName, null);\n\t}\n\n\t/**\n\t * 将Map转换为XML\n\t *\n\t * @param data      Map类型数据\n\t * @param rootName  根节点名\n\t * @param namespace 命名空间，可以为null\n\t * @return XML\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.0.4\n\t */\n\tpublic static Document mapToXml(Map<?, ?> data, String rootName, String namespace) {\n\t\tfinal Document doc = createXml();\n\t\tfinal Element root = appendChild(doc, rootName, namespace);\n\n\t\tappendMap(doc, root, data);\n\t\treturn doc;\n\t}\n\n\t/**\n\t * 将Bean转换为XML\n\t *\n\t * @param bean Bean对象\n\t * @return XML\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.3.4\n\t */\n\tpublic static Document beanToXml(Object bean) {\n\t\treturn beanToXml(bean, null);\n\t}\n\n\t/**\n\t * 将Bean转换为XML\n\t *\n\t * @param bean      Bean对象\n\t * @param namespace 命名空间，可以为null\n\t * @return XML\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.2.4\n\t */\n\tpublic static Document beanToXml(Object bean, String namespace) {\n\t\treturn beanToXml(bean, namespace, false);\n\t}\n\n\t/**\n\t * 将Bean转换为XML\n\t *\n\t * @param bean       Bean对象\n\t * @param namespace  命名空间，可以为null\n\t * @param ignoreNull 忽略值为{@code null}的属性\n\t * @return XML\n\t * @see JAXBUtil#beanToXml(Object)\n\t * @since 5.7.10\n\t */\n\tpublic static Document beanToXml(Object bean, String namespace, boolean ignoreNull) {\n\t\tif (null == bean) {\n\t\t\treturn null;\n\t\t}\n\t\treturn mapToXml(BeanUtil.beanToMap(bean, false, ignoreNull),\n\t\t\tbean.getClass().getSimpleName(), namespace);\n\t}\n\n\t/**\n\t * 给定节点是否为{@link Element} 类型节点\n\t *\n\t * @param node 节点\n\t * @return 是否为{@link Element} 类型节点\n\t * @since 4.0.8\n\t */\n\tpublic static boolean isElement(Node node) {\n\t\treturn (null != node) && Node.ELEMENT_NODE == node.getNodeType();\n\t}\n\n\t/**\n\t * 在已有节点上创建子节点\n\t *\n\t * @param node    节点\n\t * @param tagName 标签名\n\t * @return 子节点\n\t * @since 4.0.9\n\t */\n\tpublic static Element appendChild(Node node, String tagName) {\n\t\treturn appendChild(node, tagName, null);\n\t}\n\n\t/**\n\t * 在已有节点上创建子节点\n\t *\n\t * @param node      节点\n\t * @param tagName   标签名\n\t * @param namespace 命名空间，无传null\n\t * @return 子节点\n\t * @since 5.0.4\n\t */\n\tpublic static Element appendChild(Node node, String tagName, String namespace) {\n\t\tfinal Document doc = getOwnerDocument(node);\n\t\tfinal Element child = (null == namespace) ? doc.createElement(tagName) : doc.createElementNS(namespace, tagName);\n\t\tnode.appendChild(child);\n\t\treturn child;\n\t}\n\n\t/**\n\t * 创建文本子节点\n\t *\n\t * @param node 节点\n\t * @param text 文本\n\t * @return 子节点\n\t * @since 5.3.0\n\t */\n\tpublic static Node appendText(Node node, CharSequence text) {\n\t\treturn appendText(getOwnerDocument(node), node, text);\n\t}\n\n\t/**\n\t * 追加数据子节点，可以是Map、集合、文本\n\t *\n\t * @param node 节点\n\t * @param data 数据\n\t * @since 5.7.10\n\t */\n\tpublic static void append(Node node, Object data) {\n\t\tappend(getOwnerDocument(node), node, data);\n\t}\n\t// ---------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 追加数据子节点，可以是Map、集合、文本\n\t *\n\t * @param doc  {@link Document}\n\t * @param node 节点\n\t * @param data 数据\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static void append(Document doc, Node node, Object data) {\n\t\tif (data instanceof Map) {\n\t\t\t// 如果值依旧为map，递归继续\n\t\t\tappendMap(doc, node, (Map) data);\n\t\t} else if (data instanceof Iterator) {\n\t\t\t// 如果值依旧为map，递归继续\n\t\t\tappendIterator(doc, node, (Iterator) data);\n\t\t} else if (data instanceof Iterable) {\n\t\t\t// 如果值依旧为map，递归继续\n\t\t\tappendIterator(doc, node, ((Iterable) data).iterator());\n\t\t} else {\n\t\t\tappendText(doc, node, data.toString());\n\t\t}\n\t}\n\n\t/**\n\t * 追加Map数据子节点\n\t *\n\t * @param doc  {@link Document}\n\t * @param node 当前节点\n\t * @param data Map类型数据\n\t * @since 4.0.8\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tprivate static void appendMap(Document doc, Node node, Map data) {\n\t\tdata.forEach((key, value) -> {\n\t\t\tif (null != key) {\n\t\t\t\tfinal Element child = appendChild(node, key.toString());\n\t\t\t\tif (null != value) {\n\t\t\t\t\tappend(doc, child, value);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 追加集合节点\n\t *\n\t * @param doc  {@link Document}\n\t * @param node 节点\n\t * @param data 数据\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static void appendIterator(Document doc, Node node, Iterator data) {\n\t\tfinal Node parentNode = node.getParentNode();\n\t\tboolean isFirst = true;\n\t\tObject eleData;\n\t\twhile (data.hasNext()) {\n\t\t\teleData = data.next();\n\t\t\tif (isFirst) {\n\t\t\t\tappend(doc, node, eleData);\n\t\t\t\tisFirst = false;\n\t\t\t} else {\n\t\t\t\tfinal Node cloneNode = node.cloneNode(false);\n\t\t\t\tparentNode.appendChild(cloneNode);\n\t\t\t\tappend(doc, cloneNode, eleData);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 追加文本节点\n\t *\n\t * @param doc  {@link Document}\n\t * @param node 节点\n\t * @param text 文本内容\n\t * @return 增加的子节点，即Text节点\n\t * @since 5.3.0\n\t */\n\tprivate static Node appendText(Document doc, Node node, CharSequence text) {\n\t\treturn node.appendChild(doc.createTextNode(StrUtil.str(text)));\n\t}\n\n\t/**\n\t * 关闭XXE，避免漏洞攻击<br>\n\t * see: https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#JAXP_DocumentBuilderFactory.2C_SAXParserFactory_and_DOM4J\n\t *\n\t * @param dbf DocumentBuilderFactory\n\t * @return DocumentBuilderFactory\n\t */\n\tprivate static DocumentBuilderFactory disableXXE(DocumentBuilderFactory dbf) {\n\t\tString feature;\n\t\ttry {\n\t\t\t// This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented\n\t\t\t// Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl\n\t\t\tfeature = \"http://apache.org/xml/features/disallow-doctype-decl\";\n\t\t\tdbf.setFeature(feature, true);\n\t\t\t// If you can't completely disable DTDs, then at least do the following:\n\t\t\t// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities\n\t\t\t// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities\n\t\t\t// JDK7+ - http://xml.org/sax/features/external-general-entities\n\t\t\tfeature = \"http://xml.org/sax/features/external-general-entities\";\n\t\t\tdbf.setFeature(feature, false);\n\t\t\t// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities\n\t\t\t// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities\n\t\t\t// JDK7+ - http://xml.org/sax/features/external-parameter-entities\n\t\t\tfeature = \"http://xml.org/sax/features/external-parameter-entities\";\n\t\t\tdbf.setFeature(feature, false);\n\t\t\t// Disable external DTDs as well\n\t\t\tfeature = \"http://apache.org/xml/features/nonvalidating/load-external-dtd\";\n\t\t\tdbf.setFeature(feature, false);\n\t\t\t// and these as well, per Timothy Morgan's 2014 paper: \"XML Schema, DTD, and Entity Attacks\"\n\t\t\tdbf.setXIncludeAware(false);\n\t\t\tdbf.setExpandEntityReferences(false);\n\t\t} catch (ParserConfigurationException e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn dbf;\n\t}\n\n\t/**\n\t * 全局命名空间上下文<br>\n\t * 见：https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/\n\t */\n\tpublic static class UniversalNamespaceCache implements NamespaceContext {\n\t\tprivate static final String DEFAULT_NS = \"DEFAULT\";\n\t\tprivate final BiMap<String, String> prefixUri = new BiMap<>(new HashMap<>());\n\n\t\t/**\n\t\t * This constructor parses the document and stores all namespaces it can\n\t\t * find. If toplevelOnly is true, only namespaces in the root are used.\n\t\t *\n\t\t * @param node         source Node\n\t\t * @param toplevelOnly restriction of the search to enhance performance\n\t\t */\n\t\tpublic UniversalNamespaceCache(Node node, boolean toplevelOnly) {\n\t\t\texamineNode(node.getFirstChild(), toplevelOnly);\n\t\t}\n\n\t\t/**\n\t\t * A single node is read, the namespace attributes are extracted and stored.\n\t\t *\n\t\t * @param node            to examine\n\t\t * @param attributesOnly, if true no recursion happens\n\t\t */\n\t\tprivate void examineNode(Node node, boolean attributesOnly) {\n\t\t\tfinal NamedNodeMap attributes = node.getAttributes();\n\t\t\t//noinspection ConstantConditions\n\t\t\tif (null != attributes) {\n\t\t\t\tfinal int length = attributes.getLength();\n\t\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\t\tNode attribute = attributes.item(i);\n\t\t\t\t\tstoreAttribute(attribute);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (false == attributesOnly) {\n\t\t\t\tfinal NodeList childNodes = node.getChildNodes();\n\t\t\t\t//noinspection ConstantConditions\n\t\t\t\tif (null != childNodes) {\n\t\t\t\t\tNode item;\n\t\t\t\t\tfinal int childLength = childNodes.getLength();\n\t\t\t\t\tfor (int i = 0; i < childLength; i++) {\n\t\t\t\t\t\titem = childNodes.item(i);\n\t\t\t\t\t\tif (item.getNodeType() == Node.ELEMENT_NODE)\n\t\t\t\t\t\t\texamineNode(item, false);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * This method looks at an attribute and stores it, if it is a namespace\n\t\t * attribute.\n\t\t *\n\t\t * @param attribute to examine\n\t\t */\n\t\tprivate void storeAttribute(Node attribute) {\n\t\t\tif (null == attribute) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// examine the attributes in namespace xmlns\n\t\t\tif (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attribute.getNamespaceURI())) {\n\t\t\t\t// Default namespace xmlns=\"uri goes here\"\n\t\t\t\tif (XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.getNodeName())) {\n\t\t\t\t\tprefixUri.put(DEFAULT_NS, attribute.getNodeValue());\n\t\t\t\t} else {\n\t\t\t\t\t// The defined prefixes are stored here\n\t\t\t\t\tprefixUri.put(attribute.getLocalName(), attribute.getNodeValue());\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t/**\n\t\t * This method is called by XPath. It returns the default namespace, if the\n\t\t * prefix is null or \"\".\n\t\t *\n\t\t * @param prefix to search for\n\t\t * @return uri\n\t\t */\n\t\t@Override\n\t\tpublic String getNamespaceURI(String prefix) {\n\t\t\tif (prefix == null || XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {\n\t\t\t\treturn prefixUri.get(DEFAULT_NS);\n\t\t\t} else {\n\t\t\t\treturn prefixUri.get(prefix);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * This method is not needed in this context, but can be implemented in a\n\t\t * similar way.\n\t\t */\n\t\t@Override\n\t\tpublic String getPrefix(String namespaceURI) {\n\t\t\treturn prefixUri.getInverse().get(namespaceURI);\n\t\t}\n\n\t\t@Override\n\t\tpublic Iterator<String> getPrefixes(String namespaceURI) {\n\t\t\t// Not implemented\n\t\t\treturn null;\n\t\t}\n\n\t}\n\t// ---------------------------------------------------------------------------------------- Private method end\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.EnumerationIter;\nimport cn.hutool.core.compress.Deflate;\nimport cn.hutool.core.compress.Gzip;\nimport cn.hutool.core.compress.ZipCopyVisitor;\nimport cn.hutool.core.compress.ZipReader;\nimport cn.hutool.core.compress.ZipWriter;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.LimitedInputStream;\nimport cn.hutool.core.io.file.FileSystemUtil;\nimport cn.hutool.core.io.file.PathUtil;\nimport cn.hutool.core.io.resource.Resource;\n\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileFilter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.nio.file.CopyOption;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.FileSystem;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.zip.*;\n\n/**\n * 压缩工具类\n *\n * @author Looly\n * @see cn.hutool.core.compress.ZipWriter\n */\npublic class ZipUtil {\n\n\tprivate static final int DEFAULT_BYTE_ARRAY_LENGTH = 32;\n\n\t/**\n\t * 默认编码，使用平台相关编码\n\t */\n\tprivate static final Charset DEFAULT_CHARSET = CharsetUtil.defaultCharset();\n\n\t/**\n\t * 将Zip文件转换为{@link ZipFile}\n\t *\n\t * @param file    zip文件\n\t * @param charset 解析zip文件的编码，null表示{@link CharsetUtil#CHARSET_UTF_8}\n\t * @return {@link ZipFile}\n\t */\n\tpublic static ZipFile toZipFile(File file, Charset charset) {\n\t\ttry {\n\t\t\treturn new ZipFile(file, ObjectUtil.defaultIfNull(charset, CharsetUtil.CHARSET_UTF_8));\n\t\t} catch (IOException e) {\n\t\t\t// issue#I3UZ28 可能编码错误提示\n\t\t\tif(e instanceof ZipException){\n\t\t\t\tif(e.getMessage().contains(\"invalid CEN header\")){\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// 尝试使用不同编码\n\t\t\t\t\t\treturn new ZipFile(file, CharsetUtil.CHARSET_UTF_8.equals(charset) ? CharsetUtil.CHARSET_GBK : CharsetUtil.CHARSET_UTF_8);\n\t\t\t\t\t} catch (final IOException ex) {\n\t\t\t\t\t\tthrow new IORuntimeException(ex);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定{@link ZipEntry}的流，用于读取这个entry的内容<br>\n\t * 此处使用{@link LimitedInputStream} 限制最大写出大小，避免ZIP bomb漏洞\n\t *\n\t * @param zipFile  {@link ZipFile}\n\t * @param zipEntry {@link ZipEntry}\n\t * @return 流\n\t * @since 5.5.2\n\t */\n\tpublic static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) {\n\t\ttry {\n\t\t\treturn new LimitedInputStream(zipFile.getInputStream(zipEntry), zipEntry.getSize());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得 {@link ZipOutputStream}\n\t *\n\t * @param out     压缩文件流\n\t * @param charset 编码\n\t * @return {@link ZipOutputStream}\n\t * @since 5.8.0\n\t */\n\tpublic static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) {\n\t\tif (out instanceof ZipOutputStream) {\n\t\t\treturn (ZipOutputStream) out;\n\t\t}\n\t\treturn new ZipOutputStream(out, charset);\n\t}\n\n\t/**\n\t * 在zip文件中添加新文件或目录<br>\n\t * 新文件添加在zip根目录，文件夹包括其本身和内容<br>\n\t * 如果待添加文件夹是系统根路径（如/或c:/），则只复制文件夹下的内容\n\t *\n\t * @param zipPath        zip文件的Path\n\t * @param appendFilePath 待添加文件Path(可以是文件夹)\n\t * @param options        拷贝选项，可选是否覆盖等\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.15\n\t */\n\tpublic static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IORuntimeException {\n\t\ttry (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) {\n\t\t\tif (Files.isDirectory(appendFilePath)) {\n\t\t\t\tPath source = appendFilePath.getParent();\n\t\t\t\tif (null == source) {\n\t\t\t\t\t// 如果用户提供的是根路径，则不复制目录，直接复制目录下的内容\n\t\t\t\t\tsource = appendFilePath;\n\t\t\t\t}\n\t\t\t\tFiles.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options));\n\t\t\t} else {\n\t\t\t\tFiles.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options);\n\t\t\t}\n\t\t} catch (FileAlreadyExistsException ignored) {\n\t\t\t// 不覆盖情况下，文件已存在, 跳过\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 打包到当前目录，使用默认编码UTF-8\n\t *\n\t * @param srcPath 源文件路径\n\t * @return 打包好的压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(String srcPath) throws UtilException {\n\t\treturn zip(srcPath, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 打包到当前目录\n\t *\n\t * @param srcPath 源文件路径\n\t * @param charset 编码\n\t * @return 打包好的压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(String srcPath, Charset charset) throws UtilException {\n\t\treturn zip(FileUtil.file(srcPath), charset);\n\t}\n\n\t/**\n\t * 打包到当前目录，使用默认编码UTF-8\n\t *\n\t * @param srcFile 源文件或目录\n\t * @return 打包好的压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(File srcFile) throws UtilException {\n\t\treturn zip(srcFile, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 打包到当前目录\n\t *\n\t * @param srcFile 源文件或目录\n\t * @param charset 编码\n\t * @return 打包好的压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(File srcFile, Charset charset) throws UtilException {\n\t\tfinal File zipFile = FileUtil.file(srcFile.getParentFile(), FileUtil.mainName(srcFile) + \".zip\");\n\t\tzip(zipFile, charset, false, srcFile);\n\t\treturn zipFile;\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩<br>\n\t * 不包含被打包目录\n\t *\n\t * @param srcPath 要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @param zipPath 压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @return 压缩好的Zip文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(String srcPath, String zipPath) throws UtilException {\n\t\treturn zip(srcPath, zipPath, false);\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩<br>\n\t *\n\t * @param srcPath    要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @param zipPath    压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param withSrcDir 是否包含被打包目录\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(String srcPath, String zipPath, boolean withSrcDir) throws UtilException {\n\t\treturn zip(srcPath, zipPath, DEFAULT_CHARSET, withSrcDir);\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩<br>\n\t *\n\t * @param srcPath    要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @param zipPath    压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param charset    编码\n\t * @param withSrcDir 是否包含被打包目录\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(String srcPath, String zipPath, Charset charset, boolean withSrcDir) throws UtilException {\n\t\tfinal File srcFile = FileUtil.file(srcPath);\n\t\tfinal File zipFile = FileUtil.file(zipPath);\n\t\tzip(zipFile, charset, withSrcDir, srcFile);\n\t\treturn zipFile;\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩<br>\n\t * 使用默认UTF-8编码\n\t *\n\t * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param srcFiles   要压缩的源文件或目录。\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(File zipFile, boolean withSrcDir, File... srcFiles) throws UtilException {\n\t\treturn zip(zipFile, DEFAULT_CHARSET, withSrcDir, srcFiles);\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩\n\t *\n\t * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param charset    编码\n\t * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File zip(File zipFile, Charset charset, boolean withSrcDir, File... srcFiles) throws UtilException {\n\t\treturn zip(zipFile, charset, withSrcDir, null, srcFiles);\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩\n\t *\n\t * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param charset    编码\n\t * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）\n\t * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @return 压缩文件\n\t * @throws IORuntimeException IO异常\n\t * @since 4.6.5\n\t */\n\tpublic static File zip(File zipFile, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {\n\t\tvalidateFiles(zipFile, srcFiles);\n\t\t//noinspection resource\n\t\tZipWriter.of(zipFile, charset).add(withSrcDir, filter, srcFiles).close();\n\t\treturn zipFile;\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩\n\t *\n\t * @param out        生成的Zip到的目标流，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param charset    编码\n\t * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）\n\t * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @throws IORuntimeException IO异常\n\t * @since 5.1.1\n\t */\n\tpublic static void zip(OutputStream out, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {\n\t\tZipWriter.of(out, charset).add(withSrcDir, filter, srcFiles).close();\n\t}\n\n\t/**\n\t * 对文件或文件目录进行压缩\n\t *\n\t * @param zipOutputStream 生成的Zip到的目标流，自动关闭此流\n\t * @param withSrcDir      是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩\n\t * @param filter          文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）\n\t * @param srcFiles        要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径\n\t * @throws IORuntimeException IO异常\n\t * @since 5.1.1\n\t * @deprecated 请使用 {@link #zip(OutputStream, Charset, boolean, FileFilter, File...)}\n\t */\n\t@Deprecated\n\tpublic static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {\n\t\ttry (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {\n\t\t\tzipWriter.add(withSrcDir, filter, srcFiles);\n\t\t}\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件，使用默认UTF-8编码\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param path    流数据在压缩文件中的路径或文件名\n\t * @param data    要压缩的数据\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.0.6\n\t */\n\tpublic static File zip(File zipFile, String path, String data) throws UtilException {\n\t\treturn zip(zipFile, path, data, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param path    流数据在压缩文件中的路径或文件名\n\t * @param data    要压缩的数据\n\t * @param charset 编码\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static File zip(File zipFile, String path, String data, Charset charset) throws UtilException {\n\t\treturn zip(zipFile, path, IoUtil.toStream(data, charset), charset);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t * 使用默认编码UTF-8\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param path    流数据在压缩文件中的路径或文件名\n\t * @param in      要压缩的源\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.0.6\n\t */\n\tpublic static File zip(File zipFile, String path, InputStream in) throws UtilException {\n\t\treturn zip(zipFile, path, in, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param path    流数据在压缩文件中的路径或文件名\n\t * @param in      要压缩的源，默认关闭\n\t * @param charset 编码\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static File zip(File zipFile, String path, InputStream in, Charset charset) throws UtilException {\n\t\treturn zip(zipFile, new String[]{path}, new InputStream[]{in}, charset);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t * 路径列表和流列表长度必须一致\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param paths   流数据在压缩文件中的路径或文件名\n\t * @param ins     要压缩的源，添加完成后自动关闭流\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic static File zip(File zipFile, String[] paths, InputStream[] ins) throws UtilException {\n\t\treturn zip(zipFile, paths, ins, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t * 路径列表和流列表长度必须一致\n\t *\n\t * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param paths   流数据在压缩文件中的路径或文件名\n\t * @param ins     要压缩的源，添加完成后自动关闭流\n\t * @param charset 编码\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 3.0.9\n\t */\n\tpublic static File zip(File zipFile, String[] paths, InputStream[] ins, Charset charset) throws UtilException {\n\t\ttry (final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)) {\n\t\t\tzipWriter.add(paths, ins);\n\t\t}\n\n\t\treturn zipFile;\n\t}\n\n\t/**\n\t * 将文件流压缩到目标流中\n\t *\n\t * @param out   目标流，压缩完成自动关闭\n\t * @param paths 流数据在压缩文件中的路径或文件名\n\t * @param ins   要压缩的源，添加完成后自动关闭流\n\t * @since 5.5.2\n\t */\n\tpublic static void zip(OutputStream out, String[] paths, InputStream[] ins) {\n\t\tzip(getZipOutputStream(out, DEFAULT_CHARSET), paths, ins);\n\t}\n\n\t/**\n\t * 将文件流压缩到目标流中\n\t *\n\t * @param zipOutputStream 目标流，压缩完成自动关闭\n\t * @param paths           流数据在压缩文件中的路径或文件名\n\t * @param ins             要压缩的源，添加完成后自动关闭流\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.2\n\t */\n\tpublic static void zip(ZipOutputStream zipOutputStream, String[] paths, InputStream[] ins) throws IORuntimeException {\n\t\ttry (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {\n\t\t\tzipWriter.add(paths, ins);\n\t\t}\n\t}\n\n\t/**\n\t * 对流中的数据加入到压缩文件<br>\n\t * 路径列表和流列表长度必须一致\n\t *\n\t * @param zipFile   生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹\n\t * @param charset   编码\n\t * @param resources 需要压缩的资源，资源的路径为{@link Resource#getName()}\n\t * @return 压缩文件\n\t * @throws UtilException IO异常\n\t * @since 5.5.2\n\t */\n\tpublic static File zip(File zipFile, Charset charset, Resource... resources) throws UtilException {\n\t\t//noinspection resource\n\t\tZipWriter.of(zipFile, charset).add(resources).close();\n\t\treturn zipFile;\n\t}\n\n\t// ---------------------------------------------------------------------------------------------- Unzip\n\n\t/**\n\t * 解压到文件名相同的目录中，默认编码UTF-8\n\t *\n\t * @param zipFilePath 压缩文件路径\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File unzip(String zipFilePath) throws UtilException {\n\t\treturn unzip(zipFilePath, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 解压到文件名相同的目录中\n\t *\n\t * @param zipFilePath 压缩文件路径\n\t * @param charset     编码\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static File unzip(String zipFilePath, Charset charset) throws UtilException {\n\t\treturn unzip(FileUtil.file(zipFilePath), charset);\n\t}\n\n\t/**\n\t * 解压到文件名相同的目录中，使用UTF-8编码\n\t *\n\t * @param zipFile 压缩文件\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static File unzip(File zipFile) throws UtilException {\n\t\treturn unzip(zipFile, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 解压到文件名相同的目录中\n\t *\n\t * @param zipFile 压缩文件\n\t * @param charset 编码\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t * @since 3.2.2\n\t */\n\tpublic static File unzip(File zipFile, Charset charset) throws UtilException {\n\t\tfinal File destDir = FileUtil.file(zipFile.getParentFile(), FileUtil.mainName(zipFile));\n\t\treturn unzip(zipFile, destDir, charset);\n\t}\n\n\t/**\n\t * 解压，默认UTF-8编码\n\t *\n\t * @param zipFilePath 压缩文件的路径\n\t * @param outFileDir  解压到的目录\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File unzip(String zipFilePath, String outFileDir) throws UtilException {\n\t\treturn unzip(zipFilePath, outFileDir, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 解压\n\t *\n\t * @param zipFilePath 压缩文件的路径\n\t * @param outFileDir  解压到的目录\n\t * @param charset     编码\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File unzip(String zipFilePath, String outFileDir, Charset charset) throws UtilException {\n\t\treturn unzip(FileUtil.file(zipFilePath), FileUtil.mkdir(outFileDir), charset);\n\t}\n\n\t/**\n\t * 解压，默认使用UTF-8编码\n\t *\n\t * @param zipFile zip文件\n\t * @param outFile 解压到的目录\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t */\n\tpublic static File unzip(File zipFile, File outFile) throws UtilException {\n\t\treturn unzip(zipFile, outFile, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 解压\n\t *\n\t * @param zipFile zip文件\n\t * @param outFile 解压到的目录\n\t * @param charset 编码\n\t * @return 解压的目录\n\t * @since 3.2.2\n\t */\n\tpublic static File unzip(File zipFile, File outFile, Charset charset) {\n\t\treturn unzip(toZipFile(zipFile, charset), outFile);\n\t}\n\n\t/**\n\t * 解压\n\t *\n\t * @param zipFile zip文件，附带编码信息，使用完毕自动关闭\n\t * @param outFile 解压到的目录\n\t * @return 解压的目录\n\t * @throws IORuntimeException IO异常\n\t * @since 4.5.8\n\t */\n\tpublic static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException {\n\t\treturn unzip(zipFile, outFile, -1);\n\t}\n\n\t/**\n\t * 限制解压后文件大小\n\t *\n\t * @param zipFile zip文件，附带编码信息，使用完毕自动关闭\n\t * @param outFile 解压到的目录\n\t * @param limit   限制解压文件大小(单位B)\n\t * @return 解压的目录\n\t * @throws IORuntimeException IO异常\n\t * @since 5.8.5\n\t */\n\tpublic static File unzip(ZipFile zipFile, File outFile, long limit) throws IORuntimeException {\n\t\tif (outFile.exists() && outFile.isFile()) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\tStrUtil.format(\"Target path [{}] exist!\", outFile.getAbsolutePath()));\n\t\t}\n\n\t\t// pr#726@Gitee\n\t\tif (limit > 0) {\n\t\t\tfinal Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();\n\t\t\tlong zipFileSize = 0L;\n\t\t\twhile (zipEntries.hasMoreElements()) {\n\t\t\t\tfinal ZipEntry zipEntry = zipEntries.nextElement();\n\t\t\t\tzipFileSize += zipEntry.getSize();\n\t\t\t\tif (zipFileSize > limit) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"The file size exceeds the limit\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry (final ZipReader reader = new ZipReader(zipFile)) {\n\t\t\treader.readTo(outFile);\n\t\t}\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 获取压缩包中的指定文件流\n\t *\n\t * @param zipFile 压缩文件\n\t * @param charset 编码\n\t * @param path    需要提取文件的文件名或路径\n\t * @return 压缩文件流，如果未找到返回{@code null}\n\t * @since 5.5.2\n\t */\n\tpublic static InputStream get(File zipFile, Charset charset, String path) {\n\t\treturn get(toZipFile(zipFile, charset), path);\n\t}\n\n\t/**\n\t * 获取压缩包中的指定文件流\n\t *\n\t * @param zipFile 压缩文件\n\t * @param path    需要提取文件的文件名或路径\n\t * @return 压缩文件流，如果未找到返回{@code null}\n\t * @since 5.5.2\n\t */\n\tpublic static InputStream get(ZipFile zipFile, String path) {\n\t\tfinal ZipEntry entry = zipFile.getEntry(path);\n\t\tif (null != entry) {\n\t\t\treturn getStream(zipFile, entry);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 读取并处理Zip文件中的每一个{@link ZipEntry}\n\t *\n\t * @param zipFile  Zip文件\n\t * @param consumer {@link ZipEntry}处理器\n\t * @since 5.5.2\n\t */\n\tpublic static void read(ZipFile zipFile, Consumer<ZipEntry> consumer) {\n\t\ttry (final ZipReader reader = new ZipReader(zipFile)) {\n\t\t\treader.read(consumer);\n\t\t}\n\t}\n\n\t/**\n\t * 解压<br>\n\t * ZIP条目不使用高速缓冲。\n\t *\n\t * @param in      zip文件流，使用完毕自动关闭\n\t * @param outFile 解压到的目录\n\t * @param charset 编码\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t * @since 4.5.8\n\t */\n\tpublic static File unzip(InputStream in, File outFile, Charset charset) throws UtilException {\n\t\tif (null == charset) {\n\t\t\tcharset = DEFAULT_CHARSET;\n\t\t}\n\t\treturn unzip(new ZipInputStream(in, charset), outFile);\n\t}\n\n\t/**\n\t * 解压<br>\n\t * ZIP条目不使用高速缓冲。\n\t *\n\t * @param zipStream zip文件流，包含编码信息\n\t * @param outFile   解压到的目录\n\t * @return 解压的目录\n\t * @throws UtilException IO异常\n\t * @since 4.5.8\n\t */\n\tpublic static File unzip(ZipInputStream zipStream, File outFile) throws UtilException {\n\t\ttry (final ZipReader reader = new ZipReader(zipStream)) {\n\t\t\treader.readTo(outFile);\n\t\t}\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 读取并处理Zip流中的每一个{@link ZipEntry}\n\t *\n\t * @param zipStream zip文件流，包含编码信息\n\t * @param consumer  {@link ZipEntry}处理器\n\t * @since 5.5.2\n\t */\n\tpublic static void read(ZipInputStream zipStream, Consumer<ZipEntry> consumer) {\n\t\ttry (final ZipReader reader = new ZipReader(zipStream)) {\n\t\t\treader.read(consumer);\n\t\t}\n\t}\n\n\t/**\n\t * 从Zip文件中提取指定的文件为bytes\n\t *\n\t * @param zipFilePath Zip文件\n\t * @param name        文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt\n\t * @return 文件内容bytes\n\t * @since 4.1.8\n\t */\n\tpublic static byte[] unzipFileBytes(String zipFilePath, String name) {\n\t\treturn unzipFileBytes(zipFilePath, DEFAULT_CHARSET, name);\n\t}\n\n\t/**\n\t * 从Zip文件中提取指定的文件为bytes\n\t *\n\t * @param zipFilePath Zip文件\n\t * @param charset     编码\n\t * @param name        文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt\n\t * @return 文件内容bytes\n\t * @since 4.1.8\n\t */\n\tpublic static byte[] unzipFileBytes(String zipFilePath, Charset charset, String name) {\n\t\treturn unzipFileBytes(FileUtil.file(zipFilePath), charset, name);\n\t}\n\n\t/**\n\t * 从Zip文件中提取指定的文件为bytes\n\t *\n\t * @param zipFile Zip文件\n\t * @param name    文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt\n\t * @return 文件内容bytes\n\t * @since 4.1.8\n\t */\n\tpublic static byte[] unzipFileBytes(File zipFile, String name) {\n\t\treturn unzipFileBytes(zipFile, DEFAULT_CHARSET, name);\n\t}\n\n\t/**\n\t * 从Zip文件中提取指定的文件为bytes\n\t *\n\t * @param zipFile Zip文件\n\t * @param charset 编码\n\t * @param name    文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt\n\t * @return 文件内容bytes\n\t * @since 4.1.8\n\t */\n\tpublic static byte[] unzipFileBytes(File zipFile, Charset charset, String name) {\n\t\ttry (final ZipReader reader = ZipReader.of(zipFile, charset)) {\n\t\t\treturn IoUtil.readBytes(reader.get(name));\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------- Gzip\n\n\t/**\n\t * Gzip压缩处理\n\t *\n\t * @param content 被压缩的字符串\n\t * @param charset 编码\n\t * @return 压缩后的字节流\n\t * @throws UtilException IO异常\n\t */\n\tpublic static byte[] gzip(String content, String charset) throws UtilException {\n\t\treturn gzip(StrUtil.bytes(content, charset));\n\t}\n\n\t/**\n\t * Gzip压缩处理\n\t *\n\t * @param buf 被压缩的字节流\n\t * @return 压缩后的字节流\n\t * @throws UtilException IO异常\n\t */\n\tpublic static byte[] gzip(byte[] buf) throws UtilException {\n\t\treturn gzip(new ByteArrayInputStream(buf), buf.length);\n\t}\n\n\t/**\n\t * Gzip压缩文件\n\t *\n\t * @param file 被压缩的文件\n\t * @return 压缩后的字节流\n\t * @throws UtilException IO异常\n\t */\n\tpublic static byte[] gzip(File file) throws UtilException {\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treturn gzip(in, (int) file.length());\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * Gzip压缩文件\n\t *\n\t * @param in 被压缩的流\n\t * @return 压缩后的字节流\n\t * @throws UtilException IO异常\n\t * @since 4.1.18\n\t */\n\tpublic static byte[] gzip(InputStream in) throws UtilException {\n\t\treturn gzip(in, DEFAULT_BYTE_ARRAY_LENGTH);\n\t}\n\n\t/**\n\t * Gzip压缩文件\n\t *\n\t * @param in     被压缩的流\n\t * @param length 预估长度\n\t * @return 压缩后的字节流\n\t * @throws UtilException IO异常\n\t * @since 4.1.18\n\t */\n\tpublic static byte[] gzip(InputStream in, int length) throws UtilException {\n\t\tfinal ByteArrayOutputStream bos = new ByteArrayOutputStream(length);\n\t\tGzip.of(in, bos).gzip().close();\n\t\treturn bos.toByteArray();\n\t}\n\n\t/**\n\t * Gzip解压缩处理\n\t *\n\t * @param buf     压缩过的字节流\n\t * @param charset 编码\n\t * @return 解压后的字符串\n\t * @throws UtilException IO异常\n\t */\n\tpublic static String unGzip(byte[] buf, String charset) throws UtilException {\n\t\treturn StrUtil.str(unGzip(buf), charset);\n\t}\n\n\t/**\n\t * Gzip解压处理\n\t *\n\t * @param buf buf\n\t * @return bytes\n\t * @throws UtilException IO异常\n\t */\n\tpublic static byte[] unGzip(byte[] buf) throws UtilException {\n\t\treturn unGzip(new ByteArrayInputStream(buf), buf.length);\n\t}\n\n\t/**\n\t * Gzip解压处理\n\t *\n\t * @param in Gzip数据\n\t * @return 解压后的数据\n\t * @throws UtilException IO异常\n\t */\n\tpublic static byte[] unGzip(InputStream in) throws UtilException {\n\t\treturn unGzip(in, DEFAULT_BYTE_ARRAY_LENGTH);\n\t}\n\n\t/**\n\t * Gzip解压处理\n\t *\n\t * @param in     Gzip数据\n\t * @param length 估算长度，如果无法确定请传入{@link #DEFAULT_BYTE_ARRAY_LENGTH}\n\t * @return 解压后的数据\n\t * @throws UtilException IO异常\n\t * @since 4.1.18\n\t */\n\tpublic static byte[] unGzip(InputStream in, int length) throws UtilException {\n\t\tFastByteArrayOutputStream bos = new FastByteArrayOutputStream(length);\n\t\tGzip.of(in, bos).unGzip().close();\n\t\treturn bos.toByteArray();\n\t}\n\n\t// ----------------------------------------------------------------------------- Zlib\n\n\t/**\n\t * Zlib压缩处理\n\t *\n\t * @param content 被压缩的字符串\n\t * @param charset 编码\n\t * @param level   压缩级别，1~9\n\t * @return 压缩后的字节流\n\t * @since 4.1.4\n\t */\n\tpublic static byte[] zlib(String content, String charset, int level) {\n\t\treturn zlib(StrUtil.bytes(content, charset), level);\n\t}\n\n\t/**\n\t * Zlib压缩文件\n\t *\n\t * @param file  被压缩的文件\n\t * @param level 压缩级别\n\t * @return 压缩后的字节流\n\t * @since 4.1.4\n\t */\n\tpublic static byte[] zlib(File file, int level) {\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treturn zlib(in, level, (int) file.length());\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 打成Zlib压缩包\n\t *\n\t * @param buf   数据\n\t * @param level 压缩级别，0~9\n\t * @return 压缩后的bytes\n\t * @since 4.1.4\n\t */\n\tpublic static byte[] zlib(byte[] buf, int level) {\n\t\treturn zlib(new ByteArrayInputStream(buf), level, buf.length);\n\t}\n\n\t/**\n\t * 打成Zlib压缩包\n\t *\n\t * @param in    数据流\n\t * @param level 压缩级别，0~9\n\t * @return 压缩后的bytes\n\t * @since 4.1.19\n\t */\n\tpublic static byte[] zlib(InputStream in, int level) {\n\t\treturn zlib(in, level, DEFAULT_BYTE_ARRAY_LENGTH);\n\t}\n\n\t/**\n\t * 打成Zlib压缩包\n\t *\n\t * @param in     数据流\n\t * @param level  压缩级别，0~9\n\t * @param length 预估大小\n\t * @return 压缩后的bytes\n\t * @since 4.1.19\n\t */\n\tpublic static byte[] zlib(InputStream in, int level, int length) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream(length);\n\t\tDeflate.of(in, out, false).deflater(level).close();\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * Zlib解压缩处理\n\t *\n\t * @param buf     压缩过的字节流\n\t * @param charset 编码\n\t * @return 解压后的字符串\n\t * @since 4.1.4\n\t */\n\tpublic static String unZlib(byte[] buf, String charset) {\n\t\treturn StrUtil.str(unZlib(buf), charset);\n\t}\n\n\t/**\n\t * 解压缩zlib\n\t *\n\t * @param buf 数据\n\t * @return 解压后的bytes\n\t * @since 4.1.4\n\t */\n\tpublic static byte[] unZlib(byte[] buf) {\n\t\treturn unZlib(new ByteArrayInputStream(buf), buf.length);\n\t}\n\n\t/**\n\t * 解压缩zlib\n\t *\n\t * @param in 数据流\n\t * @return 解压后的bytes\n\t * @since 4.1.19\n\t */\n\tpublic static byte[] unZlib(InputStream in) {\n\t\treturn unZlib(in, DEFAULT_BYTE_ARRAY_LENGTH);\n\t}\n\n\t/**\n\t * 解压缩zlib\n\t *\n\t * @param in     数据流\n\t * @param length 预估长度\n\t * @return 解压后的bytes\n\t * @since 4.1.19\n\t */\n\tpublic static byte[] unZlib(InputStream in, int length) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream(length);\n\t\tDeflate.of(in, out, false).inflater().close();\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 获取Zip文件中指定目录下的所有文件，只显示文件，不显示目录<br>\n\t * 此方法并不会关闭{@link ZipFile}。\n\t *\n\t * @param zipFile Zip文件\n\t * @param dir     目录前缀（目录前缀不包含开头的/）\n\t * @return 文件列表\n\t * @since 4.6.6\n\t */\n\tpublic static List<String> listFileNames(ZipFile zipFile, String dir) {\n\t\tif (StrUtil.isNotBlank(dir)) {\n\t\t\t// 目录尾部添加\"/\"\n\t\t\tdir = StrUtil.addSuffixIfNot(dir, StrUtil.SLASH);\n\t\t}\n\n\t\tfinal List<String> fileNames = new ArrayList<>();\n\t\tString name;\n\t\tfor (ZipEntry entry : new EnumerationIter<>(zipFile.entries())) {\n\t\t\tname = entry.getName();\n\t\t\tif (StrUtil.isEmpty(dir) || name.startsWith(dir)) {\n\t\t\t\tfinal String nameSuffix = StrUtil.removePrefix(name, dir);\n\t\t\t\tif (StrUtil.isNotEmpty(nameSuffix) && false == StrUtil.contains(nameSuffix, CharUtil.SLASH)) {\n\t\t\t\t\tfileNames.add(nameSuffix);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn fileNames;\n\t}\n\n\t// ---------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 判断压缩文件保存的路径是否为源文件路径的子文件夹，如果是，则抛出异常（防止无限递归压缩的发生）\n\t *\n\t * @param zipFile  压缩后的产生的文件路径\n\t * @param srcFiles 被压缩的文件或目录\n\t */\n\tprivate static void validateFiles(File zipFile, File... srcFiles) throws UtilException {\n\t\tif (zipFile.isDirectory()) {\n\t\t\tthrow new UtilException(\"Zip file [{}] must not be a directory !\", zipFile.getAbsoluteFile());\n\t\t}\n\n\t\tfor (File srcFile : srcFiles) {\n\t\t\tif (null == srcFile) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (false == srcFile.exists()) {\n\t\t\t\tthrow new UtilException(StrUtil.format(\"File [{}] not exist!\", srcFile.getAbsolutePath()));\n\t\t\t}\n\n\t\t\t// issue#1961@Github\n\t\t\t// 当 zipFile =  new File(\"temp.zip\") 时, zipFile.getParentFile() == null\n\t\t\tFile parentFile;\n\t\t\ttry {\n\t\t\t\tparentFile = zipFile.getCanonicalFile().getParentFile();\n\t\t\t} catch (IOException e) {\n\t\t\t\tparentFile = zipFile.getParentFile();\n\t\t\t}\n\n\t\t\t// 压缩文件不能位于被压缩的目录内\n\t\t\tif (srcFile.isDirectory() && FileUtil.isSub(srcFile, parentFile)) {\n\t\t\t\tthrow new UtilException(\"Zip file path [{}] must not be the child directory of [{}] !\", zipFile.getPath(), srcFile.getPath());\n\t\t\t}\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------------------------- Private method end\n\n}\n"
  },
  {
    "path": "hutool-core/src/main/java/cn/hutool/core/util/package-info.java",
    "content": "/**\n * 提供各种工具方法，按照归类入口为XXXUtil，如字符串工具StrUtil等\n *\n * @author looly\n *\n */\npackage cn.hutool.core.util;"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AbstractWrappedAnnotationAttributeTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\n\npublic class AbstractWrappedAnnotationAttributeTest {\n\n\t@Test\n\tpublic void workTest() {\n\t\tAnnotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest1.class);\n\t\tMethod valueMethod = ReflectUtil.getMethod(AnnotationForTest1.class, \"value1\");\n\t\tCacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tMethod nameMethod = ReflectUtil.getMethod(AnnotationForTest1.class, \"name1\");\n\t\tCacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tAbstractWrappedAnnotationAttribute nameWrapper = new TestWrappedAnnotationAttribute(nameAttribute, valueAttribute);\n\n\t\tassertEquals(nameWrapper.getAnnotation(), annotation);\n\n\t\t// 注解属性\n\t\tassertEquals(annotation, nameWrapper.getAnnotation());\n\t\tassertEquals(annotation.annotationType(), nameWrapper.getAnnotationType());\n\t\tassertEquals(nameAttribute, nameWrapper.getOriginal());\n\t\tassertEquals(valueAttribute, nameWrapper.getLinked());\n\n\t\t// 方法属性\n\t\tassertEquals(nameMethod.getName(), nameWrapper.getAttributeName());\n\t\tassertEquals(nameMethod.getReturnType(), nameWrapper.getAttributeType());\n\t\tassertTrue(nameWrapper.isWrapped());\n\t\tassertEquals(\"value1\", nameWrapper.getValue());\n\t}\n\n\t@Test\n\tpublic void multiWrapperTest() {\n\t\t// 包装第一层： name1 + value1\n\t\tAnnotation annotation1 = ClassForTest1.class.getAnnotation(AnnotationForTest1.class);\n\t\tMethod value1Method = ReflectUtil.getMethod(AnnotationForTest1.class, \"value1\");\n\t\tCacheableAnnotationAttribute value1Attribute = new CacheableAnnotationAttribute(annotation1, value1Method);\n\t\tMethod name1Method = ReflectUtil.getMethod(AnnotationForTest1.class, \"name1\");\n\t\tCacheableAnnotationAttribute name1Attribute = new CacheableAnnotationAttribute(annotation1, name1Method);\n\t\tAbstractWrappedAnnotationAttribute wrapper1 = new TestWrappedAnnotationAttribute(name1Attribute, value1Attribute);\n\t\tassertEquals(name1Attribute, wrapper1.getNonWrappedOriginal());\n\t\tassertEquals(CollUtil.newArrayList(name1Attribute, value1Attribute), wrapper1.getAllLinkedNonWrappedAttributes());\n\n\t\t// 包装第二层：( name1 + value1 ) + value2\n\t\tAnnotation annotation2 = ClassForTest1.class.getAnnotation(AnnotationForTest2.class);\n\t\tMethod value2Method = ReflectUtil.getMethod(AnnotationForTest2.class, \"value2\");\n\t\tCacheableAnnotationAttribute value2Attribute = new CacheableAnnotationAttribute(annotation2, value2Method);\n\t\tAbstractWrappedAnnotationAttribute wrapper2 = new TestWrappedAnnotationAttribute(wrapper1, value2Attribute);\n\t\tassertEquals(name1Attribute, wrapper2.getNonWrappedOriginal());\n\t\tassertEquals(CollUtil.newArrayList(name1Attribute, value1Attribute, value2Attribute), wrapper2.getAllLinkedNonWrappedAttributes());\n\n\t\t// 包装第二层：value3 + ( ( name1 + value1 ) + value2 )\n\t\tAnnotation annotation3 = ClassForTest1.class.getAnnotation(AnnotationForTest3.class);\n\t\tMethod value3Method = ReflectUtil.getMethod(AnnotationForTest3.class, \"value3\");\n\t\tCacheableAnnotationAttribute value3Attribute = new CacheableAnnotationAttribute(annotation3, value3Method);\n\t\tAbstractWrappedAnnotationAttribute wrapper3 = new TestWrappedAnnotationAttribute(value3Attribute, wrapper2);\n\t\tassertEquals(value3Attribute, wrapper3.getNonWrappedOriginal());\n\t\tassertEquals(CollUtil.newArrayList(value3Attribute, name1Attribute, value1Attribute, value2Attribute), wrapper3.getAllLinkedNonWrappedAttributes());\n\n\t}\n\n\tstatic class TestWrappedAnnotationAttribute extends AbstractWrappedAnnotationAttribute {\n\t\tprotected TestWrappedAnnotationAttribute(AnnotationAttribute original, AnnotationAttribute linked) {\n\t\t\tsuper(original, linked);\n\t\t}\n\t\t@Override\n\t\tpublic Object getValue() {\n\t\t\treturn linked.getValue();\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean isValueEquivalentToDefaultValue() {\n\t\t\treturn getOriginal().isValueEquivalentToDefaultValue() && getLinked().isValueEquivalentToDefaultValue();\n\t\t}\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest1 {\n\t\tString value1() default \"\";\n\t\tString name1() default \"\";\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest2 {\n\t\tString value2() default \"\";\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest3 {\n\t\tString value3() default \"\";\n\t}\n\n\t@AnnotationForTest1(name1 = \"name1\", value1 = \"value1\")\n\t@AnnotationForTest2(value2 = \"value2\")\n\t@AnnotationForTest3(value3 = \"value3\")\n\tstatic class ClassForTest1 {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AliasAnnotationPostProcessorTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\npublic class AliasAnnotationPostProcessorTest {\n\n\t@Test\n\tpublic void processTest() {\n\t\tAliasAnnotationPostProcessor processor = new AliasAnnotationPostProcessor();\n\n\t\tMap<Class<?>, SynthesizedAnnotation> annotationMap = new HashMap<>();\n\t\tSynthesizedAggregateAnnotation synthesizedAnnotationAggregator = new TestSynthesizedAggregateAnnotation(annotationMap);\n\t\tAnnotationForTest annotation = ClassForTest.class.getAnnotation(AnnotationForTest.class);\n\t\tSynthesizedAnnotation synthesizedAnnotation = new TestSynthesizedAnnotation(synthesizedAnnotationAggregator, annotation);\n\t\tannotationMap.put(annotation.annotationType(), synthesizedAnnotation);\n\n\t\tprocessor.process(synthesizedAnnotation, synthesizedAnnotationAggregator);\n\t\tAnnotationAttribute valueAttribute = synthesizedAnnotation.getAttributes().get(\"value\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"value\"), valueAttribute.getAttribute());\n\t\tassertTrue(valueAttribute.isWrapped());\n\t\tassertEquals(ForceAliasedAnnotationAttribute.class, valueAttribute.getClass());\n\n\t\tAnnotationAttribute nameAttribute = synthesizedAnnotation.getAttributes().get(\"name\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"name\"), nameAttribute.getAttribute());\n\t\tassertFalse(nameAttribute.isWrapped());\n\t\tassertEquals(CacheableAnnotationAttribute.class, nameAttribute.getClass());\n\n\t\tassertEquals(nameAttribute, ((WrappedAnnotationAttribute)valueAttribute).getLinked());\n\t}\n\n\t@AnnotationForTest\n\tstatic class ClassForTest {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\t@Alias(\"name\")\n\t\tString value() default \"\";\n\t\tString name() default \"\";\n\t}\n\n\tstatic class TestSynthesizedAggregateAnnotation implements SynthesizedAggregateAnnotation {\n\n\t\tprivate final Map<Class<?>, SynthesizedAnnotation> annotationMap;\n\n\t\tpublic TestSynthesizedAggregateAnnotation(Map<Class<?>, SynthesizedAnnotation> annotationMap) {\n\t\t\tthis.annotationMap = annotationMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getSource() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationSelector getAnnotationSelector() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Collection<SynthesizedAnnotationPostProcessor> getAnnotationPostProcessors() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotation getSynthesizedAnnotation(Class<?> annotationType) {\n\t\t\treturn annotationMap.get(annotationType);\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<Class<? extends Annotation>, SynthesizedAnnotation> getAllSynthesizedAnnotation() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation[] getAnnotations() {\n\t\t\treturn new Annotation[0];\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T synthesize(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tstatic class TestSynthesizedAnnotation implements SynthesizedAnnotation {\n\n\t\tprivate final Annotation annotation;\n\t\tprivate final SynthesizedAggregateAnnotation owner;\n\t\tprivate final Map<String, AnnotationAttribute> attributeMap;\n\n\t\tpublic TestSynthesizedAnnotation(SynthesizedAggregateAnnotation owner, Annotation annotation) {\n\t\t\tthis.owner = owner;\n\t\t\tthis.attributeMap = new HashMap<>();\n\t\t\tthis.annotation = annotation;\n\t\t\tfor (Method declaredMethod : annotation.annotationType().getDeclaredMethods()) {\n\t\t\t\tattributeMap.put(declaredMethod.getName(), new CacheableAnnotationAttribute(annotation, declaredMethod));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation getAnnotation() {\n\t\t\treturn annotation;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getVerticalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getHorizontalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\t\treturn attributeMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\t\t\tattributeMap.put(attributeName, attribute);\n\t\t}\n\n\t\t@Override\n\t\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\t\t\tAnnotationAttribute annotationAttribute = attributeMap.get(attributeName);\n\t\t\tif (ObjectUtil.isNotNull(annotationAttribute)) {\n\t\t\t\tattributeMap.put(attributeName, operator.apply(annotationAttribute));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Class<? extends Annotation> annotationType() {\n\t\t\treturn annotation.annotationType();\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AliasLinkAnnotationPostProcessorTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\npublic class AliasLinkAnnotationPostProcessorTest {\n\n\t@Test\n\tpublic void processForceAliasForTest() {\n\t\tAliasLinkAnnotationPostProcessor processor = new AliasLinkAnnotationPostProcessor();\n\n\t\tMap<Class<?>, SynthesizedAnnotation> annotationMap = new HashMap<>();\n\t\tSynthesizedAggregateAnnotation synthesizedAnnotationAggregator = new TestSynthesizedAggregateAnnotation(annotationMap);\n\t\tAnnotationForTest annotation = ClassForTest.class.getAnnotation(AnnotationForTest.class);\n\t\tSynthesizedAnnotation synthesizedAnnotation = new TestSynthesizedAnnotation(synthesizedAnnotationAggregator, annotation);\n\t\tannotationMap.put(annotation.annotationType(), synthesizedAnnotation);\n\n\t\tprocessor.process(synthesizedAnnotation, synthesizedAnnotationAggregator);\n\t\tAnnotationAttribute valueAttribute = synthesizedAnnotation.getAttributes().get(\"value\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"value\"), valueAttribute.getAttribute());\n\t\tassertFalse(valueAttribute.isWrapped());\n\t\tassertEquals(CacheableAnnotationAttribute.class, valueAttribute.getClass());\n\n\t\tAnnotationAttribute nameAttribute = synthesizedAnnotation.getAttributes().get(\"name\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"name\"), nameAttribute.getAttribute());\n\t\tassertTrue(nameAttribute.isWrapped());\n\t\tassertEquals(ForceAliasedAnnotationAttribute.class, nameAttribute.getClass());\n\n\t\tassertEquals(valueAttribute, ((WrappedAnnotationAttribute)nameAttribute).getLinked());\n\t}\n\n\t@Test\n\tpublic void processAliasForTest() {\n\t\tAliasLinkAnnotationPostProcessor processor = new AliasLinkAnnotationPostProcessor();\n\n\t\tMap<Class<?>, SynthesizedAnnotation> annotationMap = new HashMap<>();\n\t\tSynthesizedAggregateAnnotation synthesizedAnnotationAggregator = new TestSynthesizedAggregateAnnotation(annotationMap);\n\t\tAnnotationForTest annotation = ClassForTest.class.getAnnotation(AnnotationForTest.class);\n\t\tSynthesizedAnnotation synthesizedAnnotation = new TestSynthesizedAnnotation(synthesizedAnnotationAggregator, annotation);\n\t\tannotationMap.put(annotation.annotationType(), synthesizedAnnotation);\n\n\t\tprocessor.process(synthesizedAnnotation, synthesizedAnnotationAggregator);\n\t\tAnnotationAttribute valueAttribute = synthesizedAnnotation.getAttributes().get(\"value2\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"value2\"), valueAttribute.getAttribute());\n\t\tassertFalse(valueAttribute.isWrapped());\n\t\tassertEquals(CacheableAnnotationAttribute.class, valueAttribute.getClass());\n\n\t\tAnnotationAttribute nameAttribute = synthesizedAnnotation.getAttributes().get(\"name2\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"name2\"), nameAttribute.getAttribute());\n\t\tassertTrue(nameAttribute.isWrapped());\n\t\tassertEquals(AliasedAnnotationAttribute.class, nameAttribute.getClass());\n\n\t\tassertEquals(valueAttribute, ((WrappedAnnotationAttribute)nameAttribute).getLinked());\n\t}\n\n\t@AnnotationForTest\n\tstatic class ClassForTest {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\t@Link(attribute = \"name\", type = RelationType.FORCE_ALIAS_FOR)\n\t\tString value() default \"\";\n\t\tString name() default \"\";\n\n\t\t@Link(attribute = \"name2\", type = RelationType.ALIAS_FOR)\n\t\tString value2() default \"\";\n\t\tString name2() default \"\";\n\t}\n\n\tstatic class TestSynthesizedAggregateAnnotation implements SynthesizedAggregateAnnotation {\n\n\t\tprivate final Map<Class<?>, SynthesizedAnnotation> annotationMap;\n\n\t\tpublic TestSynthesizedAggregateAnnotation(Map<Class<?>, SynthesizedAnnotation> annotationMap) {\n\t\t\tthis.annotationMap = annotationMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getSource() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationSelector getAnnotationSelector() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Collection<SynthesizedAnnotationPostProcessor> getAnnotationPostProcessors() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotation getSynthesizedAnnotation(Class<?> annotationType) {\n\t\t\treturn annotationMap.get(annotationType);\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<Class<? extends Annotation>, SynthesizedAnnotation> getAllSynthesizedAnnotation() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation[] getAnnotations() {\n\t\t\treturn new Annotation[0];\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T synthesize(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tstatic class TestSynthesizedAnnotation implements SynthesizedAnnotation {\n\n\t\tprivate final Annotation annotation;\n\t\tprivate final SynthesizedAggregateAnnotation owner;\n\t\tprivate final Map<String, AnnotationAttribute> attributeMap;\n\n\t\tpublic TestSynthesizedAnnotation(SynthesizedAggregateAnnotation owner, Annotation annotation) {\n\t\t\tthis.owner = owner;\n\t\t\tthis.attributeMap = new HashMap<>();\n\t\t\tthis.annotation = annotation;\n\t\t\tfor (Method declaredMethod : annotation.annotationType().getDeclaredMethods()) {\n\t\t\t\tattributeMap.put(declaredMethod.getName(), new CacheableAnnotationAttribute(annotation, declaredMethod));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation getAnnotation() {\n\t\t\treturn annotation;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getVerticalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getHorizontalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\t\treturn attributeMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\t\t\tattributeMap.put(attributeName, attribute);\n\t\t}\n\n\t\t@Override\n\t\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\t\t\tAnnotationAttribute annotationAttribute = attributeMap.get(attributeName);\n\t\t\tif (ObjectUtil.isNotNull(annotationAttribute)) {\n\t\t\t\tattributeMap.put(attributeName, operator.apply(annotationAttribute));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Class<? extends Annotation> annotationType() {\n\t\t\treturn annotation.annotationType();\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AliasedAnnotationAttributeTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\n\npublic class AliasedAnnotationAttributeTest {\n\n\t@Test\n\tpublic void baseInfoTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal AliasedAnnotationAttribute valueAnnotationAttribute = new AliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 注解属性\n\t\tassertEquals(annotation, valueAnnotationAttribute.getAnnotation());\n\t\tassertEquals(annotation.annotationType(), valueAnnotationAttribute.getAnnotationType());\n\n\t\t// 方法属性\n\t\tassertEquals(valueMethod.getAnnotation(Alias.class), valueAnnotationAttribute.getAnnotation(Alias.class));\n\t\tassertEquals(valueMethod.getName(), valueAnnotationAttribute.getAttributeName());\n\t\tassertEquals(nameMethod.getReturnType(), valueAnnotationAttribute.getAttributeType());\n\t}\n\n\t@Test\n\tpublic void workWhenValueDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal AliasedAnnotationAttribute annotationAttribute = new AliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"name\", annotationAttribute.getValue());\n\t\tassertFalse(annotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(annotationAttribute.isWrapped());\n\t}\n\n\t@Test\n\tpublic void workWhenValueNonDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest2.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal AliasedAnnotationAttribute annotationAttribute = new AliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"value\", annotationAttribute.getValue());\n\t\tassertFalse(annotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(annotationAttribute.isWrapped());\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\t@Alias(\"value\")\n\t\tString value() default \"\";\n\t\tString name() default \"\";\n\t}\n\n\t@AnnotationForTest(name = \"name\", value = \"value\")\n\tstatic class ClassForTest1 {}\n\n\t@AnnotationForTest(value = \"value\")\n\tstatic class ClassForTest2 {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 用于单元测试的注解类<br>\n * 注解类相关说明见：<a href=\"https://www.cnblogs.com/xdp-gacl/p/3622275.html\">https://www.cnblogs.com/xdp-gacl/p/3622275.html</a>\n *\n * @author looly\n */\n// Retention注解决定MyAnnotation注解的生命周期\n@Retention(RetentionPolicy.RUNTIME)\n// Target注解决定MyAnnotation注解可以加在哪些成分上，如加在类身上，或者属性身上，或者方法身上等成分\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface AnnotationForTest {\n\n\t/**\n\t * 注解的默认属性值\n\t *\n\t * @return 属性值\n\t */\n\tString value() default \"\";\n\n\t@Alias(\"value\")\n\tString retry() default \"\";\n\n\tString[] names() default \"\";\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class AnnotationUtilTest {\n\n\t@Test\n\tpublic void getCombinationAnnotationsTest(){\n\t\tfinal Annotation[] annotations = AnnotationUtil.getAnnotations(ClassWithAnnotation.class, true);\n\t\tassertNotNull(annotations);\n\t\tassertEquals(2, annotations.length);\n\t}\n\n\t@Test\n\tpublic void getCombinationAnnotationsWithClassTest(){\n\t\tfinal AnnotationForTest[] annotations = AnnotationUtil.getCombinationAnnotations(ClassWithAnnotation.class, AnnotationForTest.class);\n\t\tassertNotNull(annotations);\n\t\tassertEquals(1, annotations.length);\n\t\tassertTrue(annotations[0].value().equals(\"测试\") || annotations[0].value().equals(\"repeat-annotation\"));\n\t}\n\n\t@Test\n\tpublic void getAnnotationValueTest() {\n\t\tfinal Object value = AnnotationUtil.getAnnotationValue(ClassWithAnnotation.class, AnnotationForTest.class);\n\t\tassertTrue(value.equals(\"测试\") || value.equals(\"repeat-annotation\"));\n\n\t}\n\n\t@Test\n\tpublic void getAnnotationValueTest2() {\n\t\tfinal String[] names = AnnotationUtil.getAnnotationValue(ClassWithAnnotation.class, AnnotationForTest::names);\n\t\tassertTrue(names.length == 1 && names[0].isEmpty() || ArrayUtil.equals(names, new String[]{\"测试1\", \"测试2\"}));\n\t}\n\n\t@Test\n\tpublic void getAnnotationSyncAlias() {\n\t\t// 直接获取\n\t\tassertEquals(\"\", ClassWithAnnotation.class.getAnnotation(AnnotationForTest.class).retry());\n\n\t\t// 加别名适配\n\t\tfinal AnnotationForTest annotation = AnnotationUtil.getAnnotationAlias(ClassWithAnnotation.class, AnnotationForTest.class);\n\t\tString retryValue = annotation.retry();\n\t\tassertTrue(retryValue.equals(\"测试\") || retryValue.equals(\"repeat-annotation\"));\n\t\tassertTrue(AnnotationUtil.isSynthesizedAnnotation(annotation));\n\t}\n\n\t@Test\n\tpublic void getAnnotationSyncAliasWhenNotAnnotation() {\n\t\tgetAnnotationSyncAlias();\n\t\t// 使用AnnotationUtil.getAnnotationAlias获取对象上并不存在的注解\n\t\tfinal Alias alias = AnnotationUtil.getAnnotationAlias(ClassWithAnnotation.class, Alias.class);\n\t\tassertNull(alias);\n\t}\n\n\t@AnnotationForTest(value = \"测试\", names = {\"测试1\", \"测试2\"})\n\t@RepeatAnnotationForTest\n\tstatic class ClassWithAnnotation{\n\t\tpublic void test(){\n\n\t\t}\n\t}\n\n\t@Test\n\tpublic void scanMetaAnnotationTest() {\n\t\t// RootAnnotation -> RootMetaAnnotation1 -> RootMetaAnnotation2 -> RootMetaAnnotation3\n\t\t//                -> RootMetaAnnotation3\n\t\tfinal List<Annotation> annotations = AnnotationUtil.scanMetaAnnotation(RootAnnotation.class);\n\t\tassertEquals(4, annotations.size());\n\t\tassertTrue(annotations.get(0).annotationType() == RootMetaAnnotation3.class ||\n\t\t\t\tannotations.get(0).annotationType() == RootMetaAnnotation1.class);\n\t\tassertTrue(annotations.get(1).annotationType() == RootMetaAnnotation1.class ||\n\t\t\t\tannotations.get(1).annotationType() == RootMetaAnnotation2.class);\n\t\tassertTrue(annotations.get(2).annotationType() == RootMetaAnnotation2.class ||\n\t\t\t\tannotations.get(2).annotationType() == RootMetaAnnotation3.class);\n\t\tassertEquals(RootMetaAnnotation3.class, annotations.get(3).annotationType());\n\t}\n\n\t@Test\n\tpublic void scanClassTest() {\n\t\t// TargetClass -> TargetSuperClass ----------------------------------> SuperInterface\n\t\t//             -> TargetSuperInterface -> SuperTargetSuperInterface -> SuperInterface\n\t\tfinal List<Annotation> annotations = AnnotationUtil.scanClass(TargetClass.class);\n\t\tassertEquals(5, annotations.size());\n\t\tassertEquals(\"TargetClass\", ((AnnotationForTest)annotations.get(0)).value());\n\t\tassertEquals(\"TargetSuperClass\", ((AnnotationForTest)annotations.get(1)).value());\n\t\tassertEquals(\"TargetSuperInterface\", ((AnnotationForTest)annotations.get(2)).value());\n\t\tassertEquals(\"SuperInterface\", ((AnnotationForTest)annotations.get(3)).value());\n\t\tassertEquals(\"SuperTargetSuperInterface\", ((AnnotationForTest)annotations.get(4)).value());\n\t}\n\n\t@Test\n\tpublic void scanMethodTest() {\n\t\t// TargetClass -> TargetSuperClass\n\t\t//             -> TargetSuperInterface\n\t\tfinal Method method = ReflectUtil.getMethod(TargetClass.class, \"testMethod\");\n\t\tassertNotNull(method);\n\t\tfinal List<Annotation> annotations = AnnotationUtil.scanMethod(method);\n\t\tassertEquals(3, annotations.size());\n\t\tassertEquals(\"TargetClass\", ((AnnotationForTest)annotations.get(0)).value());\n\t\tassertEquals(\"TargetSuperClass\", ((AnnotationForTest)annotations.get(1)).value());\n\t\tassertEquals(\"TargetSuperInterface\", ((AnnotationForTest)annotations.get(2)).value());\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\tpublic @interface RootMetaAnnotation3 {}\n\n\t@RootMetaAnnotation3\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target(ElementType.ANNOTATION_TYPE)\n\tpublic @interface RootMetaAnnotation2 {}\n\n\t@RootMetaAnnotation2\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target(ElementType.ANNOTATION_TYPE)\n\tpublic @interface RootMetaAnnotation1 {}\n\n\t@RootMetaAnnotation3\n\t@RootMetaAnnotation1\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target(ElementType.TYPE_USE)\n\tpublic @interface RootAnnotation {}\n\n\t@AnnotationForTest(\"TargetClass\")\n\tstatic class TargetClass extends TargetSuperClass implements TargetSuperInterface {\n\n\t\t@Override\n\t\t@AnnotationForTest(\"TargetClass\")\n\t\tpublic List<?> testMethod() { return Collections.emptyList(); }\n\n\t}\n\n\t@AnnotationForTest(\"TargetSuperClass\")\n\tstatic class TargetSuperClass implements SuperInterface {\n\n\t\t@AnnotationForTest(\"TargetSuperClass\")\n\t\tpublic Collection<?> testMethod() { return Collections.emptyList(); }\n\n\t}\n\n\t@AnnotationForTest(\"TargetSuperInterface\")\n\tinterface TargetSuperInterface extends SuperTargetSuperInterface {\n\n\t\t@AnnotationForTest(\"TargetSuperInterface\")\n\t\tObject testMethod();\n\n\t}\n\n\t@AnnotationForTest(\"SuperTargetSuperInterface\")\n\tinterface SuperTargetSuperInterface extends SuperInterface{}\n\n\t@AnnotationForTest(\"SuperInterface\")\n\tinterface SuperInterface{}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/CacheableAnnotationAttributeTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\n\npublic class CacheableAnnotationAttributeTest {\n\n\t@Test\n\tpublic void baseInfoTest() {\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method attribute = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute annotationAttribute = new CacheableAnnotationAttribute(annotation, attribute);\n\t\t// 注解属性\n\t\tassertEquals(annotation, annotationAttribute.getAnnotation());\n\t\tassertEquals(annotation.annotationType(), annotationAttribute.getAnnotationType());\n\t\t// 方法属性\n\t\tassertEquals(attribute.getName(), annotationAttribute.getAttributeName());\n\t\tassertEquals(attribute.getReturnType(), annotationAttribute.getAttributeType());\n\t}\n\n\t@Test\n\tpublic void workWhenValueDefaultTest() {\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method attribute = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute annotationAttribute = new CacheableAnnotationAttribute(annotation, attribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"\", annotationAttribute.getValue());\n\t\tassertTrue(annotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertFalse(annotationAttribute.isWrapped());\n\t}\n\n\t@Test\n\tpublic void workWhenValueNonDefaultTest() {\n\t\tfinal Annotation annotation = ClassForTest2.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method attribute = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute annotationAttribute = new CacheableAnnotationAttribute(annotation, attribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"test\", annotationAttribute.getValue());\n\t\tassertFalse(annotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertFalse(annotationAttribute.isWrapped());\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\tString value() default \"\";\n\t}\n\n\t@AnnotationForTest(\"\")\n\tstatic class ClassForTest1 {}\n\n\t@AnnotationForTest(\"test\")\n\tstatic class ClassForTest2 {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/CacheableSynthesizedAnnotationAttributeProcessorTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.map.MapBuilder;\nimport cn.hutool.core.util.ClassUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\npublic class CacheableSynthesizedAnnotationAttributeProcessorTest {\n\n\t@Test\n\tpublic void getAttributeValueTest() {\n\t\tCacheableSynthesizedAnnotationAttributeProcessor processor = new CacheableSynthesizedAnnotationAttributeProcessor();\n\n\t\tMap<String, Object> values1 = MapBuilder.<String, Object> create().put(\"name\", \"name1\").put(\"value\", 111).build();\n\t\tSynthesizedAnnotation annotation1 = new TestSynthesizedAnnotation(1, 0, values1);\n\t\tMap<String, Object> values2 = MapBuilder.<String, Object> create().put(\"name\", \"name2\").put(\"value\", \"value2\").build();\n\t\tSynthesizedAnnotation annotation2 = new TestSynthesizedAnnotation(0, 0, values2);\n\n\t\tassertEquals(\"name2\", processor.getAttributeValue(\"name\", String.class, Arrays.asList(annotation1, annotation2)));\n\t\tassertEquals(Integer.valueOf(111), processor.getAttributeValue(\"value\", Integer.class, Arrays.asList(annotation1, annotation2)));\n\t}\n\n\tstatic class TestSynthesizedAnnotation implements SynthesizedAnnotation {\n\n\t\tprivate final int verticalDistance;\n\t\tprivate final int horizontalDistance;\n\t\tprivate final Map<String, Object> value;\n\n\t\tpublic TestSynthesizedAnnotation(int verticalDistance, int horizontalDistance, Map<String, Object> value) {\n\t\t\tthis.verticalDistance = verticalDistance;\n\t\t\tthis.horizontalDistance = horizontalDistance;\n\t\t\tthis.value = value;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation getAnnotation() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getVerticalDistance() {\n\t\t\treturn verticalDistance;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getHorizontalDistance() {\n\t\t\treturn horizontalDistance;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\t\treturn Opt.ofNullable(value.get(attributeName))\n\t\t\t\t.map(t -> ClassUtil.isAssignable(returnType, t.getClass()))\n\t\t\t\t.orElse(false);\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName) {\n\t\t\treturn value.get(attributeName);\n\t\t}\n\n\t\t@Override\n\t\tpublic Class<? extends Annotation> annotationType() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/ForceAliasedAnnotationAttributeTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\n\npublic class ForceAliasedAnnotationAttributeTest {\n\n\t@Test\n\tpublic void baseInfoTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal ForceAliasedAnnotationAttribute valueAnnotationAttribute = new ForceAliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 注解属性\n\t\tassertEquals(annotation, valueAnnotationAttribute.getAnnotation());\n\t\tassertEquals(annotation.annotationType(), valueAnnotationAttribute.getAnnotationType());\n\n\t\t// 方法属性\n\t\tassertEquals(valueMethod.getName(), valueAnnotationAttribute.getAttributeName());\n\t\tassertEquals(valueMethod.getReturnType(), valueAnnotationAttribute.getAttributeType());\n\t}\n\n\t@Test\n\tpublic void workWhenValueDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal AliasedAnnotationAttribute valueAnnotationAttribute = new AliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"name\", valueAnnotationAttribute.getValue());\n\t\tassertFalse(valueAnnotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(valueAnnotationAttribute.isWrapped());\n\t}\n\n\t@Test\n\tpublic void workWhenValueNonDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest2.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal ForceAliasedAnnotationAttribute valueAnnotationAttribute = new ForceAliasedAnnotationAttribute(valueAttribute, nameAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"\", valueAnnotationAttribute.getValue());\n\t\tassertTrue(valueAnnotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(valueAnnotationAttribute.isWrapped());\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\tString value() default \"\";\n\t\tString name() default \"\";\n\t}\n\n\t@AnnotationForTest(name = \"name\", value = \"value\")\n\tstatic class ClassForTest1 {}\n\n\t@AnnotationForTest(value = \"value\")\n\tstatic class ClassForTest2 {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/GenericSynthesizedAggregateAnnotationTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 合成注解{@link GenericSynthesizedAggregateAnnotation}的测试用例\n *\n * @author huangchengxing\n */\npublic class GenericSynthesizedAggregateAnnotationTest {\n\n\t@Test\n\tpublic void baseSynthesisAnnotationWorkTest() {\n\t\t// AnnotatedClass -> @ChildAnnotation -> @ParentAnnotation -> @GrandParentAnnotation\n\t\t//                                    -> @GrandParentAnnotation\n\t\tfinal GrandParentAnnotation grandParentAnnotation = ChildAnnotation.class.getAnnotation(GrandParentAnnotation.class);\n\t\tfinal ParentAnnotation parentAnnotation = ChildAnnotation.class.getAnnotation(ParentAnnotation.class);\n\t\tfinal ChildAnnotation childAnnotation = AnnotatedClass.class.getAnnotation(ChildAnnotation.class);\n\t\tfinal GenericSynthesizedAggregateAnnotation syntheticMetaAnnotation = new GenericSynthesizedAggregateAnnotation(childAnnotation);\n\n\t\t// Annotation & AnnotatedElement\n\t\tassertEquals(GenericSynthesizedAggregateAnnotation.class, syntheticMetaAnnotation.annotationType());\n\t\tassertTrue(syntheticMetaAnnotation.isAnnotationPresent(GrandParentAnnotation.class));\n\t\tassertTrue(syntheticMetaAnnotation.isAnnotationPresent(ParentAnnotation.class));\n\t\tassertTrue(syntheticMetaAnnotation.isAnnotationPresent(ChildAnnotation.class));\n\t\tassertEquals(grandParentAnnotation, syntheticMetaAnnotation.getAnnotation(GrandParentAnnotation.class));\n\t\tassertEquals(parentAnnotation, syntheticMetaAnnotation.getAnnotation(ParentAnnotation.class));\n\t\tassertEquals(childAnnotation, syntheticMetaAnnotation.getAnnotation(ChildAnnotation.class));\n\t\tAnnotation[] synthesizedAnnotations = syntheticMetaAnnotation.getAnnotations();\n\t\tArrays.sort(synthesizedAnnotations, Comparator.comparing(Annotation::toString));\n\t\tassertEquals(\n\t\t\tArrays.asList(childAnnotation, grandParentAnnotation, parentAnnotation),\n\t\t\tArrays.asList(synthesizedAnnotations)\n\t\t);\n\n\t\t// 扩展方法\n\t\tassertNotNull(syntheticMetaAnnotation.getSynthesizedAnnotation(GrandParentAnnotation.class));\n\t\tassertNotNull(syntheticMetaAnnotation.getSynthesizedAnnotation(ParentAnnotation.class));\n\t\tassertNotNull(syntheticMetaAnnotation.getSynthesizedAnnotation(ChildAnnotation.class));\n\t\tassertEquals(3, syntheticMetaAnnotation.getAllSynthesizedAnnotation().size());\n\n\t\t// 属性\n\t\tassertEquals(SynthesizedAnnotationSelector.NEAREST_AND_OLDEST_PRIORITY, syntheticMetaAnnotation.getAnnotationSelector());\n\t\tassertEquals(CacheableSynthesizedAnnotationAttributeProcessor.class, syntheticMetaAnnotation.getAnnotationAttributeProcessor().getClass());\n\t\tassertEquals(3, syntheticMetaAnnotation.getAnnotationPostProcessors().size());\n\t}\n\n\t@Test\n\tpublic void synthesisAnnotationAttributeTest() {\n\t\tfinal ChildAnnotation rootAnnotation = AnnotatedClass.class.getAnnotation(ChildAnnotation.class);\n\t\tGenericSynthesizedAggregateAnnotation syntheticMetaAnnotation = new GenericSynthesizedAggregateAnnotation(rootAnnotation);\n\t\tassertEquals(syntheticMetaAnnotation.getSource(), Collections.singletonList(rootAnnotation));\n\t\tassertEquals(syntheticMetaAnnotation.annotationType(), GenericSynthesizedAggregateAnnotation.class);\n\t\tassertEquals(3, syntheticMetaAnnotation.getAnnotations().length);\n\n\t\tassertEquals(\"Child!\", syntheticMetaAnnotation.getAttributeValue(\"childValue\", String.class));\n\t\tassertEquals(\"Child!\", syntheticMetaAnnotation.getAttributeValue(\"childValueAlias\", String.class));\n\t\tassertEquals(\"Child's Parent!\", syntheticMetaAnnotation.getAttributeValue(\"parentValue\", String.class));\n\t\tassertEquals(\"Child's GrandParent!\", syntheticMetaAnnotation.getAttributeValue(\"grandParentValue\", String.class));\n\t}\n\n\t@Test\n\tpublic void syntheticAnnotationTest() {\n\t\tfinal ChildAnnotation rootAnnotation = AnnotatedClass.class.getAnnotation(ChildAnnotation.class);\n\t\tGenericSynthesizedAggregateAnnotation syntheticMetaAnnotation = new GenericSynthesizedAggregateAnnotation(rootAnnotation);\n\n\t\tfinal ChildAnnotation childAnnotation = syntheticMetaAnnotation.synthesize(ChildAnnotation.class);\n\t\tSynthesizedAnnotation childSyntheticAnnotation = syntheticMetaAnnotation.getSynthesizedAnnotation(ChildAnnotation.class);\n\t\tassertNotNull(childSyntheticAnnotation);\n\t\tassertTrue(childSyntheticAnnotation.hasAttribute(\"childValue\", String.class));\n\t\tassertEquals(AnnotatedClass.class.getAnnotation(ChildAnnotation.class), childSyntheticAnnotation.getRoot());\n\t\tassertEquals(AnnotatedClass.class.getAnnotation(ChildAnnotation.class), childSyntheticAnnotation.getAnnotation());\n\t\tassertTrue(syntheticMetaAnnotation.isAnnotationPresent(ChildAnnotation.class));\n\t\tassertNotNull(childAnnotation);\n\t\tassertEquals(\"Child!\", childAnnotation.childValue());\n\t\tassertEquals(\"Child!\", childAnnotation.childValueAlias());\n\t\tassertEquals(childAnnotation.grandParentType(), Integer.class);\n\t\tassertThrows(IllegalArgumentException.class, () -> new GenericSynthesizedAggregateAnnotation(childAnnotation));\n\n\t\tfinal ParentAnnotation parentAnnotation = syntheticMetaAnnotation.synthesize(ParentAnnotation.class);\n\t\tSynthesizedAnnotation parentSyntheticAnnotation = syntheticMetaAnnotation.getSynthesizedAnnotation(ParentAnnotation.class);\n\t\tassertNotNull(parentSyntheticAnnotation);\n\t\tassertTrue(parentSyntheticAnnotation.hasAttribute(\"parentValue\", String.class));\n\t\tassertEquals(AnnotatedClass.class.getAnnotation(ChildAnnotation.class), parentSyntheticAnnotation.getRoot());\n\t\tassertEquals(ChildAnnotation.class.getAnnotation(ParentAnnotation.class), parentSyntheticAnnotation.getAnnotation());\n\t\tassertNotNull(parentAnnotation);\n\t\tassertEquals(\"Child's Parent!\", parentAnnotation.parentValue());\n\t\tassertEquals(\"java.lang.Void\", parentAnnotation.grandParentType());\n\t\tassertThrows(IllegalArgumentException.class, () -> new GenericSynthesizedAggregateAnnotation(parentAnnotation));\n\n\t\tfinal GrandParentAnnotation grandParentAnnotation = syntheticMetaAnnotation.synthesize(GrandParentAnnotation.class);\n\t\tSynthesizedAnnotation grandParentSyntheticAnnotation = syntheticMetaAnnotation.getSynthesizedAnnotation(GrandParentAnnotation.class);\n\t\tassertNotNull(grandParentSyntheticAnnotation);\n\t\tassertTrue(grandParentSyntheticAnnotation.hasAttribute(\"grandParentType\", Class.class));\n\t\tassertEquals(AnnotatedClass.class.getAnnotation(ChildAnnotation.class), grandParentSyntheticAnnotation.getRoot());\n\t\tassertEquals(ChildAnnotation.class.getAnnotation(GrandParentAnnotation.class), grandParentSyntheticAnnotation.getAnnotation());\n\t\tassertTrue(syntheticMetaAnnotation.isAnnotationPresent(GrandParentAnnotation.class));\n\t\tassertNotNull(grandParentAnnotation);\n\t\tassertEquals(\"Child's GrandParent!\", grandParentAnnotation.grandParentValue());\n\t\tassertEquals(grandParentAnnotation.grandParentType(), Integer.class);\n\t\tassertThrows(IllegalArgumentException.class, () -> new GenericSynthesizedAggregateAnnotation(grandParentAnnotation));\n\t}\n\n\t@Test\n\tpublic void linkTest() {\n\t\tfinal Method method = ReflectUtil.getMethod(AnnotationForLinkTest.class, \"value\");\n\t\tfinal SynthesizedAggregateAnnotation synthesizedAnnotationAggregator = new GenericSynthesizedAggregateAnnotation(method.getAnnotation(AliasFor.class));\n\t\tfinal Link link = synthesizedAnnotationAggregator.synthesize(Link.class);\n\t\tassertEquals(AnnotationForLinkTest.class, link.annotation());\n\t\tassertEquals(\"name\", link.attribute());\n\t}\n\n\t@Test\n\tpublic void mirrorAttributeTest() {\n\t\tAnnotationForMirrorTest annotation = ClassForMirrorTest.class.getAnnotation(AnnotationForMirrorTest.class);\n\t\tSynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tAnnotationForMirrorTest syntheticAnnotation = synthetic.synthesize(AnnotationForMirrorTest.class);\n\t\tassertEquals(\"Foo\", syntheticAnnotation.name());\n\t\tassertEquals(\"Foo\", syntheticAnnotation.value());\n\n\t\tannotation = ClassForMirrorTest2.class.getAnnotation(AnnotationForMirrorTest.class);\n\t\tsynthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tsyntheticAnnotation = synthetic.synthesize(AnnotationForMirrorTest.class);\n\t\tassertEquals(\"Foo\", syntheticAnnotation.name());\n\t\tassertEquals(\"Foo\", syntheticAnnotation.value());\n\n\t\tannotation = ClassForMirrorTest3.class.getAnnotation(AnnotationForMirrorTest.class);\n\t\tsynthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tsyntheticAnnotation = synthetic.synthesize(AnnotationForMirrorTest.class);\n\t\tAnnotationForMirrorTest finalSyntheticAnnotation = syntheticAnnotation;\n\t\tassertThrows(IllegalArgumentException.class, finalSyntheticAnnotation::name);\n\t}\n\n\t@Test\n\tpublic void aliasForTest() {\n\t\tAnnotationForAliasForTest annotation = ClassForAliasForTest.class.getAnnotation(AnnotationForAliasForTest.class);\n\t\tSynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tMetaAnnotationForAliasForTest metaAnnotation = synthetic.synthesize(MetaAnnotationForAliasForTest.class);\n\t\tassertEquals(\"Meta\", metaAnnotation.name());\n\t\tAnnotationForAliasForTest childAnnotation = synthetic.synthesize(AnnotationForAliasForTest.class);\n\t\tassertEquals(\"\", childAnnotation.value());\n\n\t\tannotation = ClassForAliasForTest2.class.getAnnotation(AnnotationForAliasForTest.class);\n\t\tsynthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tmetaAnnotation = synthetic.synthesize(MetaAnnotationForAliasForTest.class);\n\t\tassertEquals(\"Foo\", metaAnnotation.name());\n\t\tchildAnnotation = synthetic.synthesize(AnnotationForAliasForTest.class);\n\t\tassertEquals(\"Foo\", childAnnotation.value());\n\t}\n\n\t@Test\n\tpublic void forceAliasForTest() {\n\t\tAnnotationForceForAliasForTest annotation = ClassForForceAliasForTest.class.getAnnotation(AnnotationForceForAliasForTest.class);\n\t\tSynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tMetaAnnotationForForceAliasForTest metaAnnotation = synthetic.synthesize(MetaAnnotationForForceAliasForTest.class);\n\t\tassertEquals(\"\", metaAnnotation.name());\n\t\tAnnotationForceForAliasForTest childAnnotation = synthetic.synthesize(AnnotationForceForAliasForTest.class);\n\t\tassertEquals(\"\", childAnnotation.value());\n\n\t\tannotation = ClassForForceAliasForTest2.class.getAnnotation(AnnotationForceForAliasForTest.class);\n\t\tsynthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tmetaAnnotation = synthetic.synthesize(MetaAnnotationForForceAliasForTest.class);\n\t\tassertEquals(\"Foo\", metaAnnotation.name());\n\t\tchildAnnotation = synthetic.synthesize(AnnotationForceForAliasForTest.class);\n\t\tassertEquals(\"Foo\", childAnnotation.value());\n\t}\n\n\t@Test\n\tpublic void aliasForAndMirrorTest() {\n\t\tAnnotationForMirrorThenAliasForTest annotation = ClassForAliasForAndMirrorTest.class.getAnnotation(AnnotationForMirrorThenAliasForTest.class);\n\t\tSynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\t\tMetaAnnotationForMirrorThenAliasForTest metaAnnotation = synthetic.synthesize(MetaAnnotationForMirrorThenAliasForTest.class);\n\t\tassertEquals(\"test\", metaAnnotation.name());\n\t\tassertEquals(\"test\", metaAnnotation.value());\n\t\tAnnotationForMirrorThenAliasForTest childAnnotation = synthetic.synthesize(AnnotationForMirrorThenAliasForTest.class);\n\t\tassertEquals(\"test\", childAnnotation.childValue());\n\t}\n\n\t@Test\n\tpublic void multiAliasForTest() {\n\t\tfinal AnnotationForMultiAliasForTest annotation = ClassForMultiAliasForTest.class.getAnnotation(AnnotationForMultiAliasForTest.class);\n\t\tfinal SynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\n\t\tfinal MetaAnnotationForMultiAliasForTest1 metaAnnotation1 = synthetic.synthesize(MetaAnnotationForMultiAliasForTest1.class);\n\t\tassertEquals(\"test\", metaAnnotation1.name());\n\t\tassertEquals(\"test\", metaAnnotation1.value1());\n\t\tfinal MetaAnnotationForMultiAliasForTest2 metaAnnotation2 = synthetic.synthesize(MetaAnnotationForMultiAliasForTest2.class);\n\t\tassertEquals(\"test\", metaAnnotation2.value2());\n\t\tfinal AnnotationForMultiAliasForTest childAnnotation = synthetic.synthesize(AnnotationForMultiAliasForTest.class);\n\t\tassertEquals(\"test\", childAnnotation.value3());\n\t}\n\n\t@Test\n\tpublic void implicitAliasTest() {\n\t\tfinal AnnotationForImplicitAliasTest annotation = ClassForImplicitAliasTest.class.getAnnotation(AnnotationForImplicitAliasTest.class);\n\t\tfinal SynthesizedAggregateAnnotation synthetic = new GenericSynthesizedAggregateAnnotation(annotation);\n\n\t\tfinal MetaAnnotationForImplicitAliasTest metaAnnotation = synthetic.synthesize(MetaAnnotationForImplicitAliasTest.class);\n\t\tassertEquals(\"Meta\", metaAnnotation.name());\n\t\tassertEquals(\"Foo\", metaAnnotation.value());\n\t\tfinal AnnotationForImplicitAliasTest childAnnotation = synthetic.synthesize(AnnotationForImplicitAliasTest.class);\n\t\tassertEquals(\"Foo\", childAnnotation.value());\n\t}\n\n\t// 注解结构如下：\n\t// AnnotatedClass -> @ChildAnnotation -> @ParentAnnotation -> @GrandParentAnnotation\n\t//                                    -> @GrandParentAnnotation\n\t@ChildAnnotation(childValueAlias = \"Child!\", grandParentType = Integer.class)\n\tstatic class AnnotatedClass {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.ANNOTATION_TYPE })\n\t@interface GrandParentAnnotation {\n\t\tString grandParentValue() default \"\";\n\t\tClass<?> grandParentType() default Void.class;\n\t}\n\n\t@GrandParentAnnotation(grandParentValue = \"Parent's GrandParent!\") // 覆盖元注解@GrandParentAnnotation的属性\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.TYPE })\n\t@interface ParentAnnotation {\n\t\tString parentValue() default \"\";\n\t\tString grandParentType() default \"java.lang.Void\";\n\t}\n\n\t@GrandParentAnnotation(grandParentValue = \"Child's GrandParent!\") // 重复的元注解，靠近根注解的优先级高\n\t@ParentAnnotation(parentValue = \"Child's Parent!\") // 覆盖元注解@ParentAnnotation的属性\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface ChildAnnotation {\n\t\tString childValueAlias() default \"\";\n\t\t@Alias(\"childValueAlias\")\n\t\tString childValue() default \"\";\n\t\tClass<?> grandParentType() default Void.class;\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForMirrorTest {\n\t\t//@Link(attribute = \"name\")\n\t\t@MirrorFor(attribute = \"name\")\n\t\tString value() default \"\";\n\t\t//@Link(attribute = \"value\")\n\t\t@MirrorFor(attribute = \"value\")\n\t\tString name() default \"\";\n\t}\n\t@AnnotationForMirrorTest(\"Foo\")\n\tstatic class ClassForMirrorTest {}\n\t@AnnotationForMirrorTest(name = \"Foo\")\n\tstatic class ClassForMirrorTest2 {}\n\t@AnnotationForMirrorTest(value = \"Aoo\", name = \"Foo\")\n\tstatic class ClassForMirrorTest3 {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForAliasForTest {\n\t\tString name() default \"\";\n\t}\n\t@MetaAnnotationForAliasForTest(name = \"Meta\")\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForAliasForTest {\n\t\t@AliasFor(\n\t\t\tannotation = MetaAnnotationForAliasForTest.class,\n\t\t\tattribute = \"name\"\n\t\t)\n\t\tString value() default \"\";\n\t}\n\t@AnnotationForAliasForTest\n\tstatic class ClassForAliasForTest {}\n\t@AnnotationForAliasForTest(\"Foo\")\n\tstatic class ClassForAliasForTest2 {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForForceAliasForTest {\n\t\tString name() default \"\";\n\t}\n\t@MetaAnnotationForForceAliasForTest(name = \"Meta\")\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForceForAliasForTest {\n\t\t//@Link(\n\t\t//\tannotation = MetaAnnotationForForceAliasForTest.class,\n\t\t//\tattribute = \"name\",\n\t\t//\ttype = RelationType.FORCE_ALIAS_FOR\n\t\t//)\n\t\t@ForceAliasFor(annotation = MetaAnnotationForForceAliasForTest.class, attribute = \"name\")\n\t\tString value() default \"\";\n\t}\n\t@AnnotationForceForAliasForTest\n\tstatic class ClassForForceAliasForTest {}\n\t@AnnotationForceForAliasForTest(\"Foo\")\n\tstatic class ClassForForceAliasForTest2 {}\n\n\t@interface AnnotationForLinkTest {\n\t\t@AliasFor(attribute = \"name\", annotation = AnnotationForLinkTest.class)\n\t\tString value() default \"value\";\n\t\tString name() default \"name\";\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForMirrorThenAliasForTest {\n\t\t@MirrorFor(attribute = \"value\")\n\t\tString name() default \"\";\n\t\t@MirrorFor(attribute = \"name\")\n\t\tString value() default \"\";\n\t}\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@MetaAnnotationForMirrorThenAliasForTest(\"Meta\")\n\t@interface AnnotationForMirrorThenAliasForTest {\n\t\t@AliasFor(attribute = \"name\", annotation = MetaAnnotationForMirrorThenAliasForTest.class)\n\t\tString childValue() default \"value\";\n\t}\n\t@AnnotationForMirrorThenAliasForTest(childValue = \"test\")\n\tstatic class ClassForAliasForAndMirrorTest{}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForMultiAliasForTest1 {\n\t\t@MirrorFor(attribute = \"value1\")\n\t\tString name() default \"\";\n\t\t@MirrorFor(attribute = \"name\")\n\t\tString value1() default \"\";\n\t}\n\t@MetaAnnotationForMultiAliasForTest1\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForMultiAliasForTest2 {\n\t\t@AliasFor(attribute = \"name\", annotation = MetaAnnotationForMultiAliasForTest1.class)\n\t\tString value2() default \"\";\n\t}\n\t@MetaAnnotationForMultiAliasForTest2\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForMultiAliasForTest {\n\t\t@AliasFor(attribute = \"value2\", annotation = MetaAnnotationForMultiAliasForTest2.class)\n\t\tString value3() default \"value\";\n\t}\n\t@AnnotationForMultiAliasForTest(value3 = \"test\")\n\tstatic class ClassForMultiAliasForTest{}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface MetaAnnotationForImplicitAliasTest {\n\t\t@MirrorFor(attribute = \"value\")\n\t\tString name() default \"\";\n\t\t@MirrorFor(attribute = \"name\")\n\t\tString value() default \"\";\n\t}\n\t@MetaAnnotationForImplicitAliasTest(\"Meta\")\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForImplicitAliasTest {\n\t\tString value() default \"\";\n\t}\n\t@AnnotationForImplicitAliasTest(\"Foo\")\n\tstatic class ClassForImplicitAliasTest {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/MirrorLinkAnnotationPostProcessorTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\npublic class MirrorLinkAnnotationPostProcessorTest {\n\n\t@Test\n\tpublic void processTest() {\n\t\tMirrorLinkAnnotationPostProcessor processor = new MirrorLinkAnnotationPostProcessor();\n\n\t\tMap<Class<?>, SynthesizedAnnotation> annotationMap = new HashMap<>();\n\t\tSynthesizedAggregateAnnotation synthesizedAnnotationAggregator = new TestSynthesizedAggregateAnnotation(annotationMap);\n\t\tAnnotationForTest annotation = ClassForTest.class.getAnnotation(AnnotationForTest.class);\n\t\tSynthesizedAnnotation synthesizedAnnotation = new TestSynthesizedAnnotation(synthesizedAnnotationAggregator, annotation);\n\t\tannotationMap.put(annotation.annotationType(), synthesizedAnnotation);\n\n\t\tprocessor.process(synthesizedAnnotation, synthesizedAnnotationAggregator);\n\t\tAnnotationAttribute valueAttribute = synthesizedAnnotation.getAttributes().get(\"value\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"value\"), valueAttribute.getAttribute());\n\t\tassertTrue(valueAttribute.isWrapped());\n\t\tassertEquals(MirroredAnnotationAttribute.class, valueAttribute.getClass());\n\n\t\tAnnotationAttribute nameAttribute = synthesizedAnnotation.getAttributes().get(\"name\");\n\t\tassertEquals(ReflectUtil.getMethod(AnnotationForTest.class, \"name\"), nameAttribute.getAttribute());\n\t\tassertTrue(nameAttribute.isWrapped());\n\t\tassertEquals(MirroredAnnotationAttribute.class, nameAttribute.getClass());\n\n\t\tassertEquals(((WrappedAnnotationAttribute)nameAttribute).getLinked(), ((WrappedAnnotationAttribute)valueAttribute).getOriginal());\n\t\tassertEquals(((WrappedAnnotationAttribute)nameAttribute).getOriginal(), ((WrappedAnnotationAttribute)valueAttribute).getLinked());\n\t}\n\n\t@AnnotationForTest\n\tstatic class ClassForTest {}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\t@Link(attribute = \"name\", type = RelationType.MIRROR_FOR)\n\t\tString value() default \"\";\n\t\t@Link(attribute = \"value\", type = RelationType.MIRROR_FOR)\n\t\tString name() default \"\";\n\t}\n\n\tstatic class TestSynthesizedAggregateAnnotation implements SynthesizedAggregateAnnotation {\n\n\t\tprivate final Map<Class<?>, SynthesizedAnnotation> annotationMap;\n\n\t\tpublic TestSynthesizedAggregateAnnotation(Map<Class<?>, SynthesizedAnnotation> annotationMap) {\n\t\t\tthis.annotationMap = annotationMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getSource() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationSelector getAnnotationSelector() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotationAttributeProcessor getAnnotationAttributeProcessor() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Collection<SynthesizedAnnotationPostProcessor> getAnnotationPostProcessors() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic SynthesizedAnnotation getSynthesizedAnnotation(Class<?> annotationType) {\n\t\t\treturn annotationMap.get(annotationType);\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<Class<? extends Annotation>, SynthesizedAnnotation> getAllSynthesizedAnnotation() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T getAnnotation(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation[] getAnnotations() {\n\t\t\treturn new Annotation[0];\n\t\t}\n\n\t\t@Override\n\t\tpublic <T extends Annotation> T synthesize(Class<T> annotationType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t}\n\n\tstatic class TestSynthesizedAnnotation implements SynthesizedAnnotation {\n\n\t\tprivate final Annotation annotation;\n\t\tprivate final SynthesizedAggregateAnnotation owner;\n\t\tprivate final Map<String, AnnotationAttribute> attributeMap;\n\n\t\tpublic TestSynthesizedAnnotation(SynthesizedAggregateAnnotation owner, Annotation annotation) {\n\t\t\tthis.owner = owner;\n\t\t\tthis.attributeMap = new HashMap<>();\n\t\t\tthis.annotation = annotation;\n\t\t\tfor (Method declaredMethod : annotation.annotationType().getDeclaredMethods()) {\n\t\t\t\tattributeMap.put(declaredMethod.getName(), new CacheableAnnotationAttribute(annotation, declaredMethod));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation getAnnotation() {\n\t\t\treturn annotation;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getVerticalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getHorizontalDistance() {\n\t\t\treturn 0;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\t\treturn attributeMap;\n\t\t}\n\n\t\t@Override\n\t\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\t\t\tattributeMap.put(attributeName, attribute);\n\t\t}\n\n\t\t@Override\n\t\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\t\t\tAnnotationAttribute annotationAttribute = attributeMap.get(attributeName);\n\t\t\tif (ObjectUtil.isNotNull(annotationAttribute)) {\n\t\t\t\tattributeMap.put(attributeName, operator.apply(annotationAttribute));\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Class<? extends Annotation> annotationType() {\n\t\t\treturn annotation.annotationType();\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/MirroredAnnotationAttributeTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.lang.reflect.Method;\n\npublic class MirroredAnnotationAttributeTest {\n\n\t@Test\n\tpublic void baseInfoTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal MirroredAnnotationAttribute nameAnnotationAttribute = new MirroredAnnotationAttribute(nameAttribute, valueAttribute);\n\n\t\t// 注解属性\n\t\tassertEquals(annotation, nameAnnotationAttribute.getAnnotation());\n\t\tassertEquals(annotation.annotationType(), nameAnnotationAttribute.getAnnotationType());\n\n\t\t// 方法属性\n\t\tassertEquals(nameMethod.getName(), nameAnnotationAttribute.getAttributeName());\n\t\tassertEquals(nameMethod.getReturnType(), nameAnnotationAttribute.getAttributeType());\n\t}\n\n\t@Test\n\tpublic void workWhenValueDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest2.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal MirroredAnnotationAttribute nameAnnotationAttribute = new MirroredAnnotationAttribute(nameAttribute, valueAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"\", nameAnnotationAttribute.getValue());\n\t\tassertTrue(nameAnnotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(nameAnnotationAttribute.isWrapped());\n\t}\n\n\t@Test\n\tpublic void workWhenValueNonDefaultTest() {\n\t\t// 组合属性\n\t\tfinal Annotation annotation = ClassForTest1.class.getAnnotation(AnnotationForTest.class);\n\t\tfinal Method valueMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"value\");\n\t\tfinal CacheableAnnotationAttribute valueAttribute = new CacheableAnnotationAttribute(annotation, valueMethod);\n\t\tfinal Method nameMethod = ReflectUtil.getMethod(AnnotationForTest.class, \"name\");\n\t\tfinal CacheableAnnotationAttribute nameAttribute = new CacheableAnnotationAttribute(annotation, nameMethod);\n\t\tfinal MirroredAnnotationAttribute nameAnnotationAttribute = new MirroredAnnotationAttribute(nameAttribute, valueAttribute);\n\n\t\t// 值处理\n\t\tassertEquals(\"name\", nameAnnotationAttribute.getValue());\n\t\tassertFalse(nameAnnotationAttribute.isValueEquivalentToDefaultValue());\n\t\tassertTrue(nameAnnotationAttribute.isWrapped());\n\t}\n\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.METHOD, ElementType.TYPE })\n\t@interface AnnotationForTest {\n\t\tString value() default \"\";\n\t\tString name() default \"\";\n\t}\n\n\t@AnnotationForTest(value = \"name\")\n\tstatic class ClassForTest1 {}\n\n\t@AnnotationForTest\n\tstatic class ClassForTest2 {}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/RepeatAnnotationForTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * @author hongda.li 2022-04-26 17:09\n */\n@AnnotationForTest(\"repeat-annotation\")\n@Retention(RetentionPolicy.RUNTIME)\n// Target注解决定MyAnnotation注解可以加在哪些成分上，如加在类身上，或者属性身上，或者方法身上等成分\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface RepeatAnnotationForTest {\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/SynthesizedAnnotationSelectorTest.java",
    "content": "package cn.hutool.core.annotation;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\n\npublic class SynthesizedAnnotationSelectorTest {\n\n\t@Test\n\tpublic void chooseTest() {\n\t\tfinal SynthesizedAnnotationSelector.NearestAndOldestPrioritySelector selector = (SynthesizedAnnotationSelector.NearestAndOldestPrioritySelector)SynthesizedAnnotationSelector.NEAREST_AND_OLDEST_PRIORITY;\n\n\t\tTestSynthesizedAnnotation annotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tTestSynthesizedAnnotation annotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 1);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(1, 0);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\t}\n\n\t@Test\n\tpublic void nearestAndNewestPriorityTest() {\n\t\tfinal SynthesizedAnnotationSelector.NearestAndNewestPrioritySelector selector = (SynthesizedAnnotationSelector.NearestAndNewestPrioritySelector)SynthesizedAnnotationSelector.NEAREST_AND_NEWEST_PRIORITY;\n\n\t\tTestSynthesizedAnnotation annotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tTestSynthesizedAnnotation annotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 1);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tannotation2 = new TestSynthesizedAnnotation(1, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\t}\n\n\t@Test\n\tpublic void farthestAndOldestPriorityTest() {\n\t\tfinal SynthesizedAnnotationSelector.FarthestAndOldestPrioritySelector selector = (SynthesizedAnnotationSelector.FarthestAndOldestPrioritySelector)SynthesizedAnnotationSelector.FARTHEST_AND_OLDEST_PRIORITY;\n\n\t\tTestSynthesizedAnnotation annotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tTestSynthesizedAnnotation annotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 1);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tannotation2 = new TestSynthesizedAnnotation(1, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\t}\n\n\t@Test\n\tpublic void farthestAndNewestPriorityTest() {\n\t\tfinal SynthesizedAnnotationSelector.FarthestAndNewestPrioritySelector selector = (SynthesizedAnnotationSelector.FarthestAndNewestPrioritySelector)SynthesizedAnnotationSelector.FARTHEST_AND_NEWEST_PRIORITY;\n\n\t\tTestSynthesizedAnnotation annotation1 = new TestSynthesizedAnnotation(0, 0);\n\t\tTestSynthesizedAnnotation annotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(0, 1);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation2, selector.choose(annotation1, annotation2));\n\n\t\tannotation1 = new TestSynthesizedAnnotation(1, 0);\n\t\tannotation2 = new TestSynthesizedAnnotation(0, 0);\n\t\tassertEquals(annotation1, selector.choose(annotation1, annotation2));\n\t}\n\n\tstatic class TestSynthesizedAnnotation implements SynthesizedAnnotation {\n\n\t\tprivate final int verticalDistance;\n\t\tprivate final int horizontalDistance;\n\n\t\tpublic TestSynthesizedAnnotation(int verticalDistance, int horizontalDistance) {\n\t\t\tthis.verticalDistance = verticalDistance;\n\t\t\tthis.horizontalDistance = horizontalDistance;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getRoot() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Annotation getAnnotation() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getVerticalDistance() {\n\t\t\treturn this.verticalDistance;\n\t\t}\n\n\t\t@Override\n\t\tpublic int getHorizontalDistance() {\n\t\t\treturn this.horizontalDistance;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean hasAttribute(String attributeName, Class<?> returnType) {\n\t\t\treturn false;\n\t\t}\n\n\t\t@Override\n\t\tpublic Map<String, AnnotationAttribute> getAttributes() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic void setAttribute(String attributeName, AnnotationAttribute attribute) {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName) {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Class<? extends Annotation> annotationType() {\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tpublic Object getAttributeValue(String attributeName, Class<?> attributeType) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/TestIssueI8CLBJ.java",
    "content": "package cn.hutool.core.annotation;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * @author huangchengxing\n */\npublic class TestIssueI8CLBJ {\n\n\t@Test\n\tpublic void test() throws NoSuchFieldException {\n\t\tField field = Foo.class.getDeclaredField(\"name\");\n\t\tassertNotNull(field);\n\t\tAnnotation[] annotations = field.getDeclaredAnnotations();\n\t\tassertTrue(annotations.length > 0);\n\n\t\tTestAnnotation annotation = AnnotationUtil.getSynthesizedAnnotation(TestAnnotation.class, annotations);\n\t\tList<Thread> threadList = new ArrayList<>();\n\t\tfor (int i = 0; i < 30; i++) {\n\t\t\tThread thread = new Thread(() -> {\n\t\t\t\ttry {\n\t\t\t\t\tString valueFieldName = annotation.valueFieldName();\n\t\t\t\t\t//Console.log(\"valueFieldName:\" + valueFieldName);\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t});\n\t\t\tthreadList.add(thread);\n\t\t\tthread.start();\n\t\t}\n\n\t\ttry {\n\t\t\tfor (Thread thread : threadList) {\n\t\t\t\tthread.join();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\tpublic static class Foo {\n\t\tprivate Integer id;\n\t\t@TestAnnotation(\"name\")\n\t\tprivate String name;\n\t}\n\n\t@Target(ElementType.FIELD)\n\t@Retention(RetentionPolicy.RUNTIME)\n\tprivate @interface TestAnnotation {\n\t\tString value() default \"\";\n\t\t@Alias(\"value\")\n\t\tString valueFieldName() default \"\";\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/AnnotationForScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * @author huangchengxing\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })\n@interface AnnotationForScannerTest {\n\tString value() default \"\";\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/ElementAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class ElementAnnotationScannerTest {\n\n\t@Test\n\tpublic void supportTest() {\n\t\tfinal ElementAnnotationScanner scanner = new ElementAnnotationScanner();\n\t\tassertTrue(scanner.support(ReflectUtil.getField(FieldAnnotationScannerTest.Example.class, \"id\")));\n\t\tassertTrue(scanner.support(ReflectUtil.getMethod(FieldAnnotationScannerTest.Example.class, \"getId\")));\n\t\tassertFalse(scanner.support(null));\n\t\tassertTrue(scanner.support(FieldAnnotationScannerTest.Example.class));\n\t}\n\n\t@Test\n\tpublic void getAnnotationsTest() {\n\t\tfinal ElementAnnotationScanner scanner = new ElementAnnotationScanner();\n\t\tfinal Field field = ReflectUtil.getField(FieldAnnotationScannerTest.Example.class, \"id\");\n\t\tassertNotNull(field);\n\t\tassertTrue(scanner.support(field));\n\t\tList<Annotation> annotations = scanner.getAnnotations(field);\n\t\tassertEquals(1, annotations.size());\n\t\tassertEquals(AnnotationForScannerTest.class, CollUtil.getFirst(annotations).annotationType());\n\t}\n\n\t@Test\n\tpublic void scanTest() {\n\t\tfinal ElementAnnotationScanner scanner = new ElementAnnotationScanner();\n\t\tfinal Field field = ReflectUtil.getField(FieldAnnotationScannerTest.Example.class, \"id\");\n\t\tfinal Map<Integer, List<Annotation>> map = new HashMap<>();\n\t\tscanner.scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tfield, null\n\t\t);\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(AnnotationForScannerTest.class, map.get(0).get(0).annotationType());\n\t}\n\n\tpublic static class Example {\n\t\t@AnnotationForScannerTest\n\t\tprivate Integer id;\n\n\t\tpublic Integer getId() {\n\t\t\treturn id;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/FieldAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class FieldAnnotationScannerTest {\n\n\t@Test\n\tpublic void supportTest() {\n\t\tAnnotationScanner scanner = new FieldAnnotationScanner();\n\t\tassertTrue(scanner.support(ReflectUtil.getField(Example.class, \"id\")));\n\t\tassertFalse(scanner.support(ReflectUtil.getMethod(Example.class, \"getId\")));\n\t\tassertFalse(scanner.support(null));\n\t\tassertFalse(scanner.support(Example.class));\n\t}\n\n\t@Test\n\tpublic void getAnnotationsTest() {\n\t\tAnnotationScanner scanner = new FieldAnnotationScanner();\n\t\tField field = ReflectUtil.getField(Example.class, \"id\");\n\t\tassertNotNull(field);\n\t\tassertTrue(scanner.support(field));\n\t\tList<Annotation> annotations = scanner.getAnnotations(field);\n\t\tassertEquals(1, annotations.size());\n\t\tassertEquals(AnnotationForScannerTest.class, CollUtil.getFirst(annotations).annotationType());\n\t}\n\n\t@Test\n\tpublic void scanTest() {\n\t\tAnnotationScanner scanner = new FieldAnnotationScanner();\n\t\tField field = ReflectUtil.getField(Example.class, \"id\");\n\t\tMap<Integer, List<Annotation>> map = new HashMap<>();\n\t\tscanner.scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tfield, null\n\t\t);\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(AnnotationForScannerTest.class, map.get(0).get(0).annotationType());\n\t}\n\n\tpublic static class Example {\n\t\t@AnnotationForScannerTest\n\t\tprivate Integer id;\n\n\t\tpublic Integer getId() {\n\t\t\treturn id;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/GenericAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.util.List;\n\npublic class GenericAnnotationScannerTest {\n\n\t@Test\n\tpublic void scanDirectlyTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(false, false, false);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(1, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanDirectlyAndMetaAnnotationTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(true, false, false);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(2, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanSuperclassTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(false, true, false);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(2, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanSuperclassAndMetaAnnotationTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(true, true, false);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(4, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanInterfaceTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(false, false, true);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(2, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanInterfaceAndMetaAnnotationTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(true, false, true);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(4, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanTypeHierarchyTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(false, true, true);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(3, annotations.size());\n\t}\n\n\t@Test\n\tpublic void scanTypeHierarchyAndMetaAnnotationTest() {\n\t\tfinal GenericAnnotationScanner scanner = new GenericAnnotationScanner(true, true, true);\n\t\tfinal List<Annotation> annotations = scanner.getAnnotations(ClassForTest.class);\n\t\tassertEquals(6, annotations.size());\n\t}\n\n\t@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD})\n\t@Retention(RetentionPolicy.RUNTIME)\n\tpublic @interface MetaAnnotationForTest { }\n\n\t@MetaAnnotationForTest\n\t@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD})\n\t@Retention(RetentionPolicy.RUNTIME)\n\tpublic @interface AnnotationForTest { }\n\n\t@AnnotationForTest\n\tstatic class ClassForTest extends SupperForTest implements InterfaceForTest { }\n\n\t@AnnotationForTest\n\tstatic class SupperForTest { }\n\n\t@AnnotationForTest\n\tinterface InterfaceForTest { }\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/MateAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.*;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class MateAnnotationScannerTest {\n\n\t@Test\n\tpublic void supportTest() {\n\t\tAnnotationScanner scanner = new MetaAnnotationScanner();\n\t\tassertTrue(scanner.support(AnnotationForScannerTest.class));\n\t\tassertFalse(scanner.support(ReflectUtil.getField(Example.class, \"id\")));\n\t\tassertFalse(scanner.support(ReflectUtil.getMethod(Example.class, \"getId\")));\n\t\tassertFalse(scanner.support(null));\n\t\tassertFalse(scanner.support(Example.class));\n\t}\n\n\t@Test\n\tpublic void getAnnotationsTest() {\n\t\tAnnotationScanner scanner = new MetaAnnotationScanner();\n\t\tassertTrue(scanner.support(AnnotationForScannerTest3.class));\n\t\tMap<Class<? extends Annotation>, Annotation> annotations = CollUtil.toMap(scanner.getAnnotations(AnnotationForScannerTest3.class), new HashMap<>(), Annotation::annotationType);\n\t\tassertEquals(3, annotations.size());\n\t\tassertTrue(annotations.containsKey(AnnotationForScannerTest.class));\n\t\tassertTrue(annotations.containsKey(AnnotationForScannerTest1.class));\n\t\tassertTrue(annotations.containsKey(AnnotationForScannerTest2.class));\n\t\tassertFalse(annotations.containsKey(AnnotationForScannerTest3.class));\n\n\t\tscanner = new MetaAnnotationScanner(false);\n\t\tassertTrue(scanner.support(AnnotationForScannerTest3.class));\n\t\tannotations = CollUtil.toMap(scanner.getAnnotations(AnnotationForScannerTest3.class), new HashMap<>(), Annotation::annotationType);\n\t\tassertEquals(1, annotations.size());\n\t\tassertTrue(annotations.containsKey(AnnotationForScannerTest2.class));\n\t\tassertFalse(annotations.containsKey(AnnotationForScannerTest.class));\n\t\tassertFalse(annotations.containsKey(AnnotationForScannerTest1.class));\n\t\tassertFalse(annotations.containsKey(AnnotationForScannerTest3.class));\n\t}\n\n\t@Test\n\tpublic void scanTest() {\n\t\tAnnotationScanner scanner = new MetaAnnotationScanner();\n\t\tMap<Integer, List<Annotation>> map = new HashMap<>();\n\t\tscanner.scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tAnnotationForScannerTest3.class, null\n\t\t);\n\n\t\tassertEquals(3, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(AnnotationForScannerTest2.class, map.get(0).get(0).annotationType());\n\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(AnnotationForScannerTest1.class, map.get(1).get(0).annotationType());\n\n\t\tassertEquals(1, map.get(2).size());\n\t\tassertEquals(AnnotationForScannerTest.class, map.get(2).get(0).annotationType());\n\t}\n\n\tstatic class Example {\n\t\tprivate Integer id;\n\t\tpublic Integer getId() {\n\t\t\treturn id;\n\t\t}\n\t}\n\n\t@AnnotationForScannerTest\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })\n\t@interface AnnotationForScannerTest1 {}\n\n\t@AnnotationForScannerTest1\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })\n\t@interface AnnotationForScannerTest2 {}\n\n\t@AnnotationForScannerTest2\n\t@Retention(RetentionPolicy.RUNTIME)\n\t@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })\n\t@interface AnnotationForScannerTest3 {}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/MethodAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.*;\n\npublic class MethodAnnotationScannerTest {\n\n\t@Test\n\tpublic void supportTest() {\n\t\tAnnotationScanner scanner = new MethodAnnotationScanner();\n\t\tassertTrue(scanner.support(ReflectUtil.getMethod(Example.class, \"test\")));\n\t\tassertFalse(scanner.support(null));\n\t\tassertFalse(scanner.support(Example.class));\n\t\tassertFalse(scanner.support(ReflectUtil.getField(Example.class, \"id\")));\n\t}\n\n\t@Test\n\tpublic void getAnnotationsTest() {\n\t\tAnnotationScanner scanner = new MethodAnnotationScanner();\n\t\tMethod method = ReflectUtil.getMethod(Example.class, \"test\");\n\t\tassertNotNull(method);\n\n\t\t// 不查找父类中具有相同方法签名的方法\n\t\tList<Annotation> annotations = scanner.getAnnotations(method);\n\t\tassertEquals(1, annotations.size());\n\t\tassertEquals(CollUtil.getFirst(annotations).annotationType(), AnnotationForScannerTest.class);\n\n\t\t// 查找父类中具有相同方法签名的方法\n\t\tscanner = new MethodAnnotationScanner(true);\n\t\tannotations = scanner.getAnnotations(method);\n\t\tassertEquals(3, annotations.size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) annotations.get(0)).value());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) annotations.get(1)).value());\n\t\tassertEquals(\"SuperInterface\", ((AnnotationForScannerTest) annotations.get(2)).value());\n\n\t\t// 查找父类中具有相同方法签名的方法，但是不查找SuperInterface\n\t\tscanner = new MethodAnnotationScanner(true).addExcludeTypes(SuperInterface.class);\n\t\tannotations = scanner.getAnnotations(method);\n\t\tassertEquals(2, annotations.size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) annotations.get(0)).value());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) annotations.get(1)).value());\n\n\t\t// 查找父类中具有相同方法签名的方法，但是只查找SuperClass\n\t\tscanner = new MethodAnnotationScanner(true)\n\t\t\t.setFilter(t -> ClassUtil.isAssignable(SuperClass.class, t));\n\t\tannotations = scanner.getAnnotations(method);\n\t\tassertEquals(2, annotations.size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) annotations.get(0)).value());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) annotations.get(1)).value());\n\t}\n\n\t@Test\n\tpublic void scanTest() {\n\t\tMethod method = ReflectUtil.getMethod(Example.class, \"test\");\n\n\t\t// 不查找父类中具有相同方法签名的方法\n\t\tMap<Integer, List<Annotation>> map = new HashMap<>();\n\t\tnew MethodAnnotationScanner(false).scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tmethod, null\n\t\t);\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\n\t\t// 查找父类中具有相同方法签名的方法\n\t\tmap.clear();\n\t\tnew MethodAnnotationScanner(true).scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tmethod, null\n\t\t);\n\t\tassertEquals(3, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\t\tassertEquals(1, map.get(2).size());\n\t\tassertEquals(\"SuperInterface\", ((AnnotationForScannerTest) map.get(2).get(0)).value());\n\n\t\t// 查找父类中具有相同方法签名的方法，但是不查找SuperInterface\n\t\tmap.clear();\n\t\tnew MethodAnnotationScanner(true)\n\t\t\t.addExcludeTypes(SuperInterface.class)\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tmethod, null\n\t\t\t);\n\t\tassertEquals(2, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\n\t\t// 查找父类中具有相同方法签名的方法，但是只查找SuperClass\n\t\tmap.clear();\n\t\tnew MethodAnnotationScanner(true)\n\t\t\t.setFilter(t -> ClassUtil.isAssignable(SuperClass.class, t))\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tmethod, null\n\t\t\t);\n\t\tassertEquals(2, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"SuperClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\t}\n\n\tstatic class Example extends SuperClass {\n\t\tprivate Integer id;\n\n\t\t@Override\n\t\t@AnnotationForScannerTest(\"Example\")\n\t\tpublic List<?> test() { return Collections.emptyList(); }\n\t}\n\n\tstatic class SuperClass implements SuperInterface {\n\n\t\t@Override\n\t\t@AnnotationForScannerTest(\"SuperClass\")\n\t\tpublic Collection<?> test() { return Collections.emptyList(); }\n\n\t}\n\n\tinterface SuperInterface {\n\n\t\t@AnnotationForScannerTest(\"SuperInterface\")\n\t\tObject test();\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/annotation/scanner/TypeAnnotationScannerTest.java",
    "content": "package cn.hutool.core.annotation.scanner;\n\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.annotation.Annotation;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TypeAnnotationScannerTest {\n\n\t@Test\n\tpublic void supportTest() {\n\t\tAnnotationScanner scanner = new TypeAnnotationScanner();\n\t\tassertTrue(scanner.support(Example.class));\n\t\tassertFalse(scanner.support(ReflectUtil.getField(Example.class, \"id\")));\n\t\tassertFalse(scanner.support(ReflectUtil.getMethod(Example.class, \"getId\")));\n\t\tassertFalse(scanner.support(null));\n\t}\n\n\t@Test\n\tpublic void getAnnotationsTest() {\n\t\tAnnotationScanner scanner = new TypeAnnotationScanner();\n\t\tList<Annotation> annotations = scanner.getAnnotations(Example.class);\n\t\tassertEquals(3, annotations.size());\n\t\tannotations.forEach(a -> assertEquals(a.annotationType(), AnnotationForScannerTest.class));\n\n\t\t// 不查找父接口\n\t\tscanner = new TypeAnnotationScanner().setIncludeInterfaces(false);\n\t\tannotations = scanner.getAnnotations(Example.class);\n\t\tassertEquals(2, annotations.size());\n\t\tannotations.forEach(a -> assertEquals(a.annotationType(), AnnotationForScannerTest.class));\n\n\t\t// 不查找父类\n\t\tscanner = new TypeAnnotationScanner().setIncludeSuperClass(false);\n\t\tannotations = scanner.getAnnotations(Example.class);\n\t\tassertEquals(1, annotations.size());\n\t\tannotations.forEach(a -> assertEquals(a.annotationType(), AnnotationForScannerTest.class));\n\n\t\t// 不查找ExampleSupplerClass.class\n\t\tscanner = new TypeAnnotationScanner().addExcludeTypes(ExampleSupplerClass.class);\n\t\tannotations = scanner.getAnnotations(Example.class);\n\t\tassertEquals(1, annotations.size());\n\t\tannotations.forEach(a -> assertEquals(a.annotationType(), AnnotationForScannerTest.class));\n\n\t\t// 只查找ExampleSupplerClass.class\n\t\tscanner = new TypeAnnotationScanner().setFilter(t -> ClassUtil.isAssignable(ExampleSupplerClass.class, t));\n\t\tannotations = scanner.getAnnotations(Example.class);\n\t\tassertEquals(2, annotations.size());\n\t\tannotations.forEach(a -> assertEquals(a.annotationType(), AnnotationForScannerTest.class));\n\t}\n\n\t@Test\n\tpublic void scanTest() {\n\t\tMap<Integer, List<Annotation>> map = new HashMap<>();\n\n\t\t// 查找父类与父接口\n\t\tnew TypeAnnotationScanner().scan(\n\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\tExample.class, null\n\t\t);\n\t\tassertEquals(3, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"ExampleSupplerClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\t\tassertEquals(1, map.get(2).size());\n\t\tassertEquals(\"ExampleInterface\", ((AnnotationForScannerTest) map.get(2).get(0)).value());\n\n\t\t// 不查找父接口\n\t\tmap.clear();\n\t\tnew TypeAnnotationScanner()\n\t\t\t.setIncludeInterfaces(false)\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tExample.class, null\n\t\t\t);\n\t\tassertEquals(2, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"ExampleSupplerClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\n\t\t// 不查找父类\n\t\tmap.clear();\n\t\tnew TypeAnnotationScanner()\n\t\t\t.setIncludeSuperClass(false)\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tExample.class, null\n\t\t\t);\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\n\t\t// 不查找ExampleSupplerClass.class\n\t\tmap.clear();\n\t\tnew TypeAnnotationScanner()\n\t\t\t.addExcludeTypes(ExampleSupplerClass.class)\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tExample.class, null\n\t\t\t);\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\n\t\t// 只查找ExampleSupplerClass.class\n\t\tmap.clear();\n\t\tnew TypeAnnotationScanner()\n\t\t\t.setFilter(t -> ClassUtil.isAssignable(ExampleSupplerClass.class, t))\n\t\t\t.scan(\n\t\t\t\t(index, annotation) -> map.computeIfAbsent(index, i -> new ArrayList<>()).add(annotation),\n\t\t\t\tExample.class, null\n\t\t\t);\n\t\tassertEquals(2, map.size());\n\t\tassertEquals(1, map.get(0).size());\n\t\tassertEquals(\"Example\", ((AnnotationForScannerTest) map.get(0).get(0)).value());\n\t\tassertEquals(1, map.get(1).size());\n\t\tassertEquals(\"ExampleSupplerClass\", ((AnnotationForScannerTest) map.get(1).get(0)).value());\n\t}\n\n\t@AnnotationForScannerTest(\"ExampleSupplerClass\")\n\tstatic class ExampleSupplerClass implements ExampleInterface {}\n\n\t@AnnotationForScannerTest(\"ExampleInterface\")\n\tinterface ExampleInterface {}\n\n\t@AnnotationForScannerTest(\"Example\")\n\tstatic class Example extends ExampleSupplerClass {\n\t\tprivate Integer id;\n\t\tpublic Integer getId() {\n\t\t\treturn id;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/BeanCopyMappingTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Builder;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BeanCopyMappingTest {\n\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I4C48U <br>\n\t * 传递复制不要用注解别名，应该用动态映射\n\t */\n\t@Test\n\tpublic void copyPropertiesTest() {\n\t\tfinal CopyOptions copyOptions = CopyOptions.create()\n\t\t\t\t.setFieldMapping(MapUtil.of(\"car\", \"carNo\"));\n\n\t\tB b = B.builder().car(\"12312312\").build();\n\t\tA a = A.builder().build();\n\t\tC c = C.builder().build();\n\t\tBeanUtil.copyProperties(b, a, copyOptions);\n\t\tBeanUtil.copyProperties(a, c);\n\n\t\tassertEquals(\"12312312\", c.getCarNo());\n\t}\n\n\t@Data\n\t@Builder\n\tpublic static class A {\n\t\tprivate String carNo;\n\t}\n\n\t@Data\n\t@Builder\n\tpublic static class B {\n\t\tprivate String car;\n\t}\n\n\t@Data\n\t@Builder\n\tpublic static class C {\n\t\tprivate String carNo;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/BeanDescTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * {@link BeanDesc} 单元测试类\n *\n * @author looly\n *\n */\npublic class BeanDescTest {\n\n\t@Test\n\tpublic void propDescTes() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\t\tassertEquals(\"User\", desc.getSimpleName());\n\n\t\tassertEquals(\"age\", desc.getField(\"age\").getName());\n\t\tassertEquals(\"getAge\", desc.getGetter(\"age\").getName());\n\t\tassertEquals(\"setAge\", desc.getSetter(\"age\").getName());\n\t\tassertEquals(1, desc.getSetter(\"age\").getParameterTypes().length);\n\t\tassertSame(int.class, desc.getSetter(\"age\").getParameterTypes()[0]);\n\n\t}\n\n\t@Test\n\tpublic void propDescTes2() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\n\t\tPropDesc prop = desc.getProp(\"name\");\n\t\tassertEquals(\"name\", prop.getFieldName());\n\t\tassertEquals(\"getName\", prop.getGetter().getName());\n\t\tassertEquals(\"setName\", prop.getSetter().getName());\n\t\tassertEquals(1, prop.getSetter().getParameterTypes().length);\n\t\tassertSame(String.class, prop.getSetter().getParameterTypes()[0]);\n\t}\n\n\t@Test\n\tpublic void propDescOfBooleanTest() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\n\t\tassertEquals(\"isAdmin\", desc.getGetter(\"isAdmin\").getName());\n\t\tassertEquals(\"setAdmin\", desc.getSetter(\"isAdmin\").getName());\n\t\tassertEquals(\"isGender\", desc.getGetter(\"gender\").getName());\n\t\tassertEquals(\"setGender\", desc.getSetter(\"gender\").getName());\n\t}\n\n\t@Test\n\tpublic void propDescOfBooleanTest2() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\n\t\tassertEquals(\"isIsSuper\", desc.getGetter(\"isSuper\").getName());\n\t\tassertEquals(\"setIsSuper\", desc.getSetter(\"isSuper\").getName());\n\t}\n\n\t@Test\n\tpublic void getSetTest() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\n\t\tUser user = new User();\n\t\tdesc.getProp(\"name\").setValue(user, \"张三\");\n\t\tassertEquals(\"张三\", user.getName());\n\n\t\tObject value = desc.getProp(\"name\").getValue(user);\n\t\tassertEquals(\"张三\", value);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void propDescOfBooleanTest3() {\n\t\tBeanDesc desc = BeanUtil.getBeanDesc(User.class);\n\n\t\tassertEquals(\"setLastPage\", desc.getSetter(\"lastPage\").getName());\n\t\tassertEquals(\"setIsLastPage\", desc.getSetter(\"isLastPage\").getName());\n\t}\n\n\tpublic static class User {\n\t\tprivate String name;\n\t\tprivate int age;\n\t\tprivate boolean isAdmin;\n\t\tprivate boolean isSuper;\n\t\tprivate boolean gender;\n\t\tprivate Boolean lastPage;\n\t\tprivate Boolean isLastPage;\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\n\t\tpublic void setName(String name) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\tpublic int getAge() {\n\t\t\treturn age;\n\t\t}\n\n\t\tpublic User setAge(int age) {\n\t\t\tthis.age = age;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic String testMethod() {\n\t\t\treturn \"test for \" + this.name;\n\t\t}\n\n\t\tpublic boolean isAdmin() {\n\t\t\treturn isAdmin;\n\t\t}\n\n\t\tpublic void setAdmin(boolean isAdmin) {\n\t\t\tthis.isAdmin = isAdmin;\n\t\t}\n\n\t\tpublic boolean isIsSuper() {\n\t\t\treturn isSuper;\n\t\t}\n\n\t\tpublic void setIsSuper(boolean isSuper) {\n\t\t\tthis.isSuper = isSuper;\n\t\t}\n\n\t\tpublic boolean isGender() {\n\t\t\treturn gender;\n\t\t}\n\n\t\tpublic void setGender(boolean gender) {\n\t\t\tthis.gender = gender;\n\t\t}\n\n\t\tpublic Boolean getLastPage() {\n\t\t\treturn this.lastPage;\n\t\t}\n\n\t\tpublic void setLastPage(final Boolean lastPage) {\n\t\t\tthis.lastPage = lastPage;\n\t\t}\n\n\t\tpublic Boolean getIsLastPage() {\n\t\t\treturn this.isLastPage;\n\t\t}\n\n\t\tpublic void setIsLastPage(final Boolean isLastPage) {\n\t\t\tthis.isLastPage = isLastPage;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"User [name=\" + name + \", age=\" + age + \", isAdmin=\" + isAdmin + \", gender=\" + gender + \"]\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/BeanPathTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.lang.test.bean.ExamInfoDict;\nimport cn.hutool.core.lang.test.bean.UserInfoDict;\nimport cn.hutool.core.util.ArrayUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * {@link BeanPath} 单元测试\n *\n * @author looly\n */\npublic class BeanPathTest {\n\n\tMap<String, Object> tempMap;\n\n\t@BeforeEach\n\tpublic void init() {\n\t\t// ------------------------------------------------- 考试信息列表\n\t\tfinal ExamInfoDict examInfoDict = new ExamInfoDict();\n\t\texamInfoDict.setId(1);\n\t\texamInfoDict.setExamType(0);\n\t\texamInfoDict.setAnswerIs(1);\n\n\t\tfinal ExamInfoDict examInfoDict1 = new ExamInfoDict();\n\t\texamInfoDict1.setId(2);\n\t\texamInfoDict1.setExamType(0);\n\t\texamInfoDict1.setAnswerIs(0);\n\n\t\tfinal ExamInfoDict examInfoDict2 = new ExamInfoDict();\n\t\texamInfoDict2.setId(3);\n\t\texamInfoDict2.setExamType(1);\n\t\texamInfoDict2.setAnswerIs(0);\n\n\t\tfinal List<ExamInfoDict> examInfoDicts = new ArrayList<>();\n\t\texamInfoDicts.add(examInfoDict);\n\t\texamInfoDicts.add(examInfoDict1);\n\t\texamInfoDicts.add(examInfoDict2);\n\n\t\t// ------------------------------------------------- 用户信息\n\t\tfinal UserInfoDict userInfoDict = new UserInfoDict();\n\t\tuserInfoDict.setId(1);\n\t\tuserInfoDict.setPhotoPath(\"yx.mm.com\");\n\t\tuserInfoDict.setRealName(\"张三\");\n\t\tuserInfoDict.setExamInfoDict(examInfoDicts);\n\n\t\ttempMap = new HashMap<>();\n\t\ttempMap.put(\"userInfo\", userInfoDict);\n\t\ttempMap.put(\"flag\", 1);\n\t}\n\n\t@Test\n\tpublic void beanPathTest1() {\n\t\tfinal BeanPath pattern = new BeanPath(\"userInfo.examInfoDict[0].id\");\n\t\tassertEquals(\"userInfo\", pattern.patternParts.get(0));\n\t\tassertEquals(\"examInfoDict\", pattern.patternParts.get(1));\n\t\tassertEquals(\"0\", pattern.patternParts.get(2));\n\t\tassertEquals(\"id\", pattern.patternParts.get(3));\n\n\t}\n\n\t@Test\n\tpublic void beanPathTest2() {\n\t\tfinal BeanPath pattern = new BeanPath(\"[userInfo][examInfoDict][0][id]\");\n\t\tassertEquals(\"userInfo\", pattern.patternParts.get(0));\n\t\tassertEquals(\"examInfoDict\", pattern.patternParts.get(1));\n\t\tassertEquals(\"0\", pattern.patternParts.get(2));\n\t\tassertEquals(\"id\", pattern.patternParts.get(3));\n\t}\n\n\t@Test\n\tpublic void beanPathTest3() {\n\t\tfinal BeanPath pattern = new BeanPath(\"['userInfo']['examInfoDict'][0]['id']\");\n\t\tassertEquals(\"userInfo\", pattern.patternParts.get(0));\n\t\tassertEquals(\"examInfoDict\", pattern.patternParts.get(1));\n\t\tassertEquals(\"0\", pattern.patternParts.get(2));\n\t\tassertEquals(\"id\", pattern.patternParts.get(3));\n\t}\n\n\t@Test\n\tpublic void getTest() {\n\t\tfinal BeanPath pattern = BeanPath.create(\"userInfo.examInfoDict[0].id\");\n\t\tfinal Object result = pattern.get(tempMap);\n\t\tassertEquals(1, result);\n\t}\n\n\t@Test\n\tpublic void setTest() {\n\t\tfinal BeanPath pattern = BeanPath.create(\"userInfo.examInfoDict[0].id\");\n\t\tpattern.set(tempMap, 2);\n\t\tfinal Object result = pattern.get(tempMap);\n\t\tassertEquals(2, result);\n\t}\n\n\t@Test\n\tpublic void getMapTest() {\n\t\tfinal BeanPath pattern = BeanPath.create(\"userInfo[id, photoPath]\");\n\t\t@SuppressWarnings(\"unchecked\") final Map<String, Object> result = (Map<String, Object>) pattern.get(tempMap);\n\t\tassertEquals(1, result.get(\"id\"));\n\t\tassertEquals(\"yx.mm.com\", result.get(\"photoPath\"));\n\t}\n\n\t@Test\n\tpublic void issue2362Test() {\n\t\tfinal Map<String, Object> map = new HashMap<>();\n\n\t\tBeanPath beanPath = BeanPath.create(\"list[0].name\");\n\t\tbeanPath.set(map, \"张三\");\n\t\tassertEquals(\"{list=[{name=张三}]}\", map.toString());\n\n\t\tmap.clear();\n\t\tbeanPath = BeanPath.create(\"list[1].name\");\n\t\tbeanPath.set(map, \"张三\");\n\t\tassertEquals(\"{list=[null, {name=张三}]}\", map.toString());\n\n\t\tmap.clear();\n\t\tbeanPath = BeanPath.create(\"list[0].1.name\");\n\t\tbeanPath.set(map, \"张三\");\n\t\tassertEquals(\"{list=[[null, {name=张三}]]}\", map.toString());\n\t}\n\n\t@Test\n\tpublic void appendArrayTest(){\n\t\t// issue#3008@Github\n\t\tfinal MyUser myUser = new MyUser();\n\t\tBeanPath.create(\"hobby[0]\").set(myUser, \"LOL\");\n\t\tBeanPath.create(\"hobby[1]\").set(myUser, \"KFC\");\n\t\tBeanPath.create(\"hobby[2]\").set(myUser, \"COFFE\");\n\n\t\tassertEquals(\"[LOL, KFC, COFFE]\", ArrayUtil.toString(myUser.getHobby()));\n\t}\n\n\t@Test\n\tpublic void wildcardTest() {\n\t\t// 测试通配符 * 语法\n\t\tfinal BeanPath pattern = BeanPath.create(\"userInfo.examInfoDict[*].id\");\n\t\tfinal Object result = pattern.get(tempMap);\n\t\t\n\t\t// 应该返回一个包含所有 examInfoDict 元素的 id 的列表\n\t\tassertEquals(\"[1, 2, 3]\", result.toString());\n\t}\n\n\t@Data\n\tstatic class MyUser {\n\t\tprivate String[] hobby;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.date.StopWatch;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.map.MapBuilder;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.*;\nimport lombok.experimental.Accessors;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.beans.PropertyDescriptor;\nimport java.io.Serializable;\nimport java.lang.reflect.Type;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * Bean工具单元测试\n *\n * @author Looly\n */\npublic class BeanUtilTest {\n\n\t@Test\n\tpublic void isBeanTest() {\n\n\t\t// HashMap不包含setXXX方法，不是bean\n\t\tfinal boolean isBean = BeanUtil.isBean(HashMap.class);\n\t\tassertFalse(isBean);\n\t}\n\n\t@Test\n\tpublic void fillBeanTest() {\n\t\tfinal Person person = BeanUtil.fillBean(new Person(), new ValueProvider<String>() {\n\n\t\t\t@Override\n\t\t\tpublic Object value(final String key, final Type valueType) {\n\t\t\t\tswitch (key) {\n\t\t\t\t\tcase \"name\":\n\t\t\t\t\t\treturn \"张三\";\n\t\t\t\t\tcase \"age\":\n\t\t\t\t\t\treturn 18;\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean containsKey(final String key) {\n\t\t\t\t// 总是存在key\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t}, CopyOptions.create());\n\n\t\tassertEquals(\"张三\", person.getName());\n\t\tassertEquals(18, person.getAge());\n\t}\n\n\t@Test\n\tpublic void fillBeanWithMapIgnoreCaseTest() {\n\t\tfinal Map<String, Object> map = MapBuilder.<String, Object>create()\n\t\t\t.put(\"Name\", \"Joe\")\n\t\t\t.put(\"aGe\", 12)\n\t\t\t.put(\"openId\", \"DFDFSDFWERWER\")\n\t\t\t.build();\n\t\tfinal SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false);\n\t\tassertEquals(\"Joe\", person.getName());\n\t\tassertEquals(12, person.getAge());\n\t\tassertEquals(\"DFDFSDFWERWER\", person.getOpenid());\n\t}\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Map<?, ?> map = BeanUtil.toBean(person, Map.class);\n\t\tassertEquals(\"测试A11\", map.get(\"name\"));\n\t\tassertEquals(14, map.get(\"age\"));\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\t\t// static属性应被忽略\n\t\tassertFalse(map.containsKey(\"SUBNAME\"));\n\t}\n\n\t/**\n\t * 忽略转换错误测试\n\t */\n\t@Test\n\tpublic void toBeanIgnoreErrorTest() {\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"name\", \"Joe\");\n\t\t// 错误的类型，此处忽略\n\t\tmap.put(\"age\", \"aaaaaa\");\n\n\t\tfinal Person person = BeanUtil.toBeanIgnoreError(map, Person.class);\n\t\tassertEquals(\"Joe\", person.getName());\n\t\t// 错误的类型，不copy这个字段，使用对象创建的默认值\n\t\tassertEquals(0, person.getAge());\n\t}\n\n\t@Test\n\tpublic void mapToBeanIgnoreCaseTest() {\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"Name\", \"Joe\");\n\t\tmap.put(\"aGe\", 12);\n\n\t\tfinal Person person = BeanUtil.toBeanIgnoreCase(map, Person.class, false);\n\t\tassertEquals(\"Joe\", person.getName());\n\t\tassertEquals(12, person.getAge());\n\t}\n\n\t@Test\n\tpublic void mapToBeanTest() {\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"a_name\", \"Joe\");\n\t\tmap.put(\"b_age\", 12);\n\n\t\t// 别名，用于对应bean的字段名\n\t\tfinal HashMap<String, String> mapping = MapUtil.newHashMap();\n\t\tmapping.put(\"a_name\", \"name\");\n\t\tmapping.put(\"b_age\", \"age\");\n\n\t\tfinal Person person = BeanUtil.toBean(map, Person.class, CopyOptions.create().setFieldMapping(mapping));\n\t\tassertEquals(\"Joe\", person.getName());\n\t\tassertEquals(12, person.getAge());\n\t}\n\n\t/**\n\t * 测试public类型的字段注入是否成功\n\t */\n\t@Test\n\tpublic void mapToBeanTest2() {\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"name\", \"Joe\");\n\t\tmap.put(\"age\", 12);\n\n\t\t// 非空构造也可以实例化成功\n\t\tfinal Person2 person = BeanUtil.toBean(map, Person2.class, CopyOptions.create());\n\t\tassertEquals(\"Joe\", person.name);\n\t\tassertEquals(12, person.age);\n\t}\n\n\t/**\n\t * 测试在不忽略错误情况下，转换失败需要报错。\n\t */\n\t@Test\n\tpublic void mapToBeanWinErrorTest() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\tfinal Map<String, String> map = new HashMap<>();\n\t\t\tmap.put(\"age\", \"哈哈\");\n\t\t\tBeanUtil.toBean(map, Person.class);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void beanToMapTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person);\n\n\t\tassertEquals(\"测试A11\", map.get(\"name\"));\n\t\tassertEquals(14, map.get(\"age\"));\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\t\t// static属性应被忽略\n\t\tassertFalse(map.containsKey(\"SUBNAME\"));\n\t}\n\n\t@Test\n\tpublic void beanToMapNullPropertiesTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person, (String[]) null);\n\n\t\tassertEquals(\"测试A11\", map.get(\"name\"));\n\t\tassertEquals(14, map.get(\"age\"));\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\t\t// static属性应被忽略\n\t\tassertFalse(map.containsKey(\"SUBNAME\"));\n\t}\n\n\t@Test\n\tpublic void beanToMapTest2() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person, true, true);\n\t\tassertEquals(\"sub名字\", map.get(\"sub_name\"));\n\t}\n\n\t@Test\n\tpublic void beanToMapWithValueEditTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person, new LinkedHashMap<>(),\n\t\t\tCopyOptions.create().setFieldValueEditor((key, value) -> key + \"_\" + value));\n\t\tassertEquals(\"subName_sub名字\", map.get(\"subName\"));\n\t}\n\n\t@Test\n\tpublic void beanToMapWithAliasTest() {\n\t\tfinal SubPersonWithAlias person = new SubPersonWithAlias();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\t\tperson.setSlow(true);\n\t\tperson.setBooleana(true);\n\t\tperson.setBooleanb(true);\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person);\n\t\tassertEquals(\"sub名字\", map.get(\"aliasSubName\"));\n\t}\n\n\t@Test\n\tpublic void mapToBeanWithAliasTest() {\n\t\tfinal Map<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"aliasSubName\", \"sub名字\");\n\t\tmap.put(\"slow\", true);\n\t\tmap.put(\"is_booleana\", \"1\");\n\t\tmap.put(\"is_booleanb\", true);\n\n\t\tfinal SubPersonWithAlias subPersonWithAlias = BeanUtil.toBean(map, SubPersonWithAlias.class);\n\t\tassertEquals(\"sub名字\", subPersonWithAlias.getSubName());\n\n\t\t//https://gitee.com/chinabugotech/hutool/issues/I6H0XF\n\t\tassertFalse(subPersonWithAlias.isBooleana());\n\t\tassertNull(subPersonWithAlias.getBooleanb());\n\t}\n\n\t@Test\n\tpublic void beanToMapWithLocalDateTimeTest() {\n\t\tfinal LocalDateTime now = LocalDateTime.now();\n\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\t\tperson.setDate(now);\n\t\tperson.setDate2(now.toLocalDate());\n\n\t\tfinal Map<String, Object> map = BeanUtil.beanToMap(person, false, true);\n\t\tassertEquals(now, map.get(\"date\"));\n\t\tassertEquals(now.toLocalDate(), map.get(\"date2\"));\n\t}\n\n\t@Test\n\tpublic void getPropertyTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal Object name = BeanUtil.getProperty(person, \"name\");\n\t\tassertEquals(\"测试A11\", name);\n\t\tfinal Object subName = BeanUtil.getProperty(person, \"subName\");\n\t\tassertEquals(\"sub名字\", subName);\n\t}\n\n\t@Test\n\t@SuppressWarnings(\"ConstantConditions\")\n\tpublic void getNullPropertyTest() {\n\t\tfinal Object property = BeanUtil.getProperty(null, \"name\");\n\t\tassertNull(property);\n\t}\n\n\t@Test\n\tpublic void getPropertyDescriptorsTest() {\n\t\tfinal HashSet<Object> set = CollUtil.newHashSet();\n\t\tfinal PropertyDescriptor[] propertyDescriptors = BeanUtil.getPropertyDescriptors(SubPerson.class);\n\t\tfor (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {\n\t\t\tset.add(propertyDescriptor.getName());\n\t\t}\n\t\tassertTrue(set.contains(\"age\"));\n\t\tassertTrue(set.contains(\"id\"));\n\t\tassertTrue(set.contains(\"name\"));\n\t\tassertTrue(set.contains(\"openid\"));\n\t\tassertTrue(set.contains(\"slow\"));\n\t\tassertTrue(set.contains(\"subName\"));\n\t}\n\n\t@Test\n\tpublic void copyPropertiesTest() {\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tfinal SubPerson person1 = BeanUtil.copyProperties(person, SubPerson.class);\n\t\tassertEquals(14, person1.getAge());\n\t\tassertEquals(\"11213232\", person1.getOpenid());\n\t\tassertEquals(\"测试A11\", person1.getName());\n\t\tassertEquals(\"sub名字\", person1.getSubName());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void multiThreadTest() {\n\t\tfinal Student student = new Student();\n\t\tstudent.setName(\"张三\");\n\t\tstudent.setAge(123);\n\t\tstudent.setNo(3158L);\n\n\t\tfinal Student student2 = new Student();\n\t\tstudent.setName(\"李四\");\n\t\tstudent.setAge(125);\n\t\tstudent.setNo(8848L);\n\n\t\tfinal List<Student> studentList = ListUtil.of(student, student2);\n\n\t\tfor (int i = 0; i < 5000; i++) {\n\t\t\tnew Thread(() -> {\n\t\t\t\tfinal List<Student> list = ObjectUtil.clone(studentList);\n\t\t\t\tfinal List<Student> listReps = list.stream().map(s1 -> {\n\t\t\t\t\tfinal Student s2 = new Student();\n\t\t\t\t\tBeanUtil.copyProperties(s1, s2);\n\t\t\t\t\treturn s2;\n\t\t\t\t}).collect(Collectors.toList());\n\n\t\t\t\tSystem.out.println(listReps);\n\t\t\t}).start();\n\t\t}\n\n\t\tThreadUtil.waitForDie();\n\t}\n\n\t@Test\n\tpublic void copyPropertiesHasBooleanTest() {\n\t\tfinal SubPerson p1 = new SubPerson();\n\t\tp1.setSlow(true);\n\n\t\t// 测试boolean参数值isXXX形式\n\t\tfinal SubPerson p2 = new SubPerson();\n\t\tBeanUtil.copyProperties(p1, p2);\n\t\tassertTrue(p2.getSlow());\n\n\t\t// 测试boolean参数值非isXXX形式\n\t\tfinal SubPerson2 p3 = new SubPerson2();\n\t\tBeanUtil.copyProperties(p1, p3);\n\t\tassertTrue(p3.getSlow());\n\t}\n\n\t@Test\n\tpublic void copyPropertiesIgnoreNullTest() {\n\t\tfinal SubPerson p1 = new SubPerson();\n\t\tp1.setSlow(true);\n\t\tp1.setName(null);\n\n\t\tfinal SubPerson2 p2 = new SubPerson2();\n\t\tp2.setName(\"oldName\");\n\n\t\t// null值不覆盖目标属性\n\t\tBeanUtil.copyProperties(p1, p2, CopyOptions.create().ignoreNullValue());\n\t\tassertEquals(\"oldName\", p2.getName());\n\n\t\t// null覆盖目标属性\n\t\tBeanUtil.copyProperties(p1, p2);\n\t\tassertNull(p2.getName());\n\t}\n\n\t@Test\n\tpublic void copyPropertiesBeanToMapTest() {\n\t\t// 测试BeanToMap\n\t\tfinal SubPerson p1 = new SubPerson();\n\t\tp1.setSlow(true);\n\t\tp1.setName(\"测试\");\n\t\tp1.setSubName(\"sub测试\");\n\n\t\tfinal Map<String, Object> map = MapUtil.newHashMap();\n\t\tBeanUtil.copyProperties(p1, map);\n\t\tassertTrue((Boolean) map.get(\"slow\"));\n\t\tassertEquals(\"测试\", map.get(\"name\"));\n\t\tassertEquals(\"sub测试\", map.get(\"subName\"));\n\t}\n\n\t@Test\n\tpublic void copyPropertiesMapToMapTest() {\n\t\t// 测试MapToMap\n\t\tfinal Map<String, Object> p1 = new HashMap<>();\n\t\tp1.put(\"isSlow\", true);\n\t\tp1.put(\"name\", \"测试\");\n\t\tp1.put(\"subName\", \"sub测试\");\n\n\t\tfinal Map<String, Object> map = MapUtil.newHashMap();\n\t\tBeanUtil.copyProperties(p1, map);\n\t\tassertTrue((Boolean) map.get(\"isSlow\"));\n\t\tassertEquals(\"测试\", map.get(\"name\"));\n\t\tassertEquals(\"sub测试\", map.get(\"subName\"));\n\t}\n\n\t@Test\n\tpublic void copyPropertiesMapToMapIgnoreNullTest() {\n\t\t// 测试MapToMap\n\t\tfinal Map<String, Object> p1 = new HashMap<>();\n\t\tp1.put(\"isSlow\", true);\n\t\tp1.put(\"name\", \"测试\");\n\t\tp1.put(\"subName\", null);\n\n\t\tfinal Map<String, Object> map = MapUtil.newHashMap();\n\t\tBeanUtil.copyProperties(p1, map, CopyOptions.create().setIgnoreNullValue(true));\n\t\tassertTrue((Boolean) map.get(\"isSlow\"));\n\t\tassertEquals(\"测试\", map.get(\"name\"));\n\t\tassertFalse(map.containsKey(\"subName\"));\n\t}\n\n\t@Test\n\tpublic void trimBeanStrFieldsTest() {\n\t\tfinal Person person = new Person();\n\t\tperson.setAge(1);\n\t\tperson.setName(\"  张三 \");\n\t\tperson.setOpenid(null);\n\t\tfinal Person person2 = BeanUtil.trimStrFields(person);\n\n\t\t// 是否改变原对象\n\t\tassertEquals(\"张三\", person.getName());\n\t\tassertEquals(\"张三\", person2.getName());\n\t}\n\n\t// -----------------------------------------------------------------------------------------------------------------\n\t@Getter\n\t@Setter\n\tpublic static class SubPerson extends Person {\n\n\t\tpublic static final String SUBNAME = \"TEST\";\n\n\t\tprivate UUID id;\n\t\tprivate String subName;\n\t\tprivate Boolean slow;\n\t\tprivate LocalDateTime date;\n\t\tprivate LocalDate date2;\n\t}\n\n\t@Getter\n\t@Setter\n\tpublic static class SubPerson2 extends Person {\n\t\tprivate String subName;\n\t\t// boolean参数值非isXXX形式\n\t\tprivate Boolean slow;\n\t}\n\n\t@Getter\n\t@Setter\n\t@ToString\n\tpublic static class SubPersonWithAlias extends Person {\n\t\t// boolean参数值非isXXX形式\n\t\t@Alias(\"aliasSubName\")\n\t\tprivate String subName;\n\t\tprivate Boolean slow;\n\t\tprivate boolean booleana;\n\t\tprivate Boolean booleanb;\n\t}\n\n\t@Getter\n\t@Setter\n\tpublic static class SubPersonWithOverlayTransientField extends PersonWithTransientField {\n\t\t// 覆盖父类中 transient 属性\n\t\tprivate String name;\n\t}\n\n\t@Getter\n\t@Setter\n\tpublic static class Person {\n\t\tprivate String name;\n\t\tprivate int age;\n\t\tprivate String openid;\n\t}\n\n\t@Getter\n\t@Setter\n\tpublic static class PersonWithTransientField {\n\t\tprivate transient String name;\n\t\tprivate int age;\n\t\tprivate String openid;\n\t}\n\n\tpublic static class Person2 {\n\n\t\tpublic Person2(final String name, final int age, final String openid) {\n\t\t\tthis.name = name;\n\t\t\tthis.age = age;\n\t\t\tthis.openid = openid;\n\t\t}\n\n\t\tpublic String name;\n\t\tpublic int age;\n\t\tpublic String openid;\n\t}\n\n\t/**\n\t * <a href=\"https://github.com/chinabugotech/hutool/issues/1173\">#1173</a>\n\t */\n\t@Test\n\tpublic void beanToBeanOverlayFieldTest() {\n\t\tfinal SubPersonWithOverlayTransientField source = new SubPersonWithOverlayTransientField();\n\t\tsource.setName(\"zhangsan\");\n\t\tsource.setAge(20);\n\t\tsource.setOpenid(\"1\");\n\t\tfinal SubPersonWithOverlayTransientField dest = new SubPersonWithOverlayTransientField();\n\t\tBeanUtil.copyProperties(source, dest);\n\n\t\tassertEquals(source.getName(), dest.getName());\n\t\tassertEquals(source.getAge(), dest.getAge());\n\t\tassertEquals(source.getOpenid(), dest.getOpenid());\n\t}\n\n\t@Test\n\tpublic void beanToBeanTest() {\n\t\t// 修复对象无getter方法导致报错的问题\n\t\tfinal Page page1 = new Page();\n\t\tBeanUtil.toBean(page1, Page.class);\n\t}\n\n\tpublic static class Page {\n\t\tprivate boolean optimizeCountSql = true;\n\n\t\tpublic boolean optimizeCountSql() {\n\t\t\treturn optimizeCountSql;\n\t\t}\n\n\t\tpublic Page setOptimizeCountSql(final boolean optimizeCountSql) {\n\t\t\tthis.optimizeCountSql = optimizeCountSql;\n\t\t\treturn this;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void copyBeanToBeanTest() {\n\t\t// 测试在copyProperties方法中alias是否有效\n\t\tfinal Food info = new Food();\n\t\tinfo.setBookID(\"0\");\n\t\tinfo.setCode(\"123\");\n\t\tfinal HllFoodEntity entity = new HllFoodEntity();\n\t\tBeanUtil.copyProperties(info, entity);\n\t\tassertEquals(info.getBookID(), entity.getBookId());\n\t\tassertEquals(info.getCode(), entity.getCode2());\n\t}\n\n\t@Test\n\tpublic void copyBeanTest() {\n\t\tfinal Food info = new Food();\n\t\tinfo.setBookID(\"0\");\n\t\tinfo.setCode(\"123\");\n\t\tfinal Food newFood = BeanUtil.copyProperties(info, Food.class, \"code\");\n\t\tassertEquals(info.getBookID(), newFood.getBookID());\n\t\tassertNull(newFood.getCode());\n\t}\n\n\t@Test\n\tpublic void copyNullTest() {\n\t\tassertNull(BeanUtil.copyProperties(null, Food.class));\n\t}\n\n\t@Test\n\tpublic void copyBeanPropertiesFilterTest() {\n\t\tfinal Food info = new Food();\n\t\tinfo.setBookID(\"0\");\n\t\tinfo.setCode(\"\");\n\t\tfinal Food newFood = new Food();\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().setPropertiesFilter((f, v) -> !(v instanceof CharSequence) || StrUtil.isNotBlank(v.toString()));\n\t\tBeanUtil.copyProperties(info, newFood, copyOptions);\n\t\tassertEquals(info.getBookID(), newFood.getBookID());\n\t\tassertNull(newFood.getCode());\n\t}\n\n\t@Test\n\tpublic void copyBeanPropertiesFunctionFilterTest() {\n\t\t//https://gitee.com/chinabugotech/hutool/pulls/590\n\t\tfinal Person o = new Person();\n\t\to.setName(\"asd\");\n\t\to.setAge(123);\n\t\to.setOpenid(\"asd\");\n\n\t\t@SuppressWarnings(\"unchecked\") final CopyOptions copyOptions = CopyOptions.create().setIgnoreProperties(Person::getAge, Person::getOpenid);\n\t\tfinal Person n = new Person();\n\t\tBeanUtil.copyProperties(o, n, copyOptions);\n\n\t\t// 是否忽略拷贝属性\n\t\tassertNotEquals(o.getAge(), n.getAge());\n\t\tassertNotEquals(o.getOpenid(), n.getOpenid());\n\t}\n\n\t@Data\n\tpublic static class Food {\n\t\t@Alias(\"bookId\")\n\t\tprivate String bookID;\n\t\tprivate String code;\n\t}\n\n\t@Data\n\tpublic static class HllFoodEntity implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate String bookId;\n\t\t@Alias(\"code\")\n\t\tprivate String code2;\n\t}\n\n\t@Test\n\tpublic void setPropertiesTest() {\n\t\tfinal Map<String, Object> resultMap = MapUtil.newHashMap();\n\t\tBeanUtil.setProperty(resultMap, \"codeList[0].name\", \"张三\");\n\t\tassertEquals(\"{codeList=[{name=张三}]}\", resultMap.toString());\n\t}\n\n\t@Test\n\tpublic void beanCopyTest() {\n\t\tfinal Station station = new Station();\n\t\tstation.setId(123456L);\n\n\t\tfinal Station station2 = new Station();\n\n\t\tBeanUtil.copyProperties(station, station2);\n\t\tassertEquals(new Long(123456L), station2.getId());\n\t}\n\n\tenum Version {\n\t\tdev,\n\t\tprod\n\t}\n\n\t@Data\n\tpublic static class Vto {\n\t\tEnumSet<Version> versions;\n\t}\n\n\n\t@Test\n\tpublic void beanWithEnumSetTest() {\n\t\tfinal Vto v1 = new Vto();\n\t\tv1.setVersions(EnumSet.allOf(Version.class));\n\t\tfinal Vto v2 = BeanUtil.copyProperties(v1, Vto.class);\n\t\tassertNotNull(v2);\n\t\tassertNotNull(v2.getVersions());\n\t}\n\n\t@Test\n\tpublic void enumSetTest() {\n\t\tfinal Collection<Version> objects = CollUtil.create(EnumSet.class, Version.class);\n\t\tassertNotNull(objects);\n\t\tassertTrue(EnumSet.class.isAssignableFrom(objects.getClass()));\n\t}\n\n\tstatic class Station extends Tree<Long> {\n\t}\n\n\tstatic class Tree<T> extends Entity<T> {\n\t}\n\n\t@Data\n\tpublic static class Entity<T> {\n\t\tprivate T id;\n\t}\n\n\t@Test\n\tpublic void copyListTest() {\n\t\tfinal Student student = new Student();\n\t\tstudent.setName(\"张三\");\n\t\tstudent.setAge(123);\n\t\tstudent.setNo(3158L);\n\n\t\tfinal Student student2 = new Student();\n\t\tstudent.setName(\"李四\");\n\t\tstudent.setAge(125);\n\t\tstudent.setNo(8848L);\n\n\t\tfinal List<Student> studentList = ListUtil.of(student, student2);\n\t\tfinal List<Person> people = BeanUtil.copyToList(studentList, Person.class);\n\n\t\tassertEquals(studentList.size(), people.size());\n\t\tfor (int i = 0; i < studentList.size(); i++) {\n\t\t\tassertEquals(studentList.get(i).getName(), people.get(i).getName());\n\t\t\tassertEquals(studentList.get(i).getAge(), people.get(i).getAge());\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void copyLargeListTest(){\n\t\tfinal SubPerson person = new SubPerson();\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\n\t\tperson.setId(UUID.randomUUID());\n\t\tperson.setSubName(\"sub名字\");\n\t\tperson.setSlow(true);\n\t\tperson.setDate(LocalDateTime.now());\n\t\tperson.setDate2(LocalDate.now());\n\n\t\tfinal List<SubPerson> list = new ArrayList<>();\n\t\tCollUtil.padRight(list, 1000, person);\n\n\t\t// 预先构建一次缓存，防止干扰\n\t\tBeanUtil.copyProperties(person, new SubPerson2());\n\t\t// org.springframework.beans.BeanUtils.copyProperties(new SubPerson(), new SubPerson2());\n\n\t\tConsole.log(\"copy bean size: {}\\n\", list.size());\n\t\tfinal StopWatch stopWatch = new StopWatch();\n\t\tstopWatch.start(\"BeanUtil#copyToList\");\n\t\tList<SubPerson2> copyList = BeanUtil.copyToList(list, SubPerson2.class);\n\t\t// list.forEach(item -> org.springframework.beans.BeanUtils.copyProperties(item, new SubPerson2()));\n\t\tstopWatch.stop();\n\t\tConsole.log(stopWatch.prettyPrint());\n\t\tassertEquals(copyList.size(),list.size());\n\t}\n\n\n\t@Test\n\tpublic void toMapTest() {\n\t\t// 测试转map的时候返回key\n\t\tfinal PrivilegeIClassification a = new PrivilegeIClassification();\n\t\ta.setId(\"1\");\n\t\ta.setName(\"2\");\n\t\ta.setCode(\"3\");\n\t\ta.setCreateTime(new Date());\n\t\ta.setSortOrder(9L);\n\n\t\tfinal Map<String, Object> f = BeanUtil.beanToMap(\n\t\t\ta,\n\t\t\tnew LinkedHashMap<>(),\n\t\t\tfalse,\n\t\t\tkey -> Arrays.asList(\"id\", \"name\", \"code\", \"sortOrder\").contains(key) ? key : null);\n\t\tassertFalse(f.containsKey(null));\n\t}\n\n\t@Data\n\tpublic static class PrivilegeIClassification implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate String id;\n\t\tprivate String name;\n\t\tprivate String code;\n\t\tprivate Long rowStatus;\n\t\tprivate Long sortOrder;\n\t\tprivate Date createTime;\n\t}\n\n\t@Test\n\tpublic void getFieldValue() {\n\t\tfinal TestPojo testPojo = new TestPojo();\n\t\ttestPojo.setName(\"名字\");\n\n\t\tfinal TestPojo2 testPojo2 = new TestPojo2();\n\t\ttestPojo2.setAge(2);\n\t\tfinal TestPojo2 testPojo3 = new TestPojo2();\n\t\ttestPojo3.setAge(3);\n\n\n\t\ttestPojo.setTestPojo2List(new TestPojo2[]{testPojo2, testPojo3});\n\n\t\tfinal BeanPath beanPath = BeanPath.create(\"testPojo2List.age\");\n\t\tfinal Object o = beanPath.get(testPojo);\n\n\t\tassertEquals(Integer.valueOf(2), ArrayUtil.get(o, 0));\n\t\tassertEquals(Integer.valueOf(3), ArrayUtil.get(o, 1));\n\t}\n\n\t@Data\n\tpublic static class TestPojo {\n\t\tprivate String name;\n\t\tprivate TestPojo2[] testPojo2List;\n\t}\n\n\t@Data\n\tpublic static class TestPojo2 {\n\t\tprivate int age;\n\t}\n\n\t@Data\n\tpublic static class Student implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tString name;\n\t\tint age;\n\t\tLong no;\n\t}\n\n\t/**\n\t * @author dazer\n\t * copyProperties(Object source, Object target, CopyOptions copyOptions)\n\t * 当：copyOptions的 setFieldNameEditor 不为空的时候，有bug,这里进行修复；\n\t */\n\t@Test\n\tpublic void beanToBeanCopyOptionsTest() {\n\t\tfinal ChildVo1 childVo1 = new ChildVo1();\n\t\tchildVo1.setChild_address(\"中国北京五道口\");\n\t\tchildVo1.setChild_name(\"张三\");\n\t\tchildVo1.setChild_father_name(\"张无忌\");\n\t\tchildVo1.setChild_mother_name(\"赵敏敏\");\n\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().\n\t\t\t//setIgnoreNullValue(true).\n\t\t\t//setIgnoreCase(false).\n\t\t\t\tsetFieldNameEditor(StrUtil::toCamelCase);\n\n\t\tfinal ChildVo2 childVo2 = new ChildVo2();\n\t\tBeanUtil.copyProperties(childVo1, childVo2, copyOptions);\n\n\t\tassertEquals(childVo1.getChild_address(), childVo2.getChildAddress());\n\t\tassertEquals(childVo1.getChild_name(), childVo2.getChildName());\n\t\tassertEquals(childVo1.getChild_father_name(), childVo2.getChildFatherName());\n\t\tassertEquals(childVo1.getChild_mother_name(), childVo2.getChildMotherName());\n\t}\n\n\t@Data\n\tpublic static class ChildVo1 {\n\t\tString child_name;\n\t\tString child_address;\n\t\tString child_mother_name;\n\t\tString child_father_name;\n\t}\n\n\t@Data\n\tpublic static class ChildVo2 {\n\t\tString childName;\n\t\tString childAddress;\n\t\tString childMotherName;\n\t\tString childFatherName;\n\t}\n\n\t@Test\n\tpublic void issueI41WKPTest() {\n\t\tfinal Test1 t1 = new Test1().setStrList(ListUtil.toList(\"list\"));\n\t\tfinal Test2 t2_hu = new Test2();\n\t\tBeanUtil.copyProperties(t1, t2_hu, CopyOptions.create().setIgnoreError(true));\n\t\tassertNull(t2_hu.getStrList());\n\t}\n\n\t@Data\n\t@Accessors(chain = true)\n\tpublic static class Test1 {\n\t\tprivate List<String> strList;\n\t}\n\n\t@Data\n\t@Accessors(chain = true)\n\tpublic static class Test2 {\n\t\tprivate List<Integer> strList;\n\t}\n\n\t@Test\n\tpublic void issuesI53O9JTest() {\n\t\tfinal Map<String, String> map = new HashMap<>();\n\t\tmap.put(\"statusIdUpdateTime\", \"\");\n\n\t\tfinal WkCrmCustomer customer = new WkCrmCustomer();\n\t\tBeanUtil.copyProperties(map, customer);\n\n\t\tassertNull(customer.getStatusIdUpdateTime());\n\t}\n\n\t@Data\n\tpublic static class WkCrmCustomer {\n\t\tprivate LocalDateTime statusIdUpdateTime;\n\t}\n\n\t@Test\n\tpublic void valueProviderToBeanTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I5B4R7\n\t\tfinal CopyOptions copyOptions = CopyOptions.create();\n\t\tfinal Map<String, String> filedMap = new HashMap<>();\n\t\tfiledMap.put(\"name\", \"sourceId\");\n\t\tcopyOptions.setFieldMapping(filedMap);\n\t\tfinal TestPojo pojo = BeanUtil.toBean(TestPojo.class, new ValueProvider<String>() {\n\t\t\tfinal HashMap<String, Object> map = new HashMap<>();\n\n\t\t\t{\n\t\t\t\tmap.put(\"sourceId\", \"123\");\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Object value(final String key, final Type valueType) {\n\t\t\t\treturn map.get(key);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean containsKey(final String key) {\n\t\t\t\treturn map.containsKey(key);\n\t\t\t}\n\t\t}, copyOptions);\n\t\tassertEquals(\"123\", pojo.getName());\n\t}\n\n\t@Data\n\t@EqualsAndHashCode\n\tprivate static class TestUserEntity {\n\t\tprivate String username;\n\t\tprivate String name;\n\t\tprivate Integer age;\n\t\tprivate Integer sex;\n\t\tprivate String mobile;\n\t\tprivate Date createTime;\n\t}\n\n\t@Data\n\t@EqualsAndHashCode\n\tprivate static class TestUserDTO {\n\t\tprivate String name;\n\t\tprivate Integer age;\n\t\tprivate Integer sex;\n\t\tprivate String mobile;\n\t}\n\n\t@Test\n\tpublic void isCommonFieldsEqualTest() {\n\t\tfinal TestUserEntity userEntity = new TestUserEntity();\n\t\tfinal TestUserDTO userDTO = new TestUserDTO();\n\n\t\tuserDTO.setAge(20);\n\t\tuserDTO.setName(\"takaki\");\n\t\tuserDTO.setSex(1);\n\t\tuserDTO.setMobile(\"17812312023\");\n\n\t\tBeanUtil.copyProperties(userDTO, userEntity);\n\n\t\tassertTrue(BeanUtil.isCommonFieldsEqual(userDTO, userEntity));\n\n\t\tuserEntity.setAge(13);\n\t\tassertFalse(BeanUtil.isCommonFieldsEqual(userDTO, userEntity));\n\t\tassertTrue(BeanUtil.isCommonFieldsEqual(userDTO, userEntity, \"age\"));\n\n\t\tassertTrue(BeanUtil.isCommonFieldsEqual(null, null));\n\t\tassertFalse(BeanUtil.isCommonFieldsEqual(null, userEntity));\n\t\tassertFalse(BeanUtil.isCommonFieldsEqual(userEntity, null));\n\n\t\tuserEntity.setSex(0);\n\t\tassertTrue(BeanUtil.isCommonFieldsEqual(userDTO, userEntity, \"age\", \"sex\"));\n\t}\n\n\t@Test\n\tpublic void hasGetterTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I6M7Z7\n\t\tfinal boolean b = BeanUtil.hasGetter(Object.class);\n\t\tassertFalse(b);\n\t}\n\n\t@Test\n\tpublic void issueI9VTZGTest() {\n\t\tfinal boolean bean = BeanUtil.isBean(Dict.class);\n\t\tassertFalse(bean);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/DynaBeanTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * {@link DynaBean}单元测试\n *\n * @author Looly\n */\npublic class DynaBeanTest {\n\n\t@Test\n\tpublic void beanTest() {\n\t\tUser user = new User();\n\t\tDynaBean bean = DynaBean.create(user);\n\t\tbean.set(\"name\", \"李华\");\n\t\tbean.set(\"age\", 12);\n\n\t\tString name = bean.get(\"name\");\n\t\tassertEquals(user.getName(), name);\n\t\tint age = bean.get(\"age\");\n\t\tassertEquals(user.getAge(), age);\n\n\t\t//重复包装测试\n\t\tDynaBean bean2 = new DynaBean(bean);\n\t\tUser user2 = bean2.getBean();\n\t\tassertEquals(user, user2);\n\n\t\t//执行指定方法\n\t\tObject invoke = bean2.invoke(\"testMethod\");\n\t\tassertEquals(\"test for 李华\", invoke);\n\t}\n\n\n\t@Test\n\tpublic void beanByStaticClazzConstructorTest() {\n\t\tString name_before = \"李华\";\n\t\tint age_before = 12;\n\t\tDynaBean bean = DynaBean.create(User.class);\n\t\tbean.set(\"name\", name_before);\n\t\tbean.set(\"age\", age_before);\n\n\t\tString name_after = bean.get(\"name\");\n\t\tassertEquals(name_before, name_after);\n\t\tint age_after = bean.get(\"age\");\n\t\tassertEquals(age_before, age_after);\n\n\t\t//重复包装测试\n\t\tDynaBean bean2 = new DynaBean(bean);\n\t\tUser user2 = bean2.getBean();\n\t\tUser user1 = bean.getBean();\n\t\tassertEquals(user1, user2);\n\n\t\t//执行指定方法\n\t\tObject invoke = bean2.invoke(\"testMethod\");\n\t\tassertEquals(\"test for 李华\", invoke);\n\t}\n\n\n\t@Test\n\tpublic void beanByInstanceClazzConstructorTest() {\n\t\tString name_before = \"李华\";\n\t\tint age_before = 12;\n\t\tDynaBean bean = new DynaBean(User.class);\n\t\tbean.set(\"name\", name_before);\n\t\tbean.set(\"age\", age_before);\n\n\t\tString name_after = bean.get(\"name\");\n\t\tassertEquals(name_before, name_after);\n\t\tint age_after = bean.get(\"age\");\n\t\tassertEquals(age_before, age_after);\n\n\t\t//重复包装测试\n\t\tDynaBean bean2 = new DynaBean(bean);\n\t\tUser user2 = bean2.getBean();\n\t\tUser user1 = bean.getBean();\n\t\tassertEquals(user1, user2);\n\n\t\t//执行指定方法\n\t\tObject invoke = bean2.invoke(\"testMethod\");\n\t\tassertEquals(\"test for 李华\", invoke);\n\t}\n\n\t@Data\n\tpublic static class User {\n\t\tprivate String name;\n\t\tprivate int age;\n\n\t\tpublic String testMethod() {\n\t\t\treturn \"test for \" + this.name;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue1687Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/1687\n */\npublic class Issue1687Test {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tfinal SysUserFb sysUserFb = new SysUserFb();\n\t\tsysUserFb.setDepId(\"123\");\n\t\tsysUserFb.setCustomerId(\"456\");\n\n\t\tfinal SysUser sysUser = BeanUtil.toBean(sysUserFb, SysUser.class);\n\t\t// 别名错位导致找不到字段\n\t\tassertNull(sysUser.getDepart());\n\t\tassertEquals(new Long(456L), sysUser.getOrgId());\n\t}\n\n\t@Test\n\tpublic void toBeanTest2(){\n\t\tfinal SysUserFb sysUserFb = new SysUserFb();\n\t\tsysUserFb.setDepId(\"123\");\n\t\tsysUserFb.setCustomerId(\"456\");\n\n\t\t// 补救别名错位\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().setFieldMapping(\n\t\t\t\tMapUtil.builder(\"depart\", \"depId\").build()\n\t\t);\n\t\tfinal SysUser sysUser = BeanUtil.toBean(sysUserFb, SysUser.class, copyOptions);\n\n\t\tassertEquals(new Long(123L), sysUser.getDepart());\n\t\tassertEquals(new Long(456L), sysUser.getOrgId());\n\t}\n\n\t@Data\n\tstatic class SysUserFb implements Serializable {\n\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t@Alias(\"depart\")\n\t\tprivate String depId;\n\n\t\t@Alias(\"orgId\")\n\t\tprivate String customerId;\n\t}\n\n\t@Data\n\tstatic class SysUser implements Serializable {\n\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t@Alias(\"depId\")\n\t\tprivate Long depart;\n\n\t\tprivate Long orgId;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue2009Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/2009\n */\npublic class Issue2009Test {\n\n\t@SuppressWarnings(\"InnerClassMayBeStatic\")\n\tpublic class BaseA {\n\t\tprivate String paPss;\n\n\t\tpublic String getPaPss() {\n\t\t\treturn paPss;\n\t\t}\n\n\t\tpublic void setPaPss(String paPss) {\n\t\t\tthis.paPss = paPss;\n\t\t}\n\t}\n\n\n\tpublic class A extends BaseA {\n\t\tprivate String papss;\n\n\t\tpublic String getPapss() {\n\t\t\treturn papss;\n\t\t}\n\n\t\tpublic void setPapss(String papss) {\n\t\t\tthis.papss = papss;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"A{\" +\n\t\t\t\t\t\"papss='\" + papss + '\\'' +\n\t\t\t\t\t'}';\n\t\t}\n\t}\n\n\n\tpublic class B extends BaseA {\n\t\tprivate String papss;\n\n\t\tpublic B(String papss) {\n\t\t\tthis.papss = papss;\n\t\t}\n\n\t\tpublic String getPapss() {\n\t\t\treturn papss;\n\t\t}\n\n\t\tpublic void setPapss(String papss) {\n\t\t\tthis.papss = papss;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"B{\" +\n\t\t\t\t\t\"papss='\" + papss + '\\'' +\n\t\t\t\t\t'}';\n\t\t}\n\t}\n\n\t@Test\n\tpublic void test() {\n\t\tB b = new B(\"a string text\");\n\t\tA a = new A();\n\t\tBeanUtil.copyProperties(b, a);\n\n\t\tassertEquals(b.getPapss(), a.getPapss());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue2082Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/2082<br>\n * 当setXXX有重载方法的时候，BeanDesc中会匹配到重载方法，增加类型检查来规避之\n */\npublic class Issue2082Test {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tTestBean2 testBean2 = new TestBean2();\n\t\tTestBean test = BeanUtil.toBean(testBean2, TestBean.class);\n\t\tassertNull(test.getId());\n\t}\n\n\t@Data\n\tstatic class TestBean {\n\t\tprivate Long id;\n\n\t\tpublic void setId(String id) {\n\t\t\tthis.id = Long.valueOf(id);\n\t\t}\n\t}\n\n\t@Data\n\tstatic class TestBean2 {\n\t\tprivate String id;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue2202Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.text.NamingCase;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Issue2202Test {\n\n\t/**\n\t * https://github.com/chinabugotech/hutool/issues/2202\n\t */\n\t@Test\n\tpublic void mapToBeanWithFieldNameEditorTest(){\n\t\tMap<String, String> headerMap = new HashMap<>(5);\n\t\theaderMap.put(\"wechatpay-serial\", \"serial\");\n\t\theaderMap.put(\"wechatpay-nonce\", \"nonce\");\n\t\theaderMap.put(\"wechatpay-timestamp\", \"timestamp\");\n\t\theaderMap.put(\"wechatpay-signature\", \"signature\");\n\t\tResponseSignVerifyParams case1 = BeanUtil.toBean(headerMap, ResponseSignVerifyParams.class,\n\t\t\t\tCopyOptions.create().setFieldNameEditor(field -> NamingCase.toCamelCase(field, '-')));\n\n\t\tassertEquals(\"serial\", case1.getWechatpaySerial());\n\t\tassertEquals(\"nonce\", case1.getWechatpayNonce());\n\t\tassertEquals(\"timestamp\", case1.getWechatpayTimestamp());\n\t\tassertEquals(\"signature\", case1.getWechatpaySignature());\n\t}\n\n\t@Data\n\tstatic class ResponseSignVerifyParams {\n\t\tprivate String wechatpaySerial;\n\t\tprivate String wechatpaySignature;\n\t\tprivate String wechatpayTimestamp;\n\t\tprivate String wechatpayNonce;\n\t\tprivate String body;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue2697Test.java",
    "content": "package cn.hutool.core.bean;\n\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/2697\n */\npublic class Issue2697Test {\n\n\t@Test\n\tpublic void mapToMapTest(){\n\t\tfinal Map<String, String> mapA = new HashMap<>(16);\n\t\tmapA.put(\"12\", \"21\");\n\t\tmapA.put(\"121\", \"21\");\n\t\tmapA.put(\"122\", \"21\");\n\t\tfinal Map<String, String> mapB = new HashMap<>(16);\n\t\tBeanUtil.copyProperties(mapA, mapB, \"12\");\n\n\t\tassertEquals(2, mapB.size());\n\t\tassertFalse(mapB.containsKey(\"12\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue3091Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.bean;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class Issue3091Test {\n\n\t@Test\n\tpublic void copyToListTest() {\n\t\tfinal List<Long> list = Arrays.asList(1L,2L);\n\t\tfinal List<Integer> result = BeanUtil.copyToList(list, Integer.class);\n\t\tassertEquals(\"[1, 2]\", result.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue3452Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Issue3452Test {\n\n\t@Test\n\tpublic void fillBeanWithMapTest() {\n\t\tfinal Map<String, Object> properties = new HashMap<>();\n\t\tproperties.put(\"name\", \"JohnDoe\");\n\t\tproperties.put(\"user_age\", 25);\n\t\tfinal User user = BeanUtil.fillBeanWithMap(\n\t\t\tproperties, new User(), CopyOptions.create());\n\t\tassertEquals(\"JohnDoe\", user.getName());\n\t\tassertEquals(25, user.getUserAge());\n\t}\n\n\t@Data\n\tstatic class User {\n\t\tprivate String name;\n\t\tprivate int userAge;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue3497Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class Issue3497Test {\n\t@Test\n\tpublic void mapToMapTest() {\n\t\tfinal Map<String, String> aB = MapUtil.builder(\"a_b\", \"1\").build();\n\t\tfinal Map<?, ?> bean = BeanUtil.toBean(aB, Map.class, CopyOptions.create().setFieldNameEditor(StrUtil::toCamelCase));\n\t\tassertEquals(bean.toString(), \"{aB=1}\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue3645Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class Issue3645Test {\n\t@Test\n\tpublic void copyPropertiesTest() {\n\t\tUser p = new User();\n\t\tp.setUserId(123L);\n\n\t\tMap<Long, User> map = new HashMap<>();\n\t\tmap.put(123L,p);\n\n\t\tMap<Long, User> m = new HashMap<>();\n\t\tBeanUtil.copyProperties(map, m);\n\t\tUser u = m.get(123L);\n\t\tassertNotNull(u);\n\t}\n\n\t@Data\n\tstatic class User{\n\t\tprivate Long userId;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/Issue3702Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * setFieldValueEditor编辑后的值理应继续判断ignoreNullValue\n */\npublic class Issue3702Test {\n\t@Test\n\tvoid mapToMapTest() {\n\t\tMap<String,String> map= new HashMap<>();\n\t\tmap.put(\"a\",\"\");\n\t\tmap.put(\"b\",\"b\");\n\t\tmap.put(\"c\",\"c\");\n\t\tmap.put(\"d\",\"d\");\n\n\t\tMap<String,String> map2= new HashMap<>();\n\t\tmap2.put(\"a\",\"a1\");\n\t\tmap2.put(\"b\",\"b1\");\n\t\tmap2.put(\"c\",\"c1\");\n\t\tmap2.put(\"d\",\"d1\");\n\n\t\tCopyOptions option= CopyOptions.create()\n\t\t\t.setIgnoreNullValue(true)\n\t\t\t.setIgnoreError(true)\n\t\t\t.setFieldValueEditor((name, value)->{\n\t\t\tif(value.equals(\"\")){\n\t\t\t\tvalue=null;\n\t\t\t}\n\t\t\treturn value;\n\t\t});\n\t\tBeanUtil.copyProperties(map,map2,option);\n\t\tAssertions.assertEquals(\"{a=a1, b=b, c=c, d=d}\", map2.toString());\n\t\tSystem.out.println(map2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/IssueI80FP4Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI80FP4Test {\n\t@Test\n\tpublic void copyPropertiesTest() {\n\t\tfinal Dest sourceDest = new Dest();\n\t\tsourceDest.setCPF(33699);\n\t\tsourceDest.setEnderDest(\"abc\");\n\n\t\tfinal Dest dest = new Dest();\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().setIgnoreNullValue(true).setIgnoreCase(true).setIgnoreProperties(\"enderDest\");\n\t\tBeanUtil.copyProperties(sourceDest, dest, copyOptions);\n\t\tassertNull(dest.getEnderDest());\n\t}\n\n\t@Test\n\tpublic void copyPropertiesTest2() {\n\t\tfinal Dest sourceDest = new Dest();\n\t\tsourceDest.setCPF(33699);\n\t\tsourceDest.setEnderDest(\"abc\");\n\n\t\tfinal Dest dest = new Dest();\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().setIgnoreNullValue(true).setIgnoreCase(true).setIgnoreProperties(\"enderdest\");\n\t\tBeanUtil.copyProperties(sourceDest, dest, copyOptions);\n\t\tassertNull(dest.getEnderDest());\n\t}\n\n\t@Data\n\tstatic class Dest{\n\t\tprivate int cPF;\n\t\tprivate String enderDest;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/IssueI8JASOTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.annotation.Alias;\nimport lombok.Data;\nimport lombok.Setter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI8JASOTest {\n\n\t@Test\n\tpublic void copyTest() {\n\t\tfinal UserOne userOne = new UserOne();\n\t\tuserOne.setEmail(\"123@qq.com\");\n\t\tfinal UserTwo userTwo = new UserTwo();\n\t\tBeanUtil.copyProperties(userOne, userTwo);\n\t\tassertEquals(userOne.getEmail(), userTwo.getEmail());\n\t}\n\n\t@Data\n\tpublic static class UserOne {\n\t\tprivate Long id;\n\t\t@Alias(\"邮箱\")\n\t\tprivate String email;\n\t}\n\n\t@Data\n\tpublic static class UserTwo {\n\t\tprivate Long id;\n\t\t@Alias(\"邮箱\")\n\t\tprivate String email;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/IssueIAYGT0Test.java",
    "content": "package cn.hutool.core.bean;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIAYGT0Test {\n\n\t/**\n\t * BeanUtil.setProperty默认调用的是BeanPath方法，在设置值时，使用BeanUtil.setFieldValue这个方法。\n\t * 此方法默认直接给字段赋值。\n\t * 这里确实存在一定的歧义性，但是考虑到兼容性，不做处理。\n\t */\n\t@Test\n\tvoid setPropertyTest() {\n\t\tCat cat = new Cat();\n\t\tBeanUtil.setProperty(cat, \"name\", \"Kitty\");\n\t\tAssertions.assertEquals(\"Kitty\", cat.getName());\n\t}\n\n\tstatic class Cat {\n\t\tprivate String name;\n\n\t\tpublic void setName(String name) {\n\t\t\tthis.name = \"Red\" + name;\n\t\t}\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/IssueIBLTZWTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.convert.TypeConverter;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Field;\nimport java.util.Date;\n\n/**\n * 自定义某个字段的转换\n */\npublic class IssueIBLTZWTest {\n\t@Test\n\tpublic void copyTest() {\n\t\tTestBean bean = new TestBean();\n\t\tbean.setName(\"test\");\n\t\tbean.setDate(DateUtil.parse(\"2025-02-17\"));\n\n\t\tfinal TestBean2 testBean2 = new TestBean2();\n\t\tBeanUtil.copyProperties(bean, testBean2, createCopyOptions(TestBean2.class));\n\t\tAssertions.assertEquals(\"2025\", testBean2.getDate());\n\t}\n\n\t@Data\n\tstatic class TestBean {\n\t\tprivate String name;\n\t\tprivate Date date;\n\t}\n\n\t@Data\n\tstatic class TestBean2 {\n\t\tprivate String name;\n\t\tprivate String date;\n\t}\n\n\tstatic CopyOptions createCopyOptions(Class<?> targetClass) {\n\t\tCopyOptions copyOptions = CopyOptions.create();\n\t\tTypeConverter converter = (TypeConverter) ReflectUtil.getFieldValue(copyOptions, \"converter\");\n\t\tcopyOptions\n\t\t\t.setIgnoreError(true) // 忽略类型错误，避免自动转换\n\t\t\t.setConverter(null)\n\t\t\t.setFieldValueEditor((fieldName, value) -> {\n\t\t\t\ttry {\n\t\t\t\t\tField targetField = targetClass.getDeclaredField(fieldName);\n\t\t\t\t\t// Date类型的 value instanceof 结果是String\n\t\t\t\t\tif (targetField.getType() == String.class && value instanceof Date) {\n\t\t\t\t\t\treturn DateUtil.format((Date)value, \"yyyy\");\n\t\t\t\t\t}\n\t\t\t\t\treturn converter.convert(targetField.getType(), value);\n\t\t\t\t} catch (NoSuchFieldException e) {\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t});\n\t\treturn copyOptions;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/IssueICHM3OTest.java",
    "content": "package cn.hutool.core.bean;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class IssueICHM3OTest {\n\t@Test\n\tpublic void testMapToBean() {\n\t\tMap<Object,Object> map = MapUtil.builder()\n\t\t\t.put(\"doctor_name\", \"李医生\")\n\t\t\t.put(\"doctor_id_card_value\", \"12345\")\n\t\t\t.put(\"gender\", \"男\")\n\t\t\t.build();\n\t\tTestClass doctor = BeanUtil.toBean(map, TestClass.class);\n\t\tassertTrue(StrUtil.equals(doctor.name, \"李医生\"), \"姓名不一致\");\n\t\tassertTrue(StrUtil.equals(doctor.idCardValue, \"12345\"), \"证件号不一致\");\n\n\n\t\tMap<String,Object> mapData = BeanUtil.beanToMap(doctor, true, false);\n\t\tassertTrue(StrUtil.equals(mapData.get(\"doctor_name\").toString(), \"李医生\"), \"姓名不一致\");\n\t\tassertTrue(StrUtil.equals(mapData.get(\"doctor_id_card_value\").toString(), \"12345\"), \"证件号不一致\");\n\t}\n\n\t@Setter\n\t@Getter\n\tpublic static class TestClass {\n\t\t@Alias(\"doctor_name\")\n\t\tprivate String name;\n\t\t@Alias(\"doctor_id_card_value\")\n\t\tprivate String idCardValue;\n\t\t@Alias(\"doctor_name\")\n\t\tprivate String gender;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\npublic class BeanCopierTest {\n\n\t@Test\n\tpublic void beanToMapIgnoreNullTest() {\n\t\tfinal A a = new A();\n\n\t\tHashMap<Object, Object> map = BeanCopier.create(a, new HashMap<>(), CopyOptions.create()).copy();\n\t\tassertEquals(1, map.size());\n\t\tassertTrue(map.containsKey(\"value\"));\n\t\tassertNull(map.get(\"value\"));\n\n\t\t// 忽略null的情况下，空字段不写入map\n\t\tmap = BeanCopier.create(a, new HashMap<>(), CopyOptions.create().ignoreNullValue()).copy();\n\t\tassertFalse(map.containsKey(\"value\"));\n\t\tassertEquals(0, map.size());\n\t}\n\n\t/**\n\t * 测试在非覆盖模式下，目标对象有值则不覆盖\n\t */\n\t@Test\n\tpublic void beanToBeanNotOverrideTest() {\n\t\tfinal A a = new A();\n\t\ta.setValue(\"123\");\n\t\tfinal B b = new B();\n\t\tb.setValue(\"abc\");\n\n\t\tfinal BeanCopier<B> copier = BeanCopier.create(a, b, CopyOptions.create().setOverride(false));\n\t\tcopier.copy();\n\n\t\tassertEquals(\"abc\", b.getValue());\n\t}\n\n\t/**\n\t * 测试在覆盖模式下，目标对象值被覆盖\n\t */\n\t@Test\n\tpublic void beanToBeanOverrideTest() {\n\t\tfinal A a = new A();\n\t\ta.setValue(\"123\");\n\t\tfinal B b = new B();\n\t\tb.setValue(\"abc\");\n\n\t\tfinal BeanCopier<B> copier = BeanCopier.create(a, b, CopyOptions.create());\n\t\tcopier.copy();\n\n\t\tassertEquals(\"123\", b.getValue());\n\t}\n\n\t/**\n\t * 为{@code null}则写，否则忽略。如果覆盖，则不判断直接写\n\t */\n\t@Test\n\tpublic void issues2484Test() {\n\t\tfinal A a = new A();\n\t\ta.setValue(\"abc\");\n\t\tfinal B b = new B();\n\t\tb.setValue(\"123\");\n\n\t\tBeanCopier<B> copier = BeanCopier.create(a, b, CopyOptions.create().setOverride(false));\n\t\tcopier.copy();\n\t\tassertEquals(\"123\", b.getValue());\n\n\t\tb.setValue(null);\n\t\tcopier = BeanCopier.create(a, b, CopyOptions.create().setOverride(false));\n\t\tcopier.copy();\n\t\tassertEquals(\"abc\", b.getValue());\n\t}\n\n\t@Data\n\tprivate static class A {\n\t\tprivate String value;\n\t}\n\n\t@Data\n\tprivate static class B {\n\t\tprivate String value;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/bean/copier/Issue2718Test.java",
    "content": "package cn.hutool.core.bean.copier;\n\nimport lombok.Setter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.LinkedHashMap;\n\npublic class Issue2718Test {\n\n\t@Test\n\tpublic void copyToMapTest(){\n\t\tfinal Deployment deployment = new Deployment();\n\t\tdeployment.setResources(\"test\");\n\t\tfinal LinkedHashMap<String, Object> target = BeanCopier\n\t\t\t\t.create(deployment, new LinkedHashMap<String, Object>(), CopyOptions.create().setIgnoreProperties(\"resources\"))\n\t\t\t\t.copy();\n\n\t\tassertTrue(target.isEmpty());\n\t}\n\n\t@Test\n\tpublic void copyToBeanTest(){\n\t\tfinal Deployment deployment = new Deployment();\n\t\tdeployment.setResources(\"test\");\n\t\tfinal Deployment target = BeanCopier\n\t\t\t\t.create(deployment, new Deployment(), CopyOptions.create().setIgnoreProperties(\"resources\"))\n\t\t\t\t.copy();\n\n\t\tassertNull(target.resources);\n\t}\n\n\t@Setter\n\tprivate static class Deployment{\n\t\tpublic String getResources() {\n\t\t\t// setIgnoreProperties会被转换为propertiesFilter，这个filter是过滤键和值的，因此会获取源对象的值（调用getXXX方法），然后做判断。因此此方法会被执行\n\t\t\tthrow new RuntimeException(\"这个方法不应该被调用\");\n\t\t\t//return resources;\n\t\t}\n\n\t\tprivate String resources;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/builder/GenericBuilderTest.java",
    "content": "package cn.hutool.core.builder;\n\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\nimport lombok.experimental.Accessors;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * {@link GenericBuilder} 单元测试类\n *\n * @author TomXin\n */\npublic class GenericBuilderTest {\n\n\t@Test\n\tpublic void test() {\n\t\tBox box = GenericBuilder\n\t\t\t\t.of(Box::new)\n\t\t\t\t.with(Box::setId, 1024L)\n\t\t\t\t.with(Box::setTitle, \"Hello World!\")\n\t\t\t\t.with(Box::setLength, 9)\n\t\t\t\t.with(Box::setWidth, 8)\n\t\t\t\t.with(Box::setHeight, 7)\n\t\t\t\t.build();\n\n\t\tassertEquals(1024L, box.getId().longValue());\n\t\tassertEquals(\"Hello World!\", box.getTitle());\n\t\tassertEquals(9, box.getLength().intValue());\n\t\tassertEquals(8, box.getWidth().intValue());\n\t\tassertEquals(7, box.getHeight().intValue());\n\n\t\t// 对象修改\n\t\tBox boxModified = GenericBuilder\n\t\t\t\t.of(() -> box)\n\t\t\t\t.with(Box::setTitle, \"Hello Friend!\")\n\t\t\t\t.with(Box::setLength, 3)\n\t\t\t\t.with(Box::setWidth, 4)\n\t\t\t\t.with(Box::setHeight, 5)\n\t\t\t\t.build();\n\n\t\tassertEquals(1024L, boxModified.getId().longValue());\n\t\tassertEquals(\"Hello Friend!\", box.getTitle());\n\t\tassertEquals(3, boxModified.getLength().intValue());\n\t\tassertEquals(4, boxModified.getWidth().intValue());\n\t\tassertEquals(5, boxModified.getHeight().intValue());\n\n\t\t// 多参数构造\n\t\tBox box1 = GenericBuilder\n\t\t\t\t.of(Box::new, 2048L, \"Hello Partner!\", 222, 333, 444)\n\t\t\t\t.with(Box::alis)\n\t\t\t\t.build();\n\n\t\tassertEquals(2048L, box1.getId().longValue());\n\t\tassertEquals(\"Hello Partner!\", box1.getTitle());\n\t\tassertEquals(222, box1.getLength().intValue());\n\t\tassertEquals(333, box1.getWidth().intValue());\n\t\tassertEquals(444, box1.getHeight().intValue());\n\t\tassertEquals(\"TomXin:\\\"Hello Partner!\\\"\", box1.getTitleAlias());\n\t}\n\n\t@Test\n\tpublic void buildMapTest(){\n\t\t//Map创建\n\t\tHashMap<String, String> colorMap = GenericBuilder\n\t\t\t\t.of(HashMap<String,String>::new)\n\t\t\t\t.with(Map::put, \"red\", \"#FF0000\")\n\t\t\t\t.with(Map::put, \"yellow\", \"#FFFF00\")\n\t\t\t\t.with(Map::put, \"blue\", \"#0000FF\")\n\t\t\t\t.build();\n\t\tassertEquals(\"#FF0000\", colorMap.get(\"red\"));\n\t\tassertEquals(\"#FFFF00\", colorMap.get(\"yellow\"));\n\t\tassertEquals(\"#0000FF\", colorMap.get(\"blue\"));\n\t}\n\n\t@Getter\n\t@Setter\n\t@ToString\n\t@Accessors(chain = true)\n\tpublic static class Box {\n\t\tprivate Long id;\n\t\tprivate String title;\n\t\tprivate Integer length;\n\t\tprivate Integer width;\n\t\tprivate Integer height;\n\t\tprivate String titleAlias;\n\n\t\tpublic Box() {\n\t\t}\n\n\t\tpublic Box(Long id, String title, Integer length, Integer width, Integer height) {\n\t\t\tthis.id = id;\n\t\t\tthis.title = title;\n\t\t\tthis.length = length;\n\t\t\tthis.width = width;\n\t\t\tthis.height = height;\n\t\t}\n\n\t\tpublic void alis() {\n\t\t\tif (StrUtil.isNotBlank(this.title)) {\n\t\t\t\tthis.titleAlias = \"TomXin:\\\"\" + title + \"\\\"\";\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/clone/CloneTest.java",
    "content": "package cn.hutool.core.clone;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 克隆单元测试\n * @author Looly\n *\n */\npublic class CloneTest {\n\n\t@Test\n\tpublic void cloneTest(){\n\n\t\t//实现Cloneable接口\n\t\tCat cat = new Cat();\n\t\tCat cat2 = cat.clone();\n\t\tassertEquals(cat, cat2);\n\t}\n\n\t@Test\n\tpublic void cloneTest2(){\n\t\t//继承CloneSupport类\n\t\tDog dog = new Dog();\n\t\tDog dog2 = dog.clone();\n\t\tassertEquals(dog, dog2);\n\t}\n\n\t//------------------------------------------------------------------------------- private Class for test\n\t/**\n\t * 猫猫类，使用实现Cloneable方式\n\t * @author Looly\n\t *\n\t */\n\t@Data\n\tstatic class Cat implements Cloneable<Cat>{\n\t\tprivate String name = \"miaomiao\";\n\t\tprivate int age = 2;\n\n\t\t@Override\n\t\tpublic Cat clone() {\n\t\t\ttry {\n\t\t\t\treturn (Cat) super.clone();\n\t\t\t} catch (CloneNotSupportedException e) {\n\t\t\t\tthrow new CloneRuntimeException(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 狗狗类，用于继承CloneSupport类\n\t * @author Looly\n\t *\n\t */\n\t@EqualsAndHashCode(callSuper = false)\n\t@Data\n\tstatic class Dog extends CloneSupport<Dog>{\n\t\tprivate String name = \"wangwang\";\n\t\tprivate int age = 3;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/clone/DefaultCloneTest.java",
    "content": "package cn.hutool.core.clone;\n\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class DefaultCloneTest {\n\n\t@Test\n\tpublic void clone0() {\n\t\tCar oldCar = new Car();\n\t\toldCar.setId(1);\n\t\toldCar.setWheelList(Stream.of(new Wheel(\"h\")).collect(Collectors.toList()));\n\n\t\tCar newCar = oldCar.clone0();\n\t\tassertEquals(oldCar.getId(), newCar.getId());\n\t\tassertEquals(oldCar.getWheelList(), newCar.getWheelList());\n\n\t\tnewCar.setId(2);\n\t\tassertNotEquals(oldCar.getId(), newCar.getId());\n\t\tnewCar.getWheelList().add(new Wheel(\"s\"));\n\n\t\tassertNotSame(oldCar, newCar);\n\n\t}\n\n\t@Data\n\tstatic class Car implements DefaultCloneable<Car> {\n\t\tprivate Integer id;\n\t\tprivate List<Wheel> wheelList;\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class Wheel {\n\t\tprivate String direction;\n\t}\n\n}\n\n\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/BCDTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BCDTest {\n\n\t@Test\n\tpublic void bcdTest(){\n\t\tfinal String strForTest = \"123456ABCDEF\";\n\n\t\t//转BCD\n\t\tfinal byte[] bcd = BCD.strToBcd(strForTest);\n\t\tfinal String str = BCD.bcdToStr(bcd);\n\t\t//解码BCD\n\t\tassertEquals(strForTest, str);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Base32Test {\n\n\t@Test\n\tpublic void encodeAndDecodeTest(){\n\t\tString a = \"伦家是一个非常长的字符串\";\n\t\tString encode = Base32.encode(a);\n\t\tassertEquals(\"4S6KNZNOW3TJRL7EXCAOJOFK5GOZ5ZNYXDUZLP7HTKCOLLMX46WKNZFYWI======\", encode);\n\n\t\tString decodeStr = Base32.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\n\t\t// 支持小写模式解码\n\t\tdecodeStr = Base32.decodeStr(encode.toLowerCase());\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void hexEncodeAndDecodeTest(){\n\t\tString a = \"伦家是一个非常长的字符串\";\n\t\tString encode = Base32.encodeHex(StrUtil.utf8Bytes(a));\n\t\tassertEquals(\"SIUADPDEMRJ9HBV4N20E9E5AT6EPTPDON3KPBFV7JA2EBBCNSUMADP5OM8======\", encode);\n\n\t\tString decodeStr = Base32.decodeStrHex(encode);\n\t\tassertEquals(a, decodeStr);\n\n\t\t// 支持小写模式解码\n\t\tdecodeStr = Base32.decodeStrHex(encode.toLowerCase());\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeRandomTest(){\n\t\tString a = RandomUtil.randomString(RandomUtil.randomInt(1000));\n\t\tString encode = Base32.encode(a);\n\t\tString decodeStr = Base32.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void decodeTest(){\n\t\tString a = \"伦家是一个非常长的字符串\";\n\t\tString decodeStr = Base32.decodeStr(\"4S6KNZNOW3TJRL7EXCAOJOFK5GOZ5ZNYXDUZLP7HTKCOLLMX46WKNZFYWI\");\n\t\tassertEquals(a, decodeStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class Base58Test {\n\n\t@Test\n\tpublic void encodeCheckedTest() {\n\t\tString a = \"hello world\";\n\t\tString encode = Base58.encodeChecked(0, a.getBytes());\n\t\tassertEquals(1 + \"3vQB7B6MrGQZaxCuFg4oh\", encode);\n\n\t\t// 无版本位\n\t\tencode = Base58.encodeChecked(null, a.getBytes());\n\t\tassertEquals(\"3vQB7B6MrGQZaxCuFg4oh\", encode);\n\t}\n\t@Test\n\tpublic void encodeTest() {\n\t\tString a = \"hello world\";\n\t\tString encode = Base58.encode(a.getBytes(StandardCharsets.UTF_8));\n\t\tassertEquals(\"StV1DL6CwTryKyV\", encode);\n\t}\n\t@Test\n\tpublic void decodeCheckedTest() {\n\t\tString a = \"3vQB7B6MrGQZaxCuFg4oh\";\n\t\tbyte[] decode = Base58.decodeChecked(1 + a);\n\t\tassertArrayEquals(\"hello world\".getBytes(StandardCharsets.UTF_8),decode);\n\t\tdecode = Base58.decodeChecked(a);\n\t\tassertArrayEquals(\"hello world\".getBytes(StandardCharsets.UTF_8),decode);\n\t}\n\t@Test\n\tpublic void testDecode()  {\n\t\tString a = \"StV1DL6CwTryKyV\";\n\t\tbyte[] decode = Base58.decode(a);\n\t\tassertArrayEquals(\"hello world\".getBytes(StandardCharsets.UTF_8),decode);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Base62单元测试\n *\n * @author looly\n *\n */\npublic class Base62Test {\n\n\t@Test\n\tpublic void encodeAndDecodeTest() {\n\t\tString a = \"伦家是一个非常长的字符串66\";\n\t\tString encode = Base62.encode(a);\n\t\tassertEquals(\"17vKU8W4JMG8dQF8lk9VNnkdMOeWn4rJMva6F0XsLrrT53iKBnqo\", encode);\n\n\t\tString decodeStr = Base62.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeInvertedTest() {\n\t\tString a = \"伦家是一个非常长的字符串66\";\n\t\tString encode = Base62.encodeInverted(a);\n\t\tassertEquals(\"17Vku8w4jmg8Dqf8LK9vnNKDmoEwN4RjmVA6f0xSlRRt53IkbNQO\", encode);\n\n\t\tString decodeStr = Base62.decodeStrInverted(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeRandomTest() {\n\t\tString a = RandomUtil.randomString(RandomUtil.randomInt(1000));\n\t\tString encode = Base62.encode(a);\n\t\tString decodeStr = Base62.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeInvertedRandomTest() {\n\t\tString a = RandomUtil.randomString(RandomUtil.randomInt(1000));\n\t\tString encode = Base62.encodeInverted(a);\n\t\tString decodeStr = Base62.decodeStrInverted(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/Base64Test.java",
    "content": "package cn.hutool.core.codec;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Base64单元测试\n *\n * @author looly\n *\n */\npublic class Base64Test {\n\n\t@Test\n\tpublic void isBase64Test(){\n\t\tassertTrue(Base64.isBase64(Base64.encode(RandomUtil.randomString(1000))));\n\t}\n\n\t@Test\n\tpublic void isBase64Test2(){\n\t\tString base64 = \"dW1kb3MzejR3bmljM2J6djAyZzcwbWk5M213Nnk3cWQ3eDJwOHFuNXJsYmMwaXhxbmg0dmxrcmN0anRkbmd3\\n\" +\n\t\t\t\t\"ZzcyZWFwanI2NWNneTg2dnp6cmJoMHQ4MHpxY2R6c3pjazZtaQ==\";\n\t\tassertTrue(Base64.isBase64(base64));\n\n\t\t// '=' 不位于末尾\n\t\tbase64 = \"dW1kb3MzejR3bmljM2J6=djAyZzcwbWk5M213Nnk3cWQ3eDJwOHFuNXJsYmMwaXhxbmg0dmxrcmN0anRkbmd3\\n\" +\n\t\t\t\t\"ZzcyZWFwanI2NWNneTg2dnp6cmJoMHQ4MHpxY2R6c3pjazZtaQ=\";\n\t\tassertFalse(Base64.isBase64(base64));\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeTest() {\n\t\tString a = \"伦家是一个非常长的字符串66\";\n\t\tString encode = Base64.encode(a);\n\t\tassertEquals(\"5Lym5a625piv5LiA5Liq6Z2e5bi46ZW/55qE5a2X56ym5LiyNjY=\", encode);\n\n\t\tString decodeStr = Base64.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeWithoutPaddingTest() {\n\t\tString a = \"伦家是一个非常长的字符串66\";\n\t\tString encode = Base64.encodeWithoutPadding(StrUtil.utf8Bytes(a));\n\t\tassertEquals(\"5Lym5a625piv5LiA5Liq6Z2e5bi46ZW/55qE5a2X56ym5LiyNjY\", encode);\n\n\t\tString decodeStr = Base64.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeTest2() {\n\t\tString a = \"a61a5db5a67c01445ca2-HZ20181120172058/pdf/中国电信影像云单体网关Docker版-V1.2.pdf\";\n\t\tString encode = Base64.encode(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"YTYxYTVkYjVhNjdjMDE0NDVjYTItSFoyMDE4MTEyMDE3MjA1OC9wZGYv5Lit5Zu955S15L+h5b2x5YOP5LqR5Y2V5L2T572R5YWzRG9ja2Vy54mILVYxLjIucGRm\", encode);\n\n\t\tString decodeStr = Base64.decodeStr(encode, CharsetUtil.UTF_8);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeTest3() {\n\t\tString a = \":\";\n\t\tString encode = Base64.encode(a);\n\t\tassertEquals(\"Og==\", encode);\n\n\t\tString decodeStr = Base64.decodeStr(encode);\n\t\tassertEquals(a, decodeStr);\n\t}\n\n\t@Test\n\tpublic void encodeAndDecodeGbkTest(){\n\t\tString orderDescription = \"订购成功立即生效，30天内可观看专区中除单独计费影片外的所有内容，到期自动取消。\";\n\t\tString result = Base64.encode(orderDescription, \"gbk\");\n\n\t\tfinal String s = Base64.decodeStr(result, \"gbk\");\n\t\tassertEquals(orderDescription, s);\n\t}\n\n\t@Test\n\tpublic void decodeEmojiTest(){\n\t\tString str = \"😄\";\n\t\tfinal String encode = Base64.encode(str);\n//\t\tConsole.log(encode);\n\n\t\tfinal String decodeStr = Base64.decodeStr(encode);\n\t\tassertEquals(str, decodeStr);\n\t}\n\n\t@Test\n\tpublic void issuesI5QR4WTest(){\n\t\tString a = java.util.Base64.getEncoder().encodeToString(\"111\".getBytes()); //java.util.Base64\n\t\tString b = Base64.encode(\"111\"); //cn.hutool.core.codec.Base64\n\n\t\tassertEquals(a, b);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/CaesarTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CaesarTest {\n\n\t@Test\n\tpublic void caesarTest() {\n\t\tString str = \"1f2e9df6131b480b9fdddc633cf24996\";\n\n\t\tString encode = Caesar.encode(str, 3);\n\t\tassertEquals(\"1H2G9FH6131D480D9HFFFE633EH24996\", encode);\n\n\t\tString decode = Caesar.decode(encode, 3);\n\t\tassertEquals(str, decode);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/HashidsTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class HashidsTest {\n\t@Test\n\tpublic void hexEncodeDecode() {\n\t\tfinal Hashids hashids = Hashids.create(\"my awesome salt\".toCharArray());\n\t\tfinal String encoded1 = hashids.encodeFromHex(\"507f1f77bcf86cd799439011\");\n\t\tfinal String encoded2 = hashids.encodeFromHex(\"0x507f1f77bcf86cd799439011\");\n\t\tfinal String encoded3 = hashids.encodeFromHex(\"0X507f1f77bcf86cd799439011\");\n\n\t\tassertEquals(\"R2qnd2vkOJTXm7XV7yq4\", encoded1);\n\t\tassertEquals(encoded1, encoded2);\n\t\tassertEquals(encoded1, encoded3);\n\t\tfinal String decoded = hashids.decodeToHex(encoded1);\n\t\tassertEquals(\"507f1f77bcf86cd799439011\", decoded);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/MorseTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class MorseTest {\n\n\tprivate final Morse morseCoder = new Morse();\n\n\t@Test\n\tpublic void test0() {\n\t\tString text = \"Hello World!\";\n\t\tString morse = \"...././.-../.-../---/-...../.--/---/.-./.-../-../-.-.--/\";\n\t\tassertEquals(morse, morseCoder.encode(text));\n\t\tassertEquals(morseCoder.decode(morse), text.toUpperCase());\n\t}\n\n\t@Test\n\tpublic void test1() {\n\t\tString text = \"你好，世界！\";\n\t\tString morse = \"-..----.--...../-.--..-.-----.-/--------....--../-..---....-.--./---.-.-.-..--../--------.......-/\";\n\t\tassertEquals(morseCoder.encode(text), morse);\n\t\tassertEquals(morseCoder.decode(morse), text);\n\t}\n\n\t@Test\n\tpublic void test2() {\n\t\tString text = \"こんにちは\";\n\t\tString morse = \"--.....-.-..--/--....-..-..--/--.....--.-.--/--.....--....-/--.....--.----/\";\n\t\tassertEquals(morseCoder.encode(text), morse);\n\t\tassertEquals(morseCoder.decode(morse), text);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/PunyCodeTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class PunyCodeTest {\n\n\t@Test\n\tpublic void encodeDecodeTest(){\n\t\tfinal String text = \"Hutool编码器\";\n\t\tfinal String strPunyCode = PunyCode.encode(text);\n\t\tassertEquals(\"Hutool-ux9js33tgln\", strPunyCode);\n\t\tString decode = PunyCode.decode(\"Hutool-ux9js33tgln\");\n\t\tassertEquals(text, decode);\n\t\tdecode = PunyCode.decode(\"xn--Hutool-ux9js33tgln\");\n\t\tassertEquals(text, decode);\n\t}\n\n\t@Test\n\tpublic void encodeDecodeTest2(){\n\t\t// 无需编码和解码\n\t\tfinal String text = \"Hutool\";\n\t\tfinal String strPunyCode = PunyCode.encode(text);\n\t\tassertEquals(\"Hutool\", strPunyCode);\n\t}\n\n\t@Test\n\tpublic void encodeEncodeDomainTest(){\n\t\tfinal String domain = \"赵新虎.中国\";\n\t\tfinal String strPunyCode = PunyCode.encodeDomain(domain);\n\t\tfinal String decode = PunyCode.decodeDomain(strPunyCode);\n\t\tassertEquals(decode, domain);\n\t}\n\n\t@Test\n\tpublic void encodeEncodeDomainTest2(){\n\t\tfinal String domain = \"赵新虎.com\";\n\t\tfinal String strPunyCode = PunyCode.encodeDomain(domain);\n\t\tassertEquals(\"xn--efvz93e52e.com\", strPunyCode);\n\t\tfinal String decode = PunyCode.decodeDomain(strPunyCode);\n\t\tassertEquals(domain, decode);\n\t}\n\n\t@Test\n\tpublic void encodeEncodeDomainTest3(){\n\t\tfinal String domain = \"赵新虎.COM\";\n\t\tfinal String strPunyCode = PunyCode.encodeDomain(domain);\n\t\tassertEquals(\"xn--efvz93e52e.COM\", strPunyCode);\n\t\tfinal String decode = PunyCode.decodeDomain(strPunyCode);\n\t\tassertEquals(domain, decode);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/codec/RotTest.java",
    "content": "package cn.hutool.core.codec;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class RotTest {\n\n\t@Test\n\tpublic void rot13Test() {\n\t\tString str = \"1f2e9df6131b480b9fdddc633cf24996\";\n\n\t\tString encode13 = Rot.encode13(str);\n\t\tassertEquals(\"4s5r2qs9464o713o2sqqqp966ps57229\", encode13);\n\n\t\tString decode13 = Rot.decode13(encode13);\n\t\tassertEquals(str, decode13);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/CollStreamUtilTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.map.MapUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.ToString;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\nimport java.util.stream.Collector;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * CollectionStream测试方法\n */\npublic class CollStreamUtilTest {\n\n\t@Test\n\tpublic void testToIdentityMap() {\n\t\tMap<Long, Student> map = CollStreamUtil.toIdentityMap(null, Student::getStudentId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.toIdentityMap(list, Student::getStudentId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 1, 2, \"李四\"));\n\t\tlist.add(new Student(1, 1, 3, \"王五\"));\n\t\tmap = CollStreamUtil.toIdentityMap(list, Student::getStudentId);\n\t\tassertEquals(\"张三\", map.get(1L).getName());\n\t\tassertEquals(\"李四\", map.get(2L).getName());\n\t\tassertEquals(\"王五\", map.get(3L).getName());\n\t\tassertNull(map.get(4L));\n\n\t\t// 测试value为空时\n\t\tlist.add(null);\n\t\tmap = CollStreamUtil.toIdentityMap(list, Student::getStudentId);\n\t\tassertNull(map.get(4L));\n\t}\n\n\t@Test\n\tpublic void testToMap() {\n\t\tMap<Long, String> map = CollStreamUtil.toMap(null, Student::getStudentId, Student::getName);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 1, 2, \"李四\"));\n\t\tlist.add(new Student(1, 1, 3, \"王五\"));\n\t\tmap = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);\n\t\tassertEquals(\"张三\", map.get(1L));\n\t\tassertEquals(\"李四\", map.get(2L));\n\t\tassertEquals(\"王五\", map.get(3L));\n\t\tassertNull(map.get(4L));\n\n\t\t// 测试value为空时\n\t\tlist.add(new Student(1, 1, 4, null));\n\t\tmap = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);\n\t\tassertNull(map.get(4L));\n\t}\n\n\t@Test\n\tpublic void testToMap_KeyCollision_SilentlyOverwrite() {\n\t\tList<Student> list = new ArrayList<>();\n\t\tlist.add(new Student(1, 101, 1, \"张三\"));\n\t\tlist.add(new Student(1, 102, 1, \"李四\"));\n\t\tMap<Long, String> map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName, false);\n\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(\"李四\", map.get(1L));   // 确保后面的值覆盖前面的\n\t}\n\n\t@Test\n\tpublic void testToMap_NullKeyOrValue() {\n\t\tList<Student> list = new ArrayList<>();\n\t\tlist.add(new Student(1, 1, 1L, \"张三\"));\n\t\tlist.add(null);\n\t\tlist.add(new Student(1, 2, 2L, null));\n\n\t\tassertThrows(NullPointerException.class, () -> {\n\t\t\tCollStreamUtil.toMap(list, Student::getStudentId, Student::getName);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void testToMap_LargeInputPerformance() {\n\t\tList<Student> list = new ArrayList<>();\n\t\tfor (long i = 0; i < 10000; i++) {\n\t\t\tlist.add(new Student(1, 1, i, \"学生\" + i));\n\t\t}\n\t\tMap<Long, String> map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);\n\n\t\tassertEquals(10000, map.size());\n\t}\n\n\t@Test\n\tpublic void testGroupByKey() {\n\t\tMap<Long, List<Student>> map = CollStreamUtil.groupByKey(null, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.groupByKey(list, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 2, 2, \"李四\"));\n\t\tlist.add(new Student(2, 1, 1, \"擎天柱\"));\n\t\tlist.add(new Student(2, 2, 2, \"威震天\"));\n\t\tlist.add(new Student(2, 3, 2, \"霸天虎\"));\n\t\tmap = CollStreamUtil.groupByKey(list, Student::getClassId);\n\t\tMap<Long, List<Student>> compare = new HashMap<>();\n\t\tList<Student> class1 = new ArrayList<>();\n\t\tclass1.add(new Student(1, 1, 1, \"张三\"));\n\t\tclass1.add(new Student(2, 1, 1, \"擎天柱\"));\n\t\tcompare.put(1L, class1);\n\t\tList<Student> class2 = new ArrayList<>();\n\t\tclass2.add(new Student(1, 2, 2, \"李四\"));\n\t\tclass2.add(new Student(2, 2, 2, \"威震天\"));\n\n\t\tcompare.put(2L, class2);\n\t\tList<Student> class3 = new ArrayList<>();\n\t\tclass3.add(new Student(2, 3, 2, \"霸天虎\"));\n\t\tcompare.put(3L, class3);\n\t\tassertEquals(map, compare);\n\t}\n\n\t@Test\n\tpublic void testGroupBy2Key() {\n\t\tMap<Long, Map<Long, List<Student>>> map = CollStreamUtil.groupBy2Key(null, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.groupBy2Key(list, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 2, 2, \"李四\"));\n\t\tlist.add(new Student(1, 2, 3, \"王五\"));\n\t\tlist.add(new Student(2, 1, 1, \"擎天柱\"));\n\t\tlist.add(new Student(2, 2, 2, \"威震天\"));\n\t\tlist.add(new Student(2, 2, 3, \"霸天虎\"));\n\t\tmap = CollStreamUtil.groupBy2Key(list, Student::getTermId, Student::getClassId);\n\t\tMap<Long, Map<Long, List<Student>>> compare = new HashMap<>();\n\t\tMap<Long, List<Student>> map1 = new HashMap<>();\n\t\tList<Student> list11 = new ArrayList<>();\n\t\tlist11.add(new Student(1, 1, 1, \"张三\"));\n\t\tmap1.put(1L, list11);\n\t\tcompare.put(1L, map1);\n\t\tList<Student> list12 = new ArrayList<>();\n\t\tlist12.add(new Student(1, 2, 2, \"李四\"));\n\t\tlist12.add(new Student(1, 2, 3, \"王五\"));\n\t\tmap1.put(2L, list12);\n\t\tcompare.put(2L, map1);\n\t\tMap<Long, List<Student>> map2 = new HashMap<>();\n\t\tList<Student> list21 = new ArrayList<>();\n\t\tlist21.add(new Student(2, 1, 1, \"擎天柱\"));\n\t\tmap2.put(1L, list21);\n\t\tcompare.put(2L, map2);\n\n\t\tList<Student> list22 = new ArrayList<>();\n\t\tlist22.add(new Student(2, 2, 2, \"威震天\"));\n\t\tlist22.add(new Student(2, 2, 3, \"霸天虎\"));\n\t\tmap2.put(2L, list22);\n\t\tcompare.put(2L, map2);\n\t\tassertEquals(map, compare);\n\t}\n\n\t@Test\n\tpublic void testGroup2Map() {\n\t\tMap<Long, Map<Long, Student>> map = CollStreamUtil.group2Map(null, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.group2Map(list, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 2, 1, \"李四\"));\n\t\tlist.add(new Student(2, 2, 1, \"王五\"));\n\t\tmap = CollStreamUtil.group2Map(list, Student::getTermId, Student::getClassId);\n\t\tMap<Long, Map<Long, Student>> compare = new HashMap<>();\n\t\tMap<Long, Student> map1 = new HashMap<>();\n\t\tmap1.put(1L, new Student(1, 1, 1, \"张三\"));\n\t\tmap1.put(2L, new Student(1, 2, 1, \"李四\"));\n\t\tcompare.put(1L, map1);\n\t\tMap<Long, Student> map2 = new HashMap<>();\n\t\tmap2.put(2L, new Student(2, 2, 1, \"王五\"));\n\t\tcompare.put(2L, map2);\n\t\tassertEquals(compare, map);\n\n\t\t// 对null友好\n\t\tMap<Long, Map<Long, Student>> termIdClassIdStudentMap = CollStreamUtil.group2Map(Arrays.asList(null, new Student(2, 2, 1, \"王五\")), Student::getTermId, Student::getClassId);\n\t\tMap<Long, Map<Long, Student>> termIdClassIdStudentCompareMap = new HashMap<Long, Map<Long, Student>>() {\n\t\t\tprivate static final long serialVersionUID = -26683057599474572L;\n\n\t\t\t{\n\t\t\tput(null, MapUtil.of(null, null));\n\t\t\tput(2L, MapUtil.of(2L, new Student(2, 2, 1, \"王五\")));\n\t\t}};\n\t\tassertEquals(termIdClassIdStudentCompareMap, termIdClassIdStudentMap);\n\t}\n\n\t@Test\n\tpublic void testGroupKeyValue() {\n\t\tMap<Long, List<Long>> map = CollStreamUtil.groupKeyValue(null, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.groupKeyValue(list, Student::getTermId, Student::getClassId);\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 2, 1, \"李四\"));\n\t\tlist.add(new Student(2, 2, 1, \"王五\"));\n\t\tmap = CollStreamUtil.groupKeyValue(list, Student::getTermId, Student::getClassId);\n\n\t\tMap<Long, List<Long>> compare = new HashMap<>();\n\t\tcompare.put(1L, Arrays.asList(1L, 2L));\n\t\tcompare.put(2L, Collections.singletonList(2L));\n\t\tassertEquals(compare, map);\n\t}\n\n\t@Test\n\tpublic void testGroupBy() {\n\t\t// groupBy作为之前所有group函数的公共部分抽取出来，并更接近于jdk原生，灵活性更强\n\n\t\t// 参数null测试\n\t\tMap<Long, List<Student>> map = CollStreamUtil.groupBy(null, Student::getTermId, Collectors.toList());\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\n\t\t// 参数空数组测试\n\t\tList<Student> list = new ArrayList<>();\n\t\tmap = CollStreamUtil.groupBy(list, Student::getTermId, Collectors.toList());\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\n\t\t// 放入元素\n\t\tlist.add(new Student(1, 1, 1, \"张三\"));\n\t\tlist.add(new Student(1, 2, 1, \"李四\"));\n\t\tlist.add(new Student(2, 2, 1, \"王五\"));\n\t\t// 先根据termId分组，再通过classId比较，找出最大值所属的那个Student,返回的Optional\n\t\tMap<Long, Optional<Student>> longOptionalMap = CollStreamUtil.groupBy(list, Student::getTermId, Collectors.maxBy(Comparator.comparing(Student::getClassId)));\n\t\t//noinspection OptionalGetWithoutIsPresent\n\t\tassertEquals(\"李四\", longOptionalMap.get(1L).get().getName());\n\n\t\t// 先根据termId分组，再转换为Map<studentId,name>\n\t\tMap<Long, HashMap<Long, String>> groupThen = CollStreamUtil.groupBy(list, Student::getTermId, Collector.of(HashMap::new, (m, v) -> m.put(v.getStudentId(), v.getName()), (l, r) -> l));\n\t\tassertEquals(\n\t\t\t\tMapUtil.builder()\n\t\t\t\t\t\t.put(1L, MapUtil.builder().put(1L, \"李四\").build())\n\t\t\t\t\t\t.put(2L, MapUtil.builder().put(1L, \"王五\").build())\n\t\t\t\t\t\t.build(),\n\t\t\t\tgroupThen);\n\n\t\t// 总之，如果你是想要group分组后还要进行别的操作，用它就对了！\n\t\t// 并且对null值进行了友好处理，例如\n\t\tList<Student> students = Arrays.asList(null, null, new Student(1, 1, 1, \"张三\"),\n\t\t\t\tnew Student(1, 2, 1, \"李四\"));\n\t\tMap<Long, List<Student>> termIdStudentsMap = CollStreamUtil.groupBy(students, Student::getTermId, Collectors.toList());\n\t\tMap<Long, List<Student>> termIdStudentsCompareMap = new HashMap<>();\n\t\ttermIdStudentsCompareMap.put(null, Arrays.asList(null, null));\n\t\ttermIdStudentsCompareMap.put(1L, Arrays.asList(new Student(1L, 1, 1, \"张三\"), new Student(1L, 2, 1, \"李四\")));\n\t\tassertEquals(termIdStudentsCompareMap, termIdStudentsMap);\n\n\t\tMap<Long, Long> termIdCountMap = CollStreamUtil.groupBy(students, Student::getTermId, Collectors.counting());\n\t\tMap<Long, Long> termIdCountCompareMap = new HashMap<>();\n\t\ttermIdCountCompareMap.put(null, 2L);\n\t\ttermIdCountCompareMap.put(1L, 2L);\n\t\tassertEquals(termIdCountCompareMap, termIdCountMap);\n\t}\n\n\n\t@Test\n\tpublic void testTranslate2List() {\n\t\tList<String> list = CollStreamUtil.toList(null, Student::getName);\n\t\tassertEquals(Collections.EMPTY_LIST, list);\n\t\tList<Student> students = new ArrayList<>();\n\t\tlist = CollStreamUtil.toList(students, Student::getName);\n\t\tassertEquals(Collections.EMPTY_LIST, list);\n\t\tstudents.add(new Student(1, 1, 1, \"张三\"));\n\t\tstudents.add(new Student(1, 2, 2, \"李四\"));\n\t\tstudents.add(new Student(2, 1, 1, \"李四\"));\n\t\tstudents.add(new Student(2, 2, 2, \"李四\"));\n\t\tstudents.add(new Student(2, 3, 2, \"霸天虎\"));\n\t\tlist = CollStreamUtil.toList(students, Student::getName);\n\t\tList<String> compare = new ArrayList<>();\n\t\tcompare.add(\"张三\");\n\t\tcompare.add(\"李四\");\n\t\tcompare.add(\"李四\");\n\t\tcompare.add(\"李四\");\n\t\tcompare.add(\"霸天虎\");\n\t\tassertEquals(list, compare);\n\t}\n\n\t@Test\n\tpublic void testTranslate2Set() {\n\t\tSet<String> set = CollStreamUtil.toSet(null, Student::getName);\n\t\tassertEquals(Collections.EMPTY_SET, set);\n\t\tList<Student> students = new ArrayList<>();\n\t\tset = CollStreamUtil.toSet(students, Student::getName);\n\t\tassertEquals(Collections.EMPTY_SET, set);\n\t\tstudents.add(new Student(1, 1, 1, \"张三\"));\n\t\tstudents.add(new Student(1, 2, 2, \"李四\"));\n\t\tstudents.add(new Student(2, 1, 1, \"李四\"));\n\t\tstudents.add(new Student(2, 2, 2, \"李四\"));\n\t\tstudents.add(new Student(2, 3, 2, \"霸天虎\"));\n\t\tset = CollStreamUtil.toSet(students, Student::getName);\n\t\tSet<String> compare = new HashSet<>();\n\t\tcompare.add(\"张三\");\n\t\tcompare.add(\"李四\");\n\t\tcompare.add(\"霸天虎\");\n\t\tassertEquals(set, compare);\n\t}\n\n\t@Test\n\tpublic void testMerge() {\n\t\tMap<Long, Student> map1 = null;\n\t\tMap<Long, Student> map2 = Collections.emptyMap();\n\t\tMap<Long, String> map = CollStreamUtil.merge(map1, map2, (s1, s2) -> s1.getName() + s2.getName());\n\t\tassertEquals(Collections.EMPTY_MAP, map);\n\t\tmap1 = new HashMap<>();\n\t\tmap1.put(1L, new Student(1, 1, 1, \"张三\"));\n\t\tmap = CollStreamUtil.merge(map1, map2, this::merge);\n\t\tMap<Long, String> temp = new HashMap<>();\n\t\ttemp.put(1L, \"张三\");\n\t\tassertEquals(map, temp);\n\t\tmap2 = new HashMap<>();\n\t\tmap2.put(1L, new Student(2, 1, 1, \"李四\"));\n\t\tmap = CollStreamUtil.merge(map1, map2, this::merge);\n\t\tMap<Long, String> compare = new HashMap<>();\n\t\tcompare.put(1L, \"张三李四\");\n\t\tassertEquals(map, compare);\n\t}\n\n\tprivate String merge(Student student1, Student student2) {\n\t\tif (student1 == null && student2 == null) {\n\t\t\treturn null;\n\t\t} else if (student1 == null) {\n\t\t\treturn student2.getName();\n\t\t} else if (student2 == null) {\n\t\t\treturn student1.getName();\n\t\t} else {\n\t\t\treturn student1.getName() + student2.getName();\n\t\t}\n\t}\n\n\t/**\n\t * 班级类\n\t */\n\t@Data\n\t@AllArgsConstructor\n\t@ToString\n\tpublic static class Student {\n\t\tprivate long termId;//学期id\n\t\tprivate long classId;//班级id\n\t\tprivate long studentId;//班级id\n\t\tprivate String name;//学生名称\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.comparator.ComparableComparator;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 集合工具类单元测试\n *\n * @author looly\n */\npublic class CollUtilTest {\n\n\t@Test\n\tpublic void testPredicateContains() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"bbbbb\", \"aaaaa\", \"ccccc\");\n\t\tassertTrue(CollUtil.contains(list, s -> s.startsWith(\"a\")));\n\t\tassertFalse(CollUtil.contains(list, s -> s.startsWith(\"d\")));\n\t}\n\n\t@Test\n\tpublic void testRemoveWithAddIf() {\n\t\tArrayList<Integer> list = CollUtil.newArrayList(1, 2, 3);\n\t\tfinal ArrayList<Integer> exceptRemovedList = CollUtil.newArrayList(2, 3);\n\t\tfinal ArrayList<Integer> exceptResultList = CollUtil.newArrayList(1);\n\n\t\tList<Integer> resultList = CollUtil.removeWithAddIf(list, ele -> 1 == ele);\n\t\tassertEquals(list, exceptRemovedList);\n\t\tassertEquals(resultList, exceptResultList);\n\n\t\tlist = CollUtil.newArrayList(1, 2, 3);\n\t\tresultList = new ArrayList<>();\n\t\tCollUtil.removeWithAddIf(list, resultList, ele -> 1 == ele);\n\t\tassertEquals(list, exceptRemovedList);\n\t\tassertEquals(resultList, exceptResultList);\n\t}\n\n\t@Test\n\tpublic void testPadLeft() {\n\t\tList<String> srcList = CollUtil.newArrayList();\n\t\tList<String> answerList = CollUtil.newArrayList(\"a\", \"b\");\n\t\tCollUtil.padLeft(srcList, 1, \"b\");\n\t\tCollUtil.padLeft(srcList, 2, \"a\");\n\t\tassertEquals(srcList, answerList);\n\n\t\tsrcList = CollUtil.newArrayList(\"a\", \"b\");\n\t\tanswerList = CollUtil.newArrayList(\"a\", \"b\");\n\t\tCollUtil.padLeft(srcList, 2, \"a\");\n\t\tassertEquals(srcList, answerList);\n\n\t\tsrcList = CollUtil.newArrayList(\"c\");\n\t\tanswerList = CollUtil.newArrayList(\"a\", \"a\", \"c\");\n\t\tCollUtil.padLeft(srcList, 3, \"a\");\n\t\tassertEquals(srcList, answerList);\n\t}\n\n\t@Test\n\tpublic void testPadLeft_NegativeMinLen_ShouldNotModifyList() {\n\t\tList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\t\tList<String> original = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\n\t\tCollUtil.padLeft(list, -5, \"x\");\n\n\t\tassertEquals(original, list, \"List should remain unchanged when minLen is negative\");\n\t}\n\n\t@Test\n\tpublic void testPadLeft_EmptyList_MinLenZero() {\n\t\tList<String> list = CollUtil.newArrayList();\n\n\t\tCollUtil.padLeft(list, 0, \"x\");\n\n\t\tassertTrue(list.isEmpty(), \"List should remain empty when minLen is 0\");\n\t}\n\n\t@Test\n\tpublic void testPadRight() {\n\t\tfinal List<String> srcList = CollUtil.newArrayList(\"a\");\n\t\tfinal List<String> answerList = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"b\");\n\t\tCollUtil.padRight(srcList, 5, \"b\");\n\t\tassertEquals(srcList, answerList);\n\t}\n\n\t@SuppressWarnings(\"ConstantValue\")\n\t@Test\n\tpublic void isNotEmptyTest() {\n\t\tassertFalse(CollUtil.isNotEmpty((Collection<?>) null));\n\t}\n\n\t@Test\n\tpublic void newHashSetTest() {\n\t\tfinal Set<String> set = CollUtil.newHashSet((String[]) null);\n\t\tassertNotNull(set);\n\t}\n\n\t@Test\n\tpublic void valuesOfKeysTest() {\n\t\tfinal Dict v1 = Dict.create().set(\"id\", 12).set(\"name\", \"张三\").set(\"age\", 23);\n\t\tfinal Dict v2 = Dict.create().set(\"age\", 13).set(\"id\", 15).set(\"name\", \"李四\");\n\n\t\tfinal String[] keys = v1.keySet().toArray(new String[0]);\n\t\tfinal ArrayList<Object> v1s = CollUtil.valuesOfKeys(v1, keys);\n\t\tassertTrue(v1s.contains(12));\n\t\tassertTrue(v1s.contains(23));\n\t\tassertTrue(v1s.contains(\"张三\"));\n\n\t\tfinal ArrayList<Object> v2s = CollUtil.valuesOfKeys(v2, keys);\n\t\tassertTrue(v2s.contains(15));\n\t\tassertTrue(v2s.contains(13));\n\t\tassertTrue(v2s.contains(\"李四\"));\n\t}\n\n\t@Test\n\tpublic void unionTest() {\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"c\", \"d\", \"x\");\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\");\n\n\t\tfinal Collection<String> union = CollUtil.union(list1, list2);\n\n\t\tassertEquals(3, CollUtil.count(union, \"b\"::equals));\n\t}\n\n\t@Test\n\tpublic void intersectionTest() {\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"c\", \"d\", \"x\");\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\");\n\n\t\tfinal Collection<String> intersection = CollUtil.intersection(list1, list2);\n\t\tassertEquals(2, CollUtil.count(intersection, \"b\"::equals));\n\t}\n\n\t@Test\n\tpublic void intersectionDistinctTest() {\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"c\", \"d\", \"x\");\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\");\n\t\tfinal ArrayList<String> list3 = CollUtil.newArrayList();\n\n\t\tfinal Collection<String> intersectionDistinct = CollUtil.intersectionDistinct(list1, list2);\n\t\tassertEquals(CollUtil.newLinkedHashSet(\"a\", \"b\", \"c\", \"d\"), intersectionDistinct);\n\n\t\tfinal Collection<String> intersectionDistinct2 = CollUtil.intersectionDistinct(list1, list2, list3);\n\t\tassertTrue(intersectionDistinct2.isEmpty());\n\t}\n\n\t@Test\n\tpublic void disjunctionTest() {\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"c\", \"d\", \"x\");\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\", \"x2\");\n\n\t\tfinal Collection<String> disjunction = CollUtil.disjunction(list1, list2);\n\t\tassertTrue(disjunction.contains(\"b\"));\n\t\tassertTrue(disjunction.contains(\"x2\"));\n\t\tassertTrue(disjunction.contains(\"x\"));\n\n\t\tfinal Collection<String> disjunction2 = CollUtil.disjunction(list2, list1);\n\t\tassertTrue(disjunction2.contains(\"b\"));\n\t\tassertTrue(disjunction2.contains(\"x2\"));\n\t\tassertTrue(disjunction2.contains(\"x\"));\n\t}\n\n\t@Test\n\tpublic void disjunctionTest2() {\n\t\t// 任意一个集合为空，差集为另一个集合\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList();\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\", \"x2\");\n\n\t\tfinal Collection<String> disjunction = CollUtil.disjunction(list1, list2);\n\t\tassertEquals(list2, disjunction);\n\t\tfinal Collection<String> disjunction2 = CollUtil.disjunction(list2, list1);\n\t\tassertEquals(list2, disjunction2);\n\t}\n\n\t@Test\n\tpublic void disjunctionTest3() {\n\t\t// 无交集下返回共同的元素\n\t\tfinal ArrayList<String> list1 = CollUtil.newArrayList(\"1\", \"2\", \"3\");\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\n\t\tfinal Collection<String> disjunction = CollUtil.disjunction(list1, list2);\n\t\tassertTrue(disjunction.contains(\"1\"));\n\t\tassertTrue(disjunction.contains(\"2\"));\n\t\tassertTrue(disjunction.contains(\"3\"));\n\t\tassertTrue(disjunction.contains(\"a\"));\n\t\tassertTrue(disjunction.contains(\"b\"));\n\t\tassertTrue(disjunction.contains(\"c\"));\n\t\tfinal Collection<String> disjunction2 = CollUtil.disjunction(list2, list1);\n\t\tassertTrue(disjunction2.contains(\"1\"));\n\t\tassertTrue(disjunction2.contains(\"2\"));\n\t\tassertTrue(disjunction2.contains(\"3\"));\n\t\tassertTrue(disjunction2.contains(\"a\"));\n\t\tassertTrue(disjunction2.contains(\"b\"));\n\t\tassertTrue(disjunction2.contains(\"c\"));\n\t}\n\n\t@Test\n\tpublic void subtractTest() {\n\t\tfinal List<String> list1 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"c\", \"d\", \"x\");\n\t\tfinal List<String> list2 = CollUtil.newArrayList(\"a\", \"b\", \"b\", \"b\", \"c\", \"d\", \"x2\");\n\t\tfinal Collection<String> subtract = CollUtil.subtract(list1, list2);\n\t\tassertEquals(1, subtract.size());\n\t\tassertEquals(\"x\", subtract.iterator().next());\n\t}\n\n\t@Test\n\tpublic void subtractSetTest() {\n\t\tfinal HashMap<String, Object> map1 = MapUtil.newHashMap();\n\t\tfinal HashMap<String, Object> map2 = MapUtil.newHashMap();\n\t\tmap1.put(\"1\", \"v1\");\n\t\tmap1.put(\"2\", \"v2\");\n\t\tmap2.put(\"2\", \"v2\");\n\t\tfinal Collection<String> r2 = CollUtil.subtract(map1.keySet(), map2.keySet());\n\t\tassertEquals(\"[1]\", r2.toString());\n\t}\n\n\t@Test\n\tpublic void subtractSetToListTest() {\n\t\tfinal HashMap<String, Object> map1 = MapUtil.newHashMap();\n\t\tfinal HashMap<String, Object> map2 = MapUtil.newHashMap();\n\t\tmap1.put(\"1\", \"v1\");\n\t\tmap1.put(\"2\", \"v2\");\n\t\tmap2.put(\"2\", \"v2\");\n\t\tfinal List<String> r2 = CollUtil.subtractToList(map1.keySet(), map2.keySet());\n\t\tassertEquals(\"[1]\", r2.toString());\n\t}\n\n\t@Test\n    public void testSubtractWithDuplicates() {\n        Collection<String> coll1 = new ArrayList<>(Arrays.asList(\"a\", \"b\", \"b\", \"c\"));\n        Collection<String> coll2 = Collections.singletonList(\"b\");\n        Collection<String> result = CollUtil.subtract(coll1, coll2);\n\n        List<String> expected = Arrays.asList(\"a\", \"c\");\n        List<String> resultList = new ArrayList<>(result);\n        Collections.sort(resultList);\n        Collections.sort(expected);\n        assertEquals(expected, resultList);\n    }\n\n\t@Test\n\tpublic void toMapListAndToListMapTest() {\n\t\tfinal HashMap<String, String> map1 = new HashMap<>();\n\t\tmap1.put(\"a\", \"值1\");\n\t\tmap1.put(\"b\", \"值1\");\n\n\t\tfinal HashMap<String, String> map2 = new HashMap<>();\n\t\tmap2.put(\"a\", \"值2\");\n\t\tmap2.put(\"c\", \"值3\");\n\n\t\t// ----------------------------------------------------------------------------------------\n\t\tfinal ArrayList<HashMap<String, String>> list = CollUtil.newArrayList(map1, map2);\n\t\tfinal Map<String, List<String>> map = CollUtil.toListMap(list);\n\t\tassertEquals(\"值1\", map.get(\"a\").get(0));\n\t\tassertEquals(\"值2\", map.get(\"a\").get(1));\n\n\t\t// ----------------------------------------------------------------------------------------\n\t\tfinal List<Map<String, String>> listMap = CollUtil.toMapList(map);\n\t\tassertEquals(\"值1\", listMap.get(0).get(\"a\"));\n\t\tassertEquals(\"值2\", listMap.get(1).get(\"a\"));\n\t}\n\n\t@Test\n\tpublic void getFieldValuesTest() {\n\t\tfinal Dict v1 = Dict.create().set(\"id\", 12).set(\"name\", \"张三\").set(\"age\", 23);\n\t\tfinal Dict v2 = Dict.create().set(\"age\", 13).set(\"id\", 15).set(\"name\", \"李四\");\n\t\tfinal ArrayList<Dict> list = CollUtil.newArrayList(v1, v2);\n\n\t\tfinal List<Object> fieldValues = CollUtil.getFieldValues(list, \"name\");\n\t\tassertEquals(\"张三\", fieldValues.get(0));\n\t\tassertEquals(\"李四\", fieldValues.get(1));\n\t}\n\n\t@Test\n\tpublic void splitTest() {\n\t\tfinal ArrayList<Integer> list = CollUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9);\n\t\tfinal List<List<Integer>> split = CollUtil.split(list, 3);\n\t\tassertEquals(3, split.size());\n\t\tassertEquals(3, split.get(0).size());\n\t}\n\n\t@Test\n\tpublic void splitTest2() {\n\t\tfinal ArrayList<Integer> list = CollUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9);\n\t\tfinal List<List<Integer>> split = CollUtil.split(list, Integer.MAX_VALUE);\n\t\tassertEquals(1, split.size());\n\t\tassertEquals(9, split.get(0).size());\n\t}\n\n\t@Test\n\tpublic void foreachTest() {\n\t\tfinal HashMap<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\n\t\tfinal String[] result = new String[1];\n\t\tfinal String a = \"a\";\n\t\tCollUtil.forEach(map, (key, value, index) -> {\n\t\t\tif (a.equals(key)) {\n\t\t\t\tresult[0] = value;\n\t\t\t}\n\t\t});\n\t\tassertEquals(\"1\", result[0]);\n\t}\n\n\t@Test\n\tpublic void filterTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\n\t\tfinal Collection<String> filtered = CollUtil.edit(list, t -> t + 1);\n\n\t\tassertEquals(CollUtil.newArrayList(\"a1\", \"b1\", \"c1\"), filtered);\n\t}\n\n\t@Test\n\tpublic void filterTest2() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\n\t\tfinal ArrayList<String> filtered = CollUtil.filter(list, t -> !\"a\".equals(t));\n\n\t\t// 原地过滤\n\t\tassertSame(list, filtered);\n\t\tassertEquals(CollUtil.newArrayList(\"b\", \"c\"), filtered);\n\t}\n\n\t@Test\n\tpublic void filterSetTest() {\n\t\tfinal Set<String> set = CollUtil.newLinkedHashSet(\"a\", \"b\", \"\", \"  \", \"c\");\n\t\tfinal Set<String> filtered = CollUtil.filter(set, StrUtil::isNotBlank);\n\n\t\tassertEquals(CollUtil.newLinkedHashSet(\"a\", \"b\", \"c\"), filtered);\n\t}\n\n\t@Test\n\tpublic void filterRemoveTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\n\t\tfinal List<String> removed = new ArrayList<>();\n\t\tfinal ArrayList<String> filtered = CollUtil.filter(list, t -> {\n\t\t\tif (\"a\".equals(t)) {\n\t\t\t\tremoved.add(t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\n\t\tassertEquals(1, removed.size());\n\t\tassertEquals(\"a\", removed.get(0));\n\n\t\t// 原地过滤\n\t\tassertSame(list, filtered);\n\t\tassertEquals(CollUtil.newArrayList(\"b\", \"c\"), filtered);\n\t}\n\n\t@Test\n\tpublic void removeNullTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", null, \"\", \"  \");\n\n\t\tfinal ArrayList<String> filtered = CollUtil.removeNull(list);\n\n\t\t// 原地过滤\n\t\tassertSame(list, filtered);\n\t\tassertEquals(CollUtil.newArrayList(\"a\", \"b\", \"c\", \"\", \"  \"), filtered);\n\t}\n\n\t@Test\n\tpublic void removeEmptyTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", null, \"\", \"  \");\n\n\t\tfinal ArrayList<String> filtered = CollUtil.removeEmpty(list);\n\n\t\t// 原地过滤\n\t\tassertSame(list, filtered);\n\t\tassertEquals(CollUtil.newArrayList(\"a\", \"b\", \"c\", \"  \"), filtered);\n\t}\n\n\t@Test\n\tpublic void removeBlankTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", null, \"\", \"  \");\n\n\t\tfinal ArrayList<String> filtered = CollUtil.removeBlank(list);\n\n\t\t// 原地过滤\n\t\tassertSame(list, filtered);\n\t\tassertEquals(CollUtil.newArrayList(\"a\", \"b\", \"c\"), filtered);\n\t}\n\n\t@Test\n\tpublic void groupTest() {\n\t\tfinal List<String> list = CollUtil.newArrayList(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\");\n\t\tfinal List<List<String>> group = CollUtil.group(list, null);\n\t\tassertFalse(group.isEmpty());\n\n\t\tfinal List<List<String>> group2 = CollUtil.group(list, t -> {\n\t\t\t// 按照奇数偶数分类\n\t\t\treturn Integer.parseInt(t) % 2;\n\t\t});\n\t\tassertEquals(CollUtil.newArrayList(\"2\", \"4\", \"6\"), group2.get(0));\n\t\tassertEquals(CollUtil.newArrayList(\"1\", \"3\", \"5\"), group2.get(1));\n\t}\n\n\t@Test\n\tpublic void groupByFieldTest() {\n\t\tfinal List<TestBean> list = CollUtil.newArrayList(new TestBean(\"张三\", 12), new TestBean(\"李四\", 13), new TestBean(\"王五\", 12));\n\t\tfinal List<List<TestBean>> groupByField = CollUtil.groupByField(list, \"age\");\n\t\tassertEquals(\"张三\", groupByField.get(0).get(0).getName());\n\t\tassertEquals(\"王五\", groupByField.get(0).get(1).getName());\n\n\t\tassertEquals(\"李四\", groupByField.get(1).get(0).getName());\n\t}\n\n\t@Test\n\tpublic void sortByPropertyTest() {\n\t\tfinal List<TestBean> list = CollUtil.newArrayList(\n\t\t\tnew TestBean(\"张三\", 12, DateUtil.parse(\"2018-05-01\")), //\n\t\t\tnew TestBean(\"李四\", 13, DateUtil.parse(\"2018-03-01\")), //\n\t\t\tnew TestBean(\"王五\", 12, DateUtil.parse(\"2018-04-01\"))//\n\t\t);\n\n\t\tCollUtil.sortByProperty(list, \"createTime\");\n\t\tassertEquals(\"李四\", list.get(0).getName());\n\t\tassertEquals(\"王五\", list.get(1).getName());\n\t\tassertEquals(\"张三\", list.get(2).getName());\n\t}\n\n\t@Test\n\tpublic void sortByPropertyTest2() {\n\t\tfinal List<TestBean> list = CollUtil.newArrayList(\n\t\t\tnew TestBean(\"张三\", 0, DateUtil.parse(\"2018-05-01\")), //\n\t\t\tnew TestBean(\"李四\", -12, DateUtil.parse(\"2018-03-01\")), //\n\t\t\tnew TestBean(\"王五\", 23, DateUtil.parse(\"2018-04-01\"))//\n\t\t);\n\n\t\tCollUtil.sortByProperty(list, \"age\");\n\t\tassertEquals(\"李四\", list.get(0).getName());\n\t\tassertEquals(\"张三\", list.get(1).getName());\n\t\tassertEquals(\"王五\", list.get(2).getName());\n\t}\n\n\t@Test\n\tpublic void fieldValueMapTest() {\n\t\tfinal List<TestBean> list = CollUtil.newArrayList(new TestBean(\"张三\", 12, DateUtil.parse(\"2018-05-01\")), //\n\t\t\tnew TestBean(\"李四\", 13, DateUtil.parse(\"2018-03-01\")), //\n\t\t\tnew TestBean(\"王五\", 12, DateUtil.parse(\"2018-04-01\"))//\n\t\t);\n\n\t\tfinal Map<String, TestBean> map = CollUtil.fieldValueMap(list, \"name\");\n\t\tassertEquals(\"李四\", map.get(\"李四\").getName());\n\t\tassertEquals(\"王五\", map.get(\"王五\").getName());\n\t\tassertEquals(\"张三\", map.get(\"张三\").getName());\n\t}\n\n\t@Test\n\tpublic void fieldValueAsMapTest() {\n\t\tfinal List<TestBean> list = CollUtil.newArrayList(new TestBean(\"张三\", 12, DateUtil.parse(\"2018-05-01\")), //\n\t\t\tnew TestBean(\"李四\", 13, DateUtil.parse(\"2018-03-01\")), //\n\t\t\tnew TestBean(\"王五\", 14, DateUtil.parse(\"2018-04-01\"))//\n\t\t);\n\n\t\tfinal Map<String, Integer> map = CollUtil.fieldValueAsMap(list, \"name\", \"age\");\n\t\tassertEquals(new Integer(12), map.get(\"张三\"));\n\t\tassertEquals(new Integer(13), map.get(\"李四\"));\n\t\tassertEquals(new Integer(14), map.get(\"王五\"));\n\t}\n\n\t@Test\n\tpublic void emptyTest() {\n\t\tfinal SortedSet<String> emptySortedSet = CollUtil.empty(SortedSet.class);\n\t\tassertEquals(Collections.emptySortedSet(), emptySortedSet);\n\n\t\tfinal Set<String> emptySet = CollUtil.empty(Set.class);\n\t\tassertEquals(Collections.emptySet(), emptySet);\n\n\t\tfinal List<String> emptyList = CollUtil.empty(List.class);\n\t\tassertEquals(Collections.emptyList(), emptyList);\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tpublic static class TestBean {\n\t\tprivate String name;\n\t\tprivate int age;\n\t\tprivate Date createTime;\n\n\t\tpublic TestBean(final String name, final int age) {\n\t\t\tthis.name = name;\n\t\t\tthis.age = age;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void listTest() {\n\t\tfinal List<Object> list1 = CollUtil.list(false);\n\t\tfinal List<Object> list2 = CollUtil.list(true);\n\n\t\tassertInstanceOf(ArrayList.class, list1);\n\t\tassertInstanceOf(LinkedList.class, list2);\n\t}\n\n\t@Test\n\tpublic void listTest2() {\n\t\tfinal List<String> list1 = CollUtil.list(false, \"a\", \"b\", \"c\");\n\t\tfinal List<String> list2 = CollUtil.list(true, \"a\", \"b\", \"c\");\n\t\tassertEquals(\"[a, b, c]\", list1.toString());\n\t\tassertEquals(\"[a, b, c]\", list2.toString());\n\t}\n\n\t@Test\n\tpublic void listTest3() {\n\t\tfinal HashSet<String> set = new LinkedHashSet<>();\n\t\tset.add(\"a\");\n\t\tset.add(\"b\");\n\t\tset.add(\"c\");\n\n\t\tfinal List<String> list1 = CollUtil.list(false, set);\n\t\tfinal List<String> list2 = CollUtil.list(true, set);\n\t\tassertEquals(\"[a, b, c]\", list1.toString());\n\t\tassertEquals(\"[a, b, c]\", list2.toString());\n\t}\n\n\t@Test\n\tpublic void getTest() {\n\t\tfinal HashSet<String> set = CollUtil.set(true, \"A\", \"B\", \"C\", \"D\");\n\t\tString str = CollUtil.get(set, 2);\n\t\tassertEquals(\"C\", str);\n\n\t\tstr = CollUtil.get(set, -1);\n\t\tassertEquals(\"D\", str);\n\t}\n\n\t@Test\n\tpublic void addAllIfNotContainsTest() {\n\t\tfinal ArrayList<String> list1 = new ArrayList<>();\n\t\tlist1.add(\"1\");\n\t\tlist1.add(\"2\");\n\t\tfinal ArrayList<String> list2 = new ArrayList<>();\n\t\tlist2.add(\"2\");\n\t\tlist2.add(\"3\");\n\t\tCollUtil.addAllIfNotContains(list1, list2);\n\n\t\tassertEquals(3, list1.size());\n\t\tassertEquals(\"1\", list1.get(0));\n\t\tassertEquals(\"2\", list1.get(1));\n\t\tassertEquals(\"3\", list1.get(2));\n\t}\n\n\t@Test\n\tpublic void subInput1PositiveNegativePositiveOutput1() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 3;\n\t\tfinal int end = -1;\n\t\tfinal int step = 2;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tarrayList.add(null);\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1ZeroPositivePositiveOutput1() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 0;\n\t\tfinal int end = 1;\n\t\tfinal int step = 2;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tarrayList.add(null);\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1PositiveZeroOutput0() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 1;\n\t\tfinal int end = 0;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end);\n\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput0ZeroZeroZeroOutputNull() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tfinal int start = 0;\n\t\tfinal int end = 0;\n\t\tfinal int step = 0;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tassertTrue(retval.isEmpty());\n\t}\n\n\t@Test\n\tpublic void subInput1PositiveNegativeZeroOutput0() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 1;\n\t\tfinal int end = -2_147_483_648;\n\t\tfinal int step = 0;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1PositivePositivePositiveOutput0() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 2_147_483_647;\n\t\tfinal int end = 2_147_483_647;\n\t\tfinal int step = 1_073_741_824;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1PositiveNegativePositiveOutputArrayIndexOutOfBoundsException() {\n\t\tassertThrows(IndexOutOfBoundsException.class, () -> {\n\t\t\t// Arrange\n\t\t\tfinal List<Integer> list = new ArrayList<>();\n\t\t\tlist.add(null);\n\t\t\tfinal int start = 2_147_483_643;\n\t\t\tfinal int end = -2_147_483_648;\n\t\t\tfinal int step = 2;\n\n\t\t\t// Act\n\t\t\tCollUtil.sub(list, start, end, step);\n\t\t\t// Method is not expected to return due to exception thrown\n\t\t});\n\t}\n\n\t@Test\n\tpublic void subInput0ZeroPositiveNegativeOutputNull() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tfinal int start = 0;\n\t\tfinal int end = 1;\n\t\tfinal int step = -2_147_483_646;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tassertTrue(retval.isEmpty());\n\t}\n\n\t@Test\n\tpublic void subInput1PositivePositivePositiveOutput02() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(null);\n\t\tfinal int start = 2_147_483_643;\n\t\tfinal int end = 2_147_483_642;\n\t\tfinal int step = 1_073_741_824;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1ZeroZeroPositiveOutput0() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(0);\n\t\tfinal int start = 0;\n\t\tfinal int end = 0;\n\t\tfinal int step = 2;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput1NegativeZeroPositiveOutput0() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tlist.add(0);\n\t\tfinal int start = -1;\n\t\tfinal int end = 0;\n\t\tfinal int step = 2;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end, step);\n\t\t// Assert result\n\t\tfinal List<Integer> arrayList = new ArrayList<>();\n\t\tassertEquals(arrayList, retval);\n\t}\n\n\t@Test\n\tpublic void subInput0ZeroZeroOutputNull() {\n\t\t// Arrange\n\t\tfinal List<Integer> list = new ArrayList<>();\n\t\tfinal int start = 0;\n\t\tfinal int end = 0;\n\t\t// Act\n\t\tfinal List<Integer> retval = CollUtil.sub(list, start, end);\n\t\t// Assert result\n\t\tassertTrue(retval.isEmpty());\n\t}\n\n\t@Test\n\tpublic void sortPageAllTest() {\n\t\tfinal List<Integer> list = CollUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9);\n\t\tfinal List<Integer> sortPageAll = CollUtil.sortPageAll(1, 5, Comparator.reverseOrder(), list);\n\n\t\tassertEquals(CollUtil.newArrayList(4, 3, 2, 1), sortPageAll);\n\t}\n\n\t@Test\n\tpublic void containsAnyTest() {\n\t\tfinal ArrayList<Integer> list1 = CollUtil.newArrayList(1, 2, 3, 4, 5);\n\t\tfinal ArrayList<Integer> list2 = CollUtil.newArrayList(5, 3, 1, 9, 11);\n\n\t\tassertTrue(CollUtil.containsAny(list1, list2));\n\t}\n\n\t@Test\n\tpublic void containsAllTest() {\n\t\tfinal ArrayList<Integer> list1 = CollUtil.newArrayList(1, 2, 3, 4, 5);\n\t\tfinal ArrayList<Integer> list2 = CollUtil.newArrayList(5, 3, 1);\n\t\tassertTrue(CollUtil.containsAll(list1, list2));\n\n\t\tfinal ArrayList<Integer> list3 = CollUtil.newArrayList(1);\n\t\tfinal ArrayList<Integer> list4 = CollUtil.newArrayList();\n\t\tassertTrue(CollUtil.containsAll(list3, list4));\n\t}\n\n\t@Test\n\tpublic void getLastTest() {\n\t\t// 测试：空数组返回null而不是报错\n\t\tfinal List<String> test = CollUtil.newArrayList();\n\t\tfinal String last = CollUtil.getLast(test);\n\t\tassertNull(last);\n\t}\n\n\t@Test\n\tpublic void zipTest() {\n\t\tfinal Collection<String> keys = CollUtil.newArrayList(\"a\", \"b\", \"c\", \"d\");\n\t\tfinal Collection<Integer> values = CollUtil.newArrayList(1, 2, 3, 4);\n\n\t\tfinal Map<String, Integer> map = CollUtil.zip(keys, values);\n\n\t\tassertEquals(4, Objects.requireNonNull(map).size());\n\n\t\tassertEquals(1, map.get(\"a\").intValue());\n\t\tassertEquals(2, map.get(\"b\").intValue());\n\t\tassertEquals(3, map.get(\"c\").intValue());\n\t\tassertEquals(4, map.get(\"d\").intValue());\n\t}\n\n\t@Test\n\tpublic void toMapTest() {\n\t\tfinal Collection<String> keys = CollUtil.newArrayList(\"a\", \"b\", \"c\", \"d\");\n\t\tfinal Map<String, String> map = CollUtil.toMap(keys, new HashMap<>(), (value) -> \"key\" + value);\n\t\tassertEquals(\"a\", map.get(\"keya\"));\n\t\tassertEquals(\"b\", map.get(\"keyb\"));\n\t\tassertEquals(\"c\", map.get(\"keyc\"));\n\t\tassertEquals(\"d\", map.get(\"keyd\"));\n\t}\n\n\t@Test\n\tpublic void addIfAbsentTest() {\n\t\t// 为false的情况\n\t\tassertFalse(CollUtil.addIfAbsent(null, null));\n\t\tassertFalse(CollUtil.addIfAbsent(CollUtil.newArrayList(), null));\n\t\tassertFalse(CollUtil.addIfAbsent(null, \"123\"));\n\t\tassertFalse(CollUtil.addIfAbsent(CollUtil.newArrayList(\"123\"), \"123\"));\n\t\tassertFalse(CollUtil.addIfAbsent(CollUtil.newArrayList(new Animal(\"jack\", 20)),\n\t\t\tnew Animal(\"jack\", 20)));\n\n\t\t// 正常情况\n\t\tassertTrue(CollUtil.addIfAbsent(CollUtil.newArrayList(\"456\"), \"123\"));\n\t\tassertTrue(CollUtil.addIfAbsent(CollUtil.newArrayList(new Animal(\"jack\", 20)),\n\t\t\tnew Dog(\"jack\", 20)));\n\t\tassertTrue(CollUtil.addIfAbsent(CollUtil.newArrayList(new Animal(\"jack\", 20)),\n\t\t\tnew Animal(\"tom\", 20)));\n\t}\n\n\t@Test\n\tpublic void mapToMapTest() {\n\t\tfinal HashMap<String, String> oldMap = new HashMap<>();\n\t\toldMap.put(\"a\", \"1\");\n\t\toldMap.put(\"b\", \"12\");\n\t\toldMap.put(\"c\", \"134\");\n\n\t\tfinal Map<String, Long> map = CollUtil.toMap(oldMap.entrySet(),\n\t\t\tnew HashMap<>(),\n\t\t\tMap.Entry::getKey,\n\t\t\tentry -> Long.parseLong(entry.getValue()));\n\n\t\tassertEquals(1L, (long) map.get(\"a\"));\n\t\tassertEquals(12L, (long) map.get(\"b\"));\n\t\tassertEquals(134L, (long) map.get(\"c\"));\n\t}\n\n\t@Test\n\tpublic void countMapTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", \"c\", \"a\", \"b\", \"d\");\n\t\tfinal Map<String, Integer> countMap = CollUtil.countMap(list);\n\n\t\tassertEquals(Integer.valueOf(2), countMap.get(\"a\"));\n\t\tassertEquals(Integer.valueOf(2), countMap.get(\"b\"));\n\t\tassertEquals(Integer.valueOf(2), countMap.get(\"c\"));\n\t\tassertEquals(Integer.valueOf(1), countMap.get(\"d\"));\n\t}\n\n\t@Test\n\tpublic void indexOfTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", \"c\", \"a\", \"b\", \"d\");\n\t\tfinal int i = CollUtil.indexOf(list, (str) -> str.charAt(0) == 'c');\n\t\tassertEquals(2, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfTest() {\n\t\t// List有优化\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\", \"c\", \"a\", \"b\", \"d\");\n\t\tfinal int i = CollUtil.lastIndexOf(list, (str) -> str.charAt(0) == 'c');\n\t\tassertEquals(3, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfSetTest() {\n\t\tfinal Set<String> list = CollUtil.set(true, \"a\", \"b\", \"c\", \"c\", \"a\", \"b\", \"d\");\n\t\t// 去重后c排第三\n\t\tfinal int i = CollUtil.lastIndexOf(list, (str) -> str.charAt(0) == 'c');\n\t\tassertEquals(2, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOf_NoMatchExists() {\n\t\tList<String> list = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\t\tint idx = CollUtil.lastIndexOf(list, item -> item.equals(\"z\"));\n\t\tassertEquals(-1, idx);\n\t}\n\n\t@Test\n\tpublic void lastIndexOf_MatcherIsNull_MatchAll() {\n\t\tList<String> list = CollUtil.newArrayList(\"x\", \"y\", \"z\");\n\t\tint idx = CollUtil.lastIndexOf(list, null);\n\t\tassertEquals(2, idx);\n\t}\n\n\t@Test\n\tpublic void lastIndexOf_EmptyCollection() {\n\t\tList<String> list = CollUtil.newArrayList();\n\t\tint idx = CollUtil.lastIndexOf(list, Objects::nonNull);\n\t\tassertEquals(-1, idx);\n\t}\n\n\t@Test\n\tpublic void lastIndexOf_SingletonCollection_Match() {\n\t\tList<String> list = CollUtil.newArrayList(\"foo\");\n\t\tint idx = CollUtil.lastIndexOf(list, item -> item.equals(\"foo\"));\n\t\tassertEquals(0, idx);\n\t}\n\n\t@Test\n\tpublic void pageTest() {\n\t\tfinal List<Dict> objects = CollUtil.newArrayList();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tobjects.add(Dict.create().set(\"name\", \"姓名：\" + i));\n\t\t}\n\n\t\tassertEquals(0, CollUtil.page(3, 5, objects).size());\n\t}\n\n\t@Test\n\tpublic void subtractToListTest() {\n\t\tfinal List<Long> list1 = Arrays.asList(1L, 2L, 3L);\n\t\tfinal List<Long> list2 = Arrays.asList(2L, 3L);\n\n\t\tfinal List<Long> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(1, result.size());\n\t\tassertEquals(1L, (long) result.get(0));\n\t}\n\n\t@Test\n\tpublic void subtractToListAllNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tlist1.add(null);\n\t\tlist1.add(null);\n\t\tlist1.add(null);\n\t\tlist1.add(null);\n\n\t\tfinal List<String> list2 = new ArrayList<>();\n\t\tlist2.add(null);\n\t\tlist2.add(null);\n\n\t\tfinal List<String> result1 = CollUtil.subtractToList(list1, list2);\n\t\tassertTrue(result1.isEmpty());\n\t\tassertNotSame(result1, list1);\n\n\t\tlist2.add(\"c\");\n\t\tfinal List<String> result2 = CollUtil.subtractToList(list1, list2);\n\t\tassertTrue(result2.isEmpty());\n\t\tassertNotSame(result2, list1);\n\t}\n\n\t@Test\n\tpublic void subtractToListEmptyTest() {\n\t\t// 测试第一个集合为空的情况\n\t\tfinal List<String> list1 = Collections.emptyList();\n\t\tfinal List<String> list2 = Arrays.asList(\"a\", \"b\", \"c\");\n\n\t\t// 第一个集合为空时应返回空列表\n\t\tfinal List<String> result1 = CollUtil.subtractToList(list1, list2);\n\t\tassertTrue(result1.isEmpty());\n\n\t\t// 测试第二个集合为空的情况\n\t\tfinal List<String> list3 = Arrays.asList(\"a\", \"b\", \"c\");\n\t\tfinal List<String> list4 = Collections.emptyList();\n\n\t\t// 第二个集合为空时应返回第一个集合的拷贝\n\t\tfinal List<String> result2 = CollUtil.subtractToList(list3, list4);\n\t\tassertEquals(3, result2.size());\n\t\tassertEquals(list3, result2);\n\t\tassertNotSame(list3, result2);\n\t}\n\n\t@Test\n\tpublic void subtractToListDuplicateTest() {\n\t\t// 测试第一个集合中有重复元素的情况\n\t\tfinal List<String> list1 = Arrays.asList(\"a\", \"a\", \"b\", \"b\", \"c\", \"c\", \"d\");\n\t\tfinal List<String> list2 = Arrays.asList(\"b\", \"c\");\n\n\t\t// 应该返回所有不在第二个集合中的元素，包括重复的\n\t\tfinal List<String> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(3, result.size());\n\t\tassertEquals(\"a\", result.get(0));\n\t\tassertEquals(\"a\", result.get(1));\n\t\tassertEquals(\"d\", result.get(2));\n\t}\n\n\t@Test\n\tpublic void subtractToListNoCommonElementsTest() {\n\t\t// 测试集合1和集合2不包含相同元素的情况\n\t\tfinal List<String> list1 = Arrays.asList(\"a\", \"b\", \"c\");\n\t\tfinal List<String> list2 = Arrays.asList(\"d\", \"e\", \"f\");\n\n\t\t// 期望结果：返回集合1的完整拷贝\n\t\tfinal List<String> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(3, result.size());\n\t\tassertEquals(\"a\", result.get(0));\n\t\tassertEquals(\"b\", result.get(1));\n\t\tassertEquals(\"c\", result.get(2));\n\t\tassertEquals(list1, result);\n\t\tassertNotSame(list1, result); // 确保返回的是拷贝而不是原始引用\n\n\t\t// 测试集合1中有重复元素的情况\n\t\tfinal List<String> list3 = Arrays.asList(\"a\", \"a\", \"b\", \"b\", \"c\");\n\t\tfinal List<String> list4 = Arrays.asList(\"d\", \"e\", \"f\");\n\n\t\t// 期望结果：返回集合1的完整拷贝，包括重复元素\n\t\tfinal List<String> result2 = CollUtil.subtractToList(list3, list4);\n\t\tassertEquals(5, result2.size());\n\t\tassertEquals(\"a\", result2.get(0));\n\t\tassertEquals(\"a\", result2.get(1));\n\t\tassertEquals(\"b\", result2.get(2));\n\t\tassertEquals(\"b\", result2.get(3));\n\t\tassertEquals(\"c\", result2.get(4));\n\t\tassertEquals(list3, result2);\n\t\tassertNotSame(list3, result2);\n\n\t\t// 测试不同类型的元素但确保两个集合的泛型类型一致\n\t\tfinal List<Integer> list5 = Arrays.asList(1, 2, 3);\n\t\tfinal List<Integer> list6 = Arrays.asList(4, 5, 6);\n\n\t\t// 期望结果：返回集合1的完整拷贝\n\t\tfinal List<Integer> result3 = CollUtil.subtractToList(list5, list6);\n\t\tassertEquals(3, result3.size());\n\t\tassertEquals(Integer.valueOf(1), result3.get(0));\n\t\tassertEquals(Integer.valueOf(2), result3.get(1));\n\t\tassertEquals(Integer.valueOf(3), result3.get(2));\n\t\tassertEquals(list5, result3);\n\t\tassertNotSame(list5, result3);\n\t}\n\n\t@Test\n\tpublic void subtractToListWithLinkedTest() {\n\t\t// 测试指定返回LinkedList的情况\n\t\tfinal List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);\n\t\tfinal List<Integer> list2 = Arrays.asList(2, 4);\n\n\t\t// 使用LinkedList\n\t\tfinal List<Integer> result1 = CollUtil.subtractToList(list1, list2, true);\n\t\tassertInstanceOf(LinkedList.class, result1);\n\t\tassertEquals(3, result1.size());\n\t\tassertEquals(Arrays.asList(1, 3, 5), result1);\n\n\t\t// 使用ArrayList\n\t\tfinal List<Integer> result2 = CollUtil.subtractToList(list1, list2, false);\n\t\tassertInstanceOf(ArrayList.class, result2);\n\t\tassertEquals(3, result2.size());\n\t\tassertEquals(Arrays.asList(1, 3, 5), result2);\n\t}\n\n\t@Test\n\tpublic void subtractToListWithNullElementsTest() {\n\t\t// 测试包含null元素的情况\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tlist1.add(\"a\");\n\t\tlist1.add(null);\n\t\tlist1.add(\"b\");\n\n\t\tfinal List<String> list2 = Arrays.asList(\"a\", \"c\");\n\n\t\t// null元素不在list2中，应该保留\n\t\tfinal List<String> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(2, result.size());\n\t\tassertNull(result.get(0));\n\t\tassertEquals(\"b\", result.get(1));\n\t}\n\n\t@Test\n\tpublic void subtractToListLargeCollectionTest() {\n\t\t// 测试大集合性能\n\t\tfinal int size = 10000;\n\t\tfinal List<Integer> list1 = new ArrayList<>(size);\n\t\tfinal List<Integer> list2 = new ArrayList<>(size / 2);\n\n\t\t// 构建测试数据，list1 size为 10000，元素为 [0, 9999]\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tlist1.add(i);\n\t\t}\n\n\t\t// list2 size 为 5000，元素为 0, 2, 4, 6, 8, ..., 9996, 9998\n\t\tfor (int i = 0; i < size / 2; i++) {\n\t\t\tlist2.add(i * 2); // 偶数\n\t\t}\n\n\t\t// 记录开始时间\n\t\tlong startTime = System.currentTimeMillis();\n\n\t\t// 执行操作\n\t\tfinal List<Integer> result = CollUtil.subtractToList(list1, list2);\n\n\t\t// 记录结束时间\n\t\tlong endTime = System.currentTimeMillis();\n\n\t\t// 验证结果 - 应该只包含奇数\n\t\tassertEquals(size / 2, result.size());\n\t\tfor (Integer num : result) {\n\t\t\tassertEquals(1, num % 2);\n\t\t}\n\n\t\t// 输出性能指标\n\t\tSystem.out.println(\"Large collection subtractToList took: \" + (endTime - startTime) + \"ms for \" + size + \" elements\");\n\t}\n\n\t@Test\n\tpublic void subtractToListPerformanceComparisonTest() {\n\t\t// 比较不同实现方式的性能\n\t\tfinal int list1Size = 100000, list2Size = 1000;\n\t\tfinal List<Integer> list1 = new ArrayList<>(list1Size);\n\t\tfinal List<Integer> list2 = new ArrayList<>(list2Size);\n\n\t\t// 构建测试数据，list1 size 为 100000\n\t\t// 元素为 [0, 99999]\n\t\tfor (int i = 0; i < list1Size; i++) {\n\t\t\tlist1.add(i);\n\t\t}\n\n\t\t// [0, 9999]\n\t\tfor (int i = 0; i < list2Size; i++) {\n\t\t\tlist2.add(i);\n\t\t}\n\n\t\t// 测试LinkedList性能\n\t\tlong startTime1 = System.currentTimeMillis();\n\t\tfinal List<Integer> result1 = CollUtil.subtractToList(list1, list2, true);\n\t\tlong endTime1 = System.currentTimeMillis();\n\t\tlong linkedListTime = endTime1 - startTime1;\n\n\t\t// 测试ArrayList性能\n\t\tlong startTime2 = System.currentTimeMillis();\n\t\tfinal List<Integer> result2 = CollUtil.subtractToList(list1, list2, false);\n\t\tlong endTime2 = System.currentTimeMillis();\n\t\tlong arrayListTime = endTime2 - startTime2;\n\n\t\t// 验证结果相同\n\t\tassertEquals(result1.size(), result2.size());\n\t\tassertEquals(list1Size - list2Size, result1.size());\n\n\t\t// 输出性能比较\n\t\tSystem.out.println(\"subtractToList performance comparison for \" + list1Size + \" elements:\");\n\t\tSystem.out.println(\"LinkedList implementation: \" + linkedListTime + \"ms\");\n\t\tSystem.out.println(\"ArrayList implementation: \" + arrayListTime + \"ms\");\n\t}\n\n\t@Test\n\tpublic void subtractToListPreservesOrderTest() {\n\t\t// 测试确保保留原始集合的顺序\n\t\tfinal List<String> list1 = Arrays.asList(\"c\", \"a\", \"d\", \"b\", \"e\");\n\t\tfinal List<String> list2 = Arrays.asList(\"a\", \"e\");\n\n\t\t// 减去后应该保持原顺序：c, d, b\n\t\tfinal List<String> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(3, result.size());\n\t\tassertEquals(\"c\", result.get(0));\n\t\tassertEquals(\"d\", result.get(1));\n\t\tassertEquals(\"b\", result.get(2));\n\t}\n\n\t@Test\n\tpublic void subtractToListTypeTest() {\n\t\t// 测试默认返回LinkedList的特性（旧版本特性）\n\t\tfinal List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);\n\t\tfinal List<Integer> list2 = Arrays.asList(2, 4);\n\n\t\t// 不指定类型时，旧版本默认使用LinkedList\n\t\tfinal List<Integer> result = CollUtil.subtractToList(list1, list2);\n\t\tassertInstanceOf(LinkedList.class, result);\n\t\tassertEquals(3, result.size());\n\t\tassertEquals(Arrays.asList(1, 3, 5), result);\n\t}\n\n\t@Test\n\tpublic void subtractToListNullElementsComparisonTest() {\n\t\t// 测试对null元素处理的一致性\n\t\tfinal ArrayList<String> list1 = new ArrayList<>();\n\t\tlist1.add(null);\n\t\tlist1.add(\"a\");\n\t\tlist1.add(null);\n\t\tlist1.add(\"b\");\n\n\t\tfinal ArrayList<String> list2 = new ArrayList<>();\n\t\tlist2.add(\"a\");\n\n\t\t// 默认调用（旧版行为）\n\t\tfinal List<String> result1 = CollUtil.subtractToList(list1, list2);\n\t\t// 指定LinkedList（模拟旧版）\n\t\tfinal List<String> result2 = CollUtil.subtractToList(list1, list2, true);\n\t\t// 指定ArrayList（新版特性）\n\t\tfinal List<String> result3 = CollUtil.subtractToList(list1, list2, false);\n\n\t\t// 验证三种结果一致, 都应该是 [null, null, \"b\"]\n\t\tassertEquals(3, result1.size());\n\t\tassertEquals(3, result2.size());\n\t\tassertEquals(3, result3.size());\n\n\t\t// 都应该包含null元素和 \"b\"\n\t\tassertTrue(result1.contains(null));\n\t\tassertTrue(result1.contains(\"b\"));\n\t\tassertFalse(result1.contains(\"a\"));\n\n\t\t// 旧版实现和新版保持空元素顺序一致性\n\t\tassertNull(result1.get(0));\n\t\tassertNull(result1.get(1));\n\t\tassertEquals(\"b\", result1.get(2));\n\n\t\t// 结果应该相同\n\t\tassertEquals(result1, result2);\n\t\tassertEquals(result1, result3);\n\t\tassertEquals(result1.toString(), result3.toString());\n\t}\n\n\t@Test\n\tpublic void subtractToListWithCustomObjectsTest() {\n\t\t// 测试自定义对象的情况\n\t\tfinal Person p1 = new Person(\"张三\", 20, \"male\", 1);\n\t\tfinal Person p2 = new Person(\"李四\", 21, \"female\", 2);\n\t\tfinal Person p3 = new Person(\"王五\", 22, \"male\", 3);\n\t\tfinal Person p4 = new Person(\"赵六\", 23, \"female\", 4);\n\n\t\tfinal List<Person> list1 = Arrays.asList(p1, p2, p3, p4);\n\t\tfinal List<Person> list2 = Arrays.asList(p2, p4);\n\n\t\t// 减去后应该只剩下p1和p3\n\t\tfinal List<Person> result = CollUtil.subtractToList(list1, list2);\n\t\tassertEquals(2, result.size());\n\t\tassertEquals(\"张三\", result.get(0).getName());\n\t\tassertEquals(20, result.get(0).getAge());\n\t\tassertEquals(\"male\", result.get(0).getGender());\n\t\tassertEquals(1, result.get(0).getId());\n\n\t\tassertEquals(\"王五\", result.get(1).getName());\n\t\tassertEquals(22, result.get(1).getAge());\n\t\tassertEquals(\"male\", result.get(1).getGender());\n\t\tassertEquals(3, result.get(1).getId());\n\t}\n\n\t@Test\n\tpublic void subtractToListSameObjectsTest() {\n\t\t// 测试两个集合有完全相同对象的情况\n\t\tfinal List<String> list1 = Arrays.asList(\"a\", \"b\", \"c\");\n\t\tfinal List<String> list2 = Arrays.asList(\"a\", \"b\", \"c\");\n\n\t\t// 减去后应该为空列表\n\t\tfinal List<String> result = CollUtil.subtractToList(list1, list2);\n\t\tassertTrue(result.isEmpty());\n\n\t\t// 验证结果类型\n\t\tassertInstanceOf(LinkedList.class, result);\n\t\tassertNotSame(result, list1);\n\t}\n\n\t@Test\n\tpublic void subtractToListCollectionImplementationTest() {\n\t\t// 测试非List集合实现的情况\n\t\tfinal Set<String> set1 = new HashSet<>(Arrays.asList(\"a\", \"b\", \"c\", \"d\"));\n\t\tfinal Set<String> set2 = new LinkedHashSet<>(Arrays.asList(\"b\", \"d\"));\n\n\t\t// 减去后应该只剩下a和c\n\t\tfinal List<String> result = CollUtil.subtractToList(set1, set2);\n\t\tassertEquals(2, result.size());\n\t\tassertTrue(result.contains(\"a\"));\n\t\tassertTrue(result.contains(\"c\"));\n\n\t\t// 验证结果类型\n\t\tassertInstanceOf(LinkedList.class, result);\n\t}\n\n\t@Test\n\tpublic void subtractToListConsistencyTest() {\n\t\t// 测试subtractToList与subtract方法的一致性\n\t\tfinal List<String> list1 = Arrays.asList(\"a\", \"b\", \"c\", \"d\", \"e\");\n\t\tfinal List<String> list2 = Arrays.asList(\"b\", \"d\");\n\n\t\t// 使用subtract方法\n\t\tfinal Collection<String> subtractResult = CollUtil.subtract(list1, list2);\n\t\t// 使用subtractToList方法\n\t\tfinal List<String> subtractToListResult = CollUtil.subtractToList(list1, list2);\n\n\t\t// 虽然实现类型不同，但内容应该一致\n\t\tassertEquals(new HashSet<>(subtractResult), new HashSet<>(subtractToListResult));\n\t\tassertEquals(3, subtractToListResult.size());\n\t\tassertTrue(subtractToListResult.contains(\"a\"));\n\t\tassertTrue(subtractToListResult.contains(\"c\"));\n\t\tassertTrue(subtractToListResult.contains(\"e\"));\n\t}\n\n\t@Test\n\tpublic void sortComparableTest() {\n\t\tfinal List<String> of = ListUtil.toList(\"a\", \"c\", \"b\");\n\t\tfinal List<String> sort = CollUtil.sort(of, new ComparableComparator<>());\n\t\tassertEquals(\"a,b,c\", CollUtil.join(sort, \",\"));\n\t}\n\n\t@Test\n\tpublic void setValueByMapTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/pulls/482\n\t\tfinal List<Person> people = Arrays.asList(\n\t\t\tnew Person(\"aa\", 12, \"man\", 1),\n\t\t\tnew Person(\"bb\", 13, \"woman\", 2),\n\t\t\tnew Person(\"cc\", 14, \"man\", 3),\n\t\t\tnew Person(\"dd\", 15, \"woman\", 4),\n\t\t\tnew Person(\"ee\", 16, \"woman\", 5),\n\t\t\tnew Person(\"ff\", 17, \"man\", 6)\n\t\t);\n\n\t\tfinal Map<Integer, String> genderMap = new HashMap<>();\n\t\tgenderMap.put(1, null);\n\t\tgenderMap.put(2, \"妇女\");\n\t\tgenderMap.put(3, \"少女\");\n\t\tgenderMap.put(4, \"女\");\n\t\tgenderMap.put(5, \"小孩\");\n\t\tgenderMap.put(6, \"男\");\n\n\t\tassertEquals(\"woman\", people.get(1).getGender());\n\t\tCollUtil.setValueByMap(people, genderMap, Person::getId, Person::setGender);\n\t\tassertEquals(\"妇女\", people.get(1).getGender());\n\n\t\tfinal Map<Integer, Person> personMap = new HashMap<>();\n\t\tpersonMap.put(1, new Person(\"AA\", 21, \"男\", 1));\n\t\tpersonMap.put(2, new Person(\"BB\", 7, \"小孩\", 2));\n\t\tpersonMap.put(3, new Person(\"CC\", 65, \"老人\", 3));\n\t\tpersonMap.put(4, new Person(\"DD\", 35, \"女人\", 4));\n\t\tpersonMap.put(5, new Person(\"EE\", 14, \"少女\", 5));\n\t\tpersonMap.put(6, null);\n\n\t\tCollUtil.setValueByMap(people, personMap, Person::getId, (x, y) -> {\n\t\t\tx.setGender(y.getGender());\n\t\t\tx.setName(y.getName());\n\t\t\tx.setAge(y.getAge());\n\t\t});\n\n\t\tassertEquals(\"小孩\", people.get(1).getGender());\n\t}\n\n\t@Test\n\tpublic void distinctTest() {\n\t\tfinal ArrayList<Integer> distinct = CollUtil.distinct(ListUtil.of(5, 3, 10, 9, 0, 5, 10, 9));\n\t\tassertEquals(ListUtil.of(5, 3, 10, 9, 0), distinct);\n\t}\n\n\t@Test\n\tpublic void distinctByFunctionTest() {\n\t\tfinal List<Person> people = Arrays.asList(\n\t\t\tnew Person(\"aa\", 12, \"man\", 1),\n\t\t\tnew Person(\"bb\", 13, \"woman\", 2),\n\t\t\tnew Person(\"cc\", 14, \"man\", 3),\n\t\t\tnew Person(\"dd\", 15, \"woman\", 4),\n\t\t\tnew Person(\"ee\", 16, \"woman\", 5),\n\t\t\tnew Person(\"ff\", 17, \"man\", 6)\n\t\t);\n\n\t\t// 覆盖模式下ff覆盖了aa，ee覆盖了bb\n\t\tList<Person> distinct = CollUtil.distinct(people, Person::getGender, true);\n\t\tassertEquals(2, distinct.size());\n\t\tassertEquals(\"ff\", distinct.get(0).getName());\n\t\tassertEquals(\"ee\", distinct.get(1).getName());\n\n\t\t// 非覆盖模式下，保留了最早加入的aa和bb\n\t\tdistinct = CollUtil.distinct(people, Person::getGender, false);\n\t\tassertEquals(2, distinct.size());\n\t\tassertEquals(\"aa\", distinct.get(0).getName());\n\t\tassertEquals(\"bb\", distinct.get(1).getName());\n\t}\n\n\t@SuppressWarnings(\"ConstantValue\")\n\t@Test\n\tpublic void unionNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tfinal List<String> list2 = null;\n\t\tfinal List<String> list3 = null;\n\t\tfinal Collection<String> union = CollUtil.union(list1, list2, list3);\n\t\tassertNotNull(union);\n\t}\n\n\t@SuppressWarnings(\"ConstantValue\")\n\t@Test\n\tpublic void unionDistinctNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tfinal List<String> list2 = null;\n\t\tfinal List<String> list3 = null;\n\t\tfinal Set<String> set = CollUtil.unionDistinct(list1, list2, list3);\n\t\tassertNotNull(set);\n\t}\n\n\t@SuppressWarnings({\"ConfusingArgumentToVarargsMethod\", \"ConstantValue\"})\n\t@Test\n\tpublic void unionAllNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tfinal List<String> list2 = null;\n\t\tfinal List<String> list3 = null;\n\t\tfinal List<String> list = CollUtil.unionAll(list1, list2, list3);\n\t\tassertNotNull(list);\n\n\t\tfinal List<String> resList2 = CollUtil.unionAll(null, null, null);\n\t\tassertNotNull(resList2);\n\t}\n\n\t@Test\n\tpublic void unionAllOrdinaryTest() {\n\t\tfinal List<Integer> list1 = CollectionUtil.newArrayList(1, 2, 2, 3, 3);\n\t\tfinal List<Integer> list2 = CollectionUtil.newArrayList(1, 2, 3);\n\t\tfinal List<Integer> list3 = CollectionUtil.newArrayList(4, 5, 6);\n\t\tfinal List<Integer> list = CollUtil.unionAll(list1, list2, list3);\n\t\tassertNotNull(list);\n\t\tassertArrayEquals(\n\t\t\tCollectionUtil.newArrayList(1, 2, 2, 3, 3, 1, 2, 3, 4, 5, 6).toArray(),\n\t\t\tlist.toArray());\n\t}\n\n\t@Test\n\tpublic void unionAllTwoOrdinaryTest() {\n\t\tfinal List<Integer> list1 = CollectionUtil.newArrayList(1, 2, 2, 3, 3);\n\t\tfinal List<Integer> list2 = CollectionUtil.newArrayList(1, 2, 3);\n\t\tfinal List<Integer> list = CollUtil.unionAll(list1, list2);\n\t\tassertNotNull(list);\n\t\tassertArrayEquals(\n\t\t\tCollectionUtil.newArrayList(1, 2, 2, 3, 3, 1, 2, 3).toArray(),\n\t\t\tlist.toArray());\n\t}\n\n\t@Test\n\tpublic void unionAllOtherIsNullTest() {\n\t\tfinal List<Integer> list1 = CollectionUtil.newArrayList(1, 2, 2, 3, 3);\n\t\tfinal List<Integer> list2 = CollectionUtil.newArrayList(1, 2, 3);\n\t\t@SuppressWarnings(\"ConfusingArgumentToVarargsMethod\") final List<Integer> list = CollUtil.unionAll(list1, list2, null);\n\t\tassertNotNull(list);\n\t\tassertArrayEquals(\n\t\t\tCollectionUtil.newArrayList(1, 2, 2, 3, 3, 1, 2, 3).toArray(),\n\t\t\tlist.toArray());\n\t}\n\n\t@Test\n\tpublic void unionAllOtherTwoNullTest() {\n\t\tfinal List<Integer> list1 = CollectionUtil.newArrayList(1, 2, 2, 3, 3);\n\t\tfinal List<Integer> list2 = CollectionUtil.newArrayList(1, 2, 3);\n\t\tfinal List<Integer> list = CollUtil.unionAll(list1, list2, null, null);\n\t\tassertNotNull(list);\n\t\tassertArrayEquals(\n\t\t\tCollectionUtil.newArrayList(1, 2, 2, 3, 3, 1, 2, 3).toArray(),\n\t\t\tlist.toArray());\n\t}\n\n\t@SuppressWarnings(\"ConstantValue\")\n\t@Test\n\tpublic void intersectionNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tlist1.add(\"aa\");\n\t\tfinal List<String> list2 = new ArrayList<>();\n\t\tlist2.add(\"aa\");\n\t\tfinal List<String> list3 = null;\n\t\tfinal Collection<String> collection = CollUtil.intersection(list1, list2, list3);\n\t\tassertNotNull(collection);\n\t}\n\n\t@Test\n\tpublic void intersectionDistinctNullTest() {\n\t\tfinal List<String> list1 = new ArrayList<>();\n\t\tlist1.add(\"aa\");\n\t\tfinal List<String> list2 = null;\n\t\t// list2.add(\"aa\");\n\t\tfinal List<String> list3 = null;\n\t\tfinal Collection<String> collection = CollUtil.intersectionDistinct(list1, list2, list3);\n\t\tassertNotNull(collection);\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class Person {\n\t\tprivate String name;\n\t\tprivate Integer age;\n\t\tprivate String gender;\n\t\tprivate Integer id;\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\t@NoArgsConstructor\n\tstatic class Animal {\n\t\tprivate String name;\n\t\tprivate Integer age;\n\t}\n\n\t@ToString(callSuper = true)\n\t@EqualsAndHashCode(callSuper = true)\n\t@Data\n\tstatic class Dog extends Animal {\n\n\t\tpublic Dog(String name, Integer age) {\n\t\t\tsuper(name, age);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void getFirstTest() {\n\t\tfinal List<?> nullList = null;\n\t\tfinal Object first = CollUtil.getFirst(nullList);\n\t\tassertNull(first);\n\t}\n\n\t@Test\n\tpublic void testMatch() {\n\t\tList<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);\n\t\tassertTrue(CollUtil.anyMatch(list, i -> i == 1));\n\t\tassertFalse(CollUtil.anyMatch(list, i -> i > 6));\n\t\tassertFalse(CollUtil.allMatch(list, i -> i == 1));\n\t\tassertTrue(CollUtil.allMatch(list, i -> i <= 6));\n\t}\n\n\t@Test\n\tpublic void maxTest() {\n\t\tList<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);\n\t\tassertEquals((Integer) 6, CollUtil.max(list));\n\t}\n\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\t@Test\n\tpublic void maxEmptyTest() {\n\t\tfinal List<? extends Comparable> emptyList = Collections.emptyList();\n\t\tassertNull(CollUtil.max(emptyList));\n\t}\n\n\t@Test\n\tpublic void minNullTest() {\n\t\tassertNull(CollUtil.max(null));\n\t}\n\n\t@Test\n\tpublic void issueI8Z2Q4Test() {\n\t\tArrayList<String> coll1 = new ArrayList<>();\n\t\tcoll1.add(\"1\");\n\t\tcoll1.add(\"2\");\n\t\tcoll1.add(\"3\");\n\t\tcoll1.add(\"4\");\n\t\tArrayList<String> coll2 = new ArrayList<>();\n\t\tcoll2.add(\"1\");\n\t\tcoll2.add(\"1\");\n\t\tcoll2.add(\"1\");\n\t\tcoll2.add(\"1\");\n\t\tcoll2.add(\"1\");\n\n\t\tassertTrue(CollUtil.containsAll(coll1, coll2));\n\t}\n\n\t@Test\n\tvoid finOneTest(){\n\t\tAnimal dog = new Animal(\"dog\", 2);\n\t\tAnimal cat = new Animal(\"cat\", 3);\n\t\tAnimal bear = new Animal(\"bear\", 4);\n\n\t\tList<Animal> list = new ArrayList<>();\n\t\tlist.add(dog);\n\t\tlist.add(cat);\n\t\tlist.add(bear);\n\n\t\tfinal Animal cat1 = CollUtil.findOne(list, (t) -> t.getName().equals(\"cat\"));\n\t\tassertNotNull(cat1);\n\t\tassertEquals(\"cat\", cat1.getName());\n\t}\n\n\t@Test\n\tvoid issueIDBU9HTest(){\n\t\tList<ToolTest> list = new ArrayList<>();\n\t\tToolTest t1 = new ToolTest(\"a\");\n\t\tToolTest t2 = new ToolTest(\"b\");\n\t\tlist.add(t1);\n\t\tlist.add(t2);\n\t\tMap<String, ToolTest> map = list.stream().collect(Collectors.toMap(ToolTest::getName, Function.identity(), (k1, k2) -> k2));\n\t\tCollectionUtil.subtract(map.keySet(), map.keySet());\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tprivate static class ToolTest {\n\t\tprivate String name;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/FilterIterTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Iterator;\n\n/**\n * {@link FilterIter} 单元测试\n * @author chao.wang\n */\npublic class FilterIterTest {\n\n\t@Test\n\tpublic void checkFilterIter() {\n\t\tIterator<String> it = ListUtil.of(\"1\", \"2\").iterator();\n\t\t// filter 为null\n\t\tFilterIter<String> filterIter = new FilterIter<>(it, null);\n\n\t\tint count = 0;\n\t\twhile (filterIter.hasNext()) {\n\t\t\tif(filterIter.next() != null){\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\tassertEquals(2, count);\n\n\t\tit = ListUtil.of(\"1\", \"2\").iterator();\n\t\t// filter 不为空\n\t\tfilterIter = new FilterIter<>(it, (key) -> key.equals(\"1\"));\n\t\tcount = 0;\n\t\twhile (filterIter.hasNext()) {\n\t\t\tif(filterIter.next() != null){\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\t\tassertEquals(1, count);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/IterUtilTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * {@link IterUtil} 单元测试\n * @author looly\n *\n */\npublic class IterUtilTest {\n\n\t@Test\n\tpublic void getFirstTest() {\n\t\tassertNull(IterUtil.getFirst((Iterable<Object>) null));\n\t\tassertNull(IterUtil.getFirst(CollUtil.newArrayList()));\n\n\t\tassertEquals(\"1\", IterUtil.getFirst(CollUtil.newArrayList(\"1\", \"2\", \"3\")));\n\t\tfinal ArrayDeque<String> deque = new ArrayDeque<>();\n\t\tdeque.add(\"3\");\n\t\tdeque.add(\"4\");\n\t\tassertEquals(\"3\", IterUtil.getFirst(deque));\n\t}\n\n\t@Test\n\tpublic void getFirstNonNullTest(){\n\t\tfinal ArrayList<String> strings = CollUtil.newArrayList(null, null, \"123\", \"456\", null);\n\t\tassertEquals(\"123\", IterUtil.getFirstNoneNull(strings));\n\t}\n\n\t@Test\n\tpublic void fieldValueMapTest() {\n\t\tfinal ArrayList<Car> carList = CollUtil.newArrayList(new Car(\"123\", \"大众\"), new Car(\"345\", \"奔驰\"), new Car(\"567\", \"路虎\"));\n\t\tfinal Map<String, Car> carNameMap = IterUtil.fieldValueMap(carList.iterator(), \"carNumber\");\n\n\t\tassertEquals(\"大众\", carNameMap.get(\"123\").getCarName());\n\t\tassertEquals(\"奔驰\", carNameMap.get(\"345\").getCarName());\n\t\tassertEquals(\"路虎\", carNameMap.get(\"567\").getCarName());\n\t}\n\n\t@Test\n\tpublic void joinTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"1\", \"2\", \"3\", \"4\");\n\t\tfinal String join = IterUtil.join(list.iterator(), \":\");\n\t\tassertEquals(\"1:2:3:4\", join);\n\n\t\tfinal ArrayList<Integer> list1 = CollUtil.newArrayList(1, 2, 3, 4);\n\t\tfinal String join1 = IterUtil.join(list1.iterator(), \":\");\n\t\tassertEquals(\"1:2:3:4\", join1);\n\n\t\t// 包装每个节点\n\t\tfinal ArrayList<String> list2 = CollUtil.newArrayList(\"1\", \"2\", \"3\", \"4\");\n\t\tfinal String join2 = IterUtil.join(list2.iterator(), \":\", \"\\\"\", \"\\\"\");\n\t\tassertEquals(\"\\\"1\\\":\\\"2\\\":\\\"3\\\":\\\"4\\\"\", join2);\n\t}\n\n\t@Test\n\tpublic void joinWithFuncTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"1\", \"2\", \"3\", \"4\");\n\t\tfinal String join = IterUtil.join(list.iterator(), \":\", String::valueOf);\n\t\tassertEquals(\"1:2:3:4\", join);\n\t}\n\n\t@Test\n\tpublic void joinWithNullTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"1\", null, \"3\", \"4\");\n\t\tfinal String join = IterUtil.join(list.iterator(), \":\", String::valueOf);\n\t\tassertEquals(\"1:null:3:4\", join);\n\t}\n\n\t@Test\n\tpublic void testToListMap() {\n\t\tfinal Map<String, List<String>> expectedMap = new HashMap<>();\n\t\texpectedMap.put(\"a\", Collections.singletonList(\"and\"));\n\t\texpectedMap.put(\"b\", Arrays.asList(\"brave\", \"back\"));\n\n\t\tfinal Map<String, List<String>> testMap = IterUtil.toListMap(Arrays.asList(\"and\", \"brave\", \"back\"),\n\t\t\t\tv -> v.substring(0, 1));\n\t\tassertEquals(testMap, expectedMap);\n\t}\n\n\t@Test\n\tpublic void testToMap() {\n\t\tfinal Map<String, Car> expectedMap = new HashMap<>();\n\n\t\tfinal Car bmw = new Car(\"123\", \"bmw\");\n\t\texpectedMap.put(\"123\", bmw);\n\n\t\tfinal Car benz = new Car(\"456\", \"benz\");\n\t\texpectedMap.put(\"456\", benz);\n\n\t\tfinal Map<String, Car> testMap = IterUtil.toMap(Arrays.asList(bmw, benz), Car::getCarNumber);\n\t\tassertEquals(expectedMap, testMap);\n\t}\n\n\t@Test\n\tpublic void getElementTypeTest(){\n\t\tfinal List<Integer> integers = Arrays.asList(null, 1);\n\t\tfinal Class<?> elementType = IterUtil.getElementType(integers);\n\t\tassertEquals(Integer.class,elementType);\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tpublic static class Car {\n\t\tprivate String carNumber;\n\t\tprivate String carName;\n\t}\n\n\t@Test\n\tpublic void filterTest(){\n\t\tfinal List<String> obj2 = ListUtil.toList(\"3\");\n\t\tfinal List<String> obj = ListUtil.toList(\"1\", \"3\");\n\n\t\tIterUtil.filter(obj.iterator(), obj2::contains);\n\n\t\tassertEquals(1, obj.size());\n\t\tassertEquals(\"3\", obj.get(0));\n\t}\n\n\t@Test\n\tpublic void filteredTest(){\n\t\tfinal List<String> obj2 = ListUtil.toList(\"3\");\n\t\tfinal List<String> obj = ListUtil.toList(\"1\", \"3\");\n\n\t\tfinal FilterIter<String> filtered = IterUtil.filtered(obj.iterator(), obj2::contains);\n\n\t\tassertEquals(\"3\", filtered.next());\n\t\tassertFalse(filtered.hasNext());\n\t}\n\n\t@Test\n\tpublic void filterToListTest(){\n\t\tfinal List<String> obj2 = ListUtil.toList(\"3\");\n\t\tfinal List<String> obj = ListUtil.toList(\"1\", \"3\");\n\n\t\tfinal List<String> filtered = IterUtil.filterToList(obj.iterator(), obj2::contains);\n\n\t\tassertEquals(1, filtered.size());\n\t\tassertEquals(\"3\", filtered.get(0));\n\t}\n\n\t@Test\n\tpublic void getTest() {\n\t\tfinal HashSet<String> set = CollUtil.set(true, \"A\", \"B\", \"C\", \"D\");\n\t\tfinal String str = IterUtil.get(set.iterator(), 2);\n\t\tassertEquals(\"C\", str);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/ListUtilTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.date.StopWatch;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.PageUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ListUtilTest {\n\n\t@Test\n\tpublic void splitTest() {\n\t\tList<List<Object>> lists = ListUtil.split(null, 3);\n\t\tassertEquals(ListUtil.empty(), lists);\n\n\t\tlists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 1);\n\t\tassertEquals(\"[[1], [2], [3], [4]]\", lists.toString());\n\t\tlists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 2);\n\t\tassertEquals(\"[[1, 2], [3, 4]]\", lists.toString());\n\t\tlists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 3);\n\t\tassertEquals(\"[[1, 2, 3], [4]]\", lists.toString());\n\t\tlists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 4);\n\t\tassertEquals(\"[[1, 2, 3, 4]]\", lists.toString());\n\t\tlists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 5);\n\t\tassertEquals(\"[[1, 2, 3, 4]]\", lists.toString());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void splitBenchTest() {\n\t\tfinal List<String> list = new ArrayList<>();\n\t\tCollUtil.padRight(list, RandomUtil.randomInt(1000_0000, 1_0000_0000), \"test\");\n\n\t\tfinal int size = RandomUtil.randomInt(10, 1000);\n\t\tConsole.log(\"\\nlist size: {}\", list.size());\n\t\tConsole.log(\"partition size: {}\\n\", size);\n\t\tfinal StopWatch stopWatch = new StopWatch();\n\n\t\tstopWatch.start(\"CollUtil#split\");\n\t\tfinal List<List<String>> CollSplitResult = CollUtil.split(list, size);\n\t\tstopWatch.stop();\n\n\t\tstopWatch.start(\"ListUtil#split\");\n\t\tfinal List<List<String>> ListSplitResult = ListUtil.split(list, size);\n\t\tstopWatch.stop();\n\n\t\tassertEquals(CollSplitResult, ListSplitResult);\n\n\t\tConsole.log(stopWatch.prettyPrint());\n\t}\n\n\t@Test\n\tpublic void splitAvgTest() {\n\t\tList<List<Object>> lists = ListUtil.splitAvg(null, 3);\n\t\tassertEquals(ListUtil.empty(), lists);\n\n\t\tlists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 1);\n\t\tassertEquals(\"[[1, 2, 3, 4]]\", lists.toString());\n\t\tlists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 2);\n\t\tassertEquals(\"[[1, 2], [3, 4]]\", lists.toString());\n\t\tlists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 3);\n\t\tassertEquals(\"[[1, 2], [3], [4]]\", lists.toString());\n\t\tlists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 4);\n\t\tassertEquals(\"[[1], [2], [3], [4]]\", lists.toString());\n\n\t\tlists = ListUtil.splitAvg(Arrays.asList(1, 2, 3), 2);\n\t\tassertEquals(\"[[1, 2], [3]]\", lists.toString());\n\t}\n\n\t@Test\n\tpublic void splitAvgTest2() {\n\t\tList<List<Object>> lists = ListUtil.splitAvg(Arrays.asList(1, 2, 3), 5);\n\t\tassertEquals(\"[[1], [2], [3], [], []]\", lists.toString());\n\t}\n\n\t@Test\n\tpublic void splitAvgNotZero() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\t// limit不能小于等于0\n\t\t\tListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 0);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void editTest() {\n\t\tfinal List<String> a = ListUtil.toLinkedList(\"1\", \"2\", \"3\");\n\t\tfinal List<String> filter = (List<String>) CollUtil.edit(a, str -> \"edit\" + str);\n\t\tassertEquals(\"edit1\", filter.get(0));\n\t\tassertEquals(\"edit2\", filter.get(1));\n\t\tassertEquals(\"edit3\", filter.get(2));\n\t}\n\n\t@Test\n\tpublic void indexOfAll() {\n\t\tfinal List<String> a = ListUtil.toLinkedList(\"1\", \"2\", \"3\", \"4\", \"3\", \"2\", \"1\");\n\t\tfinal int[] indexArray = ListUtil.indexOfAll(a, \"2\"::equals);\n\t\tassertArrayEquals(new int[]{1, 5}, indexArray);\n\t\tfinal int[] indexArray2 = ListUtil.indexOfAll(a, \"1\"::equals);\n\t\tassertArrayEquals(new int[]{0, 6}, indexArray2);\n\t}\n\n\t@Test\n\tpublic void pageTest() {\n\t\tfinal List<Integer> a = ListUtil.toLinkedList(1, 2, 3, 4, 5);\n\n\t\tPageUtil.setFirstPageNo(1);\n\t\tfinal int[] a_1 = ListUtil.page(1, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] a1 = ListUtil.page(1, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] a2 = ListUtil.page(2, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] a3 = ListUtil.page(3, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] a4 = ListUtil.page(4, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tassertArrayEquals(new int[]{1, 2}, a_1);\n\t\tassertArrayEquals(new int[]{1, 2}, a1);\n\t\tassertArrayEquals(new int[]{3, 4}, a2);\n\t\tassertArrayEquals(new int[]{5}, a3);\n\t\tassertArrayEquals(new int[]{}, a4);\n\n\n\t\tPageUtil.setFirstPageNo(2);\n\t\tfinal int[] b_1 = ListUtil.page(1, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] b1 = ListUtil.page(2, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] b2 = ListUtil.page(3, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] b3 = ListUtil.page(4, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] b4 = ListUtil.page(5, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tassertArrayEquals(new int[]{1, 2}, b_1);\n\t\tassertArrayEquals(new int[]{1, 2}, b1);\n\t\tassertArrayEquals(new int[]{3, 4}, b2);\n\t\tassertArrayEquals(new int[]{5}, b3);\n\t\tassertArrayEquals(new int[]{}, b4);\n\n\t\tPageUtil.setFirstPageNo(0);\n\t\tfinal int[] c_1 = ListUtil.page(-1, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] c1 = ListUtil.page(0, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] c2 = ListUtil.page(1, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] c3 = ListUtil.page(2, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tfinal int[] c4 = ListUtil.page(3, 2, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tassertArrayEquals(new int[]{1, 2}, c_1);\n\t\tassertArrayEquals(new int[]{1, 2}, c1);\n\t\tassertArrayEquals(new int[]{3, 4}, c2);\n\t\tassertArrayEquals(new int[]{5}, c3);\n\t\tassertArrayEquals(new int[]{}, c4);\n\n\n\t\tPageUtil.setFirstPageNo(1);\n\t\tfinal int[] d1 = ListUtil.page(0, 8, a).stream().mapToInt(Integer::valueOf).toArray();\n\t\tassertArrayEquals(new int[]{1, 2, 3, 4, 5}, d1);\n\n\t\t// page with consumer\n\t\tfinal List<List<Integer>> pageListData = new ArrayList<>();\n\t\tListUtil.page(a, 2, pageListData::add);\n\t\tassertArrayEquals(new int[]{1, 2}, pageListData.get(0).stream().mapToInt(Integer::valueOf).toArray());\n\t\tassertArrayEquals(new int[]{3, 4}, pageListData.get(1).stream().mapToInt(Integer::valueOf).toArray());\n\t\tassertArrayEquals(new int[]{5}, pageListData.get(2).stream().mapToInt(Integer::valueOf).toArray());\n\n\n\t\tpageListData.clear();\n\t\tListUtil.page(a, 2, pageList -> {\n\t\t\tpageListData.add(pageList);\n\t\t\tif (pageList.get(0).equals(1)) {\n\t\t\t\tpageList.clear();\n\t\t\t}\n\t\t});\n\t\tassertArrayEquals(new int[]{}, pageListData.get(0).stream().mapToInt(Integer::valueOf).toArray());\n\t\tassertArrayEquals(new int[]{3, 4}, pageListData.get(1).stream().mapToInt(Integer::valueOf).toArray());\n\t\tassertArrayEquals(new int[]{5}, pageListData.get(2).stream().mapToInt(Integer::valueOf).toArray());\n\n\t\t// 恢复默认值，避免影响其他测试用例\n\t\tPageUtil.setFirstPageNo(0);\n\t}\n\n\t@Test\n\tpublic void subTest() {\n\t\tfinal List<Integer> of = ListUtil.of(1, 2, 3, 4);\n\t\tfinal List<Integer> sub = ListUtil.sub(of, 2, 4);\n\t\tsub.remove(0);\n\n\t\t// 对子列表操作不影响原列表\n\t\tassertEquals(4, of.size());\n\t\tassertEquals(1, sub.size());\n\t}\n\n\t@Test\n\tpublic void sortByPropertyTest() {\n\t\t@Data\n\t\t@AllArgsConstructor\n\t\tclass TestBean {\n\t\t\tprivate int order;\n\t\t\tprivate String name;\n\t\t}\n\n\t\tfinal List<TestBean> beanList = ListUtil.toList(\n\t\t\tnew TestBean(2, \"test2\"),\n\t\t\tnew TestBean(1, \"test1\"),\n\t\t\tnew TestBean(5, \"test5\"),\n\t\t\tnew TestBean(4, \"test4\"),\n\t\t\tnew TestBean(3, \"test3\")\n\t\t);\n\n\t\tfinal List<TestBean> order = ListUtil.sortByProperty(beanList, \"order\");\n\t\tassertEquals(\"test1\", order.get(0).getName());\n\t\tassertEquals(\"test2\", order.get(1).getName());\n\t\tassertEquals(\"test3\", order.get(2).getName());\n\t\tassertEquals(\"test4\", order.get(3).getName());\n\t\tassertEquals(\"test5\", order.get(4).getName());\n\t}\n\n\t@Test\n\tpublic void swapIndex() {\n\t\tfinal List<Integer> list = Arrays.asList(7, 2, 8, 9);\n\t\tListUtil.swapTo(list, 8, 1);\n\t\tassertEquals(8, (int) list.get(1));\n\t}\n\n\t@Test\n\tpublic void swapElement() {\n\t\tfinal Map<String, String> map1 = new HashMap<>();\n\t\tmap1.put(\"1\", \"张三\");\n\t\tfinal Map<String, String> map2 = new HashMap<>();\n\t\tmap2.put(\"2\", \"李四\");\n\t\tfinal Map<String, String> map3 = new HashMap<>();\n\t\tmap3.put(\"3\", \"王五\");\n\t\tfinal List<Map<String, String>> list = Arrays.asList(map1, map2, map3);\n\t\tListUtil.swapElement(list, map2, map3);\n\t\tMap<String, String> map = list.get(2);\n\t\tassertEquals(\"李四\", map.get(\"2\"));\n\n\t\tListUtil.swapElement(list, map2, map1);\n\t\tmap = list.get(0);\n\t\tassertEquals(\"李四\", map.get(\"2\"));\n\t}\n\n\t@Test\n\tpublic void setOrPaddingNullTest() {\n\t\tfinal List<String> list = new ArrayList<>();\n\t\tlist.add(\"1\");\n\n\t\t// 替换原值\n\t\tListUtil.setOrPadding(list, 0, \"a\");\n\t\tassertEquals(\"[a]\", list.toString());\n\n\t\t//append值\n\t\tListUtil.setOrPadding(list, 1, \"a\");\n\t\tassertEquals(\"[a, a]\", list.toString());\n\n\t\t// padding null 后加入值\n\t\tListUtil.setOrPadding(list, 3, \"a\");\n\t\tassertEquals(4, list.size());\n\t}\n\n\t@Test\n\tpublic void reverseNewTest() {\n\t\tfinal List<Integer> view = ListUtil.of(1, 2, 3);\n\t\tfinal List<Integer> reverse = ListUtil.reverseNew(view);\n\t\tassertEquals(\"[3, 2, 1]\", reverse.toString());\n\t}\n\n\t@Test\n\tpublic void testMoveElementToPosition() {\n\t\tList<String> list = new ArrayList<>(Arrays.asList(\"A\", \"B\", \"C\", \"D\"));\n\n\t\t// Move \"B\" to position 2\n\t\tList<String> expectedResult1 = new ArrayList<>(Arrays.asList(\"A\", \"C\", \"B\", \"D\"));\n\t\tassertEquals(expectedResult1, ListUtil.move(list, \"B\", 2));\n\n\t\tlist = new ArrayList<>(Arrays.asList(\"A\", \"B\", \"C\", \"D\"));\n\n\t\t// Move \"D\" to position 0\n\t\tList<String> expectedResult2 = new ArrayList<>(Arrays.asList(\"D\", \"A\", \"B\", \"C\"));\n\t\tassertEquals(expectedResult2, ListUtil.move(list, \"D\", 0));\n\n\t\tlist = new ArrayList<>(Arrays.asList(\"A\", \"B\", \"C\", \"D\"));\n\n\t\t// Move \"E\" (not in list) to position 1\n\t\tList<String> expectedResult3 = new ArrayList<>(Arrays.asList(\"A\", \"E\", \"B\", \"C\", \"D\"));\n\t\tassertEquals(expectedResult3, ListUtil.move(list, \"E\", 1));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/MapProxyTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.map.MapProxy;\n\npublic class MapProxyTest {\n\n\t@Test\n\tpublic void mapProxyTest() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\n\t\tMapProxy mapProxy = new MapProxy(map);\n\t\tInteger b = mapProxy.getInt(\"b\");\n\t\tassertEquals(new Integer(2), b);\n\n\t\tSet<Object> keys = mapProxy.keySet();\n\t\tassertFalse(keys.isEmpty());\n\n\t\tSet<Entry<Object,Object>> entrys = mapProxy.entrySet();\n\t\tassertFalse(entrys.isEmpty());\n\t}\n\n\tprivate interface Student {\n\t\tStudent setName(String name);\n\t\tStudent setAge(int age);\n\n\t\tString getName();\n\t\tint getAge();\n\t}\n\n\t@Test\n\tpublic void classProxyTest() {\n\t\tStudent student = MapProxy.create(new HashMap<>()).toProxyBean(Student.class);\n\t\tstudent.setName(\"小明\").setAge(18);\n\t\tassertEquals(student.getAge(), 18);\n\t\tassertEquals(student.getName(), \"小明\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/PartitionIterTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class PartitionIterTest {\n\n\t@Test\n\tpublic void iterTest() {\n\t\tfinal LineIter lineIter = new LineIter(ResourceUtil.getUtf8Reader(\"test_lines.csv\"));\n\t\tfinal PartitionIter<String> iter = new PartitionIter<>(lineIter, 3);\n\t\tfor (List<String> lines : iter) {\n\t\t\tassertTrue(lines.size() > 0);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void iterMaxTest() {\n\t\tfinal List<Integer> list = ListUtil.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 0, 12, 45, 12);\n\t\tfinal PartitionIter<Integer> iter = new PartitionIter<>(list.iterator(), 3);\n\t\tint max = 0;\n\t\tfor (List<Integer> lines : iter) {\n\t\t\tmax = NumberUtil.max(max, NumberUtil.max(lines.toArray(new Integer[0])));\n\t\t}\n\t\tassertEquals(45, max);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/RingIndexUtilTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport cn.hutool.core.thread.ThreadUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 集合索引环形获取工具类测试类\n *\n * @author ZhouChuGang\n */\npublic class RingIndexUtilTest {\n\n\tprivate final List<String> strList = Arrays.asList(\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\");\n\n\t/**\n\t * 观察输出的打印为不重复的\n\t */\n\t@Test\n\tpublic void ringNextIntByObjTest() {\n\t\tfinal AtomicInteger atomicInteger = new AtomicInteger();\n\t\t// 开启并发测试，每个线程获取到的元素都是唯一的\n\t\tThreadUtil.concurrencyTest(strList.size(), () -> {\n\t\t\tfinal int index = RingIndexUtil.ringNextIntByObj(strList, atomicInteger);\n\t\t\tfinal String s = strList.get(index);\n\t\t\tassertNotNull(s);\n\t\t});\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java",
    "content": "package cn.hutool.core.collection;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\n\npublic class UniqueKeySetTest {\n\n\t@Test\n\tpublic void addTest(){\n\t\tSet<UniqueTestBean> set = new UniqueKeySet<>(UniqueTestBean::getId);\n\t\tset.add(new UniqueTestBean(\"id1\", \"张三\", \"地球\"));\n\t\tset.add(new UniqueTestBean(\"id2\", \"李四\", \"火星\"));\n\t\t// id重复，替换之前的元素\n\t\tset.add(new UniqueTestBean(\"id2\", \"王五\", \"木星\"));\n\n\t\t// 后两个ID重复\n\t\tassertEquals(2, set.size());\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class UniqueTestBean{\n\t\tprivate String id;\n\t\tprivate String name;\n\t\tprivate String address;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/ArrayIndexedComparator.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport java.util.Comparator;\n\n/**\n * 按照数组的顺序正序排列，数组的元素位置决定了对象的排序先后<br>\n * 默认的，如果参与排序的元素并不在数组中，则排序在前（可以通过atEndIfMiss设置)\n *\n * @param <T> 被排序元素类型\n * @author looly\n * @since 5.8.30\n */\npublic class ArrayIndexedComparator<T> implements Comparator<T> {\n\n\tprivate final boolean atEndIfMiss;\n\tprivate final T[] array;\n\n\t/**\n\t * 构造\n\t *\n\t * @param objs 参与排序的数组，数组的元素位置决定了对象的排序先后\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic ArrayIndexedComparator(T... objs) {\n\t\tthis(false, objs);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param atEndIfMiss 如果不在列表中是否排在后边\n\t * @param objs        参与排序的数组，数组的元素位置决定了对象的排序先后\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic ArrayIndexedComparator(boolean atEndIfMiss, T... objs) {\n\t\tAssert.notNull(objs, \"'objs' array must not be null\");\n\t\tthis.atEndIfMiss = atEndIfMiss;\n\t\tthis.array = objs;\n\t}\n\n\t@Override\n\tpublic int compare(T o1, T o2) {\n\t\tfinal int index1 = getOrder(o1);\n\t\tfinal int index2 = getOrder(o2);\n\n\t\tif (index1 == index2) {\n\t\t\tif (index1 < 0 || index1 == this.array.length) {\n\t\t\t\t// 任意一个元素不在列表中, 返回原顺序\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// 位置一样，认为是同一个元素\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn Integer.compare(index1, index2);\n\t}\n\n\t/**\n\t * 查找对象类型所在列表的位置\n\t *\n\t * @param object 对象\n\t * @return 位置，未找到位置根据{@link #atEndIfMiss}取不同值，false返回-1，否则返回列表长度\n\t */\n\tprivate int getOrder(T object) {\n\t\tint order = ArrayUtil.indexOf(array, object);\n\t\tif (order < 0) {\n\t\t\torder = this.atEndIfMiss ? this.array.length : -1;\n\t\t}\n\t\treturn order;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/CompareUtilTest.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.collection.ListUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class CompareUtilTest {\n\n\t@Test\n\tpublic void compareTest(){\n\t\tint compare = CompareUtil.compare(null, \"a\", true);\n\t\tassertTrue(compare > 0);\n\n\t\tcompare = CompareUtil.compare(null, \"a\", false);\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void comparingPinyin() {\n\t\tList<String> list = ListUtil.toList(\"成都\", \"北京\", \"上海\", \"深圳\");\n\n\t\tList<String> ascendingOrderResult = ListUtil.of(\"北京\", \"成都\", \"上海\", \"深圳\");\n\t\tList<String> descendingOrderResult = ListUtil.of(\"深圳\", \"上海\", \"成都\", \"北京\");\n\n\t\t// 正序\n\t\tlist.sort(CompareUtil.comparingPinyin(e -> e));\n\t\tassertEquals(list, ascendingOrderResult);\n\n\t\t// 反序\n\t\tlist.sort(CompareUtil.comparingPinyin(e -> e, true));\n\t\tassertEquals(list, descendingOrderResult);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/IndexedComparatorTest.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.date.StopWatch;\nimport cn.hutool.core.lang.Console;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\n\npublic class IndexedComparatorTest {\n\t@Test\n\tpublic void sortTest() {\n\t\tfinal Object[] arr ={\"a\", \"b\", new User(\"9\", null), \"1\",3,null,\"2\"};\n\t\tfinal Collection<Object> set = new HashSet<>(Arrays.asList(arr));\n\n\n\t\tfinal List<Object> sortSet = CollectionUtil.sort(set, new ArrayIndexedComparator<>(arr));\n\n\t\tassertEquals(\"a\", sortSet.get(0));\n\t\tassertEquals( new User(\"9\", null), sortSet.get(2));\n\t\tassertEquals(3, sortSet.get(4));\n\t\tassertNull(sortSet.get(5));\n\t}\n\n\t@Test\n\tpublic void reversedTest() {\n\t\tfinal Object[] arr ={\"a\", \"b\", new User(\"9\", null), \"1\",3,null,\"2\"};\n\t\tfinal Collection<Object> set = new HashSet<>(Arrays.asList(arr));\n\n\t\tfinal List<Object> sortSet = CollectionUtil.sort(set, new ArrayIndexedComparator<>(arr).reversed());\n\n\t\tassertEquals(\"a\", sortSet.get(6));\n\t\tassertNull(sortSet.get(1));\n\t\tassertEquals( new User(\"9\", null), sortSet.get(4));\n\t\tassertEquals(3, sortSet.get(2));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void benchmarkSortTest() {\n\t\tfinal Object[] arr ={\"a\", \"b\", new User(\"9\", null), \"1\",3,null,\"2\"};\n\t\tfinal Collection<Object> set = new HashSet<>(Arrays.asList(arr));\n\n\t\tfinal StopWatch stopWatch = new StopWatch();\n\n\t\tstopWatch.start();\n\t\tfor (int i = 0; i < 10_000_000; i++) {\n\t\t\tfinal List<Object> sortSet = CollectionUtil.sort(set, new IndexedComparator<>(arr));\n\t\t}\n\t\tstopWatch.stop();\n\n\n\t\tstopWatch.start();\n\t\tfor (int i = 0; i < 10_000_000; i++) {\n\t\t\tfinal List<Object> sortSet = CollectionUtil.sort(set, new ArrayIndexedComparator<>(arr));\n\t\t}\n\t\tstopWatch.stop();\n\t\tConsole.log(stopWatch.prettyPrint());\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class User{\n\t\tprivate String a;\n\t\tprivate String b;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/Issue3259Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.comparator;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.ToString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Issue3259Test {\n\n\t@Test\n\tpublic void fieldsComparatorTest() {\n\t\tModel x = new Model(1, 1);\n\t\tModel y = new Model(1, RandomUtil.randomInt(2, 100));\n\n\t\tassertTrue(new FieldsComparator<>(Model.class, \"a\", \"b\").compare(x, y) < 0);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sortTest() {\n\t\tfor(int i = 2; i < 5; i++) {\n\t\t\tModel x = new Model(1, 1);\n\t\t\tModel y = new Model(1, i);\n\n\t\t\tList<Model> all = ListUtil.of(x, y);\n\t\t\tall = CollUtil.sort(new ArrayList<>(all), new FieldsComparator<>(Model.class, \"a\", \"b\"));\n\t\t\tSystem.out.println(all);\n\t\t}\n\t}\n\n\t@AllArgsConstructor\n\t@ToString\n\tpublic static class Model {\n\t\tpublic int a;\n\t\tpublic int b;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/PropertyComparatorTest.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.collection.ListUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class PropertyComparatorTest {\n\t@Test\n\tpublic void sortNullTest() {\n\t\tfinal ArrayList<User> users = ListUtil.toList(\n\t\t\t\tnew User(\"1\", \"d\"),\n\t\t\t\tnew User(\"2\", null),\n\t\t\t\tnew User(\"3\", \"a\")\n\t\t);\n\n\t\t// 默认null在末尾\n\t\tfinal List<User> sortedList1 = ListUtil.sort(users, new PropertyComparator<>(\"b\"));\n\t\tassertEquals(\"a\", sortedList1.get(0).getB());\n\t\tassertEquals(\"d\", sortedList1.get(1).getB());\n\t\tassertNull(sortedList1.get(2).getB());\n\n\t\t// null在首\n\t\tfinal List<User> sortedList2 = ListUtil.sort(users, new PropertyComparator<>(\"b\", false));\n\t\tassertNull(sortedList2.get(0).getB());\n\t\tassertEquals(\"a\", sortedList2.get(1).getB());\n\t\tassertEquals(\"d\", sortedList2.get(2).getB());\n\t}\n\n\t@Test\n\tpublic void reversedTest() {\n\t\tfinal ArrayList<User> users = ListUtil.toList(\n\t\t\t\tnew User(\"1\", \"d\"),\n\t\t\t\tnew User(\"2\", null),\n\t\t\t\tnew User(\"3\", \"a\")\n\t\t);\n\n\t\t// 反序\n\t\tfinal List<User> sortedList = ListUtil.sort(users, new PropertyComparator<>(\"b\").reversed());\n\t\tassertNull(sortedList.get(0).getB());\n\t\tassertEquals(\"d\", sortedList.get(1).getB());\n\t\tassertEquals(\"a\", sortedList.get(2).getB());\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class User{\n\t\tprivate String a;\n\t\tprivate String b;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java",
    "content": "package cn.hutool.core.comparator;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 版本比较单元测试\n *\n * @author looly\n */\npublic class VersionComparatorTest {\n\n\t@Test\n\tpublic void compareEmptyTest() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"\", \"1.12.1\");\n\t\tassertTrue(compare < 0);\n\n\t\tcompare = VersionComparator.INSTANCE.compare(\"\", null);\n\t\tassertTrue(compare > 0);\n\t\tcompare = VersionComparator.INSTANCE.compare(null, \"\");\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest1() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"1.2.1\", \"1.12.1\");\n\t\tassertTrue(compare < 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"1.12.1\", \"1.2.1\");\n\t\tassertTrue(compare > 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest2() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"1.12.1\", \"1.12.1c\");\n\t\tassertTrue(compare < 0);\n\n\t\tcompare = VersionComparator.INSTANCE.compare(\"1.12.1c\", \"1.12.1\");\n\t\tassertTrue(compare > 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest3() {\n\t\tint compare = VersionComparator.INSTANCE.compare(null, \"1.12.1c\");\n\t\tassertTrue(compare < 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"1.12.1c\", null);\n\t\tassertTrue(compare > 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest4() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"1.13.0\", \"1.12.1c\");\n\t\tassertTrue(compare > 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"1.12.1c\", \"1.13.0\");\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest5() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"V1.2\", \"V1.1\");\n\t\tassertTrue(compare > 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"V1.1\", \"V1.2\");\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTes6() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"V0.0.20170102\", \"V0.0.20170101\");\n\t\tassertTrue(compare > 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"V0.0.20170101\", \"V0.0.20170102\");\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void equalsTest() {\n\t\tVersionComparator first = new VersionComparator();\n\t\tVersionComparator other = new VersionComparator();\n\t\tassertNotEquals(first, other);\n\t}\n\n\t@Test\n\tpublic void versionComparatorTest7() {\n\t\tint compare = VersionComparator.INSTANCE.compare(\"1.12.2\", \"1.12.1c\");\n\t\tassertTrue(compare > 0);\n\n\t\t// 自反测试\n\t\tcompare = VersionComparator.INSTANCE.compare(\"1.12.1c\", \"1.12.2\");\n\t\tassertTrue(compare < 0);\n\t}\n\n\t@Test\n\tpublic void equalsTest2() {\n\t\tfinal int compare = VersionComparator.INSTANCE.compare(\"1.12.0\", \"1.12\");\n\t\tassertEquals(0, compare);\n\t}\n\n\t@Test\n\tpublic void I8Z3VETest() {\n\t\t// 传递性测试\n\t\tint compare = VersionComparator.INSTANCE.compare(\"260\", \"a-34\");\n\t\tassertTrue(compare > 0);\n\t\tcompare = VersionComparator.INSTANCE.compare(\"a-34\", \"a-3\");\n\t\tassertTrue(compare > 0);\n\t\tcompare = VersionComparator.INSTANCE.compare(\"260\", \"a-3\");\n\t\tassertTrue(compare > 0);\n\t}\n\n\t@Test\n\tpublic void startWithNoneNumberTest() {\n\t\tfinal int compare = VersionComparator.INSTANCE.compare(\"V1\", \"A1\");\n\t\tassertTrue(compare > 0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/comparator/WindowsExplorerStringComparatorTest.java",
    "content": "package cn.hutool.core.comparator;\n\nimport cn.hutool.core.lang.Assert;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * {@link WindowsExplorerStringComparator} 单元测试类\n *\n * @author YMNNs\n */\n@SuppressWarnings(\"serial\")\npublic class WindowsExplorerStringComparatorTest {\n\n\tList<String> answer1 = new ArrayList<String>() {{\n\t\tadd(\"filename\");\n\t\tadd(\"filename 00\");\n\t\tadd(\"filename 0\");\n\t\tadd(\"filename 01\");\n\t\tadd(\"filename.jpg\");\n\t\tadd(\"filename.txt\");\n\t\tadd(\"filename00.jpg\");\n\t\tadd(\"filename00a.jpg\");\n\t\tadd(\"filename00a.txt\");\n\t\tadd(\"filename0\");\n\t\tadd(\"filename0.jpg\");\n\t\tadd(\"filename0a.txt\");\n\t\tadd(\"filename0b.jpg\");\n\t\tadd(\"filename0b1.jpg\");\n\t\tadd(\"filename0b02.jpg\");\n\t\tadd(\"filename0c.jpg\");\n\t\tadd(\"filename01.0hjh45-test.txt\");\n\t\tadd(\"filename01.0hjh46\");\n\t\tadd(\"filename01.1hjh45.txt\");\n\t\tadd(\"filename01.hjh45.txt\");\n\t\tadd(\"Filename01.jpg\");\n\t\tadd(\"Filename1.jpg\");\n\t\tadd(\"filename2.hjh45.txt\");\n\t\tadd(\"filename2.jpg\");\n\t\tadd(\"filename03.jpg\");\n\t\tadd(\"filename3.jpg\");\n\t}};\n\n\tList<String> answer2 = new ArrayList<String>() {{\n\t\tadd(\"abc1.doc\");\n\t\tadd(\"abc2.doc\");\n\t\tadd(\"abc12.doc\");\n\t}};\n\n\t@Test\n\tpublic void testCompare1() {\n\t\tList<String> toSort = new ArrayList<>(answer1);\n\t\twhile (toSort.equals(answer1)) {\n\t\t\tCollections.shuffle(toSort);\n\t\t}\n\t\ttoSort.sort(new WindowsExplorerStringComparator());\n\t\tAssert.equals(toSort, answer1);\n\t}\n\n\t@Test\n\tpublic void testCompare2() {\n\t\tList<String> toSort = new ArrayList<>(answer2);\n\t\twhile (toSort.equals(answer2)) {\n\t\t\tCollections.shuffle(toSort);\n\t\t}\n\t\ttoSort.sort(new WindowsExplorerStringComparator());\n\t\tAssert.equals(toSort, answer2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java",
    "content": "package cn.hutool.core.compiler;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.ZipUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.InputStream;\n\n/**\n * Java源码编译器测试\n *\n * @author lzpeng\n */\npublic class JavaSourceCompilerTest {\n\n\t/**\n\t * 测试编译Java源码\n\t */\n\t@Test\n\tpublic void testCompile() throws ClassNotFoundException {\n\t\t// 依赖A，编译B和C\n\t\tfinal File libFile = ZipUtil.zip(FileUtil.file(\"lib.jar\"),\n\t\t\t\tnew String[]{\"a/A.class\", \"a/A$1.class\", \"a/A$InnerClass.class\"},\n\t\t\t\tnew InputStream[]{\n\t\t\t\t\t\tFileUtil.getInputStream(\"test-compile/a/A.class\"),\n\t\t\t\t\t\tFileUtil.getInputStream(\"test-compile/a/A$1.class\"),\n\t\t\t\t\t\tFileUtil.getInputStream(\"test-compile/a/A$InnerClass.class\")\n\t\t\t\t});\n\t\tfinal ClassLoader classLoader = CompilerUtil.getCompiler(null)\n\t\t\t\t.addSource(FileUtil.file(\"test-compile/b/B.java\"))\n\t\t\t\t.addSource(\"c.C\", FileUtil.readUtf8String(\"test-compile/c/C.java\"))\n\t\t\t\t.addLibrary(libFile)\n//\t\t\t\t.addLibrary(FileUtil.file(\"D:\\\\m2_repo\\\\cn\\\\hutool\\\\hutool-all\\\\5.5.7\\\\hutool-all-5.5.7.jar\"))\n\t\t\t\t.compile();\n\t\tfinal Class<?> clazz = classLoader.loadClass(\"c.C\");\n\t\tfinal Object obj = ReflectUtil.newInstance(clazz);\n\t\tassertTrue(String.valueOf(obj).startsWith(\"c.C@\"));\n\t}\n\n\t@Test\n\tpublic void testErrorCompile() {\n\t\tException exception = null;\n\t\ttry {\n\t\t\tCompilerUtil.getCompiler(null)\n\t\t\t\t\t.addSource(FileUtil.file(\"test-compile/error/ErrorClazz.java\"))\n\t\t\t\t\t.compile();\n\t\t} catch (final Exception ex) {\n\t\t\texception = ex;\n\t\t} finally {\n\t\t\tassertTrue(exception instanceof CompilerException);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/compress/IssueI5DRU0Test.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.util.ZipUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\npublic class IssueI5DRU0Test {\n\n\t@Test\n\t@Disabled\n\tpublic void appendTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I5DRU0\n\t\t// 向zip中添加文件的时候，如果添加的文件的父目录已经存在，会报错。实际中目录存在忽略即可。\n\t\tZipUtil.append(Paths.get(\"d:/test/zipTest.zip\"), Paths.get(\"d:/test/zipTest\"), StandardCopyOption.REPLACE_EXISTING);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/compress/IssueIAGYDGTest.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.util.ZipUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/IAGYDG\n */\npublic class IssueIAGYDGTest {\n\t@Test\n\t@Disabled\n\tpublic void zipTest() {\n\t\t// 第一次压缩后，IssueIAGYDG.zip也会作为文件压缩到IssueIAGYDG.zip中，导致死循环\n\t\tfinal File filea = new File(\"d:/test/\");\n\t\tfinal File fileb = new File(\"d:/test/IssueIAGYDG.zip\");\n\t\tZipUtil.zip(fileb, false, filea.listFiles());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/compress/ZipReaderTest.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.ZipUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\npublic class ZipReaderTest {\n\n\t@Test\n\t@Disabled\n\tpublic void unzipTest() {\n\t\tFile unzip = ZipUtil.unzip(\"d:/java.zip\", \"d:/test/java\");\n\t\tConsole.log(unzip);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/compress/ZipWriterTest.java",
    "content": "package cn.hutool.core.compress;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.FileResource;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ZipUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\npublic class ZipWriterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void zipDirTest() {\n\t\tZipUtil.zip(new File(\"d:/test\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addTest(){\n\t\tfinal ZipWriter writer = ZipWriter.of(FileUtil.file(\"d:/test/test.zip\"), CharsetUtil.CHARSET_UTF_8);\n\t\twriter.add(new FileResource(\"d:/test/qr_c.png\"));\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/CastUtilTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.collection.CollUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class CastUtilTest {\n\n\t@Test\n\tpublic void testCastToSuper() {\n\t\tCollection<Integer> collection= CollUtil.newLinkedList(1,2,3);\n\t\tList<Integer> list = CollUtil.newArrayList(1, 2, 3);\n\t\tSet<Integer> set = CollUtil.newHashSet(1, 2, 3);\n\t\tMap<Integer, Integer> map = new HashMap<>();\n\t\tmap.put(1, 1);\n\n\t\tCollection<Number> collection2 = CastUtil.castUp(collection);\n\t\tassertSame(collection, collection2);\n\n\t\tCollection<Integer> collection3 = CastUtil.castDown(collection2);\n\t\tassertSame(collection2, collection3);\n\n\t\tList<Number> list2 = CastUtil.castUp(list);\n\t\tassertSame(list, list2);\n\t\tList<Integer> list3 = CastUtil.castDown(list2);\n\t\tassertSame(list2, list3);\n\n\t\tSet<Number> set2 = CastUtil.castUp(set);\n\t\tassertSame(set, set2);\n\t\tSet<Integer> set3 = CastUtil.castDown(set2);\n\t\tassertSame(set2, set3);\n\n\t\tMap<Number, Serializable> map2 = CastUtil.castUp(map);\n\t\tassertSame(map, map2);\n\t\tMap<Integer, Number> map3 = CastUtil.castDown(map2);\n\t\tassertSame(map2, map3);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertOtherTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 其它转换\n * @author Looly\n *\n */\npublic class ConvertOtherTest {\n\t@Test\n\tpublic void hexTest() {\n\t\tString a = \"我是一个小小的可爱的字符串\";\n\t\tString hex = Convert.toHex(a, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2\", hex);\n\n\t\tString raw = Convert.hexToStr(hex, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(a, raw);\n\t}\n\n\t@Test\n\tpublic void unicodeTest() {\n\t\tString a = \"我是一个小小的可爱的字符串\";\n\n\t\tString unicode = Convert.strToUnicode(a);\n\t\tassertEquals(\"\\\\u6211\\\\u662f\\\\u4e00\\\\u4e2a\\\\u5c0f\\\\u5c0f\\\\u7684\\\\u53ef\\\\u7231\\\\u7684\\\\u5b57\\\\u7b26\\\\u4e32\", unicode);\n\n\t\tString raw = Convert.unicodeToStr(unicode);\n\t\tassertEquals(raw, a);\n\n\t\t// 针对有特殊空白符的Unicode\n\t\tString str = \"你 好\";\n\t\tString unicode2 = Convert.strToUnicode(str);\n\t\tassertEquals(\"\\\\u4f60\\\\u00a0\\\\u597d\", unicode2);\n\n\t\tString str2 = Convert.unicodeToStr(unicode2);\n\t\tassertEquals(str, str2);\n\t}\n\n\t@Test\n\tpublic void convertCharsetTest() {\n\t\tString a = \"我不是乱码\";\n\t\t// 转换后result为乱码\n\t\tString result = Convert.convertCharset(a, CharsetUtil.UTF_8, CharsetUtil.ISO_8859_1);\n\t\tString raw = Convert.convertCharset(result, CharsetUtil.ISO_8859_1, \"UTF-8\");\n\t\tassertEquals(raw, a);\n\t}\n\n\t@Test\n\tpublic void convertTimeTest() {\n\t\tlong a = 4535345;\n\t\tlong minutes = Convert.convertTime(a, TimeUnit.MILLISECONDS, TimeUnit.MINUTES);\n\t\tassertEquals(75, minutes);\n\t}\n\n\t@Test\n\tpublic void wrapUnwrapTest() {\n\t\t// 去包装\n\t\tClass<?> wrapClass = Integer.class;\n\t\tClass<?> unWraped = Convert.unWrap(wrapClass);\n\t\tassertEquals(int.class, unWraped);\n\n\t\t// 包装\n\t\tClass<?> primitiveClass = long.class;\n\t\tClass<?> wraped = Convert.wrap(primitiveClass);\n\t\tassertEquals(Long.class, wraped);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateException;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ByteUtil;\nimport cn.hutool.core.util.HexUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.Getter;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.math.BigDecimal;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicIntegerArray;\nimport java.util.concurrent.atomic.AtomicLongArray;\nimport java.util.concurrent.atomic.DoubleAdder;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 类型转换工具单元测试\n *\n * @author Looly\n */\npublic class ConvertTest {\n\n\t@Test\n\tpublic void toObjectTest() {\n\t\tfinal Object result = Convert.convert(Object.class, \"aaaa\");\n\t\tassertEquals(\"aaaa\", result);\n\t}\n\n\t@Test\n\tpublic void toStrTest() {\n\t\tfinal int a = 1;\n\t\tfinal long[] b = {1, 2, 3, 4, 5};\n\n\t\tassertEquals(\"[1, 2, 3, 4, 5]\", Convert.convert(String.class, b));\n\n\t\tfinal String aStr = Convert.toStr(a);\n\t\tassertEquals(\"1\", aStr);\n\t\tfinal String bStr = Convert.toStr(b);\n\t\tassertEquals(\"[1, 2, 3, 4, 5]\", Convert.toStr(bStr));\n\t}\n\n\t@Test\n\tpublic void toStrTest2() {\n\t\tfinal String result = Convert.convert(String.class, \"aaaa\");\n\t\tassertEquals(\"aaaa\", result);\n\t}\n\n\t@Test\n\tpublic void toStrTest3() {\n\t\tfinal char a = 'a';\n\t\tfinal String result = Convert.convert(String.class, a);\n\t\tassertEquals(\"a\", result);\n\t}\n\n\t@Test\n\tpublic void toStrTest4() {\n\t\t// 被当作八进制\n\t\t@SuppressWarnings(\"OctalInteger\") final String result = Convert.toStr(001200);\n\t\tassertEquals(\"640\", result);\n\t}\n\n\t@Test\n\tpublic void toIntTest() {\n\t\tfinal String a = \" 34232\";\n\t\tfinal Integer aInteger = Convert.toInt(a);\n\t\tassertEquals(Integer.valueOf(34232), aInteger);\n\t\tfinal int aInt = ConverterRegistry.getInstance().convert(int.class, a);\n\t\tassertEquals(34232, aInt);\n\n\t\t// 带小数测试\n\t\tfinal String b = \" 34232.00\";\n\t\tfinal Integer bInteger = Convert.toInt(b);\n\t\tassertEquals(Integer.valueOf(34232), bInteger);\n\t\tfinal int bInt = ConverterRegistry.getInstance().convert(int.class, b);\n\t\tassertEquals(34232, bInt);\n\n\t\t// boolean测试\n\t\tfinal boolean c = true;\n\t\tfinal Integer cInteger = Convert.toInt(c);\n\t\tassertEquals(Integer.valueOf(1), cInteger);\n\t\tfinal int cInt = ConverterRegistry.getInstance().convert(int.class, c);\n\t\tassertEquals(1, cInt);\n\n\t\t// boolean测试\n\t\tfinal String d = \"08\";\n\t\tfinal Integer dInteger = Convert.toInt(d);\n\t\tassertEquals(Integer.valueOf(8), dInteger);\n\t\tfinal int dInt = ConverterRegistry.getInstance().convert(int.class, d);\n\t\tassertEquals(8, dInt);\n\t}\n\n\t@Test\n\tpublic void toIntTest2() {\n\t\tfinal ArrayList<String> array = new ArrayList<>();\n\t\tfinal Integer aInt = Convert.convertQuietly(Integer.class, array, -1);\n\t\tassertEquals(Integer.valueOf(-1), aInt);\n\t}\n\n\t@Test\n\tpublic void toLongTest() {\n\t\tfinal String a = \" 342324545435435\";\n\t\tfinal Long aLong = Convert.toLong(a);\n\t\tassertEquals(Long.valueOf(342324545435435L), aLong);\n\t\tfinal long aLong2 = ConverterRegistry.getInstance().convert(long.class, a);\n\t\tassertEquals(342324545435435L, aLong2);\n\n\t\t// 带小数测试\n\t\tfinal String b = \" 342324545435435.245435435\";\n\t\tfinal Long bLong = Convert.toLong(b);\n\t\tassertEquals(Long.valueOf(342324545435435L), bLong);\n\t\tfinal long bLong2 = ConverterRegistry.getInstance().convert(long.class, b);\n\t\tassertEquals(342324545435435L, bLong2);\n\n\t\t// boolean测试\n\t\tfinal boolean c = true;\n\t\tfinal Long cLong = Convert.toLong(c);\n\t\tassertEquals(Long.valueOf(1), cLong);\n\t\tfinal long cLong2 = ConverterRegistry.getInstance().convert(long.class, c);\n\t\tassertEquals(1, cLong2);\n\n\t\t// boolean测试\n\t\tfinal String d = \"08\";\n\t\tfinal Long dLong = Convert.toLong(d);\n\t\tassertEquals(Long.valueOf(8), dLong);\n\t\tfinal long dLong2 = ConverterRegistry.getInstance().convert(long.class, d);\n\t\tassertEquals(8, dLong2);\n\t}\n\n\t@Test\n\tpublic void toLongFromNumberWithFormatTest() {\n\t\tfinal NumberWithFormat value = new NumberWithFormat(1678285713935L, null);\n\t\tfinal Long aLong = Convert.convertWithCheck(Long.class, value, null, false);\n\t\tassertEquals(new Long(1678285713935L), aLong);\n\t}\n\n\t@Test\n\tpublic void toCharTest() {\n\t\tfinal String str = \"aadfdsfs\";\n\t\tfinal Character c = Convert.toChar(str);\n\t\tassertEquals(Character.valueOf('a'), c);\n\n\t\t// 转换失败\n\t\tfinal Object str2 = \"\";\n\t\tfinal Character c2 = Convert.toChar(str2);\n\t\tassertNull(c2);\n\t}\n\n\t@Test\n\tpublic void toNumberTest() {\n\t\tfinal Object a = \"12.45\";\n\t\tfinal Number number = Convert.toNumber(a);\n\t\tassertEquals(12.45D, number.doubleValue(), 0);\n\t}\n\n\t@Test\n\tpublic void emptyToNumberTest() {\n\t\tfinal Object a = \"\";\n\t\tfinal Number number = Convert.toNumber(a);\n\t\tassertNull(number);\n\t}\n\n\t@Test\n\tpublic void intAndByteConvertTest() {\n\t\t// 测试 int 转 byte\n\t\tfinal int int0 = 234;\n\t\tfinal byte byte0 = Convert.intToByte(int0);\n\t\tassertEquals(-22, byte0);\n\n\t\tfinal int int1 = Convert.byteToUnsignedInt(byte0);\n\t\tassertEquals(int0, int1);\n\t}\n\n\t@Test\n\tpublic void intAndBytesTest() {\n\t\t// 测试 int 转 byte 数组\n\t\tfinal int int2 = 1417;\n\t\tfinal byte[] bytesInt = Convert.intToBytes(int2);\n\n\t\t// 测试 byte 数组转 int\n\t\tfinal int int3 = Convert.bytesToInt(bytesInt);\n\t\tassertEquals(int2, int3);\n\t}\n\n\t@Test\n\tpublic void longAndBytesTest() {\n\t\t// 测试 long 转 byte 数组\n\t\tfinal long long1 = 2223;\n\n\t\tfinal byte[] bytesLong = Convert.longToBytes(long1);\n\t\tfinal long long2 = Convert.bytesToLong(bytesLong);\n\n\t\tassertEquals(long1, long2);\n\t}\n\n\t@Test\n\tpublic void shortAndBytesTest() {\n\t\tfinal short short1 = 122;\n\t\tfinal byte[] bytes = Convert.shortToBytes(short1);\n\t\tfinal short short2 = Convert.bytesToShort(bytes);\n\n\t\tassertEquals(short2, short1);\n\t}\n\n\t@Test\n\tpublic void toListTest() {\n\t\tfinal List<String> list = Arrays.asList(\"1\", \"2\");\n\t\tfinal String str = Convert.toStr(list);\n\t\tfinal List<String> list2 = Convert.toList(String.class, str);\n\t\tassertEquals(\"1\", list2.get(0));\n\t\tassertEquals(\"2\", list2.get(1));\n\n\t\tfinal List<Integer> list3 = Convert.toList(Integer.class, str);\n\t\tassertEquals(1, list3.get(0).intValue());\n\t\tassertEquals(2, list3.get(1).intValue());\n\t}\n\n\t@Test\n\tpublic void toListTest2() {\n\t\tfinal String str = \"1,2\";\n\t\tfinal List<String> list2 = Convert.toList(String.class, str);\n\t\tassertEquals(\"1\", list2.get(0));\n\t\tassertEquals(\"2\", list2.get(1));\n\n\t\tfinal List<Integer> list3 = Convert.toList(Integer.class, str);\n\t\tassertEquals(1, list3.get(0).intValue());\n\t\tassertEquals(2, list3.get(1).intValue());\n\t}\n\n\t@Test\n\tpublic void toByteArrayTest() {\n\t\t// 测试Serializable转换为bytes，调用序列化转换\n\t\tfinal byte[] bytes = Convert.toPrimitiveByteArray(new Product(\"zhangsan\", \"张三\", \"5.1.1\"));\n\t\tassertNotNull(bytes);\n\n\t\tfinal Product product = Convert.convert(Product.class, bytes);\n\t\tassertEquals(\"zhangsan\", product.getName());\n\t\tassertEquals(\"张三\", product.getCName());\n\t\tassertEquals(\"5.1.1\", product.getVersion());\n\t}\n\n\t@Test\n\tpublic void numberToByteArrayTest() {\n\t\t// 测试Serializable转换为bytes，调用序列化转换\n\t\tfinal byte[] bytes = Convert.toPrimitiveByteArray(12L);\n\t\tassertArrayEquals(ByteUtil.longToBytes(12L), bytes);\n\t}\n\n\t@Test\n\tpublic void toAtomicIntegerArrayTest() {\n\t\tfinal String str = \"1,2\";\n\t\tfinal AtomicIntegerArray atomicIntegerArray = Convert.convert(AtomicIntegerArray.class, str);\n\t\tassertEquals(\"[1, 2]\", atomicIntegerArray.toString());\n\t}\n\n\t@Test\n\tpublic void toAtomicLongArrayTest() {\n\t\tfinal String str = \"1,2\";\n\t\tfinal AtomicLongArray atomicLongArray = Convert.convert(AtomicLongArray.class, str);\n\t\tassertEquals(\"[1, 2]\", atomicLongArray.toString());\n\t}\n\n\t@Test\n\tpublic void toClassTest() {\n\t\tfinal Class<?> convert = Convert.convert(Class.class, \"cn.hutool.core.convert.ConvertTest.Product\");\n\t\tassertSame(Product.class, convert);\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tpublic static class Product implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate String name;\n\t\tprivate String cName;\n\t\tprivate String version;\n\t}\n\n\t@Test\n\tpublic void enumToIntTest() {\n\t\tfinal Integer integer = Convert.toInt(BuildingType.CUO);\n\t\tassertEquals(1, integer.intValue());\n\t}\n\n\t@Test\n\tpublic void toSetTest() {\n\t\tfinal Set<Integer> result = Convert.convert(new TypeReference<Set<Integer>>() {\n\t\t}, \"1,2,3\");\n\t\tassertEquals(CollUtil.set(false, 1, 2, 3), result);\n\t}\n\n\t@Getter\n\tpublic enum BuildingType {\n\t\tPING(1, \"平层\"),\n\t\tCUO(2, \"错层\"),\n\t\tYUE(3, \"跃层\"),\n\t\tFUSHI(4, \"复式\"),\n\t\tKAIJIAN(5, \"开间\"),\n\t\tOTHER(6, \"其他\");\n\n\t\tprivate final int id;\n\t\tprivate final String name;\n\n\t\tBuildingType(final int id, final String name) {\n\t\t\tthis.id = id;\n\t\t\tthis.name = name;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void toDateTest() {\n\t\tassertThrows(DateException.class, () -> {\n\t\t\t// 默认转换失败报错而不是返回null\n\t\t\tConvert.convert(Date.class, \"aaaa\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void toDateTest2() {\n\t\tfinal Date date = Convert.toDate(\"2021-01\");\n\t\tassertNull(date);\n\t}\n\n\t@Test\n\tpublic void toSqlDateTest() {\n\t\tfinal java.sql.Date date = Convert.convert(java.sql.Date.class, DateUtil.parse(\"2021-07-28\"));\n\t\tassertEquals(\"2021-07-28\", date.toString());\n\t}\n\n\t@Test\n\tpublic void toHashtableTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a1\", \"v1\");\n\t\tmap.put(\"a2\", \"v2\");\n\t\tmap.put(\"a3\", \"v3\");\n\n\t\t@SuppressWarnings(\"unchecked\") final Hashtable<String, String> hashtable = Convert.convert(Hashtable.class, map);\n\t\tassertEquals(\"v1\", hashtable.get(\"a1\"));\n\t\tassertEquals(\"v2\", hashtable.get(\"a2\"));\n\t\tassertEquals(\"v3\", hashtable.get(\"a3\"));\n\t}\n\n\t@Test\n\tpublic void toBigDecimalTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/1818\n\t\tfinal String str = \"33020000210909112800000124\";\n\t\tfinal BigDecimal bigDecimal = Convert.toBigDecimal(str);\n\t\tassertEquals(str, bigDecimal.toPlainString());\n\t}\n\n\t@Test\n\tpublic void toFloatTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4M0E4\n\t\tfinal String hex2 = \"CD0CCB43\";\n\t\tfinal byte[] value = HexUtil.decodeHex(hex2);\n\t\tfinal float f = Convert.toFloat(value);\n\t\tassertEquals(406.1F, f, 0);\n\t}\n\n\t@Test\n\tpublic void floatToDoubleTest() {\n\t\tfinal float a = 0.45f;\n\t\tfinal double b = Convert.toDouble(a);\n\t\tassertEquals(0.45D, b, 0);\n\t}\n\n\t@Test\n\tpublic void floatToDoubleAddrTest() {\n\t\tfinal float a = 0.45f;\n\t\tfinal DoubleAdder adder = Convert.convert(DoubleAdder.class, a);\n\t\tassertEquals(0.45D, adder.doubleValue(), 0);\n\t}\n\n\t@Test\n\tpublic void doubleToFloatTest() {\n\t\tfinal double a = 0.45f;\n\t\tfinal float b = Convert.toFloat(a);\n\t\tassertEquals(a, b, 0);\n\t}\n\n\t@Test\n\tpublic void localDateTimeToLocalDateTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTime.now();\n\t\tfinal LocalDate convert = Convert.convert(LocalDate.class, localDateTime);\n\t\tassertEquals(localDateTime.toLocalDate(), convert);\n\t}\n\n\t@Test\n\tpublic void toSBCTest() {\n\t\tfinal String s = Convert.toSBC(null);\n\t\tassertNull(s);\n\t}\n\n\t@Test\n\tpublic void toDBCTest() {\n\t\tfinal String s = Convert.toDBC(null);\n\t\tassertNull(s);\n\t}\n\n\t@Test\n\tpublic void testChineseMoneyToNumber() {\n\t\t/*\n\t\t * s=陆万柒仟伍佰伍拾陆圆, n=67556\n\t\t * s=陆万柒仟伍佰伍拾陆元, n=67556\n\t\t * s=叁角, n=0.3\n\t\t * s=贰分, n=0.02\n\t\t * s=陆万柒仟伍佰伍拾陆元叁角, n=67556.3\n\t\t * s=陆万柒仟伍佰伍拾陆元贰分, n=67556.02\n\t\t * s=叁角贰分, n=0.32\n\t\t * s=陆万柒仟伍佰伍拾陆元叁角贰分, n=67556.32\n\t\t */\n\t\tassertEquals(67556, Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾陆圆\").longValue());\n\t\tassertEquals(67556, Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾陆元\").longValue());\n\t\tassertEquals(0.3D, Convert.chineseMoneyToNumber(\"叁角\").doubleValue(), 0);\n\t\tassertEquals(0.02, Convert.chineseMoneyToNumber(\"贰分\").doubleValue(), 0);\n\t\tassertEquals(67556.3, Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾陆元叁角\").doubleValue(), 0);\n\t\tassertEquals(67556.02, Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾陆元贰分\").doubleValue(), 0);\n\t\tassertEquals(0.32, Convert.chineseMoneyToNumber(\"叁角贰分\").doubleValue(), 0);\n\t\tassertEquals(67556.32, Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾陆元叁角贰分\").doubleValue(), 0);\n\t}\n\n\t@Test\n\tpublic void convertQuietlyTest() {\n\t\tassertThrows(Exception.class, () -> {\n\t\t\tfinal String a = \"12\";\n\t\t\tfinal Object s = Convert.convert(int.class, a, a);\n\t\t\tassertEquals(12, s);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issue3662Test() {\n\t\tString s = Convert.digitToChinese(0);\n\t\tassertEquals(\"零元整\", s);\n\n\t\ts = Convert.digitToChinese(null);\n\t\tassertEquals(\"零元整\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToArrayTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.convert.impl.ArrayConverter;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URL;\nimport java.util.ArrayList;\n\n/**\n * 类型转换工具单元测试<br>\n * 转换为数组\n *\n * @author Looly\n *\n */\npublic class ConvertToArrayTest {\n\n\t@Test\n\tpublic void toIntArrayTest() {\n\t\tString[] b = { \"1\", \"2\", \"3\", \"4\" };\n\n\t\tInteger[] integerArray = Convert.toIntArray(b);\n\t\tassertArrayEquals(integerArray, new Integer[]{1,2,3,4});\n\n\t\tint[] intArray = Convert.convert(int[].class, b);\n\t\tassertArrayEquals(intArray, new int[]{1,2,3,4});\n\n\t\tlong[] c = {1,2,3,4,5};\n\t\tInteger[] intArray2 = Convert.toIntArray(c);\n\t\tassertArrayEquals(intArray2, new Integer[]{1,2,3,4,5});\n\t}\n\n\t@Test\n\tpublic void toIntArrayTestIgnoreComponentErrorTest() {\n\t\tString[] b = { \"a\", \"1\" };\n\n\t\tfinal ArrayConverter arrayConverter = new ArrayConverter(Integer[].class, true);\n\t\tInteger[] integerArray = (Integer[]) arrayConverter.convert(b, null);\n\t\tassertArrayEquals(integerArray, new Integer[]{null, 1});\n\t}\n\n\t@Test\n\tpublic void toLongArrayTest() {\n\t\tString[] b = { \"1\", \"2\", \"3\", \"4\" };\n\n\t\tLong[] longArray = Convert.toLongArray(b);\n\t\tassertArrayEquals(longArray, new Long[]{1L,2L,3L,4L});\n\n\t\tlong[] longArray2 = Convert.convert(long[].class, b);\n\t\tassertArrayEquals(longArray2, new long[]{1L,2L,3L,4L});\n\n\t\tint[] c = {1,2,3,4,5};\n\t\tLong[] intArray2 = Convert.toLongArray(c);\n\t\tassertArrayEquals(intArray2, new Long[]{1L,2L,3L,4L,5L});\n\t}\n\n\t@Test\n\tpublic void toDoubleArrayTest() {\n\t\tString[] b = { \"1\", \"2\", \"3\", \"4\" };\n\n\t\tDouble[] doubleArray = Convert.toDoubleArray(b);\n\t\tassertArrayEquals(doubleArray, new Double[]{1D,2D,3D,4D});\n\n\t\tdouble[] doubleArray2 = Convert.convert(double[].class, b);\n\t\tassertArrayEquals(doubleArray2, new double[]{1D,2D,3D,4D}, 2);\n\n\t\tint[] c = {1,2,3,4,5};\n\t\tDouble[] intArray2 = Convert.toDoubleArray(c);\n\t\tassertArrayEquals(intArray2, new Double[]{1D,2D,3D,4D,5D});\n\t}\n\n\t@Test\n\tpublic void toPrimitiveArrayTest(){\n\n\t\t//数组转数组测试\n\t\tint[] a = new int[]{1,2,3,4};\n\t\tlong[] result = ConverterRegistry.getInstance().convert(long[].class, a);\n\t\tassertArrayEquals(new long[]{1L, 2L, 3L, 4L}, result);\n\n\t\t//数组转数组测试\n\t\tbyte[] resultBytes = ConverterRegistry.getInstance().convert(byte[].class, a);\n\t\tassertArrayEquals(new byte[]{1, 2, 3, 4}, resultBytes);\n\n\t\t//字符串转数组\n\t\tString arrayStr = \"1,2,3,4,5\";\n\t\t//获取Converter类的方法2，自己实例化相应Converter对象\n\t\tArrayConverter c3 = new ArrayConverter(int[].class);\n\t\tint[] result3 = (int[]) c3.convert(arrayStr, null);\n\t\tassertArrayEquals(new int[]{1,2,3,4,5}, result3);\n\t}\n\n\t@Test\n\tpublic void collectionToArrayTest() {\n\t\tArrayList<Object> list = new ArrayList<>();\n\t\tlist.add(\"a\");\n\t\tlist.add(\"b\");\n\t\tlist.add(\"c\");\n\n\t\tString[] result = Convert.toStrArray(list);\n\t\tassertEquals(list.get(0), result[0]);\n\t\tassertEquals(list.get(1), result[1]);\n\t\tassertEquals(list.get(2), result[2]);\n\t}\n\n\t@Test\n\tpublic void strToCharArrayTest() {\n\t\tString testStr = \"abcde\";\n\t\tCharacter[] array = Convert.toCharArray(testStr);\n\n\t\t//包装类型数组\n\t\tassertEquals(new Character('a'), array[0]);\n\t\tassertEquals(new Character('b'), array[1]);\n\t\tassertEquals(new Character('c'), array[2]);\n\t\tassertEquals(new Character('d'), array[3]);\n\t\tassertEquals(new Character('e'), array[4]);\n\n\t\t//原始类型数组\n\t\tchar[] array2 = Convert.convert(char[].class, testStr);\n\t\tassertEquals('a', array2[0]);\n\t\tassertEquals('b', array2[1]);\n\t\tassertEquals('c', array2[2]);\n\t\tassertEquals('d', array2[3]);\n\t\tassertEquals('e', array2[4]);\n\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void toUrlArrayTest() {\n\t\tFile[] files = FileUtil.file(\"D:\\\\workspace\").listFiles();\n\n\t\tURL[] urls = Convert.convert(URL[].class, files);\n\n\t\tfor (URL url : urls) {\n\t\t\tConsole.log(url.getPath());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBeanTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.bean.BeanUtilTest.SubPerson;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 类型转换工具单元测试<br>\n * 转换为数组\n *\n * @author Looly\n *\n */\npublic class ConvertToBeanTest {\n\n\t@Test\n\tpublic void beanToMapTest() {\n\t\tSubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tMap<?, ?> map = Convert.convert(Map.class, person);\n\t\tassertEquals(map.get(\"name\"), \"测试A11\");\n\t\tassertEquals(map.get(\"age\"), 14);\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\t}\n\n\t@Test\n\tpublic void beanToMapTest2() {\n\t\tSubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tMap<String, String> map = Convert.toMap(String.class, String.class, person);\n\t\tassertEquals(\"测试A11\", map.get(\"name\"));\n\t\tassertEquals(\"14\", map.get(\"age\"));\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\n\t\tfinal LinkedHashMap<String, String> map2 = Convert.convert(\n\t\t\t\tnew TypeReference<LinkedHashMap<String, String>>() {}, person);\n\t\tassertEquals(\"测试A11\", map2.get(\"name\"));\n\t\tassertEquals(\"14\", map2.get(\"age\"));\n\t\tassertEquals(\"11213232\", map2.get(\"openid\"));\n\t}\n\n\t@Test\n\tpublic void mapToMapTest() {\n\t\tLinkedHashMap<String, Integer> map1 = new LinkedHashMap<>();\n\t\tmap1.put(\"key1\", 1);\n\t\tmap1.put(\"key2\", 2);\n\t\tmap1.put(\"key3\", 3);\n\t\tmap1.put(\"key4\", 4);\n\n\t\tMap<String, String> map2 = Convert.toMap(String.class, String.class, map1);\n\n\t\tassertEquals(\"1\", map2.get(\"key1\"));\n\t\tassertEquals(\"2\", map2.get(\"key2\"));\n\t\tassertEquals(\"3\", map2.get(\"key3\"));\n\t\tassertEquals(\"4\", map2.get(\"key4\"));\n\t}\n\t@Test\n\tpublic void mapToMapWithSelfTypeTest() {\n\t\tCaseInsensitiveMap<String, Integer> caseInsensitiveMap = new CaseInsensitiveMap<>();\n\t\tcaseInsensitiveMap.put(\"jerry\", 1);\n\t\tcaseInsensitiveMap.put(\"Jerry\", 2);\n\t\tcaseInsensitiveMap.put(\"tom\", 3);\n\n\t\tMap<String, String> map = Convert.toMap(String.class, String.class, caseInsensitiveMap);\n\t\tassertEquals(\"2\", map.get(\"jerry\"));\n\t\tassertEquals(\"2\", map.get(\"Jerry\"));\n\t\tassertEquals(\"3\", map.get(\"tom\"));\n\t}\n\t@Test\n\tpublic void beanToSpecifyMapTest() {\n\t\tSubPerson person = new SubPerson();\n\t\tperson.setAge(14);\n\t\tperson.setOpenid(\"11213232\");\n\t\tperson.setName(\"测试A11\");\n\t\tperson.setSubName(\"sub名字\");\n\n\t\tMap<String, String> map = Convert.toMap(LinkedHashMap.class, String.class, String.class, person);\n\t\tassertEquals(\"测试A11\", map.get(\"name\"));\n\t\tassertEquals(\"14\", map.get(\"age\"));\n\t\tassertEquals(\"11213232\", map.get(\"openid\"));\n\t}\n\t@Test\n\tpublic void mapToBeanTest() {\n\t\tHashMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"id\", \"88dc4b28-91b1-4a1a-bab5-444b795c7ecd\");\n\t\tmap.put(\"age\", 14);\n\t\tmap.put(\"openid\", \"11213232\");\n\t\tmap.put(\"name\", \"测试A11\");\n\t\tmap.put(\"subName\", \"sub名字\");\n\n\t\tSubPerson subPerson = Convert.convert(SubPerson.class, map);\n\t\tassertEquals(\"88dc4b28-91b1-4a1a-bab5-444b795c7ecd\", subPerson.getId().toString());\n\t\tassertEquals(14, subPerson.getAge());\n\t\tassertEquals(\"11213232\", subPerson.getOpenid());\n\t\tassertEquals(\"测试A11\", subPerson.getName());\n\t\tassertEquals(\"11213232\", subPerson.getOpenid());\n\t}\n\n\t@Test\n\tpublic void nullStrToBeanTest(){\n\t\tString nullStr = \"null\";\n\t\tfinal SubPerson subPerson = Convert.convertQuietly(SubPerson.class, nullStr);\n\t\tassertNull(subPerson);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBooleanTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ConvertToBooleanTest {\n\n\t@Test\n\tpublic void intToBooleanTest(){\n\t\tint a = 100;\n\t\tfinal Boolean aBoolean = Convert.toBool(a);\n\t\tassertTrue(aBoolean);\n\n\t\tint b = 0;\n\t\tfinal Boolean bBoolean = Convert.toBool(b);\n\t\tassertFalse(bBoolean);\n\t}\n\n\t@Test\n\tvoid toBooleanWithDefaultTest() {\n\t\tAssertions.assertFalse(Convert.toBool(\"ddddd\", false));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToCollectionTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * 转换为集合测试\n *\n * @author looly\n *\n */\npublic class ConvertToCollectionTest {\n\n\t@Test\n\tpublic void toCollectionTest() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<?> list = (List<?>) Convert.convert(Collection.class, a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(1, list.get(4));\n\t}\n\n\t@Test\n\tpublic void toListTest() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<?> list = Convert.toList(a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(1, list.get(4));\n\t}\n\n\t@Test\n\tpublic void toListTest2() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<String> list = Convert.toList(String.class, a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(\"1\", list.get(4));\n\t}\n\n\t@Test\n\tpublic void toListTest3() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<String> list = Convert.toList(String.class, a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(\"1\", list.get(4));\n\t}\n\n\t@Test\n\tpublic void toListTest4() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<String> list = Convert.convert(new TypeReference<List<String>>() {}, a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(\"1\", list.get(4));\n\t}\n\n\t@Test\n\tpublic void strToListTest() {\n\t\tString a = \"a,你,好,123\";\n\t\tList<?> list = Convert.toList(a);\n\t\tassertEquals(4, list.size());\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"123\", list.get(3));\n\n\t\tString b = \"a\";\n\t\tList<?> list2 = Convert.toList(b);\n\t\tassertEquals(1, list2.size());\n\t\tassertEquals(\"a\", list2.get(0));\n\t}\n\n\t@Test\n\tpublic void strToListTest2() {\n\t\tString a = \"a,你,好,123\";\n\t\tList<String> list = Convert.toList(String.class, a);\n\t\tassertEquals(4, list.size());\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"123\", list.get(3));\n\t}\n\n\t@Test\n\tpublic void numberToListTest() {\n\t\tInteger i = 1;\n\t\tArrayList<?> list = Convert.convert(ArrayList.class, i);\n\t\tassertSame(i, list.get(0));\n\n\t\tBigDecimal b = BigDecimal.ONE;\n\t\tArrayList<?> list2 = Convert.convert(ArrayList.class, b);\n\t\tassertEquals(b, list2.get(0));\n\t}\n\n\t@Test\n\tpublic void toLinkedListTest() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tList<?> list = Convert.convert(LinkedList.class, a);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(1, list.get(4));\n\t}\n\n\t@Test\n\tpublic void toSetTest() {\n\t\tObject[] a = { \"a\", \"你\", \"好\", \"\", 1 };\n\t\tLinkedHashSet<?> set = Convert.convert(LinkedHashSet.class, a);\n\t\tArrayList<?> list = CollUtil.newArrayList(set);\n\t\tassertEquals(\"a\", list.get(0));\n\t\tassertEquals(\"你\", list.get(1));\n\t\tassertEquals(\"好\", list.get(2));\n\t\tassertEquals(\"\", list.get(3));\n\t\tassertEquals(1, list.get(4));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToNumberTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.util.concurrent.atomic.AtomicLong;\n\npublic class ConvertToNumberTest {\n\t@Test\n\tpublic void dateToLongTest(){\n\t\tfinal DateTime date = DateUtil.parse(\"2020-05-17 12:32:00\");\n\t\tfinal Long dateLong = Convert.toLong(date);\n\t\tassert date != null;\n\t\tassertEquals(date.getTime(), dateLong.longValue());\n\t}\n\n\t@Test\n\tpublic void dateToIntTest(){\n\t\tfinal DateTime date = DateUtil.parse(\"2020-05-17 12:32:00\");\n\t\tfinal Integer dateInt = Convert.toInt(date);\n\t\tassert date != null;\n\t\tassertEquals((int)date.getTime(), dateInt.intValue());\n\t}\n\n\t@Test\n\tpublic void dateToAtomicLongTest(){\n\t\tfinal DateTime date = DateUtil.parse(\"2020-05-17 12:32:00\");\n\t\tfinal AtomicLong dateLong = Convert.convert(AtomicLong.class, date);\n\t\tassert date != null;\n\t\tassertEquals(date.getTime(), dateLong.longValue());\n\t}\n\n\t@Test\n\tpublic void toBigDecimalTest(){\n\t\tBigDecimal bigDecimal = Convert.toBigDecimal(\"1.1f\");\n\t\tassertEquals(1.1f, bigDecimal.floatValue(), 0);\n\n\t\tbigDecimal = Convert.toBigDecimal(\"1L\");\n\t\tassertEquals(1L, bigDecimal.longValue());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConvertToSBCAndDBCTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 类型转换工具单元测试\n * 全角半角转换\n *\n * @author Looly\n *\n */\npublic class ConvertToSBCAndDBCTest {\n\n\t@Test\n\tpublic void toSBCTest() {\n\t\tString a = \"123456789\";\n\t\tString sbc = Convert.toSBC(a);\n\t\tassertEquals(\"１２３４５６７８９\", sbc);\n\t}\n\n\t@Test\n\tpublic void toDBCTest() {\n\t\tString a = \"１２３４５６７８９\";\n\t\tString dbc = Convert.toDBC(a);\n\t\tassertEquals(\"123456789\", dbc);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ConverterRegistryTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * ConverterRegistry 单元测试\n * @author Looly\n *\n */\npublic class ConverterRegistryTest {\n\n\t@Test\n\tpublic void getConverterTest() {\n\t\tConverter<Object> converter = ConverterRegistry.getInstance().getConverter(CharSequence.class, false);\n\t\tassertNotNull(converter);\n\t}\n\n\t@Test\n\tpublic void customTest(){\n\t\tint a = 454553;\n\t\tConverterRegistry converterRegistry = ConverterRegistry.getInstance();\n\n\t\tCharSequence result = converterRegistry.convert(CharSequence.class, a);\n\t\tassertEquals(\"454553\", result);\n\n\t\t//此处做为示例自定义CharSequence转换，因为Hutool中已经提供CharSequence转换，请尽量不要替换\n\t\t//替换可能引发关联转换异常（例如覆盖CharSequence转换会影响全局）\n\t\tconverterRegistry.putCustom(CharSequence.class, CustomConverter.class);\n\t\tresult = converterRegistry.convert(CharSequence.class, a);\n\t\tassertEquals(\"Custom: 454553\", result);\n\t}\n\n\tpublic static class CustomConverter implements Converter<CharSequence>{\n\t\t@Override\n\t\tpublic CharSequence convert(Object value, CharSequence defaultValue) throws IllegalArgumentException {\n\t\t\treturn \"Custom: \" + value.toString();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/DateConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.Timestamp;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Date;\n\npublic class DateConvertTest {\n\n\t@Test\n\tpublic void toDateTest() {\n\t\tString a = \"2017-05-06\";\n\t\tDate value = Convert.toDate(a);\n\t\tassertEquals(a, DateUtil.formatDate(value));\n\n\t\tlong timeLong = DateUtil.date().getTime();\n\t\tDate value2 = Convert.toDate(timeLong);\n\t\tassertEquals(timeLong, value2.getTime());\n\t}\n\n\t@Test\n\tpublic void toDateFromIntTest() {\n\t\tint dateLong = -1497600000;\n\t\tDate value = Convert.toDate(dateLong);\n\t\tassertNotNull(value);\n\t\tassertEquals(\"Mon Dec 15 00:00:00 CST 1969\", value.toString().replace(\"GMT+08:00\", \"CST\"));\n\n\t\tfinal java.sql.Date sqlDate = Convert.convert(java.sql.Date.class, dateLong);\n\t\tassertNotNull(sqlDate);\n\t\tassertEquals(\"1969-12-15\", sqlDate.toString());\n\t}\n\n\t@Test\n\tpublic void toDateFromLocalDateTimeTest() {\n\t\tLocalDateTime localDateTime = LocalDateTime.parse(\"2017-05-06T08:30:00\", DateTimeFormatter.ISO_DATE_TIME);\n\t\tDate value = Convert.toDate(localDateTime);\n\t\tassertNotNull(value);\n\t\tassertEquals(\"2017-05-06\", DateUtil.formatDate(value));\n\t}\n\n\t@Test\n\tpublic void toSqlDateTest() {\n\t\tString a = \"2017-05-06\";\n\t\tjava.sql.Date value = Convert.convert(java.sql.Date.class, a);\n\t\tassertEquals(\"2017-05-06\", value.toString());\n\n\t\tlong timeLong = DateUtil.date().getTime();\n\t\tjava.sql.Date value2 = Convert.convert(java.sql.Date.class, timeLong);\n\t\tassertEquals(timeLong, value2.getTime());\n\t}\n\n\t@Test\n\tpublic void toLocalDateTimeTest() {\n\t\tDate src = new Date();\n\n\t\tLocalDateTime ldt = Convert.toLocalDateTime(src);\n\t\tassertEquals(ldt, DateUtil.toLocalDateTime(src));\n\n\t\tTimestamp ts = Timestamp.from(src.toInstant());\n\t\tldt = Convert.toLocalDateTime(ts);\n\t\tassertEquals(ldt, DateUtil.toLocalDateTime(src));\n\n\t\tString str = \"2020-12-12 12:12:12.0\";\n\t\tldt = Convert.toLocalDateTime(str);\n\t\tassertEquals(ldt.format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.S\")), str);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/EnumConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Enum转换单元测试\n */\npublic class EnumConvertTest {\n\n\t@Test\n\tpublic void convertTest(){\n\t\tTestEnum bbb = Convert.convert(TestEnum.class, \"BBB\");\n\t\tassertEquals(TestEnum.B, bbb);\n\n\t\tbbb = Convert.convert(TestEnum.class, 22);\n\t\tassertEquals(TestEnum.B, bbb);\n\t}\n\n\t@Test\n\tpublic void toEnumTest(){\n\t\tTestEnum ccc = Convert.toEnum(TestEnum.class, \"CCC\");\n\t\tassertEquals(TestEnum.C, ccc);\n\n\t\tccc = Convert.toEnum(TestEnum.class, 33);\n\t\tassertEquals(TestEnum.C, ccc);\n\t}\n\n\tenum TestEnum {\n\t\tA, B, C;\n\n\t\tpublic static TestEnum parse(String str) {\n\t\t\tswitch (str) {\n\t\t\t\tcase \"AAA\":\n\t\t\t\t\treturn A;\n\t\t\t\tcase \"BBB\":\n\t\t\t\t\treturn B;\n\t\t\t\tcase \"CCC\":\n\t\t\t\t\treturn C;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tpublic static TestEnum parseByNumber(int i) {\n\t\t\tswitch (i) {\n\t\t\t\tcase 11:\n\t\t\t\t\treturn A;\n\t\t\t\tcase 22:\n\t\t\t\t\treturn B;\n\t\t\t\tcase 33:\n\t\t\t\t\treturn C;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/Issue2611Test.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.util.NumberUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\npublic class Issue2611Test {\n\n\t@Test\n\tpublic void chineseMoneyToNumberTest(){\n\t\tfinal BigDecimal value = Convert.chineseMoneyToNumber(\"陆万柒仟伍佰伍拾柒元\");\n\n\t\tassertEquals(\"67,557.00\", NumberUtil.decimalFormatMoney(value.doubleValue()));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/Issue3241Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.convert;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\npublic class Issue3241Test {\n\t@Test\n\tpublic void toBigDecimalTest() {\n\t\tassertEquals(new BigDecimal(\"9.0E+7\"), Convert.toBigDecimal(\"9.0E+7\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/IssueI7WJHHTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.convert;\n\nimport cn.hutool.core.lang.Opt;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Optional;\n\npublic class IssueI7WJHHTest {\n\n\t@Test\n\tpublic void toIntTest() {\n\t\tfinal Optional<Integer> optional = Optional.of(1);\n\t\tfinal Integer integer = Convert.toInt(optional);\n\n\t\tassertEquals(Integer.valueOf(1), integer);\n\t}\n\n\t@Test\n\tpublic void toIntTest2() {\n\t\tfinal Opt<Integer> optional = Opt.of(1);\n\t\tfinal Integer integer = Convert.toInt(optional);\n\n\t\tassertEquals(Integer.valueOf(1), integer);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/IssueIALV38Test.java",
    "content": "package cn.hutool.core.convert;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\npublic class IssueIALV38Test {\n\t@Test\n\tvoid name() {\n\t\tfinal Object o = Convert.convertWithCheck(BigDecimal.class, \" 111啊\", null, false);\n\t\tAssertions.assertEquals(new BigDecimal(\"111\"), o);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/MapConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.map.MapBuilder;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Map转换单元测试\n *\n * @author looly\n *\n */\npublic class MapConvertTest {\n\n\t@Test\n\tpublic void beanToMapTest() {\n\t\tfinal User user = new User();\n\t\tuser.setName(\"AAA\");\n\t\tuser.setAge(45);\n\n\t\tfinal HashMap<?, ?> map = Convert.convert(HashMap.class, user);\n\t\tassertEquals(\"AAA\", map.get(\"name\"));\n\t\tassertEquals(45, map.get(\"age\"));\n\t}\n\n\t@Test\n\tpublic void mapToMapTest() {\n\t\tfinal Map<String, Object> srcMap = MapBuilder\n\t\t\t\t.create(new HashMap<String, Object>())\n\t\t\t\t.put(\"name\", \"AAA\")\n\t\t\t\t.put(\"age\", 45).map();\n\n\t\tfinal LinkedHashMap<?, ?> map = Convert.convert(LinkedHashMap.class, srcMap);\n\t\tassertEquals(\"AAA\", map.get(\"name\"));\n\t\tassertEquals(45, map.get(\"age\"));\n\t}\n\n\tpublic static class User {\n\t\tprivate String name;\n\t\tprivate int age;\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\n\t\tpublic void setName(final String name) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\tpublic int getAge() {\n\t\t\treturn age;\n\t\t}\n\n\t\tpublic void setAge(final int age) {\n\t\t\tthis.age = age;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/NumberChineseFormatterTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class NumberChineseFormatterTest {\n\n\t@Test\n\tpublic void formatThousandTest() {\n\t\tString f = NumberChineseFormatter.formatThousand(10, false);\n\t\tassertEquals(\"十\", f);\n\t\tf = NumberChineseFormatter.formatThousand(11, false);\n\t\tassertEquals(\"十一\", f);\n\t\tf = NumberChineseFormatter.formatThousand(19, false);\n\t\tassertEquals(\"十九\", f);\n\t}\n\n\t// 测试千\n\t@Test\n\tpublic void formatThousandLongTest() {\n\t\tString f = NumberChineseFormatter.format(0, false);\n\t\tassertEquals(\"零\", f);\n\t\tf = NumberChineseFormatter.format(1, false);\n\t\tassertEquals(\"一\", f);\n\t\tf = NumberChineseFormatter.format(10, false);\n\t\tassertEquals(\"一十\", f);\n\t\tf = NumberChineseFormatter.format(12, false);\n\t\tassertEquals(\"一十二\", f);\n\t\tf = NumberChineseFormatter.format(100, false);\n\t\tassertEquals(\"一百\", f);\n\t\tf = NumberChineseFormatter.format(101, false);\n\t\tassertEquals(\"一百零一\", f);\n\t\tf = NumberChineseFormatter.format(110, false);\n\t\tassertEquals(\"一百一十\", f);\n\t\tf = NumberChineseFormatter.format(112, false);\n\t\tassertEquals(\"一百一十二\", f);\n\t\tf = NumberChineseFormatter.format(1000, false);\n\t\tassertEquals(\"一千\", f);\n\t\tf = NumberChineseFormatter.format(1001, false);\n\t\tassertEquals(\"一千零一\", f);\n\t\tf = NumberChineseFormatter.format(1010, false);\n\t\tassertEquals(\"一千零一十\", f);\n\t\tf = NumberChineseFormatter.format(1100, false);\n\t\tassertEquals(\"一千一百\", f);\n\t\tf = NumberChineseFormatter.format(1101, false);\n\t\tassertEquals(\"一千一百零一\", f);\n\t\tf = NumberChineseFormatter.format(9999, false);\n\t\tassertEquals(\"九千九百九十九\", f);\n\t}\n\n\t// 测试万\n\t@Test\n\tpublic void formatTenThousandLongTest() {\n\t\tString f = NumberChineseFormatter.format(1_0000, false);\n\t\tassertEquals(\"一万\", f);\n\t\tf = NumberChineseFormatter.format(1_0001, false);\n\t\tassertEquals(\"一万零一\", f);\n\t\tf = NumberChineseFormatter.format(1_0010, false);\n\t\tassertEquals(\"一万零一十\", f);\n\t\tf = NumberChineseFormatter.format(1_0100, false);\n\t\tassertEquals(\"一万零一百\", f);\n\t\tf = NumberChineseFormatter.format(1_1000, false);\n\t\tassertEquals(\"一万一千\", f);\n\t\tf = NumberChineseFormatter.format(10_1000, false);\n\t\tassertEquals(\"一十万零一千\", f);\n\t\tf = NumberChineseFormatter.format(10_0100, false);\n\t\tassertEquals(\"一十万零一百\", f);\n\t\tf = NumberChineseFormatter.format(100_1000, false);\n\t\tassertEquals(\"一百万零一千\", f);\n\t\tf = NumberChineseFormatter.format(100_0100, false);\n\t\tassertEquals(\"一百万零一百\", f);\n\t\tf = NumberChineseFormatter.format(1000_1000, false);\n\t\tassertEquals(\"一千万零一千\", f);\n\t\tf = NumberChineseFormatter.format(1000_0100, false);\n\t\tassertEquals(\"一千万零一百\", f);\n\t\tf = NumberChineseFormatter.format(9999_0000, false);\n\t\tassertEquals(\"九千九百九十九万\", f);\n\t}\n\n\t// 测试亿\n\t@Test\n\tpublic void formatHundredMillionLongTest() {\n\t\tString f = NumberChineseFormatter.format(1_0000_0000L, false);\n\t\tassertEquals(\"一亿\", f);\n\t\tf = NumberChineseFormatter.format(1_0000_0001L, false);\n\t\tassertEquals(\"一亿零一\", f);\n\t\tf = NumberChineseFormatter.format(1_0000_1000L, false);\n\t\tassertEquals(\"一亿零一千\", f);\n\t\tf = NumberChineseFormatter.format(1_0001_0000L, false);\n\t\tassertEquals(\"一亿零一万\", f);\n\t\tf = NumberChineseFormatter.format(1_0010_0000L, false);\n\t\tassertEquals(\"一亿零一十万\", f);\n\t\tf = NumberChineseFormatter.format(1_0010_0000L, false);\n\t\tassertEquals(\"一亿零一十万\", f);\n\t\tf = NumberChineseFormatter.format(1_0100_0000L, false);\n\t\tassertEquals(\"一亿零一百万\", f);\n\t\tf = NumberChineseFormatter.format(1_1000_0000L, false);\n\t\tassertEquals(\"一亿一千万\", f);\n\t\tf = NumberChineseFormatter.format(10_1000_0000L, false);\n\t\tassertEquals(\"一十亿零一千万\", f);\n\t\tf = NumberChineseFormatter.format(100_1000_0000L, false);\n\t\tassertEquals(\"一百亿零一千万\", f);\n\t\tf = NumberChineseFormatter.format(1000_1000_0000L, false);\n\t\tassertEquals(\"一千亿零一千万\", f);\n\t\tf = NumberChineseFormatter.format(1100_1000_0000L, false);\n\t\tassertEquals(\"一千一百亿零一千万\", f);\n\t\tf = NumberChineseFormatter.format(9999_0000_0000L, false);\n\t\tassertEquals(\"九千九百九十九亿\", f);\n\t}\n\n\t// 测试万亿\n\t@Test\n\tpublic void formatTrillionsLongTest() {\n\t\tString f = NumberChineseFormatter.format(1_0000_0000_0000L, false);\n\t\tassertEquals(\"一万亿\", f);\n\t\tf = NumberChineseFormatter.format(1_0000_1000_0000L, false);\n\t\tassertEquals(\"一万亿零一千万\", f);\n\t\tf = NumberChineseFormatter.format(1_0010_0000_0000L, false);\n\t\tassertEquals(\"一万零一十亿\", f);\n\t}\n\n\t@Test\n\tpublic void formatTest() {\n\t\tString f0 = NumberChineseFormatter.format(5000_8000, false);\n\t\tassertEquals(\"五千万零八千\", f0);\n\t\tString f1 = NumberChineseFormatter.format(1_0889.72356, false);\n\t\tassertEquals(\"一万零八百八十九点七二\", f1);\n\t\tf1 = NumberChineseFormatter.format(12653, false);\n\t\tassertEquals(\"一万二千六百五十三\", f1);\n\t\tf1 = NumberChineseFormatter.format(215.6387, false);\n\t\tassertEquals(\"二百一十五点六四\", f1);\n\t\tf1 = NumberChineseFormatter.format(1024, false);\n\t\tassertEquals(\"一千零二十四\", f1);\n\t\tf1 = NumberChineseFormatter.format(100350089, false);\n\t\tassertEquals(\"一亿零三十五万零八十九\", f1);\n\t\tf1 = NumberChineseFormatter.format(1200, false);\n\t\tassertEquals(\"一千二百\", f1);\n\t\tf1 = NumberChineseFormatter.format(12, false);\n\t\tassertEquals(\"一十二\", f1);\n\t\tf1 = NumberChineseFormatter.format(0.05, false);\n\t\tassertEquals(\"零点零五\", f1);\n\t}\n\n\t@Test\n\tpublic void formatTest2() {\n\t\tString f1 = NumberChineseFormatter.format(-0.3, false, false);\n\t\tassertEquals(\"负零点三\", f1);\n\n\t\tf1 = NumberChineseFormatter.format(10, false, false);\n\t\tassertEquals(\"一十\", f1);\n\t}\n\n\t@Test\n\tpublic void formatTest3() {\n//\t\tString f1 = NumberChineseFormatter.format(5000_8000, false, false);\n//\t\tassertEquals(\"五千万零八千\", f1);\n\n\t\tString f2 = NumberChineseFormatter.format(1_0035_0089, false, false);\n\t\tassertEquals(\"一亿零三十五万零八十九\", f2);\n\t}\n\n\t@Test\n\tpublic void formatMaxTest() {\n\t\tString f3 = NumberChineseFormatter.format(99_9999_9999_9999L, false, false);\n\t\tassertEquals(\"九十九万九千九百九十九亿九千九百九十九万九千九百九十九\", f3);\n\t}\n\n\t@Test\n\tpublic void formatTraditionalTest() {\n\t\tString f1 = NumberChineseFormatter.format(10889.72356, true);\n\t\tassertEquals(\"壹万零捌佰捌拾玖点柒贰\", f1);\n\t\tf1 = NumberChineseFormatter.format(12653, true);\n\t\tassertEquals(\"壹万贰仟陆佰伍拾叁\", f1);\n\t\tf1 = NumberChineseFormatter.format(215.6387, true);\n\t\tassertEquals(\"贰佰壹拾伍点陆肆\", f1);\n\t\tf1 = NumberChineseFormatter.format(1024, true);\n\t\tassertEquals(\"壹仟零贰拾肆\", f1);\n\t\tf1 = NumberChineseFormatter.format(100350089, true);\n\t\tassertEquals(\"壹亿零叁拾伍万零捌拾玖\", f1);\n\t\tf1 = NumberChineseFormatter.format(1200, true);\n\t\tassertEquals(\"壹仟贰佰\", f1);\n\t\tf1 = NumberChineseFormatter.format(12, true);\n\t\tassertEquals(\"壹拾贰\", f1);\n\t\tf1 = NumberChineseFormatter.format(0.05, true);\n\t\tassertEquals(\"零点零伍\", f1);\n\t}\n\n\t@Test\n\tpublic void formatSimpleTest() {\n\t\tString f1 = NumberChineseFormatter.formatSimple(1_2345);\n\t\tassertEquals(\"1.23万\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(-5_5555);\n\t\tassertEquals(\"-5.56万\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(1_2345_6789);\n\t\tassertEquals(\"1.23亿\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(-5_5555_5555);\n\t\tassertEquals(\"-5.56亿\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(1_2345_6789_1011L);\n\t\tassertEquals(\"1.23万亿\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(-5_5555_5555_5555L);\n\t\tassertEquals(\"-5.56万亿\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(123);\n\t\tassertEquals(\"123\", f1);\n\t\tf1 = NumberChineseFormatter.formatSimple(-123);\n\t\tassertEquals(\"-123\", f1);\n\t}\n\n\t@Test\n\tpublic void digitToChineseTest() {\n\t\tString digitToChinese = Convert.digitToChinese(12_4124_1241_2421.12);\n\t\tassertEquals(\"壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元壹角贰分\", digitToChinese);\n\n\t\tdigitToChinese = Convert.digitToChinese(12_0000_1241_2421L);\n\t\tassertEquals(\"壹拾贰万亿零壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元整\", digitToChinese);\n\n\t\tdigitToChinese = Convert.digitToChinese(12_0000_0000_2421L);\n\t\tassertEquals(\"壹拾贰万亿零贰仟肆佰贰拾壹元整\", digitToChinese);\n\n\t\tdigitToChinese = Convert.digitToChinese(12_4124_1241_2421D);\n\t\tassertEquals(\"壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元整\", digitToChinese);\n\n\t\tdigitToChinese = Convert.digitToChinese(2421.02);\n\t\tassertEquals(\"贰仟肆佰贰拾壹元零贰分\", digitToChinese);\n\t}\n\n\t@Test\n\tpublic void digitToChineseTest2() {\n\t\tdouble a = 67556.32;\n\t\tString digitUppercase = Convert.digitToChinese(a);\n\t\tassertEquals(\"陆万柒仟伍佰伍拾陆元叁角贰分\", digitUppercase);\n\n\t\ta = 1024.00;\n\t\tdigitUppercase = Convert.digitToChinese(a);\n\t\tassertEquals(\"壹仟零贰拾肆元整\", digitUppercase);\n\n\t\tdouble b = 1024;\n\t\tdigitUppercase = Convert.digitToChinese(b);\n\t\tassertEquals(\"壹仟零贰拾肆元整\", digitUppercase);\n\t}\n\n\t@Test\n\tpublic void digitToChineseTest3() {\n\t\tString digitToChinese = Convert.digitToChinese(2_0000_0000.00);\n\t\tassertEquals(\"贰亿元整\", digitToChinese);\n\t\tdigitToChinese = Convert.digitToChinese(2_0000.00);\n\t\tassertEquals(\"贰万元整\", digitToChinese);\n\t\tdigitToChinese = Convert.digitToChinese(2_0000_0000_0000.00);\n\t\tassertEquals(\"贰万亿元整\", digitToChinese);\n\t}\n\n\t@Test\n\tpublic void digitToChineseTest4() {\n\t\tString digitToChinese = Convert.digitToChinese(400_0000.00);\n\t\tassertEquals(\"肆佰万元整\", digitToChinese);\n\t}\n\n\t@Test\n\tpublic void numberCharToChineseTest() {\n\t\tString s = NumberChineseFormatter.numberCharToChinese('1', false);\n\t\tassertEquals(\"一\", s);\n\t\ts = NumberChineseFormatter.numberCharToChinese('2', false);\n\t\tassertEquals(\"二\", s);\n\t\ts = NumberChineseFormatter.numberCharToChinese('0', false);\n\t\tassertEquals(\"零\", s);\n\n\t\t// 非数字字符原样返回\n\t\ts = NumberChineseFormatter.numberCharToChinese('A', false);\n\t\tassertEquals(\"A\", s);\n\t}\n\n\t@Test\n\tpublic void chineseToNumberTest() {\n\t\tassertEquals(0, NumberChineseFormatter.chineseToNumber(\"零\"));\n\t\tassertEquals(102, NumberChineseFormatter.chineseToNumber(\"一百零二\"));\n\t\tassertEquals(112, NumberChineseFormatter.chineseToNumber(\"一百一十二\"));\n\t\tassertEquals(1012, NumberChineseFormatter.chineseToNumber(\"一千零一十二\"));\n\t\tassertEquals(1000000, NumberChineseFormatter.chineseToNumber(\"一百万\"));\n\t\tassertEquals(2000100112, NumberChineseFormatter.chineseToNumber(\"二十亿零一十万零一百一十二\"));\n\t}\n\n\t@Test\n\tpublic void chineseToNumberTest2() {\n\t\tassertEquals(120, NumberChineseFormatter.chineseToNumber(\"一百二\"));\n\t\tassertEquals(1200, NumberChineseFormatter.chineseToNumber(\"一千二\"));\n\t\tassertEquals(22000, NumberChineseFormatter.chineseToNumber(\"两万二\"));\n\t\tassertEquals(22003, NumberChineseFormatter.chineseToNumber(\"两万二零三\"));\n\t\tassertEquals(22010, NumberChineseFormatter.chineseToNumber(\"两万二零一十\"));\n\t}\n\n\t@Test\n\tpublic void chineseToNumberTest3() {\n\t\t// issue#1726，对于单位开头的数组，默认赋予1\n\t\t// 十二 -> 一十二\n\t\t// 百二 -> 一百二\n\t\tassertEquals(12, NumberChineseFormatter.chineseToNumber(\"十二\"));\n\t\tassertEquals(120, NumberChineseFormatter.chineseToNumber(\"百二\"));\n\t\tassertEquals(1300, NumberChineseFormatter.chineseToNumber(\"千三\"));\n\t}\n\n\t@Test\n\tpublic void badNumberTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\t// 连续数字检查\n\t\t\tNumberChineseFormatter.chineseToNumber(\"一百一二三\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void badNumberTest2() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\t// 非法字符\n\t\t\tNumberChineseFormatter.chineseToNumber(\"一百你三\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void singleMoneyTest() {\n\t\tString format = NumberChineseFormatter.format(0.01, false, true);\n\t\tassertEquals(\"一分\", format);\n\t\tformat = NumberChineseFormatter.format(0.10, false, true);\n\t\tassertEquals(\"一角\", format);\n\t\tformat = NumberChineseFormatter.format(0.12, false, true);\n\t\tassertEquals(\"一角二分\", format);\n\n\t\tformat = NumberChineseFormatter.format(1.00, false, true);\n\t\tassertEquals(\"一元整\", format);\n\t\tformat = NumberChineseFormatter.format(1.10, false, true);\n\t\tassertEquals(\"一元一角\", format);\n\t\tformat = NumberChineseFormatter.format(1.02, false, true);\n\t\tassertEquals(\"一元零二分\", format);\n\t}\n\n\t@Test\n\tpublic void singleNumberTest() {\n\t\tString format = NumberChineseFormatter.format(0.01, false, false);\n\t\tassertEquals(\"零点零一\", format);\n\t\tformat = NumberChineseFormatter.format(0.10, false, false);\n\t\tassertEquals(\"零点一\", format);\n\t\tformat = NumberChineseFormatter.format(0.12, false, false);\n\t\tassertEquals(\"零点一二\", format);\n\n\t\tformat = NumberChineseFormatter.format(1.00, false, false);\n\t\tassertEquals(\"一\", format);\n\t\tformat = NumberChineseFormatter.format(1.10, false, false);\n\t\tassertEquals(\"一点一\", format);\n\t\tformat = NumberChineseFormatter.format(1.02, false, false);\n\t\tassertEquals(\"一点零二\", format);\n\t}\n\n\t@Test\n\tpublic void dotTest() {\n\t\tfinal String format = NumberChineseFormatter.format(new BigDecimal(\"3.1415926\"), false, false);\n\t\tassertEquals(\"三点一四一五九二六\", format);\n\t}\n\n\t@Test\n\tpublic void issue3986Test() {\n\t\tfinal String format = NumberChineseFormatter.format(100000.0, true, true);\n\t\tAssertions.assertEquals(\"壹拾万元整\", format);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/NumberConverterTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.convert.impl.NumberConverter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class NumberConverterTest {\n\n\t@Test\n\tpublic void toDoubleTest(){\n\t\tfinal NumberConverter numberConverter = new NumberConverter(Double.class);\n\t\tfinal Number convert = numberConverter.convert(\"1,234.55\", null);\n\t\tassertEquals(1234.55D, convert);\n\t}\n\n\t@Test\n\tpublic void toIntegerTest(){\n\t\tfinal NumberConverter numberConverter = new NumberConverter(Integer.class);\n\t\tfinal Number convert = numberConverter.convert(\"1,234.55\", null);\n\t\tassertEquals(1234, convert);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class NumberWordFormatTest {\n\n\t@Test\n\tpublic void formatTest() {\n\t\tfinal String format = NumberWordFormatter.format(100.23);\n\t\tassertEquals(\"ONE HUNDRED AND CENTS TWENTY THREE ONLY\", format);\n\n\t\tfinal String format2 = NumberWordFormatter.format(\"2100.00\");\n\t\tassertEquals(\"TWO THOUSAND ONE HUNDRED AND CENTS  ONLY\", format2);\n\n\t\tfinal String format3 = NumberWordFormatter.format(\"1234567890123.12\");\n\t\tassertEquals(\"ONE TRILLION TWO HUNDRED AND THIRTY FOUR BILLION FIVE HUNDRED AND SIXTY SEVEN MILLION EIGHT HUNDRED AND NINETY THOUSAND ONE HUNDRED AND TWENTY THREE AND CENTS TWELVE ONLY\", format3);\n\t}\n\n\t@Test\n\tpublic void formatSimpleTest() {\n\t\tfinal String format1 = NumberWordFormatter.formatSimple(1200, false);\n\t\tassertEquals(\"1.2k\", format1);\n\n\t\tfinal String format2 = NumberWordFormatter.formatSimple(4384324, false);\n\t\tassertEquals(\"4.38m\", format2);\n\n\t\tfinal String format3 = NumberWordFormatter.formatSimple(4384324, true);\n\t\tassertEquals(\"438.43w\", format3);\n\n\t\tfinal String format4 = NumberWordFormatter.formatSimple(4384324);\n\t\tassertEquals(\"438.43w\", format4);\n\n\t\tfinal String format5 = NumberWordFormatter.formatSimple(438);\n\t\tassertEquals(\"438\", format5);\n\n\t\tfinal String format6 = NumberWordFormatter.formatSimple(1000000, false);\n\t\tassertEquals(\"1m\", format6);\n\t}\n\n\t@Test\n\tpublic void formatSimpleTest2(){\n\t\tfinal String s = NumberWordFormatter.formatSimple(1000);\n\t\tassertEquals(\"1k\", s);\n\t}\n\n\t@Test\n\tpublic void issue4033Test(){\n\t\tString s = NumberWordFormatter.formatSimple(1_000, false);\n\t\tAssertions.assertEquals(\"1k\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(10_000, false);\n\t\tAssertions.assertEquals(\"10k\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(100_000, false);\n\t\tAssertions.assertEquals(\"100k\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(1_000_000, false);\n\t\tAssertions.assertEquals(\"1m\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(10_000_000, false);\n\t\tAssertions.assertEquals(\"10m\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(100_000_000, false);\n\t\tAssertions.assertEquals(\"100m\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(1_000_000_000, false);\n\t\tAssertions.assertEquals(\"1b\", s);\n\t}\n\n\t@Test\n\tpublic void issue4033Test2(){\n\t\tString s = NumberWordFormatter.formatSimple(1_000, true);\n\t\tAssertions.assertEquals(\"1k\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(10_000, true);\n\t\tAssertions.assertEquals(\"1w\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(100_000, true);\n\t\tAssertions.assertEquals(\"10w\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(1_000_000, true);\n\t\tAssertions.assertEquals(\"100w\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(10_000_000, true);\n\t\tAssertions.assertEquals(\"1000w\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(100_000_000, true);\n\t\tAssertions.assertEquals(\"10000w\", s);\n\n\t\ts = NumberWordFormatter.formatSimple(1_000_000_000, true);\n\t\tAssertions.assertEquals(\"100000w\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatterTest.java",
    "content": "package cn.hutool.core.convert;\n\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class NumberWordFormatterTest {\n\n\t@Test\n\tpublic void testFormatNull() {\n\t\t// 测试传入null值的情况\n\t\tString result = NumberWordFormatter.format(null);\n\t\tassertEquals(\"\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatInteger() {\n\t\t// 测试传入整数的情况\n\t\tString result = NumberWordFormatter.format(1234);\n\t\tassertEquals(\"ONE THOUSAND TWO HUNDRED AND THIRTY FOUR ONLY\", result);\n\n\t\tresult = NumberWordFormatter.format(1204);\n\t\tassertEquals(\"ONE THOUSAND TWO HUNDRED AND FOUR ONLY\", result);\n\n\t\tresult = NumberWordFormatter.format(1004);\n\t\tassertEquals(\"ONE THOUSAND FOUR ONLY\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatDecimal() {\n\t\t// 测试传入小数的情况\n\t\tString result = NumberWordFormatter.format(1234.56);\n\t\tassertEquals(\"ONE THOUSAND TWO HUNDRED AND THIRTY FOUR AND CENTS FIFTY SIX ONLY\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatLargeNumber() {\n\t\t// 测试传入大数字的情况\n\t\tString result = NumberWordFormatter.format(1234567890123L);\n\t\tassertEquals(\"ONE TRILLION TWO HUNDRED AND THIRTY FOUR BILLION FIVE HUNDRED AND SIXTY SEVEN MILLION EIGHT HUNDRED AND NINETY THOUSAND ONE HUNDRED AND TWENTY THREE ONLY\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatNonNumeric() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\t// 测试传入非数字字符串的情况\n\t\t\tNumberWordFormatter.format(\"non-numeric\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issue3579Test() {\n\t\tassertEquals(\"ZERO AND CENTS TEN ONLY\", NumberWordFormatter.format(0.1));\n\t\tassertEquals(\"ZERO AND CENTS ONE ONLY\", NumberWordFormatter.format(0.01));\n\t}\n}\n\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/PrimitiveConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class PrimitiveConvertTest {\n\n\t@Test\n\tpublic void toIntTest() {\n\t\tfinal int convert = Convert.convert(int.class, \"123\");\n\t\tassertEquals(123, convert);\n\t}\n\n\t@Test\n\tpublic void toIntErrorTest() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\tConvert.convert(int.class, \"aaaa\");\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/StringConvertTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.date.TimeZoneUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class StringConvertTest {\n\n\t@Test\n\tpublic void timezoneToStrTest(){\n\t\tfinal String s = Convert.toStr(TimeZoneUtil.getTimeZone(\"Asia/Shanghai\"));\n\t\tassertEquals(\"Asia/Shanghai\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/TemporalAccessorConverterTest.java",
    "content": "package cn.hutool.core.convert;\n\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.OffsetDateTime;\nimport java.time.OffsetTime;\nimport java.time.ZonedDateTime;\n\npublic class TemporalAccessorConverterTest {\n\n\t@Test\n\tpublic void toInstantTest(){\n\t\tString dateStr = \"2019-02-18\";\n\n\t\t// 通过转换获取的Instant为UTC时间\n\t\tInstant instant = Convert.convert(Instant.class, dateStr);\n\t\tInstant instant1 = DateUtil.parse(dateStr).toInstant();\n\t\tassertEquals(instant1, instant);\n\t}\n\n\t@Test\n\tpublic void toLocalDateTimeTest(){\n\t\tLocalDateTime localDateTime = Convert.convert(LocalDateTime.class, \"2019-02-18\");\n\t\tassertEquals(\"2019-02-18T00:00\", localDateTime.toString());\n\t}\n\n\t@Test\n\tpublic void toLocalDateTest(){\n\t\tLocalDate localDate = Convert.convert(LocalDate.class, \"2019-02-18\");\n\t\tassertEquals(\"2019-02-18\", localDate.toString());\n\t}\n\n\t@Test\n\tpublic void toLocalTimeTest(){\n\t\tLocalTime localTime = Convert.convert(LocalTime.class, \"2019-02-18\");\n\t\tassertEquals(\"00:00\", localTime.toString());\n\t}\n\n\t@Test\n\tpublic void toZonedDateTimeTest(){\n\t\tZonedDateTime zonedDateTime = Convert.convert(ZonedDateTime.class, \"2019-02-18\");\n\t\tassertEquals(\"2019-02-18T00:00+08:00\", zonedDateTime.toString().substring(0, 22));\n\t}\n\n\t@Test\n\tpublic void toOffsetDateTimeTest(){\n\t\tOffsetDateTime zonedDateTime = Convert.convert(OffsetDateTime.class, \"2019-02-18\");\n\t\tassertEquals(\"2019-02-18T00:00+08:00\", zonedDateTime.toString());\n\t}\n\n\t@Test\n\tpublic void toOffsetTimeTest(){\n\t\tOffsetTime offsetTime = Convert.convert(OffsetTime.class, \"2019-02-18\");\n\t\tassertEquals(\"00:00+08:00\", offsetTime.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/convert/ToBytesTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.convert;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ToBytesTest {\n\t@Test\n\tpublic void toBytesTest() {\n\t\tfinal List<Byte> byteList = new ArrayList<>();\n\t\tbyteList.add((byte) 1);\n\t\tbyteList.add((byte) 2);\n\t\tbyteList.add((byte) 3);\n\n\t\tfinal byte[] bytes = Convert.convert(byte[].class, byteList);\n\t\tassertEquals(1, bytes[0]);\n\t\tassertEquals(2, bytes[1]);\n\t\tassertEquals(3, bytes[2]);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/BetweenFormatterTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.BetweenFormatter.Level;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.function.Function;\n\npublic class BetweenFormatterTest {\n\n\tFunction<Level, String> levelFormatterEn = level -> {\n\t\tswitch (level) {\n\t\t\tcase DAY:\n\t\t\t\treturn \" day\";\n\t\t\tcase HOUR:\n\t\t\t\treturn \" hour\";\n\t\t\tcase MINUTE:\n\t\t\t\treturn \" minute\";\n\t\t\tcase SECOND:\n\t\t\t\treturn \" second\";\n\t\t\tcase MILLISECOND:\n\t\t\t\treturn \" millisecond\";\n\t\t\tdefault:\n\t\t\t\treturn \" \" + level.name();\n\t\t}\n\t};\n\n\t@Test\n\tpublic void formatTest() {\n\t\tlong betweenMs = DateUtil.betweenMs(DateUtil.parse(\"2017-01-01 22:59:59\"), DateUtil.parse(\"2017-01-02 23:59:58\"));\n\t\tBetweenFormatter formater = new BetweenFormatter(betweenMs, Level.MILLISECOND, 1);\n\t\tassertEquals(formater.toString(), \"1天\");\n\t}\n\n\t@Test\n\tpublic void formatTestEn() {\n\t\tfinal long betweenMs = DateUtil.betweenMs(DateUtil.parse(\"2017-01-01 22:59:59\"), DateUtil.parse(\"2017-01-02 23:59:58\"));\n\t\tfinal BetweenFormatter formatter = new BetweenFormatter(betweenMs, BetweenFormatter.Level.MILLISECOND, 1);\n\t\tformatter.setLevelFormatter(levelFormatterEn);\n\t\tassertEquals(formatter.toString(), \"1 day\");\n\t}\n\n\t@Test\n\tpublic void formatTestEn2() {\n\t\tfinal long betweenMs = 3610001;\n\t\tfinal BetweenFormatter formatter = new BetweenFormatter(betweenMs, BetweenFormatter.Level.MILLISECOND, 5);\n\t\tformatter.setSeparator(\",\");\n\t\tformatter.setLevelFormatter(levelFormatterEn);\n\t\tassertEquals(formatter.toString(), \"1 hour,10 second,1 millisecond\");\n\t}\n\n\t@Test\n\tpublic void formatBetweenTest() {\n\t\tlong betweenMs = DateUtil.betweenMs(DateUtil.parse(\"2018-07-16 11:23:19\"), DateUtil.parse(\"2018-07-16 11:23:20\"));\n\t\tBetweenFormatter formater = new BetweenFormatter(betweenMs, Level.SECOND, 1);\n\t\tassertEquals(formater.toString(), \"1秒\");\n\t}\n\n\t@Test\n\tpublic void formatBetweenTest2() {\n\t\tlong betweenMs = DateUtil.betweenMs(DateUtil.parse(\"2018-07-16 12:25:23\"), DateUtil.parse(\"2018-07-16 11:23:20\"));\n\t\tBetweenFormatter formater = new BetweenFormatter(betweenMs, Level.SECOND, 5);\n\t\tassertEquals(formater.toString(), \"1小时2分3秒\");\n\t}\n\n\t@Test\n\tpublic void formatTest2() {\n\t\tBetweenFormatter formater = new BetweenFormatter(584, Level.SECOND, 1);\n\t\tassertEquals(formater.toString(), \"0秒\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/CalendarUtilTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Calendar;\nimport java.util.Objects;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class CalendarUtilTest {\n\n\t@Test\n\tpublic void formatChineseDate() {\n\t\tCalendar calendar = Objects.requireNonNull(DateUtil.parse(\"2018-02-24 12:13:14\")).toCalendar();\n\t\tfinal String chineseDate = CalendarUtil.formatChineseDate(calendar, false);\n\t\tassertEquals(\"二〇一八年二月二十四日\", chineseDate);\n\t\tfinal String chineseDateTime = CalendarUtil.formatChineseDate(calendar, true);\n\t\tassertEquals(\"二〇一八年二月二十四日十二时十三分十四秒\", chineseDateTime);\n\t}\n\n\t@Test\n\tpublic void parseTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal Calendar calendar = CalendarUtil.parse(\"2021-09-27 00:00:112323\", false,\n\t\t\t\tDatePattern.NORM_DATETIME_FORMAT);\n\n\t\t\t// https://github.com/chinabugotech/hutool/issues/1849\n\t\t\t// 在使用严格模式时，秒不正确，抛出异常\n\t\t\tDateUtil.date(calendar);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/ChineseDateTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\npublic class ChineseDateTest {\n\n\t@Test\n\tpublic void chineseDateTest() {\n\t\tChineseDate date = new ChineseDate(DateUtil.parseDate(\"2020-01-25\"));\n\t\tassertEquals(\"2020-01-25 00:00:00\", date.getGregorianDate().toString());\n\t\tassertEquals(2020, date.getChineseYear());\n\n\t\tassertEquals(1, date.getMonth());\n\t\tassertEquals(\"一月\", date.getChineseMonth());\n\t\tassertEquals(\"正月\", date.getChineseMonthName());\n\n\n\t\tassertEquals(1, date.getDay());\n\t\tassertEquals(\"初一\", date.getChineseDay());\n\n\t\tassertEquals(\"庚子\", date.getCyclical());\n\t\tassertEquals(\"鼠\", date.getChineseZodiac());\n\t\tassertEquals(\"春节\", date.getFestivals());\n\t\tassertEquals(\"庚子鼠年 正月初一\", date.toString());\n\n\t\tdate = new ChineseDate(DateUtil.parseDate(\"2020-01-14\"));\n\t\tassertEquals(\"己亥猪年 腊月二十\", date.toString());\n\t\tdate = new ChineseDate(DateUtil.parseDate(\"2020-01-24\"));\n\t\tassertEquals(\"己亥猪年 腊月三十\", date.toString());\n\n\t\tassertEquals(\"2019-12-30\", date.toStringNormal());\n\t}\n\n\t@Test\n\tpublic void toStringNormalTest(){\n\t\tChineseDate date = new ChineseDate(DateUtil.parseDate(\"2020-03-1\"));\n\t\tassertEquals(\"2020-02-08\", date.toStringNormal());\n\t}\n\n\t@Test\n\tpublic void parseTest(){\n\t\tChineseDate date = new ChineseDate(DateUtil.parseDate(\"1996-07-14\"));\n\t\tassertEquals(\"丙子鼠年 五月廿九\", date.toString());\n\n\t\tdate = new ChineseDate(DateUtil.parseDate(\"1996-07-15\"));\n\t\tassertEquals(\"丙子鼠年 五月三十\", date.toString());\n\t}\n\n\t@Test\n\tpublic void getChineseMonthTest(){\n\t\tChineseDate chineseDate = new ChineseDate(2020,6,15);\n\t\tassertEquals(\"2020-08-04 00:00:00\", chineseDate.getGregorianDate().toString());\n\t\tassertEquals(\"六月\", chineseDate.getChineseMonth());\n\n\t\tchineseDate = new ChineseDate(2020,4,15);\n\t\tassertEquals(\"2020-06-06 00:00:00\", chineseDate.getGregorianDate().toString());\n\t\tassertEquals(\"闰四月\", chineseDate.getChineseMonth());\n\n\t\tchineseDate = new ChineseDate(2020,5,15);\n\t\tassertEquals(\"2020-07-05 00:00:00\", chineseDate.getGregorianDate().toString());\n\t\tassertEquals(\"五月\", chineseDate.getChineseMonth());\n\t}\n\n\t@Test\n\tpublic void getFestivalsTest(){\n\t\t// issue#I1XHSF@Gitee，2023-01-20对应农历腊月29，非除夕\n\t\tChineseDate chineseDate = new ChineseDate(DateUtil.parseDate(\"2023-01-20\"));\n\t\tassertTrue(StrUtil.isEmpty(chineseDate.getFestivals()));\n\t}\n\n\t@Test\n\tpublic void dateTest(){\n\t\t// 修复这两个日期不正确的问题\n\t\t// 问题出在计算与1900-01-31相差天数的问题上了，相差天数非整天\n\t\tChineseDate date = new ChineseDate(DateUtil.parseDate(\"1991-09-14\"));\n\t\tassertEquals(\"辛未羊年 八月初七\", date.toString());\n\t\tdate = new ChineseDate(DateUtil.parseDate(\"1991-09-15\"));\n\t\tassertEquals(\"辛未羊年 八月初八\", date.toString());\n\t}\n\n\t@Test\n\tpublic void dateTest2(){\n\t\t//noinspection ConstantConditions\n\t\tChineseDate date = new ChineseDate(DateUtil.parse(\"2020-10-19\"));\n\t\tassertEquals(\"庚子鼠年 九月初三\", date.toString());\n\t}\n\n\t@Test\n\tpublic void dateTest2_2(){\n\t\t//noinspection ConstantConditions\n\t\tChineseDate date = new ChineseDate(DateUtil.parse(\"2020-07-20\"));\n\t\tassertEquals(\"庚子鼠年 五月三十\", date.toString());\n\t}\n\n\t@Test\n\tpublic void dateTest3(){\n\t\t// 初一，offset为0测试\n\t\t//noinspection ConstantConditions\n\t\tChineseDate date = new ChineseDate(DateUtil.parse(\"2099-03-22\"));\n\t\tassertEquals(\"己未羊年 闰二月初一\", date.toString());\n\t}\n\n\t@Test\n\tpublic void leapMonthTest(){\n\t\t//noinspection ConstantConditions\n\t\tfinal ChineseDate c1 = new ChineseDate(DateUtil.parse(\"2028-05-28\"));\n\t\t//noinspection ConstantConditions\n\t\tfinal ChineseDate c2 = new ChineseDate(DateUtil.parse(\"2028-06-27\"));\n\n\t\tassertEquals(\"戊申猴年 五月初五\", c1.toString());\n\t\tassertEquals(\"戊申猴年 闰五月初五\", c2.toString());\n\t}\n\n\t@Test\n\tpublic void getChineseMonthTest2(){\n\t\t//https://github.com/chinabugotech/hutool/issues/2112\n\t\tChineseDate springFestival = new ChineseDate(DateUtil.parseDate(\"2022-02-01\"));\n\t\tfinal String chineseMonth = springFestival.getChineseMonth();\n\t\tassertEquals(\"一月\", chineseMonth);\n\t}\n\n\t@Test\n\tpublic void day19700101Test(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4UTPK\n\t\tDate date = DateUtil.parse(\"1970-01-01\");\n\t\t//noinspection ConstantConditions\n\t\tChineseDate chineseDate = new ChineseDate(date);\n\t\tassertEquals(\"己酉鸡年 冬月廿四\", chineseDate.toString());\n\n\t\tdate = DateUtil.parse(\"1970-01-02\");\n\t\t//noinspection ConstantConditions\n\t\tchineseDate = new ChineseDate(date);\n\t\tassertEquals(\"己酉鸡年 冬月廿五\", chineseDate.toString());\n\n\t\tdate = DateUtil.parse(\"1970-01-03\");\n\t\t//noinspection ConstantConditions\n\t\tchineseDate = new ChineseDate(date);\n\t\tassertEquals(\"己酉鸡年 冬月廿六\", chineseDate.toString());\n\t}\n\n\t@Test\n\tpublic void day19000101Test(){\n\t\t// 1900-01-31之前不支持\n\t\tDate date = DateUtil.parse(\"1900-01-31\");\n\t\t//noinspection ConstantConditions\n\t\tChineseDate chineseDate = new ChineseDate(date);\n\t\tassertEquals(\"庚子鼠年 正月初一\", chineseDate.toString());\n\t}\n\n\t@Test\n\tpublic void getGregorianDateTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4ZSGJ\n\t\tChineseDate chineseDate = new ChineseDate(1998, 5, 1);\n\t\tassertEquals(\"1998-06-24 00:00:00\", chineseDate.getGregorianDate().toString());\n\n\t\tchineseDate = new ChineseDate(1998, 5, 1, false);\n\t\tassertEquals(\"1998-05-26 00:00:00\", chineseDate.getGregorianDate().toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateBetweenTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.BetweenFormatter.Level;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.temporal.ChronoUnit;\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DateBetweenTest {\n\n\t@Test\n\tpublic void betweenYearTest() {\n\t\tDate start = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end = DateUtil.parse(\"2018-02-01 12:23:46\");\n\t\tlong betweenYear = new DateBetween(start, end).betweenYear(false);\n\t\tassertEquals(1, betweenYear);\n\n\t\tDate start1 = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end1 = DateUtil.parse(\"2018-03-01 12:23:46\");\n\t\tlong betweenYear1 = new DateBetween(start1, end1).betweenYear(false);\n\t\tassertEquals(1, betweenYear1);\n\n\t\t// 不足1年\n\t\tDate start2 = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end2 = DateUtil.parse(\"2018-02-01 11:23:46\");\n\t\tlong betweenYear2 = new DateBetween(start2, end2).betweenYear(false);\n\t\tassertEquals(0, betweenYear2);\n\t}\n\n\t@Test\n\tpublic void betweenYearTest2() {\n\t\tDate start = DateUtil.parse(\"2000-02-29\");\n\t\tDate end = DateUtil.parse(\"2018-02-28\");\n\t\tlong betweenYear = new DateBetween(start, end).betweenYear(false);\n\t\tassertEquals(18, betweenYear);\n\t}\n\n\t@Test\n\tpublic void betweenYearTest3() {\n\t\tDate start = DateUtil.parse(\"20170301\");\n\t\tDate end = DateUtil.parse(\"2024-02-29 14:56:18\");\n\t\tlong betweenYear = new DateBetween(start, end).betweenYear(false);\n\t\tassertEquals(6, betweenYear);\n\t}\n\n\t@Test\n\tpublic void betweenMonthTest() {\n\t\tDate start = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end = DateUtil.parse(\"2018-02-01 12:23:46\");\n\t\tlong betweenMonth = new DateBetween(start, end).betweenMonth(false);\n\t\tassertEquals(12, betweenMonth);\n\n\t\tDate start1 = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end1 = DateUtil.parse(\"2018-03-01 12:23:46\");\n\t\tlong betweenMonth1 = new DateBetween(start1, end1).betweenMonth(false);\n\t\tassertEquals(13, betweenMonth1);\n\n\t\t// 不足\n\t\tDate start2 = DateUtil.parse(\"2017-02-01 12:23:46\");\n\t\tDate end2 = DateUtil.parse(\"2018-02-01 11:23:46\");\n\t\tlong betweenMonth2 = new DateBetween(start2, end2).betweenMonth(false);\n\t\tassertEquals(11, betweenMonth2);\n\t}\n\n\t@Test\n\tpublic void betweenMinuteTest() {\n\t\tDate date1 = DateUtil.parse(\"2017-03-01 20:33:23\");\n\t\tDate date2 = DateUtil.parse(\"2017-03-01 23:33:23\");\n\t\tString formatBetween = DateUtil.formatBetween(date1, date2, Level.SECOND);\n\t\tassertEquals(\"3小时\", formatBetween);\n\t}\n\n\t@Test\n\tpublic void betweenWeeksTest(){\n\t\tfinal long betweenWeek = DateUtil.betweenWeek(\n\t\t\t\tDateUtil.parse(\"2020-11-21\"),\n\t\t\t\tDateUtil.parse(\"2020-11-23\"), false);\n\n\t\tfinal long betweenWeek2 = LocalDateTimeUtil.between(\n\t\t\t\tLocalDateTimeUtil.parse(\"2020-11-21\", \"yyy-MM-dd\"),\n\t\t\t\tLocalDateTimeUtil.parse(\"2020-11-23\", \"yyy-MM-dd\"),\n\t\t\t\tChronoUnit.WEEKS);\n\t\tassertEquals(betweenWeek, betweenWeek2);\n\t}\n\n\t@Test\n\tpublic void issueI97U3JTest(){\n\t\tString dateStr1 = \"2024-02-29 23:59:59\";\n\t\tDate sdate = DateUtil.parse(dateStr1);\n\n\t\tString dateStr2 = \"2023-03-01 00:00:00\";\n\t\tDate edate = DateUtil.parse(dateStr2);\n\n\t\tlong result = DateUtil.betweenYear(sdate, edate, false);\n\t\tassertEquals(0, result);\n\t}\n\n\t@Test\n\tpublic void issueIDFVKGTest() {\n\t\tDate b = new Date(1609459200000L); // 2021-01-01 00:00:00\n\t\tDate e = new Date(1609545600000L); // 2021-01-02 00:00:00\n\t\tDateBetween db = new DateBetween(b, e);\n\n\t\t// 修改原始 date\n\t\tb.setTime(0L); // 1970-01-01\n\n\t\t// 期望 DateBetween 不受影响，间隔仍为 1 天\n\t\tassertEquals(1, db.between(DateUnit.DAY));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateFieldTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class DateFieldTest {\n\n\t@Test\n\tpublic void ofTest() {\n\t\tDateField field = DateField.of(11);\n\t\tassertEquals(DateField.HOUR_OF_DAY, field);\n\t\tfield = DateField.of(12);\n\t\tassertEquals(DateField.MINUTE, field);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateModifierTest.java",
    "content": "package cn.hutool.core.date;\n\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class DateModifierTest {\n\n\t@Test\n\tpublic void truncateTest() {\n\t\tString dateStr = \"2017-03-01 22:33:23.123\";\n\t\tDate date = DateUtil.parse(dateStr);\n\n\t\t// 毫秒\n\t\tDateTime begin = DateUtil.truncate(date, DateField.MILLISECOND);\n\t\tassertEquals(dateStr, begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 秒\n\t\tbegin = DateUtil.truncate(date, DateField.SECOND);\n\t\tassertEquals(\"2017-03-01 22:33:23.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 分\n\t\tbegin = DateUtil.truncate(date, DateField.MINUTE);\n\t\tassertEquals(\"2017-03-01 22:33:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 小时\n\t\tbegin = DateUtil.truncate(date, DateField.HOUR);\n\t\tassertEquals(\"2017-03-01 22:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.truncate(date, DateField.HOUR_OF_DAY);\n\t\tassertEquals(\"2017-03-01 22:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 上下午，原始日期是22点，上下午的起始就是12点\n\t\tbegin = DateUtil.truncate(date, DateField.AM_PM);\n\t\tassertEquals(\"2017-03-01 12:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 天，day of xxx按照day处理\n\t\tbegin = DateUtil.truncate(date, DateField.DAY_OF_WEEK_IN_MONTH);\n\t\tassertEquals(\"2017-03-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.truncate(date, DateField.DAY_OF_WEEK);\n\t\tassertEquals(\"2017-03-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.truncate(date, DateField.DAY_OF_MONTH);\n\t\tassertEquals(\"2017-03-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 星期\n\t\tbegin = DateUtil.truncate(date, DateField.WEEK_OF_MONTH);\n\t\tassertEquals(\"2017-02-27 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.truncate(date, DateField.WEEK_OF_YEAR);\n\t\tassertEquals(\"2017-02-27 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 月\n\t\tbegin = DateUtil.truncate(date, DateField.MONTH);\n\t\tassertEquals(\"2017-03-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 年\n\t\tbegin = DateUtil.truncate(date, DateField.YEAR);\n\t\tassertEquals(\"2017-01-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test\n\tpublic void truncateDayOfWeekInMonthTest() {\n\t\tString dateStr = \"2017-03-01 22:33:23.123\";\n\t\tDate date = DateUtil.parse(dateStr);\n\n\t\t// 天，day of xxx按照day处理\n\t\tDateTime begin = DateUtil.truncate(date, DateField.DAY_OF_WEEK_IN_MONTH);\n\t\tassertEquals(\"2017-03-01 00:00:00.000\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test\n\tpublic void ceilingTest() {\n\t\tString dateStr = \"2017-03-01 22:33:23.123\";\n\t\tDate date = DateUtil.parse(dateStr);\n\n\t\t// 毫秒\n\t\tDateTime begin = DateUtil.ceiling(date, DateField.MILLISECOND);\n\t\tassertEquals(dateStr, begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 秒\n\t\tbegin = DateUtil.ceiling(date, DateField.SECOND);\n\t\tassertEquals(\"2017-03-01 22:33:23.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 分\n\t\tbegin = DateUtil.ceiling(date, DateField.MINUTE);\n\t\tassertEquals(\"2017-03-01 22:33:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 小时\n\t\tbegin = DateUtil.ceiling(date, DateField.HOUR);\n\t\tassertEquals(\"2017-03-01 22:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.ceiling(date, DateField.HOUR_OF_DAY);\n\t\tassertEquals(\"2017-03-01 22:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 上下午，原始日期是22点，上下午的结束就是23点\n\t\tbegin = DateUtil.ceiling(date, DateField.AM_PM);\n\t\tassertEquals(\"2017-03-01 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 天，day of xxx按照day处理\n\t\tbegin = DateUtil.ceiling(date, DateField.DAY_OF_WEEK_IN_MONTH);\n\t\tassertEquals(\"2017-03-01 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.ceiling(date, DateField.DAY_OF_WEEK);\n\t\tassertEquals(\"2017-03-01 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.ceiling(date, DateField.DAY_OF_MONTH);\n\t\tassertEquals(\"2017-03-01 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 星期\n\t\tbegin = DateUtil.ceiling(date, DateField.WEEK_OF_MONTH);\n\t\tassertEquals(\"2017-03-05 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t\tbegin = DateUtil.ceiling(date, DateField.WEEK_OF_YEAR);\n\t\tassertEquals(\"2017-03-05 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 月\n\t\tbegin = DateUtil.ceiling(date, DateField.MONTH);\n\t\tassertEquals(\"2017-03-31 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\t// 年\n\t\tbegin = DateUtil.ceiling(date, DateField.YEAR);\n\t\tassertEquals(\"2017-12-31 23:59:59.999\", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateRangeTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\nimport java.util.List;\n\npublic class DateRangeTest {\n\t@Test\n\tvoid issue3783Test() {\n\t\tfinal Date start = DateUtil.parse(\"2024-01-01\");\n\t\tfinal Date end = DateUtil.parse(\"2024-02-01\");\n\t\tfinal List<DateTime> dateTimes = DateUtil.rangeToList(start, end, DateField.DAY_OF_MONTH, 0);\n\t\tAssertions.assertEquals(1, dateTimes.size());\n\t\tAssertions.assertEquals(\"2024-01-01 00:00:00\", dateTimes.get(0).toString());\n\t}\n\n\t@Test\n\tvoid issue3783Test2() {\n\t\tfinal Date start = DateUtil.parse(\"2024-01-01\");\n\t\tfinal Date end = DateUtil.parse(\"2024-02-01\");\n\t\tfinal List<DateTime> dateTimes = DateUtil.rangeToList(start, end, DateField.DAY_OF_MONTH, -2);\n\t\tAssertions.assertEquals(1, dateTimes.size());\n\t\tAssertions.assertEquals(\"2024-01-01 00:00:00\", dateTimes.get(0).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateTimeTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * DateTime单元测试\n *\n * @author Looly\n *\n */\npublic class DateTimeTest {\n\n\t@Test\n\tpublic void datetimeTest() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\n\t\t// 年\n\t\tint year = dateTime.year();\n\t\tassertEquals(2017, year);\n\n\t\t// 季度（非季节）\n\t\tQuarter season = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q1, season);\n\n\t\t// 月份\n\t\tMonth month = dateTime.monthEnum();\n\t\tassertEquals(Month.JANUARY, month);\n\n\t\t// 日\n\t\tint day = dateTime.dayOfMonth();\n\t\tassertEquals(5, day);\n\t}\n\n\t@Test\n\tpublic void datetimeTest2() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\");\n\n\t\t// 年\n\t\tint year = dateTime.year();\n\t\tassertEquals(2017, year);\n\n\t\t// 季度（非季节）\n\t\tQuarter season = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q1, season);\n\n\t\t// 月份\n\t\tMonth month = dateTime.monthEnum();\n\t\tassertEquals(Month.JANUARY, month);\n\n\t\t// 日\n\t\tint day = dateTime.dayOfMonth();\n\t\tassertEquals(5, day);\n\t}\n\n\t@Test\n\tpublic void quarterTest() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\t\tQuarter quarter = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q1, quarter);\n\n\t\tdateTime = new DateTime(\"2017-04-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\t\tquarter = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q2, quarter);\n\n\t\tdateTime = new DateTime(\"2017-07-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\t\tquarter = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q3, quarter);\n\n\t\tdateTime = new DateTime(\"2017-10-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\t\tquarter = dateTime.quarterEnum();\n\t\tassertEquals(Quarter.Q4, quarter);\n\n\t\t// 精确到毫秒\n\t\tDateTime beginTime = new DateTime(\"2017-10-01 00:00:00.000\", DatePattern.NORM_DATETIME_MS_FORMAT);\n\t\tdateTime = DateUtil.beginOfQuarter(dateTime);\n\t\tassertEquals(beginTime, dateTime);\n\n\t\t// 精确到毫秒\n\t\tDateTime endTime = new DateTime(\"2017-12-31 23:59:59.999\", DatePattern.NORM_DATETIME_MS_FORMAT);\n\t\tdateTime = DateUtil.endOfQuarter(dateTime);\n\t\tassertEquals(endTime, dateTime);\n\t}\n\n\t@Test\n\tpublic void mutableTest() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\n\t\t// 默认情况下DateTime为可变对象\n\t\tDateTime offsite = dateTime.offset(DateField.YEAR, 0);\n\t\tassertSame(offsite, dateTime);\n\n\t\t// 设置为不可变对象后变动将返回新对象\n\t\tdateTime.setMutable(false);\n\t\toffsite = dateTime.offset(DateField.YEAR, 0);\n\t\tassertNotSame(offsite, dateTime);\n\t}\n\n\t@Test\n\tpublic void toStringTest() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\t\tassertEquals(\"2017-01-05 12:34:23\", dateTime.toString());\n\n\t\tString dateStr = dateTime.toString(\"yyyy/MM/dd\");\n\t\tassertEquals(\"2017/01/05\", dateStr);\n\t}\n\n\t@Test\n\tpublic void toStringTest2() {\n\t\tDateTime dateTime = new DateTime(\"2017-01-05 12:34:23\", DatePattern.NORM_DATETIME_FORMAT);\n\n\t\tString dateStr = dateTime.toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN);\n\t\tassertEquals(\"2017-01-05T12:34:23+0800\", dateStr);\n\n\t\tdateStr = dateTime.toString(DatePattern.UTC_WITH_XXX_OFFSET_PATTERN);\n\t\tassertEquals(\"2017-01-05T12:34:23+08:00\", dateStr);\n\t}\n\n\t@Test\n\tpublic void monthTest() {\n\t\t//noinspection ConstantConditions\n\t\tint month = DateUtil.parse(\"2017-07-01\").month();\n\t\tassertEquals(6, month);\n\t}\n\n\t@Test\n\tpublic void weekOfYearTest() {\n\t\tDateTime date = DateUtil.parse(\"2016-12-27\");\n\t\t//noinspection ConstantConditions\n\t\tassertEquals(2016, date.year());\n\t\t//跨年的周返回的总是1\n\t\tassertEquals(1, date.weekOfYear());\n\t}\n\n\t/**\n\t * 严格模式下，不允许非常规的数字，如秒部分最多59，99则报错\n\t */\n\t@Test\n\tpublic void ofTest(){\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tString a = \"2021-09-27 00:00:99\";\n\t\t\tnew DateTime(a, DatePattern.NORM_DATETIME_FORMAT, false);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.BetweenFormatter.Level;\nimport cn.hutool.core.date.format.FastDateFormat;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.SimpleDateFormat;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 时间工具单元测试<br>\n * 此单元测试依赖时区为中国+08:00\n *\n * <pre>\n * export TZ=Asia/Shanghai\n * </pre>\n *\n * @author Looly\n */\npublic class DateUtilTest {\n\n\t@Test\n\tpublic void nowTest() {\n\t\t// 当前时间\n\t\tfinal Date date = DateUtil.date();\n\t\tassertNotNull(date);\n\t\t// 当前时间\n\t\tfinal Date date2 = DateUtil.date(Calendar.getInstance());\n\t\tassertNotNull(date2);\n\t\t// 当前时间\n\t\tfinal Date date3 = DateUtil.date(System.currentTimeMillis());\n\t\tassertNotNull(date3);\n\n\t\t// 当前日期字符串，格式：yyyy-MM-dd HH:mm:ss\n\t\tfinal String now = DateUtil.now();\n\t\tassertNotNull(now);\n\t\t// 当前日期字符串，格式：yyyy-MM-dd\n\t\tfinal String today = DateUtil.today();\n\t\tassertNotNull(today);\n\t}\n\n\t@Test\n\tpublic void formatAndParseTest() {\n\t\tfinal String dateStr = \"2017-03-01\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\tfinal String format = DateUtil.format(date, \"yyyy/MM/dd\");\n\t\tassertEquals(\"2017/03/01\", format);\n\n\t\t// 常用格式的格式化\n\t\tfinal String formatDate = DateUtil.formatDate(date);\n\t\tassertEquals(\"2017-03-01\", formatDate);\n\t\tfinal String formatDateTime = DateUtil.formatDateTime(date);\n\t\tassertEquals(\"2017-03-01 00:00:00\", formatDateTime);\n\t\tfinal String formatTime = DateUtil.formatTime(date);\n\t\tassertEquals(\"00:00:00\", formatTime);\n\t}\n\n\t@Test\n\tpublic void formatAndParseCustomTest() {\n\t\tfinal String dateStr = \"2017-03-01\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\tfinal String format = DateUtil.format(date, \"#sss\");\n\t\tassertEquals(\"1488297600\", format);\n\n\t\tfinal DateTime parse = DateUtil.parse(format, \"#sss\");\n\t\tassertEquals(date, parse);\n\t}\n\n\t@Test\n\tpublic void formatAndParseCustomTest2() {\n\t\tfinal String dateStr = \"2017-03-01\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\tfinal String format = DateUtil.format(date, \"#SSS\");\n\t\tassertEquals(\"1488297600000\", format);\n\n\t\tfinal DateTime parse = DateUtil.parse(format, \"#SSS\");\n\t\tassertEquals(date, parse);\n\t}\n\n\t@Test\n\tpublic void beginAndEndTest() {\n\t\tfinal String dateStr = \"2017-03-01 00:33:23\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\t// 一天的开始\n\t\tfinal Date beginOfDay = DateUtil.beginOfDay(date);\n\t\tassertEquals(\"2017-03-01 00:00:00\", beginOfDay.toString());\n\t\t// 一天的结束\n\t\tfinal Date endOfDay = DateUtil.endOfDay(date);\n\t\tassertEquals(\"2017-03-01 23:59:59\", endOfDay.toString());\n\t}\n\n\t@Test\n\tpublic void endOfDayTest() {\n\t\tfinal DateTime parse = DateUtil.parse(\"2020-05-31 00:00:00\");\n\t\tassertEquals(\"2020-05-31 23:59:59\", DateUtil.endOfDay(parse).toString());\n\t}\n\n\t@Test\n\tpublic void truncateTest() {\n\t\tfinal String dateStr2 = \"2020-02-29 12:59:34\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\t\tfinal DateTime dateTime = DateUtil.truncate(date2, DateField.MINUTE);\n\t\tassertEquals(\"2020-02-29 12:59:00\", dateTime.toString());\n\t}\n\n\t@Test\n\tpublic void ceilingMinuteTest() {\n\t\tfinal String dateStr2 = \"2020-02-29 12:59:34\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\n\t\tDateTime dateTime = DateUtil.ceiling(date2, DateField.MINUTE);\n\t\tassertEquals(\"2020-02-29 12:59:59.999\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tdateTime = DateUtil.ceiling(date2, DateField.MINUTE, true);\n\t\tassertEquals(\"2020-02-29 12:59:59.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test\n\tpublic void cellingAmPmTest(){\n\t\tfinal String dateStr2 = \"2020-02-29 10:59:34\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\n\t\tDateTime dateTime = DateUtil.ceiling(date2, DateField.AM_PM);\n\t\tassertEquals(\"2020-02-29 11:59:59.999\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tdateTime = DateUtil.ceiling(date2, DateField.AM_PM, true);\n\t\tassertEquals(\"2020-02-29 11:59:59.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test void roundAmPmTest() {\n\t\tfinal String dateStr = \"2020-02-29 13:59:34\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\tDateTime dateTime = DateUtil.round(date, DateField.AM_PM);\n\t\tassertEquals(\"2020-02-29 12:59:59.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tfinal String dateStr2 = \"2020-02-29 18:59:34\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\t\tDateTime dateTime2 = DateUtil.round(date2, DateField.AM_PM);\n\t\tassertEquals(\"2020-02-29 23:59:59.000\", dateTime2.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test\n\tpublic void ceilingDayTest() {\n\t\tfinal String dateStr2 = \"2020-02-29 12:59:34\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\n\t\tDateTime dateTime = DateUtil.ceiling(date2, DateField.DAY_OF_MONTH);\n\t\tassertEquals(\"2020-02-29 23:59:59.999\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tdateTime = DateUtil.ceiling(date2, DateField.DAY_OF_MONTH, true);\n\t\tassertEquals(\"2020-02-29 23:59:59.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t@Test\n\tpublic void beginOfWeekTest() {\n\t\tfinal String dateStr = \"2017-03-01 22:33:23\";\n\t\tfinal DateTime date = DateUtil.parse(dateStr);\n\t\tObjects.requireNonNull(date).setFirstDayOfWeek(Week.MONDAY);\n\n\t\t// 一周的开始\n\t\tfinal Date beginOfWeek = DateUtil.beginOfWeek(date);\n\t\tassertEquals(\"2017-02-27 00:00:00\", beginOfWeek.toString());\n\t\t// 一周的结束\n\t\tfinal Date endOfWeek = DateUtil.endOfWeek(date);\n\t\tassertEquals(\"2017-03-05 23:59:59\", endOfWeek.toString());\n\n\t\tfinal Calendar calendar = DateUtil.calendar(date);\n\t\t// 一周的开始\n\t\tfinal Calendar begin = DateUtil.beginOfWeek(calendar);\n\t\tassertEquals(\"2017-02-27 00:00:00\", DateUtil.date(begin).toString());\n\t\t// 一周的结束\n\t\tfinal Calendar end = DateUtil.endOfWeek(calendar);\n\t\tassertEquals(\"2017-03-05 23:59:59\", DateUtil.date(end).toString());\n\t}\n\n\t@Test\n\tpublic void beginOfWeekTest2() {\n\t\tfinal String beginStr = \"2020-03-11\";\n\t\tfinal DateTime date = DateUtil.parseDate(beginStr);\n\t\tfinal Calendar calendar = date.toCalendar();\n\t\tfinal Calendar begin = DateUtil.beginOfWeek(calendar, false);\n\t\tassertEquals(\"2020-03-08 00:00:00\", DateUtil.date(begin).toString());\n\n\t\tfinal Calendar calendar2 = date.toCalendar();\n\t\tfinal Calendar end = DateUtil.endOfWeek(calendar2, false);\n\t\tassertEquals(\"2020-03-14 23:59:59\", DateUtil.date(end).toString());\n\t}\n\n\t@Test\n\tpublic void offsetDateTest() {\n\t\tfinal String dateStr = \"2017-03-01 22:33:23\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\n\t\tfinal Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);\n\t\tassertNotNull(newDate);\n\t\tassertEquals(\"2017-03-03 22:33:23\", newDate.toString());\n\n\t\t// 偏移天\n\t\tfinal DateTime newDate2 = DateUtil.offsetDay(date, 3);\n\t\tassertNotNull(newDate2);\n\t\tassertEquals(\"2017-03-04 22:33:23\", newDate2.toString());\n\n\t\t// 偏移小时\n\t\tfinal DateTime newDate3 = DateUtil.offsetHour(date, -3);\n\t\tassertNotNull(newDate3);\n\t\tassertEquals(\"2017-03-01 19:33:23\", newDate3.toString());\n\n\t\t// 偏移月\n\t\tfinal DateTime offsetMonth = DateUtil.offsetMonth(date, -1);\n\t\tassertNotNull(offsetMonth);\n\t\tassertEquals(\"2017-02-01 22:33:23\", offsetMonth.toString());\n\t}\n\n\t@Test\n\tpublic void offsetMonthTest() {\n\t\tfinal DateTime st = DateUtil.parseDate(\"2018-05-31\");\n\t\tfinal List<DateTime> list = new ArrayList<>();\n\t\tfor (int i = 0; i < 4; i++) {\n\t\t\tlist.add(DateUtil.offsetMonth(st, i));\n\t\t}\n\t\tassertEquals(\"2018-05-31 00:00:00\", list.get(0).toString());\n\t\tassertEquals(\"2018-06-30 00:00:00\", list.get(1).toString());\n\t\tassertEquals(\"2018-07-31 00:00:00\", list.get(2).toString());\n\t\tassertEquals(\"2018-08-31 00:00:00\", list.get(3).toString());\n\t}\n\n\t@Test\n\tpublic void betweenTest() {\n\t\tfinal String dateStr1 = \"2017-03-01 22:34:23\";\n\t\tfinal Date date1 = DateUtil.parse(dateStr1);\n\n\t\tfinal String dateStr2 = \"2017-04-01 23:56:14\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\t\t// 相差月\n\t\tlong betweenMonth = DateUtil.betweenMonth(date1, date2, false);\n\t\tassertEquals(1, betweenMonth);// 相差一个月\n\t\t// 反向\n\t\tbetweenMonth = DateUtil.betweenMonth(date2, date1, false);\n\t\tassertEquals(1, betweenMonth);// 相差一个月\n\n\t\t// 相差天\n\t\tlong betweenDay = DateUtil.between(date1, date2, DateUnit.DAY);\n\t\tassertEquals(31, betweenDay);// 相差一个月，31天\n\t\t// 反向\n\t\tbetweenDay = DateUtil.between(date2, date1, DateUnit.DAY);\n\t\tassertEquals(31, betweenDay);// 相差一个月，31天\n\n\t\t// 相差小时\n\t\tlong betweenHour = DateUtil.between(date1, date2, DateUnit.HOUR);\n\t\tassertEquals(745, betweenHour);\n\t\t// 反向\n\t\tbetweenHour = DateUtil.between(date2, date1, DateUnit.HOUR);\n\t\tassertEquals(745, betweenHour);\n\n\t\t// 相差分\n\t\tlong betweenMinute = DateUtil.between(date1, date2, DateUnit.MINUTE);\n\t\tassertEquals(44721, betweenMinute);\n\t\t// 反向\n\t\tbetweenMinute = DateUtil.between(date2, date1, DateUnit.MINUTE);\n\t\tassertEquals(44721, betweenMinute);\n\n\t\t// 相差秒\n\t\tlong betweenSecond = DateUtil.between(date1, date2, DateUnit.SECOND);\n\t\tassertEquals(2683311, betweenSecond);\n\t\t// 反向\n\t\tbetweenSecond = DateUtil.between(date2, date1, DateUnit.SECOND);\n\t\tassertEquals(2683311, betweenSecond);\n\n\t\t// 相差秒\n\t\tlong betweenMS = DateUtil.between(date1, date2, DateUnit.MS);\n\t\tassertEquals(2683311000L, betweenMS);\n\t\t// 反向\n\t\tbetweenMS = DateUtil.between(date2, date1, DateUnit.MS);\n\t\tassertEquals(2683311000L, betweenMS);\n\t}\n\n\t@Test\n\tpublic void betweenTest2() {\n\t\tfinal long between = DateUtil.between(DateUtil.parse(\"2019-05-06 02:15:00\"), DateUtil.parse(\"2019-05-06 02:20:00\"), DateUnit.HOUR);\n\t\tassertEquals(0, between);\n\t}\n\n\t@Test\n\tpublic void betweenTest3() {\n\t\tfinal long between = DateUtil.between(DateUtil.parse(\"2020-03-31 23:59:59\"), DateUtil.parse(\"2020-04-01 00:00:00\"), DateUnit.SECOND);\n\t\tassertEquals(1, between);\n\t}\n\n\t@Test\n\tpublic void formatChineseDateTest() {\n\t\tString formatChineseDate = DateUtil.formatChineseDate(DateUtil.parse(\"2018-02-24\"), true, false);\n\t\tassertEquals(\"二〇一八年二月二十四日\", formatChineseDate);\n\n\t\tformatChineseDate = DateUtil.formatChineseDate(DateUtil.parse(\"2018-02-14\"), true, false);\n\t\tassertEquals(\"二〇一八年二月十四日\", formatChineseDate);\n\t}\n\n\t@Test\n\tpublic void formatChineseDateTimeTest() {\n\t\tString formatChineseDateTime = DateUtil.formatChineseDate(DateUtil.parse(\"2018-02-24 12:13:14\"), true, true);\n\t\tassertEquals(\"二〇一八年二月二十四日十二时十三分十四秒\", formatChineseDateTime);\n\n\t\tformatChineseDateTime = DateUtil.formatChineseDate(DateUtil.parse(\"2022-01-18 12:00:00\"), true, true);\n\t\tassertEquals(\"二〇二二年一月十八日十二时零分零秒\", formatChineseDateTime);\n\t}\n\n\t@Test\n\tpublic void formatBetweenTest() {\n\t\tfinal String dateStr1 = \"2017-03-01 22:34:23\";\n\t\tfinal Date date1 = DateUtil.parse(dateStr1);\n\n\t\tfinal String dateStr2 = \"2017-04-01 23:56:14\";\n\t\tfinal Date date2 = DateUtil.parse(dateStr2);\n\n\t\tfinal long between = DateUtil.between(date1, date2, DateUnit.MS);\n\t\tfinal String formatBetween = DateUtil.formatBetween(between, Level.MINUTE);\n\t\tassertEquals(\"31天1小时21分\", formatBetween);\n\t}\n\n\t@Test\n\tpublic void timerTest() {\n\t\tfinal TimeInterval timer = DateUtil.timer();\n\n\t\t// ---------------------------------\n\t\t// -------这是执行过程\n\t\t// ---------------------------------\n\n\t\ttimer.interval();// 花费毫秒数\n\t\ttimer.intervalRestart();// 返回花费时间，并重置开始时间\n\t\ttimer.intervalMinute();// 花费分钟数\n\t}\n\n\t@Test\n\tpublic void currentTest() {\n\t\tfinal long current = DateUtil.current();\n\t\tfinal String currentStr = String.valueOf(current);\n\t\tassertEquals(13, currentStr.length());\n\n\t\tfinal long currentNano = DateUtil.current();\n\t\tfinal String currentNanoStr = String.valueOf(currentNano);\n\t\tassertNotNull(currentNanoStr);\n\t}\n\n\t@Test\n\tpublic void weekOfYearTest() {\n\t\t// 第一周周日\n\t\tfinal int weekOfYear1 = DateUtil.weekOfYear(DateUtil.parse(\"2016-01-03\"));\n\t\tassertEquals(1, weekOfYear1);\n\n\t\t// 第二周周四\n\t\tfinal int weekOfYear2 = DateUtil.weekOfYear(DateUtil.parse(\"2016-01-07\"));\n\t\tassertEquals(2, weekOfYear2);\n\t}\n\n\t@Test\n\tpublic void timeToSecondTest() {\n\t\tint second = DateUtil.timeToSecond(\"00:01:40\");\n\t\tassertEquals(100, second);\n\t\tsecond = DateUtil.timeToSecond(\"00:00:40\");\n\t\tassertEquals(40, second);\n\t\tsecond = DateUtil.timeToSecond(\"01:00:00\");\n\t\tassertEquals(3600, second);\n\t\tsecond = DateUtil.timeToSecond(\"00:00:00\");\n\t\tassertEquals(0, second);\n\t}\n\n\t@Test\n\tpublic void secondToTimeTest() {\n\t\tString time = DateUtil.secondToTime(3600);\n\t\tassertEquals(\"01:00:00\", time);\n\t\ttime = DateUtil.secondToTime(3800);\n\t\tassertEquals(\"01:03:20\", time);\n\t\ttime = DateUtil.secondToTime(0);\n\t\tassertEquals(\"00:00:00\", time);\n\t\ttime = DateUtil.secondToTime(30);\n\t\tassertEquals(\"00:00:30\", time);\n\t}\n\n\t@Test\n\tpublic void secondToTimeTest2() {\n\t\tfinal String s1 = \"55:02:18\";\n\t\tfinal String s2 = \"55:00:50\";\n\t\tfinal int i = DateUtil.timeToSecond(s1) + DateUtil.timeToSecond(s2);\n\t\tfinal String s = DateUtil.secondToTime(i);\n\t\tassertEquals(\"110:03:08\", s);\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\t// 转换时间与SimpleDateFormat结果保持一致即可\n\t\tfinal String birthday = \"700403\";\n\t\tfinal Date birthDate = DateUtil.parse(birthday, \"yyMMdd\");\n\t\t// 获取出生年(完全表现形式,如：2010)\n\t\tfinal int sYear = DateUtil.year(birthDate);\n\t\tassertEquals(1970, sYear);\n\t}\n\n\t@Test\n\tpublic void parseTest3() {\n\t\tfinal String dateStr = \"2018-10-10 12:11:11\";\n\t\tfinal Date date = DateUtil.parse(dateStr);\n\t\tfinal String format = DateUtil.format(date, DatePattern.NORM_DATETIME_PATTERN);\n\t\tassertEquals(dateStr, format);\n\t}\n\n\t@Test\n\tpublic void parseTest4() {\n\t\tfinal String ymd = DateUtil.parse(\"2019-3-21 12:20:15\", \"yyyy-MM-dd\").toString(DatePattern.PURE_DATE_PATTERN);\n\t\tassertEquals(\"20190321\", ymd);\n\t}\n\n\t@Test\n\tpublic void parseTest5() {\n\t\t// 测试时间解析\n\t\t//noinspection ConstantConditions\n\t\tString time = DateUtil.parse(\"22:12:12\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"22:12:12\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"2:12:12\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"02:12:12\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"2:2:12\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"02:02:12\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"2:2:1\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"02:02:01\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"22:2:1\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"22:02:01\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"2:22:1\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"02:22:01\", time);\n\n\t\t// 测试两位时间解析\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"2:22\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"02:22:00\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"12:22\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"12:22:00\", time);\n\t\t//noinspection ConstantConditions\n\t\ttime = DateUtil.parse(\"12:2\").toString(DatePattern.NORM_TIME_FORMAT);\n\t\tassertEquals(\"12:02:00\", time);\n\n\t}\n\n\t@Test\n\tpublic void parseTest6() {\n\t\tfinal String str = \"Tue Jun 4 16:25:15 +0800 2019\";\n\t\tfinal DateTime dateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-06-04 16:25:15\", dateTime.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest7() {\n\t\tString str = \"2019-06-01T19:45:43.000 +0800\";\n\t\tDateTime dateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-06-01 19:45:43\", dateTime.toString());\n\n\t\tstr = \"2019-06-01T19:45:43 +08:00\";\n\t\tdateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-06-01 19:45:43\", dateTime.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest8() {\n\t\tfinal String str = \"2020-06-28T02:14:13.000Z\";\n\t\tfinal DateTime dateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2020-06-28 02:14:13\", dateTime.toString());\n\t}\n\n\t/**\n\t * 测试支持：yyyy-MM-dd HH:mm:ss.SSSSSS 格式\n\t */\n\t@Test\n\tpublic void parseNormFullTest() {\n\t\tString str = \"2020-02-06 01:58:00.000020\";\n\t\tDateTime dateTime = DateUtil.parse(str);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2020-02-06 01:58:00.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tstr = \"2020-02-06 01:58:00.00002\";\n\t\tdateTime = DateUtil.parse(str);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2020-02-06 01:58:00.000\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tstr = \"2020-02-06 01:58:00.111000\";\n\t\tdateTime = DateUtil.parse(str);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2020-02-06 01:58:00.111\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\n\t\tstr = \"2020-02-06 01:58:00.111\";\n\t\tdateTime = DateUtil.parse(str);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2020-02-06 01:58:00.111\", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));\n\t}\n\n\t/**\n\t * 测试字符串是空，返回null, 而不是直接报错；\n\t */\n\t@Test\n\tpublic void parseEmptyTest() {\n\t\tfinal String str = \" \";\n\t\tfinal DateTime dateTime = DateUtil.parse(str);\n\t\tassertNull(dateTime);\n\t}\n\n\t@Test\n\tpublic void parseUTCOffsetTest() {\n\t\t// issue#I437AP@Gitee\n\t\tString str = \"2019-06-01T19:45:43+08:00\";\n\t\tDateTime dateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-06-01 19:45:43\", dateTime.toString());\n\n\t\tstr = \"2019-06-01T19:45:43 +08:00\";\n\t\tdateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-06-01 19:45:43\", dateTime.toString());\n\t}\n\n\t@Test\n\tpublic void parseAndOffsetTest() {\n\t\t// 检查UTC时间偏移是否准确\n\t\tfinal String str = \"2019-09-17T13:26:17.948Z\";\n\t\tfinal DateTime dateTime = DateUtil.parse(str);\n\t\tassert dateTime != null;\n\t\tassertEquals(\"2019-09-17 13:26:17\", dateTime.toString());\n\n\t\tfinal DateTime offset = DateUtil.offsetHour(dateTime, 8);\n\t\tassertEquals(\"2019-09-17 21:26:17\", offset.toString());\n\t}\n\n\t@Test\n\tpublic void parseDateTest() {\n\t\tfinal String dateStr = \"2018-4-10\";\n\t\tfinal Date date = DateUtil.parseDate(dateStr);\n\t\tfinal String format = DateUtil.format(date, DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"2018-04-10\", format);\n\t}\n\n\t@Test\n\tpublic void parseToDateTimeTest1() {\n\t\tfinal String dateStr1 = \"2017-02-01\";\n\t\tfinal String dateStr2 = \"2017/02/01\";\n\t\tfinal String dateStr3 = \"2017.02.01\";\n\t\tfinal String dateStr4 = \"2017年02月01日\";\n\n\t\tfinal DateTime dt1 = DateUtil.parse(dateStr1);\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr2);\n\t\tfinal DateTime dt3 = DateUtil.parse(dateStr3);\n\t\tfinal DateTime dt4 = DateUtil.parse(dateStr4);\n\t\tassertEquals(dt1, dt2);\n\t\tassertEquals(dt2, dt3);\n\t\tassertEquals(dt3, dt4);\n\t}\n\n\t@Test\n\tpublic void parseToDateTimeTest2() {\n\t\tfinal String dateStr1 = \"2017-02-01 12:23\";\n\t\tfinal String dateStr2 = \"2017/02/01 12:23\";\n\t\tfinal String dateStr3 = \"2017.02.01 12:23\";\n\t\tfinal String dateStr4 = \"2017年02月01日 12:23\";\n\n\t\tfinal DateTime dt1 = DateUtil.parse(dateStr1);\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr2);\n\t\tfinal DateTime dt3 = DateUtil.parse(dateStr3);\n\t\tfinal DateTime dt4 = DateUtil.parse(dateStr4);\n\t\tassertEquals(dt1, dt2);\n\t\tassertEquals(dt2, dt3);\n\t\tassertEquals(dt3, dt4);\n\t}\n\n\t@Test\n\tpublic void parseToDateTimeTest3() {\n\t\tfinal String dateStr1 = \"2017-02-01 12:23:45\";\n\t\tfinal String dateStr2 = \"2017/02/01 12:23:45\";\n\t\tfinal String dateStr3 = \"2017.02.01 12:23:45\";\n\t\tfinal String dateStr4 = \"2017年02月01日 12时23分45秒\";\n\n\t\tfinal DateTime dt1 = DateUtil.parse(dateStr1);\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr2);\n\t\tfinal DateTime dt3 = DateUtil.parse(dateStr3);\n\t\tfinal DateTime dt4 = DateUtil.parse(dateStr4);\n\t\tassertEquals(dt1, dt2);\n\t\tassertEquals(dt2, dt3);\n\t\tassertEquals(dt3, dt4);\n\t}\n\n\t@Test\n\tpublic void parseToDateTimeTest4() {\n\t\tfinal String dateStr1 = \"2017-02-01 12:23:45\";\n\t\tfinal String dateStr2 = \"20170201122345\";\n\n\t\tfinal DateTime dt1 = DateUtil.parse(dateStr1);\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr2);\n\t\tassertEquals(dt1, dt2);\n\t}\n\n\t@Test\n\tpublic void parseToDateTimeTest5() {\n\t\tfinal String dateStr1 = \"2017-02-01\";\n\t\tfinal String dateStr2 = \"20170201\";\n\n\t\tfinal DateTime dt1 = DateUtil.parse(dateStr1);\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr2);\n\t\tassertEquals(dt1, dt2);\n\t}\n\n\t@Test\n\tpublic void parseISO8601Test() {\n\t\tString dateStr1 = \"2018-09-13T05:34:31Z\";\n\t\tDateTime dt = DateUtil.parseISO8601(dateStr1);\n\n\t\t// parse方法支持UTC格式测试\n\t\tfinal DateTime dt2 = DateUtil.parse(dateStr1);\n\t\tassertEquals(dt, dt2);\n\n\t\t// 默认使用Pattern对应的时区，即UTC时区\n\t\tString dateStr = dt.toString();\n\t\tassertEquals(\"2018-09-13 05:34:31\", dateStr);\n\n\t\t// 使用当前（上海）时区\n\t\tdateStr = dt.toString(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tassertEquals(\"2018-09-13 13:34:31\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:32+0800\";\n\t\tdt = DateUtil.parseISO8601(dateStr1);\n\t\tdateStr = dt.toString(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tassertEquals(\"2018-09-13 13:34:32\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:33+08:00\";\n\t\tdt = DateUtil.parseISO8601(dateStr1);\n\t\tdateStr = dt.toString(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tassertEquals(\"2018-09-13 13:34:33\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:34+0800\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tassertEquals(\"2018-09-13 13:34:34\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:35+08:00\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tassertEquals(\"2018-09-13 13:34:35\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:36.999+0800\";\n\t\tdt = DateUtil.parseISO8601(dateStr1);\n\t\tfinal SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_MS_PATTERN);\n\t\tsimpleDateFormat.setTimeZone(TimeZone.getTimeZone(\"GMT+8:00\"));\n\t\tdateStr = dt.toString(simpleDateFormat);\n\t\tassertEquals(\"2018-09-13 13:34:36.999\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:37.999+08:00\";\n\t\tdt = DateUtil.parseISO8601(dateStr1);\n\t\tdateStr = dt.toString(simpleDateFormat);\n\t\tassertEquals(\"2018-09-13 13:34:37.999\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:38.999+0800\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString(simpleDateFormat);\n\t\tassertEquals(\"2018-09-13 13:34:38.999\", dateStr);\n\n\t\tdateStr1 = \"2018-09-13T13:34:39.999+08:00\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString(simpleDateFormat);\n\t\tassertEquals(\"2018-09-13 13:34:39.999\", dateStr);\n\n\t\tdateStr1 = \"2025-07-28T20:00+08:00\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString();\n\t\tassertEquals(\"2025-07-28 20:00:00\", dateStr);\n\n\t\t// 使用UTC时区\n\t\tdateStr1 = \"2018-09-13T13:34:39.99\";\n\t\tdt = DateUtil.parse(dateStr1);\n\t\tassert dt != null;\n\t\tdateStr = dt.toString();\n\t\tassertEquals(\"2018-09-13 13:34:39\", dateStr);\n\t}\n\n\t@Test\n\tpublic void parseUTCTest() {\n\t\t// issue1503@Github\n\t\t// 检查不同毫秒长度都可以正常匹配\n\t\tString utcTime = \"2021-03-30T12:56:51.3Z\";\n\t\tDateTime parse = DateUtil.parseISO8601(utcTime);\n\t\tassertEquals(\"2021-03-30 12:56:51\", parse.toString());\n\n\t\tutcTime = \"2021-03-30T12:56:51.34Z\";\n\t\tparse = DateUtil.parseISO8601(utcTime);\n\t\tassertEquals(\"2021-03-30 12:56:51\", parse.toString());\n\n\t\tutcTime = \"2021-03-30T12:56:51.345Z\";\n\t\tparse = DateUtil.parseISO8601(utcTime);\n\t\tassertEquals(\"2021-03-30 12:56:51\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseUTCTest3() {\n\t\t// issue#I5M6DP\n\t\tfinal String dateStr = \"2022-08-13T09:30\";\n\t\tfinal DateTime dateTime = DateUtil.parse(dateStr);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2022-08-13 09:30:00\", dateTime.toString());\n\t}\n\n\t@Test\n\tpublic void parseRFC2822Test() {\n\t\tfinal String dateStr = \"Wed Sep 16 11:26:23 CST 2009\";\n\n\t\tfinal SimpleDateFormat sdf = new SimpleDateFormat(DatePattern.JDK_DATETIME_PATTERN, Locale.US);\n\t\t// Asia/Shanghai是以地区命名的地区标准时，在中国叫CST，因此如果解析CST时不使用\"Asia/Shanghai\"而使用\"GMT+08:00\"，会导致相差一个小时\n\t\tsdf.setTimeZone(TimeZone.getTimeZone(\"Asia/Shanghai\"));\n\t\tfinal DateTime parse = DateUtil.parse(dateStr, sdf);\n\n\t\tDateTime dateTime = DateUtil.parseRFC2822(dateStr);\n\t\tassertEquals(parse, dateTime);\n\n\t\tdateTime = DateUtil.parse(dateStr);\n\t\tassertEquals(parse, dateTime);\n\t}\n\n\t@Test\n\tpublic void parseCSTTest2() {\n\t\tfinal String dateStr = \"Wed Sep 16 11:26:23 CST 2009\";\n\n\t\tfinal SimpleDateFormat sdf = new SimpleDateFormat(DatePattern.JDK_DATETIME_PATTERN, Locale.US);\n\t\tsdf.setTimeZone(TimeZone.getTimeZone(\"America/Chicago\"));\n\t\tfinal DateTime parse = DateUtil.parse(dateStr, sdf);\n\n\t\tfinal FastDateFormat fdf = FastDateFormat.getInstance(DatePattern.JDK_DATETIME_PATTERN, TimeZone.getTimeZone(\"America/Chicago\"), Locale.US);\n\t\tfinal DateTime parse2 = DateUtil.parse(dateStr, fdf);\n\n\t\tassertEquals(parse, parse2);\n\t}\n\n\t@Test\n\tpublic void parseJDkTest() {\n\t\tfinal String dateStr = \"Thu May 16 17:57:18 GMT+08:00 2019\";\n\t\tfinal DateTime time = DateUtil.parse(dateStr);\n\t\tassertEquals(\"2019-05-16 17:57:18\", Objects.requireNonNull(time).toString());\n\t}\n\n\t@Test\n\tpublic void parseISOTest() {\n\t\tfinal String dateStr = \"2020-04-23T02:31:00.000Z\";\n\t\tfinal DateTime time = DateUtil.parse(dateStr);\n\t\tassertEquals(\"2020-04-23 02:31:00\", Objects.requireNonNull(time).toString());\n\t}\n\n\t@Test\n\tpublic void endOfYearTest() {\n\t\tfinal DateTime date = DateUtil.date();\n\t\tdate.setField(DateField.YEAR, 2019);\n\t\tfinal DateTime endOfYear = DateUtil.endOfYear(date);\n\t\tassertEquals(\"2019-12-31 23:59:59\", endOfYear.toString());\n\t}\n\n\t@Test\n\tpublic void endOfQuarterTest() {\n\t\tfinal Date date = DateUtil.endOfQuarter(\n\t\t\t\tDateUtil.parse(\"2020-05-31 00:00:00\"));\n\n\t\tassertEquals(\"2020-06-30 23:59:59\", DateUtil.format(date, \"yyyy-MM-dd HH:mm:ss\"));\n\t}\n\n\t@Test\n\tpublic void endOfWeekTest() {\n\t\t// 周日\n\t\tfinal DateTime now = DateUtil.parse(\"2019-09-15 13:00\");\n\n\t\tfinal DateTime startOfWeek = DateUtil.beginOfWeek(now);\n\t\tassertEquals(\"2019-09-09 00:00:00\", startOfWeek.toString());\n\t\tfinal DateTime endOfWeek = DateUtil.endOfWeek(now);\n\t\tassertEquals(\"2019-09-15 23:59:59\", endOfWeek.toString());\n\n\t\tfinal long between = DateUtil.between(endOfWeek, startOfWeek, DateUnit.DAY);\n\t\t// 周一和周日相距6天\n\t\tassertEquals(6, between);\n\t}\n\n\t@Test\n\tpublic void dayOfWeekTest() {\n\t\tfinal int dayOfWeek = DateUtil.dayOfWeek(DateUtil.parse(\"2018-03-07\"));\n\t\tassertEquals(Calendar.WEDNESDAY, dayOfWeek);\n\t\tfinal Week week = DateUtil.dayOfWeekEnum(DateUtil.parse(\"2018-03-07\"));\n\t\tassertEquals(Week.WEDNESDAY, week);\n\t}\n\n\t@Test\n\tpublic void compareTest() {\n\t\tfinal Date date1 = DateUtil.parse(\"2021-04-13 23:59:59.999\");\n\t\tfinal Date date2 = DateUtil.parse(\"2021-04-13 23:59:10\");\n\n\t\tassertEquals(1, DateUtil.compare(date1, date2));\n\t\tassertEquals(1, DateUtil.compare(date1, date2, DatePattern.NORM_DATETIME_PATTERN));\n\t\tassertEquals(0, DateUtil.compare(date1, date2, DatePattern.NORM_DATE_PATTERN));\n\t\tassertEquals(0, DateUtil.compare(date1, date2, DatePattern.NORM_DATETIME_MINUTE_PATTERN));\n\n\n\t\tfinal Date date11 = DateUtil.parse(\"2021-04-13 23:59:59.999\");\n\t\tfinal Date date22 = DateUtil.parse(\"2021-04-11 23:10:10\");\n\t\tassertEquals(0, DateUtil.compare(date11, date22, DatePattern.NORM_MONTH_PATTERN));\n\t}\n\n\t@Test\n\tpublic void yearAndQTest() {\n\t\tfinal String yearAndQuarter = DateUtil.yearAndQuarter(DateUtil.parse(\"2018-12-01\"));\n\t\tassertEquals(\"20184\", yearAndQuarter);\n\n\t\tfinal LinkedHashSet<String> yearAndQuarters = DateUtil.yearAndQuarter(DateUtil.parse(\"2018-09-10\"), DateUtil.parse(\"2018-12-20\"));\n\t\tfinal List<String> list = CollUtil.list(false, yearAndQuarters);\n\t\tassertEquals(2, list.size());\n\t\tassertEquals(\"20183\", list.get(0));\n\t\tassertEquals(\"20184\", list.get(1));\n\n\t\tfinal LinkedHashSet<String> yearAndQuarters2 = DateUtil.yearAndQuarter(DateUtil.parse(\"2018-10-10\"), DateUtil.parse(\"2018-12-10\"));\n\t\tfinal List<String> list2 = CollUtil.list(false, yearAndQuarters2);\n\t\tassertEquals(1, list2.size());\n\t\tassertEquals(\"20184\", list2.get(0));\n\t}\n\n\t@Test\n\tpublic void formatHttpDateTest() {\n\t\tfinal String formatHttpDate = DateUtil.formatHttpDate(DateUtil.parse(\"2019-01-02 22:32:01\"));\n\t\tassertEquals(\"Wed, 02 Jan 2019 14:32:01 GMT\", formatHttpDate);\n\t}\n\n\t@Test\n\tpublic void toInstantTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTime.parse(\"2017-05-06T08:30:00\", DateTimeFormatter.ISO_DATE_TIME);\n\t\tInstant instant = DateUtil.toInstant(localDateTime);\n\t\tassertEquals(\"2017-05-06T00:30:00Z\", instant.toString());\n\n\t\tfinal LocalDate localDate = localDateTime.toLocalDate();\n\t\tinstant = DateUtil.toInstant(localDate);\n\t\tassertNotNull(instant);\n\n\t\tfinal LocalTime localTime = localDateTime.toLocalTime();\n\t\tinstant = DateUtil.toInstant(localTime);\n\t\tassertNotNull(instant);\n\t}\n\n\t@Test\n\tpublic void dateTest() {\n\t\t//LocalDateTime ==> date\n\t\tfinal LocalDateTime localDateTime = LocalDateTime.parse(\"2017-05-06T08:30:00\", DateTimeFormatter.ISO_DATE_TIME);\n\t\tfinal DateTime date = DateUtil.date(localDateTime);\n\t\tassertEquals(\"2017-05-06 08:30:00\", date.toString());\n\n\t\t//LocalDate ==> date\n\t\tfinal LocalDate localDate = localDateTime.toLocalDate();\n\t\tfinal DateTime date2 = DateUtil.date(localDate);\n\t\tassertEquals(\"2017-05-06\",\n\t\t\t\tDateUtil.format(date2, DatePattern.NORM_DATE_PATTERN));\n\t}\n\n\t@Test\n\tpublic void dateTest2() {\n\t\t// 测试负数日期\n\t\tfinal long dateLong = -1497600000;\n\t\tfinal DateTime date = DateUtil.date(dateLong);\n\t\tassertEquals(\"1969-12-15 00:00:00\", date.toString());\n\t}\n\n\t@Test\n\tpublic void ageTest() {\n\t\tfinal String d1 = \"2000-02-29\";\n\t\tfinal String d2 = \"2018-02-28\";\n\t\tfinal int age = DateUtil.age(DateUtil.parseDate(d1), DateUtil.parseDate(d2));\n\n\t\t// issue#I6E6ZG，法定生日当天不算年龄，从第二天开始计算\n\t\tassertEquals(17, age);\n\t}\n\n\t@Test\n\tpublic void ageTest2() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal String d1 = \"2019-02-29\";\n\t\t\tfinal String d2 = \"2018-02-28\";\n\t\t\tDateUtil.age(DateUtil.parseDate(d1), DateUtil.parseDate(d2));\n\t\t});\n\t}\n\n\t@Test\n\tpublic void ageTest3() {\n\t\t// 按照《最高人民法院关于审理未成年人刑事案件具体应用法律若干问题的解释》第二条规定刑法第十七条规定的“周岁”，按照公历的年、月、日计算，从周岁生日的第二天起算。\n\t\t// 那我们认为就算当年是闰年，29日也算周岁生日的第二天，可以算作一岁\n\t\tfinal String d1 = \"1998-02-28\";\n\t\tfinal String d2 = \"2000-02-29\";\n\t\tfinal int age = DateUtil.age(DateUtil.parse(d1), DateUtil.parse(d2));\n\t\t// issue#I6E6ZG，法定生日当天不算年龄，从第二天开始计算\n\t\tassertEquals(2, age);\n\t}\n\n\t@Test\n\tpublic void ageTest4() {\n\t\t// 按照《最高人民法院关于审理未成年人刑事案件具体应用法律若干问题的解释》第二条规定刑法第十七条规定的“周岁”，按照公历的年、月、日计算，从周岁生日的第二天起算。\n\t\t// 那我们认为就算当年是闰年，29日也算周岁生日的第二天，可以算作一岁\n\t\tfinal String d1 = \"1999-02-28\";\n\t\tfinal String d2 = \"2000-02-29\";\n\t\tfinal int age = DateUtil.age(DateUtil.parse(d1), DateUtil.parse(d2));\n\t\t// issue#I6E6ZG，法定生日当天不算年龄，从第二天开始计算\n\t\tassertEquals(1, age);\n\t}\n\n\t@Test\n\tpublic void isExpiredTest() {\n\t\tfinal DateTime startDate = DateUtil.parse(\"2019-12-01 17:02:30\");\n\t\tfinal DateTime endDate = DateUtil.parse(\"2019-12-02 17:02:30\");\n\t\tfinal int length = 3;\n\t\t//noinspection deprecation\n\t\tfinal boolean expired = DateUtil.isExpired(startDate, DateField.DAY_OF_YEAR, length, endDate);\n\t\tassertTrue(expired);\n\t}\n\n\t@Test\n\tpublic void localDateTimeTest() {\n\t\t// 测试字符串与LocalDateTime的互相转换\n\t\tfinal String strDate = \"2019-12-01 17:02:30\";\n\t\tLocalDateTime ldt = DateUtil.parseLocalDateTime(strDate);\n\t\tString strDate1 = DateUtil.formatLocalDateTime(ldt);\n\t\tassertEquals(strDate, strDate1);\n\n\t\tfinal String strDate2 = \"2019-12-01 17:02:30.111\";\n\t\tldt = DateUtil.parseLocalDateTime(strDate2, DatePattern.NORM_DATETIME_MS_PATTERN);\n\t\tstrDate1 = DateUtil.format(ldt, DatePattern.NORM_DATETIME_PATTERN);\n\t\tassertEquals(strDate, strDate1);\n\t}\n\n\t@Test\n\tpublic void localDateTimeTest2() {\n\t\t// 测试字符串与LocalDateTime的互相转换\n\t\tfinal String strDate = \"2019-12-01\";\n\t\tfinal LocalDateTime localDateTime = DateUtil.parseLocalDateTime(strDate, \"yyyy-MM-dd\");\n\t\tassertEquals(strDate, DateUtil.format(localDateTime, DatePattern.NORM_DATE_PATTERN));\n\t}\n\n\t@Test\n\tpublic void betweenWeekTest() {\n\t\tfinal DateTime start = DateUtil.parse(\"2019-03-05\");\n\t\tfinal DateTime end = DateUtil.parse(\"2019-10-05\");\n\n\t\tfinal long weekCount = DateUtil.betweenWeek(start, end, true);\n\t\tassertEquals(30L, weekCount);\n\t}\n\n\t@Test\n\tpublic void betweenDayTest() {\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tfinal String datr = RandomUtil.randomInt(1900, 2099) + \"-01-20\";\n\t\t\tfinal long betweenDay = DateUtil.betweenDay(\n\t\t\t\t\tDateUtil.parseDate(\"1970-01-01\"),\n\t\t\t\t\tDateUtil.parseDate(datr), false);\n\t\t\tassertEquals(Math.abs(LocalDate.parse(datr).toEpochDay()), betweenDay);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void issueI9CYHITest() {\n\t\tfinal long betweenDay = DateUtil.betweenDay(\n\t\t\tDateUtil.parse(\"2024-03-01\"),\n\t\t\tDateUtil.parse(\"2024-03-31\"), true);\n\n\t\tassertEquals(30, betweenDay);\n\t}\n\n\t@Test\n\tpublic void dayOfYearTest() {\n\t\tfinal int dayOfYear = DateUtil.dayOfYear(DateUtil.parse(\"2020-01-01\"));\n\t\tassertEquals(1, dayOfYear);\n\t\tfinal int lengthOfYear = DateUtil.lengthOfYear(2020);\n\t\tassertEquals(366, lengthOfYear);\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void parseSingleNumberTest() {\n\t\tDateTime dateTime = DateUtil.parse(\"2020-5-08\");\n\t\tassertEquals(\"2020-05-08 00:00:00\", dateTime.toString());\n\t\tdateTime = DateUtil.parse(\"2020-5-8\");\n\t\tassertEquals(\"2020-05-08 00:00:00\", dateTime.toString());\n\t\tdateTime = DateUtil.parse(\"2020-05-8\");\n\t\tassertEquals(\"2020-05-08 00:00:00\", dateTime.toString());\n\n\t\t//datetime\n\t\tdateTime = DateUtil.parse(\"2020-5-8 3:12:3\");\n\t\tassertEquals(\"2020-05-08 03:12:03\", dateTime.toString());\n\t\tdateTime = DateUtil.parse(\"2020-5-8 3:2:3\");\n\t\tassertEquals(\"2020-05-08 03:02:03\", dateTime.toString());\n\t\tdateTime = DateUtil.parse(\"2020-5-8 3:12:13\");\n\t\tassertEquals(\"2020-05-08 03:12:13\", dateTime.toString());\n\n\t\tdateTime = DateUtil.parse(\"2020-5-8 4:12:26.223\");\n\t\tassertEquals(\"2020-05-08 04:12:26\", dateTime.toString());\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void parseWithMilsTest() {\n\t\tfinal String dt = \"2020-06-03 12:32:12,333\";\n\t\tfinal DateTime parse = DateUtil.parse(dt);\n\t\tassertEquals(\"2020-06-03 12:32:12\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseNotFitTest() {\n\t\t//https://github.com/chinabugotech/hutool/issues/1332\n\t\tassertThrows(DateException.class, () -> {\n\t\t\t// 在日期格式不匹配的时候，测试是否正常报错\n\t\t\tDateUtil.parse(\"2020-12-23\", DatePattern.PURE_DATE_PATTERN);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void formatTest() {\n\t\tfinal Calendar calendar = new GregorianCalendar();\n\t\tcalendar.set(2021, Calendar.JULY, 14, 23, 59, 59);\n\t\tfinal Date date = new DateTime(calendar);\n\n\t\tassertEquals(\"2021-07-14 23:59:59\", DateUtil.format(date, DatePattern.NORM_DATETIME_FORMATTER));\n\t\tassertEquals(\"2021-07-14 23:59:59\", DateUtil.format(date, DatePattern.NORM_DATETIME_FORMAT));\n\t\tassertEquals(\"2021-07-14 23:59:59\", DateUtil.format(date, DatePattern.NORM_DATETIME_PATTERN));\n\t}\n\n\t@Test\n\tpublic void formatNormDateTimeFormatterTest() {\n\t\tString format = DateUtil.format(DateUtil.parse(\"2021-07-14 10:05:38\"), DatePattern.NORM_DATETIME_FORMATTER);\n\t\tassertEquals(\"2021-07-14 10:05:38\", format);\n\n\t\tformat = DateUtil.format(LocalDateTimeUtil.parse(\"2021-07-14T10:05:38\"),\n\t\t\t\t\"yyyy-MM-dd HH:mm:ss\");\n\t\tassertEquals(\"2021-07-14 10:05:38\", format);\n\t}\n\n\t@Test\n\tpublic void isWeekendTest() {\n\t\tDateTime parse = DateUtil.parse(\"2021-07-28\");\n\t\tassertFalse(DateUtil.isWeekend(parse));\n\n\t\tparse = DateUtil.parse(\"2021-07-25\");\n\t\tassertTrue(DateUtil.isWeekend(parse));\n\t\tparse = DateUtil.parse(\"2021-07-24\");\n\t\tassertTrue(DateUtil.isWeekend(parse));\n\t}\n\n\t@Test\n\tpublic void parseSingleMonthAndDayTest() {\n\t\tDateTime parse = DateUtil.parse(\"2021-1-1\");\n\t\tassertNotNull(parse);\n\t\tassertEquals(\"2021-01-01 00:00:00\", parse.toString());\n\n\t\tparse = DateUtil.parse(\"2021-1-22 00:00:00\");\n\t\tassertNotNull(parse);\n\t\tassertEquals(\"2021-01-22 00:00:00\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseByDateTimeFormatterTest() {\n\t\tfinal DateTime parse = DateUtil.parse(\"2021-12-01\", DatePattern.NORM_DATE_FORMATTER);\n\t\tassertEquals(\"2021-12-01 00:00:00\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void isSameWeekTest() {\n\t\t// 周六与周日比较\n\t\tfinal boolean isSameWeek = DateUtil.isSameWeek(DateTime.of(\"2022-01-01\", \"yyyy-MM-dd\"), DateTime.of(\"2022-01-02\", \"yyyy-MM-dd\"), true);\n\t\tassertTrue(isSameWeek);\n\t\t// 周日与周一比较\n\t\tfinal boolean isSameWeek1 = DateUtil.isSameWeek(DateTime.of(\"2022-01-02\", \"yyyy-MM-dd\"), DateTime.of(\"2022-01-03\", \"yyyy-MM-dd\"), false);\n\t\tassertTrue(isSameWeek1);\n\t\t// 跨月比较\n\t\tfinal boolean isSameWeek2 = DateUtil.isSameWeek(DateTime.of(\"2021-12-29\", \"yyyy-MM-dd\"), DateTime.of(\"2022-01-01\", \"yyyy-MM-dd\"), true);\n\t\tassertTrue(isSameWeek2);\n\t}\n\n\t@Test\n\tpublic void parseTimeTest(){\n\t\tfinal DateTime dateTime = DateUtil.parse(\"12:23:34\");\n\t\tConsole.log(dateTime);\n\t}\n\n\t@Test\n\t@SuppressWarnings(\"ConstantConditions\")\n\tpublic void isOverlapTest() {\n\t\tfinal DateTime oneStartTime = DateUtil.parse(\"2022-01-01 10:10:10\");\n\t\tfinal DateTime oneEndTime = DateUtil.parse(\"2022-01-01 11:10:10\");\n\n\t\tfinal DateTime oneStartTime2 = DateUtil.parse(\"2022-01-01 11:20:10\");\n\t\tfinal DateTime oneEndTime2 = DateUtil.parse(\"2022-01-01 11:30:10\");\n\n\t\tfinal DateTime oneStartTime3 = DateUtil.parse(\"2022-01-01 11:40:10\");\n\t\tfinal DateTime oneEndTime3 = DateUtil.parse(\"2022-01-01 11:50:10\");\n\n\t\t//真实请假数据\n\t\tfinal DateTime realStartTime = DateUtil.parse(\"2022-01-01 11:49:10\");\n\t\tfinal DateTime realEndTime = DateUtil.parse(\"2022-01-01 12:00:10\");\n\n\t\tfinal DateTime realStartTime1 = DateUtil.parse(\"2022-03-01 08:00:00\");\n\t\tfinal DateTime realEndTime1   = DateUtil.parse(\"2022-03-01 10:00:00\");\n\n\t\tfinal DateTime startTime  = DateUtil.parse(\"2022-03-23 05:00:00\");\n\t\tfinal DateTime endTime    = DateUtil.parse(\"2022-03-23 13:00:00\");\n\n\t\tassertFalse(DateUtil.isOverlap(oneStartTime, oneEndTime, realStartTime, realEndTime));\n\t\tassertFalse(DateUtil.isOverlap(oneStartTime2, oneEndTime2, realStartTime, realEndTime));\n\t\tassertTrue(DateUtil.isOverlap(oneStartTime3, oneEndTime3, realStartTime, realEndTime));\n\n\t\tassertFalse(DateUtil.isOverlap(realStartTime1,realEndTime1,startTime,endTime));\n\t\tassertFalse(DateUtil.isOverlap(startTime,endTime,realStartTime1,realEndTime1));\n\n\t\tassertTrue(DateUtil.isOverlap(startTime,startTime,startTime,startTime));\n\t\tassertTrue(DateUtil.isOverlap(startTime,startTime,startTime,endTime));\n\t\tassertFalse(DateUtil.isOverlap(startTime,startTime,endTime,endTime));\n\t\tassertTrue(DateUtil.isOverlap(startTime,endTime,endTime,endTime));\n\t}\n\n\t@Test\n\tpublic void isOverlapTest2() {\n\t\tfinal DateTime oneStartTime = DateUtil.parseDate(\"2021-02-01\");\n\t\tfinal DateTime oneEndTime = DateUtil.parseDate(\"2022-06-30\");\n\n\t\tfinal DateTime oneStartTime2 = DateUtil.parseDate(\"2019-04-05\");\n\t\tfinal DateTime oneEndTime2 = DateUtil.parseDate(\"2021-04-05\");\n\n\t\tassertTrue(DateUtil.isOverlap(oneStartTime, oneEndTime, oneStartTime2, oneEndTime2));\n\t}\n\n\t@Test\n\tpublic void isInTest(){\n\t\tfinal String sourceStr = \"2022-04-19 00:00:00\";\n\t\tfinal String startTimeStr = \"2022-04-19 00:00:00\";\n\t\tfinal String endTimeStr = \"2022-04-19 23:59:59\";\n\t\tfinal boolean between = DateUtil.isIn(DateUtil.parse(startTimeStr),\n\t\t\t\tDateUtil.parse(endTimeStr),\n\t\t\t\tDateUtil.parse(sourceStr));\n\t\tassertTrue(between);\n\t}\n\n\t@Test\n\tpublic void isLastDayTest() {\n\t\tfinal DateTime dateTime = DateUtil.parse(\"2022-09-30\");\n\t\tassertNotNull(dateTime);\n\t\tfinal int dayOfMonth = DateUtil.getLastDayOfMonth(dateTime);\n\t\tassertEquals(dayOfMonth, dateTime.dayOfMonth());\n\t\tassertTrue(DateUtil.isLastDayOfMonth(dateTime));\n\t}\n\n\n\t/**\n\t * issue#2887 由于UTC时间的毫秒部分超出3位导致的秒数增加的问题\n\t */\n\t@Test\n\tpublic void parseUTCTest4() {\n\t\tfinal String dateStr = \"2023-02-07T00:02:16.12345+08:00\";\n\t\tfinal DateTime dateTime = DateUtil.parse(dateStr);\n\t\tassertNotNull(dateTime);\n\t\tassertEquals(\"2023-02-07 00:02:16\", dateTime.toString());\n\n\t\tfinal String dateStr2 = \"2023-02-07T00:02:16.12345-08:00\";\n\t\tfinal DateTime dateTime2 = DateUtil.parse(dateStr2);\n\t\tassertNotNull(dateTime2);\n\t\tassertEquals(\"2023-02-07 00:02:16\", dateTime2.toString());\n\n\t\tfinal String dateStr3 = \"2021-03-17T06:31:33.9999\";\n\t\tfinal DateTime dateTime3 = DateUtil.parse(dateStr3);\n\t\tassertNotNull(dateTime3);\n\t\tassertEquals(\"2021-03-17 06:31:33\", dateTime3.toString());\n\t}\n\n\t@Test\n\tpublic void calendarTest() {\n\t\tfinal Date date = DateUtil.date();\n\t\tfinal Calendar c = DateUtil.calendar(date);\n\t\tassertEquals(DateUtil.date(c), date);\n\t}\n\n\t@Test\n\tpublic void issueI7H34NTest() {\n\t\tfinal DateTime parse = DateUtil.parse(\"2019-10-22T09:56:03.000123Z\");\n\t\tassertNotNull(parse);\n\t\tassertEquals(\"2019-10-22 09:56:03\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void issueI8NMP7Test() {\n\t\tfinal String str = \"1702262524444\";\n\t\tfinal DateTime parse = DateUtil.parse(str);\n\t\tassertEquals(\"2023-12-11 10:42:04\", Objects.requireNonNull(parse).toString());\n\t}\n\n\t@Test\n\tpublic void formatSpeedTest(){\n\t\tDate value = new Date();\n\t\t//long t0 = System.currentTimeMillis();\n\t\t//此处先加载FastDateFormat对象，保存到FastDateFormat.CACHE中\n\t\t//解决后面两个for循环中保存到FastDateFormat对象创建未时差异的问题。\n\t\tFastDateFormat.getInstance(\"YYYY-MM-dd HH:mm:ss.SSS\");\n\n\t\tlong t1 = System.currentTimeMillis();\n\t\tString strTime = null;\n\t\tfor(int i=0; i<50000; i++){\n\t\t\tstrTime = DateUtil.format(value, \"YYYY-MM-dd HH:mm:ss.SSS\");\n\t\t}\n\t\tassertNotNull(strTime);\n\t\tlong t2 = System.currentTimeMillis();\n\n\t\tfor(int i=0; i<50000; i++){\n\t\t\tstrTime = FastDateFormat.getInstance(\"YYYY-MM-dd HH:mm:ss.SSS\").format(value);\n\t\t}\n\t\tassertNotNull(strTime);\n\t\tlong t3 = System.currentTimeMillis();\n\n\t\t//long initTime = t1 - t0;\n\t\tlong formtTime1 = t2 - t1;\n\t\tlong formatTime2 = t3 - t2;\n\n\t\t//此处仍然不明白，两个for循环实际执行format方法都一样，为什么第1个for时间大致是第2个for的3倍。\n\t\tassertTrue(formtTime1 > formatTime2);\n\n\t\t/*\n\t\t * \t\tSystem.out.println(\"t1-t0=\"+(t1-t0));\n\t\t * \t\tSystem.out.println(\"t2-t1=\"+(t2-t1));\n\t\t * \t\tSystem.out.println(\"t3-t2=\"+(t3-t2));\n\t\t *\n\t\t * 由日志可以看出，第1个for时间大致是第2个for的3倍\n\t\t *\n\t\t * t1-t0=46\n\t\t * t2-t1=65\n\t\t * t3-t2=25\n\t\t */\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/FastDateFormatTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.format.FastDateFormat;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.TimeZone;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class FastDateFormatTest {\n\tprivate static final TimeZone timezone = TimeZone.getTimeZone(\"Etc/Utc\");\n\n\tprivate static FastDateFormat getHutoolInstance(String pattern) {\n\t\treturn FastDateFormat.getInstance(pattern, timezone);\n\t}\n\n\t@Test\n\tpublic void yearTest() {\n\t\tDate date = DateUtil.date(0L);\n\n\t\tassertEquals(\n\t\t\t\"1970-01-01 00:00:00\",\n\t\t\tgetHutoolInstance(\"yyyy-MM-dd HH:mm:ss\").format(date)\n\t\t);\n\n\t\tassertEquals(\n\t\t\t\"1970-01-01 00:00:00\",\n\t\t\tgetHutoolInstance(\"YYYY-MM-dd HH:mm:ss\").format(date)\n\t\t);\n\n\t\tassertEquals(\n\t\t\t\"1970\",\n\t\t\tgetHutoolInstance(\"YYYY\").format(date)\n\t\t);\n\n\t\tassertEquals(\n\t\t\t\"70\",\n\t\t\tgetHutoolInstance(\"yy\").format(date)\n\t\t);\n\t}\n\n\t@SuppressWarnings(\"SuspiciousDateFormat\")\n\t@Test\n\tpublic void weekYearTest() {\n\t\tDate date = DateUtil.date(0L);\n\n\t\tassertEquals(\n\t\t\t\"70\",\n\t\t\tnew SimpleDateFormat(\"YY\").format(date)\n\t\t);\n\n\t\tassertEquals(\n\t\t\t\"70\",\n\t\t\tgetHutoolInstance(\"YY\").format(date)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/GanzhiTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.date.chinese.GanZhi;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class GanzhiTest {\n\n\t@Test\n\tpublic void getGanzhiOfYearTest(){\n\t\tassertEquals(\"庚子\", GanZhi.getGanzhiOfYear(2020));\n\t}\n\n\t@Test\n\tpublic void getCyclicalYMDTest(){\n\t\t//通过公历构建\n\t\tChineseDate chineseDate = new ChineseDate(DateUtil.parseDate(\"1993-01-06\"));\n\t\tString cyclicalYMD = chineseDate.getCyclicalYMD();\n\t\tassertEquals(\"壬申年癸丑月丁亥日\",cyclicalYMD);\n\t}\n\n\t@Test\n\tpublic void getCyclicalYMDTest2(){\n\t\t//通过农历构建\n\t\tChineseDate chineseDate = new ChineseDate(1992,12,14);\n\t\tString cyclicalYMD = chineseDate.getCyclicalYMD();\n\t\tassertEquals(\"壬申年癸丑月丁亥日\",cyclicalYMD);\n\t}\n\n\t@Test\n\tpublic void getCyclicalYMDTest3(){\n\t\t//通过公历构建\n\t\tChineseDate chineseDate = new ChineseDate(DateUtil.parseDate(\"2020-08-28\"));\n\t\tString cyclicalYMD = chineseDate.getCyclicalYMD();\n\t\tassertEquals(\"庚子年甲申月癸卯日\",cyclicalYMD);\n\t}\n\n\t@Test\n\tpublic void getCyclicalYMDTest4(){\n\t\t//通过公历构建\n\t\tChineseDate chineseDate = new ChineseDate(DateUtil.parseDate(\"1905-08-28\"));\n\t\tString cyclicalYMD = chineseDate.getCyclicalYMD();\n\t\tassertEquals(\"乙巳年甲申月己亥日\",cyclicalYMD);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue2612Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Objects;\n\npublic class Issue2612Test {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tassertEquals(\"2022-09-14 23:59:00\",\n\t\t\t\tObjects.requireNonNull(DateUtil.parse(\"2022-09-14T23:59:00-08:00\")).toString());\n\n\t\tassertEquals(\"2022-09-14 23:59:00\",\n\t\t\t\tObjects.requireNonNull(DateUtil.parse(\"2022-09-14T23:59:00-0800\")).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue2981Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2981Test {\n\t/**\n\t * https://github.com/chinabugotech/hutool/issues/2981<br>\n\t * 按照ISO8601规范，以Z结尾表示UTC时间，否则为当地时间\n\t */\n\t@SuppressWarnings(\"DataFlowIssue\")\n\t@Test\n\tpublic void parseUTCTest() {\n\t\tfinal String str1 = \"2019-01-01T00:00:00.000Z\";\n\t\tfinal String str2 = \"2019-01-01T00:00:00.000\";\n\t\tfinal String str3 = \"2019-01-01 00:00:00.000\";\n\n\t\tassertEquals(1546300800000L, DateUtil.parse(str1).getTime());\n\t\tassertEquals(1546272000000L, DateUtil.parse(str2).getTime());\n\t\tassertEquals(1546272000000L, DateUtil.parse(str3).getTime());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3011Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Calendar;\n\npublic class Issue3011Test {\n\t@Test\n\tpublic void isSameMonthTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/3011\n\t\t// 判断是否同一个月，还需考虑公元前和公元后的的情况\n\t\t// 此处公元前2020年和公元2021年返回年都是2021\n\t\tfinal Calendar calendar1 = Calendar.getInstance();\n\t\tcalendar1.set(-2020, Calendar.FEBRUARY, 12);\n\n\t\tfinal Calendar calendar2 = Calendar.getInstance();\n\t\tcalendar2.set(2021, Calendar.FEBRUARY, 12);\n\n\n\t\tassertFalse(DateUtil.isSameMonth(calendar1, calendar2));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3036Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class Issue3036Test {\n\t@Test\n\tpublic void getZodiacTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tDateUtil.getZodiac(Month.UNDECIMBER.getValue(), 10);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3301Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.TemporalAccessor;\n\npublic class Issue3301Test {\n\t@Test\n\tpublic void ofTest() {\n\t\tfinal ZonedDateTime now = ZonedDateTime.now();\n\t\t// 获得一个特殊的 temporal\n\t\tString text = DateTimeFormatter.ISO_INSTANT.format(now);\n\t\tTemporalAccessor temporal = DateTimeFormatter.ISO_INSTANT.parse(text);\n\n\t\tLocalDateTime actual = LocalDateTimeUtil.of(temporal);\n\t\tassertEquals(now.toLocalDateTime().toString(), actual.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3348Test.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3348Test {\n\n\t@Test\n\tpublic void formatChineseDateTest() {\n\t\tfinal String formatChineseDate = DateUtil.formatChineseDate(\n\t\t\tDateUtil.parse(\"2023-10-23\"), true, false);\n\t\tConsole.log(formatChineseDate);\n\t\tassertEquals(\"二〇二三年十月二十三日\", formatChineseDate);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3608Test.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.TimeZone;\n\npublic class Issue3608Test {\n\t@Test\n\t@Disabled\n\tpublic void parseTest() throws ParseException {\n\t\tString dateTime = \"1940-06-01 00:00:00\";\n\t\tfinal SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n\t\tsimpleDateFormat.setTimeZone(TimeZone.getTimeZone(\"UTC\"));\n\t\tfinal Date parse = simpleDateFormat.parse(dateTime);\n\t\tConsole.log(parse);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/Issue3798Test.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.TimeZone;\n\npublic class Issue3798Test {\n\t@Test\n\tvoid parseTest() {\n\t\tfinal String iso_datetime1 = \"2000-01-01T12:00:00+08:00\";\n\t\tfinal DateTime parse1 = DateUtil.parse(iso_datetime1);\n\t\tAssertions.assertEquals(TimeZone.getTimeZone(\"GMT+08:00\"), parse1.getTimeZone());\n\t\tAssertions.assertEquals(\"2000-01-01 12:00:00\", parse1.toString());\n\n\t\t// 伦敦时间（Greenwich Mean Time, GMT）和北京时间（China Standard Time, CST）之间的时差是8小时。北京时间比伦敦时间快8小时\n\t\tfinal String iso_datetime2 = \"2000-01-01T12:00:00+00:00\";\n\t\tfinal DateTime parse2 = DateUtil.parse(iso_datetime2);\n\t\tAssertions.assertEquals(TimeZone.getTimeZone(\"GMT+00:00\"), parse2.getTimeZone());\n\t\tAssertions.assertEquals(\"2000-01-01 20:00:00\", parse2.toString(TimeZone.getTimeZone(\"GMT+08:00\")));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueI7QI6RTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class IssueI7QI6RTest {\n\t@Test\n\tpublic void parseTest() {\n\t\tassertThrows(DateException.class, () -> {\n\t\t\tDateUtil.parse(\"2023-08-04 1\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tassertThrows(DateException.class, () -> {\n\t\t\tDateUtil.parse(\"2023-08-04-1\");\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueI7XMYWTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI7XMYWTest {\n\t@Test\n\tpublic void ageTest() {\n\t\tDateTime date1 = DateUtil.parse(\"2023-08-31\");\n\t\tassertEquals(49, DateUtil.age(DateUtil.parse(\"1973-08-31\"), date1));\n\n\t\tdate1 = DateUtil.parse(\"2023-08-30\");\n\t\tassertEquals(49, DateUtil.age(DateUtil.parse(\"1973-08-30\"), date1));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueI82Y1LTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI82Y1LTest {\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal String dt1 = \"2023-09-14T05:00:03.648519Z\";\n\t\tassertEquals(\"2023-09-14 05:10:51\", DateUtil.parse(dt1).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueI97WU6Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI97WU6Test {\n\t@Test\n\tpublic void getTermTest() {\n\t\t// 润十月没有三十，十月有三十\n\t\tfinal ChineseDate chineseDate = new ChineseDate(1984, 10, 30, false);\n\t\tassertEquals(\"甲子鼠年 寒月三十\", chineseDate.toString());\n\t\tassertEquals(\"小雪\", chineseDate.getTerm());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueI9C2D4Test.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI9C2D4Test {\n\t@Test\n\tpublic void parseHttpTest() {\n\t\tString dateStr = \"Thu, 28 Mar 2024 14:33:49 GMT\";\n\t\tfinal DateTime parse = DateUtil.parse(dateStr);\n\t\tassertEquals(\"2024-03-28 14:33:49\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseHttpTest2() {\n\t\tString dateStr = \"星期四, 28 三月 2024 14:33:49 GMT\";\n\t\tfinal DateTime parse = DateUtil.parse(dateStr);\n\t\tassertEquals(\"2024-03-28 14:33:49\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseTimeTest() {\n\t\tString dateStr = \"15时45分59秒\";\n\t\tfinal DateTime parse = DateUtil.parse(dateStr);\n\t\tassertEquals(\"15:45:59\", parse.toString().split(\" \")[1]);\n\t}\n\n\t@Test\n\tpublic void parseTimeTest2() {\n\t\tString dateStr = \"15:45:59\";\n\t\tfinal DateTime parse = DateUtil.parse(dateStr);\n\t\tassertEquals(\"15:45:59\", parse.toString().split(\" \")[1]);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueIB8OFSTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class IssueIB8OFSTest {\n\t@Test\n\tvoid rangeTest() {\n\t\tDateRange startRange = DateUtil.range(\n\t\t\tDateUtil.parse(\"2017-01-01\"),\n\t\t\tDateUtil.parse(\"2017-01-31\"), DateField.DAY_OF_YEAR);\n\t\tDateRange endRange = DateUtil.range(\n\t\t\tDateUtil.parse(\"2017-01-31\"),\n\t\t\tDateUtil.parse(\"2017-02-02\"), DateField.DAY_OF_YEAR);\n\n\t\tList<DateTime> dateTimes = DateUtil.rangeContains(startRange, endRange);\n\t\tAssertions.assertEquals(1, dateTimes.size());\n\n\t\tList<DateTime> dateNotTimes = DateUtil.rangeNotContains(startRange, endRange);\n\t\tAssertions.assertEquals(2, dateNotTimes.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueIB9NPUTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.text.SimpleDateFormat;\n\npublic class IssueIB9NPUTest {\n\t@Test\n\tvoid parseTest() {\n\t\tDateUtil.parse(\"202409032400\", new SimpleDateFormat(\"yyyyMMddHHmm\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueIBB6I5Test.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.ZoneId;\nimport java.util.TimeZone;\n\npublic class IssueIBB6I5Test {\n\t@Test\n\tvoid parseISO8601Test() {\n\t\tDateTime date = DateUtil.parseISO8601(\"2024-12-13T08:02:27Z\");\n\t\tTimeZone timeZone = TimeZone.getTimeZone(ZoneId.of(\"Asia/Shanghai\"));\n\t\tdate.setTimeZone(timeZone);\n\t\tAssertions.assertEquals(\"2024-12-13 16:02:27\", date.toString());\n\t}\n\n\t@Test\n\tvoid parseISO8601Test2() {\n\t\tDateTime date = DateUtil.parseISO8601(\"2024-12-13T08:02:27\");\n\t\tTimeZone timeZone = TimeZone.getTimeZone(ZoneId.of(\"Asia/Shanghai\"));\n\t\tdate.setTimeZone(timeZone);\n\t\tAssertions.assertEquals(\"2024-12-13 08:02:27\", date.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueIC00HGTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIC00HGTest {\n\t@Test\n\t@Disabled\n\tvoid dateToStringTest(){\n\t\tConsole.log(DateUtil.date().toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/IssueIDFMXJTest.java",
    "content": "package cn.hutool.core.date;\n\n\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Field;\n\n/**\n * StopWatch 异常未释放资源问题测试（问题2）\n */\npublic class IssueIDFMXJTest {\n\t@Test\n\tvoid stopWatchNegativeTimeTest() throws NoSuchFieldException, IllegalAccessException {\n\t\tfinal StopWatch stopWatch = new StopWatch();\n\t\tstopWatch.start(\"负耗时测试任务\");\n\n\t\t// 反射修改startTimeNanos为当前时间+1秒（模拟nanoTime回退）\n\t\tfinal Field startTimeNanosField = StopWatch.class.getDeclaredField(\"startTimeNanos\");\n\t\tstartTimeNanosField.setAccessible(true);\n\t\tstartTimeNanosField.set(stopWatch, System.nanoTime() + 1_000_000_000);\n\n\t\tstopWatch.stop();\n\n\t\tAssertions.assertEquals(0, stopWatch.getLastTaskTimeNanos());\n\t\tAssertions.assertEquals(0, stopWatch.getTotalTimeNanos());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/LocalDateTimeUtilTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoUnit;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Objects;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class LocalDateTimeUtilTest {\n\n\t@Test\n\tpublic void nowTest() {\n\t\tassertNotNull(LocalDateTimeUtil.now());\n\t}\n\n\t@Test\n\tpublic void ofTest() {\n\t\tfinal String dateStr = \"2020-01-23T12:23:56\";\n\t\tfinal DateTime dt = DateUtil.parse(dateStr);\n\n\t\tLocalDateTime of = LocalDateTimeUtil.of(dt);\n\t\tassertNotNull(of);\n\t\tassertEquals(dateStr, of.toString());\n\n\t\t// 不加Z是标准当地时间，与UTC时间不同\n\t\tof = LocalDateTimeUtil.ofUTC(dt.getTime());\n\t\tassertNotEquals(dateStr, of.toString());\n\t}\n\n\t@Test\n\tpublic void parseOffsetTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2021-07-30T16:27:27+08:00\", DateTimeFormatter.ISO_OFFSET_DATE_TIME);\n\t\tassertEquals(\"2021-07-30T16:27:27\", Objects.requireNonNull(localDateTime).toString());\n\t}\n\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\", DateTimeFormatter.ISO_DATE_TIME);\n\t\tassertEquals(\"2020-01-23T12:23:56\", Objects.requireNonNull(localDateTime).toString());\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23\", DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"2020-01-23T00:00\", Objects.requireNonNull(localDateTime).toString());\n\t}\n\n\t@Test\n\tpublic void parseTest3() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"12:23:56\", DatePattern.NORM_TIME_PATTERN);\n\t\tassertEquals(\"12:23:56\", Objects.requireNonNull(localDateTime).toLocalTime().toString());\n\t}\n\n\t@Test\n\tpublic void parseTest4() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\");\n\t\tassertEquals(\"2020-01-23T12:23:56\", localDateTime.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest5() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"19940121183604\", \"yyyyMMddHHmmss\");\n\t\tassertEquals(\"1994-01-21T18:36:04\", Objects.requireNonNull(localDateTime).toString());\n\t}\n\n\t@Test\n\tpublic void parseTest6() {\n\t\tLocalDateTime localDateTime = LocalDateTimeUtil.parse(\"19940121183604682\", \"yyyyMMddHHmmssSSS\");\n\t\tassertEquals(\"1994-01-21T18:36:04.682\", Objects.requireNonNull(localDateTime).toString());\n\n\t\tlocalDateTime = LocalDateTimeUtil.parse(\"1994012118360468\", \"yyyyMMddHHmmssSS\");\n\t\tassertEquals(\"1994-01-21T18:36:04.680\", Objects.requireNonNull(localDateTime).toString());\n\n\t\tlocalDateTime = LocalDateTimeUtil.parse(\"199401211836046\", \"yyyyMMddHHmmssS\");\n\t\tassertEquals(\"1994-01-21T18:36:04.600\", Objects.requireNonNull(localDateTime).toString());\n\t}\n\n\t@Test\n\tpublic void parseDateTest() {\n\t\tLocalDate localDate = LocalDateTimeUtil.parseDate(\"2020-01-23\");\n\t\tassertEquals(\"2020-01-23\", localDate.toString());\n\n\t\tlocalDate = LocalDateTimeUtil.parseDate(\"2020-01-23T12:23:56\", DateTimeFormatter.ISO_DATE_TIME);\n\t\tassertNotNull(localDate);\n\t\tassertEquals(\"2020-01-23\", localDate.toString());\n\t}\n\n\t@Test\n\tpublic void parseSingleMonthAndDayTest() {\n\t\tfinal LocalDate localDate = LocalDateTimeUtil.parseDate(\"2020-1-1\", \"yyyy-M-d\");\n\t\tassertEquals(\"2020-01-01\", localDate.toString());\n\t}\n\n\t@Test\n\tpublic void formatTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\");\n\t\tString format = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATETIME_PATTERN);\n\t\tassertEquals(\"2020-01-23 12:23:56\", format);\n\n\t\tformat = LocalDateTimeUtil.formatNormal(localDateTime);\n\t\tassertEquals(\"2020-01-23 12:23:56\", format);\n\n\t\tformat = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"2020-01-23\", format);\n\t}\n\n\t@Test\n\tpublic void formatLocalDateTest() {\n\t\tfinal LocalDate date = LocalDate.parse(\"2020-01-23\");\n\t\tString format = LocalDateTimeUtil.format(date, DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"2020-01-23\", format);\n\n\t\tformat = LocalDateTimeUtil.formatNormal(date);\n\t\tassertEquals(\"2020-01-23\", format);\n\t}\n\n\t@Test\n\tpublic void offset() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\");\n\t\tLocalDateTime offset = LocalDateTimeUtil.offset(localDateTime, 1, ChronoUnit.DAYS);\n\t\t// 非同一对象\n\t\tassertNotSame(localDateTime, offset);\n\n\t\tassertEquals(\"2020-01-24T12:23:56\", offset.toString());\n\n\t\toffset = LocalDateTimeUtil.offset(localDateTime, -1, ChronoUnit.DAYS);\n\t\tassertEquals(\"2020-01-22T12:23:56\", offset.toString());\n\t}\n\n\t@Test\n\tpublic void ofTest2(){\n\t\tfinal Instant instant = Objects.requireNonNull(DateUtil.parse(\"2022-02-22\")).toInstant();\n\t\tfinal LocalDateTime of = LocalDateTimeUtil.of((TemporalAccessor) instant);\n\t\tassertEquals(\"2022-02-22T00:00\", of.toString());\n\t}\n\n\t@Test\n\tpublic void between() {\n\t\tfinal Duration between = LocalDateTimeUtil.between(\n\t\t\t\tLocalDateTimeUtil.parse(\"2019-02-02T00:00:00\"),\n\t\t\t\tLocalDateTimeUtil.parse(\"2020-02-02T00:00:00\"));\n\t\tassertEquals(365, between.toDays());\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void isIn() {\n\t\t// 时间范围 8点-9点\n\t\tfinal LocalDateTime begin = LocalDateTime.parse(\"2019-02-02T08:00:00\");\n\t\tfinal LocalDateTime end = LocalDateTime.parse(\"2019-02-02T09:00:00\");\n\n\t\t// 不在时间范围内 用例\n\t\tassertFalse(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T06:00:00\"), begin, end));\n\t\tassertFalse(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T13:00:00\"), begin, end));\n\t\tassertFalse(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-01T08:00:00\"), begin, end));\n\t\tassertFalse(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-03T09:00:00\"), begin, end));\n\n\t\t// 在时间范围内 用例\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T08:00:00\"), begin, end));\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T08:00:01\"), begin, end));\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T08:11:00\"), begin, end));\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T08:22:00\"), begin, end));\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T08:59:59\"), begin, end));\n\t\tassertTrue(LocalDateTimeUtil.isIn(LocalDateTime.parse(\"2019-02-02T09:00:00\"), begin, end));\n\n\t\t// 测试边界条件\n\t\tassertTrue(LocalDateTimeUtil.isIn(begin, begin, end, true, false));\n\t\tassertFalse(LocalDateTimeUtil.isIn(begin, begin, end, false, false));\n\t\tassertTrue(LocalDateTimeUtil.isIn(end, begin, end, false, true));\n\t\tassertFalse(LocalDateTimeUtil.isIn(end, begin, end, false, false));\n\n\t\t// begin、end互换\n\t\tassertTrue(LocalDateTimeUtil.isIn(begin, end, begin, true, true));\n\n\t\t// 比较当前时间范围\n\t\tfinal LocalDateTime now = LocalDateTime.now();\n\t\tassertTrue(LocalDateTimeUtil.isIn(now, now.minusHours(1L), now.plusHours(1L)));\n\t\tassertFalse(LocalDateTimeUtil.isIn(now, now.minusHours(1L), now.minusHours(2L)));\n\t\tassertFalse(LocalDateTimeUtil.isIn(now, now.plusHours(1L), now.plusHours(2L)));\n\n\t\t// 异常入参\n\t\tassertThrows(IllegalArgumentException.class, () -> LocalDateTimeUtil.isIn(null, begin, end, false, false));\n\t\tassertThrows(IllegalArgumentException.class, () -> LocalDateTimeUtil.isIn(begin, null, end, false, false));\n\t\tassertThrows(IllegalArgumentException.class, () -> LocalDateTimeUtil.isIn(begin, begin, null, false, false));\n\t}\n\n\t@Test\n\tpublic void beginOfDayTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\");\n\t\tfinal LocalDateTime beginOfDay = LocalDateTimeUtil.beginOfDay(localDateTime);\n\t\tassertEquals(\"2020-01-23T00:00\", beginOfDay.toString());\n\t}\n\n\t@Test\n\tpublic void endOfDayTest() {\n\t\tfinal LocalDateTime localDateTime = LocalDateTimeUtil.parse(\"2020-01-23T12:23:56\");\n\n\t\tLocalDateTime endOfDay = LocalDateTimeUtil.endOfDay(localDateTime);\n\t\tassertEquals(\"2020-01-23T23:59:59.999999999\", endOfDay.toString());\n\n\t\tendOfDay = LocalDateTimeUtil.endOfDay(localDateTime, true);\n\t\tassertEquals(\"2020-01-23T23:59:59\", endOfDay.toString());\n\t}\n\n\t@Test\n\tpublic void dayOfWeekTest() {\n\t\tfinal Week one = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 20));\n\t\tassertEquals(Week.MONDAY, one);\n\n\t\tfinal Week two = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 21));\n\t\tassertEquals(Week.TUESDAY, two);\n\n\t\tfinal Week three = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 22));\n\t\tassertEquals(Week.WEDNESDAY, three);\n\n\t\tfinal Week four = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 23));\n\t\tassertEquals(Week.THURSDAY, four);\n\n\t\tfinal Week five = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 24));\n\t\tassertEquals(Week.FRIDAY, five);\n\n\t\tfinal Week six = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 25));\n\t\tassertEquals(Week.SATURDAY, six);\n\n\t\tfinal Week seven = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 26));\n\t\tassertEquals(Week.SUNDAY, seven);\n\t}\n\n\t@Test\n\tpublic void isOverlapTest(){\n\t\tfinal LocalDateTime oneStartTime = LocalDateTime.of(2022, 1, 1, 10, 10, 10);\n\t\tfinal LocalDateTime oneEndTime = LocalDateTime.of(2022, 1, 1, 11, 10, 10);\n\n\t\tfinal LocalDateTime oneStartTime2 = LocalDateTime.of(2022, 1, 1, 11, 20, 10);\n\t\tfinal LocalDateTime oneEndTime2 = LocalDateTime.of(2022, 1, 1, 11, 30, 10);\n\n\t\tfinal LocalDateTime oneStartTime3 = LocalDateTime.of(2022, 1, 1, 11, 40, 10);\n\t\tfinal LocalDateTime oneEndTime3 = LocalDateTime.of(2022, 1, 1, 11, 50, 10);\n\n\t\t//真实请假数据\n\t\tfinal LocalDateTime realStartTime = LocalDateTime.of(2022, 1, 1, 11, 49, 10);\n\t\tfinal LocalDateTime realEndTime = LocalDateTime.of(2022, 1, 1, 12, 0, 10);\n\n\t\tfinal LocalDateTime realStartTime1 = DateUtil.parseLocalDateTime(\"2022-03-01 08:00:00\");\n\t\tfinal LocalDateTime realEndTime1   = DateUtil.parseLocalDateTime(\"2022-03-01 10:00:00\");\n\n\t\tfinal LocalDateTime startTime  = DateUtil.parseLocalDateTime(\"2022-03-23 05:00:00\");\n\t\tfinal LocalDateTime endTime    = DateUtil.parseLocalDateTime(\"2022-03-23 13:00:00\");\n\n\n\n\t\tassertFalse(LocalDateTimeUtil.isOverlap(oneStartTime,oneEndTime,realStartTime,realEndTime));\n\t\tassertFalse(LocalDateTimeUtil.isOverlap(oneStartTime2,oneEndTime2,realStartTime,realEndTime));\n\t\tassertTrue(LocalDateTimeUtil.isOverlap(oneStartTime3,oneEndTime3,realStartTime,realEndTime));\n\n\t\tassertFalse(LocalDateTimeUtil.isOverlap(realStartTime1,realEndTime1,startTime,endTime));\n\t\tassertFalse(LocalDateTimeUtil.isOverlap(startTime,endTime,realStartTime1,realEndTime1));\n\n\t\tassertTrue(LocalDateTimeUtil.isOverlap(startTime,startTime,startTime,startTime));\n\t\tassertTrue(LocalDateTimeUtil.isOverlap(startTime,startTime,startTime,endTime));\n\t\tassertFalse(LocalDateTimeUtil.isOverlap(startTime,startTime,endTime,endTime));\n\t\tassertTrue(LocalDateTimeUtil.isOverlap(startTime,endTime,endTime,endTime));\n\t}\n\n\t@Test\n\tpublic void weekOfYearTest(){\n\t\tfinal LocalDate date1 = LocalDate.of(2021, 12, 31);\n\t\tfinal int weekOfYear1 = LocalDateTimeUtil.weekOfYear(date1);\n\t\tassertEquals(52, weekOfYear1);\n\n\t\tfinal int weekOfYear2 = LocalDateTimeUtil.weekOfYear(date1.atStartOfDay());\n\t\tassertEquals(52, weekOfYear2);\n\t}\n\n\t@Test\n\tpublic void weekOfYearTest2(){\n\t\tfinal LocalDate date1 = LocalDate.of(2022, 1, 31);\n\t\tfinal int weekOfYear1 = LocalDateTimeUtil.weekOfYear(date1);\n\t\tassertEquals(5, weekOfYear1);\n\n\t\tfinal int weekOfYear2 = LocalDateTimeUtil.weekOfYear(date1.atStartOfDay());\n\t\tassertEquals(5, weekOfYear2);\n\t}\n\n\t@Test\n\tpublic void parseBlankTest(){\n\t\tfinal LocalDateTime parse = LocalDateTimeUtil.parse(\"\");\n\t\tassertNull(parse);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/MonthTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.format.TextStyle;\nimport java.util.Calendar;\nimport java.util.Locale;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class MonthTest {\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void getLastDayTest(){\n\t\tint lastDay = Month.of(Calendar.JANUARY).getLastDay(false);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.FEBRUARY).getLastDay(false);\n\t\tassertEquals(28, lastDay);\n\t\tlastDay = Month.of(Calendar.FEBRUARY).getLastDay(true);\n\t\tassertEquals(29, lastDay);\n\t\tlastDay = Month.of(Calendar.MARCH).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.APRIL).getLastDay(true);\n\t\tassertEquals(30, lastDay);\n\t\tlastDay = Month.of(Calendar.MAY).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.JUNE).getLastDay(true);\n\t\tassertEquals(30, lastDay);\n\t\tlastDay = Month.of(Calendar.JULY).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.AUGUST).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.SEPTEMBER).getLastDay(true);\n\t\tassertEquals(30, lastDay);\n\t\tlastDay = Month.of(Calendar.OCTOBER).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t\tlastDay = Month.of(Calendar.NOVEMBER).getLastDay(true);\n\t\tassertEquals(30, lastDay);\n\t\tlastDay = Month.of(Calendar.DECEMBER).getLastDay(true);\n\t\tassertEquals(31, lastDay);\n\t}\n\n\t@Test\n\tpublic void toJdkMonthTest(){\n\t\tfinal java.time.Month month = Month.AUGUST.toJdkMonth();\n\t\tassertEquals(java.time.Month.AUGUST, month);\n\t}\n\n\t@Test\n\tpublic void toJdkMonthTest2(){\n\t\tassertThrows(IllegalArgumentException.class, Month.UNDECIMBER::toJdkMonth);\n\t}\n\n\t@Test\n\tpublic void ofTest(){\n\t\tMonth month = Month.of(\"Jan\");\n\t\tassertEquals(Month.JANUARY, month);\n\n\t\tmonth = Month.of(\"JAN\");\n\t\tassertEquals(Month.JANUARY, month);\n\n\t\tmonth = Month.of(\"FEBRUARY\");\n\t\tassertEquals(Month.FEBRUARY, month);\n\n\t\tmonth = Month.of(\"February\");\n\t\tassertEquals(Month.FEBRUARY, month);\n\n\t\tmonth = Month.of(java.time.Month.FEBRUARY);\n\t\tassertEquals(Month.FEBRUARY, month);\n\t}\n\n\t@Test\n\tvoid getDisplayNameTest() {\n\t\tfinal String displayName = Month.FEBRUARY.getDisplayName(TextStyle.SHORT, Locale.US);\n\t\tAssertions.assertEquals(\"Feb\", displayName);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/QuarterTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.time.MonthDay;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class QuarterTest {\n\n\t@Test\n\tvoid testQ1() {\n\t\tQuarter quarter = Quarter.of(1);\n\t\tassertSame(Quarter.Q1, quarter);\n\t\tassertSame(quarter, Quarter.valueOf(\"Q1\"));\n\t\tassertEquals(1, quarter.getValue());\n\t\tassertEquals(\"Q1\", quarter.name());\n\n\t\tassertNull(Quarter.of(0));\n\n\t\t// ==========\n\n\t\tint firstMonthValue = quarter.firstMonthValue();\n\t\tassertEquals(1, firstMonthValue);\n\n\t\tMonth firstMonth = quarter.firstMonth();\n\t\tassertEquals(Month.JANUARY, firstMonth);\n\n\t\t// ==========\n\n\t\tint lastMonthValue = quarter.lastMonthValue();\n\t\tassertEquals(3, lastMonthValue);\n\n\t\tMonth lastMonth = quarter.lastMonth();\n\t\tassertEquals(Month.MARCH, lastMonth);\n\n\t\t// ==========\n\n\t\tMonthDay firstMonthDay = quarter.firstMonthDay();\n\t\tassertEquals(firstMonthDay, MonthDay.of(1, 1));\n\n\t\tMonthDay lastMonthDay = quarter.lastMonthDay();\n\t\tassertEquals(lastMonthDay, MonthDay.of(3, 31));\n\t}\n\n\t@Test\n\tvoid testQ2() {\n\t\tQuarter quarter = Quarter.of(2);\n\t\tassertSame(Quarter.Q2, quarter);\n\t\tassertSame(quarter, Quarter.valueOf(\"Q2\"));\n\t\tassertEquals(2, quarter.getValue());\n\t\tassertEquals(\"Q2\", quarter.name());\n\n\t\tassertNull(Quarter.of(5));\n\n\t\t// ==========\n\n\t\tint firstMonthValue = quarter.firstMonthValue();\n\t\tassertEquals(4, firstMonthValue);\n\n\t\tMonth firstMonth = quarter.firstMonth();\n\t\tassertEquals(Month.APRIL, firstMonth);\n\n\t\t// ==========\n\n\t\tint lastMonthValue = quarter.lastMonthValue();\n\t\tassertEquals(6, lastMonthValue);\n\n\t\tMonth lastMonth = quarter.lastMonth();\n\t\tassertEquals(Month.JUNE, lastMonth);\n\n\t\t// ==========\n\n\t\tMonthDay firstMonthDay = quarter.firstMonthDay();\n\t\tassertEquals(firstMonthDay, MonthDay.of(4, 1));\n\n\t\tMonthDay lastMonthDay = quarter.lastMonthDay();\n\t\tassertEquals(lastMonthDay, MonthDay.of(6, 30));\n\t}\n\n\t@Test\n\tvoid testQ3() {\n\t\tQuarter quarter = Quarter.of(3);\n\t\tassertSame(Quarter.Q3, quarter);\n\t\tassertSame(quarter, Quarter.valueOf(\"Q3\"));\n\t\tassertEquals(3, quarter.getValue());\n\t\tassertEquals(\"Q3\", quarter.name());\n\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tQuarter.valueOf(\"Abc\");\n\t\t});\n\n\t\t// ==========\n\n\t\tint firstMonthValue = quarter.firstMonthValue();\n\t\tassertEquals(7, firstMonthValue);\n\n\t\tMonth firstMonth = quarter.firstMonth();\n\t\tassertEquals(Month.JULY, firstMonth);\n\n\t\t// ==========\n\n\t\tint lastMonthValue = quarter.lastMonthValue();\n\t\tassertEquals(9, lastMonthValue);\n\n\t\tMonth lastMonth = quarter.lastMonth();\n\t\tassertEquals(Month.SEPTEMBER, lastMonth);\n\n\t\t// ==========\n\n\t\tMonthDay firstMonthDay = quarter.firstMonthDay();\n\t\tassertEquals(firstMonthDay, MonthDay.of(7, 1));\n\n\t\tMonthDay lastMonthDay = quarter.lastMonthDay();\n\t\tassertEquals(lastMonthDay, MonthDay.of(9, 30));\n\t}\n\n\t@Test\n\tvoid testQ4() {\n\t\tQuarter quarter = Quarter.of(4);\n\t\tassertSame(Quarter.Q4, quarter);\n\t\tassertSame(quarter, Quarter.valueOf(\"Q4\"));\n\t\tassertEquals(4, quarter.getValue());\n\t\tassertEquals(\"Q4\", quarter.name());\n\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tQuarter.valueOf(\"Q5\");\n\t\t});\n\n\t\t// ==========\n\n\t\tint firstMonthValue = quarter.firstMonthValue();\n\t\tassertEquals(10, firstMonthValue);\n\n\t\tMonth firstMonth = quarter.firstMonth();\n\t\tassertEquals(Month.OCTOBER, firstMonth);\n\n\t\t// ==========\n\n\t\tint lastMonthValue = quarter.lastMonthValue();\n\t\tassertEquals(12, lastMonthValue);\n\t\tMonth lastMonth = quarter.lastMonth();\n\t\tassertEquals(Month.DECEMBER, lastMonth);\n\n\t\t// ==========\n\n\t\tMonthDay firstMonthDay = quarter.firstMonthDay();\n\t\tassertEquals(firstMonthDay, MonthDay.of(10, 1));\n\n\t\tMonthDay lastMonthDay = quarter.lastMonthDay();\n\t\tassertEquals(lastMonthDay, MonthDay.of(12, 31));\n\t}\n\n\t@Test\n\tvoid testPlusZeroAndPositiveRealNumbers() {\n\t\tfor (int i = 0; i < 100; i += 4) {\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = 1; i < 100 + 1; i += 4) {\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = 2; i < 100 + 2; i += 4) {\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = 3; i < 100 + 3; i += 4) {\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q4.plus(i));\n\t\t}\n\t}\n\n\t@Test\n\tvoid testPlusZeroAndNegativeNumber() {\n\t\tfor (int i = 0; i > -100; i -= 4) {\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = -1; i > -(100 + 1); i -= 4) {\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = -2; i > -(100 + 2); i -= 4) {\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q4.plus(i));\n\t\t}\n\t\tfor (int i = -3; i > -(100 + 3); i -= 4) {\n\t\t\tassertEquals(Quarter.Q2, Quarter.Q1.plus(i));\n\t\t\tassertEquals(Quarter.Q3, Quarter.Q2.plus(i));\n\t\t\tassertEquals(Quarter.Q4, Quarter.Q3.plus(i));\n\t\t\tassertEquals(Quarter.Q1, Quarter.Q4.plus(i));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/TemporalAccessorUtilTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDate;\nimport java.time.LocalTime;\n\npublic class TemporalAccessorUtilTest {\n\n\t@Test\n\tpublic void formatLocalDateTest(){\n\t\tfinal String format = TemporalAccessorUtil.format(LocalDate.of(2020, 12, 7), DatePattern.NORM_DATETIME_PATTERN);\n\t\tassertEquals(\"2020-12-07 00:00:00\", format);\n\t}\n\n\t@Test\n\tpublic void formatLocalTimeTest(){\n\t\tfinal String today = TemporalAccessorUtil.format(LocalDate.now(), DatePattern.NORM_DATE_PATTERN);\n\t\tfinal String format = TemporalAccessorUtil.format(LocalTime.MIN, DatePattern.NORM_DATETIME_PATTERN);\n\t\tassertEquals(today + \" 00:00:00\", format);\n\t}\n\n\t@Test\n\tpublic void formatCustomTest(){\n\t\tfinal String today = TemporalAccessorUtil.format(\n\t\t\t\tLocalDate.of(2021, 6, 26), \"#sss\");\n\t\tassertEquals(\"1624636800\", today);\n\n\t\tfinal String today2 = TemporalAccessorUtil.format(\n\t\t\t\tLocalDate.of(2021, 6, 26), \"#SSS\");\n\t\tassertEquals(\"1624636800000\", today2);\n\t}\n\n\t@Test\n\tpublic void isInTest(){\n\t\tfinal String sourceStr = \"2022-04-19 00:00:00\";\n\t\tfinal String startTimeStr = \"2022-04-19 00:00:00\";\n\t\tfinal String endTimeStr = \"2022-04-19 23:59:59\";\n\t\tfinal boolean between = TemporalAccessorUtil.isIn(\n\t\t\t\tLocalDateTimeUtil.parse(sourceStr, DatePattern.NORM_DATETIME_FORMATTER),\n\t\t\t\tLocalDateTimeUtil.parse(startTimeStr, DatePattern.NORM_DATETIME_FORMATTER),\n\t\t\t\tLocalDateTimeUtil.parse(endTimeStr, DatePattern.NORM_DATETIME_FORMATTER));\n\t\tassertTrue(between);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/TimeIntervalTest.java",
    "content": "package cn.hutool.core.date;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Test;\n\npublic class TimeIntervalTest {\n\t@Test\n\tpublic void intervalGroupTest(){\n\t\tfinal TimeInterval timer = new TimeInterval();\n\t\ttimer.start(\"1\");\n\t\tThreadUtil.sleep(800);\n\t\ttimer.start(\"2\");\n\t\tThreadUtil.sleep(900);\n\n\n\t\tConsole.log(\"Timer 1 took {} ms\", timer.intervalMs(\"1\"));\n\t\tConsole.log(\"Timer 2 took {} ms\", timer.intervalMs(\"2\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/TimeZoneTest.java",
    "content": "package cn.hutool.core.date;\n\nimport java.util.TimeZone;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.date.format.FastDateFormat;\n\npublic class TimeZoneTest {\n\n\t@Test\n\tpublic void timeZoneConvertTest() {\n\t\tDateTime dt = DateUtil.parse(\"2018-07-10 21:44:32\", //\n\t\t\t\tFastDateFormat.getInstance(DatePattern.NORM_DATETIME_PATTERN, TimeZone.getTimeZone(\"GMT+8:00\")));\n\t\tassertEquals(\"2018-07-10 21:44:32\", dt.toString());\n\n\t\tdt.setTimeZone(TimeZone.getTimeZone(\"Europe/London\"));\n\t\tint hour = dt.getField(DateField.HOUR_OF_DAY);\n\t\tassertEquals(14, hour);\n\t\tassertEquals(\"2018-07-10 14:44:32\", dt.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/WeekTest.java",
    "content": "package cn.hutool.core.date;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.time.DayOfWeek;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WeekTest {\n\n\t@Test\n\tpublic void ofTest() {\n\t\t//测试别名及大小写\n\t\tassertEquals(Week.SUNDAY, Week.of(\"sun\"));\n\t\tassertEquals(Week.SUNDAY, Week.of(\"SUN\"));\n\t\tassertEquals(Week.SUNDAY, Week.of(\"Sun\"));\n\t\t//测试全名及大小写\n\t\tassertEquals(Week.SUNDAY, Week.of(\"sunday\"));\n\t\tassertEquals(Week.SUNDAY, Week.of(\"Sunday\"));\n\t\tassertEquals(Week.SUNDAY, Week.of(\"SUNDAY\"));\n\n\t\tassertEquals(Week.MONDAY, Week.of(\"Mon\"));\n\t\tassertEquals(Week.MONDAY, Week.of(\"Monday\"));\n\n\t\tassertEquals(Week.TUESDAY, Week.of(\"tue\"));\n\t\tassertEquals(Week.TUESDAY, Week.of(\"tuesday\"));\n\n\t\tassertEquals(Week.WEDNESDAY, Week.of(\"wed\"));\n\t\tassertEquals(Week.WEDNESDAY, Week.of(\"WEDNESDAY\"));\n\n\t\tassertEquals(Week.THURSDAY, Week.of(\"thu\"));\n\t\tassertEquals(Week.THURSDAY, Week.of(\"THURSDAY\"));\n\n\t\tassertEquals(Week.FRIDAY, Week.of(\"fri\"));\n\t\tassertEquals(Week.FRIDAY, Week.of(\"FRIDAY\"));\n\n\t\tassertEquals(Week.SATURDAY, Week.of(\"sat\"));\n\t\tassertEquals(Week.SATURDAY, Week.of(\"SATURDAY\"));\n\t}\n\n\t@Test\n\tpublic void ofChineseTest() {\n\t\tassertEquals(Week.SUNDAY, Week.of(\"星期日\"));\n\t\tassertEquals(Week.SUNDAY, Week.of(\"周日\"));\n\n\t\tassertEquals(Week.MONDAY, Week.of(\"星期一\"));\n\t\tassertEquals(Week.MONDAY, Week.of(\"周一\"));\n\n\t\tassertEquals(Week.TUESDAY, Week.of(\"星期二\"));\n\t\tassertEquals(Week.TUESDAY, Week.of(\"周二\"));\n\n\t\tassertEquals(Week.WEDNESDAY, Week.of(\"星期三\"));\n\t\tassertEquals(Week.WEDNESDAY, Week.of(\"周三\"));\n\n\t\tassertEquals(Week.THURSDAY, Week.of(\"星期四\"));\n\t\tassertEquals(Week.THURSDAY, Week.of(\"周四\"));\n\n\t\tassertEquals(Week.FRIDAY, Week.of(\"星期五\"));\n\t\tassertEquals(Week.FRIDAY, Week.of(\"周五\"));\n\n\t\tassertEquals(Week.SATURDAY, Week.of(\"星期六\"));\n\t\tassertEquals(Week.SATURDAY, Week.of(\"周六\"));\n\t}\n\n\t@Test\n\tpublic void ofTest2() {\n\t\tassertEquals(Week.SUNDAY, Week.of(DayOfWeek.SUNDAY));\n\t\tassertEquals(Week.MONDAY, Week.of(DayOfWeek.MONDAY));\n\t\tassertEquals(Week.TUESDAY, Week.of(DayOfWeek.TUESDAY));\n\t\tassertEquals(Week.WEDNESDAY, Week.of(DayOfWeek.WEDNESDAY));\n\t\tassertEquals(Week.THURSDAY, Week.of(DayOfWeek.THURSDAY));\n\t\tassertEquals(Week.FRIDAY, Week.of(DayOfWeek.FRIDAY));\n\t\tassertEquals(Week.SATURDAY, Week.of(DayOfWeek.SATURDAY));\n\t}\n\n\t@Test\n\tpublic void toJdkDayOfWeekTest() {\n\t\tassertEquals(DayOfWeek.MONDAY, Week.MONDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.TUESDAY, Week.TUESDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.WEDNESDAY, Week.WEDNESDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.THURSDAY, Week.THURSDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.FRIDAY, Week.FRIDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.SATURDAY, Week.SATURDAY.toJdkDayOfWeek());\n\t\tassertEquals(DayOfWeek.SUNDAY, Week.SUNDAY.toJdkDayOfWeek());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/YearQuarterTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.DateTimeException;\nimport java.time.LocalDate;\nimport java.time.Year;\nimport java.time.YearMonth;\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\npublic class YearQuarterTest {\n\n\t// ================================\n\t// #region - of(int year, int quarter)\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { 1, 2, 3, 4 })\n\tvoid of_ValidYearAndQuarterValue_CreatesYearQuarter(int quarter) {\n\t\t{\n\t\t\tint year = 2024;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(Quarter.of(quarter), yearQuarter.getQuarter());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarterValue());\n\t\t}\n\t\t{\n\t\t\tint year = Year.MIN_VALUE;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(Quarter.of(quarter), yearQuarter.getQuarter());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarterValue());\n\t\t}\n\t\t{\n\t\t\tint year = Year.MAX_VALUE;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(Quarter.of(quarter), yearQuarter.getQuarter());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarterValue());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { -1, 0, 5, 108 })\n\tvoid of_ValidYearAndInvalidQuarterValue_DateTimeException(int quarter) {\n\t\tint year = 2024;\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, quarter);\n\t\t});\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 })\n\tvoid of_InvalidYearAndValidQuarterValue_DateTimeException(int year) {\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, 1);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, 2);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, 3);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, 4);\n\t\t});\n\t}\n\n\t@Test\n\tvoid of_InvalidYearAndInvalidQuarterValue_DateTimeException() {\n\t\tfinal int[] years = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 };\n\t\tfinal int[] quarters = { -1, 0, 5, 108 };\n\t\tfor (int year : years) {\n\t\t\tfinal int yearValue = year;\n\t\t\tfor (int quarter : quarters) {\n\t\t\t\tfinal int quarterValue = quarter;\n\t\t\t\tassertThrows(DateTimeException.class,\n\t\t\t\t\t\t() -> YearQuarter.of(yearValue, quarterValue));\n\t\t\t}\n\t\t}\n\t}\n\n\t// ================================\n\t// #endregion - of(int year, int quarter)\n\t// ================================\n\n\t// ================================\n\t// #region - of(int year, Quarter quarter)\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { 1, 2, 3, 4 })\n\tvoid of_ValidYearAndQuarter_CreatesYearQuarter(int quarterValue) {\n\t\t{\n\t\t\tint year = 2024;\n\t\t\tQuarter quarter = Quarter.of(quarterValue);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarter());\n\t\t\tassertEquals(quarterValue, yearQuarter.getQuarterValue());\n\t\t}\n\t\t{\n\t\t\tint year = Year.MIN_VALUE;\n\t\t\tQuarter quarter = Quarter.of(quarterValue);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarter());\n\t\t\tassertEquals(quarterValue, yearQuarter.getQuarterValue());\n\t\t}\n\t\t{\n\t\t\tint year = Year.MAX_VALUE;\n\t\t\tQuarter quarter = Quarter.of(quarterValue);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarter);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(quarter, yearQuarter.getQuarter());\n\t\t\tassertEquals(quarterValue, yearQuarter.getQuarterValue());\n\t\t}\n\t}\n\n\t@Test\n\tvoid of_ValidYearAndNullQuarter_NullPointerException() {\n\t\tint year = 2024;\n\t\tassertThrows(NullPointerException.class, () -> {\n\t\t\tYearQuarter.of(year, null);\n\t\t});\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 })\n\tvoid of_InvalidYearAndValidQuarter_DateTimeException(int year) {\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, Quarter.Q1);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, Quarter.Q2);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, Quarter.Q3);\n\t\t});\n\t\tassertThrows(DateTimeException.class, () -> {\n\t\t\tYearQuarter.of(year, Quarter.Q4);\n\t\t});\n\t}\n\n\t@Test\n\tvoid of_InvalidYearAndNullQuarter_DateTimeException() {\n\t\tfinal int[] years = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 };\n\t\tfor (int year : years) {\n\t\t\tfinal int yearValue = year;\n\t\t\tassertThrows(DateTimeException.class,\n\t\t\t\t\t() -> YearQuarter.of(yearValue, null));\n\n\t\t}\n\t}\n\n\t// ================================\n\t// #endregion - of(int year, Quarter quarter)\n\t// ================================\n\n\t// ================================\n\t// #region - of(LocalDate date)\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidLocalDate_CreatesYearQuarter_Q1(int year) {\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 1).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 1).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 2).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 2).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 3).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 3).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidLocalDate_CreatesYearQuarter_Q2(int year) {\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 4).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 4).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 5).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 5).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 6).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 6).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidLocalDate_CreatesYearQuarter_Q3(int year) {\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 7).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 7).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 8).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 8).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 9).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 9).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidLocalDate_CreatesYearQuarter_Q4(int year) {\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 10).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 10).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 11).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 11).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 12).atDay(1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tLocalDate date = YearMonth.of(year, 12).atEndOfMonth();\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t}\n\n\t@Test\n\tvoid of_NullLocalDate_NullPointerException() {\n\t\tLocalDate date = null;\n\t\tassertThrows(NullPointerException.class, () -> {\n\t\t\tYearQuarter.of(date);\n\t\t});\n\t}\n\n\t// ================================\n\t// #endregion - of(LocalDate date)\n\t// ================================\n\n\t// ================================\n\t// #region - of(Date date)\n\t// ================================\n\n\t@SuppressWarnings(\"deprecation\")\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\t1,\n\t\t\t999999,\n\t})\n\tvoid of_ValidDate_CreatesYearQuarter(int year) {\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 1 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 3 - 1, 31, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 4 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 6 - 1, 30, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 7 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 9 - 1, 30, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 10 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tDate date = new Date(year - 1900, 12 - 1, 31, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t}\n\n\t@Test\n\tvoid of_NullDate_NullPointerException() {\n\t\tDate date = null;\n\t\tassertThrows(NullPointerException.class, () -> {\n\t\t\tYearQuarter.of(date);\n\t\t});\n\t}\n\n\t// ================================\n\t// #endregion - of(Date date)\n\t// ================================\n\n\t// ================================\n\t// #region - of(Calendar date)\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\t1,\n\t\t\t999999,\n\t})\n\tvoid of_ValidCalendar_CreatesYearQuarter(int year) {\n\t\tCalendar date = Calendar.getInstance();\n\t\t{\n\t\t\tdate.set(year, 1 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 3 - 1, 31, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(1, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 4 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 6 - 1, 30, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(2, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 7 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 9 - 1, 30, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(3, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 10 - 1, 1);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t\t{\n\t\t\tdate.set(year, 12 - 1, 31, 23, 59, 59);\n\t\t\tYearQuarter yq = YearQuarter.of(date);\n\t\t\tassertEquals(year, yq.getYear());\n\t\t\tassertEquals(4, yq.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\t}\n\t}\n\n\t@Test\n\tvoid of_NullCalendar_NullPointerException() {\n\t\tCalendar date = null;\n\t\tassertThrows(NullPointerException.class, () -> {\n\t\t\tYearQuarter.of(date);\n\t\t});\n\t}\n\n\t// ================================\n\t// #endregion - of(Calendar date)\n\t// ================================\n\n\t// ================================\n\t// #region - of(YearMonth yearMonth)\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidYearMonth_CreatesYearMonth_Q1(int year) {\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 1);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(1, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 2);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(1, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 3);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(1, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q1, yearQuarter.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidYearMonth_CreatesYearMonth_Q2(int year) {\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 4);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(2, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 5);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(2, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 6);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(2, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q2, yearQuarter.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidYearMonth_CreatesYearMonth_Q3(int year) {\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 7);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(3, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 8);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(3, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 9);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(3, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q3, yearQuarter.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_ValidYearMonth_CreatesYearMonth_Q4(int year) {\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 10);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(4, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 11);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(4, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yearQuarter.getQuarter());\n\t\t}\n\t\t{\n\t\t\tYearMonth yearMonth = YearMonth.of(year, 12);\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(yearMonth);\n\t\t\tassertEquals(year, yearQuarter.getYear());\n\t\t\tassertEquals(4, yearQuarter.getQuarterValue());\n\t\t\tassertSame(Quarter.Q4, yearQuarter.getQuarter());\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = {\n\t\t\t2023, // 非闰年\n\t\t\t2024, // 闰年\n\t\t\tYear.MIN_VALUE,\n\t\t\tYear.MAX_VALUE,\n\t})\n\tvoid of_NullYearMonth_CreatesYearMonth_Q4(int year) {\n\t\tYearMonth yearMonth = null;\n\t\tassertThrows(NullPointerException.class,\n\t\t\t\t() -> YearQuarter.of(yearMonth));\n\t}\n\n\t// ================================\n\t// #endregion - of(YearMonth yearMonth)\n\t// ================================\n\n\t// ================================\n\t// #region - firstDate & lastDate\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })\n\tvoid test_getFirstDate_And_getLastDate(int year) {\n\t\t{\n\t\t\tfinal int quarterValue = 1;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarterValue);\n\n\t\t\tLocalDate expectedFirstDate = LocalDate.of(year, 1, 1);\n\t\t\tLocalDate expectedLastDate = LocalDate.of(year, 3, 31);\n\n\t\t\tassertEquals(expectedFirstDate, yearQuarter.firstDate());\n\t\t\tassertEquals(expectedLastDate, yearQuarter.lastDate());\n\t\t}\n\t\t{\n\t\t\tfinal int quarterValue = 2;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarterValue);\n\n\t\t\tLocalDate expectedFirstDate = LocalDate.of(year, 4, 1);\n\t\t\tLocalDate expectedLastDate = LocalDate.of(year, 6, 30);\n\n\t\t\tassertEquals(expectedFirstDate, yearQuarter.firstDate());\n\t\t\tassertEquals(expectedLastDate, yearQuarter.lastDate());\n\t\t}\n\t\t{\n\t\t\tfinal int quarterValue = 3;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarterValue);\n\n\t\t\tLocalDate expectedFirstDate = LocalDate.of(year, 7, 1);\n\t\t\tLocalDate expectedLastDate = LocalDate.of(year, 9, 30);\n\n\t\t\tassertEquals(expectedFirstDate, yearQuarter.firstDate());\n\t\t\tassertEquals(expectedLastDate, yearQuarter.lastDate());\n\t\t}\n\t\t{\n\t\t\tfinal int quarterValue = 4;\n\t\t\tYearQuarter yearQuarter = YearQuarter.of(year, quarterValue);\n\n\t\t\tLocalDate expectedFirstDate = LocalDate.of(year, 10, 1);\n\t\t\tLocalDate expectedLastDate = LocalDate.of(year, 12, 31);\n\n\t\t\tassertEquals(expectedFirstDate, yearQuarter.firstDate());\n\t\t\tassertEquals(expectedLastDate, yearQuarter.lastDate());\n\t\t}\n\t}\n\n\t// ================================\n\t// #endregion - firstDate & lastDate\n\t// ================================\n\n\t// ================================\n\t// #region - firstYearMonth & lastYearMonth\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })\n\tvoid test_firstYearMonth_And_lastYearMonth(int year) {\n\t\tYearQuarter yq;\n\n\t\tyq = YearQuarter.of(year, Quarter.Q1);\n\t\tassertEquals(YearMonth.of(year, 1), yq.firstYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q2);\n\t\tassertEquals(YearMonth.of(year, 4), yq.firstYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q3);\n\t\tassertEquals(YearMonth.of(year, 7), yq.firstYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q4);\n\t\tassertEquals(YearMonth.of(year, 10), yq.firstYearMonth());\n\n\t\tyq = YearQuarter.of(year, Quarter.Q1);\n\t\tassertEquals(YearMonth.of(year, 3), yq.lastYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q2);\n\t\tassertEquals(YearMonth.of(year, 6), yq.lastYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q3);\n\t\tassertEquals(YearMonth.of(year, 9), yq.lastYearMonth());\n\t\tyq = YearQuarter.of(year, Quarter.Q4);\n\t\tassertEquals(YearMonth.of(year, 12), yq.lastYearMonth());\n\t}\n\n\t// ================================\n\t// #endregion - firstYearMonth & lastYearMonth\n\t// ================================\n\n\t// ================================\n\t// #region - firstMonth & lastMonth\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })\n\tvoid testFirstMonthAndLastMonth(int year) {\n\t\tYearQuarter q1 = YearQuarter.of(year, 1);\n\t\tassertEquals(1, q1.firstMonthValue());\n\t\tassertEquals(Month.JANUARY, q1.firstMonth());\n\t\tassertEquals(3, q1.lastMonthValue());\n\t\tassertEquals(Month.MARCH, q1.lastMonth());\n\n\t\tYearQuarter q2 = YearQuarter.of(year, 2);\n\t\tassertEquals(4, q2.firstMonthValue());\n\t\tassertEquals(Month.APRIL, q2.firstMonth());\n\t\tassertEquals(6, q2.lastMonthValue());\n\t\tassertEquals(Month.JUNE, q2.lastMonth());\n\n\t\tYearQuarter q3 = YearQuarter.of(year, 3);\n\t\tassertEquals(7, q3.firstMonthValue());\n\t\tassertEquals(Month.JULY, q3.firstMonth());\n\t\tassertEquals(9, q3.lastMonthValue());\n\t\tassertEquals(Month.SEPTEMBER, q3.lastMonth());\n\n\t\tYearQuarter q4 = YearQuarter.of(year, 4);\n\t\tassertEquals(10, q4.firstMonthValue());\n\t\tassertEquals(Month.OCTOBER, q4.firstMonth());\n\t\tassertEquals(12, q4.lastMonthValue());\n\t\tassertEquals(Month.DECEMBER, q4.lastMonth());\n\t}\n\n\t// ================================\n\t// #endregion - firstMonth & lastMonth\n\t// ================================\n\n\t// ================================\n\t// #region - compareTo\n\t// ================================\n\n\t@Test\n\tvoid testCompareTo() {\n\t\tint year1;\n\t\tint quarter1;\n\t\tYearQuarter yearQuarter1;\n\n\t\tyear1 = 2024;\n\t\tquarter1 = 1;\n\t\tyearQuarter1 = YearQuarter.of(year1, Quarter.of(quarter1));\n\n\t\tfor (int year2 = 2000; year2 <= 2050; year2++) {\n\t\t\tfor (int quarter2 = 1; quarter2 <= 4; quarter2++) {\n\t\t\t\tYearQuarter yearQuarter2 = YearQuarter.of(year2, Quarter.of(quarter2));\n\n\t\t\t\tif (year1 == year2) {\n\t\t\t\t\t// 同年\n\t\t\t\t\tassertEquals(quarter1 - quarter2, yearQuarter1.compareTo(yearQuarter2));\n\n\t\t\t\t\tif (quarter1 == quarter2) {\n\t\t\t\t\t\t// 同年同季度\n\t\t\t\t\t\tassertEquals(yearQuarter1, yearQuarter2);\n\t\t\t\t\t\tassertEquals(0, yearQuarter1.compareTo(yearQuarter2));\n\t\t\t\t\t} else if (quarter1 < quarter2) {\n\t\t\t\t\t\tassertNotEquals(yearQuarter1, yearQuarter2);\n\t\t\t\t\t\tassertTrue(yearQuarter1.isBefore(yearQuarter2));\n\t\t\t\t\t\tassertFalse(yearQuarter1.isAfter(yearQuarter2));\n\t\t\t\t\t\tassertFalse(yearQuarter2.isBefore(yearQuarter1));\n\t\t\t\t\t\tassertTrue(yearQuarter2.isAfter(yearQuarter1));\n\t\t\t\t\t} else if (quarter1 > quarter2) {\n\t\t\t\t\t\tassertNotEquals(yearQuarter1, yearQuarter2);\n\t\t\t\t\t\tassertFalse(yearQuarter1.isBefore(yearQuarter2));\n\t\t\t\t\t\tassertTrue(yearQuarter1.isAfter(yearQuarter2));\n\t\t\t\t\t\tassertTrue(yearQuarter2.isBefore(yearQuarter1));\n\t\t\t\t\t\tassertFalse(yearQuarter2.isAfter(yearQuarter1));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// 不同年\n\t\t\t\t\tassertEquals(year1 - year2, yearQuarter1.compareTo(yearQuarter2));\n\t\t\t\t\tassertNotEquals(0, yearQuarter1.compareTo(yearQuarter2));\n\t\t\t\t\tif (year1 < year2) {\n\t\t\t\t\t\tassertNotEquals(yearQuarter1, yearQuarter2);\n\t\t\t\t\t\tassertTrue(yearQuarter1.isBefore(yearQuarter2));\n\t\t\t\t\t\tassertFalse(yearQuarter1.isAfter(yearQuarter2));\n\t\t\t\t\t\tassertFalse(yearQuarter2.isBefore(yearQuarter1));\n\t\t\t\t\t\tassertTrue(yearQuarter2.isAfter(yearQuarter1));\n\t\t\t\t\t} else if (year1 > year2) {\n\t\t\t\t\t\tassertNotEquals(yearQuarter1, yearQuarter2);\n\t\t\t\t\t\tassertFalse(yearQuarter1.isBefore(yearQuarter2));\n\t\t\t\t\t\tassertTrue(yearQuarter1.isAfter(yearQuarter2));\n\t\t\t\t\t\tassertTrue(yearQuarter2.isBefore(yearQuarter1));\n\t\t\t\t\t\tassertFalse(yearQuarter2.isAfter(yearQuarter1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// ================================\n\t// #endregion - compareTo\n\t// ================================\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE + 25, Year.MAX_VALUE - 25, -1, 0, 1, 1949, 1990, 2000, 2008, 2023, 2024 })\n\tvoid testPlusQuartersAndMinusQuarters(int year) {\n\t\tfor (int quarter = 1; quarter <= 4; quarter++) {\n\t\t\tYearQuarter yq1 = YearQuarter.of(year, quarter);\n\t\t\tfor (int quartersToAdd = -100; quartersToAdd <= 100; quartersToAdd++) {\n\t\t\t\tYearQuarter plus = yq1.plusQuarters(quartersToAdd);\n\t\t\t\tYearQuarter minus = yq1.minusQuarters(-quartersToAdd);\n\t\t\t\tassertEquals(plus, minus);\n\n\t\t\t\t// offset: 表示自 公元 0000年以来，经历了多少季度。所以 0 表示 -0001,Q4; 1 表示 0000 Q1\n\t\t\t\tlong offset = (year * 4L + quarter) + quartersToAdd;\n\t\t\t\tif (offset > 0) {\n\t\t\t\t\tassertEquals((offset - 1) / 4, plus.getYear());\n\t\t\t\t\tassertEquals(((offset - 1) % 4) + 1, plus.getQuarterValue());\n\t\t\t\t} else {\n\t\t\t\t\tassertEquals((offset / 4 - 1), plus.getYear());\n\t\t\t\t\tassertEquals((4 + offset % 4), plus.getQuarterValue());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE + 1, Year.MAX_VALUE - 1, -1, 0, 1, 1900, 1990, 2000, 2023, 2024 })\n\tvoid test_nextQuarter_And_lastQuarter(int year) {\n\t\tint quarter;\n\n\t\tYearQuarter yq;\n\t\tYearQuarter next;\n\t\tYearQuarter last;\n\n\t\tquarter = 1;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextQuarter();\n\t\tassertEquals(year, next.getYear());\n\t\tassertEquals(2, next.getQuarterValue());\n\t\tlast = yq.lastQuarter();\n\t\tassertEquals(year - 1, last.getYear());\n\t\tassertEquals(4, last.getQuarterValue());\n\n\t\tquarter = 2;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextQuarter();\n\t\tassertEquals(year, next.getYear());\n\t\tassertEquals(3, next.getQuarterValue());\n\t\tlast = yq.lastQuarter();\n\t\tassertEquals(year, last.getYear());\n\t\tassertEquals(1, last.getQuarterValue());\n\n\t\tquarter = 3;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextQuarter();\n\t\tassertEquals(year, next.getYear());\n\t\tassertEquals(4, next.getQuarterValue());\n\t\tlast = yq.lastQuarter();\n\t\tassertEquals(year, last.getYear());\n\t\tassertEquals(2, last.getQuarterValue());\n\n\t\tquarter = 4;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextQuarter();\n\t\tassertEquals(year + 1, next.getYear());\n\t\tassertEquals(1, next.getQuarterValue());\n\t\tlast = yq.lastQuarter();\n\t\tassertEquals(year, last.getYear());\n\t\tassertEquals(3, last.getQuarterValue());\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE + 100, Year.MAX_VALUE - 100, -1, 0, 1, 1949, 1990, 2000, 2008, 2023, 2024 })\n\tvoid test_PlusYearsAndMinusYears(int year) {\n\t\tfor (int yearToAdd = -100; yearToAdd <= 100; yearToAdd++) {\n\t\t\tYearQuarter q1 = YearQuarter.of(year, Quarter.Q1);\n\t\t\tYearQuarter plus = q1.plusYears(yearToAdd);\n\t\t\tassertEquals(year + yearToAdd, plus.getYear());\n\t\t\tassertEquals(Quarter.Q1, plus.getQuarter());\n\t\t\tYearQuarter minus = q1.minusYears(yearToAdd);\n\t\t\tassertEquals(Quarter.Q1, minus.getQuarter());\n\t\t\tassertEquals(year - yearToAdd, minus.getYear());\n\n\t\t\tassertEquals(q1.plusYears(yearToAdd), q1.minusYears(-yearToAdd));\n\t\t}\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { Year.MIN_VALUE + 1, Year.MAX_VALUE - 1, -1, 0, 1, 1900, 1990, 2000, 2023, 2024 })\n\tvoid test_nextYear_And_lastYear(int year) {\n\t\tint quarter;\n\n\t\tYearQuarter yq;\n\t\tYearQuarter next;\n\t\tYearQuarter last;\n\n\t\tquarter = 1;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextYear();\n\t\tassertSame(Quarter.Q1, yq.getQuarter());\n\t\tassertEquals(year + 1, next.getYear());\n\t\tassertSame(Quarter.Q1, next.getQuarter());\n\t\tlast = yq.lastYear();\n\t\tassertEquals(year - 1, last.getYear());\n\t\tassertSame(Quarter.Q1, last.getQuarter());\n\n\t\tquarter = 2;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextYear();\n\t\tassertSame(Quarter.Q2, yq.getQuarter());\n\t\tassertEquals(year + 1, next.getYear());\n\t\tassertSame(Quarter.Q2, next.getQuarter());\n\t\tlast = yq.lastYear();\n\t\tassertEquals(year - 1, last.getYear());\n\t\tassertSame(Quarter.Q2, last.getQuarter());\n\n\t\tquarter = 3;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextYear();\n\t\tassertSame(Quarter.Q3, yq.getQuarter());\n\t\tassertEquals(year + 1, next.getYear());\n\t\tassertSame(Quarter.Q3, next.getQuarter());\n\t\tlast = yq.lastYear();\n\t\tassertEquals(year - 1, last.getYear());\n\t\tassertSame(Quarter.Q3, last.getQuarter());\n\n\t\tquarter = 4;\n\t\tyq = YearQuarter.of(year, quarter);\n\t\tnext = yq.nextYear();\n\t\tassertSame(Quarter.Q4, yq.getQuarter());\n\t\tassertEquals(year + 1, next.getYear());\n\t\tassertSame(Quarter.Q4, next.getQuarter());\n\t\tlast = yq.lastYear();\n\t\tassertEquals(year - 1, last.getYear());\n\t\tassertSame(Quarter.Q4, last.getQuarter());\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })\n\tvoid test_compareTo_sameYear(int year) {\n\t\tYearQuarter yq1 = YearQuarter.of(year, 1);\n\t\tYearQuarter yq2 = YearQuarter.of(year, 2);\n\t\tYearQuarter yq3 = YearQuarter.of(year, 3);\n\t\tYearQuarter yq4 = YearQuarter.of(year, 4);\n\n\t\tassertTrue(yq1.equals(YearQuarter.of(year, Quarter.Q1))); // NOSONAR\n\t\tassertTrue(yq1.compareTo(yq1) == 0); // NOSONAR\n\t\tassertTrue(yq1.compareTo(yq2) < 0);\n\t\tassertTrue(yq1.compareTo(yq3) < 0);\n\t\tassertTrue(yq1.compareTo(yq4) < 0);\n\n\t\tassertTrue(yq2.equals(YearQuarter.of(year, Quarter.Q2))); // NOSONAR\n\t\tassertTrue(yq2.compareTo(yq1) > 0);\n\t\tassertTrue(yq2.compareTo(yq2) == 0); // NOSONAR\n\t\tassertTrue(yq2.compareTo(yq3) < 0);\n\t\tassertTrue(yq2.compareTo(yq4) < 0);\n\n\t\tassertTrue(yq3.equals(YearQuarter.of(year, Quarter.Q3))); // NOSONAR\n\t\tassertTrue(yq3.compareTo(yq1) > 0);\n\t\tassertTrue(yq3.compareTo(yq2) > 0);\n\t\tassertTrue(yq3.compareTo(yq3) == 0); // NOSONAR\n\t\tassertTrue(yq3.compareTo(yq4) < 0);\n\n\t\tassertTrue(yq4.equals(YearQuarter.of(year, Quarter.Q4))); // NOSONAR\n\t\tassertTrue(yq4.compareTo(yq1) > 0);\n\t\tassertTrue(yq4.compareTo(yq2) > 0);\n\t\tassertTrue(yq4.compareTo(yq3) > 0);\n\t\tassertTrue(yq4.compareTo(yq4) == 0); // NOSONAR\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })\n\tvoid test_isBefore_sameYear(int year) {\n\t\tYearQuarter yq1 = YearQuarter.of(year, 1);\n\t\tYearQuarter yq2 = YearQuarter.of(year, 2);\n\t\tYearQuarter yq3 = YearQuarter.of(year, 3);\n\t\tYearQuarter yq4 = YearQuarter.of(year, 4);\n\n\t\tassertFalse(yq1.isBefore(YearQuarter.of(year, Quarter.Q1)));\n\t\tassertTrue(yq1.isBefore(yq2));\n\t\tassertTrue(yq1.isBefore(yq3));\n\t\tassertTrue(yq1.isBefore(yq4));\n\n\t\tassertFalse(yq2.isBefore(yq1));\n\t\tassertFalse(yq2.isBefore(YearQuarter.of(year, Quarter.Q2)));\n\t\tassertTrue(yq2.isBefore(yq3));\n\t\tassertTrue(yq2.isBefore(yq4));\n\n\t\tassertFalse(yq3.isBefore(yq1));\n\t\tassertFalse(yq3.isBefore(yq2));\n\t\tassertFalse(yq3.isBefore(YearQuarter.of(year, Quarter.Q3)));\n\t\tassertTrue(yq3.isBefore(yq4));\n\n\t\tassertFalse(yq4.isBefore(yq1));\n\t\tassertFalse(yq4.isBefore(yq2));\n\t\tassertFalse(yq4.isBefore(yq3));\n\t\tassertFalse(yq4.isBefore(YearQuarter.of(year, Quarter.Q4)));\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })\n\tvoid test_isAfter_sameYear(int year) {\n\t\tYearQuarter yq1 = YearQuarter.of(year, 1);\n\t\tYearQuarter yq2 = YearQuarter.of(year, 2);\n\t\tYearQuarter yq3 = YearQuarter.of(year, 3);\n\t\tYearQuarter yq4 = YearQuarter.of(year, 4);\n\n\t\tassertFalse(yq1.isAfter(YearQuarter.of(year, Quarter.Q1)));\n\t\tassertFalse(yq1.isAfter(yq2));\n\t\tassertFalse(yq1.isAfter(yq3));\n\t\tassertFalse(yq1.isAfter(yq4));\n\n\t\tassertTrue(yq2.isAfter(yq1));\n\t\tassertFalse(yq2.isAfter(YearQuarter.of(year, Quarter.Q2)));\n\t\tassertFalse(yq2.isAfter(yq3));\n\t\tassertFalse(yq2.isAfter(yq4));\n\n\t\tassertTrue(yq3.isAfter(yq1));\n\t\tassertTrue(yq3.isAfter(yq2));\n\t\tassertFalse(yq3.isAfter(YearQuarter.of(year, Quarter.Q3)));\n\t\tassertFalse(yq3.isAfter(yq4));\n\n\t\tassertTrue(yq4.isAfter(yq1));\n\t\tassertTrue(yq4.isAfter(yq2));\n\t\tassertTrue(yq4.isAfter(yq3));\n\t\tassertFalse(yq4.isAfter(YearQuarter.of(year, Quarter.Q4)));\n\t}\n\n\t@Test\n\tvoid test_compareTo_null() {\n\t\tYearQuarter yq = YearQuarter.of(2024, 4);\n\t\tassertThrows(NullPointerException.class,\n\t\t\t\t() -> yq.compareTo(null));\n\t\tassertThrows(NullPointerException.class,\n\t\t\t\t() -> yq.isBefore(null));\n\t\tassertThrows(NullPointerException.class,\n\t\t\t\t() -> yq.isAfter(null));\n\t\tassertNotEquals(null, yq);\n\t}\n\n\t@ParameterizedTest\n\t@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE - 1, Year.MIN_VALUE + 1 })\n\tvoid test_compareTo_differentYear(int year) {\n\t\tfor (int quarter1 = 1; quarter1 <= 4; quarter1++) {\n\t\t\tYearQuarter yq = YearQuarter.of(year, quarter1);\n\t\t\tfor (int quarter2 = 1; quarter2 <= 4; quarter2++) {\n\t\t\t\t// gt\n\t\t\t\tassertTrue(yq.compareTo(YearQuarter.of(year + 1, quarter2)) < 0);\n\t\t\t\tassertTrue(yq.isBefore(YearQuarter.of(year + 1, quarter2)));\n\t\t\t\tassertTrue(YearQuarter.of(year + 1, quarter2).compareTo(yq) > 0);\n\t\t\t\tassertTrue(YearQuarter.of(year + 1, quarter2).isAfter(yq));\n\t\t\t\t// lt\n\t\t\t\tassertTrue(yq.compareTo(YearQuarter.of(year - 1, quarter2)) > 0);\n\t\t\t\tassertTrue(yq.isAfter(YearQuarter.of(year - 1, quarter2)));\n\t\t\t\tassertTrue(YearQuarter.of(year - 1, quarter2).compareTo(yq) < 0);\n\t\t\t\tassertTrue(YearQuarter.of(year - 1, quarter2).isBefore(yq));\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Calendar;\n\npublic class ZodiacTest {\n\n\t@Test\n\tpublic void getZodiacTest() {\n\t\tassertEquals(\"摩羯座\", Zodiac.getZodiac(Month.JANUARY, 19));\n\t\tassertEquals(\"水瓶座\", Zodiac.getZodiac(Month.JANUARY, 20));\n\t\tassertEquals(\"巨蟹座\", Zodiac.getZodiac(6, 17));\n\n\t\tfinal Calendar calendar = Calendar.getInstance();\n\t\tcalendar.set(2022, Calendar.JULY, 17);\n\t\tassertEquals(\"巨蟹座\", Zodiac.getZodiac(calendar.getTime()));\n\t\tassertEquals(\"巨蟹座\", Zodiac.getZodiac(calendar));\n\t\tassertNull(Zodiac.getZodiac((Calendar) null));\n\t}\n\n\t@Test\n\tpublic void getChineseZodiacTest() {\n\t\tassertEquals(\"狗\", Zodiac.getChineseZodiac(1994));\n\t\tassertEquals(\"狗\", Zodiac.getChineseZodiac(2018));\n\t\tassertEquals(\"猪\", Zodiac.getChineseZodiac(2019));\n\n\t\tfinal Calendar calendar = Calendar.getInstance();\n\t\tcalendar.set(2022, Calendar.JULY, 17);\n\t\tassertEquals(\"虎\", Zodiac.getChineseZodiac(calendar.getTime()));\n\t\tassertEquals(\"虎\", Zodiac.getChineseZodiac(calendar));\n\t\tassertNull(Zodiac.getChineseZodiac(1899));\n\t\tassertNull(Zodiac.getChineseZodiac((Calendar) null));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/ZoneUtilTest.java",
    "content": "package cn.hutool.core.date;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.ZoneId;\nimport java.util.TimeZone;\n\npublic class ZoneUtilTest {\n\t@Test\n\tpublic void toTest() {\n\t\tassertEquals(ZoneId.systemDefault(), ZoneUtil.toZoneId(null));\n\t\tassertEquals(TimeZone.getDefault(), ZoneUtil.toTimeZone(null));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/chinese/IssueI5YB1ATest.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport cn.hutool.core.date.ChineseDate;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI5YB1ATest {\n\t@Test\n\t\tpublic void chineseDateTest() {\n\t\t// 四月非闰月，因此isLeapMonth参数无效\n\t\tfinal ChineseDate date = new ChineseDate(2023, 4, 8, true);\n\t\tassertEquals(\"2023-05-26 00:00:00\", date.getGregorianDate().toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/chinese/IssueICL1BTTest.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport cn.hutool.core.date.ChineseDate;\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\npublic class IssueICL1BTTest {\n\t@Test\n\tvoid getFestivalsTest(){\n\t\tString date = \"2025-07-31\";\n\t\tDate productionDate = DateUtil.parseDate( date);\n\t\tChineseDate chineseDate = new ChineseDate(productionDate);\n\t\tSystem.out.println(chineseDate.isLeapMonth());\n\t\tAssertions.assertTrue(chineseDate.isLeapMonth());\n\t\tString festivals = chineseDate.getFestivals();\n\t\tAssertions.assertEquals(\"\", festivals);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/date/chinese/SolarTermsTest.java",
    "content": "package cn.hutool.core.date.chinese;\n\nimport cn.hutool.core.date.ChineseDate;\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class SolarTermsTest {\n\n\t@Test\n\tpublic void getTermTest1(){\n\t\tfinal int term = SolarTerms.getTerm(1987, 3);\n\t\tassertEquals(4, term);\n\t}\n\n\t@Test\n\tpublic void getTermTest() {\n\n\t\tassertEquals(\"小寒\", SolarTerms.getTerm(2021, 1, 5));\n\n\t\tassertEquals(\"大寒\", SolarTerms.getTerm(2021, 1, 20));\n\n\t\tassertEquals(\"立春\", SolarTerms.getTerm(2021, 2, 3));\n\n\t\tassertEquals(\"雨水\", SolarTerms.getTerm(2021, 2, 18));\n\n\t\tassertEquals(\"惊蛰\", SolarTerms.getTerm(2021, 3, 5));\n\n\t\tassertEquals(\"春分\", SolarTerms.getTerm(2021, 3, 20));\n\n\t\tassertEquals(\"清明\", SolarTerms.getTerm(2021, 4, 4));\n\n\t\tassertEquals(\"谷雨\", SolarTerms.getTerm(2021, 4, 20));\n\n\t\tassertEquals(\"立夏\", SolarTerms.getTerm(2021, 5, 5));\n\n\t\tassertEquals(\"小满\", SolarTerms.getTerm(2021, 5, 21));\n\n\t\tassertEquals(\"芒种\", SolarTerms.getTerm(2021, 6, 5));\n\n\t\tassertEquals(\"夏至\", SolarTerms.getTerm(2021, 6, 21));\n\n\t\tassertEquals(\"小暑\", SolarTerms.getTerm(2021, 7, 7));\n\n\t\tassertEquals(\"大暑\", SolarTerms.getTerm(2021, 7, 22));\n\n\t\tassertEquals(\"立秋\", SolarTerms.getTerm(2021, 8, 7));\n\n\t\tassertEquals(\"处暑\", SolarTerms.getTerm(2021, 8, 23));\n\n\t\tassertEquals(\"白露\", SolarTerms.getTerm(2021, 9, 7));\n\n\t\tassertEquals(\"秋分\", SolarTerms.getTerm(2021, 9, 23));\n\n\t\tassertEquals(\"寒露\", SolarTerms.getTerm(2021, 10, 8));\n\n\t\tassertEquals(\"霜降\", SolarTerms.getTerm(2021, 10, 23));\n\n\t\tassertEquals(\"立冬\", SolarTerms.getTerm(2021, 11, 7));\n\n\t\tassertEquals(\"小雪\", SolarTerms.getTerm(2021, 11, 22));\n\n\t\tassertEquals(\"大雪\", SolarTerms.getTerm(2021, 12, 7));\n\n\t\tassertEquals(\"冬至\", SolarTerms.getTerm(2021, 12, 21));\n\t}\n\n\n\t@Test\n\tpublic void getTermByDateTest() {\n\t\tassertEquals(\"春分\", SolarTerms.getTerm(DateUtil.parseDate(\"2021-03-20\")));\n\t\tassertEquals(\"处暑\", SolarTerms.getTerm(DateUtil.parseDate(\"2022-08-23\")));\n\t}\n\n\n\t@Test\n\tpublic void getTermByChineseDateTest() {\n\t\tassertEquals(\"清明\", SolarTerms.getTerm(new ChineseDate(2021, 2, 23)));\n\t\tassertEquals(\"秋分\", SolarTerms.getTerm(new ChineseDate(2022, 8, 28)));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/exceptions/CheckedUtilTest.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.core.lang.func.VoidFunc0;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.FileInputStream;\n\n/**\n * 方便的执行会抛出受检查类型异常的方法调用或者代码段\n * <p>\n * 该工具通过函数式的方式将那些需要抛出受检查异常的表达式或者代码段转化成一个标准的java8 functional 对象\n * </p>\n *\n * @author conder\n */\npublic class CheckedUtilTest {\n\n\n\t@Test\n\tpublic void sleepTest() {\n\t\tVoidFunc0 func = () -> Thread.sleep(1000L);\n\t\tfunc.callWithRuntimeException();\n\t}\n\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void supplierTest() {\n\t\tFile noFile = new File(\"./no-file\");\n\t\ttry {\n\t\t\t//本行代码原本需要抛出受检查异常，现在只抛出运行时异常\n\t\t\tCheckedUtil.uncheck(() -> new FileInputStream(noFile)).call();\n\t\t} catch (Exception re) {\n\t\t\tassertTrue(re instanceof RuntimeException);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void functionTest() {\n\t\tFunc1<String, String> afunc = (funcParam) -> {\n\t\t\tif (funcParam.length() > 5) {\n\t\t\t\tthrow new Exception(\"这是受检查异常需要屌用处显示处理\");\n\t\t\t}\n\t\t\treturn funcParam.toUpperCase();\n\t\t};\n\n\t\t//afunc.apply(\"hello world\"); 直接调用需要处理异常\n\n\n\t\ttry {\n\t\t\t//本行代码原本需要抛出受检查异常，现在只抛出运行时异常\n\t\t\tCheckedUtil.uncheck(afunc).call(\"hello world\");\n\t\t} catch (Exception re) {\n\t\t\tassertTrue(re instanceof RuntimeException);\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/exceptions/ExceptionUtilTest.java",
    "content": "package cn.hutool.core.exceptions;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IORuntimeException;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\n\n/**\n * 异常工具单元测试\n *\n * @author looly\n */\npublic class ExceptionUtilTest {\n\n\t@Test\n\tpublic void wrapTest() {\n\t\tIORuntimeException e = ExceptionUtil.wrap(new IOException(), IORuntimeException.class);\n\t\tassertNotNull(e);\n\t}\n\n\t@Test\n\tpublic void getRootTest() {\n\t\t// 查找入口方法\n\t\tStackTraceElement ele = ExceptionUtil.getRootStackElement();\n\t\tassertEquals(\"main\", ele.getMethodName());\n\t}\n\n\t@Test\n\tpublic void convertTest() {\n\t\t// RuntimeException e = new RuntimeException();\n\t\tIOException ioException = new IOException();\n\t\tIllegalArgumentException argumentException = new IllegalArgumentException(ioException);\n\t\tIOException ioException1 = ExceptionUtil.convertFromOrSuppressedThrowable(argumentException, IOException.class, true);\n\t\tassertNotNull(ioException1);\n\t}\n\n\t@Test\n\tpublic void bytesIntConvertTest(){\n\t\tfinal String s = Convert.toStr(12);\n\t\tfinal int integer = Convert.toInt(s);\n\t\tassertEquals(12, integer);\n\n\t\tfinal byte[] bytes = Convert.intToBytes(12);\n\t\tfinal int i = Convert.bytesToInt(bytes);\n\t\tassertEquals(12, i);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/img/FontUtilTest.java",
    "content": "package cn.hutool.core.img;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Font;\n\npublic class FontUtilTest {\n\n\t@Test\n\tpublic void createFontTest(){\n\t\tfinal Font font = FontUtil.createFont();\n\t\tassertNotNull(font);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/img/ImgTest.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileTypeUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.URLUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Color;\nimport java.awt.Font;\nimport java.awt.Image;\nimport java.awt.Rectangle;\nimport java.io.File;\n\npublic class ImgTest {\n\n\t@Test\n\t@Disabled\n\tpublic void cutTest1() {\n\t\tImg.from(FileUtil.file(\"e:/pic/face.jpg\")).cut(0, 0, 200).write(FileUtil.file(\"e:/pic/face_radis.png\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void compressTest() {\n\t\tImg.from(FileUtil.file(\"f:/test/4347273249269e3fb272341acc42d4e.jpg\")).setQuality(0.8).write(FileUtil.file(\"f:/test/test_dest.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void compressWithBackgroundColorTest() {\n\t\tImg.from(FileUtil.file(\"D:/test/before_compress.png\"))\n\t\t\t\t.setBackgroundColor(Color.WHITE)\n\t\t\t\t.setQuality(0.8)\n\t\t\t\t.write(FileUtil.file(\"D:/test/after_compress.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tfinal Img from = Img.from(FileUtil.file(\"d:/test/81898311-001d6100-95eb-11ea-83c2-a14d7b1010bd.png\"));\n\t\tImgUtil.write(from.getImg(), FileUtil.file(\"d:/test/dest.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void roundTest() {\n\t\tImg.from(FileUtil.file(\"e:/pic/face.jpg\")).round(0.5).write(FileUtil.file(\"e:/pic/face_round.png\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pressTextTest() {\n\t\tImg.from(FileUtil.file(\"d:/test/617180969474805871.jpg\"))\n\t\t\t\t.setPositionBaseCentre(false)\n\t\t\t\t.pressText(\"版权所有\", Color.RED, //\n\t\t\t\t\t\tnew Font(\"黑体\", Font.BOLD, 100), //\n\t\t\t\t\t\t0, //\n\t\t\t\t\t\t100, //\n\t\t\t\t\t\t1f)\n\t\t\t\t.write(FileUtil.file(\"d:/test/test2_result.png\"));\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void pressTextFullScreenTest() {\n\t\tImg.from(FileUtil.file(\"d:/test/1435859438434136064.jpg\"))\n\t\t\t\t.setTargetImageType(ImgUtil.IMAGE_TYPE_PNG)\n\t\t\t\t.pressTextFull(\"版权所有     \", Color.LIGHT_GRAY,\n\t\t\t\t\t\tnew Font(\"黑体\", Font.PLAIN, 30),\n\t\t\t\t\t\t4,\n\t\t\t\t\t\t30,\n\t\t\t\t\t\t0.8f)\n\t\t\t\t.write(FileUtil.file(\"d:/test/2_result.png\"));\n\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pressImgTest() {\n\t\tImg.from(FileUtil.file(\"d:/test/图片1.JPG\"))\n\t\t\t\t.pressImage(ImgUtil.read(\"d:/test/617180969474805871.jpg\"), new Rectangle(0, 0, 800, 800), 1f)\n\t\t\t\t.write(FileUtil.file(\"d:/test/pressImg_result.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void strokeTest() {\n\t\tImg.from(FileUtil.file(\"d:/test/公章3.png\"))\n\t\t\t\t.stroke(null, 2f)\n\t\t\t\t.write(FileUtil.file(\"d:/test/stroke_result.png\"));\n\t}\n\n\t/**\n\t * issue#I49FIU\n\t */\n\t@Test\n\t@Disabled\n\tpublic void scaleTest() {\n\t\tfinal String downloadFile = \"d:/test/1435859438434136064.JPG\";\n\t\tfinal File file = FileUtil.file(downloadFile);\n\t\tfinal File fileScale = FileUtil.file(downloadFile + \".scale.\" + FileTypeUtil.getType(file));\n\n\t\tfinal Image img = ImgUtil.getImage(URLUtil.getURL(file));\n\t\tImgUtil.scale(img, fileScale, 0.8f);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/img/ImgUtilTest.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.imageio.ImageIO;\nimport java.awt.Color;\nimport java.awt.Font;\nimport java.awt.Image;\nimport java.awt.Rectangle;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.MalformedURLException;\nimport java.net.URL;\n\npublic class ImgUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void scaleTest() {\n\t\tImgUtil.scale(FileUtil.file(\"e:/pic/test.jpg\"), FileUtil.file(\"e:/pic/test_result.jpg\"), 0.8f);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scaleTest2() {\n\t\tImgUtil.scale(\n\t\t\t\tFileUtil.file(\"d:/test/2.png\"),\n\t\t\t\tFileUtil.file(\"d:/test/2_result.jpg\"), 600, 337, null);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scalePngTest() {\n\t\tImgUtil.scale(FileUtil.file(\"f:/test/test.png\"), FileUtil.file(\"f:/test/test_result.png\"), 0.5f);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scaleByWidthAndHeightTest() {\n\t\tImgUtil.scale(FileUtil.file(\"f:/test/aaa.jpg\"), FileUtil.file(\"f:/test/aaa_result.jpg\"), 100, 400, Color.BLUE);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cutTest() {\n\t\tImgUtil.cut(FileUtil.file(\"d:/test/hutool.png\"),\n\t\t\tFileUtil.file(\"d:/test/result.png\"),\n\t\t\tnew Rectangle(0, 0, 400, 240));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cutTest2() {\n\t\tfinal Image cut = ImgUtil.cut(ImgUtil.read(\"d:/test/logo_small.jpg\"), 0, 0, 50);\n\t\tImgUtil.write(cut, FileUtil.file(\"d:/test/target.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void rotateTest() throws IOException {\n\t\tImage image = ImgUtil.rotate(ImageIO.read(FileUtil.file(\"e:/pic/366466.jpg\")), 180);\n\t\tImgUtil.write(image, FileUtil.file(\"e:/pic/result.png\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void flipTest() {\n\t\tImgUtil.flip(FileUtil.file(\"d:/logo.png\"), FileUtil.file(\"d:/result.png\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pressImgTest() {\n\t\tImgUtil.pressImage(\n\t\t\t\tFileUtil.file(\"d:/test/1435859438434136064.jpg\"),\n\t\t\t\tFileUtil.file(\"d:/test/dest.jpg\"),\n\t\t\t\tImgUtil.read(FileUtil.file(\"d:/test/qrcodeCustom.png\")), 0, 0, 0.9f);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pressTextTest() {\n\t\tImgUtil.pressText(//\n\t\t\t\tFileUtil.file(\"d:/test/2.jpg\"), //\n\t\t\t\tFileUtil.file(\"d:/test/2_result.png\"), //\n\t\t\t\t\"版权所有\", Color.RED, //\n\t\t\t\tnew Font(\"黑体\", Font.BOLD, 100), //\n\t\t\t\t0, //\n\t\t\t\t0, //\n\t\t\t\t1f);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sliceByRowsAndColsTest() {\n\t\tImgUtil.sliceByRowsAndCols(FileUtil.file(\"d:/temp/2.png\"), FileUtil.file(\"d:/temp/slice/png\"),ImgUtil.IMAGE_TYPE_PNG, 1, 5);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sliceByRowsAndColsTest2() {\n\t\tImgUtil.sliceByRowsAndCols(\n\t\t\tFileUtil.file(\"d:/test/hutool.png\"),\n\t\t\tFileUtil.file(\"d:/test/dest\"), ImgUtil.IMAGE_TYPE_PNG, 1, 5);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void convertTest() {\n\t\tImgUtil.convert(FileUtil.file(\"e:/test2.png\"), FileUtil.file(\"e:/test2Convert.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tfinal byte[] bytes = ImgUtil.toBytes(ImgUtil.read(\"d:/test/logo_484.png\"), \"png\");\n\t\tFileUtil.writeBytes(bytes, \"d:/test/result.png\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void compressTest() {\n\t\tImgUtil.compress(FileUtil.file(\"d:/test/dest.png\"),\n\t\t\t\tFileUtil.file(\"d:/test/1111_target.jpg\"), 0.1f);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyTest() {\n\t\tBufferedImage image = ImgUtil.copyImage(ImgUtil.read(\"f:/pic/test.png\"), BufferedImage.TYPE_INT_RGB);\n\t\tImgUtil.write(image, FileUtil.file(\"f:/pic/test_dest.jpg\"));\n\t}\n\n\t@Test\n\tpublic void toHexTest(){\n\t\tfinal String s = ImgUtil.toHex(Color.RED);\n\t\tassertEquals(\"#FF0000\", s);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void backgroundRemovalTest() {\n\t\t// 图片 背景 换成 透明的\n\t\tImgUtil.backgroundRemoval(\n\t\t\t\t\"d:/test/617180969474805871.jpg\",\n\t\t\t\t\"d:/test/2.jpg\", 10);\n\n\t\t// 图片 背景 换成 红色的\n\t\tImgUtil.backgroundRemoval(new File(\n\t\t\t\t\"d:/test/617180969474805871.jpg\"),\n\t\t\t\tnew File(\"d:/test/3.jpg\"),\n\t\t\t\tnew Color(200, 0, 0), 10);\n\t}\n\n\t@Test\n\tpublic void getMainColor() throws MalformedURLException {\n\t\tBufferedImage read = ImgUtil.read(new URL(\"https://pic2.zhimg.com/v2-94f5552f2b142ff575306850c5bab65d_b.png\"));\n\t\tString mainColor = ImgUtil.getMainColor(read, new int[]{64,84,116});\n\t\tSystem.out.println(mainColor);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void createImageTest() throws IORuntimeException, IOException {\n\t\tImgUtil.createImage(\n\t\t\t\t\"版权所有\",\n\t\t\t\tnew Font(\"黑体\", Font.BOLD, 50),\n\t\t\t\tColor.WHITE,\n\t\t\t\tColor.BLACK,\n\t\t\t\tImageIO.createImageOutputStream(new File(\"d:/test/createImageTest.png\"))\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void createTransparentImageTest() throws IORuntimeException, IOException {\n\t\tImgUtil.createTransparentImage(\n\t\t\t\t\"版权所有\",\n\t\t\t\tnew Font(\"黑体\", Font.BOLD, 50),\n\t\t\t\tColor.BLACK,\n\t\t\t\tImageIO.createImageOutputStream(new File(\"d:/test/createTransparentImageTest.png\"))\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/img/Issue2735Test.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Image;\n\npublic class Issue2735Test {\n\n\t@Test\n\t@Disabled\n\tpublic void scaleTest() {\n\t\tfinal Img img = Img.from(FileUtil.file(\"d:/test/hutool.png\"))\n\t\t\t\t.scale(200, 200, Image.SCALE_DEFAULT);\n\n\t\timg.write(FileUtil.file(\"d:/test/dest3.png\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/img/IssueI8L8UATest.java",
    "content": "package cn.hutool.core.img;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI8L8UATest {\n\t@Test\n\t@Disabled\n\tpublic void convertTest() {\n\t\tImgUtil.convert(\n\t\t\tFileUtil.file(\"d:/test/1.png\"),\n\t\t\tFileUtil.file(\"d:/test/1.jpg\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/BufferUtilTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.ByteBuffer;\nimport java.nio.charset.StandardCharsets;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertArrayEquals;\n\n/**\n * BufferUtil单元测试\n *\n * @author looly\n *\n */\npublic class BufferUtilTest {\n\n\t@Test\n\tpublic void copyTest() {\n\t\tbyte[] bytes = \"AAABBB\".getBytes();\n\t\tByteBuffer buffer = ByteBuffer.wrap(bytes);\n\n\t\tByteBuffer buffer2 = BufferUtil.copy(buffer, ByteBuffer.allocate(5));\n\t\tassertEquals(\"AAABB\", StrUtil.utf8Str(buffer2));\n\t}\n\n\t@Test\n\tpublic void readBytesTest() {\n\t\tbyte[] bytes = \"AAABBB\".getBytes();\n\t\tByteBuffer buffer = ByteBuffer.wrap(bytes);\n\n\t\tbyte[] bs = BufferUtil.readBytes(buffer, 5);\n\t\tassertEquals(\"AAABB\", StrUtil.utf8Str(bs));\n\t}\n\n\t@Test\n\tpublic void readBytes2Test() {\n\t\tbyte[] bytes = \"AAABBB\".getBytes();\n\t\tByteBuffer buffer = ByteBuffer.wrap(bytes);\n\n\t\tbyte[] bs = BufferUtil.readBytes(buffer, 5);\n\t\tassertEquals(\"AAABB\", StrUtil.utf8Str(bs));\n\t}\n\n\t@Test\n\tpublic void readLineTest() {\n\t\tString text = \"aa\\r\\nbbb\\ncc\";\n\t\tByteBuffer buffer = ByteBuffer.wrap(text.getBytes());\n\n\t\t// 第一行\n\t\tString line = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"aa\", line);\n\n\t\t// 第二行\n\t\tline = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"bbb\", line);\n\n\t\t// 第三行因为没有行结束标志，因此返回null\n\t\tline = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8);\n\t\tassertNull(line);\n\n\t\t// 读取剩余部分\n\t\tassertEquals(\"cc\", StrUtil.utf8Str(BufferUtil.readBytes(buffer)));\n\t}\n\n\t@Test\n\tpublic void testByteBufferSideEffect() {\n\t\tString originalText = \"Hello\";\n\t\tByteBuffer buffer = ByteBuffer.wrap(originalText.getBytes(StandardCharsets.UTF_8));\n\t\t// 此时 buffer.remaining() == 5\n\t\tassertEquals(5, buffer.remaining());\n\n\t\t// 调用工具类转换，打印buffer内容\n\t\tString result = StrUtil.str(buffer, StandardCharsets.UTF_8);\n\t\tassertEquals(originalText, result);\n\n\t\t// 预期：\n\t\t// 工具类不应该修改原 buffer 的指针，remaining 应该依然为 5\n\t\t// 再次调用工具类转换，输出结果应该不变\n\t\tassertEquals(originalText, StrUtil.str(buffer, StandardCharsets.UTF_8));\n\t}\n\n\t/**\n\t * 测试正常范围内的拷贝功能\n\t */\n\t@Test\n\tpublic void copyNormalRangeTest() {\n\t\t// 准备测试数据\n\t\tfinal byte[] originalData = {65, 66, 67, 68, 69, 70}; // 对应 \"ABCDEF\"\n\t\tfinal ByteBuffer srcBuffer = ByteBuffer.wrap(originalData);\n\n\t\t// 执行拷贝操作，从索引1到4（不包含4），即拷贝BCD\n\t\tfinal ByteBuffer resultBuffer = BufferUtil.copy(srcBuffer, 1, 4);\n\n\t\t// 验证结果\n\t\tfinal byte[] resultArray = new byte[3];\n\t\tresultBuffer.get(resultArray);\n\t\tassertArrayEquals(new byte[]{66, 67, 68}, resultArray); // BCD\n\t}\n\n\t/**\n\t * 测试从开头开始拷贝\n\t */\n\t@Test\n\tpublic void copyFromStartTest() {\n\t\tfinal byte[] originalData = {65, 66, 67, 68, 69, 70}; // 对应 \"ABCDEF\"\n\t\tfinal ByteBuffer srcBuffer = ByteBuffer.wrap(originalData);\n\n\t\t// 从索引0拷贝到3，即拷贝ABC\n\t\tfinal ByteBuffer resultBuffer = BufferUtil.copy(srcBuffer, 0, 3);\n\n\t\tfinal byte[] resultArray = new byte[3];\n\t\tresultBuffer.get(resultArray);\n\t\tassertArrayEquals(new byte[]{65, 66, 67}, resultArray); // ABC\n\t}\n\n\t/**\n\t * 测试拷贝到末尾\n\t */\n\t@Test\n\tpublic void copyToEndTest() {\n\t\tfinal byte[] originalData = {65, 66, 67, 68, 69, 70}; // 对应 \"ABCDEF\"\n\t\tfinal ByteBuffer srcBuffer = ByteBuffer.wrap(originalData);\n\n\t\t// 从索引3拷贝到末尾，即拷贝DEF\n\t\tfinal ByteBuffer resultBuffer = BufferUtil.copy(srcBuffer, 3, 6);\n\n\t\tfinal byte[] resultArray = new byte[3];\n\t\tresultBuffer.get(resultArray);\n\t\tassertArrayEquals(new byte[]{68, 69, 70}, resultArray); // DEF\n\t}\n\n\t/**\n\t * 测试空拷贝（start等于end）\n\t */\n\t@Test\n\tpublic void copyEmptyRangeTest() {\n\t\tfinal byte[] originalData = {65, 66, 67, 68, 69, 70}; // 对应 \"ABCDEF\"\n\t\tfinal ByteBuffer srcBuffer = ByteBuffer.wrap(originalData);\n\n\t\t// 拷贝相同起始和结束位置，应该得到空数组\n\t\tfinal ByteBuffer resultBuffer = BufferUtil.copy(srcBuffer, 2, 2);\n\n\t\tassertEquals(0, resultBuffer.remaining()); // 应该为空\n\t}\n\n\t/**\n\t * 测试整个数组的拷贝\n\t */\n\t@Test\n\tpublic void copyFullRangeTest() {\n\t\tfinal byte[] originalData = {65, 66, 67, 68, 69, 70}; // 对应 \"ABCDEF\"\n\t\tfinal ByteBuffer srcBuffer = ByteBuffer.wrap(originalData);\n\n\t\t// 拷贝整个数组\n\t\tfinal ByteBuffer resultBuffer = BufferUtil.copy(srcBuffer, 0, 6);\n\n\t\tfinal byte[] resultArray = new byte[6];\n\t\tresultBuffer.get(resultArray);\n\t\tassertArrayEquals(originalData, resultArray);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/CharsetDetectorTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.Charset;\n\npublic class CharsetDetectorTest {\n\n\t@Test\n\tpublic void detectTest(){\n\t\t// 测试多个Charset对同一个流的处理是否有问题\n\t\tfinal Charset detect = CharsetDetector.detect(ResourceUtil.getStream(\"test.xml\"),\n\t\t\t\tCharsetUtil.CHARSET_GBK, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(CharsetUtil.CHARSET_UTF_8, detect);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issue2547() {\n\t\tfinal Charset detect = CharsetDetector.detect(IoUtil.DEFAULT_LARGE_BUFFER_SIZE,\n\t\t\t\tResourceUtil.getStream(\"d:/test/default.txt\"));\n\t\tassertEquals(CharsetUtil.CHARSET_UTF_8, detect);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/ClassPathResourceTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.resource.ClassPathResource;\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.Properties;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * ClassPath资源读取测试\n *\n * @author Looly\n */\npublic class ClassPathResourceTest {\n\n\t@Test\n\tpublic void readStringTest() {\n\t\tClassPathResource resource = new ClassPathResource(\"test.properties\");\n\t\tString content = resource.readUtf8Str();\n\t\tassertTrue(StrUtil.isNotEmpty(content));\n\t}\n\n\t@Test\n\tpublic void readStringTest2() {\n\t\t// 读取classpath根目录测试\n\t\tClassPathResource resource = new ClassPathResource(\"/\");\n\t\tString content = resource.readUtf8Str();\n\t\tassertTrue(StrUtil.isNotEmpty(content));\n\t}\n\n\t@Test\n\tpublic void readTest() throws IOException {\n\t\tClassPathResource resource = new ClassPathResource(\"test.properties\");\n\t\tProperties properties = new Properties();\n\t\tproperties.load(resource.getStream());\n\n\t\tassertEquals(\"1\", properties.get(\"a\"));\n\t\tassertEquals(\"2\", properties.get(\"b\"));\n\t}\n\n\t@Test\n\tpublic void readFromJarTest() {\n\t\t//测试读取junit的jar包下的LICENSE.md文件\n\t\tfinal ClassPathResource resource = new ClassPathResource(\"META-INF/LICENSE.md\");\n\n\t\tString result = resource.readUtf8Str();\n\t\tassertNotNull(result);\n\n\t\t//二次读取测试，用于测试关闭流对再次读取的影响\n\t\tresult = resource.readUtf8Str();\n\t\tassertNotNull(result);\n\t}\n\n\t@Test\n\tpublic void getAbsTest() {\n\t\t// lombok 文件\n\t\tfinal ClassPathResource resource = new ClassPathResource(\"changelog.txt\");\n\t\tString absPath = resource.getAbsolutePath();\n\t\tassertTrue(absPath.contains(\"changelog.txt\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.file.FileCopier;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * 文件拷贝单元测试\n *\n * @author Looly\n */\npublic class FileCopierTest {\n\n\t@Test\n\t@Disabled\n\tpublic void dirCopyTest() {\n\t\tFileCopier copier = FileCopier.create(\"D:\\\\Java\", \"e:/eclipse/eclipse2.zip\");\n\t\tcopier.copy();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void dirCopyTest2() {\n\t\t//测试带.的文件夹复制\n\t\tFileCopier copier = FileCopier.create(\"D:\\\\workspace\\\\java\\\\.metadata\", \"D:\\\\workspace\\\\java\\\\.metadata\\\\temp\");\n\t\tcopier.copy();\n\n\t\tFileUtil.copy(\"D:\\\\workspace\\\\java\\\\looly\\\\hutool\\\\.git\", \"D:\\\\workspace\\\\java\\\\temp\", true);\n\t}\n\n\t@Test\n\tpublic void dirCopySubTest() {\n\t\tassertThrows(IORuntimeException.class, () -> {\n\t\t\t//测试父目录复制到子目录报错\n\t\t\tFileCopier copier = FileCopier.create(\"D:\\\\workspace\\\\java\\\\.metadata\", \"D:\\\\workspace\\\\java\\\\.metadata\\\\temp\");\n\t\t\tcopier.copy();\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyFileToDirTest() {\n\t\tFileCopier copier = FileCopier.create(\"d:/GReen_Soft/XshellXftpPortable.zip\", \"c:/hp/\");\n\t\tcopier.copy();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyFileByRelativePath(){\n\t\t// https://github.com/chinabugotech/hutool/pull/2188\n\t\t//  当复制的目标文件位置是相对路径的时候可以通过\n\t\tFileCopier copier = FileCopier.create(new File(\"pom.xml\"),new File(\"aaa.txt\"));\n\t\tcopier.copy();\n\t\tfinal boolean delete = new File(\"aaa.txt\").delete();\n\t\tassertTrue(delete);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/FileReaderTest.java",
    "content": "package cn.hutool.core.io;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.file.FileReader;\n\n/**\n * 文件读取测试\n * @author Looly\n *\n */\npublic class FileReaderTest {\n\n\t@Test\n\tpublic void fileReaderTest(){\n\t\tFileReader fileReader = new FileReader(\"test.properties\");\n\t\tString result = fileReader.readString();\n\t\tassertNotNull(result);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/FileTypeUtilTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.*;\n\n/**\n * 文件类型判断单元测试\n * @author Looly\n *\n */\npublic class FileTypeUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void fileTypeUtilTest() {\n\t\tfinal File file = FileUtil.file(\"hutool.jpg\");\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tassertEquals(\"jpg\", type);\n\n\t\tFileTypeUtil.putFileType(\"ffd8ffe000104a464946\", \"new_jpg\");\n\t\tfinal String newType = FileTypeUtil.getType(file);\n\t\tassertEquals(\"new_jpg\", newType);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void emptyTest() {\n\t\tfinal File file = FileUtil.file(\"d:/empty.txt\");\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tConsole.log(type);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void docTest() {\n\t\tfinal File file = FileUtil.file(\"f:/test/test.doc\");\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tConsole.log(type);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void ofdTest() {\n\t\tfinal File file = FileUtil.file(\"e:/test.ofd\");\n\t\tfinal String hex = IoUtil.readHex64Upper(FileUtil.getInputStream(file));\n\t\tConsole.log(hex);\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tConsole.log(type);\n\t\tassertEquals(\"ofd\", type);\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void inputStreamAndFilenameTest() {\n\t\tfinal File file = FileUtil.file(\"e:/laboratory/test.xlsx\");\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tassertEquals(\"xlsx\", type);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTypeFromInputStream() throws IOException {\n\t\tfinal File file = FileUtil.file(\"d:/test/pic.jpg\");\n\t\tfinal BufferedInputStream inputStream = FileUtil.getInputStream(file);\n\t\tinputStream.mark(0);\n\t\tfinal String type = FileTypeUtil.getType(inputStream);\n\n\t\tinputStream.reset();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void webpTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I5BGTF\n\t\tfinal File file = FileUtil.file(\"d:/test/a.webp\");\n\t\tfinal BufferedInputStream inputStream = FileUtil.getInputStream(file);\n\t\tfinal String type = FileTypeUtil.getType(inputStream);\n\t\tConsole.log(type);\n\t}\n\n\t@Test\n\tpublic void issueI6MACITest() {\n\t\tfinal File file = FileUtil.file(\"text.txt\");\n\t\tfinal String type = FileTypeUtil.getType(file);\n\t\tassertEquals(\"txt\", type);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issue3024Test() {\n\t\tString x = FileTypeUtil.getType(FileUtil.getInputStream(\"d:/test/TEST_WPS_DOC.doc\"),true);\n\t\tSystem.out.println(x);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.file.LineSeparator;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.EnabledOnOs;\nimport org.junit.jupiter.api.condition.OS;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link FileUtil} 单元测试类\n *\n * @author Looly\n */\npublic class FileUtilTest {\n\n\t@Test\n\tpublic void fileTest1() {\n\t\tfinal File file = FileUtil.file(\"d:/aaa\", \"bbb\");\n\t\tassertNotNull(file);\n\t}\n\n\t@Test\n\tpublic void fileTest2() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal File file = FileUtil.file(\"d:/aaa\", \"bbb\");\n\t\t\tassertNotNull(file);\n\n\t\t\t// 构建目录中出现非子目录抛出异常\n\t\t\tFileUtil.file(file, \"../ccc\");\n\n\t\t\tFileUtil.file(\"E:/\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void getAbsolutePathTest() {\n\t\tfinal String absolutePath = FileUtil.getAbsolutePath(\"LICENSE-junit.txt\");\n\t\tassertNotNull(absolutePath);\n\t\tfinal String absolutePath2 = FileUtil.getAbsolutePath(absolutePath);\n\t\tassertNotNull(absolutePath2);\n\t\tassertEquals(absolutePath, absolutePath2);\n\n\t\tString path = FileUtil.getAbsolutePath(\"中文.xml\");\n\t\tassertTrue(path.contains(\"中文.xml\"));\n\n\t\tpath = FileUtil.getAbsolutePath(\"d:\");\n\t\tassertEquals(\"d:\", path);\n\n\t}\n\n\t@Test\n\t@EnabledOnOs(OS.WINDOWS)\n\tpublic void smbPathTest() {\n\t\tfinal String smbPath = \"\\\\\\\\192.168.1.1\\\\share\\\\rc-source\";\n\t\tfinal String parseSmbPath = FileUtil.getAbsolutePath(smbPath);\n\t\tassertEquals(smbPath, parseSmbPath);\n\t\tassertTrue(FileUtil.isAbsolutePath(smbPath));\n\t\tif(FileUtil.isWindows()){\n\t\t\t// 在Windows下`\\`路径是绝对路径，也表示SMB路径\n\t\t\t// 但是在Linux下，`\\`表示转义字符，并不被识别为路径\n\t\t\tassertTrue(Paths.get(smbPath).isAbsolute());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void touchTest() {\n\t\tFileUtil.touch(\"d:\\\\tea\\\\a.jpg\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delTest() {\n\t\t// 删除一个不存在的文件，应返回true\n\t\tfinal boolean result = FileUtil.del(\"e:/Hutool_test_3434543533409843.txt\");\n\t\tassertTrue(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delTest2() {\n\t\t// 删除一个不存在的文件，应返回true\n\t\tfinal boolean result = FileUtil.del(Paths.get(\"e:/Hutool_test_3434543533409843.txt\"));\n\t\tassertTrue(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameTest() {\n\t\tFileUtil.rename(FileUtil.file(\"d:/test/3.jpg\"), \"2.jpg\", false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameTest2() {\n\t\tFileUtil.move(FileUtil.file(\"d:/test/a\"), FileUtil.file(\"d:/test/b\"), false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameTest3() {\n\t\tFileUtil.rename(FileUtil.file(\"d:/test/test2.xlsx\"), \"test3.xlsx\", true);\n\t}\n\n\t@Test\n\tpublic void copyTest() {\n\t\tfinal File srcFile = FileUtil.file(\"hutool.jpg\");\n\t\tfinal File destFile = FileUtil.file(\"hutool.copy.jpg\");\n\n\t\tFileUtil.copy(srcFile, destFile, true);\n\n\t\tassertTrue(destFile.exists());\n\t\tassertEquals(srcFile.length(), destFile.length());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyFilesFromDirTest() {\n\t\tfinal File srcFile = FileUtil.file(\"D:\\\\驱动\");\n\t\tfinal File destFile = FileUtil.file(\"d:\\\\驱动备份\");\n\n\t\tFileUtil.copyFilesFromDir(srcFile, destFile, true);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyDirTest() {\n\t\tfinal File srcFile = FileUtil.file(\"D:\\\\test\");\n\t\tfinal File destFile = FileUtil.file(\"E:\\\\\");\n\n\t\tFileUtil.copy(srcFile, destFile, true);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void moveDirTest() {\n\t\tfinal File srcFile = FileUtil.file(\"E:\\\\test2\");\n\t\tfinal File destFile = FileUtil.file(\"D:\\\\\");\n\n\t\tFileUtil.move(srcFile, destFile, true);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameToSubTest() {\n\t\tassertThrows(IllegalArgumentException.class, ()->{\n\t\t\t// 移动到子目录，报错\n\t\t\tFileUtil.move(FileUtil.file(\"d:/test/a\"), FileUtil.file(\"d:/test/a/c\"), false);\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameSameTest() {\n\t\t// 目标和源相同，不处理\n\t\tFileUtil.move(FileUtil.file(\"d:/test/a\"), FileUtil.file(\"d:/test/a\"), false);\n\t}\n\n\t@Test\n\tpublic void equalsTest() {\n\t\t// 源文件和目标文件都不存在\n\t\tfinal File srcFile = FileUtil.file(\"d:/hutool.jpg\");\n\t\tfinal File destFile = FileUtil.file(\"d:/hutool.jpg\");\n\n\t\tfinal boolean equals = FileUtil.equals(srcFile, destFile);\n\t\tassertTrue(equals);\n\n\t\t// 源文件存在，目标文件不存在\n\t\tfinal File srcFile1 = FileUtil.file(\"hutool.jpg\");\n\t\tfinal File destFile1 = FileUtil.file(\"d:/hutool.jpg\");\n\n\t\tfinal boolean notEquals = FileUtil.equals(srcFile1, destFile1);\n\t\tassertFalse(notEquals);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void convertLineSeparatorTest() {\n\t\tFileUtil.convertLineSeparator(FileUtil.file(\"d:/aaa.txt\"), CharsetUtil.CHARSET_UTF_8, LineSeparator.WINDOWS);\n\t}\n\n\t@Test\n\tpublic void normalizeTest() {\n\t\tassertEquals(\"/foo/\", FileUtil.normalize(\"/foo//\"));\n\t\tassertEquals(\"/foo/\", FileUtil.normalize(\"/foo/./\"));\n\t\tassertEquals(\"/bar\", FileUtil.normalize(\"/foo/../bar\"));\n\t\tassertEquals(\"/bar/\", FileUtil.normalize(\"/foo/../bar/\"));\n\t\tassertEquals(\"/baz\", FileUtil.normalize(\"/foo/../bar/../baz\"));\n\t\tassertEquals(\"/\", FileUtil.normalize(\"/../\"));\n\t\tassertEquals(\"foo\", FileUtil.normalize(\"foo/bar/..\"));\n\t\tassertEquals(\"../bar\", FileUtil.normalize(\"foo/../../bar\"));\n\t\tassertEquals(\"bar\", FileUtil.normalize(\"foo/../bar\"));\n\t\tassertEquals(\"/server/bar\", FileUtil.normalize(\"//server/foo/../bar\"));\n\t\tassertEquals(\"/bar\", FileUtil.normalize(\"//server/../bar\"));\n\t\tassertEquals(\"C:/bar\", FileUtil.normalize(\"C:\\\\foo\\\\..\\\\bar\"));\n\t\t//\n\t\tassertEquals(\"C:/bar\", FileUtil.normalize(\"C:\\\\..\\\\bar\"));\n\t\tassertEquals(\"../../bar\", FileUtil.normalize(\"../../bar\"));\n\t\tassertEquals(\"C:/bar\", FileUtil.normalize(\"/C:/bar\"));\n\t\tassertEquals(\"C:\", FileUtil.normalize(\"C:\"));\n\n\t\t// issue#3253，smb保留格式\n\t\tassertEquals(\"\\\\\\\\192.168.1.1\\\\Share\\\\\", FileUtil.normalize(\"\\\\\\\\192.168.1.1\\\\Share\\\\\"));\n\t}\n\n\t@Test\n\tpublic void normalizeBlankTest() {\n\t\tassertEquals(\"C:/aaa \", FileUtil.normalize(\"C:\\\\aaa \"));\n\t}\n\n\t@Test\n\tpublic void normalizeHomePathTest() {\n\t\tfinal String home = FileUtil.getUserHomePath().replace('\\\\', '/');\n\t\tassertEquals(home + \"/bar/\", FileUtil.normalize(\"~/foo/../bar/\"));\n\t}\n\n\t@Test\n\tpublic void normalizeHomePathTest2() {\n\t\tfinal String home = FileUtil.getUserHomePath().replace('\\\\', '/');\n\t\t// 多个~应该只替换开头的\n\t\tassertEquals(home + \"/~bar/\", FileUtil.normalize(\"~/foo/../~bar/\"));\n\t}\n\n\t@Test\n\tpublic void normalizeClassPathTest() {\n\t\tassertEquals(\"\", FileUtil.normalize(\"classpath:\"));\n\t}\n\n\t@Test\n\tpublic void normalizeClassPathTest2() {\n\t\tassertEquals(\"../a/b.csv\", FileUtil.normalize(\"../a/b.csv\"));\n\t\tassertEquals(\"../../../a/b.csv\", FileUtil.normalize(\"../../../a/b.csv\"));\n\t}\n\n\t@Test\n\tpublic void doubleNormalizeTest() {\n\t\tfinal String normalize = FileUtil.normalize(\"/aa/b:/c\");\n\t\tfinal String normalize2 = FileUtil.normalize(normalize);\n\t\tassertEquals(\"/aa/b:/c\", normalize);\n\t\tassertEquals(normalize, normalize2);\n\t}\n\n\t@Test\n\tpublic void subPathTest() {\n\t\tfinal Path path = Paths.get(\"/aaa/bbb/ccc/ddd/eee/fff\");\n\n\t\tPath subPath = FileUtil.subPath(path, 5, 4);\n\t\tassertEquals(\"eee\", subPath.toString());\n\t\tsubPath = FileUtil.subPath(path, 0, 1);\n\t\tassertEquals(\"aaa\", subPath.toString());\n\t\tsubPath = FileUtil.subPath(path, 1, 0);\n\t\tassertEquals(\"aaa\", subPath.toString());\n\n\t\t// 负数\n\t\tsubPath = FileUtil.subPath(path, -1, 0);\n\t\tassertEquals(\"aaa/bbb/ccc/ddd/eee\", subPath.toString().replace('\\\\', '/'));\n\t\tsubPath = FileUtil.subPath(path, -1, Integer.MAX_VALUE);\n\t\tassertEquals(\"fff\", subPath.toString());\n\t\tsubPath = FileUtil.subPath(path, -1, path.getNameCount());\n\t\tassertEquals(\"fff\", subPath.toString());\n\t\tsubPath = FileUtil.subPath(path, -2, -3);\n\t\tassertEquals(\"ddd\", subPath.toString());\n\t}\n\n\t@Test\n\tpublic void subPathTest2() {\n\t\tString subPath = FileUtil.subPath(\"d:/aaa/bbb/\", \"d:/aaa/bbb/ccc/\");\n\t\tassertEquals(\"ccc/\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb\", \"d:/aaa/bbb/ccc/\");\n\t\tassertEquals(\"ccc/\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb\", \"d:/aaa/bbb/ccc/test.txt\");\n\t\tassertEquals(\"ccc/test.txt\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb/\", \"d:/aaa/bbb/ccc\");\n\t\tassertEquals(\"ccc\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb\", \"d:/aaa/bbb/ccc\");\n\t\tassertEquals(\"ccc\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb\", \"d:/aaa/bbb\");\n\t\tassertEquals(\"\", subPath);\n\n\t\tsubPath = FileUtil.subPath(\"d:/aaa/bbb/\", \"d:/aaa/bbb\");\n\t\tassertEquals(\"\", subPath);\n\t}\n\n\t@Test\n\tpublic void getPathEle() {\n\t\tfinal Path path = Paths.get(\"/aaa/bbb/ccc/ddd/eee/fff\");\n\n\t\tPath ele = FileUtil.getPathEle(path, -1);\n\t\tassertEquals(\"fff\", ele.toString());\n\t\tele = FileUtil.getPathEle(path, 0);\n\t\tassertEquals(\"aaa\", ele.toString());\n\t\tele = FileUtil.getPathEle(path, -5);\n\t\tassertEquals(\"bbb\", ele.toString());\n\t\tele = FileUtil.getPathEle(path, -6);\n\t\tassertEquals(\"aaa\", ele.toString());\n\t}\n\n\t@Test\n\tpublic void listFileNamesTest() {\n\t\tList<String> names = FileUtil.listFileNames(\"classpath:\");\n\t\tassertTrue(names.contains(\"hutool.jpg\"));\n\n\t\tnames = FileUtil.listFileNames(\"\");\n\t\tassertTrue(names.contains(\"hutool.jpg\"));\n\n\t\tnames = FileUtil.listFileNames(\".\");\n\t\tassertTrue(names.contains(\"hutool.jpg\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void listFileNamesInJarTest() {\n\t\tfinal List<String> names = FileUtil.listFileNames(\"d:/test/hutool-core-5.1.0.jar!/cn/hutool/core/util \");\n\t\tfor (final String name : names) {\n\t\t\tConsole.log(name);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void listFileNamesTest2() {\n\t\tfinal List<String> names = FileUtil.listFileNames(\"D:\\\\m2_repo\\\\commons-cli\\\\commons-cli\\\\1.0\\\\commons-cli-1.0.jar!org/apache/commons/cli/\");\n\t\tfor (final String string : names) {\n\t\t\tConsole.log(string);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void loopFilesTest() {\n\t\tfinal List<File> files = FileUtil.loopFiles(\"d:/\");\n\t\tfor (final File file : files) {\n\t\t\tConsole.log(file.getPath());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void loopFilesTest2() {\n\t\tFileUtil.loopFiles(\"\").forEach(Console::log);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void loopFilesWithDepthTest() {\n\t\tfinal List<File> files = FileUtil.loopFiles(FileUtil.file(\"d:/m2_repo\"), 2, null);\n\t\tfor (final File file : files) {\n\t\t\tConsole.log(file.getPath());\n\t\t}\n\t}\n\n\t@Test\n\tpublic void getParentTest() {\n\t\t// 只在Windows下测试\n\t\tif (FileUtil.isWindows()) {\n\t\t\tFile parent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 0);\n\t\t\tassertEquals(FileUtil.file(\"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\"), parent);\n\n\t\t\tparent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 1);\n\t\t\tassertEquals(FileUtil.file(\"d:\\\\aaa\\\\bbb\\\\cc\"), parent);\n\n\t\t\tparent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 2);\n\t\t\tassertEquals(FileUtil.file(\"d:\\\\aaa\\\\bbb\"), parent);\n\n\t\t\tparent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 4);\n\t\t\tassertEquals(FileUtil.file(\"d:\\\\\"), parent);\n\n\t\t\tparent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 5);\n\t\t\tassertNull(parent);\n\n\t\t\tparent = FileUtil.getParent(FileUtil.file(\"d:/aaa/bbb/cc/ddd\"), 10);\n\t\t\tassertNull(parent);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void lastIndexOfSeparatorTest() {\n\t\tfinal String dir = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\";\n\t\tfinal int index = FileUtil.lastIndexOfSeparator(dir);\n\t\tassertEquals(13, index);\n\n\t\tfinal String file = \"ddd.jpg\";\n\t\tfinal int index2 = FileUtil.lastIndexOfSeparator(file);\n\t\tassertEquals(-1, index2);\n\t}\n\n\t@Test\n\tpublic void getNameTest() {\n\t\tString path = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\\\\\";\n\t\tString name = FileUtil.getName(path);\n\t\tassertEquals(\"ddd\", name);\n\n\t\tpath = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd.jpg\";\n\t\tname = FileUtil.getName(path);\n\t\tassertEquals(\"ddd.jpg\", name);\n\t}\n\n\t@Test\n\tpublic void mainNameTest() {\n\t\tString path = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\\\\\";\n\t\tString mainName = FileUtil.mainName(path);\n\t\tassertEquals(\"ddd\", mainName);\n\n\t\tpath = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\";\n\t\tmainName = FileUtil.mainName(path);\n\t\tassertEquals(\"ddd\", mainName);\n\n\t\tpath = \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd.jpg\";\n\t\tmainName = FileUtil.mainName(path);\n\t\tassertEquals(\"ddd\", mainName);\n\t}\n\n\t@Test\n\tpublic void extNameTest() {\n\t\tString path =  FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\\\\\" : \"~/Desktop/hutool/ddd/\";\n\t\tString mainName = FileUtil.extName(path);\n\t\tassertEquals(\"\", mainName);\n\n\t\tpath =  FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd\" : \"~/Desktop/hutool/ddd\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\ddd.jpg\" : \"~/Desktop/hutool/ddd.jpg\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"jpg\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\fff.xlsx\" : \"~/Desktop/hutool/fff.xlsx\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"xlsx\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\fff.tar.gz\" : \"~/Desktop/hutool/fff.tar.gz\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"tar.gz\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\fff.tar.Z\" : \"~/Desktop/hutool/fff.tar.Z\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"tar.Z\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\fff.tar.bz2\" : \"~/Desktop/hutool/fff.tar.bz2\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"tar.bz2\", mainName);\n\n\t\tpath = FileUtil.isWindows() ? \"d:\\\\aaa\\\\bbb\\\\cc\\\\fff.tar.xz\" : \"~/Desktop/hutool/fff.tar.xz\";\n\t\tmainName = FileUtil.extName(path);\n\t\tassertEquals(\"tar.xz\", mainName);\n\t}\n\n\t@Test\n\tpublic void getWebRootTest() {\n\t\tfinal File webRoot = FileUtil.getWebRoot();\n\t\tassertNotNull(webRoot);\n\t\tassertEquals(\"hutool-core\", webRoot.getName());\n\t}\n\n\t@Test\n\tpublic void getMimeTypeTest() {\n\t\tString mimeType = FileUtil.getMimeType(\"test2Write.jpg\");\n\t\tassertEquals(\"image/jpeg\", mimeType);\n\n\t\tmimeType = FileUtil.getMimeType(\"test2Write.html\");\n\t\tassertEquals(\"text/html\", mimeType);\n\n\t\tmimeType = FileUtil.getMimeType(\"main.css\");\n\t\tassertEquals(\"text/css\", mimeType);\n\n\t\tmimeType = FileUtil.getMimeType(\"test.js\");\n\t\t// 在 jdk 11+ 会获取到 text/javascript,而非 自定义的 application/x-javascript\n\t\tfinal List<String> list = ListUtil.of(\"text/javascript\", \"application/x-javascript\");\n\t\tassertTrue(list.contains(mimeType));\n\n\t\tif(FileUtil.isWindows()){\n\t\t\t// Linux下的OpenJDK无法正确识别\n\n\t\t\t// office03\n\t\t\tmimeType = FileUtil.getMimeType(\"test.doc\");\n\t\t\tassertEquals(\"application/msword\", mimeType);\n\t\t\tmimeType = FileUtil.getMimeType(\"test.xls\");\n\t\t\tassertEquals(\"application/vnd.ms-excel\", mimeType);\n\t\t\tmimeType = FileUtil.getMimeType(\"test.ppt\");\n\t\t\tassertEquals(\"application/vnd.ms-powerpoint\", mimeType);\n\n\t\t\t// office07+\n\t\t\tmimeType = FileUtil.getMimeType(\"test.docx\");\n\t\t\tassertEquals(\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\", mimeType);\n\t\t\tmimeType = FileUtil.getMimeType(\"test.xlsx\");\n\t\t\tassertEquals(\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", mimeType);\n\t\t\tmimeType = FileUtil.getMimeType(\"test.pptx\");\n\t\t\tassertEquals(\"application/vnd.openxmlformats-officedocument.presentationml.presentation\", mimeType);\n\t\t}\n\n\n\t\t// pr#2617@Github\n\t\tmimeType = FileUtil.getMimeType(\"test.wgt\");\n\t\tassertEquals(\"application/widget\", mimeType);\n\n\t\t// issue#3092\n\t\tmimeType = FileUtil.getMimeType(\"https://xxx.oss-cn-hangzhou.aliyuncs.com/xxx.webp\");\n\t\tassertEquals(\"image/webp\", mimeType);\n\t}\n\n\t@Test\n\tpublic void isSubTest() {\n\t\tfinal File file = new File(\"d:/test\");\n\t\tfinal File file2 = new File(\"d:/test2/aaa\");\n\t\tassertFalse(FileUtil.isSub(file, file2));\n\t}\n\n\t@Test\n\tpublic void isSubRelativeTest() {\n\t\tfinal File file = new File(\"..\");\n\t\tfinal File file2 = new File(\".\");\n\t\tassertTrue(FileUtil.isSub(file, file2));\n\t}\n\n\t@Test\n\tpublic void isSub_SubIsAncestorOfParentTest() {\n\t\tFile parent = new File(\"d:/home/user/docs/notes\");\n\t\tFile sub = new File(\"d:/home/user/docs\");\n\t\tassertFalse(FileUtil.isSub(parent, sub));\n\t}\n\n\t@Test\n\tpublic void isSub_SamePathTest() {\n\t\tFile parent = new File(\"d:/home/user/docs\");\n\t\tFile sub = new File(\"d:/home/user/docs\");\n\t\tassertTrue(FileUtil.isSub(parent, sub));\n\t}\n\n\t@Test\n\tpublic void isSub_NonexistentPathsTest() {\n\t\tFile parent = new File(\"d:/unlikely/to/exist/parent\");\n\t\tFile sub = new File(\"d:/unlikely/to/exist/parent/child/file.txt\");\n\t\tassertTrue(FileUtil.isSub(parent, sub));\n\n\t\tFile nonchild = new File(\"d:/also/unlikely/path.txt\");\n\t\tassertFalse(FileUtil.isSub(parent, nonchild));\n\t}\n\n\t@Test\n\tpublic void isSub_NullParentTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tFileUtil.isSub(null, new java.io.File(\"d:/any/path\"));\n\t\t});\n\t}\n\n\t@Test\n\tpublic void isSub_NullSubTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tFileUtil.isSub(new java.io.File(\"d:/any/path\"), null);\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void appendLinesTest(){\n\t\tfinal List<String> list = ListUtil.toList(\"a\", \"b\", \"c\");\n\t\tFileUtil.appendLines(list, FileUtil.file(\"d:/test/appendLines.txt\"), CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void createTempFileTest(){\n\t\tfinal File nullDirTempFile = FileUtil.createTempFile();\n\t\tassertTrue(nullDirTempFile.exists());\n\n\t\tfinal File suffixDirTempFile = FileUtil.createTempFile(\".xlsx\",true);\n\t\tassertEquals(\"xlsx\", FileUtil.getSuffix(suffixDirTempFile));\n\n\t\tfinal File prefixDirTempFile = FileUtil.createTempFile(\"prefix\",\".xlsx\",true);\n\t\tassertTrue(FileUtil.getPrefix(prefixDirTempFile).startsWith(\"prefix\"));\n\t}\n\n\t@Test\n\tpublic void getTotalLinesTest() {\n\t\t// 此文件最后一行有换行符，则最后的空行算作一行\n\t\tint totalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines.csv\"));\n\t\tassertEquals(8, totalLines);\n\n\t\ttotalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines.csv\"), -1, false);\n\t\tAssertions.assertEquals(7, totalLines);\n\t}\n\n\t@Test\n\tpublic void getTotalLinesCrTest() {\n\t\t// 此文件最后一行有换行符，则最后的空行算作一行\n\t\tint totalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines_cr.csv\"));\n\t\tassertEquals(8, totalLines);\n\n\t\ttotalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines_cr.csv\"), -1, false);\n\t\tAssertions.assertEquals(7, totalLines);\n\t}\n\n\t@Test\n\tpublic void getTotalLinesCrlfTest() {\n\t\t// 此文件最后一行有换行符，则最后的空行算作一行\n\t\tint totalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines_crlf.csv\"));\n\t\tassertEquals(8, totalLines);\n\n\t\ttotalLines = FileUtil.getTotalLines(FileUtil.file(\"test_lines_crlf.csv\"), -1, false);\n\t\tAssertions.assertEquals(7, totalLines);\n\t}\n\n\t@Test\n\tpublic void issue3591Test() {\n\t\t// 此文件最后一行末尾无换行符\n\t\tfinal int totalLines = FileUtil.getTotalLines(FileUtil.file(\"1_psi_index_0.txt\"));\n\t\tassertEquals(11, totalLines);\n\t}\n\n\t@Test\n\tpublic void isAbsolutePathTest(){\n\t\tString path = \"d:/test\\\\aaa.txt\";\n\t\tassertTrue(FileUtil.isAbsolutePath(path));\n\n\t\tpath = \"test\\\\aaa.txt\";\n\t\tassertFalse(FileUtil.isAbsolutePath(path));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyTest2(){\n\t\tfinal File copy = FileUtil.copy(\"d:/test/qrcodeCustom.png\", \"d:/test/pic\", false);\n\t\t// 当复制文件到目标目录的时候，返回复制的目标文件，而非目录\n\t\tConsole.log(copy);\n\t}\n\n\t@Test\n\tpublic void checkSlipTest() {\n\t\tassertThrows(IllegalArgumentException.class, ()->{\n\t\t\tFileUtil.checkSlip(FileUtil.file(\"test/a\"), FileUtil.file(\"test/../a\"));\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/IoUtilTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IoUtilTest {\n\n\t@Test\n\tpublic void readBytesTest() {\n\t\tfinal byte[] bytes = IoUtil.readBytes(ResourceUtil.getStream(\"hutool.jpg\"));\n\t\tassertEquals(22807, bytes.length);\n\t}\n\n\t@Test\n\tpublic void readBytesWithLengthTest() {\n\t\t// 读取固定长度\n\t\tfinal int limit = RandomUtil.randomInt(22807);\n\t\tfinal byte[] bytes = IoUtil.readBytes(ResourceUtil.getStream(\"hutool.jpg\"), limit);\n\t\tassertEquals(limit, bytes.length);\n\t}\n\n\t@Test\n\tpublic void readLinesTest() {\n\t\ttry (BufferedReader reader = ResourceUtil.getUtf8Reader(\"test_lines.csv\");) {\n\t\t\tIoUtil.readLines(reader, (LineHandler) Assertions::assertNotNull);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/Issue3846Test.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.StopWatch;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.TimeUnit;\n\npublic class Issue3846Test {\n\t@Test\n\t@Disabled\n\tvoid readBytesTest() {\n\t\tfinal StopWatch stopWatch = DateUtil.createStopWatch();\n\t\tstopWatch.start();\n\t\tfinal String filePath = \"d:/test/issue3846.data\";\n\t\tfinal byte[] bytes = IoUtil.readBytes(ResourceUtil.getStream(filePath), false);\n\t\tstopWatch.stop();\n\t\tConsole.log(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/ManifestUtilTest.java",
    "content": "package cn.hutool.core.io;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.jar.Manifest;\n\npublic class ManifestUtilTest {\n\n\t@Test\n\tpublic void getManiFestTest(){\n\t\tfinal Manifest manifest = ManifestUtil.getManifest(Test.class);\n\t\tassertNotNull(manifest);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/WatchMonitorTest.java",
    "content": "package cn.hutool.core.io;\n\nimport cn.hutool.core.io.watch.SimpleWatcher;\nimport cn.hutool.core.io.watch.WatchMonitor;\nimport cn.hutool.core.io.watch.Watcher;\nimport cn.hutool.core.io.watch.watchers.DelayWatcher;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\nimport java.nio.file.WatchKey;\n\n/**\n * 文件监听单元测试\n *\n * @author Looly\n */\npublic class WatchMonitorTest {\n\tWatchMonitor monitor;\n\tWatcher watcher = new SimpleWatcher() {\n\t\t@Override\n\t\tpublic void onCreate(final WatchEvent<?> event, final Path currentPath) {\n\t\t\tfinal Object obj = event.context();\n\t\t\tConsole.log(\"创建：{}-> {}\", currentPath, obj);\n\t\t\tfinal WatchKey watchKey = monitor.getWatchKey(currentPath);\n\t\t\tfinal Path watchable = (Path) watchKey.watchable();\n\t\t\tfinal Path fullPath = watchable.resolve((Path) event.context());\n\t\t\tConsole.log(\"Path 完整对象：{}\", fullPath);\n\t\t}\n\n\t\t@Override\n\t\tpublic void onModify(final WatchEvent<?> event, final Path currentPath) {\n\t\t\tfinal Object obj = event.context();\n\t\t\tConsole.log(\"修改：{}-> {}\", currentPath, obj);\n\t\t}\n\n\t\t@Override\n\t\tpublic void onDelete(final WatchEvent<?> event, final Path currentPath) {\n\t\t\tfinal Object obj = event.context();\n\t\t\tConsole.log(\"删除：{}-> {}\", currentPath, obj);\n\t\t}\n\n\t\t@Override\n\t\tpublic void onOverflow(final WatchEvent<?> event, final Path currentPath) {\n\t\t\tfinal Object obj = event.context();\n\t\t\tConsole.log(\"Overflow：{}-> {}\", currentPath, obj);\n\t\t}\n\t};\n\n\n\t@Test\n\t@Disabled\n\tpublic void testFile() {\n\n\t\tmonitor = WatchMonitor.createAll(\"d:/test/aaa.txt\", new DelayWatcher(watcher, 500));\n\n\t\tmonitor.setMaxDepth(0);\n\t\tmonitor.start();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testDir() {\n\t\tmonitor = WatchMonitor.createAll(\"d:/\", new DelayWatcher(watcher, 500));\n\t\tmonitor.run();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testDelay() {\n\t\tmonitor = WatchMonitor.createAll(\"d:/test\", new DelayWatcher(new SimpleWatcher(){\n\t\t\t@Override\n\t\t\tpublic void onModify(final WatchEvent<?> event, final Path currentPath) {\n\t\t\t\tfinal Object obj = event.context();\n\t\t\t\tConsole.log(\"修改：{}-> {}\", currentPath, obj);\n\t\t\t\tThreadUtil.sleep(5000);\n\t\t\t\tConsole.log(\"sleep end\");\n\t\t\t}\n\n\t\t}, 500));\n\t\tmonitor.run();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/checksum/CRC16Test.java",
    "content": "package cn.hutool.core.io.checksum;\n\nimport cn.hutool.core.io.checksum.crc16.CRC16Ansi;\nimport cn.hutool.core.io.checksum.crc16.CRC16CCITT;\nimport cn.hutool.core.io.checksum.crc16.CRC16CCITTFalse;\nimport cn.hutool.core.io.checksum.crc16.CRC16DNP;\nimport cn.hutool.core.io.checksum.crc16.CRC16IBM;\nimport cn.hutool.core.io.checksum.crc16.CRC16Maxim;\nimport cn.hutool.core.io.checksum.crc16.CRC16Modbus;\nimport cn.hutool.core.io.checksum.crc16.CRC16USB;\nimport cn.hutool.core.io.checksum.crc16.CRC16X25;\nimport cn.hutool.core.io.checksum.crc16.CRC16XModem;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CRC16Test {\n\n\tprivate final String data = \"QN=20160801085857223;ST=23;CN=2011;PW=123456;MN=010000A8900016F000169DC0;Flag=5;CP=&&DataTime=20160801085857; LA-Rtd=50.1&&\";\n\n\t@Test\n\tpublic void ccittTest(){\n\t\tfinal CRC16CCITT crc16 = new CRC16CCITT();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"c852\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void ccittFalseTest(){\n\t\tfinal CRC16CCITTFalse crc16 = new CRC16CCITTFalse();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"a5e4\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void xmodemTest(){\n\t\tfinal CRC16XModem crc16 = new CRC16XModem();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"5a8d\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void x25Test(){\n\t\tfinal CRC16X25 crc16 = new CRC16X25();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"a152\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void modbusTest(){\n\t\tfinal CRC16Modbus crc16 = new CRC16Modbus();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"25fb\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void ibmTest(){\n\t\tfinal CRC16IBM crc16 = new CRC16IBM();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"18c\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void maximTest(){\n\t\tfinal CRC16Maxim crc16 = new CRC16Maxim();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"fe73\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void usbTest(){\n\t\tfinal CRC16USB crc16 = new CRC16USB();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"da04\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void dnpTest(){\n\t\tfinal CRC16DNP crc16 = new CRC16DNP();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"3d1a\", crc16.getHexValue());\n\t}\n\n\t@Test\n\tpublic void ansiTest(){\n\t\tfinal CRC16Ansi crc16 = new CRC16Ansi();\n\t\tcrc16.update(data.getBytes());\n\t\tassertEquals(\"1e00\", crc16.getHexValue());\n\n\t\tcrc16.reset();\n\t\tString str2 = \"QN=20160801085857223;ST=32;CN=1062;PW=100000;MN=010000A8900016F000169DC0;Flag=5;CP=&&RtdInterval=30&&\";\n\t\tcrc16.update(str2.getBytes());\n\t\tassertEquals(\"1c80\", crc16.getHexValue());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/checksum/CrcTest.java",
    "content": "package cn.hutool.core.io.checksum;\n\nimport cn.hutool.core.io.checksum.crc16.CRC16XModem;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * CRC校验单元测试\n *\n * @author looly\n *\n */\npublic class CrcTest {\n\n\t@Test\n\tpublic void crc8Test() {\n\t\tfinal int CRC_POLYNOM = 0x9C;\n\t\tfinal byte CRC_INITIAL = (byte) 0xFF;\n\n\t\tfinal byte[] data = { 1, 56, -23, 3, 0, 19, 0, 0, 2, 0, 3, 13, 8, -34, 7, 9, 42, 18, 26, -5, 54, 11, -94, //\n\t\t\t\t-46, -128, 4, 48, 52, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 1, -32, -80, 0, 98, -5, 71, 0, 64, 0, 0, 0, 0, -116, 1, 104, 2 };\n\t\tCRC8 crc8 = new CRC8(CRC_POLYNOM, CRC_INITIAL);\n\t\tcrc8.update(data, 0, data.length);\n\t\tassertEquals(29, crc8.getValue());\n\t}\n\n\t@Test\n\tpublic void crc16Test() {\n\t\tCRC16 crc = new CRC16();\n\t\tcrc.update(12);\n\t\tcrc.update(16);\n\t\tassertEquals(\"cc04\", HexUtil.toHex(crc.getValue()));\n\t}\n\n\t@Test\n\tpublic void crc16Test2() {\n\t\tString str = \"QN=20160801085857223;ST=23;CN=2011;PW=123456;MN=010000A8900016F000169DC0;Flag=5;CP=&&DataTime=20160801085857; LA-Rtd=50.1&&\";\n\t\tCRC16 crc = new CRC16();\n\t\tcrc.update(str.getBytes(), 0, str.getBytes().length);\n\t\tString crc16 = HexUtil.toHex(crc.getValue());\n\t\tassertEquals(\"18c\", crc16);\n\t}\n\n\t@Test\n\tpublic void paddingTest(){\n\t\t// I3B3RV@Gitee\n\t\tString text = \"000123FFFFFF\";\n\t\tCRC16XModem crc16 = new CRC16XModem();\n\t\tcrc16.update(StrUtil.bytes(text));\n\t\tString hexValue = crc16.getHexValue(true);\n\t\tassertEquals(\"0e04\", hexValue);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/FileNameUtilTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class FileNameUtilTest {\n\n\t@Test\n\tpublic void cleanInvalidTest(){\n\t\tString name = FileNameUtil.cleanInvalid(\"1\\n2\\n\");\n\t\tassertEquals(\"12\", name);\n\n\t\tname = FileNameUtil.cleanInvalid(\"\\r1\\r\\n2\\n\");\n\t\tassertEquals(\"12\", name);\n\t}\n\n\t@Test\n\tpublic void mainNameTest() {\n\t\tfinal String s = FileNameUtil.mainName(\"abc.tar.gz\");\n\t\tassertEquals(\"abc\", s);\n\t}\n\n\t@Test\n\tpublic void extNameAndMainNameBugTest() {\n\t\t// 正确，输出前缀为 \"app-v2.3.1-star\"\n\t\tassertEquals(\"app-v2.3.1-star\",FileNameUtil.mainName(\"app-v2.3.1-star.gz\"));\n\t\t// 当前代码会失败，预期后缀结果 \"gz\"，但是输出 \"star.gz\"\n\t\tassertEquals(\"gz\", FileNameUtil.extName(\"app-v2.3.1-star.gz\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/FileSystemUtilTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\npublic class FileSystemUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void listTest(){\n\t\tfinal FileSystem fileSystem = FileSystemUtil.createZip(\"d:/test/test.zip\",\n\t\t\t\tCharsetUtil.CHARSET_GBK);\n\t\tfinal Path root = FileSystemUtil.getRoot(fileSystem);\n\t\tPathUtil.walkFiles(root, new SimpleFileVisitor<Path>() {\n\n\t\t\t@Override\n\t\t\tpublic FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {\n\t\t\t\tConsole.log(path);\n\t\t\t\treturn FileVisitResult.CONTINUE;\n\t\t\t}\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/FileWriterTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class FileWriterTest {\n\n\t@Test\n\t@Disabled\n\tvoid writeLinesAppendLineSeparatorTest() {\n\t\tfinal FileWriter writer = FileWriter.create(FileUtil.file(\"d:/test/lines_append_line_separator.txt\"));\n\t\twriter.writeLines(ListUtil.of(\"aaa\", \"bbb\", \"ccc\"), null, false, true);\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid writeLinesTest() {\n\t\tfinal FileWriter writer = FileWriter.create(FileUtil.file(\"d:/test/lines.txt\"));\n\t\twriter.writeLines(ListUtil.of(\"aaa\", \"bbb\", \"ccc\"), null, false);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/Issue3557Test.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.StandardCopyOption;\n\npublic class Issue3557Test {\n\n\t@Test\n\t@Disabled\n\tpublic void copyFileTest() {\n\t\t// 如果只是文件不存在，则不会报错\n\t\t// 如果文件所在目录不存在，则会报错\n\t\tFileUtil.copyFile(FileUtil.getInputStream(\"d:/test/aaa.xlsx\"), FileUtil.file(\"d:/test2/aaa_copy.xlsx\"), StandardCopyOption.REPLACE_EXISTING);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/IssueIAB65VTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/IAB65V\n */\npublic class IssueIAB65VTest {\n\t@Test\n\tpublic void getAbsolutePathTest() {\n\t\tString path = \"D:\\\\test\\\\personal\\n\";\n\n\t\tFile file = FileUtil.file(path);\n\t\tif(FileUtil.isWindows()){\n\t\t\t// 换行符自动去除\n\t\t\tassertEquals(\"D:\\\\test\\\\personal\", file.toString());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/PathUtilTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class PathUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void copyFileTest(){\n\t\tPathUtil.copyFile(\n\t\t\t\tPaths.get(\"d:/test/1595232240113.jpg\"),\n\t\t\t\tPaths.get(\"d:/test/1595232240113_copy.jpg\"),\n\t\t\t\tStandardCopyOption.COPY_ATTRIBUTES,\n\t\t\t\tStandardCopyOption.REPLACE_EXISTING\n\t\t\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyTest(){\n\t\tPathUtil.copy(\n\t\t\t\tPaths.get(\"d:/Red2_LYY\"),\n\t\t\t\tPaths.get(\"d:/test/aaa/aaa.txt\")\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void copyContentTest(){\n\t\tPathUtil.copyContent(\n\t\t\t\tPaths.get(\"d:/Red2_LYY\"),\n\t\t\t\tPaths.get(\"d:/test/aaa/\")\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void moveTest(){\n\t\tPathUtil.move(Paths.get(\"d:/lombok.jar\"), Paths.get(\"d:/test/\"), false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void moveDirTest(){\n\t\tPathUtil.move(Paths.get(\"c:\\\\aaa\"), Paths.get(\"d:/test/looly\"), false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delDirTest(){\n\t\tPathUtil.del(Paths.get(\"d:/test/looly\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getMimeTypeTest(){\n\t\tString mimeType = PathUtil.getMimeType(Paths.get(\"d:/test/test.jpg\"));\n\t\tassertEquals(\"image/jpeg\", mimeType);\n\n\t\tmimeType = PathUtil.getMimeType(Paths.get(\"d:/test/test.mov\"));\n\t\tassertEquals(\"video/quicktime\", mimeType);\n\t}\n\n\t@Test\n\tpublic void getMimeOfRarTest(){\n\t\tString contentType = FileUtil.getMimeType(\"a001.rar\");\n\t\tassertEquals(\"application/x-rar-compressed\", contentType);\n\t}\n\n\t@Test\n\tpublic void getMimeOf7zTest(){\n\t\tString contentType = FileUtil.getMimeType(\"a001.7z\");\n\t\tassertEquals(\"application/x-7z-compressed\", contentType);\n\t}\n\n\t@Test\n\tpublic void issue3179Test() {\n\t\tfinal String mimeType = PathUtil.getMimeType(Paths.get(\"xxxx.jpg\"));\n\t\tif(FileUtil.isWindows()){\n\t\t\t// Linux下，OpenJDK可能报路径不存在\n\t\t\tassertEquals(\"image/jpeg\", mimeType);\n\t\t}\n\t}\n\n\t/**\n\t * issue#2893 target不存在空导致异常\n\t */\n\t@Test\n\t@Disabled\n\tpublic void moveTest2(){\n\t\tPathUtil.move(Paths.get(\"D:\\\\project\\\\test1.txt\"), Paths.get(\"D:\\\\project\\\\test2.txt\"), false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delNullDirTest() {\n\t\tPath path = null;\n\t\tassertTrue(PathUtil.del(path));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/file/TailerTest.java",
    "content": "package cn.hutool.core.io.file;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\npublic class TailerTest {\n\n\t@Test\n\t@Disabled\n\tpublic void tailTest() {\n\t\tFileUtil.tail(FileUtil.file(\"d:/test/tail.txt\"), CharsetUtil.CHARSET_GBK);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void tailWithLinesTest() {\n\t\tTailer tailer = new Tailer(FileUtil.file(\"f:/test/test.log\"), Tailer.CONSOLE_HANDLER, 2);\n\t\ttailer.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/resource/ResourceUtilTest.java",
    "content": "package cn.hutool.core.io.resource;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ResourceUtilTest {\n\n\t@Test\n\tpublic void readXmlTest(){\n\t\tfinal String str = ResourceUtil.readUtf8Str(\"test.xml\");\n\t\tassertNotNull(str);\n\n\t\tResource resource = new ClassPathResource(\"test.xml\");\n\t\tfinal String xmlStr = resource.readUtf8Str();\n\n\t\tassertEquals(str, xmlStr);\n\t}\n\n\t@Test\n\tpublic void stringResourceTest(){\n\t\tfinal StringResource stringResource = new StringResource(\"testData\", \"test\");\n\t\tassertEquals(\"test\", stringResource.getName());\n\t\tassertArrayEquals(\"testData\".getBytes(), stringResource.readBytes());\n\t\tassertArrayEquals(\"testData\".getBytes(), IoUtil.readBytes(stringResource.getStream()));\n\t}\n\n\t@Test\n\tpublic void fileResourceTest(){\n\t\tfinal FileResource resource = new FileResource(FileUtil.file(\"test.xml\"));\n\t\tassertEquals(\"test.xml\", resource.getName());\n\t\tassertTrue(StrUtil.isNotEmpty(resource.readUtf8Str()));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/io/unit/DataSizeUtilTest.java",
    "content": "package cn.hutool.core.io.unit;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DataSizeUtilTest {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tlong parse = DataSizeUtil.parse(\"3M\");\n\t\tassertEquals(3145728, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3m\");\n\t\tassertEquals(3145728, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3MB\");\n\t\tassertEquals(3145728, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3mb\");\n\t\tassertEquals(3145728, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3.1M\");\n\t\tassertEquals(3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3.1m\");\n\t\tassertEquals(3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3.1MB\");\n\t\tassertEquals(3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"-3.1MB\");\n\t\tassertEquals(-3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"+3.1MB\");\n\t\tassertEquals(3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3.1mb\");\n\t\tassertEquals(3250585, parse);\n\n\t\tparse = DataSizeUtil.parse(\"3.1\");\n\t\tassertEquals(3, parse);\n\n\t\ttry {\n\t\t\tDataSizeUtil.parse(\"3.1.3\");\n\t\t} catch (IllegalArgumentException ie) {\n\t\t\tassertEquals(\"'3.1.3' is not a valid data size\", ie.getMessage());\n\t\t}\n\n\n\t}\n\n\t@Test\n\tpublic void formatTest(){\n\t\tString format = DataSizeUtil.format(Long.MAX_VALUE);\n\t\tassertEquals(\"8 EB\", format);\n\n\t\tformat = DataSizeUtil.format(1024L * 1024 * 1024 * 1024 * 1024);\n\t\tassertEquals(\"1 PB\", format);\n\n\t\tformat = DataSizeUtil.format(1024L * 1024 * 1024 * 1024);\n\t\tassertEquals(\"1 TB\", format);\n\t}\n\n\t@Test\n\tpublic void formatWithUnitTest(){\n\t\tString format = DataSizeUtil.format(Long.MAX_VALUE, DataUnit.TERABYTES);\n\t\tassertEquals(\"8388608 TB\", format);\n\n\t\tformat = DataSizeUtil.format(1024L * 1024 * 1024 * 1024 * 1024, DataUnit.GIGABYTES);\n\t\tassertEquals(\"1048576 GB\", format);\n\n\t\tformat = DataSizeUtil.format(1024L * 1024 * 1024 * 1024, DataUnit.GIGABYTES);\n\t\tassertEquals(\"1024 GB\", format);\n\t}\n\n\t@Test\n\tpublic void issueI88Z4ZTest() {\n\t\tfinal String size = DataSizeUtil.format(10240000);\n\t\tfinal long bytes = DataSize.parse(size).toBytes();\n\t\tassertEquals(10244587, bytes);\n\t}\n\n\t@Test\n\tvoid issueICXXVFTest(){\n\t\tfinal long parse = DataSizeUtil.parse(\"279.40GiB\");\n\t\tAssertions.assertEquals(300003465625L, parse);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/AssertTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AssertTest {\n\n\t@Test\n\tpublic void isNullTest() {\n\t\tString a = null;\n\t\tcn.hutool.core.lang.Assert.isNull(a);\n\t}\n\n\t@Test\n\tpublic void notNullTest() {\n\t\tString a = null;\n\t\tcn.hutool.core.lang.Assert.isNull(a);\n\t}\n\n\t@Test\n\tpublic void isTrueTest() {\n\t\tAssertions.assertThrows(IllegalArgumentException.class, () -> {\n\t\t\tint i = 0;\n\t\t\t//noinspection ConstantConditions\n\t\t\tcn.hutool.core.lang.Assert.isTrue(i > 0, IllegalArgumentException::new);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void isTrueTest2() {\n\t\tAssertions.assertThrows(IndexOutOfBoundsException.class, () -> {\n\t\t\tint i = -1;\n\t\t\t//noinspection ConstantConditions\n\t\t\tcn.hutool.core.lang.Assert.isTrue(i >= 0, IndexOutOfBoundsException::new);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void isTrueTest3() {\n\t\tAssertions.assertThrows(IndexOutOfBoundsException.class, () -> {\n\t\t\tint i = -1;\n\t\t\t//noinspection ConstantConditions\n\t\t\tAssert.isTrue(i > 0, () -> new IndexOutOfBoundsException(\"relation message to return\"));\n\t\t});\n\t}\n\n\t@Test\n\tpublic void equalsTest() {\n\t\t//String a=\"ab\";\n\t\t//final String b = new String(\"abc\");\n\t\tString a = null;\n\t\tfinal String b = null;\n\t\tAssert.equals(a, b);\n\t\tAssert.equals(a, b, \"{}不等于{}\", a, b);\n\t\tAssert.equals(a, b, () -> new RuntimeException(StrUtil.format(\"{}和{}不相等\", a, b)));\n\t}\n\n\t@Test\n\tpublic void notEqualsTest() {\n\t\t//String c=\"19\";\n\t\t//final String d = new String(\"19\");\n\t\tString c = null;\n\t\tfinal String d = \"null\";\n\t\t//Assert.notEquals(c,d);\n\t\t//Assert.notEquals(c,d,\"{}等于{}\",c,d);\n\t\tAssert.notEquals(c, d, () -> new RuntimeException(StrUtil.format(\"{}和{}相等\", c, d)));\n\n\t}\n\n\t@Test\n\tpublic void emptyCollectionTest() {\n\t\tList<Object> testList = new ArrayList<>();\n\t\tAssertions.assertDoesNotThrow(() -> Assert.empty(null));\n\t\tAssertions.assertDoesNotThrow(() -> Assert.empty(testList));\n\t\ttestList.add(new Object());\n\t\tAssertions.assertThrows(IllegalArgumentException.class, () -> Assert.empty(testList));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ClassScanerTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.ClassUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\n\npublic class ClassScanerTest {\n\n\t@Test\n\t@Disabled\n\tpublic void scanTest() {\n\t\tClassScanner scaner = new ClassScanner(\"cn.hutool.core.util\", null);\n\t\tSet<Class<?>> set = scaner.scan();\n\t\tfor (Class<?> clazz : set) {\n\t\t\tConsole.log(clazz.getName());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scanPackageBySuperTest(){\n\t\t// 扫描包，如果在classpath下找到，就不扫描JDK的jar了\n\t\tfinal Set<Class<?>> classes = ClassScanner.scanPackageBySuper(null, Iterable.class);\n\t\tConsole.log(classes.size());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scanAllPackageBySuperTest(){\n\t\t// 扫描包，如果在classpath下找到，就不扫描JDK的jar了\n\t\tfinal Set<Class<?>> classes = ClassScanner.scanAllPackageBySuper(null, Iterable.class);\n\t\tConsole.log(classes.size());\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void scanAllPackageIgnoreLoadErrorTest(){\n\t\tfinal ClassScanner classScanner = new ClassScanner(null, null);\n\t\tclassScanner.setIgnoreLoadError(true);\n\t\tfinal Set<Class<?>> classes = classScanner.scan(false);\n\t\tConsole.log(classes.size());\n\t\tConsole.log(classScanner.getClassesOfLoadError());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void scanJavaLang() {\n\t\tfinal Set<Class<?>> classes = ClassUtil.scanPackage(\"java.lang\");\n\t\tfor (final Class<?> aClass : classes) {\n\t\t\tConsole.log(aClass.getName());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTableTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ConsoleTableTest {\n\n\t@Test\n\t@Disabled\n\tpublic void printSBCTest() {\n\t\tConsoleTable t = ConsoleTable.create();\n\t\tt.addHeader(\"姓名\", \"年龄\");\n\t\tt.addBody(\"张三\", \"15\");\n\t\tt.addBody(\"李四\", \"29\");\n\t\tt.addBody(\"王二麻子\", \"37\");\n\t\tt.print();\n\n\t\tConsole.log();\n\n\t\tt = ConsoleTable.create();\n\t\tt.addHeader(\"体温\", \"占比\");\n\t\tt.addHeader(\"℃\", \"%\");\n\t\tt.addBody(\"36.8\", \"10\");\n\t\tt.addBody(\"37\", \"5\");\n\t\tt.print();\n\n\t\tConsole.log();\n\n\t\tt = ConsoleTable.create();\n\t\tt.addHeader(\"标题1\", \"标题2\");\n\t\tt.addBody(\"12345\", \"混合321654asdfcSDF\");\n\t\tt.addBody(\"sd   e3ee  ff22\", \"ff值\");\n\t\tt.print();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void printDBCTest() {\n\t\tConsoleTable t = ConsoleTable.create().setSBCMode(false);\n\t\tt.addHeader(\"姓名\", \"年龄\");\n\t\tt.addBody(\"张三\", \"15\");\n\t\tt.addBody(\"李四\", \"29\");\n\t\tt.addBody(\"王二麻子\", \"37\");\n\t\tt.print();\n\n\t\tConsole.log();\n\n\t\tt = ConsoleTable.create().setSBCMode(false);\n\t\tt.addHeader(\"体温\", \"占比\");\n\t\tt.addHeader(\"℃\", \"%\");\n\t\tt.addBody(\"36.8\", \"10\");\n\t\tt.addBody(\"37\", \"5\");\n\t\tt.print();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.thread.ThreadUtil;\n\n/**\n * 控制台单元测试\n * @author Looly\n *\n */\npublic class ConsoleTest {\n\n\t@Test\n\tpublic void logTest(){\n\t\tConsole.log();\n\n\t\tString[] a = {\"abc\", \"bcd\", \"def\"};\n\t\tConsole.log(a);\n\n\t\tConsole.log(\"This is Console log for {}.\", \"test\");\n\t}\n\n\t@Test\n\tpublic void logTest2(){\n\t\tConsole.log(\"a\", \"b\", \"c\");\n\t\tConsole.log((Object) \"a\", \"b\", \"c\");\n\t}\n\n\t@Test\n\tpublic void printTest(){\n\t\tString[] a = {\"abc\", \"bcd\", \"def\"};\n\t\tConsole.print(a);\n\n\t\tConsole.log(\"This is Console print for {}.\", \"test\");\n\t}\n\n\t@Test\n\tpublic void printTest2(){\n\t\tConsole.print(\"a\", \"b\", \"c\");\n\t\tConsole.print((Object) \"a\", \"b\", \"c\");\n\t}\n\n\t@Test\n\tpublic void errorTest(){\n\t\tConsole.error();\n\n\t\tString[] a = {\"abc\", \"bcd\", \"def\"};\n\t\tConsole.error(a);\n\n\t\tConsole.error(\"This is Console error for {}.\", \"test\");\n\t}\n\n\t@Test\n\tpublic void errorTest2(){\n\t\tConsole.error(\"a\", \"b\", \"c\");\n\t\tConsole.error((Object) \"a\", \"b\", \"c\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void inputTest() {\n\t\tConsole.log(\"Please input something: \");\n\t\tString input = Console.input();\n\t\tConsole.log(input);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void printProgressTest() {\n\t\tfor(int i = 0; i < 100; i++) {\n\t\t\tConsole.printProgress('#', 100, i / 100D);\n\t\t\tThreadUtil.sleep(200);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void printColorTest(){\n\t\tSystem.out.print(\"\\33[30;1m A \\u001b[31;2m B \\u001b[32;1m C \\u001b[33;1m D \\u001b[0m\");\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/DictTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.builder.GenericBuilder;\nimport cn.hutool.core.date.DateTime;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static cn.hutool.core.lang.OptTest.User;\n\npublic class DictTest {\n\t@Test\n\tpublic void dictTest(){\n\t\tDict dict = Dict.create()\n\t\t\t\t.set(\"key1\", 1)//int\n\t\t\t\t.set(\"key2\", 1000L)//long\n\t\t\t\t.set(\"key3\", DateTime.now());//Date\n\n\t\tLong v2 = dict.getLong(\"key2\");\n\t\tassertEquals(Long.valueOf(1000L), v2);\n\t}\n\n\t@Test\n\tpublic void dictTest2(){\n\t\tfinal Dict dict = new Dict(true);\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"A\", 1);\n\n\t\tdict.putAll(map);\n\n\t\tassertEquals(1, dict.get(\"A\"));\n\t\tassertEquals(1, dict.get(\"a\"));\n\t}\n\n\t@Test\n\tpublic void ofTest(){\n\t\tDict dict = Dict.of(\n\t\t\t\t\"RED\", \"#FF0000\",\n\t\t\t\t\"GREEN\", \"#00FF00\",\n\t\t\t\t\"BLUE\", \"#0000FF\"\n\t\t);\n\n\t\tassertEquals(\"#FF0000\", dict.get(\"RED\"));\n\t\tassertEquals(\"#00FF00\", dict.get(\"GREEN\"));\n\t\tassertEquals(\"#0000FF\", dict.get(\"BLUE\"));\n\t}\n\n\t@Test\n\tpublic void removeEqualTest(){\n\t\tDict dict = Dict.of(\n\t\t\t\"key1\", null\n\t\t);\n\n\t\tDict dict2 = Dict.of(\n\t\t\t\"key1\", null\n\t\t);\n\n\t\tdict.removeEqual(dict2);\n\n\t\tassertTrue(dict.isEmpty());\n\t}\n\n\t@Test\n\tpublic void setFieldsTest() {\n\t\tUser user = GenericBuilder.of(User::new).with(User::setUsername, \"hutool\").build();\n\t\tDict dict = Dict.create();\n\t\tdict.setFields(user::getNickname, user::getUsername);\n\t\tassertEquals(\"hutool\", dict.get(\"username\"));\n\t\tassertNull(dict.get(\"nickname\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/IssueIAOGDRTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIAOGDRTest {\n\t@Test\n\tvoid isChineseNameTest() {\n\t\tAssertions.assertFalse(Validator.isChineseName(\"张三。\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/NanoIdTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.lang.id.NanoId;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.SecureRandom;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * Tests for NanoId.\n *\n * @author David Klebanoff, Looly\n * @see NanoId\n */\npublic class NanoIdTest {\n\n\t@Test\n\tpublic void nanoIdVerify100KRandomNanoIdsAreUniqueVerifiedTest() {\n\n\t\t//It's not much, but it's a good sanity check I guess.\n\t\tfinal int idCount = 100000;\n\t\tfinal Set<String> ids = new HashSet<>(idCount);\n\n\t\tfor (int i = 0; i < idCount; i++) {\n\t\t\tfinal String id = NanoId.randomNanoId();\n\t\t\tif (ids.contains(id) == false) {\n\t\t\t\tids.add(id);\n\t\t\t} else {\n\t\t\t\tfail(\"Non-unique ID generated: \" + id);\n\t\t\t}\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void nanoIdSeededRandomSuccessTest() {\n\n\t\t//With a seed provided, we can know which IDs to expect, and subsequently verify that the\n\t\t// provided random number generator is being used as expected.\n\t\tfinal Random random = new Random(12345);\n\n\t\tfinal char[] alphabet =\n\t\t\t\t(\"_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\").toCharArray();\n\n\t\tfinal int size = 21;\n\n\t\tfinal String[] expectedIds = new String[]{\"kutqLNv1wDmIS56EcT3j7\", \"U497UttnWzKWWRPMHpLD7\",\n\t\t\t\t\"7nj2dWW1gjKLtgfzeI8eC\", \"I6BXYvyjszq6xV7L9k2A9\", \"uIolcQEyyQIcn3iM6Odoa\"};\n\n\t\tfor (final String expectedId : expectedIds) {\n\t\t\tfinal String generatedId = NanoId.randomNanoId(random, alphabet, size);\n\t\t\tassertEquals(expectedId, generatedId);\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void nanoIdVariousAlphabetsSuccessTest() {\n\n\t\t//Test ID generation with various alphabets consisting of 1 to 255 unique symbols.\n\t\tfor (int symbols = 1; symbols <= 255; symbols++) {\n\n\t\t\tfinal char[] alphabet = new char[symbols];\n\t\t\tfor (int i = 0; i < symbols; i++) {\n\t\t\t\talphabet[i] = (char) i;\n\t\t\t}\n\n\t\t\tfinal String id = NanoId\n\t\t\t\t\t.randomNanoId(null, alphabet, NanoId.DEFAULT_SIZE);\n\n\t\t\t//Create a regex pattern that only matches to the characters in the alphabet\n\t\t\tfinal StringBuilder patternBuilder = new StringBuilder();\n\t\t\tpatternBuilder.append(\"^[\");\n\t\t\tfor (final char character : alphabet) {\n\t\t\t\tpatternBuilder.append(Pattern.quote(String.valueOf(character)));\n\t\t\t}\n\t\t\tpatternBuilder.append(\"]+$\");\n\n\t\t\tassertTrue(id.matches(patternBuilder.toString()));\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void nanoIdVariousSizesSuccessTest() {\n\n\t\t//Test ID generation with all sizes between 1 and 1,000.\n\t\tfor (int size = 1; size <= 1000; size++) {\n\n\t\t\tfinal String id = NanoId.randomNanoId(size);\n\n\t\t\tassertEquals(size, id.length());\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void nanoIdWellDistributedSuccess() {\n\n\t\t//Test if symbols in the generated IDs are well distributed.\n\n\t\tfinal int idCount = 100000;\n\t\tfinal int idSize = 20;\n\t\tfinal char[] alphabet = \"abcdefghijklmnopqrstuvwxyz\".toCharArray();\n\n\t\tfinal Map<String, Long> charCounts = new HashMap<>();\n\n\t\tfor (int i = 0; i < idCount; i++) {\n\n\t\t\tfinal String id = NanoId\n\t\t\t\t\t.randomNanoId(null, alphabet, idSize);\n\n\t\t\tfor (int j = 0; j < id.length(); j++) {\n\t\t\t\tfinal String value = String.valueOf(id.charAt(j));\n\n\t\t\t\tfinal Long charCount = charCounts.get(value);\n\t\t\t\tif (charCount == null) {\n\t\t\t\t\tcharCounts.put(value, 1L);\n\t\t\t\t} else {\n\t\t\t\t\tcharCounts.put(value, charCount + 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t//Verify the distribution of characters is pretty even\n\t\tfor (final Long charCount : charCounts.values()) {\n\t\t\tfinal double distribution = (charCount * alphabet.length / (double) (idCount * idSize));\n\t\t\tassertTrue(distribution >= 0.95 && distribution <= 1.05);\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void randomNanoIdEmptyAlphabetExceptionThrownTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tNanoId.randomNanoId(new SecureRandom(), new char[]{}, 10);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void randomNanoId256AlphabetExceptionThrownTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\t//The alphabet is composed of 256 unique characters\n\t\t\tfinal char[] largeAlphabet = new char[256];\n\t\t\tfor (int i = 0; i < 256; i++) {\n\t\t\t\tlargeAlphabet[i] = (char) i;\n\t\t\t}\n\n\t\t\tNanoId.randomNanoId(new SecureRandom(), largeAlphabet, 20);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void randomNanoIdNegativeSizeExceptionThrown() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tNanoId.randomNanoId(new SecureRandom(), new char[]{'a', 'b', 'c'}, -10);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void randomNanoIdZeroSizeExceptionThrown() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tNanoId.randomNanoId(new SecureRandom(), new char[]{'a', 'b', 'c'}, 0);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ObjectIdTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport java.util.HashSet;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * ObjectId单元测试\n *\n * @author looly\n *\n */\npublic class ObjectIdTest {\n\n\t@Test\n\tpublic void distinctTest() {\n\t\t//生成10000个id测试是否重复\n\t\tHashSet<String> set = new HashSet<>();\n\t\tfor(int i = 0; i < 10000; i++) {\n\t\t\tset.add(ObjectId.next());\n\t\t}\n\n\t\tassertEquals(10000, set.size());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void nextTest() {\n\t\tConsole.log(ObjectId.next());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.NoSuchElementException;\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link Opt}的单元测试\n *\n * @author VampireAchao\n */\npublic class OptTest {\n\n\t@Test\n\tpublic void ofBlankAbleTest() {\n\t\t// ofBlankAble相对于ofNullable考虑了字符串为空串的情况\n\t\tString hutool = Opt.ofBlankAble(\"\").orElse(\"hutool\");\n\t\tassertEquals(\"hutool\", hutool);\n\t}\n\n\t@Test\n\tpublic void getTest() {\n\t\t// 和原版Optional有区别的是，get不会抛出NoSuchElementException\n\t\t// 如果想使用原版Optional中的get这样，获取一个一定不为空的值，则应该使用orElseThrow\n\t\tObject opt = Opt.ofNullable(null).get();\n\t\tassertNull(opt);\n\t}\n\n\t@Test\n\tpublic void isEmptyTest() {\n\t\t// 这是jdk11 Optional中的新函数，直接照搬了过来\n\t\t// 判断包裹内元素是否为空，注意并没有判断空字符串的情况\n\t\tboolean isEmpty = Opt.empty().isEmpty();\n\t\tassertTrue(isEmpty);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void ifPresentOrElseTest() {\n\t\t// 存在就打印对应的值，不存在则用{@code System.err.println}打印另一句字符串\n\t\tOpt.ofNullable(\"Hello Hutool!\").ifPresentOrElse(Console::log, () -> Console.error(\"Ops!Something is wrong!\"));\n\n\t\tOpt.empty().ifPresentOrElse(Console::log, () -> Console.error(\"Ops!Something is wrong!\"));\n\n\t\t// 拓展为支持链式调用\n\t\tOpt.empty().ifPresentOrElse(Console::log, () -> Console.error(\"Ops!Something is wrong!\"))\n\t\t\t\t.ifPresentOrElse(Console::log, () -> Console.error(\"Ops!Something is wrong!\"));\n\t}\n\n\t@Test\n\tpublic void peekTest() {\n\t\tUser user = new User();\n\t\t// 相当于ifPresent的链式调用\n\t\tOpt.ofNullable(\"hutool\").peek(user::setUsername).peek(user::setNickname);\n\t\tassertEquals(\"hutool\", user.getNickname());\n\t\tassertEquals(\"hutool\", user.getUsername());\n\n\t\t// 注意，传入的lambda中，对包裹内的元素执行赋值操作并不会影响到原来的元素\n\t\tString name = Opt.ofNullable(\"hutool\").peek(username -> username = \"123\").peek(username -> username = \"456\").get();\n\t\tassertEquals(\"hutool\", name);\n\t}\n\n\t@Test\n\tpublic void peeksTest() {\n\t\tUser user = new User();\n\t\t// 相当于上面peek的动态参数调用，更加灵活，你可以像操作数组一样去动态设置中间的步骤，也可以使用这种方式去编写你的代码\n\t\t// 可以一行搞定\n\t\tOpt.ofNullable(\"hutool\").peeks(user::setUsername, user::setNickname);\n\t\t// 也可以在适当的地方换行使得代码的可读性提高\n\t\tOpt.of(user).peeks(\n\t\t\t\tu -> assertEquals(\"hutool\", u.getNickname()),\n\t\t\t\tu -> assertEquals(\"hutool\", u.getUsername())\n\t\t);\n\t\tassertEquals(\"hutool\", user.getNickname());\n\t\tassertEquals(\"hutool\", user.getUsername());\n\n\t\t// 注意，传入的lambda中，对包裹内的元素执行赋值操作并不会影响到原来的元素,这是java语言的特性。。。\n\t\t// 这也是为什么我们需要getter和setter而不直接给bean中的属性赋值中的其中一个原因\n\t\tString name = Opt.ofNullable(\"hutool\").peeks(\n\t\t\t\tusername -> username = \"123\", username -> username = \"456\",\n\t\t\t\tn -> assertEquals(\"hutool\", n)).get();\n\t\tassertEquals(\"hutool\", name);\n\n\t\t// 当然，以下情况不会抛出NPE，但也没什么意义\n\t\tOpt.ofNullable(\"hutool\").peeks().peeks().peeks();\n\t\tOpt.ofNullable(null).peeks(i -> {\n\t\t});\n\n\t}\n\n\t@Test\n\tpublic void orTest() {\n\t\t// 这是jdk9 Optional中的新函数，直接照搬了过来\n\t\t// 给一个替代的Opt\n\t\tString str = Opt.<String>ofNullable(null).or(() -> Opt.ofNullable(\"Hello hutool!\")).map(String::toUpperCase).orElseThrow();\n\t\tassertEquals(\"HELLO HUTOOL!\", str);\n\n\t\tUser user = User.builder().username(\"hutool\").build();\n\t\tOpt<User> userOpt = Opt.of(user);\n\t\t// 获取昵称，获取不到则获取用户名\n\t\tString name = userOpt.map(User::getNickname).or(() -> userOpt.map(User::getUsername)).get();\n\t\tassertEquals(\"hutool\", name);\n\t}\n\n\t@Test\n\tpublic void orElseThrowTest() {\n\t\tassertThrows(NoSuchElementException.class, () -> {\n\t\t\t// 获取一个不可能为空的值，否则抛出NoSuchElementException异常\n\t\t\tObject obj = Opt.ofNullable(null).orElseThrow();\n\t\t\tassertNull(obj);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void orElseThrowTest2() {\n\t\tassertThrows(IllegalStateException.class, () -> {\n\t\t\t// 获取一个不可能为空的值，否则抛出自定义异常\n\t\t\tObject assignException = Opt.ofNullable(null).orElseThrow(IllegalStateException::new);\n\t\t\tassertNull(assignException);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void orElseThrowTest3() {\n\t\tassertThrows(IllegalStateException.class, () -> {\n\t\t\t// 获取一个不可能为空的值，否则抛出带自定义消息的自定义异常\n\t\t\tObject exceptionWithMessage = Opt.empty().orElseThrow(IllegalStateException::new, \"Ops!Something is wrong!\");\n\t\t\tassertNull(exceptionWithMessage);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void flattedMapTest() {\n\t\t// 和Optional兼容的flatMap\n\t\tList<User> userList = new ArrayList<>();\n\t\t// 以前，不兼容\n//\t\tOpt.ofNullable(userList).map(List::stream).flatMap(Stream::findFirst);\n\t\t// 现在，兼容\n\t\tUser user = Opt.ofNullable(userList).map(List::stream)\n\t\t\t\t.flattedMap(Stream::findFirst).orElseGet(User.builder()::build);\n\t\tassertNull(user.getUsername());\n\t\tassertNull(user.getNickname());\n\t}\n\n\t@Test\n\tpublic void ofEmptyAbleTest() {\n\t\t// 以前，输入一个CollectionUtil感觉要命，类似前缀的类一大堆，代码补全形同虚设(在项目中起码要输入完CollectionUtil才能在第一个调出这个函数)\n\t\t// 关键它还很常用，判空和判空集合真的太常用了...\n\t\tList<String> past = Opt.ofNullable(Collections.<String>emptyList()).filter(CollectionUtil::isNotEmpty).orElseGet(() -> Collections.singletonList(\"hutool\"));\n\t\t// 现在，一个ofEmptyAble搞定\n\t\tList<String> hutool = Opt.ofEmptyAble(Collections.<String>emptyList()).orElseGet(() -> Collections.singletonList(\"hutool\"));\n\t\tassertEquals(past, hutool);\n\t\tassertEquals(hutool, Collections.singletonList(\"hutool\"));\n\t}\n\n\t@Test\n\tpublic void mapOrElseTest() {\n\t\t// 如果值存在就转换为大写，否则打印一句字符串，支持链式调用、转换为其他类型\n\t\tString hutool = Opt.ofBlankAble(\"hutool\").mapOrElse(String::toUpperCase, () -> Console.log(\"yes\")).mapOrElse(String::intern, () -> Console.log(\"Value is not present~\")).get();\n\t\tassertEquals(\"HUTOOL\", hutool);\n\t}\n\n\t@SuppressWarnings({\"MismatchedQueryAndUpdateOfCollection\", \"ConstantConditions\"})\n\t@Test\n\tpublic void execTest() {\n\t\t// 有一些资深的程序员跟我说你这个lambda，双冒号语法糖看不懂...\n\t\t// 为了尊重资深程序员的意见，并且提升代码可读性，封装了一下 \"try catch NPE 和 数组越界\"的情况\n\n\t\t// 以前这种写法，简洁但可读性稍低，对资深程序员不太友好\n\t\tList<String> last = null;\n\t\tString npeSituation = Opt.ofEmptyAble(last).flattedMap(l -> l.stream().findFirst()).orElse(\"hutool\");\n\t\tString indexOutSituation = Opt.ofEmptyAble(last).map(l -> l.get(0)).orElse(\"hutool\");\n\n\t\t// 现在代码整洁度降低，但可读性up，如果再人说看不懂这代码...\n\t\tString npe = Opt.ofTry(() -> last.get(0)).exceptionOrElse(\"hutool\");\n\t\tString indexOut = Opt.ofTry(() -> {\n\t\t\tList<String> list = new ArrayList<>();\n\t\t\t// 你可以在里面写一长串调用链 list.get(0).getUser().getId()\n\t\t\treturn list.get(0);\n\t\t}).exceptionOrElse(\"hutool\");\n\t\tassertEquals(npe, npeSituation);\n\t\tassertEquals(indexOut, indexOutSituation);\n\t\tassertEquals(\"hutool\", npe);\n\t\tassertEquals(\"hutool\", indexOut);\n\n\t\t// 多线程下情况测试\n\t\tStream.iterate(0, i -> ++i).limit(20000).parallel().forEach(i -> {\n\t\t\tOpt<Object> opt = Opt.ofTry(() -> {\n\t\t\t\tif (i % 2 == 0) {\n\t\t\t\t\tthrow new IllegalStateException(i + \"\");\n\t\t\t\t} else {\n\t\t\t\t\tthrow new NullPointerException(i + \"\");\n\t\t\t\t}\n\t\t\t});\n\t\t\tassertTrue(\n\t\t\t\t\t(i % 2 == 0 && opt.getException() instanceof IllegalStateException) ||\n\t\t\t\t\t\t\t(i % 2 != 0 && opt.getException() instanceof NullPointerException)\n\t\t\t);\n\t\t});\n\t}\n\n\t@Data\n\t@Builder\n\t@NoArgsConstructor\n\t@AllArgsConstructor\n\tstatic class User {\n\t\tprivate String username;\n\t\tprivate String nickname;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/RangeTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.date.DateField;\nimport cn.hutool.core.date.DateRange;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\n/**\n * {@link Range} 单元测试\n *\n * @author Looly\n */\npublic class RangeTest {\n\n\t@Test\n\tpublic void dateRangeTest() {\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-02\");\n\n\t\tfinal Range<DateTime> range = new Range<>(start, end, (current, end1, index) -> {\n\t\t\tif (current.isAfterOrEquals(end1)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn current.offsetNew(DateField.DAY_OF_YEAR, 1);\n\t\t});\n\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(DateUtil.parse(\"2017-01-01\"), range.next());\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(DateUtil.parse(\"2017-01-02\"), range.next());\n\t\tassertFalse(range.hasNext());\n\t}\n\n\t@Test\n\tpublic void dateRangeFuncTest() {\n\t\tDateTime start = DateUtil.parse(\"2021-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2021-01-03\");\n\n\t\tList<Integer> dayOfMonthList = DateUtil.rangeFunc(start, end, DateField.DAY_OF_YEAR, a -> DateTime.of(a).dayOfMonth());\n\t\tassertArrayEquals(dayOfMonthList.toArray(new Integer[]{}), new Integer[]{1, 2, 3});\n\n\t\tList<Integer> dayOfMonthList2 = DateUtil.rangeFunc(null, null, DateField.DAY_OF_YEAR, a -> DateTime.of(a).dayOfMonth());\n\t\tassertArrayEquals(dayOfMonthList2.toArray(new Integer[]{}), new Integer[]{});\n\t}\n\n\t@Test\n\tpublic void dateRangeConsumeTest() {\n\t\tDateTime start = DateUtil.parse(\"2021-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2021-01-03\");\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tDateUtil.rangeConsume(start, end, DateField.DAY_OF_YEAR, a -> sb.append(DateTime.of(a).dayOfMonth()).append(\"#\"));\n\t\tassertEquals(sb.toString(), \"1#2#3#\");\n\n\t\tStringBuilder sb2 = new StringBuilder();\n\t\tDateUtil.rangeConsume(null, null, DateField.DAY_OF_YEAR, a -> sb2.append(DateTime.of(a).dayOfMonth()).append(\"#\"));\n\t\tassertEquals(sb2.toString(), StrUtil.EMPTY);\n\t}\n\n\t@Test\n\tpublic void dateRangeTest2() {\n\t\tDateTime start = DateUtil.parse(\"2021-01-31\");\n\t\tDateTime end = DateUtil.parse(\"2021-03-31\");\n\n\t\tfinal DateRange range = DateUtil.range(start, end, DateField.MONTH);\n\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(DateUtil.parse(\"2021-01-31\"), range.next());\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(DateUtil.parse(\"2021-02-28\"), range.next());\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(DateUtil.parse(\"2021-03-31\"), range.next());\n\t\tassertFalse(range.hasNext());\n\t}\n\n\t@Test\n\tpublic void intRangeTest() {\n\t\tfinal Range<Integer> range = new Range<>(1, 1, (current, end, index) -> current >= end ? null : current + 10);\n\n\t\tassertTrue(range.hasNext());\n\t\tassertEquals(Integer.valueOf(1), range.next());\n\t\tassertFalse(range.hasNext());\n\t}\n\n\t@Test\n\tpublic void rangeByStepTest() {\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-03\");\n\n\t\t// 测试包含开始和结束情况下步进为1的情况\n\t\tDateRange range = DateUtil.range(start, end, DateField.DAY_OF_YEAR);\n\t\tassertEquals(range.next(), DateUtil.parse(\"2017-01-01\"));\n\t\tassertEquals(range.next(), DateUtil.parse(\"2017-01-02\"));\n\t\tassertEquals(range.next(), DateUtil.parse(\"2017-01-03\"));\n\t\ttry {\n\t\t\trange.next();\n\t\t\tfail(\"已超过边界，下一个元素不应该存在！\");\n\t\t} catch (NoSuchElementException ignored) {\n\t\t}\n\n\t\t// 测试多步进的情况\n\t\trange = new DateRange(start, end, DateField.DAY_OF_YEAR, 2);\n\t\tassertEquals(DateUtil.parse(\"2017-01-01\"), range.next());\n\t\tassertEquals(DateUtil.parse(\"2017-01-03\"), range.next());\n\t}\n\n\t@Test\n\tpublic void rangeDayOfYearTest() {\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-05\");\n\n\t\t// 测试不包含开始结束时间的情况\n\t\tDateRange range = new DateRange(start, end, DateField.DAY_OF_YEAR, 1, false, false);\n\t\tassertEquals(DateUtil.parse(\"2017-01-02\"), range.next());\n\t\tassertEquals(DateUtil.parse(\"2017-01-03\"), range.next());\n\t\tassertEquals(DateUtil.parse(\"2017-01-04\"), range.next());\n\t\ttry {\n\t\t\trange.next();\n\t\t\tfail(\"不包含结束时间情况下，下一个元素不应该存在！\");\n\t\t} catch (NoSuchElementException ignored) {\n\t\t}\n\t}\n\n\t@Test\n\tpublic void rangeToListTest() {\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-31\");\n\n\t\tList<DateTime> rangeToList = DateUtil.rangeToList(start, end, DateField.DAY_OF_YEAR);\n\t\tassertEquals(DateUtil.parse(\"2017-01-01\"), rangeToList.get(0));\n\t\tassertEquals(DateUtil.parse(\"2017-01-02\"), rangeToList.get(1));\n\t}\n\n\n\t@Test\n\tpublic void rangeContains() {\n\t\t// 开始区间\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-31\");\n\t\tDateRange startRange = DateUtil.range(start, end, DateField.DAY_OF_YEAR);\n\t\t// 结束区间\n\t\tDateTime start1 = DateUtil.parse(\"2017-01-31\");\n\t\tDateTime end1 = DateUtil.parse(\"2017-02-02\");\n\t\tDateRange endRange = DateUtil.range(start1, end1, DateField.DAY_OF_YEAR);\n\t\t// 交集\n\t\tList<DateTime> dateTimes = DateUtil.rangeContains(startRange, endRange);\n\t\tassertEquals(1, dateTimes.size());\n\t\tassertEquals(DateUtil.parse(\"2017-01-31\"), dateTimes.get(0));\n\t}\n\n\t@Test\n\tpublic void rangeNotContains() {\n\t\t// 开始区间\n\t\tDateTime start = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end = DateUtil.parse(\"2017-01-30\");\n\t\tDateRange startRange = DateUtil.range(start, end, DateField.DAY_OF_YEAR);\n\t\t// 结束区间\n\t\tDateTime start1 = DateUtil.parse(\"2017-01-01\");\n\t\tDateTime end1 = DateUtil.parse(\"2017-01-31\");\n\t\tDateRange endRange = DateUtil.range(start1, end1, DateField.DAY_OF_YEAR);\n\t\t// 差集\n\t\tList<DateTime> dateTimes1 = DateUtil.rangeNotContains(startRange, endRange);\n\n\t\tassertEquals(1, dateTimes1.size());\n\t\tassertEquals(DateUtil.parse(\"2017-01-31\"), dateTimes1.get(0));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/SimhashTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.text.Simhash;\nimport cn.hutool.core.util.StrUtil;\n\npublic class SimhashTest {\n\n\t@Test\n\tpublic void simTest() {\n\t\tString text1 = \"我是 一个 普通 字符串\";\n\t\tString text2 = \"我是 一个 普通 字符串\";\n\n\t\tSimhash simhash = new Simhash();\n\t\tlong hash = simhash.hash(StrUtil.split(text1, ' '));\n\t\tassertTrue(hash != 0);\n\n\t\tsimhash.store(hash);\n\t\tboolean duplicate = simhash.equals(StrUtil.split(text2, ' '));\n\t\tassertTrue(duplicate);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/SimpleCacheTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ConcurrencyTester;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class SimpleCacheTest {\n\n\t@BeforeEach\n\tpublic void putTest(){\n\t\tfinal SimpleCache<String, String> cache = new SimpleCache<>();\n\t\tThreadUtil.execute(()->cache.put(\"key1\", \"value1\"));\n\t\tThreadUtil.execute(()->cache.get(\"key1\"));\n\t\tThreadUtil.execute(()->cache.put(\"key2\", \"value2\"));\n\t\tThreadUtil.execute(()->cache.get(\"key2\"));\n\t\tThreadUtil.execute(()->cache.put(\"key3\", \"value3\"));\n\t\tThreadUtil.execute(()->cache.get(\"key3\"));\n\t\tThreadUtil.execute(()->cache.put(\"key4\", \"value4\"));\n\t\tThreadUtil.execute(()->cache.get(\"key4\"));\n\t\tThreadUtil.execute(()->cache.get(\"key5\", ()->\"value5\"));\n\n\t\tcache.get(\"key5\", ()->\"value5\");\n\t}\n\n\t@Test\n\tpublic void getTest(){\n\t\tfinal SimpleCache<String, String> cache = new SimpleCache<>();\n\t\tcache.put(\"key1\", \"value1\");\n\t\tcache.get(\"key1\");\n\t\tcache.put(\"key2\", \"value2\");\n\t\tcache.get(\"key2\");\n\t\tcache.put(\"key3\", \"value3\");\n\t\tcache.get(\"key3\");\n\t\tcache.put(\"key4\", \"value4\");\n\t\tcache.get(\"key4\");\n\t\tcache.get(\"key5\", ()->\"value5\");\n\n\t\tassertEquals(\"value1\", cache.get(\"key1\"));\n\t\tassertEquals(\"value2\", cache.get(\"key2\"));\n\t\tassertEquals(\"value3\", cache.get(\"key3\"));\n\t\tassertEquals(\"value4\", cache.get(\"key4\"));\n\t\tassertEquals(\"value5\", cache.get(\"key5\"));\n\t\tassertEquals(\"value6\", cache.get(\"key6\", ()-> \"value6\"));\n\t}\n\n\t@Test\n\tpublic void getConcurrencyTest(){\n\t\tfinal SimpleCache<String, String> cache = new SimpleCache<>();\n\t\tfinal ConcurrencyTester tester = new ConcurrencyTester(500);\n\t\ttester.test(()-> cache.get(\"aaa\", ()-> {\n\t\t\tThreadUtil.sleep(200);\n\t\t\treturn \"aaaValue\";\n\t\t}));\n\n\t\tassertTrue(tester.getInterval() > 0);\n\t\tassertEquals(\"aaaValue\", cache.get(\"aaa\"));\n\t\tIoUtil.close(tester);\n\t}\n\n\t@Test\n\tvoid removeTest(){\n\t\tfinal SimpleCache<String, String> cache = new SimpleCache<>();\n\t\tcache.put(\"key1\", \"value1\");\n\t\tcache.get(\"key1\");\n\t\tcache.put(\"key2\", \"value2\");\n\t\tcache.get(\"key2\");\n\t\tcache.put(\"key3\", \"value3\");\n\t\tcache.get(\"key3\");\n\t\tcache.put(\"key4\", \"value4\");\n\t\tcache.get(\"key4\");\n\t\tcache.get(\"key5\", ()->\"value5\");\n\n\t\tString key = null;\n\t\tfor (Map.Entry<String, String> entry : cache) {\n\t\t\tif (\"value3\".equals(entry.getValue())) {\n\t\t\t\tkey = entry.getKey();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif(null != key){\n\t\t\tcache.remove(key);\n\t\t}\n\n\t\tassertEquals(\"value1\", cache.get(\"key1\"));\n\t\tassertEquals(\"value2\", cache.get(\"key2\"));\n\t\tassertEquals(\"value4\", cache.get(\"key4\"));\n\t\tassertEquals(\"value5\", cache.get(\"key5\"));\n\t\tassertNull(cache.get(\"key3\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/SingletonTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.thread.ThreadUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Duration;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTimeout;\n\npublic class SingletonTest {\n\n\t@Test\n\tpublic void getTest(){\n\t\t// 此测试中使用1000个线程获取单例对象，其间对象只被创建一次\n\t\tThreadUtil.concurrencyTest(1000, ()-> Singleton.get(TestBean.class));\n\t}\n\n\t@Data\n\tstatic class TestBean{\n\t\tprivate static volatile TestBean testSingleton;\n\n\t\tpublic TestBean(){\n\t\t\tif(null != testSingleton){\n\t\t\t\tthrow new UtilException(\"单例测试中，对象被创建了两次！\");\n\t\t\t}\n\t\t\ttestSingleton = this;\n\t\t}\n\n\t\tprivate String name;\n\t\tprivate String age;\n\t}\n\n\t/**\n\t * 测试单例构建属性锁死问题\n\t * C构建单例时候，同时构建B，此时在SimpleCache中会有写锁竞争（写入C时获取了写锁，此时要写入B，也要获取写锁）\n\t */\n\t@Test\n\tpublic void reentrantTest(){\n\t\tassertTimeout(Duration.ofMillis(1000L), ()->{\n\t\tfinal C c = Singleton.get(C.class);\n\t\tassertEquals(\"aaa\", c.getB().getA());\n\t\t});\n\t}\n\n\t@Data\n\tstatic class B{\n\t\tprivate String a = \"aaa\";\n\t}\n\n\t@Data\n\tstatic class C{\n\t\tprivate B b = Singleton.get(B.class);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/SnowflakeTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.ConcurrentHashSet;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * Snowflake单元测试\n * @author Looly\n *\n */\npublic class SnowflakeTest {\n\n\t/**\n\t * 测试-根据传入时间戳-计算ID起终点\n\t */\n\t@Test\n\tpublic void snowflakeTestGetIdScope() {\n\t\tfinal long workerId = RandomUtil.randomLong(31);\n\t\tfinal long dataCenterId = RandomUtil.randomLong(31);\n\t\tfinal Snowflake idWorker = new Snowflake(workerId, dataCenterId);\n\t\tfinal long generatedId = idWorker.nextId();\n\t\t// 随机忽略数据中心和工作机器的占位\n\t\tfinal boolean ignore = RandomUtil.randomBoolean();\n\t\tfinal long createTimestamp = idWorker.getGenerateDateTime(generatedId);\n\t\tfinal Pair<Long, Long> idScope = idWorker.getIdScopeByTimestamp(createTimestamp, createTimestamp, ignore);\n\t\tfinal long startId = idScope.getKey();\n\t\tfinal long endId = idScope.getValue();\n\n\t\t// 起点终点相差比较\n\t\tfinal long trueOffSet = endId - startId;\n\t\t// 忽略数据中心和工作机器时差值为22个1，否则为12个1\n\t\tfinal long expectedOffSet = ignore ? ~(-1 << 22) : ~(-1 << 12);\n\t\tassertEquals(trueOffSet, expectedOffSet);\n\t}\n\n\t@Test\n\tpublic void snowflakeTest1(){\n\t\t//构建Snowflake，提供终端ID和数据中心ID\n\t\tfinal Snowflake idWorker = new Snowflake(0, 0);\n\t\tfinal long nextId = idWorker.nextId();\n\t\tassertTrue(nextId > 0);\n\t}\n\n\t@Test\n\tpublic void snowflakeTest(){\n\t\tfinal HashSet<Long> hashSet = new HashSet<>();\n\n\t\t//构建Snowflake，提供终端ID和数据中心ID\n\t\tfinal Snowflake idWorker = new Snowflake(0, 0);\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tfinal long id = idWorker.nextId();\n\t\t\thashSet.add(id);\n\t\t}\n\t\tassertEquals(1000L, hashSet.size());\n\t}\n\n\t@Test\n\tpublic void snowflakeGetTest(){\n\t\t//构建Snowflake，提供终端ID和数据中心ID\n\t\tfinal Snowflake idWorker = new Snowflake(1, 2);\n\t\tfinal long nextId = idWorker.nextId();\n\n\t\tassertEquals(1, idWorker.getWorkerId(nextId));\n\t\tassertEquals(2, idWorker.getDataCenterId(nextId));\n\t\tassertTrue(idWorker.getGenerateDateTime(nextId) - System.currentTimeMillis() < 10);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uniqueTest(){\n\t\t// 测试并发环境下生成ID是否重复\n\t\tfinal Snowflake snowflake = IdUtil.getSnowflake(0, 0);\n\n\t\tfinal Set<Long> ids = new ConcurrentHashSet<>();\n\t\tThreadUtil.concurrencyTest(100, () -> {\n\t\t\tfor (int i = 0; i < 50000; i++) {\n\t\t\t\tif(false == ids.add(snowflake.nextId())){\n\t\t\t\t\tthrow new UtilException(\"重复ID！\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\tpublic void getSnowflakeLengthTest(){\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tfinal long l = IdUtil.getSnowflake(0, 0).nextId();\n\t\t\tassertEquals(19, StrUtil.toString(l).length());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void snowflakeRandomSequenceTest(){\n\t\tfinal Snowflake snowflake = new Snowflake(null, 0, 0,\n\t\t\t\tfalse, Snowflake.DEFAULT_TIME_OFFSET, 2);\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\tfinal long id = snowflake.nextId();\n\t\t\tConsole.log(id);\n\t\t\tThreadUtil.sleep(10);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uniqueOfRandomSequenceTest(){\n\t\t// 测试并发环境下生成ID是否重复\n\t\tfinal Snowflake snowflake = new Snowflake(null, 0, 0,\n\t\t\t\tfalse, Snowflake.DEFAULT_TIME_OFFSET, 100);\n\n\t\tfinal Set<Long> ids = new ConcurrentHashSet<>();\n\t\tThreadUtil.concurrencyTest(100, () -> {\n\t\t\tfor (int i = 0; i < 50000; i++) {\n\t\t\t\tif(false == ids.add(snowflake.nextId())){\n\t\t\t\t\tthrow new UtilException(\"重复ID！\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/StrFormatterTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.text.StrFormatter;\n\npublic class StrFormatterTest {\n\n\t@Test\n\tpublic void formatTest() {\n\t\t//通常使用\n\t\tString result1 = StrFormatter.format(\"this is {} for {}\", \"a\", \"b\");\n\t\tassertEquals(\"this is a for b\", result1);\n\n\t\t//转义{}\n\t\tString result2 = StrFormatter.format(\"this is \\\\{} for {}\", \"a\", \"b\");\n\t\tassertEquals(\"this is {} for a\", result2);\n\n\t\t//转义\\\n\t\tString result3 = StrFormatter.format(\"this is \\\\\\\\{} for {}\", \"a\", \"b\");\n\t\tassertEquals(\"this is \\\\a for b\", result3);\n\t}\n\n\t@Test\n\tpublic void formatWithTest() {\n\t\t//通常使用\n\t\tString result1 = StrFormatter.formatWith(\"this is ? for ?\", \"?\", \"a\", \"b\");\n\t\tassertEquals(\"this is a for b\", result1);\n\n\t\t//转义?\n\t\tString result2 = StrFormatter.formatWith(\"this is \\\\? for ?\", \"?\", \"a\", \"b\");\n\t\tassertEquals(\"this is ? for a\", result2);\n\n\t\t//转义\\\n\t\tString result3 = StrFormatter.formatWith(\"this is \\\\\\\\? for ?\", \"?\", \"a\", \"b\");\n\t\tassertEquals(\"this is \\\\a for b\", result3);\n\t}\n\n\t@Test\n\tpublic void formatWithTest2() {\n\t\t//通常使用\n\t\tString result1 = StrFormatter.formatWith(\"this is $$$ for $$$\", \"$$$\", \"a\", \"b\");\n\t\tassertEquals(\"this is a for b\", result1);\n\n\t\t//转义?\n\t\tString result2 = StrFormatter.formatWith(\"this is \\\\$$$ for $$$\", \"$$$\", \"a\", \"b\");\n\t\tassertEquals(\"this is $$$ for a\", result2);\n\n\t\t//转义\\\n\t\tString result3 = StrFormatter.formatWith(\"this is \\\\\\\\$$$ for $$$\", \"$$$\", \"a\", \"b\");\n\t\tassertEquals(\"this is \\\\a for b\", result3);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/TupleTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Locale;\nimport java.util.TimeZone;\n\npublic class TupleTest {\n\n\t@Test\n\tpublic void hashCodeTest(){\n\t\tfinal Tuple tuple = new Tuple(Locale.getDefault(), TimeZone.getDefault());\n\t\tfinal Tuple tuple2 = new Tuple(Locale.getDefault(), TimeZone.getDefault());\n\t\tassertEquals(tuple, tuple2);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/UUIDTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.collection.ConcurrentHashSet;\nimport cn.hutool.core.thread.ThreadUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\n\npublic class UUIDTest {\n\n\t/**\n\t * 测试UUID是否存在重复问题\n\t */\n\t@Test\n\tpublic void fastUUIDTest(){\n\t\tSet<String> set = new ConcurrentHashSet<>(100);\n\t\tThreadUtil.concurrencyTest(100, ()-> set.add(UUID.fastUUID().toString()));\n\t\tassertEquals(100, set.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.core.util.IdUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 验证器单元测试\n *\n * @author Looly\n */\npublic class ValidatorTest {\n\n\t@Test\n\tpublic void isNumberTest() {\n\t\tassertTrue(Validator.isNumber(\"45345365465\"));\n\t\tassertTrue(Validator.isNumber(\"0004545435\"));\n\t\tassertTrue(Validator.isNumber(\"5.222\"));\n\t\tassertTrue(Validator.isNumber(\"0.33323\"));\n\t}\n\n\t@Test\n\tpublic void hasNumberTest() {\n\t\tfinal String var1 = \"\";\n\t\tfinal String var2 = \"str\";\n\t\tfinal String var3 = \"180\";\n\t\tfinal String var4 = \"身高180体重180\";\n\t\tassertFalse(Validator.hasNumber(var1));\n\t\tassertFalse(Validator.hasNumber(var2));\n\t\tassertTrue(Validator.hasNumber(var3));\n\t\tassertTrue(Validator.hasNumber(var4));\n\t}\n\n\t@Test\n\tpublic void isLetterTest() {\n\t\tassertTrue(Validator.isLetter(\"asfdsdsfds\"));\n\t\tassertTrue(Validator.isLetter(\"asfdsdfdsfVCDFDFGdsfds\"));\n\t\tassertTrue(Validator.isLetter(\"asfdsdf你好dsfVCDFDFGdsfds\"));\n\t}\n\n\t@Test\n\tpublic void isUperCaseTest() {\n\t\tassertTrue(Validator.isUpperCase(\"VCDFDFG\"));\n\t\tassertTrue(Validator.isUpperCase(\"ASSFD\"));\n\n\t\tassertFalse(Validator.isUpperCase(\"asfdsdsfds\"));\n\t\tassertFalse(Validator.isUpperCase(\"ASSFD你好\"));\n\t}\n\n\t@Test\n\tpublic void isLowerCaseTest() {\n\t\tassertTrue(Validator.isLowerCase(\"asfdsdsfds\"));\n\n\t\tassertFalse(Validator.isLowerCase(\"aaaa你好\"));\n\t\tassertFalse(Validator.isLowerCase(\"VCDFDFG\"));\n\t\tassertFalse(Validator.isLowerCase(\"ASSFD\"));\n\t\tassertFalse(Validator.isLowerCase(\"ASSFD你好\"));\n\t}\n\n\t@Test\n\tpublic void isBirthdayTest() {\n\t\tfinal boolean b = Validator.isBirthday(\"20150101\");\n\t\tassertTrue(b);\n\t\tfinal boolean b2 = Validator.isBirthday(\"2015-01-01\");\n\t\tassertTrue(b2);\n\t\tfinal boolean b3 = Validator.isBirthday(\"2015.01.01\");\n\t\tassertTrue(b3);\n\t\tfinal boolean b4 = Validator.isBirthday(\"2015年01月01日\");\n\t\tassertTrue(b4);\n\t\tfinal boolean b5 = Validator.isBirthday(\"2015.01.01\");\n\t\tassertTrue(b5);\n\t\tfinal boolean b6 = Validator.isBirthday(\"2018-08-15\");\n\t\tassertTrue(b6);\n\n\t\t//验证年非法\n\t\tassertFalse(Validator.isBirthday(\"2095.05.01\"));\n\t\t//验证月非法\n\t\tassertFalse(Validator.isBirthday(\"2015.13.01\"));\n\t\t//验证日非法\n\t\tassertFalse(Validator.isBirthday(\"2015.02.29\"));\n\t}\n\n\t@Test\n\tpublic void isCitizenIdTest() {\n\t\t// 18为身份证号码验证\n\t\tfinal boolean b = Validator.isCitizenId(\"110101199003074477\");\n\t\tassertTrue(b);\n\n\t\t// 15位身份证号码验证\n\t\tfinal boolean b1 = Validator.isCitizenId(\"410001910101123\");\n\t\tassertTrue(b1);\n\n\t\t// 10位身份证号码验证\n\t\tfinal boolean b2 = Validator.isCitizenId(\"U193683453\");\n\t\tassertTrue(b2);\n\t}\n\n\t@Test\n\tpublic void validateTest() throws ValidateException {\n\t\tassertThrows(ValidateException.class, () -> {\n\t\t\tValidator.validateChinese(\"我是一段zhongwen\", \"内容中包含非中文\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void isEmailTest() {\n\t\tfinal boolean email = Validator.isEmail(\"abc_cde@163.com\");\n\t\tassertTrue(email);\n\t\tfinal boolean email1 = Validator.isEmail(\"abc_%cde@163.com\");\n\t\tassertTrue(email1);\n\t\tfinal boolean email2 = Validator.isEmail(\"abc_%cde@aaa.c\");\n\t\tassertTrue(email2);\n\t\tfinal boolean email3 = Validator.isEmail(\"xiaolei.lu@aaa.b\");\n\t\tassertTrue(email3);\n\t\tfinal boolean email4 = Validator.isEmail(\"xiaolei.Lu@aaa.b\");\n\t\tassertTrue(email4);\n\t\tfinal boolean email5 = Validator.isEmail(\"luxiaolei_小磊@小磊.com\", true);\n\t\tassertTrue(email5);\n\t}\n\n\t@Test\n\tpublic void isMobileTest() {\n\t\tfinal boolean m1 = Validator.isMobile(\"13900221432\");\n\t\tassertTrue(m1);\n\t\tfinal boolean m2 = Validator.isMobile(\"015100221432\");\n\t\tassertTrue(m2);\n\t\tfinal boolean m3 = Validator.isMobile(\"+8618600221432\");\n\t\tassertTrue(m3);\n\t\tfinal boolean m4 = Validator.isMobile(\"19312341234\");\n\t\tassertTrue(m4);\n\t}\n\n\t@Test\n\tpublic void isMatchTest() {\n\t\tString url = \"http://aaa-bbb.somthing.com/a.php?a=b&c=2\";\n\t\tassertTrue(Validator.isMatchRegex(PatternPool.URL_HTTP, url));\n\n\t\turl = \"https://aaa-bbb.somthing.com/a.php?a=b&c=2\";\n\t\tassertTrue(Validator.isMatchRegex(PatternPool.URL_HTTP, url));\n\n\t\turl = \"https://aaa-bbb.somthing.com:8080/a.php?a=b&c=2\";\n\t\tassertTrue(Validator.isMatchRegex(PatternPool.URL_HTTP, url));\n\t}\n\n\t@Test\n\tpublic void isGeneralTest() {\n\t\tString str = \"\";\n\t\tboolean general = Validator.isGeneral(str, -1, 5);\n\t\tassertTrue(general);\n\n\t\tstr = \"123_abc_ccc\";\n\t\tgeneral = Validator.isGeneral(str, -1, 100);\n\t\tassertTrue(general);\n\n\t\t// 不允许中文\n\t\tstr = \"123_abc_ccc中文\";\n\t\tgeneral = Validator.isGeneral(str, -1, 100);\n\t\tassertFalse(general);\n\t}\n\n\t@Test\n\tpublic void isPlateNumberTest() {\n\t\tassertTrue(Validator.isPlateNumber(\"粤BA03205\"));\n\t\tassertTrue(Validator.isPlateNumber(\"闽20401领\"));\n\t\t//issue#3979\n\t\tassertTrue(Validator.isPlateNumber(\"沪AE22075\"));\n\t}\n\n\t@Test\n\tpublic void isChineseTest() {\n\t\tassertTrue(Validator.isChinese(\"全都是中文\"));\n\t\tassertTrue(Validator.isChinese(\"㐓㐘\"));\n\t\tassertFalse(Validator.isChinese(\"not全都是中文\"));\n\t}\n\n\t@Test\n\tpublic void hasChineseTest() {\n\t\tassertTrue(Validator.hasChinese(\"黄单桑米\"));\n\t\tassertTrue(Validator.hasChinese(\"Kn 四兄弟\"));\n\t\tassertTrue(Validator.hasChinese(\"\\uD840\\uDDA3\"));\n\t\tassertFalse(Validator.hasChinese(\"Abc\"));\n\t}\n\n\t@Test\n\tpublic void isUUIDTest() {\n\t\tassertTrue(Validator.isUUID(IdUtil.randomUUID()));\n\t\tassertTrue(Validator.isUUID(IdUtil.fastSimpleUUID()));\n\n\t\tassertTrue(Validator.isUUID(IdUtil.randomUUID().toUpperCase()));\n\t\tassertTrue(Validator.isUUID(IdUtil.fastSimpleUUID().toUpperCase()));\n\t}\n\n\t@Test\n\tpublic void isZipCodeTest() {\n\t\t// 港\n\t\tboolean zipCode = Validator.isZipCode(\"999077\");\n\t\tassertTrue(zipCode);\n\t\t// 澳\n\t\tzipCode = Validator.isZipCode(\"999078\");\n\t\tassertTrue(zipCode);\n\t\t// 台（2020年3月起改用6位邮编，3+3）\n\t\tzipCode = Validator.isZipCode(\"822001\");\n\t\tassertTrue(zipCode);\n\n\t\t// 内蒙\n\t\tzipCode = Validator.isZipCode(\"016063\");\n\t\tassertTrue(zipCode);\n\t\t// 山西\n\t\tzipCode = Validator.isZipCode(\"045246\");\n\t\tassertTrue(zipCode);\n\t\t// 河北\n\t\tzipCode = Validator.isZipCode(\"066502\");\n\t\tassertTrue(zipCode);\n\t\t// 北京\n\t\tzipCode = Validator.isZipCode(\"102629\");\n\t\tassertTrue(zipCode);\n\t}\n\n\t@Test\n\tpublic void isBetweenTest() {\n\t\tassertTrue(Validator.isBetween(0, 0, 1));\n\t\tassertTrue(Validator.isBetween(1L, 0L, 1L));\n\t\tassertTrue(Validator.isBetween(0.19f, 0.1f, 0.2f));\n\t\tassertTrue(Validator.isBetween(0.19, 0.1, 0.2));\n\t}\n\n\n\t@Test\n\tpublic void isBetweenPrecisionLossTest() {\n\t\t// 使用超过 double 精度的值\n\t\tlong base = 10000000000000000L;\n\t\tlong min = base + 1;\n\t\tlong max = base + 2;\n\n\t\t// 在 double 转换下，base、min 和 max 是完全相等的，因为 double 精度不够\n\t\t// 预期结果为false，但是因为double 精度不够，导致输出为true\n\t\tassertFalse(Validator.isBetween(base, min, max));\n\t}\n\n\t@Test\n\tpublic void isCarVinTest() {\n\t\tassertTrue(Validator.isCarVin(\"LSJA24U62JG269225\"));\n\t\tassertTrue(Validator.isCarVin(\"LDC613P23A1305189\"));\n\t\tassertFalse(Validator.isCarVin(\"LOC613P23A1305189\"));\n\n\t\tassertTrue(Validator.isCarVin(\"LSJA24U62JG269225\"));    //标准分类1\n\t\tassertTrue(Validator.isCarVin(\"LDC613P23A1305189\"));    //标准分类1\n\t\tassertTrue(Validator.isCarVin(\"LBV5S3102ESJ25655\"));    //标准分类1\n\t\tassertTrue(Validator.isCarVin(\"LBV5S3102ESJPE655\"));    //标准分类2\n\t\tassertFalse(Validator.isCarVin(\"LOC613P23A1305189\"));    //错误示例\n\t}\n\n\t@Test\n\tpublic void isCarDrivingLicenceTest() {\n\t\tassertTrue(Validator.isCarDrivingLicence(\"430101758218\"));\n\t}\n\n\t@Test\n\tpublic void validateIpv4Test() {\n\t\tValidator.validateIpv4(\"192.168.1.1\", \"Error ip\");\n\t\tValidator.validateIpv4(\"8.8.8.8\", \"Error ip\");\n\t\tValidator.validateIpv4(\"0.0.0.0\", \"Error ip\");\n\t\tValidator.validateIpv4(\"255.255.255.255\", \"Error ip\");\n\t\tValidator.validateIpv4(\"127.0.0.0\", \"Error ip\");\n\t}\n\n\t@Test\n\tpublic void isUrlTest() {\n\t\tfinal String content = \"https://detail.tmall.com/item.htm?\" +\n\t\t\t\"id=639428931841&ali_refid=a3_430582_1006:1152464078:N:Sk5vwkMVsn5O6DcnvicELrFucL21A32m:0af8611e23c1d07697e\";\n\n\t\tassertTrue(Validator.isMatchRegex(Validator.URL, content));\n\t\tassertTrue(Validator.isMatchRegex(Validator.URL_HTTP, content));\n\t}\n\n\t@Test\n\tpublic void isChineseNameTest() {\n\t\tassertTrue(Validator.isChineseName(\"阿卜杜尼亚孜·毛力尼亚孜\"));\n\t\tassertFalse(Validator.isChineseName(\"阿卜杜尼亚孜./毛力尼亚孜\"));\n\t\tassertTrue(Validator.isChineseName(\"段正淳\"));\n\t\tassertTrue(Validator.isChineseName(\"刘欣䶮\"));\n\t\tassertFalse(Validator.isChineseName(\"孟  伟\"));\n\t\tassertFalse(Validator.isChineseName(\"李\"));\n\t\tassertFalse(Validator.isChineseName(\"连逍遥0\"));\n\t\tassertFalse(Validator.isChineseName(\"SHE\"));\n\n\t\t// issue#IAOGDR\n\t\tAssertions.assertFalse(Validator.isChineseName(\"张三。\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/WeightListRandomTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class WeightListRandomTest {\n\n\t@Test\n\t@Disabled\n\tpublic void nextTest() {\n\t\tMap<Integer, Times> timesMap = new HashMap<>();\n\t\tint size = 100;\n\t\tdouble sumWeight = 0.0;\n\t\tWeightListRandom<Integer> pool = new WeightListRandom<>(size);\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tdouble weight = RandomUtil.randomDouble(100);\n\t\t\tpool.add(i, weight);\n\t\t\tsumWeight += weight;\n\t\t\ttimesMap.put(i, new Times(weight));\n\t\t}\n\n\t\tdouble d = 0.0001;// 随机误差\n\t\tint times = 100000000;// 随机次数\n\t\tfor (int i = 0; i < times; i++) {\n\t\t\ttimesMap.get(pool.next()).num++;\n\t\t}\n\t\tdouble finalSumWeight = sumWeight;\n\t\ttimesMap.forEach((key, times1) -> {\n\t\t\tdouble expected = times1.weight / finalSumWeight;// 期望概率\n\t\t\tdouble actual = timesMap.get(key).num * 1.0 / times;// 真实随机概率\n\t\t\tassertTrue(Math.abs(actual - expected) < d);// 检验随机误差是否在误差范围内\n\t\t});\n\t}\n\n\tprivate static class Times {\n\t\tint num;\n\t\tdouble weight;\n\n\t\tpublic Times(double weight) {\n\t\t\tthis.weight = weight;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/WeightRandomTest.java",
    "content": "package cn.hutool.core.lang;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.collection.CollUtil;\n\npublic class WeightRandomTest {\n\n\t@Test\n\tpublic void weightRandomTest() {\n\t\tWeightRandom<String> random = WeightRandom.create();\n\t\trandom.add(\"A\", 10);\n\t\trandom.add(\"B\", 50);\n\t\trandom.add(\"C\", 100);\n\n\t\tString result = random.next();\n\t\tassertTrue(CollUtil.newArrayList(\"A\", \"B\", \"C\").contains(result));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/ansi/AnsiEncoderTest.java",
    "content": "package cn.hutool.core.lang.ansi;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Color;\n\npublic class AnsiEncoderTest {\n\n\t@Test\n\tpublic void encodeTest(){\n\t\tfinal String encode = AnsiEncoder.encode(AnsiColor.GREEN, \"Hutool test\");\n\t\tassertEquals(\"\\u001B[32mHutool test\\u001B[0;39m\", encode);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void colorfulEncodeTest(){\n\t\tString text = \"Hutool▀████▀\";\n\t\tfinal AnsiColors ansiColors = new AnsiColors(AnsiColors.BitDepth.EIGHT);\n\t\tColor[] colorArray = new Color[]{\n\t\t\t\tColor.BLACK,\t\t\tColor.BLUE,\n\t\t\t\tColor.CYAN,\t\t\t\tColor.DARK_GRAY,\n\t\t\t\tColor.GRAY,\t\t\t\tColor.GREEN,\n\t\t\t\tColor.LIGHT_GRAY,\t\tColor.MAGENTA,\n\t\t\t\tColor.ORANGE,\t\t\tColor.PINK,\n\t\t\t\tColor.RED,\t\t\t\tColor.WHITE,\n\t\t\t\tColor.YELLOW\n\t\t};\n\t\tfor (int i = 0; i < colorArray.length; i++) {\n\t\t\tColor foreColor = colorArray[i];\n\t\t\tAnsiElement foreElement = ansiColors.findClosest(foreColor).toAnsiElement(ForeOrBack.FORE);\n\t\t\tColor backColor = new Color(255 - foreColor.getRed(), 255 - foreColor.getGreen(), 255 - foreColor.getBlue());\n\t\t\tAnsiElement backElement = ansiColors.findClosest(backColor).toAnsiElement(ForeOrBack.BACK);\n\t\t\tString encode = AnsiEncoder.encode(foreElement, backElement, text);\n\t\t\tConsole.print( i%2==1?encode+\"\\n\":encode);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void colorMappingTest(){\n\t\tString text4 = \"RGB:({},{},{})--4bit \";\n\t\tString text8 = \"RGB:({},{},{})--8bit \";\n\t\tfinal AnsiColors ansiColors4Bit = new AnsiColors(AnsiColors.BitDepth.FOUR);\n\t\tfinal AnsiColors ansiColors8Bit = new AnsiColors(AnsiColors.BitDepth.EIGHT);\n\t\tint count = 0;\n\t\tint from = 100000;\n\t\tint until = 120000;\n\t\tfor (int r = 0; r < 256; r++) {\n\t\t\tif (count>until)break;\n\t\t\tfor (int g = 0; g < 256; g++) {\n\t\t\t\tif (count>until)break;\n\t\t\t\tfor (int b = 0; b < 256; b++) {\n\t\t\t\t\tcount++;\n\t\t\t\t\tif (count<from)continue;\n\t\t\t\t\tif (count>until)break;\n\t\t\t\t\tAnsiElement backElement4bit = ansiColors4Bit.findClosest(new Color(r,g,b)).toAnsiElement(ForeOrBack.BACK);\n\t\t\t\t\tAnsiElement backElement8bit = ansiColors8Bit.findClosest(new Color(r,g,b)).toAnsiElement(ForeOrBack.BACK);\n\t\t\t\t\tString encode4 = AnsiEncoder.encode( backElement4bit,text4);\n\t\t\t\t\tString encode8 = AnsiEncoder.encode( backElement8bit,text8);\n\t\t\t\t\t//Console.log(StrUtil.format(encode4,r,g,b)+StrUtil.format(encode8,r,g,b));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/caller/CallerTest.java",
    "content": "package cn.hutool.core.lang.caller;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * {@link CallerUtil} 单元测试\n * @author Looly\n *\n */\npublic class CallerTest {\n\n\t@Test\n\tpublic void getCallerTest() {\n\t\tClass<?> caller = CallerUtil.getCaller();\n\t\tassertEquals(this.getClass(), caller);\n\n\t\tClass<?> caller0 = CallerUtil.getCaller(0);\n\t\tassertEquals(CallerUtil.class, caller0);\n\n\t\tClass<?> caller1 = CallerUtil.getCaller(1);\n\t\tassertEquals(this.getClass(), caller1);\n\t}\n\n\t@Test\n\tpublic void getCallerCallerTest() {\n\t\tClass<?> callerCaller = CallerTestClass.getCaller();\n\t\tassertEquals(this.getClass(), callerCaller);\n\t}\n\n\tprivate static class CallerTestClass{\n\t\tpublic static Class<?> getCaller(){\n\t\t\treturn CallerUtil.getCallerCaller();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/caller/CallerUtilTest.java",
    "content": "package cn.hutool.core.lang.caller;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CallerUtilTest {\n\n\t@Test\n\tpublic void getCallerMethodNameTest() {\n\t\tfinal String callerMethodName = CallerUtil.getCallerMethodName(false);\n\t\tassertEquals(\"getCallerMethodNameTest\", callerMethodName);\n\n\t\tfinal String fullCallerMethodName = CallerUtil.getCallerMethodName(true);\n\t\tassertEquals(\"cn.hutool.core.lang.caller.CallerUtilTest.getCallerMethodNameTest\", fullCallerMethodName);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/func/LambdaUtilTest.java",
    "content": "package cn.hutool.core.lang.func;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.invoke.MethodHandleInfo;\n\npublic class LambdaUtilTest {\n\n\t@Test\n\tpublic void getMethodNameTest() {\n\t\tfinal String methodName = LambdaUtil.getMethodName(MyTeacher::getAge);\n\t\tassertEquals(\"getAge\", methodName);\n\t}\n\n\t@Test\n\tpublic void getFieldNameTest() {\n\t\tfinal String fieldName = LambdaUtil.getFieldName(MyTeacher::getAge);\n\t\tassertEquals(\"age\", fieldName);\n\t}\n\n\t@Test\n\tpublic void resolveTest() {\n\t\t// 引用构造函数\n\t\tassertEquals(MethodHandleInfo.REF_newInvokeSpecial,\n\t\t\t\tLambdaUtil.resolve(MyTeacher::new).getImplMethodKind());\n\t\t// 数组构造函数引用\n\t\tassertEquals(MethodHandleInfo.REF_invokeStatic,\n\t\t\t\tLambdaUtil.resolve(MyTeacher[]::new).getImplMethodKind());\n\t\t// 引用静态方法\n\t\tassertEquals(MethodHandleInfo.REF_invokeStatic,\n\t\t\t\tLambdaUtil.resolve(MyTeacher::takeAge).getImplMethodKind());\n\t\t// 引用特定对象的实例方法\n\t\tassertEquals(MethodHandleInfo.REF_invokeVirtual,\n\t\t\t\tLambdaUtil.resolve(new MyTeacher()::getAge).getImplMethodKind());\n\t\t// 引用特定类型的任意对象的实例方法\n\t\tassertEquals(MethodHandleInfo.REF_invokeVirtual,\n\t\t\t\tLambdaUtil.resolve(MyTeacher::getAge).getImplMethodKind());\n\t}\n\n\n\t@Test\n\tpublic void getRealClassTest() {\n\t\t// 引用特定类型的任意对象的实例方法\n\t\tfinal Class<MyTeacher> functionClass = LambdaUtil.getRealClass(MyTeacher::getAge);\n\t\tassertEquals(MyTeacher.class, functionClass);\n\t\t// 枚举测试，不会导致类型擦除\n\t\tfinal Class<LambdaKindEnum> enumFunctionClass = LambdaUtil.getRealClass(LambdaKindEnum::ordinal);\n\t\tassertEquals(LambdaKindEnum.class, enumFunctionClass);\n\t\t// 调用父类方法，能获取到正确的子类类型\n\t\tfinal Class<MyTeacher> superFunctionClass = LambdaUtil.getRealClass(MyTeacher::getId);\n\t\tassertEquals(MyTeacher.class, superFunctionClass);\n\n\t\tfinal MyTeacher myTeacher = new MyTeacher();\n\t\t// 引用特定对象的实例方法\n\t\tfinal Class<MyTeacher> supplierClass = LambdaUtil.getRealClass(myTeacher::getAge);\n\t\tassertEquals(MyTeacher.class, supplierClass);\n\t\t// 枚举测试，只能获取到枚举类型\n\t\tfinal Class<Enum<?>> enumSupplierClass = LambdaUtil.getRealClass(LambdaKindEnum.REF_NONE::ordinal);\n\t\tassertEquals(Enum.class, enumSupplierClass);\n\t\t// 调用父类方法，只能获取到父类类型\n\t\tfinal Class<Entity<?>> superSupplierClass = LambdaUtil.getRealClass(myTeacher::getId);\n\t\tassertEquals(Entity.class, superSupplierClass);\n\n\t\t// 引用静态带参方法，能够获取到正确的参数类型\n\t\tfinal Class<MyTeacher> staticFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeAgeBy);\n\t\tassertEquals(MyTeacher.class, staticFunctionClass);\n\t\t// 引用父类静态带参方法，只能获取到父类类型\n\t\tfinal Class<Entity<?>> staticSuperFunctionClass = LambdaUtil.getRealClass(MyTeacher::takeId);\n\t\tassertEquals(Entity.class, staticSuperFunctionClass);\n\n\t\t// 引用静态无参方法，能够获取到正确的类型\n\t\tfinal Class<MyTeacher> staticSupplierClass = LambdaUtil.getRealClass(MyTeacher::takeAge);\n\t\tassertEquals(MyTeacher.class, staticSupplierClass);\n\t\t// 引用父类静态无参方法，能够获取到正确的参数类型\n\t\tfinal Class<MyTeacher> staticSuperSupplierClass = LambdaUtil.getRealClass(MyTeacher::takeIdBy);\n\t\tassertEquals(MyTeacher.class, staticSuperSupplierClass);\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class MyStudent {\n\n\t\tprivate String name;\n\t}\n\n\t@Data\n\tpublic static class Entity<T> {\n\n\t\tprivate T id;\n\n\t\tpublic static <T> T takeId() {\n\t\t\treturn new Entity<T>().getId();\n\t\t}\n\n\t\tpublic static <T> T takeIdBy(final Entity<T> entity) {\n\t\t\treturn entity.getId();\n\t\t}\n\n\n\t}\n\n\t@Data\n\t@EqualsAndHashCode(callSuper = true)\n\tstatic class MyTeacher extends Entity<MyTeacher> {\n\n\t\tpublic static String takeAge() {\n\t\t\treturn new MyTeacher().getAge();\n\t\t}\n\n\t\tpublic static String takeAgeBy(final MyTeacher myTeacher) {\n\t\t\treturn myTeacher.getAge();\n\t\t}\n\n\t\tpublic String age;\n\t}\n\n\t/**\n\t * 测试Lambda类型枚举\n\t */\n\tenum LambdaKindEnum {\n\t\tREF_NONE,\n\t\tREF_getField,\n\t\tREF_getStatic,\n\t\tREF_putField,\n\t\tREF_putStatic,\n\t\tREF_invokeVirtual,\n\t\tREF_invokeStatic,\n\t\tREF_invokeSpecial,\n\t\tREF_newInvokeSpecial,\n\t}\n\n\t@Test\n\tpublic void lambdaClassNameTest() {\n\t\tfinal String lambdaClassName1 = LambdaUtilTestHelper.getLambdaClassName(MyTeacher::getAge);\n\t\tfinal String lambdaClassName2 = LambdaUtilTestHelper.getLambdaClassName(MyTeacher::getAge);\n\t\tassertNotEquals(lambdaClassName1, lambdaClassName2);\n\t}\n\n\tstatic class LambdaUtilTestHelper {\n\t\tpublic static <P> String getLambdaClassName(final Func1<P, ?> func) {\n\t\t\treturn func.getClass().getName();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/hash/CityHashTest.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CityHashTest {\n\n\t@Test\n\tpublic void hash32Test() {\n\t\tint hv = CityHash.hash32(StrUtil.utf8Bytes(\"你\"));\n\t\tassertEquals(1290029860, hv);\n\n\t\thv = CityHash.hash32(StrUtil.utf8Bytes(\"你好\"));\n\t\tassertEquals(1374181357, hv);\n\n\t\thv = CityHash.hash32(StrUtil.utf8Bytes(\"见到你很高兴\"));\n\t\tassertEquals(1475516842, hv);\n\t\thv = CityHash.hash32(StrUtil.utf8Bytes(\"我们将通过生成一个大的文件的方式来检验各种方法的执行效率因为这种方式在结束的时候需要执行文件\"));\n\t\tassertEquals(0x51020cae, hv);\n\t}\n\n\t@Test\n\tpublic void hash64Test() {\n\t\tlong hv = CityHash.hash64(StrUtil.utf8Bytes(\"你\"));\n\t\tassertEquals(-4296898700418225525L, hv);\n\n\t\thv = CityHash.hash64(StrUtil.utf8Bytes(\"你好\"));\n\t\tassertEquals(-4294276205456761303L, hv);\n\n\t\thv = CityHash.hash64(StrUtil.utf8Bytes(\"见到你很高兴\"));\n\t\tassertEquals(272351505337503793L, hv);\n\t\thv = CityHash.hash64(StrUtil.utf8Bytes(\"我们将通过生成一个大的文件的方式来检验各种方法的执行效率因为这种方式在结束的时候需要执行文件\"));\n\t\tassertEquals(-8234735310919228703L, hv);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/hash/MetroHashTest.java",
    "content": "package cn.hutool.core.lang.hash;\n\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * https://gitee.com/chinabugotech/hutool/pulls/532\n */\npublic class MetroHashTest {\n\n\t@Test\n\tpublic void testEmpty() {\n\t\tassertEquals(\"31290877cceaea29\", HexUtil.toHex(MetroHash.hash64(StrUtil.utf8Bytes(\"\"), 0)));\n\t}\n\n\t@Test\n\tpublic void metroHash64Test() {\n\t\tbyte[] str = \"我是一段测试123\".getBytes(CharsetUtil.CHARSET_UTF_8);\n\t\tfinal long hash64 = MetroHash.hash64(str);\n\t\tassertEquals(62920234463891865L, hash64);\n\t}\n\n\t@Test\n\tpublic void metroHash128Test() {\n\t\tbyte[] str = \"我是一段测试123\".getBytes(CharsetUtil.CHARSET_UTF_8);\n\t\tfinal long[] hash128 = MetroHash.hash128(str).getLongArray();\n\t\tassertEquals(4956592424592439349L, hash128[0]);\n\t\tassertEquals(6301214698325086246L, hash128[1]);\n\t}\n\n\t/**\n\t * 数据量越大 MetroHash 优势越明显，\n\t */\n\t@Test\n\t@Disabled\n\tpublic void bulkHashing64Test() {\n\t\tString[] strArray = getRandomStringArray();\n\t\tlong startCity = System.currentTimeMillis();\n\t\tfor (String s : strArray) {\n\t\t\tCityHash.hash64(s.getBytes());\n\t\t}\n\t\tlong endCity = System.currentTimeMillis();\n\n\t\tlong startMetro = System.currentTimeMillis();\n\t\tfor (String s : strArray) {\n\t\t\tMetroHash.hash64(StrUtil.utf8Bytes(s));\n\t\t}\n\t\tlong endMetro = System.currentTimeMillis();\n\n\t\tSystem.out.println(\"metroHash =============\" + (endMetro - startMetro));\n\t\tSystem.out.println(\"cityHash =============\" + (endCity - startCity));\n\t}\n\n\n\t/**\n\t * 数据量越大 MetroHash 优势越明显，\n\t */\n\t@Test\n\t@Disabled\n\tpublic void bulkHashing128Test() {\n\t\tString[] strArray = getRandomStringArray();\n\t\tlong startCity = System.currentTimeMillis();\n\t\tfor (String s : strArray) {\n\t\t\tCityHash.hash128(s.getBytes());\n\t\t}\n\t\tlong endCity = System.currentTimeMillis();\n\n\t\tlong startMetro = System.currentTimeMillis();\n\t\tfor (String s : strArray) {\n\t\t\tMetroHash.hash128(StrUtil.utf8Bytes(s));\n\t\t}\n\t\tlong endMetro = System.currentTimeMillis();\n\n\t\tSystem.out.println(\"metroHash =============\" + (endMetro - startMetro));\n\t\tSystem.out.println(\"cityHash =============\" + (endCity - startCity));\n\t}\n\n\n\tprivate static String[] getRandomStringArray() {\n\t\tString[] result = new String[10000000];\n\t\tint index = 0;\n\t\twhile (index < 10000000) {\n\t\t\tresult[index++] = RandomUtil.randomString(RandomUtil.randomInt(64));\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/hash/MurmurHashTest.java",
    "content": "package cn.hutool.core.lang.hash;\n\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class MurmurHashTest {\n\n\t@Test\n\tpublic void hash32Test() {\n\t\tint hv = MurmurHash.hash32(StrUtil.utf8Bytes(\"你\"));\n\t\tassertEquals(-1898877446, hv);\n\n\t\thv = MurmurHash.hash32(StrUtil.utf8Bytes(\"你好\"));\n\t\tassertEquals(337357348, hv);\n\n\t\thv = MurmurHash.hash32(StrUtil.utf8Bytes(\"见到你很高兴\"));\n\t\tassertEquals(1101306141, hv);\n\t\thv = MurmurHash.hash32(StrUtil.utf8Bytes(\"我们将通过生成一个大的文件的方式来检验各种方法的执行效率因为这种方式在结束的时候需要执行文件\"));\n\t\tassertEquals(-785444229, hv);\n\t}\n\n\t@Test\n\tpublic void hash64Test() {\n\t\tlong hv = MurmurHash.hash64(StrUtil.utf8Bytes(\"你\"));\n\t\tassertEquals(-1349759534971957051L, hv);\n\n\t\thv = MurmurHash.hash64(StrUtil.utf8Bytes(\"你好\"));\n\t\tassertEquals(-7563732748897304996L, hv);\n\n\t\thv = MurmurHash.hash64(StrUtil.utf8Bytes(\"见到你很高兴\"));\n\t\tassertEquals(-766658210119995316L, hv);\n\t\thv = MurmurHash.hash64(StrUtil.utf8Bytes(\"我们将通过生成一个大的文件的方式来检验各种方法的执行效率因为这种方式在结束的时候需要执行文件\"));\n\t\tassertEquals(-7469283059271653317L, hv);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/intern/InternUtilTest.java",
    "content": "package cn.hutool.core.lang.intern;\n\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class InternUtilTest {\n\n\t/**\n\t * 检查规范字符串是否相同\n\t */\n\t@SuppressWarnings(\"StringOperationCanBeSimplified\")\n\t@Test\n\tpublic void weakTest(){\n\t\tfinal Interner<String> interner = InternUtil.createWeakInterner();\n\t\tString a1 = RandomUtil.randomString(RandomUtil.randomInt(100));\n\t\tString a2 = new String(a1);\n\n\t\tassertNotSame(a1, a2);\n\n\t\tassertSame(interner.intern(a1), interner.intern(a2));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/loader/LazyFunLoaderTest.java",
    "content": "package cn.hutool.core.lang.loader;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class LazyFunLoaderTest {\n\n\tstatic class BigObject {\n\n\t\tprivate boolean isDestroy = false;\n\n\t\tpublic void destroy() {\n\t\t\tthis.isDestroy = true;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void test1() {\n\n\t\tLazyFunLoader<BigObject> loader = new LazyFunLoader<>(BigObject::new);\n\n\t\tassertNotNull(loader.get());\n\t\tassertTrue(loader.isInitialize());\n\n\t\t// 对于某些对象，在程序关闭时，需要进行销毁操作\n\t\tloader.ifInitialized(BigObject::destroy);\n\n\t\tassertTrue(loader.get().isDestroy);\n\t}\n\n\t@Test\n\tpublic void test2() {\n\n\t\tLazyFunLoader<BigObject> loader = new LazyFunLoader<>(BigObject::new);\n\n\t\t// 若从未使用，则可以避免不必要的初始化\n\t\tloader.ifInitialized(it -> {\n\n\t\t\tfail();\n\t\t\tit.destroy();\n\t\t});\n\n\t\tassertFalse(loader.isInitialize());\n\t}\n\n\t@Test\n\tpublic void testOnLoadStaticFactoryMethod1() {\n\n\t\tLazyFunLoader<BigObject> loader = LazyFunLoader.on(BigObject::new);\n\n\t\tassertNotNull(loader.get());\n\t\tassertTrue(loader.isInitialize());\n\n\t\t// 对于某些对象，在程序关闭时，需要进行销毁操作\n\t\tloader.ifInitialized(BigObject::destroy);\n\n\t\tassertTrue(loader.get().isDestroy);\n\t}\n\n\t@Test\n\tpublic void testOnLoadStaticFactoryMethod2() {\n\n\t\tLazyFunLoader<BigObject> loader = LazyFunLoader.on(BigObject::new);\n\n\t\t// 若从未使用，则可以避免不必要的初始化\n\t\tloader.ifInitialized(it -> {\n\n\t\t\tfail();\n\t\t\tit.destroy();\n\t\t});\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/reflect/ActualTypeMapperPoolTest.java",
    "content": "package cn.hutool.core.lang.reflect;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\n/**\n * 见：https://gitee.com/chinabugotech/hutool/pulls/447/files\n *\n * TODO 同时继承泛型和实现泛型接口需要解析，此处为F\n */\npublic class ActualTypeMapperPoolTest {\n\n\t@Test\n\tpublic void getTypeArgumentTest(){\n\t\tfinal Map<Type, Type> typeTypeMap = ActualTypeMapperPool.get(FinalClass.class);\n\t\ttypeTypeMap.forEach((key, value)->{\n\t\t\tif(\"A\".equals(key.getTypeName())){\n\t\t\t\tassertEquals(Character.class, value);\n\t\t\t} else if(\"B\".equals(key.getTypeName())){\n\t\t\t\tassertEquals(Boolean.class, value);\n\t\t\t} else if(\"C\".equals(key.getTypeName())){\n\t\t\t\tassertEquals(String.class, value);\n\t\t\t} else if(\"D\".equals(key.getTypeName())){\n\t\t\t\tassertEquals(Double.class, value);\n\t\t\t} else if(\"E\".equals(key.getTypeName())){\n\t\t\t\tassertEquals(Integer.class, value);\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\tpublic void getTypeArgumentStrKeyTest(){\n\t\tfinal Map<String, Type> typeTypeMap = ActualTypeMapperPool.getStrKeyMap(FinalClass.class);\n\t\ttypeTypeMap.forEach((key, value)->{\n\t\t\tif(\"A\".equals(key)){\n\t\t\t\tassertEquals(Character.class, value);\n\t\t\t} else if(\"B\".equals(key)){\n\t\t\t\tassertEquals(Boolean.class, value);\n\t\t\t} else if(\"C\".equals(key)){\n\t\t\t\tassertEquals(String.class, value);\n\t\t\t} else if(\"D\".equals(key)){\n\t\t\t\tassertEquals(Double.class, value);\n\t\t\t} else if(\"E\".equals(key)){\n\t\t\t\tassertEquals(Integer.class, value);\n\t\t\t}\n\t\t});\n\t}\n\n\tpublic interface BaseInterface<A, B, C> {}\n\tpublic interface FirstInterface<A, B, D, E> extends BaseInterface<A, B, String> {}\n\tpublic interface SecondInterface<A, B, F> extends BaseInterface<A, B, String> {}\n\n\tpublic static class BaseClass<A, D> implements FirstInterface<A, Boolean, D, Integer> {}\n\tpublic static class FirstClass extends BaseClass<Character, Double> implements SecondInterface<Character, Boolean, FirstClass> {}\n\tpublic static class SecondClass extends FirstClass {}\n\tpublic static class FinalClass extends SecondClass {}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/reflect/MethodHandleUtilTest.java",
    "content": "package cn.hutool.core.lang.reflect;\n\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodType;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\n\npublic class MethodHandleUtilTest {\n\n\t@Test\n\tpublic void invokeDefaultTest(){\n\t\tDuck duck = (Duck) Proxy.newProxyInstance(\n\t\t\t\tClassLoaderUtil.getClassLoader(),\n\t\t\t\tnew Class[] { Duck.class },\n\t\t\t\tMethodHandleUtil::invokeSpecial);\n\n\t\tassertEquals(\"Quack\", duck.quack());\n\n\t\t// 测试子类执行default方法\n\t\tfinal Method quackMethod = ReflectUtil.getMethod(Duck.class, \"quack\");\n\t\tString quack = MethodHandleUtil.invokeSpecial(new BigDuck(), quackMethod);\n\t\tassertEquals(\"Quack\", quack);\n\n\t\t// 测试反射执行默认方法\n\t\tquack = ReflectUtil.invoke(new Duck() {}, quackMethod);\n\t\tassertEquals(\"Quack\", quack);\n\t}\n\n\t@Test\n\tpublic void invokeDefaultByReflectTest(){\n\t\tDuck duck = (Duck) Proxy.newProxyInstance(\n\t\t\t\tClassLoaderUtil.getClassLoader(),\n\t\t\t\tnew Class[] { Duck.class },\n\t\t\t\tReflectUtil::invoke);\n\n\t\tassertEquals(\"Quack\", duck.quack());\n\t}\n\n\t@Test\n\tpublic void invokeStaticByProxyTest(){\n\t\tDuck duck = (Duck) Proxy.newProxyInstance(\n\t\t\t\tClassLoaderUtil.getClassLoader(),\n\t\t\t\tnew Class[] { Duck.class },\n\t\t\t\tReflectUtil::invoke);\n\n\t\tassertEquals(\"Quack\", duck.quack());\n\t}\n\n\t@Test\n\tpublic void invokeTest(){\n\t\t// 测试执行普通方法\n\t\tfinal int size = MethodHandleUtil.invokeSpecial(new BigDuck(),\n\t\t\t\tReflectUtil.getMethod(BigDuck.class, \"getSize\"));\n\t\tassertEquals(36, size);\n\t}\n\n\t@Test\n\tpublic void invokeStaticTest(){\n\t\t// 测试执行普通方法\n\t\tfinal String result = MethodHandleUtil.invoke(null,\n\t\t\t\tReflectUtil.getMethod(Duck.class, \"getDuck\", int.class), 78);\n\t\tassertEquals(\"Duck 78\", result);\n\t}\n\n\t@Test\n\tpublic void findMethodTest() throws Throwable {\n\t\tMethodHandle handle = MethodHandleUtil.findMethod(Duck.class, \"quack\",\n\t\t\t\tMethodType.methodType(String.class));\n\t\tassertNotNull(handle);\n\t\t// 对象方法自行需要绑定对象或者传入对象参数\n\t\tString invoke = (String) handle.invoke(new BigDuck());\n\t\tassertEquals(\"Quack\", invoke);\n\n\t\t// 对象的方法获取\n\t\thandle = MethodHandleUtil.findMethod(BigDuck.class, \"getSize\",\n\t\t\t\tMethodType.methodType(int.class));\n\t\tassertNotNull(handle);\n\t\tint invokeInt = (int) handle.invoke(new BigDuck());\n\t\tassertEquals(36, invokeInt);\n\t}\n\n\t@Test\n\tpublic void findStaticMethodTest() throws Throwable {\n\t\tfinal MethodHandle handle = MethodHandleUtil.findMethod(Duck.class, \"getDuck\",\n\t\t\t\tMethodType.methodType(String.class, int.class));\n\t\tassertNotNull(handle);\n\n\t\t// static 方法执行不需要绑定或者传入对象，直接传入参数即可\n\t\tfinal String invoke = (String) handle.invoke(12);\n\t\tassertEquals(\"Duck 12\", invoke);\n\t}\n\n\t@Test\n\tpublic void findPrivateMethodTest() throws Throwable {\n\t\tfinal MethodHandle handle = MethodHandleUtil.findMethod(BigDuck.class, \"getPrivateValue\",\n\t\t\t\tMethodType.methodType(String.class));\n\t\tassertNotNull(handle);\n\n\t\tfinal String invoke = (String) handle.invoke(new BigDuck());\n\t\tassertEquals(\"private value\", invoke);\n\t}\n\n\t@Test\n\tpublic void findSuperMethodTest() throws Throwable {\n\t\t// 查找父类的方法\n\t\tfinal MethodHandle handle = MethodHandleUtil.findMethod(BigDuck.class, \"quack\",\n\t\t\t\tMethodType.methodType(String.class));\n\t\tassertNotNull(handle);\n\n\t\tfinal String invoke = (String) handle.invoke(new BigDuck());\n\t\tassertEquals(\"Quack\", invoke);\n\t}\n\n\t@Test\n\tpublic void findPrivateStaticMethodTest() throws Throwable {\n\t\tfinal MethodHandle handle = MethodHandleUtil.findMethod(BigDuck.class, \"getPrivateStaticValue\",\n\t\t\t\tMethodType.methodType(String.class));\n\t\tassertNotNull(handle);\n\n\t\tfinal String invoke = (String) handle.invoke();\n\t\tassertEquals(\"private static value\", invoke);\n\t}\n\n\tinterface Duck {\n\t\tdefault String quack() {\n\t\t\treturn \"Quack\";\n\t\t}\n\n\t\tstatic String getDuck(int count){\n\t\t\treturn \"Duck \" + count;\n\t\t}\n\t}\n\n\tstatic class BigDuck implements Duck{\n\t\tpublic int getSize(){\n\t\t\treturn 36;\n\t\t}\n\n\t\t@SuppressWarnings(\"unused\")\n\t\tprivate String getPrivateValue(){\n\t\t\treturn \"private value\";\n\t\t}\n\n\t\t@SuppressWarnings(\"unused\")\n\t\tprivate static String getPrivateStaticValue(){\n\t\t\treturn \"private static value\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java",
    "content": "package cn.hutool.core.lang.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n *\n * @author 质量过关\n *\n */\n@Data\npublic class ExamInfoDict implements Serializable {\n\tprivate static final long serialVersionUID = 3640936499125004525L;\n\n\t// 主键\n\tprivate Integer id; // 可当作题号\n\t// 试题类型 客观题 0主观题 1\n\tprivate Integer examType;\n\t// 试题是否作答\n\tprivate Integer answerIs;\n\n\tpublic Integer getId(Integer defaultValue) {\n\t\treturn this.id == null ? defaultValue : this.id;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoDict.java",
    "content": "package cn.hutool.core.lang.test.bean;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * 用户信息\n * @author 质量过关\n *\n */\npublic class UserInfoDict implements Serializable {\n\tprivate static final long serialVersionUID = -936213991463284306L;\n\t// 用户Id\n\tprivate Integer id;\n\t// 要展示的名字\n\tprivate String realName;\n\t// 头像地址\n\tprivate String photoPath;\n\tprivate List<ExamInfoDict> examInfoDict;\n\tprivate UserInfoRedundCount userInfoRedundCount;\n\n\tpublic Integer getId() {\n\t\treturn id;\n\t}\n\tpublic void setId(Integer id) {\n\t\tthis.id = id;\n\t}\n\n\tpublic String getRealName() {\n\t\treturn realName;\n\t}\n\tpublic void setRealName(String realName) {\n\t\tthis.realName = realName;\n\t}\n\n\tpublic String getPhotoPath() {\n\t\treturn photoPath;\n\t}\n\tpublic void setPhotoPath(String photoPath) {\n\t\tthis.photoPath = photoPath;\n\t}\n\n\tpublic List<ExamInfoDict> getExamInfoDict() {\n\t\treturn examInfoDict;\n\t}\n\tpublic void setExamInfoDict(List<ExamInfoDict> examInfoDict) {\n\t\tthis.examInfoDict = examInfoDict;\n\t}\n\n\tpublic UserInfoRedundCount getUserInfoRedundCount() {\n\t\treturn userInfoRedundCount;\n\t}\n\tpublic void setUserInfoRedundCount(UserInfoRedundCount userInfoRedundCount) {\n\t\tthis.userInfoRedundCount = userInfoRedundCount;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tUserInfoDict that = (UserInfoDict) o;\n\t\treturn Objects.equals(id, that.id) && Objects.equals(realName, that.realName) && Objects.equals(photoPath, that.photoPath) && Objects.equals(examInfoDict, that.examInfoDict);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(id, realName, photoPath, examInfoDict);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"UserInfoDict [id=\" + id + \", realName=\" + realName + \", photoPath=\" + photoPath + \", examInfoDict=\" + examInfoDict + \", userInfoRedundCount=\" + userInfoRedundCount + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoRedundCount.java",
    "content": "package cn.hutool.core.lang.test.bean;\n\nimport java.io.Serializable;\n\npublic class UserInfoRedundCount implements Serializable {\n\n\tprivate static final long serialVersionUID = -8397291070139255181L;\n\tprivate String finishedRatio; // 完成率\n\n\tprivate Integer ownershipExamCount; // 自己有多少道题\n\n\tprivate Integer answeredExamCount; // 当前回答了多少道题\n\n\tpublic Integer getOwnershipExamCount() {\n\t\treturn ownershipExamCount;\n\t}\n\n\tpublic void setOwnershipExamCount(Integer ownershipExamCount) {\n\t\tthis.ownershipExamCount = ownershipExamCount;\n\t}\n\n\tpublic Integer getAnsweredExamCount() {\n\t\treturn answeredExamCount;\n\t}\n\n\tpublic void setAnsweredExamCount(Integer answeredExamCount) {\n\t\tthis.answeredExamCount = answeredExamCount;\n\t}\n\n\tpublic String getFinishedRatio() {\n\t\treturn finishedRatio;\n\t}\n\n\tpublic void setFinishedRatio(String finishedRatio) {\n\t\tthis.finishedRatio = finishedRatio;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/Issue2279Test.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.collection.ListUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class Issue2279Test {\n\n\t@Test\n\tpublic void buildSingleTest() {\n\t\tList<TestTree> list = ListUtil.of(\n\t\t\t\t// 模拟数据\n\t\t\t\tnew TestTree(1, 0, 1, 1),\n\t\t\t\tnew TestTree(2, 1, 2, 2),\n\t\t\t\tnew TestTree(3, 1, 3, 3),\n\t\t\t\tnew TestTree(4, 2, 4, 4)\n\t\t);\n\n\t\tList<Tree<String>> stringTree = TreeUtil.build(list, \"0\",\n\t\t\t\t(object, treeNode) -> {\n\t\t\t\t\ttreeNode.setId(object.getId());\n\t\t\t\t\ttreeNode.setName(object.getName());\n\t\t\t\t\ttreeNode.setParentId(object.getPid());\n\t\t\t\t\ttreeNode.putExtra(\"extra1\",object.getExtra1());\n\t\t\t\t}\n\t\t);\n\n\t\tfinal Tree<String> result = stringTree.get(0);\n\t\tassertEquals(2, result.getChildren().size());\n\t}\n\n\t@Data\n\tstatic class TestTree {\n\t\tprivate String id;\n\t\tprivate String pid;\n\t\tprivate String name;\n\t\tprivate String extra1;\n\n\t\tpublic TestTree(int id, int pid, int name, int extra1) {\n\t\t\tthis.id = String.valueOf(id);\n\t\t\tthis.pid = String.valueOf(pid);\n\t\t\tthis.name = String.valueOf(name);\n\t\t\tthis.extra1 = String.valueOf(extra1);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/IssueI795INTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.lang.tree;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class IssueI795INTest {\n\tstatic List<TreeNode<Long>> all_menu=new ArrayList<>();\n\tstatic {\n\t\t/*\n\t\t * root\n\t\t *    /module-A\n\t\t *    \t   /module-A-menu-1\n\t\t *    /module-B\n\t\t *    \t   /module-B-menu-1\n\t\t *    \t   /module-B-menu-2\n\t\t */\n\t\tall_menu.add(new TreeNode<>(1L, 0L, \"root\", 0L));\n\t\tall_menu.add(new TreeNode<>(2L,1L,\"module-A\",0L));\n\t\tall_menu.add(new TreeNode<>(3L,1L,\"module-B\",0L));\n\t\tall_menu.add(new TreeNode<>(4L,2L,\"module-A-menu-1\",0L));\n\t\tall_menu.add(new TreeNode<>(5L,3L,\"module-B-menu-1\",0L));\n\t\tall_menu.add(new TreeNode<>(6L,3L,\"module-B-menu-2\",0L));\n\t}\n\n\t@Test\n\tpublic void getParentsNameTest() {\n\t\tfinal Tree<Long> tree = TreeUtil.buildSingle(all_menu, 0L);\n\t\tfinal Tree<Long> chid = tree.getChildren().get(0).getChildren().get(0).getChildren().get(0);\n\t\tassertEquals(\"[module-A-menu-1, module-A, root]\", chid.getParentsName(true).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/IssueI9PDVFTest.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.lang.Assert;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class IssueI9PDVFTest {\n\t@Test\n\tpublic void buildTest() {\n\t\tList<TestList> list = new ArrayList<>();\n\t\tlist.add(new TestList(1790187987502895123L, \"顶级\", 0L));\n\t\tlist.add(new TestList(1790187987502895124L, \"子集\", 1790187987502895123L));\n\n\t\tList<Tree<String>> build = TreeUtil.build(list, \"0\", (testList, treeNode) -> {\n\t\t\ttreeNode.setId(testList.getId().toString());\n\t\t\ttreeNode.setName(testList.getName());\n\t\t\ttreeNode.setParentId(testList.getParentId().toString());\n\t\t});\n\n\t\tAssert.notNull(build);\n\t}\n\n\t@AllArgsConstructor\n\t@Data\n\tpublic static class TestList {\n\t\tLong id;\n\t\tString name;\n\t\tLong parentId;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/IssueIAUSHRTest.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.lang.tree.parser.NodeParser;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 如果指定rootId的节点已经存在，直接作为根节点\n */\npublic class IssueIAUSHRTest {\n\n\t@Test\n\tvoid buildSingleTest() {\n\t\tfinal List<TestDept> list= new ArrayList<>();\n\t\tTestDept sysDept = new TestDept();\n\t\tsysDept.setDeptId(1L);\n\t\tsysDept.setDeptName(\"A\");\n\t\tsysDept.setParentId(null);\n\t\tlist.add(sysDept);\n\n\t\tsysDept = new TestDept();\n\t\tsysDept.setDeptId(2L);\n\t\tsysDept.setDeptName(\"B\");\n\t\tsysDept.setParentId(1L);\n\t\tlist.add(sysDept);\n\n\t\tsysDept = new TestDept();\n\t\tsysDept.setDeptId(3L);\n\t\tsysDept.setDeptName(\"C\");\n\t\tsysDept.setParentId(1L);\n\t\tlist.add(sysDept);\n\n\t\tTreeNodeConfig treeNodeConfig = new TreeNodeConfig();\n\t\ttreeNodeConfig.setIdKey(\"deptId\");\n\t\ttreeNodeConfig.setNameKey(\"deptName\");\n\t\ttreeNodeConfig.setParentIdKey(\"parentId\");\n\t\tNodeParser<TestDept,Long> nodeParser= (dept, tree) ->\n\t\t\ttree.setId(dept.getDeptId())\n\t\t\t.setParentId(dept.getParentId())\n\t\t\t.setName(dept.getDeptName());\n\t\tTree<Long> longTree = TreeUtil.buildSingle(list, 1L, treeNodeConfig, nodeParser);\n\n\t\tassertEquals(\"A\", longTree.getName());\n\t\tassertEquals(2, longTree.getChildren().size());\n\t\tassertEquals(\"B\", longTree.getChildren().get(0).getName());\n\t\tassertEquals(\"C\", longTree.getChildren().get(1).getName());\n\t}\n\n\t@Data\n\tpublic static class TestDept {\n\t\tprivate Long deptId;\n\t\tprivate String deptName;\n\t\tprivate Long parentId;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/Issues2538Test.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class Issues2538Test {\n\n\t@Test\n\tpublic void issues2538Test() {\n\t\tTestBean test1 = new TestBean();\n\t\ttest1.setId(1);\n\t\ttest1.setParentId(0);\n\t\ttest1.setName(\"1\");\n\t\tTestBean test2 = new TestBean();\n\t\ttest2.setId(2);\n\t\ttest2.setParentId(1);\n\t\ttest2.setName(\"1\");\n\n\t\tList<TestBean> list = new ArrayList<>();\n\n\t\tlist.add(test1);\n\t\tlist.add(test2);\n\t\t// 配置\n\t\tTreeNodeConfig treeNodeConfig = new TreeNodeConfig();\n\t\t// 自定义属性名 都要默认值的\n\t\ttreeNodeConfig.setIdKey(\"id\");\n\t\ttreeNodeConfig.setDeep(Integer.MAX_VALUE);\n\t\ttreeNodeConfig.setParentIdKey(\"parentId\");\n\n\t\t// 转换器\n\t\tList<Tree<Object>> treeNodes = TreeUtil.build(list, 0L,\n\t\t\t\ttreeNodeConfig,\n\t\t\t\t(treeNode, tree) -> {\n\t\t\t\t\ttree.setId(treeNode.getId());\n\t\t\t\t\ttree.setParentId(treeNode.getParentId());\n\t\t\t\t\ttree.setName(treeNode.getName());\n\t\t\t\t\ttree.setWeight(null);\n\t\t\t\t});\n\n\t\tassertNotNull(treeNodes);\n\n\t\ttry {\n\t\t\t// 转换器\n\t\t\ttreeNodes = TreeUtil.build(list, \"0\",\n\t\t\t\t\ttreeNodeConfig,\n\t\t\t\t\t(treeNode, tree) -> {\n\t\t\t\t\t\ttree.setId(treeNode.getId());\n\t\t\t\t\t\ttree.setParentId(treeNode.getParentId());\n\t\t\t\t\t\ttree.setName(treeNode.getName());\n\t\t\t\t\t\ttree.setWeight(null);\n\t\t\t\t\t});\n\t\t}catch (Exception e) {\n\t\t\tassertEquals(e.getClass(), IllegalArgumentException.class);\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class TestBean {\n\t\tprivate long id;\n\t\tprivate long parentId;\n\t\tprivate String name;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeBuilderTest.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class TreeBuilderTest {\n\n\t@Test\n\tpublic void checkIsBuiltTest(){\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal TreeBuilder<Integer> of = TreeBuilder.of(0);\n\t\t\tof.build();\n\t\t\tof.append(new ArrayList<>());\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeSearchTest.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TreeSearchTest {\n\tstatic List<TreeNode<Long>> all_menu=new ArrayList<>();\n\tstatic {\n\t\t/*\n\t\t * root\n\t\t *    /module-A\n\t\t *    \t   /module-A-menu-1\n\t\t *    /module-B\n\t\t *    \t   /module-B-menu-1\n\t\t *    \t   /module-B-menu-2\n\t\t */\n\t\tall_menu.add(new TreeNode<>(1L, 0L, \"root\", 0L));\n\t\tall_menu.add(new TreeNode<>(2L,1L,\"module-A\",0L));\n\t\tall_menu.add(new TreeNode<>(3L,1L,\"module-B\",0L));\n\t\tall_menu.add(new TreeNode<>(4L,2L,\"module-A-menu-1\",0L));\n\t\tall_menu.add(new TreeNode<>(5L,3L,\"module-B-menu-1\",0L));\n\t\tall_menu.add(new TreeNode<>(6L,3L,\"module-B-menu-2\",0L));\n\t}\n\n\t@Test\n\tpublic void searchNode() {\n\t\tList<Tree<Long>> treeItems=TreeUtil.build(all_menu, 0L);\n\n\t\tTree<Long> tree=treeItems.get(0);\n\t\tTree<Long> searchResult=tree.getNode(3L);\n\n\t\tassertEquals(\"module-B\", searchResult.getName());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeTest.java",
    "content": "package cn.hutool.core.lang.tree;\n\nimport cn.hutool.core.collection.CollUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * 通用树测试\n *\n * @author liangbaikai\n */\npublic class TreeTest {\n\t// 模拟数据\n\tstatic List<TreeNode<String>> nodeList = CollUtil.newArrayList();\n\n\tstatic {\n\t\t// 模拟数据\n\t\tnodeList.add(new TreeNode<>(\"1\", \"0\", \"系统管理\", 5));\n\t\tnodeList.add(new TreeNode<>(\"111\", \"11\", \"用户添加\", 0));\n\t\tnodeList.add(new TreeNode<>(\"11\", \"1\", \"用户管理\", 222222));\n\n\t\tnodeList.add(new TreeNode<>(\"2\", \"0\", \"店铺管理\", 1));\n\t\tnodeList.add(new TreeNode<>(\"21\", \"2\", \"商品管理\", 44));\n\t\tnodeList.add(new TreeNode<>(\"221\", \"2\", \"商品管理2\", 2));\n\t}\n\n\n\t@Test\n\tpublic void sampleTreeTest() {\n\t\tList<Tree<String>> treeList = TreeUtil.build(nodeList, \"0\");\n\t\tfor (Tree<String> tree : treeList) {\n\t\t\tassertNotNull(tree);\n\t\t\tassertEquals(\"0\", tree.getParentId());\n//\t\t\tConsole.log(tree);\n\t\t}\n\n\t\t// 测试通过子节点查找父节点\n\t\tfinal Tree<String> rootNode0 = treeList.get(0);\n\t\tfinal Tree<String> parent = rootNode0.getChildren().get(0).getParent();\n\t\tassertEquals(rootNode0, parent);\n\t}\n\n\t@Test\n\tpublic void treeTest() {\n\n\t\t//配置\n\t\tTreeNodeConfig treeNodeConfig = new TreeNodeConfig();\n\t\t// 自定义属性名 都要默认值的\n\t\ttreeNodeConfig.setWeightKey(\"order\");\n\t\ttreeNodeConfig.setIdKey(\"rid\");\n\t\ttreeNodeConfig.setDeep(2);\n\n\t\t//转换器\n\t\tList<Tree<String>> treeNodes = TreeUtil.build(nodeList, \"0\", treeNodeConfig,\n\t\t\t(treeNode, tree) -> {\n\t\t\t\ttree.setId(treeNode.getId());\n\t\t\t\ttree.setParentId(treeNode.getParentId());\n\t\t\t\ttree.setWeight(treeNode.getWeight());\n\t\t\t\ttree.setName(treeNode.getName());\n\t\t\t\t// 扩展属性 ...\n\t\t\t\ttree.putExtra(\"extraField\", 666);\n\t\t\t\ttree.putExtra(\"other\", new Object());\n\t\t\t});\n\n\t\tassertEquals(treeNodes.size(), 2);\n\t}\n\n\t@Test\n\tpublic void walkTest() {\n\t\tList<String> ids = new ArrayList<>();\n\t\tfinal Tree<String> tree = TreeUtil.buildSingle(nodeList, \"0\");\n\t\ttree.walk((tr) -> ids.add(tr.getId()));\n\n\t\tassertEquals(7, ids.size());\n\t}\n\n\t@Test\n\tpublic void cloneTreeTest() {\n\t\tfinal Tree<String> tree = TreeUtil.buildSingle(nodeList, \"0\");\n\t\tfinal Tree<String> cloneTree = tree.cloneTree();\n\n\t\tList<String> ids = new ArrayList<>();\n\t\tcloneTree.walk((tr) -> ids.add(tr.getId()));\n\n\t\tassertEquals(7, ids.size());\n\t}\n\n\t@Test\n\tpublic void filterTest() {\n\t\t// 经过过滤，丢掉\"用户添加\"节点\n\t\tfinal Tree<String> tree = TreeUtil.buildSingle(nodeList, \"0\");\n\t\ttree.filter((t) -> {\n\t\t\tfinal CharSequence name = t.getName();\n\t\t\treturn null != name && name.toString().contains(\"店铺\");\n\t\t});\n\n\t\tList<String> ids = new ArrayList<>();\n\t\ttree.walk((tr) -> ids.add(tr.getId()));\n\t\tassertEquals(4, ids.size());\n\t}\n\n\t@Test\n\tpublic void filterNewTest() {\n\t\tfinal Tree<String> tree = TreeUtil.buildSingle(nodeList, \"0\");\n\n\t\t// 经过过滤，生成新的树\n\t\tTree<String> newTree = tree.filterNew((t) -> {\n\t\t\tfinal CharSequence name = t.getName();\n\t\t\treturn null != name && name.toString().contains(\"店铺\");\n\t\t});\n\n\t\tList<String> ids = new ArrayList<>();\n\t\tnewTree.walk((tr) -> ids.add(tr.getId()));\n\t\tassertEquals(4, ids.size());\n\n\t\tList<String> ids2 = new ArrayList<>();\n\t\ttree.walk((tr) -> ids2.add(tr.getId()));\n\t\tassertEquals(7, ids2.size());\n\t}\n\n\n\t@Data\n\tstatic class Area {\n\t\tprivate Integer id;\n\t\tprivate String name;\n\t\tprivate Integer parentId;\n\t\tprivate List<Area> childrenList;\n\n\t\tpublic Area(Integer id, String name, Integer parentId) {\n\t\t\tthis.id = id;\n\t\t\tthis.name = name;\n\t\t\tthis.parentId = parentId;\n\t\t}\n\t}\n\t// 模拟数据\n\tstatic List<Area> areaList = CollUtil.newArrayList();\n\tstatic {\n\t\tareaList.add(new Area(1, \"中国\", 0));\n\t\tareaList.add(new Area(2, \"北京\", 1));\n\t\tareaList.add(new Area(3, \"上海\", 1));\n\t\tareaList.add(new Area(4, \"广东\", 1));\n\t\tareaList.add(new Area(5, \"广州\", 4));\n\t\tareaList.add(new Area(6, \"深圳\", 4));\n\t\tareaList.add(new Area(7, \"浙江\", 1));\n\t\tareaList.add(new Area(8, \"杭州\", 7));\n\t}\n\n\t@Test\n\tpublic void builderTest() {\n\t\tList<Area> list = TreeUtil.build(areaList, 0, Area::getId, Area::getParentId, Area::setChildrenList);\n\t\tfinal Area root = list.get(0);\n\t\tfinal Integer parentId = root.getChildrenList().get(0).getParentId();\n\t\tassertEquals(root.getId(), parentId);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/BiMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\npublic class BiMapTest {\n\n\t@Test\n\tpublic void getTest(){\n\t\tfinal BiMap<String, Integer> biMap = new BiMap<>(new HashMap<>());\n\t\tbiMap.put(\"aaa\", 111);\n\t\tbiMap.put(\"bbb\", 222);\n\n\t\tassertEquals(new Integer(111), biMap.get(\"aaa\"));\n\t\tassertEquals(new Integer(222), biMap.get(\"bbb\"));\n\n\t\tassertEquals(\"aaa\", biMap.getKey(111));\n\t\tassertEquals(\"bbb\", biMap.getKey(222));\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentTest(){\n\t\tfinal BiMap<String, Integer> biMap = new BiMap<>(new HashMap<>());\n\t\tbiMap.put(\"aaa\", 111);\n\t\tbiMap.put(\"bbb\", 222);\n\n\t\tbiMap.computeIfAbsent(\"ccc\", s -> 333);\n\t\tassertEquals(new Integer(333), biMap.get(\"ccc\"));\n\t\tassertEquals(\"ccc\", biMap.getKey(333));\n\t}\n\n\t@Test\n\tpublic void putIfAbsentTest(){\n\t\tfinal BiMap<String, Integer> biMap = new BiMap<>(new HashMap<>());\n\t\tbiMap.put(\"aaa\", 111);\n\t\tbiMap.put(\"bbb\", 222);\n\n\t\tbiMap.putIfAbsent(\"ccc\", 333);\n\t\tassertEquals(new Integer(333), biMap.get(\"ccc\"));\n\t\tassertEquals(\"ccc\", biMap.getKey(333));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/CamelCaseMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.SerializeUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CamelCaseMapTest {\n\n\t@Test\n\tpublic void caseInsensitiveMapTest() {\n\t\tCamelCaseMap<String, String> map = new CamelCaseMap<>();\n\t\tmap.put(\"customKey\", \"OK\");\n\t\tassertEquals(\"OK\", map.get(\"customKey\"));\n\t\tassertEquals(\"OK\", map.get(\"custom_key\"));\n\t}\n\n\t@Test\n\tpublic void caseInsensitiveLinkedMapTest() {\n\t\tCamelCaseLinkedMap<String, String> map = new CamelCaseLinkedMap<>();\n\t\tmap.put(\"customKey\", \"OK\");\n\t\tassertEquals(\"OK\", map.get(\"customKey\"));\n\t\tassertEquals(\"OK\", map.get(\"custom_key\"));\n\t}\n\n\t@Test\n\tpublic void serializableKeyFuncTest() {\n\t\tCamelCaseMap<String, String> map = new CamelCaseMap<>();\n\t\tmap.put(\"serializable_key\", \"OK\");\n\t\tCamelCaseMap<String, String> deSerializableMap = SerializeUtil.deserialize(SerializeUtil.serialize(map));\n\t\tassertEquals(\"OK\", deSerializableMap.get(\"serializable_key\"));\n\t\tassertEquals(\"OK\", deSerializableMap.get(\"serializableKey\"));\n\t\tdeSerializableMap.put(\"serializable_func\", \"OK\");\n\t\tassertEquals(\"OK\", deSerializableMap.get(\"serializable_func\"));\n\t\tassertEquals(\"OK\", deSerializableMap.get(\"serializableFunc\"));\n\t}\n\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/CaseInsensitiveMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.lang.Pair;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CaseInsensitiveMapTest {\n\n\t@Test\n\tpublic void caseInsensitiveMapTest() {\n\t\tCaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>();\n\t\tmap.put(\"aAA\", \"OK\");\n\t\tassertEquals(\"OK\", map.get(\"aaa\"));\n\t\tassertEquals(\"OK\", map.get(\"AAA\"));\n\t}\n\n\t@Test\n\tpublic void caseInsensitiveLinkedMapTest() {\n\t\tCaseInsensitiveLinkedMap<String, String> map = new CaseInsensitiveLinkedMap<>();\n\t\tmap.put(\"aAA\", \"OK\");\n\t\tassertEquals(\"OK\", map.get(\"aaa\"));\n\t\tassertEquals(\"OK\", map.get(\"AAA\"));\n\t}\n\n\t@Test\n\tpublic void mergeTest(){\n\t\t//https://github.com/chinabugotech/hutool/issues/2086\n\t\tPair<String, String> b = new Pair<>(\"a\", \"value\");\n\t\tPair<String, String> a = new Pair<>(\"A\", \"value\");\n\t\tfinal CaseInsensitiveMap<Object, Object> map = new CaseInsensitiveMap<>();\n\t\tmap.merge(b.getKey(), b.getValue(), (A, B) -> A);\n\t\tmap.merge(a.getKey(), a.getValue(), (A, B) -> A);\n\n\t\tassertEquals(1, map.size());\n\t}\n\n\t@Test\n\tpublic void issueIA4K4FTest() {\n\t\tMap<String, Object> map = new CaseInsensitiveLinkedMap<>();\n\t\tmap.put(\"b\", 2);\n\t\tmap.put(\"a\", 1);\n\n\t\tAtomicInteger index = new AtomicInteger();\n\t\tmap.forEach((k, v) -> {\n\t\t\tif(0 == index.get()){\n\t\t\t\tassertEquals(\"b\", k);\n\t\t\t} else if(1 == index.get()){\n\t\t\t\tassertEquals(\"a\", k);\n\t\t\t}\n\n\t\t\tindex.getAndIncrement();\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issueIA4K4FTest2() {\n\t\tMap<String, Object> map = new CaseInsensitiveTreeMap<>();\n\t\tmap.put(\"b\", 2);\n\t\tmap.put(\"a\", 1);\n\n\t\tAtomicInteger index = new AtomicInteger();\n\t\tmap.forEach((k, v) -> {\n\t\t\tif(0 == index.get()){\n\t\t\t\tassertEquals(\"a\", k);\n\t\t\t} else if(1 == index.get()){\n\t\t\t\tassertEquals(\"b\", k);\n\t\t\t}\n\n\t\t\tindex.getAndIncrement();\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/CollValueMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.map.multi.ListValueMap;\nimport cn.hutool.core.map.multi.SetValueMap;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashSet;\n\npublic class CollValueMapTest {\n\n\t@Test\n\tpublic void testListValueMapRemove() {\n\t\tfinal ListValueMap<String, String> entries = new ListValueMap<>();\n\t\tentries.putValue(\"one\",\"11\");\n\t\tentries.putValue(\"one\",\"22\");\n\t\tentries.putValue(\"one\",\"33\");\n\t\tentries.putValue(\"one\",\"22\");\n\n\t\tentries.putValue(\"two\",\"44\");\n\t\tentries.putValue(\"two\",\"55\");\n\n\t\tentries.putValue(\"three\",\"11\");\n\n\t\tentries.removeValue(\"one\",\"22\");\n\n\t\tassertEquals(ListUtil.of(\"11\",\"33\",\"22\"), entries.get(\"one\"));\n\n\t\tentries.removeValues(\"two\",ListUtil.of(\"44\",\"55\"));\n\t\tassertEquals(ListUtil.empty(),entries.get(\"two\"));\n\t}\n\n\t@Test\n\tpublic void testSetValueMapRemove() {\n\t\tfinal SetValueMap<String, String> entries = new SetValueMap<>();\n\t\tentries.putValue(\"one\",\"11\");\n\t\tentries.putValue(\"one\",\"22\");\n\t\tentries.putValue(\"one\",\"33\");\n\t\tentries.putValue(\"one\",\"22\");\n\n\t\tentries.putValue(\"two\",\"44\");\n\t\tentries.putValue(\"two\",\"55\");\n\n\t\tentries.putValue(\"three\",\"11\");\n\n\t\tentries.removeValue(\"one\",\"22\");\n\t\tassertEquals(CollUtil.newHashSet(\"11\",\"33\"), entries.get(\"one\"));\n\n\t\tentries.removeValues(\"two\",ListUtil.of(\"44\",\"55\"));\n\t\tassertEquals(CollUtil.empty(HashSet.class),entries.get(\"two\"));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/FuncMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\npublic class FuncMapTest {\n\n\t@Test\n\tpublic void putGetTest(){\n\t\tfinal FuncMap<Object, Object> map = new FuncMap<>(HashMap::new,\n\t\t\t\t(key)->key.toString().toLowerCase(),\n\t\t\t\t(value)->value.toString().toUpperCase());\n\n\t\tmap.put(\"aaa\", \"b\");\n\t\tmap.put(\"BBB\", \"c\");\n\n\t\tassertEquals(\"B\", map.get(\"aaa\"));\n\t\tassertEquals(\"C\", map.get(\"bbb\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/IssueI88R5MTest.java",
    "content": "package cn.hutool.core.map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.LinkedHashMap;\n\npublic class IssueI88R5MTest {\n\t@Test\n\tpublic void biMapTest() {\n\t\tfinal BiMap<String, Integer> biMap = new BiMap<>(new LinkedHashMap<>());\n\t\tbiMap.put(\"aaa\", 111);\n\t\tbiMap.getKey(111);\n\t\tbiMap.put(\"aaa\", 222);\n\t\tassertNull(biMap.getKey(111));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/LinkedForestMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.collection.CollStreamUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class LinkedForestMapTest {\n\n\tprivate final ForestMap<String, String> treeNodeMap = new LinkedForestMap<>(false);\n\n\t@BeforeEach\n\tpublic void beforeTest() {\n\t\t// a -> b -> c -> d\n\t\ttreeNodeMap.putLinkedNodes(\"a\", \"b\", \"bbb\");\n\t\ttreeNodeMap.putLinkedNodes(\"b\", \"c\", \"ccc\");\n\t\ttreeNodeMap.putLinkedNodes(\"c\", \"d\", \"ddd\");\n\t\ttreeNodeMap.get(\"a\").setValue(\"aaa\");\n\t}\n\n\t@Test\n\tpublic void testTreeEntry() {\n\t\tfinal TreeEntry<String, String> parent = treeNodeMap.get(\"b\");\n\t\tfinal TreeEntry<String, String> treeEntry = treeNodeMap.get(\"c\");\n\t\tfinal TreeEntry<String, String> child = treeNodeMap.get(\"d\");\n\n\t\t// Entry相关\n\t\tassertEquals(\"c\", treeEntry.getKey());\n\t\tassertEquals(\"ccc\", treeEntry.getValue());\n\n\t\t// 父节点相关方法\n\t\tassertEquals(2, treeEntry.getWeight());\n\t\tassertEquals(treeNodeMap.get(\"a\"), treeEntry.getRoot());\n\t\tassertTrue(treeEntry.hasParent());\n\t\tassertEquals(parent, treeEntry.getDeclaredParent());\n\t\tassertEquals(treeNodeMap.get(\"a\"), treeEntry.getParent(\"a\"));\n\t\tassertTrue(treeEntry.containsParent(\"a\"));\n\n\t\t// 子节点相关方法\n\t\tfinal List<TreeEntry<String, String>> nodes = new ArrayList<>();\n\t\ttreeEntry.forEachChild(true, nodes::add);\n\t\tassertEquals(CollUtil.newArrayList(treeEntry, child), nodes);\n\t\tnodes.clear();\n\t\ttreeEntry.forEachChild(false, nodes::add);\n\t\tassertEquals(CollUtil.newArrayList(child), nodes);\n\n\t\tassertEquals(CollUtil.newLinkedHashSet(child), new LinkedHashSet<>(treeEntry.getDeclaredChildren().values()));\n\t\tassertEquals(CollUtil.newLinkedHashSet(child), new LinkedHashSet<>(treeEntry.getChildren().values()));\n\t\tassertTrue(treeEntry.hasChildren());\n\t\tassertEquals(treeNodeMap.get(\"d\"), treeEntry.getChild(\"d\"));\n\t\tassertTrue(treeEntry.containsChild(\"d\"));\n\t}\n\n\t@Test\n\tpublic void putTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\n\t\tTreeEntry<String, String> treeEntry = new LinkedForestMap.TreeEntryNode<>(null, \"a\", \"aaa\");\n\t\tassertNull(map.put(\"a\", treeEntry));\n\t\tassertNotEquals(map.get(\"a\"), treeEntry);\n\t\tassertEquals(map.get(\"a\").getKey(), treeEntry.getKey());\n\t\tassertEquals(map.get(\"a\").getValue(), treeEntry.getValue());\n\n\t\ttreeEntry = new LinkedForestMap.TreeEntryNode<>(null, \"a\", \"aaaa\");\n\t\tassertNotNull(map.put(\"a\", treeEntry));\n\t\tassertNotEquals(map.get(\"a\"), treeEntry);\n\t\tassertEquals(map.get(\"a\").getKey(), treeEntry.getKey());\n\t\tassertEquals(map.get(\"a\").getValue(), treeEntry.getValue());\n\t}\n\n\t@Test\n\tpublic void removeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\n\t\tfinal TreeEntry<String, String> a = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tfinal TreeEntry<String, String> c = map.get(\"c\");\n\n\t\tmap.remove(\"b\");\n\t\tassertNull(map.get(\"b\"));\n\t\tassertFalse(b.hasChildren());\n\t\tassertFalse(b.hasParent());\n\t\tassertEquals(a, c.getDeclaredParent());\n\t\tassertEquals(CollUtil.newArrayList(c), new ArrayList<>(a.getDeclaredChildren().values()));\n\t}\n\n\t@Test\n\tpublic void putAllTest() {\n\t\tfinal ForestMap<String, String> source = new LinkedForestMap<>(false);\n\t\tsource.linkNodes(\"a\", \"b\");\n\t\tsource.linkNodes(\"b\", \"c\");\n\n\t\tfinal ForestMap<String, String> target = new LinkedForestMap<>(false);\n\t\ttarget.putAll(source);\n\n\t\tfinal TreeEntry<String, String> a = target.get(\"a\");\n\t\tfinal TreeEntry<String, String> b = target.get(\"b\");\n\t\tfinal TreeEntry<String, String> c = target.get(\"c\");\n\n\t\tassertNotNull(a);\n\t\tassertEquals(\"a\", a.getKey());\n\t\tassertEquals(CollUtil.newArrayList(b, c), new ArrayList<>(a.getChildren().values()));\n\n\t\tassertNotNull(b);\n\t\tassertEquals(\"b\", b.getKey());\n\t\tassertEquals(CollUtil.newArrayList(c), new ArrayList<>(b.getChildren().values()));\n\n\t\tassertNotNull(c);\n\t\tassertEquals(\"c\", c.getKey());\n\t\tassertEquals(CollUtil.newArrayList(), new ArrayList<>(c.getChildren().values()));\n\n\t}\n\n\t@Test\n\tpublic void clearTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\n\t\tfinal TreeEntry<String, String> a = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tfinal TreeEntry<String, String> c = map.get(\"c\");\n\t\tassertFalse(a.hasParent());\n\t\tassertTrue(a.hasChildren());\n\t\tassertTrue(b.hasParent());\n\t\tassertTrue(b.hasChildren());\n\t\tassertTrue(c.hasParent());\n\t\tassertFalse(c.hasChildren());\n\n\t\tmap.clear();\n\t\tassertFalse(a.hasParent());\n\t\tassertFalse(a.hasChildren());\n\t\tassertFalse(b.hasParent());\n\t\tassertFalse(b.hasChildren());\n\t\tassertFalse(c.hasParent());\n\t\tassertFalse(c.hasChildren());\n\t}\n\n\t@Test\n\tpublic void getNodeValueTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.putNode(\"a\", \"aaa\");\n\t\tassertEquals(\"aaa\", map.getNodeValue(\"a\"));\n\t\tassertNull(map.getNodeValue(\"b\"));\n\t}\n\n\t@Test\n\tpublic void putAllNodeTest() {\n\t\tfinal ForestMap<String, Map<String, String>> map = new LinkedForestMap<>(false);\n\n\t\tfinal Map<String, String> aMap = MapBuilder.<String, String> create()\n\t\t\t.put(\"pid\", null)\n\t\t\t.put(\"id\", \"a\")\n\t\t\t.build();\n\t\tfinal Map<String, String> bMap = MapBuilder.<String, String> create()\n\t\t\t.put(\"pid\", \"a\")\n\t\t\t.put(\"id\", \"b\")\n\t\t\t.build();\n\t\tfinal Map<String, String> cMap = MapBuilder.<String, String> create()\n\t\t\t.put(\"pid\", \"b\")\n\t\t\t.put(\"id\", \"c\")\n\t\t\t.build();\n\t\tmap.putAllNode(Arrays.asList(aMap, bMap, cMap), m -> m.get(\"id\"), m -> m.get(\"pid\"), true);\n\n\t\tfinal TreeEntry<String, Map<String, String>> a = map.get(\"a\");\n\t\tassertNotNull(a);\n\t\tfinal TreeEntry<String, Map<String, String>> b = map.get(\"b\");\n\t\tassertNotNull(b);\n\t\tfinal TreeEntry<String, Map<String, String>> c = map.get(\"c\");\n\t\tassertNotNull(c);\n\n\t\tassertNull(a.getDeclaredParent());\n\t\tassertEquals(a, b.getDeclaredParent());\n\t\tassertEquals(b, c.getDeclaredParent());\n\n\t\tassertEquals(aMap, a.getValue());\n\t\tassertEquals(bMap, b.getValue());\n\t\tassertEquals(cMap, c.getValue());\n\t}\n\n\t@Test\n\tpublic void putNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\n\t\tassertNull(map.get(\"a\"));\n\n\t\tmap.putNode(\"a\", \"aaa\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertEquals(\"aaa\", map.get(\"a\").getValue());\n\n\t\tmap.putNode(\"a\", \"aaaa\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertEquals(\"aaaa\", map.get(\"a\").getValue());\n\t}\n\n\t@Test\n\tpublic void putLinkedNodesTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\n\t\tassertNull(map.get(\"a\"));\n\t\tassertNull(map.get(\"b\"));\n\n\t\tmap.putLinkedNodes(\"a\", \"b\", \"bbb\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertNull(map.get(\"a\").getValue());\n\t\tassertNotNull(map.get(\"b\"));\n\t\tassertEquals(\"bbb\", map.get(\"b\").getValue());\n\n\t\tmap.putLinkedNodes(\"a\", \"b\", \"bbbb\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertNull(map.get(\"a\").getValue());\n\t\tassertNotNull(map.get(\"b\"));\n\t\tassertEquals(\"bbbb\", map.get(\"b\").getValue());\n\t}\n\n\t@Test\n\tpublic void putLinkedNodesTest2() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\n\t\tassertNull(map.get(\"a\"));\n\t\tassertNull(map.get(\"b\"));\n\n\t\tmap.putLinkedNodes(\"a\", \"aaa\", \"b\", \"bbb\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertEquals(\"aaa\", map.get(\"a\").getValue());\n\t\tassertNotNull(map.get(\"b\"));\n\t\tassertEquals(\"bbb\", map.get(\"b\").getValue());\n\n\t\tmap.putLinkedNodes(\"a\", \"aaaa\", \"b\", \"bbbb\");\n\t\tassertNotNull(map.get(\"a\"));\n\t\tassertEquals(\"aaaa\", map.get(\"a\").getValue());\n\t\tassertNotNull(map.get(\"b\"));\n\t\tassertEquals(\"bbbb\", map.get(\"b\").getValue());\n\t}\n\n\t@Test\n\tpublic void linkNodesTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\n\t\tfinal TreeEntry<String, String> parent = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> child = map.get(\"b\");\n\n\t\tassertNotNull(parent);\n\t\tassertEquals(\"a\", parent.getKey());\n\t\tassertEquals(child, parent.getChild(\"b\"));\n\n\t\tassertNotNull(child);\n\t\tassertEquals(\"b\", child.getKey());\n\t\tassertEquals(parent, child.getDeclaredParent());\n\t}\n\n\t@Test\n\tpublic void unlinkNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tfinal TreeEntry<String, String> parent = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> child = map.get(\"b\");\n\t\tmap.unlinkNode(\"a\", \"b\");\n\t\tassertFalse(child.hasParent());\n\t\tassertFalse(parent.hasChildren());\n\t}\n\n\t@Test\n\tpublic void getTreeNodesTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\n\t\tfinal List<String> expected = CollUtil.newArrayList(\"a\", \"b\", \"c\");\n\t\tList<String> actual = CollStreamUtil.toList(map.getTreeNodes(\"a\"), TreeEntry::getKey);\n\t\tassertEquals(expected, actual);\n\t\tactual = CollStreamUtil.toList(map.getTreeNodes(\"b\"), TreeEntry::getKey);\n\t\tassertEquals(expected, actual);\n\t\tactual = CollStreamUtil.toList(map.getTreeNodes(\"c\"), TreeEntry::getKey);\n\t\tassertEquals(expected, actual);\n\t}\n\n\t@Test\n\tpublic void getRootNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\n\t\tfinal TreeEntry<String, String> root = map.get(\"a\");\n\t\tassertEquals(root, map.getRootNode(\"a\"));\n\t\tassertEquals(root, map.getRootNode(\"b\"));\n\t\tassertEquals(root, map.getRootNode(\"c\"));\n\t}\n\n\t@Test\n\tpublic void getDeclaredParentNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tfinal TreeEntry<String, String> a = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tassertEquals(a, map.getDeclaredParentNode(\"b\"));\n\t\tassertEquals(b, map.getDeclaredParentNode(\"c\"));\n\t}\n\n\t@Test\n\tpublic void getParentNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tfinal TreeEntry<String, String> a = map.get(\"a\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\n\t\tassertEquals(a, map.getParentNode(\"c\", \"a\"));\n\t\tassertEquals(b, map.getParentNode(\"c\", \"b\"));\n\t\tassertEquals(a, map.getParentNode(\"b\", \"a\"));\n\t\tassertNull(map.getParentNode(\"a\", \"a\"));\n\t}\n\n\t@Test\n\tpublic void containsParentNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tassertTrue(map.containsParentNode(\"c\", \"b\"));\n\t\tassertTrue(map.containsParentNode(\"c\", \"a\"));\n\t\tassertTrue(map.containsParentNode(\"b\", \"a\"));\n\t\tassertFalse(map.containsParentNode(\"a\", \"a\"));\n\t}\n\n\t@Test\n\tpublic void containsChildNodeTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tassertNotNull(b);\n\t\tfinal TreeEntry<String, String> c = map.get(\"c\");\n\t\tassertNotNull(c);\n\n\t\tassertTrue(map.containsChildNode(\"a\", \"b\"));\n\t\tassertTrue(map.containsChildNode(\"a\", \"c\"));\n\t\tassertTrue(map.containsChildNode(\"b\", \"c\"));\n\t\tassertFalse(map.containsChildNode(\"c\", \"c\"));\n\t}\n\n\t@Test\n\tpublic void getDeclaredChildNodesTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tfinal TreeEntry<String, String> c = map.get(\"c\");\n\n\t\tassertEquals(CollUtil.newArrayList(b), new ArrayList<>(map.getDeclaredChildNodes(\"a\")));\n\t\tassertEquals(CollUtil.newArrayList(c), new ArrayList<>(map.getDeclaredChildNodes(\"b\")));\n\t\tassertEquals(CollUtil.newArrayList(), new ArrayList<>(map.getDeclaredChildNodes(\"c\")));\n\t}\n\n\t@Test\n\tpublic void getChildNodesTest() {\n\t\tfinal ForestMap<String, String> map = new LinkedForestMap<>(false);\n\t\tmap.linkNodes(\"a\", \"b\");\n\t\tmap.linkNodes(\"b\", \"c\");\n\t\tfinal TreeEntry<String, String> b = map.get(\"b\");\n\t\tfinal TreeEntry<String, String> c = map.get(\"c\");\n\n\t\tassertEquals(CollUtil.newArrayList(b, c), new ArrayList<>(map.getChildNodes(\"a\")));\n\t\tassertEquals(CollUtil.newArrayList(c), new ArrayList<>(map.getChildNodes(\"b\")));\n\t\tassertEquals(CollUtil.newArrayList(), new ArrayList<>(map.getChildNodes(\"c\")));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/MapBuilderTest.java",
    "content": "package cn.hutool.core.map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class MapBuilderTest {\n\n\t@Test\n\tpublic void conditionPutTest() {\n\t\tMap<String, String> map = MapBuilder.<String, String>create()\n\t\t\t\t.put(true, \"a\", \"1\")\n\t\t\t\t.put(false, \"b\", \"2\")\n\t\t\t\t.put(true, \"c\", () -> getValue(3))\n\t\t\t\t.put(false, \"d\", () -> getValue(4))\n\t\t\t\t.build();\n\n\t\tassertEquals(map.get(\"a\"), \"1\");\n\t\tassertFalse(map.containsKey(\"b\"));\n\t\tassertEquals(map.get(\"c\"), \"3\");\n\t\tassertFalse(map.containsKey(\"d\"));\n\t}\n\n\tpublic String getValue(int value) {\n\t\treturn String.valueOf(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class MapUtilTest {\n\n\tenum PeopleEnum {GIRL, BOY, CHILD}\n\n\t@Data\n\t@Builder\n\tpublic static class User {\n\t\tprivate Long id;\n\t\tprivate String name;\n\t}\n\n\t@Data\n\t@Builder\n\tpublic static class Group {\n\t\tprivate Long id;\n\t\tprivate List<User> users;\n\t}\n\n\t@Data\n\t@Builder\n\tpublic static class UserGroup {\n\t\tprivate Long userId;\n\t\tprivate Long groupId;\n\t}\n\n\n\t@Test\n\tpublic void filterTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tfinal Map<String, String> map2 = MapUtil.filter(map, t -> Convert.toInt(t.getValue()) % 2 == 0);\n\n\t\tassertEquals(2, map2.size());\n\n\t\tassertEquals(\"2\", map2.get(\"b\"));\n\t\tassertEquals(\"4\", map2.get(\"d\"));\n\t}\n\n\t@Test\n\tpublic void mapTest() {\n\t\t// Add test like a foreigner\n\t\tfinal Map<Integer, String> adjectivesMap = MapUtil.<Integer, String>builder()\n\t\t\t.put(0, \"lovely\")\n\t\t\t.put(1, \"friendly\")\n\t\t\t.put(2, \"happily\")\n\t\t\t.build();\n\n\t\tfinal Map<Integer, String> resultMap = MapUtil.map(adjectivesMap, (k, v) -> v + \" \" + PeopleEnum.values()[k].name().toLowerCase());\n\n\t\tassertEquals(\"lovely girl\", resultMap.get(0));\n\t\tassertEquals(\"friendly boy\", resultMap.get(1));\n\t\tassertEquals(\"happily child\", resultMap.get(2));\n\n\t\t// 下单用户，Queue表示正在 .排队. 抢我抢不到的二次元周边！\n\t\tfinal Queue<String> customers = new ArrayDeque<>(Arrays.asList(\"刑部尚书手工耿\", \"木瓜大盗大漠叔\", \"竹鼠发烧找华农\", \"朴实无华朱一旦\"));\n\t\t// 分组\n\t\tfinal List<Group> groups = Stream.iterate(0L, i -> ++i).limit(4).map(i -> Group.builder().id(i).build()).collect(Collectors.toList());\n\t\t// 如你所见，它是一个map，key由用户id，value由用户组成\n\t\tfinal Map<Long, User> idUserMap = Stream.iterate(0L, i -> ++i).limit(4).map(i -> User.builder().id(i).name(customers.poll()).build()).collect(Collectors.toMap(User::getId, Function.identity()));\n\t\t// 如你所见，它是一个map，key由分组id，value由用户ids组成，典型的多对多关系\n\t\tfinal Map<Long, List<Long>> groupIdUserIdsMap = groups.stream().flatMap(group -> idUserMap.keySet().stream().map(userId -> UserGroup.builder().groupId(group.getId()).userId(userId).build()))\n\t\t\t.collect(Collectors.groupingBy(UserGroup::getGroupId, Collectors.mapping(UserGroup::getUserId, Collectors.toList())));\n\n\t\t// 神奇的魔法发生了， 分组id和用户ids组成的map，竟然变成了订单编号和用户实体集合组成的map\n\t\tfinal Map<Long, List<User>> groupIdUserMap = MapUtil.map(groupIdUserIdsMap, (groupId, userIds) -> userIds.stream().map(idUserMap::get).collect(Collectors.toList()));\n\n\t\t// 然后你就可以拿着这个map，去封装groups，使其能够在订单数据带出客户信息啦\n\t\tgroups.forEach(group -> Opt.ofNullable(group.getId()).map(groupIdUserMap::get).ifPresent(group::setUsers));\n\n\t\t// 下面是测试报告\n\t\tgroups.forEach(group -> {\n\t\t\tfinal List<User> users = group.getUsers();\n\t\t\tassertEquals(\"刑部尚书手工耿\", users.get(0).getName());\n\t\t\tassertEquals(\"木瓜大盗大漠叔\", users.get(1).getName());\n\t\t\tassertEquals(\"竹鼠发烧找华农\", users.get(2).getName());\n\t\t\tassertEquals(\"朴实无华朱一旦\", users.get(3).getName());\n\t\t});\n\t\t// 对null友好\n\t\tMapUtil.map(MapUtil.of(0, 0), (k, v) -> null).forEach((k, v) -> assertNull(v));\n\t}\n\n\t@Test\n\tpublic void filterMapWrapperTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tfinal Map<String, String> camelCaseMap = MapUtil.toCamelCaseMap(map);\n\n\t\tfinal Map<String, String> map2 = MapUtil.filter(camelCaseMap, t -> Convert.toInt(t.getValue()) % 2 == 0);\n\n\t\tassertEquals(2, map2.size());\n\n\t\tassertEquals(\"2\", map2.get(\"b\"));\n\t\tassertEquals(\"4\", map2.get(\"d\"));\n\t}\n\n\t@Test\n\tpublic void filterContainsTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"abc\", \"1\");\n\t\tmap.put(\"bcd\", \"2\");\n\t\tmap.put(\"def\", \"3\");\n\t\tmap.put(\"fgh\", \"4\");\n\n\t\tfinal Map<String, String> map2 = MapUtil.filter(map, t -> StrUtil.contains(t.getKey(), \"bc\"));\n\t\tassertEquals(2, map2.size());\n\t\tassertEquals(\"1\", map2.get(\"abc\"));\n\t\tassertEquals(\"2\", map2.get(\"bcd\"));\n\t}\n\n\t@Test\n\tpublic void editTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tfinal Map<String, String> map2 = MapUtil.edit(map, t -> {\n\t\t\t// 修改每个值使之*10\n\t\t\tt.setValue(t.getValue() + \"0\");\n\t\t\treturn t;\n\t\t});\n\n\t\tassertEquals(4, map2.size());\n\n\t\tassertEquals(\"10\", map2.get(\"a\"));\n\t\tassertEquals(\"20\", map2.get(\"b\"));\n\t\tassertEquals(\"30\", map2.get(\"c\"));\n\t\tassertEquals(\"40\", map2.get(\"d\"));\n\t}\n\n\t@Test\n\tpublic void reverseTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tfinal Map<String, String> map2 = MapUtil.reverse(map);\n\n\t\tassertEquals(\"a\", map2.get(\"1\"));\n\t\tassertEquals(\"b\", map2.get(\"2\"));\n\t\tassertEquals(\"c\", map2.get(\"3\"));\n\t\tassertEquals(\"d\", map2.get(\"4\"));\n\t}\n\n\t@Test\n\tpublic void toObjectArrayTest() {\n\t\tfinal Map<String, String> map = MapUtil.newHashMap(true);\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tfinal Object[][] objectArray = MapUtil.toObjectArray(map);\n\t\tassertEquals(\"a\", objectArray[0][0]);\n\t\tassertEquals(\"1\", objectArray[0][1]);\n\t\tassertEquals(\"b\", objectArray[1][0]);\n\t\tassertEquals(\"2\", objectArray[1][1]);\n\t\tassertEquals(\"c\", objectArray[2][0]);\n\t\tassertEquals(\"3\", objectArray[2][1]);\n\t\tassertEquals(\"d\", objectArray[3][0]);\n\t\tassertEquals(\"4\", objectArray[3][1]);\n\t}\n\n\t@Test\n\tpublic void sortJoinTest() {\n\t\tfinal Map<String, String> build = MapUtil.builder(new HashMap<String, String>())\n\t\t\t.put(\"key1\", \"value1\")\n\t\t\t.put(\"key3\", \"value3\")\n\t\t\t.put(\"key2\", \"value2\").build();\n\n\t\tfinal String join1 = MapUtil.sortJoin(build, StrUtil.EMPTY, StrUtil.EMPTY, false);\n\t\tassertEquals(\"key1value1key2value2key3value3\", join1);\n\n\t\tfinal String join2 = MapUtil.sortJoin(build, StrUtil.EMPTY, StrUtil.EMPTY, false, \"123\");\n\t\tassertEquals(\"key1value1key2value2key3value3123\", join2);\n\n\t\tfinal String join3 = MapUtil.sortJoin(build, StrUtil.EMPTY, StrUtil.EMPTY, false, \"123\", \"abc\");\n\t\tassertEquals(\"key1value1key2value2key3value3123abc\", join3);\n\t}\n\n\t@Test\n\tpublic void ofEntriesTest() {\n\t\tfinal Map<String, Integer> map = MapUtil.ofEntries(MapUtil.entry(\"a\", 1), MapUtil.entry(\"b\", 2));\n\t\tassertEquals(2, map.size());\n\n\t\tassertEquals(Integer.valueOf(1), map.get(\"a\"));\n\t\tassertEquals(Integer.valueOf(2), map.get(\"b\"));\n\t}\n\n\t@Test\n\tpublic void ofEntriesSimpleEntryTest() {\n\t\tfinal Map<String, Integer> map = MapUtil.ofEntries(\n\t\t\tMapUtil.entry(\"a\", 1, false),\n\t\t\tMapUtil.entry(\"b\", 2, false)\n\t\t);\n\t\tassertEquals(2, map.size());\n\n\t\tassertEquals(Integer.valueOf(1), map.get(\"a\"));\n\t\tassertEquals(Integer.valueOf(2), map.get(\"b\"));\n\t}\n\n\t@Test\n\tpublic void getIntTest() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\tfinal HashMap<String, String> map = MapUtil.of(\"age\", \"d\");\n\t\t\tfinal Integer age = MapUtil.getInt(map, \"age\");\n\t\t\tassertNotNull(age);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void joinIgnoreNullTest() {\n\t\tfinal Dict v1 = Dict.of().set(\"id\", 12).set(\"name\", \"张三\").set(\"age\", null);\n\t\tfinal String s = MapUtil.joinIgnoreNull(v1, \",\", \"=\");\n\t\tassertEquals(\"id=12,name=张三\", s);\n\t}\n\n\t@Test\n\tpublic void renameKeyTest() {\n\t\tfinal Dict v1 = Dict.of().set(\"id\", 12).set(\"name\", \"张三\").set(\"age\", null);\n\t\tfinal Map<String, Object> map = MapUtil.renameKey(v1, \"name\", \"newName\");\n\t\tassertEquals(\"张三\", map.get(\"newName\"));\n\t}\n\n\t@Test\n\tpublic void renameKeyMapEmptyNoChange() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tMap<String, String> result = MapUtil.renameKey(map, \"oldKey\", \"newKey\");\n\t\tassertTrue(result.isEmpty());\n\t}\n\n\t@Test\n\tpublic void renameKeyOldKeyNotPresentNoChange() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"anotherKey\", \"value\");\n\t\tMap<String, String> result = MapUtil.renameKey(map, \"oldKey\", \"newKey\");\n\t\tassertEquals(1, result.size());\n\t\tassertEquals(\"value\", result.get(\"anotherKey\"));\n\t}\n\n\t@Test\n\tpublic void renameKeyOldKeyPresentNewKeyNotPresentKeyRenamed() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"oldKey\", \"value\");\n\t\tMap<String, String> result = MapUtil.renameKey(map, \"oldKey\", \"newKey\");\n\t\tassertEquals(1, result.size());\n\t\tassertEquals(\"value\", result.get(\"newKey\"));\n\t}\n\n\t@Test\n\tpublic void renameKeyNewKeyPresentThrowsException() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"oldKey\", \"value\");\n\t\tmap.put(\"newKey\", \"existingValue\");\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tMapUtil.renameKey(map, \"oldKey\", \"newKey\");\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issue3162Test() {\n\t\tfinal Map<String, Object> map = new HashMap<String, Object>() {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t{\n\t\t\t\tput(\"a\", \"1\");\n\t\t\t\tput(\"b\", \"2\");\n\t\t\t\tput(\"c\", \"3\");\n\t\t\t}\n\t\t};\n\t\tfinal Map<String, Object> filtered = MapUtil.filter(map, \"a\", \"b\");\n\t\tassertEquals(2, filtered.size());\n\t\tassertEquals(\"1\", filtered.get(\"a\"));\n\t\tassertEquals(\"2\", filtered.get(\"b\"));\n\t}\n\n\n\t@Test\n\tpublic void partitionNullMapThrowsException() {\n\t\tassertThrows(IllegalArgumentException.class, () -> MapUtil.partition(null, 2));\n\t}\n\n\t@Test\n\tpublic void partitionSizeZeroThrowsException() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tassertThrows(IllegalArgumentException.class, () -> MapUtil.partition(map, 0));\n\t}\n\n\t@Test\n\tpublic void partitionSizeNegativeThrowsException() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tassertThrows(IllegalArgumentException.class, () -> MapUtil.partition(map, -1));\n\t}\n\n\t@Test\n\tpublic void partitionEmptyMapReturnsEmptyList() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tList<Map<String, String>> result = MapUtil.partition(map, 2);\n\t\tassertTrue(result.isEmpty());\n\t}\n\n\t@Test\n\tpublic void partitionMapSizeMultipleOfSizePartitionsCorrectly() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\n\t\tList<Map<String, String>> result = MapUtil.partition(map, 2);\n\n\t\tassertEquals(2, result.size());\n\t\tassertEquals(2, result.get(0).size());\n\t\tassertEquals(2, result.get(1).size());\n\t}\n\n\t@Test\n\tpublic void partitionMapSizeNotMultipleOfSizePartitionsCorrectly() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\t\tmap.put(\"e\", \"5\");\n\n\t\tList<Map<String, String>> result = MapUtil.partition(map, 2);\n\n\t\tassertEquals(3, result.size());\n\t\tassertEquals(2, result.get(0).size());\n\t\tassertEquals(2, result.get(1).size());\n\t\tassertEquals(1, result.get(2).size());\n\t}\n\n\t@Test\n\tpublic void partitionGeneralCasePartitionsCorrectly() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tmap.put(\"d\", \"4\");\n\t\tmap.put(\"e\", \"5\");\n\t\tmap.put(\"f\", \"6\");\n\n\t\tList<Map<String, String>> result = MapUtil.partition(map, 3);\n\n\t\tassertEquals(2, result.size());\n\t\tassertEquals(3, result.get(0).size());\n\t\tassertEquals(3, result.get(1).size());\n\t}\n\n\n\t// ---------MapUtil.computeIfAbsentForJdk8\n\t@Test\n\tpublic void computeIfAbsentForJdk8KeyExistsReturnsExistingValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tmap.put(\"key\", 10);\n\t\tInteger result = MapUtil.computeIfAbsentForJdk8(map, \"key\", k -> 20);\n\t\tassertEquals(10, result);\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentForJdk8KeyDoesNotExistComputesAndInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tInteger result = MapUtil.computeIfAbsentForJdk8(map, \"key\", k -> 20);\n\t\tassertEquals(20, result);\n\t\tassertEquals(20, map.get(\"key\"));\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentForJdk8ConcurrentInsertReturnsOldValue() {\n\t\tConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();\n\t\tconcurrentMap.put(\"key\", 30);\n\t\tAtomicInteger counter = new AtomicInteger(0);\n\n\t\t// 模拟并发插入\n\t\tconcurrentMap.computeIfAbsent(\"key\", k -> {\n\t\t\tcounter.incrementAndGet();\n\t\t\treturn 40;\n\t\t});\n\n\t\tInteger result = MapUtil.computeIfAbsentForJdk8(concurrentMap, \"key\", k -> 50);\n\t\tassertEquals(30, result);\n\t\tassertEquals(30, concurrentMap.get(\"key\"));\n\t\tassertEquals(0, counter.get());\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentForJdk8NullValueComputesAndInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tmap.put(\"key\", null);\n\t\tInteger result = MapUtil.computeIfAbsentForJdk8(map, \"key\", k -> 20);\n\t\tassertEquals(20, result);\n\t\tassertEquals(20, map.get(\"key\"));\n\t}\n\n\t//--------MapUtil.computeIfAbsent\n\t@Test\n\tpublic void computeIfAbsentKeyExistsReturnsExistingValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tmap.put(\"key\", 10);\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"key\", k -> 20);\n\t\tassertEquals(10, result);\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentKeyDoesNotExistComputesAndInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"key\", k -> 20);\n\t\tassertEquals(20, result);\n\t\tassertEquals(20, map.get(\"key\"));\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentConcurrentInsertReturnsOldValue() {\n\t\tConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();\n\t\tconcurrentMap.put(\"key\", 30);\n\t\tAtomicInteger counter = new AtomicInteger(0);\n\n\t\t// 模拟并发插入\n\t\tconcurrentMap.computeIfAbsent(\"key\", k -> {\n\t\t\tcounter.incrementAndGet();\n\t\t\treturn 40;\n\t\t});\n\n\t\tInteger result = MapUtil.computeIfAbsent(concurrentMap, \"key\", k -> 50);\n\t\tassertEquals(30, result);\n\t\tassertEquals(30, concurrentMap.get(\"key\"));\n\t\tassertEquals(0, counter.get());\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentNullValueComputesAndInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tmap.put(\"key\", null);\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"key\", k -> 20);\n\t\tassertEquals(20, result);\n\t\tassertEquals(20, map.get(\"key\"));\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentEmptyMapInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"newKey\", k -> 100);\n\t\tassertEquals(100, result);\n\t\tassertEquals(100, map.get(\"newKey\"));\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentJdk8KeyExistsReturnsExistingValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\t// 假设JdkUtil.ISJDK8为true\n\t\tmap.put(\"key\", 10);\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"key\", k -> 20);\n\t\tassertEquals(10, result);\n\t}\n\n\t@Test\n\tpublic void computeIfAbsentJdk8KeyDoesNotExistComputesAndInsertsValue() {\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\t// 假设JdkUtil.ISJDK8为true\n\t\tInteger result = MapUtil.computeIfAbsent(map, \"key\", k -> 20);\n\t\tassertEquals(20, result);\n\t\tassertEquals(20, map.get(\"key\"));\n\t}\n\n\n\t//----------valuesOfKeys\n\t@Test\n\tpublic void valuesOfKeysEmptyIteratorReturnsEmptyList() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tIterator<String> emptyIterator = Collections.emptyIterator();\n\t\tArrayList<String> result = MapUtil.valuesOfKeys(map, emptyIterator);\n\t\tassertEquals(new ArrayList<String>(), result);\n\t}\n\n\t@Test\n\tpublic void valuesOfKeysNonEmptyIteratorReturnsValuesList() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tIterator<String> iterator = new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = -4593258366224032110L;\n\n\t\t\t{\n\t\t\t\tadd(\"a\");\n\t\t\t\tadd(\"b\");\n\t\t\t}\n\t\t}.iterator();\n\t\tArrayList<String> result = MapUtil.valuesOfKeys(map, iterator);\n\t\tassertEquals(new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = 7218152799308667271L;\n\n\t\t\t{\n\t\t\t\tadd(\"1\");\n\t\t\t\tadd(\"2\");\n\t\t\t}\n\t\t}, result);\n\t}\n\n\t@Test\n\tpublic void valuesOfKeysKeysNotInMapReturnsNulls() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tIterator<String> iterator = new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = -5479427021989481058L;\n\n\t\t\t{\n\t\t\t\tadd(\"d\");\n\t\t\t\tadd(\"e\");\n\t\t\t}\n\t\t}.iterator();\n\t\tArrayList<String> result = MapUtil.valuesOfKeys(map, iterator);\n\t\tassertEquals(new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = 4390715387901549136L;\n\n\t\t\t{\n\t\t\t\tadd(null);\n\t\t\t\tadd(null);\n\t\t\t}\n\t\t}, result);\n\t}\n\n\t@Test\n\tpublic void valuesOfKeysMixedKeysReturnsMixedValues() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"1\");\n\t\tmap.put(\"b\", \"2\");\n\t\tmap.put(\"c\", \"3\");\n\t\tIterator<String> iterator = new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = 8510595063492828968L;\n\n\t\t\t{\n\t\t\t\tadd(\"a\");\n\t\t\t\tadd(\"d\");\n\t\t\t\tadd(\"b\");\n\t\t\t}\n\t\t}.iterator();\n\t\tArrayList<String> result = MapUtil.valuesOfKeys(map, iterator);\n\t\tassertEquals(new ArrayList<String>() {\n\t\t\tprivate static final long serialVersionUID = 6383576410597048337L;\n\t\t\t{\n\t\t\t\tadd(\"1\");\n\t\t\t\tadd(null);\n\t\t\t\tadd(\"2\");\n\t\t\t}\n\t\t}, result);\n\t}\n\n\t//--------clear\n\t@Test\n\tpublic void clearNoMapsProvidedNoAction() {\n\t\tMapUtil.clear();\n\t\t// 预期没有异常发生，且没有Map被处理\n\t}\n\n\t@Test\n\tpublic void clearEmptyMapNoChange() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tMapUtil.clear(map);\n\t\tassertTrue(map.isEmpty());\n\t}\n\n\t@Test\n\tpublic void clearNonEmptyMapClearsMap() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"key\", \"value\");\n\t\tMapUtil.clear(map);\n\t\tassertTrue(map.isEmpty());\n\t}\n\n\t@Test\n\tpublic void clearMultipleMapsClearsNonEmptyMaps() {\n\t\tMap<String, String> map1 = new HashMap<>();\n\t\tmap1.put(\"key1\", \"value1\");\n\n\t\tMap<String, String> map2 = new HashMap<>();\n\t\tmap2.put(\"key2\", \"value2\");\n\n\t\tMap<String, String> map3 = new HashMap<>();\n\n\t\tMapUtil.clear(map1, map2, map3);\n\n\t\tassertTrue(map1.isEmpty());\n\t\tassertTrue(map2.isEmpty());\n\t\tassertTrue(map3.isEmpty());\n\t}\n\n\t@Test\n\tpublic void clearMixedMapsClearsNonEmptyMaps() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"key\", \"value\");\n\n\t\tMap<String, String> emptyMap = new HashMap<>();\n\n\t\tMapUtil.clear(map, emptyMap);\n\n\t\tassertTrue(map.isEmpty());\n\t\tassertTrue(emptyMap.isEmpty());\n\t}\n\n\t//-----empty\n\n\t@Test\n\tpublic void emptyNoParametersReturnsEmptyMap() {\n\t\tMap<String, String> emptyMap = MapUtil.empty();\n\t\tassertTrue(emptyMap.isEmpty(), \"The map should be empty.\");\n\t\tassertSame(Collections.emptyMap(), emptyMap, \"The map should be the same instance as Collections.emptyMap().\");\n\t}\n\n\t@Test\n\tpublic void emptyNullMapClassReturnsEmptyMap() {\n\t\tMap<String, String> emptyMap = MapUtil.empty(null);\n\t\tassertTrue(emptyMap.isEmpty(), \"The map should be empty.\");\n\t\tassertSame(Collections.emptyMap(), emptyMap, \"The map should be the same instance as Collections.emptyMap().\");\n\t}\n\n\t@Test\n\tpublic void emptyNavigableMapClassReturnsEmptyNavigableMap() {\n\t\tMap<?, ?> map = MapUtil.empty(NavigableMap.class);\n\t\tassertTrue(map.isEmpty());\n\t\tassertInstanceOf(NavigableMap.class, map);\n\t}\n\n\t@Test\n\tpublic void emptySortedMapClassReturnsEmptySortedMap() {\n\t\tMap<?, ?> map = MapUtil.empty(SortedMap.class);\n\t\tassertTrue(map.isEmpty());\n\t\tassertInstanceOf(SortedMap.class, map);\n\t}\n\n\t@Test\n\tpublic void emptyMapClassReturnsEmptyMap() {\n\t\tMap<?, ?> map = MapUtil.empty(Map.class);\n\t\tassertTrue(map.isEmpty());\n\t}\n\n\t@Test\n\tpublic void emptyUnsupportedMapClassThrowsIllegalArgumentException() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tMapUtil.empty(TreeMap.class);\n\t\t});\n\t}\n\n\t//--------removeNullValue\n\t@Test\n\tpublic void removeNullValueNullMapReturnsNull() {\n\t\tMap<String, String> result = MapUtil.removeNullValue(null);\n\t\tassertNull(result);\n\t}\n\n\t@Test\n\tpublic void removeNullValueEmptyMapReturnsEmptyMap() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tMap<String, String> result = MapUtil.removeNullValue(map);\n\t\tassertEquals(0, result.size());\n\t}\n\n\t@Test\n\tpublic void removeNullValueNoNullValuesReturnsSameMap() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", \"value2\");\n\n\t\tMap<String, String> result = MapUtil.removeNullValue(map);\n\n\t\tassertEquals(2, result.size());\n\t\tassertEquals(\"value1\", result.get(\"key1\"));\n\t\tassertEquals(\"value2\", result.get(\"key2\"));\n\t}\n\n\t@Test\n\tpublic void removeNullValueWithNullValuesRemovesNullEntries() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", null);\n\t\tmap.put(\"key3\", \"value3\");\n\n\t\tMap<String, String> result = MapUtil.removeNullValue(map);\n\n\t\tassertEquals(2, result.size());\n\t\tassertEquals(\"value1\", result.get(\"key1\"));\n\t\tassertEquals(\"value3\", result.get(\"key3\"));\n\t\tassertNull(result.get(\"key2\"));\n\t}\n\n\t@Test\n\tpublic void removeNullValueAllNullValuesReturnsEmptyMap() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"key1\", null);\n\t\tmap.put(\"key2\", null);\n\n\t\tMap<String, String> result = MapUtil.removeNullValue(map);\n\n\t\tassertEquals(0, result.size());\n\t}\n\n\n\t//------getQuietly\n\t@Test\n\tpublic void getQuietlyMapIsNullReturnsDefaultValue() {\n\t\tString result = MapUtil.getQuietly(null, \"key1\", new TypeReference<String>() {\n\t\t}, \"default\");\n\t\tassertEquals(\"default\", result);\n\t\tresult = MapUtil.getQuietly(null, \"key1\", String.class, \"default\");\n\t\tassertEquals(\"default\", result);\n\t}\n\n\t@Test\n\tpublic void getQuietlyKeyExistsReturnsConvertedValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", 123);\n\t\tString result = MapUtil.getQuietly(map, \"key1\", new TypeReference<String>() {\n\t\t}, \"default\");\n\t\tassertEquals(\"value1\", result);\n\t}\n\n\t@Test\n\tpublic void getQuietlyKeyDoesNotExistReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", 123);\n\t\tString result = MapUtil.getQuietly(map, \"key3\", new TypeReference<String>() {\n\t\t}, \"default\");\n\t\tassertEquals(\"default\", result);\n\t}\n\n\t@Test\n\tpublic void getQuietlyConversionFailsReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", 123);\n\t\tInteger result = MapUtil.getQuietly(map, \"key1\", new TypeReference<Integer>() {\n\t\t}, 0);\n\t\tassertEquals(0, result);\n\t}\n\n\t@Test\n\tpublic void getQuietlyKeyExistsWithCorrectTypeReturnsValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", 123);\n\t\tInteger result = MapUtil.getQuietly(map, \"key2\", new TypeReference<Integer>() {\n\t\t}, 0);\n\t\tassertEquals(123, result);\n\t}\n\n\t@Test\n\tpublic void getQuietlyKeyExistsWithNullValueReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\", \"value1\");\n\t\tmap.put(\"key2\", 123);\n\t\tmap.put(\"key3\", null);\n\t\tString result = MapUtil.getQuietly(map, \"key3\", new TypeReference<String>() {\n\t\t}, \"default\");\n\t\tassertEquals(\"default\", result);\n\t}\n\n\t@Test\n\tpublic void getMapIsNullReturnsDefaultValue() {\n\t\tassertNull(MapUtil.get(null, \"age\", String.class));\n\t}\n\n\t@Test\n\tpublic void getKeyExistsReturnsConvertedValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(\"18\", MapUtil.get(map, \"age\", String.class));\n\t}\n\n\t@Test\n\tpublic void getKeyDoesNotExistReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(\"default\", MapUtil.get(map, \"nonexistent\", String.class, \"default\"));\n\t}\n\n\t@Test\n\tpublic void getTypeConversionFailsReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(18, MapUtil.get(map, \"age\", Integer.class, 0));\n\t}\n\n\t@Test\n\tpublic void getQuietlyTypeConversionFailsReturnsDefaultValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(0, MapUtil.getQuietly(map, \"name\", Integer.class, 0));\n\t}\n\n\t@Test\n\tpublic void getTypeReferenceReturnsConvertedValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(\"18\", MapUtil.get(map, \"age\", new TypeReference<String>() {\n\t\t}));\n\t}\n\n\t@Test\n\tpublic void getTypeReferenceWithDefaultValueReturnsConvertedValue() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(\"18\", MapUtil.get(map, \"age\", new TypeReference<String>() {\n\t\t}, \"default\"));\n\t}\n\n\t@Test\n\tpublic void getTypeReferenceWithDefaultValueTypeConversionFailsReturnsDefaultValue() {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"age\", \"18\");\n\t\tmap.put(\"name\", \"Hutool\");\n\t\tassertEquals(18, MapUtil.get(map, \"age\", new TypeReference<Integer>() {\n\t\t}, 0));\n\n\t\tmap = null;\n\t\tassertEquals(0, MapUtil.get(map, \"age\", new TypeReference<Integer>() {\n\t\t}, 0));\n\t}\n\n\t@Test\n\tpublic void flattenMapReturnsTest() {\n\t\tMap<String, String> clothes = new HashMap<>();\n\t\tclothes.put(\"clothesName\", \"ANTA\");\n\t\tclothes.put(\"clothesPrice\", \"200\");\n\n\t\tMap<String, Object> person = new HashMap<>();\n\t\tperson.put(\"personName\", \"XXXX\");\n\t\tperson.put(\"clothes\", clothes);\n\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"home\", \"AAA\");\n\t\tmap.put(\"person\", person);\n\n\t\tMap<String, Object> flattenMap = MapUtil.flatten(map);\n\t\tassertEquals(\"ANTA\", MapUtil.get(flattenMap, \"clothesName\", new TypeReference<String>() {\n\t\t}));\n\t\tassertEquals(\"200\", MapUtil.get(flattenMap, \"clothesPrice\", new TypeReference<String>() {\n\t\t}));\n\t\tassertEquals(\"XXXX\", MapUtil.get(flattenMap, \"personName\", new TypeReference<String>() {\n\t\t}));\n\t\tassertEquals(\"AAA\", MapUtil.get(flattenMap, \"home\", new TypeReference<String>() {\n\t\t}));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/RowKeyTableTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.map.multi.RowKeyTable;\nimport cn.hutool.core.map.multi.Table;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class RowKeyTableTest {\n\n\t@Test\n\tpublic void putGetTest(){\n\t\tfinal Table<Integer, Integer, Integer> table = new RowKeyTable<>();\n\t\ttable.put(1, 2, 3);\n\t\ttable.put(1, 6, 4);\n\n\t\tassertEquals(new Integer(3), table.get(1, 2));\n\t\tassertNull(table.get(1, 3));\n\n\t\t//判断row和column确定的二维点是否存在\n\t\tassertTrue(table.contains(1, 2));\n\t\tassertFalse(table.contains(1, 3));\n\n\t\t//判断列\n\t\tassertTrue(table.containsColumn(2));\n\t\tassertFalse(table.containsColumn(3));\n\n\t\t// 判断行\n\t\tassertTrue(table.containsRow(1));\n\t\tassertFalse(table.containsRow(2));\n\n\n\t\t// 获取列\n\t\tMap<Integer, Integer> column = table.getColumn(6);\n\t\tassertEquals(1, column.size());\n\t\tassertEquals(new Integer(4), column.get(1));\n\t}\n\n\t@Test\n\tpublic void issue3135Test() {\n\t\tfinal Table<Integer, Integer, Integer> table = new RowKeyTable<>();\n\t\ttable.put(1, 2, 3);\n\t\ttable.put(1, 6, 4);\n\n\t\tassertNull(table.getRow(2));\n\t\tassertFalse(table.contains(2, 3));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/TableMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class TableMapTest {\n\n\t@Test\n\tpublic void getTest(){\n\t\tfinal TableMap<String, Integer> tableMap = new TableMap<>(16);\n\t\ttableMap.put(\"aaa\", 111);\n\t\ttableMap.put(\"bbb\", 222);\n\n\t\tassertEquals(new Integer(111), tableMap.get(\"aaa\"));\n\t\tassertEquals(new Integer(222), tableMap.get(\"bbb\"));\n\n\t\tassertEquals(\"aaa\", tableMap.getKey(111));\n\t\tassertEquals(\"bbb\", tableMap.getKey(222));\n\t}\n\n\t@SuppressWarnings(\"OverwrittenKey\")\n\t@Test\n\tpublic void removeTest() {\n\t\tfinal TableMap<String, Integer> tableMap = new TableMap<>(16);\n\t\ttableMap.put(\"a\", 111);\n\t\ttableMap.put(\"a\", 222);\n\t\ttableMap.put(\"a\", 222);\n\n\t\ttableMap.remove(\"a\");\n\n\t\tassertEquals(0, tableMap.size());\n\t}\n\n\t@SuppressWarnings(\"OverwrittenKey\")\n\t@Test\n\tpublic void removeTest2() {\n\t\tfinal TableMap<String, Integer> tableMap = new TableMap<>(16);\n\t\ttableMap.put(\"a\", 111);\n\t\ttableMap.put(\"a\", 222);\n\t\ttableMap.put(\"a\", 222);\n\n\t\ttableMap.remove(\"a\", 222);\n\n\t\tassertEquals(1, tableMap.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/TolerantMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\npublic class TolerantMapTest {\n\n\tprivate final TolerantMap<String, String> map = TolerantMap.of(new HashMap<>(), \"default\");\n\n\t@BeforeEach\n\tpublic void before() {\n\t\tmap.put(\"monday\", \"星期一\");\n\t\tmap.put(\"tuesday\", \"星期二\");\n\t}\n\n\t@Test\n\tpublic void testSerialize() {\n\t\tbyte[] bytes = ObjectUtil.serialize(map);\n\t\tTolerantMap<String, String> serializedMap = ObjectUtil.deserialize(bytes);\n\t\tassert serializedMap != map;\n\t\tassert map.equals(serializedMap);\n\t}\n\n\t@Test\n\tpublic void testClone() {\n\t\tTolerantMap<String, String> clonedMap = ObjectUtil.clone(map);\n\t\tassert clonedMap != map;\n\t\tassert map.equals(clonedMap);\n\t}\n\n\t@Test\n\tpublic void testGet() {\n\t\tassert \"星期二\".equals(map.get(\"tuesday\"));\n\t\tassert \"default\".equals(map.get(RandomUtil.randomString(6)));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/map/WeakConcurrentMapTest.java",
    "content": "package cn.hutool.core.map;\n\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.thread.ConcurrencyTester;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class WeakConcurrentMapTest {\n\n\t@Test\n\tpublic void putAndGetTest(){\n\t\tfinal WeakKeyValueConcurrentMap<Object, Object> map = new WeakKeyValueConcurrentMap<>();\n\t\tObject\n\t\t\t\tkey1 = new Object(), value1 = new Object(),\n\t\t\t\tkey2 = new Object(), value2 = new Object(),\n\t\t\t\tkey3 = new Object(), value3 = new Object(),\n\t\t\t\tkey4 = new Object(), value4 = new Object();\n\t\tmap.put(key1, value1);\n\t\tmap.put(key2, value2);\n\t\tmap.put(key3, value3);\n\t\tmap.put(key4, value4);\n\n\t\tassertEquals(value1, map.get(key1));\n\t\tassertEquals(value2, map.get(key2));\n\t\tassertEquals(value3, map.get(key3));\n\t\tassertEquals(value4, map.get(key4));\n\n\t\t// 清空引用\n\t\t//noinspection UnusedAssignment\n\t\tkey1 = null;\n\t\t//noinspection UnusedAssignment\n\t\tkey2 = null;\n\n\t\tSystem.gc();\n\t\tThreadUtil.sleep(200L);\n\n\t\tassertEquals(2, map.size());\n\t}\n\n\t@Test\n\tpublic void getConcurrencyTest(){\n\t\tfinal WeakKeyValueConcurrentMap<String, String> cache = new WeakKeyValueConcurrentMap<>();\n\t\tfinal ConcurrencyTester tester = new ConcurrencyTester(9000);\n\t\ttester.test(()-> cache.computeIfAbsent(\"aaa\" + RandomUtil.randomInt(2), (key)-> \"aaaValue\"));\n\n\t\tassertTrue(tester.getInterval() > 0);\n\t\tString value = ObjectUtil.defaultIfNull(cache.get(\"aaa0\"), cache.get(\"aaa1\"));\n\t\tassertEquals(\"aaaValue\", value);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/math/ArrangementTest.java",
    "content": "package cn.hutool.core.math;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 排列单元测试\n * @author looly\n */\npublic class ArrangementTest {\n\n\t// ----------------------------------------------------\n\t// 基础测试\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void arrangementTest() {\n\t\tlong result = Arrangement.count(4, 2);\n\t\tassertEquals(12, result);\n\n\t\tresult = Arrangement.count(4, 1);\n\t\tassertEquals(4, result);\n\n\t\tresult = Arrangement.count(4, 0);\n\t\tassertEquals(1, result);\n\n\t\tlong resultAll = Arrangement.countAll(4);\n\t\tassertEquals(64, resultAll);\n\t}\n\n\t// ----------------------------------------------------\n\t// select 基础测试\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void selectTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"2\", \"3\", \"4\"});\n\t\tList<String[]> list = arrangement.select(2);\n\n\t\t// 校验数量一致\n\t\tassertEquals(Arrangement.count(4, 2), list.size());\n\n\t\t// 逐项严格校验顺序是否一致（按 DFS 顺序）\n\t\tassertArrayEquals(new String[]{\"1\", \"2\"}, list.get(0));\n\t\tassertArrayEquals(new String[]{\"1\", \"3\"}, list.get(1));\n\t\tassertArrayEquals(new String[]{\"1\", \"4\"}, list.get(2));\n\t\tassertArrayEquals(new String[]{\"2\", \"1\"}, list.get(3));\n\t\tassertArrayEquals(new String[]{\"2\", \"3\"}, list.get(4));\n\t\tassertArrayEquals(new String[]{\"2\", \"4\"}, list.get(5));\n\t\tassertArrayEquals(new String[]{\"3\", \"1\"}, list.get(6));\n\t\tassertArrayEquals(new String[]{\"3\", \"2\"}, list.get(7));\n\t\tassertArrayEquals(new String[]{\"3\", \"4\"}, list.get(8));\n\t\tassertArrayEquals(new String[]{\"4\", \"1\"}, list.get(9));\n\t\tassertArrayEquals(new String[]{\"4\", \"2\"}, list.get(10));\n\t\tassertArrayEquals(new String[]{\"4\", \"3\"}, list.get(11));\n\n\t\t// 测试 selectAll\n\t\tList<String[]> selectAll = arrangement.selectAll();\n\t\tassertEquals(Arrangement.countAll(4), selectAll.size());\n\n\t\t// m=0，应该返回一个空排列\n\t\tList<String[]> list2 = arrangement.select(0);\n\t\tassertEquals(1, list2.size());\n\t\tassertEquals(0, list2.get(0).length);\n\t}\n\n\t// ----------------------------------------------------\n\t// 扩展测试：边界、错误处理\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void boundaryTest() {\n\t\tArrangement arr = new Arrangement(new String[]{\"A\", \"B\", \"C\"});\n\n\t\t// m = n\n\t\tList<String[]> full = arr.select(3);\n\t\tassertEquals(6, full.size());\n\n\t\t// m = 1\n\t\tList<String[]> one = arr.select(1);\n\t\tassertEquals(3, one.size());\n\t\tassertArrayEquals(new String[]{\"A\"}, one.get(0));\n\n\t\t// m > n → empty list\n\t\tassertTrue(arr.select(10).isEmpty());\n\n\t\t// m < 0 → empty list\n\t\tassertTrue(arr.select(-1).isEmpty());\n\t}\n\n\t// ----------------------------------------------------\n\t// 扩展测试：空数组\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void emptyTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{});\n\n\t\tassertEquals(1, arrangement.select(0).size());\n\t\tassertTrue(arrangement.select(1).isEmpty());\n\t\tassertTrue(arrangement.selectAll().isEmpty()); // A(0,m) = 0 for m>0，A(0,0)=1 → 全排列 = 1 个空排列\n\t}\n\n\t// ----------------------------------------------------\n\t// 扩展测试：重复元素（用于验证去重算法）\n\t// 默认 Arrangement 不去重，因此应该包含重复排列\n\t// ----------------------------------------------------\n\t@Test\n\t@Disabled(\"默认 Arrangement 不支持去重；启用后手动检查\")\n\tpublic void duplicateElementTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"1\", \"3\"});\n\n\t\tList<String[]> list = arrangement.select(2);\n\n\t\t// 应该有 A(3,2) = 6 个\n\t\tassertEquals(6, list.size());\n\n\t\tfor (String[] s : list) {\n\t\t\tConsole.log(s);\n\t\t}\n\t}\n\n\t// ----------------------------------------------------\n\t// 扩展测试：selectAll 覆盖全部不重复排列（A(n,1..n)）\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void selectAllTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"2\", \"3\"});\n\n\t\tList<String[]> all = arrangement.selectAll();\n\n\t\t// 打印用于观测\n\t\tfor (String[] s : all) {\n\t\t\tConsole.log(s);\n\t\t}\n\n\t\t// A(3,1) + A(3,2) + A(3,3) = 3 + 6 + 6 = 15\n\t\tassertEquals(Arrangement.countAll(3), all.size());\n\t\tassertEquals(15, all.size());\n\n\t\t// spot check 不重复排列\n\t\tassertArrayEquals(new String[]{\"1\"}, all.get(0));\n\t\tassertArrayEquals(new String[]{\"1\", \"2\"}, all.get(3));\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\"}, all.get(9));\n\t}\n\n\t// ----------------------------------------------------\n\t// 迭代器测试\n\t// ----------------------------------------------------\n\t@Test\n\tpublic void iteratorTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"2\", \"3\"});\n\n\t\t// 测试 m=2 的情况\n\t\tList<String[]> iterResult = new ArrayList<>();\n\t\tfor (String[] perm : arrangement.iterate(2)) {\n\t\t\titerResult.add(perm);\n\t\t}\n\n\t\tassertEquals(6, iterResult.size());\n\t\tassertArrayEquals(new String[]{\"1\", \"2\"}, iterResult.get(0));\n\t\tassertArrayEquals(new String[]{\"1\", \"3\"}, iterResult.get(1));\n\t\tassertArrayEquals(new String[]{\"2\", \"1\"}, iterResult.get(2));\n\t\tassertArrayEquals(new String[]{\"2\", \"3\"}, iterResult.get(3));\n\t\tassertArrayEquals(new String[]{\"3\", \"1\"}, iterResult.get(4));\n\t\tassertArrayEquals(new String[]{\"3\", \"2\"}, iterResult.get(5));\n\t}\n\n\t@Test\n\tpublic void iteratorFullTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"2\", \"3\"});\n\n\t\t// 测试全排列的情况\n\t\tList<String[]> iterResult = new ArrayList<>();\n\t\tfor (String[] perm : arrangement.iterate(3)) {\n\t\t\titerResult.add(perm);\n\t\t}\n\n\t\tassertEquals(6, iterResult.size());\n\t}\n\n\t@Test\n\tpublic void iteratorBoundaryTest() {\n\t\tArrangement arrangement = new Arrangement(new String[]{\"1\", \"2\", \"3\"});\n\n\t\t// 测试 m > n 的情况\n\t\tList<String[]> iterResult = new ArrayList<>();\n\t\tfor (String[] perm : arrangement.iterate(5)) {\n\t\t\titerResult.add(perm);\n\t\t}\n\t\tassertTrue(iterResult.isEmpty());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/math/CalculatorTest.java",
    "content": "package cn.hutool.core.math;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CalculatorTest {\n\n\t@Test\n\tpublic void conversationTest(){\n\t\tfinal double conversion = Calculator.conversion(\"(0*1--3)-5/-4-(3*(-2.13))\");\n\t\tassertEquals(10.64, conversion, 0);\n\t}\n\n\t@Test\n\tpublic void conversationTest2(){\n\t\tfinal double conversion = Calculator.conversion(\"77 * 12\");\n\t\tassertEquals(924.0, conversion, 0);\n\t}\n\n\t@Test\n\tpublic void conversationTest3(){\n\t\tfinal double conversion = Calculator.conversion(\"1\");\n\t\tassertEquals(1, conversion, 0);\n\t}\n\n\t@Test\n\tpublic void conversationTest4(){\n\t\tfinal double conversion = Calculator.conversion(\"(88*66/23)%26+45%9\");\n\t\tassertEquals((88D * 66 / 23) % 26, conversion, 0.0000000001);\n\t}\n\n\t@Test\n\tpublic void conversationTest5(){\n\t\t// https://github.com/chinabugotech/hutool/issues/1984\n\t\tfinal double conversion = Calculator.conversion(\"((1/1) / (1/1) -1) * 100\");\n\t\tassertEquals(0, conversion, 0);\n\t}\n\n\t@Test\n\tpublic void conversationTest6() {\n\t\tfinal double conversion = Calculator.conversion(\"-((2.12-2) * 100)\");\n\t\tassertEquals(-1D * (2.12 - 2) * 100, conversion, 0.01);\n\t}\n\n\t@Test\n\tpublic void conversationTest7() {\n\t\t//https://gitee.com/chinabugotech/hutool/issues/I4KONB\n\t\tfinal double conversion = Calculator.conversion(\"((-2395+0) * 0.3+140.24+35+90)/30\");\n\t\tassertEquals(-15.11, conversion, 0.01);\n\t}\n\n\t@Test\n\tpublic void issue2964Test() {\n\t\t// https://github.com/chinabugotech/hutool/issues/2964\n\t\tfinal double calcValue = Calculator.conversion(\"(11+2)12\");\n\t\tassertEquals(156D, calcValue, 0.001);\n\t}\n\n\t@Test\n\tvoid issue3787Test() {\n\t\tfinal Calculator calculator1 = new Calculator();\n\t\tdouble result = calculator1.calculate(\"0+50/100x(1/0.5)\");\n\t\tassertEquals(1D, result);\n\n\t\tresult = calculator1.calculate(\"0+50/100X(1/0.5)\");\n\t\tassertEquals(1D, result);\n\t}\n\n\t@Test\n\tpublic void scientificNotationPlusTest() {\n\t\t// 测试科学记数法中的 + 号是否被正确处理\n\t\tfinal double conversion = Calculator.conversion(\"1e+3\");\n\t\tassertEquals(1000.0, conversion, 0.001);\n\n\t\t// 更复杂的科学记数法表达式\n\t\tfinal double conversion2 = Calculator.conversion(\"2.5e+2 + 1.0e-1\");\n\t\tassertEquals(250.1, conversion2, 0.001);\n\t}\n\n\t@Test\n\tpublic void unaryOperatorConsistencyTest() {\n\t\t// 测试连续的一元运算符：双重负号--3，等同于 -( -3 ) = 3\n\t\tfinal double conversion = Calculator.conversion(\"--3\");\n\t\tassertEquals(3.0, conversion, 0.001);\n\n\t\t// 测试连续的一元运算符：正号后跟负号，等同于 +( -3 ) = -3\n\t\tfinal double conversion2 = Calculator.conversion(\"+-3\");\n\t\tassertEquals(-3.0, conversion2, 0.001);\n\n\t\t// 测试表达式开始的一元+运算符\n\t\tfinal double conversion3 = Calculator.conversion(\"+3\");\n\t\tassertEquals(3.0, conversion3, 0.001);\n\n\t\t// 测试表达式开始的一元-运算符\n\t\tfinal double conversion4 = Calculator.conversion(\"-3\");\n\t\tassertEquals(-3.0, conversion4, 0.001);\n\t}\n\n\t@Test\n\tpublic void percentOperatorTest() {\n\t\t//基础 % 运算\n\t\tassertEquals(1.0, Calculator.conversion(\"10 % 3\"), 0.001);\n\n\t\t// % 运算符后跟连续的一元运算符的情况\n\t\tassertEquals(1.0, Calculator.conversion(\"10 % +-3\"), 0.001);\n\t\tassertEquals(1.0, Calculator.conversion(\"10 % -3\"), 0.001);\n\n\t\t// 带括号的 % 后一元负号\n\t\tassertEquals(1.0, Calculator.conversion(\"10 % (-3)\"), 0.001);\n\n\t\t// % 与 * / 的优先级测试\n\t\tassertEquals(2.0, Calculator.conversion(\"10 * 5 % 3\"), 0.001);\n\t\tassertEquals(1.0, Calculator.conversion(\"20 / 5 % 3\"), 0.001);\n\n\t\t//连续 % 运算\n\t\tassertEquals(2.0, Calculator.conversion(\"100 % 7 % 3\"), 0.001);\n\n\t\t// % 与 + - 混合运算\n\t\tassertEquals(13.0, Calculator.conversion(\"10 + 15 % 4\"), 0.001);\n\n\t\t//负数操作数的 % 运算\n\t\tassertEquals(-1.0, Calculator.conversion(\"-10 % 3\"), 0.001);\n\n\t\t// 两个负数的 % 运算\n\t\tassertEquals(-1.0, Calculator.conversion(\"-10 % -3\"), 0.001);\n\n\t\t// 小数的 % 运算\n\t\tassertEquals(0.9, Calculator.conversion(\"10.5 % 3.2\"), 0.001);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/math/CombinationTest.java",
    "content": "package cn.hutool.core.math;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigInteger;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 组合单元测试\n *\n * @author looly\n *\n */\npublic class CombinationTest {\n\n\t@Test\n\tpublic void countTest() {\n\t\tlong result = Combination.count(5, 2);\n\t\tassertEquals(10, result);\n\n\t\tresult = Combination.count(5, 5);\n\t\tassertEquals(1, result);\n\n\t\tresult = Combination.count(5, 0);\n\t\tassertEquals(1, result);\n\n\t\tlong resultAll = Combination.countAll(5);\n\t\tassertEquals(31, resultAll);\n\t}\n\n\t@Test\n\tpublic void selectTest() {\n\t\tCombination combination = new Combination(new String[] { \"1\", \"2\", \"3\", \"4\", \"5\" });\n\t\tList<String[]> list = combination.select(2);\n\t\tassertEquals(Combination.count(5, 2), list.size());\n\n\t\tassertArrayEquals(new String[] {\"1\", \"2\"}, list.get(0));\n\t\tassertArrayEquals(new String[] {\"1\", \"3\"}, list.get(1));\n\t\tassertArrayEquals(new String[] {\"1\", \"4\"}, list.get(2));\n\t\tassertArrayEquals(new String[] {\"1\", \"5\"}, list.get(3));\n\t\tassertArrayEquals(new String[] {\"2\", \"3\"}, list.get(4));\n\t\tassertArrayEquals(new String[] {\"2\", \"4\"}, list.get(5));\n\t\tassertArrayEquals(new String[] {\"2\", \"5\"}, list.get(6));\n\t\tassertArrayEquals(new String[] {\"3\", \"4\"}, list.get(7));\n\t\tassertArrayEquals(new String[] {\"3\", \"5\"}, list.get(8));\n\t\tassertArrayEquals(new String[] {\"4\", \"5\"}, list.get(9));\n\n\t\tList<String[]> selectAll = combination.selectAll();\n\t\tassertEquals(Combination.countAll(5), selectAll.size());\n\n\t\tList<String[]> list2 = combination.select(0);\n\t\tassertEquals(1, list2.size());\n\t}\n\n\n\t// -----------------------------\n\t// countBig() 正确性测试\n\t// -----------------------------\n\t@Test\n\tvoid testCountBig_basicCases() {\n\t\tassertEquals(BigInteger.ONE, Combination.countBig(5, 0));\n\t\tassertEquals(BigInteger.ONE, Combination.countBig(5, 5));\n\t\tassertEquals(BigInteger.valueOf(10), Combination.countBig(5, 3));\n\t\tassertEquals(BigInteger.valueOf(10), Combination.countBig(5, 2));\n\t}\n\n\t@Test\n\tvoid testCountBig_mGreaterThanN() {\n\t\tassertEquals(BigInteger.ZERO, Combination.countBig(5, 6));\n\t}\n\n\t@Test\n\tvoid testCountBig_negativeInput() {\n\t\tassertThrows(IllegalArgumentException.class, () -> Combination.countBig(-1, 3));\n\t\tassertThrows(IllegalArgumentException.class, () -> Combination.countBig(5, -2));\n\t}\n\n\t@Test\n\tvoid testCountBig_symmetry() {\n\t\tassertEquals(Combination.countBig(20, 3), Combination.countBig(20, 17));\n\t}\n\n\t@Test\n\tvoid testCountBig_largeNumbers() {\n\t\t// C(50, 3) = 19600\n\t\tassertEquals(new BigInteger(\"19600\"), Combination.countBig(50, 3));\n\n\t\t// C(100, 50) 的确切值（重要测试）\n\t\tBigInteger expected = new BigInteger(\n\t\t\t\"100891344545564193334812497256\"\n\t\t);\n\t\tassertEquals(expected, Combination.countBig(100, 50));\n\t}\n\n\t@Test\n\tvoid testCountBig_veryLargeCombination() {\n\t\t// 不比较具体值，只断言不要抛错\n\t\tBigInteger result = Combination.countBig(2000, 1000);\n\t\tassertTrue(result.signum() > 0);\n\t}\n\n\t// -----------------------------\n\t// count(long) 兼容性测试\n\t// -----------------------------\n\t@Test\n\tvoid testCount_basic() {\n\t\tassertEquals(10L, Combination.count(5, 3));\n\t\tassertEquals(1L, Combination.count(5, 0));\n\t\tassertEquals(0L, Combination.count(5, 6));\n\t}\n\n\t@Test\n\tvoid testCount_overflowBehavior() {\n\t\t// C(100, 50) 远超 long 范围，但旧版行为要求不抛异常\n\t\tlong r = Combination.count(100, 50);\n\n\t\t// longValue() 不抛异常，并且可能溢出\n\t\tassertNotNull(r);\n\t}\n\n\t@Test\n\tvoid testCount_noException() {\n\t\tassertDoesNotThrow(() -> Combination.count(5000, 2500));\n\t}\n\n\t// -----------------------------\n\t// countSafe() 安全 long 版本测试\n\t// -----------------------------\n\t@Test\n\tvoid testCountSafe_exactFitsLong() {\n\t\t// C(50, 3) = 19600 fits long\n\t\tassertEquals(19600L, Combination.countSafe(50, 3));\n\t}\n\n\t@Test\n\tvoid testCountSafe_overflowThrows() {\n\t\t// C(100, 50) 超出 long → 应抛 ArithmeticException\n\t\tassertThrows(ArithmeticException.class, () -> Combination.countSafe(100, 50));\n\t}\n\n\t@Test\n\tvoid testCountSafe_invalidInput() {\n\t\tassertThrows(IllegalArgumentException.class, () -> Combination.countSafe(-1, 3));\n\t\tassertThrows(IllegalArgumentException.class, () -> Combination.countSafe(3, -1));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/math/MoneyTest.java",
    "content": "package cn.hutool.core.math;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport java.math.BigDecimal;\nimport java.util.Currency;\n\npublic class MoneyTest {\n\n\t@Test\n\tpublic void yuanToCentTest() {\n\t\tfinal Money money = new Money(\"1234.56\");\n\t\tassertEquals(123456, money.getCent());\n\n\t\tassertEquals(123456, MathUtil.yuanToCent(1234.56));\n\t}\n\n\t@Test\n\tpublic void centToYuanTest() {\n\t\tfinal Money money = new Money(1234, 56);\n\t\tassertEquals(1234.56D, money.getAmount().doubleValue(), 0);\n\n\t\tassertEquals(1234.56D, MathUtil.centToYuan(123456), 0);\n\t}\n\n\t@Test\n\tpublic void currencyScalingTest() {\n\t\tMoney jpyMoney = new Money(0, Currency.getInstance(\"JPY\"));\n\t\tjpyMoney.setAmount(BigDecimal.ONE);\n\t\tassertEquals(1, jpyMoney.getCent());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/FormUrlencodedTest.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class FormUrlencodedTest {\n\n\t@Test\n\tpublic void encodeParamTest(){\n\t\tString encode = FormUrlencoded.ALL.encode(\"a+b\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a%2Bb\", encode);\n\n\t\tencode = FormUrlencoded.ALL.encode(\"a b\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a+b\", encode);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/Ipv4UtilTest.java",
    "content": "package cn.hutool.core.net;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class Ipv4UtilTest {\n\n\t@Test\n\tpublic void getMaskBitByMaskTest(){\n\t\tfinal int maskBitByMask = Ipv4Util.getMaskBitByMask(\"255.255.255.0\");\n\t\tassertEquals(24, maskBitByMask);\n\t}\n\n\t@Test\n\tpublic void getMaskBitByIllegalMaskTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> Ipv4Util.getMaskBitByMask(\"255.255.0.255\"));\n\t}\n\n\t@Test\n\tpublic void getMaskByMaskBitTest(){\n\t\tfinal String mask = Ipv4Util.getMaskByMaskBit(24);\n\t\tassertEquals(\"255.255.255.0\", mask);\n\t}\n\n\t@Test\n\tpublic void longToIpTest() {\n\t\tfinal String ip = \"192.168.1.255\";\n\t\tfinal long ipLong = Ipv4Util.ipv4ToLong(ip);\n\t\tfinal String ipv4 = Ipv4Util.longToIpv4(ipLong);\n\t\tassertEquals(ip, ipv4);\n\t}\n\n\t@Test\n\tpublic void getEndIpStrTest(){\n\t\tfinal String ip = \"192.168.1.1\";\n\t\tfinal int maskBitByMask = Ipv4Util.getMaskBitByMask(\"255.255.255.0\");\n\t\tfinal String endIpStr = Ipv4Util.getEndIpStr(ip, maskBitByMask);\n\t\tassertEquals(\"192.168.1.255\", endIpStr);\n\t}\n\n\t@Test\n\tpublic void listTest(){\n\t\tfinal int maskBit = Ipv4Util.getMaskBitByMask(\"255.255.255.0\");\n\t\tfinal List<String> list = Ipv4Util.list(\"192.168.100.2\", maskBit, false);\n\t\tassertEquals(254, list.size());\n\n\t\ttestGenerateIpList(\"10.1.0.1\", \"10.2.1.2\");\n\n\t\ttestGenerateIpList(\"10.2.1.1\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.2.0.1\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.0.1\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.2.1\", \"10.2.1.2\");\n\n\t\ttestGenerateIpList(\"10.2.1.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.2.0.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.1.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.2.2\", \"10.2.1.2\");\n\n\t\ttestGenerateIpList(\"10.2.0.3\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.0.3\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.1.3\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"10.1.2.3\", \"10.2.1.2\");\n\n\t\ttestGenerateIpList(\"9.255.2.1\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"9.255.2.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"9.255.2.3\", \"10.2.1.2\");\n\n\t\ttestGenerateIpList(\"9.255.1.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"9.255.0.2\", \"10.2.1.2\");\n\t\ttestGenerateIpList(\"9.255.3.2\", \"10.2.1.2\");\n\t}\n\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate void testGenerateIpList(final String fromIp, final String toIp) {\n\t\tassertEquals(\n\t\t\t\tIpv4Util.countByIpRange(fromIp, toIp),\n\t\t\t\tIpv4Util.list(fromIp, toIp).size()\n\t\t);\n\t}\n\n\t@Test\n\tpublic void isMaskValidTest() {\n\t\tfinal boolean maskValid = Ipv4Util.isMaskValid(\"255.255.255.0\");\n\t\tassertTrue(maskValid, \"掩码合法检验\");\n\t}\n\n\t@Test\n\tpublic void isMaskInvalidTest() {\n\t\tassertFalse(Ipv4Util.isMaskValid(\"255.255.0.255\"), \"掩码非法检验 - 255.255.0.255\");\n\t\tassertFalse(Ipv4Util.isMaskValid(null), \"掩码非法检验 - null值\");\n\t\tassertFalse(Ipv4Util.isMaskValid(\"\"), \"掩码非法检验 - 空字符串\");\n\t\tassertFalse(Ipv4Util.isMaskValid(\" \"), \"掩码非法检验 - 空白字符串\");\n\t}\n\n\t@Test\n\tpublic void isMaskBitValidTest() {\n\t\tfinal boolean maskBitValid = Ipv4Util.isMaskBitValid(32);\n\t\tassertTrue( maskBitValid);\n\t}\n\n\t@Test\n\tpublic void isMaskBitInvalidTest() {\n\t\tfinal boolean maskBitValid = Ipv4Util.isMaskBitValid(33);\n\t\tassertFalse(maskBitValid);\n\t}\n\n\t@Test\n\tpublic void matchesTest() {\n\t\tfinal boolean matches1 = Ipv4Util.matches(\"127.*.*.1\", \"127.0.0.1\");\n\t\tassertTrue(matches1);\n\n\t\tfinal boolean matches2 = Ipv4Util.matches(\"192.168.*.1\", \"127.0.0.1\");\n\t\tassertFalse(matches2);\n\t}\n\n\t@Test\n\tpublic void ipv4ToLongTest(){\n\t\tlong l = Ipv4Util.ipv4ToLong(\"127.0.0.1\");\n\t\tassertEquals(2130706433L, l);\n\t\tl = Ipv4Util.ipv4ToLong(\"114.114.114.114\");\n\t\tassertEquals(1920103026L, l);\n\t\tl = Ipv4Util.ipv4ToLong(\"0.0.0.0\");\n\t\tassertEquals(0L, l);\n\t\tl = Ipv4Util.ipv4ToLong(\"255.255.255.255\");\n\t\tassertEquals(4294967295L, l);\n\t}\n\n\t@Test\n\tpublic void ipv4ToLongWithDefaultTest() {\n\t\tfinal String strIP = \"不正确的 IP 地址\";\n\t\tfinal long defaultValue = 0L;\n\t\tfinal long ipOfLong = Ipv4Util.ipv4ToLong(strIP, defaultValue);\n\t\tassertEquals(ipOfLong, defaultValue);\n\n\t\tfinal String strIP2 = \"255.255.255.255\";\n\t\tfinal long defaultValue2 = 0L;\n\t\tfinal long ipOfLong2 = Ipv4Util.ipv4ToLong(strIP2, defaultValue2);\n\t\tassertEquals(ipOfLong2, 4294967295L);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/IssueI70UPUTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.net;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI70UPUTest {\n\t@Test\n\tpublic void encodeQueryTest() {\n\t\tString json = \"{\\n\" +\n\t\t\t\"  \\\"FodayGJ\\\": {\\n\" +\n\t\t\t\"    \\\"ZTMC\\\": \\\"库存\\\",\\n\" +\n\t\t\t\"    \\\"ZTBZ\\\": \\\"20\\\",\\n\" +\n\t\t\t\"    \\\"KEYID\\\": 40313,\\n\" +\n\t\t\t\"    \\\"KBH\\\": \\\"C0XFPQ-1B\\\",\\n\" +\n\t\t\t\"    \\\"GCJC\\\": \\\"蒂森\\\",\\n\" +\n\t\t\t\"    \\\"CCZL\\\": 8370,\\n\" +\n\t\t\t\"    \\\"DQZL\\\": 8370,\\n\" +\n\t\t\t\"    \\\"CZ\\\": \\\"HC340/590DPD+Z\\\",\\n\" +\n\t\t\t\"    \\\"SCRQ\\\": \\\"2023-04-24\\\",\\n\" +\n\t\t\t\"    \\\"GG\\\": \\\"1.5*1000*C\\\",\\n\" +\n\t\t\t\"    \\\"GYS\\\": \\\"佛山亚铁\\\",\\n\" +\n\t\t\t\"    \\\"WZH\\\": \\\"B19\\\"\\n\" +\n\t\t\t\"  }\\n\" +\n\t\t\t\"}\";\n\n\t\tConsole.log(URLEncodeUtil.encodeQuery(json));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.util.ReUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigInteger;\nimport java.net.HttpCookie;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * NetUtil单元测试\n *\n * @author Looly\n *\n */\npublic class NetUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getLocalhostStrTest() {\n\t\tfinal String localhost = NetUtil.getLocalhostStr();\n\t\tassertNotNull(localhost);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getLocalhostTest() {\n\t\tfinal InetAddress localhost = NetUtil.getLocalhost();\n\t\tassertNotNull(localhost);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getLocalMacAddressTest() {\n\t\tfinal String macAddress = NetUtil.getLocalMacAddress();\n\t\tassertNotNull(macAddress);\n\n\t\t// 验证MAC地址正确\n\t\tfinal boolean match = ReUtil.isMatch(PatternPool.MAC_ADDRESS, macAddress);\n\t\tassertTrue(match);\n\t}\n\n\t@Test\n\tpublic void longToIpTest() {\n\t\tfinal String ipv4 = NetUtil.longToIpv4(2130706433L);\n\t\tassertEquals(\"127.0.0.1\", ipv4);\n\t}\n\n\t@Test\n\tpublic void ipToLongTest() {\n\t\tfinal long ipLong = NetUtil.ipv4ToLong(\"127.0.0.1\");\n\t\tassertEquals(2130706433L, ipLong);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void isUsableLocalPortTest(){\n\t\tassertTrue(NetUtil.isUsableLocalPort(80));\n\t}\n\n\t@Test\n\tpublic void parseCookiesTest(){\n\t\tfinal String cookieStr = \"cookieName=\\\"cookieValue\\\";Path=\\\"/\\\";Domain=\\\"cookiedomain.com\\\"\";\n\t\tfinal List<HttpCookie> httpCookies = NetUtil.parseCookies(cookieStr);\n\t\tassertEquals(1, httpCookies.size());\n\n\t\tfinal HttpCookie httpCookie = httpCookies.get(0);\n\t\tassertEquals(0, httpCookie.getVersion());\n\t\tassertEquals(\"cookieName\", httpCookie.getName());\n\t\tassertEquals(\"cookieValue\", httpCookie.getValue());\n\t\tassertEquals(\"/\", httpCookie.getPath());\n\t\tassertEquals(\"cookiedomain.com\", httpCookie.getDomain());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getLocalHostNameTest() {\n\t\t// 注意此方法会触发反向DNS解析，导致阻塞，阻塞时间取决于网络！\n\t\tassertNotNull(NetUtil.getLocalHostName());\n\t}\n\n\t@Test\n\tpublic void getLocalHostTest() {\n\t\tassertNotNull(NetUtil.getLocalhost());\n\t}\n\n\t@Test\n\tpublic void pingTest(){\n\t\tassertTrue(NetUtil.ping(\"127.0.0.1\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void isOpenTest(){\n\t\tfinal InetSocketAddress address = new InetSocketAddress(\"www.hutool.cn\", 443);\n\t\tassertTrue(NetUtil.isOpen(address, 200));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getDnsInfoTest(){\n\t\tfinal List<String> txt = NetUtil.getDnsInfo(\"hutool.cn\", \"TXT\");\n\t\tConsole.log(txt);\n\t}\n\n\t@Test\n\tpublic void isInRangeTest(){\n\t\tassertTrue(NetUtil.isInRange(\"114.114.114.114\",\"0.0.0.0/0\"));\n\t\tassertTrue(NetUtil.isInRange(\"192.168.3.4\",\"192.0.0.0/8\"));\n\t\tassertTrue(NetUtil.isInRange(\"192.168.3.4\",\"192.168.0.0/16\"));\n\t\tassertTrue(NetUtil.isInRange(\"192.168.3.4\",\"192.168.3.0/24\"));\n\t\tassertTrue(NetUtil.isInRange(\"192.168.3.4\",\"192.168.3.4/32\"));\n\t\tassertFalse(NetUtil.isInRange(\"8.8.8.8\",\"192.0.0.0/8\"));\n\t\tassertFalse(NetUtil.isInRange(\"114.114.114.114\",\"192.168.3.4/32\"));\n\t}\n\n\t@Test\n\tpublic void issueI64P9JTest() {\n\t\t// 获取结果应该去掉空格\n\t\tfinal String ips = \"unknown, 12.34.56.78, 23.45.67.89\";\n\t\tfinal String ip = NetUtil.getMultistageReverseProxyIp(ips);\n\t\tassertEquals(\"12.34.56.78\", ip);\n\t}\n\n\t@Test\n\tvoid bigIntegerToIPv6Test() {\n\t\tBigInteger bigInteger = new BigInteger(\"21987654321098765432109876543210\", 10);\n\t\tString ipv6Address = NetUtil.bigIntegerToIPv6(bigInteger);\n\t\tAssertions.assertEquals(\"0:115:85f1:5eb3:c74d:a870:11c6:7eea\", ipv6Address);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/RFC3986Test.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.codec.PercentCodec;\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\n\npublic class RFC3986Test {\n\n\t@Test\n\tpublic void encodeQueryTest(){\n\t\tString encode = RFC3986.QUERY_PARAM_VALUE.encode(\"a=b\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=b\", encode);\n\n\t\tencode = RFC3986.QUERY_PARAM_VALUE.encode(\"a+1=b\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a+1=b\", encode);\n\t}\n\n\t@Test\n\tpublic void encodeQueryPercentTest(){\n\t\tString encode = RFC3986.QUERY_PARAM_VALUE.encode(\"a=%b\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=%25b\", encode);\n\t}\n\n\t@Test\n\tpublic void encodeQueryWithSafeTest(){\n\t\tString encode = RFC3986.QUERY_PARAM_VALUE.encode(\"a=%25\", CharsetUtil.CHARSET_UTF_8, '%');\n\t\tassertEquals(\"a=%25\", encode);\n\t}\n\n\t@Test\n\tpublic void encodeAllTest() throws UnsupportedEncodingException {\n\t\tString toVerifyText = \"行吧行吧 cargo:1.0,\\\"Deta-ils:[{\";\n\t\tfinal String encode = PercentCodec.of(RFC3986.UNRESERVED).setEncodeSpaceAsPlus(true).encode(toVerifyText, CharsetUtil.CHARSET_UTF_8);\n\t\tfinal String encodeJdk = URLEncoder.encode(toVerifyText, \"UTF-8\");\n\t\tassertEquals(encode, encodeJdk);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.net.url.UrlBuilder;\nimport cn.hutool.core.net.url.UrlPath;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class UrlBuilderTest {\n\n\t@Test\n\tpublic void buildTest() {\n\t\tUrlBuilder builder = UrlBuilder.of();\n\t\tfinal String buildUrl = builder.setHost(\"www.hutool.cn\").build();\n\t\tassertEquals(\"http://www.hutool.cn/\", buildUrl);\n\t\tassertEquals(80, builder.getPortWithDefault());\n\t}\n\n\t@Test\n\tpublic void buildWithoutSlashTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/2459\n\t\tString buildUrl = UrlBuilder.of().setScheme(\"http\").setHost(\"192.168.1.1\").setPort(8080).setWithEndTag(false).build();\n\t\tassertEquals(\"http://192.168.1.1:8080\", buildUrl);\n\n\t\tUrlBuilder urlBuilder = UrlBuilder.of();\n\t\tbuildUrl = urlBuilder.setScheme(\"http\").setHost(\"192.168.1.1\").setPort(8080).addQuery(\"url\", \"http://192.168.1.1/test/1\")\n\t\t\t\t.setWithEndTag(false).build();\n\t\tassertEquals(\"http://192.168.1.1:8080?url=http://192.168.1.1/test/1\", buildUrl);\n\t\tassertEquals(8080, urlBuilder.getPortWithDefault());\n\t}\n\n\t@Test\n\tpublic void buildTest2() {\n\t\t// path中的+不做处理\n\t\tfinal String buildUrl = UrlBuilder.ofHttp(\"http://www.hutool.cn/+8618888888888\", CharsetUtil.CHARSET_UTF_8).build();\n\t\tassertEquals(\"http://www.hutool.cn/+8618888888888\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testHost() {\n\t\tfinal String buildUrl = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\").build();\n\t\tassertEquals(\"https://www.hutool.cn/\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testHostPort() {\n\t\tfinal String buildUrl = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.setPort(8080)\n\t\t\t\t.build();\n\t\tassertEquals(\"https://www.hutool.cn:8080/\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testPathAndQuery() {\n\t\tfinal String buildUrl = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.addPath(\"/aaa\").addPath(\"bbb\")\n\t\t\t\t.addQuery(\"ie\", \"UTF-8\")\n\t\t\t\t.addQuery(\"wd\", \"test\")\n\t\t\t\t.build();\n\n\t\tassertEquals(\"https://www.hutool.cn/aaa/bbb?ie=UTF-8&wd=test\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testQueryWithChinese() {\n\t\tfinal String buildUrl = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.addPath(\"/aaa\").addPath(\"bbb\")\n\t\t\t\t.addQuery(\"ie\", \"UTF-8\")\n\t\t\t\t.addQuery(\"wd\", \"测试\")\n\t\t\t\t.build();\n\n\t\tassertEquals(\"https://www.hutool.cn/aaa/bbb?ie=UTF-8&wd=%E6%B5%8B%E8%AF%95\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testMultiQueryWithChinese() {\n\t\tfinal String buildUrl = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.addPath(\"/s\")\n\t\t\t\t.addQuery(\"ie\", \"UTF-8\")\n\t\t\t\t.addQuery(\"ie\", \"GBK\")\n\t\t\t\t.addQuery(\"wd\", \"测试\")\n\t\t\t\t.build();\n\n\t\tassertEquals(\"https://www.hutool.cn/s?ie=UTF-8&ie=GBK&wd=%E6%B5%8B%E8%AF%95\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testFragment() {\n\t\tfinal String buildUrl = new UrlBuilder()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.setFragment(\"abc\").build();\n\t\tassertEquals(\"https://www.hutool.cn/#abc\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testChineseFragment() {\n\t\tfinal String buildUrl = new UrlBuilder()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.setFragment(\"测试\").build();\n\t\tassertEquals(\"https://www.hutool.cn/#%E6%B5%8B%E8%AF%95\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testChineseFragmentWithPath() {\n\t\tfinal String buildUrl = new UrlBuilder()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.addPath(\"/s\")\n\t\t\t\t.setFragment(\"测试\").build();\n\t\tassertEquals(\"https://www.hutool.cn/s#%E6%B5%8B%E8%AF%95\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void testChineseFragmentWithPathAndQuery() {\n\t\tfinal String buildUrl = new UrlBuilder()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"www.hutool.cn\")\n\t\t\t\t.addPath(\"/s\")\n\t\t\t\t.addQuery(\"wd\", \"test\")\n\t\t\t\t.setFragment(\"测试\").build();\n\t\tassertEquals(\"https://www.hutool.cn/s?wd=test#%E6%B5%8B%E8%AF%95\", buildUrl);\n\t}\n\n\t@Test\n\tpublic void ofTest() {\n\t\tfinal UrlBuilder builder = UrlBuilder.of(\"http://www.hutool.cn/aaa/bbb/?a=1&b=2#frag1\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http\", builder.getScheme());\n\t\tassertEquals(\"www.hutool.cn\", builder.getHost());\n\n\t\tassertEquals(\"aaa\", builder.getPath().getSegment(0));\n\t\tassertEquals(\"bbb\", builder.getPath().getSegment(1));\n\n\t\tassertEquals(\"1\", builder.getQuery().get(\"a\"));\n\t\tassertEquals(\"2\", builder.getQuery().get(\"b\"));\n\n\t\tassertEquals(\"frag1\", builder.getFragment());\n\t}\n\n\t@Test\n\tpublic void ofNullQueryTest() {\n\t\tfinal UrlBuilder builder = UrlBuilder.of(\"http://www.hutool.cn/aaa/bbb\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertNotNull(builder.getQuery());\n\t\tassertNull(builder.getQuery().get(\"a\"));\n\t}\n\n\t@Test\n\tpublic void ofWithChineseTest() {\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(\"www.hutool.cn/aaa/bbb/?a=张三&b=%e6%9d%8e%e5%9b%9b#frag1\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http\", builder.getScheme());\n\t\tassertEquals(\"www.hutool.cn\", builder.getHost());\n\n\t\tassertEquals(\"aaa\", builder.getPath().getSegment(0));\n\t\tassertEquals(\"bbb\", builder.getPath().getSegment(1));\n\n\t\tassertEquals(\"张三\", builder.getQuery().get(\"a\"));\n\t\tassertEquals(\"李四\", builder.getQuery().get(\"b\"));\n\n\t\tassertEquals(\"frag1\", builder.getFragment());\n\t}\n\n\t@Test\n\tpublic void ofWithBlankTest() {\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(\" www.hutool.cn/aaa/bbb/?a=张三&b=%e6%9d%8e%e5%9b%9b#frag1\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http\", builder.getScheme());\n\t\tassertEquals(\"www.hutool.cn\", builder.getHost());\n\n\t\tassertEquals(\"aaa\", builder.getPath().getSegment(0));\n\t\tassertEquals(\"bbb\", builder.getPath().getSegment(1));\n\n\t\tassertEquals(\"张三\", builder.getQuery().get(\"a\"));\n\t\tassertEquals(\"李四\", builder.getQuery().get(\"b\"));\n\n\t\tassertEquals(\"frag1\", builder.getFragment());\n\t}\n\n\t@Test\n\tpublic void ofSpecialTest() {\n\t\t//测试不规范的或者无需解码的字符串是否成功解码\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(\" www.hutool.cn/aaa/bbb/?a=张三&b=%%e5%9b%9b#frag1\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http\", builder.getScheme());\n\t\tassertEquals(\"www.hutool.cn\", builder.getHost());\n\n\t\tassertEquals(\"aaa\", builder.getPath().getSegment(0));\n\t\tassertEquals(\"bbb\", builder.getPath().getSegment(1));\n\n\t\tassertEquals(\"张三\", builder.getQuery().get(\"a\"));\n\t\tassertEquals(\"%四\", builder.getQuery().get(\"b\"));\n\n\t\tassertEquals(\"frag1\", builder.getFragment());\n\t}\n\n\t@Test\n\tpublic void weixinUrlTest() {\n\t\tfinal String urlStr = \"https://mp.weixin.qq.com/s?\" +\n\t\t\t\t\"__biz=MzI5NjkyNTIxMg==\" +\n\t\t\t\t\"&amp;mid=100000465\" +\n\t\t\t\t\"&amp;idx=1\" +\n\t\t\t\t\"&amp;sn=1044c0d19723f74f04f4c1da34eefa35\" +\n\t\t\t\t\"&amp;chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7\";\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);\n\t\t// 原URL中的&amp;替换为&\n\t\tassertEquals(\"https://mp.weixin.qq.com/s?\" +\n\t\t\t\t\t\t\"__biz=MzI5NjkyNTIxMg==\" +\n\t\t\t\t\t\t\"&mid=100000465&idx=1\" +\n\t\t\t\t\t\t\"&sn=1044c0d19723f74f04f4c1da34eefa35\" +\n\t\t\t\t\t\t\"&chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7\",\n\t\t\t\tbuilder.toString());\n\t}\n\n\t@Test\n\tpublic void endWithSlashTest() {\n\t\t// 原URL中以/结尾，则这个规则需保留，issue#I1G44J@Gitee\n\t\tfinal String today = DateUtil.date().toString(\"yyyyMMdd\");\n\t\tfinal String getWorkDayUrl = \"https://tool.bitefu.net/jiari/?info=1&d=\" + today;\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(getWorkDayUrl, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(getWorkDayUrl, builder.toString());\n\t}\n\n\t@Test\n\tpublic void blankEncodeTest() {\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(\"http://a.com/aaa bbb.html\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http://a.com/aaa%20bbb.html\", urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void dotEncodeTest() {\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(\"http://xtbgyy.digitalgd.com.cn/ebus/../../..\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http://xtbgyy.digitalgd.com.cn/ebus/../../..\", urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void multiSlashTest() {\n\t\t//issue#I25MZL，某些URL中有多个斜杠，此为合法路径\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(\"https://hutool.cn//file/test.jpg\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"https://hutool.cn//file/test.jpg\", urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void toURITest() throws URISyntaxException {\n\t\tfinal String webUrl = \"http://exmple.com/patha/pathb?a=123\"; // 报错数据\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8);\n\t\tassertEquals(new URI(webUrl), urlBuilder.toURI());\n\t}\n\n\t@Test\n\tpublic void testEncodeInQuery() {\n\t\tfinal String webUrl = \"http://exmple.com/patha/pathb?a=123&b=4?6&c=789\"; // b=4?6  参数中有未编码的？\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8);\n\t\tassertEquals(\"a=123&b=4?6&c=789\", urlBuilder.getQueryStr());\n\t}\n\n\t@Test\n\tpublic void encodePathTest() {\n\t\t// Path中的某些符号无需转义，比如=\n\t\tfinal String urlStr = \"http://hq.sinajs.cn/list=sh600519\";\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(urlStr, urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void encodePathTest2() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4RA42\n\t\t// Path中`:`在第一个segment需要转义，之后的不需要\n\t\tfinal String urlStr = \"https://hutool.cn/aa/bb/Pre-K,Kindergarten,First,Second,Third,Fourth,Fifth/Page:3\";\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(urlStr, urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void gimg2Test() {\n\t\tfinal String url = \"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea\";\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.of(url);\n\n\t\t// PATH除了第一个path外，:是允许的\n\t\tfinal String url2 = \"https://gimg2.baidu.com/image_search/src=http:%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http:%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea\";\n\t\tassertEquals(url2, urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void fragmentEncodeTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I49KAL\n\t\t// 见：https://stackoverflow.com/questions/26088849/url-fragment-allowed-characters\n\t\tfinal String url = \"https://hutool.cn/docs/#/?id=简介\";\n\t\tUrlBuilder urlBuilder = UrlBuilder.ofHttp(url);\n\t\tassertEquals(\"https://hutool.cn/docs/#/?id=%E7%AE%80%E4%BB%8B\", urlBuilder.toString());\n\n\t\turlBuilder = UrlBuilder.ofHttp(urlBuilder.toString());\n\t\tassertEquals(urlBuilder.toString(), urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void slashEncodeTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/1904\n\t\t// 在query中，\"/\"是不可转义字符\n\t\t// 见：https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4\n\t\tfinal String url = \"https://invoice.maycur.com/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx?download/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx&e=1630491088\";\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);\n\t\tassertEquals(url, urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void addPathEncodeTest() {\n\t\tfinal String url = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"domain.cn\")\n\t\t\t\t.addPath(\"api\")\n\t\t\t\t.addPath(\"xxx\")\n\t\t\t\t.addPath(\"bbb\")\n\t\t\t\t.build();\n\n\t\tassertEquals(\"https://domain.cn/api/xxx/bbb\", url);\n\t}\n\n\t@Test\n\tpublic void addPathEncodeTest2() {\n\t\t// https://github.com/chinabugotech/hutool/issues/1912\n\t\tfinal String url = UrlBuilder.of()\n\t\t\t\t.setScheme(\"https\")\n\t\t\t\t.setHost(\"domain.cn\")\n\t\t\t\t.addPath(\"/api/xxx/bbb\")\n\t\t\t\t.build();\n\n\t\tassertEquals(\"https://domain.cn/api/xxx/bbb\", url);\n\t}\n\n\t@Test\n\tpublic void percent2BTest() {\n\t\tfinal String url = \"http://xxx.cn/a?Signature=3R013Bj9Uq4YeISzAs2iC%2BTVCL8%3D\";\n\t\tfinal UrlBuilder of = UrlBuilder.ofHttpWithoutEncode(url);\n\t\tassertEquals(url, of.toString());\n\t}\n\n\t@Test\n\tpublic void paramTest() {\n\t\tfinal String url = \"http://ci.xiaohongshu.com/spectrum/c136c98aa2047babe25b994a26ffa7b492bd8058?imageMogr2/thumbnail/x800/format/jpg\";\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(url);\n\t\tassertEquals(url, builder.toString());\n\t}\n\n\t@Test\n\tpublic void fragmentTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I49KAL#note_8060874\n\t\tfinal String url = \"https://www.hutool.cn/#/a/b?timestamp=1640391380204\";\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(url);\n\n\t\tassertEquals(url, builder.toString());\n\t}\n\n\t@Test\n\tpublic void fragmentAppendParamTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I49KAL#note_8060874\n\t\tfinal String url = \"https://www.hutool.cn/#/a/b\";\n\t\tfinal UrlBuilder builder = UrlBuilder.ofHttp(url);\n\t\tbuilder.setFragment(builder.getFragment() + \"?timestamp=1640391380204\");\n\t\tassertEquals(\"https://www.hutool.cn/#/a/b?timestamp=1640391380204\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void paramWithPlusTest() {\n\t\tfinal String url = \"http://127.0.0.1/?\" +\n\t\t\t\t\"Expires=1642734164&\" +\n\t\t\t\t\"security-token=CAIS+AF1q6Ft5B2yfSjIr5fYEeju1b1ggpPee2KGpjlgQtdfl43urjz2IHtKdXRvBu8Xs\" +\n\t\t\t\t\"/4wnmxX7f4YlqB6T55OSAmcNZEoPwKpT4zmMeT7oMWQweEurv\" +\n\t\t\t\t\"/MQBqyaXPS2MvVfJ+OLrf0ceusbFbpjzJ6xaCAGxypQ12iN+/m6\" +\n\t\t\t\t\"/Ngdc9FHHPPD1x8CcxROxFppeIDKHLVLozNCBPxhXfKB0ca0WgVy0EHsPnvm5DNs0uH1AKjkbRM9r6ceMb0M5NeW75kSMqw0eBMca7M7TVd8RAi9t0t1\" +\n\t\t\t\t\"/IVpGiY4YDAWQYLv0rda7DOltFiMkpla7MmXqlft+hzcgeQY0pc\" +\n\t\t\t\t\"/RqAAYRYVCBiyuzAexSiDiJX1VqWljg4jYp1sdyv3HpV3sXVcf6VH6AN9ot5YNTw4JNO0aNpLpLm93rRMrOKIOsve+OmNyZ4HS7qHQKt1qp7HY1A\" +\n\t\t\t\t\"/wGhJstkAoGQt+CHSMwVdIx3bVT1+ZYnJdM/oIQ/90afw4EEEQaRE51Z0rQC7z8d\";\n\t\tfinal String build = UrlBuilder.of(url).build();\n\t\tassertEquals(url, build);\n\t}\n\n\t@Test\n\tpublic void issueI4Z2ETTest() {\n\t\t// =是url参数值中的合法字符，但是某些URL强制编码了\n\t\tfinal String url = \"http://dsl-fd.dslbuy.com/fssc/1647947565522.pdf?\" +\n\t\t\t\t\"Expires=1647949365\" +\n\t\t\t\t\"&OSSAccessKeyId=STS.NTZ9hvqPSLG8ENknz2YaByLKj\" +\n\t\t\t\t\"&Signature=oYUu26JufAyPY4PdzaOp1x4sr4Q%3D\";\n\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(url, null);\n\t\tassertEquals(url, urlBuilder.toString());\n\t}\n\n\t@Test\n\tpublic void issue2215Test() {\n\t\tfinal String url = \"https://hutool.cn/v1/104303371/messages:send\";\n\t\tfinal String build = UrlBuilder.of(url).build();\n\t\tassertEquals(url, build);\n\t}\n\n\t@Test\n\tpublic void issuesI4Z2ETTest() {\n\t\tfinal String url = \"http://hutool.cn/2022/03/09/123.zip?Expires=1648704684&OSSAccessKeyId=LTAI4FncgaVtwZGBnYHHi8ox&Signature=%2BK%2B%3D\";\n\t\tfinal String build = UrlBuilder.of(url, null).build();\n\t\tassertEquals(url, build);\n\t}\n\n\t@Test\n\tpublic void issueI50NHQTest() {\n\t\tfinal String url = \"http://127.0.0.1/devicerecord/list\";\n\t\tfinal HashMap<String, Object> params = new LinkedHashMap<>();\n\t\tparams.put(\"start\", \"2022-03-31 00:00:00\");\n\t\tparams.put(\"end\", \"2022-03-31 23:59:59\");\n\t\tparams.put(\"page\", 1);\n\t\tparams.put(\"limit\", 10);\n\n\t\tfinal UrlBuilder builder = UrlBuilder.of(url);\n\t\tparams.forEach(builder::addQuery);\n\t\tassertEquals(\"http://127.0.0.1/devicerecord/list?start=2022-03-31%2000:00:00&end=2022-03-31%2023:59:59&page=1&limit=10\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void issue2242Test() {\n\n\t}\n\n\t@Test\n\tpublic void issue2243Test() {\n\t\t// https://github.com/chinabugotech/hutool/issues/2243\n\t\t// 如果用户已经做了%编码，不应该重复编码\n\t\tfinal String url = \"https://hutool.cn/v1.0?privateNum=%2B8616512884988\";\n\t\tfinal String s = UrlBuilder.of(url, null).setCharset(CharsetUtil.CHARSET_UTF_8).toString();\n\t\tassertEquals(url, s);\n\t}\n\n\t@Test\n\tpublic void issueI51T0VTest() {\n\t\t// &amp;自动转换为&\n\t\tfinal String url = \"https://hutool.cn/a.mp3?Expires=1652423884&amp;key=JMv2rKNc7Pz&amp;sign=12zva00BpVqgZcX1wcb%2BrmN7H3E%3D\";\n\t\tfinal UrlBuilder of = UrlBuilder.of(url, null);\n\t\tassertEquals(url.replace(\"&amp;\", \"&\"), of.toString());\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void issues2503Test() throws URISyntaxException {\n\t\tString duplicate = UrlBuilder.ofHttp(\"127.0.0.1:8080\")\n\t\t\t\t.addQuery(\"param[0].field\", \"编码\")\n\t\t\t\t.toURI()\n\t\t\t\t.toString();\n\t\tassertEquals(\"http://127.0.0.1:8080?param%5B0%5D.field=%E7%BC%96%E7%A0%81\", duplicate);\n\n\t\tString normal = UrlBuilder.ofHttp(\"127.0.0.1:8080\")\n\t\t\t\t.addQuery(\"param[0].field\", \"编码\")\n\t\t\t\t.toURL()\n\t\t\t\t.toURI()\n\t\t\t\t.toString();\n\t\tassertEquals(duplicate, normal);\n\t}\n\n\t@Test\n\tpublic void addPathTest() {\n\t\t//https://gitee.com/chinabugotech/hutool/issues/I5O4ML\n\t\tUrlBuilder.of().addPath(\"\");\n\t\tUrlBuilder.of().addPath(\"/\");\n\t\tUrlBuilder.of().addPath(\"//\");\n\t\tUrlBuilder.of().addPath(\"//a\");\n\t}\n\n\t@Test\n\tpublic void pathWithColonTest() {\n\t\t// https://www.ietf.org/rfc/rfc3986.html#section-3.3\n\t\t// 此处Path中是允许有`:`的，之前理解有误，应该是相对URI的第一个segment中不允许有`:`\n\t\tfinal UrlPath httpUrl = UrlPath.of(\"/a:1/b:1/c:1/d:1/\", CharsetUtil.CHARSET_UTF_8);\n\t\tfinal String build = httpUrl.build(CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"/a:1/b:1/c:1/d:1/\", build);\n\t}\n\n\t@Test\n\tpublic void issueIAAOC1Test() {\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttp(\"http://localhost:9999/getReportDataList?goodsName=工业硫酸98%&conReportTypeId=1\");\n\t\tassertEquals(\"http://localhost:9999/getReportDataList?goodsName=%E5%B7%A5%E4%B8%9A%E7%A1%AB%E9%85%B898%25&conReportTypeId=1\", urlBuilder.build());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/UrlDecoderTest.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.URLUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.StandardCharsets;\n\npublic class UrlDecoderTest {\n\t@Test\n\tpublic void decodeForPathTest(){\n\t\tassertEquals(\"+\", URLDecoder.decodeForPath(\"+\", CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void decodePlusTest() {\n\t\tfinal String decode = URLDecoder.decode(\"+\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\" \", decode);\n\t}\n\n\t@Test\n\tpublic void issue3063Test() throws UnsupportedEncodingException {\n\t\t// https://github.com/chinabugotech/hutool/issues/3063\n\n\t\tfinal String s = \"测试\";\n\t\tfinal String expectedDecode = \"%FE%FF%6D%4B%8B%D5\";\n\n\t\tfinal String s1 = URLUtil.encode(s, StandardCharsets.UTF_16);\n\t\tassertEquals(expectedDecode, s1);\n\t\tfinal String s2 = java.net.URLEncoder.encode(s, \"UTF-16\");\n\t\tassertEquals(expectedDecode, s2);\n\n\t\tfinal String decode = URLDecoder.decode(s1, StandardCharsets.UTF_16);\n\t\tassertEquals(s, decode);\n\n\t\t// 测试编码字符串和非编码字符串混合\n\t\tfinal String mixDecoded = expectedDecode + \"你好\";\n\t\tfinal String decode2 = URLDecoder.decode(mixDecoded, StandardCharsets.UTF_16);\n\t\tassertEquals(\"测试你好\", decode2);\n\n\t\tassertEquals(\n\t\t\tjava.net.URLDecoder.decode(mixDecoded, \"UTF-16\"),\n\t\t\tURLDecoder.decode(mixDecoded, StandardCharsets.UTF_16)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/net/UrlQueryTest.java",
    "content": "package cn.hutool.core.net;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.net.url.UrlBuilder;\nimport cn.hutool.core.net.url.UrlQuery;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.URLUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.TreeMap;\n\npublic class UrlQueryTest {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tString queryStr = \"a=1&b=111==\";\n\t\tUrlQuery q = new UrlQuery();\n\t\tUrlQuery parse = q.parse(queryStr, Charset.defaultCharset());\n\t\tassertEquals(\"111==\", parse.get(\"b\"));\n\t\tassertEquals(\"a=1&b=111==\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void ofHttpWithoutEncodeTest(){\n\t\t// charset为null表示不做编码\n\t\tString url = \"https://img-cloud.voc.com.cn/140/2020/09/03/c3d41b93e0d32138574af8e8b50928b376ca5ba61599127028157.png?imageMogr2/auto-orient/thumbnail/500&pid=259848\";\n\t\tfinal UrlBuilder urlBuilder = UrlBuilder.ofHttpWithoutEncode(url);\n\t\tfinal String queryStr = urlBuilder.getQueryStr();\n\t\tassertEquals(\"imageMogr2/auto-orient/thumbnail/500&pid=259848\", queryStr);\n\t}\n\n\t@Test\n\tpublic void parseTest2(){\n\t\tString requestUrl = \"http://192.168.1.1:8080/pc?=d52i5837i4ed=o39-ap9e19s5--=72e54*ll0lodl-f338868d2\";\n\t\tUrlQuery q = new UrlQuery();\n\t\tUrlQuery parse = q.parse(requestUrl, Charset.defaultCharset());\n\t\tassertEquals(\"=d52i5837i4ed=o39-ap9e19s5--=72e54*ll0lodl-f338868d2\", parse.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest3(){\n\t\t// issue#1688@Github\n\t\tString u = \"https://www.baidu.com/proxy\";\n\t\tfinal UrlQuery query = UrlQuery.of(URLUtil.url(u).getQuery(), Charset.defaultCharset());\n\t\tassertTrue(MapUtil.isEmpty(query.getQueryMap()));\n\t}\n\n\t@Test\n\tpublic void parseTest4(){\n\t\t// https://github.com/chinabugotech/hutool/issues/1989\n\t\tString queryStr = \"imageMogr2/thumbnail/x800/format/jpg\";\n\t\tfinal UrlQuery query = UrlQuery.of(queryStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(queryStr, query.toString());\n\t}\n\n\t@Test\n\tpublic void buildWithMapTest() {\n\t\tMap<String, String> map = new LinkedHashMap<>();\n\t\tmap.put(\"username\", \"SSM\");\n\t\tmap.put(\"password\", \"123456\");\n\t\tString query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"username=SSM&password=123456\", query);\n\n\t\tmap = new TreeMap<>();\n\t\tmap.put(\"username\", \"SSM\");\n\t\tmap.put(\"password\", \"123456\");\n\t\tquery = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"password=123456&username=SSM\", query);\n\t}\n\n\t@Test\n\tpublic void buildHasNullTest() {\n\t\tMap<String, String> map = new LinkedHashMap<>();\n\t\tmap.put(null, \"SSM\");\n\t\tmap.put(\"password\", \"123456\");\n\t\tString query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"password=123456\", query);\n\n\t\tmap = new TreeMap<>();\n\t\tmap.put(\"username\", \"SSM\");\n\t\tmap.put(\"password\", \"\");\n\t\tquery = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"password=&username=SSM\", query);\n\n\t\tmap = new TreeMap<>();\n\t\tmap.put(\"username\", \"SSM\");\n\t\tmap.put(\"password\", null);\n\t\tquery = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"password&username=SSM\", query);\n\t}\n\n\t@Test\n\tpublic void buildSpecialTest() {\n\t\tMap<String, String> map = new LinkedHashMap<>();\n\t\tmap.put(\"key1&\", \"SSM\");\n\t\tmap.put(\"key2\", \"123456&\");\n\t\tString query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"key1%26=SSM&key2=123456%26\", query);\n\n\t\tmap = new TreeMap<>();\n\t\tmap.put(\"username=\", \"SSM\");\n\t\tmap.put(\"password\", \"=\");\n\t\tquery = URLUtil.buildQuery(map, StandardCharsets.UTF_8);\n\t\tassertEquals(\"password==&username%3D=SSM\", query);\n\t}\n\n\t@Test\n\tpublic void plusTest(){\n\t\t// 根据RFC3986，在URL中，+是安全字符，即此符号不转义\n\t\tfinal String a = UrlQuery.of(MapUtil.of(\"a+b\", \"1+2\")).build(CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a+b=1+2\", a);\n\t}\n\n\t@Test\n\tpublic void parsePlusTest(){\n\t\t// 根据RFC3986，在URL中，+是安全字符，即此符号不转义\n\t\tfinal String a = UrlQuery.of(\"a+b=1+2\", CharsetUtil.CHARSET_UTF_8)\n\t\t\t\t.build(CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a+b=1+2\", a);\n\t}\n\n\t@Test\n\tpublic void spaceTest(){\n\t\t// 根据RFC3986，在URL中，空格编码为\"%20\"\n\t\tfinal String a = UrlQuery.of(MapUtil.of(\"a \", \" \")).build(CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a%20=%20\", a);\n\t}\n\n\t@Test\n\tpublic void parsePercentTest(){\n\t\tString queryStr = \"a%2B=ccc\";\n\t\tfinal UrlQuery query = UrlQuery.of(queryStr, null);\n\t\tassertEquals(queryStr, query.toString());\n\t}\n\n\t@Test\n\tpublic void parsePercentTest2(){\n\t\tString queryStr = \"signature=%2Br1ekUCGjXiu50Y%2Bk0MO4ovulK8%3D\";\n\t\tfinal UrlQuery query = UrlQuery.of(queryStr, null);\n\t\tassertEquals(queryStr, query.toString());\n\t}\n\n\t@Test\n\tpublic void issueI78PB1Test() {\n\t\t// 严格模式\n\t\tfinal UrlQuery query = new UrlQuery().setStrict(true);\n\t\tquery.add(\":/?#[]@!$&'()*+,;= \", \":/?#[]@!$&'()*+,;= \");\n\n\t\tfinal String string = query.build(CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%20=\" +\n\t\t\t\"%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%20\", string);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/stream/CollectorUtilTest.java",
    "content": "package cn.hutool.core.stream;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.map.MapUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic class CollectorUtilTest {\n\t@Test\n\tpublic void reduceListMapTest() {\n\t\tfinal Set<Map<String, Integer>> nameScoreMapList = StreamUtil.of(\n\t\t\t\t// 集合内的第一个map，包含两个key value\n\t\t\t\tMapUtil.builder(\"苏格拉底\", 1).put(\"特拉叙马霍斯\", 3).build(),\n\t\t\t\tMapUtil.of(\"苏格拉底\", 2),\n\t\t\t\tMapUtil.of(\"特拉叙马霍斯\", 1),\n\t\t\t\tMapUtil.of(\"特拉叙马霍斯\", 2)\n\t\t).collect(Collectors.toSet());\n\t\t// 执行聚合\n\t\tfinal Map<String, List<Integer>> nameScoresMap = nameScoreMapList.stream().collect(CollectorUtil.reduceListMap());\n\n\t\tassertEquals(MapUtil.builder(\"苏格拉底\", Arrays.asList(1, 2))\n\t\t\t\t\t\t.put(\"特拉叙马霍斯\", Arrays.asList(3, 1, 2)).build(),\n\t\t\t\tnameScoresMap);\n\n\t\tList<Map<String, String>> data = ListUtil.toList(\n\t\t\tMapUtil.builder(\"name\", \"sam\").put(\"count\", \"80\").map(),\n\t\t\tMapUtil.builder(\"name\", \"sam\").put(\"count\", \"81\").map(),\n\t\t\tMapUtil.builder(\"name\", \"sam\").put(\"count\", \"82\").map(),\n\t\t\tMapUtil.builder(\"name\", \"jack\").put(\"count\", \"80\").map(),\n\t\t\tMapUtil.builder(\"name\", \"jack\").put(\"count\", \"90\").map()\n\t\t);\n\n\t\tMap<String, Map<String, List<String>>> nameMap = data.stream()\n\t\t\t.collect(Collectors.groupingBy(e -> e.get(\"name\"), CollectorUtil.reduceListMap()));\n\t\tassertEquals(MapUtil.builder(\"jack\", MapUtil.builder(\"name\", Arrays.asList(\"jack\", \"jack\"))\n\t\t\t\t.put(\"count\", Arrays.asList(\"80\", \"90\")).build())\n\t\t\t.put(\"sam\", MapUtil.builder(\"name\", Arrays.asList(\"sam\", \"sam\", \"sam\"))\n\t\t\t\t.put(\"count\", Arrays.asList(\"80\", \"81\", \"82\")).build())\n\t\t\t.build(), nameMap);\n\t}\n\n\t@Test\n\tpublic void testGroupingByAfterValueMapped() {\n\t\tList<Integer> list = Arrays.asList(1, 1, 2, 2, 3, 4);\n\t\tMap<Boolean, Set<String>> map = list.stream()\n\t\t\t\t.collect(CollectorUtil.groupingBy(t -> (t & 1) == 0, String::valueOf, LinkedHashSet::new, LinkedHashMap::new));\n\n\t\tassertEquals(LinkedHashMap.class, map.getClass());\n\t\tassertEquals(new LinkedHashSet<>(Arrays.asList(\"2\", \"4\")), map.get(Boolean.TRUE));\n\t\tassertEquals(new LinkedHashSet<>(Arrays.asList(\"1\", \"3\")), map.get(Boolean.FALSE));\n\n\t\tmap = list.stream()\n\t\t\t\t.collect(CollectorUtil.groupingBy(t -> (t & 1) == 0, String::valueOf, LinkedHashSet::new));\n\t\tassertEquals(HashMap.class, map.getClass());\n\t\tassertEquals(new LinkedHashSet<>(Arrays.asList(\"2\", \"4\")), map.get(Boolean.TRUE));\n\t\tassertEquals(new LinkedHashSet<>(Arrays.asList(\"1\", \"3\")), map.get(Boolean.FALSE));\n\n\t\tfinal Map<Boolean, List<String>> map2 = list.stream()\n\t\t\t\t.collect(CollectorUtil.groupingBy(t -> (t & 1) == 0, String::valueOf));\n\t\tassertEquals(Arrays.asList(\"2\", \"2\", \"4\"), map2.get(Boolean.TRUE));\n\t\tassertEquals(Arrays.asList(\"1\", \"1\", \"3\"), map2.get(Boolean.FALSE));\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/stream/StreamUtilTest.java",
    "content": "package cn.hutool.core.stream;\n\nimport cn.hutool.core.collection.CollUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class StreamUtilTest {\n\n\t@Test\n\tpublic void ofTest(){\n\t\tfinal Stream<Integer> stream = StreamUtil.of(2, x -> x * 2, 4);\n\t\tfinal String result = stream.collect(CollectorUtil.joining(\",\"));\n\t\tassertEquals(\"2,4,8,16\", result);\n\t}\n\n\t// === iterator ===\n\t@Test\n\tpublic void streamTestNullIterator() {\n\t\tassertThrows(IllegalArgumentException.class, () -> StreamUtil.of((Iterator<Object>) null));\n\t}\n\n\t@SuppressWarnings({\"RedundantOperationOnEmptyContainer\", \"RedundantCollectionOperation\"})\n\t@Test\n\tpublic void streamTestEmptyListToIterator() {\n\t\tassertStreamIsEmpty(StreamUtil.of(new ArrayList<>().iterator()));\n\t}\n\n\t@Test\n\tpublic void streamTestEmptyIterator() {\n\t\tassertStreamIsEmpty(StreamUtil.of(Collections.emptyIterator()));\n\t}\n\n\t@Test\n\tpublic void streamTestOrdinaryIterator() {\n\t\tfinal ArrayList<Integer> arrayList = CollUtil.newArrayList(1, 2, 3);\n\t\tassertArrayEquals(new Integer[]{1, 2, 3}, StreamUtil.of(arrayList.iterator()).toArray());\n\n\t\tfinal HashSet<Integer> hashSet = CollUtil.newHashSet(1, 2, 3);\n\t\tassertEquals(hashSet, StreamUtil.of(hashSet.iterator()).collect(Collectors.toSet()));\n\t}\n\n\tvoid assertStreamIsEmpty(final Stream<?> stream) {\n\t\tassertNotNull(stream);\n\t\tassertEquals(0, stream.toArray().length);\n\t}\n\t// ================ stream test end ================\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/swing/ClipboardMonitorTest.java",
    "content": "package cn.hutool.core.swing;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.swing.clipboard.ClipboardUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ClipboardMonitorTest {\n\n\t@Test\n\t@Disabled\n\tpublic void monitorTest() {\n\t\t// 第一个监听\n\t\tClipboardUtil.listen((clipboard, contents) -> {\n\t\t\tObject object = ClipboardUtil.getStr(contents);\n\t\t\tConsole.log(\"1# {}\", object);\n\t\t\treturn contents;\n\t\t}, false);\n\n\t\t// 第二个监听\n\t\tClipboardUtil.listen((clipboard, contents) -> {\n\t\t\tObject object = ClipboardUtil.getStr(contents);\n\t\t\tConsole.log(\"2# {}\", object);\n\t\t\treturn contents;\n\t\t});\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/swing/ClipboardUtilTest.java",
    "content": "package cn.hutool.core.swing;\n\nimport cn.hutool.core.swing.clipboard.ClipboardUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 剪贴板工具类单元测试\n *\n * @author looly\n *\n */\npublic class ClipboardUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void setAndGetStrTest() {\n\t\ttry {\n\t\t\tClipboardUtil.setStr(\"test\");\n\n\t\t\tString test = ClipboardUtil.getStr();\n\t\t\tassertEquals(\"test\", test);\n\t\t} catch (java.awt.HeadlessException e) {\n\t\t\t// 忽略 No X11 DISPLAY variable was set, but this program performed an operation which requires it.\n\t\t\t// ignore\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/swing/DesktopUtilTest.java",
    "content": "package cn.hutool.core.swing;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class DesktopUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void browseTest() {\n\t\tDesktopUtil.browse(\"https://www.hutool.club\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/swing/RobotUtilTest.java",
    "content": "package cn.hutool.core.swing;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.FileUtil;\n\npublic class RobotUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void captureScreenTest() {\n\t\tRobotUtil.captureScreen(FileUtil.file(\"e:/screen.jpg\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/AntPathMatcherTest.java",
    "content": "package cn.hutool.core.text;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\npublic class AntPathMatcherTest {\n\n\t@Test\n\tpublic void matchesTest() {\n\t\tAntPathMatcher antPathMatcher = new AntPathMatcher();\n\t\tboolean matched = antPathMatcher.match(\"/api/org/organization/{orgId}\", \"/api/org/organization/999\");\n\t\tassertTrue(matched);\n\t}\n\n\t@Test\n\tpublic void matchesTest2() {\n\t\tAntPathMatcher antPathMatcher = new AntPathMatcher();\n\n\t\tString pattern = \"/**/*.xml*\";\n\t\tString path = \"/WEB-INF/web.xml\";\n\t\tboolean isMatched = antPathMatcher.match(pattern, path);\n\t\tassertTrue(isMatched);\n\n\t\tpattern = \"org/codelabor/*/**/*Service\";\n\t\tpath = \"org/codelabor/example/HelloWorldService\";\n\t\tisMatched = antPathMatcher.match(pattern, path);\n\t\tassertTrue(isMatched);\n\n\t\tpattern = \"org/codelabor/*/**/*Service?\";\n\t\tpath = \"org/codelabor/example/HelloWorldServices\";\n\t\tisMatched = antPathMatcher.match(pattern, path);\n\t\tassertTrue(isMatched);\n\t}\n\n\t@Test\n\tpublic void matchesTest3(){\n\t\tAntPathMatcher pathMatcher = new AntPathMatcher();\n\t\tpathMatcher.setCachePatterns(true);\n\t\tpathMatcher.setCaseSensitive(true);\n\t\tpathMatcher.setPathSeparator(\"/\");\n\t\tpathMatcher.setTrimTokens(true);\n\n\t\tassertTrue(pathMatcher.match(\"a\", \"a\"));\n\t\tassertTrue(pathMatcher.match(\"a*\", \"ab\"));\n\t\tassertTrue(pathMatcher.match(\"a*/**/a\", \"ab/asdsa/a\"));\n\t\tassertTrue(pathMatcher.match(\"a*/**/a\", \"ab/asdsa/asdasd/a\"));\n\n\t\tassertTrue(pathMatcher.match(\"*\", \"a\"));\n\t\tassertTrue(pathMatcher.match(\"*/*\", \"a/a\"));\n\t}\n\n\t/**\n\t * AntPathMatcher默认路径分隔符为“/”，而在匹配文件路径时，需要注意Windows下路径分隔符为“\\”，Linux下为“/”。靠谱写法如下两种方式：\n\t * AntPathMatcher matcher = new AntPathMatcher(File.separator);\n\t * AntPathMatcher matcher = new AntPathMatcher(System.getProperty(\"file.separator\"));\n\t */\n\t@Test\n\tpublic void matchesTest4() {\n\t\tAntPathMatcher pathMatcher = new AntPathMatcher();\n\n\t\t// 精确匹配\n\t\tassertTrue(pathMatcher.match(\"/test\", \"/test\"));\n\t\tassertFalse(pathMatcher.match(\"test\", \"/test\"));\n\n\t\t//测试通配符?\n\t\tassertTrue(pathMatcher.match(\"t?st\", \"test\"));\n\t\tassertTrue(pathMatcher.match(\"te??\", \"test\"));\n\t\tassertFalse(pathMatcher.match(\"tes?\", \"tes\"));\n\t\tassertFalse(pathMatcher.match(\"tes?\", \"testt\"));\n\n\t\t//测试通配符*\n\t\tassertTrue(pathMatcher.match(\"*\", \"test\"));\n\t\tassertTrue(pathMatcher.match(\"test*\", \"test\"));\n\t\tassertTrue(pathMatcher.match(\"test/*\", \"test/Test\"));\n\t\tassertTrue(pathMatcher.match(\"*.*\", \"test.\"));\n\t\tassertTrue(pathMatcher.match(\"*.*\", \"test.test.test\"));\n\t\tassertFalse(pathMatcher.match(\"test*\", \"test/\")); //注意这里是false 因为路径不能用*匹配\n\t\tassertFalse(pathMatcher.match(\"test*\", \"test/t\")); //这同理\n\t\tassertFalse(pathMatcher.match(\"test*aaa\", \"testblaaab\")); //这个是false 因为最后一个b无法匹配了 前面都是能匹配成功的\n\n\t\t//测试通配符** 匹配多级URL\n\t\tassertTrue(pathMatcher.match(\"/*/**\", \"/testing/testing\"));\n\t\tassertTrue(pathMatcher.match(\"/**/*\", \"/testing/testing\"));\n\t\tassertTrue(pathMatcher.match(\"/bla/**/bla\", \"/bla/testing/testing/bla/bla\")); //这里也是true哦\n\t\tassertFalse(pathMatcher.match(\"/bla*bla/test\", \"/blaXXXbl/test\"));\n\n\t\tassertFalse(pathMatcher.match(\"/????\", \"/bala/bla\"));\n\t\tassertFalse(pathMatcher.match(\"/**/*bla\", \"/bla/bla/bla/bbb\"));\n\n\t\tassertTrue(pathMatcher.match(\"/*bla*/**/bla/**\", \"/XXXblaXXXX/testing/testing/bla/testing/testing/\"));\n\t\tassertTrue(pathMatcher.match(\"/*bla*/**/bla/*\", \"/XXXblaXXXX/testing/testing/bla/testing\"));\n\t\tassertTrue(pathMatcher.match(\"/*bla*/**/bla/**\", \"/XXXblaXXXX/testing/testing/bla/testing/testing\"));\n\t\tassertTrue(pathMatcher.match(\"/*bla*/**/bla/**\", \"/XXXblaXXXX/testing/testing/bla/testing/testing.jpg\"));\n\t\tassertTrue(pathMatcher.match(\"/foo/bar/**\", \"/foo/bar\"));\n\n\t\t//这个需要特别注意：{}里面的相当于Spring MVC里接受一个参数一样，所以任何东西都会匹配的\n\t\tassertTrue(pathMatcher.match(\"/{bla}.*\", \"/testing.html\"));\n\t\tassertFalse(pathMatcher.match(\"/{bla}.htm\", \"/testing.html\")); //这样就是false了\n\t}\n\n\t/**\n\t * 测试 URI 模板变量提取\n\t */\n\t@Test\n\tpublic void testExtractUriTemplateVariables() {\n\t\tAntPathMatcher antPathMatcher = new AntPathMatcher();\n\t\tHashMap<String, String> map = (HashMap<String, String>) antPathMatcher.extractUriTemplateVariables(\"/api/org/organization/{orgId}\",\n\t\t\t\t\"/api/org\" +\n\t\t\t\t\t\t\"/organization\" +\n\t\t\t\t\t\t\"/999\");\n\t\tassertEquals(1, map.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/CharSequenceUtilTest.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.regex.Pattern;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class CharSequenceUtilTest {\n\n\t@Test\n\tpublic void replaceTest() {\n\t\tString actual = CharSequenceUtil.replace(\"SSM15930297701BeryAllen\", Pattern.compile(\"[0-9]\"), matcher -> \"\");\n\t\tassertEquals(\"SSMBeryAllen\", actual);\n\t}\n\n\t@Test\n\tpublic void replaceTest2() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4M16G\n\t\tString replace = \"#{A}\";\n\t\tString result = CharSequenceUtil.replace(replace, \"#{AAAAAAA}\", \"1\");\n\t\tassertEquals(replace, result);\n\t}\n\n\t@Test\n\tpublic void replaceByStrTest() {\n\t\tString replace = \"SSM15930297701BeryAllen\";\n\t\tString result = CharSequenceUtil.replaceByCodePoint(replace, 5, 12, \"***\");\n\t\tassertEquals(\"SSM15***01BeryAllen\", result);\n\t}\n\n\t@Test\n\tpublic void addPrefixIfNotTest() {\n\t\tString str = \"hutool\";\n\t\tString result = CharSequenceUtil.addPrefixIfNot(str, \"hu\");\n\t\tassertEquals(str, result);\n\n\t\tresult = CharSequenceUtil.addPrefixIfNot(str, \"Good\");\n\t\tassertEquals(\"Good\" + str, result);\n\t}\n\n\t@Test\n\tpublic void addSuffixIfNotTest() {\n\t\tString str = \"hutool\";\n\t\tString result = CharSequenceUtil.addSuffixIfNot(str, \"tool\");\n\t\tassertEquals(str, result);\n\n\t\tresult = CharSequenceUtil.addSuffixIfNot(str, \" is Good\");\n\t\tassertEquals(str + \" is Good\", result);\n\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4NS0F\n\t\tresult = CharSequenceUtil.addSuffixIfNot(\"\", \"/\");\n\t\tassertEquals(\"/\", result);\n\t}\n\n\t@Test\n\tpublic void normalizeTest() {\n\t\t// https://blog.csdn.net/oscar999/article/details/105326270\n\n\t\tString str1 = \"\\u00C1\";\n\t\tString str2 = \"\\u0041\\u0301\";\n\n\t\tassertNotEquals(str1, str2);\n\n\t\tstr1 = CharSequenceUtil.normalize(str1);\n\t\tstr2 = CharSequenceUtil.normalize(str2);\n\t\tassertEquals(str1, str2);\n\t}\n\n\t@Test\n\tpublic void indexOfTest() {\n\t\tint index = CharSequenceUtil.indexOf(\"abc123\", '1');\n\t\tassertEquals(3, index);\n\t\tindex = CharSequenceUtil.indexOf(\"abc123\", '3');\n\t\tassertEquals(5, index);\n\t\tindex = CharSequenceUtil.indexOf(\"abc123\", 'a');\n\t\tassertEquals(0, index);\n\t}\n\n\t@Test\n\tpublic void indexOfTest2() {\n\t\tint index = CharSequenceUtil.indexOf(\"abc123\", '1', 0, 3);\n\t\tassertEquals(-1, index);\n\n\t\tindex = CharSequenceUtil.indexOf(\"abc123\", 'b', 0, 3);\n\t\tassertEquals(1, index);\n\t}\n\n\t@Test\n\tpublic void subPreGbkTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4JO2E\n\t\tString s = \"华硕K42Intel酷睿i31代2G以下独立显卡不含机械硬盘固态硬盘120GB-192GB4GB-6GB\";\n\n\t\tString v = CharSequenceUtil.subPreGbk(s, 40, false);\n\t\tassertEquals(39, v.getBytes(CharsetUtil.CHARSET_GBK).length);\n\n\t\tv = CharSequenceUtil.subPreGbk(s, 40, true);\n\t\tassertEquals(41, v.getBytes(CharsetUtil.CHARSET_GBK).length);\n\t}\n\n\t@Test\n\tpublic void startWithTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4MV7Q\n\t\tassertFalse(CharSequenceUtil.startWith(\"123\", \"123\", false, true));\n\t\tassertFalse(CharSequenceUtil.startWith(null, null, false, true));\n\t\tassertFalse(CharSequenceUtil.startWith(\"abc\", \"abc\", true, true));\n\n\t\tassertTrue(CharSequenceUtil.startWithIgnoreCase(null, null));\n\t\tassertFalse(CharSequenceUtil.startWithIgnoreCase(null, \"abc\"));\n\t\tassertFalse(CharSequenceUtil.startWithIgnoreCase(\"abcdef\", null));\n\t\tassertTrue(CharSequenceUtil.startWithIgnoreCase(\"abcdef\", \"abc\"));\n\t\tassertTrue(CharSequenceUtil.startWithIgnoreCase(\"ABCDEF\", \"abc\"));\n\t}\n\n\t@Test\n\tpublic void endWithTest() {\n\t\tassertFalse(CharSequenceUtil.endWith(\"123\", \"123\", false, true));\n\t\tassertFalse(CharSequenceUtil.endWith(null, null, false, true));\n\t\tassertFalse(CharSequenceUtil.endWith(\"abc\", \"abc\", true, true));\n\n\t\tassertTrue(CharSequenceUtil.endWithIgnoreCase(null, null));\n\t\tassertFalse(CharSequenceUtil.endWithIgnoreCase(null, \"abc\"));\n\t\tassertFalse(CharSequenceUtil.endWithIgnoreCase(\"abcdef\", null));\n\t\tassertTrue(CharSequenceUtil.endWithIgnoreCase(\"abcdef\", \"def\"));\n\t\tassertTrue(CharSequenceUtil.endWithIgnoreCase(\"ABCDEF\", \"def\"));\n\t}\n\n\t@Test\n\tpublic void removePrefixIgnoreCaseTest(){\n\t\tassertEquals(\"de\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"abc\"));\n\t\tassertEquals(\"de\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"ABC\"));\n\t\tassertEquals(\"de\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"Abc\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", null));\n\t\tassertEquals(\"\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"ABCde\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removePrefixIgnoreCase(\"ABCde\", \"ABCdef\"));\n\t\tassertNull(CharSequenceUtil.removePrefixIgnoreCase(null, \"ABCdef\"));\n\t}\n\n\t@Test\n\tpublic void removeSuffixIgnoreCaseTest(){\n\t\tassertEquals(\"AB\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"cde\"));\n\t\tassertEquals(\"AB\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"CDE\"));\n\t\tassertEquals(\"AB\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"Cde\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", null));\n\t\tassertEquals(\"\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"ABCde\"));\n\t\tassertEquals(\"ABCde\", CharSequenceUtil.removeSuffixIgnoreCase(\"ABCde\", \"ABCdef\"));\n\t\tassertNull(CharSequenceUtil.removeSuffixIgnoreCase(null, \"ABCdef\"));\n\t}\n\n\t@Test\n\tpublic void trimToNullTest(){\n\t\tString a = \"  \";\n\t\tassertNull(CharSequenceUtil.trimToNull(a));\n\n\t\ta = \"\";\n\t\tassertNull(CharSequenceUtil.trimToNull(a));\n\n\t\ta = null;\n\t\tassertNull(CharSequenceUtil.trimToNull(a));\n\t}\n\n\t@Test\n\tpublic void commonPrefixTest() throws Exception{\n\n\t\t// -------------------------- None match -----------------------\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(\"\", \"abc\"));\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(null, \"abc\"));\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(\"abc\", null));\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(\"abc\", \"\"));\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(\"azzzj\", \"bzzzj\"));\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonPrefix(\"english中文\", \"french中文\"));\n\n\t\t// -------------------------- Matched -----------------------\n\n\t\tassertEquals(\"name_\", CharSequenceUtil.commonPrefix(\"name_abc\", \"name_efg\"));\n\n\t\tassertEquals(\"zzzj\", CharSequenceUtil.commonPrefix(\"zzzja\", \"zzzjb\"));\n\n\t\tassertEquals(\"中文\", CharSequenceUtil.commonPrefix(\"中文english\", \"中文french\"));\n\n\t\t// { space * 10 } + \"abc\"\n\t\tfinal String str1 = CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 10) + \"abc\";\n\n\t\t// { space * 5 } + \"efg\"\n\t\tfinal String str2 = CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 5) + \"efg\";\n\n\t\t// Expect common prefix: { space * 5 }\n\t\tassertEquals(CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 5), CharSequenceUtil.commonPrefix(str1, str2));\n\t}\n\n\t@Test\n\tpublic void commonSuffixTest() throws Exception{\n\n\t\t// -------------------------- None match -----------------------\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(\"\", \"abc\"));\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(null, \"abc\"));\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(\"abc\", null));\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(\"abc\", \"\"));\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(\"zzzja\", \"zzzjb\"));\n\n\t\tassertEquals(\"\", CharSequenceUtil.commonSuffix(\"中文english\", \"中文Korean\"));\n\n\t\t// -------------------------- Matched -----------------------\n\n\t\tassertEquals(\"_name\", CharSequenceUtil.commonSuffix(\"abc_name\", \"efg_name\"));\n\n\t\tassertEquals(\"zzzj\", CharSequenceUtil.commonSuffix(\"abczzzj\", \"efgzzzj\"));\n\n\t\tassertEquals(\"中文\", CharSequenceUtil.commonSuffix(\"english中文\", \"Korean中文\"));\n\n\t\t// \"abc\" + { space * 10 }\n\t\tfinal String str1 = \"abc\" + CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 10);\n\n\t\t// \"efg\" + { space * 15 }\n\t\tfinal String str2 = \"efg\" + CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 15);\n\n\t\t// Expect common suffix: { space * 10 }\n\t\tassertEquals(CharSequenceUtil.repeat(CharSequenceUtil.SPACE, 10), CharSequenceUtil.commonSuffix(str1, str2));\n\t}\n\n\t@Test\n\tpublic void testContainsOnly() {\n\t\t// 测试空字符串\n\t\tassertTrue(CharSequenceUtil.containsOnly(\"\", 'a', 'b'));\n\n\t\t// 测试字符串只包含testChars中的字符\n\t\tassertTrue(CharSequenceUtil.containsOnly(\"asdf\", 'a', 's', 'd', 'f'));\n\n\t\t// 测试字符串包含testChars中的字符和其它字符\n\t\tassertFalse(CharSequenceUtil.containsOnly(\"asdf123\", 'a', 's', 'd', 'f'));\n\n\t\t// 测试字符串不包含testChars中的任何字符\n\t\tassertFalse(CharSequenceUtil.containsOnly(\"hello\", 'a', 'b'));\n\n\t\t// 测试字符串为null\n\t\tassertTrue(CharSequenceUtil.containsOnly(null, 'a', 'b'));\n\t}\n\n\t@Test\n\tpublic void removeAllPrefixTest() {\n\t\tfinal String prefix = \"ab\";\n\n\t\tString str = \"ababcdef\";\n\t\tString result = CharSequenceUtil.removeAllPrefix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"abcdef\";\n\t\tresult = CharSequenceUtil.removeAllPrefix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"cdef\";\n\t\tresult = CharSequenceUtil.removeAllPrefix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"\";\n\t\tresult = CharSequenceUtil.removeAllPrefix(str, prefix);\n\t\tassertEquals(\"\", result);\n\n\t\tstr = null;\n\t\tresult = CharSequenceUtil.removeAllPrefix(str, prefix);\n\t\tassertNull(result);\n\t}\n\n\t@Test\n\tpublic void removeAllSuffixTest() {\n\t\tfinal String prefix = \"ab\";\n\n\t\tString str = \"cdefabab\";\n\t\tString result = CharSequenceUtil.removeAllSuffix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"cdefab\";\n\t\tresult = CharSequenceUtil.removeAllSuffix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"cdef\";\n\t\tresult = CharSequenceUtil.removeAllSuffix(str, prefix);\n\t\tassertEquals(\"cdef\", result);\n\n\t\tstr = \"\";\n\t\tresult = CharSequenceUtil.removeAllSuffix(str, prefix);\n\t\tassertEquals(\"\", result);\n\n\t\tstr = null;\n\t\tresult = CharSequenceUtil.removeAllSuffix(str, prefix);\n\t\tassertNull(result);\n\t}\n\n\t@Test\n\tpublic void stripIgnoreCaseTest() {\n\n\t\tfinal String SOURCE_STRING = \"aaa_STRIPPED_bbb\";\n\n\t\t// ---------------------------- test strip ----------------------------\n\n\t\t// Normal test\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"a\"));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"\"));\n\t\tassertEquals(\"aa_STRIPPED_bb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"A\", \"b\"));\n\n\t\t// test null param\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, null, null));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"\", \"\"));\n\t\tassertEquals(\"aaa_STRIPPED_bb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"\", \"B\"));\n\t\tassertEquals(\"aaa_STRIPPED_bb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, null, \"b\"));\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"a\", \"\"));\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.stripIgnoreCase(SOURCE_STRING, \"a\", null));\n\t\t// 本次提交前无法通过的 case\n\t\tassertEquals(\"\", CharSequenceUtil.stripIgnoreCase(\"a\", \"a\", \"a\"));\n\n\t\t// 前缀后缀有重叠，优先去掉前缀\n\t\tassertEquals(\"a\", CharSequenceUtil.stripIgnoreCase(\"aba\", \"aB\", \"bB\"));\n\t}\n\n\t@Test\n\tpublic void stripTest() {\n\n\t\tfinal String SOURCE_STRING = \"aaa_STRIPPED_bbb\";\n\n\t\t// ---------------------------- test strip ----------------------------\n\n\t\t// Normal test\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.strip(SOURCE_STRING, \"a\"));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.strip(SOURCE_STRING, \"\"));\n\t\tassertEquals(\"aa_STRIPPED_bb\", CharSequenceUtil.strip(SOURCE_STRING, \"a\", \"b\"));\n\n\t\t// test null param\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.strip(SOURCE_STRING, null, null));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.strip(SOURCE_STRING, \"\", \"\"));\n\t\tassertEquals(\"aaa_STRIPPED_bb\", CharSequenceUtil.strip(SOURCE_STRING, \"\", \"b\"));\n\t\tassertEquals(\"aaa_STRIPPED_bb\", CharSequenceUtil.strip(SOURCE_STRING, null, \"b\"));\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.strip(SOURCE_STRING, \"a\", \"\"));\n\t\tassertEquals(\"aa_STRIPPED_bbb\", CharSequenceUtil.strip(SOURCE_STRING, \"a\", null));\n\t\t// 本次提交前无法通过的 case\n\t\tassertEquals(\"\", CharSequenceUtil.strip(\"a\", \"a\", \"a\"));\n\n\t\t// 前缀后缀有重叠，优先去掉前缀\n\t\tassertEquals(\"a\", CharSequenceUtil.strip(\"aba\", \"ab\", \"ba\"));\n\t}\n\n\t@Test\n\tpublic void stripAllTest() {\n\t\tfinal String SOURCE_STRING = \"aaa_STRIPPED_bbb\";\n\n\t\t// ---------------------------- test stripAll ----------------------------\n\n\t\t// Normal test\n\t\tassertEquals(\"_STRIPPED_bbb\", CharSequenceUtil.stripAll(SOURCE_STRING, \"a\"));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripAll(SOURCE_STRING, \"\"));\n\n\t\t// test null param\n\t\tassertEquals(\"_STRIPPED_\", CharSequenceUtil.stripAll(SOURCE_STRING, \"a\", \"b\"));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripAll(SOURCE_STRING, null, null));\n\t\tassertEquals(SOURCE_STRING, CharSequenceUtil.stripAll(SOURCE_STRING, \"\", \"\"));\n\t\tassertEquals(\"aaa_STRIPPED_\", CharSequenceUtil.stripAll(SOURCE_STRING, \"\", \"b\"));\n\t\tassertEquals(\"aaa_STRIPPED_\", CharSequenceUtil.stripAll(SOURCE_STRING, null, \"b\"));\n\t\tassertEquals(\"_STRIPPED_bbb\", CharSequenceUtil.stripAll(SOURCE_STRING, \"a\", \"\"));\n\t\tassertEquals(\"_STRIPPED_bbb\", CharSequenceUtil.stripAll(SOURCE_STRING, \"a\", null));\n\n\t\t// special test\n\t\tassertEquals(\"bbb\", CharSequenceUtil.stripAll(\"aaaaaabbb\", \"aaa\", null));\n\t\tassertEquals(\"abbb\", CharSequenceUtil.stripAll(\"aaaaaaabbb\", \"aa\", null));\n\n\t\t// aaaaaaaaa (9个a) 可以被看为 aaa_aaaa_aa\n\t\tassertEquals(\"\", CharSequenceUtil.stripAll(\"aaaaaaaaa\", \"aaa\", \"aa\"));\n\t\t// 第二次迭代后会出现 from 比 to 大的情况，原本代码是强行交换，但是回导致无法去除前后缀\n\t\tassertEquals(\"\", CharSequenceUtil.stripAll(\"a\", \"a\", \"a\"));\n\n\t\t// 前缀后缀有重叠，优先去掉前缀\n\t\tassertEquals(\"a\", CharSequenceUtil.stripAll(\"aba\", \"ab\", \"ba\"));\n\t\tassertEquals(\"a\", CharSequenceUtil.stripAll(\"abababa\", \"ab\", \"ba\"));\n\t}\n\n\t@Test\n\tpublic void moveTest() {\n\t\t//Case 1: \"12\"右移4位，回到原位置\n\t\tString result1 = CharSequenceUtil.move(\"12345\", 0, 2, 4);\n\t\tassertEquals(\"12345\", result1);\n\n\t\t//Case 2: \"12\"左移 1 位\n\t\tString result2 = CharSequenceUtil.move(\"12345\", 0, 2, -1);\n\t\tassertEquals(\"34512\", result2);\n\n\t\t//Case 3: \"12\"右移 1 位\n\t\tString result3 = CharSequenceUtil.move(\"12345\", 0, 2, 1);\n\t\tassertEquals(\"31245\", result3);\n\n\t\t//Case 4: \"12\"左移 2 位\n\t\tString result4 = CharSequenceUtil.move(\"12345\", 0, 2, -2);\n\t\tassertEquals(\"34125\", result4);\n\n\t\t//Case 5: \"12\"右移 5 位 (超周期 1 次)，等效于右移 1 位\n\t\tString result5 = CharSequenceUtil.move(\"12345\", 0, 2, 5);\n\t\tassertEquals(\"31245\", result5);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/IssueI96LWHTest.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI96LWHTest {\n\t@Test\n\tpublic void replaceTest() {\n\t\tString str = \"\\uD83D\\uDC46最上方点击蓝字\";\n\t\tAssertions.assertArrayEquals(new int[]{128070, 26368, 19978, 26041, 28857, 20987, 34013, 23383}, str.codePoints().toArray());\n\t\t// 这个方法里\\uD83D\\uDC46表示一个emoji表情，使用codePoint之后，一个表情表示一个字符，因此按照一个字符对\n\t\tAssertions.assertEquals(\"\\uD83D\\uDC46最上下点击蓝字\", StrUtil.replaceByCodePoint(str, 3, 4, \"下\"));\n\t\tAssertions.assertEquals(\"\\uD83D\\uDC46最下方点击蓝字\", new StringBuilder(str).replace(3, 4, \"下\").toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/NamingCaseTest.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.CharUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class NamingCaseTest {\n\n\t@Test\n\tpublic void toCamelCaseTest() {\n\t\tDict.create()\n\t\t\t\t.set(\"Table_Test_Of_day\",\"tableTestOfDay\")\n\t\t\t\t.set(\"TableTestOfDay\",\"TableTestOfDay\")\n\t\t\t\t.set(\"abc_1d\",\"abc1d\")\n\t\t\t\t.forEach((key, value) -> assertEquals(value, NamingCase.toCamelCase(key)));\n\t}\n\n\t@Test\n\tpublic void toCamelCaseFromDashedTest() {\n\t\tDict.create()\n\t\t\t\t.set(\"Table-Test-Of-day\",\"tableTestOfDay\")\n\t\t\t\t.forEach((key, value) -> assertEquals(value, NamingCase.toCamelCase(key, CharUtil.DASHED)));\n\t}\n\n\t@Test\n\tpublic void toUnderLineCaseTest() {\n\t\tDict.create()\n\t\t\t\t.set(\"Table_Test_Of_day\", \"table_test_of_day\")\n\t\t\t\t.set(\"_Table_Test_Of_day_\", \"_table_test_of_day_\")\n\t\t\t\t.set(\"_Table_Test_Of_DAY_\", \"_table_test_of_DAY_\")\n\t\t\t\t.set(\"_TableTestOfDAYToday\", \"_table_test_of_DAY_today\")\n\t\t\t\t.set(\"HelloWorld_test\", \"hello_world_test\")\n\t\t\t\t.set(\"H2\", \"H2\")\n\t\t\t\t.set(\"H#case\", \"H#case\")\n\t\t\t\t.set(\"PNLabel\", \"PN_label\")\n\t\t\t\t.set(\"wPRunOZTime\", \"w_P_run_OZ_time\")\n\t\t\t\t// https://github.com/chinabugotech/hutool/issues/2070\n\t\t\t\t.set(\"customerNickV2\", \"customer_nick_v2\")\n\t\t\t\t// https://gitee.com/chinabugotech/hutool/issues/I4X9TT\n\t\t\t\t.set(\"DEPT_NAME\",\"DEPT_NAME\")\n\t\t\t\t.forEach((key, value) -> assertEquals(value, NamingCase.toUnderlineCase(key)));\n\t}\n\n\t@Test\n\tpublic void issue3031Test() {\n\t\tString camelCase = NamingCase.toCamelCase(\"user_name,BIRTHDAY\");\n\t\tassertEquals(\"userName,birthday\", camelCase);\n\n\t\tcamelCase = NamingCase.toCamelCase(\"user_name,BIRTHDAY\", '_', false);\n\t\tassertEquals(\"userName,BIRTHDAY\", camelCase);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/PasswdStrengthTest.java",
    "content": "package cn.hutool.core.text;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class PasswdStrengthTest {\n\t@Test\n\tpublic void strengthTest(){\n\t\tString passwd = \"2hAj5#mne-ix.86H\";\n\t\tassertEquals(13, PasswdStrength.check(passwd));\n\t}\n\n\t@Test\n\tpublic void strengthNumberTest(){\n\t\tString passwd = \"9999999999999\";\n\t\tassertEquals(0, PasswdStrength.check(passwd));\n\t}\n\n\t@Test\n\tpublic void consecutiveLettersTest() {\n\t\t// 测试连续小写字母会被降级\n\t\tassertEquals(0, PasswdStrength.check(\"abcdefghijklmn\"));\n\t\t// 测试连续大写字母会被降级\n\t\tassertEquals(0, PasswdStrength.check(\"ABCDEFGHIJKLMN\"));\n\t}\n\n\t@Test\n\tpublic void dictionaryWeakPasswordTest() {\n\t\t// 测试包含简单密码字典中的弱密码\n\t\tassertEquals(0, PasswdStrength.check(\"password\"));\n\t\tassertEquals(3, PasswdStrength.check(\"password2\"));\n\t}\n\n\t@Test\n\tpublic void numericSequenceTest() {\n\t\tassertEquals(0, PasswdStrength.check(\"01234567890\"));\n\t\tassertEquals(0, PasswdStrength.check(\"09876543210\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/StrBuilderTest.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * StrBuilder单元测试\n * @author looly\n *\n */\npublic class StrBuilderTest {\n\n\t/**\n\t * StrBuilder的性能测试\n\t */\n\t@Test\n\t@Disabled\n\tpublic void benchTest() {\n\t\tTimeInterval timer = DateUtil.timer();\n\t\tStrBuilder builder = StrBuilder.create();\n\t\tfor(int i =0; i< 1000000; i++) {\n\t\t\tbuilder.append(\"test\");\n\t\t\tbuilder.reset();\n\t\t}\n\t\tConsole.log(timer.interval());\n\n\t\ttimer.restart();\n\t\tStringBuilder b2 = new StringBuilder();\n\t\tfor(int i =0; i< 1000000; i++) {\n\t\t\tb2.append(\"test\");\n\t\t\tb2 = new StringBuilder();\n\t\t}\n\t\tConsole.log(timer.interval());\n\t}\n\n\t@Test\n\tpublic void appendTest() {\n\t\tStrBuilder builder = StrBuilder.create();\n\t\tbuilder.append(\"aaa\").append(\"你好\").append('r');\n\t\tassertEquals(\"aaa你好r\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void insertTest() {\n\t\tStrBuilder builder = StrBuilder.create(1);\n\t\tbuilder.append(\"aaa\").append(\"你好\").append('r');\n\t\tbuilder.insert(3, \"数据插入\");\n\t\tassertEquals(\"aaa数据插入你好r\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void insertTest2() {\n\t\tStrBuilder builder = StrBuilder.create(1);\n\t\tbuilder.append(\"aaa\").append(\"你好\").append('r');\n\t\tbuilder.insert(8, \"数据插入\");\n\t\tassertEquals(\"aaa你好r  数据插入\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void resetTest() {\n\t\tStrBuilder builder = StrBuilder.create(1);\n\t\tbuilder.append(\"aaa\").append(\"你好\").append('r');\n\t\tbuilder.insert(3, \"数据插入\");\n\t\tbuilder.reset();\n\t\tassertEquals(\"\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void resetTest2() {\n\t\tStrBuilder builder = StrBuilder.create(1);\n\t\tbuilder.append(\"aaa\").append(\"你好\").append('r');\n\t\tbuilder.insert(3, \"数据插入\");\n\t\tbuilder.reset();\n\t\tbuilder.append(\"bbb\".toCharArray());\n\t\tassertEquals(\"bbb\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void appendObjectTest() {\n\t\tStrBuilder builder = StrBuilder.create(1);\n\t\tbuilder.append(123).append(456.123D).append(true).append('\\n');\n\t\tassertEquals(\"123456.123true\\n\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void delTest() {\n\t\t// 删除全部测试\n\t\tStrBuilder strBuilder = new StrBuilder(\"ABCDEFG\");\n\t\tint length = strBuilder.length();\n\t\tStrBuilder builder = strBuilder.del(0, length);\n\t\tassertEquals(\"\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void delTest2() {\n\t\t// 删除中间部分测试\n\t\tStrBuilder strBuilder = new StrBuilder(\"ABCDEFG\");\n\t\tStrBuilder builder = strBuilder.del(2,6);\n\t\tassertEquals(\"ABG\", builder.toString());\n\t}\n\n\t@Test\n\tpublic void delToTest() {\n\t\tStrBuilder strBuilder = new StrBuilder(\"ABCDEFG\");\n\n\t\t// 不处理\n\t\tStrBuilder builder = strBuilder.delTo(7);\n\t\tassertEquals(\"ABCDEFG\", builder.toString());\n\n\t\t// 删除全部\n\t\tbuilder = strBuilder.delTo(0);\n\t\tassertEquals(\"\", builder.toString());\n\t}\n\n\t@Test\n\tvoid issueICTSRZTest() {\n\t\tfinal StrBuilder helloWorld = StrBuilder.create(\"Hello World\");\n\t\thelloWorld.insert(6, \"Beautiful \", 0, 10);\n\t\tAssertions.assertEquals(\"Hello Beautiful World\", helloWorld.toString());\n\t}\n\n\t@Test\n\tvoid issueICTSRZTest2() {\n\t\tfinal StrBuilder helloWorld = StrBuilder.create(\"Hello World\");\n\t\thelloWorld.insert(6, \"Beautiful \");\n\t\tAssertions.assertEquals(\"Hello Beautiful World\", helloWorld.toString());\n\t}\n\n\t@Test\n\tvoid charAtTest() {\n\t\tfinal StrBuilder helloWorld = StrBuilder.create(\"Hello World\");\n\t\tAssertions.assertEquals('d', helloWorld.charAt(-1));\n\t\tAssertions.assertEquals('H', helloWorld.charAt(0));\n\t\tAssertions.assertEquals('d', helloWorld.charAt(10));\n\t\tAssertions.assertThrows(StringIndexOutOfBoundsException.class, () -> helloWorld.charAt(11));;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/StrJoinerTest.java",
    "content": "package cn.hutool.core.text;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StrJoinerTest {\n\n\t@Test\n\tpublic void joinIntArrayTest(){\n\t\tint[] a = {1,2,3,4,5};\n\t\tfinal StrJoiner append = StrJoiner.of(\",\").append(a);\n\t\tassertEquals(\"1,2,3,4,5\", append.toString());\n\t}\n\n\t@Test\n\tpublic void joinEmptyTest(){\n\t\tList<String> list = new ArrayList<>();\n\t\tfinal StrJoiner append = StrJoiner.of(\",\").append(list);\n\t\tassertEquals(\"\", append.toString());\n\t}\n\n\t@Test\n\tpublic void noJoinTest(){\n\t\tfinal StrJoiner append = StrJoiner.of(\",\");\n\t\tassertEquals(\"\", append.toString());\n\t}\n\n\t@Test\n\tpublic void joinMultiArrayTest(){\n\t\tfinal StrJoiner append = StrJoiner.of(\",\");\n\t\tappend.append(new Object[]{ListUtil.of(\"1\", \"2\"),\n\t\t\t\tCollUtil.newLinkedHashSet(\"3\", \"4\")\n\t\t});\n\t\tassertEquals(\"1,2,3,4\", append.toString());\n\t}\n\n\t@Test\n\tpublic void joinNullModeTest(){\n\t\tStrJoiner append = StrJoiner.of(\",\")\n\t\t\t\t.setNullMode(StrJoiner.NullMode.IGNORE)\n\t\t\t\t.append(\"1\")\n\t\t\t\t.append((Object)null)\n\t\t\t\t.append(\"3\");\n\t\tassertEquals(\"1,3\", append.toString());\n\n\t\tappend = StrJoiner.of(\",\")\n\t\t\t\t.setNullMode(StrJoiner.NullMode.TO_EMPTY)\n\t\t\t\t.append(\"1\")\n\t\t\t\t.append((Object)null)\n\t\t\t\t.append(\"3\");\n\t\tassertEquals(\"1,,3\", append.toString());\n\n\t\tappend = StrJoiner.of(\",\")\n\t\t\t\t.setNullMode(StrJoiner.NullMode.NULL_STRING)\n\t\t\t\t.append(\"1\")\n\t\t\t\t.append((Object)null)\n\t\t\t\t.append(\"3\");\n\t\tassertEquals(\"1,null,3\", append.toString());\n\t}\n\n\t@Test\n\tpublic void joinWrapTest(){\n\t\tStrJoiner append = StrJoiner.of(\",\", \"[\", \"]\")\n\t\t\t\t.append(\"1\")\n\t\t\t\t.append(\"2\")\n\t\t\t\t.append(\"3\");\n\t\tassertEquals(\"[1,2,3]\", append.toString());\n\n\t\tappend = StrJoiner.of(\",\", \"[\", \"]\")\n\t\t\t\t.setWrapElement(true)\n\t\t\t\t.append(\"1\")\n\t\t\t\t.append(\"2\")\n\t\t\t\t.append(\"3\");\n\t\tassertEquals(\"[1],[2],[3]\", append.toString());\n\t}\n\n\t@Test\n\tpublic void lengthTest(){\n\t\tStrJoiner joiner = StrJoiner.of(\",\", \"[\", \"]\");\n\t\tassertEquals(joiner.toString().length(), joiner.length());\n\n\t\tjoiner.append(\"123\");\n\t\tassertEquals(joiner.toString().length(), joiner.length());\n\t}\n\n\t@Test\n\tpublic void mergeTest(){\n\t\tStrJoiner joiner1 = StrJoiner.of(\",\", \"[\", \"]\");\n\t\tjoiner1.append(\"123\");\n\t\tStrJoiner joiner2 = StrJoiner.of(\",\", \"[\", \"]\");\n\t\tjoiner1.append(\"456\");\n\t\tjoiner1.append(\"789\");\n\n\t\tfinal StrJoiner merge = joiner1.merge(joiner2);\n\t\tassertEquals(\"[123,456,789]\", merge.toString());\n\t}\n\n\t@Test\n\tpublic void issue3444Test() {\n\t\tfinal StrJoiner strJoinerEmpty = StrJoiner.of(\",\");\n\t\tassertEquals(0, strJoinerEmpty.length());\n\n\t\tfinal StrJoiner strJoinerWithContent = StrJoiner.of(\",\").append(\"haha\");\n\t\tassertEquals(4, strJoinerWithContent.length());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/StrMatcherTest.java",
    "content": "package cn.hutool.core.text;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class StrMatcherTest {\n\n\t@Test\n\tpublic void matcherTest(){\n\t\tfinal StrMatcher strMatcher = new StrMatcher(\"${name}-${age}-${gender}-${country}-${province}-${city}-${status}\");\n\t\tfinal Map<String, String> match = strMatcher.match(\"小明-19-男-中国-河南-郑州-已婚\");\n\t\tassertEquals(\"小明\", match.get(\"name\"));\n\t\tassertEquals(\"19\", match.get(\"age\"));\n\t\tassertEquals(\"男\", match.get(\"gender\"));\n\t\tassertEquals(\"中国\", match.get(\"country\"));\n\t\tassertEquals(\"河南\", match.get(\"province\"));\n\t\tassertEquals(\"郑州\", match.get(\"city\"));\n\t\tassertEquals(\"已婚\", match.get(\"status\"));\n\t}\n\n\t@Test\n\tpublic void matcherTest2(){\n\t\t// 当有无匹配项的时候，按照全不匹配对待\n\t\tfinal StrMatcher strMatcher = new StrMatcher(\"${name}-${age}-${gender}-${country}-${province}-${city}-${status}\");\n\t\tfinal Map<String, String> match = strMatcher.match(\"小明-19-男-中国-河南-郑州\");\n\t\tassertEquals(0, match.size());\n\t}\n\n\t@Test\n\tpublic void matcherTest3(){\n\t\t// 当有无匹配项的时候，按照全不匹配对待\n\t\tfinal StrMatcher strMatcher = new StrMatcher(\"${name}经过${year}年\");\n\t\tfinal Map<String, String> match = strMatcher.match(\"小明经过20年，成长为一个大人。\");\n\t\t//Console.log(match);\n\t\tassertEquals(\"小明\", match.get(\"name\"));\n\t\tassertEquals(\"20\", match.get(\"year\"));\n\t}\n\n\t@Test\n\tpublic void issueIDFNF7Test() {\n\t\tStrMatcher strMatcher = new StrMatcher(\"${a}${b}\");\n\n\t\t//final Map<String, String> match = strMatcher.match(\"XY\");\n\t\t//Console.log(match); // 此时会输出：\"{b=XY}\"\n\n\t\tassertThrows(\n\t\t\tIllegalArgumentException.class,\n\t\t\t() -> strMatcher.match(\"XY\")\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/TextSimilarityTest.java",
    "content": "package cn.hutool.core.text;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 文本相似度计算工具类单元测试\n * @author looly\n *\n */\npublic class TextSimilarityTest {\n\n\t@Test\n\tpublic void similarDegreeTest() {\n\t\tString a = \"我是一个文本，独一无二的文本\";\n\t\tString b = \"一个文本，独一无二的文本\";\n\n\t\tdouble degree = TextSimilarity.similar(a, b);\n\t\tassertEquals(0.8461538462D, degree, 0.01);\n\n\t\tString similarPercent = TextSimilarity.similar(a, b, 2);\n\t\tassertEquals(\"84.62%\", similarPercent);\n\t}\n\n\t@Test\n\tpublic void similarDegreeTest2() {\n\t\tString a = \"我是一个文本，独一无二的文本\";\n\t\tString b = \"一个文本，独一无二的文本,#,>>?#$%^%$&^&^%\";\n\n\t\tdouble degree = TextSimilarity.similar(a, b);\n\t\tassertEquals(0.8461538462D, degree, 0.01);\n\n\t\tString similarPercent = TextSimilarity.similar(a, b, 2);\n\t\tassertEquals(\"84.62%\", similarPercent);\n\t}\n\n\t@Test\n\tpublic void similarTest(){\n\t\tfinal double abd = TextSimilarity.similar(\"abd\", \"1111\");\n\t\tassertEquals(0, abd, 0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/UnicodeUtilTest.java",
    "content": "package cn.hutool.core.text;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * UnicodeUtil 单元测试\n *\n * @author looly\n *\n */\npublic class UnicodeUtilTest {\n\t@Test\n\tpublic void convertTest() {\n\t\tString s = UnicodeUtil.toUnicode(\"aaa123中文\", true);\n\t\tassertEquals(\"aaa123\\\\u4e2d\\\\u6587\", s);\n\n\t\tString s1 = UnicodeUtil.toString(s);\n\t\tassertEquals(\"aaa123中文\", s1);\n\t}\n\n\t@Test\n\tpublic void convertTest2() {\n\t\tString str = \"aaaa\\\\u0026bbbb\\\\u0026cccc\";\n\t\tString unicode = UnicodeUtil.toString(str);\n\t\tassertEquals(\"aaaa&bbbb&cccc\", unicode);\n\t}\n\n\t@Test\n\tpublic void convertTest3() {\n\t\tString str = \"aaa\\\\u111\";\n\t\tString res = UnicodeUtil.toString(str);\n\t\tassertEquals(\"aaa\\\\u111\", res);\n\t}\n\n\t@Test\n\tpublic void convertTest4() {\n\t\tString str = \"aaa\\\\U4e2d\\\\u6587\\\\u111\\\\urtyu\\\\u0026\";\n\t\tString res = UnicodeUtil.toString(str);\n\t\tassertEquals(\"aaa中文\\\\u111\\\\urtyu&\", res);\n\t}\n\n\t@Test\n\tpublic void convertTest5() {\n\t\tString str = \"{\\\"code\\\":403,\\\"enmsg\\\":\\\"Product not found\\\",\\\"cnmsg\\\":\\\"\\\\u4ea7\\\\u54c1\\\\u4e0d\\\\u5b58\\\\u5728\\\\uff0c\\\\u6216\\\\u5df2\\\\u5220\\\\u9664\\\",\\\"data\\\":null}\";\n\t\tString res = UnicodeUtil.toString(str);\n\t\tassertEquals(\"{\\\"code\\\":403,\\\"enmsg\\\":\\\"Product not found\\\",\\\"cnmsg\\\":\\\"产品不存在，或已删除\\\",\\\"data\\\":null}\", res);\n\t}\n\n\t@Test\n\tpublic void issueI50MI6Test(){\n\t\tString s = UnicodeUtil.toUnicode(\"烟\", true);\n\t\tassertEquals(\"\\\\u70df\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/CsvParserTest.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.StringReader;\n\npublic class CsvParserTest {\n\n\t@Test\n\tpublic void parseTest1() {\n\t\tStringReader reader = StrUtil.getReader(\"aaa,b\\\"bba\\\",ccc\");\n\t\tCsvParser parser = new CsvParser(reader, null);\n\t\tCsvRow row = parser.nextRow();\n\t\t//noinspection ConstantConditions\n\t\tassertEquals(\"b\\\"bba\\\"\", row.getRawList().get(1));\n\t\tIoUtil.close(parser);\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tStringReader reader = StrUtil.getReader(\"aaa,\\\"bba\\\"bbb,ccc\");\n\t\tCsvParser parser = new CsvParser(reader, null);\n\t\tCsvRow row = parser.nextRow();\n\t\t//noinspection ConstantConditions\n\t\tassertEquals(\"\\\"bba\\\"bbb\", row.getRawList().get(1));\n\t\tIoUtil.close(parser);\n\t}\n\n\t@Test\n\tpublic void parseTest3() {\n\t\tStringReader reader = StrUtil.getReader(\"aaa,\\\"bba\\\",ccc\");\n\t\tCsvParser parser = new CsvParser(reader, null);\n\t\tCsvRow row = parser.nextRow();\n\t\t//noinspection ConstantConditions\n\t\tassertEquals(\"bba\", row.getRawList().get(1));\n\t\tIoUtil.close(parser);\n\t}\n\n\t@Test\n\tpublic void parseTest4() {\n\t\tStringReader reader = StrUtil.getReader(\"aaa,\\\"\\\",ccc\");\n\t\tCsvParser parser = new CsvParser(reader, null);\n\t\tCsvRow row = parser.nextRow();\n\t\t//noinspection ConstantConditions\n\t\tassertEquals(\"\", row.getRawList().get(1));\n\t\tIoUtil.close(parser);\n\t}\n\n\t@Test\n\tpublic void parseEscapeTest(){\n\t\t// https://datatracker.ietf.org/doc/html/rfc4180#section-2\n\t\t// 第七条规则\n\t\tStringReader reader = StrUtil.getReader(\"\\\"b\\\"\\\"bb\\\"\");\n\t\tCsvParser parser = new CsvParser(reader, null);\n\t\tCsvRow row = parser.nextRow();\n\t\tassertNotNull(row);\n\t\tassertEquals(1, row.size());\n\t\tassertEquals(\"b\\\"bb\", row.get(0));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/CsvReaderTest.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Disabled;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class CsvReaderTest {\n\n\t@Test\n\tpublic void readTest() {\n\t\tCsvReader reader = new CsvReader();\n\t\tCsvData data = reader.read(ResourceUtil.getReader(\"test.csv\", CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"sss,sss\", data.getRow(0).get(0));\n\t\tassertEquals(1, data.getRow(0).getOriginalLineNumber());\n\t\tassertEquals(\"性别\", data.getRow(0).get(2));\n\t\tassertEquals(\"关注\\\"对象\\\"\", data.getRow(0).get(3));\n\t}\n\n\t@Test\n\tpublic void readMapListTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader();\n\t\tfinal List<Map<String, String>> result = reader.readMapList(\n\t\t\t\tResourceUtil.getUtf8Reader(\"test_bean.csv\"));\n\n\t\tassertEquals(\"张三\", result.get(0).get(\"姓名\"));\n\t\tassertEquals(\"男\", result.get(0).get(\"gender\"));\n\t\tassertEquals(\"无\", result.get(0).get(\"focus\"));\n\t\tassertEquals(\"33\", result.get(0).get(\"age\"));\n\n\t\tassertEquals(\"李四\", result.get(1).get(\"姓名\"));\n\t\tassertEquals(\"男\", result.get(1).get(\"gender\"));\n\t\tassertEquals(\"好对象\", result.get(1).get(\"focus\"));\n\t\tassertEquals(\"23\", result.get(1).get(\"age\"));\n\n\t\tassertEquals(\"王妹妹\", result.get(2).get(\"姓名\"));\n\t\tassertEquals(\"女\", result.get(2).get(\"gender\"));\n\t\tassertEquals(\"特别关注\", result.get(2).get(\"focus\"));\n\t\tassertEquals(\"22\", result.get(2).get(\"age\"));\n\t}\n\n\t@Test\n\tpublic void readAliasMapListTest() {\n\t\tfinal CsvReadConfig csvReadConfig = CsvReadConfig.defaultConfig();\n\t\tcsvReadConfig.addHeaderAlias(\"姓名\", \"name\");\n\n\t\tfinal CsvReader reader = CsvUtil.getReader(csvReadConfig);\n\t\tfinal List<Map<String, String>> result = reader.readMapList(\n\t\t\t\tResourceUtil.getUtf8Reader(\"test_bean.csv\"));\n\n\t\tassertEquals(\"张三\", result.get(0).get(\"name\"));\n\t\tassertEquals(\"男\", result.get(0).get(\"gender\"));\n\t\tassertEquals(\"无\", result.get(0).get(\"focus\"));\n\t\tassertEquals(\"33\", result.get(0).get(\"age\"));\n\n\t\tassertEquals(\"李四\", result.get(1).get(\"name\"));\n\t\tassertEquals(\"男\", result.get(1).get(\"gender\"));\n\t\tassertEquals(\"好对象\", result.get(1).get(\"focus\"));\n\t\tassertEquals(\"23\", result.get(1).get(\"age\"));\n\n\t\tassertEquals(\"王妹妹\", result.get(2).get(\"name\"));\n\t\tassertEquals(\"女\", result.get(2).get(\"gender\"));\n\t\tassertEquals(\"特别关注\", result.get(2).get(\"focus\"));\n\t\tassertEquals(\"22\", result.get(2).get(\"age\"));\n\t}\n\n\t@Test\n\tpublic void readBeanListTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader();\n\t\tfinal List<TestBean> result = reader.read(\n\t\t\t\tResourceUtil.getUtf8Reader(\"test_bean.csv\"), TestBean.class);\n\n\t\tassertEquals(\"张三\", result.get(0).getName());\n\t\tassertEquals(\"男\", result.get(0).getGender());\n\t\tassertEquals(\"无\", result.get(0).getFocus());\n\t\tassertEquals(Integer.valueOf(33), result.get(0).getAge());\n\n\t\tassertEquals(\"李四\", result.get(1).getName());\n\t\tassertEquals(\"男\", result.get(1).getGender());\n\t\tassertEquals(\"好对象\", result.get(1).getFocus());\n\t\tassertEquals(Integer.valueOf(23), result.get(1).getAge());\n\n\t\tassertEquals(\"王妹妹\", result.get(2).getName());\n\t\tassertEquals(\"女\", result.get(2).getGender());\n\t\tassertEquals(\"特别关注\", result.get(2).getFocus());\n\t\tassertEquals(Integer.valueOf(22), result.get(2).getAge());\n\t}\n\n\t@Data\n\tprivate static class TestBean {\n\t\t@Alias(\"姓名\")\n\t\tprivate String name;\n\t\tprivate String gender;\n\t\tprivate String focus;\n\t\tprivate Integer age;\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readTest2() {\n\t\tfinal CsvReader reader = CsvUtil.getReader();\n\t\tfinal CsvData read = reader.read(FileUtil.file(\"d:/test/test.csv\"));\n\t\tfor (CsvRow strings : read) {\n\t\t\tConsole.log(strings);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readTest3() {\n\t\tfinal CsvReadConfig csvReadConfig = CsvReadConfig.defaultConfig();\n\t\tcsvReadConfig.setContainsHeader(true);\n\t\tfinal CsvReader reader = CsvUtil.getReader(csvReadConfig);\n\t\tfinal CsvData read = reader.read(FileUtil.file(\"d:/test/ceshi.csv\"));\n\t\tfor (CsvRow row : read) {\n\t\t\tConsole.log(row.getByName(\"案件ID\"));\n\t\t}\n\t}\n\n\t@Test\n\tpublic void lineNoTest() {\n\t\tCsvReader reader = new CsvReader();\n\t\tCsvData data = reader.read(ResourceUtil.getReader(\"test_lines.csv\", CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(1, data.getRow(0).getOriginalLineNumber());\n\t\tassertEquals(\"a,b,c,d\", CollUtil.join(data.getRow(0), \",\"));\n\n\t\tassertEquals(4, data.getRow(2).getOriginalLineNumber());\n\t\tassertEquals(\"q,w,e,r,我是一段\\n带换行的内容\",\n\t\t\t\tCollUtil.join(data.getRow(2), \",\").replace(\"\\r\", \"\"));\n\n\t\t// 文件中第3行数据，对应原始行号是6（从0开始）\n\t\tassertEquals(6, data.getRow(3).getOriginalLineNumber());\n\t\tassertEquals(\"a,s,d,f\", CollUtil.join(data.getRow(3), \",\"));\n\t}\n\n\t@Test\n\tpublic void lineLimitTest() {\n\t\t// 从原始第2行开始读取\n\t\tCsvReader reader = new CsvReader(CsvReadConfig.defaultConfig().setBeginLineNo(2));\n\t\tCsvData data = reader.read(ResourceUtil.getReader(\"test_lines.csv\", CharsetUtil.CHARSET_UTF_8));\n\n\t\tassertEquals(2, data.getRow(0).getOriginalLineNumber());\n\t\tassertEquals(\"1,2,3,4\", CollUtil.join(data.getRow(0), \",\"));\n\n\t\tassertEquals(4, data.getRow(1).getOriginalLineNumber());\n\t\tassertEquals(\"q,w,e,r,我是一段\\n带换行的内容\",\n\t\t\t\tCollUtil.join(data.getRow(1), \",\").replace(\"\\r\", \"\"));\n\n\t\t// 文件中第3行数据，对应原始行号是6（从0开始）\n\t\tassertEquals(6, data.getRow(2).getOriginalLineNumber());\n\t\tassertEquals(\"a,s,d,f\", CollUtil.join(data.getRow(2), \",\"));\n\t}\n\n\t@Test\n\tpublic void lineLimitWithHeaderTest() {\n\t\t// 从原始第2行开始读取\n\t\tCsvReader reader = new CsvReader(CsvReadConfig.defaultConfig().setBeginLineNo(2).setContainsHeader(true));\n\t\tCsvData data = reader.read(ResourceUtil.getReader(\"test_lines.csv\", CharsetUtil.CHARSET_UTF_8));\n\n\t\tassertEquals(4, data.getRow(0).getOriginalLineNumber());\n\t\tassertEquals(\"q,w,e,r,我是一段\\n带换行的内容\",\n\t\t\t\tCollUtil.join(data.getRow(0), \",\").replace(\"\\r\", \"\"));\n\n\t\t// 文件中第3行数据，对应原始行号是6（从0开始）\n\t\tassertEquals(6, data.getRow(1).getOriginalLineNumber());\n\t\tassertEquals(\"a,s,d,f\", CollUtil.join(data.getRow(1), \",\"));\n\t}\n\n\t@Test\n\tpublic void customConfigTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader(\n\t\t\t\tCsvReadConfig.defaultConfig()\n\t\t\t\t\t\t.setTextDelimiter('\\'')\n\t\t\t\t\t\t.setFieldSeparator(';'));\n\t\tfinal CsvData csvRows = reader.readFromStr(\"123;456;'789;0'abc;\");\n\t\tfinal CsvRow row = csvRows.getRow(0);\n\t\tassertEquals(\"123\", row.get(0));\n\t\tassertEquals(\"456\", row.get(1));\n\t\tassertEquals(\"'789;0'abc\", row.get(2));\n\t}\n\n\t@Test\n\tpublic void readDisableCommentTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader(CsvReadConfig.defaultConfig().disableComment());\n\t\tfinal CsvData read = reader.read(ResourceUtil.getUtf8Reader(\"test.csv\"));\n\t\tfinal CsvRow row = read.getRow(0);\n\t\tassertEquals(\"# 这是一行注释，读取时应忽略\", row.get(0));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void streamTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader(ResourceUtil.getUtf8Reader(\"test_bean.csv\"));\n\t\treader.stream().limit(2).forEach(Console::log);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/CsvUtilTest.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CsvUtilTest {\n\n\t@Test\n\tpublic void readTest() {\n\t\tCsvReader reader = CsvUtil.getReader();\n\t\t//从文件中读取CSV数据\n\t\tCsvData data = reader.read(FileUtil.file(\"test.csv\"));\n\t\tList<CsvRow> rows = data.getRows();\n\t\tfinal CsvRow row0 = rows.get(0);\n\t\tassertEquals(\"sss,sss\", row0.get(0));\n\t\tassertEquals(\"姓名\", row0.get(1));\n\t\tassertEquals(\"性别\", row0.get(2));\n\t\tassertEquals(\"关注\\\"对象\\\"\", row0.get(3));\n\t\tassertEquals(\"年龄\", row0.get(4));\n\t\tassertEquals(\"\", row0.get(5));\n\t\t// 由于\"\"\"未闭合包装，因此末尾的换行符被当作包装内的内容，相当于：\"\"\"\\n\"，转义后就是\"\\n\n\t\tassertEquals(\"\\\"\\n\", row0.get(6));\n\t}\n\n\t@Test\n\tpublic void readTest2() {\n\t\tCsvReader reader = CsvUtil.getReader();\n\t\treader.read(FileUtil.getUtf8Reader(\"test.csv\"), (csvRow)-> {\n\t\t\t// 只有一行，所以直接判断\n\t\t\tassertEquals(\"sss,sss\", csvRow.get(0));\n\t\t\tassertEquals(\"姓名\", csvRow.get(1));\n\t\t\tassertEquals(\"性别\", csvRow.get(2));\n\t\t\tassertEquals(\"关注\\\"对象\\\"\", csvRow.get(3));\n\t\t\tassertEquals(\"年龄\", csvRow.get(4));\n\t\t\tassertEquals(\"\", csvRow.get(5));\n\t\t\t// 由于\"\"\"未闭合包装，因此末尾的换行符被当作包装内的内容，相当于：\"\"\"\\n\"，转义后就是\"\\n\n\t\t\tassertEquals(\"\\\"\\n\", csvRow.get(6));\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readTest3() {\n\t\tCsvReader reader = CsvUtil.getReader();\n\t\tString path = FileUtil.isWindows() ? \"d:/test/test.csv\" : \"~/test/test.csv\";\n\t\treader.read(FileUtil.getUtf8Reader(path), Console::log);\n\t}\n\n\t@Test\n\tpublic void readCsvStr1(){\n\t\tCsvData data = CsvUtil.getReader().readFromStr(\"# 这是一行注释，读取时应忽略\\n\" +\n\t\t\t\t\"\\\"sss,sss\\\",姓名,\\\"性别\\\",关注\\\"对象\\\",年龄,\\\"\\\",\\\"\\\"\\\"\\n\");\n\t\tList<CsvRow> rows = data.getRows();\n\t\tfinal CsvRow row0 = rows.get(0);\n\t\tassertEquals(\"sss,sss\", row0.get(0));\n\t\tassertEquals(\"姓名\", row0.get(1));\n\t\tassertEquals(\"性别\", row0.get(2));\n\t\tassertEquals(\"关注\\\"对象\\\"\", row0.get(3));\n\t\tassertEquals(\"年龄\", row0.get(4));\n\t\tassertEquals(\"\", row0.get(5));\n\t\t// 由于\"\"\"未闭合包装，因此末尾的换行符被当作包装内的内容，相当于：\"\"\"\\n\"，转义后就是\"\\n\n\t\tassertEquals(\"\\\"\\n\", row0.get(6));\n\t}\n\n\t@Test\n\tpublic void readCsvStr2(){\n\t\tCsvUtil.getReader().readFromStr(\"# 这是一行注释，读取时应忽略\\n\" +\n\t\t\t\t\"\\\"sss,sss\\\",姓名,\\\"性别\\\",关注\\\"对象\\\",年龄,\\\"\\\",\\\"\\\"\\\"\\n\",(csvRow)-> {\n\t\t\t// 只有一行，所以直接判断\n\t\t\tassertEquals(\"sss,sss\", csvRow.get(0));\n\t\t\tassertEquals(\"姓名\", csvRow.get(1));\n\t\t\tassertEquals(\"性别\", csvRow.get(2));\n\t\t\tassertEquals(\"关注\\\"对象\\\"\", csvRow.get(3));\n\t\t\tassertEquals(\"年龄\", csvRow.get(4));\n\t\t\tassertEquals(\"\", csvRow.get(5));\n\t\t\t// 由于\"\"\"未闭合包装，因此末尾的换行符被当作包装内的内容，相当于：\"\"\"\\n\"，转义后就是\"\\n\n\t\t\tassertEquals(\"\\\"\\n\", csvRow.get(6));\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tString path = FileUtil.isWindows() ? \"d:/test/testWrite.csv\" : \"~/test/testWrite.csv\";\n\t\tCsvWriter writer = CsvUtil.getWriter(path, CharsetUtil.CHARSET_UTF_8);\n\t\twriter.write(\n\t\t\t\tnew String[] {\"a1\", \"b1\", \"c1\", \"123345346456745756756785656\"},\n\t\t\t\tnew String[] {\"a2\", \"b2\", \"c2\"},\n\t\t\t\tnew String[] {\"a3\", \"b3\", \"c3\"}\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeBeansTest() {\n\n\t\t@Data\n\t\tclass Student {\n\t\t\tInteger id;\n\t\t\tString name;\n\t\t\tInteger age;\n\t\t}\n\n\t\tString path = FileUtil.isWindows() ? \"d:/test/testWriteBeans.csv\" : \"~/test/testWriteBeans.csv\";\n\t\tCsvWriter writer = CsvUtil.getWriter(path, CharsetUtil.CHARSET_UTF_8);\n\t\tList<Student> students = new ArrayList<>();\n\t\tStudent student1 = new Student();\n\t\tstudent1.setId(1);\n\t\tstudent1.setName(\"张三\");\n\t\tstudent1.setAge(18);\n\n\t\tStudent student2 = new Student();\n\t\tstudent2.setId(2);\n\t\tstudent2.setName(\"李四\");\n\t\tstudent2.setAge(22);\n\n\t\tStudent student3 = new Student();\n\t\tstudent3.setId(3);\n\t\tstudent3.setName(\"王五\");\n\t\tstudent3.setAge(31);\n\n\t\tstudents.add(student1);\n\t\tstudents.add(student2);\n\t\tstudents.add(student3);\n\t\twriter.writeBeans(students);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeBeansWithPropertiesTest() {\n\n\t\t@Data\n\t\tclass Student {\n\t\t\tInteger id;\n\t\t\tString name;\n\t\t\tInteger age;\n\t\t}\n\n\t\tString path = FileUtil.isWindows() ? \"d:/test/testWriteBeans.csv\" : \"~/tmp/testWriteBeans.csv\";\n\t\tCsvWriter writer = CsvUtil.getWriter(path, CharsetUtil.CHARSET_UTF_8);\n\t\tList<Student> students = new ArrayList<>();\n\t\tStudent student1 = new Student();\n\t\tstudent1.setId(1);\n\t\tstudent1.setName(\"张三\");\n\t\tstudent1.setAge(18);\n\n\t\tStudent student2 = new Student();\n\t\tstudent2.setId(2);\n\t\tstudent2.setName(\"李四\");\n\t\tstudent2.setAge(22);\n\n\t\tStudent student3 = new Student();\n\t\tstudent3.setId(3);\n\t\tstudent3.setName(\"王五\");\n\t\tstudent3.setAge(31);\n\n\t\tstudents.add(student1);\n\t\tstudents.add(student2);\n\t\tstudents.add(student3);\n\t\twriter.writeBeans(students,\"name\",\"age\");\n\t\twriter.close();\n\t}\n\t@Test\n\t@Disabled\n\tpublic void readLfTest(){\n\t\tfinal CsvReader reader = CsvUtil.getReader();\n\t\tString path = FileUtil.isWindows() ? \"d:/test/rw_test.csv\" : \"~/test/rw_test.csv\";\n\t\tfinal CsvData read = reader.read(FileUtil.file(path));\n\t\tfor (CsvRow row : read) {\n\t\t\tConsole.log(row);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeWrapTest(){\n\t\tList<List<Object>> resultList=new ArrayList<>();\n\t\tList<Object> list =new ArrayList<>();\n\t\tlist.add(\"\\\"name\\\"\");\n\t\tlist.add(\"\\\"code\\\"\");\n\t\tresultList.add(list);\n\n\t\tlist =new ArrayList<>();\n\t\tlist.add(\"\\\"wang\\\"\");\n\t\tlist.add(1);\n\t\tresultList.add(list);\n\n\t\tString path = FileUtil.isWindows() ? \"d:/test/csvWrapTest.csv\" : \"~/test/csvWrapTest.csv\";\n\t\tfinal CsvWriter writer = CsvUtil.getWriter(path, CharsetUtil.CHARSET_UTF_8);\n\t\twriter.write(resultList);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeDataTest(){\n\t\t@Data\n\t\t@AllArgsConstructor\n\t\tclass User {\n\t\t\tInteger userId;\n\t\t\tString username;\n\t\t\tString mobile;\n\t\t}\n\n\t\tList<String> header = ListUtil.of(\"用户id\", \"用户名\", \"手机号\");\n\t\tList<CsvRow> row = new ArrayList<>();\n\n\t\tList<User> datas = new ArrayList<>();\n\t\tdatas.add(new User(1, \"张三\", \"18800001111\"));\n\t\tdatas.add(new User(2, \"李四\", \"18800001112\"));\n\t\tdatas.add(new User(3, \"王五\", \"18800001113\"));\n\t\tdatas.add(new User(4, \"赵六\", \"18800001114\"));\n\n\t\t//可以为null\n\t\t//Map<String, Integer> headMap = null;\n\t\tMap<String, Integer> headMap = new HashMap<>();\n\t\theadMap.put(\"userId\", 0);\n\t\theadMap.put(\"username\", 1);\n\t\theadMap.put(\"mobile\", 2);\n\n\t\tfor (User user : datas) {\n\t\t\t// row.size() + 1, 表示从第2行开始，第一行是标题栏\n\t\t\trow.add(new CsvRow(row.size() + 1, headMap,\n\t\t\t\t\tBeanUtil.beanToMap(user).values().stream().map(Object::toString).collect(Collectors.toList())));\n\t\t}\n\n\t\tCsvData csvData = new CsvData(header, row);\n\t\tString path = FileUtil.isWindows() ? \"d:/test/csvWriteDataTest.csv\" : \"~/test/csvWriteDataTest.csv\";\n\t\tfinal CsvWriter writer = CsvUtil.getWriter(path, CharsetUtil.CHARSET_UTF_8);\n\t\twriter.write(csvData);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/CsvWriterTest.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Random;\n\npublic class CsvWriterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeWithAliasTest(){\n\t\tfinal CsvWriteConfig csvWriteConfig = CsvWriteConfig.defaultConfig()\n\t\t\t\t.addHeaderAlias(\"name\", \"姓名\")\n\t\t\t\t.addHeaderAlias(\"gender\", \"性别\");\n\n\t\tfinal CsvWriter writer = CsvUtil.getWriter(\n\t\t\t\tFileUtil.file(\"d:/test/csvAliasTest.csv\"),\n\t\t\t\tCharsetUtil.CHARSET_GBK, false, csvWriteConfig);\n\n\t\twriter.writeHeaderLine(\"name\", \"gender\", \"address\");\n\t\twriter.writeLine(\"张三\", \"男\", \"XX市XX区\");\n\t\twriter.writeLine(\"李四\", \"男\", \"XX市XX区,01号\");\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issue2255Test(){\n\t\tString fileName = \"D:/test/\" + new Random().nextInt(100) + \"-a.csv\";\n\t\tCsvWriter writer = CsvUtil.getWriter(fileName, CharsetUtil.CHARSET_UTF_8);\n\t\tList<String> list = new ArrayList<>();\n\t\tfor (int i = 0; i < 10000; i++) {\n\t\t\tlist.add(i+\"\");\n\t\t}\n\t\tConsole.log(\"{} : {}\", fileName, list.size());\n\t\tfor (String s : list) {\n\t\t\twriter.writeLine(s);\n\t\t}\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeAppendTest(){\n\t\tfinal CsvWriter writer = CsvUtil.getWriter(\n\t\t\t\tFileUtil.file(\"d:/test/writeAppendTest.csv\"),\n\t\t\t\tCharsetUtil.CHARSET_GBK, true);\n\n\t\twriter.writeHeaderLine(\"name\", \"gender\", \"address\");\n\t\twriter.writeLine(\"张三\", \"男\", \"XX市XX区\");\n\t\twriter.writeLine(\"李四\", \"男\", \"XX市XX区,01号\");\n\n\t\twriter.writeLine(\"张三2\", \"男\", \"XX市XX区\");\n\t\twriter.writeLine(\"李四2\", \"男\", \"XX市XX区,01号\");\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/Issue3705Test.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.StringWriter;\n\npublic class Issue3705Test {\n\t@Test\n\tvoid writeTest() {\n\t\tfinal StringWriter stringWriter = new StringWriter();\n\n\t\tCsvWriteConfig csvWriteConfig = new CsvWriteConfig();\n\t\ttry (CsvWriter csvWriter = new CsvWriter(stringWriter, csvWriteConfig)) {\n\t\t\t// 由于一行的第一个字段中有逗号，因此需要使用双引号包围，否则会被当成分隔符\n\t\t\tcsvWriter.writeLine(\"2024-08-20 14:24:35,\");\n\t\t\tcsvWriter.writeLine(\"最后一行\");\n\t\t\tcsvWriter.flush();\n\t\t}\n\n\t\t// CsvWriteConfig中默认为`\\r\\n`\n\t\tAssertions.assertEquals(\"\\\"2024-08-20 14:24:35,\\\"\\r\\n最后一行\", stringWriter.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/IssueI91VF1Test.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.io.FileUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class IssueI91VF1Test {\n\t@Test\n\tpublic void csvReadTest() {\n\t\tfinal CsvReader reader = CsvUtil.getReader();\n\t\tfinal List<DeviceVO> read = reader.read(FileUtil.getUtf8Reader(\"issueI91VF1.csv\"), DeviceVO.class);\n\t\tfinal DeviceVO deviceVO = read.get(0);\n\t\tassertEquals(\"192.168.1.1\", deviceVO.getDeviceIp());\n\t\tassertEquals(\"admin\", deviceVO.getUsername());\n\t\tassertEquals(\"123\", deviceVO.getPassword());\n\t}\n\n\t@Data\n\tstatic class DeviceVO{\n\t\t@Alias(\"主机\")\n\t\tprivate String deviceIp;\n\t\t@Alias(\"用户名\")\n\t\tprivate String username;\n\t\t@Alias(\"密码\")\n\t\tprivate String password;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/IssueIA8WE0Test.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/IA8WE0\n */\npublic class IssueIA8WE0Test {\n\t@Test\n\tpublic void csvReadTest() {\n\t\tfinal CsvReader csvReader = new CsvReader();\n\t\tfinal CsvData read = csvReader.read(FileUtil.file(\"issueIA8WE0.csv\"));\n\t\tfinal List<CsvRow> rows = read.getRows();\n\n\t\tassertEquals(1, rows.size());\n\t\tassertEquals(3, rows.get(0).size());\n\t\tassertEquals(\"c1_text1\", rows.get(0).get(0));\n\t\t// 如果\\n#出现在双引号中，表示实际的文本内容，并不算注释\n\t\tassertEquals(\"c1_text2\\n#c1_text2_line2\", rows.get(0).get(1));\n\t\tassertEquals(\"c1_text3\", rows.get(0).get(2));\n\n\t\tIoUtil.close(csvReader);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/IssueIB5UQ8Test.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.StringReader;\n\npublic class IssueIB5UQ8Test {\n\t@Test\n\tvoid parseEscapeTest() {\n\t\tString csv = \"\\\"Consultancy, 10\\\"\\\",, food\\\"\";\n\t\tfinal CsvReader reader = CsvUtil.getReader(new StringReader(csv));\n\t\tfinal String s = reader.read().getRow(0).get(0);\n\t\tConsole.log(s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/IssueICRMKATest.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueICRMKATest {\n\t@Test\n\tpublic void issueICRMAKTest() {\n\t\tCsvReader reader = CsvUtil.getReader();\n\t\tCsvData data = reader.read(ResourceUtil.getUtf8Reader(\"issueICRMKA.csv\"));\n\t\tfinal CsvRow row = data.getRow(1);\n\t\tAssertions.assertEquals(\"6.3\\\" Google Pixel 9 Pro 128 GB Beige\", row.get(0));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/csv/Pr1244Test.java",
    "content": "package cn.hutool.core.text.csv;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.StringReader;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 按照 https://datatracker.ietf.org/doc/html/rfc4180#section-2<br>\n * 如果字段正文中出现双引号，需要使用两个双引号表示转义\n */\npublic class Pr1244Test {\n\t@Test\n\tpublic void csvReadTest() {\n\t\tfinal String csv = \"a,q\\\"e,d,f\";\n\t\tfinal CsvReader reader = CsvUtil.getReader(new StringReader(csv));\n\t\tfinal CsvData read = reader.read();\n\t\tassertEquals(4, read.getRow(0).size());\n\t\tassertEquals(\"a\", read.getRow(0).get(0));\n\t\tassertEquals(\"q\\\"e\", read.getRow(0).get(1));\n\t\tassertEquals(\"d\", read.getRow(0).get(2));\n\t\tassertEquals(\"f\", read.getRow(0).get(3));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/finder/CharFinderTest.java",
    "content": "package cn.hutool.core.text.finder;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CharFinderTest {\n\n\t@Test\n\tpublic void startTest(){\n\t\tint start = new CharFinder('a').setText(\"cba123\").start(2);\n\t\tassertEquals(2, start);\n\n\t\tstart = new CharFinder('c').setText(\"cba123\").start(2);\n\t\tassertEquals(-1, start);\n\n\t\tstart = new CharFinder('3').setText(\"cba123\").start(2);\n\t\tassertEquals(5, start);\n\t}\n\t@Test\n\tpublic void negativeStartTest(){\n\t\tint start = new CharFinder('a').setText(\"cba123\").setNegative(true).start(2);\n\t\tassertEquals(2, start);\n\n\t\tstart = new CharFinder('2').setText(\"cba123\").setNegative(true).start(2);\n\t\tassertEquals(-1, start);\n\n\t\tstart = new CharFinder('c').setText(\"cba123\").setNegative(true).start(2);\n\t\tassertEquals(0, start);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/split/SplitIterTest.java",
    "content": "package cn.hutool.core.text.split;\n\nimport cn.hutool.core.text.finder.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Pattern;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class SplitIterTest {\n\n\t@Test\n\tpublic void splitByCharTest(){\n\t\tString str1 = \"a, ,,efedsfs,   ddf,\";\n\n\t\t//不忽略\"\"\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew CharFinder(',', false),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\tfalse\n\t\t);\n\t\tassertEquals(6, splitIter.toList(false).size());\n\t}\n\n\t@Test\n\tpublic void splitByCharIgnoreCaseTest(){\n\t\tString str1 = \"a, ,,eAedsas,   ddf,\";\n\n\t\t//不忽略\"\"\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew CharFinder('a', true),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\tfalse\n\t\t);\n\t\tassertEquals(4, splitIter.toList(false).size());\n\t}\n\n\t@Test\n\tpublic void splitByCharIgnoreEmptyTest(){\n\t\tString str1 = \"a, ,,efedsfs,   ddf,\";\n\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew CharFinder(',', false),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\ttrue\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(4, strings.size());\n\t}\n\n\t@Test\n\tpublic void splitByCharTrimTest(){\n\t\tString str1 = \"a, ,,efedsfs,   ddf,\";\n\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew CharFinder(',', false),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\ttrue\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(true);\n\t\tassertEquals(3, strings.size());\n\t\tassertEquals(\"a\", strings.get(0));\n\t\tassertEquals(\"efedsfs\", strings.get(1));\n\t\tassertEquals(\"ddf\", strings.get(2));\n\t}\n\n\t@Test\n\tpublic void splitByStrTest(){\n\t\tString str1 = \"a, ,,efedsfs,   ddf,\";\n\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew StrFinder(\"e\", false),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\ttrue\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(3, strings.size());\n\t}\n\n\t@Test\n\tpublic void splitByPatternTest(){\n\t\tString str1 = \"a, ,,efedsfs,   ddf,\";\n\n\t\tSplitIter splitIter = new SplitIter(str1,\n\t\t\t\tnew PatternFinder(Pattern.compile(\"\\\\s\")),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\ttrue\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(3, strings.size());\n\t}\n\n\t@Test\n\tpublic void splitByLengthTest(){\n\t\tString text = \"1234123412341234\";\n\t\tSplitIter splitIter = new SplitIter(text,\n\t\t\t\tnew LengthFinder(4),\n\t\t\t\tInteger.MAX_VALUE,\n\t\t\t\tfalse\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(4, strings.size());\n\t}\n\n\t@Test\n\tpublic void splitLimitTest(){\n\t\tString text = \"55:02:18\";\n\t\tSplitIter splitIter = new SplitIter(text,\n\t\t\t\tnew CharFinder(':'),\n\t\t\t\t3,\n\t\t\t\tfalse\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(3, strings.size());\n\t}\n\n\t@Test\n\tpublic void splitToSingleTest(){\n\t\tString text = \"\";\n\t\tSplitIter splitIter = new SplitIter(text,\n\t\t\t\tnew CharFinder(':'),\n\t\t\t\t3,\n\t\t\t\tfalse\n\t\t);\n\n\t\tfinal List<String> strings = splitIter.toList(false);\n\t\tassertEquals(1, strings.size());\n\t}\n\n\t// 切割字符串是空字符串时报错\n\t@Test\n\tpublic void splitByEmptyTest(){\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tString text = \"aa,bb,cc\";\n\t\t\tSplitIter splitIter = new SplitIter(text,\n\t\t\t\tnew StrFinder(\"\", false),\n\t\t\t\t3,\n\t\t\t\tfalse\n\t\t\t);\n\n\t\t\tfinal List<String> strings = splitIter.toList(false);\n\t\t\tassertEquals(1, strings.size());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issue4169Test() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < 20000; i++) { // 1万次连续分隔符，模拟递归深度风险场景\n\t\t\tsb.append(\",\");\n\t\t}\n\t\tsb.append(\"test\");\n\n\t\tSplitIter iter = new SplitIter(sb.toString(), new StrFinder(\",\",false), 0, true);\n\t\tList<String> result = iter.toList(false);\n\n\t\tassertEquals(Collections.singletonList(\"test\"), result);\n\t}\n\n\t@Test\n\tpublic void issueIDFN7YTest() {\n\t\tfinal String text = \"a,b,c\";\n\t\tfinal TextFinder finder = new StrFinder(\",\", false);\n\t\tfinal SplitIter splitIter = new SplitIter(text, finder, 0, false);\n\n\t\tList<String> firstResult = splitIter.toList(false);\n\t\tassertEquals(3, firstResult.size());\n\n\t\tsplitIter.reset();\n\t\tList<String> secondResult = splitIter.toList(false);\n\t\tassertEquals(3, secondResult.size());\n\t\tassertEquals(firstResult, secondResult);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/text/split/StrSplitterTest.java",
    "content": "package cn.hutool.core.text.split;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.text.StrSplitter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * {@link StrSplitter} 单元测试\n * @author Looly\n *\n */\npublic class StrSplitterTest {\n\n\t@Test\n\tpublic void splitByCharTest(){\n\t\tfinal String str1 = \"a, ,efedsfs,   ddf\";\n\t\tfinal List<String> split = StrSplitter.split(str1, ',', 0, true, true);\n\n\t\tassertEquals(\"ddf\", split.get(2));\n\t\tassertEquals(3, split.size());\n\t}\n\n\t@Test\n\tpublic void splitByStrTest(){\n\t\tfinal String str1 = \"aabbccaaddaaee\";\n\t\tfinal List<String> split = StrSplitter.split(str1, \"aa\", 0, true, true);\n\t\tassertEquals(\"ee\", split.get(2));\n\t\tassertEquals(3, split.size());\n\t}\n\n\t@Test\n\tpublic void splitByBlankTest(){\n\t\tfinal String str1 = \"aa bbccaa     ddaaee\";\n\t\tfinal List<String> split = StrSplitter.split(str1, 0);\n\t\tassertEquals(\"ddaaee\", split.get(2));\n\t\tassertEquals(3, split.size());\n\t}\n\n\t@Test\n\tpublic void splitPathTest(){\n\t\tfinal String str1 = \"/use/local/bin\";\n\t\tfinal List<String> split = StrSplitter.splitPath(str1, 0);\n\t\tassertEquals(\"bin\", split.get(2));\n\t\tassertEquals(3, split.size());\n\t}\n\n\t@Test\n\tpublic void splitMappingTest() {\n\t\tfinal String str = \"1.2.\";\n\t\tfinal List<Long> split = StrSplitter.split(str, '.', 0, true, true, Long::parseLong);\n\t\tassertEquals(2, split.size());\n\t\tassertEquals(Long.valueOf(1L), split.get(0));\n\t\tassertEquals(Long.valueOf(2L), split.get(1));\n\t}\n\n\t@Test\n\tpublic void splitEmptyTest(){\n\t\tfinal String str = \"\";\n\t\tfinal String[] split = str.split(\",\");\n\t\tfinal String[] strings = StrSplitter.splitToArray(str, \",\", -1, false, false);\n\t\tassertNotNull(strings);\n\t\tassertArrayEquals(split, strings);\n\t}\n\n\t@Test\n\tpublic void splitNullTest(){\n\t\tfinal String str = null;\n\t\tfinal String[] strings = StrSplitter.splitToArray(str, \",\", -1, false, false);\n\t\tassertNotNull(strings);\n\t\tassertEquals(0, strings.length);\n\t}\n\n\t/**\n\t * https://github.com/chinabugotech/hutool/issues/2099\n\t */\n\t@Test\n\tpublic void splitByRegexTest(){\n\t\tfinal String text = \"01  821   34567890182345617821\";\n\t\tList<String> strings = StrSplitter.splitByRegex(text, \"21\", 0, false, true);\n\t\tassertEquals(2, strings.size());\n\t\tassertEquals(\"01  8\", strings.get(0));\n\t\tassertEquals(\"   345678901823456178\", strings.get(1));\n\n\t\tstrings = StrSplitter.splitByRegex(text, \"21\", 0, false, false);\n\t\tassertEquals(3, strings.size());\n\t\tassertEquals(\"01  8\", strings.get(0));\n\t\tassertEquals(\"   345678901823456178\", strings.get(1));\n\t\tassertEquals(\"\", strings.get(2));\n\t}\n\n\t@Test\n\tpublic void issue3421Test() {\n\t\tList<String> strings = StrSplitter.splitByRegex(\"\", \"\", 0, false, false);\n\t\tassertEquals(ListUtil.of(\"\"), strings);\n\n\t\tstrings = StrSplitter.splitByRegex(\"aaa\", \"\", 0, false, false);\n\t\tassertEquals(ListUtil.of(\"aaa\"), strings);\n\n\t\tstrings = StrSplitter.splitByRegex(\"\", \"aaa\", 0, false, false);\n\t\tassertEquals(ListUtil.of(\"\"), strings);\n\n\t\tstrings = StrSplitter.splitByRegex(\"\", \"\", 0, false, true);\n\t\tassertEquals(ListUtil.of(), strings);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/AsyncUtilTest.java",
    "content": "package cn.hutool.core.thread;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * CompletableFuture工具类测试\n *\n * @author <achao1441470436@gmail.com>\n * @since 2021/11/10 0010 21:15\n */\npublic class AsyncUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void waitAndGetTest() {\n\t\tCompletableFuture<String> hutool = CompletableFuture.supplyAsync(() -> {\n\t\t\tThreadUtil.sleep(1, TimeUnit.SECONDS);\n\t\t\treturn \"hutool\";\n\t\t});\n\t\tCompletableFuture<String> sweater = CompletableFuture.supplyAsync(() -> {\n\t\t\tThreadUtil.sleep(2, TimeUnit.SECONDS);\n\t\t\treturn \"卫衣\";\n\t\t});\n\t\tCompletableFuture<String> warm = CompletableFuture.supplyAsync(() -> {\n\t\t\tThreadUtil.sleep(3, TimeUnit.SECONDS);\n\t\t\treturn \"真暖和\";\n\t\t});\n\t\t// 等待完成\n\t\tAsyncUtil.waitAll(hutool, sweater, warm);\n\t\t// 获取结果\n\t\tassertEquals(\"hutool卫衣真暖和\", AsyncUtil.get(hutool) + AsyncUtil.get(sweater) + AsyncUtil.get(warm));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/ConcurrencyTesterTest.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.RandomUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ConcurrencyTesterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void concurrencyTesterTest() {\n\t\tConcurrencyTester tester = ThreadUtil.concurrencyTest(100, () -> {\n\t\t\tlong delay = RandomUtil.randomLong(100, 1000);\n\t\t\tThreadUtil.sleep(delay);\n\t\t\tConsole.log(\"{} test finished, delay: {}\", Thread.currentThread().getName(), delay);\n\t\t});\n\t\tConsole.log(tester.getInterval());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void multiTest(){\n\t\tConcurrencyTester ct = new ConcurrencyTester(5);\n\t\tfor(int i=0;i<3;i++){\n\t\t\tConsole.log(\"开始执行第{}个\",i);\n\t\t\tct.test(() -> {\n\t\t\t\t// 需要并发测试的业务代码\n\t\t\t\tConsole.log(\"当前执行线程：\" + Thread.currentThread().getName()+\" 产生时间 \"+ DateUtil.now());\n\t\t\t\tThreadUtil.sleep(RandomUtil.randomInt(1000, 3000));\n\t\t\t});\n\t\t}\n\t\tConsole.log(\"全部线程执行完毕 \"+DateUtil.now());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/ExecutorBuilderTest.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.ThreadPoolExecutor;\n\npublic class ExecutorBuilderTest {\n\n\t@Test\n\t@Disabled\n\tpublic void CallerRunsPolicyTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/pulls/660\n\t\tfinal ThreadPoolExecutor executor = ExecutorBuilder.create().setCorePoolSize(1).setMaxPoolSize(1).setHandler(RejectPolicy.BLOCK.getValue()).build();\n\t\texecutor.execute(()-> Console.log(\"### 1\"));\n\t\texecutor.execute(()-> Console.log(\"### 2\"));\n\n\t\texecutor.shutdown();\n\t\texecutor.execute(()-> Console.log(\"### 3\"));\n\t\texecutor.execute(()-> Console.log(\"### 4\"));\n\t\texecutor.execute(()-> Console.log(\"### 5\"));\n\t\texecutor.execute(()-> Console.log(\"### 6\"));\n\t\tThreadUtil.sleep(3000);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/RecyclableBatchThreadPoolExecutorTest.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.thread.RecyclableBatchThreadPoolExecutor.Warp;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.function.Function;\n\n/**\n * {@link RecyclableBatchThreadPoolExecutor} 测试类\n */\npublic class RecyclableBatchThreadPoolExecutorTest {\n\n\n\t/**\n\t * 批量处理数据\n\t */\n\t@Test\n\t@Disabled\n\tpublic void test() throws InterruptedException {\n\t\tint corePoolSize = 10;// 线程池大小\n\t\tint batchSize = 100;// 每批次数据量\n\t\tint clientCount = 30;// 调用者数量\n\t\ttest(corePoolSize,batchSize,clientCount);\n\t}\n\n\t/**\n\t * 普通查询接口加速\n\t */\n\t@Test\n\t@Disabled\n\tpublic void test2() {\n\t\tRecyclableBatchThreadPoolExecutor executor = new RecyclableBatchThreadPoolExecutor(10);\n\t\tlong s = System.nanoTime();\n\t\tWarp<String> warp1 = Warp.of(this::select1);\n\t\tWarp<List<String>> warp2 = Warp.of(this::select2);\n\t\texecutor.processByWarp(warp1, warp2);\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key1\",warp1.get());\n\t\tmap.put(\"key2\",warp2.get());\n\t\tlong d = System.nanoTime() - s;\n\t\tSystem.out.printf(\"总耗时：%.2f秒%n\",d/1e9);\n\t\tSystem.out.println(map);\n\t}\n\n\tpublic void test(int corePoolSize,int batchSize,int clientCount ) throws InterruptedException{\n\t\tRecyclableBatchThreadPoolExecutor processor = new RecyclableBatchThreadPoolExecutor(corePoolSize);\n\t\t// 模拟多个调用者线程提交任务\n\t\tExecutorService testExecutor = Executors.newFixedThreadPool(clientCount);\n\t\tMap<Integer, List<Integer>> map = new HashMap<>();\n\t\tfor(int i = 0; i < clientCount; i++){\n\t\t\tmap.put(i,testDate(1000));\n\t\t}\n\t\tlong s = System.nanoTime();\n\t\tList<Future<?>> futures = new ArrayList<>();\n\t\tfor (int j = 0; j < clientCount; j++) {\n\t\t\tfinal int clientId = j;\n\t\t\tFuture<?> submit = testExecutor.submit(() -> {\n\t\t\t\tFunction<Integer, String> function = p -> {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tThread.sleep(10);\n\t\t\t\t\t} catch (InterruptedException e) {\n\t\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t\t}\n\t\t\t\t\treturn Thread.currentThread().getName() + \"#\" + p;\n\t\t\t\t};\n\t\t\t\tlong start = System.nanoTime();\n\t\t\t\tList<String> process = processor.process(map.get(clientId), batchSize, function);\n\t\t\t\tlong duration = System.nanoTime() - start;\n\t\t\t\tSystem.out.printf(\"【clientId：%s】处理结果：%s\\n处理耗时：%.2f秒%n\", clientId, process, duration / 1e9);\n\t\t\t});\n\t\t\tfutures.add(submit);\n\t\t}\n\t\tfutures.forEach(p-> {\n\t\t\ttry {\n\t\t\t\tp.get();\n\t\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t});\n\t\tlong d = System.nanoTime() - s;\n\t\tSystem.out.printf(\"总耗时：%.2f秒%n\",d/1e9);\n\t\ttestExecutor.shutdown();\n\t\tprocessor.shutdown();\n\t}\n\tpublic static List<Integer> testDate(int count){\n\t\tList<Integer> list = new ArrayList<>();\n\t\tfor(int i = 1;i<=count;i++){\n\t\t\tlist.add(i);\n\t\t}\n\t\treturn list;\n\t}\n\n\tprivate String select1()  {\n\t\ttry {\n\t\t\tThread.sleep(3000);\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\treturn \"1\";\n\t}\n\n\tprivate List<String> select2() {\n\t\ttry {\n\t\t\tThread.sleep(5000);\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\treturn Arrays.asList(\"1\",\"2\",\"3\");\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/SegmentLockTest.java",
    "content": "package cn.hutool.core.thread;\n\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.thread.lock.SegmentLock;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * SegmentLock 单元测试类\n */\npublic class SegmentLockTest {\n\n\tprivate static final int SEGMENT_COUNT = 4;\n\tprivate SegmentLock<Lock> strongLock;\n\tprivate SegmentLock<Lock> weakLock;\n\tprivate SegmentLock<Semaphore> semaphore;\n\tprivate SegmentLock<ReadWriteLock> readWriteLock;\n\n\t@BeforeEach\n\tpublic void setUp() {\n\t\tstrongLock = SegmentLock.lock(SEGMENT_COUNT);\n\t\tweakLock = SegmentLock.lazyWeakLock(SEGMENT_COUNT);\n\t\tsemaphore = SegmentLock.semaphore(SEGMENT_COUNT, 2);\n\t\treadWriteLock = SegmentLock.readWriteLock(SEGMENT_COUNT);\n\t}\n\n\t@Test\n\tpublic void testSize() {\n\t\tassertEquals(SEGMENT_COUNT, strongLock.size());\n\t\tassertEquals(SEGMENT_COUNT, weakLock.size());\n\t\tassertEquals(SEGMENT_COUNT, semaphore.size());\n\t\tassertEquals(SEGMENT_COUNT, readWriteLock.size());\n\t}\n\n\t@SuppressWarnings(\"StringOperationCanBeSimplified\")\n\t@Test\n\tpublic void testGetWithSameKey() {\n\t\t// 相同 key 应返回相同锁\n\t\tString key1 = \"testKey\";\n\t\tString key2 = new String(\"testKey\"); // equals 但不同对象\n\t\tLock lock1 = strongLock.get(key1);\n\t\tLock lock2 = strongLock.get(key2);\n\t\tassertSame(lock1, lock2, \"相同 key 应返回同一锁对象\");\n\n\t\tLock weakLock1 = weakLock.get(key1);\n\t\tLock weakLock2 = weakLock.get(key2);\n\t\tassertSame(weakLock1, weakLock2, \"弱引用锁相同 key 应返回同一锁对象\");\n\t}\n\n\t@Test\n\tpublic void testGetAt() {\n\t\tfor (int i = 0; i < SEGMENT_COUNT; i++) {\n\t\t\tLock lock = strongLock.getAt(i);\n\t\t\tassertNotNull(lock, \"getAt 返回的锁不应为 null\");\n\t\t}\n\t\tassertThrows(IllegalArgumentException.class, () -> strongLock.getAt(SEGMENT_COUNT),\n\t\t\t\"超出段数的索引应抛出异常\");\n\t}\n\n\t@Test\n\tpublic void testBulkGet() {\n\t\tList<String> keys = CollUtil.newArrayList(\"key1\", \"key2\", \"key3\");\n\t\tIterable<Lock> locks = strongLock.bulkGet(keys);\n\t\tList<Lock> lockList = CollUtil.newArrayList(locks);\n\n\t\tassertEquals(3, lockList.size(), \"bulkGet 返回的锁数量应与 key 数量一致\");\n\n\t\t// 检查顺序性\n\t\tint prevIndex = -1;\n\t\tfor (Lock lock : lockList) {\n\t\t\tint index = findIndex(strongLock, lock);\n\t\t\tassertTrue(index >= prevIndex, \"bulkGet 返回的锁应按索引升序\");\n\t\t\tprevIndex = index;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLockConcurrency() throws InterruptedException {\n\t\tint threadCount = SEGMENT_COUNT * 2;\n\t\tCountDownLatch startLatch = new CountDownLatch(1);\n\t\tCountDownLatch endLatch = new CountDownLatch(threadCount);\n\t\tExecutorService executor = Executors.newFixedThreadPool(threadCount);\n\t\tList<String> keys = new ArrayList<>();\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tkeys.add(\"key\" + i);\n\t\t}\n\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tfinal String key = keys.get(i);\n\t\t\texecutor.submit(() -> {\n\t\t\t\ttry {\n\t\t\t\t\tstartLatch.await();\n\t\t\t\t\tLock lock = strongLock.get(key);\n\t\t\t\t\tlock.lock();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tThread.sleep(100); // 模拟工作\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tlock.unlock();\n\t\t\t\t\t}\n\t\t\t\t} catch (InterruptedException e) {\n\t\t\t\t\tThread.currentThread().interrupt();\n\t\t\t\t} finally {\n\t\t\t\t\tendLatch.countDown();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tstartLatch.countDown();\n\t\tassertTrue(endLatch.await(2000, java.util.concurrent.TimeUnit.MILLISECONDS),\n\t\t\t\"并发锁测试应在 2 秒内完成\");\n\t\texecutor.shutdown();\n\t}\n\n\t@Test\n\tpublic void testSemaphore() {\n\t\tSemaphore sem = semaphore.get(\"testKey\");\n\t\tassertEquals(2, sem.availablePermits(), \"信号量初始许可应为 2\");\n\n\t\tsem.acquireUninterruptibly(2);\n\t\tassertEquals(0, sem.availablePermits(), \"获取所有许可后应为 0\");\n\n\t\tsem.release(1);\n\t\tassertEquals(1, sem.availablePermits(), \"释放一个许可后应为 1\");\n\t}\n\n\t@SuppressWarnings(\"ResultOfMethodCallIgnored\")\n\t@Test\n\tpublic void testReadWriteLock() throws InterruptedException {\n\t\tReadWriteLock rwLock = readWriteLock.get(\"testKey\");\n\t\tLock readLock = rwLock.readLock();\n\t\tLock writeLock = rwLock.writeLock();\n\n\t\t// 测试读锁可重入\n\t\treadLock.lock();\n\t\tassertTrue(readLock.tryLock(), \"读锁应允许多个线程同时持有\");\n\t\treadLock.unlock();\n\t\treadLock.unlock();\n\n\t\tCountDownLatch latch = new CountDownLatch(1);\n\t\tExecutorService executor = Executors.newSingleThreadExecutor();\n\t\tAtomicBoolean readLockAcquired = new AtomicBoolean(false);\n\n\t\twriteLock.lock();\n\t\texecutor.submit(() -> {\n\t\t\treadLockAcquired.set(readLock.tryLock());\n\t\t\tlatch.countDown();\n\t\t});\n\n\t\tlatch.await(500, TimeUnit.MILLISECONDS);\n\t\tassertFalse(readLockAcquired.get(), \"写锁持有时读锁应失败\");\n\t\twriteLock.unlock();\n\n\t\texecutor.shutdown();\n\t\texecutor.awaitTermination(1, TimeUnit.SECONDS);\n\t}\n\n\t@Test\n\tpublic void testWeakReferenceCleanup() throws InterruptedException {\n\t\tSegmentLock<Lock> weakLockLarge = SegmentLock.lazyWeakLock(1024); // 超过 LARGE_LAZY_CUTOFF\n\t\tLock lock = weakLockLarge.get(\"testKey\");\n\n\t\tSystem.gc();\n\t\tThread.sleep(100);\n\n\t\t// 弱引用锁未被其他引用，应仍可获取\n\t\tLock lockAgain = weakLockLarge.get(\"testKey\");\n\t\tassertSame(lock, lockAgain, \"弱引用锁未被回收时应返回同一对象\");\n\t}\n\n\t@Test\n\tpublic void testInvalidSegmentCount() {\n\t\tassertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(0),\n\t\t\t\"段数为 0 应抛出异常\");\n\t\tassertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(-1),\n\t\t\t\"负段数应抛出异常\");\n\t}\n\n\t@Test\n\tpublic void testHashDistribution() {\n\t\tSegmentLock<Lock> lock = SegmentLock.lock(4);\n\t\tint[] counts = new int[4];\n\t\tfor (int i = 0; i < 100; i++) {\n\t\t\tint index = findIndex(lock, lock.get(\"key\" + i));\n\t\t\tcounts[index]++;\n\t\t}\n\t\tfor (int count : counts) {\n\t\t\tassertTrue(count > 0, \"每个段都应至少被分配到一个 key\");\n\t\t}\n\t}\n\n\tprivate int findIndex(SegmentLock<Lock> lock, Lock target) {\n\t\tfor (int i = 0; i < lock.size(); i++) {\n\t\t\tif (lock.getAt(i) == target) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/SyncFinisherTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.thread;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class SyncFinisherTest {\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I716SX\n\t * 设置ExceptionHandler捕获异常\n\t */\n\t@Test\n\tpublic void executeExceptionTest() {\n\t\tfinal AtomicBoolean hasException = new AtomicBoolean(false);\n\t\tfinal SyncFinisher syncFinisher = new SyncFinisher(10);\n\t\tsyncFinisher.addWorker(()->{\n\t\t\tConsole.log(Integer.parseInt(\"XYZ\"));//这里会抛RuntimeException\n\t\t});\n\n\t\tsyncFinisher.setExceptionHandler((t, e) -> {\n\t\t\thasException.set(true);\n\t\t\tassertEquals(\"For input string: \\\"XYZ\\\"\", e.getMessage());\n\t\t});\n\n\t\tsyncFinisher.start();\n\t\tIoUtil.close(syncFinisher);\n\t\tThreadUtil.sleep(300);\n\t\tassertTrue(hasException.get());\n\t}\n\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I716SX\n\t * 默认情况下吞掉异常\n\t */\n\t@Test\n\tpublic void executeExceptionTest2() {\n\t\tfinal SyncFinisher syncFinisher = new SyncFinisher(10);\n\t\tsyncFinisher.addWorker(()->{\n\t\t\tConsole.log(Integer.parseInt(\"XYZ\"));//这里会忽略RuntimeException\n\t\t});\n\n\t\tsyncFinisher.start();\n\t\tIoUtil.close(syncFinisher);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/thread/ThreadUtilTest.java",
    "content": "package cn.hutool.core.thread;\n\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.ThreadPoolExecutor;\n\npublic class ThreadUtilTest {\n\n\n\t@Test\n\tpublic void newExecutorTest(){\n\t\tThreadPoolExecutor executor = ThreadUtil.newExecutor(5);\n\t\t// 查询线程池 线程数\n\t\tassertEquals(5, executor.getCorePoolSize());\n\t}\n\n\t@Test\n\tpublic void executeTest() {\n\t\tfinal boolean isValid = true;\n\n\t\tThreadUtil.execute(() -> assertTrue(isValid));\n\t}\n\n\t@Test\n\tpublic void safeSleepTest() {\n\t\tfinal long sleepMillis = RandomUtil.randomLong(1, 1000);\n\t\t// 随机sleep时长，确保sleep时间足够\n\t\tfinal long l = System.currentTimeMillis();\n\t\tThreadUtil.safeSleep(sleepMillis);\n\t\tassertTrue(System.currentTimeMillis() - l >= sleepMillis);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link ArrayUtil} 数组工具单元测试\n *\n * @author Looly\n */\npublic class ArrayUtilTest {\n\n\t@Test\n\tpublic void isEmptyTest() {\n\t\tint[] a = {};\n\t\tassertTrue(ArrayUtil.isEmpty(a));\n\t\tassertTrue(ArrayUtil.isEmpty((Object) a));\n\t\tint[] b = null;\n\t\t//noinspection ConstantConditions\n\t\tassertTrue(ArrayUtil.isEmpty(b));\n\t\tObject c = null;\n\t\t//noinspection ConstantConditions\n\t\tassertTrue(ArrayUtil.isEmpty(c));\n\n\t\tObject d = new Object[]{\"1\", \"2\", 3, 4D};\n\t\tboolean isEmpty = ArrayUtil.isEmpty(d);\n\t\tassertFalse(isEmpty);\n\t\td = new Object[0];\n\t\tisEmpty = ArrayUtil.isEmpty(d);\n\t\tassertTrue(isEmpty);\n\t\td = null;\n\t\t//noinspection ConstantConditions\n\t\tisEmpty = ArrayUtil.isEmpty(d);\n\t\t//noinspection ConstantConditions\n\t\tassertTrue(isEmpty);\n\n\t\t// Object数组\n\t\tObject[] e = new Object[]{\"1\", \"2\", 3, 4D};\n\t\tfinal boolean empty = ArrayUtil.isEmpty(e);\n\t\tassertFalse(empty);\n\t}\n\n\t@Test\n\tpublic void isNotEmptyTest() {\n\t\tint[] a = {1, 2};\n\t\tassertTrue(ArrayUtil.isNotEmpty(a));\n\n\t\tString[] b = {\"a\", \"b\", \"c\"};\n\t\tassertTrue(ArrayUtil.isNotEmpty(b));\n\n\t\tObject c = new Object[]{\"1\", \"2\", 3, 4D};\n\t\tassertTrue(ArrayUtil.isNotEmpty(c));\n\t}\n\n\t@Test\n\tpublic void newArrayTest() {\n\t\tString[] newArray = ArrayUtil.newArray(String.class, 3);\n\t\tassertEquals(3, newArray.length);\n\t}\n\n\t@Test\n\tpublic void cloneTest() {\n\t\tInteger[] b = {1, 2, 3};\n\t\tInteger[] cloneB = ArrayUtil.clone(b);\n\t\tassertArrayEquals(b, cloneB);\n\n\t\tint[] a = {1, 2, 3};\n\t\tint[] clone = ArrayUtil.clone(a);\n\t\tassertArrayEquals(a, clone);\n\t}\n\n\t@Test\n\tpublic void filterEditTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 5, 6};\n\t\tInteger[] filter = ArrayUtil.edit(a, t -> (t % 2 == 0) ? t : null);\n\t\tassertArrayEquals(filter, new Integer[]{2, 4, 6});\n\t}\n\n\t@Test\n\tpublic void filterTestForFilter() {\n\t\tInteger[] a = {1, 2, 3, 4, 5, 6};\n\t\tInteger[] filter = ArrayUtil.filter(a, t -> t % 2 == 0);\n\t\tassertArrayEquals(filter, new Integer[]{2, 4, 6});\n\t}\n\n\t@Test\n\tpublic void editTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 5, 6};\n\t\tInteger[] filter = ArrayUtil.edit(a, t -> (t % 2 == 0) ? t * 10 : t);\n\t\tassertArrayEquals(filter, new Integer[]{1, 20, 3, 40, 5, 60});\n\t}\n\n\t@Test\n\tpublic void indexOfTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 5, 6};\n\t\tint index = ArrayUtil.indexOf(a, 3);\n\t\tassertEquals(2, index);\n\n\t\tlong[] b = {1, 2, 3, 4, 5, 6};\n\t\tint index2 = ArrayUtil.indexOf(b, 3);\n\t\tassertEquals(2, index2);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 3, 6};\n\t\tint index = ArrayUtil.lastIndexOf(a, 3);\n\t\tassertEquals(4, index);\n\n\t\tlong[] b = {1, 2, 3, 4, 3, 6};\n\t\tint index2 = ArrayUtil.lastIndexOf(b, 3);\n\t\tassertEquals(4, index2);\n\t}\n\n\t@Test\n\tpublic void containsTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 3, 6};\n\t\tboolean contains = ArrayUtil.contains(a, 3);\n\t\tassertTrue(contains);\n\n\t\tlong[] b = {1, 2, 3, 4, 3, 6};\n\t\tboolean contains2 = ArrayUtil.contains(b, 3);\n\t\tassertTrue(contains2);\n\t}\n\n\t@Test\n\tpublic void containsAnyTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 3, 6};\n\t\tboolean contains = ArrayUtil.containsAny(a, 4, 10, 40);\n\t\tassertTrue(contains);\n\n\t\tcontains = ArrayUtil.containsAny(a, 10, 40);\n\t\tassertFalse(contains);\n\t}\n\n\t@Test\n\tpublic void containsAllTest() {\n\t\tInteger[] a = {1, 2, 3, 4, 3, 6};\n\t\tboolean contains = ArrayUtil.containsAll(a, 4, 2, 6);\n\t\tassertTrue(contains);\n\n\t\tcontains = ArrayUtil.containsAll(a, 1, 2, 3, 5);\n\t\tassertFalse(contains);\n\t}\n\n\t@Test\n\tpublic void mapTest() {\n\t\tString[] keys = {\"a\", \"b\", \"c\"};\n\t\tInteger[] values = {1, 2, 3};\n\t\tMap<String, Integer> map = ArrayUtil.zip(keys, values, true);\n\t\tassertEquals(Objects.requireNonNull(map).toString(), \"{a=1, b=2, c=3}\");\n\t}\n\n\t@Test\n\tpublic void castTest() {\n\t\tObject[] values = {\"1\", \"2\", \"3\"};\n\t\tString[] cast = (String[]) ArrayUtil.cast(String.class, values);\n\t\tassertEquals(values[0], cast[0]);\n\t\tassertEquals(values[1], cast[1]);\n\t\tassertEquals(values[2], cast[2]);\n\t}\n\n\t@Test\n\tpublic void rangeTest() {\n\t\tint[] range = ArrayUtil.range(0, 10);\n\t\tassertEquals(0, range[0]);\n\t\tassertEquals(1, range[1]);\n\t\tassertEquals(2, range[2]);\n\t\tassertEquals(3, range[3]);\n\t\tassertEquals(4, range[4]);\n\t\tassertEquals(5, range[5]);\n\t\tassertEquals(6, range[6]);\n\t\tassertEquals(7, range[7]);\n\t\tassertEquals(8, range[8]);\n\t\tassertEquals(9, range[9]);\n\t}\n\n\t@Test\n\tpublic void rangeMinTest() {\n\t\tassertThrows(NegativeArraySizeException.class, () -> {\n\t\t\tArrayUtil.range(0, Integer.MIN_VALUE);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void maxTest() {\n\t\tint max = ArrayUtil.max(1, 2, 13, 4, 5);\n\t\tassertEquals(13, max);\n\n\t\tlong maxLong = ArrayUtil.max(1L, 2L, 13L, 4L, 5L);\n\t\tassertEquals(13, maxLong);\n\n\t\tdouble maxDouble = ArrayUtil.max(1D, 2.4D, 13.0D, 4.55D, 5D);\n\t\tassertEquals(13.0, maxDouble, 0);\n\n\t\tBigDecimal one = new BigDecimal(\"1.00\");\n\t\tBigDecimal two = new BigDecimal(\"2.0\");\n\t\tBigDecimal three = new BigDecimal(\"3\");\n\t\tBigDecimal[] bigDecimals = {two, one, three};\n\n\t\tBigDecimal minAccuracy = ArrayUtil.min(bigDecimals, Comparator.comparingInt(BigDecimal::scale));\n\t\tassertEquals(minAccuracy, three);\n\n\t\tBigDecimal maxAccuracy = ArrayUtil.max(bigDecimals, Comparator.comparingInt(BigDecimal::scale));\n\t\tassertEquals(maxAccuracy, one);\n\t}\n\n\t@Test\n\tpublic void minTest() {\n\t\tint min = ArrayUtil.min(1, 2, 13, 4, 5);\n\t\tassertEquals(1, min);\n\n\t\tlong minLong = ArrayUtil.min(1L, 2L, 13L, 4L, 5L);\n\t\tassertEquals(1, minLong);\n\n\t\tdouble minDouble = ArrayUtil.min(1D, 2.4D, 13.0D, 4.55D, 5D);\n\t\tassertEquals(1.0, minDouble, 0);\n\t}\n\n\t@Test\n\tpublic void appendTest() {\n\t\tString[] a = {\"1\", \"2\", \"3\", \"4\"};\n\t\tString[] b = {\"a\", \"b\", \"c\"};\n\n\t\tString[] result = ArrayUtil.append(a, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"4\", \"a\", \"b\", \"c\"}, result);\n\t}\n\n\t@Test\n\tpublic void insertTest() {\n\t\tString[] a = {\"1\", \"2\", \"3\", \"4\"};\n\t\tString[] b = {\"a\", \"b\", \"c\"};\n\n\t\t// 在-1位置插入，相当于在3位置插入\n\t\tString[] result = ArrayUtil.insert(a, -1, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"a\", \"b\", \"c\", \"4\"}, result);\n\n\t\t// 在第0个位置插入，即在数组前追加\n\t\tresult = ArrayUtil.insert(a, 0, b);\n\t\tassertArrayEquals(new String[]{\"a\", \"b\", \"c\", \"1\", \"2\", \"3\", \"4\"}, result);\n\n\t\t// 在第2个位置插入，即\"3\"之前\n\t\tresult = ArrayUtil.insert(a, 2, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"a\", \"b\", \"c\", \"3\", \"4\"}, result);\n\n\t\t// 在第4个位置插入，即\"4\"之后，相当于追加\n\t\tresult = ArrayUtil.insert(a, 4, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"4\", \"a\", \"b\", \"c\"}, result);\n\n\t\t// 在第5个位置插入，由于数组长度为4，因此补null\n\t\tresult = ArrayUtil.insert(a, 5, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"4\", null, \"a\", \"b\", \"c\"}, result);\n\t}\n\n\t@Test\n\tpublic void joinTest() {\n\t\tString[] array = {\"aa\", \"bb\", \"cc\", \"dd\"};\n\t\tString join = ArrayUtil.join(array, \",\", \"[\", \"]\");\n\t\tassertEquals(\"[aa],[bb],[cc],[dd]\", join);\n\n\t\tObject array2 = new String[]{\"aa\", \"bb\", \"cc\", \"dd\"};\n\t\tString join2 = ArrayUtil.join(array2, \",\");\n\t\tassertEquals(\"aa,bb,cc,dd\", join2);\n\t}\n\n\t@Test\n\tpublic void getArrayTypeTest() {\n\t\tClass<?> arrayType = ArrayUtil.getArrayType(int.class);\n\t\tassertSame(int[].class, arrayType);\n\n\t\tarrayType = ArrayUtil.getArrayType(String.class);\n\t\tassertSame(String[].class, arrayType);\n\t}\n\n\t@Test\n\tpublic void distinctTest() {\n\t\tString[] array = {\"aa\", \"bb\", \"cc\", \"dd\", \"bb\", \"dd\"};\n\t\tString[] distinct = ArrayUtil.distinct(array);\n\t\tassertArrayEquals(new String[]{\"aa\", \"bb\", \"cc\", \"dd\"}, distinct);\n\t}\n\n\t@Test\n\tpublic void distinctByFunctionTest() {\n\t\tString[] array = {\"aa\", \"Aa\", \"BB\", \"bb\"};\n\n\t\t// 覆盖模式下，保留最后加入的两个元素\n\t\tString[] distinct = ArrayUtil.distinct(array, String::toLowerCase, true);\n\t\tassertArrayEquals(new String[]{\"Aa\", \"bb\"}, distinct);\n\n\t\t// 忽略模式下，保留最早加入的两个元素\n\t\tdistinct = ArrayUtil.distinct(array, String::toLowerCase, false);\n\t\tassertArrayEquals(new String[]{\"aa\", \"BB\"}, distinct);\n\t}\n\n\t@Test\n\tpublic void toStingTest() {\n\t\tint[] a = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1, 3, 56, 6, 7]\", ArrayUtil.toString(a));\n\t\tlong[] b = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1, 3, 56, 6, 7]\", ArrayUtil.toString(b));\n\t\tshort[] c = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1, 3, 56, 6, 7]\", ArrayUtil.toString(c));\n\t\tdouble[] d = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1.0, 3.0, 56.0, 6.0, 7.0]\", ArrayUtil.toString(d));\n\t\tbyte[] e = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1, 3, 56, 6, 7]\", ArrayUtil.toString(e));\n\t\tboolean[] f = {true, false, true, true, true};\n\t\tassertEquals(\"[true, false, true, true, true]\", ArrayUtil.toString(f));\n\t\tfloat[] g = {1, 3, 56, 6, 7};\n\t\tassertEquals(\"[1.0, 3.0, 56.0, 6.0, 7.0]\", ArrayUtil.toString(g));\n\t\tchar[] h = {'a', 'b', '你', '好', '1'};\n\t\tassertEquals(\"[a, b, 你, 好, 1]\", ArrayUtil.toString(h));\n\n\t\tString[] array = {\"aa\", \"bb\", \"cc\", \"dd\", \"bb\", \"dd\"};\n\t\tassertEquals(\"[aa, bb, cc, dd, bb, dd]\", ArrayUtil.toString(array));\n\t}\n\n\t@Test\n\tpublic void toArrayTest() {\n\t\tfinal ArrayList<String> list = CollUtil.newArrayList(\"A\", \"B\", \"C\", \"D\");\n\t\tfinal String[] array = ArrayUtil.toArray(list, String.class);\n\t\tassertEquals(\"A\", array[0]);\n\t\tassertEquals(\"B\", array[1]);\n\t\tassertEquals(\"C\", array[2]);\n\t\tassertEquals(\"D\", array[3]);\n\t}\n\n\t@Test\n\tpublic void addAllTest() {\n\t\tfinal int[] ints = ArrayUtil.addAll(new int[]{1, 2, 3}, new int[]{4, 5, 6});\n\t\tassertArrayEquals(new int[]{1, 2, 3, 4, 5, 6}, ints);\n\t}\n\n\t@Test\n\tpublic void isAllNotNullTest() {\n\t\tString[] allNotNull = {\"aa\", \"bb\", \"cc\", \"dd\", \"bb\", \"dd\"};\n\t\tassertTrue(ArrayUtil.isAllNotNull(allNotNull));\n\t\tString[] hasNull = {\"aa\", \"bb\", \"cc\", null, \"bb\", \"dd\"};\n\t\tassertFalse(ArrayUtil.isAllNotNull(hasNull));\n\t}\n\n\t@Test\n\tpublic void indexOfSubTest() {\n\t\tInteger[] a = {0x12, 0x34, 0x56, 0x78, 0x9A};\n\t\tInteger[] b = {0x56, 0x78};\n\t\tInteger[] c = {0x12, 0x56};\n\t\tInteger[] d = {0x78, 0x9A};\n\t\tInteger[] e = {0x78, 0x9A, 0x10};\n\n\t\tint i = ArrayUtil.indexOfSub(a, b);\n\t\tassertEquals(2, i);\n\n\t\ti = ArrayUtil.indexOfSub(a, c);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.indexOfSub(a, d);\n\t\tassertEquals(3, i);\n\n\t\ti = ArrayUtil.indexOfSub(a, e);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.indexOfSub(a, null);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.indexOfSub(null, null);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.indexOfSub(null, b);\n\t\tassertEquals(-1, i);\n\t}\n\n\t@Test\n\tpublic void indexOfSubTest2() {\n\t\tInteger[] a = {0x12, 0x56, 0x34, 0x56, 0x78, 0x9A};\n\t\tInteger[] b = {0x56, 0x78};\n\t\tint i = ArrayUtil.indexOfSub(a, b);\n\t\tassertEquals(3, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfSubTest() {\n\t\tInteger[] a = {0x12, 0x34, 0x56, 0x78, 0x9A};\n\t\tInteger[] b = {0x56, 0x78};\n\t\tInteger[] c = {0x12, 0x56};\n\t\tInteger[] d = {0x78, 0x9A};\n\t\tInteger[] e = {0x78, 0x9A, 0x10};\n\n\t\tint i = ArrayUtil.lastIndexOfSub(a, b);\n\t\tassertEquals(2, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(a, c);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(a, d);\n\t\tassertEquals(3, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(a, e);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(a, null);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(null, null);\n\t\tassertEquals(-1, i);\n\n\t\ti = ArrayUtil.lastIndexOfSub(null, b);\n\t\tassertEquals(-1, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfSubTest2() {\n\t\tInteger[] a = {0x12, 0x56, 0x78, 0x56, 0x21, 0x9A};\n\t\tInteger[] b = {0x56, 0x78};\n\t\tint i = ArrayUtil.indexOfSub(a, b);\n\t\tassertEquals(1, i);\n\t}\n\n\t@Test\n\tpublic void reverseTest() {\n\t\tint[] a = {1, 2, 3, 4};\n\t\tfinal int[] reverse = ArrayUtil.reverse(a);\n\t\tassertArrayEquals(new int[]{4, 3, 2, 1}, reverse);\n\t}\n\n\t@Test\n\tpublic void reverseTest2s() {\n\t\tObject[] a = {\"1\", '2', \"3\", 4};\n\t\tfinal Object[] reverse = ArrayUtil.reverse(a);\n\t\tassertArrayEquals(new Object[]{4, \"3\", '2', \"1\"}, reverse);\n\t}\n\n\t@Test\n\tpublic void removeEmptyTest() {\n\t\tString[] a = {\"a\", \"b\", \"\", null, \" \", \"c\"};\n\t\tString[] resultA = {\"a\", \"b\", \" \", \"c\"};\n\t\tassertArrayEquals(ArrayUtil.removeEmpty(a), resultA);\n\t}\n\n\t@Test\n\tpublic void removeBlankTest() {\n\t\tString[] a = {\"a\", \"b\", \"\", null, \" \", \"c\"};\n\t\tString[] resultA = {\"a\", \"b\", \"c\"};\n\t\tassertArrayEquals(ArrayUtil.removeBlank(a), resultA);\n\t}\n\n\t@Test\n\tpublic void nullToEmptyTest() {\n\t\tString[] a = {\"a\", \"b\", \"\", null, \" \", \"c\"};\n\t\tString[] resultA = {\"a\", \"b\", \"\", \"\", \" \", \"c\"};\n\t\tassertArrayEquals(ArrayUtil.nullToEmpty(a), resultA);\n\t}\n\n\t@Test\n\tpublic void wrapTest() {\n\t\tObject a = new int[]{1, 2, 3, 4};\n\t\tObject[] wrapA = ArrayUtil.wrap(a);\n\t\tfor (Object o : wrapA) {\n\t\t\tassertInstanceOf(Integer.class, o);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void splitTest() {\n\t\tbyte[] array = new byte[1024];\n\t\tbyte[][] arrayAfterSplit = ArrayUtil.split(array, 500);\n\t\tassertEquals(3, arrayAfterSplit.length);\n\t\tassertEquals(24, arrayAfterSplit[2].length);\n\t}\n\n\t@Test\n\tpublic void getTest() {\n\t\tString[] a = {\"a\", \"b\", \"c\"};\n\t\tfinal Object o = ArrayUtil.get(a, -1);\n\t\tassertEquals(\"c\", o);\n\t}\n\n\t@Test\n\tpublic void replaceTest() {\n\t\tString[] a = {\"1\", \"2\", \"3\", \"4\"};\n\t\tString[] b = {\"a\", \"b\", \"c\"};\n\n\t\t// 在小于0的位置，-1位置插入，返回b+a，新数组\n\t\tString[] result = ArrayUtil.replace(a, -1, b);\n\t\tassertArrayEquals(new String[]{\"a\", \"b\", \"c\", \"1\", \"2\", \"3\", \"4\"}, result);\n\n\t\t// 在第0个位置开始替换，返回a\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 0, b);\n\t\tassertArrayEquals(new String[]{\"a\", \"b\", \"c\", \"4\"}, result);\n\n\t\t// 在第1个位置替换，即\"2\"开始\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 1, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"a\", \"b\", \"c\"}, result);\n\n\t\t// 在第2个位置插入，即\"3\"之后\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 2, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"a\", \"b\", \"c\"}, result);\n\n\t\t// 在第3个位置插入，即\"4\"之后\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 3, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"a\", \"b\", \"c\"}, result);\n\n\t\t// 在第4个位置插入，数组长度为4，在索引4出替换即两个数组相加\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 4, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"4\", \"a\", \"b\", \"c\"}, result);\n\n\t\t// 在大于3个位置插入，数组长度为4，即两个数组相加\n\t\tresult = ArrayUtil.replace(ArrayUtil.clone(a), 5, b);\n\t\tassertArrayEquals(new String[]{\"1\", \"2\", \"3\", \"4\", \"a\", \"b\", \"c\"}, result);\n\n\t\tString[] e = null;\n\t\tString[] f = {\"a\", \"b\", \"c\"};\n\n\t\t// e为null 返回 f\n\t\tresult = ArrayUtil.replace(e, -1, f);\n\t\tassertArrayEquals(f, result);\n\n\t\tString[] g = {\"a\", \"b\", \"c\"};\n\t\tString[] h = null;\n\n\t\t// h为null 返回 g\n\t\tresult = ArrayUtil.replace(g, 0, h);\n\t\tassertArrayEquals(g, result);\n\t}\n\n\t@Test\n\tpublic void setOrAppendTest(){\n\t\tString[] arr = new String[0];\n\t\tString[] newArr = ArrayUtil.setOrAppend(arr, 0, \"Good\");// ClassCastException\n\t\tassertArrayEquals(new String[]{\"Good\"}, newArr);\n\t}\n\n\t@Test\n\tpublic void getAnyTest() {\n\t\tfinal String[] a = {\"a\", \"b\", \"c\", \"d\", \"e\"};\n\t\tfinal Object o = ArrayUtil.getAny(a, 3, 4);\n\t\tfinal String[] resultO = (String[]) o;\n\t\tfinal String[] c = {\"d\", \"e\"};\n\t\tassertTrue(ArrayUtil.containsAll(c, resultO[0], resultO[1]));\n\t}\n\n\t@Test\n\tpublic void testInsertPrimitive() {\n\t\tfinal boolean[] booleans = new boolean[10];\n\t\tfinal byte[] bytes = new byte[10];\n\t\tfinal char[] chars = new char[10];\n\t\tfinal short[] shorts = new short[10];\n\t\tfinal int[] ints = new int[10];\n\t\tfinal long[] longs = new long[10];\n\t\tfinal float[] floats = new float[10];\n\t\tfinal double[] doubles = new double[10];\n\n\t\tfinal boolean[] insert1 = (boolean[]) ArrayUtil.insert(booleans, 0, 0, 1, 2);\n\t\tassertNotNull(insert1);\n\t\tfinal byte[] insert2 = (byte[]) ArrayUtil.insert(bytes, 0, 1, 2, 3);\n\t\tassertNotNull(insert2);\n\t\tfinal char[] insert3 = (char[]) ArrayUtil.insert(chars, 0, 1, 2, 3);\n\t\tassertNotNull(insert3);\n\t\tfinal short[] insert4 = (short[]) ArrayUtil.insert(shorts, 0, 1, 2, 3);\n\t\tassertNotNull(insert4);\n\t\tfinal int[] insert5 = (int[]) ArrayUtil.insert(ints, 0, 1, 2, 3);\n\t\tassertNotNull(insert5);\n\t\tfinal long[] insert6 = (long[]) ArrayUtil.insert(longs, 0, 1, 2, 3);\n\t\tassertNotNull(insert6);\n\t\tfinal float[] insert7 = (float[]) ArrayUtil.insert(floats, 0, 1, 2, 3);\n\t\tassertNotNull(insert7);\n\t\tfinal double[] insert8 = (double[]) ArrayUtil.insert(doubles, 0, 1, 2, 3);\n\t\tassertNotNull(insert8);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/BooleanUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BooleanUtilTest {\n\n\t@Test\n\tpublic void toBooleanTest() {\n\t\tassertTrue(BooleanUtil.toBoolean(\"true\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"yes\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"y\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"t\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"OK\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"correct\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"success\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"1\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"On\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"是\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"对\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"真\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"對\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"正确\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"开\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"开启\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"√\"));\n\t\tassertTrue(BooleanUtil.toBoolean(\"☑\"));\n\n\t\tassertFalse(BooleanUtil.toBoolean(\"false\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"no\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"n\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"f\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"off\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"wrong\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"fail\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"0\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"Off\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"否\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"错\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"假\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"錯\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"错误\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"关\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"关闭\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"×\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"☒\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"6455434\"));\n\t\tassertFalse(BooleanUtil.toBoolean(\"\"));\n\t}\n\n\t@Test\n\tpublic void andTest(){\n\t\tassertFalse(BooleanUtil.and(true,false));\n\t\tassertFalse(BooleanUtil.andOfWrap(true,false));\n\t}\n\n\t@Test\n\tpublic void orTest(){\n\t\tassertTrue(BooleanUtil.or(true,false));\n\t\tassertTrue(BooleanUtil.orOfWrap(true,false));\n\t}\n\n\t@Test\n\tpublic void xorTest(){\n\t\tassertTrue(BooleanUtil.xor(true,false));\n\t\tassertTrue(BooleanUtil.xorOfWrap(true,false));\n\t}\n\n\tpublic void orOfWrapTest() {\n\t\tassertFalse(BooleanUtil.orOfWrap(Boolean.FALSE, null));\n\t\tassertTrue(BooleanUtil.orOfWrap(Boolean.TRUE, null));\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void isTrueIsFalseTest() {\n\t\tassertFalse(BooleanUtil.isTrue(null));\n\t\tassertFalse(BooleanUtil.isFalse(null));\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\tpublic void negateTest() {\n\t\tassertFalse(BooleanUtil.negate(Boolean.TRUE));\n\t\tassertTrue(BooleanUtil.negate(Boolean.FALSE));\n\n\t\tassertFalse(BooleanUtil.negate(Boolean.TRUE.booleanValue()));\n\t\tassertTrue(BooleanUtil.negate(Boolean.FALSE.booleanValue()));\n\t}\n\n\t@Test\n\tpublic void toStringTest() {\n\t\tassertEquals(\"true\", BooleanUtil.toStringTrueFalse(true));\n\t\tassertEquals(\"false\", BooleanUtil.toStringTrueFalse(false));\n\n\t\tassertEquals(\"yes\", BooleanUtil.toStringYesNo(true));\n\t\tassertEquals(\"no\", BooleanUtil.toStringYesNo(false));\n\n\t\tassertEquals(\"on\", BooleanUtil.toStringOnOff(true));\n\t\tassertEquals(\"off\", BooleanUtil.toStringOnOff(false));\n\t}\n\n\t@Test\n\tpublic void issue3587Test() {\n\t\tBoolean boolean1 = true;\n\t\tBoolean boolean2 = null;\n\t\tBoolean result = BooleanUtil.andOfWrap(boolean1, boolean2);\n\t\tassertFalse(result);\n\t}\n\t@Test\n\tpublic void testXorSemantics() {\n\t\t// xor 的实际语义：true 的数量为奇数\n\t\tassertTrue(BooleanUtil.xor(true, true, true));\n\t\tassertFalse(BooleanUtil.xor(true, true));\n\t}\n\n\t@Test\n\tpublic void testExactlyOneTrue() {\n\t\t// 恰好只有一个 true\n\t\tassertTrue(BooleanUtil.exactlyOneTrue(true, false, false));\n\n\t\t// 多个 true，不符合互斥语义\n\t\tassertFalse(BooleanUtil.exactlyOneTrue(true, true, false));\n\t\tassertFalse(BooleanUtil.exactlyOneTrue(true, true, true));\n\n\t\t// 没有 true\n\t\tassertFalse(BooleanUtil.exactlyOneTrue(false, false, false));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ByteUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\npublic class ByteUtilTest {\n\t@Test\n\tpublic void intAndBytesLittleEndianTest() {\n\t\t// 测试 int 转小端序 byte 数组\n\t\tint int1 = RandomUtil.randomInt((Integer.MAX_VALUE));\n\n\t\tByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);\n\t\tbuffer.order(ByteOrder.LITTLE_ENDIAN);\n\t\tbuffer.putInt(int1);\n\t\tbyte[] bytesIntFromBuffer = buffer.array();\n\n\t\tbyte[] bytesInt = ByteUtil.intToBytes(int1, ByteOrder.LITTLE_ENDIAN);\n\t\tassertArrayEquals(bytesIntFromBuffer, bytesInt);\n\n\t\tint int2 = ByteUtil.bytesToInt(bytesInt, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(int1, int2);\n\n\t\tbyte[] bytesInt2 = ByteUtil.intToBytes(int1, ByteOrder.LITTLE_ENDIAN);\n\t\tint int3 = ByteUtil.bytesToInt(bytesInt2, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(int1, int3);\n\n\t\tbyte[] bytesInt3 = ByteUtil.intToBytes(int1, ByteOrder.LITTLE_ENDIAN);\n\t\tint int4 = ByteUtil.bytesToInt(bytesInt3, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(int1, int4);\n\t}\n\n\t@Test\n\tpublic void intAndBytesBigEndianTest() {\n\t\t// 测试 int 转大端序 byte 数组\n\t\tint int2 = RandomUtil.randomInt(Integer.MAX_VALUE);\n\n\t\tByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);\n\t\tbuffer.putInt(int2);\n\t\tbyte[] bytesIntFromBuffer = buffer.array();\n\n\t\tbyte[] bytesInt = ByteUtil.intToBytes(int2, ByteOrder.BIG_ENDIAN);\n\t\tassertArrayEquals(bytesIntFromBuffer, bytesInt);\n\n\t\t// 测试大端序 byte 数组转 int\n\t\tint int3 = ByteUtil.bytesToInt(bytesInt, ByteOrder.BIG_ENDIAN);\n\t\tassertEquals(int2, int3);\n\t}\n\n\t@Test\n\tpublic void longAndBytesLittleEndianTest() {\n\t\t// 测试 long 转 byte 数组\n\t\tlong long1 = RandomUtil.randomLong(Long.MAX_VALUE);\n\n\t\tByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);\n\t\tbuffer.order(ByteOrder.LITTLE_ENDIAN);\n\t\tbuffer.putLong(long1);\n\t\tbyte[] bytesLongFromBuffer = buffer.array();\n\n\t\tbyte[] bytesLong = ByteUtil.longToBytes(long1, ByteOrder.LITTLE_ENDIAN);\n\t\tassertArrayEquals(bytesLongFromBuffer, bytesLong);\n\n\t\tlong long2 = ByteUtil.bytesToLong(bytesLong, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(long1, long2);\n\n\t\tbyte[] bytesLong2 = ByteUtil.longToBytes(long1);\n\t\tlong long3 = ByteUtil.bytesToLong(bytesLong2, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(long1, long3);\n\n\t\tbyte[] bytesLong3 = ByteUtil.longToBytes(long1, ByteOrder.LITTLE_ENDIAN);\n\t\tlong long4 = ByteUtil.bytesToLong(bytesLong3);\n\t\tassertEquals(long1, long4);\n\t}\n\n\t@Test\n\tpublic void longAndBytesBigEndianTest() {\n\t\t// 测试大端序 long 转 byte 数组\n\t\tlong long1 = RandomUtil.randomLong(Long.MAX_VALUE);\n\n\t\tByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);\n\t\tbuffer.putLong(long1);\n\t\tbyte[] bytesLongFromBuffer = buffer.array();\n\n\t\tbyte[] bytesLong = ByteUtil.longToBytes(long1, ByteOrder.BIG_ENDIAN);\n\t\tassertArrayEquals(bytesLongFromBuffer, bytesLong);\n\n\t\tlong long2 = ByteUtil.bytesToLong(bytesLong, ByteOrder.BIG_ENDIAN);\n\t\tassertEquals(long1, long2);\n\t}\n\n\t@Test\n\tpublic void floatAndBytesLittleEndianTest() {\n\t\t// 测试 long 转 byte 数组\n\t\tfloat f1 = (float) RandomUtil.randomDouble();\n\n\t\tbyte[] bytesLong = ByteUtil.floatToBytes(f1, ByteOrder.LITTLE_ENDIAN);\n\t\tfloat f2 = ByteUtil.bytesToFloat(bytesLong, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(f1, f2, 0);\n\t}\n\n\t@Test\n\tpublic void floatAndBytesBigEndianTest() {\n\t\t// 测试大端序 long 转 byte 数组\n\t\tfloat f1 = (float) RandomUtil.randomDouble();\n\n\t\tbyte[] bytesLong = ByteUtil.floatToBytes(f1, ByteOrder.BIG_ENDIAN);\n\t\tfloat f2 = ByteUtil.bytesToFloat(bytesLong, ByteOrder.BIG_ENDIAN);\n\n\t\tassertEquals(f1, f2, 0);\n\t}\n\n\t@Test\n\tpublic void shortAndBytesLittleEndianTest() {\n\t\tshort short1 = (short) RandomUtil.randomInt();\n\n\t\tbyte[] bytes = ByteUtil.shortToBytes(short1, ByteOrder.LITTLE_ENDIAN);\n\t\tshort short2 = ByteUtil.bytesToShort(bytes, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(short2, short1);\n\n\t\tbyte[] bytes2 = ByteUtil.shortToBytes(short1);\n\t\tshort short3 = ByteUtil.bytesToShort(bytes2, ByteOrder.LITTLE_ENDIAN);\n\t\tassertEquals(short3, short1);\n\n\t\tbyte[] bytes3 = ByteUtil.shortToBytes(short1, ByteOrder.LITTLE_ENDIAN);\n\t\tshort short4 = ByteUtil.bytesToShort(bytes3);\n\t\tassertEquals(short4, short1);\n\t}\n\n\t@Test\n\tpublic void shortAndBytesBigEndianTest() {\n\t\tshort short1 = 122;\n\t\tbyte[] bytes = ByteUtil.shortToBytes(short1, ByteOrder.BIG_ENDIAN);\n\t\tshort short2 = ByteUtil.bytesToShort(bytes, ByteOrder.BIG_ENDIAN);\n\n\t\tassertEquals(short2, short1);\n\t}\n\n\t@Test\n\tpublic void bytesToLongTest(){\n\t\tlong a = RandomUtil.randomLong(0, Long.MAX_VALUE);\n\t\tByteBuffer wrap = ByteBuffer.wrap(ByteUtil.longToBytes(a));\n\t\twrap.order(ByteOrder.LITTLE_ENDIAN);\n\t\tlong aLong = wrap.getLong();\n\t\tassertEquals(a, aLong);\n\n\t\twrap = ByteBuffer.wrap(ByteUtil.longToBytes(a, ByteOrder.BIG_ENDIAN));\n\t\twrap.order(ByteOrder.BIG_ENDIAN);\n\t\taLong = wrap.getLong();\n\t\tassertEquals(a, aLong);\n\t}\n\n\t@Test\n\tpublic void bytesToIntTest(){\n\t\tint a = RandomUtil.randomInt(0, Integer.MAX_VALUE);\n\t\tByteBuffer wrap = ByteBuffer.wrap(ByteUtil.intToBytes(a));\n\t\twrap.order(ByteOrder.LITTLE_ENDIAN);\n\t\tint aInt = wrap.getInt();\n\t\tassertEquals(a, aInt);\n\n\t\twrap = ByteBuffer.wrap(ByteUtil.intToBytes(a, ByteOrder.BIG_ENDIAN));\n\t\twrap.order(ByteOrder.BIG_ENDIAN);\n\t\taInt = wrap.getInt();\n\t\tassertEquals(a, aInt);\n\t}\n\n\t@Test\n\tpublic void bytesToShortTest(){\n\t\tshort a = (short) RandomUtil.randomInt(0, Short.MAX_VALUE);\n\n\t\tByteBuffer wrap = ByteBuffer.wrap(ByteUtil.shortToBytes(a));\n\t\twrap.order(ByteOrder.LITTLE_ENDIAN);\n\t\tshort aShort = wrap.getShort();\n\t\tassertEquals(a, aShort);\n\n\t\twrap = ByteBuffer.wrap(ByteUtil.shortToBytes(a, ByteOrder.BIG_ENDIAN));\n\t\twrap.order(ByteOrder.BIG_ENDIAN);\n\t\taShort = wrap.getShort();\n\t\tassertEquals(a, aShort);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/CharUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class CharUtilTest {\n\n\t@Test\n\tpublic void trimTest() {\n\t\t//此字符串中的第一个字符为不可见字符: '\\u202a'\n\t\tfinal String str = \"‪C:/Users/maple/Desktop/tone.txt\";\n\t\tassertEquals('\\u202a', str.charAt(0));\n\t\tassertTrue(CharUtil.isBlankChar(str.charAt(0)));\n\t}\n\n\t@Test\n\tpublic void isEmojiTest() {\n\t\tfinal String a = \"莉🌹\";\n\t\tassertFalse(CharUtil.isEmoji(a.charAt(0)));\n\t\tassertTrue(CharUtil.isEmoji(a.charAt(1)));\n\n\t}\n\n\t@Test\n\tpublic void isCharTest(){\n\t\tfinal char a = 'a';\n\t\tassertTrue(CharUtil.isChar(a));\n\t}\n\n\t@Test\n\tpublic void isBlankCharTest(){\n\t\tfinal char a = '\\u00A0';\n\t\tassertTrue(CharUtil.isBlankChar(a));\n\n\t\tfinal char a2 = '\\u0020';\n\t\tassertTrue(CharUtil.isBlankChar(a2));\n\n\t\tfinal char a3 = '\\u3000';\n\t\tassertTrue(CharUtil.isBlankChar(a3));\n\n\t\tfinal char a4 = '\\u0000';\n\t\tassertTrue(CharUtil.isBlankChar(a4));\n\n\t\tfinal char a5 = ' ';\n\t\tassertTrue(CharUtil.isBlankChar(a5));\n\n\t\tfinal char a6 = '\\u200c';\n\t\tassertTrue(CharUtil.isBlankChar(a6));\n\t}\n\n\t@Test\n\tpublic void toCloseCharTest(){\n\t\tassertEquals('②', CharUtil.toCloseChar('2'));\n\t\tassertEquals('Ⓜ', CharUtil.toCloseChar('M'));\n\t\tassertEquals('ⓡ', CharUtil.toCloseChar('r'));\n\t}\n\n\t@Test\n\tpublic void toCloseByNumberTest(){\n\t\tassertEquals('②', CharUtil.toCloseByNumber(2));\n\t\tassertEquals('⑫', CharUtil.toCloseByNumber(12));\n\t\tassertEquals('⑳', CharUtil.toCloseByNumber(20));\n\t}\n\n\t@Test\n\tpublic void issueI5UGSQTest(){\n\t\tchar c = '\\u3164';\n\t\tassertTrue(CharUtil.isBlankChar(c));\n\n\t\tc = '\\u2800';\n\t\tassertTrue(CharUtil.isBlankChar(c));\n\t}\n\n\t@Test\n\tpublic void issueIDFNHETest(){\n\t\t//Console.log(CharUtil.toCloseByNumber(0)); //此时会打印\"⑟\"\n\t\tassertThrows(IllegalArgumentException.class, () -> CharUtil.toCloseByNumber(0));\n\t\tassertThrows(IllegalArgumentException.class, () -> CharUtil.toCloseByNumber(-1));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ClassLoaderUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ClassLoaderUtilTest {\n\n\t@Test\n\tpublic void loadClassTest() {\n\t\tString name = ClassLoaderUtil.loadClass(\"java.lang.Thread.State\").getName();\n\t\tassertEquals(\"java.lang.Thread$State\", name);\n\n\t\tname = ClassLoaderUtil.loadClass(\"java.lang.Thread$State\").getName();\n\t\tassertEquals(\"java.lang.Thread$State\", name);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ClassUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.Objects;\n\n/**\n * {@link ClassUtil} 单元测试\n *\n * @author Looly\n *\n */\npublic class ClassUtilTest {\n\n\t@Test\n\tpublic void getClassNameTest() {\n\t\tString className = ClassUtil.getClassName(ClassUtil.class, false);\n\t\tassertEquals(\"cn.hutool.core.util.ClassUtil\", className);\n\n\t\tString simpleClassName = ClassUtil.getClassName(ClassUtil.class, true);\n\t\tassertEquals(\"ClassUtil\", simpleClassName);\n\t}\n\n\t@SuppressWarnings(\"unused\")\n\tstatic class TestClass {\n\t\tprivate String privateField;\n\t\tprotected String field;\n\n\t\tprivate void privateMethod() {\n\t\t}\n\n\t\tpublic void publicMethod() {\n\t\t}\n\t}\n\n\t@SuppressWarnings({\"unused\", \"InnerClassMayBeStatic\"})\n\tclass TestSubClass extends TestClass {\n\t\tprivate String subField;\n\n\t\tprivate void privateSubMethod() {\n\t\t}\n\n\t\tpublic void publicSubMethod() {\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void getPublicMethod() {\n\t\tMethod superPublicMethod = ClassUtil.getPublicMethod(TestSubClass.class, \"publicMethod\");\n\t\tassertNotNull(superPublicMethod);\n\t\tMethod superPrivateMethod = ClassUtil.getPublicMethod(TestSubClass.class, \"privateMethod\");\n\t\tassertNull(superPrivateMethod);\n\n\t\tMethod publicMethod = ClassUtil.getPublicMethod(TestSubClass.class, \"publicSubMethod\");\n\t\tassertNotNull(publicMethod);\n\t\tMethod privateMethod = ClassUtil.getPublicMethod(TestSubClass.class, \"privateSubMethod\");\n\t\tassertNull(privateMethod);\n\t}\n\n\t@Test\n\tpublic void getDeclaredMethod() {\n\t\tMethod noMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, \"noMethod\");\n\t\tassertNull(noMethod);\n\n\t\tMethod privateMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, \"privateMethod\");\n\t\tassertNotNull(privateMethod);\n\t\tMethod publicMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, \"publicMethod\");\n\t\tassertNotNull(publicMethod);\n\n\t\tMethod publicSubMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, \"publicSubMethod\");\n\t\tassertNotNull(publicSubMethod);\n\t\tMethod privateSubMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, \"privateSubMethod\");\n\t\tassertNotNull(privateSubMethod);\n\n\t}\n\n\t@Test\n\tpublic void getDeclaredField() {\n\t\tField noField = ClassUtil.getDeclaredField(TestSubClass.class, \"noField\");\n\t\tassertNull(noField);\n\n\t\t// 获取不到父类字段\n\t\tField field = ClassUtil.getDeclaredField(TestSubClass.class, \"field\");\n\t\tassertNull(field);\n\n\t\tField subField = ClassUtil.getDeclaredField(TestSubClass.class, \"subField\");\n\t\tassertNotNull(subField);\n\t}\n\n\t@Test\n\tpublic void getClassPathTest() {\n\t\tString classPath = ClassUtil.getClassPath();\n\t\tassertNotNull(classPath);\n\t}\n\n\t@Test\n\tpublic void getShortClassNameTest() {\n\t\tString className = \"cn.hutool.core.util.StrUtil\";\n\t\tString result = ClassUtil.getShortClassName(className);\n\t\tassertEquals(\"c.h.c.u.StrUtil\", result);\n\t}\n\n\t@Test\n\tpublic void getLocationPathTest(){\n\t\tfinal String classDir = ClassUtil.getLocationPath(ClassUtilTest.class);\n\t\tassertTrue(Objects.requireNonNull(classDir).endsWith(\"/hutool-core/target/test-classes/\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/CoordinateUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 坐标转换工具类单元测试<br>\n * 测试参考：https://github.com/wandergis/coordtransform\n *\n * @author hongzhe.qin, looly\n */\npublic class CoordinateUtilTest {\n\n\t@Test\n\tpublic void wgs84ToGcj02Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.wgs84ToGcj02(116.404, 39.915);\n\t\tassertEquals(116.41024449916938D, coordinate.getLng(), 0);\n\t\tassertEquals(39.91640428150164D, coordinate.getLat(), 0);\n\t}\n\n\t@Test\n\tpublic void gcj02ToWgs84Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.gcj02ToWgs84(116.404, 39.915);\n\t\tassertEquals(116.39775550083061D, coordinate.getLng(), 0);\n\t\tassertEquals(39.91359571849836D, coordinate.getLat(), 0);\n\t}\n\n\t@Test\n\tpublic void wgs84toBd09Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.wgs84ToBd09(116.404, 39.915);\n\t\tassertEquals(116.41662724378733D, coordinate.getLng(), 0);\n\t\tassertEquals(39.922699552216216D, coordinate.getLat(), 0);\n\t}\n\n\t@Test\n\tpublic void wgs84toBd09Test2() {\n\t\t// https://tool.lu/coordinate/\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.wgs84ToBd09(122.99395597D, 44.99804071D);\n\t\tassertEquals(123.00636516028885D, coordinate.getLng(), 0.00000000000001D);\n\t\t// 不同jdk版本、不同架构jdk, 精度有差异，数值不完全相等，这里增加精度控制delta\n\t\t// 参考：从Java Math底层实现看Arm与x86的差异：https://yikun.github.io/2020/04/10/%E4%BB%8EJava-Math%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E7%9C%8BArm%E4%B8%8Ex86%E7%9A%84%E5%B7%AE%E5%BC%82/\n\t\tassertEquals(45.00636909189589D, coordinate.getLat(), 0.00000000000001D);\n\t}\n\n\t@Test\n\tpublic void bd09toWgs84Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.bd09toWgs84(116.404, 39.915);\n\t\tassertEquals(116.3913836995125D, coordinate.getLng(), 0);\n\t\tassertEquals(39.907253214522164D, coordinate.getLat(), 0);\n\t}\n\n\t@Test\n\tpublic void gcj02ToBd09Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.gcj02ToBd09(116.404, 39.915);\n\t\tassertEquals(116.41036949371029D, coordinate.getLng(), 0);\n\t\tassertEquals(39.92133699351022D, coordinate.getLat(), 0);\n\t}\n\n\t@Test\n\tpublic void bd09toGcj02Test() {\n\t\tfinal CoordinateUtil.Coordinate coordinate = CoordinateUtil.bd09ToGcj02(116.404, 39.915);\n\t\tassertEquals(116.39762729119315D, coordinate.getLng(), 0);\n\t\tassertEquals(39.90865673957631D, coordinate.getLat(), 0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/CreditCodeUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CreditCodeUtilTest {\n\n\t@Test\n\tpublic void isCreditCodeBySimple() {\n\t\tString testCreditCode = \"91310115591693856A\";\n\t\tassertTrue(CreditCodeUtil.isCreditCodeSimple(testCreditCode));\n\t}\n\n\t@Test\n\tpublic void isCreditCode() {\n\t\tString testCreditCode = \"91310110666007217T\";\n\t\tassertTrue(CreditCodeUtil.isCreditCode(testCreditCode));\n\t}\n\n\t@Test\n\tpublic void isCreditCode2() {\n\t\t// 由于早期部分试点地区推行 法人和其他组织统一社会信用代码 较早，会存在部分代码不符合国家标准的情况。\n\t\t// 见：https://github.com/bluesky335/IDCheck\n\t\tString testCreditCode = \"91350211M00013FA1N\";\n\t\tassertFalse(CreditCodeUtil.isCreditCode(testCreditCode));\n\t}\n\n\t@Test\n\tpublic void randomCreditCode() {\n\t\tfinal String s = CreditCodeUtil.randomCreditCode();\n\t\tassertTrue(CreditCodeUtil.isCreditCode(s));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/DesensitizedUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\n/**\n * 脱敏工具类 DesensitizedUtils 安全测试\n *\n * @author dazer and nuesoft\n * @see DesensitizedUtil\n */\npublic class DesensitizedUtilTest {\n\n\t@Test\n\tpublic void desensitizedTest() {\n\t\tassertEquals(\"0\", DesensitizedUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.USER_ID));\n\t\tassertEquals(\"\", DesensitizedUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.CLEAR_TO_EMPTY));\n\t\tassertNull(DesensitizedUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.CLEAR_TO_NULL));\n\t\tassertEquals(\"段**\", DesensitizedUtil.desensitized(\"段正淳\", DesensitizedUtil.DesensitizedType.CHINESE_NAME));\n\t\tassertEquals(\"5***************1X\", DesensitizedUtil.desensitized(\"51343620000320711X\", DesensitizedUtil.DesensitizedType.ID_CARD));\n\t\tassertEquals(\"0915*****79\", DesensitizedUtil.desensitized(\"09157518479\", DesensitizedUtil.DesensitizedType.FIXED_PHONE));\n\t\tassertEquals(\"180****1999\", DesensitizedUtil.desensitized(\"18049531999\", DesensitizedUtil.DesensitizedType.MOBILE_PHONE));\n\t\tassertEquals(\"北京市海淀区马********\", DesensitizedUtil.desensitized(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.DesensitizedType.ADDRESS));\n\t\tassertEquals(\"d*************@gmail.com.cn\", DesensitizedUtil.desensitized(\"duandazhi-jack@gmail.com.cn\", DesensitizedUtil.DesensitizedType.EMAIL));\n\t\tassertEquals(\"**********\", DesensitizedUtil.desensitized(\"1234567890\", DesensitizedUtil.DesensitizedType.PASSWORD));\n\n\t\tassertEquals(\"0\", DesensitizedUtil.desensitized(\"100\", DesensitizedUtil.DesensitizedType.USER_ID));\n\t\tassertEquals(\"段**\", DesensitizedUtil.desensitized(\"段正淳\", DesensitizedUtil.DesensitizedType.CHINESE_NAME));\n\t\tassertEquals(\"5***************1X\", DesensitizedUtil.desensitized(\"51343620000320711X\", DesensitizedUtil.DesensitizedType.ID_CARD));\n\t\tassertEquals(\"0915*****79\", DesensitizedUtil.desensitized(\"09157518479\", DesensitizedUtil.DesensitizedType.FIXED_PHONE));\n\t\tassertEquals(\"180****1999\", DesensitizedUtil.desensitized(\"18049531999\", DesensitizedUtil.DesensitizedType.MOBILE_PHONE));\n\t\tassertEquals(\"北京市海淀区马********\", DesensitizedUtil.desensitized(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.DesensitizedType.ADDRESS));\n\t\tassertEquals(\"d*************@gmail.com.cn\", DesensitizedUtil.desensitized(\"duandazhi-jack@gmail.com.cn\", DesensitizedUtil.DesensitizedType.EMAIL));\n\t\tassertEquals(\"**********\", DesensitizedUtil.desensitized(\"1234567890\", DesensitizedUtil.DesensitizedType.PASSWORD));\n\t\tassertEquals(\"1101 **** **** **** 3256\", DesensitizedUtil.desensitized(\"11011111222233333256\", DesensitizedUtil.DesensitizedType.BANK_CARD));\n\t\tassertEquals(\"6227 **** **** **** 123\", DesensitizedUtil.desensitized(\"6227880100100105123\", DesensitizedUtil.DesensitizedType.BANK_CARD));\n\t\tassertEquals(\"192.*.*.*\", DesensitizedUtil.desensitized(\"192.168.1.1\", DesensitizedUtil.DesensitizedType.IPV4));\n\t\tassertEquals(\"2001:*:*:*:*:*:*:*\", DesensitizedUtil.desensitized(\"2001:0db8:86a3:08d3:1319:8a2e:0370:7344\", DesensitizedUtil.DesensitizedType.IPV6));\n\t}\n\n\t@Test\n\tpublic void userIdTest() {\n\t\tassertEquals(Long.valueOf(0L), DesensitizedUtil.userId());\n\t}\n\n\t@Test\n\tpublic void chineseNameTest() {\n\t\tassertEquals(\"段**\", DesensitizedUtil.chineseName(\"段正淳\"));\n\t}\n\n\t@Test\n\tpublic void idCardNumTest() {\n\t\tassertEquals(\"5***************1X\", DesensitizedUtil.idCardNum(\"51343620000320711X\", 1, 2));\n\t}\n\n\t@Test\n\tpublic void fixedPhoneTest() {\n\t\tassertEquals(\"0915*****79\", DesensitizedUtil.fixedPhone(\"09157518479\"));\n\t}\n\n\t@Test\n\tpublic void mobilePhoneTest() {\n\t\tassertEquals(\"180****1999\", DesensitizedUtil.mobilePhone(\"18049531999\"));\n\t}\n\n\t@Test\n\tpublic void addressTest() {\n\t\tassertEquals(\"北京市海淀区马连洼街*****\", DesensitizedUtil.address(\"北京市海淀区马连洼街道289号\", 5));\n\t\tassertEquals(\"***************\", DesensitizedUtil.address(\"北京市海淀区马连洼街道289号\", 50));\n\t\tassertEquals(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.address(\"北京市海淀区马连洼街道289号\", 0));\n\t\tassertEquals(\"北京市海淀区马连洼街道289号\", DesensitizedUtil.address(\"北京市海淀区马连洼街道289号\", -1));\n\t}\n\n\t@Test\n\tpublic void emailTest() {\n\t\tassertEquals(\"d********@126.com\", DesensitizedUtil.email(\"duandazhi@126.com\"));\n\t\tassertEquals(\"d********@gmail.com.cn\", DesensitizedUtil.email(\"duandazhi@gmail.com.cn\"));\n\t\tassertEquals(\"d*************@gmail.com.cn\", DesensitizedUtil.email(\"duandazhi-jack@gmail.com.cn\"));\n\t}\n\n\t@Test\n\tpublic void passwordTest() {\n\t\tassertEquals(\"**********\", DesensitizedUtil.password(\"1234567890\"));\n\t}\n\n\t@Test\n\tpublic void carLicenseTest() {\n\t\tassertEquals(\"\", DesensitizedUtil.carLicense(null));\n\t\tassertEquals(\"\", DesensitizedUtil.carLicense(\"\"));\n\t\tassertEquals(\"苏D4***0\", DesensitizedUtil.carLicense(\"苏D40000\"));\n\t\tassertEquals(\"陕A1****D\", DesensitizedUtil.carLicense(\"陕A12345D\"));\n\t\tassertEquals(\"京A123\", DesensitizedUtil.carLicense(\"京A123\"));\n\t}\n\n\t@Test\n\tpublic void bankCardTest(){\n\t\tassertEquals(null, DesensitizedUtil.bankCard(null));\n\t\tassertEquals(\"\", DesensitizedUtil.bankCard(\"\"));\n\t\tassertEquals(\"1234 **** **** **** **** 9\", DesensitizedUtil.bankCard(\"1234 2222 3333 4444 6789 9\"));\n\t\tassertEquals(\"1234 **** **** **** **** 91\", DesensitizedUtil.bankCard(\"1234 2222 3333 4444 6789 91\"));\n\t\tassertEquals(\"1234 **** **** **** 6789\", DesensitizedUtil.bankCard(\"1234 2222 3333 4444 6789\"));\n\t\tassertEquals(\"1234 **** **** **** 678\", DesensitizedUtil.bankCard(\"1234 2222 3333 4444 678\"));\n\n\t}\n\n    @Test\n    public void passportTest(){\n        assertEquals(null, DesensitizedUtil.passport(null));\n        assertEquals(\"\", DesensitizedUtil.passport(\"\"));\n        assertEquals(\"EM*****67\", DesensitizedUtil.passport(\"EM1234567\"));\n        assertEquals(\"*\", DesensitizedUtil.passport(\"3\"));\n    }\n\n    @Test\n    public void creditCodeTest(){\n        assertEquals(null, DesensitizedUtil.creditCode(null));\n        assertEquals(\"\", DesensitizedUtil.creditCode(\"\"));\n        assertEquals(\"9111**********CDE7\", DesensitizedUtil.creditCode(\"91110108MA01ABCDE7\"));\n    }\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/EnumUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * EnumUtil单元测试\n *\n * @author looly\n *\n */\npublic class EnumUtilTest {\n\n\t@Test\n\tpublic void getNamesTest() {\n\t\tList<String> names = EnumUtil.getNames(TestEnum.class);\n\t\tassertEquals(CollUtil.newArrayList(\"TEST1\", \"TEST2\", \"TEST3\"), names);\n\t}\n\n\t@Test\n\tpublic void getFieldValuesTest() {\n\t\tList<Object> types = EnumUtil.getFieldValues(TestEnum.class, \"type\");\n\t\tassertEquals(CollUtil.newArrayList(\"type1\", \"type2\", \"type3\"), types);\n\t}\n\n\t@Test\n\tpublic void getFieldNamesTest() {\n\t\tList<String> names = EnumUtil.getFieldNames(TestEnum.class);\n\t\tassertTrue(names.contains(\"type\"));\n\t\tassertTrue(names.contains(\"name\"));\n\t}\n\n\t@Test\n\tpublic void getByTest() {\n\t\t// 枚举中字段互相映射使用\n\t\tTestEnum testEnum = EnumUtil.getBy(TestEnum::ordinal, 1);\n\t\tassertEquals(\"TEST2\", testEnum.name());\n\t}\n\n\t@Test\n\tpublic void getFieldByTest() {\n\t\t// 枚举中字段互相映射使用\n\t\tString type = EnumUtil.getFieldBy(TestEnum::getType, Enum::ordinal, 1);\n\t\tassertEquals(\"type2\", type);\n\n\t\tint ordinal = EnumUtil.getFieldBy(TestEnum::ordinal, Enum::ordinal, 1);\n\t\tassertEquals(1, ordinal);\n\t}\n\n\t@Test\n\tpublic void likeValueOfTest() {\n\t\tTestEnum value = EnumUtil.likeValueOf(TestEnum.class, \"type2\");\n\t\tassertEquals(TestEnum.TEST2, value);\n\t}\n\n\t@Test\n\tpublic void getEnumMapTest() {\n\t\tMap<String,TestEnum> enumMap = EnumUtil.getEnumMap(TestEnum.class);\n\t\tassertEquals(TestEnum.TEST1, enumMap.get(\"TEST1\"));\n\t}\n\n\t@Test\n\tpublic void getNameFieldMapTest() {\n\t\tMap<String, Object> enumMap = EnumUtil.getNameFieldMap(TestEnum.class, \"type\");\n\t\tassert enumMap != null;\n\t\tassertEquals(\"type1\", enumMap.get(\"TEST1\"));\n\t}\n\n\t/**\n\t * 测试枚举类静态初始化中调用 EnumUtil 不会导致 Recursive update 异常\n\t * fix issue#IDQYJK\n\t */\n\t@Test\n\tpublic void getFieldValuesRecursiveTest() {\n\t\t// SelfRefEnum 在静态初始化时调用了 EnumUtil.getNames，\n\t\t// 修复前会抛出 IllegalStateException: Recursive update\n\t\t// 修复后应正常返回结果\n\t\tList<Object> values = EnumUtil.getFieldValues(SelfRefEnum.class, \"label\");\n\t\tassertNotNull(values);\n\t\tassertEquals(3, values.size());\n\t}\n\n\tpublic enum TestEnum{\n\t\tTEST1(\"type1\"), TEST2(\"type2\"), TEST3(\"type3\");\n\n\t\tTestEnum(String type) {\n\t\t\tthis.type = type;\n\t\t}\n\n\t\tprivate final String type;\n\t\t@SuppressWarnings(\"unused\")\n\t\tprivate String name;\n\n\t\tpublic String getType() {\n\t\t\treturn this.type;\n\t\t}\n\n\t\tpublic String getName() {\n\t\t\treturn this.name;\n\t\t}\n\t}\n\n\t/**\n\t * 静态初始化中使用 EnumUtil 的枚举，用于测试 fix issue#IDQYJK\n\t */\n\tpublic enum SelfRefEnum {\n\t\tA(\"labelA\"), B(\"labelB\"), C(\"labelC\");\n\n\t\t// 静态初始化块中调用 EnumUtil，触发 ConcurrentHashMap.computeIfAbsent 的递归场景\n\t\tstatic final List<String> NAMES = EnumUtil.getNames(SelfRefEnum.class);\n\n\t\tprivate final String label;\n\n\t\tSelfRefEnum(String label) {\n\t\t\tthis.label = label;\n\t\t}\n\n\t\tpublic String getLabel() {\n\t\t\treturn label;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/EscapeUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class EscapeUtilTest {\n\n\t@Test\n\tpublic void escapeHtml4Test() {\n\t\tString escapeHtml4 = EscapeUtil.escapeHtml4(\"<a>你好</a>\");\n\t\tassertEquals(\"&lt;a&gt;你好&lt;/a&gt;\", escapeHtml4);\n\n\t\tString result = EscapeUtil.unescapeHtml4(\"&#25391;&#33633;&#22120;&#31867;&#22411;\");\n\t\tassertEquals(\"振荡器类型\", result);\n\n\t\tString escape = EscapeUtil.escapeHtml4(\"*@-_+./(123你好)\");\n\t\tassertEquals(\"*@-_+./(123你好)\", escape);\n\t}\n\n\t@Test\n\tpublic void escapeTest(){\n\t\tString str = \"*@-_+./(123你好)ABCabc\";\n\t\tString escape = EscapeUtil.escape(str);\n\t\tassertEquals(\"*@-_+./%28123%u4f60%u597d%29ABCabc\", escape);\n\n\t\tString unescape = EscapeUtil.unescape(escape);\n\t\tassertEquals(str, unescape);\n\t}\n\n\t@Test\n\tpublic void escapeAllTest(){\n\t\tString str = \"*@-_+./(123你好)ABCabc\";\n\n\t\tString escape = EscapeUtil.escapeAll(str);\n\t\tassertEquals(\"%2a%40%2d%5f%2b%2e%2f%28%31%32%33%u4f60%u597d%29%41%42%43%61%62%63\", escape);\n\n\t\tString unescape = EscapeUtil.unescape(escape);\n\t\tassertEquals(str, unescape);\n\t}\n\n\t/**\n\t * https://gitee.com/chinabugotech/hutool/issues/I49JU8\n\t */\n\t@Test\n\tpublic void escapeAllTest2(){\n\t\tString str = \"٩\";\n\n\t\tString escape = EscapeUtil.escapeAll(str);\n\t\tassertEquals(\"%u0669\", escape);\n\n\t\tString unescape = EscapeUtil.unescape(escape);\n\t\tassertEquals(str, unescape);\n\t}\n\n\t@Test\n\tpublic void escapeSingleQuotesTest(){\n\t\t// 单引号不做转义\n\t\tString str = \"'some text with single quotes'\";\n\t\tfinal String s = EscapeUtil.escapeHtml4(str);\n\t\tassertEquals(\"'some text with single quotes'\", s);\n\t}\n\n\t@Test\n\tpublic void unescapeSingleQuotesTest(){\n\t\tString str = \"&apos;some text with single quotes&apos;\";\n\t\tfinal String s = EscapeUtil.unescapeHtml4(str);\n\t\tassertEquals(\"'some text with single quotes'\", s);\n\t}\n\n\t@Test\n\tpublic void escapeXmlTest(){\n\t\tfinal String a = \"<>\";\n\t\tfinal String escape = EscapeUtil.escapeXml(a);\n\t\tassertEquals(\"&lt;&gt;\", escape);\n\t\tassertEquals(\"中文“双引号”\", EscapeUtil.escapeXml(\"中文“双引号”\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeNull() {\n\t\tassertNull(EscapeUtil.unescape(null));\n\t}\n\n\t@Test\n\tvoid testUnescapeEmpty() {\n\t\tassertEquals(\"\", EscapeUtil.unescape(\"\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeBlank() {\n\t\tassertEquals(\"   \", EscapeUtil.unescape(\"   \"));\n\t}\n\n\t@Test\n\tvoid testUnescapeAsciiCharacters() {\n\t\t// 测试ASCII字符转义\n\t\tassertEquals(\"hello\", EscapeUtil.unescape(\"hello\"));\n\t\tassertEquals(\"test space\", EscapeUtil.unescape(\"test%20space\"));\n\t\tassertEquals(\"A\", EscapeUtil.unescape(\"%41\"));\n\t\tassertEquals(\"a\", EscapeUtil.unescape(\"%61\"));\n\t\tassertEquals(\"0\", EscapeUtil.unescape(\"%30\"));\n\t\tassertEquals(\"!\", EscapeUtil.unescape(\"%21\"));\n\t\tassertEquals(\"@\", EscapeUtil.unescape(\"%40\"));\n\t\tassertEquals(\"#\", EscapeUtil.unescape(\"%23\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeUnicodeCharacters() {\n\t\t// 测试Unicode字符转义\n\t\tassertEquals(\"中\", EscapeUtil.unescape(\"%u4E2D\"));\n\t\tassertEquals(\"文\", EscapeUtil.unescape(\"%u6587\"));\n\t\tassertEquals(\"测\", EscapeUtil.unescape(\"%u6D4B\"));\n\t\tassertEquals(\"试\", EscapeUtil.unescape(\"%u8BD5\"));\n\t\tassertEquals(\"😊\", EscapeUtil.unescape(\"%uD83D%uDE0A\")); // 笑脸表情\n\t}\n\n\t@Test\n\tvoid testUnescapeMixedContent() {\n\t\t// 测试混合内容\n\t\tassertEquals(\"Hello 世界!\", EscapeUtil.unescape(\"Hello%20%u4E16%u754C%21\"));\n\t\tassertEquals(\"测试: 100%\", EscapeUtil.unescape(\"%u6D4B%u8BD5%3A%20100%25\"));\n\t\tassertEquals(\"a+b=c\", EscapeUtil.unescape(\"a%2Bb%3Dc\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeIncompleteEscapeSequences() {\n\t\t// 测试不完整的转义序列\n\t\tassertEquals(\"test%\", EscapeUtil.unescape(\"test%\"));\n\t\tassertEquals(\"test%u\", EscapeUtil.unescape(\"test%u\"));\n\t\tassertEquals(\"test%u1\", EscapeUtil.unescape(\"test%u1\"));\n\t\tassertEquals(\"test%u12\", EscapeUtil.unescape(\"test%u12\"));\n\t\tassertEquals(\"test%u123\", EscapeUtil.unescape(\"test%u123\"));\n\t\tassertEquals(\"test%1\", EscapeUtil.unescape(\"test%1\"));\n\t\tassertEquals(\"test%2\", EscapeUtil.unescape(\"test%2\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeEdgeCases() {\n\t\t// 测试边界情况\n\t\tassertEquals(\"%\", EscapeUtil.unescape(\"%\"));\n\t\tassertEquals(\"%u\", EscapeUtil.unescape(\"%u\"));\n\t\tassertEquals(\"%%\", EscapeUtil.unescape(\"%%\"));\n\t\tassertEquals(\"%u%\", EscapeUtil.unescape(\"%u%\"));\n\t\tassertEquals(\"100% complete\", EscapeUtil.unescape(\"100%25%20complete\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeMultipleEscapeSequences() {\n\t\t// 测试多个连续的转义序列\n\t\tassertEquals(\"ABC\", EscapeUtil.unescape(\"%41%42%43\"));\n\t\tassertEquals(\"中文测试\", EscapeUtil.unescape(\"%u4E2D%u6587%u6D4B%u8BD5\"));\n\t\tassertEquals(\"A 中 B\", EscapeUtil.unescape(\"%41%20%u4E2D%20%42\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeSpecialCharacters() {\n\t\t// 测试特殊字符\n\t\tassertEquals(\"\\n\", EscapeUtil.unescape(\"%0A\"));\n\t\tassertEquals(\"\\r\", EscapeUtil.unescape(\"%0D\"));\n\t\tassertEquals(\"\\t\", EscapeUtil.unescape(\"%09\"));\n\t\tassertEquals(\" \", EscapeUtil.unescape(\"%20\"));\n\t\tassertEquals(\"<\", EscapeUtil.unescape(\"%3C\"));\n\t\tassertEquals(\">\", EscapeUtil.unescape(\"%3E\"));\n\t\tassertEquals(\"&\", EscapeUtil.unescape(\"%26\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeComplexScenario() {\n\t\t// 测试复杂场景\n\t\tfinal String original = \"Hello 世界! 这是测试。Email: test@example.com\";\n\t\tfinal String escaped = \"Hello%20%u4E16%u754C%21%20%u8FD9%u662F%u6D4B%u8BD5%u3002Email%3A%20test%40example.com\";\n\t\tassertEquals(original, EscapeUtil.unescape(escaped));\n\t}\n\n\t@Test\n\tvoid testUnescapeWithIncompleteAtEnd() {\n\t\t// 测试末尾有不完整转义序列\n\t\tassertEquals(\"normal%\", EscapeUtil.unescape(\"normal%\"));\n\t\tassertEquals(\"normal%u\", EscapeUtil.unescape(\"normal%u\"));\n\t\tassertEquals(\"normal%u1\", EscapeUtil.unescape(\"normal%u1\"));\n\t\tassertEquals(\"normal%1\", EscapeUtil.unescape(\"normal%1\"));\n\t}\n\n\t@Test\n\tvoid testUnescapeUppercaseHex() {\n\t\t// 测试大写十六进制\n\t\tassertEquals(\"A\", EscapeUtil.unescape(\"%41\"));\n\t\tassertEquals(\"A\", EscapeUtil.unescape(\"%41\"));\n\t\tassertEquals(\"中\", EscapeUtil.unescape(\"%u4E2D\"));\n\t\tassertEquals(\"中\", EscapeUtil.unescape(\"%u4E2D\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/HashUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class HashUtilTest {\n\n\t@Test\n\tpublic void cityHash128Test(){\n\t\tString s=\"Google发布的Hash计算算法：CityHash64 与 CityHash128\";\n\t\tfinal long[] hash = HashUtil.cityHash128(StrUtil.utf8Bytes(s));\n\t\tassertEquals(0x5944f1e788a18db0L, hash[0]);\n\t\tassertEquals(0xc2f68d8b2bf4a5cfL, hash[1]);\n\t}\n\n\t@Test\n\tpublic void cityHash64Test(){\n\t\tString s=\"Google发布的Hash计算算法：CityHash64 与 CityHash128\";\n\t\tfinal long hash = HashUtil.cityHash64(StrUtil.utf8Bytes(s));\n\t\tassertEquals(0x1d408f2bbf967e2aL, hash);\n\t}\n\n\t@Test\n\tpublic void cityHash32Test(){\n\t\tString s=\"Google发布的Hash计算算法：CityHash64 与 CityHash128\";\n\t\tfinal int hash = HashUtil.cityHash32(StrUtil.utf8Bytes(s));\n\t\tassertEquals(0xa8944fbe, hash);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/HexUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigInteger;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * HexUtil单元测试\n * @author Looly\n *\n */\npublic class HexUtilTest {\n\n\t@Test\n\tpublic void hexStrTest(){\n\t\tfinal String str = \"我是一个字符串\";\n\n\t\tfinal String hex = HexUtil.encodeHexStr(str, CharsetUtil.CHARSET_UTF_8);\n\t\tfinal String decodedStr = HexUtil.decodeHexStr(hex);\n\n\t\tassertEquals(str, decodedStr);\n\t}\n\n\t@Test\n\tpublic void issueI50MI6Test(){\n\t\tfinal String s = HexUtil.encodeHexStr(\"烟\".getBytes(StandardCharsets.UTF_16BE));\n\t\tassertEquals(\"70df\", s);\n\t}\n\n\t@Test\n\tpublic void toUnicodeHexTest() {\n\t\tString unicodeHex = HexUtil.toUnicodeHex('\\u2001');\n\t\tassertEquals(\"\\\\u2001\", unicodeHex);\n\n\t\tunicodeHex = HexUtil.toUnicodeHex('你');\n\t\tassertEquals(\"\\\\u4f60\", unicodeHex);\n\t}\n\n\t@Test\n\tpublic void isHexNumberTest() {\n\t\tassertTrue(HexUtil.isHexNumber(\"0\"));\n\t\tassertTrue(HexUtil.isHexNumber(\"002c\"));\n\n\t\tString a = \"0x3544534F444\";\n\t\tassertTrue(HexUtil.isHexNumber(a));\n\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I62H7K\n\t\ta = \"0x0000000000000001158e460913d00000\";\n\t\tassertTrue(HexUtil.isHexNumber(a));\n\n\t\t// 错误的\n\t\ta = \"0x0000001000T00001158e460913d00000\";\n\t\tassertFalse(HexUtil.isHexNumber(a));\n\n\t\t// 错误的,https://github.com/chinabugotech/hutool/issues/2857\n\t\ta = \"-1\";\n\t\tassertFalse(HexUtil.isHexNumber(a));\n\t}\n\n\t@Test\n\tpublic void isHexNumberTest2() {\n\t\tassertFalse(HexUtil.isHexNumber(\"\"));\n\t\tassertFalse(HexUtil.isHexNumber(null));\n\t}\n\n\t@Test\n\tpublic void decodeTest(){\n\t\tfinal String str = \"e8c670380cb220095268f40221fc748fa6ac39d6e930e63c30da68bad97f885d\";\n\t\tassertArrayEquals(HexUtil.decodeHex(str),\n\t\t\t\tHexUtil.decodeHex(str.toUpperCase()));\n\t}\n\n\t@Test\n\tpublic void formatHexTest(){\n\t\tfinal String hex = \"e8c670380cb220095268f40221fc748fa6ac39d6e930e63c30da68bad97f885d\";\n\t\tfinal String formatHex = HexUtil.format(hex);\n\t\tassertEquals(\"e8 c6 70 38 0c b2 20 09 52 68 f4 02 21 fc 74 8f a6 ac 39 d6 e9 30 e6 3c 30 da 68 ba d9 7f 88 5d\", formatHex);\n\t}\n\n\t@Test\n\tpublic void formatHexTest2(){\n\t\tfinal String hex = \"e8c670380cb220095268f40221fc748fa6\";\n\t\tfinal String formatHex = HexUtil.format(hex, \"0x\");\n\t\tassertEquals(\"0xe8 0xc6 0x70 0x38 0x0c 0xb2 0x20 0x09 0x52 0x68 0xf4 0x02 0x21 0xfc 0x74 0x8f 0xa6\", formatHex);\n\t}\n\n\t@Test\n\tpublic void decodeHexTest(){\n\t\tfinal String s = HexUtil.encodeHexStr(\"6\");\n\t\tfinal String s1 = HexUtil.decodeHexStr(s);\n\t\tassertEquals(\"6\", s1);\n\t}\n\n\t@Test\n\tpublic void hexToIntTest() {\n\t\tfinal String hex1 = \"FF\";\n\t\tassertEquals(255, HexUtil.hexToInt(hex1));\n\t\tfinal String hex2 = \"0xFF\";\n\t\tassertEquals(255, HexUtil.hexToInt(hex2));\n\t\tfinal String hex3 = \"#FF\";\n\t\tassertEquals(255, HexUtil.hexToInt(hex3));\n\t}\n\n\t@Test\n\tpublic void hexToLongTest() {\n\t\tfinal String hex1 = \"FF\";\n\t\tassertEquals(255L, HexUtil.hexToLong(hex1));\n\t\tfinal String hex2 = \"0xFF\";\n\t\tassertEquals(255L, HexUtil.hexToLong(hex2));\n\t\tfinal String hex3 = \"#FF\";\n\t\tassertEquals(255L, HexUtil.hexToLong(hex3));\n\t}\n\n\t@Test\n\tpublic void hexToFloatTest() {\n\t\t//测试正常浮点数值\n\t\tfloat value1 = 1.5f;\n\t\tString hex1 = HexUtil.toHex(value1);\n\t\tassertEquals(value1, HexUtil.hexToFloat(hex1));\n\n\t\t//测试负数\n\t\tfloat value2 = -1.5f;\n\t\tString hex2 = HexUtil.toHex(value2);\n\t\tassertEquals(value2, HexUtil.hexToFloat(hex2));\n\n\t\t//测试科学计数法值\n\t\tfloat value3 = 1.23456789e-5f;\n\t\tString hex3 = HexUtil.toHex(value3);\n\t\tassertEquals(value3, HexUtil.hexToFloat(hex3));\n\n\t\t//测试十六进制前缀\n\t\tassertEquals(1.5f, HexUtil.hexToFloat(\"0x3fc00000\"));\n\t\tassertEquals(1.5f, HexUtil.hexToFloat(\"#3fc00000\"));\n\t}\n\n\t@Test\n\tpublic void hexToDoubleTest() {\n\t\t//测试正常双精度浮点数值\n\t\tdouble value1 = 1.5;\n\t\tString hex1 = HexUtil.toHex(value1);\n\t\tassertEquals(value1, HexUtil.hexToDouble(hex1));\n\n\t\t//测试负数\n\t\tdouble value3 = -1.5;\n\t\tString hex3 = HexUtil.toHex(value3);\n\t\tassertEquals(value3, HexUtil.hexToDouble(hex3));\n\n\t\t//测试高精度数值\n\t\tdouble value4 = Math.PI;\n\t\tString hex4 = HexUtil.toHex(value4);\n\t\tassertEquals(value4, HexUtil.hexToDouble(hex4));\n\n\t\t//测试科学计数法值\n\t\tdouble value5 = 1.23456789012345e-10;\n\t\tString hex5 = HexUtil.toHex(value5);\n\t\tassertEquals(value5, HexUtil.hexToDouble(hex5));\n\n\t\t//测试十六进制前缀\n\t\tassertEquals(1.5, HexUtil.hexToDouble(\"0x3ff8000000000000\"));\n\t\tassertEquals(1.5, HexUtil.hexToDouble(\"#3ff8000000000000\"));\n\t}\n\n\t@Test\n\tpublic void toBigIntegerTest() {\n\t\tfinal String hex1 = \"FF\";\n\t\tassertEquals(new BigInteger(\"FF\", 16), HexUtil.toBigInteger(hex1));\n\t\tfinal String hex2 = \"0xFF\";\n\t\tassertEquals(new BigInteger(\"FF\", 16), HexUtil.toBigInteger(hex2));\n\t\tfinal String hex3 = \"#FF\";\n\t\tassertEquals(new BigInteger(\"FF\", 16), HexUtil.toBigInteger(hex3));\n\t}\n\n\t@Test\n\tpublic void testFormatEmpty() {\n\t\tString result = HexUtil.format(\"\");\n\t\tassertEquals(\"\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatSingleChar() {\n\t\tString result = HexUtil.format(\"1\");\n\t\tassertEquals(\"1\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatOddLength() {\n\t\tString result = HexUtil.format(\"123\");\n\t\tassertEquals(\"12 3\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatWithPrefixSingleChar() {\n\t\tString result = HexUtil.format(\"1\", \"0x\");\n\t\tassertEquals(\"0x1\", result);\n\t}\n\n\t@Test\n\tpublic void testFormatWithPrefixOddLength() {\n\t\tString result = HexUtil.format(\"123\", \"0x\");\n\t\tassertEquals(\"0x12 0x3\", result);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.ConcurrentHashSet;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.Snowflake;\nimport cn.hutool.core.thread.ThreadUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CountDownLatch;\n\n/**\n * {@link IdUtil} 单元测试\n *\n * @author looly\n *\n */\npublic class IdUtilTest {\n\n\t@Test\n\tpublic void randomUUIDTest() {\n\t\tString simpleUUID = IdUtil.simpleUUID();\n\t\tassertEquals(32, simpleUUID.length());\n\n\t\tString randomUUID = IdUtil.randomUUID();\n\t\tassertEquals(36, randomUUID.length());\n\t}\n\n\t@Test\n\tpublic void fastUUIDTest() {\n\t\tString simpleUUID = IdUtil.fastSimpleUUID();\n\t\tassertEquals(32, simpleUUID.length());\n\n\t\tString randomUUID = IdUtil.fastUUID();\n\t\tassertEquals(36, randomUUID.length());\n\t}\n\n\t/**\n\t * UUID的性能测试\n\t */\n\t@Test\n\t@Disabled\n\tpublic void benchTest() {\n\t\tTimeInterval timer = DateUtil.timer();\n\t\tfor (int i = 0; i < 1000000; i++) {\n\t\t\tIdUtil.simpleUUID();\n\t\t}\n\t\tConsole.log(timer.interval());\n\n\t\ttimer.restart();\n\t\tfor (int i = 0; i < 1000000; i++) {\n\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\tUUID.randomUUID().toString().replace(\"-\", \"\");\n\t\t}\n\t\tConsole.log(timer.interval());\n\t}\n\n\t@Test\n\tpublic void objectIdTest() {\n\t\tString id = IdUtil.objectId();\n\t\tassertEquals(24, id.length());\n\t}\n\n\t@Test\n\tpublic void getSnowflakeTest() {\n\t\tSnowflake snowflake = IdUtil.getSnowflake(1, 1);\n\t\tlong id = snowflake.nextId();\n\t\tassertTrue(id > 0);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void snowflakeBenchTest() {\n\t\tfinal Set<Long> set = new ConcurrentHashSet<>();\n\t\tfinal Snowflake snowflake = IdUtil.getSnowflake(1, 1);\n\n\t\t//线程数\n\t\tint threadCount = 100;\n\t\t//每个线程生成的ID数\n\t\tfinal int idCountPerThread = 10000;\n\t\tfinal CountDownLatch latch = new CountDownLatch(threadCount);\n\t\tfor(int i =0; i < threadCount; i++) {\n\t\t\tThreadUtil.execute(() -> {\n\t\t\t\tfor(int i1 = 0; i1 < idCountPerThread; i1++) {\n\t\t\t\t\tlong id = snowflake.nextId();\n\t\t\t\t\tset.add(id);\n//\t\t\t\t\t\tConsole.log(\"Add new id: {}\", id);\n\t\t\t\t}\n\t\t\t\tlatch.countDown();\n\t\t\t});\n\t\t}\n\n\t\t//等待全部线程结束\n\t\ttry {\n\t\t\tlatch.await();\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\tassertEquals(threadCount * idCountPerThread, set.size());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void snowflakeBenchTest2() {\n\t\tfinal Set<Long> set = new ConcurrentHashSet<>();\n\n\t\t//线程数\n\t\tint threadCount = 100;\n\t\t//每个线程生成的ID数\n\t\tfinal int idCountPerThread = 10000;\n\t\tfinal CountDownLatch latch = new CountDownLatch(threadCount);\n\t\tfor(int i =0; i < threadCount; i++) {\n\t\t\tThreadUtil.execute(() -> {\n\t\t\t\tfor(int i1 = 0; i1 < idCountPerThread; i1++) {\n\t\t\t\t\tlong id = IdUtil.getSnowflake(1, 1).nextId();\n\t\t\t\t\tset.add(id);\n//\t\t\t\t\t\tConsole.log(\"Add new id: {}\", id);\n\t\t\t\t}\n\t\t\t\tlatch.countDown();\n\t\t\t});\n\t\t}\n\n\t\t//等待全部线程结束\n\t\ttry {\n\t\t\tlatch.await();\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\tassertEquals(threadCount * idCountPerThread, set.size());\n\t}\n\n\t@Test\n\tpublic void getDataCenterIdTest(){\n\t\t//按照mac地址算法拼接的算法，maxDatacenterId应该是0xffffffffL>>6-1此处暂时按照0x7fffffffffffffffL-1，防止最后取模溢出\n\t\tfinal long dataCenterId = IdUtil.getDataCenterId(Long.MAX_VALUE);\n\t\tassertTrue(dataCenterId >= 0);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 身份证单元测试\n *\n * @author Looly\n *\n */\npublic class IdcardUtilTest {\n\n\tprivate static final String ID_18 = \"321083197812162119\";\n\t/**\n\t * 新版外国人永久居留身份证号码\n\t */\n\tprivate static final String FOREIGN_ID_18 = \"932682198501010017\";\n\tprivate static final String ID_15 = \"150102880730303\";\n\n\t@Test\n\tpublic void isValidCardTest() {\n\t\tboolean valid = IdcardUtil.isValidCard(ID_18);\n\t\tassertTrue(valid);\n\n\t\tboolean valid15 = IdcardUtil.isValidCard(ID_15);\n\t\tassertTrue(valid15);\n\n\t\tassertTrue(IdcardUtil.isValidCard(FOREIGN_ID_18));\n\n\t\t// 无效\n\t\tString idCard = \"360198910283844\";\n\t\tassertFalse(IdcardUtil.isValidCard(idCard));\n\n\t\t// 生日无效\n\t\tidCard = \"201511221897205960\";\n\t\tassertFalse(IdcardUtil.isValidCard(idCard));\n\n\t\t// 生日无效\n\t\tidCard = \"815727834224151\";\n\t\tassertFalse(IdcardUtil.isValidCard(idCard));\n\t}\n\n\t@Test\n\tpublic void convert15To18Test() {\n\t\tString convert15To18 = IdcardUtil.convert15To18(ID_15);\n\t\tassertEquals(\"150102198807303035\", convert15To18);\n\n\t\tString convert15To18Second = IdcardUtil.convert15To18(\"330102200403064\");\n\t\tassertEquals(\"33010219200403064X\", convert15To18Second);\n\t}\n\n\t@Test\n\tpublic void convert18To15Test() {\n\t\tString idcard15 = IdcardUtil.convert18To15(\"150102198807303035\");\n\t\tassertEquals(ID_15, idcard15);\n\t}\n\n\t@Test\n\tpublic void getAgeByIdCardTest() {\n\t\tDateTime date = DateUtil.parse(\"2017-04-10\");\n\n\t\tint age = IdcardUtil.getAgeByIdCard(ID_18, date);\n\t\tassertEquals(age, 38);\n\t\tassertEquals(IdcardUtil.getAgeByIdCard(FOREIGN_ID_18, date), 32);\n\n\t\tint age2 = IdcardUtil.getAgeByIdCard(ID_15, date);\n\t\tassertEquals(age2, 28);\n\t}\n\n\t@Test\n\tpublic void issue3651Test() {\n\t\tDateTime date = DateUtil.parse(\"2014-07-11\");\n\t\tint age = IdcardUtil.getAgeByIdCard(\"321083200807112111\", date);\n\t\tassertEquals(5, age);\n\n\t\tdate = DateUtil.parse(\"2014-07-31\");\n\t\tage = IdcardUtil.getAgeByIdCard(\"321083200807312113\", date);\n\t\tassertEquals(5, age);\n\t}\n\n\t@Test\n\tpublic void getBirthByIdCardTest() {\n\t\tString birth = IdcardUtil.getBirthByIdCard(ID_18);\n\t\tassertEquals(birth, \"19781216\");\n\n\t\tString birth2 = IdcardUtil.getBirthByIdCard(ID_15);\n\t\tassertEquals(birth2, \"19880730\");\n\t}\n\n\t@Test\n\tpublic void getProvinceByIdCardTest() {\n\t\tString province = IdcardUtil.getProvinceByIdCard(ID_18);\n\t\tassertEquals(province, \"江苏\");\n\n\t\tString province2 = IdcardUtil.getProvinceByIdCard(ID_15);\n\t\tassertEquals(province2, \"内蒙古\");\n\t}\n\n\t@Test\n\tpublic void getCityCodeByIdCardTest() {\n\t\tString codeByIdCard = IdcardUtil.getCityCodeByIdCard(ID_18);\n\t\tassertEquals(\"3210\", codeByIdCard);\n\t}\n\n\t@Test\n\tpublic void getDistrictCodeByIdCardTest() {\n\t\tString codeByIdCard = IdcardUtil.getDistrictCodeByIdCard(ID_18);\n\t\tassertEquals(\"321083\", codeByIdCard);\n\t}\n\n\t@Test\n\tpublic void getGenderByIdCardTest() {\n\t\tint gender = IdcardUtil.getGenderByIdCard(ID_18);\n\t\tassertEquals(1, gender);\n\t}\n\n\t@Test\n\tpublic void isValidCard18Test(){\n\t\tboolean isValidCard18 = IdcardUtil.isValidCard18(\"3301022011022000D6\");\n\t\tassertFalse(isValidCard18);\n\n\t\t// 不忽略大小写情况下，X严格校验必须大写\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"33010219200403064x\", false);\n\t\tassertFalse(isValidCard18);\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"33010219200403064X\", false);\n\t\tassertTrue(isValidCard18);\n\n\t\t// 非严格校验下大小写皆可\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"33010219200403064x\");\n\t\tassertTrue(isValidCard18);\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"33010219200403064X\");\n\t\tassertTrue(isValidCard18);\n\n\t\t// 香港人在大陆身份证\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"81000019980902013X\");\n\t\tassertTrue(isValidCard18);\n\n\t\t// 澳门人在大陆身份证\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"820000200009100032\");\n\t\tassertTrue(isValidCard18);\n\n\t\t// 台湾人在大陆身份证\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"830000200209060065\");\n\t\tassertTrue(isValidCard18);\n\n\t\t// 新版外国人永久居留身份证\n\t\tisValidCard18 = IdcardUtil.isValidCard18(\"932682198501010017\");\n\t\tassertTrue(isValidCard18);\n\t}\n\n\t@Test\n\tpublic void isValidHKCardIdTest(){\n\t\tString hkCard=\"P174468(6)\";\n\t\tboolean flag=IdcardUtil.isValidHKCard(hkCard);\n\t\tassertTrue(flag);\n\t}\n\n\t@Test\n\tpublic void isValidTWCardIdTest() {\n\t\tString twCard = \"B221690311\";\n\t\tboolean flag = IdcardUtil.isValidTWCard(twCard);\n\t\tassertTrue(flag);\n\t\tString errTwCard1 = \"M517086311\";\n\t\tflag = IdcardUtil.isValidTWCard(errTwCard1);\n\t\tassertFalse(flag);\n\t\tString errTwCard2 = \"B2216903112\";\n\t\tflag = IdcardUtil.isValidTWCard(errTwCard2);\n\t\tassertFalse(flag);\n\t}\n\n\t@Test\n\tpublic void issueI88YKMTest() {\n\t\tassertTrue(IdcardUtil.isValidCard(\"111111111111111\"));\n\t}\n\n\t@Test\n\tpublic void issueIAFOLITest() {\n\t\tString idcard = \"H01487002\";\n\t\tassertFalse(IdcardUtil.isValidHKCard(idcard));\n\t\tassertNull(IdcardUtil.isValidCard10(idcard));\n\t\tassertFalse(IdcardUtil.isValidCard(idcard));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/Issue3136Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.util;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/3136\n */\npublic class Issue3136Test {\n\n\t/**\n\t * 此用例中，message节点无content，理解为空节点，转换为map后，此节点值为\"\"，转为对象时，理应为null\n\t */\n\t@Test\n\tpublic void xmlToBeanTest() {\n\t\tfinal String xmlStr = \"<?xml version=\\\"1.0\\\" encoding=\\\"gbk\\\" ?><response><code>02</code><message></message></response>\";\n\t\tfinal SmsRes smsRes = XmlUtil.xmlToBean(XmlUtil.parseXml(xmlStr).getDocumentElement(), SmsRes.class);\n\n\t\tassertEquals(\"02\", smsRes.getCode());\n\t\tassertNotNull(smsRes.getMessage());\n\t\tassertEquals(new Message(), smsRes.getMessage());\n\t}\n\n\t@Data\n\tstatic class SmsRes {\n\t\t/**\n\t\t * 状态码.\n\t\t */\n\t\tprivate String code;\n\n\t\t/**\n\t\t * 消息.\n\t\t */\n\t\tprivate Message message;\n\t}\n\n\t@Data\n\tstatic class Message {\n\n\t\t/**\n\t\t * 消息项.\n\t\t */\n\t\tprivate List<MessageItem> item = new ArrayList<>();\n\t}\n\n\t@Data\n\tstatic class MessageItem {\n\n\t\t/**\n\t\t * 手机号.\n\t\t */\n\t\tprivate String desmobile;\n\t\t/**\n\t\t * 消息id.\n\t\t */\n\t\tprivate String msgid;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/Issue3423Test.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.DecimalFormat;\nimport java.text.NumberFormat;\nimport java.text.ParseException;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class Issue3423Test {\n\n\t@Test\n\tpublic void toBigDecimalOfNaNTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tNumberUtil.toBigDecimal(\"NaN\");\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void toBigDecimalOfNaNTest2() throws ParseException {\n\t\tfinal NumberFormat format = NumberFormat.getInstance();\n\t\t((DecimalFormat) format).setParseBigDecimal(true);\n\t\tfinal Number naN = format.parse(\"NaN\");\n\t\tConsole.log(naN.getClass());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/Issue3516Test.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Type;\nimport java.util.function.Function;\n\npublic class Issue3516Test {\n\n\t@Test\n\tpublic void getTypeArgumentTest() {\n\t\tfinal Type typeArgument = TypeUtil.getTypeArgument(Demo.class, 0);\n\t\tassertEquals(B.class, typeArgument);\n\t}\n\n\tstatic class Demo implements A2B{\n\t\t@Override\n\t\tpublic A apply(B arg0) {\n\t\t\tfinal A a = new A();\n\t\t\treturn a;\n\t\t}\n\t}\n\n\tstatic class A {\n\t\tprivate String name;\n\t}\n\n\tstatic class B {\n\t\tprivate String name;\n\t}\n\n\tinterface A2B extends Function<B, A> {\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/Issue3660Test.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class Issue3660Test {\n\t@Test\n\tpublic void splitTest() {\n\t\tList<String> split = StrUtil.split(\"\", ',');\n\t\tassertEquals(1, split.size());\n\n\t\tsplit = StrUtil.splitTrim(\"\", ',');\n\t\tassertEquals(0, split.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/Issue3809Test.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3809Test {\n\t@Test\n\tvoid roundStrTest() {\n\t\tAssertions.assertEquals(\"9999999999999999.99\", NumberUtil.roundStr(\"9999999999999999.99\", 2));  //输出结果不符合方法声明返回值规则\n\t\tAssertions.assertEquals(\"11111111111111119.00\", NumberUtil.roundStr(\"11111111111111119.00\", 2));\n\t\tAssertions.assertEquals(\"7999999999999999.99\", NumberUtil.roundStr(\"7999999999999999.99\", 2)); //输出结果不符合方法声明返回值规则\n\t\tAssertions.assertEquals(\"699999999991999.92\", NumberUtil.roundStr(\"699999999991999.92\", 2)); //输出结果不符合方法声明返回值规则\n\t\tAssertions.assertEquals(\"10.92\", NumberUtil.roundStr(\"10.92\", 2));\n\t\tAssertions.assertEquals(\"10.99\", NumberUtil.roundStr(\"10.99\", 2));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueI7CRIWTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Type;\n\npublic class IssueI7CRIWTest {\n\t@Test\n\tpublic void getTypeArgumentsTest() {\n\t\t// 无法从继承获取泛型，则从接口获取\n\t\tType type = TypeUtil.getTypeArgument(C.class);\n\t\tassertEquals(type, String.class);\n\n\t\t// 继承和第一个接口都非泛型接口，则从找到的第一个泛型接口获取\n\t\ttype = TypeUtil.getTypeArgument(D.class);\n\t\tassertEquals(type, String.class);\n\t}\n\n\tstatic class A{\n\n\t}\n\n\tstatic class AT<T>{\n\n\t}\n\n\tinterface Face1<T>{\n\n\t}\n\n\tinterface Face2{\n\n\t}\n\n\tstatic class B extends A{\n\n\t}\n\n\tstatic class C extends A implements Face1<String>{\n\n\t}\n\n\tstatic class D extends A implements Face2, Face1<String>{\n\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueI9IDAGTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.List;\n\npublic class IssueI9IDAGTest {\n\t@Test\n\t@Disabled\n\tpublic void loopFilesTest() {\n\t\tfinal List<File> files = FileUtil.loopFiles(\"d:/m2_repo\");\n\t\tfiles.forEach(Console::log);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueI9K494Test.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.FileInputStream;\nimport java.nio.charset.StandardCharsets;\n\npublic class IssueI9K494Test {\n\t@Test\n\t@Disabled\n\tpublic void unzipTest() {\n\t\tFileInputStream inputStream = IoUtil.toStream(FileUtil.file(\"d:/test/unzip5616889482468994725.zip\"));\n\t\tZipUtil.unzip(inputStream, FileUtil.file(\"d:/test/\"), StandardCharsets.UTF_8);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipTest2() {\n\t\tFileInputStream inputStream = IoUtil.toStream(FileUtil.file(\"d:/test/test.zip\"));\n\t\tZipUtil.unzip(inputStream, FileUtil.file(\"d:/test/\"), StandardCharsets.UTF_8);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueI9NSZ4Test.java",
    "content": "package cn.hutool.core.util;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.ToString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI9NSZ4Test {\n\n\t@Test\n\tpublic void getByTest() {\n\t\t// AnimalKindInZoo所有枚举结果的getMappedValue结果值中都无AnimalKind.DOG，返回null\n\t\tfinal AnimalKindInZoo by = EnumUtil.getBy(AnimalKindInZoo::getMappedValue, AnimalKind.DOG);\n\t\tassertNull(by);\n\t}\n\n\t@Test\n\tpublic void getByTest2() {\n\t\tfinal AnimalKindInZoo by = EnumUtil.getBy(AnimalKindInZoo::getMappedValue, AnimalKind.BIRD);\n\t\tassertEquals(AnimalKindInZoo.BIRD, by);\n\t}\n\n\t/**\n\t * 动物类型\n\t */\n\t@Getter\n\t@ToString\n\t@AllArgsConstructor\n\tpublic enum AnimalKind {\n\n\t\t/**\n\t\t * 猫\n\t\t */\n\t\tCAT(\"cat\", \"猫\"),\n\t\t/**\n\t\t * 狗\n\t\t */\n\t\tDOG(\"dog\", \"狗\"),\n\t\t/**\n\t\t * 鸟\n\t\t */\n\t\tBIRD(\"bird\", \"鸟\");\n\n\t\t/**\n\t\t * 键\n\t\t */\n\t\tprivate final String key;\n\t\t/**\n\t\t * 值\n\t\t */\n\t\tprivate final String value;\n\t}\n\n\t/**\n\t * 动物园里的动物类型\n\t */\n\t@Getter\n\t@ToString\n\t@AllArgsConstructor\n\tpublic enum AnimalKindInZoo {\n\n\t\t/**\n\t\t * 猫\n\t\t */\n\t\tCAT(\"cat\", \"猫\", AnimalKind.CAT),\n\t\t/**\n\t\t * 蛇\n\t\t */\n\t\tSNAKE(\"snake\", \"蛇\", null),\n\t\t/**\n\t\t * 鸟\n\t\t */\n\t\tBIRD(\"bird\", \"鸟\", AnimalKind.BIRD);\n\n\t\t/**\n\t\t * 键\n\t\t */\n\t\tprivate final String key;\n\t\t/**\n\t\t * 值\n\t\t */\n\t\tprivate final String value;\n\t\t/**\n\t\t * 映射值\n\t\t */\n\t\tprivate final AnimalKind mappedValue;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueI9UK5VTest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueI9UK5VTest {\n\t@Test\n\tpublic void splitTest() {\n\t\tString str = \"\";\n\t\tList<String> split = StrUtil.split(str, ',');\n\t\tassertEquals(1, split.size());\n\n\t\tsplit = StrUtil.splitTrim(str, ',');\n\t\tassertEquals(0, split.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueIAQ16ETest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueIAQ16ETest {\n\t@Test\n\tvoid lastIndexOfSubTest() {\n\t\tInteger[] bigBytes = new Integer[]{1, 2, 2, 2, 3, 2, 2, 2, 3};\n\t\tInteger[] subBytes = new Integer[]{2, 2};\n\t\tfinal int i = ArrayUtil.lastIndexOfSub(bigBytes, subBytes);\n\t\tAssertions.assertEquals(6, i);\n\t}\n\n\t@Test\n\tvoid lastIndexOfSubTest2() {\n\t\tInteger[] bigBytes = new Integer[]{1, 2, 2, 2, 3, 2, 2, 2, 3, 4, 5};\n\t\tInteger[] subBytes = new Integer[]{2, 2, 2, 3};\n\t\tfinal int i = ArrayUtil.lastIndexOfSub(bigBytes, subBytes);\n\t\tAssertions.assertEquals(5, i);\n\t\tAssertions.assertEquals(5, i);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfSubTest3() {\n\t\tInteger[] a = {0x12, 0x34, 0x56, 0x78, 0x9A};\n\t\tInteger[] b = {0x56, 0x78};\n\n\t\tint i = ArrayUtil.lastIndexOfSub(a, b);\n\t\tassertEquals(2, i);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueIB95X4Test.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.PatternPool;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIB95X4Test {\n\n\t@Test\n\tvoid isMacTest() {\n\t\tAssertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, \"ab1c.2d3e.f468\"));\n\t\tAssertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, \"ab:1c:2d:3e:f4:68\"));\n\t\tAssertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, \"ab-1c-2d-3e-f4-68\"));\n\t\tAssertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, \"ab1c2d3ef468\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueIBP6T1Test.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIBP6T1Test {\n\t@SuppressWarnings(\"DataFlowIssue\")\n\t@Test\n\tvoid isValidCard10Test(){\n\t\tAssertions.assertEquals(\"true\", IdcardUtil.isValidCard10(\"1608214(1)\")[2]);\n\t\tAssertions.assertEquals(\"true\", IdcardUtil.isValidCard10(\"1608214（1）\")[2]);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueICA9S5Test.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class IssueICA9S5Test {\n\t@Test\n\tpublic void test() {\n\t\tString a = \"ENUM{\\\\ndisable ~ 0\\\\nenable ~ 1\\\\n}\";\n\t\tfinal List<String> split = StrUtil.split(a, \"\\\\n\");\n\t\tAssertions.assertEquals(4, split.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/IssueICOJVZTest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueICOJVZTest {\n\t@Test\n\tvoid toUnderlineTest(){\n\t\tString field = \"PAGE_NAME\";\n\t\tfield = StrUtil.toUnderlineCase(field).toUpperCase();\n\t\tAssertions.assertEquals(\"PAGE_NAME\", field);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/JAXBUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.bind.annotation.*;\n\n/**\n * {@link JAXBUtil} 工具类\n * 测试 xml 和 bean 互转工具类\n *\n * @author dazer\n */\npublic class JAXBUtilTest {\n\n\tprivate final String xmlStr = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\" standalone=\\\"yes\\\"?>\\n\" +\n\t\t\t\"<school>\\n\" +\n\t\t\t\"    <school_name>西安市第一中学</school_name>\\n\" +\n\t\t\t\"    <school_address>西安市雁塔区长安堡一号</school_address>\\n\" +\n\t\t\t\"    <room>\\n\" +\n\t\t\t\"        <room_no>101</room_no>\\n\" +\n\t\t\t\"        <room_name>101教室</room_name>\\n\" +\n\t\t\t\"    </room>\\n\" +\n\t\t\t\"</school>\\n\";\n\n\t@Test\n\tpublic void beanToXmlTest() {\n\t\tSchoolVo schoolVo = new SchoolVo();\n\t\tschoolVo.setSchoolName(\"西安市第一中学\");\n\t\tschoolVo.setSchoolAddress(\"西安市雁塔区长安堡一号\");\n\n\t\tSchoolVo.RoomVo roomVo = new SchoolVo.RoomVo();\n\t\troomVo.setRoomName(\"101教室\");\n\t\troomVo.setRoomNo(\"101\");\n\t\tschoolVo.setRoom(roomVo);\n\n\t\tassertEquals(xmlStr, JAXBUtil.beanToXml(schoolVo));\n\t}\n\n\t@Test\n\tpublic void xmlToBeanTest() {\n\t\tfinal SchoolVo schoolVo = JAXBUtil.xmlToBean(xmlStr, SchoolVo.class);\n\t\tassertNotNull(schoolVo);\n\t\tassertEquals(\"西安市第一中学\", schoolVo.getSchoolName());\n\t\tassertEquals(\"西安市雁塔区长安堡一号\", schoolVo.getSchoolAddress());\n\n\t\tassertEquals(\"101教室\", schoolVo.getRoom().getRoomName());\n\t\tassertEquals(\"101\", schoolVo.getRoom().getRoomNo());\n\t}\n\n\t@XmlRootElement(name = \"school\")\n\t@XmlAccessorType(XmlAccessType.FIELD)\n\t@XmlType(propOrder={\"schoolName\", \"schoolAddress\", \"room\"})\n\tpublic static class SchoolVo {\n\t\t@XmlElement(name = \"school_name\", required = true)\n\t\tprivate String schoolName;\n\t\t@XmlElement(name = \"school_address\", required = true)\n\t\tprivate String schoolAddress;\n\t\t@XmlElement(name = \"room\", required = true)\n\t\tprivate RoomVo room;\n\n\t\t@XmlTransient\n\t\tpublic String getSchoolName() {\n\t\t\treturn schoolName;\n\t\t}\n\n\t\tpublic void setSchoolName(String schoolName) {\n\t\t\tthis.schoolName = schoolName;\n\t\t}\n\n\t\t@XmlTransient\n\t\tpublic String getSchoolAddress() {\n\t\t\treturn schoolAddress;\n\t\t}\n\n\t\tpublic void setSchoolAddress(String schoolAddress) {\n\t\t\tthis.schoolAddress = schoolAddress;\n\t\t}\n\n\t\t@XmlTransient\n\t\tpublic RoomVo getRoom() {\n\t\t\treturn room;\n\t\t}\n\n\t\tpublic void setRoom(RoomVo room) {\n\t\t\tthis.room = room;\n\t\t}\n\n\t\t@XmlAccessorType(XmlAccessType.FIELD)\n\t\t@XmlType(propOrder={\"roomNo\", \"roomName\"})\n\t\tpublic static final class RoomVo {\n\t\t\t@XmlElement(name = \"room_no\", required = true)\n\t\t\tprivate String roomNo;\n\t\t\t@XmlElement(name = \"room_name\", required = true)\n\t\t\tprivate String roomName;\n\n\t\t\t@XmlTransient\n\t\t\tpublic String getRoomNo() {\n\t\t\t\treturn roomNo;\n\t\t\t}\n\n\t\t\tpublic void setRoomNo(String roomNo) {\n\t\t\t\tthis.roomNo = roomNo;\n\t\t\t}\n\n\t\t\t@XmlTransient\n\t\t\tpublic String getRoomName() {\n\t\t\t\treturn roomName;\n\t\t\t}\n\n\t\t\tpublic void setRoomName(String roomName) {\n\t\t\t\tthis.roomName = roomName;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/JNDIUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.EnumerationIter;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attribute;\nimport javax.naming.directory.Attributes;\n\npublic class JNDIUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getDnsTest() throws NamingException {\n\t\tfinal Attributes attributes = JNDIUtil.getAttributes(\"dns:paypal.com\", \"TXT\");\n\t\tfor (Attribute attribute: new EnumerationIter<>(attributes.getAll())){\n\t\t\tConsole.log(attribute.get());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ModifierUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Method;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ModifierUtilTest {\n\n\t@Test\n\tpublic void hasModifierTest() throws NoSuchMethodException {\n\t\tMethod method = ModifierUtilTest.class.getDeclaredMethod(\"ddd\");\n\t\tassertTrue(ModifierUtil.hasModifier(method, ModifierUtil.ModifierType.PRIVATE));\n\t\tassertTrue(ModifierUtil.hasModifier(method,\n\t\t\t\tModifierUtil.ModifierType.PRIVATE,\n\t\t\t\tModifierUtil.ModifierType.STATIC)\n\t\t);\n\t}\n\n\t@Test\n\tpublic void hasModifierTest2() throws NoSuchMethodException {\n\t\tMethod method = ModifierUtilTest.class.getDeclaredMethod(\"ddd\");\n\t\tassertTrue(ModifierUtil.hasModifier(method, ModifierUtil.ModifierType.PRIVATE));\n\t\tassertTrue(ModifierUtil.hasModifier(method,\n\t\t\tModifierUtil.ModifierType.PRIVATE,\n\t\t\tModifierUtil.ModifierType.ABSTRACT)\n\t\t);\n\t}\n\n\t@Test\n\tvoid issueIAQ2U0Test() throws NoSuchMethodException {\n\t\tfinal Method method = ModifierUtilTest.class.getDeclaredMethod(\"ddd\");\n\n\t\tAssertions.assertTrue(ModifierUtil.hasModifier(method,\n\t\t\tModifierUtil.ModifierType.PRIVATE,\n\t\t\tModifierUtil.ModifierType.STATIC,\n\t\t\t// 不存在\n\t\t\tModifierUtil.ModifierType.TRANSIENT\n\t\t));\n\n\t\tAssertions.assertFalse(ModifierUtil.hasAllModifiers(method,\n\t\t\tModifierUtil.ModifierType.PRIVATE,\n\t\t\tModifierUtil.ModifierType.STATIC,\n\t\t\t// 不存在\n\t\t\tModifierUtil.ModifierType.TRANSIENT\n\t\t));\n\t}\n\n\tprivate static void ddd() {\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.math.RoundingMode;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link NumberUtil} 单元测试类\n *\n * @author Looly\n *\n */\npublic class NumberUtilTest {\n\n\t@Test\n\tpublic void addTest() {\n\t\tfinal Float a = 3.15f;\n\t\tfinal Double b = 4.22;\n\t\tfinal double result = NumberUtil.add(a, b).doubleValue();\n\t\tassertEquals(7.37, result, 0);\n\t}\n\n\t@Test\n\tpublic void addTest2() {\n\t\tfinal double a = 3.15f;//精度丢失\n\t\tfinal double b = 4.22;\n\t\tfinal double result = NumberUtil.add(a, b);\n\t\tassertEquals(7.37, result, 0.01);\n\t}\n\n\t@Test\n\tpublic void addTest3() {\n\t\tfinal float a = 3.15f;\n\t\tfinal double b = 4.22;\n\t\tfinal double result = NumberUtil.add(a, b, a, b).doubleValue();\n\t\tassertEquals(14.74, result, 0);\n\t}\n\n\t@Test\n\tpublic void addTest4() {\n\t\tfinal BigDecimal result = NumberUtil.add(new BigDecimal(\"133\"), new BigDecimal(\"331\"));\n\t\tassertEquals(new BigDecimal(\"464\"), result);\n\t}\n\n\t@Test\n\tpublic void addBlankTest() {\n\t\tfinal BigDecimal result = NumberUtil.add(\"123\", \" \");\n\t\tassertEquals(new BigDecimal(\"123\"), result);\n\t}\n\n\t@Test\n\tpublic void addTest5() {\n\t\tfinal double add = NumberUtil.add(1686036549717D, 1000D);\n\t\tassertEquals(1686036550717D, add, 0);\n\t}\n\n\t@Test\n\tpublic void isIntegerTest() {\n\t\tassertTrue(NumberUtil.isInteger(\"-12\"));\n\t\tassertTrue(NumberUtil.isInteger(\"256\"));\n\t\tassertTrue(NumberUtil.isInteger(\"0256\"));\n\t\tassertTrue(NumberUtil.isInteger(\"0\"));\n\t\tassertFalse(NumberUtil.isInteger(\"23.4\"));\n\t\tassertFalse(NumberUtil.isInteger(null));\n\t\tassertFalse(NumberUtil.isInteger(\"\"));\n\t\tassertFalse(NumberUtil.isInteger(\" \"));\n\t}\n\n\t@Test\n\tpublic void isLongTest() {\n\t\tassertTrue(NumberUtil.isLong(\"-12\"));\n\t\tassertTrue(NumberUtil.isLong(\"256\"));\n\t\tassertTrue(NumberUtil.isLong(\"0256\"));\n\t\tassertTrue(NumberUtil.isLong(\"0\"));\n\t\tassertFalse(NumberUtil.isLong(\"23.4\"));\n\t\tassertFalse(NumberUtil.isLong(null));\n\t\tassertFalse(NumberUtil.isLong(\"\"));\n\t\tassertFalse(NumberUtil.isLong(\" \"));\n\t}\n\n\t@Test\n\tpublic void isNumberTest() {\n\t\tassertTrue(NumberUtil.isNumber(\"28.55\"));\n\t\tassertTrue(NumberUtil.isNumber(\"0\"));\n\t\tassertTrue(NumberUtil.isNumber(\"+100.10\"));\n\t\tassertTrue(NumberUtil.isNumber(\"-22.022\"));\n\t\tassertTrue(NumberUtil.isNumber(\"0X22\"));\n\t}\n\n\t@Test\n\tpublic void divTest() {\n\t\tfinal double result = NumberUtil.div(0, 1);\n\t\tassertEquals(0.0, result, 0);\n\t}\n\n\t@Test\n\tpublic void divBigDecimalTest() {\n\t\tfinal BigDecimal result = NumberUtil.div(BigDecimal.ZERO, BigDecimal.ONE);\n\t\tassertEquals(BigDecimal.ZERO, result.stripTrailingZeros());\n\t}\n\n\t@Test\n\tpublic void roundTest() {\n\n\t\t// 四舍\n\t\tfinal String round1 = NumberUtil.roundStr(2.674, 2);\n\t\tfinal String round2 = NumberUtil.roundStr(\"2.674\", 2);\n\t\tassertEquals(\"2.67\", round1);\n\t\tassertEquals(\"2.67\", round2);\n\n\t\t// 五入\n\t\tfinal String round3 = NumberUtil.roundStr(2.675, 2);\n\t\tfinal String round4 = NumberUtil.roundStr(\"2.675\", 2);\n\t\tassertEquals(\"2.68\", round3);\n\t\tassertEquals(\"2.68\", round4);\n\n\t\t// 四舍六入五成双\n\t\tfinal String round31 = NumberUtil.roundStr(4.245, 2, RoundingMode.HALF_EVEN);\n\t\tfinal String round41 = NumberUtil.roundStr(\"4.2451\", 2, RoundingMode.HALF_EVEN);\n\t\tassertEquals(\"4.24\", round31);\n\t\tassertEquals(\"4.25\", round41);\n\n\t\t// 补0\n\t\tfinal String round5 = NumberUtil.roundStr(2.6005, 2);\n\t\tfinal String round6 = NumberUtil.roundStr(\"2.6005\", 2);\n\t\tassertEquals(\"2.60\", round5);\n\t\tassertEquals(\"2.60\", round6);\n\n\t\t// 补0\n\t\tfinal String round7 = NumberUtil.roundStr(2.600, 2);\n\t\tfinal String round8 = NumberUtil.roundStr(\"2.600\", 2);\n\t\tassertEquals(\"2.60\", round7);\n\t\tassertEquals(\"2.60\", round8);\n\t}\n\n\t@Test\n\tpublic void roundStrTest() {\n\t\tfinal String roundStr = NumberUtil.roundStr(2.647, 2);\n\t\tassertEquals(\"2.65\", roundStr);\n\n\t\tfinal String roundStr1 = NumberUtil.roundStr(0, 10);\n\t\tassertEquals(\"0.0000000000\", roundStr1);\n\t}\n\n\t@Test\n\tpublic void roundHalfEvenTest() {\n\t\tString roundStr = NumberUtil.roundHalfEven(4.245, 2).toString();\n\t\tassertEquals(\"4.24\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(4.2450, 2).toString();\n\t\tassertEquals(\"4.24\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(4.2451, 2).toString();\n\t\tassertEquals(\"4.25\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(4.2250, 2).toString();\n\t\tassertEquals(\"4.22\", roundStr);\n\n\t\troundStr = NumberUtil.roundHalfEven(1.2050, 2).toString();\n\t\tassertEquals(\"1.20\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2150, 2).toString();\n\t\tassertEquals(\"1.22\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2250, 2).toString();\n\t\tassertEquals(\"1.22\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2350, 2).toString();\n\t\tassertEquals(\"1.24\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2450, 2).toString();\n\t\tassertEquals(\"1.24\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2550, 2).toString();\n\t\tassertEquals(\"1.26\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2650, 2).toString();\n\t\tassertEquals(\"1.26\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2750, 2).toString();\n\t\tassertEquals(\"1.28\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2850, 2).toString();\n\t\tassertEquals(\"1.28\", roundStr);\n\t\troundStr = NumberUtil.roundHalfEven(1.2950, 2).toString();\n\t\tassertEquals(\"1.30\", roundStr);\n\t}\n\n\t@Test\n\tpublic void decimalFormatTest() {\n\t\tfinal long c = 299792458;// 光速\n\n\t\tfinal String format = NumberUtil.decimalFormat(\",###\", c);\n\t\tassertEquals(\"299,792,458\", format);\n\t}\n\n\t@Test\n\tpublic void decimalFormatNaNTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal Double a = 0D;\n\t\t\tfinal Double b = 0D;\n\n\t\t\tfinal Double c = a / b;\n\t\t\tConsole.log(NumberUtil.decimalFormat(\"#%\", c));\n\t\t});\n\t}\n\n\t@Test\n\tpublic void decimalFormatNaNTest2() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tfinal Double a = 0D;\n\t\t\tfinal Double b = 0D;\n\n\t\t\tConsole.log(NumberUtil.decimalFormat(\"#%\", a / b));\n\t\t});\n\t}\n\n\t@Test\n\tpublic void decimalFormatDoubleTest() {\n\t\tfinal Double c = 467.8101;\n\n\t\tfinal String format = NumberUtil.decimalFormat(\"0.00\", c);\n\t\tassertEquals(\"467.81\", format);\n\t}\n\n\t@Test\n\tpublic void isValidNumberTest() {\n\t\tboolean validNumber = NumberUtil.isValidNumber(1);\n\t\tassertTrue(validNumber);\n\t}\n\n\t@Test\n\tpublic void decimalFormatMoneyTest() {\n\t\tfinal double c = 299792400.543534534;\n\n\t\tfinal String format = NumberUtil.decimalFormatMoney(c);\n\t\tassertEquals(\"299,792,400.54\", format);\n\n\t\tfinal double value = 0.5;\n\t\tfinal String money = NumberUtil.decimalFormatMoney(value);\n\t\tassertEquals(\"0.50\", money);\n\t}\n\n\t@Test\n\tpublic void equalsTest() {\n\t\tassertTrue(NumberUtil.equals(new BigDecimal(\"0.00\"), BigDecimal.ZERO));\n\t}\n\n\t@Test\n\tpublic void toBigDecimalTest() {\n\t\tfinal double a = 3.14;\n\n\t\tBigDecimal bigDecimal = NumberUtil.toBigDecimal(a);\n\t\tassertEquals(\"3.14\", bigDecimal.toString());\n\n\t\tbigDecimal = NumberUtil.toBigDecimal(\"1,234.55\");\n\t\tassertEquals(\"1234.55\", bigDecimal.toString());\n\n\t\tbigDecimal = NumberUtil.toBigDecimal(\"1,234.56D\");\n\t\tassertEquals(\"1234.56\", bigDecimal.toString());\n\n\t\tassertEquals(new BigDecimal(\"9.0E+7\"), NumberUtil.toBigDecimal(\"9.0E+7\"));\n\t}\n\n\t@Test\n\tpublic void maxTest() {\n\t\tfinal int max = NumberUtil.max(5, 4, 3, 6, 1);\n\t\tassertEquals(6, max);\n\t}\n\n\t@Test\n\tpublic void minTest() {\n\t\tfinal int min = NumberUtil.min(5, 4, 3, 6, 1);\n\t\tassertEquals(1, min);\n\t}\n\n\t@Test\n\tpublic void parseIntTest() {\n\t\tint number = NumberUtil.parseInt(\"0xFE\");\n\t\tassertEquals(254, number);\n\n\t\t// 0开头\n\t\tnumber = NumberUtil.parseInt(\"010\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseInt(\"10\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseInt(\"   \");\n\t\tassertEquals(0, number);\n\n\t\tnumber = NumberUtil.parseInt(\"10F\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseInt(\"22.4D\");\n\t\tassertEquals(22, number);\n\n\t\tnumber = NumberUtil.parseInt(\"22.6D\");\n\t\tassertEquals(22, number);\n\n\t\tnumber = NumberUtil.parseInt(\"0\");\n\t\tassertEquals(0, number);\n\n\t\tnumber = NumberUtil.parseInt(\".123\");\n\t\tassertEquals(0, number);\n\t}\n\n\t@Test\n\tpublic void parseIntTest2() {\n\t\t// from 5.4.8 issue#I23ORQ@Gitee\n\t\t// 千位分隔符去掉\n\t\tfinal int v1 = NumberUtil.parseInt(\"1,482.00\");\n\t\tassertEquals(1482, v1);\n\t}\n\n\t@Test\n\tpublic void parseIntTest3() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\tfinal int v1 = NumberUtil.parseInt(\"d\");\n\t\t\tassertEquals(0, v1);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void parseIntTest4() {\n\n\t\t// -------------------------- Parse failed -----------------------\n\n\t\tAssertions.assertNull(NumberUtil.parseInt(\"abc\", null));\n\n\t\tassertEquals(456, NumberUtil.parseInt(\"abc\", 456));\n\n\t\t// -------------------------- Parse success -----------------------\n\n\t\tassertEquals(123, NumberUtil.parseInt(\"123.abc\", 789));\n\n\t\tassertEquals(123, NumberUtil.parseInt(\"123.3\", null));\n\n\t}\n\n\t@Test\n\tpublic void parseNumberTest4() {\n\t\tassertThrows(NumberFormatException.class, () -> {\n\t\t\t// issue#I5M55F\n\t\t\t// 科学计数法忽略支持，科学计数法一般用于表示非常小和非常大的数字，这类数字转换为int后精度丢失，没有意义。\n\t\t\tfinal String numberStr = \"429900013E20220812163344551\";\n\t\t\tNumberUtil.parseInt(numberStr);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void parseNumberTest() {\n\t\t// from 5.4.8 issue#I23ORQ@Gitee\n\t\t// 千位分隔符去掉\n\t\tfinal int v1 = NumberUtil.parseNumber(\"1,482.00\").intValue();\n\t\tassertEquals(1482, v1);\n\n\t\tfinal Number v2 = NumberUtil.parseNumber(\"1,482.00D\");\n\t\tassertEquals(1482L, v2.longValue());\n\t}\n\n\t@Test\n\tpublic void parseNumberTest2() {\n\t\t// issue#I5M55F\n\t\tfinal String numberStr = \"429900013E20220812163344551\";\n\t\tfinal Number number = NumberUtil.parseNumber(numberStr);\n\t\tassertNotNull(number);\n\t\tassertInstanceOf(BigDecimal.class, number);\n\t}\n\n\t@Test\n\tpublic void parseNumberTest3() {\n\n\t\t// -------------------------- Parse failed -----------------------\n\n\t\tassertNull(NumberUtil.parseNumber(\"abc\", (Number) null));\n\n\t\tassertNull(NumberUtil.parseNumber(StrUtil.EMPTY, (Number) null));\n\n\t\tassertNull(NumberUtil.parseNumber(StrUtil.repeat(StrUtil.SPACE, 10), (Number) null));\n\n\t\tassertEquals(456, NumberUtil.parseNumber(\"abc\", 456).intValue());\n\n\t\t// -------------------------- Parse success -----------------------\n\n\t\tassertEquals(123, NumberUtil.parseNumber(\"123.abc\", 789).intValue());\n\n\t\tassertEquals(123.3D, NumberUtil.parseNumber(\"123.3\", (Number) null).doubleValue());\n\n\t\tassertEquals(0.123D, NumberUtil.parseNumber(\"0.123.3\", (Number) null).doubleValue());\n\n\t}\n\n\t@Test\n\tvoid issueIDJ1NSTest(){\n\t\tfinal String numberstr1 = \"8.37095942E+9\";\n\t\tfinal BigDecimal result1 = (BigDecimal) NumberUtil.parseNumber(numberstr1);\n\t\tfinal String numberstr2 = \"8.37095942e+9\";\n\t\tfinal BigDecimal result2 = (BigDecimal) NumberUtil.parseNumber(numberstr2);\n\t\tassertEquals(new BigDecimal(\"8370959420\").toPlainString(), result1.toPlainString());\n\t\tassertEquals(new BigDecimal(\"8370959420\").toPlainString(), result2.toPlainString());\n\t}\n\n\t@Test\n\tpublic void parseHexNumberTest() {\n\t\t// 千位分隔符去掉\n\t\tfinal int v1 = NumberUtil.parseNumber(\"0xff\").intValue();\n\t\tassertEquals(255, v1);\n\t}\n\n\t@Test\n\tpublic void parseLongTest() {\n\t\tlong number = NumberUtil.parseLong(\"0xFF\");\n\t\tassertEquals(255, number);\n\n\t\t// 0开头\n\t\tnumber = NumberUtil.parseLong(\"010\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseLong(\"10\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseLong(\"   \");\n\t\tassertEquals(0, number);\n\n\t\tnumber = NumberUtil.parseLong(\"10F\");\n\t\tassertEquals(10, number);\n\n\t\tnumber = NumberUtil.parseLong(\"22.4D\");\n\t\tassertEquals(22, number);\n\n\t\tnumber = NumberUtil.parseLong(\"22.6D\");\n\t\tassertEquals(22, number);\n\n\t\tnumber = NumberUtil.parseLong(\"0\");\n\t\tassertEquals(0, number);\n\n\t\tnumber = NumberUtil.parseLong(\".123\");\n\t\tassertEquals(0, number);\n\t}\n\n\t@Test\n\tpublic void parseLongTest2() {\n\n\t\t// -------------------------- Parse failed -----------------------\n\n\t\tfinal Long v1 = NumberUtil.parseLong(null, null);\n\t\tassertNull(v1);\n\n\t\tfinal Long v2 = NumberUtil.parseLong(StrUtil.EMPTY, null);\n\t\tassertNull(v2);\n\n\t\tfinal Long v3 = NumberUtil.parseLong(\"L3221\", 1233L);\n\t\tassertEquals(1233L, v3);\n\n\t\t// -------------------------- Parse success -----------------------\n\n\t\tfinal Long v4 = NumberUtil.parseLong(\"1233L\", null);\n\t\tassertEquals(1233L, v4);\n\n\t}\n\n\t@Test\n\tpublic void parseFloatTest() {\n\n\t\t// -------------------------- Parse failed -----------------------\n\n\t\tassertNull(NumberUtil.parseFloat(\"abc\", null));\n\n\t\tassertNull(NumberUtil.parseFloat(\"a123.33\", null));\n\n\t\tassertNull(NumberUtil.parseFloat(\"..123\", null));\n\n\t\tassertEquals(1233F, NumberUtil.parseFloat(StrUtil.EMPTY, 1233F));\n\n\t\t// -------------------------- Parse success -----------------------\n\n\t\tassertEquals(123.33F, NumberUtil.parseFloat(\"123.33a\", null));\n\n\t\tassertEquals(0.123F, NumberUtil.parseFloat(\".123\", null));\n\n\t}\n\n\t@Test\n\tpublic void parseDoubleTest() {\n\n\t\t// -------------------------- Parse failed -----------------------\n\n\t\tassertNull(NumberUtil.parseDouble(\"abc\", null));\n\t\tassertNull(NumberUtil.parseDouble(\"a123.33\", null));\n\t\tassertNull(NumberUtil.parseDouble(\"..123\", null));\n\t\tassertEquals(1233D, NumberUtil.parseDouble(StrUtil.EMPTY, 1233D));\n\n\t\t// -------------------------- Parse success -----------------------\n\n\t\tassertEquals(123.33D, NumberUtil.parseDouble(\"123.33a\", null));\n\t\tassertEquals(0.123D, NumberUtil.parseDouble(\".123\", null));\n\t}\n\n\t@Test\n\tpublic void factorialTest() {\n\t\tlong factorial = NumberUtil.factorial(0);\n\t\tassertEquals(1, factorial);\n\n\t\tassertEquals(1L, NumberUtil.factorial(1));\n\t\tassertEquals(1307674368000L, NumberUtil.factorial(15));\n\t\tassertEquals(2432902008176640000L, NumberUtil.factorial(20));\n\n\t\tfactorial = NumberUtil.factorial(5, 0);\n\t\tassertEquals(120, factorial);\n\t\tfactorial = NumberUtil.factorial(5, 1);\n\t\tassertEquals(120, factorial);\n\n\t\tassertEquals(5, NumberUtil.factorial(5, 4));\n\t\tassertEquals(2432902008176640000L, NumberUtil.factorial(20, 0));\n\t}\n\n\t@Test\n\tpublic void factorialTest2() {\n\t\tlong factorial = NumberUtil.factorial(new BigInteger(\"0\")).longValue();\n\t\tassertEquals(1, factorial);\n\n\t\tassertEquals(1L, NumberUtil.factorial(new BigInteger(\"1\")).longValue());\n\t\tassertEquals(1307674368000L, NumberUtil.factorial(new BigInteger(\"15\")).longValue());\n\t\tassertEquals(2432902008176640000L, NumberUtil.factorial(20));\n\n\t\tfactorial = NumberUtil.factorial(new BigInteger(\"5\"), new BigInteger(\"0\")).longValue();\n\t\tassertEquals(120, factorial);\n\t\tfactorial = NumberUtil.factorial(new BigInteger(\"5\"), BigInteger.ONE).longValue();\n\t\tassertEquals(120, factorial);\n\n\t\tassertEquals(5, NumberUtil.factorial(new BigInteger(\"5\"), new BigInteger(\"4\")).longValue());\n\t\tassertEquals(2432902008176640000L, NumberUtil.factorial(new BigInteger(\"20\"), BigInteger.ZERO).longValue());\n\t}\n\n\t@Test\n\tpublic void mulTest() {\n\t\tfinal BigDecimal mul = NumberUtil.mul(new BigDecimal(\"10\"), null);\n\t\tassertEquals(BigDecimal.ZERO, mul);\n\t}\n\n\n\t@Test\n\tpublic void isPowerOfTwoTest() {\n\t\tassertFalse(NumberUtil.isPowerOfTwo(-1));\n\t\tassertTrue(NumberUtil.isPowerOfTwo(16));\n\t\tassertTrue(NumberUtil.isPowerOfTwo(65536));\n\t\tassertTrue(NumberUtil.isPowerOfTwo(1));\n\t\tassertFalse(NumberUtil.isPowerOfTwo(17));\n\t}\n\n\t@Test\n\tpublic void generateRandomNumberTest() {\n\t\tfinal int[] ints = NumberUtil.generateRandomNumber(10, 20, 5);\n\t\tassertEquals(5, ints.length);\n\t\tfinal Set<?> set = Convert.convert(Set.class, ints);\n\t\tassertEquals(5, set.size());\n\t}\n\n\t@Test\n\tpublic void toStrTest() {\n\t\tassertEquals(\"1\", NumberUtil.toStr(new BigDecimal(\"1.0000000000\")));\n\t\tassertEquals(\"0\", NumberUtil.toStr(NumberUtil.sub(new BigDecimal(\"9600.00000\"), new BigDecimal(\"9600.00000\"))));\n\t\tassertEquals(\"0\", NumberUtil.toStr(NumberUtil.sub(new BigDecimal(\"9600.0000000000\"), new BigDecimal(\"9600.000000\"))));\n\t\tassertEquals(\"0\", NumberUtil.toStr(new BigDecimal(\"9600.00000\").subtract(new BigDecimal(\"9600.000000000\"))));\n\t}\n\n\t@Test\n\tpublic void generateRandomNumberTest2() {\n\t\t// 检查边界\n\t\tfinal int[] ints = NumberUtil.generateRandomNumber(1, 8, 7);\n\t\tassertEquals(7, ints.length);\n\t\tfinal Set<?> set = Convert.convert(Set.class, ints);\n\t\tassertEquals(7, set.size());\n\t}\n\n\t@Test\n\tpublic void toPlainNumberTest() {\n\t\tfinal String num = \"5344.34234e3\";\n\t\tfinal String s = new BigDecimal(num).toPlainString();\n\t\tassertEquals(\"5344342.34\", s);\n\t}\n\n\t@Test\n\tpublic void generateBySetTest() {\n\t\tfinal Integer[] integers = NumberUtil.generateBySet(10, 100, 5);\n\t\tassertEquals(5, integers.length);\n\t}\n\n\t@Test\n\tpublic void isOddOrEvenTest() {\n\t\tfinal int[] a = {0, 32, -32, 123, -123};\n\t\tassertFalse(NumberUtil.isOdd(a[0]));\n\t\tassertTrue(NumberUtil.isEven(a[0]));\n\n\t\tassertFalse(NumberUtil.isOdd(a[1]));\n\t\tassertTrue(NumberUtil.isEven(a[1]));\n\n\t\tassertFalse(NumberUtil.isOdd(a[2]));\n\t\tassertTrue(NumberUtil.isEven(a[2]));\n\n\t\tassertTrue(NumberUtil.isOdd(a[3]));\n\t\tassertFalse(NumberUtil.isEven(a[3]));\n\n\t\tassertTrue(NumberUtil.isOdd(a[4]));\n\t\tassertFalse(NumberUtil.isEven(a[4]));\n\t}\n\n\t@Test\n\tpublic void divIntegerTest() {\n\t\tassertEquals(1001013, NumberUtil.div(100101300, (Number) 100).intValue());\n\t}\n\n\t@Test\n\tpublic void isDoubleTest() {\n\t\tassertFalse(NumberUtil.isDouble(null));\n\t\tassertFalse(NumberUtil.isDouble(\"\"));\n\t\tassertFalse(NumberUtil.isDouble(\"  \"));\n\t}\n\n\t@Test\n\tpublic void isPrimesTest() {\n\t\tassertTrue(NumberUtil.isPrimes(2));\n\t\tassertTrue(NumberUtil.isPrimes(3));\n\t\tassertFalse(NumberUtil.isPrimes(4));\n\t\tassertTrue(NumberUtil.isPrimes(5));\n\t\tassertTrue(NumberUtil.isPrimes(7));\n\t\tassertFalse(NumberUtil.isPrimes(9));\n\t\tassertTrue(NumberUtil.isPrimes(13));\n\t\tassertFalse(NumberUtil.isPrimes(25));\n\t\tassertFalse(NumberUtil.isPrimes(49));\n\t\tassertTrue(NumberUtil.isPrimes(113));\n\t\tassertFalse(NumberUtil.isPrimes(121));\n\t\tassertTrue(NumberUtil.isPrimes(2147483647));\n\t\tassertFalse(NumberUtil.isPrimes(2147483646));\n\t}\n\n\t@Test\n\tpublic void range() {\n\t\tassertFalse(NumberUtil.isIn(new BigDecimal(\"1\"), new BigDecimal(\"2\"), new BigDecimal(\"12\")));\n\t\tassertTrue(NumberUtil.isIn(new BigDecimal(\"1\"), new BigDecimal(\"1\"), new BigDecimal(\"2\")));\n\t\tassertTrue(NumberUtil.isIn(new BigDecimal(\"1\"), new BigDecimal(\"0\"), new BigDecimal(\"2\")));\n\t\tassertFalse(NumberUtil.isIn(new BigDecimal(\"0.23\"), new BigDecimal(\"0.12\"), new BigDecimal(\"0.22\")));\n\t\tassertTrue(NumberUtil.isIn(new BigDecimal(\"-0.12\"), new BigDecimal(\"-0.3\"), new BigDecimal(\"0\")));\n\t}\n\n\t@Test\n\tpublic void issueI79VS7Test() {\n\t\tfinal String value = \"+0.003\";\n\t\tif (NumberUtil.isNumber(value)) {\n\t\t\tassertEquals(0.003, NumberUtil.parseNumber(value).doubleValue(), 0);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void issueI7R2B6Test() {\n\t\tassertEquals(61.67D,\n\t\t\tNumberUtil.div(NumberUtil.mul(15858155520D, 100), 25715638272D, 2), 0.01);\n\n\t\tassertEquals(61.67, NumberUtil.div(NumberUtil.mul(15858155520D, 100), 25715638272D, 2), 0.01);\n\t}\n\n\t@Test\n\tpublic void issueI7R2B6Test2() {\n\t\tfinal BigDecimal mul = NumberUtil.mul((Number) 15858155520D, 100.0);\n\t\tassertEquals(\"1585815552000\", mul.toString());\n\t}\n\n\t@Test\n\tpublic void testPowZero() {\n\t\tBigDecimal number = new BigDecimal(\"2.5\");\n\t\tint exponent = 0;\n\t\tBigDecimal expected = new BigDecimal(\"1\");\n\t\tassertEquals(expected, NumberUtil.pow(number, exponent));\n\t}\n\n\t@Test\n\tpublic void testPowNegative() {\n\t\tBigDecimal number = new BigDecimal(\"2.5\");\n\t\tint exponent = -2;\n\t\tBigDecimal expected = new BigDecimal(\"0.16\");\n\t\tassertEquals(expected, NumberUtil.pow(number, exponent));\n\t}\n\n\t@Test\n\tpublic void testPowSmallNumber() {\n\t\tBigDecimal number = new BigDecimal(\"0.1\");\n\t\tint exponent = -3;\n\t\tBigDecimal expected = new BigDecimal(\"1000.00\");\n\t\tassertEquals(expected, NumberUtil.pow(number, exponent));\n\t}\n\n\t@Test\n\tpublic void testPowSmallNumberScale() {\n\t\tBigDecimal number = new BigDecimal(\"1.2\");\n\t\tint exponent = -3;\n\t\tBigDecimal expected = new BigDecimal(\"0.58\");\n\t\tassertEquals(expected, NumberUtil.pow(number, exponent));\n\t}\n\n\t@Test\n\tpublic void issue3636Test() {\n\t\tfinal Number number = NumberUtil.parseNumber(\"12,234,456\");\n\t\tassertEquals(new BigDecimal(12234456), number);\n\t}\n\n\t@Test\n\tpublic void addIntAndDoubleTest() {\n\t\tint v1 = 91007279;\n\t\tdouble v2 = 0.3545;\n\t\tfinal double result = NumberUtil.add(v1, v2);\n\t\tassertEquals(91007279.3545, result, 0);\n\t}\n\n\t@Test\n\tvoid issueIC1MXETest() {\n\t\tfinal boolean equals = NumberUtil.equals(104557543L, 104557544);\n\t\tassertFalse(equals);\n\t}\n\n\t@Test\n\tpublic void testMultipleOverflow() {\n\t\tint a = 500000;\n\t\tint b = 600000;\n\n\t\t// 原方法使用 a * b / gcd(a, b) 计算，a * b 会先溢出，得到最小公倍数为负数\n\t\t// 使用修改后的multiple方法，测试它是否能正确处理这种情况\n\t\tint result = NumberUtil.multiple(a, b);\n\t\t// 验证结果必须是正数（两个正数的最小公倍数必须为正）\n\t\tassertTrue(result > 0);\n\t}\n\n\t@Test\n\tpublic void testGetFloatBinaryStr() {\n\t\t// 获取浮点数的 IEEE 754 原始比特位字符串\n\t\tfinal String result = NumberUtil.getBinaryStr(3.5);\n\t\tassertEquals(\"0100000000001100000000000000000000000000000000000000000000000000\", result);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ObjectUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.clone.CloneSupport;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Instant;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ObjectUtilTest {\n\n\t@Test\n\tpublic void equalsTest() {\n\t\tObject a = null;\n\t\tObject b = null;\n\t\tassertTrue(ObjectUtil.equals(a, b));\n\t}\n\n\t@Test\n\tpublic void lengthTest() {\n\t\tint[] array = new int[]{1, 2, 3, 4, 5};\n\t\tint length = ObjectUtil.length(array);\n\t\tassertEquals(5, length);\n\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"a\", \"a1\");\n\t\tmap.put(\"b\", \"b1\");\n\t\tmap.put(\"c\", \"c1\");\n\t\tlength = ObjectUtil.length(map);\n\t\tassertEquals(3, length);\n\t}\n\n\t@Test\n\tpublic void containsTest() {\n\t\tint[] array = new int[]{1, 2, 3, 4, 5};\n\n\t\tfinal boolean contains = ObjectUtil.contains(array, 1);\n\t\tassertTrue(contains);\n\t}\n\n\t@Test\n\tpublic void cloneTest() {\n\t\tObj obj = new Obj();\n\t\tObj obj2 = ObjectUtil.clone(obj);\n\t\tassertEquals(\"OK\", obj2.doSomeThing());\n\t}\n\n\tstatic class Obj extends CloneSupport<Obj> {\n\t\tpublic String doSomeThing() {\n\t\t\treturn \"OK\";\n\t\t}\n\t}\n\n\t@Test\n\tpublic void toStringTest() {\n\t\tArrayList<String> strings = CollUtil.newArrayList(\"1\", \"2\");\n\t\tString result = ObjectUtil.toString(strings);\n\t\tassertEquals(\"[1, 2]\", result);\n\t}\n\n\t@Test\n\tpublic void defaultIfNullTest() {\n\t\tfinal String nullValue = null;\n\t\tfinal String dateStr = \"2020-10-23 15:12:30\";\n\t\tInstant result1 = ObjectUtil.defaultIfNull(dateStr,\n\t\t\t\t(source) -> DateUtil.parse(source, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now());\n\t\tassertNotNull(result1);\n\t\tInstant result2 = ObjectUtil.defaultIfNull(nullValue,\n\t\t\t\t(source) -> DateUtil.parse(source, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now());\n\t\tassertNotNull(result2);\n\n\t\tObj obj = new Obj();\n\t\tObj objNull = null;\n\t\tString result3 = ObjectUtil.defaultIfNull(obj, (a) -> obj.doSomeThing(), \"fail\");\n\t\tassertNotNull(result3);\n\n\t\tString result4 = ObjectUtil.defaultIfNull(objNull, Obj::doSomeThing, \"fail\");\n\t\tassertNotNull(result4);\n\t}\n\n\t@Test\n\tpublic void defaultIfEmptyTest() {\n\t\tfinal String emptyValue = \"\";\n\t\tfinal String dateStr = \"2020-10-23 15:12:30\";\n\t\tInstant result1 = ObjectUtil.defaultIfEmpty(emptyValue,\n\t\t\t\t(source) -> DateUtil.parse(source, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now());\n\t\tassertNotNull(result1);\n\t\tInstant result2 = ObjectUtil.defaultIfEmpty(dateStr,\n\t\t\t\t(source) -> DateUtil.parse(source, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now());\n\t\tassertNotNull(result2);\n\t}\n\n\t@Test\n\tpublic void isBasicTypeTest() {\n\t\tint a = 1;\n\t\tfinal boolean basicType = ObjectUtil.isBasicType(a);\n\t\tassertTrue(basicType);\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void isNotNullTest() {\n\t\tString a = null;\n\t\tassertFalse(ObjectUtil.isNotNull(a));\n\t}\n\n\t@Test\n\tpublic void testLengthConsumesIterator() {\n\t\tList<String> list = Arrays.asList(\"a\", \"b\", \"c\");\n\t\tIterator<String> iterator = list.iterator();\n\t\t// 迭代器第一次调用length\n\t\tint length1 = ObjectUtil.length(iterator);\n\t\tassertEquals(3, length1);\n\t\t// 迭代器第二次调用length - 迭代器已经被消耗，返回0\n\t\tint length2 = ObjectUtil.length(iterator);\n\t\tassertEquals(0, length2); // 但当前实现会重新遍历，但iterator已经没有元素了\n\t\t// 尝试使用迭代器 - 已经无法使用\n\t\tassertFalse(iterator.hasNext());\n\t}\n\n\t@Test\n\tpublic void testLengthConsumesEnumeration() {\n\t\tVector<String> vector = new Vector<>(Arrays.asList(\"a\", \"b\", \"c\"));\n\t\tEnumeration<String> enumeration = vector.elements();\n\t\t// 第一次调用length\n\t\tint length1 = ObjectUtil.length(enumeration);\n\t\tassertEquals(3, length1);\n\t\t// 第二次调用length - 枚举已经被消耗\n\t\tint length2 = ObjectUtil.length(enumeration);\n\t\tassertEquals(0, length2);\n\t\t// 枚举已经无法使用\n\t\tassertFalse(enumeration.hasMoreElements());\n\t}\n\n\t@Test\n\tpublic void testContainsElementToStringReturnsNull() {\n\t\tObject problematicElement = new Object() {\n\t\t\t@Override\n\t\t\tpublic String toString() {\n\t\t\t\treturn null; // 返回 null 的 toString\n\t\t\t}\n\t\t};\n\t\tassertFalse(ObjectUtil.contains(\"test\", problematicElement)); //不会抛异常\n\t}\n\n\t@Test\n\tpublic void testContainsElementToStringInvalidSyntax() {\n\t\t//字符串包含自定义User对象不符合语义\n\t\tassertFalse(ObjectUtil.contains(\"User[id=123]\", new User(123)));\n\t}\n\n\n\tstatic class User{\n\t\tprivate int id;\n\t\tpublic User(int id) {\n\t\t\tthis.id = id;\n\t\t}\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"User[\" +\n\t\t\t\t\t\"id=\" + id +\n\t\t\t\t\t']';\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testContainsCharSequenceSupported() {\n\t\t//contains方法支持String、StringBuilder、StringBuffer\n\t\tStringBuilder stringBuilder = new StringBuilder(\"hello world\");\n\t\tStringBuffer stringBuffer = new StringBuffer(\"hello world\");\n\t\tString str = \"hello world\";\n\t\tassertTrue((ObjectUtil.contains(stringBuilder, \"world\")));\n\t\tassertTrue(ObjectUtil.contains(stringBuffer, \"hello\"));\n\t\tassertTrue(ObjectUtil.contains(str, \"hello\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/PageUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 分页单元测试\n *\n * @author Looly\n */\npublic class PageUtilTest {\n\n\t@Test\n\tpublic void transToStartEndTest() {\n\t\tfinal int[] startEnd1 = PageUtil.transToStartEnd(0, 10);\n\t\tassertEquals(0, startEnd1[0]);\n\t\tassertEquals(10, startEnd1[1]);\n\n\t\tfinal int[] startEnd2 = PageUtil.transToStartEnd(1, 10);\n\t\tassertEquals(10, startEnd2[0]);\n\t\tassertEquals(20, startEnd2[1]);\n\t}\n\n\t@Test\n\tpublic void totalPage() {\n\t\tfinal int totalPage = PageUtil.totalPage(20, 3);\n\t\tassertEquals(7, totalPage);\n\t}\n\n\t@Test\n\tpublic void rainbowTest() {\n\t\tfinal int[] rainbow = PageUtil.rainbow(5, 20, 6);\n\t\tassertArrayEquals(new int[]{3, 4, 5, 6, 7, 8}, rainbow);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/PhoneUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\n\n/**\n * {@link PhoneUtil} 单元测试类\n *\n * @author dahuoyzs\n */\npublic class PhoneUtilTest {\n\n\t@Test\n\tpublic void testCheck() {\n\t\tfinal String mobile = \"13612345678\";\n\t\tfinal String tel = \"010-88993108\";\n\t\tfinal String errMobile = \"136123456781\";\n\t\tfinal String errTel = \"010-889931081\";\n\n\t\tassertTrue(PhoneUtil.isMobile(mobile));\n\t\tassertTrue(PhoneUtil.isTel(tel));\n\t\tassertTrue(PhoneUtil.isPhone(mobile));\n\t\tassertTrue(PhoneUtil.isPhone(tel));\n\n\t\tassertFalse(PhoneUtil.isMobile(errMobile));\n\t\tassertFalse(PhoneUtil.isTel(errTel));\n\t\tassertFalse(PhoneUtil.isPhone(errMobile));\n\t\tassertFalse(PhoneUtil.isPhone(errTel));\n\t}\n\n\t@Test\n\tpublic void testTel() {\n\t\tfinal ArrayList<String> tels = new ArrayList<>();\n\t\ttels.add(\"010-12345678\");\n\t\ttels.add(\"020-9999999\");\n\t\ttels.add(\"0755-7654321\");\n\t\tfinal ArrayList<String> errTels = new ArrayList<>();\n\t\terrTels.add(\"010 12345678\");\n\t\terrTels.add(\"A20-9999999\");\n\t\terrTels.add(\"0755-7654.321\");\n\t\terrTels.add(\"13619887123\");\n\t\tfor (final String s : tels) {\n\t\t\tassertTrue(PhoneUtil.isTel(s));\n\t\t}\n\t\tfor (final String s : errTels) {\n\t\t\tassertFalse(PhoneUtil.isTel(s));\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testHide() {\n\t\tfinal String mobile = \"13612345678\";\n\n\t\tassertEquals(\"*******5678\", PhoneUtil.hideBefore(mobile));\n\t\tassertEquals(\"136****5678\", PhoneUtil.hideBetween(mobile));\n\t\tassertEquals(\"1361234****\", PhoneUtil.hideAfter(mobile));\n\t}\n\n\t@Test\n\tpublic void testSubString() {\n\t\tfinal String mobile = \"13612345678\";\n\t\tassertEquals(\"136\", PhoneUtil.subBefore(mobile));\n\t\tassertEquals(\"1234\", PhoneUtil.subBetween(mobile));\n\t\tassertEquals(\"5678\", PhoneUtil.subAfter(mobile));\n\t}\n\n\t@Test\n\tpublic void testNewTel() {\n\t\tfinal ArrayList<String> tels = new ArrayList<>();\n\t\ttels.add(\"010-12345678\");\n\t\ttels.add(\"01012345678\");\n\t\ttels.add(\"020-9999999\");\n\t\ttels.add(\"0209999999\");\n\t\ttels.add(\"0755-7654321\");\n\t\ttels.add(\"07557654321\");\n\t\tfinal ArrayList<String> errTels = new ArrayList<>();\n\t\terrTels.add(\"010 12345678\");\n\t\terrTels.add(\"A20-9999999\");\n\t\terrTels.add(\"0755-7654.321\");\n\t\terrTels.add(\"13619887123\");\n\t\tfor (final String s : tels) {\n\t\t\tassertTrue(PhoneUtil.isTel(s));\n\t\t}\n\t\tfor (final String s : errTels) {\n\t\t\tassertFalse(PhoneUtil.isTel(s));\n\t\t}\n\t\tassertEquals(\"010\", PhoneUtil.subTelBefore(\"010-12345678\"));\n\t\tassertEquals(\"010\", PhoneUtil.subTelBefore(\"01012345678\"));\n\t\tassertEquals(\"12345678\", PhoneUtil.subTelAfter(\"010-12345678\"));\n\t\tassertEquals(\"12345678\", PhoneUtil.subTelAfter(\"01012345678\"));\n\n\t\tassertEquals(\"0755\", PhoneUtil.subTelBefore(\"0755-7654321\"));\n\t\tassertEquals(\"0755\", PhoneUtil.subTelBefore(\"07557654321\"));\n\t\tassertEquals(\"7654321\", PhoneUtil.subTelAfter(\"0755-7654321\"));\n\t\tassertEquals(\"7654321\", PhoneUtil.subTelAfter(\"07557654321\"));\n\t}\n\n\t@Test\n\tpublic void isTel400800Test() {\n\t\tboolean tel400800 = PhoneUtil.isTel400800(\"400-860-8608\");//800-830-3811\n\t\tassertTrue(tel400800);\n\n\t\ttel400800 = PhoneUtil.isTel400800(\"400-8608608\");//800-830-3811\n\t\tassertTrue(tel400800);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/RadixUtilTest.java",
    "content": "package cn.hutool.core.util;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class RadixUtilTest {\n\t@Test\n\tpublic void issueIDFPGRTest() {\n\t\tString radixs = \"0123456789ABC\"; // base 13\n\t\tString bad = \"1X3\"; // 'X' 不在 radix 中\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tRadixUtil.decode(radixs, bad);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.RoundingMode;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Set;\n\npublic class RandomUtilTest {\n\n\t@Test\n\tpublic void randomEleSetTest(){\n\t\tSet<Integer> set = RandomUtil.randomEleSet(CollUtil.newArrayList(1, 2, 3, 4, 5, 6), 2);\n\t\tassertEquals(set.size(), 2);\n\t}\n\n\t@Test\n\tpublic void randomElesTest(){\n\t\tList<Integer> result = RandomUtil.randomEles(CollUtil.newArrayList(1, 2, 3, 4, 5, 6), 2);\n\t\tassertEquals(result.size(), 2);\n\t}\n\n\t@Test\n\tpublic void randomDoubleTest() {\n\t\tdouble randomDouble = RandomUtil.randomDouble(0, 1, 0, RoundingMode.HALF_UP);\n\t\tassertTrue(randomDouble <= 1);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void randomBooleanTest() {\n\t\tConsole.log(RandomUtil.randomBoolean());\n\t}\n\n\t@Test\n\tpublic void randomNumberTest() {\n\t\tfinal char c = RandomUtil.randomNumber();\n\t\tassertTrue(c <= '9');\n\t}\n\n\t@Test\n\tpublic void randomIntTest() {\n\t\tfinal int c = RandomUtil.randomInt(10, 100);\n\t\tassertTrue(c >= 10 && c < 100);\n\t}\n\n\t@Test\n\tpublic void randomBytesTest() {\n\t\tfinal byte[] c = RandomUtil.randomBytes(10);\n\t\tassertNotNull(c);\n\t}\n\n\t@Test\n\tpublic void randomChineseTest(){\n\t\tchar c = RandomUtil.randomChinese();\n\t\tassertTrue(c > 0);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void randomStringWithoutStrTest() {\n\t\tfor (int i = 0; i < 100; i++) {\n\t\t\tfinal String s = RandomUtil.randomStringWithoutStr(8, \"0IPOL\");\n\t\t\tSystem.out.println(s);\n\t\t\tfor (char c : \"0IPOL\".toCharArray()) {\n\t\t\t\tassertFalse(s.contains((String.valueOf(c).toLowerCase(Locale.ROOT))));\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ReUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.PatternPool;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ReUtilTest {\n\tfinal String content = \"ZZZaaabbbccc中文1234\";\n\n\t@Test\n\tpublic void getTest() {\n\t\tfinal String resultGet = ReUtil.get(\"\\\\w{2}\", content, 0);\n\t\tassertEquals(\"ZZ\", resultGet);\n\t}\n\n\t@Test\n\tpublic void extractMultiTest() {\n\t\t// 抽取多个分组然后把它们拼接起来\n\t\tfinal String resultExtractMulti = ReUtil.extractMulti(\"(\\\\w)aa(\\\\w)\", content, \"$1-$2\");\n\t\tassertEquals(\"Z-a\", resultExtractMulti);\n\t}\n\n\t@Test\n\tpublic void extractMultiTest2() {\n\t\t// 抽取多个分组然后把它们拼接起来\n\t\tfinal String resultExtractMulti = ReUtil.extractMulti(\"(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)(\\\\w)\", content, \"$1-$2-$3-$4-$5-$6-$7-$8-$9-$10\");\n\t\tassertEquals(\"Z-Z-Z-a-a-a-b-b-b-c\", resultExtractMulti);\n\t}\n\n\t@Test\n\tpublic void delFirstTest() {\n\t\t// 删除第一个匹配到的内容\n\t\tfinal String resultDelFirst = ReUtil.delFirst(\"(\\\\w)aa(\\\\w)\", content);\n\t\tassertEquals(\"ZZbbbccc中文1234\", resultDelFirst);\n\t}\n\n\t@Test\n\tpublic void delLastTest(){\n\t\tfinal String blank = \"\";\n\t\tfinal String word = \"180公斤\";\n\t\tfinal String sentence = \"10.商品KLS100021型号xxl适合身高180体重130斤的用户\";\n\t\t//空字符串兼容\n\t\tassertEquals(blank,ReUtil.delLast(\"\\\\d+\", blank));\n\t\tassertEquals(blank,ReUtil.delLast(PatternPool.NUMBERS, blank));\n\n\t\t//去除数字\n\t\tassertEquals(\"公斤\",ReUtil.delLast(\"\\\\d+\", word));\n\t\tassertEquals(\"公斤\",ReUtil.delLast(PatternPool.NUMBERS, word));\n\t\t//去除汉字\n\t\tassertEquals(\"180\",ReUtil.delLast(\"[\\u4E00-\\u9FFF]+\", word));\n\t\tassertEquals(\"180\",ReUtil.delLast(PatternPool.CHINESES, word));\n\n\t\t//多个匹配删除最后一个 判断是否不在包含最后的数字\n\t\tString s = ReUtil.delLast(\"\\\\d+\", sentence);\n\t\tassertEquals(\"10.商品KLS100021型号xxl适合身高180体重斤的用户\", s);\n\t\ts = ReUtil.delLast(PatternPool.NUMBERS, sentence);\n\t\tassertEquals(\"10.商品KLS100021型号xxl适合身高180体重斤的用户\", s);\n\n\t\t//多个匹配删除最后一个 判断是否不在包含最后的数字\n\t\tassertFalse(ReUtil.delLast(\"[\\u4E00-\\u9FFF]+\", sentence).contains(\"斤的用户\"));\n\t\tassertFalse(ReUtil.delLast(PatternPool.CHINESES, sentence).contains(\"斤的用户\"));\n\t}\n\n\t@Test\n\tpublic void delAllTest() {\n\t\t// 删除所有匹配到的内容\n\t\tfinal String content = \"发东方大厦eee![images]http://abc.com/2.gpg]好机会eee![images]http://abc.com/2.gpg]好机会\";\n\t\tfinal String resultDelAll = ReUtil.delAll(\"!\\\\[images\\\\][^\\\\u4e00-\\\\u9fa5\\\\\\\\s]*\", content);\n\t\tassertEquals(\"发东方大厦eee好机会eee好机会\", resultDelAll);\n\t}\n\n\t@Test\n\tpublic void findAllTest() {\n\t\t// 查找所有匹配文本\n\t\tfinal List<String> resultFindAll = ReUtil.findAll(\"\\\\w{2}\", content, 0, new ArrayList<>());\n\t\tfinal ArrayList<String> expected = CollectionUtil.newArrayList(\"ZZ\", \"Za\", \"aa\", \"bb\", \"bc\", \"cc\", \"12\", \"34\");\n\t\tassertEquals(expected, resultFindAll);\n\t}\n\n\t@Test\n\tpublic void getFirstNumberTest() {\n\t\t// 找到匹配的第一个数字\n\t\tfinal Integer resultGetFirstNumber = ReUtil.getFirstNumber(content);\n\t\tassertEquals(Integer.valueOf(1234), resultGetFirstNumber);\n\t}\n\n\t@Test\n\tpublic void isMatchTest() {\n\t\t// 给定字符串是否匹配给定正则\n\t\tfinal boolean isMatch = ReUtil.isMatch(\"\\\\w+[\\u4E00-\\u9FFF]+\\\\d+\", content);\n\t\tassertTrue(isMatch);\n\t}\n\n\t@Test\n\tpublic void replaceAllTest() {\n\t\t//通过正则查找到字符串，然后把匹配到的字符串加入到replacementTemplate中，$1表示分组1的字符串\n\t\t//此处把1234替换为 ->1234<-\n\t\tfinal String replaceAll = ReUtil.replaceAll(content, \"(\\\\d+)\", \"->$1<-\");\n\t\tassertEquals(\"ZZZaaabbbccc中文->1234<-\", replaceAll);\n\t}\n\n\t@Test\n\tpublic void replaceAllTest2() {\n\t\t//此处把1234替换为 ->1234<-\n\t\tfinal String replaceAll = ReUtil.replaceAll(this.content, \"(\\\\d+)\", parameters -> \"->\" + parameters.group(1) + \"<-\");\n\t\tassertEquals(\"ZZZaaabbbccc中文->1234<-\", replaceAll);\n\t}\n\n\t@Test\n\tpublic void replaceTest() {\n\t\tfinal String str = \"AAABBCCCBBDDDBB\";\n\t\tString replace = StrUtil.replace(str, 0, \"BB\", \"22\", false);\n\t\tassertEquals(\"AAA22CCC22DDD22\", replace);\n\n\t\treplace = StrUtil.replace(str, 3, \"BB\", \"22\", false);\n\t\tassertEquals(\"AAA22CCC22DDD22\", replace);\n\n\t\treplace = StrUtil.replace(str, 4, \"BB\", \"22\", false);\n\t\tassertEquals(\"AAABBCCC22DDD22\", replace);\n\n\t\treplace = StrUtil.replace(str, 4, \"bb\", \"22\", true);\n\t\tassertEquals(\"AAABBCCC22DDD22\", replace);\n\n\t\treplace = StrUtil.replace(str, 4, \"bb\", \"\", true);\n\t\tassertEquals(\"AAABBCCCDDD\", replace);\n\n\t\treplace = StrUtil.replace(str, 4, \"bb\", null, true);\n\t\tassertEquals(\"AAABBCCCDDD\", replace);\n\t}\n\n\t@Test\n\tpublic void escapeTest() {\n\t\t//转义给定字符串，为正则相关的特殊符号转义\n\t\tfinal String escape = ReUtil.escape(\"我有个$符号{}\");\n\t\tassertEquals(\"我有个\\\\$符号\\\\{\\\\}\", escape);\n\t}\n\n\t@Test\n\tpublic void escapeTest2(){\n\t\tfinal String str = \"a[bbbc\";\n\t\tfinal String re = \"[\";\n\t\tfinal String s = ReUtil.get(ReUtil.escape(re), str, 0);\n\t\tassertEquals(\"[\", s);\n\t}\n\n\t@Test\n\tpublic void escapeTest3(){\n\t\tfinal String context = \"{prefix}_\";\n\t\tfinal String regex = \"{prefix}_\";\n\t\tfinal boolean b = ReUtil.isMatch(ReUtil.escape(regex), context);\n\t\tassertTrue(b);\n\t}\n\n\t@Test\n\tpublic void getAllGroupsTest() {\n\t\t//转义给定字符串，为正则相关的特殊符号转义\n\t\tfinal Pattern pattern = Pattern.compile(\"(\\\\d+)-(\\\\d+)-(\\\\d+)\");\n\t\tList<String> allGroups = ReUtil.getAllGroups(pattern, \"192-168-1-1\");\n\t\tassertEquals(\"192-168-1\", allGroups.get(0));\n\t\tassertEquals(\"192\", allGroups.get(1));\n\t\tassertEquals(\"168\", allGroups.get(2));\n\t\tassertEquals(\"1\", allGroups.get(3));\n\n\t\tallGroups = ReUtil.getAllGroups(pattern, \"192-168-1-1\", false);\n\t\tassertEquals(\"192\", allGroups.get(0));\n\t\tassertEquals(\"168\", allGroups.get(1));\n\t\tassertEquals(\"1\", allGroups.get(2));\n\t}\n\n\t@Test\n\tpublic void matchTest(){\n\t\tfinal boolean match = ReUtil.isMatch(\n\t\t\t\t\"(.+?)省(.+?)市(.+?)区\", \"广东省深圳市南山区\");\n\t\tConsole.log(match);\n\t}\n\n\t@Test\n\tpublic void getByGroupNameTest() {\n\t\tfinal String content = \"2021-10-11\";\n\t\tfinal String regex = \"(?<year>\\\\d+)-(?<month>\\\\d+)-(?<day>\\\\d+)\";\n\t\tfinal String year = ReUtil.get(regex, content, \"year\");\n\t\tassertEquals(\"2021\", year);\n\t\tfinal String month = ReUtil.get(regex, content, \"month\");\n\t\tassertEquals(\"10\", month);\n\t\tfinal String day = ReUtil.get(regex, content, \"day\");\n\t\tassertEquals(\"11\", day);\n\t}\n\n\t@Test\n\tpublic void getAllGroupNamesTest() {\n\t\tfinal String content = \"2021-10-11\";\n\t\tfinal String regex = \"(?<year>\\\\d+)-(?<month>\\\\d+)-(?<day>\\\\d+)\";\n\t\tfinal Map<String, String> map = ReUtil.getAllGroupNames(PatternPool.get(regex, Pattern.DOTALL), content);\n\t\tassertEquals(map.get(\"year\"), \"2021\");\n\t\tassertEquals(map.get(\"month\"), \"10\");\n\t\tassertEquals(map.get(\"day\"), \"11\");\n\t}\n\n\t@Test\n\tpublic void issuesI5TQDRTest(){\n\t\tfinal Pattern patternIp = Pattern.compile(\"((2(5[0-5]|[0-4]\\\\d))|[0-1]?\\\\d{1,2})\\\\.((2(5[0-5]|[0-4]\\\\d))|[0-1]?\\\\d{1,2})\\\\.((2(5[0-5]|[0-4]\\\\d))\"\n\t\t\t\t+ \"|[0-1]?\\\\d{1,2})\\\\.((2(5[0-5]|[0-4]\\\\d))|[0-1]?\\\\d{1,2})\");\n\t\tfinal String s = ReUtil.replaceAll(\"1.2.3.4\", patternIp, \"$1.**.**.$10\");\n\t\tassertEquals(\"1.**.**.4\", s);\n\t}\n\n\t@Test\n\tpublic void issueI6GIMTTest(){\n\t\tassertEquals(StrUtil.EMPTY, ReUtil.delAll(\"[\\\\s]*\", \" \"));\n\t}\n\n\t@Test\n\tpublic void issueI9T1TGTest() {\n\t\tString regex = \"^model\";\n\t\tString content = \"model-v\";\n\t\tString result = ReUtil.get(regex, content, 0);\n\t\tassertEquals(\"model\", result);\n\n\t\tregex = \"^model.*?\";\n\t\tcontent = \"model-v\";\n\t\tboolean match = ReUtil.isMatch(regex, content);\n\t\tassertTrue(match);\n\t}\n\n\t@Test\n\tvoid getEmailAddressTest() {\n\t\tString mail = \"姓名<a.b@Hutool.cn>\";\n\t\tString s = ReUtil.get(PatternPool.EMAIL, mail, 0);\n\t\tassertEquals(\"a.b@Hutool.cn\", s);\n\n\t\tmail = \"姓名 <a.b@Hutool.cn>\";\n\t\ts = ReUtil.get(PatternPool.EMAIL, mail, 0);\n\t\tassertEquals(\"a.b@Hutool.cn\", s);\n\n\t\tmail = \"a.b@Hutool.cn\";\n\t\ts = ReUtil.get(PatternPool.EMAIL, mail, 0);\n\t\tassertEquals(\"a.b@Hutool.cn\", s);\n\t}\n\n\t@Test\n\tvoid issueIDPHVWTest(){\n\t\tfinal String s = ReUtil.replaceAll(\"2 倾斜摄影成果\", \"(^\\\\d+(\\\\.\\\\d+)*)(\\\\s)(((.*?)(DEM|DOM)?)([（|\\\\(](.*?)[）|\\\\)])?$)\", \"$1$3$5($9)\");\n\t\tassertEquals(\"2 倾斜摄影成果()\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ReferenceUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.mutable.MutableObj;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.ref.PhantomReference;\nimport java.lang.ref.Reference;\nimport java.lang.ref.SoftReference;\nimport java.lang.ref.WeakReference;\n\npublic class ReferenceUtilTest {\n\n\t@Test\n\tpublic void createWeakTest(){\n\t\tfinal Reference<Integer> integerReference = ReferenceUtil.create(ReferenceUtil.ReferenceType.WEAK, 1);\n\t\tassertTrue(integerReference instanceof WeakReference);\n\t\tassertEquals(new Integer(1), integerReference.get());\n\t}\n\n\t@Test\n\tpublic void createSoftTest(){\n\t\tfinal Reference<Integer> integerReference = ReferenceUtil.create(ReferenceUtil.ReferenceType.SOFT, 1);\n\t\tassertTrue(integerReference instanceof SoftReference);\n\t\tassertEquals(new Integer(1), integerReference.get());\n\t}\n\n\t@Test\n\tpublic void createPhantomTest(){\n\t\tfinal Reference<Integer> integerReference = ReferenceUtil.create(ReferenceUtil.ReferenceType.PHANTOM, 1);\n\t\tassertTrue(integerReference instanceof PhantomReference);\n\t\t// get方法永远都返回null，PhantomReference只能用来监控对象的GC状况\n\t\tassertNull(integerReference.get());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void gcTest(){\n\t\t// https://blog.csdn.net/zmx729618/article/details/54093532\n\t\t// 弱引用的对象必须使用可变对象，不能使用常量对象（比如String）\n\t\tWeakReference<MutableObj<String>> reference = new WeakReference<>(new MutableObj<>(\"abc\"));\n\t\tint i=0;\n\t\twhile(true){\n\t\t\tif(reference.get()!=null){\n\t\t\t\ti++;\n\t\t\t\tConsole.log(\"Object is alive for {} loops - \", i);\n\t\t\t\tSystem.gc();\n\t\t\t}else{\n\t\t\t\tConsole.log(\"Object has been collected.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.date.Week;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.test.bean.ExamInfoDict;\nimport cn.hutool.core.util.ClassUtilTest.TestSubClass;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.FieldNameConstants;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 反射工具类单元测试\n *\n * @author Looly\n */\npublic class ReflectUtilTest {\n\n\t@Test\n\tpublic void getMethodsTest() {\n\t\tMethod[] methods = ReflectUtil.getMethods(ExamInfoDict.class);\n\t\tassertEquals(20, methods.length);\n\n\t\t//过滤器测试\n\t\tmethods = ReflectUtil.getMethods(ExamInfoDict.class, t -> Integer.class.equals(t.getReturnType()));\n\n\t\tassertEquals(4, methods.length);\n\t\tfinal Method method = methods[0];\n\t\tassertNotNull(method);\n\n\t\t//null过滤器测试\n\t\tmethods = ReflectUtil.getMethods(ExamInfoDict.class, null);\n\n\t\tassertEquals(20, methods.length);\n\t\tfinal Method method2 = methods[0];\n\t\tassertNotNull(method2);\n\t}\n\n\t@Test\n\tpublic void getMethodTest() {\n\t\tMethod method = ReflectUtil.getMethod(ExamInfoDict.class, \"getId\");\n\t\tassertEquals(\"getId\", method.getName());\n\t\tassertEquals(0, method.getParameterTypes().length);\n\n\t\tmethod = ReflectUtil.getMethod(ExamInfoDict.class, \"getId\", Integer.class);\n\t\tassertEquals(\"getId\", method.getName());\n\t\tassertEquals(1, method.getParameterTypes().length);\n\t}\n\n\t@Test\n\tpublic void getMethodIgnoreCaseTest() {\n\t\tMethod method = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, \"getId\");\n\t\tassertEquals(\"getId\", method.getName());\n\t\tassertEquals(0, method.getParameterTypes().length);\n\n\t\tmethod = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, \"GetId\");\n\t\tassertEquals(\"getId\", method.getName());\n\t\tassertEquals(0, method.getParameterTypes().length);\n\n\t\tmethod = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, \"setanswerIs\", Integer.class);\n\t\tassertEquals(\"setAnswerIs\", method.getName());\n\t\tassertEquals(1, method.getParameterTypes().length);\n\t}\n\n\t@Test\n\tpublic void getFieldTest() {\n\t\t// 能够获取到父类字段\n\t\tfinal Field privateField = ReflectUtil.getField(TestSubClass.class, \"privateField\");\n\t\tassertNotNull(privateField);\n\t}\n\n\t@Test\n\tpublic void getFieldMapTest() {\n\t\t// 获取指定类中字段名和字段对应的有序Map，包括其父类中的字段\n\t\t// 如果子类与父类中存在同名字段，则后者覆盖前者。\n\t\tMap<String, Field> fieldMap = ReflectUtil.getFieldMap(TestSubUser.class);\n\t\tassertEquals(3, fieldMap.size());\n\t}\n\n\t@Test\n\tpublic void getFieldsTest() {\n\t\t// 能够获取到父类字段\n\t\tField[] fields = ReflectUtil.getFields(TestSubClass.class);\n\t\tassertEquals(4, fields.length);\n\n\t\t// 如果子类与父类中存在同名字段，则这两个字段同时存在，子类字段在前，父类字段在后。\n\t\tfields = ReflectUtil.getFields(TestSubUser.class);\n\t\tassertEquals(4, fields.length);\n\t\tList<Field> idFieldList = Arrays.stream(fields).filter(f -> Objects.equals(f.getName(), TestSubUser.Fields.id)).collect(Collectors.toList());\n\t\tField firstIdField = CollUtil.getFirst(idFieldList);\n\t\tassertEquals(firstIdField.getDeclaringClass().getName(), TestSubUser.class.getName());\n\t}\n\n\t@Data\n\tstatic class TestBaseEntity {\n\t\tprivate Long id;\n\t\tprivate String remark;\n\t}\n\n\t@EqualsAndHashCode(callSuper = true)\n\t@Data\n\t@FieldNameConstants\n\tstatic class TestSubUser extends TestBaseEntity {\n\t\tprivate Long id;\n\t\tprivate String name;\n\t}\n\n\t@Test\n\tpublic void setFieldTest() {\n\t\tfinal TestClass testClass = new TestClass();\n\t\tReflectUtil.setFieldValue(testClass, \"a\", \"111\");\n\t\tassertEquals(111, testClass.getA());\n\t}\n\n\t@Test\n\tpublic void invokeTest() {\n\t\tfinal TestClass testClass = new TestClass();\n\t\tReflectUtil.invoke(testClass, \"setA\", 10);\n\t\tassertEquals(10, testClass.getA());\n\t}\n\n\t@Test\n\tpublic void invokeMethodTest() {\n\t\tfinal TestClass testClass = new TestClass();\n\t\tfinal Method method = ReflectUtil.getMethod(TestClass.class, \"setA\", int.class);\n\t\tReflectUtil.invoke(testClass, method, 10);\n\t\tassertEquals(10, testClass.getA());\n\t}\n\n\t@Test\n\tpublic void invokeMethodWithParamConvertTest() {\n\t\tfinal TestClass testClass = new TestClass();\n\t\tfinal Method method = ReflectUtil.getMethod(TestClass.class, \"setA\", int.class);\n\t\tReflectUtil.invoke(testClass, method, \"10\");\n\t\tassertEquals(10, testClass.getA());\n\t}\n\n\t@Test\n\tpublic void invokeMethodWithParamConvertFailedTest() {\n\t\tfinal TestClass testClass = new TestClass();\n\t\tfinal Method method = ReflectUtil.getMethod(TestClass.class, \"setA\", int.class);\n\t\tassertThrows(IllegalArgumentException.class,\n\t\t\t() -> ReflectUtil.invoke(testClass, method, \"NaN\"));\n\t}\n\n\t@Test\n\tpublic void noneStaticInnerClassTest() {\n\t\tfinal NoneStaticClass testAClass = ReflectUtil.newInstanceIfPossible(NoneStaticClass.class);\n\t\tassertNotNull(testAClass);\n\t\tassertEquals(2, testAClass.getA());\n\t}\n\n\t@Data\n\tstatic class TestClass {\n\t\tprivate int a;\n\t}\n\n\t@Data\n\t@SuppressWarnings(\"InnerClassMayBeStatic\")\n\tclass NoneStaticClass {\n\t\tprivate int a = 2;\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getMethodBenchTest() {\n\t\t// 预热\n\t\tgetMethodWithReturnTypeCheck(TestBenchClass.class, false, \"getH\");\n\n\t\tfinal TimeInterval timer = DateUtil.timer();\n\t\ttimer.start();\n\t\tfor (int i = 0; i < 100000000; i++) {\n\t\t\tReflectUtil.getMethod(TestBenchClass.class, false, \"getH\");\n\t\t}\n\t\tConsole.log(timer.interval());\n\n\t\ttimer.restart();\n\t\tfor (int i = 0; i < 100000000; i++) {\n\t\t\tgetMethodWithReturnTypeCheck(TestBenchClass.class, false, \"getH\");\n\t\t}\n\t\tConsole.log(timer.interval());\n\t}\n\n\t@Data\n\tstatic class TestBenchClass {\n\t\tprivate int a;\n\t\tprivate String b;\n\t\tprivate String c;\n\t\tprivate String d;\n\t\tprivate String e;\n\t\tprivate String f;\n\t\tprivate String g;\n\t\tprivate String h;\n\t\tprivate String i;\n\t\tprivate String j;\n\t\tprivate String k;\n\t\tprivate String l;\n\t\tprivate String m;\n\t\tprivate String n;\n\t}\n\n\t@SuppressWarnings(\"UnusedReturnValue\")\n\tpublic static Method getMethodWithReturnTypeCheck(final Class<?> clazz, final boolean ignoreCase, final String methodName, final Class<?>... paramTypes) throws SecurityException {\n\t\tif (null == clazz || StrUtil.isBlank(methodName)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tMethod res = null;\n\t\tfinal Method[] methods = ReflectUtil.getMethods(clazz);\n\t\tif (ArrayUtil.isNotEmpty(methods)) {\n\t\t\tfor (final Method method : methods) {\n\t\t\t\tif (StrUtil.equals(methodName, method.getName(), ignoreCase)\n\t\t\t\t\t&& ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes)\n\t\t\t\t\t&& (res == null\n\t\t\t\t\t|| res.getReturnType().isAssignableFrom(method.getReturnType()))) {\n\t\t\t\t\tres = method;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t@Test\n\tpublic void getMethodsFromClassExtends() {\n\t\t// 继承情况下，需解决方法去重问题\n\t\tMethod[] methods = ReflectUtil.getMethods(C2.class);\n\t\tassertEquals(15, methods.length);\n\n\t\t// 排除Object中的方法\n\t\t// 3个方法包括类\n\t\tmethods = ReflectUtil.getMethodsDirectly(C2.class, true, false);\n\t\tassertEquals(3, methods.length);\n\n\t\t// getA属于本类\n\t\tassertEquals(\"public void cn.hutool.core.util.ReflectUtilTest$C2.getA()\", methods[0].toString());\n\t\t// getB属于父类\n\t\tassertEquals(\"public void cn.hutool.core.util.ReflectUtilTest$C1.getB()\", methods[1].toString());\n\t\t// getC属于接口中的默认方法\n\t\tassertEquals(\"public default void cn.hutool.core.util.ReflectUtilTest$TestInterface1.getC()\", methods[2].toString());\n\t}\n\n\t@Test\n\tpublic void getMethodsFromInterfaceTest() {\n\t\t// 对于接口，直接调用Class.getMethods方法获取所有方法，因为接口都是public方法\n\t\t// 因此此处得到包括TestInterface1、TestInterface2、TestInterface3中一共4个方法\n\t\tfinal Method[] methods = ReflectUtil.getMethods(TestInterface3.class);\n\t\tassertEquals(4, methods.length);\n\n\t\tArrays.sort(methods, Comparator.comparing(Method::toString));\n\t\t// 接口里，调用getMethods和getPublicMethods效果相同\n\t\tfinal Method[] publicMethods = ReflectUtil.getPublicMethods(TestInterface3.class);\n\t\tArrays.sort(publicMethods, Comparator.comparing(Method::toString));\n\t\tassertArrayEquals(methods, publicMethods);\n\t}\n\n\tinterface TestInterface1 {\n\t\t@SuppressWarnings(\"unused\")\n\t\tvoid getA();\n\n\t\t@SuppressWarnings(\"unused\")\n\t\tvoid getB();\n\n\t\t@SuppressWarnings(\"unused\")\n\t\tdefault void getC() {\n\n\t\t}\n\t}\n\n\tinterface TestInterface2 extends TestInterface1 {\n\t\t@Override\n\t\tvoid getB();\n\t}\n\n\tinterface TestInterface3 extends TestInterface2 {\n\t\tvoid get3();\n\t}\n\n\t@SuppressWarnings(\"InnerClassMayBeStatic\")\n\tclass C1 implements TestInterface2 {\n\n\t\t@Override\n\t\tpublic void getA() {\n\n\t\t}\n\n\t\t@Override\n\t\tpublic void getB() {\n\n\t\t}\n\t}\n\n\tclass C2 extends C1 {\n\t\t@SuppressWarnings(\"RedundantMethodOverride\")\n\t\t@Override\n\t\tpublic void getA() {\n\n\t\t}\n\t}\n\n\t@Test\n\tpublic void newInstanceIfPossibleTest() {\n\t\t//noinspection ConstantConditions\n\t\tfinal int intValue = ReflectUtil.newInstanceIfPossible(int.class);\n\t\tassertEquals(0, intValue);\n\n\t\tfinal Integer integer = ReflectUtil.newInstanceIfPossible(Integer.class);\n\t\tassertEquals(new Integer(0), integer);\n\n\t\tfinal Map<?, ?> map = ReflectUtil.newInstanceIfPossible(Map.class);\n\t\tassertNotNull(map);\n\n\t\tfinal Collection<?> collection = ReflectUtil.newInstanceIfPossible(Collection.class);\n\t\tassertNotNull(collection);\n\n\t\tfinal Week week = ReflectUtil.newInstanceIfPossible(Week.class);\n\t\tassertEquals(Week.SUNDAY, week);\n\n\t\tfinal int[] intArray = ReflectUtil.newInstanceIfPossible(int[].class);\n\t\tassertArrayEquals(new int[0], intArray);\n\t}\n\n\tpublic static class JdbcDialects {\n\t\tprivate static final List<Number> DIALECTS =\n\t\t\tArrays.asList(1L, 2L, 3L);\n\t}\n\n\t@Test\n\tpublic void setFieldValueWithFinalTest() {\n\t\tfinal String fieldName = \"DIALECTS\";\n\t\tfinal List<Number> dialects =\n\t\t\tArrays.asList(\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t99\n\t\t\t);\n\t\tfinal Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);\n\t\tReflectUtil.removeFinalModify(field);\n\t\tReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);\n\n\t\tassertEquals(dialects, ReflectUtil.getFieldValue(JdbcDialects.class, fieldName));\n\t}\n\n\t@Test\n\tpublic void issue2625Test() {\n\t\t// 内部类继承的情况下父类方法会被定义为桥接方法，因此按照pr#1965@Github判断返回值的继承关系来代替判断桥接。\n\t\tfinal Method getThis = ReflectUtil.getMethod(A.C.class, \"getThis\");\n\t\tassertTrue(getThis.isBridge());\n\t}\n\n\t@SuppressWarnings(\"InnerClassMayBeStatic\")\n\tpublic class A {\n\n\t\tpublic class C extends B {\n\n\t\t}\n\n\t\tprotected class B {\n\t\t\tpublic B getThis() {\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\t}\n\n\t@Test\n\tpublic void newInstanceIfPossibleTest2() {\n\t\t// 测试Object.class不应该被错误地实例化为HashMap，应该返回Object实例\n\t\tObject objectInstance = ReflectUtil.newInstanceIfPossible(Object.class);\n\t\tassertNotNull(objectInstance);\n\t\tassertEquals(Object.class, objectInstance.getClass());\n\n\t\t// 测试Map.class能够正确实例化为HashMap\n\t\tMap<?, ?> mapInstance = ReflectUtil.newInstanceIfPossible(Map.class);\n\t\tassertNotNull(mapInstance);\n\t\tassertInstanceOf(HashMap.class, mapInstance);\n\n\t\t// 测试Collection.class能够正确实例化为ArrayList\n\t\tCollection<?> collectionInstance = ReflectUtil.newInstanceIfPossible(Collection.class);\n\t\tassertNotNull(collectionInstance);\n\t\tassertInstanceOf(ArrayList.class, collectionInstance);\n\n\n\t\t// 测试List.class能够正确实例化为ArrayList\n\t\tList<?> listInstance = ReflectUtil.newInstanceIfPossible(List.class);\n\t\tassertNotNull(listInstance);\n\t\tassertInstanceOf(ArrayList.class, listInstance);\n\n\t\t// 测试Set.class能够正确实例化为HashSet\n\t\tSet<?> setInstance = ReflectUtil.newInstanceIfPossible(Set.class);\n\t\tassertNotNull(setInstance);\n\t\tassertInstanceOf(HashSet.class, setInstance);\n\n\t\t// 测试Queue接口能够正确实例化为LinkedList\n\t\tQueue<?> queueInstance = ReflectUtil.newInstanceIfPossible(Queue.class);\n\t\tassertNotNull(queueInstance);\n\t\tassertInstanceOf(LinkedList.class, queueInstance);\n\n\t\t// 测试Deque接口能够正确实例化为LinkedList\n\t\tDeque<?> dequeInstance = ReflectUtil.newInstanceIfPossible(Deque.class);\n\t\tassertNotNull(dequeInstance);\n\t\tassertInstanceOf(LinkedList.class, dequeInstance);\n\t}\n\n\t/**\n\t * 验证getMethod缓存功能正常，结果与原逻辑一致\n\t */\n\t@Test\n\tpublic void testGetMethodWithCache() {\n\t\t// 1. 正常方法查找验证\n\t\tMethod method1 = ReflectUtil.getMethod(String.class, \"substring\", int.class, int.class);\n\t\tassertNotNull(method1);\n\t\tassertEquals(\"substring\", method1.getName());\n\n\t\t// 2. 验证缓存命中：多次调用返回同一个对象\n\t\tMethod method2 = ReflectUtil.getMethod(String.class, \"substring\", int.class, int.class);\n\t\tassertSame(method1, method2);\n\n\t\t// 3. 忽略大小写场景验证\n\t\tMethod methodIgnoreCase1 = ReflectUtil.getMethodIgnoreCase(String.class, \"SUBSTRING\", int.class, int.class);\n\t\tassertNotNull(methodIgnoreCase1);\n\t\tMethod methodIgnoreCase2 = ReflectUtil.getMethodIgnoreCase(String.class, \"substring\", int.class, int.class);\n\t\tassertSame(methodIgnoreCase1, methodIgnoreCase2);\n\n\t\t// 4. 边界场景：不存在的方法返回null\n\t\tMethod nullMethod = ReflectUtil.getMethod(String.class, \"notExistMethod\", String.class);\n\t\tassertNull(nullMethod);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/RuntimeUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 命令行单元测试\n * @author looly\n *\n */\npublic class RuntimeUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void execTest() {\n\t\tString str = RuntimeUtil.execForStr(\"ipconfig\");\n\t\tConsole.log(str);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void execCmdTest() {\n\t\tString str = RuntimeUtil.execForStr(\"cmd /c dir\");\n\t\tConsole.log(str);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void execCmdTest2() {\n\t\tString str = RuntimeUtil.execForStr(\"cmd /c\", \"cd \\\"C:\\\\Program Files (x86)\\\"\", \"chdir\");\n\t\tConsole.log(str);\n\t}\n\n\t@Test\n\tpublic void getUsableMemoryTest(){\n\t\tassertTrue(RuntimeUtil.getUsableMemory() > 0);\n\t}\n\n\t@Test\n\tpublic void getPidTest(){\n\t\tint pid = RuntimeUtil.getPid();\n\t\tassertTrue(pid > 0);\n\t}\n\n\t@Test\n\tpublic void getProcessorCountTest(){\n\t\tint cpu = RuntimeUtil.getProcessorCount();\n\t\tConsole.log(\"cpu个数：{}\", cpu);\n\t\tassertTrue(cpu > 0);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issueIAB5LWTest() {\n\t\tfinal String s = RuntimeUtil.execForStr(\"cmd /c netstat -aon | findstr 8080\");\n\t\tConsole.log(s);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.lang.Dict;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 字符串工具类单元测试\n *\n * @author Looly\n */\npublic class StrUtilTest {\n\n\t@Test\n\tpublic void isBlankTest() {\n\t\tfinal String blank = \"\t  　\";\n\t\tassertTrue(StrUtil.isBlank(blank));\n\t}\n\n\t@Test\n\tpublic void trimTest() {\n\t\tfinal String blank = \"\t 哈哈 　\";\n\t\tfinal String trim = StrUtil.trim(blank);\n\t\tassertEquals(\"哈哈\", trim);\n\t}\n\n\t@Test\n\tpublic void trimNewLineTest() {\n\t\tString str = \"\\r\\naaa\";\n\t\tassertEquals(\"aaa\", StrUtil.trim(str));\n\t\tstr = \"\\raaa\";\n\t\tassertEquals(\"aaa\", StrUtil.trim(str));\n\t\tstr = \"\\naaa\";\n\t\tassertEquals(\"aaa\", StrUtil.trim(str));\n\t\tstr = \"\\r\\n\\r\\naaa\";\n\t\tassertEquals(\"aaa\", StrUtil.trim(str));\n\t}\n\n\t@Test\n\tpublic void trimTabTest() {\n\t\tfinal String str = \"\\taaa\";\n\t\tassertEquals(\"aaa\", StrUtil.trim(str));\n\t}\n\n\t@Test\n\tpublic void cleanBlankTest() {\n\t\t// 包含：制表符、英文空格、不间断空白符、全角空格\n\t\tfinal String str = \"\t 你 好　\";\n\t\tfinal String cleanBlank = StrUtil.cleanBlank(str);\n\t\tassertEquals(\"你好\", cleanBlank);\n\t}\n\n\t@Test\n\tpublic void cutTest() {\n\t\tfinal String str = \"aaabbbcccdddaadfdfsdfsdf0\";\n\t\tfinal String[] cut = StrUtil.cut(str, 4);\n\t\tassertArrayEquals(new String[]{\"aaab\", \"bbcc\", \"cddd\", \"aadf\", \"dfsd\", \"fsdf\", \"0\"}, cut);\n\t}\n\n\t@Test\n\tpublic void splitTest() {\n\t\tfinal String str = \"a,b ,c,d,,e\";\n\t\tfinal List<String> split = StrUtil.split(str, ',', -1, true, true);\n\t\t// 测试空是否被去掉\n\t\tassertEquals(5, split.size());\n\t\t// 测试去掉两边空白符是否生效\n\t\tassertEquals(\"b\", split.get(1));\n\n\t\tfinal String[] strings = StrUtil.splitToArray(\"abc/\", '/');\n\t\tassertEquals(2, strings.length);\n\n\t\t// issue:I6FKSI\n\t\tassertThrows(IllegalArgumentException.class, () -> StrUtil.split(\"test length 0\", 0));\n\t}\n\n\t@Test\n\tpublic void splitEmptyTest() {\n\t\tfinal String str = \"\";\n\t\tfinal List<String> split = StrUtil.split(str, ',', -1, true, true);\n\t\t// 测试空是否被去掉\n\t\tassertEquals(0, split.size());\n\t}\n\n\t@Test\n\tpublic void splitTest2() {\n\t\tfinal String str = \"a.b.\";\n\t\tfinal List<String> split = StrUtil.split(str, '.');\n\t\tassertEquals(3, split.size());\n\t\tassertEquals(\"b\", split.get(1));\n\t\tassertEquals(\"\", split.get(2));\n\t}\n\n\t@Test\n\tpublic void splitNullTest() {\n\t\tassertEquals(0, StrUtil.split(null, '.').size());\n\t}\n\n\t@Test\n\tpublic void splitToArrayNullTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\tStrUtil.splitToArray(null, '.');\n\t\t});\n\t}\n\n\t@Test\n\tpublic void splitToLongTest() {\n\t\tfinal String str = \"1,2,3,4, 5\";\n\t\tlong[] longArray = StrUtil.splitToLong(str, ',');\n\t\tassertArrayEquals(new long[]{1, 2, 3, 4, 5}, longArray);\n\n\t\tlongArray = StrUtil.splitToLong(str, \",\");\n\t\tassertArrayEquals(new long[]{1, 2, 3, 4, 5}, longArray);\n\t}\n\n\t@Test\n\tpublic void splitToIntTest() {\n\t\tfinal String str = \"1,2,3,4, 5\";\n\t\tint[] intArray = StrUtil.splitToInt(str, ',');\n\t\tassertArrayEquals(new int[]{1, 2, 3, 4, 5}, intArray);\n\n\t\tintArray = StrUtil.splitToInt(str, \",\");\n\t\tassertArrayEquals(new int[]{1, 2, 3, 4, 5}, intArray);\n\t}\n\n\t@Test\n\tpublic void formatTest() {\n\t\tfinal String template = \"你好，我是{name}，我的电话是：{phone}\";\n\t\tfinal String result = StrUtil.format(template, Dict.create().set(\"name\", \"张三\").set(\"phone\", \"13888881111\"));\n\t\tassertEquals(\"你好，我是张三，我的电话是：13888881111\", result);\n\n\t\tfinal String result2 = StrUtil.format(template, Dict.create().set(\"name\", \"张三\").set(\"phone\", null));\n\t\tassertEquals(\"你好，我是张三，我的电话是：{phone}\", result2);\n\t}\n\n\t@Test\n\tpublic void stripTest() {\n\t\tString str = \"abcd123\";\n\t\tString strip = StrUtil.strip(str, \"ab\", \"23\");\n\t\tassertEquals(\"cd1\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.strip(str, \"ab\", \"\");\n\t\tassertEquals(\"cd123\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.strip(str, null, \"\");\n\t\tassertEquals(\"abcd123\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.strip(str, null, \"567\");\n\t\tassertEquals(\"abcd123\", strip);\n\n\t\tassertEquals(\"\", StrUtil.strip(\"a\", \"a\"));\n\t\tassertEquals(\"\", StrUtil.strip(\"a\", \"a\", \"b\"));\n\t}\n\n\t@Test\n\tpublic void stripIgnoreCaseTest() {\n\t\tString str = \"abcd123\";\n\t\tString strip = StrUtil.stripIgnoreCase(str, \"Ab\", \"23\");\n\t\tassertEquals(\"cd1\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.stripIgnoreCase(str, \"AB\", \"\");\n\t\tassertEquals(\"cd123\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.stripIgnoreCase(str, \"ab\", \"\");\n\t\tassertEquals(\"cd123\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.stripIgnoreCase(str, null, \"\");\n\t\tassertEquals(\"abcd123\", strip);\n\n\t\tstr = \"abcd123\";\n\t\tstrip = StrUtil.stripIgnoreCase(str, null, \"567\");\n\t\tassertEquals(\"abcd123\", strip);\n\t}\n\n\t@Test\n\tpublic void indexOfIgnoreCaseTest() {\n\t\tassertEquals(-1, StrUtil.indexOfIgnoreCase(null, \"balabala\", 0));\n\t\tassertEquals(-1, StrUtil.indexOfIgnoreCase(\"balabala\", null, 0));\n\t\tassertEquals(0, StrUtil.indexOfIgnoreCase(\"\", \"\", 0));\n\t\tassertEquals(0, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"A\", 0));\n\t\tassertEquals(2, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 0));\n\t\tassertEquals(1, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"AB\", 0));\n\t\tassertEquals(5, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 3));\n\t\tassertEquals(-1, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", 9));\n\t\tassertEquals(2, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"B\", -1));\n\t\tassertEquals(-1, StrUtil.indexOfIgnoreCase(\"aabaabaa\", \"\", 2));\n\t\tassertEquals(-1, StrUtil.indexOfIgnoreCase(\"abc\", \"\", 9));\n\t}\n\n\t@Test\n\tpublic void lastIndexOfTest() {\n\t\tfinal String a = \"aabbccddcc\";\n\t\tfinal int lastIndexOf = StrUtil.lastIndexOf(a, \"c\", 0, false);\n\t\tassertEquals(-1, lastIndexOf);\n\t}\n\n\t@Test\n\tpublic void lastIndexOfIgnoreCaseTest() {\n\t\tassertEquals(-1, StrUtil.lastIndexOfIgnoreCase(null, \"balabala\", 0));\n\t\tassertEquals(-1, StrUtil.lastIndexOfIgnoreCase(\"balabala\", null));\n\t\tassertEquals(0, StrUtil.lastIndexOfIgnoreCase(\"\", \"\"));\n\t\tassertEquals(7, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"A\"));\n\t\tassertEquals(5, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"B\"));\n\t\tassertEquals(4, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"AB\"));\n\t\tassertEquals(2, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"B\", 3));\n\t\tassertEquals(5, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"B\", 9));\n\t\tassertEquals(-1, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"B\", -1));\n\t\tassertEquals(-1, StrUtil.lastIndexOfIgnoreCase(\"aabaabaa\", \"\", 2));\n\t\tassertEquals(-1, StrUtil.lastIndexOfIgnoreCase(\"abc\", \"\", 9));\n\t\tassertEquals(0, StrUtil.lastIndexOfIgnoreCase(\"AAAcsd\", \"aaa\"));\n\t}\n\n\t@Test\n\tpublic void replaceTest() {\n\t\tString string = StrUtil.replaceByCodePoint(\"aabbccdd\", 2, 6, '*');\n\t\tassertEquals(\"aa****dd\", string);\n\t\tstring = StrUtil.replaceByCodePoint(\"aabbccdd\", 2, 12, '*');\n\t\tassertEquals(\"aa******\", string);\n\t}\n\n\t@Test\n\tpublic void replaceTest2() {\n\t\tfinal String result = StrUtil.replace(\"123\", \"2\", \"3\");\n\t\tassertEquals(\"133\", result);\n\t}\n\n\t@Test\n\tpublic void replaceTest3() {\n\t\tfinal String result = StrUtil.replace(\",abcdef,\", \",\", \"|\");\n\t\tassertEquals(\"|abcdef|\", result);\n\t}\n\n\t@Test\n\tpublic void replaceTest4() {\n\t\tfinal String a = \"1039\";\n\t\tfinal String result = StrUtil.padPre(a, 8, \"0\"); //在字符串1039前补4个0\n\t\tassertEquals(\"00001039\", result);\n\n\t\tfinal String aa = \"1039\";\n\t\tfinal String result1 = StrUtil.padPre(aa, -1, \"0\"); //在字符串1039前补4个0\n\t\tassertEquals(\"103\", result1);\n\t}\n\n\t@Test\n\tpublic void replaceTest5() {\n\t\tfinal String a = \"\\uD853\\uDC09秀秀\";\n\t\tfinal String result = StrUtil.replaceByCodePoint(a, 1, a.length(), '*');\n\t\tassertEquals(\"\\uD853\\uDC09**\", result);\n\n\t\tfinal String aa = \"规划大师\";\n\t\tfinal String result1 = StrUtil.replaceByCodePoint(aa, 2, a.length(), '*');\n\t\tassertEquals(\"规划**\", result1);\n\t}\n\n\t@Test\n\tpublic void upperFirstTest() {\n\t\tfinal StringBuilder sb = new StringBuilder(\"KEY\");\n\t\tfinal String s = StrUtil.upperFirst(sb);\n\t\tassertEquals(s, sb.toString());\n\t}\n\n\t@Test\n\tpublic void lowerFirstTest() {\n\t\tfinal StringBuilder sb = new StringBuilder(\"KEY\");\n\t\tfinal String s = StrUtil.lowerFirst(sb);\n\t\tassertEquals(\"kEY\", s);\n\t}\n\n\t@Test\n\tpublic void subTest() {\n\t\tfinal String a = \"abcderghigh\";\n\t\tfinal String pre = StrUtil.sub(a, -5, a.length());\n\t\tassertEquals(\"ghigh\", pre);\n\t}\n\n\t@Test\n\tpublic void subByCodePointTest() {\n\t\t// 🤔👍🍓🤔\n\t\tfinal String test = \"\\uD83E\\uDD14\\uD83D\\uDC4D\\uD83C\\uDF53\\uD83E\\uDD14\";\n\n\t\t// 不正确的子字符串\n\t\tfinal String wrongAnswer = StrUtil.sub(test, 0, 3);\n\t\tassertNotEquals(\"\\uD83E\\uDD14\\uD83D\\uDC4D\\uD83C\\uDF53\", wrongAnswer);\n\n\t\t// 正确的子字符串\n\t\tfinal String rightAnswer = StrUtil.subByCodePoint(test, 0, 3);\n\t\tassertEquals(\"\\uD83E\\uDD14\\uD83D\\uDC4D\\uD83C\\uDF53\", rightAnswer);\n\t}\n\n\t@Test\n\tpublic void subBeforeTest() {\n\t\tfinal String a = \"abcderghigh\";\n\t\tString pre = StrUtil.subBefore(a, \"d\", false);\n\t\tassertEquals(\"abc\", pre);\n\t\tpre = StrUtil.subBefore(a, 'd', false);\n\t\tassertEquals(\"abc\", pre);\n\t\tpre = StrUtil.subBefore(a, 'a', false);\n\t\tassertEquals(\"\", pre);\n\n\t\t//找不到返回原串\n\t\tpre = StrUtil.subBefore(a, 'k', false);\n\t\tassertEquals(a, pre);\n\t\tpre = StrUtil.subBefore(a, 'k', true);\n\t\tassertEquals(a, pre);\n\t}\n\n\t/**\n\t * 测试字符串反转功能，特别是对特殊字符的处理\n\t * 验证普通字符、中文字符以及Unicode代理对字符的反转行为\n\t */\n\t@Test\n\tpublic void reverseByCodePointSpecialCharactersTest() {\n\t\t//普通情况-英文字符\n\t\tassertEquals(\"dcba\", StrUtil.reverseByCodePoint(\"abcd\"));\n\n\t\t//普通情况-中文字符\n\t\tassertEquals(\"界世好你\", StrUtil.reverseByCodePoint(\"你好世界\"));\n\n\t\t//保证Unicode字符语义正确，类似emoji、组合字符\n\t\t//A😊B\n\t\tString emojiStr = \"A\\uD83D\\uDE0AB\";\n\t\tString reversedEmoji = StrUtil.reverseByCodePoint(emojiStr);\n\t\t//B😊A\n\t\tassertEquals(\"B\\uD83D\\uDE0AA\", reversedEmoji);\n\n\t\t//A🇨🇳B\n\t\tString surrogate = \"A\\uD83C\\uDDE8\\uD83C\\uDDF3B\";\n\t\tString reversedSurrogate = StrUtil.reverseByCodePoint(surrogate);\n\t\t//B🇨🇳A\n\t\tassertNotEquals(\"B\\uD83C\\uDDE8\\uD83C\\uDDF3A\", reversedSurrogate);\n\t}\n\n\t@Test\n\tpublic void subAfterTest() {\n\t\tfinal String a = \"abcderghigh\";\n\t\tString pre = StrUtil.subAfter(a, \"d\", false);\n\t\tassertEquals(\"erghigh\", pre);\n\t\tpre = StrUtil.subAfter(a, 'd', false);\n\t\tassertEquals(\"erghigh\", pre);\n\t\tpre = StrUtil.subAfter(a, 'h', true);\n\t\tassertEquals(\"\", pre);\n\n\t\t//找不到字符返回空串\n\t\tpre = StrUtil.subAfter(a, 'k', false);\n\t\tassertEquals(\"\", pre);\n\t\tpre = StrUtil.subAfter(a, 'k', true);\n\t\tassertEquals(\"\", pre);\n\t}\n\n\t@Test\n\tpublic void subSufByLengthTest() {\n\t\tassertEquals(\"cde\", StrUtil.subSufByLength(\"abcde\", 3));\n\t\tassertEquals(\"\", StrUtil.subSufByLength(\"abcde\", -1));\n\t\tassertEquals(\"\", StrUtil.subSufByLength(\"abcde\", 0));\n\t\tassertEquals(\"abcde\", StrUtil.subSufByLength(\"abcde\", 5));\n\t\tassertEquals(\"abcde\", StrUtil.subSufByLength(\"abcde\", 10));\n\t}\n\n\t@Test\n\tpublic void repeatAndJoinTest() {\n\t\tString repeatAndJoin = StrUtil.repeatAndJoin(\"?\", 5, \",\");\n\t\tassertEquals(\"?,?,?,?,?\", repeatAndJoin);\n\n\t\trepeatAndJoin = StrUtil.repeatAndJoin(\"?\", 0, \",\");\n\t\tassertEquals(\"\", repeatAndJoin);\n\n\t\trepeatAndJoin = StrUtil.repeatAndJoin(\"?\", 5, null);\n\t\tassertEquals(\"?????\", repeatAndJoin);\n\t}\n\n\t@Test\n\tpublic void moveTest() {\n\t\tfinal String str = \"aaaaaaa22222bbbbbbb\";\n\t\tString result = StrUtil.move(str, 7, 12, -3);\n\t\tassertEquals(\"aaaa22222aaabbbbbbb\", result);\n\t\tresult = StrUtil.move(str, 7, 12, -4);\n\t\tassertEquals(\"aaa22222aaaabbbbbbb\", result);\n\t\tresult = StrUtil.move(str, 7, 12, -7);\n\t\tassertEquals(\"22222aaaaaaabbbbbbb\", result);\n\t\tresult = StrUtil.move(str, 7, 12, -20);\n\t\tassertEquals(\"aaaaaa22222abbbbbbb\", result);\n\n\t\tresult = StrUtil.move(str, 7, 12, 3);\n\t\tassertEquals(\"aaaaaaabbb22222bbbb\", result);\n\t\tresult = StrUtil.move(str, 7, 12, 7);\n\t\tassertEquals(\"aaaaaaabbbbbbb22222\", result);\n\t\tresult = StrUtil.move(str, 7, 12, 20);\n\t\tassertEquals(\"aaaaaaab22222bbbbbb\", result);\n\n\t\tresult = StrUtil.move(str, 7, 12, 0);\n\t\tassertEquals(\"aaaaaaa22222bbbbbbb\", result);\n\t}\n\n\t@Test\n\tpublic void removePrefixIgnorecaseTest() {\n\t\tfinal String a = \"aaabbb\";\n\t\tString prefix = \"aaa\";\n\t\tassertEquals(\"bbb\", StrUtil.removePrefixIgnoreCase(a, prefix));\n\n\t\tprefix = \"AAA\";\n\t\tassertEquals(\"bbb\", StrUtil.removePrefixIgnoreCase(a, prefix));\n\n\t\tprefix = \"AAABBB\";\n\t\tassertEquals(\"\", StrUtil.removePrefixIgnoreCase(a, prefix));\n\t}\n\n\t@Test\n\tpublic void maxLengthTest() {\n\t\tfinal String text = \"我是一段正文，很长的正文，需要截取的正文\";\n\t\tString str = StrUtil.maxLength(text, 5);\n\t\tassertEquals(\"我是一段正...\", str);\n\t\tstr = StrUtil.maxLength(text, 21);\n\t\tassertEquals(text, str);\n\t\tstr = StrUtil.maxLength(text, 50);\n\t\tassertEquals(text, str);\n\t}\n\n\t@Test\n\tpublic void containsAnyTest() {\n\t\t//字符\n\t\tboolean containsAny = StrUtil.containsAny(\"aaabbbccc\", 'a', 'd');\n\t\tassertTrue(containsAny);\n\t\tcontainsAny = StrUtil.containsAny(\"aaabbbccc\", 'e', 'd');\n\t\tassertFalse(containsAny);\n\t\tcontainsAny = StrUtil.containsAny(\"aaabbbccc\", 'd', 'c');\n\t\tassertTrue(containsAny);\n\n\t\t//字符串\n\t\tcontainsAny = StrUtil.containsAny(\"aaabbbccc\", \"a\", \"d\");\n\t\tassertTrue(containsAny);\n\t\tcontainsAny = StrUtil.containsAny(\"aaabbbccc\", \"e\", \"d\");\n\t\tassertFalse(containsAny);\n\t\tcontainsAny = StrUtil.containsAny(\"aaabbbccc\", \"d\", \"c\");\n\t\tassertTrue(containsAny);\n\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I7WSYD\n\t\tcontainsAny = StrUtil.containsAny(\"你好啊\", \"嗯\", null);\n\t\tassertFalse(containsAny);\n\t}\n\n\t@Test\n\tpublic void centerTest() {\n\t\tassertNull(StrUtil.center(null, 10));\n\t\tassertEquals(\"    \", StrUtil.center(\"\", 4));\n\t\tassertEquals(\"ab\", StrUtil.center(\"ab\", -1));\n\t\tassertEquals(\" ab \", StrUtil.center(\"ab\", 4));\n\t\tassertEquals(\"abcd\", StrUtil.center(\"abcd\", 2));\n\t\tassertEquals(\" a  \", StrUtil.center(\"a\", 4));\n\t}\n\n\t@Test\n\tpublic void padPreTest() {\n\t\tassertNull(StrUtil.padPre(null, 10, ' '));\n\t\tassertEquals(\"001\", StrUtil.padPre(\"1\", 3, '0'));\n\t\tassertEquals(\"12\", StrUtil.padPre(\"123\", 2, '0'));\n\n\t\tassertNull(StrUtil.padPre(null, 10, \"AA\"));\n\t\tassertEquals(\"AB1\", StrUtil.padPre(\"1\", 3, \"ABC\"));\n\t\tassertEquals(\"12\", StrUtil.padPre(\"123\", 2, \"ABC\"));\n\t}\n\n\t@Test\n\tpublic void padAfterTest() {\n\t\tassertNull(StrUtil.padAfter(null, 10, ' '));\n\t\tassertEquals(\"100\", StrUtil.padAfter(\"1\", 3, '0'));\n\t\tassertEquals(\"23\", StrUtil.padAfter(\"123\", 2, '0'));\n\t\tassertEquals(\"\", StrUtil.padAfter(\"123\", -1, '0'));\n\n\t\tassertNull(StrUtil.padAfter(null, 10, \"ABC\"));\n\t\tassertEquals(\"1AB\", StrUtil.padAfter(\"1\", 3, \"ABC\"));\n\t\tassertEquals(\"23\", StrUtil.padAfter(\"123\", 2, \"ABC\"));\n\t}\n\n\t@Test\n\tpublic void subBetweenAllTest() {\n\t\tassertArrayEquals(new String[]{\"yz\", \"abc\"}, StrUtil.subBetweenAll(\"saho[yz]fdsadp[abc]a\", \"[\", \"]\"));\n\t\tassertArrayEquals(new String[]{\"abc\"}, StrUtil.subBetweenAll(\"saho[yzfdsadp[abc]a]\", \"[\", \"]\"));\n\t\tassertArrayEquals(new String[]{\"abc\", \"abc\"}, StrUtil.subBetweenAll(\"yabczyabcz\", \"y\", \"z\"));\n\t\tassertArrayEquals(new String[0], StrUtil.subBetweenAll(null, \"y\", \"z\"));\n\t\tassertArrayEquals(new String[0], StrUtil.subBetweenAll(\"\", \"y\", \"z\"));\n\t\tassertArrayEquals(new String[0], StrUtil.subBetweenAll(\"abc\", null, \"z\"));\n\t\tassertArrayEquals(new String[0], StrUtil.subBetweenAll(\"abc\", \"y\", null));\n\t}\n\n\t@Test\n\tpublic void subBetweenAllTest2() {\n\t\t//issue#861@Github，起始不匹配的时候，应该直接空\n\t\tfinal String src1 = \"/* \\n* hutool  */  asdas  /* \\n* hutool  */\";\n\t\tfinal String src2 = \"/ * hutool  */  asdas  / * hutool  */\";\n\n\t\tfinal String[] results1 = StrUtil.subBetweenAll(src1, \"/**\", \"*/\");\n\t\tassertEquals(0, results1.length);\n\n\t\tfinal String[] results2 = StrUtil.subBetweenAll(src2, \"/*\", \"*/\");\n\t\tassertEquals(0, results2.length);\n\t}\n\n\t@Test\n\tpublic void subBetweenAllTest3() {\n\t\tfinal String src1 = \"'abc'and'123'\";\n\t\tString[] strings = StrUtil.subBetweenAll(src1, \"'\", \"'\");\n\t\tassertEquals(2, strings.length);\n\t\tassertEquals(\"abc\", strings[0]);\n\t\tassertEquals(\"123\", strings[1]);\n\n\t\tfinal String src2 = \"'abc''123'\";\n\t\tstrings = StrUtil.subBetweenAll(src2, \"'\", \"'\");\n\t\tassertEquals(2, strings.length);\n\t\tassertEquals(\"abc\", strings[0]);\n\t\tassertEquals(\"123\", strings[1]);\n\n\t\tfinal String src3 = \"'abc'123'\";\n\t\tstrings = StrUtil.subBetweenAll(src3, \"'\", \"'\");\n\t\tassertEquals(1, strings.length);\n\t\tassertEquals(\"abc\", strings[0]);\n\t}\n\n\t@Test\n\tpublic void subBetweenAllTest4() {\n\t\tfinal String str = \"你好:1388681xxxx用户已开通,1877275xxxx用户已开通,无法发送业务开通短信\";\n\t\tfinal String[] strings = StrUtil.subBetweenAll(str, \"1877275xxxx\", \",\");\n\t\tassertEquals(1, strings.length);\n\t\tassertEquals(\"用户已开通\", strings[0]);\n\t}\n\n\t@Test\n\tpublic void briefTest() {\n\t\t// case: 1 至 str.length - 1\n\t\tfinal String str = RandomUtil.randomString(RandomUtil.randomInt(1, 100));\n\t\tfor (int maxLength = 1; maxLength < str.length(); maxLength++) {\n\t\t\tfinal String brief = StrUtil.brief(str, maxLength);\n\t\t\tassertEquals(brief.length(), maxLength);\n\t\t}\n\n\t\t// case: 不会格式化的值\n\t\tassertEquals(str, StrUtil.brief(str, 0));\n\t\tassertEquals(str, StrUtil.brief(str, -1));\n\t\tassertEquals(str, StrUtil.brief(str, str.length()));\n\t\tassertEquals(str, StrUtil.brief(str, str.length() + 1));\n\t}\n\n\t@Test\n\tpublic void briefTest2() {\n\t\tfinal String str = \"123\";\n\t\tint maxLength = 3;\n\t\tString brief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"123\", brief);\n\n\t\tmaxLength = 2;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1.\", brief);\n\n\t\tmaxLength = 1;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1\", brief);\n\t}\n\n\t@Test\n\tpublic void briefTest3() {\n\t\tfinal String str = \"123abc\";\n\n\t\tint maxLength = 6;\n\t\tString brief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(str, brief);\n\n\t\tmaxLength = 5;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1...c\", brief);\n\n\t\tmaxLength = 4;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1..c\", brief);\n\n\t\tmaxLength = 3;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1.c\", brief);\n\n\t\tmaxLength = 2;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1.\", brief);\n\n\t\tmaxLength = 1;\n\t\tbrief = StrUtil.brief(str, maxLength);\n\t\tassertEquals(\"1\", brief);\n\t}\n\n\t@Test\n\tpublic void filterTest() {\n\t\tfinal String filterNumber = StrUtil.filter(\"hutool678\", CharUtil::isNumber);\n\t\tassertEquals(\"678\", filterNumber);\n\t\tfinal String cleanBlank = StrUtil.filter(\"\t 你 好　\", c -> !CharUtil.isBlankChar(c));\n\t\tassertEquals(\"你好\", cleanBlank);\n\t}\n\n\t@Test\n\tpublic void wrapAllTest() {\n\t\tString[] strings = StrUtil.wrapAll(\"`\", \"`\", StrUtil.splitToArray(\"1,2,3,4\", ','));\n\t\tassertEquals(\"[`1`, `2`, `3`, `4`]\", StrUtil.utf8Str(strings));\n\n\t\tstrings = StrUtil.wrapAllWithPair(\"`\", StrUtil.splitToArray(\"1,2,3,4\", ','));\n\t\tassertEquals(\"[`1`, `2`, `3`, `4`]\", StrUtil.utf8Str(strings));\n\t}\n\n\t@Test\n\tpublic void startWithTest() {\n\t\tfinal String a = \"123\";\n\t\tfinal String b = \"123\";\n\n\t\tassertTrue(StrUtil.startWith(a, b));\n\t\tassertFalse(StrUtil.startWithIgnoreEquals(a, b));\n\t}\n\n\t@Test\n\tpublic void indexedFormatTest() {\n\t\tfinal String ret = StrUtil.indexedFormat(\"this is {0} for {1}\", \"a\", 1000);\n\t\tassertEquals(\"this is a for 1,000\", ret);\n\t}\n\n\t@Test\n\tpublic void hideTest() {\n\t\tassertNull(StrUtil.hide(null, 1, 1));\n\t\tassertEquals(\"\", StrUtil.hide(\"\", 1, 1));\n\t\tassertEquals(\"****duan@163.com\", StrUtil.hide(\"jackduan@163.com\", -1, 4));\n\t\tassertEquals(\"ja*kduan@163.com\", StrUtil.hide(\"jackduan@163.com\", 2, 3));\n\t\tassertEquals(\"jackduan@163.com\", StrUtil.hide(\"jackduan@163.com\", 3, 2));\n\t\tassertEquals(\"jackduan@163.com\", StrUtil.hide(\"jackduan@163.com\", 16, 16));\n\t\tassertEquals(\"jackduan@163.com\", StrUtil.hide(\"jackduan@163.com\", 16, 17));\n\t}\n\n\n\t@Test\n\tpublic void isCharEqualsTest() {\n\t\tfinal String a = \"aaaaaaaaa\";\n\t\tassertTrue(StrUtil.isCharEquals(a));\n\t}\n\n\t@Test\n\tpublic void isNumericTest() {\n\t\tfinal String a = \"2142342422423423\";\n\t\tassertTrue(StrUtil.isNumeric(a));\n\t}\n\n\t@Test\n\tpublic void containsAllTest() {\n\t\tfinal String a = \"2142342422423423\";\n\t\tassertTrue(StrUtil.containsAll(a, \"214\", \"234\"));\n\t}\n\n\t@Test\n\tpublic void replaceLastTest() {\n\t\tfinal String str = \"i am jackjack\";\n\t\tfinal String result = StrUtil.replaceLast(str, \"JACK\", null, true);\n\t\tassertEquals(\"i am jack\", result);\n\t}\n\n\t@Test\n\tpublic void replaceFirstTest() {\n\t\tfinal String str = \"yesyes i do\";\n\t\tfinal String result = StrUtil.replaceFirst(str, \"YES\", \"\", true);\n\t\tassertEquals(\"yes i do\", result);\n\t}\n\n\t@Test\n\tpublic void issueI5YN49Test() {\n\t\tfinal String str = \"A5E6005700000000000000000000000000000000000000090D0100000000000001003830\";\n\t\tassertEquals(\"38\", StrUtil.subWithLength(str, -2, 2));\n\t}\n\n\t@Test\n\tpublic void issueI6KKFUTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I6KKFU\n\t\tfinal String template = \"I''m {0} years old.\";\n\t\tfinal String result = StrUtil.indexedFormat(template, 10);\n\t\tassertEquals(\"I'm 10 years old.\", result);\n\t}\n\n\t@Test\n\tpublic void truncateUtf8Test() {\n\t\tfinal String str = \"这是This一段中英文\";\n\t\tString ret = StrUtil.truncateUtf8(str, 12);\n\t\tassertEquals(\"这是Thi...\", ret);\n\n\t\tret = StrUtil.truncateUtf8(str, 13);\n\t\tassertEquals(\"这是This...\", ret);\n\n\t\tret = StrUtil.truncateUtf8(str, 14);\n\t\tassertEquals(\"这是This...\", ret);\n\n\t\tret = StrUtil.truncateUtf8(str, 999);\n\t\tassertEquals(str, ret);\n\t}\n\n\t@Test\n\tpublic void truncateUtf8Test2() {\n\t\tfinal String str = \"这是This一\";\n\t\tfinal String ret = StrUtil.truncateUtf8(str, 13);\n\t\tassertEquals(\"这是This一\", ret);\n\t}\n\n\t@Test\n\tpublic void truncateUtf8Test3() {\n\t\tfinal String str = \"一二三四\";\n\t\tfinal String ret = StrUtil.truncateUtf8(str, 11);\n\t\tassertEquals(\"一二...\", ret);\n\t}\n\n\t@Test\n\tpublic void truncateByByteLengthTest() {\n\t\tfinal String str = \"This is English\";\n\t\tfinal String ret = StrUtil.truncateByByteLength(str, StandardCharsets.ISO_8859_1, 10, 1, false);\n\t\tassertEquals(\"This is En\", ret);\n\t}\n\n\t@Test\n\tpublic void issueTest() {\n\t\tfinal String s = \"abc\";\n\t\tfinal String r = StrUtil.truncateByByteLength(s, CharsetUtil.CHARSET_UTF_8, 2, 4, true);\n\t\tassertEquals(\"ab\", r);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/TypeUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport java.lang.reflect.Array;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport cn.hutool.core.lang.TypeReference;\nimport lombok.Data;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport org.junit.jupiter.api.Test;\n\npublic class TypeUtilTest {\n\n\t@Test\n\tpublic void getEleTypeTest() {\n\t\tMethod method = ReflectUtil.getMethod(TestClass.class, \"getList\");\n\t\tType type = TypeUtil.getReturnType(method);\n\t\tassertEquals(\"java.util.List<java.lang.String>\", type.toString());\n\n\t\tType type2 = TypeUtil.getTypeArgument(type);\n\t\tassertEquals(String.class, type2);\n\t}\n\n\t@Test\n\tpublic void getParamTypeTest() {\n\t\tMethod method = ReflectUtil.getMethod(TestClass.class, \"intTest\", Integer.class);\n\t\tType type = TypeUtil.getParamType(method, 0);\n\t\tassertEquals(Integer.class, type);\n\n\t\tType returnType = TypeUtil.getReturnType(method);\n\t\tassertEquals(Integer.class, returnType);\n\t}\n\n\t@Test\n\tpublic void getClasses() {\n\t\tMethod method = ReflectUtil.getMethod(Parent.class, \"getLevel\");\n\t\tType returnType = TypeUtil.getReturnType(method);\n\t\tClass<?> clazz = TypeUtil.getClass(returnType);\n\t\tassertEquals(Level1.class, clazz);\n\n\t\tmethod = ReflectUtil.getMethod(Level1.class, \"getId\");\n\t\treturnType = TypeUtil.getReturnType(method);\n\t\tclazz = TypeUtil.getClass(returnType);\n\t\tassertEquals(Object.class, clazz);\n\t}\n\n\t/**\n\t * 测试getClass方法对泛型数组类型T[]的处理\n\t * 验证未绑定泛型参数的数组类型会被正确解析为Object[]\n\t */\n\t@Test\n\tpublic void getClassForGenericArrayTypeTest() throws NoSuchFieldException {\n\t\t// 获取T[]类型字段的泛型类型\n\t\tField levelField = GenericArray.class.getDeclaredField(\"level\");\n\t\tType genericArrayType = levelField.getGenericType();\n\t\t// 调用getClass方法处理GenericArrayType\n\t\tClass<?> clazz = TypeUtil.getClass(genericArrayType);\n\t\t// 验证返回Object[]类型\n\t\tassertNotNull(clazz, \"getClass方法返回null\");\n\t\tassertTrue(clazz.isArray(), \"返回类型不是数组\");\n\t\tassertEquals(Object.class, clazz.getComponentType(), \"数组组件类型应为Object\");\n\t}\n\n\t/**\n\t * 测试getClass方法对参数化类型数组{@code List<String>[]}的处理\n\t * 验证数组组件类型能正确解析为原始类型\n\t */\n\t@Test\n\tpublic void getClassForParameterizedArrayTypeTest() {\n\t\t// 创建List<String>[]类型引用\n\t\tType genericArrayType = new TypeReference<List<String>[]>() {}.getType();\n\t\t// 调用getClass方法处理GenericArrayType\n\t\tClass<?> clazz = TypeUtil.getClass(genericArrayType);\n\t\t// 验证返回List[]类型\n\t\tassertEquals(Array.newInstance(List.class, 0).getClass(), clazz);\n\t}\n\n\tpublic static class TestClass {\n\t\tpublic List<String> getList() {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\n\t\tpublic Integer intTest(Integer integer) {\n\t\t\treturn 1;\n\t\t}\n\n\t}\n\n\t@Test\n\tpublic void getTypeArgumentTest() {\n\t\t// 测试不继承父类，而是实现泛型接口时是否可以获取成功。\n\t\tfinal Type typeArgument = TypeUtil.getTypeArgument(IPService.class);\n\t\tassertEquals(String.class, typeArgument);\n\t}\n\n\tpublic interface OperateService<T> {\n\t\tvoid service(T t);\n\t}\n\n\tpublic static class IPService implements OperateService<String> {\n\t\t@Override\n\t\tpublic void service(String string) {\n\t\t}\n\t}\n\n\t@Test\n\tpublic void getActualTypesTest() {\n\t\t// 测试多层级泛型参数是否能获取成功\n\t\tType idType = TypeUtil.getActualType(Level3.class, ReflectUtil.getField(Level3.class, \"id\"));\n\n\t\tassertEquals(Long.class, idType);\n\t}\n\n\tpublic static class Level3 extends Level2<Level3> {\n\n\t}\n\n\tpublic static class Level2<E> extends Level1<Long> {\n\n\t}\n\n\t@Data\n\tpublic static class Level1<T> {\n\t\tprivate T id;\n\t}\n\n\t@Data\n\tpublic static class Parent<T extends Level1<B>, B extends Long> {\n\t\tprivate T level;\n\t}\n\n\n\t/**\n\t * fix github:issue#3873\n\t */\n\t@Test\n\tpublic void getActualTypeForGenericArrayTest() {\n\t\tTypeReference<GenericArray<GenericArrayEle>> typeReference = new TypeReference<GenericArray<GenericArrayEle>>() {\n\n\t\t};\n\n\t\tType levelType = TypeUtil.getFieldType(GenericArray.class, \"level\");\n\t\tType actualType = TypeUtil.getActualType(typeReference.getType(), levelType);\n\t\tassertEquals(ArrayUtil.getArrayType(GenericArrayEle.class), actualType);\n\t}\n\n\t@Data\n\tpublic static class GenericArray<T> {\n\t\tprivate T[] level;\n\t}\n\n\t@Data\n\tpublic static class GenericArrayEle {\n\t\tprivate Long uid;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/URLUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\n\n/**\n * URLUtil单元测试\n *\n * @author looly\n *\n */\npublic class URLUtilTest {\n\n\t@Test\n\tpublic void normalizeTest() {\n\t\t// issue#I25MZL，多个/被允许\n\t\tString url = \"http://www.hutool.cn//aaa/bbb\";\n\t\tString normalize = URLUtil.normalize(url);\n\t\tassertEquals(\"http://www.hutool.cn//aaa/bbb\", normalize);\n\n\t\turl = \"www.hutool.cn//aaa/bbb\";\n\t\tnormalize = URLUtil.normalize(url);\n\t\tassertEquals(\"http://www.hutool.cn//aaa/bbb\", normalize);\n\t}\n\n\t@Test\n\tpublic void normalizeTest2() {\n\t\tString url = \"http://www.hutool.cn//aaa/\\\\bbb?a=1&b=2\";\n\t\tString normalize = URLUtil.normalize(url);\n\t\tassertEquals(\"http://www.hutool.cn//aaa//bbb?a=1&b=2\", normalize);\n\n\t\turl = \"www.hutool.cn//aaa/bbb?a=1&b=2\";\n\t\tnormalize = URLUtil.normalize(url);\n\t\tassertEquals(\"http://www.hutool.cn//aaa/bbb?a=1&b=2\", normalize);\n\t}\n\n\t@Test\n\tpublic void normalizeTest3() {\n\t\tString url = \"http://www.hutool.cn//aaa/\\\\bbb?a=1&b=2\";\n\t\tString normalize = URLUtil.normalize(url, true);\n\t\tassertEquals(\"http://www.hutool.cn//aaa//bbb?a=1&b=2\", normalize);\n\n\t\turl = \"www.hutool.cn//aaa/bbb?a=1&b=2\";\n\t\tnormalize = URLUtil.normalize(url, true);\n\t\tassertEquals(\"http://www.hutool.cn//aaa/bbb?a=1&b=2\", normalize);\n\n\t\turl = \"\\\\/www.hutool.cn//aaa/bbb?a=1&b=2\";\n\t\tnormalize = URLUtil.normalize(url, true);\n\t\tassertEquals(\"http://www.hutool.cn//aaa/bbb?a=1&b=2\", normalize);\n\t}\n\n\t@Test\n\tpublic void normalizeIpv6Test() {\n\t\tString url = \"http://[fe80::8f8:2022:a603:d180]:9439\";\n\t\tString normalize = URLUtil.normalize(\"http://[fe80::8f8:2022:a603:d180]:9439\", true);\n\t\tassertEquals(url, normalize);\n\t}\n\n\t@Test\n\tpublic void formatTest() {\n\t\tString url = \"//www.hutool.cn//aaa/\\\\bbb?a=1&b=2\";\n\t\tString normalize = URLUtil.normalize(url);\n\t\tassertEquals(\"http://www.hutool.cn//aaa//bbb?a=1&b=2\", normalize);\n\t}\n\n\t@Test\n\tpublic void getHostTest() throws MalformedURLException {\n\t\tString url = \"https://www.hutool.cn//aaa/\\\\bbb?a=1&b=2\";\n\t\tString normalize = URLUtil.normalize(url);\n\t\tURI host = URLUtil.getHost(new URL(normalize));\n\t\tassertEquals(\"https://www.hutool.cn\", host.toString());\n\t}\n\n\t@Test\n\tpublic void encodeTest() {\n\t\tString body = \"366466 - 副本.jpg\";\n\t\tString encode = URLUtil.encode(body);\n\t\tassertEquals(\"366466%20-%20%E5%89%AF%E6%9C%AC.jpg\", encode);\n\t\tassertEquals(body, URLUtil.decode(encode));\n\n\t\tString encode2 = URLUtil.encodeQuery(body);\n\t\tassertEquals(\"366466%20-%20%E5%89%AF%E6%9C%AC.jpg\", encode2);\n\t}\n\n\t@Test\n\tpublic void encodeQueryPlusTest() {\n\t\tString body = \"+\";\n\t\tString encode2 = URLUtil.encodeQuery(body);\n\t\tassertEquals(\"+\", encode2);\n\t}\n\n\t@Test\n\tpublic void getPathTest(){\n\t\tString url = \" http://www.aaa.bbb/search?scope=ccc&q=ddd\";\n\t\tString path = URLUtil.getPath(url);\n\t\tassertEquals(\"/search\", path);\n\t}\n\n\t@Test\n\tpublic void issue3676Test() {\n\t\tString fileFullName = \"/Uploads/20240601/aaaa.txt\";\n\t\tfinal URI uri = URLUtil.toURI(fileFullName);\n\t\tfinal URI resolve = uri.resolve(\".\");\n\t\tassertEquals(\"/Uploads/20240601/\", resolve.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/VersionUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass VersionUtilTest {\n\n\t@Test\n\tvoid isGreaterThan() {\n\t\tString currentVersion = \" 1.0.2\";\n\t\tassertTrue(VersionUtil.isGreaterThan(currentVersion, \"1.0.1\"));\n\t\tassertTrue(VersionUtil.isGreaterThan(currentVersion, \"1\"));\n\t\tassertFalse(VersionUtil.isGreaterThan(currentVersion, \"1.1\"));\n\t}\n\n\t@Test\n\tvoid isGreaterThanOrEqual() {\n\t\tString currentVersion = \"1.0.2 \";\n\t\tassertTrue(VersionUtil.isGreaterThanOrEqual(currentVersion, \"1.0.1\"));\n\t\tassertTrue(VersionUtil.isGreaterThanOrEqual(currentVersion, \"1.0.2\"));\n\t\tassertFalse(VersionUtil.isGreaterThanOrEqual(currentVersion, \"1.1\"));\n\t}\n\n\t@Test\n\tvoid isLessThan() {\n\t\tString currentVersion = \"1.0.2\";\n\t\tassertTrue(VersionUtil.isLessThan(currentVersion, \"1.0.3\"));\n\t\tassertFalse(VersionUtil.isLessThan(currentVersion, \"1\"));\n\t\tassertTrue(VersionUtil.isLessThan(currentVersion, \"1.1\"));\n\t\tassertFalse(VersionUtil.isLessThan(currentVersion, \"1.0.2\"));\n\t}\n\n\t@Test\n\tvoid isLessThanOrEqual() {\n\t\tString currentVersion = \"1.0.2\";\n\t\tassertTrue(VersionUtil.isLessThanOrEqual(currentVersion, \"1.0.2\"));\n\t\tassertFalse(VersionUtil.isLessThanOrEqual(currentVersion, \"1.0.1\"));\n\t\tassertTrue(VersionUtil.isLessThanOrEqual(currentVersion, \"1.1\"));\n\t}\n\n\t@Test\n\tvoid matchEl() {\n\t\tString currentVersion = \"1.0.2\";\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.1;1.0.2\"));\n\t\tassertFalse(VersionUtil.matchEl(currentVersion, \"1.0.1;1.0.3\"));\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.9;1.0.1-1.0.2\"));\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.9;1.0.1-1.0.3\"));\n\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.9,1.0.1-1.0.3\", \",\"));\n\t}\n\n\t@Test\n\tvoid matchEl_Exception_whenVersionDelimiterIllegal() {\n\t\tList<String> illegalDelimiters = ListUtil.of(\"-\", \">\", \">=\", \"<\", \"<=\", \"≥\", \"≤\", null, \"\", \" \");\n\n\t\tfor (String illegalDelimiter : illegalDelimiters) {\n\t\t\tassertThrows(UtilException.class, () -> {\n\t\t\t\tString currentVersion = \"1.0.2\";\n\t\t\t\tVersionUtil.matchEl(currentVersion, \"1.0.1;1.0.2\", illegalDelimiter);\n\t\t\t});\n\t\t}\n\t}\n\n\t@Test\n\tvoid anyMatch() {\n\t\tString currentVersion = \"1.0.2\";\n\t\tassertTrue(VersionUtil.anyMatch(currentVersion, ListUtil.of(\"1.0.1\", \"1.0.3\", \"1.0.2\")));\n\t\tassertTrue(VersionUtil.anyMatch(currentVersion, \"1.0.1\", \"1.0.2\"));\n\t}\n\n\t@Test\n\tvoid testMatchEl() {\n\t}\n\n\t/**\n\t * 测试版本范围表达式边界情况\n\t * 1. 左边界为空的情况: \"-1.0.3\" 应该匹配小于等于1.0.3的版本\n\t * 2. 右边界为空的情况: \"1.0.0-\" 应该匹配大于等于1.0.0的版本\n\t * 3. 双边界为空的情况: \"-\" 应该匹配所有版本\n\t * 验证 VersionUtil.matchEl 方法对边界值的正确处理\n\t */\n\t@Test\n\tvoid matchEl_rangeBoundaryCases() {\n\t\tString currentVersion = \"1.0.2\";\n\n\t\t// 测试左边界为空的情况: \"-1.0.3\" 应该匹配小于等于1.0.3的版本\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"-1.0.3\"));\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"-1.0.2\"));\n\t\tassertFalse(VersionUtil.matchEl(currentVersion, \"-1.0.0\"));\n\n\t\t// 测试右边界为空的情况: \"1.0.0-\" 应该匹配大于等于1.0.0的版本\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.0-\"));\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"1.0.2-\"));\n\t\tassertFalse(VersionUtil.matchEl(currentVersion, \"1.0.3-\"));\n\n\t\t// 测试双边为空的情况: \"-\" 应该匹配所有版本\n\t\tassertTrue(VersionUtil.matchEl(currentVersion, \"-\"));\n\t\tassertTrue(VersionUtil.matchEl(\"0.0.1\", \"-\"));\n\t\tassertTrue(VersionUtil.matchEl(\"999.999.999\", \"-\"));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/XmlUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapBuilder;\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.Attributes;\nimport org.xml.sax.helpers.DefaultHandler;\n\nimport javax.xml.xpath.XPathConstants;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link XmlUtil} 工具类\n *\n * @author Looly\n */\npublic class XmlUtilTest {\n\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal String result = \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\" ?>\"//\n\t\t\t\t+ \"<returnsms>\"//\n\t\t\t\t+ \"<returnstatus>Success</returnstatus>\"//\n\t\t\t\t+ \"<message>ok</message>\"//\n\t\t\t\t+ \"<remainpoint>1490</remainpoint>\"//\n\t\t\t\t+ \"<taskID>885</taskID>\"//\n\t\t\t\t+ \"<successCounts>1</successCounts>\"//\n\t\t\t\t+ \"</returnsms>\";\n\t\tfinal Document docResult = XmlUtil.parseXml(result);\n\t\tfinal String elementText = XmlUtil.elementText(docResult.getDocumentElement(), \"returnstatus\");\n\t\tassertEquals(\"Success\", elementText);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tfinal String result = \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\" ?>\"//\n\t\t\t\t+ \"<returnsms>\"//\n\t\t\t\t+ \"<returnstatus>Success（成功）</returnstatus>\"//\n\t\t\t\t+ \"<message>ok</message>\"//\n\t\t\t\t+ \"<remainpoint>1490</remainpoint>\"//\n\t\t\t\t+ \"<taskID>885</taskID>\"//\n\t\t\t\t+ \"<successCounts>1</successCounts>\"//\n\t\t\t\t+ \"</returnsms>\";\n\t\tfinal Document docResult = XmlUtil.parseXml(result);\n\t\tXmlUtil.toFile(docResult, \"e:/aaa.xml\", \"utf-8\");\n\t}\n\n\t@Test\n\tpublic void xpathTest() {\n\t\tfinal String result = \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\" ?>\"//\n\t\t\t\t+ \"<returnsms>\"//\n\t\t\t\t+ \"<returnstatus>Success（成功）</returnstatus>\"//\n\t\t\t\t+ \"<message>ok</message>\"//\n\t\t\t\t+ \"<remainpoint>1490</remainpoint>\"//\n\t\t\t\t+ \"<taskID>885</taskID>\"//\n\t\t\t\t+ \"<successCounts>1</successCounts>\"//\n\t\t\t\t+ \"</returnsms>\";\n\t\tfinal Document docResult = XmlUtil.parseXml(result);\n\t\tfinal Object value = XmlUtil.getByXPath(\"//returnsms/message\", docResult, XPathConstants.STRING);\n\t\tassertEquals(\"ok\", value);\n\t}\n\n\t@Test\n\tpublic void xpathTest2() {\n\t\tfinal String result = ResourceUtil.readUtf8Str(\"test.xml\");\n\t\tfinal Document docResult = XmlUtil.parseXml(result);\n\t\tfinal Object value = XmlUtil.getByXPath(\"//returnsms/message\", docResult, XPathConstants.STRING);\n\t\tassertEquals(\"ok\", value);\n\t}\n\n\t@Test\n\tpublic void xmlToMapTest() {\n\t\tfinal String xml = \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\" ?>\"//\n\t\t\t\t+ \"<returnsms>\"//\n\t\t\t\t+ \"<returnstatus>Success</returnstatus>\"//\n\t\t\t\t+ \"<message>ok</message>\"//\n\t\t\t\t+ \"<remainpoint>1490</remainpoint>\"//\n\t\t\t\t+ \"<taskID>885</taskID>\"//\n\t\t\t\t+ \"<successCounts>1</successCounts>\"//\n\t\t\t\t+ \"<newNode><sub>subText</sub></newNode>\"//\n\t\t\t\t+ \"</returnsms>\";\n\t\tfinal Map<String, Object> map = XmlUtil.xmlToMap(xml);\n\n\t\tassertEquals(6, map.size());\n\t\tassertEquals(\"Success\", map.get(\"returnstatus\"));\n\t\tassertEquals(\"ok\", map.get(\"message\"));\n\t\tassertEquals(\"1490\", map.get(\"remainpoint\"));\n\t\tassertEquals(\"885\", map.get(\"taskID\"));\n\t\tassertEquals(\"1\", map.get(\"successCounts\"));\n\t\tassertEquals(\"subText\", ((Map<?, ?>) map.get(\"newNode\")).get(\"sub\"));\n\t}\n\n\t@Test\n\tpublic void xmlToMapTest2() {\n\t\tfinal String xml = \"<root><name>张三</name><name>李四</name></root>\";\n\t\tfinal Map<String, Object> map = XmlUtil.xmlToMap(xml);\n\n\t\tassertEquals(1, map.size());\n\t\tassertEquals(CollUtil.newArrayList(\"张三\", \"李四\"), map.get(\"name\"));\n\t}\n\n\t@Test\n\tpublic void mapToXmlTest() {\n\t\tfinal Map<String, Object> map = MapBuilder.create(new LinkedHashMap<String, Object>())//\n\t\t\t\t.put(\"name\", \"张三\")//\n\t\t\t\t.put(\"age\", 12)//\n\t\t\t\t.put(\"game\", MapUtil.builder(new LinkedHashMap<String, Object>()).put(\"昵称\", \"Looly\").put(\"level\", 14).build())//\n\t\t\t\t.build();\n\t\tfinal Document doc = XmlUtil.mapToXml(map, \"user\");\n\t\t// Console.log(XmlUtil.toStr(doc, false));\n\t\tassertEquals(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\" standalone=\\\"no\\\"?>\"//\n\t\t\t\t\t\t+ \"<user>\"//\n\t\t\t\t\t\t+ \"<name>张三</name>\"//\n\t\t\t\t\t\t+ \"<age>12</age>\"//\n\t\t\t\t\t\t+ \"<game>\"//\n\t\t\t\t\t\t+ \"<昵称>Looly</昵称>\"//\n\t\t\t\t\t\t+ \"<level>14</level>\"//\n\t\t\t\t\t\t+ \"</game>\"//\n\t\t\t\t\t\t+ \"</user>\", //\n\t\t\t\tXmlUtil.toStr(doc, false));\n\t}\n\n\t@Test\n\tpublic void mapToXmlTest2() {\n\t\t// 测试List\n\t\tfinal Map<String, Object> map = MapBuilder.create(new LinkedHashMap<String, Object>())\n\t\t\t\t.put(\"Town\", CollUtil.newArrayList(\"town1\", \"town2\"))\n\t\t\t\t.build();\n\n\t\tfinal Document doc = XmlUtil.mapToXml(map, \"City\");\n\t\tassertEquals(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\" standalone=\\\"no\\\"?>\" +\n\t\t\t\t\t\t\"<City>\" +\n\t\t\t\t\t\t\"<Town>town1</Town>\" +\n\t\t\t\t\t\t\"<Town>town2</Town>\" +\n\t\t\t\t\t\t\"</City>\",\n\t\t\t\tXmlUtil.toStr(doc));\n\t}\n\n\t@Test\n\tpublic void readTest() {\n\t\tfinal Document doc = XmlUtil.readXML(\"test.xml\");\n\t\tassertNotNull(doc);\n\t}\n\n\t@Test\n\tpublic void readBySaxTest(){\n\t\tfinal Set<String> eles = CollUtil.newHashSet(\n\t\t\t\t\"returnsms\", \"returnstatus\", \"message\", \"remainpoint\", \"taskID\", \"successCounts\");\n\t\tXmlUtil.readBySax(ResourceUtil.getStream(\"test.xml\"), new DefaultHandler(){\n\t\t\t@Override\n\t\t\tpublic void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {\n\t\t\t\tassertTrue(eles.contains(localName));\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\tpublic void mapToXmlTestWithOmitXmlDeclaration() {\n\n\t\tfinal Map<String, Object> map = MapBuilder.create(new LinkedHashMap<String, Object>())\n\t\t\t\t.put(\"name\", \"ddatsh\")\n\t\t\t\t.build();\n\t\tfinal String xml = XmlUtil.mapToXmlStr(map, true);\n\t\tassertEquals(\"<xml><name>ddatsh</name></xml>\", xml);\n\t}\n\n\t@Test\n\tpublic void getByPathTest() {\n\t\tfinal String xmlStr = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\" +\n\t\t\t\t\"<soap:Envelope xmlns:soap=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\">\\n\" +\n\t\t\t\t\"  <soap:Body>\\n\" +\n\t\t\t\t\"    <ns2:testResponse xmlns:ns2=\\\"http://ws.xxx.com/\\\">\\n\" +\n\t\t\t\t\"      <return>2020/04/15 21:01:21</return>\\n\" +\n\t\t\t\t\"    </ns2:testResponse>\\n\" +\n\t\t\t\t\"  </soap:Body>\\n\" +\n\t\t\t\t\"</soap:Envelope>\\n\";\n\n\t\tfinal Document document = XmlUtil.readXML(xmlStr);\n\t\tfinal Object value = XmlUtil.getByXPath(\n\t\t\t\t\"//soap:Envelope/soap:Body/ns2:testResponse/return\",\n\t\t\t\tdocument, XPathConstants.STRING);//\n\t\tassertEquals(\"2020/04/15 21:01:21\", value);\n\t}\n\n\t@Test\n\tpublic void beanToXmlIgnoreNullTest() {\n\t\t@Data\n\t\tclass TestBean {\n\t\t\tprivate String ReqCode;\n\t\t\tprivate String AccountName;\n\t\t\tprivate String Operator;\n\t\t\tprivate String ProjectCode;\n\t\t\tprivate String BankCode;\n\t\t}\n\n\t\tfinal TestBean testBean = new TestBean();\n\t\ttestBean.setReqCode(\"1111\");\n\t\ttestBean.setAccountName(\"账户名称\");\n\t\ttestBean.setOperator(\"cz\");\n\t\ttestBean.setProjectCode(null);\n\t\ttestBean.setBankCode(\"00001\");\n\n\t\t// 不忽略空字段情况下保留自闭标签\n\t\tDocument doc = XmlUtil.beanToXml(testBean, null, false);\n\t\tassertNotNull(XmlUtil.getElement(doc.getDocumentElement(), \"ProjectCode\"));\n\n\t\t// 忽略空字段情况下无自闭标签\n\t\tdoc = XmlUtil.beanToXml(testBean, null, true);\n\t\tassertNull(XmlUtil.getElement(doc.getDocumentElement(), \"ProjectCode\"));\n\t}\n\n\t@Test\n\tpublic void xmlToBeanTest() {\n\t\t@Data\n\t\tclass TestBean {\n\t\t\tprivate String ReqCode;\n\t\t\tprivate String AccountName;\n\t\t\tprivate String Operator;\n\t\t\tprivate String ProjectCode;\n\t\t\tprivate String BankCode;\n\t\t}\n\n\t\tfinal TestBean testBean = new TestBean();\n\t\ttestBean.setReqCode(\"1111\");\n\t\ttestBean.setAccountName(\"账户名称\");\n\t\ttestBean.setOperator(\"cz\");\n\t\ttestBean.setProjectCode(\"123\");\n\t\ttestBean.setBankCode(\"00001\");\n\n\t\tfinal Document doc = XmlUtil.beanToXml(testBean);\n\t\tassertEquals(TestBean.class.getSimpleName(), doc.getDocumentElement().getTagName());\n\n\t\tfinal TestBean testBean2 = XmlUtil.xmlToBean(doc, TestBean.class);\n\t\tassertEquals(testBean.getReqCode(), testBean2.getReqCode());\n\t\tassertEquals(testBean.getAccountName(), testBean2.getAccountName());\n\t\tassertEquals(testBean.getOperator(), testBean2.getOperator());\n\t\tassertEquals(testBean.getProjectCode(), testBean2.getProjectCode());\n\t\tassertEquals(testBean.getBankCode(), testBean2.getBankCode());\n\t}\n\n\t@Test\n\tpublic void xmlToBeanTest2(){\n\t\t@Data\n\t\tclass SmsRes {\n\t\t\tprivate String code;\n\t\t}\n\n\t\t//issue#1663@Github\n\t\tfinal String xmlStr = \"<?xml version=\\\"1.0\\\" encoding=\\\"gbk\\\" ?><response><code>02</code></response>\";\n\n\t\tfinal Document doc = XmlUtil.parseXml(xmlStr);\n\n\t\t// 标准方式\n\t\tfinal Map<String, Object> map = XmlUtil.xmlToMap(doc.getFirstChild());\n\t\tfinal SmsRes res = new SmsRes();\n\t\tBeanUtil.fillBeanWithMap(map, res, true);\n\n\t\t// toBean方式\n\t\tfinal SmsRes res1 = XmlUtil.xmlToBean(doc.getFirstChild(), SmsRes.class);\n\n\t\tassertEquals(res.toString(), res1.toString());\n\t}\n\n\t@Test\n\tpublic void cleanCommentTest() {\n\t\tfinal String xmlContent = \"<info><title>hutool</title><!-- 这是注释 --><lang>java</lang></info>\";\n\t\tfinal String ret = XmlUtil.cleanComment(xmlContent);\n\t\tassertEquals(\"<info><title>hutool</title><lang>java</lang></info>\", ret);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void formatTest(){\n\t\t// https://github.com/looly/hutool/pull/1234\n\t\tfinal Document xml = XmlUtil.createXml(\"NODES\");\n\t\txml.setXmlStandalone(true);\n\n\t\tfinal NodeList parentNode = xml.getElementsByTagName(\"NODES\");\n\n\t\tfinal Element parent1Node = xml.createElement(\"NODE\");\n\n\t\tfinal Element node1 = xml.createElement(\"NODENAME\");\n\t\tnode1.setTextContent(\"走位\");\n\t\tfinal Element node2 = xml.createElement(\"STEP\");\n\t\tnode2.setTextContent(\"1\");\n\t\tfinal Element node3 = xml.createElement(\"STATE\");\n\t\tnode3.setTextContent(\"2\");\n\t\tfinal Element node4 = xml.createElement(\"TIMELIMIT\");\n\t\tnode4.setTextContent(\"\");\n\t\tfinal Element node5 = xml.createElement(\"STARTTIME\");\n\n\t\tparent1Node.appendChild(node1);\n\t\tparent1Node.appendChild(node2);\n\t\tparent1Node.appendChild(node3);\n\t\tparent1Node.appendChild(node4);\n\t\tparent1Node.appendChild(node5);\n\n\t\tparentNode.item(0).appendChild(parent1Node);\n\n\t\tfinal String format = XmlUtil.toStr(xml,\"GBK\",true);\n\t\tConsole.log(format);\n\t}\n\n\t@Test\n\tpublic void escapeTest(){\n\t\tfinal String a = \"<>\";\n\t\tfinal String escape = XmlUtil.escape(a);\n\t\tAssertions.assertEquals(\"&lt;&gt;\", escape);\n\t\tAssertions.assertEquals(\"中文“双引号”\", XmlUtil.escape(\"中文“双引号”\"));\n\t}\n\n\t@Test\n\tpublic void getParamTest(){\n\t\tfinal String xml = \"<Config name=\\\"aaaa\\\">\\n\" +\n\t\t\t\t\"    <url>222222</url>\\n\" +\n\t\t\t\t\"</Config>\";\n\n\t\tfinal Document doc = XmlUtil.parseXml(xml);\n\t\tfinal String name = doc.getDocumentElement().getAttribute(\"name\");\n\t\tassertEquals(\"aaaa\", name);\n\t}\n\n\t@Test\n\tpublic void xmlStrToBeanTest(){\n\t\tfinal String xml = \"<userInfo><name>张三</name><age>20</age><email>zhangsan@example.com</email></userInfo>\";\n\t\tfinal Document document = XmlUtil.readXML(xml);\n\t\tfinal UserInfo userInfo = XmlUtil.xmlToBean(document, UserInfo.class);\n\t\tassertEquals(\"张三\", userInfo.getName());\n\t\tassertEquals(\"20\", userInfo.getAge());\n\t\tassertEquals(\"zhangsan@example.com\", userInfo.getEmail());\n\t}\n\n\t@Data\n\tstatic class UserInfo {\n\n\t\tprivate String id;\n\t\tprivate String name;\n\t\tprivate String age;\n\t\tprivate String email;\n\t}\n\n\t@Test\n\tpublic void issue3139Test() {\n\t\tfinal String xml = \"<r>\\n\" +\n\t\t\t\"  <c>\\n\" +\n\t\t\t\"     <s>1</s>\\n\" +\n\t\t\t\"     <p>str</p>\\n\" +\n\t\t\t\"  </c>\\n\" +\n\t\t\t\"</r>\";\n\n\t\tfinal R r = XmlUtil.xmlToBean(XmlUtil.parseXml(xml), R.class);\n\t\tassertEquals(\"1\", r.getC().get(0).getS());\n\t\tassertEquals(\"str\", r.getC().get(0).getP());\n\t}\n\n\t@Data\n\tstatic class C {\n\t\tString s;\n\t\tString p;\n\t}\n\n\t@Data\n\tstatic class R {\n\t\tList<C> c;\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java",
    "content": "package cn.hutool.core.util;\n\nimport cn.hutool.core.compress.ZipReader;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.zip.ZipFile;\n\nimport static cn.hutool.core.util.ZipUtil.unzip;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * {@link ZipUtil}单元测试\n * @author Looly\n *\n */\npublic class ZipUtilTest {\n\n\t@Test\n\tpublic void appendTest() throws IOException {\n\t\tfinal File appendFile = FileUtil.file(\"test-zip/addFile.txt\");\n\t\tfinal File zipFile = FileUtil.file(\"test-zip/test.zip\");\n\n\t\t// 用于测试完成后将被测试文件恢复\n\t\tfinal File tempZipFile = FileUtil.createTempFile(FileUtil.file(\"test-zip\"));\n\t\ttempZipFile.deleteOnExit();\n\t\tFileUtil.copy(zipFile, tempZipFile, true);\n\n\t\t// test file add\n\t\tList<String> beforeNames = zipEntryNames(tempZipFile);\n\t\tZipUtil.append(tempZipFile.toPath(), appendFile.toPath());\n\t\tList<String> afterNames = zipEntryNames(tempZipFile);\n\n\t\t// 确认增加了文件\n\t\tassertEquals(beforeNames.size() + 1, afterNames.size());\n\t\tassertTrue(afterNames.containsAll(beforeNames));\n\t\tassertTrue(afterNames.contains(appendFile.getName()));\n\n\t\t// test dir add\n\t\tbeforeNames = zipEntryNames(tempZipFile);\n\t\tfinal File addDirFile = FileUtil.file(\"test-zip/test-add\");\n\t\tZipUtil.append(tempZipFile.toPath(), addDirFile.toPath());\n\t\tafterNames = zipEntryNames(tempZipFile);\n\n\t\t// 确认增加了文件和目录，增加目录和目录下一个文件，故此处+2\n\t\tassertEquals(beforeNames.size() + 2, afterNames.size());\n\t\tassertTrue(afterNames.containsAll(beforeNames));\n\t\tassertTrue(afterNames.contains(appendFile.getName()));\n\n\t\t// rollback\n\t\tassertTrue(tempZipFile.delete(), String.format(\"delete temp file %s failed\", tempZipFile.getCanonicalPath()));\n\t}\n\n\t/**\n\t * 获取zip文件中所有一级文件/文件夹的name\n\t *\n\t * @param zipFile 待测试的zip文件\n\t * @return zip文件中一级目录下的所有文件/文件夹名\n\t */\n\tprivate List<String> zipEntryNames(final File zipFile) {\n\t\tfinal List<String> fileNames = new ArrayList<>();\n\t\tfinal ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8);\n\t\treader.read(zipEntry -> fileNames.add(zipEntry.getName()));\n\t\treader.close();\n\t\treturn fileNames;\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipDirTest() {\n\t\tZipUtil.zip(new File(\"d:/test\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipTest() {\n\t\tfinal File unzip = ZipUtil.unzip(\"d:/test/hutool.zip\", \"d:\\\\test\", CharsetUtil.CHARSET_GBK);\n\t\tConsole.log(unzip);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipTest2() {\n\t\tfinal File unzip = ZipUtil.unzip(\"f:/test/各种资源.zip\", \"f:/test/各种资源\", CharsetUtil.CHARSET_GBK);\n\t\tConsole.log(unzip);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipFromStreamTest() {\n\t\tfinal File unzip = ZipUtil.unzip(FileUtil.getInputStream(\"e:/test/hutool-core-5.1.0.jar\"), FileUtil.file(\"e:/test/\"), CharsetUtil.CHARSET_UTF_8);\n\t\tConsole.log(unzip);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipChineseTest() {\n\t\tZipUtil.unzip(\"d:/测试.zip\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void unzipFileBytesTest() {\n\t\tfinal byte[] fileBytes = ZipUtil.unzipFileBytes(FileUtil.file(\"e:/02 电力相关设备及服务2-241-.zip\"), CharsetUtil.CHARSET_GBK, \"images/CE-EP-HY-MH01-ES-0001.jpg\");\n\t\tassertNotNull(fileBytes);\n\t}\n\n\t@Test\n\tpublic void gzipTest() {\n\t\tfinal String data = \"我是一个需要压缩的很长很长的字符串\";\n\t\tfinal byte[] bytes = StrUtil.utf8Bytes(data);\n\t\tfinal byte[] gzip = ZipUtil.gzip(bytes);\n\n\t\t//保证gzip长度正常\n\t\tassertEquals(68, gzip.length);\n\n\t\tfinal byte[] unGzip = ZipUtil.unGzip(gzip);\n\t\t//保证正常还原\n\t\tassertEquals(data, StrUtil.utf8Str(unGzip));\n\t}\n\n\t@Test\n\tpublic void zlibTest() {\n\t\tfinal String data = \"我是一个需要压缩的很长很长的字符串\";\n\t\tfinal byte[] bytes = StrUtil.utf8Bytes(data);\n\t\tbyte[] gzip = ZipUtil.zlib(bytes, 0);\n\n\t\t//保证zlib长度正常\n\t\tassertEquals(62, gzip.length);\n\t\tfinal byte[] unGzip = ZipUtil.unZlib(gzip);\n\t\t//保证正常还原\n\t\tassertEquals(data, StrUtil.utf8Str(unGzip));\n\n\t\tgzip = ZipUtil.zlib(bytes, 9);\n\t\t//保证zlib长度正常\n\t\tassertEquals(56, gzip.length);\n\t\tfinal byte[] unGzip2 = ZipUtil.unZlib(gzip);\n\t\t//保证正常还原\n\t\tassertEquals(data, StrUtil.utf8Str(unGzip2));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipStreamTest(){\n\t\t//https://github.com/chinabugotech/hutool/issues/944\n\t\tfinal String dir = \"d:/test\";\n\t\tfinal String zip = \"d:/test.zip\";\n\t\t//noinspection IOStreamConstructor\n\t\ttry (final OutputStream out = new FileOutputStream(zip)){\n\t\t\t//实际应用中, out 为 HttpServletResponse.getOutputStream\n\t\t\tZipUtil.zip(out, Charset.defaultCharset(), false, null, new File(dir));\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipStreamTest2(){\n\t\t// https://github.com/chinabugotech/hutool/issues/944\n\t\tfinal String file1 = \"d:/test/a.txt\";\n\t\tfinal String file2 = \"d:/test/a.txt\";\n\t\tfinal String file3 = \"d:/test/asn1.key\";\n\n\t\tfinal String zip = \"d:/test/test2.zip\";\n\t\t//实际应用中, out 为 HttpServletResponse.getOutputStream\n\t\tZipUtil.zip(FileUtil.getOutputStream(zip), Charset.defaultCharset(), false, null,\n\t\t\t\tnew File(file1),\n\t\t\t\tnew File(file2),\n\t\t\t\tnew File(file3)\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipToStreamTest(){\n\t\tfinal String zip = \"d:/test/testToStream.zip\";\n\t\tfinal OutputStream out = FileUtil.getOutputStream(zip);\n\t\tZipUtil.zip(out, new String[]{\"sm1_alias.txt\"},\n\t\t\t\tnew InputStream[]{FileUtil.getInputStream(\"d:/test/sm4_1.txt\")});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipMultiFileTest(){\n\t\tfinal File[] dd={FileUtil.file(\"d:\\\\test\\\\qr_a.jpg\")\n\t\t\t\t,FileUtil.file(\"d:\\\\test\\\\qr_b.jpg\")};\n\n\t\tZipUtil.zip(FileUtil.file(\"d:\\\\test\\\\qr.zip\"),false,dd);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sizeUnzipTest() throws IOException {\n\t\tfinal String zipPath = \"e:\\\\hutool\\\\demo.zip\";\n\t\tfinal String outPath = \"e:\\\\hutool\\\\test\";\n\t\tfinal ZipFile zipFile = new ZipFile(zipPath, Charset.forName(\"GBK\"));\n\t\tfinal File file = new File(outPath);\n\t\t// 限制解压文件大小为637KB\n\t\tfinal long size = 637*1024L;\n\n\t\t// 限制解压文件大小为636KB\n\t\t// long size = 636*1024L;\n\t\tunzip(zipFile, file, size);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issue3018Test() {\n\t\tZipUtil.unzip(\n\t\t\t\tFileUtil.getInputStream(\"d:/test/default.zip\")\n\t\t, FileUtil.file(\"d:/test/\"), CharsetUtil.CHARSET_UTF_8);\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/resources/1_psi_index_0.txt",
    "content": "0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10"
  },
  {
    "path": "hutool-core/src/test/resources/issueI91VF1.csv",
    "content": "主机,用户名,密码\n192.168.1.1,admin,123\n"
  },
  {
    "path": "hutool-core/src/test/resources/issueIA8WE0.csv",
    "content": "c1_text1,\"c1_text2\n#c1_text2_line2\",c1_text3\n"
  },
  {
    "path": "hutool-core/src/test/resources/issueICRMKA.csv",
    "content": "config,price,unit,package_name,package,equipment,pic_path,crawler_date,set_Discount,installment_details,installment_price,installment_time\n6.3\" Google Pixel 9 Pro 128 GB Beige,94999,₽,fwefwe,fewfew,\"Cores - 8x(3.1 GHz), 16 GB, 1 SIM, OLED, 2856x1280, 50+48+48 MP camera, NFC, 5G, GPS, 4700 mAh more details\",[\"https://xx.png\"],2025/8/1 4:05,,gerwgrweg,fwefw,fwefwe\n"
  },
  {
    "path": "hutool-core/src/test/resources/test-compile/a/A.java",
    "content": "package a;\n\nimport cn.hutool.core.lang.ConsoleTable;\nimport cn.hutool.core.lang.caller.CallerUtil;\n\npublic class A {\n    private class InnerClass {\n    }\n\n    public A() {\n        new InnerClass() {{\n            int i = 0;\n            Class<?> caller = CallerUtil.getCaller(i);\n            final ConsoleTable t = new ConsoleTable();\n            t.addHeader(\"类名\", \"类加载器\");\n            System.out.println(\"初始化 \" + getClass() + \" 的调用链为: \");\n            while (caller != null) {\n                t.addBody(caller.toString(), caller.getClassLoader().toString());\n                caller = CallerUtil.getCaller(++i);\n            }\n            t.print();\n        }};\n    }\n}"
  },
  {
    "path": "hutool-core/src/test/resources/test-compile/b/B.java",
    "content": "package b;\nimport a.A;\n\npublic class B {\n    public B() {\n        new A();\n    }\n}"
  },
  {
    "path": "hutool-core/src/test/resources/test-compile/c/C.java",
    "content": "package c;\n\nimport b.B;\n\npublic class C {\n    public C() {\n        new B();\n    }\n}"
  },
  {
    "path": "hutool-core/src/test/resources/test-compile/error/ErrorClazz.java",
    "content": "package error;\n\npublic class ErrorClazz {\n\n\tpublic static void 123main(String[] args) {\n\t\tSystem.out.println(\"hello world\");\n\t}\n}\n"
  },
  {
    "path": "hutool-core/src/test/resources/test-zip/addFile.txt",
    "content": "this file will be used to add into the test.zip\nbefore the add action, the test.zip won't have this file.\n"
  },
  {
    "path": "hutool-core/src/test/resources/test-zip/test-add/test.txt",
    "content": "1"
  },
  {
    "path": "hutool-core/src/test/resources/test.csv",
    "content": "# 这是一行注释，读取时应忽略\n\"sss,sss\",姓名,\"性别\",关注\"对象\",年龄,\"\",\"\"\"\n"
  },
  {
    "path": "hutool-core/src/test/resources/test.properties",
    "content": "#--------------------------------------------\n# 配置文件测试\n#--------------------------------------------\n\na = 1\nb = 2"
  },
  {
    "path": "hutool-core/src/test/resources/test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!-- returnstatus 状态\n\t message 消息\n-->\n\n<returnsms>\n<returnstatus>Success（成功）</returnstatus>\n<message>ok</message>\n<remainpoint>1490</remainpoint>\n<taskID>885</taskID>\n<successCounts>1</successCounts>\n</returnsms>\n"
  },
  {
    "path": "hutool-core/src/test/resources/test_bean.csv",
    "content": "姓名,gender,focus,age\n张三,男,无,33\n李四,男,好对象,23\n王妹妹,女,特别关注,22"
  },
  {
    "path": "hutool-core/src/test/resources/test_lines.csv",
    "content": "# 这是一行注释，读取时应忽略\na,b,c,d\n1,2,3,4\n# 这是一行注释，读取时应忽略\nq,w,e,r,\"我是一段\n带换行的内容\"\na,s,d,f\n"
  },
  {
    "path": "hutool-core/src/test/resources/test_lines_cr.csv",
    "content": "# 这是一行注释，读取时应忽略\ra,b,c,d\r1,2,3,4\r# 这是一行注释，读取时应忽略\rq,w,e,r,\"我是一段\r带换行的内容\"\ra,s,d,f\r"
  },
  {
    "path": "hutool-core/src/test/resources/test_lines_crlf.csv",
    "content": "# 这是一行注释，读取时应忽略\r\na,b,c,d\r\n1,2,3,4\r\n# 这是一行注释，读取时应忽略\r\nq,w,e,r,\"我是一段\r\n带换行的内容\"\r\na,s,d,f\r\n"
  },
  {
    "path": "hutool-core/src/test/resources/text.txt",
    "content": "1\n"
  },
  {
    "path": "hutool-cron/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-cron</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 定时任务</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.cron</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.quartz-scheduler</groupId>\n\t\t\t<artifactId>quartz</artifactId>\n\t\t\t<version>2.4.0</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/CronConfig.java",
    "content": "package cn.hutool.cron;\n\nimport java.util.TimeZone;\n\n/**\n * 定时任务配置类\n *\n * @author looly\n * @since 5.4.7\n */\npublic class CronConfig {\n\n\t/**\n\t * 时区\n\t */\n\tprotected TimeZone timezone = TimeZone.getDefault();\n\t/**\n\t * 是否支持秒匹配\n\t */\n\tprotected boolean matchSecond;\n\n\tpublic CronConfig(){\n\t}\n\n\t/**\n\t * 设置时区\n\t *\n\t * @param timezone 时区\n\t * @return this\n\t */\n\tpublic CronConfig setTimeZone(TimeZone timezone) {\n\t\tthis.timezone = timezone;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得时区，默认为 {@link TimeZone#getDefault()}\n\t *\n\t * @return 时区\n\t */\n\tpublic TimeZone getTimeZone() {\n\t\treturn this.timezone;\n\t}\n\n\t/**\n\t * 是否支持秒匹配\n\t *\n\t * @return {@code true}使用，{@code false}不使用\n\t */\n\tpublic boolean isMatchSecond() {\n\t\treturn this.matchSecond;\n\t}\n\n\t/**\n\t * 设置是否支持秒匹配，默认不使用\n\t *\n\t * @param isMatchSecond {@code true}支持，{@code false}不支持\n\t * @return this\n\t */\n\tpublic CronConfig setMatchSecond(boolean isMatchSecond) {\n\t\tthis.matchSecond = isMatchSecond;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/CronException.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 定时任务异常\n *\n * @author xiaoleilu\n */\npublic class CronException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic CronException(Throwable e) {\n\t\tsuper(e.getMessage(), e);\n\t}\n\n\tpublic CronException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic CronException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic CronException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic CronException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/CronTimer.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.core.date.DateUnit;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\nimport java.io.Serializable;\n\n/**\n * 定时任务计时器<br>\n * 计时器线程每隔一分钟（一秒钟）检查一次任务列表，一旦匹配到执行对应的Task\n * @author Looly\n *\n */\npublic class CronTimer extends Thread implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final Log log = LogFactory.get();\n\n\t/** 定时单元：秒 */\n\tprivate final long TIMER_UNIT_SECOND = DateUnit.SECOND.getMillis();\n\t/** 定时单元：分 */\n\tprivate final long TIMER_UNIT_MINUTE = DateUnit.MINUTE.getMillis();\n\n\t/** 定时任务是否已经被强制关闭 */\n\tprivate boolean isStop;\n\tprivate final Scheduler scheduler;\n\n\t/**\n\t * 构造\n\t * @param scheduler {@link Scheduler}\n\t */\n\tpublic CronTimer(Scheduler scheduler) {\n\t\tthis.scheduler = scheduler;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\tfinal long timerUnit = this.scheduler.config.matchSecond ? TIMER_UNIT_SECOND : TIMER_UNIT_MINUTE;\n\t\tfinal long doubleTimeUnit = 2 * timerUnit;\n\n\t\tlong thisTime = System.currentTimeMillis();\n\t\tlong nextTime;\n\t\tlong sleep;\n\t\twhile(false == isStop){\n\t\t\tspawnLauncher(thisTime);\n\n\t\t\t//下一时间计算是按照上一个执行点开始时间计算的\n\t\t\t//此处除以定时单位是为了清零单位以下部分，例如单位是分则秒和毫秒清零\n\t\t\tnextTime = ((thisTime / timerUnit) + 1) * timerUnit;\n\t\t\tsleep = nextTime - System.currentTimeMillis();\n\t\t\tif(sleep < 0){\n\t\t\t\t// 可能循环执行慢导致时间点跟不上系统时间，追赶系统时间并执行中间差异的时间点（issue#IB49EF@Gitee）\n\t\t\t\tthisTime = System.currentTimeMillis();\n\t\t\t\twhile(nextTime <= thisTime){\n\t\t\t\t\t// 追赶系统时间并运行执行点\n\t\t\t\t\tspawnLauncher(nextTime);\n\t\t\t\t\tnextTime = ((thisTime / timerUnit) + 1) * timerUnit;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t} else if(sleep > doubleTimeUnit){\n\t\t\t\t// 时间回退，可能用户回拨了时间或自动校准了时间，重新计算（issue#1224@Github）\n\t\t\t\tthisTime = System.currentTimeMillis();\n\t\t\t\tcontinue;\n\t\t\t} else if (false == ThreadUtil.safeSleep(sleep)) {\n\t\t\t\t//等待直到下一个时间点，如果被用户中断直接退出Timer\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// issue#3460 采用叠加方式，确保正好是1分钟或1秒，避免sleep晚醒问题\n\t\t\t// 此处无需校验，因为每次循环都是sleep与上触发点的时间差。\n\t\t\t// 当上一次晚醒后，本次会减少sleep时间，保证误差在一个unit内，并不断修正。\n\t\t\tthisTime = nextTime;\n\t\t}\n\t\tlog.debug(\"Hutool-cron timer stopped.\");\n\t}\n\n\t/**\n\t * 关闭定时器\n\t */\n\tsynchronized public void stopTimer() {\n\t\tthis.isStop = true;\n\t\tThreadUtil.interrupt(this, true);\n\t}\n\n\t/**\n\t * 启动匹配\n\t * @param millis 当前时间\n\t */\n\tprivate void spawnLauncher(final long millis){\n\t\t//Console.log(millis / 1000, System.currentTimeMillis() / 1000);\n\t\tthis.scheduler.taskLauncherManager.spawnLauncher(millis);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/CronUtil.java",
    "content": "\npackage cn.hutool.cron;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.resource.NoResourceException;\nimport cn.hutool.cron.pattern.CronPattern;\nimport cn.hutool.cron.task.Task;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.SettingRuntimeException;\n\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * 定时任务工具类<br>\n * 此工具持有一个全局{@link Scheduler}，所有定时任务在同一个调度器中执行<br>\n * {@link #setMatchSecond(boolean)} 方法用于定义是否使用秒匹配模式，如果为true，则定时任务表达式中的第一位为秒，否则为分，默认是分\n *\n * @author xiaoleilu\n *\n */\npublic class CronUtil {\n\n\t/** Crontab配置文件 */\n\tpublic static final String CRONTAB_CONFIG_PATH = \"config/cron.setting\";\n\tpublic static final String CRONTAB_CONFIG_PATH2 = \"cron.setting\";\n\n\tprivate static final Lock lock = new ReentrantLock();\n\tprivate static final Scheduler scheduler = new Scheduler();\n\tprivate static Setting crontabSetting;\n\n\t/**\n\t * 自定义定时任务配置文件\n\t *\n\t * @param cronSetting 定时任务配置文件\n\t */\n\tpublic static void setCronSetting(Setting cronSetting) {\n\t\tcrontabSetting = cronSetting;\n\t}\n\n\t/**\n\t * 自定义定时任务配置文件路径\n\t *\n\t * @param cronSettingPath 定时任务配置文件路径（相对绝对都可）\n\t */\n\tpublic static void setCronSetting(String cronSettingPath) {\n\t\ttry {\n\t\t\tcrontabSetting = new Setting(cronSettingPath, Setting.DEFAULT_CHARSET, false);\n\t\t} catch (SettingRuntimeException | NoResourceException e) {\n\t\t\t// ignore setting file parse error and no config error\n\t\t}\n\t}\n\n\t/**\n\t * 设置是否支持秒匹配<br>\n\t * 此方法用于定义是否使用秒匹配模式，如果为true，则定时任务表达式中的第一位为秒，否则为分，默认是分<br>\n\t *\n\t * @param isMatchSecond {@code true}支持，{@code false}不支持\n\t */\n\tpublic static void setMatchSecond(boolean isMatchSecond) {\n\t\tscheduler.setMatchSecond(isMatchSecond);\n\t}\n\n\t/**\n\t * 加入定时任务\n\t *\n\t * @param schedulingPattern 定时任务执行时间的crontab表达式\n\t * @param task 任务\n\t * @return 定时任务ID\n\t */\n\tpublic static String schedule(String schedulingPattern, Task task) {\n\t\treturn scheduler.schedule(schedulingPattern, task);\n\t}\n\n\t/**\n\t * 加入定时任务\n\t *\n\t * @param id 定时任务ID\n\t * @param schedulingPattern 定时任务执行时间的crontab表达式\n\t * @param task 任务\n\t * @return 定时任务ID\n\t * @since 3.3.0\n\t */\n\tpublic static String schedule(String id, String schedulingPattern, Task task) {\n\t\tscheduler.schedule(id, schedulingPattern, task);\n\t\treturn id;\n\t}\n\n\t/**\n\t * 加入定时任务\n\t *\n\t * @param schedulingPattern 定时任务执行时间的crontab表达式\n\t * @param task 任务\n\t * @return 定时任务ID\n\t */\n\tpublic static String schedule(String schedulingPattern, Runnable task) {\n\t\treturn scheduler.schedule(schedulingPattern, task);\n\t}\n\n\t/**\n\t * 批量加入配置文件中的定时任务\n\t *\n\t * @param cronSetting 定时任务设置文件\n\t */\n\tpublic static void schedule(Setting cronSetting) {\n\t\tscheduler.schedule(cronSetting);\n\t}\n\n\t/**\n\t * 移除任务\n\t *\n\t * @param schedulerId 任务ID\n\t * @return 是否移除成功，{@code false}表示未找到对应ID的任务\n\t */\n\tpublic static boolean remove(String schedulerId) {\n\t\treturn scheduler.descheduleWithStatus(schedulerId);\n\t}\n\n\t/**\n\t * 更新Task的执行时间规则\n\t *\n\t * @param id Task的ID\n\t * @param pattern {@link CronPattern}\n\t * @since 4.0.10\n\t */\n\tpublic static void updatePattern(String id, CronPattern pattern) {\n\t\tscheduler.updatePattern(id, pattern);\n\t}\n\n\t/**\n\t * @return 获得Scheduler对象\n\t */\n\tpublic static Scheduler getScheduler() {\n\t\treturn scheduler;\n\t}\n\n\t/**\n\t * 开始，非守护线程模式\n\t *\n\t * @see #start(boolean)\n\t */\n\tpublic static void start() {\n\t\tstart(false);\n\t}\n\n\t/**\n\t * 开始\n\t *\n\t * @param isDaemon 是否以守护线程方式启动，如果为true，则在调用{@link #stop()}方法后执行的定时任务立即结束，否则等待执行完毕才结束。\n\t */\n\tsynchronized public static void start(boolean isDaemon) {\n\t\tif (scheduler.isStarted()) {\n\t\t\tthrow new UtilException(\"Scheduler has been started, please stop it first!\");\n\t\t}\n\n\t\tlock.lock();\n\t\ttry {\n\t\t\tif (null == crontabSetting) {\n\t\t\t\t// 尝试查找config/cron.setting\n\t\t\t\tsetCronSetting(CRONTAB_CONFIG_PATH);\n\t\t\t}\n\t\t\t// 尝试查找cron.setting\n\t\t\tif (null == crontabSetting) {\n\t\t\t\tsetCronSetting(CRONTAB_CONFIG_PATH2);\n\t\t\t}\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\n\t\tschedule(crontabSetting);\n\t\tscheduler.start(isDaemon);\n\t}\n\n\t/**\n\t * 重新启动定时任务<br>\n\t * 此方法会清除动态加载的任务，重新启动后，守护线程与否与之前保持一致\n\t */\n\tpublic static void restart() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tif (null != crontabSetting) {\n\t\t\t\t//重新读取配置文件\n\t\t\t\tcrontabSetting.load();\n\t\t\t}\n\t\t\tif (scheduler.isStarted()) {\n\t\t\t\t//关闭并清除已有任务\n\t\t\t\tstop();\n\t\t\t}\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\n\t\t//重新加载任务\n\t\tschedule(crontabSetting);\n\t\t//重新启动\n\t\tscheduler.start();\n\t}\n\n\t/**\n\t * 停止\n\t */\n\tpublic static void stop() {\n\t\tscheduler.stop(true);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.thread.ExecutorBuilder;\nimport cn.hutool.core.thread.ThreadFactoryBuilder;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.cron.listener.TaskListener;\nimport cn.hutool.cron.listener.TaskListenerManager;\nimport cn.hutool.cron.pattern.CronPattern;\nimport cn.hutool.cron.task.InvokeTask;\nimport cn.hutool.cron.task.RunnableTask;\nimport cn.hutool.cron.task.Task;\nimport cn.hutool.log.StaticLog;\nimport cn.hutool.setting.Setting;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map.Entry;\nimport java.util.TimeZone;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * 任务调度器<br>\n *\n * 调度器启动流程：<br>\n *\n * <pre>\n * 启动Timer =》 启动TaskLauncher =》 启动TaskExecutor\n * </pre>\n *\n * 调度器关闭流程:<br>\n *\n * <pre>\n * 关闭Timer =》 关闭所有运行中的TaskLauncher =》 关闭所有运行中的TaskExecutor\n * </pre>\n *\n * 其中：\n *\n * <pre>\n * <strong>TaskLauncher</strong>：定时器每分钟调用一次（如果{@link Scheduler#isMatchSecond()}为{@code true}每秒调用一次），\n * 负责检查<strong>TaskTable</strong>是否有匹配到此时间运行的Task\n * </pre>\n *\n * <pre>\n * <strong>TaskExecutor</strong>：TaskLauncher匹配成功后，触发TaskExecutor执行具体的作业，执行完毕销毁\n * </pre>\n *\n * @author Looly\n *\n */\npublic class Scheduler implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Lock lock = new ReentrantLock();\n\n\t/** 定时任务配置 */\n\tprotected CronConfig config = new CronConfig();\n\t/** 是否已经启动 */\n\tprivate boolean started = false;\n\t/** 是否为守护线程 */\n\tprotected boolean daemon;\n\n\t/** 定时器 */\n\tprivate CronTimer timer;\n\t/** 定时任务表 */\n\tprotected TaskTable taskTable = new TaskTable();\n\t/** 启动器管理器 */\n\tprotected TaskLauncherManager taskLauncherManager;\n\t/** 执行器管理器 */\n\tprotected TaskExecutorManager taskExecutorManager;\n\t/** 监听管理器列表 */\n\tprotected TaskListenerManager listenerManager = new TaskListenerManager();\n\t/** 线程池，用于执行TaskLauncher和TaskExecutor */\n\tprotected ExecutorService threadExecutor;\n\n\t// --------------------------------------------------------- Getters and Setters start\n\t/**\n\t * 设置时区\n\t *\n\t * @param timeZone 时区\n\t * @return this\n\t */\n\tpublic Scheduler setTimeZone(TimeZone timeZone) {\n\t\tthis.config.setTimeZone(timeZone);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得时区，默认为 {@link TimeZone#getDefault()}\n\t *\n\t * @return 时区\n\t */\n\tpublic TimeZone getTimeZone() {\n\t\treturn this.config.getTimeZone();\n\t}\n\n\t/**\n\t * 设置是否为守护线程<br>\n\t * 如果为true，则在调用{@link #stop()}方法后执行的定时任务立即结束，否则等待执行完毕才结束。默认非守护线程<br>\n\t * 如果用户调用{@link #setThreadExecutor(ExecutorService)}自定义线程池则此参数无效\n\t *\n\t * @param on {@code true}为守护线程，否则非守护线程\n\t * @return this\n\t * @throws CronException 定时任务已经启动抛出此异常\n\t */\n\tpublic Scheduler setDaemon(boolean on) throws CronException {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tcheckStarted();\n\t\t\tthis.daemon = on;\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置自定义线程池<br>\n\t * 自定义线程池时须考虑方法执行的线程是否为守护线程\n\t *\n\t * @param threadExecutor 自定义线程池\n\t * @return this\n\t * @throws CronException 定时任务已经启动抛出此异常\n\t * @since 5.7.10\n\t */\n\tpublic Scheduler setThreadExecutor(ExecutorService threadExecutor) throws CronException {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tcheckStarted();\n\t\t\tthis.threadExecutor = threadExecutor;\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否为守护线程\n\t *\n\t * @return 是否为守护线程\n\t */\n\tpublic boolean isDaemon() {\n\t\treturn this.daemon;\n\t}\n\n\t/**\n\t * 是否支持秒匹配\n\t *\n\t * @return {@code true}使用，{@code false}不使用\n\t */\n\tpublic boolean isMatchSecond() {\n\t\treturn this.config.isMatchSecond();\n\t}\n\n\t/**\n\t * 设置是否支持秒匹配，默认不使用\n\t *\n\t * @param isMatchSecond {@code true}支持，{@code false}不支持\n\t * @return this\n\t */\n\tpublic Scheduler setMatchSecond(boolean isMatchSecond) {\n\t\tthis.config.setMatchSecond(isMatchSecond);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加监听器\n\t *\n\t * @param listener {@link TaskListener}\n\t * @return this\n\t */\n\tpublic Scheduler addListener(TaskListener listener) {\n\t\tthis.listenerManager.addListener(listener);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除监听器\n\t *\n\t * @param listener {@link TaskListener}\n\t * @return this\n\t */\n\tpublic Scheduler removeListener(TaskListener listener) {\n\t\tthis.listenerManager.removeListener(listener);\n\t\treturn this;\n\t}\n\t// --------------------------------------------------------- Getters and Setters end\n\n\t// -------------------------------------------------------------------- shcedule start\n\t/**\n\t * 批量加入配置文件中的定时任务<br>\n\t * 配置文件格式为： xxx.xxx.xxx.Class.method = * * * * *\n\t *\n\t * @param cronSetting 定时任务设置文件\n\t * @return this\n\t */\n\tpublic Scheduler schedule(Setting cronSetting) {\n\t\tif (MapUtil.isNotEmpty(cronSetting)) {\n\t\t\tString group;\n\t\t\tfor (Entry<String, LinkedHashMap<String, String>> groupedEntry : cronSetting.getGroupedMap().entrySet()) {\n\t\t\t\tgroup = groupedEntry.getKey();\n\t\t\t\tfor (Entry<String, String> entry : groupedEntry.getValue().entrySet()) {\n\t\t\t\t\tString jobClass = entry.getKey();\n\t\t\t\t\tif (StrUtil.isNotBlank(group)) {\n\t\t\t\t\t\tjobClass = group + CharUtil.DOT + jobClass;\n\t\t\t\t\t}\n\t\t\t\t\tfinal String pattern = entry.getValue();\n\t\t\t\t\tStaticLog.debug(\"Load job: {} {}\", pattern, jobClass);\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// issue#I5E7BM@Gitee，自定义ID避免重复从配置文件加载\n\t\t\t\t\t\tschedule(\"id_\" + jobClass, pattern, new InvokeTask(jobClass));\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tthrow new CronException(e, \"Schedule [{}] [{}] error!\", pattern, jobClass);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 新增Task，使用随机UUID\n\t *\n\t * @param pattern {@link CronPattern}对应的String表达式\n\t * @param task {@link Runnable}\n\t * @return ID\n\t */\n\tpublic String schedule(String pattern, Runnable task) {\n\t\treturn schedule(pattern, new RunnableTask(task));\n\t}\n\n\t/**\n\t * 新增Task，使用随机UUID\n\t *\n\t * @param pattern {@link CronPattern}对应的String表达式\n\t * @param task {@link Task}\n\t * @return ID\n\t */\n\tpublic String schedule(String pattern, Task task) {\n\t\tString id = IdUtil.fastUUID();\n\t\tschedule(id, pattern, task);\n\t\treturn id;\n\t}\n\n\t/**\n\t * 新增Task，如果任务ID已经存在，抛出异常\n\t *\n\t * @param id ID，为每一个Task定义一个ID\n\t * @param pattern {@link CronPattern}对应的String表达式\n\t * @param task {@link Runnable}\n\t * @return this\n\t */\n\tpublic Scheduler schedule(String id, String pattern, Runnable task) {\n\t\treturn schedule(id, new CronPattern(pattern), new RunnableTask(task));\n\t}\n\n\t/**\n\t * 新增Task，如果任务ID已经存在，抛出异常\n\t *\n\t * @param id ID，为每一个Task定义一个ID\n\t * @param pattern {@link CronPattern}对应的String表达式\n\t * @param task {@link Task}\n\t * @return this\n\t */\n\tpublic Scheduler schedule(String id, String pattern, Task task) {\n\t\treturn schedule(id, new CronPattern(pattern), task);\n\t}\n\n\t/**\n\t * 新增Task，如果任务ID已经存在，抛出异常\n\t *\n\t * @param id ID，为每一个Task定义一个ID\n\t * @param pattern {@link CronPattern}\n\t * @param task {@link Task}\n\t * @return this\n\t */\n\tpublic Scheduler schedule(String id, CronPattern pattern, Task task) {\n\t\ttaskTable.add(id, pattern, task);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除Task\n\t *\n\t * @param id Task的ID\n\t * @return this\n\t */\n\tpublic Scheduler deschedule(String id) {\n\t\tdescheduleWithStatus(id);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除Task，并返回是否移除成功\n\t *\n\t * @param id Task的ID\n\t * @return 是否移除成功，{@code false}表示未找到对应ID的任务\n\t * @since 5.7.17\n\t */\n\tpublic boolean descheduleWithStatus(String id) {\n\t\treturn this.taskTable.remove(id);\n\t}\n\n\t/**\n\t * 更新Task执行的时间规则\n\t *\n\t * @param id Task的ID\n\t * @param pattern {@link CronPattern}\n\t * @return this\n\t * @since 4.0.10\n\t */\n\tpublic Scheduler updatePattern(String id, CronPattern pattern) {\n\t\tthis.taskTable.updatePattern(id, pattern);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取定时任务表，注意此方法返回非复制对象，对返回对象的修改将影响已有定时任务\n\t *\n\t * @return 定时任务表{@link TaskTable}\n\t * @since 4.6.7\n\t */\n\tpublic TaskTable getTaskTable() {\n\t\treturn this.taskTable;\n\t}\n\n\t/**\n\t * 获得指定id的{@link CronPattern}\n\t *\n\t * @param id ID\n\t * @return {@link CronPattern}\n\t * @since 3.1.1\n\t */\n\tpublic CronPattern getPattern(String id) {\n\t\treturn this.taskTable.getPattern(id);\n\t}\n\n\t/**\n\t * 获得指定id的{@link Task}\n\t *\n\t * @param id ID\n\t * @return {@link Task}\n\t * @since 3.1.1\n\t */\n\tpublic Task getTask(String id) {\n\t\treturn this.taskTable.getTask(id);\n\t}\n\n\t/**\n\t * 是否无任务\n\t *\n\t * @return true表示无任务\n\t * @since 4.0.2\n\t */\n\tpublic boolean isEmpty() {\n\t\treturn this.taskTable.isEmpty();\n\t}\n\n\t/**\n\t * 当前任务数\n\t *\n\t * @return 当前任务数\n\t * @since 4.0.2\n\t */\n\tpublic int size() {\n\t\treturn this.taskTable.size();\n\t}\n\n\t/**\n\t * 清空任务表\n\t * @return this\n\t * @since 4.1.17\n\t */\n\tpublic Scheduler clear() {\n\t\tthis.taskTable = new TaskTable();\n\t\treturn this;\n\t}\n\t// -------------------------------------------------------------------- shcedule end\n\n\t/**\n\t * @return 是否已经启动\n\t */\n\tpublic boolean isStarted() {\n\t\treturn this.started;\n\t}\n\n\t/**\n\t * 启动\n\t *\n\t * @param isDaemon 是否以守护线程方式启动，如果为true，则在调用{@link #stop()}方法后执行的定时任务立即结束，否则等待执行完毕才结束。\n\t * @return this\n\t */\n\tpublic Scheduler start(boolean isDaemon) {\n\t\tthis.daemon = isDaemon;\n\t\treturn start();\n\t}\n\n\t/**\n\t * 启动\n\t *\n\t * @return this\n\t */\n\tpublic Scheduler start() {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tcheckStarted();\n\n\t\t\tif(null == this.threadExecutor){\n\t\t\t\t// 无界线程池，确保每一个需要执行的线程都可以及时运行，同时复用已有线程避免线程重复创建\n\t\t\t\tthis.threadExecutor = ExecutorBuilder.create().useSynchronousQueue().setThreadFactory(//\n\t\t\t\t\t\tThreadFactoryBuilder.create().setNamePrefix(\"hutool-cron-\").setDaemon(this.daemon).build()//\n\t\t\t\t).build();\n\t\t\t}\n\t\t\tthis.taskLauncherManager = new TaskLauncherManager(this);\n\t\t\tthis.taskExecutorManager = new TaskExecutorManager(this);\n\n\t\t\t// Start CronTimer\n\t\t\ttimer = new CronTimer(this);\n\t\t\ttimer.setDaemon(this.daemon);\n\t\t\ttimer.start();\n\t\t\tthis.started = true;\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 停止定时任务<br>\n\t * 此方法调用后会将定时器进程立即结束，如果为守护线程模式，则正在执行的作业也会自动结束，否则作业线程将在执行完成后结束。<br>\n\t * 此方法并不会清除任务表中的任务，请调用{@link #clear()} 方法清空任务或者使用{@link #stop(boolean)}方法可选是否清空\n\t *\n\t * @return this\n\t */\n\tpublic Scheduler stop() {\n\t\treturn stop(false);\n\t}\n\n\t/**\n\t * 停止定时任务<br>\n\t * 此方法调用后会将定时器进程立即结束，如果为守护线程模式，则正在执行的作业也会自动结束，否则作业线程将在执行完成后结束。\n\t *\n\t * @param clearTasks 是否清除所有任务\n\t * @return this\n\t * @since 4.1.17\n\t */\n\tpublic Scheduler stop(boolean clearTasks) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tif (false == started) {\n\t\t\t\tthrow new IllegalStateException(\"Scheduler not started !\");\n\t\t\t}\n\n\t\t\t// 停止CronTimer\n\t\t\tthis.timer.stopTimer();\n\t\t\tthis.timer = null;\n\n\t\t\t//停止线程池\n\t\t\tthis.threadExecutor.shutdown();\n\t\t\tthis.threadExecutor = null;\n\n\t\t\t//可选是否清空任务表\n\t\t\tif(clearTasks) {\n\t\t\t\tclear();\n\t\t\t}\n\n\t\t\t// 修改标志\n\t\t\tstarted = false;\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 检查定时任务是否已经启动\n\t *\n\t * @throws CronException 已经启动则抛出此异常\n\t */\n\tprivate void checkStarted() throws CronException{\n\t\tif (this.started) {\n\t\t\tthrow new CronException(\"Scheduler already started!\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/TaskExecutor.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.cron.task.CronTask;\nimport cn.hutool.cron.task.Task;\n\n/**\n * 作业执行器<br>\n * 执行具体的作业，执行完毕销毁<br>\n * 作业执行器唯一关联一个作业，负责管理作业的运行的生命周期。\n *\n * @author Looly\n */\npublic class TaskExecutor implements Runnable {\n\n\tprivate final Scheduler scheduler;\n\tprivate final CronTask task;\n\n\t/**\n\t * 获得原始任务对象\n\t *\n\t * @return 任务对象\n\t */\n\tpublic Task getTask() {\n\t\treturn this.task.getRaw();\n\t}\n\n\t/**\n\t * 获得原始任务对象\n\t *\n\t * @return 任务对象\n\t * @since 5.4.7\n\t */\n\tpublic CronTask getCronTask() {\n\t\treturn this.task;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param scheduler 调度器\n\t * @param task 被执行的任务\n\t */\n\tpublic TaskExecutor(Scheduler scheduler, CronTask task) {\n\t\tthis.scheduler = scheduler;\n\t\tthis.task = task;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\ttry {\n\t\t\tscheduler.listenerManager.notifyTaskStart(this);\n\t\t\ttask.execute();\n\t\t\tscheduler.listenerManager.notifyTaskSucceeded(this);\n\t\t} catch (Exception e) {\n\t\t\tscheduler.listenerManager.notifyTaskFailed(this, e);\n\t\t} finally {\n\t\t\tscheduler.taskExecutorManager.notifyExecutorCompleted(this);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/TaskExecutorManager.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.cron.task.CronTask;\nimport cn.hutool.cron.task.Task;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 作业执行管理器<br>\n * 负责管理作业的启动、停止等\n *\n * <p>\n * 此类用于管理正在运行的作业情况，作业启动后加入任务列表，任务结束移除\n * </p>\n *\n * @author Looly\n * @since 3.0.1\n */\npublic class TaskExecutorManager implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected Scheduler scheduler;\n\t/**\n\t * 执行器列表\n\t */\n\tprivate final List<TaskExecutor> executors = new ArrayList<>();\n\n\tpublic TaskExecutorManager(Scheduler scheduler) {\n\t\tthis.scheduler = scheduler;\n\t}\n\n\t/**\n\t * 获取所有正在执行的任务调度执行器\n\t *\n\t * @return 任务执行器列表\n\t * @since 4.6.7\n\t */\n\tpublic List<TaskExecutor> getExecutors() {\n\t\treturn Collections.unmodifiableList(this.executors);\n\t}\n\n\t/**\n\t * 启动 执行器TaskExecutor，即启动作业\n\t *\n\t * @param task {@link Task}\n\t * @return {@link TaskExecutor}\n\t */\n\tpublic TaskExecutor spawnExecutor(CronTask task) {\n\t\tfinal TaskExecutor executor = new TaskExecutor(this.scheduler, task);\n\t\tsynchronized (this.executors) {\n\t\t\tthis.executors.add(executor);\n\t\t}\n\t\t// 子线程是否为deamon线程取决于父线程，因此此处无需显示调用\n\t\t// executor.setDaemon(this.scheduler.daemon);\n//\t\texecutor.start();\n\t\tthis.scheduler.threadExecutor.execute(executor);\n\t\treturn executor;\n\t}\n\n\t/**\n\t * 执行器执行完毕调用此方法，将执行器从执行器列表移除，此方法由{@link TaskExecutor}对象调用，用于通知管理器自身已完成执行\n\t *\n\t * @param executor 执行器 {@link TaskExecutor}\n\t * @return this\n\t */\n\tpublic TaskExecutorManager notifyExecutorCompleted(TaskExecutor executor) {\n\t\tsynchronized (executors) {\n\t\t\texecutors.remove(executor);\n\t\t}\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/TaskLauncher.java",
    "content": "package cn.hutool.cron;\n\n/**\n * 作业启动器<br>\n * 负责检查 {@link TaskTable} 是否有匹配到此时运行的Task<br>\n * 检查完毕后启动器结束\n *\n * @author Looly\n */\npublic class TaskLauncher implements Runnable {\n\n\tprivate final Scheduler scheduler;\n\tprivate final long millis;\n\n\t/**\n\t * 构造\n\t *\n\t * @param scheduler {@link Scheduler}\n\t * @param millis    毫秒数\n\t */\n\tpublic TaskLauncher(Scheduler scheduler, long millis) {\n\t\tthis.scheduler = scheduler;\n\t\tthis.millis = millis;\n\t}\n\n\t@Override\n\tpublic void run() {\n\t\t//匹配秒部分由用户定义决定，始终不匹配年\n\t\tscheduler.taskTable.executeTaskIfMatch(this.scheduler, this.millis);\n\n\t\t//结束通知\n\t\tscheduler.taskLauncherManager.notifyLauncherCompleted(this);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/TaskLauncherManager.java",
    "content": "package cn.hutool.cron;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 作业启动管理器\n *\n * @author looly\n *\n */\npublic class TaskLauncherManager implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprotected Scheduler scheduler;\n\t/** 启动器列表 */\n\tprotected final List<TaskLauncher> launchers = new ArrayList<>();\n\n\tpublic TaskLauncherManager(Scheduler scheduler) {\n\t\tthis.scheduler = scheduler;\n\t}\n\n\t/**\n\t * 启动 TaskLauncher\n\t * @param millis 触发事件的毫秒数\n\t * @return {@link TaskLauncher}\n\t */\n\tprotected TaskLauncher spawnLauncher(long millis) {\n\t\tfinal TaskLauncher launcher = new TaskLauncher(this.scheduler, millis);\n\t\tsynchronized (this.launchers) {\n\t\t\tthis.launchers.add(launcher);\n\t\t}\n\t\t//子线程是否为deamon线程取决于父线程，因此此处无需显示调用\n\t\t//launcher.setDaemon(this.scheduler.daemon);\n//\t\tlauncher.start();\n\t\tthis.scheduler.threadExecutor.execute(launcher);\n\t\treturn launcher;\n\t}\n\n\t/**\n\t * 启动器启动完毕，启动完毕后从执行器列表中移除\n\t * @param launcher 启动器 {@link TaskLauncher}\n\t */\n\tprotected void notifyLauncherCompleted(TaskLauncher launcher) {\n\t\tsynchronized (launchers) {\n\t\t\tlaunchers.remove(launcher);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/TaskTable.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.cron.pattern.CronPattern;\nimport cn.hutool.cron.task.CronTask;\nimport cn.hutool.cron.task.Task;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\n\n/**\n * 定时任务表<br>\n * 任务表将ID、表达式、任务一一对应，定时任务执行过程中，会周期性检查定时任务表中的所有任务表达式匹配情况，从而执行其对应的任务<br>\n * 任务的添加、移除使用读写锁保证线程安全性\n *\n * @author Looly\n */\npublic class TaskTable implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final int DEFAULT_CAPACITY = 10;\n\n\tprivate final ReadWriteLock lock;\n\n\tprivate final List<String> ids;\n\tprivate final List<CronPattern> patterns;\n\tprivate final List<Task> tasks;\n\tprivate int size;\n\n\t/**\n\t * 构造\n\t */\n\tpublic TaskTable() {\n\t\tthis(DEFAULT_CAPACITY);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param initialCapacity 容量，即预估的最大任务数\n\t */\n\tpublic TaskTable(int initialCapacity) {\n\t\tlock = new ReentrantReadWriteLock();\n\n\t\tids = new ArrayList<>(initialCapacity);\n\t\tpatterns = new ArrayList<>(initialCapacity);\n\t\ttasks = new ArrayList<>(initialCapacity);\n\t}\n\n\t/**\n\t * 新增Task\n\t *\n\t * @param id      ID\n\t * @param pattern {@link CronPattern}\n\t * @param task    {@link Task}\n\t * @return this\n\t */\n\tpublic TaskTable add(String id, CronPattern pattern, Task task) {\n\t\tfinal Lock writeLock = lock.writeLock();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tif (ids.contains(id)) {\n\t\t\t\tthrow new CronException(\"Id [{}] has been existed!\", id);\n\t\t\t}\n\t\t\tids.add(id);\n\t\t\tpatterns.add(pattern);\n\t\t\ttasks.add(task);\n\t\t\tsize++;\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取所有ID，返回不可变列表，即列表不可修改\n\t *\n\t * @return ID列表\n\t * @since 4.6.7\n\t */\n\tpublic List<String> getIds() {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn Collections.unmodifiableList(this.ids);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获取所有定时任务表达式，返回不可变列表，即列表不可修改\n\t *\n\t * @return 定时任务表达式列表\n\t * @since 4.6.7\n\t */\n\tpublic List<CronPattern> getPatterns() {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn Collections.unmodifiableList(this.patterns);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获取所有定时任务，返回不可变列表，即列表不可修改\n\t *\n\t * @return 定时任务列表\n\t * @since 4.6.7\n\t */\n\tpublic List<Task> getTasks() {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn Collections.unmodifiableList(this.tasks);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 移除Task\n\t *\n\t * @param id Task的ID\n\t * @return 是否成功移除，{@code false}表示未找到对应ID的任务\n\t */\n\tpublic boolean remove(String id) {\n\t\tfinal Lock writeLock = lock.writeLock();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tfinal int index = ids.indexOf(id);\n\t\t\tif (index < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\ttasks.remove(index);\n\t\t\tpatterns.remove(index);\n\t\t\tids.remove(index);\n\t\t\tsize--;\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 更新某个Task的定时规则\n\t *\n\t * @param id      Task的ID\n\t * @param pattern 新的表达式\n\t * @return 是否更新成功，如果id对应的规则不存在则不更新\n\t * @since 4.0.10\n\t */\n\tpublic boolean updatePattern(String id, CronPattern pattern) {\n\t\tfinal Lock writeLock = lock.writeLock();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tfinal int index = ids.indexOf(id);\n\t\t\tif (index > -1) {\n\t\t\t\tpatterns.set(index, pattern);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获得指定位置的{@link Task}\n\t *\n\t * @param index 位置\n\t * @return {@link Task}\n\t * @since 3.1.1\n\t */\n\tpublic Task getTask(int index) {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn tasks.get(index);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获得指定id的{@link Task}\n\t *\n\t * @param id ID\n\t * @return {@link Task}\n\t * @since 3.1.1\n\t */\n\tpublic Task getTask(String id) {\n\t\tfinal int index = ids.indexOf(id);\n\t\tif (index > -1) {\n\t\t\treturn getTask(index);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得指定位置的{@link CronPattern}\n\t *\n\t * @param index 位置\n\t * @return {@link CronPattern}\n\t * @since 3.1.1\n\t */\n\tpublic CronPattern getPattern(int index) {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn patterns.get(index);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 任务表大小，加入的任务数\n\t *\n\t * @return 任务表大小，加入的任务数\n\t * @since 4.0.2\n\t */\n\tpublic int size() {\n\t\treturn this.size;\n\t}\n\n\t/**\n\t * 任务表是否为空\n\t *\n\t * @return true为空\n\t * @since 4.0.2\n\t */\n\tpublic boolean isEmpty() {\n\t\treturn this.size < 1;\n\t}\n\n\t/**\n\t * 获得指定id的{@link CronPattern}\n\t *\n\t * @param id ID\n\t * @return {@link CronPattern}\n\t * @since 3.1.1\n\t */\n\tpublic CronPattern getPattern(String id) {\n\t\tfinal int index = ids.indexOf(id);\n\t\tif (index > -1) {\n\t\t\treturn getPattern(index);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 如果时间匹配则执行相应的Task，带读锁\n\t *\n\t * @param scheduler {@link Scheduler}\n\t * @param millis 时间毫秒\n\t */\n\tpublic void executeTaskIfMatch(Scheduler scheduler, long millis) {\n\t\tfinal Lock readLock = lock.readLock();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\texecuteTaskIfMatchInternal(scheduler, millis);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder builder = StrUtil.builder();\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tbuilder.append(StrUtil.format(\"[{}] [{}] [{}]\\n\",\n\t\t\t\t\tids.get(i), patterns.get(i), tasks.get(i)));\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 如果时间匹配则执行相应的Task，无锁\n\t *\n\t * @param scheduler {@link Scheduler}\n\t * @param millis 时间毫秒\n\t * @since 3.1.1\n\t */\n\tprotected void executeTaskIfMatchInternal(Scheduler scheduler, long millis) {\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tif (patterns.get(i).match(scheduler.config.timezone, millis, scheduler.config.matchSecond)) {\n\t\t\t\tscheduler.taskExecutorManager.spawnExecutor(new CronTask(ids.get(i), patterns.get(i), tasks.get(i)));\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/listener/SimpleTaskListener.java",
    "content": "package cn.hutool.cron.listener;\n\nimport cn.hutool.cron.TaskExecutor;\n\n/**\n * 简单监听实现，不做任何操作<br>\n * 继承此监听后实现需要的方法即可\n * @author Looly\n *\n */\npublic class SimpleTaskListener implements TaskListener{\n\n\t@Override\n\tpublic void onStart(TaskExecutor executor) {\n\t}\n\n\t@Override\n\tpublic void onSucceeded(TaskExecutor executor) {\n\t}\n\n\t@Override\n\tpublic void onFailed(TaskExecutor executor, Throwable exception) {\n\t}\n\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListener.java",
    "content": "package cn.hutool.cron.listener;\n\nimport cn.hutool.cron.TaskExecutor;\n\n/**\n * 定时任务监听接口<br>\n * 通过实现此接口，实现对定时任务的各个环节做监听\n * @author Looly\n *\n */\npublic interface TaskListener {\n\t/**\n\t * 定时任务启动时触发\n\t * @param executor {@link TaskExecutor}\n\t */\n\tvoid onStart(TaskExecutor executor);\n\t\n\t/**\n\t * 任务成功结束时触发\n\t * \n\t * @param executor {@link TaskExecutor}\n\t */\n\tvoid onSucceeded(TaskExecutor executor);\n\n\t/**\n\t * 任务启动失败时触发\n\t * \n\t * @param executor {@link TaskExecutor}\n\t * @param exception 异常\n\t */\n\tvoid onFailed(TaskExecutor executor, Throwable exception);\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListenerManager.java",
    "content": "package cn.hutool.cron.listener;\n\nimport cn.hutool.cron.TaskExecutor;\nimport cn.hutool.log.StaticLog;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 监听调度器，统一管理监听\n * @author Looly\n *\n */\npublic class TaskListenerManager implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final List<TaskListener> listeners = new ArrayList<>();\n\n\t/**\n\t * 增加监听器\n\t * @param listener {@link TaskListener}\n\t * @return this\n\t */\n\tpublic TaskListenerManager addListener(TaskListener listener){\n\t\tsynchronized (listeners) {\n\t\t\tthis.listeners.add(listener);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除监听器\n\t * @param listener {@link TaskListener}\n\t * @return this\n\t */\n\tpublic TaskListenerManager removeListener(TaskListener listener){\n\t\tsynchronized (listeners) {\n\t\t\tthis.listeners.remove(listener);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 通知所有监听任务启动器启动\n\t * @param executor {@link TaskExecutor}\n\t */\n\tpublic void notifyTaskStart(TaskExecutor executor) {\n\t\tsynchronized (listeners) {\n\t\t\tTaskListener listener;\n\t\t\tfor (TaskListener taskListener : listeners) {\n\t\t\t\tlistener = taskListener;\n\t\t\t\tif (null != listener) {\n\t\t\t\t\tlistener.onStart(executor);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 通知所有监听任务启动器成功结束\n\t * @param executor {@link TaskExecutor}\n\t */\n\tpublic void notifyTaskSucceeded(TaskExecutor executor) {\n\t\tsynchronized (listeners) {\n\t\t\tfor (TaskListener listener : listeners) {\n\t\t\t\tlistener.onSucceeded(executor);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 通知所有监听任务启动器结束并失败<br>\n\t * 无监听将打印堆栈到命令行\n\t * @param executor {@link TaskExecutor}\n\t * @param exception 失败原因\n\t */\n\tpublic void notifyTaskFailed(TaskExecutor executor, Throwable exception) {\n\t\tsynchronized (listeners) {\n\t\t\tint size = listeners.size();\n\t\t\tif(size > 0){\n\t\t\t\tfor (TaskListener listener : listeners) {\n\t\t\t\t\tlistener.onFailed(executor, exception);\n\t\t\t\t}\n\t\t\t}else{\n\t\t\t\tStaticLog.error(exception, exception.getMessage());\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/listener/package-info.java",
    "content": "/**\n * 定时任务执行监听接口及部分实现\n * \n * @author looly\n *\n */\npackage cn.hutool.cron.listener;"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/package-info.java",
    "content": "/**\n * 定时任务模块，提供类Crontab表达式的定时任务，实现参考了Cron4j，同时可以支持秒级别的定时任务定义和年的定义（同时兼容Crontab、Cron4j、Quartz表达式）<br>\n * 定时任务模块由三部分组成：\n * <ul>\n *     <li>{@link cn.hutool.cron.Scheduler} 定时任务调度器，用于整体管理任务的增删、启停和触发运行。</li>\n *     <li>{@link cn.hutool.cron.task.Task} 定时任务实现，用于定义具体的任务</li>\n *     <li>{@link cn.hutool.cron.pattern.CronPattern} 定时任务表达式，用于定义任务触发时间</li>\n * </ul>\n *\n * 同时，提供了{@link cn.hutool.cron.CronUtil}工具类，维护一个全局的{@link cn.hutool.cron.Scheduler}。\n *\n * @author looly\n */\npackage cn.hutool.cron;\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.CalendarUtil;\nimport cn.hutool.cron.pattern.matcher.PatternMatcher;\nimport cn.hutool.cron.pattern.parser.PatternParser;\n\nimport java.time.LocalDateTime;\nimport java.util.*;\n\n/**\n * 定时任务表达式<br>\n * 表达式类似于Linux的crontab表达式，表达式使用空格分成5个部分，按顺序依次为：\n * <ol>\n * <li><strong>分</strong> ：范围：0~59</li>\n * <li><strong>时</strong> ：范围：0~23</li>\n * <li><strong>日</strong> ：范围：1~31，<strong>\"L\"</strong> 表示月的最后一天</li>\n * <li><strong>月</strong> ：范围：1~12，同时支持不区分大小写的别名：\"jan\",\"feb\", \"mar\", \"apr\", \"may\",\"jun\", \"jul\", \"aug\", \"sep\",\"oct\", \"nov\", \"dec\"</li>\n * <li><strong>周</strong> ：范围：0 (Sunday)~6(Saturday)，7也可以表示周日，同时支持不区分大小写的别名：\"sun\",\"mon\", \"tue\", \"wed\", \"thu\",\"fri\", \"sat\"，<strong>\"L\"</strong> 表示周六</li>\n * </ol>\n * <p>\n * 为了兼容Quartz表达式，同时支持6位和7位表达式，其中：<br>\n *\n * <pre>\n * 当为6位时，第一位表示<strong>秒</strong> ，范围0~59，但是第一位不做匹配\n * 当为7位时，最后一位表示<strong>年</strong> ，范围1970~2099，但是第7位不做解析，也不做匹配\n * </pre>\n * <p>\n * 当定时任务运行到的时间匹配这些表达式后，任务被启动。<br>\n * 注意：\n *\n * <pre>\n * 当isMatchSecond为{@code true}时才会匹配秒部分\n * 默认都是关闭的\n * </pre>\n * <p>\n * 对于每一个子表达式，同样支持以下形式：\n * <ul>\n * <li><strong>*</strong> ：表示匹配这个位置所有的时间</li>\n * <li><strong>?</strong> ：表示匹配这个位置任意的时间（与\"*\"作用一致）</li>\n * <li><strong>*&#47;2</strong> ：表示间隔时间，例如在分上，表示每两分钟，同样*可以使用数字列表代替，逗号分隔</li>\n * <li><strong>2-8</strong> ：表示连续区间，例如在分上，表示2,3,4,5,6,7,8分</li>\n * <li><strong>2,3,5,8</strong> ：表示列表</li>\n * <li><strong>cronA | cronB</strong> ：表示多个定时表达式</li>\n * </ul>\n * 注意：在每一个子表达式中优先级：\n *\n * <pre>\n * 间隔（/） &gt; 区间（-） &gt; 列表（,）\n * </pre>\n * <p>\n * 例如 2,3,6/3中，由于“/”优先级高，因此相当于2,3,(6/3)，结果与 2,3,6等价<br>\n * <br>\n * <p>\n * 一些例子：\n * <ul>\n * <li><strong>5 * * * *</strong> ：每个点钟的5分执行，00:05,01:05……</li>\n * <li><strong>* * * * *</strong> ：每分钟执行</li>\n * <li><strong>*&#47;2 * * * *</strong> ：每两分钟执行</li>\n * <li><strong>* 12 * * *</strong> ：12点的每分钟执行</li>\n * <li><strong>59 11 * * 1,2</strong> ：每周一和周二的11:59执行</li>\n * <li><strong>3-18&#47;5 * * * *</strong> ：3~18分，每5分钟执行一次，即0:03, 0:08, 0:13, 0:18, 1:03, 1:08……</li>\n * </ul>\n *\n * @author Looly\n */\npublic class CronPattern {\n\n\tprivate final String pattern;\n\tprivate final List<PatternMatcher> matchers;\n\n\t/**\n\t * 解析表达式为 CronPattern\n\t *\n\t * @param pattern 表达式\n\t * @return CronPattern\n\t * @since 5.8.0\n\t */\n\tpublic static CronPattern of(String pattern) {\n\t\treturn new CronPattern(pattern);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pattern 表达式\n\t */\n\tpublic CronPattern(String pattern) {\n\t\tthis.pattern = pattern;\n\t\tthis.matchers = PatternParser.parse(pattern);\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param millis        时间毫秒数\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tpublic boolean match(long millis, boolean isMatchSecond) {\n\t\treturn match(TimeZone.getDefault(), millis, isMatchSecond);\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param timezone      时区 {@link TimeZone}\n\t * @param millis        时间毫秒数\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tpublic boolean match(TimeZone timezone, long millis, boolean isMatchSecond) {\n\t\tfinal GregorianCalendar calendar = new GregorianCalendar(timezone);\n\t\tcalendar.setTimeInMillis(millis);\n\t\treturn match(calendar, isMatchSecond);\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param calendar      时间\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tpublic boolean match(Calendar calendar, boolean isMatchSecond) {\n\t\treturn match(PatternUtil.getFields(calendar, isMatchSecond));\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param dateTime      时间\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t * @since 5.8.0\n\t */\n\tpublic boolean match(LocalDateTime dateTime, boolean isMatchSecond) {\n\t\treturn match(PatternUtil.getFields(dateTime, isMatchSecond));\n\t}\n\n\t/**\n\t * 返回匹配到的下一个时间\n\t *\n\t * @param calendar 时间\n\t * @return 匹配到的下一个时间\n\t */\n\tpublic Calendar nextMatchAfter(Calendar calendar) {\n\t\t// issue#I9FQUA，当提供的时间已经匹配表达式时，增加1秒以匹配下一个时间\n\t\tif(match(calendar, true)){\n\t\t\tfinal Calendar newCalendar = Calendar.getInstance(calendar.getTimeZone());\n\t\t\tnewCalendar.setTimeInMillis(calendar.getTimeInMillis() + 1000);\n\t\t\tcalendar = newCalendar;\n\t\t}\n\n\t\treturn nextMatch(calendar);\n\t}\n\n\t/**\n\t * 返回匹配到的下一个时间，如果给定时间匹配，直接返回\n\t *\n\t * @param calendar 时间\n\t * @return 匹配到的下一个时间\n\t * @since 5.8.30\n\t */\n\tpublic Calendar nextMatch(final Calendar calendar) {\n\t\tCalendar next = nextMatchAfter(PatternUtil.getFields(calendar, true), calendar.getTimeZone());\n\t\tif (match(next, true)) {\n\t\t\treturn next;\n\t\t}\n\n\t\tnext.set(Calendar.DAY_OF_MONTH, next.get(Calendar.DAY_OF_MONTH) + 1);\n\t\tnext = CalendarUtil.beginOfDay(next);\n\t\treturn nextMatch(next);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.pattern;\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param fields 时间字段值，{second, minute, hour, dayOfMonth, month, dayOfWeek, year}\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tprivate boolean match(int[] fields) {\n\t\tfor (PatternMatcher matcher : matchers) {\n\t\t\tif (matcher.match(fields)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取下一个最近的匹配日期时间\n\t *\n\t * @param values 时间字段值，{second, minute, hour, dayOfMonth, month, dayOfWeek, year}\n\t * @param zone   时区\n\t * @return {@link Calendar}，毫秒数为0\n\t */\n\tprivate Calendar nextMatchAfter(int[] values, TimeZone zone) {\n\t\tfinal List<Calendar> nextMatches = new ArrayList<>(matchers.size());\n\t\tfor (PatternMatcher matcher : matchers) {\n\t\t\tnextMatches.add(matcher.nextMatchAfter(values, zone));\n\t\t}\n\t\t// 返回匹配到的最早日期\n\t\treturn CollUtil.min(nextMatches);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternBuilder.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.text.StrJoiner;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 定时任务表达式构建器\n *\n * @author looly\n * @since 5.8.0\n */\npublic class CronPatternBuilder implements Builder<String> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tfinal String[] parts = new String[7];\n\n\t/**\n\t * 创建构建器\n\t * @return CronPatternBuilder\n\t */\n\tpublic static CronPatternBuilder of() {\n\t\treturn new CronPatternBuilder();\n\t}\n\n\t/**\n\t * 设置值\n\t *\n\t * @param part  部分，如秒、分、时等\n\t * @param values 时间值列表\n\t * @return this\n\t */\n\tpublic CronPatternBuilder setValues(Part part, int... values) {\n\t\tfor (int value : values) {\n\t\t\tpart.checkValue(value);\n\t\t}\n\t\treturn set(part, ArrayUtil.join(values, \",\"));\n\t}\n\n\t/**\n\t * 设置区间\n\t *\n\t * @param part  部分，如秒、分、时等\n\t * @param begin 起始值\n\t * @param end   结束值\n\t * @return this\n\t */\n\tpublic CronPatternBuilder setRange(Part part, int begin, int end) {\n\t\tAssert.notNull(part );\n\t\tpart.checkValue(begin);\n\t\tpart.checkValue(end);\n\t\treturn set(part, StrUtil.format(\"{}-{}\", begin, end));\n\t}\n\n\t/**\n\t * 设置对应部分的定时任务值\n\t *\n\t * @param part  部分，如秒、分、时等\n\t * @param value 表达式值，如\"*\"、\"1,2\"、\"5-12\"等\n\t * @return this\n\t */\n\tpublic CronPatternBuilder set(Part part, String value) {\n\t\tparts[part.ordinal()] = value;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String build() {\n\t\tfor (int i = Part.MINUTE.ordinal(); i < Part.YEAR.ordinal(); i++) {\n\t\t\t// 从分到周，用户未设置使用默认值\n\t\t\t// 秒和年如果不设置，忽略之\n\t\t\tif(StrUtil.isBlank(parts[i])){\n\t\t\t\tparts[i] = \"*\";\n\t\t\t}\n\t\t}\n\n\t\treturn StrJoiner.of(StrUtil.SPACE)\n\t\t\t\t.setNullMode(StrJoiner.NullMode.IGNORE)\n\t\t\t\t.append(this.parts)\n\t\t\t\t.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternUtil.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.CalendarUtil;\nimport cn.hutool.core.date.DateUnit;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 定时任务表达式工具类\n *\n * @author looly\n *\n */\npublic class CronPatternUtil {\n\n\t/**\n\t * 列举指定日期之后内第一个匹配表达式的日期\n\t *\n\t * @param pattern 表达式\n\t * @param start 起始时间\n\t * @return 日期\n\t * @since 5.8.30\n\t */\n\tpublic static Date nextDateAfter(CronPattern pattern, Date start) {\n\t\treturn DateUtil.date(pattern.nextMatchAfter(CalendarUtil.calendar(start)));\n\t}\n\n\t/**\n\t * 列举指定日期之后内第一个匹配表达式的日期\n\t *\n\t * @param pattern 表达式\n\t * @param start 起始时间\n\t * @param isMatchSecond 是否匹配秒（无效）\n\t * @return 日期\n\t * @since 4.5.8\n\t * @deprecated isMatchSecond无效，使用 {@link #nextDateAfter(CronPattern, Date)}\n\t */\n\t@Deprecated\n\tpublic static Date nextDateAfter(CronPattern pattern, Date start, boolean isMatchSecond) {\n\t\treturn DateUtil.date(pattern.nextMatchAfter(CalendarUtil.calendar(start)));\n\t}\n\n\t/**\n\t * 列举指定日期之后（到开始日期对应年年底）内所有匹配表达式的日期\n\t *\n\t * @param patternStr 表达式字符串\n\t * @param start 起始时间\n\t * @param count 列举数量\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 日期列表\n\t */\n\tpublic static List<Date> matchedDates(String patternStr, Date start, int count, boolean isMatchSecond) {\n\t\treturn matchedDates(patternStr, start, DateUtil.endOfYear(start), count, isMatchSecond);\n\t}\n\n\t/**\n\t * 列举指定日期范围内所有匹配表达式的日期\n\t *\n\t * @param patternStr 表达式字符串\n\t * @param start 起始时间\n\t * @param end 结束时间\n\t * @param count 列举数量\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 日期列表\n\t */\n\tpublic static List<Date> matchedDates(String patternStr, Date start, Date end, int count, boolean isMatchSecond) {\n\t\treturn matchedDates(patternStr, start.getTime(), end.getTime(), count, isMatchSecond);\n\t}\n\n\t/**\n\t * 列举指定日期范围内所有匹配表达式的日期\n\t *\n\t * @param patternStr 表达式字符串\n\t * @param start 起始时间\n\t * @param end 结束时间\n\t * @param count 列举数量\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 日期列表\n\t */\n\tpublic static List<Date> matchedDates(String patternStr, long start, long end, int count, boolean isMatchSecond) {\n\t\treturn matchedDates(new CronPattern(patternStr), start, end, count, isMatchSecond);\n\t}\n\n\t/**\n\t * 列举指定日期范围内所有匹配表达式的日期\n\t *\n\t * @param pattern 表达式\n\t * @param start 起始时间\n\t * @param end 结束时间\n\t * @param count 列举数量\n\t * @param isMatchSecond 是否匹配秒\n\t * @return 日期列表\n\t */\n\tpublic static List<Date> matchedDates(CronPattern pattern, long start, long end, int count, boolean isMatchSecond) {\n\t\tAssert.isTrue(start < end, \"Start date is later than end !\");\n\n\t\tfinal List<Date> result = new ArrayList<>(count);\n\t\tlong step = isMatchSecond ? DateUnit.SECOND.getMillis() : DateUnit.MINUTE.getMillis();\n\t\tfor (long i = start; i < end; i += step) {\n\t\t\tif (pattern.match(i, isMatchSecond)) {\n\t\t\t\tresult.add(DateUtil.date(i));\n\t\t\t\tif (result.size() >= count) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/Part.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.Month;\nimport cn.hutool.core.date.Week;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.cron.CronException;\n\nimport java.util.Calendar;\n\n/**\n * 表达式各个部分的枚举，用于限定在表达式中的位置和规则（如最小值和最大值）<br>\n * {@link #ordinal()}表示此部分在表达式中的位置，如0表示秒<br>\n * 表达式各个部分的枚举位置为：\n * <pre>\n *         0       1    2        3         4       5         6\n *     [SECOND] MINUTE HOUR DAY_OF_MONTH MONTH DAY_OF_WEEK [YEAR]\n * </pre>\n *\n * @author looly\n * @since 5.8.0\n */\npublic enum Part {\n\tSECOND(Calendar.SECOND, 0, 59),\n\tMINUTE(Calendar.MINUTE, 0, 59),\n\tHOUR(Calendar.HOUR_OF_DAY, 0, 23),\n\tDAY_OF_MONTH(Calendar.DAY_OF_MONTH, 1, 32),\n\tMONTH(Calendar.MONTH, Month.JANUARY.getValueBaseOne(), Month.DECEMBER.getValueBaseOne()),\n\tDAY_OF_WEEK(Calendar.DAY_OF_WEEK, Week.SUNDAY.ordinal(), Week.SATURDAY.ordinal()),\n\tYEAR(Calendar.YEAR, 1970, 2099);\n\n\t// ---------------------------------------------------------------\n\tprivate static final Part[] ENUMS = Part.values();\n\n\tprivate final int calendarField;\n\tprivate final int min;\n\tprivate final int max;\n\n\t/**\n\t * 构造\n\t *\n\t * @param calendarField Calendar中对应字段项\n\t * @param min           限定最小值（包含）\n\t * @param max           限定最大值（包含）\n\t */\n\tPart(int calendarField, int min, int max) {\n\t\tthis.calendarField = calendarField;\n\t\tif (min > max) {\n\t\t\tthis.min = max;\n\t\t\tthis.max = min;\n\t\t} else {\n\t\t\tthis.min = min;\n\t\t\tthis.max = max;\n\t\t}\n\t}\n\n\t/**\n\t * 获取Calendar中对应字段项\n\t *\n\t * @return Calendar中对应字段项\n\t */\n\tpublic int getCalendarField() {\n\t\treturn this.calendarField;\n\t}\n\n\t/**\n\t * 获取最小值\n\t *\n\t * @return 最小值\n\t */\n\tpublic int getMin() {\n\t\treturn this.min;\n\t}\n\n\t/**\n\t * 获取最大值\n\t *\n\t * @return 最大值\n\t */\n\tpublic int getMax() {\n\t\treturn this.max;\n\t}\n\n\t/**\n\t * 检查单个值是否有效\n\t *\n\t * @param value 值\n\t * @return 检查后的值\n\t * @throws CronException 检查无效抛出此异常\n\t */\n\tpublic int checkValue(int value) throws CronException {\n\t\tAssert.checkBetween(value, min, max,\n\t\t\t\t() -> new CronException(\"{} value {} out of range: [{} , {}]\", this.name(), value, min, max));\n\t\treturn value;\n\t}\n\n\t/**\n\t * 根据位置获取Part\n\t *\n\t * @param i 位置，从0开始\n\t * @return Part\n\t */\n\tpublic static Part of(int i) {\n\t\treturn ENUMS[i];\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/PatternUtil.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.Week;\n\nimport java.time.LocalDateTime;\nimport java.util.Calendar;\n\n/**\n * 表达式工具，内部使用\n *\n * @author looly\n * @since 5.8.0\n */\nclass PatternUtil {\n\n\t/**\n\t * 获取处理后的字段列表<br>\n\t * 月份从1开始，周从0开始\n\t *\n\t * @param dateTime      {@link Calendar}\n\t * @param isMatchSecond 是否匹配秒，{@link false}则秒返回-1\n\t * @return 字段值列表\n\t * @since 5.8.0\n\t */\n\tstatic int[] getFields(LocalDateTime dateTime, boolean isMatchSecond) {\n\t\tfinal int second = isMatchSecond ? dateTime.getSecond() : -1;\n\t\tfinal int minute = dateTime.getMinute();\n\t\tfinal int hour = dateTime.getHour();\n\t\tfinal int dayOfMonth = dateTime.getDayOfMonth();\n\t\tfinal int month = dateTime.getMonthValue();// 月份从1开始\n\t\tfinal int dayOfWeek = Week.of(dateTime.getDayOfWeek()).getValue() - 1; // 星期从0开始，0和7都表示周日\n\t\tfinal int year = dateTime.getYear();\n\t\treturn new int[]{second, minute, hour, dayOfMonth, month, dayOfWeek, year};\n\t}\n\n\t/**\n\t * 获取处理后的字段列表<br>\n\t * 月份从1开始，周从0开始\n\t *\n\t * @param calendar      {@link Calendar}\n\t * @param isMatchSecond 是否匹配秒，{@link false}则秒返回-1\n\t * @return 字段值列表\n\t * @since 5.8.0\n\t */\n\tstatic int[] getFields(Calendar calendar, boolean isMatchSecond) {\n\t\tfinal int second = isMatchSecond ? calendar.get(Calendar.SECOND) : -1;\n\t\tfinal int minute = calendar.get(Calendar.MINUTE);\n\t\tfinal int hour = calendar.get(Calendar.HOUR_OF_DAY);\n\t\tfinal int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);\n\t\tfinal int month = calendar.get(Calendar.MONTH) + 1;// 月份从1开始\n\t\tfinal int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始，0和7都表示周日\n\t\tfinal int year = calendar.get(Calendar.YEAR);\n\t\treturn new int[]{second, minute, hour, dayOfMonth, month, dayOfWeek, year};\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/AlwaysTrueMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 所有值匹配，始终返回{@code true}\n *\n * @author Looly\n */\npublic class AlwaysTrueMatcher implements PartMatcher {\n\n\tpublic static AlwaysTrueMatcher INSTANCE = new AlwaysTrueMatcher();\n\n\t@Override\n\tpublic boolean match(Integer t) {\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic int nextAfter(int value) {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.format(\"[Matcher]: always true.\");\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 将表达式中的数字值列表转换为Boolean数组，匹配时匹配相应数组位\n *\n * @author Looly\n */\npublic class BoolArrayMatcher implements PartMatcher {\n\n\t/**\n\t * 用户定义此字段的最小值\n\t */\n\tprivate final int minValue;\n\t/**\n\t * 用户定义此字段的最大值\n\t * @since 5.8.41\n\t */\n\tprivate final int maxValue;\n\tprotected final boolean[] bValues;\n\n\t/**\n\t * 构造\n\t *\n\t * @param intValueList 匹配值列表\n\t */\n\tpublic BoolArrayMatcher(List<Integer> intValueList) {\n\t\tAssert.isTrue(CollUtil.isNotEmpty(intValueList), \"Values must be not empty!\");\n\t\tbValues = new boolean[Collections.max(intValueList) + 1];\n\t\tint min = Integer.MAX_VALUE;\n\t\tint max = 0;\n\t\tfor (Integer value : intValueList) {\n\t\t\tmin = Math.min(min, value);\n\t\t\tmax = Math.max(max, value);\n\t\t\tbValues[value] = true;\n\t\t}\n\t\tthis.minValue = min;\n\t\tthis.maxValue = max;\n\t}\n\n\t@Override\n\tpublic boolean match(Integer value) {\n\t\tif(null != value && value >= minValue && value <= maxValue){\n\t\t\treturn bValues[value];\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int nextAfter(int value) {\n\t\tfinal int maxValue = this.maxValue;\n\t\tif(value == maxValue){\n\t\t\treturn value;\n\t\t}\n\t\tfinal int minValue = this.minValue;\n\t\tif(value > minValue && value < maxValue){\n\t\t\tfinal boolean[] bValues = this.bValues;\n\t\t\t// 最大值永远小于数组长度，只需判断最大值边界\n\t\t\twhile(value <= maxValue){\n\t\t\t\tif(value == maxValue || bValues[value]){\n\t\t\t\t\t// 达到最大值或达到第一个匹配值\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t\tvalue++;\n\t\t\t}\n\t\t}\n\n\t\t// 两种情况返回最小值\n\t\t// 一是给定值小于最小值，那下一个匹配值就是最小值\n\t\t// 二是给定值大于最大值，那下一个匹配值也是下一轮的最小值\n\t\treturn minValue;\n\t}\n\n\t/**\n\t * 获取表达式定义的最小值\n\t *\n\t * @return 最小值\n\t */\n\tpublic int getMinValue() {\n\t\treturn this.minValue;\n\t}\n\n\t/**\n\t * 获取表达式定义的最大值\n\t *\n\t * @return 最大值\n\t * @since 5.8.41\n\t */\n\tpublic int getMaxValue() {\n\t\treturn this.maxValue;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.format(\"Matcher:{}\", new Object[]{this.bValues});\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport cn.hutool.core.date.Month;\n\nimport java.util.List;\n\n/**\n * 每月第几天匹配<br>\n * 考虑每月的天数不同，且存在闰年情况，日匹配单独使用\n *\n * @author Looly\n */\npublic class DayOfMonthMatcher extends BoolArrayMatcher {\n\n\t/**\n\t * 最后一天\n\t */\n\tprivate static final int LAST_DAY = 32;\n\n\t/**\n\t * 构造\n\t *\n\t * @param intValueList 匹配的日值\n\t */\n\tpublic DayOfMonthMatcher(List<Integer> intValueList) {\n\t\tsuper(intValueList);\n\t}\n\n\t/**\n\t * 给定的日期是否匹配当前匹配器\n\t *\n\t * @param dayValue   被检查的值，此处为日\n\t * @param month      实际的月份，从1开始\n\t * @param isLeapYear 是否闰年\n\t * @return 是否匹配\n\t */\n\tpublic boolean match(int dayValue, int month, boolean isLeapYear) {\n\t\treturn (super.match(dayValue) // 在约定日范围内的某一天\n\t\t\t//匹配器中用户定义了最后一天（32表示最后一天）\n\t\t\t|| matchLastDay(dayValue, month, isLeapYear));\n\t}\n\n\t/**\n\t * 获取指定日之后的匹配值，也可以是其本身<br>\n\t * 如果表达式中存在最后一天（如使用\"L\"），则：\n\t * <ul>\n\t *     <li>4月、6月、9月、11月最多匹配到30日</li>\n\t *     <li>4月闰年匹配到29日，非闰年28日</li>\n\t * </ul>\n\t *\n\t * @param dayValue   指定的天值\n\t * @param month      月份，从1开始\n\t * @param isLeapYear 是否为闰年\n\t * @return 匹配到的值或之后的值\n\t * @since 5.8.41\n\t */\n\tpublic int nextAfter(int dayValue, final int month, final boolean isLeapYear) {\n\t\tfinal int maxValue = getMaxValue(month, isLeapYear);\n\t\tfinal int minValue = getMinValue(month, isLeapYear);\n\t\tif (dayValue > minValue) {\n\t\t\tfinal boolean[] bValues = this.bValues;\n\t\t\t// 最大值永远小于数组长度，只需判断最大值边界\n\t\t\twhile (dayValue <= maxValue) {\n\t\t\t\t// 匹配到有效值\n\t\t\t\tif (bValues[dayValue] ||\n\t\t\t\t\t// 如果最大值不在有效值中，这个最大值表示最后一天，则在包含了最后一天的情况下返回最后一天\n\t\t\t\t\t(dayValue == maxValue && match(LAST_DAY))) {\n\t\t\t\t\treturn dayValue;\n\t\t\t\t}\n\t\t\t\tdayValue++;\n\t\t\t}\n\t\t}\n\n\t\t// 两种情况返回最小值\n\t\t// 一是给定值小于最小值，那下一个匹配值就是最小值\n\t\t// 二是给定值大于最大值，那下一个匹配值也是下一轮的最小值\n\t\treturn minValue;\n\t}\n\n\t/**\n\t * 是否包含最后一天\n\t *\n\t * @return 包含最后一天\n\t */\n\tpublic boolean isLast() {\n\t\treturn match(32);\n\t}\n\n\t/**\n\t * 检查value是这个月的最后一天\n\t *\n\t * @param value 被检查的值\n\t * @param month 月份，从1开始\n\t * @param isLeapYear 是否闰年\n\t * @return 是否是这个月的最后\n\t */\n\tpublic boolean isLastDay(Integer value, Integer month, boolean isLeapYear) {\n\t\treturn matchLastDay(value, month, isLeapYear);\n\t}\n\n\t/**\n\t * 获取表达式定义中指定月的最小日的值\n\t *\n\t * @param month      月，base1\n\t * @param isLeapYear 是否闰年\n\t * @return 匹配的最小值\n\t * @since 5.8.41\n\t */\n\tpublic int getMinValue(final int month, final boolean isLeapYear) {\n\t\tfinal int minValue = super.getMinValue();\n\t\tif (LAST_DAY == minValue) {\n\t\t\t// 用户指定了 L 等表示最后一天\n\t\t\treturn getLastDay(month, isLeapYear);\n\t\t}\n\t\treturn minValue;\n\t}\n\n\t/**\n\t * 获取表达式定义中指定月的最大日的值<br>\n\t * 首先获取表达式定义的最大值，如果这个值大于本月最后一天，则返回最后一天，否则返回用户定义的最大值<br>\n\t * 注意最后一天可能不是表达式中定义的有效值\n\t *\n\t * @param month      月，base1\n\t * @param isLeapYear 是否闰年\n\t * @return 匹配的最大值\n\t * @since 5.8.41\n\t */\n\tpublic int getMaxValue(final int month, final boolean isLeapYear) {\n\t\treturn Math.min(super.getMaxValue(), getLastDay(month, isLeapYear));\n\t}\n\n\t/**\n\t * 是否匹配本月最后一天，规则如下：\n\t * <pre>\n\t * 1、闰年2月匹配是否为29\n\t * 2、其它月份是否匹配最后一天的日期（可能为30或者31）\n\t * 3、表达式包含最后一天（使用31表示）\n\t * </pre>\n\t *\n\t * @param dayValue   被检查的值\n\t * @param month      月，base1\n\t * @param isLeapYear 是否闰年\n\t * @return 是否为本月最后一天\n\t */\n\tprivate boolean matchLastDay(final int dayValue, final int month, final boolean isLeapYear) {\n\t\treturn dayValue > 27\n\t\t\t// 表达式中定义包含了最后一天\n\t\t\t&& match(LAST_DAY)\n\t\t\t// 用户指定的日正好是最后一天\n\t\t\t&& dayValue == getLastDay(month, isLeapYear);\n\t}\n\n\t/**\n\t * 获取最后一天\n\t *\n\t * @param month      月，base1\n\t * @param isLeapYear 是否闰年\n\t * @return 最后一天\n\t */\n\tprivate static int getLastDay(final int month, final boolean isLeapYear) {\n\t\treturn Month.getLastDay(month - 1, isLeapYear);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PartMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport cn.hutool.core.lang.Matcher;\n\n/**\n * 表达式中的某个位置部分匹配器<br>\n * 用于匹配日期位中对应数字是否匹配\n *\n * @author Looly\n */\npublic interface PartMatcher extends Matcher<Integer> {\n\n\t/**\n\t * 获取指定值之后的匹配值，也可以是指定值本身\n\t *\n\t * @param value 指定的值\n\t * @return 匹配到的值或之后的值\n\t */\n\tint nextAfter(int value);\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PatternMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.Month;\nimport cn.hutool.cron.pattern.Part;\n\nimport java.time.Year;\nimport java.util.Calendar;\nimport java.util.Objects;\nimport java.util.TimeZone;\n\n/**\n * 单一表达式的匹配器，匹配器由7个{@link PartMatcher}组成，分别是：\n * <pre>\n *         0      1     2        3         4       5        6\n *      SECOND MINUTE HOUR DAY_OF_MONTH MONTH DAY_OF_WEEK YEAR\n * </pre>\n *\n * @author looly\n * @since 5.8.0\n */\npublic class PatternMatcher {\n\n\tprivate final PartMatcher[] matchers;\n\n\t/**\n\t * 构造\n\t *\n\t * @param secondMatcher     秒匹配器\n\t * @param minuteMatcher     分匹配器\n\t * @param hourMatcher       时匹配器\n\t * @param dayOfMonthMatcher 日匹配器\n\t * @param monthMatcher      月匹配器\n\t * @param dayOfWeekMatcher  周匹配器\n\t * @param yearMatcher       年匹配器\n\t */\n\tpublic PatternMatcher(PartMatcher secondMatcher,\n\t\t\t\t\t\t  PartMatcher minuteMatcher,\n\t\t\t\t\t\t  PartMatcher hourMatcher,\n\t\t\t\t\t\t  PartMatcher dayOfMonthMatcher,\n\t\t\t\t\t\t  PartMatcher monthMatcher,\n\t\t\t\t\t\t  PartMatcher dayOfWeekMatcher,\n\t\t\t\t\t\t  PartMatcher yearMatcher) {\n\n\t\tmatchers = new PartMatcher[]{\n\t\t\t\tsecondMatcher,\n\t\t\t\tminuteMatcher,\n\t\t\t\thourMatcher,\n\t\t\t\tdayOfMonthMatcher,\n\t\t\t\tmonthMatcher,\n\t\t\t\tdayOfWeekMatcher,\n\t\t\t\tyearMatcher\n\t\t};\n\t}\n\n\t/**\n\t * 根据表达式位置，获取对应的{@link PartMatcher}\n\t *\n\t * @param part 表达式位置\n\t * @return {@link PartMatcher}\n\t */\n\tpublic PartMatcher get(Part part) {\n\t\treturn matchers[part.ordinal()];\n\t}\n\n\t//region match\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param fields 时间字段值，{second, minute, hour, dayOfMonth, month, dayOfWeek, year}\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tpublic boolean match(int[] fields) {\n\t\treturn match(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6]);\n\t}\n\n\t/**\n\t * 给定周的值是否匹配定时任务表达式对应部分\n\t *\n\t * @param dayOfWeekValue dayOfMonth值，星期从0开始，0和7都表示周日\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t * @since 5.8.0\n\t */\n\tpublic boolean matchWeek(int dayOfWeekValue) {\n\t\treturn matchers[5].match(dayOfWeekValue);\n\t}\n\n\t/**\n\t * 给定时间是否匹配定时任务表达式\n\t *\n\t * @param second     秒数，-1表示不匹配此项\n\t * @param minute     分钟\n\t * @param hour       小时\n\t * @param dayOfMonth 天\n\t * @param month      月，从1开始\n\t * @param dayOfWeek  周，从0开始，0和7都表示周日\n\t * @param year       年\n\t * @return 如果匹配返回 {@code true}, 否则返回 {@code false}\n\t */\n\tprivate boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {\n\t\treturn ((second < 0) || matchers[0].match(second)) // 匹配秒（非秒匹配模式下始终返回true）\n\t\t\t\t&& matchers[1].match(minute)// 匹配分\n\t\t\t\t&& matchers[2].match(hour)// 匹配时\n\t\t\t\t&& matchDayOfMonth(matchers[3], dayOfMonth, month, Year.isLeap(year))// 匹配日\n\t\t\t\t&& matchers[4].match(month) // 匹配月\n\t\t\t\t&& matchers[5].match(dayOfWeek)// 匹配周\n\t\t\t\t&& matchers[6].match(year);// 匹配年\n\t}\n\n\t/**\n\t * 是否匹配日（指定月份的第几天）\n\t *\n\t * @param matcher    {@link PartMatcher}\n\t * @param dayOfMonth 日\n\t * @param month      月\n\t * @param isLeapYear 是否闰年\n\t * @return 是否匹配\n\t */\n\tprivate static boolean matchDayOfMonth(PartMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) {\n\t\treturn ((matcher instanceof DayOfMonthMatcher) //\n\t\t\t\t? ((DayOfMonthMatcher) matcher).match(dayOfMonth, month, isLeapYear) //\n\t\t\t\t: matcher.match(dayOfMonth));\n\t}\n\t//endregion\n\n\t//region nextMatchAfter\n\n\t/**\n\t * 获取下一个匹配日期时间<br>\n\t * 获取方法是，先从年开始查找对应部分的下一个值：\n\t * <ul>\n\t *     <li>如果此部分下个值不变，获取下一个部分</li>\n\t *     <li>如果此部分下个值大于给定值，以下所有值置为最小值</li>\n\t *     <li>如果此部分下个值小于给定值，回退到上一个值获取下一个新值，之后的值置为最小值</li>\n\t * </ul>\n\t *\n\t * <pre>\n\t *        秒 分 时 日 月 周 年\n\t *     下 &lt;-----------------&gt; 上\n\t * </pre>\n\t *\n\t * @param values 时间字段值，{second, minute, hour, dayOfMonth, month, dayOfWeek, year}\n\t * @param zone   时区\n\t * @return {@link Calendar}，毫秒数为0\n\t */\n\tpublic Calendar nextMatchAfter(int[] values, TimeZone zone) {\n\t\tfinal Calendar calendar = Calendar.getInstance(zone);\n\t\tcalendar.set(Calendar.MILLISECOND, 0);\n\n\t\tfinal int[] newValues = nextMatchValuesAfter(values);\n\t\tfor (int i = 0; i < newValues.length; i++) {\n\t\t\t// 周无需设置\n\t\t\tif (i != Part.DAY_OF_WEEK.ordinal()) {\n\t\t\t\tsetValue(calendar, Part.of(i), newValues[i]);\n\t\t\t}\n\t\t}\n\n\t\treturn calendar;\n\t}\n\n\t/**\n\t * 获取下一个匹配日期时间<br>\n\t * 获取方法是，先从年开始查找对应部分的下一个值：\n\t * <ul>\n\t *     <li>如果此部分下个值不变，获取下一个部分</li>\n\t *     <li>如果此部分下个值大于给定值，以下所有值置为最小值</li>\n\t *     <li>如果此部分下个值小于给定值，回退到上一个值获取下一个新值，之后的值置为最小值</li>\n\t * </ul>\n\t *\n\t * <pre>\n\t *        秒 分 时 日 月 周 年\n\t *     下 &lt;-----------------&gt; 上\n\t * </pre>\n\t *\n\t * @param values 时间字段值，{second, minute, hour, dayOfMonth, month, dayOfWeek, year}\n\t * @return {@link Calendar}，毫秒数为0\n\t */\n\tprivate int[] nextMatchValuesAfter(int[] values) {\n\t\tint i = Part.YEAR.ordinal();\n\t\t// 新值，-1表示标识为回退\n\t\tint nextValue = 0;\n\t\twhile (i >= 0) {\n\t\t\tif (i == Part.DAY_OF_WEEK.ordinal()) {\n\t\t\t\t// 周不参与计算\n\t\t\t\ti--;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tnextValue = getNextMatch(values, i, 0);\n\n\t\t\tif (nextValue > values[i]) {\n\t\t\t\t// 此部分正常获取新值，结束循环，后续的部分置最小值\n\t\t\t\tvalues[i] = nextValue;\n\t\t\t\ti--;\n\t\t\t\tbreak;\n\t\t\t} else if (nextValue < values[i]) {\n\t\t\t\t// 此部分下一个值获取到的值产生回退，回到上一个部分，继续获取新值\n\t\t\t\ti++;\n\t\t\t\tnextValue = -1;// 标记回退查找\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// 值不变，检查下一个部分\n\t\t\ti--;\n\t\t}\n\n\t\t// 值产生回退，向上查找变更值\n\t\tif (-1 == nextValue) {\n\t\t\twhile (i <= Part.YEAR.ordinal()) {\n\t\t\t\tif (i == Part.DAY_OF_WEEK.ordinal()) {\n\t\t\t\t\t// 周不参与计算\n\t\t\t\t\ti++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tnextValue = getNextMatch(values, i, 1);\n\n\t\t\t\tif (nextValue > values[i]) {\n\t\t\t\t\tvalues[i] = nextValue;\n\t\t\t\t\ti--;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t}\n\t\t}\n\n\t\t// 修改值以下的字段全部归最小值\n\t\tsetToMin(values, i);\n\t\treturn values;\n\t}\n\n\t/**\n\t * 获取指定部分的下一个匹配值，三种结果：\n\t * <ul>\n\t *     <li>结果值大于原值：此部分已更新，后续部分取匹配的最小值。</li>\n\t *     <li>结果值小于原值：此部分获取到了最小值，上一个部分需要继续取下一个值。</li>\n\t *     <li>结果值等于原值：此部分匹配，获取下一个部分的next值</li>\n\t * </ul>\n\t *\n\t * @param newValues   时间字段值，{second, minute, hour, dayOfMonth, monthBase1, dayOfWeekBase0, year}\n\t * @param partOrdinal 序号\n\t * @param plusValue   获取的偏移值\n\t * @return 下一个值\n\t */\n\tprivate int getNextMatch(final int[] newValues, final int partOrdinal, final int plusValue) {\n\t\tif (partOrdinal == Part.DAY_OF_MONTH.ordinal() && matchers[partOrdinal] instanceof DayOfMonthMatcher) {\n\t\t\t// 对于日需要考虑月份和闰年，单独处理\n\t\t\tfinal boolean isLeapYear = DateUtil.isLeapYear(newValues[Part.YEAR.ordinal()]);\n\t\t\tfinal int month = newValues[Part.MONTH.ordinal()];\n\t\t\treturn ((DayOfMonthMatcher) matchers[partOrdinal]).nextAfter(newValues[partOrdinal] + plusValue, month, isLeapYear);\n\t\t}\n\n\t\treturn matchers[partOrdinal].nextAfter(newValues[partOrdinal] + plusValue);\n\t}\n\n\t/**\n\t * 设置从{@link Part#SECOND}到指定部分，全部设置为最小值\n\t *\n\t * @param values 值数组\n\t * @param toPart 截止的部分\n\t */\n\tprivate void setToMin(final int[] values, final int toPart) {\n\t\tPart part;\n\t\tfor (int i = toPart; i >= 0; i--) {\n\t\t\tpart = Part.of(i);\n\t\t\tif (part == Part.DAY_OF_MONTH) {\n\t\t\t\tfinal boolean isLeapYear = DateUtil.isLeapYear(values[Part.YEAR.ordinal()]);\n\t\t\t\tfinal int month = values[Part.MONTH.ordinal()];\n\t\t\t\tfinal PartMatcher partMatcher = get(part);\n\t\t\t\tif (partMatcher instanceof DayOfMonthMatcher) {\n\t\t\t\t\tvalues[i] = ((DayOfMonthMatcher) partMatcher).getMinValue(month, isLeapYear);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvalues[i] = getMin(part);\n\t\t}\n\t}\n\n\t/**\n\t * 获取表达式部分的最小值\n\t *\n\t * @param part {@link Part}\n\t * @return 最小值，如果匹配所有，返回对应部分范围的最小值\n\t */\n\tprivate int getMin(Part part) {\n\t\tPartMatcher matcher = get(part);\n\n\t\tint min;\n\t\tif (matcher instanceof AlwaysTrueMatcher) {\n\t\t\tmin = part.getMin();\n\t\t} else if (matcher instanceof BoolArrayMatcher) {\n\t\t\tmin = ((BoolArrayMatcher) matcher).getMinValue();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"Invalid matcher: \" + matcher.getClass().getName());\n\t\t}\n\t\treturn min;\n\t}\n\t//endregion\n\n\t/**\n\t * 设置对应部分修正后的值<br>\n\t * <ul>\n\t *     <li>月在表达式中从1开始，但是{@link Calendar}中是从0开始的，需要-1</li>\n\t *     <li>周在表达式中从0开始（0表示周日），但是{@link Calendar}中是从1开始的（1表示周日），需要+1</li>\n\t * </ul>\n\t *\n\t * @param calendar {@link Calendar}\n\t * @param part     表达式部分\n\t * @param value    值\n\t * @return {@link Calendar}\n\t */\n\tprivate Calendar setValue(Calendar calendar, Part part, int value) {\n\t\tswitch (part) {\n\t\t\tcase MONTH:\n\t\t\t\tvalue -= 1;\n\t\t\t\tbreak;\n\t\t\tcase DAY_OF_WEEK:\n\t\t\t\tvalue += 1;\n\t\t\t\tbreak;\n\t\t}\n\t\t//noinspection MagicConstant\n\t\tcalendar.set(part.getCalendarField(), value);\n\t\t//Console.log(\"Set [{}] as [{}]\", part, value);\n\t\treturn calendar;\n\t}\n\n\tprivate static int getLastDay(int monthBase1, int year){\n\t\treturn Objects.requireNonNull(Month.of(monthBase1 - 1))\n\t\t\t.getLastDay(DateUtil.isLeapYear(year));\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/YearValueMatcher.java",
    "content": "package cn.hutool.cron.pattern.matcher;\n\nimport java.util.Collection;\nimport java.util.LinkedHashSet;\n\n/**\n * 年匹配<br>\n * 考虑年数字太大，不适合boolean数组，单独使用{@link LinkedHashSet}匹配\n *\n * @author Looly\n */\npublic class YearValueMatcher implements PartMatcher {\n\n\tprivate final LinkedHashSet<Integer> valueList;\n\n\tpublic YearValueMatcher(Collection<Integer> intValueList) {\n\t\tthis.valueList = new LinkedHashSet<>(intValueList);\n\t}\n\n\t@Override\n\tpublic boolean match(Integer t) {\n\t\treturn valueList.contains(t);\n\t}\n\n\t@Override\n\tpublic int nextAfter(int value) {\n\t\tfor (Integer year : valueList) {\n\t\t\tif (year >= value) {\n\t\t\t\treturn year;\n\t\t\t}\n\t\t}\n\n\t\t// 年无效，此表达式整体无效\n\t\treturn -1;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/package-info.java",
    "content": "/**\n * 定时任务表达式匹配器，内部使用<br>\n * 单一表达式使用{@link cn.hutool.cron.pattern.matcher.PatternMatcher}表示<br>\n * {@link cn.hutool.cron.pattern.matcher.PatternMatcher}由7个{@link cn.hutool.cron.pattern.matcher.PartMatcher}组成，\n * 分别表示定时任务表达式中的7个位置:\n * <pre>\n *         0      1     2        3         4       5        6\n *      SECOND MINUTE HOUR DAY_OF_MONTH MONTH DAY_OF_WEEK YEAR\n * </pre>\n *\n * @author looly\n *\n */\npackage cn.hutool.cron.pattern.matcher;\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/package-info.java",
    "content": "/**\n * 定时任务表达式解析，核心为CronPattern\n * \n * @author looly\n *\n */\npackage cn.hutool.cron.pattern;"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java",
    "content": "package cn.hutool.cron.pattern.parser;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.Month;\nimport cn.hutool.core.date.Week;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.cron.CronException;\nimport cn.hutool.cron.pattern.Part;\nimport cn.hutool.cron.pattern.matcher.*;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 定时任务表达式各个部分的解析器，根据{@link Part}指定不同部分，解析为{@link PartMatcher}<br>\n * 每个部分支持：\n * <ul>\n *   <li><strong>*</strong> ：表示匹配这个位置所有的时间</li>\n *   <li><strong>?</strong> ：表示匹配这个位置任意的时间（与\"*\"作用一致）</li>\n *   <li><strong>L</strong> ：表示匹配这个位置允许的最大值</li>\n *   <li><strong>*&#47;2</strong> ：表示间隔时间，例如在分上，表示每两分钟，同样*可以使用数字列表代替，逗号分隔</li>\n *   <li><strong>2-8</strong> ：表示连续区间，例如在分上，表示2,3,4,5,6,7,8分</li>\n *   <li><strong>2,3,5,8</strong> ：表示列表</li>\n *   <li><strong>wed</strong> ：表示周别名</li>\n *   <li><strong>jan</strong> ：表示月别名</li>\n * </ul>\n *\n * @author looly\n * @since 5.8.0\n */\npublic class PartParser {\n\n\tprivate final Part part;\n\n\t/**\n\t * 创建解析器\n\t *\n\t * @param part 对应解析的部分枚举\n\t * @return 解析器\n\t */\n\tpublic static PartParser of(Part part) {\n\t\treturn new PartParser(part);\n\t}\n\n\t/**\n\t * 构造\n\t * @param part 对应解析的部分枚举\n\t */\n\tpublic PartParser(Part part) {\n\t\tthis.part = part;\n\t}\n\n\t/**\n\t * 将表达式解析为{@link PartMatcher}<br>\n\t * <ul>\n\t *     <li>* 或者 ? 返回{@link AlwaysTrueMatcher}</li>\n\t *     <li>{@link Part#DAY_OF_MONTH} 返回{@link DayOfMonthMatcher}</li>\n\t *     <li>{@link Part#YEAR} 返回{@link YearValueMatcher}</li>\n\t *     <li>其他 返回{@link BoolArrayMatcher}</li>\n\t * </ul>\n\t *\n\t * @param value 表达式\n\t * @return {@link PartMatcher}\n\t */\n\tpublic PartMatcher parse(String value) {\n\t\t// 是否是查询最后一天\n\t\tif (isMatchAllStr(value)) {\n\t\t\t//兼容Quartz的\"?\"表达式，不会出现互斥情况，与\"*\"作用相同\n\t\t\treturn new AlwaysTrueMatcher();\n\t\t}\n\n\t\tfinal List<Integer> values = parseArray(value);\n\t\tif (values.isEmpty()) {\n\t\t\tthrow new CronException(\"Invalid part value: [{}]\", value);\n\t\t}\n\n\t\tswitch (this.part) {\n\t\t\tcase DAY_OF_MONTH:\n\t\t\t\treturn new DayOfMonthMatcher(values);\n\t\t\tcase YEAR:\n\t\t\t\treturn new YearValueMatcher(values);\n\t\t\tdefault:\n\t\t\t\treturn new BoolArrayMatcher(values);\n\t\t}\n\t}\n\n\t/**\n\t * 处理数组形式表达式<br>\n\t * 处理的形式包括：\n\t * <ul>\n\t * <li><strong>a</strong> 或 <strong>*</strong></li>\n\t * <li><strong>a,b,c,d</strong></li>\n\t * </ul>\n\t *\n\t * @param value 子表达式值\n\t * @return 值列表\n\t */\n\tprivate List<Integer> parseArray(String value) {\n\t\tfinal List<Integer> values = new ArrayList<>();\n\n\t\tfinal List<String> parts = StrUtil.split(value, StrUtil.C_COMMA);\n\t\tfor (String part : parts) {\n\t\t\tCollUtil.addAllIfNotContains(values, parseStep(part));\n\t\t}\n\t\treturn values;\n\t}\n\n\t/**\n\t * 处理间隔形式的表达式<br>\n\t * 处理的形式包括：\n\t * <ul>\n\t * <li><strong>a</strong> 或 <strong>*</strong></li>\n\t * <li><strong>a&#47;b</strong> 或 <strong>*&#47;b</strong></li>\n\t * <li><strong>a-b/2</strong></li>\n\t * </ul>\n\t *\n\t * @param value 表达式值\n\t * @return List\n\t */\n\tprivate List<Integer> parseStep(String value) {\n\t\tfinal List<String> parts = StrUtil.split(value, StrUtil.C_SLASH);\n\t\tint size = parts.size();\n\n\t\tList<Integer> results;\n\t\tif (size == 1) {// 普通形式\n\t\t\tresults = parseRange(value, -1);\n\t\t} else if (size == 2) {// 间隔形式\n\t\t\t// issue#I7SMP7，步进不检查范围\n\t\t\tfinal int step = parseNumber(parts.get(1), false);\n\t\t\tif (step < 1) {\n\t\t\t\tthrow new CronException(\"Non positive divisor for field: [{}]\", value);\n\t\t\t}\n\t\t\tresults = parseRange(parts.get(0), step);\n\t\t} else {\n\t\t\tthrow new CronException(\"Invalid syntax of field: [{}]\", value);\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * 处理表达式中范围表达式 处理的形式包括：\n\t * <ul>\n\t * <li>*</li>\n\t * <li>2</li>\n\t * <li>3-8</li>\n\t * <li>8-3</li>\n\t * <li>3-3</li>\n\t * </ul>\n\t *\n\t * @param value 范围表达式\n\t * @param step  步进\n\t * @return List\n\t */\n\tprivate List<Integer> parseRange(String value, int step) {\n\t\tfinal List<Integer> results = new ArrayList<>();\n\n\t\t// 全部匹配形式\n\t\tif (value.length() <= 2) {\n\t\t\t//根据步进的第一个数字确定起始时间，类似于 12/3则从12（秒、分等）开始\n\t\t\tint minValue = part.getMin();\n\t\t\tif (false == isMatchAllStr(value)) {\n\t\t\t\tminValue = Math.max(minValue, parseNumber(value, true));\n\t\t\t} else {\n\t\t\t\t//在全匹配模式下，如果步进不存在，表示步进为1\n\t\t\t\tif (step < 1) {\n\t\t\t\t\tstep = 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (step > 0) {\n\t\t\t\tfinal int maxValue = part.getMax();\n\t\t\t\tif (minValue > maxValue) {\n\t\t\t\t\tthrow new CronException(\"Invalid value {} > {}\", minValue, maxValue);\n\t\t\t\t}\n\t\t\t\t//有步进\n\t\t\t\tfor (int i = minValue; i <= maxValue; i += step) {\n\t\t\t\t\tresults.add(i);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t//固定时间\n\t\t\t\tresults.add(minValue);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\n\t\t//Range模式\n\t\tList<String> parts = StrUtil.split(value, '-');\n\t\tint size = parts.size();\n\t\tif (size == 1) {// 普通值\n\t\t\tfinal int v1 = parseNumber(value, true);\n\t\t\tif (step > 0) {//类似 20/2的形式\n\t\t\t\tNumberUtil.appendRange(v1, part.getMax(), step, results);\n\t\t\t} else {\n\t\t\t\tresults.add(v1);\n\t\t\t}\n\t\t} else if (size == 2) {// range值\n\t\t\tfinal int v1 = parseNumber(parts.get(0), true);\n\t\t\tfinal int v2 = parseNumber(parts.get(1), true);\n\t\t\tif (step < 1) {\n\t\t\t\t//在range模式下，如果步进不存在，表示步进为1\n\t\t\t\tstep = 1;\n\t\t\t}\n\t\t\tif (v1 <= v2) {// 正常范围，例如：2-5，3-3\n\t\t\t\tNumberUtil.appendRange(v1, v2, step, results);\n\t\t\t} else {// 逆向范围，反选模式，例如：5-2\n\t\t\t\tNumberUtil.appendRange(v1, part.getMax(), step, results);\n\t\t\t\tNumberUtil.appendRange(part.getMin(), v2, step, results);\n\t\t\t}\n\t\t} else {\n\t\t\tthrow new CronException(\"Invalid syntax of field: [{}]\", value);\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * 是否为全匹配符<br>\n\t * 全匹配符指 * 或者 ?\n\t *\n\t * @param value 被检查的值\n\t * @return 是否为全匹配符\n\t * @since 4.1.18\n\t */\n\tprivate static boolean isMatchAllStr(String value) {\n\t\treturn (1 == value.length()) && (\"*\".equals(value) || \"?\".equals(value));\n\t}\n\n\t/**\n\t * 解析单个int值，支持别名\n\t *\n\t * @param value      被解析的值\n\t * @param checkValue 是否检查值在有效范围内\n\t * @return 解析结果\n\t * @throws CronException 当无效数字或无效别名时抛出\n\t */\n\tprivate int parseNumber(String value, boolean checkValue) throws CronException {\n\t\tint i;\n\t\ttry {\n\t\t\ti = Integer.parseInt(value);\n\t\t} catch (NumberFormatException ignore) {\n\t\t\ti = parseAlias(value);\n\t\t}\n\n\t\t// 支持负数\n\t\tif(i < 0){\n\t\t\ti += part.getMax();\n\t\t}\n\n\t\t// 周日可以用0或7表示，统一转换为0\n\t\tif(Part.DAY_OF_WEEK.equals(this.part) && Week.SUNDAY.getIso8601Value() == i){\n\t\t\ti = Week.SUNDAY.ordinal();\n\t\t}\n\n\t\treturn checkValue ? part.checkValue(i) : i;\n\t}\n\n\t/**\n\t * 解析别名支持包括：<br>\n\t * <ul>\n\t *     <li><strong>L 表示最大值</strong></li>\n\t *     <li>{@link Part#MONTH}和{@link Part#DAY_OF_WEEK}别名</li>\n\t * </ul>\n\t *\n\t * @param name 别名\n\t * @return 解析int值\n\t * @throws CronException 无匹配别名时抛出异常\n\t */\n\tprivate int parseAlias(String name) throws CronException {\n\t\tif (\"L\".equalsIgnoreCase(name)) {\n\t\t\t// L表示最大值\n\t\t\treturn part.getMax();\n\t\t}\n\n\t\tswitch (this.part) {\n\t\t\tcase MONTH:\n\t\t\t\t// 月份从1开始\n\t\t\t\treturn Month.of(name).getValueBaseOne();\n\t\t\tcase DAY_OF_WEEK:\n\t\t\t\t// 周从0开始，0表示周日\n\t\t\t\treturn Week.of(name).ordinal();\n\t\t}\n\n\t\tthrow new CronException(\"Invalid alias value: [{}]\", name);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PatternParser.java",
    "content": "package cn.hutool.cron.pattern.parser;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.cron.CronException;\nimport cn.hutool.cron.pattern.Part;\nimport cn.hutool.cron.pattern.matcher.AlwaysTrueMatcher;\nimport cn.hutool.cron.pattern.matcher.PartMatcher;\nimport cn.hutool.cron.pattern.matcher.PatternMatcher;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 定时任务表达式解析器，用于将表达式字符串解析为{@link PatternMatcher}的列表\n *\n * @author looly\n * @since 5.8.0\n */\npublic class PatternParser {\n\n\tprivate static final PartParser SECOND_VALUE_PARSER = PartParser.of(Part.SECOND);\n\tprivate static final PartParser MINUTE_VALUE_PARSER = PartParser.of(Part.MINUTE);\n\tprivate static final PartParser HOUR_VALUE_PARSER = PartParser.of(Part.HOUR);\n\tprivate static final PartParser DAY_OF_MONTH_VALUE_PARSER = PartParser.of(Part.DAY_OF_MONTH);\n\tprivate static final PartParser MONTH_VALUE_PARSER = PartParser.of(Part.MONTH);\n\tprivate static final PartParser DAY_OF_WEEK_VALUE_PARSER = PartParser.of(Part.DAY_OF_WEEK);\n\tprivate static final PartParser YEAR_VALUE_PARSER = PartParser.of(Part.YEAR);\n\n\t/**\n\t * 解析表达式到匹配列表中\n\t *\n\t * @param cronPattern 复合表达式\n\t * @return {@link List}\n\t */\n\tpublic static List<PatternMatcher> parse(String cronPattern) {\n\t\treturn parseGroupPattern(cronPattern);\n\t}\n\n\t/**\n\t * 解析复合任务表达式，格式为：\n\t * <pre>\n\t *     cronA | cronB | ...\n\t * </pre>\n\t *\n\t * @param groupPattern 复合表达式\n\t * @return {@link List}\n\t */\n\tprivate static List<PatternMatcher> parseGroupPattern(String groupPattern) {\n\t\tfinal List<String> patternList = StrUtil.splitTrim(groupPattern, '|');\n\t\tfinal List<PatternMatcher> patternMatchers = new ArrayList<>(patternList.size());\n\t\tfor (String pattern : patternList) {\n\t\t\tpatternMatchers.add(parseSingle(pattern));\n\t\t}\n\t\treturn patternMatchers;\n\t}\n\n\t/**\n\t * 解析单一定时任务表达式\n\t *\n\t * @param pattern 表达式\n\t * @return {@link PatternMatcher}\n\t */\n\tprivate static PatternMatcher parseSingle(String pattern) {\n\t\tfinal String[] parts = pattern.split(\"\\\\s+\");\n\t\tAssert.checkBetween(parts.length, 5, 7,\n\t\t\t\t() -> new CronException(\"Pattern [{}] is invalid, it must be 5-7 parts!\", pattern));\n\n\t\t// 偏移量用于兼容Quartz表达式，当表达式有6或7项时，第一项为秒\n\t\tint offset = 0;\n\t\tif (parts.length == 6 || parts.length == 7) {\n\t\t\toffset = 1;\n\t\t}\n\n\t\t// 秒，如果不支持秒的表达式，则第一位赋值0，表示整分匹配\n\t\tfinal String secondPart = (1 == offset) ? parts[0] : \"0\";\n\n\t\t// 年\n\t\tPartMatcher yearMatcher;\n\t\tif (parts.length == 7) {// 支持年的表达式\n\t\t\tyearMatcher = YEAR_VALUE_PARSER.parse(parts[6]);\n\t\t} else {// 不支持年的表达式，全部匹配\n\t\t\tyearMatcher = AlwaysTrueMatcher.INSTANCE;\n\t\t}\n\n\t\treturn new PatternMatcher(\n\t\t\t\t// 秒\n\t\t\t\tSECOND_VALUE_PARSER.parse(secondPart),\n\t\t\t\t// 分\n\t\t\t\tMINUTE_VALUE_PARSER.parse(parts[offset]),\n\t\t\t\t// 时\n\t\t\t\tHOUR_VALUE_PARSER.parse(parts[1 + offset]),\n\t\t\t\t// 天\n\t\t\t\tDAY_OF_MONTH_VALUE_PARSER.parse(parts[2 + offset]),\n\t\t\t\t// 月\n\t\t\t\tMONTH_VALUE_PARSER.parse(parts[3 + offset]),\n\t\t\t\t// 周\n\t\t\t\tDAY_OF_WEEK_VALUE_PARSER.parse(parts[4 + offset]),\n\t\t\t\t// 年\n\t\t\t\tyearMatcher\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/package-info.java",
    "content": "/**\n * 定时任务表达式解析器，内部使用\n * \n * @author looly\n *\n */\npackage cn.hutool.cron.pattern.parser;"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/task/CronTask.java",
    "content": "package cn.hutool.cron.task;\n\nimport cn.hutool.cron.pattern.CronPattern;\n\n/**\n * 定时作业，此类除了定义了作业，也定义了作业的执行周期以及ID。\n *\n * @author looly\n * @since 5.4.7\n */\npublic class CronTask implements Task{\n\n\tprivate final String id;\n\tprivate CronPattern pattern;\n\tprivate final Task task;\n\n\t/**\n\t * 构造\n\t * @param id ID\n\t * @param pattern 表达式\n\t * @param task 作业\n\t */\n\tpublic CronTask(String id, CronPattern pattern, Task task) {\n\t\tthis.id = id;\n\t\tthis.pattern = pattern;\n\t\tthis.task = task;\n\t}\n\n\t@Override\n\tpublic void execute() {\n\t\ttask.execute();\n\t}\n\n\t/**\n\t * 获取作业ID\n\t *\n\t * @return 作业ID\n\t */\n\tpublic String getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * 获取表达式\n\t *\n\t * @return 表达式\n\t */\n\tpublic CronPattern getPattern() {\n\t\treturn pattern;\n\t}\n\n\t/**\n\t * 设置新的定时表达式\n\t * @param pattern 表达式\n\t * @return this\n\t */\n\tpublic CronTask setPattern(CronPattern pattern){\n\t\tthis.pattern = pattern;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取原始作业\n\t *\n\t * @return 作业\n\t */\n\tpublic Task getRaw(){\n\t\treturn this.task;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/task/InvokeTask.java",
    "content": "package cn.hutool.cron.task;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.cron.CronException;\n\nimport java.lang.reflect.Method;\n\n/**\n * 反射执行任务<br>\n * 通过传入类名#方法名，通过反射执行相应的方法<br>\n * 如果是静态方法直接执行，如果是对象方法，需要类有默认的构造方法。\n * \n * @author Looly\n *\n */\npublic class InvokeTask implements Task{\n\n\tprivate final Object obj;\n\tprivate final Method method;\n\t\n\t/**\n\t * 构造\n\t * @param classNameWithMethodName 类名与方法名的字符串表示，方法名和类名使用#隔开或者.隔开\n\t */\n\tpublic InvokeTask(String classNameWithMethodName) {\n\t\tint splitIndex = classNameWithMethodName.lastIndexOf('#');\n\t\tif(splitIndex <= 0){\n\t\t\tsplitIndex = classNameWithMethodName.lastIndexOf('.');\n\t\t}\n\t\tif (splitIndex <= 0) {\n\t\t\tthrow new UtilException(\"Invalid classNameWithMethodName [{}]!\", classNameWithMethodName);\n\t\t}\n\n\t\t//类\n\t\tfinal String className = classNameWithMethodName.substring(0, splitIndex);\n\t\tif(StrUtil.isBlank(className)) {\n\t\t\tthrow new IllegalArgumentException(\"Class name is blank !\");\n\t\t}\n\t\tfinal Class<?> clazz = ClassLoaderUtil.loadClass(className);\n\t\tif(null == clazz) {\n\t\t\tthrow new IllegalArgumentException(\"Load class with name of [\" + className + \"] fail !\");\n\t\t}\n\t\tthis.obj = ReflectUtil.newInstanceIfPossible(clazz);\n\t\t\n\t\t//方法\n\t\tfinal String methodName = classNameWithMethodName.substring(splitIndex + 1);\n\t\tif(StrUtil.isBlank(methodName)) {\n\t\t\tthrow new IllegalArgumentException(\"Method name is blank !\");\n\t\t}\n\t\tthis.method = ClassUtil.getPublicMethod(clazz, methodName);\n\t\tif(null == this.method) {\n\t\t\tthrow new IllegalArgumentException(\"No method with name of [\" + methodName + \"] !\");\n\t\t}\n\t}\n\n\t@Override\n\tpublic void execute() {\n\t\ttry {\n\t\t\tReflectUtil.invoke(this.obj, this.method);\n\t\t} catch (UtilException e) {\n\t\t\tthrow new CronException(e.getCause());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/task/RunnableTask.java",
    "content": "package cn.hutool.cron.task;\n\n/**\n * {@link Runnable} 的 {@link Task}包装\n * @author Looly\n *\n */\npublic class RunnableTask implements Task{\n\tprivate final Runnable runnable;\n\t\n\tpublic RunnableTask(Runnable runnable) {\n\t\tthis.runnable = runnable;\n\t}\n\n\t@Override\n\tpublic void execute() {\n\t\trunnable.run();\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/task/Task.java",
    "content": "package cn.hutool.cron.task;\n\n/**\n * 定时作业接口，通过实现execute方法执行具体的任务\n * <p>\n * 作业执行是异步执行，即不同作业、相同作业在不同时间的执行是相互独立的。<br>\n * 假如前一个作业未完成，下一个调度开始，则不会等待前一个作业，直接执行。<br>\n * 关于作业的互斥，请自行加锁完成。\n * </p>\n *\n * @author Looly\n */\n@FunctionalInterface\npublic interface Task {\n\n\t/**\n\t * 执行作业\n\t * <p>\n\t * 作业的具体实现需考虑异常情况，默认情况下任务异常在监听中统一监听处理，如果不加入监听，异常会被忽略<br>\n\t * 因此最好自行捕获异常后处理\n\t */\n\tvoid execute();\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/task/package-info.java",
    "content": "/**\n * 定时任务中作业的抽象封装和实现，包括Runnable实现和反射实现<br>\n * {@link cn.hutool.cron.task.Task}表示一个具体的任务，当满足时间匹配要求时，会执行{@link cn.hutool.cron.task.Task#execute()}方法。\n *\n * @author looly\n *\n */\npackage cn.hutool.cron.task;\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/timingwheel/SystemTimer.java",
    "content": "package cn.hutool.cron.timingwheel;\n\nimport cn.hutool.core.thread.ThreadUtil;\n\nimport java.util.concurrent.DelayQueue;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 系统计时器\n *\n * @author eliasyaoyc, looly\n */\npublic class SystemTimer {\n\t/**\n\t * 底层时间轮\n\t */\n\tprivate final TimingWheel timeWheel;\n\n\t/**\n\t * 一个Timer只有一个delayQueue\n\t */\n\tprivate final DelayQueue<TimerTaskList> delayQueue = new DelayQueue<>();\n\n\t/**\n\t * 执行队列取元素超时时长，单位毫秒，默认100\n\t */\n\tprivate long delayQueueTimeout = 100;\n\n\t/**\n\t * 轮询delayQueue获取过期任务线程\n\t */\n\tprivate ExecutorService bossThreadPool;\n\tprivate volatile boolean isRunning;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SystemTimer() {\n\t\ttimeWheel = new TimingWheel(1, 20, delayQueue::offer);\n\t}\n\n\t/**\n\t * 设置执行队列取元素超时时长，单位毫秒\n\t * @param delayQueueTimeout 执行队列取元素超时时长，单位毫秒\n\t * @return this\n\t */\n\tpublic SystemTimer setDelayQueueTimeout(long delayQueueTimeout){\n\t\tthis.delayQueueTimeout = delayQueueTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 启动，异步\n\t *\n\t * @return this\n\t */\n\tpublic SystemTimer start() {\n\t\tbossThreadPool = ThreadUtil.newSingleExecutor();\n\t\tisRunning = true;\n\t\tbossThreadPool.submit(() -> {\n\t\t\twhile (true) {\n\t\t\t\tif(false == advanceClock()){\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn this;\n\t}\n\n\t/**\n\t * 强制结束\n\t */\n\tpublic void stop(){\n\t\tthis.isRunning = false;\n\t\tthis.bossThreadPool.shutdown();\n\t}\n\n\t/**\n\t * 添加任务\n\t *\n\t * @param timerTask 任务\n\t */\n\tpublic void addTask(TimerTask timerTask) {\n\t\t//添加失败任务直接执行\n\t\tif (false == timeWheel.addTask(timerTask)) {\n\t\t\tThreadUtil.execAsync(timerTask.getTask());\n\t\t}\n\t}\n\n\t/**\n\t * 指针前进并获取过期任务\n\t *\n\t * @return 是否结束\n\t */\n\tprivate boolean advanceClock() {\n\t\tif(false == isRunning){\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tTimerTaskList timerTaskList = poll();\n\t\t\tif (null != timerTaskList) {\n\t\t\t\t//推进时间\n\t\t\t\ttimeWheel.advanceClock(timerTaskList.getExpire());\n\t\t\t\t//执行过期任务（包含降级操作）\n\t\t\t\ttimerTaskList.flush(this::addTask);\n\t\t\t}\n\t\t} catch (InterruptedException ignore) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 执行队列取任务列表\n\t * @return 任务列表\n\t * @throws InterruptedException 中断异常\n\t */\n\tprivate TimerTaskList poll() throws InterruptedException {\n\t\treturn this.delayQueueTimeout > 0 ?\n\t\t\t\tdelayQueue.poll(delayQueueTimeout, TimeUnit.MILLISECONDS) :\n\t\t\t\tdelayQueue.poll();\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/timingwheel/TimerTask.java",
    "content": "package cn.hutool.cron.timingwheel;\n\n/**\n * 延迟任务\n *\n * @author eliasyaoyc, looly\n */\npublic class TimerTask {\n\n\t/**\n\t * 延迟时间\n\t */\n\tprivate final long delayMs;\n\n\t/**\n\t * 任务\n\t */\n\tprivate final Runnable task;\n\n\t/**\n\t * 时间槽\n\t */\n\tprotected TimerTaskList timerTaskList;\n\n\t/**\n\t * 下一个节点\n\t */\n\tprotected TimerTask next;\n\n\t/**\n\t * 上一个节点\n\t */\n\tprotected TimerTask prev;\n\n\t/**\n\t * 任务描述\n\t */\n\tpublic String desc;\n\n\t/**\n\t * 构造\n\t *\n\t * @param task 任务\n\t * @param delayMs 延迟毫秒数（以当前时间为准）\n\t */\n\tpublic TimerTask(Runnable task, long delayMs) {\n\t\tthis.delayMs = System.currentTimeMillis() + delayMs;\n\t\tthis.task = task;\n\t}\n\n\t/**\n\t * 获取任务\n\t *\n\t * @return 任务\n\t */\n\tpublic Runnable getTask() {\n\t\treturn task;\n\t}\n\n\t/**\n\t * 获取延迟时间点，即创建时间+延迟时长（单位毫秒）\n\t * @return 延迟时间点\n\t */\n\tpublic long getDelayMs() {\n\t\treturn delayMs;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn desc;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/timingwheel/TimerTaskList.java",
    "content": "package cn.hutool.cron.timingwheel;\n\nimport java.util.concurrent.Delayed;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Consumer;\n\n/**\n * 任务队列，任务双向链表\n *\n * @author siran.yao，looly\n */\npublic class TimerTaskList implements Delayed {\n\n\t/**\n\t * 过期时间\n\t */\n\tprivate final AtomicLong expire;\n\n\t/**\n\t * 根节点\n\t */\n\tprivate final TimerTask root;\n\n\t/**\n\t * 构造\n\t */\n\tpublic TimerTaskList(){\n\t\texpire = new AtomicLong(-1L);\n\n\t\troot = new TimerTask( null,-1L);\n\t\troot.prev = root;\n\t\troot.next = root;\n\t}\n\n\t/**\n\t * 设置过期时间\n\t *\n\t * @param expire 过期时间，单位毫秒\n\t * @return 是否设置成功\n\t */\n\tpublic boolean setExpiration(long expire) {\n\t\treturn this.expire.getAndSet(expire) != expire;\n\t}\n\n\t/**\n\t * 获取过期时间\n\t * @return 过期时间\n\t */\n\tpublic long getExpire() {\n\t\treturn expire.get();\n\t}\n\n\t/**\n\t * 新增任务，将任务加入到双向链表的头部\n\t *\n\t * @param timerTask 延迟任务\n\t */\n\tpublic void addTask(TimerTask timerTask) {\n\t\tsynchronized (this) {\n\t\t\tif (timerTask.timerTaskList == null) {\n\t\t\t\ttimerTask.timerTaskList = this;\n\t\t\t\tTimerTask tail = root.prev;\n\t\t\t\ttimerTask.next = root;\n\t\t\t\ttimerTask.prev = tail;\n\t\t\t\ttail.next = timerTask;\n\t\t\t\troot.prev = timerTask;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 移除任务\n\t *\n\t * @param timerTask 任务\n\t */\n\tpublic void removeTask(TimerTask timerTask) {\n\t\tsynchronized (this) {\n\t\t\tif (this.equals(timerTask.timerTaskList)) {\n\t\t\t\ttimerTask.next.prev = timerTask.prev;\n\t\t\t\ttimerTask.prev.next = timerTask.next;\n\t\t\t\ttimerTask.timerTaskList = null;\n\t\t\t\ttimerTask.next = null;\n\t\t\t\ttimerTask.prev = null;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 重新分配，即将列表中的任务全部处理\n\t *\n\t * @param flush 任务处理函数\n\t */\n\tpublic synchronized void flush(Consumer<TimerTask> flush) {\n\t\tTimerTask timerTask = root.next;\n\t\twhile (false == timerTask.equals(root)) {\n\t\t\tthis.removeTask(timerTask);\n\t\t\tflush.accept(timerTask);\n\t\t\ttimerTask = root.next;\n\t\t}\n\t\texpire.set(-1L);\n\t}\n\n\t@Override\n\tpublic long getDelay(TimeUnit unit) {\n\t\treturn Math.max(0, unit.convert(expire.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS));\n\t}\n\n\t@Override\n\tpublic int compareTo(Delayed o) {\n\t\tif (o instanceof TimerTaskList) {\n\t\t\treturn Long.compare(expire.get(), ((TimerTaskList) o).expire.get());\n\t\t}\n\t\treturn 0;\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/timingwheel/TimingWheel.java",
    "content": "package cn.hutool.cron.timingwheel;\n\nimport cn.hutool.log.StaticLog;\n\nimport java.util.function.Consumer;\n\n/**\n * 多层时间轮，常用于延时任务。<br>\n * 时间轮是一种环形数据结构，由多个槽组成，每个槽中存放任务集合。<br>\n * 一个单独的线程推进时间一槽一槽的移动，并执行槽中的任务。\n *\n * @author eliasyaoyc, looly\n */\npublic class TimingWheel {\n\n\t/**\n\t * 一个时间槽的范围\n\t */\n\tprivate final long tickMs;\n\n\t/**\n\t * 时间轮大小，时间轮中时间槽的个数\n\t */\n\tprivate final int wheelSize;\n\n\t/**\n\t * 时间跨度，当前时间轮总间隔，即单个槽的跨度*槽个数\n\t */\n\tprivate final long interval;\n\n\t/**\n\t * 时间槽\n\t */\n\tprivate final TimerTaskList[] timerTaskLists;\n\n\t/**\n\t * 当前时间，指向当前操作的时间格，代表当前时间\n\t */\n\tprivate long currentTime;\n\n\t/**\n\t * 上层时间轮\n\t */\n\tprivate volatile TimingWheel overflowWheel;\n\n\t/**\n\t * 任务处理器\n\t */\n\tprivate final Consumer<TimerTaskList> consumer;\n\n\t/**\n\t * 构造\n\t *\n\t * @param tickMs    一个时间槽的范围，单位毫秒\n\t * @param wheelSize 时间轮大小\n\t * @param consumer  任务处理器\n\t */\n\tpublic TimingWheel(long tickMs, int wheelSize, Consumer<TimerTaskList> consumer) {\n\t\tthis(tickMs, wheelSize, System.currentTimeMillis(), consumer);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tickMs      一个时间槽的范围，单位毫秒\n\t * @param wheelSize   时间轮大小\n\t * @param currentTime 当前时间\n\t * @param consumer    任务处理器\n\t */\n\tpublic TimingWheel(long tickMs, int wheelSize, long currentTime, Consumer<TimerTaskList> consumer) {\n\t\tthis.tickMs = tickMs;\n\t\tthis.wheelSize = wheelSize;\n\t\tthis.interval = tickMs * wheelSize;\n\t\tthis.timerTaskLists = new TimerTaskList[wheelSize];\n\t\tinitTimerTaskList();\n\t\t//currentTime为tickMs的整数倍 这里做取整操作\n\t\tthis.currentTime = currentTime - (currentTime % tickMs);\n\t\tthis.consumer = consumer;\n\t}\n\n\t/**\n\t * 添加任务到时间轮\n\t *\n\t * @param timerTask 任务\n\t * @return 是否成功\n\t */\n\tpublic boolean addTask(TimerTask timerTask) {\n\t\tlong expiration = timerTask.getDelayMs();\n\t\t//过期任务直接执行\n\t\tif (expiration < currentTime + tickMs) {\n\t\t\treturn false;\n\t\t} else if (expiration < currentTime + interval) {\n\t\t\t//当前时间轮可以容纳该任务 加入时间槽\n\t\t\tlong virtualId = expiration / tickMs;\n\t\t\tint index = (int) (virtualId % wheelSize);\n\t\t\tStaticLog.debug(\"tickMs: {} ------index: {} ------expiration: {}\", tickMs, index, expiration);\n\t\t\tTimerTaskList timerTaskList = timerTaskLists[index];\n\t\t\ttimerTaskList.addTask(timerTask);\n\t\t\tif (timerTaskList.setExpiration(virtualId * tickMs)) {\n\t\t\t\t//添加到delayQueue中\n\t\t\t\tconsumer.accept(timerTaskList);\n\t\t\t}\n\t\t} else {\n\t\t\t//放到上一层的时间轮\n\t\t\tTimingWheel timeWheel = getOverflowWheel();\n\t\t\ttimeWheel.addTask(timerTask);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 推进时间\n\t *\n\t * @param timestamp 推进的时间\n\t */\n\tpublic void advanceClock(long timestamp) {\n\t\tif (timestamp >= currentTime + tickMs) {\n\t\t\tcurrentTime = timestamp - (timestamp % tickMs);\n\t\t\tif (overflowWheel != null) {\n\t\t\t\t//推进上层时间轮时间\n\t\t\t\tthis.getOverflowWheel().advanceClock(timestamp);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 创建或者获取上层时间轮\n\t */\n\tprivate TimingWheel getOverflowWheel() {\n\t\tif (overflowWheel == null) {\n\t\t\tsynchronized (this) {\n\t\t\t\tif (overflowWheel == null) {\n\t\t\t\t\toverflowWheel = new TimingWheel(interval, wheelSize, currentTime, consumer);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn overflowWheel;\n\t}\n\n\t/**\n\t * 初始 timerTaskLists\n\t */\n\tprivate void initTimerTaskList() {\n\t\tfor (int i = 0; i < this.timerTaskLists.length; i++) {\n\t\t\tthis.timerTaskLists[i] = new TimerTaskList();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/main/java/cn/hutool/cron/timingwheel/package-info.java",
    "content": "/**\n * 时间轮实现，重写了kafka的TimingWheel<br>\n * 时间轮一般会实现成一个环形结构，类似一个时钟，分为很多槽，一个槽代表一个时间间隔，每个槽使用双向链表存储定时任务。指针周期性地跳动，跳动到一个槽位，就执行该槽位的定时任务。\n *\n * <p>\n * 时间轮算法介绍：https://www.confluent.io/blog/apache-kafka-purgatory-hierarchical-timing-wheels/<br>\n * 参考：https://github.com/eliasyaoyc/timingwheel\n *\n * @author looly\n */\npackage cn.hutool.cron.timingwheel;\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/TaskTableTest.java",
    "content": "package cn.hutool.cron;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.cron.pattern.CronPattern;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class TaskTableTest {\n\n\t@Test\n\t@Disabled\n\tpublic void toStringTest(){\n\t\tfinal TaskTable taskTable = new TaskTable();\n\t\ttaskTable.add(IdUtil.fastUUID(), new CronPattern(\"*/10 * * * * *\"), ()-> Console.log(\"Task 1\"));\n\t\ttaskTable.add(IdUtil.fastUUID(), new CronPattern(\"*/20 * * * * *\"), ()-> Console.log(\"Task 2\"));\n\t\ttaskTable.add(IdUtil.fastUUID(), new CronPattern(\"*/30 * * * * *\"), ()-> Console.log(\"Task 3\"));\n\n\t\tConsole.log(taskTable);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/AddAndRemoveMainTest.java",
    "content": "package cn.hutool.cron.demo;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.cron.CronUtil;\n\npublic class AddAndRemoveMainTest {\n\t\n\tpublic static void main(String[] args) {\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start(false);\n\t\tCronUtil.getScheduler().clear();\n\t\tString id = CronUtil.schedule(\"*/2 * * * * *\", (Runnable) () -> Console.log(\"task running : 2s\"));\n\t\tThreadUtil.sleep(3000);\n\t\tCronUtil.remove(id);\n\t\tConsole.log(\"Task Removed\");\n\n\t\tCronUtil.schedule(\"*/3 * * * * *\", (Runnable) () -> Console.log(\"New task add running : 3s\"));\n\t\tConsole.log(\"New Task added.\");\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java",
    "content": "package cn.hutool.cron.demo;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.cron.CronUtil;\nimport cn.hutool.cron.Scheduler;\nimport cn.hutool.cron.TaskExecutor;\nimport cn.hutool.cron.listener.TaskListener;\nimport cn.hutool.cron.task.Task;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 定时任务样例\n */\npublic class CronTest {\n\n\t@Test\n\t@Disabled\n\tvoid emptyScheduleTest() {\n\t\tfinal Scheduler scheduler = new Scheduler();\n\t\t// 支持秒级别定时任务\n\t\tscheduler.setMatchSecond(true);\n\n\t\tscheduler.start();\n\n\t\tThreadUtil.waitForDie();\n\t\tConsole.log(\"Exit.\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void customCronTest() {\n\t\tCronUtil.schedule(\"*/2 * * * * *\", (Task) () -> Console.log(\"Task executed.\"));\n\n\t\t// 支持秒级别定时任务\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start();\n\n\t\tThreadUtil.waitForDie();\n\t\tConsole.log(\"Exit.\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cronTest() {\n\t\t// 支持秒级别定时任务\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.getScheduler().setDaemon(false);\n\t\tCronUtil.start();\n\n\t\tThreadUtil.waitForDie();\n\t\tCronUtil.stop();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cronWithListenerTest() {\n\t\tCronUtil.getScheduler().addListener(new TaskListener() {\n\t\t\t@Override\n\t\t\tpublic void onStart(TaskExecutor executor) {\n\t\t\t\tConsole.log(\"Found task:[{}] start!\", executor.getCronTask().getId());\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void onSucceeded(TaskExecutor executor) {\n\t\t\t\tConsole.log(\"Found task:[{}] success!\", executor.getCronTask().getId());\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void onFailed(TaskExecutor executor, Throwable exception) {\n\t\t\t\tConsole.error(\"Found task:[{}] failed!\", executor.getCronTask().getId());\n\t\t\t}\n\t\t});\n\n\t\t// 支持秒级别定时任务\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start();\n\n\t\tThreadUtil.waitForDie();\n\t\tConsole.log(\"Exit.\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addAndRemoveTest() {\n\t\tString id = CronUtil.schedule(\"*/2 * * * * *\", (Runnable) () -> Console.log(\"task running : 2s\"));\n\n\t\tConsole.log(id);\n\t\tCronUtil.remove(id);\n\n\t\t// 支持秒级别定时任务\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/DeamonMainTest.java",
    "content": "package cn.hutool.cron.demo;\n\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.cron.CronUtil;\nimport cn.hutool.cron.task.InvokeTask;\n\npublic class DeamonMainTest {\n\tpublic static void main(String[] args) {\n\t\t// 测试守护线程是否对作业线程有效\n\t\tCronUtil.schedule(\"*/2 * * * * *\", new InvokeTask(\"cn.hutool.cron.demo.TestJob.doWhileTest\"));\n\t\t// 当为守护线程时，stop方法调用后doWhileTest里的循环输出将终止，表示作业线程正常结束\n\t\t// 当非守护线程时，stop方法调用后，不再产生新的作业，原作业正常执行。\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start(true);\n\n\t\tThreadUtil.sleep(3000);\n\t\tCronUtil.stop();\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/JobMainTest.java",
    "content": "package cn.hutool.cron.demo;\n\nimport cn.hutool.cron.CronUtil;\n\n/**\n * 定时任务样例\n */\npublic class JobMainTest {\n\n\tpublic static void main(String[] args) {\n\t\tCronUtil.setMatchSecond(true);\n\t\tCronUtil.start(false);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob.java",
    "content": "package cn.hutool.cron.demo;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.IdUtil;\n\n/**\n * 测试定时任务，当触发到定时的时间点时，执行doTest方法\n * \n * @author looly\n *\n */\npublic class TestJob {\n\t\n\tprivate final String jobId = IdUtil.simpleUUID();\n\n\t/**\n\t * 执行定时任务内容\n\t */\n\tpublic void doTest() {\n//\t\tString name = Thread.currentThread().getName();\n\t\tConsole.log(\"Test Job {} running... at {}\", jobId, DateUtil.now());\n\t}\n\n\t/**\n\t * 执行循环定时任务，测试在定时任务结束时作为deamon线程是否能正常结束\n\t */\n\t@SuppressWarnings(\"InfiniteLoopStatement\")\n\tpublic void doWhileTest() {\n\t\tString name = Thread.currentThread().getName();\n\t\twhile (true) {\n\t\t\tConsole.log(\"Job {} while running...\", name);\n\t\t\tThreadUtil.sleep(2000);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob2.java",
    "content": "package cn.hutool.cron.demo;\n\nimport java.util.concurrent.TimeUnit;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\n\n/**\n * 测试定时任务，当触发到定时的时间点时，执行doTest方法\n * \n * @author looly\n *\n */\npublic class TestJob2 {\n\n\t/**\n\t * 执行定时任务内容\n\t */\n\tpublic void doTest() {\n\t\tConsole.log(\"TestJob2.doTest开始执行……\");\n\t\tThreadUtil.sleep(20, TimeUnit.SECONDS);\n\t\tConsole.log(\"延迟20s打印testJob2\");\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternBuilderTest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.cron.CronException;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class CronPatternBuilderTest {\n\n\t@Test\n\tpublic void buildMatchAllTest(){\n\t\tString build = CronPatternBuilder.of().build();\n\t\tassertEquals(\"* * * * *\", build);\n\n\t\tbuild = CronPatternBuilder.of()\n\t\t\t\t.set(Part.SECOND, \"*\")\n\t\t\t\t.build();\n\t\tassertEquals(\"* * * * * *\", build);\n\n\t\tbuild = CronPatternBuilder.of()\n\t\t\t\t.set(Part.SECOND, \"*\")\n\t\t\t\t.set(Part.YEAR, \"*\")\n\t\t\t\t.build();\n\t\tassertEquals(\"* * * * * * *\", build);\n\t}\n\n\t@Test\n\tpublic void buildRangeTest(){\n\t\tString build = CronPatternBuilder.of()\n\t\t\t\t.set(Part.SECOND, \"*\")\n\t\t\t\t.setRange(Part.HOUR, 2, 9)\n\t\t\t\t.build();\n\t\tassertEquals(\"* * 2-9 * * *\", build);\n\t}\n\n\t@Test\n\tpublic void buildRangeErrorTest(){\n\t\tassertThrows(CronException.class, () -> {\n\t\t\tString build = CronPatternBuilder.of()\n\t\t\t\t.set(Part.SECOND, \"*\")\n\t\t\t\t// 55无效值\n\t\t\t\t.setRange(Part.HOUR, 2, 55)\n\t\t\t\t.build();\n\t\t\tassertEquals(\"* * 2-9 * * *\", build);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternNextMatchTest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateField;\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Calendar;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class CronPatternNextMatchTest {\n\n\t@Test\n\tpublic void nextMatchAllAfterTest() {\n\t\t// 匹配所有，返回下一秒的时间\n\t\tCronPattern pattern = new CronPattern(\"* * * * * * *\");\n\t\tDateTime date = DateUtil.truncate(DateUtil.date(), DateField.SECOND);\n\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tassertEquals(date.getTime() + 1000, DateUtil.date(calendar).getTime());\n\n\t\t// 匹配所有分，返回下一分钟\n\t\tpattern = new CronPattern(\"0 * * * * * *\");\n\t\tdate = DateUtil.parse(\"2022-04-08 07:44:16\");\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tassertEquals(DateUtil.parse(\"2022-04-08 07:45:00\"), DateUtil.date(calendar));\n\n\t\t// 匹配所有时，返回下一小时\n\t\tpattern = new CronPattern(\"0 0 * * * * *\");\n\t\tdate = DateUtil.parse(\"2022-04-08 07:44:16\");\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tassertEquals(DateUtil.parse(\"2022-04-08 08:00:00\"), DateUtil.date(calendar));\n\n\t\t// 匹配所有天，返回明日\n\t\tpattern = new CronPattern(\"0 0 0 * * * *\");\n\t\tdate = DateUtil.parse(\"2022-04-08 07:44:16\");\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tassertEquals(DateUtil.parse(\"2022-04-09 00:00:00\"), DateUtil.date(calendar));\n\n\t\t// 匹配所有月，返回下一月\n\t\tpattern = new CronPattern(\"0 0 0 1 * * *\");\n\t\tdate = DateUtil.parse(\"2022-04-08 07:44:16\");\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tassertEquals(DateUtil.parse(\"2022-05-01 00:00:00\"), DateUtil.date(calendar));\n\t}\n\n\t@Test\n\tpublic void nextMatchAfterTest(){\n\t\tCronPattern pattern = new CronPattern(\"23 12 * 12 * * *\");\n\n\t\t// 时间正常递增\n\t\t//noinspection ConstantConditions\n\t\tCalendar calendar = pattern.nextMatchAfter(\n\t\t\tDateUtil.parse(\"2022-04-12 09:12:12\").toCalendar());\n\n\t\tassertTrue(pattern.match(calendar, true));\n\t\tassertEquals(\"2022-04-12 09:12:23\", DateUtil.date(calendar).toString());\n\n\t\t// 秒超出规定值的最大值，分+1，秒取最小值\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(\n\t\t\tDateUtil.parse(\"2022-04-12 09:09:24\").toCalendar());\n\t\tassertTrue(pattern.match(calendar, true));\n\t\tassertEquals(\"2022-04-12 09:12:23\", DateUtil.date(calendar).toString());\n\n\t\t// 秒超出规定值的最大值，分不变，小时+1，秒和分使用最小值\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(\n\t\t\tDateUtil.parse(\"2022-04-12 09:12:24\").toCalendar());\n\t\tassertTrue(pattern.match(calendar, true));\n\t\tassertEquals(\"2022-04-12 10:12:23\", DateUtil.date(calendar).toString());\n\n\t\t// 天超出规定值的最大值，月+1，天、时、分、秒取最小值\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(\n\t\t\tDateUtil.parse(\"2022-04-13 09:12:24\").toCalendar());\n\t\tassertTrue(pattern.match(calendar, true));\n\t\tassertEquals(\"2022-05-12 00:12:23\", DateUtil.date(calendar).toString());\n\n\t\t// 跨年\n\t\t//noinspection ConstantConditions\n\t\tcalendar = pattern.nextMatchAfter(\n\t\t\tDateUtil.parse(\"2021-12-22 00:00:00\").toCalendar());\n\t\tassertTrue(pattern.match(calendar, true));\n\t\tassertEquals(\"2022-01-12 00:12:23\", DateUtil.date(calendar).toString());\n\t}\n\n\t@Test\n\tpublic void nextMatchAfterByWeekTest(){\n\t\tCronPattern pattern = new CronPattern(\"1 1 1 * * Sat *\");\n\t\t// 周日，下个周六在4月9日\n\t\tfinal DateTime time = DateUtil.parse(\"2022-04-03\");\n\t\tassert time != null;\n\t\tfinal Calendar calendar = pattern.nextMatchAfter(time.toCalendar());\n\t\tassertEquals(\"2022-04-09 01:01:01\", DateUtil.date(calendar).toString());\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryMonth1() {\n\t\tDateTime date = DateUtil.parse(\"2023-01-08 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-01-31 03:02:01\");\n\t\t// 匹配所有月，生成每个月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 3 L * ?\");\n\t\tfor (int i = 0; i < 30; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一个月的最后一天\n\t\t\tresult = result.offset(DateField.DAY_OF_MONTH, 1);\n\t\t\tint lastDayOfMonth = DateUtil.getLastDayOfMonth(result);\n\t\t\tresult.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryMonth2() {\n\t\tDateTime date = DateUtil.parse(\"2023-03-08 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-03-31 03:02:01\");\n\t\t// 匹配所有月，生成每个月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 3 L * ?\");\n\t\tfor (int i = 0; i < 30; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一个月的最后一天\n\t\t\tresult = result.offset(DateField.DAY_OF_MONTH, 1);\n\t\t\tint lastDayOfMonth = DateUtil.getLastDayOfMonth(result);\n\t\t\tresult.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryYear1() {\n\t\tDateTime date = DateUtil.parse(\"2023-01-08 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-02-28 03:02:01\");\n\t\t// 匹配每一年2月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 3 L 2 ?\");\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一年的最后一天\n\t\t\tresult = result.offset(DateField.YEAR, 1);\n\t\t\tint lastDayOfMonth = DateUtil.getLastDayOfMonth(result);\n\t\t\tresult.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryYear2() {\n\t\tDateTime date = DateUtil.parse(\"2022-03-08 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-02-28 03:02:01\");\n\t\t// 匹配每一年2月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 3 L 2 ?\");\n\t\tfor (int i = 0; i < 30; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一年的最后一天\n\t\t\tresult = result.offset(DateField.YEAR, 1);\n\t\t\tint lastDayOfMonth = DateUtil.getLastDayOfMonth(result);\n\t\t\tresult.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryYear3() {\n\t\tDateTime date = DateUtil.parse(\"2022-03-08 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-02-28 03:02:01\");\n\t\t// 匹配每一年2月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 3 L 2 ?\");\n\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\tConsole.log(DateUtil.date(calendar));\n\t}\n\n\t@Test\n\tpublic void testEveryHour() {\n\t\tDateTime date = DateUtil.parse(\"2022-02-28 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2022-02-28 08:02:01\");\n\t\t// 匹配每一年2月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"1 2 */1 * * ?\");\n\t\tfor (int i = 0; i < 30; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一个小时\n\t\t\tresult = result.offset(DateField.HOUR_OF_DAY, 1);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testLastDayOfMonthForEveryHour() {\n\t\tDateTime date = DateUtil.parse(\"2023-01-28 07:44:16\");\n\t\tDateTime result = DateUtil.parse(\"2023-01-31 00:00:00\");\n\t\t// 匹配每一年2月的最后一天\n\t\tCronPattern pattern = new CronPattern(\"0 0 */1 L * ?\");\n\t\tfor (int i = 0; i < 400; i++) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tCalendar calendar = pattern.nextMatchAfter(date.toCalendar());\n\t\t\tdate = DateUtil.date(calendar);\n\t\t\tassertEquals(date, result);\n\t\t\t// 加一秒\n\t\t\tdate = date.offset(DateField.SECOND, 1);\n\n\t\t\t// 移动到下一个小时\n\t\t\tDateTime t = result.setMutable(false).offset(DateField.HOUR_OF_DAY, 1);\n\t\t\tif (t.dayOfMonth() != result.dayOfMonth()) {\n\t\t\t\t// 移动到下个月最后一天的开始\n\t\t\t\tresult = result.offset(DateField.DAY_OF_MONTH, 1);\n\t\t\t\tint lastDayOfMonth = DateUtil.getLastDayOfMonth(result);\n\t\t\t\tresult = result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);\n\t\t\t\tresult = DateUtil.beginOfDay(result);\n\t\t\t} else {\n\t\t\t\tresult = t;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternTest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.cron.CronException;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * 定时任务单元测试类\n *\n * @author Looly\n */\npublic class CronPatternTest {\n\n\t@Test\n\tpublic void matchAllTest() {\n\t\tCronPattern pattern;\n\t\t// 任何时间匹配\n\t\tpattern = CronPattern.of(\"* * * * * *\");\n\t\tassertMatch(pattern, DateUtil.now());\n\t}\n\n\t@Test\n\tpublic void matchAllTest2() {\n\t\t// 在5位表达式中，秒部分并不是任意匹配，而是一个固定值0\n\t\t// 因此此处匹配就不能匹配秒\n\t\tCronPattern pattern;\n\t\t// 任何时间匹配\n\t\t// 分 时 天 月 周\n\t\tpattern = CronPattern.of(\"* * * * *\");\n\n\t\t// 测试时，秒归零，则任意时间匹配\n\t\tassertMatch(pattern, DateUtil.beginOfMinute(DateUtil.date()).toString());\n\t}\n\n\t@Test\n\tpublic void cronPatternTest() {\n\t\tCronPattern pattern;\n\n\t\t// 12:11匹配\n\t\tpattern = CronPattern.of(\"39 11 12 * * *\");\n\t\tassertMatch(pattern, \"12:11:39\");\n\n\t\t// 每5分钟匹配，匹配分钟为：[0,5,10,15,20,25,30,35,40,45,50,55]\n\t\tpattern = CronPattern.of(\"39 */5 * * * *\");\n\t\tassertMatch(pattern, \"12:00:39\");\n\t\tassertMatch(pattern, \"12:05:39\");\n\t\tassertMatch(pattern, \"12:10:39\");\n\t\tassertMatch(pattern, \"12:15:39\");\n\t\tassertMatch(pattern, \"12:20:39\");\n\t\tassertMatch(pattern, \"12:25:39\");\n\t\tassertMatch(pattern, \"12:30:39\");\n\t\tassertMatch(pattern, \"12:35:39\");\n\t\tassertMatch(pattern, \"12:40:39\");\n\t\tassertMatch(pattern, \"12:45:39\");\n\t\tassertMatch(pattern, \"12:50:39\");\n\t\tassertMatch(pattern, \"12:55:39\");\n\n\t\t// 2:01,3:01,4:01\n\t\tpattern = CronPattern.of(\"39 1 2-4 * * *\");\n\t\tassertMatch(pattern, \"02:01:39\");\n\t\tassertMatch(pattern, \"03:01:39\");\n\t\tassertMatch(pattern, \"04:01:39\");\n\n\t\t// 2:01,3:01,4:01\n\t\tpattern = CronPattern.of(\"39 1 2,3,4 * * *\");\n\t\tassertMatch(pattern, \"02:01:39\");\n\t\tassertMatch(pattern, \"03:01:39\");\n\t\tassertMatch(pattern, \"04:01:39\");\n\n\t\t// 08-07, 08-06\n\t\tpattern = CronPattern.of(\"39 0 0 6,7 8 *\");\n\t\tassertMatch(pattern, \"2016-08-07 00:00:39\");\n\t\tassertMatch(pattern, \"2016-08-06 00:00:39\");\n\n\t\t// 别名忽略大小写\n\t\tpattern = CronPattern.of(\"39 0 0 6,7 Aug *\");\n\t\tassertMatch(pattern, \"2016-08-06 00:00:39\");\n\t\tassertMatch(pattern, \"2016-08-07 00:00:39\");\n\n\t\tpattern = CronPattern.of(\"39 0 0 7 aug *\");\n\t\tassertMatch(pattern, \"2016-08-07 00:00:39\");\n\t}\n\n\t@Test\n\tpublic void matchDayOfWeekTest() {\n\t\t// 星期四\n\t\tCronPattern pattern = CronPattern.of(\"39 0 0 * * Thu\");\n\t\tassertMatch(pattern, \"2017-02-09 00:00:39\");\n\n\t\t// 星期日的三种形式\n\t\tpattern = CronPattern.of(\"39 0 0 * * Sun\");\n\t\tassertMatch(pattern, \"2022-03-27 00:00:39\");\n\n\t\tpattern = CronPattern.of(\"39 0 0 * * 0\");\n\t\tassertMatch(pattern, \"2022-03-27 00:00:39\");\n\n\t\tpattern = CronPattern.of(\"39 0 0 * * 7\");\n\t\tassertMatch(pattern, \"2022-03-27 00:00:39\");\n\t}\n\n\t@SuppressWarnings(\"ConstantConditions\")\n\t@Test\n\tpublic void CronPatternTest2() {\n\t\tCronPattern pattern = CronPattern.of(\"0/30 * * * *\");\n\t\tassertTrue(pattern.match(DateUtil.parse(\"2018-10-09 12:00:00\").getTime(), false));\n\t\tassertTrue(pattern.match(DateUtil.parse(\"2018-10-09 12:30:00\").getTime(), false));\n\n\t\tpattern = CronPattern.of(\"32 * * * *\");\n\t\tassertTrue(pattern.match(DateUtil.parse(\"2018-10-09 12:32:00\").getTime(), false));\n\t}\n\n\t@Test\n\tpublic void patternTest() {\n\t\tCronPattern pattern = CronPattern.of(\"* 0 4 * * ?\");\n\t\tassertMatch(pattern, \"2017-02-09 04:00:00\");\n\t\tassertMatch(pattern, \"2017-02-19 04:00:33\");\n\n\t\t// 6位Quartz风格表达式\n\t\tpattern = CronPattern.of(\"* 0 4 * * ?\");\n\t\tassertMatch(pattern, \"2017-02-09 04:00:00\");\n\t\tassertMatch(pattern, \"2017-02-19 04:00:33\");\n\t}\n\n\t@Test\n\tpublic void patternNegativeTest() {\n\t\t// -4表示倒数的数字，此处在小时上，-4表示 23 - 4，为19\n\t\tCronPattern pattern = CronPattern.of(\"* 0 -4 * * ?\");\n\t\tassertMatch(pattern, \"2017-02-09 19:00:00\");\n\t\tassertMatch(pattern, \"2017-02-19 19:00:33\");\n\t}\n\n\t@Test\n\tpublic void rangePatternTest() {\n\t\tCronPattern pattern = CronPattern.of(\"* 20/2 * * * ?\");\n\t\tassertMatch(pattern, \"2017-02-09 04:20:00\");\n\t\tassertMatch(pattern, \"2017-02-09 05:20:00\");\n\t\tassertMatch(pattern, \"2017-02-19 04:22:33\");\n\n\t\tpattern = CronPattern.of(\"* 2-20/2 * * * ?\");\n\t\tassertMatch(pattern, \"2017-02-09 04:02:00\");\n\t\tassertMatch(pattern, \"2017-02-09 05:04:00\");\n\t\tassertMatch(pattern, \"2017-02-19 04:20:33\");\n\t}\n\n\t@Test\n\tpublic void lastTest() {\n\t\t// 每月最后一天的任意时间\n\t\tCronPattern pattern = CronPattern.of(\"* * * L * ?\");\n\t\tassertMatch(pattern, \"2017-07-31 04:20:00\");\n\t\tassertMatch(pattern, \"2017-02-28 04:20:00\");\n\n\t\t// 最后一个月的任意时间\n\t\tpattern = CronPattern.of(\"* * * * L ?\");\n\t\tassertMatch(pattern, \"2017-12-02 04:20:00\");\n\n\t\t// 任意天的最后时间\n\t\tpattern = CronPattern.of(\"L L L * * ?\");\n\t\tassertMatch(pattern, \"2017-12-02 23:59:59\");\n\t}\n\n\t@Test\n\tpublic void rangeYearTest() {\n\t\tassertThrows(CronException.class, () -> {\n\t\t\t// year的范围是1970~2099年，超出报错\n\t\t\tCronPattern.of(\"0/1 * * * 1/1 ? 2020-2120\");\n\t\t});\n\t}\n\n\t/**\n\t * 表达式是否匹配日期\n\t *\n\t * @param pattern 表达式\n\t * @param date    日期，标准日期时间字符串\n\t */\n\t@SuppressWarnings(\"ConstantConditions\")\n\tprivate void assertMatch(CronPattern pattern, String date) {\n\t\tassertTrue(pattern.match(DateUtil.parse(date).toCalendar(), false));\n\t\tassertTrue(pattern.match(DateUtil.parse(date).toCalendar(), true));\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CronPatternUtilTest {\n\n\t@Test\n\tpublic void matchedDatesTest() {\n\t\t//测试每30秒执行\n\t\tList<Date> matchedDates = CronPatternUtil.matchedDates(\"0/30 * 8-18 * * ?\", DateUtil.parse(\"2018-10-15 14:33:22\"), 5, true);\n\t\tassertEquals(5, matchedDates.size());\n\t\tassertEquals(\"2018-10-15 14:33:30\", matchedDates.get(0).toString());\n\t\tassertEquals(\"2018-10-15 14:34:00\", matchedDates.get(1).toString());\n\t\tassertEquals(\"2018-10-15 14:34:30\", matchedDates.get(2).toString());\n\t\tassertEquals(\"2018-10-15 14:35:00\", matchedDates.get(3).toString());\n\t\tassertEquals(\"2018-10-15 14:35:30\", matchedDates.get(4).toString());\n\t}\n\n\t@Test\n\tpublic void matchedDatesTest2() {\n\t\t//测试每小时执行\n\t\tList<Date> matchedDates = CronPatternUtil.matchedDates(\"0 0 */1 * * *\", DateUtil.parse(\"2018-10-15 14:33:22\"), 5, true);\n\t\tassertEquals(5, matchedDates.size());\n\t\tassertEquals(\"2018-10-15 15:00:00\", matchedDates.get(0).toString());\n\t\tassertEquals(\"2018-10-15 16:00:00\", matchedDates.get(1).toString());\n\t\tassertEquals(\"2018-10-15 17:00:00\", matchedDates.get(2).toString());\n\t\tassertEquals(\"2018-10-15 18:00:00\", matchedDates.get(3).toString());\n\t\tassertEquals(\"2018-10-15 19:00:00\", matchedDates.get(4).toString());\n\t}\n\n\t@Test\n\tpublic void matchedDatesTest3() {\n\t\t//测试最后一天\n\t\tList<Date> matchedDates = CronPatternUtil.matchedDates(\"0 0 */1 L * *\", DateUtil.parse(\"2018-10-30 23:33:22\"), 5, true);\n\t\tassertEquals(5, matchedDates.size());\n\t\tassertEquals(\"2018-10-31 00:00:00\", matchedDates.get(0).toString());\n\t\tassertEquals(\"2018-10-31 01:00:00\", matchedDates.get(1).toString());\n\t\tassertEquals(\"2018-10-31 02:00:00\", matchedDates.get(2).toString());\n\t\tassertEquals(\"2018-10-31 03:00:00\", matchedDates.get(3).toString());\n\t\tassertEquals(\"2018-10-31 04:00:00\", matchedDates.get(4).toString());\n\t}\n\n\t@Test\n\tpublic void issue4056Test() {\n\t\t// \"*/5\"和\"1/5\"意义相同，从1号开始，每5天一个匹配，则匹配的天为：\n\t\t// 2025-02-01, 2025-02-06, 2025-02-11, 2025-02-16, 2025-02-21, 2025-02-26\n\t\t// 2025-03-01, 2025-03-06, 2025-03-11, 2025-03-16, 2025-03-21, 2025-03-26, 2025-03-31\n\t\tfinal String cron = \"0 0 0 */5 * ? *\";\n\t\tfinal CronPattern cronPattern = new CronPattern(cron);\n\n\t\t// 2025-02-28不应该在匹配之列\n\t\tboolean match = cronPattern.match(DateUtil.parse(\"2025-02-28 00:00:00\").toCalendar(), true);\n\t\tAssertions.assertFalse( match);\n\n\t\tmatch = cronPattern.match(DateUtil.parse(\"2025-03-01 00:00:00\").toCalendar(), true);\n\t\tAssertions.assertTrue( match);\n\n\t\tmatch = cronPattern.match(DateUtil.parse(\"2025-03-31 00:00:00\").toCalendar(), true);\n\t\tAssertions.assertTrue( match);\n\t}\n\n\t@Test\n\tpublic void issue4056Test2() {\n\t\tfinal String cron = \"0 0 0 */5 * ? *\";\n\t\tfinal CronPattern cronPattern = new CronPattern(cron);\n\n\t\tfinal DateTime judgeTime = DateUtil.parse(\"2025-02-27 23:59:59\");\n\t\tfinal Date nextDate = CronPatternUtil.nextDateAfter(cronPattern, judgeTime);\n\t\t// \"*/5\"和\"1/5\"意义相同，从1号开始，每5天一个匹配，则匹配的天为：\n\t\t// 2025-02-01, 2025-02-06, 2025-02-11, 2025-02-16, 2025-02-21, 2025-02-26\n\t\t// 2025-03-01, 2025-03-06, 2025-03-11, 2025-03-16, 2025-03-21, 2025-03-26, 2025-03-31\n\t\t// 下一个匹配日期应为2025-03-01\n\t\tAssertions.assertEquals(\"2025-03-01 00:00:00\", nextDate.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue3685Test.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class Issue3685Test {\n\t@Test\n\tpublic void nextDateAfterTest() {\n\t\tDate date = CronPatternUtil.nextDateAfter(CronPattern.of(\"0 0 * * MON\"), DateUtil.parse(\"2024-08-01\"));\n\t\tassertEquals(\"2024-08-05 00:00:00\", date.toString());\n\n\t\tdate = CronPatternUtil.nextDateAfter(CronPattern.of(\"0 0 * * MON\"), DateUtil.parse(\"2024-08-02\"));\n\t\tassertEquals(\"2024-08-05 00:00:00\", date.toString());\n\n\t\tdate = CronPatternUtil.nextDateAfter(CronPattern.of(\"0 0 * * MON\"), DateUtil.parse(\"2024-08-03\"));\n\t\tassertEquals(\"2024-08-05 00:00:00\", date.toString());\n\n\t\tdate = CronPatternUtil.nextDateAfter(CronPattern.of(\"0 0 * * MON\"), DateUtil.parse(\"2024-08-04\"));\n\t\tassertEquals(\"2024-08-05 00:00:00\", date.toString());\n\n\t\tdate = CronPatternUtil.nextDateAfter(CronPattern.of(\"0 0 * * MON\"), DateUtil.parse(\"2024-08-05\"));\n\t\tassertEquals(\"2024-08-12 00:00:00\", date.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue4006Test.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\npublic class Issue4006Test {\n\t@Test\n\tvoid testCron() {\n//        String cron = \"0 0 0 */1 * ?\";\n\t\tString cron = \"0 0 0 */1 * ? *\";\n\t\tDateTime judgeTime = DateTime.of(new Date());\n\t\tCronPattern cronPattern = new CronPattern(cron);\n\n\t\tConsole.log(\"cronPattern = \" + cronPattern);\n\t\tDate nextDate = CronPatternUtil.nextDateAfter(cronPattern, judgeTime, true);\n\t\tConsole.log(\"nextDate = \" + nextDate);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue4056Test.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.quartz.CronExpression;\n\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.Date;\n\npublic class Issue4056Test {\n\n\t/**\n\t * 见：https://github.com/quartz-scheduler/quartz/issues/1298\n\t * Quartz-2.5.0这块有bug，只能使用2.4.0测认\n\t *\n\t * @throws ParseException 解析错误\n\t */\n\t@Test\n\tvoid testCronAll() throws ParseException {\n\t\tfinal ArrayList<String> cronsList = new ArrayList<>();\n\t\tfinal ArrayList<DateTime> judgeTimes = new ArrayList<>();\n\n\t\t// 1. Cron 表达式（40个）\n\t\tcronsList.add(\"0 0 0 * * ? *\"); // 每天00:00\n\t\tcronsList.add(\"0 0 12 * * ? *\"); // 每天中午12:00\n\t\tcronsList.add(\"0 0 18 * * ? *\"); // 每天傍晚18:00\n\t\tcronsList.add(\"0 0 6,12,18 * * ? *\"); // 每天6点、12点、18点\n\t\tcronsList.add(\"0 0 */6 * * ? *\"); // 每6小时\n\t\tcronsList.add(\"0 30 */8 * * ? *\"); // 每8小时的30分\n\t\tcronsList.add(\"0 */15 * * * ? *\"); // 每15分钟\n\t\tcronsList.add(\"0 */5 9-17 * * ? *\"); // 工作时间内每5分钟\n\t\tcronsList.add(\"0 0 0-23/2 * * ? *\"); // 每2小时\n\t\tcronsList.add(\"0 0 0 */8 * ? *\"); // 每8天的00:00\n\t\tcronsList.add(\"0 0 12 15 * ? *\"); // 每月15日12:00\n\t\tcronsList.add(\"0 0 0 L * ? *\"); // 每月最后一天00:00\n\t\tcronsList.add(\"0 0 0 29 2 ? *\"); // 2月29日00:00（闰年）\n\t\tcronsList.add(\"0 0 0 1 1 ? *\"); // 每年1月1日00:00\n\t\tcronsList.add(\"0 0/30 * * * ? *\"); // 每小时0分和30分\n\t\tcronsList.add(\"0 0 */4 * * ? *\"); // 每4小时\n\t\tcronsList.add(\"0 0 0 1/3 * ? *\"); // 每3天00:00\n\t\tcronsList.add(\"0 0 2 28-31 * ? *\"); // 每月最后几天2:00\n\t\tcronsList.add(\"0 0 0 1,15 * ? *\"); // 每月1日和15日00:00\n\t\tcronsList.add(\"0 0 0 1/5 * ? *\"); // 每5天00:00\n\t\tcronsList.add(\"0 0 0 1/10 * ? *\"); // 每10天00:00\n\t\tcronsList.add(\"0 0 0 1 */3 ? *\"); // 每3个月的第1天00:00\n\t\tcronsList.add(\"0 0 0 25 12 ? *\"); // 圣诞节00:00\n\t\tcronsList.add(\"0 0 12 31 12 ? *\"); // 新年前夜12:00\n\t\tcronsList.add(\"0 0 0 14 2 ? *\"); // 情人节00:00\n\t\tcronsList.add(\"0 0 10 1 5 ? *\"); // 劳动节10:00\n\t\tcronsList.add(\"0 0 9 8 3 ? *\"); // 妇女节09:00\n\t\tcronsList.add(\"0 0 0 1 4 ? *\"); // 愚人节00:00\n\t\tcronsList.add(\"0 0 12 4 7 ? *\"); // 美国独立日12:00\n\t\tcronsList.add(\"0 0 0 31 10 ? *\"); // 万圣节00:00\n\t\tcronsList.add(\"0 7,19,31,43,55 * * * ? *\"); // 特定分钟\n\t\tcronsList.add(\"0 */7 * * * ? *\"); // 每7分钟\n\t\tcronsList.add(\"0 15-45/5 * * * ? *\"); // 每小时的15-45分之间每5分钟\n\t\tcronsList.add(\"0 0-30/2 * * * ? *\"); // 每小时前30分钟每2分钟\n\t\tcronsList.add(\"0 45 23 * * ? *\"); // 每天23:45\n\t\tcronsList.add(\"0 59 23 * * ? *\"); // 每天23:59\n\t\tcronsList.add(\"0 0 */3 * * ? *\"); // 每3小时\n\t\tcronsList.add(\"0 0 9-18/2 * * ? *\"); // 9点到18点每2小时\n\t\tcronsList.add(\"0 0 22-2 * * ? *\"); // 22点到次日2点每小时\n\t\tcronsList.add(\"0 30 16 L * ? *\"); // 每月最后一天16:30\n\n\n\t\t// 2. 测试时间 (50个)\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-02-01 18:20:10\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-02-29 10:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-12-31 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-06-15 12:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-30 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-02-28 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-31 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-04-30 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-06-30 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-09-30 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2026-01-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-02-28 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-02-29 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-02-29 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2023-02-28 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2028-02-29 12:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-06-15 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-06-15 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-31 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-04-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-07-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-10-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-06 09:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-10 17:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-11 12:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-01-12 12:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-09 01:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-09 03:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-11-02 01:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-11-02 01:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-12-31 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2024-01-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2026-12-31 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2026-01-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-05-15 08:45:30\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-08-22 14:20:15\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-11-03 19:10:45\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-02-14 09:30:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-07-07 07:07:07\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-09-09 09:09:09\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-10-10 10:10:10\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-12-12 12:12:12\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-03-03 03:03:03\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-06-06 06:06:06\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-04-16 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-04-30 23:59:59\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-05-01 00:00:00\"));\n\t\tjudgeTimes.add(DateUtil.parse(\"2025-05-01 00:00:01\"));\n\n\t\t// 3. 计算并比对结果\n\t\tfor (final String cron : cronsList) {\n\t\t\tfinal CronPattern hutoolCorn = new CronPattern(cron);\n\t\t\tfinal CronExpression quartzCorn = new CronExpression(cron);\n\t\t\tfor (final DateTime judgeTime : judgeTimes) {\n\t\t\t\tfinal Date quartzDate = quartzCorn.getNextValidTimeAfter(judgeTime);\n\t\t\t\tfinal Date hutoolDate = CronPatternUtil.nextDateAfter(hutoolCorn, judgeTime);\n\t\t\t\tAssertions.assertEquals(quartzDate, hutoolDate);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Test\n\tvoid issue4056Test() {\n\t\tfinal String cron = \"0 0 0 1/3 * ? *\";\n\t\tfinal CronPattern hutoolCorn = new CronPattern(cron);\n\t\tfinal Date hutoolDate = CronPatternUtil.nextDateAfter(hutoolCorn, DateUtil.parse(\"2025-02-28 00:00:00\"));\n\t\tSystem.out.println(DateUtil.formatDateTime(hutoolDate));\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/IssueI7SMP7Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.cron.pattern;\n\nimport cn.hutool.cron.pattern.matcher.PatternMatcher;\nimport cn.hutool.cron.pattern.parser.PatternParser;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class IssueI7SMP7Test {\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal List<PatternMatcher> parse = PatternParser.parse(\"0 0 3 1 1 ? */1\");\n\t\tassertNotNull(parse);\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/IssueI82CSHTest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\nimport java.util.List;\n\npublic class IssueI82CSHTest {\n\n\t@Test\n\tpublic void test() {\n\t\tfinal DateTime begin = DateUtil.parse(\"2023-09-20\");\n\t\tfinal DateTime end = DateUtil.parse(\"2025-09-20\");\n\t\tfinal List<Date> dates = CronPatternUtil.matchedDates(\"0 0 1 3-3,9 *\", begin, end, 20, false);\n\t\t//dates.forEach(Console::log);\n\t\tassertEquals(4,  dates.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/pattern/IssueI9FQUATest.java",
    "content": "package cn.hutool.cron.pattern;\n\nimport cn.hutool.core.date.DateUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Calendar;\n\npublic class IssueI9FQUATest {\n\t@Test\n\tpublic void nextDateAfterTest() {\n\t\tfinal String cron = \"0/5 * * * * ?\";\n\t\tfinal Calendar calendar = CronPattern.of(cron).nextMatchAfter(\n\t\t\tDateUtil.parse(\"2024-01-01 00:00:00\").toCalendar());\n\n\t\t//Console.log(DateUtil.date(calendar));\n\t\tassertEquals(\"2024-01-01 00:00:05\", DateUtil.date(calendar).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/java/cn/hutool/cron/timingwheel/Issue3090Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.cron.timingwheel;\n\nimport cn.hutool.core.lang.Console;\n\npublic class Issue3090Test {\n\tpublic static void main(String[] args) {\n\t\tfinal SystemTimer timer = new SystemTimer();\n\t\ttimer.setDelayQueueTimeout(1000);\n\t\ttimer.start();\n\t\ttimer.addTask(new TimerTask(() -> {\n\t\t\tConsole.log(1);\n\t\t\tConsole.log(\"任务已经完成\");\n\t\t}, 1000));\n\n\t\ttimer.stop();\n\t\tConsole.log(\"线程池已经关闭\");\n\t}\n}\n"
  },
  {
    "path": "hutool-cron/src/test/resources/config/cron.setting",
    "content": "#------------------------------------------------------------------\n# 定时任务配置文件\n# 定时任务表达分为以下几种情况：\n# 1. 表达式为5位，此时兼容Linux的Crontab模式，第一位匹配分，此时如果为秒匹配模式，则秒部分为固定值（取决于加入表达式时当前时间秒数）\n# 2. 表达式为6位，此时兼容Quartz模式，第一位匹配秒，但是只有秒匹配模式时秒部分定义才有效\n# 3. 表达式为7位，此时兼容Quartz模式，第一位匹配秒，最后一位匹配年\n#------------------------------------------------------------------\n\n# cn.hutool.cron.demo.TestJob.doTest = */1 * * * * *\n\n[cn.hutool.cron.demo]\n# 6位表达式在秒匹配模式下可用，此处表示每秒执行一次\n# TestJob.doTest = */1 * * * * *\n# 5位表达式在分匹配模式下可用，此处表示每分钟执行一次\n# 如果此时为秒匹配模式，则秒部分为固定数字（此秒取决于加入表达式当前时间的秒数）\nTestJob.doTest = 0/30 * 8-18 * * ?\nTestJob2.doTest = */3 * * * * *\n"
  },
  {
    "path": "hutool-crypto/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-crypto</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 加密解密</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.crypto</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.bouncycastle</groupId>\n\t\t\t<artifactId>bcpkix-jdk18on</artifactId>\n\t\t\t<version>${bouncycastle.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/ASN1Util.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.io.IORuntimeException;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1Encoding;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.ASN1Object;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.BERSequence;\nimport org.bouncycastle.asn1.DERSequence;\nimport org.bouncycastle.asn1.DLSequence;\nimport org.bouncycastle.asn1.util.ASN1Dump;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * ASN.1 – Abstract Syntax Notation dot one，抽象记法1 工具类。<br>\n * ASN.1描述了一种对数据进行表示、编码、传输和解码的数据格式。它的编码格式包括DER、BER、DL等<br>\n *\n * @author looly\n * @since 5.7.10\n */\npublic class ASN1Util {\n\n\t/**\n\t * 编码为DER格式\n\t *\n\t * @param elements ASN.1元素\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encodeDer(ASN1Encodable... elements) {\n\t\treturn encode(ASN1Encoding.DER, elements);\n\t}\n\n\t/**\n\t * 编码为指定ASN1格式\n\t *\n\t * @param asn1Encoding 编码格式，见{@link ASN1Encoding}，可选DER、BER或DL\n\t * @param elements     ASN.1元素\n\t * @return 编码后的bytes\n\t */\n\tpublic static byte[] encode(String asn1Encoding, ASN1Encodable... elements) {\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream();\n\t\tencodeTo(asn1Encoding, out, elements);\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 编码为指定ASN1格式\n\t *\n\t * @param asn1Encoding 编码格式，见{@link ASN1Encoding}，可选DER、BER或DL\n\t * @param out          输出流\n\t * @param elements     ASN.1元素\n\t */\n\tpublic static void encodeTo(String asn1Encoding, OutputStream out, ASN1Encodable... elements) {\n\t\tASN1Sequence sequence;\n\t\tswitch (asn1Encoding) {\n\t\t\tcase ASN1Encoding.DER:\n\t\t\t\tsequence = new DERSequence(elements);\n\t\t\t\tbreak;\n\t\t\tcase ASN1Encoding.BER:\n\t\t\t\tsequence = new BERSequence(elements);\n\t\t\t\tbreak;\n\t\t\tcase ASN1Encoding.DL:\n\t\t\t\tsequence = new DLSequence(elements);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new CryptoException(\"Unsupported ASN1 encoding: {}\", asn1Encoding);\n\t\t}\n\t\ttry {\n\t\t\tsequence.encodeTo(out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取ASN.1数据流为{@link ASN1Object}\n\t *\n\t * @param in ASN.1数据\n\t * @return {@link ASN1Object}\n\t */\n\tpublic static ASN1Object decode(InputStream in) {\n\t\tfinal ASN1InputStream asn1In = new ASN1InputStream(in);\n\t\ttry {\n\t\t\treturn asn1In.readObject();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取ASN1格式的导出格式，一般用于调试\n\t *\n\t * @param in ASN.1数据\n\t * @return {@link ASN1Object}的字符串表示形式\n\t * @see ASN1Dump#dumpAsString(Object)\n\t */\n\tpublic static String getDumpStr(InputStream in) {\n\t\treturn ASN1Dump.dumpAsString(decode(in));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport org.bouncycastle.asn1.pkcs.PrivateKeyInfo;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\nimport org.bouncycastle.asn1.x9.X9ECParameters;\nimport org.bouncycastle.crypto.params.AsymmetricKeyParameter;\nimport org.bouncycastle.crypto.params.ECDomainParameters;\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.crypto.params.ECPublicKeyParameters;\nimport org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;\nimport org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;\nimport org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;\nimport org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;\nimport org.bouncycastle.jce.spec.ECNamedCurveSpec;\nimport org.bouncycastle.jce.spec.ECParameterSpec;\nimport org.bouncycastle.jce.spec.ECPrivateKeySpec;\nimport org.bouncycastle.math.ec.ECCurve;\nimport org.bouncycastle.util.BigIntegers;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.math.BigInteger;\nimport java.security.Key;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.spec.ECPoint;\nimport java.security.spec.ECPublicKeySpec;\n\n/**\n * Bouncy Castle相关工具类封装\n *\n * @author looly\n * @since 4.5.0\n */\npublic class BCUtil {\n\n\t/**\n\t * 只获取私钥里的d，32字节\n\t *\n\t * @param privateKey {@link PublicKey}，必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey\n\t * @return 压缩得到的X\n\t * @since 5.1.6\n\t */\n\tpublic static byte[] encodeECPrivateKey(PrivateKey privateKey) {\n\t\treturn ((BCECPrivateKey) privateKey).getD().toByteArray();\n\t}\n\n\t/**\n\t * 解码恢复EC私钥,支持Base64和Hex编码,（基于BouncyCastle）\n\t *\n\t * @param d         私钥d值\n\t * @param curveName EC曲线名\n\t * @return 私钥\n\t * @since 5.8.36\n\t */\n\tpublic static PrivateKey decodeECPrivateKey(final byte[] d, final String curveName) {\n\t\tfinal X9ECParameters x9ECParameters = ECUtil.getNamedCurveByName(curveName);\n\t\tfinal ECParameterSpec ecSpec = new ECParameterSpec(\n\t\t\tx9ECParameters.getCurve(),\n\t\t\tx9ECParameters.getG(),\n\t\t\tx9ECParameters.getN(),\n\t\t\tx9ECParameters.getH()\n\t\t);\n\n\t\tfinal ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(BigIntegers.fromUnsignedByteArray(d), ecSpec);\n\t\treturn KeyUtil.generatePrivateKey(\"EC\", privateKeySpec);\n\t}\n\n\t/**\n\t * 编码压缩EC公钥（基于BouncyCastle），即Q值<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param publicKey {@link PublicKey}，必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey\n\t * @return 压缩得到的Q\n\t * @since 4.4.4\n\t */\n\tpublic static byte[] encodeECPublicKey(PublicKey publicKey) {\n\t\treturn encodeECPublicKey(publicKey, true);\n\t}\n\n\t/**\n\t * 编码压缩EC公钥（基于BouncyCastle），即Q值<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param publicKey {@link PublicKey}，必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey\n\t * @param isCompressed 是否压缩\n\t * @return 得到的Q\n\t * @since 5.5.9\n\t */\n\tpublic static byte[] encodeECPublicKey(PublicKey publicKey, boolean isCompressed) {\n\t\treturn ((BCECPublicKey) publicKey).getQ().getEncoded(isCompressed);\n\t}\n\n\t/**\n\t * 解码恢复EC压缩公钥,支持Base64和Hex编码,（基于BouncyCastle）<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param encode    压缩公钥\n\t * @param curveName EC曲线名\n\t * @return 公钥\n\t * @since 4.4.4\n\t */\n\tpublic static PublicKey decodeECPoint(String encode, String curveName) {\n\t\treturn decodeECPoint(SecureUtil.decode(encode), curveName);\n\t}\n\n\t/**\n\t * 解码恢复EC压缩公钥,支持Base64和Hex编码,（基于BouncyCastle）\n\t *\n\t * @param encodeByte 压缩公钥\n\t * @param curveName  EC曲线名，例如{@link SmUtil#SM2_DOMAIN_PARAMS}\n\t * @return 公钥\n\t * @since 4.4.4\n\t */\n\tpublic static PublicKey decodeECPoint(byte[] encodeByte, String curveName) {\n\t\tfinal X9ECParameters x9ECParameters = ECUtil.getNamedCurveByName(curveName);\n\t\tfinal ECCurve curve = x9ECParameters.getCurve();\n\t\tfinal ECPoint point = EC5Util.convertPoint(curve.decodePoint(encodeByte));\n\n\t\t// 根据曲线恢复公钥格式\n\t\tfinal ECNamedCurveSpec ecSpec = new ECNamedCurveSpec(curveName, curve, x9ECParameters.getG(), x9ECParameters.getN());\n\t\treturn KeyUtil.generatePublicKey(\"EC\", new ECPublicKeySpec(point, ecSpec));\n\t}\n\n\t/**\n\t * 构建ECDomainParameters对象\n\t *\n\t * @param parameterSpec ECParameterSpec\n\t * @return {@link ECDomainParameters}\n\t * @since 5.2.0\n\t */\n\tpublic static ECDomainParameters toDomainParams(ECParameterSpec parameterSpec) {\n\t\treturn new ECDomainParameters(\n\t\t\t\tparameterSpec.getCurve(),\n\t\t\t\tparameterSpec.getG(),\n\t\t\t\tparameterSpec.getN(),\n\t\t\t\tparameterSpec.getH());\n\t}\n\n\t/**\n\t * 构建ECDomainParameters对象\n\t *\n\t * @param curveName Curve名称\n\t * @return {@link ECDomainParameters}\n\t * @since 5.2.0\n\t */\n\tpublic static ECDomainParameters toDomainParams(String curveName) {\n\t\treturn toDomainParams(ECUtil.getNamedCurveByName(curveName));\n\t}\n\n\t/**\n\t * 构建ECDomainParameters对象\n\t *\n\t * @param x9ECParameters {@link X9ECParameters}\n\t * @return {@link ECDomainParameters}\n\t * @since 5.2.0\n\t */\n\tpublic static ECDomainParameters toDomainParams(X9ECParameters x9ECParameters) {\n\t\treturn new ECDomainParameters(\n\t\t\t\tx9ECParameters.getCurve(),\n\t\t\t\tx9ECParameters.getG(),\n\t\t\t\tx9ECParameters.getN(),\n\t\t\t\tx9ECParameters.getH()\n\t\t);\n\t}\n\n\t/**\n\t * 密钥转换为AsymmetricKeyParameter\n\t *\n\t * @param key PrivateKey或者PublicKey\n\t * @return ECPrivateKeyParameters或者ECPublicKeyParameters\n\t * @since 5.2.0\n\t */\n\tpublic static AsymmetricKeyParameter toParams(Key key) {\n\t\treturn ECKeyUtil.toParams(key);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2Params(String d) {\n\t\treturn ECKeyUtil.toSm2PrivateParams(d);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param dHex             私钥d值16进制字符串\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toParams(String dHex, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPrivateParams(dHex, domainParameters);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2Params(byte[] d) {\n\t\treturn ECKeyUtil.toSm2PrivateParams(d);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d                私钥d值\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toParams(byte[] d, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPrivateParams(d, domainParameters);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2Params(BigInteger d) {\n\t\treturn ECKeyUtil.toSm2PrivateParams(d);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d                私钥d值\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toParams(BigInteger d, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPrivateParams(d, domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param x                公钥X\n\t * @param y                公钥Y\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toParams(BigInteger x, BigInteger y, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPublicParams(x, y, domainParameters);\n\t}\n\n\t/**\n\t * 转换为SM2的ECPublicKeyParameters\n\t *\n\t * @param xHex 公钥X\n\t * @param yHex 公钥Y\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2Params(String xHex, String yHex) {\n\t\treturn ECKeyUtil.toSm2PublicParams(xHex, yHex);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param xHex             公钥X\n\t * @param yHex             公钥Y\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toParams(String xHex, String yHex, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPublicParams(xHex, yHex, domainParameters);\n\t}\n\n\t/**\n\t * 转换为SM2的ECPublicKeyParameters\n\t *\n\t * @param xBytes 公钥X\n\t * @param yBytes 公钥Y\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2Params(byte[] xBytes, byte[] yBytes) {\n\t\treturn ECKeyUtil.toSm2PublicParams(xBytes, yBytes);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param xBytes           公钥X\n\t * @param yBytes           公钥Y\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toParams(byte[] xBytes, byte[] yBytes, ECDomainParameters domainParameters) {\n\t\treturn ECKeyUtil.toPublicParams(xBytes, yBytes, domainParameters);\n\t}\n\n\t/**\n\t * 公钥转换为 {@link ECPublicKeyParameters}\n\t *\n\t * @param publicKey 公钥，传入null返回null\n\t * @return {@link ECPublicKeyParameters}或null\n\t */\n\tpublic static ECPublicKeyParameters toParams(PublicKey publicKey) {\n\t\treturn ECKeyUtil.toPublicParams(publicKey);\n\t}\n\n\t/**\n\t * 私钥转换为 {@link ECPrivateKeyParameters}\n\t *\n\t * @param privateKey 私钥，传入null返回null\n\t * @return {@link ECPrivateKeyParameters}或null\n\t */\n\tpublic static ECPrivateKeyParameters toParams(PrivateKey privateKey) {\n\t\treturn ECKeyUtil.toPrivateParams(privateKey);\n\t}\n\n\t/**\n\t * 读取PEM格式的私钥\n\t *\n\t * @param pemStream pem流\n\t * @return {@link PrivateKey}\n\t * @since 5.2.5\n\t * @see PemUtil#readPemPrivateKey(InputStream)\n\t */\n\tpublic static PrivateKey readPemPrivateKey(InputStream pemStream) {\n\t\treturn PemUtil.readPemPrivateKey(pemStream);\n\t}\n\n\t/**\n\t * 读取PEM格式的公钥\n\t *\n\t * @param pemStream pem流\n\t * @return {@link PublicKey}\n\t * @since 5.2.5\n\t * @see PemUtil#readPemPublicKey(InputStream)\n\t */\n\tpublic static PublicKey readPemPublicKey(InputStream pemStream) {\n\t\treturn PemUtil.readPemPublicKey(pemStream);\n\t}\n\n\t/**\n\t * Java中的PKCS#8格式私钥转换为OpenSSL支持的PKCS#1格式\n\t *\n\t * @param privateKey PKCS#8格式私钥\n\t * @return PKCS#1格式私钥\n\t * @since 5.5.9\n\t */\n\tpublic static byte[] toPkcs1(PrivateKey privateKey){\n\t\tfinal PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(privateKey.getEncoded());\n\t\ttry {\n\t\t\treturn pkInfo.parsePrivateKey().toASN1Primitive().getEncoded();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * Java中的X.509格式公钥转换为OpenSSL支持的PKCS#1格式\n\t *\n\t * @param publicKey X.509格式公钥\n\t * @return PKCS#1格式公钥\n\t * @since 5.5.9\n\t */\n\tpublic static byte[] toPkcs1(PublicKey publicKey){\n\t\tfinal SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo\n\t\t\t\t.getInstance(publicKey.getEncoded());\n\t\ttry {\n\t\t\treturn spkInfo.parsePublicKey().getEncoded();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/CipherMode.java",
    "content": "package cn.hutool.crypto;\n\nimport javax.crypto.Cipher;\n\n/**\n * Cipher模式的枚举封装\n *\n * @author looly\n * @since 5.4.3\n */\npublic enum CipherMode {\n\t/**\n\t * 加密模式\n\t */\n\tencrypt(Cipher.ENCRYPT_MODE),\n\t/**\n\t * 解密模式\n\t */\n\tdecrypt(Cipher.DECRYPT_MODE),\n\t/**\n\t * 包装模式\n\t */\n\twrap(Cipher.WRAP_MODE),\n\t/**\n\t * 拆包模式\n\t */\n\tunwrap(Cipher.UNWRAP_MODE);\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 见{@link Cipher}\n\t */\n\tCipherMode(int value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final int value;\n\n\t/**\n\t * 获取枚举值对应的int表示\n\t *\n\t * @return 枚举值对应的int表示\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/CipherWrapper.java",
    "content": "package cn.hutool.crypto;\n\nimport javax.crypto.Cipher;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.Key;\nimport java.security.SecureRandom;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * {@link Cipher}包装类，提供初始化模式等额外方法<br>\n * 包装之后可提供自定义或默认的：\n * <ul>\n *     <li>{@link AlgorithmParameterSpec}</li>\n *     <li>{@link SecureRandom}</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.17\n */\npublic class CipherWrapper {\n\n\tprivate final Cipher cipher;\n\t/**\n\t * 算法参数\n\t */\n\tprivate AlgorithmParameterSpec params;\n\t/**\n\t * 随机数生成器，可自定义随机数种子\n\t */\n\tprivate SecureRandom random;\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法名称\n\t */\n\tpublic CipherWrapper(String algorithm) {\n\t\tthis(SecureUtil.createCipher(algorithm));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param cipher {@link Cipher}\n\t */\n\tpublic CipherWrapper(Cipher cipher) {\n\t\tthis.cipher = cipher;\n\t}\n\n\t/**\n\t * 获取{@link AlgorithmParameterSpec}<br>\n\t * 在某些算法中，需要特别的参数，例如在ECIES中，此处为IESParameterSpec\n\t *\n\t * @return {@link AlgorithmParameterSpec}\n\t */\n\tpublic AlgorithmParameterSpec getParams() {\n\t\treturn this.params;\n\t}\n\n\t/**\n\t * 设置 {@link AlgorithmParameterSpec}，通常用于加盐或偏移向量\n\t *\n\t * @param params {@link AlgorithmParameterSpec}\n\t * @return this\n\t */\n\tpublic CipherWrapper setParams(AlgorithmParameterSpec params) {\n\t\tthis.params = params;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置随机数生成器，可自定义随机数种子\n\t *\n\t * @param random 随机数生成器，可自定义随机数种子\n\t * @return this\n\t */\n\tpublic CipherWrapper setRandom(SecureRandom random) {\n\t\tthis.random = random;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取被包装的{@link Cipher}\n\t *\n\t * @return {@link Cipher}\n\t */\n\tpublic Cipher getCipher() {\n\t\treturn this.cipher;\n\t}\n\n\t/**\n\t * 初始化{@link Cipher}为加密或者解密模式\n\t *\n\t * @param mode 模式，见{@link Cipher#ENCRYPT_MODE} 或 {@link Cipher#DECRYPT_MODE}\n\t * @param key  密钥\n\t * @return this\n\t * @throws InvalidKeyException                无效key\n\t * @throws InvalidAlgorithmParameterException 无效算法\n\t */\n\tpublic CipherWrapper initMode(int mode, Key key)\n\t\t\tthrows InvalidKeyException, InvalidAlgorithmParameterException {\n\t\tfinal Cipher cipher = this.cipher;\n\t\tfinal AlgorithmParameterSpec params = this.params;\n\t\tfinal SecureRandom random = this.random;\n\t\tif (null != params) {\n\t\t\tif (null != random) {\n\t\t\t\tcipher.init(mode, key, params, random);\n\t\t\t} else {\n\t\t\t\tcipher.init(mode, key, params);\n\t\t\t}\n\t\t} else {\n\t\t\tif (null != random) {\n\t\t\t\tcipher.init(mode, key, random);\n\t\t\t} else {\n\t\t\t\tcipher.init(mode, key);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/CryptoException.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 加密异常\n * @author Looly\n *\n */\npublic class CryptoException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8068509879445395353L;\n\n\tpublic CryptoException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic CryptoException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic CryptoException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic CryptoException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic CryptoException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic CryptoException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/ECKeyUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport org.bouncycastle.asn1.pkcs.PrivateKeyInfo;\nimport org.bouncycastle.asn1.sec.ECPrivateKey;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\nimport org.bouncycastle.asn1.x9.X9ObjectIdentifiers;\nimport org.bouncycastle.crypto.params.AsymmetricKeyParameter;\nimport org.bouncycastle.crypto.params.ECDomainParameters;\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.crypto.params.ECPublicKeyParameters;\nimport org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;\nimport org.bouncycastle.jcajce.spec.OpenSSHPrivateKeySpec;\nimport org.bouncycastle.jcajce.spec.OpenSSHPublicKeySpec;\nimport org.bouncycastle.math.ec.ECCurve;\nimport org.bouncycastle.math.ec.ECPoint;\nimport org.bouncycastle.math.ec.FixedPointCombMultiplier;\nimport org.bouncycastle.util.BigIntegers;\n\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.security.InvalidKeyException;\nimport java.security.Key;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.spec.KeySpec;\n\n/**\n * EC密钥参数相关工具类封装\n *\n * @author looly\n * @since 5.4.3\n */\npublic class ECKeyUtil {\n\n\t/**\n\t * 密钥转换为AsymmetricKeyParameter\n\t *\n\t * @param key PrivateKey或者PublicKey\n\t * @return ECPrivateKeyParameters或者ECPublicKeyParameters\n\t */\n\tpublic static AsymmetricKeyParameter toParams(Key key) {\n\t\tif (key instanceof PrivateKey) {\n\t\t\treturn toPrivateParams((PrivateKey) key);\n\t\t} else if (key instanceof PublicKey) {\n\t\t\treturn toPublicParams((PublicKey) key);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据私钥参数获取公钥参数\n\t *\n\t * @param privateKeyParameters 私钥参数\n\t * @return 公钥参数\n\t * @since 5.5.9\n\t */\n\tpublic static ECPublicKeyParameters getPublicParams(ECPrivateKeyParameters privateKeyParameters) {\n\t\tfinal ECDomainParameters domainParameters = privateKeyParameters.getParameters();\n\t\tfinal ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), privateKeyParameters.getD());\n\t\treturn new ECPublicKeyParameters(q, domainParameters);\n\t}\n\n\t//--------------------------------------------------------------------------- Public Key\n\n\t/**\n\t * 转换为 ECPublicKeyParameters\n\t *\n\t * @param q 公钥Q值\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2PublicParams(byte[] q) {\n\t\treturn toPublicParams(q, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为 ECPublicKeyParameters\n\t *\n\t * @param q 公钥Q值\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2PublicParams(String q) {\n\t\treturn toPublicParams(q, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为SM2的ECPublicKeyParameters\n\t *\n\t * @param x 公钥X\n\t * @param y 公钥Y\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2PublicParams(String x, String y) {\n\t\treturn toPublicParams(x, y, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为SM2的ECPublicKeyParameters\n\t *\n\t * @param xBytes 公钥X\n\t * @param yBytes 公钥Y\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toSm2PublicParams(byte[] xBytes, byte[] yBytes) {\n\t\treturn toPublicParams(xBytes, yBytes, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param x                公钥X\n\t * @param y                公钥Y\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters，x或y为{@code null}则返回{@code null}\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(String x, String y, ECDomainParameters domainParameters) {\n\t\treturn toPublicParams(SecureUtil.decode(x), SecureUtil.decode(y), domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param xBytes           公钥X\n\t * @param yBytes           公钥Y\n\t * @param domainParameters ECDomainParameters曲线参数\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(byte[] xBytes, byte[] yBytes, ECDomainParameters domainParameters) {\n\t\tif (null == xBytes || null == yBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn toPublicParams(BigIntegers.fromUnsignedByteArray(xBytes), BigIntegers.fromUnsignedByteArray(yBytes), domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param x                公钥X\n\t * @param y                公钥Y\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(BigInteger x, BigInteger y, ECDomainParameters domainParameters) {\n\t\tif (null == x || null == y) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal ECCurve curve = domainParameters.getCurve();\n\t\treturn toPublicParams(curve.createPoint(x, y), domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param pointEncoded     被编码的曲线坐标点\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t * @since 5.4.3\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(String pointEncoded, ECDomainParameters domainParameters) {\n\t\tfinal ECCurve curve = domainParameters.getCurve();\n\t\treturn toPublicParams(curve.decodePoint(SecureUtil.decode(pointEncoded)), domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param pointEncoded     被编码的曲线坐标点\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t * @since 5.4.3\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(byte[] pointEncoded, ECDomainParameters domainParameters) {\n\t\tfinal ECCurve curve = domainParameters.getCurve();\n\t\treturn toPublicParams(curve.decodePoint(pointEncoded), domainParameters);\n\t}\n\n\t/**\n\t * 转换为ECPublicKeyParameters\n\t *\n\t * @param point            曲线坐标点\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPublicKeyParameters\n\t * @since 5.4.3\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(org.bouncycastle.math.ec.ECPoint point, ECDomainParameters domainParameters) {\n\t\treturn new ECPublicKeyParameters(point, domainParameters);\n\t}\n\n\t/**\n\t * 公钥转换为 {@link ECPublicKeyParameters}\n\t *\n\t * @param publicKey 公钥，传入null返回null\n\t * @return {@link ECPublicKeyParameters}或null\n\t */\n\tpublic static ECPublicKeyParameters toPublicParams(PublicKey publicKey) {\n\t\tif (null == publicKey) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(publicKey);\n\t\t} catch (InvalidKeyException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t//--------------------------------------------------------------------------- Private Key\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值16进制字符串\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2PrivateParams(String d) {\n\t\treturn toPrivateParams(d, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2PrivateParams(byte[] d) {\n\t\treturn toPrivateParams(d, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d 私钥d值\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toSm2PrivateParams(BigInteger d) {\n\t\treturn toPrivateParams(d, SmUtil.SM2_DOMAIN_PARAMS);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d                私钥d值16进制字符串\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toPrivateParams(String d, ECDomainParameters domainParameters) {\n\t\tif (null == d) {\n\t\t\treturn null;\n\t\t}\n\t\treturn toPrivateParams(BigIntegers.fromUnsignedByteArray(SecureUtil.decode(d)), domainParameters);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d                私钥d值\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toPrivateParams(byte[] d, ECDomainParameters domainParameters) {\n\t\treturn toPrivateParams(BigIntegers.fromUnsignedByteArray(d), domainParameters);\n\t}\n\n\t/**\n\t * 转换为 ECPrivateKeyParameters\n\t *\n\t * @param d                私钥d值\n\t * @param domainParameters ECDomainParameters\n\t * @return ECPrivateKeyParameters\n\t */\n\tpublic static ECPrivateKeyParameters toPrivateParams(BigInteger d, ECDomainParameters domainParameters) {\n\t\tif (null == d) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new ECPrivateKeyParameters(d, domainParameters);\n\t}\n\n\t/**\n\t * 私钥转换为 {@link ECPrivateKeyParameters}\n\t *\n\t * @param privateKey 私钥，传入null返回null\n\t * @return {@link ECPrivateKeyParameters}或null\n\t */\n\tpublic static ECPrivateKeyParameters toPrivateParams(PrivateKey privateKey) {\n\t\tif (null == privateKey) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey);\n\t\t} catch (InvalidKeyException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将SM2算法的{@link ECPrivateKey} 转换为 {@link PrivateKey}\n\t *\n\t * @param privateKey {@link ECPrivateKey}\n\t * @return {@link PrivateKey}\n\t */\n\tpublic static PrivateKey toSm2PrivateKey(ECPrivateKey privateKey) {\n\t\ttry {\n\t\t\tfinal PrivateKeyInfo info = new PrivateKeyInfo(\n\t\t\t\t\tnew AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SmUtil.ID_SM2_PUBLIC_KEY_PARAM), privateKey);\n\t\t\treturn KeyUtil.generatePrivateKey(\"SM2\", info.getEncoded());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建{@link OpenSSHPrivateKeySpec}\n\t *\n\t * @param key 私钥，需为PKCS#1格式\n\t * @return {@link OpenSSHPrivateKeySpec}\n\t * @since 5.5.9\n\t */\n\tpublic static KeySpec createOpenSSHPrivateKeySpec(byte[] key) {\n\t\treturn new OpenSSHPrivateKeySpec(key);\n\t}\n\n\t/**\n\t * 创建{@link OpenSSHPublicKeySpec}\n\t *\n\t * @param key 公钥，需为PKCS#1格式\n\t * @return {@link OpenSSHPublicKeySpec}\n\t * @since 5.5.9\n\t */\n\tpublic static KeySpec createOpenSSHPublicKeySpec(byte[] key) {\n\t\treturn new OpenSSHPublicKeySpec(key);\n\t}\n\n\t/**\n\t * 尝试解析转换各种类型私钥为{@link ECPrivateKeyParameters}，支持包括：\n\t *\n\t * <ul>\n\t *     <li>D值</li>\n\t *     <li>PKCS#8</li>\n\t *     <li>PKCS#1</li>\n\t * </ul>\n\t *\n\t * @param privateKeyBytes 私钥\n\t * @return {@link ECPrivateKeyParameters}\n\t * @since 5.5.9\n\t */\n\tpublic static ECPrivateKeyParameters decodePrivateKeyParams(byte[] privateKeyBytes) {\n\t\ttry {\n\t\t\t// 尝试D值\n\t\t\treturn toSm2PrivateParams(privateKeyBytes);\n\t\t} catch (Exception ignore) {\n\t\t\t// ignore\n\t\t}\n\n\t\tPrivateKey privateKey;\n\t\t//尝试PKCS#8\n\t\ttry {\n\t\t\tprivateKey = KeyUtil.generatePrivateKey(\"sm2\", privateKeyBytes);\n\t\t} catch (Exception ignore) {\n\t\t\t// 尝试PKCS#1\n\t\t\tprivateKey = KeyUtil.generatePrivateKey(\"sm2\", createOpenSSHPrivateKeySpec(privateKeyBytes));\n\t\t}\n\n\t\treturn toPrivateParams(privateKey);\n\t}\n\n\t/**\n\t * 尝试解析转换各种类型公钥为{@link ECPublicKeyParameters}，支持包括：\n\t *\n\t * <ul>\n\t *     <li>Q值</li>\n\t *     <li>X.509</li>\n\t *     <li>PKCS#1</li>\n\t * </ul>\n\t *\n\t * @param publicKeyBytes 公钥\n\t * @return {@link ECPublicKeyParameters}\n\t * @since 5.5.9\n\t */\n\tpublic static ECPublicKeyParameters decodePublicKeyParams(byte[] publicKeyBytes) {\n\t\ttry {\n\t\t\t// 尝试Q值\n\t\t\treturn toSm2PublicParams(publicKeyBytes);\n\t\t} catch (Exception ignore) {\n\t\t\t// ignore\n\t\t}\n\n\t\tPublicKey publicKey;\n\t\t//尝试X.509\n\t\ttry {\n\t\t\tpublicKey = KeyUtil.generatePublicKey(\"sm2\", publicKeyBytes);\n\t\t} catch (Exception ignore) {\n\t\t\t// 尝试PKCS#1\n\t\t\tpublicKey = KeyUtil.generatePublicKey(\"sm2\", createOpenSSHPublicKeySpec(publicKeyBytes));\n\t\t}\n\n\t\treturn toPublicParams(publicKey);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/GlobalBouncyCastleProvider.java",
    "content": "package cn.hutool.crypto;\n\nimport java.security.Provider;\n\n/**\n * 全局单例的 org.bouncycastle.jce.provider.BouncyCastleProvider 对象\n *\n * @author looly\n */\npublic enum GlobalBouncyCastleProvider {\n\t/**\n\t * 单例\n\t */\n\tINSTANCE;\n\n\tprivate Provider provider;\n\tprivate static boolean useBouncyCastle = true;\n\n\tGlobalBouncyCastleProvider() {\n\t\ttry {\n\t\t\tthis.provider = ProviderFactory.createBouncyCastleProvider();\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t/**\n\t * 获取{@link Provider}\n\t *\n\t * @return {@link Provider}\n\t */\n\tpublic Provider getProvider() {\n\t\treturn useBouncyCastle ? this.provider : null;\n\t}\n\n\t/**\n\t * 设置是否使用Bouncy Castle库<br>\n\t * 如果设置为false，表示强制关闭Bouncy Castle而使用JDK\n\t *\n\t * @param isUseBouncyCastle 是否使用BouncyCastle库\n\t * @since 4.5.2\n\t */\n\tpublic static void setUseBouncyCastle(boolean isUseBouncyCastle) {\n\t\tuseBouncyCastle = isUseBouncyCastle;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;\nimport cn.hutool.crypto.symmetric.SymmetricAlgorithm;\n\nimport javax.crypto.KeyGenerator;\nimport javax.crypto.SecretKey;\nimport javax.crypto.SecretKeyFactory;\nimport javax.crypto.spec.DESKeySpec;\nimport javax.crypto.spec.DESedeKeySpec;\nimport javax.crypto.spec.PBEKeySpec;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.math.BigInteger;\nimport java.security.*;\nimport java.security.cert.Certificate;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.interfaces.RSAPrivateCrtKey;\nimport java.security.spec.*;\n\n/**\n * 密钥工具类\n *\n * <p>\n * 包括:\n * <pre>\n * 1、生成密钥（单密钥、密钥对）\n * 2、读取密钥文件\n * </pre>\n *\n * @author looly, Gsealy\n * @since 4.4.1\n */\npublic class KeyUtil {\n\n\t/**\n\t * Java密钥库(Java Key Store，JKS)KEY_STORE\n\t */\n\tpublic static final String KEY_TYPE_JKS = \"JKS\";\n\t/**\n\t * jceks\n\t */\n\tpublic static final String KEY_TYPE_JCEKS = \"jceks\";\n\t/**\n\t * PKCS12是公钥加密标准，它规定了可包含所有私钥、公钥和证书。其以二进制格式存储，也称为 PFX 文件\n\t */\n\tpublic static final String KEY_TYPE_PKCS12 = \"pkcs12\";\n\t/**\n\t * Certification类型：X.509\n\t */\n\tpublic static final String CERT_TYPE_X509 = \"X.509\";\n\n\t/**\n\t * 默认密钥字节数\n\t *\n\t * <pre>\n\t * RSA/DSA\n\t * Default Keysize 1024\n\t * Keysize must be a multiple of 64, ranging from 512 to 1024 (inclusive).\n\t * </pre>\n\t */\n\tpublic static final int DEFAULT_KEY_SIZE = 1024;\n\n\t/**\n\t * SM2默认曲线\n\t *\n\t * <pre>\n\t * Default SM2 curve\n\t * </pre>\n\t */\n\tpublic static final String SM2_DEFAULT_CURVE = SmUtil.SM2_CURVE_NAME;\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成\n\t *\n\t * @param algorithm 算法，支持PBE算法\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm) {\n\t\treturn generateKey(algorithm, -1);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成<br>\n\t * 当指定keySize&lt;0时，AES默认长度为128，其它算法不指定。\n\t *\n\t * @param algorithm 算法，支持PBE算法\n\t * @param keySize   密钥长度，&lt;0表示不设定密钥长度，即使用默认长度\n\t * @return {@link SecretKey}\n\t * @since 3.1.2\n\t */\n\tpublic static SecretKey generateKey(String algorithm, int keySize) {\n\t\treturn generateKey(algorithm, keySize, null);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成<br>\n\t * 当指定keySize&lt;0时，AES默认长度为128，其它算法不指定。\n\t *\n\t * @param algorithm 算法，支持PBE算法\n\t * @param keySize   密钥长度，&lt;0表示不设定密钥长度，即使用默认长度\n\t * @param random    随机数生成器，null表示默认\n\t * @return {@link SecretKey}\n\t * @since 5.5.2\n\t */\n\tpublic static SecretKey generateKey(String algorithm, int keySize, SecureRandom random) {\n\t\talgorithm = getMainAlgorithm(algorithm);\n\n\t\tfinal KeyGenerator keyGenerator = getKeyGenerator(algorithm);\n\t\tif (keySize <= 0 && SymmetricAlgorithm.AES.getValue().equals(algorithm)) {\n\t\t\t// 对于AES的密钥，除非指定，否则强制使用128位\n\t\t\tkeySize = 128;\n\t\t}\n\n\t\tif (keySize > 0) {\n\t\t\tif (null == random) {\n\t\t\t\tkeyGenerator.init(keySize);\n\t\t\t} else {\n\t\t\t\tkeyGenerator.init(keySize, random);\n\t\t\t}\n\t\t}\n\t\treturn keyGenerator.generateKey();\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥，如果为{@code null} 自动生成随机密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm, byte[] key) {\n\t\tAssert.notBlank(algorithm, \"Algorithm is blank!\");\n\t\tSecretKey secretKey;\n\t\tif (algorithm.startsWith(\"PBE\")) {\n\t\t\t// PBE密钥\n\t\t\tsecretKey = generatePBEKey(algorithm, (null == key) ? null : StrUtil.utf8Str(key).toCharArray());\n\t\t} else if (algorithm.startsWith(\"DES\")) {\n\t\t\t// DES密钥\n\t\t\tsecretKey = generateDESKey(algorithm, key);\n\t\t} else {\n\t\t\t// 其它算法密钥\n\t\t\tsecretKey = (null == key) ? generateKey(algorithm) : new SecretKeySpec(key, algorithm);\n\t\t}\n\t\treturn secretKey;\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}\n\t *\n\t * @param algorithm DES算法，包括DES、DESede等\n\t * @param key       密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateDESKey(String algorithm, byte[] key) {\n\t\tif (StrUtil.isBlank(algorithm) || false == algorithm.startsWith(\"DES\")) {\n\t\t\tthrow new CryptoException(\"Algorithm [{}] is not a DES algorithm!\", algorithm);\n\t\t}\n\n\t\tSecretKey secretKey;\n\t\tif (null == key) {\n\t\t\tsecretKey = generateKey(algorithm);\n\t\t} else {\n\t\t\tKeySpec keySpec;\n\t\t\ttry {\n\t\t\t\tif (algorithm.startsWith(\"DESede\")) {\n\t\t\t\t\t// DESede兼容\n\t\t\t\t\tkeySpec = new DESedeKeySpec(key);\n\t\t\t\t} else {\n\t\t\t\t\tkeySpec = new DESKeySpec(key);\n\t\t\t\t}\n\t\t\t} catch (InvalidKeyException e) {\n\t\t\t\tthrow new CryptoException(e);\n\t\t\t}\n\t\t\tsecretKey = generateKey(algorithm, keySpec);\n\t\t}\n\t\treturn secretKey;\n\t}\n\n\t/**\n\t * 生成PBE {@link SecretKey}\n\t *\n\t * @param algorithm PBE算法，包括：PBEWithMD5AndDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40等\n\t * @param key       密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generatePBEKey(String algorithm, char[] key) {\n\t\tif (StrUtil.isBlank(algorithm) || false == algorithm.startsWith(\"PBE\")) {\n\t\t\tthrow new CryptoException(\"Algorithm [{}] is not a PBE algorithm!\", algorithm);\n\t\t}\n\n\t\tif (null == key) {\n\t\t\tkey = RandomUtil.randomString(32).toCharArray();\n\t\t}\n\t\tPBEKeySpec keySpec = new PBEKeySpec(key);\n\t\treturn generateKey(algorithm, keySpec);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法\n\t *\n\t * @param algorithm 算法\n\t * @param keySpec   {@link KeySpec}\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm, KeySpec keySpec) {\n\t\tfinal SecretKeyFactory keyFactory = getSecretKeyFactory(algorithm);\n\t\ttry {\n\t\t\treturn keyFactory.generateSecret(keySpec);\n\t\t} catch (InvalidKeySpecException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 生成RSA私钥，仅用于非对称加密\n\t * 算法见：<a href=\"https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\">...</a>\n\t *\n\t * @param key 密钥，支持XML和Base64两种格式，XML为C#生成格式，见{@link SpecUtil#xmlToRSAPrivateCrtKeySpec(String)}\n\t * @return RSA私钥 {@link PrivateKey}\n\t * @since 7.0.0\n\t */\n\tpublic static PrivateKey generateRSAPrivateKey(String key) {\n\t\tAssert.notBlank(key, \"Key is blank!\");\n\t\tkey = StrUtil.trim(key);\n\t\tif(StrUtil.startWith(key, '<')){\n\t\t\treturn generateRSAPrivateKey(SpecUtil.xmlToRSAPrivateCrtKeySpec(key));\n\t\t}\n\n\t\treturn generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), Base64.decode(key));\n\t}\n\n\t/**\n\t * 生成RSA私钥，仅用于非对称加密<br>\n\t * 采用PKCS#8规范，此规范定义了私钥信息语法和加密私钥语法<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param key 密钥，必须为DER编码存储\n\t * @return RSA私钥 {@link PrivateKey}\n\t * @since 4.5.2\n\t */\n\tpublic static PrivateKey generateRSAPrivateKey(byte[] key) {\n\t\treturn generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), key);\n\t}\n\n\t/**\n\t * 生成RSA私钥，仅用于非对称加密<br>\n\t * 算法见：<a href=\"https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\">...</a>\n\t *\n\t * @param keySpec {@link KeySpec}\n\t * @return RSA私钥 {@link PrivateKey}\n\t * @since 5.8.41\n\t */\n\tpublic static PrivateKey generateRSAPrivateKey(final KeySpec keySpec) {\n\t\treturn generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), keySpec);\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密<br>\n\t * 采用PKCS#8规范，此规范定义了私钥信息语法和加密私钥语法<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法，如RSA、EC、SM2等\n\t * @param key       密钥，PKCS#8格式\n\t * @return 私钥 {@link PrivateKey}\n\t */\n\tpublic static PrivateKey generatePrivateKey(String algorithm, byte[] key) {\n\t\tif (null == key) {\n\t\t\treturn null;\n\t\t}\n\t\treturn generatePrivateKey(algorithm, new PKCS8EncodedKeySpec(key));\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法，如RSA、EC、SM2等\n\t * @param keySpec   {@link KeySpec}\n\t * @return 私钥 {@link PrivateKey}\n\t * @since 3.1.1\n\t */\n\tpublic static PrivateKey generatePrivateKey(String algorithm, KeySpec keySpec) {\n\t\tif (null == keySpec) {\n\t\t\treturn null;\n\t\t}\n\t\talgorithm = getAlgorithmAfterWith(algorithm);\n\t\ttry {\n\t\t\treturn getKeyFactory(algorithm).generatePrivate(keySpec);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密\n\t *\n\t * @param keyStore {@link KeyStore}\n\t * @param alias    别名\n\t * @param password 密码\n\t * @return 私钥 {@link PrivateKey}\n\t */\n\tpublic static PrivateKey generatePrivateKey(KeyStore keyStore, String alias, char[] password) {\n\t\ttry {\n\t\t\treturn (PrivateKey) keyStore.getKey(alias, password);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 生成RSA公钥，仅用于非对称加密<br>\n\t * 采用X509证书规范<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param key 密钥，必须为DER编码存储\n\t * @return 公钥 {@link PublicKey}\n\t * @since 4.5.2\n\t */\n\tpublic static PublicKey generateRSAPublicKey(byte[] key) {\n\t\treturn generatePublicKey(AsymmetricAlgorithm.RSA.getValue(), key);\n\t}\n\n\t/**\n\t * 生成公钥，仅用于非对称加密<br>\n\t * 采用X509证书规范<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥，必须为DER编码存储\n\t * @return 公钥 {@link PublicKey}\n\t */\n\tpublic static PublicKey generatePublicKey(String algorithm, byte[] key) {\n\t\tif (null == key) {\n\t\t\treturn null;\n\t\t}\n\t\treturn generatePublicKey(algorithm, new X509EncodedKeySpec(key));\n\t}\n\n\t/**\n\t * 生成公钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param keySpec   {@link KeySpec}\n\t * @return 公钥 {@link PublicKey}\n\t * @since 3.1.1\n\t */\n\tpublic static PublicKey generatePublicKey(String algorithm, KeySpec keySpec) {\n\t\tif (null == keySpec) {\n\t\t\treturn null;\n\t\t}\n\t\talgorithm = getAlgorithmAfterWith(algorithm);\n\t\ttry {\n\t\t\treturn getKeyFactory(algorithm).generatePublic(keySpec);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥，仅用于非对称加密<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm) {\n\t\tint keySize = DEFAULT_KEY_SIZE;\n\t\tif (\"ECIES\".equalsIgnoreCase(algorithm)) {\n\t\t\t// ECIES算法对KEY的长度有要求，此处默认256\n\t\t\tkeySize = 256;\n\t\t}\n\n\t\treturn generateKeyPair(algorithm, keySize);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize) {\n\t\treturn generateKeyPair(algorithm, keySize, null);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度\n\t * @param seed      种子\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed) {\n\t\t// SM2算法需要单独定义其曲线生成\n\t\tif (\"SM2\".equalsIgnoreCase(algorithm)) {\n\t\t\tfinal ECGenParameterSpec sm2p256v1 = new ECGenParameterSpec(SM2_DEFAULT_CURVE);\n\t\t\treturn generateKeyPair(algorithm, keySize, seed, sm2p256v1);\n\t\t}\n\n\t\treturn generateKeyPair(algorithm, keySize, seed, (AlgorithmParameterSpec[]) null);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param params    {@link AlgorithmParameterSpec}\n\t * @return {@link KeyPair}\n\t * @since 4.3.3\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, AlgorithmParameterSpec params) {\n\t\treturn generateKeyPair(algorithm, null, params);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param param     {@link AlgorithmParameterSpec}\n\t * @param seed      种子\n\t * @return {@link KeyPair}\n\t * @since 4.3.3\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, byte[] seed, AlgorithmParameterSpec param) {\n\t\treturn generateKeyPair(algorithm, DEFAULT_KEY_SIZE, seed, param);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * <p>\n\t * 对于非对称加密算法，密钥长度有严格限制，具体如下：\n\t *\n\t * <p>\n\t * <b>RSA：</b>\n\t * <pre>\n\t * RS256、PS256：2048 bits\n\t * RS384、PS384：3072 bits\n\t * RS512、RS512：4096 bits\n\t * </pre>\n\t *\n\t * <p>\n\t * <b>EC（Elliptic Curve）：</b>\n\t * <pre>\n\t * EC256：256 bits\n\t * EC384：384 bits\n\t * EC512：512 bits\n\t * </pre>\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度（单位bit）\n\t * @param seed      种子\n\t * @param params    {@link AlgorithmParameterSpec}\n\t * @return {@link KeyPair}\n\t * @since 4.3.3\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed, AlgorithmParameterSpec... params) {\n\t\treturn generateKeyPair(algorithm, keySize, RandomUtil.createSecureRandom(seed), params);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * <p>\n\t * 对于非对称加密算法，密钥长度有严格限制，具体如下：\n\t *\n\t * <p>\n\t * <b>RSA：</b>\n\t * <pre>\n\t * RS256、PS256：2048 bits\n\t * RS384、PS384：3072 bits\n\t * RS512、RS512：4096 bits\n\t * </pre>\n\t *\n\t * <p>\n\t * <b>EC（Elliptic Curve）：</b>\n\t * <pre>\n\t * EC256：256 bits\n\t * EC384：384 bits\n\t * EC512：512 bits\n\t * </pre>\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度（单位bit）\n\t * @param random    {@link SecureRandom} 对象，创建时可选传入seed\n\t * @param params    {@link AlgorithmParameterSpec}\n\t * @return {@link KeyPair}\n\t * @since 4.6.5\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize, SecureRandom random, AlgorithmParameterSpec... params) {\n\t\talgorithm = getAlgorithmAfterWith(algorithm);\n\t\tfinal KeyPairGenerator keyPairGen = getKeyPairGenerator(algorithm);\n\n\t\t// 密钥模（modulus ）长度初始化定义\n\t\tif (keySize > 0) {\n\t\t\t// key长度适配修正\n\t\t\tif (\"EC\".equalsIgnoreCase(algorithm) && keySize > 256) {\n\t\t\t\t// 对于EC（EllipticCurve）算法，密钥长度有限制，在此使用默认256\n\t\t\t\tkeySize = 256;\n\t\t\t}\n\t\t\tif (null != random) {\n\t\t\t\tkeyPairGen.initialize(keySize, random);\n\t\t\t} else {\n\t\t\t\tkeyPairGen.initialize(keySize);\n\t\t\t}\n\t\t}\n\n\t\t// 自定义初始化参数\n\t\tif (ArrayUtil.isNotEmpty(params)) {\n\t\t\tfor (AlgorithmParameterSpec param : params) {\n\t\t\t\tif (null == param) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tif (null != random) {\n\t\t\t\t\t\tkeyPairGen.initialize(param, random);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tkeyPairGen.initialize(param);\n\t\t\t\t\t}\n\t\t\t\t} catch (InvalidAlgorithmParameterException e) {\n\t\t\t\t\tthrow new CryptoException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn keyPairGen.generateKeyPair();\n\t}\n\n\t/**\n\t * 获取{@link KeyPairGenerator}\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @return {@link KeyPairGenerator}\n\t * @since 4.4.3\n\t */\n\tpublic static KeyPairGenerator getKeyPairGenerator(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tKeyPairGenerator keyPairGen;\n\t\ttry {\n\t\t\tkeyPairGen = (null == provider) //\n\t\t\t\t\t? KeyPairGenerator.getInstance(getMainAlgorithm(algorithm)) //\n\t\t\t\t\t: KeyPairGenerator.getInstance(getMainAlgorithm(algorithm), provider);//\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn keyPairGen;\n\t}\n\n\t/**\n\t * 获取{@link KeyFactory}\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @return {@link KeyFactory}\n\t * @since 4.4.4\n\t */\n\tpublic static KeyFactory getKeyFactory(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tKeyFactory keyFactory;\n\t\ttry {\n\t\t\tkeyFactory = (null == provider) //\n\t\t\t\t\t? KeyFactory.getInstance(getMainAlgorithm(algorithm)) //\n\t\t\t\t\t: KeyFactory.getInstance(getMainAlgorithm(algorithm), provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn keyFactory;\n\t}\n\n\t/**\n\t * 获取{@link SecretKeyFactory}\n\t *\n\t * @param algorithm 对称加密算法\n\t * @return {@link KeyFactory}\n\t * @since 4.5.2\n\t */\n\tpublic static SecretKeyFactory getSecretKeyFactory(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tSecretKeyFactory keyFactory;\n\t\ttry {\n\t\t\tkeyFactory = (null == provider) //\n\t\t\t\t\t? SecretKeyFactory.getInstance(getMainAlgorithm(algorithm)) //\n\t\t\t\t\t: SecretKeyFactory.getInstance(getMainAlgorithm(algorithm), provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn keyFactory;\n\t}\n\n\t/**\n\t * 获取{@link KeyGenerator}\n\t *\n\t * @param algorithm 对称加密算法\n\t * @return {@link KeyGenerator}\n\t * @since 4.5.2\n\t */\n\tpublic static KeyGenerator getKeyGenerator(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tKeyGenerator generator;\n\t\ttry {\n\t\t\tgenerator = (null == provider) //\n\t\t\t\t\t? KeyGenerator.getInstance(getMainAlgorithm(algorithm)) //\n\t\t\t\t\t: KeyGenerator.getInstance(getMainAlgorithm(algorithm), provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn generator;\n\t}\n\n\t/**\n\t * 获取主体算法名，例如RSA/ECB/PKCS1Padding的主体算法是RSA\n\t *\n\t * @param algorithm XXXwithXXX算法\n\t * @return 主体算法名\n\t * @since 4.5.2\n\t */\n\tpublic static String getMainAlgorithm(String algorithm) {\n\t\tAssert.notBlank(algorithm, \"Algorithm must be not blank!\");\n\t\tfinal int slashIndex = algorithm.indexOf(CharUtil.SLASH);\n\t\tif (slashIndex > 0) {\n\t\t\treturn algorithm.substring(0, slashIndex);\n\t\t}\n\t\treturn algorithm;\n\t}\n\n\t/**\n\t * 获取用于密钥生成的算法<br>\n\t * 获取XXXwithXXX算法的后半部分算法，如果为ECDSA或SM2，返回算法为EC\n\t *\n\t * @param algorithm XXXwithXXX算法\n\t * @return 算法\n\t */\n\tpublic static String getAlgorithmAfterWith(String algorithm) {\n\t\tAssert.notNull(algorithm, \"algorithm must be not null !\");\n\n\t\tif (StrUtil.startWithIgnoreCase(algorithm, \"ECIESWith\")) {\n\t\t\treturn \"EC\";\n\t\t}\n\n\t\talgorithm = getMainAlgorithm(algorithm);\n\t\tint indexOfWith = StrUtil.lastIndexOfIgnoreCase(algorithm, \"with\");\n\t\tif (indexOfWith > 0) {\n\t\t\talgorithm = StrUtil.subSuf(algorithm, indexOfWith + \"with\".length());\n\t\t}\n\t\tif (\"ECDSA\".equalsIgnoreCase(algorithm)\n\t\t\t\t|| \"SM2\".equalsIgnoreCase(algorithm)\n\t\t\t\t|| \"ECIES\".equalsIgnoreCase(algorithm)\n\t\t) {\n\t\t\talgorithm = \"EC\";\n\t\t}\n\t\treturn algorithm;\n\t}\n\n\t/**\n\t * 读取密钥库(Java Key Store，JKS) KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param keyFile  证书文件\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t * @since 5.0.0\n\t */\n\tpublic static KeyStore readJKSKeyStore(File keyFile, char[] password) {\n\t\treturn readKeyStore(KEY_TYPE_JKS, keyFile, password);\n\t}\n\n\t/**\n\t * 读取密钥库(Java Key Store，JKS) KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t */\n\tpublic static KeyStore readJKSKeyStore(InputStream in, char[] password) {\n\t\treturn readKeyStore(KEY_TYPE_JKS, in, password);\n\t}\n\n\t/**\n\t * 读取PKCS12 KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存\n\t *\n\t * @param keyFile  证书文件\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t * @since 5.0.0\n\t */\n\tpublic static KeyStore readPKCS12KeyStore(File keyFile, char[] password) {\n\t\treturn readKeyStore(KEY_TYPE_PKCS12, keyFile, password);\n\t}\n\n\t/**\n\t * 读取PKCS12 KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存\n\t *\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t * @since 5.0.0\n\t */\n\tpublic static KeyStore readPKCS12KeyStore(InputStream in, char[] password) {\n\t\treturn readKeyStore(KEY_TYPE_PKCS12, in, password);\n\t}\n\n\t/**\n\t * 读取KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type     类型\n\t * @param keyFile  证书文件\n\t * @param password 密码，null表示无密码\n\t * @return {@link KeyStore}\n\t * @since 5.0.0\n\t */\n\tpublic static KeyStore readKeyStore(String type, File keyFile, char[] password) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(keyFile);\n\t\t\treturn readKeyStore(type, in, password);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 读取KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type     类型\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码，null表示无密码\n\t * @return {@link KeyStore}\n\t */\n\tpublic static KeyStore readKeyStore(String type, InputStream in, char[] password) {\n\t\tfinal KeyStore keyStore = getKeyStore(type);\n\t\ttry {\n\t\t\tkeyStore.load(in, password);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn keyStore;\n\t}\n\n\t/**\n\t * 获取{@link KeyStore}对象\n\t *\n\t * @param type 类型\n\t * @return {@link KeyStore}\n\t */\n\tpublic static KeyStore getKeyStore(final String type) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\t\ttry {\n\t\t\treturn null == provider ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider);\n\t\t} catch (final KeyStoreException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 从KeyStore中获取私钥公钥\n\t *\n\t * @param type     类型\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyPair}\n\t * @since 4.4.1\n\t */\n\tpublic static KeyPair getKeyPair(String type, InputStream in, char[] password, String alias) {\n\t\tfinal KeyStore keyStore = readKeyStore(type, in, password);\n\t\treturn getKeyPair(keyStore, password, alias);\n\t}\n\n\t/**\n\t * 从KeyStore中获取私钥公钥\n\t *\n\t * @param keyStore {@link KeyStore}\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyPair}\n\t * @since 4.4.1\n\t */\n\tpublic static KeyPair getKeyPair(KeyStore keyStore, char[] password, String alias) {\n\t\tPublicKey publicKey;\n\t\tPrivateKey privateKey;\n\t\ttry {\n\t\t\tpublicKey = keyStore.getCertificate(alias).getPublicKey();\n\t\t\tprivateKey = (PrivateKey) keyStore.getKey(alias, password);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn new KeyPair(publicKey, privateKey);\n\t}\n\n\t/**\n\t * 读取X.509 Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in       {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readX509Certificate(InputStream in, char[] password, String alias) {\n\t\treturn readCertificate(CERT_TYPE_X509, in, password, alias);\n\t}\n\n\t/**\n\t * 读取X.509 Certification文件中的公钥<br>\n\t * Certification为证书文件<br>\n\t * see: https://www.cnblogs.com/yinliang/p/10115519.html\n\t *\n\t * @param in {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @return {@link KeyStore}\n\t * @since 4.5.2\n\t */\n\tpublic static PublicKey readPublicKeyFromCert(InputStream in) {\n\t\tfinal Certificate certificate = readX509Certificate(in);\n\t\tif (null != certificate) {\n\t\t\treturn certificate.getPublicKey();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 读取X.509 Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readX509Certificate(InputStream in) {\n\t\treturn readCertificate(CERT_TYPE_X509, in);\n\t}\n\n\t/**\n\t * 读取Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type     类型，例如X.509\n\t * @param in       {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readCertificate(String type, InputStream in, char[] password, String alias) {\n\t\tfinal KeyStore keyStore = readKeyStore(type, in, password);\n\t\ttry {\n\t\t\treturn keyStore.getCertificate(alias);\n\t\t} catch (KeyStoreException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type 类型，例如X.509\n\t * @param in   {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @return {@link Certificate}\n\t */\n\tpublic static Certificate readCertificate(String type, InputStream in) {\n\t\ttry {\n\t\t\treturn getCertificateFactory(type).generateCertificate(in);\n\t\t} catch (CertificateException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得 Certification\n\t *\n\t * @param keyStore {@link KeyStore}\n\t * @param alias    别名\n\t * @return {@link Certificate}\n\t */\n\tpublic static Certificate getCertificate(KeyStore keyStore, String alias) {\n\t\ttry {\n\t\t\treturn keyStore.getCertificate(alias);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取{@link CertificateFactory}\n\t *\n\t * @param type 类型，例如X.509\n\t * @return {@link KeyPairGenerator}\n\t * @since 4.5.0\n\t */\n\tpublic static CertificateFactory getCertificateFactory(String type) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tCertificateFactory factory;\n\t\ttry {\n\t\t\tfactory = (null == provider) ? CertificateFactory.getInstance(type) : CertificateFactory.getInstance(type, provider);\n\t\t} catch (CertificateException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn factory;\n\t}\n\n\t/**\n\t * 编码压缩EC公钥（基于BouncyCastle）<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param publicKey {@link PublicKey}，必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey\n\t * @return 压缩得到的X\n\t * @since 4.4.4\n\t */\n\tpublic static byte[] encodeECPublicKey(PublicKey publicKey) {\n\t\treturn BCUtil.encodeECPublicKey(publicKey);\n\t}\n\n\t/**\n\t * 解码恢复EC压缩公钥,支持Base64和Hex编码,（基于BouncyCastle）<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param encode    压缩公钥\n\t * @param curveName EC曲线名\n\t * @return 公钥\n\t * @since 4.4.4\n\t */\n\tpublic static PublicKey decodeECPoint(String encode, String curveName) {\n\t\treturn BCUtil.decodeECPoint(encode, curveName);\n\t}\n\n\t/**\n\t * 解码恢复EC压缩公钥,支持Base64和Hex编码,（基于BouncyCastle）<br>\n\t * 见：https://www.cnblogs.com/xinzhao/p/8963724.html\n\t *\n\t * @param encodeByte 压缩公钥\n\t * @param curveName  EC曲线名\n\t * @return 公钥\n\t * @since 4.4.4\n\t */\n\tpublic static PublicKey decodeECPoint(byte[] encodeByte, String curveName) {\n\t\treturn BCUtil.decodeECPoint(encodeByte, curveName);\n\t}\n\n\t/**\n\t * 通过RSA私钥生成RSA公钥\n\t *\n\t * @param privateKey RSA私钥\n\t * @return RSA公钥，null表示私钥不被支持\n\t * @since 5.3.6\n\t */\n\tpublic static PublicKey getRSAPublicKey(PrivateKey privateKey) {\n\t\tif (privateKey instanceof RSAPrivateCrtKey) {\n\t\t\tfinal RSAPrivateCrtKey privk = (RSAPrivateCrtKey) privateKey;\n\t\t\treturn getRSAPublicKey(privk.getModulus(), privk.getPublicExponent());\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得RSA公钥对象\n\t *\n\t * @param modulus        Modulus\n\t * @param publicExponent Public Exponent\n\t * @return 公钥\n\t * @since 5.3.6\n\t */\n\tpublic static PublicKey getRSAPublicKey(String modulus, String publicExponent) {\n\t\treturn getRSAPublicKey(\n\t\t\t\tnew BigInteger(modulus, 16), new BigInteger(publicExponent, 16));\n\t}\n\n\t/**\n\t * 获得RSA公钥对象\n\t *\n\t * @param modulus        Modulus\n\t * @param publicExponent Public Exponent\n\t * @return 公钥\n\t * @since 5.3.6\n\t */\n\tpublic static PublicKey getRSAPublicKey(BigInteger modulus, BigInteger publicExponent) {\n\t\tfinal RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent);\n\t\ttry {\n\t\t\treturn getKeyFactory(\"RSA\").generatePublic(publicKeySpec);\n\t\t} catch (InvalidKeySpecException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将密钥编码为Base64格式\n\t *\n\t * @param key 密钥\n\t * @return Base64格式密钥\n\t * @since 5.7.22\n\t */\n\tpublic static String toBase64(Key key) {\n\t\treturn Base64.encode(key.getEncoded());\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/Mode.java",
    "content": "package cn.hutool.crypto;\n\n/**\n * 模式\n *\n * <p>\n * 加密算法模式，是用来描述加密算法（此处特指分组密码，不包括流密码，）在加密时对明文分组的模式，它代表了不同的分组方式\n *\n * @author Looly\n * @see <a href=\"https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher\"> Cipher章节</a>\n * @since 3.0.8\n */\npublic enum Mode {\n\t/**\n\t * 无模式\n\t */\n\tNONE,\n\t/**\n\t * 密码分组连接模式（Cipher Block Chaining）\n\t */\n\tCBC,\n\t/**\n\t * 密文反馈模式（Cipher Feedback）\n\t */\n\tCFB,\n\t/**\n\t * 计数器模式（A simplification of OFB）\n\t */\n\tCTR,\n\t/**\n\t * Cipher Text Stealing\n\t */\n\tCTS,\n\t/**\n\t * 电子密码本模式（Electronic CodeBook）\n\t */\n\tECB,\n\t/**\n\t * 输出反馈模式（Output Feedback）\n\t */\n\tOFB,\n\t/**\n\t * Propagating Cipher Block\n\t */\n\tPCBC\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/OpensslKeyUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport org.bouncycastle.asn1.pkcs.PrivateKeyInfo;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\n\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.bouncycastle.openssl.PEMDecryptorProvider;\nimport org.bouncycastle.openssl.PEMEncryptedKeyPair;\nimport org.bouncycastle.openssl.PEMException;\nimport org.bouncycastle.openssl.PEMKeyPair;\nimport org.bouncycastle.openssl.PEMParser;\nimport org.bouncycastle.openssl.X509TrustedCertificateBlock;\nimport org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;\nimport org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;\nimport org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;\nimport org.bouncycastle.operator.InputDecryptorProvider;\nimport org.bouncycastle.operator.OperatorCreationException;\nimport org.bouncycastle.pkcs.PKCS10CertificationRequest;\nimport org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;\nimport org.bouncycastle.pkcs.PKCSException;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.security.Key;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * 基于bcpkix封装的Openssl相关工具，包括密钥转换、Pem密钥文件读取等<br>\n * 注意此工具需要引入org.bouncycastle:bcpkix-jdk15to18\n *\n * @author changhr2013, looly\n * @since 5.8.5\n */\npublic class OpensslKeyUtil {\n\n\tprivate static final JcaPEMKeyConverter pemKeyConverter = new JcaPEMKeyConverter().setProvider(GlobalBouncyCastleProvider.INSTANCE.getProvider());\n\n\t/**\n\t * 转换{@link PrivateKeyInfo}为{@link PrivateKey}\n\t *\n\t * @param privateKeyInfo {@link PrivateKeyInfo}\n\t * @return {@link PrivateKey}\n\t * @throws CryptoException {@link PEMException}包装\n\t */\n\tpublic static PrivateKey getPrivateKey(final PrivateKeyInfo privateKeyInfo) throws CryptoException {\n\t\ttry {\n\t\t\treturn pemKeyConverter.getPrivateKey(privateKeyInfo);\n\t\t} catch (final PEMException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 转换{@link SubjectPublicKeyInfo}为{@link PublicKey}\n\t *\n\t * @param publicKeyInfo {@link SubjectPublicKeyInfo}\n\t * @return {@link PublicKey}\n\t * @throws CryptoException {@link PEMException}包装\n\t */\n\tpublic static PublicKey getPublicKey(final SubjectPublicKeyInfo publicKeyInfo) throws CryptoException {\n\t\ttry {\n\t\t\treturn pemKeyConverter.getPublicKey(publicKeyInfo);\n\t\t} catch (final PEMException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 转换{@link PEMKeyPair}为{@link KeyPair}\n\t *\n\t * @param keyPair {@link PEMKeyPair}\n\t * @return {@link KeyPair}\n\t * @throws CryptoException {@link PEMException}包装\n\t */\n\tpublic static KeyPair getKeyPair(final PEMKeyPair keyPair) throws CryptoException {\n\t\ttry {\n\t\t\treturn pemKeyConverter.getKeyPair(keyPair);\n\t\t} catch (final PEMException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 从pem文件中读取公钥或私钥<br>\n\t * 根据类型返回 {@link PublicKey} 或者 {@link PrivateKey}\n\t *\n\t * @param keyStream pem 流\n\t * @param password  私钥密码\n\t * @return {@link Key}，null 表示无法识别的密钥类型\n\t * @since 5.8.5\n\t */\n\tpublic static Key readPemKey(final InputStream keyStream, final char[] password) {\n\t\ttry (final PEMParser pemParser = new PEMParser(new InputStreamReader(keyStream))) {\n\t\t\treturn readPemKeyFromKeyObject(pemParser.readObject(), password);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 解密{@link PKCS8EncryptedPrivateKeyInfo}为{@link PrivateKeyInfo}\n\t *\n\t * @param pkcs8Info {@link PKCS8EncryptedPrivateKeyInfo}\n\t * @param password  密码\n\t * @return {@link PrivateKeyInfo}\n\t * @throws CryptoException OperatorCreationException和PKCSException包装\n\t */\n\tpublic static PrivateKeyInfo decrypt(final PKCS8EncryptedPrivateKeyInfo pkcs8Info, final char[] password) throws CryptoException {\n\t\tfinal InputDecryptorProvider decryptProvider;\n\t\ttry {\n\t\t\tdecryptProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider(GlobalBouncyCastleProvider.INSTANCE.getProvider()).build(password);\n\t\t\treturn pkcs8Info.decryptPrivateKeyInfo(decryptProvider);\n\t\t} catch (final OperatorCreationException | PKCSException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 解密{@link PEMEncryptedKeyPair}为{@link PEMKeyPair}\n\t *\n\t * @param pemEncryptedKeyPair {@link PKCS8EncryptedPrivateKeyInfo}\n\t * @param password            密码\n\t * @return {@link PEMKeyPair}\n\t * @throws IORuntimeException IOException包装\n\t */\n\tpublic static PEMKeyPair decrypt(final PEMEncryptedKeyPair pemEncryptedKeyPair, final char[] password) throws IORuntimeException {\n\t\tfinal PEMDecryptorProvider decryptProvider;\n\t\ttry {\n\t\t\tdecryptProvider = new JcePEMDecryptorProviderBuilder().setProvider(GlobalBouncyCastleProvider.INSTANCE.getProvider()).build(password);\n\t\t\treturn pemEncryptedKeyPair.decryptKeyPair(decryptProvider);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取Pem文件中的密钥，密钥支持包括：<br>\n\t * <ul>\n\t *     <li>{@link PrivateKeyInfo}</li>\n\t *     <li>{@link PEMKeyPair}，默认读取私钥</li>\n\t *     <li>{@link PKCS8EncryptedPrivateKeyInfo}</li>\n\t *     <li>{@link PEMEncryptedKeyPair}，默认读取私钥</li>\n\t *     <li>{@link X509CertificateHolder}</li>\n\t *     <li>{@link X509TrustedCertificateBlock}</li>\n\t *     <li>{@link PKCS10CertificationRequest}</li>\n\t * </ul>\n\t *\n\t * @param keyObject 密钥内容对象\n\t * @param password  密码（部分加密的pem使用）\n\t * @return {@link Key}\n\t * @throws CryptoException 读取异常或不支持的类型\n\t */\n\tprivate static Key readPemKeyFromKeyObject(final Object keyObject, final char[] password) throws CryptoException {\n\t\tif (keyObject instanceof PrivateKeyInfo) {\n\t\t\t// PrivateKeyInfo\n\t\t\treturn getPrivateKey((PrivateKeyInfo) keyObject);\n\t\t} else if (keyObject instanceof PEMKeyPair) {\n\t\t\t// PemKeyPair\n\t\t\treturn getKeyPair((PEMKeyPair) keyObject).getPrivate();\n\t\t} else if (keyObject instanceof PKCS8EncryptedPrivateKeyInfo) {\n\t\t\t// Encrypted PrivateKeyInfo\n\t\t\treturn getPrivateKey(decrypt((PKCS8EncryptedPrivateKeyInfo) keyObject, password));\n\t\t} else if (keyObject instanceof PEMEncryptedKeyPair) {\n\t\t\t// Encrypted PemKeyPair\n\t\t\treturn getPrivateKey(decrypt((PEMEncryptedKeyPair) keyObject, password).getPrivateKeyInfo());\n\t\t} else if (keyObject instanceof SubjectPublicKeyInfo) {\n\t\t\t// SubjectPublicKeyInfo\n\t\t\treturn getPublicKey((SubjectPublicKeyInfo) keyObject);\n\t\t} else if (keyObject instanceof X509CertificateHolder) {\n\t\t\t// X509 Certificate\n\t\t\treturn getPublicKey(((X509CertificateHolder) keyObject).getSubjectPublicKeyInfo());\n\t\t} else if (keyObject instanceof X509TrustedCertificateBlock) {\n\t\t\t// X509 Trusted Certificate\n\t\t\treturn getPublicKey(((X509TrustedCertificateBlock) keyObject).getCertificateHolder().getSubjectPublicKeyInfo());\n\t\t} else if (keyObject instanceof PKCS10CertificationRequest) {\n\t\t\t// PKCS#10 CSR\n\t\t\treturn getPublicKey(((PKCS10CertificationRequest) keyObject).getSubjectPublicKeyInfo());\n\t\t} else {\n\t\t\t// 表示无法识别的密钥类型\n\t\t\tthrow new CryptoException(\"Unsupported key object type: {}\", keyObject.getClass());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/Padding.java",
    "content": "package cn.hutool.crypto;\n\n/**\n * 补码方式\n *\n * <p>\n * 补码方式是在分组密码中，当明文长度不是分组长度的整数倍时，需要在最后一个分组中填充一些数据使其凑满一个分组的长度。\n *\n * @author Looly\n * @see <a href=\"https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher\"> Cipher章节</a>\n * @since 3.0.8\n */\npublic enum Padding {\n\t/**\n\t * 无补码\n\t */\n\tNoPadding,\n\t/**\n\t * 0补码，即不满block长度时使用0填充\n\t */\n\tZeroPadding,\n\t/**\n\t * This padding for block ciphers is described in 5.2 Block Encryption Algorithms in the W3C's \"XML Encryption Syntax and Processing\" document.\n\t */\n\tISO10126Padding,\n\t/**\n\t * Optimal Asymmetric Encryption Padding scheme defined in PKCS1\n\t */\n\tOAEPPadding,\n\t/**\n\t * The padding scheme described in PKCS #1, used with the RSA algorithm\n\t */\n\tPKCS1Padding,\n\t/**\n\t * The padding scheme described in RSA Laboratories, \"PKCS #5: Password-Based Encryption Standard,\" version 1.5, November 1993.\n\t */\n\tPKCS5Padding,\n\t/**\n\t * The padding scheme defined in the SSL Protocol Version 3.0, November 18, 1996, section 5.2.3.2 (CBC block cipher)\n\t */\n\tSSL3Padding\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/PemUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport org.bouncycastle.util.io.pem.PemObject;\nimport org.bouncycastle.util.io.pem.PemObjectGenerator;\nimport org.bouncycastle.util.io.pem.PemReader;\nimport org.bouncycastle.util.io.pem.PemWriter;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Reader;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.security.Key;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * PEM(Privacy Enhanced Mail)格式相关工具类。（基于Bouncy Castle）\n *\n * <p>\n * PEM一般为文本格式，以 -----BEGIN... 开头，以 -----END... 结尾，中间的内容是 BASE64 编码。\n * <p>\n * 这种格式可以保存证书和私钥，有时我们也把PEM格式的私钥的后缀改为 .key 以区别证书与私钥。\n *\n * @author looly\n * @since 5.1.6\n */\npublic class PemUtil {\n\n\t/**\n\t * 读取PEM格式的私钥，支持PKCS#8和PKCS#1的ECC格式\n\t *\n\t * @param pemStream pem流\n\t * @return {@link PrivateKey}\n\t * @since 4.5.2\n\t */\n\tpublic static PrivateKey readPemPrivateKey(InputStream pemStream) {\n\t\treturn (PrivateKey) readPemKey(pemStream);\n\t}\n\n\t/**\n\t * 读取PEM格式的公钥\n\t *\n\t * @param pemStream pem流\n\t * @return {@link PublicKey}\n\t * @since 4.5.2\n\t */\n\tpublic static PublicKey readPemPublicKey(InputStream pemStream) {\n\t\treturn (PublicKey) readPemKey(pemStream);\n\t}\n\n\t/**\n\t * 从pem文件中读取公钥或私钥<br>\n\t * 根据类型返回 {@link PublicKey} 或者 {@link PrivateKey}\n\t *\n\t * @param keyStream pem流\n\t * @return {@link Key}，null表示无法识别的密钥类型\n\t * @since 5.1.6\n\t */\n\tpublic static Key readPemKey(InputStream keyStream) {\n\t\tfinal PemObject object = readPemObject(keyStream);\n\t\tfinal String type = object.getType();\n\t\tif (StrUtil.isNotBlank(type)) {\n\t\t\t//private\n\t\t\tif (type.endsWith(\"EC PRIVATE KEY\")) {\n\t\t\t\ttry {\n\t\t\t\t\t// 尝试PKCS#8\n\t\t\t\t\treturn KeyUtil.generatePrivateKey(\"EC\", object.getContent());\n\t\t\t\t} catch (final Exception e) {\n\t\t\t\t\t// 尝试PKCS#1\n\t\t\t\t\treturn KeyUtil.generatePrivateKey(\"EC\", ECKeyUtil.createOpenSSHPrivateKeySpec(object.getContent()));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (type.endsWith(\"PRIVATE KEY\")) {\n\t\t\t\treturn KeyUtil.generateRSAPrivateKey(object.getContent());\n\t\t\t}\n\n\t\t\t// public\n\t\t\tif (type.endsWith(\"EC PUBLIC KEY\")) {\n\t\t\t\ttry {\n\t\t\t\t\t// 尝试DER\n\t\t\t\t\treturn KeyUtil.generatePublicKey(\"EC\", object.getContent());\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t// 尝试PKCS#1\n\t\t\t\t\treturn KeyUtil.generatePublicKey(\"EC\", ECKeyUtil.createOpenSSHPublicKeySpec(object.getContent()));\n\t\t\t\t}\n\t\t\t} else if (type.endsWith(\"PUBLIC KEY\")) {\n\t\t\t\treturn KeyUtil.generateRSAPublicKey(object.getContent());\n\t\t\t} else if (type.endsWith(\"CERTIFICATE\")) {\n\t\t\t\treturn KeyUtil.readPublicKeyFromCert(IoUtil.toStream(object.getContent()));\n\t\t\t}\n\t\t}\n\n\t\t//表示无法识别的密钥类型\n\t\treturn null;\n\t}\n\n\t/**\n\t * 从pem流中读取公钥或私钥\n\t *\n\t * @param keyStream pem流\n\t * @return 密钥bytes\n\t * @since 5.1.6\n\t */\n\tpublic static byte[] readPem(InputStream keyStream) {\n\t\tfinal PemObject pemObject = readPemObject(keyStream);\n\t\tif (null != pemObject) {\n\t\t\treturn pemObject.getContent();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 读取pem文件中的信息，包括类型、头信息和密钥内容\n\t *\n\t * @param keyStream pem流\n\t * @return {@link PemObject}\n\t * @since 4.5.2\n\t */\n\tpublic static PemObject readPemObject(InputStream keyStream) {\n\t\treturn readPemObject(IoUtil.getUtf8Reader(keyStream));\n\t}\n\n\t/**\n\t * 读取pem文件中的信息，包括类型、头信息和密钥内容\n\t *\n\t * @param reader pem Reader\n\t * @return {@link PemObject}\n\t * @since 5.1.6\n\t */\n\tpublic static PemObject readPemObject(Reader reader) {\n\t\tPemReader pemReader = null;\n\t\ttry {\n\t\t\tpemReader = new PemReader(reader);\n\t\t\treturn pemReader.readPemObject();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(pemReader);\n\t\t}\n\t}\n\n\t/**\n\t * 读取OpenSSL生成的ANS1格式的Pem私钥文件，必须为PKCS#1格式\n\t *\n\t * @param keyStream 私钥pem流\n\t * @return {@link PrivateKey}\n\t * @deprecated 请使用 {@link #readPemPrivateKey(InputStream)}\n\t */\n\t@Deprecated\n\tpublic static PrivateKey readSm2PemPrivateKey(InputStream keyStream) {\n\t\treturn readPemPrivateKey(keyStream);\n\t}\n\n\t/**\n\t * 将私钥或公钥转换为PEM格式的字符串\n\t *\n\t * @param type    密钥类型（私钥、公钥、证书）\n\t * @param content 密钥内容\n\t * @return PEM内容\n\t * @since 5.5.9\n\t */\n\tpublic static String toPem(String type, byte[] content) {\n\t\tfinal StringWriter stringWriter = new StringWriter();\n\t\twritePemObject(type, content, stringWriter);\n\t\treturn stringWriter.toString();\n\t}\n\n\t/**\n\t * 写出pem密钥（私钥、公钥、证书）\n\t *\n\t * @param type      密钥类型（私钥、公钥、证书）\n\t * @param content   密钥内容，需为PKCS#1格式\n\t * @param keyStream pem流\n\t * @since 5.1.6\n\t */\n\tpublic static void writePemObject(String type, byte[] content, OutputStream keyStream) {\n\t\twritePemObject(new PemObject(type, content), keyStream);\n\t}\n\n\t/**\n\t * 写出pem密钥（私钥、公钥、证书）\n\t *\n\t * @param type    密钥类型（私钥、公钥、证书）\n\t * @param content 密钥内容，需为PKCS#1或PKCS#8格式\n\t * @param writer  pemWriter\n\t * @since 5.5.9\n\t */\n\tpublic static void writePemObject(String type, byte[] content, Writer writer) {\n\t\twritePemObject(new PemObject(type, content), writer);\n\t}\n\n\t/**\n\t * 写出pem密钥（私钥、公钥、证书）\n\t *\n\t * @param pemObject pem对象，包括密钥和密钥类型等信息\n\t * @param keyStream pem流\n\t * @since 5.1.6\n\t */\n\tpublic static void writePemObject(PemObjectGenerator pemObject, OutputStream keyStream) {\n\t\twritePemObject(pemObject, IoUtil.getUtf8Writer(keyStream));\n\t}\n\n\t/**\n\t * 写出pem密钥（私钥、公钥、证书）\n\t *\n\t * @param pemObject pem对象，包括密钥和密钥类型等信息\n\t * @param writer    pemWriter\n\t * @since 5.5.9\n\t */\n\tpublic static void writePemObject(PemObjectGenerator pemObject, Writer writer) {\n\t\tfinal PemWriter pemWriter = new PemWriter(writer);\n\t\ttry {\n\t\t\tpemWriter.writeObject(pemObject);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(pemWriter);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/ProviderFactory.java",
    "content": "package cn.hutool.crypto;\n\nimport java.security.Provider;\nimport java.security.Security;\n\n/**\n * Provider对象生产工厂类\n *\n * <pre>\n * 1. 调用{@link #createBouncyCastleProvider()} 用于新建一个org.bouncycastle.jce.provider.BouncyCastleProvider对象\n * </pre>\n *\n * @author looly\n * @since 4.2.1\n */\npublic class ProviderFactory {\n\n\tprivate ProviderFactory() {\n\t}\n\n\t/**\n\t * 创建Bouncy Castle 提供者<br>\n\t * 如果用户未引入bouncycastle库，则此方法抛出{@link NoClassDefFoundError} 异常\n\t *\n\t * @return {@link Provider}\n\t */\n\tpublic static Provider createBouncyCastleProvider() {\n\t\tProvider provider = Security.getProvider(org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME);\n\t\tif (provider == null) {\n\t\t\tprovider = new org.bouncycastle.jce.provider.BouncyCastleProvider();\n\t\t\t// issue#2631@Github\n\t\t\tSecureUtil.addProvider(provider);\n\t\t}\n\t\treturn provider;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Validator;\nimport cn.hutool.core.util.*;\nimport cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;\nimport cn.hutool.crypto.asymmetric.RSA;\nimport cn.hutool.crypto.asymmetric.Sign;\nimport cn.hutool.crypto.asymmetric.SignAlgorithm;\nimport cn.hutool.crypto.digest.DigestAlgorithm;\nimport cn.hutool.crypto.digest.Digester;\nimport cn.hutool.crypto.digest.HMac;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\nimport cn.hutool.crypto.digest.MD5;\nimport cn.hutool.crypto.symmetric.AES;\nimport cn.hutool.crypto.symmetric.DES;\nimport cn.hutool.crypto.symmetric.DESede;\nimport cn.hutool.crypto.symmetric.PBKDF2;\nimport cn.hutool.crypto.symmetric.RC4;\nimport cn.hutool.crypto.symmetric.SymmetricCrypto;\nimport cn.hutool.crypto.symmetric.ZUC;\nimport cn.hutool.crypto.symmetric.fpe.FPE;\nimport org.bouncycastle.crypto.AlphabetMapper;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.Mac;\nimport javax.crypto.SecretKey;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.security.KeyPair;\nimport java.security.KeyStore;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.PrivateKey;\nimport java.security.Provider;\nimport java.security.PublicKey;\nimport java.security.Security;\nimport java.security.Signature;\nimport java.security.cert.Certificate;\nimport java.security.spec.AlgorithmParameterSpec;\nimport java.security.spec.KeySpec;\nimport java.util.Map;\n\n/**\n * 安全相关工具类<br>\n * 加密分为三种：<br>\n * 1、对称加密（symmetric），例如：AES、DES等<br>\n * 2、非对称加密（asymmetric），例如：RSA、DSA等<br>\n * 3、摘要加密（digest），例如：MD5、SHA-1、SHA-256、HMAC等<br>\n *\n * @author Looly, Gsealy\n */\npublic class SecureUtil {\n\n\t/** Hutool自定义系统属性：是否解码Hex字符 issue#I90M9D */\n\tpublic static String HUTOOL_CRYPTO_DECODE_HEX = \"hutool.crypto.decodeHex\";\n\n\t/**\n\t * 默认密钥字节数\n\t *\n\t * <pre>\n\t * RSA/DSA\n\t * Default Keysize 1024\n\t * Keysize must be a multiple of 64, ranging from 512 to 1024 (inclusive).\n\t * </pre>\n\t */\n\tpublic static final int DEFAULT_KEY_SIZE = KeyUtil.DEFAULT_KEY_SIZE;\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成\n\t *\n\t * @param algorithm 算法，支持PBE算法\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm) {\n\t\treturn KeyUtil.generateKey(algorithm);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成\n\t *\n\t * @param algorithm 算法，支持PBE算法\n\t * @param keySize   密钥长度\n\t * @return {@link SecretKey}\n\t * @since 3.1.2\n\t */\n\tpublic static SecretKey generateKey(String algorithm, int keySize) {\n\t\treturn KeyUtil.generateKey(algorithm, keySize);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法密钥生成\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥，如果为{@code null} 自动生成随机密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm, byte[] key) {\n\t\treturn KeyUtil.generateKey(algorithm, key);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}\n\t *\n\t * @param algorithm DES算法，包括DES、DESede等\n\t * @param key       密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateDESKey(String algorithm, byte[] key) {\n\t\treturn KeyUtil.generateDESKey(algorithm, key);\n\t}\n\n\t/**\n\t * 生成PBE {@link SecretKey}\n\t *\n\t * @param algorithm PBE算法，包括：PBEWithMD5AndDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40等\n\t * @param key       密钥\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generatePBEKey(String algorithm, char[] key) {\n\t\treturn KeyUtil.generatePBEKey(algorithm, key);\n\t}\n\n\t/**\n\t * 生成 {@link SecretKey}，仅用于对称加密和摘要算法\n\t *\n\t * @param algorithm 算法\n\t * @param keySpec   {@link KeySpec}\n\t * @return {@link SecretKey}\n\t */\n\tpublic static SecretKey generateKey(String algorithm, KeySpec keySpec) {\n\t\treturn KeyUtil.generateKey(algorithm, keySpec);\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @return 私钥 {@link PrivateKey}\n\t */\n\tpublic static PrivateKey generatePrivateKey(String algorithm, byte[] key) {\n\t\treturn KeyUtil.generatePrivateKey(algorithm, key);\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param keySpec   {@link KeySpec}\n\t * @return 私钥 {@link PrivateKey}\n\t * @since 3.1.1\n\t */\n\tpublic static PrivateKey generatePrivateKey(String algorithm, KeySpec keySpec) {\n\t\treturn KeyUtil.generatePrivateKey(algorithm, keySpec);\n\t}\n\n\t/**\n\t * 生成私钥，仅用于非对称加密\n\t *\n\t * @param keyStore {@link KeyStore}\n\t * @param alias    别名\n\t * @param password 密码\n\t * @return 私钥 {@link PrivateKey}\n\t */\n\tpublic static PrivateKey generatePrivateKey(KeyStore keyStore, String alias, char[] password) {\n\t\treturn KeyUtil.generatePrivateKey(keyStore, alias, password);\n\t}\n\n\t/**\n\t * 生成公钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @return 公钥 {@link PublicKey}\n\t */\n\tpublic static PublicKey generatePublicKey(String algorithm, byte[] key) {\n\t\treturn KeyUtil.generatePublicKey(algorithm, key);\n\t}\n\n\t/**\n\t * 生成公钥，仅用于非对称加密<br>\n\t * 算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory\n\t *\n\t * @param algorithm 算法\n\t * @param keySpec   {@link KeySpec}\n\t * @return 公钥 {@link PublicKey}\n\t * @since 3.1.1\n\t */\n\tpublic static PublicKey generatePublicKey(String algorithm, KeySpec keySpec) {\n\t\treturn KeyUtil.generatePublicKey(algorithm, keySpec);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥，仅用于非对称加密<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm) {\n\t\treturn KeyUtil.generateKeyPair(algorithm);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize) {\n\t\treturn KeyUtil.generateKeyPair(algorithm, keySize);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param keySize   密钥模（modulus ）长度\n\t * @param seed      种子\n\t * @return {@link KeyPair}\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed) {\n\t\treturn KeyUtil.generateKeyPair(algorithm, keySize, seed);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param params    {@link AlgorithmParameterSpec}\n\t * @return {@link KeyPair}\n\t * @since 4.3.3\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, AlgorithmParameterSpec params) {\n\t\treturn KeyUtil.generateKeyPair(algorithm, params);\n\t}\n\n\t/**\n\t * 生成用于非对称加密的公钥和私钥<br>\n\t * 密钥对生成算法见：https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n\t *\n\t * @param algorithm 非对称加密算法\n\t * @param seed      种子\n\t * @param params    {@link AlgorithmParameterSpec}\n\t * @return {@link KeyPair}\n\t * @since 4.3.3\n\t */\n\tpublic static KeyPair generateKeyPair(String algorithm, byte[] seed, AlgorithmParameterSpec params) {\n\t\treturn KeyUtil.generateKeyPair(algorithm, seed, params);\n\t}\n\n\t/**\n\t * 获取用于密钥生成的算法<br>\n\t * 获取XXXwithXXX算法的后半部分算法，如果为ECDSA或SM2，返回算法为EC\n\t *\n\t * @param algorithm XXXwithXXX算法\n\t * @return 算法\n\t */\n\tpublic static String getAlgorithmAfterWith(String algorithm) {\n\t\treturn KeyUtil.getAlgorithmAfterWith(algorithm);\n\t}\n\n\t/**\n\t * 生成算法，格式为XXXwithXXX\n\t *\n\t * @param asymmetricAlgorithm 非对称算法\n\t * @param digestAlgorithm     摘要算法\n\t * @return 算法\n\t * @since 4.4.1\n\t */\n\tpublic static String generateAlgorithm(AsymmetricAlgorithm asymmetricAlgorithm, DigestAlgorithm digestAlgorithm) {\n\t\tfinal String digestPart = (null == digestAlgorithm) ? \"NONE\" : digestAlgorithm.name();\n\t\treturn StrUtil.format(\"{}with{}\", digestPart, asymmetricAlgorithm.getValue());\n\t}\n\n\t/**\n\t * 生成签名对象，仅用于非对称加密\n\t *\n\t * @param asymmetricAlgorithm {@link AsymmetricAlgorithm} 非对称加密算法\n\t * @param digestAlgorithm     {@link DigestAlgorithm} 摘要算法\n\t * @return {@link Signature}\n\t */\n\tpublic static Signature generateSignature(AsymmetricAlgorithm asymmetricAlgorithm, DigestAlgorithm digestAlgorithm) {\n\t\ttry {\n\t\t\treturn Signature.getInstance(generateAlgorithm(asymmetricAlgorithm, digestAlgorithm));\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取密钥库(Java Key Store，JKS) KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t */\n\tpublic static KeyStore readJKSKeyStore(InputStream in, char[] password) {\n\t\treturn KeyUtil.readJKSKeyStore(in, password);\n\t}\n\n\t/**\n\t * 读取KeyStore文件<br>\n\t * KeyStore文件用于数字证书的密钥对保存<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type     类型\n\t * @param in       {@link InputStream} 如果想从文件读取.keystore文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @return {@link KeyStore}\n\t */\n\tpublic static KeyStore readKeyStore(String type, InputStream in, char[] password) {\n\t\treturn KeyUtil.readKeyStore(type, in, password);\n\t}\n\n\t/**\n\t * 读取X.509 Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in       {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readX509Certificate(InputStream in, char[] password, String alias) {\n\t\treturn KeyUtil.readX509Certificate(in, password, alias);\n\t}\n\n\t/**\n\t * 读取X.509 Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param in {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readX509Certificate(InputStream in) {\n\t\treturn KeyUtil.readX509Certificate(in);\n\t}\n\n\t/**\n\t * 读取Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type     类型，例如X.509\n\t * @param in       {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @param password 密码\n\t * @param alias    别名\n\t * @return {@link KeyStore}\n\t * @since 4.4.1\n\t */\n\tpublic static Certificate readCertificate(String type, InputStream in, char[] password, String alias) {\n\t\treturn KeyUtil.readCertificate(type, in, password, alias);\n\t}\n\n\t/**\n\t * 读取Certification文件<br>\n\t * Certification为证书文件<br>\n\t * see: http://snowolf.iteye.com/blog/391931\n\t *\n\t * @param type 类型，例如X.509\n\t * @param in   {@link InputStream} 如果想从文件读取.cer文件，使用 {@link FileUtil#getInputStream(java.io.File)} 读取\n\t * @return {@link Certificate}\n\t */\n\tpublic static Certificate readCertificate(String type, InputStream in) {\n\t\treturn KeyUtil.readCertificate(type, in);\n\t}\n\n\t/**\n\t * 获得 Certification\n\t *\n\t * @param keyStore {@link KeyStore}\n\t * @param alias    别名\n\t * @return {@link Certificate}\n\t */\n\tpublic static Certificate getCertificate(KeyStore keyStore, String alias) {\n\t\treturn KeyUtil.getCertificate(keyStore, alias);\n\t}\n\n\t// ------------------------------------------------------------------- 对称加密算法\n\n\t/**\n\t * AES加密，生成随机KEY。注意解密时必须使用相同 {@link AES}对象或者使用相同KEY<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * AES加密：aes().encrypt(data)\n\t * AES解密：aes().decrypt(data)\n\t * </pre>\n\t *\n\t * @return {@link AES}\n\t */\n\tpublic static AES aes() {\n\t\treturn new AES();\n\t}\n\n\t/**\n\t * AES加密<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * AES加密：aes(key).encrypt(data)\n\t * AES解密：aes(key).decrypt(data)\n\t * </pre>\n\t *\n\t * @param key 密钥\n\t * @return {@link SymmetricCrypto}\n\t */\n\tpublic static AES aes(byte[] key) {\n\t\treturn new AES(key);\n\t}\n\n\t/**\n\t * DES加密，生成随机KEY。注意解密时必须使用相同 {@link DES}对象或者使用相同KEY<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * DES加密：des().encrypt(data)\n\t * DES解密：des().decrypt(data)\n\t * </pre>\n\t *\n\t * @return {@link DES}\n\t */\n\tpublic static DES des() {\n\t\treturn new DES();\n\t}\n\n\t/**\n\t * DES加密<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * DES加密：des(key).encrypt(data)\n\t * DES解密：des(key).decrypt(data)\n\t * </pre>\n\t *\n\t * @param key 密钥\n\t * @return {@link DES}\n\t */\n\tpublic static DES des(byte[] key) {\n\t\treturn new DES(key);\n\t}\n\n\t/**\n\t * DESede加密（又名3DES、TripleDES），生成随机KEY。注意解密时必须使用相同 {@link DESede}对象或者使用相同KEY<br>\n\t * Java中默认实现为：DESede/ECB/PKCS5Padding<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * DESede加密：desede().encrypt(data)\n\t * DESede解密：desede().decrypt(data)\n\t * </pre>\n\t *\n\t * @return {@link DESede}\n\t * @since 3.3.0\n\t */\n\tpublic static DESede desede() {\n\t\treturn new DESede();\n\t}\n\n\t/**\n\t * DESede加密（又名3DES、TripleDES）<br>\n\t * Java中默认实现为：DESede/ECB/PKCS5Padding<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * DESede加密：desede(key).encrypt(data)\n\t * DESede解密：desede(key).decrypt(data)\n\t * </pre>\n\t *\n\t * @param key 密钥\n\t * @return {@link DESede}\n\t * @since 3.3.0\n\t */\n\tpublic static DESede desede(byte[] key) {\n\t\treturn new DESede(key);\n\t}\n\n\t// ------------------------------------------------------------------- 摘要算法\n\n\t/**\n\t * MD5加密<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * MD5加密：md5().digest(data)\n\t * MD5加密并转为16进制字符串：md5().digestHex(data)\n\t * </pre>\n\t *\n\t * @return {@link Digester}\n\t */\n\tpublic static MD5 md5() {\n\t\treturn new MD5();\n\t}\n\n\t/**\n\t * MD5加密，生成16进制MD5字符串<br>\n\t *\n\t * @param data 数据\n\t * @return MD5字符串\n\t */\n\tpublic static String md5(String data) {\n\t\treturn new MD5().digestHex(data);\n\t}\n\n\t/**\n\t * MD5加密，生成16进制MD5字符串<br>\n\t *\n\t * @param data 数据\n\t * @return MD5字符串\n\t */\n\tpublic static String md5(InputStream data) {\n\t\treturn new MD5().digestHex(data);\n\t}\n\n\t/**\n\t * MD5加密文件，生成16进制MD5字符串<br>\n\t *\n\t * @param dataFile 被加密文件\n\t * @return MD5字符串\n\t */\n\tpublic static String md5(File dataFile) {\n\t\treturn new MD5().digestHex(dataFile);\n\t}\n\n\t/**\n\t * SHA1加密<br>\n\t * 例：<br>\n\t * SHA1加密：sha1().digest(data)<br>\n\t * SHA1加密并转为16进制字符串：sha1().digestHex(data)<br>\n\t *\n\t * @return {@link Digester}\n\t */\n\tpublic static Digester sha1() {\n\t\treturn new Digester(DigestAlgorithm.SHA1);\n\t}\n\n\t/**\n\t * SHA1加密，生成16进制SHA1字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SHA1字符串\n\t */\n\tpublic static String sha1(String data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(data);\n\t}\n\n\t/**\n\t * SHA1加密，生成16进制SHA1字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SHA1字符串\n\t */\n\tpublic static String sha1(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(data);\n\t}\n\n\t/**\n\t * SHA1加密文件，生成16进制SHA1字符串<br>\n\t *\n\t * @param dataFile 被加密文件\n\t * @return SHA1字符串\n\t */\n\tpublic static String sha1(File dataFile) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(dataFile);\n\t}\n\n\t/**\n\t * SHA256加密<br>\n\t * 例：<br>\n\t * SHA256加密：sha256().digest(data)<br>\n\t * SHA256加密并转为16进制字符串：sha256().digestHex(data)<br>\n\t *\n\t * @return {@link Digester}\n\t * @since 4.3.2\n\t */\n\tpublic static Digester sha256() {\n\t\treturn new Digester(DigestAlgorithm.SHA256);\n\t}\n\n\t/**\n\t * SHA256加密，生成16进制SHA256字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SHA256字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String sha256(String data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(data);\n\t}\n\n\t/**\n\t * SHA256加密，生成16进制SHA256字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SHA256字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String sha256(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(data);\n\t}\n\n\t/**\n\t * SHA256加密文件，生成16进制SHA256字符串<br>\n\t *\n\t * @param dataFile 被加密文件\n\t * @return SHA256字符串\n\t * @since 4.3.2\n\t */\n\tpublic static String sha256(File dataFile) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(dataFile);\n\t}\n\n\t/**\n\t * 创建HMac对象，调用digest方法可获得hmac值\n\t *\n\t * @param algorithm {@link HmacAlgorithm}\n\t * @param key       密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.3.0\n\t */\n\tpublic static HMac hmac(HmacAlgorithm algorithm, String key) {\n\t\treturn hmac(algorithm, StrUtil.isNotEmpty(key)? StrUtil.utf8Bytes(key): null);\n\t}\n\n\t/**\n\t * 创建HMac对象，调用digest方法可获得hmac值\n\t *\n\t * @param algorithm {@link HmacAlgorithm}\n\t * @param key       密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.0.3\n\t */\n\tpublic static HMac hmac(HmacAlgorithm algorithm, byte[] key) {\n\t\tif (ArrayUtil.isEmpty(key)) {\n\t\t\tkey = generateKey(algorithm.getValue()).getEncoded();\n\t\t}\n\t\treturn new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * 创建HMac对象，调用digest方法可获得hmac值\n\t *\n\t * @param algorithm {@link HmacAlgorithm}\n\t * @param key       密钥{@link SecretKey}，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.0.3\n\t */\n\tpublic static HMac hmac(HmacAlgorithm algorithm, SecretKey key) {\n\t\tif (ObjectUtil.isNull(key)) {\n\t\t\tkey = generateKey(algorithm.getValue());\n\t\t}\n\t\treturn new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * HmacMD5加密器<br>\n\t * 例：<br>\n\t * HmacMD5加密：hmacMd5(key).digest(data)<br>\n\t * HmacMD5加密并转为16进制字符串：hmacMd5(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.3.0\n\t */\n\tpublic static HMac hmacMd5(String key) {\n\t\treturn hmacMd5(StrUtil.isNotEmpty(key)? StrUtil.utf8Bytes(key): null);\n\t}\n\n\t/**\n\t * HmacMD5加密器<br>\n\t * 例：<br>\n\t * HmacMD5加密：hmacMd5(key).digest(data)<br>\n\t * HmacMD5加密并转为16进制字符串：hmacMd5(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t */\n\tpublic static HMac hmacMd5(byte[] key) {\n\t\tif (ArrayUtil.isEmpty(key)) {\n\t\t\tkey = generateKey(HmacAlgorithm.HmacMD5.getValue()).getEncoded();\n\t\t}\n\t\treturn new HMac(HmacAlgorithm.HmacMD5, key);\n\t}\n\n\t/**\n\t * HmacMD5加密器，生成随机KEY<br>\n\t * 例：<br>\n\t * HmacMD5加密：hmacMd5().digest(data)<br>\n\t * HmacMD5加密并转为16进制字符串：hmacMd5().digestHex(data)<br>\n\t *\n\t * @return {@link HMac}\n\t */\n\tpublic static HMac hmacMd5() {\n\t\treturn new HMac(HmacAlgorithm.HmacMD5);\n\t}\n\n\t/**\n\t * HmacSHA1加密器<br>\n\t * 例：<br>\n\t * HmacSHA1加密：hmacSha1(key).digest(data)<br>\n\t * HmacSHA1加密并转为16进制字符串：hmacSha1(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.3.0\n\t */\n\tpublic static HMac hmacSha1(String key) {\n\t\treturn hmacSha1(StrUtil.isNotEmpty(key)? StrUtil.utf8Bytes(key): null);\n\t}\n\n\t/**\n\t * HmacSHA1加密器<br>\n\t * 例：<br>\n\t * HmacSHA1加密：hmacSha1(key).digest(data)<br>\n\t * HmacSHA1加密并转为16进制字符串：hmacSha1(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t */\n\tpublic static HMac hmacSha1(byte[] key) {\n\t\tif (ArrayUtil.isEmpty(key)) {\n\t\t\tkey = generateKey(HmacAlgorithm.HmacSHA1.getValue()).getEncoded();\n\t\t}\n\t\treturn new HMac(HmacAlgorithm.HmacSHA1, key);\n\t}\n\n\t/**\n\t * HmacSHA1加密器，生成随机KEY<br>\n\t * 例：<br>\n\t * HmacSHA1加密：hmacSha1().digest(data)<br>\n\t * HmacSHA1加密并转为16进制字符串：hmacSha1().digestHex(data)<br>\n\t *\n\t * @return {@link HMac}\n\t */\n\tpublic static HMac hmacSha1() {\n\t\treturn new HMac(HmacAlgorithm.HmacSHA1);\n\t}\n\n\t/**\n\t * HmacSHA256加密器<br>\n\t * 例：<br>\n\t * HmacSHA256加密：hmacSha256(key).digest(data)<br>\n\t * HmacSHA256加密并转为16进制字符串：hmacSha256(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 5.6.0\n\t */\n\tpublic static HMac hmacSha256(String key) {\n\t\treturn hmacSha256(StrUtil.isNotEmpty(key)? StrUtil.utf8Bytes(key): null);\n\t}\n\n\t/**\n\t * HmacSHA256加密器<br>\n\t * 例：<br>\n\t * HmacSHA256加密：hmacSha256(key).digest(data)<br>\n\t * HmacSHA256加密并转为16进制字符串：hmacSha256(key).digestHex(data)<br>\n\t *\n\t * @param key 加密密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 5.6.0\n\t */\n\tpublic static HMac hmacSha256(byte[] key) {\n\t\tif (ArrayUtil.isEmpty(key)) {\n\t\t\tkey = generateKey(HmacAlgorithm.HmacSHA256.getValue()).getEncoded();\n\t\t}\n\t\treturn new HMac(HmacAlgorithm.HmacSHA256, key);\n\t}\n\n\t/**\n\t * HmacSHA256加密器，生成随机KEY<br>\n\t * 例：<br>\n\t * HmacSHA256加密：hmacSha256().digest(data)<br>\n\t * HmacSHA256加密并转为16进制字符串：hmacSha256().digestHex(data)<br>\n\t *\n\t * @return {@link HMac}\n\t * @since 5.6.0\n\t */\n\tpublic static HMac hmacSha256() {\n\t\treturn new HMac(HmacAlgorithm.HmacSHA256);\n\t}\n\n\t// ------------------------------------------------------------------- 非称加密算法\n\n\t/**\n\t * 创建RSA算法对象<br>\n\t * 生成新的私钥公钥对\n\t *\n\t * @return {@link RSA}\n\t * @since 3.0.5\n\t */\n\tpublic static RSA rsa() {\n\t\treturn new RSA();\n\t}\n\n\t/**\n\t * 创建RSA算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyBase64 私钥Base64\n\t * @param publicKeyBase64  公钥Base64\n\t * @return {@link RSA}\n\t * @since 3.0.5\n\t */\n\tpublic static RSA rsa(String privateKeyBase64, String publicKeyBase64) {\n\t\treturn new RSA(privateKeyBase64, publicKeyBase64);\n\t}\n\n\t/**\n\t * 创建RSA算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return {@link RSA}\n\t * @since 3.0.5\n\t */\n\tpublic static RSA rsa(byte[] privateKey, byte[] publicKey) {\n\t\treturn new RSA(privateKey, publicKey);\n\t}\n\n\t/**\n\t * 创建签名算法对象<br>\n\t * 生成新的私钥公钥对\n\t *\n\t * @param algorithm 签名算法\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm) {\n\t\treturn SignUtil.sign(algorithm);\n\t}\n\n\t/**\n\t * 创建签名算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm        签名算法\n\t * @param privateKeyBase64 私钥Base64\n\t * @param publicKeyBase64  公钥Base64\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm, String privateKeyBase64, String publicKeyBase64) {\n\t\treturn SignUtil.sign(algorithm, privateKeyBase64, publicKeyBase64);\n\t}\n\n\t/**\n\t * 创建Sign算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  算法枚举\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm, byte[] privateKey, byte[] publicKey) {\n\t\treturn SignUtil.sign(algorithm, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param crypto      对称加密算法\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(SymmetricCrypto crypto, Map<?, ?> params, String... otherParams) {\n\t\treturn SignUtil.signParams(crypto, params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串\n\t *\n\t * @param crypto            对称加密算法\n\t * @param params            参数\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(SymmetricCrypto crypto, Map<?, ?> params, String separator,\n\t\t\t\t\t\t\t\t\tString keyValueSeparator, boolean isIgnoreNull, String... otherParams) {\n\t\treturn SignUtil.signParams(crypto, params, separator, keyValueSeparator, isIgnoreNull, otherParams);\n\t}\n\n\t/**\n\t * 对参数做md5签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParamsMd5(Map<?, ?> params, String... otherParams) {\n\t\treturn SignUtil.signParamsMd5(params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做Sha1签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.8\n\t */\n\tpublic static String signParamsSha1(Map<?, ?> params, String... otherParams) {\n\t\treturn SignUtil.signParamsSha1(params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做Sha256签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParamsSha256(Map<?, ?> params, String... otherParams) {\n\t\treturn SignUtil.signParamsSha256(params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param digestAlgorithm 摘要算法\n\t * @param params          参数\n\t * @param otherParams     其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(DigestAlgorithm digestAlgorithm, Map<?, ?> params, String... otherParams) {\n\t\treturn SignUtil.signParams(digestAlgorithm, params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串\n\t *\n\t * @param digestAlgorithm   摘要算法\n\t * @param params            参数\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(DigestAlgorithm digestAlgorithm, Map<?, ?> params, String separator,\n\t\t\t\t\t\t\t\t\tString keyValueSeparator, boolean isIgnoreNull, String... otherParams) {\n\t\treturn SignUtil.signParams(digestAlgorithm, params, separator, keyValueSeparator, isIgnoreNull, otherParams);\n\t}\n\n\t/**\n\t * 增加加密解密的算法提供者，默认优先使用，例如：\n\t *\n\t * <pre>\n\t * addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());\n\t * </pre>\n\t *\n\t * @param provider 算法提供者\n\t * @since 4.1.22\n\t */\n\tpublic static void addProvider(Provider provider) {\n\t\tSecurity.insertProviderAt(provider, 0);\n\t}\n\n\t/**\n\t * 解码字符串密钥，可支持的编码如下：\n\t *\n\t * <pre>\n\t * 1. Hex（16进制）编码\n\t * 1. Base64编码\n\t * </pre>\n\t *\n\t * @param key 被解码的密钥字符串\n\t * @return 密钥\n\t * @since 4.3.3\n\t */\n\tpublic static byte[] decode(String key) {\n\t\t// issue#I90M9D\n\t\t// 某些特殊字符串会无法区分Hex还是Base64，此处使用系统属性强制关闭Hex解析\n\t\tfinal boolean decodeHex = SystemPropsUtil.getBoolean(HUTOOL_CRYPTO_DECODE_HEX, true);\n\t\treturn (decodeHex && Validator.isHex(key)) ? HexUtil.decodeHex(key) : Base64.decode(key);\n\t}\n\n\t/**\n\t * 创建{@link Cipher}\n\t *\n\t * @param algorithm 算法\n\t * @return {@link Cipher}\n\t * @since 4.5.2\n\t */\n\tpublic static Cipher createCipher(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tCipher cipher;\n\t\ttry {\n\t\t\tcipher = (null == provider) ? Cipher.getInstance(algorithm) : Cipher.getInstance(algorithm, provider);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\n\t\treturn cipher;\n\t}\n\n\t/**\n\t * 创建{@link MessageDigest}\n\t *\n\t * @param algorithm 算法\n\t * @return {@link MessageDigest}\n\t * @since 4.5.2\n\t */\n\tpublic static MessageDigest createMessageDigest(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tMessageDigest messageDigest;\n\t\ttry {\n\t\t\tmessageDigest = (null == provider) ? MessageDigest.getInstance(algorithm) : MessageDigest.getInstance(algorithm, provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\n\t\treturn messageDigest;\n\t}\n\n\t/**\n\t * 创建{@link MessageDigest}，使用JDK默认的Provider<br>\n\t *\n\t * @param algorithm 算法\n\t * @return {@link MessageDigest}\n\t */\n\tpublic static MessageDigest createJdkMessageDigest(final String algorithm) {\n\t\ttry {\n\t\t\treturn MessageDigest.getInstance(algorithm);\n\t\t} catch (final NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建{@link Mac}\n\t *\n\t * @param algorithm 算法\n\t * @return {@link Mac}\n\t * @since 4.5.13\n\t */\n\tpublic static Mac createMac(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tMac mac;\n\t\ttry {\n\t\t\tmac = (null == provider) ? Mac.getInstance(algorithm) : Mac.getInstance(algorithm, provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\n\t\treturn mac;\n\t}\n\n\t/**\n\t * 创建{@link Signature}\n\t *\n\t * @param algorithm 算法\n\t * @return {@link Signature}\n\t * @since 5.7.0\n\t */\n\tpublic static Signature createSignature(String algorithm) {\n\t\tfinal Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();\n\n\t\tSignature signature;\n\t\ttry {\n\t\t\tsignature = (null == provider) ? Signature.getInstance(algorithm) : Signature.getInstance(algorithm, provider);\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\n\t\treturn signature;\n\t}\n\n\t/**\n\t * RC4算法\n\t *\n\t * @param key 密钥\n\t * @return {@link RC4}\n\t */\n\tpublic static RC4 rc4(String key) {\n\t\treturn new RC4(key);\n\t}\n\n\t/**\n\t * 强制关闭Bouncy Castle库的使用，全局有效\n\t *\n\t * @since 4.5.2\n\t */\n\tpublic static void disableBouncyCastle() {\n\t\tGlobalBouncyCastleProvider.setUseBouncyCastle(false);\n\t}\n\n\t/**\n\t * PBKDF2加密密码\n\t *\n\t * @param password 密码\n\t * @param salt     盐\n\t * @return 盐，一般为16位\n\t * @since 5.6.0\n\t */\n\tpublic static String pbkdf2(char[] password, byte[] salt) {\n\t\treturn new PBKDF2().encryptHex(password, salt);\n\t}\n\n\t/**\n\t * FPE(Format Preserving Encryption)实现，支持FF1和FF3-1模式。\n\t *\n\t * @param mode   FPE模式枚举，可选FF1或FF3-1\n\t * @param key    密钥，{@code null}表示随机密钥，长度必须是16bit、24bit或32bit\n\t * @param mapper Alphabet字典映射，被加密的字符范围和这个映射必须一致，例如手机号、银行卡号等字段可以采用数字字母字典表\n\t * @param tweak  Tweak是为了解决因局部加密而导致结果冲突问题，通常情况下将数据的不可变部分作为Tweak\n\t * @return {@link FPE}\n\t * @since 5.7.12\n\t */\n\tpublic static FPE fpe(FPE.FPEMode mode, byte[] key, AlphabetMapper mapper, byte[] tweak) {\n\t\treturn new FPE(mode, key, mapper, tweak);\n\t}\n\n\t/**\n\t * 祖冲之算法集（ZUC-128算法）实现，基于BouncyCastle实现。\n\t *\n\t * @param key 密钥，长度16bytes\n\t * @param iv  加盐，长度16bytes，{@code null}是随机加盐\n\t * @return {@link ZUC}\n\t * @since 5.7.12\n\t */\n\tpublic static ZUC zuc128(byte[] key, byte[] iv) {\n\t\treturn new ZUC(ZUC.ZUCAlgorithm.ZUC_128, key, iv);\n\t}\n\n\t/**\n\t * 祖冲之算法集（ZUC-256算法）实现，基于BouncyCastle实现。\n\t *\n\t * @param key 密钥，长度32bytes\n\t * @param iv  加盐，长度16bytes，{@code null}是随机加盐\n\t * @return {@link ZUC}\n\t * @since 5.7.12\n\t */\n\tpublic static ZUC zuc256(byte[] key, byte[] iv) {\n\t\treturn new ZUC(ZUC.ZUCAlgorithm.ZUC_256, key, iv);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/SignUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.asymmetric.Sign;\nimport cn.hutool.crypto.asymmetric.SignAlgorithm;\nimport cn.hutool.crypto.digest.DigestAlgorithm;\nimport cn.hutool.crypto.digest.Digester;\nimport cn.hutool.crypto.symmetric.SymmetricCrypto;\n\nimport java.util.Map;\n\n/**\n * 签名工具类<br>\n * 封装包括：\n * <ul>\n *     <li>非对称签名，签名算法支持见{@link SignAlgorithm}</li>\n *     <li>对称签名，支持Map类型参数排序后签名</li>\n *     <li>摘要签名，支持Map类型参数排序后签名，签名方法见：{@link DigestAlgorithm}</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.20\n */\npublic class SignUtil {\n\n\t/**\n\t * 创建签名算法对象<br>\n\t * 生成新的私钥公钥对\n\t *\n\t * @param algorithm 签名算法\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm) {\n\t\treturn new Sign(algorithm);\n\t}\n\n\t/**\n\t * 创建签名算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm        签名算法\n\t * @param privateKeyBase64 私钥Base64\n\t * @param publicKeyBase64  公钥Base64\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm, String privateKeyBase64, String publicKeyBase64) {\n\t\treturn new Sign(algorithm, privateKeyBase64, publicKeyBase64);\n\t}\n\n\t/**\n\t * 创建Sign算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  算法枚举\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return {@link Sign}\n\t * @since 3.3.0\n\t */\n\tpublic static Sign sign(SignAlgorithm algorithm, byte[] privateKey, byte[] publicKey) {\n\t\treturn new Sign(algorithm, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param crypto      对称加密算法\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(SymmetricCrypto crypto, Map<?, ?> params, String... otherParams) {\n\t\treturn signParams(crypto, params, StrUtil.EMPTY, StrUtil.EMPTY, true, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串\n\t *\n\t * @param crypto            对称加密算法\n\t * @param params            参数\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(SymmetricCrypto crypto, Map<?, ?> params, String separator,\n\t\t\t\t\t\t\t\t\tString keyValueSeparator, boolean isIgnoreNull, String... otherParams) {\n\t\treturn crypto.encryptHex(MapUtil.sortJoin(params, separator, keyValueSeparator, isIgnoreNull, otherParams));\n\t}\n\n\t/**\n\t * 对参数做md5签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParamsMd5(Map<?, ?> params, String... otherParams) {\n\t\treturn signParams(DigestAlgorithm.MD5, params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做Sha1签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.8\n\t */\n\tpublic static String signParamsSha1(Map<?, ?> params, String... otherParams) {\n\t\treturn signParams(DigestAlgorithm.SHA1, params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做Sha256签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param params      参数\n\t * @param otherParams 其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParamsSha256(Map<?, ?> params, String... otherParams) {\n\t\treturn signParams(DigestAlgorithm.SHA256, params, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串<br>\n\t * 拼接后的字符串键值对之间无符号，键值对之间无符号，忽略null值\n\t *\n\t * @param digestAlgorithm 摘要算法\n\t * @param params          参数\n\t * @param otherParams     其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(DigestAlgorithm digestAlgorithm, Map<?, ?> params, String... otherParams) {\n\t\treturn signParams(digestAlgorithm, params, StrUtil.EMPTY, StrUtil.EMPTY, true, otherParams);\n\t}\n\n\t/**\n\t * 对参数做签名<br>\n\t * 参数签名为对Map参数按照key的顺序排序后拼接为字符串，然后根据提供的签名算法生成签名字符串\n\t *\n\t * @param digestAlgorithm   摘要算法\n\t * @param params            参数\n\t * @param separator         entry之间的连接符\n\t * @param keyValueSeparator kv之间的连接符\n\t * @param isIgnoreNull      是否忽略null的键和值\n\t * @param otherParams       其它附加参数字符串（例如密钥）\n\t * @return 签名\n\t * @since 4.0.1\n\t */\n\tpublic static String signParams(DigestAlgorithm digestAlgorithm, Map<?, ?> params, String separator,\n\t\t\t\t\t\t\t\t\tString keyValueSeparator, boolean isIgnoreNull, String... otherParams) {\n\t\treturn new Digester(digestAlgorithm).digestHex(MapUtil.sortJoin(params, separator, keyValueSeparator, isIgnoreNull, otherParams));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.crypto.asymmetric.SM2;\nimport cn.hutool.crypto.digest.HMac;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\nimport cn.hutool.crypto.digest.SM3;\nimport cn.hutool.crypto.digest.mac.BCHMacEngine;\nimport cn.hutool.crypto.digest.mac.MacEngine;\nimport cn.hutool.crypto.symmetric.SM4;\nimport cn.hutool.crypto.symmetric.SymmetricCrypto;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.gm.GMNamedCurves;\nimport org.bouncycastle.crypto.digests.SM3Digest;\nimport org.bouncycastle.crypto.params.ECDomainParameters;\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.crypto.params.ECPublicKeyParameters;\nimport org.bouncycastle.crypto.signers.StandardDSAEncoding;\nimport org.bouncycastle.util.Arrays;\nimport org.bouncycastle.util.encoders.Hex;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.math.BigInteger;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * SM国密算法工具类<br>\n * 此工具类依赖org.bouncycastle:bcprov-jdk15to18\n *\n * <p>封装包括：</p>\n * <ul>\n *     <li>SM2 椭圆曲线非对称加密和签名</li>\n *     <li>SM3 杂凑算法</li>\n *     <li>SM4 对称加密</li>\n * </ul>\n *\n * @author looly\n * @since 4.3.2\n */\npublic class SmUtil {\n\n\tprivate final static int RS_LEN = 32;\n\t/**\n\t * SM2默认曲线\n\t */\n\tpublic static final String SM2_CURVE_NAME = \"sm2p256v1\";\n\t/**\n\t * SM2推荐曲线参数（来自https://github.com/ZZMarquis/gmhelper）\n\t */\n\tpublic static final ECDomainParameters SM2_DOMAIN_PARAMS = BCUtil.toDomainParams(GMNamedCurves.getByName(SM2_CURVE_NAME));\n\t/**\n\t * SM2国密算法公钥参数的Oid标识\n\t */\n\tpublic static final ASN1ObjectIdentifier ID_SM2_PUBLIC_KEY_PARAM = new ASN1ObjectIdentifier(\"1.2.156.10197.1.301\");\n\n\t/**\n\t * 创建SM2算法对象<br>\n\t * 生成新的私钥公钥对\n\t *\n\t * @return {@link SM2}\n\t */\n\tpublic static SM2 sm2() {\n\t\treturn new SM2();\n\t}\n\n\t/**\n\t * 创建SM2算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr  公钥Hex或Base64表示\n\t * @return {@link SM2}\n\t */\n\tpublic static SM2 sm2(String privateKeyStr, String publicKeyStr) {\n\t\treturn new SM2(privateKeyStr, publicKeyStr);\n\t}\n\n\t/**\n\t * 创建SM2算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥，必须使用PKCS#8规范\n\t * @param publicKey  公钥，必须使用X509规范\n\t * @return {@link SM2}\n\t */\n\tpublic static SM2 sm2(byte[] privateKey, byte[] publicKey) {\n\t\treturn new SM2(privateKey, publicKey);\n\t}\n\n\t/**\n\t * 创建SM2算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return {@link SM2}\n\t * @since 5.5.9\n\t */\n\tpublic static SM2 sm2(PrivateKey privateKey, PublicKey publicKey) {\n\t\treturn new SM2(privateKey, publicKey);\n\t}\n\n\t/**\n\t * 创建SM2算法对象<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyParams 私钥参数\n\t * @param publicKeyParams  公钥参数\n\t * @return {@link SM2}\n\t * @since 5.5.9\n\t */\n\tpublic static SM2 sm2(ECPrivateKeyParameters privateKeyParams, ECPublicKeyParameters publicKeyParams) {\n\t\treturn new SM2(privateKeyParams, publicKeyParams);\n\t}\n\n\t/**\n\t * SM3加密<br>\n\t * 例：<br>\n\t * SM3加密：sm3().digest(data)<br>\n\t * SM3加密并转为16进制字符串：sm3().digestHex(data)<br>\n\t *\n\t * @return {@link SM3}\n\t */\n\tpublic static SM3 sm3() {\n\t\treturn new SM3();\n\t}\n\n\t/**\n\t * SM3加密，可以传入盐\n\t *\n\t * @param salt 加密盐\n\t * @return {@link SM3}\n\t * @since 5.7.16\n\t */\n\tpublic static SM3 sm3WithSalt(byte[] salt) {\n\t\treturn new SM3(salt);\n\t}\n\n\t/**\n\t * SM3加密，生成16进制SM3字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SM3字符串\n\t */\n\tpublic static String sm3(String data) {\n\t\treturn sm3().digestHex(data);\n\t}\n\n\t/**\n\t * SM3加密，生成16进制SM3字符串<br>\n\t *\n\t * @param data 数据\n\t * @return SM3字符串\n\t */\n\tpublic static String sm3(InputStream data) {\n\t\treturn sm3().digestHex(data);\n\t}\n\n\t/**\n\t * SM3加密文件，生成16进制SM3字符串<br>\n\t *\n\t * @param dataFile 被加密文件\n\t * @return SM3字符串\n\t */\n\tpublic static String sm3(File dataFile) {\n\t\treturn sm3().digestHex(dataFile);\n\t}\n\n\t/**\n\t * SM4加密，生成随机KEY。注意解密时必须使用相同 {@link SymmetricCrypto}对象或者使用相同KEY<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * SM4加密：sm4().encrypt(data)\n\t * SM4解密：sm4().decrypt(data)\n\t * </pre>\n\t *\n\t * @return {@link SymmetricCrypto}\n\t */\n\tpublic static SM4 sm4() {\n\t\treturn new SM4();\n\t}\n\n\t/**\n\t * SM4加密<br>\n\t * 例：\n\t *\n\t * <pre>\n\t * SM4加密：sm4(key).encrypt(data)\n\t * SM4解密：sm4(key).decrypt(data)\n\t * </pre>\n\t *\n\t * @param key 密钥\n\t * @return {@link SM4}\n\t */\n\tpublic static SM4 sm4(byte[] key) {\n\t\treturn new SM4(key);\n\t}\n\n\t/**\n\t * bc加解密使用旧标c1||c2||c3，此方法在加密后调用，将结果转化为c1||c3||c2\n\t *\n\t * @param c1c2c3             加密后的bytes，顺序为C1C2C3\n\t * @param ecDomainParameters {@link ECDomainParameters}\n\t * @return 加密后的bytes，顺序为C1C3C2\n\t */\n\tpublic static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3, ECDomainParameters ecDomainParameters) {\n\t\t// sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。\n\t\tfinal int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;\n\t\tfinal int c3Len = 32; // new SM3Digest().getDigestSize();\n\t\tbyte[] result = new byte[c1c2c3.length];\n\t\tSystem.arraycopy(c1c2c3, 0, result, 0, c1Len); // c1\n\t\tSystem.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); // c3\n\t\tSystem.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); // c2\n\t\treturn result;\n\t}\n\n\t/**\n\t * bc加解密使用旧标c1||c3||c2，此方法在解密前调用，将密文转化为c1||c2||c3再去解密\n\t *\n\t * @param c1c3c2             加密后的bytes，顺序为C1C3C2\n\t * @param ecDomainParameters {@link ECDomainParameters}\n\t * @return c1c2c3 加密后的bytes，顺序为C1C2C3\n\t */\n\tpublic static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2, ECDomainParameters ecDomainParameters) {\n\t\t// sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。\n\t\tfinal int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;\n\t\tfinal int c3Len = 32; // new SM3Digest().getDigestSize();\n\t\tbyte[] result = new byte[c1c3c2.length];\n\t\tSystem.arraycopy(c1c3c2, 0, result, 0, c1Len); // c1: 0->65\n\t\tSystem.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); // c2\n\t\tSystem.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); // c3\n\t\treturn result;\n\t}\n\n\t/**\n\t * BC的SM3withSM2签名得到的结果的rs是asn1格式的，这个方法转化成直接拼接r||s\n\t *\n\t * @param rsDer rs in asn1 format\n\t * @return sign result in plain byte array\n\t * @since 4.5.0\n\t */\n\tpublic static byte[] rsAsn1ToPlain(byte[] rsDer) {\n\t\tfinal BigInteger[] decode;\n\t\ttry {\n\t\t\tdecode = StandardDSAEncoding.INSTANCE.decode(SM2_DOMAIN_PARAMS.getN(), rsDer);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tfinal byte[] r = bigIntToFixedLengthBytes(decode[0]);\n\t\tfinal byte[] s = bigIntToFixedLengthBytes(decode[1]);\n\n\t\treturn ArrayUtil.addAll(r, s);\n\t}\n\n\t/**\n\t * BC的SM3withSM2验签需要的rs是asn1格式的，这个方法将直接拼接r||s的字节数组转化成asn1格式\n\t *\n\t * @param sign in plain byte array\n\t * @return rs result in asn1 format\n\t * @since 4.5.0\n\t */\n\tpublic static byte[] rsPlainToAsn1(byte[] sign) {\n\t\tif (sign.length != RS_LEN * 2) {\n\t\t\tthrow new CryptoException(\"err rs. \");\n\t\t}\n\t\tBigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, RS_LEN));\n\t\tBigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, RS_LEN, RS_LEN * 2));\n\t\ttry {\n\t\t\treturn StandardDSAEncoding.INSTANCE.encode(SM2_DOMAIN_PARAMS.getN(), r, s);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建HmacSM3算法的{@link MacEngine}\n\t *\n\t * @param key 密钥\n\t * @return {@link MacEngine}\n\t * @since 4.5.13\n\t */\n\tpublic static MacEngine createHmacSm3Engine(byte[] key) {\n\t\treturn new BCHMacEngine(new SM3Digest(), key);\n\t}\n\n\t/**\n\t * HmacSM3算法实现\n\t *\n\t * @param key 密钥\n\t * @return {@link HMac} 对象，调用digestXXX即可\n\t * @since 4.5.13\n\t */\n\tpublic static HMac hmacSm3(byte[] key) {\n\t\treturn new HMac(HmacAlgorithm.HmacSM3, key);\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * BigInteger转固定长度bytes\n\t *\n\t * @param rOrS {@link BigInteger}\n\t * @return 固定长度bytes\n\t * @since 4.5.0\n\t */\n\tprivate static byte[] bigIntToFixedLengthBytes(BigInteger rOrS) {\n\t\t// for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123,\n\t\t// r and s are the result of mod n, so they should be less than n and have length<=32\n\t\tbyte[] rs = rOrS.toByteArray();\n\t\tif (rs.length == RS_LEN) {\n\t\t\treturn rs;\n\t\t} else if (rs.length == RS_LEN + 1 && rs[0] == 0) {\n\t\t\treturn Arrays.copyOfRange(rs, 1, RS_LEN + 1);\n\t\t} else if (rs.length < RS_LEN) {\n\t\t\tbyte[] result = new byte[RS_LEN];\n\t\t\tArrays.fill(result, (byte) 0);\n\t\t\tSystem.arraycopy(rs, 0, result, RS_LEN - rs.length, rs.length);\n\t\t\treturn result;\n\t\t} else {\n\t\t\tthrow new CryptoException(\"Error rs: {}\", Hex.toHexString(rs));\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.XmlUtil;\nimport org.w3c.dom.Element;\n\nimport javax.crypto.spec.*;\nimport java.math.BigInteger;\nimport java.security.InvalidKeyException;\nimport java.security.spec.KeySpec;\nimport java.security.spec.RSAPrivateCrtKeySpec;\n\n/**\n * 规范相关工具类，用于生成密钥规范、参数规范等快捷方法。\n * <ul>\n *     <li>{@link KeySpec}: 密钥规范</li>\n *     <li>{@link java.security.spec.AlgorithmParameterSpec}: 参数规范</li>\n * </ul>\n *\n * @author Looly\n * @since 5.8.41\n */\npublic class SpecUtil {\n\n\t/**\n\t * 根据算法创建{@link KeySpec}\n\t * <ul>\n\t *     <li>DESede: {@link DESedeKeySpec}</li>\n\t *     <li>DES   : {@link DESedeKeySpec}</li>\n\t *     <li>其它  : {@link SecretKeySpec}</li>\n\t * </ul>\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @return {@link KeySpec}\n\t */\n\tpublic static KeySpec createKeySpec(final String algorithm, byte[] key) {\n\t\ttry {\n\t\t\tif (algorithm.startsWith(\"DESede\")) {\n\t\t\t\tif (null == key) {\n\t\t\t\t\tkey = RandomUtil.randomBytes(24);\n\t\t\t\t}\n\t\t\t\t// DESede兼容\n\t\t\t\treturn new DESedeKeySpec(key);\n\t\t\t} else if (algorithm.startsWith(\"DES\")) {\n\t\t\t\tif (null == key) {\n\t\t\t\t\tkey = RandomUtil.randomBytes(8);\n\t\t\t\t}\n\t\t\t\treturn new DESKeySpec(key);\n\t\t\t}\n\t\t} catch (final InvalidKeyException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\n\t\treturn new SecretKeySpec(key, algorithm);\n\t}\n\n\t/**\n\t * 创建{@link PBEKeySpec}<br>\n\t * PBE算法没有密钥的概念，密钥在其它对称加密算法中是经过算法计算得出来的，PBE算法则是使用口令替代了密钥。\n\t *\n\t * @param password 口令\n\t * @return {@link PBEKeySpec}\n\t */\n\tpublic static PBEKeySpec createPBEKeySpec(char[] password) {\n\t\tif (null == password) {\n\t\t\tpassword = RandomUtil.randomStringLower(32).toCharArray();\n\t\t}\n\t\treturn new PBEKeySpec(password);\n\t}\n\n\t/**\n\t * 创建{@link PBEParameterSpec}\n\t *\n\t * @param salt           加盐值\n\t * @param iterationCount 摘要次数\n\t * @return {@link PBEParameterSpec}\n\t */\n\tpublic static PBEParameterSpec createPBEParameterSpec(final byte[] salt, final int iterationCount) {\n\t\treturn new PBEParameterSpec(salt, iterationCount);\n\t}\n\n\t/**\n\t * 将XML格式的密钥参数转化为{@link RSAPrivateCrtKeySpec}，XML为C#生成格式，类似于：\n\t * <pre>{@code\n\t * <RSAKeyValue>\n\t *     <Modulus>xx</Modulus>\n\t *     <Exponent>xx</Exponent>\n\t *     <P>xxxxxxxxx</P>\n\t *     <Q>xxxxxxxxx</Q>\n\t *     <DP>xxxxxxxx</DP>\n\t *     <DQ>xxxxxxxx</DQ>\n\t *     <InverseQ>xx</InverseQ>\n\t *     <D>xxxxxxxxx</D>\n\t * </RSAKeyValue>\n\t * }</pre>\n\t *\n\t * @param xml xml格式密钥字符串\n\t * @return {@link RSAPrivateCrtKeySpec}\n\t */\n\tpublic static RSAPrivateCrtKeySpec xmlToRSAPrivateCrtKeySpec(final String xml) {\n\t\t// 1. 解析XML\n\t\tfinal Element rootElement = XmlUtil.getRootElement(XmlUtil.parseXml(xml));\n\n\t\t// 2. 提取各个字段\n\t\tfinal String modulusB64 = XmlUtil.elementText(rootElement, \"Modulus\");\n\t\tfinal String exponentB64 = XmlUtil.elementText(rootElement, \"Exponent\");\n\t\tfinal String pB64 = XmlUtil.elementText(rootElement, \"P\");\n\t\tfinal String qB64 = XmlUtil.elementText(rootElement, \"Q\");\n\t\tfinal String dpB64 = XmlUtil.elementText(rootElement, \"DP\");\n\t\tfinal String dqB64 = XmlUtil.elementText(rootElement, \"DQ\");\n\t\tfinal String inverseQB64 = XmlUtil.elementText(rootElement, \"InverseQ\");\n\t\tfinal String dB64 = XmlUtil.elementText(rootElement, \"D\");\n\n\t\t// 3. Base64解码\n\t\tfinal byte[] modulus = Base64.decode(modulusB64);\n\t\tfinal byte[] publicExponent = Base64.decode(exponentB64);\n\t\tfinal byte[] privateExponent = Base64.decode(dB64);\n\t\tfinal byte[] primeP = Base64.decode(pB64);\n\t\tfinal byte[] primeQ = Base64.decode(qB64);\n\t\tfinal byte[] primeExponentP = Base64.decode(dpB64);\n\t\tfinal byte[] primeExponentQ = Base64.decode(dqB64);\n\t\tfinal byte[] crtCoefficient = Base64.decode(inverseQB64);\n\n\t\t// 4. 创建RSAPrivateCrtKeySpec\n\t\treturn new RSAPrivateCrtKeySpec(\n\t\t\tnew BigInteger(1, modulus),\n\t\t\tnew BigInteger(1, publicExponent),\n\t\t\tnew BigInteger(1, privateExponent),\n\t\t\tnew BigInteger(1, primeP),\n\t\t\tnew BigInteger(1, primeQ),\n\t\t\tnew BigInteger(1, primeExponentP),\n\t\t\tnew BigInteger(1, primeExponentQ),\n\t\t\tnew BigInteger(1, crtCoefficient)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AbstractAsymmetricCrypto.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * 抽象的非对称加密对象，包装了加密和解密为Hex和Base64的封装\n *\n * @param <T> 返回自身类型\n * @author Looly\n */\npublic abstract class AbstractAsymmetricCrypto<T extends AbstractAsymmetricCrypto<T>>\n\t\textends BaseAsymmetric<T>\n\t\timplements AsymmetricEncryptor, AsymmetricDecryptor{\n\tprivate static final long serialVersionUID = 1L;\n\n\t// ------------------------------------------------------------------ Constructor start\n\t/**\n\t * 构造\n\t * <p>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @since 3.1.1\n\t */\n\tpublic AbstractAsymmetricCrypto(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(algorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricAlgorithm.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\n/**\n * 非对称算法类型<br>\n * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator\n * \n * @author Looly\n *\n */\npublic enum AsymmetricAlgorithm {\n\t/** RSA算法 */\n\tRSA(\"RSA\"), \n\t/** RSA算法，此算法用了默认补位方式为RSA/ECB/PKCS1Padding */\n\tRSA_ECB_PKCS1(\"RSA/ECB/PKCS1Padding\"), \n\t/** RSA算法，此算法用了默认补位方式为RSA/ECB/NoPadding */\n\tRSA_ECB(\"RSA/ECB/NoPadding\"),\n\t/** RSA算法，此算法用了RSA/None/NoPadding */\n\tRSA_None(\"RSA/None/NoPadding\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t * @param value 算法字符表示，区分大小写\n\t */\n\tAsymmetricAlgorithm(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取算法字符串表示，区分大小写\n\t * @return 算法字符串表示\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricCrypto.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.crypto.CipherWrapper;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport cn.hutool.crypto.symmetric.SymmetricAlgorithm;\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.IllegalBlockSizeException;\nimport java.io.IOException;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.Key;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.SecureRandom;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * 非对称加密算法\n *\n * <pre>\n * 1、签名：使用私钥加密，公钥解密。\n * 用于让所有公钥所有者验证私钥所有者的身份并且用来防止私钥所有者发布的内容被篡改，但是不用来保证内容不被他人获得。\n *\n * 2、加密：用公钥加密，私钥解密。\n * 用于向公钥所有者发布信息,这个信息可能被他人篡改,但是无法被他人获得。\n * </pre>\n *\n * @author Looly\n */\npublic class AsymmetricCrypto extends AbstractAsymmetricCrypto<AsymmetricCrypto> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * Cipher负责完成加密或解密工作\n\t */\n\tprotected CipherWrapper cipherWrapper;\n\n\t/**\n\t * 加密的块大小\n\t */\n\tprotected int encryptBlockSize = -1;\n\t/**\n\t * 解密的块大小\n\t */\n\tprotected int decryptBlockSize = -1;\n\t// ------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造，创建新的私钥公钥对\n\t *\n\t * @param algorithm {@link SymmetricAlgorithm}\n\t */\n\t@SuppressWarnings(\"RedundantCast\")\n\tpublic AsymmetricCrypto(AsymmetricAlgorithm algorithm) {\n\t\tthis(algorithm, (byte[]) null, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造，创建新的私钥公钥对\n\t *\n\t * @param algorithm 算法\n\t */\n\t@SuppressWarnings(\"RedundantCast\")\n\tpublic AsymmetricCrypto(String algorithm) {\n\t\tthis(algorithm, (byte[]) null, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm     {@link SymmetricAlgorithm}\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr  公钥Hex或Base64表示\n\t */\n\tpublic AsymmetricCrypto(AsymmetricAlgorithm algorithm, String privateKeyStr, String publicKeyStr) {\n\t\tthis(algorithm.getValue(), SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr));\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  {@link SymmetricAlgorithm}\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic AsymmetricCrypto(AsymmetricAlgorithm algorithm, byte[] privateKey, byte[] publicKey) {\n\t\tthis(algorithm.getValue(), privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  {@link SymmetricAlgorithm}\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @since 3.1.1\n\t */\n\tpublic AsymmetricCrypto(AsymmetricAlgorithm algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tthis(algorithm.getValue(), privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm        非对称加密算法\n\t * @param privateKeyBase64 私钥Base64\n\t * @param publicKeyBase64  公钥Base64\n\t */\n\tpublic AsymmetricCrypto(String algorithm, String privateKeyBase64, String publicKeyBase64) {\n\t\tthis(algorithm, Base64.decode(privateKeyBase64), Base64.decode(publicKeyBase64));\n\t}\n\n\t/**\n\t * 构造\n\t * <p>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic AsymmetricCrypto(String algorithm, byte[] privateKey, byte[] publicKey) {\n\t\tthis(algorithm, //\n\t\t\t\tKeyUtil.generatePrivateKey(algorithm, privateKey), //\n\t\t\t\tKeyUtil.generatePublicKey(algorithm, publicKey)//\n\t\t);\n\t}\n\n\t/**\n\t * 构造\n\t * <p>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @since 3.1.1\n\t */\n\tpublic AsymmetricCrypto(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(algorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 获取加密块大小\n\t *\n\t * @return 加密块大小\n\t */\n\tpublic int getEncryptBlockSize() {\n\t\treturn encryptBlockSize;\n\t}\n\n\t/**\n\t * 设置加密块大小\n\t *\n\t * @param encryptBlockSize 加密块大小\n\t */\n\tpublic void setEncryptBlockSize(int encryptBlockSize) {\n\t\tlock.lock();\n\t\ttry{\n\t\t\tthis.encryptBlockSize = encryptBlockSize;\n\t\t}finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获取解密块大小\n\t *\n\t * @return 解密块大小\n\t */\n\tpublic int getDecryptBlockSize() {\n\t\treturn decryptBlockSize;\n\t}\n\n\t/**\n\t * 设置解密块大小\n\t *\n\t * @param decryptBlockSize 解密块大小\n\t */\n\tpublic void setDecryptBlockSize(int decryptBlockSize) {\n\t\tlock.lock();\n\t\ttry{\n\t\t\tthis.decryptBlockSize = decryptBlockSize;\n\t\t}finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获取{@link AlgorithmParameterSpec}<br>\n\t * 在某些算法中，需要特别的参数，例如在ECIES中，此处为IESParameterSpec\n\t *\n\t * @return {@link AlgorithmParameterSpec}\n\t * @since 5.4.3\n\t */\n\tpublic AlgorithmParameterSpec getAlgorithmParameterSpec() {\n\t\treturn this.cipherWrapper.getParams();\n\t}\n\n\t/**\n\t * 设置{@link AlgorithmParameterSpec}<br>\n\t * 在某些算法中，需要特别的参数，例如在ECIES中，此处为IESParameterSpec\n\t *\n\t * @param algorithmParameterSpec {@link AlgorithmParameterSpec}\n\t * @since 5.4.3\n\t */\n\tpublic void setAlgorithmParameterSpec(AlgorithmParameterSpec algorithmParameterSpec) {\n\t\tlock.lock();\n\t\ttry{\n\t\t\tthis.cipherWrapper.setParams(algorithmParameterSpec);\n\t\t}finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 设置随机数生成器，可自定义随机数种子\n\t *\n\t * @param random 随机数生成器，可自定义随机数种子\n\t * @return this\n\t * @since 5.7.17\n\t */\n\tpublic AsymmetricCrypto setRandom(SecureRandom random) {\n\t\tlock.lock();\n\t\ttry{\n\t\t\tthis.cipherWrapper.setRandom(random);\n\t\t}finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic AsymmetricCrypto init(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tlock.lock();\n\t\ttry{\n\t\t\tsuper.init(algorithm, privateKey, publicKey);\n\t\t\tinitCipher();\n\t\t}finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------------------------- Encrypt\n\n\t@Override\n\tpublic byte[] encrypt(byte[] data, KeyType keyType) {\n\t\tfinal Key key = getKeyByType(keyType);\n\t\tlock.lock();\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.ENCRYPT_MODE, key);\n\n\t\t\tif (this.encryptBlockSize < 0) {\n\t\t\t\t// 在引入BC库情况下，自动获取块大小\n\t\t\t\tfinal int blockSize = cipher.getBlockSize();\n\t\t\t\tif (blockSize > 0) {\n\t\t\t\t\tthis.encryptBlockSize = blockSize;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn doFinal(data, this.encryptBlockSize < 0 ? data.length : this.encryptBlockSize);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------- Decrypt\n\n\t@Override\n\tpublic byte[] decrypt(byte[] data, KeyType keyType) {\n\t\tfinal Key key = getKeyByType(keyType);\n\t\tlock.lock();\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.DECRYPT_MODE, key);\n\n\t\t\tif (this.decryptBlockSize < 0) {\n\t\t\t\t// 在引入BC库情况下，自动获取块大小\n\t\t\t\tfinal int blockSize = cipher.getBlockSize();\n\t\t\t\tif (blockSize > 0) {\n\t\t\t\t\tthis.decryptBlockSize = blockSize;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn doFinal(data, this.decryptBlockSize < 0 ? data.length : this.decryptBlockSize);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------- Getters and Setters\n\n\t/**\n\t * 获得加密或解密器\n\t *\n\t * @return 加密或解密\n\t * @since 5.4.3\n\t */\n\tpublic Cipher getCipher() {\n\t\treturn this.cipherWrapper.getCipher();\n\t}\n\n\t/**\n\t * 初始化{@link Cipher}，默认尝试加载BC库\n\t *\n\t * @since 4.5.2\n\t */\n\tprotected void initCipher() {\n\t\tthis.cipherWrapper = new CipherWrapper(this.algorithm);\n\t}\n\n\t/**\n\t * 加密或解密\n\t *\n\t * @param data         被加密或解密的内容数据\n\t * @param maxBlockSize 最大块（分段）大小\n\t * @return 加密或解密后的数据\n\t * @throws IllegalBlockSizeException 分段异常\n\t * @throws BadPaddingException       padding错误异常\n\t * @throws IOException               IO异常，不会被触发\n\t */\n\tprivate byte[] doFinal(byte[] data, int maxBlockSize) throws IllegalBlockSizeException, BadPaddingException, IOException {\n\t\t// 模长\n\t\tfinal int dataLength = data.length;\n\n\t\t// 不足分段\n\t\tif (dataLength <= maxBlockSize) {\n\t\t\treturn getCipher().doFinal(data, 0, dataLength);\n\t\t}\n\n\t\t// 分段解密\n\t\treturn doFinalWithBlock(data, maxBlockSize);\n\t}\n\n\t/**\n\t * 分段加密或解密\n\t *\n\t * @param data         数据\n\t * @param maxBlockSize 最大分段的段大小，不能为小于1\n\t * @return 加密或解密后的数据\n\t * @throws IllegalBlockSizeException 分段异常\n\t * @throws BadPaddingException       padding错误异常\n\t * @throws IOException               IO异常，不会被触发\n\t */\n\tprivate byte[] doFinalWithBlock(byte[] data, int maxBlockSize) throws IllegalBlockSizeException, BadPaddingException, IOException {\n\t\tfinal int dataLength = data.length;\n\t\t@SuppressWarnings(\"resource\") final FastByteArrayOutputStream out = new FastByteArrayOutputStream();\n\n\t\tint offSet = 0;\n\t\t// 剩余长度\n\t\tint remainLength = dataLength;\n\t\tint blockSize;\n\t\t// 对数据分段处理\n\t\twhile (remainLength > 0) {\n\t\t\tblockSize = Math.min(remainLength, maxBlockSize);\n\t\t\tout.write(getCipher().doFinal(data, offSet, blockSize));\n\n\t\t\toffSet += blockSize;\n\t\t\tremainLength = dataLength - offSet;\n\t\t}\n\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 初始化{@link Cipher}的模式，如加密模式或解密模式\n\t *\n\t * @param mode 模式，可选{@link Cipher#ENCRYPT_MODE}或者{@link Cipher#DECRYPT_MODE}\n\t * @param key  密钥\n\t * @return {@link Cipher}\n\t * @throws InvalidAlgorithmParameterException 异常算法错误\n\t * @throws InvalidKeyException                异常KEY错误\n\t */\n\tprivate Cipher initMode(int mode, Key key) throws InvalidAlgorithmParameterException, InvalidKeyException {\n\t\treturn this.cipherWrapper.initMode(mode, key).getCipher();\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricDecryptor.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.BCD;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 非对称解密器接口，提供：\n * <ul>\n *     <li>从bytes解密</li>\n *     <li>从Hex(16进制)解密</li>\n *     <li>从Base64解密</li>\n *     <li>从BCD解密</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.12\n */\npublic interface AsymmetricDecryptor {\n\n\t/**\n\t * 解密\n\t *\n\t * @param bytes   被解密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 解密后的bytes\n\t */\n\tbyte[] decrypt(byte[] bytes, KeyType keyType);\n\n\t/**\n\t * 解密\n\t *\n\t * @param data    被解密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 解密后的bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tdefault byte[] decrypt(InputStream data, KeyType keyType) throws IORuntimeException {\n\t\treturn decrypt(IoUtil.readBytes(data), keyType);\n\t}\n\n\t/**\n\t * 从Hex或Base64字符串解密，编码为UTF-8格式\n\t *\n\t * @param data    Hex（16进制）或Base64字符串\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 解密后的bytes\n\t * @since 4.5.2\n\t */\n\tdefault byte[] decrypt(String data, KeyType keyType) {\n\t\treturn decrypt(SecureUtil.decode(data), keyType);\n\t}\n\n\t/**\n\t * 解密为字符串，密文需为Hex（16进制）或Base64字符串\n\t *\n\t * @param data    数据，Hex（16进制）或Base64字符串\n\t * @param keyType 密钥类型\n\t * @param charset 加密前编码\n\t * @return 解密后的密文\n\t * @since 4.5.2\n\t */\n\tdefault String decryptStr(String data, KeyType keyType, Charset charset) {\n\t\treturn StrUtil.str(decrypt(data, keyType), charset);\n\t}\n\n\t/**\n\t * 解密为字符串，密文需为Hex（16进制）或Base64字符串\n\t *\n\t * @param data    数据，Hex（16进制）或Base64字符串\n\t * @param keyType 密钥类型\n\t * @return 解密后的密文\n\t * @since 4.5.2\n\t */\n\tdefault String decryptStr(String data, KeyType keyType) {\n\t\treturn decryptStr(data, keyType, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 解密BCD\n\t *\n\t * @param data    数据\n\t * @param keyType 密钥类型\n\t * @return 解密后的密文\n\t * @since 4.1.0\n\t */\n\tdefault byte[] decryptFromBcd(String data, KeyType keyType) {\n\t\treturn decryptFromBcd(data, keyType, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 分组解密\n\t *\n\t * @param data    数据\n\t * @param keyType 密钥类型\n\t * @param charset 加密前编码\n\t * @return 解密后的密文\n\t * @since 4.1.0\n\t * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n\t */\n\t@Deprecated\n\tdefault byte[] decryptFromBcd(String data, KeyType keyType, Charset charset) {\n\t\tAssert.notNull(data, \"Bcd string must be not null!\");\n\t\tfinal byte[] dataBytes = BCD.ascToBcd(StrUtil.bytes(data, charset));\n\t\treturn decrypt(dataBytes, keyType);\n\t}\n\n\t/**\n\t * 解密为字符串，密文需为BCD格式\n\t *\n\t * @param data    数据，BCD格式\n\t * @param keyType 密钥类型\n\t * @param charset 加密前编码\n\t * @return 解密后的密文\n\t * @since 4.5.2\n\t * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n\t */\n\t@Deprecated\n\tdefault String decryptStrFromBcd(String data, KeyType keyType, Charset charset) {\n\t\treturn StrUtil.str(decryptFromBcd(data, keyType, charset), charset);\n\t}\n\n\t/**\n\t * 解密为字符串，密文需为BCD格式，编码为UTF-8格式\n\t *\n\t * @param data    数据，BCD格式\n\t * @param keyType 密钥类型\n\t * @return 解密后的密文\n\t * @since 4.5.2\n\t * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n\t */\n\t@Deprecated\n\tdefault String decryptStrFromBcd(String data, KeyType keyType) {\n\t\treturn decryptStrFromBcd(data, keyType, CharsetUtil.CHARSET_UTF_8);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricEncryptor.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.BCD;\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 非对称加密器接口，提供：\n * <ul>\n *     <li>加密为bytes</li>\n *     <li>加密为Hex(16进制)</li>\n *     <li>加密为Base64</li>\n *     <li>加密为BCD</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.12\n */\npublic interface AsymmetricEncryptor {\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t */\n\tbyte[] encrypt(byte[] data, KeyType keyType);\n\n\t/**\n\t * 编码为Hex字符串\n\t *\n\t * @param data    被加密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Hex字符串\n\t */\n\tdefault String encryptHex(byte[] data, KeyType keyType) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 编码为Base64字符串\n\t *\n\t * @param data    被加密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Base64字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptBase64(byte[] data, KeyType keyType) {\n\t\treturn Base64.encode(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data, String charset, KeyType keyType) {\n\t\treturn encrypt(StrUtil.bytes(data, charset), keyType);\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data, Charset charset, KeyType keyType) {\n\t\treturn encrypt(StrUtil.bytes(data, charset), keyType);\n\t}\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data    被加密的字符串\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data, KeyType keyType) {\n\t\treturn encrypt(StrUtil.utf8Bytes(data), keyType);\n\t}\n\n\t/**\n\t * 编码为Hex字符串\n\t *\n\t * @param data    被加密的字符串\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Hex字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptHex(String data, KeyType keyType) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 编码为Hex字符串\n\t *\n\t * @param data    被加密的bytes\n\t * @param charset 编码\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Hex字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptHex(String data, Charset charset, KeyType keyType) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, charset, keyType));\n\t}\n\n\t/**\n\t * 编码为Base64字符串，使用UTF-8编码\n\t *\n\t * @param data    被加密的字符串\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Base64字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptBase64(String data, KeyType keyType) {\n\t\treturn Base64.encode(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 编码为Base64字符串\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Base64字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptBase64(String data, Charset charset, KeyType keyType) {\n\t\treturn Base64.encode(encrypt(data, charset, keyType));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的数据流\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tdefault byte[] encrypt(InputStream data, KeyType keyType) throws IORuntimeException {\n\t\treturn encrypt(IoUtil.readBytes(data), keyType);\n\t}\n\n\t/**\n\t * 编码为Hex字符串\n\t *\n\t * @param data    被加密的数据流\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Hex字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptHex(InputStream data, KeyType keyType) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 编码为Base64字符串\n\t *\n\t * @param data    被加密的数据流\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return Base64字符串\n\t * @since 4.0.1\n\t */\n\tdefault String encryptBase64(InputStream data, KeyType keyType) {\n\t\treturn Base64.encode(encrypt(data, keyType));\n\t}\n\n\t/**\n\t * 分组加密\n\t *\n\t * @param data    数据\n\t * @param keyType 密钥类型\n\t * @return 加密后的密文\n\t * @since 4.1.0\n\t * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n\t */\n\t@Deprecated\n\tdefault String encryptBcd(String data, KeyType keyType) {\n\t\treturn encryptBcd(data, keyType, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 分组加密\n\t *\n\t * @param data    数据\n\t * @param keyType 密钥类型\n\t * @param charset 加密前编码\n\t * @return 加密后的密文\n\t * @since 4.1.0\n\t * @deprecated 由于对于ASCII的编码解码有缺陷，且这种BCD实现并不规范，因此会在6.0.0中移除\n\t */\n\t@Deprecated\n\tdefault String encryptBcd(String data, KeyType keyType, Charset charset) {\n\t\treturn BCD.bcdToStr(encrypt(data, charset, keyType));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/BaseAsymmetric.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.KeyUtil;\n\nimport java.io.Serializable;\nimport java.security.Key;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * 非对称基础，提供锁、私钥和公钥的持有\n *\n * @author Looly\n * @since 3.3.0\n */\npublic class BaseAsymmetric<T extends BaseAsymmetric<T>> implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 算法\n\t */\n\tprotected String algorithm;\n\t/**\n\t * 公钥\n\t */\n\tprotected PublicKey publicKey;\n\t/**\n\t * 私钥\n\t */\n\tprotected PrivateKey privateKey;\n\t/**\n\t * 锁\n\t */\n\tprotected final Lock lock = new ReentrantLock();\n\n\t// ------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造\n\t * <p>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @since 3.1.1\n\t */\n\tpublic BaseAsymmetric(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tinit(algorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 初始化<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密（签名）或者解密（校验）\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprotected T init(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tthis.algorithm = algorithm;\n\n\t\tif (null == privateKey && null == publicKey) {\n\t\t\tinitKeys();\n\t\t} else {\n\t\t\tif (null != privateKey) {\n\t\t\t\tthis.privateKey = privateKey;\n\t\t\t}\n\t\t\tif (null != publicKey) {\n\t\t\t\tthis.publicKey = publicKey;\n\t\t\t}\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 生成公钥和私钥\n\t *\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T initKeys() {\n\t\tKeyPair keyPair = KeyUtil.generateKeyPair(this.algorithm);\n\t\tthis.publicKey = keyPair.getPublic();\n\t\tthis.privateKey = keyPair.getPrivate();\n\t\treturn (T) this;\n\t}\n\n\t// --------------------------------------------------------------------------------- Getters and Setters\n\n\t/**\n\t * 获得公钥\n\t *\n\t * @return 获得公钥\n\t */\n\tpublic PublicKey getPublicKey() {\n\t\treturn this.publicKey;\n\t}\n\n\t/**\n\t * 获得公钥\n\t *\n\t * @return 获得公钥\n\t */\n\tpublic String getPublicKeyBase64() {\n\t\tfinal PublicKey publicKey = getPublicKey();\n\t\treturn (null == publicKey) ? null : Base64.encode(publicKey.getEncoded());\n\t}\n\n\t/**\n\t * 设置公钥\n\t *\n\t * @param publicKey 公钥\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T setPublicKey(PublicKey publicKey) {\n\t\tthis.publicKey = publicKey;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 获得私钥\n\t *\n\t * @return 获得私钥\n\t */\n\tpublic PrivateKey getPrivateKey() {\n\t\treturn this.privateKey;\n\t}\n\n\t/**\n\t * 获得私钥\n\t *\n\t * @return 获得私钥\n\t */\n\tpublic String getPrivateKeyBase64() {\n\t\tfinal PrivateKey privateKey = getPrivateKey();\n\t\treturn (null == privateKey) ? null : Base64.encode(privateKey.getEncoded());\n\t}\n\n\t/**\n\t * 设置私钥\n\t *\n\t * @param privateKey 私钥\n\t * @return this\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T setPrivateKey(PrivateKey privateKey) {\n\t\tthis.privateKey = privateKey;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置密钥，可以是公钥{@link PublicKey}或者私钥{@link PrivateKey}\n\t *\n\t * @param key 密钥，可以是公钥{@link PublicKey}或者私钥{@link PrivateKey}\n\t * @return this\n\t * @since 5.2.0\n\t */\n\tpublic T setKey(Key key) {\n\t\tAssert.notNull(key, \"key must be not null !\");\n\n\t\tif (key instanceof PublicKey) {\n\t\t\treturn setPublicKey((PublicKey) key);\n\t\t} else if (key instanceof PrivateKey) {\n\t\t\treturn setPrivateKey((PrivateKey) key);\n\t\t}\n\t\tthrow new CryptoException(\"Unsupported key type: {}\", key.getClass());\n\t}\n\n\t/**\n\t * 根据密钥类型获得相应密钥\n\t *\n\t * @param type 类型 {@link KeyType}\n\t * @return {@link Key}\n\t */\n\tprotected Key getKeyByType(KeyType type) {\n\t\tswitch (type) {\n\t\t\tcase PrivateKey:\n\t\t\t\tif (null == this.privateKey) {\n\t\t\t\t\tthrow new NullPointerException(\"Private key must not null when use it !\");\n\t\t\t\t}\n\t\t\t\treturn this.privateKey;\n\t\t\tcase PublicKey:\n\t\t\t\tif (null == this.publicKey) {\n\t\t\t\t\tthrow new NullPointerException(\"Public key must not null when use it !\");\n\t\t\t\t}\n\t\t\t\treturn this.publicKey;\n\t\t}\n\t\tthrow new CryptoException(\"Unsupported key type: \" + type);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/ECIES.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * ECIES（集成加密方案，elliptic curve integrate encrypt scheme）\n *\n * <p>\n * 详细介绍见：https://blog.csdn.net/baidu_26954729/article/details/90437344\n * 此算法必须引入Bouncy Castle库\n *\n * @author loolly\n * @since 5.3.10\n */\npublic class ECIES extends AsymmetricCrypto{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 默认的ECIES算法 */\n\tprivate static final String ALGORITHM_ECIES = \"ECIES\";\n\n\t// ------------------------------------------------------------------ Constructor start\n\t/**\n\t * 构造，生成新的私钥公钥对\n\t */\n\tpublic ECIES() {\n\t\tsuper(ALGORITHM_ECIES);\n\t}\n\n\t/**\n\t * 构造，生成新的私钥公钥对\n\t *\n\t * @param eciesAlgorithm 自定义ECIES算法，例如ECIESwithDESede/NONE/PKCS7Padding\n\t */\n\tpublic ECIES(String eciesAlgorithm) {\n\t\tsuper(eciesAlgorithm);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr 公钥Hex或Base64表示\n\t */\n\tpublic ECIES(String privateKeyStr, String publicKeyStr) {\n\t\tsuper(ALGORITHM_ECIES, privateKeyStr, publicKeyStr);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param eciesAlgorithm 自定义ECIES算法，例如ECIESwithDESede/NONE/PKCS7Padding\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr 公钥Hex或Base64表示\n\t * @since 4.5.8\n\t */\n\tpublic ECIES(String eciesAlgorithm, String privateKeyStr, String publicKeyStr) {\n\t\tsuper(eciesAlgorithm, privateKeyStr, publicKeyStr);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t */\n\tpublic ECIES(byte[] privateKey, byte[] publicKey) {\n\t\tsuper(ALGORITHM_ECIES, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t * @since 3.1.1\n\t */\n\tpublic ECIES(PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(ALGORITHM_ECIES, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param eciesAlgorithm 自定义ECIES算法，例如ECIESwithDESede/NONE/PKCS7Padding\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t * @since 4.5.8\n\t */\n\tpublic ECIES(String eciesAlgorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(eciesAlgorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/KeyType.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport javax.crypto.Cipher;\n\n/**\n * 密钥类型\n * \n * @author Looly\n *\n */\npublic enum KeyType {\n\t/**\n\t * 公钥\n\t */\n\tPublicKey(Cipher.PUBLIC_KEY),\n\t/**\n\t * 私钥\n\t */\n\tPrivateKey(Cipher.PRIVATE_KEY),\n\t/**\n\t * 密钥\n\t */\n\tSecretKey(Cipher.SECRET_KEY);\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 见{@link Cipher}\n\t */\n\tKeyType(int value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final int value;\n\n\t/**\n\t * 获取枚举值对应的int表示\n\t *\n\t * @return 枚举值对应的int表示\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n}"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/RSA.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.GlobalBouncyCastleProvider;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.math.BigInteger;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.interfaces.RSAKey;\nimport java.security.spec.RSAPrivateKeySpec;\nimport java.security.spec.RSAPublicKeySpec;\n\n/**\n * <p>\n * RSA公钥/私钥/签名加密解密\n * </p>\n * <p>\n * 罗纳德·李维斯特（Ron [R]ivest）、阿迪·萨莫尔（Adi [S]hamir）和伦纳德·阿德曼（Leonard [A]dleman）\n * </p>\n * <p>\n * 由于非对称加密速度极其缓慢，一般文件不使用它来加密而是使用对称加密，<br>\n * 非对称加密算法可以用来对对称加密的密钥加密，这样保证密钥的安全也就保证了数据的安全\n * </p>\n *\n * @author Looly\n *\n */\npublic class RSA extends AsymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 默认的RSA算法 */\n\tprivate static final AsymmetricAlgorithm ALGORITHM_RSA = AsymmetricAlgorithm.RSA_ECB_PKCS1;\n\n\t// ------------------------------------------------------------------ Static method start\n\t/**\n\t * 生成RSA私钥\n\t *\n\t * @param modulus N特征值\n\t * @param privateExponent d特征值\n\t * @return {@link PrivateKey}\n\t */\n\tpublic static PrivateKey generatePrivateKey(BigInteger modulus, BigInteger privateExponent) {\n\t\treturn SecureUtil.generatePrivateKey(ALGORITHM_RSA.getValue(), new RSAPrivateKeySpec(modulus, privateExponent));\n\t}\n\n\t/**\n\t * 生成RSA公钥\n\t *\n\t * @param modulus N特征值\n\t * @param publicExponent e特征值\n\t * @return {@link PublicKey}\n\t */\n\tpublic static PublicKey generatePublicKey(BigInteger modulus, BigInteger publicExponent) {\n\t\treturn SecureUtil.generatePublicKey(ALGORITHM_RSA.getValue(), new RSAPublicKeySpec(modulus, publicExponent));\n\t}\n\t// ------------------------------------------------------------------ Static method end\n\n\t// ------------------------------------------------------------------ Constructor start\n\t/**\n\t * 构造，生成新的私钥公钥对\n\t */\n\tpublic RSA() {\n\t\tsuper(ALGORITHM_RSA);\n\t}\n\n\t/**\n\t * 构造，生成新的私钥公钥对\n\t *\n\t * @param rsaAlgorithm 自定义RSA算法，例如RSA/ECB/PKCS1Padding\n\t */\n\tpublic RSA(String rsaAlgorithm) {\n\t\tsuper(rsaAlgorithm);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr 公钥Hex或Base64表示\n\t */\n\tpublic RSA(String privateKeyStr, String publicKeyStr) {\n\t\tsuper(ALGORITHM_RSA, privateKeyStr, publicKeyStr);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param rsaAlgorithm 自定义RSA算法，例如RSA/ECB/PKCS1Padding\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr 公钥Hex或Base64表示\n\t * @since 4.5.8\n\t */\n\tpublic RSA(String rsaAlgorithm, String privateKeyStr, String publicKeyStr) {\n\t\tsuper(rsaAlgorithm, privateKeyStr, publicKeyStr);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t */\n\tpublic RSA(byte[] privateKey, byte[] publicKey) {\n\t\tsuper(ALGORITHM_RSA, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param modulus N特征值\n\t * @param privateExponent d特征值\n\t * @param publicExponent e特征值\n\t * @since 3.1.1\n\t */\n\tpublic RSA(BigInteger modulus, BigInteger privateExponent, BigInteger publicExponent) {\n\t\tthis(generatePrivateKey(modulus, privateExponent), generatePublicKey(modulus, publicExponent));\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t * @since 3.1.1\n\t */\n\tpublic RSA(PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(ALGORITHM_RSA, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param rsaAlgorithm 自定义RSA算法，例如RSA/ECB/PKCS1Padding\n\t * @param privateKey 私钥\n\t * @param publicKey 公钥\n\t * @since 4.5.8\n\t */\n\tpublic RSA(String rsaAlgorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(rsaAlgorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n\n\t@Override\n\tpublic byte[] encrypt(byte[] data, KeyType keyType) {\n\t\t// 在非使用BC库情况下，blockSize使用默认的算法\n\t\tif (this.encryptBlockSize < 0 && null == GlobalBouncyCastleProvider.INSTANCE.getProvider()) {\n\t\t\t// 加密数据长度 <= 模长-11\n\t\t\tthis.encryptBlockSize = ((RSAKey) getKeyByType(keyType)).getModulus().bitLength() / 8 - 11;\n\t\t}\n\t\treturn super.encrypt(data, keyType);\n\t}\n\n\t@Override\n\tpublic byte[] decrypt(byte[] bytes, KeyType keyType) {\n\t\t// 在非使用BC库情况下，blockSize使用默认的算法\n\t\tif (this.decryptBlockSize < 0 && null == GlobalBouncyCastleProvider.INSTANCE.getProvider()) {\n\t\t\t// 加密数据长度 <= 模长-11\n\t\t\tthis.decryptBlockSize = ((RSAKey) getKeyByType(keyType)).getModulus().bitLength() / 8;\n\t\t}\n\t\treturn super.decrypt(bytes, keyType);\n\t}\n\n\t@Override\n\tprotected void initCipher() {\n\t\ttry {\n\t\t\tsuper.initCipher();\n\t\t} catch (CryptoException e) {\n\t\t\tfinal Throwable cause = e.getCause();\n\t\t\tif(cause instanceof NoSuchAlgorithmException) {\n\t\t\t\t// 在Linux下，未引入BC库可能会导致RSA/ECB/PKCS1Padding算法无法找到，此时使用默认算法\n\t\t\t\tthis.algorithm = AsymmetricAlgorithm.RSA.getValue();\n\t\t\t\tsuper.initCipher();\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.crypto.BCUtil;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.ECKeyUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.Digest;\nimport org.bouncycastle.crypto.InvalidCipherTextException;\nimport org.bouncycastle.crypto.digests.SM3Digest;\nimport org.bouncycastle.crypto.engines.SM2Engine;\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.crypto.params.ECPublicKeyParameters;\nimport org.bouncycastle.crypto.params.ParametersWithID;\nimport org.bouncycastle.crypto.params.ParametersWithRandom;\nimport org.bouncycastle.crypto.signers.DSAEncoding;\nimport org.bouncycastle.crypto.signers.PlainDSAEncoding;\nimport org.bouncycastle.crypto.signers.SM2Signer;\nimport org.bouncycastle.crypto.signers.StandardDSAEncoding;\nimport org.bouncycastle.util.BigIntegers;\nimport org.bouncycastle.util.encoders.Hex;\n\nimport java.math.BigInteger;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * 国密SM2非对称算法实现，基于BC库<br>\n * SM2算法只支持公钥加密，私钥解密<br>\n * 参考：https://blog.csdn.net/pridas/article/details/86118774\n *\n * <p>\n * 国密算法包括：\n * <ol>\n *     <li>非对称加密和签名：SM2</li>\n *     <li>摘要签名算法：SM3</li>\n *     <li>对称加密：SM4</li>\n * </ol>\n *\n * @author looly\n * @since 4.3.2\n */\npublic class SM2 extends AbstractAsymmetricCrypto<SM2> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 算法EC\n\t */\n\tprivate static final String ALGORITHM_SM2 = \"SM2\";\n\n\tprotected SM2Engine engine;\n\tprotected SM2Signer signer;\n\n\tprivate ECPrivateKeyParameters privateKeyParams;\n\tprivate ECPublicKeyParameters publicKeyParams;\n\n\tprivate DSAEncoding encoding = StandardDSAEncoding.INSTANCE;\n\tprivate Digest digest = new SM3Digest();\n\tprivate SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2;\n\n\t// ------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造，生成新的私钥公钥对\n\t */\n\tpublic SM2() {\n\t\tthis(null, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyStr 私钥Hex或Base64表示，必须使用PKCS#8规范\n\t * @param publicKeyStr  公钥Hex或Base64表示，必须使用X509规范\n\t */\n\tpublic SM2(String privateKeyStr, String publicKeyStr) {\n\t\tthis(SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr));\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥，可以使用PKCS#8、D值或PKCS#1规范\n\t * @param publicKey  公钥，可以使用X509、Q值或PKCS#1规范\n\t */\n\tpublic SM2(byte[] privateKey, byte[] publicKey) {\n\t\tthis(\n\t\t\t\tECKeyUtil.decodePrivateKeyParams(privateKey),\n\t\t\t\tECKeyUtil.decodePublicKeyParams(publicKey)\n\t\t);\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic SM2(PrivateKey privateKey, PublicKey publicKey) {\n\t\tthis(BCUtil.toParams(privateKey), BCUtil.toParams(publicKey));\n\t\tif (null != privateKey) {\n\t\t\tthis.privateKey = privateKey;\n\t\t}\n\t\tif (null != publicKey) {\n\t\t\tthis.publicKey = publicKey;\n\t\t}\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyHex      私钥16进制\n\t * @param publicKeyPointXHex 公钥X16进制\n\t * @param publicKeyPointYHex 公钥Y16进制\n\t * @since 5.2.0\n\t */\n\tpublic SM2(String privateKeyHex, String publicKeyPointXHex, String publicKeyPointYHex) {\n\t\tthis(BCUtil.toSm2Params(privateKeyHex), BCUtil.toSm2Params(publicKeyPointXHex, publicKeyPointYHex));\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKey      私钥\n\t * @param publicKeyPointX 公钥X\n\t * @param publicKeyPointY 公钥Y\n\t * @since 5.2.0\n\t */\n\tpublic SM2(byte[] privateKey, byte[] publicKeyPointX, byte[] publicKeyPointY) {\n\t\tthis(BCUtil.toSm2Params(privateKey), BCUtil.toSm2Params(publicKeyPointX, publicKeyPointY));\n\t}\n\n\t/**\n\t * 构造 <br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密或者解密\n\t *\n\t * @param privateKeyParams 私钥，可以为null\n\t * @param publicKeyParams  公钥，可以为null\n\t */\n\tpublic SM2(ECPrivateKeyParameters privateKeyParams, ECPublicKeyParameters publicKeyParams) {\n\t\tsuper(ALGORITHM_SM2, null, null);\n\t\tthis.privateKeyParams = privateKeyParams;\n\t\tthis.publicKeyParams = publicKeyParams;\n\t\tthis.init();\n\t}\n\n\t// ------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 初始化<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做加密（签名）或者解密（校验）\n\t *\n\t * @return this\n\t */\n\tpublic SM2 init() {\n\t\tif (null == this.privateKeyParams && null == this.publicKeyParams) {\n\t\t\tsuper.initKeys();\n\t\t\tthis.privateKeyParams = BCUtil.toParams(this.privateKey);\n\t\t\tthis.publicKeyParams = BCUtil.toParams(this.publicKey);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SM2 initKeys() {\n\t\t// 阻断父类中自动生成密钥对的操作，此操作由本类中进行。\n\t\t// 由于用户可能传入Params而非key，因此此时key必定为null，故此不再生成\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------------------------- Encrypt\n\n\t/**\n\t * 使用公钥加密，SM2非对称加密的结果由C1,C3,C2三部分组成，其中：\n\t *\n\t * <pre>\n\t * C1 生成随机数的计算出的椭圆曲线点\n\t * C3 SM3的摘要值\n\t * C2 密文数据\n\t * </pre>\n\t *\n\t * @param data    被加密的bytes\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t * @since 5.7.10\n\t */\n\tpublic byte[] encrypt(byte[] data) throws CryptoException {\n\t\treturn encrypt(data, KeyType.PublicKey);\n\t}\n\n\t/**\n\t * 加密，SM2非对称加密的结果由C1,C3,C2三部分组成，其中：\n\t *\n\t * <pre>\n\t * C1 生成随机数的计算出的椭圆曲线点\n\t * C3 SM3的摘要值\n\t * C2 密文数据\n\t * </pre>\n\t *\n\t * @param data    被加密的bytes\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t */\n\t@Override\n\tpublic byte[] encrypt(byte[] data, KeyType keyType) throws CryptoException {\n\t\tif (KeyType.PublicKey != keyType) {\n\t\t\tthrow new IllegalArgumentException(\"Encrypt is only support by public key\");\n\t\t}\n\t\treturn encrypt(data, new ParametersWithRandom(getCipherParameters(keyType)));\n\t}\n\n\t/**\n\t * 加密，SM2非对称加密的结果由C1,C2,C3三部分组成，其中：\n\t *\n\t * <pre>\n\t * C1 生成随机数的计算出的椭圆曲线点\n\t * C2 密文数据\n\t * C3 SM3的摘要值\n\t * </pre>\n\t *\n\t * @param data             被加密的bytes\n\t * @param pubKeyParameters 公钥参数\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t * @since 5.1.6\n\t */\n\tpublic byte[] encrypt(byte[] data, CipherParameters pubKeyParameters) throws CryptoException {\n\t\tlock.lock();\n\t\tfinal SM2Engine engine = getEngine();\n\t\ttry {\n\t\t\tengine.init(true, pubKeyParameters);\n\t\t\treturn engine.processBlock(data, 0, data.length);\n\t\t} catch (InvalidCipherTextException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------- Decrypt\n\n\t/**\n\t * 使用私钥解密\n\t *\n\t * @param data    SM2密文，实际包含三部分：ECC公钥、真正的密文、公钥和原文的SM3-HASH值\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t * @since 5.7.10\n\t */\n\tpublic byte[] decrypt(byte[] data) throws CryptoException {\n\t\treturn decrypt(data, KeyType.PrivateKey);\n\t}\n\n\t/**\n\t * 解密\n\t *\n\t * @param data    SM2密文，实际包含三部分：ECC公钥、真正的密文、公钥和原文的SM3-HASH值\n\t * @param keyType 私钥或公钥 {@link KeyType}\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t */\n\t@Override\n\tpublic byte[] decrypt(byte[] data, KeyType keyType) throws CryptoException {\n\t\tif (KeyType.PrivateKey != keyType) {\n\t\t\tthrow new IllegalArgumentException(\"Decrypt is only support by private key\");\n\t\t}\n\t\treturn decrypt(data, getCipherParameters(keyType));\n\t}\n\n\t/**\n\t * 解密\n\t *\n\t * @param data                 SM2密文，实际包含三部分：ECC公钥、真正的密文、公钥和原文的SM3-HASH值\n\t * @param privateKeyParameters 私钥参数\n\t * @return 加密后的bytes\n\t * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常\n\t * @since 5.1.6\n\t */\n\tpublic byte[] decrypt(byte[] data, CipherParameters privateKeyParameters) throws CryptoException {\n\t\tAssert.isTrue(data.length > 1, \"Invalid SM2 cipher text, must be at least 1 byte long\");\n\t\t// 检查数据，gmssl等库生成的密文不包含04前缀（非压缩数据标识），此处检查并补充\n\t\t// 参考：https://blog.csdn.net/softt/article/details/139978608\n\t\t// 根据公钥压缩形态不同，密文分为两种压缩形式：\n\t\t// C1( 03 + X ) + C3（32个字节）+ C2\n\t\t// C1( 02 + X ) + C3（32个字节）+ C2\n\t\t// 非压缩公钥正常形态为04 + X  + Y，由于各个算法库差异，04有时候会省略\n\t\t// 非压缩密文正常形态为04 + C1 + C3 + C2\n\t\tif (data[0] != 0x04 && data[0] != 0x02 && data[0] != 0x03) {\n\t\t\tdata = (byte[]) ArrayUtil.insert(data, 0, 0x04);\n\t\t}\n\n\t\tlock.lock();\n\t\tfinal SM2Engine engine = getEngine();\n\t\ttry {\n\t\t\tengine.init(false, privateKeyParameters);\n\t\t\treturn engine.processBlock(data, 0, data.length);\n\t\t} catch (InvalidCipherTextException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\t// --------------------------------------------------------------------------------- Sign and Verify\n\n\t/**\n\t * 用私钥对信息生成数字签名\n\t *\n\t * @param dataHex 被签名的数据\n\t * @return 签名\n\t */\n\tpublic String signHexFromHex(String dataHex) {\n\t\treturn signHex(dataHex, null);\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名\n\t *\n\t * @param dataHex 被签名的数据\n\t * @return 签名\n\t * @deprecated 歧义，使用{@link #signHexFromHex(String)}\n\t */\n\t@Deprecated\n\tpublic String signHex(String dataHex) {\n\t\treturn signHex(dataHex, null);\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名\n\t *\n\t * @param dataHex 被签名的数据\n\t * @param idHex   可以为null，若为null，则默认withId为字节数组:\"1234567812345678\".getBytes()\n\t * @return 签名\n\t */\n\tpublic String signHexFromHex(String dataHex, String idHex) {\n\t\treturn HexUtil.encodeHexStr(sign(HexUtil.decodeHex(dataHex), HexUtil.decodeHex(idHex)));\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名\n\t *\n\t * @param dataHex 被签名的数据\n\t * @param idHex   可以为null，若为null，则默认withId为字节数组:\"1234567812345678\".getBytes()\n\t * @return 签名\n\t * @deprecated 歧义，使用{@link #signHexFromHex(String, String)}\n\t */\n\t@Deprecated\n\tpublic String signHex(String dataHex, String idHex) {\n\t\treturn HexUtil.encodeHexStr(sign(HexUtil.decodeHex(dataHex), HexUtil.decodeHex(idHex)));\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名，签名格式为ASN1<br>\n\t * * 在硬件签名中，返回结果为R+S，可以通过调用{@link cn.hutool.crypto.SmUtil#rsAsn1ToPlain(byte[])}方法转换之。\n\t *\n\t * @param data 被签名的数据\n\t * @return 签名\n\t */\n\tpublic byte[] sign(byte[] data) {\n\t\treturn sign(data, null);\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名，签名格式为ASN1<br>\n\t * 在硬件签名中，返回结果为R+S，可以通过调用{@link cn.hutool.crypto.SmUtil#rsAsn1ToPlain(byte[])}方法转换之。\n\t *\n\t * @param data 被签名的数据\n\t * @param id   可以为null，若为null，则默认withId为字节数组:\"1234567812345678\".getBytes()\n\t * @return 签名\n\t */\n\tpublic byte[] sign(byte[] data, byte[] id) {\n\t\tlock.lock();\n\t\tfinal SM2Signer signer = getSigner();\n\t\ttry {\n\t\t\tCipherParameters param = new ParametersWithRandom(getCipherParameters(KeyType.PrivateKey));\n\t\t\tif (id != null) {\n\t\t\t\tparam = new ParametersWithID(param, id);\n\t\t\t}\n\t\t\tsigner.init(true, param);\n\t\t\tsigner.update(data, 0, data.length);\n\t\t\treturn signer.generateSignature();\n\t\t} catch (org.bouncycastle.crypto.CryptoException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 用公钥检验数字签名的合法性\n\t *\n\t * @param dataHex 数据\n\t * @param signHex 签名\n\t * @return 是否验证通过\n\t * @since 5.2.0\n\t */\n\tpublic boolean verifyHex(String dataHex, String signHex) {\n\t\treturn verifyHex(dataHex, signHex, null);\n\t}\n\n\t/**\n\t * 用公钥检验数字签名的合法性\n\t *\n\t * @param data 数据\n\t * @param sign 签名\n\t * @return 是否验证通过\n\t */\n\tpublic boolean verify(byte[] data, byte[] sign) {\n\t\treturn verify(data, sign, null);\n\t}\n\n\t/**\n\t * 用公钥检验数字签名的合法性\n\t *\n\t * @param dataHex 数据的Hex值\n\t * @param signHex 签名的Hex值\n\t * @param idHex   ID的Hex值\n\t * @return 是否验证通过\n\t * @since 5.2.0\n\t */\n\tpublic boolean verifyHex(String dataHex, String signHex, String idHex) {\n\t\treturn verify(HexUtil.decodeHex(dataHex), HexUtil.decodeHex(signHex), HexUtil.decodeHex(idHex));\n\t}\n\n\t/**\n\t * 用公钥检验数字签名的合法性\n\t *\n\t * @param data 数据\n\t * @param sign 签名\n\t * @param id   可以为null，若为null，则默认withId为字节数组:\"1234567812345678\".getBytes()\n\t * @return 是否验证通过\n\t */\n\tpublic boolean verify(byte[] data, byte[] sign, byte[] id) {\n\t\tlock.lock();\n\t\tfinal SM2Signer signer = getSigner();\n\t\ttry {\n\t\t\tCipherParameters param = getCipherParameters(KeyType.PublicKey);\n\t\t\tif (id != null) {\n\t\t\t\tparam = new ParametersWithID(param, id);\n\t\t\t}\n\t\t\tsigner.init(false, param);\n\t\t\tsigner.update(data, 0, data.length);\n\t\t\treturn signer.verifySignature(sign);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic SM2 setPrivateKey(PrivateKey privateKey) {\n\t\tsuper.setPrivateKey(privateKey);\n\n\t\t// 重新初始化密钥参数，防止重新设置密钥时导致密钥无法更新\n\t\tthis.privateKeyParams = BCUtil.toParams(privateKey);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置私钥参数\n\t *\n\t * @param privateKeyParams 私钥参数\n\t * @return this\n\t * @since 5.2.0\n\t */\n\tpublic SM2 setPrivateKeyParams(ECPrivateKeyParameters privateKeyParams) {\n\t\tthis.privateKeyParams = privateKeyParams;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SM2 setPublicKey(PublicKey publicKey) {\n\t\tsuper.setPublicKey(publicKey);\n\n\t\t// 重新初始化密钥参数，防止重新设置密钥时导致密钥无法更新\n\t\tthis.publicKeyParams = BCUtil.toParams(publicKey);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置公钥参数\n\t *\n\t * @param publicKeyParams 公钥参数\n\t * @return this\n\t */\n\tpublic SM2 setPublicKeyParams(ECPublicKeyParameters publicKeyParams) {\n\t\tthis.publicKeyParams = publicKeyParams;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置DSA signatures的编码为PlainDSAEncoding\n\t *\n\t * @return this\n\t * @since 5.3.1\n\t */\n\tpublic SM2 usePlainEncoding() {\n\t\treturn setEncoding(PlainDSAEncoding.INSTANCE);\n\t}\n\n\t/**\n\t * 设置DSA signatures的编码\n\t *\n\t * @param encoding {@link DSAEncoding}实现\n\t * @return this\n\t * @since 5.3.1\n\t */\n\tpublic SM2 setEncoding(DSAEncoding encoding) {\n\t\tthis.encoding = encoding;\n\t\tthis.signer = null;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Hash算法\n\t *\n\t * @param digest {@link Digest}实现\n\t * @return this\n\t * @since 5.3.1\n\t */\n\tpublic SM2 setDigest(Digest digest) {\n\t\tthis.digest = digest;\n\t\tthis.engine = null;\n\t\tthis.signer = null;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置SM2模式，旧版是C1C2C3，新版本是C1C3C2\n\t *\n\t * @param mode {@link SM2Engine.Mode}\n\t * @return this\n\t */\n\tpublic SM2 setMode(SM2Engine.Mode mode) {\n\t\tthis.mode = mode;\n\t\tthis.engine = null;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得私钥D值（编码后的私钥）\n\t *\n\t * @return D值\n\t * @since 5.5.9\n\t */\n\tpublic byte[] getD() {\n\t\treturn BigIntegers.asUnsignedByteArray(32,getDBigInteger());\n\t}\n\n\t/**\n\t * 获得私钥D值（编码后的私钥）\n\t *\n\t * @return D值\n\t * @since 5.7.17\n\t */\n\tpublic String getDHex() {\n\t\treturn new String(Hex.encode(getD()));\n\t}\n\n\t/**\n\t * 获得私钥D值\n\t *\n\t * @return D值\n\t * @since 5.7.17\n\t */\n\tpublic BigInteger getDBigInteger() {\n\t\treturn this.privateKeyParams.getD();\n\t}\n\n\t/**\n\t * 获得公钥Q值（编码后的公钥）\n\t *\n\t * @param isCompressed 是否压缩\n\t * @return Q值\n\t * @since 5.5.9\n\t */\n\tpublic byte[] getQ(boolean isCompressed) {\n\t\treturn this.publicKeyParams.getQ().getEncoded(isCompressed);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 获取密钥类型对应的加密参数对象{@link CipherParameters}\n\t *\n\t * @param keyType Key类型枚举，包括私钥或公钥\n\t * @return {@link CipherParameters}\n\t */\n\tprivate CipherParameters getCipherParameters(KeyType keyType) {\n\t\tswitch (keyType) {\n\t\t\tcase PublicKey:\n\t\t\t\tAssert.notNull(this.publicKeyParams, \"PublicKey must be not null !\");\n\t\t\t\treturn this.publicKeyParams;\n\t\t\tcase PrivateKey:\n\t\t\t\tAssert.notNull(this.privateKeyParams, \"PrivateKey must be not null !\");\n\t\t\t\treturn this.privateKeyParams;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取{@link SM2Engine}，此对象为懒加载模式\n\t *\n\t * @return {@link SM2Engine}\n\t */\n\tprivate SM2Engine getEngine() {\n\t\tif (null == this.engine) {\n\t\t\tAssert.notNull(this.digest, \"digest must be not null !\");\n\t\t\tthis.engine = new SM2Engine(this.digest, this.mode);\n\t\t}\n\t\tthis.digest.reset();\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 获取{@link SM2Signer}，此对象为懒加载模式\n\t *\n\t * @return {@link SM2Signer}\n\t */\n\tprivate SM2Signer getSigner() {\n\t\tif (null == this.signer) {\n\t\t\tAssert.notNull(this.digest, \"digest must be not null !\");\n\t\t\tthis.signer = new SM2Signer(this.encoding, this.digest);\n\t\t}\n\t\tthis.digest.reset();\n\t\treturn this.signer;\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/Sign.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.security.Signature;\nimport java.security.cert.Certificate;\nimport java.security.cert.X509Certificate;\nimport java.security.spec.AlgorithmParameterSpec;\nimport java.util.Set;\n\n/**\n * 签名包装，{@link Signature} 包装类\n *\n * @author looly\n * @since 3.3.0\n */\npublic class Sign extends BaseAsymmetric<Sign> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 签名，用于签名和验证\n\t */\n\tprotected Signature signature;\n\n\t// ------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造，创建新的私钥公钥对\n\t *\n\t * @param algorithm {@link SignAlgorithm}\n\t */\n\tpublic Sign(SignAlgorithm algorithm) {\n\t\tthis(algorithm, null, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造，创建新的私钥公钥对\n\t *\n\t * @param algorithm 算法\n\t */\n\tpublic Sign(String algorithm) {\n\t\tthis(algorithm, null, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm     {@link SignAlgorithm}\n\t * @param privateKeyStr 私钥Hex或Base64表示\n\t * @param publicKeyStr  公钥Hex或Base64表示\n\t */\n\tpublic Sign(SignAlgorithm algorithm, String privateKeyStr, String publicKeyStr) {\n\t\tthis(algorithm.getValue(), SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr));\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  {@link SignAlgorithm}\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic Sign(SignAlgorithm algorithm, byte[] privateKey, byte[] publicKey) {\n\t\tthis(algorithm.getValue(), privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm {@link SignAlgorithm}\n\t * @param keyPair   密钥对（包括公钥和私钥）\n\t */\n\tpublic Sign(SignAlgorithm algorithm, KeyPair keyPair) {\n\t\tthis(algorithm.getValue(), keyPair);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  {@link SignAlgorithm}\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic Sign(SignAlgorithm algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tthis(algorithm.getValue(), privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm        非对称加密算法\n\t * @param privateKeyBase64 私钥Base64\n\t * @param publicKeyBase64  公钥Base64\n\t */\n\tpublic Sign(String algorithm, String privateKeyBase64, String publicKeyBase64) {\n\t\tthis(algorithm, Base64.decode(privateKeyBase64), Base64.decode(publicKeyBase64));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic Sign(String algorithm, byte[] privateKey, byte[] publicKey) {\n\t\tthis(algorithm, //\n\t\t\tSecureUtil.generatePrivateKey(algorithm, privateKey), //\n\t\t\tSecureUtil.generatePublicKey(algorithm, publicKey)//\n\t\t);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm 算法，见{@link SignAlgorithm}\n\t * @param keyPair   密钥对（包括公钥和私钥）\n\t */\n\tpublic Sign(String algorithm, KeyPair keyPair) {\n\t\tthis(algorithm, keyPair.getPrivate(), keyPair.getPublic());\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>\n\t * 私钥和公钥可以单独传入一个，如此则只能使用此钥匙来做签名或验证\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t */\n\tpublic Sign(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tsuper(algorithm, privateKey, publicKey);\n\t}\n\t// ------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm  算法\n\t * @param privateKey 私钥\n\t * @param publicKey  公钥\n\t * @return this\n\t */\n\t@Override\n\tpublic Sign init(String algorithm, PrivateKey privateKey, PublicKey publicKey) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tsignature = SecureUtil.createSignature(algorithm);\n\t\t\tsuper.init(algorithm, privateKey, publicKey);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置签名的参数\n\t *\n\t * @param params {@link AlgorithmParameterSpec}\n\t * @return this\n\t * @since 4.6.5\n\t */\n\tpublic Sign setParameter(AlgorithmParameterSpec params) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tthis.signature.setParameter(params);\n\t\t} catch (InvalidAlgorithmParameterException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------------------------- Sign and Verify\n\n\t/**\n\t * 生成文件签名\n\t *\n\t * @param data    被签名数据\n\t * @param charset 编码\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic byte[] sign(String data, Charset charset) {\n\t\treturn sign(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 生成文件签名\n\t *\n\t * @param data 被签名数据\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic byte[] sign(String data) {\n\t\treturn sign(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 生成文件签名，并转为16进制字符串\n\t *\n\t * @param data    被签名数据\n\t * @param charset 编码\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic String signHex(String data, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(sign(data, charset));\n\t}\n\n\t/**\n\t * 生成文件签名\n\t *\n\t * @param data 被签名数据\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic String signHex(String data) {\n\t\treturn signHex(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 用私钥对信息生成数字签名\n\t *\n\t * @param data 加密数据\n\t * @return 签名\n\t */\n\tpublic byte[] sign(byte[] data) {\n\t\treturn sign(new ByteArrayInputStream(data), -1);\n\t}\n\n\t/**\n\t * 生成签名，并转为16进制字符串<br>\n\t *\n\t * @param data 被签名数据\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic String signHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(sign(data));\n\t}\n\n\t/**\n\t * 生成签名，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data 被签名数据\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic String signHex(InputStream data) {\n\t\treturn HexUtil.encodeHexStr(sign(data));\n\t}\n\n\t/**\n\t * 生成签名，使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @return 签名bytes\n\t * @since 5.7.0\n\t */\n\tpublic byte[] sign(InputStream data) {\n\t\treturn sign(data, IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 生成签名，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data         被签名数据\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 签名\n\t * @since 5.7.0\n\t */\n\tpublic String digestHex(InputStream data, int bufferLength) {\n\t\treturn HexUtil.encodeHexStr(sign(data, bufferLength));\n\t}\n\n\t/**\n\t * 生成签名\n\t *\n\t * @param data         {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 签名bytes\n\t * @since 5.7.0\n\t */\n\tpublic byte[] sign(InputStream data, int bufferLength) {\n\t\tif (bufferLength < 1) {\n\t\t\tbufferLength = IoUtil.DEFAULT_BUFFER_SIZE;\n\t\t}\n\n\t\tfinal byte[] buffer = new byte[bufferLength];\n\t\tlock.lock();\n\t\ttry {\n\t\t\tsignature.initSign(this.privateKey);\n\t\t\tbyte[] result;\n\t\t\ttry {\n\t\t\t\tint read = data.read(buffer, 0, bufferLength);\n\t\t\t\twhile (read > -1) {\n\t\t\t\t\tsignature.update(buffer, 0, read);\n\t\t\t\t\tread = data.read(buffer, 0, bufferLength);\n\t\t\t\t}\n\t\t\t\tresult = signature.sign();\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new CryptoException(e);\n\t\t\t}\n\t\t\treturn result;\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 用公钥检验数字签名的合法性\n\t *\n\t * @param data 数据\n\t * @param sign 签名\n\t * @return 是否验证通过\n\t */\n\tpublic boolean verify(byte[] data, byte[] sign) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tsignature.initVerify(this.publicKey);\n\t\t\tsignature.update(data);\n\t\t\treturn signature.verify(sign);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 获得签名对象\n\t *\n\t * @return {@link Signature}\n\t */\n\tpublic Signature getSignature() {\n\t\treturn signature;\n\t}\n\n\t/**\n\t * 设置签名\n\t *\n\t * @param signature 签名对象 {@link Signature}\n\t * @return 自身 {@link AsymmetricCrypto}\n\t */\n\tpublic Sign setSignature(Signature signature) {\n\t\tthis.signature = signature;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置{@link Certificate} 为PublicKey<br>\n\t * 如果Certificate是X509Certificate，我们需要检查是否有密钥扩展\n\t *\n\t * @param certificate {@link Certificate}\n\t * @return this\n\t */\n\tpublic Sign setCertificate(Certificate certificate) {\n\t\t// If the certificate is of type X509Certificate,\n\t\t// we should check whether it has a Key Usage\n\t\t// extension marked as critical.\n\t\tif (certificate instanceof X509Certificate) {\n\t\t\t// Check whether the cert has a key usage extension\n\t\t\t// marked as a critical extension.\n\t\t\t// The OID for KeyUsage extension is 2.5.29.15.\n\t\t\tfinal X509Certificate cert = (X509Certificate) certificate;\n\t\t\tfinal Set<String> critSet = cert.getCriticalExtensionOIDs();\n\n\t\t\tif (CollUtil.isNotEmpty(critSet) && critSet.contains(\"2.5.29.15\")) {\n\t\t\t\tfinal boolean[] keyUsageInfo = cert.getKeyUsage();\n\t\t\t\t// keyUsageInfo[0] is for digitalSignature.\n\t\t\t\tif ((keyUsageInfo != null) && (keyUsageInfo[0] == false)) {\n\t\t\t\t\tthrow new CryptoException(\"Wrong key usage\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.publicKey = certificate.getPublicKey();\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SignAlgorithm.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\n/**\n * 签名算法类型<br>\n * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Signature\n *\n * @author Looly\n */\npublic enum SignAlgorithm {\n\t// The RSA signature algorithm\n\tNONEwithRSA(\"NONEwithRSA\"),\n\n\t// The MD2/MD5 with RSA Encryption signature algorithm\n\tMD2withRSA(\"MD2withRSA\"),\n\tMD5withRSA(\"MD5withRSA\"),\n\n\t// The signature algorithm with SHA-* and the RSA\n\tSHA1withRSA(\"SHA1withRSA\"),\n\tSHA256withRSA(\"SHA256withRSA\"),\n\tSHA384withRSA(\"SHA384withRSA\"),\n\tSHA512withRSA(\"SHA512withRSA\"),\n\n\t// The Digital Signature Algorithm\n\tNONEwithDSA(\"NONEwithDSA\"),\n\t// The DSA with SHA-1 signature algorithm\n\tSHA1withDSA(\"SHA1withDSA\"),\n\n\t// The ECDSA signature algorithms\n\tNONEwithECDSA(\"NONEwithECDSA\"),\n\tSHA1withECDSA(\"SHA1withECDSA\"),\n\tSHA256withECDSA(\"SHA256withECDSA\"),\n\tSHA384withECDSA(\"SHA384withECDSA\"),\n\tSHA512withECDSA(\"SHA512withECDSA\"),\n\n\t// 需要BC库加入支持\n\tSHA256withRSA_PSS(\"SHA256WithRSA/PSS\"),\n\tSHA384withRSA_PSS(\"SHA384WithRSA/PSS\"),\n\tSHA512withRSA_PSS(\"SHA512WithRSA/PSS\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 算法字符表示，区分大小写\n\t */\n\tSignAlgorithm(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取算法字符串表示，区分大小写\n\t *\n\t * @return 算法字符串表示\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/package-info.java",
    "content": "/**\n * 非对称加密的实现，包括RSA等\n * \n * @author looly\n *\n */\npackage cn.hutool.crypto.asymmetric;"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/Argon2.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport org.bouncycastle.crypto.generators.Argon2BytesGenerator;\nimport org.bouncycastle.crypto.params.Argon2Parameters;\n\n/**\n * Argon2加密实现\n *\n * @author changhr2013\n * @author Looly\n * @since 5.8.38\n */\npublic class Argon2 {\n\n\t/**\n\t * 默认hash长度\n\t */\n\tpublic static final int DEFAULT_HASH_LENGTH = 32;\n\n\tprivate int hashLength = DEFAULT_HASH_LENGTH;\n\tprivate final Argon2Parameters.Builder paramsBuilder;\n\n\t/**\n\t * 构造，默认使用{@link Argon2Parameters#ARGON2_id}类型\n\t */\n\tpublic Argon2(){\n\t\tthis(Argon2Parameters.ARGON2_id);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param type {@link Argon2Parameters#ARGON2_d}、{@link Argon2Parameters#ARGON2_i}、{@link Argon2Parameters#ARGON2_id}\n\t */\n\tpublic Argon2(int type){\n\t\tthis(new Argon2Parameters.Builder(type));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param paramsBuilder 参数构造器\n\t */\n\tpublic Argon2(Argon2Parameters.Builder paramsBuilder){\n\t\tthis.paramsBuilder = paramsBuilder;\n\t}\n\n\t/**\n\t * 设置hash长度\n\t *\n\t * @param hashLength hash长度\n\t * @return this\n\t */\n\tpublic Argon2 setHashLength(int hashLength){\n\t\tthis.hashLength = hashLength;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置版本\n\t *\n\t * @param version 版本\n\t * @return this\n\t * @see Argon2Parameters#ARGON2_VERSION_10\n\t * @see Argon2Parameters#ARGON2_VERSION_13\n\t */\n\tpublic Argon2 setVersion(int version){\n\t\tthis.paramsBuilder.withVersion(version);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置盐\n\t *\n\t * @param salt 盐\n\t * @return this\n\t */\n\tpublic Argon2 setSalt(byte[] salt){\n\t\tthis.paramsBuilder.withSalt(salt);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置可选的密钥数据，用于增加哈希的复杂性\n\t *\n\t * @param secret 密钥\n\t * @return this\n\t */\n\tpublic Argon2 setSecret(byte[] secret){\n\t\tthis.paramsBuilder.withSecret(secret);\n\t\treturn this;\n\t}\n\n\t/**\n\t * @param additional 附加数据\n\t * @return this\n\t */\n\tpublic Argon2 setAdditional(byte[] additional){\n\t\tthis.paramsBuilder.withAdditional(additional);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置迭代次数<br>\n\t * 迭代次数越多，生成哈希的时间就越长，破解哈希就越困难\n\t *\n\t * @param iterations 迭代次数\n\t * @return this\n\t */\n\tpublic Argon2 setIterations(int iterations){\n\t\tthis.paramsBuilder.withIterations(iterations);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置内存，单位KB<br>\n\t * 内存越大，生成哈希的时间就越长，破解哈希就越困难\n\t *\n\t * @param memoryAsKB 内存，单位KB\n\t * @return this\n\t */\n\tpublic Argon2 setMemoryAsKB(int memoryAsKB){\n\t\tthis.paramsBuilder.withMemoryAsKB(memoryAsKB);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置并行度，即同时使用的核心数<br>\n\t * 值越高，生成哈希的时间就越长，破解哈希就越困难\n\t *\n\t * @param parallelism 并行度\n\t * @return this\n\t */\n\tpublic Argon2 setParallelism(int parallelism){\n\t\tthis.paramsBuilder.withParallelism(parallelism);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 生成hash值\n\t *\n\t * @param password 密码\n\t * @return hash值\n\t */\n\tpublic byte[] digest(char[] password){\n\t\tfinal Argon2BytesGenerator generator = new Argon2BytesGenerator();\n\t\tgenerator.init(paramsBuilder.build());\n\t\tbyte[] result = new byte[hashLength];\n\t\tgenerator.generateBytes(password, result);\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/BCrypt.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.security.SecureRandom;\n\n/**\n * BCrypt加密算法实现。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符，并将在内部被转化为448位的密钥。\n * <p>\n * 此类来自于https://github.com/jeremyh/jBCrypt/\n * <p>\n * 使用方法如下：\n * <p>\n * {@code\n * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());\n * }\n * <p>\n * 使用checkpw方法检查被加密的字符串是否与原始字符串匹配：\n * <p>\n * {@code\n * BCrypt.checkpw(candidate_password, stored_hash);\n * }\n * <p>\n * gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少，也决定了加密的复杂度:\n * <p>\n * {@code\n * String strong_salt = BCrypt.gensalt(10);\n * String stronger_salt = BCrypt.gensalt(12);\n * }\n *\n * @author Damien Miller\n * @since 4.1.1\n */\npublic class BCrypt {\n\t// BCrypt parameters\n\tprivate static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;\n\tprivate static final int BCRYPT_SALT_LEN = 16;\n\n\t// Blowfish parameters\n\tprivate static final int BLOWFISH_NUM_ROUNDS = 16;\n\n\t// Initial contents of key schedule\n\tprivate static final int[] P_orig = {0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7,\n\t\t\t0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b};\n\tprivate static final int[] S_orig = {0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8,\n\t\t\t0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,\n\t\t\t0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34,\n\t\t\t0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af,\n\t\t\t0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482,\n\t\t\t0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,\n\t\t\t0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72,\n\t\t\t0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4,\n\t\t\t0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857,\n\t\t\t0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,\n\t\t\t0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d,\n\t\t\t0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01,\n\t\t\t0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0,\n\t\t\t0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,\n\t\t\t0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb,\n\t\t\t0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a,\n\t\t\t0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d,\n\t\t\t0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,\n\t\t\t0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6,\n\t\t\t0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d,\n\t\t\t0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146,\n\t\t\t0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,\n\t\t\t0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366,\n\t\t\t0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b,\n\t\t\t0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6,\n\t\t\t0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,\n\t\t\t0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775,\n\t\t\t0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca,\n\t\t\t0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829,\n\t\t\t0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,\n\t\t\t0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a,\n\t\t\t0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea,\n\t\t\t0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa,\n\t\t\t0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,\n\t\t\t0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7,\n\t\t\t0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941,\n\t\t\t0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c,\n\t\t\t0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,\n\t\t\t0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c,\n\t\t\t0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1,\n\t\t\t0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e,\n\t\t\t0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,\n\t\t\t0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b,\n\t\t\t0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7,\n\t\t\t0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057,\n\t\t\t0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,\n\t\t\t0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591,\n\t\t\t0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df,\n\t\t\t0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28,\n\t\t\t0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,\n\t\t\t0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce,\n\t\t\t0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315,\n\t\t\t0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb,\n\t\t\t0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,\n\t\t\t0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9,\n\t\t\t0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc,\n\t\t\t0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b,\n\t\t\t0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,\n\t\t\t0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf,\n\t\t\t0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e,\n\t\t\t0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82,\n\t\t\t0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,\n\t\t\t0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71,\n\t\t\t0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df,\n\t\t\t0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166,\n\t\t\t0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,\n\t\t\t0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60,\n\t\t\t0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0,\n\t\t\t0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6};\n\n\t// bcrypt IV: \"OrpheanBeholderScryDoubt\". The C implementation calls\n\t// this \"ciphertext\", but it is really plaintext or an IV. We keep\n\t// the name to make code comparison easier.\n\tstatic private final int[] bf_crypt_ciphertext = {0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274};\n\n\t// Table for Base64 encoding\n\tstatic private final char[] base64_code = {'.', '/', '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',\n\t\t\t'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};\n\n\t// Table for Base64 decoding\n\tstatic private final byte[] index_64 = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n\t\t\t-1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,\n\t\t\t25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1};\n\n\t// Expanded Blowfish key\n\tprivate int[] P;\n\tprivate int[] S;\n\n\t/**\n\t * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this is *not* compatible with the standard MIME-base64 encoding.\n\t *\n\t * @param d   the byte array to encode\n\t * @param len the number of bytes to encode\n\t * @return base64-encoded string\n\t * @throws IllegalArgumentException if the length is invalid\n\t */\n\tprivate static String encode_base64(byte[] d, int len) throws IllegalArgumentException {\n\t\tint off = 0;\n\t\tStringBuilder rs = new StringBuilder();\n\t\tint c1, c2;\n\n\t\tif (len <= 0 || len > d.length)\n\t\t\tthrow new IllegalArgumentException(\"Invalid len\");\n\n\t\twhile (off < len) {\n\t\t\tc1 = d[off++] & 0xff;\n\t\t\trs.append(base64_code[(c1 >> 2) & 0x3f]);\n\t\t\tc1 = (c1 & 0x03) << 4;\n\t\t\tif (off >= len) {\n\t\t\t\trs.append(base64_code[c1 & 0x3f]);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tc2 = d[off++] & 0xff;\n\t\t\tc1 |= (c2 >> 4) & 0x0f;\n\t\t\trs.append(base64_code[c1 & 0x3f]);\n\t\t\tc1 = (c2 & 0x0f) << 2;\n\t\t\tif (off >= len) {\n\t\t\t\trs.append(base64_code[c1 & 0x3f]);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tc2 = d[off++] & 0xff;\n\t\t\tc1 |= (c2 >> 6) & 0x03;\n\t\t\trs.append(base64_code[c1 & 0x3f]);\n\t\t\trs.append(base64_code[c2 & 0x3f]);\n\t\t}\n\t\treturn rs.toString();\n\t}\n\n\t/**\n\t * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table\n\t *\n\t * @param x the base64-encoded value\n\t * @return the decoded value of x\n\t */\n\tprivate static byte char64(char x) {\n\t\tif ((int) x > index_64.length)\n\t\t\treturn -1;\n\t\treturn index_64[x];\n\t}\n\n\t/**\n\t * Decode a string encoded using bcrypt's base64 scheme to a byte array.<br>\n\t * Note that this is *not* compatible with the standard MIME-base64 encoding.\n\t *\n\t * @param s       the string to decode\n\t * @param maxolen the maximum number of bytes to decode\n\t * @return an array containing the decoded bytes\n\t * @throws IllegalArgumentException if maxolen is invalid\n\t */\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate static byte[] decodeBase64(String s, int maxolen) throws IllegalArgumentException {\n\t\tfinal StringBuilder rs = new StringBuilder();\n\t\tint off = 0, slen = s.length(), olen = 0;\n\t\tbyte[] ret;\n\t\tbyte c1, c2, c3, c4, o;\n\n\t\tif (maxolen <= 0)\n\t\t\tthrow new IllegalArgumentException(\"Invalid maxolen\");\n\n\t\twhile (off < slen - 1 && olen < maxolen) {\n\t\t\tc1 = char64(s.charAt(off++));\n\t\t\tc2 = char64(s.charAt(off++));\n\t\t\tif (c1 == -1 || c2 == -1)\n\t\t\t\tbreak;\n\t\t\to = (byte) (c1 << 2);\n\t\t\to |= (c2 & 0x30) >> 4;\n\t\t\trs.append((char) o);\n\t\t\tif (++olen >= maxolen || off >= slen)\n\t\t\t\tbreak;\n\t\t\tc3 = char64(s.charAt(off++));\n\t\t\tif (c3 == -1)\n\t\t\t\tbreak;\n\t\t\to = (byte) ((c2 & 0x0f) << 4);\n\t\t\to |= (c3 & 0x3c) >> 2;\n\t\t\trs.append((char) o);\n\t\t\tif (++olen >= maxolen || off >= slen)\n\t\t\t\tbreak;\n\t\t\tc4 = char64(s.charAt(off++));\n\t\t\to = (byte) ((c3 & 0x03) << 6);\n\t\t\to |= c4;\n\t\t\trs.append((char) o);\n\t\t\t++olen;\n\t\t}\n\n\t\tret = new byte[olen];\n\t\tfor (off = 0; off < olen; off++)\n\t\t\tret[off] = (byte) rs.charAt(off);\n\t\treturn ret;\n\t}\n\n\t/**\n\t * Blowfish encipher a single 64-bit block encoded as two 32-bit halves\n\t *\n\t * @param lr  an array containing the two 32-bit half blocks\n\t * @param off the position in the array of the blocks\n\t */\n\tprivate void encipher(int[] lr, int off) {\n\t\tint i, n, l = lr[off], r = lr[off + 1];\n\n\t\tl ^= P[0];\n\t\tfor (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) {\n\t\t\t// Feistel substitution on left word\n\t\t\tn = S[(l >> 24) & 0xff];\n\t\t\tn += S[0x100 | ((l >> 16) & 0xff)];\n\t\t\tn ^= S[0x200 | ((l >> 8) & 0xff)];\n\t\t\tn += S[0x300 | (l & 0xff)];\n\t\t\tr ^= n ^ P[++i];\n\n\t\t\t// Feistel substitution on right word\n\t\t\tn = S[(r >> 24) & 0xff];\n\t\t\tn += S[0x100 | ((r >> 16) & 0xff)];\n\t\t\tn ^= S[0x200 | ((r >> 8) & 0xff)];\n\t\t\tn += S[0x300 | (r & 0xff)];\n\t\t\tl ^= n ^ P[++i];\n\t\t}\n\t\tlr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1];\n\t\tlr[off + 1] = l;\n\t}\n\n\t/**\n\t * Cycically extract a word of key material\n\t *\n\t * @param data the string to extract the data from\n\t * @param offp a \"pointer\" (as a one-entry array) to the current offset into data\n\t * @return the next word of material from data\n\t */\n\tprivate static int streamToWord(byte[] data, int[] offp) {\n\t\tint i;\n\t\tint word = 0;\n\t\tint off = offp[0];\n\n\t\tfor (i = 0; i < 4; i++) {\n\t\t\tword = (word << 8) | (data[off] & 0xff);\n\t\t\toff = (off + 1) % data.length;\n\t\t}\n\n\t\toffp[0] = off;\n\t\treturn word;\n\t}\n\n\t/**\n\t * Initialise the Blowfish key schedule\n\t */\n\tprivate void init_key() {\n\t\tP = P_orig.clone();\n\t\tS = S_orig.clone();\n\t}\n\n\t/**\n\t * Key the Blowfish cipher\n\t *\n\t * @param key an array containing the key\n\t */\n\tprivate void key(byte[] key) {\n\t\tint i;\n\t\tint[] koffp = {0};\n\t\tint[] lr = {0, 0};\n\t\tint plen = P.length, slen = S.length;\n\n\t\tfor (i = 0; i < plen; i++)\n\t\t\tP[i] = P[i] ^ streamToWord(key, koffp);\n\n\t\tfor (i = 0; i < plen; i += 2) {\n\t\t\tencipher(lr, 0);\n\t\t\tP[i] = lr[0];\n\t\t\tP[i + 1] = lr[1];\n\t\t}\n\n\t\tfor (i = 0; i < slen; i += 2) {\n\t\t\tencipher(lr, 0);\n\t\t\tS[i] = lr[0];\n\t\t\tS[i + 1] = lr[1];\n\t\t}\n\t}\n\n\t/**\n\t * Perform the \"enhanced key schedule\" step described by Provos and Mazieres in \"A Future-Adaptable Password Scheme\" http://www.openbsd.org/papers/bcrypt-paper.ps\n\t *\n\t * @param data salt information\n\t * @param key  password information\n\t */\n\tprivate void ekskey(byte[] data, byte[] key) {\n\t\tint i;\n\t\tint[] koffp = {0};\n\t\tint[] doffp = {0};\n\t\tint[] lr = {0, 0};\n\t\tint plen = P.length, slen = S.length;\n\n\t\tfor (i = 0; i < plen; i++)\n\t\t\tP[i] = P[i] ^ streamToWord(key, koffp);\n\n\t\tfor (i = 0; i < plen; i += 2) {\n\t\t\tlr[0] ^= streamToWord(data, doffp);\n\t\t\tlr[1] ^= streamToWord(data, doffp);\n\t\t\tencipher(lr, 0);\n\t\t\tP[i] = lr[0];\n\t\t\tP[i + 1] = lr[1];\n\t\t}\n\n\t\tfor (i = 0; i < slen; i += 2) {\n\t\t\tlr[0] ^= streamToWord(data, doffp);\n\t\t\tlr[1] ^= streamToWord(data, doffp);\n\t\t\tencipher(lr, 0);\n\t\t\tS[i] = lr[0];\n\t\t\tS[i + 1] = lr[1];\n\t\t}\n\t}\n\n\t/**\n\t * 加密密文\n\t *\n\t * @param password   明文密码\n\t * @param salt       加盐\n\t * @param log_rounds hash中叠加的对数\n\t * @param cdata      加密数据\n\t * @return 加密后的密文\n\t */\n\tpublic byte[] crypt(byte[] password, byte[] salt, int log_rounds, int[] cdata) {\n\t\tint rounds, i, j;\n\t\tint clen = cdata.length;\n\t\tbyte[] ret;\n\n\t\tif (log_rounds < 4 || log_rounds > 30)\n\t\t\tthrow new IllegalArgumentException(\"Bad number of rounds\");\n\t\trounds = 1 << log_rounds;\n\t\tif (salt.length != BCRYPT_SALT_LEN)\n\t\t\tthrow new IllegalArgumentException(\"Bad salt length\");\n\n\t\tinit_key();\n\t\tekskey(salt, password);\n\t\tfor (i = 0; i != rounds; i++) {\n\t\t\tkey(password);\n\t\t\tkey(salt);\n\t\t}\n\n\t\tfor (i = 0; i < 64; i++) {\n\t\t\tfor (j = 0; j < (clen >> 1); j++)\n\t\t\t\tencipher(cdata, j << 1);\n\t\t}\n\n\t\tret = new byte[clen * 4];\n\t\tfor (i = 0, j = 0; i < clen; i++) {\n\t\t\tret[j++] = (byte) ((cdata[i] >> 24) & 0xff);\n\t\t\tret[j++] = (byte) ((cdata[i] >> 16) & 0xff);\n\t\t\tret[j++] = (byte) ((cdata[i] >> 8) & 0xff);\n\t\t\tret[j++] = (byte) (cdata[i] & 0xff);\n\t\t}\n\t\treturn ret;\n\t}\n\n\t/**\n\t * 生成密文，使用长度为10的加盐方式\n\t *\n\t * @param password 需要加密的明文\n\t * @return 密文\n\t */\n\tpublic static String hashpw(String password) {\n\t\treturn hashpw(password, gensalt());\n\t}\n\n\t/**\n\t * 生成密文\n\t *\n\t * @param password 需要加密的明文\n\t * @param salt     盐，使用{@link #gensalt()} 生成\n\t * @return 密文\n\t */\n\tpublic static String hashpw(String password, String salt) {\n\t\tBCrypt bcrypt;\n\t\tString real_salt;\n\t\tbyte[] saltb;\n\t\tbyte[] hashed;\n\t\tchar minor = (char) 0;\n\t\tint rounds, off;\n\t\tStringBuilder rs = new StringBuilder();\n\n\t\tif (salt.charAt(0) != '$' || salt.charAt(1) != '2')\n\t\t\tthrow new IllegalArgumentException(\"Invalid salt version\");\n\t\tif (salt.charAt(2) == '$')\n\t\t\toff = 3;\n\t\telse {\n\t\t\tminor = salt.charAt(2);\n\t\t\t// pr#1560@Github\n\t\t\t// 修正一个在Blowfish实现上的安全风险\n\t\t\tif ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$')\n\t\t\t\tthrow new IllegalArgumentException(\"Invalid salt revision\");\n\t\t\toff = 4;\n\t\t}\n\n\t\t// Extract number of rounds\n\t\tif (salt.charAt(off + 2) > '$')\n\t\t\tthrow new IllegalArgumentException(\"Missing salt rounds\");\n\t\trounds = Integer.parseInt(salt.substring(off, off + 2));\n\n\t\treal_salt = salt.substring(off + 3, off + 25);\n\t\tbyte[] passwordb = (password + (minor >= 'a' ? \"\\000\" : \"\")).getBytes(CharsetUtil.CHARSET_UTF_8);\n\t\tsaltb = decodeBase64(real_salt, BCRYPT_SALT_LEN);\n\n\t\tbcrypt = new BCrypt();\n\t\thashed = bcrypt.crypt(passwordb, saltb, rounds, bf_crypt_ciphertext.clone());\n\n\t\trs.append(\"$2\");\n\t\tif (minor >= 'a')\n\t\t\trs.append(minor);\n\t\trs.append(\"$\");\n\t\tif (rounds < 10)\n\t\t\trs.append(\"0\");\n\t\tif (rounds > 30) {\n\t\t\tthrow new IllegalArgumentException(\"rounds exceeds maximum (30)\");\n\t\t}\n\t\trs.append(rounds);\n\t\trs.append(\"$\");\n\t\trs.append(encode_base64(saltb, saltb.length));\n\t\trs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1));\n\t\treturn rs.toString();\n\t}\n\n\t/**\n\t * 生成盐\n\t *\n\t * @param log_rounds hash中叠加的2的对数 - the work factor therefore increases as 2**log_rounds.\n\t * @param random     {@link SecureRandom}\n\t * @return an encoded salt value\n\t */\n\tpublic static String gensalt(int log_rounds, SecureRandom random) {\n\t\tfinal StringBuilder rs = new StringBuilder();\n\t\tbyte[] rnd = new byte[BCRYPT_SALT_LEN];\n\n\t\trandom.nextBytes(rnd);\n\n\t\trs.append(\"$2a$\");\n\t\tif (log_rounds < 10)\n\t\t\trs.append(\"0\");\n\t\tif (log_rounds > 30) {\n\t\t\tthrow new IllegalArgumentException(\"log_rounds exceeds maximum (30)\");\n\t\t}\n\t\trs.append(log_rounds);\n\t\trs.append(\"$\");\n\t\trs.append(encode_base64(rnd, rnd.length));\n\t\treturn rs.toString();\n\t}\n\n\t/**\n\t * 生成盐\n\t *\n\t * @param log_rounds the log2 of the number of rounds of hashing to apply - the work factor therefore increases as 2**log_rounds.\n\t * @return 盐\n\t */\n\tpublic static String gensalt(int log_rounds) {\n\t\treturn gensalt(log_rounds, new SecureRandom());\n\t}\n\n\t/**\n\t * 生成盐\n\t *\n\t * @return 盐\n\t */\n\tpublic static String gensalt() {\n\t\treturn gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);\n\t}\n\n\t/**\n\t * 检查明文密码文本是否匹配加密后的文本\n\t *\n\t * @param plaintext 需要验证的明文密码\n\t * @param hashed    密文\n\t * @return 是否匹配\n\t */\n\tpublic static boolean checkpw(String plaintext, String hashed) {\n\t\tbyte[] hashed_bytes;\n\t\tbyte[] try_bytes;\n\n\t\tString try_pw;\n\t\ttry{\n\t\t\ttry_pw = hashpw(plaintext, hashed);\n\t\t} catch (Exception ignore){\n\t\t\t// 生成密文时错误直接返回false issue#1377@Github\n\t\t\treturn false;\n\t\t}\n\t\thashed_bytes = hashed.getBytes(CharsetUtil.CHARSET_UTF_8);\n\t\ttry_bytes = try_pw.getBytes(CharsetUtil.CHARSET_UTF_8);\n\t\tif (hashed_bytes.length != try_bytes.length) {\n\t\t\treturn false;\n\t\t}\n\t\tbyte ret = 0;\n\t\tfor (int i = 0; i < try_bytes.length; i++)\n\t\t\tret |= hashed_bytes[i] ^ try_bytes[i];\n\t\treturn ret == 0;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestAlgorithm.java",
    "content": "package cn.hutool.crypto.digest;\n\n/**\n * 摘要算法类型<br>\n * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest\n * \n * @author Looly\n */\npublic enum DigestAlgorithm {\n\tMD2(\"MD2\"), \n\tMD5(\"MD5\"), \n\tSHA1(\"SHA-1\"), \n\tSHA256(\"SHA-256\"), \n\tSHA384(\"SHA-384\"), \n\tSHA512(\"SHA-512\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t * \n\t * @param value 算法字符串表示\n\t */\n\tDigestAlgorithm(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取算法字符串表示\n\t * @return 算法字符串表示\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestUtil.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.util.CharsetUtil;\n\nimport javax.crypto.SecretKey;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 摘要算法工具类\n *\n * @author Looly\n */\npublic class DigestUtil {\n\n\t// ------------------------------------------------------------------------------------------- MD5\n\n\t/**\n\t * 计算32位MD5摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] md5(byte[] data) {\n\t\treturn new MD5().digest(data);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] md5(String data, String charset) {\n\t\treturn new MD5().digest(data, charset);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，使用UTF-8编码\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] md5(String data) {\n\t\treturn md5(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] md5(InputStream data) {\n\t\treturn new MD5().digest(data);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值\n\t *\n\t * @param file 被摘要文件\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] md5(File file) {\n\t\treturn new MD5().digest(file);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t */\n\tpublic static String md5Hex(byte[] data) {\n\t\treturn new MD5().digestHex(data);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return MD5摘要的16进制表示\n\t */\n\tpublic static String md5Hex(String data, String charset) {\n\t\treturn new MD5().digestHex(data, charset);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex(String data, Charset charset) {\n\t\treturn new MD5().digestHex(data, charset);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t */\n\tpublic static String md5Hex(String data) {\n\t\treturn md5Hex(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t */\n\tpublic static String md5Hex(InputStream data) {\n\t\treturn new MD5().digestHex(data);\n\t}\n\n\t/**\n\t * 计算32位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param file 被摘要文件\n\t * @return MD5摘要的16进制表示\n\t */\n\tpublic static String md5Hex(File file) {\n\t\treturn new MD5().digestHex(file);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- MD5 16\n\n\t/**\n\t * 计算16位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex16(byte[] data) {\n\t\treturn new MD5().digestHex16(data);\n\t}\n\n\t/**\n\t * 计算16位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex16(String data, Charset charset) {\n\t\treturn new MD5().digestHex16(data, charset);\n\t}\n\n\t/**\n\t * 计算16位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex16(String data) {\n\t\treturn md5Hex16(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 计算16位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex16(InputStream data) {\n\t\treturn new MD5().digestHex16(data);\n\t}\n\n\t/**\n\t * 计算16位MD5摘要值，并转为16进制字符串\n\t *\n\t * @param file 被摘要文件\n\t * @return MD5摘要的16进制表示\n\t * @since 4.6.0\n\t */\n\tpublic static String md5Hex16(File file) {\n\t\treturn new MD5().digestHex16(file);\n\t}\n\n\t/**\n\t * 32位MD5转16位MD5\n\t *\n\t * @param md5Hex 32位MD5\n\t * @return 16位MD5\n\t * @since 4.4.1\n\t */\n\tpublic static String md5HexTo16(String md5Hex) {\n\t\treturn md5Hex.substring(8, 24);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- SHA-1\n\n\t/**\n\t * 计算SHA-1摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-1摘要\n\t */\n\tpublic static byte[] sha1(byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-1摘要\n\t */\n\tpublic static byte[] sha1(String data, String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digest(data, charset);\n\t}\n\n\t/**\n\t * 计算sha1摘要值，使用UTF-8编码\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] sha1(String data) {\n\t\treturn sha1(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-1摘要\n\t */\n\tpublic static byte[] sha1(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-1摘要\n\t */\n\tpublic static byte[] sha1(File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digest(file);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-1摘要的16进制表示\n\t */\n\tpublic static String sha1Hex(byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-1摘要的16进制表示\n\t */\n\tpublic static String sha1Hex(String data, String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(data, charset);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-1摘要的16进制表示\n\t */\n\tpublic static String sha1Hex(String data) {\n\t\treturn sha1Hex(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-1摘要的16进制表示\n\t */\n\tpublic static String sha1Hex(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-1摘要的16进制表示\n\t */\n\tpublic static String sha1Hex(File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA1).digestHex(file);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- SHA-256\n\n\t/**\n\t * 计算SHA-256摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha256(byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-256摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha256(String data, String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digest(data, charset);\n\t}\n\n\t/**\n\t * 计算sha256摘要值，使用UTF-8编码\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha256(String data) {\n\t\treturn sha256(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha256(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-256摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha256(File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digest(file);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要的16进制表示\n\t * @since 3.0.8\n\t */\n\tpublic static String sha256Hex(byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-256摘要的16进制表示\n\t * @since 3.0.8\n\t */\n\tpublic static String sha256Hex(String data, String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(data, charset);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要的16进制表示\n\t * @since 3.0.8\n\t */\n\tpublic static String sha256Hex(String data) {\n\t\treturn sha256Hex(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-256摘要的16进制表示\n\t * @since 3.0.8\n\t */\n\tpublic static String sha256Hex(InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-256摘要值，并转为16进制字符串\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-256摘要的16进制表示\n\t * @since 3.0.8\n\t */\n\tpublic static String sha256Hex(File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA256).digestHex(file);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- Hmac\n\n\t/**\n\t * 创建HMac对象，调用digest方法可获得hmac值\n\t *\n\t * @param algorithm {@link HmacAlgorithm}\n\t * @param key       密钥，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.0.3\n\t */\n\tpublic static HMac hmac(HmacAlgorithm algorithm, byte[] key) {\n\t\treturn new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * 创建HMac对象，调用digest方法可获得hmac值\n\t *\n\t * @param algorithm {@link HmacAlgorithm}\n\t * @param key       密钥{@link SecretKey}，如果为{@code null}生成随机密钥\n\t * @return {@link HMac}\n\t * @since 3.0.3\n\t */\n\tpublic static HMac hmac(HmacAlgorithm algorithm, SecretKey key) {\n\t\treturn new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * 新建摘要器\n\t *\n\t * @param algorithm 签名算法\n\t * @return Digester\n\t * @since 4.0.1\n\t */\n\tpublic static Digester digester(DigestAlgorithm algorithm) {\n\t\treturn new Digester(algorithm);\n\t}\n\n\t/**\n\t * 新建摘要器\n\t *\n\t * @param algorithm 签名算法\n\t * @return Digester\n\t * @since 4.2.1\n\t */\n\tpublic static Digester digester(String algorithm) {\n\t\treturn new Digester(algorithm);\n\t}\n\n\t/**\n\t * 生成Bcrypt加密后的密文\n\t *\n\t * @param password 明文密码\n\t * @return 加密后的密文\n\t * @since 4.1.1\n\t */\n\tpublic static String bcrypt(String password) {\n\t\treturn BCrypt.hashpw(password);\n\t}\n\n\t/**\n\t * 验证密码是否与Bcrypt加密后的密文匹配\n\t *\n\t * @param password 明文密码\n\t * @param hashed   hash值（加密后的值）\n\t * @return 是否匹配\n\t * @since 4.1.1\n\t */\n\tpublic static boolean bcryptCheck(String password, String hashed) {\n\t\treturn BCrypt.checkpw(password, hashed);\n\t}\n\n\t// ------------------------------------------------------------------------------------------- SHA-512\n\n\t/**\n\t * 计算SHA-512摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-512摘要\n\t */\n\tpublic static byte[] sha512(final byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-512摘要\n\t * @since 3.0.8\n\t */\n\tpublic static byte[] sha512(final String data, final String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digest(data, charset);\n\t}\n\n\t/**\n\t * 计算sha512摘要值，使用UTF-8编码\n\t *\n\t * @param data 被摘要数据\n\t * @return MD5摘要\n\t */\n\tpublic static byte[] sha512(final String data) {\n\t\treturn sha512(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-512摘要\n\t */\n\tpublic static byte[] sha512(final InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digest(data);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-512摘要\n\t */\n\tpublic static byte[] sha512(final File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digest(file);\n\t}\n\n\t/**\n\t * 计算SHA-1摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-512摘要的16进制表示\n\t */\n\tpublic static String sha512Hex(final byte[] data) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return SHA-512摘要的16进制表示\n\t */\n\tpublic static String sha512Hex(final String data, final String charset) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digestHex(data, charset);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-512摘要的16进制表示\n\t */\n\tpublic static String sha512Hex(final String data) {\n\t\treturn sha512Hex(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @return SHA-512摘要的16进制表示\n\t */\n\tpublic static String sha512Hex(final InputStream data) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digestHex(data);\n\t}\n\n\t/**\n\t * 计算SHA-512摘要值，并转为16进制字符串\n\t *\n\t * @param file 被摘要文件\n\t * @return SHA-512摘要的16进制表示\n\t */\n\tpublic static String sha512Hex(final File file) {\n\t\treturn new Digester(DigestAlgorithm.SHA512).digestHex(file);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.Provider;\n\n/**\n * 摘要算法<br>\n * 注意：此对象实例化后为非线程安全！\n *\n * @author Looly\n *\n */\npublic class Digester implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate MessageDigest digest;\n\t/** 盐值 */\n\tprotected byte[] salt;\n\t/** 加盐位置，即将盐值字符串放置在数据的index数，默认0 */\n\tprotected int saltPosition;\n\t/** 散列次数 */\n\tprotected int digestCount;\n\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法枚举\n\t */\n\tpublic Digester(DigestAlgorithm algorithm) {\n\t\tthis(algorithm.getValue());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法枚举\n\t */\n\tpublic Digester(String algorithm) {\n\t\tthis(algorithm, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param provider 算法提供者，null表示JDK默认，可以引入Bouncy Castle等来提供更多算法支持\n\t * @since 4.5.1\n\t */\n\tpublic Digester(DigestAlgorithm algorithm, Provider provider) {\n\t\tinit(algorithm.getValue(), provider);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param provider 算法提供者，null表示JDK默认，可以引入Bouncy Castle等来提供更多算法支持\n\t * @since 4.5.1\n\t */\n\tpublic Digester(String algorithm, Provider provider) {\n\t\tinit(algorithm, provider);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param messageDigest {@link MessageDigest}\n\t */\n\tpublic Digester(final MessageDigest messageDigest) {\n\t\tthis.digest = messageDigest;\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm 算法\n\t * @param provider 算法提供者，null表示JDK默认，可以引入Bouncy Castle等来提供更多算法支持\n\t * @return Digester\n\t * @throws CryptoException Cause by IOException\n\t */\n\tpublic Digester init(String algorithm, Provider provider) {\n\t\tif(null == provider) {\n\t\t\tthis.digest = SecureUtil.createMessageDigest(algorithm);\n\t\t}else {\n\t\t\ttry {\n\t\t\t\tthis.digest = MessageDigest.getInstance(algorithm, provider);\n\t\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\t\tthrow new CryptoException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置加盐内容\n\t *\n\t * @param salt 盐值\n\t * @return this\n\t * @since 4.4.3\n\t */\n\tpublic Digester setSalt(byte[] salt) {\n\t\tthis.salt = salt;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置加盐的位置，只有盐值存在时有效<br>\n\t * 加盐的位置指盐位于数据byte数组中的位置，例如：\n\t *\n\t * <pre>\n\t * data: 0123456\n\t * </pre>\n\t *\n\t * 则当saltPosition = 2时，盐位于data的1和2中间，即第二个空隙，即：\n\t *\n\t * <pre>\n\t * data: 01[salt]23456\n\t * </pre>\n\t *\n\t *\n\t * @param saltPosition 盐的位置\n\t * @return this\n\t * @since 4.4.3\n\t */\n\tpublic Digester setSaltPosition(int saltPosition) {\n\t\tthis.saltPosition = saltPosition;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置重复计算摘要值次数\n\t *\n\t * @param digestCount 摘要值次数\n\t * @return this\n\t */\n\tpublic Digester setDigestCount(int digestCount) {\n\t\tthis.digestCount = digestCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置{@link MessageDigest}\n\t *\n\t * @return this\n\t * @since 4.5.1\n\t */\n\tpublic Digester reset() {\n\t\tthis.digest.reset();\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------------------------------------------------- Digest\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @param charsetName 编码\n\t * @return 摘要\n\t */\n\tpublic byte[] digest(String data, String charsetName) {\n\t\treturn digest(data, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @param charset 编码\n\t * @return 摘要\n\t * @since 4.6.0\n\t */\n\tpublic byte[] digest(String data, Charset charset) {\n\t\treturn digest(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic byte[] digest(String data) {\n\t\treturn digest(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @param charsetName 编码\n\t * @return 摘要\n\t */\n\tpublic String digestHex(String data, String charsetName) {\n\t\treturn digestHex(data, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为16进制字符串\n\t *\n\t * @param data 被摘要数据\n\t * @param charset 编码\n\t * @return 摘要\n\t * @since 4.6.0\n\t */\n\tpublic String digestHex(String data, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(digest(data, charset));\n\t}\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(String data) {\n\t\treturn digestHex(data, CharsetUtil.UTF_8);\n\t}\n\n\t/**\n\t * 生成文件摘要<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param file 被摘要文件\n\t * @return 摘要bytes\n\t * @throws CryptoException Cause by IOException\n\t */\n\tpublic byte[] digest(File file) throws CryptoException {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treturn digest(in);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param file 被摘要文件\n\t * @return 摘要\n\t */\n\tpublic String digestHex(File file) {\n\t\treturn HexUtil.encodeHexStr(digest(file));\n\t}\n\n\t/**\n\t * 生成摘要，考虑加盐和重复摘要次数\n\t *\n\t * @param data 数据bytes\n\t * @return 摘要bytes\n\t */\n\tpublic byte[] digest(byte[] data) {\n\t\tbyte[] result;\n\t\tif (this.saltPosition <= 0) {\n\t\t\t// 加盐在开头，自动忽略空盐值\n\t\t\tresult = doDigest(this.salt, data);\n\t\t} else if (this.saltPosition >= data.length) {\n\t\t\t// 加盐在末尾，自动忽略空盐值\n\t\t\tresult = doDigest(data, this.salt);\n\t\t} else if (ArrayUtil.isNotEmpty(this.salt)) {\n\t\t\t// 加盐在中间\n\t\t\tthis.digest.update(data, 0, this.saltPosition);\n\t\t\tthis.digest.update(this.salt);\n\t\t\tthis.digest.update(data, this.saltPosition, data.length - this.saltPosition);\n\t\t\tresult = this.digest.digest();\n\t\t} else {\n\t\t\t// 无加盐\n\t\t\tresult = doDigest(data);\n\t\t}\n\n\t\treturn resetAndRepeatDigest(result);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(digest(data));\n\t}\n\n\t/**\n\t * 生成摘要，使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @return 摘要bytes\n\t */\n\tpublic byte[] digest(InputStream data) {\n\t\treturn digest(data, IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(InputStream data) {\n\t\treturn HexUtil.encodeHexStr(digest(data));\n\t}\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic byte[] digest(InputStream data, int bufferLength) throws IORuntimeException {\n\t\tif (bufferLength < 1) {\n\t\t\tbufferLength = IoUtil.DEFAULT_BUFFER_SIZE;\n\t\t}\n\n\t\tbyte[] result;\n\t\ttry {\n\t\t\tif (ArrayUtil.isEmpty(this.salt)) {\n\t\t\t\tresult = digestWithoutSalt(data, bufferLength);\n\t\t\t} else {\n\t\t\t\tresult = digestWithSalt(data, bufferLength);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn resetAndRepeatDigest(result);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data 被摘要数据\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要\n\t */\n\tpublic String digestHex(InputStream data, int bufferLength) {\n\t\treturn HexUtil.encodeHexStr(digest(data, bufferLength));\n\t}\n\n\t/**\n\t * 获得 {@link MessageDigest}\n\t *\n\t * @return {@link MessageDigest}\n\t */\n\tpublic MessageDigest getDigest() {\n\t\treturn digest;\n\t}\n\n\t/**\n\t * 获取散列长度，0表示不支持此方法\n\t *\n\t * @return 散列长度，0表示不支持此方法\n\t * @since 4.5.0\n\t */\n\tpublic int getDigestLength() {\n\t\treturn this.digest.getDigestLength();\n\t}\n\n\t// -------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要bytes\n\t * @throws IOException 从流中读取数据引发的IO异常\n\t */\n\tprivate byte[] digestWithoutSalt(InputStream data, int bufferLength) throws IOException {\n\t\tfinal byte[] buffer = new byte[bufferLength];\n\t\tint read;\n\t\twhile ((read = data.read(buffer, 0, bufferLength)) > -1) {\n\t\t\tthis.digest.update(buffer, 0, read);\n\t\t}\n\t\treturn this.digest.digest();\n\t}\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要bytes\n\t * @throws IOException 从流中读取数据引发的IO异常\n\t */\n\tprivate byte[] digestWithSalt(InputStream data, int bufferLength) throws IOException {\n\t\tif (this.saltPosition <= 0) {\n\t\t\t// 加盐在开头\n\t\t\tthis.digest.update(this.salt);\n\t\t}\n\n\t\tfinal byte[] buffer = new byte[bufferLength];\n\t\tint total = 0;\n\t\tint read;\n\t\twhile ((read = data.read(buffer, 0, bufferLength)) > -1) {\n\t\t\ttotal += read;\n\t\t\tif (this.saltPosition > 0 && total >= this.saltPosition) {\n\t\t\t\tif (total != this.saltPosition) {\n\t\t\t\t\tdigest.update(buffer, 0, total - this.saltPosition);\n\t\t\t\t}\n\t\t\t\t// 加盐在中间\n\t\t\t\tthis.digest.update(this.salt);\n\t\t\t\tthis.digest.update(buffer, total - this.saltPosition, read);\n\t\t\t} else {\n\t\t\t\tthis.digest.update(buffer, 0, read);\n\t\t\t}\n\t\t}\n\n\t\tif (total < this.saltPosition) {\n\t\t\t// 加盐在末尾\n\t\t\tthis.digest.update(this.salt);\n\t\t}\n\n\t\treturn this.digest.digest();\n\t}\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param datas 数据bytes\n\t * @return 摘要bytes\n\t * @since 4.4.3\n\t */\n\tprivate byte[] doDigest(byte[]... datas) {\n\t\tfor (byte[] data : datas) {\n\t\t\tif (null != data) {\n\t\t\t\tthis.digest.update(data);\n\t\t\t}\n\t\t}\n\t\treturn this.digest.digest();\n\t}\n\n\t/**\n\t * 重复计算摘要，取决于{@link #digestCount} 值<br>\n\t * 每次计算摘要前都会重置{@link #digest}\n\t *\n\t * @param digestData 第一次摘要过的数据\n\t * @return 摘要\n\t */\n\tprivate byte[] resetAndRepeatDigest(byte[] digestData) {\n\t\tfinal int digestCount = Math.max(1, this.digestCount);\n\t\treset();\n\t\tfor (int i = 0; i < digestCount - 1; i++) {\n\t\t\tdigestData = doDigest(digestData);\n\t\t\treset();\n\t\t}\n\t\treturn digestData;\n\t}\n\t// -------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigesterFactory.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.crypto.digest;\n\nimport cn.hutool.crypto.GlobalBouncyCastleProvider;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.security.MessageDigest;\n\n/**\n * {@link Digester}创建简单工厂，用于生产{@link Digester}对象<br>\n * 参考Guava方式，工厂负责持有一个原始的{@link MessageDigest}对象，使用时优先通过clone方式创建对象，提高初始化性能。\n *\n * @author looly\n */\npublic class DigesterFactory {\n\n\t/**\n\t * 创建工厂\n\t *\n\t * @param algorithm 算法\n\t * @return DigesterFactory\n\t */\n\tpublic static DigesterFactory ofJdk(final String algorithm) {\n\t\treturn of(SecureUtil.createJdkMessageDigest(algorithm));\n\t}\n\n\t/**\n\t * 创建工厂，使用{@link GlobalBouncyCastleProvider}找到的提供方。\n\t *\n\t * @param algorithm 算法\n\t * @return DigesterFactory\n\t */\n\tpublic static DigesterFactory of(final String algorithm) {\n\t\treturn of(SecureUtil.createMessageDigest(algorithm));\n\t}\n\n\t/**\n\t * 创建工厂\n\t *\n\t * @param messageDigest {@link MessageDigest}，可以通过{@link SecureUtil#createMessageDigest(String)} 创建\n\t * @return DigesterFactory\n\t */\n\tpublic static DigesterFactory of(final MessageDigest messageDigest) {\n\t\treturn new DigesterFactory(messageDigest);\n\t}\n\n\tprivate final MessageDigest prototype;\n\tprivate final boolean cloneSupport;\n\n\t/**\n\t * 构造\n\t *\n\t * @param messageDigest {@link MessageDigest}模板\n\t */\n\tprivate DigesterFactory(final MessageDigest messageDigest) {\n\t\tthis.prototype = messageDigest;\n\t\tthis.cloneSupport = checkCloneSupport(messageDigest);\n\t}\n\n\t/**\n\t * 创建{@link Digester}\n\t *\n\t * @return {@link Digester}\n\t */\n\tpublic Digester createDigester() {\n\t\treturn new Digester(createMessageDigester());\n\t}\n\n\t/**\n\t * 创建{@link MessageDigest}\n\t *\n\t * @return {@link MessageDigest}\n\t */\n\tpublic MessageDigest createMessageDigester() {\n\t\tif (cloneSupport) {\n\t\t\ttry {\n\t\t\t\treturn (MessageDigest) prototype.clone();\n\t\t\t} catch (final CloneNotSupportedException ignore) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\treturn SecureUtil.createJdkMessageDigest(prototype.getAlgorithm());\n\t}\n\n\t/**\n\t * 检查{@link MessageDigest}对象是否支持clone方法\n\t *\n\t * @param messageDigest {@link MessageDigest}\n\t * @return 是否支持clone方法\n\t */\n\tprivate static boolean checkCloneSupport(final MessageDigest messageDigest) {\n\t\ttry {\n\t\t\tmessageDigest.clone();\n\t\t\treturn true;\n\t\t} catch (final CloneNotSupportedException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.crypto.digest.mac.Mac;\nimport cn.hutool.crypto.digest.mac.MacEngine;\nimport cn.hutool.crypto.digest.mac.MacEngineFactory;\n\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.Key;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * HMAC摘要算法<br>\n * HMAC，全称为“Hash Message Authentication Code”，中文名“散列消息鉴别码”<br>\n * 主要是利用哈希算法，以一个密钥和一个消息为输入，生成一个消息摘要作为输出。<br>\n * 一般的，消息鉴别码用于验证传输于两个共 同享有一个密钥的单位之间的消息。<br>\n * HMAC 可以与任何迭代散列函数捆绑使用。MD5 和 SHA-1 就是这种散列函数。HMAC 还可以使用一个用于计算和确认消息鉴别值的密钥。<br>\n * 注意：此对象实例化后为非线程安全！\n *\n * @author Looly\n */\npublic class HMac extends Mac {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，自动生成密钥\n\t *\n\t * @param algorithm 算法 {@link HmacAlgorithm}\n\t */\n\tpublic HMac(HmacAlgorithm algorithm) {\n\t\tthis(algorithm, (Key) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法 {@link HmacAlgorithm}\n\t * @param key       密钥\n\t */\n\tpublic HMac(HmacAlgorithm algorithm, byte[] key) {\n\t\tthis(algorithm.getValue(), key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法 {@link HmacAlgorithm}\n\t * @param key       密钥\n\t */\n\tpublic HMac(HmacAlgorithm algorithm, Key key) {\n\t\tthis(algorithm.getValue(), key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @since 4.5.13\n\t */\n\tpublic HMac(String algorithm, byte[] key) {\n\t\tthis(algorithm, new SecretKeySpec(key, algorithm));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @since 4.5.13\n\t */\n\tpublic HMac(String algorithm, Key key) {\n\t\tthis(algorithm, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @param spec      {@link AlgorithmParameterSpec}\n\t * @since 5.6.12\n\t */\n\tpublic HMac(String algorithm, Key key, AlgorithmParameterSpec spec) {\n\t\tthis(MacEngineFactory.createEngine(algorithm, key, spec));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine MAC算法实现引擎\n\t * @since 4.5.13\n\t */\n\tpublic HMac(MacEngine engine) {\n\t\tsuper(engine);\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/HmacAlgorithm.java",
    "content": "package cn.hutool.crypto.digest;\n\n/**\n * HMAC算法类型<br>\n * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac\n *\n * @author Looly\n */\npublic enum HmacAlgorithm {\n\tHmacMD5(\"HmacMD5\"),\n\tHmacSHA1(\"HmacSHA1\"),\n\tHmacSHA256(\"HmacSHA256\"),\n\tHmacSHA384(\"HmacSHA384\"),\n\tHmacSHA512(\"HmacSHA512\"),\n\t/** HmacSM3算法实现，需要BouncyCastle库支持 */\n\tHmacSM3(\"HmacSM3\"),\n\t/** SM4 CMAC模式实现，需要BouncyCastle库支持 */\n\tSM4CMAC(\"SM4CMAC\");\n\n\tprivate final String value;\n\n\tHmacAlgorithm(String value) {\n\t\tthis.value = value;\n\t}\n\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/MD5.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * MD5算法\n *\n * @author looly\n * @since 4.4.3\n */\npublic class MD5 extends Digester {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// issue#I6ZIQH\n\t// MD5算法不使用BC库，使用JDK默认以提高初始性能\n\tprivate static final DigesterFactory FACTORY = DigesterFactory.ofJdk(DigestAlgorithm.MD5.getValue());\n\n\t/**\n\t * 创建MD5实例\n\t *\n\t * @return MD5\n\t * @since 4.6.0\n\t */\n\tpublic static MD5 create() {\n\t\treturn new MD5();\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic MD5() {\n\t\tsuper(FACTORY.createMessageDigester());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt 盐值\n\t */\n\tpublic MD5(byte[] salt) {\n\t\tthis(salt, 0, 1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt 盐值\n\t * @param digestCount 摘要次数，当此值小于等于1,默认为1。\n\t */\n\tpublic MD5(byte[] salt, int digestCount) {\n\t\tthis(salt, 0, digestCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt 盐值\n\t * @param saltPosition 加盐位置，即将盐值字符串放置在数据的index数，默认0\n\t * @param digestCount 摘要次数，当此值小于等于1,默认为1。\n\t */\n\tpublic MD5(byte[] salt, int saltPosition, int digestCount) {\n\t\tthis();\n\t\tthis.salt = salt;\n\t\tthis.saltPosition = saltPosition;\n\t\tthis.digestCount = digestCount;\n\t}\n\n\t/**\n\t * 生成16位MD5摘要\n\t *\n\t * @param data 数据\n\t * @param charset 编码\n\t * @return 16位MD5摘要\n\t * @since 4.6.0\n\t */\n\tpublic String digestHex16(String data, Charset charset) {\n\t\treturn DigestUtil.md5HexTo16(digestHex(data, charset));\n\t}\n\n\t/**\n\t * 生成16位MD5摘要\n\t *\n\t * @param data 数据\n\t * @return 16位MD5摘要\n\t * @since 4.5.1\n\t */\n\tpublic String digestHex16(String data) {\n\t\treturn DigestUtil.md5HexTo16(digestHex(data));\n\t}\n\n\t/**\n\t * 生成16位MD5摘要\n\t *\n\t * @param data 数据\n\t * @return 16位MD5摘要\n\t * @since 4.5.1\n\t */\n\tpublic String digestHex16(InputStream data) {\n\t\treturn DigestUtil.md5HexTo16(digestHex(data));\n\t}\n\n\t/**\n\t * 生成16位MD5摘要\n\t *\n\t * @param data 数据\n\t * @return 16位MD5摘要\n\t */\n\tpublic String digestHex16(File data) {\n\t\treturn DigestUtil.md5HexTo16(digestHex(data));\n\t}\n\n\t/**\n\t * 生成16位MD5摘要\n\t *\n\t * @param data 数据\n\t * @return 16位MD5摘要\n\t * @since 4.5.1\n\t */\n\tpublic String digestHex16(byte[] data) {\n\t\treturn DigestUtil.md5HexTo16(digestHex(data));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/SM3.java",
    "content": "package cn.hutool.crypto.digest;\n\n/**\n * 国密SM3杂凑（摘要）算法\n *\n * <p>\n * 国密算法包括：\n * <ol>\n *     <li>非对称加密和签名：SM2</li>\n *     <li>摘要签名算法：SM3</li>\n *     <li>对称加密：SM4</li>\n * </ol>\n *\n * @author looly\n * @since 4.6.8\n */\npublic class SM3 extends Digester {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final String ALGORITHM_NAME = \"SM3\";\n\n\t/**\n\t * 创建SM3实例\n\t *\n\t * @return SM3\n\t * @since 4.6.0\n\t */\n\tpublic static SM3 create() {\n\t\treturn new SM3();\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic SM3() {\n\t\tsuper(ALGORITHM_NAME);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt 盐值\n\t */\n\tpublic SM3(byte[] salt) {\n\t\tthis(salt, 0, 1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt        盐值\n\t * @param digestCount 摘要次数，当此值小于等于1,默认为1。\n\t */\n\tpublic SM3(byte[] salt, int digestCount) {\n\t\tthis(salt, 0, digestCount);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param salt         盐值\n\t * @param saltPosition 加盐位置，即将盐值字符串放置在数据的index数，默认0\n\t * @param digestCount  摘要次数，当此值小于等于1,默认为1。\n\t */\n\tpublic SM3(byte[] salt, int saltPosition, int digestCount) {\n\t\tthis();\n\t\tthis.salt = salt;\n\t\tthis.saltPosition = saltPosition;\n\t\tthis.digestCount = digestCount;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCHMacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.Digest;\nimport org.bouncycastle.crypto.Mac;\nimport org.bouncycastle.crypto.macs.HMac;\nimport org.bouncycastle.crypto.params.KeyParameter;\nimport org.bouncycastle.crypto.params.ParametersWithIV;\n\n/**\n * BouncyCastle的HMAC算法实现引擎，使用{@link Mac} 实现摘要<br>\n * 当引入BouncyCastle库时自动使用其作为Provider\n *\n * @author Looly\n * @since 4.5.13\n */\npublic class BCHMacEngine extends BCMacEngine {\n\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param digest 摘要算法，为{@link Digest} 的接口实现\n\t * @param key    密钥\n\t * @param iv     加盐\n\t * @since 5.7.12\n\t */\n\tpublic BCHMacEngine(Digest digest, byte[] key, byte[] iv) {\n\t\tthis(digest, new ParametersWithIV(new KeyParameter(key), iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param digest 摘要算法，为{@link Digest} 的接口实现\n\t * @param key    密钥\n\t * @since 4.5.13\n\t */\n\tpublic BCHMacEngine(Digest digest, byte[] key) {\n\t\tthis(digest, new KeyParameter(key));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param digest 摘要算法\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @since 4.5.13\n\t */\n\tpublic BCHMacEngine(Digest digest, CipherParameters params) {\n\t\tthis(new HMac(digest), params);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mac {@link HMac}\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @since 5.8.0\n\t */\n\tpublic BCHMacEngine(HMac mac, CipherParameters params) {\n\t\tsuper(mac, params);\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param digest 摘要算法\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @return this\n\t * @see #init(Mac, CipherParameters)\n\t */\n\tpublic BCHMacEngine init(Digest digest, CipherParameters params) {\n\t\treturn (BCHMacEngine) init(new HMac(digest), params);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCMacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.Mac;\nimport org.bouncycastle.crypto.params.KeyParameter;\n\n/**\n * BouncyCastle的MAC算法实现引擎，使用{@link Mac} 实现摘要<br>\n * 当引入BouncyCastle库时自动使用其作为Provider\n *\n * @author Looly\n * @since 5.8.0\n */\npublic class BCMacEngine implements MacEngine {\n\n\tprivate Mac mac;\n\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param mac    {@link Mac}\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @since 5.8.0\n\t */\n\tpublic BCMacEngine(Mac mac, CipherParameters params) {\n\t\tinit(mac, params);\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param mac    摘要算法\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @return this\n\t * @since 5.8.0\n\t */\n\tpublic BCMacEngine init(Mac mac, CipherParameters params) {\n\t\tmac.init(params);\n\t\tthis.mac = mac;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得 {@link Mac}\n\t *\n\t * @return {@link Mac}\n\t */\n\tpublic Mac getMac() {\n\t\treturn mac;\n\t}\n\n\t@Override\n\tpublic void update(byte[] in, int inOff, int len) {\n\t\tthis.mac.update(in, inOff, len);\n\t}\n\n\t@Override\n\tpublic byte[] doFinal() {\n\t\tfinal byte[] result = new byte[getMacLength()];\n\t\tthis.mac.doFinal(result, 0);\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\tthis.mac.reset();\n\t}\n\n\t@Override\n\tpublic int getMacLength() {\n\t\treturn mac.getMacSize();\n\t}\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn this.mac.getAlgorithmName();\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/CBCBlockCipherMacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport org.bouncycastle.crypto.BlockCipher;\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.Digest;\nimport org.bouncycastle.crypto.Mac;\nimport org.bouncycastle.crypto.macs.CBCBlockCipherMac;\nimport org.bouncycastle.crypto.params.KeyParameter;\nimport org.bouncycastle.crypto.params.ParametersWithIV;\n\nimport java.security.Key;\n\n/**\n * {@link CBCBlockCipherMac}实现的MAC算法，使用CBC Block方式\n *\n * @author looly\n * @since 5.8.0\n */\npublic class CBCBlockCipherMacEngine extends BCMacEngine {\n\n\t/**\n\t * 构造\n\t *\n\t * @param digest        摘要算法，为{@link Digest} 的接口实现\n\t * @param macSizeInBits mac结果的bits长度，必须为8的倍数\n\t * @param key           密钥\n\t * @param iv            加盐\n\t * @since 5.7.12\n\t */\n\tpublic CBCBlockCipherMacEngine(BlockCipher digest, int macSizeInBits, Key key, byte[] iv) {\n\t\tthis(digest, macSizeInBits, key.getEncoded(), iv);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param digest        摘要算法，为{@link Digest} 的接口实现\n\t * @param macSizeInBits mac结果的bits长度，必须为8的倍数\n\t * @param key           密钥\n\t * @param iv            加盐\n\t */\n\tpublic CBCBlockCipherMacEngine(BlockCipher digest, int macSizeInBits, byte[] key, byte[] iv) {\n\t\tthis(digest, macSizeInBits, new ParametersWithIV(new KeyParameter(key), iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param cipher        算法，为{@link BlockCipher} 的接口实现\n\t * @param macSizeInBits mac结果的bits长度，必须为8的倍数\n\t * @param key           密钥\n\t */\n\tpublic CBCBlockCipherMacEngine(BlockCipher cipher, int macSizeInBits, Key key) {\n\t\tthis(cipher, macSizeInBits, key.getEncoded());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param cipher        算法，为{@link BlockCipher} 的接口实现\n\t * @param macSizeInBits mac结果的bits长度，必须为8的倍数\n\t * @param key           密钥\n\t */\n\tpublic CBCBlockCipherMacEngine(BlockCipher cipher, int macSizeInBits, byte[] key) {\n\t\tthis(cipher, macSizeInBits, new KeyParameter(key));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param cipher        算法，为{@link BlockCipher} 的接口实现\n\t * @param macSizeInBits mac结果的bits长度，必须为8的倍数\n\t * @param params        参数，例如密钥可以用{@link KeyParameter}\n\t */\n\tpublic CBCBlockCipherMacEngine(BlockCipher cipher, int macSizeInBits, CipherParameters params) {\n\t\tthis(new CBCBlockCipherMac(cipher, macSizeInBits), params);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mac    {@link CBCBlockCipherMac}\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t */\n\tpublic CBCBlockCipherMacEngine(CBCBlockCipherMac mac, CipherParameters params) {\n\t\tsuper(mac, params);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param cipher {@link BlockCipher}\n\t * @param params 参数，例如密钥可以用{@link KeyParameter}\n\t * @return this\n\t * @see #init(Mac, CipherParameters)\n\t */\n\tpublic CBCBlockCipherMacEngine init(BlockCipher cipher, CipherParameters params) {\n\t\treturn (CBCBlockCipherMacEngine) init(new CBCBlockCipherMac(cipher), params);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/DefaultHMacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.SecureUtil;\n\nimport javax.crypto.Mac;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.Key;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * 默认的HMAC算法实现引擎，使用{@link Mac} 实现摘要<br>\n * 当引入BouncyCastle库时自动使用其作为Provider\n *\n * @author Looly\n * @since 4.5.13\n */\npublic class DefaultHMacEngine implements MacEngine {\n\n\tprivate Mac mac;\n\n\t// ------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @since 4.5.13\n\t */\n\tpublic DefaultHMacEngine(String algorithm, byte[] key) {\n\t\tthis(algorithm, (null == key) ? null : new SecretKeySpec(key, algorithm));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @since 4.5.13\n\t */\n\tpublic DefaultHMacEngine(String algorithm, Key key) {\n\t\tthis(algorithm, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @param spec {@link AlgorithmParameterSpec}\n\t * @since 5.7.12\n\t */\n\tpublic DefaultHMacEngine(String algorithm, Key key, AlgorithmParameterSpec spec) {\n\t\tinit(algorithm, key, spec);\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @return this\n\t */\n\tpublic DefaultHMacEngine init(String algorithm, byte[] key) {\n\t\treturn init(algorithm, (null == key) ? null : new SecretKeySpec(key, algorithm));\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥 {@link SecretKey}\n\t * @return this\n\t * @throws CryptoException Cause by IOException\n\t */\n\tpublic DefaultHMacEngine init(String algorithm, Key key) {\n\t\treturn init(algorithm, key, null);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥 {@link SecretKey}\n\t * @param spec      {@link AlgorithmParameterSpec}\n\t * @return this\n\t * @throws CryptoException Cause by IOException\n\t * @since 5.7.12\n\t */\n\tpublic DefaultHMacEngine init(String algorithm, Key key, AlgorithmParameterSpec spec) {\n\t\ttry {\n\t\t\tmac = SecureUtil.createMac(algorithm);\n\t\t\tif (null == key) {\n\t\t\t\tkey = SecureUtil.generateKey(algorithm);\n\t\t\t}\n\t\t\tif (null != spec) {\n\t\t\t\tmac.init(key, spec);\n\t\t\t} else {\n\t\t\t\tmac.init(key);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得 {@link Mac}\n\t *\n\t * @return {@link Mac}\n\t */\n\tpublic Mac getMac() {\n\t\treturn mac;\n\t}\n\n\t@Override\n\tpublic void update(byte[] in) {\n\t\tthis.mac.update(in);\n\t}\n\n\t@Override\n\tpublic void update(byte[] in, int inOff, int len) {\n\t\tthis.mac.update(in, inOff, len);\n\t}\n\n\t@Override\n\tpublic byte[] doFinal() {\n\t\treturn this.mac.doFinal();\n\t}\n\n\t@Override\n\tpublic void reset() {\n\t\tthis.mac.reset();\n\t}\n\n\t@Override\n\tpublic int getMacLength() {\n\t\treturn mac.getMacLength();\n\t}\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn this.mac.getAlgorithm();\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/Mac.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CryptoException;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.security.MessageDigest;\n\n/**\n * MAC摘要算法（此类兼容和JCE的 {@code javax.crypto.Mac}对象和BC库的{@code org.bouncycastle.crypto.Mac}对象）<br>\n * MAC，全称为“Message Authentication Code”，中文名“消息鉴别码”<br>\n * 主要是利用指定算法，以一个密钥和一个消息为输入，生成一个消息摘要作为输出。<br>\n * 一般的，消息鉴别码用于验证传输于两个共同享有一个密钥的单位之间的消息。<br>\n * 注意：此对象实例化后为非线程安全！\n *\n * @author Looly\n * @since 5.8.0\n */\npublic class Mac implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final MacEngine engine;\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine MAC算法实现引擎\n\t */\n\tpublic Mac(MacEngine engine) {\n\t\tthis.engine = engine;\n\t}\n\t// ------------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获得MAC算法引擎\n\t *\n\t * @return MAC算法引擎\n\t */\n\tpublic MacEngine getEngine() {\n\t\treturn this.engine;\n\t}\n\n\t// ------------------------------------------------------------------------------------------- Digest\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return 摘要\n\t */\n\tpublic byte[] digest(String data, Charset charset) {\n\t\treturn digest(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic byte[] digest(String data) {\n\t\treturn digest(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为Base64\n\t *\n\t * @param data      被摘要数据\n\t * @param isUrlSafe 是否使用URL安全字符\n\t * @return 摘要\n\t */\n\tpublic String digestBase64(String data, boolean isUrlSafe) {\n\t\treturn digestBase64(data, CharsetUtil.CHARSET_UTF_8, isUrlSafe);\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为Base64\n\t *\n\t * @param data      被摘要数据\n\t * @param charset   编码\n\t * @param isUrlSafe 是否使用URL安全字符\n\t * @return 摘要\n\t */\n\tpublic String digestBase64(String data, Charset charset, boolean isUrlSafe) {\n\t\tfinal byte[] digest = digest(data, charset);\n\t\treturn isUrlSafe ? Base64.encodeUrlSafe(digest) : Base64.encode(digest);\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为16进制字符串\n\t *\n\t * @param data    被摘要数据\n\t * @param charset 编码\n\t * @return 摘要\n\t */\n\tpublic String digestHex(String data, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(digest(data, charset));\n\t}\n\n\t/**\n\t * 生成文件摘要\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(String data) {\n\t\treturn digestHex(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 生成文件摘要<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param file 被摘要文件\n\t * @return 摘要bytes\n\t * @throws CryptoException Cause by IOException\n\t */\n\tpublic byte[] digest(File file) throws CryptoException {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\treturn digest(in);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 生成文件摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param file 被摘要文件\n\t * @return 摘要\n\t */\n\tpublic String digestHex(File file) {\n\t\treturn HexUtil.encodeHexStr(digest(file));\n\t}\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data 数据bytes\n\t * @return 摘要bytes\n\t */\n\tpublic byte[] digest(byte[] data) {\n\t\treturn digest(new ByteArrayInputStream(data), -1);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(digest(data));\n\t}\n\n\t/**\n\t * 生成摘要，使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @return 摘要bytes\n\t */\n\tpublic byte[] digest(InputStream data) {\n\t\treturn digest(data, IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data 被摘要数据\n\t * @return 摘要\n\t */\n\tpublic String digestHex(InputStream data) {\n\t\treturn HexUtil.encodeHexStr(digest(data));\n\t}\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data         {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要bytes\n\t */\n\tpublic byte[] digest(InputStream data, int bufferLength) {\n\t\treturn this.engine.digest(data, bufferLength);\n\t}\n\n\t/**\n\t * 生成摘要，并转为16进制字符串<br>\n\t * 使用默认缓存大小，见 {@link IoUtil#DEFAULT_BUFFER_SIZE}\n\t *\n\t * @param data         被摘要数据\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要\n\t */\n\tpublic String digestHex(InputStream data, int bufferLength) {\n\t\treturn HexUtil.encodeHexStr(digest(data, bufferLength));\n\t}\n\n\t/**\n\t * 验证生成的摘要与给定的摘要比较是否一致<br>\n\t * 简单比较每个byte位是否相同\n\t *\n\t * @param digest          生成的摘要\n\t * @param digestToCompare 需要比较的摘要\n\t * @return 是否一致\n\t * @see MessageDigest#isEqual(byte[], byte[])\n\t * @since 5.6.8\n\t */\n\tpublic boolean verify(byte[] digest, byte[] digestToCompare) {\n\t\treturn MessageDigest.isEqual(digest, digestToCompare);\n\t}\n\n\t/**\n\t * 获取MAC算法块长度\n\t *\n\t * @return MAC算法块长度\n\t * @since 5.3.3\n\t */\n\tpublic int getMacLength() {\n\t\treturn this.engine.getMacLength();\n\t}\n\n\t/**\n\t * 获取算法\n\t *\n\t * @return 算法\n\t * @since 5.3.3\n\t */\n\tpublic String getAlgorithm() {\n\t\treturn this.engine.getAlgorithm();\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.crypto.CryptoException;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * MAC（Message Authentication Code）算法引擎\n *\n * @author Looly\n * @since 4.5.13\n */\npublic interface MacEngine {\n\n\t/**\n\t * 加入需要被摘要的内容\n\t * @param in 内容\n\t * @since 5.7.0\n\t */\n\tdefault void update(byte[] in){\n\t\tupdate(in, 0, in.length);\n\t}\n\n\t/**\n\t * 加入需要被摘要的内容\n\t * @param in 内容\n\t * @param inOff 内容起始位置\n\t * @param len 内容长度\n\t * @since 5.7.0\n\t */\n\tvoid update(byte[] in, int inOff, int len);\n\n\t/**\n\t * 结束并生成摘要\n\t *\n\t * @return 摘要内容\n\t * @since 5.7.0\n\t */\n\tbyte[] doFinal();\n\n\t/**\n\t * 重置\n\t * @since 5.7.0\n\t */\n\tvoid reset();\n\n\t/**\n\t * 生成摘要\n\t *\n\t * @param data {@link InputStream} 数据流\n\t * @param bufferLength 缓存长度，不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值\n\t * @return 摘要bytes\n\t */\n\tdefault byte[] digest(InputStream data, int bufferLength){\n\t\tif (bufferLength < 1) {\n\t\t\tbufferLength = IoUtil.DEFAULT_BUFFER_SIZE;\n\t\t}\n\n\t\tfinal byte[] buffer = new byte[bufferLength];\n\n\t\tbyte[] result;\n\t\ttry {\n\t\t\tint read = data.read(buffer, 0, bufferLength);\n\n\t\t\twhile (read > -1) {\n\t\t\t\tupdate(buffer, 0, read);\n\t\t\t\tread = data.read(buffer, 0, bufferLength);\n\t\t\t}\n\t\t\tresult = doFinal();\n\t\t} catch (IOException e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\treset();\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取MAC算法块大小\n\t *\n\t * @return MAC算法块大小\n\t */\n\tint getMacLength();\n\n\t/**\n\t * 获取当前算法\n\t *\n\t * @return 算法\n\t */\n\tString getAlgorithm();\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngineFactory.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport cn.hutool.crypto.SmUtil;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\n\nimport java.security.Key;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * {@link MacEngine} 实现工厂类\n *\n * @author Looly\n * @since 4.5.13\n */\npublic class MacEngineFactory {\n\n\t/**\n\t * 根据给定算法和密钥生成对应的{@link MacEngine}\n\t *\n\t * @param algorithm 算法，见{@link HmacAlgorithm}\n\t * @param key       密钥\n\t * @return {@link MacEngine}\n\t */\n\tpublic static MacEngine createEngine(String algorithm, Key key) {\n\t\treturn createEngine(algorithm, key, null);\n\t}\n\n\t/**\n\t * 根据给定算法和密钥生成对应的{@link MacEngine}\n\t *\n\t * @param algorithm 算法，见{@link HmacAlgorithm}\n\t * @param key       密钥\n\t * @param spec      spec\n\t * @return {@link MacEngine}\n\t * @since 5.7.12\n\t */\n\tpublic static MacEngine createEngine(String algorithm, Key key, AlgorithmParameterSpec spec) {\n\t\tif (algorithm.equalsIgnoreCase(HmacAlgorithm.HmacSM3.getValue())) {\n\t\t\t// HmacSM3算法是BC库实现的，忽略加盐\n\t\t\treturn SmUtil.createHmacSm3Engine(key.getEncoded());\n\t\t}\n\t\treturn new DefaultHMacEngine(algorithm, key, spec);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/SM4MacEngine.java",
    "content": "package cn.hutool.crypto.digest.mac;\n\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.engines.SM4Engine;\n\n/**\n * SM4算法的MAC引擎实现\n *\n * @author looly\n * @since 5.8.0\n */\npublic class SM4MacEngine extends CBCBlockCipherMacEngine {\n\n\tprivate static final int MAC_SIZE = 128;\n\n\t/**\n\t * 构造\n\t *\n\t * @param params {@link CipherParameters}\n\t */\n\tpublic SM4MacEngine(CipherParameters params) {\n\t\tsuper(new SM4Engine(), MAC_SIZE, params);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/package-info.java",
    "content": "/**\n * HMAC，全称为“Hash Message Authentication Code”，中文名“散列消息鉴别码”<br>\n * 主要是利用哈希算法，以一个密钥和一个消息为输入，生成一个消息摘要作为输出。<br>\n * 一般的，消息鉴别码用于验证传输于两个共 同享有一个密钥的单位之间的消息。<br>\n * HMAC 可以与任何迭代散列函数捆绑使用。MD5 和 SHA-1 就是这种散列函数。HMAC 还可以使用一个用于计算和确认消息鉴别值的密钥。<br>\n * \n * @author Looly\n * @since 4.5.13\n */\npackage cn.hutool.crypto.digest.mac;"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/otp/HOTP.java",
    "content": "package cn.hutool.crypto.digest.otp;\n\nimport cn.hutool.core.codec.Base32;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.digest.HMac;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\n\n/**\n * <p>HMAC-based one-time passwords (HOTP) 基于HMAC算法一次性密码生成器，\n * 规范见：<a href=\"https://tools.ietf.org/html/rfc4226\">RFC&nbsp;4226</a>.</p>\n *\n * <p>基于事件同步，通过某一特定的事件次序及相同的种子值作为输入，通过HASH算法运算出一致的密码。</p>\n *\n * <p>参考：https://github.com/jchambers/java-otp</p>\n *\n * @author Looly\n */\npublic class HOTP {\n\n\t/**\n\t * 数子量级\n\t */\n\tprivate static final int[] MOD_DIVISORS = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};\n\t/**\n\t * 默认密码长度.\n\t */\n\tpublic static final int DEFAULT_PASSWORD_LENGTH = 6;\n\n\t/**\n\t * 默认HMAC算法.\n\t */\n\tpublic static final HmacAlgorithm HOTP_HMAC_ALGORITHM = HmacAlgorithm.HmacSHA1;\n\n\tprivate final HMac mac;\n\tprivate final int passwordLength;\n\tprivate final int modDivisor;\n\n\tprivate final byte[] buffer;\n\n\t/**\n\t * 构造，使用默认密码长度和默认HMAC算法(HmacSHA1)\n\t *\n\t * @param key 共享密码，RFC 4226要求最少128位\n\t */\n\tpublic HOTP(byte[] key) {\n\t\tthis(DEFAULT_PASSWORD_LENGTH, key);\n\t}\n\n\t/**\n\t * 构造，使用默认HMAC算法(HmacSHA1)\n\t *\n\t * @param passwordLength 密码长度，可以是6,7,8\n\t * @param key            共享密码，RFC 4226要求最少128位\n\t */\n\tpublic HOTP(int passwordLength, byte[] key) {\n\t\tthis(passwordLength, HOTP_HMAC_ALGORITHM, key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param passwordLength 密码长度，可以是6,7,8\n\t * @param algorithm      HMAC算法枚举\n\t * @param key            共享密码，RFC 4226要求最少128位\n\t */\n\tpublic HOTP(int passwordLength, HmacAlgorithm algorithm, byte[] key) {\n\t\tif(passwordLength >= MOD_DIVISORS.length){\n\t\t\tthrow new IllegalArgumentException(\"Password length must be < \" + MOD_DIVISORS.length);\n\t\t}\n\t\tthis.mac = new HMac(algorithm, key);\n\t\tthis.modDivisor = MOD_DIVISORS[passwordLength];\n\t\tthis.passwordLength = passwordLength;\n\t\tthis.buffer = new byte[8];\n\t}\n\n\t/**\n\t * 生成一次性密码\n\t *\n\t * @param counter 事件计数的值，8 字节的整数，称为移动因子（moving factor），\n\t *                可以是基于计次的动移动因子，也可以是计时移动因子\n\t * @return 一次性密码的int值\n\t */\n\tpublic synchronized int generate(long counter) {\n\t\t// C 的整数值需要用二进制的字符串表达，比如某个事件计数为 3，\n\t\t// 则C是 \"11\"（此处省略了前面的二进制的数字0）\n\t\tthis.buffer[0] = (byte) ((counter & 0xff00000000000000L) >>> 56);\n\t\tthis.buffer[1] = (byte) ((counter & 0x00ff000000000000L) >>> 48);\n\t\tthis.buffer[2] = (byte) ((counter & 0x0000ff0000000000L) >>> 40);\n\t\tthis.buffer[3] = (byte) ((counter & 0x000000ff00000000L) >>> 32);\n\t\tthis.buffer[4] = (byte) ((counter & 0x00000000ff000000L) >>> 24);\n\t\tthis.buffer[5] = (byte) ((counter & 0x0000000000ff0000L) >>> 16);\n\t\tthis.buffer[6] = (byte) ((counter & 0x000000000000ff00L) >>> 8);\n\t\tthis.buffer[7] = (byte) (counter & 0x00000000000000ffL);\n\n\t\tfinal byte[] digest = this.mac.digest(this.buffer);\n\n\t\treturn truncate(digest);\n\t}\n\n\t/**\n\t * 生成共享密钥的Base32表示形式\n\t *\n\t * @param numBytes 将生成的种子字节数量。\n\t * @return 共享密钥\n\t * @since 5.7.4\n\t */\n\tpublic static String generateSecretKey(int numBytes) {\n\t\treturn Base32.encode(RandomUtil.getSHA1PRNGRandom(RandomUtil.randomBytes(256)).generateSeed(numBytes));\n\t}\n\n\t/**\n\t * 获取密码长度，可以是6,7,8\n\t *\n\t * @return 密码长度，可以是6,7,8\n\t */\n\tpublic int getPasswordLength() {\n\t\treturn this.passwordLength;\n\t}\n\n\t/**\n\t * 获取HMAC算法\n\t *\n\t * @return HMAC算法\n\t */\n\tpublic String getAlgorithm() {\n\t\treturn this.mac.getAlgorithm();\n\t}\n\n\t/**\n\t * 截断\n\t *\n\t * @param digest HMAC的hash值\n\t * @return 截断值\n\t */\n\tprivate int truncate(byte[] digest) {\n\t\tfinal int offset = digest[digest.length - 1] & 0x0f;\n\t\treturn ((digest[offset] & 0x7f) << 24 |\n\t\t\t\t(digest[offset + 1] & 0xff) << 16 |\n\t\t\t\t(digest[offset + 2] & 0xff) << 8 |\n\t\t\t\t(digest[offset + 3] & 0xff)) %\n\t\t\t\tthis.modDivisor;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/otp/TOTP.java",
    "content": "package cn.hutool.crypto.digest.otp;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\n/**\n * <p>time-based one-time passwords (TOTP) 基于时间戳算法的一次性密码生成器，\n * 规范见：<a href=\"https://tools.ietf.org/html/rfc6238\">RFC&nbsp;6238</a>.</p>\n *\n * <p>时间同步，基于客户端的动态口令和动态口令验证服务器的时间比对，一般每30秒产生一个新口令，\n * 要求客户端和服务器能够十分精确的保持正确的时钟，客户端和服务端基于时间计算的动态口令才能一致。</p>\n *\n * <p>参考：https://github.com/jchambers/java-otp</p>\n *\n * @author Looly\n */\npublic class TOTP extends HOTP {\n\n\t/**\n\t * 默认步进 (30秒).\n\t */\n\tpublic static final Duration DEFAULT_TIME_STEP = Duration.ofSeconds(30);\n\n\tprivate final Duration timeStep;\n\n\t/**\n\t * 构造，使用默认HMAC算法(HmacSHA1)\n\t *\n\t * @param key 共享密码，RFC 4226要求最少128位\n\t */\n\tpublic TOTP(byte[] key) {\n\t\tthis(DEFAULT_TIME_STEP, key);\n\t}\n\n\t/**\n\t * 构造，使用默认HMAC算法(HmacSHA1)\n\t *\n\t * @param timeStep 日期步进，用于生成移动因子（moving factor）\n\t * @param key      共享密码，RFC 4226要求最少128位\n\t */\n\tpublic TOTP(Duration timeStep, byte[] key) {\n\t\tthis(timeStep, DEFAULT_PASSWORD_LENGTH, key);\n\t}\n\n\t/**\n\t * 构造，使用默认HMAC算法(HmacSHA1)\n\t *\n\t * @param timeStep       日期步进，用于生成移动因子（moving factor）\n\t * @param passwordLength 密码长度，可以是6,7,8\n\t * @param key            共享密码，RFC 4226要求最少128位\n\t */\n\tpublic TOTP(Duration timeStep, int passwordLength, byte[] key) {\n\t\tthis(timeStep, passwordLength, HOTP_HMAC_ALGORITHM, key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param timeStep       日期步进，用于生成移动因子（moving factor）\n\t * @param passwordLength 密码长度，可以是6,7,8\n\t * @param algorithm      HMAC算法枚举\n\t * @param key            共享密码，RFC 4226要求最少128位\n\t */\n\tpublic TOTP(Duration timeStep, int passwordLength, HmacAlgorithm algorithm, byte[] key) {\n\t\tsuper(passwordLength, algorithm, key);\n\t\tthis.timeStep = timeStep;\n\t}\n\n\t/**\n\t * 使用给定的时间戳生成一次性密码.\n\t *\n\t * @param timestamp 用于生成密码的时间戳\n\t * @return 一次性密码的int形式\n\t */\n\tpublic int generate(Instant timestamp) {\n\t\treturn this.generate(timestamp.toEpochMilli() / this.timeStep.toMillis());\n\t}\n\n\t/**\n\t * 用于验证code是否正确\n\t *\n\t * @param timestamp  验证时间戳\n\t * @param offsetSize 误差范围\n\t * @param code       code\n\t * @return 是否通过\n\t * @since 5.7.4\n\t */\n\tpublic boolean validate(Instant timestamp, int offsetSize, int code) {\n\t\tif (offsetSize == 0) {\n\t\t\treturn generate(timestamp) == code;\n\t\t}\n\t\tfor (int i = -offsetSize; i <= offsetSize; i++) {\n\t\t\tif (generate(timestamp.plus(getTimeStep().multipliedBy(i))) == code) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 生成谷歌认证器的字符串（扫码字符串）\n\t * 基于时间的，计数器不适合\n\t *\n\t * @param account  账户名。\n\t * @param numBytes 将生成的种子字节数量。\n\t * @return 共享密钥\n\t * @since 5.7.4\n\t */\n\tpublic static String generateGoogleSecretKey(String account, int numBytes) {\n\t\treturn StrUtil.format(\"otpauth://totp/{}?secret={}\", account, generateSecretKey(numBytes));\n\t}\n\n\t/**\n\t * 获取步进\n\t *\n\t * @return 步进\n\t */\n\tpublic Duration getTimeStep() {\n\t\treturn this.timeStep;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/otp/package-info.java",
    "content": "/**\n * OTP 是 One-Time Password的简写，表示一次性密码。\n * <p>\n * 计算OTP串的公式：\n * <pre>\n * OTP(K,C) = Truncate(HMAC-SHA-1(K,C))\n * K：表示秘钥串\n * C：是一个数字，表示随机数\n * Truncate：是一个函数，就是怎么截取加密后的串，并取加密后串的哪些字段组成一个数字。\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.crypto.digest.otp;"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/digest/package-info.java",
    "content": "/**\n * 摘要加密算法实现，入口为DigestUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.crypto.digest;"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/package-info.java",
    "content": "/**\n * 加密解密模块，实现了对JDK中加密解密算法的封装，入口为SecureUtil，实现了：\n * \n * <pre>\n * 1. 对称加密（symmetric），例如：AES、DES等\n * 2. 非对称加密（asymmetric），例如：RSA、DSA等\n * 3. 摘要加密（digest），例如：MD5、SHA-1、SHA-256、HMAC等\n * </pre>\n * \n * @author looly\n *\n */\npackage cn.hutool.crypto;"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\nimport java.security.spec.AlgorithmParameterSpec;\n\n/**\n * AES加密算法实现<br>\n * 高级加密标准（英语：Advanced Encryption Standard，缩写：AES），在密码学中又称Rijndael加密法<br>\n * 对于Java中AES的默认模式是：AES/ECB/PKCS5Padding，如果使用CryptoJS，请调整为：padding: CryptoJS.pad.Pkcs7\n *\n * <p>\n * 相关概念说明：\n * <pre>\n * mode:    加密算法模式，是用来描述加密算法（此处特指分组密码，不包括流密码，）在加密时对明文分组的模式，它代表了不同的分组方式\n * padding: 补码方式是在分组密码中，当明文长度不是分组长度的整数倍时，需要在最后一个分组中填充一些数据使其凑满一个分组的长度。\n * iv:      在对明文分组加密时，会将明文分组与前一个密文分组进行XOR运算（即异或运算），但是加密第一个明文分组时不存在“前一个密文分组”，\n *          因此需要事先准备一个与分组长度相等的比特序列来代替，这个比特序列就是偏移量。\n * </pre>\n * <p>\n * 相关概念见：https://blog.csdn.net/OrangeJack/article/details/82913804\n *\n * @author Looly\n * @since 3.0.8\n */\npublic class AES extends SymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\t//------------------------------------------------------------------------- Constrctor start\n\n\t/**\n\t * 构造，默认AES/ECB/PKCS5Padding，使用随机密钥\n\t */\n\tpublic AES() {\n\t\tsuper(SymmetricAlgorithm.AES);\n\t}\n\n\t/**\n\t * 构造，使用默认的AES/ECB/PKCS5Padding\n\t *\n\t * @param key 密钥\n\t */\n\tpublic AES(byte[] key) {\n\t\tsuper(SymmetricAlgorithm.AES, key);\n\t}\n\n\t/**\n\t * 构造，使用默认的AES/ECB/PKCS5Padding\n\t *\n\t * @param key 密钥\n\t * @since 5.5.2\n\t */\n\tpublic AES(SecretKey key) {\n\t\tsuper(SymmetricAlgorithm.AES, key);\n\t}\n\n\t/**\n\t * 构造，使用随机密钥\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t */\n\tpublic AES(Mode mode, Padding padding) {\n\t\tthis(mode.name(), padding.name());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t */\n\tpublic AES(Mode mode, Padding padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t * @param iv      偏移向量，加盐\n\t * @since 3.3.0\n\t */\n\tpublic AES(Mode mode, Padding padding, byte[] key, byte[] iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t * @since 3.3.0\n\t */\n\tpublic AES(Mode mode, Padding padding, SecretKey key) {\n\t\tthis(mode, padding, key, (IvParameterSpec) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t * @param iv      偏移向量，加盐\n\t * @since 4.6.7\n\t */\n\tpublic AES(Mode mode, Padding padding, SecretKey key, byte[] iv) {\n\t\tthis(mode, padding, key, ArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode       模式{@link Mode}\n\t * @param padding    {@link Padding}补码方式\n\t * @param key        密钥，支持三种密钥长度：128、192、256位\n\t * @param paramsSpec 算法参数，例如加盐等\n\t * @since 3.3.0\n\t */\n\tpublic AES(Mode mode, Padding padding, SecretKey key, AlgorithmParameterSpec paramsSpec) {\n\t\tthis(mode.name(), padding.name(), key, paramsSpec);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t */\n\tpublic AES(String mode, String padding) {\n\t\tthis(mode, padding, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t */\n\tpublic AES(String mode, String padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t * @param iv      加盐\n\t */\n\tpublic AES(String mode, String padding, byte[] key, byte[] iv) {\n\t\tthis(mode, padding,//\n\t\t\t\tKeyUtil.generateKey(SymmetricAlgorithm.AES.getValue(), key),//\n\t\t\t\tArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持三种密钥长度：128、192、256位\n\t */\n\tpublic AES(String mode, String padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode       模式\n\t * @param padding    补码方式\n\t * @param key        密钥，支持三种密钥长度：128、192、256位\n\t * @param paramsSpec 算法参数，例如加盐等\n\t */\n\tpublic AES(String mode, String padding, SecretKey key, AlgorithmParameterSpec paramsSpec) {\n\t\tsuper(StrUtil.format(\"AES/{}/{}\", mode, padding), key, paramsSpec);\n\t}\n\t//------------------------------------------------------------------------- Constrctor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/ChaCha20.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.KeyUtil;\n\nimport javax.crypto.spec.IvParameterSpec;\n\n/**\n * ChaCha20算法实现<br>\n * ChaCha系列流密码，作为salsa密码的改良版，具有更强的抵抗密码分析攻击的特性，“20”表示该算法有20轮的加密计算。\n *\n * @author looly\n * @since 5.7.12\n */\npublic class ChaCha20 extends SymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final String ALGORITHM_NAME = \"ChaCha20\";\n\n\t/**\n\t * 构造\n\t *\n\t * @param key 密钥\n\t * @param iv  加盐，12bytes（64bit）\n\t */\n\tpublic ChaCha20(byte[] key, byte[] iv) {\n\t\tsuper(ALGORITHM_NAME,\n\t\t\t\tKeyUtil.generateKey(ALGORITHM_NAME, key),\n\t\t\t\tgenerateIvParam(iv));\n\t}\n\n\t/**\n\t * 生成加盐参数\n\t *\n\t * @param iv 加盐\n\t * @return {@link IvParameterSpec}\n\t */\n\tprivate static IvParameterSpec generateIvParam(byte[] iv) {\n\t\tif (null == iv) {\n\t\t\tiv = RandomUtil.randomBytes(12);\n\t\t}\n\t\treturn new IvParameterSpec(iv);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DES.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SecureUtil;\n\n/**\n * DES加密算法实现<br>\n * DES全称为Data Encryption Standard，即数据加密标准，是一种使用密钥加密的块算法<br>\n * Java中默认实现为：DES/ECB/PKCS5Padding\n * \n * @author Looly\n * @since 3.0.8\n */\npublic class DES extends SymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// ------------------------------------------------------------------------- Constrctor start\n\t/**\n\t * 构造，默认DES/ECB/PKCS5Padding，使用随机密钥\n\t */\n\tpublic DES() {\n\t\tsuper(SymmetricAlgorithm.DES);\n\t}\n\n\t/**\n\t * 构造，使用默认的DES/ECB/PKCS5Padding\n\t * \n\t * @param key 密钥\n\t */\n\tpublic DES(byte[] key) {\n\t\tsuper(SymmetricAlgorithm.DES, key);\n\t}\n\n\t/**\n\t * 构造，使用随机密钥\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t */\n\tpublic DES(Mode mode, Padding padding) {\n\t\tthis(mode.name(), padding.name());\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度：8的倍数\n\t */\n\tpublic DES(Mode mode, Padding padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度：8的倍数\n\t * @param iv 偏移向量，加盐\n\t * @since 3.3.0\n\t */\n\tpublic DES(Mode mode, Padding padding, byte[] key, byte[] iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度：8的倍数\n\t * @since 3.3.0\n\t */\n\tpublic DES(Mode mode, Padding padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度：8的倍数\n\t * @param iv 偏移向量，加盐\n\t * @since 3.3.0\n\t */\n\tpublic DES(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t */\n\tpublic DES(String mode, String padding) {\n\t\tthis(mode, padding, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度：8的倍数\n\t */\n\tpublic DES(String mode, String padding, byte[] key) {\n\t\tthis(mode, padding, SecureUtil.generateKey(\"DES\", key), null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度：8的倍数\n\t * @param iv 加盐\n\t */\n\tpublic DES(String mode, String padding, byte[] key, byte[] iv) {\n\t\tthis(mode, padding, SecureUtil.generateKey(\"DES\", key), null == iv ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度：8的倍数\n\t */\n\tpublic DES(String mode, String padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度：8的倍数\n\t * @param iv 加盐\n\t */\n\tpublic DES(String mode, String padding, SecretKey key, IvParameterSpec iv) {\n\t\tsuper(StrUtil.format(\"DES/{}/{}\", mode, padding), key, iv);\n\t}\n\t// ------------------------------------------------------------------------- Constrctor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DESede.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SecureUtil;\n\n/**\n * DESede是由DES对称加密算法改进后的一种对称加密算法，又名3DES、TripleDES。<br>\n * 使用 168 位的密钥对资料进行三次加密的一种机制；它通常（但非始终）提供极其强大的安全性。<br>\n * 如果三个 56 位的子元素都相同，则三重 DES 向后兼容 DES。<br>\n * Java中默认实现为：DESede/ECB/PKCS5Padding\n * \n * @author Looly\n * @since 3.3.0\n */\npublic class DESede extends SymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// ------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，默认DESede/ECB/PKCS5Padding，使用随机密钥\n\t */\n\tpublic DESede() {\n\t\tsuper(SymmetricAlgorithm.DESede);\n\t}\n\n\t/**\n\t * 构造，使用默认的DESede/ECB/PKCS5Padding\n\t * \n\t * @param key 密钥\n\t */\n\tpublic DESede(byte[] key) {\n\t\tsuper(SymmetricAlgorithm.DESede, key);\n\t}\n\n\t/**\n\t * 构造，使用随机密钥\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t */\n\tpublic DESede(Mode mode, Padding padding) {\n\t\tthis(mode.name(), padding.name());\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度24位\n\t */\n\tpublic DESede(Mode mode, Padding padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度24位\n\t * @param iv 偏移向量，加盐\n\t * @since 3.3.0\n\t */\n\tpublic DESede(Mode mode, Padding padding, byte[] key, byte[] iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度24位\n\t * @since 3.3.0\n\t */\n\tpublic DESede(Mode mode, Padding padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key 密钥，长度24位\n\t * @param iv 偏移向量，加盐\n\t * @since 3.3.0\n\t */\n\tpublic DESede(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t */\n\tpublic DESede(String mode, String padding) {\n\t\tthis(mode, padding, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度24位\n\t */\n\tpublic DESede(String mode, String padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥，长度24位\n\t * @param iv 加盐\n\t */\n\tpublic DESede(String mode, String padding, byte[] key, byte[] iv) {\n\t\tthis(mode, padding, SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue(), key), null == iv ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥\n\t */\n\tpublic DESede(String mode, String padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式\n\t * @param padding 补码方式\n\t * @param key 密钥\n\t * @param iv 加盐\n\t */\n\tpublic DESede(String mode, String padding, SecretKey key, IvParameterSpec iv) {\n\t\tsuper(StrUtil.format(\"{}/{}/{}\", SymmetricAlgorithm.DESede.getValue(), mode, padding), key, iv);\n\t}\n\t// ------------------------------------------------------------------------- Constructor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/PBKDF2.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.crypto.KeyUtil;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.PBEKeySpec;\n\n/**\n * PBKDF2应用一个伪随机函数以导出密钥，PBKDF2简单而言就是将salted hash进行多次重复计算。\n * 参考：https://blog.csdn.net/huoji555/article/details/83659687\n *\n * @author looly\n */\npublic class PBKDF2 {\n\n\tprivate String algorithm = \"PBKDF2WithHmacSHA1\";\n\t//生成密文的长度\n\tprivate int keyLength = 512;\n\n\t//迭代次数\n\tprivate int iterationCount = 1000;\n\n\t/**\n\t * 构造，算法PBKDF2WithHmacSHA1，盐长度16，密文长度512，迭代次数1000\n\t */\n\tpublic PBKDF2() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm      算法，一般为PBKDF2WithXXX\n\t * @param keyLength      生成密钥长度，默认512\n\t * @param iterationCount 迭代次数，默认1000\n\t */\n\tpublic PBKDF2(String algorithm, int keyLength, int iterationCount) {\n\t\tthis.algorithm = algorithm;\n\t\tthis.keyLength = keyLength;\n\t\tthis.iterationCount = iterationCount;\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param password 密码\n\t * @param salt     盐\n\t * @return 加密后的密码\n\t */\n\tpublic byte[] encrypt(char[] password, byte[] salt) {\n\t\tfinal PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, iterationCount, keyLength);\n\t\tfinal SecretKey secretKey = KeyUtil.generateKey(algorithm, pbeKeySpec);\n\t\treturn secretKey.getEncoded();\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param password 密码\n\t * @param salt     盐\n\t * @return 加密后的密码\n\t */\n\tpublic String encryptHex(char[] password, byte[] salt) {\n\t\treturn HexUtil.encodeHexStr(encrypt(password, salt));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/RC4.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;\n\n/**\n * RC4加密解密算法实现<br>\n * 注意：由于安全问题，已经基本不在HTTPS中使用了<br>\n * 来自：https://github.com/xSAVIKx/RC4-cipher/blob/master/src/main/java/com/github/xsavikx/rc4/RC4.java\n *\n * @author Iurii Sergiichuk，Looly\n */\npublic class RC4 implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int SBOX_LENGTH = 256;\n\t/** 密钥最小长度 */\n\tprivate static final int KEY_MIN_LENGTH = 5;\n\n\t/** Sbox */\n\tprivate int[] sbox;\n\n\tprivate final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();\n\n\t/**\n\t * 构造\n\t *\n\t * @param key 密钥\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic RC4(String key) throws CryptoException {\n\t\tsetKey(key);\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param message 消息\n\t * @param charset 编码\n\t * @return 密文\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic byte[] encrypt(String message, Charset charset) throws CryptoException {\n\t\treturn crypt(StrUtil.bytes(message, charset));\n\t}\n\n\t/**\n\t * 加密，使用默认编码：UTF-8\n\t *\n\t * @param message 消息\n\t * @return 密文\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic byte[] encrypt(String message) throws CryptoException {\n\t\treturn encrypt(message, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据\n\t * @return 加密后的Hex\n\t * @since 4.5.12\n\t */\n\tpublic String encryptHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(crypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据\n\t * @return 加密后的Base64\n\t * @since 4.5.12\n\t */\n\tpublic String encryptBase64(byte[] data) {\n\t\treturn Base64.encode(crypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Hex\n\t * @since 4.5.12\n\t */\n\tpublic String encryptHex(String data, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, charset));\n\t}\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Hex\n\t * @since 5.4.4\n\t */\n\tpublic String encryptHex(String data) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Base64\n\t * @since 4.5.12\n\t */\n\tpublic String encryptBase64(String data, Charset charset) {\n\t\treturn Base64.encode(encrypt(data, charset));\n\t}\n\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Base64\n\t * @since 5.4.4\n\t */\n\tpublic String encryptBase64(String data) {\n\t\treturn Base64.encode(encrypt(data));\n\t}\n\n\t/**\n\t * 解密\n\t *\n\t * @param message 消息\n\t * @param charset 编码\n\t * @return 明文\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic String decrypt(byte[] message, Charset charset) throws CryptoException {\n\t\treturn StrUtil.str(crypt(message), charset);\n\t}\n\n\t/**\n\t * 解密，使用默认编码UTF-8\n\t *\n\t * @param message 消息\n\t * @return 明文\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic String decrypt(byte[] message) throws CryptoException {\n\t\treturn decrypt(message, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 解密Hex（16进制）或Base64表示的字符串，使用默认编码UTF-8\n\t *\n\t * @param message 消息\n\t * @return 明文\n\t * @since 5.4.4\n\t */\n\tpublic String decrypt(String message) {\n\t\treturn decrypt(SecureUtil.decode(message));\n\t}\n\n\t/**\n\t * 解密Hex（16进制）或Base64表示的字符串\n\t *\n\t * @param message    明文\n\t * @param charset 解密后的charset\n\t * @return 明文\n\t * @since 5.4.4\n\t */\n\tpublic String decrypt(String message, Charset charset) {\n\t\treturn StrUtil.str(decrypt(message), charset);\n\t}\n\n\n\t/**\n\t * 加密或解密指定值，调用此方法前需初始化密钥\n\t *\n\t * @param msg 要加密或解密的消息\n\t * @return 加密或解密后的值\n\t */\n\tpublic byte[] crypt(final byte[] msg) {\n\t\tfinal ReadLock readLock = this.lock.readLock();\n\t\tbyte[] code;\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal int[] sbox = this.sbox.clone();\n\t\t\tcode = new byte[msg.length];\n\t\t\tint i = 0;\n\t\t\tint j = 0;\n\t\t\tfor (int n = 0; n < msg.length; n++) {\n\t\t\t\ti = (i + 1) % SBOX_LENGTH;\n\t\t\t\tj = (j + sbox[i]) % SBOX_LENGTH;\n\t\t\t\tswap(i, j, sbox);\n\t\t\t\tint rand = sbox[(sbox[i] + sbox[j]) % SBOX_LENGTH];\n\t\t\t\tcode[n] = (byte) (rand ^ msg[n]);\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn code;\n\t}\n\n\t/**\n\t * 设置密钥\n\t *\n\t * @param key 密钥\n\t * @throws CryptoException key长度小于5或者大于255抛出此异常\n\t */\n\tpublic void setKey(String key) throws CryptoException {\n\t\tfinal int length = key.length();\n\t\tif (length < KEY_MIN_LENGTH || length >= SBOX_LENGTH) {\n\t\t\tthrow new CryptoException(\"Key length has to be between {} and {}\", KEY_MIN_LENGTH, (SBOX_LENGTH - 1));\n\t\t}\n\n\t\tfinal WriteLock writeLock = this.lock.writeLock();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tthis.sbox = initSBox(StrUtil.utf8Bytes(key));\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t}\n\n\t//----------------------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 初始化Sbox\n\t *\n\t * @param key 密钥\n\t * @return sbox\n\t */\n\tprivate int[] initSBox(byte[] key) {\n\t\tint[] sbox = new int[SBOX_LENGTH];\n\t\tint j = 0;\n\n\t\tfor (int i = 0; i < SBOX_LENGTH; i++) {\n\t\t\tsbox[i] = i;\n\t\t}\n\n\t\tfor (int i = 0; i < SBOX_LENGTH; i++) {\n\t\t\t// fix: 运算符优先级修正，& 0xFF 应先与 key 字节值进行位与运算，再参与加法，避免有符号 byte 导致计算偏差\n\t\t\tj = (j + sbox[i] + (key[i % key.length] & 0xFF)) % SBOX_LENGTH;\n\t\t\tswap(i, j, sbox);\n\t\t}\n\t\treturn sbox;\n\t}\n\n\t/**\n\t * 交换指定两个位置的值\n\t *\n\t * @param i 位置1\n\t * @param j 位置2\n\t * @param sbox 数组\n\t */\n\tprivate void swap(int i, int j, int[] sbox) {\n\t\tint temp = sbox[i];\n\t\tsbox[i] = sbox[j];\n\t\tsbox[j] = temp;\n\t}\n\t//----------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SM4.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SecureUtil;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\n\n/**\n * 国密对称堆成加密算法SM4实现\n *\n * <p>\n * 国密算法包括：\n * <ol>\n *     <li>非对称加密和签名：SM2</li>\n *     <li>摘要签名算法：SM3</li>\n *     <li>对称加密：SM4</li>\n * </ol>\n *\n * @author Looly\n * @since 4.6.8\n */\npublic class SM4 extends SymmetricCrypto{\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final String ALGORITHM_NAME = \"SM4\";\n\n\t//------------------------------------------------------------------------- Constrctor start\n\t/**\n\t * 构造，使用随机密钥\n\t */\n\tpublic SM4() {\n\t\tsuper(ALGORITHM_NAME);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param key 密钥\n\t */\n\tpublic SM4(byte[] key) {\n\t\tsuper(ALGORITHM_NAME, key);\n\t}\n\n\t/**\n\t * 构造，使用随机密钥\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t */\n\tpublic SM4(Mode mode, Padding padding) {\n\t\tthis(mode.name(), padding.name());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t */\n\tpublic SM4(Mode mode, Padding padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t * @param iv      偏移向量，加盐\n\t */\n\tpublic SM4(Mode mode, Padding padding, byte[] key, byte[] iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t */\n\tpublic SM4(Mode mode, Padding padding, SecretKey key) {\n\t\tthis(mode, padding, key, (IvParameterSpec) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t * @param iv      偏移向量，加盐\n\t */\n\tpublic SM4(Mode mode, Padding padding, SecretKey key, byte[] iv) {\n\t\tthis(mode, padding, key, ArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式{@link Mode}\n\t * @param padding {@link Padding}补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t * @param iv      偏移向量，加盐\n\t */\n\tpublic SM4(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) {\n\t\tthis(mode.name(), padding.name(), key, iv);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t */\n\tpublic SM4(String mode, String padding) {\n\t\tthis(mode, padding, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t */\n\tpublic SM4(String mode, String padding, byte[] key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t * @param iv      加盐\n\t */\n\tpublic SM4(String mode, String padding, byte[] key, byte[] iv) {\n\t\tthis(mode, padding,//\n\t\t\t\tSecureUtil.generateKey(ALGORITHM_NAME, key),//\n\t\t\t\tArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t */\n\tpublic SM4(String mode, String padding, SecretKey key) {\n\t\tthis(mode, padding, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode    模式\n\t * @param padding 补码方式\n\t * @param key     密钥，支持密钥长度：128位\n\t * @param iv      加盐\n\t */\n\tpublic SM4(String mode, String padding, SecretKey key, IvParameterSpec iv) {\n\t\tsuper(StrUtil.format(\"SM4/{}/{}\", mode, padding), key, iv);\n\t}\n\t//------------------------------------------------------------------------- Constrctor end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricAlgorithm.java",
    "content": "package cn.hutool.crypto.symmetric;\n\n/**\n * 对称算法类型<br>\n * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyGenerator\n *\n * @author Looly\n *\n */\npublic enum SymmetricAlgorithm {\n\t/** 默认的AES加密方式：AES/ECB/PKCS5Padding */\n\tAES(\"AES\"),\n\tARCFOUR(\"ARCFOUR\"),\n\tBlowfish(\"Blowfish\"),\n\t/** 默认的DES加密方式：DES/ECB/PKCS5Padding */\n\tDES(\"DES\"),\n\t/** 3DES算法，默认实现为：DESede/ECB/PKCS5Padding */\n\tDESede(\"DESede\"),\n\tRC2(\"RC2\"),\n\n\tPBEWithMD5AndDES(\"PBEWithMD5AndDES\"),\n\tPBEWithSHA1AndDESede(\"PBEWithSHA1AndDESede\"),\n\tPBEWithSHA1AndRC2_40(\"PBEWithSHA1AndRC2_40\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t * @param value 算法的字符串表示，区分大小写\n\t */\n\tSymmetricAlgorithm(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获得算法的字符串表示形式\n\t * @return 算法字符串\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricCrypto.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Opt;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CipherMode;\nimport cn.hutool.crypto.CipherWrapper;\nimport cn.hutool.crypto.CryptoException;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Padding;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.CipherInputStream;\nimport javax.crypto.CipherOutputStream;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.PBEParameterSpec;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.SecureRandom;\nimport java.security.spec.AlgorithmParameterSpec;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * 对称加密算法<br>\n * 在对称加密算法中，数据发信方将明文（原始数据）和加密密钥一起经过特殊加密算法处理后，使其变成复杂的加密密文发送出去。<br>\n * 收信方收到密文后，若想解读原文，则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密，才能使其恢复成可读明文。<br>\n * 在对称加密算法中，使用的密钥只有一个，发收信双方都使用这个密钥对数据进行加密和解密，这就要求解密方事先必须知道加密密钥。<br>\n *\n * @author Looly\n */\npublic class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate CipherWrapper cipherWrapper;\n\t/**\n\t * SecretKey 负责保存对称密钥\n\t */\n\tprivate SecretKey secretKey;\n\t/**\n\t * 是否0填充\n\t */\n\tprivate boolean isZeroPadding;\n\tprivate final Lock lock = new ReentrantLock();\n\n\t// ------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 构造，使用随机密钥\n\t *\n\t * @param algorithm {@link SymmetricAlgorithm}\n\t */\n\tpublic SymmetricCrypto(SymmetricAlgorithm algorithm) {\n\t\tthis(algorithm, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造，使用随机密钥\n\t *\n\t * @param algorithm 算法，可以是\"algorithm/mode/padding\"或者\"algorithm\"\n\t */\n\tpublic SymmetricCrypto(String algorithm) {\n\t\tthis(algorithm, (byte[]) null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法 {@link SymmetricAlgorithm}\n\t * @param key       自定义KEY\n\t */\n\tpublic SymmetricCrypto(SymmetricAlgorithm algorithm, byte[] key) {\n\t\tthis(algorithm.getValue(), key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法 {@link SymmetricAlgorithm}\n\t * @param key       自定义KEY\n\t * @since 3.1.2\n\t */\n\tpublic SymmetricCrypto(SymmetricAlgorithm algorithm, SecretKey key) {\n\t\tthis(algorithm.getValue(), key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t */\n\tpublic SymmetricCrypto(String algorithm, byte[] key) {\n\t\tthis(algorithm, KeyUtil.generateKey(algorithm, key));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t * @since 3.1.2\n\t */\n\tpublic SymmetricCrypto(String algorithm, SecretKey key) {\n\t\tthis(algorithm, key, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm  算法\n\t * @param key        密钥\n\t * @param paramsSpec 算法参数，例如加盐等\n\t * @since 3.3.0\n\t */\n\tpublic SymmetricCrypto(String algorithm, SecretKey key, AlgorithmParameterSpec paramsSpec) {\n\t\tinit(algorithm, key);\n\t\tinitParams(algorithm, paramsSpec);\n\t}\n\n\t// ------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥，如果为{@code null}自动生成一个key\n\t * @return SymmetricCrypto的子对象，即子对象自身\n\t */\n\tpublic SymmetricCrypto init(String algorithm, SecretKey key) {\n\t\tAssert.notBlank(algorithm, \"'algorithm' must be not blank !\");\n\t\tthis.secretKey = key;\n\n\t\t// 检查是否为ZeroPadding，是则替换为NoPadding，并标记以便单独处理\n\t\tif (algorithm.contains(Padding.ZeroPadding.name())) {\n\t\t\talgorithm = StrUtil.replace(algorithm, Padding.ZeroPadding.name(), Padding.NoPadding.name());\n\t\t\tthis.isZeroPadding = true;\n\t\t}\n\n\t\tthis.cipherWrapper = new CipherWrapper(algorithm);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得对称密钥\n\t *\n\t * @return 获得对称密钥\n\t */\n\tpublic SecretKey getSecretKey() {\n\t\treturn secretKey;\n\t}\n\n\t/**\n\t * 获得加密或解密器\n\t *\n\t * @return 加密或解密\n\t */\n\tpublic Cipher getCipher() {\n\t\treturn cipherWrapper.getCipher();\n\t}\n\n\t/**\n\t * 设置偏移向量\n\t *\n\t * @param iv 偏移向量，加盐\n\t * @return 自身\n\t */\n\tpublic SymmetricCrypto setIv(byte[] iv) {\n\t\treturn setIv(new IvParameterSpec(iv));\n\t}\n\n\t/**\n\t * 设置偏移向量\n\t *\n\t * @param iv {@link IvParameterSpec}偏移向量\n\t * @return 自身\n\t */\n\tpublic SymmetricCrypto setIv(IvParameterSpec iv) {\n\t\treturn setParams(iv);\n\t}\n\n\t/**\n\t * 设置 {@link AlgorithmParameterSpec}，通常用于加盐或偏移向量\n\t *\n\t * @param params {@link AlgorithmParameterSpec}\n\t * @return 自身\n\t */\n\tpublic SymmetricCrypto setParams(AlgorithmParameterSpec params) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tthis.cipherWrapper.setParams(params);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置随机数生成器，可自定义随机数种子\n\t *\n\t * @param random 随机数生成器，可自定义随机数种子\n\t * @return this\n\t * @since 5.7.17\n\t */\n\tpublic SymmetricCrypto setRandom(SecureRandom random) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tthis.cipherWrapper.setRandom(random);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------------------------- Update\n\n\t/**\n\t * 初始化模式并清空数据\n\t *\n\t * @param mode 模式枚举\n\t * @return this\n\t * @since 5.7.12\n\t */\n\tpublic SymmetricCrypto setMode(CipherMode mode) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tinitMode(mode.getValue());\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 更新数据，分组加密中间结果可以当作随机数<br>\n\t * 第一次更新数据前需要调用{@link #setMode(CipherMode)}初始化加密或解密模式，然后每次更新数据都是累加模式\n\t *\n\t * @param data 被加密的bytes\n\t * @return update之后的bytes\n\t * @since 5.6.8\n\t */\n\tpublic byte[] update(byte[] data) {\n\t\tfinal Cipher cipher = cipherWrapper.getCipher();\n\t\tlock.lock();\n\t\ttry {\n\t\t\treturn cipher.update(paddingDataWithZero(data, cipher.getBlockSize()));\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 更新数据，分组加密中间结果可以当作随机数<br>\n\t * 第一次更新数据前需要调用{@link #setMode(CipherMode)}初始化加密或解密模式，然后每次更新数据都是累加模式\n\t *\n\t * @param data 被加密的bytes\n\t * @return update之后的hex数据\n\t * @since 5.6.8\n\t */\n\tpublic String updateHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(update(data));\n\t}\n\n\t// --------------------------------------------------------------------------------- Encrypt\n\n\t@Override\n\tpublic byte[] encrypt(byte[] data) {\n\t\tlock.lock();\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.ENCRYPT_MODE);\n\t\t\treturn cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void encrypt(InputStream data, OutputStream out, boolean isClose) throws IORuntimeException {\n\t\tlock.lock();\n\t\tCipherOutputStream cipherOutputStream = null;\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.ENCRYPT_MODE);\n\t\t\tcipherOutputStream = new CipherOutputStream(out, cipher);\n\t\t\tlong length = IoUtil.copy(data, cipherOutputStream);\n\t\t\tif (this.isZeroPadding) {\n\t\t\t\tfinal int blockSize = cipher.getBlockSize();\n\t\t\t\tif (blockSize > 0) {\n\t\t\t\t\t// 按照块拆分后的数据中多余的数据\n\t\t\t\t\tfinal int remainLength = (int) (length % blockSize);\n\t\t\t\t\tif (remainLength > 0) {\n\t\t\t\t\t\t// 补充0\n\t\t\t\t\t\tcipherOutputStream.write(new byte[blockSize - remainLength]);\n\t\t\t\t\t\tcipherOutputStream.flush();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (IORuntimeException e) {\n\t\t\tthrow e;\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t\t// issue#I4EMST@Gitee\n\t\t\t// CipherOutputStream必须关闭，才能完全写出\n\t\t\tIoUtil.close(cipherOutputStream);\n\t\t\tif (isClose) {\n\t\t\t\tIoUtil.close(data);\n\t\t\t}\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------- Decrypt\n\n\t@Override\n\tpublic byte[] decrypt(byte[] bytes) {\n\t\tfinal int blockSize;\n\t\tfinal byte[] decryptData;\n\n\t\tlock.lock();\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.DECRYPT_MODE);\n\t\t\tblockSize = cipher.getBlockSize();\n\t\t\tdecryptData = cipher.doFinal(bytes);\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t}\n\n\t\treturn removePadding(decryptData, blockSize);\n\t}\n\n\t@Override\n\tpublic void decrypt(InputStream data, OutputStream out, boolean isClose) throws IORuntimeException {\n\t\tlock.lock();\n\t\tCipherInputStream cipherInputStream = null;\n\t\ttry {\n\t\t\tfinal Cipher cipher = initMode(Cipher.DECRYPT_MODE);\n\t\t\tcipherInputStream = new CipherInputStream(data, cipher);\n\t\t\tif (this.isZeroPadding) {\n\t\t\t\tfinal int blockSize = cipher.getBlockSize();\n\t\t\t\tif (blockSize > 0) {\n\t\t\t\t\tcopyForZeroPadding(cipherInputStream, out, blockSize);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tIoUtil.copy(cipherInputStream, out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (IORuntimeException e) {\n\t\t\tthrow e;\n\t\t} catch (Exception e) {\n\t\t\tthrow new CryptoException(e);\n\t\t} finally {\n\t\t\tlock.unlock();\n\t\t\t// issue#I4EMST@Gitee\n\t\t\t// CipherOutputStream必须关闭，才能完全写出\n\t\t\tIoUtil.close(cipherInputStream);\n\t\t\tif (isClose) {\n\t\t\t\tIoUtil.close(data);\n\t\t\t}\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------- Getters\n\n\t// --------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 初始化加密解密参数，如IV等\n\t *\n\t * @param algorithm  算法\n\t * @param paramsSpec 用户定义的{@link AlgorithmParameterSpec}\n\t * @return this\n\t * @since 5.7.11\n\t */\n\tprivate SymmetricCrypto initParams(String algorithm, AlgorithmParameterSpec paramsSpec) {\n\t\tif (null == paramsSpec) {\n\t\t\tbyte[] iv = Opt.ofNullable(cipherWrapper)\n\t\t\t\t.map(CipherWrapper::getCipher).map(Cipher::getIV).get();\n\n\t\t\t// 随机IV\n\t\t\tif (StrUtil.startWithIgnoreCase(algorithm, \"PBE\")) {\n\t\t\t\t// 对于PBE算法使用随机数加盐\n\t\t\t\tif (null == iv) {\n\t\t\t\t\tiv = RandomUtil.randomBytes(8);\n\t\t\t\t}\n\t\t\t\tparamsSpec = new PBEParameterSpec(iv, 100);\n\t\t\t} else if (StrUtil.startWithIgnoreCase(algorithm, \"AES\")) {\n\t\t\t\tif (null != iv) {\n\t\t\t\t\t//AES使用Cipher默认的随机盐\n\t\t\t\t\tparamsSpec = new IvParameterSpec(iv);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn setParams(paramsSpec);\n\t}\n\n\t/**\n\t * 初始化{@link Cipher}为加密或者解密模式\n\t *\n\t * @param mode 模式，见{@link Cipher#ENCRYPT_MODE} 或 {@link Cipher#DECRYPT_MODE}\n\t * @return {@link Cipher}\n\t * @throws InvalidKeyException                无效key\n\t * @throws InvalidAlgorithmParameterException 无效算法\n\t */\n\tprivate Cipher initMode(int mode) throws InvalidKeyException, InvalidAlgorithmParameterException {\n\t\treturn this.cipherWrapper.initMode(mode, this.secretKey).getCipher();\n\t}\n\n\t/**\n\t * 数据按照blockSize的整数倍长度填充填充0\n\t *\n\t * <p>\n\t * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据\n\t *\n\t * <p>\n\t * 见：https://blog.csdn.net/OrangeJack/article/details/82913804\n\t *\n\t * @param data      数据\n\t * @param blockSize 块大小\n\t * @return 填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据\n\t * @since 4.6.7\n\t */\n\tprivate byte[] paddingDataWithZero(byte[] data, int blockSize) {\n\t\tif (this.isZeroPadding) {\n\t\t\tfinal int length = data.length;\n\t\t\t// 按照块拆分后的数据中多余的数据\n\t\t\tfinal int remainLength = length % blockSize;\n\t\t\tif (remainLength > 0) {\n\t\t\t\t// 新长度为blockSize的整数倍，多余部分填充0\n\t\t\t\treturn ArrayUtil.resize(data, length + blockSize - remainLength);\n\t\t\t}\n\t\t}\n\t\treturn data;\n\t}\n\n\t/**\n\t * 数据按照blockSize去除填充部分，用于解密\n\t *\n\t * <p>\n\t * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据\n\t *\n\t * @param data      数据\n\t * @param blockSize 块大小，必须大于0\n\t * @return 去除填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据\n\t * @since 4.6.7\n\t */\n\tprivate byte[] removePadding(byte[] data, int blockSize) {\n\t\tif (this.isZeroPadding && blockSize > 0) {\n\t\t\tfinal int length = data.length;\n\t\t\tfinal int remainLength = length % blockSize;\n\t\t\tif (remainLength == 0) {\n\t\t\t\t// 解码后的数据正好是块大小的整数倍，说明可能存在补0的情况，去掉末尾所有的0\n\t\t\t\tint i = length - 1;\n\t\t\t\twhile (i >= 0 && 0 == data[i]) {\n\t\t\t\t\ti--;\n\t\t\t\t}\n\t\t\t\treturn ArrayUtil.resize(data, i + 1);\n\t\t\t}\n\t\t}\n\t\treturn data;\n\t}\n\n\t/**\n\t * 拷贝解密后的流\n\t *\n\t * @param in        {@link CipherInputStream}\n\t * @param out       输出流\n\t * @param blockSize 块大小\n\t * @throws IOException IO异常\n\t */\n\tprivate static void copyForZeroPadding(CipherInputStream in, OutputStream out, int blockSize) throws IOException {\n\t\tint n = 1;\n\t\tif (IoUtil.DEFAULT_BUFFER_SIZE > blockSize) {\n\t\t\tn = Math.max(n, IoUtil.DEFAULT_BUFFER_SIZE / blockSize);\n\t\t}\n\t\t// 此处缓存buffer使用blockSize的整数倍，方便读取时可以正好将补位的0读在一个buffer中\n\t\tfinal int bufSize = blockSize * n;\n\t\tfinal byte[] preBuffer = new byte[bufSize];\n\t\tfinal byte[] buffer = new byte[bufSize];\n\n\t\tboolean isFirst = true;\n\t\tint preReadSize = 0;\n\t\tfor (int readSize; (readSize = in.read(buffer)) != IoUtil.EOF; ) {\n\t\t\tif (isFirst) {\n\t\t\t\tisFirst = false;\n\t\t\t} else {\n\t\t\t\t// 将前一批数据写出\n\t\t\t\tout.write(preBuffer, 0, preReadSize);\n\t\t\t}\n\t\t\tArrayUtil.copy(buffer, preBuffer, readSize);\n\t\t\tpreReadSize = readSize;\n\t\t}\n\t\t// 去掉末尾所有的补位0\n\t\tint i = preReadSize - 1;\n\t\twhile (i >= 0 && 0 == preBuffer[i]) {\n\t\t\ti--;\n\t\t}\n\t\tout.write(preBuffer, 0, i + 1);\n\t\tout.flush();\n\t}\n\t// --------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricDecryptor.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.SecureUtil;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 对称解密器接口，提供：\n * <ul>\n *     <li>从bytes解密</li>\n *     <li>从Hex(16进制)解密</li>\n *     <li>从Base64解密</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.12\n */\npublic interface SymmetricDecryptor {\n\t/**\n\t * 解密\n\t *\n\t * @param bytes 被解密的bytes\n\t * @return 解密后的bytes\n\t */\n\tbyte[] decrypt(byte[] bytes);\n\n\t/**\n\t * 解密，针对大数据量，结束后不关闭流\n\t *\n\t * @param data    加密的字符串\n\t * @param out     输出流，可以是文件或网络位置\n\t * @param isClose 是否关闭流，包括输入和输出流\n\t * @throws IORuntimeException IO异常\n\t */\n\tvoid decrypt(InputStream data, OutputStream out, boolean isClose);\n\n\t/**\n\t * 解密为字符串\n\t *\n\t * @param bytes   被解密的bytes\n\t * @param charset 解密后的charset\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(byte[] bytes, Charset charset) {\n\t\treturn StrUtil.str(decrypt(bytes), charset);\n\t}\n\n\t/**\n\t * 解密为字符串，默认UTF-8编码\n\t *\n\t * @param bytes 被解密的bytes\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(byte[] bytes) {\n\t\treturn decryptStr(bytes, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 解密Hex（16进制）或Base64表示的字符串\n\t *\n\t * @param data 被解密的String，必须为16进制字符串或Base64表示形式\n\t * @return 解密后的bytes\n\t */\n\tdefault byte[] decrypt(String data) {\n\t\treturn decrypt(SecureUtil.decode(data));\n\t}\n\n\t/**\n\t * 解密Hex（16进制）或Base64表示的字符串\n\t *\n\t * @param data    被解密的String\n\t * @param charset 解密后的charset\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(String data, Charset charset) {\n\t\treturn StrUtil.str(decrypt(data), charset);\n\t}\n\n\t/**\n\t * 解密Hex（16进制）或Base64表示的字符串，默认UTF-8编码\n\t *\n\t * @param data 被解密的String\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(String data) {\n\t\treturn decryptStr(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 解密，会关闭流\n\t *\n\t * @param data 被解密的bytes\n\t * @return 解密后的bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tdefault byte[] decrypt(InputStream data) throws IORuntimeException {\n\t\treturn decrypt(IoUtil.readBytes(data));\n\t}\n\n\t/**\n\t * 解密，不会关闭流\n\t *\n\t * @param data    被解密的InputStream\n\t * @param charset 解密后的charset\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(InputStream data, Charset charset) {\n\t\treturn StrUtil.str(decrypt(data), charset);\n\t}\n\n\t/**\n\t * 解密\n\t *\n\t * @param data 被解密的InputStream\n\t * @return 解密后的String\n\t */\n\tdefault String decryptStr(InputStream data) {\n\t\treturn decryptStr(data, CharsetUtil.CHARSET_UTF_8);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricEncryptor.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 对称加密器接口，提供：\n * <ul>\n *     <li>加密为bytes</li>\n *     <li>加密为Hex(16进制)</li>\n *     <li>加密为Base64</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.12\n */\npublic interface SymmetricEncryptor {\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 被加密的bytes\n\t * @return 加密后的bytes\n\t */\n\tbyte[] encrypt(byte[] data);\n\n\t/**\n\t * 加密，针对大数据量，可选结束后是否关闭流\n\t *\n\t * @param data    被加密的字符串\n\t * @param out     输出流，可以是文件或网络位置\n\t * @param isClose 是否关闭流\n\t * @throws IORuntimeException IO异常\n\t */\n\tvoid encrypt(InputStream data, OutputStream out, boolean isClose);\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据\n\t * @return 加密后的Hex\n\t */\n\tdefault String encryptHex(byte[] data) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据\n\t * @return 加密后的Base64\n\t */\n\tdefault String encryptBase64(byte[] data) {\n\t\treturn Base64.encode(encrypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data, String charset) {\n\t\treturn encrypt(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data, Charset charset) {\n\t\treturn encrypt(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Hex\n\t */\n\tdefault String encryptHex(String data, String charset) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, charset));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Hex\n\t */\n\tdefault String encryptHex(String data, Charset charset) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data, charset));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Base64\n\t */\n\tdefault String encryptBase64(String data, String charset) {\n\t\treturn Base64.encode(encrypt(data, charset));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data    被加密的字符串\n\t * @param charset 编码\n\t * @return 加密后的Base64\n\t * @since 4.5.12\n\t */\n\tdefault String encryptBase64(String data, Charset charset) {\n\t\treturn Base64.encode(encrypt(data, charset));\n\t}\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的bytes\n\t */\n\tdefault byte[] encrypt(String data) {\n\t\treturn encrypt(StrUtil.bytes(data, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Hex\n\t */\n\tdefault String encryptHex(String data) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data));\n\t}\n\n\t/**\n\t * 加密，使用UTF-8编码\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Base64\n\t */\n\tdefault String encryptBase64(String data) {\n\t\treturn Base64.encode(encrypt(data));\n\t}\n\n\t/**\n\t * 加密，加密后关闭流\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的bytes\n\t * @throws IORuntimeException IO异常\n\t */\n\tdefault byte[] encrypt(InputStream data) throws IORuntimeException {\n\t\treturn encrypt(IoUtil.readBytes(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Hex\n\t */\n\tdefault String encryptHex(InputStream data) {\n\t\treturn HexUtil.encodeHexStr(encrypt(data));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 被加密的字符串\n\t * @return 加密后的Base64\n\t */\n\tdefault String encryptBase64(InputStream data) {\n\t\treturn Base64.encode(encrypt(data));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/Vigenere.java",
    "content": "package cn.hutool.crypto.symmetric;\n\n/**\n * 维吉尼亚密码实现。<br>\n * 人们在恺撒移位密码的基础上扩展出多表密码，称为维吉尼亚密码。<br>\n * 算法实现来自：https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption\n * \n * @author looly,zhaorenjie110\n * @since 4.4.1\n */\npublic class Vigenere {\n\n\t/**\n\t * 加密\n\t * \n\t * @param data 数据\n\t * @param cipherKey 密钥\n\t * @return 密文\n\t */\n\tpublic static String encrypt(CharSequence data, CharSequence cipherKey) {\n\t\tfinal int dataLen = data.length();\n\t\tfinal int cipherKeyLen = cipherKey.length();\n\n\t\tfinal char[] cipherArray = new char[dataLen];\n\t\tfor (int i = 0; i < dataLen / cipherKeyLen + 1; i++) {\n\t\t\tfor (int t = 0; t < cipherKeyLen; t++) {\n\t\t\t\tif (t + i * cipherKeyLen < dataLen) {\n\t\t\t\t\tfinal char dataChar = data.charAt(t + i * cipherKeyLen);\n\t\t\t\t\tfinal char cipherKeyChar = cipherKey.charAt(t);\n\t\t\t\t\tcipherArray[t + i * cipherKeyLen] = (char) ((dataChar + cipherKeyChar - 64) % 95 + 32);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn String.valueOf(cipherArray);\n\t}\n\n\t/**\n\t * 解密\n\t * \n\t * @param data 密文\n\t * @param cipherKey 密钥\n\t * @return 明文\n\t */\n\tpublic static String decrypt(CharSequence data, CharSequence cipherKey) {\n\t\tfinal int dataLen = data.length();\n\t\tfinal int cipherKeyLen = cipherKey.length();\n\n\t\tfinal char[] clearArray = new char[dataLen];\n\t\tfor (int i = 0; i < dataLen; i++) {\n\t\t\tfor (int t = 0; t < cipherKeyLen; t++) {\n\t\t\t\tif (t + i * cipherKeyLen < dataLen) {\n\t\t\t\t\tfinal char dataChar = data.charAt(t + i * cipherKeyLen);\n\t\t\t\t\tfinal char cipherKeyChar = cipherKey.charAt(t);\n\t\t\t\t\tif (dataChar - cipherKeyChar >= 0) {\n\t\t\t\t\t\tclearArray[t + i * cipherKeyLen] = (char) ((dataChar - cipherKeyChar) % 95 + 32);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclearArray[t + i * cipherKeyLen] = (char) ((dataChar - cipherKeyChar + 95) % 95 + 32);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn String.valueOf(clearArray);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/XXTEA.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Serializable;\n\n/**\n * XXTEA（Corrected Block Tiny Encryption Algorithm）算法实现<br>\n * 来自：https://github.com/xxtea/xxtea-java\n *\n * @author Ma Bingyao\n */\npublic class XXTEA implements SymmetricEncryptor, SymmetricDecryptor, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate static final int DELTA = 0x9E3779B9;\n\n\tprivate final byte[] key;\n\n\t/**\n\t * 构造\n\t *\n\t * @param key 密钥，16位\n\t */\n\tpublic XXTEA(byte[] key) {\n\t\tthis.key = key;\n\t}\n\n\t@Override\n\tpublic byte[] encrypt(byte[] data) {\n\t\tif (data.length == 0) {\n\t\t\treturn data;\n\t\t}\n\t\treturn toByteArray(encrypt(\n\t\t\t\ttoIntArray(data, true),\n\t\t\t\ttoIntArray(fixKey(key), false)), false);\n\t}\n\n\t@Override\n\tpublic void encrypt(InputStream data, OutputStream out, boolean isClose) {\n\t\tIoUtil.write(out, isClose, encrypt(IoUtil.readBytes(data)));\n\t}\n\n\t@Override\n\tpublic byte[] decrypt(byte[] data) {\n\t\tif (data.length == 0) {\n\t\t\treturn data;\n\t\t}\n\t\treturn toByteArray(decrypt(\n\t\t\t\ttoIntArray(data, false),\n\t\t\t\ttoIntArray(fixKey(key), false)), true);\n\t}\n\n\t@Override\n\tpublic void decrypt(InputStream data, OutputStream out, boolean isClose) {\n\t\tIoUtil.write(out, isClose, decrypt(IoUtil.readBytes(data)));\n\t}\n\n\t//region Private Method\n\tprivate static int[] encrypt(int[] v, int[] k) {\n\t\tint n = v.length - 1;\n\n\t\tif (n < 1) {\n\t\t\treturn v;\n\t\t}\n\t\tint p, q = 6 + 52 / (n + 1);\n\t\tint z = v[n], y, sum = 0, e;\n\n\t\twhile (q-- > 0) {\n\t\t\tsum = sum + DELTA;\n\t\t\te = sum >>> 2 & 3;\n\t\t\tfor (p = 0; p < n; p++) {\n\t\t\t\ty = v[p + 1];\n\t\t\t\tz = v[p] += mx(sum, y, z, p, e, k);\n\t\t\t}\n\t\t\ty = v[0];\n\t\t\tz = v[n] += mx(sum, y, z, p, e, k);\n\t\t}\n\t\treturn v;\n\t}\n\n\tprivate static int[] decrypt(int[] v, int[] k) {\n\t\tint n = v.length - 1;\n\n\t\tif (n < 1) {\n\t\t\treturn v;\n\t\t}\n\t\tint p, q = 6 + 52 / (n + 1);\n\t\tint z, y = v[0], sum = q * DELTA, e;\n\n\t\twhile (sum != 0) {\n\t\t\te = sum >>> 2 & 3;\n\t\t\tfor (p = n; p > 0; p--) {\n\t\t\t\tz = v[p - 1];\n\t\t\t\ty = v[p] -= mx(sum, y, z, p, e, k);\n\t\t\t}\n\t\t\tz = v[n];\n\t\t\ty = v[0] -= mx(sum, y, z, p, e, k);\n\t\t\tsum = sum - DELTA;\n\t\t}\n\t\treturn v;\n\t}\n\n\tprivate static int mx(int sum, int y, int z, int p, int e, int[] k) {\n\t\treturn (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);\n\t}\n\n\tprivate static byte[] fixKey(byte[] key) {\n\t\tif (key.length == 16) {\n\t\t\treturn key;\n\t\t}\n\t\tbyte[] fixedkey = new byte[16];\n\t\tSystem.arraycopy(key, 0, fixedkey, 0, Math.min(key.length, 16));\n\t\treturn fixedkey;\n\t}\n\n\tprivate static int[] toIntArray(byte[] data, boolean includeLength) {\n\t\tint n = (((data.length & 3) == 0)\n\t\t\t\t? (data.length >>> 2)\n\t\t\t\t: ((data.length >>> 2) + 1));\n\t\tint[] result;\n\n\t\tif (includeLength) {\n\t\t\tresult = new int[n + 1];\n\t\t\tresult[n] = data.length;\n\t\t} else {\n\t\t\tresult = new int[n];\n\t\t}\n\t\tn = data.length;\n\t\tfor (int i = 0; i < n; ++i) {\n\t\t\tresult[i >>> 2] |= (0x000000ff & data[i]) << ((i & 3) << 3);\n\t\t}\n\t\treturn result;\n\t}\n\n\tprivate static byte[] toByteArray(int[] data, boolean includeLength) {\n\t\tint n = data.length << 2;\n\n\t\tif (includeLength) {\n\t\t\tint m = data[data.length - 1];\n\t\t\tn -= 4;\n\t\t\tif ((m < n - 3) || (m > n)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tn = m;\n\t\t}\n\t\tbyte[] result = new byte[n];\n\n\t\tfor (int i = 0; i < n; ++i) {\n\t\t\tresult[i] = (byte) (data[i >>> 2] >>> ((i & 3) << 3));\n\t\t}\n\t\treturn result;\n\t}\n\t//endregion\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/ZUC.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.KeyUtil;\n\nimport javax.crypto.spec.IvParameterSpec;\n\n/**\n * 祖冲之算法集（ZUC算法）实现，基于BouncyCastle实现。\n *\n * @author looly\n * @since 5.7.12\n */\npublic class ZUC extends SymmetricCrypto {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 生成ZUC算法密钥\n\t *\n\t * @param algorithm ZUC算法\n\t * @return 密钥\n\t *\n\t * @see KeyUtil#generateKey(String)\n\t */\n\tpublic static byte[] generateKey(ZUCAlgorithm algorithm) {\n\t\treturn KeyUtil.generateKey(algorithm.value).getEncoded();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm ZUC算法枚举，包括128位和256位两种\n\t * @param key       密钥\n\t * @param iv        加盐，128位加盐是16bytes，256位是25bytes，{@code null}是随机加盐\n\t */\n\tpublic ZUC(ZUCAlgorithm algorithm, byte[] key, byte[] iv) {\n\t\tsuper(algorithm.value,\n\t\t\t\tKeyUtil.generateKey(algorithm.value, key),\n\t\t\t\tgenerateIvParam(algorithm, iv));\n\t}\n\n\t/**\n\t * ZUC类型，包括128位和256位\n\t *\n\t * @author looly\n\t */\n\tpublic enum ZUCAlgorithm {\n\t\tZUC_128(\"ZUC-128\"),\n\t\tZUC_256(\"ZUC-256\");\n\n\t\tprivate final String value;\n\n\t\t/**\n\t\t * 构造\n\t\t *\n\t\t * @param value 算法的字符串表示，区分大小写\n\t\t */\n\t\tZUCAlgorithm(String value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\t/**\n\t\t * 获得算法的字符串表示形式\n\t\t *\n\t\t * @return 算法字符串\n\t\t */\n\t\tpublic String getValue() {\n\t\t\treturn this.value;\n\t\t}\n\t}\n\n\t/**\n\t * 生成加盐参数\n\t *\n\t * @param algorithm ZUC算法\n\t * @param iv 加盐，128位加盐是16bytes，256位是25bytes，{@code null}是随机加盐\n\t * @return {@link IvParameterSpec}\n\t */\n\tprivate static IvParameterSpec generateIvParam(ZUCAlgorithm algorithm, byte[] iv){\n\t\tif(null == iv){\n\t\t\tswitch (algorithm){\n\t\t\t\tcase ZUC_128:\n\t\t\t\t\tiv = RandomUtil.randomBytes(16);\n\t\t\t\t\tbreak;\n\t\t\t\tcase ZUC_256:\n\t\t\t\t\tiv = RandomUtil.randomBytes(25);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn new IvParameterSpec(iv);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/fpe/FPE.java",
    "content": "package cn.hutool.crypto.symmetric.fpe;\n\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.symmetric.AES;\nimport org.bouncycastle.crypto.AlphabetMapper;\nimport org.bouncycastle.jcajce.spec.FPEParameterSpec;\n\nimport java.io.Serializable;\n\n/**\n * FPE(Format Preserving Encryption)实现，支持FF1和FF3-1模式。<br>\n * 相关介绍见：https://anquan.baidu.com/article/193\n *\n * <p>\n * FPE是一种格式保持与明文相同的加密方式，通常用于数据脱敏中，因为它需要保持明密文的格式相同，\n * 例如社保号经过加密之后并不是固定长度的杂文，而是相同格式、打乱的号码，依然是社保号的格式。\n * </p>\n * <p>\n * FPE算法可以保证：\n *\n * <ul>\n *     <li>数据长度不变。加密前长度是N，加密后长度仍然是N</li>\n *     <li>数据类型不变，加密前是数字类型，加密后仍然是数字类型</li>\n *     <li>加密过程可逆，加密后的数据可以通过密钥解密还原原始数据</li>\n * </ul>\n *\n * 此类基于BouncyCastle实现。\n *\n * @author looly\n * @since 5.7.12\n */\npublic class FPE implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 映射字符表，规定了明文和密文的字符范围\n\tprivate final AES aes;\n\tprivate final AlphabetMapper mapper;\n\n\t/**\n\t * 构造，使用空的Tweak\n\t *\n\t * @param mode   FPE模式枚举，可选FF1或FF3-1\n\t * @param key    密钥，{@code null}表示随机密钥，长度必须是16bit、24bit或32bit\n\t * @param mapper Alphabet字典映射，被加密的字符范围和这个映射必须一致，例如手机号、银行卡号等字段可以采用数字字母字典表\n\t */\n\tpublic FPE(FPEMode mode, byte[] key, AlphabetMapper mapper) {\n\t\tthis(mode, key, mapper, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mode   FPE模式枚举，可选FF1或FF3-1\n\t * @param key    密钥，{@code null}表示随机密钥，长度必须是16bit、24bit或32bit\n\t * @param mapper Alphabet字典映射，被加密的字符范围和这个映射必须一致，例如手机号、银行卡号等字段可以采用数字字母字典表\n\t * @param tweak  Tweak是为了解决因局部加密而导致结果冲突问题，通常情况下将数据的不可变部分作为Tweak，{@code null}使用默认长度全是0的bytes\n\t */\n\tpublic FPE(FPEMode mode, byte[] key, AlphabetMapper mapper, byte[] tweak) {\n\t\tif (null == mode) {\n\t\t\tmode = FPEMode.FF1;\n\t\t}\n\n\t\tif (null == tweak) {\n\t\t\tswitch (mode) {\n\t\t\t\tcase FF1:\n\t\t\t\t\ttweak = new byte[0];\n\t\t\t\t\tbreak;\n\t\t\t\tcase FF3_1:\n\t\t\t\t\t// FF3-1要求必须为56 bits\n\t\t\t\t\ttweak = new byte[7];\n\t\t\t}\n\t\t}\n\t\tthis.aes = new AES(mode.value, Padding.NoPadding.name(),\n\t\t\t\tKeyUtil.generateKey(mode.value, key),\n\t\t\t\tnew FPEParameterSpec(mapper.getRadix(), tweak));\n\t\tthis.mapper = mapper;\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据，数据必须在构造传入的{@link AlphabetMapper}中定义的范围\n\t * @return 密文结果\n\t */\n\tpublic String encrypt(String data) {\n\t\tif (null == data) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new String(encrypt(data.toCharArray()));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 数据，数据必须在构造传入的{@link AlphabetMapper}中定义的范围\n\t * @return 密文结果\n\t */\n\tpublic char[] encrypt(char[] data) {\n\t\tif (null == data) {\n\t\t\treturn null;\n\t\t}\n\t\t// 通过 mapper 将密文输出处理为原始格式\n\t\treturn mapper.convertToChars(aes.encrypt(mapper.convertToIndexes(data)));\n\t}\n\n\t/**\n\t * 解密\n\t *\n\t * @param data 密文数据，数据必须在构造传入的{@link AlphabetMapper}中定义的范围\n\t * @return 明文结果\n\t */\n\tpublic String decrypt(String data) {\n\t\tif (null == data) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new String(decrypt(data.toCharArray()));\n\t}\n\n\t/**\n\t * 加密\n\t *\n\t * @param data 密文数据，数据必须在构造传入的{@link AlphabetMapper}中定义的范围\n\t * @return 明文结果\n\t */\n\tpublic char[] decrypt(char[] data) {\n\t\tif (null == data) {\n\t\t\treturn null;\n\t\t}\n\t\t// 通过 mapper 将密文输出处理为原始格式\n\t\treturn mapper.convertToChars(aes.decrypt(mapper.convertToIndexes(data)));\n\t}\n\n\t/**\n\t * FPE模式<br>\n\t * FPE包括两种模式：FF1和FF3（FF2弃用），核心均为Feistel网络结构。\n\t *\n\t * @author looly\n\t */\n\tpublic enum FPEMode {\n\t\t/**\n\t\t * FF1模式\n\t\t */\n\t\tFF1(\"FF1\"),\n\t\t/**\n\t\t * FF3-1 模式\n\t\t */\n\t\tFF3_1(\"FF3-1\");\n\n\t\tprivate final String value;\n\n\t\tFPEMode(String name) {\n\t\t\tthis.value = name;\n\t\t}\n\n\t\t/**\n\t\t * 获取模式名\n\t\t *\n\t\t * @return 模式名\n\t\t */\n\t\tpublic String getValue() {\n\t\t\treturn value;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/package-info.java",
    "content": "/**\n * 对称加密算法实现，包括AES、DES、DESede等\n * \n * @author looly\n *\n */\npackage cn.hutool.crypto.symmetric;"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/BCUtilTest.java",
    "content": "package cn.hutool.crypto;\n\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.crypto.params.ECPublicKeyParameters;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BCUtilTest {\n\n\t/**\n\t * 密钥生成来自：https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg\n\t */\n\t@Test\n\tpublic void createECPublicKeyParametersTest() {\n\t\tString x = \"706AD9DAA3E5CEAC3DA59F583429E8043BAFC576BE10092C4EA4D8E19846CA62\";\n\t\tString y = \"F7E938B02EED7280277493B8556E5B01CB436E018A562DFDC53342BF41FDF728\";\n\n\t\tfinal ECPublicKeyParameters keyParameters = BCUtil.toSm2Params(x, y);\n\t\tassertNotNull(keyParameters);\n\t}\n\n\t@Test\n\tpublic void createECPrivateKeyParametersTest() {\n\t\tString privateKeyHex = \"5F6CA5BB044C40ED2355F0372BF72A5B3AE6943712F9FDB7C1FFBAECC06F3829\";\n\n\t\tfinal ECPrivateKeyParameters keyParameters = BCUtil.toSm2Params(privateKeyHex);\n\t\tassertNotNull(keyParameters);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/Issue3512Test.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.crypto.asymmetric.SignAlgorithm;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3512Test {\n\t@Test\n\tpublic void signTest() {\n\t\tSecureUtil.sign(SignAlgorithm.SHA256withRSA);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/KeyUtilTest.java",
    "content": "package cn.hutool.crypto;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class KeyUtilTest {\n\n\t/**\n\t * 测试关闭BouncyCastle支持时是否会正常抛出异常，即关闭是否有效\n\t */\n\t@Test\n\t@Disabled\n\tpublic void generateKeyPairTest() {\n\t\tassertThrows(CryptoException.class, () -> {\n\t\t\tGlobalBouncyCastleProvider.setUseBouncyCastle(false);\n\t\t\tKeyPair pair = KeyUtil.generateKeyPair(\"SM2\");\n\t\t\tassertNotNull(pair);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void getRSAPublicKeyTest(){\n\t\tfinal KeyPair keyPair = KeyUtil.generateKeyPair(\"RSA\");\n\t\tfinal PrivateKey aPrivate = keyPair.getPrivate();\n\t\tfinal PublicKey rsaPublicKey = KeyUtil.getRSAPublicKey(aPrivate);\n\t\tassertEquals(rsaPublicKey, keyPair.getPublic());\n\t}\n\n\t/**\n\t * 测试EC和ECIES算法生成的KEY是一致的\n\t */\n\t@Test\n\tpublic void generateECIESKeyTest(){\n\t\tfinal KeyPair ecies = KeyUtil.generateKeyPair(\"ECIES\");\n\t\tassertNotNull(ecies.getPrivate());\n\t\tassertNotNull(ecies.getPublic());\n\n\t\tbyte[] privateKeyBytes = ecies.getPrivate().getEncoded();\n\n\t\tfinal PrivateKey privateKey = KeyUtil.generatePrivateKey(\"EC\", privateKeyBytes);\n\t\tassertEquals(ecies.getPrivate(), privateKey);\n\t}\n\n\t@Test\n\tpublic void generateDHTest(){\n\t\tfinal KeyPair dh = KeyUtil.generateKeyPair(\"DH\");\n\t\tassertNotNull(dh.getPrivate());\n\t\tassertNotNull(dh.getPublic());\n\n\t\tbyte[] privateKeyBytes = dh.getPrivate().getEncoded();\n\n\t\tfinal PrivateKey privateKey = KeyUtil.generatePrivateKey(\"DH\", privateKeyBytes);\n\t\tassertEquals(dh.getPrivate(), privateKey);\n\t}\n\n\t@Test\n\tpublic void generateSm4KeyTest(){\n\t\t// https://github.com/chinabugotech/hutool/issues/2150\n\t\tassertEquals(16, KeyUtil.generateKey(\"sm4\").getEncoded().length);\n\t\tassertEquals(32, KeyUtil.generateKey(\"sm4\", 256).getEncoded().length);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/OpensslKeyUtilTest.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.asymmetric.SM2;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class OpensslKeyUtilTest {\n\t@Test\n\tpublic void verifyPemUtilReadKey() {\n\t\t// 公钥\n\t\t// PKCS#10 文件读取公钥\n\t\tfinal PublicKey csrPublicKey = (PublicKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_certificate_request.csr\"), null);\n\n\t\t// 证书读取公钥\n\t\tfinal PublicKey certPublicKey = (PublicKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_certificate.cer\"), null);\n\n\t\t// PEM 公钥\n\t\tfinal PublicKey plainPublicKey = (PublicKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_public_key.pem\"), null);\n\n\t\t// 私钥\n\t\t// 加密的 PEM 私钥\n\t\tfinal PrivateKey encPrivateKey = (PrivateKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_encrypted_private_key.key\"), \"123456\".toCharArray());\n\n\t\t// PKCS#8 私钥\n\t\tfinal PrivateKey pkcs8PrivateKey = (PrivateKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_pkcs8_private_key.key\"), null);\n\n\t\t// SEC 1 私钥\n\t\tfinal PrivateKey sec1PrivateKey = (PrivateKey) OpensslKeyUtil.readPemKey(ResourceUtil.getStream(\"test_ec_sec1_private_key.pem\"), null);\n\n\t\t// 组装还原后的公钥和私钥列表\n\t\tfinal List<PublicKey> publicKeyList = Arrays.asList(csrPublicKey, certPublicKey, plainPublicKey);\n\t\tfinal List<PrivateKey> privateKeyList = Arrays.asList(encPrivateKey, pkcs8PrivateKey, sec1PrivateKey);\n\n\t\t// 做笛卡尔积循环验证\n\t\tfor (final PrivateKey privateKeyItem : privateKeyList) {\n\t\t\tfor (final PublicKey publicKeyItem : publicKeyList) {\n\t\t\t\t// 校验公私钥\n\t\t\t\tfinal SM2 genSm2 = new SM2(privateKeyItem, publicKeyItem);\n\t\t\t\tgenSm2.usePlainEncoding();\n\n\t\t\t\tfinal String content = \"我是Hanley.\";\n\t\t\t\tfinal byte[] sign = genSm2.sign(StrUtil.utf8Bytes(content));\n\t\t\t\tfinal boolean verify = genSm2.verify(StrUtil.utf8Bytes(content), sign);\n\t\t\t\tassertTrue(verify);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/PemUtilTest.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.asymmetric.KeyType;\nimport cn.hutool.crypto.asymmetric.RSA;\nimport cn.hutool.crypto.asymmetric.SM2;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\npublic class PemUtilTest {\n\n\t@Test\n\tpublic void readPrivateKeyTest() {\n\t\tfinal PrivateKey privateKey = PemUtil.readPemPrivateKey(ResourceUtil.getStream(\"test_private_key.pem\"));\n\t\tassertNotNull(privateKey);\n\t}\n\n\t@Test\n\tpublic void readPublicKeyTest() {\n\t\tfinal PublicKey publicKey = PemUtil.readPemPublicKey(ResourceUtil.getStream(\"test_public_key.csr\"));\n\t\tassertNotNull(publicKey);\n\t}\n\n\t@Test\n\tpublic void readPemKeyTest() {\n\t\tfinal PublicKey publicKey = (PublicKey) PemUtil.readPemKey(ResourceUtil.getStream(\"test_public_key.csr\"));\n\t\tassertNotNull(publicKey);\n\t}\n\n\t@Test\n\tpublic void validateKey() {\n\t\tfinal PrivateKey privateKey = PemUtil.readPemPrivateKey(ResourceUtil.getStream(\"test_private_key.pem\"));\n\t\tfinal PublicKey publicKey = PemUtil.readPemPublicKey(ResourceUtil.getStream(\"test_public_key.csr\"));\n\n\t\tfinal RSA rsa = new RSA(privateKey, publicKey);\n\t\tfinal String str = \"你好，Hutool\";//测试字符串\n\n\t\tfinal String encryptStr = rsa.encryptBase64(str, KeyType.PublicKey);\n\t\tfinal String decryptStr = rsa.decryptStr(encryptStr, KeyType.PrivateKey);\n\t\tassertEquals(str, decryptStr);\n\t}\n\n\t@Test\n\tpublic void readECPrivateKeyTest() {\n\t\tfinal PrivateKey privateKey = PemUtil.readPemPrivateKey(ResourceUtil.getStream(\"test_ec_sec1_private_key.pem\"));\n\t\tfinal SM2 sm2 = new SM2(privateKey, null);\n\t\tsm2.usePlainEncoding();\n\n\t\t//需要签名的明文,得到明文对应的字节数组\n\t\tfinal byte[] dataBytes = \"我是一段测试aaaa\".getBytes(StandardCharsets.UTF_8);\n\n\t\tfinal byte[] sign = sm2.sign(dataBytes, null);\n\t\t// 64位签名\n\t\tassertEquals(64, sign.length);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readECPrivateKeyTest2() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I37Z75\n\t\tfinal byte[] d = PemUtil.readPem(FileUtil.getInputStream(\"d:/test/keys/priv.key\"));\n\t\tfinal byte[] publicKey = PemUtil.readPem(FileUtil.getInputStream(\"d:/test/keys/pub.key\"));\n\n\t\tfinal SM2 sm2 = new SM2(d, publicKey);\n\t\tsm2.usePlainEncoding();\n\n\t\tfinal String content = \"我是Hanley.\";\n\t\tfinal byte[] sign = sm2.sign(StrUtil.utf8Bytes(content));\n\t\tfinal boolean verify = sm2.verify(StrUtil.utf8Bytes(content), sign);\n\t\tassertTrue(verify);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/SecureUtilTest.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;\nimport cn.hutool.crypto.asymmetric.Sign;\nimport cn.hutool.crypto.asymmetric.SignAlgorithm;\nimport cn.hutool.crypto.digest.*;\nimport cn.hutool.crypto.symmetric.AES;\nimport cn.hutool.crypto.symmetric.DES;\nimport org.junit.jupiter.api.Test;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.security.KeyPair;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * SecureUtil类单元测试\n */\npublic class SecureUtilTest {\n\n\tprivate static final String TEST_CONTENT = \"test中文\";\n\tprivate static final String TEST_DATA = \"test data\";\n\tprivate static final byte[] TEST_KEY = RandomUtil.randomBytes(16);\n\n\t@Test\n\tpublic void getAlgorithmAfterWithTest() {\n\t\tString algorithm = SecureUtil.getAlgorithmAfterWith(\"SHA256withRSA\");\n\t\tassertEquals(\"RSA\", algorithm);\n\n\t\talgorithm = SecureUtil.getAlgorithmAfterWith(\"NONEwithECDSA\");\n\t\tassertEquals(\"EC\", algorithm);\n\t}\n\n\t@Test\n\tpublic void generateAlgorithmTest() {\n\t\tString algorithm = SecureUtil.generateAlgorithm(AsymmetricAlgorithm.RSA, DigestAlgorithm.SHA256);\n\t\tassertEquals(\"SHA256withRSA\", algorithm);\n\n\t\talgorithm = SecureUtil.generateAlgorithm(AsymmetricAlgorithm.RSA, null);\n\t\tassertEquals(\"NONEwithRSA\", algorithm);\n\t}\n\n\t@Test\n\tpublic void aesTest() {\n\t\tAES aes = SecureUtil.aes();\n\t\tassertNotNull(aes);\n\n\t\tAES aesWithKey = SecureUtil.aes(TEST_KEY);\n\t\tassertNotNull(aesWithKey);\n\n\t\t// 测试加密解密\n\t\tString encrypted = aesWithKey.encryptBase64(TEST_CONTENT);\n\t\tString decrypted = aesWithKey.decryptStr(encrypted, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(TEST_CONTENT, decrypted);\n\t}\n\n\t@Test\n\tpublic void desTest() {\n\t\tDES des = SecureUtil.des();\n\t\tassertNotNull(des);\n\n\t\tDES desWithKey = SecureUtil.des(RandomUtil.randomBytes(8));\n\t\tassertNotNull(desWithKey);\n\n\t\t// 测试加密解密\n\t\tString encrypted = desWithKey.encryptBase64(TEST_CONTENT);\n\t\tString decrypted = desWithKey.decryptStr(encrypted, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(TEST_CONTENT, decrypted);\n\t}\n\n\t@Test\n\tpublic void md5Test() {\n\t\t// 测试MD5对象\n\t\tMD5 md5 = SecureUtil.md5();\n\t\tassertNotNull(md5);\n\n\t\t// 测试字符串MD5\n\t\tString md5Str = SecureUtil.md5(TEST_DATA);\n\t\tassertNotNull(md5Str);\n\t\tassertEquals(32, md5Str.length()); // MD5是32位十六进制字符串\n\n\t\t// 测试文件MD5\n\t\ttry {\n\t\t\tFile tempFile = File.createTempFile(\"test\", \".txt\");\n\t\t\tFileUtil.writeString(TEST_DATA, tempFile, CharsetUtil.CHARSET_UTF_8);\n\t\t\tString fileMd5 = SecureUtil.md5(tempFile);\n\t\t\tassertNotNull(fileMd5);\n\t\t\tassertEquals(32, fileMd5.length());\n\t\t} catch (Exception e) {\n\t\t\tfail(\"File MD5 test failed: \" + e.getMessage());\n\t\t}\n\n\t\t// 测试InputStream MD5\n\t\tInputStream is = new ByteArrayInputStream(TEST_DATA.getBytes());\n\t\tString streamMd5 = SecureUtil.md5(is);\n\t\tassertNotNull(streamMd5);\n\t\tassertEquals(32, streamMd5.length());\n\t}\n\n\t@Test\n\tpublic void sha1Test() {\n\t\t// 测试SHA1对象\n\t\tDigester sha1 = SecureUtil.sha1();\n\t\tassertNotNull(sha1);\n\n\t\t// 测试字符串SHA1\n\t\tString sha1Str = SecureUtil.sha1(TEST_DATA);\n\t\tassertNotNull(sha1Str);\n\t\tassertEquals(40, sha1Str.length()); // SHA1是40位十六进制字符串\n\n\t\t// 测试InputStream SHA1\n\t\tInputStream is = new ByteArrayInputStream(TEST_DATA.getBytes());\n\t\tString streamSha1 = SecureUtil.sha1(is);\n\t\tassertNotNull(streamSha1);\n\t\tassertEquals(40, streamSha1.length());\n\t}\n\n\t@Test\n\tpublic void sha256Test() {\n\t\t// 测试SHA256对象\n\t\tDigester sha256 = SecureUtil.sha256();\n\t\tassertNotNull(sha256);\n\n\t\t// 测试字符串SHA256\n\t\tString sha256Str = SecureUtil.sha256(TEST_DATA);\n\t\tassertNotNull(sha256Str);\n\t\tassertEquals(64, sha256Str.length()); // SHA256是64位十六进制字符串\n\n\t\t// 测试InputStream SHA256\n\t\tInputStream is = new ByteArrayInputStream(TEST_DATA.getBytes());\n\t\tString streamSha256 = SecureUtil.sha256(is);\n\t\tassertNotNull(streamSha256);\n\t\tassertEquals(64, streamSha256.length());\n\t}\n\n\t@Test\n\tpublic void hmacSha1AndSha256KeyGenerationTest() {\n\t\t// 验证当传入null时，生成的密钥类型是否正确\n\t\tHMac hmacSha1 = SecureUtil.hmacSha1((byte[]) null);\n\t\tHMac hmacSha256 = SecureUtil.hmacSha256((byte[]) null);\n\n\t\tassertNotNull(hmacSha1);\n\t\tassertNotNull(hmacSha256);\n\n\t\t// 验证两个HMac对象使用不同的算法，结果长度也应不同\n\t\tString sha1Result = hmacSha1.digestHex(TEST_DATA);\n\t\tString sha256Result = hmacSha256.digestHex(TEST_DATA);\n\n\t\tassertEquals(40, sha1Result.length()); // SHA1 HMAC应为40字符\n\t\tassertEquals(64, sha256Result.length()); // SHA256 HMAC应为64字符\n\t}\n\n\t@Test\n\tpublic void hmacTest() {\n\t\t// 测试HMac对象生成\n\t\tHMac hmac = SecureUtil.hmac(HmacAlgorithm.HmacSHA256, TEST_KEY);\n\t\tassertNotNull(hmac);\n\n\t\t// 测试字符串密钥\n\t\tHMac hmac2 = SecureUtil.hmac(HmacAlgorithm.HmacMD5, \"testkey\");\n\t\tassertNotNull(hmac2);\n\n\t\t// 测试SecretKey\n\t\tSecretKey secretKey = new SecretKeySpec(TEST_KEY, \"HmacSHA256\");\n\t\tHMac hmac3 = SecureUtil.hmac(HmacAlgorithm.HmacSHA256, secretKey);\n\t\tassertNotNull(hmac3);\n\t}\n\n\t@Test\n\tpublic void hmacMd5Test() {\n\t\tHMac hmacMd5 = SecureUtil.hmacMd5();\n\t\tassertNotNull(hmacMd5);\n\n\t\tHMac hmacMd5WithKey = SecureUtil.hmacMd5(\"testkey\");\n\t\tassertNotNull(hmacMd5WithKey);\n\n\t\tHMac hmacMd5WithBytes = SecureUtil.hmacMd5(TEST_KEY);\n\t\tassertNotNull(hmacMd5WithBytes);\n\n\t\t// 验证加密结果\n\t\tString result = hmacMd5WithKey.digestHex(TEST_DATA);\n\t\tassertNotNull(result);\n\t\tassertEquals(32, result.length()); // MD5 HMAC是32位十六进制字符串\n\t}\n\n\t@Test\n\tpublic void hmacSha1Test() {\n\t\tHMac hmacSha1 = SecureUtil.hmacSha1();\n\t\tassertNotNull(hmacSha1);\n\n\t\tHMac hmacSha1WithKey = SecureUtil.hmacSha1(\"testkey\");\n\t\tassertNotNull(hmacSha1WithKey);\n\n\t\tHMac hmacSha1WithBytes = SecureUtil.hmacSha1(TEST_KEY);\n\t\tassertNotNull(hmacSha1WithBytes);\n\n\t\t// 验证加密结果\n\t\tString result = hmacSha1WithKey.digestHex(TEST_DATA);\n\t\tassertNotNull(result);\n\t\tassertEquals(40, result.length()); // SHA1 HMAC是40位十六进制字符串\n\t}\n\n\t@Test\n\tpublic void hmacSha256Test() {\n\t\tHMac hmacSha256 = SecureUtil.hmacSha256();\n\t\tassertNotNull(hmacSha256);\n\n\t\tHMac hmacSha256WithKey = SecureUtil.hmacSha256(\"testkey\");\n\t\tassertNotNull(hmacSha256WithKey);\n\n\t\tHMac hmacSha256WithBytes = SecureUtil.hmacSha256(TEST_KEY);\n\t\tassertNotNull(hmacSha256WithBytes);\n\n\t\t// 验证加密结果\n\t\tString result = hmacSha256WithKey.digestHex(TEST_DATA);\n\t\tassertNotNull(result);\n\t\tassertEquals(64, result.length()); // SHA256 HMAC是64位十六进制字符串\n\t}\n\n\t@Test\n\tpublic void signTest() {\n\t\t// 测试生成签名对象\n\t\tSign sign = SecureUtil.sign(SignAlgorithm.NONEwithRSA);\n\t\tassertNotNull(sign);\n\n\t\t// 测试使用密钥生成签名对象\n\t\tKeyPair keyPair = SecureUtil.generateKeyPair(\"RSA\", 512);\n\t\tSign sign2 = SecureUtil.sign(SignAlgorithm.SHA256withRSA, keyPair.getPrivate().getEncoded(),\n\t\t\tkeyPair.getPublic().getEncoded());\n\t\tassertNotNull(sign2);\n\n\t\t// 测试签名功能\n\t\tbyte[] signed = sign2.sign(TEST_DATA.getBytes());\n\t\tassertTrue(sign2.verify(TEST_DATA.getBytes(), signed));\n\t}\n\n\t@Test\n\tpublic void decodeTest() {\n\t\t// 测试Hex解码\n\t\tString hexStr = HexUtil.encodeHexStr(TEST_DATA.getBytes());\n\t\tbyte[] decodedHex = SecureUtil.decode(hexStr);\n\t\tassertArrayEquals(TEST_DATA.getBytes(), decodedHex);\n\n\t\t// 测试Base64解码\n\t\tString base64Str = Base64.encode(TEST_DATA);\n\t\tbyte[] decodedBase64 = SecureUtil.decode(base64Str);\n\t\tassertArrayEquals(TEST_DATA.getBytes(), decodedBase64);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/SmTest.java",
    "content": "package cn.hutool.crypto;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SmUtil;\nimport cn.hutool.crypto.digest.HMac;\nimport cn.hutool.crypto.symmetric.SM4;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.crypto.SecretKey;\n\n/**\n * SM单元测试\n *\n * @author looly\n *\n */\npublic class SmTest {\n\n\t@Test\n\tpublic void sm3Test() {\n\t\tString digestHex = SmUtil.sm3(\"aaaaa\");\n\t\tassertEquals(\"136ce3c86e4ed909b76082055a61586af20b4dab674732ebd4b599eef080c9be\", digestHex);\n\t}\n\n\t@Test\n\tpublic void sm4Test() {\n\t\tString content = \"test中文\";\n\t\tSM4 sm4 = SmUtil.sm4();\n\n\t\tString encryptHex = sm4.encryptHex(content);\n\t\tString decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void sm4Test2() {\n\t\tString content = \"test中文\";\n\t\tSM4 sm4 = new SM4(Mode.CTR, Padding.PKCS5Padding);\n\t\tsm4.setIv(\"aaaabbbb\".getBytes());\n\n\t\tString encryptHex = sm4.encryptHex(content);\n\t\tString decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void sm4ECBPKCS5PaddingTest2() {\n\t\tString content = \"test中文\";\n\t\tSM4 sm4 = new SM4(Mode.ECB, Padding.PKCS5Padding);\n\t\tassertEquals(\"SM4/ECB/PKCS5Padding\", sm4.getCipher().getAlgorithm());\n\n\t\tString encryptHex = sm4.encryptHex(content);\n\t\tString decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void sm4TestWithCustomKeyTest() {\n\t\tString content = \"test中文\";\n\n\t\tSecretKey key = KeyUtil.generateKey(SM4.ALGORITHM_NAME);\n\n\t\tSM4 sm4 = new SM4(Mode.ECB, Padding.PKCS5Padding, key);\n\t\tassertEquals(\"SM4/ECB/PKCS5Padding\", sm4.getCipher().getAlgorithm());\n\n\t\tString encryptHex = sm4.encryptHex(content);\n\t\tString decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void sm4TestWithCustomKeyTest2() {\n\t\tString content = \"test中文frfewrewrwerwer---------------------------------------------------\";\n\n\t\tbyte[] key = KeyUtil.generateKey(SM4.ALGORITHM_NAME, 128).getEncoded();\n\n\t\tSM4 sm4 = SmUtil.sm4(key);\n\t\tassertEquals(\"SM4\", sm4.getCipher().getAlgorithm());\n\n\t\tString encryptHex = sm4.encryptHex(content);\n\t\tString decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void hmacSm3Test() {\n\t\tString content = \"test中文\";\n\t\tHMac hMac = SmUtil.hmacSm3(\"password\".getBytes());\n\t\tString digest = hMac.digestHex(content);\n\t\tassertEquals(\"493e3f9a1896b43075fbe54658076727960d69632ac6b6ed932195857a6840c6\", digest);\n\t}\n\n\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/ECIESTest.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.bouncycastle.jce.spec.IESParameterSpec;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ECIESTest {\n\n\t@Test\n\tpublic void eciesTest(){\n\t\tfinal ECIES ecies = new ECIES();\n\t\tfinal IESParameterSpec iesParameterSpec = new IESParameterSpec(null, null, 128);\n\t\tecies.setAlgorithmParameterSpec(iesParameterSpec);\n\n\t\tdoTest(ecies, ecies);\n\t}\n\n\t@Test\n\tpublic void eciesTest2(){\n\t\tfinal IESParameterSpec iesParameterSpec = new IESParameterSpec(null, null, 128);\n\n\t\tfinal ECIES ecies = new ECIES();\n\t\tecies.setAlgorithmParameterSpec(iesParameterSpec);\n\n\t\tfinal byte[] privateKeyBytes = ecies.getPrivateKey().getEncoded();\n\t\tfinal ECIES ecies2 = new ECIES(privateKeyBytes, null);\n\t\tecies2.setAlgorithmParameterSpec(iesParameterSpec);\n\n\t\tdoTest(ecies, ecies2);\n\t}\n\n\t/**\n\t * 测试用例\n\t *\n\t * @param cryptoForEncrypt 加密的Crypto\n\t * @param cryptoForDecrypt 解密的Crypto\n\t */\n\tprivate void doTest(AsymmetricCrypto cryptoForEncrypt, AsymmetricCrypto cryptoForDecrypt){\n\t\tString textBase = \"我是一段特别长的测试\";\n\t\tStringBuilder text = new StringBuilder();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\ttext.append(textBase);\n\t\t}\n\n\t\t// 公钥加密，私钥解密\n\t\tString encryptStr = cryptoForEncrypt.encryptBase64(text.toString(), KeyType.PublicKey);\n\n\t\tString decryptStr = StrUtil.utf8Str(cryptoForDecrypt.decrypt(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text.toString(), decryptStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/Issue3925Test.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport org.bouncycastle.crypto.DataLengthException;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3925Test {\n\t@Test\n\tvoid sm2Test() {\n\t\tfinal SM2 sm2 = new SM2();\n\t\tAssertions.assertThrows(DataLengthException.class, ()->sm2.encrypt(\"\", KeyType.PublicKey));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueI6OQJATest.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n@SuppressWarnings(\"FieldCanBeLocal\")\npublic class IssueI6OQJATest {\n\n\tprivate static final String privateKey = \"MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOgjgkzy33Kg3XzV4r0dpa3o6klM85TVn5jS6woBsTRuU0SsDJzqjXdF34G9uZgbHN74RHoOjO2sRM98tnjEJ8WvqqPBOimvpBeTqVGrWRXtelXhPXaSfYdipGIp2stHr270GTg5+chTrfOn7rp1PA09AoRM+HULaU31St0wntf/AgMBAAECgYBwb9qJ6M1f2RjOgU58aSK5dGoeLN6CRWIzBF4Bj8ZD7ff4+Bh33Ie+sKJMVhfR27gFK10HfYq3B8ygbvh20BOumU9U6xFMOff5yPjOoCAFfa7k69hjPaq8Ls/H9kT4sG+djZAyc43JVjUv0J9VFRlCtgEJHNpWUlTPLaqc+1ScEQJBAP1Ewd2nStmXjgHeMiB+NBhY0QSIN5HBW07MlmsMbJir+OWN0t8YKoYZXynei6UDu6wrwTCCDRhcSCpFy9bA9+cCQQDqpGouK8zhvcM/yT1C+f/Hh9cFIlqLKsHssmSva0lTKVE5O7104VXEwNjufjRwaGLc0bRgs/aJh8W7EcGp2zwpAkEAjsk40xIB7PK4qOzwLcl47VEFZhy114K/S4mkM+3pO5mY1TJD9GrXborXT++bowibwdFZNVPctiMwvERlS0m3eQJBAKEKvyV5QmEdEMjSoY06cGbNwLHxZhtl+TsvJROQmv7MuMaDTgDON0OW6Eynqe4Mdu3/r8E/QtIZsYg3I6gkpCECQQDijoNBop46kR/udUEPCaMjy3lzUVklAGAMKE+mc7n50+A3CeMDXAYU/OCf7vMjo2Uq44CE/yIHtYURn0usCzoB\";\n\tprivate static final String publicKey = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoI4JM8t9yoN181eK9HaWt6OpJTPOU1Z+Y0usKAbE0blNErAyc6o13Rd+BvbmYGxze+ER6DoztrETPfLZ4xCfFr6qjwTopr6QXk6lRq1kV7XpV4T12kn2HYqRiKdrLR69u9Bk4OfnIU63zp+66dTwNPQKETPh1C2lN9UrdMJ7X/wIDAQAB\";\n\n\t@Test\n\tpublic void genKeyTest() {\n\t\tassertEquals(passCryto(\"123\"), passCryto(\"123\"));\n\t}\n\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate String passCryto(final String value){\n\t\treturn SecureUtil.rsa(privateKey, publicKey)\n\t\t\t\t.encryptBase64(value, KeyType.PrivateKey);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueID1EIKTest.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.crypto.KeyUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.PrivateKey;\n\npublic class IssueID1EIKTest {\n\n\t@Test\n\tvoid rsaTest(){\n\t\t// 1. Base64解码\n\t\tString str = \"PFJTQUtleVZhbHVlPjxNb2R1bHVzPnVscHlkSXJydHJUMzJBSnFDV0FFMHQxNXdHYjBKUTJqSnpBUW1FakpRRzhkcnUrdDhyQUtzekVoNXRRL2x4eTdnMFVMR3dzWjNmekQrdm12d2lKWkx5d1dncmszMDdRbFpXSkU3dWIxM2ZtN2pUa0RLOXM0L294alNabm5JTHcrc0lwVGFoLzdlL2hLNkxEN0VFbzNuTHZZK0VjTzdHa21IYXVCUW5CZmhPaz08L01vZHVsdXM+PEV4cG9uZW50PkFRQUI8L0V4cG9uZW50PjxQPjZlSFdVYUZNdWRTV0svODJPeWxxNHZ2Y0FDbmNHUHYvN1VKWVVETnY1elBZVGE5UFNXUTRzNUk3RHBDTWJYcExLK0VldE5mOUFCZ1ZwVjZERTJlMTR3PT08L1A+PFE+eS9uMkc3d2FYZVlGUnZXWjNROW96NVkyVEpHdUdaSXIzeis3QlVGOWZIckp1Nk9SU2V0YUVkdW5tcjgzSFVNN3E4TGIvWGxtdmVpS0p0OWh2NWx6d3c9PTwvUT48RFA+Sy9IdExTVmJuMGNjZUdQWnNzQVRmMWJIZlpoZjdLbmM2cDJlcm1NYjBadGlOeWFMaFVTNWlyUWRPSjFjWlcybkZqV1VhWEp6N1VLWlBwdEZrYTNZOVE9PTwvRFA+PERRPkJSbm9QTU5VaVhxaU1TY2RSUGtJcndCYnRVaURhU0pOdEpTY2NjSTBpRE50N2lKbUZNb3RBM3RSMHIzcmUvRGRnaXNxWTBsdzkxamtjNXBza0dVZkR3PT08L0RRPjxJbnZlcnNlUT5rVGpLTzBpcXU4M3pTZGpqbWNoT2lYQ0k0bm5veTg5c0JiOFFqMk92TXpnRnhOazhVV1hoT29ZdGVnUDNiVUFhZEJBT3VGSnRCcE1RMmdCemo2ekRWZz09PC9JbnZlcnNlUT48RD5COUhQeDdBa24vQU1EbFpibUxVY3ZyUm9iWGhrZWtHT1BSQzVRWXFjVjBYU1d3clhvNzFiVlpXVU5KbG5hYkhjOUc4clBpRkRIcHVDcGI5Z2JxYitVdmdKRXFrd0t5cU5HSmdnSm9yS1Irb2doWFh3czRuZVVTV1lENnpqbGQvN2U0QlNRM05ScTJGbEFPSEZnRnp3aElhazZwY1pOT2pwazlTUWdSY2ZaSGs9PC9EPjwvUlNBS2V5VmFsdWU+\";\n\t\tfinal String xml = Base64.decodeStr(str);\n\n\t\tfinal PrivateKey privateKey = KeyUtil.generateRSAPrivateKey(xml);\n\t\tRSA rsa = new RSA(privateKey, null);\n\t\tString decrypt = rsa.decryptStr(\"tqmp7hGri5WYcZT8bJXJK3SKVlkAx1i1JSpOlOIGB+EAA5OoWS0PtCcWdwLou/qVM28exXKGpmehYbx0Ez0Co8bLHMMnXU3bxp3PXstF2MvrODJoEz+nEzxQ92ngg2n/96Du1rCbwkletYFRO47HpkcEYSTKBsi6NtC98JhUsYSXG15hCJu/I8vOWDF9sB4FCFF9qScpEOUndhctDvAH/UvxBqvSix8mJdL9pyz6Er3zhhQ//4LnI3dQQM0saTq4rZITliTxalT32DRfz0Vj5hNj/So54SspX6fbHjRu0jEaMAotebYZ1Tgpw4AHCYy1DIYoVeGSACd4kc+6ka67gI8jXD7H0tIhI2zyTU3MWQWm2tSOCj+WllELlmCn7ssDp37M6hNO9Imzzj32hWQrsvYsCFufAh+KqRQ1zoF1CQVK8wHRf2ppSFjfR9cCcunpqHqeRrJIpzhJ11dvGZ3JokcjOfDrTNKyXXr7+NVkmc9jPvByEGJXcgkJuX1EHyMv\", KeyType.\n\t\t\tPrivateKey);\n\n\t\tString decodeStr = \"cpu=178BFBFF00A50F00\\r\\n\" +\n\t\t\t\"baseBoard=MP242ML1\\r\\n\" +\n\t\t\t\"bios=MP242ML1\\r\\n\" +\n\t\t\t\"mac=00:FF:CB:EF:28:18|00:FF:03:A2:FC:D7|C8:94:02:F8:8A:83\\r\\n\" +\n\t\t\t\"cusname=123\\r\\n\" +\n\t\t\t\"serviceno=12121\\r\\n\" +\n\t\t\t\"kcliccount=1\\r\\n\" +\n\t\t\t\"cjliccount=1\\r\\n\" +\n\t\t\t\"venprintliccount=1\\r\\n\" +\n\t\t\t\"beginTime=2025-10-11 14:05:10\\r\\n\" +\n\t\t\t\"endTime=2026-10-11 14:05:10\\r\\n\" +\n\t\t\t\"lictype=租赁\\r\\n\" +\n\t\t\t\"serviceendtime=1\\r\\n\" +\n\t\t\t\"validate=1\\r\\n\" +\n\t\t\t\"validateunit=年\\r\\n\";\n\t\tAssertions.assertEquals(decodeStr, decrypt);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/RSATest.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.*;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.crypto.Cipher;\nimport java.math.BigInteger;\nimport java.security.KeyPair;\nimport java.security.PublicKey;\n\n/**\n * RSA算法单元测试\n *\n * @author Looly\n */\npublic class RSATest {\n\n\t@Test\n\tpublic void generateKeyPairTest() {\n\t\tfinal KeyPair pair = KeyUtil.generateKeyPair(\"RSA\");\n\t\tassertNotNull(pair.getPrivate());\n\t\tassertNotNull(pair.getPublic());\n\t}\n\n\t@Test\n\tpublic void rsaCustomKeyTest() {\n\t\tfinal KeyPair pair = KeyUtil.generateKeyPair(\"RSA\");\n\t\tfinal byte[] privateKey = pair.getPrivate().getEncoded();\n\t\tfinal byte[] publicKey = pair.getPublic().getEncoded();\n\n\t\tfinal RSA rsa = SecureUtil.rsa(privateKey, publicKey);\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaTest() {\n\t\tfinal RSA rsa = new RSA();\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(rsa.getPrivateKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\t\tassertNotNull(rsa.getPublicKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaECBTest() {\n\t\tfinal RSA rsa = new RSA(AsymmetricAlgorithm.RSA_ECB.getValue());\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(rsa.getPrivateKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\t\tassertNotNull(rsa.getPublicKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaOAEPTest() {\n\t\tfinal RSA rsa = new RSA(\"RSA/ECB/OAEPWithSHA-1AndMGF1Padding\");\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(rsa.getPrivateKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\t\tassertNotNull(rsa.getPublicKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaNoneTest() {\n\t\tfinal RSA rsa = new RSA(AsymmetricAlgorithm.RSA_None.getValue());\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(rsa.getPrivateKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\t\tassertNotNull(rsa.getPublicKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaWithBlockTest2() {\n\t\tfinal RSA rsa = new RSA();\n\t\trsa.setEncryptBlockSize(3);\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(rsa.getPrivateKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\t\tassertNotNull(rsa.getPublicKey());\n\t\tassertNotNull(rsa.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal byte[] encrypt = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\t\tfinal byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal byte[] encrypt2 = rsa.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);\n\t\tfinal byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaBcdTest() {\n\t\tfinal String text = \"我是一段测试aaaa\";\n\n\t\tfinal RSA rsa = new RSA();\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal String encryptStr = rsa.encryptBcd(text, KeyType.PublicKey);\n\t\tfinal String decryptStr = StrUtil.utf8Str(rsa.decryptFromBcd(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text, decryptStr);\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal String encrypt2 = rsa.encryptBcd(text, KeyType.PrivateKey);\n\t\tfinal String decrypt2 = StrUtil.utf8Str(rsa.decryptFromBcd(encrypt2, KeyType.PublicKey));\n\t\tassertEquals(text, decrypt2);\n\t}\n\n\t@Test\n\tpublic void rsaBase64Test() {\n\t\tfinal String textBase = \"我是一段特别长的测试\";\n\t\tfinal StringBuilder text = new StringBuilder();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\ttext.append(textBase);\n\t\t}\n\n\t\tfinal RSA rsa = new RSA();\n\n\t\t// 公钥加密，私钥解密\n\t\tfinal String encryptStr = rsa.encryptBase64(text.toString(), KeyType.PublicKey);\n\t\tfinal String decryptStr = StrUtil.utf8Str(rsa.decrypt(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text.toString(), decryptStr);\n\n\t\t// 私钥加密，公钥解密\n\t\tfinal String encrypt2 = rsa.encryptBase64(text.toString(), KeyType.PrivateKey);\n\t\tfinal String decrypt2 = StrUtil.utf8Str(rsa.decrypt(encrypt2, KeyType.PublicKey));\n\t\tassertEquals(text.toString(), decrypt2);\n\t}\n\n\t@Test\n\tpublic void rsaDecodeTest() {\n\t\tfinal String PRIVATE_KEY = \"MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIL7pbQ+5KKGYRhw7jE31hmA\" //\n\t\t\t\t+ \"f8Q60ybd+xZuRmuO5kOFBRqXGxKTQ9TfQI+aMW+0lw/kibKzaD/EKV91107xE384qOy6IcuBfaR5lv39OcoqNZ\"//\n\t\t\t\t+ \"5l+Dah5ABGnVkBP9fKOFhPgghBknTRo0/rZFGI6Q1UHXb+4atP++LNFlDymJcPAgMBAAECgYBammGb1alndta\" //\n\t\t\t\t+ \"xBmTtLLdveoBmp14p04D8mhkiC33iFKBcLUvvxGg2Vpuc+cbagyu/NZG+R/WDrlgEDUp6861M5BeFN0L9O4hz\"//\n\t\t\t\t+ \"GAEn8xyTE96f8sh4VlRmBOvVdwZqRO+ilkOM96+KL88A9RKdp8V2tna7TM6oI3LHDyf/JBoXaQJBAMcVN7fKlYP\" //\n\t\t\t\t+ \"Skzfh/yZzW2fmC0ZNg/qaW8Oa/wfDxlWjgnS0p/EKWZ8BxjR/d199L3i/KMaGdfpaWbYZLvYENqUCQQCobjsuCW\"//\n\t\t\t\t+ \"nlZhcWajjzpsSuy8/bICVEpUax1fUZ58Mq69CQXfaZemD9Ar4omzuEAAs2/uee3kt3AvCBaeq05NyjAkBme8SwB0iK\"//\n\t\t\t\t+ \"kLcaeGuJlq7CQIkjSrobIqUEf+CzVZPe+AorG+isS+Cw2w/2bHu+G0p5xSYvdH59P0+ZT0N+f9LFAkA6v3Ae56OrI\"//\n\t\t\t\t+ \"wfMhrJksfeKbIaMjNLS9b8JynIaXg9iCiyOHmgkMl5gAbPoH/ULXqSKwzBw5mJ2GW1gBlyaSfV3AkA/RJC+adIjsRGg\"//\n\t\t\t\t+ \"JOkiRjSmPpGv3FOhl9fsBPjupZBEIuoMWOC8GXK/73DHxwmfNmN7C9+sIi4RBcjEeQ5F5FHZ\";\n\n\t\tfinal RSA rsa = new RSA(PRIVATE_KEY, null);\n\n\t\tfinal String a = \"2707F9FD4288CEF302C972058712F24A5F3EC62C5A14AD2FC59DAB93503AA0FA17113A020EE4EA35EB53F\" //\n\t\t\t\t+ \"75F36564BA1DABAA20F3B90FD39315C30E68FE8A1803B36C29029B23EB612C06ACF3A34BE815074F5EB5AA3A\"//\n\t\t\t\t+ \"C0C8832EC42DA725B4E1C38EF4EA1B85904F8B10B2D62EA782B813229F9090E6F7394E42E6F44494BB8\";\n\n\t\tfinal byte[] aByte = HexUtil.decodeHex(a);\n\t\tfinal byte[] decrypt = rsa.decrypt(aByte, KeyType.PrivateKey);\n\n\t\tassertEquals(\"虎头闯杭州,多抬头看天,切勿只管种地\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void rsaTest2() throws Exception {\n\t\tfinal String publicKeyStr = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6s\" +\n\t\t\t\t\"XqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9S\" +\n\t\t\t\t\"dB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\";\n\n\t\tfinal byte[] keyBytes = Base64.decode(publicKeyStr);\n\t\tfinal PublicKey publicKey = KeyUtil.generateRSAPublicKey(keyBytes);\n\n\t\tfinal byte[] data = RandomUtil.randomString(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\", 16).getBytes();\n\t\t//长度不满足128补0\n\t\tfinal byte[] finalData = ArrayUtil.resize(data, 128);\n\n\t\t//jdk原生加密\n\t\tfinal Cipher cipher = Cipher.getInstance(\"RSA/ECB/NoPadding\");\n\t\tcipher.init(Cipher.ENCRYPT_MODE, publicKey);\n\t\tfinal String result1 = HexUtil.encodeHexStr(cipher.doFinal(finalData));\n\n\t\t//hutool加密\n\t\tfinal RSA rsa = new RSA(\"RSA/ECB/NoPadding\", null, publicKeyStr);\n\t\trsa.setEncryptBlockSize(128);\n\t\tfinal String result2 = rsa.encryptHex(finalData, KeyType.PublicKey);\n\n\t\tassertEquals(result1, result2);\n\t}\n\n\t@Test\n\tpublic void exponentTest(){\n\t\tfinal String modulus = \"BD99BAAB9E56B7FD85FB8BCF53CAD2913C1ACEF9063E7C913CD6FC4FEE040DA44D8\" +\n\t\t\t\t\"ADAA35A9DCABD6E936C402D47278049638407135BAB22BB091396CB6873195C8AC8B0B7AB123\" +\n\t\t\t\t\"C3BF7A6341A4419BDBC0EFB85DBCD9A3AD12C99E2265BDCC1197913749E2AFA568EB7623DA3A\" +\n\t\t\t\t\"361335AA1F9FFA6E1801DDC8228AA86306B87\";\n\t\tfinal String publicExponent = \"65537\";\n\t\tfinal RSA rsa = new RSA(new BigInteger(modulus, 16), null, new BigInteger(publicExponent));\n\n\t\tfinal String encryptBase64 = rsa.encryptBase64(\"测试内容\", KeyType.PublicKey);\n\t\tassertNotNull(encryptBase64);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/SM2Test.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.ECKeyUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport cn.hutool.crypto.SmUtil;\nimport org.bouncycastle.crypto.DataLengthException;\nimport org.bouncycastle.crypto.engines.SM2Engine;\nimport org.bouncycastle.crypto.params.ECPrivateKeyParameters;\nimport org.bouncycastle.jcajce.spec.OpenSSHPrivateKeySpec;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * SM2算法单元测试\n *\n * @author Looly, Gsealy\n */\npublic class SM2Test {\n\n\t@Test\n\tpublic void generateKeyPairTest() {\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\t\tassertNotNull(pair.getPrivate());\n\t\tassertNotNull(pair.getPublic());\n\t}\n\n\t@Test\n\tpublic void KeyPairOIDTest() {\n\t\t// OBJECT IDENTIFIER 1.2.156.10197.1.301\n\t\tString OID = \"06082A811CCF5501822D\";\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\t\tassertTrue(HexUtil.encodeHexStr(pair.getPrivate().getEncoded()).toUpperCase().contains(OID));\n\t\tassertTrue(HexUtil.encodeHexStr(pair.getPublic().getEncoded()).toUpperCase().contains(OID));\n\t}\n\n\t@Test\n\tpublic void sm2CustomKeyTest() {\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\t\tbyte[] privateKey = pair.getPrivate().getEncoded();\n\t\tbyte[] publicKey = pair.getPublic().getEncoded();\n\n\t\tSM2 sm2 = SmUtil.sm2(privateKey, publicKey);\n\t\tsm2.setMode(SM2Engine.Mode.C1C2C3);\n\n\t\t// 公钥加密，私钥解密\n\t\tbyte[] encrypt = sm2.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\t\tbyte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void sm2Test() {\n\t\tfinal SM2 sm2 = SmUtil.sm2();\n\n\t\t// 获取私钥和公钥\n\t\tassertNotNull(sm2.getPrivateKey());\n\t\tassertNotNull(sm2.getPrivateKeyBase64());\n\t\tassertNotNull(sm2.getPublicKey());\n\t\tassertNotNull(sm2.getPrivateKeyBase64());\n\n\t\t// 公钥加密，私钥解密\n\t\tbyte[] encrypt = sm2.encrypt(StrUtil.bytes(\"我是一段测试aaaa\", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);\n\t\tbyte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"我是一段测试aaaa\", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t@Test\n\tpublic void sm2BcdTest() {\n\t\tString text = \"我是一段测试aaaa\";\n\n\t\tfinal SM2 sm2 = SmUtil.sm2();\n\n\t\t// 公钥加密，私钥解密\n\t\tString encryptStr = sm2.encryptBcd(text, KeyType.PublicKey);\n\t\tString decryptStr = StrUtil.utf8Str(sm2.decryptFromBcd(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text, decryptStr);\n\t}\n\n\t@Test\n\tpublic void sm2Base64Test() {\n\t\tString textBase = \"我是一段特别长的测试\";\n\t\tStringBuilder text = new StringBuilder();\n\t\tfor (int i = 0; i < 100; i++) {\n\t\t\ttext.append(textBase);\n\t\t}\n\n\t\tSM2 sm2 = new SM2();\n\n\t\t// 公钥加密，私钥解密\n\t\tString encryptStr = sm2.encryptBase64(text.toString(), KeyType.PublicKey);\n\t\tString decryptStr = StrUtil.utf8Str(sm2.decrypt(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text.toString(), decryptStr);\n\n\t\t// 测试自定义密钥后是否生效\n\t\tPrivateKey privateKey = sm2.getPrivateKey();\n\t\tPublicKey publicKey = sm2.getPublicKey();\n\n\t\tsm2 = SmUtil.sm2();\n\t\tsm2.setPrivateKey(privateKey);\n\t\tsm2.setPublicKey(publicKey);\n\t\tString decryptStr2 = StrUtil.utf8Str(sm2.decrypt(encryptStr, KeyType.PrivateKey));\n\t\tassertEquals(text.toString(), decryptStr2);\n\t}\n\n\t@Test\n\tpublic void sm2SignTest(){\n\t\t//需要签名的明文,得到明文对应的字节数组\n\t\tbyte[] dataBytes = \"我是一段测试aaaa\".getBytes(StandardCharsets.UTF_8);\n\n\t\t//指定的私钥\n\t\tString privateKeyHex = \"1ebf8b341c695ee456fd1a41b82645724bc25d79935437d30e7e4b0a554baa5e\";\n\t\tfinal SM2 sm2 = new SM2(privateKeyHex, null, null);\n\t\tsm2.usePlainEncoding();\n\t\tbyte[] sign = sm2.sign(dataBytes, null);\n\t\t// 64位签名\n\t\tassertEquals(64, sign.length);\n\t}\n\n\t@Test\n\tpublic void sm2VerifyTest(){\n\t\t//指定的公钥\n\t\tString publicKeyHex = \"04db9629dd33ba568e9507add5df6587a0998361a03d3321948b448c653c2c1b7056434884ab6f3d1c529501f166a336e86f045cea10dffe58aa82ea13d7253763\";\n\t\t//需要加密的明文,得到明文对应的字节数组\n\t\tbyte[] dataBytes = \"我是一段测试aaaa\".getBytes(StandardCharsets.UTF_8);\n\t\t//签名值\n\t\tString signHex = \"2881346e038d2ed706ccdd025f2b1dafa7377d5cf090134b98756fafe084dddbcdba0ab00b5348ed48025195af3f1dda29e819bb66aa9d4d088050ff148482a1\";\n\n\t\tfinal SM2 sm2 = new SM2(null, publicKeyHex);\n\t\tsm2.usePlainEncoding();\n\n\t\tboolean verify = sm2.verify(dataBytes, HexUtil.decodeHex(signHex));\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void sm2SignAndVerifyTest() {\n\t\tString content = \"我是Hanley.\";\n\n\t\tfinal SM2 sm2 = SmUtil.sm2();\n\n\t\tbyte[] sign = sm2.sign(StrUtil.utf8Bytes(content));\n\t\tboolean verify = sm2.verify(StrUtil.utf8Bytes(content), sign);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void sm2SignAndVerifyHexTest() {\n\t\tString content = \"我是Hanley.\";\n\n\t\tfinal SM2 sm2 = SmUtil.sm2();\n\n\t\tString sign = sm2.signHexFromHex(HexUtil.encodeHexStr(content));\n\t\tboolean verify = sm2.verifyHex(HexUtil.encodeHexStr(content), sign);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void sm2SignAndVerifyUseKeyTest() {\n\t\tString content = \"我是Hanley.\";\n\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\n\t\tfinal SM2 sm2 = new SM2(pair.getPrivate(), pair.getPublic());\n\n\t\tbyte[] sign = sm2.sign(content.getBytes(StandardCharsets.UTF_8));\n\t\tboolean verify = sm2.verify(content.getBytes(StandardCharsets.UTF_8), sign);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void sm2SignAndVerifyUseKeyTest2() {\n\t\tString content = \"我是Hanley.\";\n\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\n\t\tfinal SM2 sm2 = new SM2(//\n\t\t\t\tHexUtil.encodeHexStr(pair.getPrivate().getEncoded()), //\n\t\t\t\tHexUtil.encodeHexStr(pair.getPublic().getEncoded())//\n\t\t);\n\n\t\tbyte[] sign = sm2.sign(content.getBytes(StandardCharsets.UTF_8));\n\t\tboolean verify = sm2.verify(content.getBytes(StandardCharsets.UTF_8), sign);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void sm2PublicKeyEncodeDecodeTest() {\n\t\tKeyPair pair = SecureUtil.generateKeyPair(\"SM2\");\n\t\tPublicKey publicKey = pair.getPublic();\n\t\tbyte[] data = KeyUtil.encodeECPublicKey(publicKey);\n\t\tString encodeHex = HexUtil.encodeHexStr(data);\n\t\tString encodeB64 = Base64.encode(data);\n\t\tPublicKey Hexdecode = KeyUtil.decodeECPoint(encodeHex, KeyUtil.SM2_DEFAULT_CURVE);\n\t\tPublicKey B64decode = KeyUtil.decodeECPoint(encodeB64, KeyUtil.SM2_DEFAULT_CURVE);\n\t\tassertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(Hexdecode.getEncoded()));\n\t\tassertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(B64decode.getEncoded()));\n\t}\n\n\t@Test\n\tpublic void sm2WithPointTest() {\n\t\tString d = \"FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0\";\n\t\tString x = \"9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3\";\n\t\tString y = \"CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF\";\n\n\t\tString data = \"434477813974bf58f94bcf760833c2b40f77a5fc360485b0b9ed1bd9682edb45\";\n\t\tString id = \"31323334353637383132333435363738\";\n\n\t\tfinal SM2 sm2 = new SM2(d, x, y);\n\t\tfinal String sign = sm2.signHex(data, id);\n\t\tassertTrue(sm2.verifyHex(data, sign));\n\t}\n\n\t@Test\n\tpublic void sm2WithNullPriPointTest() {\n\t\tString x = \"9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3\";\n\t\tString y = \"CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF\";\n\t\tString q = \"04\" + x + y;\n\t\tfinal SM2 sm1 = new SM2(null, x, y);\n\t\tfinal SM2 sm2 = new SM2(null, q);\n        assertNotNull(sm1);\n\t\tassertNotNull(sm2);\n\t}\n\n\t@Test\n\tpublic void sm2PlainWithPointTest() {\n\t\t// 测试地址：https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg\n\n\t\tString d = \"FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0\";\n\t\tString x = \"9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3\";\n\t\tString y = \"CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF\";\n\n\t\tString data = \"434477813974bf58f94bcf760833c2b40f77a5fc360485b0b9ed1bd9682edb45\";\n\t\tString id = \"31323334353637383132333435363738\";\n\n\t\tfinal SM2 sm2 = new SM2(d, x, y);\n\t\t// 生成的签名是64位\n\t\tsm2.usePlainEncoding();\n\n\n\t\tString sign = \"DCA0E80A7F46C93714B51C3EFC55A922BCEF7ECF0FE9E62B53BA6A7438B543A76C145A452CA9036F3CB70D7E6C67D4D9D7FE114E5367A2F6F5A4D39F2B10F3D6\";\n\t\tassertTrue(sm2.verifyHex(data, sign));\n\n\t\tString sign2 = sm2.signHex(data, id);\n\t\tassertTrue(sm2.verifyHex(data, sign2));\n\t}\n\n\t@Test\n\tpublic void sm2PlainWithPointTest2() {\n\t\tString d = \"4BD9A450D7E68A5D7E08EB7A0BFA468FD3EB32B71126246E66249A73A9E4D44A\";\n\t\tString q = \"04970AB36C3B870FBC04041087DB1BC36FB4C6E125B5EA406DB0EC3E2F80F0A55D8AFF28357A0BB215ADC2928BE76F1AFF869BF4C0A3852A78F3B827812C650AD3\";\n\n\t\tString data = \"123456\";\n\n\t\tfinal SM2 sm2 = new SM2(d, q);\n\t\tsm2.setMode(SM2Engine.Mode.C1C2C3);\n\t\tfinal String encryptHex = sm2.encryptHex(data, KeyType.PublicKey);\n\t\tfinal String decryptStr = sm2.decryptStr(encryptHex, KeyType.PrivateKey);\n\n\t\tassertEquals(data, decryptStr);\n\t}\n\n\t@Test\n\tpublic void encryptAndSignTest(){\n\t\tSM2 sm2 = SmUtil.sm2();\n\n\t\tString src = \"Sm2Test\";\n\t\tbyte[] data = sm2.encrypt(src, KeyType.PublicKey);\n\t\tbyte[] sign =  sm2.sign(src.getBytes(StandardCharsets.UTF_8));\n\n\t\tassertTrue(sm2.verify( src.getBytes(StandardCharsets.UTF_8), sign));\n\n\t\tbyte[] dec =  sm2.decrypt(data, KeyType.PrivateKey);\n\t\tassertArrayEquals(dec, src.getBytes(StandardCharsets.UTF_8));\n\t}\n\n\t@Test\n\tpublic void getPublicKeyByPrivateKeyTest(){\n\t\t// issue#I38SDP，openSSL生成的PKCS#1格式私钥\n\t\tString priKey = \"MHcCAQEEIE29XqAFV/rkJbnJzCoQRJLTeAHG2TR0h9ZCWag0+ZMEoAoGCCqBHM9VAYItoUQDQgAESkOzNigIsH5ehFvr9y\" +\n\t\t\t\t\"QNQ66genyOrm+Q4umCA4aWXPeRzmcTAWSlTineiReTFN2lqor2xaulT8u3a4w3AM/F6A==\";\n\n\t\tPrivateKey privateKey = KeyUtil.generatePrivateKey(\"sm2\", new OpenSSHPrivateKeySpec(SecureUtil.decode(priKey)));\n\t\tfinal ECPrivateKeyParameters privateKeyParameters = ECKeyUtil.toPrivateParams(privateKey);\n\n\t\tfinal SM2 sm2 = new SM2(privateKeyParameters, ECKeyUtil.getPublicParams(privateKeyParameters));\n\n\t\tString src = \"Sm2Test\";\n\t\tbyte[] data = sm2.encrypt(src, KeyType.PublicKey);\n\t\tbyte[] sign =  sm2.sign(src.getBytes(StandardCharsets.UTF_8));\n\n\t\tassertTrue(sm2.verify( src.getBytes(StandardCharsets.UTF_8), sign));\n\n\t\tbyte[] dec =  sm2.decrypt(data, KeyType.PrivateKey);\n\t\tassertArrayEquals(dec, src.getBytes(StandardCharsets.UTF_8));\n\t}\n\n\t@Test\n\tpublic void readPublicKeyTest(){\n\t\tString priKey = \"MHcCAQEEIE29XqAFV/rkJbnJzCoQRJLTeAHG2TR0h9ZCWag0+ZMEoAoGCCqBHM9VAYItoUQDQgAESkOzNigIsH5ehFvr9y\" +\n\t\t\t\t\"QNQ66genyOrm+Q4umCA4aWXPeRzmcTAWSlTineiReTFN2lqor2xaulT8u3a4w3AM/F6A==\";\n\t\tString pubKey = \"MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAESkOzNigIsH5ehFvr9yQNQ66genyOrm+Q4umCA4aWXPeRzmcTAWSlTineiReTFN2lqor2xaulT8u3a4w3AM/F6A==\";\n\n\t\tSM2 sm2 = SmUtil.sm2(priKey, pubKey);\n\n\t\tString src = \"Sm2Test中文\";\n\t\tbyte[] data = sm2.encrypt(src, KeyType.PublicKey);\n\t\tbyte[] sign =  sm2.sign(src.getBytes(StandardCharsets.UTF_8));\n\n\t\tassertTrue(sm2.verify( src.getBytes(StandardCharsets.UTF_8), sign));\n\n\t\tbyte[] dec =  sm2.decrypt(data, KeyType.PrivateKey);\n\t\tassertArrayEquals(dec, src.getBytes(StandardCharsets.UTF_8));\n\t}\n\n\t@Test\n\tpublic void dLengthTest(){\n\t\tfinal SM2 sm2 = SmUtil.sm2();\n\t\tassertEquals(64, sm2.getDHex().length());\n\t\tassertEquals(32, sm2.getD().length);\n\n\t\t// 04占位一个字节\n\t\tassertEquals(65, sm2.getQ(false).length);\n\t}\n\n\t@Test\n\tpublic void issueI6ROLTTest(){\n\t\tString publicKey = \"04bf347dfa32b9bc4c378232898ea43a210887a9b9ed6cc188f91b653706b44fa8434518d54412606788f34be8097cc233608f780edaf695c7e2b1d1c1b7b0d7c3\";\n\t\tnew SM2(null, publicKey);\n\t}\n\n\t@Test\n\tpublic void issueIA824PTest() {\n\t\tassertThrows(DataLengthException.class, () -> {\n\t\t\tSM2 sm2 = SmUtil.sm2();\n\t\t\tString emptyStr = \"\";\n\t\t\tsm2.encryptHex(emptyStr, KeyType.PublicKey);\n\t\t});\n\t}\n\n\t@Test\n\tvoid decryptFromGmSSLTest() {\n\t\t// https://the-x.cn/zh-cn/cryptography/Sm2.aspx\n\t\t// python gmssl加密后的内容无04标识，检查并补充\n\t\tString privateKey = \"MHcCAQEEICxTSOhWA4oYj2DI95zunPqHHEKZSi5QFLvWz57BfIGVoAoGCCqBHM9VAYItoUQDQgAEIGRS/PssvgZ8Paw2YeFaW4VXrkgceBELKPWcXmq/p3iMhHxYfcaFAa5AzvPJOmYmVzVwu9QygMMrg/30Ok1npw==\";\n\t\tfinal SM2 sm2 = new SM2(privateKey, null);\n\t\tsm2.setMode(SM2Engine.Mode.C1C2C3);\n\n\t\tString encrypt = \"x0KA1DKkmuA/YZdmvMr8X+1ZQb7a19Pr5nSxxe2ItUYpDAioa263tm9u7vST38hAEUoOxxXftD+7bRQ7Y8v1tcFXeheKodetA6LrPIuh0QYZMdBqIKSKdmlGeVE0Vdm3excisbtC\";\n\t\tfinal byte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey);\n\t\tassertEquals(\"123456\", StrUtil.utf8Str(decrypt));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/SignTest.java",
    "content": "package cn.hutool.crypto.asymmetric;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 签名单元测试\n *\n * @author looly\n *\n */\npublic class SignTest {\n\n\t@Test\n\tpublic void signAndVerifyUseKeyTest() {\n\t\tString content = \"我是Hanley.\";\n\n\t\tString privateKey = \"MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ4fG8vJ0tzu7tjXMSJhyNjlE5B7GkTKMKEQlR6LY3IhIhMFVjuA6W+DqH1VMxl9h3GIM4yCKG2VRZEYEPazgVxa5/ifO8W0pfmrzWCPrddUq4t0Slz5u2lLKymLpPjCzboHoDb8VlF+1HOxjKQckAXq9q7U7dV5VxOzJDuZXlz3AgMBAAECgYABo2LfVqT3owYYewpIR+kTzjPIsG3SPqIIWSqiWWFbYlp/BfQhw7EndZ6+Ra602ecYVwfpscOHdx90ZGJwm+WAMkKT4HiWYwyb0ZqQzRBGYDHFjPpfCBxrzSIJ3QL+B8c8YHq4HaLKRKmq7VUF1gtyWaek87rETWAmQoGjt8DyAQJBAOG4OxsT901zjfxrgKwCv6fV8wGXrNfDSViP1t9r3u6tRPsE6Gli0dfMyzxwENDTI75sOEAfyu6xBlemQGmNsfcCQQCzVWQkl9YUoVDWEitvI5MpkvVKYsFLRXKvLfyxLcY3LxpLKBcEeJ/n5wLxjH0GorhJMmM2Rw3hkjUTJCoqqe0BAkATt8FKC0N2O5ryqv1xiUfuxGzW/cX2jzOwDdiqacTuuqok93fKBPzpyhUS8YM2iss7jj6Xs29JzKMOMxK7ZcpfAkAf21lwzrAu9gEgJhYlJhKsXfjJAAYKUwnuaKLs7o65mtp242ZDWxI85eK1+hjzptBJ4HOTXsfufESFY/VBovIBAkAltO886qQRoNSc0OsVlCi4X1DGo6x2RqQ9EsWPrxWEZGYuyEdODrc54b8L+zaUJLfMJdsCIHEUbM7WXxvFVXNv\";\n\t\tSign sign = SecureUtil.sign(SignAlgorithm.SHA1withRSA, privateKey, null);\n\t\tassertNull(sign.getPublicKeyBase64());\n\t\t// 签名\n\t\tbyte[] signed = sign.sign(content.getBytes());\n\n\t\tString publicKey = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeHxvLydLc7u7Y1zEiYcjY5ROQexpEyjChEJUei2NyISITBVY7gOlvg6h9VTMZfYdxiDOMgihtlUWRGBD2s4FcWuf4nzvFtKX5q81gj63XVKuLdEpc+btpSyspi6T4ws26B6A2/FZRftRzsYykHJAF6vau1O3VeVcTsyQ7mV5c9wIDAQAB\";\n\t\tsign = SecureUtil.sign(SignAlgorithm.SHA1withRSA, null, publicKey);\n\t\t// 验证签名\n\t\tboolean verify = sign.verify(content.getBytes(), signed);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void signAndVerifyTest() {\n\t\tsignAndVerify(SignAlgorithm.NONEwithRSA);\n\t\tsignAndVerify(SignAlgorithm.MD2withRSA);\n\t\tsignAndVerify(SignAlgorithm.MD5withRSA);\n\n\t\tsignAndVerify(SignAlgorithm.SHA1withRSA);\n\t\tsignAndVerify(SignAlgorithm.SHA256withRSA);\n\t\tsignAndVerify(SignAlgorithm.SHA384withRSA);\n\t\tsignAndVerify(SignAlgorithm.SHA512withRSA);\n\n\t\tsignAndVerify(SignAlgorithm.NONEwithDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA1withDSA);\n\n\t\tsignAndVerify(SignAlgorithm.NONEwithECDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA1withECDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA1withECDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA256withECDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA384withECDSA);\n\t\tsignAndVerify(SignAlgorithm.SHA512withECDSA);\n\t}\n\n\t/**\n\t * 测试各种算法的签名和验证签名\n\t *\n\t * @param signAlgorithm 算法\n\t */\n\tprivate void signAndVerify(SignAlgorithm signAlgorithm) {\n\t\tbyte[] data = StrUtil.utf8Bytes(\"我是一段测试ab\");\n\t\tSign sign = SecureUtil.sign(signAlgorithm);\n\n\t\t// 签名\n\t\tbyte[] signed = sign.sign(data);\n\n\t\t// 验证签名\n\t\tboolean verify = sign.verify(data, signed);\n\t\tassertTrue(verify);\n\t}\n\n\t/**\n\t * 测试MD5withRSA算法的签名和验证签名\n\t */\n\t@Test\n\tpublic void signAndVerifyTest2() {\n\t\tString str = \"wx2421b1c4370ec43b 支付测试 JSAPI支付测试 10000100 1add1a30ac87aa2db72f57a2375d8fec http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php oUpF8uMuAJO_M2pxb1Q9zNjWeS6o 1415659990 14.23.150.211 1 JSAPI 0CB01533B8C1EF103065174F50BCA001\";\n\t\tbyte[] data = StrUtil.utf8Bytes(str);\n\t\tSign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA);\n\n\t\t// 签名\n\t\tbyte[] signed = sign.sign(data);\n\n\t\t// 验证签名\n\t\tboolean verify = sign.verify(data, signed);\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void signParamsTest(){\n\t\tMap<String, String> build = MapUtil.builder(new HashMap<String, String>())\n\t\t\t\t.put(\"key1\", \"value1\")\n\t\t\t\t.put(\"key2\", \"value2\").build();\n\n\t\tString sign1 = SecureUtil.signParamsSha1(build);\n\t\tassertEquals(\"9ed30bfe2efbc7038a824b6c55c24a11bfc0dce5\", sign1);\n\t\tString sign2 = SecureUtil.signParamsSha1(build, \"12345678\");\n\t\tassertEquals(\"944b68d94c952ec178c4caf16b9416b6661f7720\", sign2);\n\t\tString sign3 = SecureUtil.signParamsSha1(build, \"12345678\", \"abc\");\n\t\tassertEquals(\"edee1b477af1b96ebd20fdf08d818f352928d25d\", sign3);\n\t}\n\n\t/**\n\t * 测试MD5withRSA算法的签名和验证签名\n\t */\n\t@Test\n\tpublic void signAndVerifyPSSTest() {\n\t\tString str = \"wx2421b1c4370ec43b 支付测试 JSAPI支付测试 10000100 1add1a30ac87aa2db72f57a2375d8fec http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php oUpF8uMuAJO_M2pxb1Q9zNjWeS6o 1415659990 14.23.150.211 1 JSAPI 0CB01533B8C1EF103065174F50BCA001\";\n\t\tbyte[] data = StrUtil.utf8Bytes(str);\n\t\tSign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA_PSS);\n\n\t\t// 签名\n\t\tbyte[] signed = sign.sign(data);\n\n\t\t// 验证签名\n\t\tboolean verify = sign.verify(data, signed);\n\t\tassertTrue(verify);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/Argon2Test.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.codec.Base64;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class Argon2Test {\n\t@Test\n\tpublic void argon2Test() {\n\t\tArgon2 argon2 = new Argon2();\n\t\tfinal byte[] digest = argon2.digest(\"123456\".toCharArray());\n\t\tAssertions.assertEquals(\"wVGMOdzf5EdKGANPeHjaUnaFEJA0BnAq6HcF2psFmFo=\", Base64.encode(digest));\n\t}\n\n\t@Test\n\tpublic void argon2WithSaltTest() {\n\t\tfinal Argon2 argon2 = new Argon2();\n\t\targon2.setSalt(\"123456\".getBytes());\n\t\tfinal byte[] digest = argon2.digest(\"123456\".toCharArray());\n\t\tAssertions.assertEquals(\"sEpbXTdMWra36JXPVxrZMm3xyoR5GkMlLhtW0Kwp9Ag=\", Base64.encode(digest));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/BCryptTest.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BCryptTest {\n\n\t@Test\n\tpublic void checkpwTest(){\n\t\tassertFalse(BCrypt.checkpw(\"xxx\",\n\t\t\t\t\"$2a$2a$10$e4lBTlZ019KhuAFyqAlgB.Jxc6cM66GwkSR/5/xXNQuHUItPLyhzy\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/CBCBlockCipherMacEngineTest.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.digest.mac.Mac;\nimport cn.hutool.crypto.digest.mac.SM4MacEngine;\nimport org.bouncycastle.crypto.CipherParameters;\nimport org.bouncycastle.crypto.params.KeyParameter;\nimport org.bouncycastle.crypto.params.ParametersWithIV;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CBCBlockCipherMacEngineTest {\n\n\t@Test\n\tpublic void SM4CMACTest(){\n\t\t// https://github.com/chinabugotech/hutool/issues/2206\n\t\tfinal byte[] key = new byte[16];\n\t\tfinal CipherParameters parameter = new KeyParameter(KeyUtil.generateKey(\"SM4\", key).getEncoded());\n\t\tMac mac = new Mac(new SM4MacEngine(parameter));\n\n\t\t// 原文\n\t\tString testStr = \"test中文\";\n\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"3212e848db7f816a4bd591ad9948debf\", macHex1);\n\t}\n\n\t@Test\n\tpublic void SM4CMACWithIVTest(){\n\t\t// https://github.com/chinabugotech/hutool/issues/2206\n\t\tfinal byte[] key = new byte[16];\n\t\tfinal byte[] iv = new byte[16];\n\t\tCipherParameters parameter = new KeyParameter(KeyUtil.generateKey(\"SM4\", key).getEncoded());\n\t\tparameter = new ParametersWithIV(parameter, iv);\n\t\tMac mac = new Mac(new SM4MacEngine(parameter));\n\n\t\t// 原文\n\t\tString testStr = \"test中文\";\n\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"3212e848db7f816a4bd591ad9948debf\", macHex1);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/DigestTest.java",
    "content": "package cn.hutool.crypto.digest;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\n/**\n * 摘要算法单元测试\n * @author Looly\n *\n */\npublic class DigestTest {\n\n\t@Test\n\tpublic void digesterTest(){\n\t\tString testStr = \"test中文\";\n\n\t\tDigester md5 = new Digester(DigestAlgorithm.MD5);\n\t\tString digestHex = md5.digestHex(testStr);\n\t\tassertEquals(\"5393554e94bf0eb6436f240a4fd71282\", digestHex);\n\t}\n\n\t@Test\n\tpublic void md5Test(){\n\t\tString testStr = \"test中文\";\n\n\t\tString md5Hex1 = DigestUtil.md5Hex(testStr);\n\t\tassertEquals(\"5393554e94bf0eb6436f240a4fd71282\", md5Hex1);\n\n\t\tString md5Hex2 = DigestUtil.md5Hex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"5393554e94bf0eb6436f240a4fd71282\", md5Hex2);\n\t}\n\n\t@Test\n\tpublic void md5WithSaltTest(){\n\t\tString testStr = \"test中文\";\n\n\t\tDigester md5 = new Digester(DigestAlgorithm.MD5);\n\n\t\t//加盐\n\t\tmd5.setSalt(\"saltTest\".getBytes());\n\t\tString md5Hex1 = md5.digestHex(testStr);\n\t\tassertEquals(\"762f7335200299dfa09bebbb601a5bc6\", md5Hex1);\n\t\tString md5Hex2 = md5.digestHex(IoUtil.toUtf8Stream(testStr));\n\t\tassertEquals(\"762f7335200299dfa09bebbb601a5bc6\", md5Hex2);\n\n\t\t//重复2次\n\t\tmd5.setDigestCount(2);\n\t\tString md5Hex3 = md5.digestHex(testStr);\n\t\tassertEquals(\"2b0616296f6755d25efc07f90afe9684\", md5Hex3);\n\t\tString md5Hex4 = md5.digestHex(IoUtil.toUtf8Stream(testStr));\n\t\tassertEquals(\"2b0616296f6755d25efc07f90afe9684\", md5Hex4);\n\t}\n\n\t@Test\n\tpublic void sha1Test(){\n\t\tString testStr = \"test中文\";\n\n\t\tString sha1Hex1 = DigestUtil.sha1Hex(testStr);\n\t\tassertEquals(\"ecabf586cef0d3b11c56549433ad50b81110a836\", sha1Hex1);\n\n\t\tString sha1Hex2 = DigestUtil.sha1Hex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"ecabf586cef0d3b11c56549433ad50b81110a836\", sha1Hex2);\n\t}\n\n\t@Test\n\tpublic void hash256Test() {\n\t\tString testStr = \"Test中文\";\n\t\tString hex = DigestUtil.sha256Hex(testStr);\n\t\tassertEquals(64, hex.length());\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/HmacTest.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport cn.hutool.crypto.symmetric.ZUC;\nimport org.junit.jupiter.api.Test;\n\nimport javax.crypto.spec.IvParameterSpec;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Hmac单元测试\n * @author Looly\n *\n */\npublic class HmacTest {\n\n\t@Test\n\tpublic void hmacTest(){\n\t\tString testStr = \"test中文\";\n\n\t\tbyte[] key = \"password\".getBytes();\n\t\tHMac mac = new HMac(HmacAlgorithm.HmacMD5, key);\n\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"b977f4b13f93f549e06140971bded384\", macHex1);\n\n\t\tString macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"b977f4b13f93f549e06140971bded384\", macHex2);\n\t}\n\n\t@Test\n\tpublic void hmacMd5Test(){\n\t\tString testStr = \"test中文\";\n\n\t\tHMac mac = SecureUtil.hmacMd5(\"password\");\n\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"b977f4b13f93f549e06140971bded384\", macHex1);\n\n\t\tString macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"b977f4b13f93f549e06140971bded384\", macHex2);\n\t}\n\n\t@Test\n\tpublic void hmacSha1Test(){\n\t\tHMac mac = SecureUtil.hmacSha1(\"password\");\n\n\t\tString testStr = \"test中文\";\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"1dd68d2f119d5640f0d416e99d3f42408b88d511\", macHex1);\n\n\t\tString macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8));\n\t\tassertEquals(\"1dd68d2f119d5640f0d416e99d3f42408b88d511\", macHex2);\n\t}\n\n\t@Test\n\tpublic void zuc128MacTest(){\n\t\tbyte[] iv = new byte[16];\n\t\tfinal byte[] key = new byte[16];\n\t\tHMac mac = new HMac(\"ZUC-128\",\n\t\t\t\tKeyUtil.generateKey(ZUC.ZUCAlgorithm.ZUC_128.getValue(), key),\n\t\t\t\tnew IvParameterSpec(iv));\n\n\t\tString testStr = \"test中文\";\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"1e0b9455\", macHex1);\n\t}\n\n\t@Test\n\tpublic void zuc256MacTest(){\n\t\tfinal byte[] key = new byte[32];\n\t\tbyte[] iv = new byte[25];\n\t\tHMac mac = new HMac(\"ZUC-256\",\n\t\t\t\tKeyUtil.generateKey(ZUC.ZUCAlgorithm.ZUC_128.getValue(), key),\n\t\t\t\tnew IvParameterSpec(iv));\n\n\t\tString testStr = \"test中文\";\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"d9ad618357c1bfb1d9d1200a763d5eaa\", macHex1);\n\t}\n\n\t@Test\n\tpublic void sm4CMACTest(){\n\t\t// https://github.com/chinabugotech/hutool/issues/2206\n\t\tfinal byte[] key = new byte[16];\n\t\tHMac mac = new HMac(HmacAlgorithm.SM4CMAC,\n\t\t\t\tKeyUtil.generateKey(\"SM4\", key));\n\n\t\t// 原文\n\t\tString testStr = \"test中文\";\n\n\t\tString macHex1 = mac.digestHex(testStr);\n\t\tassertEquals(\"58a0d231315664af51b858a174eabc21\", macHex1);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/Md5Test.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ConcurrencyTester;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * MD5 单元测试\n *\n * @author Looly\n *\n */\npublic class Md5Test {\n\n\t@Test\n\tpublic void md5To16Test() {\n\t\tString hex16 = new MD5().digestHex16(\"中国\");\n\t\tassertEquals(16, hex16.length());\n\t\tassertEquals(\"cb143acd6c929826\", hex16);\n\t}\n\n\t@Test\n\tpublic void md5ThreadSafeTest() {\n\t\tfinal String text = \"Hutool md5 test str\";\n\t\tfinal ConcurrencyTester tester = new ConcurrencyTester(1000);\n\t\ttester.test(()->{\n\t\t\tfinal String digest = new MD5().digestHex(text);\n\t\t\tassertEquals(\"8060075dd8df47bac3247438e940a728\", digest);\n\t\t});\n\t\tIoUtil.close(tester);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/digest/OTPTest.java",
    "content": "package cn.hutool.crypto.digest;\n\nimport cn.hutool.core.codec.Base32;\nimport cn.hutool.crypto.digest.otp.HOTP;\nimport cn.hutool.crypto.digest.otp.TOTP;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Duration;\nimport java.time.Instant;\n\n/**\n * author: xlgogo@outlook.com\n * date: 2021-07-01 18:14\n */\npublic class OTPTest {\n\n\t@Test\n\tpublic void genKeyTest() {\n\t\tString key = TOTP.generateSecretKey(8);\n\t\tassertEquals(8, Base32.decode(key).length);\n\t}\n\n\t@Test\n\tpublic void validTest() {\n\t\tString key = \"VYCFSW2QZ3WZO\";\n\t\t// 2021/7/1下午6:29:54 显示code为 106659\n\t\t//assertEquals(new TOTP(Base32.decode(key)).generate(Instant.ofEpochSecond(1625135394L)),106659);\n\t\tTOTP totp = new TOTP(Base32.decode(key));\n\t\tInstant instant = Instant.ofEpochSecond(1625135394L);\n\t\tassertTrue(totp.validate(instant, 0, 106659));\n\t\tassertTrue(totp.validate(instant.plusSeconds(30), 1, 106659));\n\t\tassertTrue(totp.validate(instant.plusSeconds(60), 2, 106659));\n\n\t\tassertFalse(totp.validate(instant.plusSeconds(60), 1, 106659));\n\t\tassertFalse(totp.validate(instant.plusSeconds(90), 2, 106659));\n\t}\n\n\t@Test\n\tpublic void googleAuthTest() {\n\t\tString str = TOTP.generateGoogleSecretKey(\"xl7@qq.com\", 10);\n\t\tassertTrue(str.startsWith(\"otpauth://totp/xl7@qq.com?secret=\"));\n\t}\n\n\t@Test\n\tpublic void longPasswordLengthTest() {\n\t\tassertThrows(IllegalArgumentException.class, () -> new HOTP(9, \"123\".getBytes()));\n\t}\n\n\t@Test\n\tpublic void generateHOPTTest(){\n\t\tbyte[] key = \"12345678901234567890\".getBytes();\n\t\tfinal HOTP hotp = new HOTP(key);\n\t\tassertEquals(755224, hotp.generate(0));\n\t\tassertEquals(287082, hotp.generate(1));\n\t\tassertEquals(359152, hotp.generate(2));\n\t\tassertEquals(969429, hotp.generate(3));\n\t\tassertEquals(338314, hotp.generate(4));\n\t\tassertEquals(254676, hotp.generate(5));\n\t\tassertEquals(287922, hotp.generate(6));\n\t\tassertEquals(162583, hotp.generate(7));\n\t\tassertEquals(399871, hotp.generate(8));\n\t\tassertEquals(520489, hotp.generate(9));\n\t}\n\n\t@Test\n\tpublic void getTimeStepTest() {\n\t\tfinal Duration timeStep = Duration.ofSeconds(97);\n\n\t\tfinal TOTP totp = new TOTP(timeStep, \"123\".getBytes());\n\n\t\tassertEquals(timeStep, totp.getTimeStep());\n\t}\n\n\t@Test\n\tpublic void generateHmacSHA1TOPTTest(){\n\t\tHmacAlgorithm algorithm = HmacAlgorithm.HmacSHA1;\n\t\tbyte[] key = \"12345678901234567890\".getBytes();\n\t\tTOTP totp = new TOTP(Duration.ofSeconds(30), 8, algorithm, key);\n\n\t\tint generate = totp.generate(Instant.ofEpochSecond(59L));\n\t\tassertEquals(94287082, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111109L));\n\t\tassertEquals(7081804, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111111L));\n\t\tassertEquals(14050471, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1234567890L));\n\t\tassertEquals(89005924, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(2000000000L));\n\t\tassertEquals(69279037, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(20000000000L));\n\t\tassertEquals(65353130, generate);\n\t}\n\n\t@Test\n\tpublic void generateHmacSHA256TOPTTest(){\n\t\tHmacAlgorithm algorithm = HmacAlgorithm.HmacSHA256;\n\t\tbyte[] key = \"12345678901234567890123456789012\".getBytes();\n\t\tTOTP totp = new TOTP(Duration.ofSeconds(30), 8, algorithm, key);\n\n\t\tint generate = totp.generate(Instant.ofEpochSecond(59L));\n\t\tassertEquals(46119246, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111109L));\n\t\tassertEquals(68084774, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111111L));\n\t\tassertEquals(67062674, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1234567890L));\n\t\tassertEquals(91819424, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(2000000000L));\n\t\tassertEquals(90698825, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(20000000000L));\n\t\tassertEquals(77737706, generate);\n\t}\n\n\t@Test\n\tpublic void generateHmacSHA512TOPTTest(){\n\t\tHmacAlgorithm algorithm = HmacAlgorithm.HmacSHA512;\n\t\tbyte[] key = \"1234567890123456789012345678901234567890123456789012345678901234\".getBytes();\n\t\tTOTP totp = new TOTP(Duration.ofSeconds(30), 8, algorithm, key);\n\n\t\tint generate = totp.generate(Instant.ofEpochSecond(59L));\n\t\tassertEquals(90693936, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111109L));\n\t\tassertEquals(25091201, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1111111111L));\n\t\tassertEquals(99943326, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(1234567890L));\n\t\tassertEquals(93441116, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(2000000000L));\n\t\tassertEquals(38618901, generate);\n\t\tgenerate = totp.generate(Instant.ofEpochSecond(20000000000L));\n\t\tassertEquals(47863826, generate);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/AESTest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.HexUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.GCMParameterSpec;\nimport java.security.SecureRandom;\n\npublic class AESTest {\n\n\t@Test\n\tpublic void encryptCBCTest() {\n\t\t// 构建\n\t\tAES aes = new AES(Mode.CBC, Padding.PKCS5Padding,\n\t\t\t\t\"1234567890123456\".getBytes(), \"1234567890123456\".getBytes());\n\t\tString encryptHex = aes.encryptHex(\"123456\");\n\t\tassertEquals(\"d637735ae9e21ba50cb686b74fab8d2c\", encryptHex);\n\t}\n\n\t@Test\n\tpublic void encryptCTSTest() {\n\t\tString content = \"test中文\";\n\t\tAES aes = new AES(Mode.CTS, Padding.PKCS5Padding,\n\t\t\t\t\"0CoJUm6Qyw8W8jue\".getBytes(), \"0102030405060708\".getBytes());\n\t\tfinal String encryptHex = aes.encryptHex(content);\n\t\tassertEquals(\"8dc9de7f050e86ca2c8261dde56dfec9\", encryptHex);\n\t}\n\n\t@Test\n\tpublic void encryptPKCS7Test() {\n\t\t// 构建\n\t\tAES aes = new AES(Mode.CBC.name(), \"pkcs7padding\",\n\t\t\t\t\"1234567890123456\".getBytes(), \"1234567890123456\".getBytes());\n\t\tString encryptHex = aes.encryptHex(\"123456\");\n\t\tassertEquals(\"d637735ae9e21ba50cb686b74fab8d2c\", encryptHex);\n\t}\n\n\t/**\n\t * AES加密/解密\n\t * 加解密模式:ECB模式 数据填充模式:PKCS7\n\t * <p>\n\t * 数据：16c5\n\t * 密钥: 0102030405060708090a0b0c0d0e0f10\n\t * 数据格式:hex格式 加解密模式:ECB模式 数据填充模式:PKCS7\n\t * 结果: 25869eb3ff227d9e34b3512d3c3c92ed 【加密后的Hex】\n\t * 结果: JYaes/8ifZ40s1EtPDyS7Q== 【加密后的Base64】\n\t * <p>\n\t * 数据：16c5\n\t * 密钥: 0102030405060708090a0b0c0d0e0f10\n\t * 数据格式:UTF-8格式 加解密模式:ECB模式 数据填充模式:PKCS7\n\t * 结果: 79c210d3e304932cf9ea6a9c887c6d7c 【加密后的Hex】\n\t * 结果: ecIQ0+MEkyz56mqciHxtfA== 【加密后的Base64】\n\t * <p>\n\t * AES在线解密 AES在线加密 Aes online hex 十六进制密钥 - The X 在线工具\n\t * https://the-x.cn/cryptography/Aes.aspx\n\t */\n\t@Test\n\tpublic void encryptPKCS7Test2() {\n\t\t// 构建\n\t\tAES aes = new AES(Mode.ECB.name(), \"pkcs7padding\",\n\t\t\t\tHexUtil.decodeHex(\"0102030405060708090a0b0c0d0e0f10\"));\n\n\t\t// ------------------------------------------------------------------------\n\t\t// 加密数据为16进制字符串\n\t\tString encryptHex = aes.encryptHex(HexUtil.decodeHex(\"16c5\"));\n\t\t// 加密后的Hex\n\t\tassertEquals(\"25869eb3ff227d9e34b3512d3c3c92ed\", encryptHex);\n\n\t\t// 加密数据为16进制字符串\n\t\tString encryptHex2 = aes.encryptBase64(HexUtil.decodeHex(\"16c5\"));\n\t\t// 加密后的Base64\n\t\tassertEquals(\"JYaes/8ifZ40s1EtPDyS7Q==\", encryptHex2);\n\n\t\t// 解密\n\t\tassertEquals(\"16c5\", HexUtil.encodeHexStr(aes.decrypt(\"25869eb3ff227d9e34b3512d3c3c92ed\")));\n\t\tassertEquals(\"16c5\", HexUtil.encodeHexStr(aes.decrypt(HexUtil.encodeHexStr(Base64.decode(\"JYaes/8ifZ40s1EtPDyS7Q==\")))));\n\t\t// ------------------------------------------------------------------------\n\n\t\t// ------------------------------------------------------------------------\n\t\t// 加密数据为字符串(UTF-8)\n\t\tString encryptStr = aes.encryptHex(\"16c5\");\n\t\t// 加密后的Hex\n\t\tassertEquals(\"79c210d3e304932cf9ea6a9c887c6d7c\", encryptStr);\n\n\t\t// 加密数据为字符串(UTF-8)\n\t\tString encryptStr2 = aes.encryptBase64(\"16c5\");\n\t\t// 加密后的Base64\n\t\tassertEquals(\"ecIQ0+MEkyz56mqciHxtfA==\", encryptStr2);\n\n\t\t// 解密\n\t\tassertEquals(\"16c5\", aes.decryptStr(\"79c210d3e304932cf9ea6a9c887c6d7c\"));\n\t\tassertEquals(\"16c5\", aes.decryptStr(Base64.decode(\"ecIQ0+MEkyz56mqciHxtfA==\")));\n\t\t// ------------------------------------------------------------------------\n\t}\n\n\t@Test\n\tpublic void aesWithSha1PrngTest() {\n\t\tfinal SecureRandom random = RandomUtil.getSecureRandom(\"123456\".getBytes());\n\t\tfinal SecretKey secretKey = KeyUtil.generateKey(\"AES\", 128, random);\n\n\t\tString content = \"12sdfsdfs你好啊！\";\n\t\tAES aes = new AES(secretKey);\n\t\tfinal String result1 = aes.encryptBase64(content);\n\n\t\tfinal String decryptStr = aes.decryptStr(result1);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t/**\n\t * 见：https://blog.csdn.net/weixin_42468911/article/details/114358682\n\t */\n\t@Test\n\tpublic void gcmTest() {\n\t\tfinal SecretKey key = KeyUtil.generateKey(\"AES\");\n\t\tbyte[] iv = RandomUtil.randomBytes(12);\n\n\t\tAES aes = new AES(\"GCM\", \"NoPadding\",\n\t\t\t\tkey,\n\t\t\t\tnew GCMParameterSpec(128, iv));\n\n\t\t// 原始数据\n\t\tString phone = \"13534534567\";\n\t\t// 加密\n\t\tbyte[] encrypt = aes.encrypt(phone);\n\t\tfinal String decryptStr = aes.decryptStr(encrypt);\n\t\tassertEquals(phone, decryptStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/ChaCha20Test.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 见：https://stackoverflow.com/questions/32672241/using-bouncycastles-chacha-for-file-encryption\n */\npublic class ChaCha20Test {\n\n\t@Test\n\tpublic void encryptAndDecryptTest() {\n\t\t// 32 for 256 bit key or 16 for 128 bit\n\t\tbyte[] key = RandomUtil.randomBytes(32);\n\t\t// 64 bit IV required by ChaCha20\n\t\tbyte[] iv = RandomUtil.randomBytes(12);\n\n\t\tfinal ChaCha20 chacha = new ChaCha20(key, iv);\n\n\t\tString content = \"test中文\";\n\t\t// 加密为16进制表示\n\t\tString encryptHex = chacha.encryptHex(content);\n\t\t// 解密为字符串\n\t\tString decryptStr = chacha.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/DesTest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * DES加密解密单元测试\n */\npublic class DesTest {\n\n\t@Test\n\tpublic void encryptDecryptTest(){\n\t\tString content = \"我是一个测试的test字符串123\";\n\t\tfinal DES des = SecureUtil.des();\n\n\t\tfinal String encryptHex = des.encryptHex(content);\n\t\tfinal String result = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, result);\n\t}\n\n\t@Test\n\tpublic void encryptDecryptWithCustomTest(){\n\t\tString content = \"我是一个测试的test字符串123\";\n\t\tfinal DES des = new DES(\n\t\t\t\tMode.CTS,\n\t\t\t\tPadding.PKCS5Padding,\n\t\t\t\tStrUtil.bytes(\"12345678\"),\n\t\t\t\tStrUtil.bytes(\"11223344\")\n\t\t);\n\n\t\tfinal String encryptHex = des.encryptHex(content);\n\t\tfinal String result = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, result);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/PBKDF2Test.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class PBKDF2Test {\n\n\t@Test\n\tpublic void encryptTest(){\n\t\tfinal String s = SecureUtil.pbkdf2(\"123456\".toCharArray(), RandomUtil.randomBytes(16));\n\t\tassertEquals(128, s.length());\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/RC4Test.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class RC4Test {\n\n\t@Test\n\tpublic void testCryptMessage() {\n\t\tString key = \"This is pretty long key\";\n\t\tRC4 rc4 = new RC4(key);\n\t\tString message = \"Hello, World!\";\n\t\tbyte[] crypt = rc4.encrypt(message);\n\t\tString msg = rc4.decrypt(crypt);\n\t\tassertEquals(message, msg);\n\n\t\tString message2 = \"Hello, World， this is megssage 2\";\n\t\tbyte[] crypt2 = rc4.encrypt(message2);\n\t\tString msg2 = rc4.decrypt(crypt2);\n\t\tassertEquals(message2, msg2);\n\t}\n\n\t@Test\n\tpublic void testCryptWithChineseCharacters() {\n\t\tString message = \"这是一个中文消息！\";\n\t\tString key = \"我是一个文件密钥\";\n\t\tRC4 rc4 = new RC4(key);\n\t\tbyte[] crypt = rc4.encrypt(message);\n\t\tString msg = rc4.decrypt(crypt);\n\t\tassertEquals(message, msg);\n\n\t\tString message2 = \"这是第二个中文消息！\";\n\t\tbyte[] crypt2 = rc4.encrypt(message2);\n\t\tString msg2 = rc4.decrypt(crypt2);\n\t\tassertEquals(message2, msg2);\n\t}\n\n\t@Test\n\tpublic void testDecryptWithHexMessage() {\n\t\tString message = \"这是第一个用来测试密文为十六进制字符串的消息！\";\n\t\tString key = \"生成一个密钥\";\n\t\tRC4 rc4 = new RC4(key);\n\t\tString encryptHex = rc4.encryptHex(message, CharsetUtil.CHARSET_UTF_8);\n\t\tString msg = rc4.decrypt(encryptHex);\n\t\tassertEquals(message, msg);\n\n\t\tString message2 = \"这是第二个用来测试密文为十六进制字符串的消息！\";\n\t\tString encryptHex2 = rc4.encryptHex(message2);\n\t\tString msg2 = rc4.decrypt(encryptHex2);\n\t\tassertEquals(message2, msg2);\n\t}\n\n\n\t@Test\n\tpublic void testDecryptWithBase64Message() {\n\t\tString message = \"这是第一个用来测试密文为Base64编码的消息！\";\n\t\tString key = \"生成一个密钥\";\n\t\tRC4 rc4 = new RC4(key);\n\t\tString encryptHex = rc4.encryptBase64(message, CharsetUtil.CHARSET_UTF_8);\n\t\tString msg = rc4.decrypt(encryptHex);\n\t\tassertEquals(message, msg);\n\n\t\tString message2 = \"这是第一个用来测试密文为Base64编码的消息！\";\n\t\tString encryptHex2 = rc4.encryptBase64(message2);\n\t\tString msg2 = rc4.decrypt(encryptHex2);\n\t\tassertEquals(message2, msg2);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/Sm4StreamTest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I4EMST\n */\npublic class Sm4StreamTest {\n\n\tprivate static final SM4 sm4 = new SM4();\n\n\tprivate static final boolean IS_CLOSE = false;\n\n\t@Test\n\t@Disabled\n\tpublic void sm4Test(){\n\t\tString source = \"d:/test/sm4_1.txt\";\n\t\tString target = \"d:/test/sm4_2.data\";\n\t\tString target2 = \"d:/test/sm4_3.txt\";\n\t\tencrypt(source, target);\n\t\tdecrypt(target, target2);\n\t}\n\n\tpublic static void encrypt(String source, String target) {\n\t\ttry (InputStream input = new FileInputStream(source);\n\t\t\t OutputStream out = new FileOutputStream(target)) {\n\t\t\tsm4.encrypt(input, out, IS_CLOSE);\n\t\t\tSystem.out.println(\"============encrypt end\");\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\tpublic static void decrypt(String source, String target) {\n\t\ttry (InputStream input = new FileInputStream(source);\n\t\t\t OutputStream out = new FileOutputStream(target)) {\n\t\t\tsm4.decrypt(input, out, IS_CLOSE);\n\t\t\tSystem.out.println(\"============decrypt end\");\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/SymmetricTest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.CipherMode;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.crypto.Mode;\nimport cn.hutool.crypto.Padding;\nimport cn.hutool.crypto.SecureUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * 对称加密算法单元测试\n *\n * @author Looly\n */\npublic class SymmetricTest {\n\n\t@Test\n\tpublic void aesTest() {\n\t\tString content = \"test中文\";\n\n\t\t// 随机生成密钥\n\t\tbyte[] key = KeyUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded();\n\n\t\t// 构建\n\t\tSymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key);\n\n\t\t// 加密\n\t\tbyte[] encrypt = aes.encrypt(content);\n\t\t// 解密\n\t\tbyte[] decrypt = aes.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\t\t// 解密为字符串\n\t\tString decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void aesTest2() {\n\t\tString content = \"test中文\";\n\n\t\t// 随机生成密钥\n\t\tbyte[] key = KeyUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded();\n\n\t\t// 构建\n\t\tAES aes = SecureUtil.aes(key);\n\n\t\t// 加密\n\t\tbyte[] encrypt = aes.encrypt(content);\n\t\t// 解密\n\t\tbyte[] decrypt = aes.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\t\t// 解密为字符串\n\t\tString decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void aesTest3() {\n\t\tString content = \"test中文aaaaaaaaaaaaaaaaaaaaa\";\n\n\t\tAES aes = new AES(Mode.CTS, Padding.PKCS5Padding, \"0CoJUm6Qyw8W8jud\".getBytes(), \"0102030405060708\".getBytes());\n\n\t\t// 加密\n\t\tbyte[] encrypt = aes.encrypt(content);\n\t\t// 解密\n\t\tbyte[] decrypt = aes.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\t\t// 解密为字符串\n\t\tString decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void aesTest4() {\n\t\tString content = \"4321c9a2db2e6b08987c3b903d8d11ff\";\n\t\tAES aes = new AES(Mode.CBC, Padding.PKCS5Padding, \"0123456789ABHAEQ\".getBytes(), \"DYgjCEIMVrj2W9xN\".getBytes());\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\n\t\tassertEquals(\"cd0e3a249eaf0ed80c330338508898c4bddcfd665a1b414622164a273ca5daf7b4ebd2c00aaa66b84dd0a237708dac8e\", encryptHex);\n\t}\n\n\t@Test\n\tpublic void pbeWithoutIvTest() {\n\t\tString content = \"4321c9a2db2e6b08987c3b903d8d11ff\";\n\t\tSymmetricCrypto crypto = new SymmetricCrypto(SymmetricAlgorithm.PBEWithMD5AndDES,\n\t\t\t\t\"0123456789ABHAEQ\".getBytes());\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = crypto.encryptHex(content);\n\t\tfinal String data = crypto.decryptStr(encryptHex);\n\n\t\tassertEquals(content, data);\n\t}\n\n\t@Test\n\tpublic void aesUpdateTest() {\n\t\tString content = \"4321c9a2db2e6b08987c3b903d8d11ff\";\n\t\tAES aes = new AES(Mode.CBC, Padding.PKCS5Padding, \"0123456789ABHAEQ\".getBytes(), \"DYgjCEIMVrj2W9xN\".getBytes());\n\n\t\t// 加密为16进制表示\n\t\taes.setMode(CipherMode.encrypt);\n\t\tString randomData = aes.updateHex(content.getBytes(StandardCharsets.UTF_8));\n\t\taes.setMode(CipherMode.encrypt);\n\t\tString randomData2 = aes.updateHex(content.getBytes(StandardCharsets.UTF_8));\n\t\tassertEquals(randomData2, randomData);\n\t\tassertEquals(randomData, \"cd0e3a249eaf0ed80c330338508898c4\");\n\t}\n\n\n\t@Test\n\tpublic void aesZeroPaddingTest() {\n\t\tString content = RandomUtil.randomString(RandomUtil.randomInt(200));\n\t\tAES aes = new AES(Mode.CBC, Padding.ZeroPadding, \"0123456789ABHAEQ\".getBytes(), \"DYgjCEIMVrj2W9xN\".getBytes());\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\t\t// 解密\n\t\tString decryptStr = aes.decryptStr(encryptHex);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void aesZeroPaddingTest2() {\n\t\tString content = \"RandomUtil.randomString(RandomUtil.randomInt(2000))\";\n\t\tAES aes = new AES(Mode.CBC, Padding.ZeroPadding, \"0123456789ABHAEQ\".getBytes(), \"DYgjCEIMVrj2W9xN\".getBytes());\n\n\t\tfinal ByteArrayOutputStream encryptStream = new ByteArrayOutputStream();\n\t\taes.encrypt(IoUtil.toUtf8Stream(content), encryptStream, true);\n\n\t\tfinal ByteArrayOutputStream contentStream = new ByteArrayOutputStream();\n\t\taes.decrypt(IoUtil.toStream(encryptStream), contentStream, true);\n\n\t\tassertEquals(content, StrUtil.utf8Str(contentStream.toByteArray()));\n\t}\n\n\t@Test\n\tpublic void aesPkcs7PaddingTest() {\n\t\tString content = RandomUtil.randomString(RandomUtil.randomInt(200));\n\t\tAES aes = new AES(\"CBC\", \"PKCS7Padding\",\n\t\t\t\tRandomUtil.randomBytes(32),\n\t\t\t\t\"DYgjCEIMVrj2W9xN\".getBytes());\n\n\t\t// 加密为16进制表示\n\t\tString encryptHex = aes.encryptHex(content);\n\t\t// 解密\n\t\tString decryptStr = aes.decryptStr(encryptHex);\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void desTest() {\n\t\tString content = \"test中文\";\n\n\t\tbyte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue()).getEncoded();\n\n\t\tSymmetricCrypto des = new SymmetricCrypto(SymmetricAlgorithm.DES, key);\n\t\tbyte[] encrypt = des.encrypt(content);\n\t\tbyte[] decrypt = des.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\tString encryptHex = des.encryptHex(content);\n\t\tString decryptStr = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void desTest2() {\n\t\tString content = \"test中文\";\n\n\t\tbyte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue()).getEncoded();\n\n\t\tDES des = SecureUtil.des(key);\n\t\tbyte[] encrypt = des.encrypt(content);\n\t\tbyte[] decrypt = des.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\tString encryptHex = des.encryptHex(content);\n\t\tString decryptStr = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void desTest3() {\n\t\tString content = \"test中文\";\n\n\t\tDES des = new DES(Mode.CTS, Padding.PKCS5Padding, \"0CoJUm6Qyw8W8jud\".getBytes(), \"01020304\".getBytes());\n\n\t\tbyte[] encrypt = des.encrypt(content);\n\t\tbyte[] decrypt = des.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\tString encryptHex = des.encryptHex(content);\n\t\tString decryptStr = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void desdeTest() {\n\t\tString content = \"test中文\";\n\n\t\tbyte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue()).getEncoded();\n\n\t\tDESede des = SecureUtil.desede(key);\n\n\t\tbyte[] encrypt = des.encrypt(content);\n\t\tbyte[] decrypt = des.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\tString encryptHex = des.encryptHex(content);\n\t\tString decryptStr = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void desdeTest2() {\n\t\tString content = \"test中文\";\n\n\t\tbyte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue()).getEncoded();\n\n\t\tDESede des = new DESede(Mode.CBC, Padding.PKCS5Padding, key, \"12345678\".getBytes());\n\n\t\tbyte[] encrypt = des.encrypt(content);\n\t\tbyte[] decrypt = des.decrypt(encrypt);\n\n\t\tassertEquals(content, StrUtil.utf8Str(decrypt));\n\n\t\tString encryptHex = des.encryptHex(content);\n\t\tString decryptStr = des.decryptStr(encryptHex);\n\n\t\tassertEquals(content, decryptStr);\n\t}\n\n\t@Test\n\tpublic void vigenereTest() {\n\t\tString content = \"Wherethereisawillthereisaway\";\n\t\tString key = \"CompleteVictory\";\n\n\t\tString encrypt = Vigenere.encrypt(content, key);\n\t\tassertEquals(\"zXScRZ]KIOMhQjc0\\\\bYRXZOJK[Vi\", encrypt);\n\t\tString decrypt = Vigenere.decrypt(encrypt, key);\n\t\tassertEquals(content, decrypt);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/TEATest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * TEA（Tiny Encryption Algorithm）和 XTEA算法单元测试\n */\npublic class TEATest {\n\n\t@Test\n\tpublic void teaTest() {\n\t\tString data = \"测试的加密数据 by Hutool\";\n\n\t\t// 密钥必须为128bit\n\t\tfinal SymmetricCrypto tea = new SymmetricCrypto(\"TEA\", \"MyPassword123456\".getBytes());\n\t\tfinal byte[] encrypt = tea.encrypt(data);\n\n\t\t// 解密\n\t\tfinal String decryptStr = tea.decryptStr(encrypt);\n\n\t\tassertEquals(data, decryptStr);\n\t}\n\n\t@Test\n\tpublic void xteaTest() {\n\t\tString data = \"测试的加密数据 by Hutool\";\n\n\t\t// 密钥必须为128bit\n\t\tfinal SymmetricCrypto tea = new SymmetricCrypto(\"XTEA\", \"MyPassword123456\".getBytes());\n\t\tfinal byte[] encrypt = tea.encrypt(data);\n\n\t\t// 解密\n\t\tfinal String decryptStr = tea.decryptStr(encrypt);\n\n\t\tassertEquals(data, decryptStr);\n\t}\n\n\t@Test\n\tpublic void xxteaTest() {\n\t\tString data = \"测试的加密数据 by Hutool\";\n\n\t\t// 密钥必须为128bit\n\t\tfinal XXTEA tea = new XXTEA(\"MyPassword123456\".getBytes());\n\t\tfinal byte[] encrypt = tea.encrypt(data);\n\n\t\t// 解密\n\t\tfinal String decryptStr = tea.decryptStr(encrypt);\n\n\t\tassertEquals(data, decryptStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/ZucTest.java",
    "content": "package cn.hutool.crypto.symmetric;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ZucTest {\n\n\t@Test\n\tpublic void zuc128Test(){\n\t\tfinal byte[] secretKey = ZUC.generateKey(ZUC.ZUCAlgorithm.ZUC_128);\n\t\tbyte[] iv = RandomUtil.randomBytes(16);\n\t\tfinal ZUC zuc = new ZUC(ZUC.ZUCAlgorithm.ZUC_128, secretKey, iv);\n\n\t\tString msg = RandomUtil.randomString(500);\n\t\tbyte[] crypt2 = zuc.encrypt(msg);\n\t\tString msg2 = zuc.decryptStr(crypt2, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(msg, msg2);\n\t}\n\n\t@Test\n\tpublic void zuc256Test(){\n\t\tfinal byte[] secretKey = ZUC.generateKey(ZUC.ZUCAlgorithm.ZUC_256);\n\t\tbyte[] iv = RandomUtil.randomBytes(25);\n\t\tfinal ZUC zuc = new ZUC(ZUC.ZUCAlgorithm.ZUC_256, secretKey, iv);\n\n\t\tString msg = RandomUtil.randomString(500);\n\t\tbyte[] crypt2 = zuc.encrypt(msg);\n\t\tString msg2 = zuc.decryptStr(crypt2, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(msg, msg2);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/java/cn/hutool/crypto/symmetric/fpe/FPETest.java",
    "content": "package cn.hutool.crypto.symmetric.fpe;\n\nimport cn.hutool.core.util.RandomUtil;\nimport org.bouncycastle.crypto.util.BasicAlphabetMapper;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class FPETest {\n\n\t@Test\n\tpublic void ff1Test(){\n\t\t// 映射字符表，规定了明文和密文的字符范围\n\t\tBasicAlphabetMapper numberMapper = new BasicAlphabetMapper(\"A0123456789\");\n\t\t// 初始化 aes 密钥\n\t\tbyte[] keyBytes = RandomUtil.randomBytes(16);\n\n\t\tfinal FPE fpe = new FPE(FPE.FPEMode.FF1, keyBytes, numberMapper, null);\n\n\t\t// 原始数据\n\t\tString phone = RandomUtil.randomString(\"A0123456789\", 13);\n\t\tfinal String encrypt = fpe.encrypt(phone);\n\t\t// 加密后与原密文长度一致\n\t\tassertEquals(phone.length(), encrypt.length());\n\n\t\tfinal String decrypt = fpe.decrypt(encrypt);\n\t\tassertEquals(phone, decrypt);\n\t}\n\n\t@Test\n\tpublic void ff3Test(){\n\t\t// 映射字符表，规定了明文和密文的字符范围\n\t\tBasicAlphabetMapper numberMapper = new BasicAlphabetMapper(\"A0123456789\");\n\t\t// 初始化 aes 密钥\n\t\tbyte[] keyBytes = RandomUtil.randomBytes(16);\n\n\t\tfinal FPE fpe = new FPE(FPE.FPEMode.FF3_1, keyBytes, numberMapper, null);\n\n\t\t// 原始数据\n\t\tString phone = RandomUtil.randomString(\"A0123456789\", 13);\n\t\tfinal String encrypt = fpe.encrypt(phone);\n\t\t// 加密后与原密文长度一致\n\t\tassertEquals(phone.length(), encrypt.length());\n\n\t\tfinal String decrypt = fpe.decrypt(encrypt);\n\t\tassertEquals(phone, decrypt);\n\t}\n}\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_certificate.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBozCCAUegAwIBAgIIJQJK8f5oQVQwDAYIKoEcz1UBg3UFADAjMQswCQYDVQQG\nEwJDTjEUMBIGA1UEAwwLSHV0b29sIFRlc3QwHhcNMjIwNzE3MDcyMzU4WhcNMjMw\nNzE3MDcyMzU4WjAjMQswCQYDVQQGEwJDTjEUMBIGA1UEAwwLSHV0b29sIFRlc3Qw\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASXXjbO+6vY7vdD5aXoi2EMHUq0itI8\nkG6FN3cgLBFFoelyy3JxX94h7RpH4ylpNUXeRNuzv1VcPa06nsN1OjTWo2MwYTAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUFHvMM9CZ\nVUxWKt1+CHI5uYLpLgQwHwYDVR0jBBgwFoAUFHvMM9CZVUxWKt1+CHI5uYLpLgQw\nDAYIKoEcz1UBg3UFAANIADBFAiA5p0Gh7mvuuHMVG2SmbGj5HQpWcpwCaUF90BQ9\n/QEYZgIhAIXmeD2bDlOCPc3vxS4aNGxSd1wq/MT9bBJhKyiXWjx5\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_certificate_request.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBODCB3wIBADBWMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHQmVpamluZzEQMA4G\nA1UEBxMHQmVpamluZzEPMA0GA1UECxMGSHV0b29sMRIwEAYDVQQDEwlodXRvb2wu\nY24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASXXjbO+6vY7vdD5aXoi2EMHUq0\nitI8kG6FN3cgLBFFoelyy3JxX94h7RpH4ylpNUXeRNuzv1VcPa06nsN1OjTWoCcw\nJQYJKoZIhvcNAQkOMRgwFjAUBgNVHREEDTALgglodXRvb2wuY24wCgYIKoZIzj0E\nAwIDSAAwRQIhAIH0w1XbGnRfbM1flsqRHzfur+pEZ3wiqpChxAI39lpIAiAsNAwn\no7PsXpTXLhq1ZgqurIjFeFuvY1hszUODmO5ySQ==\n-----END CERTIFICATE REQUEST-----"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_encrypted_private_key.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,04b396b78c1f1172857a8b76682b63ef\n\nNoaLaSy87gLE6C3yCi7JwiB86NSYipZmMVlLIcHaBL2ECRUcGDmXEZu6OqFyrbDc\nXWXraEl3OieYduiVmuJ0GQ8oeWd5DNgHLBYTPnfgjBowbluAO9/R9AUh4R8Fz918\n/zsMZJckjSv3Gs6NWZW02v9OvhTDSJBNwu/M2WTWH10=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_pkcs8_private_key.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgEZmc6NdYu8UzpzhX\n1bcRzfWBlcGgnPtqfVsRzXfXZ/6gCgYIKoZIzj0DAQehRANCAASXXjbO+6vY7vdD\n5aXoi2EMHUq0itI8kG6FN3cgLBFFoelyy3JxX94h7RpH4ylpNUXeRNuzv1VcPa06\nnsN1OjTW\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_public_key.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl142zvur2O73Q+Wl6IthDB1KtIrS\nPJBuhTd3ICwRRaHpcstycV/eIe0aR+MpaTVF3kTbs79VXD2tOp7DdTo01g==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_ec_sec1_private_key.pem",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBGZnOjXWLvFM6c4V9W3Ec31gZXBoJz7an1bEc1312f+oAoGCCqGSM49\nAwEHoUQDQgAEl142zvur2O73Q+Wl6IthDB1KtIrSPJBuhTd3ICwRRaHpcstycV/e\nIe0aR+MpaTVF3kTbs79VXD2tOp7DdTo01g==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_private_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQC3G09zGCmlMetvNaWiHbp9d8jItkj5ik0wKcn7jBy/eOdlno5m\ny+eijTP/KX8D2QNj2vlF+31/AThYoxI80qUZ3imw8vDVc0cBeGLxEDLVweCQEy7C\nivpkmEWCeyoqThenrwONoIjajG7ZJggFTrsXHL6HsbkGxYrABG2PmQ/w0QIDAQAB\nAoGBAIxvTcggSBCC8OciZh6oXlfMfxoxdFavU/QUmO1s0L+pow+1Q9JjoQxy7+ZL\nlTcGQitbzsN11xKJhQW2TE6J4EVimJZQSAE4DDmYpMOrkjnBQhkUlaZkkukvDSRS\nJqwBI/04G7se+RouHyXjRS9U76HnPM8+/IS2h+T6CbXLOpYBAkEA2j0JmyGVs+WV\nI9sG5glamJqTBa4CfTORrdFW4EULoGkUc24ZFFqn9W4e5yfl/pCkPptCenvIrAWp\n/ymnHeLn6QJBANbKGO9uBizAt4+o+kHYdANcbU/Cs3PLj8yOOtjkuMbH4tPNQmB6\n/u3npiVk7/Txfkg0BjRzDDZib109eKbvGKkCQBgMneBghRS7+gFng40Z/sfOUOFR\nWajeY/FZnk88jJlyuvQ1b8IUc2nSZslmViwFWHQlu9+vgF+kiCU8O9RJSvECQQCl\nVkx7giYerPqgC2MY7JXhQHSkwSuCJ2A6BgImk2npGlTw1UATJJq4Z2jtwBU2Z+7d\nha6BEU6FTqCLFZaaadKBAkEAxko4hrgBsX9BKpFJE3aUIUcMTJfJQdiAhq0k4DV8\n5GVrcp8zl6mUTPZDaOmDhuAjGdAQJqj0Xo0PZ0fOZPtR+w==\n-----END RSA PRIVATE KEY-----"
  },
  {
    "path": "hutool-crypto/src/test/resources/test_public_key.csr",
    "content": "-----BEGIN CERTIFICATE-----\nMIICYTCCAcoCCQCs45mePIbzRTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV\nUzENMAsGA1UECAwETWFyczETMBEGA1UEBwwKaVRyYW5zd2FycDETMBEGA1UECgwK\naVRyYW5zd2FycDETMBEGA1UECwwKaVRyYW5zd2FycDEYMBYGA1UEAwwPd3d3LjU5\nMXdpZmkuY29tMB4XDTE4MTAxNzAyMTA0OFoXDTI4MTAxNDAyMTA0OFowdTELMAkG\nA1UEBhMCVVMxDTALBgNVBAgMBE1hcnMxEzARBgNVBAcMCmlUcmFuc3dhcnAxEzAR\nBgNVBAoMCmlUcmFuc3dhcnAxEzARBgNVBAsMCmlUcmFuc3dhcnAxGDAWBgNVBAMM\nD3d3dy41OTF3aWZpLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtxtP\ncxgppTHrbzWloh26fXfIyLZI+YpNMCnJ+4wcv3jnZZ6OZsvnoo0z/yl/A9kDY9r5\nRft9fwE4WKMSPNKlGd4psPLw1XNHAXhi8RAy1cHgkBMuwor6ZJhFgnsqKk4Xp68D\njaCI2oxu2SYIBU67Fxy+h7G5BsWKwARtj5kP8NECAwEAATANBgkqhkiG9w0BAQUF\nAAOBgQC2Pko8q1NicJ0oPuhFTPm7n03LtPhCaV/aDf3mqtGxraYifg8iFTxVyZ1c\nol0eEJFsibrQrPEwdSuSVqzwif5Tab9dV92PPFm+Sq0D1Uc0xI4ziXQ+a55K9wrV\nTKXxS48TOpnTA8fVFNkUkFNB54Lhh9AwKsx123kJmyaWccbt9Q==\n-----END CERTIFICATE-----"
  },
  {
    "path": "hutool-db/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-db</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 数据库JDBC的ORM封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.db</Automatic-Module-Name>\n\n\t\t<!-- versions -->\n\t\t<c3p0.version>0.12.0</c3p0.version>\n\t\t<dbcp2.version>2.12.0</dbcp2.version>\n\t\t<tomcat-jdbc.version>10.0.23</tomcat-jdbc.version>\n\t\t<druid.version>1.2.23</druid.version>\n\t\t<!-- 此处固定4.x，支持到JDK8 -->\n\t\t<hikariCP.version>4.0.3</hikariCP.version>\n\t\t<mongo4.version>4.9.1</mongo4.version>\n\t\t<sqlite.version>3.46.0.1</sqlite.version>\n\t\t<!-- 此处固定2.5.x，支持到JDK8 -->\n\t\t<hsqldb.version>2.5.2</hsqldb.version>\n\t\t<jedis.version>5.0.2</jedis.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 数据库连接池 -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.tomcat</groupId>\n\t\t\t<artifactId>tomcat-jdbc</artifactId>\n\t\t\t<version>${tomcat-jdbc.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba</groupId>\n\t\t\t<artifactId>druid</artifactId>\n\t\t\t<version>${druid.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.zaxxer</groupId>\n\t\t\t<artifactId>HikariCP</artifactId>\n\t\t\t<version>${hikariCP.version}</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.mchange</groupId>\n\t\t\t<artifactId>c3p0</artifactId>\n\t\t\t<version>${c3p0.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.chris2018998</groupId>\n\t\t\t<artifactId>beecp</artifactId>\n\t\t\t<version>3.5.0</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t<artifactId>commons-dbcp2</artifactId>\n\t\t\t<version>${dbcp2.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.mongodb</groupId>\n\t\t\t<artifactId>mongodb-driver-sync</artifactId>\n\t\t\t<version>${mongo4.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- Redis Java客户端 -->\n\t\t<dependency>\n\t\t\t<groupId>redis.clients</groupId>\n\t\t\t<artifactId>jedis</artifactId>\n\t\t\t<version>${jedis.version}</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>commons-pool2</artifactId>\n\t\t\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 测试用依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.xerial</groupId>\n\t\t\t<artifactId>sqlite-jdbc</artifactId>\n\t\t\t<version>${sqlite.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.hsqldb</groupId>\n\t\t\t<artifactId>hsqldb</artifactId>\n\t\t\t<version>${hsqldb.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.mysql</groupId>\n\t\t\t<artifactId>mysql-connector-j</artifactId>\n\t\t\t<version>8.3.0</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.postgresql</groupId>\n\t\t\t<artifactId>postgresql</artifactId>\n\t\t\t<version>42.7.3</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.microsoft.sqlserver</groupId>\n\t\t\t<artifactId>mssql-jdbc</artifactId>\n\t\t\t<version>13.2.1.jre8</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t<artifactId>slf4j-simple</artifactId>\n\t\t\t<version>1.7.36</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<!-- H2数据库，版本固定2.2.x ，支持到JDK8 -->\n\t\t<dependency>\n\t\t\t<groupId>com.h2database</groupId>\n\t\t\t<artifactId>h2</artifactId>\n\t\t\t<version>2.2.224</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.clickhouse</groupId>\n\t\t\t<artifactId>clickhouse-jdbc</artifactId>\n\t\t\t<version>0.6.3</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.dameng</groupId>\n\t\t\t<artifactId>DmJdbcDriver18</artifactId>\n\t\t\t<version>8.1.3.140</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.oracle.database.jdbc</groupId>\n\t\t\t<artifactId>ojdbc8</artifactId>\n\t\t\t<version>23.5.0.24.07</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.sap.cloud.db.jdbc</groupId>\n\t\t\t<artifactId>ngdbc</artifactId>\n\t\t\t<version>2.24.7</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/AbstractDb.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.handler.*;\nimport cn.hutool.db.sql.Condition;\nimport cn.hutool.db.sql.Condition.LikeType;\nimport cn.hutool.db.sql.LogicalOperator;\nimport cn.hutool.db.sql.Query;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.SqlExecutor;\nimport cn.hutool.db.sql.SqlUtil;\nimport cn.hutool.db.sql.Wrapper;\n\nimport javax.sql.DataSource;\nimport java.io.Serializable;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 抽象数据库操作类<br>\n * 通过给定的数据源执行给定SQL或者给定数据源和方言，执行相应的CRUD操作<br>\n * 提供抽象方法getConnection和closeConnection，用于自定义数据库连接的打开和关闭\n *\n * @author Luxiaolei\n */\npublic abstract class AbstractDb implements Serializable {\n\tprivate static final long serialVersionUID = 3858951941916349062L;\n\n\tprotected final DataSource ds;\n\t/**\n\t * 是否支持事务\n\t */\n\tprotected Boolean isSupportTransaction = null;\n\t/**\n\t * 是否大小写不敏感（默认大小写不敏感）\n\t */\n\tprotected boolean caseInsensitive = GlobalDbConfig.caseInsensitive;\n\tprotected SqlConnRunner runner;\n\n\t// ------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds      数据源\n\t * @param dialect 数据库方言\n\t */\n\tpublic AbstractDb(DataSource ds, Dialect dialect) {\n\t\tthis.ds = ds;\n\t\tthis.runner = new SqlConnRunner(dialect);\n\t}\n\t// ------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获得数据源\n\t *\n\t * @return 数据源\n\t * @since 5.8.33\n\t */\n\tpublic DataSource getDs() {\n\t\treturn ds;\n\t}\n\n\t/**\n\t * 获得链接。根据实现不同，可以自定义获取连接的方式\n\t *\n\t * @return {@link Connection}\n\t * @throws SQLException 连接获取异常\n\t */\n\tpublic abstract Connection getConnection() throws SQLException;\n\n\t/**\n\t * 关闭连接<br>\n\t * 自定义关闭连接有利于自定义回收连接机制，或者不关闭\n\t *\n\t * @param conn 连接 {@link Connection}\n\t */\n\tpublic abstract void closeConnection(Connection conn);\n\n\t/**\n\t * 查询\n\t *\n\t * @param sql    查询语句\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.1.1\n\t */\n\tpublic List<Entity> query(String sql, Map<String, Object> params) throws SQLException {\n\t\treturn query(sql, new EntityListHandler(this.caseInsensitive), params);\n\t}\n\n\t/**\n\t * 查询\n\t *\n\t * @param sql    查询语句\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.1.1\n\t */\n\tpublic List<Entity> query(String sql, Object... params) throws SQLException {\n\t\treturn query(sql, new EntityListHandler(this.caseInsensitive), params);\n\t}\n\n\t/**\n\t * 查询\n\t *\n\t * @param <T>       结果集需要处理的对象类型\n\t * @param sql       查询语句\n\t * @param beanClass 元素Bean类型\n\t * @param params    参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic <T> List<T> query(String sql, Class<T> beanClass, Object... params) throws SQLException {\n\t\treturn query(sql, new BeanListHandler<>(beanClass), params);\n\t}\n\n\t/**\n\t * 查询单条记录\n\t *\n\t * @param sql    查询语句\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Entity queryOne(String sql, Object... params) throws SQLException {\n\t\treturn query(sql, new EntityHandler(this.caseInsensitive), params);\n\t}\n\n\t/**\n\t * 查询单条单个字段记录,并将其转换为Number\n\t *\n\t * @param sql    查询语句\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Number queryNumber(String sql, Object... params) throws SQLException {\n\t\treturn query(sql, new NumberHandler(), params);\n\t}\n\n\t/**\n\t * 查询单条单个字段记录,并将其转换为String\n\t *\n\t * @param sql    查询语句\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic String queryString(String sql, Object... params) throws SQLException {\n\t\treturn query(sql, new StringHandler(), params);\n\t}\n\n\t/**\n\t * 查询\n\t *\n\t * @param <T>    结果集需要处理的对象类型\n\t * @param sql    查询语句\n\t * @param rsh    结果集处理对象\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T query(String sql, RsHandler<T> rsh, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.query(conn, sql, rsh, params);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 支持占位符的查询，例如：select * from table where field1=:name1\n\t *\n\t * @param <T>      结果集需要处理的对象类型\n\t * @param sql      查询语句，使用参数名占位符，例如:name\n\t * @param rsh      结果集处理对象\n\t * @param paramMap 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.2.2\n\t */\n\tpublic <T> T query(String sql, RsHandler<T> rsh, Map<String, Object> paramMap) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.query(conn, sql, rsh, paramMap);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 执行自定义的{@link PreparedStatement}，结果使用{@link RsHandler}处理<br>\n\t * 此方法主要用于自定义场景，如游标查询等\n\t *\n\t * @param <T>           结果集需要处理的对象类型\n\t * @param statementFunc 自定义{@link PreparedStatement}创建函数\n\t * @param rsh           结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.17\n\t */\n\tpublic <T> T query(Func1<Connection, PreparedStatement> statementFunc, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.query(conn, statementFunc, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 执行非查询语句<br>\n\t * 语句包括 插入、更新、删除\n\t *\n\t * @param sql    SQL\n\t * @param params 参数\n\t * @return 影响行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int execute(String sql, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.execute(conn, sql, params);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 执行非查询语句<br>\n\t * 语句包括 插入、更新、删除\n\t *\n\t * @param sql    SQL\n\t * @param params 参数\n\t * @return 主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Long executeForGeneratedKey(String sql, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.executeForGeneratedKey(conn, sql, params);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句\n\t *\n\t * @param sql         SQL\n\t * @param paramsBatch 批量的参数\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @deprecated 编译器无法区分重载\n\t */\n\t@Deprecated\n\tpublic int[] executeBatch(String sql, Object[]... paramsBatch) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.executeBatch(conn, sql, paramsBatch);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句\n\t *\n\t * @param sql         SQL\n\t * @param paramsBatch 批量的参数\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.4.2\n\t */\n\tpublic int[] executeBatch(String sql, Iterable<Object[]> paramsBatch) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.executeBatch(conn, sql, paramsBatch);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句\n\t *\n\t * @param sqls SQL列表\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.6\n\t */\n\tpublic int[] executeBatch(String... sqls) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.executeBatch(conn, sqls);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句\n\t *\n\t * @param sqls SQL列表\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.4.2\n\t */\n\tpublic int[] executeBatch(Iterable<String> sqls) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn SqlExecutor.executeBatch(conn, sqls);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------- CRUD start\n\n\t/**\n\t * 插入数据\n\t *\n\t * @param record 记录\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int insert(Entity record) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.insert(conn, record);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 插入或更新数据<br>\n\t * 根据给定的字段名查询数据，如果存在则更新这些数据，否则执行插入\n\t *\n\t * @param record 记录\n\t * @param keys   需要检查唯一性的字段\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.10\n\t */\n\tpublic int insertOrUpdate(Entity record, String... keys) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.insertOrUpdate(conn, record, keys);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 使用upsert语义插入或更新数据<br>\n\t * 根据给定的字段名查询数据，如果存在则更新这些数据，否则执行插入\n\t * 如果方言未实现本方法，内部会自动调用insertOrUpdate来实现功能，由于upsert和insert使用有区别，为了兼容性保留原有insertOrUpdate不做变动\n\t * @param record 记录\n\t * @param keys   需要检查唯一性的字段\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.21\n\t */\n\tpublic int upsert(Entity record, String... keys) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.upsert(conn, record, keys);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 批量插入数据<br>\n\t * 需要注意的是，批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构，之后的数据会按照这个格式插入。<br>\n\t * 也就是说假如第一条数据只有2个字段，后边数据多于这两个字段的部分将被抛弃。\n\t *\n\t * @param records 记录列表\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int[] insert(Collection<Entity> records) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.insert(conn, records);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 插入数据\n\t *\n\t * @param record 记录\n\t * @return 主键列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Object> insertForGeneratedKeys(Entity record) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.insertForGeneratedKeys(conn, record);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 插入数据\n\t *\n\t * @param record 记录\n\t * @return 主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Long insertForGeneratedKey(Entity record) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.insertForGeneratedKey(conn, record);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 删除数据\n\t *\n\t * @param tableName 表名\n\t * @param field     字段名，最好是主键\n\t * @param value     值，值可以是列表或数组，被当作IN查询处理\n\t * @return 删除行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int del(String tableName, String field, Object value) throws SQLException {\n\t\treturn del(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 删除数据\n\t *\n\t * @param where 条件\n\t * @return 影响行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int del(Entity where) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.del(conn, where);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 更新数据<br>\n\t * 更新条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param record 记录\n\t * @param where  条件\n\t * @return 影响行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int update(Entity record, Entity where) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.update(conn, record, where);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------- Get start\n\n\t/**\n\t * 根据某个字段（最好是唯一字段）查询单个记录<br>\n\t * 当有多条返回时，只显示查询到的第一条\n\t *\n\t * @param <T>       字段值类型\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param value     字段值\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> Entity get(String tableName, String field, T value) throws SQLException {\n\t\treturn this.get(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 根据条件实体查询单个记录，当有多条返回时，只显示查询到的第一条\n\t *\n\t * @param where 条件\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Entity get(Entity where) throws SQLException {\n\t\treturn find(where.getFieldNames(), where, new EntityHandler(this.caseInsensitive));\n\n\t}\n\t// ------------------------------------------------------------- Get end\n\n\t/**\n\t * 查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>    需要处理成的结果对象类型\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @param rsh    结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T find(Collection<String> fields, Entity where, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.find(conn, fields, where, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @return 结果Entity列表\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.16\n\t */\n\tpublic List<Entity> find(Collection<String> fields, Entity where) throws SQLException {\n\t\treturn find(fields, where, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 查询<br>\n\t * Query为查询所需数据的一个实体类，此对象中可以定义返回字段、查询条件，查询的表、分页等信息\n\t *\n\t * @param <T>   需要处理成的结果对象类型\n\t * @param query {@link Query}对象，此对象中可以定义返回字段、查询条件，查询的表、分页等信息\n\t * @param rsh   结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.0\n\t */\n\tpublic <T> T find(Query query, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.find(conn, query, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 查询，返回所有字段<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>    需要处理成的结果对象类型\n\t * @param where  条件实体类（包含表名）\n\t * @param rsh    结果集处理对象\n\t * @param fields 字段列表，可变长参数如果无值表示查询全部字段\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T find(Entity where, RsHandler<T> rsh, String... fields) throws SQLException {\n\t\treturn find(CollUtil.newArrayList(fields), where, rsh);\n\t}\n\n\t/**\n\t * 查询数据列表，返回字段由where参数指定<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where 条件实体类（包含表名）\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.1\n\t */\n\tpublic List<Entity> find(Entity where) throws SQLException {\n\t\treturn find(where.getFieldNames(), where, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 查询数据列表，返回字段由where参数指定<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>       Bean类型\n\t * @param where     条件实体类（包含表名）\n\t * @param beanClass Bean类\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic <T> List<T> find(Entity where, Class<T> beanClass) throws SQLException {\n\t\treturn find(where.getFieldNames(), where, BeanListHandler.create(beanClass));\n\t}\n\n\t/**\n\t * 查询数据列表，返回所有字段<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where 条件实体类（包含表名）\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findAll(Entity where) throws SQLException {\n\t\treturn find(where, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 查询数据列表，返回所有字段<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>       Bean类型\n\t * @param where     条件实体类（包含表名）\n\t * @param beanClass 返回的对象类型\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic <T> List<T> findAll(Entity where, Class<T> beanClass) throws SQLException {\n\t\treturn find(where, BeanListHandler.create(beanClass));\n\t}\n\n\t/**\n\t * 查询数据列表，返回所有字段\n\t *\n\t * @param tableName 表名\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findAll(String tableName) throws SQLException {\n\t\treturn findAll(Entity.create(tableName));\n\t}\n\n\t/**\n\t * 根据某个字段名条件查询数据列表，返回所有字段\n\t *\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param value     字段值\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findBy(String tableName, String field, Object value) throws SQLException {\n\t\treturn findAll(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 根据多个条件查询数据列表，返回所有字段\n\t *\n\t * @param tableName 表名\n\t * @param wheres    条件，多个条件的连接逻辑使用{@link Condition#setLinkOperator(LogicalOperator)} 定义\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.0\n\t */\n\tpublic List<Entity> findBy(String tableName, Condition... wheres) throws SQLException {\n\t\tfinal Query query = new Query(wheres, tableName);\n\t\treturn find(query, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 根据某个字段名条件查询数据列表，返回所有字段\n\t *\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param value     字段值\n\t * @param likeType  {@link LikeType}\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findLike(String tableName, String field, String value, LikeType likeType) throws SQLException {\n\t\treturn findAll(Entity.create(tableName).set(field, SqlUtil.buildLikeValue(value, likeType, true)));\n\t}\n\n\t/**\n\t * 结果的条目数\n\t *\n\t * @param where 查询条件\n\t * @return 复合条件的结果数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic long count(Entity where) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.count(conn, where);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 结果的条目数\n\t *\n\t * @param sql sql构造器\n\t * @return 复合条件的结果数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic long count(SqlBuilder sql) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.count(conn, sql);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 结果的条目数\n\t *\n\t * @param selectSql 查询SQL语句\n\t * @param params    查询参数\n\t * @return 复合条件的结果数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.6.6\n\t */\n\tpublic long count(CharSequence selectSql, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.count(conn, selectSql, params);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>        结果对象类型\n\t * @param fields     返回的字段列表，null则返回所有字段\n\t * @param where      条件实体类（包含表名）\n\t * @param page       页码，0表示第一页\n\t * @param numPerPage 每页条目数\n\t * @param rsh        结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(Collection<String> fields, Entity where, int page, int numPerPage, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, fields, where, page, numPerPage, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>        结果对象类型\n\t * @param where      条件实体类（包含表名）\n\t * @param page       页码，0表示第一页\n\t * @param numPerPage 每页条目数\n\t * @param rsh        结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic <T> T page(Entity where, int page, int numPerPage, RsHandler<T> rsh) throws SQLException {\n\t\treturn page(where, new Page(page, numPerPage), rsh);\n\t}\n\n\t/**\n\t * 分页查询，结果为Entity列表，不计算总数<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where      条件实体类（包含表名）\n\t * @param page       页码，0表示第一页\n\t * @param numPerPage 每页条目数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic List<Entity> pageForEntityList(Entity where, int page, int numPerPage) throws SQLException {\n\t\treturn pageForEntityList(where, new Page(page, numPerPage));\n\t}\n\n\t/**\n\t * 分页查询，结果为Entity列表，不计算总数<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where 条件实体类（包含表名）\n\t * @param page  分页对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic List<Entity> pageForEntityList(Entity where, Page page) throws SQLException {\n\t\treturn page(where, page, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>   结果对象类型\n\t * @param where 条件实体类（包含表名）\n\t * @param page  分页对象\n\t * @param rsh   结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic <T> T page(Entity where, Page page, RsHandler<T> rsh) throws SQLException {\n\t\treturn page(where.getFieldNames(), where, page, rsh);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param <T>    结果对象类型\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @param page   分页对象\n\t * @param rsh    结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(Collection<String> fields, Entity where, Page page, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, fields, where, page, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t *\n\t * @param <T>    结果对象类型\n\t * @param sql    SQL构建器，可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL\n\t * @param page   分页对象\n\t * @param rsh    结果集处理对象\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.6.6\n\t */\n\tpublic <T> T page(CharSequence sql, Page page, RsHandler<T> rsh, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, SqlBuilder.of(sql).addParams(params), page, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t *\n\t * @param <T>             Bean类型\n\t * @param sql             SQL构建器，可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL\n\t * @param page            分页对象\n\t * @param elementBeanType 结果集处理对象\n\t * @param params          参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.8.11\n\t */\n\tpublic <T> PageResult<T> page(CharSequence sql, Page page, Class<T> elementBeanType, Object... params) throws SQLException {\n\t\tfinal PageResult<T> result = new PageResult<>(page.getPageNumber(), page.getPageSize(), (int) count(sql, params));\n\t\treturn page(sql, page,\n\t\t\t(RsHandler<? extends PageResult<T>>) rs -> HandleHelper.handleRsToBeanList(rs, result, elementBeanType),\n\t\t\tparams);\n\t}\n\n\t/**\n\t * 分页查询\n\t *\n\t * @param <T>  处理结果类型，可以将ResultSet转换为给定类型\n\t * @param sql  SQL构建器\n\t * @param page 分页对象\n\t * @param rsh  结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(SqlBuilder sql, Page page, RsHandler<T> rsh) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, sql, page, rsh);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询\n\t *\n\t * @param sql    SQL语句字符串\n\t * @param page   分页对象\n\t * @param params 参数列表\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tpublic PageResult<Entity> page(CharSequence sql, Page page, Object... params) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, SqlBuilder.of(sql).addParams(params), page);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param fields     返回的字段列表，null则返回所有字段\n\t * @param where      条件实体类（包含表名）\n\t * @param pageNumber 页码\n\t * @param pageSize   每页结果数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Collection<String> fields, Entity where, int pageNumber, int pageSize) throws SQLException {\n\t\treturn page(fields, where, new Page(pageNumber, pageSize));\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @param page   分页对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Collection<String> fields, Entity where, Page page) throws SQLException {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = this.getConnection();\n\t\t\treturn runner.page(conn, fields, where, page);\n\t\t} finally {\n\t\t\tthis.closeConnection(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where      条件实体类（包含表名）\n\t * @param page       页码\n\t * @param numPerPage 每页条目数\n\t * @return 分页结果集\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.2\n\t */\n\tpublic PageResult<Entity> page(Entity where, int page, int numPerPage) throws SQLException {\n\t\treturn this.page(where, new Page(page, numPerPage));\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 查询条件为多个key value对表示，默认key = value，如果使用其它条件可以使用：where.put(\"key\", \" &gt; 1\")，value也可以传Condition对象，key被忽略\n\t *\n\t * @param where 条件实体类（包含表名）\n\t * @param page  分页对象\n\t * @return 分页结果集\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Entity where, Page page) throws SQLException {\n\t\treturn this.page(where.getFieldNames(), where, page);\n\t}\n\t// ---------------------------------------------------------------------------- CRUD end\n\n\t// ---------------------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 设置是否在结果中忽略大小写<br>\n\t * 如果忽略，则在Entity中调用getXXX时，字段值忽略大小写，默认忽略\n\t *\n\t * @param caseInsensitive 否在结果中忽略大小写\n\t * @since 5.2.4\n\t */\n\tpublic void setCaseInsensitive(boolean caseInsensitive) {\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t/**\n\t * 获取{@link SqlConnRunner}\n\t *\n\t * @return {@link SqlConnRunner}\n\t */\n\tpublic SqlConnRunner getRunner() {\n\t\treturn runner;\n\t}\n\n\t/**\n\t * 设置 {@link SqlConnRunner}\n\t *\n\t * @param runner {@link SqlConnRunner}\n\t */\n\tpublic void setRunner(SqlConnRunner runner) {\n\t\tthis.runner = runner;\n\t}\n\n\t/**\n\t * 设置包装器，包装器用于对表名、字段名进行符号包装（例如双引号），防止关键字与这些表名或字段冲突\n\t *\n\t * @param wrapperChar 包装字符，字符会在SQL生成时位于表名和字段名两边，null时表示取消包装\n\t * @return this\n\t * @since 4.0.0\n\t */\n\tpublic AbstractDb setWrapper(Character wrapperChar) {\n\t\treturn setWrapper(new Wrapper(wrapperChar));\n\t}\n\n\t/**\n\t * 设置包装器，包装器用于对表名、字段名进行符号包装（例如双引号），防止关键字与这些表名或字段冲突\n\t *\n\t * @param wrapper 包装器，null表示取消包装\n\t * @return this\n\t * @since 4.0.0\n\t */\n\tpublic AbstractDb setWrapper(Wrapper wrapper) {\n\t\tthis.runner.setWrapper(wrapper);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 取消包装器<br>\n\t * 取消自动添加到字段名、表名上的包装符（例如双引号）\n\t *\n\t * @return this\n\t * @since 4.5.7\n\t */\n\tpublic AbstractDb disableWrapper() {\n\t\treturn setWrapper((Wrapper) null);\n\t}\n\t// ---------------------------------------------------------------------------- Getters and Setters end\n\n\t// ---------------------------------------------------------------------------- protected method start\n\n\t/**\n\t * 检查数据库是否支持事务，此项检查同一个数据源只检查一次，如果不支持抛出DbRuntimeException异常\n\t *\n\t * @param conn Connection\n\t * @throws SQLException       获取元数据信息失败\n\t * @throws DbRuntimeException 不支持事务\n\t */\n\tprotected void checkTransactionSupported(Connection conn) throws SQLException, DbRuntimeException {\n\t\tif (null == isSupportTransaction) {\n\t\t\tisSupportTransaction = conn.getMetaData().supportsTransactions();\n\t\t}\n\t\tif (false == isSupportTransaction) {\n\t\t\tthrow new DbRuntimeException(\"Transaction not supported for current database!\");\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------- protected method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ActiveEntity.java",
    "content": "package cn.hutool.db;\n\nimport java.sql.SQLException;\nimport java.util.Collection;\n\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.map.MapUtil;\n\n/**\n * 动态实体类<br>\n * 提供了针对自身实体的增删改方法\n * \n * @author Looly\n *\n */\npublic class ActiveEntity extends Entity {\n\tprivate static final long serialVersionUID = 6112321379601134750L;\n\n\tprivate final Db db;\n\n\t// --------------------------------------------------------------- Static method start\n\t/**\n\t * 创建ActiveEntity\n\t * \n\t * @return ActiveEntity\n\t */\n\tpublic static ActiveEntity create() {\n\t\treturn new ActiveEntity();\n\t}\n\n\t/**\n\t * 创建ActiveEntity\n\t * \n\t * @param tableName 表名\n\t * @return ActiveEntity\n\t */\n\tpublic static ActiveEntity create(String tableName) {\n\t\treturn new ActiveEntity(tableName);\n\t}\n\n\t/**\n\t * 将PO对象转为Entity\n\t * \n\t * @param <T> Bean对象类型\n\t * @param bean Bean对象\n\t * @return ActiveEntity\n\t */\n\tpublic static <T> ActiveEntity parse(T bean) {\n\t\treturn create(null).parseBean(bean);\n\t}\n\n\t/**\n\t * 将PO对象转为ActiveEntity\n\t * \n\t * @param <T> Bean对象类型\n\t * @param bean Bean对象\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue 是否忽略值为空的字段\n\t * @return ActiveEntity\n\t */\n\tpublic static <T> ActiveEntity parse(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\treturn create(null).parseBean(bean, isToUnderlineCase, ignoreNullValue);\n\t}\n\n\t/**\n\t * 将PO对象转为ActiveEntity,并采用下划线法转换字段\n\t * \n\t * @param <T> Bean对象类型\n\t * @param bean Bean对象\n\t * @return ActiveEntity\n\t */\n\tpublic static <T> ActiveEntity parseWithUnderlineCase(T bean) {\n\t\treturn create(null).parseBean(bean, true, true);\n\t}\n\t// --------------------------------------------------------------- Static method end\n\n\t// -------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic ActiveEntity() {\n\t\tthis(Db.use(), (String) null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param tableName 表名\n\t */\n\tpublic ActiveEntity(String tableName) {\n\t\tthis(Db.use(), tableName);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param entity 非动态实体\n\t */\n\tpublic ActiveEntity(Entity entity) {\n\t\tthis(Db.use(), entity);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param db {@link Db}\n\t * @param tableName 表名\n\t */\n\tpublic ActiveEntity(Db db, String tableName) {\n\t\tsuper(tableName);\n\t\tthis.db = db;\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param db {@link Db}\n\t * @param entity 非动态实体\n\t */\n\tpublic ActiveEntity(Db db, Entity entity) {\n\t\tsuper(entity.getTableName());\n\t\tthis.putAll(entity);\n\t\tthis.db = db;\n\t}\n\t// -------------------------------------------------------------------------- Constructor end\n\t\n\t@Override\n\tpublic ActiveEntity setTableName(String tableName) {\n\t\treturn (ActiveEntity) super.setTableName(tableName);\n\t}\n\t\n\t@Override\n\tpublic ActiveEntity setFieldNames(Collection<String> fieldNames) {\n\t\treturn (ActiveEntity) super.setFieldNames(fieldNames);\n\t}\n\t\n\t@Override\n\tpublic ActiveEntity setFieldNames(String... fieldNames) {\n\t\treturn (ActiveEntity) super.setFieldNames(fieldNames);\n\t}\n\n\t/**\n\t * 通过lambda批量设置值\n\t * @param fields lambda,不能为空\n\t * @return this\n\t */\n\t@Override\n\tpublic ActiveEntity setFields(Func0<?>... fields) {\n\t\treturn (ActiveEntity) super.setFields(fields);\n\t}\n\n\t@Override\n\tpublic ActiveEntity addFieldNames(String... fieldNames) {\n\t\treturn (ActiveEntity) super.addFieldNames(fieldNames);\n\t}\n\t\n\t@Override\n\tpublic <T> ActiveEntity parseBean(T bean) {\n\t\treturn (ActiveEntity) super.parseBean(bean);\n\t}\n\t\n\t@Override\n\tpublic <T> ActiveEntity parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\treturn (ActiveEntity) super.parseBean(bean, isToUnderlineCase, ignoreNullValue);\n\t}\n\t\n\t@Override\n\tpublic ActiveEntity set(String field, Object value) {\n\t\treturn (ActiveEntity) super.set(field, value);\n\t}\n\t\n\t@Override\n\tpublic ActiveEntity setIgnoreNull(String field, Object value) {\n\t\treturn (ActiveEntity) super.setIgnoreNull(field, value);\n\t}\n\t\n\t@Override\n\tpublic ActiveEntity clone() {\n\t\treturn (ActiveEntity) super.clone();\n\t}\n\n\t// -------------------------------------------------------------------------- CRUD start\n\t/**\n\t * 根据Entity中现有字段条件从数据库中增加一条数据\n\t * \n\t * @return this\n\t */\n\tpublic ActiveEntity add() {\n\t\ttry {\n\t\t\tdb.insert(this);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据Entity中现有字段条件从数据库中加载一个Entity对象\n\t * \n\t * @return this\n\t */\n\tpublic ActiveEntity load() {\n\t\ttry {\n\t\t\tfinal Entity result = db.get(this);\n\t\t\tif(MapUtil.isNotEmpty(result)) {\n\t\t\t\tthis.putAll(result);\n\t\t\t}\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据现有Entity中的条件删除与之匹配的数据库记录\n\t * \n\t * @return this\n\t */\n\tpublic ActiveEntity del() {\n\t\ttry {\n\t\t\tdb.del(this);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据现有Entity中的条件删除与之匹配的数据库记录\n\t * \n\t * @param primaryKey 主键名\n\t * @return this\n\t */\n\tpublic ActiveEntity update(String primaryKey) {\n\t\ttry {\n\t\t\tdb.update(this, Entity.create().set(primaryKey, this.get(primaryKey)));\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\t// -------------------------------------------------------------------------- CRUD end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.DSFactory;\n\nimport javax.sql.DataSource;\nimport java.sql.SQLException;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 数据访问层模板<br>\n * 此模板用于简化对指定表的操作，简化的操作如下：<br>\n * 1、在初始化时指定了表名，CRUD操作时便不需要表名<br>\n * 2、在初始化时指定了主键，某些需要主键的操作便不需要指定主键类型\n *\n * @author Looly\n */\npublic class DaoTemplate {\n\n\t/**\n\t * 表名\n\t */\n\tprotected String tableName;\n\t/**\n\t * 本表的主键字段，请在子类中覆盖或构造方法中指定，默认为id\n\t */\n\tprotected String primaryKeyField = \"id\";\n\t/**\n\t * SQL运行器\n\t */\n\tprotected Db db;\n\n\t//--------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，此构造需要自定义SqlRunner，主键默认为id\n\t *\n\t * @param tableName 数据库表名\n\t */\n\tpublic DaoTemplate(String tableName) {\n\t\tthis(tableName, (String) null);\n\t}\n\n\t/**\n\t * 构造，使用默认的池化连接池，读取默认配置文件的空分组，适用于只有一个数据库的情况\n\t *\n\t * @param tableName       数据库表名\n\t * @param primaryKeyField 主键字段名\n\t */\n\tpublic DaoTemplate(String tableName, String primaryKeyField) {\n\t\tthis(tableName, primaryKeyField, DSFactory.get());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName 表\n\t * @param ds 数据源\n\t */\n\tpublic DaoTemplate(String tableName, DataSource ds) {\n\t\tthis(tableName, null, ds);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName       表名\n\t * @param primaryKeyField 主键字段名\n\t * @param ds              数据源\n\t */\n\tpublic DaoTemplate(String tableName, String primaryKeyField, DataSource ds) {\n\t\tthis(tableName, primaryKeyField, Db.use(ds));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName       表名\n\t * @param primaryKeyField 主键字段名\n\t * @param db              Db对象\n\t */\n\tpublic DaoTemplate(String tableName, String primaryKeyField, Db db) {\n\t\tthis.tableName = tableName;\n\t\tif (StrUtil.isNotBlank(primaryKeyField)) {\n\t\t\tthis.primaryKeyField = primaryKeyField;\n\t\t}\n\t\tthis.db = db;\n\t}\n\t//--------------------------------------------------------------- Constructor end\n\n\t//------------------------------------------------------------- Add start\n\n\t/**\n\t * 添加\n\t *\n\t * @param entity 实体对象\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int add(Entity entity) throws SQLException {\n\t\treturn db.insert(fixEntity(entity));\n\t}\n\n\t/**\n\t * 添加\n\t *\n\t * @param entity 实体对象\n\t * @return 主键列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Object> addForGeneratedKeys(Entity entity) throws SQLException {\n\t\treturn db.insertForGeneratedKeys(fixEntity(entity));\n\t}\n\n\t/**\n\t * 添加\n\t *\n\t * @param entity 实体对象\n\t * @return 自增主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Long addForGeneratedKey(Entity entity) throws SQLException {\n\t\treturn db.insertForGeneratedKey(fixEntity(entity));\n\t}\n\t//------------------------------------------------------------- Add end\n\n\t//------------------------------------------------------------- Delete start\n\n\t/**\n\t * 删除\n\t *\n\t * @param <T> 主键类型\n\t * @param pk  主键\n\t * @return 删除行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> int del(T pk) throws SQLException {\n\t\tif (pk == null) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn this.del(Entity.create(tableName).set(primaryKeyField, pk));\n\t}\n\n\t/**\n\t * 删除\n\t *\n\t * @param <T>   主键类型\n\t * @param field 字段名\n\t * @param value 字段值\n\t * @return 删除行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> int del(String field, T value) throws SQLException {\n\t\tif (StrUtil.isBlank(field)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn this.del(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 删除\n\t *\n\t * @param <T>   主键类型\n\t * @param where 删除条件，当条件为空时，返回0（防止误删全表）\n\t * @return 删除行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> int del(Entity where) throws SQLException {\n\t\tif (MapUtil.isEmpty(where)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn db.del(fixEntity(where));\n\t}\n\t//------------------------------------------------------------- Delete end\n\n\t//------------------------------------------------------------- Update start\n\n\t/**\n\t * 按照条件更新\n\t *\n\t * @param record 更新的内容\n\t * @param where  条件\n\t * @return 更新条目数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int update(Entity record, Entity where) throws SQLException {\n\t\tif (MapUtil.isEmpty(record)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn db.update(fixEntity(record), where);\n\t}\n\n\t/**\n\t * 更新\n\t *\n\t * @param entity 实体对象，必须包含主键\n\t * @return 更新行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int update(Entity entity) throws SQLException {\n\t\tif (MapUtil.isEmpty(entity)) {\n\t\t\treturn 0;\n\t\t}\n\t\tentity = fixEntity(entity);\n\t\tObject pk = entity.get(primaryKeyField);\n\t\tif (null == pk) {\n\t\t\tthrow new SQLException(StrUtil.format(\"Please determine `{}` for update\", primaryKeyField));\n\t\t}\n\n\t\tfinal Entity where = Entity.create(tableName).set(primaryKeyField, pk);\n\t\tfinal Entity record = entity.clone();\n\t\trecord.remove(primaryKeyField);\n\n\t\treturn db.update(record, where);\n\t}\n\n\t/**\n\t * 增加或者更新实体\n\t *\n\t * @param entity 实体，当包含主键时更新，否则新增\n\t * @return 新增或更新条数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int addOrUpdate(Entity entity) throws SQLException {\n\t\treturn null == entity.get(primaryKeyField) ? add(entity) : update(entity);\n\t}\n\t//------------------------------------------------------------- Update end\n\n\t//------------------------------------------------------------- Get start\n\n\t/**\n\t * 根据主键获取单个记录\n\t *\n\t * @param <T> 主键类型\n\t * @param pk  主键值\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> Entity get(T pk) throws SQLException {\n\t\treturn this.get(primaryKeyField, pk);\n\t}\n\n\t/**\n\t * 根据某个字段（最好是唯一字段）查询单个记录<br>\n\t * 当有多条返回时，只显示查询到的第一条\n\t *\n\t * @param <T>   字段值类型\n\t * @param field 字段名\n\t * @param value 字段值\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> Entity get(String field, T value) throws SQLException {\n\t\treturn this.get(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 根据条件实体查询单个记录，当有多条返回时，只显示查询到的第一条\n\t *\n\t * @param where 条件\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Entity get(Entity where) throws SQLException {\n\t\treturn db.get(fixEntity(where));\n\t}\n\t//------------------------------------------------------------- Get end\n\n\t//------------------------------------------------------------- Find start\n\n\t/**\n\t * 根据某个字段值查询结果\n\t *\n\t * @param <T>   字段值类型\n\t * @param field 字段名\n\t * @param value 字段值\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> List<Entity> find(String field, T value) throws SQLException {\n\t\treturn this.find(Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 查询当前表的所有记录\n\t *\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findAll() throws SQLException {\n\t\treturn this.find(Entity.create(tableName));\n\t}\n\n\t/**\n\t * 根据某个字段值查询结果\n\t *\n\t * @param where 查询条件\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> find(Entity where) throws SQLException {\n\t\treturn db.find(null, fixEntity(where));\n\t}\n\n\t/**\n\t * 根据SQL语句查询结果<br>\n\t * SQL语句可以是非完整SQL语句，可以只提供查询的条件部分（例如WHERE部分）<br>\n\t * 此方法会自动补全SELECT * FROM [tableName] 部分，这样就无需关心表名，直接提供条件即可\n\t *\n\t * @param sql    SQL语句\n\t * @param params SQL占位符中对应的参数\n\t * @return 记录\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findBySql(String sql, Object... params) throws SQLException {\n\t\tString selectKeyword = StrUtil.subPre(sql.trim(), 6).toLowerCase();\n\t\tif (false == \"select\".equals(selectKeyword)) {\n\t\t\tsql = \"SELECT * FROM \" + this.tableName + \" \" + sql;\n\t\t}\n\t\treturn db.query(sql, params);\n\t}\n\n\t/**\n\t * 分页\n\t *\n\t * @param where        条件\n\t * @param page         分页对象\n\t * @param selectFields 查询的字段列表\n\t * @return 分页结果集\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Entity where, Page page, String... selectFields) throws SQLException {\n\t\treturn db.page(Arrays.asList(selectFields), fixEntity(where), page);\n\t}\n\n\t/**\n\t * 分页\n\t *\n\t * @param where 条件\n\t * @param page  分页对象\n\t * @return 分页结果集\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Entity where, Page page) throws SQLException {\n\t\treturn db.page(fixEntity(where), page);\n\t}\n\n\t/**\n\t * 满足条件的数据条目数量\n\t *\n\t * @param where 条件\n\t * @return 数量\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic long count(Entity where) throws SQLException {\n\t\treturn db.count(fixEntity(where));\n\t}\n\n\t/**\n\t * 指定条件的数据是否存在\n\t *\n\t * @param where 条件\n\t * @return 是否存在\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic boolean exist(Entity where) throws SQLException {\n\t\treturn this.count(where) > 0;\n\t}\n\t//------------------------------------------------------------- Find end\n\n\t/**\n\t * 修正Entity对象，避免null和填充表名\n\t *\n\t * @param entity 实体类\n\t * @return 修正后的实体类\n\t */\n\tprivate Entity fixEntity(Entity entity) {\n\t\tif (null == entity) {\n\t\t\tentity = Entity.create(tableName);\n\t\t} else if (StrUtil.isBlank(entity.getTableName())) {\n\t\t\tentity.setTableName(tableName);\n\t\t}\n\t\treturn entity;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/Db.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.func.VoidFunc1;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectFactory;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.db.sql.Wrapper;\nimport cn.hutool.db.transaction.TransactionLevel;\nimport cn.hutool.log.StaticLog;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.SQLException;\n\n/**\n * 数据库操作类<br>\n * 通过给定的数据源执行给定SQL或者给定数据源和方言，执行相应的CRUD操作<br>\n *\n * @author Looly\n * @since 4.1.2\n */\npublic class Db extends AbstractDb {\n\tprivate static final long serialVersionUID = -3378415769645309514L;\n\n\t/**\n\t * 创建Db<br>\n\t * 使用默认数据源，自动探测数据库连接池\n\t *\n\t * @return Db\n\t */\n\tpublic static Db use() {\n\t\treturn use(DSFactory.get());\n\t}\n\n\t/**\n\t * 创建Db<br>\n\t * 使用默认数据源，自动探测数据库连接池\n\t *\n\t * @param group 数据源分组\n\t * @return Db\n\t */\n\tpublic static Db use(String group) {\n\t\treturn use(DSFactory.get(group));\n\t}\n\n\t/**\n\t * 创建Db<br>\n\t * 会根据数据源连接的元信息识别目标数据库类型，进而使用合适的数据源\n\t *\n\t * @param ds 数据源\n\t * @return Db\n\t */\n\tpublic static Db use(DataSource ds) {\n\t\treturn ds == null ? null : new Db(ds);\n\t}\n\n\t/**\n\t * 创建Db\n\t *\n\t * @param ds 数据源\n\t * @param dialect 方言\n\t * @return Db\n\t */\n\tpublic static Db use(DataSource ds, Dialect dialect) {\n\t\treturn new Db(ds, dialect);\n\t}\n\n\t/**\n\t * 创建Db\n\t *\n\t * @param ds 数据源\n\t * @param driverClassName 数据库连接驱动类名\n\t * @return Db\n\t */\n\tpublic static Db use(DataSource ds, String driverClassName) {\n\t\treturn new Db(ds, DialectFactory.newDialect(driverClassName));\n\t}\n\n\t// ---------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，从DataSource中识别方言\n\t *\n\t * @param ds 数据源\n\t */\n\tpublic Db(DataSource ds) {\n\t\tthis(ds, DialectFactory.getDialect(ds));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds 数据源\n\t * @param driverClassName 数据库连接驱动类名，用于识别方言\n\t */\n\tpublic Db(DataSource ds, String driverClassName) {\n\t\tthis(ds, DialectFactory.newDialect(driverClassName));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds 数据源\n\t * @param dialect 方言\n\t */\n\tpublic Db(DataSource ds, Dialect dialect) {\n\t\tsuper(ds, dialect);\n\t}\n\t// ---------------------------------------------------------------------------- Constructor end\n\n\t// ---------------------------------------------------------------------------- Getters and Setters start\n\t@Override\n\tpublic Db setWrapper(Character wrapperChar) {\n\t\treturn (Db) super.setWrapper(wrapperChar);\n\t}\n\n\t@Override\n\tpublic Db setWrapper(Wrapper wrapper) {\n\t\treturn (Db) super.setWrapper(wrapper);\n\t}\n\n\t@Override\n\tpublic Db disableWrapper() {\n\t\treturn (Db)super.disableWrapper();\n\t}\n\t// ---------------------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic Connection getConnection() throws SQLException {\n\t\treturn ThreadLocalConnection.INSTANCE.get(this.ds);\n\t}\n\n\t@Override\n\tpublic void closeConnection(Connection conn) {\n\t\ttry {\n\t\t\tif (conn != null && false == conn.getAutoCommit()) {\n\t\t\t\t// 事务中的Session忽略关闭事件\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (SQLException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\tThreadLocalConnection.INSTANCE.close(this.ds);\n\t}\n\n\t/**\n\t * 执行事务，使用默认的事务级别<br>\n\t * 在同一事务中，所有对数据库操作都是原子的，同时提交或者同时回滚\n\t *\n\t * @param func 事务函数，所有操作应在同一函数下执行，确保在同一事务中\n\t * @return this\n\t * @throws SQLException SQL异常\n\t */\n\tpublic Db tx(VoidFunc1<Db> func) throws SQLException {\n\t\treturn tx(null, func);\n\t}\n\n\t/**\n\t * 执行事务<br>\n\t * 在同一事务中，所有对数据库操作都是原子的，同时提交或者同时回滚\n\t *\n\t * @param transactionLevel 事务级别枚举，null表示使用JDBC默认事务\n\t * @param func 事务函数，所有操作应在同一函数下执行，确保在同一事务中\n\t * @return this\n\t * @throws SQLException SQL异常\n\t */\n\tpublic Db tx(TransactionLevel transactionLevel, VoidFunc1<Db> func) throws SQLException {\n\t\tfinal Connection conn = getConnection();\n\n\t\t// 检查是否支持事务\n\t\tcheckTransactionSupported(conn);\n\n\t\t// 设置事务级别\n\t\tif (null != transactionLevel) {\n\t\t\tfinal int level = transactionLevel.getLevel();\n\t\t\tif (conn.getTransactionIsolation() < level) {\n\t\t\t\t// 用户定义的事务级别如果比默认级别更严格，则按照严格的级别进行\n\t\t\t\t//noinspection MagicConstant\n\t\t\t\tconn.setTransactionIsolation(level);\n\t\t\t}\n\t\t}\n\n\t\t// 开始事务\n\t\tboolean autoCommit = conn.getAutoCommit();\n\t\tif (autoCommit) {\n\t\t\tconn.setAutoCommit(false);\n\t\t}\n\n\t\t// 执行事务\n\t\ttry {\n\t\t\tfunc.call(this);\n\t\t\t// 提交\n\t\t\tconn.commit();\n\t\t} catch (Throwable e) {\n\t\t\tquietRollback(conn);\n\t\t\tthrow (e instanceof SQLException) ? (SQLException) e : new SQLException(e);\n\t\t} finally {\n\t\t\t// 还原事务状态\n\t\t\tquietSetAutoCommit(conn, autoCommit);\n\t\t\t// 关闭连接或将连接归还连接池\n\t\t\tcloseConnection(conn);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t// ---------------------------------------------------------------------------- Private method start\n\t/**\n\t * 静默回滚事务\n\t *\n\t * @param conn Connection\n\t */\n\tprivate void quietRollback(Connection conn) {\n\t\tif (null != conn) {\n\t\t\ttry {\n\t\t\t\tconn.rollback();\n\t\t\t} catch (Exception e) {\n\t\t\t\tStaticLog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 静默设置自动提交\n\t *\n\t * @param conn Connection\n\t * @param autoCommit 是否自动提交\n\t */\n\tprivate void quietSetAutoCommit(Connection conn, Boolean autoCommit) {\n\t\tif (null != conn && null != autoCommit) {\n\t\t\ttry {\n\t\t\t\tconn.setAutoCommit(autoCommit);\n\t\t\t} catch (Exception e) {\n\t\t\t\tStaticLog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\t// ---------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/DbRuntimeException.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 数据库异常\n * @author xiaoleilu\n */\npublic class DbRuntimeException extends RuntimeException{\n\tprivate static final long serialVersionUID = 3624487785708765623L;\n\n\tpublic DbRuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic DbRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic DbRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic DbRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic DbRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic DbRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/DbUtil.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectFactory;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.db.sql.SqlLog;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.level.Level;\nimport cn.hutool.setting.Setting;\n\nimport javax.naming.InitialContext;\nimport javax.naming.NamingException;\nimport javax.sql.DataSource;\nimport java.sql.Connection;\n\n/**\n * 数据库操作工具类\n *\n * @author Luxiaolei\n */\npublic final class DbUtil {\n\tprivate final static Log log = Log.get();\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param dialect 数据源\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner newSqlConnRunner(Dialect dialect) {\n\t\treturn SqlConnRunner.create(dialect);\n\t}\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param ds 数据源\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner newSqlConnRunner(DataSource ds) {\n\t\treturn SqlConnRunner.create(ds);\n\t}\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param conn 数据库连接对象\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner newSqlConnRunner(Connection conn) {\n\t\treturn SqlConnRunner.create(DialectFactory.newDialect(conn));\n\t}\n\n\t/**\n\t * 实例化一个新的Db，使用默认数据源\n\t *\n\t * @return SQL执行类\n\t */\n\tpublic static Db use() {\n\t\treturn Db.use();\n\t}\n\n\t/**\n\t * 实例化一个新的Db对象\n\t *\n\t * @param ds 数据源\n\t * @return SQL执行类\n\t */\n\tpublic static Db use(DataSource ds) {\n\t\treturn Db.use(ds);\n\t}\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param ds      数据源\n\t * @param dialect SQL方言\n\t * @return SQL执行类\n\t */\n\tpublic static Db use(DataSource ds, Dialect dialect) {\n\t\treturn Db.use(ds, dialect);\n\t}\n\n\t/**\n\t * 新建数据库会话，使用默认数据源\n\t *\n\t * @return 数据库会话\n\t */\n\tpublic static Session newSession() {\n\t\treturn Session.create(getDs());\n\t}\n\n\t/**\n\t * 新建数据库会话\n\t *\n\t * @param ds 数据源\n\t * @return 数据库会话\n\t */\n\tpublic static Session newSession(DataSource ds) {\n\t\treturn Session.create(ds);\n\t}\n\n\t/**\n\t * 连续关闭一系列的SQL相关对象<br>\n\t * 这些对象必须按照顺序关闭，否则会出错。\n\t *\n\t * @param objsToClose 需要关闭的对象\n\t */\n\tpublic static void close(Object... objsToClose) {\n\t\tfor (Object obj : objsToClose) {\n\t\t\tif (null != obj) {\n\t\t\t\tif (obj instanceof AutoCloseable) {\n\t\t\t\t\tIoUtil.close((AutoCloseable) obj);\n\t\t\t\t} else {\n\t\t\t\t\tlog.warn(\"Object {} not a ResultSet or Statement or PreparedStatement or Connection!\", obj.getClass().getName());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获得默认数据源\n\t *\n\t * @return 默认数据源\n\t */\n\tpublic static DataSource getDs() {\n\t\treturn DSFactory.get();\n\t}\n\n\t/**\n\t * 获取指定分组的数据源\n\t *\n\t * @param group 分组\n\t * @return 数据源\n\t */\n\tpublic static DataSource getDs(String group) {\n\t\treturn DSFactory.get(group);\n\t}\n\n\t/**\n\t * 获得JNDI数据源\n\t *\n\t * @param jndiName JNDI名称\n\t * @return 数据源\n\t */\n\tpublic static DataSource getJndiDsWithLog(String jndiName) {\n\t\ttry {\n\t\t\treturn getJndiDs(jndiName);\n\t\t} catch (DbRuntimeException e) {\n\t\t\tlog.error(e.getCause(), \"Find JNDI datasource error!\");\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得JNDI数据源\n\t *\n\t * @param jndiName JNDI名称\n\t * @return 数据源\n\t */\n\tpublic static DataSource getJndiDs(String jndiName) {\n\t\ttry {\n\t\t\treturn (DataSource) new InitialContext().lookup(jndiName);\n\t\t} catch (NamingException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 移除配置文件中的Show SQL相关配置项<br>\n\t * 此方法用于移除用户配置在分组下的配置项目\n\t *\n\t * @param setting 配置项\n\t * @since 5.7.2\n\t */\n\tpublic static void removeShowSqlParams(Setting setting) {\n\t\tsetting.remove(SqlLog.KEY_SHOW_SQL);\n\t\tsetting.remove(SqlLog.KEY_FORMAT_SQL);\n\t\tsetting.remove(SqlLog.KEY_SHOW_PARAMS);\n\t\tsetting.remove(SqlLog.KEY_SQL_LEVEL);\n\t}\n\n\t/**\n\t * 从配置文件中读取SQL打印选项，读取后会去除相应属性\n\t *\n\t * @param setting 配置文件\n\t * @since 4.1.7\n\t */\n\tpublic static void setShowSqlGlobal(Setting setting) {\n\t\t// 初始化SQL显示\n\t\tfinal boolean isShowSql = Convert.toBool(setting.remove(SqlLog.KEY_SHOW_SQL), false);\n\t\tfinal boolean isFormatSql = Convert.toBool(setting.remove(SqlLog.KEY_FORMAT_SQL), false);\n\t\tfinal boolean isShowParams = Convert.toBool(setting.remove(SqlLog.KEY_SHOW_PARAMS), false);\n\t\tString sqlLevelStr = setting.remove(SqlLog.KEY_SQL_LEVEL);\n\t\tif (null != sqlLevelStr) {\n\t\t\tsqlLevelStr = sqlLevelStr.toUpperCase();\n\t\t}\n\t\tfinal Level level = Convert.toEnum(Level.class, sqlLevelStr, Level.DEBUG);\n\t\tlog.debug(\"Show sql: [{}], format sql: [{}], show params: [{}], level: [{}]\", isShowSql, isFormatSql, isShowParams, level);\n\t\tsetShowSqlGlobal(isShowSql, isFormatSql, isShowParams, level);\n\t}\n\n\t/**\n\t * 设置全局配置：是否通过debug日志显示SQL\n\t *\n\t * @param isShowSql    是否显示SQL\n\t * @param isFormatSql  是否格式化显示的SQL\n\t * @param isShowParams 是否打印参数\n\t * @param level        SQL打印到的日志等级\n\t * @see GlobalDbConfig#setShowSql(boolean, boolean, boolean, Level)\n\t * @since 4.1.7\n\t */\n\tpublic static void setShowSqlGlobal(boolean isShowSql, boolean isFormatSql, boolean isShowParams, Level level) {\n\t\tGlobalDbConfig.setShowSql(isShowSql, isFormatSql, isShowParams, level);\n\t}\n\n\t/**\n\t * 设置全局是否在结果中忽略大小写<br>\n\t * 如果忽略，则在Entity中调用getXXX时，字段值忽略大小写，默认忽略\n\t *\n\t * @param caseInsensitive 否在结果中忽略大小写\n\t * @see GlobalDbConfig#setCaseInsensitive(boolean)\n\t * @since 5.2.4\n\t */\n\tpublic static void setCaseInsensitiveGlobal(boolean caseInsensitive) {\n\t\tGlobalDbConfig.setCaseInsensitive(caseInsensitive);\n\t}\n\n\t/**\n\t * 设置全局是否INSERT语句中默认返回主键（默认返回主键）<br>\n\t * 如果false，则在Insert操作后，返回影响行数\n\t * 主要用于某些数据库不支持返回主键的情况\n\t *\n\t * @param returnGeneratedKey 是否INSERT语句中默认返回主键\n\t * @see GlobalDbConfig#setReturnGeneratedKey(boolean)\n\t * @since 5.3.10\n\t */\n\tpublic static void setReturnGeneratedKeyGlobal(boolean returnGeneratedKey) {\n\t\tGlobalDbConfig.setReturnGeneratedKey(returnGeneratedKey);\n\t}\n\n\t/**\n\t * 自定义数据库配置文件路径（绝对路径或相对classpath路径）\n\t *\n\t * @param dbSettingPath 自定义数据库配置文件路径（绝对路径或相对classpath路径）\n\t * @see GlobalDbConfig#setDbSettingPath(String)\n\t * @since 5.8.0\n\t */\n\tpublic static void setDbSettingPathGlobal(String dbSettingPath) {\n\t\tGlobalDbConfig.setDbSettingPath(dbSettingPath);\n\t}\n\n\t/**\n\t * 设置构造结果集时每次从数据库取到的行数\n\t *\n\t * @param fetchSize 每一轮网络请求获取的行数\n\t * @since 5.8.39\n\t */\n\tpublic static void setStatementFetchSizeGlobal(Integer fetchSize) {\n\t\tGlobalDbConfig.setStatementFetchSize(fetchSize);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/DialectRunner.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectFactory;\nimport cn.hutool.db.handler.NumberHandler;\nimport cn.hutool.db.handler.RsHandler;\nimport cn.hutool.db.sql.*;\n\nimport java.io.Serializable;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n/**\n * 提供基于方言的原始增删改查执行封装\n *\n * @author looly\n * @since 5.5.3\n */\npublic class DialectRunner implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Dialect dialect;\n\t/**\n\t * 是否大小写不敏感（默认大小写不敏感）\n\t */\n\tprotected boolean caseInsensitive = GlobalDbConfig.caseInsensitive;\n\n\t/**\n\t * 构造\n\t *\n\t * @param dialect 方言\n\t */\n\tpublic DialectRunner(Dialect dialect) {\n\t\tthis.dialect = dialect;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param driverClassName 驱动类名，用于识别方言\n\t */\n\tpublic DialectRunner(String driverClassName) {\n\t\tthis(DialectFactory.newDialect(driverClassName));\n\t}\n\n\t//---------------------------------------------------------------------------- CRUD start\n\n\t/**\n\t * 批量插入数据<br>\n\t * 批量插入必须严格保持Entity的结构一致，不一致会导致插入数据出现不可预知的结果<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn    数据库连接\n\t * @param records 记录列表，记录KV必须严格一致\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int[] insert(Connection conn, Entity... records) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (ArrayUtil.isEmpty(records)) {\n\t\t\treturn new int[]{0};\n\t\t}\n\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tif (1 == records.length) {\n\t\t\t\t//单条单独处理\n\t\t\t\tps = dialect.psForInsert(conn, records[0]);\n\t\t\t\treturn new int[]{ps.executeUpdate()};\n\t\t\t}\n\n\t\t\t// 批量\n\t\t\tps = dialect.psForInsertBatch(conn, records);\n\t\t\treturn ps.executeBatch();\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 更新或插入数据<br>\n\t * 此方法不会关闭Connection\n\t * 如果方言未实现此方法则内部自动使用insertOrUpdate来替代功能\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @param keys   需要检查唯一性的字段\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.20\n\t */\n\tpublic int upsert(Connection conn, Entity record, String... keys) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\ttry{\n\t\t\tps = getDialect().psForUpsert(conn, record, keys);\n\t\t}catch (SQLException ignore){\n\t\t\t// 方言不支持，使用默认\n\t\t}\n\t\tif (null != ps) {\n\t\t\ttry {\n\t\t\t\treturn ps.executeUpdate();\n\t\t\t} finally {\n\t\t\t\tDbUtil.close(ps);\n\t\t\t}\n\t\t} else {\n\t\t\treturn insertOrUpdate(conn, record, keys);\n\t\t}\n\t}\n\n\t/**\n\t * 插入或更新数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @param keys   需要检查唯一性的字段\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int insertOrUpdate(Connection conn, Entity record, String... keys) throws SQLException {\n\t\tfinal Entity where = record.filter(keys);\n\t\tif (MapUtil.isNotEmpty(where) && count(conn, where) > 0) {\n\t\t\t// issue#I6W91Z\n\t\t\t// 更新时，给定的字段无需更新\n\t\t\treturn update(conn, record.removeNew(keys), where);\n\t\t} else {\n\t\t\treturn insert(conn, record)[0];\n\t\t}\n\t}\n\n\t/**\n\t * 插入数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>                  主键类型，可能为数字或对象列表\n\t * @param conn                 数据库连接\n\t * @param record               记录\n\t * @param generatedKeysHandler 自增主键处理器，用于定义返回自增主键的范围和类型\n\t * @return 主键列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T insert(Connection conn, Entity record, RsHandler<T> generatedKeysHandler) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (MapUtil.isEmpty(record)) {\n\t\t\tthrow new SQLException(\"Empty entity provided!\");\n\t\t}\n\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = dialect.psForInsert(conn, record);\n\t\t\tps.executeUpdate();\n\t\t\tif (null == generatedKeysHandler) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn StatementUtil.getGeneratedKeys(ps, generatedKeysHandler);\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 删除数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn  数据库连接\n\t * @param where 条件\n\t * @return 影响行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int del(Connection conn, Entity where) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (MapUtil.isEmpty(where)) {\n\t\t\t//不允许做全表删除\n\t\t\tthrow new SQLException(\"Empty entity provided!\");\n\t\t}\n\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = dialect.psForDelete(conn, Query.of(where));\n\t\t\treturn ps.executeUpdate();\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 更新数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @param where  条件\n\t * @return 影响行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int update(Connection conn, Entity record, Entity where) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (MapUtil.isEmpty(record)) {\n\t\t\tthrow new SQLException(\"Empty entity provided!\");\n\t\t}\n\t\tif (MapUtil.isEmpty(where)) {\n\t\t\t//不允许做全表更新\n\t\t\tthrow new SQLException(\"Empty where provided!\");\n\t\t}\n\n\t\t//表名可以从被更新记录的Entity中获得，也可以从Where中获得\n\t\tString tableName = record.getTableName();\n\t\tif (StrUtil.isBlank(tableName)) {\n\t\t\ttableName = where.getTableName();\n\t\t\trecord.setTableName(tableName);\n\t\t}\n\n\t\tfinal Query query = new Query(SqlUtil.buildConditions(where), tableName);\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = dialect.psForUpdate(conn, record, query);\n\t\t\treturn ps.executeUpdate();\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>   结果对象类型\n\t * @param conn  数据库连接对象\n\t * @param query {@link Query}\n\t * @param rsh   结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T find(Connection conn, Query query, RsHandler<T> rsh) throws SQLException {\n\t\tcheckConn(conn);\n\t\tAssert.notNull(query, \"[query] is null !\");\n\t\treturn SqlExecutor.queryAndClosePs(dialect.psForFind(conn, query), rsh);\n\t}\n\n\t/**\n\t * 获取结果总数，生成类似于select count(1) from XXX wher XXX=? and YYY=?\n\t *\n\t * @param conn  数据库连接对象\n\t * @param where 查询条件\n\t * @return 复合条件的结果数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic long count(Connection conn, Entity where) throws SQLException {\n\t\tcheckConn(conn);\n\t\treturn SqlExecutor.queryAndClosePs(dialect.psForCount(conn, Query.of(where)), new NumberHandler()).longValue();\n\t}\n\n\t/**\n\t * 获取查询结果总数，生成类似于 SELECT count(1) from (sql) hutool_alias_count_<br>\n\t * 此方法会重新构建{@link SqlBuilder}，并去除末尾的order by子句\n\t *\n\t * @param conn       数据库连接对象\n\t * @param sqlBuilder 查询语句\n\t * @return 复合条件的结果数\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.2\n\t */\n\tpublic long count(Connection conn, SqlBuilder sqlBuilder) throws SQLException {\n\t\tcheckConn(conn);\n\n\t\t// 去除order by 子句\n\t\tfinal String selectSql = SqlUtil.removeOuterOrderBy(sqlBuilder.build());\n\n\t\treturn SqlExecutor.queryAndClosePs(dialect.psForCount(conn,\n\t\t\t\tSqlBuilder.of(selectSql).addParams(sqlBuilder.getParamValueArray())),\n\t\t\t\tnew NumberHandler()).longValue();\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>   结果对象类型\n\t * @param conn  数据库连接对象\n\t * @param query 查询条件（包含表名）\n\t * @param rsh   结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(Connection conn, Query query, RsHandler<T> rsh) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (null == query.getPage()) {\n\t\t\treturn this.find(conn, query, rsh);\n\t\t}\n\n\t\treturn SqlExecutor.queryAndClosePs(dialect.psForPage(conn, query), rsh);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>        结果对象类型\n\t * @param conn       数据库连接对象\n\t * @param sqlBuilder SQL构建器，可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL\n\t * @param page       分页对象\n\t * @param rsh        结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tpublic <T> T page(Connection conn, SqlBuilder sqlBuilder, Page page, RsHandler<T> rsh) throws SQLException {\n\t\tcheckConn(conn);\n\t\tif (null == page) {\n\t\t\treturn SqlExecutor.query(conn, sqlBuilder, rsh);\n\t\t}\n\n\t\treturn SqlExecutor.queryAndClosePs(dialect.psForPage(conn, sqlBuilder, page), rsh);\n\t}\n\t//---------------------------------------------------------------------------- CRUD end\n\n\t//---------------------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 设置是否在结果中忽略大小写<br>\n\t * 如果忽略，则在Entity中调用getXXX时，字段值忽略大小写，默认忽略\n\t *\n\t * @param caseInsensitive 否在结果中忽略大小写\n\t * @since 5.2.4\n\t */\n\tpublic void setCaseInsensitive(boolean caseInsensitive) {\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t/**\n\t * @return SQL方言\n\t */\n\tpublic Dialect getDialect() {\n\t\treturn dialect;\n\t}\n\n\t/**\n\t * 设置SQL方言\n\t *\n\t * @param dialect 方言\n\t */\n\tpublic void setDialect(Dialect dialect) {\n\t\tthis.dialect = dialect;\n\t}\n\n\t/**\n\t * 设置包装器，包装器用于对表名、字段名进行符号包装（例如双引号），防止关键字与这些表名或字段冲突\n\t *\n\t * @param wrapperChar 包装字符，字符会在SQL生成时位于表名和字段名两边，null时表示取消包装\n\t */\n\tpublic void setWrapper(Character wrapperChar) {\n\t\tsetWrapper(new Wrapper(wrapperChar));\n\t}\n\n\t/**\n\t * 设置包装器，包装器用于对表名、字段名进行符号包装（例如双引号），防止关键字与这些表名或字段冲突\n\t *\n\t * @param wrapper 包装器，null表示取消包装\n\t */\n\tpublic void setWrapper(Wrapper wrapper) {\n\t\tthis.dialect.setWrapper(wrapper);\n\t}\n\t//---------------------------------------------------------------------------- Getters and Setters end\n\n\t//---------------------------------------------------------------------------- Private method start\n\tprivate void checkConn(Connection conn) {\n\t\tAssert.notNull(conn, \"Connection object must be not null!\");\n\t}\n\t//---------------------------------------------------------------------------- Private method start\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/Entity.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.lang.func.Func0;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.sql.Condition;\nimport cn.hutool.db.sql.SqlUtil;\n\nimport java.nio.charset.Charset;\nimport java.sql.*;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.Set;\n\n/**\n * 数据实体对象<br>\n * 数据实体类充当两个角色：<br>\n * 1. 数据的载体，一个Entity对应数据库中的一个row<br>\n * 2. SQL条件，Entity中的每一个字段对应一个条件，字段值对应条件的值\n *\n * @author loolly\n */\npublic class Entity extends Dict {\n\tprivate static final long serialVersionUID = -1951012511464327448L;\n\n\t// --------------------------------------------------------------- Static method start\n\n\t/**\n\t * 创建Entity\n\t *\n\t * @return Entity\n\t */\n\tpublic static Entity create() {\n\t\treturn new Entity();\n\t}\n\n\t/**\n\t * 创建Entity\n\t *\n\t * @param tableName 表名\n\t * @return Entity\n\t */\n\tpublic static Entity create(String tableName) {\n\t\treturn new Entity(tableName);\n\t}\n\n\t/**\n\t * 将PO对象转为Entity\n\t *\n\t * @param <T>  Bean对象类型\n\t * @param bean Bean对象\n\t * @return Entity\n\t */\n\tpublic static <T> Entity parse(T bean) {\n\t\treturn create(null).parseBean(bean);\n\t}\n\n\t/**\n\t * 将PO对象转为Entity\n\t *\n\t * @param <T>               Bean对象类型\n\t * @param bean              Bean对象\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue   是否忽略值为空的字段\n\t * @return Entity\n\t */\n\tpublic static <T> Entity parse(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\treturn create(null).parseBean(bean, isToUnderlineCase, ignoreNullValue);\n\t}\n\n\t/**\n\t * 将PO对象转为Entity,并采用下划线法转换字段\n\t *\n\t * @param <T>  Bean对象类型\n\t * @param bean Bean对象\n\t * @return Entity\n\t */\n\tpublic static <T> Entity parseWithUnderlineCase(T bean) {\n\t\treturn create(null).parseBean(bean, true, true);\n\t}\n\t// --------------------------------------------------------------- Static method end\n\n\t/* 表名 */\n\tprivate String tableName;\n\t/* 字段名列表，用于限制加入的字段的值 */\n\tprivate Set<String> fieldNames;\n\n\t// --------------------------------------------------------------- Constructor start\n\tpublic Entity() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName 数据表名\n\t */\n\n\tpublic Entity(String tableName) {\n\t\tthis.tableName = tableName;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName       数据表名\n\t * @param caseInsensitive 是否大小写不敏感\n\t * @since 4.5.16\n\t */\n\tpublic Entity(String tableName, boolean caseInsensitive) {\n\t\tsuper(caseInsensitive);\n\t\tthis.tableName = tableName;\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * @return 获得表名\n\t */\n\tpublic String getTableName() {\n\t\treturn tableName;\n\t}\n\n\t/**\n\t * 设置表名\n\t *\n\t * @param tableName 表名\n\t * @return 本身\n\t */\n\tpublic Entity setTableName(String tableName) {\n\t\tthis.tableName = tableName;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 字段集合\n\t */\n\tpublic Set<String> getFieldNames() {\n\t\treturn this.fieldNames;\n\t}\n\n\t/**\n\t * 设置字段列表，用于限制加入的字段的值\n\t *\n\t * @param fieldNames 字段列表\n\t * @return 自身\n\t */\n\tpublic Entity setFieldNames(Collection<String> fieldNames) {\n\t\tif (CollectionUtil.isNotEmpty(fieldNames)) {\n\t\t\tthis.fieldNames = CollectionUtil.newHashSet(true, fieldNames);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置字段列表，用于限制加入的字段的值\n\t *\n\t * @param fieldNames 字段列表\n\t * @return 自身\n\t */\n\tpublic Entity setFieldNames(String... fieldNames) {\n\t\tif (ArrayUtil.isNotEmpty(fieldNames)) {\n\t\t\tthis.fieldNames = CollectionUtil.newLinkedHashSet(fieldNames);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 通过lambda批量设置值\n\t * @param fields lambda,不能为空\n\t * @return this\n\t */\n\t@Override\n\tpublic Entity setFields(Func0<?>... fields) {\n\t\treturn (Entity) super.setFields(fields);\n\t}\n\n\t/**\n\t * 添加字段列表\n\t *\n\t * @param fieldNames 字段列表\n\t * @return 自身\n\t */\n\tpublic Entity addFieldNames(String... fieldNames) {\n\t\tif (ArrayUtil.isNotEmpty(fieldNames)) {\n\t\t\tif (null == this.fieldNames) {\n\t\t\t\treturn setFieldNames(fieldNames);\n\t\t\t} else {\n\t\t\t\tCollections.addAll(this.fieldNames, fieldNames);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 将值对象转换为Entity<br>\n\t * 类名会被当作表名，小写第一个字母\n\t *\n\t * @param <T>  Bean对象类型\n\t * @param bean Bean对象\n\t * @return 自己\n\t */\n\t@Override\n\tpublic <T> Entity parseBean(T bean) {\n\t\tif (StrUtil.isBlank(this.tableName)) {\n\t\t\tthis.setTableName(StrUtil.lowerFirst(bean.getClass().getSimpleName()));\n\t\t}\n\t\treturn (Entity) super.parseBean(bean);\n\t}\n\n\t/**\n\t * 将值对象转换为Entity<br>\n\t * 类名会被当作表名，小写第一个字母\n\t *\n\t * @param <T>               Bean对象类型\n\t * @param bean              Bean对象\n\t * @param isToUnderlineCase 是否转换为下划线模式\n\t * @param ignoreNullValue   是否忽略值为空的字段\n\t * @return 自己\n\t */\n\t@Override\n\tpublic <T> Entity parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) {\n\t\tif (StrUtil.isBlank(this.tableName)) {\n\t\t\tString simpleName = bean.getClass().getSimpleName();\n\t\t\tthis.setTableName(isToUnderlineCase ? StrUtil.toUnderlineCase(simpleName) : StrUtil.lowerFirst(simpleName));\n\t\t}\n\t\treturn (Entity) super.parseBean(bean, isToUnderlineCase, ignoreNullValue);\n\t}\n\n\t/**\n\t * 过滤Map保留指定键值对，如果键不存在跳过\n\t *\n\t * @param keys 键列表\n\t * @return Dict 结果\n\t * @since 4.0.10\n\t */\n\t@Override\n\tpublic Entity filter(String... keys) {\n\t\tfinal Entity result = new Entity(this.tableName);\n\t\tresult.setFieldNames(this.fieldNames);\n\n\t\tfor (String key : keys) {\n\t\t\tif (this.containsKey(key)) {\n\t\t\t\tresult.put(key, this.get(key));\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 过滤Map去除指定键值对，如果键不存在跳过\n\t *\n\t * @param keys 键列表\n\t * @return Dict 结果\n\t * @since 5.8.19\n\t */\n\tpublic Entity removeNew(final String... keys) {\n\t\treturn (Entity) MapUtil.removeAny(this.clone(), keys);\n\t}\n\n\t// -------------------------------------------------------------------- Put and Set start\n\t/**\n\t * 添加条件\n\t *\n\t * @param condition 条件\n\t * @return this\n\t * @since 5.8.34\n\t */\n\tpublic Entity addCondition(final Condition condition) {\n\t\treturn set(condition.getField(), condition);\n\t}\n\n\t@Override\n\tpublic Entity set(String field, Object value) {\n\t\treturn (Entity) super.set(field, value);\n\t}\n\n\t@Override\n\tpublic Entity setIgnoreNull(String field, Object value) {\n\t\treturn (Entity) super.setIgnoreNull(field, value);\n\t}\n\t// -------------------------------------------------------------------- Put and Set end\n\n\t// -------------------------------------------------------------------- Get start\n\n\t/**\n\t * 获得Clob类型结果\n\t *\n\t * @param field 参数\n\t * @return Clob\n\t */\n\tpublic Clob getClob(String field) {\n\t\treturn get(field, null);\n\t}\n\n\t/**\n\t * 获得Blob类型结果\n\t *\n\t * @param field 参数\n\t * @return Blob\n\t * @since 3.0.6\n\t */\n\tpublic Blob getBlob(String field) {\n\t\treturn get(field, null);\n\t}\n\n\t@Override\n\tpublic Time getTime(String field) {\n\t\tObject obj = get(field);\n\t\tTime result = null;\n\t\tif (null != obj) {\n\t\t\ttry {\n\t\t\t\tresult = (Time) obj;\n\t\t\t} catch (Exception e) {\n\t\t\t\t// try oracle.sql.TIMESTAMP\n\t\t\t\tresult = ReflectUtil.invoke(obj, \"timeValue\");\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic Date getDate(String field) {\n\t\tObject obj = get(field);\n\t\tDate result = null;\n\t\tif (null != obj) {\n\t\t\ttry {\n\t\t\t\tresult = (Date) obj;\n\t\t\t} catch (Exception e) {\n\t\t\t\t// try oracle.sql.TIMESTAMP\n\t\t\t\tresult = ReflectUtil.invoke(obj, \"dateValue\");\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic Timestamp getTimestamp(String field) {\n\t\tObject obj = get(field);\n\t\tTimestamp result = null;\n\t\tif (null != obj) {\n\t\t\ttry {\n\t\t\t\tresult = (Timestamp) obj;\n\t\t\t} catch (Exception e) {\n\t\t\t\t// try oracle.sql.TIMESTAMP\n\t\t\t\tresult = ReflectUtil.invoke(obj, \"timestampValue\");\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic String getStr(String field) {\n\t\treturn getStr(field, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 获得字符串值<br>\n\t * 支持Clob、Blob、RowId\n\t *\n\t * @param field   字段名\n\t * @param charset 编码\n\t * @return 字段对应值\n\t * @since 3.0.6\n\t */\n\tpublic String getStr(String field, Charset charset) {\n\t\tfinal Object obj = get(field);\n\t\tif (obj instanceof Clob) {\n\t\t\treturn SqlUtil.clobToStr((Clob) obj);\n\t\t} else if (obj instanceof Blob) {\n\t\t\treturn SqlUtil.blobToStr((Blob) obj, charset);\n\t\t} else if (obj instanceof RowId) {\n\t\t\tfinal RowId rowId = (RowId) obj;\n\t\t\treturn StrUtil.str(rowId.getBytes(), charset);\n\t\t}\n\t\treturn super.getStr(field);\n\t}\n\n\t/**\n\t * 获得rowid\n\t *\n\t * @return RowId\n\t */\n\tpublic RowId getRowId() {\n\t\treturn getRowId(\"ROWID\");\n\t}\n\n\t/**\n\t * 获得rowid\n\t *\n\t * @param field rowid属性名\n\t * @return RowId\n\t */\n\tpublic RowId getRowId(String field) {\n\t\tObject obj = this.get(field);\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tif (obj instanceof RowId) {\n\t\t\treturn (RowId) obj;\n\t\t}\n\t\tthrow new DbRuntimeException(\"Value of field [{}] is not a rowid!\", field);\n\t}\n\n\t// -------------------------------------------------------------------- Get end\n\n\t// -------------------------------------------------------------------- 特殊方法 start\n\t@Override\n\tpublic Entity clone() {\n\t\treturn (Entity) super.clone();\n\t}\n\t// -------------------------------------------------------------------- 特殊方法 end\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Entity {tableName=\" + tableName + \", fieldNames=\" + fieldNames + \", fields=\" + super.toString() + \"}\";\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/GlobalDbConfig.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.io.resource.NoResourceException;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.db.sql.SqlLog;\nimport cn.hutool.log.level.Level;\nimport cn.hutool.setting.Setting;\n\n/**\n * DB全局配置配置项\n *\n * @author looly\n * @since 5.3.10\n */\npublic class GlobalDbConfig {\n\t/**\n\t * 数据库配置文件可选路径1\n\t */\n\tprivate static final String DEFAULT_DB_SETTING_PATH = \"config/db.setting\";\n\t/**\n\t * 数据库配置文件可选路径2\n\t */\n\tprivate static final String DEFAULT_DB_SETTING_PATH2 = \"db.setting\";\n\n\t/**\n\t * 是否大小写不敏感（默认大小写不敏感）\n\t */\n\tprotected static boolean caseInsensitive = true;\n\t/**\n\t * 是否INSERT语句中默认返回主键（默认返回主键）\n\t */\n\tprotected static boolean returnGeneratedKey = true;\n\t/**\n\t * 自定义数据库配置文件路径（绝对路径或相对classpath路径）\n\t *\n\t * @since 5.8.0\n\t */\n\tprivate static String dbSettingPath = null;\n\t/**\n\t * 自定义构造结果集时每次从数据库取的行数\n\t */\n\tprotected static Integer statementFetchSize = null;\n\n\t/**\n\t * 设置全局是否在结果中忽略大小写<br>\n\t * 如果忽略，则在Entity中调用getXXX时，字段值忽略大小写，默认忽略\n\t *\n\t * @param isCaseInsensitive 否在结果中忽略大小写\n\t */\n\tpublic static void setCaseInsensitive(boolean isCaseInsensitive) {\n\t\tcaseInsensitive = isCaseInsensitive;\n\t}\n\n\t/**\n\t * 设置全局是否INSERT语句中默认返回主键（默认返回主键）<br>\n\t * 如果false，则在Insert操作后，返回影响行数\n\t * 主要用于某些数据库不支持返回主键的情况\n\t *\n\t * @param isReturnGeneratedKey 是否INSERT语句中默认返回主键\n\t */\n\tpublic static void setReturnGeneratedKey(boolean isReturnGeneratedKey) {\n\t\treturnGeneratedKey = isReturnGeneratedKey;\n\t}\n\n\t/**\n\t * 自定义数据库配置文件路径（绝对路径或相对classpath路径）\n\t *\n\t * @param customDbSettingPath 自定义数据库配置文件路径（绝对路径或相对classpath路径）\n\t * @since 5.8.0\n\t */\n\tpublic static void setDbSettingPath(String customDbSettingPath) {\n\t\tdbSettingPath = customDbSettingPath;\n\t}\n\n\t/**\n\t * 获取自定义或默认位置数据库配置{@link Setting}\n\t *\n\t * @return 数据库配置\n\t * @since 5.8.0\n\t */\n\tpublic static Setting createDbSetting() {\n\t\tSetting setting;\n\t\tif (null != dbSettingPath) {\n\t\t\t// 自定义数据库配置文件位置\n\t\t\ttry {\n\t\t\t\tsetting = new Setting(dbSettingPath, false);\n\t\t\t} catch (NoResourceException e3) {\n\t\t\t\tthrow new NoResourceException(\"Customize db setting file [{}] not found !\", dbSettingPath);\n\t\t\t}\n\t\t} else {\n\t\t\tsetting = tryDefaultDbSetting();\n\t\t}\n\t\treturn setting;\n\t}\n\n\t/**\n\t * 获取自定义或默认位置数据库配置{@link Setting}\n\t *\n\t * @return 数据库配置\n\t * @since 5.8.36\n\t */\n\tprivate static Setting tryDefaultDbSetting() {\n\t\tfinal String[] defaultDbSettingPaths = {\"file:\" + DEFAULT_DB_SETTING_PATH, \"file:\" + DEFAULT_DB_SETTING_PATH2, DEFAULT_DB_SETTING_PATH, DEFAULT_DB_SETTING_PATH2};\n\t\tfor (final String settingPath : defaultDbSettingPaths) {\n\t\t\ttry {\n\t\t\t\treturn new Setting(settingPath, true);\n\t\t\t} catch (final NoResourceException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\n\t\tthrow new NoResourceException(\"Default db settings [{}] in classpath not found !\", ArrayUtil.join(defaultDbSettingPaths, \",\"));\n\t}\n\n\t/**\n\t * 设置全局配置：是否通过debug日志显示SQL\n\t *\n\t * @param isShowSql    是否显示SQL\n\t * @param isFormatSql  是否格式化显示的SQL\n\t * @param isShowParams 是否打印参数\n\t * @param level        SQL打印到的日志等级\n\t */\n\tpublic static void setShowSql(boolean isShowSql, boolean isFormatSql, boolean isShowParams, Level level) {\n\t\tSqlLog.INSTANCE.init(isShowSql, isFormatSql, isShowParams, level);\n\t}\n\n\t/**\n\t * 设置构造结果集时每次从数据库取到的行数\n\t *\n\t * @param statementFetchSize 每一轮网络请求获取的行数\n\t * @since 5.8.39\n\t */\n\tpublic static void setStatementFetchSize(Integer statementFetchSize) {\n\t\tGlobalDbConfig.statementFetchSize = statementFetchSize;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/Page.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Segment;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.PageUtil;\nimport cn.hutool.db.sql.Order;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\n\n/**\n * 分页对象\n *\n * @author Looly\n */\npublic class Page implements Segment<Integer>, Serializable {\n\tprivate static final long serialVersionUID = 97792549823353462L;\n\n\tpublic static final int DEFAULT_PAGE_SIZE = 20;\n\n\t/**\n\t * 页码，0表示第一页\n\t */\n\tprivate int pageNumber;\n\t/**\n\t * 每页结果数\n\t */\n\tprivate int pageSize;\n\t/**\n\t * 排序\n\t */\n\tprivate Order[] orders;\n\n\t/**\n\t * 创建Page对象\n\t *\n\t * @param pageNumber 页码，0表示第一页\n\t * @param pageSize   每页结果数\n\t * @return Page\n\t * @since 5.5.3\n\t */\n\tpublic static Page of(int pageNumber, int pageSize) {\n\t\treturn new Page(pageNumber, pageSize);\n\t}\n\n\t// ---------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，默认第0页，每页{@value #DEFAULT_PAGE_SIZE} 条\n\t *\n\t * @since 4.5.16\n\t */\n\tpublic Page() {\n\t\tthis(0, DEFAULT_PAGE_SIZE);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pageNumber 页码，0表示第一页\n\t * @param pageSize   每页结果数\n\t */\n\tpublic Page(int pageNumber, int pageSize) {\n\t\tthis.pageNumber = Math.max(pageNumber, 0);\n\t\tthis.pageSize = pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param pageNumber 页码，0表示第一页\n\t * @param pageSize   每页结果数\n\t * @param order      排序对象\n\t */\n\tpublic Page(int pageNumber, int pageSize, Order order) {\n\t\tthis(pageNumber, pageSize);\n\t\tthis.orders = new Order[]{order};\n\t}\n\t// ---------------------------------------------------------- Constructor start\n\n\t// ---------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * @return 页码，0表示第一页\n\t */\n\tpublic int getPageNumber() {\n\t\treturn pageNumber;\n\t}\n\n\t/**\n\t * 设置页码，0表示第一页\n\t *\n\t * @param pageNumber 页码\n\t */\n\tpublic void setPageNumber(int pageNumber) {\n\t\tthis.pageNumber = Math.max(pageNumber, 0);\n\t}\n\n\t/**\n\t * @return 每页结果数\n\t */\n\tpublic int getPageSize() {\n\t\treturn pageSize;\n\t}\n\n\t/**\n\t * 设置每页结果数\n\t *\n\t * @param pageSize 每页结果数\n\t */\n\tpublic void setPageSize(int pageSize) {\n\t\tthis.pageSize = (pageSize <= 0) ? DEFAULT_PAGE_SIZE : pageSize;\n\t}\n\n\t/**\n\t * @return 排序\n\t */\n\tpublic Order[] getOrders() {\n\t\treturn this.orders;\n\t}\n\n\t/**\n\t * 设置排序\n\t *\n\t * @param orders 排序\n\t */\n\tpublic void setOrder(Order... orders) {\n\t\tthis.orders = orders;\n\t}\n\n\t/**\n\t * 设置排序\n\t *\n\t * @param orders 排序\n\t */\n\tpublic void addOrder(Order... orders) {\n\t\tthis.orders = ArrayUtil.append(this.orders, orders);\n\t}\n\t// ---------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * @return 开始位置\n\t * @see #getStartIndex()\n\t */\n\tpublic int getStartPosition() {\n\t\treturn getStartIndex();\n\t}\n\n\t@Override\n\tpublic Integer getStartIndex() {\n\t\treturn PageUtil.getStart(this.pageNumber, this.pageSize);\n\t}\n\n\t/**\n\t * @return 结束位置\n\t * @see #getEndIndex()\n\t */\n\tpublic int getEndPosition() {\n\t\treturn getEndIndex();\n\t}\n\n\t@Override\n\tpublic Integer getEndIndex() {\n\t\treturn PageUtil.getEnd(this.pageNumber, this.pageSize);\n\t}\n\n\t/**\n\t * 开始位置和结束位置<br>\n\t * 例如：\n\t *\n\t * <pre>\n\t * 页码：0，每页10 =》 [0, 10]\n\t * 页码：1，每页10 =》 [10, 20]\n\t * 页码：2，每页10 =》 [21, 30]\n\t * 。。。\n\t * </pre>\n\t *\n\t * @return 第一个数为开始位置，第二个数为结束位置\n\t */\n\tpublic int[] getStartEnd() {\n\t\treturn PageUtil.transToStartEnd(pageNumber, pageSize);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Page [page=\" + pageNumber + \", pageSize=\" + pageSize + \", order=\" + Arrays.toString(orders) + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/PageResult.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.util.PageUtil;\n\nimport java.util.ArrayList;\n\n/**\n * 分页数据结果集\n *\n * @param <T> 结果集项的类型\n * @author Looly\n */\npublic class PageResult<T> extends ArrayList<T> {\n\tprivate static final long serialVersionUID = 9056411043515781783L;\n\n\tpublic static final int DEFAULT_PAGE_SIZE = Page.DEFAULT_PAGE_SIZE;\n\n\t/**\n\t * 页码，{@link PageUtil#getFirstPageNo()}表示第一页\n\t */\n\tprivate int page;\n\t/**\n\t * 每页结果数\n\t */\n\tprivate int pageSize;\n\t/**\n\t * 总页数\n\t */\n\tprivate int totalPage;\n\t/**\n\t * 总数\n\t */\n\tprivate int total;\n\n\t//---------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic PageResult() {\n\t\tthis(0, DEFAULT_PAGE_SIZE);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param page     页码，0表示第一页\n\t * @param pageSize 每页结果数\n\t */\n\tpublic PageResult(int page, int pageSize) {\n\t\tsuper(pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize);\n\n\t\tthis.page = Math.max(page, 0);\n\t\tthis.pageSize = pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param page     页码，0表示第一页\n\t * @param pageSize 每页结果数\n\t * @param total    结果总数\n\t */\n\tpublic PageResult(int page, int pageSize, int total) {\n\t\tthis(page, pageSize);\n\n\t\tthis.total = total;\n\t\tthis.totalPage = PageUtil.totalPage(total, pageSize);\n\t}\n\t//---------------------------------------------------------- Constructor end\n\n\t//---------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 页码，0表示第一页\n\t *\n\t * @return 页码，0表示第一页\n\t */\n\tpublic int getPage() {\n\t\treturn page;\n\t}\n\n\t/**\n\t * 设置页码，0表示第一页\n\t *\n\t * @param page 页码\n\t */\n\tpublic void setPage(int page) {\n\t\tthis.page = page;\n\t}\n\n\t/**\n\t * @return 每页结果数\n\t */\n\tpublic int getPageSize() {\n\t\treturn pageSize;\n\t}\n\n\t/**\n\t * 设置每页结果数\n\t *\n\t * @param pageSize 每页结果数\n\t */\n\tpublic void setPageSize(int pageSize) {\n\t\tthis.pageSize = pageSize;\n\t}\n\n\t/**\n\t * @return 总页数\n\t */\n\tpublic int getTotalPage() {\n\t\treturn totalPage;\n\t}\n\n\t/**\n\t * 设置总页数\n\t *\n\t * @param totalPage 总页数\n\t */\n\tpublic void setTotalPage(int totalPage) {\n\t\tthis.totalPage = totalPage;\n\t}\n\n\t/**\n\t * @return 总数\n\t */\n\tpublic int getTotal() {\n\t\treturn total;\n\t}\n\n\t/**\n\t * 设置总数\n\t *\n\t * @param total 总数\n\t */\n\tpublic void setTotal(int total) {\n\t\tthis.total = total;\n\t}\n\t//---------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * @return 是否第一页\n\t */\n\tpublic boolean isFirst() {\n\t\treturn this.page == PageUtil.getFirstPageNo();\n\t}\n\n\t/**\n\t * @return 是否最后一页\n\t */\n\tpublic boolean isLast() {\n\t\treturn this.page >= (this.totalPage - 1);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/Session.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.func.VoidFunc1;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectFactory;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.db.sql.Wrapper;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\nimport javax.sql.DataSource;\nimport java.io.Closeable;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Savepoint;\n\n/**\n * 数据库SQL执行会话<br>\n * 会话通过共用Connection而可以实现JDBC事务<br>\n * 一个会话只维护一个连接，推荐在执行完后关闭Session，避免重用<br>\n * 本对象并不是线程安全的，多个线程共用一个Session将会导致不可预知的问题\n *\n * @author loolly\n *\n */\npublic class Session extends AbstractDb implements Closeable {\n\tprivate static final long serialVersionUID = 3421251905539056945L;\n\tprivate final static Log log = LogFactory.get();\n\n\t/**\n\t * 创建默认数据源会话\n\t *\n\t * @return Session\n\t * @since 3.2.3\n\t */\n\tpublic static Session create() {\n\t\treturn new Session(DSFactory.get());\n\t}\n\n\t/**\n\t * 创建会话\n\t *\n\t * @param group 分组\n\t * @return Session\n\t * @since 4.0.11\n\t */\n\tpublic static Session create(String group) {\n\t\treturn new Session(DSFactory.get(group));\n\t}\n\n\t/**\n\t * 创建会话\n\t *\n\t * @param ds 数据源\n\t * @return Session\n\t */\n\tpublic static Session create(DataSource ds) {\n\t\treturn new Session(ds);\n\t}\n\n\t// ---------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，从DataSource中识别方言\n\t *\n\t * @param ds 数据源\n\t */\n\tpublic Session(DataSource ds) {\n\t\tthis(ds, DialectFactory.getDialect(ds));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds 数据源\n\t * @param driverClassName 数据库连接驱动类名，用于识别方言\n\t */\n\tpublic Session(DataSource ds, String driverClassName) {\n\t\tthis(ds, DialectFactory.newDialect(driverClassName));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds 数据源\n\t * @param dialect 方言\n\t */\n\tpublic Session(DataSource ds, Dialect dialect) {\n\t\tsuper(ds, dialect);\n\t}\n\t// ---------------------------------------------------------------------------- Constructor end\n\n\t// ---------------------------------------------------------------------------- Getters and Setters end\n\t/**\n\t * 获得{@link SqlConnRunner}\n\t *\n\t * @return {@link SqlConnRunner}\n\t */\n\t@Override\n\tpublic SqlConnRunner getRunner() {\n\t\treturn runner;\n\t}\n\t// ---------------------------------------------------------------------------- Getters and Setters end\n\n\t// ---------------------------------------------------------------------------- Transaction method start\n\t/**\n\t * 开始事务\n\t *\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void beginTransaction() throws SQLException {\n\t\tfinal Connection conn = getConnection();\n\t\tcheckTransactionSupported(conn);\n\t\tconn.setAutoCommit(false);\n\t}\n\n\t/**\n\t * 提交事务\n\t *\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void commit() throws SQLException {\n\t\ttry {\n\t\t\tgetConnection().commit();\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tgetConnection().setAutoCommit(true); // 事务结束，恢复自动提交\n\t\t\t} catch (SQLException e) {\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 回滚事务\n\t *\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void rollback() throws SQLException {\n\t\ttry {\n\t\t\tgetConnection().rollback();\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tgetConnection().setAutoCommit(true); // 事务结束，恢复自动提交\n\t\t\t} catch (SQLException e) {\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 静默回滚事务<br>\n\t * 回滚事务\n\t */\n\tpublic void quietRollback() {\n\t\ttry {\n\t\t\tgetConnection().rollback();\n\t\t} catch (Exception e) {\n\t\t\tlog.error(e);\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tgetConnection().setAutoCommit(true); // 事务结束，恢复自动提交\n\t\t\t} catch (SQLException e) {\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 回滚到某个保存点，保存点的设置请使用setSavepoint方法\n\t *\n\t * @param savepoint 保存点\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void rollback(Savepoint savepoint) throws SQLException {\n\t\ttry {\n\t\t\tgetConnection().rollback(savepoint);\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tgetConnection().setAutoCommit(true); // 事务结束，恢复自动提交\n\t\t\t} catch (SQLException e) {\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 静默回滚到某个保存点，保存点的设置请使用setSavepoint方法\n\t *\n\t * @param savepoint 保存点\n\t */\n\tpublic void quietRollback(Savepoint savepoint) {\n\t\ttry {\n\t\t\tgetConnection().rollback(savepoint);\n\t\t} catch (Exception e) {\n\t\t\tlog.error(e);\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tgetConnection().setAutoCommit(true); // 事务结束，恢复自动提交\n\t\t\t} catch (SQLException e) {\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 设置保存点\n\t *\n\t * @return 保存点对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Savepoint setSavepoint() throws SQLException {\n\t\treturn getConnection().setSavepoint();\n\t}\n\n\t/**\n\t * 设置保存点\n\t *\n\t * @param name 保存点的名称\n\t * @return 保存点对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Savepoint setSavepoint(String name) throws SQLException {\n\t\treturn getConnection().setSavepoint(name);\n\t}\n\n\t/**\n\t * 设置事务的隔离级别<br>\n\t *\n\t * Connection.TRANSACTION_NONE 驱动不支持事务<br>\n\t * Connection.TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读<br>\n\t * Connection.TRANSACTION_READ_COMMITTED 禁止脏读，但允许不可重复读和幻读<br>\n\t * Connection.TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读，单运行幻读<br>\n\t * Connection.TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读<br>\n\t *\n\t * @param level 隔离级别\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void setTransactionIsolation(int level) throws SQLException {\n\t\tif (getConnection().getMetaData().supportsTransactionIsolationLevel(level) == false) {\n\t\t\tthrow new SQLException(StrUtil.format(\"Transaction isolation [{}] not support!\", level));\n\t\t}\n\t\tgetConnection().setTransactionIsolation(level);\n\t}\n\n\t/**\n\t * 在事务中执行操作，通过实现{@link VoidFunc1}接口的call方法执行多条SQL语句从而完成事务\n\t *\n\t * @param func 函数抽象，在函数中执行多个SQL操作，多个操作会被合并为同一事务\n\t * @throws SQLException SQL异常\n\t * @since 3.2.3\n\t */\n\tpublic void tx(VoidFunc1<Session> func) throws SQLException {\n\t\ttry {\n\t\t\tbeginTransaction();\n\t\t\tfunc.call(this);\n\t\t\tcommit();\n\t\t} catch (Throwable e) {\n\t\t\tquietRollback();\n\t\t\tthrow (e instanceof SQLException) ? (SQLException) e : new SQLException(e);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------------------- Transaction method end\n\n\t// ---------------------------------------------------------------------------- Getters and Setters start\n\t@Override\n\tpublic Session setWrapper(Character wrapperChar) {\n\t\treturn (Session) super.setWrapper(wrapperChar);\n\t}\n\n\t@Override\n\tpublic Session setWrapper(Wrapper wrapper) {\n\t\treturn (Session) super.setWrapper(wrapper);\n\t}\n\n\t@Override\n\tpublic Session disableWrapper() {\n\t\treturn (Session) super.disableWrapper();\n\t}\n\t// ---------------------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic Connection getConnection() throws SQLException {\n\t\treturn ThreadLocalConnection.INSTANCE.get(this.ds);\n\t}\n\n\t@Override\n\tpublic void closeConnection(Connection conn) {\n\t\ttry {\n\t\t\tif(conn != null && false == conn.getAutoCommit()) {\n\t\t\t\t// 事务中的Session忽略关闭事件\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (SQLException e) {\n\t\t\tlog.error(e);\n\t\t}\n\n\t\t// 普通请求关闭（或归还）连接\n\t\tThreadLocalConnection.INSTANCE.close(this.ds);\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tcloseConnection(null);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectFactory;\nimport cn.hutool.db.handler.EntityListHandler;\nimport cn.hutool.db.handler.HandleHelper;\nimport cn.hutool.db.handler.PageResultHandler;\nimport cn.hutool.db.handler.RsHandler;\nimport cn.hutool.db.sql.Condition.LikeType;\nimport cn.hutool.db.sql.Query;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.SqlUtil;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * SQL执行类<br>\n * 此执行类只接受方言参数，不需要数据源，只有在执行方法时需要数据库连接对象<br>\n * 此对象存在的意义在于，可以由使用者自定义数据库连接对象，并执行多个方法，方便事务的统一控制或减少连接对象的创建关闭<br>\n * 相比{@link DialectRunner}，此类中提供了更多重载方法\n *\n * @author Luxiaolei\n */\npublic class SqlConnRunner extends DialectRunner {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param dialect 方言\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner create(Dialect dialect) {\n\t\treturn new SqlConnRunner(dialect);\n\t}\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param ds 数据源\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner create(DataSource ds) {\n\t\treturn new SqlConnRunner(DialectFactory.getDialect(ds));\n\t}\n\n\t/**\n\t * 实例化一个新的SQL运行对象\n\t *\n\t * @param driverClassName 驱动类名\n\t * @return SQL执行类\n\t */\n\tpublic static SqlConnRunner create(String driverClassName) {\n\t\treturn new SqlConnRunner(driverClassName);\n\t}\n\n\t//------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param dialect 方言\n\t */\n\tpublic SqlConnRunner(Dialect dialect) {\n\t\tsuper(dialect);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param driverClassName 驱动类名，用于识别方言\n\t */\n\tpublic SqlConnRunner(String driverClassName) {\n\t\tsuper(driverClassName);\n\t}\n\t//------------------------------------------------------- Constructor end\n\n\t//---------------------------------------------------------------------------- CRUD start\n\n\t/**\n\t * 批量插入数据<br>\n\t * 需要注意的是，批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构，之后的数据会按照这个格式插入。<br>\n\t * 也就是说假如第一条数据只有2个字段，后边数据多于这两个字段的部分将被抛弃。\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn    数据库连接\n\t * @param records 记录列表，记录KV必须严格一致\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int[] insert(Connection conn, Collection<Entity> records) throws SQLException {\n\t\treturn insert(conn, records.toArray(new Entity[0]));\n\t}\n\n\t/**\n\t * 插入数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @return 插入行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic int insert(Connection conn, Entity record) throws SQLException {\n\t\treturn insert(conn, new Entity[]{record})[0];\n\t}\n\n\t/**\n\t * 插入数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @return 主键列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Object> insertForGeneratedKeys(Connection conn, Entity record) throws SQLException {\n\t\treturn insert(conn, record, HandleHelper::handleRowToList);\n\t}\n\n\t/**\n\t * 插入数据<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接\n\t * @param record 记录\n\t * @return 自增主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic Long insertForGeneratedKey(Connection conn, Entity record) throws SQLException {\n\t\treturn insert(conn, record, (rs) -> {\n\t\t\tLong generatedKey = null;\n\t\t\tif (rs != null && rs.next()) {\n\t\t\t\ttry {\n\t\t\t\t\tgeneratedKey = rs.getLong(1);\n\t\t\t\t} catch (SQLException e) {\n\t\t\t\t\t// 自增主键不为数字或者为Oracle的rowid，跳过\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn generatedKey;\n\t\t});\n\t}\n\n\t/**\n\t * 查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>    结果对象类型\n\t * @param conn   数据库连接对象\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @param rsh    结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T find(Connection conn, Collection<String> fields, Entity where, RsHandler<T> rsh) throws SQLException {\n\t\treturn find(conn, Query.of(where).setFields(fields), rsh);\n\t}\n\n\t/**\n\t * 查询，返回指定字段列表<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>    结果对象类型\n\t * @param conn   数据库连接对象\n\t * @param where  条件实体类（包含表名）\n\t * @param rsh    结果集处理对象\n\t * @param fields 字段列表，可变长参数如果无值表示查询全部字段\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T find(Connection conn, Entity where, RsHandler<T> rsh, String... fields) throws SQLException {\n\t\treturn find(conn, CollUtil.newArrayList(fields), where, rsh);\n\t}\n\n\t/**\n\t * 查询数据列表，返回字段在where参数中定义\n\t *\n\t * @param conn  数据库连接对象\n\t * @param where 条件实体类（包含表名）\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t * @since 3.2.1\n\t */\n\tpublic List<Entity> find(Connection conn, Entity where) throws SQLException {\n\t\treturn find(conn, where.getFieldNames(), where, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 查询数据列表，返回所有字段\n\t *\n\t * @param conn  数据库连接对象\n\t * @param where 条件实体类（包含表名）\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findAll(Connection conn, Entity where) throws SQLException {\n\t\treturn find(conn, where, new EntityListHandler(this.caseInsensitive));\n\t}\n\n\t/**\n\t * 查询数据列表，返回所有字段\n\t *\n\t * @param conn      数据库连接对象\n\t * @param tableName 表名\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findAll(Connection conn, String tableName) throws SQLException {\n\t\treturn findAll(conn, Entity.create(tableName));\n\t}\n\n\t/**\n\t * 根据某个字段名条件查询数据列表，返回所有字段\n\t *\n\t * @param conn      数据库连接对象\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param value     字段值\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findBy(Connection conn, String tableName, String field, Object value) throws SQLException {\n\t\treturn findAll(conn, Entity.create(tableName).set(field, value));\n\t}\n\n\t/**\n\t * 根据某个字段名条件查询数据列表，返回所有字段\n\t *\n\t * @param conn      数据库连接对象\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param value     字段值\n\t * @param likeType  {@link LikeType}\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findLike(Connection conn, String tableName, String field, String value, LikeType likeType) throws SQLException {\n\t\treturn findAll(conn, Entity.create(tableName).set(field, SqlUtil.buildLikeValue(value, likeType, true)));\n\t}\n\n\t/**\n\t * 根据某个字段名条件查询数据列表，返回所有字段\n\t *\n\t * @param conn      数据库连接对象\n\t * @param tableName 表名\n\t * @param field     字段名\n\t * @param values    字段值列表\n\t * @return 数据对象列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic List<Entity> findIn(Connection conn, String tableName, String field, Object... values) throws SQLException {\n\t\treturn findAll(conn, Entity.create(tableName).set(field, values));\n\t}\n\n\t/**\n\t * 获取查询结果总数，生成类似于 SELECT count(1) from (sql) as _count\n\t *\n\t * @param conn      数据库连接对象\n\t * @param selectSql 查询语句\n\t * @param params    查询参数\n\t * @return 结果数\n\t * @throws SQLException SQL异常\n\t * @since 5.6.6\n\t */\n\tpublic long count(Connection conn, CharSequence selectSql, Object... params) throws SQLException {\n\t\treturn count(conn, SqlBuilder.of(selectSql).addParams(params));\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>        结果对象类型\n\t * @param conn       数据库连接对象\n\t * @param fields     返回的字段列表，null则返回所有字段\n\t * @param where      条件实体类（包含表名）\n\t * @param pageNumber 页码\n\t * @param numPerPage 每页条目数\n\t * @param rsh        结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(Connection conn, Collection<String> fields, Entity where, int pageNumber, int numPerPage, RsHandler<T> rsh) throws SQLException {\n\t\treturn page(conn, Query.of(where).setFields(fields).setPage(new Page(pageNumber, numPerPage)), rsh);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn       数据库连接对象\n\t * @param sqlBuilder SQL构建器，可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL\n\t * @param page       分页对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tpublic PageResult<Entity> page(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException {\n\t\tfinal PageResultHandler pageResultHandler = new PageResultHandler(\n\t\t\t\tnew PageResult<>(page.getPageNumber(), page.getPageSize(), (int) count(conn, sqlBuilder)),\n\t\t\t\tthis.caseInsensitive);\n\t\treturn page(conn, sqlBuilder, page, pageResultHandler);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn       数据库连接对象\n\t * @param fields     返回的字段列表，null则返回所有字段\n\t * @param where      条件实体类（包含表名）\n\t * @param page       页码\n\t * @param numPerPage 每页条目数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Connection conn, Collection<String> fields, Entity where, int page, int numPerPage) throws SQLException {\n\t\treturn page(conn, fields, where, new Page(page, numPerPage));\n\t}\n\n\t/**\n\t * 分页全字段查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn  数据库连接对象\n\t * @param where 条件实体类（包含表名）\n\t * @param page  分页对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Connection conn, Entity where, Page page) throws SQLException {\n\t\treturn this.page(conn, null, where, page);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn   数据库连接对象\n\t * @param fields 返回的字段列表，null则返回所有字段\n\t * @param where  条件实体类（包含表名）\n\t * @param page   分页对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic PageResult<Entity> page(Connection conn, Collection<String> fields, Entity where, Page page) throws SQLException {\n\t\tfinal PageResultHandler pageResultHandler = new PageResultHandler(\n\t\t\t\tnew PageResult<>(page.getPageNumber(), page.getPageSize(), (int) count(conn, where)),\n\t\t\t\tthis.caseInsensitive);\n\t\treturn page(conn, fields, where, page, pageResultHandler);\n\t}\n\n\t/**\n\t * 分页查询<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T>     结果类型，取决于 {@link RsHandler} 的处理逻辑\n\t * @param conn    数据库连接对象\n\t * @param fields  返回的字段列表，null则返回所有字段\n\t * @param where   条件实体类（包含表名）\n\t * @param page    分页对象\n\t * @param handler 结果集处理器\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic <T> T page(Connection conn, Collection<String> fields, Entity where, Page page, RsHandler<T> handler) throws SQLException {\n\t\treturn this.page(conn, Query.of(where).setFields(fields).setPage(page), handler);\n\t}\n\t//---------------------------------------------------------------------------- CRUD end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/StatementUtil.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.handler.HandleHelper;\nimport cn.hutool.db.handler.RsHandler;\nimport cn.hutool.db.sql.NamedSql;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.SqlLog;\nimport cn.hutool.db.sql.SqlUtil;\n\nimport java.io.InputStream;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.sql.*;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Statement和PreparedStatement工具类\n *\n * @author looly\n * @since 4.0.10\n */\npublic class StatementUtil {\n\t/**\n\t * 填充SQL的参数。\n\t *\n\t * @param ps     PreparedStatement\n\t * @param params SQL参数\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static PreparedStatement fillParams(PreparedStatement ps, Object... params) throws SQLException {\n\t\tif (ArrayUtil.isEmpty(params)) {\n\t\t\treturn ps;\n\t\t}\n\t\treturn fillParams(ps, new ArrayIter<>(params));\n\t}\n\n\t/**\n\t * 填充SQL的参数。<br>\n\t * 对于日期对象特殊处理：传入java.util.Date默认按照Timestamp处理\n\t *\n\t * @param ps     PreparedStatement\n\t * @param params SQL参数\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static PreparedStatement fillParams(PreparedStatement ps, Iterable<?> params) throws SQLException {\n\t\treturn fillParams(ps, params, null);\n\t}\n\n\t/**\n\t * 填充SQL的参数。<br>\n\t * 对于日期对象特殊处理：传入java.util.Date默认按照Timestamp处理\n\t *\n\t * @param ps            PreparedStatement\n\t * @param params        SQL参数\n\t * @param nullTypeCache null参数的类型缓存，避免循环中重复获取类型\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL执行异常\n\t * @since 4.6.7\n\t */\n\tpublic static PreparedStatement fillParams(PreparedStatement ps, Iterable<?> params, Map<Integer, Integer> nullTypeCache) throws SQLException {\n\t\tif (null == params) {\n\t\t\treturn ps;// 无参数\n\t\t}\n\n\t\tint paramIndex = 1;//第一个参数从1计数\n\t\tfor (Object param : params) {\n\t\t\tsetParam(ps, paramIndex++, param, nullTypeCache);\n\t\t}\n\t\treturn ps;\n\t}\n\n\t/**\n\t * 创建{@link PreparedStatement}\n\t *\n\t * @param conn       数据库连接\n\t * @param sqlBuilder {@link SqlBuilder}包括SQL语句和参数\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 4.1.3\n\t */\n\tpublic static PreparedStatement prepareStatement(Connection conn, SqlBuilder sqlBuilder) throws SQLException {\n\t\treturn prepareStatement(conn, sqlBuilder.build(), sqlBuilder.getParamValueArray());\n\t}\n\n\t/**\n\t * 创建{@link PreparedStatement}\n\t *\n\t * @param conn   数据库连接\n\t * @param sql    SQL语句，使用\"?\"做为占位符\n\t * @param params \"?\"对应参数列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 3.2.3\n\t */\n\tpublic static PreparedStatement prepareStatement(Connection conn, String sql, Collection<Object> params) throws SQLException {\n\t\treturn prepareStatement(conn, sql, params.toArray(new Object[0]));\n\t}\n\n\t/**\n\t * 创建{@link PreparedStatement}\n\t *\n\t * @param conn   数据库连接\n\t * @param sql    SQL语句，使用\"?\"做为占位符\n\t * @param params \"?\"对应参数列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 3.2.3\n\t */\n\tpublic static PreparedStatement prepareStatement(Connection conn, String sql, Object... params) throws SQLException {\n\t\treturn prepareStatement(GlobalDbConfig.returnGeneratedKey, conn, sql, params);\n\t}\n\n\t/**\n\t * 创建{@link PreparedStatement}\n\t *\n\t * @param returnGeneratedKey 当为insert语句时，是否返回主键\n\t * @param conn   数据库连接\n\t * @param sql    SQL语句，使用\"?\"做为占位符\n\t * @param params \"?\"对应参数列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 5.8.19\n\t */\n\tpublic static PreparedStatement prepareStatement(boolean returnGeneratedKey, Connection conn, String sql, Object... params) throws SQLException {\n\t\tAssert.notBlank(sql, \"Sql String must be not blank!\");\n\t\tsql = sql.trim();\n\n\t\tif(ArrayUtil.isNotEmpty(params) && 1 == params.length && params[0] instanceof Map){\n\t\t\t// 检查参数是否为命名方式的参数\n\t\t\tfinal NamedSql namedSql = new NamedSql(sql, Convert.toMap(String.class, Object.class, params[0]));\n\t\t\tsql = namedSql.getSql();\n\t\t\tparams = namedSql.getParams();\n\t\t}\n\n\t\tSqlLog.INSTANCE.log(sql, ArrayUtil.isEmpty(params) ? null : params);\n\t\tPreparedStatement ps;\n\t\tif (returnGeneratedKey && StrUtil.startWithIgnoreCase(sql, \"insert\")) {\n\t\t\t// 插入默认返回主键\n\t\t\tps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);\n\t\t} else {\n\t\t\tps = conn.prepareStatement(sql);\n\t\t}\n\t\tsetFetchSize(ps);\n\t\treturn fillParams(ps, params);\n\t}\n\n\t/**\n\t * 创建批量操作的{@link PreparedStatement}\n\t *\n\t * @param conn        数据库连接\n\t * @param sql         SQL语句，使用\"?\"做为占位符\n\t * @param paramsBatch \"?\"对应参数批次列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 4.1.13\n\t */\n\tpublic static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Object[]... paramsBatch) throws SQLException {\n\t\treturn prepareStatementForBatch(conn, sql, new ArrayIter<>(paramsBatch));\n\t}\n\n\t/**\n\t * 创建批量操作的{@link PreparedStatement}\n\t *\n\t * @param conn        数据库连接\n\t * @param sql         SQL语句，使用\"?\"做为占位符\n\t * @param paramsBatch \"?\"对应参数批次列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 4.1.13\n\t */\n\tpublic static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Iterable<Object[]> paramsBatch) throws SQLException {\n\t\tAssert.notBlank(sql, \"Sql String must be not blank!\");\n\n\t\tsql = sql.trim();\n\t\tSqlLog.INSTANCE.log(sql, paramsBatch);\n\t\tPreparedStatement ps = conn.prepareStatement(sql);\n\t\tfinal Map<Integer, Integer> nullTypeMap = new HashMap<>();\n\t\tfor (Object[] params : paramsBatch) {\n\t\t\tfillParams(ps, new ArrayIter<>(params), nullTypeMap);\n\t\t\tps.addBatch();\n\t\t}\n\t\tsetFetchSize(ps);\n\t\treturn ps;\n\t}\n\n\t/**\n\t * 创建批量操作的{@link PreparedStatement}\n\t *\n\t * @param conn     数据库连接\n\t * @param sql      SQL语句，使用\"?\"做为占位符\n\t * @param fields   字段列表，用于获取对应值\n\t * @param entities \"?\"对应参数批次列表\n\t * @return {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t * @since 4.6.7\n\t */\n\tpublic static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Iterable<String> fields, Entity... entities) throws SQLException {\n\t\tAssert.notBlank(sql, \"Sql String must be not blank!\");\n\n\t\tsql = sql.trim();\n\t\tSqlLog.INSTANCE.logForBatch(sql);\n\t\tPreparedStatement ps = conn.prepareStatement(sql);\n\t\t//null参数的类型缓存，避免循环中重复获取类型\n\t\tfinal Map<Integer, Integer> nullTypeMap = new HashMap<>();\n\t\tfor (Entity entity : entities) {\n\t\t\tfillParams(ps, CollUtil.valuesOfKeys(entity, fields), nullTypeMap);\n\t\t\tps.addBatch();\n\t\t}\n\t\tsetFetchSize(ps);\n\t\treturn ps;\n\t}\n\n\t/**\n\t * 创建{@link CallableStatement}\n\t *\n\t * @param conn   数据库连接\n\t * @param sql    SQL语句，使用\"?\"做为占位符\n\t * @param params \"?\"对应参数列表\n\t * @return {@link CallableStatement}\n\t * @throws SQLException SQL异常\n\t * @since 4.1.13\n\t */\n\tpublic static CallableStatement prepareCall(Connection conn, String sql, Object... params) throws SQLException {\n\t\tAssert.notBlank(sql, \"Sql String must be not blank!\");\n\n\t\tsql = sql.trim();\n\t\tSqlLog.INSTANCE.log(sql, params);\n\t\tfinal CallableStatement call = conn.prepareCall(sql);\n\t\tfillParams(call, params);\n\t\treturn call;\n\t}\n\n\t/**\n\t * 获得自增键的值<br>\n\t * 此方法对于Oracle无效（返回null）\n\t *\n\t * @param ps PreparedStatement\n\t * @return 自增键的值，不存在返回null\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static Long getGeneratedKeyOfLong(Statement ps) throws SQLException {\n\t\treturn getGeneratedKeys(ps, (rs)->{\n\t\t\tLong generatedKey = null;\n\t\t\tif (rs != null && rs.next()) {\n\t\t\t\ttry {\n\t\t\t\t\tgeneratedKey = rs.getLong(1);\n\t\t\t\t} catch (SQLException e) {\n\t\t\t\t\t// 自增主键不为数字或者为Oracle的rowid，跳过\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn generatedKey;\n\t\t});\n\t}\n\n\t/**\n\t * 获得所有主键\n\t *\n\t * @param ps PreparedStatement\n\t * @return 所有主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static List<Object> getGeneratedKeys(Statement ps) throws SQLException {\n\t\treturn getGeneratedKeys(ps, HandleHelper::handleRowToList);\n\t}\n\n\t/**\n\t * 获取主键，并使用{@link RsHandler} 处理后返回\n\t * @param statement {@link Statement}\n\t * @param rsHandler 主键结果集处理器\n\t * @param <T> 自定义主键类型\n\t * @return 主键\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tpublic static <T> T getGeneratedKeys(Statement statement, RsHandler<T> rsHandler) throws SQLException {\n\t\ttry (final ResultSet rs = statement.getGeneratedKeys()) {\n\t\t\treturn rsHandler.handle(rs);\n\t\t}\n\t}\n\n\t/**\n\t * 获取null字段对应位置的数据类型<br>\n\t * 有些数据库对于null字段必须指定类型，否则会插入报错，此方法用于获取其类型，如果获取失败，使用默认的{@link Types#VARCHAR}\n\t *\n\t * @param ps         {@link Statement}\n\t * @param paramIndex 参数位置，第一位从1开始\n\t * @return 数据类型，默认{@link Types#VARCHAR}\n\t * @since 4.6.7\n\t */\n\tpublic static int getTypeOfNull(PreparedStatement ps, int paramIndex) {\n\t\tint sqlType = Types.VARCHAR;\n\n\t\tfinal ParameterMetaData pmd;\n\t\ttry {\n\t\t\tpmd = ps.getParameterMetaData();\n\t\t\tif (pmd == null) {\n\t\t\t\treturn sqlType;\n\t\t\t}\n\t\t\tsqlType = pmd.getParameterType(paramIndex);\n\t\t} catch (SQLException ignore) {\n\t\t\t// ignore\n\t\t\t// log.warn(\"Null param of index [{}] type get failed, by: {}\", paramIndex, e.getMessage());\n\t\t}\n\n\t\treturn sqlType;\n\t}\n\n\t/**\n\t * 为{@link PreparedStatement} 设置单个参数\n\t *\n\t * @param ps         {@link PreparedStatement}\n\t * @param paramIndex 参数位置，从1开始\n\t * @param param      参数\n\t * @throws SQLException SQL异常\n\t * @since 4.6.7\n\t */\n\tpublic static void setParam(PreparedStatement ps, int paramIndex, Object param) throws SQLException {\n\t\tsetParam(ps, paramIndex, param, null);\n\t}\n\n\t//--------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 为{@link PreparedStatement} 设置单个参数\n\t *\n\t * @param ps            {@link PreparedStatement}\n\t * @param paramIndex    参数位置，从1开始\n\t * @param param         参数，不能为{@code null}\n\t * @param nullTypeCache 用于缓存参数为null位置的类型，避免重复获取\n\t * @throws SQLException SQL异常\n\t * @since 4.6.7\n\t */\n\tprivate static void setParam(PreparedStatement ps, int paramIndex, Object param, Map<Integer, Integer> nullTypeCache) throws SQLException {\n\t\tif (null == param) {\n\t\t\tInteger type = (null == nullTypeCache) ? null : nullTypeCache.get(paramIndex);\n\t\t\tif (null == type) {\n\t\t\t\ttype = getTypeOfNull(ps, paramIndex);\n\t\t\t\tif (null != nullTypeCache) {\n\t\t\t\t\tnullTypeCache.put(paramIndex, type);\n\t\t\t\t}\n\t\t\t}\n\t\t\tps.setNull(paramIndex, type);\n\t\t}\n\n\t\t// 日期特殊处理，默认按照时间戳传入，避免毫秒丢失\n\t\tif (param instanceof java.util.Date) {\n\t\t\tif (param instanceof java.sql.Date) {\n\t\t\t\tps.setDate(paramIndex, (java.sql.Date) param);\n\t\t\t} else if (param instanceof java.sql.Time) {\n\t\t\t\tps.setTime(paramIndex, (java.sql.Time) param);\n\t\t\t} else {\n\t\t\t\tps.setTimestamp(paramIndex, SqlUtil.toSqlTimestamp((java.util.Date) param));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// 针对大数字类型的特殊处理\n\t\tif (param instanceof Number) {\n\t\t\tif (param instanceof BigDecimal) {\n\t\t\t\t// BigDecimal的转换交给JDBC驱动处理\n\t\t\t\tps.setBigDecimal(paramIndex, (BigDecimal) param);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (param instanceof BigInteger) {\n\t\t\t\t// BigInteger转为BigDecimal\n\t\t\t\tps.setBigDecimal(paramIndex, new BigDecimal((BigInteger) param));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// 忽略其它数字类型，按照默认类型传入\n\t\t}\n\n\t\t//InputStream，解决oracle情况下setObject(inputStream)报错问题，java.sql.SQLException: 无效的列类型\n\t\tif(param instanceof InputStream){\n\t\t\tps.setBinaryStream(paramIndex, (InputStream) param);\n\t\t\treturn;\n\t\t}\n\n\t\t//java.sql.Blob\n\t\tif(param instanceof Blob){\n\t\t\tps.setBlob(paramIndex, (Blob) param);\n\t\t}\n\n\n\t\t// 其它参数类型\n\t\tps.setObject(paramIndex, param);\n\t}\n\n\t/**\n\t * 为{@link PreparedStatement} 设置FetchSize\n\t * @param ps {@link PreparedStatement}\n\t * @throws SQLException SQL异常\n\t */\n\tprivate static void setFetchSize(PreparedStatement ps) throws SQLException {\n\t\tif(GlobalDbConfig.statementFetchSize!=null)\n\t\t\tps.setFetchSize(GlobalDbConfig.statementFetchSize);\n\t}\n\t//--------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ThreadLocalConnection.java",
    "content": "package cn.hutool.db;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 线程相关的数据库连接持有器<br>\n * 此对象为单例类，用于存储线程相关的Connection对象。<br>\n * 在多数据源情况下，由于数据源的不同，连接对象也不同，因此获取连接时需要DataSource关联获取\n * \n * @author looly\n *\n */\npublic enum ThreadLocalConnection {\n\tINSTANCE;\n\n\tprivate final ThreadLocal<GroupedConnection> threadLocal = new ThreadLocal<>();\n\n\t/**\n\t * 获取数据源对应的数据库连接\n\t * \n\t * @param ds 数据源\n\t * @return Connection\n\t * @throws SQLException SQL异常\n\t */\n\tpublic Connection get(DataSource ds) throws SQLException {\n\t\tGroupedConnection groupedConnection = threadLocal.get();\n\t\tif (null == groupedConnection) {\n\t\t\tgroupedConnection = new GroupedConnection();\n\t\t\tthreadLocal.set(groupedConnection);\n\t\t}\n\t\treturn groupedConnection.get(ds);\n\t}\n\n\t/**\n\t * 关闭数据库，并从线程池中移除\n\t * \n\t * @param ds 数据源\n\t * @since 4.1.7\n\t */\n\tpublic void close(DataSource ds) {\n\t\tGroupedConnection groupedConnection = threadLocal.get();\n\t\tif (null != groupedConnection) {\n\t\t\tgroupedConnection.close(ds);\n\t\t\tif (groupedConnection.isEmpty()) {\n\t\t\t\t// 当所有分组都没有持有的连接时，移除这个分组连接\n\t\t\t\tthreadLocal.remove();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 分组连接，根据不同的分组获取对应的连接，用于多数据源情况\n\t * \n\t * @author Looly\n\t */\n\tpublic static class GroupedConnection {\n\n\t\t/** 连接的Map，考虑到大部分情况是单数据库，故此处初始大小1 */\n\t\tprivate final Map<DataSource, Connection> connMap = new HashMap<>(1, 1);\n\n\t\t/**\n\t\t * 获取连接，如果获取的连接为空或者已被关闭，重新创建连接\n\t\t * \n\t\t * @param ds 数据源\n\t\t * @return Connection\n\t\t * @throws SQLException SQL异常\n\t\t */\n\t\tpublic Connection get(DataSource ds) throws SQLException {\n\t\t\tConnection conn = connMap.get(ds);\n\t\t\tif (null == conn || conn.isClosed()) {\n\t\t\t\tconn = ds.getConnection();\n\t\t\t\tconnMap.put(ds, conn);\n\t\t\t}\n\t\t\treturn conn;\n\t\t}\n\n\t\t/**\n\t\t * 关闭并移除Connection<br>\n\t\t * 如果处于事务中，则不进行任何操作\n\t\t * \n\t\t * @param ds 数据源\n\t\t * @return this\n\t\t */\n\t\tpublic GroupedConnection close(DataSource ds) {\n\t\t\tfinal Connection conn = connMap.get(ds);\n\t\t\tif (null != conn) {\n\t\t\t\ttry {\n\t\t\t\t\tif (false == conn.getAutoCommit()) {\n\t\t\t\t\t\t// 非自动提交事务的连接，不做关闭（可能处于事务中）\n\t\t\t\t\t\treturn this;\n\t\t\t\t\t}\n\t\t\t\t} catch (SQLException e) {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t\tconnMap.remove(ds);\n\t\t\t\tDbUtil.close(conn);\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\t/**\n\t\t * 持有的连接是否为空\n\t\t * \n\t\t * @return 持有的连接是否为空\n\t\t * @since 4.6.4\n\t\t */\n\t\tpublic boolean isEmpty() {\n\t\t\treturn connMap.isEmpty();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java",
    "content": "package cn.hutool.db.dialect;\n\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.sql.Order;\nimport cn.hutool.db.sql.Query;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\nimport java.io.Serializable;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n/**\n * SQL方言，不同的数据库由于在某些SQL上有所区别，故为每种数据库配置不同的方言。<br>\n * 由于不同数据库间SQL语句的差异，导致无法统一拼接SQL，<br>\n * Dialect接口旨在根据不同的数据库，使用不同的方言实现类，来拼接对应的SQL，并将SQL和参数放入PreparedStatement中\n *\n * @author loolly\n */\npublic interface Dialect extends Serializable {\n\n\t/**\n\t * @return 包装器\n\t */\n\tWrapper getWrapper();\n\n\t/**\n\t * 设置包装器\n\t *\n\t * @param wrapper 包装器\n\t */\n\tvoid setWrapper(Wrapper wrapper);\n\n\t// -------------------------------------------- Execute\n\n\t/**\n\t * 构建用于插入的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Entity}转换为带有占位符的SQL语句及参数列表\n\t *\n\t * @param conn   数据库连接对象\n\t * @param entity 数据实体类（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException;\n\n\t/**\n\t * 构建用于批量插入的PreparedStatement<br>\n\t * 用户实现需按照数据库方言格式，将{@link Entity}转换为带有占位符的SQL语句及参数列表\n\t *\n\t * @param conn     数据库连接对象\n\t * @param entities 数据实体，实体的结构必须全部一致，否则插入结果将不可预知\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException;\n\n\t/**\n\t * 构建用于删除的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * {@link Query}中包含了删除所需的表名、查询条件等信息，可借助SqlBuilder完成SQL语句生成。\n\t *\n\t * @param conn  数据库连接对象\n\t * @param query 查找条件（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForDelete(Connection conn, Query query) throws SQLException;\n\n\t/**\n\t * 构建用于更新的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Entity}配合{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * 其中{@link Entity}中包含需要更新的数据信息，{@link Query}包含更新的查找条件信息。\n\t *\n\t * @param conn   数据库连接对象\n\t * @param entity 数据实体类（包含表名）\n\t * @param query  查找条件（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException;\n\n\t// -------------------------------------------- Query\n\n\t/**\n\t * 构建用于获取多条记录的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * {@link Query}中包含了查询所需的表名、查询条件等信息，可借助SqlBuilder完成SQL语句生成。\n\t *\n\t * @param conn  数据库连接对象\n\t * @param query 查询条件（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForFind(Connection conn, Query query) throws SQLException;\n\n\t/**\n\t * 构建用于分页查询的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * {@link Query}中包含了分页查询所需的表名、查询条件、分页等信息，可借助SqlBuilder完成SQL语句生成。\n\t *\n\t * @param conn  数据库连接对象\n\t * @param query 查询条件（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tPreparedStatement psForPage(Connection conn, Query query) throws SQLException;\n\n\t/**\n\t * 构建用于分页查询的{@link PreparedStatement}<br>\n\t * 可以在此方法中使用{@link SqlBuilder#orderBy(Order...)}方法加入排序信息，\n\t * 排序信息通过{@link Page#getOrders()}获取\n\t *\n\t * @param conn       数据库连接对象\n\t * @param sqlBuilder SQL构建器，可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL\n\t * @param page       分页对象\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tPreparedStatement psForPage(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException;\n\n\t/**\n\t * 构建用于查询行数的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * {@link Query}中包含了表名、查询条件等信息，可借助SqlBuilder完成SQL语句生成。\n\t *\n\t * @param conn  数据库连接对象\n\t * @param query 查询条件（包含表名）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t */\n\tdefault PreparedStatement psForCount(Connection conn, Query query) throws SQLException {\n\t\treturn psForCount(conn, SqlBuilder.create(getWrapper()).query(query));\n\t}\n\n\t/**\n\t * 构建用于查询行数的{@link PreparedStatement}<br>\n\t * 用户实现需按照数据库方言格式，将{@link Query}转换为带有占位符的SQL语句及参数列表<br>\n\t * {@link Query}中包含了表名、查询条件等信息，可借助SqlBuilder完成SQL语句生成。\n\t *\n\t * @param conn       数据库连接对象\n\t * @param sqlBuilder 查询语句，应该包含分页等信息\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.2\n\t */\n\tdefault PreparedStatement psForCount(Connection conn, SqlBuilder sqlBuilder) throws SQLException {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I713XQ\n\t\t// 为了兼容informix等数据库，此处使用count(*)而非count(1)\n\t\tsqlBuilder = sqlBuilder\n\t\t\t\t.insertPreFragment(\"SELECT count(*) from(\")\n\t\t\t\t// issue#I3IJ8X@Gitee，在子查询时需设置单独别名，此处为了防止和用户的表名冲突，使用自定义的较长别名\n\t\t\t\t.append(\") hutool_alias_count_\");\n\t\treturn psForPage(conn, sqlBuilder, null);\n\t}\n\n\t/**\n\t * 构建用于upsert的{@link PreparedStatement}<br>\n\t * 方言实现需实现此默认方法，如果没有实现，抛出{@link SQLException}\n\t *\n\t * @param conn   数据库连接对象\n\t * @param entity 数据实体类（包含表名）\n\t * @param keys   查找字段，某些数据库此字段必须，如H2，某些数据库无需此字段，如MySQL（通过主键）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常，或方言数据不支持此操作\n\t * @since 5.7.20\n\t */\n\tdefault PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tthrow new SQLException(\"Unsupported upsert operation of \" + dialectName());\n\t}\n\n\n\t/**\n\t * 方言名\n\t *\n\t * @return 方言名\n\t * @since 5.5.3\n\t */\n\tString dialectName();\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java",
    "content": "package cn.hutool.db.dialect;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.ClassLoaderUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.dialect.impl.*;\nimport cn.hutool.log.StaticLog;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.util.Map;\n\n/**\n * 方言工厂类\n *\n * @author loolly\n */\npublic class DialectFactory implements DriverNamePool {\n\n\tprivate static final Map<DataSource, Dialect> DIALECT_POOL = new SafeConcurrentHashMap<>();\n\n\tprivate DialectFactory() {\n\t}\n\n\t/**\n\t * 根据驱动名创建方言<br>\n\t * 驱动名是不分区大小写完全匹配的\n\t *\n\t * @param driverName JDBC驱动类名\n\t * @return 方言\n\t */\n\tpublic static Dialect newDialect(String driverName) {\n\t\tfinal Dialect dialect = internalNewDialect(driverName);\n\t\tStaticLog.debug(\"Use Dialect: [{}].\", dialect.getClass().getSimpleName());\n\t\treturn dialect;\n\t}\n\n\t/**\n\t * 根据驱动名创建方言<br>\n\t * 驱动名是不分区大小写完全匹配的\n\t *\n\t * @param driverName JDBC驱动类名\n\t * @return 方言\n\t */\n\tprivate static Dialect internalNewDialect(String driverName) {\n\t\tif (StrUtil.isNotBlank(driverName)) {\n\t\t\tif (DRIVER_MYSQL.equalsIgnoreCase(driverName) || DRIVER_MYSQL_V6.equalsIgnoreCase(driverName)\n\t\t\t\t|| DRIVER_GOLDENDB.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new MysqlDialect();\n\t\t\t} else if (DRIVER_ORACLE.equalsIgnoreCase(driverName) || DRIVER_ORACLE_OLD.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new OracleDialect();\n\t\t\t} else if (DRIVER_SQLLITE3.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new Sqlite3Dialect();\n\t\t\t} else if (DRIVER_POSTGRESQL.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new PostgresqlDialect();\n\t\t\t} else if (DRIVER_H2.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new H2Dialect();\n\t\t\t} else if (DRIVER_SQLSERVER.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new SqlServer2012Dialect();\n\t\t\t} else if (DRIVER_PHOENIX.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new PhoenixDialect();\n\t\t\t} else if (DRIVER_DM7.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new DmDialect();\n\t\t\t} else if (DRIVER_HANA.equalsIgnoreCase(driverName)) {\n\t\t\t\treturn new HanaDialect();\n\t\t\t}\n\t\t}\n\t\t// 无法识别可支持的数据库类型默认使用ANSI方言，可兼容大部分SQL语句\n\t\treturn new AnsiSqlDialect();\n\t}\n\n\t/**\n\t * 通过JDBC URL等信息识别JDBC驱动名\n\t *\n\t * @param nameContainsProductInfo 包含数据库标识的字符串\n\t * @return 驱动\n\t */\n\tpublic static String identifyDriver(String nameContainsProductInfo) {\n\t\treturn identifyDriver(nameContainsProductInfo, null);\n\t}\n\n\t/**\n\t * 通过JDBC URL等信息识别JDBC驱动名\n\t *\n\t * @param nameContainsProductInfo 包含数据库标识的字符串\n\t * @param classLoader             类加载器，{@code null}表示默认上下文的类加载器\n\t * @return 驱动\n\t */\n\tpublic static String identifyDriver(String nameContainsProductInfo, ClassLoader classLoader) {\n\t\tif (StrUtil.isBlank(nameContainsProductInfo)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 全部转为小写，忽略大小写\n\t\tnameContainsProductInfo = StrUtil.cleanBlank(nameContainsProductInfo.toLowerCase());\n\n\t\t// 首先判断是否为标准的JDBC URL，截取jdbc:xxxx:中间部分\n\t\tfinal String name = ReUtil.getGroup1(\"jdbc:(.*?):\", nameContainsProductInfo);\n\t\tif (StrUtil.isNotBlank(name)) {\n\t\t\tnameContainsProductInfo = name;\n\t\t}\n\n\t\tString driver = null;\n\t\tif (nameContainsProductInfo.contains(\"mysql\") || nameContainsProductInfo.contains(\"cobar\")) {\n\t\t\tdriver = ClassLoaderUtil.isPresent(DRIVER_MYSQL_V6, classLoader) ? DRIVER_MYSQL_V6 : DRIVER_MYSQL;\n\t\t} else if (nameContainsProductInfo.contains(\"oceanbase\")) {\n\t\t\tdriver = DRIVER_OCEANBASE;\n\t\t} else if (nameContainsProductInfo.contains(\"oracle\")) {\n\t\t\tdriver = ClassLoaderUtil.isPresent(DRIVER_ORACLE, classLoader) ? DRIVER_ORACLE : DRIVER_ORACLE_OLD;\n\t\t} else if (nameContainsProductInfo.contains(\"postgresql\")) {\n\t\t\tdriver = DRIVER_POSTGRESQL;\n\t\t} else if (nameContainsProductInfo.contains(\"sqlite\")) {\n\t\t\tdriver = DRIVER_SQLLITE3;\n\t\t} else if (nameContainsProductInfo.contains(\"sqlserver\") || nameContainsProductInfo.contains(\"microsoft\")) {\n\t\t\tdriver = DRIVER_SQLSERVER;\n\t\t} else if (nameContainsProductInfo.contains(\"hive2\")) {\n\t\t\tdriver = DRIVER_HIVE2;\n\t\t} else if (nameContainsProductInfo.contains(\"hive\")) {\n\t\t\tdriver = DRIVER_HIVE;\n\t\t} else if (nameContainsProductInfo.contains(\"h2\")) {\n\t\t\tdriver = DRIVER_H2;\n\t\t} else if (nameContainsProductInfo.contains(\"derby\")) {\n\t\t\t// 嵌入式Derby数据库\n\t\t\tdriver = DRIVER_DERBY;\n\t\t} else if (nameContainsProductInfo.contains(\"hsqldb\")) {\n\t\t\t// HSQLDB\n\t\t\tdriver = DRIVER_HSQLDB;\n\t\t} else if (nameContainsProductInfo.contains(\"dm\")) {\n\t\t\t// 达梦7\n\t\t\tdriver = DRIVER_DM7;\n\t\t} else if (nameContainsProductInfo.contains(\"kingbase8\")) {\n\t\t\t// 人大金仓8\n\t\t\tdriver = DRIVER_KINGBASE8;\n\t\t} else if (nameContainsProductInfo.contains(\"ignite\")) {\n\t\t\t// Ignite thin\n\t\t\tdriver = DRIVER_IGNITE_THIN;\n\t\t} else if (nameContainsProductInfo.contains(\"clickhouse\")) {\n\t\t\t// ClickHouse\n\t\t\tdriver = DRIVER_CLICK_HOUSE;\n\t\t} else if (nameContainsProductInfo.contains(\"highgo\")) {\n\t\t\t// 瀚高\n\t\t\tdriver = DRIVER_HIGHGO;\n\t\t} else if (nameContainsProductInfo.contains(\"db2\")) {\n\t\t\t// DB2\n\t\t\tdriver = DRIVER_DB2;\n\t\t} else if (nameContainsProductInfo.contains(\"xugu\")) {\n\t\t\t// 虚谷\n\t\t\tdriver = DRIVER_XUGU;\n\t\t} else if (nameContainsProductInfo.contains(\"phoenix\")) {\n\t\t\t// Apache Phoenix\n\t\t\tdriver = DRIVER_PHOENIX;\n\t\t} else if (nameContainsProductInfo.contains(\"zenith\")) {\n\t\t\t// 华为高斯\n\t\t\tdriver = DRIVER_GAUSS;\n\t\t} else if (nameContainsProductInfo.contains(\"oscar\")) {\n\t\t\t// 神州数据库\n\t\t\tdriver = DRIVER_OSCAR;\n\t\t} else if (nameContainsProductInfo.contains(\"sybase\")) {\n\t\t\t// Sybase\n\t\t\tdriver = DRIVER_SYBASE;\n\t\t} else if (nameContainsProductInfo.contains(\"mariadb\")) {\n\t\t\t// mariadb\n\t\t\tdriver = DRIVER_MARIADB;\n\t\t} else if (nameContainsProductInfo.contains(\"opengauss\")) {\n\t\t\t// OpenGauss\n\t\t\tdriver = DRIVER_OPENGAUSS;\n\t\t} else if (nameContainsProductInfo.contains(\"goldendb\")) {\n\t\t\t// GoldenDB\n\t\t\tdriver = DRIVER_GOLDENDB;\n\t\t} else if (nameContainsProductInfo.contains(\"sap\")) {\n\t\t\t// sap hana\n\t\t\tdriver = DRIVER_HANA;\n\t\t} else if (nameContainsProductInfo.contains(\"gbasedbt-sqli\")) {\n\t\t\t// GBase 8s，见：https://www.gbase.cn/community/post/4029\n\t\t\tdriver = DRIVER_GBASE8S;\n\t\t} else if (nameContainsProductInfo.contains(\"gbase8c\")) {\n\t\t\t// GBase 8c，见：https://www.gbase.cn/download/gbase-8c?category=DRIVER_PACKAGE 页面 GBase8c_JDBC.zip 中的《JDBC 使用手册_V1.0_20230818.pdf》p14\n\t\t\tdriver = DRIVER_GBASE8C;\n\t\t} else if (nameContainsProductInfo.contains(\"gbase\")) {\n\t\t\t// 南大通用数据库 GBase 8a\n\t\t\tdriver = DRIVER_GBASE;\n\t\t} else if (nameContainsProductInfo.contains(\"tdsql-pg\")) {\n\t\t\t// 腾讯数据库 TDSQL PostgreSQL 版本，见：https://cloud.tencent.com/document/product/1129/116487\n\t\t\tdriver = DRIVER_TDSQL_POSTGRESQL;\n\t\t} else if (nameContainsProductInfo.contains(\"snowflake\")) {\n\t\t\t// Snowflake，见：https://docs.snowflake.cn/zh/developer-guide/jdbc/jdbc-configure#label-jdbc-connection-string\n\t\t\tdriver = DRIVER_SNOWFLAKE;\n\t\t} else if (nameContainsProductInfo.contains(\"teradata\")) {\n\t\t\t// Teradata，见：https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/frameset.html 页面 JDBC Interfaces A-L 部分\n\t\t\tdriver = DRIVER_TERADATA;\n\t\t}\n\n\t\treturn driver;\n\t}\n\n\t/**\n\t * 获取共享方言\n\t *\n\t * @param ds 数据源，每一个数据源对应一个唯一方言\n\t * @return {@link Dialect}方言\n\t */\n\tpublic static Dialect getDialect(DataSource ds) {\n\t\tDialect dialect = DIALECT_POOL.get(ds);\n\t\tif (null == dialect) {\n\t\t\t// 数据源作为锁的意义在于：不同数据源不会导致阻塞，相同数据源获取方言时可保证互斥\n\t\t\t//noinspection SynchronizationOnLocalVariableOrMethodParameter\n\t\t\tsynchronized (ds) {\n\t\t\t\tdialect = DIALECT_POOL.computeIfAbsent(ds, DialectFactory::newDialect);\n\t\t\t}\n\t\t}\n\t\treturn dialect;\n\t}\n\n\t/**\n\t * 创建方言\n\t *\n\t * @param ds 数据源\n\t * @return 方言\n\t */\n\tpublic static Dialect newDialect(DataSource ds) {\n\t\treturn newDialect(DriverUtil.identifyDriver(ds));\n\t}\n\n\t/**\n\t * 创建方言\n\t *\n\t * @param conn 数据库连接对象\n\t * @return 方言\n\t */\n\tpublic static Dialect newDialect(Connection conn) {\n\t\treturn newDialect(DriverUtil.identifyDriver(conn));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/DialectName.java",
    "content": "package cn.hutool.db.dialect;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 方言名<br>\n * 方言枚举列出了Hutool支持的所有数据库方言\n *\n * @author Looly\n */\npublic enum DialectName {\n\tANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM, HANA;\n\n\t/**\n\t * 是否为指定数据库方言，检查时不分区大小写\n\t *\n\t * @param dialectName 方言名\n\t * @return 是否时Oracle数据库\n\t * @since 5.7.2\n\t */\n\tpublic boolean match(String dialectName) {\n\t\treturn StrUtil.equalsIgnoreCase(dialectName, name());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/DriverNamePool.java",
    "content": "package cn.hutool.db.dialect;\n\n/**\n * 常用数据库驱动池\n *\n * @author looly\n * @since 5.6.3\n */\npublic interface DriverNamePool {\n\n\t/**\n\t * JDBC 驱动 MySQL\n\t */\n\tString DRIVER_MYSQL = \"com.mysql.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 Oceanbase\n\t */\n\tString DRIVER_OCEANBASE = \"com.oceanbase.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 MySQL，在6.X版本中变动驱动类名，且使用SPI机制\n\t */\n\tString DRIVER_MYSQL_V6 = \"com.mysql.cj.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 MariaDB\n\t */\n\tString DRIVER_MARIADB = \"org.mariadb.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 Oracle\n\t */\n\tString DRIVER_ORACLE = \"oracle.jdbc.OracleDriver\";\n\t/**\n\t * JDBC 驱动 Oracle，旧版使用\n\t */\n\tString DRIVER_ORACLE_OLD = \"oracle.jdbc.driver.OracleDriver\";\n\t/**\n\t * JDBC 驱动 PostgreSQL\n\t */\n\tString DRIVER_POSTGRESQL = \"org.postgresql.Driver\";\n\t/**\n\t * JDBC 驱动 SQLLite3\n\t */\n\tString DRIVER_SQLLITE3 = \"org.sqlite.JDBC\";\n\t/**\n\t * JDBC 驱动 SQLServer\n\t */\n\tString DRIVER_SQLSERVER = \"com.microsoft.sqlserver.jdbc.SQLServerDriver\";\n\t/**\n\t * JDBC 驱动 Hive\n\t */\n\tString DRIVER_HIVE = \"org.apache.hadoop.hive.jdbc.HiveDriver\";\n\t/**\n\t * JDBC 驱动 Hive2\n\t */\n\tString DRIVER_HIVE2 = \"org.apache.hive.jdbc.HiveDriver\";\n\t/**\n\t * JDBC 驱动 H2\n\t */\n\tString DRIVER_H2 = \"org.h2.Driver\";\n\t/**\n\t * JDBC 驱动 Derby\n\t */\n\tString DRIVER_DERBY = \"org.apache.derby.jdbc.AutoloadedDriver\";\n\t/**\n\t * JDBC 驱动 HSQLDB\n\t */\n\tString DRIVER_HSQLDB = \"org.hsqldb.jdbc.JDBCDriver\";\n\t/**\n\t * JDBC 驱动 达梦7\n\t */\n\tString DRIVER_DM7 = \"dm.jdbc.driver.DmDriver\";\n\t/**\n\t * JDBC 驱动 人大金仓\n\t */\n\tString DRIVER_KINGBASE8 = \"com.kingbase8.Driver\";\n\t/**\n\t * JDBC 驱动 Ignite thin\n\t */\n\tString DRIVER_IGNITE_THIN = \"org.apache.ignite.IgniteJdbcThinDriver\";\n\t/**\n\t * JDBC 驱动 ClickHouse\n\t */\n\tString DRIVER_CLICK_HOUSE = \"com.clickhouse.jdbc.ClickHouseDriver\";\n\t/**\n\t * JDBC 驱动 瀚高数据库\n\t */\n\tString DRIVER_HIGHGO = \"com.highgo.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 DB2\n\t */\n\tString DRIVER_DB2 = \"com.ibm.db2.jdbc.app.DB2Driver\";\n\t/**\n\t * JDBC 驱动 虚谷数据库\n\t */\n\tString DRIVER_XUGU = \"com.xugu.cloudjdbc.Driver\";\n\t/**\n\t * JDBC 驱动 Apache Phoenix\n\t */\n\tString DRIVER_PHOENIX = \"org.apache.phoenix.jdbc.PhoenixDriver\";\n\t/**\n\t * JDBC 驱动 华为高斯\n\t */\n\tString DRIVER_GAUSS = \"com.huawei.gauss.jdbc.ZenithDriver\";\n\t/**\n\t * JDBC 驱动 南大通用 GBase 8a\n\t */\n\tString DRIVER_GBASE = \"com.gbase.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 南大通用 GBase 8s<br>\n\t * 见：https://www.gbase.cn/community/post/4029\n\t */\n\tString DRIVER_GBASE8S = \"com.gbasedbt.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 南大通用 GBase 8c<br>\n\t * 见：https://www.gbase.cn/download/gbase-8c?category=DRIVER_PACKAGE 页面 GBase8c_JDBC.zip 中的《JDBC 使用手册_V1.0_20230818.pdf》p14\n\t */\n\tString DRIVER_GBASE8C = \"cn.gbase8c.Driver\";\n\t/**\n\t * JDBC 驱动 腾讯 TDSQL PostgreSQL 版本<br>\n\t * 见：https://cloud.tencent.com/document/product/1129/116487\n\t */\n\tString DRIVER_TDSQL_POSTGRESQL = \"com.tencentcloud.tdsql.pg.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 腾讯 TDSQL-H LibraDB<br>\n\t * 见：https://cloud.tencent.com/document/product/1488/79810\n\t */\n\tString DRIVER_TDSQL_H_LIBRADB = \"ru.yandex.clickhouse.ClickHouseDriver\";\n\t/**\n\t * JDBC 驱动 Snowflake<br>\n\t * 见：https://docs.snowflake.cn/zh/developer-guide/jdbc/jdbc-configure#label-jdbc-connection-string\n\t */\n\tString DRIVER_SNOWFLAKE = \"net.snowflake.client.jdbc.SnowflakeDriver\";\n\t/**\n\t * JDBC 驱动 Teradata<br>\n\t * 见：https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/frameset.html 页面 JDBC Interfaces A-L 部分\n\t */\n\tString DRIVER_TERADATA = \"com.teradata.jdbc.TeraDriver\";\n\t/**\n\t * JDBC 驱动 神州数据库\n\t */\n\tString DRIVER_OSCAR = \"com.oscar.Driver\";\n\t/**\n\t * JDBC 驱动 Sybase\n\t */\n\tString DRIVER_SYBASE = \"com.sybase.jdbc4.jdbc.SybDriver\";\n\t/**\n\t * JDBC 驱动 OpenGauss\n\t */\n\tString DRIVER_OPENGAUSS = \"org.opengauss.Driver\";\n\t/**\n\t * JDBC 驱动 GoldenDB\n\t */\n\tString DRIVER_GOLDENDB = \"com.goldendb.jdbc.Driver\";\n\t/**\n\t * JDBC 驱动 Sap Hana\n\t */\n\tString DRIVER_HANA = \"com.sap.db.jdbc.Driver\";\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/DriverUtil.java",
    "content": "package cn.hutool.db.dialect;\n\nimport java.sql.Connection;\nimport java.sql.DatabaseMetaData;\nimport java.sql.SQLException;\n\nimport javax.sql.DataSource;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.db.ds.DataSourceWrapper;\n\n/**\n * 驱动相关工具类，包括自动获取驱动类名\n * \n * @author looly\n * @since 4.0.10\n */\npublic class DriverUtil {\n\t/**\n\t * 通过JDBC URL等信息识别JDBC驱动名\n\t * \n\t * @param nameContainsProductInfo 包含数据库标识的字符串\n\t * @return 驱动\n\t * @see DialectFactory#identifyDriver(String)\n\t */\n\tpublic static String identifyDriver(String nameContainsProductInfo) {\n\t\treturn DialectFactory.identifyDriver(nameContainsProductInfo);\n\t}\n\n\t/**\n\t * 识别JDBC驱动名\n\t * \n\t * @param ds 数据源\n\t * @return 驱动\n\t */\n\tpublic static String identifyDriver(DataSource ds) {\n\t\tif(ds instanceof DataSourceWrapper) {\n\t\t\tfinal String driver = ((DataSourceWrapper)ds).getDriver();\n\t\t\tif(StrUtil.isNotBlank(driver)) {\n\t\t\t\treturn driver;\n\t\t\t}\n\t\t}\n\t\t\n\t\tConnection conn = null;\n\t\tString driver;\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tconn = ds.getConnection();\n\t\t\t} catch (SQLException e) {\n\t\t\t\tthrow new DbRuntimeException(\"Get Connection error !\", e);\n\t\t\t} catch (NullPointerException e) {\n\t\t\t\tthrow new DbRuntimeException(\"Unexpected NullPointException, maybe [jdbcUrl] or [url] is empty!\", e);\n\t\t\t}\n\t\t\tdriver = identifyDriver(conn);\n\t\t} finally {\n\t\t\tDbUtil.close(conn);\n\t\t}\n\n\t\treturn driver;\n\t}\n\n\t/**\n\t * 识别JDBC驱动名\n\t * \n\t * @param conn 数据库连接对象\n\t * @return 驱动\n\t * @throws DbRuntimeException SQL异常包装，获取元数据信息失败\n\t */\n\tpublic static String identifyDriver(Connection conn) throws DbRuntimeException {\n\t\tString driver;\n\t\tDatabaseMetaData meta;\n\t\ttry {\n\t\t\tmeta = conn.getMetaData();\n\t\t\tdriver = identifyDriver(meta.getDatabaseProductName());\n\t\t\tif (StrUtil.isBlank(driver)) {\n\t\t\t\tdriver = identifyDriver(meta.getDriverName());\n\t\t\t}\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(\"Identify driver error!\", e);\n\t\t}\n\n\t\treturn driver;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.Dialect;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.Condition;\nimport cn.hutool.db.sql.Query;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.Set;\n\n/**\n * ANSI SQL 方言\n *\n * @author loolly\n *\n */\npublic class AnsiSqlDialect implements Dialect {\n\tprivate static final long serialVersionUID = 2088101129774974580L;\n\n\tprotected Wrapper wrapper = new Wrapper();\n\n\t@Override\n\tpublic Wrapper getWrapper() {\n\t\treturn this.wrapper;\n\t}\n\n\t@Override\n\tpublic void setWrapper(Wrapper wrapper) {\n\t\tthis.wrapper = wrapper;\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException {\n\t\tfinal SqlBuilder insert = SqlBuilder.create(wrapper).insert(entity, this.dialectName());\n\n\t\treturn StatementUtil.prepareStatement(conn, insert);\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException {\n\t\tif (ArrayUtil.isEmpty(entities)) {\n\t\t\tthrow new DbRuntimeException(\"Entities for batch insert is empty !\");\n\t\t}\n\t\t// 批量，根据第一行数据结构生成SQL占位符\n\t\tfinal SqlBuilder insert = SqlBuilder.create(wrapper).insert(entities[0], this.dialectName());\n\t\tfinal Set<String> fields = CollUtil.filter(entities[0].keySet(), StrUtil::isNotBlank);\n\t\treturn StatementUtil.prepareStatementForBatch(conn, insert.build(), fields, entities);\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForDelete(Connection conn, Query query) throws SQLException {\n\t\tAssert.notNull(query, \"query must be not null !\");\n\n\t\tfinal Condition[] where = query.getWhere();\n\t\tif (ArrayUtil.isEmpty(where)) {\n\t\t\t// 对于无条件的删除语句直接抛出异常禁止，防止误删除\n\t\t\tthrow new SQLException(\"No 'WHERE' condition, we can't prepared statement for delete everything.\");\n\t\t}\n\t\tfinal SqlBuilder delete = SqlBuilder.create(wrapper).delete(query.getFirstTableName()).where(where);\n\n\t\treturn StatementUtil.prepareStatement(conn, delete);\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException {\n\t\tAssert.notNull(query, \"query must be not null !\");\n\n\t\tfinal Condition[] where = query.getWhere();\n\t\tif (ArrayUtil.isEmpty(where)) {\n\t\t\t// 对于无条件的删除语句直接抛出异常禁止，防止误删除\n\t\t\tthrow new SQLException(\"No 'WHERE' condition, we can't prepare statement for update everything.\");\n\t\t}\n\n\t\tfinal SqlBuilder update = SqlBuilder.create(wrapper).update(entity).where(where);\n\n\t\treturn StatementUtil.prepareStatement(conn, update);\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForFind(Connection conn, Query query) throws SQLException {\n\t\treturn psForPage(conn, query);\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForPage(Connection conn, Query query) throws SQLException {\n\t\tAssert.notNull(query, \"query must be not null !\");\n\t\tif (StrUtil.hasBlank(query.getTableNames())) {\n\t\t\tthrow new DbRuntimeException(\"Table name must be not empty !\");\n\t\t}\n\n\t\tfinal SqlBuilder find = SqlBuilder.create(wrapper).query(query);\n\t\treturn psForPage(conn, find, query.getPage());\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForPage(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException {\n\t\t// 根据不同数据库在查询SQL语句基础上包装其分页的语句\n\t\tif(null != page){\n\t\t\tsqlBuilder = wrapPageSql(sqlBuilder.orderBy(page.getOrders()), page);\n\t\t}\n\t\treturn StatementUtil.prepareStatement(conn, sqlBuilder);\n\t}\n\n\t/**\n\t * 根据不同数据库在查询SQL语句基础上包装其分页的语句<br>\n\t * 各自数据库通过重写此方法实现最小改动情况下修改分页语句\n\t *\n\t * @param find 标准查询语句\n\t * @param page 分页对象\n\t * @return 分页语句\n\t * @since 3.2.3\n\t */\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\t// limit A offset B 表示：A就是你需要多少行，B就是查询的起点位置。\n\t\treturn find\n\t\t\t\t.append(\" limit \")\n\t\t\t\t.append(page.getPageSize())\n\t\t\t\t.append(\" offset \")\n\t\t\t\t.append(page.getStartPosition());\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.ANSI.name();\n\t}\n\n\t// ---------------------------------------------------------------------------- Protected method start\n\t// ---------------------------------------------------------------------------- Protected method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/DmDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 达梦数据库方言\n *\n * @author wb04307201\n */\npublic class DmDialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = 3415348435502927423L;\n\n\tpublic DmDialect() {\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.DM.name();\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tAssert.notEmpty(keys, \"Keys must be not empty for DM MERGE SQL.\");\n\t\tSqlBuilder.validateEntity(entity);\n\t\tfinal SqlBuilder builder = SqlBuilder.create(wrapper);\n\t\tList<String> keyList = Arrays.asList(keys);\n\n\t\tfinal StringBuilder keyFieldsPart = new StringBuilder();\n\t\tfinal StringBuilder updateFieldsPart = new StringBuilder();\n\t\tfinal StringBuilder insertFieldsPart = new StringBuilder();\n\t\tfinal StringBuilder insertPlaceHolder = new StringBuilder();\n\n\t\t// 构建字段部分和参数占位符部分\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field) && keyList.contains(field)) {\n\t\t\t\tif (keyFieldsPart.length() > 0) {\n\t\t\t\t\tkeyFieldsPart.append(\" and \");\n\t\t\t\t}\n\t\t\t\tkeyFieldsPart.append(field + \"= ?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field) && !keyList.contains(field)) {\n\t\t\t\tif (updateFieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tupdateFieldsPart.append(\", \");\n\t\t\t\t}\n\t\t\t\tupdateFieldsPart.append(field).append(\"= ?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (insertFieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tinsertFieldsPart.append(\", \");\n\t\t\t\t\tinsertPlaceHolder.append(\", \");\n\t\t\t\t}\n\t\t\t\tinsertFieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field);\n\t\t\t\tinsertPlaceHolder.append(\"?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != this.wrapper) {\n\t\t\ttableName = this.wrapper.wrap(tableName);\n\t\t}\n\n\t\tbuilder.append(\"MERGE INTO \").append(tableName).append(\" USING DUAL ON \").append(keyFieldsPart).append(\" WHEN MATCHED THEN UPDATE SET \").append(updateFieldsPart).append(\" WHEN NOT MATCHED THEN INSERT (\").append(insertFieldsPart).append(\") VALUES (\").append(insertPlaceHolder).append(\")\");\n\n\t\treturn StatementUtil.prepareStatement(conn, builder);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n/**\n * H2数据库方言\n *\n * @author loolly\n */\npublic class H2Dialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = 1490520247974768214L;\n\n\tpublic H2Dialect() {\n//\t\twrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.H2.name();\n\t}\n\n\t@Override\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\t// limit A , B 表示：A就是查询的起点位置，B就是你需要多少行。\n\t\treturn find.append(\" limit \").append(page.getStartPosition()).append(\" , \").append(page.getPageSize());\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tAssert.notEmpty(keys, \"Keys must be not empty for H2 MERGE SQL.\");\n\t\tSqlBuilder.validateEntity(entity);\n\t\tfinal SqlBuilder builder = SqlBuilder.create(wrapper);\n\n\t\tfinal StringBuilder fieldsPart = new StringBuilder();\n\t\tfinal StringBuilder placeHolder = new StringBuilder();\n\n\t\t// 构建字段部分和参数占位符部分\n\t\tentity.forEach((field, value)->{\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (fieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tfieldsPart.append(\", \");\n\t\t\t\t\tplaceHolder.append(\", \");\n\t\t\t\t}\n\n\t\t\t\tfieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field);\n\t\t\t\tplaceHolder.append(\"?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != this.wrapper) {\n\t\t\ttableName = this.wrapper.wrap(tableName);\n\t\t\tkeys = wrapper.wrap(keys);\n\t\t}\n\t\tbuilder.append(\"MERGE INTO \").append(tableName)\n\t\t\t\t// 字段列表\n\t\t\t\t.append(\" (\").append(fieldsPart)\n\t\t\t\t// 更新关键字列表\n\t\t\t\t.append(\") KEY(\").append(ArrayUtil.join(keys, \", \"))\n\t\t\t\t// 更新值列表\n\t\t\t\t.append(\") VALUES (\").append(placeHolder).append(\")\");\n\n\t\treturn StatementUtil.prepareStatement(conn, builder);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/HanaDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Hana数据库方言\n *\n * @author daoyou.dev\n */\npublic class HanaDialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t */\n\tpublic HanaDialect() {\n\t\twrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.HANA.name();\n\t}\n\n\t@Override\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\t// SAP HANA 使用 OFFSET LIMIT 分页\n\t\treturn find.append(\" LIMIT \").append(page.getPageSize())\n\t\t\t.append(\" OFFSET \").append(page.getStartPosition());\n\t}\n\n\t/**\n\t * 构建用于upsert的{@link PreparedStatement}。\n\t * SAP HANA 使用 MERGE INTO 语法来实现 UPSERT 操作。\n\t * <p>\n\t * 生成 SQL 语法为：\n\t * <pre>\n\t *     MERGE INTO demo AS target\n\t *     USING (SELECT ? AS a, ? AS b, ? AS c FROM DUMMY) AS source\n\t *     ON target.id = source.id\n\t *     WHEN MATCHED THEN\n\t *         UPDATE SET target.a = source.a, target.b = source.b, target.c = source.c\n\t *     WHEN NOT MATCHED THEN\n\t *         INSERT (a, b, c) VALUES (source.a, source.b, source.c);\n\t * </pre>\n\t *\n\t * @param conn   数据库连接对象\n\t * @param entity 数据实体类（包含表名）\n\t * @param keys   主键字段数组，通常用于确定匹配条件（联合主键）\n\t * @return PreparedStatement\n\t * @throws SQLException SQL 执行异常\n\t */\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tSqlBuilder.validateEntity(entity);\n\t\tfinal SqlBuilder builder = SqlBuilder.create(wrapper);\n\n\t\tfinal List<String> columns = new ArrayList<>();\n\t\tfinal List<Object> values = new ArrayList<>();\n\n\t\t// 构建字段部分和参数占位符部分\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tcolumns.add(wrapper != null ? wrapper.wrap(field) : field);\n\t\t\t\tvalues.add(value);\n\t\t\t}\n\t\t});\n\n\t\tString tableName = entity.getTableName();\n\t\tif (wrapper != null) {\n\t\t\ttableName = wrapper.wrap(tableName);\n\t\t}\n\n\t\t// 构建 UPSERT 语句\n\t\tbuilder.append(\"UPSERT \").append(tableName).append(\" (\");\n\t\tbuilder.append(String.join(\", \", columns));\n\t\tbuilder.append(\") VALUES (\");\n\t\tbuilder.append(String.join(\", \", Collections.nCopies(columns.size(), \"?\")));\n\t\tbuilder.append(\") WITH PRIMARY KEY\");\n\n\t\treturn StatementUtil.prepareStatement(conn, builder.toString(), values);\n\t}\n}\n\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n/**\n * MySQL方言\n * @author loolly\n *\n */\npublic class MysqlDialect extends AnsiSqlDialect{\n\tprivate static final long serialVersionUID = -3734718212043823636L;\n\n\tpublic MysqlDialect() {\n\t\twrapper = new Wrapper('`');\n\t}\n\n\t@Override\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\treturn find.append(\" LIMIT \").append(page.getStartPosition()).append(\", \").append(page.getPageSize());\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.MYSQL.toString();\n\t}\n\n\t/**\n\t * 构建用于upsert的{@link PreparedStatement}<br>\n\t * MySQL通过主键方式实现Upsert，故keys无效，生成SQL语法为：\n\t * <pre>\n\t *     INSERT INTO demo(a,b,c) values(?, ?, ?) ON DUPLICATE KEY UPDATE a=values(a), b=values(b), c=values(c);\n\t * </pre>\n\t *\n\t * @param conn   数据库连接对象\n\t * @param entity 数据实体类（包含表名）\n\t * @param keys   此参数无效\n\t * @return PreparedStatement\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.20\n\t */\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tSqlBuilder.validateEntity(entity);\n\t\tfinal SqlBuilder builder = SqlBuilder.create(wrapper);\n\n\t\tfinal StringBuilder fieldsPart = new StringBuilder();\n\t\tfinal StringBuilder placeHolder = new StringBuilder();\n\t\tfinal StringBuilder updateHolder = new StringBuilder();\n\n\t\t// 构建字段部分和参数占位符部分\n\t\tentity.forEach((field, value)->{\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (fieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tfieldsPart.append(\", \");\n\t\t\t\t\tplaceHolder.append(\", \");\n\t\t\t\t\tupdateHolder.append(\", \");\n\t\t\t\t}\n\n\t\t\t\tfield = (null != wrapper) ? wrapper.wrap(field) : field;\n\t\t\t\tfieldsPart.append(field);\n\t\t\t\tupdateHolder.append(field).append(\"=values(\").append(field).append(\")\");\n\t\t\t\tplaceHolder.append(\"?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != this.wrapper) {\n\t\t\ttableName = this.wrapper.wrap(tableName);\n\t\t}\n\t\tbuilder.append(\"INSERT INTO \").append(tableName)\n\t\t\t\t// 字段列表\n\t\t\t\t.append(\" (\").append(fieldsPart)\n\t\t\t\t// 更新值列表\n\t\t\t\t.append(\") VALUES (\").append(placeHolder)\n\t\t\t\t// 主键冲突后的更新操作\n\t\t\t\t.append(\") ON DUPLICATE KEY UPDATE \").append(updateHolder);\n\n\t\treturn StatementUtil.prepareStatement(conn, builder);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\n\n/**\n * Oracle 方言\n *\n * @author loolly\n */\npublic class OracleDialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = 6122761762247483015L;\n\n\t/**\n\t * 检查字段值是否为Oracle自增字段，自增字段以`.nextval`结尾\n\t *\n\t * @param value 检查的字段值\n\t * @return 是否为Oracle自增字段\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isNextVal(Object value) {\n\t\treturn (value instanceof CharSequence) && StrUtil.endWithIgnoreCase(value.toString(), \".nextval\");\n\t}\n\n\tpublic OracleDialect() {\n\t\t//Oracle所有字段名用双引号包围，防止字段名或表名与系统关键字冲突\n\t\t//wrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\tfinal int[] startEnd = page.getStartEnd();\n\t\treturn find\n\t\t\t\t.insertPreFragment(\"SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( \")\n\t\t\t\t.append(\" ) row_ where rownum <= \").append(startEnd[1])//\n\t\t\t\t.append(\") table_alias_\")//\n\t\t\t\t.append(\" where table_alias_.rownum_ > \").append(startEnd[0]);//\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.ORACLE.name();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/PhoenixDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.Query;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n/**\n * Phoenix数据库方言\n *\n * @author loolly\n * @since 5.7.2\n */\npublic class PhoenixDialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic PhoenixDialect() {\n//\t\twrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException {\n\t\t// Phoenix的插入、更新语句是统一的，统一使用upsert into关键字\n\t\t// Phoenix只支持通过主键更新操作，因此query无效，自动根据entity中的主键更新\n\t\treturn super.psForInsert(conn, entity);\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.PHOENIX.name();\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\t// Phoenix只支持通过主键更新操作，因此query无效，自动根据entity中的主键更新\n\t\treturn psForInsert(conn, entity);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\n\n\n/**\n * Postgree方言\n * @author loolly\n *\n */\npublic class PostgresqlDialect extends AnsiSqlDialect{\n\tprivate static final long serialVersionUID = 3889210427543389642L;\n\n\t/**\n\t * 构造\n\t */\n\tpublic PostgresqlDialect() {\n\t\twrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.POSTGRESQL.name();\n\t}\n\n\t@Override\n\tpublic PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException {\n\t\tAssert.notEmpty(keys, \"Keys must be not empty for Postgres.\");\n\t\tSqlBuilder.validateEntity(entity);\n\t\tfinal SqlBuilder builder = SqlBuilder.create(wrapper);\n\n\t\tfinal StringBuilder fieldsPart = new StringBuilder();\n\t\tfinal StringBuilder placeHolder = new StringBuilder();\n\t\tfinal StringBuilder updateHolder = new StringBuilder();\n\n\t\t// 构建字段部分和参数占位符部分\n\t\tentity.forEach((field, value)->{\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (fieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tfieldsPart.append(\", \");\n\t\t\t\t\tplaceHolder.append(\", \");\n\t\t\t\t\tupdateHolder.append(\", \");\n\t\t\t\t}\n\n\t\t\t\tfinal String wrapedField = (null != wrapper) ? wrapper.wrap(field) : field;\n\t\t\t\tfieldsPart.append(wrapedField);\n\t\t\t\tupdateHolder.append(wrapedField).append(\"=EXCLUDED.\").append(wrapedField);\n\t\t\t\tplaceHolder.append(\"?\");\n\t\t\t\tbuilder.addParams(value);\n\t\t\t}\n\t\t});\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != this.wrapper) {\n\t\t\ttableName = this.wrapper.wrap(tableName);\n\t\t\tkeys = wrapper.wrap(keys);\n\t\t}\n\t\tbuilder.append(\"INSERT INTO \").append(tableName)\n\t\t\t\t// 字段列表\n\t\t\t\t.append(\" (\").append(fieldsPart)\n\t\t\t\t// 更新值列表\n\t\t\t\t.append(\") VALUES (\").append(placeHolder)\n\t\t\t\t// 定义检查冲突的主键或字段\n\t\t\t\t.append(\") ON CONFLICT (\").append(ArrayUtil.join(keys,\", \"))\n\t\t\t\t// 主键冲突后的更新操作\n\t\t\t\t.append(\") DO UPDATE SET \").append(updateHolder);\n\n\t\treturn StatementUtil.prepareStatement(conn, builder);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Page;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.Wrapper;\n\n/**\n * SQLServer2012 方言\n * \n * @author loolly\n *\n */\npublic class SqlServer2012Dialect extends AnsiSqlDialect {\n\tprivate static final long serialVersionUID = -37598166015777797L;\n\n\tpublic SqlServer2012Dialect() {\n\t\t//双引号和中括号适用，双引号更广泛\n\t\t wrapper = new Wrapper('\"');\n\t}\n\n\t@Override\n\tprotected SqlBuilder wrapPageSql(SqlBuilder find, Page page) {\n\t\tif (false == StrUtil.containsIgnoreCase(find.toString(), \"order by\")) {\n\t\t\t//offset 分页必须要跟在order by后面，没有情况下补充默认排序\n\t\t\tfind.append(\" order by current_timestamp\");\n\t\t}\n\t\treturn find.append(\" offset \")\n\t\t\t\t.append(page.getStartPosition())//\n\t\t\t\t.append(\" row fetch next \")//row和rows同义词\n\t\t\t\t.append(page.getPageSize())//\n\t\t\t\t.append(\" row only\");//\n\t}\n\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.SQLSERVER2012.name();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java",
    "content": "package cn.hutool.db.dialect.impl;\n\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.sql.Wrapper;\n\n/**\n * SqlLite3方言\n * @author loolly\n *\n */\npublic class Sqlite3Dialect extends AnsiSqlDialect{\n\tprivate static final long serialVersionUID = -3527642408849291634L;\n\n\tpublic Sqlite3Dialect() {\n\t\twrapper = new Wrapper('[', ']');\n\t}\n\t\n\t@Override\n\tpublic String dialectName() {\n\t\treturn DialectName.SQLITE3.name();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/impl/package-info.java",
    "content": "/**\n * 数据库方言实现，包括MySQL、Oracle、PostgreSQL、Sqlite3、H2、SqlServer2012等\n * \n * @author looly\n *\n */\npackage cn.hutool.db.dialect.impl;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/dialect/package-info.java",
    "content": "/**\n * 数据库方言封装，包括数据库方言以及方言自动识别等\n * \n * @author looly\n *\n */\npackage cn.hutool.db.dialect;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/AbstractDSFactory.java",
    "content": "package cn.hutool.db.ds;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.db.GlobalDbConfig;\nimport cn.hutool.db.dialect.DriverUtil;\nimport cn.hutool.setting.Setting;\n\nimport javax.sql.DataSource;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 抽象数据源工厂<br>\n * 此工厂抽象类用于实现数据源的缓存，当用户多次调用{@link #getDataSource(String)} 时，工厂只需创建一次即可。<br>\n * 数据源是与配置文件中的分组相关的，每个分组的数据源相互独立，也就是每个分组的数据源是单例存在的。\n *\n * @author looly\n */\npublic abstract class AbstractDSFactory extends DSFactory {\n\tprivate static final long serialVersionUID = -6407302276272379881L;\n\n\t/**\n\t * 数据库连接配置文件\n\t */\n\tprivate final Setting setting;\n\t/**\n\t * 数据源池\n\t */\n\tprivate final Map<String, DataSourceWrapper> dsMap;\n\n\t/**\n\t * 构造\n\t *\n\t * @param dataSourceName  数据源名称\n\t * @param dataSourceClass 数据库连接池实现类，用于检测所提供的DataSource类是否存在，当传入的DataSource类不存在时抛出ClassNotFoundException<br>\n\t *                        此参数的作用是在detectDSFactory方法自动检测所用连接池时，如果实现类不存在，调用此方法会自动抛出异常，从而切换到下一种连接池的检测。\n\t * @param setting         数据库连接配置，如果为{@code null}，则读取全局自定义或默认配置\n\t */\n\tpublic AbstractDSFactory(String dataSourceName, Class<? extends DataSource> dataSourceClass, Setting setting) {\n\t\tsuper(dataSourceName);\n\t\t//此参数的作用是在detectDSFactory方法自动检测所用连接池时，如果实现类不存在，调用此方法会自动抛出异常，从而切换到下一种连接池的检测。\n\t\tAssert.notNull(dataSourceClass);\n\n\t\tif (null == setting) {\n\t\t\tsetting = GlobalDbConfig.createDbSetting();\n\t\t}\n\n\t\t// 读取配置，用于SQL打印\n\t\tDbUtil.setShowSqlGlobal(setting);\n\n\t\tthis.setting = setting;\n\t\tthis.dsMap = new SafeConcurrentHashMap<>();\n\t}\n\n\t/**\n\t * 获取配置，用于自定义添加配置项\n\t *\n\t * @return Setting\n\t * @since 4.0.3\n\t */\n\tpublic Setting getSetting() {\n\t\treturn this.setting;\n\t}\n\n\t@Override\n\tsynchronized public DataSource getDataSource(String group) {\n\t\tif (group == null) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t}\n\n\t\t// 如果已经存在已有数据源（连接池）直接返回\n\t\tfinal DataSourceWrapper existedDataSource = dsMap.get(group);\n\t\tif (existedDataSource != null) {\n\t\t\treturn existedDataSource;\n\t\t}\n\n\t\tfinal DataSourceWrapper ds = createDataSource(group);\n\t\t// 添加到数据源池中，以备下次使用\n\t\tdsMap.put(group, ds);\n\t\treturn ds;\n\t}\n\n\t/**\n\t * 创建数据源\n\t *\n\t * @param group 分组\n\t * @return {@link DataSourceWrapper} 数据源包装\n\t */\n\tprivate DataSourceWrapper createDataSource(String group) {\n\t\tif (group == null) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal Setting config = setting.getSetting(group);\n\t\tif (MapUtil.isEmpty(config)) {\n\t\t\tthrow new DbRuntimeException(\"No config for group: [{}]\", group);\n\t\t}\n\n\t\t// 基本信息\n\t\tfinal String url = config.getAndRemoveStr(KEY_ALIAS_URL);\n\t\tif (StrUtil.isBlank(url)) {\n\t\t\tthrow new DbRuntimeException(\"No JDBC URL for group: [{}]\", group);\n\t\t}\n\n\t\t// 移除用户可能误加入的show sql配置项\n\t\t// issue#I3VW0R@Gitee\n\t\tDbUtil.removeShowSqlParams(config);\n\n\t\t// 自动识别Driver\n\t\tString driver = config.getAndRemoveStr(KEY_ALIAS_DRIVER);\n\t\tif (StrUtil.isBlank(driver)) {\n\t\t\tdriver = DriverUtil.identifyDriver(url);\n\t\t}\n\t\tfinal String user = config.getAndRemoveStr(KEY_ALIAS_USER);\n\t\tfinal String pass = config.getAndRemoveStr(KEY_ALIAS_PASSWORD);\n\n\t\treturn DataSourceWrapper.wrap(createDataSource(url, driver, user, pass, config), driver);\n\t}\n\n\t/**\n\t * 创建新的{@link DataSource}<br>\n\t *\n\t * @param jdbcUrl     JDBC连接字符串\n\t * @param driver      数据库驱动类名\n\t * @param user        用户名\n\t * @param pass        密码\n\t * @param poolSetting 分组下的连接池配置文件\n\t * @return {@link DataSource}\n\t */\n\tprotected abstract DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting);\n\n\t@Override\n\tpublic void close(String group) {\n\t\tif (group == null) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t}\n\n\t\tDataSourceWrapper ds = dsMap.get(group);\n\t\tif (ds != null) {\n\t\t\tds.close();\n\t\t\tdsMap.remove(group);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void destroy() {\n\t\tif (MapUtil.isNotEmpty(dsMap)) {\n\t\t\tCollection<DataSourceWrapper> values = dsMap.values();\n\t\t\tfor (DataSourceWrapper ds : values) {\n\t\t\t\tds.close();\n\t\t\t}\n\t\t\tdsMap.clear();\n\t\t}\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + ((dataSourceName == null) ? 0 : dataSourceName.hashCode());\n\t\tresult = prime * result + ((setting == null) ? 0 : setting.hashCode());\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tAbstractDSFactory other = (AbstractDSFactory) obj;\n\t\tif (dataSourceName == null) {\n\t\t\tif (other.dataSourceName != null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (!dataSourceName.equals(other.dataSourceName)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (setting == null) {\n\t\t\treturn other.setting == null;\n\t\t} else {\n\t\t\treturn setting.equals(other.setting);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/DSFactory.java",
    "content": "package cn.hutool.db.ds;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.bee.BeeDSFactory;\nimport cn.hutool.db.ds.c3p0.C3p0DSFactory;\nimport cn.hutool.db.ds.dbcp.DbcpDSFactory;\nimport cn.hutool.db.ds.druid.DruidDSFactory;\nimport cn.hutool.db.ds.hikari.HikariDSFactory;\nimport cn.hutool.db.ds.pooled.PooledDSFactory;\nimport cn.hutool.db.ds.tomcat.TomcatDSFactory;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.setting.Setting;\n\nimport javax.sql.DataSource;\nimport java.io.Closeable;\nimport java.io.Serializable;\n\n/**\n * 抽象数据源工厂类<br>\n * 通过实现{@link #getDataSource(String)} 方法实现数据源的获取<br>\n * 如果{@link DataSource} 的实现是数据库连接池库，应该在getDataSource调用时创建数据源并缓存\n *\n * @author Looly\n *\n */\npublic abstract class DSFactory implements Closeable, Serializable{\n\tprivate static final long serialVersionUID = -8789780234095234765L;\n\n\tprivate static final Log log = LogFactory.get();\n\n\t/** 某些数据库需要的特殊配置项需要的配置项 */\n\tpublic static final String[] KEY_CONN_PROPS = {\"remarks\", \"useInformationSchema\"};\n\n\t/** 别名字段名：URL */\n\tpublic static final String[] KEY_ALIAS_URL = { \"url\", \"jdbcUrl\" };\n\t/** 别名字段名：驱动名 */\n\tpublic static final String[] KEY_ALIAS_DRIVER = { \"driver\", \"driverClassName\" };\n\t/** 别名字段名：用户名 */\n\tpublic static final String[] KEY_ALIAS_USER = { \"user\", \"username\" };\n\t/** 别名字段名：密码 */\n\tpublic static final String[] KEY_ALIAS_PASSWORD = { \"pass\", \"password\" };\n\n\t/** 数据源名 */\n\tprotected final String dataSourceName;\n\n\t/**\n\t * 构造\n\t *\n\t * @param dataSourceName 数据源名称\n\t */\n\tpublic DSFactory(String dataSourceName) {\n\t\tthis.dataSourceName = dataSourceName;\n\t}\n\n\t/**\n\t * 获得默认数据源\n\t *\n\t * @return 数据源\n\t */\n\tpublic DataSource getDataSource() {\n\t\treturn getDataSource(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 获得分组对应数据源\n\t *\n\t * @param group 分组名\n\t * @return 数据源\n\t */\n\tpublic abstract DataSource getDataSource(String group);\n\n\t/**\n\t * 关闭默认数据源（空组）\n\t */\n\t@Override\n\tpublic void close() {\n\t\tclose(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 关闭对应数据源\n\t *\n\t * @param group 分组\n\t */\n\tpublic abstract void close(String group);\n\n\t/**\n\t * 销毁工厂类，关闭所有数据源\n\t */\n\tpublic abstract void destroy();\n\n\t// ------------------------------------------------------------------------- Static start\n\t/**\n\t * 获得数据源<br>\n\t * 使用默认配置文件的无分组配置\n\t *\n\t * @return 数据源\n\t */\n\tpublic static DataSource get() {\n\t\treturn get(null);\n\t}\n\n\t/**\n\t * 获得数据源\n\t *\n\t * @param group 配置文件中对应的分组\n\t * @return 数据源\n\t */\n\tpublic static DataSource get(String group) {\n\t\treturn GlobalDSFactory.get().getDataSource(group);\n\t}\n\n\t/**\n\t * 设置全局的数据源工厂<br>\n\t * 在项目中存在多个连接池库的情况下，我们希望使用低优先级的库时使用此方法自定义之<br>\n\t * 重新定义全局的数据源工厂此方法可在以下两种情况下调用：\n\t *\n\t * <pre>\n\t * 1. 在get方法调用前调用此方法来自定义全局的数据源工厂\n\t * 2. 替换已存在的全局数据源工厂，当已存在时会自动关闭\n\t * </pre>\n\t *\n\t * @param dsFactory 数据源工厂\n\t * @return 自定义的数据源工厂\n\t */\n\tpublic static DSFactory setCurrentDSFactory(DSFactory dsFactory) {\n\t\treturn GlobalDSFactory.set(dsFactory);\n\t}\n\n\t/**\n\t * 创建数据源实现工厂<br>\n\t * 此方法通过“试错”方式查找引入项目的连接池库，按照优先级寻找，一旦寻找到则创建对应的数据源工厂<br>\n\t * 连接池优先级：Hikari &gt; Druid &gt; Tomcat &gt; Dbcp &gt; C3p0 &gt; Hutool Pooled\n\t *\n\t * @param setting 数据库配置项\n\t * @return 日志实现类\n\t */\n\tpublic static DSFactory create(Setting setting) {\n\t\tfinal DSFactory dsFactory = doCreate(setting);\n\t\tlog.debug(\"Use [{}] DataSource As Default\", dsFactory.dataSourceName);\n\t\treturn dsFactory;\n\t}\n\n\t/**\n\t * 创建数据源实现工厂<br>\n\t * 此方法通过“试错”方式查找引入项目的连接池库，按照优先级寻找，一旦寻找到则创建对应的数据源工厂<br>\n\t * 连接池优先级：Hikari &gt; Druid &gt; Tomcat &gt; BeeCP &gt; Dbcp &gt; C3p0 &gt; Hutool Pooled\n\t *\n\t * @param setting 数据库配置项\n\t * @return 日志实现类\n\t * @since 4.1.3\n\t */\n\tprivate static DSFactory doCreate(Setting setting) {\n\t\ttry {\n\t\t\treturn new HikariDSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t\ttry {\n\t\t\treturn new DruidDSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t\ttry {\n\t\t\treturn new TomcatDSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t//如果未引入包，此处会报org.apache.tomcat.jdbc.pool.PoolConfiguration未找到错误\n\t\t\t//因为org.apache.tomcat.jdbc.pool.DataSource实现了此接口，会首先检查接口的存在与否\n\t\t\t// ignore\n\t\t}\n\t\ttry {\n\t\t\treturn new BeeDSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t\ttry {\n\t\t\treturn new DbcpDSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t\ttry {\n\t\t\treturn new C3p0DSFactory(setting);\n\t\t} catch (NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn new PooledDSFactory(setting);\n\t}\n\t// ------------------------------------------------------------------------- Static end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/DataSourceWrapper.java",
    "content": "package cn.hutool.db.ds;\n\nimport cn.hutool.core.clone.CloneRuntimeException;\nimport cn.hutool.core.io.IoUtil;\n\nimport javax.sql.DataSource;\nimport java.io.Closeable;\nimport java.io.PrintWriter;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.SQLFeatureNotSupportedException;\nimport java.util.logging.Logger;\n\n/**\n * {@link DataSource} 数据源实现包装，通过包装，提供基本功能外的额外功能和参数持有，包括：\n *\n * <pre>\n * 1. 提供驱动名的持有，用于确定数据库方言\n * </pre>\n *\n * @author looly\n * @since 4.3.2\n */\npublic class DataSourceWrapper implements DataSource, Closeable, Cloneable {\n\n\tprivate final DataSource ds;\n\tprivate final String driver;\n\n\t/**\n\t * 包装指定的DataSource\n\t *\n\t * @param ds     原始的DataSource\n\t * @param driver 数据库驱动类名\n\t * @return DataSourceWrapper\n\t */\n\tpublic static DataSourceWrapper wrap(DataSource ds, String driver) {\n\t\treturn new DataSourceWrapper(ds, driver);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds     原始的DataSource\n\t * @param driver 数据库驱动类名\n\t */\n\tpublic DataSourceWrapper(DataSource ds, String driver) {\n\t\tthis.ds = ds;\n\t\tthis.driver = driver;\n\t}\n\n\t/**\n\t * 获取驱动名\n\t *\n\t * @return 驱动名\n\t */\n\tpublic String getDriver() {\n\t\treturn this.driver;\n\t}\n\n\t/**\n\t * 获取原始的数据源\n\t *\n\t * @return 原始数据源\n\t */\n\tpublic DataSource getRaw() {\n\t\treturn this.ds;\n\t}\n\n\t@Override\n\tpublic PrintWriter getLogWriter() throws SQLException {\n\t\treturn ds.getLogWriter();\n\t}\n\n\t@Override\n\tpublic void setLogWriter(PrintWriter out) throws SQLException {\n\t\tds.setLogWriter(out);\n\t}\n\n\t@Override\n\tpublic void setLoginTimeout(int seconds) throws SQLException {\n\t\tds.setLoginTimeout(seconds);\n\t}\n\n\t@Override\n\tpublic int getLoginTimeout() throws SQLException {\n\t\treturn ds.getLoginTimeout();\n\t}\n\n\t@Override\n\tpublic Logger getParentLogger() throws SQLFeatureNotSupportedException {\n\t\treturn ds.getParentLogger();\n\t}\n\n\t@Override\n\tpublic <T> T unwrap(Class<T> iface) throws SQLException {\n\t\treturn ds.unwrap(iface);\n\t}\n\n\t@Override\n\tpublic boolean isWrapperFor(Class<?> iface) throws SQLException {\n\t\treturn ds.isWrapperFor(iface);\n\t}\n\n\t@Override\n\tpublic Connection getConnection() throws SQLException {\n\t\treturn ds.getConnection();\n\t}\n\n\t@Override\n\tpublic Connection getConnection(String username, String password) throws SQLException {\n\t\treturn ds.getConnection(username, password);\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tif (this.ds instanceof AutoCloseable) {\n\t\t\tIoUtil.close((AutoCloseable) this.ds);\n\t\t}\n\t}\n\n\t@Override\n\tpublic DataSourceWrapper clone() {\n\t\ttry {\n\t\t\treturn (DataSourceWrapper) super.clone();\n\t\t} catch (CloneNotSupportedException e) {\n\t\t\tthrow new CloneRuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/GlobalDSFactory.java",
    "content": "package cn.hutool.db.ds;\n\nimport cn.hutool.log.StaticLog;\n\n/**\n * 全局的数据源工厂<br>\n * 一般情况下，一个应用默认只使用一种数据库连接池，因此维护一个全局的数据源工厂类减少判断连接池类型造成的性能浪费\n *\n * @author looly\n * @since 4.0.2\n */\npublic class GlobalDSFactory {\n\n\tprivate static volatile DSFactory factory;\n\tprivate static final Object lock = new Object();\n\n\t/*\n\t * 设置在JVM关闭时关闭所有数据库连接\n\t */\n\tstatic {\n\t\t// JVM关闭时关闭所有连接池\n\t\tRuntime.getRuntime().addShutdownHook(new Thread() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\tif (null != factory) {\n\t\t\t\t\tfactory.destroy();\n\t\t\t\t\tStaticLog.debug(\"DataSource: [{}] destroyed.\", factory.dataSourceName);\n\t\t\t\t\tfactory = null;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 获取默认的数据源工厂，读取默认数据库配置文件<br>\n\t * 此处使用懒加载模式，在第一次调用此方法时才创建默认数据源工厂<br>\n\t * 如果想自定义全局的数据源工厂，请在第一次调用此方法前调用{@link #set(DSFactory)} 方法自行定义\n\t *\n\t * @return 当前使用的数据源工厂\n\t */\n\tpublic static DSFactory get() {\n\t\tif (null == factory) {\n\t\t\tsynchronized (lock) {\n\t\t\t\tif (null == factory) {\n\t\t\t\t\tfactory = DSFactory.create(null);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn factory;\n\t}\n\n\t/**\n\t * 设置全局的数据源工厂<br>\n\t * 在项目中存在多个连接池库的情况下，我们希望使用低优先级的库时使用此方法自定义之<br>\n\t * 重新定义全局的数据源工厂此方法可在以下两种情况下调用：\n\t *\n\t * <pre>\n\t * 1. 在get方法调用前调用此方法来自定义全局的数据源工厂\n\t * 2. 替换已存在的全局数据源工厂，当已存在时会自动关闭\n\t * </pre>\n\t *\n\t * @param customDSFactory 自定义数据源工厂\n\t * @return 自定义的数据源工厂\n\t */\n\tpublic static DSFactory set(DSFactory customDSFactory) {\n\t\tsynchronized (lock) {\n\t\t\tif (null != factory) {\n\t\t\t\tif (factory.equals(customDSFactory)) {\n\t\t\t\t\treturn factory;// 数据源工厂不变时返回原数据源工厂\n\t\t\t\t}\n\t\t\t\t// 自定义数据源工厂前关闭之前的数据源\n\t\t\t\tfactory.destroy();\n\t\t\t}\n\n\t\t\tStaticLog.debug(\"Custom use [{}] DataSource.\", customDSFactory.dataSourceName);\n\t\t\tfactory = customDSFactory;\n\t\t}\n\t\treturn factory;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/bee/BeeDSFactory.java",
    "content": "package cn.hutool.db.ds.bee;\n\nimport cn.beecp.BeeDataSource;\nimport cn.beecp.BeeDataSourceConfig;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\n\nimport javax.sql.DataSource;\n\n/**\n * BeeCP数据源工厂类\n *\n * @author Looly\n *\n */\npublic class BeeDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final String DS_NAME = \"BeeCP\";\n\n\tpublic BeeDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic BeeDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, BeeDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\n\t\tfinal BeeDataSourceConfig beeConfig = new BeeDataSourceConfig(driver, jdbcUrl, user, pass);\n\t\tpoolSetting.toBean(beeConfig);\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tbeeConfig.addConnectProperty(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\treturn new BeeDataSource(beeConfig);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/bee/package-info.java",
    "content": "/**\n * BeeCP封装\n * 见:https://github.com/Chris2018998/BeeCP\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.bee;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/c3p0/C3p0DSFactory.java",
    "content": "package cn.hutool.db.ds.c3p0;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.dialect.Props;\nimport com.mchange.v2.c3p0.ComboPooledDataSource;\n\nimport javax.sql.DataSource;\nimport java.beans.PropertyVetoException;\n\n/**\n * Druid数据源工厂类\n *\n * @author Looly\n *\n */\npublic class C3p0DSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = -6090788225842047281L;\n\n\tpublic static final String DS_NAME = \"C3P0\";\n\n\t/**\n\t * 构造，使用默认配置\n\t */\n\tpublic C3p0DSFactory() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 配置\n\t */\n\tpublic C3p0DSFactory(Setting setting) {\n\t\tsuper(DS_NAME, ComboPooledDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tfinal ComboPooledDataSource ds = new ComboPooledDataSource();\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tfinal Props connProps = new Props();\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tconnProps.setProperty(key, connValue);\n\t\t\t}\n\t\t}\n\t\tif(MapUtil.isNotEmpty(connProps)){\n\t\t\tds.setProperties(connProps);\n\t\t}\n\n\t\tds.setJdbcUrl(jdbcUrl);\n\t\ttry {\n\t\t\tds.setDriverClass(driver);\n\t\t} catch (PropertyVetoException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\tds.setUser(user);\n\t\tds.setPassword(pass);\n\n\t\t// 注入属性\n\t\tpoolSetting.toBean(ds);\n\n\t\treturn ds;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/c3p0/package-info.java",
    "content": "/**\n * C3P0封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.c3p0;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/dbcp/DbcpDSFactory.java",
    "content": "package cn.hutool.db.ds.dbcp;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\nimport org.apache.commons.dbcp2.BasicDataSource;\n\nimport javax.sql.DataSource;\n\n/**\n * DBCP2数据源工厂类\n * \n * @author Looly\n *\n */\npublic class DbcpDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = -9133501414334104548L;\n\t\n\tpublic static final String DS_NAME = \"commons-dbcp2\";\n\n\tpublic DbcpDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic DbcpDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, BasicDataSource.class, setting);\n\t}\n\t\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tfinal BasicDataSource ds = new BasicDataSource();\n\t\t\n\t\tds.setUrl(jdbcUrl);\n\t\tds.setDriverClassName(driver);\n\t\tds.setUsername(user);\n\t\tds.setPassword(pass);\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tds.addConnectionProperty(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\t// 注入属性\n\t\tpoolSetting.toBean(ds);\n\n\t\treturn ds;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/dbcp/package-info.java",
    "content": "/**\n * DBCP封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.dbcp;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/druid/DruidDSFactory.java",
    "content": "package cn.hutool.db.ds.druid;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.dialect.Props;\nimport com.alibaba.druid.pool.DruidDataSource;\n\nimport javax.sql.DataSource;\n\n/**\n * Druid数据源工厂类\n *\n * @author Looly\n *\n */\npublic class DruidDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 4680621702534433222L;\n\n\tpublic static final String DS_NAME = \"Druid\";\n\n\t/**\n\t * 构造，使用默认配置文件\n\t */\n\tpublic DruidDSFactory() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 数据库配置\n\t */\n\tpublic DruidDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, DruidDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tfinal DruidDataSource ds = new DruidDataSource();\n\n\t\t// 基本信息\n\t\tds.setUrl(jdbcUrl);\n\t\tds.setDriverClassName(driver);\n\t\tds.setUsername(user);\n\t\tds.setPassword(pass);\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\t// Druid中也可以通过 druid.connectProperties 属性设置\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tds.addConnectionProperty(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\t// Druid连接池配置信息，规范化属性名\n\t\tfinal Props druidProps = new Props();\n\t\tpoolSetting.forEach((key, value)-> druidProps.put(StrUtil.addPrefixIfNot(key, \"druid.\"), value));\n\t\tds.configFromPropety(druidProps);\n\n\t\t//issue#I4ZKCW 某些非属性设置单独设置\n\t\t// connectionErrorRetryAttempts\n\t\tfinal String connectionErrorRetryAttemptsKey = \"druid.connectionErrorRetryAttempts\";\n\t\tif(druidProps.containsKey(connectionErrorRetryAttemptsKey)){\n\t\t\tds.setConnectionErrorRetryAttempts(druidProps.getInt(connectionErrorRetryAttemptsKey));\n\t\t}\n\t\t// timeBetweenConnectErrorMillis\n\t\tfinal String timeBetweenConnectErrorMillisKey = \"druid.timeBetweenConnectErrorMillis\";\n\t\tif(druidProps.containsKey(timeBetweenConnectErrorMillisKey)){\n\t\t\tds.setTimeBetweenConnectErrorMillis(druidProps.getInt(timeBetweenConnectErrorMillisKey));\n\t\t}\n\t\t// breakAfterAcquireFailure\n\t\tfinal String breakAfterAcquireFailureKey = \"druid.breakAfterAcquireFailure\";\n\t\tif(druidProps.containsKey(breakAfterAcquireFailureKey)){\n\t\t\tds.setBreakAfterAcquireFailure(druidProps.getBool(breakAfterAcquireFailureKey));\n\t\t}\n\n\t\t// issue#I8STFC 补充\n\t\t// validationQueryTimeout\n\t\tfinal String validationQueryTimeout = \"druid.validationQueryTimeout\";\n\t\tif(druidProps.containsKey(validationQueryTimeout)){\n\t\t\tds.setValidationQueryTimeout(druidProps.getInt(validationQueryTimeout));\n\t\t}\n\t\t// queryTimeout\n\t\tfinal String queryTimeout = \"druid.queryTimeout\";\n\t\tif(druidProps.containsKey(queryTimeout)){\n\t\t\tds.setQueryTimeout(druidProps.getInt(queryTimeout));\n\t\t}\n\t\t// connectTimeout\n\t\tfinal String connectTimeout = \"druid.connectTimeout\";\n\t\tif(druidProps.containsKey(connectTimeout)){\n\t\t\tds.setConnectTimeout(druidProps.getInt(connectTimeout));\n\t\t}\n\t\t// socketTimeout\n\t\tfinal String socketTimeout = \"druid.socketTimeout\";\n\t\tif(druidProps.containsKey(socketTimeout)){\n\t\t\tds.setSocketTimeout(druidProps.getInt(socketTimeout));\n\t\t}\n\t\t// transactionQueryTimeout\n\t\tfinal String transactionQueryTimeout = \"druid.transactionQueryTimeout\";\n\t\tif(druidProps.containsKey(transactionQueryTimeout)){\n\t\t\tds.setTransactionQueryTimeout(druidProps.getInt(transactionQueryTimeout));\n\t\t}\n\t\t// loginTimeout\n\t\tfinal String loginTimeout = \"druid.loginTimeout\";\n\t\tif(druidProps.containsKey(loginTimeout)){\n\t\t\tds.setLoginTimeout(druidProps.getInt(loginTimeout));\n\t\t}\n\n\t\t// 检查关联配置，在用户未设置某项配置时，设置默认值\n\t\tif (null == ds.getValidationQuery()) {\n\t\t\t// 在validationQuery未设置的情况下，以下三项设置都将无效\n\t\t\tds.setTestOnBorrow(false);\n\t\t\tds.setTestOnReturn(false);\n\t\t\tds.setTestWhileIdle(false);\n\t\t}\n\n\t\treturn ds;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/druid/package-info.java",
    "content": "/**\n * Druid封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.druid;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/hikari/HikariDSFactory.java",
    "content": "package cn.hutool.db.ds.hikari;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.dialect.Props;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\n\nimport javax.sql.DataSource;\n\n/**\n * HikariCP数据源工厂类\n * \n * @author Looly\n *\n */\npublic class HikariDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = -8834744983614749401L;\n\t\n\tpublic static final String DS_NAME = \"HikariCP\";\n\n\tpublic HikariDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic HikariDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, HikariDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tfinal Props connProps = new Props();\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tconnProps.setProperty(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\tfinal Props config = new Props();\n\t\tconfig.putAll(poolSetting);\n\n\t\tconfig.put(\"jdbcUrl\", jdbcUrl);\n\t\tif (null != driver) {\n\t\t\tconfig.put(\"driverClassName\", driver);\n\t\t}\n\t\tif (null != user) {\n\t\t\tconfig.put(\"username\", user);\n\t\t}\n\t\tif (null != pass) {\n\t\t\tconfig.put(\"password\", pass);\n\t\t}\n\n\t\tfinal HikariConfig hikariConfig = new HikariConfig(config);\n\t\thikariConfig.setDataSourceProperties(connProps);\n\n\t\treturn new HikariDataSource(hikariConfig);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/hikari/package-info.java",
    "content": "/**\n * Hikari封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.hikari;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/jndi/JndiDSFactory.java",
    "content": "package cn.hutool.db.ds.jndi;\n\nimport javax.sql.DataSource;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\n\n/**\n * JNDI数据源工厂类<br>\n * Setting配置样例：<br>\n * ---------------------<br>\n * [group]<br>\n * jndi = jdbc/TestDB<br>\n * ---------------------<br>\n *\n * @author Looly\n *\n */\npublic class JndiDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 1573625812927370432L;\n\n\tpublic static final String DS_NAME = \"JNDI DataSource\";\n\n\tpublic JndiDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic JndiDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, DataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tString jndiName = poolSetting.getStr(\"jndi\");\n\t\tif (StrUtil.isEmpty(jndiName)) {\n\t\t\tthrow new DbRuntimeException(\"No setting name [jndi] for this group.\");\n\t\t}\n\t\treturn DbUtil.getJndiDs(jndiName);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/jndi/package-info.java",
    "content": "/**\n * JNDI封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.jndi;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/package-info.java",
    "content": "/**\n * 数据源封装，对各类数据库连接池的封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/ConnectionWraper.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport java.sql.Array;\nimport java.sql.Blob;\nimport java.sql.CallableStatement;\nimport java.sql.Clob;\nimport java.sql.Connection;\nimport java.sql.DatabaseMetaData;\nimport java.sql.NClob;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLClientInfoException;\nimport java.sql.SQLException;\nimport java.sql.SQLWarning;\nimport java.sql.SQLXML;\nimport java.sql.Savepoint;\nimport java.sql.Statement;\nimport java.sql.Struct;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.concurrent.Executor;\n\n/**\n * 连接包装，用于丰富功能\n * @author Looly\n *\n */\npublic abstract class ConnectionWraper implements Connection{\n\t\n\tprotected Connection raw;//真正的连接\n\n\t@Override\n\tpublic <T> T unwrap(Class<T> iface) throws SQLException {\n\t\treturn raw.unwrap(iface);\n\t}\n\n\t@Override\n\tpublic boolean isWrapperFor(Class<?> iface) throws SQLException {\n\t\treturn raw.isWrapperFor(iface);\n\t}\n\n\t@Override\n\tpublic Statement createStatement() throws SQLException {\n\t\treturn raw.createStatement();\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql) throws SQLException {\n\t\treturn raw.prepareStatement(sql);\n\t}\n\n\t@Override\n\tpublic CallableStatement prepareCall(String sql) throws SQLException {\n\t\treturn raw.prepareCall(sql);\n\t}\n\n\t@Override\n\tpublic String nativeSQL(String sql) throws SQLException {\n\t\treturn raw.nativeSQL(sql);\n\t}\n\n\t@Override\n\tpublic void setAutoCommit(boolean autoCommit) throws SQLException {\n\t\traw.setAutoCommit(autoCommit);\n\t}\n\n\t@Override\n\tpublic boolean getAutoCommit() throws SQLException {\n\t\treturn raw.getAutoCommit();\n\t}\n\n\t@Override\n\tpublic void commit() throws SQLException {\n\t\traw.commit();\n\t}\n\n\t@Override\n\tpublic void rollback() throws SQLException {\n\t\traw.rollback();\n\t}\n\n\t@Override\n\tpublic DatabaseMetaData getMetaData() throws SQLException {\n\t\treturn raw.getMetaData();\n\t}\n\n\t@Override\n\tpublic void setReadOnly(boolean readOnly) throws SQLException {\n\t\traw.setReadOnly(readOnly);\n\t}\n\n\t@Override\n\tpublic boolean isReadOnly() throws SQLException {\n\t\treturn raw.isReadOnly();\n\t}\n\n\t@Override\n\tpublic void setCatalog(String catalog) throws SQLException {\n\t\traw.setCatalog(catalog);\n\t}\n\n\t@Override\n\tpublic String getCatalog() throws SQLException {\n\t\treturn raw.getCatalog();\n\t}\n\n\t@Override\n\tpublic void setTransactionIsolation(int level) throws SQLException {\n\t\traw.setTransactionIsolation(level);\n\t}\n\n\t@Override\n\tpublic int getTransactionIsolation() throws SQLException {\n\t\treturn raw.getTransactionIsolation();\n\t}\n\n\t@Override\n\tpublic SQLWarning getWarnings() throws SQLException {\n\t\treturn raw.getWarnings();\n\t}\n\n\t@Override\n\tpublic void clearWarnings() throws SQLException {\n\t\traw.clearWarnings();\n\t}\n\n\t@Override\n\tpublic Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {\n\t\treturn raw.createStatement(resultSetType, resultSetConcurrency);\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {\n\t\treturn raw.prepareStatement(sql, resultSetType, resultSetConcurrency);\n\t}\n\n\t@Override\n\tpublic CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {\n\t\treturn raw.prepareCall(sql, resultSetType, resultSetConcurrency);\n\t}\n\n\t@Override\n\tpublic Map<String, Class<?>> getTypeMap() throws SQLException {\n\t\treturn raw.getTypeMap();\n\t}\n\n\t@Override\n\tpublic void setTypeMap(Map<String, Class<?>> map) throws SQLException {\n\t\traw.setTypeMap(map);\n\t}\n\n\t@Override\n\tpublic void setHoldability(int holdability) throws SQLException {\n\t\traw.setHoldability(holdability);\n\t}\n\n\t@Override\n\tpublic int getHoldability() throws SQLException {\n\t\treturn raw.getHoldability();\n\t}\n\n\t@Override\n\tpublic Savepoint setSavepoint() throws SQLException {\n\t\treturn raw.setSavepoint();\n\t}\n\n\t@Override\n\tpublic Savepoint setSavepoint(String name) throws SQLException {\n\t\treturn raw.setSavepoint(name);\n\t}\n\n\t@Override\n\tpublic void rollback(Savepoint savepoint) throws SQLException {\n\t\traw.rollback(savepoint);\n\t}\n\n\t@Override\n\tpublic void releaseSavepoint(Savepoint savepoint) throws SQLException {\n\t\traw.releaseSavepoint(savepoint);\n\t}\n\n\t@Override\n\tpublic Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {\n\t\treturn raw.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {\n\t\treturn raw.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);\n\t}\n\n\t@Override\n\tpublic CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {\n\t\treturn raw.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {\n\t\treturn raw.prepareStatement(sql, autoGeneratedKeys);\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {\n\t\treturn raw.prepareStatement(sql, columnIndexes);\n\t}\n\n\t@Override\n\tpublic PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {\n\t\treturn raw.prepareStatement(sql, columnNames);\n\t}\n\n\t@Override\n\tpublic Clob createClob() throws SQLException {\n\t\treturn raw.createClob();\n\t}\n\n\t@Override\n\tpublic Blob createBlob() throws SQLException {\n\t\treturn raw.createBlob();\n\t}\n\n\t@Override\n\tpublic NClob createNClob() throws SQLException {\n\t\treturn raw.createNClob();\n\t}\n\n\t@Override\n\tpublic SQLXML createSQLXML() throws SQLException {\n\t\treturn raw.createSQLXML();\n\t}\n\n\t@Override\n\tpublic boolean isValid(int timeout) throws SQLException {\n\t\treturn raw.isValid(timeout);\n\t}\n\n\t@Override\n\tpublic void setClientInfo(String name, String value) throws SQLClientInfoException {\n\t\traw.setClientInfo(name, value);\n\t}\n\n\t@Override\n\tpublic void setClientInfo(Properties properties) throws SQLClientInfoException {\n\t\traw.setClientInfo(properties);\n\t}\n\n\t@Override\n\tpublic String getClientInfo(String name) throws SQLException {\n\t\treturn raw.getClientInfo(name);\n\t}\n\n\t@Override\n\tpublic Properties getClientInfo() throws SQLException {\n\t\treturn raw.getClientInfo();\n\t}\n\n\t@Override\n\tpublic Array createArrayOf(String typeName, Object[] elements) throws SQLException {\n\t\treturn raw.createArrayOf(typeName, elements);\n\t}\n\n\t@Override\n\tpublic Struct createStruct(String typeName, Object[] attributes) throws SQLException {\n\t\treturn raw.createStruct(typeName, attributes);\n\t}\n\n\t@Override\n\tpublic void setSchema(String schema) throws SQLException {\n\t\traw.setSchema(schema);\n\t}\n\n\t@Override\n\tpublic String getSchema() throws SQLException {\n\t\treturn raw.getSchema();\n\t}\n\n\t@Override\n\tpublic void abort(Executor executor) throws SQLException {\n\t\traw.abort(executor);\n\t}\n\n\t@Override\n\tpublic void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {\n\t\traw.setNetworkTimeout(executor, milliseconds);\n\t}\n\n\t@Override\n\tpublic int getNetworkTimeout() throws SQLException {\n\t\treturn raw.getNetworkTimeout();\n\t}\n\n\t/**\n\t * @return 实际的连接对象\n\t */\n\tpublic Connection getRaw(){\n\t\treturn this.raw;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbConfig.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.dialect.DriverUtil;\n\nimport java.util.Properties;\n\n/**\n * 数据库配置\n *\n * @author Looly\n */\npublic class DbConfig {\n\n\t//-------------------------------------------------------------------- Fields start\n\tprivate String driver;        //数据库驱动\n\tprivate String url;            //jdbc url\n\tprivate String user;            //用户名\n\tprivate String pass;            //密码\n\n\tprivate int initialSize;        //初始连接数\n\tprivate int minIdle;            //最小闲置连接数\n\tprivate int maxActive;        //最大活跃连接数\n\tprivate long maxWait;        //获取连接的超时等待\n\n\t// 连接配置\n\tprivate Properties connProps;\n\t//-------------------------------------------------------------------- Fields end\n\n\t//-------------------------------------------------------------------- Constructor start\n\tpublic DbConfig() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url  jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic DbConfig(String url, String user, String pass) {\n\t\tinit(url, user, pass);\n\t}\n\t//-------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param url  jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic void init(String url, String user, String pass) {\n\t\tthis.url = url;\n\t\tthis.user = user;\n\t\tthis.pass = pass;\n\t\tthis.driver = DriverUtil.identifyDriver(url);\n\t\ttry {\n\t\t\tClass.forName(this.driver);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new DbRuntimeException(e, \"Get jdbc driver from [{}] error!\", url);\n\t\t}\n\t}\n\n\t//-------------------------------------------------------------------- Getters and Setters start\n\tpublic String getDriver() {\n\t\treturn driver;\n\t}\n\n\tpublic void setDriver(String driver) {\n\t\tthis.driver = driver;\n\t}\n\n\tpublic String getUrl() {\n\t\treturn url;\n\t}\n\n\tpublic void setUrl(String url) {\n\t\tthis.url = url;\n\t}\n\n\tpublic String getUser() {\n\t\treturn user;\n\t}\n\n\tpublic void setUser(String user) {\n\t\tthis.user = user;\n\t}\n\n\tpublic String getPass() {\n\t\treturn pass;\n\t}\n\n\tpublic void setPass(String pass) {\n\t\tthis.pass = pass;\n\t}\n\n\tpublic int getInitialSize() {\n\t\treturn initialSize;\n\t}\n\n\tpublic void setInitialSize(int initialSize) {\n\t\tthis.initialSize = initialSize;\n\t}\n\n\tpublic int getMinIdle() {\n\t\treturn minIdle;\n\t}\n\n\tpublic void setMinIdle(int minIdle) {\n\t\tthis.minIdle = minIdle;\n\t}\n\n\tpublic int getMaxActive() {\n\t\treturn maxActive;\n\t}\n\n\tpublic void setMaxActive(int maxActive) {\n\t\tthis.maxActive = maxActive;\n\t}\n\n\tpublic long getMaxWait() {\n\t\treturn maxWait;\n\t}\n\n\tpublic void setMaxWait(long maxWait) {\n\t\tthis.maxWait = maxWait;\n\t}\n\n\tpublic Properties getConnProps() {\n\t\treturn connProps;\n\t}\n\n\tpublic void setConnProps(Properties connProps) {\n\t\tthis.connProps = connProps;\n\t}\n\n\tpublic void addConnProps(String key, String value){\n\t\tif(null == this.connProps){\n\t\t\tthis.connProps = new Properties();\n\t\t}\n\t\tthis.connProps.setProperty(key, value);\n\t}\n\t//-------------------------------------------------------------------- Getters and Setters end\n}"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbSetting.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.dialect.DriverUtil;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.setting.Setting;\n\n/**\n * 数据库配置文件类，此类对应一个数据库配置文件\n *\n * @author Looly\n *\n */\npublic class DbSetting {\n\t/** 默认的数据库连接配置文件路径 */\n\tpublic final static String DEFAULT_DB_CONFIG_PATH = \"config/db.setting\";\n\n\tprivate final Setting setting;\n\n\t/**\n\t * 构造\n\t */\n\tpublic DbSetting() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 数据库配置\n\t */\n\tpublic DbSetting(Setting setting) {\n\t\tif (null == setting) {\n\t\t\tthis.setting = new Setting(DEFAULT_DB_CONFIG_PATH);\n\t\t} else {\n\t\t\tthis.setting = setting;\n\t\t}\n\t}\n\n\t/**\n\t * 获得数据库连接信息\n\t *\n\t * @param group 分组\n\t * @return 分组\n\t */\n\tpublic DbConfig getDbConfig(String group) {\n\t\tfinal Setting config = setting.getSetting(group);\n\t\tif (MapUtil.isEmpty(config)) {\n\t\t\tthrow new DbRuntimeException(\"No Hutool pool config for group: [{}]\", group);\n\t\t}\n\n\t\tfinal DbConfig dbConfig = new DbConfig();\n\n\t\t// 基本信息\n\t\tfinal String url = config.getAndRemoveStr(DSFactory.KEY_ALIAS_URL);\n\t\tif (StrUtil.isBlank(url)) {\n\t\t\tthrow new DbRuntimeException(\"No JDBC URL for group: [{}]\", group);\n\t\t}\n\t\tdbConfig.setUrl(url);\n\t\t// 自动识别Driver\n\t\tfinal String driver = config.getAndRemoveStr(DSFactory.KEY_ALIAS_DRIVER);\n\t\tdbConfig.setDriver(StrUtil.isNotBlank(driver) ? driver : DriverUtil.identifyDriver(url));\n\t\tdbConfig.setUser(config.getAndRemoveStr(DSFactory.KEY_ALIAS_USER));\n\t\tdbConfig.setPass(config.getAndRemoveStr(DSFactory.KEY_ALIAS_PASSWORD));\n\n\t\t// 连接池相关信息\n\t\tdbConfig.setInitialSize(setting.getInt(\"initialSize\", group, 0));\n\t\tdbConfig.setMinIdle(setting.getInt(\"minIdle\", group, 0));\n\t\tdbConfig.setMaxActive(setting.getInt(\"maxActive\", group, 8));\n\t\tdbConfig.setMaxWait(setting.getLong(\"maxWait\", group, 6000L));\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tString connValue;\n\t\tfor (String key : DSFactory.KEY_CONN_PROPS) {\n\t\t\tconnValue = config.get(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tdbConfig.addConnProps(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\treturn dbConfig;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledConnection.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.setting.dialect.Props;\n\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.SQLException;\nimport java.util.Properties;\n\n/**\n * 池化\n * @author Looly\n *\n */\npublic class PooledConnection extends ConnectionWraper{\n\n\tprivate final PooledDataSource ds;\n\tprivate boolean isClosed;\n\n\t/**\n\t * 构造\n\t *\n\t * @param ds 数据源\n\t * @throws SQLException SQL异常\n\t */\n\tpublic PooledConnection(PooledDataSource ds) throws SQLException {\n\t\tthis.ds = ds;\n\t\tfinal DbConfig config = ds.getConfig();\n\n\t\t// issue#IA6EUQ 部分驱动无法自动加载，此处手动完成\n\t\tfinal String driver = config.getDriver();\n\t\tif(StrUtil.isNotBlank(driver)){\n\t\t\ttry {\n\t\t\t\tClass.forName(driver);\n\t\t\t} catch (ClassNotFoundException e) {\n\t\t\t\tthrow new DbRuntimeException(e);\n\t\t\t}\n\t\t}\n\n\t\tfinal Props info = new Props();\n\t\tfinal String user = config.getUser();\n\t\tif (user != null) {\n\t\t\tinfo.setProperty(\"user\", user);\n\t\t}\n\t\tfinal String password = config.getPass();\n\t\tif (password != null) {\n\t\t\tinfo.setProperty(\"password\", password);\n\t\t}\n\n\t\t// 其它参数\n\t\tfinal Properties connProps = config.getConnProps();\n\t\tif(MapUtil.isNotEmpty(connProps)){\n\t\t\tinfo.putAll(connProps);\n\t\t}\n\n\t\tthis.raw = DriverManager.getConnection(config.getUrl(), info);\n\t}\n\n\tpublic PooledConnection(PooledDataSource ds, Connection conn) {\n\t\tthis.ds = ds;\n\t\tthis.raw = conn;\n\t}\n\n\t/**\n\t * 重写关闭连接，实际操作是归还到连接池中\n\t */\n\t@Override\n\tpublic void close() {\n\t\tthis.ds.free(this);\n\t\tthis.isClosed = true;\n\t}\n\n\t/**\n\t * 连接是否关闭，关闭条件：<br>\n\t * 1、被归还到池中\n\t * 2、实际连接已关闭\n\t */\n\t@Override\n\tpublic boolean isClosed() throws SQLException {\n\t\treturn isClosed || raw.isClosed();\n\t}\n\n\t/**\n\t * 打开连接\n\t * @return this\n\t */\n\tprotected PooledConnection open() {\n\t\tthis.isClosed = false;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 释放连接\n\t * @return this\n\t */\n\tprotected PooledConnection release() {\n\t\tDbUtil.close(this.raw);\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDSFactory.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\n\nimport javax.sql.DataSource;\n\n/**\n * Hutool自身实现的池化数据源工厂类\n * \n * @author Looly\n *\n */\npublic class PooledDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 8093886210895248277L;\n\t\n\tpublic static final String DS_NAME = \"Hutool-Pooled-DataSource\";\n\n\tpublic PooledDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic PooledDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, PooledDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tfinal DbConfig dbConfig = new DbConfig();\n\t\tdbConfig.setUrl(jdbcUrl);\n\t\tdbConfig.setDriver(driver);\n\t\tdbConfig.setUser(user);\n\t\tdbConfig.setPass(pass);\n\n\t\t// 连接池相关信息\n\t\tdbConfig.setInitialSize(poolSetting.getInt(\"initialSize\", 0));\n\t\tdbConfig.setMinIdle(poolSetting.getInt(\"minIdle\", 0));\n\t\tdbConfig.setMaxActive(poolSetting.getInt(\"maxActive\", 8));\n\t\tdbConfig.setMaxWait(poolSetting.getLong(\"maxWait\", 6000L));\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.get(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tdbConfig.addConnProps(key, connValue);\n\t\t\t}\n\t\t}\n\n\t\treturn new PooledDataSource(dbConfig);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDataSource.java",
    "content": "package cn.hutool.db.ds.pooled;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.ds.simple.AbstractDataSource;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.LinkedList;\nimport java.util.Queue;\n\n/**\n * 池化数据源\n *\n * @author Looly\n *\n */\npublic class PooledDataSource extends AbstractDataSource {\n\n\tprivate Queue<PooledConnection> freePool;\n\tprivate int activeCount; // 活跃连接数\n\n\tprivate final DbConfig config;\n\n\t/**\n\t * 获得一个数据源\n\t *\n\t * @param group 数据源分组\n\t * @return {@link PooledDataSource}\n\t */\n\tsynchronized public static PooledDataSource getDataSource(String group) {\n\t\treturn new PooledDataSource(group);\n\t}\n\n\t/**\n\t * 获得一个数据源，使用空分组\n\t *\n\t * @return {@link PooledDataSource}\n\t */\n\tsynchronized public static PooledDataSource getDataSource() {\n\t\treturn new PooledDataSource();\n\t}\n\n\t// -------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造，读取默认的配置文件和默认分组\n\t */\n\tpublic PooledDataSource() {\n\t\tthis(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 构造，读取默认的配置文件\n\t *\n\t * @param group 分组\n\t */\n\tpublic PooledDataSource(String group) {\n\t\tthis(new DbSetting(), group);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 数据库配置文件对象\n\t * @param group 分组\n\t */\n\tpublic PooledDataSource(DbSetting setting, String group) {\n\t\tthis(setting.getDbConfig(group));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 数据库配置\n\t */\n\tpublic PooledDataSource(DbConfig config) {\n\t\tthis.config = config;\n\t\tfreePool = new LinkedList<>();\n\t\tint initialSize = config.getInitialSize();\n\t\ttry {\n\t\t\twhile (initialSize-- > 0) {\n\t\t\t\tfreePool.offer(newConnection());\n\t\t\t}\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 从数据库连接池中获取数据库连接对象\n\t */\n\t@Override\n\tpublic synchronized Connection getConnection() throws SQLException {\n\t\treturn getConnection(config.getMaxWait());\n\t}\n\n\t@Override\n\tpublic Connection getConnection(String username, String password) throws SQLException {\n\t\tthrow new SQLException(\"Pooled DataSource is not allow to get special Connection!\");\n\t}\n\n\t/**\n\t * 释放连接，连接会被返回给连接池\n\t *\n\t * @param conn 连接\n\t * @return 释放成功与否\n\t */\n\tprotected synchronized boolean free(PooledConnection conn) {\n\t\tactiveCount--;\n\t\treturn freePool.offer(conn);\n\t}\n\n\t/**\n\t * 创建新连接\n\t *\n\t * @return 新连接\n\t * @throws SQLException SQL异常\n\t */\n\tpublic PooledConnection newConnection() throws SQLException {\n\t\treturn new PooledConnection(this);\n\t}\n\n\tpublic DbConfig getConfig() {\n\t\treturn config;\n\t}\n\n\t/**\n\t * 获取连接对象\n\t *\n\t * @param wait 当池中无连接等待的毫秒数\n\t * @return 连接对象\n\t * @throws SQLException SQL异常\n\t */\n\tpublic PooledConnection getConnection(long wait) throws SQLException {\n\t\ttry {\n\t\t\treturn getConnectionDirect();\n\t\t} catch (Exception e) {\n\t\t\tThreadUtil.sleep(wait);\n\t\t}\n\t\treturn getConnectionDirect();\n\t}\n\n\t@Override\n\tsynchronized public void close() {\n\t\tif (CollectionUtil.isNotEmpty(this.freePool)) {\n\t\t\tthis.freePool.forEach(PooledConnection::release);\n\t\t\tthis.freePool.clear();\n\t\t\tthis.freePool = null;\n\t\t}\n\t}\n\n\t@Override\n\tprotected void finalize() {\n\t\tIoUtil.close(this);\n\t}\n\n\t/**\n\t * 直接从连接池中获取连接，如果池中无连接直接抛出异常\n\t *\n\t * @return PooledConnection\n\t * @throws SQLException SQL异常\n\t */\n\tprivate PooledConnection getConnectionDirect() throws SQLException {\n\t\tif (null == freePool) {\n\t\t\tthrow new SQLException(\"PooledDataSource is closed!\");\n\t\t}\n\n\t\tfinal int maxActive = config.getMaxActive();\n\t\tif (maxActive <= 0 || maxActive < this.activeCount) {\n\t\t\t// 超过最大使用限制\n\t\t\tthrow new SQLException(\"In used Connection is more than Max Active.\");\n\t\t}\n\n\t\tPooledConnection conn = freePool.poll();\n\t\tif (null == conn || conn.open().isClosed()) {\n\t\t\tconn = this.newConnection();\n\t\t}\n\t\tactiveCount++;\n\t\treturn conn;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/pooled/package-info.java",
    "content": "/**\n * Hutool对连接池的简单实现\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.pooled;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/simple/AbstractDataSource.java",
    "content": "package cn.hutool.db.ds.simple;\n\nimport java.io.Closeable;\nimport java.io.PrintWriter;\nimport java.sql.DriverManager;\nimport java.sql.SQLException;\nimport java.sql.SQLFeatureNotSupportedException;\nimport java.util.logging.Logger;\n\nimport javax.sql.DataSource;\n\n/**\n * 数据源抽象实现\n * @author Looly\n *\n */\npublic abstract class AbstractDataSource implements DataSource, Cloneable, Closeable{\n\t@Override\n\tpublic PrintWriter getLogWriter() {\n\t\treturn DriverManager.getLogWriter();\n\t}\n\n\t@Override\n\tpublic void setLogWriter(PrintWriter out) {\n\t\tDriverManager.setLogWriter(out);\n\t}\n\n\t@Override\n\tpublic void setLoginTimeout(int seconds) {\n\t\tDriverManager.setLoginTimeout(seconds);\n\t}\n\n\t@Override\n\tpublic int getLoginTimeout() {\n\t\treturn DriverManager.getLoginTimeout();\n\t}\n\n\t@Override\n\tpublic <T> T unwrap(Class<T> iface) throws SQLException {\n\t\tthrow new SQLException(\"Can't support unwrap method!\");\n\t}\n\n\t@Override\n\tpublic boolean isWrapperFor(Class<?> iface) throws SQLException {\n\t\tthrow new SQLException(\"Can't support isWrapperFor method!\");\n\t}\n\n\t/**\n\t * Support from JDK7\n\t * @since 1.7\n\t */\n\t@Override\n\tpublic Logger getParentLogger() throws SQLFeatureNotSupportedException {\n\t\tthrow new SQLFeatureNotSupportedException(\"DataSource can't support getParentLogger method!\");\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDSFactory.java",
    "content": "package cn.hutool.db.ds.simple;\n\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\n\nimport javax.sql.DataSource;\n\n/**\n * 简单数据源工厂类\n * \n * @author Looly\n *\n */\npublic class SimpleDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 4738029988261034743L;\n\t\n\tpublic static final String DS_NAME = \"Hutool-Simple-DataSource\";\n\n\tpublic SimpleDSFactory() {\n\t\tthis(null);\n\t}\n\n\tpublic SimpleDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, SimpleDataSource.class, setting);\n\t}\n\n\t@Override\n\tprotected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tSimpleDataSource ds = new SimpleDataSource(//\n\t\t\t\tjdbcUrl, //\n\t\t\t\tuser, //\n\t\t\t\tpass, //\n\t\t\t\tdriver//\n\t\t);\n\t\tds.setConnProps(poolSetting.getProps(Setting.DEFAULT_GROUP));\n\t\treturn ds;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDataSource.java",
    "content": "package cn.hutool.db.ds.simple;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.dialect.DriverUtil;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.dialect.Props;\n\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.SQLException;\nimport java.util.Properties;\n\n/***\n * 简易数据源，没有使用连接池，仅供测试或打开关闭连接非常少的场合使用！\n *\n * @author loolly\n *\n */\npublic class SimpleDataSource extends AbstractDataSource {\n\n\t/** 默认的数据库连接配置文件路径 */\n\tpublic final static String DEFAULT_DB_CONFIG_PATH = \"config/db.setting\";\n\n\t// -------------------------------------------------------------------- Fields start\n\tprivate String driver; // 数据库驱动\n\tprivate String url; // jdbc url\n\tprivate String user; // 用户名\n\tprivate String pass; // 密码\n\n\t// 连接配置\n\tprivate Properties connProps;\n\t// -------------------------------------------------------------------- Fields end\n\n\t/**\n\t * 获得一个数据源\n\t *\n\t * @param group 数据源分组\n\t * @return SimpleDataSource\n\t */\n\tsynchronized public static SimpleDataSource getDataSource(String group) {\n\t\treturn new SimpleDataSource(group);\n\t}\n\n\t/**\n\t * 获得一个数据源，无分组\n\t *\n\t * @return SimpleDataSource\n\t */\n\tsynchronized public static SimpleDataSource getDataSource() {\n\t\treturn new SimpleDataSource();\n\t}\n\n\t// -------------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t */\n\tpublic SimpleDataSource() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param group 数据库配置文件中的分组\n\t */\n\tpublic SimpleDataSource(String group) {\n\t\tthis(null, group);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 数据库配置\n\t * @param group 数据库配置文件中的分组\n\t */\n\tpublic SimpleDataSource(Setting setting, String group) {\n\t\tif (null == setting) {\n\t\t\tsetting = new Setting(DEFAULT_DB_CONFIG_PATH);\n\t\t}\n\t\tfinal Setting config = setting.getSetting(group);\n\t\tif (MapUtil.isEmpty(config)) {\n\t\t\tthrow new DbRuntimeException(\"No DataSource config for group: [{}]\", group);\n\t\t}\n\n\t\tinit(//\n\t\t\t\tconfig.getAndRemoveStr(DSFactory.KEY_ALIAS_URL), //\n\t\t\t\tconfig.getAndRemoveStr(DSFactory.KEY_ALIAS_USER), //\n\t\t\t\tconfig.getAndRemoveStr(DSFactory.KEY_ALIAS_PASSWORD), //\n\t\t\t\tconfig.getAndRemoveStr(DSFactory.KEY_ALIAS_DRIVER)//\n\t\t);\n\n\t\t// 其它连接参数\n\t\tthis.connProps = config.getProps(Setting.DEFAULT_GROUP);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic SimpleDataSource(String url, String user, String pass) {\n\t\tinit(url, user, pass);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t * @param driver JDBC驱动类\n\t * @since 3.1.2\n\t */\n\tpublic SimpleDataSource(String url, String user, String pass, String driver) {\n\t\tinit(url, user, pass, driver);\n\t}\n\t// -------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param url jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic void init(String url, String user, String pass) {\n\t\tinit(url, user, pass, null);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param url jdbc url\n\t * @param user 用户名\n\t * @param pass 密码\n\t * @param driver JDBC驱动类，传入空则自动识别驱动类\n\t * @since 3.1.2\n\t */\n\tpublic void init(String url, String user, String pass, String driver) {\n\t\tthis.driver = StrUtil.isNotBlank(driver) ? driver : DriverUtil.identifyDriver(url);\n\t\ttry {\n\t\t\tClass.forName(this.driver);\n\t\t} catch (ClassNotFoundException e) {\n\t\t\tthrow new DbRuntimeException(e, \"Get jdbc driver [{}] error!\", driver);\n\t\t}\n\t\tthis.url = url;\n\t\tthis.user = user;\n\t\tthis.pass = pass;\n\t}\n\n\t// -------------------------------------------------------------------- Getters and Setters start\n\tpublic String getDriver() {\n\t\treturn driver;\n\t}\n\n\tpublic void setDriver(String driver) {\n\t\tthis.driver = driver;\n\t}\n\n\tpublic String getUrl() {\n\t\treturn url;\n\t}\n\n\tpublic void setUrl(String url) {\n\t\tthis.url = url;\n\t}\n\n\tpublic String getUser() {\n\t\treturn user;\n\t}\n\n\tpublic void setUser(String user) {\n\t\tthis.user = user;\n\t}\n\n\tpublic String getPass() {\n\t\treturn pass;\n\t}\n\n\tpublic void setPass(String pass) {\n\t\tthis.pass = pass;\n\t}\n\n\tpublic Properties getConnProps() {\n\t\treturn connProps;\n\t}\n\n\tpublic void setConnProps(Properties connProps) {\n\t\tthis.connProps = connProps;\n\t}\n\n\tpublic void addConnProps(String key, String value){\n\t\tif(null == this.connProps){\n\t\t\tthis.connProps = new Properties();\n\t\t}\n\t\tthis.connProps.setProperty(key, value);\n\t}\n\t// -------------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic Connection getConnection() throws SQLException {\n\t\tfinal Props info = new Props();\n\t\tif (this.user != null) {\n\t\t\tinfo.setProperty(\"user\", this.user);\n\t\t}\n\t\tif (this.pass != null) {\n\t\t\tinfo.setProperty(\"password\", this.pass);\n\t\t}\n\n\t\t// 其它参数\n\t\tfinal Properties connProps = this.connProps;\n\t\tif(MapUtil.isNotEmpty(connProps)){\n\t\t\tinfo.putAll(connProps);\n\t\t}\n\n\t\treturn DriverManager.getConnection(this.url, info);\n\t}\n\n\t@Override\n\tpublic Connection getConnection(String username, String password) throws SQLException {\n\t\treturn DriverManager.getConnection(this.url, username, password);\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\t// Not need to close;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/simple/package-info.java",
    "content": "/**\n * JDBC中DriverManager简易封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.simple;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/tomcat/TomcatDSFactory.java",
    "content": "package cn.hutool.db.ds.tomcat;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.ds.AbstractDSFactory;\nimport cn.hutool.setting.Setting;\nimport cn.hutool.setting.dialect.Props;\nimport org.apache.tomcat.jdbc.pool.DataSource;\nimport org.apache.tomcat.jdbc.pool.PoolProperties;\n\n/**\n * Tomcat-Jdbc-Pool数据源工厂类\n * \n * @author Looly\n *\n */\npublic class TomcatDSFactory extends AbstractDSFactory {\n\tprivate static final long serialVersionUID = 4925514193275150156L;\n\t\n\tpublic static final String DS_NAME = \"Tomcat-Jdbc-Pool\";\n\n\t/**\n\t * 构造\n\t */\n\tpublic TomcatDSFactory() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param setting Setting数据库配置\n\t */\n\tpublic TomcatDSFactory(Setting setting) {\n\t\tsuper(DS_NAME, DataSource.class, setting);\n\t}\n\t\n\t@Override\n\tprotected javax.sql.DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) {\n\t\tfinal PoolProperties poolProps = new PoolProperties();\n\t\tpoolProps.setUrl(jdbcUrl);\n\t\tpoolProps.setDriverClassName(driver);\n\t\tpoolProps.setUsername(user);\n\t\tpoolProps.setPassword(pass);\n\n\t\t// remarks等特殊配置，since 5.3.8\n\t\tfinal Props connProps = new Props();\n\t\tString connValue;\n\t\tfor (String key : KEY_CONN_PROPS) {\n\t\t\tconnValue = poolSetting.getAndRemoveStr(key);\n\t\t\tif(StrUtil.isNotBlank(connValue)){\n\t\t\t\tconnProps.setProperty(key, connValue);\n\t\t\t}\n\t\t}\n\t\tpoolProps.setDbProperties(connProps);\n\n\t\t// 连接池相关参数\n\t\tpoolSetting.toBean(poolProps);\n\t\t\n\t\treturn new DataSource(poolProps);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/ds/tomcat/package-info.java",
    "content": "/**\n * Tomcat-Pool封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.ds.tomcat;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/BeanHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.sql.ResultSet;\nimport java.sql.ResultSetMetaData;\nimport java.sql.SQLException;\n\n/**\n * Bean对象处理器，只处理第一条数据\n * \n * @param <E> 处理对象类型\n * @author loolly\n *@since 3.1.0\n */\npublic class BeanHandler<E> implements RsHandler<E>{\n\tprivate static final long serialVersionUID = -5491214744966544475L;\n\n\tprivate final Class<E> elementBeanType;\n\t\n\t/**\n\t * 创建一个 BeanHandler对象\n\t * \n\t * @param <E> 处理对象类型\n\t * @param beanType Bean类型\n\t * @return BeanHandler对象\n\t */\n\tpublic static <E> BeanHandler<E> create(Class<E> beanType) {\n\t\treturn new BeanHandler<>(beanType);\n\t}\n\n\tpublic BeanHandler(Class<E> beanType) {\n\t\tthis.elementBeanType = beanType;\n\t}\n\n\t@Override\n\tpublic E handle(ResultSet rs) throws SQLException {\n\t\tfinal ResultSetMetaData  meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\t\treturn rs.next() ? HandleHelper.handleRow(columnCount, meta, rs, this.elementBeanType) : null;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/BeanListHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 结果集处理类 ，处理出的结果为Bean列表\n * \n * @param <E> 处理对象类型\n * @author loolly\n * @since 3.1.0\n */\npublic class BeanListHandler<E> implements RsHandler<List<E>> {\n\tprivate static final long serialVersionUID = 4510569754766197707L;\n\t\n\tprivate final Class<E> elementBeanType;\n\n\t/**\n\t * 创建一个 BeanListHandler对象\n\t * \n\t * @param <E> 处理对象类型\n\t * @param beanType Bean类型\n\t * @return BeanListHandler对象\n\t */\n\tpublic static <E> BeanListHandler<E> create(Class<E> beanType) {\n\t\treturn new BeanListHandler<>(beanType);\n\t}\n\n\t/**\n\t * 构造\n\t * @param beanType Bean类型\n\t */\n\tpublic BeanListHandler(Class<E> beanType) {\n\t\tthis.elementBeanType = beanType;\n\t}\n\n\t@Override\n\tpublic List<E> handle(ResultSet rs) throws SQLException {\n\t\treturn HandleHelper.handleRsToBeanList(rs, new ArrayList<>(), elementBeanType);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/EntityHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport cn.hutool.db.Entity;\n\nimport java.sql.ResultSet;\nimport java.sql.ResultSetMetaData;\nimport java.sql.SQLException;\n\n/**\n * Entity对象处理器，只处理第一条数据\n * \n * @author loolly\n *\n */\npublic class EntityHandler implements RsHandler<Entity>{\n\tprivate static final long serialVersionUID = -8742432871908355992L;\n\n\t/** 是否大小写不敏感 */\n\tprivate final boolean caseInsensitive;\n\n\t/**\n\t * 创建一个 EntityHandler对象\n\t * @return EntityHandler对象\n\t */\n\tpublic static EntityHandler create() {\n\t\treturn new EntityHandler();\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic EntityHandler() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic EntityHandler(boolean caseInsensitive) {\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic Entity handle(ResultSet rs) throws SQLException {\n\t\tfinal ResultSetMetaData  meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\t\t\n\t\treturn rs.next() ? HandleHelper.handleRow(columnCount, meta, rs, this.caseInsensitive) : null;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/EntityListHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport cn.hutool.db.Entity;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 结果集处理类 ，处理出的结果为Entity列表\n * @author loolly\n *\n */\npublic class EntityListHandler implements RsHandler<List<Entity>>{\n\tprivate static final long serialVersionUID = -2846240126316979895L;\n\t\n\t/** 是否大小写不敏感 */\n\tprivate final boolean caseInsensitive;\n\t\n\t/**\n\t * 创建一个 EntityListHandler对象\n\t * @return EntityListHandler对象\n\t */\n\tpublic static EntityListHandler create() {\n\t\treturn new EntityListHandler();\n\t}\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic EntityListHandler() {\n\t\tthis(false);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic EntityListHandler(boolean caseInsensitive) {\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic List<Entity> handle(ResultSet rs) throws SQLException {\n\t\treturn HandleHelper.handleRs(rs, new ArrayList<>(), this.caseInsensitive);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/EntitySetHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport cn.hutool.db.Entity;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.LinkedHashSet;\n\n/**\n * 结果集处理类 ，处理出的结果为Entity列表，结果不能重复（按照Entity对象去重）\n * @author loolly\n *\n */\npublic class EntitySetHandler implements RsHandler<LinkedHashSet<Entity>>{\n\tprivate static final long serialVersionUID = 8191723216703506736L;\n\n\t/** 是否大小写不敏感 */\n\tprivate final boolean caseInsensitive;\n\n\t/**\n\t * 创建一个 EntityHandler对象\n\t * @return EntityHandler对象\n\t */\n\tpublic static EntitySetHandler create() {\n\t\treturn new EntitySetHandler();\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic EntitySetHandler() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic EntitySetHandler(boolean caseInsensitive) {\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic LinkedHashSet<Entity> handle(ResultSet rs) throws SQLException {\n\t\treturn HandleHelper.handleRs(rs, new LinkedHashSet<>(), this.caseInsensitive);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/HandleHelper.java",
    "content": "package cn.hutool.db.handler;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.PropDesc;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\nimport cn.hutool.db.Entity;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.sql.ResultSet;\nimport java.sql.ResultSetMetaData;\nimport java.sql.SQLException;\nimport java.sql.Types;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 数据结果集处理辅助类\n *\n * @author loolly\n *\n */\npublic class HandleHelper {\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param <T> Bean类型\n\t * @param columnCount 列数\n\t * @param meta ResultSetMetaData\n\t * @param rs 数据集\n\t * @param bean 目标Bean\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t * @since 3.3.1\n\t */\n\tpublic static <T> T handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, T bean) throws SQLException {\n\t\treturn handleRow(columnCount, meta, rs).toBeanIgnoreCase(bean);\n\t}\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param <T> Bean类型\n\t * @param columnCount 列数\n\t * @param meta ResultSetMetaData\n\t * @param rs 数据集\n\t * @param beanClass 目标Bean类型\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t * @since 3.3.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, Class<T> beanClass) throws SQLException {\n\t\tAssert.notNull(beanClass, \"Bean Class must be not null !\");\n\n\t\tif(beanClass.isArray()) {\n\t\t\t//返回数组\n\t\t\tfinal Class<?> componentType = beanClass.getComponentType();\n\t\t\tfinal Object[] result = ArrayUtil.newArray(componentType, columnCount);\n\t\t\tfor(int i = 0,j = 1; i < columnCount; i++, j++) {\n\t\t\t\tresult[i] = getColumnValue(rs, j, meta.getColumnType(j), componentType);\n\t\t\t}\n\t\t\treturn (T) result;\n\t\t} else if(Iterable.class.isAssignableFrom(beanClass)) {\n\t\t\t//集合\n\t\t\tfinal Object[] objRow = handleRow(columnCount, meta, rs, Object[].class);\n\t\t\treturn Convert.convert(beanClass, objRow);\n\t\t} else if(beanClass.isAssignableFrom(Entity.class)) {\n\t\t\t//Entity的父类都可按照Entity返回\n\t\t\treturn (T) handleRow(columnCount, meta, rs);\n\t\t} else if(String.class == beanClass) {\n\t\t\t//字符串\n\t\t\tfinal Object[] objRow = handleRow(columnCount, meta, rs, Object[].class);\n\t\t\treturn (T) StrUtil.join(\", \", objRow);\n\t\t}\n\n\t\t//普通bean\n\t\tfinal T bean = ReflectUtil.newInstanceIfPossible(beanClass);\n\t\t//忽略字段大小写\n\t\tfinal Map<String, PropDesc> propMap = BeanUtil.getBeanDesc(beanClass).getPropMap(true);\n\t\tString columnLabel;\n\t\tPropDesc pd;\n\t\tMethod setter;\n\t\tObject value;\n\t\tfor (int i = 1; i <= columnCount; i++) {\n\t\t\tcolumnLabel = meta.getColumnLabel(i);\n\t\t\tpd = propMap.get(columnLabel);\n\t\t\tif(null == pd) {\n\t\t\t\t// 尝试驼峰命名风格\n\t\t\t\tpd = propMap.get(StrUtil.toCamelCase(columnLabel));\n\t\t\t}\n\t\t\tsetter = (null == pd) ? null : pd.getSetter();\n\t\t\tif(null != setter) {\n\t\t\t\tvalue = getColumnValue(rs, i,  meta.getColumnType(i), TypeUtil.getFirstParamType(setter));\n\t\t\t\tReflectUtil.invokeWithCheck(bean, setter, value);\n\t\t\t}\n\t\t}\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param columnCount 列数\n\t * @param meta ResultSetMetaData\n\t * @param rs 数据集\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static Entity handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs) throws SQLException {\n\t\treturn handleRow(columnCount, meta, rs, false);\n\t}\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param columnCount 列数\n\t * @param meta ResultSetMetaData\n\t * @param rs 数据集\n\t * @param caseInsensitive 是否大小写不敏感\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.16\n\t */\n\tpublic static Entity handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, boolean caseInsensitive) throws SQLException {\n\t\treturn handleRow(new Entity(null, caseInsensitive), columnCount, meta, rs, true);\n\t}\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param <T> Entity及其子对象\n\t * @param row Entity对象\n\t * @param columnCount 列数\n\t * @param meta ResultSetMetaData\n\t * @param rs 数据集\n\t * @param withMetaInfo 是否包含表名、字段名等元信息\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t * @since 3.3.1\n\t */\n\tpublic static <T extends Entity> T handleRow(T row, int columnCount, ResultSetMetaData meta, ResultSet rs, boolean withMetaInfo) throws SQLException {\n\t\tint type;\n\t\tString columnLabel;\n\t\tfor (int i = 1; i <= columnCount; i++) {\n\t\t\ttype = meta.getColumnType(i);\n\t\t\tcolumnLabel = meta.getColumnLabel(i);\n\t\t\tif(\"rownum_\".equalsIgnoreCase(columnLabel)){\n\t\t\t\t// issue#2618@Github\n\t\t\t\t// 分页时会查出rownum字段，此处忽略掉读取\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\trow.put(columnLabel, getColumnValue(rs, i, type, null));\n\t\t}\n\t\tif (withMetaInfo) {\n\t\t\ttry {\n\t\t\t\trow.setTableName(meta.getTableName(1));\n\t\t\t} catch (SQLException ignore){\n\t\t\t\t//issue#I2AGLU@Gitee\n\t\t\t\t// Hive等NoSQL中无表的概念，此处报错，跳过。\n\t\t\t}\n\t\t\trow.setFieldNames(row.keySet());\n\t\t}\n\t\treturn row;\n\t}\n\n\t/**\n\t * 处理单条数据\n\t *\n\t * @param rs 数据集\n\t * @return 每一行的Entity\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static Entity handleRow(ResultSet rs) throws SQLException {\n\t\tfinal ResultSetMetaData meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\t\treturn handleRow(columnCount, meta, rs);\n\t}\n\n\t/**\n\t * 处理单行数据\n\t *\n\t * @param rs 数据集（行）\n\t * @return 每一行的List\n\t * @throws SQLException SQL执行异常\n\t * @since 5.1.6\n\t */\n\tpublic static List<Object> handleRowToList(ResultSet rs) throws SQLException {\n\t\tfinal ResultSetMetaData meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\t\tfinal List<Object> row = new ArrayList<>(columnCount);\n\t\tfor (int i = 1; i <= columnCount; i++) {\n\t\t\trow.add(getColumnValue(rs, i, meta.getColumnType(i), null));\n\t\t}\n\n\t\treturn row;\n\t}\n\n\t/**\n\t * 处理多条数据\n\t *\n\t * @param <T> 集合类型\n\t * @param rs 数据集\n\t * @param collection 数据集\n\t * @return Entity列表\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static <T extends Collection<Entity>> T handleRs(ResultSet rs, T collection) throws SQLException {\n\t\treturn handleRs(rs, collection, false);\n\t}\n\n\t/**\n\t * 处理多条数据\n\t *\n\t * @param <T> 集合类型\n\t * @param rs 数据集\n\t * @param collection 数据集\n\t * @param caseInsensitive 是否大小写不敏感\n\t * @return Entity列表\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.16\n\t */\n\tpublic static <T extends Collection<Entity>> T handleRs(ResultSet rs, T collection, boolean caseInsensitive) throws SQLException {\n\t\tfinal ResultSetMetaData meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\n\t\twhile (rs.next()) {\n\t\t\tcollection.add(HandleHelper.handleRow(columnCount, meta, rs, caseInsensitive));\n\t\t}\n\n\t\treturn collection;\n\t}\n\n\t/**\n\t * 处理多条数据并返回一个Bean列表\n\t *\n\t * @param <E> 集合元素类型\n\t * @param <T> 集合类型\n\t * @param rs 数据集\n\t * @param collection 数据集\n\t * @param elementBeanType Bean类型\n\t * @return Entity列表\n\t * @throws SQLException SQL执行异常\n\t * @since 3.1.0\n\t */\n\tpublic static <E, T extends Collection<E>> T handleRsToBeanList(ResultSet rs, T collection, Class<E> elementBeanType) throws SQLException {\n\t\tfinal ResultSetMetaData meta = rs.getMetaData();\n\t\tfinal int columnCount = meta.getColumnCount();\n\n\t\twhile (rs.next()) {\n\t\t\tcollection.add(handleRow(columnCount, meta, rs, elementBeanType));\n\t\t}\n\n\t\treturn collection;\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 获取字段值<br>\n\t * 针对日期时间等做单独处理判断\n\t *\n\t * @param rs {@link ResultSet}\n\t * @param columnIndex 字段索引\n\t * @param type 字段类型，默认Object\n\t * @param targetColumnType 结果要求的类型，需进行二次转换（null或者Object不转换）\n\t * @return 字段值\n\t * @throws SQLException SQL异常\n\t */\n\tprivate static Object getColumnValue(ResultSet rs, int columnIndex, int type, Type targetColumnType) throws SQLException {\n\t\tObject rawValue = null;\n\t\tswitch (type) {\n\t\tcase Types.TIMESTAMP:\n\t\t\ttry{\n\t\t\t\trawValue = rs.getTimestamp(columnIndex);\n\t\t\t} catch (SQLException ignore){\n\t\t\t\t// issue#776@Github\n\t\t\t\t// 当数据库中日期为0000-00-00 00:00:00报错，转为null\n\t\t\t}\n\t\t\tbreak;\n\t\tcase Types.TIME:\n\t\t\trawValue = rs.getTime(columnIndex);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\trawValue = rs.getObject(columnIndex);\n\t\t}\n\t\tif (null == targetColumnType || Object.class == targetColumnType) {\n\t\t\t// 无需转换\n\t\t\treturn rawValue;\n\t\t} else {\n\t\t\t// 按照返回值要求转换\n\t\t\treturn Convert.convert(targetColumnType, rawValue);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 处理为数字结果，当查询结果为单个数字时使用此处理器（例如select count(1)）\n * @author loolly\n *\n */\npublic class NumberHandler implements RsHandler<Number>{\n\tprivate static final long serialVersionUID = 4081498054379705596L;\n\n\t/**\n\t * 创建一个 NumberHandler对象\n\t * @return NumberHandler对象\n\t */\n\tpublic static NumberHandler create() {\n\t\treturn new NumberHandler();\n\t}\n\n\t@Override\n\tpublic Number handle(ResultSet rs) throws SQLException {\n\t\treturn (null != rs && rs.next()) ? rs.getBigDecimal(1) : null;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/PageResultHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.PageResult;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 分页结果集处理类 ，处理出的结果为PageResult\n *\n * @author loolly\n */\npublic class PageResultHandler implements RsHandler<PageResult<Entity>> {\n\tprivate static final long serialVersionUID = -1474161855834070108L;\n\n\tprivate final PageResult<Entity> pageResult;\n\t/**\n\t * 是否大小写不敏感\n\t */\n\tprivate final boolean caseInsensitive;\n\n\t/**\n\t * 创建一个 EntityHandler对象<br>\n\t * 结果集根据给定的分页对象查询数据库，填充结果\n\t *\n\t * @param pageResult 分页结果集空对象\n\t * @return EntityHandler对象\n\t */\n\tpublic static PageResultHandler create(PageResult<Entity> pageResult) {\n\t\treturn new PageResultHandler(pageResult);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 结果集根据给定的分页对象查询数据库，填充结果\n\t *\n\t * @param pageResult 分页结果集空对象\n\t */\n\tpublic PageResultHandler(PageResult<Entity> pageResult) {\n\t\tthis(pageResult, false);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 结果集根据给定的分页对象查询数据库，填充结果\n\t *\n\t * @param pageResult      分页结果集空对象\n\t * @param caseInsensitive 是否大小写不敏感\n\t */\n\tpublic PageResultHandler(PageResult<Entity> pageResult, boolean caseInsensitive) {\n\t\tthis.pageResult = pageResult;\n\t\tthis.caseInsensitive = caseInsensitive;\n\t}\n\n\t@Override\n\tpublic PageResult<Entity> handle(ResultSet rs) throws SQLException {\n\t\treturn HandleHelper.handleRs(rs, pageResult, this.caseInsensitive);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.io.Serializable;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 结果集处理接口<br>\n * 此接口用于实现{@link ResultSet} 转换或映射为用户指定的pojo对象\n * \n * 默认实现有：\n * @see EntityHandler\n * @see EntityListHandler\n * @see EntitySetHandler\n * @see EntitySetHandler\n * @see NumberHandler\n * @see PageResultHandler\n * \n * @author Luxiaolei\n *\n */\n@FunctionalInterface\npublic interface RsHandler<T> extends Serializable{\n\t\n\t/**\n\t * 处理结果集<br>\n\t * 结果集处理后不需要关闭\n\t * @param rs 结果集\n\t * @return 处理后生成的对象\n\t * @throws SQLException SQL异常\n\t */\n\tT handle(ResultSet rs) throws SQLException;\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/StringHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 处理为字符串结果，当查询结果为单个字符串时使用此处理器\n * \n * @author  weibaohui\n */\npublic class StringHandler implements RsHandler<String>{\n\tprivate static final long serialVersionUID = -5296733366845720383L;\n\n\t/**\n\t * 创建一个 NumberHandler对象\n\t * @return NumberHandler对象\n\t */\n\tpublic static StringHandler create() {\n\t\treturn new StringHandler();\n\t}\n\n\t@Override\n\tpublic String handle(ResultSet rs) throws SQLException {\n\t\treturn rs.next() ? rs.getString(1) : null;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/ValueListHandler.java",
    "content": "package cn.hutool.db.handler;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 结果集处理类 ，处理出的结果为List列表\n * @author loolly\n *\n */\npublic class ValueListHandler implements RsHandler<List<List<Object>>>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建一个 EntityListHandler对象\n\t * @return EntityListHandler对象\n\t */\n\tpublic static ValueListHandler create() {\n\t\treturn new ValueListHandler();\n\t}\n\n\t@Override\n\tpublic List<List<Object>> handle(ResultSet rs) throws SQLException {\n\t\tfinal ArrayList<List<Object>> result = new ArrayList<>();\n\t\twhile (rs.next()) {\n\t\t\tresult.add(HandleHelper.handleRowToList(rs));\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/handler/package-info.java",
    "content": "/**\n * JDBC结果集（ResultSet）转换封装，通过实现RsHandler接口，将ResultSet转换为我们想要的数据类型\n * \n * @author looly\n *\n */\npackage cn.hutool.db.handler;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/Column.java",
    "content": "package cn.hutool.db.meta;\n\nimport cn.hutool.core.util.BooleanUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.db.DbRuntimeException;\n\nimport java.io.Serializable;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 数据库表的列信息\n *\n * @author loolly\n */\npublic class Column implements Serializable, Cloneable {\n\tprivate static final long serialVersionUID = 577527740359719367L;\n\n\t// ----------------------------------------------------- Fields start\n\t/**\n\t * 表名\n\t */\n\tprivate String tableName;\n\n\t/**\n\t * 列名\n\t */\n\tprivate String name;\n\t/**\n\t * 类型，对应java.sql.Types中的类型\n\t */\n\tprivate int type;\n\t/**\n\t * 类型名称\n\t */\n\tprivate String typeName;\n\t/**\n\t * 大小或数据长度\n\t */\n\tprivate long size;\n\tprivate Integer digit;\n\t/**\n\t * 是否为可空\n\t */\n\tprivate boolean isNullable;\n\t/**\n\t * 注释\n\t */\n\tprivate String comment;\n\t/**\n\t * 是否自增\n\t */\n\tprivate boolean autoIncrement;\n\t/**\n\t * 字段默认值<br>\n\t *  default value for the column, which should be interpreted as a string when the value is enclosed in single quotes (may be {@code null})\n\t */\n\tprivate String columnDef;\n\t/**\n\t * 是否为主键\n\t */\n\tprivate boolean isPk;\n\t// ----------------------------------------------------- Fields end\n\n\t/**\n\t * 创建列对象\n\t *\n\t * @param columnMetaRs 列元信息的ResultSet\n\t * @param table        表信息\n\t * @return 列对象\n\t * @since 5.4.3\n\t */\n\tpublic static Column create(Table table, ResultSet columnMetaRs) {\n\t\treturn new Column(table, columnMetaRs);\n\t}\n\n\t// ----------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic Column() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param table        表信息\n\t * @param columnMetaRs Meta信息的ResultSet\n\t * @since 5.4.3\n\t */\n\tpublic Column(Table table, ResultSet columnMetaRs) {\n\t\ttry {\n\t\t\tinit(table, columnMetaRs);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e, \"Get table [{}] meta info error!\", tableName);\n\t\t}\n\t}\n\t// ----------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化\n\t *\n\t * @param table        表信息\n\t * @param columnMetaRs 列的meta ResultSet\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic void init(Table table, ResultSet columnMetaRs) throws SQLException {\n\t\tthis.tableName = table.getTableName();\n\n\t\tthis.name = columnMetaRs.getString(\"COLUMN_NAME\");\n\t\tthis.isPk = table.isPk(this.name);\n\n\t\tthis.type = columnMetaRs.getInt(\"DATA_TYPE\");\n\n\t\tString typeName = columnMetaRs.getString(\"TYPE_NAME\");\n\t\t//issue#2201@Gitee\n\t\ttypeName = ReUtil.delLast(\"\\\\(\\\\d+\\\\)\", typeName);\n\t\tthis.typeName = typeName;\n\n\t\tthis.size = columnMetaRs.getLong(\"COLUMN_SIZE\");\n\t\tthis.isNullable = columnMetaRs.getBoolean(\"NULLABLE\");\n\t\tthis.comment = columnMetaRs.getString(\"REMARKS\");\n\t\tthis.columnDef = columnMetaRs.getString(\"COLUMN_DEF\");\n\n\t\t// 保留小数位数\n\t\ttry {\n\t\t\tthis.digit = columnMetaRs.getInt(\"DECIMAL_DIGITS\");\n\t\t} catch (SQLException ignore) {\n\t\t\t//某些驱动可能不支持，跳过\n\t\t}\n\n\t\t// 是否自增\n\t\ttry {\n\t\t\tString auto = columnMetaRs.getString(\"IS_AUTOINCREMENT\");\n\t\t\tif (BooleanUtil.toBoolean(auto)) {\n\t\t\t\tthis.autoIncrement = true;\n\t\t\t}\n\t\t} catch (SQLException ignore) {\n\t\t\t//某些驱动可能不支持，跳过\n\t\t}\n\t}\n\n\t// ----------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 获取表名\n\t *\n\t * @return 表名\n\t */\n\tpublic String getTableName() {\n\t\treturn tableName;\n\t}\n\n\t/**\n\t * 设置表名\n\t *\n\t * @param tableName 表名\n\t * @return this\n\t */\n\tpublic Column setTableName(String tableName) {\n\t\tthis.tableName = tableName;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取列名\n\t *\n\t * @return 列名\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * 设置列名\n\t *\n\t * @param name 列名\n\t * @return this\n\t */\n\tpublic Column setName(String name) {\n\t\tthis.name = name;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取字段类型的枚举\n\t *\n\t * @return 阻断类型枚举\n\t * @since 4.5.8\n\t */\n\tpublic JdbcType getTypeEnum() {\n\t\treturn JdbcType.valueOf(this.type);\n\t}\n\n\t/**\n\t * 获取类型，对应{@link java.sql.Types}中的类型\n\t *\n\t * @return 类型\n\t */\n\tpublic int getType() {\n\t\treturn type;\n\t}\n\n\t/**\n\t * 设置类型，对应java.sql.Types中的类型\n\t *\n\t * @param type 类型\n\t * @return this\n\t */\n\tpublic Column setType(int type) {\n\t\tthis.type = type;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取类型名称\n\t *\n\t * @return 类型名称\n\t */\n\tpublic String getTypeName() {\n\t\treturn typeName;\n\t}\n\n\t/**\n\t * 设置类型名称\n\t *\n\t * @param typeName 类型名称\n\t * @return this\n\t */\n\tpublic Column setTypeName(String typeName) {\n\t\tthis.typeName = typeName;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取大小或数据长度\n\t *\n\t * @return 大小或数据长度\n\t */\n\tpublic long getSize() {\n\t\treturn size;\n\t}\n\n\t/**\n\t * 设置大小或数据长度\n\t *\n\t * @param size 大小或数据长度\n\t * @return this\n\t */\n\tpublic Column setSize(int size) {\n\t\tthis.size = size;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取小数位数\n\t *\n\t * @return 大小或数据长度\n\t */\n\tpublic Integer getDigit() {\n\t\treturn digit;\n\t}\n\n\t/**\n\t * 设置小数位数\n\t *\n\t * @param digit 小数位数\n\t * @return this\n\t */\n\tpublic Column setDigit(int digit) {\n\t\tthis.digit = digit;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否为可空\n\t *\n\t * @return 是否为可空\n\t */\n\tpublic boolean isNullable() {\n\t\treturn isNullable;\n\t}\n\n\t/**\n\t * 设置是否为可空\n\t *\n\t * @param isNullable 是否为可空\n\t * @return this\n\t */\n\tpublic Column setNullable(boolean isNullable) {\n\t\tthis.isNullable = isNullable;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取注释\n\t *\n\t * @return 注释\n\t */\n\tpublic String getComment() {\n\t\treturn comment;\n\t}\n\n\t/**\n\t * 设置注释\n\t *\n\t * @param comment 注释\n\t * @return this\n\t */\n\tpublic Column setComment(String comment) {\n\t\tthis.comment = comment;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否自增\n\t *\n\t * @return 是否自增\n\t * @since 5.4.3\n\t */\n\tpublic boolean isAutoIncrement() {\n\t\treturn autoIncrement;\n\t}\n\n\t/**\n\t * 设置是否自增\n\t *\n\t * @param autoIncrement 是否自增\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic Column setAutoIncrement(boolean autoIncrement) {\n\t\tthis.autoIncrement = autoIncrement;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否主键\n\t *\n\t * @return 是否主键\n\t * @since 5.4.3\n\t */\n\tpublic boolean isPk() {\n\t\treturn isPk;\n\t}\n\n\t/**\n\t * 设置是否主键\n\t *\n\t * @param isPk 是否主键\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic Column setPk(boolean isPk) {\n\t\tthis.isPk = isPk;\n\t\treturn this;\n\t}\n\t/**\n\t * 获取默认值\n\t *\n\t * @return 默认值\n\t */\n\tpublic String getColumnDef() {\n\t\treturn columnDef;\n\t}\n\n\t/**\n\t * 设置默认值\n\t *\n\t * @param columnDef 默认值\n\t * @return this\n\t */\n\tpublic Column setColumnDef(String columnDef) {\n\t\tthis.columnDef = columnDef;\n\t\treturn this;\n\t}\n\n\t// ----------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Column [tableName=\" + tableName + \", name=\" + name + \", type=\" + type + \", size=\" + size + \", isNullable=\" + isNullable + \"]\";\n\t}\n\n\t@Override\n\tpublic Column clone() throws CloneNotSupportedException {\n\t\treturn (Column) super.clone();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/ColumnIndexInfo.java",
    "content": "package cn.hutool.db.meta;\n\nimport cn.hutool.db.DbRuntimeException;\n\nimport java.io.Serializable;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\n/**\n * 索引中的列信息\n *\n * @author huzhongying\n * @since 5.7.23\n */\npublic class ColumnIndexInfo implements Serializable, Cloneable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 根据DatabaseMetaData#getIndexInfo获取的{@link ResultSet}构建索引列信息\n\t *\n\t * @param rs 结果集，通过DatabaseMetaData#getIndexInfo获取\n\t * @return ColumnIndexInfo\n\t */\n\tpublic static ColumnIndexInfo create(ResultSet rs) {\n\t\ttry {\n\t\t\treturn new ColumnIndexInfo(\n\t\t\t\t\trs.getString(\"COLUMN_NAME\"),\n\t\t\t\t\trs.getString(\"ASC_OR_DESC\"));\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 列名\n\t */\n\tprivate String columnName;\n\t/**\n\t * 列排序顺序，“A”: 升序，“D” : 降序，如果不支持排序顺序，可能为空\n\t */\n\tprivate String ascOrDesc;\n\n\t/**\n\t * 构造\n\t *\n\t * @param columnName 索引列名\n\t * @param ascOrDesc  正序或反序，null表示无顺序表示\n\t */\n\tpublic ColumnIndexInfo(String columnName, String ascOrDesc) {\n\t\tthis.columnName = columnName;\n\t\tthis.ascOrDesc = ascOrDesc;\n\t}\n\n\tpublic String getColumnName() {\n\t\treturn columnName;\n\t}\n\n\tpublic void setColumnName(String columnName) {\n\t\tthis.columnName = columnName;\n\t}\n\n\tpublic String getAscOrDesc() {\n\t\treturn ascOrDesc;\n\t}\n\n\tpublic void setAscOrDesc(String ascOrDesc) {\n\t\tthis.ascOrDesc = ascOrDesc;\n\t}\n\n\t@Override\n\tpublic ColumnIndexInfo clone() throws CloneNotSupportedException {\n\t\treturn (ColumnIndexInfo) super.clone();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ColumnIndexInfo{\" +\n\t\t\t\t\"columnName='\" + columnName + '\\'' +\n\t\t\t\t\", ascOrDesc='\" + ascOrDesc + '\\'' +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/IndexInfo.java",
    "content": "package cn.hutool.db.meta;\n\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * 数据库表的索引信息<br>\n * 如果时单列索引，只有一个{@link ColumnIndexInfo}，联合索引则拥有多个{@link ColumnIndexInfo}\n *\n * @author huzhongying\n */\npublic class IndexInfo implements Serializable, Cloneable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 索引值是否可以不唯一\n\t */\n\tprivate boolean nonUnique;\n\n\t/**\n\t * 索引名称\n\t */\n\tprivate String indexName;\n\n\t/**\n\t * 表名\n\t */\n\tprivate String tableName;\n\n\t/**\n\t * table所在的schema\n\t */\n\tprivate String schema;\n\t/**\n\t * table所在的catalog\n\t */\n\tprivate String catalog;\n\n\t/**\n\t * 索引中的列信息,按索引顺序排列\n\t */\n\tprivate List<ColumnIndexInfo> columnIndexInfoList;\n\n\t/**\n\t * 构造\n\t *\n\t * @param nonUnique 索引值是否可以不唯一\n\t * @param indexName 索引名称\n\t * @param tableName 表名\n\t * @param schema    table所在的schema\n\t * @param catalog   table所在的catalog\n\t */\n\tpublic IndexInfo(boolean nonUnique, String indexName, String tableName, String schema, String catalog) {\n\t\tthis.nonUnique = nonUnique;\n\t\tthis.indexName = indexName;\n\t\tthis.tableName = tableName;\n\t\tthis.schema = schema;\n\t\tthis.catalog = catalog;\n\t\tthis.setColumnIndexInfoList(new ArrayList<>());\n\t}\n\n\tpublic boolean isNonUnique() {\n\t\treturn nonUnique;\n\t}\n\n\tpublic void setNonUnique(boolean nonUnique) {\n\t\tthis.nonUnique = nonUnique;\n\t}\n\n\tpublic String getIndexName() {\n\t\treturn indexName;\n\t}\n\n\tpublic void setIndexName(String indexName) {\n\t\tthis.indexName = indexName;\n\t}\n\n\tpublic String getTableName() {\n\t\treturn tableName;\n\t}\n\n\tpublic void setTableName(String tableName) {\n\t\tthis.tableName = tableName;\n\t}\n\n\tpublic String getSchema() {\n\t\treturn schema;\n\t}\n\n\tpublic void setSchema(String schema) {\n\t\tthis.schema = schema;\n\t}\n\n\tpublic String getCatalog() {\n\t\treturn catalog;\n\t}\n\n\tpublic void setCatalog(String catalog) {\n\t\tthis.catalog = catalog;\n\t}\n\n\tpublic List<ColumnIndexInfo> getColumnIndexInfoList() {\n\t\treturn columnIndexInfoList;\n\t}\n\n\tpublic void setColumnIndexInfoList(List<ColumnIndexInfo> columnIndexInfoList) {\n\t\tthis.columnIndexInfoList = columnIndexInfoList;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tIndexInfo indexInfo = (IndexInfo) o;\n\t\treturn ObjectUtil.equals(indexName, indexInfo.indexName)\n\t\t\t\t&& ObjectUtil.equals(tableName, indexInfo.tableName);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(indexName, tableName);\n\t}\n\n\t@Override\n\tpublic IndexInfo clone() throws CloneNotSupportedException {\n\t\treturn (IndexInfo) super.clone();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"IndexInfo{\" +\n\t\t\t\t\"nonUnique=\" + nonUnique +\n\t\t\t\t\", indexName='\" + indexName + '\\'' +\n\t\t\t\t\", tableName='\" + tableName + '\\'' +\n\t\t\t\t\", schema='\" + schema + '\\'' +\n\t\t\t\t\", catalog='\" + catalog + '\\'' +\n\t\t\t\t\", columnIndexInfoList=\" + columnIndexInfoList +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/JdbcType.java",
    "content": "package cn.hutool.db.meta;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\n\nimport java.util.Map;\n\n/**\n * JDBC中字段类型枚举\n *\n * @author Clinton Begin\n * @see java.sql.Types\n */\npublic enum JdbcType {\n\tARRAY(java.sql.Types.ARRAY), //\n\tBIT(java.sql.Types.BIT), //\n\tTINYINT(java.sql.Types.TINYINT), //\n\tSMALLINT(java.sql.Types.SMALLINT), //\n\tINTEGER(java.sql.Types.INTEGER), //\n\tBIGINT(java.sql.Types.BIGINT), //\n\tFLOAT(java.sql.Types.FLOAT), //\n\tREAL(java.sql.Types.REAL), //\n\tDOUBLE(java.sql.Types.DOUBLE), //\n\tNUMERIC(java.sql.Types.NUMERIC), //\n\tDECIMAL(java.sql.Types.DECIMAL), //\n\tCHAR(java.sql.Types.CHAR), //\n\tVARCHAR(java.sql.Types.VARCHAR), //\n\tLONGVARCHAR(java.sql.Types.LONGVARCHAR), //\n\tDATE(java.sql.Types.DATE), //\n\tTIME(java.sql.Types.TIME), //\n\tTIMESTAMP(java.sql.Types.TIMESTAMP), //\n\tBINARY(java.sql.Types.BINARY), //\n\tVARBINARY(java.sql.Types.VARBINARY), //\n\tLONGVARBINARY(java.sql.Types.LONGVARBINARY), //\n\tNULL(java.sql.Types.NULL), //\n\tOTHER(java.sql.Types.OTHER), //\n\tBLOB(java.sql.Types.BLOB), //\n\tCLOB(java.sql.Types.CLOB), //\n\tBOOLEAN(java.sql.Types.BOOLEAN), //\n\tCURSOR(-10), // Oracle\n\tUNDEFINED(Integer.MIN_VALUE + 1000), //\n\tNVARCHAR(java.sql.Types.NVARCHAR), // JDK6\n\tNCHAR(java.sql.Types.NCHAR), // JDK6\n\tNCLOB(java.sql.Types.NCLOB), // JDK6\n\tSTRUCT(java.sql.Types.STRUCT), //\n\tJAVA_OBJECT(java.sql.Types.JAVA_OBJECT), //\n\tDISTINCT(java.sql.Types.DISTINCT), //\n\tREF(java.sql.Types.REF), //\n\tDATALINK(java.sql.Types.DATALINK), //\n\tROWID(java.sql.Types.ROWID), // JDK6\n\tLONGNVARCHAR(java.sql.Types.LONGNVARCHAR), // JDK6\n\tSQLXML(java.sql.Types.SQLXML), // JDK6\n\tDATETIMEOFFSET(-155), // SQL Server 2008\n\tTIME_WITH_TIMEZONE(2013), // JDBC 4.2 JDK8\n\tTIMESTAMP_WITH_TIMEZONE(2014); // JDBC 4.2 JDK8\n\n\tpublic final int typeCode;\n\n\t/**\n\t * 构造\n\t *\n\t * @param code {@link java.sql.Types} 中对应的值\n\t */\n\tJdbcType(int code) {\n\t\tthis.typeCode = code;\n\t}\n\n\tprivate static final Map<Integer, JdbcType> CODE_MAP = new SafeConcurrentHashMap<>(100, 1);\n\tstatic {\n\t\tfor (JdbcType type : JdbcType.values()) {\n\t\t\tCODE_MAP.put(type.typeCode, type);\n\t\t}\n\t}\n\n\t/**\n\t * 通过{@link java.sql.Types}中对应int值找到enum值\n\t *\n\t * @param code Jdbc type值\n\t * @return {@code JdbcType}\n\t */\n\tpublic static JdbcType valueOf(int code) {\n\t\treturn CODE_MAP.get(code);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java",
    "content": "package cn.hutool.db.meta;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.db.Entity;\n\nimport javax.sql.DataSource;\nimport java.sql.*;\nimport java.util.*;\n\n/**\n * 数据库元数据信息工具类\n *\n * <p>\n * 需要注意的是，此工具类在某些数据库（比如Oracle）下无效，此时需要手动在数据库配置中增加：\n * <pre>\n *  remarks = true\n *  useInformationSchema = true\n * </pre>\n *\n * @author looly\n */\npublic class MetaUtil {\n\t/**\n\t * 获得所有表名\n\t *\n\t * @param ds 数据源\n\t * @return 表名列表\n\t */\n\tpublic static List<String> getTables(DataSource ds) {\n\t\treturn getTables(ds, TableType.TABLE);\n\t}\n\n\t/**\n\t * 获得所有表名\n\t *\n\t * @param ds    数据源\n\t * @param types 表类型\n\t * @return 表名列表\n\t */\n\tpublic static List<String> getTables(DataSource ds, TableType... types) {\n\t\treturn getTables(ds, null, null, types);\n\t}\n\n\t/**\n\t * 获得所有表名\n\t *\n\t * @param ds     数据源\n\t * @param schema 表数据库名，对于Oracle为用户名\n\t * @param types  表类型\n\t * @return 表名列表\n\t * @since 3.3.1\n\t */\n\tpublic static List<String> getTables(DataSource ds, String schema, TableType... types) {\n\t\treturn getTables(ds, schema, null, types);\n\t}\n\n\t/**\n\t * 获得所有表名\n\t *\n\t * @param ds        数据源\n\t * @param schema    表数据库名，对于Oracle为用户名\n\t * @param tableName 表名\n\t * @param types     表类型\n\t * @return 表名列表\n\t * @since 3.3.1\n\t */\n\tpublic static List<String> getTables(DataSource ds, String schema, String tableName, TableType... types) {\n\t\tfinal List<String> tables = new ArrayList<>();\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = ds.getConnection();\n\n\t\t\t// catalog和schema获取失败默认使用null代替\n\t\t\tfinal String catalog = getCatalog(conn);\n\t\t\tif (null == schema) {\n\t\t\t\tschema = getSchema(conn);\n\t\t\t}\n\n\t\t\tfinal DatabaseMetaData metaData = conn.getMetaData();\n\t\t\ttry (final ResultSet rs = metaData.getTables(catalog, schema, tableName, Convert.toStrArray(types))) {\n\t\t\t\tif (null != rs) {\n\t\t\t\t\tString table;\n\t\t\t\t\twhile (rs.next()) {\n\t\t\t\t\t\ttable = rs.getString(\"TABLE_NAME\");\n\t\t\t\t\t\tif (StrUtil.isNotBlank(table)) {\n\t\t\t\t\t\t\ttables.add(table);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new DbRuntimeException(\"Get tables error!\", e);\n\t\t} finally {\n\t\t\tDbUtil.close(conn);\n\t\t}\n\t\treturn tables;\n\t}\n\n\t/**\n\t * 获得结果集的所有列名\n\t *\n\t * @param rs 结果集\n\t * @return 列名数组\n\t * @throws DbRuntimeException SQL执行异常\n\t */\n\tpublic static String[] getColumnNames(ResultSet rs) throws DbRuntimeException {\n\t\ttry {\n\t\t\tfinal ResultSetMetaData rsmd = rs.getMetaData();\n\t\t\tfinal int columnCount = rsmd.getColumnCount();\n\t\t\tfinal String[] labelNames = new String[columnCount];\n\t\t\tfor (int i = 0; i < labelNames.length; i++) {\n\t\t\t\tlabelNames[i] = rsmd.getColumnLabel(i + 1);\n\t\t\t}\n\t\t\treturn labelNames;\n\t\t} catch (Exception e) {\n\t\t\tthrow new DbRuntimeException(\"Get colunms error!\", e);\n\t\t}\n\t}\n\n\t/**\n\t * 获得表的所有列名\n\t *\n\t * @param ds        数据源\n\t * @param tableName 表名\n\t * @return 列数组\n\t * @throws DbRuntimeException SQL执行异常\n\t */\n\tpublic static String[] getColumnNames(DataSource ds, String tableName) {\n\t\tfinal List<String> columnNames = new ArrayList<>();\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = ds.getConnection();\n\n\t\t\t// catalog和schema获取失败默认使用null代替\n\t\t\tfinal String catalog = getCatalog(conn);\n\t\t\tfinal String schema = getSchema(conn);\n\n\t\t\tfinal DatabaseMetaData metaData = conn.getMetaData();\n\t\t\ttry (final ResultSet rs = metaData.getColumns(catalog, schema, tableName, null)) {\n\t\t\t\tif (null != rs) {\n\t\t\t\t\twhile (rs.next()) {\n\t\t\t\t\t\tcolumnNames.add(rs.getString(\"COLUMN_NAME\"));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn columnNames.toArray(new String[0]);\n\t\t} catch (Exception e) {\n\t\t\tthrow new DbRuntimeException(\"Get columns error!\", e);\n\t\t} finally {\n\t\t\tDbUtil.close(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 创建带有字段限制的Entity对象<br>\n\t * 此方法读取数据库中对应表的字段列表，加入到Entity中，当Entity被设置内容时，会忽略对应表字段外的所有KEY\n\t *\n\t * @param ds        数据源\n\t * @param tableName 表名\n\t * @return Entity对象\n\t */\n\tpublic static Entity createLimitedEntity(DataSource ds, String tableName) {\n\t\tfinal String[] columnNames = getColumnNames(ds, tableName);\n\t\treturn Entity.create(tableName).setFieldNames(columnNames);\n\t}\n\n\t/**\n\t * 获得表的元信息<br>\n\t * 注意如果需要获取注释，某些数据库如MySQL，需要在配置中添加:\n\t * <pre>\n\t *     remarks = true\n\t *     useInformationSchema = true\n\t * </pre>\n\t *\n\t * @param ds        数据源\n\t * @param tableName 表名\n\t * @return Table对象\n\t */\n\tpublic static Table getTableMeta(DataSource ds, String tableName) {\n\t\treturn getTableMeta(ds, null, null, tableName);\n\t}\n\n\t/**\n\t * 获得表的元信息<br>\n\t * 注意如果需要获取注释，某些数据库如MySQL，需要在配置中添加:\n\t * <pre>\n\t *     remarks = true\n\t *     useInformationSchema = true\n\t * </pre>\n\t *\n\t * @param ds        数据源\n\t * @param tableName 表名\n\t * @param catalog   catalog name，{@code null}表示自动获取，见：{@link #getCatalog(Connection)}\n\t * @param schema    a schema name pattern，{@code null}表示自动获取，见：{@link #getSchema(Connection)}\n\t * @return Table对象\n\t * @since 5.7.22\n\t */\n\tpublic static Table getTableMeta(DataSource ds, String catalog, String schema, String tableName) {\n\t\tConnection conn = null;\n\t\ttry {\n\t\t\tconn = ds.getConnection();\n\t\t\treturn getTableMeta(conn, catalog, schema, tableName);\n\t\t} catch (final SQLException e){\n\t\t\tthrow new DbRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(conn);\n\t\t}\n\t}\n\n\t/**\n\t * 获得表的元信息<br>\n\t * 注意如果需要获取注释，某些数据库如MySQL，需要在配置中添加:\n\t * <pre>\n\t *     remarks = true\n\t *     useInformationSchema = true\n\t * </pre>\n\t *\n\t * @param conn       数据库连接对象，使用结束后不会关闭。\n\t * @param tableName 表名\n\t * @param catalog   catalog name，{@code null}表示自动获取，见：{@link #getCatalog(Connection)}\n\t * @param schema    a schema name pattern，{@code null}表示自动获取，见：{@link #getSchema(Connection)}\n\t * @return Table对象\n\t * @since 5.8.28\n\t */\n\tpublic static Table getTableMeta(final Connection conn, String catalog, String schema, final String tableName) {\n\t\tfinal Table table = Table.create(tableName);\n\n\t\t// catalog和schema获取失败默认使用null代替\n\t\tif (null == catalog) {\n\t\t\tcatalog = getCatalog(conn);\n\t\t}\n\t\ttable.setCatalog(catalog);\n\t\tif (null == schema) {\n\t\t\tschema = getSchema(conn);\n\t\t}\n\t\ttable.setSchema(schema);\n\n\t\tfinal DatabaseMetaData metaData = getMetaData(conn);\n\t\t// 获取原始表名\n\t\tfinal String pureTableName = unWrapIfOracle(metaData, tableName);\n\t\t// 获得表元数据（表注释）\n\t\ttable.setComment(getRemarks(metaData, catalog, schema, pureTableName));\n\t\t// 获得主键\n\t\ttable.setPkNames(getPrimaryKeys(metaData, catalog, schema, pureTableName));\n\t\t// 获得列\n\t\tfetchColumns(metaData, catalog, schema, table);\n\t\t// 获得索引信息(since 5.7.23)\n\t\tfinal Map<String, IndexInfo> indexInfoMap = getIndexInfo(metaData, catalog, schema, tableName);\n\t\ttable.setIndexInfoList(ListUtil.toList(indexInfoMap.values()));\n\n\t\treturn table;\n\t}\n\n\t/**\n\t * 获取catalog，获取失败返回{@code null}\n\t *\n\t * @param conn {@link Connection} 数据库连接，{@code null}时返回null\n\t * @return catalog，获取失败返回{@code null}\n\t * @since 4.6.0\n\t * @deprecated 拼写错误，请使用{@link #getCatalog(Connection)}\n\t */\n\t@Deprecated\n\tpublic static String getCataLog(Connection conn) {\n\t\treturn getCatalog(conn);\n\t}\n\n\t/**\n\t * 获取catalog，获取失败返回{@code null}\n\t *\n\t * @param conn {@link Connection} 数据库连接，{@code null}时返回null\n\t * @return catalog，获取失败返回{@code null}\n\t * @since 5.7.23\n\t */\n\tpublic static String getCatalog(Connection conn) {\n\t\tif (null == conn) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn conn.getCatalog();\n\t\t} catch (SQLException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取schema，获取失败返回{@code null}\n\t *\n\t * @param conn {@link Connection} 数据库连接，{@code null}时返回null\n\t * @return schema，获取失败返回{@code null}\n\t * @since 4.6.0\n\t */\n\tpublic static String getSchema(Connection conn) {\n\t\tif (null == conn) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn conn.getSchema();\n\t\t} catch (SQLException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取数据库连接的元数据信息。\n\t *\n\t * @param conn 数据库连接对象。如果连接为null，则返回null。\n\t * @return DatabaseMetaData 数据库元数据对象，如果获取失败或连接为null，则返回null。\n\t * @since 5.8.28\n\t */\n\tpublic static DatabaseMetaData getMetaData(final Connection conn) {\n\t\tif (null == conn) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn conn.getMetaData();\n\t\t} catch (final SQLException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定表的备注信息。\n\t *\n\t * @param metaData  数据库元数据，用于查询表信息。\n\t * @param catalog   目录名称，用于指定查询的数据库（可为{@code null}，表示任意目录）。\n\t * @param schema    方案名称，用于指定表所属的schema（可为{@code null}，表示任意schema）。\n\t * @param tableName 表名称，指定要查询备注信息的表。\n\t * @return 表的备注信息。未找到指定的表或查询成功但无结果，则返回null。\n\t * @since 5.8.28\n\t */\n\tpublic static String getRemarks(final DatabaseMetaData metaData, final String catalog, final String schema, String tableName) {\n\t\t// issue#I9BANE Oracle中特殊表名需要解包\n\t\ttableName = unWrapIfOracle(metaData, tableName);\n\n\t\ttry (final ResultSet rs = metaData.getTables(catalog, schema, tableName, new String[]{TableType.TABLE.value()})) {\n\t\t\tif (null != rs) {\n\t\t\t\tif (rs.next()) {\n\t\t\t\t\treturn rs.getString(\"REMARKS\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\t// 未找到指定的表或查询成功但无结果\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定表的主键列名列表。\n\t *\n\t * @param metaData  数据库元数据，用于查询主键信息。\n\t * @param catalog   数据库目录，用于限定查询范围。\n\t * @param schema    数据库模式，用于限定查询范围。\n\t * @param tableName 表名，指定要查询主键的表。\n\t * @return 主键列名的列表。如果表没有主键，则返回空列表。\n\t * @throws DbRuntimeException 如果查询过程中发生SQLException，将抛出DbRuntimeException。\n\t * @since 5.8.28\n\t */\n\tpublic static Set<String> getPrimaryKeys(final DatabaseMetaData metaData, final String catalog, final String schema, String tableName) {\n\t\t// issue#I9BANE Oracle中特殊表名需要解包\n\t\ttableName = unWrapIfOracle(metaData, tableName);\n\n\t\t// 初始化主键列表\n\t\tSet<String> primaryKeys = null;\n\t\ttry (final ResultSet rs = metaData.getPrimaryKeys(catalog, schema, tableName)) {\n\t\t\t// 如果结果集不为空，遍历结果集获取主键列名\n\t\t\tif (null != rs) {\n\t\t\t\tprimaryKeys = new LinkedHashSet<>(rs.getFetchSize(), 1);\n\t\t\t\twhile (rs.next()) {\n\t\t\t\t\tprimaryKeys.add(rs.getString(\"COLUMN_NAME\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final SQLException e) {\n\t\t\t// 将SQLException转换为自定义的DbException抛出\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn primaryKeys;\n\t}\n\n\t/**\n\t * 获取指定表的索引信息。\n\t *\n\t * @param metaData  数据库元数据，用于查询索引信息。\n\t * @param catalog   数据库目录，用于限定查询范围。\n\t * @param schema    数据库模式，用于限定查询范围。\n\t * @param tableName 需要查询索引信息的表名。\n\t * @return 返回一个映射，其中包含表的索引信息。键是表名和索引名的组合，值是索引信息对象。\n\t * @since 5.8.28\n\t */\n\tpublic static Map<String, IndexInfo> getIndexInfo(final DatabaseMetaData metaData, final String catalog, final String schema, final String tableName) {\n\t\tfinal Map<String, IndexInfo> indexInfoMap = new LinkedHashMap<>();\n\n\t\ttry (final ResultSet rs = metaData.getIndexInfo(catalog, schema, tableName, false, false)) {\n\t\t\tif (null != rs) {\n\t\t\t\twhile (rs.next()) {\n\t\t\t\t\t//排除统计（tableIndexStatistic）类型索引\n\t\t\t\t\tif (0 == rs.getShort(\"TYPE\")) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tfinal String indexName = rs.getString(\"INDEX_NAME\");\n\t\t\t\t\tfinal String key = StrUtil.join(\"&\", tableName, indexName);\n\t\t\t\t\t// 联合索引情况下一个索引会有多个列，此处须组合索引列到一个索引信息对象下\n\t\t\t\t\tIndexInfo indexInfo = indexInfoMap.get(key);\n\t\t\t\t\tif (null == indexInfo) {\n\t\t\t\t\t\tindexInfo = new IndexInfo(rs.getBoolean(\"NON_UNIQUE\"), indexName, tableName, schema, catalog);\n\t\t\t\t\t\tindexInfoMap.put(key, indexInfo);\n\t\t\t\t\t}\n\t\t\t\t\tindexInfo.getColumnIndexInfoList().add(ColumnIndexInfo.create(rs));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn indexInfoMap;\n\t}\n\n\t/**\n\t * 判断当前数据库是否为Oracle。\n\t *\n\t * @param metaData 数据库元数据，用于获取数据库产品名称。\n\t * @return 返回true表示当前数据库是Oracle，否则返回false。\n\t * @throws DbRuntimeException 如果获取数据库产品名称时发生SQLException，将抛出DbRuntimeException。\n\t * @since 5.8.28\n\t */\n\tpublic static boolean isOracle(final DatabaseMetaData metaData) throws DbRuntimeException {\n\t\ttry {\n\t\t\treturn StrUtil.equalsIgnoreCase(\"Oracle\", metaData.getDatabaseProductName());\n\t\t} catch (final SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 如果是在Oracle数据库中并且表名被双引号包裹，则移除这些引号。\n\t *\n\t * @param metaData  数据库元数据，用于判断是否为Oracle数据库。\n\t * @param tableName 待处理的表名，可能被双引号包裹。\n\t * @return 处理后的表名，如果原表名被双引号包裹且是Oracle数据库，则返回去除了双引号的表名；否则返回原表名。\n\t */\n\tprivate static String unWrapIfOracle(final DatabaseMetaData metaData, String tableName) {\n\t\tfinal char wrapChar = '\"';\n\t\t// 判断表名是否被双引号包裹且当前数据库为Oracle，如果是，则移除双引号\n\t\tif (StrUtil.isWrap(tableName, wrapChar) && isOracle(metaData)) {\n\t\t\ttableName = StrUtil.unWrap(tableName, wrapChar);\n\t\t}\n\t\treturn tableName;\n\t}\n\n\t/**\n\t * 从数据库元数据中获取指定表的列信息。\n\t *\n\t * @param metaData 数据库元数据，用于查询列信息。\n\t * @param catalog  数据库目录，用于过滤列信息。\n\t * @param schema   数据库模式，用于过滤列信息。\n\t * @param table    表对象，用于存储获取到的列信息。\n\t */\n\tprivate static void fetchColumns(final DatabaseMetaData metaData, final String catalog, final String schema, final Table table) {\n\t\t// issue#I9BANE Oracle中特殊表名需要解包\n\t\tfinal String tableName = unWrapIfOracle(metaData, table.getTableName());\n\n\t\t// 获得列\n\t\ttry (final ResultSet rs = metaData.getColumns(catalog, schema, tableName, null)) {\n\t\t\tif (null != rs) {\n\t\t\t\twhile (rs.next()) {\n\t\t\t\t\ttable.setColumn(Column.create(table, rs));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/Table.java",
    "content": "package cn.hutool.db.meta;\n\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 数据库表信息\n *\n * @author loolly\n */\npublic class Table implements Serializable, Cloneable {\n\tprivate static final long serialVersionUID = -810699625961392983L;\n\n\t/**\n\t * table所在的schema\n\t */\n\tprivate String schema;\n\t/**\n\t * tables所在的catalog\n\t */\n\tprivate String catalog;\n\t/**\n\t * 表名\n\t */\n\tprivate String tableName;\n\t/**\n\t * 注释\n\t */\n\tprivate String comment;\n\t/**\n\t * 主键字段名列表\n\t */\n\tprivate Set<String> pkNames = new LinkedHashSet<>();\n\t/**\n\t * 索引信息\n\t */\n\tprivate List<IndexInfo> indexInfoList;\n\t/**\n\t * 列映射，列名-列对象\n\t */\n\tprivate final Map<String, Column> columns = new LinkedHashMap<>();\n\n\tpublic static Table create(String tableName) {\n\t\treturn new Table(tableName);\n\t}\n\n\t// ----------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param tableName 表名\n\t */\n\tpublic Table(String tableName) {\n\t\tthis.setTableName(tableName);\n\t}\n\t// ----------------------------------------------------- Constructor end\n\n\t// ----------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 获取 schema\n\t *\n\t * @return schema\n\t * @since 5.4.3\n\t */\n\tpublic String getSchema() {\n\t\treturn schema;\n\t}\n\n\t/**\n\t * 设置schema\n\t *\n\t * @param schema schema\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic Table setSchema(String schema) {\n\t\tthis.schema = schema;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取catalog\n\t *\n\t * @return catalog\n\t * @since 5.4.3\n\t */\n\tpublic String getCatalog() {\n\t\treturn catalog;\n\t}\n\n\t/**\n\t * 设置catalog\n\t *\n\t * @param catalog catalog\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic Table setCatalog(String catalog) {\n\t\tthis.catalog = catalog;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取表名\n\t *\n\t * @return 表名\n\t */\n\tpublic String getTableName() {\n\t\treturn tableName;\n\t}\n\n\t/**\n\t * 设置表名\n\t *\n\t * @param tableName 表名\n\t */\n\tpublic void setTableName(String tableName) {\n\t\tthis.tableName = tableName;\n\t}\n\n\t/**\n\t * 获取注释\n\t *\n\t * @return 注释\n\t */\n\tpublic String getComment() {\n\t\treturn comment;\n\t}\n\n\t/**\n\t * 设置注释\n\t *\n\t * @param comment 注释\n\t * @return this\n\t */\n\tpublic Table setComment(String comment) {\n\t\tthis.comment = comment;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取主键列表\n\t *\n\t * @return 主键列表\n\t */\n\tpublic Set<String> getPkNames() {\n\t\treturn pkNames;\n\t}\n\n\t/**\n\t * 给定列名是否为主键\n\t *\n\t * @param columnName 列名\n\t * @return 是否为主键\n\t * @since 5.4.3\n\t */\n\tpublic boolean isPk(String columnName) {\n\t\treturn getPkNames().contains(columnName);\n\t}\n\n\t/**\n\t * 设置主键列表\n\t *\n\t * @param pkNames 主键列表\n\t */\n\tpublic void setPkNames(Set<String> pkNames) {\n\t\tthis.pkNames = pkNames;\n\t}\n\t// ----------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 设置列对象\n\t *\n\t * @param column 列对象\n\t * @return 自己\n\t */\n\tpublic Table setColumn(Column column) {\n\t\tthis.columns.put(column.getName(), column);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取某列信息\n\t *\n\t * @param name 列名\n\t * @return 列对象\n\t * @since 4.2.2\n\t */\n\tpublic Column getColumn(String name) {\n\t\treturn this.columns.get(name);\n\t}\n\n\t/**\n\t * 获取所有字段元信息\n\t *\n\t * @return 字段元信息集合\n\t * @since 4.5.8\n\t */\n\tpublic Collection<Column> getColumns() {\n\t\treturn this.columns.values();\n\t}\n\n\t/**\n\t * 添加主键\n\t *\n\t * @param pkColumnName 主键的列名\n\t * @return 自己\n\t */\n\tpublic Table addPk(String pkColumnName) {\n\t\tthis.pkNames.add(pkColumnName);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取索引信息\n\t *\n\t * @return 索引信息\n\t * @since 5.7.23\n\t */\n\tpublic List<IndexInfo> getIndexInfoList() {\n\t\treturn indexInfoList;\n\t}\n\n\t/**\n\t * 设置索引信息\n\t *\n\t * @param indexInfoList 索引信息列表\n\t * @since 5.7.23\n\t */\n\tpublic void setIndexInfoList(List<IndexInfo> indexInfoList) {\n\t\tthis.indexInfoList = indexInfoList;\n\t}\n\n\t@Override\n\tpublic Table clone() throws CloneNotSupportedException {\n\t\treturn (Table) super.clone();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/TableType.java",
    "content": "package cn.hutool.db.meta;\n\n/**\n * 元信息中表的类型\n *\n * @author Looly\n */\npublic enum TableType {\n\tTABLE(\"TABLE\"),\n\tVIEW(\"VIEW\"),\n\tSYSTEM_TABLE(\"SYSTEM TABLE\"),\n\tGLOBAL_TEMPORARY(\"GLOBAL TEMPORARY\"),\n\tLOCAL_TEMPORARY(\"LOCAL TEMPORARY\"),\n\tALIAS(\"ALIAS\"),\n\tSYNONYM(\"SYNONYM\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tTableType(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @return 值\n\t */\n\tpublic String value() {\n\t\treturn this.value;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.value();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/meta/package-info.java",
    "content": "/**\n * JDBC数据表元数据信息封装，包括表结构、列信息的封装，入口为MetaUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.db.meta;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java",
    "content": "package cn.hutool.db.nosql.mongo;\n\nimport cn.hutool.core.exceptions.NotInitedException;\nimport cn.hutool.core.net.NetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.log.Log;\nimport cn.hutool.setting.Setting;\nimport com.mongodb.MongoClientSettings;\nimport com.mongodb.MongoCredential;\nimport com.mongodb.ServerAddress;\nimport com.mongodb.client.MongoClient;\nimport com.mongodb.client.MongoClients;\nimport com.mongodb.client.MongoCollection;\nimport com.mongodb.client.MongoDatabase;\nimport com.mongodb.connection.ConnectionPoolSettings;\nimport com.mongodb.connection.SocketSettings;\nimport org.bson.Document;\n\nimport java.io.Closeable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * MongoDB4工具类\n *\n * @author VampireAchao\n */\npublic class MongoDS implements Closeable {\n\n\tprivate final static Log log = Log.get();\n\n\t/**\n\t * 默认配置文件\n\t */\n\tpublic final static String MONGO_CONFIG_PATH = \"config/mongo.setting\";\n\n\t// MongoDB配置文件\n\tprivate Setting setting;\n\t// MongoDB实例连接列表\n\tprivate String[] groups;\n\t// MongoDB单点连接信息\n\tprivate ServerAddress serverAddress;\n\t// MongoDB客户端对象\n\tprivate MongoClient mongo;\n\n\t// --------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造MongoDB数据源<br>\n\t * 调用者必须持有MongoDS实例，否则会被垃圾回收导致写入失败！\n\t *\n\t * @param host 主机（域名或者IP）\n\t * @param port 端口\n\t */\n\tpublic MongoDS(String host, int port) {\n\t\tthis.serverAddress = createServerAddress(host, port);\n\t\tinitSingle();\n\t}\n\n\t/**\n\t * 构造MongoDB数据源<br>\n\t * 调用者必须持有MongoDS实例，否则会被垃圾回收导致写入失败！\n\t *\n\t * @param mongoSetting MongoDB的配置文件，如果是null则读取默认配置文件或者使用MongoDB默认客户端配置\n\t * @param host         主机（域名或者IP）\n\t * @param port         端口\n\t */\n\tpublic MongoDS(Setting mongoSetting, String host, int port) {\n\t\tthis.setting = mongoSetting;\n\t\tthis.serverAddress = createServerAddress(host, port);\n\t\tinitSingle();\n\t}\n\n\t/**\n\t * 构造MongoDB数据源<br>\n\t * 当提供多个数据源时，这些数据源将为一个副本集或者多个mongos<br>\n\t * 调用者必须持有MongoDS实例，否则会被垃圾回收导致写入失败！ 官方文档： http://docs.mongodb.org/manual/administration/replica-sets/\n\t *\n\t * @param groups 分组列表，当为null或空时使用无分组配置，一个分组使用单一模式，否则使用副本集模式\n\t */\n\tpublic MongoDS(String... groups) {\n\t\tthis.groups = groups;\n\t\tinit();\n\t}\n\n\t/**\n\t * 构造MongoDB数据源<br>\n\t * 当提供多个数据源时，这些数据源将为一个副本集或者mongos<br>\n\t * 调用者必须持有MongoDS实例，否则会被垃圾回收导致写入失败！<br>\n\t * 官方文档： http://docs.mongodb.org/manual/administration/replica-sets/\n\t *\n\t * @param mongoSetting MongoDB的配置文件，必须有\n\t * @param groups       分组列表，当为null或空时使用无分组配置，一个分组使用单一模式，否则使用副本集模式\n\t */\n\tpublic MongoDS(Setting mongoSetting, String... groups) {\n\t\tif (mongoSetting == null) {\n\t\t\tthrow new DbRuntimeException(\"Mongo setting is null!\");\n\t\t}\n\t\tthis.setting = mongoSetting;\n\t\tthis.groups = groups;\n\t\tinit();\n\t}\n\t// --------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化，当给定分组数大于一个时使用\n\t */\n\tpublic void init() {\n\t\tif (groups != null && groups.length > 1) {\n\t\t\tinitCloud();\n\t\t} else {\n\t\t\tinitSingle();\n\t\t}\n\t}\n\n\t/**\n\t * 初始化<br>\n\t * 设定文件中的host和端口有三种形式：\n\t *\n\t * <pre>\n\t * host = host:port\n\t * </pre>\n\t *\n\t * <pre>\n\t * host = host\n\t * port = port\n\t * </pre>\n\t *\n\t * <pre>\n\t * host = host\n\t * </pre>\n\t */\n\tsynchronized public void initSingle() {\n\t\tif (setting == null) {\n\t\t\ttry {\n\t\t\t\tsetting = new Setting(MONGO_CONFIG_PATH, true);\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 在single模式下，可以没有配置文件。\n\t\t\t}\n\t\t}\n\n\t\tString group = StrUtil.EMPTY;\n\t\tif (null == this.serverAddress) {\n\t\t\t//存在唯一分组\n\t\t\tif (groups != null && groups.length == 1) {\n\t\t\t\tgroup = groups[0];\n\t\t\t}\n\t\t\tserverAddress = createServerAddress(group);\n\t\t}\n\n\t\tfinal MongoCredential credentail = createCredentail(group);\n\t\ttry {\n\t\t\tMongoClientSettings.Builder clusterSettingsBuilder = MongoClientSettings.builder()\n\t\t\t\t\t.applyToClusterSettings(b -> b.hosts(Collections.singletonList(serverAddress)));\n\t\t\tbuildMongoClientSettings(clusterSettingsBuilder, group);\n\t\t\tif (null != credentail) {\n\t\t\t\tclusterSettingsBuilder.credential(credentail);\n\t\t\t}\n\t\t\tmongo = MongoClients.create(clusterSettingsBuilder.build());\n\t\t} catch (Exception e) {\n\t\t\tthrow new DbRuntimeException(StrUtil.format(\"Init MongoDB pool with connection to [{}] error!\", serverAddress), e);\n\t\t}\n\n\t\tlog.info(\"Init MongoDB pool with connection to [{}]\", serverAddress);\n\t}\n\n\t/**\n\t * 初始化集群<br>\n\t * 集群的其它客户端设定参数使用全局设定<br>\n\t * 集群中每一个实例成员用一个group表示，例如：\n\t *\n\t * <pre>\n\t * user = test1\n\t * pass = 123456\n\t * database = test\n\t * [db0]\n\t * host = 192.168.1.1:27117\n\t * [db1]\n\t * host = 192.168.1.1:27118\n\t * [db2]\n\t * host = 192.168.1.1:27119\n\t * </pre>\n\t */\n\tsynchronized public void initCloud() {\n\t\tif (groups == null || groups.length == 0) {\n\t\t\tthrow new DbRuntimeException(\"Please give replication set groups!\");\n\t\t}\n\n\t\tif (setting == null) {\n\t\t\t// 若未指定配置文件，则使用默认配置文件\n\t\t\tsetting = new Setting(MONGO_CONFIG_PATH, true);\n\t\t}\n\n\t\tfinal List<ServerAddress> addrList = new ArrayList<>();\n\t\tfor (String group : groups) {\n\t\t\taddrList.add(createServerAddress(group));\n\t\t}\n\n\t\tfinal MongoCredential credentail = createCredentail(StrUtil.EMPTY);\n\t\ttry {\n\t\t\tMongoClientSettings.Builder clusterSettingsBuilder = MongoClientSettings.builder()\n\t\t\t\t\t.applyToClusterSettings(b -> b.hosts(addrList));\n\t\t\tbuildMongoClientSettings(clusterSettingsBuilder, StrUtil.EMPTY);\n\t\t\tif (null != credentail) {\n\t\t\t\tclusterSettingsBuilder.credential(credentail);\n\t\t\t}\n\t\t\tmongo = MongoClients.create(clusterSettingsBuilder.build());\n\t\t} catch (Exception e) {\n\t\t\tlog.error(e, \"Init MongoDB connection error!\");\n\t\t\treturn;\n\t\t}\n\n\t\tlog.info(\"Init MongoDB cloud Set pool with connection to {}\", addrList);\n\t}\n\n\t/**\n\t * 设定MongoDB配置文件\n\t *\n\t * @param setting 配置文件\n\t */\n\tpublic void setSetting(Setting setting) {\n\t\tthis.setting = setting;\n\t}\n\n\t/**\n\t * @return 获得MongoDB客户端对象\n\t */\n\tpublic MongoClient getMongo() {\n\t\treturn mongo;\n\t}\n\n\t/**\n\t * 获得DB\n\t *\n\t * @param dbName DB\n\t * @return DB\n\t */\n\tpublic MongoDatabase getDb(String dbName) {\n\t\treturn mongo.getDatabase(dbName);\n\t}\n\n\t/**\n\t * 获得MongoDB中指定集合对象\n\t *\n\t * @param dbName         库名\n\t * @param collectionName 集合名\n\t * @return DBCollection\n\t */\n\tpublic MongoCollection<Document> getCollection(String dbName, String collectionName) {\n\t\treturn getDb(dbName).getCollection(collectionName);\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tmongo.close();\n\t}\n\n\t// --------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 创建ServerAddress对象，会读取配置文件中的相关信息\n\t *\n\t * @param group 分组，如果为{@code null}或者\"\"默认为无分组\n\t * @return ServerAddress\n\t */\n\tprivate ServerAddress createServerAddress(String group) {\n\t\tfinal Setting setting = checkSetting();\n\n\t\tif (group == null) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal String tmpHost = setting.getByGroup(\"host\", group);\n\t\tif (StrUtil.isBlank(tmpHost)) {\n\t\t\tthrow new NotInitedException(\"Host name is empy of group: {}\", group);\n\t\t}\n\n\t\tfinal int defaultPort = setting.getInt(\"port\", group, 27017);\n\t\treturn new ServerAddress(NetUtil.buildInetSocketAddress(tmpHost, defaultPort));\n\t}\n\n\t/**\n\t * 创建ServerAddress对象\n\t *\n\t * @param host 主机域名或者IP（如果为空默认127.0.0.1）\n\t * @param port 端口（如果为空默认为）\n\t * @return ServerAddress\n\t */\n\tprivate ServerAddress createServerAddress(String host, int port) {\n\t\treturn new ServerAddress(host, port);\n\t}\n\n\t/**\n\t * 创建{@link MongoCredential}，用于服务端验证<br>\n\t * 此方法会首先读取指定分组下的属性，用户没有定义则读取空分组下的属性\n\t *\n\t * @param group 分组\n\t * @return {@link MongoCredential}，如果用户未指定用户名密码返回null\n\t * @since 4.1.20\n\t */\n\tprivate MongoCredential createCredentail(String group) {\n\t\tfinal Setting setting = this.setting;\n\t\tif (null == setting) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal String user = setting.getStr(\"user\", group, setting.getStr(\"user\"));\n\t\tfinal String pass = setting.getStr(\"pass\", group, setting.getStr(\"pass\"));\n\t\tfinal String database = setting.getStr(\"database\", group, setting.getStr(\"database\"));\n\t\treturn createCredentail(user, database, pass);\n\t}\n\n\t/**\n\t * 创建{@link MongoCredential}，用于服务端验证\n\t *\n\t * @param userName 用户名\n\t * @param database 数据库名\n\t * @param password 密码\n\t * @return {@link MongoCredential}\n\t * @since 4.1.20\n\t */\n\tprivate MongoCredential createCredentail(String userName, String database, String password) {\n\t\tif (StrUtil.hasEmpty(userName, database, database)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn MongoCredential.createCredential(userName, database, password.toCharArray());\n\t}\n\n\t/**\n\t * 构件MongoDB连接选项<br>\n\t *\n\t * @param group 分组，当分组对应的选项不存在时会读取根选项，如果也不存在使用默认值\n\t * @return Builder\n\t */\n\tprivate MongoClientSettings.Builder buildMongoClientSettings(MongoClientSettings.Builder builder, String group) {\n\t\tif (setting == null) {\n\t\t\treturn builder;\n\t\t}\n\n\t\tif (StrUtil.isEmpty(group)) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t} else {\n\t\t\tgroup = group + StrUtil.DOT;\n\t\t}\n\n\t\t// 每个主机答应的连接数（每个主机的连接池大小），当连接池被用光时，会被阻塞住\n\t\tInteger connectionsPerHost = setting.getInt(group + \"connectionsPerHost\");\n\t\tif (StrUtil.isBlank(group) == false && connectionsPerHost == null) {\n\t\t\tconnectionsPerHost = setting.getInt(\"connectionsPerHost\");\n\t\t}\n\t\tConnectionPoolSettings.Builder connectionPoolSettingsBuilder = ConnectionPoolSettings.builder();\n\t\tif (connectionsPerHost != null) {\n\t\t\tconnectionPoolSettingsBuilder.maxSize(connectionsPerHost);\n\t\t\tlog.debug(\"MongoDB connectionsPerHost: {}\", connectionsPerHost);\n\t\t}\n\n\t\t// 被阻塞线程从连接池获取连接的最长等待时间（ms） --int\n\t\tInteger connectTimeout = setting.getInt(group + \"connectTimeout\");\n\t\tif (StrUtil.isBlank(group) == false && connectTimeout == null) {\n\t\t\tsetting.getInt(\"connectTimeout\");\n\t\t}\n\t\tif (connectTimeout != null) {\n\t\t\tconnectionPoolSettingsBuilder.maxWaitTime(connectTimeout, TimeUnit.MILLISECONDS);\n\t\t\tlog.debug(\"MongoDB connectTimeout: {}\", connectTimeout);\n\t\t}\n\t\tbuilder.applyToConnectionPoolSettings(b -> b.applySettings(connectionPoolSettingsBuilder.build()));\n\n\t\t// 套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0（无穷） --int\n\t\tInteger socketTimeout = setting.getInt(group + \"socketTimeout\");\n\t\tif (StrUtil.isBlank(group) == false && socketTimeout == null) {\n\t\t\tsetting.getInt(\"socketTimeout\");\n\t\t}\n\t\tif (socketTimeout != null) {\n\t\t\tSocketSettings socketSettings = SocketSettings.builder().connectTimeout(socketTimeout, TimeUnit.MILLISECONDS).build();\n\t\t\tbuilder.applyToSocketSettings(b -> b.applySettings(socketSettings));\n\t\t\tlog.debug(\"MongoDB socketTimeout: {}\", socketTimeout);\n\t\t}\n\n\t\treturn builder;\n\t}\n\n\t/**\n\t * 检查Setting配置文件\n\t *\n\t * @return Setting配置文件\n\t */\n\tprivate Setting checkSetting() {\n\t\tif (null == this.setting) {\n\t\t\tthrow new DbRuntimeException(\"Please indicate setting file or create default [{}]\", MONGO_CONFIG_PATH);\n\t\t}\n\t\treturn this.setting;\n\t}\n\t// --------------------------------------------------------------------------- Private method end\n\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java",
    "content": "package cn.hutool.db.nosql.mongo;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.RuntimeUtil;\nimport cn.hutool.setting.Setting;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * {@link MongoDS}工厂类，用于创建\n *\n * @author Looly, VampireAchao\n */\npublic class MongoFactory {\n\n\t/**\n\t * 各分组做组合key的时候分隔符\n\t */\n\tprivate final static String GROUP_SEPRATER = \",\";\n\n\t/**\n\t * 数据源池\n\t */\n\tprivate static final Map<String, MongoDS> DS_MAP = new SafeConcurrentHashMap<>();\n\n\t// JVM关闭前关闭MongoDB连接\n\tstatic {\n\t\tRuntimeUtil.addShutdownHook(MongoFactory::closeAll);\n\t}\n\n\t// ------------------------------------------------------------------------ Get DS start\n\n\t/**\n\t * 获取MongoDB数据源<br>\n\t *\n\t * @param host 主机\n\t * @param port 端口\n\t * @return MongoDB连接\n\t */\n\tpublic static MongoDS getDS(String host, int port) {\n\t\tfinal String key = host + \":\" + port;\n\t\treturn DS_MAP.computeIfAbsent(key, (k)->new MongoDS(host, port));\n\t}\n\n\t/**\n\t * 获取MongoDB数据源<br>\n\t * 多个分组名对应的连接组成集群\n\t *\n\t * @param groups 分组列表\n\t * @return MongoDB连接\n\t */\n\tpublic static MongoDS getDS(String... groups) {\n\t\tfinal String key = ArrayUtil.join(groups, GROUP_SEPRATER);\n\t\tMongoDS ds = DS_MAP.get(key);\n\t\tif (null == ds) {\n\t\t\t// 没有在池中加入之\n\t\t\tds = new MongoDS(groups);\n\t\t\tDS_MAP.put(key, ds);\n\t\t}\n\n\t\treturn ds;\n\t}\n\n\t/**\n\t * 获取MongoDB数据源<br>\n\t *\n\t * @param groups 分组列表\n\t * @return MongoDB连接\n\t */\n\tpublic static MongoDS getDS(Collection<String> groups) {\n\t\treturn getDS(groups.toArray(new String[0]));\n\t}\n\n\t/**\n\t * 获取MongoDB数据源<br>\n\t *\n\t * @param setting 设定文件\n\t * @param groups  分组列表\n\t * @return MongoDB连接\n\t */\n\tpublic static MongoDS getDS(Setting setting, String... groups) {\n\t\tfinal String key = setting.getSettingPath() + GROUP_SEPRATER + ArrayUtil.join(groups, GROUP_SEPRATER);\n\t\tMongoDS ds = DS_MAP.get(key);\n\t\tif (null == ds) {\n\t\t\t// 没有在池中加入之\n\t\t\tds = new MongoDS(setting, groups);\n\t\t\tDS_MAP.put(key, ds);\n\t\t}\n\n\t\treturn ds;\n\t}\n\n\t/**\n\t * 获取MongoDB数据源<br>\n\t *\n\t * @param setting 配置文件\n\t * @param groups  分组列表\n\t * @return MongoDB连接\n\t */\n\tpublic static MongoDS getDS(Setting setting, Collection<String> groups) {\n\t\treturn getDS(setting, groups.toArray(new String[0]));\n\t}\n\t// ------------------------------------------------------------------------ Get DS ends\n\n\t/**\n\t * 关闭全部连接\n\t */\n\tpublic static void closeAll() {\n\t\tif (MapUtil.isNotEmpty(DS_MAP)) {\n\t\t\tfor (MongoDS ds : DS_MAP.values()) {\n\t\t\t\tds.close();\n\t\t\t}\n\t\t\tDS_MAP.clear();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/mongo/package-info.java",
    "content": "/**\n * MongoDB数据库操作的封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.nosql.mongo;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/package-info.java",
    "content": "/**\n * NoSQL封装，包括Redis和MongoDB等数据库操作的封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.nosql;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/redis/RedisDS.java",
    "content": "package cn.hutool.db.nosql.redis;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.setting.Setting;\nimport redis.clients.jedis.Jedis;\nimport redis.clients.jedis.JedisPool;\nimport redis.clients.jedis.JedisPoolConfig;\nimport redis.clients.jedis.Protocol;\n\nimport java.io.Closeable;\nimport java.io.Serializable;\n\n/**\n * Jedis数据源\n *\n * @author looly\n * @since 3.2.3\n */\npublic class RedisDS implements Closeable, Serializable {\n\tprivate static final long serialVersionUID = -5605411972456177456L;\n\t/** 默认配置文件 */\n\tpublic final static String REDIS_CONFIG_PATH = \"config/redis.setting\";\n\n\t/** 配置文件 */\n\tprivate Setting setting;\n\t/** Jedis连接池 */\n\tprivate JedisPool pool;\n\tprivate String group;\n\n\t// --------------------------------------------------------------------------------- Static method start\n\t/**\n\t * 创建RedisDS，使用默认配置文件，默认分组\n\t *\n\t * @return RedisDS\n\t */\n\tpublic static RedisDS create() {\n\t\treturn new RedisDS();\n\t}\n\n\t/**\n\t * 创建RedisDS，使用默认配置文件\n\t *\n\t * @param group 配置文件中配置分组\n\t * @return RedisDS\n\t */\n\tpublic static RedisDS create(String group) {\n\t\treturn new RedisDS(group);\n\t}\n\n\t/**\n\t * 创建RedisDS\n\t *\n\t * @param setting 配置文件\n\t * @param group 配置文件中配置分组\n\t * @return RedisDS\n\t */\n\tpublic static RedisDS create(Setting setting, String group) {\n\t\treturn new RedisDS(setting, group);\n\t}\n\t// --------------------------------------------------------------------------------- Static method end\n\n\t/**\n\t * 构造，使用默认配置文件，默认分组\n\t */\n\tpublic RedisDS() {\n\t\tthis(null, null);\n\t}\n\n\t/**\n\t * 构造，使用默认配置文件\n\t *\n\t * @param group 配置文件中配置分组\n\t */\n\tpublic RedisDS(String group) {\n\t\tthis(null, group);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 配置文件\n\t * @param group 配置文件中配置分组\n\t */\n\tpublic RedisDS(Setting setting, String group) {\n\t\tthis.setting = setting;\n\t\tinit(group);\n\t}\n\n\t/**\n\t * 初始化Jedis客户端\n\t *\n\t * @param group Redis服务器信息分组\n\t * @return this\n\t */\n\tpublic RedisDS init(String group) {\n\t\tthis.group = group;\n\t\tif (null == setting) {\n\t\t\tsetting = new Setting(REDIS_CONFIG_PATH, true);\n\t\t}\n\n\t\tfinal JedisPoolConfig config = new JedisPoolConfig();\n\t\t// 共用配置\n\t\tsetting.toBean(config);\n\t\tif (StrUtil.isNotBlank(group)) {\n\t\t\t// 特有配置\n\t\t\tsetting.toBean(group, config);\n\t\t}\n\n\t\t//issue#I54TZ9\n\t\tfinal Long maxWaitMillis = setting.getLong(\"maxWaitMillis\");\n\t\tif(null != maxWaitMillis){\n\t\t\t//noinspection deprecation\n\t\t\tconfig.setMaxWaitMillis(maxWaitMillis);\n\t\t}\n\n\t\tthis.pool = new JedisPool(config,\n\t\t\t\t// 地址\n\t\t\t\tsetting.getStr(\"host\", group, Protocol.DEFAULT_HOST),\n\t\t\t\t// 端口\n\t\t\t\tsetting.getInt(\"port\", group, Protocol.DEFAULT_PORT),\n\t\t\t\t// 连接超时\n\t\t\t\tsetting.getInt(\"connectionTimeout\", group, setting.getInt(\"timeout\", group, Protocol.DEFAULT_TIMEOUT)),\n\t\t\t\t// 读取数据超时\n\t\t\t\tsetting.getInt(\"soTimeout\", group, setting.getInt(\"timeout\", group, Protocol.DEFAULT_TIMEOUT)),\n\t\t\t\t// 用户名，issue#I8XEQ4\n\t\t\t\tsetting.getStr(\"user\", group, null),\n\t\t\t\t// 密码\n\t\t\t\tsetting.getStr(\"password\", group, null),\n\t\t\t\t// 数据库序号\n\t\t\t\tsetting.getInt(\"database\", group, Protocol.DEFAULT_DATABASE),\n\t\t\t\t// 客户端名\n\t\t\t\tsetting.getStr(\"clientName\", group, \"Hutool\"),\n\t\t\t\t// 是否使用SSL\n\t\t\t\tsetting.getBool(\"ssl\", group, false),\n\t\t\t\t// SSL相关，使用默认\n\t\t\t\tnull, null, null);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取Jedis连接池\n\t *\n\t * @return Jedis连接池\n\t * @since 5.8.41\n\t */\n\tpublic JedisPool getPool() {\n\t\treturn pool;\n\t}\n\n\t/**\n\t * 获取配置\n\t *\n\t * @return 配置\n\t * @since 5.8.41\n\t */\n\tpublic Setting getSetting() {\n\t\treturn setting;\n\t}\n\n\t/**\n\t * 获取分组\n\t *\n\t * @return 分组\n\t * @since 5.8.41\n\t */\n\tpublic String getGroup() {\n\t\treturn group;\n\t}\n\n\t/**\n\t * 从资源池中获取{@link Jedis}\n\t *\n\t * @return {@link Jedis}\n\t */\n\tpublic Jedis getJedis() {\n\t\treturn this.pool.getResource();\n\t}\n\n\t/**\n\t * 从Redis中获取值\n\t *\n\t * @param key 键\n\t * @return 值\n\t */\n\tpublic String getStr(String key) {\n\t\ttry (Jedis jedis = getJedis()) {\n\t\t\treturn jedis.get(key);\n\t\t}\n\t}\n\n\t/**\n\t * 从Redis中设置值\n\t *\n\t * @param key 键\n\t * @param value 值\n\t * @return 状态码\n\t */\n\tpublic String setStr(String key, String value) {\n\t\ttry (Jedis jedis = getJedis()) {\n\t\t\treturn jedis.set(key, value);\n\t\t}\n\t}\n\n\t/**\n\t * 从Redis中删除多个值\n\t *\n\t * @param keys 需要删除值对应的键列表\n\t * @return 删除个数，0表示无key可删除\n\t */\n\tpublic Long del(String... keys) {\n\t\ttry (Jedis jedis = getJedis()) {\n\t\t\treturn jedis.del(keys);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(pool);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/nosql/redis/package-info.java",
    "content": "/**\n * Redis（Jedis）数据库操作的封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.nosql.redis;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/package-info.java",
    "content": "/**\n * Hutool-db是一个在JDBC基础上封装的数据库操作工具类，通过包装，使用ActiveRecord思想操作数据库。<br>\n * 在Hutool-db中，使用Entity（本质上是个Map）代替Bean来使数据库操作更加灵活，同时提供Bean和Entity的转换提供传统ORM的兼容支持。\n * \n * @author looly\n *\n */\npackage cn.hutool.db;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/Condition.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.clone.CloneSupport;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.text.StrSplitter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * 条件对象<br>\n *\n * @author Looly\n */\npublic class Condition extends CloneSupport<Condition> {\n\n\t/**\n\t * SQL中 LIKE 语句查询方式<br>\n\t *\n\t * @author Looly\n\t */\n\tpublic enum LikeType {\n\t\t/**\n\t\t * 以给定值开头，拼接后的SQL \"value%\"\n\t\t */\n\t\tStartWith,\n\t\t/**\n\t\t * 以给定值开头，拼接后的SQL \"%value\"\n\t\t */\n\t\tEndWith,\n\t\t/**\n\t\t * 包含给定值，拼接后的SQL \"%value%\"\n\t\t */\n\t\tContains\n\t}\n\n\tprivate static final String OPERATOR_LIKE = \"LIKE\";\n\tprivate static final String OPERATOR_IN = \"IN\";\n\tprivate static final String OPERATOR_IS = \"IS\";\n\tprivate static final String OPERATOR_IS_NOT = \"IS NOT\";\n\tprivate static final String OPERATOR_BETWEEN = \"BETWEEN\";\n\tprivate static final List<String> OPERATORS = Arrays.asList(\"<>\", \"<=\", \"<\", \">=\", \">\", \"=\", \"!=\", OPERATOR_IN);\n\n\tprivate static final String VALUE_NULL = \"NULL\";\n\n\t/**\n\t * 字段\n\t */\n\tprivate String field;\n\t/**\n\t * 运算符（大于号，小于号，等于号 like 等）\n\t */\n\tprivate String operator;\n\t/**\n\t * 值\n\t */\n\tprivate Object value;\n\t/**\n\t * 是否使用条件值占位符\n\t */\n\tprivate boolean isPlaceHolder = true;\n\t/**\n\t * between firstValue and secondValue\n\t */\n\tprivate Object secondValue;\n\n\t/**\n\t * 与前一个Condition连接的逻辑运算符，可以是and或or\n\t */\n\tprivate LogicalOperator linkOperator = LogicalOperator.AND;\n\n\t/**\n\t * 解析为Condition\n\t *\n\t * @param field      字段名\n\t * @param expression 表达式或普通值\n\t * @return Condition\n\t */\n\tpublic static Condition parse(String field, Object expression) {\n\t\treturn new Condition(field, expression);\n\t}\n\n\t// --------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t */\n\tpublic Condition() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param isPlaceHolder 是否使用条件值占位符\n\t */\n\tpublic Condition(boolean isPlaceHolder) {\n\t\tthis.isPlaceHolder = isPlaceHolder;\n\t}\n\n\t/**\n\t * 构造，使用等于表达式（运算符是=）\n\t *\n\t * @param field 字段\n\t * @param value 值\n\t */\n\tpublic Condition(String field, Object value) {\n\t\tthis(field, \"=\", value);\n\t\tparseValue();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param field    字段\n\t * @param operator 运算符（大于号，小于号，等于号 like 等）\n\t * @param value    值\n\t */\n\tpublic Condition(String field, String operator, Object value) {\n\t\tthis.field = field;\n\t\tthis.operator = operator;\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 构造LIKE value条件，支持：\n\t * <ul>\n\t *     <li>{@link LikeType#StartWith}：LIKE value%</li>\n\t *     <li>{@link LikeType#EndWith}：LIKE %value</li>\n\t *     <li>{@link LikeType#Contains}：LIKE %value%</li>\n\t * </ul>\n\t *\n\t * @param field    字段\n\t * @param value    值\n\t * @param likeType {@link LikeType}\n\t */\n\tpublic Condition(String field, String value, LikeType likeType) {\n\t\tthis.field = field;\n\t\tthis.operator = OPERATOR_LIKE;\n\t\tthis.value = SqlUtil.buildLikeValue(value, likeType, false);\n\t}\n\n\t/**\n\t * 构造BETWEEN leftValue and rightValue条件\n\t *\n\t * @param field    字段\n\t * @param leftValue  左值\n\t * @param rightValue 右值\n\t * @since 5.8.41\n\t */\n\tpublic Condition(final String field, final Object leftValue, final Object rightValue){\n\t\tthis.field = field;\n\t\tthis.operator = OPERATOR_BETWEEN;\n\t\tthis.value = leftValue;\n\t\tthis.secondValue = rightValue;\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * @return 字段\n\t */\n\tpublic String getField() {\n\t\treturn field;\n\t}\n\n\t/**\n\t * 设置字段名\n\t *\n\t * @param field 字段名\n\t */\n\tpublic void setField(String field) {\n\t\tthis.field = field;\n\t}\n\n\t/**\n\t * 获得运算符<br>\n\t * 大于号，小于号，等于号 等\n\t *\n\t * @return 运算符\n\t */\n\tpublic String getOperator() {\n\t\treturn operator;\n\t}\n\n\t/**\n\t * 设置运算符<br>\n\t * 大于号，小于号，等于号 等\n\t *\n\t * @param operator 运算符\n\t */\n\tpublic void setOperator(String operator) {\n\t\tthis.operator = operator;\n\t}\n\n\t/**\n\t * 获得值\n\t *\n\t * @return 值\n\t */\n\tpublic Object getValue() {\n\t\treturn value;\n\t}\n\n\t/**\n\t * 设置值，不解析表达式\n\t *\n\t * @param value 值\n\t */\n\tpublic void setValue(Object value) {\n\t\tsetValue(value, false);\n\t}\n\n\t/**\n\t * 设置值\n\t *\n\t * @param value   值\n\t * @param isParse 是否解析值表达式\n\t */\n\tpublic void setValue(Object value, boolean isParse) {\n\t\tthis.value = value;\n\t\tif (isParse) {\n\t\t\tparseValue();\n\t\t}\n\t}\n\n\t/**\n\t * 是否使用条件占位符\n\t *\n\t * @return 是否使用条件占位符\n\t */\n\tpublic boolean isPlaceHolder() {\n\t\treturn isPlaceHolder;\n\t}\n\n\t/**\n\t * 设置是否使用条件占位符\n\t *\n\t * @param isPlaceHolder 是否使用条件占位符\n\t */\n\tpublic void setPlaceHolder(boolean isPlaceHolder) {\n\t\tthis.isPlaceHolder = isPlaceHolder;\n\t}\n\n\t/**\n\t * 是否 between x and y 类型\n\t *\n\t * @return 是否 between x and y 类型\n\t * @since 4.0.1\n\t */\n\tpublic boolean isOperatorBetween() {\n\t\treturn OPERATOR_BETWEEN.equalsIgnoreCase(this.operator);\n\t}\n\n\t/**\n\t * 是否IN条件\n\t *\n\t * @return 是否IN条件\n\t * @since 4.0.1\n\t */\n\tpublic boolean isOperatorIn() {\n\t\treturn OPERATOR_IN.equalsIgnoreCase(this.operator);\n\t}\n\n\t/**\n\t * 是否IS条件\n\t *\n\t * @return 是否IS条件\n\t * @since 4.0.1\n\t */\n\tpublic boolean isOperatorIs() {\n\t\treturn OPERATOR_IS.equalsIgnoreCase(this.operator);\n\t}\n\n\t/**\n\t * 是否 IS NOT条件\n\t *\n\t * @return 是否IS NOT条件\n\t * @since 5.8.0\n\t */\n\tpublic boolean isOperatorIsNot() {\n\t\treturn OPERATOR_IS_NOT.equalsIgnoreCase(this.operator);\n\t}\n\n\n\t/**\n\t * 是否LIKE条件\n\t *\n\t * @return 是否LIKE条件\n\t * @since 5.7.14\n\t */\n\tpublic boolean isOperatorLike() {\n\t\treturn OPERATOR_LIKE.equalsIgnoreCase(this.operator);\n\t}\n\n\t/**\n\t * 检查值是否为null，如果为null转换为 \"IS NULL\"或\"IS NOT NULL\"形式\n\t *\n\t * @return this\n\t */\n\tpublic Condition checkValueNull() {\n\t\tif (null == this.value) {\n\t\t\t// 检查是否为\"不等于\"操作符（包括!=和<>）\n\t\t\tif (\"!=\".equalsIgnoreCase(this.operator) || \"<>\".equalsIgnoreCase(this.operator)) {\n\t\t\t\tthis.operator = OPERATOR_IS_NOT;\n\t\t\t} else {\n\t\t\t\tthis.operator = OPERATOR_IS;\n\t\t\t}\n\t\t\tthis.value = VALUE_NULL;\n\t\t}\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 获得between 类型中第二个值\n\t *\n\t * @return 值\n\t */\n\tpublic Object getSecondValue() {\n\t\treturn secondValue;\n\t}\n\n\t/**\n\t * 设置between 类型中第二个值\n\t *\n\t * @param secondValue 第二个值\n\t */\n\tpublic void setSecondValue(Object secondValue) {\n\t\tthis.secondValue = secondValue;\n\t}\n\n\t/**\n\t * 获取与前一个Condition连接的逻辑运算符，可以是and或or\n\t *\n\t * @return 与前一个Condition连接的逻辑运算符，可以是and或or\n\t * @since 5.4.3\n\t */\n\tpublic LogicalOperator getLinkOperator() {\n\t\treturn linkOperator;\n\t}\n\n\t/**\n\t * 设置与前一个Condition连接的逻辑运算符，可以是and或or\n\t *\n\t * @param linkOperator 与前一个Condition连接的逻辑运算符，可以是and或or\n\t * @since 5.4.3\n\t */\n\tpublic void setLinkOperator(LogicalOperator linkOperator) {\n\t\tthis.linkOperator = linkOperator;\n\t}\n\n\t// --------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic String toString() {\n\t\treturn toString(null);\n\t}\n\n\t/**\n\t * 转换为条件字符串，并回填占位符对应的参数值\n\t *\n\t * @param paramValues 参数列表，用于回填占位符对应参数值\n\t * @return 条件字符串\n\t */\n\tpublic String toString(List<Object> paramValues) {\n\t\tfinal StringBuilder conditionStrBuilder = StrUtil.builder();\n\t\t// 判空值\n\t\tcheckValueNull();\n\n\t\t// 固定前置，例如：\"name =\"、\"name IN\"、\"name BETWEEN\"、\"name LIKE\"\n\t\tconditionStrBuilder.append(this.field).append(StrUtil.SPACE).append(this.operator);\n\n\t\tif (isOperatorBetween()) {\n\t\t\tbuildValuePartForBETWEEN(conditionStrBuilder, paramValues);\n\t\t} else if (isOperatorIn()) {\n\t\t\t// 类似：\" (?,?,?)\" 或者 \" (1,2,3,4)\"\n\t\t\tbuildValuePartForIN(conditionStrBuilder, paramValues);\n\t\t} else {\n\t\t\tif (isPlaceHolder() && !isOperatorIs() && !isOperatorIsNot()) {\n\t\t\t\t// 使用条件表达式占位符，条件表达式并不适用于 IS NULL 和 IS NOT NULL\n\t\t\t\tconditionStrBuilder.append(\" ?\");\n\t\t\t\tif (null != paramValues) {\n\t\t\t\t\tparamValues.add(this.value);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 直接使用条件值\n\t\t\t\tfinal String valueStr = String.valueOf(this.value);\n\t\t\t\tconditionStrBuilder.append(\" \").append(isOperatorLike() ?\n\t\t\t\t\tStrUtil.wrap(valueStr, \"'\") : valueStr);\n\t\t\t}\n\t\t}\n\n\t\treturn conditionStrBuilder.toString();\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 构建BETWEEN语句中的值部分<br>\n\t * 开头必须加空格，类似：\" ? AND ?\" 或者 \" 1 AND 2\"\n\t *\n\t * @param conditionStrBuilder 条件语句构建器\n\t * @param paramValues         参数集合，用于参数占位符对应参数回填\n\t */\n\tprivate void buildValuePartForBETWEEN(StringBuilder conditionStrBuilder, List<Object> paramValues) {\n\t\t// BETWEEN x AND y 的情况，两个参数\n\t\tif (isPlaceHolder()) {\n\t\t\t// 使用条件表达式占位符\n\t\t\tconditionStrBuilder.append(\" ?\");\n\t\t\tif (null != paramValues) {\n\t\t\t\tparamValues.add(this.value);\n\t\t\t}\n\t\t} else {\n\t\t\t// 直接使用条件值\n\t\t\tconditionStrBuilder.append(CharUtil.SPACE).append(this.value);\n\t\t}\n\n\t\t// 处理 AND y\n\t\tconditionStrBuilder.append(StrUtil.SPACE).append(LogicalOperator.AND);\n\t\tif (isPlaceHolder()) {\n\t\t\t// 使用条件表达式占位符\n\t\t\tconditionStrBuilder.append(\" ?\");\n\t\t\tif (null != paramValues) {\n\t\t\t\tparamValues.add(this.secondValue);\n\t\t\t}\n\t\t} else {\n\t\t\t// 直接使用条件值\n\t\t\tconditionStrBuilder.append(CharUtil.SPACE).append(this.secondValue);\n\t\t}\n\t}\n\n\t/**\n\t * 构建IN语句中的值部分<br>\n\t * 开头必须加空格，类似：\" (?,?,?)\" 或者 \" (1,2,3,4)\"\n\t *\n\t * @param conditionStrBuilder 条件语句构建器\n\t * @param paramValues         参数集合，用于参数占位符对应参数回填\n\t */\n\tprivate void buildValuePartForIN(StringBuilder conditionStrBuilder, List<Object> paramValues) {\n\t\tconditionStrBuilder.append(\" (\");\n\t\tfinal Object value = this.value;\n\t\tif (isPlaceHolder()) {\n\t\t\tCollection<?> valuesForIn;\n\t\t\t// 占位符对应值列表\n\t\t\tif (value instanceof Collection) {\n\t\t\t\t// pr#2046@Github\n\t\t\t\tvaluesForIn = (Collection<?>) value;\n\t\t\t} else if (value instanceof CharSequence) {\n\t\t\t\tvaluesForIn = StrUtil.split((CharSequence) value, ',');\n\t\t\t} else {\n\t\t\t\tvaluesForIn = Arrays.asList(Convert.convert(Object[].class, value));\n\t\t\t}\n\t\t\tconditionStrBuilder.append(StrUtil.repeatAndJoin(\"?\", valuesForIn.size(), \",\"));\n\t\t\tif (null != paramValues) {\n\t\t\t\tparamValues.addAll(valuesForIn);\n\t\t\t}\n\t\t} else {\n\t\t\tconditionStrBuilder.append(StrUtil.join(\",\", value));\n\t\t}\n\t\tconditionStrBuilder.append(')');\n\t}\n\n\t/**\n\t * 解析值表达式<br>\n\t * 支持\"<>\", \"<=\", \"< \", \">=\", \"> \", \"= \", \"!=\", \"IN\", \"LIKE\", \"IS\", \"IS NOT\"表达式<br>\n\t * 如果无法识别表达式，则表达式为\"=\"，表达式与值用空格隔开<br>\n\t * 例如字段为name，那value可以为：\"> 1\"或者 \"LIKE %Tom\"此类\n\t */\n\tprivate void parseValue() {\n\t\t// 当值无时，视为空判定\n\t\tif (null == this.value) {\n\t\t\tthis.operator = OPERATOR_IS;\n\t\t\tthis.value = VALUE_NULL;\n\t\t\treturn;\n\t\t}\n\n\t\t// 对数组和集合值按照 IN 处理\n\t\tif (this.value instanceof Collection || ArrayUtil.isArray(this.value)) {\n\t\t\tthis.operator = OPERATOR_IN;\n\t\t\treturn;\n\t\t}\n\n\t\t// 其他类型值，跳过\n\t\tif (false == (this.value instanceof String)) {\n\t\t\treturn;\n\t\t}\n\n\t\tString valueStr = ((String) value);\n\t\tif (StrUtil.isBlank(valueStr)) {\n\t\t\t// 空字段不做处理\n\t\t\treturn;\n\t\t}\n\n\t\tvalueStr = StrUtil.trim(valueStr);\n\n\t\t// 处理null\n\t\tif (StrUtil.endWithIgnoreCase(valueStr, \"null\")) {\n\t\t\tif (StrUtil.equalsIgnoreCase(\"= null\", valueStr) || StrUtil.equalsIgnoreCase(\"is null\", valueStr)) {\n\t\t\t\t// 处理\"= null\"和\"is null\"转换为\"IS NULL\"\n\t\t\t\tthis.operator = OPERATOR_IS;\n\t\t\t\tthis.value = VALUE_NULL;\n\t\t\t\tthis.isPlaceHolder = false;\n\t\t\t\treturn;\n\t\t\t} else if (StrUtil.equalsIgnoreCase(\"!= null\", valueStr) || StrUtil.equalsIgnoreCase(\"is not null\", valueStr)) {\n\t\t\t\t// 处理\"!= null\"和\"is not null\"转换为\"IS NOT NULL\"\n\t\t\t\tthis.operator = OPERATOR_IS_NOT;\n\t\t\t\tthis.value = VALUE_NULL;\n\t\t\t\tthis.isPlaceHolder = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tfinal List<String> strs = StrUtil.split(valueStr, StrUtil.C_SPACE, 2);\n\t\tif (strs.size() < 2) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 处理常用符号和IN\n\t\tfinal String firstPart = strs.get(0).trim().toUpperCase();\n\t\tif (OPERATORS.contains(firstPart)) {\n\t\t\tthis.operator = firstPart;\n\t\t\t// 比较符号后跟大部分为数字，此处做转换（IN不做转换）\n\t\t\tfinal String valuePart = strs.get(1);\n\t\t\tthis.value = isOperatorIn() ? valuePart : tryToNumber(valuePart);\n\t\t\treturn;\n\t\t}\n\n\t\t// 处理LIKE\n\t\tif (OPERATOR_LIKE.equals(firstPart)) {\n\t\t\tthis.operator = OPERATOR_LIKE;\n\t\t\tthis.value = unwrapQuote(strs.get(1));\n\t\t\treturn;\n\t\t}\n\n\t\t// 处理BETWEEN x AND y\n\t\tif (OPERATOR_BETWEEN.equals(firstPart)) {\n\t\t\tfinal List<String> betweenValueStrs = StrSplitter.splitTrimIgnoreCase(strs.get(1), LogicalOperator.AND.toString(), 2, true);\n\t\t\tif (betweenValueStrs.size() < 2) {\n\t\t\t\t// 必须满足a AND b格式，不满足被当作普通值\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.operator = OPERATOR_BETWEEN;\n\t\t\tthis.value = unwrapQuote(betweenValueStrs.get(0));\n\t\t\tthis.secondValue = unwrapQuote(betweenValueStrs.get(1));\n\t\t}\n\t}\n\n\t/**\n\t * 去掉包围在字符串两端的单引号或双引号\n\t *\n\t * @param value 值\n\t * @return 去掉引号后的值\n\t */\n\tprivate static String unwrapQuote(String value) {\n\t\tif (null == value) {\n\t\t\treturn null;\n\t\t}\n\t\tvalue = value.trim();\n\n\t\tint from = 0;\n\t\tint to = value.length();\n\t\tchar startChar = value.charAt(0);\n\t\tchar endChar = value.charAt(value.length() - 1);\n\t\tif (startChar == endChar) {\n\t\t\tif ('\\'' == startChar || '\"' == startChar) {\n\t\t\t\tfrom = 1;\n\t\t\t\tto--;\n\t\t\t}\n\t\t}\n\n\t\tif (from == 0) {\n\t\t\t// 并不包含，返回原值\n\t\t\treturn value;\n\t\t}\n\t\treturn value.substring(from, to);\n\t}\n\n\t/**\n\t * 尝试转换为数字，转换失败返回字符串\n\t *\n\t * @param value 被转换的字符串值\n\t * @return 转换后的值\n\t */\n\tprivate static Object tryToNumber(String value) {\n\t\tvalue = StrUtil.trim(value);\n\t\tif (false == NumberUtil.isNumber(value)) {\n\t\t\treturn value;\n\t\t}\n\t\ttry {\n\t\t\treturn NumberUtil.parseNumber(value);\n\t\t} catch (Exception ignore) {\n\t\t\treturn value;\n\t\t}\n\t}\n\t// ----------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/ConditionBuilder.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 多条件构建封装<br>\n * 可以将多个条件构建为SQL语句的一部分，并将参数值转换为占位符，并提取对应位置的参数值。<br>\n * 例如：name = ? AND type IN (?, ?) AND other LIKE ?\n *\n * @author looly\n * @since 5.4.3\n */\npublic class ConditionBuilder implements Builder<String> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 创建构建器\n\t *\n\t * @param conditions 条件列表\n\t * @return ConditionBuilder\n\t */\n\tpublic static ConditionBuilder of(Condition... conditions) {\n\t\treturn new ConditionBuilder(conditions);\n\t}\n\n\t/**\n\t * 条件数组\n\t */\n\tprivate final Condition[] conditions;\n\t/**\n\t * 占位符对应的值列表\n\t */\n\tprivate List<Object> paramValues;\n\n\t/**\n\t * 构造\n\t *\n\t * @param conditions 条件列表\n\t */\n\tpublic ConditionBuilder(Condition... conditions) {\n\t\tthis.conditions = conditions;\n\t}\n\n\t/**\n\t * 返回构建后的参数列表<br>\n\t * 此方法调用前必须调用{@link #build()}\n\t *\n\t * @return 参数列表\n\t */\n\tpublic List<Object> getParamValues() {\n\t\treturn ListUtil.unmodifiable(this.paramValues);\n\t}\n\n\t/**\n\t * 构建组合条件<br>\n\t * 例如：name = ? AND type IN (?, ?) AND other LIKE ?\n\t *\n\t * @return 构建后的SQL语句条件部分\n\t */\n\t@Override\n\tpublic String build() {\n\t\tif(null == this.paramValues){\n\t\t\tthis.paramValues = new ArrayList<>();\n\t\t} else {\n\t\t\tthis.paramValues.clear();\n\t\t}\n\t\treturn build(this.paramValues);\n\t}\n\n\t/**\n\t * 构建组合条件<br>\n\t * 例如：name = ? AND type IN (?, ?) AND other LIKE ?\n\t *\n\t * @param paramValues 用于写出参数的List,构建时会将参数写入此List\n\t * @return 构建后的SQL语句条件部分\n\t */\n\tpublic String build(List<Object> paramValues) {\n\t\tif (ArrayUtil.isEmpty(conditions)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal StringBuilder conditionStrBuilder = new StringBuilder();\n\t\tboolean isFirst = true;\n\t\tfor (Condition condition : conditions) {\n\t\t\t// 添加逻辑运算符\n\t\t\tif (isFirst) {\n\t\t\t\tisFirst = false;\n\t\t\t} else {\n\t\t\t\t// \" AND \" 或者 \" OR \"\n\t\t\t\tconditionStrBuilder.append(CharUtil.SPACE).append(condition.getLinkOperator()).append(CharUtil.SPACE);\n\t\t\t}\n\n\t\t\t// 构建条件部分：\"name = ?\"、\"name IN (?,?,?)\"、\"name BETWEEN ？AND ？\"、\"name LIKE ?\"\n\t\t\tconditionStrBuilder.append(condition.toString(paramValues));\n\t\t}\n\t\treturn conditionStrBuilder.toString();\n\t}\n\n\t/**\n\t * 构建组合条件<br>\n\t * 例如：name = ? AND type IN (?, ?) AND other LIKE ?\n\t *\n\t * @return 构建后的SQL语句条件部分\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn build();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/ConditionGroup.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.List;\n\n/**\n * 条件组<br>\n * 用于构建复杂where条件\n *\n * @author tjh\n * @since 5.7.21\n */\npublic class ConditionGroup extends Condition {\n\t/**\n\t * 条件列表\n\t */\n\tprivate Condition[] conditions;\n\n\t/**\n\t * 追加条件\n\t *\n\t * @param conditions 条件列表\n\t */\n\tpublic void addConditions(Condition... conditions) {\n\t\tif (null == this.conditions) {\n\t\t\tthis.conditions = conditions;\n\t\t} else {\n\t\t\tthis.conditions = ArrayUtil.addAll(this.conditions, conditions);\n\t\t}\n\t}\n\n\t/**\n\t * 将条件组转换为条件字符串，使用括号包裹，并回填占位符对应的参数值\n\t *\n\t * @param paramValues 参数列表，用于回填占位符对应参数值\n\t * @return 条件字符串\n\t */\n\t@Override\n\tpublic String toString(List<Object> paramValues) {\n\t\tif (ArrayUtil.isEmpty(conditions)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal StringBuilder conditionStrBuilder = StrUtil.builder();\n\t\tconditionStrBuilder.append(\"(\");\n\t\t// 将组内的条件构造为SQL，因为toString，会进行递归，处理所有的条件组\n\t\tconditionStrBuilder.append(ConditionBuilder.of(this.conditions).build(paramValues));\n\t\tconditionStrBuilder.append(\")\");\n\n\t\treturn conditionStrBuilder.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/Direction.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 排序方式（升序或者降序）\n *\n * @author Looly\n */\npublic enum Direction {\n\t/**\n\t * 升序\n\t */\n\tASC,\n\t/**\n\t * 降序\n\t */\n\tDESC;\n\n\t/**\n\t * 根据字符串值返回对应Direction值\n\t *\n\t * @param value 排序方式字符串，只能是 ASC或DESC\n\t * @return Direction，{@code null}表示提供的value为空\n\t * @throws IllegalArgumentException in case the given value cannot be parsed into an enum value.\n\t */\n\tpublic static Direction fromString(String value) throws IllegalArgumentException {\n\t\tif (StrUtil.isEmpty(value)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 兼容元数据中ASC和DESC表示\n\t\tif (1 == value.length()) {\n\t\t\tif (\"A\".equalsIgnoreCase(value)) {\n\t\t\t\treturn ASC;\n\t\t\t} else if (\"D\".equalsIgnoreCase(value)) {\n\t\t\t\treturn DESC;\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\treturn Direction.valueOf(value.toUpperCase());\n\t\t} catch (Exception e) {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\n\t\t\t\t\t\"Invalid value [{}] for orders given! Has to be either 'desc' or 'asc' (case insensitive).\", value), e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/LogicalOperator.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 逻辑运算符\n * @author Looly\n *\n */\npublic enum LogicalOperator{\n\t/** 且，两个条件都满足 */\n\tAND,\n\t/** 或，满足多个条件的一个即可 */\n\tOR;\n\t\n\t/**\n\t * 给定字符串逻辑运算符是否与当前逻辑运算符一致，不区分大小写，自动去除两边空白符\n\t * \n\t * @param logicalOperatorStr 逻辑运算符字符串\n\t * @return 是否与当前逻辑运算符一致\n\t * @since 3.2.1\n\t */\n\tpublic boolean isSame(String logicalOperatorStr) {\n\t\tif(StrUtil.isBlank(logicalOperatorStr)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn this.name().equalsIgnoreCase(logicalOperatorStr.trim());\n\t}\n}"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/NamedSql.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 使用命名占位符的SQL，例如：select * from table where field1=:name1<br>\n * 支持的占位符格式为：\n * <pre>\n * 1、:name\n * 2、@name\n * 3、?name\n * </pre>\n *\n * @author looly\n * @since 4.0.10\n */\npublic class NamedSql {\n\n\tprivate static final char[] NAME_START_CHARS = {':', '@', '?'};\n\n\tprivate String sql;\n\tprivate final List<Object> params;\n\n\t/**\n\t * 构造\n\t *\n\t * @param namedSql 命名占位符的SQL\n\t * @param paramMap 名和参数的对应Map\n\t */\n\tpublic NamedSql(String namedSql, Map<String, Object> paramMap) {\n\t\tthis.params = new LinkedList<>();\n\t\tparse(namedSql, paramMap);\n\t}\n\n\t/**\n\t * 获取SQL\n\t *\n\t * @return SQL\n\t */\n\tpublic String getSql() {\n\t\treturn this.sql;\n\t}\n\n\t/**\n\t * 获取参数列表，按照占位符顺序\n\t *\n\t * @return 参数数组\n\t */\n\tpublic Object[] getParams() {\n\t\treturn this.params.toArray(new Object[0]);\n\t}\n\n\t/**\n\t * 获取参数列表，按照占位符顺序\n\t *\n\t * @return 参数列表\n\t */\n\tpublic List<Object> getParamList() {\n\t\treturn this.params;\n\t}\n\n\t/**\n\t * 解析命名占位符的SQL\n\t *\n\t * @param namedSql 命名占位符的SQL\n\t * @param paramMap 名和参数的对应Map\n\t */\n\tprivate void parse(String namedSql, Map<String, Object> paramMap) {\n\t\tif(MapUtil.isEmpty(paramMap)){\n\t\t\tthis.sql = namedSql;\n\t\t\treturn;\n\t\t}\n\n\t\tfinal int len = namedSql.length();\n\n\t\tfinal StrBuilder name = StrUtil.strBuilder();\n\t\tfinal StrBuilder sqlBuilder = StrUtil.strBuilder();\n\t\tchar c;\n\t\tCharacter nameStartChar = null;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = namedSql.charAt(i);\n\t\t\tif (ArrayUtil.contains(NAME_START_CHARS, c)) {\n\t\t\t\t// 新的变量开始符出现，要处理之前的变量\n\t\t\t\treplaceVar(nameStartChar, name, sqlBuilder, paramMap);\n\t\t\t\tnameStartChar = c;\n\t\t\t} else if (null != nameStartChar) {\n\t\t\t\t// 变量状态\n\t\t\t\tif (isGenerateChar(c)) {\n\t\t\t\t\t// 变量名\n\t\t\t\t\tname.append(c);\n\t\t\t\t} else {\n\t\t\t\t\t// 非标准字符也非变量开始的字符出现表示变量名结束，开始替换\n\t\t\t\t\treplaceVar(nameStartChar, name, sqlBuilder, paramMap);\n\t\t\t\t\tnameStartChar = null;\n\t\t\t\t\tsqlBuilder.append(c);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 变量以外的字符原样输出\n\t\t\t\tsqlBuilder.append(c);\n\t\t\t}\n\t\t}\n\n\t\t// 收尾，如果SQL末尾存在变量，处理之\n\t\tif (false == name.isEmpty()) {\n\t\t\treplaceVar(nameStartChar, name, sqlBuilder, paramMap);\n\t\t}\n\n\t\tthis.sql = sqlBuilder.toString();\n\t}\n\n\t/**\n\t * 替换变量，如果无变量，原样输出到SQL中去\n\t *\n\t * @param nameStartChar 变量开始字符\n\t * @param name 变量名\n\t * @param sqlBuilder 结果SQL缓存\n\t * @param paramMap 变量map（非空）\n\t */\n\tprivate void replaceVar(Character nameStartChar, StrBuilder name, StrBuilder sqlBuilder, Map<String, Object> paramMap){\n\t\tif(name.isEmpty()){\n\t\t\tif(null != nameStartChar){\n\t\t\t\t// 类似于:的情况，需要补上:\n\t\t\t\tsqlBuilder.append(nameStartChar);\n\t\t\t}\n\t\t\t// 无变量，按照普通字符处理\n\t\t\treturn;\n\t\t}\n\n\t\t// 变量结束\n\t\tfinal String nameStr = name.toString();\n\t\tif(paramMap.containsKey(nameStr)) {\n\t\t\t// 有变量对应值（值可以为null），替换占位符为?，变量值放入相应index位置\n\t\t\tfinal Object paramValue = paramMap.get(nameStr);\n\t\t\tif(ArrayUtil.isArray(paramValue) && SqlUtil.isInClause(sqlBuilder)){\n\t\t\t\t// 可能为select in (xxx)语句，则拆分参数为多个参数，变成in (?,?,?)\n\t\t\t\tfinal int length = ArrayUtil.length(paramValue);\n\t\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\t\tif(0 != i){\n\t\t\t\t\t\tsqlBuilder.append(',');\n\t\t\t\t\t}\n\t\t\t\t\tsqlBuilder.append('?');\n\t\t\t\t\tthis.params.add(ArrayUtil.get(paramValue, i));\n\t\t\t\t}\n\t\t\t} else{\n\t\t\t\tsqlBuilder.append('?');\n\t\t\t\tthis.params.add(paramValue);\n\t\t\t}\n\t\t} else {\n\t\t\t// 无变量对应值，原样输出\n\t\t\tsqlBuilder.append(nameStartChar).append(name);\n\t\t}\n\n\t\t//清空变量，表示此变量处理结束\n\t\tname.clear();\n\t}\n\n\t/**\n\t * 是否为标准的字符，包括大小写字母、下划线和数字\n\t *\n\t * @param c 字符\n\t * @return 是否标准字符\n\t */\n\tprivate static boolean isGenerateChar(char c) {\n\t\treturn (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9');\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/Order.java",
    "content": "package cn.hutool.db.sql;\n\nimport java.io.Serializable;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * SQL排序对象\n * @author Looly\n *\n */\npublic class Order implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\t\n\t/** 排序的字段 */\n\tprivate String field;\n\t/** 排序方式（正序还是反序） */\n\tprivate Direction direction;\n\t\n\t//---------------------------------------------------------- Constructor start\n\tpublic Order() {\n\t}\n\t\n\t/**\n\t * 构造\n\t * @param field 排序字段\n\t */\n\tpublic Order(String field) {\n\t\tthis.field = field;\n\t}\n\t\n\t/**\n\t * 构造\n\t * @param field 排序字段\n\t * @param direction 排序方式\n\t */\n\tpublic Order(String field, Direction direction) {\n\t\tthis(field);\n\t\tthis.direction = direction;\n\t}\n\t\n\t//---------------------------------------------------------- Constructor end\n\n\t//---------------------------------------------------------- Getters and Setters start\n\t/**\n\t * @return 排序字段\n\t */\n\tpublic String getField() {\n\t\treturn this.field;\n\t}\n\t/**\n\t * 设置排序字段\n\t * @param field 排序字段\n\t */\n\tpublic void setField(String field) {\n\t\tthis.field = field;\n\t}\n\n\t/**\n\t * @return 排序方向\n\t */\n\tpublic Direction getDirection() {\n\t\treturn direction;\n\t}\n\t/**\n\t * 设置排序方向\n\t * @param direction 排序方向\n\t */\n\tpublic void setDirection(Direction direction) {\n\t\tthis.direction = direction;\n\t}\n\t//---------------------------------------------------------- Getters and Setters end\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.builder().append(this.field).append(StrUtil.SPACE).append(null == direction ? StrUtil.EMPTY : direction).toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/Query.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.Page;\n\nimport java.util.Collection;\nimport java.util.Set;\n\n/**\n * 查询对象，用于传递查询所需的字段值<br>\n * 查询对象根据表名（可以多个），多个条件 {@link Condition} 构建查询对象完成查询。<br>\n * 如果想自定义返回结果，则可在查询对象中自定义要查询的字段名，分页{@link Page}信息来自定义结果。\n *\n * @author Looly\n *\n */\npublic class Query {\n\n\t/** 查询的字段名列表 */\n\tCollection<String> fields;\n\t/** 查询的表名 */\n\tString[] tableNames;\n\t/** 查询的条件语句 */\n\tCondition[] where;\n\t/** 分页对象 */\n\tPage page;\n\n\t/**\n\t * 从{@link Entity}构建Query\n\t * @param where 条件查询{@link Entity}，包含条件Map和表名\n\t * @return Query\n\t * @since 5.5.3\n\t */\n\tpublic static Query of(Entity where){\n\t\tfinal Query query = new Query(SqlUtil.buildConditions(where), where.getTableName());\n\t\tfinal Set<String> fieldNames = where.getFieldNames();\n\t\tif(CollUtil.isNotEmpty(fieldNames)){\n\t\t\tquery.setFields(fieldNames);\n\t\t}\n\n\t\treturn query;\n\t}\n\n\t// --------------------------------------------------------------- Constructor start\n\t/**\n\t * 构造\n\t *\n\t * @param tableNames 表名\n\t */\n\tpublic Query(String... tableNames) {\n\t\tthis(null, tableNames);\n\t\tthis.tableNames = tableNames;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param where 条件语句\n\t * @param tableNames 表名\n\t */\n\tpublic Query(Condition[] where, String... tableNames) {\n\t\tthis(where, null, tableNames);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param where 条件语句\n\t * @param page 分页\n\t * @param tableNames 表名\n\t */\n\tpublic Query(Condition[] where, Page page, String... tableNames) {\n\t\tthis(null, tableNames, where, page);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param fields 字段\n\t * @param tableNames 表名\n\t * @param where 条件\n\t * @param page 分页\n\t */\n\tpublic Query(Collection<String> fields, String[] tableNames, Condition[] where, Page page) {\n\t\tthis.fields = fields;\n\t\tthis.tableNames = tableNames;\n\t\tthis.where = where;\n\t\tthis.page = page;\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Getters and Setters start\n\t/**\n\t * 获得查询的字段名列表\n\t *\n\t * @return 查询的字段名列表\n\t */\n\tpublic Collection<String> getFields() {\n\t\treturn fields;\n\t}\n\n\t/**\n\t * 设置查询的字段名列表\n\t *\n\t * @param fields 查询的字段名列表\n\t * @return this\n\t */\n\tpublic Query setFields(Collection<String> fields) {\n\t\tthis.fields = fields;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置查询的字段名列表\n\t *\n\t * @param fields 查询的字段名列表\n\t * @return this\n\t */\n\tpublic Query setFields(String... fields) {\n\t\tthis.fields = CollectionUtil.newArrayList(fields);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得表名数组\n\t *\n\t * @return 表名数组\n\t */\n\tpublic String[] getTableNames() {\n\t\treturn tableNames;\n\t}\n\n\t/**\n\t * 设置表名\n\t *\n\t * @param tableNames 表名\n\t * @return this\n\t */\n\tpublic Query setTableNames(String... tableNames) {\n\t\tthis.tableNames = tableNames;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得条件语句\n\t *\n\t * @return 条件语句\n\t */\n\tpublic Condition[] getWhere() {\n\t\treturn where;\n\t}\n\n\t/**\n\t * 设置条件语句\n\t *\n\t * @param where 条件语句\n\t * @return this\n\t */\n\tpublic Query setWhere(Condition... where) {\n\t\tthis.where = where;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得分页对象，无分页返回{@code null}\n\t *\n\t * @return 分页对象 or {@code null}\n\t */\n\tpublic Page getPage() {\n\t\treturn page;\n\t}\n\n\t/**\n\t * 设置分页对象\n\t *\n\t * @param page 分页对象\n\t * @return this\n\t */\n\tpublic Query setPage(Page page) {\n\t\tthis.page = page;\n\t\treturn this;\n\t}\n\t// --------------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 获得第一个表名\n\t *\n\t * @return 表名\n\t * @throws DbRuntimeException 没有表\n\t */\n\tpublic String getFirstTableName() throws DbRuntimeException {\n\t\tif (ArrayUtil.isEmpty(this.tableNames)) {\n\t\t\tthrow new DbRuntimeException(\"No tableName!\");\n\t\t}\n\t\treturn this.tableNames[0];\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.dialect.DialectName;\nimport cn.hutool.db.dialect.impl.OracleDialect;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * SQL构建器<br>\n * 首先拼接SQL语句，值使用 ? 占位<br>\n * 调用getParamValues()方法获得占位符对应的值\n *\n * @author Looly\n */\npublic class SqlBuilder implements Builder<String> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// --------------------------------------------------------------- Static methods start\n\n\t/**\n\t * 创建SQL构建器\n\t *\n\t * @return SQL构建器\n\t */\n\tpublic static SqlBuilder create() {\n\t\treturn new SqlBuilder();\n\t}\n\n\t/**\n\t * 创建SQL构建器\n\t *\n\t * @param wrapper 包装器\n\t * @return SQL构建器\n\t */\n\tpublic static SqlBuilder create(Wrapper wrapper) {\n\t\treturn new SqlBuilder(wrapper);\n\t}\n\n\t/**\n\t * 从已有的SQL中构建一个SqlBuilder\n\t *\n\t * @param sql SQL语句\n\t * @return SqlBuilder\n\t * @since 5.5.3\n\t */\n\tpublic static SqlBuilder of(CharSequence sql) {\n\t\treturn create().append(sql);\n\t}\n\n\t/**\n\t * 验证实体类对象的有效性\n\t *\n\t * @param entity 实体类对象\n\t * @throws DbRuntimeException SQL异常包装，获取元数据信息失败\n\t */\n\tpublic static void validateEntity(Entity entity) throws DbRuntimeException {\n\t\tif (null == entity) {\n\t\t\tthrow new DbRuntimeException(\"Entity is null !\");\n\t\t}\n\t\tif (StrUtil.isBlank(entity.getTableName())) {\n\t\t\tthrow new DbRuntimeException(\"Entity`s table name is null !\");\n\t\t}\n\t\tif (entity.isEmpty()) {\n\t\t\tthrow new DbRuntimeException(\"No filed and value in this entity !\");\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------- Static methods end\n\n\t// --------------------------------------------------------------- Enums start\n\n\t/**\n\t * SQL中多表关联用的关键字\n\t *\n\t * @author Looly\n\t */\n\tpublic enum Join {\n\t\t/**\n\t\t * 如果表中有至少一个匹配，则返回行\n\t\t */\n\t\tINNER,\n\t\t/**\n\t\t * 即使右表中没有匹配，也从左表返回所有的行\n\t\t */\n\t\tLEFT,\n\t\t/**\n\t\t * 即使左表中没有匹配，也从右表返回所有的行\n\t\t */\n\t\tRIGHT,\n\t\t/**\n\t\t * 只要其中一个表中存在匹配，就返回行\n\t\t */\n\t\tFULL\n\t}\n\t// --------------------------------------------------------------- Enums end\n\n\tprivate final StringBuilder sql = new StringBuilder();\n\t/**\n\t * 占位符对应的值列表\n\t */\n\tprivate final List<Object> paramValues = new ArrayList<>();\n\t/**\n\t * 包装器\n\t */\n\tprivate Wrapper wrapper;\n\n\t// --------------------------------------------------------------- Constructor start\n\tpublic SqlBuilder() {\n\t}\n\n\tpublic SqlBuilder(Wrapper wrapper) {\n\t\tthis.wrapper = wrapper;\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Builder start\n\n\t/**\n\t * 插入，使用默认的ANSI方言\n\t *\n\t * @param entity 实体\n\t * @return 自己\n\t */\n\tpublic SqlBuilder insert(Entity entity) {\n\t\treturn this.insert(entity, DialectName.ANSI);\n\t}\n\n\t/**\n\t * 插入<br>\n\t * 插入会忽略空的字段名及其对应值，但是对于有字段名对应值为{@code null}的情况不忽略\n\t *\n\t * @param entity      实体\n\t * @param dialectName 方言名，用于对特殊数据库特殊处理\n\t * @return 自己\n\t */\n\tpublic SqlBuilder insert(Entity entity, DialectName dialectName) {\n\t\treturn insert(entity, dialectName.name());\n\t}\n\n\t/**\n\t * 插入<br>\n\t * 插入会忽略空的字段名及其对应值，但是对于有字段名对应值为{@code null}的情况不忽略\n\t *\n\t * @param entity      实体\n\t * @param dialectName 方言名，用于对特殊数据库特殊处理\n\t * @return 自己\n\t * @since 5.5.3\n\t */\n\tpublic SqlBuilder insert(Entity entity, String dialectName) {\n\t\t// 验证\n\t\tvalidateEntity(entity);\n\n\t\tfinal boolean isOracle = DialectName.ORACLE.match(dialectName);// 对Oracle的特殊处理\n\t\tfinal StringBuilder fieldsPart = new StringBuilder();\n\t\tfinal StringBuilder placeHolder = new StringBuilder();\n\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (fieldsPart.length() > 0) {\n\t\t\t\t\t// 非第一个参数，追加逗号\n\t\t\t\t\tfieldsPart.append(\", \");\n\t\t\t\t\tplaceHolder.append(\", \");\n\t\t\t\t}\n\n\t\t\t\tfieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field);\n\t\t\t\tif (isOracle && OracleDialect.isNextVal(value)) {\n\t\t\t\t\t// Oracle的特殊自增键，通过字段名.nextval获得下一个值\n\t\t\t\t\tplaceHolder.append(value);\n\t\t\t\t} else {\n\t\t\t\t\t// 普通字段使用占位符\n\t\t\t\t\tplaceHolder.append(\"?\");\n\t\t\t\t\tthis.paramValues.add(value);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// issue#1656@Github Phoenix兼容\n\t\tif (DialectName.PHOENIX.match(dialectName)) {\n\t\t\tsql.append(\"UPSERT INTO \");\n\t\t} else {\n\t\t\tsql.append(\"INSERT INTO \");\n\t\t}\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != this.wrapper) {\n\t\t\t// 包装表名 entity = wrapper.wrap(entity);\n\t\t\ttableName = this.wrapper.wrap(tableName);\n\t\t}\n\t\tsql.append(tableName)\n\t\t\t\t.append(\" (\").append(fieldsPart).append(\") VALUES (\")//\n\t\t\t\t.append(placeHolder).append(\")\");\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 删除\n\t *\n\t * @param tableName 表名\n\t * @return 自己\n\t */\n\tpublic SqlBuilder delete(String tableName) {\n\t\tif (StrUtil.isBlank(tableName)) {\n\t\t\tthrow new DbRuntimeException(\"Table name is blank !\");\n\t\t}\n\n\t\tif (null != wrapper) {\n\t\t\t// 包装表名\n\t\t\ttableName = wrapper.wrap(tableName);\n\t\t}\n\n\t\tsql.append(\"DELETE FROM \").append(tableName);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 更新\n\t *\n\t * @param entity 要更新的实体\n\t * @return 自己\n\t */\n\tpublic SqlBuilder update(Entity entity) {\n\t\t// 验证\n\t\tvalidateEntity(entity);\n\n\t\tString tableName = entity.getTableName();\n\t\tif (null != wrapper) {\n\t\t\t// 包装表名\n\t\t\ttableName = wrapper.wrap(tableName);\n\t\t}\n\n\t\tsql.append(\"UPDATE \").append(tableName).append(\" SET \");\n\t\tentity.forEach((field, value) -> {\n\t\t\tif (StrUtil.isNotBlank(field)) {\n\t\t\t\tif (paramValues.size() > 0) {\n\t\t\t\t\tsql.append(\", \");\n\t\t\t\t}\n\t\t\t\tsql.append((null != wrapper) ? wrapper.wrap(field) : field).append(\" = ? \");\n\t\t\t\tthis.paramValues.add(value);// 更新不对空做处理，因为存在清空字段的情况\n\t\t\t}\n\t\t});\n\t\treturn this;\n\t}\n\n\t/**\n\t * 查询\n\t *\n\t * @param isDistinct 是否添加DISTINCT关键字（查询唯一结果）\n\t * @param fields     查询的字段\n\t * @return 自己\n\t */\n\tpublic SqlBuilder select(boolean isDistinct, String... fields) {\n\t\treturn select(isDistinct, Arrays.asList(fields));\n\t}\n\n\t/**\n\t * 查询\n\t *\n\t * @param isDistinct 是否添加DISTINCT关键字（查询唯一结果）\n\t * @param fields     查询的字段\n\t * @return 自己\n\t */\n\tpublic SqlBuilder select(boolean isDistinct, Collection<String> fields) {\n\t\tsql.append(\"SELECT \");\n\t\tif (isDistinct) {\n\t\t\tsql.append(\"DISTINCT \");\n\t\t}\n\n\t\tif (CollectionUtil.isEmpty(fields)) {\n\t\t\tsql.append(\"*\");\n\t\t} else {\n\t\t\tif (null != wrapper) {\n\t\t\t\t// 包装字段名\n\t\t\t\tfields = wrapper.wrap(fields);\n\t\t\t}\n\t\t\tsql.append(CollectionUtil.join(fields, StrUtil.COMMA));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 查询（非Distinct）\n\t *\n\t * @param fields 查询的字段\n\t * @return 自己\n\t */\n\tpublic SqlBuilder select(String... fields) {\n\t\treturn select(false, fields);\n\t}\n\n\t/**\n\t * 查询（非Distinct）\n\t *\n\t * @param fields 查询的字段\n\t * @return 自己\n\t */\n\tpublic SqlBuilder select(Collection<String> fields) {\n\t\treturn select(false, fields);\n\t}\n\n\t/**\n\t * 添加 from语句\n\t *\n\t * @param tableNames 表名列表（多个表名用于多表查询）\n\t * @return 自己\n\t */\n\tpublic SqlBuilder from(String... tableNames) {\n\t\tif (ArrayUtil.isEmpty(tableNames) || StrUtil.hasBlank(tableNames)) {\n\t\t\tthrow new DbRuntimeException(\"Table name is blank in table names !\");\n\t\t}\n\n\t\tif (null != wrapper) {\n\t\t\t// 包装表名\n\t\t\ttableNames = wrapper.wrap(tableNames);\n\t\t}\n\n\t\tsql.append(\" FROM \").append(ArrayUtil.join(tableNames, StrUtil.COMMA));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加Where语句，所有逻辑之间关系使用{@link Condition#setLinkOperator(LogicalOperator)} 定义\n\t *\n\t * @param conditions 条件，当条件为空时，只添加WHERE关键字\n\t * @return 自己\n\t * @since 4.4.4\n\t */\n\tpublic SqlBuilder where(Condition... conditions) {\n\t\tif (ArrayUtil.isNotEmpty(conditions)) {\n\t\t\twhere(buildCondition(conditions));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加Where语句<br>\n\t *\n\t * @param where WHERE语句之后跟的条件语句字符串\n\t * @return 自己\n\t */\n\tpublic SqlBuilder where(String where) {\n\t\tif (StrUtil.isNotBlank(where)) {\n\t\t\tsql.append(\" WHERE \").append(where);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 多值选择\n\t *\n\t * @param <T>    值类型\n\t * @param field  字段名\n\t * @param values 值列表\n\t * @return 自身\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> SqlBuilder in(String field, T... values) {\n\t\tsql.append(wrapper.wrap(field)).append(\" IN \").append(\"(\").append(ArrayUtil.join(values, StrUtil.COMMA)).append(\")\");\n\t\treturn this;\n\t}\n\n\t/**\n\t * 分组\n\t *\n\t * @param fields 字段\n\t * @return 自己\n\t */\n\tpublic SqlBuilder groupBy(String... fields) {\n\t\tif (ArrayUtil.isNotEmpty(fields)) {\n\t\t\tif (null != wrapper) {\n\t\t\t\t// 包装字段名\n\t\t\t\tfields = wrapper.wrap(fields);\n\t\t\t}\n\n\t\t\tsql.append(\" GROUP BY \").append(ArrayUtil.join(fields, StrUtil.COMMA));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加Having语句，所有逻辑之间关系使用{@link Condition#setLinkOperator(LogicalOperator)} 定义\n\t *\n\t * @param conditions 条件\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic SqlBuilder having(Condition... conditions) {\n\t\tif (ArrayUtil.isNotEmpty(conditions)) {\n\t\t\thaving(buildCondition(conditions));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加Having语句\n\t *\n\t * @param having 条件语句\n\t * @return 自己\n\t */\n\tpublic SqlBuilder having(String having) {\n\t\tif (StrUtil.isNotBlank(having)) {\n\t\t\tsql.append(\" HAVING \").append(having);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 排序\n\t *\n\t * @param orders 排序对象\n\t * @return 自己\n\t */\n\tpublic SqlBuilder orderBy(Order... orders) {\n\t\tif (ArrayUtil.isEmpty(orders)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tsql.append(\" ORDER BY \");\n\t\tString field;\n\t\tboolean isFirst = true;\n\t\tfor (Order order : orders) {\n\t\t\tfield = order.getField();\n\t\t\tif (null != wrapper) {\n\t\t\t\t// 包装字段名\n\t\t\t\tfield = wrapper.wrap(field);\n\t\t\t}\n\t\t\tif (StrUtil.isBlank(field)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 只有在非第一项前添加逗号\n\t\t\tif (isFirst) {\n\t\t\t\tisFirst = false;\n\t\t\t} else {\n\t\t\t\tsql.append(StrUtil.COMMA);\n\t\t\t}\n\t\t\tsql.append(field);\n\t\t\tfinal Direction direction = order.getDirection();\n\t\t\tif (null != direction) {\n\t\t\t\tsql.append(StrUtil.SPACE).append(direction);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 多表关联\n\t *\n\t * @param tableName 被关联的表名\n\t * @param join      内联方式\n\t * @return 自己\n\t */\n\tpublic SqlBuilder join(String tableName, Join join) {\n\t\tif (StrUtil.isBlank(tableName)) {\n\t\t\tthrow new DbRuntimeException(\"Table name is blank !\");\n\t\t}\n\n\t\tif (null != join) {\n\t\t\tsql.append(StrUtil.SPACE).append(join).append(\" JOIN \");\n\t\t\tif (null != wrapper) {\n\t\t\t\t// 包装表名\n\t\t\t\ttableName = wrapper.wrap(tableName);\n\t\t\t}\n\t\t\tsql.append(tableName);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 配合JOIN的 ON语句，多表关联的条件语句，所有逻辑之间关系使用{@link Condition#setLinkOperator(LogicalOperator)} 定义\n\t *\n\t * @param conditions 条件\n\t * @return this\n\t * @since 5.4.3\n\t */\n\tpublic SqlBuilder on(Condition... conditions) {\n\t\tif (ArrayUtil.isNotEmpty(conditions)) {\n\t\t\ton(buildCondition(conditions));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 配合JOIN的 ON语句，多表关联的条件语句<br>\n\t * 只支持单一的逻辑运算符（例如多个条件之间）\n\t *\n\t * @param on 条件\n\t * @return 自己\n\t */\n\tpublic SqlBuilder on(String on) {\n\t\tif (StrUtil.isNotBlank(on)) {\n\t\t\tthis.sql.append(\" ON \").append(on);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在SQL的开头补充SQL片段\n\t *\n\t * @param sqlFragment SQL片段\n\t * @return this\n\t * @since 4.1.3\n\t */\n\tpublic SqlBuilder insertPreFragment(Object sqlFragment) {\n\t\tif (null != sqlFragment) {\n\t\t\tthis.sql.insert(0, sqlFragment);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加SQL其它部分片段，此方法只是简单的追加SQL字符串，空格需手动加入，例如：\n\t *\n\t * <pre>\n\t *     SqlBuilder builder = SqlBuilder.of(\"select *\");\n\t *     builder.append(\" from \").append(\"user\");\n\t * </pre>\n\t * <p>\n\t * 如果需要追加带占位符的片段，需调用{@link #addParams(Object...)} 方法加入对应参数值。\n\t *\n\t * @param sqlFragment SQL其它部分片段\n\t * @return this\n\t */\n\tpublic SqlBuilder append(Object sqlFragment) {\n\t\tif (null != sqlFragment) {\n\t\t\tthis.sql.append(sqlFragment);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 手动增加参数，调用此方法前需确认SQL中有对应占位符，主要用于自定义SQL片段中有占位符的情况，例如：\n\t *\n\t * <pre>\n\t *     SqlBuilder builder = SqlBuilder.of(\"select * from user where id=?\");\n\t *     builder.append(\" and name=?\")\n\t *     builder.addParams(1, \"looly\");\n\t * </pre>\n\t *\n\t * @param params 参数列表\n\t * @return this\n\t * @since 5.5.3\n\t */\n\tpublic SqlBuilder addParams(Object... params) {\n\t\tif (ArrayUtil.isNotEmpty(params)) {\n\t\t\tCollections.addAll(this.paramValues, params);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建查询SQL\n\t *\n\t * @param query {@link Query}\n\t * @return this\n\t */\n\tpublic SqlBuilder query(Query query) {\n\t\treturn this.select(query.getFields()).from(query.getTableNames()).where(query.getWhere());\n\t}\n\t// --------------------------------------------------------------- Builder end\n\n\t/**\n\t * 获得占位符对应的值列表<br>\n\t *\n\t * @return 占位符对应的值列表\n\t */\n\tpublic List<Object> getParamValues() {\n\t\treturn this.paramValues;\n\t}\n\n\t/**\n\t * 获得占位符对应的值列表<br>\n\t *\n\t * @return 占位符对应的值列表\n\t */\n\tpublic Object[] getParamValueArray() {\n\t\treturn this.paramValues.toArray(new Object[0]);\n\t}\n\n\t/**\n\t * 构建，默认打印SQL日志\n\t *\n\t * @return 构建好的SQL语句\n\t */\n\t@Override\n\tpublic String build() {\n\t\treturn this.sql.toString();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.build();\n\t}\n\n\t/**\n\t * 格式化SQL语句\n\t * @return SqlBuilder\n\t */\n\tpublic SqlBuilder format() {\n\t\tthis.sql.replace(0, this.sql.length(), SqlFormatter.format(this.sql.toString()));\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------- private method start\n\n\t/**\n\t * 构建组合条件<br>\n\t * 例如：name = ? AND type IN (?, ?) AND other LIKE ?\n\t *\n\t * @param conditions 条件对象\n\t * @return 构建后的SQL语句条件部分\n\t */\n\tprivate String buildCondition(Condition... conditions) {\n\t\tif (ArrayUtil.isEmpty(conditions)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tif (null != wrapper) {\n\t\t\t// 包装字段名\n\t\t\tconditions = wrapper.wrap(conditions);\n\t\t}\n\n\t\treturn ConditionBuilder.of(conditions).build(this.paramValues);\n\t}\n\t// --------------------------------------------------------------- private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.lang.func.Func1;\nimport cn.hutool.db.DbUtil;\nimport cn.hutool.db.StatementUtil;\nimport cn.hutool.db.handler.RsHandler;\n\nimport java.sql.CallableStatement;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.util.Map;\n\n/**\n * SQL执行器，全部为静态方法，执行查询或非查询的SQL语句<br>\n * 此方法为JDBC的简单封装，与数据库类型无关\n *\n * @author loolly\n *\n */\npublic class SqlExecutor {\n\n\t/**\n\t * 执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL，使用name做为占位符，例如:name\n\t * @param paramMap 参数Map\n\t * @return 影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.10\n\t */\n\tpublic static int execute(Connection conn, String sql, Map<String, Object> paramMap) throws SQLException {\n\t\tfinal NamedSql namedSql = new NamedSql(sql, paramMap);\n\t\treturn execute(conn, namedSql.getSql(), namedSql.getParams());\n\t}\n\n\t/**\n\t * 执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param params 参数\n\t * @return 影响的行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static int execute(Connection conn, String sql, Object... params) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = StatementUtil.prepareStatement(false, conn, sql, params);\n\t\t\treturn ps.executeUpdate();\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 执行调用存储过程<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param params 参数\n\t * @return 如果执行后第一个结果是ResultSet，则返回true，否则返回false。\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static boolean call(Connection conn, String sql, Object... params) throws SQLException {\n\t\tCallableStatement call = null;\n\t\ttry {\n\t\t\tcall = StatementUtil.prepareCall(conn, sql, params);\n\t\t\treturn call.execute();\n\t\t} finally {\n\t\t\tDbUtil.close(call);\n\t\t}\n\t}\n\n\t/**\n\t * 执行调用存储过程<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param params 参数\n\t * @return ResultSet\n\t * @throws SQLException SQL执行异常\n\t * @since 4.1.4\n\t */\n\tpublic static ResultSet callQuery(Connection conn, String sql, Object... params) throws SQLException {\n\t\treturn StatementUtil.prepareCall(conn, sql, params).executeQuery();\n\t}\n\n\t/**\n\t * 执行非查询语句，返回主键<br>\n\t * 发查询语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param paramMap 参数Map\n\t * @return 主键\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.10\n\t */\n\tpublic static Long executeForGeneratedKey(Connection conn, String sql, Map<String, Object> paramMap) throws SQLException {\n\t\tfinal NamedSql namedSql = new NamedSql(sql, paramMap);\n\t\treturn executeForGeneratedKey(conn, namedSql.getSql(), namedSql.getParams());\n\t}\n\n\t/**\n\t * 执行非查询语句，返回主键<br>\n\t * 发查询语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param params 参数\n\t * @return 主键\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static Long executeForGeneratedKey(Connection conn, String sql, Object... params) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\tResultSet rs = null;\n\t\ttry {\n\t\t\tps = StatementUtil.prepareStatement(true, conn, sql, params);\n\t\t\tps.executeUpdate();\n\t\t\trs = ps.getGeneratedKeys();\n\t\t\tif (rs != null && rs.next()) {\n\t\t\t\ttry {\n\t\t\t\t\treturn rs.getLong(1);\n\t\t\t\t} catch (SQLException e) {\n\t\t\t\t\t// 可能会出现没有主键返回的情况\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t\tDbUtil.close(rs);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param paramsBatch 批量的参数\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @deprecated 重载导致编译器无法区分\n\t */\n\t@Deprecated\n\tpublic static int[] executeBatch(Connection conn, String sql, Object[]... paramsBatch) throws SQLException {\n\t\treturn executeBatch(conn, sql, new ArrayIter<>(paramsBatch));\n\t}\n\n\t/**\n\t * 批量执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sql SQL\n\t * @param paramsBatch 批量的参数\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static int[] executeBatch(Connection conn, String sql, Iterable<Object[]> paramsBatch) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = StatementUtil.prepareStatementForBatch(conn, sql, paramsBatch);\n\t\t\treturn ps.executeBatch();\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 批量执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sqls SQL列表\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.6\n\t */\n\tpublic static int[] executeBatch(Connection conn, String... sqls) throws SQLException {\n\t\treturn executeBatch(conn, new ArrayIter<>(sqls));\n\t}\n\n\t/**\n\t * 批量执行非查询语句<br>\n\t * 语句包括 插入、更新、删除<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param conn 数据库连接对象\n\t * @param sqls SQL列表\n\t * @return 每个SQL执行影响的行数\n\t * @throws SQLException SQL执行异常\n\t * @since 4.5.6\n\t */\n\tpublic static int[] executeBatch(Connection conn, Iterable<String> sqls) throws SQLException {\n\t\tStatement statement = null;\n\t\ttry {\n\t\t\tstatement = conn.createStatement();\n\t\t\tfor (String sql : sqls) {\n\t\t\t\tstatement.addBatch(sql);\n\t\t\t}\n\t\t\treturn statement.executeBatch();\n\t\t} finally {\n\t\t\tDbUtil.close(statement);\n\t\t}\n\t}\n\n\t/**\n\t * 执行查询语句，例如：select * from table where field1=:name1 <br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T> 处理结果类型\n\t * @param conn 数据库连接对象\n\t * @param sql 查询语句，使用参数名占位符，例如:name\n\t * @param rsh 结果集处理对象\n\t * @param paramMap 参数对\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 4.0.10\n\t */\n\tpublic static <T> T query(Connection conn, String sql, RsHandler<T> rsh, Map<String, Object> paramMap) throws SQLException {\n\t\tfinal NamedSql namedSql = new NamedSql(sql, paramMap);\n\t\treturn query(conn, namedSql.getSql(), rsh, namedSql.getParams());\n\t}\n\n\t/**\n\t * 执行查询语句<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T> 处理结果类型\n\t * @param conn 数据库连接对象\n\t * @param sqlBuilder SQL构建器，包含参数\n\t * @param rsh 结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 5.5.3\n\t */\n\tpublic static <T> T query(Connection conn, SqlBuilder sqlBuilder, RsHandler<T> rsh) throws SQLException {\n\t\treturn query(conn, sqlBuilder.build(), rsh, sqlBuilder.getParamValueArray());\n\t}\n\n\t/**\n\t * 执行查询语句<br>\n\t * 此方法不会关闭Connection\n\t *\n\t * @param <T> 处理结果类型\n\t * @param conn 数据库连接对象\n\t * @param sql 查询语句\n\t * @param rsh 结果集处理对象\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static <T> T query(Connection conn, String sql, RsHandler<T> rsh, Object... params) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = StatementUtil.prepareStatement(false, conn, sql, params);\n\t\t\treturn executeQuery(ps, rsh);\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t/**\n\t * 执行自定义的{@link PreparedStatement}，结果使用{@link RsHandler}处理<br>\n\t * 此方法主要用于自定义场景，如游标查询等\n\t *\n\t * @param <T> 处理结果类型\n\t * @param conn 数据库连接对象\n\t * @param statementFunc 自定义{@link PreparedStatement}创建函数\n\t * @param rsh 自定义结果集处理\n\t * @return 结果\n\t * @throws SQLException SQL执行异常\n\t * @since 5.7.17\n\t */\n\tpublic static <T> T query(Connection conn, Func1<Connection, PreparedStatement> statementFunc, RsHandler<T> rsh) throws SQLException {\n\t\tPreparedStatement ps = null;\n\t\ttry {\n\t\t\tps = statementFunc.call(conn);\n\t\t\treturn executeQuery(ps, rsh);\n\t\t} catch (Exception e) {\n\t\t\tif(e instanceof SQLException){\n\t\t\t\tthrow (SQLException) e;\n\t\t\t}\n\t\t\tthrow ExceptionUtil.wrapRuntime(e);\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------- Execute With PreparedStatement\n\t/**\n\t * 用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQL DDL（数据定义语言）语句，例如 CREATE TABLE 和 DROP TABLE。<br>\n\t * INSERT、UPDATE 或 DELETE 语句的效果是修改表中零行或多行中的一列或多列。<br>\n\t * executeUpdate 的返回值是一个整数（int），指示受影响的行数（即更新计数）。<br>\n\t * 对于 CREATE TABLE 或 DROP TABLE 等不操作行的语句，executeUpdate 的返回值总为零。<br>\n\t * 此方法不会关闭PreparedStatement\n\t *\n\t * @param ps PreparedStatement对象\n\t * @param params 参数\n\t * @return 影响的行数\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static int executeUpdate(PreparedStatement ps, Object... params) throws SQLException {\n\t\tStatementUtil.fillParams(ps, params);\n\t\treturn ps.executeUpdate();\n\t}\n\n\t/**\n\t * 可用于执行任何SQL语句，返回一个boolean值，表明执行该SQL语句是否返回了ResultSet。<br>\n\t * 如果执行后第一个结果是ResultSet，则返回true，否则返回false。<br>\n\t * 此方法不会关闭PreparedStatement\n\t *\n\t * @param ps PreparedStatement对象\n\t * @param params 参数\n\t * @return 如果执行后第一个结果是ResultSet，则返回true，否则返回false。\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static boolean execute(PreparedStatement ps, Object... params) throws SQLException {\n\t\tStatementUtil.fillParams(ps, params);\n\t\treturn ps.execute();\n\t}\n\n\t/**\n\t * 执行查询语句<br>\n\t * 此方法不会关闭PreparedStatement\n\t *\n\t * @param <T> 处理结果类型\n\t * @param ps PreparedStatement\n\t * @param rsh 结果集处理对象\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static <T> T query(PreparedStatement ps, RsHandler<T> rsh, Object... params) throws SQLException {\n\t\tStatementUtil.fillParams(ps, params);\n\t\treturn executeQuery(ps, rsh);\n\t}\n\n\t/**\n\t * 执行查询语句并关闭PreparedStatement\n\t *\n\t * @param <T> 处理结果类型\n\t * @param ps PreparedStatement\n\t * @param rsh 结果集处理对象\n\t * @param params 参数\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t */\n\tpublic static <T> T queryAndClosePs(PreparedStatement ps, RsHandler<T> rsh, Object... params) throws SQLException {\n\t\ttry {\n\t\t\treturn query(ps, rsh, params);\n\t\t} finally {\n\t\t\tDbUtil.close(ps);\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 执行查询\n\t *\n\t * @param ps {@link PreparedStatement}\n\t * @param rsh 结果集处理对象\n\t * @return 结果对象\n\t * @throws SQLException SQL执行异常\n\t * @since 4.1.13\n\t */\n\tprivate static <T> T executeQuery(PreparedStatement ps, RsHandler<T> rsh) throws SQLException {\n\t\tResultSet rs = null;\n\t\ttry {\n\t\t\trs = ps.executeQuery();\n\t\t\treturn rsh.handle(rs);\n\t\t} finally {\n\t\t\tDbUtil.close(rs);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.HashSet;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.StringTokenizer;\n\n/**\n * SQL格式化器 from Hibernate\n *\n * @author looly\n */\npublic class SqlFormatter {\n\tprivate static final Set<String> BEGIN_CLAUSES = new HashSet<>();\n\tprivate static final Set<String> END_CLAUSES = new HashSet<>();\n\tprivate static final Set<String> LOGICAL = new HashSet<>();\n\tprivate static final Set<String> QUANTIFIERS = new HashSet<>();\n\tprivate static final Set<String> DML = new HashSet<>();\n\tprivate static final Set<String> MISC = new HashSet<>();\n\n\tstatic {\n\t\tBEGIN_CLAUSES.add(\"left\");\n\t\tBEGIN_CLAUSES.add(\"right\");\n\t\tBEGIN_CLAUSES.add(\"inner\");\n\t\tBEGIN_CLAUSES.add(\"outer\");\n\t\tBEGIN_CLAUSES.add(\"group\");\n\t\tBEGIN_CLAUSES.add(\"order\");\n\n\t\tEND_CLAUSES.add(\"where\");\n\t\tEND_CLAUSES.add(\"set\");\n\t\tEND_CLAUSES.add(\"having\");\n\t\tEND_CLAUSES.add(\"join\");\n\t\tEND_CLAUSES.add(\"from\");\n\t\tEND_CLAUSES.add(\"by\");\n\t\tEND_CLAUSES.add(\"into\");\n\t\tEND_CLAUSES.add(\"union\");\n\n\t\tLOGICAL.add(\"and\");\n\t\tLOGICAL.add(\"or\");\n\t\tLOGICAL.add(\"when\");\n\t\tLOGICAL.add(\"else\");\n\t\tLOGICAL.add(\"end\");\n\n\t\tQUANTIFIERS.add(\"in\");\n\t\tQUANTIFIERS.add(\"all\");\n\t\tQUANTIFIERS.add(\"exists\");\n\t\tQUANTIFIERS.add(\"some\");\n\t\tQUANTIFIERS.add(\"any\");\n\n\t\tDML.add(\"insert\");\n\t\tDML.add(\"update\");\n\t\tDML.add(\"delete\");\n\n\t\tMISC.add(\"select\");\n\t\tMISC.add(\"on\");\n\t}\n\n\tprivate static final String indentString = \"    \";\n\tprivate static final String initial = \"\\n    \";\n\n\tpublic static String format(String source) {\n\t\treturn new FormatProcess(source).perform().trim();\n\t}\n\n\t//------------------------------------------------------------------------------------------------\n\n\tprivate static class FormatProcess {\n\t\tboolean beginLine = true;\n\t\tboolean afterBeginBeforeEnd = false;\n\t\tboolean afterByOrSetOrFromOrSelect = false;\n\t\t//\t\tboolean afterValues = false;\n\t\tboolean afterOn = false;\n\t\tboolean afterBetween = false;\n\t\tboolean afterInsert = false;\n\t\tint inFunction = 0;\n\t\tint parensSinceSelect = 0;\n\t\tprivate final LinkedList<Integer> parenCounts = new LinkedList<>();\n\t\tprivate final LinkedList<Boolean> afterByOrFromOrSelects = new LinkedList<>();\n\n\t\tint indent = 1;\n\n\t\tStringBuffer result = new StringBuffer();\n\t\tStringTokenizer tokens;\n\t\tString lastToken;\n\t\tString token;\n\t\tString lcToken;\n\n\t\tpublic FormatProcess(String sql) {\n\t\t\tthis.tokens = new StringTokenizer(sql, \"()+*/-=<>'`\\\"[], \\n\\r\\f\\t\", true);\n\t\t}\n\n\t\tpublic String perform() {\n\t\t\tthis.result.append(initial);\n\n\t\t\twhile (this.tokens.hasMoreTokens()) {\n\t\t\t\tthis.token = this.tokens.nextToken();\n\t\t\t\tthis.lcToken = this.token.toLowerCase();\n\n\t\t\t\tif (\"'\".equals(this.token)) {\n\t\t\t\t\tString t;\n\t\t\t\t\tdo {\n\t\t\t\t\t\tt = this.tokens.nextToken();\n\t\t\t\t\t\tthis.token += t;\n\t\t\t\t\t} while ((!\"'\".equals(t)) && (this.tokens.hasMoreTokens()));\n\t\t\t\t} else if (\"\\\"\".equals(this.token)) {\n\t\t\t\t\tString t;\n\t\t\t\t\tdo {\n\t\t\t\t\t\tt = this.tokens.nextToken();\n\t\t\t\t\t\tthis.token += t;\n\t\t\t\t\t} while (!\"\\\"\".equals(t));\n\t\t\t\t} else if (\"`\".equals(this.token)) {\n\t\t\t\t\tString t;\n\t\t\t\t\tdo {\n\t\t\t\t\t\tt = this.tokens.nextToken();\n\t\t\t\t\t\tthis.token += t;\n\t\t\t\t\t} while (!\"`\".equals(t));\n\t\t\t\t}\n\n\t\t\t\tif ((this.afterByOrSetOrFromOrSelect) && (\",\".equals(this.token))) {\n\t\t\t\t\tcommaAfterByOrFromOrSelect();\n\t\t\t\t} else if ((this.afterOn) && (\",\".equals(this.token))) {\n\t\t\t\t\tcommaAfterOn();\n\t\t\t\t} else if (\"(\".equals(this.token)) {\n\t\t\t\t\topenParen();\n\t\t\t\t} else if (\")\".equals(this.token)) {\n\t\t\t\t\tcloseParen();\n\t\t\t\t} else if (BEGIN_CLAUSES.contains(this.lcToken)) {\n\t\t\t\t\tbeginNewClause();\n\t\t\t\t} else if (END_CLAUSES.contains(this.lcToken)) {\n\t\t\t\t\tendNewClause();\n\t\t\t\t} else if (\"select\".equals(this.lcToken)) {\n\t\t\t\t\tselect();\n\t\t\t\t} else if (DML.contains(this.lcToken)) {\n\t\t\t\t\tupdateOrInsertOrDelete();\n\t\t\t\t} else if (\"values\".equals(this.lcToken)) {\n\t\t\t\t\tvalues();\n\t\t\t\t} else if (\"on\".equals(this.lcToken)) {\n\t\t\t\t\ton();\n\t\t\t\t} else if ((this.afterBetween) && (\"and\".equals(this.lcToken))) {\n\t\t\t\t\tmisc();\n\t\t\t\t\tthis.afterBetween = false;\n\t\t\t\t} else if (LOGICAL.contains(this.lcToken)) {\n\t\t\t\t\tlogical();\n\t\t\t\t} else if (isWhitespace(this.token)) {\n\t\t\t\t\twhite();\n\t\t\t\t} else {\n\t\t\t\t\tmisc();\n\t\t\t\t}\n\n\t\t\t\tif (false == isWhitespace(this.token)) {\n\t\t\t\t\tthis.lastToken = this.lcToken;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this.result.toString();\n\t\t}\n\n\t\tprivate void commaAfterOn() {\n\t\t\tout();\n\t\t\tthis.indent -= 1;\n\t\t\tnewline();\n\t\t\tthis.afterOn = false;\n\t\t\tthis.afterByOrSetOrFromOrSelect = true;\n\t\t}\n\n\t\tprivate void commaAfterByOrFromOrSelect() {\n\t\t\tout();\n\t\t\tnewline();\n\t\t}\n\n\t\tprivate void logical() {\n\t\t\tif (\"end\".equals(this.lcToken)) {\n\t\t\t\tthis.indent -= 1;\n\t\t\t}\n\t\t\tnewline();\n\t\t\tout();\n\t\t\tthis.beginLine = false;\n\t\t}\n\n\t\tprivate void on() {\n\t\t\tthis.indent += 1;\n\t\t\tthis.afterOn = true;\n\t\t\tnewline();\n\t\t\tout();\n\t\t\tthis.beginLine = false;\n\t\t}\n\n\t\tprivate void misc() {\n\t\t\tout();\n\t\t\tif (\"between\".equals(this.lcToken)) {\n\t\t\t\tthis.afterBetween = true;\n\t\t\t}\n\t\t\tif (this.afterInsert) {\n\t\t\t\tnewline();\n\t\t\t\tthis.afterInsert = false;\n\t\t\t} else {\n\t\t\t\tthis.beginLine = false;\n\t\t\t\tif (\"case\".equals(this.lcToken)) {\n\t\t\t\t\tthis.indent += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tprivate void white() {\n\t\t\tif (!this.beginLine) {\n\t\t\t\tthis.result.append(\" \");\n\t\t\t}\n\t\t}\n\n\t\tprivate void updateOrInsertOrDelete() {\n\t\t\tout();\n\t\t\tthis.indent += 1;\n\t\t\tthis.beginLine = false;\n\t\t\tif (\"update\".equals(this.lcToken)) {\n\t\t\t\tnewline();\n\t\t\t}\n\t\t\tif (\"insert\".equals(this.lcToken)) {\n\t\t\t\tthis.afterInsert = true;\n\t\t\t}\n\t\t}\n\n\t\tprivate void select() {\n\t\t\tout();\n\t\t\tthis.indent += 1;\n\t\t\tnewline();\n\t\t\tthis.parenCounts.addLast(this.parensSinceSelect);\n\t\t\tthis.afterByOrFromOrSelects.addLast(this.afterByOrSetOrFromOrSelect);\n\t\t\tthis.parensSinceSelect = 0;\n\t\t\tthis.afterByOrSetOrFromOrSelect = true;\n\t\t}\n\n\t\tprivate void out() {\n\t\t\tthis.result.append(this.token);\n\t\t}\n\n\t\tprivate void endNewClause() {\n\t\t\tif (!this.afterBeginBeforeEnd) {\n\t\t\t\tthis.indent -= 1;\n\t\t\t\tif (this.afterOn) {\n\t\t\t\t\tthis.indent -= 1;\n\t\t\t\t\tthis.afterOn = false;\n\t\t\t\t}\n\t\t\t\tnewline();\n\t\t\t}\n\t\t\tout();\n\t\t\tif (!\"union\".equals(this.lcToken)) {\n\t\t\t\tthis.indent += 1;\n\t\t\t}\n\t\t\tnewline();\n\t\t\tthis.afterBeginBeforeEnd = false;\n\t\t\tthis.afterByOrSetOrFromOrSelect = ((\"by\".equals(this.lcToken)) || (\"set\".equals(this.lcToken)) || (\"from\".equals(this.lcToken)));\n\t\t}\n\n\t\tprivate void beginNewClause() {\n\t\t\tif (!this.afterBeginBeforeEnd) {\n\t\t\t\tif (this.afterOn) {\n\t\t\t\t\tthis.indent -= 1;\n\t\t\t\t\tthis.afterOn = false;\n\t\t\t\t}\n\t\t\t\tthis.indent -= 1;\n\t\t\t\tnewline();\n\t\t\t}\n\t\t\tout();\n\t\t\tthis.beginLine = false;\n\t\t\tthis.afterBeginBeforeEnd = true;\n\t\t}\n\n\t\tprivate void values() {\n\t\t\tthis.indent -= 1;\n\t\t\tnewline();\n\t\t\tout();\n\t\t\tthis.indent += 1;\n\t\t\tnewline();\n//\t\t\tthis.afterValues = true;\n\t\t}\n\n\t\tprivate void closeParen() {\n\t\t\tthis.parensSinceSelect -= 1;\n\t\t\tif (this.parensSinceSelect < 0) {\n\t\t\t\tthis.indent -= 1;\n\t\t\t\tthis.parensSinceSelect = this.parenCounts.removeLast();\n\t\t\t\tthis.afterByOrSetOrFromOrSelect = this.afterByOrFromOrSelects.removeLast();\n\t\t\t}\n\t\t\tif (this.inFunction > 0) {\n\t\t\t\tthis.inFunction -= 1;\n\t\t\t} else {\n\t\t\t\tif (!this.afterByOrSetOrFromOrSelect) {\n\t\t\t\t\tthis.indent -= 1;\n\t\t\t\t\tnewline();\n\t\t\t\t}\n\t\t\t}\n\t\t\tout();\n\t\t\tthis.beginLine = false;\n\t\t}\n\n\t\tprivate void openParen() {\n\t\t\tif ((isFunctionName(this.lastToken)) || (this.inFunction > 0)) {\n\t\t\t\tthis.inFunction += 1;\n\t\t\t}\n\t\t\tthis.beginLine = false;\n\t\t\tif (this.inFunction > 0) {\n\t\t\t\tout();\n\t\t\t} else {\n\t\t\t\tout();\n\t\t\t\tif (!this.afterByOrSetOrFromOrSelect) {\n\t\t\t\t\tthis.indent += 1;\n\t\t\t\t\tnewline();\n\t\t\t\t\tthis.beginLine = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.parensSinceSelect += 1;\n\t\t}\n\n\t\tprivate static boolean isFunctionName(String tok) {\n\t\t\tif(StrUtil.isEmpty(tok)){\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tchar begin = tok.charAt(0);\n\t\t\tboolean isIdentifier = (Character.isJavaIdentifierStart(begin)) || ('\"' == begin);\n\t\t\treturn (isIdentifier) && (!LOGICAL.contains(tok)) && (!END_CLAUSES.contains(tok)) && (!QUANTIFIERS.contains(tok)) && (!DML.contains(tok)) && (!MISC.contains(tok));\n\t\t}\n\n\t\tprivate static boolean isWhitespace(String token) {\n\t\t\treturn \" \\n\\r\\f\\t\".contains(token);\n\t\t}\n\n\t\tprivate void newline() {\n\t\t\tthis.result.append(\"\\n\");\n\t\t\tfor (int i = 0; i < this.indent; i++) {\n\t\t\t\tthis.result.append(indentString);\n\t\t\t}\n\t\t\tthis.beginLine = true;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/SqlLog.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.level.Level;\n\n/**\n * SQL在日志中打印配置\n *\n * @author looly\n * @since 4.1.0\n */\npublic enum SqlLog {\n\tINSTANCE;\n\n\t/**\n\t * 配置文件中配置属性名：是否显示SQL\n\t */\n\tpublic static final String KEY_SHOW_SQL = \"showSql\";\n\t/**\n\t * 配置文件中配置属性名：是否格式化SQL\n\t */\n\tpublic static final String KEY_FORMAT_SQL = \"formatSql\";\n\t/**\n\t * 配置文件中配置属性名：是否显示参数\n\t */\n\tpublic static final String KEY_SHOW_PARAMS = \"showParams\";\n\t/**\n\t * 配置文件中配置属性名：显示的日志级别\n\t */\n\tpublic static final String KEY_SQL_LEVEL = \"sqlLevel\";\n\n\tprivate final static Log log = LogFactory.get();\n\n\t/** 是否debugSQL */\n\tprivate boolean showSql;\n\t/** 是否格式化SQL */\n\tprivate boolean formatSql;\n\t/** 是否显示参数 */\n\tprivate boolean showParams;\n\t/** 默认日志级别 */\n\tprivate Level level = Level.DEBUG;\n\n\t/**\n\t * 设置全局配置：是否通过debug日志显示SQL\n\t *\n\t * @param isShowSql 是否显示SQL\n\t * @param isFormatSql 是否格式化显示的SQL\n\t * @param isShowParams 是否打印参数\n\t * @param level 日志级别\n\t */\n\tpublic void init(boolean isShowSql, boolean isFormatSql, boolean isShowParams, Level level) {\n\t\tthis.showSql = isShowSql;\n\t\tthis.formatSql = isFormatSql;\n\t\tthis.showParams = isShowParams;\n\t\tthis.level = level;\n\t}\n\n\t/**\n\t * 打印SQL日志\n\t *\n\t * @param sql SQL语句\n\t * @since 4.6.7\n\t */\n\tpublic void log(String sql) {\n\t\tlog(sql, null);\n\t}\n\n\t/**\n\t * 打印批量 SQL日志\n\t *\n\t * @param sql SQL语句\n\t * @since 4.6.7\n\t */\n\tpublic void logForBatch(String sql) {\n\t\tif (this.showSql) {\n\t\t\tlog.log(this.level, \"\\n[Batch SQL] -> {}\", this.formatSql ? SqlFormatter.format(sql) : sql);\n\t\t}\n\t}\n\n\t/**\n\t * 打印SQL日志\n\t *\n\t * @param sql SQL语句\n\t * @param paramValues 参数，可为null\n\t */\n\tpublic void log(String sql, Object paramValues) {\n\t\tif (this.showSql) {\n\t\t\tif (null != paramValues && this.showParams) {\n\t\t\t\tlog.log(this.level, \"\\n[SQL] -> {}\\nParams -> {}\", this.formatSql ? SqlFormatter.format(sql) : sql, paramValues);\n\t\t\t} else {\n\t\t\t\tlog.log(this.level, \"\\n[SQL] -> {}\", this.formatSql ? SqlFormatter.format(sql) : sql);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/SqlUtil.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.Entity;\nimport cn.hutool.db.sql.Condition.LikeType;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Reader;\nimport java.nio.charset.Charset;\nimport java.sql.*;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.regex.Pattern;\n\n/**\n * SQL相关工具类，包括相关SQL语句拼接等\n *\n * @author looly\n * @since 4.0.10\n */\npublic class SqlUtil {\n\n\t/**\n\t * 创建SQL中的order by语句的正则\n\t */\n\tprivate static final Pattern PATTERN_ORDER_BY = PatternPool.get(\"(.*)\\\\s+order\\\\s+by\\\\s+[^\\\\s]+\", Pattern.CASE_INSENSITIVE);\n\t/**\n\t * SQL中的in语句部分的正则\n\t */\n\tprivate static final Pattern PATTERN_IN_CLAUSE = PatternPool.get(\"\\\\s+in\\\\s+[(]\\\\s*$\", Pattern.CASE_INSENSITIVE);\n\n\t/**\n\t * 构件相等条件的where语句<br>\n\t * 如果没有条件语句，泽返回空串，表示没有条件\n\t *\n\t * @param entity      条件实体\n\t * @param paramValues 条件值得存放List\n\t * @return 带where关键字的SQL部分\n\t */\n\tpublic static String buildEqualsWhere(Entity entity, List<Object> paramValues) {\n\t\tif (null == entity || entity.isEmpty()) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder(\" WHERE \");\n\t\tboolean isNotFirst = false;\n\t\tfor (Entry<String, Object> entry : entity.entrySet()) {\n\t\t\tif (isNotFirst) {\n\t\t\t\tsb.append(\" and \");\n\t\t\t} else {\n\t\t\t\tisNotFirst = true;\n\t\t\t}\n\t\t\tsb.append(\"`\").append(entry.getKey()).append(\"`\").append(\" = ?\");\n\t\t\tparamValues.add(entry.getValue());\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 通过实体对象构建条件对象\n\t *\n\t * @param entity 实体对象\n\t * @return 条件对象\n\t */\n\tpublic static Condition[] buildConditions(Entity entity) {\n\t\tif (null == entity || entity.isEmpty()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Condition[] conditions = new Condition[entity.size()];\n\t\tint i = 0;\n\t\tObject value;\n\t\tfor (Entry<String, Object> entry : entity.entrySet()) {\n\t\t\tvalue = entry.getValue();\n\t\t\tif (value instanceof Condition) {\n\t\t\t\tconditions[i++] = (Condition) value;\n\t\t\t} else {\n\t\t\t\tconditions[i++] = new Condition(entry.getKey(), value);\n\t\t\t}\n\t\t}\n\n\t\treturn conditions;\n\t}\n\n\t/**\n\t * 创建LIKE语句中的值，创建的结果为：\n\t *\n\t * <pre>\n\t * 1、LikeType.StartWith: '%value'\n\t * 2、LikeType.EndWith: 'value%'\n\t * 3、LikeType.Contains: '%value%'\n\t * </pre>\n\t * <p>\n\t * 如果withLikeKeyword为true，则结果为：\n\t *\n\t * <pre>\n\t * 1、LikeType.StartWith: LIKE '%value'\n\t * 2、LikeType.EndWith: LIKE 'value%'\n\t * 3、LikeType.Contains: LIKE '%value%'\n\t * </pre>\n\t *\n\t * @param value           被查找值\n\t * @param likeType        LIKE值类型 {@link LikeType}\n\t * @param withLikeKeyword 是否包含LIKE关键字\n\t * @return 拼接后的like值\n\t */\n\tpublic static String buildLikeValue(String value, LikeType likeType, boolean withLikeKeyword) {\n\t\tif (null == value) {\n\t\t\treturn null;\n\t\t}\n\n\t\tStringBuilder likeValue = StrUtil.builder(withLikeKeyword ? \"LIKE \" : \"\");\n\t\tswitch (likeType) {\n\t\t\tcase StartWith:\n\t\t\t\tlikeValue.append(value).append('%');\n\t\t\t\tbreak;\n\t\t\tcase EndWith:\n\t\t\t\tlikeValue.append('%').append(value);\n\t\t\t\tbreak;\n\t\t\tcase Contains:\n\t\t\t\tlikeValue.append('%').append(value).append('%');\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\treturn likeValue.toString();\n\t}\n\n\t/**\n\t * 格式化SQL\n\t *\n\t * @param sql SQL\n\t * @return 格式化后的SQL\n\t */\n\tpublic static String formatSql(String sql) {\n\t\treturn SqlFormatter.format(sql);\n\t}\n\n\t/**\n\t * 将RowId转为字符串\n\t *\n\t * @param rowId RowId\n\t * @return RowId字符串\n\t */\n\tpublic static String rowIdToString(RowId rowId) {\n\t\treturn StrUtil.str(rowId.getBytes(), CharsetUtil.CHARSET_ISO_8859_1);\n\t}\n\n\t/**\n\t * Clob字段值转字符串\n\t *\n\t * @param clob {@link Clob}\n\t * @return 字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String clobToStr(Clob clob) {\n\t\tReader reader = null;\n\t\ttry {\n\t\t\treader = clob.getCharacterStream();\n\t\t\treturn IoUtil.read(reader);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t}\n\n\t/**\n\t * Blob字段值转字符串\n\t *\n\t * @param blob    {@link Blob}\n\t * @param charset 编码\n\t * @return 字符串\n\t * @since 3.0.6\n\t */\n\tpublic static String blobToStr(Blob blob, Charset charset) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = blob.getBinaryStream();\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 创建Blob对象\n\t *\n\t * @param conn          {@link Connection}\n\t * @param dataStream    数据流，使用完毕后关闭\n\t * @param closeAfterUse 使用完毕是否关闭流\n\t * @return {@link Blob}\n\t * @since 4.5.13\n\t */\n\tpublic static Blob createBlob(Connection conn, InputStream dataStream, boolean closeAfterUse) {\n\t\tBlob blob;\n\t\tOutputStream out = null;\n\t\ttry {\n\t\t\tblob = conn.createBlob();\n\t\t\tout = blob.setBinaryStream(1);\n\t\t\tIoUtil.copy(dataStream, out);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tif (closeAfterUse) {\n\t\t\t\tIoUtil.close(dataStream);\n\t\t\t}\n\t\t}\n\t\treturn blob;\n\t}\n\n\t/**\n\t * 创建Blob对象\n\t *\n\t * @param conn {@link Connection}\n\t * @param data 数据\n\t * @return {@link Blob}\n\t * @since 4.5.13\n\t */\n\tpublic static Blob createBlob(Connection conn, byte[] data) {\n\t\tBlob blob;\n\t\ttry {\n\t\t\tblob = conn.createBlob();\n\t\t\tblob.setBytes(0, data);\n\t\t} catch (SQLException e) {\n\t\t\tthrow new DbRuntimeException(e);\n\t\t}\n\t\treturn blob;\n\t}\n\n\t/**\n\t * 转换为{@link java.sql.Date}\n\t *\n\t * @param date {@link java.util.Date}\n\t * @return {@link java.sql.Date}\n\t * @since 3.1.2\n\t */\n\tpublic static java.sql.Date toSqlDate(java.util.Date date) {\n\t\treturn new java.sql.Date(date.getTime());\n\t}\n\n\t/**\n\t * 转换为{@link java.sql.Timestamp}\n\t *\n\t * @param date {@link java.util.Date}\n\t * @return {@link java.sql.Timestamp}\n\t * @since 3.1.2\n\t */\n\tpublic static java.sql.Timestamp toSqlTimestamp(java.util.Date date) {\n\t\treturn new java.sql.Timestamp(date.getTime());\n\t}\n\n\t/**\n\t * 移除 SQL中的 ORDER BY 子句\n\t *\n\t * @param selectSql 原始 SQL\n\t * @return 移除 ORDER BY 子句后的 SQL\n\t * @since 5.8.41\n\t */\n\tpublic static String removeOuterOrderBy(final String selectSql) {\n\t\t// 去除order by 子句\n\t\tString sql = ReUtil.getGroup1(PATTERN_ORDER_BY, selectSql);\n\t\treturn sql == null ? selectSql : sql;\n\t}\n\n\t/**\n\t * 判断当前上下文是否在 IN 子句中\n\t * 通过检查变量前的SQL文本，判断是否符合 IN 子句的模式\n\t *\n\t * @param sql 当前已构建的SQL\n\t * @return 是否在 IN 子句中\n\t * @since 5.8.41\n\t */\n\tpublic static boolean isInClause(final CharSequence sql) {\n\t\treturn ReUtil.contains(PATTERN_IN_CLAUSE, sql);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/StatementWrapper.java",
    "content": "package cn.hutool.db.sql;\n\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.math.BigDecimal;\nimport java.net.URL;\nimport java.sql.Array;\nimport java.sql.Blob;\nimport java.sql.Clob;\nimport java.sql.Connection;\nimport java.sql.Date;\nimport java.sql.NClob;\nimport java.sql.ParameterMetaData;\nimport java.sql.PreparedStatement;\nimport java.sql.Ref;\nimport java.sql.ResultSet;\nimport java.sql.ResultSetMetaData;\nimport java.sql.RowId;\nimport java.sql.SQLException;\nimport java.sql.SQLWarning;\nimport java.sql.SQLXML;\nimport java.sql.Time;\nimport java.sql.Timestamp;\nimport java.util.Calendar;\n\n/**\n * {@link PreparedStatement} 包装类，用于添加拦截方法功能<br>\n * 拦截方法包括：\n *\n * <pre>\n * 1. 提供参数注入\n * 2. 提供SQL打印日志拦截\n * </pre>\n *\n * @author looly\n * @since 4.1.0\n */\npublic class StatementWrapper implements PreparedStatement {\n\n\tprivate PreparedStatement rawStatement;\n\n\t/**\n\t * 构造\n\t *\n\t * @param rawStatement {@link PreparedStatement}\n\t */\n\tpublic StatementWrapper(PreparedStatement rawStatement) {\n\t\tthis.rawStatement = rawStatement;\n\t}\n\n\t@Override\n\tpublic ResultSet executeQuery(String sql) throws SQLException {\n\t\treturn rawStatement.executeQuery(sql);\n\t}\n\n\t@Override\n\tpublic int executeUpdate(String sql) throws SQLException {\n\t\treturn rawStatement.executeUpdate(sql);\n\t}\n\n\t@Override\n\tpublic void close() throws SQLException {\n\t\trawStatement.close();\n\t}\n\n\t@Override\n\tpublic int getMaxFieldSize() throws SQLException {\n\t\treturn rawStatement.getMaxFieldSize();\n\t}\n\n\t@Override\n\tpublic void setMaxFieldSize(int max) throws SQLException {\n\t\trawStatement.setMaxFieldSize(max);\n\t}\n\n\t@Override\n\tpublic int getMaxRows() throws SQLException {\n\t\treturn rawStatement.getMaxRows();\n\t}\n\n\t@Override\n\tpublic void setMaxRows(int max) throws SQLException {\n\t\trawStatement.setMaxRows(max);\n\t}\n\n\t@Override\n\tpublic void setEscapeProcessing(boolean enable) throws SQLException {\n\t\trawStatement.setEscapeProcessing(enable);\n\t}\n\n\t@Override\n\tpublic int getQueryTimeout() throws SQLException {\n\t\treturn rawStatement.getQueryTimeout();\n\t}\n\n\t@Override\n\tpublic void setQueryTimeout(int seconds) throws SQLException {\n\t\trawStatement.setQueryTimeout(seconds);\n\t}\n\n\t@Override\n\tpublic void cancel() throws SQLException {\n\t\trawStatement.cancel();\n\t}\n\n\t@Override\n\tpublic SQLWarning getWarnings() throws SQLException {\n\t\treturn rawStatement.getWarnings();\n\t}\n\n\t@Override\n\tpublic void clearWarnings() throws SQLException {\n\t\trawStatement.clearWarnings();\n\t}\n\n\t@Override\n\tpublic void setCursorName(String name) throws SQLException {\n\t\trawStatement.setCursorName(name);\n\t}\n\n\t@Override\n\tpublic boolean execute(String sql) throws SQLException {\n\t\treturn rawStatement.execute(sql);\n\t}\n\n\t@Override\n\tpublic ResultSet getResultSet() throws SQLException {\n\t\treturn rawStatement.getResultSet();\n\t}\n\n\t@Override\n\tpublic int getUpdateCount() throws SQLException {\n\t\treturn rawStatement.getUpdateCount();\n\t}\n\n\t@Override\n\tpublic boolean getMoreResults() throws SQLException {\n\t\treturn rawStatement.getMoreResults();\n\t}\n\n\t@Override\n\tpublic void setFetchDirection(int direction) throws SQLException {\n\t\trawStatement.setFetchDirection(direction);\n\t}\n\n\t@Override\n\tpublic int getFetchDirection() throws SQLException {\n\t\treturn rawStatement.getFetchDirection();\n\t}\n\n\t@Override\n\tpublic void setFetchSize(int rows) throws SQLException {\n\t\trawStatement.setFetchSize(rows);\n\t}\n\n\t@Override\n\tpublic int getFetchSize() throws SQLException {\n\t\treturn rawStatement.getFetchSize();\n\t}\n\n\t@Override\n\tpublic int getResultSetConcurrency() throws SQLException {\n\t\treturn rawStatement.getResultSetConcurrency();\n\t}\n\n\t@Override\n\tpublic int getResultSetType() throws SQLException {\n\t\treturn rawStatement.getResultSetType();\n\t}\n\n\t@Override\n\tpublic void addBatch(String sql) throws SQLException {\n\t\trawStatement.addBatch(sql);\n\t}\n\n\t@Override\n\tpublic void clearBatch() throws SQLException {\n\t\trawStatement.clearBatch();\n\t}\n\n\t@Override\n\tpublic int[] executeBatch() throws SQLException {\n\t\treturn rawStatement.executeBatch();\n\t}\n\n\t@Override\n\tpublic Connection getConnection() throws SQLException {\n\t\treturn rawStatement.getConnection();\n\t}\n\n\t@Override\n\tpublic boolean getMoreResults(int current) throws SQLException {\n\t\treturn rawStatement.getMoreResults(current);\n\t}\n\n\t@Override\n\tpublic ResultSet getGeneratedKeys() throws SQLException {\n\t\treturn rawStatement.getGeneratedKeys();\n\t}\n\n\t@Override\n\tpublic int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {\n\t\treturn rawStatement.executeUpdate(sql, autoGeneratedKeys);\n\t}\n\n\t@Override\n\tpublic int executeUpdate(String sql, int[] columnIndexes) throws SQLException {\n\t\treturn rawStatement.executeUpdate(sql, columnIndexes);\n\t}\n\n\t@Override\n\tpublic int executeUpdate(String sql, String[] columnNames) throws SQLException {\n\t\treturn rawStatement.executeUpdate(sql, columnNames);\n\t}\n\n\t@Override\n\tpublic boolean execute(String sql, int autoGeneratedKeys) throws SQLException {\n\t\treturn rawStatement.execute(sql, autoGeneratedKeys);\n\t}\n\n\t@Override\n\tpublic boolean execute(String sql, int[] columnIndexes) throws SQLException {\n\t\treturn rawStatement.execute(sql, columnIndexes);\n\t}\n\n\t@Override\n\tpublic boolean execute(String sql, String[] columnNames) throws SQLException {\n\t\treturn rawStatement.execute(sql, columnNames);\n\t}\n\n\t@Override\n\tpublic int getResultSetHoldability() throws SQLException {\n\t\treturn rawStatement.getResultSetHoldability();\n\t}\n\n\t@Override\n\tpublic boolean isClosed() throws SQLException {\n\t\treturn rawStatement.isClosed();\n\t}\n\n\t@Override\n\tpublic void setPoolable(boolean poolable) throws SQLException {\n\t\trawStatement.setPoolable(poolable);\n\t}\n\n\t@Override\n\tpublic boolean isPoolable() throws SQLException {\n\t\treturn rawStatement.isPoolable();\n\t}\n\n\t@Override\n\tpublic void closeOnCompletion() throws SQLException {\n\t\trawStatement.closeOnCompletion();\n\t}\n\n\t@Override\n\tpublic boolean isCloseOnCompletion() throws SQLException {\n\t\treturn rawStatement.isCloseOnCompletion();\n\t}\n\n\t@Override\n\tpublic <T> T unwrap(Class<T> iface) throws SQLException {\n\t\treturn rawStatement.unwrap(iface);\n\t}\n\n\t@Override\n\tpublic boolean isWrapperFor(Class<?> iface) throws SQLException {\n\t\treturn rawStatement.isWrapperFor(iface);\n\t}\n\n\t@Override\n\tpublic ResultSet executeQuery() throws SQLException {\n\t\treturn rawStatement.executeQuery();\n\t}\n\n\t@Override\n\tpublic int executeUpdate() throws SQLException {\n\t\treturn rawStatement.executeUpdate();\n\t}\n\n\t@Override\n\tpublic void setNull(int parameterIndex, int sqlType) throws SQLException {\n\t\trawStatement.setNull(parameterIndex, sqlType);\n\t}\n\n\t@Override\n\tpublic void setBoolean(int parameterIndex, boolean x) throws SQLException {\n\t\trawStatement.setBoolean(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setByte(int parameterIndex, byte x) throws SQLException {\n\t\trawStatement.setByte(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setShort(int parameterIndex, short x) throws SQLException {\n\t\trawStatement.setShort(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setInt(int parameterIndex, int x) throws SQLException {\n\t\trawStatement.setInt(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setLong(int parameterIndex, long x) throws SQLException {\n\t\trawStatement.setLong(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setFloat(int parameterIndex, float x) throws SQLException {\n\t\trawStatement.setFloat(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setDouble(int parameterIndex, double x) throws SQLException {\n\t\trawStatement.setDouble(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {\n\t\trawStatement.setBigDecimal(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setString(int parameterIndex, String x) throws SQLException {\n\t\trawStatement.setString(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setBytes(int parameterIndex, byte[] x) throws SQLException {\n\t\trawStatement.setBytes(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setDate(int parameterIndex, Date x) throws SQLException {\n\t\trawStatement.setDate(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setTime(int parameterIndex, Time x) throws SQLException {\n\t\trawStatement.setTime(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {\n\t\trawStatement.setTimestamp(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {\n\t\trawStatement.setAsciiStream(parameterIndex, x, length);\n\t}\n\n\t@Override\n\t@Deprecated\n\tpublic void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {\n\t\trawStatement.setUnicodeStream(parameterIndex, x, length);\n\t}\n\n\t@Override\n\tpublic void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {\n\t\trawStatement.setBinaryStream(parameterIndex, x, length);\n\t}\n\n\t@Override\n\tpublic void clearParameters() throws SQLException {\n\t\trawStatement.clearParameters();\n\t}\n\n\t@Override\n\tpublic void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {\n\t\trawStatement.setObject(parameterIndex, x, targetSqlType, targetSqlType);\n\t}\n\n\t@Override\n\tpublic void setObject(int parameterIndex, Object x) throws SQLException {\n\t\trawStatement.setObject(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic boolean execute() throws SQLException {\n\t\treturn rawStatement.execute();\n\t}\n\n\t@Override\n\tpublic void addBatch() throws SQLException {\n\t\trawStatement.addBatch();\n\t}\n\n\t@Override\n\tpublic void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {\n\t\trawStatement.setCharacterStream(parameterIndex, reader, length);\n\t}\n\n\t@Override\n\tpublic void setRef(int parameterIndex, Ref x) throws SQLException {\n\t\trawStatement.setRef(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setBlob(int parameterIndex, Blob x) throws SQLException {\n\t\trawStatement.setBlob(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setClob(int parameterIndex, Clob x) throws SQLException {\n\t\trawStatement.setClob(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setArray(int parameterIndex, Array x) throws SQLException {\n\t\trawStatement.setArray(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic ResultSetMetaData getMetaData() throws SQLException {\n\t\treturn rawStatement.getMetaData();\n\t}\n\n\t@Override\n\tpublic void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {\n\t\trawStatement.setDate(parameterIndex, x, cal);\n\t}\n\n\t@Override\n\tpublic void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {\n\t\trawStatement.setTime(parameterIndex, x, cal);\n\t}\n\n\t@Override\n\tpublic void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {\n\t\trawStatement.setTimestamp(parameterIndex, x, cal);\n\t}\n\n\t@Override\n\tpublic void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {\n\t\trawStatement.setNull(parameterIndex, sqlType, typeName);\n\t}\n\n\t@Override\n\tpublic void setURL(int parameterIndex, URL x) throws SQLException {\n\t\trawStatement.setURL(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic ParameterMetaData getParameterMetaData() throws SQLException {\n\t\treturn rawStatement.getParameterMetaData();\n\t}\n\n\t@Override\n\tpublic void setRowId(int parameterIndex, RowId x) throws SQLException {\n\t\trawStatement.setRowId(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setNString(int parameterIndex, String value) throws SQLException {\n\t\trawStatement.setNString(parameterIndex, value);\n\t}\n\n\t@Override\n\tpublic void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {\n\t\trawStatement.setCharacterStream(parameterIndex, value, length);\n\t}\n\n\t@Override\n\tpublic void setNClob(int parameterIndex, NClob value) throws SQLException {\n\t\trawStatement.setNClob(parameterIndex, value);\n\t}\n\n\t@Override\n\tpublic void setClob(int parameterIndex, Reader reader, long length) throws SQLException {\n\t\trawStatement.setClob(parameterIndex, reader, length);\n\t}\n\n\t@Override\n\tpublic void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {\n\t\trawStatement.setBlob(parameterIndex, inputStream, length);\n\t}\n\n\t@Override\n\tpublic void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {\n\t\trawStatement.setNClob(parameterIndex, reader, length);\n\t}\n\n\t@Override\n\tpublic void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {\n\t\trawStatement.setSQLXML(parameterIndex, xmlObject);\n\t}\n\n\t@Override\n\tpublic void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {\n\t\trawStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength);\n\t}\n\n\t@Override\n\tpublic void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {\n\t\trawStatement.setAsciiStream(parameterIndex, x, length);\n\t}\n\n\t@Override\n\tpublic void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {\n\t\trawStatement.setBinaryStream(parameterIndex, x, length);\n\t}\n\n\t@Override\n\tpublic void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {\n\t\trawStatement.setCharacterStream(parameterIndex, reader, length);\n\t}\n\n\t@Override\n\tpublic void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {\n\t\trawStatement.setAsciiStream(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {\n\t\trawStatement.setBinaryStream(parameterIndex, x);\n\t}\n\n\t@Override\n\tpublic void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {\n\t\trawStatement.setCharacterStream(parameterIndex, reader);\n\t}\n\n\t@Override\n\tpublic void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {\n\t\trawStatement.setNCharacterStream(parameterIndex, value);\n\t}\n\n\t@Override\n\tpublic void setClob(int parameterIndex, Reader reader) throws SQLException {\n\t\trawStatement.setClob(parameterIndex, reader);\n\t}\n\n\t@Override\n\tpublic void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {\n\t\trawStatement.setBlob(parameterIndex, inputStream);\n\t}\n\n\t@Override\n\tpublic void setNClob(int parameterIndex, Reader reader) throws SQLException {\n\t\trawStatement.setNClob(parameterIndex, reader);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.Entity;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Map.Entry;\n\n/**\n * 包装器<br>\n * 主要用于字段名的包装（在字段名的前后加字符，例如反引号来避免与数据库的关键字冲突）\n *\n * @author Looly\n */\npublic class Wrapper implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 前置包装符号\n\t */\n\tprivate Character preWrapQuote;\n\t/**\n\t * 后置包装符号\n\t */\n\tprivate Character sufWrapQuote;\n\n\tpublic Wrapper() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param wrapQuote 单包装字符\n\t */\n\tpublic Wrapper(Character wrapQuote) {\n\t\tthis.preWrapQuote = wrapQuote;\n\t\tthis.sufWrapQuote = wrapQuote;\n\t}\n\n\t/**\n\t * 包装符号\n\t *\n\t * @param preWrapQuote 前置包装符号\n\t * @param sufWrapQuote 后置包装符号\n\t */\n\tpublic Wrapper(Character preWrapQuote, Character sufWrapQuote) {\n\t\tthis.preWrapQuote = preWrapQuote;\n\t\tthis.sufWrapQuote = sufWrapQuote;\n\t}\n\n\t//--------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * @return 前置包装符号\n\t */\n\tpublic char getPreWrapQuote() {\n\t\treturn preWrapQuote;\n\t}\n\n\t/**\n\t * 设置前置包装的符号\n\t *\n\t * @param preWrapQuote 前置包装符号\n\t */\n\tpublic void setPreWrapQuote(Character preWrapQuote) {\n\t\tthis.preWrapQuote = preWrapQuote;\n\t}\n\n\t/**\n\t * @return 后置包装符号\n\t */\n\tpublic char getSufWrapQuote() {\n\t\treturn sufWrapQuote;\n\t}\n\n\t/**\n\t * 设置后置包装的符号\n\t *\n\t * @param sufWrapQuote 后置包装符号\n\t */\n\tpublic void setSufWrapQuote(Character sufWrapQuote) {\n\t\tthis.sufWrapQuote = sufWrapQuote;\n\t}\n\t//--------------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 包装字段名<br>\n\t * 有时字段与SQL的某些关键字冲突，导致SQL出错，因此需要将字段名用单引号或者反引号包装起来，避免冲突\n\t *\n\t * @param field 字段名\n\t * @return 包装后的字段名\n\t */\n\tpublic String wrap(String field) {\n\t\tif (preWrapQuote == null || sufWrapQuote == null || StrUtil.isBlank(field)) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//如果已经包含包装的引号，返回原字符\n\t\tif (StrUtil.isSurround(field, preWrapQuote, sufWrapQuote)) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//如果字段中包含通配符或者括号（字段通配符或者函数），不做包装\n\t\tif (StrUtil.containsAnyIgnoreCase(field, \"*\", \"(\", \" \", \" as \")) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//对于Oracle这类数据库，表名中包含用户名需要单独拆分包装\n\t\tif (field.contains(StrUtil.DOT)) {\n\t\t\tfinal Collection<String> target = CollUtil.edit(StrUtil.split(field, CharUtil.DOT, 2), t -> StrUtil.format(\"{}{}{}\", preWrapQuote, t, sufWrapQuote));\n\t\t\treturn CollectionUtil.join(target, StrUtil.DOT);\n\t\t}\n\n\t\treturn StrUtil.format(\"{}{}{}\", preWrapQuote, field, sufWrapQuote);\n\t}\n\n\t/**\n\t * 解包装字段名<br>\n\t *\n\t * @param field 字段名\n\t * @return 未包装的字段名\n\t * @since 5.8.27\n\t */\n\tpublic String unWrap(String field) {\n\t\tif (preWrapQuote == null || sufWrapQuote == null || StrUtil.isBlank(field)) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//如果已经包含包装的引号，返回原字符\n\t\tif (!StrUtil.isSurround(field, preWrapQuote, sufWrapQuote)) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//如果字段中包含通配符或者括号（字段通配符或者函数），不做解包装\n\t\tif (StrUtil.containsAnyIgnoreCase(field, \"*\", \"(\", \" \", \" as \")) {\n\t\t\treturn field;\n\t\t}\n\n\t\t//对于Oracle这类数据库，表名中包含用户名需要单独拆分包装\n\t\tif (field.contains(StrUtil.DOT)) {\n\t\t\tfinal Collection<String> target = CollUtil.edit(StrUtil.split(field, CharUtil.DOT, 2), t -> StrUtil.unWrap(t, preWrapQuote, sufWrapQuote));\n\t\t\treturn CollectionUtil.join(target, StrUtil.DOT);\n\t\t}\n\n\t\treturn StrUtil.unWrap(field, preWrapQuote, sufWrapQuote);\n\t}\n\n\t/**\n\t * 包装字段名<br>\n\t * 有时字段与SQL的某些关键字冲突，导致SQL出错，因此需要将字段名用单引号或者反引号包装起来，避免冲突\n\t *\n\t * @param fields 字段名\n\t * @return 包装后的字段名\n\t */\n\tpublic String[] wrap(String... fields) {\n\t\tif (ArrayUtil.isEmpty(fields)) {\n\t\t\treturn fields;\n\t\t}\n\n\t\tString[] wrappedFields = new String[fields.length];\n\t\tfor (int i = 0; i < fields.length; i++) {\n\t\t\twrappedFields[i] = wrap(fields[i]);\n\t\t}\n\n\t\treturn wrappedFields;\n\t}\n\n\t/**\n\t * 包装字段名<br>\n\t * 有时字段与SQL的某些关键字冲突，导致SQL出错，因此需要将字段名用单引号或者反引号包装起来，避免冲突\n\t *\n\t * @param fields 字段名\n\t * @return 包装后的字段名\n\t */\n\tpublic Collection<String> wrap(Collection<String> fields) {\n\t\tif (CollectionUtil.isEmpty(fields)) {\n\t\t\treturn fields;\n\t\t}\n\n\t\treturn Arrays.asList(wrap(fields.toArray(new String[0])));\n\t}\n\n\t/**\n\t * 包装表名和字段名，此方法返回一个新的Entity实体类<br>\n\t * 有时字段与SQL的某些关键字冲突，导致SQL出错，因此需要将字段名用单引号或者反引号包装起来，避免冲突\n\t *\n\t * @param entity 被包装的实体\n\t * @return 新的实体\n\t */\n\tpublic Entity wrap(Entity entity) {\n\t\tif (null == entity) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal Entity wrapedEntity = new Entity();\n\n\t\t//wrap table name\n\t\twrapedEntity.setTableName(wrap(entity.getTableName()));\n\n\t\t//wrap fields\n\t\tfor (Entry<String, Object> entry : entity.entrySet()) {\n\t\t\twrapedEntity.set(wrap(entry.getKey()), entry.getValue());\n\t\t}\n\n\t\treturn wrapedEntity;\n\t}\n\n\t/**\n\t * 包装字段名<br>\n\t * 有时字段与SQL的某些关键字冲突，导致SQL出错，因此需要将字段名用单引号或者反引号包装起来，避免冲突\n\t *\n\t * @param conditions 被包装的实体\n\t * @return 包装后的字段名\n\t */\n\tpublic Condition[] wrap(Condition... conditions) {\n\t\tfinal Condition[] clonedConditions = new Condition[conditions.length];\n\t\tif (ArrayUtil.isNotEmpty(conditions)) {\n\t\t\tCondition clonedCondition;\n\t\t\tfor (int i = 0; i < conditions.length; i++) {\n\t\t\t\tclonedCondition = conditions[i].clone();\n\t\t\t\tclonedCondition.setField(wrap(clonedCondition.getField()));\n\t\t\t\tclonedConditions[i] = clonedCondition;\n\t\t\t}\n\t\t}\n\n\t\treturn clonedConditions;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/sql/package-info.java",
    "content": "/**\n * SQL语句和Statement构建封装\n * \n * @author looly\n *\n */\npackage cn.hutool.db.sql;"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/transaction/TransactionLevel.java",
    "content": "package cn.hutool.db.transaction;\n\nimport java.sql.Connection;\n\n/**\n * 事务级别枚举\n * \n * <p>\n * <b>脏读（Dirty Read）</b>：<br>\n * 一个事务会读到另一个事务更新后但未提交的数据，如果另一个事务回滚，那么当前事务读到的数据就是脏数据\n * <p>\n * <b>不可重复读（Non Repeatable Read）</b>：<br>\n * 在一个事务内，多次读同一数据，在这个事务还没有结束时，如果另一个事务恰好修改了这个数据，那么，在第一个事务中，两次读取的数据就可能不一致\n * <p>\n * <b>幻读（Phantom Read）</b>：<br>\n * 在一个事务中，第一次查询某条记录，发现没有，但是，当试图更新这条不存在的记录时，竟然能成功，且可以再次读取同一条记录。\n * \n * @see Connection#TRANSACTION_NONE\n * @see Connection#TRANSACTION_READ_UNCOMMITTED\n * @see Connection#TRANSACTION_READ_COMMITTED\n * @see Connection#TRANSACTION_REPEATABLE_READ\n * @see Connection#TRANSACTION_SERIALIZABLE\n * @author looly\n * @since 4.1.2\n */\npublic enum TransactionLevel {\n\t/** 驱动不支持事务 */\n\tNONE(Connection.TRANSACTION_NONE),\n\n\t/**\n\t * 允许脏读、不可重复读和幻读\n\t * <p>\n\t * 在这种隔离级别下，一个事务会读到另一个事务更新后但未提交的数据，如果另一个事务回滚，那么当前事务读到的数据就是脏数据，这就是脏读（Dirty Read）\n\t */\n\tREAD_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),\n\n\t/**\n\t * 禁止脏读，但允许不可重复读和幻读\n\t * <p>\n\t * 此级别下，一个事务可能会遇到不可重复读（Non Repeatable Read）的问题。<br>\n\t * 不可重复读是指，在一个事务内，多次读同一数据，在这个事务还没有结束时，如果另一个事务恰好修改了这个数据，那么，在第一个事务中，两次读取的数据就可能不一致。\n\t */\n\tREAD_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),\n\n\t/**\n\t * 禁止脏读和不可重复读，但允许幻读，MySQL的InnoDB引擎默认使用此隔离级别。\n\t * <p>\n\t * 此级别下，一个事务可能会遇到幻读（Phantom Read）的问题。<br>\n\t * 幻读是指，在一个事务中，第一次查询某条记录，发现没有，但是，当试图更新这条不存在的记录时，可以成功，且可以再次读取同一条记录。\n\t */\n\tREPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),\n\n\t/**\n\t * 禁止脏读、不可重复读和幻读\n\t * <p>\n\t * 虽然Serializable隔离级别下的事务具有最高的安全性，但是，由于事务是串行执行，所以效率会大大下降，应用程序的性能会急剧降低。\n\t */\n\tSERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);\n\n\t/** 事务级别，对应Connection中的常量值 */\n\tprivate final int level;\n\n\tTransactionLevel(int level) {\n\t\tthis.level = level;\n\t}\n\n\t/**\n\t * 获取数据库事务级别int值\n\t * \n\t * @return 数据库事务级别int值\n\t */\n\tpublic int getLevel() {\n\t\treturn this.level;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/main/java/cn/hutool/db/transaction/package-info.java",
    "content": "/**\n * 事务相关类和操作\n * \n * @author looly\n *\n */\npackage cn.hutool.db.transaction;"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/CRUDTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.db.handler.EntityListHandler;\nimport cn.hutool.db.pojo.User;\nimport cn.hutool.db.sql.Condition;\nimport cn.hutool.db.sql.Condition.LikeType;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.sql.SQLException;\nimport java.util.List;\n\n/**\n * 增删改查测试\n *\n * @author looly\n */\npublic class CRUDTest {\n\n\tprivate static final Db db = Db.use(\"test\");\n\n\t@Test\n\tpublic void findIsNullTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", \"is null\"));\n\t\tassertEquals(0, results.size());\n\t}\n\n\t@Test\n\tpublic void findIsNullTest2() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", \"= null\"));\n\t\tassertEquals(0, results.size());\n\t}\n\n\t@Test\n\tpublic void findIsNullTest3() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", null));\n\t\tassertEquals(0, results.size());\n\t}\n\n\t@Test\n\tpublic void findBetweenTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", \"between '18' and '40'\"));\n\t\tassertEquals(1, results.size());\n\t}\n\n\t@Test\n\tpublic void findByBigIntegerTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", new BigInteger(\"12\")));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findByBigDecimalTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"age\", new BigDecimal(\"12\")));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findLikeTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"name\", \"like \\\"%三%\\\"\"));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findLikeTest2() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"name\", new Condition(\"name\", \"三\", LikeType.Contains)));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findLikeTest3() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"name\", new Condition(\"name\", null, LikeType.Contains)));\n\t\tassertEquals(0, results.size());\n\t}\n\n\t@Test\n\tpublic void findInTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\").set(\"id\", \"in 1,2,3\"));\n\t\tConsole.log(results);\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findInTest2() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\")\n\t\t\t\t.set(\"id\", new Condition(\"id\", new long[]{1, 2, 3})));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findInTest3() throws SQLException {\n\t\tList<Entity> results = db.findAll(Entity.create(\"user\")\n\t\t\t\t.set(\"id\", new long[]{1, 2, 3}));\n\t\tassertEquals(2, results.size());\n\t}\n\n\t@Test\n\tpublic void findAllTest() throws SQLException {\n\t\tList<Entity> results = db.findAll(\"user\");\n\t\tassertEquals(4, results.size());\n\t}\n\n\t@Test\n\tpublic void findTest() throws SQLException {\n\t\tList<Entity> find = db.find(CollUtil.newArrayList(\"name AS name2\"), Entity.create(\"user\"), EntityListHandler.create());\n\t\tassertFalse(find.isEmpty());\n\t}\n\n\t@Test\n\tpublic void findActiveTest() {\n\t\tActiveEntity entity = new ActiveEntity(db, \"user\");\n\t\tentity.setFieldNames(\"name AS name2\").load();\n\t\tassertEquals(\"user\", entity.getTableName());\n\t\tassertFalse(entity.isEmpty());\n\t}\n\n\t/**\n\t * 对增删改查做单元测试\n\t *\n\t * @throws SQLException SQL异常\n\t */\n\t@Test\n\t@Disabled\n\tpublic void crudTest() throws SQLException {\n\n\t\t// 增\n\t\tLong id = db.insertForGeneratedKey(Entity.create(\"user\").set(\"name\", \"unitTestUser\").set(\"age\", 66));\n\t\tassertTrue(id > 0);\n\t\tEntity result = db.get(\"user\", \"name\", \"unitTestUser\");\n\t\tassertSame(66, result.getInt(\"age\"));\n\n\t\t// 改\n\t\tint update = db.update(Entity.create().set(\"age\", 88), Entity.create(\"user\").set(\"name\", \"unitTestUser\"));\n\t\tassertTrue(update > 0);\n\t\tEntity result2 = db.get(\"user\", \"name\", \"unitTestUser\");\n\t\tassertSame(88, result2.getInt(\"age\"));\n\n\t\t// 删\n\t\tint del = db.del(\"user\", \"name\", \"unitTestUser\");\n\t\tassertTrue(del > 0);\n\t\tEntity result3 = db.get(\"user\", \"name\", \"unitTestUser\");\n\t\tassertNull(result3);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertBatchTest() throws SQLException {\n\t\tUser user1 = new User();\n\t\tuser1.setName(\"张三\");\n\t\tuser1.setAge(12);\n\t\tuser1.setBirthday(\"19900112\");\n\t\tuser1.setGender(true);\n\n\t\tUser user2 = new User();\n\t\tuser2.setName(\"李四\");\n\t\tuser2.setAge(12);\n\t\tuser2.setBirthday(\"19890512\");\n\t\tuser2.setGender(false);\n\n\t\tEntity data1 = Entity.parse(user1);\n\t\tdata1.put(\"name\", null);\n\t\tEntity data2 = Entity.parse(user2);\n\n\t\tConsole.log(data1);\n\t\tConsole.log(data2);\n\n\t\tint[] result = db.insert(CollUtil.newArrayList(data1, data2));\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertBatchOneTest() throws SQLException {\n\t\tUser user1 = new User();\n\t\tuser1.setName(\"张三\");\n\t\tuser1.setAge(12);\n\t\tuser1.setBirthday(\"19900112\");\n\t\tuser1.setGender(true);\n\n\t\tEntity data1 = Entity.parse(user1);\n\n\t\tConsole.log(data1);\n\n\t\tint[] result = db.insert(CollUtil.newArrayList(data1));\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\tpublic void selectInTest() throws SQLException {\n\t\tfinal List<Entity> results = db.query(\"select * from user where id in (:ids)\",\n\t\t\t\tMapUtil.of(\"ids\", new int[]{1, 2, 3}));\n\t\tassertEquals(2, results.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/ConcurentTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.db.handler.EntityListHandler;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\n/**\n * SqlRunner线程安全测试\n *\n * @author looly\n *\n */\n@Disabled\npublic class ConcurentTest {\n\n\tprivate Db db;\n\n\t@BeforeEach\n\tpublic void init() {\n\t\tdb = Db.use(\"test\");\n\t}\n\n\t@Test\n\tpublic void findTest() {\n\t\tfor(int i = 0; i < 10000; i++) {\n\t\t\tThreadUtil.execute(() -> {\n\t\t\t\tList<Entity> find;\n\t\t\t\ttry {\n\t\t\t\t\tfind = db.find(CollectionUtil.newArrayList(\"name AS name2\"), Entity.create(\"user\"), EntityListHandler.create());\n\t\t\t\t} catch (SQLException e) {\n\t\t\t\t\tthrow new DbRuntimeException(e);\n\t\t\t\t}\n\t\t\t\tConsole.log(find);\n\t\t\t});\n\t\t}\n\n\t\t//主线程关闭会导致连接池销毁，sleep避免此情况引起的问题\n\t\tThreadUtil.sleep(5000);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/DbTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.db.handler.EntityListHandler;\nimport cn.hutool.db.sql.Condition;\nimport cn.hutool.log.StaticLog;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.List;\n\n/**\n * Db对象单元测试\n * @author looly\n *\n */\npublic class DbTest {\n\n\t@Test\n\tpublic void queryTest() throws SQLException {\n\t\tList<Entity> find = Db.use().query(\"select * from user where age = ?\", 18);\n\t\tassertEquals(\"王五\", find.get(0).get(\"name\"));\n\t}\n\n\t@Test\n\tpublic void findTest() throws SQLException {\n\t\tList<Entity> find = Db.use().find(Entity.create(\"user\").set(\"age\", 18));\n\t\tassertEquals(\"王五\", find.get(0).get(\"name\"));\n\t}\n\n\t@Test\n\tpublic void pageTest() throws SQLException {\n\t\t// 测试数据库中一共4条数据，第0页有3条，第1页有1条\n\t\tList<Entity> page0 = Db.use().page(Entity.create(\"user\"), 0, 3);\n\t\tassertEquals(3, page0.size());\n\t\tList<Entity> page1 = Db.use().page(Entity.create(\"user\"), 1, 3);\n\t\tassertEquals(1, page1.size());\n\t}\n\n\t@Test\n\tpublic void pageTest2() throws SQLException {\n\t\tString sql = \"select * from user order by name\";\n\t\t// 测试数据库中一共4条数据，第0页有3条，第1页有1条\n\t\tList<Entity> page0 = Db.use().page(\n\t\t\t\tsql, Page.of(0, 3));\n\t\tassertEquals(3, page0.size());\n\n\t\tList<Entity> page1 = Db.use().page(\n\t\t\t\tsql, Page.of(1, 3));\n\t\tassertEquals(1, page1.size());\n\t}\n\n\t@Test\n\tpublic void pageWithParamsTest() throws SQLException {\n\t\tString sql = \"select * from user where name = ?\";\n\t\tPageResult<Entity> result = Db.use().page(\n\t\t\t\tsql, Page.of(0, 3), \"张三\");\n\n\t\tassertEquals(2, result.getTotal());\n\t\tassertEquals(1, result.getTotalPage());\n\t\tassertEquals(2, result.size());\n\t}\n\n\t@Test\n\tpublic void countTest() throws SQLException {\n\t\tfinal long count = Db.use().count(\"select * from user\");\n\t\tassertEquals(4, count);\n\t}\n\n\t@Test\n\tpublic void countByQueryTest() throws SQLException {\n\t\tfinal long count = Db.use().count(Entity.create(\"user\"));\n\t\tassertEquals(4, count);\n\t}\n\n\t@Test\n\tpublic void countTest2() throws SQLException {\n\t\tfinal long count = Db.use().count(\"select * from user order by name DESC\");\n\t\tassertEquals(4, count);\n\t}\n\n\t@Test\n\tpublic void findLikeTest() throws SQLException {\n\t\t// 方式1\n\t\tList<Entity> find = Db.use().find(Entity.create(\"user\").set(\"name\", \"like 王%\"));\n\t\tassertEquals(\"王五\", find.get(0).get(\"name\"));\n\n\t\t// 方式2\n\t\tfind = Db.use().findLike(\"user\", \"name\", \"王\", Condition.LikeType.StartWith);\n\t\tassertEquals(\"王五\", find.get(0).get(\"name\"));\n\n\t\t// 方式3\n\t\tfind = Db.use().query(\"select * from user where name like ?\", \"王%\");\n\t\tassertEquals(\"王五\", find.get(0).get(\"name\"));\n\t}\n\n\t@Test\n\tpublic void findByTest() throws SQLException {\n\t\tList<Entity> find = Db.use().findBy(\"user\",\n\t\t\t\tCondition.parse(\"age\", \"> 18\"),\n\t\t\t\tCondition.parse(\"age\", \"< 100\")\n\t\t);\n\t\tfor (Entity entity : find) {\n\t\t\tStaticLog.debug(\"{}\", entity);\n\t\t}\n\t\tassertEquals(\"unitTestUser\", find.get(0).get(\"name\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void txTest() throws SQLException {\n\t\tDb.use().tx(db -> {\n\t\t\tdb.insert(Entity.create(\"user\").set(\"name\", \"unitTestUser2\"));\n\t\t\tdb.update(Entity.create().set(\"age\", 79), Entity.create(\"user\").set(\"name\", \"unitTestUser2\"));\n\t\t\tdb.del(\"user\", \"name\", \"unitTestUser2\");\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void queryFetchTest() throws SQLException {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4JXWN\n\t\tDb.use().query((conn->{\n\t\t\tPreparedStatement ps = conn.prepareStatement(\"select * from table\",\n\t\t\t\t\tResultSet.TYPE_FORWARD_ONLY,\n\t\t\t\t\tResultSet.CONCUR_READ_ONLY);\n\t\t\tps.setFetchSize(Integer.MIN_VALUE);\n\t\t\tps.setFetchDirection(ResultSet.FETCH_FORWARD);\n\t\t\treturn ps;\n\t\t}), EntityListHandler.create());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void findWithDotTest() throws SQLException {\n\t\tDb.use().find(Entity.create(\"user\").set(\"a.b\", \"1\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/DerbyTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Derby数据库单元测试\n *\n * @author looly\n *\n */\npublic class DerbyTest {\n\n\tprivate static final String DS_GROUP_NAME = \"derby\";\n\n\t//@BeforeAll\n\tpublic static void init() throws SQLException {\n\t\tDb db = Db.use(DS_GROUP_NAME);\n\t\ttry{\n\t\t\tdb.execute(\"CREATE TABLE test(a INTEGER, b BIGINT)\");\n\t\t}catch (SQLException e){\n\t\t\t// 数据库已存在\n\t\t\treturn;\n\t\t}\n\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 1).set(\"b\", 11));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 2).set(\"b\", 21));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 3).set(\"b\", 31));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 4).set(\"b\", 41));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void queryTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).query(\"select * from test\");\n\t\tassertEquals(4, query.size());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void findTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).find(Entity.create(\"test\"));\n\t\tassertEquals(4, query.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/DmTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 达梦数据库单元测试\n *\n * @author wb04307201\n */\npublic class DmTest {\n\n\tprivate static final String DS_GROUP_NAME = \"dm\";\n\n\t//@BeforeAll\n\tpublic static void init() throws SQLException {\n\t\tDb db = Db.use(DS_GROUP_NAME);\n\t\tdb.execute(\"CREATE TABLE test(a INTEGER, b INTEGER)\");\n\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 1).set(\"b\", 11));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 2).set(\"b\", 21));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 3).set(\"b\", 31));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 4).set(\"b\", 41));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void upsertTest() throws SQLException {\n\t\tDb db = Db.use(DS_GROUP_NAME);\n\t\tdb.upsert(Entity.create(\"test\").set(\"a\", 1).set(\"b\", 111), \"a\");\n\t\tEntity a1 = db.get(\"test\", \"a\", 1);\n\t\tassertEquals(Long.valueOf(111), a1.getLong(\"b\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/DsTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.db.ds.DataSourceWrapper;\nimport cn.hutool.db.ds.bee.BeeDSFactory;\nimport cn.hutool.db.ds.c3p0.C3p0DSFactory;\nimport cn.hutool.db.ds.dbcp.DbcpDSFactory;\nimport cn.hutool.db.ds.druid.DruidDSFactory;\nimport cn.hutool.db.ds.hikari.HikariDSFactory;\nimport cn.hutool.db.ds.pooled.PooledDSFactory;\nimport cn.hutool.db.ds.tomcat.TomcatDSFactory;\nimport com.mchange.v2.c3p0.ComboPooledDataSource;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.sql.DataSource;\nimport java.sql.SQLException;\nimport java.util.List;\n\n/**\n * 数据源单元测试\n *\n * @author Looly\n *\n */\npublic class DsTest {\n\n\t@Test\n\tpublic void defaultDsTest() throws SQLException {\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void hikariDsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new HikariDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void druidDsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new DruidDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void tomcatDsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new TomcatDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void beeCPDsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new BeeDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void dbcpDsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new DbcpDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void c3p0DsTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new C3p0DSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n\n\t@Test\n\tpublic void c3p0DsUserAndPassTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4T7XZ\n\t\tDSFactory.setCurrentDSFactory(new C3p0DSFactory());\n\t\tComboPooledDataSource ds = (ComboPooledDataSource) ((DataSourceWrapper) DSFactory.get(\"mysql\")).getRaw();\n\t\tassertEquals(\"root\", ds.getUser());\n\t\tassertEquals(\"123456\", ds.getPassword());\n\t}\n\n\t@Test\n\tpublic void hutoolPoolTest() throws SQLException {\n\t\tDSFactory.setCurrentDSFactory(new PooledDSFactory());\n\t\tDataSource ds = DSFactory.get(\"test\");\n\t\tDb db = Db.use(ds);\n\t\tList<Entity> all = db.findAll(\"user\");\n\t\tassertTrue(CollUtil.isNotEmpty(all));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/EntityTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.db.pojo.User;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Entity测试\n *\n * @author looly\n *\n */\npublic class EntityTest {\n\n\t@Test\n\tpublic void parseTest() {\n\t\tUser user = new User();\n\t\tuser.setId(1);\n\t\tuser.setName(\"test\");\n\n\t\tEntity entity = Entity.create(\"testTable\").parseBean(user);\n\t\tassertEquals(Integer.valueOf(1), entity.getInt(\"id\"));\n\t\tassertEquals(\"test\", entity.getStr(\"name\"));\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tUser user = new User();\n\t\tuser.setId(1);\n\t\tuser.setName(\"test\");\n\n\t\tEntity entity = Entity.create().parseBean(user);\n\t\tassertEquals(Integer.valueOf(1), entity.getInt(\"id\"));\n\t\tassertEquals(\"test\", entity.getStr(\"name\"));\n\t\tassertEquals(\"user\", entity.getTableName());\n\t}\n\n\t@Test\n\tpublic void parseTest3() {\n\t\tUser user = new User();\n\t\tuser.setName(\"test\");\n\n\t\tEntity entity = Entity.create().parseBean(user, false, true);\n\n\t\tassertFalse(entity.containsKey(\"id\"));\n\t\tassertEquals(\"test\", entity.getStr(\"name\"));\n\t\tassertEquals(\"user\", entity.getTableName());\n\t}\n\n\t@Test\n\tpublic void entityToBeanIgnoreCaseTest() {\n\t\tEntity entity = Entity.create().set(\"ID\", 2).set(\"NAME\", \"testName\");\n\t\tUser user = entity.toBeanIgnoreCase(User.class);\n\n\t\tassertEquals(Integer.valueOf(2), user.getId());\n\t\tassertEquals(\"testName\", user.getName());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/FindBeanTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.db.pojo.User;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Entity测试\n *\n * @author looly\n *\n */\npublic class FindBeanTest {\n\n\tDb db;\n\n\t@BeforeEach\n\tpublic void init() {\n\t\tdb = Db.use(\"test\");\n\t}\n\n\t@Test\n\tpublic void findAllBeanTest() throws SQLException {\n\t\tList<User> results = db.findAll(Entity.create(\"user\"), User.class);\n\n\t\tassertEquals(4, results.size());\n\t\tassertEquals(Integer.valueOf(1), results.get(0).getId());\n\t\tassertEquals(\"张三\", results.get(0).getName());\n\t}\n\n\t@Test\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic void findAllListTest() throws SQLException {\n\t\tList<List> results = db.findAll(Entity.create(\"user\"), List.class);\n\n\t\tassertEquals(4, results.size());\n\t\tassertEquals(1, results.get(0).get(0));\n\t\tassertEquals(\"张三\", results.get(0).get(1));\n\t}\n\n\t@Test\n\tpublic void findAllArrayTest() throws SQLException {\n\t\tList<Object[]> results = db.findAll(Entity.create(\"user\"), Object[].class);\n\n\t\tassertEquals(4, results.size());\n\t\tassertEquals(1, results.get(0)[0]);\n\t\tassertEquals(\"张三\", results.get(0)[1]);\n\t}\n\n\t@Test\n\tpublic void findAllStringTest() throws SQLException {\n\t\tList<String> results = db.findAll(Entity.create(\"user\"), String.class);\n\t\tassertEquals(4, results.size());\n\t}\n\n\t@Test\n\tpublic void findAllStringArrayTest() throws SQLException {\n\t\tList<String[]> results = db.findAll(Entity.create(\"user\"), String[].class);\n\n\t\tassertEquals(4, results.size());\n\t\tassertEquals(\"1\", results.get(0)[0]);\n\t\tassertEquals(\"张三\", results.get(0)[1]);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/GlobalDbConfigTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.Test;\n\npublic class GlobalDbConfigTest {\n\t@Test\n\tvoid createDbSettingTest(){\n\t\tGlobalDbConfig.createDbSetting();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/H2Test.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * H2数据库单元测试\n *\n * @author looly\n *\n */\npublic class H2Test {\n\n\tprivate static final String DS_GROUP_NAME = \"h2\";\n\n\t@BeforeAll\n\tpublic static void init() throws SQLException {\n\t\tDb db = Db.use(DS_GROUP_NAME);\n\t\tdb.execute(\"CREATE TABLE test(a INTEGER, b BIGINT)\");\n\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 1).set(\"b\", 11));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 2).set(\"b\", 21));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 3).set(\"b\", 31));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 4).set(\"b\", 41));\n\t}\n\n\t@Test\n\tpublic void queryTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).query(\"select * from test\");\n\t\tassertEquals(4, query.size());\n\t}\n\n\t@Test\n\tpublic void pageTest() throws SQLException {\n\t\tString sql = \"select * from test where a = @a and b = :b\";\n\t\tMap<String, Object> paramMap = MapUtil.builder(new CaseInsensitiveMap<String, Object>())\n\t\t\t\t.put(\"A\", 3)\n\t\t\t\t.put(\"b\", 31)\n\t\t\t\t.build();\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).page(sql, Page.of(0, 3), paramMap);\n\t\tassertEquals(1, query.size());\n\t}\n\n\t@Test\n\tpublic void findTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).find(Entity.create(\"test\"));\n\t\tassertEquals(4, query.size());\n\t}\n\n\t@Test\n\tpublic void upsertTest() throws SQLException {\n\t\tDb db=Db.use(DS_GROUP_NAME);\n\t\tdb.upsert(Entity.create(\"test\").set(\"a\",1).set(\"b\",111),\"a\");\n\t\tEntity a1=db.get(\"test\",\"a\",1);\n\t\tassertEquals(Long.valueOf(111),a1.getLong(\"b\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/HanaTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * hana操作单元测试\n *\n * @author daoyou.dev\n */\npublic class HanaTest {\n\t//@BeforeAll\n\tpublic static void createTable() throws SQLException {\n\t\tDb db = Db.use(\"hana\");\n\t\tlong count = db.count(\"SELECT * FROM SYS.TABLES WHERE TABLE_NAME = ? AND SCHEMA_NAME = CURRENT_SCHEMA\", \"user\");\n\t\tif (count > 0) {\n\t\t\tdb.execute(\"drop table \\\"user\\\"\");\n\t\t}\n\t\tdb.execute(\"CREATE COLUMN TABLE \\\"user\\\" (\\\"id\\\" INT NOT NULL, \\\"account\\\" VARCHAR(255), \\\"name\\\" VARCHAR(255), \\\"text\\\" VARCHAR(255), \\\"test1\\\" VARCHAR(255), \\\"pass\\\" VARCHAR(255), PRIMARY KEY (\\\"id\\\"))\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertTest() throws SQLException {\n\t\tDbUtil.setReturnGeneratedKeyGlobal(false);\n\t\tfor (int id = 100; id < 200; id++) {\n\t\t\tDb.use(\"hana\").insert(Entity.create(\"user\")//\n\t\t\t\t.set(\"id\", id)//\n\t\t\t\t.set(\"name\", \"测试用户\" + id)//\n\t\t\t\t.set(\"text\", \"描述\" + id)//\n\t\t\t\t.set(\"test1\", \"t\" + id)//\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * 事务测试<br>\n\t * 更新三条信息，低2条后抛出异常，正常情况下三条都应该不变\n\t *\n\t * @throws SQLException SQL异常\n\t */\n\t@Test\n\t@Disabled\n\tpublic void txTest() throws SQLException {\n\t\tDb.use(\"hana\").tx(db -> {\n\t\t\tint update = db.update(Entity.create(\"user\").set(\"text\", \"描述100\"), Entity.create().set(\"id\", 100));\n\t\t\tdb.update(Entity.create(\"user\").set(\"text\", \"描述101\"), Entity.create().set(\"id\", 101));\n\t\t\tif (1 == update) {\n\t\t\t\t// 手动指定异常，然后测试回滚触发\n\t\t\t\tthrow new RuntimeException(\"Error\");\n\t\t\t}\n\t\t\tdb.update(Entity.create(\"user\").set(\"text\", \"描述102\"), Entity.create().set(\"id\", 102));\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pageTest() throws SQLException {\n\t\tPageResult<Entity> result = Db.use(\"hana\").page(Entity.create(\"\\\"user\\\"\"), new Page(2, 10));\n\t\tfor (Entity entity : result) {\n\t\t\tConsole.log(entity.get(\"id\"));\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTimeStampTest() throws SQLException {\n\t\tfinal List<Entity> all = Db.use(\"hana\").findAll(\"user\");\n\t\tConsole.log(all);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void upsertTest() throws SQLException {\n\t\tDbUtil.setReturnGeneratedKeyGlobal(false);\n\t\tDb db = Db.use(\"hana\");\n\t\tdb.insert(Entity.create(\"user\").set(\"id\", 1).set(\"account\", \"ice\").set(\"pass\", \"123456\"));\n\t\tdb.upsert(Entity.create(\"user\").set(\"id\", 1).set(\"account\", \"daoyou\").set(\"pass\", \"a123456\").set(\"name\", \"道友\"));\n\t\tEntity user = db.get(Entity.create(\"user\").set(\"id\", 1));\n\t\tSystem.out.println(\"user=======\" + user.getStr(\"account\") + \"___\" + user.getStr(\"pass\"));\n\t\tassertEquals(\"daoyou\", user.getStr(\"account\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/HsqldbTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * HSQLDB数据库单元测试\n *\n * @author looly\n *\n */\npublic class HsqldbTest {\n\n\tprivate static final String DS_GROUP_NAME = \"hsqldb\";\n\n\t@BeforeAll\n\tpublic static void init() throws SQLException {\n\t\tDb db = Db.use(DS_GROUP_NAME);\n\t\tdb.execute(\"CREATE TABLE test(a INTEGER, b BIGINT)\");\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 1).set(\"b\", 11));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 2).set(\"b\", 21));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 3).set(\"b\", 31));\n\t\tdb.insert(Entity.create(\"test\").set(\"a\", 4).set(\"b\", 41));\n\t}\n\n\t@Test\n\tpublic void connTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).query(\"select * from test\");\n\t\tassertEquals(4, query.size());\n\t}\n\n\t@Test\n\tpublic void findTest() throws SQLException {\n\t\tList<Entity> query = Db.use(DS_GROUP_NAME).find(Entity.create(\"test\"));\n\t\tassertEquals(4, query.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/IssueI73770Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.db;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I73770\n */\npublic class IssueI73770Test {\n\t@Test\n\tpublic void pageTest() throws SQLException {\n\t\tfinal PageResult<User> result = Db.use()\n\t\t\t.page(\"select * from user where id = ?\"\n\t\t\t\t, new Page(0, 10), User.class, 9);\n\n\t\tassertEquals(1, result.size());\n\t\tassertEquals(Integer.valueOf(9), result.get(0).getId());\n\t}\n\n\t@Data\n\tstatic class User {\n\t\tprivate Integer id;\n\t\tprivate String name;\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/IssueI9BANETest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.db.ds.DSFactory;\nimport cn.hutool.db.meta.MetaUtil;\nimport cn.hutool.db.meta.Table;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.sql.DataSource;\nimport java.sql.SQLException;\n\npublic class IssueI9BANETest {\n\t@Test\n\t@Disabled\n\tpublic void metaTest() throws SQLException {\n\t\tfinal Db db = Db.use(\"orcl\");\n\t\tdb.find(Entity.create(\"\\\"1234\\\"\"));\n\n\t\tfinal DataSource ds = DSFactory.get(\"orcl\");\n\t\tfinal Table tableMeta = MetaUtil.getTableMeta(ds, null, null, \"\\\"1234\\\"\");\n\t\tConsole.log(\"remarks: \" + tableMeta.getComment());\n\t\tConsole.log(\"pks: \" + tableMeta.getPkNames());\n\t\tConsole.log(\"columns: \" + tableMeta.getColumns());\n\t\tConsole.log(\"index: \" + tableMeta.getIndexInfoList());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/MySQLTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\n/**\n * MySQL操作单元测试\n *\n * @author looly\n */\npublic class MySQLTest {\n\t//@BeforeAll\n\tpublic static void createTable() throws SQLException {\n\t\tDb db = Db.use(\"mysql\");\n\t\tdb.executeBatch(\"drop table if exists testuser\", \"CREATE TABLE if not exists `testuser` ( `id` int(11) NOT NULL, `account` varchar(255) DEFAULT NULL, `pass` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertTest() throws SQLException {\n\t\tfor (int id = 100; id < 200; id++) {\n\t\t\tDb.use(\"mysql\").insert(Entity.create(\"user\")//\n\t\t\t\t\t.set(\"id\", id)//\n\t\t\t\t\t.set(\"name\", \"测试用户\" + id)//\n\t\t\t\t\t.set(\"text\", \"描述\" + id)//\n\t\t\t\t\t.set(\"test1\", \"t\" + id)//\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * 事务测试<br>\n\t * 更新三条信息，低2条后抛出异常，正常情况下三条都应该不变\n\t */\n\t@Test\n\t@Disabled\n\tpublic void txTest() {\n\t\tassertThrows(SQLException.class, () -> {\n\t\t\tDb.use(\"mysql\").tx(db -> {\n\t\t\t\tint update = db.update(Entity.create(\"user\").set(\"text\", \"描述100\"), Entity.create().set(\"id\", 100));\n\t\t\t\tdb.update(Entity.create(\"user\").set(\"text\", \"描述101\"), Entity.create().set(\"id\", 101));\n\t\t\t\tif (1 == update) {\n\t\t\t\t\t// 手动指定异常，然后测试回滚触发\n\t\t\t\t\tthrow new RuntimeException(\"Error\");\n\t\t\t\t}\n\t\t\t\tdb.update(Entity.create(\"user\").set(\"text\", \"描述102\"), Entity.create().set(\"id\", 102));\n\t\t\t});\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pageTest() throws SQLException {\n\t\tPageResult<Entity> result = Db.use(\"mysql\").page(Entity.create(\"user\"), new Page(2, 10));\n\t\tfor (Entity entity : result) {\n\t\t\tConsole.log(entity.get(\"id\"));\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTimeStampTest() throws SQLException {\n\t\tfinal List<Entity> all = Db.use(\"mysql\").findAll(\"test\");\n\t\tConsole.log(all);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void upsertTest() throws SQLException {\n\t\tDb db = Db.use(\"mysql\");\n\t\tdb.insert(Entity.create(\"testuser\").set(\"id\", 1).set(\"account\", \"ice\").set(\"pass\", \"123456\"));\n\t\tdb.upsert(Entity.create(\"testuser\").set(\"id\", 1).set(\"account\", \"icefairy\").set(\"pass\", \"a123456\"));\n\t\tEntity user = db.get(Entity.create(\"testuser\").set(\"id\", 1));\n\t\tSystem.out.println(\"user=======\"+user.getStr(\"account\")+\"___\"+user.getStr(\"pass\"));\n\t\tassertEquals(user.getStr(\"account\"), \"icefairy\");\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/NamedSqlTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.db.sql.NamedSql;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class NamedSqlTest {\n\n\t@Test\n\tpublic void parseTest() {\n\t\tString sql = \"select * from table where id=@id and name = @name1 and nickName = :subName\";\n\n\t\tMap<String, Object> paramMap = MapUtil\n\t\t\t\t.builder(\"name1\", (Object)\"张三\")\n\t\t\t\t.put(\"age\", 12)\n\t\t\t\t.put(\"subName\", \"小豆豆\")\n\t\t\t\t.build();\n\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\t\t//未指定参数原样输出\n\t\tassertEquals(\"select * from table where id=@id and name = ? and nickName = ?\", namedSql.getSql());\n\t\tassertEquals(\"张三\", namedSql.getParams()[0]);\n\t\tassertEquals(\"小豆豆\", namedSql.getParams()[1]);\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tString sql = \"select * from table where id=@id and name = @name1 and nickName = :subName\";\n\n\t\tMap<String, Object> paramMap = MapUtil\n\t\t\t\t.builder(\"name1\", (Object)\"张三\")\n\t\t\t\t.put(\"age\", 12)\n\t\t\t\t.put(\"subName\", \"小豆豆\")\n\t\t\t\t.put(\"id\", null)\n\t\t\t\t.build();\n\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\t\tassertEquals(\"select * from table where id=? and name = ? and nickName = ?\", namedSql.getSql());\n\t\t//指定了null参数的依旧替换，参数值为null\n\t\tassertNull(namedSql.getParams()[0]);\n\t\tassertEquals(\"张三\", namedSql.getParams()[1]);\n\t\tassertEquals(\"小豆豆\", namedSql.getParams()[2]);\n\t}\n\n\t@Test\n\tpublic void parseTest3() {\n\t\t// 测试连续变量名出现是否有问题\n\t\tString sql = \"SELECT to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') as sysdate FROM dual\";\n\n\t\tMap<String, Object> paramMap = MapUtil\n\t\t\t\t.builder(\"name1\", (Object)\"张三\")\n\t\t\t\t.build();\n\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\t\tassertEquals(sql, namedSql.getSql());\n\t}\n\n\t@Test\n\tpublic void parseTest4() {\n\t\t// 测试postgre中形如data_value::numeric是否出错\n\t\tString sql = \"select device_key, min(data_value::numeric) as data_value from device\";\n\n\t\tMap<String, Object> paramMap = MapUtil\n\t\t\t\t.builder(\"name1\", (Object)\"张三\")\n\t\t\t\t.build();\n\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\t\tassertEquals(sql, namedSql.getSql());\n\t}\n\n\t@Test\n\tpublic void parseInTest(){\n\t\tString sql = \"select * from user where id in (:ids)\";\n\t\tfinal HashMap<String, Object> paramMap = MapUtil.of(\"ids\", new int[]{1, 2, 3});\n\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\t\tassertEquals(\"select * from user where id in (?,?,?)\", namedSql.getSql());\n\t\tassertEquals(1, namedSql.getParams()[0]);\n\t\tassertEquals(2, namedSql.getParams()[1]);\n\t\tassertEquals(3, namedSql.getParams()[2]);\n\t}\n\n\t@Test\n\tpublic void queryTest() throws SQLException {\n\t\tMap<String, Object> paramMap = MapUtil\n\t\t\t\t.builder(\"name1\", (Object)\"王五\")\n\t\t\t\t.put(\"age1\", 18).build();\n\t\tString sql = \"select * from user where name = @name1 and age = @age1\";\n\n\t\tList<Entity> query = Db.use().query(sql, paramMap);\n\t\tassertEquals(1, query.size());\n\n\t\t// 采用传统方式查询是否能识别Map类型参数\n\t\tquery = Db.use().query(sql, new Object[]{paramMap});\n\t\tassertEquals(1, query.size());\n\t}\n\n\t@Test\n\tpublic void parseInTest2() {\n\t\t// 测试表名包含\"in\"但不是IN子句的情况\n\t\tfinal String sql = \"select * from information where info_data = :info\";\n\t\tfinal HashMap<String, Object> paramMap = MapUtil.of(\"info\", new int[]{10, 20});\n\n\t\tfinal NamedSql namedSql = new NamedSql(sql, paramMap);\n\t\t// sql语句不包含IN子句，不会展开数组\n\t\tassertEquals(\"select * from information where info_data = ?\", namedSql.getSql());\n\t\tassertArrayEquals(new int[]{10, 20}, (int[]) namedSql.getParams()[0]);\n\t}\n\n\t@Test\n\tpublic void parseInTest3() {\n\t\t// 测试字符串中包含\"in\"关键字但不是IN子句的情况\n\t\tfinal String sql = \"select * from user where comment = 'include in text' and id = :id\";\n\t\tfinal HashMap<String, Object> paramMap = MapUtil.of(\"id\", new int[]{5, 6});\n\n\t\tfinal NamedSql namedSql = new NamedSql(sql, paramMap);\n\t\t// sql语句不包含IN子句，不会展开数组\n\t\tassertEquals(\"select * from user where comment = 'include in text' and id = ?\", namedSql.getSql());\n\t\tassertArrayEquals(new int[]{5, 6}, (int[]) namedSql.getParams()[0]);\n\t}\n\n\t@Test\n\tvoid selectCaseInTest() {\n\t\tfinal HashMap<String, Object> paramMap = MapUtil.of(\"number\", new int[]{1, 2, 3});\n\n\t\tNamedSql namedSql = new NamedSql(\"select case when 2 = any(ARRAY[:number]) and 1 in (1) then 1 else 0 end\", paramMap);\n\t\tassertEquals(\"select case when 2 = any(ARRAY[?]) and 1 in (1) then 1 else 0 end\", namedSql.getSql());\n\t\tassertArrayEquals(new int[]{1, 2, 3}, (int[])namedSql.getParams()[0]);\n\t}\n\n\t@Test\n\tpublic void parseInsertMultiRowTest() {\n\t\t// 多行 INSERT 语句\n\t\tfinal Map<String, Object> paramMap = new LinkedHashMap<>();\n\t\tparamMap.put(\"user1\", new Object[]{1, \"looly\"});\n\t\tparamMap.put(\"user2\", new Object[]{2, \"xxxtea\"});\n\n\t\tString sql = \"INSERT INTO users (id, name) VALUES (:user1), (:user2)\";\n\t\tNamedSql namedSql = new NamedSql(sql, paramMap);\n\n\t\tassertEquals(\"INSERT INTO users (id, name) VALUES (?), (?)\", namedSql.getSql());\n\t\tassertArrayEquals(new Object[]{new Object[]{1, \"looly\"}, new Object[]{2, \"xxxtea\"}}, namedSql.getParams());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/OracleTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.db.sql.Query;\nimport cn.hutool.db.sql.SqlBuilder;\nimport cn.hutool.db.sql.SqlUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\n\n/**\n * Oracle操作单元测试\n *\n * @author looly\n *\n */\npublic class OracleTest {\n\n\t@Test\n\tpublic void oraclePageSqlTest() {\n\t\tPage page = new Page(0, 10);\n\t\tEntity where = Entity.create(\"PMCPERFORMANCEINFO\").set(\"yearPI\", \"2017\");\n\t\tfinal Query query = new Query(SqlUtil.buildConditions(where), where.getTableName());\n\t\tquery.setPage(page);\n\n\t\tSqlBuilder find = SqlBuilder.create(null).query(query).orderBy(page.getOrders());\n\t\tfinal int[] startEnd = page.getStartEnd();\n\t\tSqlBuilder builder = SqlBuilder.create(null).append(\"SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( \")//\n\t\t\t\t.append(find)//\n\t\t\t\t.append(\" ) row_ where rownum <= \").append(startEnd[1])//\n\t\t\t\t.append(\") table_alias\")//\n\t\t\t\t.append(\" where table_alias.rownum_ >= \").append(startEnd[0]);//\n\n\t\tString ok = \"SELECT * FROM \"//\n\t\t\t\t+ \"( SELECT row_.*, rownum rownum_ from ( SELECT * FROM PMCPERFORMANCEINFO WHERE yearPI = ? ) row_ \"//\n\t\t\t\t+ \"where rownum <= 10) table_alias where table_alias.rownum_ >= 0\";//\n\t\tassertEquals(ok, builder.toString());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertTest() throws SQLException {\n\t\tfor (int id = 100; id < 200; id++) {\n\t\t\tDb.use(\"orcl\").insert(Entity.create(\"T_USER\")//\n\t\t\t\t\t.set(\"ID\", id)//\n\t\t\t\t\t.set(\"name\", \"测试用户\" + id)//\n\t\t\t\t\t.set(\"TEXT\", \"描述\" + id)//\n\t\t\t\t\t.set(\"TEST1\", \"t\" + id)//\n\t\t\t);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pageTest() throws SQLException {\n\t\tPageResult<Entity> result = Db.use(\"orcl\").page(Entity.create(\"T_USER\"), new Page(2, 10));\n\t\tfor (Entity entity : result) {\n\t\t\tConsole.log(entity.get(\"ID\"));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/PageResultTest.java",
    "content": "package cn.hutool.db;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class PageResultTest {\n\n\t@Test\n\tpublic void isLastTest(){\n\t\t// 每页2条，共10条，总共5页，第一页是0，最后一页应该是4\n\t\tfinal PageResult<String> result = new PageResult<>(4, 2, 10);\n\t\tassertTrue(result.isLast());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/PageTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.db.sql.Order;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class PageTest {\n\n\t@Test\n\tpublic void addOrderTest() {\n\t\tPage page = new Page();\n\t\tpage.addOrder(new Order(\"aaa\"));\n\t\tassertEquals(page.getOrders().length, 1);\n\t\tpage.addOrder(new Order(\"aaa\"));\n\t\tassertEquals(page.getOrders().length, 2);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/PicTransferTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\npublic class PicTransferTest {\n\n\t@Test\n\t@Disabled\n\tpublic void findTest() throws SQLException {\n\t\tDb.use().find(\n\t\t\t\tListUtil.of(\"NAME\", \"TYPE\", \"GROUP\", \"PIC\"),\n\t\t\t\tEntity.create(\"PIC_INFO\").set(\"TYPE\", 1),\n\t\t\t\trs -> {\n\t\t\t\t\twhile(rs.next()){\n\t\t\t\t\t\tsave(rs);\n\t\t\t\t\t}\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t);\n\t}\n\n\tprivate static void save(ResultSet rs) throws SQLException{\n\t\tString destDir = \"d:/test/pic\";\n\t\tString path = StrUtil.format(\"{}/{}-{}.jpg\", destDir, rs.getString(\"NAME\"), rs.getString(\"GROUP\"));\n\t\tFileUtil.writeFromStream(rs.getBlob(\"PIC\").getBinaryStream(), path);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/PostgreTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.db.sql.NamedSql;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * PostgreSQL 单元测试\n *\n * @author looly\n */\npublic class PostgreTest {\n\n\t@Test\n\t@Disabled\n\tpublic void insertTest() throws SQLException {\n\t\tfor (int id = 100; id < 200; id++) {\n\t\t\tDb.use(\"postgre\").insert(Entity.create(\"user\")//\n\t\t\t\t\t.set(\"id\", id)//\n\t\t\t\t\t.set(\"name\", \"测试用户\" + id)//\n\t\t\t);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pageTest() throws SQLException {\n\t\tPageResult<Entity> result = Db.use(\"postgre\").page(Entity.create(\"user\"), new Page(2, 10));\n\t\tfor (Entity entity : result) {\n\t\t\tConsole.log(entity.get(\"id\"));\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void upsertTest() throws SQLException {\n\t\tDb db = Db.use(\"postgre\");\n\t\tdb.executeBatch(\"drop table if exists ctest\",\n\t\t\t\t\"create table if not exists \\\"ctest\\\" ( \\\"id\\\" serial4, \\\"t1\\\" varchar(255) COLLATE \\\"pg_catalog\\\".\\\"default\\\", \\\"t2\\\" varchar(255) COLLATE \\\"pg_catalog\\\".\\\"default\\\", \\\"t3\\\" varchar(255) COLLATE \\\"pg_catalog\\\".\\\"default\\\", CONSTRAINT \\\"ctest_pkey\\\" PRIMARY KEY (\\\"id\\\") )  \");\n\t\tdb.insert(Entity.create(\"ctest\").set(\"id\", 1).set(\"t1\", \"111\").set(\"t2\", \"222\").set(\"t3\", \"333\"));\n\t\tdb.upsert(Entity.create(\"ctest\").set(\"id\", 1).set(\"t1\", \"new111\").set(\"t2\", \"new222\").set(\"t3\", \"bew333\"),\"id\");\n\t\tEntity et=db.get(Entity.create(\"ctest\").set(\"id\", 1));\n\t\tassertEquals(\"new111\",et.getStr(\"t1\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tvoid namedSqlWithInTest() throws SQLException {\n\t\tfinal HashMap<String, Object> paramMap = MapUtil.of(\"number\", new int[]{1, 2, 3});\n\t\tNamedSql namedSql = new NamedSql(\"select case when 2 = any(ARRAY[:number]) and 1 in (1) then 1 else 0 end\", paramMap);\n\t\tfinal Db db = Db.use(\"postgre\");\n\t\tfinal List<Entity> query = db.query(namedSql.getSql(), namedSql.getParams());\n\t\tConsole.log(query);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/SessionTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\n\n/**\n * 事务性数据库操作单元测试\n * @author looly\n *\n */\npublic class SessionTest {\n\n\t@Test\n\t@Disabled\n\tpublic void transTest() {\n\t\tSession session = Session.create(\"test\");\n\t\ttry {\n\t\t\tsession.beginTransaction();\n\t\t\tsession.update(Entity.create().set(\"age\", 76), Entity.create(\"user\").set(\"name\", \"unitTestUser\"));\n\t\t\tsession.commit();\n\t\t} catch (SQLException e) {\n\t\t\tsession.quietRollback();\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void txTest() throws SQLException {\n\t\tSession.create(\"test\").tx(session -> session.update(Entity.create().set(\"age\", 78), Entity.create(\"user\").set(\"name\", \"unitTestUser\")));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/SqlServerTest.java",
    "content": "package cn.hutool.db;\n\nimport java.sql.SQLException;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.lang.Console;\n\n/**\n * SQL Server操作单元测试\n *\n * @author looly\n *\n */\npublic class SqlServerTest {\n\n\t@Test\n\t@Disabled\n\tpublic void createTableTest() throws SQLException {\n\t\tDb.use(\"sqlserver\").execute(\"create table T_USER(ID bigint, name varchar(255))\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void insertTest() throws SQLException {\n\t\tfor (int id = 100; id < 200; id++) {\n\t\t\tDb.use(\"sqlserver\").insert(Entity.create(\"T_USER\")//\n\t\t\t\t\t.set(\"ID\", id)//\n\t\t\t\t\t.set(\"name\", \"测试用户\" + id)//\n\t\t\t);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pageTest() throws SQLException {\n\t\tPageResult<Entity> result = Db.use(\"sqlserver\").page(Entity.create(\"T_USER\"), new Page(2, 10));\n\t\tfor (Entity entity : result) {\n\t\t\tConsole.log(entity.get(\"ID\"));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/UpdateTest.java",
    "content": "package cn.hutool.db;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\n\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class UpdateTest {\n\n\tDb db;\n\n\t@BeforeEach\n\tpublic void init() {\n\t\tdb = Db.use(\"test\");\n\t}\n\n\t/**\n\t * 对更新做单元测试\n\t *\n\t * @throws SQLException SQL异常\n\t */\n\t@Test\n\t@Disabled\n\tpublic void updateTest() throws SQLException {\n\n\t\t// 改\n\t\tint update = db.update(Entity.create(\"user\").set(\"age\", 88), Entity.create().set(\"name\", \"unitTestUser\"));\n\t\tassertTrue(update > 0);\n\t\tEntity result2 = db.get(\"user\", \"name\", \"unitTestUser\");\n\t\tassertSame(88, result2.getInt(\"age\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/WrapperTest.java",
    "content": "package cn.hutool.db;\n\nimport cn.hutool.db.sql.Wrapper;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * @author bwcx_jzy\n * @since 24/3/28 028\n */\npublic class WrapperTest {\n\n\t@Test\n\t@Disabled\n\tpublic void test() {\n\t\tWrapper wrapper = new Wrapper('`');\n\t\tString originalName = \"name\";\n\t\tString wrapName = wrapper.wrap(originalName);\n\t\tString unWrapName = wrapper.unWrap(wrapName);\n\t\tassertEquals(unWrapName, originalName);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testDotWrap() {\n\t\tWrapper wrapper = new Wrapper('`');\n\t\tString originalName = \"name.age\";\n\t\tString wrapName = wrapper.wrap(originalName);\n\t\tString unWrapName = wrapper.unWrap(wrapName);\n\t\tassertEquals(unWrapName, originalName);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testError() {\n\t\tWrapper wrapper = new Wrapper('`');\n\t\tString originalName = \"name.age*\";\n\t\tString wrapName = wrapper.wrap(originalName);\n\t\tString unWrapName = wrapper.unWrap(wrapName);\n\t\tassertEquals(unWrapName, originalName);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/dialect/DialectFactoryTest.java",
    "content": "package cn.hutool.db.dialect;\n\nimport cn.hutool.core.util.RandomUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static cn.hutool.db.dialect.DriverNamePool.*;\n\npublic class DialectFactoryTest {\n\n\t@Test\n\tpublic void identifyDriverTest(){\n\n\t\tMap<String,String> map = new HashMap<>(25);\n\t\tmap.put(\"mysql\",DRIVER_MYSQL_V6);\n\t\tmap.put(\"cobar\",DRIVER_MYSQL_V6);\n\t\tmap.put(\"oracle\",DRIVER_ORACLE);\n\t\tmap.put(\"postgresql\",DRIVER_POSTGRESQL);\n\t\tmap.put(\"sqlite\",DRIVER_SQLLITE3);\n\t\tmap.put(\"sqlserver\",DRIVER_SQLSERVER);\n\t\tmap.put(\"microsoft\",DRIVER_SQLSERVER);\n\n\t\t// 单元测试歧义\n\t\t//map.put(\"hive2\",DRIVER_HIVE2);\n\t\t//map.put(\"hive\",DRIVER_HIVE);\n\t\tmap.put(\"h2\",DRIVER_H2);\n\t\tmap.put(\"derby\",DRIVER_DERBY);\n\t\tmap.put(\"hsqldb\",DRIVER_HSQLDB);\n\t\tmap.put(\"dm\",DRIVER_DM7);\n\t\tmap.put(\"kingbase8\",DRIVER_KINGBASE8);\n\t\tmap.put(\"ignite\",DRIVER_IGNITE_THIN);\n\t\tmap.put(\"clickhouse\",DRIVER_CLICK_HOUSE);\n\t\tmap.put(\"highgo\",DRIVER_HIGHGO);\n\t\tmap.put(\"db2\",DRIVER_DB2);\n\t\tmap.put(\"xugu\",DRIVER_XUGU);\n\t\tmap.put(\"phoenix\",DRIVER_PHOENIX);\n\t\tmap.put(\"zenith\",DRIVER_GAUSS);\n\t\tmap.put(\"gbase\",DRIVER_GBASE);\n\t\tmap.put(\"oscar\",DRIVER_OSCAR);\n\t\tmap.put(\"sybase\",DRIVER_SYBASE);\n\t\tmap.put(\"mariadb\",DRIVER_MARIADB);\n\t\tmap.put(\"goldendb\",DRIVER_GOLDENDB);\n\n\n\t\tmap.forEach((k,v) -> assertEquals(v,\n\t\t\t\tDialectFactory.identifyDriver(k+ RandomUtil.randomString(2),null) ));\n\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/dialect/DriverUtilTest.java",
    "content": "package cn.hutool.db.dialect;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class DriverUtilTest {\n\n\t@Test\n\tpublic void identifyDriverTest(){\n\t\tString url = \"jdbc:h2:file:./db/test;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL\";\n\t\tString driver = DriverUtil.identifyDriver(url); // driver 返回 mysql 的 driver\n\t\tassertEquals(\"org.h2.Driver\", driver);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/ds/DataSourceWrapperTest.java",
    "content": "package cn.hutool.db.ds;\n\nimport cn.hutool.db.ds.simple.SimpleDataSource;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class DataSourceWrapperTest {\n\n\t@Test\n\tpublic void cloneTest(){\n\t\tfinal SimpleDataSource simpleDataSource = new SimpleDataSource(\"jdbc:sqlite:test.db\", \"\", \"\");\n\t\tfinal DataSourceWrapper wrapper = new DataSourceWrapper(simpleDataSource, \"test.driver\");\n\n\t\tfinal DataSourceWrapper clone = wrapper.clone();\n\t\tassertEquals(\"test.driver\", clone.getDriver());\n\t\tassertEquals(simpleDataSource, clone.getRaw());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/ds/IssueI70J95Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.db.ds;\n\nimport cn.hutool.setting.Setting;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.sql.DataSource;\n\npublic class IssueI70J95Test {\n\n\t@Test\n\tpublic void getDataSourceTest() {\n\t\tfinal String dbSourceName = \"group_db1\";\n\n\t\tfinal Setting dbSetting = new Setting();\n\t\tdbSetting.setByGroup(\"url\", dbSourceName, \"jdbc:sqlite:test.db\");\n\t\tdbSetting.setByGroup(\"username\", dbSourceName, \"****\");\n\t\tdbSetting.setByGroup(\"password\", dbSourceName, \"***\");\n\t\tDSFactory.create(dbSetting).getDataSource(dbSourceName);\n\t}\n\n\t@Test\n\tpublic void getDataSourceTest2() {\n\t\tfinal Setting dbSetting = new Setting(\"config/db.setting\");\n\t\tfinal DataSource dataSource = DSFactory.create(dbSetting).getDataSource(\"\");\n\t\tassertNotNull(dataSource);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/meta/MetaUtilTest.java",
    "content": "package cn.hutool.db.meta;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.db.DbRuntimeException;\nimport cn.hutool.db.ds.DSFactory;\nimport org.junit.jupiter.api.Test;\n\nimport javax.sql.DataSource;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * 元数据信息单元测试\n *\n * @author Looly\n *\n */\npublic class MetaUtilTest {\n\tDataSource ds = DSFactory.get(\"test\");\n\n\t@Test\n\tpublic void getTablesTest() {\n\t\tfinal List<String> tables = MetaUtil.getTables(ds);\n\t\tassertEquals(\"user\", tables.get(0));\n\t}\n\n\t@Test\n\tpublic void getTableMetaTest() {\n\t\tfinal Table table = MetaUtil.getTableMeta(ds, \"user\");\n\t\tassertEquals(CollectionUtil.newHashSet(\"id\"), table.getPkNames());\n\t}\n\n\t@Test\n\tpublic void getColumnNamesTest() {\n\t\tfinal String[] names = MetaUtil.getColumnNames(ds, \"user\");\n\t\tassertArrayEquals(StrUtil.splitToArray(\"id,name,age,birthday,gender\", ','), names);\n\t}\n\n\t@Test\n\tpublic void getTableIndexInfoTest() {\n\t\tfinal Table table = MetaUtil.getTableMeta(ds, \"user_1\");\n\t\tassertEquals(table.getIndexInfoList().size(), 2);\n\t}\n\n\t/**\n\t * 表不存在抛出异常。\n\t */\n\t@Test\n\tpublic void getTableNotExistTest() {\n\t\tassertThrows(DbRuntimeException.class, () -> {\n\t\t\tfinal Table table = MetaUtil.getTableMeta(ds, \"user_not_exist\");\n\t\t\tassertEquals(table.getIndexInfoList().size(), 2);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/nosql/MongoDBTest.java",
    "content": "package cn.hutool.db.nosql;\n\nimport cn.hutool.db.nosql.mongo.MongoFactory;\nimport com.mongodb.client.MongoDatabase;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * @author VampireAchao\n */\npublic class MongoDBTest {\n\n\t@Test\n\t@Disabled\n\tpublic void mongoDSTest() {\n\t\tMongoDatabase db = MongoFactory.getDS(\"master\").getDb(\"test\");\n\t\tassertEquals(\"test\", db.getName());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/nosql/RedisDSTest.java",
    "content": "package cn.hutool.db.nosql;\n\nimport cn.hutool.db.nosql.redis.RedisDS;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport redis.clients.jedis.Jedis;\n\npublic class RedisDSTest {\n\n\t@Test\n\t@Disabled\n\tpublic void redisDSTest(){\n\t\tfinal Jedis jedis = RedisDS.create().getJedis();\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/pojo/User.java",
    "content": "package cn.hutool.db.pojo;\n\n/**\n * 测试用POJO，与测试数据库中的user表对应\n * \n * @author looly\n *\n */\npublic class User {\n\tprivate Integer id;\n\tprivate String name;\n\tprivate int age;\n\tprivate String birthday;\n\tprivate boolean gender;\n\n\tpublic Integer getId() {\n\t\treturn id;\n\t}\n\n\tpublic void setId(Integer id) {\n\t\tthis.id = id;\n\t}\n\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic String getBirthday() {\n\t\treturn birthday;\n\t}\n\n\tpublic void setBirthday(String birthday) {\n\t\tthis.birthday = birthday;\n\t}\n\n\tpublic boolean isGender() {\n\t\treturn gender;\n\t}\n\n\tpublic void setGender(boolean gender) {\n\t\tthis.gender = gender;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"User [id=\" + id + \", name=\" + name + \", age=\" + age + \", birthday=\" + birthday + \", gender=\" + gender + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/ConditionBuilderTest.java",
    "content": "package cn.hutool.db.sql;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ConditionBuilderTest {\n\n\t@Test\n\tpublic void buildTest(){\n\t\tCondition c1 = new Condition(\"user\", null);\n\t\tCondition c2 = new Condition(\"name\", \"!= null\");\n\t\tc2.setLinkOperator(LogicalOperator.OR);\n\t\tCondition c3 = new Condition(\"group\", \"like %aaa\");\n\n\t\tfinal ConditionBuilder builder = ConditionBuilder.of(c1, c2, c3);\n\t\tfinal String sql = builder.build();\n\t\tassertEquals(\"user IS NULL OR name IS NOT NULL AND group LIKE ?\", sql);\n\t\tassertEquals(1, builder.getParamValues().size());\n\t\tassertEquals(\"%aaa\", builder.getParamValues().get(0));\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/ConditionGroupTest.java",
    "content": "package cn.hutool.db.sql;\n\nimport cn.hutool.core.collection.ListUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class ConditionGroupTest {\n\t@Test\n\tpublic void ConditionGroupToStringTest() {\n\t\tCondition condition1 = new Condition(\"a\", \"A\");\n\t\tCondition condition2 = new Condition(\"b\", \"B\");\n\t\tcondition2.setLinkOperator(LogicalOperator.OR);\n\t\tCondition condition3 = new Condition(\"c\", \"C\");\n\t\tCondition condition4 = new Condition(\"d\", \"D\");\n\n\t\tConditionGroup cg = new ConditionGroup();\n\t\tcg.addConditions(condition1, condition2);\n\n\t\t// 条件组嵌套情况\n\t\tConditionGroup cg2 = new ConditionGroup();\n\t\tcg2.addConditions(cg, condition3);\n\n\t\tfinal ConditionBuilder conditionBuilder = ConditionBuilder.of(cg2, condition4);\n\n\t\tassertEquals(\"((a = ? OR b = ?) AND c = ?) AND d = ?\", conditionBuilder.build());\n\t\tassertEquals(ListUtil.of(\"A\", \"B\", \"C\", \"D\"), conditionBuilder.getParamValues());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/ConditionTest.java",
    "content": "package cn.hutool.db.sql;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\npublic class ConditionTest {\n\n\t@Test\n\tpublic void toStringTest() {\n\t\tCondition conditionNull = new Condition(\"user\", null);\n\t\tassertEquals(\"user IS NULL\", conditionNull.toString());\n\n\t\tCondition conditionNull2 = new Condition(\"user\", \"=\", (String) null);\n\t\tassertEquals(\"user IS NULL\", conditionNull2.toString());\n\n\t\tCondition conditionNotNull = new Condition(\"user\", \"!= null\");\n\t\tassertEquals(\"user IS NOT NULL\", conditionNotNull.toString());\n\n\t\tCondition conditionNotNull2 = new Condition(\"user\", \"!=\", (String) null);\n\t\tassertEquals(\"user IS NOT NULL\", conditionNotNull2.toString());\n\n\t\tCondition conditionNotNull3 = new Condition(\"user\", \"<>\", (String) null);\n\t\tassertEquals(\"user IS NOT NULL\", conditionNotNull3.toString());\n\n\t\tCondition condition2 = new Condition(\"user\", \"= zhangsan\");\n\t\tassertEquals(\"user = ?\", condition2.toString());\n\n\t\tCondition conditionLike = new Condition(\"user\", \"like %aaa\");\n\t\tassertEquals(\"user LIKE ?\", conditionLike.toString());\n\n\t\tCondition conditionIn = new Condition(\"user\", \"in 1,2,3\");\n\t\tassertEquals(\"user IN (?,?,?)\", conditionIn.toString());\n\n\t\tCondition conditionBetween = new Condition(\"user\", \"between 12 and 13\");\n\t\tassertEquals(\"user BETWEEN ? AND ?\", conditionBetween.toString());\n\t}\n\n\t@Test\n\tpublic void toStringNoPlaceHolderTest() {\n\t\tCondition conditionNull = new Condition(\"user\", null);\n\t\tconditionNull.setPlaceHolder(false);\n\t\tassertEquals(\"user IS NULL\", conditionNull.toString());\n\n\t\tCondition conditionNotNull = new Condition(\"user\", \"!= null\");\n\t\tconditionNotNull.setPlaceHolder(false);\n\t\tassertEquals(\"user IS NOT NULL\", conditionNotNull.toString());\n\n\t\tCondition conditionEquals = new Condition(\"user\", \"= zhangsan\");\n\t\tconditionEquals.setPlaceHolder(false);\n\t\tassertEquals(\"user = zhangsan\", conditionEquals.toString());\n\n\t\tCondition conditionLike = new Condition(\"user\", \"like %aaa\");\n\t\tconditionLike.setPlaceHolder(false);\n\t\tassertEquals(\"user LIKE '%aaa'\", conditionLike.toString());\n\n\t\tCondition conditionIn = new Condition(\"user\", \"in 1,2,3\");\n\t\tconditionIn.setPlaceHolder(false);\n\t\tassertEquals(\"user IN (1,2,3)\", conditionIn.toString());\n\n\t\tCondition conditionBetween = new Condition(\"user\", \"between 12 and 13\");\n\t\tconditionBetween.setPlaceHolder(false);\n\t\tassertEquals(\"user BETWEEN 12 AND 13\", conditionBetween.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest(){\n\t\tfinal Condition age = Condition.parse(\"age\", \"< 10\");\n\t\tassertEquals(\"age < ?\", age.toString());\n\t\t// issue I38LTM\n\t\tassertSame(BigDecimal.class, age.getValue().getClass());\n\t}\n\n\t@Test\n\tpublic void parseInTest(){\n\t\tfinal Condition age = Condition.parse(\"age\", \"in 1,2,3\");\n\t\tassertEquals(\"age IN (?,?,?)\", age.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/Issue4066Test.java",
    "content": "package cn.hutool.db.sql;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class Issue4066Test {\n\t/**\n\t * 基础测试：简单的 ORDER BY 语句\n\t */\n\t@Test\n\tpublic void removeOuterOrderByTest1() {\n\t\t// 测试基本的ORDER BY移除\n\t\tfinal String sql = \"SELECT * FROM users ORDER BY name\";\n\t\tfinal String result = SqlUtil.removeOuterOrderBy(sql);\n\n\t\tassertEquals(\"SELECT * FROM users\", result);\n\t}\n\n\t/**\n\t * 多字段 ORDER BY 测试：包含多个排序字段的复杂 ORDER BY语句\n\t */\n\t@Test\n\tpublic void removeOuterOrderByTest2() {\n\t\t// 测试多字段ORDER BY移除\n\t\tfinal String sql = \"SELECT id, name, age FROM users WHERE status = 'active' ORDER BY name ASC, age DESC, created_date\";\n\t\tfinal String result = SqlUtil.removeOuterOrderBy(sql);\n\n\t\tassertEquals(\"SELECT id, name, age FROM users WHERE status = 'active'\", result);\n\t}\n\n\t/**\n\t * 测试不含Order by的语句\n\t */\n\t@Test\n\tpublic void removeOuterOrderByTest3() {\n\t\tfinal String sql = \"SELECT * FROM users\";\n\t\tfinal String result = SqlUtil.removeOuterOrderBy(sql);\n\n\t\tassertEquals(\"SELECT * FROM users\", result);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/Issue4200Test.java",
    "content": "package cn.hutool.db.sql;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class Issue4200Test {\n\n\t/**\n\t * issues中提及的SQL\n\t */\n\tprivate final static String TEST_SQL = \"select case when  1 in (1) and 2 = any(:number) then 1 else 0 end\";\n\n\n\t/**\n\t * 基础测试：in子句测试\n\t */\n\t@Test\n\tpublic void isInClauseTest0() {\n\t\tfinal String sql = \"select case when 1=1 and 2 in ( \";\n\t\tassertTrue(SqlUtil.isInClause(sql));\n\t}\n\n\t/**\n\t * 基础测试：in子句测试\n\t */\n\t@Test\n\tpublic void isInClauseTest1() {\n\t\tfinal String sql = \"select case when 1=1 and 2 in (\";\n\t\tassertTrue(SqlUtil.isInClause(sql));\n\t}\n\n\t/**\n\t * 基础测试：非in子句测试\n\t */\n\t@Test\n\tpublic void isInClauseTest2() {\n\t\t// 测试基本的ORDER BY移除\n\t\t//final String sql = \"select case when  1 in (1) and 2 = any(:number) then 1 else 0 end\";\n\t\tfinal String sql = \"select case when 1=1 and 2 = any(\";\n\n\t\tassertFalse(SqlUtil.isInClause(sql));\n\t}\n\n\t/**\n\t * 测试 包含in但非param对应的in子句测试\n\t */\n\t@Test\n\tpublic void isInClauseTest3() {\n\t\t// 截断前sql\n\t\t//final String sql =\n\t\tfinal String sql = \"select case when 1 in (?,?,?) and 2 = any(\";\n\n\t\tassertFalse(SqlUtil.isInClause(sql));\n\t}\n\n\t/**\n\t * 测试 包含in但非param对应的in子句测试\n\t */\n\t@Test\n\tpublic void isInClauseTest4() {\n\t\t// 截断前sql\n\t\t//final String sql =\n\t\tfinal String sql = \"select case when 1 in (?,?,?) and 2 = any(\";\n\n\t\tassertFalse(SqlUtil.isInClause(sql));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/SqlBuilderTest.java",
    "content": "package cn.hutool.db.sql;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class SqlBuilderTest {\n\n\t@Test\n\tpublic void queryNullTest() {\n\t\tSqlBuilder builder = SqlBuilder.create().select().from(\"user\").where(new Condition(\"name\", \"= null\"));\n\t\tassertEquals(\"SELECT * FROM user WHERE name IS NULL\", builder.build());\n\n\t\tSqlBuilder builder2 = SqlBuilder.create().select().from(\"user\").where(new Condition(\"name\", \"is null\"));\n\t\tassertEquals(\"SELECT * FROM user WHERE name IS NULL\", builder2.build());\n\n\t\tSqlBuilder builder3 = SqlBuilder.create().select().from(\"user\").where(new Condition(\"name\", \"!= null\"));\n\t\tassertEquals(\"SELECT * FROM user WHERE name IS NOT NULL\", builder3.build());\n\n\t\tSqlBuilder builder4 = SqlBuilder.create().select().from(\"user\").where(new Condition(\"name\", \"is not null\"));\n\t\tassertEquals(\"SELECT * FROM user WHERE name IS NOT NULL\", builder4.build());\n\t}\n\n\t@Test\n\tpublic void orderByTest() {\n\t\tSqlBuilder builder = SqlBuilder.create().select(\"id\", \"username\").from(\"user\")\n\t\t\t\t.join(\"role\", SqlBuilder.Join.INNER)\n\t\t\t\t.on(\"user.id = role.user_id\")\n\t\t\t\t.where(new Condition(\"age\", \">=\", 18),\n\t\t\t\t\t\tnew Condition(\"username\", \"abc\", Condition.LikeType.Contains)\n\t\t\t\t).orderBy(new Order(\"id\"));\n\n\t\tassertEquals(\"SELECT id,username FROM user INNER JOIN role ON user.id = role.user_id WHERE age >= ? AND username LIKE ? ORDER BY id\", builder.build());\n\t}\n\n\t@Test\n\tpublic void likeTest() {\n\t\tCondition conditionEquals = new Condition(\"user\", \"123\", Condition.LikeType.Contains);\n\t\tconditionEquals.setPlaceHolder(false);\n\n\t\tSqlBuilder sqlBuilder = new SqlBuilder();\n\t\tsqlBuilder.select(\"id\");\n\t\tsqlBuilder.from(\"user\");\n\t\tsqlBuilder.where(conditionEquals);\n\t\tString s1 = sqlBuilder.build();\n\t\tassertEquals(\"SELECT id FROM user WHERE user LIKE '%123%'\", s1);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/java/cn/hutool/db/sql/SqlFormatterTest.java",
    "content": "package cn.hutool.db.sql;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class SqlFormatterTest {\n\n\t@Test\n\tpublic void formatTest(){\n\t\t// issue#I3XS44@Gitee\n\t\t// 测试是否空指针错误\n\t\tfinal String sql = \"(select 1 from dual) union all (select 1 from dual)\";\n\t\tSqlFormatter.format(sql);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testKeyword() {\n\t\tfinal String sql = \"select * from `order`\";\n\t\tfinal String format = SqlFormatter.format(sql);\n\t\tSystem.out.println(format);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testSqlBuilderFormat() {\n\t\tfinal String sql = \"SELECT `link_table_a`.`value_a` AS `link_table_a.value_a`,`link_table_a`.`id` AS `link_table_a.id`,`link_table_b`.`value_b` AS `link_table_b.value_b`,`link_table_c`.`id` AS `link_table_c.id`,`link_table_b`.`id` AS `link_table_b.id`,`link_table_c`.`value_c` AS `link_table_c.value_c` FROM `link_table_a` INNER JOIN `link_table_b` ON `link_table_a`.`table_b_id` = `link_table_b`.`id` INNER JOIN `link_table_c` ON `link_table_b`.`table_c_id` = `link_table_c`.`id`\";\n\t\tfinal String format = SqlBuilder.of(sql).format().build();\n\t\tSystem.out.println(format);\n\t}\n}\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/db.setting",
    "content": "# suppress inspection \"Annotator\" for whole file\n#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n## 打印SQL的配置\n# 是否在日志中显示执行的SQL，默认false\nshowSql = true\n# 是否格式化显示的SQL，默认false\nformatSql = true\n# 是否显示SQL参数，默认false\nshowParams = true\n# 打印SQL的日志等级，默认debug\nsqlLevel = debug\n\n# 默认数据源\nurl = jdbc:sqlite:test.db\nremarks = true\n\n\n# 测试数据源\n[test]\nurl = jdbc:sqlite:test.db\nremarks = true\n\n# 测试用HSQLDB数据库\n[hsqldb]\nurl = jdbc:hsqldb:mem:mem_hutool\nuser = SA\npass =\nremarks = true\n\n# 测试用HSQLDB数据库\n[h2]\nurl = jdbc:h2:mem:h2_hutool\nuser = sa\npass =\nremarks = true\n\n# 测试用HSQLDB数据库\n[derby]\nurl = jdbc:derby:.derby/test_db;create=true\nremarks = true\n\n# 测试用Oracle数据库\n[orcl]\nurl = jdbc:oracle:thin:@//localhost:1521/XEPDB1\nuser = system\npass = 123456\nremarks = true\n\n[mysql]\nurl = jdbc:mysql://looly.centos8:3306/hutool_test?useSSL=false\nuser = root\npass = 123456\nremarks = true\n\n[postgre]\nurl = jdbc:postgresql://localhost:5432/test_hutool\nuser = postgres\npass = 123456\nremarks = true\n\n[sqlserver]\nurl = jdbc:sqlserver://looly.database.chinacloudapi.cn:1433;database=test;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.chinacloudapi.cn;loginTimeout=30;\nuser = looly@looly\npass = 123\nremarks = true\n\n# 测试用dm数据库\n[dm]\nurl = jdbc:dm://127.0.0.1:30236/schema=dm8_test\nuser = SYSDBA\npass = 123456789\nremarks = true\n\n[hana]\nurl = jdbc:sap://127.0.0.1:30015/HAP_CONN?autoReconnect=true\nuser = DB\npass = 123456\nremarks = true\n\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/db-example-c3p0.setting",
    "content": "#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n#----------------------------------------------------------------------------------------------------------------\n## 基本配置信息\n# JDBC URL，根据不同的数据库，使用相应的JDBC连接字符串\nurl = jdbc:mysql://<host>:<port>/<database_name>\n# 用户名，此处也可以使用 user 代替\nusername = 用户名\n# 密码，此处也可以使用 pass 代替\npassword = 密码\n# JDBC驱动名，可选（Hutool会自动识别）\ndriver = com.mysql.jdbc.Driver\n# 是否在日志中显示执行的SQL\nshowSql = true\n# 是否格式化显示的SQL\nformatSql = true\n\n#----------------------------------------------------------------------------------------------------------------\n## 连接池配置项\n\n## ---------------------------------------------------- C3P0\n# 连接池中保留的最大连接数。默认值: 15\nmaxPoolSize = 15\n# 连接池中保留的最小连接数，默认为：3\nminPoolSize = 3\n# 初始化连接池中的连接数，取值应在minPoolSize与maxPoolSize之间，默认为3\ninitialPoolSize = 3\n# 最大空闲时间，60秒内未使用则连接被丢弃。若为0则永不丢弃。默认值: 0\nmaxIdleTime = 0\n# 当连接池连接耗尽时，客户端调用getConnection()后等待获取新连接的时间，超时后将抛出SQLException，如设为0则无限期等待。单位毫秒。默认: 0\ncheckoutTimeout = 0\n# 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。默认值: 3\nacquireIncrement = 3\n# 定义在从数据库获取新连接失败后重复尝试的次数。默认值: 30 ；小于等于0表示无限次\nacquireRetryAttempts = 0\n# 重新尝试的时间间隔，默认为：1000毫秒\nacquireRetryDelay = 1000\n# 关闭连接时，是否提交未提交的事务，默认为false，即关闭连接，回滚未提交的事务\nautoCommitOnClose = false\n# c3p0将建一张名为Test的空表，并使用其自带的查询语句进行测试。如果定义了这个参数那么属性preferredTestQuery将被忽略。你不能在这张Test表上进行任何操作，它将只供c3p0测试使用。默认值: null\nautomaticTestTable = null\n# 如果为false，则获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常，但是数据源仍有效保留，并在下次调用getConnection()的时候继续尝试获取连接。如果设为true，那么在尝试获取连接失败后该数据源将申明已断开并永久关闭。默认: false\nbreakAfterAcquireFailure = false\n# 检查所有连接池中的空闲连接的检查频率。默认值: 0，不检查\nidleConnectionTestPeriod = 0\n# c3p0全局的PreparedStatements缓存的大小。如果maxStatements与maxStatementsPerConnection均为0，则缓存不生效，只要有一个不为0，则语句的缓存就能生效。如果默认值: 0\nmaxStatements = 0\n# maxStatementsPerConnection定义了连接池内单个连接所拥有的最大缓存statements数。默认值: 0\nmaxStatementsPerConnection = 0\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/db-example-dbcp.setting",
    "content": "#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n#----------------------------------------------------------------------------------------------------------------\n## 基本配置信息\n# JDBC URL，根据不同的数据库，使用相应的JDBC连接字符串\nurl = jdbc:mysql://<host>:<port>/<database_name>\n# 用户名，此处也可以使用 user 代替\nusername = 用户名\n# 密码，此处也可以使用 pass 代替\npassword = 密码\n# JDBC驱动名，可选（Hutool会自动识别）\ndriver = com.mysql.jdbc.Driver\n# 是否在日志中显示执行的SQL\nshowSql = true\n# 是否格式化显示的SQL\nformatSql = true\n\n\n#----------------------------------------------------------------------------------------------------------------\n## 连接池配置项\n\n## ---------------------------------------------------- Dbcp\n# (boolean) 连接池创建的连接的默认的auto-commit 状态\ndefaultAutoCommit = true\n# (boolean) 连接池创建的连接的默认的read-only 状态。 如果没有设置则setReadOnly 方法将不会被调用。 ( 某些驱动不支持只读模式， 比如：Informix)\ndefaultReadOnly = false\n# (String) 连接池创建的连接的默认的TransactionIsolation 状态。 下面列表当中的某一个： ( 参考javadoc) NONE READ_COMMITTED EAD_UNCOMMITTED REPEATABLE_READ SERIALIZABLE\ndefaultTransactionIsolation = NONE\n# (int) 初始化连接： 连接池启动时创建的初始化连接数量，1。2 版本后支持\ninitialSize = 10\n# (int) 最大活动连接： 连接池在同一时间能够分配的最大活动连接的数量， 如果设置为非正数则表示不限制\nmaxActive = 100\n# (int) 最大空闲连接： 连接池中容许保持空闲状态的最大连接数量， 超过的空闲连接将被释放， 如果设置为负数表示不限制 如果启用，将定期检查限制连接，如果空闲时间超过minEvictableIdleTimeMillis 则释放连接 （ 参考testWhileIdle ）\nmaxIdle = 8\n# (int) 最小空闲连接： 连接池中容许保持空闲状态的最小连接数量， 低于这个数量将创建新的连接， 如果设置为0 则不创建 如果连接验证失败将缩小这个值（ 参考testWhileIdle ）\nminIdle = 0\n# (int) 最大等待时间： 当没有可用连接时， 连接池等待连接被归还的最大时间( 以毫秒计数)， 超过时间则抛出异常， 如果设置为-1 表示无限等待\nmaxWait = 30000\n# (String) SQL 查询， 用来验证从连接池取出的连接， 在将连接返回给调用者之前。 如果指定， 则查询必须是一个SQL SELECT 并且必须返回至少一行记录 查询不必返回记录，但这样将不能抛出SQL异常\nvalidationQuery = SELECT 1\n# (boolean) 指明是否在从池中取出连接前进行检验， 如果检验失败， 则从池中去除连接并尝试取出另一个。注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串 参考validationInterval以获得更有效的验证\ntestOnBorrow = false\n# (boolean) 指明是否在归还到池中前进行检验 注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串\ntestOnReturn = false\n# (boolean) 指明连接是否被空闲连接回收器( 如果有) 进行检验。 如果检测失败， 则连接将被从池中去除。注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串\ntestWhileIdle = false\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/db-example-druid.setting",
    "content": "#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n#----------------------------------------------------------------------------------------------------------------\n## 基本配置信息\n# JDBC URL，根据不同的数据库，使用相应的JDBC连接字符串\nurl = jdbc:mysql://<host>:<port>/<database_name>\n# 用户名，此处也可以使用 user 代替\nusername = 用户名\n# 密码，此处也可以使用 pass 代替\npassword = 密码\n# JDBC驱动名，可选（Hutool会自动识别）\ndriver = com.mysql.jdbc.Driver\n# 是否在日志中显示执行的SQL\nshowSql = true\n# 是否格式化显示的SQL\nformatSql = true\n\n\n#----------------------------------------------------------------------------------------------------------------\n## 连接池配置项\n\n## ---------------------------------------------------- Druid\n# 初始化时建立物理连接的个数。初始化发生在显示调用init方法，或者第一次getConnection时\ninitialSize = 0\n# 最大连接池数量\nmaxActive = 8\n# 最小连接池数量\nminIdle = 0\n# 获取连接时最大等待时间，单位毫秒。配置了maxWait之后， 缺省启用公平锁，并发效率会有所下降， 如果需要可以通过配置useUnfairLock属性为true使用非公平锁。\nmaxWait = 0\n# 是否缓存preparedStatement，也就是PSCache。 PSCache对支持游标的数据库性能提升巨大，比如说oracle。 在mysql5.5以下的版本中没有PSCache功能，建议关闭掉。作者在5.5版本中使用PSCache，通过监控界面发现PSCache有缓存命中率记录， 该应该是支持PSCache。\npoolPreparedStatements = false\n# 要启用PSCache，必须配置大于0，当大于0时， poolPreparedStatements自动触发修改为true。 在Druid中，不会存在Oracle下PSCache占用内存过多的问题， 可以把这个数值配置大一些，比如说100\nmaxOpenPreparedStatements = -1\n# 用来检测连接是否有效的sql，要求是一个查询语句。 如果validationQuery为null，testOnBorrow、testOnReturn、 testWhileIdle都不会其作用。\nvalidationQuery = SELECT 1\n# 申请连接时执行validationQuery检测连接是否有效，做了这个配置会降低性能。\ntestOnBorrow = true\n# 归还连接时执行validationQuery检测连接是否有效，做了这个配置会降低性能\ntestOnReturn = false\n# 建议配置为true，不影响性能，并且保证安全性。 申请连接的时候检测，如果空闲时间大于 timeBetweenEvictionRunsMillis，执行validationQuery检测连接是否有效。\ntestWhileIdle = false\n# 有两个含义： 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据，详细看testWhileIdle属性的说明\ntimeBetweenEvictionRunsMillis = 60000\n# 物理连接初始化的时候执行的sql\nconnectionInitSqls = SELECT 1\n# 属性类型是字符串，通过别名的方式配置扩展插件， 常用的插件有： 监控统计用的filter:stat  日志用的filter:log4j 防御sql注入的filter:wall\nfilters = stat\n# 类型是List<com.alibaba.druid.filter.Filter>， 如果同时配置了filters和proxyFilters， 是组合关系，并非替换关系\nproxyFilters =\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/db-example-hikari.setting",
    "content": "#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n#----------------------------------------------------------------------------------------------------------------\n## 基本配置信息\n# JDBC URL，根据不同的数据库，使用相应的JDBC连接字符串\nurl = jdbc:mysql://<host>:<port>/<database_name>\n# 用户名，此处也可以使用 user 代替\nusername = 用户名\n# 密码，此处也可以使用 pass 代替\npassword = 密码\n# JDBC驱动名，可选（Hutool会自动识别）\ndriver = com.mysql.jdbc.Driver\n# 是否在日志中显示执行的SQL\nshowSql = true\n# 是否格式化显示的SQL\nformatSql = true\n\n\n#----------------------------------------------------------------------------------------------------------------\n## 连接池配置项\n\n## ---------------------------------------------------- HikariCP\n# 自动提交\nautoCommit = true\n# 等待连接池分配连接的最大时长（毫秒），超过这个时长还没可用的连接则发生SQLException， 缺省:30秒\nconnectionTimeout = 30000\n# 一个连接idle状态的最大时长（毫秒），超时则被释放（retired），缺省:10分钟\nidleTimeout = 600000\n# 一个连接的生命时长（毫秒），超时而且没被使用则被释放（retired），缺省:30分钟，建议设置比数据库超时时长少30秒，参考MySQL wait_timeout参数（show variables like '%timeout%';）\nmaxLifetime = 1800000\n# 获取连接前的测试SQL\nconnectionTestQuery = SELECT 1\n# 最小闲置连接数\nminimumIdle = 10\n# 连接池中允许的最大连接数。缺省值：10；推荐的公式：((core_count * 2) + effective_spindle_count)\nmaximumPoolSize = 10\n# 连接只读数据库时配置为true， 保证安全\nreadOnly = false\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/db-example-tomcat.setting",
    "content": "#===================================================================\n# 数据库配置文件样例\n# DsFactory默认读取的配置文件是config/db.setting\n# db.setting的配置包括两部分：基本连接信息和连接池配置信息。\n# 基本连接信息所有连接池都支持，连接池配置信息根据不同的连接池，连接池配置是根据连接池相应的配置项移植而来\n#===================================================================\n\n#----------------------------------------------------------------------------------------------------------------\n## 基本配置信息\n# JDBC URL，根据不同的数据库，使用相应的JDBC连接字符串\nurl = jdbc:mysql://<host>:<port>/<database_name>\n# 用户名，此处也可以使用 user 代替\nusername = 用户名\n# 密码，此处也可以使用 pass 代替\npassword = 密码\n# JDBC驱动名，可选（Hutool会自动识别）\ndriver = com.mysql.jdbc.Driver\n# 是否在日志中显示执行的SQL\nshowSql = true\n# 是否格式化显示的SQL\nformatSql = true\n\n\n#----------------------------------------------------------------------------------------------------------------\n## 连接池配置项\n\n## ---------------------------------------------------- Tomcat-Jdbc-Pool\n# (boolean) 连接池创建的连接的默认的auto-commit 状态\ndefaultAutoCommit = true\n# (boolean) 连接池创建的连接的默认的read-only 状态。 如果没有设置则setReadOnly 方法将不会被调用。 ( 某些驱动不支持只读模式， 比如：Informix)\ndefaultReadOnly = false\n# (String) 连接池创建的连接的默认的TransactionIsolation 状态。 下面列表当中的某一个： ( 参考javadoc) NONE READ_COMMITTED EAD_UNCOMMITTED REPEATABLE_READ SERIALIZABLE\ndefaultTransactionIsolation = NONE\n# (int) 初始化连接： 连接池启动时创建的初始化连接数量，1。2 版本后支持\ninitialSize = 10\n# (int) 最大活动连接： 连接池在同一时间能够分配的最大活动连接的数量， 如果设置为非正数则表示不限制\nmaxActive = 100\n# (int) 最大空闲连接： 连接池中容许保持空闲状态的最大连接数量， 超过的空闲连接将被释放， 如果设置为负数表示不限制 如果启用，将定期检查限制连接，如果空闲时间超过minEvictableIdleTimeMillis 则释放连接 （ 参考testWhileIdle ）\nmaxIdle = 8\n# (int) 最小空闲连接： 连接池中容许保持空闲状态的最小连接数量， 低于这个数量将创建新的连接， 如果设置为0 则不创建 如果连接验证失败将缩小这个值（ 参考testWhileIdle ）\nminIdle = 0\n# (int) 最大等待时间： 当没有可用连接时， 连接池等待连接被归还的最大时间( 以毫秒计数)， 超过时间则抛出异常， 如果设置为-1 表示无限等待\nmaxWait = 30000\n# (String) SQL 查询， 用来验证从连接池取出的连接， 在将连接返回给调用者之前。 如果指定， 则查询必须是一个SQL SELECT 并且必须返回至少一行记录 查询不必返回记录，但这样将不能抛出SQL异常\nvalidationQuery = SELECT 1\n# (boolean) 指明是否在从池中取出连接前进行检验， 如果检验失败， 则从池中去除连接并尝试取出另一个。注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串 参考validationInterval以获得更有效的验证\ntestOnBorrow = false\n# (boolean) 指明是否在归还到池中前进行检验 注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串\ntestOnReturn = false\n# (boolean) 指明连接是否被空闲连接回收器( 如果有) 进行检验。 如果检测失败， 则连接将被从池中去除。注意： 设置为true 后如果要生效，validationQuery 参数必须设置为非空字符串\ntestWhileIdle = false\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/example/mongo-example.setting",
    "content": "#--------------------------------------\n# MongoDB 连接设定\n# author xiaoleilu\n#--------------------------------------\n\n#每个主机答应的连接数（每个主机的连接池大小），当连接池被用光时，会被阻塞住 ，默以为10 --int\nconnectionsPerHost=100\n#线程队列数，它以connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误 --int\nthreadsAllowedToBlockForConnectionMultiplier=10\n#被阻塞线程从连接池获取连接的最长等待时间（ms） --int\nmaxWaitTime = 120000\n#在建立（打开）套接字连接时的超时时间（ms），默以为0（无穷） --int\nconnectTimeout=0\n#套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0（无穷） --int\nsocketTimeout=0\n#是否打开长连接. defaults to false --boolean\nsocketKeepAlive=false\n\nuser = test1\npass = 123456\ndatabase = test\n\n#---------------------------------- MongoDB实例连接\n[master]\nhost = 127.0.0.1:27017\n\n[slave]\nhost = 127.0.0.1:27017\n#-----------------------------------------------------"
  },
  {
    "path": "hutool-db/src/test/resources/config/mongo.setting",
    "content": "#每个主机答应的连接数（每个主机的连接池大小），当连接池被用光时，会被阻塞住 ，默以为10 --int\nconnectionsPerHost=100\n#线程队列数，它以connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误 --int\nthreadsAllowedToBlockForConnectionMultiplier=10\n#被阻塞线程从连接池获取连接的最长等待时间（ms） --int\nmaxWaitTime = 120000\n#在建立（打开）套接字连接时的超时时间（ms），默以为0（无穷） --int\nconnectTimeout=0\n#套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0（无穷） --int\nsocketTimeout=0\n#是否打开长连接. defaults to false --boolean\nsocketKeepAlive=false\n\n#---------------------------------- MongoDB实例连接\n[master]\nhost = localhost:27017\n\n[slave]\nhost = localhost:27018\n#-----------------------------------------------------\n"
  },
  {
    "path": "hutool-db/src/test/resources/config/redis.setting",
    "content": "# suppress inspection \"Annotator\" for whole file\n#----------------------------------------------------------------------------------\n# Redis客户端配置样例\n# 每一个分组代表一个Redis实例\n# 无分组的Pool配置为所有分组的共用配置，如果分组自己定义Pool配置，则覆盖共用配置\n# 池配置来自于：https://www.cnblogs.com/jklk/p/7095067.html\n#----------------------------------------------------------------------------------\n\n#----- 默认（公有）配置\n# 地址，默认localhost\nhost = localhost\n# 端口，默认6379\nport = 6379\n# 超时，默认2000\ntimeout = 2000\n# 连接超时，默认timeout\nconnectionTimeout = 2000\n# 读取超时，默认timeout\nsoTimeout = 2000\n# 密码，默认无\n#password =\n# 数据库序号，默认0\ndatabase = 0\n# 客户端名，默认\"Hutool\"\nclientName = Hutool\n# SSL连接，默认false\nssl = false;\n\n#----- 自定义分组的连接\n[custom]\n# 地址，默认localhost\nhost = localhost\n# 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true\nBlockWhenExhausted = true;\n# 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数)\nevictionPolicyClassName = org.apache.commons.pool2.impl.DefaultEvictionPolicy\n# 是否启用pool的jmx管理功能, 默认true\njmxEnabled = true;\n# 是否启用后进先出, 默认true\nlifo = true;\n# 最大空闲连接数, 默认8个\nmaxIdle = 8\n# 最小空闲连接数, 默认0\nminIdle = 0\n# 最大连接数, 默认8个\nmaxTotal = 8\n# 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1\nmaxWaitMillis = -1\n# 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)\nminEvictableIdleTimeMillis = 1800000\n# 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3\nnumTestsPerEvictionRun = 3;\n# 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断  (默认逐出策略)\nSoftMinEvictableIdleTimeMillis = 1800000\n# 在获取连接的时候检查有效性, 默认false\ntestOnBorrow = false\n# 在空闲时检查有效性, 默认false\ntestWhileIdle = false\n# 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1\ntimeBetweenEvictionRunsMillis = -1\n"
  },
  {
    "path": "hutool-db/src/test/resources/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<configuration scan=\"false\">\n\t<property name=\"format\" value=\"%d{HH:mm:ss.SSS} [%thread] %-5level %c:%L- %msg%n\" />\n\n\t<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n\t\t<!-- encoder 默认配置为PatternLayoutEncoder -->\n\t\t<encoder>\n\t\t\t<pattern>${format}</pattern>\n\t\t</encoder>\n\t</appender>\n\t\n\t<root level=\"debug\">\n\t\t<appender-ref ref=\"STDOUT\" />\n\t</root>\n\n</configuration>"
  },
  {
    "path": "hutool-db/src/test/resources/simplelogger.properties",
    "content": "org.slf4j.simpleLogger.defaultLogLevel = debug\norg.slf4j.simpleLogger.log.com.zaxxer.hikari = info"
  },
  {
    "path": "hutool-dfa/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-dfa</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 基于DFA的关键词查找</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.dfa</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.lang.DefaultSegment;\n\n/**\n * <p>\n * 匹配到的单词，包含单词，text中匹配单词的内容，以及匹配内容在text中的下标，\n * 下标可以用来做单词的进一步处理，如果替换成**\n *\n * @author 肖海斌\n */\npublic class FoundWord extends DefaultSegment<Integer> {\n\t/**\n\t * 生效的单词，即单词树中的词\n\t */\n\tprivate final String word;\n\t/**\n\t * 单词匹配到的内容，即文中的单词\n\t */\n\tprivate final String foundWord;\n\n\t/**\n\t * 构造\n\t *\n\t * @param word 生效的单词，即单词树中的词\n\t * @param foundWord 单词匹配到的内容，即文中的单词\n\t * @param startIndex 起始位置（包含）\n\t * @param endIndex 结束位置（包含）\n\t */\n\tpublic FoundWord(String word, String foundWord, int startIndex, int endIndex) {\n\t\tsuper(startIndex, endIndex);\n\t\tthis.word = word;\n\t\tthis.foundWord = foundWord;\n\t}\n\n\t/**\n\t * 获取生效的单词，即单词树中的词\n\t *\n\t * @return 生效的单词\n\t */\n\tpublic String getWord() {\n\t\treturn word;\n\t}\n\n\t/**\n\t * 获取单词匹配到的内容，即文中的单词\n\t * @return 单词匹配到的内容\n\t */\n\tpublic String getFoundWord() {\n\t\treturn foundWord;\n\t}\n\n\t/**\n\t * 默认的，只输出匹配到的关键字\n\t * @return 匹配到的关键字\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn this.foundWord;\n\t}\n}\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java",
    "content": "package cn.hutool.dfa;\n\n/**\n * @author 肖海斌\n * 敏感词过滤处理器，默认按字符数替换成*\n */\npublic interface SensitiveProcessor {\n\n\t/**\n\t * 敏感词过滤处理\n\t * @param foundWord 敏感词匹配到的内容\n\t * @return 敏感词过滤后的内容，默认按字符数替换成*\n\t */\n\tdefault String process(FoundWord foundWord) {\n\t\tint length = foundWord.getFoundWord().length();\n\t\tStringBuilder sb = new StringBuilder(length);\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tsb.append(\"*\");\n\t\t}\n\t\treturn sb.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONUtil;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 敏感词工具类\n *\n * @author Looly\n */\npublic final class SensitiveUtil {\n\n\tpublic static final char DEFAULT_SEPARATOR = StrUtil.C_COMMA;\n\tprivate static final WordTree sensitiveTree = new WordTree();\n\n\t/**\n\t * @return 是否已经被初始化\n\t */\n\tpublic static boolean isInited() {\n\t\treturn false == sensitiveTree.isEmpty();\n\t}\n\n\t/**\n\t * 初始化敏感词树\n\t *\n\t * @param isAsync        是否异步初始化\n\t * @param sensitiveWords 敏感词列表\n\t */\n\tpublic static void init(final Collection<String> sensitiveWords, boolean isAsync) {\n\t\tif (isAsync) {\n\t\t\tThreadUtil.execAsync(() -> {\n\t\t\t\tinit(sensitiveWords);\n\t\t\t\treturn true;\n\t\t\t});\n\t\t} else {\n\t\t\tinit(sensitiveWords);\n\t\t}\n\t}\n\n\t/**\n\t * 初始化敏感词树\n\t *\n\t * @param sensitiveWords 敏感词列表\n\t */\n\tpublic static void init(Collection<String> sensitiveWords) {\n\t\tsensitiveTree.clear();\n\t\tsensitiveTree.addWords(sensitiveWords);\n//\t\tlog.debug(\"Sensitive init finished, sensitives: {}\", sensitiveWords);\n\t}\n\n\t/**\n\t * 初始化敏感词树\n\t *\n\t * @param sensitiveWords 敏感词列表组成的字符串\n\t * @param isAsync        是否异步初始化\n\t * @param separator      分隔符\n\t */\n\tpublic static void init(String sensitiveWords, char separator, boolean isAsync) {\n\t\tif (StrUtil.isNotBlank(sensitiveWords)) {\n\t\t\tinit(StrUtil.split(sensitiveWords, separator), isAsync);\n\t\t}\n\t}\n\n\t/**\n\t * 初始化敏感词树，使用逗号分隔每个单词\n\t *\n\t * @param sensitiveWords 敏感词列表组成的字符串\n\t * @param isAsync        是否异步初始化\n\t */\n\tpublic static void init(String sensitiveWords, boolean isAsync) {\n\t\tinit(sensitiveWords, DEFAULT_SEPARATOR, isAsync);\n\t}\n\n\t/**\n\t * 设置字符过滤规则，通过定义字符串过滤规则，过滤不需要的字符<br>\n\t * 当accept为false时，此字符不参与匹配\n\t *\n\t * @param charFilter 过滤函数\n\t * @since 5.4.4\n\t */\n\tpublic static void setCharFilter(Filter<Character> charFilter) {\n\t\tif (charFilter != null) {\n\t\t\tsensitiveTree.setCharFilter(charFilter);\n\t\t}\n\t}\n\n\t/**\n\t * 是否包含敏感词\n\t *\n\t * @param text 文本\n\t * @return 是否包含\n\t */\n\tpublic static boolean containsSensitive(String text) {\n\t\treturn sensitiveTree.isMatch(text);\n\t}\n\n\t/**\n\t * 是否包含敏感词\n\t *\n\t * @param obj bean，会被转为JSON字符串\n\t * @return 是否包含\n\t */\n\tpublic static boolean containsSensitive(Object obj) {\n\t\treturn sensitiveTree.isMatch(JSONUtil.toJsonStr(obj));\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的第一个敏感词\n\t *\n\t * @param text 文本\n\t * @return 敏感词\n\t * @since 5.5.3\n\t */\n\tpublic static FoundWord getFoundFirstSensitive(String text) {\n\t\treturn sensitiveTree.matchWord(text);\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的第一个敏感词\n\t *\n\t * @param obj bean，会被转为JSON字符串\n\t * @return 敏感词\n\t */\n\tpublic static FoundWord getFoundFirstSensitive(Object obj) {\n\t\treturn sensitiveTree.matchWord(JSONUtil.toJsonStr(obj));\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的所有敏感词\n\t *\n\t * @param text 文本\n\t * @return 敏感词\n\t * @since 5.5.3\n\t */\n\tpublic static List<FoundWord> getFoundAllSensitive(String text) {\n\t\treturn sensitiveTree.matchAllWords(text);\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的所有敏感词<br>\n\t * 密集匹配原则：假如关键词有 ab,b，文本是abab，将匹配 [ab,b,ab]<br>\n\t * 贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t *\n\t * @param text           文本\n\t * @param isDensityMatch 是否使用密集匹配原则\n\t * @param isGreedMatch   是否使用贪婪匹配（最长匹配）原则\n\t * @return 敏感词\n\t */\n\tpublic static List<FoundWord> getFoundAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) {\n\t\treturn sensitiveTree.matchAllWords(text, -1, isDensityMatch, isGreedMatch);\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的所有敏感词\n\t *\n\t * @param bean 对象，会被转为JSON\n\t * @return 敏感词\n\t * @since 5.5.3\n\t */\n\tpublic static List<FoundWord> getFoundAllSensitive(Object bean) {\n\t\treturn sensitiveTree.matchAllWords(JSONUtil.toJsonStr(bean));\n\t}\n\n\t/**\n\t * 查找敏感词，返回找到的所有敏感词<br>\n\t * 密集匹配原则：假如关键词有 ab,b，文本是abab，将匹配 [ab,b,ab]<br>\n\t * 贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t *\n\t * @param bean           对象，会被转为JSON\n\t * @param isDensityMatch 是否使用密集匹配原则\n\t * @param isGreedMatch   是否使用贪婪匹配（最长匹配）原则\n\t * @return 敏感词\n\t * @since 5.5.3\n\t */\n\tpublic static List<FoundWord> getFoundAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) {\n\t\treturn getFoundAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch);\n\t}\n\n\t/**\n\t * 敏感词过滤\n\t *\n\t * @param bean               对象，会被转为JSON\n\t * @param isGreedMatch       贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t * @param sensitiveProcessor 敏感词处理器，默认按匹配内容的字符数替换成*\n\t * @param <T>                bean的class类型\n\t * @return 敏感词过滤处理后的bean对象\n\t */\n\tpublic static <T> T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) {\n\t\tfinal String jsonText = JSONUtil.toJsonStr(bean);\n\t\t@SuppressWarnings(\"unchecked\") final Class<T> c = (Class<T>) bean.getClass();\n\t\treturn JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c);\n\t}\n\n\t/**\n\t * 处理过滤文本中的敏感词，默认替换成*\n\t *\n\t * @param text 文本\n\t * @return 敏感词过滤处理后的文本\n\t * @since 5.7.21\n\t */\n\tpublic static String sensitiveFilter(String text) {\n\t\treturn sensitiveFilter(text, true, null);\n\t}\n\n\t/**\n\t * 处理过滤文本中的敏感词，默认替换成*\n\t *\n\t * @param text               文本\n\t * @param isGreedMatch       贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t * @param sensitiveProcessor 敏感词处理器，默认按匹配内容的字符数替换成*\n\t * @return 敏感词过滤处理后的文本\n\t */\n\tpublic static String sensitiveFilter(String text, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) {\n\t\tif (StrUtil.isEmpty(text)) {\n\t\t\treturn text;\n\t\t}\n\n\t\t//敏感词过滤场景下，不需要密集匹配\n\t\tfinal List<FoundWord> foundWordList = getFoundAllSensitive(text, true, isGreedMatch);\n\t\tif (CollUtil.isEmpty(foundWordList)) {\n\t\t\treturn text;\n\t\t}\n\t\tsensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() {\n\t\t} : sensitiveProcessor;\n\n\t\tfinal Map<Integer, FoundWord> foundWordMap = new HashMap<>(foundWordList.size(), 1);\n\t\tfoundWordList.forEach(foundWord -> foundWordMap.put(foundWord.getStartIndex(), foundWord));\n\t\tfinal int length = text.length();\n\t\tfinal StringBuilder textStringBuilder = new StringBuilder();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tfinal FoundWord fw = foundWordMap.get(i);\n\t\t\tif (fw != null) {\n\t\t\t\ttextStringBuilder.append(sensitiveProcessor.process(fw));\n\t\t\t\ti = fw.getEndIndex();\n\t\t\t} else {\n\t\t\t\ttextStringBuilder.append(text.charAt(i));\n\t\t\t}\n\t\t}\n\t\treturn textStringBuilder.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/StopChar.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.collection.CollUtil;\n\nimport java.util.Set;\n\n/**\n * 过滤词及一些简单处理\n *\n * @author Looly\n */\npublic class StopChar {\n\t/** 不需要处理的词，如标点符号、空格等 */\n\tpublic static final Set<Character> STOP_WORD = CollUtil.newHashSet(' ', '\\'', '、', '。', //\n\t\t'·', 'ˉ', 'ˇ', '々', '—', '～', '‖', '…', '‘', '’', '“', '”', '〔', '〕', '〈', '〉', '《', '》', '「', '」', '『', //\n\t\t'』', '〖', '〗', '【', '】', '±', '＋', '－', '×', '÷', '∧', '∨', '∑', '∏', '∪', '∩', '∈', '√', '⊥', '⊙', '∫', //\n\t\t'∮', '≡', '≌', '≈', '∽', '∝', '≠', '≮', '≯', '≤', '≥', '∞', '∶', '∵', '∴', '∷', '♂', '♀', '°', '′', '〃', //\n\t\t'℃', '＄', '¤', '￠', '￡', '‰', '§', '☆', '★', '〇', '○', '●', '◎', '◇', '◆', '□', '■', '△', '▽', '⊿', '▲', //\n\t\t'▼', '◣', '◤', '◢', '◥', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▉', '▊', '▋', '▌', '▍', '▎', '▏', '▓', //\n\t\t'※', '→', '←', '↑', '↓', '↖', '↗', '↘', '↙', '〓', 'ⅰ', 'ⅱ', 'ⅲ', 'ⅳ', 'ⅴ', 'ⅵ', 'ⅶ', 'ⅷ', 'ⅸ', 'ⅹ', '①', //\n\t\t'②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⒈', '⒉', '⒊', '⒋', '⒌', '⒍', '⒎', '⒏', '⒐', '⒑', '⒒', '⒓', //\n\t\t'⒔', '⒕', '⒖', '⒗', '⒘', '⒙', '⒚', '⒛', '⑴', '⑵', '⑶', '⑷', '⑸', '⑹', '⑺', '⑻', '⑼', '⑽', '⑾', '⑿', '⒀', //\n\t\t'⒁', '⒂', '⒃', '⒄', '⒅', '⒆', '⒇', 'Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ', 'Ⅺ', 'Ⅻ', '！', //\n\t\t'＃', '￥', '％', '＆', '（', '）', '＊', '，', '．', '／', '０', '１', '２', '３', '４', '５', '６', '７', //\n\t\t'８', '９', '：', '；', '＜', '＝', '＞', '？', '＠', '〔', '＼', '〕', '＾', '＿', '‘', '｛', '｜', '｝', 'Ρ', //\n\t\t'Υ', 'Φ', 'Χ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', //\n\t\t'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', '（', '）', '＾', '﹊', '﹍', '╭', '╮', '╰', '╯', '', '_', //\n\t\t'', '^', '（', '：', '！', '/', '\\\\', '\\\"', '<', '>', '`', '{', '}', '~', '(', ')', '-', //\n\t\t'$', '@', '*', '&', '#', '卐', '㎎', '㎏', '㎜', '㎝', '㎞', '㎡', '㏄', '㏎', '㏑', '㏒', '㏕', '+', '=', '?',\n\t\t':', '.', '!', ';', ']','|','%');\n\n\t/**\n\t * 判断指定的词是否是不处理的词。 如果参数为空，则返回true，因为空也属于不处理的字符。\n\t *\n\t * @param ch 指定的词\n\t * @return 是否是不处理的词\n\t */\n\tpublic static boolean isStopChar(char ch) {\n\t\treturn Character.isWhitespace(ch) || STOP_WORD.contains(ch);\n\t}\n\n\t/**\n\t * 是否为合法字符（待处理字符）\n\t *\n\t * @param ch 指定的词\n\t * @return 是否为合法字符（待处理字符）\n\t */\n\tpublic static boolean isNotStopChar(char ch) {\n\t\treturn false == isStopChar(ch);\n\t}\n}\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * DFA（Deterministic Finite Automaton 确定有穷自动机）\n * DFA单词树（以下简称单词树），常用于在某大段文字中快速查找某几个关键词是否存在。<br>\n * 单词树使用group区分不同的关键字集合，不同的分组可以共享树枝，避免重复建树。<br>\n * 单词树使用树状结构表示一组单词。<br>\n * 例如：红领巾，红河构建树后为：<br>\n * 红                    <br>\n * /      \\                 <br>\n * 领         河             <br>\n * /                            <br>\n * 巾                            <br>\n * 其中每个节点都是一个WordTree对象，查找时从上向下查找。<br>\n *\n * @author Looly\n */\npublic class WordTree extends HashMap<Character, WordTree> {\n\tprivate static final long serialVersionUID = -4646423269465809276L;\n\n\t/**\n\t * 单词字符末尾标识，用于标识单词末尾字符\n\t */\n\tprivate final Set<Character> endCharacterSet = new HashSet<>();\n\t/**\n\t * 字符过滤规则，通过定义字符串过滤规则，过滤不需要的字符，当accept为false时，此字符不参与匹配\n\t */\n\tprivate Filter<Character> charFilter = StopChar::isNotStopChar;\n\n\t//--------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic WordTree() {\n\t}\n\t//--------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 设置字符过滤规则，通过定义字符串过滤规则，过滤不需要的字符<br>\n\t * 当accept为false时，此字符不参与匹配\n\t *\n\t * @param charFilter 过滤函数\n\t * @return this\n\t * @since 5.2.0\n\t */\n\tpublic WordTree setCharFilter(Filter<Character> charFilter) {\n\t\tthis.charFilter = charFilter;\n\t\treturn this;\n\t}\n\n\t//------------------------------------------------------------------------------- add word\n\n\t/**\n\t * 增加一组单词\n\t *\n\t * @param words 单词集合\n\t * @return this\n\t */\n\tpublic WordTree addWords(Collection<String> words) {\n\t\tif (false == (words instanceof Set)) {\n\t\t\twords = new HashSet<>(words);\n\t\t}\n\t\tfor (String word : words) {\n\t\t\taddWord(word);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加一组单词\n\t *\n\t * @param words 单词数组\n\t *              @return this\n\t */\n\tpublic WordTree addWords(String... words) {\n\t\tfor (String word : CollUtil.newHashSet(words)) {\n\t\t\taddWord(word);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加单词，使用默认类型\n\t *\n\t * @param word 单词\n\t * @return this\n\t */\n\tpublic WordTree addWord(String word) {\n\t\tfinal Filter<Character> charFilter = this.charFilter;\n\t\tWordTree parent = null;\n\t\tWordTree current = this;\n\t\tWordTree child;\n\t\tCharacter lastAcceptedChar = null;\n\n\t\tfinal int length = word.length();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tchar currentChar = word.charAt(i);\n\t\t\tif (charFilter.accept(currentChar)) {//只处理合法字符\n\t\t\t\tchild = current.computeIfAbsent(currentChar, c -> new WordTree());\n\t\t\t\tparent = current;\n\t\t\t\tcurrent = child;\n\t\t\t\tlastAcceptedChar = currentChar;\n\t\t\t}\n\t\t}\n\t\t// 仅当存在父节点且存在非停顿词时，才设置词尾标记\n\t\t// 当 null != parent 条件成立时，lastAcceptedChar != null 必然成立，故也可以省去\n\t\tif (null != parent) {\n\t\t\tparent.setEnd(lastAcceptedChar);\n\t\t}\n\t\treturn this;\n\t}\n\t//------------------------------------------------------------------------------- match\n\n\t/**\n\t * 指定文本是否包含树中的词\n\t *\n\t * @param text 被检查的文本\n\t * @return 是否包含\n\t */\n\tpublic boolean isMatch(String text) {\n\t\tif (null == text) {\n\t\t\treturn false;\n\t\t}\n\t\treturn null != matchWord(text);\n\t}\n\n\t/**\n\t * 获得第一个匹配的关键字\n\t *\n\t * @param text 被检查的文本\n\t * @return 匹配到的关键字\n\t */\n\tpublic String match(String text) {\n\t\tfinal FoundWord foundWord = matchWord(text);\n\t\treturn null != foundWord ? foundWord.toString() : null;\n\t}\n\n\t/**\n\t * 获得第一个匹配的关键字\n\t *\n\t * @param text 被检查的文本\n\t * @return 匹配到的关键字\n\t * @since 5.5.3\n\t */\n\tpublic FoundWord matchWord(String text) {\n\t\tif (null == text) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal List<FoundWord> matchAll = matchAllWords(text, 1);\n\t\treturn CollUtil.get(matchAll, 0);\n\t}\n\n\t//------------------------------------------------------------------------------- match all\n\n\t/**\n\t * 找出所有匹配的关键字\n\t *\n\t * @param text 被检查的文本\n\t * @return 匹配的词列表\n\t */\n\tpublic List<String> matchAll(String text) {\n\t\treturn matchAll(text, -1);\n\t}\n\n\t/**\n\t * 找出所有匹配的关键字\n\t *\n\t * @param text 被检查的文本\n\t * @return 匹配的词列表\n\t * @since 5.5.3\n\t */\n\tpublic List<FoundWord> matchAllWords(String text) {\n\t\treturn matchAllWords(text, -1);\n\t}\n\n\t/**\n\t * 找出所有匹配的关键字\n\t *\n\t * @param text  被检查的文本\n\t * @param limit 限制匹配个数\n\t * @return 匹配的词列表\n\t */\n\tpublic List<String> matchAll(String text, int limit) {\n\t\treturn matchAll(text, limit, false, false);\n\t}\n\n\t/**\n\t * 找出所有匹配的关键字\n\t *\n\t * @param text  被检查的文本\n\t * @param limit 限制匹配个数\n\t * @return 匹配的词列表\n\t * @since 5.5.3\n\t */\n\tpublic List<FoundWord> matchAllWords(String text, int limit) {\n\t\treturn matchAllWords(text, limit, false, false);\n\t}\n\n\t/**\n\t * 找出所有匹配的关键字<br>\n\t * 密集匹配原则：假如关键词有 ab,b，文本是abab，将匹配 [ab,b,ab]<br>\n\t * 贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t *\n\t * @param text           被检查的文本\n\t * @param limit          限制匹配个数\n\t * @param isDensityMatch 是否使用密集匹配原则\n\t * @param isGreedMatch   是否使用贪婪匹配（最长匹配）原则\n\t * @return 匹配的词列表\n\t */\n\tpublic List<String> matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) {\n\t\tfinal List<FoundWord> matchAllWords = matchAllWords(text, limit, isDensityMatch, isGreedMatch);\n\t\treturn CollUtil.map(matchAllWords, FoundWord::toString, true);\n\t}\n\n\t/**\n\t * 找出所有匹配的关键字<br>\n\t * 密集匹配原则：假如关键词有 ab,b，文本是abab，将匹配 [ab,b,ab]<br>\n\t * 贪婪匹配（最长匹配）原则：假如关键字a,ab，最长匹配将匹配[a, ab]\n\t *\n\t * @param text           被检查的文本\n\t * @param limit          限制匹配个数\n\t * @param isDensityMatch 是否使用密集匹配原则\n\t * @param isGreedMatch   是否使用贪婪匹配（最长匹配）原则\n\t * @return 匹配的词列表\n\t * @since 5.5.3\n\t */\n\tpublic List<FoundWord> matchAllWords(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) {\n\t\tif (null == text) {\n\t\t\treturn null;\n\t\t}\n\n\t\tList<FoundWord> foundWords = new ArrayList<>();\n\t\tWordTree current = this;\n\t\tfinal int length = text.length();\n\t\tfinal Filter<Character> charFilter = this.charFilter;\n\t\t//存放查找到的字符缓存。完整出现一个词时加到findedWords中，否则清空\n\t\tfinal StringBuilder wordBuffer = StrUtil.builder();\n\t\tfinal StringBuilder keyBuffer = StrUtil.builder();\n\t\tchar currentChar;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\twordBuffer.setLength(0);\n\t\t\tkeyBuffer.setLength(0);\n\t\t\tfor (int j = i; j < length; j++) {\n\t\t\t\tcurrentChar = text.charAt(j);\n//\t\t\t\tConsole.log(\"i: {}, j: {}, currentChar: {}\", i, j, currentChar);\n\t\t\t\tif (false == charFilter.accept(currentChar)) {\n\t\t\t\t\tif (wordBuffer.length() > 0) {\n\t\t\t\t\t\t//做为关键词中间的停顿词被当作关键词的一部分被返回\n\t\t\t\t\t\twordBuffer.append(currentChar);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t//停顿词做为关键词的第一个字符时需要跳过\n\t\t\t\t\t\ti++;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (false == current.containsKey(currentChar)) {\n\t\t\t\t\t//非关键字符被整体略过，重新以下个字符开始检查\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\twordBuffer.append(currentChar);\n\t\t\t\tkeyBuffer.append(currentChar);\n\t\t\t\tif (current.isEnd(currentChar)) {\n\t\t\t\t\t//到达单词末尾，关键词成立，从此词的下一个位置开始查找\n\t\t\t\t\tfoundWords.add(new FoundWord(keyBuffer.toString(), wordBuffer.toString(), i, j));\n\t\t\t\t\tif (limit > 0 && foundWords.size() >= limit) {\n\t\t\t\t\t\t//超过匹配限制个数，直接返回\n\t\t\t\t\t\treturn foundWords;\n\t\t\t\t\t}\n\t\t\t\t\tif (false == isDensityMatch) {\n\t\t\t\t\t\t//如果非密度匹配，跳过匹配到的词\n\t\t\t\t\t\ti = j;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (false == isGreedMatch) {\n\t\t\t\t\t\t//如果懒惰匹配（非贪婪匹配）。当遇到第一个结尾标记就结束本轮匹配\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurrent = current.get(currentChar);\n\t\t\t\tif (null == current) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcurrent = this;\n\t\t}\n\t\treturn foundWords;\n\t}\n\t//--------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 是否末尾\n\t *\n\t * @param c 检查的字符\n\t * @return 是否末尾\n\t */\n\tprivate boolean isEnd(Character c) {\n\t\treturn this.endCharacterSet.contains(c);\n\t}\n\n\t/**\n\t * 设置是否到达末尾\n\t *\n\t * @param c 设置结尾的字符\n\t */\n\tprivate void setEnd(Character c) {\n\t\tif (null != c) {\n\t\t\tthis.endCharacterSet.add(c);\n\t\t}\n\t}\n\n\t/**\n\t * 清除所有的词,\n\t * 此方法调用后, wordTree 将被清空\n\t * endCharacterSet 也将清空\n\t */\n\t@Override\n\tpublic void clear() {\n\t\tsuper.clear();\n\t\tthis.endCharacterSet.clear();\n\t}\n\t//--------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-dfa/src/main/java/cn/hutool/dfa/package-info.java",
    "content": "/**\n * DFA全称为：Deterministic Finite Automaton,即确定有穷自动机。<br>\n * 解释起来原理其实也不难，就是用所有关键字构造一棵树，然后用正文遍历这棵树，遍历到叶子节点即表示文章中存在这个关键字。<br>\n * 我们暂且忽略构建关键词树的时间，每次查找正文只需要O(n)复杂度就可以搞定。<br>\n * \n * @author looly\n *\n */\npackage cn.hutool.dfa;"
  },
  {
    "path": "hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.collection.CollUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * DFA单元测试\n *\n * @author Looly\n *\n */\npublic class DfaTest {\n\n\t// 构建被查询的文本，包含停顿词\n\tString text = \"我有一颗$大土^豆，刚出锅的\";\n\n\t@Test\n\tpublic void matchAllTest() {\n\t\t// 构建查询树\n\t\tWordTree tree = buildWordTree();\n\n\t\t// -----------------------------------------------------------------------------------------------------------------------------------\n\t\t// 情况一：标准匹配，匹配到最短关键词，并跳过已经匹配的关键词\n\t\t// 匹配到【大】，就不再继续匹配了，因此【大土豆】不匹配\n\t\t// 匹配到【刚出锅】，就跳过这三个字了，因此【出锅】不匹配（由于刚首先被匹配，因此长的被匹配，最短匹配只针对第一个字相同选最短）\n\t\tList<String> matchAll = tree.matchAll(text, -1, false, false);\n\t\tassertEquals(CollUtil.newArrayList(\"大\", \"土^豆\", \"刚出锅\"), matchAll);\n\t}\n\n\t/**\n\t * 密集匹配原则（最短匹配）测试\n\t */\n\t@Test\n\tpublic void densityMatchTest() {\n\t\t// 构建查询树\n\t\tWordTree tree = buildWordTree();\n\n\t\t// -----------------------------------------------------------------------------------------------------------------------------------\n\t\t// 情况二：匹配到最短关键词，不跳过已经匹配的关键词\n\t\t// 【大】被匹配，最短匹配原则【大土豆】被跳过，【土豆继续被匹配】\n\t\t// 【刚出锅】被匹配，由于不跳过已经匹配的词，【出锅】被匹配\n\t\tList<String> matchAll = tree.matchAll(text, -1, true, false);\n\t\tassertEquals(matchAll, CollUtil.newArrayList(\"大\", \"土^豆\", \"刚出锅\", \"出锅\"));\n\t}\n\n\t/**\n\t * 贪婪非密集匹配原则测试\n\t */\n\t@Test\n\tpublic void greedMatchTest() {\n\t\t// 构建查询树\n\t\tWordTree tree = buildWordTree();\n\n\t\t// -----------------------------------------------------------------------------------------------------------------------------------\n\t\t// 情况三：匹配到最长关键词，跳过已经匹配的关键词\n\t\t// 匹配到【大】，由于非密集匹配，因此从下一个字符开始查找，匹配到【土豆】接着被匹配\n\t\t// 由于【刚出锅】被匹配，由于非密集匹配，【出锅】被跳过\n\t\tList<String> matchAll = tree.matchAll(text, -1, false, true);\n\t\tassertEquals(matchAll, CollUtil.newArrayList(\"大\", \"土^豆\", \"刚出锅\"));\n\n\t}\n\n\t/**\n\t * 密集匹配原则（最长匹配）和贪婪匹配原则测试\n\t */\n\t@Test\n\tpublic void densityAndGreedMatchTest() {\n\t\t// 构建查询树\n\t\tWordTree tree = buildWordTree();\n\n\t\t// -----------------------------------------------------------------------------------------------------------------------------------\n\t\t// 情况四：匹配到最长关键词，不跳过已经匹配的关键词（最全关键词）\n\t\t// 匹配到【大】，由于到最长匹配，因此【大土豆】接着被匹配，由于不跳过已经匹配的关键词，土豆继续被匹配\n\t\t// 【刚出锅】被匹配，由于不跳过已经匹配的词，【出锅】被匹配\n\t\tList<String> matchAll = tree.matchAll(text, -1, true, true);\n\t\tassertEquals(matchAll, CollUtil.newArrayList(\"大\", \"大土^豆\", \"土^豆\", \"刚出锅\", \"出锅\"));\n\n\t}\n\n\t@Test\n\tpublic void densityAndGreedMatchTest2(){\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"赵\");\n\t\ttree.addWord(\"赵阿\");\n\t\ttree.addWord(\"赵阿三\");\n\n\t\tfinal List<FoundWord> result = tree.matchAllWords(\"赵阿三在做什么\", -1, true, true);\n\t\tassertEquals(3, result.size());\n\n\t\tassertEquals(\"赵\", result.get(0).getWord());\n\t\tassertEquals(0, result.get(0).getStartIndex().intValue());\n\t\tassertEquals(0, result.get(0).getEndIndex().intValue());\n\n\t\tassertEquals(\"赵阿\", result.get(1).getWord());\n\t\tassertEquals(0, result.get(1).getStartIndex().intValue());\n\t\tassertEquals(1, result.get(1).getEndIndex().intValue());\n\n\t\tassertEquals(\"赵阿三\", result.get(2).getWord());\n\t\tassertEquals(0, result.get(2).getStartIndex().intValue());\n\t\tassertEquals(2, result.get(2).getEndIndex().intValue());\n\t}\n\n\t/**\n\t * 停顿词测试\n\t */\n\t@Test\n\tpublic void stopWordTest() {\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"tio\");\n\n\t\tList<String> all = tree.matchAll(\"AAAAAAAt-ioBBBBBBB\");\n\t\tassertEquals(all, CollUtil.newArrayList(\"t-io\"));\n\t}\n\n\t/**\n\t * Github Issue #4091\n\t * 测试当关键词以停顿词结尾时，其合法前缀是否能被正确匹配\n\t */\n\t@Test\n\tpublic void addWordWithTrailingFilteredCharTest() {\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"hello(\"); // 以停顿词 '(' 结尾\n\n\t\tList<String> matches = tree.matchAll(\"hello\", -1);\n\t\tassertEquals(1, matches.size());\n\t\tassertEquals(\"hello\", matches.get(0));\n\t}\n\n\t/**\n\t * Github Issue #4091\n\t * 测试关键词中间包含停顿词的情况\n\t */\n\t@Test\n\tpublic void addWordWithMiddleFilteredCharTest() {\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"he(llo\"); // 中间 '(' 被过滤\n\n\t\tList<String> matches = tree.matchAll(\"hello\", -1);\n\t\tassertEquals(1, matches.size());\n\t\tassertEquals(\"hello\", matches.get(0));\n\t}\n\n\t@Test\n\tpublic void aTest(){\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"women\");\n\t\tString text = \"a WOMEN todo.\".toLowerCase();\n\t\tList<String> matchAll = tree.matchAll(text, -1, false, false);\n\t\tassertEquals(\"[women]\", matchAll.toString());\n\t}\n\n\t@Test\n\tpublic void clearTest(){\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"黑\");\n\t\tassertTrue(tree.matchAll(\"黑大衣\").contains(\"黑\"));\n\t\t//clear时直接调用Map的clear并没有把endCharacterSet清理掉\n\t\ttree.clear();\n\t\ttree.addWords(\"黑大衣\",\"红色大衣\");\n\n\t\t//clear() 覆写前 这里想匹配到黑大衣，但是却匹配到了黑\n//\t\tassertFalse(tree.matchAll(\"黑大衣\").contains(\"黑大衣\"));\n//\t\tassertTrue(tree.matchAll(\"黑大衣\").contains(\"黑\"));\n\t\t//clear() 覆写后\n\t\tassertTrue(tree.matchAll(\"黑大衣\").contains(\"黑大衣\"));\n\t\tassertFalse(tree.matchAll(\"黑大衣\").contains(\"黑\"));\n\t\tassertTrue(tree.matchAll(\"红色大衣\").contains(\"红色大衣\"));\n\n\t\t//如果不覆写只能通过new出新对象才不会有问题\n\t\ttree = new WordTree();\n\t\ttree.addWords(\"黑大衣\",\"红色大衣\");\n\t\tassertTrue(tree.matchAll(\"黑大衣\").contains(\"黑大衣\"));\n\t\tassertTrue(tree.matchAll(\"红色大衣\").contains(\"红色大衣\"));\n\t}\n\n\t// ----------------------------------------------------------------------------------------------------------\n\t/**\n\t * 构建查找树\n\t *\n\t * @return 查找树\n\t */\n\tprivate WordTree buildWordTree() {\n\t\t// 构建查询树\n\t\tWordTree tree = new WordTree();\n\t\ttree.addWord(\"大\");\n\t\ttree.addWord(\"大土豆\");\n\t\ttree.addWord(\"土豆\");\n\t\ttree.addWord(\"刚出锅\");\n\t\ttree.addWord(\"出锅\");\n\t\treturn tree;\n\t}\n}\n"
  },
  {
    "path": "hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java",
    "content": "package cn.hutool.dfa;\n\nimport cn.hutool.core.collection.ListUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SensitiveUtilTest {\n\n\t@Test\n\tpublic void testSensitiveFilter() {\n\t\tList<String> wordList = new ArrayList<>();\n\t\twordList.add(\"大\");\n\t\twordList.add(\"大土豆\");\n\t\twordList.add(\"土豆\");\n\t\twordList.add(\"刚出锅\");\n\t\twordList.add(\"出锅\");\n\t\tTestBean bean = new TestBean();\n\t\tbean.setStr(\"我有一颗$大土^豆，刚出锅的\");\n\t\tbean.setNum(100);\n\t\tSensitiveUtil.init(wordList);\n\t\tbean = SensitiveUtil.sensitiveFilter(bean, true, null);\n\t\tassertEquals(bean.getStr(), \"我有一颗$****，***的\");\n\t}\n\n\t@Data\n\tpublic static class TestBean {\n\t\tprivate String str;\n\t\tprivate Integer num;\n\t}\n\n\t@Test\n\tpublic void issue2126(){\n\t\tSensitiveUtil.init(ListUtil.of(\"赵\", \"赵阿\", \"赵阿三\"));\n\n\t\tString result = SensitiveUtil.sensitiveFilter(\"赵阿三在做什么。\", true, null);\n\t\tassertEquals(\"***在做什么。\", result);\n\t}\n\n\t@Test\n\tvoid issue4182Test(){\n\t\textracted();\n\t\tfinal String s = SensitiveUtil.sensitiveFilter(\"creator_user_id=2000907612345839744\");\n\t\tassertEquals(\"creator_user_id=2000907612345839744\", s);\n\t}\n\n\tprivate static void extracted() {\n\t\tSensitiveUtil.init(ListUtil.of(\"12宝宝龙\", \"34皮卡丘\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-extra</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 扩展工具类（提供其它类库的封装）</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.extra</Automatic-Module-Name>\n\t\t<!-- versions -->\n\t\t<velocity.version>2.3</velocity.version>\n\t\t<beetl.version>3.15.14.RELEASE</beetl.version>\n\t\t<rythm.version>1.4.2</rythm.version>\n\t\t<freemarker.version>2.3.32</freemarker.version>\n\t\t<enjoy.version>5.1.3</enjoy.version>\n\t\t<thymeleaf.version>3.1.2.RELEASE</thymeleaf.version>\n\t\t<mail.version>1.6.2</mail.version>\n\t\t<jsch.version>0.1.55</jsch.version>\n\t\t<sshj.version>0.38.0</sshj.version>\n\t\t<zxing.version>3.5.3</zxing.version>\n\t\t<net.version>3.9.0</net.version>\n\t\t<emoji-java.version>5.1.1</emoji-java.version>\n\t\t<servlet-api.version>4.0.1</servlet-api.version>\n\t\t<spring-boot.version>2.7.18</spring-boot.version>\n\t\t<cglib.version>3.3.0</cglib.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-setting</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>javax.servlet</groupId>\n\t\t\t<artifactId>javax.servlet-api</artifactId>\n\t\t\t<version>${servlet-api.version}</version>\n\t\t\t<scope>provided</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>jakarta.servlet</groupId>\n\t\t\t<artifactId>jakarta.servlet-api</artifactId>\n\t\t\t<version>5.0.0</version>\n\t\t\t<scope>provided</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 模板引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.velocity</groupId>\n\t\t\t<artifactId>velocity-engine-core</artifactId>\n\t\t\t<version>${velocity.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.ibeetl</groupId>\n\t\t\t<artifactId>beetl</artifactId>\n\t\t\t<version>${beetl.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.rythmengine</groupId>\n\t\t\t<artifactId>rythm-engine</artifactId>\n\t\t\t<version>${rythm.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>commons-lang3</artifactId>\n\t\t\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>mvel2</artifactId>\n\t\t\t\t\t<groupId>org.mvel</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.freemarker</groupId>\n\t\t\t<artifactId>freemarker</artifactId>\n\t\t\t<version>${freemarker.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.jfinal</groupId>\n\t\t\t<artifactId>enjoy</artifactId>\n\t\t\t<version>${enjoy.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.thymeleaf</groupId>\n\t\t\t<artifactId>thymeleaf</artifactId>\n\t\t\t<version>${thymeleaf.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.febit.wit</groupId>\n\t\t\t<artifactId>wit-core</artifactId>\n\t\t\t<version>2.6.0</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.subchen</groupId>\n\t\t\t<artifactId>jetbrick-template</artifactId>\n\t\t\t<version>2.1.10</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 邮件 -->\n\t\t<dependency>\n\t\t\t<groupId>com.sun.mail</groupId>\n\t\t\t<artifactId>javax.mail</artifactId>\n\t\t\t<version>${mail.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>jakarta.mail</groupId>\n\t\t\t<artifactId>jakarta.mail-api</artifactId>\n\t\t\t<version>2.1.3</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.eclipse.angus</groupId>\n\t\t\t<artifactId>jakarta.mail</artifactId>\n\t\t\t<version>2.0.3</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\n\t\t<!-- SSH安全连接所使用的类库 -->\n\t\t<dependency>\n\t\t\t<groupId>com.jcraft</groupId>\n\t\t\t<artifactId>jsch</artifactId>\n\t\t\t<version>${jsch.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.hierynomus</groupId>\n\t\t\t<artifactId>sshj</artifactId>\n\t\t\t<version>${sshj.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>ch.ethz.ganymed</groupId>\n\t\t\t<artifactId>ganymed-ssh2</artifactId>\n\t\t\t<version>262</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- 二维码 -->\n\t\t<dependency>\n\t\t\t<groupId>com.google.zxing</groupId>\n\t\t\t<artifactId>core</artifactId>\n\t\t\t<version>${zxing.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- FTP工具 -->\n\t\t<dependency>\n\t\t\t<groupId>commons-net</groupId>\n\t\t\t<artifactId>commons-net</artifactId>\n\t\t\t<version>${net.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.ftpserver</groupId>\n\t\t\t<artifactId>ftpserver-core</artifactId>\n\t\t\t<version>1.2.0</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- Emoji工具依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>com.vdurmont</groupId>\n\t\t\t<artifactId>emoji-java</artifactId>\n\t\t\t<version>${emoji-java.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 分词实现 -->\n\t\t<dependency>\n\t\t\t<groupId>org.ansj</groupId>\n\t\t\t<artifactId>ansj_seg</artifactId>\n\t\t\t<version>5.1.6</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.huaban</groupId>\n\t\t\t<artifactId>jieba-analysis</artifactId>\n\t\t\t<version>1.0.2</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>commons-lang3</artifactId>\n\t\t\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.lionsoul</groupId>\n\t\t\t<artifactId>jcseg-core</artifactId>\n\t\t\t<version>2.6.3</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.chenlb.mmseg4j</groupId>\n\t\t\t<artifactId>mmseg4j-core</artifactId>\n\t\t\t<version>1.10.0</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.janeluo</groupId>\n\t\t\t<artifactId>ikanalyzer</artifactId>\n\t\t\t<version>2012_u6</version>\n\t\t\t<optional>true</optional>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t\t\t<artifactId>lucene-analyzers-common</artifactId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t\t\t<artifactId>lucene-queryparser</artifactId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t\t\t<artifactId>lucene-core</artifactId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.hankcs</groupId>\n\t\t\t<artifactId>hanlp</artifactId>\n\t\t\t<version>portable-1.8.4</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t<artifactId>lucene-analyzers-smartcn</artifactId>\n\t\t\t<version>8.11.2</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apdplat</groupId>\n\t\t\t<artifactId>word</artifactId>\n\t\t\t<version>1.3.1</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>logback-classic</artifactId>\n\t\t\t\t\t<groupId>ch.qos.logback</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>pinyin4j</artifactId>\n\t\t\t\t\t<groupId>com.belerweb</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>lucene-analyzers-common</artifactId>\n\t\t\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>lucene-core</artifactId>\n\t\t\t\t\t<groupId>org.apache.lucene</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>jedis</artifactId>\n\t\t\t\t\t<groupId>redis.clients</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.mayabot.mynlp</groupId>\n\t\t\t<artifactId>mynlp-segment</artifactId>\n\t\t\t<version>3.0.2</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- Spring Boot -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter</artifactId>\n\t\t\t<version>${spring-boot.version}</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>spring-expression</artifactId>\n\t\t\t\t\t<groupId>org.springframework</groupId>\n\t\t\t\t</exclusion>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>io.github.biezhi</groupId>\n\t\t\t<artifactId>TinyPinyin</artifactId>\n\t\t\t<version>2.0.3.RELEASE</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.belerweb</groupId>\n\t\t\t<artifactId>pinyin4j</artifactId>\n\t\t\t<version>2.5.1</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.stuxuhai</groupId>\n\t\t\t<artifactId>jpinyin</artifactId>\n\t\t\t<version>1.1.8</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.rnkrsoft.bopomofo4j</groupId>\n\t\t\t<artifactId>bopomofo4j</artifactId>\n\t\t\t<version>1.0.1</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.houbb</groupId>\n\t\t\t<artifactId>pinyin</artifactId>\n\t\t\t<version>0.4.0</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cglib</groupId>\n\t\t\t<artifactId>cglib</artifactId>\n\t\t\t<version>${cglib.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- 验证工具可选依赖 begin -->\n\t\t<dependency>\n\t\t\t<groupId>jakarta.validation</groupId>\n\t\t\t<artifactId>jakarta.validation-api</artifactId>\n\t\t\t<version>3.0.2</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.hibernate.validator</groupId>\n\t\t\t<artifactId>hibernate-validator</artifactId>\n\t\t\t<version>7.0.4.Final</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.glassfish</groupId>\n\t\t\t<artifactId>jakarta.el</artifactId>\n\t\t\t<version>4.0.2</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<!-- 验证工具可选依赖 end -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t<version>${spring-boot.version}</version>\n\t\t\t<scope>test</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>junit-jupiter</artifactId>\n\t\t\t\t\t<groupId>org.junit.jupiter</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>ch.qos.logback</groupId>\n\t\t\t<artifactId>logback-classic</artifactId>\n\t\t\t<version>1.4.14</version>\n\t\t\t<scope>test</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t</dependency>\n\t\t<!-- 表达式引擎可选依赖 begin -->\n\t\t<dependency>\n\t\t\t<groupId>com.googlecode.aviator</groupId>\n\t\t\t<artifactId>aviator</artifactId>\n\t\t\t<version>5.4.3</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- 手动引入aviator的关联依赖，解决版本问题 -->\n\t\t<dependency>\n\t\t\t<groupId>commons-beanutils</groupId>\n\t\t\t<artifactId>commons-beanutils</artifactId>\n\t\t\t<version>1.9.4</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t<artifactId>commons-jexl3</artifactId>\n\t\t\t<version>3.3</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.mvel</groupId>\n\t\t\t<artifactId>mvel2</artifactId>\n\t\t\t<version>2.5.2.Final</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.jfirer</groupId>\n\t\t\t<artifactId>jfireEl</artifactId>\n\t\t\t<version>1.0</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-expression</artifactId>\n\t\t\t<version>5.3.39</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.mozilla</groupId>\n\t\t\t<artifactId>rhino</artifactId>\n\t\t\t<version>1.7.15.1</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba</groupId>\n\t\t\t<artifactId>QLExpress</artifactId>\n\t\t\t<version>3.3.4</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- 表达式引擎可选依赖 end -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t<artifactId>commons-compress</artifactId>\n\t\t\t<version>1.28.0</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/cglib/BeanCopierCache.java",
    "content": "package cn.hutool.extra.cglib;\n\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.util.StrUtil;\nimport net.sf.cglib.beans.BeanCopier;\nimport net.sf.cglib.core.Converter;\n\n/**\n * BeanCopier属性缓存<br>\n * 缓存用于防止多次反射造成的性能问题\n *\n * @author Looly\n * @since 5.4.1\n */\npublic enum BeanCopierCache {\n\t/**\n\t * BeanCopier属性缓存单例\n\t */\n\tINSTANCE;\n\n\tprivate final WeakKeyValueConcurrentMap<String, BeanCopier> cache = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素\n\t *\n\t * @param srcClass    源Bean的类\n\t * @param targetClass 目标Bean的类\n\t * @param converter   转换器\n\t * @return Map中对应的BeanCopier\n\t */\n\tpublic BeanCopier get(Class<?> srcClass, Class<?> targetClass, Converter converter) {\n\t\treturn get(srcClass, targetClass, null != converter);\n\t}\n\n\t/**\n\t * 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素\n\t *\n\t * @param srcClass     源Bean的类\n\t * @param targetClass  目标Bean的类\n\t * @param useConverter 是否使用转换器\n\t * @return Map中对应的BeanCopier\n\t * @since 5.8.0\n\t */\n\tpublic BeanCopier get(Class<?> srcClass, Class<?> targetClass, boolean useConverter) {\n\t\tfinal String key = genKey(srcClass, targetClass, useConverter);\n\t\treturn cache.computeIfAbsent(key, (k) -> BeanCopier.create(srcClass, targetClass, useConverter));\n\t}\n\n\t/**\n\t * 获得类与转换器生成的key<br>\n\t * 结构类似于：srcClassName#targetClassName#1 或者 srcClassName#targetClassName#0\n\t *\n\t * @param srcClass     源Bean的类\n\t * @param targetClass  目标Bean的类\n\t * @param useConverter 是否使用转换器\n\t * @return 属性名和Map映射的key\n\t */\n\tprivate String genKey(Class<?> srcClass, Class<?> targetClass, boolean useConverter) {\n\t\tfinal StringBuilder key = StrUtil.builder()\n\t\t\t\t.append(srcClass.getName())\n\t\t\t\t.append('#').append(targetClass.getName())\n\t\t\t\t.append('#').append(useConverter ? 1 : 0);\n\t\treturn key.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/cglib/CglibUtil.java",
    "content": "package cn.hutool.extra.cglib;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ReflectUtil;\nimport net.sf.cglib.beans.BeanCopier;\nimport net.sf.cglib.beans.BeanMap;\nimport net.sf.cglib.core.Converter;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * Cglib工具类\n *\n * @author looly\n * @since 5.4.1\n */\npublic class CglibUtil {\n\n\t/**\n\t * 拷贝Bean对象属性到目标类型<br>\n\t * 此方法通过指定目标类型自动创建之，然后拷贝属性\n\t *\n\t * @param <T>         目标对象类型\n\t * @param source      源bean对象\n\t * @param targetClass 目标bean类，自动实例化此对象\n\t * @return 目标对象\n\t */\n\tpublic static <T> T copy(Object source, Class<T> targetClass) {\n\t\treturn copy(source, targetClass, null);\n\t}\n\n\t/**\n\t * 拷贝Bean对象属性<br>\n\t * 此方法通过指定目标类型自动创建之，然后拷贝属性\n\t *\n\t * @param <T>         目标对象类型\n\t * @param source      源bean对象\n\t * @param targetClass 目标bean类，自动实例化此对象\n\t * @param converter   转换器，无需可传{@code null}\n\t * @return 目标对象\n\t */\n\tpublic static <T> T copy(Object source, Class<T> targetClass, Converter converter) {\n\t\tfinal T target = ReflectUtil.newInstanceIfPossible(targetClass);\n\t\tcopy(source, target, converter);\n\t\treturn target;\n\t}\n\n\t/**\n\t * 拷贝Bean对象属性\n\t *\n\t * @param source 源bean对象\n\t * @param target 目标bean对象\n\t */\n\tpublic static void copy(Object source, Object target) {\n\t\tcopy(source, target, null);\n\t}\n\n\t/**\n\t * 拷贝Bean对象属性\n\t *\n\t * @param source    源bean对象\n\t * @param target    目标bean对象\n\t * @param converter 转换器，无需可传{@code null}\n\t */\n\tpublic static void copy(Object source, Object target, Converter converter) {\n\t\tAssert.notNull(source, \"Source bean must be not null.\");\n\t\tAssert.notNull(target, \"Target bean must be not null.\");\n\n\t\tfinal Class<?> sourceClass = source.getClass();\n\t\tfinal Class<?> targetClass = target.getClass();\n\t\tfinal BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(sourceClass, targetClass, converter);\n\n\t\tbeanCopier.copy(source, target, converter);\n\t}\n\n\t/**\n\t * 拷贝List Bean对象属性\n\t *\n\t * @param <S>    源bean类型\n\t * @param <T>    目标bean类型\n\t * @param source 源bean对象list\n\t * @param target 目标bean对象\n\t * @return 目标bean对象list\n\t */\n\tpublic static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target) {\n\t\treturn copyList(source, target, null, null);\n\t}\n\n\t/**\n\t * 拷贝List Bean对象属性\n\t *\n\t * @param source    源bean对象list\n\t * @param target    目标bean对象\n\t * @param converter 转换器，无需可传{@code null}\n\t * @param <S>       源bean类型\n\t * @param <T>       目标bean类型\n\t * @return 目标bean对象list\n\t * @since 5.4.1\n\t */\n\tpublic static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, Converter converter) {\n\t\treturn copyList(source, target, converter, null);\n\t}\n\n\t/**\n\t * 拷贝List Bean对象属性\n\t *\n\t * @param source   源bean对象list\n\t * @param target   目标bean对象\n\t * @param callback 回调对象\n\t * @param <S>      源bean类型\n\t * @param <T>      目标bean类型\n\t * @return 目标bean对象list\n\t * @since 5.4.1\n\t */\n\tpublic static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, BiConsumer<S, T> callback) {\n\t\treturn copyList(source, target, null, callback);\n\t}\n\n\t/**\n\t * 拷贝List Bean对象属性\n\t *\n\t * @param source    源bean对象list\n\t * @param target    目标bean对象\n\t * @param converter 转换器，无需可传{@code null}\n\t * @param callback  回调对象\n\t * @param <S>       源bean类型\n\t * @param <T>       目标bean类型\n\t * @return 目标bean对象list\n\t */\n\tpublic static <S, T> List<T> copyList(Collection<S> source, Supplier<T> target, Converter converter, BiConsumer<S, T> callback) {\n\t\treturn source.stream().map(s -> {\n\t\t\tT t = target.get();\n\t\t\tcopy(s, t, converter);\n\t\t\tif (callback != null) {\n\t\t\t\tcallback.accept(s, t);\n\t\t\t}\n\t\t\treturn t;\n\t\t}).collect(Collectors.toList());\n\t}\n\n\t/**\n\t * 将Bean转换为Map\n\t *\n\t * @param bean Bean对象\n\t * @return {@link BeanMap}\n\t * @since 5.4.1\n\t */\n\tpublic static BeanMap toMap(Object bean) {\n\t\treturn BeanMap.create(bean);\n\t}\n\n\t/**\n\t * 将Map中的内容填充至Bean中\n\t * @param map Map\n\t * @param bean Bean\n\t * @param <T> Bean类型\n\t * @return bean\n\t * @since 5.6.3\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static <T> T fillBean(Map map, T bean){\n\t\tBeanMap.create(bean).putAll(map);\n\t\treturn bean;\n\t}\n\n\t/**\n\t * 将Map转换为Bean\n\t * @param map Map\n\t * @param beanClass Bean类\n\t * @param <T> Bean类型\n\t * @return bean\n\t * @since 5.6.3\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static <T> T toBean(Map map, Class<T> beanClass){\n\t\treturn fillBean(map, ReflectUtil.newInstanceIfPossible(beanClass));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/cglib/package-info.java",
    "content": "/**\n * Cglib库方法封装\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.cglib;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/CompressException.java",
    "content": "package cn.hutool.extra.compress;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 压缩解压异常语言异常\n *\n * @author Looly\n */\npublic class CompressException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic CompressException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic CompressException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic CompressException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic CompressException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic CompressException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic CompressException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/CompressUtil.java",
    "content": "package cn.hutool.extra.compress;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.compress.archiver.Archiver;\nimport cn.hutool.extra.compress.archiver.SevenZArchiver;\nimport cn.hutool.extra.compress.archiver.StreamArchiver;\nimport cn.hutool.extra.compress.extractor.Extractor;\nimport cn.hutool.extra.compress.extractor.SevenZExtractor;\nimport cn.hutool.extra.compress.extractor.StreamExtractor;\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory;\nimport org.apache.commons.compress.archivers.StreamingNotSupportedException;\nimport org.apache.commons.compress.compressors.CompressorException;\nimport org.apache.commons.compress.compressors.CompressorInputStream;\nimport org.apache.commons.compress.compressors.CompressorOutputStream;\nimport org.apache.commons.compress.compressors.CompressorStreamFactory;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 压缩工具类<br>\n * 基于commons-compress的压缩解压封装\n *\n * @author looly\n * @since 5.5.0\n */\npublic class CompressUtil {\n\n\t/**\n\t * 获取压缩输出流，用于压缩指定内容，支持的格式例如：\n\t * <ul>\n\t *     <li>{@value CompressorStreamFactory#GZIP}</li>\n\t *     <li>{@value CompressorStreamFactory#BZIP2}</li>\n\t *     <li>{@value CompressorStreamFactory#XZ}</li>\n\t *     <li>{@value CompressorStreamFactory#PACK200}</li>\n\t *     <li>{@value CompressorStreamFactory#SNAPPY_FRAMED}</li>\n\t *     <li>{@value CompressorStreamFactory#LZ4_BLOCK}</li>\n\t *     <li>{@value CompressorStreamFactory#LZ4_FRAMED}</li>\n\t *     <li>{@value CompressorStreamFactory#ZSTANDARD}</li>\n\t *     <li>{@value CompressorStreamFactory#DEFLATE}</li>\n\t * </ul>\n\t *\n\t * @param compressorName 压缩名称，见：{@link CompressorStreamFactory}\n\t * @param out            输出流，可以输出到内存、网络或文件\n\t * @return {@link CompressorOutputStream}\n\t */\n\tpublic static CompressorOutputStream getOut(String compressorName, OutputStream out) {\n\t\ttry {\n\t\t\treturn new CompressorStreamFactory().createCompressorOutputStream(compressorName, out);\n\t\t} catch (CompressorException e) {\n\t\t\tthrow new CompressException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取压缩输入流，用于解压缩指定内容，支持的格式例如：\n\t * <ul>\n\t *     <li>{@value CompressorStreamFactory#GZIP}</li>\n\t *     <li>{@value CompressorStreamFactory#BZIP2}</li>\n\t *     <li>{@value CompressorStreamFactory#XZ}</li>\n\t *     <li>{@value CompressorStreamFactory#PACK200}</li>\n\t *     <li>{@value CompressorStreamFactory#SNAPPY_FRAMED}</li>\n\t *     <li>{@value CompressorStreamFactory#LZ4_BLOCK}</li>\n\t *     <li>{@value CompressorStreamFactory#LZ4_FRAMED}</li>\n\t *     <li>{@value CompressorStreamFactory#ZSTANDARD}</li>\n\t *     <li>{@value CompressorStreamFactory#DEFLATE}</li>\n\t * </ul>\n\t *\n\t * @param compressorName 压缩名称，见：{@link CompressorStreamFactory}，null表示自动检测\n\t * @param in            输出流，可以输出到内存、网络或文件\n\t * @return {@link CompressorOutputStream}\n\t */\n\tpublic static CompressorInputStream getIn(String compressorName, InputStream in) {\n\t\tin = IoUtil.toMarkSupportStream(in);\n\t\ttry {\n\t\t\tif(StrUtil.isBlank(compressorName)){\n\t\t\t\tcompressorName = CompressorStreamFactory.detect(in);\n\t\t\t}\n\t\t\treturn new CompressorStreamFactory().createCompressorInputStream(compressorName, in);\n\t\t} catch (CompressorException e) {\n\t\t\tthrow new CompressException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建归档器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param file         归档输出的文件\n\t * @return Archiver\n\t */\n\tpublic static Archiver createArchiver(Charset charset, String archiverName, File file) {\n\t\tif (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {\n\t\t\treturn new SevenZArchiver(file);\n\t\t}\n\t\treturn StreamArchiver.create(charset, archiverName, file);\n\t}\n\n\t/**\n\t * 创建归档器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param out          归档输出的流\n\t * @return Archiver\n\t */\n\tpublic static Archiver createArchiver(Charset charset, String archiverName, OutputStream out) {\n\t\tif (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {\n\t\t\treturn new SevenZArchiver(out);\n\t\t}\n\t\treturn StreamArchiver.create(charset, archiverName, out);\n\t}\n\n\t/**\n\t * 创建归档解包器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset 编码，7z格式此参数无效\n\t * @param file    归档文件\n\t * @return {@link Extractor}\n\t */\n\tpublic static Extractor createExtractor(Charset charset, File file) {\n\t\treturn createExtractor(charset, null, file);\n\t}\n\n\t/**\n\t * 创建归档解包器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset      编码，7z格式此参数无效\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}，null表示自动识别\n\t * @param file         归档文件\n\t * @return {@link Extractor}\n\t */\n\tpublic static Extractor createExtractor(Charset charset, String archiverName, File file) {\n\t\tif (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {\n\t\t\treturn new SevenZExtractor(file);\n\t\t}\n\n\t\tif(StrUtil.isBlank(archiverName)){\n\t\t\tfinal String name = file.getName().toLowerCase();\n\t\t\tif(name.endsWith(\".tgz\")){\n\t\t\t\tarchiverName = \"tgz\";\n\t\t\t} else if(name.endsWith(\".tar.gz\")){\n\t\t\t\tarchiverName = \"tar.gz\";\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\treturn new StreamExtractor(charset, archiverName, file);\n\t\t} catch (CompressException e) {\n\t\t\tfinal Throwable cause = e.getCause();\n\t\t\tif (cause instanceof StreamingNotSupportedException && cause.getMessage().contains(\"7z\")) {\n\t\t\t\treturn new SevenZExtractor(file);\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * 创建归档解包器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset 编码，7z格式此参数无效\n\t * @param in      归档输入的流\n\t * @return {@link Extractor}\n\t */\n\tpublic static Extractor createExtractor(Charset charset, InputStream in) {\n\t\treturn createExtractor(charset, null, in);\n\t}\n\n\t/**\n\t * 创建归档解包器，支持：\n\t * <ul>\n\t *     <li>{@link ArchiveStreamFactory#AR}</li>\n\t *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n\t *     <li>{@link ArchiveStreamFactory#JAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#TAR}</li>\n\t *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n\t *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>\n\t * </ul>\n\t *\n\t * @param charset      编码，7z格式此参数无效\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}，null表示自动识别\n\t * @param in           归档输入的流\n\t * @return {@link Extractor}\n\t */\n\tpublic static Extractor createExtractor(Charset charset, String archiverName, InputStream in) {\n\t\tif (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {\n\t\t\treturn new SevenZExtractor(in);\n\t\t}\n\n\t\ttry {\n\t\t\treturn new StreamExtractor(charset, archiverName, in);\n\t\t} catch (CompressException e) {\n\t\t\tfinal Throwable cause = e.getCause();\n\t\t\tif (cause instanceof StreamingNotSupportedException && cause.getMessage().contains(\"7z\")) {\n\t\t\t\treturn new SevenZExtractor(in);\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/Archiver.java",
    "content": "package cn.hutool.extra.compress.archiver;\n\nimport cn.hutool.core.lang.Filter;\n\nimport java.io.Closeable;\nimport java.io.File;\n\n/**\n * 数据归档封装，归档即将几个文件或目录打成一个压缩包\n *\n * @author looly\n */\npublic interface Archiver extends Closeable {\n\n\t/**\n\t * 将文件或目录加入归档，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file 文件或目录\n\t * @return this\n\t */\n\tdefault Archiver add(File file) {\n\t\treturn add(file, null);\n\t}\n\n\t/**\n\t * 将文件或目录加入归档，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file   文件或目录\n\t * @param filter 文件过滤器，指定哪些文件或目录可以加入，当{@link Filter#accept(Object)}为true时加入。\n\t * @return this\n\t */\n\tdefault Archiver add(File file, Filter<File> filter) {\n\t\treturn add(file, null, filter);\n\t}\n\n\t/**\n\t * 将文件或目录加入归档包，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file   文件或目录\n\t * @param path   文件或目录的初始路径，null表示位于根路径\n\t * @param filter 文件过滤器，指定哪些文件或目录可以加入，当{@link Filter#accept(Object)}为true时加入。\n\t * @return this\n\t */\n\tArchiver add(File file, String path, Filter<File> filter);\n\n\t/**\n\t * 结束已经增加的文件归档，此方法不会关闭归档流，可以继续添加文件\n\t *\n\t * @return this\n\t */\n\tArchiver finish();\n\n\t/**\n\t * 无异常关闭\n\t */\n\t@Override\n\tvoid close();\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/SevenZArchiver.java",
    "content": "package cn.hutool.extra.compress.archiver;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;\nimport org.apache.commons.compress.utils.SeekableInMemoryByteChannel;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.channels.SeekableByteChannel;\n\n/**\n * 7zip格式的归档封装\n *\n * @author looly\n */\npublic class SevenZArchiver implements Archiver {\n\n\tprivate final SevenZOutputFile sevenZOutputFile;\n\n\tprivate SeekableByteChannel channel;\n\tprivate OutputStream out;\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 归档输出的文件\n\t */\n\tpublic SevenZArchiver(File file) {\n\t\ttry {\n\t\t\tthis.sevenZOutputFile = new SevenZOutputFile(file);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param out 归档输出的流\n\t */\n\tpublic SevenZArchiver(OutputStream out) {\n\t\tthis.out = out;\n\t\tthis.channel = new SeekableInMemoryByteChannel();\n\t\ttry {\n\t\t\tthis.sevenZOutputFile = new SevenZOutputFile(channel);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel 归档输出的文件\n\t */\n\tpublic SevenZArchiver(SeekableByteChannel channel) {\n\t\ttry {\n\t\t\tthis.sevenZOutputFile = new SevenZOutputFile(channel);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取{@link SevenZOutputFile}以便自定义相关设置\n\t *\n\t * @return {@link SevenZOutputFile}\n\t */\n\tpublic SevenZOutputFile getSevenZOutputFile() {\n\t\treturn this.sevenZOutputFile;\n\t}\n\n\t@Override\n\tpublic SevenZArchiver add(File file, String path, Filter<File> filter) {\n\t\ttry {\n\t\t\taddInternal(file, path, filter);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SevenZArchiver finish() {\n\t\ttry {\n\t\t\tthis.sevenZOutputFile.finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\ttry {\n\t\t\tfinish();\n\t\t} catch (Exception ignore) {\n\t\t\t//ignore\n\t\t}\n\t\tif (null != out && this.channel instanceof SeekableInMemoryByteChannel) {\n\t\t\ttry {\n\t\t\t\tout.write(((SeekableInMemoryByteChannel) this.channel).array());\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\tIoUtil.close(this.sevenZOutputFile);\n\t}\n\n\t/**\n\t * 将文件或目录加入归档包，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file   文件或目录\n\t * @param path   文件或目录的初始路径，null表示位于根路径\n\t * @param filter 文件过滤器，指定哪些文件或目录可以加入，当{@link Filter#accept(Object)}为true时加入。\n\t */\n\tprivate void addInternal(File file, String path, Filter<File> filter) throws IOException {\n\t\tif (null != filter && false == filter.accept(file)) {\n\t\t\treturn;\n\t\t}\n\t\tfinal SevenZOutputFile out = this.sevenZOutputFile;\n\n\t\tfinal String entryName;\n\t\tif(StrUtil.isNotEmpty(path)){\n\t\t\t// 非空拼接路径，格式为：path/name\n\t\t\tentryName = StrUtil.addSuffixIfNot(path, StrUtil.SLASH) + file.getName();\n\t\t} else{\n\t\t\t// 路径空直接使用文件名或目录名\n\t\t\tentryName = file.getName();\n\t\t}\n\t\tout.putArchiveEntry(out.createArchiveEntry(file, entryName));\n\n\t\tif (file.isDirectory()) {\n\t\t\t// 目录遍历写入\n\t\t\tfinal File[] files = file.listFiles();\n\t\t\tif(ArrayUtil.isNotEmpty(files)){\n\t\t\t\tfor (File childFile : files) {\n\t\t\t\t\taddInternal(childFile, entryName, filter);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (file.isFile()) {\n\t\t\t\t// 文件直接写入\n\t\t\t\tout.write(FileUtil.readBytes(file));\n\t\t\t}\n\t\t\tout.closeArchiveEntry();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/StreamArchiver.java",
    "content": "package cn.hutool.extra.compress.archiver;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.compress.CompressException;\nimport org.apache.commons.compress.archivers.ArchiveException;\nimport org.apache.commons.compress.archivers.ArchiveOutputStream;\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory;\nimport org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;\nimport org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;\nimport org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 数据归档封装，归档即将几个文件或目录打成一个压缩包<br>\n * 支持的归档文件格式为：\n * <ul>\n *     <li>{@link ArchiveStreamFactory#AR}</li>\n *     <li>{@link ArchiveStreamFactory#CPIO}</li>\n *     <li>{@link ArchiveStreamFactory#JAR}</li>\n *     <li>{@link ArchiveStreamFactory#TAR}</li>\n *     <li>{@link ArchiveStreamFactory#ZIP}</li>\n * </ul>\n *\n * @author looly\n */\npublic class StreamArchiver implements Archiver {\n\n\t/**\n\t * 创建归档器\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param file         归档输出的文件\n\t * @return StreamArchiver\n\t */\n\tpublic static StreamArchiver create(Charset charset, String archiverName, File file) {\n\t\treturn new StreamArchiver(charset, archiverName, file);\n\t}\n\n\t/**\n\t * 创建归档器\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param out          归档输出的流\n\t * @return StreamArchiver\n\t */\n\tpublic static StreamArchiver create(Charset charset, String archiverName, OutputStream out) {\n\t\treturn new StreamArchiver(charset, archiverName, out);\n\t}\n\n\tprivate final ArchiveOutputStream out;\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param file         归档输出的文件\n\t */\n\tpublic StreamArchiver(Charset charset, String archiverName, File file) {\n\t\tthis(charset, archiverName, FileUtil.getOutputStream(file));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档类型名称，见{@link ArchiveStreamFactory}\n\t * @param targetStream 归档输出的流\n\t */\n\tpublic StreamArchiver(Charset charset, String archiverName, OutputStream targetStream) {\n\t\tif(\"tgz\".equalsIgnoreCase(archiverName) || \"tar.gz\".equalsIgnoreCase(archiverName)){\n\t\t\t//issue#I5J33E，支持tgz格式解压\n\t\t\ttry {\n\t\t\t\tthis.out = new TarArchiveOutputStream(new GzipCompressorOutputStream(targetStream));\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t} else {\n\t\t\tfinal ArchiveStreamFactory factory = new ArchiveStreamFactory(charset.name());\n\t\t\ttry {\n\t\t\t\tthis.out = factory.createArchiveOutputStream(archiverName, targetStream);\n\t\t\t} catch (ArchiveException e) {\n\t\t\t\tthrow new CompressException(e);\n\t\t\t}\n\t\t}\n\n\t\t//特殊设置\n\t\tif(this.out instanceof TarArchiveOutputStream){\n\t\t\t((TarArchiveOutputStream)out).setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);\n\t\t} else if(this.out instanceof ArArchiveOutputStream){\n\t\t\t((ArArchiveOutputStream)out).setLongFileMode(ArArchiveOutputStream.LONGFILE_BSD);\n\t\t}\n\t}\n\n\t/**\n\t * 将文件或目录加入归档包，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file   文件或目录\n\t * @param path   文件或目录的初始路径，null表示位于根路径\n\t * @param filter 文件过滤器，指定哪些文件或目录可以加入，当{@link Filter#accept(Object)}为true时加入。\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\t@Override\n\tpublic StreamArchiver add(File file, String path, Filter<File> filter) throws IORuntimeException {\n\t\ttry {\n\t\t\taddInternal(file, path, filter);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 结束已经增加的文件归档，此方法不会关闭归档流，可以继续添加文件\n\t *\n\t * @return this\n\t */\n\t@Override\n\tpublic StreamArchiver finish() {\n\t\ttry {\n\t\t\tthis.out.finish();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\ttry{\n\t\t\tfinish();\n\t\t} catch (Exception ignore) {\n\t\t\t//ignore\n\t\t}\n\t\tIoUtil.close(this.out);\n\t}\n\n\t/**\n\t * 将文件或目录加入归档包，目录采取递归读取方式按照层级加入\n\t *\n\t * @param file   文件或目录\n\t * @param path   文件或目录的初始路径，{@code null}表示位于根路径\n\t * @param filter 文件过滤器，指定哪些文件或目录可以加入，当{@link Filter#accept(Object)}为true时加入。\n\t */\n\tprivate void addInternal(File file, String path, Filter<File> filter) throws IOException {\n\t\tif (null != filter && false == filter.accept(file)) {\n\t\t\treturn;\n\t\t}\n\t\tfinal ArchiveOutputStream out = this.out;\n\n\t\tfinal String entryName;\n\t\tif(StrUtil.isNotEmpty(path)){\n\t\t\t// 非空拼接路径，格式为：path/name\n\t\t\tentryName = StrUtil.addSuffixIfNot(path, StrUtil.SLASH) + file.getName();\n\t\t} else{\n\t\t\t// 路径空直接使用文件名或目录名\n\t\t\tentryName = file.getName();\n\t\t}\n\t\tout.putArchiveEntry(out.createArchiveEntry(file, entryName));\n\n\t\tif (file.isDirectory()) {\n\t\t\t// 目录遍历写入\n\t\t\tfinal File[] files = file.listFiles();\n\t\t\tif(ArrayUtil.isNotEmpty(files)){\n\t\t\t\tfor (File childFile : files) {\n\t\t\t\t\taddInternal(childFile, entryName, filter);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tout.closeArchiveEntry();\n\t\t\t}\n\t\t} else {\n\t\t\tif (file.isFile()) {\n\t\t\t\t// 文件直接写入\n\t\t\t\tFileUtil.writeToStream(file, out);\n\t\t\t}\n\t\t\tout.closeArchiveEntry();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/package-info.java",
    "content": "/**\n * 基于commons-compress的打包（压缩）封装\n *\n *  <p>\n *     见：https://commons.apache.org/proper/commons-compress/\n * </p>\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.compress.archiver;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/extractor/Extractor.java",
    "content": "package cn.hutool.extra.compress.extractor;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.commons.compress.archivers.ArchiveEntry;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.util.List;\n\n/**\n * 归档数据解包封装，用于将zip、tar等包解包为文件\n *\n * @author looly\n * @since 5.5.0\n */\npublic interface Extractor extends Closeable {\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir 目标目录\n\t */\n\tdefault void extract(File targetDir) {\n\t\textract(targetDir, null);\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir 目标目录\n\t * @param filter    解压文件过滤器，用于指定需要释放的文件，{@code null}表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t */\n\tdefault void extract(File targetDir, Filter<ArchiveEntry> filter) {\n\t\tthis.extract(targetDir, 0, filter);\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir       目标目录\n\t * @param stripComponents 清除(剥离)压缩包里面的 n 级文件夹名\n\t */\n\tdefault void extract(File targetDir, int stripComponents) {\n\t\tthis.extract(targetDir, stripComponents, null);\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir       目标目录\n\t * @param stripComponents 清除(剥离)压缩包里面的 n 级文件夹名\n\t * @param filter          解压文件过滤器，用于指定需要释放的文件，{@code null}表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t */\n\tvoid extract(File targetDir, int stripComponents, Filter<ArchiveEntry> filter);\n\n\t/**\n\t * 剥离名称\n\t *\n\t * @param name            文件名\n\t * @param stripComponents 剥离层级\n\t * @return 剥离后的文件名\n\t */\n\tdefault String stripName(String name, int stripComponents) {\n\t\tif (stripComponents <= 0) {\n\t\t\treturn name;\n\t\t}\n\t\tList<String> nameList = StrUtil.splitTrim(name, StrUtil.SLASH);\n\t\tint size = nameList.size();\n\t\tif (size > stripComponents) {\n\t\t\tnameList = CollUtil.sub(nameList, stripComponents, size);\n\t\t\treturn CollUtil.join(nameList, StrUtil.SLASH);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 无异常关闭\n\t */\n\t@Override\n\tvoid close();\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/extractor/Seven7EntryInputStream.java",
    "content": "package cn.hutool.extra.compress.extractor;\n\nimport org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;\nimport org.apache.commons.compress.archivers.sevenz.SevenZFile;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * 7z解压中文件流读取的封装\n *\n * @author looly\n * @since 5.5.0\n */\npublic class Seven7EntryInputStream extends InputStream {\n\n\tprivate final SevenZFile sevenZFile;\n\tprivate final long size;\n\tprivate long readSize = 0;\n\n\t/**\n\t * 构造\n\t *\n\t * @param sevenZFile {@link SevenZFile}\n\t * @param entry      {@link SevenZArchiveEntry}\n\t */\n\tpublic Seven7EntryInputStream(SevenZFile sevenZFile, SevenZArchiveEntry entry) {\n\t\tthis.sevenZFile = sevenZFile;\n\t\tthis.size = entry.getSize();\n\t}\n\n\t@Override\n\tpublic int available() throws IOException {\n\t\ttry {\n\t\t\treturn Math.toIntExact(this.size);\n\t\t} catch (ArithmeticException e) {\n\t\t\tthrow new IOException(\"Entry size is too large!(max than Integer.MAX)\", e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取读取的长度（字节数）\n\t *\n\t * @return 读取的字节数\n\t * @since 5.7.14\n\t */\n\tpublic long getReadSize() {\n\t\treturn this.readSize;\n\t}\n\n\t@Override\n\tpublic int read() throws IOException {\n\t\tthis.readSize++;\n\t\treturn this.sevenZFile.read();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/extractor/SevenZExtractor.java",
    "content": "package cn.hutool.extra.compress.extractor;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.commons.compress.archivers.ArchiveEntry;\nimport org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;\nimport org.apache.commons.compress.archivers.sevenz.SevenZFile;\nimport org.apache.commons.compress.utils.SeekableInMemoryByteChannel;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.channels.SeekableByteChannel;\nimport java.util.RandomAccess;\n\n/**\n * 7z格式数据解压器，即将归档打包的数据释放\n *\n * @author looly\n * @since 5.5.0\n */\npublic class SevenZExtractor implements Extractor, RandomAccess {\n\n\tprivate final SevenZFile sevenZFile;\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 包文件\n\t */\n\tpublic SevenZExtractor(File file) {\n\t\tthis(file, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file     包文件\n\t * @param password 密码，null表示无密码\n\t */\n\tpublic SevenZExtractor(File file, char[] password) {\n\t\ttry {\n\t\t\tthis.sevenZFile = new SevenZFile(file, password);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param in 包流\n\t */\n\tpublic SevenZExtractor(InputStream in) {\n\t\tthis(in, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param in       包流\n\t * @param password 密码，null表示无密码\n\t */\n\tpublic SevenZExtractor(InputStream in, char[] password) {\n\t\tthis(new SeekableInMemoryByteChannel(IoUtil.readBytes(in)), password);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel {@link SeekableByteChannel}\n\t */\n\tpublic SevenZExtractor(SeekableByteChannel channel) {\n\t\tthis(channel, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel  {@link SeekableByteChannel}\n\t * @param password 密码，null表示无密码\n\t */\n\tpublic SevenZExtractor(SeekableByteChannel channel, char[] password) {\n\t\ttry {\n\t\t\tthis.sevenZFile = new SevenZFile(channel, password);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir 目标目录\n\t * @param filter    解压文件过滤器，用于指定需要释放的文件，null表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t */\n\t@Override\n\tpublic void extract(File targetDir, int stripComponents, Filter<ArchiveEntry> filter) {\n\t\ttry {\n\t\t\textractInternal(targetDir, stripComponents, filter);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tclose();\n\t\t}\n\t}\n\n\t/**\n\t * 获取满足指定过滤要求的压缩包内的第一个文件流\n\t *\n\t * @param filter 用于指定需要释放的文件，null表示不过滤。当{@link Filter#accept(Object)}为true时返回对应流。\n\t * @return 满足过滤要求的第一个文件的流, 无满足条件的文件返回{@code null}\n\t * @since 5.7.14\n\t */\n\tpublic InputStream getFirst(Filter<ArchiveEntry> filter) {\n\t\tfinal SevenZFile sevenZFile = this.sevenZFile;\n\t\tfor (SevenZArchiveEntry entry : sevenZFile.getEntries()) {\n\t\t\tif (null != filter && false == filter.accept(entry)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn sevenZFile.getInputStream(entry);\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取指定名称的文件流\n\t *\n\t * @param entryName entry名称\n\t * @return 文件流，无文件返回{@code null}\n\t * @since 5.7.14\n\t */\n\tpublic InputStream get(String entryName) {\n\t\treturn getFirst((entry) -> StrUtil.equals(entryName, entry.getName()));\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录\n\t *\n\t * @param targetDir       目标目录\n\t * @param stripComponents 清除(剥离)压缩包里面的 n 级文件夹名\n\t * @param filter          解压文件过滤器，用于指定需要释放的文件，null表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t * @throws IOException IO异常\n\t */\n\tprivate void extractInternal(File targetDir, int stripComponents, Filter<ArchiveEntry> filter) throws IOException {\n\t\tAssert.isTrue(null != targetDir && ((false == targetDir.exists()) || targetDir.isDirectory()), \"target must be dir.\");\n\t\tfinal SevenZFile sevenZFile = this.sevenZFile;\n\t\tSevenZArchiveEntry entry;\n\t\tFile outItemFile;\n\t\twhile (null != (entry = this.sevenZFile.getNextEntry())) {\n\t\t\tif (null != filter && false == filter.accept(entry)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tString entryName = this.stripName(entry.getName(), stripComponents);\n\t\t\tif (entryName == null) {\n\t\t\t\t// 剥离文件夹层级\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\toutItemFile = FileUtil.file(targetDir, entryName);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// 创建对应目录\n\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\toutItemFile.mkdirs();\n\t\t\t} else if (entry.hasStream()) {\n\t\t\t\t// 读取entry对应数据流\n\t\t\t\tFileUtil.writeFromStream(new Seven7EntryInputStream(sevenZFile, entry), outItemFile);\n\t\t\t} else {\n\t\t\t\t// 无数据流的文件创建为空文件\n\t\t\t\tFileUtil.touch(outItemFile);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.sevenZFile);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/extractor/StreamExtractor.java",
    "content": "package cn.hutool.extra.compress.extractor;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.compress.CompressException;\nimport org.apache.commons.compress.archivers.ArchiveEntry;\nimport org.apache.commons.compress.archivers.ArchiveException;\nimport org.apache.commons.compress.archivers.ArchiveInputStream;\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory;\nimport org.apache.commons.compress.archivers.tar.TarArchiveInputStream;\nimport org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 数据解压器，即将归档打包的数据释放\n *\n * @author looly\n * @since 5.5.0\n */\npublic class StreamExtractor implements Extractor {\n\n\tprivate final ArchiveInputStream in;\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset 编码\n\t * @param file    包文件\n\t */\n\tpublic StreamExtractor(Charset charset, File file) {\n\t\tthis(charset, null, file);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档包格式，null表示自动检测\n\t * @param file         包文件\n\t */\n\tpublic StreamExtractor(Charset charset, String archiverName, File file) {\n\t\tthis(charset, archiverName, FileUtil.getInputStream(file));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset 编码\n\t * @param in      包流\n\t */\n\tpublic StreamExtractor(Charset charset, InputStream in) {\n\t\tthis(charset, null, in);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 如果抛出异常，则提供的流将被关闭\n\t *\n\t * @param charset      编码\n\t * @param archiverName 归档包格式，null表示自动检测\n\t * @param in           包流\n\t */\n\tpublic StreamExtractor(Charset charset, String archiverName, InputStream in) {\n\t\t// issue#2736 自定义ArchiveInputStream\n\t\tif (in instanceof ArchiveInputStream) {\n\t\t\tthis.in = (ArchiveInputStream) in;\n\t\t\treturn;\n\t\t}\n\n\t\tfinal ArchiveStreamFactory factory = new ArchiveStreamFactory(charset.name());\n\t\ttry {\n\t\t\tin = IoUtil.toBuffered(in);\n\t\t\tif (StrUtil.isBlank(archiverName)) {\n\t\t\t\tthis.in = factory.createArchiveInputStream(in);\n\t\t\t} else if (\"tgz\".equalsIgnoreCase(archiverName) || \"tar.gz\".equalsIgnoreCase(archiverName)) {\n\t\t\t\t//issue#I5J33E，支持tgz格式解压\n\t\t\t\ttry {\n\t\t\t\t\tthis.in = new TarArchiveInputStream(new GzipCompressorInputStream(in));\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.in = factory.createArchiveInputStream(archiverName, in);\n\t\t\t}\n\t\t} catch (ArchiveException e) {\n\t\t\t// issue#2384，如果报错可能持有文件句柄，导致无法删除文件\n\t\t\tIoUtil.close(in);\n\t\t\tthrow new CompressException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录，结束后自动关闭流，此方法只能调用一次\n\t *\n\t * @param targetDir 目标目录\n\t * @param filter    解压文件过滤器，用于指定需要释放的文件，null表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t */\n\t@Override\n\tpublic void extract(File targetDir, int stripComponents, Filter<ArchiveEntry> filter) {\n\t\ttry {\n\t\t\textractInternal(targetDir, stripComponents, filter);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tclose();\n\t\t}\n\t}\n\n\t/**\n\t * 释放（解压）到指定目录\n\t *\n\t * @param targetDir       目标目录\n\t * @param stripComponents 清除(剥离)压缩包里面的 n 级文件夹名\n\t * @param filter          解压文件过滤器，用于指定需要释放的文件，null表示不过滤。当{@link Filter#accept(Object)}为true时释放。\n\t * @throws IOException IO异常\n\t */\n\tprivate void extractInternal(File targetDir, int stripComponents, Filter<ArchiveEntry> filter) throws IOException {\n\t\tAssert.isTrue(null != targetDir && ((false == targetDir.exists()) || targetDir.isDirectory()), \"target must be dir.\");\n\t\tfinal ArchiveInputStream in = this.in;\n\t\tArchiveEntry entry;\n\t\tFile outItemFile;\n\t\twhile (null != (entry = in.getNextEntry())) {\n\t\t\tif (null != filter && false == filter.accept(entry)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (false == in.canReadEntryData(entry)) {\n\t\t\t\t// 无法读取的文件直接跳过\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tString entryName = this.stripName(entry.getName(), stripComponents);\n\t\t\tif (entryName == null) {\n\t\t\t\t// 剥离文件夹层级\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\toutItemFile = FileUtil.file(targetDir, entryName);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// 创建对应目录\n\t\t\t\t//noinspection ResultOfMethodCallIgnored\n\t\t\t\toutItemFile.mkdirs();\n\t\t\t} else {\n\t\t\t\tFileUtil.writeFromStream(in, outItemFile, false);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.in);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/extractor/package-info.java",
    "content": "/**\n * 基于commons-compress的解包（解压缩）封装\n *\n *  <p>\n *     见：https://commons.apache.org/proper/commons-compress/\n * </p>\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.compress.extractor;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/compress/package-info.java",
    "content": "/**\n * 基于commons-compress的压缩解压封装<br>\n * 支持包括：gzip, bzip2, xz, lzma, Pack200, DEFLATE, Brotli, DEFLATE64, ZStandard and Z, the archiver formats are 7z,<br>\n * ar, arj, cpio, dump, tar and zip等格式。\n *\n * <p>\n *     见：https://commons.apache.org/proper/commons-compress/\n * </p>\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.compress;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/emoji/EmojiUtil.java",
    "content": "package cn.hutool.extra.emoji;\n\nimport com.vdurmont.emoji.Emoji;\nimport com.vdurmont.emoji.EmojiManager;\nimport com.vdurmont.emoji.EmojiParser;\nimport com.vdurmont.emoji.EmojiParser.FitzpatrickAction;\n\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * 基于https://github.com/vdurmont/emoji-java的Emoji表情工具类\n * <p>\n * emoji-java文档以及别名列表见：https://github.com/vdurmont/emoji-java\n *\n * @author looly\n * @since 4.2.1\n */\npublic class EmojiUtil {\n\n\t/**\n\t * 是否为Emoji表情的Unicode符\n\t *\n\t * @param str 被测试的字符串\n\t * @return 是否为Emoji表情的Unicode符\n\t */\n\tpublic static boolean isEmoji(String str) {\n\t\treturn EmojiManager.isEmoji(str);\n\t}\n\n\t/**\n\t * 是否包含Emoji表情的Unicode符\n\t *\n\t * @param str 被测试的字符串\n\t * @return 是否包含Emoji表情的Unicode符\n\t * @since 4.5.11\n\t */\n\tpublic static boolean containsEmoji(String str) {\n\t\treturn EmojiManager.containsEmoji(str);\n\t}\n\n\t/**\n\t * 通过tag方式获取对应的所有Emoji表情\n\t *\n\t * @param tag tag标签，例如“happy”\n\t * @return Emoji表情集合，如果找不到返回null\n\t */\n\tpublic static Set<Emoji> getByTag(String tag) {\n\t\treturn EmojiManager.getForTag(tag);\n\t}\n\n\t/**\n\t * 通过别名获取Emoji\n\t *\n\t * @param alias 别名，例如“smile”\n\t * @return Emoji对象，如果找不到返回null\n\t */\n\tpublic static Emoji get(String alias) {\n\t\treturn EmojiManager.getForAlias(alias);\n\t}\n\n\t/**\n\t * 将子串中的Emoji别名（两个\":\"包围的格式）和其HTML表示形式替换为为Unicode Emoji符号\n\t * <p>\n\t * 例如：\n\t *\n\t * <pre>\n\t *  <code>:smile:</code>  替换为 <code>😄</code>\n\t * <code>&amp;#128516;</code> 替换为 <code>😄</code>\n\t * <code>:boy|type_6:</code> 替换为 <code>👦🏿</code>\n\t * </pre>\n\t *\n\t * @param str 包含Emoji别名或者HTML表现形式的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String toUnicode(String str) {\n\t\treturn EmojiParser.parseToUnicode(str);\n\t}\n\n\t/**\n\t * 将字符串中的Unicode Emoji字符转换为别名表现形式（两个\":\"包围的格式）\n\t * <p>\n\t * 例如： <code>😄</code> 转换为 <code>:smile:</code>\n\t *\n\t * <p>\n\t * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#PARSE}，则别名后会增加\"|\"并追加fitzpatrick类型\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>:boy|type_6:</code>\n\t *\n\t * <p>\n\t * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#REMOVE}，则别名后的\"|\"和类型将被去除\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>:boy:</code>\n\t *\n\t * <p>\n\t * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#IGNORE}，则别名后的类型将被忽略\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>:boy:🏿</code>\n\t *\n\t * @param str 包含Emoji Unicode字符的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String toAlias(String str) {\n\t\treturn toAlias(str, FitzpatrickAction.PARSE);\n\t}\n\n\t/**\n\t * 将字符串中的Unicode Emoji字符转换为别名表现形式（两个\":\"包围的格式），别名后会增加\"|\"并追加fitzpatrick类型\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>:boy|type_6:</code>\n\t *\n\t * @param str               包含Emoji Unicode字符的字符串\n\t * @param fitzpatrickAction {@link FitzpatrickAction}\n\t * @return 替换后的字符串\n\t */\n\tpublic static String toAlias(String str, FitzpatrickAction fitzpatrickAction) {\n\t\treturn EmojiParser.parseToAliases(str, fitzpatrickAction);\n\t}\n\n\t/**\n\t * 将字符串中的Unicode Emoji字符转换为HTML 16进制表现形式\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>&amp;#x1f466;</code>\n\t *\n\t * @param str 包含Emoji Unicode字符的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String toHtmlHex(String str) {\n\t\treturn toHtml(str, true);\n\t}\n\n\t/**\n\t * 将字符串中的Unicode Emoji字符转换为HTML表现形式（Hex方式）\n\t * <p>\n\t * 例如：<code>👦🏿</code> 转换为 <code>&amp;#128102;</code>\n\t *\n\t * @param str 包含Emoji Unicode字符的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String toHtml(String str) {\n\t\treturn toHtml(str, false);\n\t}\n\n\t/**\n\t * 将字符串中的Unicode Emoji字符转换为HTML表现形式，例如：\n\t * <pre>\n\t * 如果为hex形式，<code>👦🏿</code> 转换为 <code>&amp;#x1f466;</code>\n\t * 否则，<code>👦🏿</code> 转换为 <code>&amp;#128102;</code>\n\t * </pre>\n\t *\n\t * @param str   包含Emoji Unicode字符的字符串\n\t * @param isHex 是否hex形式\n\t * @return 替换后的字符串\n\t * @since 5.7.21\n\t */\n\tpublic static String toHtml(String str, boolean isHex) {\n\t\treturn isHex ? EmojiParser.parseToHtmlHexadecimal(str) :\n\t\t\t\tEmojiParser.parseToHtmlDecimal(str);\n\t}\n\n\t/**\n\t * 去除字符串中所有的Emoji Unicode字符\n\t *\n\t * @param str 包含Emoji字符的字符串\n\t * @return 替换后的字符串\n\t */\n\tpublic static String removeAllEmojis(String str) {\n\t\treturn EmojiParser.removeAllEmojis(str);\n\t}\n\n\t/**\n\t * 提取字符串中所有的Emoji Unicode\n\t *\n\t * @param str 包含Emoji字符的字符串\n\t * @return Emoji字符列表\n\t */\n\tpublic static List<String> extractEmojis(String str) {\n\t\treturn EmojiParser.extractEmojis(str);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/emoji/package-info.java",
    "content": "/**\n * 基于https://github.com/vdurmont/emoji-java的Emoji表情工具类\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.emoji;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/ExpressionEngine.java",
    "content": "package cn.hutool.extra.expression;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 表达式引擎API接口，通过实现此接口，完成表达式的解析和执行\n *\n * @author looll, independenter\n * @since 5.5.0\n */\npublic interface ExpressionEngine {\n\n\t/**\n\t * 执行表达式\n\t *\n\t * @param expression    表达式\n\t * @param context       表达式上下文，用于存储表达式中所需的变量值等\n\t * @param allowClassSet 允许的Class白名单\n\t * @return 执行结果\n\t */\n\tObject eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet);\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/ExpressionException.java",
    "content": "package cn.hutool.extra.expression;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 表达式语言异常\n *\n * @author Looly\n */\npublic class ExpressionException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic ExpressionException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic ExpressionException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic ExpressionException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ExpressionException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic ExpressionException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic ExpressionException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/ExpressionUtil.java",
    "content": "package cn.hutool.extra.expression;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.extra.expression.engine.ExpressionFactory;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 表达式引擎工具类\n *\n * @author looly\n * @since 5.5.0\n */\npublic class ExpressionUtil {\n\n\t/**\n\t * 获得全局单例的表达式引擎\n\t *\n\t * @return 全局单例的表达式引擎\n\t */\n\tpublic static ExpressionEngine getEngine() {\n\t\treturn ExpressionFactory.get();\n\t}\n\n\t/**\n\t * 执行表达式\n\t *\n\t * @param expression 表达式\n\t * @param context    表达式上下文，用于存储表达式中所需的变量值等\n\t * @return 执行结果\n\t */\n\tpublic static Object eval(String expression, Map<String, Object> context) {\n\t\treturn eval(expression, context, ListUtil.empty());\n\t}\n\n\t/**\n\t * 执行表达式\n\t *\n\t * @param expression 表达式\n\t * @param context    表达式上下文，用于存储表达式中所需的变量值等\n\t * @param allowClassSet 允许的Class白名单\n\t * @return 执行结果\n\t * @since 5.8.21\n\t */\n\tpublic static Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\treturn getEngine().eval(expression, context, allowClassSet);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/ExpressionFactory.java",
    "content": "package cn.hutool.extra.expression.engine;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.ServiceLoaderUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.StaticLog;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport cn.hutool.extra.expression.ExpressionException;\n\n/**\n * 表达式语言引擎工厂类，用于根据用户引入的表达式jar，自动创建对应的引擎对象\n *\n * @since 5.5.0\n * @author looly\n */\npublic class ExpressionFactory {\n\n\t/**\n\t * 获得单例的{@link ExpressionEngine}\n\t *\n\t * @return 单例的{@link ExpressionEngine}\n\t */\n\tpublic static ExpressionEngine get(){\n\t\treturn Singleton.get(ExpressionEngine.class.getName(), ExpressionFactory::create);\n\t}\n\n\t/**\n\t * 根据用户引入的表达式引擎jar，自动创建对应的拼音引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @return {@link ExpressionEngine}\n\t */\n\tpublic static ExpressionEngine create() {\n\t\tfinal ExpressionEngine engine = doCreate();\n\t\tStaticLog.debug(\"Use [{}] Engine As Default.\", StrUtil.removeSuffix(engine.getClass().getSimpleName(), \"Engine\"));\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 根据用户引入的拼音引擎jar，自动创建对应的拼音引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @return {@link ExpressionEngine}\n\t */\n\tprivate static ExpressionEngine doCreate() {\n\t\tfinal ExpressionEngine engine = ServiceLoaderUtil.loadFirstAvailable(ExpressionEngine.class);\n\t\tif(null != engine){\n\t\t\treturn engine;\n\t\t}\n\n\t\tthrow new ExpressionException(\"No expression jar found ! Please add one of it to your project !\");\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/aviator/AviatorEngine.java",
    "content": "package cn.hutool.extra.expression.engine.aviator;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport com.googlecode.aviator.AviatorEvaluator;\nimport com.googlecode.aviator.AviatorEvaluatorInstance;\nimport com.googlecode.aviator.Options;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\n\n/**\n * Aviator引擎封装<br>\n * 见：https://github.com/killme2008/aviatorscript\n *\n * @author looly\n * @since 5.5.0\n */\npublic class AviatorEngine implements ExpressionEngine {\n\n\tprivate final AviatorEvaluatorInstance engine;\n\n\t/**\n\t * 构造\n\t */\n\tpublic AviatorEngine() {\n\t\tengine = AviatorEvaluator.getInstance();\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\t// issue#I6AJWJ\n\t\tengine.setOption(Options.ALLOWED_CLASS_SET,\n\t\t\tCollUtil.isEmpty(allowClassSet) ? Collections.emptySet() : CollUtil.newHashSet(allowClassSet));\n\t\treturn engine.execute(expression, context);\n\t}\n\n\t/**\n\t * 获取{@link AviatorEvaluatorInstance}\n\t *\n\t * @return {@link AviatorEvaluatorInstance}\n\t */\n\tpublic AviatorEvaluatorInstance getEngine() {\n\t\treturn this.engine;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/aviator/package-info.java",
    "content": "/**\n * Aviator引擎封装，见：https://github.com/killme2008/aviatorscript\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.expression.engine.aviator;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/jexl/JexlEngine.java",
    "content": "package cn.hutool.extra.expression.engine.jexl;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport org.apache.commons.jexl3.JexlBuilder;\nimport org.apache.commons.jexl3.MapContext;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * Jexl3引擎封装<br>\n * 见：https://github.com/apache/commons-jexl\n *\n * @since 5.5.0\n * @author looly\n */\npublic class JexlEngine implements ExpressionEngine {\n\n\tprivate final org.apache.commons.jexl3.JexlEngine engine;\n\n\tpublic JexlEngine(){\n\t\tengine = (new JexlBuilder()).cache(512).strict(true).silent(false).create();\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\tfinal MapContext mapContext = new MapContext(context);\n\n\t\ttry{\n\t\t\treturn engine.createExpression(expression).evaluate(mapContext);\n\t\t} catch (Exception ignore){\n\t\t\t// https://gitee.com/chinabugotech/hutool/issues/I4B70D\n\t\t\t// 支持脚本\n\t\t\treturn engine.createScript(expression).execute(mapContext);\n\t\t}\n\t}\n\n\t/**\n\t * 获取{@link org.apache.commons.jexl3.JexlEngine}\n\t *\n\t * @return {@link org.apache.commons.jexl3.JexlEngine}\n\t */\n\tpublic org.apache.commons.jexl3.JexlEngine getEngine() {\n\t\treturn this.engine;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/jexl/package-info.java",
    "content": "/**\n * Jexl3引擎封装,见：https://github.com/apache/commons-jexl\n * \n * @author looly\n */\npackage cn.hutool.extra.expression.engine.jexl;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/jfireel/JfireELEngine.java",
    "content": "package cn.hutool.extra.expression.engine.jfireel;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport com.jfirer.jfireel.expression.Expression;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * JfireEL引擎封装<br>\n * 见：https://gitee.com/eric_ds/jfireEL\n *\n * @since 5.5.0\n * @author looly\n */\npublic class JfireELEngine implements ExpressionEngine {\n\n\tstatic {\n\t\tcheckEngineExist(Expression.class);\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic JfireELEngine(){\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\treturn Expression.parse(expression).calculate(context);\n\t}\n\n\tprivate static void checkEngineExist(Class<?> clazz){\n\t\t// do nothing\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/jfireel/package-info.java",
    "content": "/**\n * JfireEL引擎封装<br>\n * 见：https://gitee.com/eric_ds/jfireEL\n *\n * @author looly\n */\npackage cn.hutool.extra.expression.engine.jfireel;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/MvelEngine.java",
    "content": "package cn.hutool.extra.expression.engine.mvel;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport org.mvel2.MVEL;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * MVEL (MVFLEX Expression Language)引擎封装<br>\n * 见：https://github.com/mvel/mvel\n *\n * @since 5.5.0\n * @author looly\n */\npublic class MvelEngine implements ExpressionEngine {\n\n\tstatic {\n\t\tcheckEngineExist(MVEL.class);\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic MvelEngine(){\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\treturn MVEL.eval(expression, context);\n\t}\n\n\tprivate static void checkEngineExist(Class<?> clazz){\n\t\t// do nothing\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/package-info.java",
    "content": "/**\n * MVEL (MVFLEX Expression Language)引擎封装<br>\n * 见：https://github.com/mvel/mvel\n * \n * @author looly\n */\npackage cn.hutool.extra.expression.engine.mvel;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/package-info.java",
    "content": "/**\n * 表达式语言引擎封装\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.expression.engine;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/qlexpress/QLExpressEngine.java",
    "content": "package cn.hutool.extra.expression.engine.qlexpress;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport cn.hutool.extra.expression.ExpressionException;\nimport com.ql.util.express.DefaultContext;\nimport com.ql.util.express.ExpressRunner;\nimport com.ql.util.express.config.QLExpressRunStrategy;\n\nimport javax.naming.InitialContext;\nimport java.lang.reflect.Method;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * QLExpress引擎封装<br>\n * 见：https://github.com/alibaba/QLExpress\n *\n * @author looly\n * @since 5.8.9\n */\npublic class QLExpressEngine implements ExpressionEngine {\n\n\tprivate final ExpressRunner engine;\n\n\t/**\n\t * 构造\n\t */\n\tpublic QLExpressEngine() {\n\t\tengine = new ExpressRunner();\n\n\t\t// issue#3994@Github\n\t\t// Enforce blacklisting of high-risk method invocations\n\t\tQLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);\n\t\t// Explicitly forbid JNDI lookup calls through InitialContext\n\t\tQLExpressRunStrategy.addSecurityRiskMethod(InitialContext.class, \"doLookup\");\n\t}\n\n\t@Override\n\tpublic Object eval(final String expression, final Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\t// issue#3994@Github\n\t\tif (null != allowClassSet) {\n\t\t\tfor (Class<?> clazz : allowClassSet) {\n\t\t\t\tfor (Method method : clazz.getDeclaredMethods()) {\n\t\t\t\t\tQLExpressRunStrategy.addSecureMethod(clazz, method.getName());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfinal DefaultContext<String, Object> defaultContext = new DefaultContext<>();\n\t\tdefaultContext.putAll(context);\n\t\ttry {\n\t\t\treturn engine.execute(expression, defaultContext, null, true, false);\n\t\t} catch (final Exception e) {\n\t\t\tthrow new ExpressionException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/qlexpress/package-info.java",
    "content": "/**\n * QLExpress引擎封装<br>\n * 见：https://github.com/alibaba/QLExpress\n *\n * @author looly\n */\npackage cn.hutool.extra.expression.engine.qlexpress;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/RhinoEngine.java",
    "content": "package cn.hutool.extra.expression.engine.rhino;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport org.mozilla.javascript.Context;\nimport org.mozilla.javascript.Scriptable;\nimport org.mozilla.javascript.ScriptableObject;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * rhino引擎封装<br>\n * 见：https://github.com/mozilla/rhino\n *\n * @author lzpeng\n * @since 5.5.2\n */\npublic class RhinoEngine implements ExpressionEngine {\n\n\tstatic {\n\t\tcheckEngineExist(Context.class);\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\tfinal Context ctx = Context.enter();\n\t\tfinal Scriptable scope = ctx.initStandardObjects();\n\t\tif (MapUtil.isNotEmpty(context)) {\n\t\t\tcontext.forEach((key, value)->{\n\t\t\t\t// 将java对象转为js对象后放置于JS的作用域中\n\t\t\t\tScriptableObject.putProperty(scope, key, Context.javaToJS(value, scope));\n\t\t\t});\n\t\t}\n\t\tfinal Object result = ctx.evaluateString(scope, expression, \"rhino.js\", 1, null);\n\t\tContext.exit();\n\t\treturn result;\n\t}\n\n\tprivate static void checkEngineExist(Class<?> clazz){\n\t\t// do nothing\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/package-info.java",
    "content": "/**\n * rhino引擎封装<br>\n * 见：https://github.com/mozilla/rhino\n *\n * @author lzpeng\n */\npackage cn.hutool.extra.expression.engine.rhino;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/SpELEngine.java",
    "content": "package cn.hutool.extra.expression.engine.spel;\n\nimport cn.hutool.extra.expression.ExpressionEngine;\nimport org.springframework.expression.EvaluationContext;\nimport org.springframework.expression.ExpressionParser;\nimport org.springframework.expression.spel.standard.SpelExpressionParser;\nimport org.springframework.expression.spel.support.StandardEvaluationContext;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * Spring-Expression引擎封装<br>\n * 见：https://github.com/spring-projects/spring-framework/tree/master/spring-expression\n *\n * @since 5.5.0\n * @author looly\n */\npublic class SpELEngine implements ExpressionEngine {\n\n\tprivate final ExpressionParser parser;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SpELEngine(){\n\t\tparser = new SpelExpressionParser();\n\t}\n\n\t@Override\n\tpublic Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {\n\t\tfinal EvaluationContext evaluationContext = new StandardEvaluationContext();\n\t\tcontext.forEach(evaluationContext::setVariable);\n\t\treturn parser.parseExpression(expression).getValue(evaluationContext);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/package-info.java",
    "content": "/**\n * Spring-Expression引擎封装<br>\n * 见：https://github.com/spring-projects/spring-framework/tree/master/spring-expression\n *\n * @author looly\n */\npackage cn.hutool.extra.expression.engine.spel;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/expression/package-info.java",
    "content": "/**\n * 表达式语言引擎封装\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.expression;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.nio.charset.Charset;\nimport java.util.List;\n\n/**\n * 抽象FTP类，用于定义通用的FTP方法\n *\n * @author looly\n * @since 4.1.14\n */\npublic abstract class AbstractFtp implements Closeable {\n\n\t/**\n\t * 默认编码\n\t */\n\tpublic static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\n\tprotected FtpConfig ftpConfig;\n\n\t/**\n\t * 构造\n\t *\n\t * @param config FTP配置\n\t * @since 5.3.3\n\t */\n\tprotected AbstractFtp(FtpConfig config) {\n\t\tthis.ftpConfig = config;\n\t}\n\n\t/**\n\t * 如果连接超时的话，重新进行连接\n\t *\n\t * @return this\n\t * @since 4.5.2\n\t */\n\tpublic abstract AbstractFtp reconnectIfTimeout();\n\n\t/**\n\t * 打开指定目录，具体逻辑取决于实现，例如在FTP中，进入失败返回{@code false}， SFTP中则抛出异常\n\t *\n\t * @param directory directory\n\t * @return 是否打开目录\n\t */\n\tpublic abstract boolean cd(String directory);\n\n\t/**\n\t * 打开上级目录\n\t *\n\t * @return 是否打开目录\n\t * @since 4.0.5\n\t */\n\tpublic boolean toParent() {\n\t\treturn cd(\"..\");\n\t}\n\n\t/**\n\t * 远程当前目录（工作目录）\n\t *\n\t * @return 远程当前目录\n\t */\n\tpublic abstract String pwd();\n\n\t/**\n\t * 判断给定路径是否为目录\n\t *\n\t * @param dir 被判断的路径\n\t * @return 是否为目录\n\t * @since 5.7.5\n\t */\n\tpublic boolean isDir(String dir) {\n\t\tfinal String workDir = pwd();\n\t\ttry {\n\t\t\treturn cd(dir);\n\t\t} finally {\n\t\t\tcd(workDir);\n\t\t}\n\t}\n\n\t/**\n\t * 在当前远程目录（工作目录）下创建新的目录\n\t *\n\t * @param dir 目录名\n\t * @return 是否创建成功\n\t */\n\tpublic abstract boolean mkdir(String dir);\n\n\t/**\n\t * 文件或目录是否存在\n\t *\n\t * @param path 目录\n\t * @return 是否存在\n\t */\n\tpublic boolean exist(String path) {\n\t\tif (StrUtil.isBlank(path)) {\n\t\t\treturn false;\n\t\t}\n\t\t// 目录验证\n\t\tif (isDir(path)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (CharUtil.isFileSeparator(path.charAt(path.length() - 1))) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal String fileName = FileUtil.getName(path);\n\t\tif (\".\".equals(fileName) || \"..\".equals(fileName)) {\n\t\t\treturn false;\n\t\t}\n\t\t// 文件验证\n\t\tfinal String dir = StrUtil.emptyToDefault(StrUtil.removeSuffix(path, fileName), \".\");\n\t\t// issue#I7CSQ9 检查父目录为目录且是否存在\n\t\tif(false == isDir(dir)){\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal List<String> names;\n\t\ttry {\n\t\t\tnames = ls(dir);\n\t\t} catch (FtpException ignore) {\n\t\t\treturn false;\n\t\t}\n\t\treturn containsIgnoreCase(names, fileName);\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件和目录，不会递归遍历\n\t *\n\t * @param path 需要遍历的目录\n\t * @return 文件和目录列表\n\t */\n\tpublic abstract List<String> ls(String path);\n\n\t/**\n\t * 删除指定目录下的指定文件\n\t *\n\t * @param path 目录路径\n\t * @return 是否存在\n\t */\n\tpublic abstract boolean delFile(String path);\n\n\t/**\n\t * 删除文件夹及其文件夹下的所有文件\n\t *\n\t * @param dirPath 文件夹路径\n\t * @return boolean 是否删除成功\n\t */\n\tpublic abstract boolean delDir(String dirPath);\n\n\t/**\n\t * 创建指定文件夹及其父目录，从根目录开始创建，创建完成后回到默认的工作目录\n\t *\n\t * @param dir 文件夹路径，绝对路径\n\t */\n\tpublic void mkDirs(String dir) {\n\t\tfinal String[] dirs = StrUtil.trim(dir).split(\"[\\\\\\\\/]+\");\n\n\t\tfinal String now = pwd();\n\t\tif (dirs.length > 0 && StrUtil.isEmpty(dirs[0])) {\n\t\t\t//首位为空，表示以/开头\n\t\t\tthis.cd(StrUtil.SLASH);\n\t\t}\n\t\tfor (String s : dirs) {\n\t\t\tif (StrUtil.isNotEmpty(s)) {\n\t\t\t\tboolean exist = true;\n\t\t\t\ttry {\n\t\t\t\t\tif (false == cd(s)) {\n\t\t\t\t\t\texist = false;\n\t\t\t\t\t}\n\t\t\t\t} catch (FtpException e) {\n\t\t\t\t\texist = false;\n\t\t\t\t}\n\t\t\t\tif (false == exist) {\n\t\t\t\t\t//目录不存在时创建\n\t\t\t\t\tmkdir(s);\n\t\t\t\t\tcd(s);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// 切换回工作目录\n\t\tcd(now);\n\t}\n\n\t/**\n\t * 将本地文件上传到目标服务器，目标文件名为destPath，若destPath为目录，则目标文件名将与file文件名相同。\n\t * 覆盖模式\n\t *\n\t * @param destPath 服务端路径，可以为{@code null} 或者相对路径或绝对路径\n\t * @param file     需要上传的文件\n\t * @return 是否成功\n\t */\n\tpublic abstract boolean upload(String destPath, File file);\n\n\t/**\n\t * 下载文件\n\t *\n\t * @param path    文件路径\n\t * @param outFile 输出文件或目录\n\t */\n\tpublic abstract void download(String path, File outFile);\n\n\t/**\n\t * 下载文件-避免未完成的文件<br>\n\t * 来自：https://gitee.com/chinabugotech/hutool/pulls/407<br>\n\t * 此方法原理是先在目标文件同级目录下创建临时文件，下载之，等下载完毕后重命名，避免因下载错误导致的文件不完整。\n\t *\n\t * @param path     文件路径\n\t * @param outFile  输出文件或目录\n\t * @param tempFileSuffix 临时文件后缀，默认\".temp\"\n\t * @since 5.7.12\n\t */\n\tpublic void download(String path, File outFile, String tempFileSuffix) {\n\t\tif(StrUtil.isBlank(tempFileSuffix)){\n\t\t\ttempFileSuffix = \".temp\";\n\t\t} else {\n\t\t\ttempFileSuffix = StrUtil.addPrefixIfNot(tempFileSuffix, StrUtil.DOT);\n\t\t}\n\n\t\t// 目标文件真实名称\n\t\tfinal String fileName = outFile.isDirectory() ? FileUtil.getName(path) : outFile.getName();\n\t\t// 临时文件名称\n\t\tfinal String tempFileName = fileName + tempFileSuffix;\n\n\t\t// 临时文件\n\t\toutFile = new File(outFile.isDirectory() ? outFile : outFile.getParentFile(), tempFileName);\n\t\ttry {\n\t\t\tdownload(path, outFile);\n\t\t\t// 重命名下载好的临时文件\n\t\t\tFileUtil.rename(outFile, fileName, true);\n\t\t} catch (Throwable e) {\n\t\t\t// 异常则删除临时文件\n\t\t\tFileUtil.del(outFile);\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 递归下载FTP服务器上文件到本地(文件目录和服务器同步), 服务器上有新文件会覆盖本地文件\n\t *\n\t * @param sourcePath ftp服务器目录\n\t * @param destDir    本地目录\n\t * @since 5.3.5\n\t */\n\tpublic abstract void recursiveDownloadFolder(String sourcePath, File destDir);\n\n\t/**\n\t * 重命名文件/目录\n\t *\n\t * @param from 原路径\n\t * @param to   目标路径\n\t * @since 5.8.38\n\t */\n\tpublic abstract void rename(String from, String to);\n\n\t// ---------------------------------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 是否包含指定字符串，忽略大小写\n\t *\n\t * @param names      文件或目录名列表\n\t * @param nameToFind 要查找的文件或目录名\n\t * @return 是否包含\n\t */\n\tprivate static boolean containsIgnoreCase(List<String> names, String nameToFind) {\n\t\tif (CollUtil.isEmpty(names)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (StrUtil.isEmpty(nameToFind)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (String name : names) {\n\t\t\tif (nameToFind.equalsIgnoreCase(name)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t// ---------------------------------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.commons.net.ftp.FTPClient;\nimport org.apache.commons.net.ftp.FTPClientConfig;\nimport org.apache.commons.net.ftp.FTPFile;\nimport org.apache.commons.net.ftp.FTPReply;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * FTP客户端封装<br>\n * 此客户端基于Apache-Commons-Net\n * <p>\n * 常见搭建ftp的工具有\n * 1、filezila server ;根目录一般都是空\n * 2、linux vsftpd ; 使用的 系统用户的目录，这里往往都是不是根目录，如：/home/ftpuser/ftp\n *\n * @author looly, xhzou\n * @since 4.1.8\n */\npublic class Ftp extends AbstractFtp {\n\n\t/**\n\t * 默认端口\n\t */\n\tpublic static final int DEFAULT_PORT = 21;\n\n\tprivate FTPClient client;\n\tprivate FtpMode mode;\n\t/**\n\t * 执行完操作是否返回当前目录\n\t */\n\tprivate boolean backToPwd;\n\n\t/**\n\t * 构造，匿名登录\n\t *\n\t * @param host 域名或IP\n\t */\n\tpublic Ftp(String host) {\n\t\tthis(host, DEFAULT_PORT);\n\t}\n\n\t/**\n\t * 构造，匿名登录\n\t *\n\t * @param host 域名或IP\n\t * @param port 端口\n\t */\n\tpublic Ftp(String host, int port) {\n\t\tthis(host, port, \"anonymous\", \"\");\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host     域名或IP\n\t * @param port     端口\n\t * @param user     用户名\n\t * @param password 密码\n\t */\n\tpublic Ftp(String host, int port, String user, String password) {\n\t\tthis(host, port, user, password, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host     域名或IP\n\t * @param port     端口\n\t * @param user     用户名\n\t * @param password 密码\n\t * @param charset  编码\n\t */\n\tpublic Ftp(String host, int port, String user, String password, Charset charset) {\n\t\tthis(host, port, user, password, charset, null, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host               域名或IP\n\t * @param port               端口\n\t * @param user               用户名\n\t * @param password           密码\n\t * @param charset            编码\n\t * @param serverLanguageCode 服务器语言 例如：zh\n\t * @param systemKey          服务器标识 例如：org.apache.commons.net.ftp.FTPClientConfig.SYST_NT\n\t */\n\tpublic Ftp(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey) {\n\t\tthis(host, port, user, password, charset, serverLanguageCode, systemKey, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host               域名或IP\n\t * @param port               端口\n\t * @param user               用户名\n\t * @param password           密码\n\t * @param charset            编码\n\t * @param serverLanguageCode 服务器语言\n\t * @param systemKey          系统关键字\n\t * @param mode               模式\n\t */\n\tpublic Ftp(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey, FtpMode mode) {\n\t\tthis(new FtpConfig(host, port, user, password, charset, serverLanguageCode, systemKey), mode);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config FTP配置\n\t * @param mode   模式\n\t */\n\tpublic Ftp(FtpConfig config, FtpMode mode) {\n\t\tsuper(config);\n\t\tthis.mode = mode;\n\t\tthis.init();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param client 自定义实例化好的{@link FTPClient}\n\t * @since 5.7.22\n\t */\n\tpublic Ftp(FTPClient client) {\n\t\tsuper(FtpConfig.create());\n\t\tthis.client = client;\n\t}\n\n\t/**\n\t * 初始化连接\n\t *\n\t * @return this\n\t */\n\tpublic Ftp init() {\n\t\treturn this.init(this.ftpConfig, this.mode);\n\t}\n\n\t/**\n\t * 初始化连接\n\t *\n\t * @param host     域名或IP\n\t * @param port     端口\n\t * @param user     用户名\n\t * @param password 密码\n\t * @return this\n\t */\n\tpublic Ftp init(String host, int port, String user, String password) {\n\t\treturn this.init(host, port, user, password, null);\n\t}\n\n\t/**\n\t * 初始化连接\n\t *\n\t * @param host     域名或IP\n\t * @param port     端口\n\t * @param user     用户名\n\t * @param password 密码\n\t * @param mode     模式\n\t * @return this\n\t */\n\tpublic Ftp init(String host, int port, String user, String password, FtpMode mode) {\n\t\treturn init(new FtpConfig(host, port, user, password, this.ftpConfig.getCharset(), null, null), mode);\n\t}\n\n\t/**\n\t * 初始化连接\n\t *\n\t * @param config FTP配置\n\t * @param mode   模式\n\t * @return this\n\t */\n\tpublic Ftp init(FtpConfig config, FtpMode mode) {\n\t\tfinal FTPClient client = new FTPClient();\n\t\t// issue#I3O81Y@Gitee\n\t\tclient.setRemoteVerificationEnabled(false);\n\n\t\tfinal Charset charset = config.getCharset();\n\t\tif (null != charset) {\n\t\t\tclient.setControlEncoding(charset.toString());\n\t\t}\n\t\tclient.setConnectTimeout((int) config.getConnectionTimeout());\n\t\tfinal String systemKey = config.getSystemKey();\n\t\tif (StrUtil.isNotBlank(systemKey)) {\n\t\t\tfinal FTPClientConfig conf = new FTPClientConfig(systemKey);\n\t\t\tfinal String serverLanguageCode = config.getServerLanguageCode();\n\t\t\tif (StrUtil.isNotBlank(serverLanguageCode)) {\n\t\t\t\tconf.setServerLanguageCode(config.getServerLanguageCode());\n\t\t\t}\n\t\t\tclient.configure(conf);\n\t\t}\n\n\t\t// connect\n\t\ttry {\n\t\t\t// 连接ftp服务器\n\t\t\tclient.connect(config.getHost(), config.getPort());\n\t\t\tclient.setSoTimeout((int) config.getSoTimeout());\n\t\t\t// 登录ftp服务器\n\t\t\tclient.login(config.getUser(), config.getPassword());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tfinal int replyCode = client.getReplyCode(); // 是否成功登录服务器\n\t\tif (false == FTPReply.isPositiveCompletion(replyCode)) {\n\t\t\ttry {\n\t\t\t\tclient.disconnect();\n\t\t\t} catch (IOException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t\tthrow new FtpException(\"Login failed for user [{}], reply code is: [{}]\", config.getUser(), replyCode);\n\t\t}\n\t\tthis.client = client;\n\t\tif (mode != null) {\n\t\t\tsetMode(mode);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置FTP连接模式，可选主动和被动模式\n\t *\n\t * @param mode 模式枚举\n\t * @return this\n\t * @since 4.1.19\n\t */\n\tpublic Ftp setMode(FtpMode mode) {\n\t\tthis.mode = mode;\n\t\tswitch (mode) {\n\t\t\tcase Active:\n\t\t\t\tthis.client.enterLocalActiveMode();\n\t\t\t\tbreak;\n\t\t\tcase Passive:\n\t\t\t\tthis.client.enterLocalPassiveMode();\n\t\t\t\tbreak;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置执行完操作是否返回当前目录\n\t *\n\t * @param backToPwd 执行完操作是否返回当前目录\n\t * @return this\n\t * @since 4.6.0\n\t */\n\tpublic Ftp setBackToPwd(boolean backToPwd) {\n\t\tthis.backToPwd = backToPwd;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否执行完操作返回当前目录\n\t * @return 执行完操作是否返回当前目录\n\t * @since 5.7.17\n\t */\n\tpublic boolean isBackToPwd(){\n\t\treturn this.backToPwd;\n\t}\n\n\t/**\n\t * 如果连接超时的话，重新进行连接 经测试，当连接超时时，client.isConnected()仍然返回ture，无法判断是否连接超时 因此，通过发送pwd命令的方式，检查连接是否超时\n\t *\n\t * @return this\n\t */\n\t@Override\n\tpublic Ftp reconnectIfTimeout() {\n\t\tString pwd = null;\n\t\ttry {\n\t\t\tpwd = pwd();\n\t\t} catch (IORuntimeException fex) {\n\t\t\t// ignore\n\t\t}\n\n\t\tif (pwd == null) {\n\t\t\treturn this.init();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 改变目录\n\t *\n\t * @param directory 目录\n\t * @return 是否成功\n\t */\n\t@Override\n\tsynchronized public boolean cd(String directory) {\n\t\tif (StrUtil.isBlank(directory)) {\n\t\t\t// 当前目录\n\t\t\treturn true;\n\t\t}\n\n\t\ttry {\n\t\t\treturn client.changeWorkingDirectory(directory);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 远程当前目录\n\t *\n\t * @return 远程当前目录\n\t * @since 4.1.14\n\t */\n\t@Override\n\tpublic String pwd() {\n\t\ttry {\n\t\t\treturn client.printWorkingDirectory();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic List<String> ls(String path) {\n\t\treturn ArrayUtil.map(lsFiles(path), FTPFile::getName);\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件和目录，不会递归遍历<br>\n\t * 此方法自动过滤\".\"和\"..\"两种目录\n\t *\n\t * @param path   目录\n\t * @param filter 过滤器，null表示不过滤，默认去掉\".\"和\"..\"两种目录\n\t * @return 文件或目录列表\n\t * @since 5.3.5\n\t */\n\tpublic List<FTPFile> lsFiles(String path, Filter<FTPFile> filter) {\n\t\tfinal FTPFile[] ftpFiles = lsFiles(path);\n\t\tif (ArrayUtil.isEmpty(ftpFiles)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal List<FTPFile> result = new ArrayList<>(ftpFiles.length - 2 <= 0 ? ftpFiles.length : ftpFiles.length - 2);\n\t\tString fileName;\n\t\tfor (FTPFile ftpFile : ftpFiles) {\n\t\t\tfileName = ftpFile.getName();\n\t\t\tif (false == StrUtil.equals(\".\", fileName) && false == StrUtil.equals(\"..\", fileName)) {\n\t\t\t\tif (null == filter || filter.accept(ftpFile)) {\n\t\t\t\t\tresult.add(ftpFile);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件和目录，不会递归遍历\n\t *\n\t * @param path 目录，如果目录不存在，抛出异常\n\t * @return 文件或目录列表\n\t * @throws FtpException       路径不存在\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic FTPFile[] lsFiles(String path) throws FtpException, IORuntimeException {\n\t\tString pwd = null;\n\t\tif (StrUtil.isNotBlank(path)) {\n\t\t\tpwd = pwd();\n\t\t\tif (false == cd(path)) {\n\t\t\t\tthrow new FtpException(\"Change dir to [{}] error, maybe path not exist!\", path);\n\t\t\t}\n\t\t}\n\n\t\tFTPFile[] ftpFiles;\n\t\ttry {\n\t\t\tftpFiles = this.client.listFiles();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\t// 回到原目录\n\t\t\tcd(pwd);\n\t\t}\n\n\t\treturn ftpFiles;\n\t}\n\n\t@Override\n\tpublic boolean mkdir(String dir) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn this.client.makeDirectory(dir);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取服务端目录状态。\n\t *\n\t * @param path 路径\n\t * @return 状态int，服务端不同，返回不同\n\t * @since 5.4.3\n\t */\n\tpublic int stat(String path) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn this.client.stat(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 判断ftp服务器目录内是否还有子元素（目录或文件）\n\t *\n\t * @param path 文件路径\n\t * @return 是否存在\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean existFile(String path) throws IORuntimeException {\n\t\tFTPFile[] ftpFileArr;\n\t\ttry {\n\t\t\tftpFileArr = client.listFiles(path);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn ArrayUtil.isNotEmpty(ftpFileArr);\n\t}\n\n\t@Override\n\tpublic boolean delFile(String path) throws IORuntimeException {\n\t\tfinal String pwd = pwd();\n\t\tfinal String fileName = FileUtil.getName(path);\n\t\tfinal String dir = StrUtil.removeSuffix(path, fileName);\n\t\tif (false == cd(dir)) {\n\t\t\tthrow new FtpException(\"Change dir to [{}] error, maybe dir not exist!\", path);\n\t\t}\n\n\t\tboolean isSuccess;\n\t\ttry {\n\t\t\tisSuccess = client.deleteFile(fileName);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\t// 回到原目录\n\t\t\tcd(pwd);\n\t\t}\n\t\treturn isSuccess;\n\t}\n\n\t@Override\n\tpublic boolean delDir(String dirPath) throws IORuntimeException {\n\t\tFTPFile[] dirs;\n\t\ttry {\n\t\t\tdirs = client.listFiles(dirPath);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tString name;\n\t\tString childPath;\n\t\tfor (FTPFile ftpFile : dirs) {\n\t\t\tname = ftpFile.getName();\n\t\t\tchildPath = StrUtil.format(\"{}/{}\", dirPath, name);\n\t\t\tif (ftpFile.isDirectory()) {\n\t\t\t\t// 上级和本级目录除外\n\t\t\t\tif (false == \".\".equals(name) && false == \"..\".equals(name)) {\n\t\t\t\t\tdelDir(childPath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdelFile(childPath);\n\t\t\t}\n\t\t}\n\n\t\t// 删除空目录\n\t\ttry {\n\t\t\treturn this.client.removeDirectory(dirPath);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 上传文件到指定目录，可选：\n\t *\n\t * <pre>\n\t * 1. destPath为null或\"\"上传到当前路径\n\t * 2. destPath为相对路径则相对于当前路径的子路径\n\t * 3. destPath为绝对路径则上传到此路径\n\t * </pre>\n\t *\n\t * @param destPath 服务端路径，可以为{@code null} 或者相对路径或绝对路径\n\t * @param file     文件\n\t * @return 是否上传成功\n\t */\n\t@Override\n\tpublic boolean upload(String destPath, File file) {\n\t\tAssert.notNull(file, \"file to upload is null !\");\n\t\treturn upload(destPath, file.getName(), file);\n\t}\n\n\t/**\n\t * 上传文件到指定目录，可选：\n\t *\n\t * <pre>\n\t * 1. destPath为null或\"\"上传到当前路径\n\t * 2. destPath为相对路径则相对于当前路径的子路径\n\t * 3. destPath为绝对路径则上传到此路径\n\t * </pre>\n\t *\n\t * @param file     文件\n\t * @param destPath     服务端路径，可以为{@code null} 或者相对路径或绝对路径\n\t * @param fileName 自定义在服务端保存的文件名\n\t * @return 是否上传成功\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean upload(String destPath, String fileName, File file) throws IORuntimeException {\n\t\ttry (final InputStream in = FileUtil.getInputStream(file)) {\n\t\t\treturn upload(destPath, fileName, in);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 上传文件到指定目录，可选：\n\t *\n\t * <pre>\n\t * 1. destPath为null或\"\"上传到当前路径\n\t * 2. destPath为相对路径则相对于当前路径的子路径\n\t * 3. destPath为绝对路径则上传到此路径\n\t * </pre>\n\t *\n\t * @param destPath       服务端路径，可以为{@code null} 或者相对路径或绝对路径\n\t * @param fileName   文件名\n\t * @param fileStream 文件流\n\t * @return 是否上传成功\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic boolean upload(String destPath, String fileName, InputStream fileStream) throws IORuntimeException {\n\t\ttry {\n\t\t\tclient.setFileType(FTPClient.BINARY_FILE_TYPE);\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tString pwd = null;\n\t\tif (this.backToPwd) {\n\t\t\tpwd = pwd();\n\t\t}\n\n\t\tif (StrUtil.isNotBlank(destPath)) {\n\t\t\tmkDirs(destPath);\n\t\t\tif (false == cd(destPath)) {\n\t\t\t\tthrow new FtpException(\"Change dir to [{}] error, maybe dir not exist!\", destPath);\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\treturn client.storeFile(fileName, fileStream);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (this.backToPwd) {\n\t\t\t\tcd(pwd);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 递归上传文件（支持目录）<br>\n\t * 上传时，如果uploadFile为目录，只复制目录下所有目录和文件到目标路径下，并不会复制目录本身<br>\n\t * 上传时，自动创建父级目录\n\t *\n\t * @param remotePath 目录路径\n\t * @param uploadFile 上传文件或目录\n\t */\n\tpublic void uploadFileOrDirectory(final String remotePath, final File uploadFile) {\n\t\tif (false == FileUtil.isDirectory(uploadFile)) {\n\t\t\t// 上传文件\n\t\t\tthis.upload(remotePath, uploadFile);\n\t\t\treturn;\n\t\t}\n\n\t\tfinal File[] files = uploadFile.listFiles();\n\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal List<File> dirs = new ArrayList<>(files.length);\n\t\t//第一次只处理文件，防止目录在前面导致先处理子目录，而引发文件所在目录不正确\n\t\tfor (final File f : files) {\n\t\t\tif (f.isDirectory()) {\n\t\t\t\tdirs.add(f);\n\t\t\t} else {\n\t\t\t\tthis.upload(remotePath, f);\n\t\t\t}\n\t\t}\n\t\t//第二次只处理目录\n\t\tfor (final File f : dirs) {\n\t\t\tfinal String dir = FileUtil.normalize(remotePath + \"/\" + f.getName());\n\t\t\tthis.uploadFileOrDirectory(dir, f);\n\t\t}\n\t}\n\n\t/**\n\t * 下载文件\n\t *\n\t * @param path    文件路径，包含文件名\n\t * @param outFile 输出文件或目录，当为目录时，使用服务端的文件名\n\t */\n\t@Override\n\tpublic void download(String path, File outFile) {\n\t\tfinal String fileName = FileUtil.getName(path);\n\t\tfinal String dir = StrUtil.removeSuffix(path, fileName);\n\t\tdownload(dir, fileName, outFile);\n\t}\n\n\t/**\n\t * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)\n\t *\n\t * @param sourcePath ftp服务器目录\n\t * @param destDir    本地目录\n\t */\n\t@Override\n\tpublic void recursiveDownloadFolder(String sourcePath, File destDir) {\n\t\tString fileName;\n\t\tString srcFile;\n\t\tFile destFile;\n\t\tfor (FTPFile ftpFile : lsFiles(sourcePath, null)) {\n\t\t\tfileName = ftpFile.getName();\n\t\t\tsrcFile = StrUtil.format(\"{}/{}\", sourcePath, fileName);\n\t\t\tdestFile = FileUtil.file(destDir, fileName);\n\n\t\t\tif (false == ftpFile.isDirectory()) {\n\t\t\t\t// 本地不存在文件或者ftp上文件有修改则下载\n\t\t\t\tif (false == FileUtil.exist(destFile)\n\t\t\t\t\t\t|| (ftpFile.getTimestamp().getTimeInMillis() > destFile.lastModified())) {\n\t\t\t\t\tdownload(srcFile, destFile);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 服务端依旧是目录，继续递归\n\t\t\t\tFileUtil.mkdir(destFile);\n\t\t\t\trecursiveDownloadFolder(srcFile, destFile);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 下载文件\n\t *\n\t * @param path     文件所在路径（远程目录），不包含文件名\n\t * @param fileName 文件名\n\t * @param outFile  输出文件或目录，当为目录时使用服务端文件名\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void download(String path, String fileName, File outFile) throws IORuntimeException {\n\t\tif (outFile.isDirectory()) {\n\t\t\toutFile = new File(outFile, fileName);\n\t\t}\n\t\tif (false == outFile.exists()) {\n\t\t\tFileUtil.touch(outFile);\n\t\t}\n\t\ttry (OutputStream out = FileUtil.getOutputStream(outFile)) {\n\t\t\tdownload(path, fileName, out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 下载文件到输出流\n\t *\n\t * @param path     文件路径\n\t * @param fileName 文件名\n\t * @param out      输出位置\n\t */\n\tpublic void download(String path, String fileName, OutputStream out) {\n\t\tdownload(path, fileName, out, null);\n\t}\n\n\t/**\n\t * 下载文件到输出流\n\t *\n\t * @param path            服务端的文件路径\n\t * @param fileName        服务端的文件名\n\t * @param out             输出流，下载的文件写出到这个流中\n\t * @param fileNameCharset 文件名编码，通过此编码转换文件名编码为ISO8859-1\n\t * @throws IORuntimeException IO异常\n\t * @since 5.5.7\n\t */\n\tpublic void download(String path, String fileName, OutputStream out, Charset fileNameCharset) throws IORuntimeException {\n\t\tString pwd = null;\n\t\tif (this.backToPwd) {\n\t\t\tpwd = pwd();\n\t\t}\n\n\t\tif (false == cd(path)) {\n\t\t\tthrow new FtpException(\"Change dir to [{}] error, maybe dir not exist!\", path);\n\t\t}\n\n\t\tif (null != fileNameCharset) {\n\t\t\tfileName = new String(fileName.getBytes(fileNameCharset), StandardCharsets.ISO_8859_1);\n\t\t}\n\n\t\tboolean isSuccess;\n\t\ttry {\n\t\t\tclient.setFileType(FTPClient.BINARY_FILE_TYPE);\n\t\t\tisSuccess = client.retrieveFile(fileName, out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (backToPwd) {\n\t\t\t\tcd(pwd);\n\t\t\t}\n\t\t}\n\n\t\tif(false == isSuccess){\n\t\t\tthrow new FtpException(\"retrieveFile return false\");\n\t\t}\n\t}\n\n\t@Override\n\tpublic void rename(String from, String to) {\n\t\ttry {\n\t\t\tif (!client.rename(from, to)) {\n\t\t\t\tthrow new FtpException(\"rename [{}] to [{}] fail\", from, to);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取FTPClient客户端对象\n\t *\n\t * @return {@link FTPClient}\n\t */\n\tpublic FTPClient getClient() {\n\t\treturn this.client;\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tif (null != this.client) {\n\t\t\tthis.client.logout();\n\t\t\tif (this.client.isConnected()) {\n\t\t\t\tthis.client.disconnect();\n\t\t\t}\n\t\t\tthis.client = null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpConfig.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\n\n/**\n * FTP配置项，提供FTP各种参数信息\n *\n * @author looly\n */\npublic class FtpConfig implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static FtpConfig create() {\n\t\treturn new FtpConfig();\n\t}\n\n\t/**\n\t * 主机\n\t */\n\tprivate String host;\n\t/**\n\t * 端口\n\t */\n\tprivate int port;\n\t/**\n\t * 用户名\n\t */\n\tprivate String user;\n\t/**\n\t * 密码\n\t */\n\tprivate String password;\n\t/**\n\t * 编码\n\t */\n\tprivate Charset charset;\n\n\t/**\n\t * 连接超时时长，单位毫秒\n\t */\n\tprivate long connectionTimeout;\n\n\t/**\n\t * Socket连接超时时长，单位毫秒\n\t */\n\tprivate long soTimeout;\n\n\t/**\n\t * 设置服务器语言\n\t */\n\tprivate String serverLanguageCode;\n\n\t/**\n\t * 设置服务器系统关键词\n\t */\n\tprivate String systemKey;\n\n\t/**\n\t * 构造\n\t */\n\tpublic FtpConfig() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host               主机\n\t * @param port               端口\n\t * @param user               用户名\n\t * @param password           密码\n\t * @param charset            编码\n\t */\n\tpublic FtpConfig(String host, int port, String user, String password, Charset charset) {\n\t\tthis(host, port, user, password, charset, null, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param host               主机\n\t * @param port               端口\n\t * @param user               用户名\n\t * @param password           密码\n\t * @param charset            编码\n\t * @param serverLanguageCode 服务器语言\n\t * @param systemKey          系统关键字\n\t * @since 5.5.7\n\t */\n\tpublic FtpConfig(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey) {\n\t\tthis.host = host;\n\t\tthis.port = port;\n\t\tthis.user = user;\n\t\tthis.password = password;\n\t\tthis.charset = charset;\n\t\tthis.serverLanguageCode = serverLanguageCode;\n\t\tthis.systemKey = systemKey;\n\t}\n\n\tpublic String getHost() {\n\t\treturn host;\n\t}\n\n\tpublic FtpConfig setHost(String host) {\n\t\tthis.host = host;\n\t\treturn this;\n\t}\n\n\tpublic int getPort() {\n\t\treturn port;\n\t}\n\n\tpublic FtpConfig setPort(int port) {\n\t\tthis.port = port;\n\t\treturn this;\n\t}\n\n\tpublic String getUser() {\n\t\treturn user;\n\t}\n\n\tpublic FtpConfig setUser(String user) {\n\t\tthis.user = user;\n\t\treturn this;\n\t}\n\n\tpublic String getPassword() {\n\t\treturn password;\n\t}\n\n\tpublic FtpConfig setPassword(String password) {\n\t\tthis.password = password;\n\t\treturn this;\n\t}\n\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\tpublic FtpConfig setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\tpublic long getConnectionTimeout() {\n\t\treturn connectionTimeout;\n\t}\n\n\tpublic FtpConfig setConnectionTimeout(long connectionTimeout) {\n\t\tthis.connectionTimeout = connectionTimeout;\n\t\treturn this;\n\t}\n\n\tpublic long getSoTimeout() {\n\t\treturn soTimeout;\n\t}\n\n\tpublic FtpConfig setSoTimeout(long soTimeout) {\n\t\tthis.soTimeout = soTimeout;\n\t\treturn this;\n\t}\n\n\tpublic String getServerLanguageCode() {\n\t\treturn serverLanguageCode;\n\t}\n\n\tpublic FtpConfig setServerLanguageCode(String serverLanguageCode) {\n\t\tthis.serverLanguageCode = serverLanguageCode;\n\t\treturn this;\n\t}\n\n\tpublic String getSystemKey() {\n\t\treturn systemKey;\n\t}\n\n\tpublic FtpConfig setSystemKey(String systemKey) {\n\t\tthis.systemKey = systemKey;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpException.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Ftp异常\n *\n * @author xiaoleilu\n */\npublic class FtpException extends RuntimeException {\n\tprivate static final long serialVersionUID = -8490149159895201756L;\n\n\tpublic FtpException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic FtpException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic FtpException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic FtpException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic FtpException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic FtpException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpMode.java",
    "content": "package cn.hutool.extra.ftp;\n\n/**\n * FTP连接模式\n * \n * <p>\n * 见：https://www.cnblogs.com/huhaoshida/p/5412615.html\n * \n * @author looly\n * @since 4.1.19\n */\npublic enum FtpMode {\n\t/** 主动模式 */\n\tActive,\n\t/** 被动模式 */\n\tPassive\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/SimpleFtpServer.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.NetUtil;\nimport org.apache.ftpserver.ConnectionConfig;\nimport org.apache.ftpserver.FtpServerFactory;\nimport org.apache.ftpserver.ftplet.Authority;\nimport org.apache.ftpserver.ftplet.Ftplet;\nimport org.apache.ftpserver.ftplet.User;\nimport org.apache.ftpserver.ftplet.UserManager;\nimport org.apache.ftpserver.listener.ListenerFactory;\nimport org.apache.ftpserver.ssl.SslConfiguration;\nimport org.apache.ftpserver.ssl.SslConfigurationFactory;\nimport org.apache.ftpserver.usermanager.PropertiesUserManagerFactory;\nimport org.apache.ftpserver.usermanager.impl.BaseUser;\nimport org.apache.ftpserver.usermanager.impl.WritePermission;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 基于 Apache FtpServer（http://mina.apache.org/ftpserver-project/）的FTP服务端简单封装。\n *\n * @author looly\n * @since 5.5.0\n */\npublic class SimpleFtpServer {\n\n\t/**\n\t * 创建FTP服务器，调用{@link SimpleFtpServer#start()}启动即可\n\t *\n\t * @return SimpleFtpServer\n\t */\n\tpublic static SimpleFtpServer create() {\n\t\treturn new SimpleFtpServer();\n\t}\n\n\tFtpServerFactory serverFactory;\n\tListenerFactory listenerFactory;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SimpleFtpServer() {\n\t\tserverFactory = new FtpServerFactory();\n\t\tlistenerFactory = new ListenerFactory();\n\t}\n\n\t/**\n\t * 获取 {@link FtpServerFactory}，用于设置FTP服务器相关信息\n\t *\n\t * @return {@link FtpServerFactory}\n\t */\n\tpublic FtpServerFactory getServerFactory() {\n\t\treturn this.serverFactory;\n\t}\n\n\t/**\n\t * 设置连接相关配置，使用ConnectionConfigFactory创建{@link ConnectionConfig}对象\n\t *\n\t * @param connectionConfig 连接配置\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setConnectionConfig(ConnectionConfig connectionConfig) {\n\t\tthis.serverFactory.setConnectionConfig(connectionConfig);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取{@link ListenerFactory}，用于设置端口、用户、SSL等信息\n\t *\n\t * @return {@link ListenerFactory}\n\t */\n\tpublic ListenerFactory getListenerFactory() {\n\t\treturn this.listenerFactory;\n\t}\n\n\t/**\n\t * 自定义默认端口，如果不设置，使用默认端口：21\n\t *\n\t * @param port 端口\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setPort(int port) {\n\t\tAssert.isTrue(NetUtil.isValidPort(port), \"Invalid port!\");\n\t\tthis.listenerFactory.setPort(port);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取用户管理器，用于新增、查找和删除用户信息\n\t *\n\t * @return 用户管理器\n\t */\n\tpublic UserManager getUserManager() {\n\t\treturn this.serverFactory.getUserManager();\n\t}\n\n\t/**\n\t * 增加FTP用户\n\t *\n\t * @param user FTP用户信息\n\t * @return this\n\t */\n\tpublic SimpleFtpServer addUser(User user) {\n\t\ttry {\n\t\t\tgetUserManager().save(user);\n\t\t} catch (org.apache.ftpserver.ftplet.FtpException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加匿名用户\n\t *\n\t * @param homePath 用户路径，匿名用户对此路径有读写权限\n\t * @return this\n\t */\n\tpublic SimpleFtpServer addAnonymous(String homePath) {\n\t\tfinal BaseUser user = new BaseUser();\n\t\tuser.setName(\"anonymous\");\n\t\tuser.setHomeDirectory(homePath);\n\t\tfinal List<Authority> authorities = new ArrayList<>();\n\t\t// 添加用户读写权限\n\t\tauthorities.add(new WritePermission());\n\t\tuser.setAuthorities(authorities);\n\t\treturn addUser(user);\n\t}\n\n\t/**\n\t * 删除用户\n\t *\n\t * @param userName 用户名\n\t * @return this\n\t */\n\tpublic SimpleFtpServer delUser(String userName) {\n\t\ttry {\n\t\t\tgetUserManager().delete(userName);\n\t\t} catch (org.apache.ftpserver.ftplet.FtpException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 使用SSL安全连接，可以使用SslConfigurationFactory创建{@link SslConfiguration}\n\t *\n\t * @param ssl {@link SslConfiguration}\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setSsl(SslConfiguration ssl) {\n\t\tthis.listenerFactory.setSslConfiguration(ssl);\n\t\tlistenerFactory.setImplicitSsl(true);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 使用SSL安全连接\n\t *\n\t * @param keystoreFile 密钥文件\n\t * @param password     密钥文件密码\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setSsl(File keystoreFile, String password) {\n\t\tfinal SslConfigurationFactory sslFactory = new SslConfigurationFactory();\n\t\tsslFactory.setKeystoreFile(keystoreFile);\n\t\tsslFactory.setKeystorePassword(password);\n\t\treturn setSsl(sslFactory.createSslConfiguration());\n\t}\n\n\t/**\n\t * 自定义用户管理器，一般用于使用配置文件配置用户信息\n\t *\n\t * @param userManager {@link UserManager}\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setUserManager(UserManager userManager) {\n\t\tthis.serverFactory.setUserManager(userManager);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 自定义用户信息配置文件，此方法会重置用户管理器\n\t *\n\t * @param propertiesFile 配置文件\n\t * @return this\n\t */\n\tpublic SimpleFtpServer setUsersConfig(File propertiesFile) {\n\t\tfinal PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();\n\t\tuserManagerFactory.setFile(propertiesFile);\n\t\treturn this.setUserManager(userManagerFactory.createUserManager());\n\t}\n\n\t/**\n\t * 增加FTP动作行为监听处理器，通过实现{@link Ftplet}，可以对用户的行为监听并执行相应动作\n\t *\n\t * @param name   名称\n\t * @param ftplet {@link Ftplet}，用户自定义监听规则\n\t * @return this\n\t */\n\tpublic SimpleFtpServer addFtplet(String name, Ftplet ftplet) {\n\t\tthis.serverFactory.getFtplets().put(name, ftplet);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 启动FTP服务，阻塞当前线程\n\t */\n\tpublic void start() {\n\t\t// 一个Listener对应一个监听端口\n\t\t// 可以创建多个监听，此处默认只监听一个\n\t\tserverFactory.addListener(\"default\", listenerFactory.createListener());\n\t\ttry {\n\t\t\tserverFactory.createServer().start();\n\t\t} catch (org.apache.ftpserver.ftplet.FtpException e) {\n\t\t\tthrow new cn.hutool.extra.ftp.FtpException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ftp/package-info.java",
    "content": "/**\n * 基于Apache Commons Net封装的FTP工具\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.ftp;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/GlobalMailAccount.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.io.IORuntimeException;\n\n/**\n * 全局邮件帐户，依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATHS}\n *\n * @author looly\n *\n */\npublic enum GlobalMailAccount {\n\tINSTANCE;\n\n\tprivate final MailAccount mailAccount;\n\n\t/**\n\t * 构造\n\t */\n\tGlobalMailAccount() {\n\t\tmailAccount = createDefaultAccount();\n\t}\n\n\t/**\n\t * 获得邮件帐户\n\t *\n\t * @return 邮件帐户\n\t */\n\tpublic MailAccount getAccount() {\n\t\treturn this.mailAccount;\n\t}\n\n\t/**\n\t * 创建默认帐户\n\t *\n\t * @return MailAccount\n\t */\n\tprivate MailAccount createDefaultAccount() {\n\t\tfor (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {\n\t\t\ttry {\n\t\t\t\treturn new MailAccount(mailSettingPath);\n\t\t\t} catch (IORuntimeException ignore) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport javax.mail.internet.AddressException;\nimport javax.mail.internet.InternetAddress;\nimport javax.mail.internet.MimeUtility;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 邮件内部工具类\n * @author looly\n * @since 3.2.3\n */\npublic class InternalMailUtil {\n\n\t/**\n\t * 将多个字符串邮件地址转为{@link InternetAddress}列表<br>\n\t * 单个字符串地址可以是多个地址合并的字符串\n\t *\n\t * @param addrStrs 地址数组\n\t * @param charset 编码（主要用于中文用户名的编码）\n\t * @return 地址数组\n\t * @since 4.0.3\n\t */\n\tpublic static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {\n\t\tfinal List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);\n\t\tInternetAddress[] addrs;\n\t\tfor (String addrStr : addrStrs) {\n\t\t\taddrs = parseAddress(addrStr, charset);\n\t\t\tif (ArrayUtil.isNotEmpty(addrs)) {\n\t\t\t\tCollections.addAll(resultList, addrs);\n\t\t\t}\n\t\t}\n\t\treturn resultList.toArray(new InternetAddress[0]);\n\t}\n\n\t/**\n\t * 解析第一个地址\n\t *\n\t * @param address 地址字符串\n\t * @param charset 编码，{@code null}表示使用系统属性定义的编码或系统编码\n\t * @return 地址列表\n\t */\n\tpublic static InternetAddress parseFirstAddress(String address, Charset charset) {\n\t\tfinal InternetAddress[] internetAddresses = parseAddress(address, charset);\n\t\tif (ArrayUtil.isEmpty(internetAddresses)) {\n\t\t\ttry {\n\t\t\t\treturn new InternetAddress(address);\n\t\t\t} catch (AddressException e) {\n\t\t\t\tthrow new MailException(e);\n\t\t\t}\n\t\t}\n\t\treturn internetAddresses[0];\n\t}\n\n\t/**\n\t * 将一个地址字符串解析为多个地址<br>\n\t * 地址间使用\" \"、\",\"、\";\"分隔\n\t *\n\t * @param address 地址字符串\n\t * @param charset 编码，{@code null}表示使用系统属性定义的编码或系统编码\n\t * @return 地址列表\n\t */\n\tpublic static InternetAddress[] parseAddress(String address, Charset charset) {\n\t\tInternetAddress[] addresses;\n\t\ttry {\n\t\t\taddresses = InternetAddress.parse(address);\n\t\t} catch (AddressException e) {\n\t\t\tthrow new MailException(e);\n\t\t}\n\t\t//编码用户名\n\t\tif (ArrayUtil.isNotEmpty(addresses)) {\n\t\t\tfinal String charsetStr = null == charset ? null : charset.name();\n\t\t\tfor (InternetAddress internetAddress : addresses) {\n\t\t\t\ttry {\n\t\t\t\t\tinternetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);\n\t\t\t\t} catch (UnsupportedEncodingException e) {\n\t\t\t\t\tthrow new MailException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn addresses;\n\t}\n\n\t/**\n\t * 编码中文字符<br>\n\t * 编码失败返回原字符串\n\t *\n\t * @param text 被编码的文本\n\t * @param charset 编码\n\t * @return 编码后的结果\n\t */\n\tpublic static String encodeText(String text, Charset charset) {\n\t\ttry {\n\t\t\treturn MimeUtility.encodeText(text, charset.name(), null);\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn text;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaInternalMailUtil.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.util.ArrayUtil;\n\nimport jakarta.mail.internet.AddressException;\nimport jakarta.mail.internet.InternetAddress;\nimport jakarta.mail.internet.MimeUtility;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 邮件内部工具类\n * @author looly\n * @since 3.2.3\n */\npublic class JakartaInternalMailUtil {\n\n\t/**\n\t * 将多个字符串邮件地址转为{@link InternetAddress}列表<br>\n\t * 单个字符串地址可以是多个地址合并的字符串\n\t *\n\t * @param addrStrs 地址数组\n\t * @param charset 编码（主要用于中文用户名的编码）\n\t * @return 地址数组\n\t * @since 4.0.3\n\t */\n\tpublic static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {\n\t\tfinal List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);\n\t\tInternetAddress[] addrs;\n\t\tfor (String addrStr : addrStrs) {\n\t\t\taddrs = parseAddress(addrStr, charset);\n\t\t\tif (ArrayUtil.isNotEmpty(addrs)) {\n\t\t\t\tCollections.addAll(resultList, addrs);\n\t\t\t}\n\t\t}\n\t\treturn resultList.toArray(new InternetAddress[0]);\n\t}\n\n\t/**\n\t * 解析第一个地址\n\t *\n\t * @param address 地址字符串\n\t * @param charset 编码，{@code null}表示使用系统属性定义的编码或系统编码\n\t * @return 地址列表\n\t */\n\tpublic static InternetAddress parseFirstAddress(String address, Charset charset) {\n\t\tfinal InternetAddress[] internetAddresses = parseAddress(address, charset);\n\t\tif (ArrayUtil.isEmpty(internetAddresses)) {\n\t\t\ttry {\n\t\t\t\treturn new InternetAddress(address);\n\t\t\t} catch (AddressException e) {\n\t\t\t\tthrow new MailException(e);\n\t\t\t}\n\t\t}\n\t\treturn internetAddresses[0];\n\t}\n\n\t/**\n\t * 将一个地址字符串解析为多个地址<br>\n\t * 地址间使用\" \"、\",\"、\";\"分隔\n\t *\n\t * @param address 地址字符串\n\t * @param charset 编码，{@code null}表示使用系统属性定义的编码或系统编码\n\t * @return 地址列表\n\t */\n\tpublic static InternetAddress[] parseAddress(String address, Charset charset) {\n\t\tInternetAddress[] addresses;\n\t\ttry {\n\t\t\taddresses = InternetAddress.parse(address);\n\t\t} catch (AddressException e) {\n\t\t\tthrow new MailException(e);\n\t\t}\n\t\t//编码用户名\n\t\tif (ArrayUtil.isNotEmpty(addresses)) {\n\t\t\tfinal String charsetStr = null == charset ? null : charset.name();\n\t\t\tfor (InternetAddress internetAddress : addresses) {\n\t\t\t\ttry {\n\t\t\t\t\tinternetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);\n\t\t\t\t} catch (UnsupportedEncodingException e) {\n\t\t\t\t\tthrow new MailException(e);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn addresses;\n\t}\n\n\t/**\n\t * 编码中文字符<br>\n\t * 编码失败返回原字符串\n\t *\n\t * @param text 被编码的文本\n\t * @param charset 编码\n\t * @return 编码后的结果\n\t */\n\tpublic static String encodeText(String text, Charset charset) {\n\t\ttry {\n\t\t\treturn MimeUtility.encodeText(text, charset.name(), null);\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\t// ignore\n\t\t}\n\t\treturn text;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMail.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport jakarta.activation.DataHandler;\nimport jakarta.activation.DataSource;\nimport jakarta.activation.FileDataSource;\nimport jakarta.activation.FileTypeMap;\nimport jakarta.mail.*;\nimport jakarta.mail.internet.MimeBodyPart;\nimport jakarta.mail.internet.MimeMessage;\nimport jakarta.mail.internet.MimeMultipart;\nimport jakarta.mail.internet.MimeUtility;\nimport jakarta.mail.util.ByteArrayDataSource;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PrintStream;\nimport java.nio.charset.Charset;\nimport java.util.Date;\n\n/**\n * 邮件发送客户端\n *\n * @author looly\n * @since 3.2.0\n */\npublic class JakartaMail implements Builder<MimeMessage> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 邮箱帐户信息以及一些客户端配置信息\n\t */\n\tprivate final MailAccount mailAccount;\n\t/**\n\t * 收件人列表\n\t */\n\tprivate String[] tos;\n\t/**\n\t * 抄送人列表（carbon copy）\n\t */\n\tprivate String[] ccs;\n\t/**\n\t * 密送人列表（blind carbon copy）\n\t */\n\tprivate String[] bccs;\n\t/**\n\t * 回复地址(reply-to)\n\t */\n\tprivate String[] reply;\n\t/**\n\t * 标题\n\t */\n\tprivate String title;\n\t/**\n\t * 内容\n\t */\n\tprivate String content;\n\t/**\n\t * 是否为HTML\n\t */\n\tprivate boolean isHtml;\n\t/**\n\t * 正文、附件和图片的混合部分\n\t */\n\tprivate final Multipart multipart = new MimeMultipart();\n\t/**\n\t * 是否使用全局会话，默认为false\n\t */\n\tprivate boolean useGlobalSession = false;\n\n\t/**\n\t * debug输出位置，可以自定义debug日志\n\t */\n\tprivate PrintStream debugOutput;\n\n\t/**\n\t * 创建邮件客户端\n\t *\n\t * @param mailAccount 邮件帐号\n\t * @return Mail\n\t */\n\tpublic static JakartaMail create(MailAccount mailAccount) {\n\t\treturn new JakartaMail(mailAccount);\n\t}\n\n\t/**\n\t * 创建邮件客户端，使用全局邮件帐户\n\t *\n\t * @return Mail\n\t */\n\tpublic static JakartaMail create() {\n\t\treturn new JakartaMail();\n\t}\n\n\t// --------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，使用全局邮件帐户\n\t */\n\tpublic JakartaMail() {\n\t\tthis(GlobalMailAccount.INSTANCE.getAccount());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mailAccount 邮件帐户，如果为null使用默认配置文件的全局邮件配置\n\t */\n\tpublic JakartaMail(MailAccount mailAccount) {\n\t\tmailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();\n\t\tthis.mailAccount = mailAccount.defaultIfEmpty();\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 设置收件人\n\t *\n\t * @param tos 收件人列表\n\t * @return this\n\t * @see #setTos(String...)\n\t */\n\tpublic JakartaMail to(String... tos) {\n\t\treturn setTos(tos);\n\t}\n\n\t/**\n\t * 设置多个收件人\n\t *\n\t * @param tos 收件人列表\n\t * @return this\n\t */\n\tpublic JakartaMail setTos(String... tos) {\n\t\tthis.tos = tos;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个抄送人（carbon copy）\n\t *\n\t * @param ccs 抄送人列表\n\t * @return this\n\t * @since 4.0.3\n\t */\n\tpublic JakartaMail setCcs(String... ccs) {\n\t\tthis.ccs = ccs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个密送人（blind carbon copy）\n\t *\n\t * @param bccs 密送人列表\n\t * @return this\n\t * @since 4.0.3\n\t */\n\tpublic JakartaMail setBccs(String... bccs) {\n\t\tthis.bccs = bccs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个回复地址(reply-to)\n\t *\n\t * @param reply 回复地址(reply-to)列表\n\t * @return this\n\t * @since 4.6.0\n\t */\n\tpublic JakartaMail setReply(String... reply) {\n\t\tthis.reply = reply;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置标题\n\t *\n\t * @param title 标题\n\t * @return this\n\t */\n\tpublic JakartaMail setTitle(String title) {\n\t\tthis.title = title;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置正文<br>\n\t * 正文可以是普通文本也可以是HTML（默认普通文本），可以通过调用{@link #setHtml(boolean)} 设置是否为HTML\n\t *\n\t * @param content 正文\n\t * @return this\n\t */\n\tpublic JakartaMail setContent(String content) {\n\t\tthis.content = content;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否是HTML\n\t *\n\t * @param isHtml 是否为HTML\n\t * @return this\n\t */\n\tpublic JakartaMail setHtml(boolean isHtml) {\n\t\tthis.isHtml = isHtml;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置正文\n\t *\n\t * @param content 正文内容\n\t * @param isHtml  是否为HTML\n\t * @return this\n\t */\n\tpublic JakartaMail setContent(String content, boolean isHtml) {\n\t\tsetContent(content);\n\t\treturn setHtml(isHtml);\n\t}\n\n\t/**\n\t * 设置文件类型附件，文件可以是图片文件，此时自动设置cid（正文中引用图片），默认cid为文件名\n\t *\n\t * @param files 附件文件列表\n\t * @return this\n\t */\n\tpublic JakartaMail setFiles(File... files) {\n\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tfinal DataSource[] attachments = new DataSource[files.length];\n\t\tfor (int i = 0; i < files.length; i++) {\n\t\t\tattachments[i] = new FileDataSource(files[i]);\n\t\t}\n\t\treturn setAttachments(attachments);\n\t}\n\n\t/**\n\t * 增加附件或图片，附件使用{@link DataSource} 形式表示，可以使用{@link FileDataSource}包装文件表示文件附件\n\t *\n\t * @param attachments 附件列表\n\t * @return this\n\t * @since 4.0.9\n\t */\n\tpublic JakartaMail setAttachments(DataSource... attachments) {\n\t\tif (ArrayUtil.isNotEmpty(attachments)) {\n\t\t\tfinal Charset charset = this.mailAccount.getCharset();\n\t\t\tMimeBodyPart bodyPart;\n\t\t\tString nameEncoded;\n\t\t\ttry {\n\t\t\t\tfor (DataSource attachment : attachments) {\n\t\t\t\t\tbodyPart = new MimeBodyPart();\n\t\t\t\t\tbodyPart.setDataHandler(new DataHandler(attachment));\n\t\t\t\t\tnameEncoded = attachment.getName();\n\t\t\t\t\tif (this.mailAccount.isEncodefilename()) {\n\t\t\t\t\t\tnameEncoded = JakartaInternalMailUtil.encodeText(nameEncoded, charset);\n\t\t\t\t\t}\n\t\t\t\t\t// 普通附件文件名\n\t\t\t\t\tbodyPart.setFileName(nameEncoded);\n\t\t\t\t\tif (StrUtil.startWith(attachment.getContentType(), \"image/\")) {\n\t\t\t\t\t\t// 图片附件，用于正文中引用图片\n\t\t\t\t\t\tbodyPart.setContentID(nameEncoded);\n\t\t\t\t\t\t// 图片附件设置内联,否则无法正常引用图片\n\t\t\t\t\t\tbodyPart.setDisposition(MimeBodyPart.INLINE);\n\t\t\t\t\t}\n\t\t\t\t\tthis.multipart.addBodyPart(bodyPart);\n\t\t\t\t}\n\t\t\t} catch (MessagingException e) {\n\t\t\t\tthrow new MailException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串，图片类型默认为\"image/jpeg\"\n\t *\n\t * @param cid         图片与占位符，占位符格式为cid:${cid}\n\t * @param imageStream 图片文件\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic JakartaMail addImage(String cid, InputStream imageStream) {\n\t\treturn addImage(cid, imageStream, null);\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串\n\t *\n\t * @param cid         图片与占位符，占位符格式为cid:${cid}\n\t * @param imageStream 图片流，不关闭\n\t * @param contentType 图片类型，null赋值默认的\"image/jpeg\"\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic JakartaMail addImage(String cid, InputStream imageStream, String contentType) {\n\t\tByteArrayDataSource imgSource;\n\t\ttry {\n\t\t\timgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, \"image/jpeg\"));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\timgSource.setName(cid);\n\t\treturn setAttachments(imgSource);\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串\n\t *\n\t * @param cid       图片与占位符，占位符格式为cid:${cid}\n\t * @param imageFile 图片文件\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic JakartaMail addImage(String cid, File imageFile) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(imageFile);\n\t\t\treturn addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 设置字符集编码\n\t *\n\t * @param charset 字符集编码\n\t * @return this\n\t * @see MailAccount#setCharset(Charset)\n\t */\n\tpublic JakartaMail setCharset(Charset charset) {\n\t\tthis.mailAccount.setCharset(charset);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否使用全局会话，默认为true\n\t *\n\t * @param isUseGlobalSession 是否使用全局会话，默认为true\n\t * @return this\n\t * @since 4.0.2\n\t */\n\tpublic JakartaMail setUseGlobalSession(boolean isUseGlobalSession) {\n\t\tthis.useGlobalSession = isUseGlobalSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置debug输出位置，可以自定义debug日志\n\t *\n\t * @param debugOutput debug输出位置\n\t * @return this\n\t * @since 5.5.6\n\t */\n\tpublic JakartaMail setDebugOutput(PrintStream debugOutput) {\n\t\tthis.debugOutput = debugOutput;\n\t\treturn this;\n\t}\n\t// --------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic MimeMessage build() {\n\t\ttry {\n\t\t\treturn buildMsg();\n\t\t} catch (MessagingException e) {\n\t\t\tthrow new MailException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 发送\n\t *\n\t * @return message-id\n\t * @throws MailException 邮件发送异常\n\t */\n\tpublic String send() throws MailException {\n\t\ttry {\n\t\t\treturn doSend();\n\t\t} catch (MessagingException e) {\n\t\t\tif (e instanceof SendFailedException) {\n\t\t\t\t// 当地址无效时，显示更加详细的无效地址信息\n\t\t\t\tfinal Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();\n\t\t\t\tfinal String msg = StrUtil.format(\"Invalid Addresses: {}\", ArrayUtil.toString(invalidAddresses));\n\t\t\t\tthrow new MailException(msg, e);\n\t\t\t}\n\t\t\tthrow new MailException(e);\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------- Private method start\n\n\t/**\n\t * 执行发送\n\t *\n\t * @return message-id\n\t * @throws MessagingException 发送异常\n\t */\n\tprivate String doSend() throws MessagingException {\n\t\tfinal MimeMessage mimeMessage = buildMsg();\n\t\tTransport.send(mimeMessage);\n\t\treturn mimeMessage.getMessageID();\n\t}\n\n\t/**\n\t * 构建消息\n\t *\n\t * @return {@link MimeMessage}消息\n\t * @throws MessagingException 消息异常\n\t */\n\tprivate MimeMessage buildMsg() throws MessagingException {\n\t\tfinal Charset charset = this.mailAccount.getCharset();\n\t\tfinal MimeMessage msg = new MimeMessage(getSession());\n\t\t// 发件人\n\t\tfinal String from = this.mailAccount.getFrom();\n\t\tif (StrUtil.isEmpty(from)) {\n\t\t\t// 用户未提供发送方，则从Session中自动获取\n\t\t\tmsg.setFrom();\n\t\t} else {\n\t\t\tmsg.setFrom(JakartaInternalMailUtil.parseFirstAddress(from, charset));\n\t\t}\n\t\t// 标题\n\t\tmsg.setSubject(this.title, (null == charset) ? null : charset.name());\n\t\t// 发送时间\n\t\tmsg.setSentDate(new Date());\n\t\t// 内容和附件\n\t\tmsg.setContent(buildContent(charset));\n\t\t// 收件人\n\t\tmsg.setRecipients(MimeMessage.RecipientType.TO, JakartaInternalMailUtil.parseAddressFromStrs(this.tos, charset));\n\t\t// 抄送人\n\t\tif (ArrayUtil.isNotEmpty(this.ccs)) {\n\t\t\tmsg.setRecipients(MimeMessage.RecipientType.CC, JakartaInternalMailUtil.parseAddressFromStrs(this.ccs, charset));\n\t\t}\n\t\t// 密送人\n\t\tif (ArrayUtil.isNotEmpty(this.bccs)) {\n\t\t\tmsg.setRecipients(MimeMessage.RecipientType.BCC, JakartaInternalMailUtil.parseAddressFromStrs(this.bccs, charset));\n\t\t}\n\t\t// 回复地址(reply-to)\n\t\tif (ArrayUtil.isNotEmpty(this.reply)) {\n\t\t\tmsg.setReplyTo(JakartaInternalMailUtil.parseAddressFromStrs(this.reply, charset));\n\t\t}\n\n\t\treturn msg;\n\t}\n\n\t/**\n\t * 构建邮件信息主体\n\t *\n\t * @param charset 编码，{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}\n\t * @return 邮件信息主体\n\t * @throws MessagingException 消息异常\n\t */\n\tprivate Multipart buildContent(Charset charset) throws MessagingException {\n\t\tfinal String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();\n\t\t// 正文\n\t\tfinal MimeBodyPart body = new MimeBodyPart();\n\t\tbody.setContent(content, StrUtil.format(\"text/{}; charset={}\", isHtml ? \"html\" : \"plain\", charsetStr));\n\t\tthis.multipart.addBodyPart(body);\n\n\t\treturn this.multipart;\n\t}\n\n\t/**\n\t * 获取默认邮件会话<br>\n\t * 如果为全局单例的会话，则全局只允许一个邮件帐号，否则每次发送邮件会新建一个新的会话\n\t *\n\t * @return 邮件会话 {@link Session}\n\t */\n\tprivate Session getSession() {\n\t\tfinal Session session = JakartaMailUtil.getSession(this.mailAccount, this.useGlobalSession);\n\n\t\tif (null != this.debugOutput) {\n\t\t\tsession.setDebugOut(debugOutput);\n\t\t}\n\n\t\treturn session;\n\t}\n\t// --------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMailUtil.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport jakarta.mail.Authenticator;\nimport jakarta.mail.Session;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * 邮件工具类，基于jakarta.mail封装\n *\n * @author looly\n * @since 5.8.30\n */\npublic class JakartaMailUtil {\n\t/**\n\t * 使用配置文件中设置的账户发送文本邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendText(String to, String subject, String content, File... files) {\n\t\treturn send(to, subject, content, false, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(String to, String subject, String content, File... files) {\n\t\treturn send(to, subject, content, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(String to, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人、抄送人、密送人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param cc      抄送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param bcc     密送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送文本邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String sendText(Collection<String> tos, String subject, String content, File... files) {\n\t\treturn send(tos, subject, content, false, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(Collection<String> tos, String subject, String content, File... files) {\n\t\treturn send(tos, subject, content, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(tos, null, null, subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param ccs     抄送人列表，可以为null或空\n\t * @param bccs    密送人列表，可以为null或空\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件认证对象\n\t * @param to          收件人，多个收件人逗号或者分号隔开\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, splitAddress(to), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, tos, null, null, subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param ccs         抄送人列表，可以为null或空\n\t * @param bccs        密送人列表，可以为null或空\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) {\n\t\treturn send(to, subject, content, imageMap, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人、抄送人、密送人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param cc       抄送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param bcc      密送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) {\n\t\treturn send(tos, subject, content, imageMap, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(tos, null, null, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param ccs      抄送人列表，可以为null或空\n\t * @param bccs     密送人列表，可以为null或空\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件认证对象\n\t * @param to          收件人，多个收件人逗号或者分号隔开\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param ccs         抄送人列表，可以为null或空\n\t * @param bccs        密送人列表，可以为null或空\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap,\n\t\t\t\t\t\t\t  boolean isHtml, File... files) {\n\t\treturn send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 根据配置文件，获取邮件客户端会话\n\t *\n\t * @param mailAccount 邮件账户配置\n\t * @param isSingleton 是否单例（全局共享会话）\n\t * @return {@link Session}\n\t * @since 5.5.7\n\t */\n\tpublic static Session getSession(MailAccount mailAccount, boolean isSingleton) {\n\t\tAuthenticator authenticator = null;\n\t\tif (mailAccount.isAuth()) {\n\t\t\tauthenticator = new JakartaUserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());\n\t\t}\n\n\t\treturn isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //\n\t\t\t\t: Session.getInstance(mailAccount.getSmtpProps(), authenticator);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount      邮件帐户信息\n\t * @param useGlobalSession 是否全局共享Session\n\t * @param tos              收件人列表\n\t * @param ccs              抄送人列表，可以为null或空\n\t * @param bccs             密送人列表，可以为null或空\n\t * @param subject          标题\n\t * @param content          正文\n\t * @param imageMap         图片与占位符，占位符格式为cid:${cid}\n\t * @param isHtml           是否为HTML格式\n\t * @param files            附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tprivate static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,\n\t\t\t\t\t\t\t   Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\tfinal JakartaMail mail = JakartaMail.create(mailAccount).setUseGlobalSession(useGlobalSession);\n\n\t\t// 可选抄送人\n\t\tif (CollUtil.isNotEmpty(ccs)) {\n\t\t\tmail.setCcs(ccs.toArray(new String[0]));\n\t\t}\n\t\t// 可选密送人\n\t\tif (CollUtil.isNotEmpty(bccs)) {\n\t\t\tmail.setBccs(bccs.toArray(new String[0]));\n\t\t}\n\n\t\tmail.setTos(tos.toArray(new String[0]));\n\t\tmail.setTitle(subject);\n\t\tmail.setContent(content);\n\t\tmail.setHtml(isHtml);\n\t\tmail.setFiles(files);\n\n\t\t// 图片\n\t\tif (MapUtil.isNotEmpty(imageMap)) {\n\t\t\tfor (Entry<String, InputStream> entry : imageMap.entrySet()) {\n\t\t\t\tmail.addImage(entry.getKey(), entry.getValue());\n\t\t\t\t// 关闭流\n\t\t\t\tIoUtil.close(entry.getValue());\n\t\t\t}\n\t\t}\n\n\t\treturn mail.send();\n\t}\n\n\t/**\n\t * 将多个联系人转为列表，分隔符为逗号或者分号\n\t *\n\t * @param addresses 多个联系人，如果为空返回null\n\t * @return 联系人列表\n\t */\n\tprivate static List<String> splitAddress(String addresses) {\n\t\tif (StrUtil.isBlank(addresses)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tList<String> result;\n\t\tif (StrUtil.contains(addresses, CharUtil.COMMA)) {\n\t\t\tresult = StrUtil.splitTrim(addresses, CharUtil.COMMA);\n\t\t} else if (StrUtil.contains(addresses, ';')) {\n\t\t\tresult = StrUtil.splitTrim(addresses, ';');\n\t\t} else {\n\t\t\tresult = CollUtil.newArrayList(addresses);\n\t\t}\n\t\treturn result;\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaUserPassAuthenticator.java",
    "content": "package cn.hutool.extra.mail;\n\nimport jakarta.mail.Authenticator;\nimport jakarta.mail.PasswordAuthentication;\n\n/**\n * jakarta用户名密码验证器\n *\n * @author looly\n * @since 5.8.30\n */\npublic class JakartaUserPassAuthenticator extends Authenticator {\n\n\tprivate final String user;\n\tprivate final String pass;\n\n\t/**\n\t * 构造\n\t *\n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic JakartaUserPassAuthenticator(String user, String pass) {\n\t\tthis.user = user;\n\t\tthis.pass = pass;\n\t}\n\n\t@Override\n\tprotected PasswordAuthentication getPasswordAuthentication() {\n\t\treturn new PasswordAuthentication(this.user, this.pass);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.builder.Builder;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.activation.*;\nimport javax.mail.Address;\nimport javax.mail.MessagingException;\nimport javax.mail.Multipart;\nimport javax.mail.SendFailedException;\nimport javax.mail.Session;\nimport javax.mail.Transport;\nimport javax.mail.internet.MimeBodyPart;\nimport javax.mail.internet.MimeMessage;\nimport javax.mail.internet.MimeMultipart;\nimport javax.mail.internet.MimeUtility;\nimport javax.mail.util.ByteArrayDataSource;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PrintStream;\nimport java.nio.charset.Charset;\nimport java.util.Date;\n\n/**\n * 邮件发送客户端\n *\n * @author looly\n * @since 3.2.0\n */\npublic class Mail implements Builder<MimeMessage> {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 邮箱帐户信息以及一些客户端配置信息\n\t */\n\tprivate final MailAccount mailAccount;\n\t/**\n\t * 收件人列表\n\t */\n\tprivate String[] tos;\n\t/**\n\t * 抄送人列表（carbon copy）\n\t */\n\tprivate String[] ccs;\n\t/**\n\t * 密送人列表（blind carbon copy）\n\t */\n\tprivate String[] bccs;\n\t/**\n\t * 回复地址(reply-to)\n\t */\n\tprivate String[] reply;\n\t/**\n\t * 标题\n\t */\n\tprivate String title;\n\t/**\n\t * 内容\n\t */\n\tprivate String content;\n\t/**\n\t * 是否为HTML\n\t */\n\tprivate boolean isHtml;\n\t/**\n\t * 正文、附件和图片的混合部分\n\t */\n\tprivate final Multipart multipart = new MimeMultipart();\n\t/**\n\t * 是否使用全局会话，默认为false\n\t */\n\tprivate boolean useGlobalSession = false;\n\n\t/**\n\t * debug输出位置，可以自定义debug日志\n\t */\n\tprivate PrintStream debugOutput;\n\n\t/**\n\t * 创建邮件客户端\n\t *\n\t * @param mailAccount 邮件帐号\n\t * @return Mail\n\t */\n\tpublic static Mail create(MailAccount mailAccount) {\n\t\treturn new Mail(mailAccount);\n\t}\n\n\t/**\n\t * 创建邮件客户端，使用全局邮件帐户\n\t *\n\t * @return Mail\n\t */\n\tpublic static Mail create() {\n\t\treturn new Mail();\n\t}\n\n\t// --------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，使用全局邮件帐户\n\t */\n\tpublic Mail() {\n\t\tthis(GlobalMailAccount.INSTANCE.getAccount());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param mailAccount 邮件帐户，如果为null使用默认配置文件的全局邮件配置\n\t */\n\tpublic Mail(MailAccount mailAccount) {\n\t\tmailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();\n\t\tthis.mailAccount = mailAccount.defaultIfEmpty();\n\t}\n\t// --------------------------------------------------------------- Constructor end\n\n\t// --------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 设置收件人\n\t *\n\t * @param tos 收件人列表\n\t * @return this\n\t * @see #setTos(String...)\n\t */\n\tpublic Mail to(String... tos) {\n\t\treturn setTos(tos);\n\t}\n\n\t/**\n\t * 设置多个收件人\n\t *\n\t * @param tos 收件人列表\n\t * @return this\n\t */\n\tpublic Mail setTos(String... tos) {\n\t\tthis.tos = tos;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个抄送人（carbon copy）\n\t *\n\t * @param ccs 抄送人列表\n\t * @return this\n\t * @since 4.0.3\n\t */\n\tpublic Mail setCcs(String... ccs) {\n\t\tthis.ccs = ccs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个密送人（blind carbon copy）\n\t *\n\t * @param bccs 密送人列表\n\t * @return this\n\t * @since 4.0.3\n\t */\n\tpublic Mail setBccs(String... bccs) {\n\t\tthis.bccs = bccs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置多个回复地址(reply-to)\n\t *\n\t * @param reply 回复地址(reply-to)列表\n\t * @return this\n\t * @since 4.6.0\n\t */\n\tpublic Mail setReply(String... reply) {\n\t\tthis.reply = reply;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置标题\n\t *\n\t * @param title 标题\n\t * @return this\n\t */\n\tpublic Mail setTitle(String title) {\n\t\tthis.title = title;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置正文<br>\n\t * 正文可以是普通文本也可以是HTML（默认普通文本），可以通过调用{@link #setHtml(boolean)} 设置是否为HTML\n\t *\n\t * @param content 正文\n\t * @return this\n\t */\n\tpublic Mail setContent(String content) {\n\t\tthis.content = content;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否是HTML\n\t *\n\t * @param isHtml 是否为HTML\n\t * @return this\n\t */\n\tpublic Mail setHtml(boolean isHtml) {\n\t\tthis.isHtml = isHtml;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置正文\n\t *\n\t * @param content 正文内容\n\t * @param isHtml  是否为HTML\n\t * @return this\n\t */\n\tpublic Mail setContent(String content, boolean isHtml) {\n\t\tsetContent(content);\n\t\treturn setHtml(isHtml);\n\t}\n\n\t/**\n\t * 设置文件类型附件，文件可以是图片文件，此时自动设置cid（正文中引用图片），默认cid为文件名\n\t *\n\t * @param files 附件文件列表\n\t * @return this\n\t */\n\tpublic Mail setFiles(File... files) {\n\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tfinal DataSource[] attachments = new DataSource[files.length];\n\t\tfor (int i = 0; i < files.length; i++) {\n\t\t\tattachments[i] = new FileDataSource(files[i]);\n\t\t}\n\t\treturn setAttachments(attachments);\n\t}\n\n\t/**\n\t * 增加附件或图片，附件使用{@link DataSource} 形式表示，可以使用{@link FileDataSource}包装文件表示文件附件\n\t *\n\t * @param attachments 附件列表\n\t * @return this\n\t * @since 4.0.9\n\t */\n\tpublic Mail setAttachments(DataSource... attachments) {\n\t\tif (ArrayUtil.isNotEmpty(attachments)) {\n\t\t\tfinal Charset charset = this.mailAccount.getCharset();\n\t\t\tMimeBodyPart bodyPart;\n\t\t\tString nameEncoded;\n\t\t\ttry {\n\t\t\t\tfor (DataSource attachment : attachments) {\n\t\t\t\t\tbodyPart = new MimeBodyPart();\n\t\t\t\t\tbodyPart.setDataHandler(new DataHandler(attachment));\n\t\t\t\t\tnameEncoded = attachment.getName();\n\t\t\t\t\tif (this.mailAccount.isEncodefilename()) {\n\t\t\t\t\t\tnameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);\n\t\t\t\t\t}\n\t\t\t\t\t// 普通附件文件名\n\t\t\t\t\tbodyPart.setFileName(nameEncoded);\n\t\t\t\t\tif (StrUtil.startWith(attachment.getContentType(), \"image/\")) {\n\t\t\t\t\t\t// 图片附件，用于正文中引用图片\n\t\t\t\t\t\tbodyPart.setContentID(nameEncoded);\n\t\t\t\t\t\t// 图片附件设置内联,否则无法正常引用图片\n\t\t\t\t\t\tbodyPart.setDisposition(MimeBodyPart.INLINE);\n\t\t\t\t\t}\n\t\t\t\t\tthis.multipart.addBodyPart(bodyPart);\n\t\t\t\t}\n\t\t\t} catch (MessagingException e) {\n\t\t\t\tthrow new MailException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串，图片类型默认为\"image/jpeg\"\n\t *\n\t * @param cid         图片与占位符，占位符格式为cid:${cid}\n\t * @param imageStream 图片文件\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic Mail addImage(String cid, InputStream imageStream) {\n\t\treturn addImage(cid, imageStream, null);\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串\n\t *\n\t * @param cid         图片与占位符，占位符格式为cid:${cid}\n\t * @param imageStream 图片流，不关闭\n\t * @param contentType 图片类型，null赋值默认的\"image/jpeg\"\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic Mail addImage(String cid, InputStream imageStream, String contentType) {\n\t\tByteArrayDataSource imgSource;\n\t\ttry {\n\t\t\timgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, \"image/jpeg\"));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\timgSource.setName(cid);\n\t\treturn setAttachments(imgSource);\n\t}\n\n\t/**\n\t * 增加图片，图片的键对应到邮件模板中的占位字符串\n\t *\n\t * @param cid       图片与占位符，占位符格式为cid:${cid}\n\t * @param imageFile 图片文件\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic Mail addImage(String cid, File imageFile) {\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(imageFile);\n\t\t\treturn addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 设置字符集编码\n\t *\n\t * @param charset 字符集编码\n\t * @return this\n\t * @see MailAccount#setCharset(Charset)\n\t */\n\tpublic Mail setCharset(Charset charset) {\n\t\tthis.mailAccount.setCharset(charset);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否使用全局会话，默认为true\n\t *\n\t * @param isUseGlobalSession 是否使用全局会话，默认为true\n\t * @return this\n\t * @since 4.0.2\n\t */\n\tpublic Mail setUseGlobalSession(boolean isUseGlobalSession) {\n\t\tthis.useGlobalSession = isUseGlobalSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置debug输出位置，可以自定义debug日志\n\t *\n\t * @param debugOutput debug输出位置\n\t * @return this\n\t * @since 5.5.6\n\t */\n\tpublic Mail setDebugOutput(PrintStream debugOutput) {\n\t\tthis.debugOutput = debugOutput;\n\t\treturn this;\n\t}\n\t// --------------------------------------------------------------- Getters and Setters end\n\n\t@Override\n\tpublic MimeMessage build() {\n\t\ttry {\n\t\t\treturn buildMsg();\n\t\t} catch (MessagingException e) {\n\t\t\tthrow new MailException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 发送\n\t *\n\t * @return message-id\n\t * @throws MailException 邮件发送异常\n\t */\n\tpublic String send() throws MailException {\n\t\ttry {\n\t\t\treturn doSend();\n\t\t} catch (MessagingException e) {\n\t\t\tif (e instanceof SendFailedException) {\n\t\t\t\t// 当地址无效时，显示更加详细的无效地址信息\n\t\t\t\tfinal Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();\n\t\t\t\tfinal String msg = StrUtil.format(\"Invalid Addresses: {}\", ArrayUtil.toString(invalidAddresses));\n\t\t\t\tthrow new MailException(msg, e);\n\t\t\t}\n\t\t\tthrow new MailException(e);\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------- Private method start\n\n\t/**\n\t * 执行发送\n\t *\n\t * @return message-id\n\t * @throws MessagingException 发送异常\n\t */\n\tprivate String doSend() throws MessagingException {\n\t\tfinal MimeMessage mimeMessage = buildMsg();\n\t\tTransport.send(mimeMessage);\n\t\treturn mimeMessage.getMessageID();\n\t}\n\n\t/**\n\t * 构建消息\n\t *\n\t * @return {@link MimeMessage}消息\n\t * @throws MessagingException 消息异常\n\t */\n\tprivate MimeMessage buildMsg() throws MessagingException {\n\t\tfinal Charset charset = this.mailAccount.getCharset();\n\t\tfinal MimeMessage msg = new MimeMessage(getSession());\n\t\t// 发件人\n\t\tfinal String from = this.mailAccount.getFrom();\n\t\tif (StrUtil.isEmpty(from)) {\n\t\t\t// 用户未提供发送方，则从Session中自动获取\n\t\t\tmsg.setFrom();\n\t\t} else {\n\t\t\tmsg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));\n\t\t}\n\t\t// 标题\n\t\tmsg.setSubject(this.title, (null == charset) ? null : charset.name());\n\t\t// 发送时间\n\t\tmsg.setSentDate(new Date());\n\t\t// 内容和附件\n\t\tmsg.setContent(buildContent(charset));\n\t\t// 收件人\n\t\tmsg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));\n\t\t// 抄送人\n\t\tif (ArrayUtil.isNotEmpty(this.ccs)) {\n\t\t\tmsg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));\n\t\t}\n\t\t// 密送人\n\t\tif (ArrayUtil.isNotEmpty(this.bccs)) {\n\t\t\tmsg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));\n\t\t}\n\t\t// 回复地址(reply-to)\n\t\tif (ArrayUtil.isNotEmpty(this.reply)) {\n\t\t\tmsg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));\n\t\t}\n\n\t\treturn msg;\n\t}\n\n\t/**\n\t * 构建邮件信息主体\n\t *\n\t * @param charset 编码，{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}\n\t * @return 邮件信息主体\n\t * @throws MessagingException 消息异常\n\t */\n\tprivate Multipart buildContent(Charset charset) throws MessagingException {\n\t\tfinal String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();\n\t\t// 正文\n\t\tfinal MimeBodyPart body = new MimeBodyPart();\n\t\tbody.setContent(content, StrUtil.format(\"text/{}; charset={}\", isHtml ? \"html\" : \"plain\", charsetStr));\n\t\tthis.multipart.addBodyPart(body, 0);\n\n\t\treturn this.multipart;\n\t}\n\n\t/**\n\t * 获取默认邮件会话<br>\n\t * 如果为全局单例的会话，则全局只允许一个邮件帐号，否则每次发送邮件会新建一个新的会话\n\t *\n\t * @return 邮件会话 {@link Session}\n\t */\n\tprivate Session getSession() {\n\t\tfinal Session session = MailUtil.getSession(this.mailAccount, this.useGlobalSession);\n\n\t\tif (null != this.debugOutput) {\n\t\t\tsession.setDebugOut(debugOutput);\n\t\t}\n\n\t\treturn session;\n\t}\n\t// --------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/MailAccount.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.lang.PatternPool;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.setting.Setting;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\n/**\n * 邮件账户对象\n *\n * @author Luxiaolei\n */\npublic class MailAccount implements Serializable {\n\tprivate static final long serialVersionUID = -6937313421815719204L;\n\n\tprivate static final String MAIL_PROTOCOL = \"mail.transport.protocol\";\n\tprivate static final String SMTP_HOST = \"mail.smtp.host\";\n\tprivate static final String SMTP_PORT = \"mail.smtp.port\";\n\tprivate static final String SMTP_AUTH = \"mail.smtp.auth\";\n\tprivate static final String SMTP_TIMEOUT = \"mail.smtp.timeout\";\n\tprivate static final String SMTP_CONNECTION_TIMEOUT = \"mail.smtp.connectiontimeout\";\n\tprivate static final String SMTP_WRITE_TIMEOUT = \"mail.smtp.writetimeout\";\n\n\t// SSL\n\tprivate static final String STARTTLS_ENABLE = \"mail.smtp.starttls.enable\";\n\tprivate static final String SSL_ENABLE = \"mail.smtp.ssl.enable\";\n\tprivate static final String SSL_PROTOCOLS = \"mail.smtp.ssl.protocols\";\n\tprivate static final String SOCKET_FACTORY = \"mail.smtp.socketFactory.class\";\n\tprivate static final String SOCKET_FACTORY_FALLBACK = \"mail.smtp.socketFactory.fallback\";\n\tprivate static final String SOCKET_FACTORY_PORT = \"smtp.socketFactory.port\";\n\n\t// System Properties\n\tprivate static final String SPLIT_LONG_PARAMS = \"mail.mime.splitlongparameters\";\n\t//private static final String ENCODE_FILE_NAME = \"mail.mime.encodefilename\";\n\t//private static final String CHARSET = \"mail.mime.charset\";\n\n\t// 其他\n\tprivate static final String MAIL_DEBUG = \"mail.debug\";\n\n\tpublic static final String[] MAIL_SETTING_PATHS = new String[]{\"config/mail.setting\", \"config/mailAccount.setting\", \"mail.setting\"};\n\n\t/**\n\t * SMTP服务器域名\n\t */\n\tprivate String host;\n\t/**\n\t * SMTP服务端口\n\t */\n\tprivate Integer port;\n\t/**\n\t * 是否需要用户名密码验证\n\t */\n\tprivate Boolean auth;\n\t/**\n\t * 用户名\n\t */\n\tprivate String user;\n\t/**\n\t * 密码\n\t */\n\tprivate String pass;\n\t/**\n\t * 发送方，遵循RFC-822标准\n\t */\n\tprivate String from;\n\n\t/**\n\t * 是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\n\t */\n\tprivate boolean debug;\n\t/**\n\t * 编码用于编码邮件正文和发送人、收件人等中文\n\t */\n\tprivate Charset charset = CharsetUtil.CHARSET_UTF_8;\n\t/**\n\t * 对于超长参数是否切分为多份，默认为false（国内邮箱附件不支持切分的附件名）\n\t */\n\tprivate boolean splitlongparameters = false;\n\t/**\n\t * 对于文件名是否使用{@link #charset}编码，默认为 {@code true}\n\t */\n\tprivate boolean encodefilename = true;\n\n\t/**\n\t * 使用 STARTTLS安全连接，STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接（TLS或SSL）， 而不是使用一个单独的加密通信端口。\n\t */\n\tprivate boolean starttlsEnable = false;\n\t/**\n\t * 使用 SSL安全连接\n\t */\n\tprivate Boolean sslEnable;\n\n\t/**\n\t * SSL协议，多个协议用空格分隔\n\t */\n\tprivate String sslProtocols;\n\n\t/**\n\t * 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字\n\t */\n\tprivate String socketFactoryClass = \"javax.net.ssl.SSLSocketFactory\";\n\t/**\n\t * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\n\t */\n\tprivate boolean socketFactoryFallback;\n\t/**\n\t * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口\n\t */\n\tprivate int socketFactoryPort = 465;\n\n\t/**\n\t * SMTP超时时长，单位毫秒，缺省值不超时\n\t */\n\tprivate long timeout;\n\t/**\n\t * Socket连接超时值，单位毫秒，缺省值不超时\n\t */\n\tprivate long connectionTimeout;\n\t/**\n\t * Socket写出超时值，单位毫秒，缺省值不超时\n\t */\n\tprivate long writeTimeout;\n\n\t/**\n\t * 自定义的其他属性，此自定义属性会覆盖默认属性\n\t */\n\tprivate final Map<String, Object> customProperty = new HashMap<>();\n\n\t// -------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造,所有参数需自行定义或保持默认值\n\t */\n\tpublic MailAccount() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param settingPath 配置文件路径\n\t */\n\tpublic MailAccount(String settingPath) {\n\t\tthis(new Setting(settingPath));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param setting 配置文件\n\t */\n\tpublic MailAccount(Setting setting) {\n\t\tsetting.toBean(this);\n\n\t\t// since 5.8.30, custom property\n\t\tsetting.forEach((key, value) -> {\n\t\t\tif (StrUtil.startWith(key, \"mail.\")) {\n\t\t\t\tthis.setCustomProperty(key, value);\n\t\t\t}\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获得SMTP服务器域名\n\t *\n\t * @return SMTP服务器域名\n\t */\n\tpublic String getHost() {\n\t\treturn host;\n\t}\n\n\t/**\n\t * 设置SMTP服务器域名\n\t *\n\t * @param host SMTP服务器域名\n\t * @return this\n\t */\n\tpublic MailAccount setHost(String host) {\n\t\tthis.host = host;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得SMTP服务端口\n\t *\n\t * @return SMTP服务端口\n\t */\n\tpublic Integer getPort() {\n\t\treturn port;\n\t}\n\n\t/**\n\t * 设置SMTP服务端口\n\t *\n\t * @param port SMTP服务端口\n\t * @return this\n\t */\n\tpublic MailAccount setPort(Integer port) {\n\t\tthis.port = port;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否需要用户名密码验证\n\t *\n\t * @return 是否需要用户名密码验证\n\t */\n\tpublic Boolean isAuth() {\n\t\treturn auth;\n\t}\n\n\t/**\n\t * 设置是否需要用户名密码验证\n\t *\n\t * @param isAuth 是否需要用户名密码验证\n\t * @return this\n\t */\n\tpublic MailAccount setAuth(Boolean isAuth) {\n\t\tthis.auth = isAuth;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取用户名\n\t *\n\t * @return 用户名\n\t */\n\tpublic String getUser() {\n\t\treturn user;\n\t}\n\n\t/**\n\t * 设置用户名\n\t *\n\t * @param user 用户名\n\t * @return this\n\t */\n\tpublic MailAccount setUser(String user) {\n\t\tthis.user = user;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取密码\n\t *\n\t * @return 密码\n\t */\n\tpublic String getPass() {\n\t\treturn pass;\n\t}\n\n\t/**\n\t * 设置密码\n\t *\n\t * @param pass 密码\n\t * @return this\n\t */\n\tpublic MailAccount setPass(String pass) {\n\t\tthis.pass = pass;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取发送方，遵循RFC-822标准\n\t *\n\t * @return 发送方，遵循RFC-822标准\n\t */\n\tpublic String getFrom() {\n\t\treturn from;\n\t}\n\n\t/**\n\t * 设置发送方，遵循RFC-822标准<br>\n\t * 发件人可以是以下形式：\n\t *\n\t * <pre>\n\t * 1. user@xxx.xx\n\t * 2.  name &lt;user@xxx.xx&gt;\n\t * </pre>\n\t *\n\t * @param from 发送方，遵循RFC-822标准\n\t * @return this\n\t */\n\tpublic MailAccount setFrom(String from) {\n\t\tthis.from = from;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\n\t *\n\t * @return 是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\n\t * @since 4.0.2\n\t */\n\tpublic boolean isDebug() {\n\t\treturn debug;\n\t}\n\n\t/**\n\t * 设置是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\n\t *\n\t * @param debug 是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\n\t * @return this\n\t * @since 4.0.2\n\t */\n\tpublic MailAccount setDebug(boolean debug) {\n\t\tthis.debug = debug;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取字符集编码\n\t *\n\t * @return 编码，可能为{@code null}\n\t */\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\t/**\n\t * 设置字符集编码，此选项不会修改全局配置，若修改全局配置，请设置此项为{@code null}并设置：\n\t * <pre>\n\t * \tSystem.setProperty(\"mail.mime.charset\", charset);\n\t * </pre>\n\t *\n\t * @param charset 字符集编码，{@code null} 则表示使用全局设置的默认编码，全局编码为mail.mime.charset系统属性\n\t * @return this\n\t */\n\tpublic MailAccount setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 对于超长参数是否切分为多份，默认为false（国内邮箱附件不支持切分的附件名）\n\t *\n\t * @return 对于超长参数是否切分为多份\n\t */\n\tpublic boolean isSplitlongparameters() {\n\t\treturn splitlongparameters;\n\t}\n\n\t/**\n\t * 设置对于超长参数是否切分为多份，默认为false（国内邮箱附件不支持切分的附件名）<br>\n\t * 注意此项为全局设置，此项会调用\n\t * <pre>\n\t * System.setProperty(\"mail.mime.splitlongparameters\", true)\n\t * </pre>\n\t *\n\t * @param splitlongparameters 对于超长参数是否切分为多份\n\t */\n\tpublic void setSplitlongparameters(boolean splitlongparameters) {\n\t\tthis.splitlongparameters = splitlongparameters;\n\t}\n\n\t/**\n\t * 对于文件名是否使用{@link #charset}编码，默认为 {@code true}\n\t *\n\t * @return 对于文件名是否使用{@link #charset}编码，默认为 {@code true}\n\t * @since 5.7.16\n\t */\n\tpublic boolean isEncodefilename() {\n\n\t\treturn encodefilename;\n\t}\n\n\t/**\n\t * 设置对于文件名是否使用{@link #charset}编码，此选项不会修改全局配置<br>\n\t * 如果此选项设置为{@code false}，则是否编码取决于两个系统属性：\n\t * <ul>\n\t *     <li>mail.mime.encodefilename  是否编码附件文件名</li>\n\t *     <li>mail.mime.charset         编码文件名的编码</li>\n\t * </ul>\n\t *\n\t * @param encodefilename 对于文件名是否使用{@link #charset}编码\n\t * @since 5.7.16\n\t */\n\tpublic void setEncodefilename(boolean encodefilename) {\n\t\tthis.encodefilename = encodefilename;\n\t}\n\n\t/**\n\t * 是否使用 STARTTLS安全连接，STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接（TLS或SSL）， 而不是使用一个单独的加密通信端口。\n\t *\n\t * @return 是否使用 STARTTLS安全连接\n\t */\n\tpublic boolean isStarttlsEnable() {\n\t\treturn this.starttlsEnable;\n\t}\n\n\t/**\n\t * 设置是否使用STARTTLS安全连接，STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接（TLS或SSL）， 而不是使用一个单独的加密通信端口。\n\t *\n\t * @param startttlsEnable 是否使用STARTTLS安全连接\n\t * @return this\n\t */\n\tpublic MailAccount setStarttlsEnable(boolean startttlsEnable) {\n\t\tthis.starttlsEnable = startttlsEnable;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否使用 SSL安全连接\n\t *\n\t * @return 是否使用 SSL安全连接\n\t */\n\tpublic Boolean isSslEnable() {\n\t\treturn this.sslEnable;\n\t}\n\n\t/**\n\t * 设置是否使用SSL安全连接\n\t *\n\t * @param sslEnable 是否使用SSL安全连接\n\t * @return this\n\t */\n\tpublic MailAccount setSslEnable(Boolean sslEnable) {\n\t\tthis.sslEnable = sslEnable;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取SSL协议，多个协议用空格分隔\n\t *\n\t * @return SSL协议，多个协议用空格分隔\n\t * @since 5.5.7\n\t */\n\tpublic String getSslProtocols() {\n\t\treturn sslProtocols;\n\t}\n\n\t/**\n\t * 设置SSL协议，多个协议用空格分隔\n\t *\n\t * @param sslProtocols SSL协议，多个协议用空格分隔\n\t * @since 5.5.7\n\t */\n\tpublic void setSslProtocols(String sslProtocols) {\n\t\tthis.sslProtocols = sslProtocols;\n\t}\n\n\t/**\n\t * 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字\n\t *\n\t * @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字\n\t */\n\tpublic String getSocketFactoryClass() {\n\t\treturn socketFactoryClass;\n\t}\n\n\t/**\n\t * 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字\n\t *\n\t * @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字\n\t * @return this\n\t */\n\tpublic MailAccount setSocketFactoryClass(String socketFactoryClass) {\n\t\tthis.socketFactoryClass = socketFactoryClass;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\n\t *\n\t * @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\n\t */\n\tpublic boolean isSocketFactoryFallback() {\n\t\treturn socketFactoryFallback;\n\t}\n\n\t/**\n\t * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\n\t *\n\t * @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\n\t * @return this\n\t */\n\tpublic MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {\n\t\tthis.socketFactoryFallback = socketFactoryFallback;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口\n\t *\n\t * @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口\n\t */\n\tpublic int getSocketFactoryPort() {\n\t\treturn socketFactoryPort;\n\t}\n\n\t/**\n\t * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口\n\t *\n\t * @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口\n\t * @return this\n\t */\n\tpublic MailAccount setSocketFactoryPort(int socketFactoryPort) {\n\t\tthis.socketFactoryPort = socketFactoryPort;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置SMTP超时时长，单位毫秒，缺省值不超时\n\t *\n\t * @param timeout SMTP超时时长，单位毫秒，缺省值不超时\n\t * @return this\n\t * @since 4.1.17\n\t */\n\tpublic MailAccount setTimeout(long timeout) {\n\t\tthis.timeout = timeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Socket连接超时值，单位毫秒，缺省值不超时\n\t *\n\t * @param connectionTimeout Socket连接超时值，单位毫秒，缺省值不超时\n\t * @return this\n\t * @since 4.1.17\n\t */\n\tpublic MailAccount setConnectionTimeout(long connectionTimeout) {\n\t\tthis.connectionTimeout = connectionTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Socket写出超时值，单位毫秒，缺省值不超时\n\t *\n\t * @param writeTimeout Socket写出超时值，单位毫秒，缺省值不超时\n\t * @return this\n\t * @since 5.8.3\n\t */\n\tpublic MailAccount setWriteTimeout(long writeTimeout) {\n\t\tthis.writeTimeout = writeTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取自定义属性列表\n\t *\n\t * @return 自定义参数列表\n\t * @since 5.6.4\n\t */\n\tpublic Map<String, Object> getCustomProperty() {\n\t\treturn customProperty;\n\t}\n\n\t/**\n\t * 设置自定义属性，如mail.smtp.ssl.socketFactory\n\t *\n\t * @param key   属性名，空白被忽略\n\t * @param value 属性值， null被忽略\n\t * @return this\n\t * @since 5.6.4\n\t */\n\tpublic MailAccount setCustomProperty(String key, Object value) {\n\t\tif (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {\n\t\t\tthis.customProperty.put(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得SMTP相关信息\n\t *\n\t * @return {@link Properties}\n\t */\n\tpublic Properties getSmtpProps() {\n\t\t//全局系统参数\n\t\tSystem.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));\n\n\t\tfinal Properties p = new Properties();\n\t\tp.put(MAIL_PROTOCOL, \"smtp\");\n\t\tp.put(SMTP_HOST, this.host);\n\t\tp.put(SMTP_PORT, String.valueOf(this.port));\n\t\tp.put(SMTP_AUTH, String.valueOf(this.auth));\n\t\tif (this.timeout > 0) {\n\t\t\tp.put(SMTP_TIMEOUT, String.valueOf(this.timeout));\n\t\t}\n\t\tif (this.connectionTimeout > 0) {\n\t\t\tp.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));\n\t\t}\n\t\t// issue#2355\n\t\tif (this.writeTimeout > 0) {\n\t\t\tp.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));\n\t\t}\n\n\t\tp.put(MAIL_DEBUG, String.valueOf(this.debug));\n\n\t\tif (this.starttlsEnable) {\n\t\t\t//STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接（TLS或SSL）， 而不是使用一个单独的加密通信端口。\n\t\t\tp.put(STARTTLS_ENABLE, \"true\");\n\n\t\t\tif (null == this.sslEnable) {\n\t\t\t\t//为了兼容旧版本，当用户没有此项配置时，按照starttlsEnable开启状态时对待\n\t\t\t\tthis.sslEnable = true;\n\t\t\t}\n\t\t}\n\n\t\t// SSL\n\t\tif (null != this.sslEnable && this.sslEnable) {\n\t\t\tp.put(SSL_ENABLE, \"true\");\n\t\t\tp.put(SOCKET_FACTORY, socketFactoryClass);\n\t\t\tp.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));\n\t\t\tp.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));\n\t\t\t// issue#IZN95@Gitee，在Linux下需自定义SSL协议版本\n\t\t\tif (StrUtil.isNotBlank(this.sslProtocols)) {\n\t\t\t\tp.put(SSL_PROTOCOLS, this.sslProtocols);\n\t\t\t}\n\t\t}\n\n\t\t// 补充自定义属性，允许自定属性覆盖已经设置的值\n\t\tp.putAll(this.customProperty);\n\n\t\treturn p;\n\t}\n\n\t/**\n\t * 如果某些值为null，使用默认值\n\t *\n\t * @return this\n\t */\n\tpublic MailAccount defaultIfEmpty() {\n\t\t// 去掉发件人的姓名部分\n\t\tfinal String fromAddress = ReUtil.get(PatternPool.EMAIL, this.from, 0);\n\n\t\tif (StrUtil.isBlank(this.host)) {\n\t\t\t// 如果SMTP地址为空，默认使用smtp.<发件人邮箱后缀>\n\t\t\tthis.host = StrUtil.format(\"smtp.{}\", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));\n\t\t}\n\t\tif (StrUtil.isBlank(user)) {\n\t\t\t// 如果用户名为空，默认为发件人（issue#I4FYVY@Gitee）\n\t\t\t//this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));\n\t\t\tthis.user = fromAddress;\n\t\t}\n\t\tif (null == this.auth) {\n\t\t\t// 如果密码非空白，则使用认证模式\n\t\t\tthis.auth = (false == StrUtil.isBlank(this.pass));\n\t\t}\n\t\tif (null == this.port) {\n\t\t\t// 端口在SSL状态下默认与socketFactoryPort一致，非SSL状态下默认为25\n\t\t\tthis.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;\n\t\t}\n\t\tif (null == this.charset) {\n\t\t\t// 默认UTF-8编码\n\t\t\tthis.charset = CharsetUtil.CHARSET_UTF_8;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"MailAccount [host=\" + host + \", port=\" + port + \", auth=\" + auth + \", user=\" + user + \", pass=\" + (StrUtil.isEmpty(this.pass) ? \"\" : \"******\") + \", from=\" + from + \", startttlsEnable=\"\n\t\t\t\t+ starttlsEnable + \", socketFactoryClass=\" + socketFactoryClass + \", socketFactoryFallback=\" + socketFactoryFallback + \", socketFactoryPort=\" + socketFactoryPort + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/MailException.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 邮件异常\n * @author xiaoleilu\n */\npublic class MailException extends RuntimeException{\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic MailException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic MailException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic MailException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic MailException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic MailException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.mail.Authenticator;\nimport javax.mail.Session;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * 邮件工具类，基于javax.mail封装\n *\n * @author looly\n * @since 3.1.2\n */\npublic class MailUtil {\n\t/**\n\t * 使用配置文件中设置的账户发送文本邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendText(String to, String subject, String content, File... files) {\n\t\treturn send(to, subject, content, false, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(String to, String subject, String content, File... files) {\n\t\treturn send(to, subject, content, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(String to, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人、抄送人、密送人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to      收件人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param cc      抄送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param bcc     密送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送文本邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String sendText(Collection<String> tos, String subject, String content, File... files) {\n\t\treturn send(tos, subject, content, false, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(Collection<String> tos, String subject, String content, File... files) {\n\t\treturn send(tos, subject, content, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(tos, null, null, subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos     收件人列表\n\t * @param ccs     抄送人列表，可以为null或空\n\t * @param bccs    密送人列表，可以为null或空\n\t * @param subject 标题\n\t * @param content 正文\n\t * @param isHtml  是否为HTML\n\t * @param files   附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件认证对象\n\t * @param to          收件人，多个收件人逗号或者分号隔开\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, splitAddress(to), subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, tos, null, null, subject, content, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param ccs         抄送人列表，可以为null或空\n\t * @param bccs        密送人列表，可以为null或空\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) {\n\t\treturn send(to, subject, content, imageMap, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送单个或多个收件人<br>\n\t * 多个收件人、抄送人、密送人可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t *\n\t * @param to       收件人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param cc       抄送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param bcc      密送人，可以使用逗号“,”分隔，也可以通过分号“;”分隔\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送HTML邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) {\n\t\treturn send(tos, subject, content, imageMap, true, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t */\n\tpublic static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(tos, null, null, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 使用配置文件中设置的账户发送邮件，发送给多人\n\t *\n\t * @param tos      收件人列表\n\t * @param ccs      抄送人列表，可以为null或空\n\t * @param bccs     密送人列表，可以为null或空\n\t * @param subject  标题\n\t * @param content  正文\n\t * @param imageMap 图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml   是否为HTML\n\t * @param files    附件列表\n\t * @return message-id\n\t * @since 4.0.3\n\t */\n\tpublic static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件认证对象\n\t * @param to          收件人，多个收件人逗号或者分号隔开\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 3.2.0\n\t */\n\tpublic static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\treturn send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount 邮件帐户信息\n\t * @param tos         收件人列表\n\t * @param ccs         抄送人列表，可以为null或空\n\t * @param bccs        密送人列表，可以为null或空\n\t * @param subject     标题\n\t * @param content     正文\n\t * @param imageMap    图片与占位符，占位符格式为cid:$IMAGE_PLACEHOLDER\n\t * @param isHtml      是否为HTML格式\n\t * @param files       附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tpublic static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap,\n\t\t\t\t\t\t\t  boolean isHtml, File... files) {\n\t\treturn send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);\n\t}\n\n\t/**\n\t * 根据配置文件，获取邮件客户端会话\n\t *\n\t * @param mailAccount 邮件账户配置\n\t * @param isSingleton 是否单例（全局共享会话）\n\t * @return {@link Session}\n\t * @since 5.5.7\n\t */\n\tpublic static Session getSession(MailAccount mailAccount, boolean isSingleton) {\n\t\tAuthenticator authenticator = null;\n\t\tif (mailAccount.isAuth()) {\n\t\t\tauthenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());\n\t\t}\n\n\t\treturn isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //\n\t\t\t\t: Session.getInstance(mailAccount.getSmtpProps(), authenticator);\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------------ Private method start\n\n\t/**\n\t * 发送邮件给多人\n\t *\n\t * @param mailAccount      邮件帐户信息\n\t * @param useGlobalSession 是否全局共享Session\n\t * @param tos              收件人列表\n\t * @param ccs              抄送人列表，可以为null或空\n\t * @param bccs             密送人列表，可以为null或空\n\t * @param subject          标题\n\t * @param content          正文\n\t * @param imageMap         图片与占位符，占位符格式为cid:${cid}\n\t * @param isHtml           是否为HTML格式\n\t * @param files            附件列表\n\t * @return message-id\n\t * @since 4.6.3\n\t */\n\tprivate static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,\n\t\t\t\t\t\t\t   Map<String, InputStream> imageMap, boolean isHtml, File... files) {\n\t\tfinal Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);\n\n\t\t// 可选抄送人\n\t\tif (CollUtil.isNotEmpty(ccs)) {\n\t\t\tmail.setCcs(ccs.toArray(new String[0]));\n\t\t}\n\t\t// 可选密送人\n\t\tif (CollUtil.isNotEmpty(bccs)) {\n\t\t\tmail.setBccs(bccs.toArray(new String[0]));\n\t\t}\n\n\t\tmail.setTos(tos.toArray(new String[0]));\n\t\tmail.setTitle(subject);\n\t\tmail.setContent(content);\n\t\tmail.setHtml(isHtml);\n\t\tmail.setFiles(files);\n\n\t\t// 图片\n\t\tif (MapUtil.isNotEmpty(imageMap)) {\n\t\t\tfor (Entry<String, InputStream> entry : imageMap.entrySet()) {\n\t\t\t\tmail.addImage(entry.getKey(), entry.getValue());\n\t\t\t\t// 关闭流\n\t\t\t\tIoUtil.close(entry.getValue());\n\t\t\t}\n\t\t}\n\n\t\treturn mail.send();\n\t}\n\n\t/**\n\t * 将多个联系人转为列表，分隔符为逗号或者分号\n\t *\n\t * @param addresses 多个联系人，如果为空返回null\n\t * @return 联系人列表\n\t */\n\tprivate static List<String> splitAddress(String addresses) {\n\t\tif (StrUtil.isBlank(addresses)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tList<String> result;\n\t\tif (StrUtil.contains(addresses, CharUtil.COMMA)) {\n\t\t\tresult = StrUtil.splitTrim(addresses, CharUtil.COMMA);\n\t\t} else if (StrUtil.contains(addresses, ';')) {\n\t\t\tresult = StrUtil.splitTrim(addresses, ';');\n\t\t} else {\n\t\t\tresult = CollUtil.newArrayList(addresses);\n\t\t}\n\t\treturn result;\n\t}\n\t// ------------------------------------------------------------------------------------------------------------------------ Private method end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/UserPassAuthenticator.java",
    "content": "package cn.hutool.extra.mail;\n\nimport javax.mail.Authenticator;\nimport javax.mail.PasswordAuthentication;\n\n/**\n * 用户名密码验证器\n * \n * @author looly\n * @since 3.1.2\n */\npublic class UserPassAuthenticator extends Authenticator {\n\n\tprivate final String user;\n\tprivate final String pass;\n\n\t/**\n\t * 构造\n\t * \n\t * @param user 用户名\n\t * @param pass 密码\n\t */\n\tpublic UserPassAuthenticator(String user, String pass) {\n\t\tthis.user = user;\n\t\tthis.pass = pass;\n\t}\n\n\t@Override\n\tprotected PasswordAuthentication getPasswordAuthentication() {\n\t\treturn new PasswordAuthentication(this.user, this.pass);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/mail/package-info.java",
    "content": "/**\n * 邮件封装，基于javax-mail库，入口为MailUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.mail;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/package-info.java",
    "content": "/**\n * 由于Hutool的原则是不依赖于其它配置文件，但是很多时候我们需要针对第三方非常棒的库做一些工具类化的支持，因此Hutoo-extra包主要用于支持第三方库的工具类支持。\n * \n * @author looly\n *\n */\npackage cn.hutool.extra;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/PinyinEngine.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.List;\n\n/**\n * 拼音引擎接口，具体的拼音实现通过实现此接口，完成具体实现功能\n *\n * @author looly\n * @since 5.3.3\n */\npublic interface PinyinEngine {\n\n\t/**\n\t * 如果c为汉字，则返回大写拼音；如果c不是汉字，则返回String.valueOf(c)\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tString getPinyin(char c);\n\n\t/**\n\t * 如果c为汉字，则返回大写拼音；如果c不是汉字，则返回String.valueOf(c)\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @param tone 是否返回声调\n\t * @return 汉字返回拼音，非汉字原样返回\n\t * @since 5.8.37\n\t */\n\tdefault String getPinyin(char c, boolean tone){\n\t\treturn getPinyin(c);\n\t}\n\n\t/**\n\t * 获取字符串对应的完整拼音，非中文返回原字符\n\t *\n\t * @param str 字符串\n\t * @param separator 拼音之间的分隔符\n\t * @return 拼音\n\t */\n\tString getPinyin(String str, String separator);\n\n\t/**\n\t * 获取字符串对应的完整拼音，非中文返回原字符\n\t *\n\t * @param str       字符串\n\t * @param separator 拼音之间的分隔符\n\t * @param tone 是否返回声调\n\t * @return 拼音\n\t * @since 5.8.37\n\t */\n\tdefault String getPinyin(String str, String separator,boolean tone){\n\t\treturn getPinyin(str, separator);\n\t}\n\n\n\t/**\n\t * 将输入字符串转为拼音首字母，其它字符原样返回\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tdefault char getFirstLetter(char c) {\n\t\treturn getPinyin(c).charAt(0);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音首字母，其它字符原样返回\n\t *\n\t * @param str 任意字符，汉字返回拼音，非汉字原样返回\n\t * @param separator 分隔符\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tdefault String getFirstLetter(String str, String separator) {\n\t\tfinal String splitSeparator = StrUtil.isEmpty(separator) ? \"#\" : separator;\n\t\tfinal List<String> split = StrUtil.split(getPinyin(str, splitSeparator), splitSeparator);\n\t\treturn CollUtil.join(split, separator, (s)->String.valueOf(s.length() > 0 ? s.charAt(0) : StrUtil.EMPTY));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/PinyinException.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 模板异常\n *\n * @author xiaoleilu\n */\npublic class PinyinException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic PinyinException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic PinyinException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic PinyinException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic PinyinException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic PinyinException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic PinyinException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/PinyinUtil.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.pinyin.engine.PinyinFactory;\n\n/**\n * 拼音工具类，封装了TinyPinyin、JPinyin、Pinyin4j，通过SPI自动识别。\n *\n * @author looly\n */\npublic class PinyinUtil {\n\n\tprivate static final String CHINESE_REGEX = \"[\\\\u4e00-\\\\u9fa5]\";\n\n\t/**\n\t * 获得全局单例的拼音引擎\n\t *\n\t * @return 全局单例的拼音引擎\n\t */\n\tpublic static PinyinEngine getEngine(){\n\t\treturn PinyinFactory.get();\n\t}\n\n\t/**\n\t * 如果c为汉字，则返回大写拼音；如果c不是汉字，则返回String.valueOf(c)\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(char c) {\n\t\treturn getEngine().getPinyin(c);\n\t}\n\n\t/**\n\t * 如果c为汉字，则返回大写拼音；如果c不是汉字，则返回String.valueOf(c)\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @param tone 是否返回声调\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(final char c, boolean tone) {\n\t\treturn getEngine().getPinyin(c,tone);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音，每个字之间的拼音使用空格分隔\n\t *\n\t * @param str 任意字符，汉字返回拼音，非汉字原样返回\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(String str) {\n\t\treturn getPinyin(str, StrUtil.SPACE);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音，每个字之间的拼音使用空格分隔\n\t *\n\t * @param str 任意字符，汉字返回拼音，非汉字原样返回\n\t * @param tone 是否返回声调\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(final String str, boolean tone) {\n\t\treturn getPinyin(str, StrUtil.SPACE, tone);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音，以字符为单位插入分隔符\n\t *\n\t * @param str       任意字符，汉字返回拼音，非汉字原样返回\n\t * @param separator 每个字拼音之间的分隔符\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(String str, String separator) {\n\t\treturn getEngine().getPinyin(str, separator);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音，以字符为单位插入分隔符\n\t *\n\t * @param str       任意字符，汉字返回拼音，非汉字原样返回\n\t * @param separator 每个字拼音之间的分隔符\n\t * @param tone 是否返回声调\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getPinyin(final String str, final String separator, boolean tone) {\n\t\treturn getEngine().getPinyin(str, separator, tone);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音首字母，其它字符原样返回\n\t *\n\t * @param c 任意字符，汉字返回拼音，非汉字原样返回\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static char getFirstLetter(char c) {\n\t\treturn getEngine().getFirstLetter(c);\n\t}\n\n\t/**\n\t * 将输入字符串转为拼音首字母，其它字符原样返回\n\t *\n\t * @param str       任意字符，汉字返回拼音，非汉字原样返回\n\t * @param separator 分隔符\n\t * @return 汉字返回拼音，非汉字原样返回\n\t */\n\tpublic static String getFirstLetter(String str, String separator) {\n\t\treturn (str == null) ? null : getEngine().getFirstLetter(str, separator);\n\t}\n\n\t/**\n\t * 是否为中文字符\n\t *\n\t * @param c 字符\n\t * @return 是否为中文字符\n\t */\n\tpublic static boolean isChinese(char c) {\n\t\treturn '〇' == c || String.valueOf(c).matches(CHINESE_REGEX);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/PinyinFactory.java",
    "content": "package cn.hutool.extra.pinyin.engine;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.ServiceLoaderUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.StaticLog;\n\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport cn.hutool.extra.pinyin.PinyinException;\n\n/**\n * 简单拼音引擎工厂，用于根据用户引入的拼音库jar，自动创建对应的拼音引擎对象\n *\n * @author looly\n */\npublic class PinyinFactory {\n\n\t/**\n\t * 获得单例的PinyinEngine\n\t *\n\t * @return 单例的PinyinEngine\n\t */\n\tpublic static PinyinEngine get(){\n\t\treturn Singleton.get(PinyinEngine.class.getName(), PinyinFactory::create);\n\t}\n\n\t/**\n\t * 根据用户引入的拼音引擎jar，自动创建对应的拼音引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @return {@link PinyinEngine}\n\t */\n\tpublic static PinyinEngine create() {\n\t\tfinal PinyinEngine engine = doCreate();\n\t\tStaticLog.debug(\"Use [{}] Engine As Default.\", StrUtil.removeSuffix(engine.getClass().getSimpleName(), \"Engine\"));\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 根据用户引入的拼音引擎jar，自动创建对应的拼音引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @return {@link PinyinEngine}\n\t */\n\tprivate static PinyinEngine doCreate() {\n\t\tfinal PinyinEngine engine = ServiceLoaderUtil.loadFirstAvailable(PinyinEngine.class);\n\t\tif(null != engine){\n\t\t\treturn engine;\n\t\t}\n\n\t\tthrow new PinyinException(\"No pinyin jar found ! Please add one of it to your project !\");\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/bopomofo4j/Bopomofo4jEngine.java",
    "content": "package cn.hutool.extra.pinyin.engine.bopomofo4j;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport com.rnkrsoft.bopomofo4j.Bopomofo4j;\nimport com.rnkrsoft.bopomofo4j.ToneType;\n\n/**\n * 封装了Bopomofo4j的引擎。\n *\n * <p>\n * Bopomofo4j封装，项目：https://gitee.com/rnkrsoft/Bopomofo4j。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.rnkrsoft.bopomofo4j&lt;/groupId&gt;\n *     &lt;artifactId&gt;bopomofo4j&lt;/artifactId&gt;\n *     &lt;version&gt;1.0.0&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n * @since 5.4.5\n */\npublic class Bopomofo4jEngine implements PinyinEngine {\n\n\tpublic Bopomofo4jEngine(){\n\t\tBopomofo4j.local();\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c) {\n\t\treturn Bopomofo4j.pinyin(String.valueOf(c), ToneType.WITHOUT_TONE, false, false, StrUtil.EMPTY);\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c, boolean tone) {\n\t\tif (tone) {\n\t\t\treturn Bopomofo4j.pinyin(String.valueOf(c), ToneType.WITH_VOWEL_TONE, false, false, StrUtil.EMPTY);\n\t\t}else{\n\t\t\treturn getPinyin(c);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator) {\n\t\treturn Bopomofo4j.pinyin(str, ToneType.WITHOUT_TONE, false, false, separator);\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator, boolean tone) {\n\t\tif (tone) {\n\t\t\treturn Bopomofo4j.pinyin(str, ToneType.WITH_VOWEL_TONE, false, false, separator);\n\t\t}else{\n\t\t\treturn getPinyin(str, separator);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/bopomofo4j/package-info.java",
    "content": "/**\n * 封装了Bopomofo4j的引擎。\n *\n * <p>\n * Bopomofo4j封装，项目：https://gitee.com/rnkrsoft/Bopomofo4j。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.rnkrsoft.bopomofo4j&lt;/groupId&gt;\n *     &lt;artifactId&gt;bopomofo4j&lt;/artifactId&gt;\n *     &lt;version&gt;1.0.0&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.extra.pinyin.engine.bopomofo4j;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/houbbpinyin/HoubbPinyinEngine.java",
    "content": "package cn.hutool.extra.pinyin.engine.houbbpinyin;\n\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport com.github.houbb.pinyin.constant.enums.PinyinStyleEnum;\nimport com.github.houbb.pinyin.util.PinyinHelper;\n\n/**\n * 封装了 houbb Pinyin 的引擎。\n *\n * <p>\n * houbb pinyin(https://github.com/houbb/pinyin)封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.github.houbb&lt;/groupId&gt;\n *     &lt;artifactId&gt;pinyin&lt;/artifactId&gt;\n *     &lt;version&gt;0.2.0&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npublic class HoubbPinyinEngine implements PinyinEngine {\n\n\t/**\n\t * 汉字拼音输出的格式\n\t */\n\tPinyinStyleEnum format;\n\n\t/**\n\t * 构造\n\t */\n\tpublic HoubbPinyinEngine() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param format 格式\n\t */\n\tpublic HoubbPinyinEngine(PinyinStyleEnum format) {\n\t\tinit(format);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param format 格式\n\t */\n\tpublic void init(PinyinStyleEnum format) {\n\t\tif (null == format) {\n\t\t\tformat = PinyinStyleEnum.NORMAL;\n\t\t}\n\t\tthis.format = format;\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c) {\n\t\tString result;\n\t\tresult = PinyinHelper.toPinyin(String.valueOf(c), format);\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c, boolean tone) {\n\t\tif (tone){\n\t\t\treturn  PinyinHelper.toPinyin(String.valueOf(c), PinyinStyleEnum.DEFAULT);\n\t\t}else {\n\t\t\treturn  getPinyin(c);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator) {\n\t\tString result;\n\t\tresult = PinyinHelper.toPinyin(str, format, separator);\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator, boolean tone) {\n\t\t\tif(tone){\n\t\t\t\treturn PinyinHelper.toPinyin(str, PinyinStyleEnum.DEFAULT, separator);\n\t\t\t}else {\n\t\t\t\treturn getPinyin(str,separator);\n\t\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/houbbpinyin/package-info.java",
    "content": "/**\n * 封装了 Pinyin 的引擎。\n *\n * <p>\n * pinyin(https://github.com/houbb/pinyin)封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.github.houbb&lt;/groupId&gt;\n *     &lt;artifactId&gt;pinyin&lt;/artifactId&gt;\n *     &lt;version&gt;0.2.0&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.extra.pinyin.engine.houbbpinyin;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/jpinyin/JPinyinEngine.java",
    "content": "package cn.hutool.extra.pinyin.engine.jpinyin;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport com.github.stuxuhai.jpinyin.PinyinException;\nimport com.github.stuxuhai.jpinyin.PinyinFormat;\nimport com.github.stuxuhai.jpinyin.PinyinHelper;\n\n/**\n * 封装了Jpinyin的引擎。\n *\n * <p>\n * jpinyin（github库作者已删除）封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.github.stuxuhai&lt;/groupId&gt;\n *     &lt;artifactId&gt;jpinyin&lt;/artifactId&gt;\n *     &lt;version&gt;1.1.8&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npublic class JPinyinEngine implements PinyinEngine {\n\n\t//设置汉字拼音输出的格式\n\tPinyinFormat format;\n\n\tpublic JPinyinEngine(){\n\t\tthis(null);\n\t}\n\n\tpublic JPinyinEngine(PinyinFormat format){\n\t\tinit(format);\n\t}\n\n\tpublic void init(PinyinFormat format){\n\t\tif(null == format){\n\t\t\t// 不加声调\n\t\t\tformat = PinyinFormat.WITHOUT_TONE;\n\t\t}\n\t\tthis.format = format;\n\t}\n\n\n\t@Override\n\tpublic String getPinyin(char c) {\n\t\tString[] results = PinyinHelper.convertToPinyinArray(c, format);\n\t\treturn ArrayUtil.isEmpty(results) ? String.valueOf(c) : results[0];\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c, boolean tone) {\n\t\tif (tone) {\n\t\t\tString[] results = PinyinHelper.convertToPinyinArray(c, PinyinFormat.WITH_TONE_MARK);\n\t\t\treturn ArrayUtil.isEmpty(results) ? String.valueOf(c) : results[0];\n\t\t}else {\n\t\t\treturn getPinyin(c);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator) {\n\t\ttry {\n\t\t\treturn PinyinHelper.convertToPinyinString(str, separator, format);\n\t\t} catch (PinyinException e) {\n\t\t\tthrow new cn.hutool.extra.pinyin.PinyinException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator, boolean tone) {\n\t\tif (tone) {\n\t\t\ttry {\n\t\t\t\treturn PinyinHelper.convertToPinyinString(str, separator, PinyinFormat.WITH_TONE_MARK);\n\t\t\t} catch (PinyinException e) {\n\t\t\t\tthrow new cn.hutool.extra.pinyin.PinyinException(e);\n\t\t\t}\n\t\t}else {\n\t\t\treturn getPinyin(str, separator);\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/jpinyin/package-info.java",
    "content": "/**\n * 封装了Jpinyin的引擎。\n *\n * <p>\n * jpinyin（github库作者已删除）封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.github.stuxuhai&lt;/groupId&gt;\n *     &lt;artifactId&gt;jpinyin&lt;/artifactId&gt;\n *     &lt;version&gt;1.1.8&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.extra.pinyin.engine.jpinyin;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/package-info.java",
    "content": "/**\n * 拼音具体实现\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.pinyin.engine;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/pinyin4j/Pinyin4jEngine.java",
    "content": "package cn.hutool.extra.pinyin.engine.pinyin4j;\n\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport cn.hutool.extra.pinyin.PinyinException;\nimport net.sourceforge.pinyin4j.PinyinHelper;\nimport net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;\nimport net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;\nimport net.sourceforge.pinyin4j.format.HanyuPinyinToneType;\nimport net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;\nimport net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;\n\n/**\n * 封装了Pinyin4j的引擎。\n *\n * <p>\n * pinyin4j(http://sourceforge.net/projects/pinyin4j)封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.belerweb&lt;/groupId&gt;\n *     &lt;artifactId&gt;pinyin4j&lt;/artifactId&gt;\n *     &lt;version&gt;2.5.1&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npublic class Pinyin4jEngine implements PinyinEngine {\n\n\t//设置汉字拼音输出的格式\n\tHanyuPinyinOutputFormat format;\n\n\t/**\n\t * 构造\n\t */\n\tpublic Pinyin4jEngine() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param format 格式\n\t */\n\tpublic Pinyin4jEngine(HanyuPinyinOutputFormat format) {\n\t\tinit(format);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param format 格式\n\t */\n\tpublic void init(HanyuPinyinOutputFormat format) {\n\t\tif (null == format) {\n\t\t\tformat = new HanyuPinyinOutputFormat();\n\t\t\t// 小写\n\t\t\tformat.setCaseType(HanyuPinyinCaseType.LOWERCASE);\n\t\t\t// 不加声调\n\t\t\tformat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);\n\t\t\t// 'ü' 使用 \"v\" 代替\n\t\t\tformat.setVCharType(HanyuPinyinVCharType.WITH_V);\n\t\t}\n\t\tthis.format = format;\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c) {\n\t\tString result;\n\t\ttry {\n\t\t\tString[] results = PinyinHelper.toHanyuPinyinStringArray(c, format);\n\t\t\tresult = ArrayUtil.isEmpty(results) ? String.valueOf(c) : results[0];\n\t\t} catch (BadHanyuPinyinOutputFormatCombination e) {\n\t\t\tresult = String.valueOf(c);\n\t\t}\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c, boolean tone) {\n\t\tString result;\n\t\tif(tone){\n\t\t\ttry {\n\t\t\t\t//增加声调\n\t\t\t\tHanyuPinyinOutputFormat formatTemp = new HanyuPinyinOutputFormat();\n\t\t\t\t// 小写\n\t\t\t\tformatTemp.setCaseType(HanyuPinyinCaseType.LOWERCASE);\n\t\t\t\t// 加声调\n\t\t\t\tformatTemp.setToneType(HanyuPinyinToneType.WITH_TONE_MARK);\n\t\t\t\t//\n\t\t\t\tformatTemp.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);\n\t\t\t\tString[] results = PinyinHelper.toHanyuPinyinStringArray(c, formatTemp);\n\t\t\t\tresult = ArrayUtil.isEmpty(results) ? String.valueOf(c) : results[0];\n\t\t\t} catch (BadHanyuPinyinOutputFormatCombination e) {\n\t\t\t\tresult = String.valueOf(c);\n\t\t\t}\n\t\t}else {\n\t\t\tresult = getPinyin(c);\n\t\t}\n\t\treturn result;\n\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator) {\n\t\tfinal StrBuilder result = StrUtil.strBuilder();\n\t\tboolean isFirst = true;\n\t\tfinal int strLen = str.length();\n\t\ttry {\n\t\t\tfor(int i = 0; i < strLen; i++){\n\t\t\t\tif(isFirst){\n\t\t\t\t\tisFirst = false;\n\t\t\t\t} else{\n\t\t\t\t\tresult.append(separator);\n\t\t\t\t}\n\t\t\t\tfinal String[] pinyinStringArray = PinyinHelper.toHanyuPinyinStringArray(str.charAt(i), format);\n\t\t\t\tif(ArrayUtil.isEmpty(pinyinStringArray)){\n\t\t\t\t\tresult.append(str.charAt(i));\n\t\t\t\t} else{\n\t\t\t\t\tresult.append(pinyinStringArray[0]);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (BadHanyuPinyinOutputFormatCombination e) {\n\t\t\tthrow new PinyinException(e);\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator, boolean tone) {\n\t\tif(tone){\n\t\t\tfinal StrBuilder result = StrUtil.strBuilder();\n\t\t\tboolean isFirst = true;\n\t\t\tfinal int strLen = str.length();\n\t\t\ttry {\n\t\t\t\tfor(int i = 0; i < strLen; i++){\n\t\t\t\t\tif(isFirst){\n\t\t\t\t\t\tisFirst = false;\n\t\t\t\t\t} else{\n\t\t\t\t\t\tresult.append(separator);\n\t\t\t\t\t}\n\t\t\t\t\t//增加声调\n\t\t\t\t\tHanyuPinyinOutputFormat formatTemp = new HanyuPinyinOutputFormat();\n\t\t\t\t\t// 小写\n\t\t\t\t\tformatTemp.setCaseType(HanyuPinyinCaseType.LOWERCASE);\n\t\t\t\t\t// 加声调\n\t\t\t\t\tformatTemp.setToneType(HanyuPinyinToneType.WITH_TONE_MARK);\n\t\t\t\t\t//\n\t\t\t\t\tformatTemp.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);\n\t\t\t\t\tfinal String[] pinyinStringArray = PinyinHelper.toHanyuPinyinStringArray(str.charAt(i), formatTemp);\n\t\t\t\t\tif(ArrayUtil.isEmpty(pinyinStringArray)){\n\t\t\t\t\t\tresult.append(str.charAt(i));\n\t\t\t\t\t} else{\n\t\t\t\t\t\tresult.append(pinyinStringArray[0]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (BadHanyuPinyinOutputFormatCombination e) {\n\t\t\t\tthrow new PinyinException(e);\n\t\t\t}\n\t\t\treturn result.toString();\n\t\t}else {\n\t\t\treturn getPinyin(str, separator);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/pinyin4j/package-info.java",
    "content": "/**\n * 封装了Pinyin4j的引擎。\n *\n * <p>\n * pinyin4j(http://sourceforge.net/projects/pinyin4j)封装。\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;com.belerweb&lt;/groupId&gt;\n *     &lt;artifactId&gt;pinyin4j&lt;/artifactId&gt;\n *     &lt;version&gt;2.5.1&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.extra.pinyin.engine.pinyin4j;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/tinypinyin/TinyPinyinEngine.java",
    "content": "package cn.hutool.extra.pinyin.engine.tinypinyin;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.pinyin.PinyinEngine;\nimport com.github.promeg.pinyinhelper.Pinyin;\n\n/**\n * 封装了TinyPinyin的引擎。\n *\n * <p>\n * TinyPinyin(https://github.com/promeG/TinyPinyin)提供者未提交Maven中央库，<br>\n * 因此使用\n * https://github.com/biezhi/TinyPinyin打包的版本\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;io.github.biezhi&lt;/groupId&gt;\n *     &lt;artifactId&gt;TinyPinyin&lt;/artifactId&gt;\n *     &lt;version&gt;2.0.3.RELEASE&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npublic class TinyPinyinEngine implements PinyinEngine {\n\n\t/**\n\t * 构造\n\t */\n\tpublic TinyPinyinEngine(){\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造\n\t * @param config 配置\n\t */\n\tpublic TinyPinyinEngine(Pinyin.Config config){\n\t\tPinyin.init(config);\n\t}\n\n\t@Override\n\tpublic String getPinyin(char c) {\n\t\tif(false == Pinyin.isChinese(c)){\n\t\t\treturn String.valueOf(c);\n\t\t}\n\t\treturn Pinyin.toPinyin(c).toLowerCase();\n\t}\n\n\t@Override\n\tpublic String getPinyin(String str, String separator) {\n\t\tfinal String pinyin = Pinyin.toPinyin(str, separator);\n\t\treturn StrUtil.isEmpty(pinyin) ? pinyin : pinyin.toLowerCase();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/engine/tinypinyin/package-info.java",
    "content": "/**\n * 封装了TinyPinyin的引擎。\n *\n * <p>\n * TinyPinyin(https://github.com/promeG/TinyPinyin)提供者未提交Maven中央库，<br>\n * 因此使用\n * https://github.com/biezhi/TinyPinyin打包的版本\n * </p>\n *\n * <p>\n * 引入：\n * <pre>\n * &lt;dependency&gt;\n *     &lt;groupId&gt;io.github.biezhi&lt;/groupId&gt;\n *     &lt;artifactId&gt;TinyPinyin&lt;/artifactId&gt;\n *     &lt;version&gt;2.0.3.RELEASE&lt;/version&gt;\n * &lt;/dependency&gt;\n * </pre>\n *\n * @author looly\n */\npackage cn.hutool.extra.pinyin.engine.tinypinyin;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/pinyin/package-info.java",
    "content": "/**\n * 拼音工具封装，入口为PinyinUtil\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.pinyin;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/qrcode/BufferedImageLuminanceSource.java",
    "content": "package cn.hutool.extra.qrcode;\n\nimport com.google.zxing.LuminanceSource;\n\nimport java.awt.Graphics2D;\nimport java.awt.geom.AffineTransform;\nimport java.awt.image.BufferedImage;\n\n/**\n * {@link BufferedImage} 图片二维码源<br>\n * 来自：http://blog.csdn.net/yangxin_blog/article/details/50850701<br>\n * 此类同样在zxing-j2se包中也有提供\n *\n * @author zxing, Looly\n * @since 4.0.2\n */\npublic final class BufferedImageLuminanceSource extends LuminanceSource {\n\n\tprivate final BufferedImage image;\n\tprivate final int left;\n\tprivate final int top;\n\n\t/**\n\t * 构造\n\t *\n\t * @param image {@link BufferedImage}\n\t */\n\tpublic BufferedImageLuminanceSource(BufferedImage image) {\n\t\tthis(image, 0, 0, image.getWidth(), image.getHeight());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param image {@link BufferedImage}\n\t * @param left 左边间隔\n\t * @param top 顶部间隔\n\t * @param width 宽度\n\t * @param height 高度\n\t */\n\tpublic BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) {\n\t\tsuper(width, height);\n\n\t\tint sourceWidth = image.getWidth();\n\t\tint sourceHeight = image.getHeight();\n\t\tif (left + width > sourceWidth || top + height > sourceHeight) {\n\t\t\tthrow new IllegalArgumentException(\"Crop rectangle does not fit within image data.\");\n\t\t}\n\n\t\tfor (int y = top; y < top + height; y++) {\n\t\t\tfor (int x = left; x < left + width; x++) {\n\t\t\t\tif ((image.getRGB(x, y) & 0xFF000000) == 0) {\n\t\t\t\t\timage.setRGB(x, y, 0xFFFFFFFF); // = white\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);\n\t\tthis.image.getGraphics().drawImage(image, 0, 0, null);\n\t\tthis.left = left;\n\t\tthis.top = top;\n\t}\n\n\t@Override\n\tpublic byte[] getRow(int y, byte[] row) {\n\t\tif (y < 0 || y >= getHeight()) {\n\t\t\tthrow new IllegalArgumentException(\"Requested row is outside the image: \" + y);\n\t\t}\n\t\tint width = getWidth();\n\t\tif (row == null || row.length < width) {\n\t\t\trow = new byte[width];\n\t\t}\n\t\timage.getRaster().getDataElements(left, top + y, width, 1, row);\n\t\treturn row;\n\t}\n\n\t@Override\n\tpublic byte[] getMatrix() {\n\t\tint width = getWidth();\n\t\tint height = getHeight();\n\t\tint area = width * height;\n\t\tbyte[] matrix = new byte[area];\n\t\timage.getRaster().getDataElements(left, top, width, height, matrix);\n\t\treturn matrix;\n\t}\n\n\t@Override\n\tpublic boolean isCropSupported() {\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic LuminanceSource crop(int left, int top, int width, int height) {\n\t\treturn new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);\n\t}\n\n\t@Override\n\tpublic boolean isRotateSupported() {\n\t\treturn true;\n\t}\n\n\t@SuppressWarnings(\"SuspiciousNameCombination\")\n\t@Override\n\tpublic LuminanceSource rotateCounterClockwise() {\n\n\t\tint sourceWidth = image.getWidth();\n\t\tint sourceHeight = image.getHeight();\n\n\t\tAffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);\n\n\t\tBufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);\n\n\t\tGraphics2D g = rotatedImage.createGraphics();\n\t\tg.drawImage(image, transform, null);\n\t\tg.dispose();\n\n\t\tint width = getWidth();\n\t\treturn new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeException.java",
    "content": "package cn.hutool.extra.qrcode;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Qrcode异常\n *\n * @author xiaoleilu\n */\npublic class QrCodeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic QrCodeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic QrCodeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic QrCodeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic QrCodeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic QrCodeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic QrCodeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java",
    "content": "package cn.hutool.extra.qrcode;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.img.Img;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.ansi.AnsiColors;\nimport cn.hutool.core.lang.ansi.AnsiElement;\nimport cn.hutool.core.lang.ansi.AnsiEncoder;\nimport cn.hutool.core.lang.ansi.ForeOrBack;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport com.google.zxing.*;\nimport com.google.zxing.common.BitMatrix;\nimport com.google.zxing.common.GlobalHistogramBinarizer;\nimport com.google.zxing.common.HybridBinarizer;\n\nimport java.awt.Color;\nimport java.awt.Image;\nimport java.awt.Rectangle;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 基于Zxing的二维码工具类，支持：\n * <ul>\n *     <li>二维码生成和识别，见{@link BarcodeFormat#QR_CODE}</li>\n *     <li>条形码生成和识别，见{@link BarcodeFormat#CODE_39}等很多标准格式</li>\n * </ul>\n *\n * @author looly\n * @since 4.0.2\n */\npublic class QrCodeUtil {\n\n\tpublic static final String QR_TYPE_SVG = \"svg\";// SVG矢量图格式\n\tpublic static final String QR_TYPE_TXT = \"txt\";// Ascii Art字符画文本\n\tprivate static final AnsiColors ansiColors = new AnsiColors(AnsiColors.BitDepth.EIGHT);\n\n\t/**\n\t * 生成代 logo 图片的 Base64 编码格式的二维码，以 String 形式表示\n\t *\n\t * @param content    内容\n\t * @param qrConfig   二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @param logoBase64 logo 图片的 base64 编码\n\t * @return 图片 Base64 编码字符串\n\t */\n\tpublic static String generateAsBase64(String content, QrConfig qrConfig, String targetType, String logoBase64) {\n\t\treturn generateAsBase64(content, qrConfig, targetType, Base64.decode(logoBase64));\n\t}\n\n\t/**\n\t * 生成代 logo 图片的 Base64 编码格式的二维码，以 String 形式表示\n\t *\n\t * @param content    内容\n\t * @param qrConfig   二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @param logo       logo 图片的byte[]\n\t * @return 图片 Base64 编码字符串\n\t */\n\tpublic static String generateAsBase64(String content, QrConfig qrConfig, String targetType, byte[] logo) {\n\t\treturn generateAsBase64(content, qrConfig, targetType, ImgUtil.toImage(logo));\n\t}\n\n\t/**\n\t * 生成代 logo 图片的 Base64 编码格式的二维码，以 String 形式表示\n\t *\n\t * @param content    内容\n\t * @param qrConfig   二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @param logo       logo 图片的byte[]\n\t * @return 图片 Base64 编码字符串\n\t */\n\tpublic static String generateAsBase64(String content, QrConfig qrConfig, String targetType, Image logo) {\n\t\tqrConfig.setImg(logo);\n\t\treturn generateAsBase64(content, qrConfig, targetType);\n\t}\n\n\t/**\n\t * 生成 Base64 编码格式的二维码，以 String 形式表示\n\t *\n\t * <p>\n\t * 输出格式为: data:image/[type];base64,[data]\n\t * </p>\n\t *\n\t * @param content    内容\n\t * @param qrConfig   二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @return 图片 Base64 编码字符串\n\t */\n\tpublic static String generateAsBase64(String content, QrConfig qrConfig, String targetType) {\n\t\tString result;\n\t\tswitch (targetType) {\n\t\t\tcase QR_TYPE_SVG:\n\t\t\t\tString svg = generateAsSvg(content, qrConfig);\n\t\t\t\tresult = svgToBase64(svg);\n\t\t\t\tbreak;\n\t\t\tcase QR_TYPE_TXT:\n\t\t\t\tString txt = generateAsAsciiArt(content, qrConfig);\n\t\t\t\tresult = txtToBase64(txt);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfinal BufferedImage img = generate(content, qrConfig);\n\t\t\t\tresult = ImgUtil.toBase64DataUri(img, targetType);\n\t\t\t\tbreak;\n\t\t}\n\n\n\t\treturn result;\n\t}\n\n\tprivate static String txtToBase64(String txt) {\n\t\treturn URLUtil.getDataUri(\"text/plain\", \"base64\", Base64.encode(txt));\n\t}\n\n\tprivate static String svgToBase64(String svg) {\n\t\treturn URLUtil.getDataUri(\"image/svg+xml\", \"base64\", Base64.encode(svg));\n\t}\n\n\t/**\n\t * @param content  内容\n\t * @param qrConfig 二维码配置，包括宽度、高度、边距、颜色等\n\t * @return SVG矢量图（字符串）\n\t * @since 5.8.6\n\t */\n\tpublic static String generateAsSvg(String content, QrConfig qrConfig) {\n\t\tfinal BitMatrix bitMatrix = encode(content, qrConfig);\n\t\treturn toSVG(bitMatrix, qrConfig);\n\t}\n\n\t/**\n\t * 生成ASCII Art字符画形式的二维码\n\t *\n\t * @param content 内容\n\t * @return ASCII Art字符画形式的二维码字符串\n\t * @since 5.8.6\n\t */\n\tpublic static String generateAsAsciiArt(String content) {\n\t\treturn generateAsAsciiArt(content, 0, 0, 1);\n\t}\n\n\t/**\n\t * 生成ASCII Art字符画形式的二维码\n\t *\n\t * @param content  内容\n\t * @param qrConfig 二维码配置，仅宽度、高度、边距配置有效\n\t * @return ASCII Art字符画形式的二维码\n\t * @since 5.8.6\n\t */\n\tpublic static String generateAsAsciiArt(String content, QrConfig qrConfig) {\n\t\tfinal BitMatrix bitMatrix = encode(content, qrConfig);\n\t\treturn toAsciiArt(bitMatrix, qrConfig);\n\t}\n\n\t/**\n\t * @param content 内容\n\t * @param width   宽度（单位：字符▄的大小）\n\t * @param height  高度（单位：字符▄的大小）\n\t * @param margin  边距大小（1~4）\n\t * @return ASCII Art字符画形式的二维码\n\t * @since 5.8.6\n\t */\n\tpublic static String generateAsAsciiArt(String content, int width, int height, int margin) {\n\t\tQrConfig qrConfig = new QrConfig(width, height).setMargin(margin);\n\t\treturn generateAsAsciiArt(content, qrConfig);\n\t}\n\n\n\t/**\n\t * 生成PNG格式的二维码图片，以byte[]形式表示\n\t *\n\t * @param content 内容\n\t * @param width   宽度（单位：像素）\n\t * @param height  高度（单位：像素）\n\t * @return 图片的byte[]\n\t * @since 4.0.10\n\t */\n\tpublic static byte[] generatePng(String content, int width, int height) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\tgenerate(content, width, height, ImgUtil.IMAGE_TYPE_PNG, out);\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 生成PNG格式的二维码图片，以byte[]形式表示\n\t *\n\t * @param content 内容\n\t * @param config  二维码配置，包括宽度、高度、边距、颜色等\n\t * @return 图片的byte[]\n\t * @since 4.1.2\n\t */\n\tpublic static byte[] generatePng(String content, QrConfig config) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\tgenerate(content, config, ImgUtil.IMAGE_TYPE_PNG, out);\n\t\treturn out.toByteArray();\n\t}\n\n\t/**\n\t * 生成二维码到文件，二维码图片格式取决于文件的扩展名\n\t *\n\t * @param content    文本内容\n\t * @param width      宽度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param height     高度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param targetFile 目标文件，扩展名决定输出格式\n\t * @return 目标文件\n\t */\n\tpublic static File generate(String content, int width, int height, File targetFile) {\n\t\tString extName = FileUtil.extName(targetFile);\n\t\tswitch (extName) {\n\t\t\tcase QR_TYPE_SVG:\n\t\t\t\tString svg = generateAsSvg(content, new QrConfig(width, height));\n\t\t\t\tFileUtil.writeString(svg, targetFile, StandardCharsets.UTF_8);\n\t\t\t\tbreak;\n\t\t\tcase QR_TYPE_TXT:\n\t\t\t\tString txt = generateAsAsciiArt(content, new QrConfig(width, height));\n\t\t\t\tFileUtil.writeString(txt, targetFile, StandardCharsets.UTF_8);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfinal BufferedImage image = generate(content, width, height);\n\t\t\t\tImgUtil.write(image, targetFile);\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn targetFile;\n\t}\n\n\t/**\n\t * 生成二维码到文件，二维码图片格式取决于文件的扩展名\n\t *\n\t * @param content    文本内容\n\t * @param config     二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetFile 目标文件，扩展名决定输出格式\n\t * @return 目标文件\n\t * @since 4.1.2\n\t */\n\tpublic static File generate(String content, QrConfig config, File targetFile) {\n\t\tString extName = FileUtil.extName(targetFile);\n\t\tswitch (extName) {\n\t\t\tcase QR_TYPE_SVG:\n\t\t\t\tfinal String svg = generateAsSvg(content, config);\n\t\t\t\tFileUtil.writeString(svg, targetFile, StandardCharsets.UTF_8);\n\t\t\t\tbreak;\n\t\t\tcase QR_TYPE_TXT:\n\t\t\t\tfinal String txt = generateAsAsciiArt(content, config);\n\t\t\t\tFileUtil.writeString(txt, targetFile, StandardCharsets.UTF_8);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfinal BufferedImage image = generate(content, config);\n\t\t\t\tImgUtil.write(image, targetFile);\n\t\t\t\tbreak;\n\t\t}\n\t\treturn targetFile;\n\t}\n\n\t/**\n\t * 生成二维码到输出流\n\t *\n\t * @param content    文本内容\n\t * @param width      宽度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param height     高度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @param out        目标流\n\t */\n\tpublic static void generate(String content, int width, int height, String targetType, OutputStream out) {\n\t\tswitch (targetType) {\n\t\t\tcase QR_TYPE_SVG:\n\t\t\t\tfinal String svg = generateAsSvg(content, new QrConfig(width, height));\n\t\t\t\tIoUtil.writeUtf8(out, false, svg);\n\t\t\t\tbreak;\n\t\t\tcase QR_TYPE_TXT:\n\t\t\t\tfinal String txt = generateAsAsciiArt(content, new QrConfig(width, height));\n\t\t\t\tIoUtil.writeUtf8(out, false, txt);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfinal BufferedImage image = generate(content, width, height);\n\t\t\t\tImgUtil.write(image, targetType, out);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * 生成二维码到输出流\n\t *\n\t * @param content    文本内容\n\t * @param config     二维码配置，包括宽度、高度、边距、颜色等\n\t * @param targetType 类型（图片扩展名），见{@link #QR_TYPE_SVG}、 {@link #QR_TYPE_TXT}、{@link ImgUtil}\n\t * @param out        目标流\n\t * @since 4.1.2\n\t */\n\tpublic static void generate(String content, QrConfig config, String targetType, OutputStream out) {\n\t\tswitch (targetType) {\n\t\t\tcase QR_TYPE_SVG:\n\t\t\t\tfinal String svg = generateAsSvg(content, config);\n\t\t\t\tIoUtil.writeUtf8(out, false, svg);\n\t\t\t\tbreak;\n\t\t\tcase QR_TYPE_TXT:\n\t\t\t\tfinal String txt = generateAsAsciiArt(content, config);\n\t\t\t\tIoUtil.writeUtf8(out, false, txt);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfinal BufferedImage image = generate(content, config);\n\t\t\t\tImgUtil.write(image, targetType, out);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * 生成二维码图片\n\t *\n\t * @param content 文本内容\n\t * @param width   宽度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param height  高度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @return 二维码图片（黑白）\n\t */\n\tpublic static BufferedImage generate(String content, int width, int height) {\n\t\treturn generate(content, new QrConfig(width, height));\n\t}\n\n\t/**\n\t * 生成二维码或条形码图片\n\t *\n\t * @param content 文本内容\n\t * @param format  格式，可选二维码或者条形码\n\t * @param width   宽度（单位：像素）\n\t * @param height  高度（单位：像素）\n\t * @return 二维码图片（黑白）\n\t */\n\tpublic static BufferedImage generate(String content, BarcodeFormat format, int width, int height) {\n\t\treturn generate(content, format, new QrConfig(width, height));\n\t}\n\n\t/**\n\t * 生成二维码图片\n\t *\n\t * @param content 文本内容\n\t * @param config  二维码配置，包括宽度、高度、边距、颜色等\n\t * @return 二维码图片（黑白）\n\t * @since 4.1.2\n\t */\n\tpublic static BufferedImage generate(String content, QrConfig config) {\n\t\treturn generate(content, BarcodeFormat.QR_CODE, config);\n\t}\n\n\t/**\n\t * 生成二维码或条形码图片<br>\n\t * 只有二维码时QrConfig中的图片才有效\n\t *\n\t * @param content 文本内容\n\t * @param format  格式，可选二维码、条形码等\n\t * @param config  二维码配置，包括宽度、高度、边距、颜色等\n\t * @return 二维码图片（黑白）\n\t * @since 4.1.14\n\t */\n\tpublic static BufferedImage generate(String content, BarcodeFormat format, QrConfig config) {\n\t\tfinal BitMatrix bitMatrix = encode(content, format, config);\n\t\tfinal BufferedImage image = toImage(bitMatrix, config.foreColor != null ? config.foreColor : Color.BLACK.getRGB(), config.backColor);\n\t\tfinal Image logoImg = config.img;\n\t\tif (null != logoImg && BarcodeFormat.QR_CODE == format) {\n\t\t\t// 只有二维码可以贴图\n\t\t\tfinal int qrWidth = image.getWidth();\n\t\t\tfinal int qrHeight = image.getHeight();\n\t\t\tint width;\n\t\t\tint height;\n\t\t\t// 按照最短的边做比例缩放\n\t\t\tif (qrWidth < qrHeight) {\n\t\t\t\twidth = qrWidth / config.ratio;\n\t\t\t\theight = logoImg.getHeight(null) * width / logoImg.getWidth(null);\n\t\t\t} else {\n\t\t\t\theight = qrHeight / config.ratio;\n\t\t\t\twidth = logoImg.getWidth(null) * height / logoImg.getHeight(null);\n\t\t\t}\n\n\t\t\tImg.from(image).pressImage(//\n\t\t\t\t\tImg.from(logoImg).round(config.round).getImg(), // 圆角\n\t\t\t\t\tnew Rectangle(width, height), //\n\t\t\t\t\t1//\n\t\t\t);\n\t\t}\n\t\treturn image;\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------- encode\n\n\t/**\n\t * 将文本内容编码为二维码\n\t *\n\t * @param content 文本内容\n\t * @param width   宽度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param height  高度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @return {@link BitMatrix}\n\t */\n\tpublic static BitMatrix encode(String content, int width, int height) {\n\t\treturn encode(content, BarcodeFormat.QR_CODE, width, height);\n\t}\n\n\t/**\n\t * 将文本内容编码为二维码\n\t *\n\t * @param content 文本内容\n\t * @param config  二维码配置，包括宽度、高度、边距、颜色等\n\t * @return {@link BitMatrix}\n\t * @since 4.1.2\n\t */\n\tpublic static BitMatrix encode(String content, QrConfig config) {\n\t\treturn encode(content, BarcodeFormat.QR_CODE, config);\n\t}\n\n\t/**\n\t * 将文本内容编码为条形码或二维码\n\t *\n\t * @param content 文本内容\n\t * @param format  格式枚举\n\t * @param width   宽度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @param height  高度（单位：类型为一般图片或SVG时，单位是像素，类型为 Ascii Art 字符画时，单位是字符▄或▀的大小）\n\t * @return {@link BitMatrix}\n\t */\n\tpublic static BitMatrix encode(String content, BarcodeFormat format, int width, int height) {\n\t\treturn encode(content, format, new QrConfig(width, height));\n\t}\n\n\t/**\n\t * 将文本内容编码为条形码或二维码\n\t *\n\t * @param content 文本内容\n\t * @param format  格式枚举\n\t * @param config  二维码配置，包括宽度、高度、边距、颜色等\n\t * @return {@link BitMatrix}\n\t * @since 4.1.2\n\t */\n\tpublic static BitMatrix encode(String content, BarcodeFormat format, QrConfig config) {\n\t\tfinal MultiFormatWriter multiFormatWriter = new MultiFormatWriter();\n\t\tif (null == config) {\n\t\t\t// 默认配置\n\t\t\tconfig = new QrConfig();\n\t\t}\n\n\t\tBitMatrix bitMatrix;\n\t\ttry {\n\t\t\tbitMatrix = multiFormatWriter.encode(content, format, config.width, config.height, config.toHints(format));\n\t\t} catch (WriterException e) {\n\t\t\tthrow new QrCodeException(e);\n\t\t}\n\n\t\treturn bitMatrix;\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------------------- decode\n\n\t/**\n\t * 解码二维码或条形码图片为文本\n\t *\n\t * @param qrCodeInputStream 二维码输入流\n\t * @return 解码文本\n\t */\n\tpublic static String decode(InputStream qrCodeInputStream) {\n\t\tBufferedImage image = null;\n\t\ttry{\n\t\t\timage = ImgUtil.read(qrCodeInputStream);\n\t\t\treturn decode(image);\n\t\t} finally {\n\t\t\tImgUtil.flush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 解码二维码或条形码图片为文本\n\t *\n\t * @param qrCodeFile 二维码文件\n\t * @return 解码文本\n\t */\n\tpublic static String decode(File qrCodeFile) {\n\t\tBufferedImage image = null;\n\t\ttry{\n\t\t\timage = ImgUtil.read(qrCodeFile);\n\t\t\treturn decode(image);\n\t\t} finally {\n\t\t\tImgUtil.flush(image);\n\t\t}\n\t}\n\n\t/**\n\t * 将二维码或条形码图片解码为文本\n\t *\n\t * @param image {@link Image} 二维码图片\n\t * @return 解码后的文本\n\t */\n\tpublic static String decode(Image image) {\n\t\treturn decode(image, true, false);\n\t}\n\n\t/**\n\t * 将二维码或条形码图片解码为文本<br>\n\t * 此方法会尝试使用{@link HybridBinarizer}和{@link GlobalHistogramBinarizer}两种模式解析<br>\n\t * 需要注意部分二维码如果不带logo，使用PureBarcode模式会解析失败，此时须设置此选项为false。\n\t *\n\t * @param image         {@link Image} 二维码图片\n\t * @param isTryHarder   是否优化精度\n\t * @param isPureBarcode 是否使用复杂模式，扫描带logo的二维码设为true\n\t * @return 解码后的文本\n\t * @since 4.3.1\n\t */\n\tpublic static String decode(Image image, boolean isTryHarder, boolean isPureBarcode) {\n\t\treturn decode(image, buildHints(isTryHarder, isPureBarcode));\n\t}\n\n\t/**\n\t * 将二维码或条形码图片解码为文本<br>\n\t * 此方法会尝试使用{@link HybridBinarizer}和{@link GlobalHistogramBinarizer}两种模式解析<br>\n\t * 需要注意部分二维码如果不带logo，使用PureBarcode模式会解析失败，此时须设置此选项为false。\n\t *\n\t * @param image {@link Image} 二维码图片\n\t * @param hints 自定义扫码配置，包括算法、编码、复杂模式等\n\t * @return 解码后的文本\n\t * @since 5.7.12\n\t */\n\tpublic static String decode(Image image, Map<DecodeHintType, Object> hints) {\n\t\tfinal MultiFormatReader formatReader = new MultiFormatReader();\n\t\tformatReader.setHints(hints);\n\n\t\tfinal LuminanceSource source = new BufferedImageLuminanceSource(ImgUtil.toBufferedImage(image));\n\n\t\tResult result = _decode(formatReader, new HybridBinarizer(source));\n\t\tif (null == result) {\n\t\t\tresult = _decode(formatReader, new GlobalHistogramBinarizer(source));\n\t\t}\n\n\t\treturn null != result ? result.getText() : null;\n\t}\n\n\t/**\n\t * BitMatrix转BufferedImage\n\t *\n\t * @param matrix    BitMatrix\n\t * @param foreColor 前景色\n\t * @param backColor 背景色(null表示透明背景)\n\t * @return BufferedImage\n\t * @since 4.1.2\n\t */\n\tpublic static BufferedImage toImage(BitMatrix matrix, int foreColor, Integer backColor) {\n\t\tfinal int width = matrix.getWidth();\n\t\tfinal int height = matrix.getHeight();\n\t\tBufferedImage image = new BufferedImage(width, height, null == backColor ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);\n\t\tfor (int x = 0; x < width; x++) {\n\t\t\tfor (int y = 0; y < height; y++) {\n\t\t\t\tif (matrix.get(x, y)) {\n\t\t\t\t\timage.setRGB(x, y, foreColor);\n\t\t\t\t} else if (null != backColor) {\n\t\t\t\t\timage.setRGB(x, y, backColor);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn image;\n\t}\n\n\t/**\n\t * BitMatrix转SVG(字符串)\n\t *\n\t * @param matrix   BitMatrix\n\t * @param qrConfig 二维码配置，包括宽度、高度、边距、颜色等\n\t * @return SVG矢量图（字符串）\n\t * @since 5.8.6\n\t */\n\tpublic static String toSVG(BitMatrix matrix, QrConfig qrConfig) {\n\t\treturn toSVG(matrix, qrConfig.foreColor, qrConfig.backColor, qrConfig.img, qrConfig.getRatio());\n\t}\n\n\t/**\n\t * BitMatrix转SVG(字符串)\n\t *\n\t * @param matrix    BitMatrix\n\t * @param foreColor 前景色\n\t * @param backColor 背景色(null表示透明背景)\n\t * @param logoImg   LOGO图片\n\t * @param ratio     二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5\n\t * @return SVG矢量图（字符串）\n\t * @since 5.8.6\n\t */\n\tpublic static String toSVG(BitMatrix matrix, Integer foreColor, Integer backColor, Image logoImg, int ratio) {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tint qrWidth = matrix.getWidth();\n\t\tint qrHeight = matrix.getHeight();\n\t\tint moduleHeight = (qrHeight == 1) ? qrWidth / 2 : 1;\n\t\tfor (int y = 0; y < qrHeight; y++) {\n\t\t\tfor (int x = 0; x < qrWidth; x++) {\n\t\t\t\tif (matrix.get(x, y)) {\n\t\t\t\t\tsb.append(\" M\").append(x).append(\",\").append(y).append(\"h1v\").append(moduleHeight).append(\"h-1z\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tqrHeight *= moduleHeight;\n\t\tString logoBase64 = \"\";\n\t\tint logoWidth = 0;\n\t\tint logoHeight = 0;\n\t\tint logoX = 0;\n\t\tint logoY = 0;\n\t\tif (logoImg != null) {\n\t\t\tlogoBase64 = ImgUtil.toBase64DataUri(logoImg, \"png\");\n\t\t\t// 按照最短的边做比例缩放\n\t\t\tif (qrWidth < qrHeight) {\n\t\t\t\tlogoWidth = qrWidth / ratio;\n\t\t\t\tlogoHeight = logoImg.getHeight(null) * logoWidth / logoImg.getWidth(null);\n\t\t\t} else {\n\t\t\t\tlogoHeight = qrHeight / ratio;\n\t\t\t\tlogoWidth = logoImg.getWidth(null) * logoHeight / logoImg.getHeight(null);\n\t\t\t}\n\t\t\tlogoX = (qrWidth - logoWidth) / 2;\n\t\t\tlogoY = (qrHeight - logoHeight) / 2;\n\n\t\t}\n\n\t\tStringBuilder result = StrUtil.builder();\n\t\tresult.append(\"<svg width=\\\"\").append(qrWidth).append(\"\\\" height=\\\"\").append(qrHeight).append(\"\\\" \\n\");\n\t\tif (backColor != null) {\n\t\t\tColor back = new Color(backColor, true);\n\t\t\tresult.append(\"style=\\\"background-color:rgba(\").append(back.getRed()).append(\",\").append(back.getGreen()).append(\",\").append(back.getBlue()).append(\",\").append(back.getAlpha()).append(\")\\\"\\n\");\n\t\t}\n\t\tresult.append(\"viewBox=\\\"0 0 \").append(qrWidth).append(\" \").append(qrHeight).append(\"\\\" \\n\");\n\t\tresult.append(\"xmlns=\\\"http://www.w3.org/2000/svg\\\" \\n\");\n\t\tresult.append(\"xmlns:xlink=\\\"http://www.w3.org/1999/xlink\\\" >\\n\");\n\t\tresult.append(\"<path d=\\\"\").append(sb).append(\"\\\" \");\n\t\tif (foreColor != null) {\n\t\t\tColor fore = new Color(foreColor, true);\n\t\t\tresult.append(\"stroke=\\\"rgba(\").append(fore.getRed()).append(\",\").append(fore.getGreen()).append(\",\").append(fore.getBlue()).append(\",\").append(fore.getAlpha()).append(\")\\\"\");\n\t\t}\n\t\tresult.append(\" /> \\n\");\n\t\tif (StrUtil.isNotBlank(logoBase64)) {\n\t\t\tresult.append(\"<image xlink:href=\\\"\").append(logoBase64).append(\"\\\" height=\\\"\").append(logoHeight).append(\"\\\" width=\\\"\").append(logoWidth).append(\"\\\" y=\\\"\").append(logoY).append(\"\\\" x=\\\"\").append(logoX).append(\"\\\" />\\n\");\n\t\t}\n\t\tresult.append(\"</svg>\");\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * BitMatrix转ASCII Art字符画形式的二维码\n\t *\n\t * @param bitMatrix BitMatrix\n\t * @param qrConfig  QR设置\n\t * @return ASCII Art字符画形式的二维码\n\t * @since 5.8.6\n\t */\n\tpublic static String toAsciiArt(BitMatrix bitMatrix, QrConfig qrConfig) {\n\t\tfinal int width = bitMatrix.getWidth();\n\t\tfinal int height = bitMatrix.getHeight();\n\n\n\t\tfinal AnsiElement foreground = qrConfig.foreColor == null ? null : rgbToAnsi8BitElement(qrConfig.foreColor, ForeOrBack.FORE);\n\t\tfinal AnsiElement background = qrConfig.backColor == null ? null : rgbToAnsi8BitElement(qrConfig.backColor, ForeOrBack.BACK);\n\n\t\tStringBuilder builder = new StringBuilder();\n\t\tfor (int i = 0; i <= height; i += 2) {\n\t\t\tStringBuilder rowBuilder = new StringBuilder();\n\t\t\tfor (int j = 0; j < width; j++) {\n\t\t\t\tboolean tp = bitMatrix.get(i, j);\n\t\t\t\tboolean bt = i + 1 >= height || bitMatrix.get(i + 1, j);\n\t\t\t\tif (tp && bt) {\n\t\t\t\t\trowBuilder.append(' ');//'\\u0020'\n\t\t\t\t} else if (tp) {\n\t\t\t\t\trowBuilder.append('▄');//'\\u2584'\n\t\t\t\t} else if (bt) {\n\t\t\t\t\trowBuilder.append('▀');//'\\u2580'\n\t\t\t\t} else {\n\t\t\t\t\trowBuilder.append('█');//'\\u2588'\n\t\t\t\t}\n\t\t\t}\n\t\t\tbuilder.append(AnsiEncoder.encode(foreground, background, rowBuilder)).append('\\n');\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/*\t*/\n\n\t/**\n\t * rgb转AnsiElement\n\t *\n\t * @param rgb        rgb颜色值\n\t * @param foreOrBack 前景or背景\n\t * @return AnsiElement\n\t * @since 5.8.6\n\t */\n\tprivate static AnsiElement rgbToAnsi8BitElement(int rgb, ForeOrBack foreOrBack) {\n\t\treturn ansiColors.findClosest(new Color(rgb)).toAnsiElement(foreOrBack);\n\t}\n\n\n\t/**\n\t * 创建解码选项\n\t *\n\t * @param isTryHarder   是否优化精度\n\t * @param isPureBarcode 是否使用复杂模式，扫描带logo的二维码设为true\n\t * @return 选项Map\n\t */\n\tprivate static Map<DecodeHintType, Object> buildHints(boolean isTryHarder, boolean isPureBarcode) {\n\t\tfinal HashMap<DecodeHintType, Object> hints = new HashMap<>();\n\t\thints.put(DecodeHintType.CHARACTER_SET, CharsetUtil.UTF_8);\n\n\t\t// 优化精度\n\t\tif (isTryHarder) {\n\t\t\thints.put(DecodeHintType.TRY_HARDER, true);\n\t\t}\n\t\t// 复杂模式，开启PURE_BARCODE模式\n\t\tif (isPureBarcode) {\n\t\t\thints.put(DecodeHintType.PURE_BARCODE, true);\n\t\t}\n\t\treturn hints;\n\t}\n\n\t/**\n\t * 解码多种类型的码，包括二维码和条形码\n\t *\n\t * @param formatReader {@link MultiFormatReader}\n\t * @param binarizer    {@link Binarizer}\n\t * @return {@link Result}\n\t */\n\tprivate static Result _decode(MultiFormatReader formatReader, Binarizer binarizer) {\n\t\ttry {\n\t\t\treturn formatReader.decodeWithState(new BinaryBitmap(binarizer));\n\t\t} catch (NotFoundException e) {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java",
    "content": "package cn.hutool.extra.qrcode;\n\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport com.google.zxing.BarcodeFormat;\nimport com.google.zxing.EncodeHintType;\nimport com.google.zxing.datamatrix.encoder.SymbolShapeHint;\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;\n\nimport java.awt.Color;\nimport java.awt.Image;\nimport java.io.File;\nimport java.nio.charset.Charset;\nimport java.util.HashMap;\n\n/**\n * 二维码设置\n *\n * @author looly\n * @since 4.1.2\n */\npublic class QrConfig {\n\n\tprivate static final int BLACK = 0xFF000000;\n\tprivate static final int WHITE = 0xFFFFFFFF;\n\n\n\t/**\n\t * 宽度（单位：像素或▄）\n\t * <p>当二维码类型为一般图片或者SVG时，单位是像素</p>\n\t * <p>当二维码类型Ascii Art字符画时，单位是字符▄或▀的大小</p>\n\t */\n\tprotected int width;\n\n\t/**\n\t * 高度（单位：像素或▄）\n\t * <p>当二维码类型为一般图片或者SVG时，单位是像素</p>\n\t * <p>当二维码类型Ascii Art字符画时，单位是字符▄或▀的大小</p>\n\t */\n\tprotected int height;\n\t/** 前景色（二维码颜色） */\n\tprotected Integer foreColor = BLACK;\n\t/** 背景色，默认白色，null表示透明 */\n\tprotected Integer backColor = WHITE;\n\t/** 边距1~4 */\n\tprotected Integer margin = 2;\n\t/** 设置二维码中的信息量，可设置0-40的整数 */\n\tprotected Integer qrVersion;\n\t/** 纠错级别 */\n\tprotected ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.M;\n\t/** 编码 */\n\tprotected Charset charset = CharsetUtil.CHARSET_UTF_8;\n\t/** 二维码中的Logo */\n\tprotected Image img;\n\t/** 二维码中的Logo圆角弧度 */\n\tprotected double round = 0.3;\n\t/** 二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5 */\n\tprotected int ratio = 6;\n\t/**\n\t * DATA_MATRIX的符号形状\n\t */\n\tprotected SymbolShapeHint shapeHint = SymbolShapeHint.FORCE_NONE;\n\n\t/**\n\t * 创建QrConfig\n\t * @return QrConfig\n\t * @since 4.1.14\n\t */\n\tpublic static QrConfig create() {\n\t\treturn new QrConfig();\n\t}\n\n\t/**\n\t * 构造，默认长宽为300\n\t */\n\tpublic QrConfig() {\n\t\tthis(300, 300);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param width 宽\n\t * @param height 长\n\t */\n\tpublic QrConfig(int width, int height) {\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t}\n\n\t/**\n\t * 获取宽度\n\t *\n\t * @return 宽度\n\t */\n\tpublic int getWidth() {\n\t\treturn width;\n\t}\n\n\t/**\n\t * 设置宽度\n\t *\n\t * @param width 宽度\n\t * @return this\n\t */\n\tpublic QrConfig setWidth(int width) {\n\t\tthis.width = width;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取高度\n\t *\n\t * @return 高度\n\t */\n\tpublic int getHeight() {\n\t\treturn height;\n\t}\n\n\t/**\n\t * 设置高度\n\t *\n\t * @param height 高度\n\t * @return this;\n\t */\n\tpublic QrConfig setHeight(int height) {\n\t\tthis.height = height;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取前景色\n\t *\n\t * @return 前景色\n\t */\n\tpublic int getForeColor() {\n\t\treturn foreColor;\n\t}\n\n\t/**\n\t * 设置前景色，例如：Color.BLUE.getRGB()\n\t *\n\t * @param foreColor 前景色\n\t * @return this\n\t * @deprecated 请使用 {@link #setForeColor(Color)}\n\t */\n\t@Deprecated\n\tpublic QrConfig setForeColor(int foreColor) {\n\t\tthis.foreColor = foreColor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置前景色，例如：Color.BLUE.getRGB()\n\t *\n\t * @param foreColor 前景色\n\t * @return this\n\t * @since 5.1.1\n\t */\n\tpublic QrConfig setForeColor(Color foreColor) {\n\t\tif(null == foreColor){\n\t\t\tthis.foreColor = null;\n\t\t} else {\n\t\t\tthis.foreColor = foreColor.getRGB();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取背景色\n\t *\n\t * @return 背景色\n\t */\n\tpublic int getBackColor() {\n\t\treturn backColor;\n\t}\n\n\t/**\n\t * 设置背景色，例如：Color.BLUE.getRGB()\n\t *\n\t * @param backColor 背景色\n\t * @return this\n\t * @deprecated 请使用 {@link #setBackColor(Color)}\n\t */\n\t@Deprecated\n\tpublic QrConfig setBackColor(int backColor) {\n\t\tthis.backColor = backColor;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置背景色，例如：Color.BLUE\n\t *\n\t * @param backColor 背景色,null表示透明背景\n\t * @return this\n\t * @since 5.1.1\n\t */\n\tpublic QrConfig setBackColor(Color backColor) {\n\t\tif(null == backColor){\n\t\t\tthis.backColor = null;\n\t\t} else {\n\t\t\tthis.backColor = backColor.getRGB();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取边距\n\t *\n\t * @return 边距\n\t */\n\tpublic Integer getMargin() {\n\t\treturn margin;\n\t}\n\n\t/**\n\t * 设置边距\n\t *\n\t * @param margin 边距\n\t * @return this\n\t */\n\tpublic QrConfig setMargin(Integer margin) {\n\t\tthis.margin = margin;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置二维码中的信息量，可设置0-40的整数，二维码图片也会根据qrVersion而变化，0表示根据传入信息自动变化\n\t *\n\t * @return 二维码中的信息量\n\t */\n\tpublic Integer getQrVersion() {\n\t\treturn qrVersion;\n\t}\n\n\t/**\n\t * 设置二维码中的信息量，可设置0-40的整数，二维码图片也会根据qrVersion而变化，0表示根据传入信息自动变化\n\t *\n\t * @param qrVersion 二维码中的信息量\n\t * @return this\n\t */\n\tpublic QrConfig setQrVersion(Integer qrVersion) {\n\t\tthis.qrVersion = qrVersion;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取纠错级别\n\t *\n\t * @return 纠错级别\n\t */\n\tpublic ErrorCorrectionLevel getErrorCorrection() {\n\t\treturn errorCorrection;\n\t}\n\n\t/**\n\t * 设置纠错级别\n\t *\n\t * @param errorCorrection 纠错级别\n\t * @return this\n\t */\n\tpublic QrConfig setErrorCorrection(ErrorCorrectionLevel errorCorrection) {\n\t\tthis.errorCorrection = errorCorrection;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取编码\n\t *\n\t * @return 编码\n\t */\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return this\n\t */\n\tpublic QrConfig setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取二维码中的Logo\n\t *\n\t * @return Logo图片\n\t */\n\tpublic Image getImg() {\n\t\treturn img;\n\t}\n\n\t/**\n\t * 设置二维码中的Logo文件\n\t *\n\t * @param imgPath 二维码中的Logo路径\n\t * @return this;\n\t */\n\tpublic QrConfig setImg(String imgPath) {\n\t\treturn setImg(FileUtil.file(imgPath));\n\t}\n\n\t/**\n\t * 设置二维码中的Logo文件\n\t *\n\t * @param imgFile 二维码中的Logo\n\t * @return this;\n\t */\n\tpublic QrConfig setImg(File imgFile) {\n\t\treturn setImg(ImgUtil.read(imgFile));\n\t}\n\n\t/**\n\t * 设置二维码中的Logo\n\t *\n\t * @param img 二维码中的Logo\n\t * @return this;\n\t */\n\tpublic QrConfig setImg(Image img) {\n\t\tthis.img = img;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5\n\t *\n\t * @return 二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5\n\t */\n\tpublic int getRatio() {\n\t\treturn this.ratio;\n\t}\n\n\t/**\n\t * 设置二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5\n\t *\n\t * @param ratio 二维码中的Logo缩放的比例系数，如5表示长宽最小值的1/5\n\t * @return this;\n\t */\n\tpublic QrConfig setRatio(int ratio) {\n\t\tthis.ratio = ratio;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取二维码中的Logo圆角弧度\n\t *\n\t * @return 二维码中的Logo圆角弧度\n\t */\n\tpublic double getRound() {\n\t\treturn round;\n\t}\n\n\t/**\n\t * 设置二维码中的Logo圆角弧度\n\t *\n\t * @param round 二维码中的Logo圆角弧度\n\t * @return this;\n\t */\n\tpublic QrConfig setRound(double round) {\n\t\tthis.round = round;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置DATA_MATRIX的符号形状\n\t *\n\t * @param shapeHint DATA_MATRIX的符号形状\n\t * @return this;\n\t */\n\tpublic QrConfig setShapeHint(SymbolShapeHint shapeHint) {\n\t\tthis.shapeHint = shapeHint;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 转换为Zxing的二维码配置\n\t *\n\t * @return 配置\n\t */\n\tpublic HashMap<EncodeHintType, Object> toHints() {\n\t\treturn toHints(BarcodeFormat.QR_CODE);\n\t}\n\n\t/**\n\t * 转换为Zxing的二维码配置\n\t *\n\t * @param format 格式，根据格式不同，{@link #errorCorrection}的值类型有所不同\n\t * @return 配置\n\t */\n\tpublic HashMap<EncodeHintType, Object> toHints(BarcodeFormat format) {\n\t\t// 配置\n\t\tfinal HashMap<EncodeHintType, Object> hints = new HashMap<>();\n\t\tif (null != this.charset) {\n\t\t\thints.put(EncodeHintType.CHARACTER_SET, charset.toString().toLowerCase());\n\t\t}\n\t\tif (null != this.errorCorrection) {\n\t\t\tObject value;\n\t\t\tif(BarcodeFormat.AZTEC == format || BarcodeFormat.PDF_417 == format){\n\t\t\t\t// issue#I4FE3U@Gitee\n\t\t\t\tvalue = this.errorCorrection.getBits();\n\t\t\t} else {\n\t\t\t\tvalue = this.errorCorrection;\n\t\t\t}\n\n\t\t\thints.put(EncodeHintType.ERROR_CORRECTION, value);\n\t\t\thints.put(EncodeHintType.DATA_MATRIX_SHAPE, shapeHint);\n\t\t}\n\t\tif (null != this.margin) {\n\t\t\thints.put(EncodeHintType.MARGIN, this.margin);\n\t\t}\n\t\tif (null != this.qrVersion){\n\t\t\thints.put(EncodeHintType.QR_VERSION, this.qrVersion);\n\t\t}\n\t\treturn hints;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/qrcode/package-info.java",
    "content": "/**\n * 二维码封装，基于zxing库，入口为QrCodeUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.qrcode;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/servlet/JakartaServletUtil.java",
    "content": "package cn.hutool.extra.servlet;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.net.NetUtil;\nimport cn.hutool.core.net.multipart.MultipartFormData;\nimport cn.hutool.core.net.multipart.UploadSetting;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport jakarta.servlet.ServletOutputStream;\nimport jakarta.servlet.ServletRequest;\nimport jakarta.servlet.http.Cookie;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PrintWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.List;\n\n/**\n * Servlet相关工具类封装\n *\n * @author looly\n * @since 3.2.0\n */\npublic class JakartaServletUtil {\n\n\tpublic static final String METHOD_DELETE = \"DELETE\";\n\tpublic static final String METHOD_HEAD = \"HEAD\";\n\tpublic static final String METHOD_GET = \"GET\";\n\tpublic static final String METHOD_OPTIONS = \"OPTIONS\";\n\tpublic static final String METHOD_POST = \"POST\";\n\tpublic static final String METHOD_PUT = \"PUT\";\n\tpublic static final String METHOD_TRACE = \"TRACE\";\n\n\t// --------------------------------------------------------- getParam start\n\n\t/**\n\t * 获得所有请求参数\n\t *\n\t * @param request 请求对象{@link ServletRequest}\n\t * @return Map\n\t */\n\tpublic static Map<String, String[]> getParams(ServletRequest request) {\n\t\tfinal Map<String, String[]> map = request.getParameterMap();\n\t\treturn Collections.unmodifiableMap(map);\n\t}\n\n\t/**\n\t * 获得所有请求参数\n\t *\n\t * @param request 请求对象{@link ServletRequest}\n\t * @return Map\n\t */\n\tpublic static Map<String, String> getParamMap(ServletRequest request) {\n\t\tMap<String, String> params = new HashMap<>();\n\t\tfor (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {\n\t\t\tparams.put(entry.getKey(), ArrayUtil.join(entry.getValue(), StrUtil.COMMA));\n\t\t}\n\t\treturn params;\n\t}\n\n\t/**\n\t * 获取请求体<br>\n\t * 调用该方法后，getParam方法将失效\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return 获得请求体\n\t * @since 4.0.2\n\t */\n\tpublic static String getBody(ServletRequest request) {\n\t\ttry (final BufferedReader reader = request.getReader()) {\n\t\t\treturn IoUtil.read(reader);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取请求体byte[]<br>\n\t * 调用该方法后，getParam方法将失效\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return 获得请求体byte[]\n\t * @since 4.0.2\n\t */\n\tpublic static byte[] getBodyBytes(ServletRequest request) {\n\t\ttry {\n\t\t\treturn IoUtil.readBytes(request.getInputStream());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\t// --------------------------------------------------------- getParam end\n\n\t// --------------------------------------------------------- fillBean start\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>         Bean类型\n\t * @param request     ServletRequest\n\t * @param bean        Bean\n\t * @param copyOptions 注入时的设置\n\t * @return Bean\n\t * @since 3.0.4\n\t */\n\tpublic static <T> T fillBean(final ServletRequest request, T bean, CopyOptions copyOptions) {\n\t\tfinal String beanName = StrUtil.lowerFirst(bean.getClass().getSimpleName());\n\t\treturn BeanUtil.fillBean(bean, new ValueProvider<String>() {\n\t\t\t@Override\n\t\t\tpublic Object value(String key, Type valueType) {\n\t\t\t\tString[] values = request.getParameterValues(key);\n\t\t\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\t\t\tvalues = request.getParameterValues(beanName + StrUtil.DOT + key);\n\t\t\t\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (1 == values.length) {\n\t\t\t\t\t// 单值表单直接返回这个值\n\t\t\t\t\treturn values[0];\n\t\t\t\t} else {\n\t\t\t\t\t// 多值表单返回数组\n\t\t\t\t\treturn values;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean containsKey(String key) {\n\t\t\t\t// 对于Servlet来说，返回值null意味着无此参数\n\t\t\t\treturn (null != request.getParameter(key)) || (null != request.getParameter(beanName + StrUtil.DOT + key));\n\t\t\t}\n\t\t}, copyOptions);\n\t}\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>           Bean类型\n\t * @param request       {@link ServletRequest}\n\t * @param bean          Bean\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBean(ServletRequest request, T bean, boolean isIgnoreError) {\n\t\treturn fillBean(request, bean, CopyOptions.create().setIgnoreError(isIgnoreError));\n\t}\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>           Bean类型\n\t * @param request       ServletRequest\n\t * @param beanClass     Bean Class\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T toBean(ServletRequest request, Class<T> beanClass, boolean isIgnoreError) {\n\t\treturn fillBean(request, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError);\n\t}\n\t// --------------------------------------------------------- fillBean end\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * 默认检测的Header:\n\t *\n\t * <pre>\n\t * 1、X-Forwarded-For\n\t * 2、X-Real-IP\n\t * 3、Proxy-Client-IP\n\t * 4、WL-Proxy-Client-IP\n\t * </pre>\n\t *\n\t * <p>\n\t * otherHeaderNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param request          请求对象{@link HttpServletRequest}\n\t * @param otherHeaderNames 其他自定义头文件，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t */\n\tpublic static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {\n\t\tString[] headers = {\"X-Forwarded-For\", \"X-Real-IP\", \"Proxy-Client-IP\", \"WL-Proxy-Client-IP\", \"HTTP_CLIENT_IP\", \"HTTP_X_FORWARDED_FOR\"};\n\t\tif (ArrayUtil.isNotEmpty(otherHeaderNames)) {\n\t\t\theaders = ArrayUtil.addAll(otherHeaderNames, headers);\n\t\t}\n\n\t\treturn getClientIPByHeader(request, headers);\n\t}\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * headerNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param request     请求对象{@link HttpServletRequest}\n\t * @param headerNames 自定义头，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t * @since 4.4.1\n\t */\n\tpublic static String getClientIPByHeader(HttpServletRequest request, String... headerNames) {\n\t\tString ip;\n\t\tfor (String header : headerNames) {\n\t\t\tip = request.getHeader(header);\n\t\t\tif (false == NetUtil.isUnknown(ip)) {\n\t\t\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t\t\t}\n\t\t}\n\n\t\tip = request.getRemoteAddr();\n\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t}\n\n\t/**\n\t * 获得MultiPart表单内容，多用于获得上传的文件 在同一次请求中，此方法只能被执行一次！\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return MultipartFormData\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.2\n\t */\n\tpublic static MultipartFormData getMultipart(ServletRequest request) throws IORuntimeException {\n\t\treturn getMultipart(request, new UploadSetting());\n\t}\n\n\t/**\n\t * 获得multipart/form-data 表单内容<br>\n\t * 包括文件和普通表单数据<br>\n\t * 在同一次请求中，此方法只能被执行一次！\n\t *\n\t * @param request       {@link ServletRequest}\n\t * @param uploadSetting 上传文件的设定，包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等\n\t * @return MultiPart表单\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.2\n\t */\n\tpublic static MultipartFormData getMultipart(ServletRequest request, UploadSetting uploadSetting) throws IORuntimeException {\n\t\tfinal MultipartFormData formData = new MultipartFormData(uploadSetting);\n\t\ttry {\n\t\t\tformData.parseRequestStream(request.getInputStream(), CharsetUtil.charset(request.getCharacterEncoding()));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn formData;\n\t}\n\n\t// --------------------------------------------------------- Header start\n\n\t/**\n\t * 获取请求所有的头（header）信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return header值\n\t * @since 4.6.2\n\t */\n\tpublic static Map<String, String> getHeaderMap(HttpServletRequest request) {\n\t\tfinal Map<String, String> headerMap = new HashMap<>();\n\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\theaderMap.put(name, request.getHeader(name));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\t/**\n\t * 获取请求所有的头（header）信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return header值\n\t */\n\tpublic static Map<String, List<String>> getHeadersMap(final HttpServletRequest request) {\n\t\tfinal Map<String, List<String>> headerMap = new LinkedHashMap<>();\n\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\theaderMap.put(name, ListUtil.list(false, request.getHeaders(name)));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\t/**\n\t * 获取响应所有的头（header）信息\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @return header值\n\t */\n\tpublic static Map<String, Collection<String>> getHeadersMap(final HttpServletResponse response) {\n\t\tfinal Map<String, Collection<String>> headerMap = new HashMap<>();\n\n\t\tfinal Collection<String> names = response.getHeaderNames();\n\t\tfor (final String name : names) {\n\t\t\theaderMap.put(name, response.getHeaders(name));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\n\t/**\n\t * 忽略大小写获得请求header中的信息\n\t *\n\t * @param request        请求对象{@link HttpServletRequest}\n\t * @param nameIgnoreCase 忽略大小写头信息的KEY\n\t * @return header值\n\t */\n\tpublic static String getHeaderIgnoreCase(HttpServletRequest request, String nameIgnoreCase) {\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\tif (name != null && name.equalsIgnoreCase(nameIgnoreCase)) {\n\t\t\t\treturn request.getHeader(name);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param request     请求对象{@link HttpServletRequest}\n\t * @param name        头信息的KEY\n\t * @param charsetName 字符集\n\t * @return header值\n\t */\n\tpublic static String getHeader(HttpServletRequest request, String name, String charsetName) {\n\t\treturn getHeader(request, name, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @param name    头信息的KEY\n\t * @param charset 字符集\n\t * @return header值\n\t * @since 4.6.2\n\t */\n\tpublic static String getHeader(HttpServletRequest request, String name, Charset charset) {\n\t\tfinal String header = request.getHeader(name);\n\t\tif (null != header) {\n\t\t\treturn CharsetUtil.convert(header, CharsetUtil.CHARSET_ISO_8859_1, charset);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 客户浏览器是否为IE\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 客户浏览器是否为IE\n\t */\n\tpublic static boolean isIE(HttpServletRequest request) {\n\t\tString userAgent = getHeaderIgnoreCase(request, \"User-Agent\");\n\t\tif (StrUtil.isNotBlank(userAgent)) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tuserAgent = userAgent.toUpperCase();\n\t\t\treturn userAgent.contains(\"MSIE\") || userAgent.contains(\"TRIDENT\");\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否为GET请求\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为GET请求\n\t */\n\tpublic static boolean isGetMethod(HttpServletRequest request) {\n\t\treturn METHOD_GET.equalsIgnoreCase(request.getMethod());\n\t}\n\n\t/**\n\t * 是否为POST请求\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为POST请求\n\t */\n\tpublic static boolean isPostMethod(HttpServletRequest request) {\n\t\treturn METHOD_POST.equalsIgnoreCase(request.getMethod());\n\t}\n\n\t/**\n\t * 是否为Multipart类型表单，此类型表单用于文件上传\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为Multipart类型表单，此类型表单用于文件上传\n\t */\n\tpublic static boolean isMultipart(HttpServletRequest request) {\n\t\tif (false == isPostMethod(request)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString contentType = request.getContentType();\n\t\tif (StrUtil.isBlank(contentType)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn contentType.toLowerCase().startsWith(\"multipart/\");\n\t}\n\t// --------------------------------------------------------- Header end\n\n\t// --------------------------------------------------------- Cookie start\n\n\t/**\n\t * 获得指定的Cookie\n\t *\n\t * @param httpServletRequest {@link HttpServletRequest}\n\t * @param name               cookie名\n\t * @return Cookie对象\n\t */\n\tpublic static Cookie getCookie(HttpServletRequest httpServletRequest, String name) {\n\t\treturn readCookieMap(httpServletRequest).get(name);\n\t}\n\n\t/**\n\t * 将cookie封装到Map里面\n\t *\n\t * @param httpServletRequest {@link HttpServletRequest}\n\t * @return Cookie map\n\t */\n\tpublic static Map<String, Cookie> readCookieMap(HttpServletRequest httpServletRequest) {\n\t\tfinal Cookie[] cookies = httpServletRequest.getCookies();\n\t\tif (ArrayUtil.isEmpty(cookies)) {\n\t\t\treturn MapUtil.empty();\n\t\t}\n\n\t\treturn IterUtil.toMap(\n\t\t\t\tnew ArrayIter<>(httpServletRequest.getCookies()),\n\t\t\t\tnew CaseInsensitiveMap<>(),\n\t\t\t\tCookie::getName);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param cookie   Servlet Cookie对象\n\t */\n\tpublic static void addCookie(HttpServletResponse response, Cookie cookie) {\n\t\tresponse.addCookie(cookie);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param name     Cookie名\n\t * @param value    Cookie值\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value) {\n\t\tresponse.addCookie(new Cookie(name, value));\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response        响应对象{@link HttpServletResponse}\n\t * @param name            cookie名\n\t * @param value           cookie值\n\t * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. &gt;0 : Cookie存在的秒数.\n\t * @param path            Cookie的有效路径\n\t * @param domain          the domain name within which this cookie is visible; form is according to RFC 2109\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain) {\n\t\tCookie cookie = new Cookie(name, value);\n\t\tif (domain != null) {\n\t\t\tcookie.setDomain(domain);\n\t\t}\n\t\tcookie.setMaxAge(maxAgeInSeconds);\n\t\tcookie.setPath(path);\n\t\taddCookie(response, cookie);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie<br>\n\t * Path: \"/\"<br>\n\t * No Domain\n\t *\n\t * @param response        响应对象{@link HttpServletResponse}\n\t * @param name            cookie名\n\t * @param value           cookie值\n\t * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. &gt;0 : Cookie存在的秒数.\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {\n\t\taddCookie(response, name, value, maxAgeInSeconds, \"/\", null);\n\t}\n\n\t// --------------------------------------------------------- Cookie end\n\t// --------------------------------------------------------- Response start\n\n\t/**\n\t * 获得PrintWriter\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @return 获得PrintWriter\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static PrintWriter getWriter(HttpServletResponse response) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn response.getWriter();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param text        返回的内容\n\t * @param contentType 返回的类型\n\t */\n\tpublic static void write(HttpServletResponse response, String text, String contentType) {\n\t\tresponse.setContentType(contentType);\n\t\tWriter writer = null;\n\t\ttry {\n\t\t\twriter = response.getWriter();\n\t\t\twriter.write(text);\n\t\t\twriter.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 返回文件给客户端\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param file     写出的文件对象\n\t * @since 4.1.15\n\t */\n\tpublic static void write(HttpServletResponse response, File file) {\n\t\tfinal String fileName = file.getName();\n\t\tfinal String contentType = ObjectUtil.defaultIfNull(FileUtil.getMimeType(fileName), \"application/octet-stream\");\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\twrite(response, in, contentType, fileName);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型，可以使用{@link FileUtil#getMimeType(String)}获取对应扩展名的MIME信息\n\t *                    <ul>\n\t *                      <li>application/pdf</li>\n\t *                      <li>application/vnd.ms-excel</li>\n\t *                      <li>application/msword</li>\n\t *                      <li>application/vnd.ms-powerpoint</li>\n\t *                    </ul>\n\t *                    docx、xlsx 这种 office 2007 格式 设置 MIME;网页里面docx 文件是没问题，但是下载下来了之后就变成doc格式了\n\t *                    参考：<a href=\"https://my.oschina.net/shixiaobao17145/blog/32489\">https://my.oschina.net/shixiaobao17145/blog/32489</a>\n\t *                    <ul>\n\t *                      <li>MIME_EXCELX_TYPE = \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";</li>\n\t *                      <li>MIME_PPTX_TYPE = \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";</li>\n\t *                      <li>MIME_WORDX_TYPE = \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";</li>\n\t *                      <li>MIME_STREAM_TYPE = \"application/octet-stream;charset=utf-8\"; #原始字节流</li>\n\t *                    </ul>\n\t * @param fileName    文件名，自动添加双引号\n\t * @since 4.1.15\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, String contentType, String fileName) {\n\t\tfinal String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8);\n\t\tfinal String encodeText = URLUtil.encodeAll(fileName, CharsetUtil.charset(charset));\n\t\tresponse.setHeader(\"Content-Disposition\",\n\t\t\t\tStrUtil.format(\"attachment;filename=\\\"{}\\\";filename*={}''{}\", encodeText, charset, encodeText));\n\t\tresponse.setContentType(contentType);\n\t\twrite(response, in);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, String contentType) {\n\t\tresponse.setContentType(contentType);\n\t\twrite(response, in);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param in       需要返回客户端的内容\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in) {\n\t\twrite(response, in, IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response   响应对象{@link HttpServletResponse}\n\t * @param in         需要返回客户端的内容\n\t * @param bufferSize 缓存大小\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, int bufferSize) {\n\t\tServletOutputStream out = null;\n\t\ttry {\n\t\t\tout = response.getOutputStream();\n\t\t\tIoUtil.copy(in, out, bufferSize);\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 设置响应的Header\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param name     名\n\t * @param value    值，可以是String，Date， int\n\t */\n\tpublic static void setHeader(HttpServletResponse response, String name, Object value) {\n\t\tif (value instanceof String) {\n\t\t\tresponse.setHeader(name, (String) value);\n\t\t} else if (Date.class.isAssignableFrom(value.getClass())) {\n\t\t\tresponse.setDateHeader(name, ((Date) value).getTime());\n\t\t} else if (value instanceof Integer || \"int\".equalsIgnoreCase(value.getClass().getSimpleName())) {\n\t\t\tresponse.setIntHeader(name, (int) value);\n\t\t} else {\n\t\t\tresponse.setHeader(name, value.toString());\n\t\t}\n\t}\n\t// --------------------------------------------------------- Response end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java",
    "content": "package cn.hutool.extra.servlet;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.net.NetUtil;\nimport cn.hutool.core.net.multipart.MultipartFormData;\nimport cn.hutool.core.net.multipart.UploadSetting;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport javax.servlet.ServletOutputStream;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PrintWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Servlet相关工具类封装\n *\n * @author looly\n * @since 3.2.0\n */\npublic class ServletUtil {\n\n\tpublic static final String METHOD_DELETE = \"DELETE\";\n\tpublic static final String METHOD_HEAD = \"HEAD\";\n\tpublic static final String METHOD_GET = \"GET\";\n\tpublic static final String METHOD_OPTIONS = \"OPTIONS\";\n\tpublic static final String METHOD_POST = \"POST\";\n\tpublic static final String METHOD_PUT = \"PUT\";\n\tpublic static final String METHOD_TRACE = \"TRACE\";\n\n\t// --------------------------------------------------------- getParam start\n\n\t/**\n\t * 获得所有请求参数\n\t *\n\t * @param request 请求对象{@link ServletRequest}\n\t * @return Map\n\t */\n\tpublic static Map<String, String[]> getParams(ServletRequest request) {\n\t\tfinal Map<String, String[]> map = request.getParameterMap();\n\t\treturn Collections.unmodifiableMap(map);\n\t}\n\n\t/**\n\t * 获得所有请求参数\n\t *\n\t * @param request 请求对象{@link ServletRequest}\n\t * @return Map\n\t */\n\tpublic static Map<String, String> getParamMap(ServletRequest request) {\n\t\tMap<String, String> params = new HashMap<>();\n\t\tfor (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {\n\t\t\tparams.put(entry.getKey(), ArrayUtil.join(entry.getValue(), StrUtil.COMMA));\n\t\t}\n\t\treturn params;\n\t}\n\n\t/**\n\t * 获取请求体<br>\n\t * 调用该方法后，getParam方法将失效\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return 获得请求体\n\t * @since 4.0.2\n\t */\n\tpublic static String getBody(ServletRequest request) {\n\t\ttry (final BufferedReader reader = request.getReader()) {\n\t\t\treturn IoUtil.read(reader);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取请求体byte[]<br>\n\t * 调用该方法后，getParam方法将失效\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return 获得请求体byte[]\n\t * @since 4.0.2\n\t */\n\tpublic static byte[] getBodyBytes(ServletRequest request) {\n\t\ttry {\n\t\t\treturn IoUtil.readBytes(request.getInputStream());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\t// --------------------------------------------------------- getParam end\n\n\t// --------------------------------------------------------- fillBean start\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>         Bean类型\n\t * @param request     ServletRequest\n\t * @param bean        Bean\n\t * @param copyOptions 注入时的设置\n\t * @return Bean\n\t * @since 3.0.4\n\t */\n\tpublic static <T> T fillBean(final ServletRequest request, T bean, CopyOptions copyOptions) {\n\t\tfinal String beanName = StrUtil.lowerFirst(bean.getClass().getSimpleName());\n\t\treturn BeanUtil.fillBean(bean, new ValueProvider<String>() {\n\t\t\t@Override\n\t\t\tpublic Object value(String key, Type valueType) {\n\t\t\t\tString[] values = request.getParameterValues(key);\n\t\t\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\t\t\tvalues = request.getParameterValues(beanName + StrUtil.DOT + key);\n\t\t\t\t\tif (ArrayUtil.isEmpty(values)) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (1 == values.length) {\n\t\t\t\t\t// 单值表单直接返回这个值\n\t\t\t\t\treturn values[0];\n\t\t\t\t} else {\n\t\t\t\t\t// 多值表单返回数组\n\t\t\t\t\treturn values;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean containsKey(String key) {\n\t\t\t\t// 对于Servlet来说，返回值null意味着无此参数\n\t\t\t\treturn (null != request.getParameter(key)) || (null != request.getParameter(beanName + StrUtil.DOT + key));\n\t\t\t}\n\t\t}, copyOptions);\n\t}\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>           Bean类型\n\t * @param request       {@link ServletRequest}\n\t * @param bean          Bean\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T fillBean(ServletRequest request, T bean, boolean isIgnoreError) {\n\t\treturn fillBean(request, bean, CopyOptions.create().setIgnoreError(isIgnoreError));\n\t}\n\n\t/**\n\t * ServletRequest 参数转Bean\n\t *\n\t * @param <T>           Bean类型\n\t * @param request       ServletRequest\n\t * @param beanClass     Bean Class\n\t * @param isIgnoreError 是否忽略注入错误\n\t * @return Bean\n\t */\n\tpublic static <T> T toBean(ServletRequest request, Class<T> beanClass, boolean isIgnoreError) {\n\t\treturn fillBean(request, ReflectUtil.newInstanceIfPossible(beanClass), isIgnoreError);\n\t}\n\t// --------------------------------------------------------- fillBean end\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * 默认检测的Header:\n\t *\n\t * <pre>\n\t * 1、X-Forwarded-For\n\t * 2、X-Real-IP\n\t * 3、Proxy-Client-IP\n\t * 4、WL-Proxy-Client-IP\n\t * </pre>\n\t *\n\t * <p>\n\t * otherHeaderNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param request          请求对象{@link HttpServletRequest}\n\t * @param otherHeaderNames 其他自定义头文件，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t */\n\tpublic static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {\n\t\tString[] headers = {\"X-Forwarded-For\", \"X-Real-IP\", \"Proxy-Client-IP\", \"WL-Proxy-Client-IP\", \"HTTP_CLIENT_IP\", \"HTTP_X_FORWARDED_FOR\"};\n\t\tif (ArrayUtil.isNotEmpty(otherHeaderNames)) {\n\t\t\theaders = ArrayUtil.addAll(otherHeaderNames, headers);\n\t\t}\n\n\t\treturn getClientIPByHeader(request, headers);\n\t}\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * headerNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param request     请求对象{@link HttpServletRequest}\n\t * @param headerNames 自定义头，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t * @since 4.4.1\n\t */\n\tpublic static String getClientIPByHeader(HttpServletRequest request, String... headerNames) {\n\t\tString ip;\n\t\tfor (String header : headerNames) {\n\t\t\tip = request.getHeader(header);\n\t\t\tif (false == NetUtil.isUnknown(ip)) {\n\t\t\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t\t\t}\n\t\t}\n\n\t\tip = request.getRemoteAddr();\n\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t}\n\n\t/**\n\t * 获得MultiPart表单内容，多用于获得上传的文件 在同一次请求中，此方法只能被执行一次！\n\t *\n\t * @param request {@link ServletRequest}\n\t * @return MultipartFormData\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.2\n\t */\n\tpublic static MultipartFormData getMultipart(ServletRequest request) throws IORuntimeException {\n\t\treturn getMultipart(request, new UploadSetting());\n\t}\n\n\t/**\n\t * 获得multipart/form-data 表单内容<br>\n\t * 包括文件和普通表单数据<br>\n\t * 在同一次请求中，此方法只能被执行一次！\n\t *\n\t * @param request       {@link ServletRequest}\n\t * @param uploadSetting 上传文件的设定，包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等\n\t * @return MultiPart表单\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.2\n\t */\n\tpublic static MultipartFormData getMultipart(ServletRequest request, UploadSetting uploadSetting) throws IORuntimeException {\n\t\tfinal MultipartFormData formData = new MultipartFormData(uploadSetting);\n\t\ttry {\n\t\t\tformData.parseRequestStream(request.getInputStream(), CharsetUtil.charset(request.getCharacterEncoding()));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn formData;\n\t}\n\n\t// --------------------------------------------------------- Header start\n\n\t/**\n\t * 获取请求所有的头（header）信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return header值\n\t * @since 4.6.2\n\t */\n\tpublic static Map<String, String> getHeaderMap(HttpServletRequest request) {\n\t\tfinal Map<String, String> headerMap = new HashMap<>();\n\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\theaderMap.put(name, request.getHeader(name));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\t/**\n\t * 获取请求所有的头（header）信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return header值\n\t */\n\tpublic static Map<String, List<String>> getHeadersMap(final HttpServletRequest request) {\n\t\tfinal Map<String, List<String>> headerMap = new LinkedHashMap<>();\n\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\theaderMap.put(name, ListUtil.list(false, request.getHeaders(name)));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\t/**\n\t * 获取响应所有的头（header）信息\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @return header值\n\t */\n\tpublic static Map<String, Collection<String>> getHeadersMap(HttpServletResponse response) {\n\t\tfinal Map<String, Collection<String>> headerMap = new HashMap<>();\n\n\t\tfinal Collection<String> names = response.getHeaderNames();\n\t\tfor (String name : names) {\n\t\t\theaderMap.put(name, response.getHeaders(name));\n\t\t}\n\n\t\treturn headerMap;\n\t}\n\n\t/**\n\t * 忽略大小写获得请求header中的信息\n\t *\n\t * @param request        请求对象{@link HttpServletRequest}\n\t * @param nameIgnoreCase 忽略大小写头信息的KEY\n\t * @return header值\n\t */\n\tpublic static String getHeaderIgnoreCase(HttpServletRequest request, String nameIgnoreCase) {\n\t\tfinal Enumeration<String> names = request.getHeaderNames();\n\t\tString name;\n\t\twhile (names.hasMoreElements()) {\n\t\t\tname = names.nextElement();\n\t\t\tif (name != null && name.equalsIgnoreCase(nameIgnoreCase)) {\n\t\t\t\treturn request.getHeader(name);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param request     请求对象{@link HttpServletRequest}\n\t * @param name        头信息的KEY\n\t * @param charsetName 字符集\n\t * @return header值\n\t */\n\tpublic static String getHeader(HttpServletRequest request, String name, String charsetName) {\n\t\treturn getHeader(request, name, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @param name    头信息的KEY\n\t * @param charset 字符集\n\t * @return header值\n\t * @since 4.6.2\n\t */\n\tpublic static String getHeader(HttpServletRequest request, String name, Charset charset) {\n\t\tfinal String header = request.getHeader(name);\n\t\tif (null != header) {\n\t\t\treturn CharsetUtil.convert(header, CharsetUtil.CHARSET_ISO_8859_1, charset);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 客户浏览器是否为IE\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 客户浏览器是否为IE\n\t */\n\tpublic static boolean isIE(HttpServletRequest request) {\n\t\tString userAgent = getHeaderIgnoreCase(request, \"User-Agent\");\n\t\tif (StrUtil.isNotBlank(userAgent)) {\n\t\t\t//noinspection ConstantConditions\n\t\t\tuserAgent = userAgent.toUpperCase();\n\t\t\treturn userAgent.contains(\"MSIE\") || userAgent.contains(\"TRIDENT\");\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 是否为GET请求\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为GET请求\n\t */\n\tpublic static boolean isGetMethod(HttpServletRequest request) {\n\t\treturn METHOD_GET.equalsIgnoreCase(request.getMethod());\n\t}\n\n\t/**\n\t * 是否为POST请求\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为POST请求\n\t */\n\tpublic static boolean isPostMethod(HttpServletRequest request) {\n\t\treturn METHOD_POST.equalsIgnoreCase(request.getMethod());\n\t}\n\n\t/**\n\t * 是否为Multipart类型表单，此类型表单用于文件上传\n\t *\n\t * @param request 请求对象{@link HttpServletRequest}\n\t * @return 是否为Multipart类型表单，此类型表单用于文件上传\n\t */\n\tpublic static boolean isMultipart(HttpServletRequest request) {\n\t\tif (false == isPostMethod(request)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString contentType = request.getContentType();\n\t\tif (StrUtil.isBlank(contentType)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn contentType.toLowerCase().startsWith(\"multipart/\");\n\t}\n\t// --------------------------------------------------------- Header end\n\n\t// --------------------------------------------------------- Cookie start\n\n\t/**\n\t * 获得指定的Cookie\n\t *\n\t * @param httpServletRequest {@link HttpServletRequest}\n\t * @param name               cookie名\n\t * @return Cookie对象\n\t */\n\tpublic static Cookie getCookie(HttpServletRequest httpServletRequest, String name) {\n\t\treturn readCookieMap(httpServletRequest).get(name);\n\t}\n\n\t/**\n\t * 将cookie封装到Map里面\n\t *\n\t * @param httpServletRequest {@link HttpServletRequest}\n\t * @return Cookie map\n\t */\n\tpublic static Map<String, Cookie> readCookieMap(HttpServletRequest httpServletRequest) {\n\t\tfinal Cookie[] cookies = httpServletRequest.getCookies();\n\t\tif (ArrayUtil.isEmpty(cookies)) {\n\t\t\treturn MapUtil.empty();\n\t\t}\n\n\t\treturn IterUtil.toMap(\n\t\t\t\tnew ArrayIter<>(httpServletRequest.getCookies()),\n\t\t\t\tnew CaseInsensitiveMap<>(),\n\t\t\t\tCookie::getName);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param cookie   Servlet Cookie对象\n\t */\n\tpublic static void addCookie(HttpServletResponse response, Cookie cookie) {\n\t\tresponse.addCookie(cookie);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param name     Cookie名\n\t * @param value    Cookie值\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value) {\n\t\tresponse.addCookie(new Cookie(name, value));\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie\n\t *\n\t * @param response        响应对象{@link HttpServletResponse}\n\t * @param name            cookie名\n\t * @param value           cookie值\n\t * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. &gt;0 : Cookie存在的秒数.\n\t * @param path            Cookie的有效路径\n\t * @param domain          the domain name within which this cookie is visible; form is according to RFC 2109\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain) {\n\t\tCookie cookie = new Cookie(name, value);\n\t\tif (domain != null) {\n\t\t\tcookie.setDomain(domain);\n\t\t}\n\t\tcookie.setMaxAge(maxAgeInSeconds);\n\t\tcookie.setPath(path);\n\t\taddCookie(response, cookie);\n\t}\n\n\t/**\n\t * 设定返回给客户端的Cookie<br>\n\t * Path: \"/\"<br>\n\t * No Domain\n\t *\n\t * @param response        响应对象{@link HttpServletResponse}\n\t * @param name            cookie名\n\t * @param value           cookie值\n\t * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. &gt;0 : Cookie存在的秒数.\n\t */\n\tpublic static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {\n\t\taddCookie(response, name, value, maxAgeInSeconds, \"/\", null);\n\t}\n\n\t// --------------------------------------------------------- Cookie end\n\t// --------------------------------------------------------- Response start\n\n\t/**\n\t * 获得PrintWriter\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @return 获得PrintWriter\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static PrintWriter getWriter(HttpServletResponse response) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn response.getWriter();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param text        返回的内容\n\t * @param contentType 返回的类型\n\t */\n\tpublic static void write(HttpServletResponse response, String text, String contentType) {\n\t\tresponse.setContentType(contentType);\n\t\tWriter writer = null;\n\t\ttry {\n\t\t\twriter = response.getWriter();\n\t\t\twriter.write(text);\n\t\t\twriter.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 返回文件给客户端\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param file     写出的文件对象\n\t * @since 4.1.15\n\t */\n\tpublic static void write(HttpServletResponse response, File file) {\n\t\tfinal String fileName = file.getName();\n\t\tfinal String contentType = ObjectUtil.defaultIfNull(FileUtil.getMimeType(fileName), \"application/octet-stream\");\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\twrite(response, in, contentType, fileName);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型，可以使用{@link FileUtil#getMimeType(String)}获取对应扩展名的MIME信息\n\t *                    <ul>\n\t *                      <li>application/pdf</li>\n\t *                      <li>application/vnd.ms-excel</li>\n\t *                      <li>application/msword</li>\n\t *                      <li>application/vnd.ms-powerpoint</li>\n\t *                    </ul>\n\t *                    docx、xlsx 这种 office 2007 格式 设置 MIME;网页里面docx 文件是没问题，但是下载下来了之后就变成doc格式了\n\t *                    参考：<a href=\"https://my.oschina.net/shixiaobao17145/blog/32489\">https://my.oschina.net/shixiaobao17145/blog/32489</a>\n\t *                    <ul>\n\t *                      <li>MIME_EXCELX_TYPE = \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";</li>\n\t *                      <li>MIME_PPTX_TYPE = \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";</li>\n\t *                      <li>MIME_WORDX_TYPE = \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";</li>\n\t *                      <li>MIME_STREAM_TYPE = \"application/octet-stream;charset=utf-8\"; #原始字节流</li>\n\t *                    </ul>\n\t * @param fileName    文件名，自动添加双引号\n\t * @since 4.1.15\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, String contentType, String fileName) {\n\t\tfinal String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8);\n\t\tfinal String encodeText = URLUtil.encodeAll(fileName, CharsetUtil.charset(charset));\n\t\tresponse.setHeader(\"Content-Disposition\",\n\t\t\t\tStrUtil.format(\"attachment;filename=\\\"{}\\\";filename*={}''{}\", encodeText, charset, encodeText));\n\t\tresponse.setContentType(contentType);\n\t\twrite(response, in);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response    响应对象{@link HttpServletResponse}\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, String contentType) {\n\t\tresponse.setContentType(contentType);\n\t\twrite(response, in);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param in       需要返回客户端的内容\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in) {\n\t\twrite(response, in, IoUtil.DEFAULT_BUFFER_SIZE);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param response   响应对象{@link HttpServletResponse}\n\t * @param in         需要返回客户端的内容\n\t * @param bufferSize 缓存大小\n\t */\n\tpublic static void write(HttpServletResponse response, InputStream in, int bufferSize) {\n\t\tServletOutputStream out = null;\n\t\ttry {\n\t\t\tout = response.getOutputStream();\n\t\t\tIoUtil.copy(in, out, bufferSize);\n\t\t} catch (IOException e) {\n\t\t\tthrow new UtilException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 设置响应的Header\n\t *\n\t * @param response 响应对象{@link HttpServletResponse}\n\t * @param name     名\n\t * @param value    值，可以是String，Date， int\n\t */\n\tpublic static void setHeader(HttpServletResponse response, String name, Object value) {\n\t\tif (value instanceof String) {\n\t\t\tresponse.setHeader(name, (String) value);\n\t\t} else if (Date.class.isAssignableFrom(value.getClass())) {\n\t\t\tresponse.setDateHeader(name, ((Date) value).getTime());\n\t\t} else if (value instanceof Integer || \"int\".equalsIgnoreCase(value.getClass().getSimpleName())) {\n\t\t\tresponse.setIntHeader(name, (int) value);\n\t\t} else {\n\t\t\tresponse.setHeader(name, value.toString());\n\t\t}\n\t}\n\t// --------------------------------------------------------- Response end\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/servlet/package-info.java",
    "content": "/**\n * Servlet封装，包括Servlet参数获取、文件上传、Response写出等，入口为ServletUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.servlet;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/spring/EnableSpringUtil.java",
    "content": "package cn.hutool.extra.spring;\n\nimport org.springframework.context.annotation.Import;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 启用SpringUtil, 即注入SpringUtil到容器中\n *\n * @author sidian\n */\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Import(SpringUtil.class)\npublic @interface EnableSpringUtil {\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/spring/SpringUtil.java",
    "content": "package cn.hutool.extra.spring;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.util.ArrayUtil;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.ListableBeanFactory;\nimport org.springframework.beans.factory.config.BeanFactoryPostProcessor;\nimport org.springframework.beans.factory.config.ConfigurableListableBeanFactory;\nimport org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.core.ResolvableType;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.ParameterizedType;\nimport java.util.Arrays;\nimport java.util.Map;\n\n/**\n * Spring(Spring boot)工具封装，包括：\n *\n * <ol>\n *     <li>Spring IOC容器中的bean对象获取</li>\n *     <li>注册和注销Bean</li>\n * </ol>\n *\n * @author loolly\n * @since 5.1.0\n */\n@Component\npublic class SpringUtil implements BeanFactoryPostProcessor, ApplicationContextAware {\n\n\t/**\n\t * \"@PostConstruct\"注解标记的类中，由于ApplicationContext还未加载，导致空指针<br>\n\t * 因此实现BeanFactoryPostProcessor注入ConfigurableListableBeanFactory实现bean的操作\n\t */\n\tprivate static ConfigurableListableBeanFactory beanFactory;\n\t/**\n\t * Spring应用上下文环境\n\t */\n\tprivate static ApplicationContext applicationContext;\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {\n\t\tSpringUtil.beanFactory = beanFactory;\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic void setApplicationContext(ApplicationContext applicationContext) {\n\t\tSpringUtil.applicationContext = applicationContext;\n\t}\n\n\t/**\n\t * 获取{@link ApplicationContext}\n\t *\n\t * @return {@link ApplicationContext}\n\t */\n\tpublic static ApplicationContext getApplicationContext() {\n\t\treturn applicationContext;\n\t}\n\n\t/**\n\t * 获取{@link ListableBeanFactory}，可能为{@link ConfigurableListableBeanFactory} 或 {@link ApplicationContextAware}\n\t *\n\t * @return {@link ListableBeanFactory}\n\t * @since 5.7.0\n\t */\n\tpublic static ListableBeanFactory getBeanFactory() {\n\t\tfinal ListableBeanFactory factory = null == beanFactory ? applicationContext : beanFactory;\n\t\tif (null == factory) {\n\t\t\tthrow new UtilException(\"No ConfigurableListableBeanFactory or ApplicationContext injected, maybe not in the Spring environment?\");\n\t\t}\n\t\treturn factory;\n\t}\n\n\t/**\n\t * 获取{@link ConfigurableListableBeanFactory}\n\t *\n\t * @return {@link ConfigurableListableBeanFactory}\n\t * @throws UtilException 当上下文非ConfigurableListableBeanFactory抛出异常\n\t * @since 5.7.7\n\t */\n\tpublic static ConfigurableListableBeanFactory getConfigurableBeanFactory() throws UtilException {\n\t\tfinal ConfigurableListableBeanFactory factory;\n\t\tif (null != beanFactory) {\n\t\t\tfactory = beanFactory;\n\t\t} else if (applicationContext instanceof ConfigurableApplicationContext) {\n\t\t\tfactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();\n\t\t} else {\n\t\t\tthrow new UtilException(\"No ConfigurableListableBeanFactory from context!\");\n\t\t}\n\t\treturn factory;\n\t}\n\n\t//通过name获取 Bean.\n\n\t/**\n\t * 通过name获取 Bean\n\t *\n\t * @param <T>  Bean类型\n\t * @param name Bean名称\n\t * @return Bean\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getBean(String name) {\n\t\treturn (T) getBeanFactory().getBean(name);\n\t}\n\n\t/**\n\t * 通过class获取Bean\n\t *\n\t * @param <T>   Bean类型\n\t * @param clazz Bean类\n\t * @return Bean对象\n\t */\n\tpublic static <T> T getBean(Class<T> clazz) {\n\t\treturn getBeanFactory().getBean(clazz);\n\t}\n\n\t/**\n\t * 通过class获取Bean\n\t *\n\t * @param <T>   Bean类型\n\t * @param clazz Bean类\n\t * @param args  创建bean需要的参数属性\n\t * @return Bean对象\n\t * @since 5.8.34\n\t */\n\tpublic static <T> T getBean(Class<T> clazz, Object... args) {\n\t\treturn getBeanFactory().getBean(clazz, args);\n\t}\n\n\t/**\n\t * 通过name,以及Clazz返回指定的Bean\n\t *\n\t * @param <T>   bean类型\n\t * @param name  Bean名称\n\t * @param clazz bean类型\n\t * @return Bean对象\n\t */\n\tpublic static <T> T getBean(String name, Class<T> clazz) {\n\t\treturn getBeanFactory().getBean(name, clazz);\n\t}\n\n\t/**\n\t * 通过name,以及Clazz返回指定的Bean\n\t *\n\t * @param name Bean名称\n\t * @param args 创建bean需要的参数属性\n\t * @return Bean对象\n\t * @since 5.8.34\n\t */\n\tpublic static Object getBean(String name, Object... args) {\n\t\treturn getBeanFactory().getBean(name, args);\n\t}\n\n\t/**\n\t * 通过类型参考返回带泛型参数的Bean\n\t *\n\t * @param reference 类型参考，用于持有转换后的泛型类型\n\t * @param <T>       Bean类型\n\t * @return 带泛型参数的Bean\n\t * @since 5.4.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getBean(TypeReference<T> reference) {\n\t\tfinal ParameterizedType parameterizedType = (ParameterizedType) reference.getType();\n\t\tfinal Class<T> rawType = (Class<T>) parameterizedType.getRawType();\n\t\tfinal Class<?>[] genericTypes = Arrays.stream(parameterizedType.getActualTypeArguments()).map(type -> (Class<?>) type).toArray(Class[]::new);\n\t\tfinal String[] beanNames = getBeanFactory().getBeanNamesForType(ResolvableType.forClassWithGenerics(rawType, genericTypes));\n\t\treturn getBean(beanNames[0], rawType);\n\t}\n\n\t/**\n\t * 获取指定类型对应的所有Bean，包括子类\n\t *\n\t * @param <T>  Bean类型\n\t * @param type 类、接口，null表示获取所有bean\n\t * @return 类型对应的bean，key是bean注册的name，value是Bean\n\t * @since 5.3.3\n\t */\n\tpublic static <T> Map<String, T> getBeansOfType(Class<T> type) {\n\t\treturn getBeanFactory().getBeansOfType(type);\n\t}\n\n\t/**\n\t * 获取指定类型对应的Bean名称，包括子类\n\t *\n\t * @param type 类、接口，null表示获取所有bean名称\n\t * @return bean名称\n\t * @since 5.3.3\n\t */\n\tpublic static String[] getBeanNamesForType(Class<?> type) {\n\t\treturn getBeanFactory().getBeanNamesForType(type);\n\t}\n\n\t/**\n\t * 获取配置文件配置项的值\n\t *\n\t * @param key 配置项key\n\t * @return 属性值\n\t * @since 5.3.3\n\t */\n\tpublic static String getProperty(String key) {\n\t\tif (null == applicationContext) {\n\t\t\treturn null;\n\t\t}\n\t\treturn applicationContext.getEnvironment().getProperty(key);\n\t}\n\n\t/**\n\t * 获取配置文件配置项的值\n\t *\n\t * @param key          配置项key\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t * @since 5.8.24\n\t */\n\tpublic static String getProperty(String key, String defaultValue) {\n\t\tif (null == applicationContext) {\n\t\t\treturn null;\n\t\t}\n\t\treturn applicationContext.getEnvironment().getProperty(key, defaultValue);\n\t}\n\n\t/**\n\t * 获取配置文件配置项的值\n\t *\n\t * @param <T>          属性值类型\n\t * @param key          配置项key\n\t * @param targetType   配置项类型\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t * @since 5.8.24\n\t */\n\tpublic static <T> T getProperty(String key, Class<T> targetType, T defaultValue) {\n\t\tif (null == applicationContext) {\n\t\t\treturn null;\n\t\t}\n\t\treturn applicationContext.getEnvironment().getProperty(key, targetType, defaultValue);\n\t}\n\n\t/**\n\t * 获取应用程序名称\n\t *\n\t * @return 应用程序名称\n\t * @since 5.7.12\n\t */\n\tpublic static String getApplicationName() {\n\t\treturn getProperty(\"spring.application.name\");\n\t}\n\n\t/**\n\t * 获取当前的环境配置，无配置返回null\n\t *\n\t * @return 当前的环境配置\n\t * @since 5.3.3\n\t */\n\tpublic static String[] getActiveProfiles() {\n\t\tif (null == applicationContext) {\n\t\t\treturn null;\n\t\t}\n\t\treturn applicationContext.getEnvironment().getActiveProfiles();\n\t}\n\n\t/**\n\t * 获取当前的环境配置，当有多个环境配置时，只获取第一个\n\t *\n\t * @return 当前的环境配置\n\t * @since 5.3.3\n\t */\n\tpublic static String getActiveProfile() {\n\t\tfinal String[] activeProfiles = getActiveProfiles();\n\t\treturn ArrayUtil.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;\n\t}\n\n\t/**\n\t * 动态向Spring注册Bean\n\t * <p>\n\t * 由{@link org.springframework.beans.factory.BeanFactory} 实现，通过工具开放API\n\t * <p>\n\t * 更新: shadow 2021-07-29 17:20:44 增加自动注入，修复注册bean无法反向注入的问题\n\t *\n\t * @param <T>      Bean类型\n\t * @param beanName 名称\n\t * @param bean     bean\n\t * @author shadow\n\t * @since 5.4.2\n\t */\n\tpublic static <T> void registerBean(String beanName, T bean) {\n\t\tfinal ConfigurableListableBeanFactory factory = getConfigurableBeanFactory();\n\t\tfactory.autowireBean(bean);\n\t\tfactory.registerSingleton(beanName, bean);\n\t}\n\n\t/**\n\t * 注销bean\n\t * <p>\n\t * 将Spring中的bean注销，请谨慎使用\n\t *\n\t * @param beanName bean名称\n\t * @author shadow\n\t * @since 5.7.7\n\t */\n\tpublic static void unregisterBean(String beanName) {\n\t\tfinal ConfigurableListableBeanFactory factory = getConfigurableBeanFactory();\n\t\tif (factory instanceof DefaultSingletonBeanRegistry) {\n\t\t\tDefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) factory;\n\t\t\tregistry.destroySingleton(beanName);\n\t\t} else {\n\t\t\tthrow new UtilException(\"Can not unregister bean, the factory is not a DefaultSingletonBeanRegistry!\");\n\t\t}\n\t}\n\n\t/**\n\t * 发布事件\n\t *\n\t * @param event 待发布的事件，事件必须是{@link ApplicationEvent}的子类\n\t * @since 5.7.12\n\t */\n\tpublic static void publishEvent(ApplicationEvent event) {\n\t\tif (null != applicationContext) {\n\t\t\tapplicationContext.publishEvent(event);\n\t\t}\n\t}\n\n\t/**\n\t * 发布事件\n\t * Spring 4.2+ 版本事件可以不再是{@link ApplicationEvent}的子类\n\t *\n\t * @param event 待发布的事件\n\t * @since 5.7.21\n\t */\n\tpublic static void publishEvent(Object event) {\n\t\tif (null != applicationContext) {\n\t\t\tapplicationContext.publishEvent(event);\n\t\t}\n\t}\n}\n\n\n\n\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/spring/package-info.java",
    "content": "/**\n * Spring相关工具封装\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.spring;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/ChannelType.java",
    "content": "package cn.hutool.extra.ssh;\n\n/**\n * Jsch支持的Channel类型\n * \n * @author looly\n * @since 4.5.2\n */\npublic enum ChannelType {\n\t/** Session */\n\tSESSION(\"session\"),\n\t/** shell */\n\tSHELL(\"shell\"),\n\t/** exec */\n\tEXEC(\"exec\"),\n\t/** x11 */\n\tX11(\"x11\"),\n\t/** agent forwarding */\n\tAGENT_FORWARDING(\"auth-agent@openssh.com\"),\n\t/** direct tcpip */\n\tDIRECT_TCPIP(\"direct-tcpip\"),\n\t/** forwarded tcpip */\n\tFORWARDED_TCPIP(\"forwarded-tcpip\"),\n\t/** sftp */\n\tSFTP(\"sftp\"),\n\t/** subsystem */\n\tSUBSYSTEM(\"subsystem\");\n\n\t/** channel值 */\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t * \n\t * @param value 类型值\n\t */\n\tChannelType(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取值\n\t * \n\t * @return 值\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/Connector.java",
    "content": "package cn.hutool.extra.ssh;\n\n/**\n * 连接者对象，提供一些连接的基本信息\n * \n * @author Luxiaolei\n *\n */\npublic class Connector {\n\tprivate String host;\n\tprivate int port;\n\tprivate String user;\n\tprivate String password;\n\tprivate String group;\n\n\t// ----------------------------------------------------------------------- 构造 start\n\tpublic Connector() {\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param user 用户名\n\t * @param password 密码\n\t * @param group 组\n\t */\n\tpublic Connector(String user, String password, String group) {\n\t\tthis.user = user;\n\t\tthis.password = password;\n\t\tthis.group = group;\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param host 主机名\n\t * @param port 端口\n\t * @param user 用户名\n\t * @param password 密码\n\t */\n\tpublic Connector(String host, int port, String user, String password) {\n\t\tthis.host = host;\n\t\tthis.port = port;\n\t\tthis.user = user;\n\t\tthis.password = password;\n\t}\n\t// ----------------------------------------------------------------------- 构造 end\n\n\t/**\n\t * 获得主机名\n\t * \n\t * @return 主机名\n\t */\n\tpublic String getHost() {\n\t\treturn host;\n\t}\n\n\t/**\n\t * 设定主机名\n\t * \n\t * @param host 主机名\n\t */\n\tpublic void setHost(String host) {\n\t\tthis.host = host;\n\t}\n\n\t/**\n\t * 获得端口号\n\t * \n\t * @return 端口号\n\t */\n\tpublic int getPort() {\n\t\treturn port;\n\t}\n\n\t/**\n\t * 设定端口号\n\t * \n\t * @param port 端口号\n\t */\n\tpublic void setPort(int port) {\n\t\tthis.port = port;\n\t}\n\n\t/**\n\t * 获得用户名\n\t * \n\t * @return 用户名\n\t */\n\tpublic String getUser() {\n\t\treturn user;\n\t}\n\n\t/**\n\t * 设定用户名\n\t * \n\t * @param name 用户名\n\t */\n\tpublic void setUser(String name) {\n\t\tthis.user = name;\n\t}\n\n\t/**\n\t * 获得密码\n\t * \n\t * @return 密码\n\t */\n\tpublic String getPassword() {\n\t\treturn password;\n\t}\n\n\t/**\n\t * 设定密码\n\t * \n\t * @param password 密码\n\t */\n\tpublic void setPassword(String password) {\n\t\tthis.password = password;\n\t}\n\n\t/**\n\t * 获得用户组名\n\t * \n\t * @return 用户组\n\t */\n\tpublic String getGroup() {\n\t\treturn group;\n\t}\n\n\t/**\n\t * 设定用户组名\n\t * \n\t * @param group 用户组\n\t */\n\tpublic void setGroup(String group) {\n\t\tthis.group = group;\n\t}\n\n\t/**\n\t * toString方法仅用于测试显示\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Connector [host=\" + host + \", port=\" + port + \", user=\" + user + \", password=\" + password + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/GanymedUtil.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport ch.ethz.ssh2.Connection;\nimport ch.ethz.ssh2.Session;\nimport ch.ethz.ssh2.StreamGobbler;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Ganymed-SSH2封装，见：http://www.ganymed.ethz.ch/ssh2/\n *\n * @author looly\n * @since 5.5.3\n */\npublic class GanymedUtil {\n\n\t/**\n\t * 连接到服务器\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @return {@link Connection}\n\t */\n\tpublic static Connection connect(String sshHost, int sshPort) {\n\t\tConnection conn = new Connection(sshHost, sshPort);\n\t\ttry {\n\t\t\tconn.connect();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn conn;\n\t}\n\n\t/**\n\t * 打开远程会话\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名，如果为null，默认root\n\t * @param sshPass 密码\n\t * @return {@link Session}\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\t// 默认root用户\n\t\tif (StrUtil.isEmpty(sshUser)) {\n\t\t\tsshUser = \"root\";\n\t\t}\n\n\t\tfinal Connection connect = connect(sshHost, sshPort);\n\t\ttry {\n\t\t\tconnect.authenticateWithPassword(sshUser, sshPass);\n\t\t\treturn connect.openSession();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行Shell命令（使用EXEC方式）\n\t * <p>\n\t * 此方法单次发送一个命令到服务端，不读取环境变量，执行结束后自动关闭Session，不会产生阻塞。\n\t * </p>\n\t *\n\t * @param session   Session会话\n\t * @param cmd       命令\n\t * @param charset   发送和读取内容的编码\n\t * @param errStream 错误信息输出到的位置\n\t * @return 执行返回结果\n\t */\n\tpublic static String exec(Session session, String cmd, Charset charset, OutputStream errStream) {\n\t\tfinal String result;\n\t\ttry {\n\t\t\tsession.execCommand(cmd, charset.name());\n\t\t\tresult = IoUtil.read(new StreamGobbler(session.getStdout()), charset);\n\n\t\t\t// 错误输出\n\t\t\tIoUtil.copy(new StreamGobbler(session.getStderr()), errStream);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tclose(session);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 执行Shell命令\n\t * <p>\n\t * 此方法单次发送一个命令到服务端，自动读取环境变量，执行结束后自动关闭Session，可能产生阻塞。\n\t * </p>\n\t *\n\t * @param session   Session会话\n\t * @param cmd       命令\n\t * @param charset   发送和读取内容的编码\n\t * @param errStream 错误信息输出到的位置\n\t * @return 执行返回结果\n\t */\n\tpublic static String execByShell(Session session, String cmd, Charset charset, OutputStream errStream) {\n\t\tfinal String result;\n\t\ttry {\n\t\t\tsession.requestDumbPTY();\n\t\t\tIoUtil.write(session.getStdin(), charset, true, cmd);\n\n\t\t\tresult = IoUtil.read(new StreamGobbler(session.getStdout()), charset);\n\t\t\tif(null != errStream){\n\t\t\t\t// 错误输出\n\t\t\t\tIoUtil.copy(new StreamGobbler(session.getStderr()), errStream);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tclose(session);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 关闭会话\n\t *\n\t * @param session 会话通道\n\t */\n\tpublic static void close(Session session) {\n\t\tif (session != null) {\n\t\t\tsession.close();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/JschRuntimeException.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Jsch异常\n * @author xiaoleilu\n */\npublic class JschRuntimeException extends RuntimeException{\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic JschRuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic JschRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic JschRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic JschRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic JschRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic JschRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/JschSessionPool.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.lang.SimpleCache;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\nimport com.jcraft.jsch.Session;\n\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * Jsch会话池\n *\n * @author looly\n */\npublic enum JschSessionPool {\n\n\t/**\n\t * 单例对象\n\t */\n\tINSTANCE;\n\n\t/**\n\t * SSH会话池，key：host，value：Session对象\n\t */\n\tprivate final SimpleCache<String, Session> cache = new SimpleCache<>(new SafeConcurrentHashMap<>());\n\n\t/**\n\t * 获取Session，不存在返回null\n\t *\n\t * @param key 键\n\t * @return Session\n\t */\n\tpublic Session get(String key) {\n\t\treturn cache.get(key);\n\t}\n\n\t/**\n\t * 获得一个SSH跳板机会话，重用已经使用的会话\n\t *\n\t * @param sshHost 跳板机主机\n\t * @param sshPort 跳板机端口\n\t * @param sshUser 跳板机用户名\n\t * @param sshPass 跳板机密码\n\t * @return SSH会话\n\t */\n\tpublic Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\tfinal String key = StrUtil.format(\"{}@{}:{}\", sshUser, sshHost, sshPort);\n\t\treturn this.cache.get(key, Session::isConnected, ()-> JschUtil.openSession(sshHost, sshPort, sshUser, sshPass));\n\t}\n\n\t/**\n\t * 获得一个SSH跳板机会话，重用已经使用的会话\n\t *\n\t * @param sshHost    跳板机主机\n\t * @param sshPort    跳板机端口\n\t * @param sshUser    跳板机用户名\n\t * @param prvkey     跳板机私钥路径\n\t * @param passphrase 跳板机私钥密码\n\t * @return SSH会话\n\t */\n\tpublic Session getSession(String sshHost, int sshPort, String sshUser, String prvkey, byte[] passphrase) {\n\t\tfinal String key = StrUtil.format(\"{}@{}:{}\", sshUser, sshHost, sshPort);\n\t\treturn this.cache.get(key, Session::isConnected, ()->JschUtil.openSession(sshHost, sshPort, sshUser, prvkey, passphrase));\n\t}\n\n\t/**\n\t * 获得一个SSH跳板机会话，重用已经使用的会话\n\t *\n\t * @param sshHost    跳板机主机\n\t * @param sshPort    跳板机端口\n\t * @param sshUser    跳板机用户名\n\t * @param prvkey     跳板机私钥内容\n\t * @param passphrase 跳板机私钥密码\n\t * @return SSH会话\n\t * @since 5.8.18\n\t */\n\tpublic Session getSession(String sshHost, int sshPort, String sshUser, byte[] prvkey, byte[] passphrase) {\n\t\tfinal String key = StrUtil.format(\"{}@{}:{}\", sshUser, sshHost, sshPort);\n\t\treturn this.cache.get(key, Session::isConnected, ()->JschUtil.openSession(sshHost, sshPort, sshUser, prvkey, passphrase));\n\t}\n\n\t/**\n\t * 加入Session\n\t *\n\t * @param key     键\n\t * @param session Session\n\t */\n\tpublic void put(String key, Session session) {\n\t\tthis.cache.put(key, session);\n\t}\n\n\t/**\n\t * 关闭SSH连接会话\n\t *\n\t * @param key 主机，格式为user@host:port\n\t */\n\tpublic void close(String key) {\n\t\tSession session = get(key);\n\t\tif (session != null && session.isConnected()) {\n\t\t\tsession.disconnect();\n\t\t}\n\t\tthis.cache.remove(key);\n\t}\n\n\t/**\n\t * 移除指定Session\n\t *\n\t * @param session Session会话\n\t * @since 4.1.15\n\t */\n\tpublic void remove(Session session) {\n\t\tif (null != session) {\n\t\t\tString key = null;\n\t\t\tfor (Map.Entry<String, Session> entry : cache) {\n\t\t\t\tif (session.equals(entry.getValue())) {\n\t\t\t\t\tkey = entry.getKey();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(null != key){\n\t\t\t\tcache.remove(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 关闭所有SSH连接会话\n\t */\n\tpublic void closeAll() {\n\t\tSession session;\n\t\tfor (Entry<String, Session> entry : this.cache) {\n\t\t\tsession = entry.getValue();\n\t\t\tif (session != null && session.isConnected()) {\n\t\t\t\tsession.disconnect();\n\t\t\t}\n\t\t}\n\t\tcache.clear();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.LocalPortGenerater;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport com.jcraft.jsch.*;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Jsch工具类<br>\n * Jsch是Java Secure Channel的缩写。JSch是一个SSH2的纯Java实现。<br>\n * 它允许你连接到一个SSH服务器，并且可以使用端口转发，X11转发，文件传输等。<br>\n *\n * @author Looly\n * @since 4.0.0\n */\npublic class JschUtil {\n\n\t/**\n\t * 不使用SSH的值\n\t */\n\tpublic final static String SSH_NONE = \"none\";\n\n\t/**\n\t * 本地端口生成器\n\t */\n\tprivate static final LocalPortGenerater portGenerater = new LocalPortGenerater(10000);\n\n\t/**\n\t * 生成一个本地端口，用于远程端口映射\n\t *\n\t * @return 未被使用的本地端口\n\t */\n\tpublic static int generateLocalPort() {\n\t\treturn portGenerater.generate();\n\t}\n\n\t/**\n\t * 获得一个SSH会话，重用已经使用的会话\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t * @return SSH会话\n\t */\n\tpublic static Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\treturn JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, sshPass);\n\t}\n\n\t/**\n\t * 获得一个SSH会话，重用已经使用的会话\n\t *\n\t * @param sshHost        主机\n\t * @param sshPort        端口\n\t * @param sshUser        用户名\n\t * @param privateKeyPath 私钥路径\n\t * @param passphrase     私钥密码\n\t * @return SSH会话\n\t */\n\tpublic static Session getSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) {\n\t\treturn JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, privateKeyPath, passphrase);\n\t}\n\n\t/**\n\t * 获得一个SSH会话，重用已经使用的会话\n\t *\n\t * @param sshHost    主机\n\t * @param sshPort    端口\n\t * @param sshUser    用户名\n\t * @param privateKey 私钥内容\n\t * @param passphrase 私钥密码\n\t * @return SSH会话\n\t * @since 5.8.18\n\t */\n\tpublic static Session getSession(String sshHost, int sshPort, String sshUser, byte[] privateKey, byte[] passphrase) {\n\t\treturn JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, privateKey, passphrase);\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t * @return SSH会话\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\treturn openSession(sshHost, sshPort, sshUser, sshPass, 0);\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t * @param timeout Socket连接超时时长，单位毫秒\n\t * @return SSH会话\n\t * @since 5.3.3\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass, int timeout) {\n\t\tfinal Session session = createSession(sshHost, sshPort, sshUser, sshPass);\n\t\ttry {\n\t\t\tsession.connect(timeout);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost        主机\n\t * @param sshPort        端口\n\t * @param sshUser        用户名\n\t * @param privateKeyPath 私钥的路径\n\t * @param passphrase     私钥文件的密码，可以为null\n\t * @return SSH会话\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) {\n\t\treturn openSession(sshHost, sshPort, sshUser, privateKeyPath, passphrase, 0);\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost    主机\n\t * @param sshPort    端口\n\t * @param sshUser    用户名\n\t * @param privateKey 私钥内容\n\t * @param passphrase 私钥文件的密码，可以为null\n\t * @return SSH会话\n\t * @since 5.8.18\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, byte[] privateKey, byte[] passphrase) {\n\t\treturn openSession(sshHost, sshPort, sshUser, privateKey, passphrase, 0);\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost    主机\n\t * @param sshPort    端口\n\t * @param sshUser    用户名\n\t * @param privateKey 私钥内容\n\t * @param passphrase 私钥文件的密码，可以为null\n\t * @param timeOut    超时时长\n\t * @return SSH会话\n\t * @since 5.8.18\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, byte[] privateKey, byte[] passphrase, int timeOut) {\n\t\tfinal Session session = createSession(sshHost, sshPort, sshUser, privateKey, passphrase);\n\t\ttry {\n\t\t\tsession.connect(timeOut);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 打开一个新的SSH会话\n\t *\n\t * @param sshHost        主机\n\t * @param sshPort        端口\n\t * @param sshUser        用户名\n\t * @param privateKeyPath 私钥的路径\n\t * @param passphrase     私钥文件的密码，可以为null\n\t * @param timeOut        超时时间，单位毫秒\n\t * @return SSH会话\n\t * @since 5.8.4\n\t */\n\tpublic static Session openSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase, int timeOut) {\n\t\tfinal Session session = createSession(sshHost, sshPort, sshUser, privateKeyPath, passphrase);\n\t\ttry {\n\t\t\tsession.connect(timeOut);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 新建一个新的SSH会话，此方法并不打开会话（既不调用connect方法）\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名，如果为null，默认root\n\t * @param sshPass 密码\n\t * @return SSH会话\n\t * @since 4.5.2\n\t */\n\tpublic static Session createSession(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\tfinal JSch jsch = new JSch();\n\t\tfinal Session session = createSession(jsch, sshHost, sshPort, sshUser);\n\n\t\tif (StrUtil.isNotEmpty(sshPass)) {\n\t\t\tsession.setPassword(sshPass);\n\t\t}\n\n\t\treturn session;\n\t}\n\n\t/**\n\t * 新建一个新的SSH会话，此方法并不打开会话（既不调用connect方法）\n\t *\n\t * @param sshHost        主机\n\t * @param sshPort        端口\n\t * @param sshUser        用户名，如果为null，默认root\n\t * @param privateKeyPath 私钥的路径\n\t * @param passphrase     私钥文件的密码，可以为null\n\t * @return SSH会话\n\t * @since 5.0.0\n\t */\n\tpublic static Session createSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) {\n\t\tAssert.notEmpty(privateKeyPath, \"PrivateKey Path must be not empty!\");\n\n\t\tfinal JSch jsch = new JSch();\n\t\ttry {\n\t\t\tjsch.addIdentity(privateKeyPath, passphrase);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\n\t\treturn createSession(jsch, sshHost, sshPort, sshUser);\n\t}\n\n\t/**\n\t * 新建一个新的SSH会话，此方法并不打开会话（既不调用connect方法）\n\t *\n\t * @param sshHost    主机\n\t * @param sshPort    端口\n\t * @param sshUser    用户名，如果为null，默认root\n\t * @param privateKey 私钥内容\n\t * @param passphrase 私钥文件的密码，可以为null\n\t * @return SSH会话\n\t * @since 5.8.18\n\t */\n\tpublic static Session createSession(String sshHost, int sshPort, String sshUser, byte[] privateKey, byte[] passphrase) {\n\t\tAssert.isTrue(privateKey != null && privateKey.length > 0, \"PrivateKey must be not empty!\");\n\n\t\tfinal JSch jsch = new JSch();\n\t\tfinal String identityName = StrUtil.format(\"{}@{}:{}\", sshUser, sshHost, sshPort);\n\t\ttry {\n\t\t\tjsch.addIdentity(identityName, privateKey, null, passphrase);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\n\t\treturn createSession(jsch, sshHost, sshPort, sshUser);\n\t}\n\n\t/**\n\t * 创建一个SSH会话，重用已经使用的会话\n\t *\n\t * @param jsch    {@link JSch}\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名，如果为null，默认root\n\t * @return {@link Session}\n\t * @since 5.0.3\n\t */\n\tpublic static Session createSession(JSch jsch, String sshHost, int sshPort, String sshUser) {\n\t\tAssert.notEmpty(sshHost, \"SSH Host must be not empty!\");\n\t\tAssert.isTrue(sshPort > 0, \"SSH port must be > 0\");\n\n\t\t// 默认root用户\n\t\tif (StrUtil.isEmpty(sshUser)) {\n\t\t\tsshUser = \"root\";\n\t\t}\n\n\t\tif (null == jsch) {\n\t\t\tjsch = new JSch();\n\t\t}\n\n\t\tSession session;\n\t\ttry {\n\t\t\tsession = jsch.getSession(sshUser, sshHost, sshPort);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\n\t\t// 设置第一次登录的时候提示，可选值：(ask | yes | no)\n\t\tsession.setConfig(\"StrictHostKeyChecking\", \"no\");\n\n\t\t// 设置登录认证方式，跳过Kerberos身份验证\n\t\tsession.setConfig(\"PreferredAuthentications\",\"publickey,keyboard-interactive,password\");\n\n\t\treturn session;\n\t}\n\n\t/**\n\t * 绑定端口到本地。 一个会话可绑定多个端口\n\t *\n\t * @param session    需要绑定端口的SSH会话\n\t * @param remoteHost 远程主机\n\t * @param remotePort 远程端口\n\t * @param localPort  本地端口\n\t * @return 成功与否\n\t * @throws JschRuntimeException 端口绑定失败异常\n\t */\n\tpublic static boolean bindPort(Session session, String remoteHost, int remotePort, int localPort) throws JschRuntimeException {\n\t\treturn bindPort(session, remoteHost, remotePort, \"127.0.0.1\", localPort);\n\t}\n\n\t/**\n\t * 绑定端口到本地。 一个会话可绑定多个端口\n\t *\n\t * @param session    需要绑定端口的SSH会话\n\t * @param remoteHost 远程主机\n\t * @param remotePort 远程端口\n\t * @param localHost  本地主机\n\t * @param localPort  本地端口\n\t * @return 成功与否\n\t * @throws JschRuntimeException 端口绑定失败异常\n\t * @since 5.7.8\n\t */\n\tpublic static boolean bindPort(Session session, String remoteHost, int remotePort, String localHost, int localPort) throws JschRuntimeException {\n\t\tif (session != null && session.isConnected()) {\n\t\t\ttry {\n\t\t\t\tsession.setPortForwardingL(localHost, localPort, remoteHost, remotePort);\n\t\t\t} catch (JSchException e) {\n\t\t\t\tthrow new JschRuntimeException(e, \"From [{}:{}] mapping to [{}:{}] error！\", remoteHost, remotePort, localHost, localPort);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\n\t/**\n\t * 绑定ssh服务端的serverPort端口, 到host主机的port端口上. <br>\n\t * 即数据从ssh服务端的serverPort端口, 流经ssh客户端, 达到host:port上.\n\t *\n\t * @param session  与ssh服务端建立的会话\n\t * @param bindPort ssh服务端上要被绑定的端口\n\t * @param host     转发到的host\n\t * @param port     host上的端口\n\t * @return 成功与否\n\t * @throws JschRuntimeException 端口绑定失败异常\n\t * @since 5.4.2\n\t */\n\tpublic static boolean bindRemotePort(Session session, int bindPort, String host, int port) throws JschRuntimeException {\n\t\tif (session != null && session.isConnected()) {\n\t\t\ttry {\n\t\t\t\tsession.setPortForwardingR(bindPort, host, port);\n\t\t\t} catch (JSchException e) {\n\t\t\t\tthrow new JschRuntimeException(e, \"From [{}] mapping to [{}] error！\", bindPort, port);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\n\t/**\n\t * 解除端口映射\n\t *\n\t * @param session   需要解除端口映射的SSH会话\n\t * @param localPort 需要解除的本地端口\n\t * @return 解除成功与否\n\t */\n\tpublic static boolean unBindPort(Session session, int localPort) {\n\t\ttry {\n\t\t\tsession.delPortForwardingL(localPort);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 打开SSH会话，并绑定远程端口到本地的一个随机端口\n\t *\n\t * @param sshConn    SSH连接信息对象\n\t * @param remoteHost 远程主机\n\t * @param remotePort 远程端口\n\t * @return 映射后的本地端口\n\t * @throws JschRuntimeException 连接异常\n\t */\n\tpublic static int openAndBindPortToLocal(Connector sshConn, String remoteHost, int remotePort) throws JschRuntimeException {\n\t\tfinal Session session = openSession(sshConn.getHost(), sshConn.getPort(), sshConn.getUser(), sshConn.getPassword());\n\t\tfinal int localPort = generateLocalPort();\n\t\tbindPort(session, remoteHost, remotePort, localPort);\n\t\treturn localPort;\n\t}\n\n\t/**\n\t * 打开SFTP连接\n\t *\n\t * @param session Session会话\n\t * @return {@link ChannelSftp}\n\t * @since 4.0.3\n\t */\n\tpublic static ChannelSftp openSftp(Session session) {\n\t\treturn openSftp(session, 0);\n\t}\n\n\t/**\n\t * 打开SFTP连接\n\t *\n\t * @param session Session会话\n\t * @param timeout 连接超时时长，单位毫秒\n\t * @return {@link ChannelSftp}\n\t * @since 5.3.3\n\t */\n\tpublic static ChannelSftp openSftp(Session session, int timeout) {\n\t\treturn (ChannelSftp) openChannel(session, ChannelType.SFTP, timeout);\n\t}\n\n\t/**\n\t * 创建Sftp\n\t *\n\t * @param sshHost 远程主机\n\t * @param sshPort 远程主机端口\n\t * @param sshUser 远程主机用户名\n\t * @param sshPass 远程主机密码\n\t * @return {@link Sftp}\n\t * @since 4.0.3\n\t */\n\tpublic static Sftp createSftp(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\treturn new Sftp(sshHost, sshPort, sshUser, sshPass);\n\t}\n\n\t/**\n\t * 创建Sftp\n\t *\n\t * @param session SSH会话\n\t * @return {@link Sftp}\n\t * @since 4.0.5\n\t */\n\tpublic static Sftp createSftp(Session session) {\n\t\treturn new Sftp(session);\n\t}\n\n\t/**\n\t * 打开Shell连接\n\t *\n\t * @param session Session会话\n\t * @return {@link ChannelShell}\n\t * @since 4.0.3\n\t */\n\tpublic static ChannelShell openShell(Session session) {\n\t\treturn (ChannelShell) openChannel(session, ChannelType.SHELL);\n\t}\n\n\t/**\n\t * 打开Channel连接\n\t *\n\t * @param session     Session会话\n\t * @param channelType 通道类型，可以是shell或sftp等，见{@link ChannelType}\n\t * @return {@link Channel}\n\t * @since 4.5.2\n\t */\n\tpublic static Channel openChannel(Session session, ChannelType channelType) {\n\t\treturn openChannel(session, channelType, 0);\n\t}\n\n\t/**\n\t * 打开Channel连接\n\t *\n\t * @param session     Session会话\n\t * @param channelType 通道类型，可以是shell或sftp等，见{@link ChannelType}\n\t * @param timeout     连接超时时长，单位毫秒\n\t * @return {@link Channel}\n\t * @since 5.3.3\n\t */\n\tpublic static Channel openChannel(Session session, ChannelType channelType, int timeout) {\n\t\tfinal Channel channel = createChannel(session, channelType);\n\t\ttry {\n\t\t\tchannel.connect(Math.max(timeout, 0));\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn channel;\n\t}\n\n\t/**\n\t * 创建Channel连接\n\t *\n\t * @param session     Session会话\n\t * @param channelType 通道类型，可以是shell或sftp等，见{@link ChannelType}\n\t * @return {@link Channel}\n\t * @since 4.5.2\n\t */\n\tpublic static Channel createChannel(Session session, ChannelType channelType) {\n\t\tChannel channel;\n\t\ttry {\n\t\t\tif (false == session.isConnected()) {\n\t\t\t\tsession.connect();\n\t\t\t}\n\t\t\tchannel = session.openChannel(channelType.getValue());\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn channel;\n\t}\n\n\t/**\n\t * 执行Shell命令\n\t *\n\t * @param session Session会话\n\t * @param cmd     命令\n\t * @param charset 发送和读取内容的编码\n\t * @return {@link ChannelExec}\n\t * @since 4.0.3\n\t */\n\tpublic static String exec(Session session, String cmd, Charset charset) {\n\t\treturn exec(session, cmd, charset, System.err);\n\t}\n\n\t/**\n\t * 执行Shell命令（使用EXEC方式）\n\t * <p>\n\t * 此方法单次发送一个命令到服务端，不读取环境变量，执行结束后自动关闭channel，不会产生阻塞。\n\t * </p>\n\t *\n\t * @param session   Session会话\n\t * @param cmd       命令\n\t * @param charset   发送和读取内容的编码\n\t * @param errStream 错误信息输出到的位置\n\t * @return 执行结果内容\n\t * @since 4.3.1\n\t */\n\tpublic static String exec(Session session, String cmd, Charset charset, OutputStream errStream) {\n\t\tif (null == charset) {\n\t\t\tcharset = CharsetUtil.CHARSET_UTF_8;\n\t\t}\n\t\tfinal ChannelExec channel = (ChannelExec) createChannel(session, ChannelType.EXEC);\n\t\tchannel.setCommand(StrUtil.bytes(cmd, charset));\n\t\tchannel.setInputStream(null);\n\t\tchannel.setErrStream(errStream);\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tchannel.connect();\n\t\t\tin = channel.getInputStream();\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (JSchException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t\tclose(channel);\n\t\t}\n\t}\n\n\t/**\n\t * 执行Shell命令\n\t * <p>\n\t * 此方法单次发送一个命令到服务端，自动读取环境变量，执行结束后自动关闭channel，不会产生阻塞。\n\t * </p>\n\t *\n\t * @param session Session会话\n\t * @param cmd     命令\n\t * @param charset 发送和读取内容的编码\n\t * @return {@link ChannelExec}\n\t * @since 5.2.5\n\t */\n\tpublic static String execByShell(Session session, String cmd, Charset charset) {\n\t\tfinal ChannelShell shell = openShell(session);\n\t\t// 开始连接\n\t\tshell.setPty(true);\n\t\tOutputStream out = null;\n\t\tInputStream in = null;\n\t\ttry {\n\t\t\tout = shell.getOutputStream();\n\t\t\tin = shell.getInputStream();\n\n\t\t\tout.write(StrUtil.bytes(cmd, charset));\n\t\t\tout.flush();\n\n\t\t\treturn IoUtil.read(in, charset);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tIoUtil.close(in);\n\t\t\tclose(shell);\n\t\t}\n\t}\n\n\t/**\n\t * 关闭SSH连接会话\n\t *\n\t * @param session SSH会话\n\t */\n\tpublic static void close(Session session) {\n\t\tif (session != null && session.isConnected()) {\n\t\t\tsession.disconnect();\n\t\t}\n\t\tJschSessionPool.INSTANCE.remove(session);\n\t}\n\n\t/**\n\t * 关闭会话通道\n\t *\n\t * @param channel 会话通道\n\t * @since 4.0.3\n\t */\n\tpublic static void close(Channel channel) {\n\t\tif (channel != null && channel.isConnected()) {\n\t\t\tchannel.disconnect();\n\t\t}\n\t}\n\n\t/**\n\t * 关闭SSH连接会话\n\t *\n\t * @param key 主机，格式为user@host:port\n\t */\n\tpublic static void close(String key) {\n\t\tJschSessionPool.INSTANCE.close(key);\n\t}\n\n\t/**\n\t * 关闭所有SSH连接会话\n\t */\n\tpublic static void closeAll() {\n\t\tJschSessionPool.INSTANCE.closeAll();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.ftp.AbstractFtp;\nimport cn.hutool.extra.ftp.FtpConfig;\nimport cn.hutool.extra.ftp.FtpException;\nimport com.jcraft.jsch.*;\nimport com.jcraft.jsch.ChannelSftp.LsEntry;\nimport com.jcraft.jsch.ChannelSftp.LsEntrySelector;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Vector;\n\n/**\n * SFTP是Secure File Transfer Protocol的缩写，安全文件传送协议。可以为传输文件提供一种安全的加密方法。<br>\n * SFTP 为 SSH的一部份，是一种传输文件到服务器的安全方式。SFTP是使用加密传输认证信息和传输的数据，所以，使用SFTP是非常安全的。<br>\n * 但是，由于这种传输方式使用了加密/解密技术，所以传输效率比普通的FTP要低得多，如果您对网络安全性要求更高时，可以使用SFTP代替FTP。<br>\n *\n * <p>\n * 此类为基于jsch的SFTP实现<br>\n * 参考：<a href=\"https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html\">https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html</a>\n * </p>\n *\n * @author looly\n * @since 4.0.2\n */\npublic class Sftp extends AbstractFtp {\n\n\tprivate Session session;\n\tprivate ChannelSftp channel;\n\n\t// ---------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 远程主机\n\t * @param sshPort 远程主机端口\n\t * @param sshUser 远程主机用户名\n\t * @param sshPass 远程主机密码\n\t */\n\tpublic Sftp(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\tthis(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 远程主机\n\t * @param sshPort 远程主机端口\n\t * @param sshUser 远程主机用户名\n\t * @param sshPass 远程主机密码\n\t * @param charset 编码\n\t * @since 4.1.14\n\t */\n\tpublic Sftp(String sshHost, int sshPort, String sshUser, String sshPass, Charset charset) {\n\t\tthis(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config FTP配置\n\t * @since 5.3.3\n\t */\n\tpublic Sftp(FtpConfig config) {\n\t\tthis(config, true);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config FTP配置\n\t * @param init   是否立即初始化\n\t * @since 5.8.4\n\t */\n\tpublic Sftp(FtpConfig config, boolean init) {\n\t\tsuper(config);\n\t\tif (init) {\n\t\t\tinit(config);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param session {@link Session}\n\t */\n\tpublic Sftp(Session session) {\n\t\tthis(session, DEFAULT_CHARSET);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param session {@link Session}\n\t * @param charset 编码\n\t * @since 4.1.14\n\t */\n\tpublic Sftp(Session session, Charset charset) {\n\t\tsuper(FtpConfig.create().setCharset(charset).setHost(session.getHost()).setPort(session.getPort()));\n\t\tinit(session, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param session {@link Session}\n\t * @param charset 编码\n\t * @param timeOut 超时时间，单位毫秒\n\t * @since 5.8.4\n\t */\n\tpublic Sftp(Session session, Charset charset, long timeOut) {\n\t\tsuper(FtpConfig.create().setCharset(charset).setConnectionTimeout(timeOut));\n\t\tinit(session, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel {@link ChannelSftp}\n\t * @param charset 编码\n\t * @param timeOut 超时时间，单位毫秒\n\t * @since 5.8.4\n\t */\n\tpublic Sftp(ChannelSftp channel, Charset charset, long timeOut) {\n\t\tsuper(FtpConfig.create().setCharset(charset).setConnectionTimeout(timeOut));\n\t\tinit(channel, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel {@link ChannelSftp}\n\t * @param charset 编码\n\t */\n\tpublic Sftp(ChannelSftp channel, Charset charset) {\n\t\tsuper(FtpConfig.create().setCharset(charset));\n\t\tinit(channel, charset);\n\t}\n\t// ---------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 远程主机\n\t * @param sshPort 远程主机端口\n\t * @param sshUser 远程主机用户名\n\t * @param sshPass 远程主机密码\n\t * @param charset 编码\n\t */\n\tpublic void init(String sshHost, int sshPort, String sshUser, String sshPass, Charset charset) {\n\t\tinit(JschUtil.getSession(sshHost, sshPort, sshUser, sshPass), charset);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @since 5.3.3\n\t */\n\tpublic void init() {\n\t\t// issue#IB69U8 如果用户传入Session对象，则不能使用配置初始化，而是尝试重新连接\n\t\tif(null != this.session){\n\t\t\ttry {\n\t\t\t\tthis.session.connect((int) this.ftpConfig.getConnectionTimeout());\n\t\t\t} catch (JSchException e) {\n\t\t\t\tthrow new JschRuntimeException(e);\n\t\t\t}\n\t\t\tinit(this.session, this.ftpConfig.getCharset());\n\t\t\treturn;\n\t\t}\n\n\t\tinit(this.ftpConfig);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param config FTP配置\n\t * @since 5.3.3\n\t */\n\tpublic void init(FtpConfig config) {\n\t\tinit(config.getHost(), config.getPort(), config.getUser(), config.getPassword(), config.getCharset());\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param session {@link Session}\n\t * @param charset 编码\n\t */\n\tpublic void init(Session session, Charset charset) {\n\t\tthis.session = session;\n\t\tinit(JschUtil.openSftp(session, (int) this.ftpConfig.getConnectionTimeout()), charset);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param channel {@link ChannelSftp}\n\t * @param charset 编码\n\t */\n\tpublic void init(ChannelSftp channel, Charset charset) {\n\t\tthis.ftpConfig.setCharset(charset);\n\t\ttry {\n\t\t\tchannel.setFilenameEncoding(charset.toString());\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\tthis.channel = channel;\n\t}\n\n\t@Override\n\tpublic Sftp reconnectIfTimeout() {\n\t\tif (StrUtil.isBlank(this.ftpConfig.getHost())) {\n\t\t\tthrow new FtpException(\"Host is blank!\");\n\t\t}\n\t\ttry {\n\t\t\tthis.cd(StrUtil.SLASH);\n\t\t} catch (Exception e) {\n\t\t\tclose();\n\t\t\tinit();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取SFTP通道客户端\n\t *\n\t * @return 通道客户端\n\t * @since 4.1.14\n\t */\n\tpublic ChannelSftp getClient() {\n\t\tif(false == this.channel.isConnected()){\n\t\t\tinit();\n\t\t}\n\t\treturn this.channel;\n\t}\n\n\t/**\n\t * 远程当前目录\n\t *\n\t * @return 远程当前目录\n\t */\n\t@Override\n\tpublic String pwd() {\n\t\ttry {\n\t\t\treturn getClient().pwd();\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取HOME路径\n\t *\n\t * @return HOME路径\n\t * @since 4.0.5\n\t */\n\tpublic String home() {\n\t\ttry {\n\t\t\treturn getClient().getHome();\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件或目录，不会递归遍历\n\t *\n\t * @param path 遍历某个目录下所有文件或目录\n\t * @return 目录或文件名列表\n\t * @since 4.0.5\n\t */\n\t@Override\n\tpublic List<String> ls(String path) {\n\t\treturn ls(path, null);\n\t}\n\n\t/**\n\t * 遍历某个目录下所有目录，不会递归遍历\n\t *\n\t * @param path 遍历某个目录下所有目录\n\t * @return 目录名列表\n\t * @since 4.0.5\n\t */\n\tpublic List<String> lsDirs(String path) {\n\t\treturn ls(path, t -> t.getAttrs().isDir());\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件，不会递归遍历\n\t *\n\t * @param path 遍历某个目录下所有文件\n\t * @return 文件名列表\n\t * @since 4.0.5\n\t */\n\tpublic List<String> lsFiles(String path) {\n\t\treturn ls(path, t -> false == t.getAttrs().isDir());\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件或目录，不会递归遍历<br>\n\t * 此方法自动过滤\".\"和\"..\"两种目录\n\t *\n\t * @param path   遍历某个目录下所有文件或目录\n\t * @param filter 文件或目录过滤器，可以实现过滤器返回自己需要的文件或目录名列表\n\t * @return 目录或文件名列表\n\t * @since 4.0.5\n\t */\n\tpublic List<String> ls(String path, final Filter<LsEntry> filter) {\n\t\tfinal List<LsEntry> entries = lsEntries(path, filter);\n\t\tif (CollUtil.isEmpty(entries)) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\t\treturn CollUtil.map(entries, LsEntry::getFilename, true);\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件或目录，生成LsEntry列表，不会递归遍历<br>\n\t * 此方法自动过滤\".\"和\"..\"两种目录\n\t *\n\t * @param path 遍历某个目录下所有文件或目录\n\t * @return 目录或文件名列表\n\t * @since 5.3.5\n\t */\n\tpublic List<LsEntry> lsEntries(String path) {\n\t\treturn lsEntries(path, null);\n\t}\n\n\t/**\n\t * 遍历某个目录下所有文件或目录，生成LsEntry列表，不会递归遍历<br>\n\t * 此方法自动过滤\".\"和\"..\"两种目录\n\t *\n\t * @param path   遍历某个目录下所有文件或目录\n\t * @param filter 文件或目录过滤器，可以实现过滤器返回自己需要的文件或目录名列表\n\t * @return 目录或文件名列表\n\t * @since 5.3.5\n\t */\n\tpublic List<LsEntry> lsEntries(String path, Filter<LsEntry> filter) {\n\t\tfinal List<LsEntry> entryList = new ArrayList<>();\n\t\ttry {\n\t\t\tgetClient().ls(path, entry -> {\n\t\t\t\tfinal String fileName = entry.getFilename();\n\t\t\t\tif (false == StrUtil.equals(\".\", fileName) && false == StrUtil.equals(\"..\", fileName)) {\n\t\t\t\t\tif (null == filter || filter.accept(entry)) {\n\t\t\t\t\t\tentryList.add(entry);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn LsEntrySelector.CONTINUE;\n\t\t\t});\n\t\t} catch (SftpException e) {\n\t\t\tif (false == StrUtil.startWithIgnoreCase(e.getMessage(), \"No such file\")) {\n\t\t\t\tthrow new JschRuntimeException(e);\n\t\t\t}\n\t\t\t// 文件不存在忽略\n\t\t}\n\t\treturn entryList;\n\t}\n\n\t@Override\n\tpublic boolean mkdir(String dir) {\n\t\tif (isDir(dir)) {\n\t\t\t// 目录已经存在，创建直接返回\n\t\t\treturn true;\n\t\t}\n\t\ttry {\n\t\t\tgetClient().mkdir(dir);\n\t\t\treturn true;\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean isDir(String dir) {\n\t\tfinal SftpATTRS sftpATTRS;\n\t\ttry {\n\t\t\tsftpATTRS = getClient().stat(dir);\n\t\t} catch (SftpException e) {\n\t\t\tfinal String msg = e.getMessage();\n\t\t\t// issue#I4P9ED@Gitee\n\t\t\tif (StrUtil.containsAnyIgnoreCase(msg, \"No such file\", \"does not exist\")) {\n\t\t\t\t// 文件不存在直接返回false\n\t\t\t\t// pr#378@Gitee\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t\treturn sftpATTRS.isDir();\n\t}\n\n\t/**\n\t * 打开指定目录，如果指定路径非目录或不存在抛出异常\n\t *\n\t * @param directory directory\n\t * @return 是否打开目录\n\t * @throws FtpException 进入目录失败异常\n\t */\n\t@Override\n\tsynchronized public boolean cd(String directory) throws FtpException {\n\t\tif (StrUtil.isBlank(directory)) {\n\t\t\t// 当前目录\n\t\t\treturn true;\n\t\t}\n\t\ttry {\n\t\t\tgetClient().cd(directory.replace('\\\\', '/'));\n\t\t\treturn true;\n\t\t} catch (SftpException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 删除文件\n\t *\n\t * @param filePath 要删除的文件绝对路径\n\t */\n\t@Override\n\tpublic boolean delFile(String filePath) {\n\t\ttry {\n\t\t\tgetClient().rm(filePath);\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 删除文件夹及其文件夹下的所有文件\n\t *\n\t * @param dirPath 文件夹路径\n\t * @return boolean 是否删除成功\n\t */\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic boolean delDir(String dirPath) {\n\t\tif (false == cd(dirPath)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal ChannelSftp channel = getClient();\n\n\t\tVector<LsEntry> list;\n\t\ttry {\n\t\t\tlist = channel.ls(channel.pwd());\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\n\t\tString fileName;\n\t\tfor (LsEntry entry : list) {\n\t\t\tfileName = entry.getFilename();\n\t\t\tif (false == \".\".equals(fileName) && false == \"..\".equals(fileName)) {\n\t\t\t\tif (entry.getAttrs().isDir()) {\n\t\t\t\t\t// pr#1380Gitee 当目录名包含特殊字符（如 \\u000b）时，会导致不断进入同一目录循环\n\t\t\t\t\t// 此处强制使用绝对路径\n\t\t\t\t\tdelDir(dirPath + \"/\" + fileName);\n\t\t\t\t} else {\n\t\t\t\t\t// pr#1380Gitee 当目录名包含特殊字符（如 \\u000b）时，会导致不断进入同一目录循环\n\t\t\t\t\t// 此处强制使用绝对路径\n\t\t\t\t\tdelFile(dirPath + \"/\" + fileName);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (false == cd(\"..\")) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 删除空目录\n\t\ttry {\n\t\t\tchannel.rmdir(dirPath);\n\t\t\treturn true;\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将本地文件或者文件夹同步（覆盖）上传到远程路径\n\t *\n\t * @param file       文件或者文件夹\n\t * @param remotePath 远程路径\n\t * @since 5.7.6\n\t */\n\tpublic void syncUpload(File file, String remotePath) {\n\t\tif (false == FileUtil.exist(file)) {\n\t\t\treturn;\n\t\t}\n\t\tif (file.isDirectory()) {\n\t\t\tFile[] files = file.listFiles();\n\t\t\tif (files == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (File fileItem : files) {\n\t\t\t\tif (fileItem.isDirectory()) {\n\t\t\t\t\tString mkdir = FileUtil.normalize(remotePath + \"/\" + fileItem.getName());\n\t\t\t\t\tthis.syncUpload(fileItem, mkdir);\n\t\t\t\t} else {\n\t\t\t\t\tthis.syncUpload(fileItem, remotePath);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.mkDirs(remotePath);\n\t\t\tthis.upload(remotePath, file);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean upload(String destPath, File file) {\n\t\tput(FileUtil.getAbsolutePath(file), destPath);\n\t\treturn true;\n\t}\n\n\t/**\n\t * 上传文件到指定目录，可选：\n\t *\n\t * <pre>\n\t * 1. path为null或\"\"上传到当前路径\n\t * 2. path为相对路径则相对于当前路径的子路径\n\t * 3. path为绝对路径则上传到此路径\n\t * </pre>\n\t *\n\t * @param destPath   服务端路径，可以为{@code null} 或者相对路径或绝对路径\n\t * @param fileName   文件名\n\t * @param fileStream 文件流\n\t * @return 是否上传成功\n\t * @since 5.7.16\n\t */\n\tpublic boolean upload(String destPath, String fileName, InputStream fileStream) {\n\t\tAssert.notEmpty(fileName);\n\t\tdestPath = StrUtil.addSuffixIfNot(StrUtil.nullToEmpty(destPath), StrUtil.SLASH) + StrUtil.removePrefix(fileName, StrUtil.SLASH);\n\t\tput(fileStream, destPath, null, Mode.OVERWRITE);\n\t\treturn true;\n\t}\n\n\t/**\n\t * 将本地文件上传到目标服务器，目标文件名为destPath，若destPath为目录，则目标文件名将与srcFilePath文件名相同。覆盖模式\n\t *\n\t * @param srcFilePath 本地文件路径\n\t * @param destPath    目标路径，\n\t * @return this\n\t */\n\tpublic Sftp put(String srcFilePath, String destPath) {\n\t\treturn put(srcFilePath, destPath, Mode.OVERWRITE);\n\t}\n\n\t/**\n\t * 将本地文件上传到目标服务器，目标文件名为destPath，若destPath为目录，则目标文件名将与srcFilePath文件名相同。\n\t *\n\t * @param srcFilePath 本地文件路径\n\t * @param destPath    目标路径，\n\t * @param mode        {@link Mode} 模式\n\t * @return this\n\t */\n\tpublic Sftp put(String srcFilePath, String destPath, Mode mode) {\n\t\treturn put(srcFilePath, destPath, null, mode);\n\t}\n\n\t/**\n\t * 将本地文件上传到目标服务器，目标文件名为destPath，若destPath为目录，则目标文件名将与srcFilePath文件名相同。\n\t *\n\t * @param srcFilePath 本地文件路径\n\t * @param destPath    目标路径，{@code null}表示当前路径\n\t * @param monitor     上传进度监控，通过实现此接口完成进度显示\n\t * @param mode        {@link Mode} 模式\n\t * @return this\n\t * @since 4.6.5\n\t */\n\tpublic Sftp \tput(String srcFilePath, String destPath, SftpProgressMonitor monitor, Mode mode) {\n\t\tif(null == destPath){\n\t\t\tdestPath = pwd();\n\t\t}\n\t\ttry {\n\t\t\tgetClient().put(srcFilePath, destPath, monitor, mode.ordinal());\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将本地数据流上传到目标服务器，目标文件名为destPath，目标必须为文件\n\t *\n\t * @param srcStream 本地的数据流\n\t * @param destPath  目标路径，{@code null}表示当前路径\n\t * @param monitor   上传进度监控，通过实现此接口完成进度显示\n\t * @param mode      {@link Mode} 模式\n\t * @return this\n\t * @since 5.7.16\n\t */\n\tpublic Sftp put(InputStream srcStream, String destPath, SftpProgressMonitor monitor, Mode mode) {\n\t\tif(null == destPath){\n\t\t\tdestPath = pwd();\n\t\t}\n\t\ttry {\n\t\t\tgetClient().put(srcStream, destPath, monitor, mode.ordinal());\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void download(String src, File destFile) {\n\t\tget(src, FileUtil.getAbsolutePath(destFile));\n\t}\n\n\t/**\n\t * 下载文件到{@link OutputStream}中\n\t *\n\t * @param src 源文件路径，包括文件名\n\t * @param out 目标流\n\t * @see #get(String, OutputStream)\n\t */\n\tpublic void download(String src, OutputStream out) {\n\t\tget(src, out);\n\t}\n\n\t/**\n\t * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)\n\t *\n\t * @param sourcePath ftp服务器目录，必须为目录\n\t * @param destDir    本地目录\n\t */\n\t@Override\n\tpublic void recursiveDownloadFolder(String sourcePath, File destDir) throws JschRuntimeException {\n\t\tString fileName;\n\t\tString srcFile;\n\t\tFile destFile;\n\t\tfor (LsEntry item : lsEntries(sourcePath)) {\n\t\t\tfileName = item.getFilename();\n\t\t\tsrcFile = StrUtil.format(\"{}/{}\", sourcePath, fileName);\n\t\t\tdestFile = FileUtil.file(destDir, fileName);\n\n\t\t\tif (false == item.getAttrs().isDir()) {\n\t\t\t\t// 本地不存在文件或者ftp上文件有修改则下载\n\t\t\t\tif (false == FileUtil.exist(destFile)\n\t\t\t\t\t\t|| (item.getAttrs().getMTime() > (destFile.lastModified() / 1000))) {\n\t\t\t\t\tdownload(srcFile, destFile);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 服务端依旧是目录，继续递归\n\t\t\t\tFileUtil.mkdir(destFile);\n\t\t\t\trecursiveDownloadFolder(srcFile, destFile);\n\t\t\t}\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic void rename(String from, String to) {\n\t\ttry {\n\t\t\tgetClient().rename(from, to);\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取远程文件\n\t *\n\t * @param src  远程文件路径\n\t * @param dest 目标文件路径\n\t * @return this\n\t */\n\tpublic Sftp get(String src, String dest) {\n\t\ttry {\n\t\t\tgetClient().get(src, dest);\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取远程文件\n\t *\n\t * @param src 远程文件路径\n\t * @param out 目标流\n\t * @return this\n\t * @since 5.7.0\n\t */\n\tpublic Sftp get(String src, OutputStream out) {\n\t\ttry {\n\t\t\tgetClient().get(src, out);\n\t\t} catch (SftpException e) {\n\t\t\tthrow new JschRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tJschUtil.close(this.channel);\n\t\tthis.channel = null;\n\t\tJschUtil.close(this.session);\n\t\tthis.session = null;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Sftp{\" +\n\t\t\t\t\"host='\" + this.ftpConfig.getHost() + '\\'' +\n\t\t\t\t\", port=\" + this.ftpConfig.getPort() +\n\t\t\t\t\", user='\" + this.ftpConfig.getUser() + '\\'' +\n\t\t\t\t'}';\n\t}\n\n\t/**\n\t * JSch支持的三种文件传输模式\n\t *\n\t * @author looly\n\t */\n\tpublic enum Mode {\n\t\t/**\n\t\t * 完全覆盖模式，这是JSch的默认文件传输模式，即如果目标文件已经存在，传输的文件将完全覆盖目标文件，产生新的文件。\n\t\t */\n\t\tOVERWRITE,\n\t\t/**\n\t\t * 恢复模式，如果文件已经传输一部分，这时由于网络或其他任何原因导致文件传输中断，如果下一次传输相同的文件，则会从上一次中断的地方续传。\n\t\t */\n\t\tRESUME,\n\t\t/**\n\t\t * 追加模式，如果目标文件已存在，传输的文件将在目标文件后追加。\n\t\t */\n\t\tAPPEND\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/SshjSftp.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.ftp.AbstractFtp;\nimport cn.hutool.extra.ftp.FtpConfig;\nimport cn.hutool.extra.ftp.FtpException;\nimport net.schmizz.sshj.SSHClient;\nimport net.schmizz.sshj.connection.channel.direct.Session;\nimport net.schmizz.sshj.sftp.RemoteResourceInfo;\nimport net.schmizz.sshj.sftp.SFTPClient;\nimport net.schmizz.sshj.transport.verification.PromiscuousVerifier;\nimport net.schmizz.sshj.xfer.FileSystemFile;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.util.List;\n\n/**\n * 在使用jsch 进行sftp协议下载文件时，总是中文乱码，而该框架源码又不允许设置编码。故：站在巨人的肩膀上，此类便孕育而出。\n *\n * <p>\n * 基于sshj 框架适配。<br>\n * 参考：https://github.com/hierynomus/sshj\n * </p>\n *\n * @author youyongkun\n * @since 5.7.19\n */\npublic class SshjSftp extends AbstractFtp {\n\n\tprivate SSHClient ssh;\n\tprivate SFTPClient sftp;\n\tprivate Session session;\n\tprivate String workingDir;\n\n\t/**\n\t * 构造，使用默认端口\n\t *\n\t * @param sshHost 主机\n\t */\n\tpublic SshjSftp(String sshHost) {\n\t\tthis(new FtpConfig(sshHost, 22, null, null, DEFAULT_CHARSET));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 主机\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t */\n\tpublic SshjSftp(String sshHost, String sshUser, String sshPass) {\n\t\tthis(new FtpConfig(sshHost, 22, sshUser, sshPass, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t */\n\tpublic SshjSftp(String sshHost, int sshPort, String sshUser, String sshPass) {\n\t\tthis(new FtpConfig(sshHost, sshPort, sshUser, sshPass, CharsetUtil.CHARSET_UTF_8));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param sshHost 主机\n\t * @param sshPort 端口\n\t * @param sshUser 用户名\n\t * @param sshPass 密码\n\t * @param charset 编码\n\t */\n\tpublic SshjSftp(String sshHost, int sshPort, String sshUser, String sshPass, Charset charset) {\n\t\tthis(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config FTP配置\n\t * @since 5.3.3\n\t */\n\tpublic SshjSftp(FtpConfig config) {\n\t\tsuper(config);\n\t\tinit();\n\t}\n\n\t/**\n\t * SSH 初始化并创建一个sftp客户端.\n\t *\n\t * @author youyongkun\n\t * @since 5.7.18\n\t */\n\tpublic void init() {\n\t\tthis.ssh = new SSHClient();\n\t\tssh.addHostKeyVerifier(new PromiscuousVerifier());\n\t\ttry {\n\t\t\tssh.connect(ftpConfig.getHost(), ftpConfig.getPort());\n\t\t\tssh.authPassword(ftpConfig.getUser(), ftpConfig.getPassword());\n\t\t\tssh.setRemoteCharset(ftpConfig.getCharset());\n\t\t\tthis.sftp = ssh.newSFTPClient();\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(\"sftp 初始化失败.\", e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic AbstractFtp reconnectIfTimeout() {\n\t\tif (StrUtil.isBlank(this.ftpConfig.getHost())) {\n\t\t\tthrow new FtpException(\"Host is blank!\");\n\t\t}\n\t\ttry {\n\t\t\tthis.cd(StrUtil.SLASH);\n\t\t} catch (FtpException e) {\n\t\t\tclose();\n\t\t\tinit();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 改变目录，注意目前不支持..\n\t * @param directory directory\n\t * @return true:成功\n\t */\n\t@Override\n\tpublic boolean cd(String directory) {\n\t\tString newPath = getPath(directory);\n\t\ttry {\n\t\t\tsftp.ls(newPath);\n\t\t\tthis.workingDir = newPath;\n\t\t\treturn true;\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String pwd() {\n\t\treturn getPath(null);\n\t}\n\n\t@Override\n\tpublic boolean mkdir(String dir) {\n\t\ttry {\n\t\t\tsftp.mkdir(getPath(dir));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t\treturn containsFile(getPath(dir));\n\t}\n\n\t@Override\n\tpublic List<String> ls(String path) {\n\t\tList<RemoteResourceInfo> infoList;\n\t\ttry {\n\t\t\tinfoList = sftp.ls(getPath(path));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t\tif (CollUtil.isNotEmpty(infoList)) {\n\t\t\treturn CollUtil.map(infoList, RemoteResourceInfo::getName, true);\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic boolean delFile(String path) {\n\t\ttry {\n\t\t\tsftp.rm(getPath(path));\n\t\t\treturn !containsFile(getPath(path));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean delDir(String dirPath) {\n\t\ttry {\n\t\t\tsftp.rmdir(getPath(dirPath));\n\t\t\treturn !containsFile(getPath(dirPath));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean upload(String destPath, File file) {\n\t\ttry {\n\t\t\tif (StrUtil.endWith(destPath, StrUtil.SLASH)) {\n\t\t\t\tdestPath += file.getName();\n\t\t\t}\n\t\t\tsftp.put(new FileSystemFile(file), getPath(destPath));\n\t\t\treturn containsFile(getPath(destPath));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void download(String path, File outFile) {\n\t\ttry {\n\t\t\tsftp.get(getPath(path), new FileSystemFile(outFile));\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void recursiveDownloadFolder(String sourcePath, File destDir) {\n\t\tif (!destDir.exists() || !destDir.isDirectory()) {\n\t\t\tif (!destDir.mkdirs()) {\n\t\t\t\tthrow new FtpException(\"创建目录\" + destDir.getAbsolutePath() + \"失败\");\n\t\t\t}\n\t\t}\n\n\t\tList<String> files = ls(getPath(sourcePath));\n\t\tif (files != null && !files.isEmpty()) {\n\t\t\tfiles.forEach(file -> download(sourcePath + \"/\" + file, FileUtil.file(destDir, file)));\n\t\t}\n\t}\n\n\t@Override\n\tpublic void rename(String from, String to) {\n\t\ttry {\n\t\t\tsftp.rename(from, to);\n\t\t} catch (IOException e) {\n\t\t\tthrow new FtpException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.session);\n\t\tIoUtil.close(this.sftp);\n\t\tIoUtil.close(this.ssh);\n\t}\n\n\t/**\n\t * 是否包含该文件\n\t *\n\t * @param fileDir 文件绝对路径\n\t * @return true:包含 false:不包含\n\t * @author youyongkun\n\t * @since 5.7.18\n\t */\n\tpublic boolean containsFile(String fileDir) {\n\t\ttry {\n\t\t\tsftp.lstat(getPath(fileDir));\n\t\t\treturn true;\n\t\t} catch (IOException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\n\t/**\n\t * 执行Linux 命令\n\t *\n\t * @param exec 命令\n\t * @return 返回响应结果.\n\t * @author youyongkun\n\t * @since 5.7.19\n\t */\n\tpublic String command(String exec) {\n\t\tfinal Session session = this.initSession();\n\n\t\tSession.Command command = null;\n\t\ttry {\n\t\t\tcommand = session.exec(exec);\n\t\t\tInputStream inputStream = command.getInputStream();\n\t\t\treturn IoUtil.read(inputStream, this.ftpConfig.getCharset());\n\t\t} catch (Exception e) {\n\t\t\tthrow new FtpException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(command);\n\t\t}\n\t}\n\n\t/**\n\t * 初始化Session并返回\n\t *\n\t * @return session\n\t */\n\tprivate Session initSession() {\n\t\tSession session = this.session;\n\t\tif (null == session || !session.isOpen()) {\n\t\t\tIoUtil.close(session);\n\t\t\ttry {\n\t\t\t\tsession = this.ssh.startSession();\n\t\t\t} catch (final Exception e) {\n\t\t\t\tthrow new FtpException(e);\n\t\t\t}\n\t\t\tthis.session = session;\n\t\t}\n\t\treturn session;\n\t}\n\n\tprivate String getPath(String path) {\n\t\tif (StrUtil.isBlank(this.workingDir)) {\n\t\t\ttry {\n\t\t\t\tthis.workingDir = sftp.canonicalize(\"\");\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new FtpException(e);\n\t\t\t}\n\t\t}\n\n\t\tif (StrUtil.isBlank(path)) {\n\t\t\treturn this.workingDir;\n\t\t}\n\n\t\t// 如果是绝对路径，则返回\n\t\tif (StrUtil.startWith(path, StrUtil.SLASH)) {\n\t\t\treturn path;\n\t\t} else {\n\t\t\tString tmp = StrUtil.removeSuffix(this.workingDir, StrUtil.SLASH);\n\t\t\treturn StrUtil.format(\"{}/{}\", tmp, path);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/ssh/package-info.java",
    "content": "/**\n * Jsch封装，包括端口映射、SFTP封装等，入口为JschUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.ssh;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/AbstractTemplate.java",
    "content": "package cn.hutool.extra.template;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.StringWriter;\nimport java.util.Map;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\n\n/**\n * 抽象模板，提供将模板融合后写出到文件、返回字符串等方法\n *\n * @author looly\n *\n */\npublic abstract class AbstractTemplate implements Template{\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, File file) {\n\t\tBufferedOutputStream out = null;\n\t\ttry {\n\t\t\tout = FileUtil.getOutputStream(file);\n\t\t\tthis.render(bindingMap, out);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String render(Map<?, ?> bindingMap) {\n\t\tfinal StringWriter writer = new StringWriter();\n\t\trender(bindingMap, writer);\n\t\treturn writer.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/Template.java",
    "content": "package cn.hutool.extra.template;\n\nimport java.io.File;\nimport java.io.OutputStream;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * 抽象模板接口\n * \n * @author looly\n *\n */\npublic interface Template {\n\n\t/**\n\t * 将模板与绑定参数融合后输出到Writer\n\t * \n\t * @param bindingMap 绑定的参数，此Map中的参数会替换模板中的变量\n\t * @param writer 输出\n\t */\n\tvoid render(Map<?, ?> bindingMap, Writer writer);\n\n\t/**\n\t * 将模板与绑定参数融合后输出到流\n\t * \n\t * @param bindingMap 绑定的参数，此Map中的参数会替换模板中的变量\n\t * @param out 输出\n\t */\n\tvoid render(Map<?, ?> bindingMap, OutputStream out);\n\n\t/**\n\t * 写出到文件\n\t * @param bindingMap 绑定的参数，此Map中的参数会替换模板中的变量\n\t * @param file 输出到的文件\n\t */\n\tvoid render(Map<?, ?> bindingMap, File file);\n\n\t/**\n\t * 将模板与绑定参数融合后返回为字符串\n\t * \n\t * @param bindingMap 绑定的参数，此Map中的参数会替换模板中的变量\n\t * @return 融合后的内容\n\t */\n\tString render(Map<?, ?> bindingMap);\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/TemplateConfig.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.util.CharsetUtil;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.Objects;\n\n/**\n * 模板配置\n *\n * @author looly\n * @since 4.1.0\n */\npublic class TemplateConfig implements Serializable {\n\tprivate static final long serialVersionUID = 2933113779920339523L;\n\n\tpublic static final TemplateConfig DEFAULT = new TemplateConfig();\n\n\t/**\n\t * 编码\n\t */\n\tprivate Charset charset;\n\t/**\n\t * 模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t */\n\tprivate String path;\n\t/**\n\t * 模板资源加载方式\n\t */\n\tprivate ResourceMode resourceMode;\n\t/**\n\t * 自定义引擎，当多个jar包引入时，可以自定使用的默认引擎\n\t */\n\tprivate Class<? extends TemplateEngine> customEngine;\n\n\t/**\n\t * 是否使用缓存\n\t */\n\tprivate boolean useCache = true;\n\n\t/**\n\t * 默认构造，使用UTF8编码，默认从ClassPath获取模板\n\t */\n\tpublic TemplateConfig() {\n\t\tthis(null);\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param path 模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t */\n\tpublic TemplateConfig(String path) {\n\t\tthis(path, ResourceMode.STRING);\n\t}\n\n\t/**\n\t * 构造，默认UTF-8编码\n\t *\n\t * @param path         模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t * @param resourceMode 模板资源加载方式\n\t */\n\tpublic TemplateConfig(String path, ResourceMode resourceMode) {\n\t\tthis(CharsetUtil.CHARSET_UTF_8, path, resourceMode);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param charset      编码\n\t * @param path         模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t * @param resourceMode 模板资源加载方式\n\t */\n\tpublic TemplateConfig(Charset charset, String path, ResourceMode resourceMode) {\n\t\tthis.charset = charset;\n\t\tthis.path = path;\n\t\tthis.resourceMode = resourceMode;\n\t}\n\n\t/**\n\t * 获取编码\n\t *\n\t * @return 编码\n\t */\n\tpublic Charset getCharset() {\n\t\treturn charset;\n\t}\n\n\t/**\n\t * 获取编码\n\t *\n\t * @return 编码\n\t * @since 4.1.11\n\t */\n\tpublic String getCharsetStr() {\n\t\tif (null == this.charset) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.charset.toString();\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t */\n\tpublic void setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t}\n\n\t/**\n\t * 获取模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t *\n\t * @return 模板路径\n\t */\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\n\t/**\n\t * 设置模板路径，如果ClassPath或者WebRoot模式，则表示相对路径\n\t *\n\t * @param path 模板路径\n\t */\n\tpublic void setPath(String path) {\n\t\tthis.path = path;\n\t}\n\n\t/**\n\t * 获取模板资源加载方式\n\t *\n\t * @return 模板资源加载方式\n\t */\n\tpublic ResourceMode getResourceMode() {\n\t\treturn resourceMode;\n\t}\n\n\t/**\n\t * 设置模板资源加载方式\n\t *\n\t * @param resourceMode 模板资源加载方式\n\t */\n\tpublic void setResourceMode(ResourceMode resourceMode) {\n\t\tthis.resourceMode = resourceMode;\n\t}\n\n\t/**\n\t * 获取自定义引擎，null表示系统自动判断\n\t *\n\t * @return 自定义引擎，null表示系统自动判断\n\t * @since 5.2.1\n\t */\n\tpublic Class<? extends TemplateEngine> getCustomEngine() {\n\t\treturn customEngine;\n\t}\n\n\n\t/**\n\t * 设置自定义引擎，null表示系统自动判断\n\t *\n\t * @param customEngine 自定义引擎，null表示系统自动判断\n\t * @return this\n\t * @since 5.2.1\n\t */\n\tpublic TemplateConfig setCustomEngine(Class<? extends TemplateEngine> customEngine) {\n\t\tthis.customEngine = customEngine;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否使用缓存\n\t *\n\t * @return 是否使用缓存\n\t * @since 5.8.38\n\t */\n\tpublic boolean isUseCache() {\n\t\treturn useCache;\n\t}\n\n\t/**\n\t * 设置是否使用缓存\n\t *\n\t * @param useCache 是否使用缓存\n\t * @return this\n\t * @since 5.8.38\n\t */\n\tpublic TemplateConfig setUseCache(boolean useCache) {\n\t\tthis.useCache = useCache;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 资源加载方式枚举\n\t *\n\t * @author looly\n\t */\n\tpublic enum ResourceMode {\n\t\t/**\n\t\t * 从ClassPath加载模板\n\t\t */\n\t\tCLASSPATH,\n\t\t/**\n\t\t * 从File目录加载模板\n\t\t */\n\t\tFILE,\n\t\t/**\n\t\t * 从WebRoot目录加载模板\n\t\t */\n\t\tWEB_ROOT,\n\t\t/**\n\t\t * 从模板文本加载模板\n\t\t */\n\t\tSTRING,\n\t\t/**\n\t\t * 复合加载模板（分别从File、ClassPath、Web-root、String方式尝试查找模板）\n\t\t */\n\t\tCOMPOSITE\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o){\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()){\n\t\t\treturn false;\n\t\t}\n\t\tTemplateConfig that = (TemplateConfig) o;\n\t\treturn Objects.equals(charset, that.charset) &&\n\t\t\t\tObjects.equals(path, that.path) &&\n\t\t\t\tresourceMode == that.resourceMode &&\n\t\t\t\tObjects.equals(customEngine, that.customEngine);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(charset, path, resourceMode, customEngine);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/TemplateEngine.java",
    "content": "package cn.hutool.extra.template;\n\n/**\n * 引擎接口，通过实现此接口从而使用对应的模板引擎\n *\n * @author looly\n */\npublic interface TemplateEngine {\n\n\t/**\n\t * 使用指定配置文件初始化模板引擎\n\t *\n\t * @param config 配置文件\n\t * @return this\n\t */\n\tTemplateEngine init(TemplateConfig config);\n\n\t/**\n\t * 获取模板\n\t *\n\t * @param resource 资源，根据实现不同，此资源可以是模板本身，也可以是模板的相对路径\n\t * @return 模板实现\n\t */\n\tTemplate getTemplate(String resource);\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/TemplateException.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 模板异常\n *\n * @author xiaoleilu\n */\npublic class TemplateException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic TemplateException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic TemplateException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic TemplateException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic TemplateException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic TemplateException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic TemplateException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/TemplateUtil.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.extra.template.engine.TemplateFactory;\n\n/**\n * 模板工具类\n * \n * @author looly\n * @since 4.1.0\n */\npublic class TemplateUtil {\n\t\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象，使用默认配置<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t * \n\t * @return {@link TemplateEngine}\n\t * @since 4.1.11\n\t */\n\tpublic static TemplateEngine createEngine() {\n\t\treturn TemplateFactory.create();\n\t}\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t * \n\t * @param config 模板配置，包括编码、模板文件path等信息\n\t * @return {@link TemplateEngine}\n\t */\n\tpublic static TemplateEngine createEngine(TemplateConfig config) {\n\t\treturn TemplateFactory.create(config);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/TemplateFactory.java",
    "content": "package cn.hutool.extra.template.engine;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.ServiceLoaderUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport cn.hutool.extra.template.TemplateException;\nimport cn.hutool.log.StaticLog;\n\n/**\n * 简单模板工厂，用于根据用户引入的模板引擎jar，自动创建对应的模板引擎对象\n *\n * @author looly\n */\npublic class TemplateFactory {\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象<br>\n\t * 获得的是单例的TemplateEngine\n\t *\n\t * @return 单例的TemplateEngine\n\t */\n\tpublic static TemplateEngine get(){\n\t\treturn Singleton.get(TemplateEngine.class.getName(), TemplateFactory::create);\n\t}\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @return {@link TemplateEngine}\n\t * @since 5.3.3\n\t */\n\tpublic static TemplateEngine create() {\n\t\treturn create(new TemplateConfig());\n\t}\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @param config 模板配置，包括编码、模板文件path等信息\n\t * @return {@link TemplateEngine}\n\t */\n\tpublic static TemplateEngine create(TemplateConfig config) {\n\t\tfinal TemplateEngine engine = doCreate(config);\n\t\tStaticLog.debug(\"Use [{}] Engine As Default.\", StrUtil.removeSuffix(engine.getClass().getSimpleName(), \"Engine\"));\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的模板引擎对象<br>\n\t * 推荐创建的引擎单例使用，此方法每次调用会返回新的引擎\n\t *\n\t * @param config 模板配置，包括编码、模板文件path等信息\n\t * @return {@link TemplateEngine}\n\t */\n\tprivate static TemplateEngine doCreate(TemplateConfig config) {\n\t\tfinal Class<? extends TemplateEngine> customEngineClass = config.getCustomEngine();\n\t\tfinal TemplateEngine engine;\n\t\tif(null != customEngineClass){\n\t\t\tengine = ReflectUtil.newInstance(customEngineClass);\n\t\t}else{\n\t\t\tengine = ServiceLoaderUtil.loadFirstAvailable(TemplateEngine.class);\n\t\t}\n\t\tif(null != engine){\n\t\t\treturn engine.init(config);\n\t\t}\n\n\t\tthrow new TemplateException(\"No template found ! Please add one of template jar to your project !\");\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlEngine.java",
    "content": "package cn.hutool.extra.template.engine.beetl;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport org.beetl.core.Configuration;\nimport org.beetl.core.GroupTemplate;\nimport org.beetl.core.ResourceLoader;\nimport org.beetl.core.resource.ClasspathResourceLoader;\nimport org.beetl.core.resource.CompositeResourceLoader;\nimport org.beetl.core.resource.FileResourceLoader;\nimport org.beetl.core.resource.StringTemplateResourceLoader;\nimport org.beetl.core.resource.WebAppResourceLoader;\n\nimport java.io.IOException;\n\n/**\n * Beetl模板引擎封装\n *\n * @author looly\n */\npublic class BeetlEngine implements TemplateEngine {\n\n\tprivate GroupTemplate engine;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 默认构造\n\t */\n\tpublic BeetlEngine() {}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic BeetlEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link GroupTemplate}\n\t */\n\tpublic BeetlEngine(GroupTemplate engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t * @param engine 引擎\n\t */\n\tprivate void init(GroupTemplate engine){\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif(null == this.engine){\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\treturn BeetlTemplate.wrap(engine.getTemplate(resource));\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link GroupTemplate}\n\t * @since 5.8.7\n\t */\n\tpublic GroupTemplate getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link GroupTemplate}\n\t */\n\tprivate static GroupTemplate createEngine(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\n\t\tswitch (config.getResourceMode()) {\n\t\tcase CLASSPATH:\n\t\t\treturn createGroupTemplate(new ClasspathResourceLoader(config.getPath(), config.getCharsetStr()));\n\t\tcase FILE:\n\t\t\treturn createGroupTemplate(new FileResourceLoader(config.getPath(), config.getCharsetStr()));\n\t\tcase WEB_ROOT:\n\t\t\treturn createGroupTemplate(new WebAppResourceLoader(config.getPath(), config.getCharsetStr()));\n\t\tcase STRING:\n\t\t\treturn createGroupTemplate(new StringTemplateResourceLoader());\n\t\tcase COMPOSITE:\n\t\t\t//TODO 需要定义复合资源加载器\n\t\t\treturn createGroupTemplate(new CompositeResourceLoader());\n\t\tdefault:\n\t\t\treturn new GroupTemplate();\n\t\t}\n\t}\n\n\t/**\n\t * 创建自定义的模板组 {@link GroupTemplate}，配置文件使用全局默认<br>\n\t * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置\n\t *\n\t * @param loader {@link ResourceLoader}，资源加载器\n\t * @return {@link GroupTemplate}\n\t * @since 3.2.0\n\t */\n\tprivate static GroupTemplate createGroupTemplate(ResourceLoader<?> loader) {\n\t\ttry {\n\t\t\treturn createGroupTemplate(loader, Configuration.defaultConfiguration());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建自定义的 {@link GroupTemplate}\n\t *\n\t * @param loader {@link ResourceLoader}，资源加载器\n\t * @param conf {@link Configuration} 配置文件\n\t * @return {@link GroupTemplate}\n\t */\n\tprivate static GroupTemplate createGroupTemplate(ResourceLoader<?> loader, Configuration conf) {\n\t\treturn new GroupTemplate(loader, conf);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlTemplate.java",
    "content": "package cn.hutool.extra.template.engine.beetl;\n\nimport cn.hutool.extra.template.AbstractTemplate;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Beetl模板实现\n * \n * @author looly\n */\npublic class BeetlTemplate extends AbstractTemplate implements Serializable{\n\tprivate static final long serialVersionUID = -8157926902932567280L;\n\n\tprivate final org.beetl.core.Template rawTemplate;\n\t\n\t/**\n\t * 包装Beetl模板\n\t * \n\t * @param beetlTemplate Beetl的模板对象 {@link org.beetl.core.Template}\n\t * @return BeetlTemplate\n\t */\n\tpublic static BeetlTemplate wrap(org.beetl.core.Template beetlTemplate) {\n\t\treturn (null == beetlTemplate) ? null : new BeetlTemplate(beetlTemplate);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param beetlTemplate Beetl的模板对象 {@link org.beetl.core.Template}\n\t */\n\tpublic BeetlTemplate(org.beetl.core.Template beetlTemplate) {\n\t\tthis.rawTemplate = beetlTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\trawTemplate.binding(bindingMap);\n\t\trawTemplate.renderTo(writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\trawTemplate.binding(bindingMap);\n\t\trawTemplate.renderTo(out);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/package-info.java",
    "content": "/**\n * Beetl实现，模板引擎介绍见：http://ibeetl.com/\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine.beetl;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyEngine.java",
    "content": "package cn.hutool.extra.template.engine.enjoy;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateConfig.ResourceMode;\nimport cn.hutool.extra.template.TemplateEngine;\nimport com.jfinal.template.source.FileSourceFactory;\n\nimport java.io.File;\n\n/**\n * Enjoy库的引擎包装\n *\n * @author looly\n * @since 4.1.10\n */\npublic class EnjoyEngine implements TemplateEngine {\n\n\tprivate com.jfinal.template.Engine engine;\n\tprivate ResourceMode resourceMode;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic EnjoyEngine() {}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic EnjoyEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link com.jfinal.template.Engine}\n\t */\n\tpublic EnjoyEngine(com.jfinal.template.Engine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tif(null == config){\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\t\tthis.resourceMode = config.getResourceMode();\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t * @param engine 引擎\n\t */\n\tprivate void init(com.jfinal.template.Engine engine){\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif(null == this.engine){\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\tif (ObjectUtil.equal(ResourceMode.STRING, this.resourceMode)) {\n\t\t\treturn EnjoyTemplate.wrap(this.engine.getTemplateByString(resource));\n\t\t}\n\t\treturn EnjoyTemplate.wrap(this.engine.getTemplate(resource));\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link com.jfinal.template.Engine}\n\t * @since 5.8.7\n\t */\n\tpublic com.jfinal.template.Engine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link com.jfinal.template.Engine}\n\t */\n\tprivate static com.jfinal.template.Engine createEngine(TemplateConfig config) {\n\t\tfinal com.jfinal.template.Engine engine = com.jfinal.template.Engine.create(\"Hutool-Enjoy-Engine-\" + IdUtil.fastSimpleUUID());\n\t\tengine.setEncoding(config.getCharsetStr());\n\n\t\tswitch (config.getResourceMode()) {\n\t\t\tcase STRING:\n\t\t\t\t// 默认字符串类型资源:\n\t\t\t\tbreak;\n\t\t\tcase CLASSPATH:\n\t\t\t\tengine.setToClassPathSourceFactory();\n\t\t\t\tengine.setBaseTemplatePath(config.getPath());\n\t\t\t\tbreak;\n\t\t\tcase FILE:\n\t\t\t\tengine.setSourceFactory(new FileSourceFactory());\n\t\t\t\tengine.setBaseTemplatePath(config.getPath());\n\t\t\t\tbreak;\n\t\t\tcase WEB_ROOT:\n\t\t\t\tengine.setSourceFactory(new FileSourceFactory());\n\t\t\t\tfinal File root = FileUtil.file(FileUtil.getWebRoot(), config.getPath());\n\t\t\t\tengine.setBaseTemplatePath(FileUtil.getAbsolutePath(root));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn engine;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyTemplate.java",
    "content": "package cn.hutool.extra.template.engine.enjoy;\n\nimport cn.hutool.extra.template.AbstractTemplate;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Engoy模板实现\n *\n * @author looly\n * @since 4.1.9\n */\npublic class EnjoyTemplate extends AbstractTemplate implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final com.jfinal.template.Template rawTemplate;\n\n\t/**\n\t * 包装Enjoy模板\n\t *\n\t * @param EnjoyTemplate Enjoy的模板对象 {@link com.jfinal.template.Template}\n\t * @return {@link EnjoyTemplate}\n\t */\n\tpublic static EnjoyTemplate wrap(com.jfinal.template.Template EnjoyTemplate) {\n\t\treturn (null == EnjoyTemplate) ? null : new EnjoyTemplate(EnjoyTemplate);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param EnjoyTemplate Enjoy的模板对象 {@link com.jfinal.template.Template}\n\t */\n\tpublic EnjoyTemplate(com.jfinal.template.Template EnjoyTemplate) {\n\t\tthis.rawTemplate = EnjoyTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\trawTemplate.render(bindingMap, writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\trawTemplate.render(bindingMap, out);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/package-info.java",
    "content": "/**\n * Jfinal家的Enjoy模板引擎实现，见：https://jfinal.com/doc/6-1\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine.enjoy;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerEngine.java",
    "content": "package cn.hutool.extra.template.engine.freemarker;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport cn.hutool.extra.template.TemplateException;\nimport freemarker.cache.ClassTemplateLoader;\nimport freemarker.cache.FileTemplateLoader;\nimport freemarker.template.Configuration;\n\nimport java.io.IOException;\n\n/**\n * FreeMarker模板引擎封装<br>\n * 见：<a href=\"https://freemarker.apache.org/\">https://freemarker.apache.org/</a>\n *\n * @author looly\n */\npublic class FreemarkerEngine implements TemplateEngine {\n\n\tprivate Configuration cfg;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic FreemarkerEngine() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic FreemarkerEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param freemarkerCfg {@link Configuration}\n\t */\n\tpublic FreemarkerEngine(Configuration freemarkerCfg) {\n\t\tinit(freemarkerCfg);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\t\tinit(createCfg(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t *\n\t * @param freemarkerCfg Configuration\n\t */\n\tprivate void init(Configuration freemarkerCfg) {\n\t\tthis.cfg = freemarkerCfg;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif (null == this.cfg) {\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\ttry {\n\t\t\treturn FreemarkerTemplate.wrap(this.cfg.getTemplate(resource));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (Exception e) {\n\t\t\tthrow new TemplateException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link Configuration}\n\t * @since 5.8.7\n\t */\n\tpublic Configuration getConfiguration() {\n\t\treturn this.cfg;\n\t}\n\n\t/**\n\t * 创建配置项\n\t *\n\t * @param config 模板配置\n\t * @return {@link Configuration }\n\t */\n\tprivate static Configuration createCfg(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = new TemplateConfig();\n\t\t}\n\n\t\tfinal Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n\t\tcfg.setLocalizedLookup(false);\n\t\tcfg.setDefaultEncoding(config.getCharset().toString());\n\n\t\tswitch (config.getResourceMode()) {\n\t\t\tcase CLASSPATH:\n\t\t\t\tcfg.setTemplateLoader(new ClassTemplateLoader(ClassUtil.getClassLoader(), config.getPath()));\n\t\t\t\tbreak;\n\t\t\tcase FILE:\n\t\t\t\ttry {\n\t\t\t\t\tcfg.setTemplateLoader(new FileTemplateLoader(FileUtil.file(config.getPath())));\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase WEB_ROOT:\n\t\t\t\ttry {\n\t\t\t\t\tcfg.setTemplateLoader(new FileTemplateLoader(FileUtil.file(FileUtil.getWebRoot(), config.getPath())));\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase STRING:\n\t\t\t\tcfg.setTemplateLoader(new SimpleStringTemplateLoader());\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn cfg;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerTemplate.java",
    "content": "package cn.hutool.extra.template.engine.freemarker;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.extra.template.AbstractTemplate;\nimport cn.hutool.extra.template.TemplateException;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\n/**\n * Freemarker模板实现\n *\n * @author looly\n */\npublic class FreemarkerTemplate extends AbstractTemplate implements Serializable{\n\tprivate static final long serialVersionUID = -8157926902932567280L;\n\n\tfreemarker.template.Template rawTemplate;\n\n\t/**\n\t * 包装Freemarker模板\n\t *\n\t * @param beetlTemplate Beetl的模板对象 {@link freemarker.template.Template}\n\t * @return this\n\t */\n\tpublic static FreemarkerTemplate wrap(freemarker.template.Template beetlTemplate) {\n\t\treturn (null == beetlTemplate) ? null : new FreemarkerTemplate(beetlTemplate);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param freemarkerTemplate Beetl的模板对象 {@link freemarker.template.Template}\n\t */\n\tpublic FreemarkerTemplate(freemarker.template.Template freemarkerTemplate) {\n\t\tthis.rawTemplate = freemarkerTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\ttry {\n\t\t\trawTemplate.process(bindingMap, writer);\n\t\t} catch (freemarker.template.TemplateException e) {\n\t\t\tthrow new TemplateException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\trender(bindingMap, IoUtil.getWriter(out, Charset.forName(this.rawTemplate.getEncoding())));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/SimpleStringTemplateLoader.java",
    "content": "package cn.hutool.extra.template.engine.freemarker;\n\nimport freemarker.cache.TemplateLoader;\n\nimport java.io.Reader;\nimport java.io.StringReader;\n\n/**\n * {@link TemplateLoader} 字符串实现形式<br>\n * 用于直接获取字符串模板\n * \n * @author looly\n * @since 4.3.3\n */\npublic class SimpleStringTemplateLoader implements TemplateLoader {\n\n\t@Override\n\tpublic Object findTemplateSource(String name) {\n\t\treturn name;\n\t}\n\n\t@Override\n\tpublic long getLastModified(Object templateSource) {\n\t\treturn System.currentTimeMillis();\n\t}\n\n\t@Override\n\tpublic Reader getReader(Object templateSource, String encoding) {\n\t\treturn new StringReader((String) templateSource);\n\t}\n\n\t@Override\n\tpublic void closeTemplateSource(Object templateSource) {\n\t\t// ignore\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/package-info.java",
    "content": "/**\n * Freemarker实现<br>\n * 见：https://freemarker.apache.org/\n *\n * @author looly\n */\npackage cn.hutool.extra.template.engine.freemarker;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/jetbrick/JetbrickEngine.java",
    "content": "package cn.hutool.extra.template.engine.jetbrick;\n\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport jetbrick.template.JetEngine;\n\nimport java.util.Properties;\n\n/**\n * Jetbrick模板引擎封装<br>\n * 见：https://github.com/subchen/jetbrick-template-2x\n *\n * @author looly\n * @since 5.7.21\n */\npublic class JetbrickEngine implements TemplateEngine {\n\n\tprivate JetEngine engine;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 默认构造\n\t */\n\tpublic JetbrickEngine() {}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic JetbrickEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link JetEngine}\n\t */\n\tpublic JetbrickEngine(JetEngine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t * @param engine 引擎\n\t */\n\tprivate void init(JetEngine engine){\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif(null == this.engine){\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\treturn JetbrickTemplate.wrap(engine.getTemplate(resource));\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link JetEngine}\n\t * @since 5.8.7\n\t */\n\tpublic JetEngine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link JetEngine}\n\t */\n\tprivate static JetEngine createEngine(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\n\t\tProperties props = new Properties();\n\t\tprops.setProperty(\"jetx.input.encoding\", config.getCharsetStr());\n\t\tprops.setProperty(\"jetx.output.encoding\", config.getCharsetStr());\n\t\tprops.setProperty(\"jetx.template.loaders\", \"$loader\");\n\n\t\tswitch (config.getResourceMode()){\n\t\t\tcase CLASSPATH:\n\t\t\t\tprops.setProperty(\"$loader\", \"jetbrick.template.loader.ClasspathResourceLoader\");\n\t\t\t\tprops.setProperty(\"$loader.root\", config.getPath());\n\t\t\t\tbreak;\n\t\t\tcase FILE:\n\t\t\t\tprops.setProperty(\"$loader\", \"jetbrick.template.loader.FileSystemResourceLoader\");\n\t\t\t\tprops.setProperty(\"$loader.root\", config.getPath());\n\t\t\t\tbreak;\n\t\t\tcase WEB_ROOT:\n\t\t\t\tprops.setProperty(\"$loader\", \"jetbrick.template.loader.ServletResourceLoader\");\n\t\t\t\tprops.setProperty(\"$loader.root\", config.getPath());\n\t\t\t\tbreak;\n\t\t\tcase STRING:\n\t\t\t\tprops.setProperty(\"$loader\", \"cn.hutool.extra.template.engine.jetbrick.loader.StringResourceLoader\");\n\t\t\t\tprops.setProperty(\"$loader.charset\", config.getCharsetStr());\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// 默认\n\t\t\t\treturn JetEngine.create();\n\t\t}\n\n\t\treturn JetEngine.create(props);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/jetbrick/JetbrickTemplate.java",
    "content": "package cn.hutool.extra.template.engine.jetbrick;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.extra.template.AbstractTemplate;\nimport jetbrick.template.JetTemplate;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Jetbrick模板实现<br>\n * 见：https://github.com/subchen/jetbrick-template-2x\n *\n * @author looly\n * @since 5.7.21\n */\npublic class JetbrickTemplate extends AbstractTemplate implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final JetTemplate rawTemplate;\n\n\t/**\n\t * 包装Jetbrick模板\n\t *\n\t * @param jetTemplate Jetbrick的模板对象 {@link JetTemplate }\n\t * @return JetbrickTemplate\n\t */\n\tpublic static JetbrickTemplate wrap(JetTemplate jetTemplate) {\n\t\treturn (null == jetTemplate) ? null : new JetbrickTemplate(jetTemplate);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param jetTemplate Jetbrick的模板对象 {@link JetTemplate }\n\t */\n\tpublic JetbrickTemplate(JetTemplate jetTemplate) {\n\t\tthis.rawTemplate = jetTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\trawTemplate.render(map, writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\trawTemplate.render(map, out);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/jetbrick/loader/StringResourceLoader.java",
    "content": "package cn.hutool.extra.template.engine.jetbrick.loader;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport jetbrick.io.resource.AbstractResource;\nimport jetbrick.io.resource.Resource;\nimport jetbrick.io.resource.ResourceNotFoundException;\nimport jetbrick.template.loader.AbstractResourceLoader;\n\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.charset.Charset;\n\n/**\n * 字符串模板加载器\n *\n * @author looly\n * @since 5.7.21\n */\npublic class StringResourceLoader extends AbstractResourceLoader {\n\n\tprivate Charset charset;\n\n\t@Override\n\tpublic Resource load(String name) {\n\t\treturn new StringTemplateResource(name, charset);\n\t}\n\n\t/**\n\t * 设置编码\n\t * @param charset 编码\n\t */\n\tpublic void setCharset(Charset charset){\n\t\tthis.charset = charset;\n\t}\n\n\t/**\n\t * 字符串模板\n\t *\n\t * @author looly\n\t */\n\tstatic class StringTemplateResource extends AbstractResource {\n\n\t\tprivate final String content;\n\t\tprivate final Charset charset;\n\n\t\t/**\n\t\t * 构造\n\t\t * @param content 模板内容\n\t\t * @param charset 编码\n\t\t */\n\t\tpublic StringTemplateResource(String content, Charset charset){\n\t\t\tthis.content = content;\n\t\t\tthis.charset = charset;\n\t\t}\n\n\t\t@Override\n\t\tpublic InputStream openStream() throws ResourceNotFoundException {\n\t\t\treturn IoUtil.toStream(content, charset);\n\t\t}\n\n\t\t@Override\n\t\tpublic URL getURL() {\n\t\t\tthrow new UnsupportedOperationException();\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean exist() {\n\t\t\treturn StrUtil.isEmpty(content);\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn this.content;\n\t\t}\n\n\t\t@Override\n\t\tpublic long lastModified() {\n\t\t\treturn 1;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/jetbrick/loader/package-info.java",
    "content": "/**\n * jetbrick-template实现，特殊资源加载器<br>\n * 模板引擎介绍见：<a href=\"https://github.com/subchen/jetbrick-template-2x\">https://github.com/subchen/jetbrick-template-2x</a>\n *\n * @author looly\n */\npackage cn.hutool.extra.template.engine.jetbrick.loader;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/jetbrick/package-info.java",
    "content": "/**\n * jetbrick-template实现，模板引擎介绍见：https://github.com/subchen/jetbrick-template-2x\n *\n * @author looly\n */\npackage cn.hutool.extra.template.engine.jetbrick;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/package-info.java",
    "content": "/**\n * 第三方模板引擎实现\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmEngine.java",
    "content": "package cn.hutool.extra.template.engine.rythm;\n\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\n\nimport java.util.Properties;\n\n/**\n * Rythm模板引擎<br>\n * 文档：http://rythmengine.org/doc/index\n *\n * @author looly\n *\n */\npublic class RythmEngine implements TemplateEngine {\n\n\torg.rythmengine.RythmEngine engine;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 默认构造\n\t */\n\tpublic RythmEngine() {}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic RythmEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link org.rythmengine.RythmEngine}\n\t */\n\tpublic RythmEngine(org.rythmengine.RythmEngine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tif(null == config){\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link org.rythmengine.RythmEngine}\n\t * @since 5.8.7\n\t */\n\tpublic org.rythmengine.RythmEngine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t * @param engine 引擎\n\t */\n\tprivate void init(org.rythmengine.RythmEngine engine){\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif(null == this.engine){\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\treturn RythmTemplate.wrap(this.engine.getTemplate(resource));\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link org.rythmengine.RythmEngine}\n\t */\n\tprivate static org.rythmengine.RythmEngine createEngine(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = new TemplateConfig();\n\t\t}\n\n\t\tfinal Properties props = new Properties();\n\t\tfinal String path = config.getPath();\n\t\tif (null != path) {\n\t\t\tprops.put(\"home.template\", path);\n\t\t}\n\n\t\treturn new org.rythmengine.RythmEngine(props);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmTemplate.java",
    "content": "package cn.hutool.extra.template.engine.rythm;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.extra.template.AbstractTemplate;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Rythm模板包装\n * \n * @author looly\n *\n */\npublic class RythmTemplate extends AbstractTemplate implements Serializable {\n\tprivate static final long serialVersionUID = -132774960373894911L;\n\n\tprivate final org.rythmengine.template.ITemplate rawTemplate;\n\t\n\t/**\n\t * 包装Rythm模板\n\t * \n\t * @param template Rythm的模板对象 {@link org.rythmengine.template.ITemplate}\n\t * @return {@link RythmTemplate}\n\t */\n\tpublic static RythmTemplate wrap(org.rythmengine.template.ITemplate template) {\n\t\treturn (null == template) ? null : new RythmTemplate(template);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param rawTemplate Velocity模板对象\n\t */\n\tpublic RythmTemplate(org.rythmengine.template.ITemplate rawTemplate) {\n\t\tthis.rawTemplate = rawTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\trawTemplate.__setRenderArgs(map);\n\t\trawTemplate.render(writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\trawTemplate.__setRenderArgs(bindingMap);\n\t\trawTemplate.render(out);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/package-info.java",
    "content": "/**\n * Rythm实现，见：http://www.rythmengine.org/\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine.rythm;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafEngine.java",
    "content": "package cn.hutool.extra.template.engine.thymeleaf;\n\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;\nimport org.thymeleaf.templateresolver.DefaultTemplateResolver;\nimport org.thymeleaf.templateresolver.FileTemplateResolver;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\n\n/**\n * Thymeleaf模板引擎实现\n *\n * @author looly\n * @since 4.1.11\n */\npublic class ThymeleafEngine implements TemplateEngine {\n\n\torg.thymeleaf.TemplateEngine engine;\n\tTemplateConfig config;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic ThymeleafEngine() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic ThymeleafEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link org.thymeleaf.TemplateEngine}\n\t */\n\tpublic ThymeleafEngine(org.thymeleaf.TemplateEngine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\t\tthis.config = config;\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t *\n\t * @param engine 引擎\n\t */\n\tprivate void init(org.thymeleaf.TemplateEngine engine) {\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif (null == this.engine) {\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\treturn ThymeleafTemplate.wrap(this.engine, resource, (null == this.config) ? null : this.config.getCharset());\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link org.thymeleaf.TemplateEngine}\n\t * @since 5.8.7\n\t */\n\tpublic org.thymeleaf.TemplateEngine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link TemplateEngine}\n\t */\n\tprivate static org.thymeleaf.TemplateEngine createEngine(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = new TemplateConfig();\n\t\t}\n\n\t\tITemplateResolver resolver;\n\t\tswitch (config.getResourceMode()) {\n\t\t\tcase CLASSPATH:\n\t\t\t\tfinal ClassLoaderTemplateResolver classLoaderResolver = new ClassLoaderTemplateResolver();\n\t\t\t\tclassLoaderResolver.setCharacterEncoding(config.getCharsetStr());\n\t\t\t\tclassLoaderResolver.setTemplateMode(TemplateMode.HTML);\n\t\t\t\tclassLoaderResolver.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), \"/\"));\n\t\t\t\tclassLoaderResolver.setCacheable(config.isUseCache());\n\t\t\t\tresolver = classLoaderResolver;\n\t\t\t\tbreak;\n\t\t\tcase FILE:\n\t\t\t\tfinal FileTemplateResolver fileResolver = new FileTemplateResolver();\n\t\t\t\tfileResolver.setCharacterEncoding(config.getCharsetStr());\n\t\t\t\tfileResolver.setTemplateMode(TemplateMode.HTML);\n\t\t\t\tfileResolver.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), \"/\"));\n\t\t\t\tfileResolver.setCacheable(config.isUseCache());\n\t\t\t\tresolver = fileResolver;\n\t\t\t\tbreak;\n\t\t\tcase WEB_ROOT:\n\t\t\t\tfinal FileTemplateResolver webRootResolver = new FileTemplateResolver();\n\t\t\t\twebRootResolver.setCharacterEncoding(config.getCharsetStr());\n\t\t\t\twebRootResolver.setTemplateMode(TemplateMode.HTML);\n\t\t\t\twebRootResolver.setPrefix(StrUtil.addSuffixIfNot(FileUtil.getAbsolutePath(FileUtil.file(FileUtil.getWebRoot(), config.getPath())), \"/\"));\n\t\t\t\twebRootResolver.setCacheable(config.isUseCache());\n\t\t\t\tresolver = webRootResolver;\n\t\t\t\tbreak;\n\t\t\tcase STRING:\n\t\t\t\tresolver = new StringTemplateResolver();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tresolver = new DefaultTemplateResolver();\n\t\t\t\tbreak;\n\t\t}\n\n\t\tfinal org.thymeleaf.TemplateEngine engine = new org.thymeleaf.TemplateEngine();\n\t\tengine.setTemplateResolver(resolver);\n\t\treturn engine;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafTemplate.java",
    "content": "package cn.hutool.extra.template.engine.thymeleaf;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.extra.template.AbstractTemplate;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.nio.charset.Charset;\nimport java.util.Locale;\nimport java.util.Map;\n\n/**\n * Thymeleaf模板实现\n * \n * @author looly\n * @since 4.1.11\n */\npublic class ThymeleafTemplate extends AbstractTemplate implements Serializable {\n\tprivate static final long serialVersionUID = 781284916568562509L;\n\n\tprivate final TemplateEngine engine;\n\tprivate final String template;\n\tprivate final Charset charset;\n\n\t/**\n\t * 包装Thymeleaf模板\n\t * \n\t * @param engine Thymeleaf的模板引擎对象 {@link TemplateEngine}\n\t * @param template 模板路径或模板内容\n\t * @param charset 编码\n\t * @return {@link ThymeleafTemplate}\n\t */\n\tpublic static ThymeleafTemplate wrap(TemplateEngine engine, String template, Charset charset) {\n\t\treturn (null == engine) ? null : new ThymeleafTemplate(engine, template, charset);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param engine Thymeleaf的模板对象 {@link TemplateEngine}\n\t * @param template 模板路径或模板内容\n\t * @param charset 编码\n\t */\n\tpublic ThymeleafTemplate(TemplateEngine engine, String template, Charset charset) {\n\t\tthis.engine = engine;\n\t\tthis.template = template;\n\t\tthis.charset = ObjectUtil.defaultIfNull(charset, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\tfinal Context context = new Context(Locale.getDefault(), map);\n\t\tthis.engine.process(this.template, context, writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\trender(bindingMap, IoUtil.getWriter(out, this.charset));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/package-info.java",
    "content": "/**\n * Thymeleaf实现，见：https://www.thymeleaf.org/\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine.thymeleaf;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/SimpleStringResourceLoader.java",
    "content": "package cn.hutool.extra.template.engine.velocity;\n\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.StringReader;\n\nimport org.apache.velocity.exception.ResourceNotFoundException;\nimport org.apache.velocity.runtime.resource.Resource;\nimport org.apache.velocity.runtime.resource.loader.ResourceLoader;\nimport org.apache.velocity.util.ExtProperties;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.CharsetUtil;\n\n/**\n * {@link ResourceLoader} 的字符串实现形式<br>\n * 用于直接获取字符串模板\n * \n * @author looly\n *\n */\npublic class SimpleStringResourceLoader extends ResourceLoader {\n\n\t@Override\n\tpublic void init(ExtProperties configuration) {\n\t}\n\n\t/**\n\t * 获取资源流\n\t * \n\t * @param source 字符串模板\n\t * @return 流\n\t * @throws ResourceNotFoundException 资源未找到\n\t */\n\tpublic InputStream getResourceStream(String source) throws ResourceNotFoundException {\n\t\treturn IoUtil.toStream(source, CharsetUtil.CHARSET_UTF_8);\n\t}\n\t\n\t@Override\n\tpublic Reader getResourceReader(String source, String encoding) throws ResourceNotFoundException {\n\t\treturn new StringReader(source);\n\t}\n\n\t@Override\n\tpublic boolean isSourceModified(Resource resource) {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic long getLastModified(Resource resource) {\n\t\treturn 0;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityEngine.java",
    "content": "package cn.hutool.extra.template.engine.velocity;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport org.apache.velocity.app.Velocity;\n\n/**\n * Velocity模板引擎<br>\n * 见：<a href=\"http://velocity.apache.org/\">http://velocity.apache.org/</a>\n *\n * @author looly\n */\npublic class VelocityEngine implements TemplateEngine {\n\n\tprivate org.apache.velocity.app.VelocityEngine engine;\n\tprivate TemplateConfig config;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 默认构造\n\t */\n\tpublic VelocityEngine() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic VelocityEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link org.apache.velocity.app.VelocityEngine}\n\t */\n\tpublic VelocityEngine(org.apache.velocity.app.VelocityEngine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = TemplateConfig.DEFAULT;\n\t\t}\n\t\tthis.config = config;\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t *\n\t * @param engine 引擎\n\t */\n\tprivate void init(org.apache.velocity.app.VelocityEngine engine) {\n\t\tthis.engine = engine;\n\t}\n\n\t/**\n\t * 获取原始的引擎对象\n\t *\n\t * @return 原始引擎对象\n\t * @since 5.5.8\n\t */\n\tpublic org.apache.velocity.app.VelocityEngine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif (null == this.engine) {\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\n\t\t// 目录前缀\n\t\tString root;\n\t\t// 自定义编码\n\t\tString charsetStr = null;\n\t\tif (null != this.config) {\n\t\t\troot = this.config.getPath();\n\t\t\tcharsetStr = this.config.getCharsetStr();\n\n\t\t\t// 修正template目录，在classpath或者web_root模式下，按照配置添加默认前缀\n\t\t\t// 如果用户已经自行添加了前缀，则忽略之\n\t\t\tfinal TemplateConfig.ResourceMode resourceMode = this.config.getResourceMode();\n\t\t\tif (TemplateConfig.ResourceMode.CLASSPATH == resourceMode\n\t\t\t\t\t|| TemplateConfig.ResourceMode.WEB_ROOT == resourceMode) {\n\t\t\t\tresource = StrUtil.addPrefixIfNot(resource, StrUtil.addSuffixIfNot(root, \"/\"));\n\t\t\t}\n\t\t}\n\n\t\treturn VelocityTemplate.wrap(engine.getTemplate(resource, charsetStr));\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link org.apache.velocity.app.VelocityEngine}\n\t */\n\tprivate static org.apache.velocity.app.VelocityEngine createEngine(TemplateConfig config) {\n\t\tif (null == config) {\n\t\t\tconfig = new TemplateConfig();\n\t\t}\n\n\t\tfinal org.apache.velocity.app.VelocityEngine ve = new org.apache.velocity.app.VelocityEngine();\n\t\t// 编码\n\t\tfinal String charsetStr = config.getCharset().toString();\n\t\tve.setProperty(Velocity.INPUT_ENCODING, charsetStr);\n\t\t// ve.setProperty(Velocity.OUTPUT_ENCODING, charsetStr);\n\t\tif(config.isUseCache()){\n\t\t\t// issue#IC3JRY 可定制是否使用缓存\n\t\t\tve.setProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, true); // 使用缓存\n\t\t}\n\n\t\t// loader\n\t\tswitch (config.getResourceMode()) {\n\t\t\tcase CLASSPATH:\n\t\t\t\t// 新版Velocity弃用\n//\t\t\tve.setProperty(\"file.resource.loader.class\", \"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader\");\n\t\t\t\tve.setProperty(\"resource.loader.file.class\", \"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader\");\n\t\t\t\tbreak;\n\t\t\tcase FILE:\n\t\t\t\t// path\n\t\t\t\tfinal String path = config.getPath();\n\t\t\t\tif (null != path) {\n\t\t\t\t\tve.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, path);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase WEB_ROOT:\n\t\t\t\tve.setProperty(Velocity.RESOURCE_LOADERS, \"webapp\");\n\t\t\t\tve.setProperty(\"webapp.resource.loader.class\", \"org.apache.velocity.tools.view.servlet.WebappLoader\");\n\t\t\t\tve.setProperty(\"webapp.resource.loader.path\", StrUtil.nullToDefault(config.getPath(), StrUtil.SLASH));\n\t\t\t\tbreak;\n\t\t\tcase STRING:\n\t\t\t\tve.setProperty(Velocity.RESOURCE_LOADERS, \"str\");\n\t\t\t\tve.setProperty(\"resource.loader.str.class\", SimpleStringResourceLoader.class.getName());\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\n\t\tve.init();\n\t\treturn ve;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityTemplate.java",
    "content": "package cn.hutool.extra.template.engine.velocity;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.AbstractTemplate;\nimport org.apache.velocity.VelocityContext;\nimport org.apache.velocity.app.Velocity;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Velocity模板包装\n *\n * @author looly\n *\n */\npublic class VelocityTemplate extends AbstractTemplate implements Serializable {\n\tprivate static final long serialVersionUID = -132774960373894911L;\n\n\tprivate final org.apache.velocity.Template rawTemplate;\n\tprivate String charset;\n\n\t/**\n\t * 包装Velocity模板\n\t *\n\t * @param template Velocity的模板对象 {@link org.apache.velocity.Template}\n\t * @return VelocityTemplate\n\t */\n\tpublic static VelocityTemplate wrap(org.apache.velocity.Template template) {\n\t\treturn (null == template) ? null : new VelocityTemplate(template);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rawTemplate Velocity模板对象\n\t */\n\tpublic VelocityTemplate(org.apache.velocity.Template rawTemplate) {\n\t\tthis.rawTemplate = rawTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\trawTemplate.merge(toContext(bindingMap), writer);\n\t\tIoUtil.flush(writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\tif(null == charset) {\n\t\t\tloadEncoding();\n\t\t}\n\t\trender(bindingMap, IoUtil.getWriter(out, CharsetUtil.charset(this.charset)));\n\t}\n\n\t/**\n\t * 将Map转为VelocityContext\n\t *\n\t * @param bindingMap 参数绑定的Map\n\t * @return {@link VelocityContext}\n\t */\n\tprivate VelocityContext toContext(Map<?, ?> bindingMap) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\treturn new VelocityContext(map);\n\t}\n\n\t/**\n\t * 加载可用的Velocity中预定义的编码\n\t */\n\tprivate void loadEncoding() {\n\t\tfinal String charset = (String) Velocity.getProperty(Velocity.INPUT_ENCODING);\n\t\tthis.charset = StrUtil.isEmpty(charset) ? CharsetUtil.UTF_8 : charset;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/package-info.java",
    "content": "/**\n * Velocity实现<br>\n * 见：http://velocity.apache.org/\n *\n * @author looly\n */\npackage cn.hutool.extra.template.engine.velocity;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/wit/WitEngine.java",
    "content": "package cn.hutool.extra.template.engine.wit;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.extra.template.Template;\nimport cn.hutool.extra.template.TemplateConfig;\nimport cn.hutool.extra.template.TemplateEngine;\nimport cn.hutool.extra.template.TemplateException;\nimport org.febit.wit.Engine;\nimport org.febit.wit.exceptions.ResourceNotFoundException;\nimport org.febit.wit.util.Props;\n\nimport java.io.File;\n\n/**\n * Wit(http://zqq90.github.io/webit-script/)模板引擎封装\n *\n * @author looly\n */\npublic class WitEngine implements TemplateEngine {\n\n\tprivate Engine engine;\n\n\t// --------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 默认构造\n\t */\n\tpublic WitEngine() {}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config 模板配置\n\t */\n\tpublic WitEngine(TemplateConfig config) {\n\t\tinit(config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param engine {@link Engine}\n\t */\n\tpublic WitEngine(Engine engine) {\n\t\tinit(engine);\n\t}\n\t// --------------------------------------------------------------------------------- Constructor end\n\n\n\t@Override\n\tpublic TemplateEngine init(TemplateConfig config) {\n\t\tinit(createEngine(config));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化引擎\n\t * @param engine 引擎\n\t */\n\tprivate void init(Engine engine){\n\t\tthis.engine = engine;\n\t}\n\n\t@Override\n\tpublic Template getTemplate(String resource) {\n\t\tif(null == this.engine){\n\t\t\tinit(TemplateConfig.DEFAULT);\n\t\t}\n\t\ttry {\n\t\t\treturn WitTemplate.wrap(engine.getTemplate(resource));\n\t\t} catch (ResourceNotFoundException e) {\n\t\t\tthrow new TemplateException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取原始引擎的钩子方法，用于自定义特殊属性，如插件等\n\t *\n\t * @return {@link Engine}\n\t * @since 5.8.7\n\t */\n\tpublic Engine getRawEngine() {\n\t\treturn this.engine;\n\t}\n\n\t/**\n\t * 创建引擎\n\t *\n\t * @param config 模板配置\n\t * @return {@link Engine}\n\t */\n\tprivate static Engine createEngine(TemplateConfig config) {\n\t\tfinal Props configProps = Engine.createConfigProps(\"\");\n\t\tDict dict = null;\n\n\t\tif (null != config) {\n\t\t\tdict = Dict.create();\n\t\t\t// 自定义编码\n\t\t\tdict.set(\"DEFAULT_ENCODING\", config.getCharset());\n\n\t\t\tswitch (config.getResourceMode()){\n\t\t\t\tcase CLASSPATH:\n\t\t\t\t\tconfigProps.set(\"pathLoader.root\", config.getPath());\n\t\t\t\t\tconfigProps.set(\"routeLoader.defaultLoader\", \"classpathLoader\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase STRING:\n\t\t\t\t\tconfigProps.set(\"routeLoader.defaultLoader\", \"stringLoader\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase FILE:\n\t\t\t\t\tconfigProps.set(\"pathLoader.root\", config.getPath());\n\t\t\t\t\tconfigProps.set(\"routeLoader.defaultLoader\", \"fileLoader\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase WEB_ROOT:\n\t\t\t\t\tfinal File root = FileUtil.file(FileUtil.getWebRoot(), config.getPath());\n\t\t\t\t\tconfigProps.set(\"pathLoader.root\", FileUtil.getAbsolutePath(root));\n\t\t\t\t\tconfigProps.set(\"routeLoader.defaultLoader\", \"fileLoader\");\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn Engine.create(configProps,dict);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/wit/WitTemplate.java",
    "content": "package cn.hutool.extra.template.engine.wit;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.extra.template.AbstractTemplate;\nimport org.febit.wit.Template;\n\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.io.Writer;\nimport java.util.Map;\n\n/**\n * Wit模板实现\n *\n * @author looly\n */\npublic class WitTemplate extends AbstractTemplate implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Template rawTemplate;\n\n\t/**\n\t * 包装Wit模板\n\t *\n\t * @param witTemplate Wit的模板对象 {@link Template}\n\t * @return WitTemplate\n\t */\n\tpublic static WitTemplate wrap(Template witTemplate) {\n\t\treturn (null == witTemplate) ? null : new WitTemplate(witTemplate);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param witTemplate Wit的模板对象 {@link Template}\n\t */\n\tpublic WitTemplate(Template witTemplate) {\n\t\tthis.rawTemplate = witTemplate;\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, Writer writer) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\trawTemplate.merge(map, writer);\n\t}\n\n\t@Override\n\tpublic void render(Map<?, ?> bindingMap, OutputStream out) {\n\t\tfinal Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);\n\t\trawTemplate.merge(map, out);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/engine/wit/package-info.java",
    "content": "/**\n * Wit实现，见：http://zqq90.github.io/webit-script/\n *\n * @author looly\n *\n */\npackage cn.hutool.extra.template.engine.wit;\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/template/package-info.java",
    "content": "/**\n * 第三方模板引擎封装，提供统一的接口用于适配第三方模板引擎\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.template;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/AbstractResult.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport cn.hutool.core.collection.ComputeIter;\n\nimport java.util.Iterator;\n\n/**\n * 对于未实现{@link Iterator}接口的普通结果类，装饰为{@link Result}<br>\n * 普通的结果类只需实现{@link #nextWord()} 即可\n *\n * @author looly\n *\n */\npublic abstract class AbstractResult extends ComputeIter<Word> implements Result{\n\n\t/**\n\t * 下一个单词，通过实现此方法获取下一个单词，null表示无下一个结果。\n\t * @return 下一个单词或null\n\t */\n\tprotected abstract Word nextWord();\n\n\t@Override\n\tprotected Word computeNext() {\n\t\treturn nextWord();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Result.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport cn.hutool.core.collection.IterableIter;\n\n/**\n * 分词结果接口定义<br>\n * 实现此接口包装分词器的分词结果，通过实现Iterator相应方法获取分词中的单词\n *\n * @author looly\n *\n */\npublic interface Result extends IterableIter<Word> {\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerEngine.java",
    "content": "package cn.hutool.extra.tokenizer;\n\n/**\n * 分词引擎接口定义，用户通过实现此接口完成特定分词引擎的适配\n * \n * @author looly\n *\n */\npublic interface TokenizerEngine {\n\t\n\t/**\n\t * 文本分词处理接口，通过实现此接口完成分词，产生分词结果\n\t * \n\t * @param text 需要分词的文本\n\t * @return {@link Result}分词结果实现\n\t */\n\tResult parse(CharSequence text);\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerException.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 分词异常\n *\n * @author Looly\n */\npublic class TokenizerException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8074865854534335463L;\n\n\tpublic TokenizerException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic TokenizerException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic TokenizerException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic TokenizerException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic TokenizerException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic TokenizerException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerUtil.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport cn.hutool.extra.tokenizer.engine.TokenizerFactory;\n\n/**\n * 分词工具类\n * \n * @author looly\n * @since 4.3.3\n */\npublic class TokenizerUtil {\n\n\t/**\n\t * 根据用户引入的分词引擎jar，自动创建对应的分词引擎对象\n\t * \n\t * @return {@link TokenizerEngine}\n\t */\n\tpublic static TokenizerEngine createEngine() {\n\t\treturn TokenizerFactory.create();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Word.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport java.io.Serializable;\n\n/**\n * 表示分词中的一个词\n * \n * @author looly\n *\n */\npublic interface Word extends Serializable {\n\n\t/**\n\t * 获取单词文本\n\t * \n\t * @return 单词文本\n\t */\n\tString getText();\n\n\t/**\n\t * 获取本词的起始位置\n\t * \n\t * @return 起始位置\n\t */\n\tint getStartOffset();\n\n\t/**\n\t * 获取本词的结束位置\n\t * \n\t * @return 结束位置\n\t */\n\tint getEndOffset();\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/TokenizerFactory.java",
    "content": "package cn.hutool.extra.tokenizer.engine;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.ServiceLoaderUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport cn.hutool.log.StaticLog;\n\n/**\n * 简单分词引擎工厂，用于根据用户引入的分词引擎jar，自动创建对应的引擎\n * \n * @author looly\n *\n */\npublic class TokenizerFactory {\n\n\t/**\n\t * 根据用户引入的模板引擎jar，自动创建对应的分词引擎对象<br>\n\t * 获得的是单例的TokenizerEngine\n\t *\n\t * @return 单例的TokenizerEngine\n\t * @since 5.3.3\n\t */\n\tpublic static TokenizerEngine get(){\n\t\treturn Singleton.get(TokenizerEngine.class.getName(), TokenizerFactory::create);\n\t}\n\n\t/**\n\t * 根据用户引入的分词引擎jar，自动创建对应的分词引擎对象\n\t * \n\t * @return {@link TokenizerEngine}\n\t */\n\tpublic static TokenizerEngine create() {\n\t\tfinal TokenizerEngine engine = doCreate();\n\t\tStaticLog.debug(\"Use [{}] Tokenizer Engine As Default.\", StrUtil.removeSuffix(engine.getClass().getSimpleName(), \"Engine\"));\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 根据用户引入的分词引擎jar，自动创建对应的分词引擎对象\n\t * \n\t * @return {@link TokenizerEngine}\n\t */\n\tprivate static TokenizerEngine doCreate() {\n\t\tfinal TokenizerEngine engine = ServiceLoaderUtil.loadFirstAvailable(TokenizerEngine.class);\n\t\tif(null != engine){\n\t\t\treturn engine;\n\t\t}\n\n\t\tthrow new TokenizerException(\"No tokenizer found ! Please add some tokenizer jar to your project !\");\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.analysis;\n\nimport java.io.IOException;\n\nimport org.apache.lucene.analysis.Analyzer;\nimport org.apache.lucene.analysis.TokenStream;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport cn.hutool.extra.tokenizer.TokenizerException;\n\n/**\n * Lucene-analysis分词抽象封装<br>\n * 项目地址：https://github.com/apache/lucene-solr/tree/master/lucene/analysis\n * \n * @author looly\n *\n */\npublic class AnalysisEngine implements TokenizerEngine {\n\n\tprivate final Analyzer analyzer;\n\n\t/**\n\t * 构造\n\t * \n\t * @param analyzer 分析器{@link Analyzer}\n\t */\n\tpublic AnalysisEngine(Analyzer analyzer) {\n\t\tthis.analyzer = analyzer;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\tTokenStream stream;\n\t\ttry {\n\t\t\tstream = analyzer.tokenStream(\"text\", StrUtil.str(text));\n\t\t\tstream.reset();\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\treturn new AnalysisResult(stream);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.analysis;\n\nimport java.io.IOException;\n\nimport org.apache.lucene.analysis.TokenStream;\nimport org.apache.lucene.analysis.tokenattributes.CharTermAttribute;\n\nimport cn.hutool.extra.tokenizer.AbstractResult;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * Lucene-analysis分词抽象结果封装<br>\n * 项目地址：https://github.com/apache/lucene-solr/tree/master/lucene/analysis\n * \n * @author looly\n *\n */\npublic class AnalysisResult extends AbstractResult {\n\n\tprivate final TokenStream stream;\n\n\t/**\n\t * 构造\n\t * \n\t * @param stream 分词结果\n\t */\n\tpublic AnalysisResult(TokenStream stream) {\n\t\tthis.stream = stream;\n\t}\n\n\t@Override\n\tprotected Word nextWord() {\n\t\ttry {\n\t\t\tif(this.stream.incrementToken()) {\n\t\t\t\treturn new AnalysisWord(this.stream.getAttribute(CharTermAttribute.class));\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.analysis;\n\nimport org.apache.lucene.analysis.tokenattributes.CharTermAttribute;\nimport org.apache.lucene.analysis.tokenattributes.OffsetAttribute;\nimport org.apache.lucene.util.Attribute;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * Lucene-analysis分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class AnalysisWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Attribute word;\n\n\t/**\n\t * 构造\n\t * \n\t * @param word {@link CharTermAttribute}\n\t */\n\tpublic AnalysisWord(CharTermAttribute word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.toString();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\tif(this.word instanceof OffsetAttribute) {\n\t\t\treturn ((OffsetAttribute)this.word).startOffset();\n\t\t}\n\t\treturn -1;\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\tif(this.word instanceof OffsetAttribute) {\n\t\t\treturn ((OffsetAttribute)this.word).endOffset();\n\t\t}\n\t\treturn -1;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/SmartcnEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.analysis;\n\nimport org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;\n\n/**\n * Lucene-smartcn分词引擎实现<br>\n * 项目地址：https://github.com/apache/lucene-solr/tree/master/lucene/analysis/smartcn\n * \n * @author looly\n *\n */\npublic class SmartcnEngine extends AnalysisEngine {\n\n\t/**\n\t * 构造\n\t */\n\tpublic SmartcnEngine() {\n\t\tsuper(new SmartChineseAnalyzer());\n\t}\n\t\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/package-info.java",
    "content": "/**\n * Lucene-analysis分词抽象封装<br>\n * 项目地址：https://github.com/apache/lucene-solr/tree/master/lucene/analysis\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.analysis;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ansj;\n\nimport org.ansj.splitWord.Analysis;\nimport org.ansj.splitWord.analysis.ToAnalysis;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\n\n/**\n * Ansj分词引擎实现<br>\n * 项目地址：https://github.com/NLPchina/ansj_seg\n * \n * @author looly\n *\n */\npublic class AnsjEngine implements TokenizerEngine {\n\n\tprivate final Analysis analysis;\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic AnsjEngine() {\n\t\tthis(new ToAnalysis());\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param analysis {@link Analysis}\n\t */\n\tpublic AnsjEngine(Analysis analysis) {\n\t\tthis.analysis = analysis;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\treturn new AnsjResult(analysis.parseStr(StrUtil.str(text)));\n\t}\n\t\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ansj;\n\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.Word;\nimport org.ansj.domain.Term;\n\nimport java.util.Iterator;\n\n/**\n * Ansj分词结果实现<br>\n * 项目地址：https://github.com/NLPchina/ansj_seg\n *\n * @author looly\n */\npublic class AnsjResult implements Result {\n\n\tprivate final Iterator<Term> result;\n\n\t/**\n\t * 构造\n\t *\n\t * @param ansjResult 分词结果\n\t */\n\tpublic AnsjResult(org.ansj.domain.Result ansjResult) {\n\t\tthis.result = ansjResult.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn result.hasNext();\n\t}\n\n\t@Override\n\tpublic Word next() {\n\t\treturn new AnsjWord(result.next());\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tresult.remove();\n\t}\n\n\t@Override\n\tpublic Iterator<Word> iterator() {\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ansj;\n\nimport org.ansj.domain.Term;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * Ansj分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class AnsjWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Term term;\n\n\t/**\n\t * 构造\n\t * \n\t * @param term {@link Term}\n\t */\n\tpublic AnsjWord(Term term) {\n\t\tthis.term = term;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn term.getName();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn this.term.getOffe();\n\t}\n\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn getStartOffset() + getText().length();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/package-info.java",
    "content": "/**\n * Ansj分词实现<br>\n * 项目地址：https://github.com/NLPchina/ansj_seg\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.ansj;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.hanlp;\n\nimport com.hankcs.hanlp.HanLP;\nimport com.hankcs.hanlp.seg.Segment;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport cn.hutool.extra.tokenizer.Result;\n\n/**\n * HanLP分词引擎实现<br>\n * 项目地址：https://github.com/hankcs/HanLP\n * \n * @author looly\n *\n */\npublic class HanLPEngine implements TokenizerEngine {\n\n\tprivate final Segment seg;\n\t\n\t/**\n\t * 构造\n\t * \n\t */\n\tpublic HanLPEngine() {\n\t\tthis(HanLP.newSegment());\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param seg {@link Segment}\n\t */\n\tpublic HanLPEngine(Segment seg) {\n\t\tthis.seg = seg;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\treturn new HanLPResult(this.seg.seg(StrUtil.str(text)));\n\t}\n\t\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.hanlp;\n\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.Word;\nimport com.hankcs.hanlp.seg.common.Term;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * HanLP分词结果实现<br>\n * 项目地址：https://github.com/hankcs/HanLP\n *\n * @author looly\n *\n */\npublic class HanLPResult implements Result {\n\n\tIterator<Term> result;\n\n\tpublic HanLPResult(List<Term> termList) {\n\t\tthis.result = termList.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn result.hasNext();\n\t}\n\n\t@Override\n\tpublic Word next() {\n\t\treturn new HanLPWord(result.next());\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tresult.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.hanlp;\n\nimport com.hankcs.hanlp.seg.common.Term;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * HanLP分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class HanLPWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tprivate final Term term;\n\n\t/**\n\t * 构造\n\t * \n\t * @param term {@link Term}\n\t */\n\tpublic HanLPWord(Term term) {\n\t\tthis.term = term;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn term.word;\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn this.term.offset;\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn getStartOffset() + this.term.length();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/package-info.java",
    "content": "/**\n * HanLP分词引擎实现<br>\n * 项目地址：https://github.com/hankcs/HanLP\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.hanlp;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ikanalyzer;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport org.wltea.analyzer.core.IKSegmenter;\n\n/**\n * IKAnalyzer分词引擎实现<br>\n * 项目地址：https://github.com/yozhao/IKAnalyzer\n *\n * @author looly\n */\npublic class IKAnalyzerEngine implements TokenizerEngine {\n\n\t/**\n\t * 构造\n\t */\n\tpublic IKAnalyzerEngine() {\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param seg {@link IKSegmenter}\n\t * @deprecated 并发问题，导致无法共用IKSegmenter，因此废弃\n\t */\n\t@Deprecated\n\tpublic IKAnalyzerEngine(IKSegmenter seg) {\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\tfinal IKSegmenter copySeg = new IKSegmenter(null, true);\n\t\tcopySeg.reset(StrUtil.getReader(text));\n\t\treturn new IKAnalyzerResult(copySeg);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ikanalyzer;\n\nimport cn.hutool.extra.tokenizer.AbstractResult;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport cn.hutool.extra.tokenizer.Word;\nimport org.wltea.analyzer.core.IKSegmenter;\nimport org.wltea.analyzer.core.Lexeme;\n\nimport java.io.IOException;\n\n/**\n * IKAnalyzer分词结果实现<br>\n * 项目地址：https://github.com/yozhao/IKAnalyzer\n *\n * @author looly\n *\n */\npublic class IKAnalyzerResult extends AbstractResult {\n\n\tprivate final IKSegmenter seg;\n\n\t/**\n\t * 构造\n\t *\n\t * @param seg 分词结果\n\t */\n\tpublic IKAnalyzerResult(IKSegmenter seg) {\n\t\tthis.seg = seg;\n\t}\n\n\t@Override\n\tprotected Word nextWord() {\n\t\tLexeme next;\n\t\ttry {\n\t\t\tnext = this.seg.next();\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\tif (null == next) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new IKAnalyzerWord(next);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.ikanalyzer;\n\nimport org.wltea.analyzer.core.Lexeme;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * IKAnalyzer分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class IKAnalyzerWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tprivate final Lexeme word;\n\n\t/**\n\t * 构造\n\t * \n\t * @param word {@link Lexeme}\n\t */\n\tpublic IKAnalyzerWord(Lexeme word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.getLexemeText();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn word.getBeginPosition();\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn word.getEndPosition();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/package-info.java",
    "content": "/**\n * IKAnalyzer分词引擎实现<br>\n * 项目地址：https://github.com/yozhao/IKAnalyzer\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.ikanalyzer;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jcseg;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport org.lionsoul.jcseg.ISegment;\nimport org.lionsoul.jcseg.dic.ADictionary;\nimport org.lionsoul.jcseg.dic.DictionaryFactory;\nimport org.lionsoul.jcseg.segmenter.SegmenterConfig;\n\nimport java.io.IOException;\nimport java.io.StringReader;\n\n/**\n * Jcseg分词引擎实现<br>\n * 项目地址：https://gitee.com/lionsoul/jcseg\n * \n * @author looly\n *\n */\npublic class JcsegEngine implements TokenizerEngine {\n\n\tprivate final ISegment segment;\n\n\t/**\n\t * 构造\n\t */\n\tpublic JcsegEngine() {\n\t\t// 创建SegmenterConfig分词配置实例，自动查找加载jcseg.properties配置项来初始化\n\t\tfinal SegmenterConfig config = new SegmenterConfig(true);\n\t\t// 创建默认单例词库实现，并且按照config配置加载词库\n\t\tfinal ADictionary dic = DictionaryFactory.createSingletonDictionary(config);\n\n\t\t// 依据给定的ADictionary和SegmenterConfig来创建ISegment\n\t\tthis.segment = ISegment.COMPLEX.factory.create(config, dic);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param segment {@link ISegment}\n\t */\n\tpublic JcsegEngine(ISegment segment) {\n\t\tthis.segment = segment;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\ttry {\n\t\t\tthis.segment.reset(new StringReader(StrUtil.str(text)));\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\treturn new JcsegResult(this.segment);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jcseg;\n\nimport cn.hutool.extra.tokenizer.AbstractResult;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport cn.hutool.extra.tokenizer.Word;\nimport org.lionsoul.jcseg.ISegment;\nimport org.lionsoul.jcseg.IWord;\n\nimport java.io.IOException;\n\n/**\n * Jcseg分词结果包装<br>\n * 项目地址：https://gitee.com/lionsoul/jcseg\n *\n * @author looly\n *\n */\npublic class JcsegResult extends AbstractResult {\n\n\tprivate final ISegment result;\n\n\t/**\n\t * 构造\n\t * @param segment 分词结果\n\t */\n\tpublic JcsegResult(ISegment segment) {\n\t\tthis.result = segment;\n\t}\n\n\t@Override\n\tprotected Word nextWord() {\n\t\tIWord word;\n\t\ttry {\n\t\t\tword = this.result.next();\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\tif(null == word){\n\t\t\treturn null;\n\t\t}\n\t\treturn new JcsegWord(word);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jcseg;\n\nimport cn.hutool.extra.tokenizer.Word;\nimport org.lionsoul.jcseg.IWord;\n\n/**\n * Jcseg分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class JcsegWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final IWord word;\n\n\t/**\n\t * 构造\n\t * \n\t * @param word {@link IWord}\n\t */\n\tpublic JcsegWord(IWord word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.getValue();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn word.getPosition();\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn getStartOffset() + word.getLength();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/package-info.java",
    "content": "/**\n * Jcseg分词引擎实现<br>\n * 项目地址：https://gitee.com/lionsoul/jcseg\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.jcseg;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jieba;\n\nimport com.huaban.analysis.jieba.JiebaSegmenter;\nimport com.huaban.analysis.jieba.JiebaSegmenter.SegMode;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport cn.hutool.extra.tokenizer.Result;\n\n/**\n * Jieba分词引擎实现<br>\n * 项目地址：https://github.com/huaban/jieba-analysis\n * \n * @author looly\n *\n */\npublic class JiebaEngine implements TokenizerEngine {\n\n\tprivate final JiebaSegmenter jiebaSegmenter;\n\tprivate final SegMode mode;\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic JiebaEngine() {\n\t\tthis(SegMode.SEARCH);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param mode 模式{@link SegMode}\n\t */\n\tpublic JiebaEngine(SegMode mode) {\n\t\tthis.jiebaSegmenter = new JiebaSegmenter();\n\t\tthis.mode = mode;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\treturn new JiebaResult(jiebaSegmenter.process(StrUtil.str(text), mode));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jieba;\n\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.Word;\nimport com.huaban.analysis.jieba.SegToken;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Jieba分词结果实现<br>\n * 项目地址：https://github.com/huaban/jieba-analysis\n *\n * @author looly\n *\n */\npublic class JiebaResult implements Result{\n\n\tIterator<SegToken> result;\n\n\t/**\n\t * 构造\n\t * @param segTokenList 分词结果\n\t */\n\tpublic JiebaResult(List<SegToken> segTokenList) {\n\t\tthis.result = segTokenList.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn result.hasNext();\n\t}\n\n\t@Override\n\tpublic Word next() {\n\t\treturn new JiebaWord(result.next());\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tresult.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.jieba;\n\nimport com.huaban.analysis.jieba.SegToken;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * Jieba分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class JiebaWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final SegToken segToken;\n\n\t/**\n\t * 构造\n\t * \n\t * @param segToken {@link SegToken}\n\t */\n\tpublic JiebaWord(SegToken segToken) {\n\t\tthis.segToken = segToken;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn segToken.word;\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn segToken.startOffset;\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn segToken.endOffset;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/package-info.java",
    "content": "/**\n * Jieba分词引擎实现<br>\n * 项目地址：https://github.com/huaban/jieba-analysis\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.jieba;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mmseg;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\nimport com.chenlb.mmseg4j.ComplexSeg;\nimport com.chenlb.mmseg4j.Dictionary;\nimport com.chenlb.mmseg4j.MMSeg;\n\nimport java.io.StringReader;\n\n/**\n * mmseg4j分词引擎实现<br>\n * 项目地址：https://github.com/chenlb/mmseg4j-core\n * \n * @author looly\n *\n */\npublic class MmsegEngine implements TokenizerEngine {\n\n\tprivate final MMSeg mmSeg;\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic MmsegEngine() {\n\t\tfinal Dictionary dict = Dictionary.getInstance();\n\t\tfinal ComplexSeg seg = new ComplexSeg(dict);\n\t\tthis.mmSeg = new MMSeg(new StringReader(\"\"), seg);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param mmSeg 模式{@link MMSeg}\n\t */\n\tpublic MmsegEngine(MMSeg mmSeg) {\n\t\tthis.mmSeg = mmSeg;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\tthis.mmSeg.reset(StrUtil.getReader(text));\n\t\treturn new MmsegResult(this.mmSeg);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mmseg;\n\nimport cn.hutool.extra.tokenizer.AbstractResult;\nimport cn.hutool.extra.tokenizer.TokenizerException;\nimport cn.hutool.extra.tokenizer.Word;\nimport com.chenlb.mmseg4j.MMSeg;\n\nimport java.io.IOException;\n\n/**\n * mmseg4j分词结果实现<br>\n * 项目地址：https://github.com/chenlb/mmseg4j-core\n *\n * @author looly\n *\n */\npublic class MmsegResult extends AbstractResult {\n\n\tprivate final MMSeg mmSeg;\n\n\t/**\n\t * 构造\n\t *\n\t * @param mmSeg 分词结果\n\t */\n\tpublic MmsegResult(MMSeg mmSeg) {\n\t\tthis.mmSeg = mmSeg;\n\t}\n\n\t@Override\n\tprotected Word nextWord() {\n\t\tcom.chenlb.mmseg4j.Word next;\n\t\ttry {\n\t\t\tnext = this.mmSeg.next();\n\t\t} catch (IOException e) {\n\t\t\tthrow new TokenizerException(e);\n\t\t}\n\t\tif (null == next) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new MmsegWord(next);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mmseg;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * mmseg分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class MmsegWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tprivate final com.chenlb.mmseg4j.Word word;\n\n\t/**\n\t * 构造\n\t * \n\t * @param word {@link com.chenlb.mmseg4j.Word}\n\t */\n\tpublic MmsegWord(com.chenlb.mmseg4j.Word word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.getString();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn this.word.getStartOffset();\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn this.word.getEndOffset();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/package-info.java",
    "content": "/**\n * mmseg4j分词引擎实现<br>\n * 项目地址：https://github.com/chenlb/mmseg4j-core\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.mmseg;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mynlp/MynlpEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mynlp;\n\nimport com.mayabot.nlp.segment.Lexer;\nimport com.mayabot.nlp.segment.Lexers;\nimport com.mayabot.nlp.segment.Sentence;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\n\n/**\n * MYNLP 中文NLP工具包分词实现<br>\n * 项目地址：https://github.com/mayabot/mynlp/\n * \n * @author looly\n *\n */\npublic class MynlpEngine implements TokenizerEngine {\n\n\tprivate final Lexer lexer;\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic MynlpEngine() {\n\t\tthis.lexer = Lexers.core();\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param lexer 分词器接口{@link Lexer}\n\t */\n\tpublic MynlpEngine(Lexer lexer) {\n\t\tthis.lexer = lexer;\n\t}\n\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\tfinal Sentence sentence = this.lexer.scan(StrUtil.str(text));\n\t\treturn new MynlpResult(sentence);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mynlp/MynlpResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mynlp;\n\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.Word;\nimport com.mayabot.nlp.segment.Sentence;\nimport com.mayabot.nlp.segment.WordTerm;\n\nimport java.util.Iterator;\n\n/**\n * MYNLP 中文NLP工具包分词结果实现<br>\n * 项目地址：https://github.com/mayabot/mynlp/\n *\n * @author looly\n *\n */\npublic class MynlpResult implements Result {\n\n\tprivate final Iterator<WordTerm> result;\n\n\t/**\n\t * 构造\n\t *\n\t * @param sentence 分词结果（中文句子）\n\t */\n\tpublic MynlpResult(Sentence sentence) {\n\t\tthis.result = sentence.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn result.hasNext();\n\t}\n\n\t@Override\n\tpublic Word next() {\n\t\treturn new MynlpWord(result.next());\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tresult.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mynlp/MynlpWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.mynlp;\n\nimport com.mayabot.nlp.segment.WordTerm;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * mmseg分词中的一个单词包装\n *\n * @author looly\n */\npublic class MynlpWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final WordTerm word;\n\n\t/**\n\t * 构造\n\t *\n\t * @param word {@link WordTerm}\n\t */\n\tpublic MynlpWord(WordTerm word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.getWord();\n\t}\n\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn this.word.offset;\n\t}\n\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn getStartOffset() + word.word.length();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mynlp/package-info.java",
    "content": "/**\n * MYNLP 中文NLP工具包分词实现<br>\n * 项目地址：https://github.com/mayabot/mynlp/\n * \n * @author Looly\n * @since 4.6.5\n */\npackage cn.hutool.extra.tokenizer.engine.mynlp;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/package-info.java",
    "content": "/**\n * 第三方分词引擎实现\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordEngine.java",
    "content": "package cn.hutool.extra.tokenizer.engine.word;\n\nimport org.apdplat.word.segmentation.Segmentation;\nimport org.apdplat.word.segmentation.SegmentationAlgorithm;\nimport org.apdplat.word.segmentation.SegmentationFactory;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.TokenizerEngine;\n\n/**\n * Word分词引擎实现<br>\n * 项目地址：https://github.com/ysc/word\n * \n * @author looly\n *\n */\npublic class WordEngine implements TokenizerEngine {\n\t\n\tprivate final Segmentation segmentation;\n\n\t/**\n\t * 构造\n\t */\n\tpublic WordEngine() {\n\t\tthis(SegmentationAlgorithm.BidirectionalMaximumMatching);\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param algorithm {@link SegmentationAlgorithm}分词算法枚举\n\t */\n\tpublic WordEngine(SegmentationAlgorithm algorithm) {\n\t\tthis(SegmentationFactory.getSegmentation(algorithm));\n\t}\n\t\n\t/**\n\t * 构造\n\t * \n\t * @param segmentation {@link Segmentation}分词实现\n\t */\n\tpublic WordEngine(Segmentation segmentation) {\n\t\tthis.segmentation = segmentation;\n\t}\n\t\n\t@Override\n\tpublic Result parse(CharSequence text) {\n\t\treturn new WordResult(this.segmentation.seg(StrUtil.str(text)));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordResult.java",
    "content": "package cn.hutool.extra.tokenizer.engine.word;\n\nimport cn.hutool.extra.tokenizer.Result;\nimport cn.hutool.extra.tokenizer.Word;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Word分词结果实现<br>\n * 项目地址：https://github.com/ysc/word\n *\n * @author looly\n *\n */\npublic class WordResult implements Result{\n\n\tprivate final Iterator<org.apdplat.word.segmentation.Word> wordIter;\n\n\t/**\n\t * 构造\n\t *\n\t * @param result 分词结果\n\t */\n\tpublic WordResult(List<org.apdplat.word.segmentation.Word> result) {\n\t\tthis.wordIter = result.iterator();\n\t}\n\n\t@Override\n\tpublic boolean hasNext() {\n\t\treturn this.wordIter.hasNext();\n\t}\n\n\t@Override\n\tpublic Word next() {\n\t\treturn new WordWord(this.wordIter.next());\n\t}\n\n\t@Override\n\tpublic void remove() {\n\t\tthis.wordIter.remove();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordWord.java",
    "content": "package cn.hutool.extra.tokenizer.engine.word;\n\nimport cn.hutool.extra.tokenizer.Word;\n\n/**\n * Word分词中的一个单词包装\n * \n * @author looly\n *\n */\npublic class WordWord implements Word {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tprivate final org.apdplat.word.segmentation.Word word;\n\n\t/**\n\t * 构造\n\t * \n\t * @param word {@link org.apdplat.word.segmentation.Word}\n\t */\n\tpublic WordWord(org.apdplat.word.segmentation.Word word) {\n\t\tthis.word = word;\n\t}\n\n\t@Override\n\tpublic String getText() {\n\t\treturn word.getText();\n\t}\n\t\n\t@Override\n\tpublic int getStartOffset() {\n\t\treturn -1;\n\t}\n\t\n\t@Override\n\tpublic int getEndOffset() {\n\t\treturn -1;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/package-info.java",
    "content": "/**\n * Word分词引擎实现<br>\n * 项目地址：https://github.com/ysc/word\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer.engine.word;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/tokenizer/package-info.java",
    "content": "/**\n * 中文分词封装<br>\n * 通过定义统一接口，适配第三方分词引擎\n * \n * @author looly\n *\n */\npackage cn.hutool.extra.tokenizer;"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/validation/BeanValidationResult.java",
    "content": "package cn.hutool.extra.validation;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * bean 校验结果\n *\n * @author chengqiang\n * @since 5.5.0\n */\npublic class BeanValidationResult {\n\t/**\n\t * 校验是否成功\n\t */\n\tprivate boolean success;\n\t/**\n\t * 错误消息\n\t */\n\tprivate List<ErrorMessage> errorMessages = new ArrayList<>();\n\n\t/**\n\t * 构造\n\t *\n\t * @param success 是否验证成功\n\t */\n\tpublic BeanValidationResult(boolean success) {\n\t\tthis.success = success;\n\t}\n\n\t/**\n\t * 是否验证通过\n\t *\n\t * @return 是否验证通过\n\t */\n\tpublic boolean isSuccess() {\n\t\treturn success;\n\t}\n\n\t/**\n\t * 设置是否通过\n\t *\n\t * @param success 是否通过\n\t * @return this\n\t */\n\tpublic BeanValidationResult setSuccess(boolean success) {\n\t\tthis.success = success;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取错误信息列表\n\t *\n\t * @return 错误信息列表\n\t */\n\tpublic List<ErrorMessage> getErrorMessages() {\n\t\treturn errorMessages;\n\t}\n\n\t/**\n\t * 设置错误信息列表\n\t *\n\t * @param errorMessages 错误信息列表\n\t * @return this\n\t */\n\tpublic BeanValidationResult setErrorMessages(List<ErrorMessage> errorMessages) {\n\t\tthis.errorMessages = errorMessages;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加错误信息\n\t *\n\t * @param errorMessage 错误信息\n\t * @return this\n\t */\n\tpublic BeanValidationResult addErrorMessage(ErrorMessage errorMessage) {\n\t\tthis.errorMessages.add(errorMessage);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 错误消息，包括字段名（字段路径）、消息内容和字段值\n\t */\n\tpublic static class ErrorMessage {\n\t\t/**\n\t\t * 属性字段名称\n\t\t */\n\t\tprivate String propertyName;\n\t\t/**\n\t\t * 错误信息\n\t\t */\n\t\tprivate String message;\n\t\t/**\n\t\t * 错误值\n\t\t */\n\t\tprivate Object value;\n\n\t\tpublic String getPropertyName() {\n\t\t\treturn propertyName;\n\t\t}\n\n\t\tpublic void setPropertyName(String propertyName) {\n\t\t\tthis.propertyName = propertyName;\n\t\t}\n\n\t\tpublic String getMessage() {\n\t\t\treturn message;\n\t\t}\n\n\t\tpublic void setMessage(String message) {\n\t\t\tthis.message = message;\n\t\t}\n\n\t\tpublic Object getValue() {\n\t\t\treturn value;\n\t\t}\n\n\t\tpublic void setValue(Object value) {\n\t\t\tthis.value = value;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"ErrorMessage{\" +\n\t\t\t\t\t\"propertyName='\" + propertyName + '\\'' +\n\t\t\t\t\t\", message='\" + message + '\\'' +\n\t\t\t\t\t\", value=\" + value +\n\t\t\t\t\t'}';\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/validation/ValidationUtil.java",
    "content": "package cn.hutool.extra.validation;\n\nimport cn.hutool.extra.validation.BeanValidationResult.ErrorMessage;\n\nimport jakarta.validation.ConstraintViolation;\nimport jakarta.validation.Validation;\nimport jakarta.validation.Validator;\nimport jakarta.validation.ValidatorFactory;\n\nimport java.util.Set;\n\n\n/**\n * java bean 校验工具类，此工具类基于validation-api（jakarta.validation-api）封装\n *\n * <p>在实际使用中，用户需引入validation-api的实现，如：hibernate-validator</p>\n * <p>注意：hibernate-validator还依赖了javax.el，需自行引入。</p>\n *\n * @author chengqiang\n * @since 5.5.0\n */\npublic class ValidationUtil {\n\t/**\n\t * 默认{@link Validator} 对象\n\t */\n\tprivate static final Validator validator;\n\n\tstatic {\n\t\ttry(ValidatorFactory factory = Validation.buildDefaultValidatorFactory()){\n\t\t\tvalidator = factory.getValidator();\n\t\t}\n\t}\n\n\t/**\n\t * 获取原生{@link Validator} 对象\n\t *\n\t * @return {@link Validator} 对象\n\t */\n\tpublic static Validator getValidator() {\n\t\treturn validator;\n\t}\n\n\t/**\n\t * 校验对象\n\t *\n\t * @param <T>    Bean类型\n\t * @param bean   bean\n\t * @param groups 校验组\n\t * @return {@link Set}\n\t */\n\tpublic static <T> Set<ConstraintViolation<T>> validate(T bean, Class<?>... groups) {\n\t\treturn validator.validate(bean, groups);\n\t}\n\n\t/**\n\t * 校验bean的某一个属性\n\t *\n\t * @param <T>          Bean类型\n\t * @param bean         bean\n\t * @param propertyName 属性名称\n\t * @param groups       验证分组\n\t * @return {@link Set}\n\t */\n\tpublic static <T> Set<ConstraintViolation<T>> validateProperty(T bean, String propertyName, Class<?>... groups) {\n\t\treturn validator.validateProperty(bean, propertyName, groups);\n\t}\n\n\t/**\n\t * 校验对象\n\t *\n\t * @param <T>    Bean类型\n\t * @param bean   bean\n\t * @param groups 校验组\n\t * @return {@link BeanValidationResult}\n\t */\n\tpublic static <T> BeanValidationResult warpValidate(T bean, Class<?>... groups) {\n\t\treturn warpBeanValidationResult(validate(bean, groups));\n\t}\n\n\t/**\n\t * 校验bean的某一个属性\n\t *\n\t * @param <T>  bean类型\n\t * @param bean         bean\n\t * @param propertyName 属性名称\n\t * @param groups       验证分组\n\t * @return {@link BeanValidationResult}\n\t */\n\tpublic static <T> BeanValidationResult warpValidateProperty(T bean, String propertyName, Class<?>... groups) {\n\t\treturn warpBeanValidationResult(validateProperty(bean, propertyName, groups));\n\t}\n\n\t/**\n\t * 包装校验结果\n\t *\n\t * @param constraintViolations 校验结果集\n\t * @return {@link BeanValidationResult}\n\t */\n\tprivate static <T> BeanValidationResult warpBeanValidationResult(Set<ConstraintViolation<T>> constraintViolations) {\n\t\tBeanValidationResult result = new BeanValidationResult(constraintViolations.isEmpty());\n\t\tfor (ConstraintViolation<T> constraintViolation : constraintViolations) {\n\t\t\tErrorMessage errorMessage = new ErrorMessage();\n\t\t\terrorMessage.setPropertyName(constraintViolation.getPropertyPath().toString());\n\t\t\terrorMessage.setMessage(constraintViolation.getMessage());\n\t\t\terrorMessage.setValue(constraintViolation.getInvalidValue());\n\t\t\tresult.addErrorMessage(errorMessage);\n\t\t}\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/main/java/cn/hutool/extra/validation/package-info.java",
    "content": "/**\n * 基于JSR-380标准的校验工具类，封装了javax.validation的API\n *\n * @author chengqiang\n */\npackage cn.hutool.extra.validation;\n"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/services/cn.hutool.extra.expression.ExpressionEngine",
    "content": "cn.hutool.extra.expression.engine.aviator.AviatorEngine\ncn.hutool.extra.expression.engine.jexl.JexlEngine\ncn.hutool.extra.expression.engine.mvel.MvelEngine\ncn.hutool.extra.expression.engine.jfireel.JfireELEngine\ncn.hutool.extra.expression.engine.spel.SpELEngine\ncn.hutool.extra.expression.engine.rhino.RhinoEngine\ncn.hutool.extra.expression.engine.qlexpress.QLExpressEngine\n"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/services/cn.hutool.extra.pinyin.PinyinEngine",
    "content": "cn.hutool.extra.pinyin.engine.tinypinyin.TinyPinyinEngine\ncn.hutool.extra.pinyin.engine.jpinyin.JPinyinEngine\ncn.hutool.extra.pinyin.engine.pinyin4j.Pinyin4jEngine\ncn.hutool.extra.pinyin.engine.bopomofo4j.Bopomofo4jEngine\ncn.hutool.extra.pinyin.engine.houbbpinyin.HoubbPinyinEngine\n"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/services/cn.hutool.extra.template.TemplateEngine",
    "content": "cn.hutool.extra.template.engine.beetl.BeetlEngine\ncn.hutool.extra.template.engine.freemarker.FreemarkerEngine\ncn.hutool.extra.template.engine.velocity.VelocityEngine\ncn.hutool.extra.template.engine.rythm.RythmEngine\ncn.hutool.extra.template.engine.enjoy.EnjoyEngine\ncn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine\ncn.hutool.extra.template.engine.wit.WitEngine\ncn.hutool.extra.template.engine.jetbrick.JetbrickEngine\n"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/services/cn.hutool.extra.tokenizer.TokenizerEngine",
    "content": "cn.hutool.extra.tokenizer.engine.ansj.AnsjEngine\ncn.hutool.extra.tokenizer.engine.hanlp.HanLPEngine\ncn.hutool.extra.tokenizer.engine.ikanalyzer.IKAnalyzerEngine\ncn.hutool.extra.tokenizer.engine.jcseg.JcsegEngine\ncn.hutool.extra.tokenizer.engine.jieba.JiebaEngine\ncn.hutool.extra.tokenizer.engine.mmseg.MmsegEngine\ncn.hutool.extra.tokenizer.engine.word.WordEngine\ncn.hutool.extra.tokenizer.engine.analysis.SmartcnEngine\ncn.hutool.extra.tokenizer.engine.mynlp.MynlpEngine"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.hutool.extra.spring.SpringUtil\n"
  },
  {
    "path": "hutool-extra/src/main/resources/META-INF/spring.factories",
    "content": "# Auto Configure\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.hutool.extra.spring.SpringUtil\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/cglib/CglibUtilTest.java",
    "content": "package cn.hutool.extra.cglib;\n\nimport cn.hutool.core.convert.Convert;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class CglibUtilTest {\n\n\t@Test\n\tpublic void copyTest() {\n\t\tSampleBean bean = new SampleBean();\n\t\tOtherSampleBean otherBean = new OtherSampleBean();\n\t\tbean.setValue(\"Hello world\");\n\t\tbean.setValue2(\"123\");\n\n\t\tCglibUtil.copy(bean, otherBean);\n\t\tassertEquals(\"Hello world\", otherBean.getValue());\n\t\t// 无定义转换器，转换失败\n\t\tassertEquals(0, otherBean.getValue2());\n\n\t\tOtherSampleBean otherBean2 = CglibUtil.copy(bean, OtherSampleBean.class);\n\t\tassertEquals(\"Hello world\", otherBean2.getValue());\n\t\t// 无定义转换器，转换失败\n\t\tassertEquals(0, otherBean.getValue2());\n\n\t\totherBean = new OtherSampleBean();\n\t\t//自定义转换器\n\t\tCglibUtil.copy(bean, otherBean, (value, target, context) -> Convert.convertQuietly(target, value));\n\t\tassertEquals(\"Hello world\", otherBean.getValue());\n\t\tassertEquals(123, otherBean.getValue2());\n\t}\n\n\t@Data\n\tpublic static class SampleBean {\n\t\tprivate String value;\n\t\tprivate String value2;\n\t}\n\n\t@Data\n\tpublic static class OtherSampleBean {\n\t\tprivate String value;\n\t\tprivate int value2;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/compress/ArchiverTest.java",
    "content": "package cn.hutool.extra.compress;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.extra.compress.archiver.StreamArchiver;\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\n@SuppressWarnings(\"resource\")\npublic class ArchiverTest {\n\n\t@Test\n\t@Disabled\n\tpublic void zipTest(){\n\t\tfinal File file = FileUtil.file(\"d:/test/compress/test.zip\");\n\t\tStreamArchiver.create(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.ZIP, file)\n\t\t\t\t.add(FileUtil.file(\"d:/Java\"), (f)->{\n\t\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finish().close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void tarTest(){\n\t\tfinal File file = FileUtil.file(\"d:/test/compress/test.tar\");\n\t\tStreamArchiver.create(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.TAR, file)\n\t\t\t\t.add(FileUtil.file(\"d:/Java\"), (f)->{\n\t\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finish().close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cpioTest(){\n\t\tfinal File file = FileUtil.file(\"d:/test/compress/test.cpio\");\n\t\tStreamArchiver.create(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.CPIO, file)\n\t\t\t\t.add(FileUtil.file(\"d:/Java\"), (f)->{\n\t\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finish().close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sevenZTest(){\n\t\tfinal File file = FileUtil.file(\"d:/test/compress/test.7z\");\n\t\tCompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.SEVEN_Z, file)\n\t\t\t\t.add(FileUtil.file(\"d:/Java/apache-maven-3.8.1\"), (f)->{\n\t\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finish().close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void tgzTest(){\n\t\tfinal File file = FileUtil.file(\"d:/test/compress/test.tgz\");\n\t\tCompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, \"tgz\", file)\n\t\t\t\t.add(FileUtil.file(\"d:/Java/apache-maven-3.8.1\"), (f)->{\n\t\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finish().close();\n\t}\n\n\t/**\n\t * Add: D:\\disk-all\n\t * Add: D:\\disk-all\\els-app\n\t * Add: D:\\disk-all\\els-app\\db-backup\n\t * Add: D:\\disk-all\\els-app\\新建 文本文档.txt\n\t * Add: D:\\disk-all\\新建 文本文档.txt\n\t * Add: D:\\disk-all\\新建文件夹\n\t */\n\t@Test\n\t@Disabled\n\tpublic void emptyTest(){\n\t\tfinal File file = FileUtil.file(\"d:/disk-all.tgz\");\n\t\tCompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, \"tgz\", file)\n\t\t\t.add(FileUtil.file(\"D:\\\\disk-all\"), (f)->{\n\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\treturn true;\n\t\t\t})\n\t\t\t.finish().close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void emptyZTest(){\n\t\tfinal File file = FileUtil.file(\"d:/disk-all.7z\");\n\t\tCompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, \"7z\", file)\n\t\t\t.add(FileUtil.file(\"D:\\\\disk-all\"), (f)->{\n\t\t\t\tConsole.log(\"Add: {}\", f.getPath());\n\t\t\t\treturn true;\n\t\t\t})\n\t\t\t.finish().close();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/compress/ExtractorTest.java",
    "content": "package cn.hutool.extra.compress;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.extra.compress.extractor.Extractor;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\npublic class ExtractorTest {\n\n\t@Test\n\t@Disabled\n\tpublic void zipTest() {\n\t\tfinal Extractor extractor = CompressUtil.createExtractor(\n\t\t\t\tCharsetUtil.defaultCharset(),\n\t\t\t\tFileUtil.file(\"d:/test/c_1344112734760931330_20201230104703032.zip\"));\n\n\t\textractor.extract(FileUtil.file(\"d:/test/compress/test2/\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sevenZTest() {\n\t\tfinal Extractor extractor = CompressUtil.createExtractor(\n\t\t\t\tCharsetUtil.defaultCharset(),\n\t\t\t\tFileUtil.file(\"d:/test/compress/test.7z\"));\n\n\t\textractor.extract(FileUtil.file(\"d:/test/compress/test2/\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void tgzTest() {\n\t\tfinal Extractor extractor = CompressUtil.createExtractor(\n\t\t\t\tCharsetUtil.defaultCharset(),\n\t\t\t\t\"tgz\",\n\t\t\t\tFileUtil.file(\"d:/test/test.tgz\"));\n\n\t\textractor.extract(FileUtil.file(\"d:/test/tgz/\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sevenZTest2() {\n\t\tfinal File targetDir = FileUtil.file(\"d:/test/sevenZ2/\");\n\t\tFileUtil.clean(targetDir);\n\t\t//\n\t\tfinal Extractor extractor = CompressUtil.createExtractor(\n\t\t\t\tCharsetUtil.defaultCharset(),\n\t\t\t\tFileUtil.file(\"D:/System-Data/Downloads/apache-tomcat-10.0.27.7z\"));\n\n\t\textractor.extract(targetDir, 1);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void zipTest2() {\n\t\tfinal File targetDir = FileUtil.file(\"d:/test/zip2/\");\n\t\tFileUtil.clean(targetDir);\n\t\t//\n\t\tfinal Extractor extractor = CompressUtil.createExtractor(\n\t\t\t\tCharsetUtil.defaultCharset(),\n\t\t\t\tFileUtil.file(\"D:/System-Data/Downloads/apache-tomcat-10.0.27.zip\"));\n\n\t\textractor.extract(targetDir, 1);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/compress/IssueI7PMJ0Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.extra.compress;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.charset.StandardCharsets;\n\npublic class IssueI7PMJ0Test {\n\n\t@Test\n\t@Disabled\n\tpublic void createArchiverTest() {\n\t\tfinal File tarFile = FileUtil.file(\"d:/test/issueI7PMJ0.zip\");\n\t\tCompressUtil.createArchiver(StandardCharsets.UTF_8, ArchiveStreamFactory.ZIP, tarFile)\n\t\t\t.add(FileUtil.file(\"d:/test/aaa.xml\"), (file) -> !file.getName().equals(\"aaa.xml\"))\n\t\t\t.finish().close();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/emoji/EmojiUtilTest.java",
    "content": "package cn.hutool.extra.emoji;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class EmojiUtilTest {\n\n\t@Test\n\tpublic void toUnicodeTest() {\n\t\tString emoji = EmojiUtil.toUnicode(\":smile:\");\n\t\tassertEquals(\"😄\", emoji);\n\t}\n\n\t@Test\n\tpublic void toAliasTest() {\n\t\tString alias = EmojiUtil.toAlias(\"😄\");\n\t\tassertEquals(\":smile:\", alias);\n\t}\n\n\t@Test\n\tpublic void containsEmojiTest() {\n\t\tboolean containsEmoji = EmojiUtil.containsEmoji(\"测试一下是否包含EMOJ:😄\");\n\t\tassertTrue(containsEmoji);\n\t\tboolean notContainsEmoji = EmojiUtil.containsEmoji(\"不包含EMOJ:^_^\");\n\t\tassertFalse(notContainsEmoji);\n\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/expression/AviatorTest.java",
    "content": "package cn.hutool.extra.expression;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.extra.expression.engine.aviator.AviatorEngine;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\n/**\n * Aviator引擎单元测试，来自https://github.com/looly/hutool/pull/1203\n */\npublic class AviatorTest {\n\n\t@Test\n\tpublic void simpleTest(){\n\t\tFoo foo = new Foo(100, 3.14f, DateUtil.parseDate(\"2020-11-12\"));\n\t\tExpressionEngine engine = new AviatorEngine();\n\t\tString exp =\n\t\t\t\t\"\\\"[foo i=\\\"+ foo.i + \\\", f=\\\" + foo.f + \\\", date.year=\\\" + (foo.date.year+1900) + \\\", date.month=\\\" + foo.date.month + \\\", bars[0].name=\\\" + #foo.bars[0].name + \\\"]\\\"\";\n\t\tString result = (String) engine.eval(exp, Dict.create().set(\"foo\", foo), null);\n\t\tassertEquals(\"[foo i=100, f=3.14, date.year=2020, date.month=10, bars[0].name=bar]\", result);\n\n\t\t// Assignment.\n\t\texp = \"#foo.bars[0].name='hello aviator' ; #foo.bars[0].name\";\n\t\tresult = (String) engine.eval(exp, Dict.create().set(\"foo\", foo), null);\n\t\tassertEquals(\"hello aviator\", result);\n\t\tassertEquals(\"hello aviator\", foo.bars[0].getName());\n\n\t\texp = \"foo.bars[0] = nil ; foo.bars[0]\";\n\t\tresult = (String) engine.eval(exp, Dict.create().set(\"foo\", foo), null);\n\t\tConsole.log(\"Execute expression: \" + exp);\n\t\tassertNull(result);\n\t\tassertNull(foo.bars[0]);\n\t}\n\n\t@Data\n\tpublic static class Bar {\n\t\tpublic Bar() {\n\t\t\tthis.name = \"bar\";\n\t\t}\n\t\tprivate String name;\n\t}\n\n\t@Data\n\tpublic static class Foo {\n\t\tint i;\n\t\tfloat f;\n\t\tDate date;\n\t\tBar[] bars = new Bar[1];\n\n\t\tpublic Foo(final int i, final float f, final Date date) {\n\t\t\tthis.i = i;\n\t\t\tthis.f = f;\n\t\t\tthis.date = date;\n\t\t\tthis.bars[0] = new Bar();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/expression/ExpressionUtilTest.java",
    "content": "package cn.hutool.extra.expression;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.extra.expression.engine.jexl.JexlEngine;\nimport cn.hutool.extra.expression.engine.jfireel.JfireELEngine;\nimport cn.hutool.extra.expression.engine.mvel.MvelEngine;\nimport cn.hutool.extra.expression.engine.qlexpress.QLExpressEngine;\nimport cn.hutool.extra.expression.engine.rhino.RhinoEngine;\nimport cn.hutool.extra.expression.engine.spel.SpELEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ExpressionUtilTest {\n\n\t@Test\n\tpublic void evalTest(){\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = ExpressionUtil.eval(\"a-(b-c)\", dict);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void jexlTest(){\n\t\tfinal ExpressionEngine engine = new JexlEngine();\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"a-(b-c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void jexlScriptTest(){\n\t\tfinal ExpressionEngine engine = new JexlEngine();\n\n\t\tfinal String exps2=\"if(a>0){return 100;}\";\n\t\tfinal Map<String,Object> map2=new HashMap<>();\n\t\tmap2.put(\"a\", 1);\n\t\tfinal Object eval1 = engine.eval(exps2, map2, null);\n\t\tassertEquals(100, eval1);\n\t}\n\n\t@Test\n\tpublic void mvelTest(){\n\t\tfinal ExpressionEngine engine = new MvelEngine();\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"a-(b-c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void jfireELTest(){\n\t\tfinal ExpressionEngine engine = new JfireELEngine();\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"a-(b-c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void spELTest(){\n\t\tfinal ExpressionEngine engine = new SpELEngine();\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"#a-(#b-#c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void rhinoTest(){\n\t\tfinal ExpressionEngine engine = new RhinoEngine();\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"a-(b-c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n\t@Test\n\tpublic void qlExpressTest(){\n\t\tfinal ExpressionEngine engine = new QLExpressEngine();\n\n\t\tfinal Dict dict = Dict.of()\n\t\t\t\t.set(\"a\", 100.3)\n\t\t\t\t.set(\"b\", 45)\n\t\t\t\t.set(\"c\", -199.100);\n\t\tfinal Object eval = engine.eval(\"a-(b-c)\", dict, null);\n\t\tassertEquals(-143.8, (double)eval, 0);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java",
    "content": "package cn.hutool.extra.ftp;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.extra.ssh.Sftp;\nimport org.apache.commons.net.ftp.FTPFile;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class FtpTest {\n\n\t@Test\n\t@Disabled\n\tpublic void cdTest() {\n\t\tfinal Ftp ftp = new Ftp(\"looly.centos\");\n\n\t\tftp.cd(\"/file/aaa\");\n\t\tConsole.log(ftp.pwd());\n\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadTest() {\n\t\tfinal Ftp ftp = new Ftp(\"localhost\");\n\n\t\tfinal boolean upload = ftp.upload(\"/temp\", FileUtil.file(\"d:/test/test.zip\"));\n\t\tConsole.log(upload);\n\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadDirectorTest() {\n\t\tfinal Ftp ftp = new Ftp(\"localhost\");\n\n\t\tftp.uploadFileOrDirectory(\"/temp\", FileUtil.file(\"d:/test/\"));\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void reconnectIfTimeoutTest() throws InterruptedException {\n\t\tfinal Ftp ftp = new Ftp(\"looly.centos\");\n\n\t\tConsole.log(\"打印pwd: \" + ftp.pwd());\n\n\t\tConsole.log(\"休眠一段时间，然后再次发送pwd命令，抛出异常表明连接超时\");\n\t\tThread.sleep(35 * 1000);\n\n\t\ttry{\n\t\t\tConsole.log(\"打印pwd: \" + ftp.pwd());\n\t\t}catch (final FtpException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\n\t\tConsole.log(\"判断是否超时并重连...\");\n\t\tftp.reconnectIfTimeout();\n\n\t\tConsole.log(\"打印pwd: \" + ftp.pwd());\n\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void recursiveDownloadFolder() {\n\t\tfinal Ftp ftp = new Ftp(\"looly.centos\");\n\t\tftp.recursiveDownloadFolder(\"/\",FileUtil.file(\"d:/test/download\"));\n\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void recursiveDownloadFolderSftp() {\n\t\tfinal Sftp ftp = new Sftp(\"127.0.0.1\", 22, \"test\", \"test\");\n\n\t\tftp.cd(\"/file/aaa\");\n\t\tConsole.log(ftp.pwd());\n\t\tftp.recursiveDownloadFolder(\"/\",FileUtil.file(\"d:/test/download\"));\n\n\t\tIoUtil.close(ftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTest() {\n\t\tString downloadPath = \"d:/test/download/\";\n\t\ttry (final Ftp ftp = new Ftp(\"localhost\")) {\n\t\t\tfinal List<FTPFile> ftpFiles = ftp.lsFiles(\"temp/\", null);\n\t\t\tfor (final FTPFile ftpFile : ftpFiles) {\n\t\t\t\tString name = ftpFile.getName();\n\t\t\t\tif (ftpFile.isDirectory()) {\n\t\t\t\t\tFile dp = new File(downloadPath + name);\n\t\t\t\t\tif (!dp.exists()) {\n\t\t\t\t\t\tdp.mkdir();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tftp.download(\"\", name, FileUtil.file(downloadPath + name));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void isDirTest() throws Exception {\n\t\ttry (final Ftp ftp = new Ftp(\"127.0.0.1\", 21)) {\n\t\t\tConsole.log(ftp.pwd());\n\t\t\tftp.isDir(\"/test\");\n\t\t\tConsole.log(ftp.pwd());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void existSftpTest() throws Exception {\n\t\ttry (final Sftp ftp = new Sftp(\"127.0.0.1\", 22, \"test\", \"test\")) {\n\t\t\tConsole.log(ftp.pwd());\n\t\t\tConsole.log(ftp.exist(null));\n\t\t\tConsole.log(ftp.exist(\"\"));\n\t\t\tConsole.log(ftp.exist(\".\"));\n\t\t\tConsole.log(ftp.exist(\"..\"));\n\t\t\tConsole.log(ftp.exist(\"/\"));\n\t\t\tConsole.log(ftp.exist(\"a\"));\n\t\t\tConsole.log(ftp.exist(\"/home/test\"));\n\t\t\tConsole.log(ftp.exist(\"/home/test/\"));\n\t\t\tConsole.log(ftp.exist(\"/home/test//////\"));\n\t\t\tConsole.log(ftp.exist(\"/home/test/file1\"));\n\t\t\tConsole.log(ftp.exist(\"/home/test/file1/\"));\n\t\t\tConsole.log(ftp.exist(\"///////////\"));\n\t\t\tConsole.log(ftp.exist(\"./\"));\n\t\t\tConsole.log(ftp.exist(\"./file1\"));\n\t\t\tConsole.log(ftp.pwd());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void existFtpTest() throws Exception {\n\t\ttry (final Ftp ftp = new Ftp(\"127.0.0.1\", 21)) {\n\t\t\tConsole.log(ftp.pwd());\n\t\t\tConsole.log(ftp.exist(null));\n\t\t\tConsole.log(ftp.exist(\"\"));\n\t\t\tConsole.log(ftp.exist(\".\"));\n\t\t\tConsole.log(ftp.exist(\"..\"));\n\t\t\tConsole.log(ftp.exist(\"/\"));\n\t\t\tConsole.log(ftp.exist(\"a\"));\n\t\t\tConsole.log(ftp.exist(\"/test\"));\n\t\t\tConsole.log(ftp.exist(\"/test/\"));\n\t\t\tConsole.log(ftp.exist(\"/test//////\"));\n\t\t\tConsole.log(ftp.exist(\"/test/..\"));\n\t\t\tConsole.log(ftp.exist(\"/test/.\"));\n\t\t\tConsole.log(ftp.exist(\"/file1\"));\n\t\t\tConsole.log(ftp.exist(\"/file1/\"));\n\t\t\tConsole.log(ftp.exist(\"///////////\"));\n\t\t\tConsole.log(ftp.exist(\"./\"));\n\t\t\tConsole.log(ftp.exist(\"./file1\"));\n\t\t\tConsole.log(ftp.exist(\"./2/3/4/..\"));\n\t\t\tConsole.log(ftp.pwd());\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameTest() {\n\t\tfinal Ftp ftp = new Ftp(\"localhost\", 21, \"test\", \"test\");\n\n\t\tftp.mkdir(\"/ftp-1\");\n\t\tassertTrue(ftp.exist(\"/ftp-1\"));\n\t\tftp.rename(\"/ftp-1\", \"/ftp-2\");\n\t\tassertTrue(ftp.exist(\"/ftp-2\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/ftp/SimpleFtpServerTest.java",
    "content": "package cn.hutool.extra.ftp;\n\npublic class SimpleFtpServerTest {\n\n\tpublic static void main(String[] args) {\n\t\tSimpleFtpServer\n\t\t\t\t.create()\n\t\t\t\t.addAnonymous(\"d:/test/ftp/\")\n\t\t\t\t.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/mail/JakartaMailTest.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 邮件发送测试\n * @author looly\n *\n */\npublic class JakartaMailTest {\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithFileTest() {\n\t\tJakartaMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true, FileUtil.file(\"d:/测试附件文本.txt\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithLongNameFileTest() {\n\t\t//附件名长度大于60时的测试\n\t\tJakartaMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true, FileUtil.file(\"d:/6-LongLong一阶段平台建设周报2018.3.12-3.16.xlsx\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithImageTest() {\n\t\tMap<String, InputStream> map = new HashMap<>();\n\t\tmap.put(\"testImage\", FileUtil.getInputStream(\"f:/test/me.png\"));\n\t\tJakartaMailUtil.sendHtml(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1><img src=\\\"cid:testImage\\\" />\", map);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendHtmlWithImageTest() {\n\t\tMap<String, InputStream> map = new HashMap<>();\n\t\tInputStream in = getClass().getClassLoader().getResourceAsStream(\"image/hutool.png\");\n\t\tmap.put(\"<image-1>\", in);\n\t\tJakartaMailUtil.sendHtml(\"hutool@foxmail.com;li7hai26@outlook.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1><img src=\\\"cid:image-1\\\" />\", map);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendHtmlTest() {\n\t\tJakartaMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendByAccountTest() {\n\t\tMailAccount account = new MailAccount();\n\t\taccount.setHost(\"smtp.yeah.net\");\n\t\taccount.setPort(465);\n\t\taccount.setSslEnable(true);\n\t\taccount.setFrom(\"hutool@yeah.net\");\n\t\taccount.setUser(\"hutool\");\n\t\taccount.setPass(\"123456\");\n\t\tJakartaMailUtil.send(account, \"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true);\n\t}\n\n\t@Test\n\tpublic void mailAccountTest() {\n\t\tMailAccount account = new MailAccount();\n\t\taccount.setFrom(\"hutool@yeah.net\");\n\t\taccount.setDebug(true);\n\t\taccount.defaultIfEmpty();\n\t\tProperties props = account.getSmtpProps();\n\t\tassertEquals(\"true\", props.getProperty(\"mail.debug\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/mail/MailAccountTest.java",
    "content": "package cn.hutool.extra.mail;\n\nimport com.sun.mail.util.MailSSLSocketFactory;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.GeneralSecurityException;\n\n/**\n * 默认邮件帐户设置测试\n *\n * @author looly\n */\npublic class MailAccountTest {\n\n\t@Test\n\tpublic void parseSettingTest() {\n\t\tMailAccount account = GlobalMailAccount.INSTANCE.getAccount();\n\t\taccount.getSmtpProps();\n\n\t\tassertNotNull(account.getCharset());\n\t\tassertTrue(account.isSslEnable());\n\t}\n\n\t/**\n\t * 测试案例：使用QQ邮箱、AOL邮箱，如果不改SocketFactory实例，会报错（unable to find valid certification path to requested target），\n\t * hutool mail中仅提供了'mail.smtp.socketFactory.class'属性，但是没提供'mail.smtp.ssl.socketFactory'属性\n\t * 参见 com.sun.mail.util.SocketFetcher.getSocket(java.lang.String, int, java.util.Properties, java.lang.String, boolean)\n\t * <p>\n\t * 已经测试通过\n\t */\n\t@Test\n\t@Disabled\n\tpublic void customPropertyTest() throws GeneralSecurityException {\n\t\tMailAccount mailAccount = new MailAccount();\n\t\tmailAccount.setFrom(\"xxx@xxx.com\");\n\t\tmailAccount.setPass(\"xxxxxx\");\n\n\t\tmailAccount.setHost(\"smtp.aol.com\");\n\n\t\t// 使用其他配置属性\n\t\tMailSSLSocketFactory sf = new MailSSLSocketFactory();\n\t\tsf.setTrustAllHosts(true);\n\t\tmailAccount.setCustomProperty(\"mail.smtp.ssl.socketFactory\", sf);\n\n\t\tmailAccount.setAuth(true);\n\t\tmailAccount.setSslEnable(true);\n\n\t\tMail mail = Mail.create(mailAccount)\n\t\t\t\t.setTos(\"xx@xx.com\")\n\t\t\t\t.setTitle(\"邮箱验证\")\n\t\t\t\t.setContent(\"您的验证码是：<h3>2333</h3>\")\n\t\t\t\t.setHtml(true);\n\n\t\tmail.send();\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java",
    "content": "package cn.hutool.extra.mail;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * 邮件发送测试\n * @author looly\n *\n */\npublic class MailTest {\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithFileTest() {\n\t\tMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true, FileUtil.file(\"d:/测试附件文本.txt\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithLongNameFileTest() {\n\t\t//附件名长度大于60时的测试\n\t\tMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true, FileUtil.file(\"d:/6-LongLong一阶段平台建设周报2018.3.12-3.16.xlsx\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendWithImageTest() {\n\t\tMap<String, InputStream> map = new HashMap<>();\n\t\tmap.put(\"testImage\", FileUtil.getInputStream(\"f:/test/me.png\"));\n\t\tMailUtil.sendHtml(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1><img src=\\\"cid:testImage\\\" />\", map);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendHtmlWithImageTest() {\n\t\tMap<String, InputStream> map = new HashMap<>();\n\t\tInputStream in = getClass().getClassLoader().getResourceAsStream(\"image/hutool.png\");\n\t\tmap.put(\"<image-1>\", in);\n\t\tMailUtil.sendHtml(\"hutool@foxmail.com;li7hai26@outlook.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1><img src=\\\"cid:image-1\\\" />\", map);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendHtmlTest() {\n\t\tMailUtil.send(\"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sendByAccountTest() {\n\t\tMailAccount account = new MailAccount();\n\t\taccount.setHost(\"smtp.yeah.net\");\n\t\taccount.setPort(465);\n\t\taccount.setSslEnable(true);\n\t\taccount.setFrom(\"hutool@yeah.net\");\n\t\taccount.setUser(\"hutool\");\n\t\taccount.setPass(\"123456\");\n\t\tMailUtil.send(account, \"hutool@foxmail.com\", \"测试\", \"<h1>邮件来自Hutool测试</h1>\", true);\n\t}\n\n\t@Test\n\tpublic void mailAccountTest() {\n\t\tMailAccount account = new MailAccount();\n\t\taccount.setFrom(\"hutool@yeah.net\");\n\t\taccount.setDebug(true);\n\t\taccount.defaultIfEmpty();\n\t\tProperties props = account.getSmtpProps();\n\t\tassertEquals(\"true\", props.getProperty(\"mail.debug\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/Bopomofo4jTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.extra.pinyin.engine.bopomofo4j.Bopomofo4jEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Bopomofo4jTest {\n\n\tfinal Bopomofo4jEngine engine = new Bopomofo4jEngine();\n\n\t@Test\n\tpublic void getFirstLetterByBopomofo4jTest(){\n\t\tfinal String result = engine.getFirstLetter(\"林海\", \"\");\n\t\tassertEquals(\"lh\", result);\n\t}\n\n\t@Test\n\tpublic void getPinyinByBopomofo4jTest() {\n\t\tfinal String pinyin = engine.getPinyin(\"你好h\", \" \");\n\t\tassertEquals(\"ni haoh\", pinyin);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/HoubbPinyinTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.extra.pinyin.engine.houbbpinyin.HoubbPinyinEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class HoubbPinyinTest {\n\n\tfinal HoubbPinyinEngine engine = new HoubbPinyinEngine();\n\n\t@Test\n\tpublic void getFirstLetterTest(){\n\t\tfinal String result = engine.getFirstLetter(\"林海\", \"\");\n\t\tassertEquals(\"lh\", result);\n\t}\n\n\t@Test\n\tpublic void getPinyinTest() {\n\t\tfinal String pinyin = engine.getPinyin(\"你好h\", \" \");\n\t\tassertEquals(\"ni hao h\", pinyin);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/JpinyinTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.extra.pinyin.engine.jpinyin.JPinyinEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class JpinyinTest {\n\n\tfinal JPinyinEngine engine = new JPinyinEngine();\n\n\t@Test\n\tpublic void getFirstLetterByPinyin4jTest(){\n\t\tfinal String result = engine.getFirstLetter(\"林海\", \"\");\n\t\tassertEquals(\"lh\", result);\n\t}\n\n\t@Test\n\tpublic void getPinyinByPinyin4jTest() {\n\t\tfinal String pinyin = engine.getPinyin(\"你好h\", \" \");\n\t\tassertEquals(\"ni hao h\", pinyin);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/Pinyin4jTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.extra.pinyin.engine.pinyin4j.Pinyin4jEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Pinyin4jTest {\n\n\tfinal Pinyin4jEngine engine = new Pinyin4jEngine();\n\n\t@Test\n\tpublic void getFirstLetterByPinyin4jTest(){\n\t\tfinal String result = engine.getFirstLetter(\"林海\", \"\");\n\t\tassertEquals(\"lh\", result);\n\t}\n\n\t@Test\n\tpublic void getPinyinByPinyin4jTest() {\n\t\tfinal String pinyin = engine.getPinyin(\"你好h\", \" \");\n\t\tassertEquals(\"ni hao h\", pinyin);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/PinyinUtilTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class PinyinUtilTest {\n\n\t@Test\n\tpublic void getPinyinTest(){\n\t\tfinal String pinyin = PinyinUtil.getPinyin(\"你好怡\", \" \");\n\t\tassertEquals(\"ni hao yi\", pinyin);\n\t}\n\n\t@Test\n\tpublic void getFirstLetterTest(){\n\t\tfinal String result = PinyinUtil.getFirstLetter(\"H是第一个\", \", \");\n\t\tassertEquals(\"h, s, d, y, g\", result);\n\t}\n\n\t@Test\n\tpublic void getFirstLetterTest2(){\n\t\tfinal String result = PinyinUtil.getFirstLetter(\"崞阳\", \", \");\n\t\tassertEquals(\"g, y\", result);\n\t}\n\n\t@Test\n\tpublic void getFirstLetterTest3(){\n\t\tfinal String result = PinyinUtil.getFirstLetter(null, \", \");\n\t\tassertNull(result);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/pinyin/TinyPinyinTest.java",
    "content": "package cn.hutool.extra.pinyin;\n\nimport cn.hutool.extra.pinyin.engine.tinypinyin.TinyPinyinEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class TinyPinyinTest {\n\n\tfinal TinyPinyinEngine engine = new TinyPinyinEngine();\n\n\t@Test\n\tpublic void getFirstLetterByPinyin4jTest(){\n\t\tfinal String result = engine.getFirstLetter(\"林海\", \"\");\n\t\tassertEquals(\"lh\", result);\n\t}\n\n\t@Test\n\tpublic void getPinyinByPinyin4jTest() {\n\t\tfinal String pinyin = engine.getPinyin(\"你好h\", \" \");\n\t\tassertEquals(\"ni hao h\", pinyin);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/qrcode/IssueI7RUIVTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.extra.qrcode;\n\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Color;\nimport java.io.File;\n\npublic class IssueI7RUIVTest {\n\n\t@Test\n\t@Disabled\n\tpublic void generateTest() {\n\t\tfinal QrConfig config = new QrConfig(300, 300);\n\n\t\t// 设置前景色，既二维码颜色（青色）\n\t\tconfig.setForeColor(Color.CYAN);\n\t\t// 设置背景色（灰色）\n\t\tconfig.setBackColor(Color.GRAY);\n\n\t\t// 生成二维码到文件，也可以到流\n\t\tfinal File file = QrCodeUtil.generate(\"https://hutool.cn/\", config, FileUtil.file(\"d:/test/qrcode.jpg\"));\n\n\t\t// 识别二维码\n\t\t// decode -> \"http://hutool.cn/\"\n\t\tfinal String decode = QrCodeUtil.decode(ImgUtil.read(\"d:/test/qrcode.jpg\"), true, true);\n\t\tConsole.log(\"decode info = \" + decode);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java",
    "content": "package cn.hutool.extra.qrcode;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.img.ImgUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Assert;\nimport com.google.zxing.BarcodeFormat;\nimport com.google.zxing.datamatrix.encoder.SymbolShapeHint;\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.Color;\nimport java.awt.image.BufferedImage;\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * 二维码工具类单元测试\n *\n * @author looly\n */\npublic class QrCodeUtilTest {\n\n\t@Test\n\tpublic void generateTest() {\n\t\tfinal BufferedImage image = QrCodeUtil.generate(\"https://hutool.cn/\", 300, 300);\n\t\tAssert.notNull(image);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void generateCustomTest() {\n\t\tfinal QrConfig config = new QrConfig();\n\t\tconfig.setMargin(0);\n\t\tconfig.setForeColor(Color.CYAN);\n\t\t// 背景色透明\n\t\tconfig.setBackColor(null);\n\t\tconfig.setErrorCorrection(ErrorCorrectionLevel.H);\n\t\tfinal String path = FileUtil.isWindows() ? \"d:/test/qrcodeCustom.png\" : \"~/Desktop/hutool/qrcodeCustom.png\";\n\t\tQrCodeUtil.generate(\"https://hutool.cn/\", config, FileUtil.touch(path));\n\t}\n\t@Test\n\t@Disabled\n\tpublic void generateNoCustomColorTest() {\n\t\tfinal QrConfig config = new QrConfig();\n\t\tconfig.setMargin(0);\n\t\tconfig.setForeColor(null);\n\t\t// 背景色透明\n\t\tconfig.setBackColor(null);\n\t\tconfig.setErrorCorrection(ErrorCorrectionLevel.H);\n\t\tfinal String path = FileUtil.isWindows() ? \"d:/test/qrcodeCustom.png\" : \"~/Desktop/hutool/qrcodeCustom.png\";\n\t\tQrCodeUtil.generate(\"https://hutool.cn/\", config, FileUtil.touch(path));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void generateWithLogoTest() {\n\t\tfinal String icon = FileUtil.isWindows() ? \"d:/test/pic/face.jpg\" : \"~/Desktop/hutool/pic/face.jpg\";\n\t\tfinal String targetPath = FileUtil.isWindows() ? \"d:/test/qrcodeWithLogo.jpg\" : \"~/Desktop/hutool/qrcodeWithLogo.jpg\";\n\t\tQrCodeUtil.generate(//\n\t\t\t\t\"https://hutool.cn/\", //\n\t\t\t\tQrConfig.create().setImg(icon), //\n\t\t\t\tFileUtil.touch(targetPath));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void decodeTest() {\n\t\tfinal String decode = QrCodeUtil.decode(FileUtil.file(\"d:/test/pic/qr.png\"));\n\t\t//Console.log(decode);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void decodeTest2() {\n\t\t// 条形码\n\t\tfinal String decode = QrCodeUtil.decode(FileUtil.file(\"d:/test/90.png\"));\n\t\t//Console.log(decode);\n\t}\n\n\t@Test\n\tpublic void generateAsBase64Test() {\n\t\tfinal String base64 = QrCodeUtil.generateAsBase64(\"https://hutool.cn/\", new QrConfig(400, 400), \"png\");\n\t\tAssert.notNull(base64);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void generateAsBase64Test2() {\n\t\tfinal byte[] bytes = FileUtil.readBytes(\n\t\t\t\tnew File(\"d:/test/qr.png\"));\n\t\tfinal String encode = Base64.encode(bytes);\n\t\tfinal String base641 = QrCodeUtil.generateAsBase64(\"https://hutool.cn/\", new QrConfig(400, 400), \"png\", encode);\n\t\tAssert.notNull(base641);\n\t}\n\n\t@Test\n\tpublic void generateAsBase64Test3() {\n\t\tfinal String base64 = QrCodeUtil.generateAsBase64(\"https://hutool.cn/\", new QrConfig(400, 400), \"svg\");\n\t\tAssert.notNull(base64);\n\t\t//Console.log(base64);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void decodeTest3() {\n\t\tfinal String decode = QrCodeUtil.decode(ImgUtil.read(\"d:/test/qr_a.png\"), false, true);\n\t\t//Console.log(decode);\n\t}\n\n\t@Test\n\tpublic void pdf417Test() {\n\t\tfinal BufferedImage image = QrCodeUtil.generate(\"content111\", BarcodeFormat.PDF_417, QrConfig.create());\n\t\tAssert.notNull(image);\n\t}\n\n\t@Test\n\tpublic void generateDataMatrixTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create();\n\t\tqrConfig.setShapeHint(SymbolShapeHint.FORCE_RECTANGLE);\n\t\tfinal BufferedImage image = QrCodeUtil.generate(\"content111\", BarcodeFormat.DATA_MATRIX, qrConfig);\n\t\tAssert.notNull(image);\n\t\tfinal QrConfig config = QrConfig.create();\n\t\tconfig.setShapeHint(SymbolShapeHint.FORCE_SQUARE);\n\t\tfinal BufferedImage imageSquare = QrCodeUtil.generate(\"content111\", BarcodeFormat.DATA_MATRIX, qrConfig);\n\t\tAssert.notNull(imageSquare);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void generateSvgTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setImg(\"d:/test/logo.png\")\n\t\t\t\t.setForeColor(Color.blue)\n\t\t\t\t.setBackColor(Color.pink)\n\t\t\t\t.setRatio(8)\n\t\t\t\t.setErrorCorrection(ErrorCorrectionLevel.M)\n\t\t\t\t.setMargin(1);\n\t\tfinal String svg = QrCodeUtil.generateAsSvg(\"https://hutool.cn/\", qrConfig);\n\t\tAssert.notNull(svg);\n\t\tFileUtil.writeString(svg, FileUtil.touch(\"d:/test/hutool_qr.svg\"),StandardCharsets.UTF_8);\n\t}\n\n\t@Test\n\tpublic void generateAsciiArtTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setForeColor(Color.BLUE)\n\t\t\t\t.setBackColor(Color.MAGENTA)\n\t\t\t\t.setWidth(0)\n\t\t\t\t.setHeight(0).setMargin(1);\n\t\tfinal String asciiArt = QrCodeUtil.generateAsAsciiArt(\"https://hutool.cn/\",qrConfig);\n\t\tAssert.notNull(asciiArt);\n\t\t//Console.log(asciiArt);\n\t}\n\n\t@Test\n\tpublic void generateAsciiArtNoCustomColorTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setForeColor(null)\n\t\t\t\t.setBackColor(null)\n\t\t\t\t.setWidth(0)\n\t\t\t\t.setHeight(0).setMargin(1);\n\t\tfinal String asciiArt = QrCodeUtil.generateAsAsciiArt(\"https://hutool.cn/\",qrConfig);\n\t\tAssert.notNull(asciiArt);\n\t\t//Console.log(asciiArt);\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void generateToFileTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setForeColor(Color.BLUE)\n\t\t\t\t.setBackColor(new Color(0,200,255))\n\t\t\t\t.setWidth(0)\n\t\t\t\t.setHeight(0).setMargin(1);\n\t\tfinal File qrFile = QrCodeUtil.generate(\"https://hutool.cn/\", qrConfig, FileUtil.touch(\"d:/test/ascii_art_qr_code.txt\"));\n\t\t//final BufferedReader reader = FileUtil.getReader(qrFile, StandardCharsets.UTF_8);\n\t\t//reader.lines().forEach(Console::log);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void generateToStreamTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setForeColor(Color.BLUE)\n\t\t\t\t.setBackColor(new Color(0,200,255))\n\t\t\t\t.setWidth(0)\n\t\t\t\t.setHeight(0).setMargin(1);\n\t\tfinal String filepath = \"d:/test/qr_stream_to_txt.txt\";\n\t\ttry (final BufferedOutputStream outputStream = FileUtil.getOutputStream(filepath)) {\n\t\t\tQrCodeUtil.generate(\"https://hutool.cn/\", qrConfig,\"txt\", outputStream);\n\t\t}catch (final IOException e){\n\t\t\te.printStackTrace();\n\t\t}\n\t\t//final BufferedReader reader = FileUtil.getReader(filepath, StandardCharsets.UTF_8);\n\t\t//reader.lines().forEach(Console::log);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void comparePngAndSvgAndAsciiArtTest() {\n\t\tfinal QrConfig qrConfig = QrConfig.create()\n\t\t\t\t.setForeColor(null)\n\t\t\t\t.setBackColor(Color.WHITE)\n\t\t\t\t.setWidth(200)\n\t\t\t\t.setHeight(200).setMargin(1);\n\t\tQrCodeUtil.generate(\"https://hutool.cn\", qrConfig, FileUtil.touch(\"d:/test/compare/config_null_color.jpg\"));\n\t\tQrCodeUtil.generate(\"https://hutool.cn\", qrConfig, FileUtil.touch(\"d:/test/compare/config_null_color.txt\"));\n\t\tQrCodeUtil.generate(\"https://hutool.cn\", qrConfig, FileUtil.touch(\"d:/test/compare/config_null_color.png\"));\n\t\tQrCodeUtil.generate(\"https://hutool.cn\", qrConfig, FileUtil.touch(\"d:/test/compare/config_null_color.svg\"));\n\t}\n\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/servlet/ServletUtilTest.java",
    "content": "package cn.hutool.extra.servlet;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.ByteArrayInputStream;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * ServletUtil工具类测试\n *\n * @author dazer\n * @see ServletUtil\n * @see JakartaServletUtil\n */\npublic class ServletUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tHttpServletResponse response = null;\n\t\tbyte[] bytes = StrUtil.utf8Bytes(\"地球是我们共同的家园，需要大家珍惜.\");\n\n\t\t//下载文件\n\t\t// 这里没法直接测试，直接写到这里，方便调用；\n\t\t//noinspection ConstantConditions\n\t\tif (response != null) {\n\t\t\tString fileName = \"签名文件.pdf\";\n\t\t\tString contentType = \"application/pdf\";// application/octet-stream、image/jpeg、image/gif\n\t\t\tresponse.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 必须设置否则乱码; 但是 safari乱码\n\t\t\tServletUtil.write(response, new ByteArrayInputStream(bytes), contentType, fileName);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void jakartaWriteTest() {\n\t\tjakarta.servlet.http.HttpServletResponse response = null;\n\t\tbyte[] bytes = StrUtil.utf8Bytes(\"地球是我们共同的家园，需要大家珍惜.\");\n\n\t\t//下载文件\n\t\t// 这里没法直接测试，直接写到这里，方便调用；\n\t\t//noinspection ConstantConditions\n\t\tif (response != null) {\n\t\t\tString fileName = \"签名文件.pdf\";\n\t\t\tString contentType = \"application/pdf\";// application/octet-stream、image/jpeg、image/gif\n\t\t\tresponse.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 必须设置否则乱码; 但是 safari乱码\n\t\t\tJakartaServletUtil.write(response, new ByteArrayInputStream(bytes), contentType, fileName);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSpringUtilTest.java",
    "content": "package cn.hutool.extra.spring;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * @author sidian\n */\n@SpringBootTest(classes = EnableSpringUtilTest.class)\n@EnableSpringUtil\n@ExtendWith(SpringExtension.class)\npublic class EnableSpringUtilTest {\n\n\t@Test\n\tpublic void test() {\n\t\t// 使用@EnableSpringUtil注解后, 能获取上下文\n\t\tassertNotNull(SpringUtil.getApplicationContext());\n\t\t// 不使用时, 为null\n//        assertNull(SpringUtil.getApplicationContext());\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/spring/SpringUtilTest.java",
    "content": "package cn.hutool.extra.spring;\n\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.test.context.TestPropertySource;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\n\nimport javax.annotation.Resource;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@ExtendWith(SpringExtension.class)\n@SpringBootTest(classes = {SpringUtil.class, SpringUtilTest.Demo2.class})\n// @ActiveProfiles(\"dev\") // SpringUtil.getActiveProfile()效果与下面方式一致\n@TestPropertySource(properties = {\"spring.profiles.active=dev\"})\npublic class SpringUtilTest {\n\n\t/**\n\t * 注册验证bean\n\t */\n\t@Test\n\tpublic void registerBeanTest() {\n\t\tDemo2 registerBean = new Demo2();\n\t\tregisterBean.setId(123);\n\t\tregisterBean.setName(\"222\");\n\t\tSpringUtil.registerBean(\"registerBean\", registerBean);\n\n\t\tDemo2 registerBean2 = SpringUtil.getBean(\"registerBean\");\n\t\tassertEquals(123, registerBean2.getId());\n\t\tassertEquals(\"222\", registerBean2.getName());\n\n\n\t}\n\n\t/**\n\t * 测试注销bean\n\t */\n\t@Test\n\tpublic void unregisterBeanTest() {\n\t\tregisterTestAutoWired();\n\t\tassertNotNull(SpringUtil.getBean(\"testAutoWired\"));\n\t\tSpringUtil.unregisterBean(\"testAutoWired1\");\n\t\ttry {\n\t\t\tSpringUtil.getBean(\"testAutoWired\");\n\t\t} catch (NoSuchBeanDefinitionException e) {\n\t\t\tassertEquals(e.getClass(), NoSuchBeanDefinitionException.class);\n\t\t}\n\t}\n\n\t/**\n\t * 测试自动注入\n\t */\n\tprivate void registerTestAutoWired() {\n\t\tTestAutoWired testAutoWired = new TestAutoWired();\n\t\tTestBean testBean = new TestBean();\n\t\ttestBean.setId(\"123\");\n\t\tSpringUtil.registerBean(\"testBean\", testBean);\n\t\tSpringUtil.registerBean(\"testAutoWired\", testAutoWired);\n\n\t\ttestAutoWired = SpringUtil.getBean(\"testAutoWired\");\n\t\tassertNotNull(testAutoWired);\n\t\tassertNotNull(testAutoWired.getAutowiredBean());\n\t\tassertNotNull(testAutoWired.getResourceBean());\n\t\tassertEquals(\"123\", testAutoWired.getAutowiredBean().getId());\n\n\t}\n\n\t@Test\n\tpublic void getBeanTest(){\n\t\tfinal Demo2 testDemo = SpringUtil.getBean(\"testDemo\");\n\t\tassertEquals(12345, testDemo.getId());\n\t\tassertEquals(\"test\", testDemo.getName());\n\t}\n\n\t@Test\n\tpublic void getBeanWithTypeReferenceTest() {\n\t\tMap<String, Object> mapBean = SpringUtil.getBean(new TypeReference<Map<String, Object>>() {});\n\t\tassertNotNull(mapBean);\n\t\tassertEquals(\"value1\", mapBean.get(\"key1\"));\n\t\tassertEquals(\"value2\", mapBean.get(\"key2\"));\n\t}\n\n\t@Data\n\tpublic static class Demo2{\n\t\tprivate long id;\n\t\tprivate String name;\n\n\t\t@Bean(name=\"testDemo\")\n\t\tpublic Demo2 generateDemo() {\n\t\t\tDemo2 demo = new Demo2();\n\t\t\tdemo.setId(12345);\n\t\t\tdemo.setName(\"test\");\n\t\t\treturn demo;\n\t\t}\n\n\t\t@Bean(name=\"mapDemo\")\n\t\tpublic Map<String, Object> generateMap() {\n\t\t\tHashMap<String, Object> map = MapUtil.newHashMap();\n\t\t\tmap.put(\"key1\", \"value1\");\n\t\t\tmap.put(\"key2\", \"value2\");\n\t\t\treturn map;\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class TestAutoWired {\n\n\t\t@Autowired\n\t\t// @Resource\n\t\tprivate TestBean autowiredBean;\n\n\t\t @Resource\n\t\tprivate TestBean resourceBean;\n\t}\n\n\t@Data\n\tpublic static class TestBean {\n\t\tprivate String id;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/spring/SpringUtilWithAutoConfigTest.java",
    "content": "package cn.hutool.extra.spring;\n\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.MapUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.springframework.boot.autoconfigure.EnableAutoConfiguration;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@ExtendWith(SpringExtension.class)\n@SpringBootTest(classes = {SpringUtilWithAutoConfigTest.Demo2.class})\n@EnableAutoConfiguration\npublic class SpringUtilWithAutoConfigTest {\n\n\t/**\n\t * 注册验证bean\n\t */\n\t@Test\n\tpublic void registerBeanTest() {\n\t\tDemo2 registerBean = new Demo2();\n\t\tregisterBean.setId(123);\n\t\tregisterBean.setName(\"222\");\n\t\tSpringUtil.registerBean(\"registerBean\", registerBean);\n\n\t\tDemo2 registerBean2 = SpringUtil.getBean(\"registerBean\");\n\t\tassertEquals(123, registerBean2.getId());\n\t\tassertEquals(\"222\", registerBean2.getName());\n\t}\n\n\t@Test\n\tpublic void getBeanTest(){\n\t\tfinal Demo2 testDemo = SpringUtil.getBean(\"testDemo\");\n\t\tassertEquals(12345, testDemo.getId());\n\t\tassertEquals(\"test\", testDemo.getName());\n\t}\n\n\t@Test\n\tpublic void getBeanWithTypeReferenceTest() {\n\t\tMap<String, Object> mapBean = SpringUtil.getBean(new TypeReference<Map<String, Object>>() {});\n\t\tassertNotNull(mapBean);\n\t\tassertEquals(\"value1\", mapBean.get(\"key1\"));\n\t\tassertEquals(\"value2\", mapBean.get(\"key2\"));\n\t}\n\n\t@Data\n\tpublic static class Demo2{\n\t\tprivate long id;\n\t\tprivate String name;\n\n\t\t@Bean(name=\"testDemo\")\n\t\tpublic Demo2 generateDemo() {\n\t\t\tDemo2 demo = new Demo2();\n\t\t\tdemo.setId(12345);\n\t\t\tdemo.setName(\"test\");\n\t\t\treturn demo;\n\t\t}\n\n\t\t@Bean(name=\"mapDemo\")\n\t\tpublic Map<String, Object> generateMap() {\n\t\t\tHashMap<String, Object> map = MapUtil.newHashMap();\n\t\t\tmap.put(\"key1\", \"value1\");\n\t\t\tmap.put(\"key2\", \"value2\");\n\t\t\treturn map;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/ssh/JschUtilTest.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Console;\nimport com.jcraft.jsch.Session;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\n\n/**\n * Jsch工具类单元测试\n *\n * @author looly\n *\n */\npublic class JschUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void bindPortTest() {\n\t\t//新建会话，此会话用于ssh连接到跳板机（堡垒机），此处为10.1.1.1:22\n\t\tSession session = JschUtil.getSession(\"looly.centos\", 22, \"test\", \"123456\");\n\t\t// 将堡垒机保护的内网8080端口映射到localhost，我们就可以通过访问http://localhost:8080/访问内网服务了\n\t\tJschUtil.bindPort(session, \"172.20.12.123\", 8080, 8080);\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void bindRemotePort() throws InterruptedException {\n\t\t// 建立会话\n\t\tSession session = JschUtil.getSession(\"looly.centos\", 22, \"test\", \"123456\");\n\t\t// 绑定ssh服务端8089端口到本机的8000端口上\n\t\tboolean b = JschUtil.bindRemotePort(session, 8089, \"localhost\", 8000);\n\t\tassertTrue(b);\n\t\t// 保证一直运行\n//\t\twhile (true){\n//\t\t\tThread.sleep(3000);\n//\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sftpTest() {\n\t\tSession session = JschUtil.getSession(\"looly.centos\", 22, \"root\", \"123456\");\n\t\tSftp sftp = JschUtil.createSftp(session);\n\t\tsftp.mkDirs(\"/opt/test/aaa/bbb\");\n\t\tConsole.log(\"OK\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void reconnectIfTimeoutTest() throws InterruptedException {\n\t\tSession session = JschUtil.getSession(\"sunnyserver\", 22,\"mysftp\",\"liuyang1234\");\n\t\tSftp sftp = JschUtil.createSftp(session);\n\n\t\tConsole.log(\"打印pwd: \" + sftp.pwd());\n\t\tConsole.log(\"cd / : \" + sftp.cd(\"/\"));\n\t\tConsole.log(\"休眠一段时间，查看是否超时\");\n\t\tThread.sleep(30 * 1000);\n\n\t\ttry{\n\t\t\t// 当连接超时时，isConnected()仍然返回true，pwd命令也能正常返回，因此，利用发送cd命令的返回结果，来判断是否连接超时\n\t\t\tConsole.log(\"isConnected \" + sftp.getClient().isConnected());\n\t\t\tConsole.log(\"打印pwd: \" + sftp.pwd());\n\t\t\tConsole.log(\"cd / : \" + sftp.cd(\"/\"));\n\t\t}catch (JschRuntimeException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\n\t\tConsole.log(\"调用reconnectIfTimeout方法，判断是否超时并重连\");\n\t\tsftp.reconnectIfTimeout();\n\n\t\tConsole.log(\"打印pwd: \" + sftp.pwd());\n\n\t\tIoUtil.close(sftp);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getSessionTest(){\n\t\tJschUtil.getSession(\"192.168.1.134\", 22, \"root\", \"aaa\", null);\n\t}\n\t@Test\n\t@Disabled\n\tpublic void sftpPrivateKeyTest(){\n\t\tSession session = JschUtil.getSession(\"192.168.1.109\", 22, \"root\", (\"-----BEGIN RSA PRIVATE KEY-----\\n\" +\n\t\t\t\"MIIEpAIBAAKCAQEA5SJ1bjhSA0uQJjbbF/LCFiQvs+nMKgkSnSE+JEll7azv7jnh\\n\" +\n\t\t\t\"oBEJdg63tf66oDXCDCMdrYbTtenw1TqkQI2PO8sHuvAZ2UUjqk5zlcrWLiNTCWBw\\n\" +\n\t\t\t\"IgIxbVj3/nQliaZSufLxepf8qr6wXWP/PG6p+ScFhTSGjK2Z4r/t9cqPaTtfYJye\\n\" +\n\t\t\t\"lbXNsrn0JK99XGj3cNvHzljAHRUCwGRTiHAOJ7Gk9WLGcZQRKuMPrQFMnoLoxZ8m\\n\" +\n\t\t\t\"0y+1+AoND6pKLad9/52JCHBi5d7XgwPPEKxVqi8Aklpgb45G4A3FXSKx3tkolXAP\\n\" +\n\t\t\t\"3zGm95CwmqdI9Q8t72SkOWDJR4lFPb12k6H0FwIDAQABAoIBAQCTeO4jllQSktuf\\n\" +\n\t\t\t\"/MZeT3vjTD73iI5Cr7wvLWoVaLgVlKyHovE4WD7CoQ5UMDJlUrQlo6RCPvibqIm8\\n\" +\n\t\t\t\"cxWsBnAdh7rd8hJw6DLgNcXmrrnS0CFtc4g4Gzk8q3pRZueSBF5SF66bvJ5+NmTE\\n\" +\n\t\t\t\"dsubVY5IMXk4FmpSuJjGe8jn3QsYKkptOa/s28UekaWzqnstIx42IgS33w7qpUx9\\n\" +\n\t\t\t\"v0XKoOCj41HxwGYexNmOiIufh6dEzZtNZGQb+f7JiIpbClpCXO0Dfi+jkNOBjI6Z\\n\" +\n\t\t\t\"VMLxIdAZpb9Q97g03hWxH+ZQ9UBeYc9n4p2UzKMnXMBM0lu/cx++hyu6OfAxL8Yr\\n\" +\n\t\t\t\"eeZEMIrBAoGBAPslHABM0tCf2UC0vs0mfQFqQiO+Mu9Lp6wGbtvNPt+aNdw+ia+g\\n\" +\n\t\t\t\"CPcvBWdpMKq6eg8XEwLZ59YyoBDlipCG4h/5Xq4wyf77ydamy9mdH6nOcuUDy9KH\\n\" +\n\t\t\t\"07O39wDdU8EH9Jq29lUYIQgkakRGpi45unq10eo8zAy0ggN1hgI1tYwpAoGBAOmQ\\n\" +\n\t\t\t\"bB8hj1e1ozrrMWSv/xIntmRGswR+RwLCJECJy/Oai3SvdB9L1Rh1SUOD6Q/vakXd\\n\" +\n\t\t\t\"pYAzEkEAxm6h+YecxhlwiOFi8rLAWYMLu31LH7WGGVM7SvwLxB/YmL/hpZ0Jqrc6\\n\" +\n\t\t\t\"Au8MoQtFDtP7IntRkC70Sx7GoKqKudFJlnzyv4Y/AoGAHImn9+TC48/2KOMg90DT\\n\" +\n\t\t\t\"XZDMeTFIqmZnZCXK/RECfvgP/LnifWFrA2OFcq3CSPQtoH9XurA2JuHTzHe42hlp\\n\" +\n\t\t\t\"ooZ8msCSg3XrBogni8/N5EbASYO36nFivf4+hAuiU8HqqpX1wc+fGUTCCoYYphIL\\n\" +\n\t\t\t\"PZxhgQNtkFgGmgwFsUSXH5kCgYEAtvxoSSeQ1yW+Qb3cD8d3LjEgy4U8YavRVI7n\\n\" +\n\t\t\t\"ugx7VlphIcUIVDCkPio9gQDKyqpG93/EVyEsDvNdg3WxOpcP+QRaqUJNZNAgEPRT\\n\" +\n\t\t\t\"KsF9kUkDdFsCz18kg9K9Ma/Ggbb+Idj4TXL2hQ7QpDGf/T+Ul8TbSbxSSeqv1BE0\\n\" +\n\t\t\t\"LqY8eR8CgYBqzKpZcqnNuuwZgdLEnZ/rQoeubw4cHoMWBOkP0N/+mgcE/eEpkr0n\\n\" +\n\t\t\t\"9llf+wJg96adgqhwlEHkOMQdsF+FfQA5yJWW4FHA8bfA9YsxjBgheL2RU1Z2iTp4\\n\" +\n\t\t\t\"aLmoAsh17p8MGk/3Zfh10t3tq4c67WlFS2kX2qPBXuwDnm51iNyp2A==\\n\" +\n\t\t\t\"-----END RSA PRIVATE KEY-----\\n\").getBytes(StandardCharsets.UTF_8), null);\n\t\tSftp sftp = JschUtil.createSftp(session);\n\t\tsftp.mkDirs(\"/opt/test/aaa1/bbb1\");\n\t\tConsole.log(\"OK\");\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/ssh/SftpTest.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.Before;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Disabled;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * 基于sshj 框架SFTP 封装测试.\n *\n * @author youyongkun\n * @since 5.7.18\n */\npublic class SftpTest {\n\n\tprivate Sftp sftp;\n\n\t@Before\n\t@Disabled\n\tpublic void init() {\n\t\tsftp = new Sftp(\"127.0.0.1\", 8022, \"test\", \"test\", CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lsTest() {\n\t\tList<String> files = sftp.ls(\"/\");\n\t\tif (files != null && !files.isEmpty()) {\n\t\t\tfiles.forEach(System.out::println);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTest() {\n\t\tsftp.recursiveDownloadFolder(\"/temp/20250427/\", new File(\"D:\\\\temp\\\\20250430\\\\20250427\\\\\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadTest() {\n\t\tsftp.upload(\"/ftp-2/20250430/\", new File(\"D:\\\\temp\\\\20250430\\\\test.txt\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mkDirTest() {\n\t\tboolean flag = sftp.mkdir(\"/ftp-2/20250430-1\");\n\t\tSystem.out.println(\"是否创建成功: \" + flag);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pwdTest() {\n\t\tString pwd = sftp.pwd();\n\t\tSystem.out.println(\"PWD: \" + pwd);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mkDirsTest() {\n\t\t// 在当前目录下批量创建目录\n\t\tsftp.mkDirs(\"/ftp-2/20250430-2/t1/t2/\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delDirTest() {\n\t\tsftp.delDir(\"/ftp-2/20250430-2/t1/t2\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void cdTest() {\n\t\tSystem.out.println(sftp.cd(\"/ftp-2\"));\n\t\tSystem.out.println(sftp.cd(\"/ftp-4\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/ssh/SshjSftpTest.java",
    "content": "package cn.hutool.extra.ssh;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * 基于sshj 框架SFTP 封装测试.\n *\n * @author youyongkun\n * @since 5.7.18\n */\n@Disabled\nclass SshjSftpTest {\n\n\tprivate static SshjSftp sshjSftp;\n\n\t@BeforeAll\n\tpublic static void init() {\n\t\tsshjSftp = new SshjSftp(\"localhost\", 8022, \"test\", \"test\", CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void lsTest() {\n\t\tList<String> files = sshjSftp.ls(\"/\");\n\t\tif (files != null && !files.isEmpty()) {\n\t\t\tfiles.forEach(System.out::println);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTest() {\n\t\tsshjSftp.recursiveDownloadFolder(\"/home/test/temp\", new File(\"C:\\\\Users\\\\akwangl\\\\Downloads\\\\temp\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadTest() {\n\t\tsshjSftp.upload(\"/home/test/temp/\", new File(\"C:\\\\Users\\\\akwangl\\\\Downloads\\\\temp\\\\辽宁_20190718_104324.CIME\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mkDirTest() {\n\t\tboolean flag = sshjSftp.mkdir(\"/home/test/temp\");\n\t\tSystem.out.println(\"是否创建成功: \" + flag);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mkDirsTest() {\n\t\t// 在当前目录下批量创建目录\n\t\tsshjSftp.mkDirs(\"/home/test/temp\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void delDirTest() {\n\t\tsshjSftp.delDir(\"/home/test/temp\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void pwdTest() {\n//\t\tmkDirsTest();\n\t\tsshjSftp.cd(\"/ftp\");\n\t\tString pwd = sshjSftp.pwd();\n\t\tSystem.out.println(\"当前目录: \" + pwd);\n\t\tassertEquals(\"/ftp\", pwd);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renameTest() {\n//\t\tsshjSftp.mkdir(\"/ftp-1\");\n\t\tassertTrue(sshjSftp.exist(\"/ftp-1\"));\n\t\tsshjSftp.rename(\"/ftp-1\", \"/ftp-2\");\n\t\tassertTrue(sshjSftp.exist(\"/ftp-2\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/template/Issue3488Test.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3488Test {\n\t@Test\n\tpublic void freemarkerTest() {\n\t\tfinal TemplateConfig config = new TemplateConfig(\"templates\", TemplateConfig.ResourceMode.CLASSPATH);\n\t\tconfig.setCustomEngine(FreemarkerEngine.class);\n\t\tconfig.setCharset(CharsetUtil.CHARSET_UTF_8);\n\n\t\tfinal TemplateEngine engine = TemplateUtil.createEngine(config);\n\t\tTemplate template = engine.getTemplate(\"issue3488.ftl\");\n\n\t\tfinal UserService userService = new UserService();\n\t\tuserService.setOperator(\"hutool\");\n\t\tfinal PaymentInfo paymentInfo = new PaymentInfo();\n\t\tpaymentInfo.setUserService(userService);\n\n\t\tString result = template.render(Dict.create().set(\"paymentInfo\", paymentInfo));\n\t\t//Console.log(result);\n\t\t//assertEquals(\"你好,hutool\", result);\n\t}\n\n\t@Data\n\tstatic class PaymentInfo{\n\t\tprivate UserService userService;\n\t}\n\n\t@Data\n\tstatic class UserService{\n\t\tprivate String operator;\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/template/JetbrickTest.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.engine.jetbrick.JetbrickEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class JetbrickTest {\n\n\t@Test\n\tpublic void jetbrickEngineTest() {\n\t\t//classpath模板\n\t\tTemplateConfig config = new TemplateConfig(\"templates\", TemplateConfig.ResourceMode.CLASSPATH)\n\t\t\t\t.setCustomEngine(JetbrickEngine.class);\n\t\tTemplateEngine engine = TemplateUtil.createEngine(config);\n\t\tTemplate template = engine.getTemplate(\"jetbrick_test.jetx\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"你好,hutool\", StrUtil.trim(result));\n\t}\n\n\t@Test\n\tpublic void jetbrickEngineWithStringTest() {\n\t\t// 字符串模板\n\t\tTemplateConfig config = new TemplateConfig(\"templates\", TemplateConfig.ResourceMode.STRING)\n\t\t\t\t.setCustomEngine(JetbrickEngine.class);\n\t\tTemplateEngine engine = TemplateUtil.createEngine(config);\n\t\tTemplate template = engine.getTemplate(\"hello,${name}\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", StrUtil.trim(result));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/template/TemplateUtilTest.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.extra.template.TemplateConfig.ResourceMode;\nimport cn.hutool.extra.template.engine.beetl.BeetlEngine;\nimport cn.hutool.extra.template.engine.enjoy.EnjoyEngine;\nimport cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;\nimport cn.hutool.extra.template.engine.rythm.RythmEngine;\nimport cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine;\nimport cn.hutool.extra.template.engine.velocity.VelocityEngine;\nimport cn.hutool.extra.template.engine.wit.WitEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 模板引擎单元测试\n *\n * @author looly\n *\n */\npublic class TemplateUtilTest {\n\n\t@Test\n\tpublic void createEngineTest() {\n\t\t// 字符串模板, 默认模板引擎，此处为Beetl\n\t\tTemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig());\n\t\tTemplate template = engine.getTemplate(\"hello,${name}\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result);\n\n\t\t// classpath中获取模板\n\t\tengine = TemplateUtil.createEngine(new TemplateConfig(\"templates\", ResourceMode.CLASSPATH));\n\t\tTemplate template2 = engine.getTemplate(\"beetl_test.btl\");\n\t\tString result2 = template2.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result2);\n\t}\n\n\t@Test\n\tpublic void beetlEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = new BeetlEngine(new TemplateConfig(\"templates\"));\n\t\tTemplate template = engine.getTemplate(\"hello,${name}\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result);\n\n\t\t// classpath中获取模板\n\t\tengine = new BeetlEngine(new TemplateConfig(\"templates\", ResourceMode.CLASSPATH));\n\t\tTemplate template2 = engine.getTemplate(\"beetl_test.btl\");\n\t\tString result2 = template2.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result2);\n\t}\n\n\t@Test\n\tpublic void rythmEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\").setCustomEngine(RythmEngine.class));\n\t\tTemplate template = engine.getTemplate(\"hello,@name\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result);\n\n\t\t// classpath中获取模板\n\t\tTemplate template2 = engine.getTemplate(\"rythm_test.tmpl\");\n\t\tString result2 = template2.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result2);\n\t}\n\n\t@Test\n\tpublic void freemarkerEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.STRING).setCustomEngine(FreemarkerEngine.class));\n\t\tTemplate template = engine.getTemplate(\"hello,${name}\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result);\n\n\t\t//ClassPath模板\n\t\tengine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.CLASSPATH).setCustomEngine(FreemarkerEngine.class));\n\t\ttemplate = engine.getTemplate(\"freemarker_test.ftl\");\n\t\tresult = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", result);\n\t}\n\n\t@Test\n\tpublic void velocityEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.STRING).setCustomEngine(VelocityEngine.class));\n\t\tTemplate template = engine.getTemplate(\"你好,$name\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"你好,hutool\", result);\n\n\t\t//ClassPath模板\n\t\tengine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.CLASSPATH).setCustomEngine(VelocityEngine.class));\n\t\ttemplate = engine.getTemplate(\"velocity_test.vtl\");\n\t\tresult = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"你好,hutool\", result);\n\n\t\ttemplate = engine.getTemplate(\"templates/velocity_test.vtl\");\n\t\tresult = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"你好,hutool\", result);\n\t}\n\n\t@Test\n\tpublic void enjoyEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\").setCustomEngine(EnjoyEngine.class));\n\t\tTemplate template = engine.getTemplate(\"#(x + 123)\");\n\t\tString result = template.render(Dict.create().set(\"x\", 1));\n\t\tassertEquals(\"124\", result);\n\n\t\t//ClassPath模板\n\t\tengine = new EnjoyEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.CLASSPATH).setCustomEngine(EnjoyEngine.class));\n\t\ttemplate = engine.getTemplate(\"enjoy_test.etl\");\n\t\tresult = template.render(Dict.create().set(\"x\", 1));\n\t\tassertEquals(\"124\", result);\n\t}\n\n\t@Test\n\tpublic void thymeleafEngineTest() {\n\t\t// 字符串模板\n\t\tTemplateEngine engine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\").setCustomEngine(ThymeleafEngine.class));\n\t\tTemplate template = engine.getTemplate(\"<h3 th:text=\\\"${message}\\\"></h3>\");\n\t\tString result = template.render(Dict.create().set(\"message\", \"Hutool\"));\n\t\tassertEquals(\"<h3>Hutool</h3>\", result);\n\n\t\t//ClassPath模板\n\t\tengine = TemplateUtil.createEngine(\n\t\t\t\tnew TemplateConfig(\"templates\", ResourceMode.CLASSPATH).setCustomEngine(ThymeleafEngine.class));\n\t\ttemplate = engine.getTemplate(\"thymeleaf_test.ttl\");\n\t\tresult = template.render(Dict.create().set(\"message\", \"Hutool\"));\n\t\tassertEquals(\"<h3>Hutool</h3>\", result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void renderToFileTest() {\n\t\tTemplateEngine engine = new BeetlEngine(new TemplateConfig(\"templates\", ResourceMode.CLASSPATH));\n\t\tTemplate template = engine.getTemplate(\"freemarker_test.ftl\");\n\n\t\tfinal Map<String, Object> bindingMap = new HashMap<>();\n\t\tbindingMap.put(\"name\", \"aa\");\n\t\tFile outputFile = new File(\"e:/test.txt\");\n\t\ttemplate.render(bindingMap, outputFile);\n\t}\n\n\t@Test\n\tpublic void witEngineTest() {\n\t\t//classpath模板\n\t\tTemplateConfig config = new TemplateConfig(\"templates\", ResourceMode.CLASSPATH)\n\t\t\t\t.setCustomEngine(WitEngine.class);\n\t\tTemplateEngine engine = TemplateUtil.createEngine(config);\n\t\tTemplate template = engine.getTemplate(\"/wit_test.wit\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", StrUtil.trim(result));\n\n\t\t// 字符串模板\n\t\tconfig = new TemplateConfig(\"templates\", ResourceMode.STRING)\n\t\t\t\t.setCustomEngine(WitEngine.class);\n\t\tengine = TemplateUtil.createEngine(config);\n\t\ttemplate = engine.getTemplate(\"<%var name;%>hello,${name}\");\n\t\tresult = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"hello,hutool\", StrUtil.trim(result));\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/template/ThymeleafTest.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.standard.StandardDialect;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\n\nimport java.io.StringWriter;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Thymeleaf单元测试\n *\n * @author looly\n *\n */\npublic class ThymeleafTest {\n\n\t/**\n\t * <a href=\"https://github.com/chinabugotech/hutool/issues/2530\">...</a>\n\t * 自定义操作原始引擎\n\t */\n\t@Test\n\t@Disabled\n\tpublic void addDialectTest(){\n\t\tfinal TemplateEngine engine = TemplateUtil.createEngine();\n\t\tif(engine instanceof ThymeleafEngine){\n\t\t\tfinal org.thymeleaf.TemplateEngine rawEngine = ((ThymeleafEngine) engine).getRawEngine();\n\t\t\trawEngine.addDialect(new StandardDialect());\n\t\t}\n\t}\n\n\t@Test\n\tpublic void thymeleafEngineTest() {\n\t\tMap<String, Object> map1 = new HashMap<>();\n\t\tmap1.put(\"name\", \"a\");\n\n\t\tMap<String, Object> map2 = new HashMap<>();\n\t\tmap2.put(\"name\", \"b\");\n\n\t\t// 日期测试\n\t\tMap<String, Object> map3 = new HashMap<>();\n\t\tmap3.put(\"name\", DateUtil.parse(\"2019-01-01\"));\n\n\t\tList<Map<String, Object>> list = new ArrayList<>();\n\t\tlist.add(map1);\n\t\tlist.add(map2);\n\t\tlist.add(map3);\n\n\t\t// 字符串模板\n\t\tTemplateEngine engine = new ThymeleafEngine(new TemplateConfig());\n\t\tTemplate template = engine.getTemplate(\"<h3 th:each=\\\"item : ${list}\\\" th:text=\\\"${item.name}\\\"></h3>\");\n\t\tString render = template.render(Dict.create().set(\"list\", list));\n\t\tassertEquals(\"<h3>a</h3><h3>b</h3><h3>2019-01-01 00:00:00</h3>\", render);\n\t}\n\n\t@Test\n\tpublic void thymeleafEngineTest2() {\n\t\tMap<String, Object> map1 = new HashMap<>();\n\t\tmap1.put(\"name\", \"a\");\n\n\t\tMap<String, Object> map2 = new HashMap<>();\n\t\tmap2.put(\"name\", \"b\");\n\n\t\t// 日期测试\n\t\tMap<String, Object> map3 = new HashMap<>();\n\t\tmap3.put(\"name\", DateUtil.parse(\"2019-01-01\"));\n\n\t\tList<Map<String, Object>> list = new ArrayList<>();\n\t\tlist.add(map1);\n\t\tlist.add(map2);\n\t\tlist.add(map3);\n\n\t\tLinkedHashMap<String, Object> map = new LinkedHashMap<>();\n\t\tmap.put(\"list\", list);\n\n\t\t hutoolApi(map);\n\t\tthymeleaf(map);\n\t}\n\n\t@SuppressWarnings({ \"rawtypes\", \"unchecked\" })\n\tprivate static void thymeleaf(Map map) {\n\t\torg.thymeleaf.TemplateEngine templateEngine = new org.thymeleaf.TemplateEngine();\n\t\tStringTemplateResolver stringTemplateResolver = new StringTemplateResolver();\n\t\ttemplateEngine.addTemplateResolver(stringTemplateResolver);\n\n\t\tStringWriter writer = new StringWriter();\n\t\tContext context = new Context(Locale.getDefault(), map);\n\t\ttemplateEngine.process(\"<h3 th:each=\\\"item : ${list}\\\" th:text=\\\"${item.name}\\\"></h3>\", context, writer);\n\n\t\tassertEquals(\"<h3>a</h3><h3>b</h3><h3>2019-01-01 00:00:00</h3>\", writer.toString());\n\t}\n\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static void hutoolApi(Map map) {\n\n\t\t// 字符串模板\n\t\tTemplateEngine engine = new ThymeleafEngine(new TemplateConfig());\n\t\tTemplate template = engine.getTemplate(\"<h3 th:each=\\\"item : ${list}\\\" th:text=\\\"${item.name}\\\"></h3>\");\n\t\t// \"<h3 th:text=\\\"${nestMap.nestKey}\\\"></h3>\"\n\t\tString render = template.render(map);\n\t\tassertEquals(\"<h3>a</h3><h3>b</h3><h3>2019-01-01 00:00:00</h3>\", render);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/template/VelocityTest.java",
    "content": "package cn.hutool.extra.template;\n\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.extra.template.engine.velocity.VelocityEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class VelocityTest {\n\n\t@Test\n\tpublic void charsetTest(){\n\t\tfinal TemplateConfig config = new TemplateConfig(\"templates\", TemplateConfig.ResourceMode.CLASSPATH);\n\t\tconfig.setCustomEngine(VelocityEngine.class);\n\t\tconfig.setCharset(CharsetUtil.CHARSET_GBK);\n\t\tfinal TemplateEngine engine = TemplateUtil.createEngine(config);\n\t\tTemplate template = engine.getTemplate(\"velocity_test_gbk.vtl\");\n\t\tString result = template.render(Dict.create().set(\"name\", \"hutool\"));\n\t\tassertEquals(\"你好,hutool\", result);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/tokenizer/TokenizerUtilTest.java",
    "content": "package cn.hutool.extra.tokenizer;\n\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.extra.tokenizer.engine.analysis.SmartcnEngine;\nimport cn.hutool.extra.tokenizer.engine.hanlp.HanLPEngine;\nimport cn.hutool.extra.tokenizer.engine.ikanalyzer.IKAnalyzerEngine;\nimport cn.hutool.extra.tokenizer.engine.jcseg.JcsegEngine;\nimport cn.hutool.extra.tokenizer.engine.jieba.JiebaEngine;\nimport cn.hutool.extra.tokenizer.engine.mmseg.MmsegEngine;\nimport cn.hutool.extra.tokenizer.engine.mynlp.MynlpEngine;\nimport cn.hutool.extra.tokenizer.engine.word.WordEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 模板引擎单元测试\n *\n * @author looly\n *\n */\npublic class TokenizerUtilTest {\n\n\tString text = \"这两个方法的区别在于返回值\";\n\n\t@Test\n\tpublic void createEngineTest() {\n\t\t// 默认分词引擎，此处为Ansj\n\t\tfinal TokenizerEngine engine = TokenizerUtil.createEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tcheckResult(result);\n\t}\n\n\t@Test\n\tpublic void hanlpTest() {\n\t\tfinal TokenizerEngine engine = new HanLPEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这 两 个 方法 的 区别 在于 返回 值\", resultStr);\n\t}\n\n\t@Test\n\tpublic void ikAnalyzerTest() {\n\t\tfinal TokenizerEngine engine = new IKAnalyzerEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这两个 方法 的 区别 在于 返回值\", resultStr);\n\t}\n\n\t@Test\n\tpublic void jcsegTest() {\n\t\tfinal TokenizerEngine engine = new JcsegEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tcheckResult(result);\n\t}\n\n\t@Test\n\tpublic void jiebaTest() {\n\t\tfinal TokenizerEngine engine = new JiebaEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这 两个 方法 的 区别 在于 返回值\", resultStr);\n\t}\n\n\t@Test\n\tpublic void mmsegTest() {\n\t\tfinal TokenizerEngine engine = new MmsegEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tcheckResult(result);\n\t}\n\n\t@Test\n\tpublic void smartcnTest() {\n\t\tfinal TokenizerEngine engine = new SmartcnEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这 两 个 方法 的 区别 在于 返回 值\", resultStr);\n\t}\n\n\t@Test\n\tpublic void wordTest() {\n\t\tfinal TokenizerEngine engine = new WordEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这两个 方法 的 区别 在于 返回值\", resultStr);\n\t}\n\n\t@Test\n\tpublic void mynlpTest() {\n\t\tfinal TokenizerEngine engine = new MynlpEngine();\n\t\tfinal Result result = engine.parse(text);\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这 两个 方法 的 区别 在于 返回 值\", resultStr);\n\t}\n\n\tprivate void checkResult(final Result result) {\n\t\tfinal String resultStr = IterUtil.join(result, \" \");\n\t\tassertEquals(\"这 两个 方法 的 区别 在于 返回 值\", resultStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/java/cn/hutool/extra/validation/BeanValidatorUtilTest.java",
    "content": "package cn.hutool.extra.validation;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport jakarta.validation.constraints.NotBlank;\n\n/**\n * java bean 校验工具类单元测试\n *\n * @author chengqiang\n */\npublic class BeanValidatorUtilTest {\n\n\tpublic static class TestClass {\n\n\t\t@NotBlank(message = \"姓名不能为空\")\n\t\tprivate String name;\n\n\t\t@NotBlank(message = \"地址不能为空\")\n\t\tprivate String address;\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\n\t\tpublic void setName(String name) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\tpublic String getAddress() {\n\t\t\treturn address;\n\t\t}\n\n\t\tpublic void setAddress(String address) {\n\t\t\tthis.address = address;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void beanValidatorTest() {\n\t\tBeanValidationResult result = ValidationUtil.warpValidate(new TestClass());\n\t\tassertFalse(result.isSuccess());\n\t\tassertEquals(2, result.getErrorMessages().size());\n\t}\n\n\t@Test\n\tpublic void propertyValidatorTest() {\n\t\tBeanValidationResult result = ValidationUtil.warpValidateProperty(new TestClass(), \"name\");\n\t\tassertFalse(result.isSuccess());\n\t\tassertEquals(1, result.getErrorMessages().size());\n\t}\n}\n"
  },
  {
    "path": "hutool-extra/src/test/resources/beetl.properties",
    "content": "#\\u9ed8\\u8ba4\\u914d\\u7f6e\nENGINE=org.beetl.core.engine.FastRuntimeEngine\n\n# \\u6307\\u5b9a\\u4e86\\u5360\\u4f4d\\u7b26\\u53f7\\uff0c\\u9ed8\\u8ba4\\u662f${ }\\uff0c\\u4e5f\\u53ef\\u4ee5\\u6307\\u5b9a\\u4e3a\\u5176\\u4ed6\\u5360\\u4f4d\\u7b26\\u3002\nDELIMITER_PLACEHOLDER_START=${\nDELIMITER_PLACEHOLDER_END=}\n\n# \\u6307\\u5b9a\\u4e86\\u8bed\\u53e5\\u7684\\u5b9a\\u754c\\u7b26\\u53f7\\uff0c\\u9ed8\\u8ba4\\u662f<% %>\\uff0c\\u4e5f\\u53ef\\u4ee5\\u6307\\u5b9a\\u4e3a\\u5176\\u4ed6\\u5b9a\\u754c\\u7b26\\u53f7\nDELIMITER_STATEMENT_START=<%\nDELIMITER_STATEMENT_END=%>\n\n# \\u6307\\u5b9aIO\\u8f93\\u51fa\\u6a21\\u5f0f\\uff0c\\u9ed8\\u8ba4\\u662fFALSE,\\u5373\\u901a\\u5e38\\u7684\\u5b57\\u7b26\\u8f93\\u51fa\\uff0c\\u5728\\u8003\\u8651\\u9ad8\\u6027\\u80fd\\u60c5\\u51b5\\u4e0b\\uff0c\\u53ef\\u4ee5\\u8bbe\\u7f6e\\u6210true\\u3002\\u8be6\\u7ec6\\u8bf7\\u53c2\\u8003\\u9ad8\\u7ea7\\u7528\\u6cd5\nDIRECT_BYTE_OUTPUT = FALSE\n\n# \\u6307\\u5b9a\\u4e86\\u652f\\u6301HTML\\u6807\\u7b7e\\uff0c\\u4e14\\u7b26\\u53f7\\u4e3a#,\\u9ed8\\u8ba4\\u914d\\u7f6e\\u4e0b\\uff0c\\u6a21\\u677f\\u5f15\\u64ce\\u8bc6\\u522b<#tag ></#tag>\\u8fd9\\u6837\\u7684\\u7c7b\\u4f3chtml\\u6807\\u7b7e\\uff0c\\u5e76\\u80fd\\u8c03\\u7528\\u76f8\\u5e94\\u7684\\u6807\\u7b7e\\u51fd\\u6570\\u6216\\u8005\\u6a21\\u677f\\u6587\\u4ef6\\u3002\\u4f60\\u4e5f\\u53ef\\u4ee5\\u6307\\u5b9a\\u522b\\u7684\\u7b26\\u53f7\\uff0c\\u5982bg: \\u5219\\u8bc6\\u522b<bg:\nHTML_TAG_SUPPORT = true\nHTML_TAG_FLAG = #\n\n# \\u6307\\u5b9a\\u5982\\u679c\\u6807\\u7b7e\\u5c5e\\u6027\\u6709var\\uff0c\\u5219\\u8ba4\\u4e3a\\u662f\\u9700\\u8981\\u7ed1\\u5b9a\\u53d8\\u91cf\\u7ed9\\u6a21\\u677f\\u7684\\u6807\\u7b7e\\u51fd\\u6570\nHTML_TAG_BINDING_ATTRIBUTE = var\n\n# \\u6307\\u5b9a\\u5141\\u8bb8\\u672c\\u5730Class\\u76f4\\u63a5\\u8c03\\u7528\nNATIVE_CALL = TRUE\n\n# \\u6307\\u5b9a\\u6a21\\u677f\\u5b57\\u7b26\\u96c6\\u662fUTF-8\nTEMPLATE_CHARSET = UTF-8\n\n# \\u6307\\u5b9a\\u5f02\\u5e38\\u7684\\u89e3\\u6790\\u7c7b\\uff0c\\u9ed8\\u8ba4\\u662fConsoleErrorHandler\\uff0c\\u4ed6\\u5c06\\u5728render\\u53d1\\u751f\\u5f02\\u5e38\\u7684\\u65f6\\u5019\\u5728\\u540e\\u53f0\\u6253\\u5370\\u51fa\\u9519\\u8bef\\u4fe1\\u606f\\uff08System.out)\nERROR_HANDLER = org.beetl.core.ConsoleErrorHandler\n\n# \\u6307\\u5b9a\\u4e86\\u672c\\u5730Class\\u8c03\\u7528\\u7684\\u5b89\\u5168\\u7b56\\u7565\nNATIVE_SECUARTY_MANAGER= org.beetl.core.DefaultNativeSecurityManager\n\n# \\u914d\\u7f6e\\u4e86\\u662f\\u5426\\u8fdb\\u884c\\u4e25\\u683cMVC\\uff0c\\u901a\\u5e38\\u60c5\\u51b5\\u4e0b\\uff0c\\u6b64\\u5904\\u8bbe\\u7f6e\\u4e3afalse\nMVC_STRICT = FALSE\n\n#\\u8d44\\u6e90\\u914d\\u7f6e\\uff0cresource\\u540e\\u7684\\u5c5e\\u6027\\u53ea\\u9650\\u4e8e\\u7279\\u5b9aResourceLoader\nRESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader\n\n#classpath \\u6839\\u8def\\u5f84\nRESOURCE.root= /\n\n#\\u662f\\u5426\\u68c0\\u6d4b\\u6587\\u4ef6\\u53d8\\u5316,\\u5f00\\u53d1\\u7528true\\u5408\\u9002\\uff0c\\u4f46\\u7ebf\\u4e0a\\u8981\\u6539\\u4e3afalse\nRESOURCE.autoCheck= true\n\n#\\u81ea\\u5b9a\\u4e49\\u811a\\u672c\\u65b9\\u6cd5\\u6587\\u4ef6\\u7684Root\\u76ee\\u5f55\\u548c\\u540e\\u7f00\nRESOURCE.functionRoot = functions\nRESOURCE.functionSuffix = html\n\n#\\u81ea\\u5b9a\\u4e49\\u6807\\u7b7e\\u6587\\u4ef6Root\\u76ee\\u5f55\\u548c\\u540e\\u7f00\nRESOURCE.tagRoot = htmltag\nRESOURCE.tagSuffix = tag\n\n#####  \\u6269\\u5c55 ##############\n# \\u5185\\u7f6e\\u7684\\u65b9\\u6cd5\nFN.date = org.beetl.ext.fn.DateFunction\n#\\u5185\\u7f6e\\u7684\\u529f\\u80fd\\u5305\nFNP.strutil = org.beetl.ext.fn.StringUtil\n# \\u5185\\u7f6e\\u7684\\u9ed8\\u8ba4\\u683c\\u5f0f\\u5316\\u51fd\\u6570\nFTC.java.util.Date = org.beetl.ext.format.DateFormat\n# \\u6807\\u7b7e\\u7c7b\nTAG.include= org.beetl.ext.tag.IncludeTag"
  },
  {
    "path": "hutool-extra/src/test/resources/config/mail.setting",
    "content": "# **************************************************************\n# ----- Setting File with UTF8-----\n# ----- 邮件发送功能的账户信息配置文件 -----\n# ----- 此邮箱专门注册用于Hutool发送邮件测试，为公开邮箱 -----\n# **************************************************************\n\n# 邮件服务器的SMTP地址\nhost = smtp.yeah.net\n# 邮件服务器的SMTP端口\nport = 465\n# 发件人（必须正确，否则发送失败）\nfrom = 小磊<hutool@yeah.net>\n# 用户名（注意：如果使用foxmail邮箱，此处user为qq号）\nuser = hutool@yeah.net\n# 密码\npass = 123456\n# 使用 STARTTLS安全连接\nstarttlsEnable = true\n# 是否开启SSL\nsslEnable = true\n# 调试模式\ndebug = true\n# 对于超长参数是否切分为多份，默认为false（国内邮箱附件不支持切分的附件名）\nsplitlongparameters = false\n# 是否编码附件文件名（默认true）\nencodefilename = true\n"
  },
  {
    "path": "hutool-extra/src/test/resources/example/beetl-example.properties",
    "content": "#---------------------------------------------------------------------------\n# Template for beetl.properties\n# see http://ibeetl.com/guide/beetl.html#header-c6267\n#---------------------------------------------------------------------------\n\n#\\u9ed8\\u8ba4\\u914d\\u7f6e\n\n#\\u5f15\\u64ce\\u5b9e\\u73b0\\u7c7b\\uff0c\\u9ed8\\u8ba4\\u5373\\u53ef\nENGINE=org.beetl.core.engine.FastRuntimeEngine\n\n#\\u5360\\u4f4d\\u7b26\\u53f7\\u5f00\\u59cb\nDELIMITER_PLACEHOLDER_START=${\n#\\u5360\\u4f4d\\u7b26\\u53f7\\u7ed3\\u675f\nDELIMITER_PLACEHOLDER_END=}\n\n#\\u8bed\\u53e5\\u7684\\u5b9a\\u754c\\u7b26\\u53f7\\u5f00\\u59cb\nDELIMITER_STATEMENT_START=<%\n#\\u8bed\\u53e5\\u7684\\u5b9a\\u754c\\u7b26\\u53f7\\u7ed3\\u675f\nDELIMITER_STATEMENT_END=%>\n\n#IO\\u8f93\\u51fa\\u6a21\\u5f0f\\uff0c\\u9ed8\\u8ba4\\u662fFALSE,\\u5373\\u901a\\u5e38\\u7684\\u5b57\\u7b26\\u8f93\\u51fa\\uff0c\\u5728\\u8003\\u8651\\u9ad8\\u6027\\u80fd\\u60c5\\u51b5\\u4e0b\\uff0c\\u53ef\\u4ee5\\u8bbe\\u7f6e\\u6210true\nDIRECT_BYTE_OUTPUT = FALSE\n\n#\\u652f\\u6301HTML\\u6807\\u7b7e\\uff0c\\u4e14\\u7b26\\u53f7\\u4e3a#,\\u9ed8\\u8ba4\\u914d\\u7f6e\\u4e0b\\uff0c\\u6a21\\u677f\\u5f15\\u64ce\\u8bc6\\u522b<#tag ></#tag>\\u8fd9\\u6837\\u7684\\u7c7b\\u4f3chtml\\u6807\\u7b7e\\uff0c\\u5e76\\u80fd\\u8c03\\u7528\\u76f8\\u5e94\\u7684\\u6807\\u7b7e\\u51fd\\u6570\\u6216\\u8005\\u6a21\\u677f\\u6587\\u4ef6\\u3002\\u4f60\\u4e5f\\u53ef\\u4ee5\\u6307\\u5b9a\\u522b\\u7684\\u7b26\\u53f7\\uff0c\\u5982bg: \\u5219\\u8bc6\\u522b<bg:\nHTML_TAG_SUPPORT = true\nHTML_TAG_FLAG = #\n\n#\\u6307\\u5b9a\\u5982\\u679c\\u6807\\u7b7e\\u5c5e\\u6027\\u6709var\\uff0c\\u5219\\u8ba4\\u4e3a\\u662f\\u9700\\u8981\\u7ed1\\u5b9a\\u53d8\\u91cf\\u7ed9\\u6a21\\u677f\\u7684\\u6807\\u7b7e\\u51fd\\u6570\nHTML_TAG_BINDING_ATTRIBUTE = var\n\n#\\u6307\\u5b9a\\u5141\\u8bb8\\u672c\\u5730Class\\u76f4\\u63a5\\u8c03\\u7528\nNATIVE_CALL = TRUE\n\n#\\u6307\\u5b9a\\u6a21\\u677f\\u5b57\\u7b26\\u96c6\\u662fUTF-8\nTEMPLATE_CHARSET = UTF-8\n\n#\\u6307\\u5b9a\\u5f02\\u5e38\\u7684\\u89e3\\u6790\\u7c7b\\uff0c\\u9ed8\\u8ba4\\u662fConsoleErrorHandler\\uff0c\\u4ed6\\u5c06\\u5728render\\u53d1\\u751f\\u5f02\\u5e38\\u7684\\u65f6\\u5019\\u5728\\u540e\\u53f0\\u6253\\u5370\\u51fa\\u9519\\u8bef\\u4fe1\\u606f\\uff08System.out)\nERROR_HANDLER = org.beetl.core.ConsoleErrorHandler\n\n#\\u6307\\u5b9a\\u4e86\\u672c\\u5730Class\\u8c03\\u7528\\u7684\\u5b89\\u5168\\u7b56\\u7565\nNATIVE_SECUARTY_MANAGER= org.beetl.core.DefaultNativeSecurityManager\n\n#\\u914d\\u7f6e\\u4e86\\u662f\\u5426\\u8fdb\\u884c\\u4e25\\u683cMVC\\uff0c\\u901a\\u5e38\\u60c5\\u51b5\\u4e0b\\uff0c\\u6b64\\u5904\\u8bbe\\u7f6e\\u4e3afalse\nMVC_STRICT = FALSE\n\n\n#\\u9ed8\\u8ba4\\u4f7f\\u7528\\u7684\\u6a21\\u677f\\u8d44\\u6e90\\u52a0\\u8f7d\\u5668\\uff0c\\u8d44\\u6e90\\u914d\\u7f6e\\uff0cresource\\u540e\\u7684\\u5c5e\\u6027\\u53ea\\u9650\\u4e8e\\u7279\\u5b9aResourceLoader\nRESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader\n#classpath \\u6839\\u8def\\u5f84\nRESOURCE.root= /\n#\\u662f\\u5426\\u68c0\\u6d4b\\u6587\\u4ef6\\u53d8\\u5316\nRESOURCE.autoCheck= true\n#\\u81ea\\u5b9a\\u4e49\\u811a\\u672c\\u65b9\\u6cd5\\u6587\\u4ef6\\u7684Root\\u76ee\\u5f55\\u548c\\u540e\\u7f00\nRESOURCE.functionRoot = functions\nRESOURCE.functionSuffix = html\n#\\u81ea\\u5b9a\\u4e49\\u6807\\u7b7e\\u6587\\u4ef6Root\\u76ee\\u5f55\\u548c\\u540e\\u7f00\nRESOURCE.tagRoot = htmltag\nRESOURCE.tagSuffix = tag\n\n#####  \\u6269\\u5c55 ##############\n## \\u5185\\u7f6e\\u7684\\u65b9\\u6cd5\nFN.date = org.beetl.ext.fn.DateFunction\n##\\u5185\\u7f6e\\u7684\\u529f\\u80fd\\u5305\nFNP.strutil = org.beetl.ext.fn.StringUtil\n##\\u5185\\u7f6e\\u7684\\u9ed8\\u8ba4\\u683c\\u5f0f\\u5316\\u51fd\\u6570\nFTC.java.util.Date = org.beetl.ext.format.DateFormat\n## \\u6807\\u7b7e\\u7c7b\nTAG.include= org.beetl.ext.tag.IncludeTag"
  },
  {
    "path": "hutool-extra/src/test/resources/example/mail-example.setting",
    "content": "# **************************************************************\n# ----- Setting File with UTF8-----\n# ----- 邮件发送功能的账户信息配置文件 -----\n# ----- 此邮箱专门注册用于Hutool发送邮件测试，为公开邮箱 -----\n# **************************************************************\n\n# 邮件服务器的SMTP地址\nhost = smtp.yeah.net\n# 邮件服务器的SMTP端口\nport = 465\n# 发件人（必须正确，否则发送失败）\nfrom = 小磊<hutool@yeah.net>\n# 用户名\nuser = hutool\n# 密码\npass = 123456\n\n# 是否打开调试模式，调试模式会显示与邮件服务器通信过程，默认不开启\ndebug = true\n# 编码，决定了邮件正文编码和发件人、收件人、抄送人、密送人姓名中的中文编码，默认UTF-8\ncharset = UTF-8\n\n#使用 STARTTLS安全连接\nstartttlsEnable = true\n#使用 SSL安全连接\nsslEnable = true\n# 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字\nsocketFactoryClass = javax.net.ssl.SSLSocketFactory\n# 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true\nsocketFactoryFallback = true\n# 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口456\nsocketFactoryPort = 465\n\n# 对于超长参数是否切分为多份，默认为false（国内邮箱附件不支持切分的附件名）\nsplitlongparameters = false\n\n# SMTP超时时长，单位毫秒，缺省值不超时\ntimeout = 0\n# Socket连接超时值，单位毫秒，缺省值不超时\nconnectionTimeout = 0\n# Socket写出超时值，单位毫秒，缺省值不超时\nwriteTimeout = 0\n"
  },
  {
    "path": "hutool-extra/src/test/resources/example/velocity-example.vm",
    "content": "##这是一行注释，不会输出\n#*\n\t这是多行注释，不会输出\n\t多行注释\n*#\n// ---------- 1.变量赋值输出------------\nWelcome $name to hutool.cn!\ntoday is $date.\ntdday is $mydae.//未被定义的变量将当成字符串\n\n// -----------2.设置变量值,所有变量都以$开头----------------\n#set( $iAmVariable = \"good!\" )\nWelcome $name to Javayou.com!\ntoday is $date.\n$iAmVariable\n\n//-------------3.if,else判断--------------------------\n#set ($admin = \"admin\")\n#set ($user = \"user\")\n#if ($admin == $user)\n\tWelcome admin!\n#else\n\tWelcome user!\n#end\n\n//--------------4.迭代数据List---------------------\n#foreach( $product in $list )\n\t$product\n#end\n\n// ------------5.迭代数据HashSet-----------------\n#foreach($key in $hashVariable.keySet() )  \n\t$key ‘s value: $ hashVariable.get($key)\n#end\n\n//-----------6.迭代数据List Bean($velocityCount为列举序号，默认从1开始，可调整)\n#foreach ($s in $listBean)\n\t<$velocityCount> Address: $s.address\n#end\n\n//-------------7.模板嵌套---------------------\n#foreach ($element in $list)\n\t#foreach ($element in $list)\n\t\tinner:This is ($velocityCount)- $element.\n\t#end\nouter:This is ($velocityCount)- $element.\n#end\n\n//-----------8.导入其它文件,多个文件用逗号隔开（非模板静态）--------------\n#include(\"com/test2/test.txt\")\n//-----------8.导入其它文件,多个文件用逗号隔开（模板文件）--------------\n#parse(\"com/test2/test.txt\")"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/beetl_test.btl",
    "content": "hello,${name}"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/enjoy_test.etl",
    "content": "#(x + 123)"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/freemarker_test.ftl",
    "content": "hello,${name}"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/issue3488.ftl",
    "content": "hello,${paymentInfo}\n"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/jetbrick_test.jetx",
    "content": "你好,${name}\n"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/rythm_test.tmpl",
    "content": "@args String name\nhello,@name"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/thymeleaf_test.ttl",
    "content": "<h3 th:text=\"${message}\"></h3>"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/velocity_test.vtl",
    "content": "你好,$name"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/velocity_test_gbk.vtl",
    "content": ",$name"
  },
  {
    "path": "hutool-extra/src/test/resources/templates/wit_test.wit",
    "content": "<%\nvar name;\n%>\nhello,${name}\n"
  },
  {
    "path": "hutool-http/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-http</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool Http客户端</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.http</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>javax.xml.soap</groupId>\n\t\t\t<artifactId>javax.xml.soap-api</artifactId>\n\t\t\t<version>1.4.0</version>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>jakarta.xml.soap</groupId>\n\t\t\t<artifactId>jakarta.xml.soap-api</artifactId>\n\t\t\t<version>2.0.1</version>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ContentType.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\n\n/**\n * 常用Content-Type类型枚举\n *\n * @author looly\n * @since 4.0.11\n */\npublic enum ContentType {\n\n\t/**\n\t * 标准表单编码，当action为get时候，浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串（name1=value1&amp;name2=value2…）\n\t */\n\tFORM_URLENCODED(\"application/x-www-form-urlencoded\"),\n\t/**\n\t * 文件上传编码，浏览器会把整个表单以控件为单位分割，并为每个部分加上Content-Disposition，并加上分割符(boundary)\n\t */\n\tMULTIPART(\"multipart/form-data\"),\n\t/**\n\t * Rest请求JSON编码\n\t */\n\tJSON(\"application/json\"),\n\t/**\n\t * Rest请求XML编码\n\t */\n\tXML(\"application/xml\"),\n\t/**\n\t * text/plain编码\n\t */\n\tTEXT_PLAIN(\"text/plain\"),\n\t/**\n\t * Rest请求text/xml编码\n\t */\n\tTEXT_XML(\"text/xml\"),\n\t/**\n\t * text/html编码\n\t */\n\tTEXT_HTML(\"text/html\"),\n\t/**\n\t * application/octet-stream编码\n\t */\n\tOCTET_STREAM(\"application/octet-stream\"),\n\t/**\n\t * text/event-stream编码\n\t */\n\tEVENT_STREAM(\"text/event-stream\");\n\n\tprivate final String value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value ContentType值\n\t */\n\tContentType(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取value值\n\t *\n\t * @return value值\n\t * @since 5.2.6\n\t */\n\tpublic String getValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getValue();\n\t}\n\n\t/**\n\t * 输出Content-Type字符串，附带编码信息\n\t *\n\t * @param charset 编码\n\t * @return Content-Type字符串\n\t */\n\tpublic String toString(Charset charset) {\n\t\treturn build(this.value, charset);\n\t}\n\n\t/**\n\t * 是否为默认Content-Type，默认包括{@code null}和application/x-www-form-urlencoded\n\t *\n\t * @param contentType 内容类型\n\t * @return 是否为默认Content-Type\n\t * @since 4.1.5\n\t */\n\tpublic static boolean isDefault(String contentType) {\n\t\treturn null == contentType || isFormUrlEncode(contentType);\n\t}\n\n\t/**\n\t * 是否为application/x-www-form-urlencoded\n\t *\n\t * @param contentType 内容类型\n\t * @return 是否为application/x-www-form-urlencoded\n\t */\n\tpublic static boolean isFormUrlEncode(String contentType) {\n\t\treturn StrUtil.startWithIgnoreCase(contentType, FORM_URLENCODED.toString());\n\t}\n\n\t/**\n\t * 从请求参数的body中判断请求的Content-Type类型，支持的类型有：\n\t *\n\t * <pre>\n\t * 1. application/json\n\t * 1. application/xml\n\t * </pre>\n\t *\n\t * @param body 请求参数体\n\t * @return Content-Type类型，如果无法判断返回null\n\t */\n\tpublic static ContentType get(String body) {\n\t\tContentType contentType = null;\n\t\tif (StrUtil.isNotBlank(body)) {\n\t\t\tchar firstChar = StrUtil.trimStart(body).charAt(0);\n\t\t\tswitch (firstChar) {\n\t\t\t\tcase '{':\n\t\t\t\tcase '[':\n\t\t\t\t\t// JSON请求体\n\t\t\t\t\tcontentType = JSON;\n\t\t\t\t\tbreak;\n\t\t\t\tcase '<':\n\t\t\t\t\t// XML请求体\n\t\t\t\t\tcontentType = XML;\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn contentType;\n\t}\n\n\t/**\n\t * 输出Content-Type字符串，附带编码信息\n\t *\n\t * @param contentType Content-Type类型\n\t * @param charset     编码\n\t * @return Content-Type字符串\n\t * @since 4.5.4\n\t */\n\tpublic static String build(String contentType, Charset charset) {\n\t\treturn StrUtil.format(\"{};charset={}\", contentType, charset.name());\n\t}\n\n\t/**\n\t * 输出Content-Type字符串，附带编码信息\n\t *\n\t * @param contentType Content-Type 枚举类型\n\t * @param charset     编码\n\t * @return Content-Type字符串\n\t * @since 5.7.15\n\t */\n\tpublic static String build(ContentType contentType, Charset charset) {\n\t\treturn build(contentType.getValue(), charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * 全局头部信息<br>\n * 所有Http请求将共用此全局头部信息，除非在{@link HttpRequest}中自定义头部信息覆盖之\n *\n * @author looly\n */\npublic enum GlobalHeaders {\n\tINSTANCE;\n\n\t/**\n\t * 存储头信息\n\t */\n\tfinal Map<String, List<String>> headers = new HashMap<>();\n\n\t/**\n\t * 构造\n\t */\n\tGlobalHeaders() {\n\t\tputDefault(false);\n\t}\n\n\t/**\n\t * 加入默认的头部信息\n\t *\n\t * @param isReset 是否重置所有头部信息（删除自定义保留默认）\n\t * @return this\n\t */\n\tpublic GlobalHeaders putDefault(boolean isReset) {\n\t\t// 解决HttpURLConnection中无法自定义Host等头信息的问题\n\t\t// https://stackoverflow.com/questions/9096987/how-to-overwrite-http-header-host-in-a-httpurlconnection/9098440\n\t\tSystem.setProperty(\"sun.net.http.allowRestrictedHeaders\", \"true\");\n\n\t\t//解决server certificate change is restricted during renegotiation问题\n\t\tSystem.setProperty(\"jdk.tls.allowUnsafeServerCertChange\", \"true\");\n\t\tSystem.setProperty(\"sun.security.ssl.allowUnsafeRenegotiation\", \"true\");\n\n\t\tif (isReset) {\n\t\t\tthis.headers.clear();\n\t\t}\n\n\t\theader(Header.ACCEPT, \"text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\", true);\n\t\theader(Header.ACCEPT_ENCODING, \"gzip, deflate\", true);\n\t\t// issue#3258，某些场景（如国外）不能指定中文\n\t\t//header(Header.ACCEPT_LANGUAGE, \"zh-CN,zh;q=0.8\", true);\n\t\t// 此Header只有在post请求中有用，因此在HttpRequest的method方法中设置此头信息，此处去掉\n\t\t// header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(CharsetUtil.CHARSET_UTF_8), true);\n\t\theader(Header.USER_AGENT, \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool\", true);\n\t\treturn this;\n\t}\n\n\t// ---------------------------------------------------------------- Headers start\n\n\t/**\n\t * 根据name获取头信息\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t */\n\tpublic String header(String name) {\n\t\tfinal List<String> values = headerList(name);\n\t\tif (CollectionUtil.isEmpty(values)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn values.get(0);\n\t}\n\n\t/**\n\t * 根据name获取头信息列表\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t * @since 3.1.1\n\t */\n\tpublic List<String> headerList(String name) {\n\t\tif (StrUtil.isBlank(name)) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn headers.get(name.trim());\n\t}\n\n\t/**\n\t * 根据name获取头信息\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t */\n\tpublic String header(Header name) {\n\t\tif (null == name) {\n\t\t\treturn null;\n\t\t}\n\t\treturn header(name.toString());\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 如果覆盖模式，则替换之前的值，否则加入到值列表中\n\t *\n\t * @param name       Header名\n\t * @param value      Header值\n\t * @param isOverride 是否覆盖已有值\n\t * @return this\n\t */\n\tsynchronized public GlobalHeaders header(String name, String value, boolean isOverride) {\n\t\tif (null != name && null != value) {\n\t\t\tfinal List<String> values = headers.get(name.trim());\n\t\t\tif (isOverride || CollectionUtil.isEmpty(values)) {\n\t\t\t\tfinal ArrayList<String> valueList = new ArrayList<>();\n\t\t\t\tvalueList.add(value);\n\t\t\t\theaders.put(name.trim(), valueList);\n\t\t\t} else {\n\t\t\t\tvalues.add(value.trim());\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 如果覆盖模式，则替换之前的值，否则加入到值列表中\n\t *\n\t * @param name       Header名\n\t * @param value      Header值\n\t * @param isOverride 是否覆盖已有值\n\t * @return this\n\t */\n\tpublic GlobalHeaders header(Header name, String value, boolean isOverride) {\n\t\treturn header(name.toString(), value, isOverride);\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 覆盖模式，则替换之前的值\n\t *\n\t * @param name  Header名\n\t * @param value Header值\n\t * @return this\n\t */\n\tpublic GlobalHeaders header(Header name, String value) {\n\t\treturn header(name.toString(), value, true);\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 覆盖模式，则替换之前的值\n\t *\n\t * @param name  Header名\n\t * @param value Header值\n\t * @return this\n\t */\n\tpublic GlobalHeaders header(String name, String value) {\n\t\treturn header(name, value, true);\n\t}\n\n\t/**\n\t * 设置请求头<br>\n\t * 不覆盖原有请求头\n\t *\n\t * @param headers 请求头\n\t * @return this\n\t */\n\tpublic GlobalHeaders header(Map<String, List<String>> headers) {\n\t\tif (MapUtil.isEmpty(headers)) {\n\t\t\treturn this;\n\t\t}\n\n\t\tString name;\n\t\tfor (Entry<String, List<String>> entry : headers.entrySet()) {\n\t\t\tname = entry.getKey();\n\t\t\tfor (String value : entry.getValue()) {\n\t\t\t\tthis.header(name, StrUtil.nullToEmpty(value), false);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除一个头信息\n\t *\n\t * @param name Header名\n\t * @return this\n\t */\n\tsynchronized public GlobalHeaders removeHeader(String name) {\n\t\tif (name != null) {\n\t\t\theaders.remove(name.trim());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除一个头信息\n\t *\n\t * @param name Header名\n\t * @return this\n\t */\n\tpublic GlobalHeaders removeHeader(Header name) {\n\t\treturn removeHeader(name.toString());\n\t}\n\n\t/**\n\t * 获取headers\n\t *\n\t * @return Headers Map\n\t */\n\tpublic Map<String, List<String>> headers() {\n\t\treturn Collections.unmodifiableMap(headers);\n\t}\n\n\t/**\n\t * 清除所有头信息，包括全局头信息\n\t *\n\t * @return this\n\t * @since 5.7.13\n\t */\n\tsynchronized public GlobalHeaders clearHeaders() {\n\t\tthis.headers.clear();\n\t\treturn this;\n\t}\n\t// ---------------------------------------------------------------- Headers end\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/GlobalInterceptor.java",
    "content": "package cn.hutool.http;\n\n/**\n * 全局的拦截器<br>\n * 包括请求拦截器和响应拦截器\n *\n * @author looly\n * @since 5.8.0\n */\npublic enum GlobalInterceptor {\n\tINSTANCE;\n\n\tprivate final HttpInterceptor.Chain<HttpRequest> requestInterceptors = new HttpInterceptor.Chain<>();\n\tprivate final HttpInterceptor.Chain<HttpResponse> responseInterceptors = new HttpInterceptor.Chain<>();\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t */\n\tsynchronized public GlobalInterceptor addRequestInterceptor(HttpInterceptor<HttpRequest> interceptor) {\n\t\tthis.requestInterceptors.addChain(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置拦截器，用于在响应读取后完成编辑或读取\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t */\n\tsynchronized public GlobalInterceptor addResponseInterceptor(HttpInterceptor<HttpResponse> interceptor) {\n\t\tthis.responseInterceptors.addChain(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空请求和响应拦截器\n\t *\n\t * @return this\n\t */\n\tpublic GlobalInterceptor clear() {\n\t\tclearRequest();\n\t\tclearResponse();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空请求拦截器\n\t *\n\t * @return this\n\t */\n\tsynchronized public GlobalInterceptor clearRequest() {\n\t\trequestInterceptors.clear();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空响应拦截器\n\t *\n\t * @return this\n\t */\n\tsynchronized public GlobalInterceptor clearResponse() {\n\t\tresponseInterceptors.clear();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 复制请求过滤器列表\n\t *\n\t * @return {@link cn.hutool.http.HttpInterceptor.Chain}\n\t */\n\tHttpInterceptor.Chain<HttpRequest> getCopiedRequestInterceptor() {\n\t\tfinal HttpInterceptor.Chain<HttpRequest> copied = new HttpInterceptor.Chain<>();\n\t\tfor (HttpInterceptor<HttpRequest> interceptor : this.requestInterceptors) {\n\t\t\tcopied.addChain(interceptor);\n\t\t}\n\t\treturn copied;\n\t}\n\n\t/**\n\t * 复制响应过滤器列表\n\t *\n\t * @return {@link cn.hutool.http.HttpInterceptor.Chain}\n\t */\n\tHttpInterceptor.Chain<HttpResponse> getCopiedResponseInterceptor() {\n\t\tfinal HttpInterceptor.Chain<HttpResponse> copied = new HttpInterceptor.Chain<>();\n\t\tfor (HttpInterceptor<HttpResponse> interceptor : this.responseInterceptors) {\n\t\t\tcopied.addChain(interceptor);\n\t\t}\n\t\treturn copied;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HTMLFilter.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.CharUtil;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * HTML过滤器，用于去除XSS(Cross Site Scripting) 漏洞隐患。\n *\n * <p>\n * 此类中的方法非线程安全\n * </p>\n *\n * <pre>\n *     String clean = new HTMLFilter().filter(input);\n * </pre>\n * <p>\n * 此类来自：http://xss-html-filter.sf.net\n *\n * @author Joseph O'Connell\n * @author Cal Hendersen\n * @author Michael Semb Wever\n */\npublic final class HTMLFilter {\n\n\t/**\n\t * regex flag union representing /si modifiers in php\n\t **/\n\tprivate static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;\n\tprivate static final Pattern P_COMMENTS = Pattern.compile(\"<!--(.*?)-->\", Pattern.DOTALL);\n\tprivate static final Pattern P_COMMENT = Pattern.compile(\"^!--(.*)--$\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_TAGS = Pattern.compile(\"<(.*?)>\", Pattern.DOTALL);\n\tprivate static final Pattern P_END_TAG = Pattern.compile(\"^/([a-z0-9]+)\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_START_TAG = Pattern.compile(\"^([a-z0-9]+)(.*?)(/?)$\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile(\"([a-z0-9]+)=([\\\"'])(.*?)\\\\2\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile(\"([a-z0-9]+)(=)([^\\\"\\\\s']+)\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_PROTOCOL = Pattern.compile(\"^([^:]+):\", REGEX_FLAGS_SI);\n\tprivate static final Pattern P_ENTITY = Pattern.compile(\"&#(\\\\d+);?\");\n\tprivate static final Pattern P_ENTITY_UNICODE = Pattern.compile(\"&#x([0-9a-f]+);?\");\n\tprivate static final Pattern P_ENCODE = Pattern.compile(\"%([0-9a-f]{2});?\");\n\tprivate static final Pattern P_VALID_ENTITIES = Pattern.compile(\"&([^&;]*)(?=(;|&|$))\");\n\tprivate static final Pattern P_VALID_QUOTES = Pattern.compile(\"(>|^)([^<]+?)(<|$)\", Pattern.DOTALL);\n\tprivate static final Pattern P_END_ARROW = Pattern.compile(\"^>\");\n\tprivate static final Pattern P_BODY_TO_END = Pattern.compile(\"<([^>]*?)(?=<|$)\");\n\tprivate static final Pattern P_XML_CONTENT = Pattern.compile(\"(^|>)([^<]*?)(?=>)\");\n\tprivate static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile(\"<([^>]*?)(?=<|$)\");\n\tprivate static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile(\"(^|>)([^<]*?)(?=>)\");\n\tprivate static final Pattern P_AMP = Pattern.compile(\"&\");\n\tprivate static final Pattern P_QUOTE = Pattern.compile(\"\\\"\");\n\tprivate static final Pattern P_LEFT_ARROW = Pattern.compile(\"<\");\n\tprivate static final Pattern P_RIGHT_ARROW = Pattern.compile(\">\");\n\tprivate static final Pattern P_BOTH_ARROWS = Pattern.compile(\"<>\");\n\n\t// @xxx could grow large... maybe use sesat's ReferenceMap\n\tprivate static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new SafeConcurrentHashMap<>();\n\tprivate static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new SafeConcurrentHashMap<>();\n\n\t/**\n\t * set of allowed html elements, along with allowed attributes for each element\n\t **/\n\tprivate final Map<String, List<String>> vAllowed;\n\t/**\n\t * counts of open tags for each (allowable) html element\n\t **/\n\tprivate final Map<String, Integer> vTagCounts = new HashMap<>();\n\n\t/**\n\t * html elements which must always be self-closing (e.g. \"&lt;img /&gt;\")\n\t **/\n\tprivate final String[] vSelfClosingTags;\n\t/**\n\t * html elements which must always have separate opening and closing tags (e.g. \"&lt;b&gt;&lt;/b&gt;\")\n\t **/\n\tprivate final String[] vNeedClosingTags;\n\t/**\n\t * set of disallowed html elements\n\t **/\n\tprivate final String[] vDisallowed;\n\t/**\n\t * attributes which should be checked for valid protocols\n\t **/\n\tprivate final String[] vProtocolAtts;\n\t/**\n\t * allowed protocols\n\t **/\n\tprivate final String[] vAllowedProtocols;\n\t/**\n\t * tags which should be removed if they contain no content (e.g. \"&lt;b&gt;&lt;/b&gt;\" or \"&lt;b /&gt;\")\n\t **/\n\tprivate final String[] vRemoveBlanks;\n\t/**\n\t * entities allowed within html markup\n\t **/\n\tprivate final String[] vAllowedEntities;\n\t/**\n\t * flag determining whether comments are allowed in input String.\n\t */\n\tprivate final boolean stripComment;\n\tprivate final boolean encodeQuotes;\n\tprivate boolean vDebug = false;\n\t/**\n\t * flag determining whether to try to make tags when presented with \"unbalanced\" angle brackets (e.g. \"&lt;b text &lt;/b&gt;\" becomes \"&lt;b&gt; text &lt;/g&gt;\").\n\t * If set to false, unbalanced angle brackets will be\n\t * html escaped.\n\t */\n\tprivate final boolean alwaysMakeTags;\n\n\t/**\n\t * Default constructor.\n\t */\n\tpublic HTMLFilter() {\n\t\tvAllowed = new HashMap<>();\n\n\t\tfinal ArrayList<String> a_atts = new ArrayList<>();\n\t\ta_atts.add(\"href\");\n\t\ta_atts.add(\"target\");\n\t\tvAllowed.put(\"a\", a_atts);\n\n\t\tfinal ArrayList<String> img_atts = new ArrayList<>();\n\t\timg_atts.add(\"src\");\n\t\timg_atts.add(\"width\");\n\t\timg_atts.add(\"height\");\n\t\timg_atts.add(\"alt\");\n\t\tvAllowed.put(\"img\", img_atts);\n\n\t\tfinal ArrayList<String> no_atts = new ArrayList<>();\n\t\tvAllowed.put(\"b\", no_atts);\n\t\tvAllowed.put(\"strong\", no_atts);\n\t\tvAllowed.put(\"i\", no_atts);\n\t\tvAllowed.put(\"em\", no_atts);\n\t\tvAllowed.put(\"p\", no_atts);\n\n\t\tvSelfClosingTags = new String[]{\"img\"};\n\t\tvNeedClosingTags = new String[]{\"a\", \"b\", \"strong\", \"i\", \"em\"};\n\t\tvDisallowed = new String[]{};\n\t\tvAllowedProtocols = new String[]{\"http\", \"mailto\", \"https\"}; // no ftp.\n\t\tvProtocolAtts = new String[]{\"src\", \"href\"};\n\t\tvRemoveBlanks = new String[]{\"a\", \"b\", \"strong\", \"i\", \"em\"};\n\t\tvAllowedEntities = new String[]{\"amp\", \"gt\", \"lt\", \"quot\"};\n\t\tstripComment = true;\n\t\tencodeQuotes = true;\n\t\talwaysMakeTags = true;\n\t}\n\n\t/**\n\t * Set debug flag to true. Otherwise use default settings. See the default constructor.\n\t *\n\t * @param debug turn debug on with a true argument\n\t */\n\tpublic HTMLFilter(final boolean debug) {\n\t\tthis();\n\t\tvDebug = debug;\n\n\t}\n\n\t/**\n\t * Map-parameter configurable constructor.\n\t *\n\t * @param conf map containing configuration. keys match field names.\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic HTMLFilter(final Map<String, Object> conf) {\n\n\t\tassert conf.containsKey(\"vAllowed\") : \"configuration requires vAllowed\";\n\t\tassert conf.containsKey(\"vSelfClosingTags\") : \"configuration requires vSelfClosingTags\";\n\t\tassert conf.containsKey(\"vNeedClosingTags\") : \"configuration requires vNeedClosingTags\";\n\t\tassert conf.containsKey(\"vDisallowed\") : \"configuration requires vDisallowed\";\n\t\tassert conf.containsKey(\"vAllowedProtocols\") : \"configuration requires vAllowedProtocols\";\n\t\tassert conf.containsKey(\"vProtocolAtts\") : \"configuration requires vProtocolAtts\";\n\t\tassert conf.containsKey(\"vRemoveBlanks\") : \"configuration requires vRemoveBlanks\";\n\t\tassert conf.containsKey(\"vAllowedEntities\") : \"configuration requires vAllowedEntities\";\n\n\t\tvAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get(\"vAllowed\"));\n\t\tvSelfClosingTags = (String[]) conf.get(\"vSelfClosingTags\");\n\t\tvNeedClosingTags = (String[]) conf.get(\"vNeedClosingTags\");\n\t\tvDisallowed = (String[]) conf.get(\"vDisallowed\");\n\t\tvAllowedProtocols = (String[]) conf.get(\"vAllowedProtocols\");\n\t\tvProtocolAtts = (String[]) conf.get(\"vProtocolAtts\");\n\t\tvRemoveBlanks = (String[]) conf.get(\"vRemoveBlanks\");\n\t\tvAllowedEntities = (String[]) conf.get(\"vAllowedEntities\");\n\t\tstripComment = conf.containsKey(\"stripComment\") ? (Boolean) conf.get(\"stripComment\") : true;\n\t\tencodeQuotes = conf.containsKey(\"encodeQuotes\") ? (Boolean) conf.get(\"encodeQuotes\") : true;\n\t\talwaysMakeTags = conf.containsKey(\"alwaysMakeTags\") ? (Boolean) conf.get(\"alwaysMakeTags\") : true;\n\t}\n\n\tprivate void reset() {\n\t\tvTagCounts.clear();\n\t}\n\n\tprivate void debug(final String msg) {\n\t\tif (vDebug) {\n\t\t\tConsole.log(msg);\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------\n\t// my versions of some PHP library functions\n\tpublic static String chr(final int decimal) {\n\t\treturn String.valueOf((char) decimal);\n\t}\n\n\tpublic static String htmlSpecialChars(final String s) {\n\t\tString result = s;\n\t\tresult = regexReplace(P_AMP, \"&amp;\", result);\n\t\tresult = regexReplace(P_QUOTE, \"&quot;\", result);\n\t\tresult = regexReplace(P_LEFT_ARROW, \"&lt;\", result);\n\t\tresult = regexReplace(P_RIGHT_ARROW, \"&gt;\", result);\n\t\treturn result;\n\t}\n\n\t// ---------------------------------------------------------------\n\n\t/**\n\t * given a user submitted input String, filter out any invalid or restricted html.\n\t *\n\t * @param input text (i.e. submitted by a user) than may contain html\n\t * @return \"clean\" version of input, with only valid, whitelisted html elements allowed\n\t */\n\tpublic String filter(final String input) {\n\t\treset();\n\t\tString s = input;\n\n\t\tdebug(\"************************************************\");\n\t\tdebug(\"              INPUT: \" + input);\n\n\t\ts = escapeComments(s);\n\t\tdebug(\"     escapeComments: \" + s);\n\n\t\ts = balanceHTML(s);\n\t\tdebug(\"        balanceHTML: \" + s);\n\n\t\ts = checkTags(s);\n\t\tdebug(\"          checkTags: \" + s);\n\n\t\ts = processRemoveBlanks(s);\n\t\tdebug(\"processRemoveBlanks: \" + s);\n\n\t\ts = validateEntities(s);\n\t\tdebug(\"    validateEntites: \" + s);\n\n\t\tdebug(\"************************************************\\n\\n\");\n\t\treturn s;\n\t}\n\n\tpublic boolean isAlwaysMakeTags() {\n\t\treturn alwaysMakeTags;\n\t}\n\n\tpublic boolean isStripComments() {\n\t\treturn stripComment;\n\t}\n\n\tprivate String escapeComments(final String s) {\n\t\tfinal Matcher m = P_COMMENTS.matcher(s);\n\t\tfinal StringBuffer buf = new StringBuffer();\n\t\tif (m.find()) {\n\t\t\tfinal String match = m.group(1); // (.*?)\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(\"<!--\" + htmlSpecialChars(match) + \"-->\"));\n\t\t}\n\t\tm.appendTail(buf);\n\n\t\treturn buf.toString();\n\t}\n\n\tprivate String balanceHTML(String s) {\n\t\tif (alwaysMakeTags) {\n\t\t\t//\n\t\t\t// try and form html\n\t\t\t//\n\t\t\ts = regexReplace(P_END_ARROW, \"\", s);\n\t\t\ts = regexReplace(P_BODY_TO_END, \"<$1>\", s);\n\t\t\ts = regexReplace(P_XML_CONTENT, \"$1<$2\", s);\n\n\t\t} else {\n\t\t\t//\n\t\t\t// escape stray brackets\n\t\t\t//\n\t\t\ts = regexReplace(P_STRAY_LEFT_ARROW, \"&lt;$1\", s);\n\t\t\ts = regexReplace(P_STRAY_RIGHT_ARROW, \"$1$2&gt;<\", s);\n\n\t\t\t//\n\t\t\t// the last regexp causes '<>' entities to appear\n\t\t\t// (we need to do a lookahead assertion so that the last bracket can\n\t\t\t// be used in the next pass of the regexp)\n\t\t\t//\n\t\t\ts = regexReplace(P_BOTH_ARROWS, \"\", s);\n\t\t}\n\n\t\treturn s;\n\t}\n\n\tprivate String checkTags(String s) {\n\t\tMatcher m = P_TAGS.matcher(s);\n\n\t\tfinal StringBuffer buf = new StringBuffer();\n\t\twhile (m.find()) {\n\t\t\tString replaceStr = m.group(1);\n\t\t\treplaceStr = processTag(replaceStr);\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));\n\t\t}\n\t\tm.appendTail(buf);\n\n\t\t// these get tallied in processTag\n\t\t// (remember to reset before subsequent calls to filter method)\n\t\tfinal StringBuilder sBuilder = new StringBuilder(buf.toString());\n\t\tfor (String key : vTagCounts.keySet()) {\n\t\t\tfor (int ii = 0; ii < vTagCounts.get(key); ii++) {\n\t\t\t\tsBuilder.append(\"</\").append(key).append(\">\");\n\t\t\t}\n\t\t}\n\t\ts = sBuilder.toString();\n\n\t\treturn s;\n\t}\n\n\tprivate String processRemoveBlanks(final String s) {\n\t\tString result = s;\n\t\tfor (String tag : vRemoveBlanks) {\n\t\t\tif (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) {\n\t\t\t\tP_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile(\"<\" + tag + \"(\\\\s[^>]*)?></\" + tag + \">\"));\n\t\t\t}\n\t\t\tresult = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), \"\", result);\n\t\t\tif (!P_REMOVE_SELF_BLANKS.containsKey(tag)) {\n\t\t\t\tP_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile(\"<\" + tag + \"(\\\\s[^>]*)?/>\"));\n\t\t\t}\n\t\t\tresult = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), \"\", result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {\n\t\tMatcher m = regex_pattern.matcher(s);\n\t\treturn m.replaceAll(replacement);\n\t}\n\n\tprivate String processTag(final String s) {\n\t\t// ending tags\n\t\tMatcher m = P_END_TAG.matcher(s);\n\t\tif (m.find()) {\n\t\t\tfinal String name = m.group(1).toLowerCase();\n\t\t\tif (allowed(name)) {\n\t\t\t\tif (false == inArray(name, vSelfClosingTags)) {\n\t\t\t\t\tif (vTagCounts.containsKey(name)) {\n\t\t\t\t\t\tvTagCounts.put(name, vTagCounts.get(name) - 1);\n\t\t\t\t\t\treturn \"</\" + name + \">\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// starting tags\n\t\tm = P_START_TAG.matcher(s);\n\t\tif (m.find()) {\n\t\t\tfinal String name = m.group(1).toLowerCase();\n\t\t\tfinal String body = m.group(2);\n\t\t\tString ending = m.group(3);\n\n\t\t\t// debug( \"in a starting tag, name='\" + name + \"'; body='\" + body + \"'; ending='\" + ending + \"'\" );\n\t\t\tif (allowed(name)) {\n\t\t\t\tfinal StringBuilder params = new StringBuilder();\n\n\t\t\t\tfinal Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);\n\t\t\t\tfinal Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);\n\t\t\t\tfinal List<String> paramNames = new ArrayList<>();\n\t\t\t\tfinal List<String> paramValues = new ArrayList<>();\n\t\t\t\twhile (m2.find()) {\n\t\t\t\t\tparamNames.add(m2.group(1)); // ([a-z0-9]+)\n\t\t\t\t\tparamValues.add(m2.group(3)); // (.*?)\n\t\t\t\t}\n\t\t\t\twhile (m3.find()) {\n\t\t\t\t\tparamNames.add(m3.group(1)); // ([a-z0-9]+)\n\t\t\t\t\tparamValues.add(m3.group(3)); // ([^\\\"\\\\s']+)\n\t\t\t\t}\n\n\t\t\t\tString paramName, paramValue;\n\t\t\t\tfor (int ii = 0; ii < paramNames.size(); ii++) {\n\t\t\t\t\tparamName = paramNames.get(ii).toLowerCase();\n\t\t\t\t\tparamValue = paramValues.get(ii);\n\n\t\t\t\t\t// debug( \"paramName='\" + paramName + \"'\" );\n\t\t\t\t\t// debug( \"paramValue='\" + paramValue + \"'\" );\n\t\t\t\t\t// debug( \"allowed? \" + vAllowed.get( name ).contains( paramName ) );\n\n\t\t\t\t\tif (allowedAttribute(name, paramName)) {\n\t\t\t\t\t\tif (inArray(paramName, vProtocolAtts)) {\n\t\t\t\t\t\t\tparamValue = processParamProtocol(paramValue);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparams.append(CharUtil.SPACE).append(paramName).append(\"=\\\"\").append(paramValue).append(\"\\\"\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (inArray(name, vSelfClosingTags)) {\n\t\t\t\t\tending = \" /\";\n\t\t\t\t}\n\n\t\t\t\tif (inArray(name, vNeedClosingTags)) {\n\t\t\t\t\tending = \"\";\n\t\t\t\t}\n\n\t\t\t\tif (ending == null || ending.length() < 1) {\n\t\t\t\t\tif (vTagCounts.containsKey(name)) {\n\t\t\t\t\t\tvTagCounts.put(name, vTagCounts.get(name) + 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvTagCounts.put(name, 1);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tending = \" /\";\n\t\t\t\t}\n\t\t\t\treturn \"<\" + name + params + ending + \">\";\n\t\t\t} else {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t}\n\n\t\t// comments\n\t\tm = P_COMMENT.matcher(s);\n\t\tif (!stripComment && m.find()) {\n\t\t\treturn \"<\" + m.group() + \">\";\n\t\t}\n\n\t\treturn \"\";\n\t}\n\n\tprivate String processParamProtocol(String s) {\n\t\ts = decodeEntities(s);\n\t\tfinal Matcher m = P_PROTOCOL.matcher(s);\n\t\tif (m.find()) {\n\t\t\tfinal String protocol = m.group(1);\n\t\t\tif (!inArray(protocol, vAllowedProtocols)) {\n\t\t\t\t// bad protocol, turn into local anchor link instead\n\t\t\t\ts = \"#\" + s.substring(protocol.length() + 1);\n\t\t\t\tif (s.startsWith(\"#//\")) {\n\t\t\t\t\ts = \"#\" + s.substring(3);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn s;\n\t}\n\n\tprivate String decodeEntities(String s) {\n\t\tStringBuffer buf = new StringBuffer();\n\n\t\tMatcher m = P_ENTITY.matcher(s);\n\t\twhile (m.find()) {\n\t\t\tfinal String match = m.group(1);\n\t\t\tfinal int decimal = Integer.decode(match);\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));\n\t\t}\n\t\tm.appendTail(buf);\n\t\ts = buf.toString();\n\n\t\tbuf = new StringBuffer();\n\t\tm = P_ENTITY_UNICODE.matcher(s);\n\t\twhile (m.find()) {\n\t\t\tfinal String match = m.group(1);\n\t\t\tfinal int decimal = Integer.parseInt(match, 16);\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));\n\t\t}\n\t\tm.appendTail(buf);\n\t\ts = buf.toString();\n\n\t\tbuf = new StringBuffer();\n\t\tm = P_ENCODE.matcher(s);\n\t\twhile (m.find()) {\n\t\t\tfinal String match = m.group(1);\n\t\t\tfinal int decimal = Integer.parseInt(match, 16);\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));\n\t\t}\n\t\tm.appendTail(buf);\n\t\ts = buf.toString();\n\n\t\ts = validateEntities(s);\n\t\treturn s;\n\t}\n\n\tprivate String validateEntities(final String s) {\n\t\tStringBuffer buf = new StringBuffer();\n\n\t\t// validate entities throughout the string\n\t\tMatcher m = P_VALID_ENTITIES.matcher(s);\n\t\twhile (m.find()) {\n\t\t\tfinal String one = m.group(1); // ([^&;]*)\n\t\t\tfinal String two = m.group(2); // (?=(;|&|$))\n\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));\n\t\t}\n\t\tm.appendTail(buf);\n\n\t\treturn encodeQuotes(buf.toString());\n\t}\n\n\tprivate String encodeQuotes(final String s) {\n\t\tif (encodeQuotes) {\n\t\t\tStringBuffer buf = new StringBuffer();\n\t\t\tMatcher m = P_VALID_QUOTES.matcher(s);\n\t\t\twhile (m.find()) {\n\t\t\t\tfinal String one = m.group(1); // (>|^)\n\t\t\t\tfinal String two = m.group(2); // ([^<]+?)\n\t\t\t\tfinal String three = m.group(3); // (<|$)\n\t\t\t\tm.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, \"&quot;\", two) + three));\n\t\t\t}\n\t\t\tm.appendTail(buf);\n\t\t\treturn buf.toString();\n\t\t} else {\n\t\t\treturn s;\n\t\t}\n\t}\n\n\tprivate String checkEntity(final String preamble, final String term) {\n\n\t\treturn \";\".equals(term) && isValidEntity(preamble) ? '&' + preamble : \"&amp;\" + preamble;\n\t}\n\n\tprivate boolean isValidEntity(final String entity) {\n\t\treturn inArray(entity, vAllowedEntities);\n\t}\n\n\tprivate static boolean inArray(final String s, final String[] array) {\n\t\tfor (String item : array) {\n\t\t\tif (item != null && item.equals(s)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate boolean allowed(final String name) {\n\t\treturn (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);\n\t}\n\n\tprivate boolean allowedAttribute(final String name, final String paramName) {\n\t\treturn allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/Header.java",
    "content": "package cn.hutool.http;\n\n/**\n * Http 头域\n *\n * @author Looly\n */\npublic enum Header {\n\n\t//------------------------------------------------------------- 通用头域\n\t/**\n\t * 提供验证头，例如：\n\t * <pre>\n\t * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\t * </pre>\n\t */\n\tAUTHORIZATION(\"Authorization\"),\n\t/**\n\t * 提供给代理服务器的用于身份验证的凭证，例如：\n\t * <pre>\n\t * Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\t * </pre>\n\t */\n\tPROXY_AUTHORIZATION(\"Proxy-Authorization\"),\n\t/**\n\t * 提供日期和时间标志,说明报文是什么时间创建的\n\t */\n\tDATE(\"Date\"),\n\t/**\n\t * 允许客户端和服务器指定与请求/响应连接有关的选项\n\t */\n\tCONNECTION(\"Connection\"),\n\t/**\n\t * 给出发送端使用的MIME版本\n\t */\n\tMIME_VERSION(\"MIME-Version\"),\n\t/**\n\t * 如果报文采用了分块传输编码(chunked transfer encoding) 方式,就可以用这个首部列出位于报文拖挂(trailer)部分的首部集合\n\t */\n\tTRAILER(\"Trailer\"),\n\t/**\n\t * 告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式\n\t */\n\tTRANSFER_ENCODING(\"Transfer-Encoding\"),\n\t/**\n\t * 给出了发送端可能想要\"升级\"使用的新版本和协议\n\t */\n\tUPGRADE(\"Upgrade\"),\n\t/**\n\t * 显示了报文经过的中间节点\n\t */\n\tVIA(\"Via\"),\n\t/**\n\t * 指定请求和响应遵循的缓存机制\n\t */\n\tCACHE_CONTROL(\"Cache-Control\"),\n\t/**\n\t * 用来包含实现特定的指令，最常用的是Pragma:no-cache。在HTTP/1.1协议中，它的含义和Cache- Control:no-cache相同\n\t */\n\tPRAGMA(\"Pragma\"),\n\t/**\n\t * 请求表示提交内容类型或返回返回内容的MIME类型\n\t */\n\tCONTENT_TYPE(\"Content-Type\"),\n\n\t//------------------------------------------------------------- 请求头域\n\t/**\n\t * 指定请求资源的Intenet主机和端口号，必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域，否则系统会以400状态码返回\n\t */\n\tHOST(\"Host\"),\n\t/**\n\t * 允许客户端指定请求uri的源资源地址，这可以允许服务器生成回退链表，可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被 追踪。如果请求的uri没有自己的uri地址，Referer不能被发送。如果指定的是部分uri地址，则此地址应该是一个相对地址\n\t */\n\tREFERER(\"Referer\"),\n\t/**\n\t * 指定请求的域\n\t */\n\tORIGIN(\"Origin\"),\n\t/**\n\t * HTTP客户端运行的浏览器类型的详细信息。通过该头部信息，web服务器可以判断到当前HTTP请求的客户端浏览器类别\n\t */\n\tUSER_AGENT(\"User-Agent\"),\n\t/**\n\t * 指定客户端能够接收的内容类型，内容类型中的先后次序表示客户端接收的先后次序\n\t */\n\tACCEPT(\"Accept\"),\n\t/**\n\t * 指定HTTP客户端浏览器用来展示返回信息所优先选择的语言\n\t */\n\tACCEPT_LANGUAGE(\"Accept-Language\"),\n\t/**\n\t * 指定客户端浏览器可以支持的web服务器返回内容压缩编码类型\n\t */\n\tACCEPT_ENCODING(\"Accept-Encoding\"),\n\t/**\n\t * 浏览器可以接受的字符编码集\n\t */\n\tACCEPT_CHARSET(\"Accept-Charset\"),\n\t/**\n\t * HTTP请求发送时，会把保存在该请求域名下的所有cookie值一起发送给web服务器\n\t */\n\tCOOKIE(\"Cookie\"),\n\t/**\n\t * 请求的内容长度\n\t */\n\tCONTENT_LENGTH(\"Content-Length\"),\n\n\t//------------------------------------------------------------- 响应头域\n\t/**\n\t * 提供WWW验证响应头\n\t */\n\tWWW_AUTHENTICATE(\"WWW-Authenticate\"),\n\t/**\n\t * Cookie\n\t */\n\tSET_COOKIE(\"Set-Cookie\"),\n\t/**\n\t * Content-Encoding\n\t */\n\tCONTENT_ENCODING(\"Content-Encoding\"),\n\t/**\n\t * Content-Disposition\n\t */\n\tCONTENT_DISPOSITION(\"Content-Disposition\"),\n\t/**\n\t * ETag\n\t */\n\tETAG(\"ETag\"),\n\t/**\n\t * 重定向指示到的URL\n\t */\n\tLOCATION(\"Location\");\n\n\tprivate final String value;\n\n\tHeader(String value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取值\n\t *\n\t * @return 值\n\t */\n\tpublic String getValue(){\n\t\treturn this.value;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getValue();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HtmlUtil.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.EscapeUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * HTML工具类\n *\n * <p>\n * 比如我们在使用爬虫爬取HTML页面后，需要对返回页面的HTML内容做一定处理，<br>\n * 比如去掉指定标签（例如广告栏等）、去除JS、去掉样式等等，这些操作都可以使用此工具类完成。\n *\n * @author xiaoleilu\n *\n */\npublic class HtmlUtil {\n\n\tpublic static final String NBSP = StrUtil.HTML_NBSP;\n\tpublic static final String AMP = StrUtil.HTML_AMP;\n\tpublic static final String QUOTE = StrUtil.HTML_QUOTE;\n\tpublic static final String APOS = StrUtil.HTML_APOS;\n\tpublic static final String LT = StrUtil.HTML_LT;\n\tpublic static final String GT = StrUtil.HTML_GT;\n\n\tpublic static final String RE_HTML_MARK = \"(<[^<]*?>)|(<[\\\\s]*?/[^<]*?>)|(<[^<]*?/[\\\\s]*?>)\";\n\tpublic static final String RE_HTML_EMPTY_MARK = \"<(\\\\w+)([^>]*)>\\\\s*</\\\\1>\";\n\tpublic static final String RE_SCRIPT = \"<[\\\\s]*?script[^>]*?>.*?<[\\\\s]*?\\\\/[\\\\s]*?script[\\\\s]*?>\";\n\n\tprivate static final char[][] TEXT = new char[256][];\n\n\tstatic {\n\t\t// ascii码值最大的是【0x7f=127】，扩展ascii码值最大的是【0xFF=255】，因为ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能的字符，标准ASCII码也叫基础ASCII码。\n\t\tfor (int i = 0; i < 256; i++) {\n\t\t\tTEXT[i] = new char[] { (char) i };\n\t\t}\n\n\t\t// special HTML characters\n\t\tTEXT['\\''] = \"&#039;\".toCharArray(); // 单引号 ('&apos;' doesn't work - it is not by the w3 specs)\n\t\tTEXT['\"'] = QUOTE.toCharArray(); // 双引号\n\t\tTEXT['&'] = AMP.toCharArray(); // &符\n\t\tTEXT['<'] = LT.toCharArray(); // 小于号\n\t\tTEXT['>'] = GT.toCharArray(); // 大于号\n\t\tTEXT[' '] = NBSP.toCharArray(); // 不断开空格（non-breaking space，缩写nbsp。ASCII值是32：是用键盘输入的空格；ASCII值是160：不间断空格，即 &nbsp，所产生的空格，作用是在页面换行时不被打断）\n\t}\n\n\t/**\n\t * 转义文本中的HTML字符为安全的字符，以下字符被转义：\n\t * <ul>\n\t * <li>' 替换为 &amp;#039; (&amp;apos; doesn't work in HTML4)</li>\n\t * <li>\" 替换为 &amp;quot;</li>\n\t * <li>&amp; 替换为 &amp;amp;</li>\n\t * <li>&lt; 替换为 &amp;lt;</li>\n\t * <li>&gt; 替换为 &amp;gt;</li>\n\t * </ul>\n\t *\n\t * @param text 被转义的文本\n\t * @return 转义后的文本\n\t */\n\tpublic static String escape(String text) {\n\t\treturn encode(text);\n\t}\n\n\t/**\n\t * 还原被转义的HTML特殊字符\n\t *\n\t * @param htmlStr 包含转义符的HTML内容\n\t * @return 转换后的字符串\n\t */\n\tpublic static String unescape(String htmlStr) {\n\t\tif (StrUtil.isBlank(htmlStr)) {\n\t\t\treturn htmlStr;\n\t\t}\n\n\t\treturn EscapeUtil.unescapeHtml4(htmlStr);\n\t}\n\n\t// ---------------------------------------------------------------- encode text\n\n\t/**\n\t * 清除所有HTML标签，但是不删除标签内的内容\n\t *\n\t * @param content 文本\n\t * @return 清除标签后的文本\n\t */\n\tpublic static String cleanHtmlTag(String content) {\n\t\treturn content.replaceAll(RE_HTML_MARK, \"\");\n\t}\n\n\t/**\n\t * 清除所有HTML空标签<br>\n\t * 例如：&lt;p&gt;&lt;/p&gt;\n\t *\n\t * @param content 文本\n\t * @return 清除空标签后的文本\n\t */\n\tpublic static String cleanEmptyTag(String content) {\n\t\treturn content.replaceAll(RE_HTML_EMPTY_MARK, \"\");\n\t}\n\n\t/**\n\t * 清除指定HTML标签和被标签包围的内容<br>\n\t * 不区分大小写\n\t *\n\t * @param content 文本\n\t * @param tagNames 要清除的标签\n\t * @return 去除标签后的文本\n\t */\n\tpublic static String removeHtmlTag(String content, String... tagNames) {\n\t\treturn removeHtmlTag(content, true, tagNames);\n\t}\n\n\t/**\n\t * 清除指定HTML标签，不包括内容<br>\n\t * 不区分大小写\n\t *\n\t * @param content 文本\n\t * @param tagNames 要清除的标签\n\t * @return 去除标签后的文本\n\t */\n\tpublic static String unwrapHtmlTag(String content, String... tagNames) {\n\t\treturn removeHtmlTag(content, false, tagNames);\n\t}\n\n\t/**\n\t * 清除指定HTML标签<br>\n\t * 不区分大小写\n\t *\n\t * @param content 文本\n\t * @param withTagContent 是否去掉被包含在标签中的内容\n\t * @param tagNames 要清除的标签\n\t * @return 去除标签后的文本\n\t */\n\tpublic static String removeHtmlTag(String content, boolean withTagContent, String... tagNames) {\n\t\tString regex;\n\t\tfor (String tagName : tagNames) {\n\t\t\tif (StrUtil.isBlank(tagName)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttagName = tagName.trim();\n\t\t\t// (?i)表示其后面的表达式忽略大小写\n\t\t\tif (withTagContent) {\n\t\t\t\t// 标签及其包含内容\n\t\t\t\tregex = StrUtil.format(\"(?i)<{}(\\\\s+[^>]*?)?/?>(.*?</{}>)?\", tagName, tagName);\n\t\t\t} else {\n\t\t\t\t// 标签不包含内容\n\t\t\t\tregex = StrUtil.format(\"(?i)<{}(\\\\s+[^>]*?)?/?>|</?{}>\", tagName, tagName);\n\t\t\t}\n\n\t\t\tcontent = ReUtil.delAll(regex, content); // 非自闭标签小写\n\t\t}\n\t\treturn content;\n\t}\n\n\t/**\n\t * 去除HTML标签中的属性，如果多个标签有相同属性，都去除\n\t *\n\t * @param content 文本\n\t * @param attrs 属性名（不区分大小写）\n\t * @return 处理后的文本\n\t */\n\tpublic static String removeHtmlAttr(String content, String... attrs) {\n\t\tString regex;\n\t\tfor (final String attr : attrs) {\n\t\t\t// (?i)     表示忽略大小写\n\t\t\t// \\s*      属性名前后的空白符去除\n\t\t\t// [^>]+?   属性值，至少有一个非>的字符，>表示标签结束\n\t\t\t// \\s+(?=>) 表示属性值后跟空格加>，即末尾的属性，此时去掉空格\n\t\t\t// (?=\\s|>) 表示属性值后跟空格（属性后还有别的属性）或者跟>（最后一个属性）\n\t\t\tregex = StrUtil.format(\"(?i)(\\\\s*{}\\\\s*=\\\\s*)\" +\n\t\t\t\t\"(\" +\n\t\t\t\t// name=\"xxxx\"\n\t\t\t\t\"([\\\"][^\\\"]+?[\\\"])|\" +\n\t\t\t\t// name=xxx > 或者 name=xxx> 或者 name=xxx name2=xxx\n\t\t\t\t\"([^>]+?\\\\s*(?=\\\\s|>))\" +\n\t\t\t\t\")\", attr);\n\t\t\tcontent = content.replaceAll(regex, StrUtil.EMPTY);\n\t\t}\n\n\t\t// issue#I8YV0K 去除尾部空格\n\t\tcontent = ReUtil.replaceAll(content, \"\\\\s+(>|/>)\", \"$1\");\n\n\t\treturn content;\n\t}\n\n\t/**\n\t * 去除指定标签的所有属性\n\t *\n\t * @param content 内容\n\t * @param tagNames 指定标签\n\t * @return 处理后的文本\n\t */\n\tpublic static String removeAllHtmlAttr(String content, String... tagNames) {\n\t\tString regex;\n\t\tfor (String tagName : tagNames) {\n\t\t\tregex = StrUtil.format(\"(?i)<{}[^>]*?>\", tagName);\n\t\t\tcontent = content.replaceAll(regex, StrUtil.format(\"<{}>\", tagName));\n\t\t}\n\t\treturn content;\n\t}\n\n\t/**\n\t * Encoder\n\t *\n\t * @param text 被编码的文本\n\t * @return 编码后的字符\n\t */\n\tprivate static String encode(String text) {\n\t\tint len;\n\t\tif ((text == null) || ((len = text.length()) == 0)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t\tStringBuilder buffer = new StringBuilder(len + (len >> 2));\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = text.charAt(i);\n\t\t\tif (c < 256) {\n\t\t\t\tbuffer.append(TEXT[c]);\n\t\t\t} else {\n\t\t\t\tbuffer.append(c);\n\t\t\t}\n\t\t}\n\t\treturn buffer.toString();\n\t}\n\n\t/**\n\t * 过滤HTML文本，防止XSS攻击\n\t *\n\t * @param htmlContent HTML内容\n\t * @return 过滤后的内容\n\t */\n\tpublic static String filter(String htmlContent) {\n\t\treturn new HTMLFilter().filter(htmlContent);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpBase.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * http基类\n *\n * @param <T> 子类类型，方便链式编程\n * @author Looly\n */\n@SuppressWarnings(\"unchecked\")\npublic abstract class HttpBase<T> {\n\n\t/**\n\t * 默认的请求编码、URL的encode、decode编码\n\t */\n\tprotected static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\n\t/**\n\t * HTTP/1.0\n\t */\n\tpublic static final String HTTP_1_0 = \"HTTP/1.0\";\n\t/**\n\t * HTTP/1.1\n\t */\n\tpublic static final String HTTP_1_1 = \"HTTP/1.1\";\n\n\t/**\n\t * 是否聚合重复请求头\n\t */\n\tprotected boolean isHeaderAggregated = false;\n\t/**\n\t * 存储头信息\n\t */\n\tprotected Map<String, List<String>> headers = new HashMap<>();\n\t/**\n\t * 编码\n\t */\n\tprotected Charset charset = DEFAULT_CHARSET;\n\t/**\n\t * http版本\n\t */\n\tprotected String httpVersion = HTTP_1_1;\n\t/**\n\t * 存储主体\n\t */\n\tprotected Resource body;\n\n\t// ---------------------------------------------------------------- Headers start\n\n\t/**\n\t * 根据name获取头信息<br>\n\t * 根据RFC2616规范，header的name不区分大小写\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t */\n\tpublic String header(String name) {\n\t\tfinal List<String> values = headerList(name);\n\t\tif (CollectionUtil.isEmpty(values)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn values.get(0);\n\t}\n\n\t/**\n\t * 根据name获取头信息列表\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t * @since 3.1.1\n\t */\n\tpublic List<String> headerList(String name) {\n\t\tif (StrUtil.isBlank(name)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal CaseInsensitiveMap<String, List<String>> headersIgnoreCase = new CaseInsensitiveMap<>(this.headers);\n\t\treturn headersIgnoreCase.get(name.trim());\n\t}\n\n\t/**\n\t * 根据name获取头信息\n\t *\n\t * @param name Header名\n\t * @return Header值\n\t */\n\tpublic String header(Header name) {\n\t\tif (null == name) {\n\t\t\treturn null;\n\t\t}\n\t\treturn header(name.toString());\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 如果覆盖模式，则替换之前的值，否则加入到值列表中\n\t *\n\t * @param name       Header名\n\t * @param value      Header值\n\t * @param isOverride 是否覆盖已有值\n\t * @return T 本身\n\t */\n\tpublic T header(String name, String value, boolean isOverride) {\n\t\tif (null != name && null != value) {\n\t\t\tfinal List<String> values = headers.get(name.trim());\n\t\t\tif (isOverride || CollectionUtil.isEmpty(values)) {\n\t\t\t\tfinal ArrayList<String> valueList = new ArrayList<>();\n\t\t\t\tvalueList.add(value);\n\t\t\t\theaders.put(name.trim(), valueList);\n\t\t\t} else {\n\t\t\t\tvalues.add(value.trim());\n\t\t\t}\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 如果覆盖模式，则替换之前的值，否则加入到值列表中\n\t *\n\t * @param name       Header名\n\t * @param value      Header值\n\t * @param isOverride 是否覆盖已有值\n\t * @return T 本身\n\t */\n\tpublic T header(Header name, String value, boolean isOverride) {\n\t\treturn header(name.toString(), value, isOverride);\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 覆盖模式，则替换之前的值\n\t *\n\t * @param name  Header名\n\t * @param value Header值\n\t * @return T 本身\n\t */\n\tpublic T header(Header name, String value) {\n\t\treturn header(name.toString(), value, true);\n\t}\n\n\t/**\n\t * 设置一个header<br>\n\t * 覆盖模式，则替换之前的值\n\t *\n\t * @param name  Header名\n\t * @param value Header值\n\t * @return T 本身\n\t */\n\tpublic T header(String name, String value) {\n\t\treturn header(name, value, true);\n\t}\n\n\t/**\n\t * 设置请求头\n\t *\n\t * @param headers    请求头\n\t * @param isOverride 是否覆盖已有头信息\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic T headerMap(Map<String, String> headers, boolean isOverride) {\n\t\tif (MapUtil.isEmpty(headers)) {\n\t\t\treturn (T) this;\n\t\t}\n\n\t\tfor (Entry<String, String> entry : headers.entrySet()) {\n\t\t\tthis.header(entry.getKey(), StrUtil.nullToEmpty(entry.getValue()), isOverride);\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置请求头<br>\n\t * 不覆盖原有请求头\n\t *\n\t * @param headers 请求头\n\t * @return this\n\t */\n\tpublic T header(Map<String, List<String>> headers) {\n\t\treturn header(headers, false);\n\t}\n\n\t/**\n\t * 设置请求头\n\t *\n\t * @param headers    请求头\n\t * @param isOverride 是否覆盖已有头信息\n\t * @return this\n\t * @since 4.0.8\n\t */\n\tpublic T header(Map<String, List<String>> headers, boolean isOverride) {\n\t\tif (MapUtil.isEmpty(headers)) {\n\t\t\treturn (T) this;\n\t\t}\n\n\t\tString name;\n\t\tfor (Entry<String, List<String>> entry : headers.entrySet()) {\n\t\t\tname = entry.getKey();\n\t\t\tfor (String value : entry.getValue()) {\n\t\t\t\tthis.header(name, StrUtil.nullToEmpty(value), isOverride);\n\t\t\t}\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 新增请求头<br>\n\t * 不覆盖原有请求头\n\t *\n\t * @param headers 请求头\n\t * @return this\n\t * @since 4.0.3\n\t */\n\tpublic T addHeaders(Map<String, String> headers) {\n\t\tif (MapUtil.isEmpty(headers)) {\n\t\t\treturn (T) this;\n\t\t}\n\n\t\tfor (Entry<String, String> entry : headers.entrySet()) {\n\t\t\tthis.header(entry.getKey(), StrUtil.nullToEmpty(entry.getValue()), false);\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 移除一个头信息\n\t *\n\t * @param name Header名\n\t * @return this\n\t */\n\tpublic T removeHeader(String name) {\n\t\tif (name != null) {\n\t\t\theaders.remove(name.trim());\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 移除一个头信息\n\t *\n\t * @param name Header名\n\t * @return this\n\t */\n\tpublic T removeHeader(Header name) {\n\t\treturn removeHeader(name.toString());\n\t}\n\n\t/**\n\t * 获取headers\n\t *\n\t * @return Headers Map\n\t */\n\tpublic Map<String, List<String>> headers() {\n\t\treturn Collections.unmodifiableMap(headers);\n\t}\n\n\t/**\n\t * 清除所有头信息，包括全局头信息\n\t *\n\t * @return this\n\t * @since 5.7.13\n\t */\n\tpublic T clearHeaders() {\n\t\tthis.headers.clear();\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置是否需要聚合重复的请求头，将重复的请求头聚合为,拼接\n\t *\n\t * @param aggregate 是否需要聚合\n\t * @return this\n\t * @since 5.8.37\n\t */\n\tpublic T headerAggregation(boolean aggregate) {\n\t\tthis.isHeaderAggregated = aggregate;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 获取是否需要聚合请求头状态\n\t *\n\t * @return isHeaderAggregated 请求头聚合状态\n\t * @since 5.8.37\n\t */\n\tpublic boolean isHeaderAggregated() {\n\t\treturn isHeaderAggregated;\n\t}\n\t// ---------------------------------------------------------------- Headers end\n\n\t/**\n\t * 返回http版本\n\t *\n\t * @return String\n\t */\n\tpublic String httpVersion() {\n\t\treturn httpVersion;\n\t}\n\n\t/**\n\t * 设置http版本，此方法不会影响到实际请求的HTTP版本，只用于帮助判断是否connect:Keep-Alive\n\t *\n\t * @param httpVersion Http版本，{@link HttpBase#HTTP_1_0}，{@link HttpBase#HTTP_1_1}\n\t * @return this\n\t */\n\tpublic T httpVersion(String httpVersion) {\n\t\tthis.httpVersion = httpVersion;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 获取bodyBytes存储字节码\n\t *\n\t * @return byte[]\n\t */\n\tpublic byte[] bodyBytes() {\n\t\treturn this.body == null ? null : this.body.readBytes();\n\t}\n\n\t/**\n\t * 返回字符集\n\t *\n\t * @return 字符集\n\t */\n\tpublic String charset() {\n\t\treturn charset.name();\n\t}\n\n\t/**\n\t * 设置字符集\n\t *\n\t * @param charset 字符集\n\t * @return T 自己\n\t * @see CharsetUtil\n\t */\n\tpublic T charset(String charset) {\n\t\tif (StrUtil.isNotBlank(charset)) {\n\t\t\tcharset(Charset.forName(charset));\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 设置字符集\n\t *\n\t * @param charset 字符集\n\t * @return T 自己\n\t * @see CharsetUtil\n\t */\n\tpublic T charset(Charset charset) {\n\t\tif (null != charset) {\n\t\t\tthis.charset = charset;\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder sb = StrUtil.builder();\n\t\tsb.append(\"Headers: \").append(StrUtil.CRLF);\n\t\tfor (Entry<String, List<String>> entry : this.headers.entrySet()) {\n\t\t\tsb.append(\"    \").append(\n\t\t\t\t\t\t\tentry.getKey()).append(\": \").append(CollUtil.join(entry.getValue(), \",\"))\n\t\t\t\t\t.append(StrUtil.CRLF);\n\t\t}\n\n\t\tsb.append(\"Body: \").append(StrUtil.CRLF);\n\t\tsb.append(\"    \").append(StrUtil.str(this.bodyBytes(), this.charset)).append(StrUtil.CRLF);\n\n\t\treturn sb.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpConfig.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.net.SSLUtil;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLSocketFactory;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\n\n/**\n * Http配置项\n *\n * @author looly\n * @since 5.8.0\n */\npublic class HttpConfig {\n\n\t/**\n\t * 创建默认Http配置信息\n\t *\n\t * @return HttpConfig\n\t */\n\tpublic static HttpConfig create() {\n\t\treturn new HttpConfig();\n\t}\n\n\t/**\n\t * 默认连接超时\n\t */\n\tint connectionTimeout = HttpGlobalConfig.getTimeout();\n\t/**\n\t * 默认读取超时\n\t */\n\tint readTimeout = HttpGlobalConfig.getTimeout();\n\n\t/**\n\t * 是否禁用缓存\n\t */\n\tboolean isDisableCache;\n\n\t/**\n\t * 最大重定向次数\n\t */\n\tint maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount();\n\n\t/**\n\t * 代理\n\t */\n\tProxy proxy;\n\n\t/**\n\t * HostnameVerifier，用于HTTPS安全连接\n\t */\n\tHostnameVerifier hostnameVerifier;\n\t/**\n\t * SSLSocketFactory，用于HTTPS安全连接\n\t */\n\tSSLSocketFactory ssf;\n\n\t/**\n\t * Chuncked块大小，0或小于0表示不设置Chuncked模式\n\t */\n\tint blockSize;\n\n\t/**\n\t * 获取是否忽略响应读取时可能的EOF异常。<br>\n\t * 在Http协议中，对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>\n\t * 如果服务端未遵循这个规范或响应没有正常结束，会报EOF异常，此选项用于是否忽略这个异常。\n\t */\n\tboolean ignoreEOFError = HttpGlobalConfig.isIgnoreEOFError();\n\t/**\n\t * 获取是否忽略解码URL，包括URL中的Path部分和Param部分。<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果此参数为{@code true}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t */\n\tboolean decodeUrl = HttpGlobalConfig.isDecodeUrl();\n\n\t/**\n\t * 请求前的拦截器，用于在请求前重新编辑请求\n\t */\n\tfinal HttpInterceptor.Chain<HttpRequest> requestInterceptors = GlobalInterceptor.INSTANCE.getCopiedRequestInterceptor();\n\t/**\n\t * 响应后的拦截器，用于在响应后处理逻辑\n\t */\n\tfinal HttpInterceptor.Chain<HttpResponse> responseInterceptors = GlobalInterceptor.INSTANCE.getCopiedResponseInterceptor();\n\n\t/**\n\t * 重定向时是否使用拦截器\n\t */\n\tboolean interceptorOnRedirect;\n\n\t/**\n\t * 自动重定向时是否处理cookie\n\t */\n\tboolean followRedirectsCookie;\n\t/**\n\t * issue#3719 如果为true，则当请求头中Content-Type为空时，使用默认的Content-Type，即application/x-www-form-urlencoded\n\t *\n\t * @since 5.8.33\n\t */\n\tboolean useDefaultContentTypeIfNull = true;\n\n\t/**\n\t * 是否忽略Content-Length，如果为true，则忽略Content-Length，自动根据响应内容计算Content-Length<br>\n\t * issue#ICB1B8@Gitee，此参数主要解决服务端响应中Content-Length错误的问题\n\t */\n\tboolean ignoreContentLength = false;\n\n\t/**\n\t * 设置超时，单位：毫秒<br>\n\t * 超时包括：\n\t *\n\t * <pre>\n\t * 1. 连接超时\n\t * 2. 读取响应超时\n\t * </pre>\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @see #setConnectionTimeout(int)\n\t * @see #setReadTimeout(int)\n\t */\n\tpublic HttpConfig timeout(int milliseconds) {\n\t\tsetConnectionTimeout(milliseconds);\n\t\tsetReadTimeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t */\n\tpublic HttpConfig setConnectionTimeout(int milliseconds) {\n\t\tthis.connectionTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t */\n\tpublic HttpConfig setReadTimeout(int milliseconds) {\n\t\tthis.readTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 禁用缓存\n\t *\n\t * @return this\n\t */\n\tpublic HttpConfig disableCache() {\n\t\tthis.isDisableCache = true;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置最大重定向次数<br>\n\t * 如果次数小于1则表示不重定向，大于等于1表示打开重定向\n\t *\n\t * @param maxRedirectCount 最大重定向次数\n\t * @return this\n\t */\n\tpublic HttpConfig setMaxRedirectCount(int maxRedirectCount) {\n\t\tthis.maxRedirectCount = Math.max(maxRedirectCount, 0);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置域名验证器<br>\n\t * 只针对HTTPS请求，如果不设置，不做验证，所有域名被信任\n\t *\n\t * @param hostnameVerifier HostnameVerifier\n\t * @return this\n\t */\n\tpublic HttpConfig setHostnameVerifier(HostnameVerifier hostnameVerifier) {\n\t\t// 验证域\n\t\tthis.hostnameVerifier = hostnameVerifier;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Http代理\n\t *\n\t * @param host 代理 主机\n\t * @param port 代理 端口\n\t * @return this\n\t */\n\tpublic HttpConfig setHttpProxy(String host, int port) {\n\t\tfinal Proxy proxy = new Proxy(Proxy.Type.HTTP,\n\t\t\tnew InetSocketAddress(host, port));\n\t\treturn setProxy(proxy);\n\t}\n\n\t/**\n\t * 设置代理\n\t *\n\t * @param proxy 代理 {@link Proxy}\n\t * @return this\n\t */\n\tpublic HttpConfig setProxy(Proxy proxy) {\n\t\tthis.proxy = proxy;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置SSLSocketFactory<br>\n\t * 只针对HTTPS请求，如果不设置，使用默认的SSLSocketFactory<br>\n\t * 默认SSLSocketFactory为：SSLSocketFactoryBuilder.create().build();\n\t *\n\t * @param ssf SSLScketFactory\n\t * @return this\n\t */\n\tpublic HttpConfig setSSLSocketFactory(SSLSocketFactory ssf) {\n\t\tthis.ssf = ssf;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置HTTPS安全连接协议，只针对HTTPS请求，可以使用的协议包括：<br>\n\t * 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。\n\t *\n\t * <pre>\n\t * 1. TLSv1.2\n\t * 2. TLSv1.1\n\t * 3. SSLv3\n\t * ...\n\t * </pre>\n\t *\n\t * @param protocol 协议\n\t * @return this\n\t * @see SSLUtil#createSSLContext(String)\n\t * @see #setSSLSocketFactory(SSLSocketFactory)\n\t */\n\tpublic HttpConfig setSSLProtocol(String protocol) {\n\t\tAssert.notBlank(protocol, \"protocol must be not blank!\");\n\t\tsetSSLSocketFactory(SSLUtil.createSSLContext(protocol).getSocketFactory());\n\t\treturn this;\n\t}\n\n\t/**\n\t * 采用流方式上传数据，无需本地缓存数据。<br>\n\t * HttpUrlConnection默认是将所有数据读到本地缓存，然后再发送给服务器，这样上传大文件时就会导致内存溢出。\n\t *\n\t * @param blockSize 块大小（bytes数），0或小于0表示不设置Chuncked模式\n\t * @return this\n\t */\n\tpublic HttpConfig setBlockSize(int blockSize) {\n\t\tthis.blockSize = blockSize;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否忽略响应读取时可能的EOF异常。<br>\n\t * 在Http协议中，对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>\n\t * 如果服务端未遵循这个规范或响应没有正常结束，会报EOF异常，此选项用于是否忽略这个异常。\n\t *\n\t * @param ignoreEOFError 是否忽略响应读取时可能的EOF异常。\n\t * @return this\n\t * @since 5.7.20\n\t */\n\tpublic HttpConfig setIgnoreEOFError(boolean ignoreEOFError) {\n\t\tthis.ignoreEOFError = ignoreEOFError;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否忽略解码URL，包括URL中的Path部分和Param部分。<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果此参数为{@code true}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t *\n\t * @param decodeUrl 是否忽略解码URL\n\t * @return this\n\t */\n\tpublic HttpConfig setDecodeUrl(boolean decodeUrl) {\n\t\tthis.decodeUrl = decodeUrl;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t */\n\tpublic HttpConfig addRequestInterceptor(HttpInterceptor<HttpRequest> interceptor) {\n\t\tthis.requestInterceptors.addChain(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t */\n\tpublic HttpConfig addResponseInterceptor(HttpInterceptor<HttpResponse> interceptor) {\n\t\tthis.responseInterceptors.addChain(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重定向时是否使用拦截器\n\t *\n\t * @param interceptorOnRedirect 重定向时是否使用拦截器\n\t * @return this\n\t */\n\tpublic HttpConfig setInterceptorOnRedirect(boolean interceptorOnRedirect) {\n\t\tthis.interceptorOnRedirect = interceptorOnRedirect;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 自动重定向时是否处理cookie\n\t *\n\t * @param followRedirectsCookie 自动重定向时是否处理cookie\n\t * @return this\n\t * @since 5.8.15\n\t */\n\tpublic HttpConfig setFollowRedirectsCookie(boolean followRedirectsCookie) {\n\t\tthis.followRedirectsCookie = followRedirectsCookie;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否使用默认Content-Type，如果请求中未设置Content-Type，是否使用默认值\n\t *\n\t * @param useDefaultContentTypeIfNull 是否使用默认Content-Type\n\t * @return this\n\t * @since 5.8.33\n\t */\n\tpublic HttpConfig setUseDefaultContentTypeIfNull(boolean useDefaultContentTypeIfNull) {\n\t\tthis.useDefaultContentTypeIfNull = useDefaultContentTypeIfNull;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否忽略Content-Length，如果为true，则忽略Content-Length，自动根据响应内容计算Content-Length\n\t * @param ignoreContentLength 是否忽略Content-Length\n\t * @return this\n\t * @since 5.8.39\n\t */\n\tpublic HttpConfig setIgnoreContentLength(boolean ignoreContentLength) {\n\t\tthis.ignoreContentLength = ignoreContentLength;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpConnection.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.http.ssl.DefaultSSLInfo;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.HttpsURLConnection;\nimport javax.net.ssl.SSLSocketFactory;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.ProtocolException;\nimport java.net.Proxy;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.nio.charset.Charset;\nimport java.nio.charset.UnsupportedCharsetException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * http连接对象，对HttpURLConnection的包装\n *\n * @author Looly\n */\npublic class HttpConnection {\n\n\tprivate final URL url;\n\tprivate final Proxy proxy;\n\tprivate HttpURLConnection conn;\n\n\t/**\n\t * 创建HttpConnection\n\t *\n\t * @param urlStr URL\n\t * @param proxy  代理，无代理传{@code null}\n\t * @return HttpConnection\n\t */\n\tpublic static HttpConnection create(String urlStr, Proxy proxy) {\n\t\treturn create(URLUtil.toUrlForHttp(urlStr), proxy);\n\t}\n\n\t/**\n\t * 创建HttpConnection\n\t *\n\t * @param url   URL\n\t * @param proxy 代理，无代理传{@code null}\n\t * @return HttpConnection\n\t */\n\tpublic static HttpConnection create(URL url, Proxy proxy) {\n\t\treturn new HttpConnection(url, proxy);\n\t}\n\n\t// --------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造HttpConnection\n\t *\n\t * @param url   URL\n\t * @param proxy 代理\n\t */\n\tpublic HttpConnection(URL url, Proxy proxy) {\n\t\tthis.url = url;\n\t\tthis.proxy = proxy;\n\n\t\t// 初始化Http连接\n\t\tinitConn();\n\t}\n\n\t// --------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化连接相关信息\n\t *\n\t * @return HttpConnection\n\t * @since 4.4.1\n\t */\n\tpublic HttpConnection initConn() {\n\t\ttry {\n\t\t\tthis.conn = openHttp();\n\t\t} catch (IOException e) {\n\t\t\tthrow new HttpException(e);\n\t\t}\n\n\t\t// 默认读取响应内容\n\t\tthis.conn.setDoInput(true);\n\n\t\treturn this;\n\t}\n\n\t// --------------------------------------------------------------- Getters And Setters start\n\n\t/**\n\t * 获取请求方法,GET/POST\n\t *\n\t * @return 请求方法, GET/POST\n\t */\n\tpublic Method getMethod() {\n\t\treturn Method.valueOf(this.conn.getRequestMethod());\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param method 请求方法\n\t * @return 自己\n\t */\n\tpublic HttpConnection setMethod(Method method) {\n\t\tif (Method.POST.equals(method) //\n\t\t\t\t|| Method.PUT.equals(method)//\n\t\t\t\t|| Method.PATCH.equals(method)//\n\t\t\t\t|| Method.DELETE.equals(method)) {\n\t\t\tthis.conn.setUseCaches(false);\n\n\t\t\t// 增加PATCH方法支持\n\t\t\tif (Method.PATCH.equals(method)) {\n\t\t\t\ttry {\n\t\t\t\t\tHttpGlobalConfig.allowPatch();\n\t\t\t\t} catch (Exception ignore){\n\t\t\t\t\t// ignore\n\t\t\t\t\t// https://github.com/chinabugotech/hutool/issues/2832\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// method\n\t\ttry {\n\t\t\tthis.conn.setRequestMethod(method.toString());\n\t\t} catch (ProtocolException e) {\n\t\t\tif(Method.PATCH.equals(method)){\n\t\t\t\t// 如果全局设置失效，此处针对单独链接重新设置\n\t\t\t\treflectSetMethod(method);\n\t\t\t}else{\n\t\t\t\tthrow new HttpException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取请求URL\n\t *\n\t * @return 请求URL\n\t */\n\tpublic URL getUrl() {\n\t\treturn url;\n\t}\n\n\t/**\n\t * 获得代理\n\t *\n\t * @return {@link Proxy}\n\t */\n\tpublic Proxy getProxy() {\n\t\treturn proxy;\n\t}\n\n\t/**\n\t * 获取HttpURLConnection对象\n\t *\n\t * @return HttpURLConnection\n\t */\n\tpublic HttpURLConnection getHttpURLConnection() {\n\t\treturn conn;\n\t}\n\n\t// --------------------------------------------------------------- Getters And Setters end\n\n\t// ---------------------------------------------------------------- Headers start\n\n\t/**\n\t * 设置请求头<br>\n\t * 当请求头存在时，覆盖之\n\t *\n\t * @param header     头名\n\t * @param value      头值\n\t * @param isOverride 是否覆盖旧值\n\t * @return HttpConnection\n\t */\n\tpublic HttpConnection header(String header, String value, boolean isOverride) {\n\t\tif (null != this.conn) {\n\t\t\tif (isOverride) {\n\t\t\t\tthis.conn.setRequestProperty(header, value);\n\t\t\t} else {\n\t\t\t\tthis.conn.addRequestProperty(header, value);\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置请求头<br>\n\t * 当请求头存在时，覆盖之\n\t *\n\t * @param header     头名\n\t * @param value      头值\n\t * @param isOverride 是否覆盖旧值\n\t * @return HttpConnection\n\t */\n\tpublic HttpConnection header(Header header, String value, boolean isOverride) {\n\t\treturn header(header.toString(), value, isOverride);\n\t}\n\n\t/**\n\t * 设置请求头<br>\n\t * 不覆盖原有请求头\n\t *\n\t * @param headerMap  请求头\n\t * @param isOverride 是否覆盖\n\t * @return this\n\t */\n\tpublic HttpConnection header(Map<String, List<String>> headerMap, boolean isOverride) {\n\t\tif (MapUtil.isNotEmpty(headerMap)) {\n\t\t\tString name;\n\t\t\tfor (Entry<String, List<String>> entry : headerMap.entrySet()) {\n\t\t\t\tname = entry.getKey();\n\t\t\t\tfor (String value : entry.getValue()) {\n\t\t\t\t\tthis.header(name, StrUtil.nullToEmpty(value), isOverride);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置请求头<br>\n\t * 不覆盖原有请求头并判断是否需要聚合请求头\n\t *\n\t * @param headerMap 请求头\n\t * @param isOverride 是否覆盖\n\t * @param isHeaderAggregated 是否聚合\n\t * @return this\n\t * @since 5.8.37\n\t */\n\tpublic HttpConnection header(Map<String, List<String>> headerMap, boolean isOverride, boolean isHeaderAggregated) {\n\t\tif (!isHeaderAggregated){\n\t\t\treturn header(headerMap,isOverride);\n\t\t}\n\t\tif (MapUtil.isNotEmpty(headerMap)) {\n\t\t\tString name;\n\t\t\tfor (Entry<String, List<String>> entry : headerMap.entrySet()) {\n\t\t\t\tname = entry.getKey();\n\t\t\t\tList<String> values = entry.getValue();\n\t\t\t\tString headValues = StrUtil.join(\",\", values);\n\t\t\t\tthis.header(name, StrUtil.nullToEmpty(headValues), true);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取Http请求头\n\t *\n\t * @param name Header名\n\t * @return Http请求头值\n\t */\n\tpublic String header(String name) {\n\t\treturn this.conn.getHeaderField(name);\n\t}\n\n\t/**\n\t * 获取Http请求头\n\t *\n\t * @param name Header名\n\t * @return Http请求头值\n\t */\n\tpublic String header(Header name) {\n\t\treturn header(name.toString());\n\t}\n\n\t/**\n\t * 获取所有Http请求头\n\t *\n\t * @return Http请求头Map\n\t */\n\tpublic Map<String, List<String>> headers() {\n\t\treturn this.conn.getHeaderFields();\n\t}\n\n\t// ---------------------------------------------------------------- Headers end\n\n\t/**\n\t * 设置https请求参数<br>\n\t * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现，此为sun内部api，按照普通http请求处理\n\t *\n\t * @param hostnameVerifier 域名验证器，非https传入null\n\t * @param ssf              SSLSocketFactory，非https传入null\n\t * @return this\n\t * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装\n\t */\n\tpublic HttpConnection setHttpsInfo(HostnameVerifier hostnameVerifier, SSLSocketFactory ssf) throws HttpException {\n\t\tfinal HttpURLConnection conn = this.conn;\n\n\t\tif (conn instanceof HttpsURLConnection) {\n\t\t\t// Https请求\n\t\t\tfinal HttpsURLConnection httpsConn = (HttpsURLConnection) conn;\n\t\t\t// 验证域\n\t\t\thttpsConn.setHostnameVerifier(ObjectUtil.defaultIfNull(hostnameVerifier,\n\t\t\t\t// CVE-2022-22885 https://github.com/chinabugotech/hutool/issues/2042\n\t\t\t\t// 增加全局变量可选是否不验证host\n\t\t\t\tHttpGlobalConfig.isTrustAnyHost() ? DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER : HttpsURLConnection.getDefaultHostnameVerifier()));\n\t\t\thttpsConn.setSSLSocketFactory(ObjectUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭缓存\n\t *\n\t * @return this\n\t * @see HttpURLConnection#setUseCaches(boolean)\n\t */\n\tpublic HttpConnection disableCache() {\n\t\tthis.conn.setUseCaches(false);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时\n\t *\n\t * @param timeout 超时\n\t * @return this\n\t */\n\tpublic HttpConnection setConnectTimeout(int timeout) {\n\t\tif (timeout > 0 && null != this.conn) {\n\t\t\tthis.conn.setConnectTimeout(timeout);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置读取超时\n\t *\n\t * @param timeout 超时\n\t * @return this\n\t */\n\tpublic HttpConnection setReadTimeout(int timeout) {\n\t\tif (timeout > 0 && null != this.conn) {\n\t\t\tthis.conn.setReadTimeout(timeout);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接和读取的超时时间\n\t *\n\t * @param timeout 超时时间\n\t * @return this\n\t */\n\tpublic HttpConnection setConnectionAndReadTimeout(int timeout) {\n\t\tsetConnectTimeout(timeout);\n\t\tsetReadTimeout(timeout);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Cookie\n\t *\n\t * @param cookie Cookie\n\t * @return this\n\t */\n\tpublic HttpConnection setCookie(String cookie) {\n\t\tif (cookie != null) {\n\t\t\theader(Header.COOKIE, cookie, true);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置固定长度的流模式，会设置HTTP请求头中的Content-Length字段，告知服务器整个请求体的精确字节大小。<br>\n\t * 这在上传文件或大数据量时非常有用，因为它允许服务器准确地知道何时接收完所有的请求数据，而不需要依赖于连接的关闭来判断数据传输的结束。\n\t *\n\t * @param contentLength 固定长度\n\t * @return this\n\t */\n\tpublic HttpConnection setFixedLengthStreamingMode(long contentLength){\n\t\tif(contentLength > 0){\n\t\t\tconn.setFixedLengthStreamingMode(contentLength);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 采用流方式上传数据，无需本地缓存数据。<br>\n\t * HttpUrlConnection默认是将所有数据读到本地缓存，然后再发送给服务器，这样上传大文件时就会导致内存溢出。\n\t *\n\t * @param blockSize 块大小（bytes数），0或小于0表示不设置Chuncked模式\n\t * @return this\n\t */\n\tpublic HttpConnection setChunkedStreamingMode(int blockSize) {\n\t\tif (blockSize > 0) {\n\t\t\tconn.setChunkedStreamingMode(blockSize);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置自动HTTP 30X跳转\n\t *\n\t * @param isInstanceFollowRedirects 是否自定跳转\n\t * @return this\n\t */\n\tpublic HttpConnection setInstanceFollowRedirects(boolean isInstanceFollowRedirects) {\n\t\tconn.setInstanceFollowRedirects(isInstanceFollowRedirects);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 连接\n\t *\n\t * @return this\n\t * @throws IOException IO异常\n\t */\n\tpublic HttpConnection connect() throws IOException {\n\t\tif (null != this.conn) {\n\t\t\tthis.conn.connect();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 静默断开连接。不抛出异常\n\t *\n\t * @return this\n\t * @since 4.6.0\n\t */\n\tpublic HttpConnection disconnectQuietly() {\n\t\ttry {\n\t\t\tdisconnect();\n\t\t} catch (Throwable e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 断开连接\n\t *\n\t * @return this\n\t */\n\tpublic HttpConnection disconnect() {\n\t\tif (null != this.conn) {\n\t\t\tthis.conn.disconnect();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得输入流对象<br>\n\t * 输入流对象用于读取数据\n\t *\n\t * @return 输入流对象\n\t * @throws IOException IO异常\n\t */\n\tpublic InputStream getInputStream() throws IOException {\n\t\tif (null != this.conn) {\n\t\t\treturn this.conn.getInputStream();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 当返回错误代码时，获得错误内容流\n\t *\n\t * @return 错误内容\n\t */\n\tpublic InputStream getErrorStream() {\n\t\tif (null != this.conn) {\n\t\t\treturn this.conn.getErrorStream();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取输出流对象 输出流对象用于发送数据\n\t *\n\t * @return OutputStream\n\t * @throws IOException IO异常\n\t */\n\tpublic OutputStream getOutputStream() throws IOException {\n\t\tif (null == this.conn) {\n\t\t\tthrow new IOException(\"HttpURLConnection has not been initialized.\");\n\t\t}\n\n\t\tfinal Method method = getMethod();\n\n\t\t// 当有写出需求时，自动打开之\n\t\tthis.conn.setDoOutput(true);\n\t\tfinal OutputStream out = this.conn.getOutputStream();\n\n\t\t// 解决在Rest请求中，GET请求附带body导致GET请求被强制转换为POST\n\t\t// 在sun.net.www.protocol.http.HttpURLConnection.getOutputStream0方法中，会把GET方法\n\t\t// 修改为POST，而且无法调用setRequestMethod方法修改，因此此处使用反射强制修改字段属性值\n\t\t// https://stackoverflow.com/questions/978061/http-get-with-request-body/983458\n\t\tif(method == Method.GET && method != getMethod()){\n\t\t\treflectSetMethod(method);\n\t\t}\n\n\t\treturn out;\n\t}\n\n\t/**\n\t * 获取响应码\n\t *\n\t * @return 响应码\n\t * @throws IOException IO异常\n\t */\n\tpublic int responseCode() throws IOException {\n\t\tif (null != this.conn) {\n\t\t\treturn this.conn.getResponseCode();\n\t\t}\n\t\treturn 0;\n\t}\n\n\t/**\n\t * 获得字符集编码<br>\n\t * 从Http连接的头信息中获得字符集<br>\n\t * 从ContentType中获取\n\t *\n\t * @return 字符集编码\n\t */\n\tpublic String getCharsetName() {\n\t\treturn HttpUtil.getCharset(conn);\n\t}\n\n\t/**\n\t * 获取字符集编码<br>\n\t * 从Http连接的头信息中获得字符集<br>\n\t * 从ContentType中获取\n\t *\n\t * @return {@link Charset}编码\n\t * @since 3.0.9\n\t */\n\tpublic Charset getCharset() {\n\t\tCharset charset = null;\n\t\tfinal String charsetName = getCharsetName();\n\t\tif (StrUtil.isNotBlank(charsetName)) {\n\t\t\ttry {\n\t\t\t\tcharset = Charset.forName(charsetName);\n\t\t\t} catch (UnsupportedCharsetException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\treturn charset;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = StrUtil.builder();\n\t\tsb.append(\"Request URL: \").append(this.url).append(StrUtil.CRLF);\n\t\tsb.append(\"Request Method: \").append(this.getMethod()).append(StrUtil.CRLF);\n\t\t// sb.append(\"Request Headers: \").append(StrUtil.CRLF);\n\t\t// for (Entry<String, List<String>> entry : this.conn.getHeaderFields().entrySet()) {\n\t\t// sb.append(\" \").append(entry).append(StrUtil.CRLF);\n\t\t// }\n\n\t\treturn sb.toString();\n\t}\n\n\t// --------------------------------------------------------------- Private Method start\n\n\t/**\n\t * 初始化http或https请求参数<br>\n\t * 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现，此为sun内部api，按照普通http请求处理\n\t *\n\t * @return {@link HttpURLConnection}，https返回{@link HttpsURLConnection}\n\t */\n\tprivate HttpURLConnection openHttp() throws IOException {\n\t\tfinal URLConnection conn = openConnection();\n\t\tif (false == conn instanceof HttpURLConnection) {\n\t\t\t// 防止其它协议造成的转换异常\n\t\t\tthrow new HttpException(\"'{}' of URL [{}] is not a http connection, make sure URL is format for http.\", conn.getClass().getName(), this.url);\n\t\t}\n\n\t\treturn (HttpURLConnection) conn;\n\t}\n\n\t/**\n\t * 建立连接\n\t *\n\t * @return {@link URLConnection}\n\t * @throws IOException IO异常\n\t */\n\tprivate URLConnection openConnection() throws IOException {\n\t\treturn (null == this.proxy) ? url.openConnection() : url.openConnection(this.proxy);\n\t}\n\n\t/**\n\t * 通过反射设置方法名，首先设置HttpURLConnection本身的方法名，再检查是否为代理类，如果是，设置代理对象的方法名。\n\t * @param method 方法名\n\t */\n\tprivate void reflectSetMethod(Method method){\n\t\ttry {\n\t\t\tReflectUtil.setFieldValue(this.conn, \"method\", method.name());\n\n\t\t\t// HttpsURLConnectionImpl实现中，使用了代理类，需要修改被代理类的method方法\n\t\t\tfinal Object delegate = ReflectUtil.getFieldValue(this.conn, \"delegate\");\n\t\t\tif(null != delegate){\n\t\t\t\tReflectUtil.setFieldValue(delegate, \"method\", method.name());\n\t\t\t}\n\t\t} catch (Exception e){\n\t\t\t// ignore\n\t\t\t// https://github.com/chinabugotech/hutool/issues/4109\n\t\t}\n\t}\n\t// --------------------------------------------------------------- Private Method end\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpDownloader.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.File;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * 下载封装，下载统一使用{@code GET}请求，默认支持30x跳转\n *\n * @author looly\n * @since 5.6.4\n */\npublic class HttpDownloader {\n\n\t/**\n\t * 下载远程文本\n\t *\n\t * @param url           请求的url\n\t * @param customCharset 自定义的字符集，可以使用{@code CharsetUtil#charset} 方法转换\n\t * @param streamPress   进度条 {@link StreamProgress}\n\t * @return 文本\n\t */\n\tpublic static String downloadString(String url, Charset customCharset, StreamProgress streamPress) {\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream();\n\t\tdownload(url, out, true, streamPress);\n\t\treturn null == customCharset ? out.toString() : out.toString(customCharset);\n\t}\n\n\t/**\n\t * 下载远程文件数据，支持30x跳转\n\t *\n\t * @param url 请求的url\n\t * @return 文件数据\n\t */\n\tpublic static byte[] downloadBytes(String url) {\n\t\treturn downloadBytes(url, 0);\n\t}\n\n\t/**\n\t * 下载远程文件数据，支持30x跳转\n\t *\n\t * @param url     请求的url\n\t * @param timeout 超时毫秒数\n\t * @return 文件数据\n\t * @since 5.8.28\n\t */\n\tpublic static byte[] downloadBytes(String url, int timeout) {\n\t\treturn requestDownload(url, timeout).bodyBytes();\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url             请求的url\n\t * @param targetFileOrDir 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout         超时，单位毫秒，-1表示默认超时\n\t * @param streamProgress  进度条\n\t * @return 文件大小\n\t */\n\tpublic static long downloadFile(String url, File targetFileOrDir, int timeout, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] is null !\");\n\n\t\treturn requestDownload(url, timeout).writeBody(targetFileOrDir, streamProgress);\n\t}\n\n\t/**\n\t * 下载文件-避免未完成的文件<br>\n\t * 来自：https://gitee.com/chinabugotech/hutool/pulls/407<br>\n\t * 此方法原理是先在目标文件同级目录下创建临时文件，下载之，等下载完毕后重命名，避免因下载错误导致的文件不完整。\n\t *\n\t * @param url             请求的url\n\t * @param targetFileOrDir 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param tempFileSuffix  临时文件后缀，默认\".temp\"\n\t * @param timeout         超时，单位毫秒，-1表示默认超时\n\t * @param streamProgress  进度条\n\t * @return 下载大小\n\t * @since 5.7.12\n\t */\n\tpublic static long downloadFile(String url, File targetFileOrDir, String tempFileSuffix, int timeout, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] is null !\");\n\n\t\treturn requestDownload(url, timeout).writeBody(targetFileOrDir, tempFileSuffix, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件，返回文件\n\t *\n\t * @param url             请求的url\n\t * @param targetFileOrDir 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout         超时，单位毫秒，-1表示默认超时\n\t * @param streamProgress  进度条\n\t * @return 文件\n\t */\n\tpublic static File downloadForFile(String url, File targetFileOrDir, int timeout, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] is null !\");\n\n\t\t// writeBody后会自动关闭网络流\n\t\treturn requestDownload(url, timeout).writeBodyForFile(targetFileOrDir, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param out            将下载内容写到输出流中 {@link OutputStream}\n\t * @param isCloseOut     是否关闭输出流\n\t * @param streamProgress 进度条\n\t * @return 文件大小\n\t */\n\tpublic static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {\n\t\tAssert.notNull(out, \"[out] is null !\");\n\n\t\t// writeBody后会自动关闭网络流\n\t\treturn requestDownload(url, -1).writeBody(out, isCloseOut, streamProgress);\n\t}\n\n\t/**\n\t * 请求下载文件\n\t *\n\t * @param url     请求下载文件地址\n\t * @param timeout 超时时间\n\t * @return HttpResponse\n\t * @since 5.4.1\n\t */\n\tprivate static HttpResponse requestDownload(String url, int timeout) {\n\t\tAssert.notBlank(url, \"[url] is blank !\");\n\n\t\tfinal HttpRequest request = HttpUtil.createGet(url, true);\n\t\tif (timeout > 0) {\n\t\t\t// 只有用户自定义了超时时长才有效，否则使用全局默认的超时时长。\n\t\t\trequest.timeout(timeout);\n\t\t}\n\n\n\t\tfinal HttpResponse response = request.executeAsync();\n\t\tif (response.isOk()) {\n\t\t\treturn response;\n\t\t}\n\n\t\tthrow new HttpException(\"Server response error with status code: [{}]\", response.getStatus());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpException.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * HTTP异常\n *\n * @author xiaoleilu\n */\npublic class HttpException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic HttpException(Throwable e) {\n\t\tsuper(e.getMessage(), e);\n\t}\n\n\tpublic HttpException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic HttpException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic HttpException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic HttpException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic HttpException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.RandomUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.http.cookie.GlobalCookieManager;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.net.CookieManager;\nimport java.net.HttpURLConnection;\n\n/**\n * HTTP 全局参数配置\n *\n * @author Looly\n * @since 4.6.2\n */\npublic class HttpGlobalConfig implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * -1: 含义，永不超时。\n\t * 如果：设置timeout = 3s(3000 ms), 那一次请求最大超时：就是：6s\n\t * 官方含义：timeout of zero is interpreted as an infinite timeout. （0的超时被解释为无限超时。）\n\t * 这里实际项目一定要进行修改，防止把系统拖死.\n\t * 底层调用：{@link HttpURLConnection#setReadTimeout(int)} 同时设置: 读取超时\n\t * 底层调用：{@link HttpURLConnection#setConnectTimeout(int)} 同时设置: 连接超时\n\t */\n\tprivate static int timeout = -1;\n\tprivate static boolean isAllowPatch = false;\n\tprivate static String boundary = \"--------------------Hutool_\" + RandomUtil.randomString(16);\n\tprivate static int maxRedirectCount = 0;\n\tprivate static boolean ignoreEOFError = true;\n\tprivate static boolean decodeUrl = false;\n\tprivate static boolean trustAnyHost = true;\n\n\t/**\n\t * 获取全局默认的超时时长\n\t *\n\t * @return 全局默认的超时时长\n\t */\n\tpublic static int getTimeout() {\n\t\treturn timeout;\n\t}\n\n\t/**\n\t * 设置默认的连接和读取超时时长<br>\n\t * -1: 含义，永不超时。<br>\n\t * 如果：设置timeout = 3s(3000 ms), 那一次请求最大超时：就是：6s<br>\n\t * 官方含义：timeout of zero is interpreted as an infinite timeout. （0的超时被解释为无限超时。）<br>\n\t * 这里实际项目一定要进行修改，防止把系统拖死.<br>\n\t * 底层调用：{@link HttpURLConnection#setReadTimeout(int)} 同时设置: 读取超时<br>\n\t * 底层调用：{@link HttpURLConnection#setConnectTimeout(int)} 同时设置: 连接超时\n\t *\n\t * @param customTimeout 超时时长\n\t */\n\tsynchronized public static void setTimeout(int customTimeout) {\n\t\ttimeout = customTimeout;\n\t}\n\n\t/**\n\t * 获取全局默认的Multipart边界\n\t *\n\t * @return 全局默认的Multipart边界\n\t * @since 5.7.17\n\t */\n\tpublic static String getBoundary() {\n\t\treturn boundary;\n\t}\n\n\t/**\n\t * 设置默认的Multipart边界\n\t *\n\t * @param customBoundary 自定义Multipart边界\n\t * @since 5.7.17\n\t */\n\tsynchronized public static void setBoundary(String customBoundary) {\n\t\tboundary = customBoundary;\n\t}\n\n\t/**\n\t * 获取全局默认的最大重定向次数，如设置0表示不重定向<br>\n\t * 如果设置为1，表示重定向一次，即请求两次\n\t *\n\t * @return 全局默认的最大重定向次数\n\t * @since 5.7.19\n\t */\n\tpublic static int getMaxRedirectCount() {\n\t\treturn maxRedirectCount;\n\t}\n\n\t/**\n\t * 设置默认全局默认的最大重定向次数，如设置0表示不重定向<br>\n\t * 如果设置为1，表示重定向一次，即请求两次\n\t *\n\t * @param customMaxRedirectCount 全局默认的最大重定向次数\n\t * @since 5.7.19\n\t */\n\tsynchronized public static void setMaxRedirectCount(int customMaxRedirectCount) {\n\t\tmaxRedirectCount = customMaxRedirectCount;\n\t}\n\n\t/**\n\t * 获取是否忽略响应读取时可能的EOF异常。<br>\n\t * 在Http协议中，对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>\n\t * 如果服务端未遵循这个规范或响应没有正常结束，会报EOF异常，此选项用于是否忽略这个异常。\n\t *\n\t * @return 是否忽略响应读取时可能的EOF异常\n\t * @since 5.7.20\n\t */\n\tpublic static boolean isIgnoreEOFError() {\n\t\treturn ignoreEOFError;\n\t}\n\n\t/**\n\t * 设置是否忽略响应读取时可能的EOF异常。<br>\n\t * 在Http协议中，对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>\n\t * 如果服务端未遵循这个规范或响应没有正常结束，会报EOF异常，此选项用于是否忽略这个异常。\n\t *\n\t * @param customIgnoreEOFError 是否忽略响应读取时可能的EOF异常。\n\t * @since 5.7.20\n\t */\n\tsynchronized public static void setIgnoreEOFError(boolean customIgnoreEOFError) {\n\t\tignoreEOFError = customIgnoreEOFError;\n\t}\n\n\t/**\n\t * 获取是否忽略解码URL，包括URL中的Path部分和Param部分。<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果此参数为{@code true}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t *\n\t * @return 是否忽略解码URL\n\t * @since 5.7.22\n\t */\n\tpublic static boolean isDecodeUrl() {\n\t\treturn decodeUrl;\n\t}\n\n\t/**\n\t * 设置是否忽略解码URL，包括URL中的Path部分和Param部分。<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果此参数为{@code true}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t *\n\t * @param customDecodeUrl 是否忽略解码URL\n\t * @since 5.7.22\n\t */\n\tsynchronized public static void setDecodeUrl(boolean customDecodeUrl) {\n\t\tdecodeUrl = customDecodeUrl;\n\t}\n\n\t/**\n\t * 获取Cookie管理器，用于自定义Cookie管理\n\t *\n\t * @return {@link CookieManager}\n\t * @see GlobalCookieManager#getCookieManager()\n\t * @since 4.1.0\n\t */\n\tpublic static CookieManager getCookieManager() {\n\t\treturn GlobalCookieManager.getCookieManager();\n\t}\n\n\t/**\n\t * 自定义{@link CookieManager}\n\t *\n\t * @param customCookieManager 自定义的{@link CookieManager}\n\t * @see GlobalCookieManager#setCookieManager(CookieManager)\n\t * @since 4.5.14\n\t */\n\tsynchronized public static void setCookieManager(CookieManager customCookieManager) {\n\t\tGlobalCookieManager.setCookieManager(customCookieManager);\n\t}\n\n\t/**\n\t * 关闭Cookie\n\t *\n\t * @see GlobalCookieManager#setCookieManager(CookieManager)\n\t * @since 4.1.9\n\t */\n\tsynchronized public static void closeCookie() {\n\t\tGlobalCookieManager.setCookieManager(null);\n\t}\n\n\t/**\n\t * 增加支持的METHOD方法<br>\n\t * 此方法通过注入方式修改{@link HttpURLConnection}中的methods静态属性，增加PATCH方法<br>\n\t * see: <a href=\"https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch\">https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch</a>\n\t *\n\t * @since 5.7.4\n\t */\n\tsynchronized public static void allowPatch() {\n\t\tif (isAllowPatch) {\n\t\t\treturn;\n\t\t}\n\t\tfinal Field methodsField = ReflectUtil.getField(HttpURLConnection.class, \"methods\");\n\t\tif (null == methodsField) {\n\t\t\tthrow new HttpException(\"None static field [methods] with Java version: [{}]\", System.getProperty(\"java.version\"));\n\t\t}\n\n\t\t// 去除final修饰\n\t\tReflectUtil.removeFinalModify(methodsField);\n\t\tfinal String[] methods = {\n\t\t\t\"GET\", \"POST\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\", \"TRACE\", \"PATCH\"\n\t\t};\n\t\tReflectUtil.setFieldValue(null, methodsField, methods);\n\n\t\t// 检查注入是否成功\n\t\tfinal Object staticFieldValue = ReflectUtil.getStaticFieldValue(methodsField);\n\t\tif (false == ArrayUtil.equals(methods, staticFieldValue)) {\n\t\t\tthrow new HttpException(\"Inject value to field [methods] failed!\");\n\t\t}\n\n\t\tisAllowPatch = true;\n\t}\n\n\t/**\n\t * 是否信任所有Host\n\t * @return 是否信任所有Host\n\t * @since 5.8.27\n\t */\n\tpublic static boolean isTrustAnyHost(){\n\t\treturn trustAnyHost;\n\t}\n\n\t/**\n\t * 是否信任所有Host<br>\n\t * 见：https://github.com/chinabugotech/hutool/issues/2042<br>\n\t *\n\t * @param customTrustAnyHost 如果设置为{@code false}，则按照JDK默认验证机制，验证目标服务器的证书host和请求host是否一致，{@code true}表示不验证。\n\t * @since 5.8.27\n\t */\n\tpublic static void setTrustAnyHost(boolean customTrustAnyHost) {\n\t\ttrustAnyHost = customTrustAnyHost;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpInputStream.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.zip.GZIPInputStream;\nimport java.util.zip.Inflater;\nimport java.util.zip.InflaterInputStream;\n\n/**\n * HTTP输入流，此流用于包装Http请求响应内容的流，用于解析各种压缩、分段的响应流内容\n *\n * @author Looly\n *\n */\npublic class HttpInputStream extends InputStream {\n\n\t/** 原始流 */\n\tprivate InputStream in;\n\n\t/**\n\t * 构造\n\t *\n\t * @param response 响应对象\n\t */\n\tpublic HttpInputStream(HttpResponse response) {\n\t\tinit(response);\n\t}\n\n\t@Override\n\tpublic int read() throws IOException {\n\t\treturn this.in.read();\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic int read(byte[] b, int off, int len) throws IOException {\n\t\treturn this.in.read(b, off, len);\n\t}\n\n\t@Override\n\tpublic long skip(long n) throws IOException {\n\t\treturn this.in.skip(n);\n\t}\n\n\t@Override\n\tpublic int available() throws IOException {\n\t\treturn this.in.available();\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tthis.in.close();\n\t}\n\n\t@Override\n\tpublic synchronized void mark(int readlimit) {\n\t\tthis.in.mark(readlimit);\n\t}\n\n\t@Override\n\tpublic synchronized void reset() throws IOException {\n\t\tthis.in.reset();\n\t}\n\n\t@Override\n\tpublic boolean markSupported() {\n\t\treturn this.in.markSupported();\n\t}\n\n\t/**\n\t * 初始化流\n\t *\n\t * @param response 响应对象\n\t */\n\tprivate void init(HttpResponse response) {\n\t\ttry {\n\t\t\tthis.in = (response.status < HttpStatus.HTTP_BAD_REQUEST) ? response.httpConnection.getInputStream() : response.httpConnection.getErrorStream();\n\t\t} catch (IOException e) {\n\t\t\tif (false == (e instanceof FileNotFoundException)) {\n\t\t\t\tthrow new HttpException(e);\n\t\t\t}\n\t\t\t// 服务器无返回内容，忽略之\n\t\t}\n\n\t\t// 在一些情况下，返回的流为null，此时提供状态码说明\n\t\tif (null == this.in) {\n\t\t\tthis.in = new ByteArrayInputStream(StrUtil.format(\"Error request, response status: {}\", response.status).getBytes());\n\t\t\treturn;\n\t\t}\n\n\t\tif (response.isGzip() && false == (response.in instanceof GZIPInputStream)) {\n\t\t\t// Accept-Encoding: gzip\n\t\t\ttry {\n\t\t\t\tthis.in = new GZIPInputStream(this.in);\n\t\t\t} catch (IOException e) {\n\t\t\t\t// 在类似于Head等方法中无body返回，此时GZIPInputStream构造会出现错误，在此忽略此错误读取普通数据\n\t\t\t\t// ignore\n\t\t\t}\n\t\t} else if (response.isDeflate() && false == (this.in instanceof InflaterInputStream)) {\n\t\t\t// Accept-Encoding: defalte\n\t\t\tthis.in = new InflaterInputStream(this.in, new Inflater(true));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpInterceptor.java",
    "content": "package cn.hutool.http;\n\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * Http拦截器接口，通过实现此接口，完成请求发起前或结束后对请求的编辑工作\n *\n * @param <T> 过滤参数类型，HttpRequest或者HttpResponse\n * @author looly\n * @since 5.7.16\n */\n@FunctionalInterface\npublic interface HttpInterceptor<T extends HttpBase<T>> {\n\n\t/**\n\t * 处理请求\n\t *\n\t * @param httpObj 请求或响应对象\n\t */\n\tvoid process(T httpObj);\n\n\t/**\n\t * 拦截器链\n\t *\n\t * @param <T> 过滤参数类型，HttpRequest或者HttpResponse\n\t * @author looly\n\t * @since 5.7.16\n\t */\n\tclass Chain<T extends HttpBase<T>> implements cn.hutool.core.lang.Chain<HttpInterceptor<T>, Chain<T>> {\n\t\tprivate final List<HttpInterceptor<T>> interceptors = new LinkedList<>();\n\n\t\t@Override\n\t\tpublic Chain<T> addChain(HttpInterceptor<T> element) {\n\t\t\tinterceptors.add(element);\n\t\t\treturn this;\n\t\t}\n\n\t\t@Override\n\t\tpublic Iterator<HttpInterceptor<T>> iterator() {\n\t\t\treturn interceptors.iterator();\n\t\t}\n\n\t\t/**\n\t\t * 清空\n\t\t *\n\t\t * @return this\n\t\t * @since 5.8.0\n\t\t */\n\t\tpublic Chain<T> clear() {\n\t\t\tinterceptors.clear();\n\t\t\treturn this;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpRequest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.resource.BytesResource;\nimport cn.hutool.core.io.resource.FileResource;\nimport cn.hutool.core.io.resource.MultiFileResource;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.TableMap;\nimport cn.hutool.core.net.SSLUtil;\nimport cn.hutool.core.net.url.UrlBuilder;\nimport cn.hutool.core.net.url.UrlQuery;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.body.FormUrlEncodedBody;\nimport cn.hutool.http.body.MultipartBody;\nimport cn.hutool.http.body.RequestBody;\nimport cn.hutool.http.body.ResourceBody;\nimport cn.hutool.http.cookie.GlobalCookieManager;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLSocketFactory;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.*;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * http请求类<br>\n * Http请求类用于构建Http请求并同步获取结果，此类通过CookieManager持有域名对应的Cookie值，再次请求时会自动附带Cookie信息\n *\n * @author Looly\n */\npublic class HttpRequest extends HttpBase<HttpRequest> {\n\n\t// ---------------------------------------------------------------- static Http Method start\n\n\t/**\n\t * POST请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest post(String url) {\n\t\treturn of(url).method(Method.POST);\n\t}\n\n\t/**\n\t * GET请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest get(String url) {\n\t\treturn of(url).method(Method.GET);\n\t}\n\n\t/**\n\t * HEAD请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest head(String url) {\n\t\treturn of(url).method(Method.HEAD);\n\t}\n\n\t/**\n\t * OPTIONS请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest options(String url) {\n\t\treturn of(url).method(Method.OPTIONS);\n\t}\n\n\t/**\n\t * PUT请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest put(String url) {\n\t\treturn of(url).method(Method.PUT);\n\t}\n\n\t/**\n\t * PATCH请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t * @since 3.0.9\n\t */\n\tpublic static HttpRequest patch(String url) {\n\t\treturn of(url).method(Method.PATCH);\n\t}\n\n\t/**\n\t * DELETE请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest delete(String url) {\n\t\treturn of(url).method(Method.DELETE);\n\t}\n\n\t/**\n\t * TRACE请求\n\t *\n\t * @param url URL\n\t * @return HttpRequest\n\t */\n\tpublic static HttpRequest trace(String url) {\n\t\treturn of(url).method(Method.TRACE);\n\t}\n\n\t/**\n\t * 构建一个HTTP请求<br>\n\t * 对于传入的URL，可以自定义是否解码已经编码的内容，设置见{@link HttpGlobalConfig#setDecodeUrl(boolean)}<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果{@link HttpGlobalConfig#isDecodeUrl()}为{@code true}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t *\n\t * @param url URL链接，默认自动编码URL中的参数等信息\n\t * @return HttpRequest\n\t * @since 5.7.18\n\t */\n\tpublic static HttpRequest of(String url) {\n\t\treturn of(url, HttpGlobalConfig.isDecodeUrl() ? DEFAULT_CHARSET : null);\n\t}\n\n\t/**\n\t * 构建一个HTTP请求<br>\n\t * 对于传入的URL，可以自定义是否解码已经编码的内容。<br>\n\t * 在构建Http请求时，用户传入的URL可能有编码后和未编码的内容混合在一起，如果charset参数不为{@code null}，则会统一解码编码后的参数，<br>\n\t * 按照RFC3986规范，在发送请求时，全部编码之。如果为{@code false}，则不会解码已经编码的内容，在请求时只编码需要编码的部分。\n\t *\n\t * @param url     URL链接\n\t * @param charset 编码，如果为{@code null}不自动解码编码URL\n\t * @return HttpRequest\n\t * @since 5.7.18\n\t */\n\tpublic static HttpRequest of(String url, Charset charset) {\n\t\treturn of(UrlBuilder.ofHttp(url, charset));\n\t}\n\n\t/**\n\t * 构建一个HTTP请求<br>\n\t *\n\t * @param url {@link UrlBuilder}\n\t * @return HttpRequest\n\t * @since 5.8.0\n\t */\n\tpublic static HttpRequest of(UrlBuilder url) {\n\t\treturn new HttpRequest(url);\n\t}\n\n\t/**\n\t * 设置全局默认的连接和读取超时时长\n\t *\n\t * @param customTimeout 超时时长\n\t * @see HttpGlobalConfig#setTimeout(int)\n\t * @since 4.6.2\n\t */\n\tpublic static void setGlobalTimeout(int customTimeout) {\n\t\tHttpGlobalConfig.setTimeout(customTimeout);\n\t}\n\n\t/**\n\t * 获取Cookie管理器，用于自定义Cookie管理\n\t *\n\t * @return {@link CookieManager}\n\t * @see GlobalCookieManager#getCookieManager()\n\t * @since 4.1.0\n\t */\n\tpublic static CookieManager getCookieManager() {\n\t\treturn GlobalCookieManager.getCookieManager();\n\t}\n\n\t/**\n\t * 自定义{@link CookieManager}\n\t *\n\t * @param customCookieManager 自定义的{@link CookieManager}\n\t * @see GlobalCookieManager#setCookieManager(CookieManager)\n\t * @since 4.5.14\n\t */\n\tpublic static void setCookieManager(CookieManager customCookieManager) {\n\t\tGlobalCookieManager.setCookieManager(customCookieManager);\n\t}\n\n\t/**\n\t * 关闭Cookie\n\t *\n\t * @see GlobalCookieManager#setCookieManager(CookieManager)\n\t * @since 4.1.9\n\t */\n\tpublic static void closeCookie() {\n\t\tGlobalCookieManager.setCookieManager(null);\n\t}\n\t// ---------------------------------------------------------------- static Http Method end\n\n\tprivate HttpConfig config = HttpConfig.create();\n\tprivate UrlBuilder url;\n\tprivate URLStreamHandler urlHandler;\n\tprivate Method method = Method.GET;\n\t/**\n\t * 连接对象\n\t */\n\tprivate HttpConnection httpConnection;\n\n\t/**\n\t * 存储表单数据\n\t */\n\tprivate Map<String, Object> form;\n\t/**\n\t * Cookie\n\t */\n\tprivate String cookie;\n\t/**\n\t * 是否为Multipart表单\n\t */\n\tprivate boolean isMultiPart;\n\t/**\n\t * 是否是REST请求模式\n\t */\n\tprivate boolean isRest;\n\t/**\n\t * 重定向次数计数器，内部使用\n\t */\n\tprivate int redirectCount;\n\t/**\n\t * 固定长度，用于设置HttpURLConnection.setFixedLengthStreamingMode，默认为0，表示使用默认值，默认值由HttpURLConnection内部决定，通常为0\n\t */\n\tprivate long fixedContentLength;\n\n\t/**\n\t * 构造，URL编码默认使用UTF-8\n\t *\n\t * @param url URL\n\t * @deprecated 请使用 {@link #of(String)}\n\t */\n\t@Deprecated\n\tpublic HttpRequest(String url) {\n\t\tthis(UrlBuilder.ofHttp(url));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url {@link UrlBuilder}\n\t */\n\tpublic HttpRequest(UrlBuilder url) {\n\t\tthis.url = Assert.notNull(url, \"URL must be not null!\");\n\t\t// 给定默认URL编码\n\t\tfinal Charset charset = url.getCharset();\n\t\tif (null != charset) {\n\t\t\tthis.charset(charset);\n\t\t}\n\t\t// 给定一个默认头信息\n\t\tthis.header(GlobalHeaders.INSTANCE.headers);\n\t}\n\n\t/**\n\t * 获取请求URL\n\t *\n\t * @return URL字符串\n\t * @since 4.1.8\n\t */\n\tpublic String getUrl() {\n\t\treturn url.toString();\n\t}\n\n\t/**\n\t * 设置URL\n\t *\n\t * @param url url字符串\n\t * @return this\n\t * @since 4.1.8\n\t */\n\tpublic HttpRequest setUrl(String url) {\n\t\treturn setUrl(UrlBuilder.ofHttp(url, this.charset));\n\t}\n\n\t/**\n\t * 设置URL\n\t *\n\t * @param urlBuilder url字符串\n\t * @return this\n\t * @since 5.3.1\n\t */\n\tpublic HttpRequest setUrl(UrlBuilder urlBuilder) {\n\t\tthis.url = urlBuilder;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置{@link URLStreamHandler}\n\t * <p>\n\t * 部分环境下需要单独设置此项，例如当 WebLogic Server 实例充当 SSL 客户端角色（它会尝试通过 SSL 连接到其他服务器或应用程序）时，<br>\n\t * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配，则删除此连接。<br>\n\t * 因此weblogic不支持https的sni协议的主机名验证，此时需要将此值设置为sun.net.www.protocol.https.Handler对象。\n\t * <p>\n\t * 相关issue见：<a href=\"https://gitee.com/chinabugotech/hutool/issues/IMD1X\">https://gitee.com/chinabugotech/hutool/issues/IMD1X</a>\n\t *\n\t * @param urlHandler {@link URLStreamHandler}\n\t * @return this\n\t * @since 4.1.9\n\t */\n\tpublic HttpRequest setUrlHandler(URLStreamHandler urlHandler) {\n\t\tthis.urlHandler = urlHandler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取Http请求方法\n\t *\n\t * @return {@link Method}\n\t * @since 4.1.8\n\t */\n\tpublic Method getMethod() {\n\t\treturn this.method;\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param method HTTP方法\n\t * @return HttpRequest\n\t * @see #method(Method)\n\t * @since 4.1.8\n\t */\n\tpublic HttpRequest setMethod(Method method) {\n\t\treturn method(method);\n\t}\n\n\t/**\n\t * 获取{@link HttpConnection}<br>\n\t * 在{@link #execute()} 执行前此对象为null\n\t *\n\t * @return {@link HttpConnection}\n\t * @since 4.2.2\n\t */\n\tpublic HttpConnection getConnection() {\n\t\treturn this.httpConnection;\n\t}\n\n\t/**\n\t * 设置固定长度的流模式，会设置HTTP请求头中的Content-Length字段，告知服务器整个请求体的精确字节大小。<br>\n\t * 这在上传文件或大数据量时非常有用，因为它允许服务器准确地知道何时接收完所有的请求数据，而不需要依赖于连接的关闭来判断数据传输的结束。\n\t *\n\t * @param contentLength 固定长度\n\t * @return this\n\t * @since 5.8.33\n\t */\n\tpublic HttpRequest setFixedContentLength(long contentLength) {\n\t\tthis.fixedContentLength = contentLength;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param method HTTP方法\n\t * @return HttpRequest\n\t */\n\tpublic HttpRequest method(Method method) {\n\t\tthis.method = method;\n\t\treturn this;\n\t}\n\n\t// ---------------------------------------------------------------- Http Request Header start\n\n\t/**\n\t * 设置contentType\n\t *\n\t * @param contentType contentType\n\t * @return HttpRequest\n\t */\n\tpublic HttpRequest contentType(String contentType) {\n\t\theader(Header.CONTENT_TYPE, contentType);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否为长连接\n\t *\n\t * @param isKeepAlive 是否长连接\n\t * @return HttpRequest\n\t */\n\tpublic HttpRequest keepAlive(boolean isKeepAlive) {\n\t\theader(Header.CONNECTION, isKeepAlive ? \"Keep-Alive\" : \"Close\");\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 获取是否为长连接\n\t */\n\tpublic boolean isKeepAlive() {\n\t\tString connection = header(Header.CONNECTION);\n\t\tif (connection == null) {\n\t\t\treturn false == HTTP_1_0.equalsIgnoreCase(httpVersion);\n\t\t}\n\n\t\treturn false == \"close\".equalsIgnoreCase(connection);\n\t}\n\n\t/**\n\t * 获取内容长度\n\t *\n\t * @return String\n\t */\n\tpublic String contentLength() {\n\t\treturn header(Header.CONTENT_LENGTH);\n\t}\n\n\t/**\n\t * 设置内容长度\n\t *\n\t * @param value 长度\n\t * @return HttpRequest\n\t */\n\tpublic HttpRequest contentLength(int value) {\n\t\theader(Header.CONTENT_LENGTH, String.valueOf(value));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Cookie<br>\n\t * 自定义Cookie后会覆盖Hutool的默认Cookie行为\n\t *\n\t * @param cookies Cookie值数组，如果为{@code null}则设置无效，使用默认Cookie行为\n\t * @return this\n\t * @since 5.4.1\n\t */\n\tpublic HttpRequest cookie(Collection<HttpCookie> cookies) {\n\t\treturn cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));\n\t}\n\n\t/**\n\t * 设置Cookie<br>\n\t * 自定义Cookie后会覆盖Hutool的默认Cookie行为\n\t *\n\t * @param cookies Cookie值数组，如果为{@code null}则设置无效，使用默认Cookie行为\n\t * @return this\n\t * @since 3.1.1\n\t */\n\tpublic HttpRequest cookie(HttpCookie... cookies) {\n\t\tif (ArrayUtil.isEmpty(cookies)) {\n\t\t\treturn disableCookie();\n\t\t}\n\t\t// 名称/值对之间用分号和空格 ('; ')\n\t\t// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cookie\n\t\treturn cookie(ArrayUtil.join(cookies, \"; \"));\n\t}\n\n\t/**\n\t * 设置Cookie<br>\n\t * 自定义Cookie后会覆盖Hutool的默认Cookie行为\n\t *\n\t * @param cookie Cookie值，如果为{@code null}则设置无效，使用默认Cookie行为\n\t * @return this\n\t * @since 3.0.7\n\t */\n\tpublic HttpRequest cookie(String cookie) {\n\t\tthis.cookie = cookie;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 禁用默认Cookie行为，此方法调用后会将Cookie置为空。<br>\n\t * 如果想重新启用Cookie，请调用：{@link #cookie(String)}方法自定义Cookie。<br>\n\t * 如果想启动默认的Cookie行为（自动回填服务器传回的Cookie），则调用{@link #enableDefaultCookie()}\n\t *\n\t * @return this\n\t * @since 3.0.7\n\t */\n\tpublic HttpRequest disableCookie() {\n\t\treturn cookie(StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 打开默认的Cookie行为（自动回填服务器传回的Cookie）\n\t *\n\t * @return this\n\t */\n\tpublic HttpRequest enableDefaultCookie() {\n\t\treturn cookie((String) null);\n\t}\n\t// ---------------------------------------------------------------- Http Request Header end\n\n\t// ---------------------------------------------------------------- Form start\n\n\t/**\n\t * 设置表单数据<br>\n\t *\n\t * @param name  名\n\t * @param value 值\n\t * @return this\n\t */\n\tpublic HttpRequest form(String name, Object value) {\n\t\tif (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {\n\t\t\treturn this; // 忽略非法的form表单项内容;\n\t\t}\n\n\t\t// 停用body\n\t\tthis.body = null;\n\n\t\tif (value instanceof File) {\n\t\t\t// 文件上传\n\t\t\treturn this.form(name, (File) value);\n\t\t}\n\n\t\tif (value instanceof Resource) {\n\t\t\treturn form(name, (Resource) value);\n\t\t}\n\n\t\t// 普通值\n\t\tString strValue;\n\t\tif (value instanceof Iterable) {\n\t\t\t// 列表对象\n\t\t\tstrValue = CollUtil.join((Iterable<?>) value, \",\");\n\t\t} else if (ArrayUtil.isArray(value)) {\n\t\t\tif (File.class == ArrayUtil.getComponentType(value)) {\n\t\t\t\t// 多文件\n\t\t\t\treturn this.form(name, (File[]) value);\n\t\t\t}\n\t\t\t// 数组对象\n\t\t\tstrValue = ArrayUtil.join((Object[]) value, \",\");\n\t\t} else {\n\t\t\t// 其他对象一律转换为字符串\n\t\t\tstrValue = Convert.toStr(value, null);\n\t\t}\n\n\t\treturn putToForm(name, strValue);\n\t}\n\n\t/**\n\t * 设置表单数据\n\t *\n\t * @param name       名\n\t * @param value      值\n\t * @param parameters 参数对，奇数为名，偶数为值\n\t * @return this\n\t */\n\tpublic HttpRequest form(String name, Object value, Object... parameters) {\n\t\tform(name, value);\n\n\t\tfor (int i = 0; i < parameters.length; i += 2) {\n\t\t\tform(parameters[i].toString(), parameters[i + 1]);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置map类型表单数据\n\t *\n\t * @param formMap 表单内容\n\t * @return this\n\t */\n\tpublic HttpRequest form(Map<String, Object> formMap) {\n\t\tif (MapUtil.isNotEmpty(formMap)) {\n\t\t\tformMap.forEach(this::form);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置map&lt;String, String&gt;类型表单数据\n\t *\n\t * @param formMapStr 表单内容\n\t * @return this\n\t * @since 5.6.7\n\t */\n\tpublic HttpRequest formStr(Map<String, String> formMapStr) {\n\t\tif (MapUtil.isNotEmpty(formMapStr)) {\n\t\t\tformMapStr.forEach(this::form);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 文件表单项<br>\n\t * 一旦有文件加入，表单变为multipart/form-data\n\t *\n\t * @param name  名\n\t * @param files 需要上传的文件，为空跳过\n\t * @return this\n\t */\n\tpublic HttpRequest form(String name, File... files) {\n\t\tif (ArrayUtil.isEmpty(files)) {\n\t\t\treturn this;\n\t\t}\n\t\tif (1 == files.length) {\n\t\t\tfinal File file = files[0];\n\t\t\treturn form(name, file, file.getName());\n\t\t}\n\t\treturn form(name, new MultiFileResource(files));\n\t}\n\n\t/**\n\t * 文件表单项<br>\n\t * 一旦有文件加入，表单变为multipart/form-data\n\t *\n\t * @param name 名\n\t * @param file 需要上传的文件\n\t * @return this\n\t */\n\tpublic HttpRequest form(String name, File file) {\n\t\treturn form(name, file, file.getName());\n\t}\n\n\t/**\n\t * 文件表单项<br>\n\t * 一旦有文件加入，表单变为multipart/form-data\n\t *\n\t * @param name     名\n\t * @param file     需要上传的文件\n\t * @param fileName 文件名，为空使用文件默认的文件名\n\t * @return this\n\t */\n\tpublic HttpRequest form(String name, File file, String fileName) {\n\t\tif (null != file) {\n\t\t\tform(name, new FileResource(file, fileName));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 文件byte[]表单项<br>\n\t * 一旦有文件加入，表单变为multipart/form-data\n\t *\n\t * @param name      名\n\t * @param fileBytes 需要上传的文件\n\t * @param fileName  文件名\n\t * @return this\n\t * @since 4.1.0\n\t */\n\tpublic HttpRequest form(String name, byte[] fileBytes, String fileName) {\n\t\tif (null != fileBytes) {\n\t\t\tform(name, new BytesResource(fileBytes, fileName));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 文件表单项<br>\n\t * 一旦有文件加入，表单变为multipart/form-data\n\t *\n\t * @param name     名\n\t * @param resource 数据源，文件可以使用{@link FileResource}包装使用\n\t * @return this\n\t * @since 4.0.9\n\t */\n\tpublic HttpRequest form(String name, Resource resource) {\n\t\tif (null != resource) {\n\t\t\tif (false == isKeepAlive()) {\n\t\t\t\tkeepAlive(true);\n\t\t\t}\n\n\t\t\tthis.isMultiPart = true;\n\t\t\treturn putToForm(name, resource);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取表单数据\n\t *\n\t * @return 表单Map\n\t */\n\tpublic Map<String, Object> form() {\n\t\treturn this.form;\n\t}\n\n\t/**\n\t * 获取文件表单数据\n\t *\n\t * @return 文件表单Map\n\t * @since 3.3.0\n\t */\n\tpublic Map<String, Resource> fileForm() {\n\t\tfinal Map<String, Resource> result = MapUtil.newHashMap();\n\t\tthis.form.forEach((key, value) -> {\n\t\t\tif (value instanceof Resource) {\n\t\t\t\tresult.put(key, (Resource) value);\n\t\t\t}\n\t\t});\n\t\treturn result;\n\t}\n\t// ---------------------------------------------------------------- Form end\n\n\t// ---------------------------------------------------------------- Body start\n\n\t/**\n\t * 设置内容主体<br>\n\t * 请求体body参数支持两种类型：\n\t *\n\t * <pre>\n\t * 1. 标准参数，例如 a=1&amp;b=2 这种格式\n\t * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type\n\t * </pre>\n\t *\n\t * @param body 请求体\n\t * @return this\n\t */\n\tpublic HttpRequest body(String body) {\n\t\treturn this.body(body, null);\n\t}\n\n\t/**\n\t * 设置内容主体<br>\n\t * 请求体body参数支持两种类型：\n\t *\n\t * <pre>\n\t * 1. 标准参数，例如 a=1&amp;b=2 这种格式\n\t * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type\n\t * </pre>\n\t *\n\t * @param body        请求体\n\t * @param contentType 请求体类型，{@code null}表示自动判断类型\n\t * @return this\n\t */\n\tpublic HttpRequest body(String body, String contentType) {\n\t\tbyte[] bytes = StrUtil.bytes(body, this.charset);\n\t\tbody(bytes);\n\t\tthis.form = null; // 当使用body时，停止form的使用\n\n\t\tif (null != contentType) {\n\t\t\t// Content-Type自定义设置\n\t\t\tthis.contentType(contentType);\n\t\t} else {\n\t\t\t// 在用户未自定义的情况下自动根据内容判断\n\t\t\tcontentType = HttpUtil.getContentTypeByRequestBody(body);\n\t\t\tif (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) {\n\t\t\t\tif (null != this.charset) {\n\t\t\t\t\t// 附加编码信息\n\t\t\t\t\tcontentType = ContentType.build(contentType, this.charset);\n\t\t\t\t}\n\t\t\t\tthis.contentType(contentType);\n\t\t\t}\n\t\t}\n\n\t\t// 判断是否为rest请求\n\t\tif (StrUtil.containsAnyIgnoreCase(contentType, \"json\", \"xml\")) {\n\t\t\tthis.isRest = true;\n\t\t\tcontentLength(bytes.length);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置主体字节码<br>\n\t * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8\n\t *\n\t * @param bodyBytes 主体\n\t * @return this\n\t */\n\tpublic HttpRequest body(byte[] bodyBytes) {\n\t\tif (ArrayUtil.isNotEmpty(bodyBytes)) {\n\t\t\treturn body(new BytesResource(bodyBytes));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置主体字节码<br>\n\t * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8\n\t *\n\t * @param resource 主体\n\t * @return this\n\t */\n\tpublic HttpRequest body(Resource resource) {\n\t\tif (null != resource) {\n\t\t\tthis.body = resource;\n\t\t}\n\t\treturn this;\n\t}\n\t// ---------------------------------------------------------------- Body end\n\n\t/**\n\t * 将新的配置加入<br>\n\t * 注意加入的配置可能被修改\n\t *\n\t * @param config 配置\n\t * @return this\n\t */\n\tpublic HttpRequest setConfig(HttpConfig config) {\n\t\tthis.config = config;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置超时，单位：毫秒<br>\n\t * 超时包括：\n\t *\n\t * <pre>\n\t * 1. 连接超时\n\t * 2. 读取响应超时\n\t * </pre>\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @see #setConnectionTimeout(int)\n\t * @see #setReadTimeout(int)\n\t */\n\tpublic HttpRequest timeout(int milliseconds) {\n\t\tconfig.timeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic HttpRequest setConnectionTimeout(int milliseconds) {\n\t\tconfig.setConnectionTimeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic HttpRequest setReadTimeout(int milliseconds) {\n\t\tconfig.setReadTimeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 禁用缓存\n\t *\n\t * @return this\n\t */\n\tpublic HttpRequest disableCache() {\n\t\tconfig.disableCache();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否打开重定向，如果打开默认重定向次数为2<br>\n\t * 此方法效果与{@link #setMaxRedirectCount(int)} 一致\n\t *\n\t * <p>\n\t * 需要注意的是，当设置为{@code true}时，如果全局重定向次数非0，直接复用，否则设置默认2次。<br>\n\t * 当设置为{@code false}时，无论全局是否设置次数，都设置为0。<br>\n\t * 不调用此方法的情况下，使用全局默认的次数。\n\t * </p>\n\t *\n\t * @param isFollowRedirects 是否打开重定向\n\t * @return this\n\t */\n\tpublic HttpRequest setFollowRedirects(boolean isFollowRedirects) {\n\t\tif (isFollowRedirects) {\n\t\t\tif (config.maxRedirectCount <= 0) {\n\t\t\t\t// 默认两次跳转\n\t\t\t\treturn setMaxRedirectCount(2);\n\t\t\t}\n\t\t} else {\n\t\t\t// 手动强制关闭重定向，此时不受全局重定向设置影响\n\t\t\tif (config.maxRedirectCount < 0) {\n\t\t\t\treturn setMaxRedirectCount(0);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 自动重定向时是否处理cookie\n\t *\n\t * @param followRedirectsCookie 自动重定向时是否处理cookie\n\t * @return this\n\t */\n\tpublic HttpRequest setFollowRedirectsCookie(boolean followRedirectsCookie) {\n\t\tconfig.setFollowRedirectsCookie(followRedirectsCookie);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置最大重定向次数<br>\n\t * 如果次数小于1则表示不重定向，大于等于1表示打开重定向\n\t *\n\t * @param maxRedirectCount 最大重定向次数\n\t * @return this\n\t * @since 3.3.0\n\t */\n\tpublic HttpRequest setMaxRedirectCount(int maxRedirectCount) {\n\t\tconfig.setMaxRedirectCount(maxRedirectCount);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置域名验证器<br>\n\t * 只针对HTTPS请求，如果不设置，不做验证，所有域名被信任\n\t *\n\t * @param hostnameVerifier HostnameVerifier\n\t * @return this\n\t */\n\tpublic HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {\n\t\tconfig.setHostnameVerifier(hostnameVerifier);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Http代理\n\t *\n\t * @param host 代理 主机\n\t * @param port 代理 端口\n\t * @return this\n\t * @since 5.4.5\n\t */\n\tpublic HttpRequest setHttpProxy(String host, int port) {\n\t\tconfig.setHttpProxy(host, port);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置代理\n\t *\n\t * @param proxy 代理 {@link Proxy}\n\t * @return this\n\t */\n\tpublic HttpRequest setProxy(Proxy proxy) {\n\t\tconfig.setProxy(proxy);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置SSLSocketFactory<br>\n\t * 只针对HTTPS请求，如果不设置，使用默认的SSLSocketFactory<br>\n\t * 默认SSLSocketFactory为：SSLSocketFactoryBuilder.create().build();\n\t *\n\t * @param ssf SSLScketFactory\n\t * @return this\n\t */\n\tpublic HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {\n\t\tconfig.setSSLSocketFactory(ssf);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置HTTPS安全连接协议，只针对HTTPS请求，可以使用的协议包括：<br>\n\t * 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。\n\t *\n\t * <pre>\n\t * 1. TLSv1.2\n\t * 2. TLSv1.1\n\t * 3. SSLv3\n\t * ...\n\t * </pre>\n\t *\n\t * @param protocol 协议\n\t * @return this\n\t * @see SSLUtil#createSSLContext(String)\n\t * @see #setSSLSocketFactory(SSLSocketFactory)\n\t */\n\tpublic HttpRequest setSSLProtocol(String protocol) {\n\t\tconfig.setSSLProtocol(protocol);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否rest模式<br>\n\t * rest模式下get请求不会把参数附加到URL之后\n\t *\n\t * @param isRest 是否rest模式\n\t * @return this\n\t * @since 4.5.0\n\t */\n\tpublic HttpRequest setRest(boolean isRest) {\n\t\tthis.isRest = isRest;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 采用流方式上传数据，无需本地缓存数据。<br>\n\t * HttpUrlConnection默认是将所有数据读到本地缓存，然后再发送给服务器，这样上传大文件时就会导致内存溢出。\n\t *\n\t * @param blockSize 块大小（bytes数），0或小于0表示不设置Chuncked模式\n\t * @return this\n\t * @since 4.6.5\n\t */\n\tpublic HttpRequest setChunkedStreamingMode(int blockSize) {\n\t\tconfig.setBlockSize(blockSize);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t * @see #addRequestInterceptor(HttpInterceptor)\n\t * @since 5.7.16\n\t */\n\tpublic HttpRequest addInterceptor(HttpInterceptor<HttpRequest> interceptor) {\n\t\treturn addRequestInterceptor(interceptor);\n\t}\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t * @since 5.8.0\n\t */\n\tpublic HttpRequest addRequestInterceptor(HttpInterceptor<HttpRequest> interceptor) {\n\t\tconfig.addRequestInterceptor(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置拦截器，用于在请求前重新编辑请求\n\t *\n\t * @param interceptor 拦截器实现\n\t * @return this\n\t * @since 5.8.0\n\t */\n\tpublic HttpRequest addResponseInterceptor(HttpInterceptor<HttpResponse> interceptor) {\n\t\tconfig.addResponseInterceptor(interceptor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 执行Reuqest请求\n\t *\n\t * @return this\n\t */\n\tpublic HttpResponse execute() {\n\t\treturn this.execute(false);\n\t}\n\n\t/**\n\t * 异步请求<br>\n\t * 异步请求后获取的{@link HttpResponse} 为异步模式，执行完此方法后发送请求到服务器，但是并不立即读取响应内容。<br>\n\t * 此时保持Http连接不关闭，直调用获取内容方法为止。\n\t *\n\t * <p>\n\t * 一般执行完execute之后会把响应内容全部读出来放在一个 byte数组里，如果你响应的内容太多内存就爆了，此法是发送完请求不直接读响应内容，等有需要的时候读。\n\t *\n\t * @return 异步对象，使用get方法获取HttpResponse对象\n\t */\n\tpublic HttpResponse executeAsync() {\n\t\treturn this.execute(true);\n\t}\n\n\t/**\n\t * 执行Reuqest请求\n\t *\n\t * @param isAsync 是否异步\n\t * @return this\n\t */\n\tpublic HttpResponse execute(boolean isAsync) {\n\t\treturn doExecute(isAsync, config.requestInterceptors, config.responseInterceptors);\n\t}\n\n\t/**\n\t * 执行Request请求后，对响应内容后续处理<br>\n\t * 处理结束后关闭连接\n\t *\n\t * @param consumer 响应内容处理函数\n\t * @since 5.7.8\n\t */\n\tpublic void then(Consumer<HttpResponse> consumer) {\n\t\ttry (final HttpResponse response = execute(true)) {\n\t\t\tconsumer.accept(response);\n\t\t}\n\t}\n\n\t/**\n\t * 执行Request请求后，对响应内容后续处理<br>\n\t * 处理结束后关闭连接\n\t *\n\t * @param <T>      处理结果类型\n\t * @param function 响应内容处理函数\n\t * @return 处理结果\n\t * @since 5.8.5\n\t */\n\tpublic <T> T thenFunction(Function<HttpResponse, T> function) {\n\t\ttry (final HttpResponse response = execute(true)) {\n\t\t\treturn function.apply(response);\n\t\t}\n\t}\n\n\t/**\n\t * 简单验证，生成的头信息类似于：\n\t * <pre>\n\t * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\t * </pre>\n\t *\n\t * @param username 用户名\n\t * @param password 密码\n\t * @return this\n\t */\n\tpublic HttpRequest basicAuth(String username, String password) {\n\t\treturn auth(HttpUtil.buildBasicAuth(username, password, charset));\n\t}\n\n\t/**\n\t * 简单代理验证，生成的头信息类似于：\n\t * <pre>\n\t * Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\t * </pre>\n\t *\n\t * @param username 用户名\n\t * @param password 密码\n\t * @return this\n\t * @since 5.4.6\n\t */\n\tpublic HttpRequest basicProxyAuth(String username, String password) {\n\t\treturn proxyAuth(HttpUtil.buildBasicAuth(username, password, charset));\n\t}\n\n\t/**\n\t * 令牌验证，生成的头类似于：\"Authorization: Bearer XXXXX\"，一般用于JWT\n\t *\n\t * @param token 令牌内容\n\t * @return HttpRequest\n\t * @since 5.5.3\n\t */\n\tpublic HttpRequest bearerAuth(String token) {\n\t\treturn auth(\"Bearer \" + token);\n\t}\n\n\t/**\n\t * 验证，简单插入Authorization头\n\t *\n\t * @param content 验证内容\n\t * @return HttpRequest\n\t * @since 5.2.4\n\t */\n\tpublic HttpRequest auth(String content) {\n\t\theader(Header.AUTHORIZATION, content, true);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 验证，简单插入Authorization头\n\t *\n\t * @param content 验证内容\n\t * @return HttpRequest\n\t * @since 5.4.6\n\t */\n\tpublic HttpRequest proxyAuth(String content) {\n\t\theader(Header.PROXY_AUTHORIZATION, content, true);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal StringBuilder sb = StrUtil.builder();\n\t\tsb.append(\"Request Url: \").append(this.url.setCharset(this.charset)).append(StrUtil.CRLF);\n\n\t\t// header\n\t\tsb.append(\"Request Headers: \").append(StrUtil.CRLF);\n\t\tfor (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {\n\t\t\tsb.append(\"    \").append(\n\t\t\t\t\tentry.getKey()).append(\": \").append(CollUtil.join(entry.getValue(), \",\"))\n\t\t\t\t.append(StrUtil.CRLF);\n\t\t}\n\n\t\t// body\n\t\tsb.append(\"Request Body: \").append(StrUtil.CRLF);\n\t\tsb.append(\"    \").append(createBody()).append(StrUtil.CRLF);\n\t\treturn sb.toString();\n\t}\n\n\t// ---------------------------------------------------------------- Private method start\n\n\t/**\n\t * 执行Reuqest请求\n\t *\n\t * @param isAsync              是否异步\n\t * @param requestInterceptors  请求拦截器列表\n\t * @param responseInterceptors 响应拦截器列表\n\t * @return this\n\t */\n\tprivate HttpResponse doExecute(boolean isAsync, HttpInterceptor.Chain<HttpRequest> requestInterceptors,\n\t\t\t\t\t\t\t\t   HttpInterceptor.Chain<HttpResponse> responseInterceptors) {\n\t\tif (null != requestInterceptors) {\n\t\t\tfor (HttpInterceptor<HttpRequest> interceptor : requestInterceptors) {\n\t\t\t\tinterceptor.process(this);\n\t\t\t}\n\t\t}\n\n\t\t// 初始化URL\n\t\turlWithParamIfGet();\n\t\t// 初始化 connection\n\t\tinitConnection();\n\t\t// 发送请求\n\t\tsend();\n\n\t\t// 手动实现重定向\n\t\tHttpResponse httpResponse = sendRedirectIfPossible(isAsync);\n\n\t\t// 获取响应\n\t\tif (null == httpResponse) {\n\t\t\thttpResponse = new HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody());\n\t\t}\n\n\t\t// 拦截响应\n\t\tif (null != responseInterceptors) {\n\t\t\tfor (HttpInterceptor<HttpResponse> interceptor : responseInterceptors) {\n\t\t\t\tinterceptor.process(httpResponse);\n\t\t\t}\n\t\t}\n\n\t\treturn httpResponse;\n\t}\n\n\t/**\n\t * 初始化网络连接\n\t */\n\tprivate void initConnection() {\n\t\tif (null != this.httpConnection) {\n\t\t\t// 执行下次请求时自动关闭上次请求（常用于转发）\n\t\t\tthis.httpConnection.disconnectQuietly();\n\t\t}\n\n\t\tthis.httpConnection = HttpConnection\n\t\t\t// issue#I50NHQ\n\t\t\t// 在生成正式URL前，设置自定义编码\n\t\t\t.create(this.url.setCharset(this.charset).toURL(this.urlHandler), config.proxy)//\n\t\t\t.setConnectTimeout(config.connectionTimeout)//\n\t\t\t.setReadTimeout(config.readTimeout)//\n\t\t\t.setMethod(this.method)//\n\t\t\t.setHttpsInfo(config.hostnameVerifier, config.ssf)//\n\t\t\t// 关闭JDK自动转发，采用手动转发方式\n\t\t\t.setInstanceFollowRedirects(false)\n\t\t\t// 流方式上传数据\n\t\t\t.setChunkedStreamingMode(config.blockSize)\n\t\t\t// issue#3462 自定义body长度\n\t\t\t.setFixedLengthStreamingMode(this.fixedContentLength)\n\t\t\t// 覆盖默认Header\n\t\t\t.header(this.headers, false, this.isHeaderAggregated);\n\n\t\tif (null != this.cookie) {\n\t\t\t// 当用户自定义Cookie时，全局Cookie自动失效\n\t\t\tthis.httpConnection.setCookie(this.cookie);\n\t\t} else {\n\t\t\t// 读取全局Cookie信息并附带到请求中\n\t\t\tGlobalCookieManager.add(this.httpConnection);\n\t\t}\n\n\t\t// 是否禁用缓存\n\t\tif (config.isDisableCache) {\n\t\t\tthis.httpConnection.disableCache();\n\t\t}\n\t}\n\n\t/**\n\t * 对于GET请求将参数加到URL中<br>\n\t * 此处不对URL中的特殊字符做单独编码<br>\n\t * 对于非rest的GET请求，且处于重定向时，参数丢弃\n\t */\n\tprivate void urlWithParamIfGet() {\n\t\tif (Method.GET.equals(method) && false == this.isRest && this.redirectCount <= 0) {\n\t\t\tUrlQuery query = this.url.getQuery();\n\t\t\tif (null == query) {\n\t\t\t\tquery = new UrlQuery();\n\t\t\t\tthis.url.setQuery(query);\n\t\t\t}\n\n\t\t\t// 优先使用body形式的参数，不存在使用form\n\t\t\tif (null != this.body) {\n\t\t\t\tquery.parse(StrUtil.str(this.body.readBytes(), this.charset), this.charset);\n\t\t\t} else {\n\t\t\t\tquery.addAll(this.form);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 调用转发，如果需要转发返回转发结果，否则返回{@code null}\n\t *\n\t * @param isAsync 是否异步\n\t * @return {@link HttpResponse}，无转发返回 {@code null}\n\t */\n\tprivate HttpResponse sendRedirectIfPossible(boolean isAsync) {\n\t\t// 手动实现重定向\n\t\tif (config.maxRedirectCount > 0) {\n\t\t\tfinal int responseCode;\n\t\t\ttry {\n\t\t\t\tresponseCode = httpConnection.responseCode();\n\t\t\t} catch (IOException e) {\n\t\t\t\t// 错误时静默关闭连接\n\t\t\t\tthis.httpConnection.disconnectQuietly();\n\t\t\t\tthrow new HttpException(e);\n\t\t\t}\n\t\t\t// 支持自动重定向时处理cookie\n\t\t\t// https://github.com/chinabugotech/hutool/issues/2960\n\t\t\tif (config.followRedirectsCookie) {\n\t\t\t\tGlobalCookieManager.store(httpConnection);\n\t\t\t}\n\t\t\tif (responseCode != HttpURLConnection.HTTP_OK) {\n\t\t\t\tif (HttpStatus.isRedirected(responseCode)) {\n\t\t\t\t\tfinal UrlBuilder redirectUrl;\n\t\t\t\t\tString location = httpConnection.header(Header.LOCATION);\n\t\t\t\t\tif (false == HttpUtil.isHttp(location) && false == HttpUtil.isHttps(location)) {\n\t\t\t\t\t\t// issue#I5TPSY, location可能为相对路径\n\t\t\t\t\t\tif (false == location.startsWith(\"/\")) {\n\t\t\t\t\t\t\tlocation = StrUtil.addSuffixIfNot(this.url.getPathStr(), \"/\") + location;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// issue#3265, 相对路径中可能存在参数，单独处理参数\n\t\t\t\t\t\tfinal String query;\n\t\t\t\t\t\tfinal List<String> split = StrUtil.split(location, '?', 2);\n\t\t\t\t\t\tif (split.size() == 2) {\n\t\t\t\t\t\t\t// 存在参数\n\t\t\t\t\t\t\tlocation = split.get(0);\n\t\t\t\t\t\t\tquery = split.get(1);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tquery = null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tredirectUrl = UrlBuilder.of(this.url.getScheme(), this.url.getHost(), this.url.getPort()\n\t\t\t\t\t\t\t, location, query, null, this.charset);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tredirectUrl = UrlBuilder.ofHttpWithoutEncode(location);\n\t\t\t\t\t}\n\t\t\t\t\tsetUrl(redirectUrl);\n\t\t\t\t\t// https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7\n\t\t\t\t\t// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections\n\t\t\t\t\t// 307、308方法和消息主体都不发生变化。\n\t\t\t\t\tif (HttpStatus.HTTP_TEMP_REDIRECT != responseCode && HttpStatus.HTTP_PERMANENT_REDIRECT != responseCode) {\n\t\t\t\t\t\t// 重定向默认使用GET\n\t\t\t\t\t\tmethod(Method.GET);\n\t\t\t\t\t}\n\t\t\t\t\tif (redirectCount < config.maxRedirectCount) {\n\t\t\t\t\t\tredirectCount++;\n\t\t\t\t\t\t// 重定向可选是否走过滤器\n\t\t\t\t\t\treturn doExecute(isAsync, config.interceptorOnRedirect ? config.requestInterceptors : null,\n\t\t\t\t\t\t\tconfig.interceptorOnRedirect ? config.responseInterceptors : null);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 发送数据流\n\t *\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void send() throws IORuntimeException {\n\t\ttry {\n\t\t\tif (Method.POST.equals(this.method) //\n\t\t\t\t|| Method.PUT.equals(this.method) //\n\t\t\t\t|| Method.DELETE.equals(this.method) //\n\t\t\t\t|| this.isRest) {\n\t\t\t\tif (isMultipart()) {\n\t\t\t\t\tsendMultipart(); // 文件上传表单\n\t\t\t\t} else {\n\t\t\t\t\tsendFormUrlEncoded();// 普通表单\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.httpConnection.connect();\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\t// 异常时关闭连接\n\t\t\tthis.httpConnection.disconnectQuietly();\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 发送普通表单<br>\n\t * 发送数据后自动关闭输出流\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void sendFormUrlEncoded() throws IOException {\n\t\tif (this.config.useDefaultContentTypeIfNull && StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) {\n\t\t\t// 如果未自定义Content-Type，使用默认的application/x-www-form-urlencoded\n\t\t\tthis.httpConnection.header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(this.charset), true);\n\t\t}\n\n\t\t// Write的时候会优先使用body中的内容，write时自动关闭OutputStream\n\t\tcreateBody().writeClose(this.httpConnection.getOutputStream());\n\t}\n\n\t/**\n\t * 创建body\n\t *\n\t * @return body\n\t */\n\tprivate RequestBody createBody() {\n\t\t// Write的时候会优先使用body中的内容，write时自动关闭OutputStream\n\t\tif (null != this.body) {\n\t\t\treturn ResourceBody.create(this.body);\n\t\t} else {\n\t\t\treturn FormUrlEncodedBody.create(this.form, this.charset);\n\t\t}\n\t}\n\n\t/**\n\t * 发送多组件请求（例如包含文件的表单）<br>\n\t * 发送数据后自动关闭输出流\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void sendMultipart() throws IOException {\n\t\tfinal RequestBody body;\n\t\t// issue#3158，当用户自定义为multipart同时传入body，则不做单独处理\n\t\tif (null == form && null != this.body) {\n\t\t\tbody = ResourceBody.create(this.body);\n\t\t} else {\n\t\t\tfinal MultipartBody multipartBody = MultipartBody.create(this.form, this.charset);\n\t\t\t//设置表单类型为Multipart（文件上传）\n\t\t\tthis.httpConnection.header(Header.CONTENT_TYPE, multipartBody.getContentType(), true);\n\t\t\tbody = multipartBody;\n\t\t}\n\n\t\tbody.writeClose(this.httpConnection.getOutputStream());\n\t}\n\n\t/**\n\t * 是否忽略读取响应body部分<br>\n\t * HEAD、CONNECT、TRACE方法将不读取响应体\n\t *\n\t * @return 是否需要忽略响应body部分\n\t * @since 3.1.2\n\t */\n\tprivate boolean isIgnoreResponseBody() {\n\t\treturn Method.HEAD == this.method //\n\t\t\t|| Method.CONNECT == this.method //\n\t\t\t|| Method.TRACE == this.method;\n\t}\n\n\t/**\n\t * 判断是否为multipart/form-data表单，条件如下：\n\t *\n\t * <pre>\n\t *     1. 存在资源对象（fileForm非空）\n\t *     2. 用户自定义头为multipart/form-data开头\n\t * </pre>\n\t *\n\t * @return 是否为multipart/form-data表单\n\t * @since 5.3.5\n\t */\n\tprivate boolean isMultipart() {\n\t\tif (this.isMultiPart) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfinal String contentType = header(Header.CONTENT_TYPE);\n\t\treturn StrUtil.isNotEmpty(contentType) &&\n\t\t\tcontentType.startsWith(ContentType.MULTIPART.getValue());\n\t}\n\n\t/**\n\t * 将参数加入到form中，如果form为空，新建之。\n\t *\n\t * @param name  表单属性名\n\t * @param value 属性值\n\t * @return this\n\t */\n\tprivate HttpRequest putToForm(String name, Object value) {\n\t\tif (null == name || null == value) {\n\t\t\treturn this;\n\t\t}\n\t\tif (null == this.form) {\n\t\t\tthis.form = new TableMap<>(16);\n\t\t}\n\t\tthis.form.put(name, value);\n\t\treturn this;\n\t}\n\t// ---------------------------------------------------------------- Private method end\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpResource.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\n\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.net.URL;\n\n/**\n * HTTP资源，可自定义Content-Type\n *\n * @author looly\n * @since 5.7.17\n */\npublic class HttpResource implements Resource, Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Resource resource;\n\tprivate final String contentType;\n\n\t/**\n\t * 构造\n\t *\n\t * @param resource    资源，非空\n\t * @param contentType Content-Type类型，{@code null}表示不设置\n\t */\n\tpublic HttpResource(Resource resource, String contentType) {\n\t\tthis.resource = Assert.notNull(resource, \"Resource must be not null !\");\n\t\tthis.contentType = contentType;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn resource.getName();\n\t}\n\n\t@Override\n\tpublic URL getUrl() {\n\t\treturn resource.getUrl();\n\t}\n\n\t@Override\n\tpublic InputStream getStream() {\n\t\treturn resource.getStream();\n\t}\n\n\t/**\n\t * 获取自定义Content-Type类型\n\t *\n\t * @return Content-Type类型\n\t */\n\tpublic String getContentType() {\n\t\treturn this.contentType;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpResponse.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.FastByteArrayOutputStream;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.io.resource.BytesResource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.http.cookie.GlobalCookieManager;\n\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.HttpCookie;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map.Entry;\n\n/**\n * Http响应类<br>\n * 非线程安全对象\n *\n * @author Looly\n */\npublic class HttpResponse extends HttpBase<HttpResponse> implements Closeable {\n\n\t/**\n\t * Http配置\n\t */\n\tprotected HttpConfig config;\n\t/**\n\t * 持有连接对象\n\t */\n\tprotected HttpConnection httpConnection;\n\t/**\n\t * Http请求原始流\n\t */\n\tprotected InputStream in;\n\t/**\n\t * 是否异步，异步下只持有流，否则将在初始化时直接读取body内容\n\t */\n\tprivate volatile boolean isAsync;\n\t/**\n\t * 响应状态码\n\t */\n\tprotected int status;\n\t/**\n\t * 是否忽略读取Http响应体\n\t */\n\tprivate final boolean ignoreBody;\n\t/**\n\t * 从响应中获取的编码\n\t */\n\tprivate Charset charsetFromResponse;\n\n\t/**\n\t * 构造\n\t *\n\t * @param httpConnection {@link HttpConnection}\n\t * @param config         Http配置\n\t * @param charset        编码，从请求编码中获取默认编码\n\t * @param isAsync        是否异步\n\t * @param isIgnoreBody   是否忽略读取响应体\n\t * @since 3.1.2\n\t */\n\tprotected HttpResponse(HttpConnection httpConnection, HttpConfig config, Charset charset, boolean isAsync, boolean isIgnoreBody) {\n\t\tthis.httpConnection = httpConnection;\n\t\tthis.config = config;\n\t\tthis.charset = charset;\n\t\tthis.isAsync = isAsync;\n\t\tthis.ignoreBody = isIgnoreBody;\n\t\tinitWithDisconnect();\n\t}\n\n\t/**\n\t * 获取状态码\n\t *\n\t * @return 状态码\n\t */\n\tpublic int getStatus() {\n\t\treturn this.status;\n\t}\n\n\t/**\n\t * 请求是否成功，判断依据为：状态码范围在200~299内。\n\t *\n\t * @return 是否成功请求\n\t * @since 4.1.9\n\t */\n\tpublic boolean isOk() {\n\t\treturn this.status >= 200 && this.status < 300;\n\t}\n\n\t/**\n\t * 同步<br>\n\t * 如果为异步状态，则暂时不读取服务器中响应的内容，而是持有Http链接的{@link InputStream}。<br>\n\t * 当调用此方法时，异步状态转为同步状态，此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态，则不进行任何操作。\n\t *\n\t * @return this\n\t */\n\tpublic HttpResponse sync() {\n\t\treturn this.isAsync ? forceSync() : this;\n\t}\n\n\t// ---------------------------------------------------------------- Http Response Header start\n\n\t/**\n\t * 获取内容编码\n\t *\n\t * @return String\n\t */\n\tpublic String contentEncoding() {\n\t\treturn header(Header.CONTENT_ENCODING);\n\t}\n\n\t/**\n\t * 获取内容长度，以下情况长度无效：\n\t * <ul>\n\t *     <li>Transfer-Encoding: Chunked</li>\n\t *     <li>Content-Encoding: XXX</li>\n\t * </ul>\n\t * 参考：https://blog.csdn.net/jiang7701037/article/details/86304302\n\t *\n\t * @return 长度，-1表示服务端未返回或长度无效\n\t * @since 5.7.9\n\t */\n\tpublic long contentLength() {\n\t\tlong contentLength = Convert.toLong(header(Header.CONTENT_LENGTH), -1L);\n\t\tif (contentLength > 0 && (isChunked() || StrUtil.isNotBlank(contentEncoding()))) {\n\t\t\t//按照HTTP协议规范，在 Transfer-Encoding和Content-Encoding设置后 Content-Length 无效。\n\t\t\tcontentLength = -1;\n\t\t}\n\t\treturn contentLength;\n\t}\n\n\t/**\n\t * 是否为gzip压缩过的内容\n\t *\n\t * @return 是否为gzip压缩过的内容\n\t */\n\tpublic boolean isGzip() {\n\t\tfinal String contentEncoding = contentEncoding();\n\t\treturn \"gzip\".equalsIgnoreCase(contentEncoding);\n\t}\n\n\t/**\n\t * 是否为zlib(Deflate)压缩过的内容\n\t *\n\t * @return 是否为zlib(Deflate)压缩过的内容\n\t * @since 4.5.7\n\t */\n\tpublic boolean isDeflate() {\n\t\tfinal String contentEncoding = contentEncoding();\n\t\treturn \"deflate\".equalsIgnoreCase(contentEncoding);\n\t}\n\n\t/**\n\t * 是否为Transfer-Encoding:Chunked的内容\n\t *\n\t * @return 是否为Transfer-Encoding:Chunked的内容\n\t * @since 4.6.2\n\t */\n\tpublic boolean isChunked() {\n\t\tfinal String transferEncoding = header(Header.TRANSFER_ENCODING);\n\t\treturn \"Chunked\".equalsIgnoreCase(transferEncoding);\n\t}\n\n\t/**\n\t * 获取本次请求服务器返回的Cookie信息\n\t *\n\t * @return Cookie字符串\n\t * @since 3.1.1\n\t */\n\tpublic String getCookieStr() {\n\t\treturn header(Header.SET_COOKIE);\n\t}\n\n\t/**\n\t * 获取Cookie\n\t *\n\t * @return Cookie列表\n\t * @since 3.1.1\n\t */\n\tpublic List<HttpCookie> getCookies() {\n\t\treturn GlobalCookieManager.getCookies(this.httpConnection);\n\t}\n\n\t/**\n\t * 获取Cookie\n\t *\n\t * @param name Cookie名\n\t * @return {@link HttpCookie}\n\t * @since 4.1.4\n\t */\n\tpublic HttpCookie getCookie(String name) {\n\t\tList<HttpCookie> cookie = getCookies();\n\t\tif (null != cookie) {\n\t\t\tfor (HttpCookie httpCookie : cookie) {\n\t\t\t\tif (httpCookie.getName().equals(name)) {\n\t\t\t\t\treturn httpCookie;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取Cookie值\n\t *\n\t * @param name Cookie名\n\t * @return Cookie值\n\t * @since 4.1.4\n\t */\n\tpublic String getCookieValue(String name) {\n\t\tfinal HttpCookie cookie = getCookie(name);\n\t\treturn (null == cookie) ? null : cookie.getValue();\n\t}\n\t// ---------------------------------------------------------------- Http Response Header end\n\n\t// ---------------------------------------------------------------- Body start\n\n\t/**\n\t * 获得服务区响应流<br>\n\t * 异步模式下获取Http原生流，同步模式下获取获取到的在内存中的副本<br>\n\t * 如果想在同步模式下获取流，请先调用{@link #sync()}方法强制同步<br>\n\t * 流获取后处理完毕需关闭此类\n\t *\n\t * @return 响应流\n\t */\n\tpublic InputStream bodyStream() {\n\t\tif (isAsync) {\n\t\t\treturn this.in;\n\t\t}\n\t\treturn null == this.body ? null : this.body.getStream();\n\t}\n\n\t/**\n\t * 获取响应流字节码<br>\n\t * 此方法会转为同步模式\n\t *\n\t * @return byte[]\n\t */\n\t@Override\n\tpublic byte[] bodyBytes() {\n\t\tsync();\n\t\treturn super.bodyBytes();\n\t}\n\n\t/**\n\t * 设置主体字节码<br>\n\t * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8\n\t *\n\t * @param bodyBytes 主体\n\t * @return this\n\t */\n\tpublic HttpResponse body(byte[] bodyBytes) {\n\t\tsync();\n\t\tif (null != bodyBytes) {\n\t\t\tthis.body = new BytesResource(bodyBytes);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取响应主体\n\t *\n\t * @return String\n\t * @throws HttpException 包装IO异常\n\t */\n\tpublic String body() throws HttpException {\n\t\treturn HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);\n\t}\n\n\t/**\n\t * 将响应内容写出到{@link OutputStream}<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param out            写出的流\n\t * @param isCloseOut     是否关闭输出流\n\t * @param streamProgress 进度显示接口，通过实现此接口显示下载进度\n\t * @return 写出bytes数\n\t * @since 3.3.2\n\t */\n\tpublic long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {\n\t\tAssert.notNull(out, \"[out] must be not null!\");\n\t\tfinal long contentLength = contentLength();\n\t\ttry {\n\t\t\treturn copyBody(bodyStream(), out, contentLength, streamProgress, this.config.ignoreEOFError);\n\t\t} finally {\n\t\t\tIoUtil.close(this);\n\t\t\tif (isCloseOut) {\n\t\t\t\tIoUtil.close(out);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将响应内容写出到文件<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param targetFileOrDir 写出到的文件或目录\n\t * @param streamProgress  进度显示接口，通过实现此接口显示下载进度\n\t * @return 写出bytes数\n\t * @since 3.3.2\n\t */\n\tpublic long writeBody(File targetFileOrDir, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] must be not null!\");\n\n\t\tfinal File outFile = completeFileNameFromHeader(targetFileOrDir);\n\t\treturn writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);\n\t}\n\n\t/**\n\t * 将响应内容写出到文件-避免未完成的文件<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）<br>\n\t * 来自：https://gitee.com/chinabugotech/hutool/pulls/407<br>\n\t * 此方法原理是先在目标文件同级目录下创建临时文件，下载之，等下载完毕后重命名，避免因下载错误导致的文件不完整。\n\t *\n\t * @param targetFileOrDir 写出到的文件或目录\n\t * @param tempFileSuffix  临时文件后缀，默认\".temp\"\n\t * @param streamProgress  进度显示接口，通过实现此接口显示下载进度\n\t * @return 写出bytes数\n\t * @since 5.7.12\n\t */\n\tpublic long writeBody(File targetFileOrDir, String tempFileSuffix, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] must be not null!\");\n\n\t\tFile outFile = completeFileNameFromHeader(targetFileOrDir);\n\n\t\tif (StrUtil.isBlank(tempFileSuffix)) {\n\t\t\ttempFileSuffix = \".temp\";\n\t\t} else {\n\t\t\ttempFileSuffix = StrUtil.addPrefixIfNot(tempFileSuffix, StrUtil.DOT);\n\t\t}\n\n\t\t// 目标文件真实名称\n\t\tfinal String fileName = outFile.getName();\n\t\t// 临时文件名称\n\t\tfinal String tempFileName = fileName + tempFileSuffix;\n\n\t\t// 临时文件\n\t\toutFile = new File(outFile.getParentFile(), tempFileName);\n\n\t\tlong length;\n\t\ttry {\n\t\t\tlength = writeBody(outFile, streamProgress);\n\t\t\t// 重命名下载好的临时文件\n\t\t\tFileUtil.rename(outFile, fileName, true);\n\t\t} catch (Throwable e) {\n\t\t\t// 异常则删除临时文件\n\t\t\tFileUtil.del(outFile);\n\t\t\tthrow new HttpException(e);\n\t\t}\n\t\treturn length;\n\t}\n\n\t/**\n\t * 将响应内容写出到文件<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param targetFileOrDir 写出到的文件\n\t * @param streamProgress  进度显示接口，通过实现此接口显示下载进度\n\t * @return 写出的文件\n\t * @since 5.6.4\n\t */\n\tpublic File writeBodyForFile(File targetFileOrDir, StreamProgress streamProgress) {\n\t\tAssert.notNull(targetFileOrDir, \"[targetFileOrDir] must be not null!\");\n\n\t\tfinal File outFile = completeFileNameFromHeader(targetFileOrDir);\n\t\twriteBody(FileUtil.getOutputStream(outFile), true, streamProgress);\n\n\t\treturn outFile;\n\t}\n\n\t/**\n\t * 将响应内容写出到文件<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param targetFileOrDir 写出到的文件或目录\n\t * @return 写出bytes数\n\t * @since 3.3.2\n\t */\n\tpublic long writeBody(File targetFileOrDir) {\n\t\treturn writeBody(targetFileOrDir, null);\n\t}\n\n\t/**\n\t * 将响应内容写出到文件<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param targetFileOrDir 写出到的文件或目录的路径\n\t * @return 写出bytes数\n\t * @since 3.3.2\n\t */\n\tpublic long writeBody(String targetFileOrDir) {\n\t\treturn writeBody(FileUtil.file(targetFileOrDir));\n\t}\n\t// ---------------------------------------------------------------- Body end\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.in);\n\t\tthis.in = null;\n\t\t// 关闭连接\n\t\tthis.httpConnection.disconnectQuietly();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder sb = StrUtil.builder();\n\t\tsb.append(\"Response Headers: \").append(StrUtil.CRLF);\n\t\tfor (Entry<String, List<String>> entry : this.headers.entrySet()) {\n\t\t\tsb.append(\"    \").append(entry).append(StrUtil.CRLF);\n\t\t}\n\n\t\tsb.append(\"Response Body: \").append(StrUtil.CRLF);\n\t\tsb.append(\"    \").append(this.body()).append(StrUtil.CRLF);\n\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 从响应头补全下载文件名\n\t *\n\t * @param targetFileOrDir 目标文件夹或者目标文件\n\t * @return File 保存的文件\n\t * @since 5.4.1\n\t */\n\tpublic File completeFileNameFromHeader(File targetFileOrDir) {\n\t\tif (false == targetFileOrDir.isDirectory()) {\n\t\t\t// 非目录直接返回\n\t\t\treturn targetFileOrDir;\n\t\t}\n\n\t\t// 从头信息中获取文件名\n\t\tString fileName = getFileNameFromDisposition(null);\n\t\tif (StrUtil.isBlank(fileName)) {\n\t\t\tfinal String path = httpConnection.getUrl().getPath();\n\t\t\t// 从路径中获取文件名\n\t\t\tfileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);\n\t\t\tif (StrUtil.isBlank(fileName)) {\n\t\t\t\t// 编码后的路径做为文件名\n\t\t\t\tfileName = URLUtil.encodeQuery(path, charset);\n\t\t\t} else {\n\t\t\t\t// issue#I4K0FS@Gitee\n\t\t\t\tfileName = URLUtil.decode(fileName, charset);\n\t\t\t}\n\t\t}\n\t\treturn FileUtil.file(targetFileOrDir, fileName);\n\t}\n\n\t/**\n\t * 从Content-Disposition头中获取文件名\n\t *\n\t * @return 文件名，empty表示无\n\t */\n\tpublic String getFileNameFromDisposition() {\n\t\treturn getFileNameFromDisposition(null);\n\t}\n\n\t/**\n\t * 从Content-Disposition头中获取文件名，以参数名为`filename`为例，规则为：\n\t * <ul>\n\t *     <li>首先按照RFC5987规范检查`filename*`参数对应的值，即：`filename*=\"example.txt\"`，则获取`example.txt`</li>\n\t *     <li>如果找不到`filename*`参数，则检查`filename`参数对应的值，即：`filename=\"example.txt\"`，则获取`example.txt`</li>\n\t * </ul>\n\t * 按照规范，`Content-Disposition`可能返回多个，此处遍历所有返回头，并且`filename*`始终优先获取，即使`filename`存在并更靠前。<br>\n\t * 参考：https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition\n\t *\n\t * @param paramName 文件参数名，如果为{@code null}则使用默认的`filename`\n\t * @return 文件名，empty表示无\n\t */\n\tpublic String getFileNameFromDisposition(String paramName) {\n\t\tparamName = ObjUtil.defaultIfNull(paramName, \"filename\");\n\t\tfinal List<String> dispositions = headerList(Header.CONTENT_DISPOSITION.getValue());\n\t\tString fileName = null;\n\t\tif (CollUtil.isNotEmpty(dispositions)) {\n\n\t\t\t// filename* 采用了 RFC 5987 中规定的编码方式，优先读取\n\t\t\tfileName = getFileNameFromDispositions(dispositions, StrUtil.addSuffixIfNot(paramName, \"*\"));\n\t\t\tif ((!StrUtil.endWith(fileName, \"*\")) && StrUtil.isBlank(fileName)) {\n\t\t\t\tfileName = getFileNameFromDispositions(dispositions, paramName);\n\t\t\t}\n\t\t}\n\n\t\treturn fileName;\n\t}\n\n\t// ---------------------------------------------------------------- Private method start\n\t/**\n\t * 从Content-Disposition头中获取文件名\n\t *\n\t * @param dispositions Content-Disposition头列表\n\t * @param paramName    文件参数名\n\t * @return 文件名，empty表示无\n\t */\n\tprivate static String getFileNameFromDispositions(final List<String> dispositions, String paramName) {\n\t\t// 正则转义\n\t\tparamName = StrUtil.replace(paramName, \"*\", \"\\\\*\");\n\t\tString fileName = null;\n\t\tfor (final String disposition : dispositions) {\n\t\t\tfileName = ReUtil.getGroup1(paramName + \"=([^;]+)\", disposition);\n\t\t\tif (StrUtil.isNotBlank(fileName)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn getRfc5987Value(fileName);\n\t}\n\n\t/**\n\t * 获取rfc5987标准的值，标准见：https://www.rfc-editor.org/rfc/rfc5987#section-3.2.1<br>\n\t * 包括：\n\t *\n\t *<ul>\n\t *     <li>Non-extended：无双引号包裹的值</li>\n\t *     <li>Non-extended：双引号包裹的值</li>\n\t *     <li>Extended notation：编码'语言'值</li>\n\t *</ul>\n\t *\n\t * @param value 值\n\t * @return 结果值\n\t */\n\tprivate static String getRfc5987Value(final String value){\n\t\tfinal List<String> split = StrUtil.split(value, '\\'');\n\t\tif(3 == split.size()){\n\t\t\treturn split.get(2);\n\t\t}\n\n\t\t// 普通值\n\t\treturn StrUtil.unWrap(value, '\"');\n\t}\n\n\t/**\n\t * 初始化Http响应，并在报错时关闭连接。<br>\n\t * 初始化包括：\n\t *\n\t * <pre>\n\t * 1、读取Http状态\n\t * 2、读取头信息\n\t * 3、持有Http流，并不关闭流\n\t * </pre>\n\t *\n\t * @return this\n\t * @throws HttpException IO异常\n\t */\n\tprivate HttpResponse initWithDisconnect() throws HttpException {\n\t\ttry {\n\t\t\tinit();\n\t\t} catch (HttpException e) {\n\t\t\tthis.httpConnection.disconnectQuietly();\n\t\t\tthrow e;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 初始化Http响应<br>\n\t * 初始化包括：\n\t *\n\t * <pre>\n\t * 1、读取Http状态\n\t * 2、读取头信息\n\t * 3、持有Http流，并不关闭流\n\t * </pre>\n\t *\n\t * @return this\n\t * @throws HttpException IO异常\n\t */\n\tprivate HttpResponse init() throws HttpException {\n\t\t// 获取响应状态码\n\t\ttry {\n\t\t\tthis.status = httpConnection.responseCode();\n\t\t} catch (IOException e) {\n\t\t\tif (false == (e instanceof FileNotFoundException)) {\n\t\t\t\tthrow new HttpException(e);\n\t\t\t}\n\t\t\t// 服务器无返回内容，忽略之\n\t\t}\n\n\n\t\t// 读取响应头信息\n\t\ttry {\n\t\t\tthis.headers = httpConnection.headers();\n\t\t} catch (IllegalArgumentException e) {\n\t\t\t// ignore\n\t\t\t// StaticLog.warn(e, e.getMessage());\n\t\t}\n\n\t\t// 存储服务端设置的Cookie信息\n\t\tGlobalCookieManager.store(httpConnection);\n\n\t\t// 获取响应编码\n\t\tfinal Charset charset = httpConnection.getCharset();\n\t\tthis.charsetFromResponse = charset;\n\t\tif (null != charset) {\n\t\t\tthis.charset = charset;\n\t\t}\n\n\t\t// 获取响应内容流\n\t\tthis.in = new HttpInputStream(this);\n\n\t\t// 同步情况下强制同步\n\t\treturn this.isAsync ? this : forceSync();\n\t}\n\n\t/**\n\t * 强制同步，用于初始化<br>\n\t * 强制同步后变化如下：\n\t *\n\t * <pre>\n\t * 1、读取body内容到内存\n\t * 2、异步状态设为false（变为同步状态）\n\t * 3、关闭Http流\n\t * 4、断开与服务器连接\n\t * </pre>\n\t *\n\t * @return this\n\t */\n\tprivate HttpResponse forceSync() {\n\t\t// 非同步状态转为同步状态\n\t\ttry {\n\t\t\tthis.readBody(this.in);\n\t\t} catch (IORuntimeException e) {\n\t\t\t//noinspection StatementWithEmptyBody\n\t\t\tif (e.getCause() instanceof FileNotFoundException) {\n\t\t\t\t// 服务器无返回内容，忽略之\n\t\t\t} else {\n\t\t\t\tthrow new HttpException(e);\n\t\t\t}\n\t\t} finally {\n\t\t\tif (this.isAsync) {\n\t\t\t\tthis.isAsync = false;\n\t\t\t}\n\t\t\tthis.close();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 读取主体，忽略EOFException异常\n\t *\n\t * @param in 输入流\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void readBody(InputStream in) throws IORuntimeException {\n\t\tif (ignoreBody) {\n\t\t\treturn;\n\t\t}\n\n\t\t// issue#ICB1B8，如果用户定义忽略contentLength头，则不读取\n\t\tfinal long contentLength = config.ignoreContentLength ? -1 : contentLength();\n\t\tfinal FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength);\n\t\tcopyBody(in, out, contentLength, null, this.config.ignoreEOFError);\n\t\tthis.body = new BytesResource(out.toByteArray());\n\t}\n\n\t/**\n\t * 将响应内容写出到{@link OutputStream}<br>\n\t * 异步模式下直接读取Http流写出，同步模式下将存储在内存中的响应内容写出<br>\n\t * 写出后会关闭Http流（异步模式）\n\t *\n\t * @param in               输入流\n\t * @param out              写出的流\n\t * @param contentLength    总长度，-1表示未知\n\t * @param streamProgress   进度显示接口，通过实现此接口显示下载进度\n\t * @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常\n\t * @return 拷贝长度\n\t */\n\tprivate static long copyBody(InputStream in, OutputStream out, long contentLength, StreamProgress streamProgress, boolean isIgnoreEOFError) {\n\t\tif (null == out) {\n\t\t\tthrow new NullPointerException(\"[out] is null!\");\n\t\t}\n\n\t\tlong copyLength = -1;\n\t\ttry {\n\t\t\tcopyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress);\n\t\t} catch (IORuntimeException e) {\n\t\t\t//noinspection StatementWithEmptyBody\n\t\t\tif (isIgnoreEOFError\n\t\t\t\t&& (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), \"Premature EOF\"))) {\n\t\t\t\t// 忽略读取HTTP流中的EOF错误\n\t\t\t} else {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t\treturn copyLength;\n\t}\n\t// ---------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpStatus.java",
    "content": "package cn.hutool.http;\n\n/**\n * HTTP状态码\n *\n * @author Looly\n * @author Ningqingsheng\n * @see java.net.HttpURLConnection\n *\n */\npublic class HttpStatus {\n\n\t/* 1XX: Informational */\n\n\t/**\n\t * HTTP Status-Code 100: Continue.\n\t */\n\tpublic static final int HTTP_CONTINUE = 100;\n\n\t/**\n\t * HTTP Status-Code 101: Switching Protocols.\n\t */\n\tpublic static final int HTTP_SWITCHING_PROTOCOLS = 101;\n\n\t/**\n\t * HTTP Status-Code 102: Processing.\n\t */\n\tpublic static final int HTTP_PROCESSING = 102;\n\n\t/**\n\t * HTTP Status-Code 103: Checkpoint.\n\t */\n\tpublic static final int HTTP_CHECKPOINT = 103;\n\n\t/* 2XX: generally \"OK\" */\n\n\t/**\n\t * HTTP Status-Code 200: OK.\n\t */\n\tpublic static final int HTTP_OK = 200;\n\n\t/**\n\t * HTTP Status-Code 201: Created.\n\t */\n\tpublic static final int HTTP_CREATED = 201;\n\n\t/**\n\t * HTTP Status-Code 202: Accepted.\n\t */\n\tpublic static final int HTTP_ACCEPTED = 202;\n\n\t/**\n\t * HTTP Status-Code 203: Non-Authoritative Information.\n\t */\n\tpublic static final int HTTP_NOT_AUTHORITATIVE = 203;\n\n\t/**\n\t * HTTP Status-Code 204: No Content.\n\t */\n\tpublic static final int HTTP_NO_CONTENT = 204;\n\n\t/**\n\t * HTTP Status-Code 205: Reset Content.\n\t */\n\tpublic static final int HTTP_RESET = 205;\n\n\t/**\n\t * HTTP Status-Code 206: Partial Content.\n\t */\n\tpublic static final int HTTP_PARTIAL = 206;\n\n\t/**\n\t * HTTP Status-Code 207: Multi-Status.\n\t */\n\tpublic static final int HTTP_MULTI_STATUS = 207;\n\n\t/**\n\t * HTTP Status-Code 208: Already Reported.\n\t */\n\tpublic static final int HTTP_ALREADY_REPORTED = 208;\n\n\t/**\n\t * HTTP Status-Code 226: IM Used.\n\t */\n\tpublic static final int HTTP_IM_USED = 226;\n\n\t/* 3XX: relocation/redirect */\n\n\t/**\n\t * HTTP Status-Code 300: Multiple Choices.\n\t */\n\tpublic static final int HTTP_MULT_CHOICE = 300;\n\n\t/**\n\t * HTTP Status-Code 301: Moved Permanently.\n\t */\n\tpublic static final int HTTP_MOVED_PERM = 301;\n\n\t/**\n\t * HTTP Status-Code 302: Temporary Redirect.\n\t */\n\tpublic static final int HTTP_MOVED_TEMP = 302;\n\n\t/**\n\t * HTTP Status-Code 303: See Other.\n\t */\n\tpublic static final int HTTP_SEE_OTHER = 303;\n\n\t/**\n\t * HTTP Status-Code 304: Not Modified.\n\t */\n\tpublic static final int HTTP_NOT_MODIFIED = 304;\n\n\t/**\n\t * HTTP Status-Code 305: Use Proxy.\n\t */\n\tpublic static final int HTTP_USE_PROXY = 305;\n\n\t/**\n\t * HTTP 1.1 Status-Code 307: Temporary Redirect.<br>\n\t * 见：RFC-7231\n\t */\n\tpublic static final int HTTP_TEMP_REDIRECT = 307;\n\n\t/**\n\t * HTTP 1.1 Status-Code 308: Permanent Redirect 永久重定向<br>\n\t * 见：RFC-7231\n\t */\n\tpublic static final int HTTP_PERMANENT_REDIRECT = 308;\n\n\t/* 4XX: client error */\n\n\t/**\n\t * HTTP Status-Code 400: Bad Request.\n\t */\n\tpublic static final int HTTP_BAD_REQUEST = 400;\n\n\t/**\n\t * HTTP Status-Code 401: Unauthorized.\n\t */\n\tpublic static final int HTTP_UNAUTHORIZED = 401;\n\n\t/**\n\t * HTTP Status-Code 402: Payment Required.\n\t */\n\tpublic static final int HTTP_PAYMENT_REQUIRED = 402;\n\n\t/**\n\t * HTTP Status-Code 403: Forbidden.\n\t */\n\tpublic static final int HTTP_FORBIDDEN = 403;\n\n\t/**\n\t * HTTP Status-Code 404: Not Found.\n\t */\n\tpublic static final int HTTP_NOT_FOUND = 404;\n\n\t/**\n\t * HTTP Status-Code 405: Method Not Allowed.\n\t */\n\tpublic static final int HTTP_BAD_METHOD = 405;\n\n\t/**\n\t * HTTP Status-Code 406: Not Acceptable.\n\t */\n\tpublic static final int HTTP_NOT_ACCEPTABLE = 406;\n\n\t/**\n\t * HTTP Status-Code 407: Proxy Authentication Required.\n\t */\n\tpublic static final int HTTP_PROXY_AUTH = 407;\n\n\t/**\n\t * HTTP Status-Code 408: Request Time-Out.\n\t */\n\tpublic static final int HTTP_CLIENT_TIMEOUT = 408;\n\n\t/**\n\t * HTTP Status-Code 409: Conflict.\n\t */\n\tpublic static final int HTTP_CONFLICT = 409;\n\n\t/**\n\t * HTTP Status-Code 410: Gone.\n\t */\n\tpublic static final int HTTP_GONE = 410;\n\n\t/**\n\t * HTTP Status-Code 411: Length Required.\n\t */\n\tpublic static final int HTTP_LENGTH_REQUIRED = 411;\n\n\t/**\n\t * HTTP Status-Code 412: Precondition Failed.\n\t */\n\tpublic static final int HTTP_PRECON_FAILED = 412;\n\n\t/**\n\t * HTTP Status-Code 413: Request Entity Too Large.\n\t */\n\tpublic static final int HTTP_ENTITY_TOO_LARGE = 413;\n\n\t/**\n\t * HTTP Status-Code 414: Request-URI Too Large.\n\t */\n\tpublic static final int HTTP_REQ_TOO_LONG = 414;\n\n\t/**\n\t * HTTP Status-Code 415: Unsupported Media Type.\n\t */\n\tpublic static final int HTTP_UNSUPPORTED_TYPE = 415;\n\n\t/**\n\t * HTTP Status-Code 416: Requested Range Not Satisfiable.\n\t */\n\tpublic static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;\n\n\t/**\n\t * HTTP Status-Code 417: Expectation Failed.\n\t */\n\tpublic static final int HTTP_EXPECTATION_FAILED = 417;\n\n\t/**\n\t * HTTP Status-Code 418: I'm a teapot.\n\t */\n\tpublic static final int HTTP_I_AM_A_TEAPOT = 418;\n\n\t/**\n\t * HTTP Status-Code 422: Unprocessable Entity.\n\t */\n\tpublic static final int HTTP_UNPROCESSABLE_ENTITY = 422;\n\n\t/**\n\t * HTTP Status-Code 423: Locked.\n\t */\n\tpublic static final int HTTP_LOCKED = 423;\n\n\t/**\n\t * HTTP Status-Code 424: Failed Dependency.\n\t */\n\tpublic static final int HTTP_FAILED_DEPENDENCY = 424;\n\n\t/**\n\t * HTTP Status-Code 425: Too Early.\n\t */\n\tpublic static final int HTTP_TOO_EARLY = 425;\n\n\t/**\n\t * HTTP Status-Code 426: Upgrade Required.\n\t */\n\tpublic static final int HTTP_UPGRADE_REQUIRED = 426;\n\n\t/**\n\t * HTTP Status-Code 428: Precondition Required.\n\t */\n\tpublic static final int HTTP_PRECONDITION_REQUIRED = 428;\n\n\t/**\n\t * HTTP Status-Code 429: Too Many Requests.\n\t */\n\tpublic static final int HTTP_TOO_MANY_REQUESTS = 429;\n\n\t/**\n\t * HTTP Status-Code 431: Request Header Fields Too Large.\n\t */\n\tpublic static final int HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;\n\n\t/**\n\t * HTTP Status-Code 451: Unavailable For Legal Reasons.\n\t */\n\tpublic static final int HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;\n\n\t/* 5XX: server error */\n\n\t/**\n\t * HTTP Status-Code 500: Internal Server Error.\n\t */\n\tpublic static final int HTTP_INTERNAL_ERROR = 500;\n\n\t/**\n\t * HTTP Status-Code 501: Not Implemented.\n\t */\n\tpublic static final int HTTP_NOT_IMPLEMENTED = 501;\n\n\t/**\n\t * HTTP Status-Code 502: Bad Gateway.\n\t */\n\tpublic static final int HTTP_BAD_GATEWAY = 502;\n\n\t/**\n\t * HTTP Status-Code 503: Service Unavailable.\n\t */\n\tpublic static final int HTTP_UNAVAILABLE = 503;\n\n\t/**\n\t * HTTP Status-Code 504: Gateway Timeout.\n\t */\n\tpublic static final int HTTP_GATEWAY_TIMEOUT = 504;\n\n\t/**\n\t * HTTP Status-Code 505: HTTP Version Not Supported.\n\t */\n\tpublic static final int HTTP_VERSION = 505;\n\n\t/**\n\t * HTTP Status-Code 506: Variant Also Negotiates.\n\t */\n\tpublic static final int HTTP_VARIANT_ALSO_NEGOTIATES = 506;\n\n\t/**\n\t * HTTP Status-Code 507: Insufficient Storage.\n\t */\n\tpublic static final int HTTP_INSUFFICIENT_STORAGE = 507;\n\n\t/**\n\t * HTTP Status-Code 508: Loop Detected.\n\t */\n\tpublic static final int HTTP_LOOP_DETECTED = 508;\n\n\t/**\n\t * HTTP Status-Code 509: Bandwidth Limit Exceeded.\n\t */\n\tpublic static final int HTTP_BANDWIDTH_LIMIT_EXCEEDED = 509;\n\n\t/**\n\t * HTTP Status-Code 510: Not Extended.\n\t */\n\tpublic static final int HTTP_NOT_EXTENDED = 510;\n\n\t/**\n\t * HTTP Status-Code 511: Network Authentication Required.\n\t */\n\tpublic static final int HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;\n\n\t/**\n\t * 是否为重定向状态码\n\t * @param responseCode 被检查的状态码\n\t * @return 是否为重定向状态码\n\t * @since 5.6.3\n\t */\n\tpublic static boolean isRedirected(int responseCode){\n\t\treturn responseCode == HTTP_MOVED_PERM\n\t\t\t\t|| responseCode == HTTP_MOVED_TEMP\n\t\t\t\t|| responseCode == HTTP_SEE_OTHER\n\t\t\t\t// issue#1504@Github，307和308是RFC 7538中http 1.1定义的规范\n\t\t\t\t|| responseCode == HTTP_TEMP_REDIRECT\n\t\t\t\t|| responseCode == HTTP_PERMANENT_REDIRECT;\n\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/HttpUtil.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.net.RFC3986;\nimport cn.hutool.core.net.url.UrlQuery;\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.util.*;\nimport cn.hutool.http.cookie.GlobalCookieManager;\nimport cn.hutool.http.server.SimpleServer;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.CookieManager;\nimport java.net.HttpURLConnection;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\n/**\n * Http请求工具类\n *\n * @author xiaoleilu\n */\npublic class HttpUtil {\n\n\t/**\n\t * 正则：Content-Type中的编码信息\n\t */\n\tpublic static final Pattern CHARSET_PATTERN = Pattern.compile(\"charset\\\\s*=\\\\s*([a-z0-9-]*)\", Pattern.CASE_INSENSITIVE);\n\t/**\n\t * 正则：匹配meta标签的编码信息\n\t */\n\tpublic static final Pattern META_CHARSET_PATTERN = Pattern.compile(\"<meta[^>]*?charset\\\\s*=\\\\s*['\\\"]?([a-z0-9-]*)\", Pattern.CASE_INSENSITIVE);\n\n\t/**\n\t * 检测是否https\n\t *\n\t * @param url URL\n\t * @return 是否https\n\t */\n\tpublic static boolean isHttps(String url) {\n\t\treturn StrUtil.startWithIgnoreCase(url, \"https:\");\n\t}\n\n\t/**\n\t * 检测是否http\n\t *\n\t * @param url URL\n\t * @return 是否http\n\t * @since 5.3.8\n\t */\n\tpublic static boolean isHttp(String url) {\n\t\treturn StrUtil.startWithIgnoreCase(url, \"http:\");\n\t}\n\n\t/**\n\t * 创建Http请求对象\n\t *\n\t * @param method 方法枚举{@link Method}\n\t * @param url    请求的URL，可以使HTTP或者HTTPS\n\t * @return {@link HttpRequest}\n\t * @since 3.0.9\n\t */\n\tpublic static HttpRequest createRequest(Method method, String url) {\n\t\treturn HttpRequest.of(url).method(method);\n\t}\n\n\t/**\n\t * 创建Http GET请求对象\n\t *\n\t * @param url 请求的URL，可以使HTTP或者HTTPS\n\t * @return {@link HttpRequest}\n\t * @since 3.2.0\n\t */\n\tpublic static HttpRequest createGet(String url) {\n\t\treturn createGet(url, false);\n\t}\n\n\t/**\n\t * 创建Http GET请求对象\n\t *\n\t * @param url               请求的URL，可以使HTTP或者HTTPS\n\t * @param isFollowRedirects 是否打开重定向\n\t * @return {@link HttpRequest}\n\t * @since 5.6.4\n\t */\n\tpublic static HttpRequest createGet(String url, boolean isFollowRedirects) {\n\t\treturn HttpRequest.get(url).setFollowRedirects(isFollowRedirects);\n\t}\n\n\t/**\n\t * 创建Http POST请求对象\n\t *\n\t * @param url 请求的URL，可以使HTTP或者HTTPS\n\t * @return {@link HttpRequest}\n\t * @since 3.2.0\n\t */\n\tpublic static HttpRequest createPost(String url) {\n\t\treturn HttpRequest.post(url);\n\t}\n\n\t/**\n\t * 发送get请求\n\t *\n\t * @param urlString     网址\n\t * @param customCharset 自定义请求字符集，如果字符集获取不到，使用此字符集\n\t * @return 返回内容，如果只检查状态码，正常只返回 \"\"，不正常返回 null\n\t */\n\tpublic static String get(String urlString, Charset customCharset) {\n\t\treturn HttpRequest.get(urlString).charset(customCharset).execute().body();\n\t}\n\n\t/**\n\t * 发送get请求\n\t *\n\t * @param urlString 网址\n\t * @return 返回内容，如果只检查状态码，正常只返回 \"\"，不正常返回 null\n\t */\n\tpublic static String get(String urlString) {\n\t\treturn get(urlString, HttpGlobalConfig.getTimeout());\n\t}\n\n\t/**\n\t * 发送get请求\n\t *\n\t * @param urlString 网址\n\t * @param timeout   超时时长，-1表示默认超时，单位毫秒\n\t * @return 返回内容，如果只检查状态码，正常只返回 \"\"，不正常返回 null\n\t * @since 3.2.0\n\t */\n\tpublic static String get(String urlString, int timeout) {\n\t\treturn HttpRequest.get(urlString).timeout(timeout).execute().body();\n\t}\n\n\t/**\n\t * 发送get请求\n\t *\n\t * @param urlString 网址\n\t * @param paramMap  post表单数据\n\t * @return 返回数据\n\t */\n\tpublic static String get(String urlString, Map<String, Object> paramMap) {\n\t\treturn HttpRequest.get(urlString).form(paramMap).execute().body();\n\t}\n\n\t/**\n\t * 发送get请求\n\t *\n\t * @param urlString 网址\n\t * @param paramMap  post表单数据\n\t * @param timeout   超时时长，-1表示默认超时，单位毫秒\n\t * @return 返回数据\n\t * @since 3.3.0\n\t */\n\tpublic static String get(String urlString, Map<String, Object> paramMap, int timeout) {\n\t\treturn HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body();\n\t}\n\n\t/**\n\t * 发送post请求\n\t *\n\t * @param urlString 网址\n\t * @param paramMap  post表单数据\n\t * @return 返回数据\n\t */\n\tpublic static String post(String urlString, Map<String, Object> paramMap) {\n\t\treturn post(urlString, paramMap, HttpGlobalConfig.getTimeout());\n\t}\n\n\t/**\n\t * 发送post请求\n\t *\n\t * @param urlString 网址\n\t * @param paramMap  post表单数据\n\t * @param timeout   超时时长，-1表示默认超时，单位毫秒\n\t * @return 返回数据\n\t * @since 3.2.0\n\t */\n\tpublic static String post(String urlString, Map<String, Object> paramMap, int timeout) {\n\t\treturn HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body();\n\t}\n\n\t/**\n\t * 发送post请求<br>\n\t * 请求体body参数支持两种类型：\n\t *\n\t * <pre>\n\t * 1. 标准参数，例如 a=1&amp;b=2 这种格式\n\t * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type\n\t * </pre>\n\t *\n\t * @param urlString 网址\n\t * @param body      post表单数据\n\t * @return 返回数据\n\t */\n\tpublic static String post(String urlString, String body) {\n\t\treturn post(urlString, body, HttpGlobalConfig.getTimeout());\n\t}\n\n\t/**\n\t * 发送post请求<br>\n\t * 请求体body参数支持两种类型：\n\t *\n\t * <pre>\n\t * 1. 标准参数，例如 a=1&amp;b=2 这种格式\n\t * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type\n\t * </pre>\n\t *\n\t * @param urlString 网址\n\t * @param body      post表单数据\n\t * @param timeout   超时时长，-1表示默认超时，单位毫秒\n\t * @return 返回数据\n\t * @since 3.2.0\n\t */\n\tpublic static String post(String urlString, String body, int timeout) {\n\t\treturn HttpRequest.post(urlString).timeout(timeout).body(body).execute().body();\n\t}\n\n\t// ---------------------------------------------------------------------------------------- download\n\n\t/**\n\t * 下载远程文本\n\t *\n\t * @param url               请求的url\n\t * @param customCharsetName 自定义的字符集\n\t * @return 文本\n\t */\n\tpublic static String downloadString(String url, String customCharsetName) {\n\t\treturn downloadString(url, CharsetUtil.charset(customCharsetName), null);\n\t}\n\n\t/**\n\t * 下载远程文本\n\t *\n\t * @param url           请求的url\n\t * @param customCharset 自定义的字符集，可以使用{@link CharsetUtil#charset} 方法转换\n\t * @return 文本\n\t */\n\tpublic static String downloadString(String url, Charset customCharset) {\n\t\treturn downloadString(url, customCharset, null);\n\t}\n\n\t/**\n\t * 下载远程文本\n\t *\n\t * @param url           请求的url\n\t * @param customCharset 自定义的字符集，可以使用{@link CharsetUtil#charset} 方法转换\n\t * @param streamPress   进度条 {@link StreamProgress}\n\t * @return 文本\n\t */\n\tpublic static String downloadString(String url, Charset customCharset, StreamProgress streamPress) {\n\t\treturn HttpDownloader.downloadString(url, customCharset, streamPress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url  请求的url\n\t * @param dest 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @return 文件大小\n\t */\n\tpublic static long downloadFile(String url, String dest) {\n\t\treturn downloadFile(url, FileUtil.file(dest));\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url      请求的url\n\t * @param destFile 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @return 文件大小\n\t */\n\tpublic static long downloadFile(String url, File destFile) {\n\t\treturn downloadFile(url, destFile, null);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url      请求的url\n\t * @param destFile 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout  超时，单位毫秒，-1表示默认超时\n\t * @return 文件大小\n\t * @since 4.0.4\n\t */\n\tpublic static long downloadFile(String url, File destFile, int timeout) {\n\t\treturn downloadFile(url, destFile, timeout, null);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param destFile       目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param streamProgress 进度条\n\t * @return 文件大小\n\t */\n\tpublic static long downloadFile(String url, File destFile, StreamProgress streamProgress) {\n\t\treturn downloadFile(url, destFile, -1, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param destFile       目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout        超时，单位毫秒，-1表示默认超时\n\t * @param streamProgress 进度条\n\t * @return 文件大小\n\t * @since 4.0.4\n\t */\n\tpublic static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) {\n\t\treturn HttpDownloader.downloadFile(url, destFile, timeout, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url  请求的url\n\t * @param dest 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @return 下载的文件对象\n\t * @since 5.4.1\n\t */\n\tpublic static File downloadFileFromUrl(String url, String dest) {\n\t\treturn downloadFileFromUrl(url, FileUtil.file(dest));\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url      请求的url\n\t * @param destFile 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @return 下载的文件对象\n\t * @since 5.4.1\n\t */\n\tpublic static File downloadFileFromUrl(String url, File destFile) {\n\t\treturn downloadFileFromUrl(url, destFile, null);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url      请求的url\n\t * @param destFile 目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout  超时，单位毫秒，-1表示默认超时\n\t * @return 下载的文件对象\n\t * @since 5.4.1\n\t */\n\tpublic static File downloadFileFromUrl(String url, File destFile, int timeout) {\n\t\treturn downloadFileFromUrl(url, destFile, timeout, null);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param destFile       目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param streamProgress 进度条\n\t * @return 下载的文件对象\n\t * @since 5.4.1\n\t */\n\tpublic static File downloadFileFromUrl(String url, File destFile, StreamProgress streamProgress) {\n\t\treturn downloadFileFromUrl(url, destFile, -1, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param destFile       目标文件或目录，当为目录时，取URL中的文件名，取不到使用编码后的URL做为文件名\n\t * @param timeout        超时，单位毫秒，-1表示默认超时\n\t * @param streamProgress 进度条\n\t * @return 下载的文件对象\n\t * @since 5.4.1\n\t */\n\tpublic static File downloadFileFromUrl(String url, File destFile, int timeout, StreamProgress streamProgress) {\n\t\treturn HttpDownloader.downloadForFile(url, destFile, timeout, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url        请求的url\n\t * @param out        将下载内容写到输出流中 {@link OutputStream}\n\t * @param isCloseOut 是否关闭输出流\n\t * @return 文件大小\n\t */\n\tpublic static long download(String url, OutputStream out, boolean isCloseOut) {\n\t\treturn download(url, out, isCloseOut, null);\n\t}\n\n\t/**\n\t * 下载远程文件\n\t *\n\t * @param url            请求的url\n\t * @param out            将下载内容写到输出流中 {@link OutputStream}\n\t * @param isCloseOut     是否关闭输出流\n\t * @param streamProgress 进度条\n\t * @return 文件大小\n\t */\n\tpublic static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {\n\t\treturn HttpDownloader.download(url, out, isCloseOut, streamProgress);\n\t}\n\n\t/**\n\t * 下载远程文件数据，支持30x跳转\n\t *\n\t * @param url 请求的url\n\t * @return 文件数据\n\t * @since 5.3.6\n\t */\n\tpublic static byte[] downloadBytes(String url) {\n\t\treturn HttpDownloader.downloadBytes(url);\n\t}\n\n\t/**\n\t * 将Map形式的Form表单数据转换为Url参数形式，会自动url编码键和值\n\t *\n\t * @param paramMap 表单数据\n\t * @return url参数\n\t */\n\tpublic static String toParams(Map<String, ?> paramMap) {\n\t\treturn toParams(paramMap, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * 将Map形式的Form表单数据转换为Url参数形式<br>\n\t * 编码键和值对\n\t *\n\t * @param paramMap    表单数据\n\t * @param charsetName 编码\n\t * @return url参数\n\t * @deprecated 请使用 {@link #toParams(Map, Charset)}\n\t */\n\t@Deprecated\n\tpublic static String toParams(Map<String, Object> paramMap, String charsetName) {\n\t\treturn toParams(paramMap, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 将Map形式的Form表单数据转换为Url参数形式<br>\n\t * paramMap中如果key为空（null和\"\"）会被忽略，如果value为null，会被做为空白符（\"\"）<br>\n\t * 会自动url编码键和值<br>\n\t * 此方法用于拼接URL中的Query部分，并不适用于POST请求中的表单\n\t *\n\t * <pre>\n\t * key1=v1&amp;key2=&amp;key3=v3\n\t * </pre>\n\t *\n\t * @param paramMap 表单数据\n\t * @param charset  编码，{@code null} 表示不encode键值对\n\t * @return url参数\n\t * @see #toParams(Map, Charset, boolean)\n\t */\n\tpublic static String toParams(Map<String, ?> paramMap, Charset charset) {\n\t\treturn toParams(paramMap, charset, false);\n\t}\n\n\t/**\n\t * 将Map形式的Form表单数据转换为Url参数形式<br>\n\t * paramMap中如果key为空（null和\"\"）会被忽略，如果value为null，会被做为空白符（\"\"）<br>\n\t * 会自动url编码键和值\n\t *\n\t * <pre>\n\t * key1=v1&amp;key2=&amp;key3=v3\n\t * </pre>\n\t *\n\t * @param paramMap 表单数据\n\t * @param charset  编码，null表示不encode键值对\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @return url参数\n\t * @since 5.7.16\n\t */\n\tpublic static String toParams(Map<String, ?> paramMap, Charset charset, boolean isFormUrlEncoded) {\n\t\treturn UrlQuery.of(paramMap, isFormUrlEncoded).build(charset);\n\t}\n\n\t/**\n\t * 对URL参数做编码，只编码键和值<br>\n\t * 提供的值可以是url附带参数，但是不能只是url\n\t *\n\t * <p>注意，此方法只能标准化整个URL，并不适合于单独编码参数值</p>\n\t *\n\t * @param urlWithParams url和参数，可以包含url本身，也可以单独参数\n\t * @param charset       编码\n\t * @return 编码后的url和参数\n\t * @since 4.0.1\n\t */\n\tpublic static String encodeParams(String urlWithParams, Charset charset) {\n\t\tif (StrUtil.isBlank(urlWithParams)) {\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\n\t\tString urlPart = null; // url部分，不包括问号\n\t\tString paramPart; // 参数部分\n\t\tfinal int pathEndPos = urlWithParams.indexOf('?');\n\t\tif (pathEndPos > -1) {\n\t\t\t// url + 参数\n\t\t\turlPart = StrUtil.subPre(urlWithParams, pathEndPos);\n\t\t\tparamPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1);\n\t\t\tif (StrUtil.isBlank(paramPart)) {\n\t\t\t\t// 无参数，返回url\n\t\t\t\treturn urlPart;\n\t\t\t}\n\t\t} else if (false == StrUtil.contains(urlWithParams, '=')) {\n\t\t\t// 无参数的URL\n\t\t\treturn urlWithParams;\n\t\t} else {\n\t\t\t// 无URL的参数\n\t\t\tparamPart = urlWithParams;\n\t\t}\n\n\t\tparamPart = normalizeParams(paramPart, charset);\n\n\t\treturn StrUtil.isBlank(urlPart) ? paramPart : urlPart + \"?\" + paramPart;\n\t}\n\n\t/**\n\t * 标准化参数字符串，即URL中？后的部分\n\t *\n\t * <p>注意，此方法只能标准化整个URL，并不适合于单独编码参数值</p>\n\t *\n\t * @param paramPart 参数字符串\n\t * @param charset   编码\n\t * @return 标准化的参数字符串\n\t * @since 4.5.2\n\t */\n\tpublic static String normalizeParams(String paramPart, Charset charset) {\n\t\tif(StrUtil.isEmpty(paramPart)){\n\t\t\treturn paramPart;\n\t\t}\n\t\tfinal StrBuilder builder = StrBuilder.create(paramPart.length() + 16);\n\t\tfinal int len = paramPart.length();\n\t\tString name = null;\n\t\tint pos = 0; // 未处理字符开始位置\n\t\tchar c; // 当前字符\n\t\tint i; // 当前字符位置\n\t\tfor (i = 0; i < len; i++) {\n\t\t\tc = paramPart.charAt(i);\n\t\t\tif (c == '=') { // 键值对的分界点\n\t\t\t\tif (null == name) {\n\t\t\t\t\t// 只有=前未定义name时被当作键值分界符，否则做为普通字符\n\t\t\t\t\tname = (pos == i) ? StrUtil.EMPTY : paramPart.substring(pos, i);\n\t\t\t\t\tpos = i + 1;\n\t\t\t\t}\n\t\t\t} else if (c == '&') { // 参数对的分界点\n\t\t\t\tif (null == name) {\n\t\t\t\t\t// 对于像&a&这类无参数值的字符串，我们将name为a的值设为\"\"\n\t\t\t\t\tif(pos != i){\n\t\t\t\t\t\tname = paramPart.substring(pos, i);\n\t\t\t\t\t\tbuilder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tbuilder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')\n\t\t\t\t\t\t.append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&');\n\t\t\t\t}\n\t\t\t\tname = null;\n\t\t\t\tpos = i + 1;\n\t\t\t}\n\t\t}\n\n\t\t// 结尾处理\n\t\tif (null != name) {\n\t\t\tbuilder.append(URLUtil.encodeQuery(name, charset)).append('=');\n\t\t}\n\t\tif (pos != i) {\n\t\t\tif (null == name && pos > 0) {\n\t\t\t\tbuilder.append('=');\n\t\t\t}\n\t\t\tbuilder.append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset));\n\t\t}\n\n\t\t// 以&结尾则去除之\n\t\tif (!builder.isEmpty()) {\n\t\t\tint lastIndex = builder.length() - 1;\n\t\t\tif ('&' == builder.charAt(lastIndex)) {\n\t\t\t\tbuilder.delTo(lastIndex);\n\t\t\t}\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 将URL参数解析为Map（也可以解析Post中的键值对参数）\n\t *\n\t * @param paramsStr 参数字符串（或者带参数的Path）\n\t * @param charset   字符集\n\t * @return 参数Map\n\t * @since 5.2.6\n\t */\n\tpublic static Map<String, String> decodeParamMap(String paramsStr, Charset charset) {\n\t\tfinal Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();\n\t\tif (MapUtil.isEmpty(queryMap)) {\n\t\t\treturn MapUtil.empty();\n\t\t}\n\t\treturn Convert.toMap(String.class, String.class, queryMap);\n\t}\n\n\t/**\n\t * 将URL参数解析为Map（也可以解析Post中的键值对参数）\n\t *\n\t * @param paramsStr 参数字符串（或者带参数的Path）\n\t * @param charset   字符集\n\t * @return 参数Map\n\t */\n\tpublic static Map<String, List<String>> decodeParams(String paramsStr, String charset) {\n\t\treturn decodeParams(paramsStr, charset, false);\n\t}\n\n\t/**\n\t * 将URL参数解析为Map（也可以解析Post中的键值对参数）\n\t *\n\t * @param paramsStr 参数字符串（或者带参数的Path）\n\t * @param charset   字符集\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @return 参数Map\n\t * @since 5.8.12\n\t */\n\tpublic static Map<String, List<String>> decodeParams(String paramsStr, String charset, boolean isFormUrlEncoded) {\n\t\treturn decodeParams(paramsStr, CharsetUtil.charset(charset), isFormUrlEncoded);\n\t}\n\n\t/**\n\t * 将URL QueryString参数解析为Map\n\t *\n\t * @param paramsStr 参数字符串（或者带参数的Path）\n\t * @param charset   字符集\n\t * @return 参数Map\n\t * @since 5.2.6\n\t */\n\tpublic static Map<String, List<String>> decodeParams(String paramsStr, Charset charset) {\n\t\treturn decodeParams(paramsStr, charset, false);\n\t}\n\n\t/**\n\t * 将URL参数解析为Map（也可以解析Post中的键值对参数）\n\t *\n\t * @param paramsStr 参数字符串（或者带参数的Path）\n\t * @param charset   字符集\n\t * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t * @return 参数Map\n\t */\n\tpublic static Map<String, List<String>> decodeParams(String paramsStr, Charset charset, boolean isFormUrlEncoded) {\n\t\tfinal Map<CharSequence, CharSequence> queryMap =\n\t\t\t\tUrlQuery.of(paramsStr, charset, true, isFormUrlEncoded).getQueryMap();\n\t\tif (MapUtil.isEmpty(queryMap)) {\n\t\t\treturn MapUtil.empty();\n\t\t}\n\n\t\tfinal Map<String, List<String>> params = new LinkedHashMap<>();\n\t\tqueryMap.forEach((key, value) -> {\n\t\t\tfinal List<String> values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1));\n\t\t\t// 一般是一个参数\n\t\t\tvalues.add(StrUtil.str(value));\n\t\t});\n\t\treturn params;\n\t}\n\n\t/**\n\t * 将表单数据加到URL中（用于GET表单提交）<br>\n\t * 表单的键值对会被url编码，但是url中原参数不会被编码\n\t *\n\t * @param url            URL\n\t * @param form           表单数据\n\t * @param charset        编码\n\t * @param isEncodeParams 是否对键和值做转义处理\n\t * @return 合成后的URL\n\t */\n\tpublic static String urlWithForm(String url, Map<String, Object> form, Charset charset, boolean isEncodeParams) {\n\t\t// url和参数是分别编码的\n\t\treturn urlWithForm(url, toParams(form, charset), charset, isEncodeParams);\n\t}\n\n\t/**\n\t * 将表单数据加到URL中（用于GET表单提交）\n\t * 表单的键值对会被url编码，但是url中原参数不会被编码\n\t *  且对form参数进行  FormUrlEncoded ，x-www-form-urlencoded模式，此模式下空格会编码为'+'\n\t *\n\t * @param url            URL\n\t * @param form           表单数据\n\t * @param charset        编码   null表示不encode键值对\n\t * @return 合成后的URL\n\t */\n\tpublic static String urlWithFormUrlEncoded(String url, Map<String, Object> form, Charset charset) {\n\t\t// issue#3536，urlWithForm会对参数进行一次编码，因此toParams无需编码\n\t\treturn urlWithForm(url, toParams(form, null, false), charset, true);\n\t}\n\n\t/**\n\t * 将表单数据字符串加到URL中（用于GET表单提交）\n\t *\n\t * @param url         URL\n\t * @param queryString 表单数据字符串\n\t * @param charset     编码\n\t * @param isEncode    是否对键和值做转义处理\n\t * @return 拼接后的字符串\n\t */\n\tpublic static String urlWithForm(String url, String queryString, Charset charset, boolean isEncode) {\n\t\tif (StrUtil.isBlank(queryString)) {\n\t\t\t// 无额外参数\n\t\t\tif (StrUtil.contains(url, '?')) {\n\t\t\t\t// url中包含参数\n\t\t\t\treturn isEncode ? encodeParams(url, charset) : url;\n\t\t\t}\n\t\t\treturn url;\n\t\t}\n\n\t\t// 始终有参数\n\t\tfinal StrBuilder urlBuilder = StrBuilder.create(url.length() + queryString.length() + 16);\n\t\tint qmIndex = url.indexOf('?');\n\t\tif (qmIndex > 0) {\n\t\t\t// 原URL带参数，则对这部分参数单独编码（如果选项为进行编码）\n\t\t\turlBuilder.append(isEncode ? encodeParams(url, charset) : url);\n\t\t\tif (false == StrUtil.endWith(url, '&')) {\n\t\t\t\t// 已经带参数的情况下追加参数\n\t\t\t\turlBuilder.append('&');\n\t\t\t}\n\t\t} else {\n\t\t\t// 原url无参数，则不做编码\n\t\t\turlBuilder.append(url);\n\t\t\tif (qmIndex < 0) {\n\t\t\t\t// 无 '?' 追加之\n\t\t\t\turlBuilder.append('?');\n\t\t\t}\n\t\t}\n\t\turlBuilder.append(isEncode ? encodeParams(queryString, charset) : queryString);\n\t\treturn urlBuilder.toString();\n\t}\n\n\t/**\n\t * 从Http连接的头信息中获得字符集<br>\n\t * 从ContentType中获取\n\t *\n\t * @param conn HTTP连接对象\n\t * @return 字符集\n\t */\n\tpublic static String getCharset(HttpURLConnection conn) {\n\t\tif (conn == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getCharset(conn.getContentType());\n\t}\n\n\t/**\n\t * 从Http连接的头信息中获得字符集<br>\n\t * 从ContentType中获取\n\t *\n\t * @param contentType Content-Type\n\t * @return 字符集\n\t * @since 5.2.6\n\t */\n\tpublic static String getCharset(String contentType) {\n\t\tif (StrUtil.isBlank(contentType)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ReUtil.get(CHARSET_PATTERN, contentType, 1);\n\t}\n\n\t/**\n\t * 从流中读取内容<br>\n\t * 首先尝试使用charset编码读取内容（如果为空默认UTF-8），如果isGetCharsetFromContent为true，则通过正则在正文中获取编码信息，转换为指定编码；\n\t *\n\t * @param in                      输入流\n\t * @param charset                 字符集\n\t * @param isGetCharsetFromContent 是否从返回内容中获得编码信息\n\t * @return 内容\n\t */\n\tpublic static String getString(InputStream in, Charset charset, boolean isGetCharsetFromContent) {\n\t\tfinal byte[] contentBytes = IoUtil.readBytes(in);\n\t\treturn getString(contentBytes, charset, isGetCharsetFromContent);\n\t}\n\n\t/**\n\t * 从流中读取内容<br>\n\t * 首先尝试使用charset编码读取内容（如果为空默认UTF-8），如果isGetCharsetFromContent为true，则通过正则在正文中获取编码信息，转换为指定编码；\n\t *\n\t * @param contentBytes            内容byte数组\n\t * @param charset                 字符集\n\t * @param isGetCharsetFromContent 是否从返回内容中获得编码信息\n\t * @return 内容\n\t */\n\tpublic static String getString(byte[] contentBytes, Charset charset, boolean isGetCharsetFromContent) {\n\t\tif (null == contentBytes) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (null == charset) {\n\t\t\tcharset = CharsetUtil.CHARSET_UTF_8;\n\t\t}\n\t\tString content = new String(contentBytes, charset);\n\t\tif (isGetCharsetFromContent) {\n\t\t\tfinal String charsetInContentStr = ReUtil.get(META_CHARSET_PATTERN, content, 1);\n\t\t\tif (StrUtil.isNotBlank(charsetInContentStr)) {\n\t\t\t\tCharset charsetInContent = null;\n\t\t\t\ttry {\n\t\t\t\t\tcharsetInContent = Charset.forName(charsetInContentStr);\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tif (StrUtil.containsIgnoreCase(charsetInContentStr, \"utf-8\") || StrUtil.containsIgnoreCase(charsetInContentStr, \"utf8\")) {\n\t\t\t\t\t\tcharsetInContent = CharsetUtil.CHARSET_UTF_8;\n\t\t\t\t\t} else if (StrUtil.containsIgnoreCase(charsetInContentStr, \"gbk\")) {\n\t\t\t\t\t\tcharsetInContent = CharsetUtil.CHARSET_GBK;\n\t\t\t\t\t}\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t\tif (null != charsetInContent && false == charset.equals(charsetInContent)) {\n\t\t\t\t\tcontent = new String(contentBytes, charsetInContent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn content;\n\t}\n\n\t/**\n\t * 根据文件扩展名获得MimeType\n\t *\n\t * @param filePath     文件路径或文件名\n\t * @param defaultValue 当获取MimeType为null时的默认值\n\t * @return MimeType\n\t * @see FileUtil#getMimeType(String)\n\t * @since 4.6.5\n\t */\n\tpublic static String getMimeType(String filePath, String defaultValue) {\n\t\treturn ObjectUtil.defaultIfNull(getMimeType(filePath), defaultValue);\n\t}\n\n\t/**\n\t * 根据文件扩展名获得MimeType\n\t *\n\t * @param filePath 文件路径或文件名\n\t * @return MimeType\n\t * @see FileUtil#getMimeType(String)\n\t */\n\tpublic static String getMimeType(String filePath) {\n\t\treturn FileUtil.getMimeType(filePath);\n\t}\n\n\t/**\n\t * 从请求参数的body中判断请求的Content-Type类型，支持的类型有：\n\t *\n\t * <pre>\n\t * 1. application/json\n\t * 1. application/xml\n\t * </pre>\n\t *\n\t * @param body 请求参数体\n\t * @return Content-Type类型，如果无法判断返回null\n\t * @see ContentType#get(String)\n\t * @since 3.2.0\n\t */\n\tpublic static String getContentTypeByRequestBody(String body) {\n\t\tfinal ContentType contentType = ContentType.get(body);\n\t\treturn (null == contentType) ? null : contentType.toString();\n\t}\n\n\t/**\n\t * 创建简易的Http服务器\n\t *\n\t * @param port 端口\n\t * @return {@link SimpleServer}\n\t * @since 5.2.6\n\t */\n\tpublic static SimpleServer createServer(int port) {\n\t\treturn new SimpleServer(port);\n\t}\n\n\t/**\n\t * 构建简单的账号秘密验证信息，构建后类似于：\n\t * <pre>\n\t *     Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\t * </pre>\n\t *\n\t * @param username 账号\n\t * @param password 密码\n\t * @param charset  编码（如果账号或密码中有非ASCII字符适用）\n\t * @return 密码验证信息\n\t * @since 5.4.6\n\t */\n\tpublic static String buildBasicAuth(String username, String password, Charset charset) {\n\t\tAssert.notNull(username, \"username must not be null\");\n\t\tAssert.notNull(password, \"password must not be null\");\n\t\tfinal String data = username.concat(\":\").concat(password);\n\t\treturn \"Basic \" + Base64.encode(data, charset);\n\t}\n\n\t/**\n\t * 关闭Cookie\n\t *\n\t * @see GlobalCookieManager#setCookieManager(CookieManager)\n\t * @since 5.6.5\n\t */\n\tpublic static void closeCookie() {\n\t\tGlobalCookieManager.setCookieManager(null);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/Method.java",
    "content": "package cn.hutool.http;\n\n/**\n * Http方法枚举\n *\n * @author Looly\n */\npublic enum Method {\n\tGET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/MultipartOutputStream.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.MultiResource;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.io.resource.StringResource;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Multipart/form-data输出流封装<br>\n * 遵循RFC2388规范\n *\n * @author looly\n * @since 5.7.17\n */\npublic class MultipartOutputStream extends OutputStream {\n\n\tprivate static final String CONTENT_DISPOSITION_TEMPLATE = \"Content-Disposition: form-data; name=\\\"{}\\\"\\r\\n\";\n\tprivate static final String CONTENT_DISPOSITION_FILE_TEMPLATE = \"Content-Disposition: form-data; name=\\\"{}\\\"; filename=\\\"{}\\\"\\r\\n\";\n\n\tprivate static final String CONTENT_TYPE_FILE_TEMPLATE = \"Content-Type: {}\\r\\n\";\n\n\tprivate final OutputStream out;\n\tprivate final Charset charset;\n\tprivate final String boundary;\n\n\tprivate boolean isFinish;\n\n\t/**\n\t * 构造，使用全局默认的边界字符串\n\t *\n\t * @param out     HTTP写出流\n\t * @param charset 编码\n\t */\n\tpublic MultipartOutputStream(OutputStream out, Charset charset) {\n\t\tthis(out, charset, HttpGlobalConfig.getBoundary());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param out      HTTP写出流\n\t * @param charset  编码\n\t * @param boundary 边界符\n\t * @since 5.7.17\n\t */\n\tpublic MultipartOutputStream(OutputStream out, Charset charset, String boundary) {\n\t\tthis.out = out;\n\t\tthis.charset = charset;\n\t\tthis.boundary = boundary;\n\t}\n\n\t/**\n\t * 添加Multipart表单的数据项<br>\n\t * <pre>\n\t *     --分隔符(boundary)[换行]\n\t *     Content-Disposition: form-data; name=\"参数名\"[换行]\n\t *     [换行]\n\t *     参数值[换行]\n\t * </pre>\n\t * <p>\n\t * 或者：\n\t *\n\t * <pre>\n\t *     --分隔符(boundary)[换行]\n\t *     Content-Disposition: form-data; name=\"表单名\"; filename=\"文件名\"[换行]\n\t *     Content-Type: MIME类型[换行]\n\t *     [换行]\n\t *     文件的二进制内容[换行]\n\t * </pre>\n\t *\n\t * @param formFieldName 表单名\n\t * @param value         值，可以是普通值、资源（如文件等）\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic MultipartOutputStream write(String formFieldName, Object value) throws IORuntimeException {\n\t\t// 多资源\n\t\tif (value instanceof MultiResource) {\n\t\t\tfor (Resource subResource : (MultiResource) value) {\n\t\t\t\twrite(formFieldName, subResource);\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\t// --分隔符(boundary)[换行]\n\t\tbeginPart();\n\n\t\tif (value instanceof Resource) {\n\t\t\tappendResource(formFieldName, (Resource) value);\n\t\t} else {\n\t\t\tappendResource(formFieldName,\n\t\t\t\t\tnew StringResource(Convert.toStr(value), null, this.charset));\n\t\t}\n\n\t\twrite(StrUtil.CRLF);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void write(int b) throws IOException {\n\t\tthis.out.write(b);\n\t}\n\n\t/**\n\t * 上传表单结束\n\t *\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic void finish() throws IORuntimeException {\n\t\tif (false == isFinish) {\n\t\t\twrite(StrUtil.format(\"--{}--\\r\\n\", boundary));\n\t\t\tthis.isFinish = true;\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tfinish();\n\t\tIoUtil.close(this.out);\n\t}\n\n\t/**\n\t * 添加Multipart表单的Resource数据项，支持包括{@link HttpResource}资源格式\n\t *\n\t * @param formFieldName 表单名\n\t * @param resource      资源\n\t * @throws IORuntimeException IO异常\n\t */\n\tprivate void appendResource(String formFieldName, Resource resource) throws IORuntimeException {\n\t\tfinal String fileName = resource.getName();\n\n\t\t// Content-Disposition\n\t\tif (null == fileName) {\n\t\t\t// Content-Disposition: form-data; name=\"参数名\"[换行]\n\t\t\twrite(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName));\n\t\t} else {\n\t\t\t// Content-Disposition: form-data; name=\"参数名\"; filename=\"文件名\"[换行]\n\t\t\twrite(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, fileName));\n\t\t}\n\n\t\t// Content-Type\n\t\tif (resource instanceof HttpResource) {\n\t\t\tfinal String contentType = ((HttpResource) resource).getContentType();\n\t\t\tif (StrUtil.isNotBlank(contentType)) {\n\t\t\t\t// Content-Type: 类型[换行]\n\t\t\t\twrite(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, contentType));\n\t\t\t}\n\t\t} else if (StrUtil.isNotEmpty(fileName)) {\n\t\t\t// 根据name的扩展名指定互联网媒体类型，默认二进制流数据\n\t\t\twrite(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE,\n\t\t\t\t\tHttpUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue())));\n\t\t}\n\n\t\t// 内容\n\t\twrite(\"\\r\\n\");\n\t\tresource.writeTo(this);\n\t}\n\n\t/**\n\t * part开始，写出:<br>\n\t * <pre>\n\t *     --分隔符(boundary)[换行]\n\t * </pre>\n\t */\n\tprivate void beginPart() {\n\t\t// --分隔符(boundary)[换行]\n\t\twrite(\"--\", boundary, StrUtil.CRLF);\n\t}\n\n\t/**\n\t * 写出对象\n\t *\n\t * @param objs 写出的对象（转换为字符串）\n\t */\n\tprivate void write(Object... objs) {\n\t\tIoUtil.write(this, this.charset, false, objs);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/Status.java",
    "content": "package cn.hutool.http;\n\n/**\n * 返回状态码\n * @author Looly\n *\n */\ninterface Status {\n\t/**\n\t * HTTP Status-Code 200: OK.\n\t */\n\tint HTTP_OK = 200;\n\n\t/**\n\t * HTTP Status-Code 201: Created.\n\t */\n\tint HTTP_CREATED = 201;\n\n\t/**\n\t * HTTP Status-Code 202: Accepted.\n\t */\n\tint HTTP_ACCEPTED = 202;\n\n\t/**\n\t * HTTP Status-Code 203: Non-Authoritative Information.\n\t */\n\tint HTTP_NOT_AUTHORITATIVE = 203;\n\n\t/**\n\t * HTTP Status-Code 204: No Content.\n\t */\n\tint HTTP_NO_CONTENT = 204;\n\n\t/**\n\t * HTTP Status-Code 205: Reset Content.\n\t */\n\tint HTTP_RESET = 205;\n\n\t/**\n\t * HTTP Status-Code 206: Partial Content.\n\t */\n\tint HTTP_PARTIAL = 206;\n\n\t/* 3XX: relocation/redirect */\n\n\t/**\n\t * HTTP Status-Code 300: Multiple Choices.\n\t */\n\tint HTTP_MULT_CHOICE = 300;\n\n\t/**\n\t * HTTP Status-Code 301: Moved Permanently.\n\t */\n\tint HTTP_MOVED_PERM = 301;\n\n\t/**\n\t * HTTP Status-Code 302: Temporary Redirect.\n\t */\n\tint HTTP_MOVED_TEMP = 302;\n\n\t/**\n\t * HTTP Status-Code 303: See Other.\n\t */\n\tint HTTP_SEE_OTHER = 303;\n\n\t/**\n\t * HTTP Status-Code 304: Not Modified.\n\t */\n\tint HTTP_NOT_MODIFIED = 304;\n\n\t/**\n\t * HTTP Status-Code 305: Use Proxy.\n\t */\n\tint HTTP_USE_PROXY = 305;\n\n\t/* 4XX: client error */\n\n\t/**\n\t * HTTP Status-Code 400: Bad Request.\n\t */\n\tint HTTP_BAD_REQUEST = 400;\n\n\t/**\n\t * HTTP Status-Code 401: Unauthorized.\n\t */\n\tint HTTP_UNAUTHORIZED = 401;\n\n\t/**\n\t * HTTP Status-Code 402: Payment Required.\n\t */\n\tint HTTP_PAYMENT_REQUIRED = 402;\n\n\t/**\n\t * HTTP Status-Code 403: Forbidden.\n\t */\n\tint HTTP_FORBIDDEN = 403;\n\n\t/**\n\t * HTTP Status-Code 404: Not Found.\n\t */\n\tint HTTP_NOT_FOUND = 404;\n\n\t/**\n\t * HTTP Status-Code 405: Method Not Allowed.\n\t */\n\tint HTTP_BAD_METHOD = 405;\n\n\t/**\n\t * HTTP Status-Code 406: Not Acceptable.\n\t */\n\tint HTTP_NOT_ACCEPTABLE = 406;\n\n\t/**\n\t * HTTP Status-Code 407: Proxy Authentication Required.\n\t */\n\tint HTTP_PROXY_AUTH = 407;\n\n\t/**\n\t * HTTP Status-Code 408: Request Time-Out.\n\t */\n\tint HTTP_CLIENT_TIMEOUT = 408;\n\n\t/**\n\t * HTTP Status-Code 409: Conflict.\n\t */\n\tint HTTP_CONFLICT = 409;\n\n\t/**\n\t * HTTP Status-Code 410: Gone.\n\t */\n\tint HTTP_GONE = 410;\n\n\t/**\n\t * HTTP Status-Code 411: Length Required.\n\t */\n\tint HTTP_LENGTH_REQUIRED = 411;\n\n\t/**\n\t * HTTP Status-Code 412: Precondition Failed.\n\t */\n\tint HTTP_PRECON_FAILED = 412;\n\n\t/**\n\t * HTTP Status-Code 413: Request Entity Too Large.\n\t */\n\tint HTTP_ENTITY_TOO_LARGE = 413;\n\n\t/**\n\t * HTTP Status-Code 414: Request-URI Too Large.\n\t */\n\tint HTTP_REQ_TOO_LONG = 414;\n\n\t/**\n\t * HTTP Status-Code 415: Unsupported Media Type.\n\t */\n\tint HTTP_UNSUPPORTED_TYPE = 415;\n\n\t/* 5XX: server error */\n\n\t/**\n\t * HTTP Status-Code 500: Internal Server Error.\n\t */\n\tint HTTP_INTERNAL_ERROR = 500;\n\n\t/**\n\t * HTTP Status-Code 501: Not Implemented.\n\t */\n\tint HTTP_NOT_IMPLEMENTED = 501;\n\n\t/**\n\t * HTTP Status-Code 502: Bad Gateway.\n\t */\n\tint HTTP_BAD_GATEWAY = 502;\n\n\t/**\n\t * HTTP Status-Code 503: Service Unavailable.\n\t */\n\tint HTTP_UNAVAILABLE = 503;\n\n\t/**\n\t * HTTP Status-Code 504: Gateway Timeout.\n\t */\n\tint HTTP_GATEWAY_TIMEOUT = 504;\n\n\t/**\n\t * HTTP Status-Code 505: HTTP Version Not Supported.\n\t */\n\tint HTTP_VERSION = 505;\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/BytesBody.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.OutputStream;\n\n/**\n * bytes类型的Http request body，主要发送编码后的表单数据或rest body（如JSON或XML）\n *\n * @since 5.7.17\n * @author looly\n */\npublic class BytesBody implements RequestBody {\n\n\tprivate final byte[] content;\n\n\t/**\n\t * 创建 Http request body\n\t * @param content body内容，编码后\n\t * @return BytesBody\n\t */\n\tpublic static BytesBody create(byte[] content){\n\t\treturn new BytesBody(content);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param content Body内容，编码后\n\t */\n\tpublic BytesBody(byte[] content) {\n\t\tthis.content = content;\n\t}\n\n\t@Override\n\tpublic void write(OutputStream out) {\n\t\tIoUtil.write(out, false, content);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/FormUrlEncodedBody.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.net.url.UrlQuery;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\n/**\n * application/x-www-form-urlencoded 类型请求body封装\n *\n * @author looly\n * @since 5.7.17\n */\npublic class FormUrlEncodedBody extends BytesBody {\n\n\t/**\n\t * 创建 Http request body\n\t *\n\t * @param form    表单\n\t * @param charset 编码\n\t * @return FormUrlEncodedBody\n\t */\n\tpublic static FormUrlEncodedBody create(Map<String, Object> form, Charset charset) {\n\t\treturn new FormUrlEncodedBody(form, charset);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param form    表单\n\t * @param charset 编码\n\t */\n\tpublic FormUrlEncodedBody(Map<String, Object> form, Charset charset) {\n\t\tsuper(StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset));\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal ByteArrayOutputStream result = new ByteArrayOutputStream();\n\t\twrite(result);\n\t\treturn result.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpGlobalConfig;\nimport cn.hutool.http.MultipartOutputStream;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\n/**\n * Multipart/form-data数据的请求体封装<br>\n * 遵循RFC2388规范\n *\n * @author looly\n * @since 5.3.5\n */\npublic class MultipartBody implements RequestBody {\n\n\tprivate static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + \"; boundary=\";\n\n\t/**\n\t * 存储表单数据\n\t */\n\tprivate final Map<String, Object> form;\n\t/**\n\t * 编码\n\t */\n\tprivate final Charset charset;\n\t/**\n\t * 边界\n\t */\n\tprivate final String boundary = HttpGlobalConfig.getBoundary();\n\n\t/**\n\t * 根据已有表单内容，构建MultipartBody\n\t *\n\t * @param form    表单\n\t * @param charset 编码\n\t * @return MultipartBody\n\t */\n\tpublic static MultipartBody create(Map<String, Object> form, Charset charset) {\n\t\treturn new MultipartBody(form, charset);\n\t}\n\n\t/**\n\t * 获取Multipart的Content-Type类型\n\t *\n\t * @return Multipart的Content-Type类型\n\t */\n\tpublic String getContentType() {\n\t\treturn CONTENT_TYPE_MULTIPART_PREFIX + boundary;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param form    表单\n\t * @param charset 编码\n\t */\n\tpublic MultipartBody(Map<String, Object> form, Charset charset) {\n\t\tthis.form = form;\n\t\tthis.charset = charset;\n\t}\n\n\t/**\n\t * 写出Multiparty数据，不关闭流\n\t *\n\t * @param out out流\n\t */\n\t@Override\n\tpublic void write(OutputStream out) {\n\t\tfinal MultipartOutputStream stream = new MultipartOutputStream(out, this.charset, this.boundary);\n\t\tif (MapUtil.isNotEmpty(this.form)) {\n\t\t\tthis.form.forEach(stream::write);\n\t\t}\n\t\tstream.finish();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\twrite(out);\n\t\treturn IoUtil.toStr(out, this.charset);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/RequestBody.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.io.IoUtil;\n\nimport java.io.OutputStream;\n\n/**\n * 定义请求体接口\n */\npublic interface RequestBody {\n\n\t/**\n\t * 写出数据，不关闭流\n\t *\n\t * @param out out流\n\t */\n\tvoid write(OutputStream out);\n\n\t/**\n\t * 写出并关闭{@link OutputStream}\n\t *\n\t * @param out {@link OutputStream}\n\t * @since 5.7.17\n\t */\n\tdefault void writeClose(OutputStream out) {\n\t\ttry {\n\t\t\twrite(out);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/ResourceBody.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.io.resource.Resource;\n\nimport java.io.OutputStream;\n\n/**\n * {@link Resource}类型的Http request body，主要发送编码后的表单数据或rest body（如JSON或XML）\n *\n * @author looly\n * @since 5.8.13\n */\npublic class ResourceBody implements RequestBody {\n\n\tprivate final Resource resource;\n\n\t/**\n\t * 创建 Http request body\n\t *\n\t * @param resource body内容，编码后\n\t * @return BytesBody\n\t */\n\tpublic static ResourceBody create(Resource resource) {\n\t\treturn new ResourceBody(resource);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param resource Body内容，编码后\n\t */\n\tpublic ResourceBody(Resource resource) {\n\t\tthis.resource = resource;\n\t}\n\n\t@Override\n\tpublic void write(OutputStream out) {\n\t\tif(null != this.resource){\n\t\t\tthis.resource.writeTo(out);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.resource.readUtf8Str();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/body/package-info.java",
    "content": "/**\n * 请求体封装实现\n * \n * @author looly\n *\n */\npackage cn.hutool.http.body;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/cookie/GlobalCookieManager.java",
    "content": "package cn.hutool.http.cookie;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.http.HttpConnection;\n\nimport java.io.IOException;\nimport java.net.CookieManager;\nimport java.net.CookiePolicy;\nimport java.net.HttpCookie;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 全局Cookie管理器，只针对Hutool请求有效\n *\n * @author Looly\n * @since 4.5.15\n */\npublic class GlobalCookieManager {\n\n\t/** Cookie管理 */\n\tprivate static CookieManager cookieManager;\n\tstatic {\n\t\tcookieManager = new CookieManager(new ThreadLocalCookieStore(), CookiePolicy.ACCEPT_ALL);\n\t}\n\n\t/**\n\t * 自定义{@link CookieManager}\n\t *\n\t * @param customCookieManager 自定义的{@link CookieManager}\n\t */\n\tpublic static void setCookieManager(CookieManager customCookieManager) {\n\t\tcookieManager = customCookieManager;\n\t}\n\n\t/**\n\t * 获取全局{@link CookieManager}\n\t *\n\t * @return {@link CookieManager}\n\t */\n\tpublic static CookieManager getCookieManager() {\n\t\treturn cookieManager;\n\t}\n\n\t/**\n\t * 获取指定域名下所有Cookie信息\n\t *\n\t * @param conn HTTP连接\n\t * @return Cookie信息列表\n\t * @since 4.6.9\n\t */\n\tpublic static List<HttpCookie> getCookies(HttpConnection conn){\n\t\treturn cookieManager.getCookieStore().get(getURI(conn));\n\t}\n\n\t/**\n\t * 将本地存储的Cookie信息附带到Http请求中，不覆盖用户定义好的Cookie\n\t *\n\t * @param conn {@link HttpConnection}\n\t */\n\tpublic static void add(HttpConnection conn) {\n\t\tif(null == cookieManager) {\n\t\t\t// 全局Cookie管理器关闭\n\t\t\treturn;\n\t\t}\n\n\t\tMap<String, List<String>> cookieHeader;\n\t\ttry {\n\t\t\tcookieHeader = cookieManager.get(getURI(conn), new HashMap<>(0));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\t// 不覆盖模式回填Cookie头，这样用户定义的Cookie将优先\n\t\tconn.header(cookieHeader, false);\n\t}\n\n\t/**\n\t * 存储响应的Cookie信息到本地\n\t *\n\t * @param conn {@link HttpConnection}\n\t */\n\tpublic static void store(HttpConnection conn) {\n\t\tif(null == cookieManager) {\n\t\t\t// 全局Cookie管理器关闭\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tcookieManager.put(getURI(conn), conn.headers());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取连接的URL中URI信息\n\t * @param conn HttpConnection\n\t * @return URI\n\t */\n\tprivate static URI getURI(HttpConnection conn){\n\t\treturn URLUtil.toURI(conn.getUrl());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/cookie/ThreadLocalCookieStore.java",
    "content": "package cn.hutool.http.cookie;\n\nimport java.net.CookieManager;\nimport java.net.CookieStore;\nimport java.net.HttpCookie;\nimport java.net.URI;\nimport java.util.List;\n\n/**\n * 线程隔离的Cookie存储。多线程环境下Cookie隔离使用，防止Cookie覆盖<br>\n * \n * 见：https://stackoverflow.com/questions/16305486/cookiemanager-for-multiple-threads\n * \n * @author looly\n * @since 4.1.18\n */\npublic class ThreadLocalCookieStore implements CookieStore {\n\n\tprivate final static ThreadLocal<CookieStore> STORES = new ThreadLocal<CookieStore>() {\n\t\t@Override\n\t\tprotected synchronized CookieStore initialValue() {\n\t\t\t/* InMemoryCookieStore */\n\t\t\treturn (new CookieManager()).getCookieStore();\n\t\t}\n\t};\n\n\t/**\n\t * 获取本线程下的CookieStore\n\t * \n\t * @return CookieStore\n\t */\n\tpublic CookieStore getCookieStore() {\n\t\treturn STORES.get();\n\t}\n\n\t/**\n\t * 移除当前线程的Cookie\n\t * \n\t * @return this\n\t */\n\tpublic ThreadLocalCookieStore removeCurrent() {\n\t\tSTORES.remove();\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void add(URI uri, HttpCookie cookie) {\n\t\tgetCookieStore().add(uri, cookie);\n\t}\n\n\t@Override\n\tpublic List<HttpCookie> get(URI uri) {\n\t\treturn getCookieStore().get(uri);\n\t}\n\n\t@Override\n\tpublic List<HttpCookie> getCookies() {\n\t\treturn getCookieStore().getCookies();\n\t}\n\n\t@Override\n\tpublic List<URI> getURIs() {\n\t\treturn getCookieStore().getURIs();\n\t}\n\n\t@Override\n\tpublic boolean remove(URI uri, HttpCookie cookie) {\n\t\treturn getCookieStore().remove(uri, cookie);\n\t}\n\n\t@Override\n\tpublic boolean removeAll() {\n\t\treturn getCookieStore().removeAll();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/cookie/package-info.java",
    "content": "/**\n * 自定义Cookie\n * \n * @author looly\n *\n */\npackage cn.hutool.http.cookie;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/package-info.java",
    "content": "/**\n * Hutool-http针对JDK的HttpUrlConnection做一层封装，简化了HTTPS请求、文件上传、Cookie记忆等操作，使Http请求变得无比简单。\n * \n * @author looly\n *\n */\npackage cn.hutool.http;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/HttpExchangeWrapper.java",
    "content": "package cn.hutool.http.server;\n\nimport com.sun.net.httpserver.Headers;\nimport com.sun.net.httpserver.HttpContext;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpPrincipal;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.InetSocketAddress;\nimport java.net.URI;\n\n/**\n * {@link HttpExchange}包装类，提供增强方法和缓存\n *\n * @author looly\n */\npublic class HttpExchangeWrapper extends HttpExchange {\n\n\tprivate final HttpExchange raw;\n\tprivate final HttpServerRequest request;\n\tprivate final HttpServerResponse response;\n\n\t/**\n\t * 构造\n\t *\n\t * @param raw {@link HttpExchange}\n\t */\n\tpublic HttpExchangeWrapper(final HttpExchange raw) {\n\t\tthis.raw = raw;\n\t\tthis.request = new HttpServerRequest(this);\n\t\tthis.response = new HttpServerResponse(this);\n\t}\n\n\t/**\n\t * 获取原始对象\n\t * @return 对象\n\t */\n\tpublic HttpExchange getRaw() {\n\t\treturn this.raw;\n\t}\n\n\t/**\n\t * 获取请求\n\t *\n\t * @return 请求\n\t */\n\tpublic HttpServerRequest getRequest() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 获取响应\n\t *\n\t * @return 响应\n\t */\n\tpublic HttpServerResponse getResponse() {\n\t\treturn response;\n\t}\n\n\t// region ----- HttpExchange methods\n\t@Override\n\tpublic Headers getRequestHeaders() {\n\t\treturn this.raw.getRequestHeaders();\n\t}\n\n\t@Override\n\tpublic Headers getResponseHeaders() {\n\t\treturn this.raw.getResponseHeaders();\n\t}\n\n\t@Override\n\tpublic URI getRequestURI() {\n\t\treturn this.raw.getRequestURI();\n\t}\n\n\t@Override\n\tpublic String getRequestMethod() {\n\t\treturn this.raw.getRequestMethod();\n\t}\n\n\t@Override\n\tpublic HttpContext getHttpContext() {\n\t\treturn this.raw.getHttpContext();\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tthis.raw.close();\n\t}\n\n\t@Override\n\tpublic InputStream getRequestBody() {\n\t\treturn this.raw.getRequestBody();\n\t}\n\n\t@Override\n\tpublic OutputStream getResponseBody() {\n\t\treturn this.raw.getResponseBody();\n\t}\n\n\t@Override\n\tpublic void sendResponseHeaders(final int rCode, final long responseLength) throws IOException {\n\t\tthis.raw.sendResponseHeaders(rCode, responseLength);\n\t}\n\n\t@Override\n\tpublic InetSocketAddress getRemoteAddress() {\n\t\treturn this.raw.getRemoteAddress();\n\t}\n\n\t@Override\n\tpublic int getResponseCode() {\n\t\treturn this.raw.getResponseCode();\n\t}\n\n\t@Override\n\tpublic InetSocketAddress getLocalAddress() {\n\t\treturn this.raw.getLocalAddress();\n\t}\n\n\t@Override\n\tpublic String getProtocol() {\n\t\treturn this.raw.getProtocol();\n\t}\n\n\t@Override\n\tpublic Object getAttribute(final String name) {\n\t\treturn this.raw.getAttribute(name);\n\t}\n\n\t@Override\n\tpublic void setAttribute(final String name, final Object value) {\n\t\tthis.raw.setAttribute(name, value);\n\t}\n\n\t@Override\n\tpublic void setStreams(final InputStream i, final OutputStream o) {\n\t\tthis.raw.setStreams(i, o);\n\t}\n\n\t@Override\n\tpublic HttpPrincipal getPrincipal() {\n\t\treturn this.raw.getPrincipal();\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/HttpServerBase.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport com.sun.net.httpserver.HttpContext;\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.io.Closeable;\nimport java.nio.charset.Charset;\n\n/**\n * HttpServer公用对象，提供HttpExchange包装和公用方法\n *\n * @author looly\n * @since 5.2.6\n */\npublic class HttpServerBase implements Closeable {\n\n\tfinal static Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\n\tfinal HttpExchange httpExchange;\n\n\t/**\n\t * 构造\n\t *\n\t * @param httpExchange {@link HttpExchange}\n\t */\n\tpublic HttpServerBase(HttpExchange httpExchange) {\n\t\tthis.httpExchange = httpExchange;\n\t}\n\n\t/**\n\t * 获取{@link HttpExchange}对象\n\t *\n\t * @return {@link HttpExchange}对象\n\t */\n\tpublic HttpExchange getHttpExchange() {\n\t\treturn this.httpExchange;\n\t}\n\n\t/**\n\t * 获取{@link HttpContext}\n\t *\n\t * @return {@link HttpContext}\n\t * @since 5.5.7\n\t */\n\tpublic HttpContext getHttpContext() {\n\t\treturn getHttpExchange().getHttpContext();\n\t}\n\n\t/**\n\t * 调用{@link HttpExchange#close()}，关闭请求流和响应流\n\t */\n\t@Override\n\tpublic void close() {\n\t\tthis.httpExchange.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/HttpServerRequest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.LimitedInputStream;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.multi.ListValueMap;\nimport cn.hutool.core.net.NetUtil;\nimport cn.hutool.core.net.multipart.MultipartFormData;\nimport cn.hutool.core.net.multipart.UploadSetting;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.Header;\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.http.Method;\nimport cn.hutool.http.useragent.UserAgent;\nimport cn.hutool.http.useragent.UserAgentUtil;\nimport com.sun.net.httpserver.Headers;\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpCookie;\nimport java.net.URI;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Http请求对象，对{@link HttpExchange}封装\n *\n * @author looly\n * @since 5.2.6\n */\npublic class HttpServerRequest extends HttpServerBase {\n\n\tprivate Map<String, HttpCookie> cookieCache;\n\tprivate ListValueMap<String, String> paramsCache;\n\tprivate MultipartFormData multipartFormDataCache;\n\tprivate Charset charsetCache;\n\tprivate byte[] bodyCache;\n\n\t/**\n\t * 构造\n\t *\n\t * @param httpExchange {@link HttpExchange}\n\t */\n\tpublic HttpServerRequest(HttpExchange httpExchange) {\n\t\tsuper(httpExchange);\n\t}\n\n\t/**\n\t * 获得Http Method\n\t *\n\t * @return Http Method\n\t */\n\tpublic String getMethod() {\n\t\treturn this.httpExchange.getRequestMethod();\n\t}\n\n\t/**\n\t * 是否为GET请求\n\t *\n\t * @return 是否为GET请求\n\t */\n\tpublic boolean isGetMethod() {\n\t\treturn Method.GET.name().equalsIgnoreCase(getMethod());\n\t}\n\n\t/**\n\t * 是否为POST请求\n\t *\n\t * @return 是否为POST请求\n\t */\n\tpublic boolean isPostMethod() {\n\t\treturn Method.POST.name().equalsIgnoreCase(getMethod());\n\t}\n\n\t/**\n\t * 获得请求URI\n\t *\n\t * @return 请求URI\n\t */\n\tpublic URI getURI() {\n\t\treturn this.httpExchange.getRequestURI();\n\t}\n\n\t/**\n\t * 获得请求路径Path\n\t *\n\t * @return 请求路径\n\t */\n\tpublic String getPath() {\n\t\treturn getURI().getPath();\n\t}\n\n\t/**\n\t * 获取请求参数\n\t *\n\t * @return 参数字符串\n\t */\n\tpublic String getQuery() {\n\t\treturn getURI().getQuery();\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @return header值\n\t */\n\tpublic Headers getHeaders() {\n\t\treturn this.httpExchange.getRequestHeaders();\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param headerKey 头信息的KEY\n\t * @return header值\n\t */\n\tpublic String getHeader(Header headerKey) {\n\t\treturn getHeader(headerKey.toString());\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param headerKey 头信息的KEY\n\t * @return header值\n\t */\n\tpublic String getHeader(String headerKey) {\n\t\treturn getHeaders().getFirst(headerKey);\n\t}\n\n\t/**\n\t * 获得请求header中的信息\n\t *\n\t * @param headerKey 头信息的KEY\n\t * @param charset   字符集\n\t * @return header值\n\t */\n\tpublic String getHeader(String headerKey, Charset charset) {\n\t\tfinal String header = getHeader(headerKey);\n\t\tif (null != header) {\n\t\t\treturn CharsetUtil.convert(header, CharsetUtil.CHARSET_ISO_8859_1, charset);\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取Content-Type头信息\n\t *\n\t * @return Content-Type头信息\n\t */\n\tpublic String getContentType() {\n\t\treturn getHeader(Header.CONTENT_TYPE);\n\t}\n\n\t/**\n\t * 获取编码，获取失败默认使用UTF-8，获取规则如下：\n\t *\n\t * <pre>\n\t *     1、从Content-Type头中获取编码，类似于：text/html;charset=utf-8\n\t * </pre>\n\t *\n\t * @return 编码，默认UTF-8\n\t */\n\tpublic Charset getCharset() {\n\t\tif(null == this.charsetCache){\n\t\t\tfinal String contentType = getContentType();\n\t\t\tfinal String charsetStr = HttpUtil.getCharset(contentType);\n\t\t\tthis.charsetCache = CharsetUtil.parse(charsetStr, DEFAULT_CHARSET);\n\t\t}\n\n\t\treturn this.charsetCache;\n\t}\n\n\t/**\n\t * 获得User-Agent\n\t *\n\t * @return User-Agent字符串\n\t */\n\tpublic String getUserAgentStr() {\n\t\treturn getHeader(Header.USER_AGENT);\n\t}\n\n\t/**\n\t * 获得User-Agent，未识别返回null\n\t *\n\t * @return User-Agent字符串，未识别返回null\n\t */\n\tpublic UserAgent getUserAgent() {\n\t\treturn UserAgentUtil.parse(getUserAgentStr());\n\t}\n\n\t/**\n\t * 获得Cookie信息字符串\n\t *\n\t * @return cookie字符串\n\t */\n\tpublic String getCookiesStr() {\n\t\treturn getHeader(Header.COOKIE);\n\t}\n\n\t/**\n\t * 获得Cookie信息列表\n\t *\n\t * @return Cookie信息列表\n\t */\n\tpublic Collection<HttpCookie> getCookies() {\n\t\treturn getCookieMap().values();\n\t}\n\n\t/**\n\t * 获得Cookie信息Map，键为Cookie名，值为HttpCookie对象\n\t *\n\t * @return Cookie信息Map\n\t */\n\tpublic Map<String, HttpCookie> getCookieMap() {\n\t\tif (null == this.cookieCache) {\n\t\t\tcookieCache = Collections.unmodifiableMap(CollUtil.toMap(\n\t\t\t\t\tNetUtil.parseCookies(getCookiesStr()),\n\t\t\t\t\tnew CaseInsensitiveMap<>(),\n\t\t\t\t\tHttpCookie::getName));\n\t\t}\n\t\treturn cookieCache;\n\t}\n\n\t/**\n\t * 获得指定Cookie名对应的HttpCookie对象\n\t *\n\t * @param cookieName Cookie名\n\t * @return HttpCookie对象\n\t */\n\tpublic HttpCookie getCookie(String cookieName) {\n\t\treturn getCookieMap().get(cookieName);\n\t}\n\n\t/**\n\t * 是否为Multipart类型表单，此类型表单用于文件上传\n\t *\n\t * @return 是否为Multipart类型表单，此类型表单用于文件上传\n\t */\n\tpublic boolean isMultipart() {\n\t\tif (false == isPostMethod()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfinal String contentType = getContentType();\n\t\tif (StrUtil.isBlank(contentType)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn contentType.toLowerCase().startsWith(\"multipart/\");\n\t}\n\n\t/**\n\t * 获取请求体文本，可以是form表单、json、xml等任意内容<br>\n\t * 使用{@link #getCharset()}判断编码，判断失败使用UTF-8编码\n\t *\n\t * @return 请求\n\t */\n\tpublic String getBody() {\n\t\treturn getBody(getCharset());\n\t}\n\n\t/**\n\t * 获取请求体文本，可以是form表单、json、xml等任意内容\n\t *\n\t * @param charset 编码\n\t * @return 请求\n\t */\n\tpublic String getBody(Charset charset) {\n\t\treturn StrUtil.str(getBodyBytes(), charset);\n\t}\n\n\t/**\n\t * 获取body的bytes数组\n\t *\n\t * @return body的bytes数组\n\t */\n\tpublic byte[] getBodyBytes(){\n\t\tif(null == this.bodyCache){\n\t\t\tthis.bodyCache = IoUtil.readBytes(getBodyStream(), true);\n\t\t}\n\t\treturn this.bodyCache;\n\t}\n\n\t/**\n\t * 获取请求体的流，流中可以读取请求内容，包括请求表单数据或文件上传数据\n\t *\n\t * @return 流\n\t */\n\tpublic InputStream getBodyStream() {\n\t\tInputStream bodyStream = this.httpExchange.getRequestBody();\n\n\t\t//issue#I6Q30X，读取body长度，避免读取结束后无法正常结束问题\n\t\tfinal String contentLengthStr = getHeader(Header.CONTENT_LENGTH);\n\t\tlong contentLength = 0;\n\t\tif(StrUtil.isNotBlank(contentLengthStr)){\n\t\t\ttry{\n\t\t\t\tcontentLength = Long.parseLong(contentLengthStr);\n\t\t\t} catch (final NumberFormatException ignore){\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\n\t\tif(contentLength > 0){\n\t\t\tbodyStream = new LimitedInputStream(bodyStream, contentLength);\n\t\t}\n\n\t\treturn bodyStream;\n\t}\n\n\t/**\n\t * 获取指定名称的参数值，取第一个值\n\t * @param name 参数名\n\t * @return 参数值\n\t * @since 5.5.8\n\t */\n\tpublic String getParam(String name){\n\t\treturn getParams().get(name, 0);\n\t}\n\n\t/**\n\t * 获取指定名称的参数值\n\t *\n\t * @param name 参数名\n\t * @return 参数值\n\t * @since 5.5.8\n\t */\n\tpublic List<String> getParams(String name){\n\t\treturn getParams().get(name);\n\t}\n\n\t/**\n\t * 获取参数Map\n\t *\n\t * @return 参数map\n\t */\n\tpublic ListValueMap<String, String> getParams() {\n\t\tif (null == this.paramsCache) {\n\t\t\tthis.paramsCache = new ListValueMap<>();\n\t\t\tfinal Charset charset = getCharset();\n\n\t\t\t//解析URL中的参数\n\t\t\tfinal String query = getQuery();\n\t\t\tif(StrUtil.isNotBlank(query)){\n\t\t\t\tthis.paramsCache.putAll(HttpUtil.decodeParams(query, charset, false));\n\t\t\t}\n\n\t\t\t// 解析multipart中的参数\n\t\t\tif(isMultipart()){\n\t\t\t\tthis.paramsCache.putAll(getMultipart().getParamListMap());\n\t\t\t} else{\n\t\t\t\t// 解析body中的参数\n\t\t\t\tfinal String body = getBody();\n\t\t\t\tif(StrUtil.isNotBlank(body)){\n\t\t\t\t\tthis.paramsCache.putAll(HttpUtil.decodeParams(body, charset, true));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.paramsCache;\n\t}\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * 默认检测的Header:\n\t *\n\t * <pre>\n\t * 1、X-Forwarded-For\n\t * 2、X-Real-IP\n\t * 3、Proxy-Client-IP\n\t * 4、WL-Proxy-Client-IP\n\t * </pre>\n\t *\n\t * <p>\n\t * otherHeaderNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param otherHeaderNames 其他自定义头文件，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t */\n\tpublic String getClientIP(String... otherHeaderNames) {\n\t\tString[] headers = {\"X-Forwarded-For\", \"X-Real-IP\", \"Proxy-Client-IP\", \"WL-Proxy-Client-IP\", \"HTTP_CLIENT_IP\", \"HTTP_X_FORWARDED_FOR\"};\n\t\tif (ArrayUtil.isNotEmpty(otherHeaderNames)) {\n\t\t\theaders = ArrayUtil.addAll(otherHeaderNames, headers);\n\t\t}\n\n\t\treturn getClientIPByHeader(headers);\n\t}\n\n\t/**\n\t * 获取客户端IP\n\t *\n\t * <p>\n\t * headerNames参数用于自定义检测的Header<br>\n\t * 需要注意的是，使用此方法获取的客户IP地址必须在Http服务器（例如Nginx）中配置头信息，否则容易造成IP伪造。\n\t * </p>\n\t *\n\t * @param headerNames 自定义头，通常在Http服务器（例如Nginx）中配置\n\t * @return IP地址\n\t * @since 4.4.1\n\t */\n\tpublic String getClientIPByHeader(String... headerNames) {\n\t\tString ip;\n\t\tfor (String header : headerNames) {\n\t\t\tip = getHeader(header);\n\t\t\tif (false == NetUtil.isUnknown(ip)) {\n\t\t\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t\t\t}\n\t\t}\n\n\t\tip = this.httpExchange.getRemoteAddress().getHostName();\n\t\treturn NetUtil.getMultistageReverseProxyIp(ip);\n\t}\n\n\t/**\n\t * 获得MultiPart表单内容，多用于获得上传的文件\n\t *\n\t * @return MultipartFormData\n\t * @throws IORuntimeException IO异常\n\t * @since 5.3.0\n\t */\n\tpublic MultipartFormData getMultipart() throws IORuntimeException {\n\t\tif(null == this.multipartFormDataCache){\n\t\t\tthis.multipartFormDataCache = parseMultipart(new UploadSetting());\n\t\t}\n\t\treturn this.multipartFormDataCache;\n\t}\n\n\t/**\n\t * 获得multipart/form-data 表单内容<br>\n\t * 包括文件和普通表单数据<br>\n\t * 在同一次请求中，此方法只能被执行一次！\n\t *\n\t * @param uploadSetting 上传文件的设定，包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等\n\t * @return MultiPart表单\n\t * @throws IORuntimeException IO异常\n\t * @since 5.3.0\n\t */\n\tpublic MultipartFormData parseMultipart(UploadSetting uploadSetting) throws IORuntimeException {\n\t\tfinal MultipartFormData formData = new MultipartFormData(uploadSetting);\n\t\ttry {\n\t\t\tformData.parseRequestStream(getBodyStream(), getCharset());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn formData;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/HttpServerResponse.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.Header;\nimport cn.hutool.http.HttpStatus;\nimport cn.hutool.http.HttpUtil;\nimport com.sun.net.httpserver.Headers;\nimport com.sun.net.httpserver.HttpExchange;\n\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Http响应对象，用于写出数据到客户端\n */\npublic class HttpServerResponse extends HttpServerBase {\n\n\tprivate Charset charset;\n\t/**\n\t * 是否已经发送了Http状态码，如果没有，提前写出状态码\n\t */\n\tprivate boolean isSendCode;\n\n\t/**\n\t * 构造\n\t *\n\t * @param httpExchange {@link HttpExchange}\n\t */\n\tpublic HttpServerResponse(HttpExchange httpExchange) {\n\t\tsuper(httpExchange);\n\t}\n\n\t/**\n\t * 发送HTTP状态码，Content-Length为0不定长度，会输出Transfer-encoding: chunked\n\t *\n\t * @param httpStatusCode HTTP状态码，见HttpStatus\n\t * @return this\n\t */\n\tpublic HttpServerResponse send(int httpStatusCode) {\n\t\treturn send(httpStatusCode, 0);\n\t}\n\n\t/**\n\t * 发送成功状态码\n\t *\n\t * @return this\n\t */\n\tpublic HttpServerResponse sendOk() {\n\t\treturn send(HttpStatus.HTTP_OK);\n\t}\n\n\t/**\n\t * 发送成功状态码\n\t *\n\t * @param bodyLength 响应体长度，默认0表示不定长度，会输出Transfer-encoding: chunked\n\t * @return this\n\t * @since 5.5.7\n\t */\n\tpublic HttpServerResponse sendOk(int bodyLength) {\n\t\treturn send(HttpStatus.HTTP_OK, bodyLength);\n\t}\n\n\t/**\n\t * 发送404错误页\n\t *\n\t * @param content 错误页页面内容，默认text/html类型\n\t * @return this\n\t */\n\tpublic HttpServerResponse send404(String content) {\n\t\treturn sendError(HttpStatus.HTTP_NOT_FOUND, content);\n\t}\n\n\t/**\n\t * 发送错误页\n\t *\n\t * @param errorCode HTTP错误状态码，见HttpStatus\n\t * @param content   错误页页面内容，默认text/html类型\n\t * @return this\n\t */\n\tpublic HttpServerResponse sendError(int errorCode, String content) {\n\t\tsend(errorCode);\n\t\tsetContentType(ContentType.TEXT_HTML.toString());\n\t\treturn write(content);\n\t}\n\n\t/**\n\t * 发送HTTP状态码\n\t *\n\t * @param httpStatusCode HTTP状态码，见HttpStatus\n\t * @param bodyLength     响应体长度，默认0表示不定长度，会输出Transfer-encoding: chunked\n\t * @return this\n\t */\n\tpublic HttpServerResponse send(int httpStatusCode, long bodyLength) {\n\t\tif (this.isSendCode) {\n\t\t\tthrow new IORuntimeException(\"Http status code has been send!\");\n\t\t}\n\n\t\ttry {\n\t\t\tthis.httpExchange.sendResponseHeaders(httpStatusCode, bodyLength);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tthis.isSendCode = true;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得所有响应头，获取后可以添加新的响应头\n\t *\n\t * @return 响应头\n\t */\n\tpublic Headers getHeaders() {\n\t\treturn this.httpExchange.getResponseHeaders();\n\t}\n\n\t/**\n\t * 添加响应头，如果已经存在，则追加\n\t *\n\t * @param header 头key\n\t * @param value  值\n\t * @return this\n\t */\n\tpublic HttpServerResponse addHeader(String header, String value) {\n\t\tgetHeaders().add(header, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置响应头，如果已经存在，则覆盖\n\t *\n\t * @param header 头key\n\t * @param value  值\n\t * @return this\n\t */\n\tpublic HttpServerResponse setHeader(Header header, String value) {\n\t\treturn setHeader(header.getValue(), value);\n\t}\n\n\t/**\n\t * 设置响应头，如果已经存在，则覆盖\n\t *\n\t * @param header 头key\n\t * @param value  值\n\t * @return this\n\t */\n\tpublic HttpServerResponse setHeader(String header, String value) {\n\t\tgetHeaders().set(header, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置响应头，如果已经存在，则覆盖\n\t *\n\t * @param header 头key\n\t * @param value  值列表\n\t * @return this\n\t */\n\tpublic HttpServerResponse setHeader(String header, List<String> value) {\n\t\tgetHeaders().put(header, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置所有响应头，如果已经存在，则覆盖\n\t *\n\t * @param headers 响应头map\n\t * @return this\n\t */\n\tpublic HttpServerResponse setHeaders(Map<String, List<String>> headers) {\n\t\tgetHeaders().putAll(headers);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Content-Type头，类似于:text/html;charset=utf-8<br>\n\t * 如果用户传入的信息无charset信息，自动根据charset补充，charset设置见{@link #setCharset(Charset)}\n\t *\n\t * @param contentType Content-Type头内容\n\t * @return this\n\t */\n\tpublic HttpServerResponse setContentType(String contentType) {\n\t\tif (null != contentType && null != this.charset) {\n\t\t\tif (false == contentType.contains(\";charset=\")) {\n\t\t\t\tcontentType = ContentType.build(contentType, this.charset);\n\t\t\t}\n\t\t}\n\n\t\treturn setHeader(Header.CONTENT_TYPE, contentType);\n\t}\n\n\t/**\n\t * 设置Content-Length头\n\t *\n\t * @param contentLength Content-Length头内容\n\t * @return this\n\t */\n\tpublic HttpServerResponse setContentLength(long contentLength) {\n\t\treturn setHeader(Header.CONTENT_LENGTH, String.valueOf(contentLength));\n\t}\n\n\t/**\n\t * 设置响应的编码\n\t *\n\t * @param charset 编码\n\t * @return this\n\t */\n\tpublic HttpServerResponse setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置属性\n\t *\n\t * @param name  属性名\n\t * @param value 属性值\n\t * @return this\n\t */\n\tpublic HttpServerResponse setAttr(String name, Object value) {\n\t\tthis.httpExchange.setAttribute(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取响应数据流\n\t *\n\t * @return 响应数据流\n\t */\n\tpublic OutputStream getOut() {\n\t\tif (false == this.isSendCode) {\n\t\t\tsendOk();\n\t\t}\n\t\treturn this.httpExchange.getResponseBody();\n\t}\n\n\t/**\n\t * 获取响应数据流\n\t *\n\t * @return 响应数据流\n\t */\n\tpublic PrintWriter getWriter() {\n\t\tfinal Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);\n\t\treturn new PrintWriter(new OutputStreamWriter(getOut(), charset));\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param data        数据\n\t * @param contentType Content-Type类型\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(String data, String contentType) {\n\t\tsetContentType(contentType);\n\t\treturn write(data);\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param data 数据\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(String data) {\n\t\tfinal Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);\n\t\treturn write(StrUtil.bytes(data, charset));\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param data        数据\n\t * @param contentType 返回的类型\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(byte[] data, String contentType) {\n\t\tsetContentType(contentType);\n\t\treturn write(data);\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param data 数据\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(byte[] data) {\n\t\tfinal ByteArrayInputStream in = new ByteArrayInputStream(data);\n\t\treturn write(in, in.available());\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型\n\t * @return this\n\t * @since 5.2.6\n\t */\n\tpublic HttpServerResponse write(InputStream in, String contentType) {\n\t\treturn write(in, 0, contentType);\n\t}\n\n\t/**\n\t * 返回数据给客户端\n\t *\n\t * @param in          需要返回客户端的内容\n\t * @param length 内容长度，默认0表示不定长度，会输出Transfer-encoding: chunked\n\t * @param contentType 返回的类型\n\t * @return this\n\t * @since 5.2.7\n\t */\n\tpublic HttpServerResponse write(InputStream in, int length, String contentType) {\n\t\tsetContentType(contentType);\n\t\treturn write(in, length);\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param in 数据流\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(InputStream in) {\n\t\treturn write(in, 0);\n\t}\n\n\t/**\n\t * 写出数据到客户端\n\t *\n\t * @param in     数据流\n\t * @param length 指定响应内容长度，默认0表示不定长度，会输出Transfer-encoding: chunked\n\t * @return this\n\t */\n\tpublic HttpServerResponse write(InputStream in, int length) {\n\t\tif (false == isSendCode) {\n\t\t\tsendOk(Math.max(0, length));\n\t\t}\n\t\tOutputStream out = null;\n\t\ttry {\n\t\t\tout = this.httpExchange.getResponseBody();\n\t\t\tIoUtil.copy(in, out);\n\t\t} finally {\n\t\t\tIoUtil.close(out);\n\t\t\tIoUtil.close(in);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回文件给客户端（文件下载）\n\t *\n\t * @param file 写出的文件对象\n\t * @return this\n\t * @since 5.2.6\n\t */\n\tpublic HttpServerResponse write(File file) {\n\t\treturn write(file, null);\n\t}\n\n\t/**\n\t * 返回文件给客户端（文件下载）\n\t *\n\t * @param file 写出的文件对象\n\t * @param fileName 文件名\n\t * @return this\n\t * @since 5.5.8\n\t */\n\tpublic HttpServerResponse write(File file, String fileName) {\n\t\tfinal long fileSize = file.length();\n\t\tif(fileSize > Integer.MAX_VALUE){\n\t\t\tthrow new IllegalArgumentException(\"File size is too bigger than \" + Integer.MAX_VALUE);\n\t\t}\n\n\t\tif(StrUtil.isBlank(fileName)){\n\t\t\tfileName = file.getName();\n\t\t}\n\t\tfinal String contentType = ObjectUtil.defaultIfNull(HttpUtil.getMimeType(fileName), \"application/octet-stream\");\n\t\tBufferedInputStream in = null;\n\t\ttry {\n\t\t\tin = FileUtil.getInputStream(file);\n\t\t\twrite(in, (int)fileSize, contentType, fileName);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回文件数据给客户端（文件下载）\n\t *\n\t * @param in          需要返回客户端的内容\n\t * @param contentType 返回的类型\n\t * @param fileName    文件名\n\t * @since 5.2.6\n\t */\n\tpublic void write(InputStream in, String contentType, String fileName) {\n\t\twrite(in, 0, contentType, fileName);\n\t}\n\n\t/**\n\t * 返回文件数据给客户端（文件下载）\n\t *\n\t * @param in          需要返回客户端的内容\n\t * @param length 长度\n\t * @param contentType 返回的类型\n\t * @param fileName    文件名\n\t * @return this\n\t * @since 5.2.7\n\t */\n\tpublic HttpServerResponse write(InputStream in, int length, String contentType, String fileName) {\n\t\tfinal Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);\n\n\t\tif (false == contentType.startsWith(\"text/\")) {\n\t\t\t// 非文本类型数据直接走下载\n\t\t\tsetHeader(Header.CONTENT_DISPOSITION, StrUtil.format(\"attachment;filename={}\", URLUtil.encode(fileName, charset)));\n\t\t}\n\t\treturn write(in, length, contentType);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/SimpleServer.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.GlobalThreadPool;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.server.action.Action;\nimport cn.hutool.http.server.action.RootAction;\nimport cn.hutool.http.server.filter.HttpFilter;\nimport cn.hutool.http.server.filter.SimpleFilter;\nimport cn.hutool.http.server.handler.ActionHandler;\nimport com.sun.net.httpserver.Filter;\nimport com.sun.net.httpserver.HttpContext;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport com.sun.net.httpserver.HttpServer;\nimport com.sun.net.httpserver.HttpsConfigurator;\nimport com.sun.net.httpserver.HttpsServer;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Executor;\n\n/**\n * 简易Http服务器，基于{@link HttpServer}\n *\n * @author looly\n * @since 5.2.5\n */\npublic class SimpleServer {\n\n\tprivate final HttpServer server;\n\tprivate final List<Filter> filters;\n\n\t/**\n\t * 构造\n\t *\n\t * @param port 监听端口\n\t */\n\tpublic SimpleServer(int port) {\n\t\tthis(new InetSocketAddress(port));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param hostname 监听地址\n\t * @param port     监听端口\n\t */\n\tpublic SimpleServer(String hostname, int port) {\n\t\tthis(new InetSocketAddress(hostname, port));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 监听地址\n\t */\n\tpublic SimpleServer(InetSocketAddress address) {\n\t\tthis(address, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 监听地址\n\t * @param configurator https配置信息，用于使用自定义SSL（TLS）证书等\n\t */\n\tpublic SimpleServer(InetSocketAddress address, HttpsConfigurator configurator) {\n\t\ttry {\n\t\t\tif(null != configurator){\n\t\t\t\tfinal HttpsServer server = HttpsServer.create(address, 0);\n\t\t\t\tserver.setHttpsConfigurator(configurator);\n\t\t\t\tthis.server = server;\n\t\t\t} else{\n\t\t\t\tthis.server = HttpServer.create(address, 0);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\tsetExecutor(GlobalThreadPool.getExecutor());\n\t\tfilters = new ArrayList<>();\n\t}\n\n\t/**\n\t * 增加请求过滤器，此过滤器对所有请求有效<br>\n\t * 此方法需在以下方法前之前调用：\n\t *\n\t * <ul>\n\t *     <li>{@link #setRoot(File)}  </li>\n\t *     <li>{@link #setRoot(String)}  </li>\n\t *     <li>{@link #createContext(String, HttpHandler)} </li>\n\t *     <li>{@link #addHandler(String, HttpHandler)}</li>\n\t *     <li>{@link #addAction(String, Action)} </li>\n\t * </ul>\n\t *\n\t * @param filter {@link Filter} 请求过滤器\n\t * @return this\n\t * @since 5.5.7\n\t */\n\tpublic SimpleServer addFilter(Filter filter) {\n\t\tthis.filters.add(filter);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加请求过滤器，此过滤器对所有请求有效<br>\n\t * 此方法需在以下方法前之前调用：\n\t *\n\t * <ul>\n\t *     <li>{@link #setRoot(File)}  </li>\n\t *     <li>{@link #setRoot(String)}  </li>\n\t *     <li>{@link #createContext(String, HttpHandler)} </li>\n\t *     <li>{@link #addHandler(String, HttpHandler)}</li>\n\t *     <li>{@link #addAction(String, Action)} </li>\n\t * </ul>\n\t *\n\t * @param filter {@link Filter} 请求过滤器\n\t * @return this\n\t * @since 5.5.7\n\t */\n\tpublic SimpleServer addFilter(HttpFilter filter) {\n\t\treturn addFilter(new SimpleFilter() {\n\t\t\t@Override\n\t\t\tpublic void doFilter(HttpExchange httpExchange, Chain chain) throws IOException {\n\t\t\t\tfinal HttpExchangeWrapper httpExchangeWrapper = new HttpExchangeWrapper(httpExchange);\n\t\t\t\tfilter.doFilter(httpExchangeWrapper.getRequest(), httpExchangeWrapper.getResponse(), chain);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 增加请求处理规则\n\t *\n\t * @param path    路径，例如:/a/b 或者 a/b\n\t * @param handler 处理器，包括请求和响应处理\n\t * @return this\n\t * @see #createContext(String, HttpHandler)\n\t */\n\tpublic SimpleServer addHandler(String path, HttpHandler handler) {\n\t\tcreateContext(path, handler);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建请求映射上下文，创建后，用户访问指定路径可使用{@link HttpHandler} 中的规则进行处理\n\t *\n\t * @param path    路径，例如:/a/b 或者 a/b\n\t * @param handler 处理器，包括请求和响应处理\n\t * @return {@link HttpContext}\n\t * @since 5.5.7\n\t */\n\tpublic HttpContext createContext(String path, HttpHandler handler) {\n\t\t// 非/开头的路径会报错\n\t\tpath = StrUtil.addPrefixIfNot(path, StrUtil.SLASH);\n\t\tfinal HttpContext context = this.server.createContext(path, handler);\n\t\t// 增加整体过滤器\n\t\tcontext.getFilters().addAll(this.filters);\n\t\treturn context;\n\t}\n\n\t/**\n\t * 设置根目录，默认的页面从root目录中读取解析返回\n\t *\n\t * @param root 路径\n\t * @return this\n\t */\n\tpublic SimpleServer setRoot(String root) {\n\t\treturn setRoot(new File(root));\n\t}\n\n\t/**\n\t * 设置根目录，默认的页面从root目录中读取解析返回\n\t *\n\t * @param root 路径\n\t * @return this\n\t */\n\tpublic SimpleServer setRoot(File root) {\n\t\treturn addAction(\"/\", new RootAction(root));\n\t}\n\n\t/**\n\t * 增加请求处理规则\n\t *\n\t * @param path   路径\n\t * @param action 处理器\n\t * @return this\n\t */\n\tpublic SimpleServer addAction(String path, Action action) {\n\t\treturn addHandler(path, new ActionHandler(action));\n\t}\n\n\t/**\n\t * 设置自定义线程池\n\t *\n\t * @param executor {@link Executor}\n\t * @return this\n\t */\n\tpublic SimpleServer setExecutor(Executor executor) {\n\t\tthis.server.setExecutor(executor);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获得原始HttpServer对象\n\t *\n\t * @return {@link HttpServer}\n\t */\n\tpublic HttpServer getRawServer() {\n\t\treturn this.server;\n\t}\n\n\t/**\n\t * 获取服务器地址信息\n\t *\n\t * @return {@link InetSocketAddress}\n\t */\n\tpublic InetSocketAddress getAddress() {\n\t\treturn this.server.getAddress();\n\t}\n\n\t/**\n\t * 启动Http服务器，启动后会阻塞当前线程\n\t */\n\tpublic void start() {\n\t\tfinal InetSocketAddress address = getAddress();\n\t\tConsole.log(\"Hutool Simple Http Server listen on 【{}:{}】\", address.getHostName(), address.getPort());\n\t\tthis.server.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/action/Action.java",
    "content": "package cn.hutool.http.server.action;\n\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\n\nimport java.io.IOException;\n\n/**\n * 请求处理接口<br>\n * 当用户请求某个Path，则调用相应Action的doAction方法\n *\n * @author Looly\n * @since 5.2.6\n */\n@FunctionalInterface\npublic interface Action {\n\n\t/**\n\t * 处理请求\n\t *\n\t * @param request  请求对象\n\t * @param response 响应对象\n\t * @throws IOException IO异常\n\t */\n\tvoid doAction(HttpServerRequest request, HttpServerResponse response) throws IOException;\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/action/RootAction.java",
    "content": "package cn.hutool.http.server.action;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * 默认的处理器，通过解析用户传入的path，找到网页根目录下对应文件后返回\n *\n * @author looly\n * @since 5.2.6\n */\npublic class RootAction implements Action {\n\n\tpublic static final String DEFAULT_INDEX_FILE_NAME = \"index.html\";\n\n\tprivate final File rootDir;\n\tprivate final List<String> indexFileNames;\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootDir 网页根目录\n\t */\n\tpublic RootAction(String rootDir) {\n\t\tthis(new File(rootDir));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootDir 网页根目录\n\t */\n\tpublic RootAction(File rootDir) {\n\t\tthis(rootDir, DEFAULT_INDEX_FILE_NAME);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootDir        网页根目录\n\t * @param indexFileNames 主页文件名列表\n\t */\n\tpublic RootAction(String rootDir, String... indexFileNames) {\n\t\tthis(new File(rootDir), indexFileNames);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param rootDir        网页根目录\n\t * @param indexFileNames 主页文件名列表\n\t * @since 5.4.0\n\t */\n\tpublic RootAction(File rootDir, String... indexFileNames) {\n\t\tthis.rootDir = rootDir;\n\t\tthis.indexFileNames = CollUtil.toList(indexFileNames);\n\t}\n\n\t@Override\n\tpublic void doAction(HttpServerRequest request, HttpServerResponse response) {\n\t\tfinal String path = request.getPath();\n\n\t\tFile file = FileUtil.file(rootDir, path);\n\t\tif (file.exists()) {\n\t\t\tif (file.isDirectory()) {\n\t\t\t\tfor (String indexFileName : indexFileNames) {\n\t\t\t\t\t//默认读取主页\n\t\t\t\t\tfile = FileUtil.file(file, indexFileName);\n\t\t\t\t\tif (file.exists() && file.isFile()) {\n\t\t\t\t\t\tresponse.write(file);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else{\n\t\t\t\tfinal String name = request.getParam(\"name\");\n\t\t\t\tresponse.write(file, name);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tresponse.send404(\"404 Not Found !\");\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/action/package-info.java",
    "content": "/**\n * {@link com.sun.net.httpserver.HttpServer} 封装\n *\n * @author looly\n */\npackage cn.hutool.http.server.action;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/filter/DefaultExceptionFilter.java",
    "content": "/*\n * Copyright (c) 2023. looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          https://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http.server.filter;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\n\n/**\n * 默认异常处理拦截器\n *\n * @author looly\n */\npublic class DefaultExceptionFilter extends ExceptionFilter{\n\n\tprivate final static String TEMPLATE_ERROR = \"<!DOCTYPE html><html><head><title>Hutool - Error report</title><style>h1,h3 {color:white; background-color: gray;}</style></head><body><h1>HTTP Status {} - {}</h1><hr size=\\\"1\\\" noshade=\\\"noshade\\\" /><p>{}</p><hr size=\\\"1\\\" noshade=\\\"noshade\\\" /><h3>Hutool</h3></body></html>\";\n\n\t@Override\n\tpublic void afterException(final HttpServerRequest req, final HttpServerResponse res, final Throwable e) {\n\t\tString content = ExceptionUtil.stacktraceToString(e);\n\t\tcontent = content.replace(\"\\n\", \"<br/>\\n\");\n\t\tcontent = StrUtil.format(TEMPLATE_ERROR, 500, req.getURI(), content);\n\n\t\tres.sendError(500, content);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/filter/ExceptionFilter.java",
    "content": "/*\n * Copyright (c) 2023. looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          https://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http.server.filter;\n\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\nimport com.sun.net.httpserver.Filter;\n\n/**\n * 异常处理过滤器\n *\n * @author looly\n */\npublic abstract class ExceptionFilter implements HttpFilter {\n\n\t@Override\n\tpublic void doFilter(final HttpServerRequest req, final HttpServerResponse res, final Filter.Chain chain) {\n\t\ttry {\n\t\t\tchain.doFilter(req.getHttpExchange());\n\t\t} catch (final Throwable e) {\n\t\t\tafterException(req, res, e);\n\t\t}\n\t}\n\n\t/**\n\t * 异常之后的处理逻辑\n\t *\n\t * @param req {@link HttpServerRequest}\n\t * @param res {@link HttpServerResponse}\n\t * @param e   异常\n\t */\n\tpublic abstract void afterException(final HttpServerRequest req, final HttpServerResponse res, final Throwable e);\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/filter/HttpFilter.java",
    "content": "package cn.hutool.http.server.filter;\n\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\nimport com.sun.net.httpserver.Filter;\n\nimport java.io.IOException;\n\n/**\n * 过滤器接口，用于简化{@link Filter} 使用\n *\n * @author looly\n * @since 5.5.7\n */\n@FunctionalInterface\npublic interface HttpFilter {\n\n\t/**\n\t * 执行过滤\n\t * @param req {@link HttpServerRequest} 请求对象，用于获取请求内容\n\t * @param res {@link HttpServerResponse} 响应对象，用于写出内容\n\t * @param chain {@link Filter.Chain}\n\t * @throws IOException IO异常\n\t */\n\tvoid doFilter(HttpServerRequest req, HttpServerResponse res, Filter.Chain chain) throws IOException;\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/filter/SimpleFilter.java",
    "content": "package cn.hutool.http.server.filter;\n\nimport com.sun.net.httpserver.Filter;\n\n/**\n * 匿名简单过滤器，跳过了描述\n *\n * @author looly\n * @since 5.5.7\n */\npublic abstract class SimpleFilter extends Filter {\n\n\t@Override\n\tpublic String description() {\n\t\treturn \"Anonymous Filter\";\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/filter/package-info.java",
    "content": "/**\n * {@link com.sun.net.httpserver.Filter} 实现包装\n */\npackage cn.hutool.http.server.filter;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/handler/ActionHandler.java",
    "content": "package cn.hutool.http.server.handler;\n\nimport cn.hutool.http.server.HttpExchangeWrapper;\nimport cn.hutool.http.server.HttpServerRequest;\nimport cn.hutool.http.server.HttpServerResponse;\nimport cn.hutool.http.server.action.Action;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\n\nimport java.io.IOException;\n\n/**\n * Action处理器，用于将HttpHandler转换为Action形式\n *\n * @author looly\n * @since 5.2.6\n */\npublic class ActionHandler implements HttpHandler {\n\n\tprivate final Action action;\n\n\t/**\n\t * 构造\n\t *\n\t * @param action Action\n\t */\n\tpublic ActionHandler(Action action) {\n\t\tthis.action = action;\n\t}\n\n\t@Override\n\tpublic void handle(HttpExchange httpExchange) throws IOException {\n\t\tfinal HttpServerRequest request;\n\t\tfinal HttpServerResponse response;\n\t\tif (httpExchange instanceof HttpExchangeWrapper) {\n\t\t\t// issue#3343 当使用Filter时，可能读取了请求参数，此时使用共享的req和res，可复用缓存\n\t\t\tfinal HttpExchangeWrapper wrapper = (HttpExchangeWrapper) httpExchange;\n\t\t\trequest = wrapper.getRequest();\n\t\t\tresponse = wrapper.getResponse();\n\t\t} else {\n\t\t\trequest = new HttpServerRequest(httpExchange);\n\t\t\tresponse = new HttpServerResponse(httpExchange);\n\t\t}\n\t\taction.doAction(request, response);\n\t\thttpExchange.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/handler/package-info.java",
    "content": "/**\n * {@link com.sun.net.httpserver.HttpHandler} 实现包装\n */\npackage cn.hutool.http.server.handler;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/server/package-info.java",
    "content": "/**\n * Http服务器封装\n * \n * @author looly\n *\n */\npackage cn.hutool.http.server;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/AndroidSupportSSLFactory.java",
    "content": "package cn.hutool.http.ssl;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.net.SSLProtocols;\n\n/**\n * 兼容android低版本SSL连接<br>\n * 在测试HttpUrlConnection的时候，发现一部分手机无法连接[GithubPage]\n *\n * <p>\n * 最后发现原来是某些SSL协议没有开启\n *\n * @author MikaGuraNTK\n */\npublic class AndroidSupportSSLFactory extends CustomProtocolsSSLFactory {\n\n\t// Android低版本不重置的话某些SSL访问就会失败\n\tprivate static final String[] protocols = {\n\t\t\tSSLProtocols.SSLv3, SSLProtocols.TLSv1, SSLProtocols.TLSv11, SSLProtocols.TLSv12};\n\n\tpublic AndroidSupportSSLFactory() throws IORuntimeException {\n\t\tsuper(protocols);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java",
    "content": "package cn.hutool.http.ssl;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.net.SSLUtil;\nimport cn.hutool.core.util.ArrayUtil;\n\nimport javax.net.ssl.SSLSocket;\nimport javax.net.ssl.SSLSocketFactory;\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.Socket;\n\n/**\n * 自定义支持协议类型的SSLSocketFactory\n *\n * @author looly\n */\npublic class CustomProtocolsSSLFactory extends SSLSocketFactory {\n\n\tprivate final String[] protocols;\n\tprivate final SSLSocketFactory base;\n\n\t/**\n\t * 构造\n\t *\n\t * @param protocols 支持协议列表\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic CustomProtocolsSSLFactory(String... protocols) throws IORuntimeException {\n\t\tthis.protocols = protocols;\n\t\tthis.base = SSLUtil.createSSLContext(null).getSocketFactory();\n\t}\n\n\t@Override\n\tpublic String[] getDefaultCipherSuites() {\n\t\treturn base.getDefaultCipherSuites();\n\t}\n\n\t@Override\n\tpublic String[] getSupportedCipherSuites() {\n\t\treturn base.getSupportedCipherSuites();\n\t}\n\n\t@Override\n\tpublic Socket createSocket() throws IOException {\n\t\tfinal SSLSocket sslSocket = (SSLSocket) base.createSocket();\n\t\tresetProtocols(sslSocket);\n\t\treturn sslSocket;\n\t}\n\n\t@Override\n\tpublic SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {\n\t\tfinal SSLSocket socket = (SSLSocket) base.createSocket(s, host, port, autoClose);\n\t\tresetProtocols(socket);\n\t\treturn socket;\n\t}\n\n\t@Override\n\tpublic Socket createSocket(String host, int port) throws IOException {\n\t\tfinal SSLSocket socket = (SSLSocket) base.createSocket(host, port);\n\t\tresetProtocols(socket);\n\t\treturn socket;\n\t}\n\n\t@Override\n\tpublic Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {\n\t\tfinal SSLSocket socket = (SSLSocket) base.createSocket(host, port, localHost, localPort);\n\t\tresetProtocols(socket);\n\t\treturn socket;\n\t}\n\n\t@Override\n\tpublic Socket createSocket(InetAddress host, int port) throws IOException {\n\t\tfinal SSLSocket socket = (SSLSocket) base.createSocket(host, port);\n\t\tresetProtocols(socket);\n\t\treturn socket;\n\t}\n\n\t@Override\n\tpublic Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {\n\t\tfinal SSLSocket socket = (SSLSocket) base.createSocket(address, port, localAddress, localPort);\n\t\tresetProtocols(socket);\n\t\treturn socket;\n\t}\n\n\t/**\n\t * 重置可用策略\n\t *\n\t * @param socket SSLSocket\n\t */\n\tprivate void resetProtocols(SSLSocket socket) {\n\t\tif (ArrayUtil.isNotEmpty(this.protocols)) {\n\t\t\tsocket.setEnabledProtocols(this.protocols);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/DefaultSSLFactory.java",
    "content": "package cn.hutool.http.ssl;\n\n/**\n * 默认的SSLSocketFactory\n *\n * @author Looly\n * @since 5.1.2\n */\npublic class DefaultSSLFactory extends CustomProtocolsSSLFactory {\n\n\tpublic DefaultSSLFactory() {\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/DefaultSSLInfo.java",
    "content": "package cn.hutool.http.ssl;\n\nimport cn.hutool.core.util.JdkUtil;\n\nimport javax.net.ssl.SSLSocketFactory;\n\n/**\n * 默认的全局SSL配置，当用户未设置相关信息时，使用默认设置，默认设置为单例模式。\n *\n * @author looly\n * @since 5.1.2\n */\npublic class DefaultSSLInfo {\n\t/**\n\t * 默认信任全部的域名校验器\n\t */\n\tpublic static final TrustAnyHostnameVerifier TRUST_ANY_HOSTNAME_VERIFIER;\n\t/**\n\t * 默认的SSLSocketFactory，区分安卓\n\t */\n\tpublic static final SSLSocketFactory DEFAULT_SSF;\n\n\tstatic {\n\t\tTRUST_ANY_HOSTNAME_VERIFIER = new TrustAnyHostnameVerifier();\n\t\tif (JdkUtil.IS_ANDROID) {\n\t\t\t// 兼容android低版本SSL连接\n\t\t\tDEFAULT_SSF = new AndroidSupportSSLFactory();\n\t\t} else {\n\t\t\tDEFAULT_SSF = new DefaultSSLFactory();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java",
    "content": "package cn.hutool.http.ssl;\n\nimport cn.hutool.core.net.SSLContextBuilder;\nimport cn.hutool.core.net.SSLProtocols;\n\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.SSLSocketFactory;\nimport javax.net.ssl.TrustManager;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\n\n/**\n * SSLSocketFactory构建器\n *\n * @author Looly\n * @see SSLContextBuilder\n * @deprecated 请使用 {@link SSLContextBuilder}\n */\n@Deprecated\npublic class SSLSocketFactoryBuilder implements SSLProtocols {\n\n\tSSLContextBuilder sslContextBuilder;\n\n\t/**\n\t * 构造\n\t */\n\tpublic SSLSocketFactoryBuilder() {\n\t\tthis.sslContextBuilder = SSLContextBuilder.create();\n\t}\n\n\t/**\n\t * 创建 SSLSocketFactoryBuilder\n\t *\n\t * @return SSLSocketFactoryBuilder\n\t */\n\tpublic static SSLSocketFactoryBuilder create() {\n\t\treturn new SSLSocketFactoryBuilder();\n\t}\n\n\t/**\n\t * 设置协议\n\t *\n\t * @param protocol 协议\n\t * @return 自身\n\t */\n\tpublic SSLSocketFactoryBuilder setProtocol(String protocol) {\n\t\tthis.sslContextBuilder.setProtocol(protocol);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置信任信息\n\t *\n\t * @param trustManagers TrustManager列表\n\t * @return 自身\n\t */\n\tpublic SSLSocketFactoryBuilder setTrustManagers(TrustManager... trustManagers) {\n\t\tthis.sslContextBuilder.setTrustManagers(trustManagers);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置 JSSE key managers\n\t *\n\t * @param keyManagers JSSE key managers\n\t * @return 自身\n\t */\n\tpublic SSLSocketFactoryBuilder setKeyManagers(KeyManager... keyManagers) {\n\t\tthis.sslContextBuilder.setKeyManagers(keyManagers);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置 SecureRandom\n\t *\n\t * @param secureRandom SecureRandom\n\t * @return 自己\n\t */\n\tpublic SSLSocketFactoryBuilder setSecureRandom(SecureRandom secureRandom) {\n\t\tthis.sslContextBuilder.setSecureRandom(secureRandom);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 构建SSLSocketFactory\n\t *\n\t * @return SSLSocketFactory\n\t * @throws NoSuchAlgorithmException 无此算法\n\t * @throws KeyManagementException   Key管理异常\n\t */\n\tpublic SSLSocketFactory build() throws NoSuchAlgorithmException, KeyManagementException {\n\t\treturn this.sslContextBuilder.buildChecked().getSocketFactory();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java",
    "content": "package cn.hutool.http.ssl;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLSession;\n\n/**\n * https 域名校验\n * \n * @author Looly\n */\npublic class TrustAnyHostnameVerifier implements HostnameVerifier {\n\t\n\t@Override\n\tpublic boolean verify(String hostname, SSLSession session) {\n\t\treturn true;// 直接返回true\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/ssl/package-info.java",
    "content": "/**\n * SSL封装\n * \n * @author looly\n *\n */\npackage cn.hutool.http.ssl;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/Browser.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReUtil;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n/**\n * 浏览器对象\n *\n * @author looly\n * @since 4.2.1\n */\npublic class Browser extends UserAgentInfo {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 未知\n\t */\n\tpublic static final Browser Unknown = new Browser(NameUnknown, null, null);\n\t/**\n\t * 其它版本\n\t */\n\tpublic static final String Other_Version = \"[\\\\/ ]([\\\\d\\\\w\\\\.\\\\-]+)\";\n\n\t/**\n\t * 支持的浏览器类型\n\t */\n\tpublic static final List<Browser> browers = CollUtil.newArrayList(\n\t\t// 部分特殊浏览器是基于安卓、Iphone等的，需要优先判断\n\t\t// 企业微信 企业微信使用微信浏览器内核,会包含 MicroMessenger 所以要放在前面\n\t\tnew Browser(\"wxwork\", \"wxwork\", \"wxwork\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\t// issue#IB3SJF 微信电脑端\n\t\tnew Browser(\"WindowsWechat\", \"WindowsWechat\", \"MicroMessenger\" + Other_Version),\n\t\t// 微信\n\t\tnew Browser(\"MicroMessenger\", \"MicroMessenger\", Other_Version),\n\t\t// 微信小程序\n\t\tnew Browser(\"miniProgram\", \"miniProgram\", Other_Version),\n\t\t// QQ浏览器\n\t\tnew Browser(\"QQBrowser\", \"QQBrowser\", \"QQBrowser\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\t// 钉钉PC端浏览器\n\t\tnew Browser(\"DingTalk-win\", \"dingtalk-win\", \"DingTalk\\\\(([\\\\d\\\\w\\\\.\\\\-]+)\\\\)\"),\n\t\t// 钉钉内置浏览器\n\t\tnew Browser(\"DingTalk\", \"DingTalk\", \"AliApp\\\\(DingTalk\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\\\\)\"),\n\t\t// 支付宝内置浏览器\n\t\tnew Browser(\"Alipay\", \"AlipayClient\", \"AliApp\\\\(AP\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\\\\)\"),\n\t\t// 淘宝内置浏览器\n\t\tnew Browser(\"Taobao\", \"taobao\", \"AliApp\\\\(TB\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\\\\)\"),\n\t\t// UC浏览器\n\t\tnew Browser(\"UCBrowser\", \"UC?Browser\", \"UC?Browser\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\t// XiaoMi 浏览器\n\t\tnew Browser(\"MiuiBrowser\", \"MiuiBrowser|mibrowser\", \"MiuiBrowser\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\t// 夸克浏览器\n\t\tnew Browser(\"Quark\", \"Quark\", Other_Version),\n\t\t// 联想浏览器\n\t\tnew Browser(\"Lenovo\", \"SLBrowser\", \"SLBrowser/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"MSEdge\", \"Edge|Edg\", \"(?:edge|Edg|EdgA)\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Chrome\", \"chrome|(iphone.*crios.*safari)\", \"(?:Chrome|CriOS)\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Firefox\", \"firefox\", Other_Version),\n\t\tnew Browser(\"IEMobile\", \"iemobile\", Other_Version),\n\t\tnew Browser(\"Android Browser\", \"android\", \"version\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Safari\", \"safari\", \"version\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Opera\", \"opera\", Other_Version),\n\t\tnew Browser(\"Konqueror\", \"konqueror\", Other_Version),\n\t\tnew Browser(\"PS3\", \"playstation 3\", \"([\\\\d\\\\w\\\\.\\\\-]+)\\\\)\\\\s*$\"),\n\t\tnew Browser(\"PSP\", \"playstation portable\", \"([\\\\d\\\\w\\\\.\\\\-]+)\\\\)?\\\\s*$\"),\n\t\tnew Browser(\"Lotus\", \"lotus.notes\", \"Lotus-Notes\\\\/([\\\\w.]+)\"),\n\t\tnew Browser(\"Thunderbird\", \"thunderbird\", Other_Version),\n\t\tnew Browser(\"Netscape\", \"netscape\", Other_Version),\n\t\tnew Browser(\"Seamonkey\", \"seamonkey\", Other_Version),\n\t\tnew Browser(\"Outlook\", \"microsoft.outlook\", Other_Version),\n\t\tnew Browser(\"Evolution\", \"evolution\", Other_Version),\n\t\tnew Browser(\"MSIE\", \"msie\", \"msie ([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"MSIE11\", \"rv:11\", \"rv:([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Gabble\", \"Gabble\", Other_Version),\n\t\tnew Browser(\"Yammer Desktop\", \"AdobeAir\", \"([\\\\d\\\\w\\\\.\\\\-]+)\\\\/Yammer\"),\n\t\tnew Browser(\"Yammer Mobile\", \"Yammer[\\\\s]+([\\\\d\\\\w\\\\.\\\\-]+)\", \"Yammer[\\\\s]+([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"Apache HTTP Client\", \"Apache\\\\\\\\-HttpClient\", \"Apache\\\\-HttpClient\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\tnew Browser(\"BlackBerry\", \"BlackBerry\", \"BlackBerry[\\\\d]+\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"),\n\t\t// issue#I847JY 百度浏览器\n\t\tnew Browser(\"Baidu\", \"Baidu\", \"baiduboxapp\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\")\n\t);\n\n\t/**\n\t * 添加自定义的浏览器类型\n\t *\n\t * @param name         浏览器名称\n\t * @param regex        关键字或表达式\n\t * @param versionRegex 匹配版本的正则\n\t * @since 5.7.4\n\t */\n\tsynchronized public static void addCustomBrowser(String name, String regex, String versionRegex) {\n\t\tbrowers.add(new Browser(name, regex, versionRegex));\n\t}\n\n\tprivate Pattern versionPattern;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name         浏览器名称\n\t * @param regex        关键字或表达式\n\t * @param versionRegex 匹配版本的正则\n\t */\n\tpublic Browser(String name, String regex, String versionRegex) {\n\t\tsuper(name, regex);\n\t\tif (Other_Version.equals(versionRegex)) {\n\t\t\tversionRegex = name + versionRegex;\n\t\t}\n\t\tif (null != versionRegex) {\n\t\t\tthis.versionPattern = Pattern.compile(versionRegex, Pattern.CASE_INSENSITIVE);\n\t\t}\n\t}\n\n\t/**\n\t * 获取浏览器版本\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 版本\n\t */\n\tpublic String getVersion(String userAgentString) {\n\t\tif (isUnknown()) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ReUtil.getGroup1(this.versionPattern, userAgentString);\n\t}\n\n\t/**\n\t * 是否移动浏览器\n\t *\n\t * @return 是否移动浏览器\n\t */\n\tpublic boolean isMobile() {\n\t\tfinal String name = this.getName();\n\t\treturn \"PSP\".equals(name) ||\n\t\t\t\"Yammer Mobile\".equals(name) ||\n\t\t\t\"Android Browser\".equals(name) ||\n\t\t\t\"IEMobile\".equals(name) ||\n\t\t\t\"MicroMessenger\".equals(name) ||\n\t\t\t\"miniProgram\".equals(name) ||\n\t\t\t\"DingTalk\".equals(name);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/Engine.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReUtil;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n/**\n * 引擎对象\n *\n * @author looly\n * @since 4.2.1\n */\npublic class Engine extends UserAgentInfo {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 未知 */\n\tpublic static final Engine Unknown = new Engine(NameUnknown, null);\n\n\t/**\n\t * 支持的引擎类型\n\t */\n\tpublic static final List<Engine> engines = CollUtil.newArrayList(\n\t\t\tnew Engine(\"Trident\", \"trident\"),\n\t\t\tnew Engine(\"Webkit\", \"webkit\"),\n\t\t\tnew Engine(\"Chrome\", \"chrome\"),\n\t\t\tnew Engine(\"Opera\", \"opera\"),\n\t\t\tnew Engine(\"Presto\", \"presto\"),\n\t\t\tnew Engine(\"Gecko\", \"gecko\"),\n\t\t\tnew Engine(\"KHTML\", \"khtml\"),\n\t\t\tnew Engine(\"Konqueror\", \"konqueror\"),\n\t\t\tnew Engine(\"MIDP\", \"MIDP\")\n\t);\n\n\tprivate final Pattern versionPattern;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 引擎名称\n\t * @param regex 关键字或表达式\n\t */\n\tpublic Engine(String name, String regex) {\n\t\tsuper(name, regex);\n\t\tthis.versionPattern = Pattern.compile(name + \"[/\\\\- ]([\\\\d\\\\w.\\\\-]+)\", Pattern.CASE_INSENSITIVE);\n\t}\n\n\t/**\n\t * 获取引擎版本\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 版本\n\t * @since 5.7.4\n\t */\n\tpublic String getVersion(String userAgentString) {\n\t\tif(isUnknown()){\n\t\t\treturn null;\n\t\t}\n\t\treturn ReUtil.getGroup1(this.versionPattern, userAgentString);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/OS.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ReUtil;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\n/**\n * 系统对象\n *\n * @author looly\n * @since 4.2.1\n */\npublic class OS extends UserAgentInfo {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 未知\n\t */\n\tpublic static final OS Unknown = new OS(NameUnknown, null);\n\n\t/**\n\t * 支持的引擎类型\n\t */\n\tpublic static final List<OS> oses = CollUtil.newArrayList(//\n\t\t\tnew OS(\"Windows 10 or Windows Server 2016\", \"windows nt 10\\\\.0\", \"windows nt (10\\\\.0)\"),//\n\t\t\tnew OS(\"Windows 8.1 or Windows Server 2012R2\", \"windows nt 6\\\\.3\", \"windows nt (6\\\\.3)\"),//\n\t\t\tnew OS(\"Windows 8 or Windows Server 2012\", \"windows nt 6\\\\.2\", \"windows nt (6\\\\.2)\"),//\n\t\t\tnew OS(\"Windows Vista\", \"windows nt 6\\\\.0\", \"windows nt (6\\\\.0)\"), //\n\t\t\tnew OS(\"Windows 7 or Windows Server 2008R2\", \"windows nt 6\\\\.1\", \"windows nt (6\\\\.1)\"), //\n\t\t\tnew OS(\"Windows 2003\", \"windows nt 5\\\\.2\", \"windows nt (5\\\\.2)\"), //\n\t\t\tnew OS(\"Windows XP\", \"windows nt 5\\\\.1\", \"windows nt (5\\\\.1)\"), //\n\t\t\tnew OS(\"Windows 2000\", \"windows nt 5\\\\.0\", \"windows nt (5\\\\.0)\"), //\n\t\t\tnew OS(\"Windows Phone\", \"windows (ce|phone|mobile)( os)?\", \"windows (?:ce|phone|mobile) (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"Windows\", \"windows\"), //\n\t\t\tnew OS(\"OSX\", \"os x (\\\\d+)[._](\\\\d+)\", \"os x (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"Android\", \"Android\", \"Android (\\\\d+([._]\\\\d+)*)\"),//\n\t\t\tnew OS(\"Harmony\", \"OpenHarmony\", \"OpenHarmony (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"Android\", \"XiaoMi|MI\\\\s+\", \"\\\\(X(\\\\d+([._]\\\\d+)*)\"),//\n\t\t\tnew OS(\"Linux\", \"linux\"), //\n\t\t\tnew OS(\"Wii\", \"wii\", \"wii libnup/(\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"PS3\", \"playstation 3\", \"playstation 3; (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"PSP\", \"playstation portable\", \"Portable\\\\); (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"iPad\", \"\\\\(iPad.*os (\\\\d+)[._](\\\\d+)\", \"\\\\(iPad.*os (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"iPhone\", \"\\\\(iPhone.*os (\\\\d+)[._](\\\\d+)\", \"\\\\(iPhone.*os (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"YPod\", \"iPod touch[\\\\s\\\\;]+iPhone.*os (\\\\d+)[._](\\\\d+)\", \"iPod touch[\\\\s\\\\;]+iPhone.*os (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"YPad\", \"iPad[\\\\s\\\\;]+iPhone.*os (\\\\d+)[._](\\\\d+)\", \"iPad[\\\\s\\\\;]+iPhone.*os (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"YPhone\", \"iPhone[\\\\s\\\\;]+iPhone.*os (\\\\d+)[._](\\\\d+)\", \"iPhone[\\\\s\\\\;]+iPhone.*os (\\\\d+([._]\\\\d+)*)\"), //\n\t\t\tnew OS(\"Symbian\", \"symbian(os)?\"), //\n\t\t\tnew OS(\"Darwin\", \"Darwin\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\", \"Darwin\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"), //\n\t\t\tnew OS(\"Adobe Air\", \"AdobeAir\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\", \"AdobeAir\\\\/([\\\\d\\\\w\\\\.\\\\-]+)\"), //\n\t\t\tnew OS(\"Java\", \"Java[\\\\s]+([\\\\d\\\\w\\\\.\\\\-]+)\", \"Java[\\\\s]+([\\\\d\\\\w\\\\.\\\\-]+)\")//\n\t);\n\n\t/**\n\t * 添加自定义的系统类型\n\t *\n\t * @param name         浏览器名称\n\t * @param regex        关键字或表达式\n\t * @param versionRegex 匹配版本的正则\n\t * @since 5.7.4\n\t */\n\tsynchronized public static void addCustomOs(String name, String regex, String versionRegex) {\n\t\toses.add(new OS(name, regex, versionRegex));\n\t}\n\n\tprivate Pattern versionPattern;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name  系统名称\n\t * @param regex 关键字或表达式\n\t */\n\tpublic OS(String name, String regex) {\n\t\tthis(name, regex, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param name         系统名称\n\t * @param regex        关键字或表达式\n\t * @param versionRegex 版本正则表达式\n\t * @since 5.7.4\n\t */\n\tpublic OS(String name, String regex, String versionRegex) {\n\t\tsuper(name, regex);\n\t\tif (null != versionRegex) {\n\t\t\tthis.versionPattern = Pattern.compile(versionRegex, Pattern.CASE_INSENSITIVE);\n\t\t}\n\t}\n\n\t/**\n\t * 获取浏览器版本\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 版本\n\t */\n\tpublic String getVersion(String userAgentString) {\n\t\tif(isUnknown() || null == this.versionPattern){\n\t\t\t// 无版本信息\n\t\t\treturn null;\n\t\t}\n\t\treturn ReUtil.getGroup1(this.versionPattern, userAgentString);\n\t}\n\n\t/**\n\t * 是否为MacOS\n\t *\n\t * @return 是否为MacOS\n\t * @since 5.8.29\n\t */\n\tpublic boolean isMacOS(){\n\t\treturn \"OSX\".equals(getName());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/Platform.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.collection.CollUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 平台对象\n *\n * @author looly\n * @since 4.2.1\n */\npublic class Platform extends UserAgentInfo {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 未知\n\t */\n\tpublic static final Platform Unknown = new Platform(NameUnknown, null);\n\n\t/**\n\t * Iphone\n\t */\n\tpublic static final Platform IPHONE = new Platform(\"iPhone\", \"iphone\");\n\t/**\n\t * ipod\n\t */\n\tpublic static final Platform IPOD = new Platform(\"iPod\", \"ipod\");\n\t/**\n\t * ipad\n\t */\n\tpublic static final Platform IPAD = new Platform(\"iPad\", \"ipad\");\n\n\t/**\n\t * android\n\t */\n\tpublic static final Platform ANDROID = new Platform(\"Android\", \"android\");\n\n\t/**\n\t * harmony\n\t */\n\tpublic static final Platform HARMONY = new Platform(\"Harmony\", \"OpenHarmony\");\n\t/**\n\t * android\n\t */\n\tpublic static final Platform GOOGLE_TV = new Platform(\"GoogleTV\", \"googletv\");\n\n\t/**\n\t * Windows Phone\n\t */\n\tpublic static final Platform WINDOWS_PHONE = new Platform(\"Windows Phone\", \"windows (ce|phone|mobile)( os)?\");\n\n\t/**\n\t * 支持的移动平台类型\n\t */\n\tpublic static final List<Platform> mobilePlatforms = CollUtil.newArrayList(//\n\t\t\tWINDOWS_PHONE, //\n\t\t\tIPAD, //\n\t\t\tIPOD, //\n\t\t\tIPHONE, //\n\t\t\tnew Platform(\"Android\", \"XiaoMi|MI\\\\s+\"), //\n\t\t\tANDROID, //\n\t\t\tGOOGLE_TV, //\n\t\t\tnew Platform(\"htcFlyer\", \"htc_flyer\"), //\n\t\t\tnew Platform(\"Symbian\", \"symbian(os)?\"), //\n\t\t\tnew Platform(\"Blackberry\", \"blackberry\"), //\n\t\t\tHARMONY\n\t);\n\n\t/**\n\t * 支持的桌面平台类型\n\t */\n\tpublic static final List<Platform> desktopPlatforms = CollUtil.newArrayList(//\n\t\t\tnew Platform(\"Windows\", \"windows\"), //\n\t\t\tnew Platform(\"Mac\", \"(macintosh|darwin)\"), //\n\t\t\tnew Platform(\"Linux\", \"linux\"), //\n\t\t\tnew Platform(\"Wii\", \"wii\"), //\n\t\t\tnew Platform(\"Playstation\", \"playstation\"), //\n\t\t\tnew Platform(\"Java\", \"java\") //\n\t);\n\n\t/**\n\t * 支持的平台类型\n\t */\n\tpublic static final List<Platform> platforms;\n\n\tstatic {\n\t\tplatforms = new ArrayList<>(13);\n\t\tplatforms.addAll(mobilePlatforms);\n\t\tplatforms.addAll(desktopPlatforms);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param name  平台名称\n\t * @param regex 关键字或表达式\n\t */\n\tpublic Platform(String name, String regex) {\n\t\tsuper(name, regex);\n\t}\n\n\t/**\n\t * 是否为移动平台\n\t *\n\t * @return 是否为移动平台\n\t */\n\tpublic boolean isMobile() {\n\t\treturn mobilePlatforms.contains(this);\n\t}\n\n\t/**\n\t * 是否为Iphone或者iPod设备\n\t *\n\t * @return 是否为Iphone或者iPod设备\n\t * @since 5.2.3\n\t */\n\tpublic boolean isIPhoneOrIPod() {\n\t\treturn this.equals(IPHONE) || this.equals(IPOD);\n\t}\n\n\t/**\n\t * 是否为Iphone或者iPod设备\n\t *\n\t * @return 是否为Iphone或者iPod设备\n\t * @since 5.2.3\n\t */\n\tpublic boolean isIPad() {\n\t\treturn this.equals(IPAD);\n\t}\n\n\t/**\n\t * 是否为IOS平台，包括IPhone、IPod、IPad\n\t *\n\t * @return 是否为IOS平台，包括IPhone、IPod、IPad\n\t * @since 5.2.3\n\t */\n\tpublic boolean isIos() {\n\t\treturn isIPhoneOrIPod() || isIPad();\n\t}\n\n\t/**\n\t * 是否为Android平台，包括Android和Google TV\n\t *\n\t * @return 是否为Android平台，包括Android和Google TV\n\t * @since 5.2.3\n\t */\n\tpublic boolean isAndroid() {\n\t\treturn this.equals(ANDROID) || this.equals(GOOGLE_TV);\n\t}\n\n\t/**\n\t * 是否为Harmony平台\n\t *\n\t * @return 是否为Harmony平台\n\t */\n\tpublic boolean isHarmony() {\n\t\treturn this.equals(HARMONY);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/UserAgent.java",
    "content": "package cn.hutool.http.useragent;\n\nimport java.io.Serializable;\n\n/**\n * User-Agent信息对象\n *\n * @author looly\n * @since 4.2.1\n */\npublic class UserAgent implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 是否为移动平台\n\t */\n\tprivate boolean mobile;\n\t/**\n\t * 浏览器类型\n\t */\n\tprivate Browser browser;\n\t/**\n\t * 浏览器版本\n\t */\n\tprivate String version;\n\n\t/**\n\t * 平台类型\n\t */\n\tprivate Platform platform;\n\n\t/**\n\t * 系统类型\n\t */\n\tprivate OS os;\n\t/**\n\t * 系统版本\n\t */\n\tprivate String osVersion;\n\n\t/**\n\t * 引擎类型\n\t */\n\tprivate Engine engine;\n\t/**\n\t * 引擎版本\n\t */\n\tprivate String engineVersion;\n\n\t/**\n\t * 是否为移动平台\n\t *\n\t * @return 是否为移动平台\n\t */\n\tpublic boolean isMobile() {\n\t\treturn mobile;\n\t}\n\n\t/**\n\t * 设置是否为移动平台\n\t *\n\t * @param mobile 是否为移动平台\n\t */\n\tpublic void setMobile(boolean mobile) {\n\t\tthis.mobile = mobile;\n\t}\n\n\t/**\n\t * 获取浏览器类型\n\t *\n\t * @return 浏览器类型\n\t */\n\tpublic Browser getBrowser() {\n\t\treturn browser;\n\t}\n\n\t/**\n\t * 设置浏览器类型\n\t *\n\t * @param browser 浏览器类型\n\t */\n\tpublic void setBrowser(Browser browser) {\n\t\tthis.browser = browser;\n\t}\n\n\t/**\n\t * 获取平台类型\n\t *\n\t * @return 平台类型\n\t */\n\tpublic Platform getPlatform() {\n\t\treturn platform;\n\t}\n\n\t/**\n\t * 设置平台类型\n\t *\n\t * @param platform 平台类型\n\t */\n\tpublic void setPlatform(Platform platform) {\n\t\tthis.platform = platform;\n\t}\n\n\t/**\n\t * 获取系统类型\n\t *\n\t * @return 系统类型\n\t */\n\tpublic OS getOs() {\n\t\treturn os;\n\t}\n\n\t/**\n\t * 设置系统类型\n\t *\n\t * @param os 系统类型\n\t */\n\tpublic void setOs(OS os) {\n\t\tthis.os = os;\n\t}\n\n\t/**\n\t * 获取系统版本\n\t *\n\t * @return 系统版本\n\t * @since 5.7.4\n\t */\n\tpublic String getOsVersion() {\n\t\treturn this.osVersion;\n\t}\n\n\t/**\n\t * 设置系统版本\n\t *\n\t * @param osVersion 系统版本\n\t * @since 5.7.4\n\t */\n\tpublic void setOsVersion(String osVersion) {\n\t\tthis.osVersion = osVersion;\n\t}\n\n\t/**\n\t * 获取引擎类型\n\t *\n\t * @return 引擎类型\n\t */\n\tpublic Engine getEngine() {\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 设置引擎类型\n\t *\n\t * @param engine 引擎类型\n\t */\n\tpublic void setEngine(Engine engine) {\n\t\tthis.engine = engine;\n\t}\n\n\t/**\n\t * 获取浏览器版本\n\t *\n\t * @return 浏览器版本\n\t */\n\tpublic String getVersion() {\n\t\treturn version;\n\t}\n\n\t/**\n\t * 设置浏览器版本\n\t *\n\t * @param version 浏览器版本\n\t */\n\tpublic void setVersion(String version) {\n\t\tthis.version = version;\n\t}\n\n\t/**\n\t * 获取引擎版本\n\t *\n\t * @return 引擎版本\n\t */\n\tpublic String getEngineVersion() {\n\t\treturn engineVersion;\n\t}\n\n\t/**\n\t * 设置引擎版本\n\t *\n\t * @param engineVersion 引擎版本\n\t */\n\tpublic void setEngineVersion(String engineVersion) {\n\t\tthis.engineVersion = engineVersion;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentInfo.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.util.ReUtil;\n\nimport java.io.Serializable;\nimport java.util.regex.Pattern;\n\n/**\n * User-agent信息\n *\n * @author looly\n * @since 4.2.1\n */\npublic class UserAgentInfo implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 未知类型\n\t */\n\tpublic static final String NameUnknown = \"Unknown\";\n\n\t/** 信息名称 */\n\tprivate final String name;\n\t/** 信息匹配模式 */\n\tprivate final Pattern pattern;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 名字\n\t * @param regex 表达式\n\t */\n\tpublic UserAgentInfo(String name, String regex) {\n\t\tthis(name, (null == regex) ? null : Pattern.compile(regex, Pattern.CASE_INSENSITIVE));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 名字\n\t * @param pattern 匹配模式\n\t */\n\tpublic UserAgentInfo(String name, Pattern pattern) {\n\t\tthis.name = name;\n\t\tthis.pattern = pattern;\n\t}\n\n\t/**\n\t * 获取信息名称\n\t *\n\t * @return 信息名称\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * 获取匹配模式\n\t *\n\t * @return 匹配模式\n\t */\n\tpublic Pattern getPattern() {\n\t\treturn pattern;\n\t}\n\n\t/**\n\t * 指定内容中是否包含匹配此信息的内容\n\t *\n\t * @param content User-Agent字符串\n\t * @return 是否包含匹配此信息的内容\n\t */\n\tpublic boolean isMatch(String content) {\n\t\treturn ReUtil.contains(this.pattern, content);\n\t}\n\n\t/**\n\t * 是否为Unknown\n\t *\n\t * @return 是否为Unknown\n\t */\n\tpublic boolean isUnknown() {\n\t\treturn NameUnknown.equals(this.name);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + ((name == null) ? 0 : name.hashCode());\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal UserAgentInfo other = (UserAgentInfo) obj;\n\t\tif (name == null) {\n\t\t\treturn other.name == null;\n\t\t} else return name.equals(other.name);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn this.name;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentParser.java",
    "content": "package cn.hutool.http.useragent;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * User-Agent解析器\n *\n * @author looly\n * @since 4.2.1\n */\npublic class UserAgentParser {\n\n\t/**\n\t * 解析User-Agent\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return {@link UserAgent}\n\t */\n\tpublic static UserAgent parse(String userAgentString) {\n\t\tif(StrUtil.isBlank(userAgentString)){\n\t\t\treturn null;\n\t\t}\n\t\tfinal UserAgent userAgent = new UserAgent();\n\n\t\t// 浏览器\n\t\tfinal Browser browser = parseBrowser(userAgentString);\n\t\tuserAgent.setBrowser(browser);\n\t\tuserAgent.setVersion(browser.getVersion(userAgentString));\n\n\t\t// 浏览器引擎\n\t\tfinal Engine engine = parseEngine(userAgentString);\n\t\tuserAgent.setEngine(engine);\n\t\tuserAgent.setEngineVersion(engine.getVersion(userAgentString));\n\n\t\t// 操作系统\n\t\tfinal OS os = parseOS(userAgentString);\n\t\tuserAgent.setOs(os);\n\t\tuserAgent.setOsVersion(os.getVersion(userAgentString));\n\n\t\t// 平台\n\t\tfinal Platform platform = parsePlatform(userAgentString);\n\t\tuserAgent.setPlatform(platform);\n\n\t\t// issue#IA74K2 MACOS下的微信不属于移动平台\n\t\tif(platform.isMobile() || browser.isMobile()){\n\t\t\tif(false == os.isMacOS()){\n\t\t\t\tuserAgent.setMobile(true);\n\t\t\t}\n\t\t}\n\n\n\t\treturn userAgent;\n\t}\n\n\t/**\n\t * 解析浏览器类型\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 浏览器类型\n\t */\n\tprivate static Browser parseBrowser(String userAgentString) {\n\t\tfor (Browser browser : Browser.browers) {\n\t\t\tif (browser.isMatch(userAgentString)) {\n\t\t\t\treturn browser;\n\t\t\t}\n\t\t}\n\t\treturn Browser.Unknown;\n\t}\n\n\t/**\n\t * 解析引擎类型\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 引擎类型\n\t */\n\tprivate static Engine parseEngine(String userAgentString) {\n\t\tfor (Engine engine : Engine.engines) {\n\t\t\tif (engine.isMatch(userAgentString)) {\n\t\t\t\treturn engine;\n\t\t\t}\n\t\t}\n\t\treturn Engine.Unknown;\n\t}\n\n\t/**\n\t * 解析系统类型\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 系统类型\n\t */\n\tprivate static OS parseOS(String userAgentString) {\n\t\tfor (OS os : OS.oses) {\n\t\t\tif (os.isMatch(userAgentString)) {\n\t\t\t\treturn os;\n\t\t\t}\n\t\t}\n\t\treturn OS.Unknown;\n\t}\n\n\t/**\n\t * 解析平台类型\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return 平台类型\n\t */\n\tprivate static Platform parsePlatform(String userAgentString) {\n\t\tfor (Platform platform : Platform.platforms) {\n\t\t\tif (platform.isMatch(userAgentString)) {\n\t\t\t\treturn platform;\n\t\t\t}\n\t\t}\n\t\treturn Platform.Unknown;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentUtil.java",
    "content": "package cn.hutool.http.useragent;\n\n/**\n * User-Agent工具类\n *\n * @author looly\n *\n */\npublic class UserAgentUtil {\n\n\t/**\n\t * 解析User-Agent\n\t *\n\t * @param userAgentString User-Agent字符串\n\t * @return {@link UserAgent}\n\t */\n\tpublic static UserAgent parse(String userAgentString) {\n\t\treturn UserAgentParser.parse(userAgentString);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/useragent/package-info.java",
    "content": "/**\n * User-Agent解析\n * \n * @author looly\n *\n */\npackage cn.hutool.http.useragent;"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapClient.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.XmlUtil;\nimport cn.hutool.http.HttpBase;\nimport cn.hutool.http.HttpGlobalConfig;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport jakarta.xml.soap.*;\n\nimport javax.xml.XMLConstants;\nimport javax.xml.namespace.QName;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * SOAP客户端\n *\n * <p>\n * 此对象用于构建一个SOAP消息，并通过HTTP接口发出消息内容。\n * SOAP消息本质上是一个XML文本，可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体\n * <p>\n * 使用方法：\n *\n * <pre>\n * SoapClient client = SoapClient.create(url)\n * .setMethod(methodName, namespaceURI)\n * .setCharset(CharsetUtil.CHARSET_GBK)\n * .setParam(param1, \"XXX\");\n *\n * String response = client.send(true);\n *\n * </pre>\n *\n * @author looly\n * @since 5.8.42\n */\npublic class JakartaSoapClient extends HttpBase<JakartaSoapClient> {\n\n\t/**\n\t * XML消息体的Content-Type\n\t * soap1.1 : text/xml\n\t * soap1.2 : application/soap+xml\n\t * soap1.1与soap1.2区别:  https://www.cnblogs.com/qlqwjy/p/7577147.html\n\t */\n\tprivate static final String CONTENT_TYPE_SOAP11_TEXT_XML = \"text/xml;charset=\";\n\tprivate static final String CONTENT_TYPE_SOAP12_SOAP_XML = \"application/soap+xml;charset=\";\n\n\t/**\n\t * 请求的URL地址\n\t */\n\tprivate String url;\n\n\t/**\n\t * 默认连接超时\n\t */\n\tprivate int connectionTimeout = HttpGlobalConfig.getTimeout();\n\t/**\n\t * 默认读取超时\n\t */\n\tprivate int readTimeout = HttpGlobalConfig.getTimeout();\n\n\t/**\n\t * 消息工厂，用于创建消息\n\t */\n\tprivate MessageFactory factory;\n\t/**\n\t * SOAP消息\n\t */\n\tprivate SOAPMessage message;\n\t/**\n\t * 消息方法节点\n\t */\n\tprivate SOAPBodyElement methodEle;\n\t/**\n\t * 应用于方法上的命名空间URI\n\t */\n\tprivate final String namespaceURI;\n\t/**\n\t * Soap协议\n\t * soap1.1 : text/xml\n\t * soap1.2 : application/soap+xml\n\t */\n\tprivate final SoapProtocol protocol;\n\n\t/**\n\t * 创建SOAP客户端，默认使用soap1.1版本协议\n\t *\n\t * @param url WS的URL地址\n\t * @return this\n\t */\n\tpublic static JakartaSoapClient create(String url) {\n\t\treturn new JakartaSoapClient(url);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url      WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @return this\n\t */\n\tpublic static JakartaSoapClient create(String url, SoapProtocol protocol) {\n\t\treturn new JakartaSoapClient(url, protocol);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url          WS的URL地址\n\t * @param protocol     协议，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic static JakartaSoapClient create(String url, SoapProtocol protocol, String namespaceURI) {\n\t\treturn new JakartaSoapClient(url, protocol, namespaceURI);\n\t}\n\n\t/**\n\t * 构造，默认使用soap1.1版本协议\n\t *\n\t * @param url WS的URL地址\n\t */\n\tpublic JakartaSoapClient(String url) {\n\t\tthis(url, SoapProtocol.SOAP_1_1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url      WS的URL地址\n\t * @param protocol 协议版本，见{@link SoapProtocol}\n\t */\n\tpublic JakartaSoapClient(String url, SoapProtocol protocol) {\n\t\tthis(url, protocol, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url          WS的URL地址\n\t * @param protocol     协议版本，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient(String url, SoapProtocol protocol, String namespaceURI) {\n\t\tthis.url = url;\n\t\tthis.namespaceURI = namespaceURI;\n\t\tthis.protocol = protocol;\n\t\tinit(protocol);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param protocol 协议版本枚举，见{@link SoapProtocol}\n\t * @return this\n\t */\n\tpublic JakartaSoapClient init(SoapProtocol protocol) {\n\t\t// 创建消息工厂\n\t\ttry {\n\t\t\tthis.factory = MessageFactory.newInstance(protocol.getValue());\n\t\t\t// 根据消息工厂创建SoapMessage\n\t\t\tthis.message = factory.createMessage();\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置SOAP客户端，用于客户端复用\n\t *\n\t * <p>\n\t * 重置后需调用serMethod方法重新指定请求方法，并调用setParam方法重新定义参数\n\t *\n\t * @return this\n\t * @since 4.6.7\n\t */\n\tpublic JakartaSoapClient reset() {\n\t\ttry {\n\t\t\tthis.message = factory.createMessage();\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\tthis.methodEle = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return this\n\t * @see #charset(Charset)\n\t */\n\tpublic JakartaSoapClient setCharset(Charset charset) {\n\t\treturn this.charset(charset);\n\t}\n\n\t@Override\n\tpublic JakartaSoapClient charset(Charset charset) {\n\t\tsuper.charset(charset);\n\t\ttry {\n\t\t\tthis.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset());\n\t\t\tthis.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, \"true\");\n\t\t} catch (SOAPException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Webservice请求地址\n\t *\n\t * @param url Webservice请求地址\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setUrl(String url) {\n\t\tthis.url = url;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param name           头信息标签名\n\t * @param actorURI       中间的消息接收者\n\t * @param roleUri        Role的URI\n\t * @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的\n\t * @param relay          relay属性\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.4\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) {\n\t\tfinal SOAPHeaderElement ele = addSOAPHeader(name);\n\t\ttry {\n\t\t\tif (StrUtil.isNotBlank(roleUri)) {\n\t\t\t\tele.setRole(roleUri);\n\t\t\t}\n\t\t\tif (null != relay) {\n\t\t\t\tele.setRelay(relay);\n\t\t\t}\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\tif (StrUtil.isNotBlank(actorURI)) {\n\t\t\tele.setActor(actorURI);\n\t\t}\n\t\tif (null != mustUnderstand) {\n\t\t\tele.setMustUnderstand(mustUnderstand);\n\t\t}\n\n\t\treturn ele;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param localName 头节点名称\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.7\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(String localName) {\n\t\treturn addSOAPHeader(new QName(localName));\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param localName 头节点名称\n\t * @param value     头节点的值\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.7\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(String localName, String value) {\n\t\tfinal SOAPHeaderElement soapHeaderElement = addSOAPHeader(localName);\n\t\tsoapHeaderElement.setTextContent(value);\n\t\treturn soapHeaderElement;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param name 头节点名称\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.4\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(QName name) {\n\t\tSOAPHeaderElement ele;\n\t\ttry {\n\t\t\tele = this.message.getSOAPHeader().addHeaderElement(name);\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\treturn ele;\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name            方法名及其命名空间\n\t * @param params          参数\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setMethod(Name name, Map<String, Object> params, boolean useMethodPrefix) {\n\t\treturn setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix);\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name            方法名及其命名空间\n\t * @param params          参数\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setMethod(QName name, Map<String, Object> params, boolean useMethodPrefix) {\n\t\tsetMethod(name);\n\t\tfinal String prefix = useMethodPrefix ? name.getPrefix() : null;\n\t\tfinal SOAPBodyElement methodEle = this.methodEle;\n\t\tfor (Entry<String, Object> entry : MapUtil.wrap(params)) {\n\t\t\tsetParam(methodEle, entry.getKey(), entry.getValue(), prefix);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置请求方法<br>\n\t * 方法名自动识别前缀，前缀和方法名使用“:”分隔<br>\n\t * 当识别到前缀后，自动添加xmlns属性，关联到默认的namespaceURI\n\t *\n\t * @param methodName 方法名\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setMethod(String methodName) {\n\t\treturn setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI));\n\t}\n\n\t/**\n\t * 设置请求方法<br>\n\t * 方法名自动识别前缀，前缀和方法名使用“:”分隔<br>\n\t * 当识别到前缀后，自动添加xmlns属性，关联到传入的namespaceURI\n\t *\n\t * @param methodName   方法名（可有前缀也可无）\n\t * @param namespaceURI 命名空间URI\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setMethod(String methodName, String namespaceURI) {\n\t\tfinal List<String> methodNameList = StrUtil.split(methodName, ':');\n\t\tfinal QName qName;\n\t\tif (2 == methodNameList.size()) {\n\t\t\tqName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0));\n\t\t} else {\n\t\t\tqName = new QName(namespaceURI, methodName);\n\t\t}\n\t\treturn setMethod(qName);\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name 方法名及其命名空间\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setMethod(QName name) {\n\t\ttry {\n\t\t\tthis.methodEle = this.message.getSOAPBody().addBodyElement(name);\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置方法参数，使用方法的前缀\n\t *\n\t * @param name  参数名\n\t * @param value 参数值，可以是字符串或Map或{@link SOAPElement}\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setParam(String name, Object value) {\n\t\treturn setParam(name, value, true);\n\t}\n\n\t/**\n\t * 设置方法参数\n\t *\n\t * @param name            参数名\n\t * @param value           参数值，可以是字符串或Map或{@link SOAPElement}\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic JakartaSoapClient setParam(String name, Object value, boolean useMethodPrefix) {\n\t\tsetParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 批量设置参数，使用方法的前缀\n\t *\n\t * @param params 参数列表\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient setParams(Map<String, Object> params) {\n\t\treturn setParams(params, true);\n\t}\n\n\t/**\n\t * 批量设置参数\n\t *\n\t * @param params          参数列表\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient setParams(Map<String, Object> params, boolean useMethodPrefix) {\n\t\tfor (Entry<String, Object> entry : MapUtil.wrap(params)) {\n\t\t\tsetParam(entry.getKey(), entry.getValue(), useMethodPrefix);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取方法节点<br>\n\t * 用于创建子节点等操作\n\t *\n\t * @return {@link SOAPBodyElement}\n\t * @since 4.5.6\n\t */\n\tpublic SOAPBodyElement getMethodEle() {\n\t\treturn this.methodEle;\n\t}\n\n\t/**\n\t * 获取SOAP消息对象 {@link SOAPMessage}\n\t *\n\t * @return {@link SOAPMessage}\n\t * @since 4.5.6\n\t */\n\tpublic SOAPMessage getMessage() {\n\t\treturn this.message;\n\t}\n\n\t/**\n\t * 获取SOAP请求消息\n\t *\n\t * @param pretty 是否格式化\n\t * @return 消息字符串\n\t */\n\tpublic String getMsgStr(boolean pretty) {\n\t\treturn JakartaSoapUtil.toString(this.message, pretty, this.charset);\n\t}\n\n\t/**\n\t * 将SOAP消息的XML内容输出到流\n\t *\n\t * @param out 输出流\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient write(OutputStream out) {\n\t\ttry {\n\t\t\tthis.message.writeTo(out);\n\t\t} catch (SOAPException | IOException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置超时，单位：毫秒<br>\n\t * 超时包括：\n\t *\n\t * <pre>\n\t * 1. 连接超时\n\t * 2. 读取响应超时\n\t * </pre>\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @see #setConnectionTimeout(int)\n\t * @see #setReadTimeout(int)\n\t */\n\tpublic JakartaSoapClient timeout(int milliseconds) {\n\t\tsetConnectionTimeout(milliseconds);\n\t\tsetReadTimeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient setConnectionTimeout(int milliseconds) {\n\t\tthis.connectionTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic JakartaSoapClient setReadTimeout(int milliseconds) {\n\t\tthis.readTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @return 返回结果\n\t */\n\tpublic SOAPMessage sendForMessage() {\n\t\tfinal HttpResponse res = sendForResponse();\n\t\tfinal MimeHeaders headers = new MimeHeaders();\n\t\tfor (Entry<String, List<String>> entry : res.headers().entrySet()) {\n\t\t\tif (StrUtil.isNotEmpty(entry.getKey())) {\n\t\t\t\theaders.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0));\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\treturn this.factory.createMessage(headers, res.bodyStream());\n\t\t} catch (IOException | SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(res);\n\t\t}\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @return 返回结果\n\t */\n\tpublic String send() {\n\t\treturn send(false);\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @param pretty 是否格式化\n\t * @return 返回结果\n\t */\n\tpublic String send(boolean pretty) {\n\t\tfinal String body = sendForResponse().body();\n\t\treturn pretty ? XmlUtil.format(body) : body;\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 发送请求，获取异步响应\n\t *\n\t * @return 响应对象\n\t */\n\tpublic HttpResponse sendForResponse() {\n\t\treturn HttpRequest.post(this.url)//\n\t\t\t\t.setFollowRedirects(true)//\n\t\t\t\t.setConnectionTimeout(this.connectionTimeout)\n\t\t\t\t.setReadTimeout(this.readTimeout)\n\t\t\t\t.contentType(getXmlContentType())//\n\t\t\t\t.header(this.headers())\n\t\t\t\t.body(getMsgStr(false))//\n\t\t\t\t.executeAsync();\n\t}\n\n\t/**\n\t * 获取请求的Content-Type，附加编码信息\n\t *\n\t * @return 请求的Content-Type\n\t */\n\tprivate String getXmlContentType() {\n\t\tswitch (this.protocol){\n\t\t\tcase SOAP_1_1:\n\t\t\t\treturn CONTENT_TYPE_SOAP11_TEXT_XML.concat(this.charset.toString());\n\t\t\tcase SOAP_1_2:\n\t\t\t\treturn CONTENT_TYPE_SOAP12_SOAP_XML.concat(this.charset.toString());\n\t\t\tdefault:\n\t\t\t\tthrow new SoapRuntimeException(\"Unsupported protocol: {}\", this.protocol);\n\t\t}\n\t}\n\n\t/**\n\t * 设置方法参数\n\t *\n\t * @param ele    方法节点\n\t * @param name   参数名\n\t * @param value  参数值\n\t * @param prefix 命名空间前缀， {@code null}表示不使用前缀\n\t * @return {@link SOAPElement}子节点\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) {\n\t\tfinal SOAPElement childEle;\n\t\ttry {\n\t\t\tif (StrUtil.isNotBlank(prefix)) {\n\t\t\t\tchildEle = ele.addChildElement(name, prefix);\n\t\t\t} else {\n\t\t\t\tchildEle = ele.addChildElement(name);\n\t\t\t}\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\tif (null != value) {\n\t\t\tif (value instanceof SOAPElement) {\n\t\t\t\t// 单个子节点\n\t\t\t\ttry {\n\t\t\t\t\tele.addChildElement((SOAPElement) value);\n\t\t\t\t} catch (SOAPException e) {\n\t\t\t\t\tthrow new SoapRuntimeException(e);\n\t\t\t\t}\n\t\t\t} else if (value instanceof Map) {\n\t\t\t\t// 多个字节点\n\t\t\t\tEntry entry;\n\t\t\t\tfor (Object obj : ((Map) value).entrySet()) {\n\t\t\t\t\tentry = (Entry) obj;\n\t\t\t\t\tsetParam(childEle, entry.getKey().toString(), entry.getValue(), prefix);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 单个值\n\t\t\t\tchildEle.setValue(value.toString());\n\t\t\t}\n\t\t}\n\n\t\treturn childEle;\n\t}\n\t// -------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapProtocol.java",
    "content": "package cn.hutool.http.webservice;\n\nimport jakarta.xml.soap.SOAPConstants;\n\n/**\n * SOAP协议版本枚举\n *\n * @author looly\n *\n */\npublic enum JakartaSoapProtocol {\n\t/** SOAP 1.1协议 */\n\tSOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL),\n\t/** SOAP 1.2协议 */\n\tSOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL);\n\n\t/**\n\t * 构造\n\t *\n\t * @param value {@link SOAPConstants} 中的协议版本值\n\t */\n\tJakartaSoapProtocol(String value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final String value;\n\n\t/**\n\t * 获取版本值信息\n\t *\n\t * @return 版本值信息\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapUtil.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.XmlUtil;\n\nimport jakarta.xml.soap.SOAPException;\nimport jakarta.xml.soap.SOAPMessage;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.Charset;\n\n/**\n * SOAP相关工具类\n *\n * @author looly\n * @since 4.5.7\n */\npublic class JakartaSoapUtil {\n\n\t/**\n\t * 创建SOAP客户端，默认使用soap1.1版本协议\n\t *\n\t * @param url WS的URL地址\n\t * @return {@link SoapClient}\n\t */\n\tpublic static SoapClient createClient(String url) {\n\t\treturn SoapClient.create(url);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @return {@link SoapClient}\n\t */\n\tpublic static SoapClient createClient(String url, SoapProtocol protocol) {\n\t\treturn SoapClient.create(url, protocol);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @return {@link SoapClient}\n\t * @since 4.5.6\n\t */\n\tpublic static SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) {\n\t\treturn SoapClient.create(url, protocol, namespaceURI);\n\t}\n\n\t/**\n\t * {@link SOAPMessage} 转为字符串\n\t *\n\t * @param message SOAP消息对象\n\t * @param pretty 是否格式化\n\t * @return SOAP XML字符串\n\t */\n\tpublic static String toString(SOAPMessage message, boolean pretty) {\n\t\treturn toString(message, pretty, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * {@link SOAPMessage} 转为字符串\n\t *\n\t * @param message SOAP消息对象\n\t * @param pretty 是否格式化\n\t * @param charset 编码\n\t * @return SOAP XML字符串\n\t * @since 4.5.7\n\t */\n\tpublic static String toString(SOAPMessage message, boolean pretty, Charset charset) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\ttry {\n\t\t\tmessage.writeTo(out);\n\t\t} catch (SOAPException | IOException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\tString messageToString;\n\t\ttry {\n\t\t\tmessageToString = out.toString(charset.toString());\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\treturn pretty ? XmlUtil.format(messageToString) : messageToString;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.XmlUtil;\nimport cn.hutool.http.HttpBase;\nimport cn.hutool.http.HttpGlobalConfig;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\n\nimport javax.xml.XMLConstants;\nimport javax.xml.namespace.QName;\nimport javax.xml.soap.MessageFactory;\nimport javax.xml.soap.MimeHeaders;\nimport javax.xml.soap.Name;\nimport javax.xml.soap.SOAPBodyElement;\nimport javax.xml.soap.SOAPElement;\nimport javax.xml.soap.SOAPException;\nimport javax.xml.soap.SOAPHeaderElement;\nimport javax.xml.soap.SOAPMessage;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * SOAP客户端\n *\n * <p>\n * 此对象用于构建一个SOAP消息，并通过HTTP接口发出消息内容。\n * SOAP消息本质上是一个XML文本，可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体\n * <p>\n * 使用方法：\n *\n * <pre>\n * SoapClient client = SoapClient.create(url)\n * .setMethod(methodName, namespaceURI)\n * .setCharset(CharsetUtil.CHARSET_GBK)\n * .setParam(param1, \"XXX\");\n *\n * String response = client.send(true);\n *\n * </pre>\n *\n * @author looly\n * @since 4.5.4\n */\npublic class SoapClient extends HttpBase<SoapClient> {\n\n\t/**\n\t * XML消息体的Content-Type\n\t * soap1.1 : text/xml\n\t * soap1.2 : application/soap+xml\n\t * soap1.1与soap1.2区别:  https://www.cnblogs.com/qlqwjy/p/7577147.html\n\t */\n\tprivate static final String CONTENT_TYPE_SOAP11_TEXT_XML = \"text/xml;charset=\";\n\tprivate static final String CONTENT_TYPE_SOAP12_SOAP_XML = \"application/soap+xml;charset=\";\n\n\t/**\n\t * 请求的URL地址\n\t */\n\tprivate String url;\n\n\t/**\n\t * 默认连接超时\n\t */\n\tprivate int connectionTimeout = HttpGlobalConfig.getTimeout();\n\t/**\n\t * 默认读取超时\n\t */\n\tprivate int readTimeout = HttpGlobalConfig.getTimeout();\n\n\t/**\n\t * 消息工厂，用于创建消息\n\t */\n\tprivate MessageFactory factory;\n\t/**\n\t * SOAP消息\n\t */\n\tprivate SOAPMessage message;\n\t/**\n\t * 消息方法节点\n\t */\n\tprivate SOAPBodyElement methodEle;\n\t/**\n\t * 应用于方法上的命名空间URI\n\t */\n\tprivate final String namespaceURI;\n\t/**\n\t * Soap协议\n\t * soap1.1 : text/xml\n\t * soap1.2 : application/soap+xml\n\t */\n\tprivate final SoapProtocol protocol;\n\n\t/**\n\t * 创建SOAP客户端，默认使用soap1.1版本协议\n\t *\n\t * @param url WS的URL地址\n\t * @return this\n\t */\n\tpublic static SoapClient create(String url) {\n\t\treturn new SoapClient(url);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url      WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @return this\n\t */\n\tpublic static SoapClient create(String url, SoapProtocol protocol) {\n\t\treturn new SoapClient(url, protocol);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t *\n\t * @param url          WS的URL地址\n\t * @param protocol     协议，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic static SoapClient create(String url, SoapProtocol protocol, String namespaceURI) {\n\t\treturn new SoapClient(url, protocol, namespaceURI);\n\t}\n\n\t/**\n\t * 构造，默认使用soap1.1版本协议\n\t *\n\t * @param url WS的URL地址\n\t */\n\tpublic SoapClient(String url) {\n\t\tthis(url, SoapProtocol.SOAP_1_1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url      WS的URL地址\n\t * @param protocol 协议版本，见{@link SoapProtocol}\n\t */\n\tpublic SoapClient(String url, SoapProtocol protocol) {\n\t\tthis(url, protocol, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url          WS的URL地址\n\t * @param protocol     协议版本，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient(String url, SoapProtocol protocol, String namespaceURI) {\n\t\tthis.url = url;\n\t\tthis.namespaceURI = namespaceURI;\n\t\tthis.protocol = protocol;\n\t\tinit(protocol);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param protocol 协议版本枚举，见{@link SoapProtocol}\n\t * @return this\n\t */\n\tpublic SoapClient init(SoapProtocol protocol) {\n\t\t// 创建消息工厂\n\t\ttry {\n\t\t\tthis.factory = MessageFactory.newInstance(protocol.getValue());\n\t\t\t// 根据消息工厂创建SoapMessage\n\t\t\tthis.message = factory.createMessage();\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置SOAP客户端，用于客户端复用\n\t *\n\t * <p>\n\t * 重置后需调用serMethod方法重新指定请求方法，并调用setParam方法重新定义参数\n\t *\n\t * @return this\n\t * @since 4.6.7\n\t */\n\tpublic SoapClient reset() {\n\t\ttry {\n\t\t\tthis.message = factory.createMessage();\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\tthis.methodEle = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return this\n\t * @see #charset(Charset)\n\t */\n\tpublic SoapClient setCharset(Charset charset) {\n\t\treturn this.charset(charset);\n\t}\n\n\t@Override\n\tpublic SoapClient charset(Charset charset) {\n\t\tsuper.charset(charset);\n\t\ttry {\n\t\t\tthis.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset());\n\t\t\tthis.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, \"true\");\n\t\t} catch (SOAPException e) {\n\t\t\t// ignore\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Webservice请求地址\n\t *\n\t * @param url Webservice请求地址\n\t * @return this\n\t */\n\tpublic SoapClient setUrl(String url) {\n\t\tthis.url = url;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param name           头信息标签名\n\t * @param actorURI       中间的消息接收者\n\t * @param roleUri        Role的URI\n\t * @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的\n\t * @param relay          relay属性\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.4\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) {\n\t\tfinal SOAPHeaderElement ele = addSOAPHeader(name);\n\t\ttry {\n\t\t\tif (StrUtil.isNotBlank(roleUri)) {\n\t\t\t\tele.setRole(roleUri);\n\t\t\t}\n\t\t\tif (null != relay) {\n\t\t\t\tele.setRelay(relay);\n\t\t\t}\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\tif (StrUtil.isNotBlank(actorURI)) {\n\t\t\tele.setActor(actorURI);\n\t\t}\n\t\tif (null != mustUnderstand) {\n\t\t\tele.setMustUnderstand(mustUnderstand);\n\t\t}\n\n\t\treturn ele;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param localName 头节点名称\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.7\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(String localName) {\n\t\treturn addSOAPHeader(new QName(localName));\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param localName 头节点名称\n\t * @param value     头节点的值\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.7\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(String localName, String value) {\n\t\tfinal SOAPHeaderElement soapHeaderElement = addSOAPHeader(localName);\n\t\tsoapHeaderElement.setTextContent(value);\n\t\treturn soapHeaderElement;\n\t}\n\n\t/**\n\t * 增加SOAP头信息，方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点\n\t *\n\t * @param name 头节点名称\n\t * @return {@link SOAPHeaderElement}\n\t * @since 5.4.4\n\t */\n\tpublic SOAPHeaderElement addSOAPHeader(QName name) {\n\t\tSOAPHeaderElement ele;\n\t\ttry {\n\t\t\tele = this.message.getSOAPHeader().addHeaderElement(name);\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\treturn ele;\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name            方法名及其命名空间\n\t * @param params          参数\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic SoapClient setMethod(Name name, Map<String, Object> params, boolean useMethodPrefix) {\n\t\treturn setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix);\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name            方法名及其命名空间\n\t * @param params          参数\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic SoapClient setMethod(QName name, Map<String, Object> params, boolean useMethodPrefix) {\n\t\tsetMethod(name);\n\t\tfinal String prefix = useMethodPrefix ? name.getPrefix() : null;\n\t\tfinal SOAPBodyElement methodEle = this.methodEle;\n\t\tfor (Entry<String, Object> entry : MapUtil.wrap(params)) {\n\t\t\tsetParam(methodEle, entry.getKey(), entry.getValue(), prefix);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置请求方法<br>\n\t * 方法名自动识别前缀，前缀和方法名使用“:”分隔<br>\n\t * 当识别到前缀后，自动添加xmlns属性，关联到默认的namespaceURI\n\t *\n\t * @param methodName 方法名\n\t * @return this\n\t */\n\tpublic SoapClient setMethod(String methodName) {\n\t\treturn setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI));\n\t}\n\n\t/**\n\t * 设置请求方法<br>\n\t * 方法名自动识别前缀，前缀和方法名使用“:”分隔<br>\n\t * 当识别到前缀后，自动添加xmlns属性，关联到传入的namespaceURI\n\t *\n\t * @param methodName   方法名（可有前缀也可无）\n\t * @param namespaceURI 命名空间URI\n\t * @return this\n\t */\n\tpublic SoapClient setMethod(String methodName, String namespaceURI) {\n\t\tfinal List<String> methodNameList = StrUtil.split(methodName, ':');\n\t\tfinal QName qName;\n\t\tif (2 == methodNameList.size()) {\n\t\t\tqName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0));\n\t\t} else {\n\t\t\tqName = new QName(namespaceURI, methodName);\n\t\t}\n\t\treturn setMethod(qName);\n\t}\n\n\t/**\n\t * 设置请求方法\n\t *\n\t * @param name 方法名及其命名空间\n\t * @return this\n\t */\n\tpublic SoapClient setMethod(QName name) {\n\t\ttry {\n\t\t\tthis.methodEle = this.message.getSOAPBody().addBodyElement(name);\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置方法参数，使用方法的前缀\n\t *\n\t * @param name  参数名\n\t * @param value 参数值，可以是字符串或Map或{@link SOAPElement}\n\t * @return this\n\t */\n\tpublic SoapClient setParam(String name, Object value) {\n\t\treturn setParam(name, value, true);\n\t}\n\n\t/**\n\t * 设置方法参数\n\t *\n\t * @param name            参数名\n\t * @param value           参数值，可以是字符串或Map或{@link SOAPElement}\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t */\n\tpublic SoapClient setParam(String name, Object value, boolean useMethodPrefix) {\n\t\tsetParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 批量设置参数，使用方法的前缀\n\t *\n\t * @param params 参数列表\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient setParams(Map<String, Object> params) {\n\t\treturn setParams(params, true);\n\t}\n\n\t/**\n\t * 批量设置参数\n\t *\n\t * @param params          参数列表\n\t * @param useMethodPrefix 是否使用方法的命名空间前缀\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient setParams(Map<String, Object> params, boolean useMethodPrefix) {\n\t\tfor (Entry<String, Object> entry : MapUtil.wrap(params)) {\n\t\t\tsetParam(entry.getKey(), entry.getValue(), useMethodPrefix);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取方法节点<br>\n\t * 用于创建子节点等操作\n\t *\n\t * @return {@link SOAPBodyElement}\n\t * @since 4.5.6\n\t */\n\tpublic SOAPBodyElement getMethodEle() {\n\t\treturn this.methodEle;\n\t}\n\n\t/**\n\t * 获取SOAP消息对象 {@link SOAPMessage}\n\t *\n\t * @return {@link SOAPMessage}\n\t * @since 4.5.6\n\t */\n\tpublic SOAPMessage getMessage() {\n\t\treturn this.message;\n\t}\n\n\t/**\n\t * 获取SOAP请求消息\n\t *\n\t * @param pretty 是否格式化\n\t * @return 消息字符串\n\t */\n\tpublic String getMsgStr(boolean pretty) {\n\t\treturn SoapUtil.toString(this.message, pretty, this.charset);\n\t}\n\n\t/**\n\t * 将SOAP消息的XML内容输出到流\n\t *\n\t * @param out 输出流\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient write(OutputStream out) {\n\t\ttry {\n\t\t\tthis.message.writeTo(out);\n\t\t} catch (SOAPException | IOException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置超时，单位：毫秒<br>\n\t * 超时包括：\n\t *\n\t * <pre>\n\t * 1. 连接超时\n\t * 2. 读取响应超时\n\t * </pre>\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @see #setConnectionTimeout(int)\n\t * @see #setReadTimeout(int)\n\t */\n\tpublic SoapClient timeout(int milliseconds) {\n\t\tsetConnectionTimeout(milliseconds);\n\t\tsetReadTimeout(milliseconds);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient setConnectionTimeout(int milliseconds) {\n\t\tthis.connectionTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置连接超时，单位：毫秒\n\t *\n\t * @param milliseconds 超时毫秒数\n\t * @return this\n\t * @since 4.5.6\n\t */\n\tpublic SoapClient setReadTimeout(int milliseconds) {\n\t\tthis.readTimeout = milliseconds;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @return 返回结果\n\t */\n\tpublic SOAPMessage sendForMessage() {\n\t\tfinal HttpResponse res = sendForResponse();\n\t\tfinal MimeHeaders headers = new MimeHeaders();\n\t\tfor (Entry<String, List<String>> entry : res.headers().entrySet()) {\n\t\t\tif (StrUtil.isNotEmpty(entry.getKey())) {\n\t\t\t\theaders.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0));\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\treturn this.factory.createMessage(headers, res.bodyStream());\n\t\t} catch (IOException | SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(res);\n\t\t}\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @return 返回结果\n\t */\n\tpublic String send() {\n\t\treturn send(false);\n\t}\n\n\t/**\n\t * 执行Webservice请求，即发送SOAP内容\n\t *\n\t * @param pretty 是否格式化\n\t * @return 返回结果\n\t */\n\tpublic String send(boolean pretty) {\n\t\tfinal String body = sendForResponse().body();\n\t\treturn pretty ? XmlUtil.format(body) : body;\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 发送请求，获取异步响应\n\t *\n\t * @return 响应对象\n\t */\n\tpublic HttpResponse sendForResponse() {\n\t\treturn HttpRequest.post(this.url)//\n\t\t\t\t.setFollowRedirects(true)//\n\t\t\t\t.setConnectionTimeout(this.connectionTimeout)\n\t\t\t\t.setReadTimeout(this.readTimeout)\n\t\t\t\t.contentType(getXmlContentType())//\n\t\t\t\t.header(this.headers())\n\t\t\t\t.body(getMsgStr(false))//\n\t\t\t\t.executeAsync();\n\t}\n\n\t/**\n\t * 获取请求的Content-Type，附加编码信息\n\t *\n\t * @return 请求的Content-Type\n\t */\n\tprivate String getXmlContentType() {\n\t\tswitch (this.protocol){\n\t\t\tcase SOAP_1_1:\n\t\t\t\treturn CONTENT_TYPE_SOAP11_TEXT_XML.concat(this.charset.toString());\n\t\t\tcase SOAP_1_2:\n\t\t\t\treturn CONTENT_TYPE_SOAP12_SOAP_XML.concat(this.charset.toString());\n\t\t\tdefault:\n\t\t\t\tthrow new SoapRuntimeException(\"Unsupported protocol: {}\", this.protocol);\n\t\t}\n\t}\n\n\t/**\n\t * 设置方法参数\n\t *\n\t * @param ele    方法节点\n\t * @param name   参数名\n\t * @param value  参数值\n\t * @param prefix 命名空间前缀， {@code null}表示不使用前缀\n\t * @return {@link SOAPElement}子节点\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tprivate static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) {\n\t\tfinal SOAPElement childEle;\n\t\ttry {\n\t\t\tif (StrUtil.isNotBlank(prefix)) {\n\t\t\t\tchildEle = ele.addChildElement(name, prefix);\n\t\t\t} else {\n\t\t\t\tchildEle = ele.addChildElement(name);\n\t\t\t}\n\t\t} catch (SOAPException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\n\t\tif (null != value) {\n\t\t\tif (value instanceof SOAPElement) {\n\t\t\t\t// 单个子节点\n\t\t\t\ttry {\n\t\t\t\t\tele.addChildElement((SOAPElement) value);\n\t\t\t\t} catch (SOAPException e) {\n\t\t\t\t\tthrow new SoapRuntimeException(e);\n\t\t\t\t}\n\t\t\t} else if (value instanceof Map) {\n\t\t\t\t// 多个字节点\n\t\t\t\tEntry entry;\n\t\t\t\tfor (Object obj : ((Map) value).entrySet()) {\n\t\t\t\t\tentry = (Entry) obj;\n\t\t\t\t\tsetParam(childEle, entry.getKey().toString(), entry.getValue(), prefix);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 单个值\n\t\t\t\tchildEle.setValue(value.toString());\n\t\t\t}\n\t\t}\n\n\t\treturn childEle;\n\t}\n\t// -------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/SoapProtocol.java",
    "content": "package cn.hutool.http.webservice;\n\nimport javax.xml.soap.SOAPConstants;\n\n/**\n * SOAP协议版本枚举\n * \n * @author looly\n *\n */\npublic enum SoapProtocol {\n\t/** SOAP 1.1协议 */\n\tSOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL),\n\t/** SOAP 1.2协议 */\n\tSOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL);\n\n\t/**\n\t * 构造\n\t * \n\t * @param value {@link SOAPConstants} 中的协议版本值\n\t */\n\tSoapProtocol(String value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final String value;\n\n\t/**\n\t * 获取版本值信息\n\t * \n\t * @return 版本值信息\n\t */\n\tpublic String getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/SoapRuntimeException.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * SOAP异常\n * \n * @author xiaoleilu\n */\npublic class SoapRuntimeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic SoapRuntimeException(Throwable e) {\n\t\tsuper(e.getMessage(), e);\n\t}\n\n\tpublic SoapRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic SoapRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic SoapRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic SoapRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/SoapUtil.java",
    "content": "package cn.hutool.http.webservice;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.Charset;\n\nimport javax.xml.soap.SOAPException;\nimport javax.xml.soap.SOAPMessage;\n\nimport cn.hutool.core.exceptions.UtilException;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.XmlUtil;\n\n/**\n * SOAP相关工具类\n * \n * @author looly\n * @since 4.5.7\n */\npublic class SoapUtil {\n\n\t/**\n\t * 创建SOAP客户端，默认使用soap1.1版本协议\n\t * \n\t * @param url WS的URL地址\n\t * @return {@link SoapClient}\n\t */\n\tpublic static SoapClient createClient(String url) {\n\t\treturn SoapClient.create(url);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t * \n\t * @param url WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @return {@link SoapClient}\n\t */\n\tpublic static SoapClient createClient(String url, SoapProtocol protocol) {\n\t\treturn SoapClient.create(url, protocol);\n\t}\n\n\t/**\n\t * 创建SOAP客户端\n\t * \n\t * @param url WS的URL地址\n\t * @param protocol 协议，见{@link SoapProtocol}\n\t * @param namespaceURI 方法上的命名空间URI\n\t * @return {@link SoapClient}\n\t * @since 4.5.6\n\t */\n\tpublic static SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) {\n\t\treturn SoapClient.create(url, protocol, namespaceURI);\n\t}\n\n\t/**\n\t * {@link SOAPMessage} 转为字符串\n\t *\n\t * @param message SOAP消息对象\n\t * @param pretty 是否格式化\n\t * @return SOAP XML字符串\n\t */\n\tpublic static String toString(SOAPMessage message, boolean pretty) {\n\t\treturn toString(message, pretty, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/**\n\t * {@link SOAPMessage} 转为字符串\n\t *\n\t * @param message SOAP消息对象\n\t * @param pretty 是否格式化\n\t * @param charset 编码\n\t * @return SOAP XML字符串\n\t * @since 4.5.7\n\t */\n\tpublic static String toString(SOAPMessage message, boolean pretty, Charset charset) {\n\t\tfinal ByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\ttry {\n\t\t\tmessage.writeTo(out);\n\t\t} catch (SOAPException | IOException e) {\n\t\t\tthrow new SoapRuntimeException(e);\n\t\t}\n\t\tString messageToString;\n\t\ttry {\n\t\t\tmessageToString = out.toString(charset.toString());\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new UtilException(e);\n\t\t}\n\t\treturn pretty ? XmlUtil.format(messageToString) : messageToString;\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/main/java/cn/hutool/http/webservice/package-info.java",
    "content": "/**\n * Webservice客户端封装实现\n * \n * @author looly\n *\n */\npackage cn.hutool.http.webservice;"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/ContentTypeTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * ContentType 单元测试\n *\n *\n */\npublic class ContentTypeTest {\n\n\t@Test\n\tpublic void testBuild() {\n\t\tString result = ContentType.build(ContentType.JSON, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"application/json;charset=UTF-8\", result);\n\t}\n\n\t@Test\n\tpublic void testGetWithLeadingSpace() {\n\t\tString json = \" {\\n\" +\n\t\t\t\"     \\\"name\\\": \\\"hutool\\\"\\n\" +\n\t\t\t\" }\";\n\t\tContentType contentType = ContentType.get(json);\n\t\tassertEquals(ContentType.JSON, contentType);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/DownloadTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.StreamProgress;\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\n/**\n * 下载单元测试\n *\n * @author looly\n */\npublic class DownloadTest {\n\n\t@Test\n\t@Disabled\n\tpublic void downloadPicTest() {\n\t\tfinal String url = \"http://wx.qlogo.cn/mmopen/vKhlFcibVUtNBVDjcIowlg0X8aJfHXrTNCEFBukWVH9ta99pfEN88lU39MKspCUCOP3yrFBH3y2NbV7sYtIIlon8XxLwAEqv2/0\";\n\t\tHttpUtil.downloadFile(url, \"e:/pic/t3.jpg\");\n\t\tConsole.log(\"ok\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadSizeTest() {\n\t\tfinal String url = \"https://res.t-io.org/im/upload/img/67/8948/1119501/88097554/74541310922/85/231910/366466 - 副本.jpg\";\n\t\tHttpRequest.get(url).setSSLProtocol(\"TLSv1.2\").executeAsync().writeBody(\"e:/pic/366466.jpg\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTest1() {\n\t\tfinal long size = HttpUtil.downloadFile(\"http://explorer.bbfriend.com/crossdomain.xml\", \"e:/temp/\");\n\t\tSystem.out.println(\"Download size: \" + size);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTest() {\n\t\t// 带进度显示的文件下载\n\t\tHttpUtil.downloadFile(\"http://mirrors.sohu.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso\", FileUtil.file(\"d:/\"), new StreamProgress() {\n\n\t\t\tfinal long time = System.currentTimeMillis();\n\n\t\t\t@Override\n\t\t\tpublic void start() {\n\t\t\t\tConsole.log(\"开始下载。。。。\");\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void progress(final long contentLength, final long progressSize) {\n\t\t\t\tfinal long speed = progressSize / (System.currentTimeMillis() - time) * 1000;\n\t\t\t\tConsole.log(\"总大小:{}, 已下载：{}, 速度：{}/s\", FileUtil.readableFileSize(contentLength), FileUtil.readableFileSize(progressSize), FileUtil.readableFileSize(speed));\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void finish() {\n\t\t\t\tConsole.log(\"下载完成！\");\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadFileFromUrlTest1() {\n\t\tfinal File file = HttpUtil.downloadFileFromUrl(\"http://groovy-lang.org/changelogs/changelog-3.0.5.html\", \"d:/download/temp\");\n\t\tassertNotNull(file);\n\t\tassertTrue(file.isFile());\n\t\tassertTrue(file.length() > 0);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadFileFromUrlTest2() {\n\t\tFile file = null;\n\t\ttry {\n\t\t\tfile = HttpUtil.downloadFileFromUrl(\"https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar\", FileUtil.file(\"d:/download/temp\"), 1, new StreamProgress() {\n\t\t\t\t@Override\n\t\t\t\tpublic void start() {\n\t\t\t\t\tSystem.out.println(\"start\");\n\t\t\t\t}\n\n\t\t\t\t@Override\n\t\t\t\tpublic void progress(final long contentLength, final long progressSize) {\n\t\t\t\t\tSystem.out.println(\"download size:\" + progressSize);\n\t\t\t\t}\n\n\t\t\t\t@Override\n\t\t\t\tpublic void finish() {\n\t\t\t\t\tSystem.out.println(\"end\");\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tassertNotNull(file);\n\t\t\tassertTrue(file.exists());\n\t\t\tassertTrue(file.isFile());\n\t\t\tassertTrue(file.length() > 0);\n\t\t\tassertTrue(file.getName().length() > 0);\n\t\t} catch (final Exception e) {\n\t\t\tassertTrue(e instanceof IORuntimeException);\n\t\t} finally {\n\t\t\tFileUtil.del(file);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadFileFromUrlTest3() {\n\t\tFile file = null;\n\t\ttry {\n\t\t\tfile = HttpUtil.downloadFileFromUrl(\"https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar\", FileUtil.file(\"d:/download/temp\"), new StreamProgress() {\n\t\t\t\t@Override\n\t\t\t\tpublic void start() {\n\t\t\t\t\tSystem.out.println(\"start\");\n\t\t\t\t}\n\n\t\t\t\t@Override\n\t\t\t\tpublic void progress(final long contentLength, final long progressSize) {\n\t\t\t\t\tSystem.out.println(\"contentLength:\" + contentLength + \"download size:\" + progressSize);\n\t\t\t\t}\n\n\t\t\t\t@Override\n\t\t\t\tpublic void finish() {\n\t\t\t\t\tSystem.out.println(\"end\");\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tassertNotNull(file);\n\t\t\tassertTrue(file.exists());\n\t\t\tassertTrue(file.isFile());\n\t\t\tassertTrue(file.length() > 0);\n\t\t\tassertTrue(file.getName().length() > 0);\n\t\t} finally {\n\t\t\tFileUtil.del(file);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadFileFromUrlTest4() {\n\t\tFile file = null;\n\t\ttry {\n\t\t\tfile = HttpUtil.downloadFileFromUrl(\"http://groovy-lang.org/changelogs/changelog-3.0.5.html\", FileUtil.file(\"d:/download/temp\"), 1);\n\n\t\t\tassertNotNull(file);\n\t\t\tassertTrue(file.exists());\n\t\t\tassertTrue(file.isFile());\n\t\t\tassertTrue(file.length() > 0);\n\t\t\tassertTrue(file.getName().length() > 0);\n\t\t} catch (final Exception e) {\n\t\t\tassertTrue(e instanceof IORuntimeException);\n\t\t} finally {\n\t\t\tFileUtil.del(file);\n\t\t}\n\t}\n\n\n\t@Test\n\t@Disabled\n\tpublic void downloadFileFromUrlTest5() {\n\t\tFile file = null;\n\t\ttry {\n\t\t\tfile = HttpUtil.downloadFileFromUrl(\"http://groovy-lang.org/changelogs/changelog-3.0.5.html\", FileUtil.file(\"d:/download/temp\", UUID.randomUUID().toString()));\n\n\t\t\tassertNotNull(file);\n\t\t\tassertTrue(file.exists());\n\t\t\tassertTrue(file.isFile());\n\t\t\tassertTrue(file.length() > 0);\n\t\t} finally {\n\t\t\tFileUtil.del(file);\n\t\t}\n\n\t\tFile file1 = null;\n\t\ttry {\n\t\t\tfile1 = HttpUtil.downloadFileFromUrl(\"http://groovy-lang.org/changelogs/changelog-3.0.5.html\", FileUtil.file(\"d:/download/temp\"));\n\n\t\t\tassertNotNull(file1);\n\t\t\tassertTrue(file1.exists());\n\t\t\tassertTrue(file1.isFile());\n\t\t\tassertTrue(file1.length() > 0);\n\t\t} finally {\n\t\t\tFileUtil.del(file1);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadTeamViewerTest() throws IOException {\n\t\t// 此URL有3次重定向, 需要请求4次\n\t\tfinal String url = \"https://download.teamviewer.com/download/TeamViewer_Setup_x64.exe\";\n\t\tHttpGlobalConfig.setMaxRedirectCount(20);\n\t\tfinal Path temp = Files.createTempFile(\"tmp\", \".exe\");\n\t\tfinal File file = HttpUtil.downloadFileFromUrl(url, temp.toFile());\n\t\tConsole.log(file.length());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HTMLFilterTest.java",
    "content": "package cn.hutool.http;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class HTMLFilterTest {\n\t@Test\n\tpublic void issue3433Test() {\n\t\tString p1 = \"<p>a</p>\";\n\t\tString p2 = \"<p onclick=\\\"bbbb\\\">a</p>\";\n\n\t\tfinal HTMLFilter htmlFilter = new HTMLFilter();\n\t\tString filter = htmlFilter.filter(p1);\n\t\tassertEquals(\"<p>a</p>\", filter);\n\n\t\tfilter = htmlFilter.filter(p2);\n\t\tassertEquals(\"<p>a</p>\", filter);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HtmlUtilTest.java",
    "content": "package cn.hutool.http;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Html单元测试\n *\n * @author looly\n *\n */\npublic class HtmlUtilTest {\n\n\t@Test\n\tpublic void removeHtmlTagTest() {\n\t\t//非闭合标签\n\t\tString str = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\">\";\n\t\tString result = HtmlUtil.removeHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img>\";\n\t\tresult = HtmlUtil.removeHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\" />\";\n\t\tresult = HtmlUtil.removeHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img />\";\n\t\tresult = HtmlUtil.removeHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//包含内容标签\n\t\tstr = \"pre<div class=\\\"test_div\\\">dfdsfdsfdsf</div>\";\n\t\tresult = HtmlUtil.removeHtmlTag(str, \"div\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//带换行\n\t\tstr = \"pre<div class=\\\"test_div\\\">\\r\\n\\t\\tdfdsfdsfdsf\\r\\n</div>\";\n\t\tresult = HtmlUtil.removeHtmlTag(str, \"div\");\n\t\tassertEquals(\"pre\", result);\n\t}\n\n\t@Test\n\tpublic void cleanHtmlTagTest() {\n\t\t//非闭合标签\n\t\tString str = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\">\";\n\t\tString result = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img>\";\n\t\tresult = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\" />\";\n\t\tresult = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img />\";\n\t\tresult = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"pre\", result);\n\n\t\t//包含内容标签\n\t\tstr = \"pre<div class=\\\"test_div\\\">dfdsfdsfdsf</div>\";\n\t\tresult = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"predfdsfdsfdsf\", result);\n\n\t\t//带换行\n\t\tstr = \"pre<div class=\\\"test_div\\\">\\r\\n\\t\\tdfdsfdsfdsf\\r\\n</div><div class=\\\"test_div\\\">BBBB</div>\";\n\t\tresult = HtmlUtil.cleanHtmlTag(str);\n\t\tassertEquals(\"pre\\r\\n\\t\\tdfdsfdsfdsf\\r\\nBBBB\", result);\n\t}\n\n\t@Test\n\tpublic void cleanEmptyTagTest() {\n\t\tString str = \"<p></p><div></div>\";\n\t\tString result = HtmlUtil.cleanEmptyTag(str);\n\t\tassertEquals(\"\", result);\n\n\t\tstr = \"<p>TEXT</p><div></div>\";\n\t\tresult = HtmlUtil.cleanEmptyTag(str);\n\t\tassertEquals(\"<p>TEXT</p>\", result);\n\n\t\tstr = \"<p></p><div>TEXT</div>\";\n\t\tresult = HtmlUtil.cleanEmptyTag(str);\n\t\tassertEquals(\"<div>TEXT</div>\", result);\n\n\t\tstr = \"<p>TEXT</p><div>TEXT</div>\";\n\t\tresult = HtmlUtil.cleanEmptyTag(str);\n\t\tassertEquals(\"<p>TEXT</p><div>TEXT</div>\", result);\n\n\t\tstr = \"TEXT<p></p><div></div>TEXT\";\n\t\tresult = HtmlUtil.cleanEmptyTag(str);\n\t\tassertEquals(\"TEXTTEXT\", result);\n\t}\n\n\t@Test\n\tpublic void unwrapHtmlTagTest() {\n\t\t//非闭合标签\n\t\tString str = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\">\";\n\t\tString result = HtmlUtil.unwrapHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img>\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img src=\\\"xxx/dfdsfds/test.jpg\\\" />\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img />\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//闭合标签\n\t\tstr = \"pre<img/>\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"img\");\n\t\tassertEquals(\"pre\", result);\n\n\t\t//包含内容标签\n\t\tstr = \"pre<div class=\\\"test_div\\\">abc</div>\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"div\");\n\t\tassertEquals(\"preabc\", result);\n\n\t\t//带换行\n\t\tstr = \"pre<div class=\\\"test_div\\\">\\r\\n\\t\\tabc\\r\\n</div>\";\n\t\tresult = HtmlUtil.unwrapHtmlTag(str, \"div\");\n\t\tassertEquals(\"pre\\r\\n\\t\\tabc\\r\\n\", result);\n\t}\n\n\t@Test\n\tpublic void unwrapTest2() {\n\t\t// 避免移除i却误删img标签的情况\n\t\tfinal String htmlString = \"<html><img src='aaa'><i>测试文本</i></html>\";\n\t\tfinal String tagString = \"i,br\";\n\t\tfinal String cleanTxt = HtmlUtil.removeHtmlTag(htmlString, false, tagString.split(\",\"));\n\t\tassertEquals(\"<html><img src='aaa'>测试文本</html>\", cleanTxt);\n\t}\n\n\t@Test\n\tpublic void escapeTest() {\n\t\tfinal String html = \"<html><body>123'123'</body></html>\";\n\t\tfinal String escape = HtmlUtil.escape(html);\n\t\tassertEquals(\"&lt;html&gt;&lt;body&gt;123&#039;123&#039;&lt;/body&gt;&lt;/html&gt;\", escape);\n\t\tfinal String restoreEscaped = HtmlUtil.unescape(escape);\n\t\tassertEquals(html, restoreEscaped);\n\t\tassertEquals(\"'\", HtmlUtil.unescape(\"&apos;\"));\n\t}\n\n\t@Test\n\tpublic void escapeTest2() {\n\t\tfinal char c = ' '; // 不断开空格（non-breaking space，缩写nbsp。)\n\t\tassertEquals(c, 160);\n\t\tfinal String html = \"<html><body> </body></html>\";\n\t\tfinal String escape = HtmlUtil.escape(html);\n\t\tassertEquals(\"&lt;html&gt;&lt;body&gt;&nbsp;&lt;/body&gt;&lt;/html&gt;\", escape);\n\t\tassertEquals(\" \", HtmlUtil.unescape(\"&nbsp;\"));\n\t}\n\n\t@Test\n\tpublic void filterTest() {\n\t\tfinal String html = \"<alert></alert>\";\n\t\tfinal String filter = HtmlUtil.filter(html);\n\t\tassertEquals(\"\", filter);\n\t}\n\n\t@Test\n\tpublic void removeHtmlAttrTest() {\n\n\t\t// 去除的属性加双引号测试\n\t\tString html = \"<div class=\\\"test_div\\\"></div><span class=\\\"test_div\\\"></span>\";\n\t\tString result = HtmlUtil.removeHtmlAttr(html, \"class\");\n\t\tassertEquals(\"<div></div><span></span>\", result);\n\n\t\t// 去除的属性后跟空格、加单引号、不加引号测试\n\t\thtml = \"<div class=test_div></div><span Class='test_div' ></span>\";\n\t\tresult = HtmlUtil.removeHtmlAttr(html, \"class\");\n\t\tassertEquals(\"<div></div><span></span>\", result);\n\n\t\t// 去除的属性位于标签末尾、其它属性前测试\n\t\thtml = \"<div style=\\\"margin:100%\\\" class=test_div></div><span Class='test_div' width=100></span>\";\n\t\tresult = HtmlUtil.removeHtmlAttr(html, \"class\");\n\t\tassertEquals(\"<div style=\\\"margin:100%\\\"></div><span width=100></span>\", result);\n\n\t\t// 去除的属性名和值之间存在空格\n\t\thtml = \"<div style = \\\"margin:100%\\\" class = test_div></div><span Class = 'test_div' width=100></span>\";\n\t\tresult = HtmlUtil.removeHtmlAttr(html, \"class\");\n\t\tassertEquals(\"<div style = \\\"margin:100%\\\"></div><span width=100></span>\", result);\n\t}\n\n\t@Test\n\tpublic void removeAllHtmlAttrTest() {\n\t\tfinal String html = \"<div class=\\\"test_div\\\" width=\\\"120\\\"></div>\";\n\t\tfinal String result = HtmlUtil.removeAllHtmlAttr(html, \"div\");\n\t\tassertEquals(\"<div></div>\", result);\n\t}\n\n\t@Test\n\tpublic void issueI6YNTFTest() {\n\t\tString html = \"<html><body><div class=\\\"a1 a2\\\">hello world</div></body></html>\";\n\t\tString cleanText = HtmlUtil.removeHtmlAttr(html,\"class\");\n\t\tassertEquals(\"<html><body><div>hello world</div></body></html>\", cleanText);\n\n\t\thtml = \"<html><body><div class=a1>hello world</div></body></html>\";\n\t\tcleanText = HtmlUtil.removeHtmlAttr(html,\"class\");\n\t\tassertEquals(\"<html><body><div>hello world</div></body></html>\", cleanText);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HttpGlobalConfigTest.java",
    "content": "package cn.hutool.http;\n\nimport org.junit.jupiter.api.Test;\n\npublic class HttpGlobalConfigTest {\n\t@Test\n\tpublic void allowPatchTest() {\n\t\tHttpGlobalConfig.allowPatch();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TimeInterval;\nimport cn.hutool.core.io.resource.StringResource;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.net.SSLProtocols;\nimport cn.hutool.core.net.url.UrlBuilder;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.CookieManager;\nimport java.net.HttpCookie;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * {@link HttpRequest}单元测试\n *\n * @author Looly\n */\npublic class HttpRequestTest {\n\tfinal String url = \"http://photo.qzone.qq.com/fcgi-bin/fcg_list_album?uin=88888&outstyle=2\";\n\n\t@Test\n\t@Disabled\n\tpublic void getHttpsTest() {\n\t\tfinal String body = HttpRequest.get(\"https://www.hutool.cn/\").timeout(10).execute().body();\n\t\tConsole.log(body);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getHttpsThenTest() {\n\t\tHttpRequest\n\t\t\t\t.get(\"https://hutool.cn\")\n\t\t\t\t.then(response -> Console.log(response.body()));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getCookiesTest() {\n\t\t// 检查在Connection关闭情况下Cookie是否可以正常获取\n\t\tfinal HttpResponse res = HttpRequest.get(\"https://www.oschina.net/\").execute();\n\t\tfinal String body = res.body();\n\t\tConsole.log(res.getCookies());\n\t\tConsole.log(body);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void toStringTest() {\n\t\tfinal String url = \"https://hutool.cn?ccc=你好\";\n\n\t\tfinal HttpRequest request = HttpRequest.get(url).form(\"a\", \"测试\");\n\t\tConsole.log(request.toString());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void asyncHeadTest() {\n\t\tfinal HttpResponse response = HttpRequest.head(url).execute();\n\t\tfinal Map<String, List<String>> headers = response.headers();\n\t\tConsole.log(headers);\n\t\tConsole.log(response.body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void asyncGetTest() {\n\t\tfinal TimeInterval timer = DateUtil.timer();\n\t\tfinal HttpResponse body = HttpRequest.get(url).charset(\"GBK\").executeAsync();\n\t\tfinal long interval = timer.interval();\n\t\ttimer.restart();\n\t\tConsole.log(body.body());\n\t\tfinal long interval2 = timer.interval();\n\t\tConsole.log(\"Async response spend {}ms, body spend {}ms\", interval, interval2);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void syncGetTest() {\n\t\tfinal TimeInterval timer = DateUtil.timer();\n\t\tfinal HttpResponse body = HttpRequest.get(url).charset(\"GBK\").execute();\n\t\tfinal long interval = timer.interval();\n\t\ttimer.restart();\n\t\tConsole.log(body.body());\n\t\tfinal long interval2 = timer.interval();\n\t\tConsole.log(\"Async response spend {}ms, body spend {}ms\", interval, interval2);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void customGetTest() {\n\t\t// 自定义构建HTTP GET请求，发送Http GET请求，针对HTTPS安全加密，可以自定义SSL\n\t\tfinal HttpRequest request = HttpRequest.get(url)\n\t\t\t\t// 自定义返回编码\n\t\t\t\t.charset(CharsetUtil.CHARSET_GBK)\n\t\t\t\t// 禁用缓存\n\t\t\t\t.disableCache()\n\t\t\t\t// 自定义SSL版本\n\t\t\t\t.setSSLProtocol(SSLProtocols.TLSv12);\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getDeflateTest() {\n\t\tfinal String res = HttpRequest.get(\"https://comment.bilibili.com/67573272.xml\")\n\t\t\t\t.execute().body();\n\t\tConsole.log(res);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void bodyTest() {\n\t\tfinal String ddddd1 = HttpRequest.get(\"https://baijiahao.baidu.com/s\").body(\"id=1625528941695652600\").execute().body();\n\t\tConsole.log(ddddd1);\n\t}\n\n\t/**\n\t * 测试GET请求附带body体是否会变更为POST\n\t */\n\t@Test\n\t@Disabled\n\tpublic void getLocalTest() {\n\t\tfinal List<String> list = new ArrayList<>();\n\t\tlist.add(\"hhhhh\");\n\t\tlist.add(\"sssss\");\n\n\t\tfinal Map<String, Object> map = new HashMap<>(16);\n\t\tmap.put(\"recordId\", \"12321321\");\n\t\tmap.put(\"page\", \"1\");\n\t\tmap.put(\"size\", \"2\");\n\t\tmap.put(\"sizes\", list);\n\n\t\tHttpRequest\n\t\t\t\t.get(\"http://localhost:8888/get\")\n\t\t\t\t.form(map)\n\t\t\t\t.then(resp -> Console.log(resp.body()));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getWithoutEncodeTest() {\n\t\tfinal String url = \"https://img-cloud.voc.com.cn/140/2020/09/03/c3d41b93e0d32138574af8e8b50928b376ca5ba61599127028157.png?imageMogr2/auto-orient/thumbnail/500&pid=259848\";\n\t\tfinal HttpRequest get = HttpUtil.createGet(url);\n\t\tConsole.log(get.getUrl());\n\t\tfinal HttpResponse execute = get.execute();\n\t\tConsole.log(execute.body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void followRedirectsTest() {\n\t\t// 从5.7.19开始关闭JDK的自动重定向功能，改为手动重定向\n\t\t// 当有多层重定向时，JDK的重定向会失效，或者说只有最后一个重定向有效，因此改为手动更易控制次数\n\t\t// 此链接有两次重定向，当设置次数为1时，表示最多执行一次重定向，即请求2次\n\t\tfinal String url = \"http://api.rosysun.cn/sjtx/?type=2\";\n//\t\tString url = \"https://api.btstu.cn/sjtx/api.php?lx=b1\";\n\n\t\t// 方式1：全局设置\n\t\tHttpGlobalConfig.setMaxRedirectCount(1);\n\t\tHttpResponse execute = HttpRequest.get(url).execute();\n\t\tConsole.log(execute.getStatus(), execute.header(Header.LOCATION));\n\n\t\t// 方式2，单独设置\n\t\texecute = HttpRequest.get(url).setMaxRedirectCount(1).execute();\n\t\tConsole.log(execute.getStatus(), execute.header(Header.LOCATION));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void followRedirectsCookieTrueTest() {\n\t\tfinal String url = \"http://localhost:8888/redirect1\";\n\t\tCookieManager cookieManager = new CookieManager();\n\t\tHttpRequest.setCookieManager(cookieManager);\n\t\tHttpResponse execute = HttpRequest.get(url)\n\t\t\t\t.setMaxRedirectCount(20)\n\t\t\t\t.setFollowRedirectsCookie(true)\n\t\t\t\t.execute();\n\t\tList<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();\n\t\tConsole.log(execute.getStatus(), cookies);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void followRedirectsCookieFalseTest() {\n\t\tfinal String url = \"http://localhost:8888/redirect1\";\n\t\tCookieManager cookieManager = new CookieManager();\n\t\tHttpRequest.setCookieManager(cookieManager);\n\t\tHttpResponse execute = HttpRequest.get(url)\n\t\t\t\t.setMaxRedirectCount(20)\n\t\t\t\t.execute();\n\t\tList<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();\n\t\tConsole.log(execute.getStatus(), cookies);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addInterceptorTest() {\n\t\tHttpUtil.createGet(\"https://hutool.cn\")\n\t\t\t\t.addInterceptor(Console::log)\n\t\t\t\t.addResponseInterceptor((res)-> Console.log(res.getStatus()))\n\t\t\t\t.execute();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addGlobalInterceptorTest() {\n\t\tGlobalInterceptor.INSTANCE.addRequestInterceptor(Console::log);\n\t\tHttpUtil.createGet(\"https://hutool.cn\").execute();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getWithFormTest(){\n\t\tfinal String url = \"https://postman-echo.com/get\";\n\t\tfinal Map<String, Object> map = new HashMap<>();\n\t\tmap.put(\"aaa\", \"application+1@qqq.com\");\n\t\tfinal HttpRequest request =HttpUtil.createGet(url).form(map);\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void urlWithParamIfGetTest(){\n\t\tfinal UrlBuilder urlBuilder = new UrlBuilder();\n\t\turlBuilder.setScheme(\"https\").setHost(\"hutool.cn\");\n\n\t\tfinal HttpRequest httpRequest = new HttpRequest(urlBuilder);\n\t\thttpRequest.setMethod(Method.GET).execute();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issueI5Y68WTest() {\n\t\tfinal HttpResponse httpResponse = HttpRequest.get(\"http://82.157.17.173:8100/app/getAddress\").execute();\n\t\tConsole.log(httpResponse.body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issueIAAE88Test() {\n\t\tfinal HttpRequest request = HttpRequest.of(\"http://127.0.0.1:8003/com.rnd.aiq:message/message/getName/15\", null);\n\t\tConsole.log(request);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testHttpResource() {\n\n\t\tHttpRequest httpRequest = HttpRequest.post(\"http://127.0.0.1:8080/testHttpResource\");\n\t\tHttpResponse response = httpRequest.form(\"user\", new HttpResource(new StringResource(\"{\\n\" +\n\t\t\t\t\t\t\"    \\\"name\\\": \\\"张三\\\",\\n\" +\n\t\t\t\t\t\t\"    \\\"age\\\": \\\"16\\\"\\n\" +\n\t\t\t\t\t\t\"}\"), \"application/json\"))\n\t\t\t\t.form(\"passport\", \"12456\")\n\t\t\t\t.execute();\n\t\tConsole.log(response.body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issueIAAOC1Test() {\n\t\tHttpGlobalConfig.setDecodeUrl(true);\n\t\tHttpRequest request = HttpRequest.get(\"http://localhost:9999/qms/bus/qmsBusReportCenterData/getReportDataList?reportProcessNo=A00&goodsName=工业硫酸98%&conReportTypeId=1010100000000000007&measureDateStr=2024-07-01\");\n\t\trequest.execute();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testHttpHead(){\n\t\tMap<String,String> map = new HashMap<>();\n\t\tmap.put(\"test\",\"test\");\n\t\tHttpRequest httpRequest = HttpRequest.post(\"http://127.0.0.1:8080/testHttpHead\")\n\t\t\t.header(\"test\",\"test1\")\n\t\t\t.headerMap(map,false)\n\t\t\t.headerAggregation(true);\n\t\tSystem.out.println(httpRequest.headers());\n\t\tHttpResponse execute = httpRequest.execute();\n\t\tSystem.out.println(execute.body());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.net.NetUtil;\nimport cn.hutool.core.net.url.UrlBuilder;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ReUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class HttpUtilTest {\n\n\n\t@Test\n\tpublic void isHttpTest(){\n\t\tassertTrue(HttpUtil.isHttp(\"Http://aaa.bbb\"));\n\t\tassertTrue(HttpUtil.isHttp(\"HTTP://aaa.bbb\"));\n\t\tassertFalse(HttpUtil.isHttp(\"FTP://aaa.bbb\"));\n\t}\n\n\t@Test\n\tpublic void isHttpsTest(){\n\t\tassertTrue(HttpUtil.isHttps(\"Https://aaa.bbb\"));\n\t\tassertTrue(HttpUtil.isHttps(\"HTTPS://aaa.bbb\"));\n\t\tassertTrue(HttpUtil.isHttps(\"https://aaa.bbb\"));\n\t\tassertFalse(HttpUtil.isHttps(\"ftp://aaa.bbb\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void postTest() {\n\t\tfinal String result = HttpUtil.createPost(\"api.uhaozu.com/goods/description/1120448506\")\n\t\t\t\t.charset(CharsetUtil.UTF_8)\n\t\t\t\t.execute().body();\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void postTest2() {\n\t\t// 某些接口对Accept头有特殊要求，此处自定义头\n\t\tfinal String result = HttpUtil\n\t\t\t\t.createPost(\"http://cmp.ishanghome.com/cmp/v1/community/queryClusterCommunity\")\n\t\t\t\t.header(Header.ACCEPT, \"*/*\")\n\t\t\t\t.execute()\n\t\t\t\t.body();\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tfinal String result1 = HttpUtil.get(\"http://photo.qzone.qq.com/fcgi-bin/fcg_list_album?uin=88888&outstyle=2\", CharsetUtil.CHARSET_GBK);\n\t\tConsole.log(result1);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest2() {\n\t\t// 此链接较为特殊，User-Agent去掉后进入一个JS跳转页面，如果设置了，需要开启302跳转\n\t\t// 自定义的默认header无效\n\t\tfinal String result = HttpRequest\n\t\t\t\t.get(\"https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101457313&redirect_uri=http%3A%2F%2Fwww.benmovip.com%2Fpay-cloud%2Fqqlogin%2FgetCode&state=ok\")\n\t\t\t\t.removeHeader(Header.USER_AGENT).execute().body();\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest3() {\n\t\t// 测试url中带有空格的情况\n\t\tfinal String result1 = HttpUtil.get(\"http://hutool.cn:5000/kf?abc= d\");\n\t\tConsole.log(result1);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest4() {\n\t\t// 测试url中带有空格的情况\n\t\tfinal byte[] str = HttpRequest.get(\"http://img01.fs.yiban.cn/mobile/2D0Y71\").execute().bodyBytes();\n\n\t\tFileUtil.writeBytes(str, \"f:/test/2D.jpg\");\n\t\tConsole.log(str);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest5() {\n\t\tString url2 = \"http://storage.chancecloud.com.cn/20200413_%E7%B2%A4B12313_386.pdf\";\n\t\tfinal ByteArrayOutputStream os2 = new ByteArrayOutputStream();\n\t\tHttpUtil.download(url2, os2, false);\n\n\t\turl2 = \"http://storage.chancecloud.com.cn/20200413_粤B12313_386.pdf\";\n\t\tHttpUtil.download(url2, os2, false);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void get12306Test() {\n\t\tHttpRequest.get(\"https://kyfw.12306.cn/otn/\")\n\t\t\t\t.setFollowRedirects(true)\n\t\t\t\t.then(response -> Console.log(response.body()));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void downloadStringTest() {\n\t\tfinal String url = \"https://www.baidu.com\";\n\t\t// 从远程直接读取字符串，需要自定义编码，直接调用JDK方法\n\t\tfinal String content2 = HttpUtil.downloadString(url, CharsetUtil.UTF_8);\n\t\tConsole.log(content2);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void oschinaTest() {\n\t\t// 请求列表页\n\t\tString listContent = HttpUtil.get(\"https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=2\");\n\t\t// 使用正则获取所有标题\n\t\tfinal List<String> titles = ReUtil.findAll(\"<span class=\\\"text-ellipsis\\\">(.*?)</span>\", listContent, 1);\n\t\tfor (final String title : titles) {\n\t\t\t// 打印标题\n\t\t\tConsole.log(title);\n\t\t}\n\n\t\t// 请求下一页，检查Cookie是否复用\n\t\tlistContent = HttpUtil.get(\"https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=3\");\n\t\tConsole.log(listContent);\n\t}\n\n\t@Test\n\tpublic void decodeParamsTest() {\n\t\tfinal String paramsStr = \"uuuu=0&a=b&c=%3F%23%40!%24%25%5E%26%3Ddsssss555555\";\n\t\tfinal Map<String, List<String>> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.UTF_8);\n\t\tassertEquals(\"0\", map.get(\"uuuu\").get(0));\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"?#@!$%^&=dsssss555555\", map.get(\"c\").get(0));\n\t}\n\n\t@Test\n\tpublic void decodeParamMapTest() {\n\t\t// 参数值存在分界标记等号时\n\t\tfinal Map<String, String> paramMap = HttpUtil.decodeParamMap(\"https://www.xxx.com/api.action?aa=123&f_token=NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"123\",paramMap.get(\"aa\"));\n\t\tassertEquals(\"NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=\",paramMap.get(\"f_token\"));\n\t}\n\n\t@Test\n\tpublic void toParamsTest() {\n\t\tfinal String paramsStr = \"uuuu=0&a=b&c=3Ddsssss555555\";\n\t\tfinal Map<String, List<String>> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.UTF_8);\n\n\t\tfinal String encodedParams = HttpUtil.toParams(map);\n\t\tassertEquals(paramsStr, encodedParams);\n\t}\n\n\t@Test\n\tpublic void encodeParamTest() {\n\t\t// ?单独存在去除之，&单位位于末尾去除之\n\t\tString paramsStr = \"?a=b&c=d&\";\n\t\tString encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=b&c=d\", encode);\n\n\t\t// url不参与转码\n\t\tparamsStr = \"http://www.abc.dd?a=b&c=d&\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"http://www.abc.dd?a=b&c=d\", encode);\n\n\t\t// b=b中的=被当作值的一部分，不做encode\n\t\tparamsStr = \"a=b=b&c=d&\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=b=b&c=d\", encode);\n\n\t\t// =d的情况被处理为key为空\n\t\tparamsStr = \"a=bbb&c=d&=d\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=bbb&c=d&=d\", encode);\n\n\t\t// d=的情况被处理为value为空\n\t\tparamsStr = \"a=bbb&c=d&d=\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=bbb&c=d&d=\", encode);\n\n\t\t// 多个&&被处理为单个，相当于空条件\n\t\tparamsStr = \"a=bbb&c=d&&&d=\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=bbb&c=d&d=\", encode);\n\n\t\t// &d&相当于只有键，无值得情况\n\t\tparamsStr = \"a=bbb&c=d&d&\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=bbb&c=d&d=\", encode);\n\n\t\t// 中文的键和值被编码\n\t\tparamsStr = \"a=bbb&c=你好&哈喽&\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=\", encode);\n\n\t\t// URL原样输出\n\t\tparamsStr = \"https://www.hutool.cn/\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(paramsStr, encode);\n\n\t\t// URL原样输出\n\t\tparamsStr = \"https://www.hutool.cn/?\";\n\t\tencode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"https://www.hutool.cn/\", encode);\n\t}\n\n\t@Test\n\tpublic void decodeParamTest() {\n\t\t// 开头的？被去除\n\t\tString a = \"?a=b&c=d&\";\n\t\tMap<String, List<String>> map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\n\t\t// =e被当作空为key，e为value\n\t\ta = \"?a=b&c=d&=e\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\t\tassertEquals(\"e\", map.get(\"\").get(0));\n\n\t\t// 多余的&去除\n\t\ta = \"?a=b&c=d&=e&&&&\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\t\tassertEquals(\"e\", map.get(\"\").get(0));\n\n\t\t// 值为空\n\t\ta = \"?a=b&c=d&e=\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\t\tassertEquals(\"\", map.get(\"e\").get(0));\n\n\t\t// &=被作为键和值都为空\n\t\ta = \"a=b&c=d&=\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\t\tassertEquals(\"\", map.get(\"\").get(0));\n\n\t\t// &e&这类单独的字符串被当作key\n\t\ta = \"a=b&c=d&e&\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"b\", map.get(\"a\").get(0));\n\t\tassertEquals(\"d\", map.get(\"c\").get(0));\n\t\tassertNull(map.get(\"e\").get(0));\n\t\tassertNull(map.get(\"\").get(0));\n\n\t\t// 被编码的键和值被还原\n\t\ta = \"a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=\";\n\t\tmap = HttpUtil.decodeParams(a, CharsetUtil.UTF_8);\n\t\tassertEquals(\"bbb\", map.get(\"a\").get(0));\n\t\tassertEquals(\"你好\", map.get(\"c\").get(0));\n\t\tassertEquals(\"\", map.get(\"哈喽\").get(0));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void patchTest() {\n\t\t// 验证patch请求是否可用\n\t\tfinal String body = HttpRequest.patch(\"https://www.baidu.com\").execute().body();\n\t\tConsole.log(body);\n\t}\n\n\t@Test\n\tpublic void urlWithFormTest() {\n\t\tfinal Map<String, Object> param = new LinkedHashMap<>();\n\t\tparam.put(\"AccessKeyId\", \"123\");\n\t\tparam.put(\"Action\", \"DescribeDomainRecords\");\n\t\tparam.put(\"Format\", \"date\");\n\t\tparam.put(\"DomainName\", \"lesper.cn\"); // 域名地址\n\t\tparam.put(\"SignatureMethod\", \"POST\");\n\t\tparam.put(\"SignatureNonce\", \"123\");\n\t\tparam.put(\"SignatureVersion\", \"4.3.1\");\n\t\tparam.put(\"Timestamp\", 123432453);\n\t\tparam.put(\"Version\", \"1.0\");\n\n\t\tString urlWithForm = HttpUtil.urlWithForm(\"http://api.hutool.cn/login?type=aaa\", param, CharsetUtil.CHARSET_UTF_8, false);\n\t\tassertEquals(\n\t\t\t\t\"http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0\",\n\t\t\t\turlWithForm);\n\n\t\turlWithForm = HttpUtil.urlWithForm(\"http://api.hutool.cn/login?type=aaa\", param, CharsetUtil.CHARSET_UTF_8, false);\n\t\tassertEquals(\n\t\t\t\t\"http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0\",\n\t\t\t\turlWithForm);\n\t}\n\n\t@Test\n\tpublic void getCharsetTest() {\n\t\tString charsetName = ReUtil.get(HttpUtil.CHARSET_PATTERN, \"Charset=UTF-8;fq=0.9\", 1);\n\t\tassertEquals(\"UTF-8\", charsetName);\n\n\t\tcharsetName = ReUtil.get(HttpUtil.META_CHARSET_PATTERN, \"<meta charset=utf-8\", 1);\n\t\tassertEquals(\"utf-8\", charsetName);\n\t\tcharsetName = ReUtil.get(HttpUtil.META_CHARSET_PATTERN, \"<meta charset='utf-8'\", 1);\n\t\tassertEquals(\"utf-8\", charsetName);\n\t\tcharsetName = ReUtil.get(HttpUtil.META_CHARSET_PATTERN, \"<meta charset=\\\"utf-8\\\"\", 1);\n\t\tassertEquals(\"utf-8\", charsetName);\n\t\tcharsetName = ReUtil.get(HttpUtil.META_CHARSET_PATTERN, \"<meta charset = \\\"utf-8\\\"\", 1);\n\t\tassertEquals(\"utf-8\", charsetName);\n\t}\n\n\t@Test\n\tpublic void normalizeParamsTest() {\n\t\tfinal String encodeResult = HttpUtil.normalizeParams(\"参数\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"%E5%8F%82%E6%95%B0\", encodeResult);\n\t}\n\n\t@Test\n\tpublic void normalizeBlankParamsTest() {\n\t\tfinal String encodeResult = HttpUtil.normalizeParams(\"\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"\", encodeResult);\n\t}\n\n\t@Test\n\tpublic void normalizEampersandParamsTest() {\n\t\tfinal String encodeResult = HttpUtil.normalizeParams(\"&\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"\", encodeResult);\n\t}\n\n\t@Test\n\tpublic void getMimeTypeTest() {\n\t\tfinal String mimeType = HttpUtil.getMimeType(\"aaa.aaa\");\n\t\tassertNull(mimeType);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getWeixinTest(){\n\t\t// 测试特殊URL，即URL中有&amp;情况是否请求正常\n\t\tfinal String url = \"https://mp.weixin.qq.com/s?__biz=MzI5NjkyNTIxMg==&amp;mid=100000465&amp;idx=1&amp;sn=1044c0d19723f74f04f4c1da34eefa35&amp;chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7\";\n\t\tfinal String s = HttpUtil.get(url);\n\t\tConsole.log(s);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getNocovTest(){\n\t\tfinal String url = \"https://qiniu.nocov.cn/medical-manage%2Ftest%2FBANNER_IMG%2F444004467954556928%2F1595215173047icon.png~imgReduce?e=1597081986&token=V2lJYVgQgAv_sbypfEZ0qpKs6TzD1q5JIDVr0Tw8:89cbBkLLwEc9JsMoCLkAEOu820E=\";\n\t\tfinal String s = HttpUtil.get(url);\n\t\tConsole.log(s);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void sinajsTest(){\n\t\tfinal String s = HttpUtil.get(\"http://hq.sinajs.cn/list=sh600519\");\n\t\tConsole.log(s);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void gimg2Test(){\n\t\tfinal byte[] bytes = HttpUtil.downloadBytes(\"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea\");\n\t\tConsole.log(Base64.encode(bytes));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void acplayTest(){\n\t\tfinal String body = HttpRequest.get(\"https://api.acplay.net/api/v2/bangumi/9541\")\n\t\t\t\t.execute().body();\n\t\tConsole.log(body);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getPicTest(){\n\t\tHttpGlobalConfig.setDecodeUrl(false);\n\t\tfinal String url = \"https://p3-sign.douyinpic.com/tos-cn-i-0813/f41afb2e79a94dcf80970affb9a69415~noop.webp?x-expires=1647738000&x-signature=%2Br1ekUCGjXiu50Y%2Bk0MO4ovulK8%3D&from=4257465056&s=PackSourceEnum_DOUYIN_REFLOW&se=false&sh=&sc=&l=2022021809224601020810013524310DD3&biz_tag=aweme_images\";\n\n\t\tfinal HttpRequest request = HttpRequest.of(url).method(Method.GET);\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\tpublic void httpParameterDecodeTest () {\n\t\tfinal String test = \"this is test测试\";\n\t\tfinal int port = NetUtil.getUsableLocalPort();\n\t\tHttpUtil.createServer(port)\n\t\t\t\t.addAction(\"/formEncoded\", (req, resp) -> resp.write(req.getParam(\"test\")))\n\t\t\t\t.addAction(\"/urlEncoded\", (req, resp) -> resp.write(req.getParam(\"test\")))\n\t\t\t\t.start();\n\n\t\tfinal String resp = HttpUtil.createPost(String.format(\"http://localhost:%s/formEncoded\", port))\n\t\t\t\t.form(\"test\", test).execute().body();\n\t\tassertEquals(test, resp);\n\n\t\tfinal String urlGet = UrlBuilder.of(String.format(\"http://localhost:%s/urlEncoded\", port)).addQuery(\"test\", test).build();\n\t\tfinal String resp2 = HttpUtil.createGet(urlGet).execute().body();\n\t\tassertEquals(test, resp2);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/HttpsTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class HttpsTest {\n\n\t/**\n\t * 测试单例的SSLSocketFactory是否有线程安全问题\n\t */\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tfinal AtomicInteger count = new AtomicInteger();\n\t\tfor(int i =0; i < 100; i++){\n\t\t\tThreadUtil.execute(()->{\n\t\t\t\tfinal String s = HttpUtil.get(\"https://www.baidu.com/\");\n\t\t\t\tConsole.log(count.incrementAndGet());\n\t\t\t});\n\t\t}\n\t\tThreadUtil.sync(this);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/Issue2658Test.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.http.cookie.GlobalCookieManager;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.HttpCookie;\nimport java.util.List;\n\npublic class Issue2658Test {\n\n\t@SuppressWarnings(\"resource\")\n\t@Test\n\t@Disabled\n\tpublic void getWithCookieTest(){\n\t\tHttpRequest.get(\"https://www.baidu.com/\").execute();\n\t\tfinal List<HttpCookie> cookies = GlobalCookieManager.getCookieManager().getCookieStore().getCookies();\n\t\tConsole.log(\"###\" + cookies);\n\t\tHttpRequest.get(\"https://www.baidu.com/\").execute();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/Issue3074Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3074Test {\n\n\t@Test\n\t@Disabled\n\tpublic void bodyTest() {\n\t\tHttpUtil.createPost(\"http://localhost:8888/body\")\n\t\t\t.contentType(ContentType.JSON.getValue())\n\t\t\t.body(\"aaa\")\n\t\t\t.execute();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/Issue3197Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3197Test {\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tfinal String s = HttpUtil.get(\"http://localhost:8080/index\");\n\t\tConsole.log(s);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/Issue3314Test.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3314Test {\n\t@Test\n\t@Disabled\n\tpublic void postTest() {\n\t\tString url = \"https://hutool.cn/test/getList\";\n\t\tfinal String body = HttpRequest.get(url)\n\t\t\t.setRest(true)\n\t\t\t.body(FileUtil.readBytes(\"d:/test/3314.xlsx\"))\n\t\t\t.execute()\n\t\t\t.body();\n\n\t\tConsole.log(body);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/Issue3536Test.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Issue3536Test {\n\n\t@Test\n\tpublic void urlWithFormUrlEncodedTest() {\n\t\tString url = \"https://hutool.cn/test\";\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"redirect_uri\", \"https://api.hutool.cn/v1/test\");\n\t\tparamMap.put(\"scope\", \"a,b,c你\");\n\n\t\tfinal String s = HttpUtil.urlWithFormUrlEncoded(url, paramMap, CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"https://hutool.cn/test?scope=a,b,c%E4%BD%A0&redirect_uri=https://api.hutool.cn/v1/test\", s);\n\t}\n\n\t@Test\n\tpublic void toParamsTest() {\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"redirect_uri\", \"https://api.hutool.cn/v1/test\");\n\t\tparamMap.put(\"scope\", \"a,b,c你\");\n\n\t\tfinal String params = HttpUtil.toParams(paramMap, CharsetUtil.CHARSET_UTF_8, true);\n\t\tassertEquals(\"scope=a%2Cb%2Cc%E4%BD%A0&redirect_uri=https%3A%2F%2Fapi.hutool.cn%2Fv1%2Ftest\", params);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI5TPSYTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.HttpCookie;\n\npublic class IssueI5TPSYTest {\n\t@Test\n\t@Disabled\n\tpublic void redirectTest() {\n\t\tfinal String url = \"https://bsxt.gdzwfw.gov.cn/UnifiedReporting/auth/newIndex\";\n\t\tfinal HttpResponse res = HttpUtil.createGet(url).setFollowRedirects(true)\n\t\t\t\t.cookie(new HttpCookie(\"iPlanetDirectoryPro\", \"123\"))\n\t\t\t\t.execute();\n\t\tConsole.log(res.body());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI6RE7JTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class IssueI6RE7JTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tHttpGlobalConfig.setDecodeUrl(true);\n\t\tfinal String baseUrl = \"http://192.168.98.73/PIAPI/ArcValue\";\n\n\t\tfinal Map<String,Object> map = new HashMap<>();\n\t\tmap.put(\"tag\",\"TXSM_SC2202-苯乙烯%\");\n\t\tmap.put(\"time\",\"2023/03/28 08:00:00\");\n\t\tHttpUtil.get(baseUrl, map);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI7EHSETest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI7EHSETest {\n\t@Test\n\t@Disabled\n\tpublic void encodePathTest() {\n\t\tString body = HttpUtil\n\t\t\t.createGet(\"http://hq.sinajs.cn/list=s_sh600090\")\n\t\t\t.header(\"Referer\", \"http://finance.sina.com.cn\")\n\t\t\t.execute()\n\t\t\t.body();\n\n\t\tConsole.log(body);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI7WZEOTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI7WZEOTest {\n\t@Test\n\t@Disabled\n\tpublic void postTest() {\n\t\tfinal String post = HttpUtil.post(\"https://tenapi.cn/v2/video\", \"url=https://v.douyin.com/ie1EX3LH/\");\n\t\tConsole.log(post);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI7ZRJUTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.net.ssl.SSLSocketFactory;\n\npublic class IssueI7ZRJUTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tfinal HttpRequest httpRequest = HttpRequest.get(\"https://expired.badssl.com/\");\n\t\thttpRequest.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());\n\n\t\tConsole.log(httpRequest.execute().body());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueI8YV0KTest.java",
    "content": "package cn.hutool.http;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI8YV0KTest {\n\n\t@Test\n\tpublic void removeHtmlAttrTest(){\n\t\tfinal String str = \"<content styleCode=\\\"xmChange yes\\\">\";\n\t\tassertEquals(\"<content>\", HtmlUtil.removeHtmlAttr(str, \"styleCode\"));\n\t}\n\n\t@Test\n\tpublic void removeHtmlAttrTest2(){\n\t\tfinal String str = \"<content styleCode=\\\"xmChange\\\"/>\";\n\t\tassertEquals(\"<content/>\", HtmlUtil.removeHtmlAttr(str, \"styleCode\"));\n\t}\n\n\t@Test\n\tpublic void removeHtmlAttrTest3(){\n\t\tString str = \"<content styleCode=\\\"dada ada\\\" data=\\\"dsad\\\" >\";\n\t\tassertEquals(\"<content data=\\\"dsad\\\">\", HtmlUtil.removeHtmlAttr(str, \"styleCode\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueIB7REWTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIB7REWTest {\n\t@Test\n\t@Disabled\n\tvoid getTest() {\n\t\tSystem.setProperty(\"jdk.tls.namedCurves\", \"secp256r1,secp384r1,secp521r1\");\n\t\tfinal String s = HttpUtil.get(\"https://ebssec.boc.cn/\");\n\t\tConsole.log(s);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueIBQIYQTest.java",
    "content": "package cn.hutool.http;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class IssueIBQIYQTest {\n\t@Test\n\tvoid normalizeParamsTest1() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"id\", \"\");\n\t\tmap.put(\"type\", \"4\");\n\t\tString url = HttpUtil.toParams(map);\n\t\tAssertions.assertEquals(\"id=&type=4\", url);\n\t\turl = HttpUtil.normalizeParams(url, null);\n\t\tAssertions.assertEquals(\"id=&type=4\", url);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/IssueIBRVE4Test.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueIBRVE4Test {\n\t@Test\n\tpublic void decodeParamMapNoParamTest() {\n\t\t// 参数值不存在分界标记等号时\n\t\t// 无参数值时\n\t\tfinal Map<String, List<String>> paramMap = HttpUtil.decodeParams(\"https://hutool.cn/api.action\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(0,paramMap.size());\n\t}\n\t@Test\n\tpublic void decodeParamMapListNoParamTest() {\n\t\t// 参数值不存在分界标记等号时\n\t\t// 无参数值时\n\t\tfinal Map<String, String> paramMap1 = HttpUtil.decodeParamMap(\"https://hutool.cn/api.action\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(0,paramMap1.size());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/RestTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.resource.StringResource;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.json.JSONUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Rest类型请求单元测试\n *\n * @author looly\n */\npublic class RestTest {\n\n\t@Test\n\tpublic void contentTypeTest() {\n\t\tHttpRequest request = HttpRequest.post(\"http://localhost:8090/rest/restTest/\")//\n\t\t\t\t.body(JSONUtil.createObj()\n\t\t\t\t\t\t.set(\"aaa\", \"aaaValue\")\n\t\t\t\t\t\t.set(\"键2\", \"值2\").toString());\n\t\tassertEquals(\"application/json;charset=UTF-8\", request.header(\"Content-Type\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void postTest() {\n\t\tHttpRequest request = HttpRequest.post(\"http://localhost:8888/restTest/\")//\n\t\t\t\t.body(new StringResource(JSONUtil.createObj()\n\t\t\t\t\t\t.set(\"aaa\", \"aaaValue\")\n\t\t\t\t\t\t.set(\"键2\", \"值2\").toString()));\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void postTest2() {\n\t\tString result = HttpUtil.post(\"http://localhost:8090/rest/restTest/\", JSONUtil.createObj()//\n\t\t\t\t.set(\"aaa\", \"aaaValue\")\n\t\t\t\t.set(\"键2\", \"值2\").toString());\n\t\tConsole.log(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getWithBodyTest() {\n\t\tHttpRequest request = HttpRequest.get(\"http://localhost:8888/restTest\")//\n\t\t\t\t.header(Header.CONTENT_TYPE, \"application/json\")\n\t\t\t\t.body(JSONUtil.createObj()\n\t\t\t\t\t\t.set(\"aaa\", \"aaaValue\")\n\t\t\t\t\t\t.set(\"键2\", \"值2\").toString());\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getWithBodyTest2() {\n\t\tHttpRequest request = HttpRequest.get(\"https://ad.oceanengine.com/open_api/2/advertiser/info/\")//\n\t\t\t\t// Charles代理\n\t\t\t\t.setHttpProxy(\"localhost\", 8888)\n\t\t\t\t.header(\"Access-Token\", \"\")\n\t\t\t\t.body(JSONUtil.createObj()\n\t\t\t\t\t\t.set(\"advertiser_ids\", new Long[]{1690657248243790L})\n\t\t\t\t\t\t.set(\"fields\", new String[]{\"id\", \"name\", \"status\"}).toString());\n\t\tConsole.log(request);\n\t\tConsole.log(request.execute().body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getTest() {\n\t\tfinal HttpRequest request = HttpRequest.get(\"http://localhost:8888/restTest\");\n\t\tfinal HttpResponse execute = request.execute();\n\t\tConsole.log(execute.headers());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/UploadTest.java",
    "content": "package cn.hutool.http;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 文件上传单元测试\n *\n * @author looly\n */\npublic class UploadTest {\n\n\t/**\n\t * 多文件上传测试\n\t */\n\t@Test\n\t@Disabled\n\tpublic void uploadFilesTest() {\n\t\tFile file = FileUtil.file(\"d:\\\\图片1.JPG\");\n\t\tFile file2 = FileUtil.file(\"d:\\\\图片3.png\");\n\n\t\t// 方法一：自定义构建表单\n\t\tHttpRequest request = HttpRequest//\n\t\t\t\t.post(\"http://localhost:8888/file\")//\n\t\t\t\t.form(\"file\", file2, file)//\n\t\t\t\t.form(\"fileType\", \"图片\");\n\t\tHttpResponse response = request.execute();\n\t\tConsole.log(response.body());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadFileTest() {\n\t\tFile file = FileUtil.file(\"D:\\\\face.jpg\");\n\n\t\t// 方法二：使用统一的表单，Http模块会自动识别参数类型，并完成上传\n\t\tHashMap<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"city\", \"北京\");\n\t\tparamMap.put(\"file\", file);\n\t\tString result = HttpUtil.post(\"http://wthrcdn.etouch.cn/weather_mini\", paramMap);\n\t\tSystem.out.println(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void uploadTest2() {\n\t\t//客户端\n\t\tString url = \"http://192.168.1.200:8888/meta/upload/img\";\n\t\tPath file = Paths.get(\"D:\\\\test\\\\testBigData_upload.xlsx\");\n\t\tMap<String, String> headers = new HashMap<>(16);\n\t\theaders.put(\"md5\", \"aaaaaaaa\");\n\n\t\tMap<String, Object> params = new HashMap<>(16);\n\t\tparams.put(\"fileName\", file.toFile().getName());\n\t\tparams.put(\"file\", file.toFile());\n\t\tHttpRequest httpRequest = HttpRequest.post(url)\n\t\t\t\t.setChunkedStreamingMode(1024 * 1024)\n\t\t\t\t.headerMap(headers, false)\n\t\t\t\t.form(params);\n\t\tHttpResponse httpResponse = httpRequest.execute();\n\t\tConsole.log(httpResponse);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void smmsTest(){\n\t\t// https://github.com/chinabugotech/hutool/issues/2079\n\t\t// hutool的user agent 被封了\n\t\tString token = \"test\";\n\t\tString url = \"https://sm.ms/api/v2/upload\";\n\t\tString result = HttpUtil.createPost(url)\n\t\t\t\t.header(Header.USER_AGENT, \"PostmanRuntime/7.28.4\")\n\t\t\t\t.auth(token)\n\t\t\t\t.form(\"smfile\", FileUtil.file(\"d:/test/qrcodeCustom.png\"))\n\t\t\t\t.execute().body();\n\n\t\tConsole.log(result);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java",
    "content": "package cn.hutool.http.body;\n\nimport cn.hutool.core.io.resource.StringResource;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.http.HttpResource;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class MultipartBodyTest {\n\n\t@Test\n\tpublic void buildTest(){\n\t\tMap<String, Object> form = new HashMap<>();\n\t\tform.put(\"pic1\", \"pic1 content\");\n\t\tform.put(\"pic2\", new HttpResource(\n\t\t\t\tnew StringResource(\"pic2 content\"), \"text/plain\"));\n\t\tform.put(\"pic3\", new HttpResource(\n\t\t\t\tnew StringResource(\"pic3 content\", \"pic3.jpg\"), \"image/jpeg\"));\n\n\t\tfinal MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8);\n\n\t\tassertNotNull(body.toString());\n//\t\tConsole.log(body);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/BlankServerTest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.swing.DesktopUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpUtil;\n\npublic class BlankServerTest {\n\tpublic static void main(String[] args) {\n\t\tHttpUtil.createServer(8888)\n\t\t\t\t.addAction(\"/\", (req, res)-> res.write(\"Hello Hutool Server\", ContentType.JSON.getValue()))\n\t\t\t\t.start();\n\n\t\tDesktopUtil.browse(\"http://localhost:8888/\");\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/DocServerTest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.swing.DesktopUtil;\nimport cn.hutool.http.HttpUtil;\n\npublic class DocServerTest {\n\n\tpublic static void main(String[] args) {\n\t\tHttpUtil.createServer(80)\n\t\t\t\t// 设置默认根目录，\n\t\t\t\t.setRoot(\"D:\\\\workspace\\\\site\\\\hutool-site\")\n\t\t\t\t.start();\n\n\t\tDesktopUtil.browse(\"http://localhost/\");\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/ExceptionServerTest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.http.server.filter.DefaultExceptionFilter;\n\nimport java.io.IOException;\n\npublic class ExceptionServerTest {\n\tpublic static void main(final String[] args) {\n\t\tHttpUtil.createServer(8888)\n\t\t\t.addFilter(new DefaultExceptionFilter())\n\t\t\t.addAction(\"/\", (req, res) -> {\n\t\t\t\tthrow new RuntimeException(\"Test Exception\");\n\t\t\t})\n\t\t\t.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/Issue3343Test.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.multi.ListValueMap;\nimport cn.hutool.http.HttpUtil;\n\n/**\n * http://localhost:8888/?name=hutool\n */\npublic class Issue3343Test {\n\tpublic static void main(final String[] args) {\n\t\tfinal SimpleServer server = HttpUtil.createServer(8888)\n\t\t\t.addFilter((req, res, chain) -> {\n\t\t\t\tConsole.log(DateUtil.now() + \" got request: \" + req.getPath());\n\t\t\t\tConsole.log(\" >   from : \" + req.getClientIP());\n\t\t\t\t// 过滤器中获取请求参数\n\t\t\t\tConsole.log(\" > params : \" + req.getParams());\n\t\t\t\tchain.doFilter(req.getHttpExchange());\n\t\t\t});\n\n\t\tserver.addAction(\"/\", Issue3343Test::index);\n\n\t\tserver.start();\n\t}\n\n\tprivate static void index(HttpServerRequest request, HttpServerResponse response) {\n\t\t// 具体逻辑中再次获取请求参数\n\t\tListValueMap<String, String> params = request.getParams();\n\t\tConsole.log(\"index params: \" + params);\n\t\tresponse.getWriter().write(\"GOT: \" + params);\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/Issue3568Test.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.http.HttpUtil;\n\nimport java.io.IOException;\n\npublic class Issue3568Test {\n\tpublic static void main(String[] args) {\n\t\tHttpUtil.createServer(8888)\n\t\t\t.addHandler(\"/\", httpExchange -> {\n\t\t\t\tthrow new IOException(\"111\");\n\t\t\t})\n\t\t\t.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/Issue3723Test.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpUtil;\n\npublic class Issue3723Test {\n\tpublic static void main(String[] args) {\n\t\tSimpleServer server = HttpUtil.createServer(8888);\n\t\tserver.addFilter((req, res, chain) -> {\n\t\t\tString requestId = IdUtil.fastSimpleUUID();\n\t\t\treq.getHttpExchange().setAttribute(\"requestId\", requestId);\n\t\t\tres.addHeader(\"X-Request-Id\", requestId);\n\n\t\t\tres.write(\"new Content\");\n\n\t\t\tchain.doFilter(req.getHttpExchange());\n\t\t});\n\t\tserver.addAction(\"/\", (req, res)-> res.write(\"Hello Hutool Server\", ContentType.JSON.getValue()));\n\t\tserver.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/RedirectServerTest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.http.Header;\nimport cn.hutool.http.HttpUtil;\n\npublic class RedirectServerTest {\n\n\tpublic static void main(String[] args) {\n\t\tHttpUtil.createServer(8888).addAction(\"/redirect1\", (request, response) -> {\n\t\t\tresponse.addHeader(Header.LOCATION.getValue(), \"http://localhost:8888/redirect2\");\n\t\t\tresponse.addHeader(Header.SET_COOKIE.getValue(), \"redirect1=1; path=/; HttpOnly\");\n\t\t\tresponse.send(301);\n\t\t}).addAction(\"/redirect2\", (request, response) -> {\n\t\t\tresponse.addHeader(Header.LOCATION.getValue(), \"http://localhost:8888/redirect3\");\n\t\t\tresponse.addHeader(Header.SET_COOKIE.getValue(), \"redirect2=2; path=/; HttpOnly\");\n\t\t\tresponse.send(301);\n\t\t}).addAction(\"/redirect3\", (request, response) -> {\n\t\t\tresponse.addHeader(Header.LOCATION.getValue(), \"http://localhost:8888/redirect4\");\n\t\t\tresponse.addHeader(Header.SET_COOKIE.getValue(), \"redirect3=3; path=/; HttpOnly\");\n\t\t\tresponse.send(301);\n\t\t}).addAction(\"/redirect4\", (request, response) -> {\n\t\t\tfinal String cookie = request.getHeader(Header.COOKIE);\n\t\t\tConsole.log(cookie);\n\t\t\tresponse.sendOk();\n\t\t}).start();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/server/SimpleServerTest.java",
    "content": "package cn.hutool.http.server;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.net.multipart.UploadFile;\nimport cn.hutool.http.ContentType;\nimport cn.hutool.http.HttpUtil;\nimport cn.hutool.json.JSONUtil;\n\npublic class SimpleServerTest {\n\n\tpublic static void main(String[] args) {\n\t\tHttpUtil.createServer(8888)\n\t\t\t.addFilter(((req, res, chain) -> {\n\t\t\t\tConsole.log(\"Filter: \" + req.getPath());\n\t\t\t\tchain.doFilter(req.getHttpExchange());\n\t\t\t}))\n\t\t\t// 设置默认根目录，classpath/html\n\t\t\t.setRoot(FileUtil.file(\"html\"))\n\t\t\t// get数据测试，返回请求的PATH\n\t\t\t.addAction(\"/get\", (request, response) ->\n\t\t\t\tresponse.write(request.getURI().toString(), ContentType.TEXT_PLAIN.toString())\n\t\t\t)\n\t\t\t// 返回JSON数据测试\n\t\t\t.addAction(\"/restTest\", (request, response) -> {\n\t\t\t\tString res = JSONUtil.createObj()\n\t\t\t\t\t.set(\"id\", 1)\n\t\t\t\t\t.set(\"method\", request.getMethod())\n\t\t\t\t\t.set(\"request\", request.getBody())\n\t\t\t\t\t.toStringPretty();\n\t\t\t\tresponse.write(res, ContentType.JSON.toString());\n\t\t\t})\n\t\t\t// 获取表单数据测试\n\t\t\t// http://localhost:8888/formTest?a=1&a=2&b=3\n\t\t\t.addAction(\"/formTest\", (request, response) ->\n\t\t\t\tresponse.write(request.getParams().toString(), ContentType.TEXT_PLAIN.toString())\n\t\t\t)\n\n\t\t\t// 文件上传测试\n\t\t\t// http://localhost:8888/formForUpload.html\n\t\t\t.addAction(\"/file\", (request, response) -> {\n\t\t\t\t\tConsole.log(\"Upload file...\");\n\t\t\t\t\tConsole.log(request.getParams());\n\t\t\t\t\tfinal UploadFile[] files = request.getMultipart().getFiles(\"file\");\n\t\t\t\t\t// 传入目录，默认读取HTTP头中的文件名然后创建文件\n\t\t\t\t\tfor (UploadFile file : files) {\n\t\t\t\t\t\tfile.write(\"d:/test/\");\n\t\t\t\t\t\tConsole.log(\"Write file: d:/test/\" + file.getFileName());\n\t\t\t\t\t}\n\t\t\t\t\tresponse.write(request.getMultipart().getParamMap().toString(), ContentType.TEXT_PLAIN.toString());\n\t\t\t\t}\n\t\t\t)\n\t\t\t// 测试输出响应内容是否能正常返回Content-Length头信息\n\t\t\t.addAction(\"test/zeroStr\", (req, res) -> {\n\t\t\t\tres.write(\"0\");\n\t\t\t\tConsole.log(\"Write 0 OK\");\n\t\t\t})\n\t\t\t.addAction(\"/body\", (req, resp) -> {\n\t\t\t\tConsole.log(req.getBody());\n\t\t\t\tresp.write(req.getBody());\n\t\t\t})\n\t\t\t.start();\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/useragent/IssueIB3SJFTest.java",
    "content": "package cn.hutool.http.useragent;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIB3SJFTest {\n\t@Test\n\tvoid isMobileTest() {\n\t\tString str=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 NetType/WIFI \" +\n\t\t\t\"MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090c11) XWEB/11275 Flue\";\n\t\tUserAgent ua = UserAgentUtil.parse(str);\n\n\t\tAssertions.assertFalse(ua.isMobile());\n\t\tAssertions.assertEquals(\"7.0.20.1781\", ua.getBrowser().getVersion(str));\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/useragent/UserAgentUtilTest.java",
    "content": "package cn.hutool.http.useragent;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class UserAgentUtilTest {\n\n\t@Test\n\tpublic void parseDesktopTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1\";\n\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Chrome\", ua.getBrowser().toString());\n\t\tassertEquals(\"14.0.835.163\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"535.1\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 7 or Windows Server 2008R2\", ua.getOs().toString());\n\t\tassertEquals(\"6.1\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseMobileTest() {\n\t\tfinal String uaStr = \"User-Agent:Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5\";\n\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Safari\", ua.getBrowser().toString());\n\t\tassertEquals(\"5.0.2\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"533.17.9\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"4_3_3\", ua.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseMiui10WithChromeTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Linux; Android 9; MIX 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Chrome\", ua.getBrowser().toString());\n\t\tassertEquals(\"70.0.3538.80\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"9\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseHuaweiPhoneWithNativeBrowserTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Linux; Android 10; EML-AL00 Build/HUAWEIEML-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Mobile Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"Android Browser\", ua.getBrowser().toString());\n\t\tassertEquals(\"4.0\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"10\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseSamsungPhoneWithNativeBrowserTest() {\n\t\tfinal String uaString = \"Dalvik/2.1.0 (Linux; U; Android 9; SM-G950U Build/PPR1.180610.011)\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"Android Browser\", ua.getBrowser().toString());\n\t\tassertNull(ua.getVersion());\n\t\tassertEquals(\"Unknown\", ua.getEngine().toString());\n\t\tassertNull(ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"9\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWindows10WithChromeTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Chrome\", ua.getBrowser().toString());\n\t\tassertEquals(\"70.0.3538.102\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWindows10WithIe11Test() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSIE11\", ua.getBrowser().toString());\n\t\tassertEquals(\"11.0\", ua.getVersion());\n\t\tassertEquals(\"Trident\", ua.getEngine().toString());\n\t\tassertEquals(\"7.0\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWindows10WithIeMobileLumia520Test() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 520) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537 \";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"IEMobile\", ua.getBrowser().toString());\n\t\tassertEquals(\"11.0\", ua.getVersion());\n\t\tassertEquals(\"Trident\", ua.getEngine().toString());\n\t\tassertEquals(\"7.0\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows Phone\", ua.getOs().toString());\n\t\tassertEquals(\"8.1\", ua.getOsVersion());\n\t\tassertEquals(\"Windows Phone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWindows10WithIe8EmulatorTest() {\n\t\tfinal String uaStr = \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSIE\", ua.getBrowser().toString());\n\t\tassertEquals(\"8.0\", ua.getVersion());\n\t\tassertEquals(\"Trident\", ua.getEngine().toString());\n\t\tassertEquals(\"4.0\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 7 or Windows Server 2008R2\", ua.getOs().toString());\n\t\tassertEquals(\"6.1\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWindows10WithEdgeTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSEdge\", ua.getBrowser().toString());\n\t\tassertEquals(\"18.17763\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseEdgeOnLumia950XLTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Mobile Safari/537.36 Edge/15.14900\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSEdge\", ua.getBrowser().toString());\n\t\tassertEquals(\"15.14900\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows Phone\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows Phone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseChromeOnWindowsServer2012R2Test() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Chrome\", ua.getBrowser().toString());\n\t\tassertEquals(\"63.0.3239.132\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 8.1 or Windows Server 2012R2\", ua.getOs().toString());\n\t\tassertEquals(\"6.3\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseIE11OnWindowsServer2008R2Test() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSIE11\", ua.getBrowser().toString());\n\t\tassertEquals(\"11.0\", ua.getVersion());\n\t\tassertEquals(\"Trident\", ua.getEngine().toString());\n\t\tassertEquals(\"7.0\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 7 or Windows Server 2008R2\", ua.getOs().toString());\n\t\tassertEquals(\"6.1\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseEdgeTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.69 Safari/537.36 Edg/81.0.416.34\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSEdge\", ua.getBrowser().toString());\n\t\tassertEquals(\"81.0.416.34\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t/**\n\t * https://github.com/chinabugotech/hutool/issues/1177\n\t */\n\t@Test\n\tpublic void parseMicroMessengerTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Mobile/15A372 MicroMessenger/7.0.17(0x17001127) NetType/WIFI Language/zh_CN\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"MicroMessenger\", ua.getBrowser().toString());\n\t\tassertEquals(\"7.0.17\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"604.1.38\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"11_0\", ua.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWorkWxTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/3.0.31 MicroMessenger/7.0.1 Language/zh\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"wxwork\", ua.getBrowser().toString());\n\t\tassertEquals(\"3.0.31\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"605.1.15\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseQQTest() {\n\t\tfinal String uaString = \"User-Agent: MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"QQBrowser\", ua.getBrowser().toString());\n\t\tassertEquals(\"26\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"533.1\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"2.3.7\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseDingTalkTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/18A373 AliApp(DingTalk/5.1.33) com.laiwang.DingTalk/13976299 Channel/201200 language/zh-Hans-CN WK\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"DingTalk\", ua.getBrowser().toString());\n\t\tassertEquals(\"5.1.33\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"605.1.15\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"14_0\", ua.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseAlipayTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Linux; U; Android 7.0; zh-CN; FRD-AL00 Build/HUAWEIFRD-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 UCBrowser/11.3.8.909 UWS/2.10.2.5 Mobile Safari/537.36 UCBS/2.10.2.5 Nebula AlipayDefined(nt:WIFI,ws:360|0|3.0) AliApp(AP/10.0.18.062203) AlipayClient/10.0.18.062203 Language/zh-Hans useStatusBar/true\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"Alipay\", ua.getBrowser().toString());\n\t\tassertEquals(\"10.0.18.062203\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"7.0\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseTaobaoTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; MI 2C Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36 AliApp(TB/4.9.2) WindVane/5.2.2 TBANDROID/700342@taobao_android_4.9.2 720X1280\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"Taobao\", ua.getBrowser().toString());\n\t\tassertEquals(\"4.9.2\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"4.4.4\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseUCTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 UBrowser/4.0.3214.0 Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"UCBrowser\", ua.getBrowser().toString());\n\t\tassertEquals(\"4.0.3214.0\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 7 or Windows Server 2008R2\", ua.getOs().toString());\n\t\tassertEquals(\"6.1\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseUCTest2() {\n\t\tfinal String uaString = \"Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X; zh-CN) AppleWebKit/537.51.1 (KHTML, like Gecko) Mobile/16G102 UCBrowser/12.7.6.1251 Mobile AliApp(TUnionSDK/0.1.20.3)\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"UCBrowser\", ua.getBrowser().toString());\n\t\tassertEquals(\"12.7.6.1251\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.51.1\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"12_4_1\", ua.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseQuarkTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X; zh-cn) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/16G102 Quark/3.6.2.993 Mobile\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"Quark\", ua.getBrowser().toString());\n\t\tassertEquals(\"3.6.2.993\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"601.1.46\", ua.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua.getOs().toString());\n\t\tassertEquals(\"12_4_1\", ua.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWxworkTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/4.0.1326.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 wxwork/3.1.10 (MicroMessenger/6.2) WindowsWechat\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"wxwork\", ua.getBrowser().toString());\n\t\tassertEquals(\"3.1.10\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseWxworkMobileTest() {\n\t\tfinal String uaString = \"Mozilla/5.0 (Linux; Android 10; JSN-AL00 Build/HONORJSN-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 MQQBrowser/6.2 TBS/045710 Mobile Safari/537.36 wxwork/3.1.10 ColorScheme/Light MicroMessenger/7.0.1 NetType/WIFI Language/zh Lang/zh\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaString);\n\t\tassertEquals(\"wxwork\", ua.getBrowser().toString());\n\t\tassertEquals(\"3.1.10\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"10\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseEdgATest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4MCBP\n\t\tfinal String uaStr = \"userAgent: Mozilla/5.0 (Linux; Android 11; MI 9 Transparent Edition) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Mobile Safari/537.36 EdgA/96.0.1054.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"MSEdge\", ua.getBrowser().toString());\n\t\tassertEquals(\"96.0.1054.36\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"11\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseLenovoTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I4QBMD\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.6241 SLBChan/30\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\n\t\tassertEquals(\"Lenovo\", ua.getBrowser().toString());\n\t\tassertEquals(\"7.0.0.6241\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseXiaoMiTest(){\n\t\tfinal String uaStr = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 XiaoMi/MiuiBrowser/16.0.18 swan-mibrowser\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\n\t\tassertEquals(\"MiuiBrowser\", ua.getBrowser().toString());\n\t\tassertEquals(\"16.0.18\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"534.24\", ua.getEngineVersion());\n\t\tassertEquals(\"Android\", ua.getOs().toString());\n\t\tassertEquals(\"11\", ua.getOsVersion());\n\t\tassertEquals(\"Android\", ua.getPlatform().toString());\n\t\tassertTrue(ua.isMobile());\n\t}\n\n\t@Test\n\tpublic void parseFromDeepinTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I50YGY\n\t\tfinal String uaStr = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Linux\", ua.getOs().toString());\n\t}\n\n\t@Test\n\tpublic void parseHarmonyUATest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Phone; OpenHarmony 4.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36  ArkWeb/4.1.6.1 Mobile \";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Harmony\", ua.getPlatform().toString());\n\t\tassertTrue(ua.getPlatform().isHarmony());\n\t\tassertEquals(\"Harmony\", ua.getOs().toString());\n\t\tassertEquals(\"4.1\", ua.getOsVersion());\n\t}\n\n\t@Test\n\tpublic void issueI60UOPTest() {\n\t\tfinal String uaStr = \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36 dingtalk-win/1.0.0 nw(0.14.7) DingTalk(6.5.40-Release.9059101) Mojo/1.0.0 Native AppType(release) Channel/201200\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"DingTalk-win\", ua.getBrowser().toString());\n\t\tassertEquals(\"6.5.40-Release.9059101\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua.getOs().toString());\n\t\tassertEquals(\"10.0\", ua.getOsVersion());\n\t\tassertEquals(\"Windows\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n\n\t/**\n\t * <a href=\"https://gitee.com/chinabugotech/hutool/issues/I7OTCU\">fix : issues I7OTCU </a>\n\t */\n\t@Test\n\tpublic void issuseI7OTCUTest() {\n\t\t// MAC Chrome 浏览器 ua\n\t\tfinal String uaStr = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\";\n\t\tfinal UserAgent ua = UserAgentUtil.parse(uaStr);\n\t\tassertEquals(\"Chrome\", ua.getBrowser().toString());\n\t\tassertEquals(\"114.0.0.0\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua.getEngineVersion());\n\t\tassertEquals(\"OSX\", ua.getOs().toString());\n\t\tassertEquals(\"10_15_7\", ua.getOsVersion());\n\t\tassertEquals(\"Mac\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\n\t\t// iphone Chrome 浏览器ua\n\t\tfinal String uaStr2 = \"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1\";\n\t\tfinal UserAgent ua2 = UserAgentUtil.parse(uaStr2);\n\t\tassertEquals(\"Chrome\", ua2.getBrowser().toString());\n\t\tassertEquals(\"56.0.2924.75\", ua2.getVersion());\n\t\tassertEquals(\"Webkit\", ua2.getEngine().toString());\n\t\tassertEquals(\"602.1.50\", ua2.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua2.getOs().toString());\n\t\tassertEquals(\"10_3\", ua2.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua2.getPlatform().toString());\n\t\tassertTrue(ua2.isMobile());\n\t}\n\n\t@Test\n\tpublic void issueI847JYTest() {\n\t\tfinal String s = \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) \" +\n\t\t\t\"Mobile/15E148 SP-engine/2.80.0 main%2F1.0 baiduboxapp/13.42.0.11 (Baidu; P2 17.0) NABar/1.0 themeUA=Them\";\n\t\tfinal UserAgent ua2 = UserAgentUtil.parse(s);\n\n\t\tassertEquals(\"Baidu\", ua2.getBrowser().toString());\n\t\tassertEquals(\"13.42.0.11\", ua2.getVersion());\n\t\tassertEquals(\"Webkit\", ua2.getEngine().toString());\n\t\tassertEquals(\"605.1.15\", ua2.getEngineVersion());\n\t\tassertEquals(\"iPhone\", ua2.getOs().toString());\n\t\tassertEquals(\"17_0\", ua2.getOsVersion());\n\t\tassertEquals(\"iPhone\", ua2.getPlatform().toString());\n\t\tassertTrue(ua2.isMobile());\n\t}\n\n\t@Test\n\tpublic void issueI8X5XQTest() {\n\t\tfinal String s = \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 \" +\n\t\t\t\"Safari/537.36 Core/1.94.218.400 QQBrowser/12.1.5496.400\";\n\t\tfinal UserAgent ua2 = UserAgentUtil.parse(s);\n\n\t\tassertEquals(\"QQBrowser\", ua2.getBrowser().toString());\n\t\tassertEquals(\"12.1.5496.400\", ua2.getVersion());\n\t\tassertEquals(\"Webkit\", ua2.getEngine().toString());\n\t\tassertEquals(\"537.36\", ua2.getEngineVersion());\n\t\tassertEquals(\"Windows 10 or Windows Server 2016\", ua2.getOs().toString());\n\t\tassertEquals(\"10.0\", ua2.getOsVersion());\n\t\tassertEquals(\"Windows\", ua2.getPlatform().toString());\n\t\tassertFalse(ua2.isMobile());\n\t}\n\n\t@Test\n\tpublic void issueIA74K2Test() {\n\t\tUserAgent ua = UserAgentUtil.parse(\n\t\t\t\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) MicroMessenger/6.8.0(0x16080000) MacWechat/3.8.7(0x13080710) Safari/605.1.15 NetType/WIFI\");\n\n\t\tassertEquals(\"MicroMessenger\", ua.getBrowser().toString());\n\t\tassertEquals(\"6.8.0\", ua.getVersion());\n\t\tassertEquals(\"Webkit\", ua.getEngine().toString());\n\t\tassertEquals(\"605.1.15\", ua.getEngineVersion());\n\t\tassertEquals(\"OSX\", ua.getOs().toString());\n\t\tassertEquals(\"10_15_7\", ua.getOsVersion());\n\t\tassertEquals(\"Mac\", ua.getPlatform().toString());\n\t\tassertFalse(ua.isMobile());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/webservice/JakartaSoapClientTest.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport jakarta.xml.soap.SOAPException;\nimport jakarta.xml.soap.SOAPMessage;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * SOAP相关单元测试\n *\n * @author looly\n *\n */\npublic class JakartaSoapClientTest {\n\n\t@Test\n\t@Disabled\n\tpublic void requestTest() {\n\t\tJakartaSoapClient client = JakartaSoapClient.create(\"http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx\")\n\t\t.setMethod(\"web:getCountryCityByIp\", \"http://WebXml.com.cn/\")\n\t\t.setCharset(CharsetUtil.CHARSET_GBK)\n\t\t.setParam(\"theIpAddress\", \"218.21.240.106\");\n\n\t\tConsole.log(client.getMsgStr(true));\n\n\t\tConsole.log(client.send(true));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void requestForMessageTest() throws SOAPException {\n\t\tJakartaSoapClient client = JakartaSoapClient.create(\"http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx\")\n\t\t\t\t.setMethod(\"web:getCountryCityByIp\", \"http://WebXml.com.cn/\")\n\t\t\t\t.setParam(\"theIpAddress\", \"218.21.240.106\");\n\n\t\tSOAPMessage message = client.sendForMessage();\n\t\tConsole.log(message.getSOAPBody().getTextContent());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/java/cn/hutool/http/webservice/SoapClientTest.java",
    "content": "package cn.hutool.http.webservice;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.CharsetUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.soap.SOAPException;\nimport javax.xml.soap.SOAPMessage;\n\n/**\n * SOAP相关单元测试\n *\n * @author looly\n *\n */\npublic class SoapClientTest {\n\n\t@Test\n\t@Disabled\n\tpublic void requestTest() {\n\t\tSoapClient client = SoapClient.create(\"http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx\")\n\t\t.setMethod(\"web:getCountryCityByIp\", \"http://WebXml.com.cn/\")\n\t\t.setCharset(CharsetUtil.CHARSET_GBK)\n\t\t.setParam(\"theIpAddress\", \"218.21.240.106\");\n\n\t\tConsole.log(client.getMsgStr(true));\n\n\t\tConsole.log(client.send(true));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void requestForMessageTest() throws SOAPException {\n\t\tSoapClient client = SoapClient.create(\"http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx\")\n\t\t\t\t.setMethod(\"web:getCountryCityByIp\", \"http://WebXml.com.cn/\")\n\t\t\t\t.setParam(\"theIpAddress\", \"218.21.240.106\");\n\n\t\tSOAPMessage message = client.sendForMessage();\n\t\tConsole.log(message.getSOAPBody().getTextContent());\n\t}\n}\n"
  },
  {
    "path": "hutool-http/src/test/resources/html/formForUpload.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>文件上传表单提交</title>\n</head>\n<body>\n    <h1>文件上传测试</h1>\n    <form action=\"file\" method=\"post\" enctype=\"multipart/form-data\">\n        文件：<input type=\"file\" name=\"file\"><br>\n        <input type=\"submit\" />\n    </form>\n</body>\n</html>"
  },
  {
    "path": "hutool-http/src/test/resources/html/index.html",
    "content": "<!DOCTYPE html>\n<html class=\"no-js\">\n\t<body>\n\t\t<h1>Hutool</h1>\n\t\t<h3>Simple Server of Hutool</h3>\n\t</body>\n</html>"
  },
  {
    "path": "hutool-json/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-json</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool JSON封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.json</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.mutable.MutablePair;\nimport cn.hutool.core.map.CaseInsensitiveLinkedMap;\nimport cn.hutool.core.map.CaseInsensitiveTreeMap;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.math.BigDecimal;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.TreeMap;\n\n/**\n * 内部JSON工具类，仅用于JSON内部使用\n *\n * @author Looly\n */\npublic final class InternalJSONUtil {\n\n\tprivate InternalJSONUtil() {\n\t}\n\n\t/**\n\t * 如果对象是Number 且是 NaN or infinite，将抛出异常\n\t *\n\t * @param obj 被检查的对象\n\t * @return 检测后的值\n\t * @throws JSONException If o is a non-finite number.\n\t */\n\tstatic Object testValidity(Object obj) throws JSONException {\n\t\tif (false == ObjectUtil.isValidIfNumber(obj)) {\n\t\t\tthrow new JSONException(\"JSON does not allow non-finite numbers.\");\n\t\t}\n\t\treturn obj;\n\t}\n\n\t/**\n\t * 值转为String，用于JSON中。规则为：\n\t * <ul>\n\t *     <li>对象如果实现了{@link JSONString}接口，调用{@link JSONString#toJSONString()}方法</li>\n\t *     <li>对象如果实现了{@link JSONString}接口，调用{@link JSONString#toJSONString()}方法</li>\n\t *     <li>对象如果是数组或Collection，包装为{@link JSONArray}</li>\n\t *     <li>对象如果是Map，包装为{@link JSONObject}</li>\n\t *     <li>对象如果是数字，使用{@link NumberUtil#toStr(Number)}转换为字符串</li>\n\t *     <li>其他情况调用toString并使用双引号包装</li>\n\t * </ul>\n\t *\n\t * @param value 需要转为字符串的对象\n\t * @return 字符串\n\t * @throws JSONException If the value is or contains an invalid number.\n\t */\n\tstatic String valueToString(Object value) throws JSONException {\n\t\tif (value == null || value instanceof JSONNull) {\n\t\t\treturn JSONNull.NULL.toString();\n\t\t}\n\t\tif (value instanceof JSONString) {\n\t\t\ttry {\n\t\t\t\treturn ((JSONString) value).toJSONString();\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new JSONException(e);\n\t\t\t}\n\t\t} else if (value instanceof Number) {\n\t\t\treturn NumberUtil.toStr((Number) value);\n\t\t} else if (value instanceof Boolean || value instanceof JSONObject || value instanceof JSONArray) {\n\t\t\treturn value.toString();\n\t\t} else if (value instanceof Map) {\n\t\t\tMap<?, ?> map = (Map<?, ?>) value;\n\t\t\treturn new JSONObject(map).toString();\n\t\t} else if (value instanceof Collection) {\n\t\t\tCollection<?> coll = (Collection<?>) value;\n\t\t\treturn new JSONArray(coll).toString();\n\t\t} else if (ArrayUtil.isArray(value)) {\n\t\t\treturn new JSONArray(value).toString();\n\t\t} else {\n\t\t\treturn JSONUtil.quote(value.toString());\n\t\t}\n\t}\n\n\t/**\n\t * 尝试转换字符串为number, boolean, or null，无法转换返回String\n\t *\n\t * @param string A String.\n\t * @return A simple JSON value.\n\t */\n\tpublic static Object stringToValue(String string) {\n\t\t// null处理\n\t\tif (StrUtil.isEmpty(string) || StrUtil.NULL.equalsIgnoreCase(string)) {\n\t\t\treturn JSONNull.NULL;\n\t\t}\n\n\t\t// boolean处理\n\t\tif (\"true\".equalsIgnoreCase(string)) {\n\t\t\treturn Boolean.TRUE;\n\t\t}\n\t\tif (\"false\".equalsIgnoreCase(string)) {\n\t\t\treturn Boolean.FALSE;\n\t\t}\n\n\t\t// Number处理\n\t\tchar b = string.charAt(0);\n\t\tif ((b >= '0' && b <= '9') || b == '-') {\n\t\t\ttry {\n\t\t\t\tif (StrUtil.containsAnyIgnoreCase(string, \".\", \"e\")) {\n\t\t\t\t\t// pr#192@Gitee，Double会出现小数精度丢失问题，此处使用BigDecimal\n\t\t\t\t\treturn new BigDecimal(string);\n\t\t\t\t} else {\n\t\t\t\t\tfinal long myLong = Long.parseLong(string);\n\t\t\t\t\tif (string.equals(Long.toString(myLong))) {\n\t\t\t\t\t\tif (myLong == (int) myLong) {\n\t\t\t\t\t\t\treturn (int) myLong;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn myLong;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (Exception ignore) {\n\t\t\t}\n\t\t}\n\n\t\t// 其它情况返回原String值下\n\t\treturn string;\n\t}\n\n\t/**\n\t * 将Property的键转化为JSON形式<br>\n\t * 用于识别类似于：com.luxiaolei.package.hutool这类用点隔开的键<br>\n\t * 注意：是否允许重复键，取决于JSONObject配置\n\t *\n\t * @param jsonObject JSONObject\n\t * @param key        键\n\t * @param value      值\n\t * @return JSONObject\n\t */\n\tstatic JSONObject propertyPut(JSONObject jsonObject, Object key, Object value, Filter<MutablePair<String, Object>> filter) {\n\t\tfinal String[] path = StrUtil.splitToArray(Convert.toStr(key), CharUtil.DOT);\n\t\tfinal int last = path.length - 1;\n\t\tJSONObject target = jsonObject;\n\t\tfor (int i = 0; i < last; i += 1) {\n\t\t\tfinal String segment = path[i];\n\t\t\tJSONObject nextTarget = target.getJSONObject(segment);\n\t\t\tif (nextTarget == null) {\n\t\t\t\tnextTarget = new JSONObject(target.getConfig());\n\t\t\t\ttarget.set(segment, nextTarget, filter, target.getConfig().isCheckDuplicate());\n\t\t\t}\n\t\t\ttarget = nextTarget;\n\t\t}\n\t\ttarget.set(path[last], value, filter, target.getConfig().isCheckDuplicate());\n\t\treturn jsonObject;\n\t}\n\n\t/**\n\t * 默认情况下是否忽略null值的策略选择，以下对象不忽略null值，其它对象忽略：\n\t *\n\t * <pre>\n\t *     1. CharSequence\n\t *     2. JSONTokener\n\t *     3. Map\n\t * </pre>\n\t *\n\t * @param obj 需要检查的对象\n\t * @return 是否忽略null值\n\t * @since 4.3.1\n\t */\n\tstatic boolean defaultIgnoreNullValue(Object obj) {\n\t\treturn (false == (obj instanceof CharSequence))//\n\t\t\t\t&& (false == (obj instanceof JSONTokener))//\n\t\t\t\t&& (false == (obj instanceof Map));\n\t}\n\n\t/**\n\t * 将{@link JSONConfig}参数转换为Bean拷贝所用的{@link CopyOptions}\n\t *\n\t * @param config {@link JSONConfig}\n\t * @return {@link CopyOptions}\n\t * @since 5.8.0\n\t */\n\tstatic CopyOptions toCopyOptions(JSONConfig config) {\n\t\treturn toCopyOptions(config, null);\n\t}\n\n\t/**\n\t * 将 {@link JSONConfig} 的参数和过滤器 {@link Filter}转换为 Bean 拷贝所需的 {@link CopyOptions}\n\t *\n\t * @param config {@link JSONConfig}\n\t * @param filter {@link Filter}\n\t * @return {@link CopyOptions}\n\t * @since 5.8.43\n\t */\n\tstatic CopyOptions toCopyOptions(JSONConfig config, Filter<MutablePair<String, Object>> filter) {\n\t\tCopyOptions copyOptions = CopyOptions.create()\n\t\t\t.setIgnoreCase(config.isIgnoreCase())\n\t\t\t.setIgnoreError(config.isIgnoreError())\n\t\t\t.setIgnoreNullValue(config.isIgnoreNullValue())\n\t\t\t.setTransientSupport(config.isTransientSupport());\n\n\t\tif (filter != null) {\n\t\t\tcopyOptions.setPropertiesFilter((field, value) -> {\n\t\t\t\tMutablePair<String, Object> pair = new MutablePair<>(field.getName(), value);\n\t\t\t\treturn filter.accept(pair);\n\t\t\t});\n\t\t}\n\n\t\treturn copyOptions;\n\t}\n\n\t/**\n\t * 根据配置创建对应的原始Map\n\t *\n\t * @param capacity 初始大小\n\t * @param config   JSON配置项，{@code null}则使用默认配置\n\t * @return Map\n\t */\n\tstatic Map<String, Object> createRawMap(int capacity, JSONConfig config) {\n\t\tfinal Map<String, Object> rawHashMap;\n\t\tif (null == config) {\n\t\t\tconfig = JSONConfig.create();\n\t\t}\n\t\tfinal Comparator<String> keyComparator = config.getKeyComparator();\n\t\tif (config.isIgnoreCase()) {\n\t\t\tif (null != keyComparator) {\n\t\t\t\trawHashMap = new CaseInsensitiveTreeMap<>(keyComparator);\n\t\t\t} else {\n\t\t\t\trawHashMap = new CaseInsensitiveLinkedMap<>(capacity);\n\t\t\t}\n\t\t} else {\n\t\t\tif (null != keyComparator) {\n\t\t\t\trawHashMap = new TreeMap<>(keyComparator);\n\t\t\t} else {\n\t\t\t\trawHashMap = new LinkedHashMap<>(capacity);\n\t\t\t}\n\t\t}\n\t\treturn rawHashMap;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSON.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.bean.copier.IJSONTypeConverter;\nimport cn.hutool.core.lang.TypeReference;\n\nimport java.io.Serializable;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\n\n/**\n * JSON接口\n *\n * @author Looly\n */\npublic interface JSON extends Cloneable, Serializable, IJSONTypeConverter {\n\n\t/**\n\t * 获取JSON配置\n\t *\n\t * @return {@link JSONConfig}\n\t * @since 5.8.6\n\t */\n\tJSONConfig getConfig();\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param expression 表达式\n\t * @return 对象\n\t * @see BeanPath#get(Object)\n\t * @since 4.0.6\n\t */\n\tObject getByPath(String expression);\n\n\t/**\n\t * 设置表达式指定位置（或filed对应）的值<br>\n\t * 若表达式指向一个JSONArray则设置其坐标对应位置的值，若指向JSONObject则put对应key的值<br>\n\t * 注意：如果为JSONArray，设置值下标小于其长度，将替换原有值，否则追加新值<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param expression 表达式\n\t * @param value      值\n\t */\n\tvoid putByPath(String expression, Object value);\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t * <p>\n\t * 获取表达式对应值后转换为对应类型的值\n\t *\n\t * @param <T>        返回值类型\n\t * @param expression 表达式\n\t * @param resultType 返回值类型\n\t * @return 对象\n\t * @see BeanPath#get(Object)\n\t * @since 4.0.6\n\t */\n\t<T> T getByPath(String expression, Class<T> resultType);\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t * <p>\n\t * 获取表达式对应值后转换为对应类型的值\n\t *\n\t * @param expression 表达式\n\t * @param targetType 返回值类型\n\t * @param <T>        获取对象类型\n\t * @return 对象\n\t * @see BeanPath#get(Object)\n\t * @since 5.8.34\n\t */\n\t<T> T getByPath(String expression, TypeReference<T> targetType);\n\n\t/**\n\t * 格式化打印JSON，缩进为4个空格\n\t *\n\t * @return 格式化后的JSON字符串\n\t * @throws JSONException 包含非法数抛出此异常\n\t * @since 3.0.9\n\t */\n\tdefault String toStringPretty() throws JSONException {\n\t\treturn this.toJSONString(4);\n\t}\n\n\t/**\n\t * 格式化输出JSON字符串\n\t *\n\t * @param indentFactor 每层缩进空格数\n\t * @return JSON字符串\n\t * @throws JSONException 包含非法数抛出此异常\n\t */\n\tdefault String toJSONString(int indentFactor) throws JSONException {\n\t\tfinal StringWriter sw = new StringWriter();\n\t\treturn this.write(sw, indentFactor, 0).toString();\n\t}\n\n\t/**\n\t * 将JSON内容写入Writer，无缩进<br>\n\t * Warning: This method assumes that the data structure is acyclical.\n\t *\n\t * @param writer Writer\n\t * @return Writer\n\t * @throws JSONException JSON相关异常\n\t */\n\tdefault Writer write(Writer writer) throws JSONException {\n\t\treturn this.write(writer, 0, 0);\n\t}\n\n\t/**\n\t * 将JSON内容写入Writer<br>\n\t * Warning: This method assumes that the data structure is acyclical.\n\t *\n\t * @param writer       writer\n\t * @param indentFactor 缩进因子，定义每一级别增加的缩进量\n\t * @param indent       本级别缩进量\n\t * @return Writer\n\t * @throws JSONException JSON相关异常\n\t */\n\tWriter write(Writer writer, int indentFactor, int indent) throws JSONException;\n\n\t/**\n\t * 转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>   Bean类型\n\t * @param clazz 实体类\n\t * @return 实体类对象\n\t */\n\tdefault <T> T toBean(Class<T> clazz) {\n\t\treturn toBean((Type) clazz);\n\t}\n\n\t/**\n\t * 转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>       Bean类型\n\t * @param reference {@link TypeReference}类型参考子类，可以获取其泛型参数中的Type类型\n\t * @return 实体类对象\n\t * @since 4.2.2\n\t */\n\tdefault <T> T toBean(TypeReference<T> reference) {\n\t\treturn toBean(reference.getType());\n\t}\n\n\t/**\n\t * 转为实体类对象\n\t *\n\t * @param <T>  Bean类型\n\t * @param type {@link Type}\n\t * @return 实体类对象\n\t * @since 3.0.8\n\t */\n\tdefault <T> T toBean(Type type) {\n\t\treturn JSONConverter.jsonConvert(type, this, getConfig());\n\t}\n\n\t/**\n\t * 转为实体类对象\n\t *\n\t * @param <T>         Bean类型\n\t * @param type        {@link Type}\n\t * @param ignoreError 是否忽略转换错误\n\t * @return 实体类对象\n\t * @since 4.3.2\n\t * @deprecated 请使用 {@link #toBean(Type)}, ignoreError在JSONConfig中生效\n\t */\n\t@Deprecated\n\tdefault <T> T toBean(Type type, boolean ignoreError) {\n\t\treturn JSONConverter.jsonConvert(type, this, JSONConfig.create().setIgnoreError(ignoreError));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONArray.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.lang.Validator;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutableObj;\nimport cn.hutool.core.lang.mutable.MutablePair;\nimport cn.hutool.core.text.StrJoiner;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.json.serialize.JSONWriter;\n\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.ListIterator;\nimport java.util.RandomAccess;\n\n/**\n * JSON数组<br>\n * JSON数组是表示中括号括住的数据表现形式<br>\n * 对应的JSON字符串格格式例如:\n *\n * <pre>\n * [\"a\", \"b\", \"c\", 12]\n * </pre>\n *\n * @author looly\n */\npublic class JSONArray implements JSON, JSONGetter<Integer>, List<Object>, RandomAccess {\n\tprivate static final long serialVersionUID = 2664900568717612292L;\n\n\t/**\n\t * 默认初始大小\n\t */\n\tpublic static final int DEFAULT_CAPACITY = 10;\n\n\t/**\n\t * 持有原始数据的List\n\t */\n\tprivate List<Object> rawList;\n\t/**\n\t * 配置项\n\t */\n\tprivate final JSONConfig config;\n\n\t// region Constructors\n\n\t/**\n\t * 构造<br>\n\t * 默认使用{@link ArrayList} 实现\n\t */\n\tpublic JSONArray() {\n\t\tthis(DEFAULT_CAPACITY);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 默认使用{@link ArrayList} 实现\n\t *\n\t * @param initialCapacity 初始大小\n\t * @since 3.2.2\n\t */\n\tpublic JSONArray(int initialCapacity) {\n\t\tthis(initialCapacity, JSONConfig.create());\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 默认使用{@link ArrayList} 实现\n\t *\n\t * @param config JSON配置项\n\t * @since 4.6.5\n\t */\n\tpublic JSONArray(JSONConfig config) {\n\t\tthis(DEFAULT_CAPACITY, config);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 默认使用{@link ArrayList} 实现\n\t *\n\t * @param initialCapacity 初始大小\n\t * @param config          JSON配置项\n\t * @since 4.1.19\n\t */\n\tpublic JSONArray(int initialCapacity, JSONConfig config) {\n\t\tthis.rawList = new ArrayList<>(initialCapacity);\n\t\tthis.config = ObjectUtil.defaultIfNull(config, JSONConfig::create);\n\t}\n\n\t/**\n\t * 从对象构造，忽略{@code null}的值<br>\n\t * 支持以下类型的参数：\n\t *\n\t * <pre>\n\t * 1. 数组\n\t * 2. {@link Iterable}对象\n\t * 3. JSON数组字符串\n\t * </pre>\n\t *\n\t * @param object 数组或集合或JSON数组字符串\n\t * @throws JSONException 非数组或集合\n\t */\n\tpublic JSONArray(Object object) throws JSONException {\n\t\tthis(object, true);\n\t}\n\n\t/**\n\t * 从对象构造<br>\n\t * 支持以下类型的参数：\n\t *\n\t * <pre>\n\t * 1. 数组\n\t * 2. {@link Iterable}对象\n\t * 3. JSON数组字符串\n\t * </pre>\n\t *\n\t * @param object          数组或集合或JSON数组字符串\n\t * @param ignoreNullValue 是否忽略空值\n\t * @throws JSONException 非数组或集合\n\t */\n\tpublic JSONArray(Object object, boolean ignoreNullValue) throws JSONException {\n\t\tthis(object, JSONConfig.create().setIgnoreNullValue(ignoreNullValue));\n\t}\n\n\t/**\n\t * 从对象构造<br>\n\t * 支持以下类型的参数：\n\t *\n\t * <pre>\n\t * 1. 数组\n\t * 2. {@link Iterable}对象\n\t * 3. JSON数组字符串\n\t * </pre>\n\t *\n\t * @param object     数组或集合或JSON数组字符串\n\t * @param jsonConfig JSON选项\n\t * @throws JSONException 非数组或集合\n\t * @since 4.6.5\n\t */\n\tpublic JSONArray(Object object, JSONConfig jsonConfig) throws JSONException {\n\t\tthis(object, jsonConfig, null);\n\t}\n\n\t/**\n\t * 从对象构造<br>\n\t * 支持以下类型的参数：\n\t *\n\t * <pre>\n\t * 1. 数组\n\t * 2. {@link Iterable}对象\n\t * 3. JSON数组字符串\n\t * </pre>\n\t *\n\t * @param object     数组或集合或JSON数组字符串\n\t * @param jsonConfig JSON选项\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null}表示不过滤\n\t * @throws JSONException 非数组或集合\n\t * @since 5.8.0\n\t */\n\tpublic JSONArray(Object object, JSONConfig jsonConfig, Filter<Mutable<Object>> filter) throws JSONException {\n\t\tthis(DEFAULT_CAPACITY, jsonConfig);\n\t\tObjectMapper.of(object).map(this, filter);\n\t}\n\t// endregion\n\n\t@Override\n\tpublic JSONConfig getConfig() {\n\t\treturn this.config;\n\t}\n\n\t/**\n\t * 设置转为字符串时的日期格式，默认为时间戳（null值）\n\t *\n\t * @param format 格式，null表示使用时间戳\n\t * @return this\n\t * @since 4.1.19\n\t */\n\tpublic JSONArray setDateFormat(String format) {\n\t\tthis.config.setDateFormat(format);\n\t\treturn this;\n\t}\n\n\t/**\n\t * JSONArray转为以{@code separator}为分界符的字符串\n\t *\n\t * @param separator 分界符\n\t * @return a string.\n\t * @throws JSONException If the array contains an invalid number.\n\t */\n\tpublic String join(String separator) throws JSONException {\n\t\treturn StrJoiner.of(separator)\n\t\t\t.append(this, InternalJSONUtil::valueToString).toString();\n\t}\n\n\t@Override\n\tpublic Object get(int index) {\n\t\treturn this.rawList.get(index);\n\t}\n\n\t@Override\n\tpublic Object getObj(Integer index, Object defaultValue) {\n\t\treturn (index < 0 || index >= this.size()) ? defaultValue : this.rawList.get(index);\n\t}\n\n\t@Override\n\tpublic Object getByPath(String expression) {\n\t\treturn BeanPath.create(expression).get(this);\n\t}\n\n\t@Override\n\tpublic <T> T getByPath(String expression, Class<T> resultType) {\n\t\treturn JSONConverter.jsonConvert(resultType, getByPath(expression), getConfig());\n\t}\n\n\t@Override\n\tpublic <T> T getByPath(String expression, TypeReference<T> targetType) {\n\t\treturn JSONConverter.jsonConvert(targetType, getByPath(expression), getConfig());\n\t}\n\n\t@Override\n\tpublic void putByPath(String expression, Object value) {\n\t\tBeanPath.create(expression).set(this, value);\n\t}\n\n\t/**\n\t * Append an object value. This increases the array's length by one. <br>\n\t * 加入元素，数组长度+1，等同于 {@link JSONArray#add(Object)}\n\t *\n\t * @param value 值，可以是： Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONNull.NULL。\n\t * @return this.\n\t * @see #set(Object)\n\t */\n\tpublic JSONArray put(Object value) {\n\t\treturn set(value);\n\t}\n\n\t/**\n\t * Append an object value. This increases the array's length by one. <br>\n\t * 加入元素，数组长度+1，等同于 {@link JSONArray#add(Object)}\n\t *\n\t * @param value 值，可以是： Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONNull.NULL。\n\t * @return this.\n\t * @since 5.2.5\n\t */\n\tpublic JSONArray set(Object value) {\n\t\tthis.add(value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 加入或者替换JSONArray中指定Index的值，如果index大于JSONArray的长度，将在指定index设置值，之前的位置填充JSONNull.Null\n\t *\n\t * @param index 位置\n\t * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return this.\n\t * @throws JSONException index &lt; 0 或者非有限的数字\n\t * @see #set(int, Object)\n\t */\n\tpublic JSONArray put(int index, Object value) throws JSONException {\n\t\tthis.set(index, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据给定名列表，与其位置对应的值组成JSONObject\n\t *\n\t * @param names 名列表，位置与JSONArray中的值位置对应\n\t * @return A JSONObject，无名或值返回null\n\t * @throws JSONException 如果任何一个名为null\n\t */\n\tpublic JSONObject toJSONObject(JSONArray names) throws JSONException {\n\t\tif (names == null || names.size() == 0 || this.size() == 0) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal JSONObject jo = new JSONObject(this.config);\n\t\tfor (int i = 0; i < names.size(); i += 1) {\n\t\t\tjo.set(names.getStr(i), this.getObj(i));\n\t\t}\n\t\treturn jo;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + ((rawList == null) ? 0 : rawList.hashCode());\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal JSONArray other = (JSONArray) obj;\n\t\tif (rawList == null) {\n\t\t\treturn other.rawList == null;\n\t\t} else {\n\t\t\treturn rawList.equals(other.rawList);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Iterator<Object> iterator() {\n\t\treturn rawList.iterator();\n\t}\n\n\t/**\n\t * 当此JSON列表的每个元素都是一个JSONObject时，可以调用此方法返回一个Iterable，便于使用foreach语法遍历\n\t *\n\t * @return Iterable\n\t * @since 4.0.12\n\t */\n\tpublic Iterable<JSONObject> jsonIter() {\n\t\treturn new JSONObjectIter(iterator());\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn rawList.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn rawList.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean contains(Object o) {\n\t\treturn rawList.contains(o);\n\t}\n\n\t@Override\n\tpublic Object[] toArray() {\n\t\treturn rawList.toArray();\n\t}\n\n\t@Override\n\t@SuppressWarnings({\"unchecked\"})\n\tpublic <T> T[] toArray(T[] a) {\n\t\treturn (T[]) JSONConverter.toArray(this, a.getClass().getComponentType());\n\t}\n\n\t@Override\n\tpublic boolean add(Object e) {\n\t\treturn addRaw(JSONUtil.wrap(e, this.config), null);\n\t}\n\n\t@Override\n\tpublic Object remove(int index) {\n\t\treturn index >= 0 && index < this.size() ? this.rawList.remove(index) : null;\n\t}\n\n\t@Override\n\tpublic boolean remove(Object o) {\n\t\treturn rawList.remove(o);\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean containsAll(Collection<?> c) {\n\t\treturn rawList.containsAll(c);\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean addAll(Collection<?> c) {\n\t\tif (CollUtil.isEmpty(c)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (Object obj : c) {\n\t\t\tthis.add(obj);\n\t\t}\n\t\treturn true;\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean addAll(int index, Collection<?> c) {\n\t\tif (CollUtil.isEmpty(c)) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal ArrayList<Object> list = new ArrayList<>(c.size());\n\t\tfor (Object object : c) {\n\t\t\tlist.add(JSONUtil.wrap(object, this.config));\n\t\t}\n\t\treturn rawList.addAll(index, list);\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean removeAll(Collection<?> c) {\n\t\treturn this.rawList.removeAll(c);\n\t}\n\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic boolean retainAll(Collection<?> c) {\n\t\treturn this.rawList.retainAll(c);\n\t}\n\n\t@Override\n\tpublic void clear() {\n\t\tthis.rawList.clear();\n\n\t}\n\n\t/**\n\t * 加入或者替换JSONArray中指定Index的值，如果index大于JSONArray的长度，将在指定index设置值，之前的位置填充JSONNull.Null\n\t *\n\t * @param index   位置\n\t * @param element 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return 替换的值，即之前的值\n\t */\n\t@Override\n\tpublic Object set(int index, Object element) {\n\t\treturn set(index, element, null);\n\t}\n\n\t/**\n\t * 加入或者替换JSONArray中指定Index的值，如果index大于JSONArray的长度，将在指定index设置值，之前的位置填充JSONNull.Null\n\t *\n\t * @param index   位置\n\t * @param element 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @param filter  过滤器，可以修改值，key（index）无法修改\n\t * @return 替换的值，即之前的值\n\t * @since 5.8.0\n\t */\n\tpublic Object set(int index, Object element, Filter<MutablePair<Integer, Object>> filter) {\n\t\t// 添加前置过滤，通过MutablePair实现过滤、修改键值对等\n\t\tif (null != filter) {\n\t\t\tfinal MutablePair<Integer, Object> pair = new MutablePair<>(index, element);\n\t\t\tif (filter.accept(pair)) {\n\t\t\t\t// 使用修改后的值\n\t\t\t\telement = pair.getValue();\n\t\t\t}\n\t\t}\n\n\t\tif (index >= size()) {\n\t\t\tadd(index, element);\n\t\t\treturn null;\n\t\t}\n\t\treturn this.rawList.set(index, JSONUtil.wrap(element, this.config));\n\t}\n\n\t@Override\n\tpublic void add(int index, Object element) {\n\t\tfinal boolean ignoreNullValue = config.isIgnoreNullValue();\n\t\tif (null == element && ignoreNullValue) {\n\t\t\treturn;\n\t\t}\n\t\tif (index < 0) {\n\t\t\tthrow new JSONException(\"JSONArray[{}] not found.\", index);\n\t\t}\n\t\tif (index < this.size()) {\n\t\t\tInternalJSONUtil.testValidity(element);\n\t\t\tthis.rawList.add(index, JSONUtil.wrap(element, this.config));\n\t\t} else {\n\t\t\tif(false == ignoreNullValue){\n\t\t\t\t// issue#3286, 增加安全检查，最多增加10倍\n\t\t\t\tValidator.checkIndexLimit(index, this.size());\n\t\t\t\twhile (index != this.size()) {\n\t\t\t\t\tthis.add(JSONNull.NULL);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.add(element);\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic int indexOf(Object o) {\n\t\treturn this.rawList.indexOf(o);\n\t}\n\n\t@Override\n\tpublic int lastIndexOf(Object o) {\n\t\treturn this.rawList.lastIndexOf(o);\n\t}\n\n\t@Override\n\tpublic ListIterator<Object> listIterator() {\n\t\treturn this.rawList.listIterator();\n\t}\n\n\t@Override\n\tpublic ListIterator<Object> listIterator(int index) {\n\t\treturn this.rawList.listIterator(index);\n\t}\n\n\t@Override\n\tpublic List<Object> subList(int fromIndex, int toIndex) {\n\t\treturn this.rawList.subList(fromIndex, toIndex);\n\t}\n\n\t/**\n\t * 转为Bean数组\n\t *\n\t * @param arrayClass 数组元素类型\n\t * @return 实体类对象\n\t */\n\tpublic Object toArray(Class<?> arrayClass) {\n\t\treturn JSONConverter.toArray(this, arrayClass);\n\t}\n\n\t/**\n\t * 转为{@link ArrayList}\n\t *\n\t * @param <T>         元素类型\n\t * @param elementType 元素类型\n\t * @return {@link ArrayList}\n\t * @since 3.0.8\n\t */\n\tpublic <T> List<T> toList(Class<T> elementType) {\n\t\treturn JSONConverter.toList(this, elementType);\n\t}\n\n\t/**\n\t * 转为JSON字符串，无缩进\n\t *\n\t * @return JSONArray字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn this.toJSONString(0);\n\t}\n\n\t/**\n\t * 返回JSON字符串<br>\n\t * 支持过滤器，即选择哪些字段或值不写出\n\t *\n\t * @param indentFactor 每层缩进空格数\n\t * @param filter       过滤器，可以修改值，key（index）无法修改\n\t * @return JSON字符串\n\t * @since 5.7.15\n\t */\n\tpublic String toJSONString(int indentFactor, Filter<MutablePair<Object, Object>> filter) {\n\t\tfinal StringWriter sw = new StringWriter();\n\t\tsynchronized (sw.getBuffer()) {\n\t\t\treturn this.write(sw, indentFactor, 0, filter).toString();\n\t\t}\n\t}\n\n\t@Override\n\tpublic Writer write(Writer writer, int indentFactor, int indent) throws JSONException {\n\t\treturn write(writer, indentFactor, indent, null);\n\t}\n\n\t/**\n\t * 将JSON内容写入Writer<br>\n\t * 支持过滤器，即选择哪些字段或值不写出\n\t *\n\t * @param writer       writer\n\t * @param indentFactor 缩进因子，定义每一级别增加的缩进量\n\t * @param indent       本级别缩进量\n\t * @param filter       过滤器，可以修改值，key（index）无法修改\n\t * @return Writer\n\t * @throws JSONException JSON相关异常\n\t * @since 5.7.15\n\t */\n\tpublic Writer write(Writer writer, int indentFactor, int indent, Filter<MutablePair<Object, Object>> filter) throws JSONException {\n\t\tfinal JSONWriter jsonWriter = JSONWriter.of(writer, indentFactor, indent, config).beginArray();\n\n\t\tCollUtil.forEach(this, (value, index) -> jsonWriter.writeField(new MutablePair<>(index, value), filter));\n\t\tjsonWriter.end();\n\t\t// 此处不关闭Writer，考虑writer后续还需要填内容\n\t\treturn writer;\n\t}\n\n\t@Override\n\tpublic Object clone() throws CloneNotSupportedException {\n\t\tfinal JSONArray clone = (JSONArray) super.clone();\n\t\tclone.rawList = ObjectUtil.clone(this.rawList);\n\t\treturn clone;\n\t}\n\n\t/**\n\t * 原始添加，添加的对象不做任何处理\n\t *\n\t * @param obj    添加的对象\n\t * @param filter 键值对过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null}表示不过滤\n\t * @return 是否加入成功\n\t * @since 5.8.0\n\t */\n\tprotected boolean addRaw(Object obj, Filter<Mutable<Object>> filter) {\n\t\t// 添加前置过滤，通过MutablePair实现过滤、修改键值对等\n\t\tif (null != filter) {\n\t\t\tfinal Mutable<Object> mutable = new MutableObj<>(obj);\n\t\t\tif (filter.accept(mutable)) {\n\t\t\t\t// 使用修改后的值\n\t\t\t\tobj = mutable.get();\n\t\t\t}else{\n\t\t\t\t// 键值对被过滤\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// issue#3759\n\t\tfinal boolean ignoreNullValue = this.config.isIgnoreNullValue();\n\t\tif (ObjectUtil.isNull(obj) && ignoreNullValue) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn this.rawList.add(obj);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONBeanParser.java",
    "content": "package cn.hutool.json;\n\n/**\n * 实现此接口的类可以通过实现{@code parse(value)}方法来将JSON中的值解析为此对象的值\n *\n * @author Looly\n * @since 5.7.8\n * @param <T> 参数类型\n */\npublic interface JSONBeanParser<T> {\n\n\t/**\n\t * value转Bean<br>\n\t * 通过实现此接口，将JSON中的值填充到当前对象的字段值中，即对象自行实现JSON反序列化逻辑\n\t *\n\t * @param value 被解析的对象类型，可能为JSON或者普通String、Number等\n\t */\n\tvoid parse(T value);\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONConfig.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.comparator.CompareUtil;\n\nimport java.io.Serializable;\nimport java.util.Comparator;\n\n/**\n * JSON配置项\n *\n * @author looly\n * @since 4.1.19\n */\npublic class JSONConfig implements Serializable {\n\tprivate static final long serialVersionUID = 119730355204738278L;\n\n\t/**\n\t * 键排序规则，{@code null}表示不排序，不排序情况下，按照加入顺序排序\n\t */\n\tprivate Comparator<String> keyComparator;\n\t/**\n\t * 是否忽略转换过程中的异常\n\t */\n\tprivate boolean ignoreError;\n\t/**\n\t * 是否忽略键的大小写\n\t */\n\tprivate boolean ignoreCase;\n\t/**\n\t * 日期格式，null表示默认的时间戳\n\t */\n\tprivate String dateFormat;\n\t/**\n\t * 是否忽略null值\n\t */\n\tprivate boolean ignoreNullValue = true;\n\t/**\n\t * 是否支持transient关键字修饰和@Transient注解，如果支持，被修饰的字段或方法对应的字段将被忽略。\n\t */\n\tprivate boolean transientSupport = true;\n\n\t/**\n\t * 是否去除末尾多余0，例如如果为true,5.0返回5\n\t */\n\tprivate boolean stripTrailingZeros = true;\n\n\t/**\n\t * 是否检查重复key\n\t */\n\tprivate boolean checkDuplicate;\n\n\t/**\n\t * 是否将Long值写出为字符串类型\n\t * @since 5.8.28\n\t */\n\tprivate boolean writeLongAsString;\n\n\t/**\n\t * 创建默认的配置项\n\t *\n\t * @return JSONConfig\n\t */\n\tpublic static JSONConfig create() {\n\t\treturn new JSONConfig();\n\t}\n\n\t/**\n\t * 是否有序，顺序按照加入顺序排序，只针对JSONObject有效\n\t *\n\t * @return 是否有序\n\t * @deprecated 始终返回 {@code true}\n\t */\n\t@Deprecated\n\tpublic boolean isOrder() {\n\t\treturn true;\n\t}\n\n\t/**\n\t * 设置是否有序，顺序按照加入顺序排序，只针对JSONObject有效\n\t *\n\t * @param order 是否有序\n\t * @return this\n\t * @deprecated 始终有序，无需设置\n\t */\n\t@SuppressWarnings(\"unused\")\n\t@Deprecated\n\tpublic JSONConfig setOrder(boolean order) {\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取键排序规则<br>\n\t * 键排序规则，{@code null}表示不排序，不排序情况下，按照加入顺序排序\n\t *\n\t * @return 键排序规则\n\t * @since 5.7.21\n\t */\n\tpublic Comparator<String> getKeyComparator() {\n\t\treturn this.keyComparator;\n\t}\n\n\t/**\n\t * 设置自然排序，即按照字母顺序排序\n\t *\n\t * @return this\n\t * @since 5.7.21\n\t */\n\tpublic JSONConfig setNatureKeyComparator() {\n\t\treturn setKeyComparator(CompareUtil.naturalComparator());\n\t}\n\n\t/**\n\t * 设置键排序规则<br>\n\t * 键排序规则，{@code null}表示不排序，不排序情况下，按照加入顺序排序\n\t *\n\t * @param keyComparator 键排序规则\n\t * @return this\n\t * @since 5.7.21\n\t */\n\tpublic JSONConfig setKeyComparator(Comparator<String> keyComparator) {\n\t\tthis.keyComparator = keyComparator;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否忽略转换过程中的异常\n\t *\n\t * @return 是否忽略转换过程中的异常\n\t */\n\tpublic boolean isIgnoreError() {\n\t\treturn ignoreError;\n\t}\n\n\t/**\n\t * 设置是否忽略转换过程中的异常\n\t *\n\t * @param ignoreError 是否忽略转换过程中的异常\n\t * @return this\n\t */\n\tpublic JSONConfig setIgnoreError(boolean ignoreError) {\n\t\tthis.ignoreError = ignoreError;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否忽略键的大小写\n\t *\n\t * @return 是否忽略键的大小写\n\t */\n\tpublic boolean isIgnoreCase() {\n\t\treturn ignoreCase;\n\t}\n\n\t/**\n\t * 设置是否忽略键的大小写\n\t *\n\t * @param ignoreCase 是否忽略键的大小写\n\t * @return this\n\t */\n\tpublic JSONConfig setIgnoreCase(boolean ignoreCase) {\n\t\tthis.ignoreCase = ignoreCase;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 日期格式，null表示默认的时间戳\n\t *\n\t * @return 日期格式，null表示默认的时间戳\n\t */\n\tpublic String getDateFormat() {\n\t\treturn dateFormat;\n\t}\n\n\t/**\n\t * 设置日期格式，null表示默认的时间戳<br>\n\t * 此方法设置的日期格式仅对转换为JSON字符串有效，对解析JSON为bean无效。\n\t *\n\t * @param dateFormat 日期格式，null表示默认的时间戳\n\t * @return this\n\t */\n\tpublic JSONConfig setDateFormat(String dateFormat) {\n\t\tthis.dateFormat = dateFormat;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否忽略null值\n\t *\n\t * @return 是否忽略null值\n\t */\n\tpublic boolean isIgnoreNullValue() {\n\t\treturn this.ignoreNullValue;\n\t}\n\n\t/**\n\t * 设置是否忽略null值\n\t *\n\t * @param ignoreNullValue 是否忽略null值\n\t * @return this\n\t */\n\tpublic JSONConfig setIgnoreNullValue(boolean ignoreNullValue) {\n\t\tthis.ignoreNullValue = ignoreNullValue;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否支持transient关键字修饰和@Transient注解，如果支持，被修饰的字段或方法对应的字段将被忽略。\n\t *\n\t * @return 是否支持\n\t * @since 5.4.2\n\t */\n\tpublic boolean isTransientSupport() {\n\t\treturn this.transientSupport;\n\t}\n\n\t/**\n\t * 设置是否支持transient关键字修饰和@Transient注解，如果支持，被修饰的字段或方法对应的字段将被忽略。\n\t *\n\t * @param transientSupport 是否支持\n\t * @return this\n\t * @since 5.4.2\n\t */\n\tpublic JSONConfig setTransientSupport(boolean transientSupport) {\n\t\tthis.transientSupport = transientSupport;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否去除末尾多余0，例如如果为true,5.0返回5\n\t *\n\t * @return 是否去除末尾多余0，例如如果为true,5.0返回5\n\t * @since 5.6.2\n\t */\n\tpublic boolean isStripTrailingZeros() {\n\t\treturn stripTrailingZeros;\n\t}\n\n\t/**\n\t * 设置是否去除末尾多余0，例如如果为true,5.0返回5\n\t *\n\t * @param stripTrailingZeros 是否去除末尾多余0，例如如果为true,5.0返回5\n\t * @return this\n\t * @since 5.6.2\n\t */\n\tpublic JSONConfig setStripTrailingZeros(boolean stripTrailingZeros) {\n\t\tthis.stripTrailingZeros = stripTrailingZeros;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否检查多个相同的key\n\t *\n\t * @return 是否检查多个相同的key\n\t * @since 5.8.5\n\t */\n\tpublic boolean isCheckDuplicate() {\n\t\treturn checkDuplicate;\n\t}\n\n\t/**\n\t * 是否检查多个相同的key\n\t *\n\t * @param checkDuplicate 是否检查多个相同的key\n\t * @return this\n\t * @since 5.8.5\n\t */\n\tpublic JSONConfig setCheckDuplicate(boolean checkDuplicate) {\n\t\tthis.checkDuplicate = checkDuplicate;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否将Long值写出为字符串类型\n\t * @return 是否将Long值写出为字符串类型\n\t * @since 5.8.28\n\t */\n\tpublic boolean isWriteLongAsString() {\n\t\treturn writeLongAsString;\n\t}\n\n\t/**\n\t * 设置是否将Long值写出为字符串类型\n\t * @param writeLongAsString 是否将Long值写出为字符串类型\n\t * @return this\n\t */\n\tpublic JSONConfig setWriteLongAsString(boolean writeLongAsString) {\n\t\tthis.writeLongAsString = writeLongAsString;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONConverter.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.convert.Converter;\nimport cn.hutool.core.convert.ConverterRegistry;\nimport cn.hutool.core.convert.impl.ArrayConverter;\nimport cn.hutool.core.convert.impl.BeanConverter;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\nimport cn.hutool.json.serialize.GlobalSerializeMapping;\nimport cn.hutool.json.serialize.JSONDeserializer;\n\nimport java.lang.reflect.Type;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * JSON转换器\n *\n * @author looly\n * @since 4.2.2\n */\npublic class JSONConverter implements Converter<JSON> {\n\n\tstatic {\n\t\t// 注册到转换中心\n\t\tfinal ConverterRegistry registry = ConverterRegistry.getInstance();\n\t\tregistry.putCustom(JSON.class, JSONConverter.class);\n\t\tregistry.putCustom(JSONObject.class, JSONConverter.class);\n\t\tregistry.putCustom(JSONArray.class, JSONConverter.class);\n\t}\n\n\t/**\n\t * JSONArray转数组\n\t *\n\t * @param jsonArray JSONArray\n\t * @param arrayClass 数组元素类型\n\t * @return 数组对象\n\t */\n\tprotected static Object toArray(JSONArray jsonArray, Class<?> arrayClass) {\n\t\treturn new ArrayConverter(arrayClass).convert(jsonArray, null);\n\t}\n\n\t/**\n\t * 将JSONArray转换为指定类型的对量列表\n\t *\n\t * @param <T> 元素类型\n\t * @param jsonArray JSONArray\n\t * @param elementType 对象元素类型\n\t * @return 对象列表\n\t */\n\tprotected static <T> List<T> toList(JSONArray jsonArray, Class<T> elementType) {\n\t\treturn Convert.toList(elementType, jsonArray);\n\t}\n\n\t/**\n\t * JSON递归转换<br>\n\t * 首先尝试JDK类型转换，如果失败尝试JSON转Bean<br>\n\t * 如果遇到{@link JSONBeanParser}，则调用其{@link JSONBeanParser#parse(Object)}方法转换。\n\t *\n\t * @param <T> 转换后的对象类型\n\t * @param targetType 目标类型\n\t * @param value 值\n\t * @param jsonConfig JSON配置\n\t * @return 目标类型的值\n\t * @throws ConvertException 转换失败\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tprotected static <T> T jsonConvert(Type targetType, Object value, JSONConfig jsonConfig) throws ConvertException {\n\t\tif (JSONUtil.isNull(value)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// since 5.7.8，增加自定义Bean反序列化接口\n\t\tif(targetType instanceof Class){\n\t\t\tfinal Class<?> clazz = (Class<?>) targetType;\n\t\t\tif (JSONBeanParser.class.isAssignableFrom(clazz)){\n\t\t\t\t@SuppressWarnings(\"rawtypes\")\n\t\t\t\tfinal JSONBeanParser target = (JSONBeanParser) ReflectUtil.newInstanceIfPossible(clazz);\n\t\t\t\tif(null == target){\n\t\t\t\t\tthrow new ConvertException(\"Can not instance [{}]\", targetType);\n\t\t\t\t}\n\t\t\t\ttarget.parse(value);\n\t\t\t\treturn (T) target;\n\t\t\t} else if(targetType == byte[].class && value instanceof CharSequence){\n\t\t\t\t// issue#I59LW4\n\t\t\t\treturn (T) Base64.decode((CharSequence) value);\n\t\t\t}\n\t\t}\n\n\t\treturn jsonToBean(targetType, value, jsonConfig.isIgnoreError());\n\t}\n\n\t/**\n\t * JSON递归转换<br>\n\t * 首先尝试JDK类型转换，如果失败尝试JSON转Bean\n\t *\n\t * @param <T> 转换后的对象类型\n\t * @param targetType 目标类型\n\t * @param value 值，JSON格式\n\t * @param ignoreError 是否忽略转换错误\n\t * @return 目标类型的值\n\t * @throws ConvertException 转换失败\n\t * @since 5.7.10\n\t */\n\tprotected static <T> T jsonToBean(Type targetType, Object value, boolean ignoreError) throws ConvertException {\n\t\tif (JSONUtil.isNull(value)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif(value instanceof JSON){\n\t\t\tfinal JSONDeserializer<?> deserializer = GlobalSerializeMapping.getDeserializer(targetType);\n\t\t\tif(null != deserializer) {\n\t\t\t\t//noinspection unchecked\n\t\t\t\treturn (T) deserializer.deserialize((JSON) value);\n\t\t\t}\n\n\t\t\t// issue#2212@Github\n\t\t\t// 在JSONObject转Bean时，读取JSONObject本身的配置文件\n\t\t\tif(value instanceof JSONGetter\n\t\t\t\t\t&& targetType instanceof Class\n\t\t\t\t// Map.Entry特殊处理\n\t\t\t\t&& (false == Map.Entry.class.isAssignableFrom((Class<?>)targetType)\n\t\t\t\t&& BeanUtil.hasSetter((Class<?>) targetType))){\n\n\t\t\t\tfinal JSONConfig config = ((JSONGetter<?>) value).getConfig();\n\t\t\t\tfinal Converter<T> converter = new BeanConverter<>(targetType,\n\t\t\t\t\t\tInternalJSONUtil.toCopyOptions(config).setIgnoreError(ignoreError).setFormatIfDate(config.getDateFormat()));\n\t\t\t\treturn converter.convertWithCheck(value, null, ignoreError);\n\t\t\t}\n\t\t}\n\n\t\tfinal T targetValue = Convert.convertWithCheck(targetType, value, null, ignoreError);\n\n\t\tif (null == targetValue && false == ignoreError) {\n\t\t\tif (StrUtil.isBlankIfStr(value)) {\n\t\t\t\t// 对于传入空字符串的情况，如果转换的目标对象是非字符串或非原始类型，转换器会返回false。\n\t\t\t\t// 此处特殊处理，认为返回null属于正常情况\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tthrow new ConvertException(\"Can not convert {} to type {}\", value, ObjectUtil.defaultIfNull(TypeUtil.getClass(targetType), targetType));\n\t\t}\n\n\t\treturn targetValue;\n\t}\n\n\t@Override\n\tpublic JSON convert(Object value, JSON defaultValue) throws IllegalArgumentException {\n\t\treturn JSONUtil.parse(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONException.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * JSON异常\n *\n * @author looly\n * @since 3.0.2\n */\npublic class JSONException extends RuntimeException {\n\tprivate static final long serialVersionUID = 0;\n\n\tpublic JSONException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic JSONException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic JSONException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic JSONException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic JSONException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic JSONException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONGetter.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.convert.NumberWithFormat;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.LocalDateTimeUtil;\nimport cn.hutool.core.getter.OptNullBasicTypeFromObjectGetter;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.time.LocalDateTime;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * 用于JSON的Getter类，提供各种类型的Getter方法\n *\n * @param <K> Key类型\n * @author Looly\n */\npublic interface JSONGetter<K> extends OptNullBasicTypeFromObjectGetter<K> {\n\n\t/**\n\t * 获取JSON配置\n\t *\n\t * @return {@link JSONConfig}\n\t * @since 5.3.0\n\t */\n\tJSONConfig getConfig();\n\n\t/**\n\t * key对应值是否为{@code null}或无此key\n\t *\n\t * @param key 键\n\t * @return true 无此key或值为{@code null}或{@link JSONNull#NULL}返回{@code false}，其它返回{@code true}\n\t */\n\tdefault boolean isNull(K key) {\n\t\treturn JSONUtil.isNull(this.getObj(key));\n\t}\n\n\t/**\n\t * 获取字符串类型值，并转义不可见字符，如'\\n'换行符会被转义为字符串\"\\n\"\n\t *\n\t * @param key 键\n\t * @return 字符串类型值\n\t * @since 4.2.2\n\t */\n\tdefault String getStrEscaped(K key) {\n\t\treturn getStrEscaped(key, null);\n\t}\n\n\t/**\n\t * 获取字符串类型值，并转义不可见字符，如'\\n'换行符会被转义为字符串\"\\n\"\n\t *\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return 字符串类型值\n\t * @since 4.2.2\n\t */\n\tdefault String getStrEscaped(K key, String defaultValue) {\n\t\treturn JSONUtil.escape(getStr(key, defaultValue));\n\t}\n\n\t/**\n\t * 获得JSONArray对象<br>\n\t * 如果值为其它类型对象，尝试转换为{@link JSONArray}返回，否则抛出异常\n\t *\n\t * @param key KEY\n\t * @return JSONArray对象，如果值为{@code null}，返回{@code null}，非JSONArray类型，尝试转换，转换失败抛出异常\n\t */\n\tdefault JSONArray getJSONArray(K key) {\n\t\tfinal Object object = this.getObj(key);\n\t\tif (JSONUtil.isNull(object)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (object instanceof JSON) {\n\t\t\treturn (JSONArray) object;\n\t\t}\n\t\treturn new JSONArray(object, getConfig());\n\t}\n\n\t/**\n\t * 获得JSONObject对象<br>\n\t * 如果值为其它类型对象，尝试转换为{@link JSONObject}返回，否则抛出异常\n\t *\n\t * @param key KEY\n\t * @return JSONObject对象，如果值为{@code null}，返回{@code null}，非JSONObject类型，尝试转换，转换失败抛出异常\n\t */\n\tdefault JSONObject getJSONObject(K key) {\n\t\tfinal Object object = this.getObj(key);\n\t\tif (JSONUtil.isNull(object)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (object instanceof JSON) {\n\t\t\treturn (JSONObject) object;\n\t\t}\n\t\treturn new JSONObject(object, getConfig());\n\t}\n\n\t/**\n\t * 从JSON中直接获取Bean对象<br>\n\t * 先获取JSONObject对象，然后转为Bean对象\n\t *\n\t * @param <T>      Bean类型\n\t * @param key      KEY\n\t * @param beanType Bean类型\n\t * @return Bean对象，如果值为null或者非JSONObject类型，返回null\n\t * @since 3.1.1\n\t */\n\tdefault <T> T getBean(K key, Class<T> beanType) {\n\t\tfinal JSONObject obj = getJSONObject(key);\n\t\treturn (null == obj) ? null : obj.toBean(beanType);\n\t}\n\n\t/**\n\t * 从JSON中直接获取Bean的List列表<br>\n\t * 先获取JSONArray对象，然后转为Bean的List\n\t *\n\t * @param <T>      Bean类型\n\t * @param key      KEY\n\t * @param beanType Bean类型\n\t * @return Bean的List，如果值为null或者非JSONObject类型，返回null\n\t * @since 5.7.20\n\t */\n\tdefault <T> List<T> getBeanList(K key, Class<T> beanType) {\n\t\tfinal JSONArray jsonArray = getJSONArray(key);\n\t\treturn (null == jsonArray) ? null : jsonArray.toList(beanType);\n\t}\n\n\t@Override\n\tdefault Date getDate(K key, Date defaultValue) {\n\t\t// 默认转换\n\t\tfinal Object obj = getObj(key);\n\t\tif (JSONUtil.isNull(obj)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\tif (obj instanceof Date) {\n\t\t\treturn (Date) obj;\n\t\t} else if(obj instanceof NumberWithFormat){\n\t\t\treturn (Date) ((NumberWithFormat) obj).convert(Date.class, obj);\n\t\t}\n\n\t\tfinal Optional<String> formatOps = Optional.ofNullable(getConfig()).map(JSONConfig::getDateFormat);\n\t\tif (formatOps.isPresent()) {\n\t\t\tfinal String format = formatOps.get();\n\t\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\t\t// 用户指定了日期格式，获取日期属性时使用对应格式\n\t\t\t\tfinal String str = Convert.toStr(obj);\n\t\t\t\tif (null == str) {\n\t\t\t\t\treturn defaultValue;\n\t\t\t\t}\n\t\t\t\treturn DateUtil.parse(str, format);\n\t\t\t}\n\t\t}\n\n\t\treturn Convert.toDate(obj, defaultValue);\n\t}\n\n\t/**\n\t * 获取{@link LocalDateTime}类型值\n\t *\n\t * @param key          键\n\t * @param defaultValue 默认值\n\t * @return {@link LocalDateTime}\n\t * @since 5.7.7\n\t */\n\tdefault LocalDateTime getLocalDateTime(K key, LocalDateTime defaultValue) {\n\t\t// 默认转换\n\t\tfinal Object obj = getObj(key);\n\t\tif (JSONUtil.isNull(obj)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\tif (obj instanceof LocalDateTime) {\n\t\t\treturn (LocalDateTime) obj;\n\t\t}\n\n\t\tfinal Optional<String> formatOps = Optional.ofNullable(getConfig()).map(JSONConfig::getDateFormat);\n\t\tif (formatOps.isPresent()) {\n\t\t\tfinal String format = formatOps.get();\n\t\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\t\t// 用户指定了日期格式，获取日期属性时使用对应格式\n\t\t\t\tfinal String str = Convert.toStr(obj);\n\t\t\t\tif (null == str) {\n\t\t\t\t\treturn defaultValue;\n\t\t\t\t}\n\t\t\t\treturn LocalDateTimeUtil.parse(str, format);\n\t\t\t}\n\t\t}\n\n\t\treturn Convert.toLocalDateTime(obj, defaultValue);\n\t}\n\n\t/**\n\t * 获取byte[]数据\n\t *\n\t * @param key 键\n\t * @return 值\n\t * @since 5.8.2\n\t */\n\tdefault byte[] getBytes(K key) {\n\t\treturn get(key, byte[].class);\n\t}\n\n\t/**\n\t * 获取指定类型的对象<br>\n\t * 转换失败或抛出异常\n\t *\n\t * @param <T>  获取的对象类型\n\t * @param key  键\n\t * @param type 获取对象类型\n\t * @return 对象\n\t * @throws ConvertException 转换异常\n\t * @since 3.0.8\n\t */\n\tdefault <T> T get(K key, Class<T> type) throws ConvertException {\n\t\treturn get(key, type, false);\n\t}\n\n\t/**\n\t * 获取指定类型的对象\n\t *\n\t * @param <T>         获取的对象类型\n\t * @param key         键\n\t * @param type        获取对象类型\n\t * @param ignoreError 是否跳过转换失败的对象或值\n\t * @return 对象\n\t * @throws ConvertException 转换异常\n\t * @since 3.0.8\n\t */\n\tdefault <T> T get(K key, Class<T> type, boolean ignoreError) throws ConvertException {\n\t\tfinal Object value = this.getObj(key);\n\t\tif (JSONUtil.isNull(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn JSONConverter.jsonConvert(type, value, JSONConfig.create().setIgnoreError(ignoreError));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONNull.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\n\n/**\n * 用于定义{@code null}，与Javascript中null相对应<br>\n * Java中的{@code null}值在js中表示为undefined。\n *\n * @author Looly\n */\npublic class JSONNull implements Serializable {\n\tprivate static final long serialVersionUID = 2633815155870764938L;\n\n\t/**\n\t * {@code NULL} 对象用于减少歧义来表示Java 中的{@code null} <br>\n\t * {@code NULL.equals(null)} 返回 {@code true}. <br>\n\t * {@code NULL.toString()} 返回 {@code \"null\"}.\n\t */\n\tpublic static final JSONNull NULL = new JSONNull();\n\n\t/**\n\t * A Null object is equal to the null value and to itself.\n\t * 对象与其本身和{@code null}值相等\n\t *\n\t * @param object An object to test for nullness.\n\t * @return true if the object parameter is the JSONObject.NULL object or null.\n\t */\n\t@SuppressWarnings(\"EqualsWhichDoesntCheckParameterClass\")\n\t@Override\n\tpublic boolean equals(Object object) {\n\t\treturn object == null || (object == this);\n\t}\n\n\t/**\n\t * Get the \"null\" string value.\n\t * 获得“null”字符串\n\t *\n\t * @return The string \"null\".\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn StrUtil.NULL;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONObject.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanPath;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.lang.mutable.MutablePair;\nimport cn.hutool.core.map.CaseInsensitiveMap;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.MapWrapper;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.json.serialize.JSONWriter;\n\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * JSON对象<br>\n * 例：<br>\n *\n * <pre>\n * json = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, World!&quot;).toString();\n * </pre>\n *\n * @author looly\n */\npublic class JSONObject extends MapWrapper<String, Object> implements JSON, JSONGetter<String> {\n\tprivate static final long serialVersionUID = -330220388580734346L;\n\n\t/**\n\t * 默认初始大小\n\t */\n\tpublic static final int DEFAULT_CAPACITY = MapUtil.DEFAULT_INITIAL_CAPACITY;\n\n\t/**\n\t * 配置项\n\t */\n\tprivate JSONConfig config;\n\n\t// -------------------------------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，初始容量为 {@link #DEFAULT_CAPACITY}，KEY无序\n\t */\n\tpublic JSONObject() {\n\t\tthis(DEFAULT_CAPACITY, false);\n\t}\n\n\t/**\n\t * 构造，初始容量为 {@link #DEFAULT_CAPACITY}\n\t *\n\t * @param isOrder 是否有序\n\t * @since 3.0.9\n\t */\n\tpublic JSONObject(boolean isOrder) {\n\t\tthis(DEFAULT_CAPACITY, isOrder);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 初始大小\n\t * @param isOrder  是否有序\n\t * @since 3.0.9\n\t */\n\tpublic JSONObject(int capacity, boolean isOrder) {\n\t\tthis(capacity, false, isOrder);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity     初始大小\n\t * @param isIgnoreCase 是否忽略KEY大小写\n\t * @param isOrder      是否有序\n\t * @since 3.3.1\n\t * @deprecated isOrder无效\n\t */\n\t@SuppressWarnings(\"unused\")\n\t@Deprecated\n\tpublic JSONObject(int capacity, boolean isIgnoreCase, boolean isOrder) {\n\t\tthis(capacity, JSONConfig.create().setIgnoreCase(isIgnoreCase));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param config JSON配置项\n\t * @since 4.6.5\n\t */\n\tpublic JSONObject(JSONConfig config) {\n\t\tthis(DEFAULT_CAPACITY, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param capacity 初始大小\n\t * @param config   JSON配置项，{@code null}则使用默认配置\n\t * @since 4.1.19\n\t */\n\tpublic JSONObject(int capacity, JSONConfig config) {\n\t\tsuper(InternalJSONUtil.createRawMap(capacity, ObjectUtil.defaultIfNull(config, JSONConfig::create)));\n\t\tthis.config = ObjectUtil.defaultIfNull(config, JSONConfig::create);\n\t}\n\n\t/**\n\t * 构建JSONObject，JavaBean默认忽略null值，其它对象不忽略，规则如下：\n\t * <ol>\n\t * <li>value为Map，将键值对加入JSON对象</li>\n\t * <li>value为JSON字符串（CharSequence），使用JSONTokener解析</li>\n\t * <li>value为JSONTokener，直接解析</li>\n\t * <li>value为普通JavaBean，如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。\n\t * 例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n\t * </ol>\n\t *\n\t * @param source JavaBean或者Map对象或者String\n\t */\n\tpublic JSONObject(Object source) {\n\t\tthis(source, InternalJSONUtil.defaultIgnoreNullValue(source));\n\t}\n\n\t/**\n\t * 构建JSONObject，规则如下：\n\t * <ol>\n\t * <li>value为Map，将键值对加入JSON对象</li>\n\t * <li>value为JSON字符串（CharSequence），使用JSONTokener解析</li>\n\t * <li>value为JSONTokener，直接解析</li>\n\t * <li>value为普通JavaBean，如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n\t * </ol>\n\t *\n\t * @param source          JavaBean或者Map对象或者String\n\t * @param ignoreNullValue 是否忽略空值\n\t * @since 3.0.9\n\t */\n\tpublic JSONObject(Object source, boolean ignoreNullValue) {\n\t\tthis(source, JSONConfig.create().setIgnoreNullValue(ignoreNullValue));\n\t}\n\n\t/**\n\t * 构建JSONObject，规则如下：\n\t * <ol>\n\t * <li>value为Map，将键值对加入JSON对象</li>\n\t * <li>value为JSON字符串（CharSequence），使用JSONTokener解析</li>\n\t * <li>value为JSONTokener，直接解析</li>\n\t * <li>value为普通JavaBean，如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n\t * </ol>\n\t *\n\t * @param source          JavaBean或者Map对象或者String\n\t * @param ignoreNullValue 是否忽略空值，如果source为JSON字符串，不忽略空值\n\t * @param isOrder         是否有序\n\t * @since 4.2.2\n\t * @deprecated isOrder参数不再需要，JSONObject默认有序！\n\t */\n\t@SuppressWarnings(\"unused\")\n\t@Deprecated\n\tpublic JSONObject(Object source, boolean ignoreNullValue, boolean isOrder) {\n\t\tthis(source, JSONConfig.create()//\n\t\t\t.setIgnoreCase((source instanceof CaseInsensitiveMap))//\n\t\t\t.setIgnoreNullValue(ignoreNullValue)\n\t\t);\n\t}\n\n\t/**\n\t * 构建JSONObject，规则如下：\n\t * <ol>\n\t * <li>value为Map，将键值对加入JSON对象</li>\n\t * <li>value为JSON字符串（CharSequence），使用JSONTokener解析</li>\n\t * <li>value为JSONTokener，直接解析</li>\n\t * <li>value为普通JavaBean，如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n\t * </ol>\n\t * <p>\n\t * 如果给定值为Map，将键值对加入JSON对象;<br>\n\t * 如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象<br>\n\t * 例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"\n\t *\n\t * @param source JavaBean或者Map对象或者String\n\t * @param config JSON配置文件，{@code null}则使用默认配置\n\t * @since 4.2.2\n\t */\n\tpublic JSONObject(Object source, JSONConfig config) {\n\t\tthis(source, config, null);\n\t}\n\n\t/**\n\t * 构建JSONObject，规则如下：\n\t * <ol>\n\t * <li>value为Map，将键值对加入JSON对象</li>\n\t * <li>value为JSON字符串（CharSequence），使用JSONTokener解析</li>\n\t * <li>value为JSONTokener，直接解析</li>\n\t * <li>value为普通JavaBean，如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n\t * </ol>\n\t * <p>\n\t * 如果给定值为Map，将键值对加入JSON对象;<br>\n\t * 如果为普通的JavaBean，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象<br>\n\t * 例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"\n\t *\n\t * @param source JavaBean或者Map对象或者String\n\t * @param config JSON配置文件，{@code null}则使用默认配置\n\t * @param filter 键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t * @since 5.8.0\n\t */\n\tpublic JSONObject(Object source, JSONConfig config, Filter<MutablePair<String, Object>> filter) {\n\t\tthis(DEFAULT_CAPACITY, config);\n\t\tObjectMapper.of(source).map(this, filter);\n\t}\n\n\t/**\n\t * 构建指定name列表对应的键值对为新的JSONObject，情况如下：\n\t *\n\t * <pre>\n\t * 1. 若obj为Map，则获取name列表对应键值对\n\t * 2. 若obj为普通Bean，使用反射方式获取字段名和字段值\n\t * </pre>\n\t * <p>\n\t * KEY或VALUE任意一个为null则不加入，字段不存在也不加入<br>\n\t * 若names列表为空，则字段全部加入\n\t *\n\t * @param source 包含需要字段的Bean对象或者Map对象\n\t * @param names  需要构建JSONObject的字段名列表\n\t */\n\tpublic JSONObject(Object source, String... names) {\n\t\tthis();\n\t\tif (ArrayUtil.isEmpty(names)) {\n\t\t\tObjectMapper.of(source).map(this, null);\n\t\t\treturn;\n\t\t}\n\n\t\tif (source instanceof Map) {\n\t\t\tObject value;\n\t\t\tfor (String name : names) {\n\t\t\t\tvalue = ((Map<?, ?>) source).get(name);\n\t\t\t\tthis.set(name, value, null, getConfig().isCheckDuplicate());\n\t\t\t}\n\t\t} else {\n\t\t\tfor (String name : names) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.putOpt(name, ReflectUtil.getFieldValue(source, name));\n\t\t\t\t} catch (Exception ignore) {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从JSON字符串解析为JSON对象，对于排序单独配置参数\n\t *\n\t * @param source  以大括号 {} 包围的字符串，其中KEY和VALUE使用 : 分隔，每个键值对使用逗号分隔\n\t * @param isOrder 是否有序\n\t * @throws JSONException JSON字符串语法错误\n\t * @since 4.2.2\n\t * @deprecated isOrder无效\n\t */\n\t@SuppressWarnings(\"unused\")\n\t@Deprecated\n\tpublic JSONObject(CharSequence source, boolean isOrder) throws JSONException {\n\t\tthis(source, JSONConfig.create());\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic JSONConfig getConfig() {\n\t\treturn this.config;\n\t}\n\n\t/**\n\t * 设置转为字符串时的日期格式，默认为时间戳（null值）<br>\n\t * 此方法设置的日期格式仅对转换为JSON字符串有效，对解析JSON为bean无效。\n\t *\n\t * @param format 格式，null表示使用时间戳\n\t * @return this\n\t * @since 4.1.19\n\t */\n\tpublic JSONObject setDateFormat(String format) {\n\t\tthis.config.setDateFormat(format);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将指定KEY列表的值组成新的JSONArray\n\t *\n\t * @param names KEY列表\n\t * @return A JSONArray of values.\n\t * @throws JSONException If any of the values are non-finite numbers.\n\t */\n\tpublic JSONArray toJSONArray(Collection<String> names) throws JSONException {\n\t\tif (CollectionUtil.isEmpty(names)) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal JSONArray ja = new JSONArray(this.config);\n\t\tObject value;\n\t\tfor (String name : names) {\n\t\t\tvalue = this.get(name);\n\t\t\tif (null != value) {\n\t\t\t\tja.set(value);\n\t\t\t}\n\t\t}\n\t\treturn ja;\n\t}\n\n\t@Override\n\tpublic Object getObj(String key, Object defaultValue) {\n\t\treturn this.getOrDefault(key, defaultValue);\n\t}\n\n\t@Override\n\tpublic Object getByPath(String expression) {\n\t\treturn BeanPath.create(expression).get(this);\n\t}\n\n\t@Override\n\tpublic <T> T getByPath(String expression, Class<T> resultType) {\n\t\treturn JSONConverter.jsonConvert(resultType, getByPath(expression), getConfig());\n\t}\n\n\t@Override\n\tpublic <T> T getByPath(String expression, TypeReference<T> targetType) {\n\t\treturn JSONConverter.jsonConvert(targetType, getByPath(expression), getConfig());\n\t}\n\n\t@Override\n\tpublic void putByPath(String expression, Object value) {\n\t\tBeanPath.create(expression).set(this, value);\n\t}\n\n\t/**\n\t * PUT 键值对到JSONObject中，在忽略null模式下，如果值为{@code null}，将此键移除\n\t *\n\t * @param key   键\n\t * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return this.\n\t * @throws JSONException 值是无穷数字抛出此异常\n\t * @deprecated 此方法存在歧义，原Map接口返回的是之前的值，重写后返回this了，未来版本此方法会修改，请使用{@link #set(String, Object)}\n\t */\n\t@Override\n\t@Deprecated\n\tpublic JSONObject put(String key, Object value) throws JSONException {\n\t\treturn set(key, value);\n\t}\n\n\t/**\n\t * 设置键值对到JSONObject中，在忽略null模式下，如果值为{@code null}，将此键移除\n\t *\n\t * @param key   键\n\t * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return this.\n\t * @throws JSONException 值是无穷数字抛出此异常\n\t */\n\tpublic JSONObject set(String key, Object value) throws JSONException {\n\t\treturn set(key, value, null, false);\n\t}\n\n\t/**\n\t * 设置键值对到JSONObject中，在忽略null模式下，如果值为{@code null}，将此键移除\n\t *\n\t * @param key            键\n\t * @param value          值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @param filter         键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t * @param checkDuplicate 是否检查重复键，如果为{@code true}，则出现重复键时抛出{@link JSONException}异常\n\t * @return this.\n\t * @throws JSONException 值是无穷数字抛出此异常\n\t * @since 5.8.0\n\t */\n\tpublic JSONObject set(String key, Object value, Filter<MutablePair<String, Object>> filter, boolean checkDuplicate) throws JSONException {\n\t\tif (null == key) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// 添加前置过滤，通过MutablePair实现过滤、修改键值对等\n\t\tif (null != filter) {\n\t\t\tfinal MutablePair<String, Object> pair = new MutablePair<>(key, value);\n\t\t\tif (filter.accept(pair)) {\n\t\t\t\t// 使用修改后的键值对\n\t\t\t\tkey = pair.getKey();\n\t\t\t\tvalue = pair.getValue();\n\t\t\t} else {\n\t\t\t\t// 键值对被过滤\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\tfinal boolean ignoreNullValue = this.config.isIgnoreNullValue();\n\t\tif (ObjectUtil.isNull(value) && ignoreNullValue) {\n\t\t\t// 忽略值模式下如果值为空清除key\n\t\t\tthis.remove(key);\n\t\t} else {\n\t\t\tif (checkDuplicate && containsKey(key)) {\n\t\t\t\tthrow new JSONException(\"Duplicate key \\\"{}\\\"\", key);\n\t\t\t}\n\n\t\t\tsuper.put(key, JSONUtil.wrap(InternalJSONUtil.testValidity(value), this.config));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 一次性Put 键值对，如果key已经存在抛出异常，如果键值中有null值，忽略\n\t *\n\t * @param key   键\n\t * @param value 值对象，可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return this.\n\t * @throws JSONException 值是无穷数字、键重复抛出异常\n\t */\n\tpublic JSONObject putOnce(String key, Object value) throws JSONException {\n\t\treturn setOnce(key, value, null);\n\t}\n\n\t/**\n\t * 一次性Put 键值对，如果key已经存在抛出异常，如果键值中有null值，忽略\n\t *\n\t * @param key    键\n\t * @param value  值对象，可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @param filter 键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t * @return this\n\t * @throws JSONException 值是无穷数字、键重复抛出异常\n\t * @since 5.8.0\n\t */\n\tpublic JSONObject setOnce(String key, Object value, Filter<MutablePair<String, Object>> filter) throws JSONException {\n\t\treturn set(key, value, filter, true);\n\t}\n\n\t/**\n\t * 在键和值都为非空的情况下put到JSONObject中\n\t *\n\t * @param key   键\n\t * @param value 值对象，可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.\n\t * @return this.\n\t * @throws JSONException 值是无穷数字\n\t */\n\tpublic JSONObject putOpt(String key, Object value) throws JSONException {\n\t\tif (key != null && value != null) {\n\t\t\tthis.set(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void putAll(Map<? extends String, ?> m) {\n\t\tfor (Entry<? extends String, ?> entry : m.entrySet()) {\n\t\t\tthis.set(entry.getKey(), entry.getValue());\n\t\t}\n\t}\n\n\t/**\n\t * 积累值。类似于set，当key对应value已经存在时，与value组成新的JSONArray. <br>\n\t * 如果只有一个值，此值就是value，如果多个值，则是添加到新的JSONArray中\n\t *\n\t * @param key   键\n\t * @param value 被积累的值\n\t * @return this.\n\t * @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray\n\t */\n\tpublic JSONObject accumulate(String key, Object value) throws JSONException {\n\t\tInternalJSONUtil.testValidity(value);\n\t\tObject object = this.getObj(key);\n\t\tif (object == null) {\n\t\t\tthis.set(key, value);\n\t\t} else if (object instanceof JSONArray) {\n\t\t\t((JSONArray) object).set(value);\n\t\t} else {\n\t\t\tthis.set(key, JSONUtil.createArray(this.config).set(object).set(value));\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加值，如果key无对应值，就添加一个JSONArray，其元素只有value，如果值已经是一个JSONArray，则添加到值JSONArray中。\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @return this.\n\t * @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray\n\t */\n\tpublic JSONObject append(String key, Object value) throws JSONException {\n\t\tInternalJSONUtil.testValidity(value);\n\t\tObject object = this.getObj(key);\n\t\tif (object == null) {\n\t\t\tthis.set(key, new JSONArray(this.config).set(value));\n\t\t} else if (object instanceof JSONArray) {\n\t\t\tthis.set(key, ((JSONArray) object).set(value));\n\t\t} else {\n\t\t\tthrow new JSONException(\"JSONObject [\" + key + \"] is not a JSONArray.\");\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 对值加一，如果值不存在，赋值1，如果为数字类型，做加一操作\n\t *\n\t * @param key A key string.\n\t * @return this.\n\t * @throws JSONException 如果存在值非Integer, Long, Double, 或 Float.\n\t */\n\tpublic JSONObject increment(String key) throws JSONException {\n\t\tObject value = this.getObj(key);\n\t\tif (value == null) {\n\t\t\tthis.set(key, 1);\n\t\t} else if (value instanceof BigInteger) {\n\t\t\tthis.set(key, ((BigInteger) value).add(BigInteger.ONE));\n\t\t} else if (value instanceof BigDecimal) {\n\t\t\tthis.set(key, ((BigDecimal) value).add(BigDecimal.ONE));\n\t\t} else if (value instanceof Integer) {\n\t\t\tthis.set(key, (Integer) value + 1);\n\t\t} else if (value instanceof Long) {\n\t\t\tthis.set(key, (Long) value + 1);\n\t\t} else if (value instanceof Double) {\n\t\t\tthis.set(key, (Double) value + 1);\n\t\t} else if (value instanceof Float) {\n\t\t\tthis.set(key, (Float) value + 1);\n\t\t} else {\n\t\t\tthrow new JSONException(\"Unable to increment [\" + JSONUtil.quote(key) + \"].\");\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回JSON字符串<br>\n\t * 如果解析错误，返回{@code null}\n\t *\n\t * @return JSON字符串\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn this.toJSONString(0);\n\t}\n\n\t/**\n\t * 返回JSON字符串<br>\n\t * 支持过滤器，即选择哪些字段或值不写出\n\t *\n\t * @param indentFactor 每层缩进空格数\n\t * @param filter       过滤器，同时可以修改编辑键和值\n\t * @return JSON字符串\n\t * @since 5.7.15\n\t */\n\tpublic String toJSONString(int indentFactor, Filter<MutablePair<Object, Object>> filter) {\n\t\tfinal StringWriter sw = new StringWriter();\n\t\tsynchronized (sw.getBuffer()) {\n\t\t\treturn this.write(sw, indentFactor, 0, filter).toString();\n\t\t}\n\t}\n\n\t@Override\n\tpublic Writer write(Writer writer, int indentFactor, int indent) throws JSONException {\n\t\treturn write(writer, indentFactor, indent, null);\n\t}\n\n\t/**\n\t * 将JSON内容写入Writer<br>\n\t * 支持过滤器，即选择哪些字段或值不写出\n\t *\n\t * @param writer       writer\n\t * @param indentFactor 缩进因子，定义每一级别增加的缩进量\n\t * @param indent       本级别缩进量\n\t * @param filter       过滤器，同时可以修改编辑键和值\n\t * @return Writer\n\t * @throws JSONException JSON相关异常\n\t * @since 5.7.15\n\t */\n\tpublic Writer write(Writer writer, int indentFactor, int indent, Filter<MutablePair<Object, Object>> filter) throws JSONException {\n\t\tfinal JSONWriter jsonWriter = JSONWriter.of(writer, indentFactor, indent, config)\n\t\t\t.beginObj();\n\t\tthis.forEach((key, value) -> jsonWriter.writeField(new MutablePair<>(key, value), filter));\n\t\tjsonWriter.end();\n\t\t// 此处不关闭Writer，考虑writer后续还需要填内容\n\t\treturn writer;\n\t}\n\n\t@Override\n\tpublic JSONObject clone() throws CloneNotSupportedException {\n\t\tfinal JSONObject clone = (JSONObject) super.clone();\n\t\tclone.config = this.config;\n\t\treturn clone;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONObjectIter.java",
    "content": "package cn.hutool.json;\n\nimport java.util.Iterator;\n\n/**\n * 此类用于在JSONAray中便于遍历JSONObject而封装的Iterable，可以借助foreach语法遍历\n * \n * @author looly\n * @since 4.0.12\n */\npublic class JSONObjectIter implements Iterable<JSONObject> {\n\n\tIterator<Object> iterator;\n\t\n\tpublic JSONObjectIter(Iterator<Object> iterator) {\n\t\tthis.iterator = iterator;\n\t}\n\n\t@Override\n\tpublic Iterator<JSONObject> iterator() {\n\t\treturn new Iterator<JSONObject>() {\n\n\t\t\t@Override\n\t\t\tpublic boolean hasNext() {\n\t\t\t\treturn iterator.hasNext();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic JSONObject next() {\n\t\t\t\treturn (JSONObject) iterator.next();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void remove() {\n\t\t\t\titerator.remove();\n\t\t\t}\n\t\t};\n\t}\n\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONParser.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutablePair;\n\n/**\n * JSON字符串解析器\n *\n * @author looly\n * @since 5.8.0\n */\npublic class JSONParser {\n\n\t/**\n\t * 创建JSONParser\n\t *\n\t * @param tokener {@link JSONTokener}\n\t * @return JSONParser\n\t */\n\tpublic static JSONParser of(JSONTokener tokener) {\n\t\treturn new JSONParser(tokener);\n\t}\n\n\tprivate final JSONTokener tokener;\n\n\t/**\n\t * 构造\n\t *\n\t * @param tokener {@link JSONTokener}\n\t */\n\tpublic JSONParser(JSONTokener tokener) {\n\t\tthis.tokener = tokener;\n\t}\n\n\t// region parseTo\n\n\t/**\n\t * 解析{@link JSONTokener}中的字符到目标的{@link JSONObject}中\n\t *\n\t * @param jsonObject {@link JSONObject}\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t */\n\tpublic void parseTo(JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {\n\t\tfinal JSONTokener tokener = this.tokener;\n\n\t\tif (tokener.nextClean() != '{') {\n\t\t\tthrow tokener.syntaxError(\"A JSONObject text must begin with '{'\");\n\t\t}\n\n\t\tchar prev;\n\t\tchar c;\n\t\tString key;\n\t\twhile (true) {\n\t\t\tprev = tokener.getPrevious();\n\t\t\tc = tokener.nextClean();\n\t\t\tswitch (c) {\n\t\t\t\tcase 0:\n\t\t\t\t\tthrow tokener.syntaxError(\"A JSONObject text must end with '}'\");\n\t\t\t\tcase '}':\n\t\t\t\t\treturn;\n\t\t\t\tcase '{':\n\t\t\t\tcase '[':\n\t\t\t\t\tif (prev == '{') {\n\t\t\t\t\t\tthrow tokener.syntaxError(\"A JSONObject can not directly nest another JSONObject or JSONArray.\");\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\ttokener.back();\n\t\t\t\t\tkey = tokener.nextStringValue();\n\t\t\t}\n\n\t\t\t// The key is followed by ':'.\n\n\t\t\tc = tokener.nextClean();\n\t\t\tif (c != ':') {\n\t\t\t\tthrow tokener.syntaxError(\"Expected a ':' after a key\");\n\t\t\t}\n\n\t\t\tjsonObject.set(key, tokener.nextValue(), filter, jsonObject.getConfig().isCheckDuplicate());\n\n\t\t\t// Pairs are separated by ','.\n\n\t\t\tswitch (tokener.nextClean()) {\n\t\t\t\tcase ';':\n\t\t\t\tcase ',':\n\t\t\t\t\tif (tokener.nextClean() == '}') {\n\t\t\t\t\t\t// issue#2380\n\t\t\t\t\t\t// 尾后逗号（Trailing Commas），JSON中虽然不支持，但是ECMAScript 2017支持，此处做兼容。\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\ttokener.back();\n\t\t\t\t\tbreak;\n\t\t\t\tcase '}':\n\t\t\t\t\treturn;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow tokener.syntaxError(\"Expected a ',' or '}'\");\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 解析JSON字符串到{@link JSONArray}中\n\t *\n\t * @param jsonArray {@link JSONArray}\n\t * @param filter    键值对过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null} 表示不过滤\n\t */\n\tpublic void parseTo(JSONArray jsonArray, Filter<Mutable<Object>> filter) {\n\t\tfinal JSONTokener x = this.tokener;\n\n\t\tif (x.nextClean() != '[') {\n\t\t\tthrow x.syntaxError(\"A JSONArray text must start with '['\");\n\t\t}\n\t\tif (x.nextClean() != ']') {\n\t\t\tx.back();\n\t\t\tfor (; ; ) {\n\t\t\t\tif (x.nextClean() == ',') {\n\t\t\t\t\tx.back();\n\t\t\t\t\tjsonArray.addRaw(JSONNull.NULL, filter);\n\t\t\t\t} else {\n\t\t\t\t\tx.back();\n\t\t\t\t\tjsonArray.addRaw(x.nextValue(), filter);\n\t\t\t\t}\n\t\t\t\tswitch (x.nextClean()) {\n\t\t\t\t\tcase ',':\n\t\t\t\t\t\tif (x.nextClean() == ']') {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tx.back();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase ']':\n\t\t\t\t\t\treturn;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow x.syntaxError(\"Expected a ',' or ']'\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// endregion\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONStrFormatter.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * JSON字符串格式化工具，用于简单格式化JSON字符串<br>\n * from http://blog.csdn.net/lovelong8808/article/details/54580278\n *\n * @author looly\n * @since 3.1.2\n */\npublic class JSONStrFormatter {\n\n\t/**\n\t * 单位缩进字符串。\n\t */\n\tprivate static final String SPACE = \"    \";\n\t/**\n\t * 换行符\n\t */\n\tprivate static final char NEW_LINE = StrUtil.C_LF;\n\n\t/**\n\t * 返回格式化JSON字符串。\n\t *\n\t * @param json 未格式化的JSON字符串。\n\t * @return 格式化的JSON字符串。\n\t */\n\tpublic static String format(String json) {\n\t\tfinal StringBuilder result = new StringBuilder();\n\n\t\tCharacter wrapChar = null;\n\t\tboolean isEscapeMode = false;\n\t\tint length = json.length();\n\t\tint number = 0;\n\t\tchar key;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tkey = json.charAt(i);\n\n\t\t\tif (CharUtil.DOUBLE_QUOTES == key || CharUtil.SINGLE_QUOTE == key) {\n\t\t\t\tif (null == wrapChar) {\n\t\t\t\t\t//字符串模式开始\n\t\t\t\t\twrapChar = key;\n\t\t\t\t} else if (wrapChar.equals(key)) {\n\t\t\t\t\tif (isEscapeMode) {\n\t\t\t\t\t\t//字符串模式下，遇到结束符号，也同时结束转义\n\t\t\t\t\t\tisEscapeMode = false;\n\t\t\t\t\t}\n\n\t\t\t\t\t//字符串包装结束\n\t\t\t\t\twrapChar = null;\n\t\t\t\t}\n\n\t\t\t\tif ((i > 1) && (json.charAt(i - 1) == CharUtil.COLON)) {\n\t\t\t\t\tresult.append(CharUtil.SPACE);\n\t\t\t\t}\n\n\t\t\t\tresult.append(key);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (CharUtil.BACKSLASH == key) {\n\t\t\t\tif (null != wrapChar) {\n\t\t\t\t\t//字符串模式下转义有效\n\t\t\t\t\tisEscapeMode = !isEscapeMode;\n\t\t\t\t\tresult.append(key);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\tresult.append(key);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (null != wrapChar) {\n\t\t\t\t//字符串模式\n\t\t\t\tresult.append(key);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t//如果当前字符是前方括号、前花括号做如下处理：\n\t\t\tif ((key == CharUtil.BRACKET_START) || (key == CharUtil.DELIM_START)) {\n\t\t\t\t//如果前面还有字符，并且字符为“：”，打印：换行和缩进字符字符串。\n\t\t\t\tif ((i > 1) && (json.charAt(i - 1) == CharUtil.COLON)) {\n\t\t\t\t\tresult.append(NEW_LINE);\n\t\t\t\t\tresult.append(indent(number));\n\t\t\t\t}\n\t\t\t\tresult.append(key);\n\t\t\t\t//前方括号、前花括号，的后面必须换行。打印：换行。\n\t\t\t\tresult.append(NEW_LINE);\n\t\t\t\t//每出现一次前方括号、前花括号；缩进次数增加一次。打印：新行缩进。\n\t\t\t\tnumber++;\n\t\t\t\tresult.append(indent(number));\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 3、如果当前字符是后方括号、后花括号做如下处理：\n\t\t\tif ((key == CharUtil.BRACKET_END) || (key == CharUtil.DELIM_END)) {\n\t\t\t\t// （1）后方括号、后花括号，的前面必须换行。打印：换行。\n\t\t\t\tresult.append(NEW_LINE);\n\t\t\t\t// （2）每出现一次后方括号、后花括号；缩进次数减少一次。打印：缩进。\n\t\t\t\tnumber--;\n\t\t\t\tresult.append(indent(number));\n\t\t\t\t// （3）打印：当前字符。\n\t\t\t\tresult.append(key);\n\t\t\t\t// （4）如果当前字符后面还有字符，并且字符不为“，”，打印：换行。\n//\t\t\t\tif (((i + 1) < length) && (json.charAt(i + 1) != ',')) {\n//\t\t\t\t\tresult.append(NEW_LINE);\n//\t\t\t\t}\n\t\t\t\t// （5）继续下一次循环。\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 4、如果当前字符是逗号。逗号后面换行，并缩进，不改变缩进次数。\n\t\t\tif ((key == ',')) {\n\t\t\t\tresult.append(key);\n\t\t\t\tresult.append(NEW_LINE);\n\t\t\t\tresult.append(indent(number));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ((i > 1) && (json.charAt(i - 1) == CharUtil.COLON)) {\n\t\t\t\tresult.append(CharUtil.SPACE);\n\t\t\t}\n\n\t\t\t// 5、打印：当前字符。\n\t\t\tresult.append(key);\n\t\t}\n\n\t\treturn result.toString();\n\t}\n\n\t/**\n\t * 返回指定次数的缩进字符串。每一次缩进4个空格，即SPACE。\n\t *\n\t * @param number 缩进次数。\n\t * @return 指定缩进次数的字符串。\n\t */\n\tprivate static String indent(int number) {\n\t\treturn StrUtil.repeat(SPACE, number);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONString.java",
    "content": "package cn.hutool.json;\n\n/**\n * {@code JSONString}接口定义了一个{@code toJSONString()}<br>\n * 实现此接口的类可以通过实现{@code toJSONString()}方法来改变转JSON字符串的方式。\n *\n * @author Looly\n *\n */\npublic interface JSONString {\n\n\t/**\n\t * 自定义转JSON字符串的方法\n\t *\n\t * @return JSON字符串\n\t */\n\tString toJSONString();\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONSupport.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\n\n/**\n * JSON支持<br>\n * 继承此类实现实体类与JSON的相互转换\n *\n * @author Looly\n */\npublic class JSONSupport implements JSONString, JSONBeanParser<JSON> {\n\n\t/**\n\t * JSON String转Bean\n\t *\n\t * @param jsonString JSON String\n\t */\n\tpublic void parse(String jsonString) {\n\t\tparse(new JSONObject(jsonString));\n\t}\n\n\t/**\n\t * JSON转Bean\n\t *\n\t * @param json JSON\n\t */\n\t@Override\n\tpublic void parse(JSON json) {\n\t\tfinal JSONSupport support = JSONConverter.jsonToBean(getClass(), json, false);\n\t\tBeanUtil.copyProperties(support, this);\n\t}\n\n\t/**\n\t * @return JSON对象\n\t */\n\tpublic JSONObject toJSON() {\n\t\treturn new JSONObject(this);\n\t}\n\n\t@Override\n\tpublic String toJSONString() {\n\t\treturn toJSON().toString();\n\t}\n\n\t/**\n\t * 美化的JSON（使用回车缩进显示JSON），用于打印输出debug\n\t *\n\t * @return 美化的JSON\n\t */\n\tpublic String toPrettyString() {\n\t\treturn toJSON().toStringPretty();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn toJSONString();\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONTokener.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.StringReader;\n\n/**\n * JSON解析器，用于将JSON字符串解析为JSONObject或者JSONArray\n *\n * @author from JSON.org\n */\npublic class JSONTokener {\n\n\tprivate long character;\n\t/**\n\t * 是否结尾 End of stream\n\t */\n\tprivate boolean eof;\n\t/**\n\t * 在Reader的位置（解析到第几个字符）\n\t */\n\tprivate long index;\n\t/**\n\t * 当前所在行\n\t */\n\tprivate long line;\n\t/**\n\t * 前一个字符\n\t */\n\tprivate char previous;\n\t/**\n\t * 是否使用前一个字符\n\t */\n\tprivate boolean usePrevious;\n\t/**\n\t * 源\n\t */\n\tprivate final Reader reader;\n\n\t/**\n\t * JSON配置\n\t */\n\tprivate final JSONConfig config;\n\n\t// ------------------------------------------------------------------------------------ Constructor start\n\n\t/**\n\t * 从Reader中构建\n\t *\n\t * @param reader Reader\n\t * @param config JSON配置\n\t */\n\tpublic JSONTokener(Reader reader, JSONConfig config) {\n\t\tthis.reader = reader.markSupported() ? reader : new BufferedReader(reader);\n\t\tthis.eof = false;\n\t\tthis.usePrevious = false;\n\t\tthis.previous = 0;\n\t\tthis.index = 0;\n\t\tthis.character = 1;\n\t\tthis.line = 1;\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * 从InputStream中构建，使用UTF-8编码\n\t *\n\t * @param inputStream InputStream\n\t * @param config      JSON配置\n\t */\n\tpublic JSONTokener(InputStream inputStream, JSONConfig config) throws JSONException {\n\t\tthis(IoUtil.getUtf8Reader(inputStream), config);\n\t}\n\n\t/**\n\t * 从字符串中构建\n\t *\n\t * @param s      JSON字符串\n\t * @param config JSON配置\n\t */\n\tpublic JSONTokener(CharSequence s, JSONConfig config) {\n\t\tthis(new StringReader(StrUtil.str(s)), config);\n\t}\n\t// ------------------------------------------------------------------------------------ Constructor end\n\n\t/**\n\t * 将标记回退到第一个字符，重新开始解析新的JSON\n\t */\n\tpublic void back() throws JSONException {\n\t\tif (this.usePrevious || this.index <= 0) {\n\t\t\tthrow new JSONException(\"Stepping back two steps is not supported\");\n\t\t}\n\t\tthis.index -= 1;\n\t\tthis.character -= 1;\n\t\tthis.usePrevious = true;\n\t\tthis.eof = false;\n\t}\n\n\t/**\n\t * @return 是否进入结尾\n\t */\n\tpublic boolean end() {\n\t\treturn this.eof && false == this.usePrevious;\n\t}\n\n\t/**\n\t * 源字符串是否有更多的字符\n\t *\n\t * @return 如果未达到结尾返回true，否则false\n\t */\n\tpublic boolean more() throws JSONException {\n\t\tthis.next();\n\t\tif (this.end()) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.back();\n\t\treturn true;\n\t}\n\n\t/**\n\t * 获得源字符串中的下一个字符\n\t *\n\t * @return 下一个字符, or 0 if past the end of the source string.\n\t * @throws JSONException JSON异常，包装IO异常\n\t */\n\tpublic char next() throws JSONException {\n\t\tint c;\n\t\tif (this.usePrevious) {\n\t\t\tthis.usePrevious = false;\n\t\t\tc = this.previous;\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tc = this.reader.read();\n\t\t\t} catch (IOException exception) {\n\t\t\t\tthrow new JSONException(exception);\n\t\t\t}\n\n\t\t\tif (c <= 0) { // End of stream\n\t\t\t\tthis.eof = true;\n\t\t\t\tc = 0;\n\t\t\t}\n\t\t}\n\t\tthis.index += 1;\n\t\tif (this.previous == '\\r') {\n\t\t\tthis.line += 1;\n\t\t\tthis.character = c == '\\n' ? 0 : 1;\n\t\t} else if (c == '\\n') {\n\t\t\tthis.line += 1;\n\t\t\tthis.character = 0;\n\t\t} else {\n\t\t\tthis.character += 1;\n\t\t}\n\t\tthis.previous = (char) c;\n\t\treturn this.previous;\n\t}\n\n\t/**\n\t * Get the last character read from the input or '\\0' if nothing has been read yet.\n\t *\n\t * @return the last character read from the input.\n\t */\n\tprotected char getPrevious() {\n\t\treturn this.previous;\n\t}\n\n\t/**\n\t * 读取下一个字符，并比对是否和指定字符匹配\n\t *\n\t * @param c 被匹配的字符\n\t * @return The character 匹配到的字符\n\t * @throws JSONException 如果不匹配抛出此异常\n\t */\n\tpublic char next(char c) throws JSONException {\n\t\tchar n = this.next();\n\t\tif (n != c) {\n\t\t\tthrow this.syntaxError(\"Expected '\" + c + \"' and instead saw '\" + n + \"'\");\n\t\t}\n\t\treturn n;\n\t}\n\n\t/**\n\t * 获得接下来的n个字符\n\t *\n\t * @param n 字符数\n\t * @return 获得的n个字符组成的字符串\n\t * @throws JSONException 如果源中余下的字符数不足以提供所需的字符数，抛出此异常\n\t */\n\tpublic String next(int n) throws JSONException {\n\t\tif (n == 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tchar[] chars = new char[n];\n\t\tint pos = 0;\n\t\twhile (pos < n) {\n\t\t\tchars[pos] = this.next();\n\t\t\tif (this.end()) {\n\t\t\t\tthrow this.syntaxError(\"Substring bounds error\");\n\t\t\t}\n\t\t\tpos += 1;\n\t\t}\n\t\treturn new String(chars);\n\t}\n\n\t/**\n\t * 获得下一个字符，跳过空白符\n\t *\n\t * @return 获得的字符，0表示没有更多的字符\n\t * @throws JSONException 获得下一个字符时抛出的异常\n\t */\n\tpublic char nextClean() throws JSONException {\n\t\tchar c;\n\t\twhile (true) {\n\t\t\tc = this.next();\n\t\t\tif (c == 0 || c > ' ') {\n\t\t\t\treturn c;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 返回当前位置到指定引号前的所有字符，反斜杠的转义符也会被处理。<br>\n\t * 标准的JSON是不允许使用单引号包含字符串的，但是此实现允许。\n\t *\n\t * @param quote 字符引号, 包括 {@code \"}（双引号） 或 {@code '}（单引号）。\n\t * @return 截止到引号前的字符串\n\t * @throws JSONException 出现无结束的字符串时抛出此异常\n\t */\n\tpublic String nextString(char quote) throws JSONException {\n\t\tchar c;\n\t\tStringBuilder sb = new StringBuilder();\n\t\twhile (true) {\n\t\t\tc = this.next();\n\t\t\tswitch (c) {\n\t\t\t\tcase 0:\n\t\t\t\tcase '\\n':\n\t\t\t\tcase '\\r':\n\t\t\t\t\tthrow this.syntaxError(\"Unterminated string\");\n\t\t\t\tcase '\\\\':// 转义符\n\t\t\t\t\tc = this.next();\n\t\t\t\t\tswitch (c) {\n\t\t\t\t\t\tcase 'b':\n\t\t\t\t\t\t\tsb.append('\\b');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 't':\n\t\t\t\t\t\t\tsb.append('\\t');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'n':\n\t\t\t\t\t\t\tsb.append('\\n');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'f':\n\t\t\t\t\t\t\tsb.append('\\f');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'r':\n\t\t\t\t\t\t\tsb.append('\\r');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'u':// Unicode符\n\t\t\t\t\t\t\tsb.append((char) Integer.parseInt(this.next(4), 16));\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase '\"':\n\t\t\t\t\t\tcase '\\'':\n\t\t\t\t\t\tcase '\\\\':\n\t\t\t\t\t\tcase '/':\n\t\t\t\t\t\t\tsb.append(c);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tthrow this.syntaxError(\"Illegal escape.\");\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tif (c == quote) {\n\t\t\t\t\t\treturn sb.toString();\n\t\t\t\t\t}\n\t\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the text up but not including the specified character or the end of line, whichever comes first. <br>\n\t * 获得从当前位置直到分隔符（不包括分隔符）或行尾的的所有字符。\n\t *\n\t * @param delimiter 分隔符\n\t * @return 字符串\n\t */\n\tpublic String nextTo(char delimiter) throws JSONException {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (; ; ) {\n\t\t\tchar c = this.next();\n\t\t\tif (c == delimiter || c == 0 || c == '\\n' || c == '\\r') {\n\t\t\t\tif (c != 0) {\n\t\t\t\t\tthis.back();\n\t\t\t\t}\n\t\t\t\treturn sb.toString().trim();\n\t\t\t}\n\t\t\tsb.append(c);\n\t\t}\n\t}\n\n\t/**\n\t * Get the text up but not including one of the specified delimiter characters or the end of line, whichever comes first.\n\t *\n\t * @param delimiters A set of delimiter characters.\n\t * @return A string, trimmed.\n\t */\n\tpublic String nextTo(String delimiters) throws JSONException {\n\t\tchar c;\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (; ; ) {\n\t\t\tc = this.next();\n\t\t\tif (delimiters.indexOf(c) >= 0 || c == 0 || c == '\\n' || c == '\\r') {\n\t\t\t\tif (c != 0) {\n\t\t\t\t\tthis.back();\n\t\t\t\t}\n\t\t\t\treturn sb.toString().trim();\n\t\t\t}\n\t\t\tsb.append(c);\n\t\t}\n\t}\n\n\t/**\n\t * 获取下一个String格式的值，用户获取key\n\t * @return String格式的值\n\t * @since 5.8.22\n\t */\n\tpublic String nextStringValue(){\n\t\tchar c = this.nextClean();\n\n\t\tswitch (c) {\n\t\t\tcase '\"':\n\t\t\tcase '\\'':\n\t\t\t\treturn this.nextString(c);\n\t\t\tcase '{':\n\t\t\tcase '[':\n\t\t\t\tthrow this.syntaxError(\"Sting value must be not begin with a '{' or '['\");\n\t\t}\n\n\t\t/*\n\t\t * Handle unquoted text. This could be the values true, false, or null, or it can be a number.\n\t\t * An implementation (such as this one) is allowed to also accept non-standard forms. Accumulate\n\t\t * characters until we reach the end of the text or a formatting character.\n\t\t */\n\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\twhile (c >= ' ' && \",:]}/\\\\\\\"[{;=#\".indexOf(c) < 0) {\n\t\t\tsb.append(c);\n\t\t\tc = this.next();\n\t\t}\n\t\tthis.back();\n\n\t\tfinal String string = sb.toString().trim();\n\t\tif (string.isEmpty()) {\n\t\t\tthrow this.syntaxError(\"Missing value\");\n\t\t}\n\t\treturn string;\n\t}\n\n\t/**\n\t * 获得下一个值，值类型可以是Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL\n\t *\n\t * @return Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL\n\t * @throws JSONException 语法错误\n\t */\n\tpublic Object nextValue() throws JSONException {\n\t\tchar c = this.nextClean();\n\t\tString string;\n\n\t\tswitch (c) {\n\t\t\tcase '\"':\n\t\t\tcase '\\'':\n\t\t\t\treturn this.nextString(c);\n\t\t\tcase '{':\n\t\t\t\tthis.back();\n\t\t\t\ttry {\n\t\t\t\t\treturn new JSONObject(this, this.config);\n\t\t\t\t} catch (final StackOverflowError e) {\n\t\t\t\t\tthrow new JSONException(\"JSONObject depth too large to process.\", e);\n\t\t\t\t}\n\t\t\tcase '[':\n\t\t\t\tthis.back();\n\t\t\t\ttry {\n\t\t\t\t\treturn new JSONArray(this, this.config);\n\t\t\t\t} catch (final StackOverflowError e) {\n\t\t\t\t\tthrow new JSONException(\"JSONArray depth too large to process.\", e);\n\t\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Handle unquoted text. This could be the values true, false, or null, or it can be a number.\n\t\t * An implementation (such as this one) is allowed to also accept non-standard forms. Accumulate\n\t\t * characters until we reach the end of the text or a formatting character.\n\t\t */\n\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\twhile (c >= ' ' && \",:]}/\\\\\\\"[{;=#\".indexOf(c) < 0) {\n\t\t\tsb.append(c);\n\t\t\tc = this.next();\n\t\t}\n\t\tthis.back();\n\n\t\tstring = sb.toString().trim();\n\t\tif (string.isEmpty()) {\n\t\t\tthrow this.syntaxError(\"Missing value\");\n\t\t}\n\t\treturn InternalJSONUtil.stringToValue(string);\n\t}\n\n\t/**\n\t * Skip characters until the next character is the requested character. If the requested character is not found, no characters are skipped. 在遇到指定字符前，跳过其它字符。如果字符未找到，则不跳过任何字符。\n\t *\n\t * @param to 需要定位的字符\n\t * @return 定位的字符，如果字符未找到返回0\n\t */\n\tpublic char skipTo(char to) throws JSONException {\n\t\tchar c;\n\t\ttry {\n\t\t\tlong startIndex = this.index;\n\t\t\tlong startCharacter = this.character;\n\t\t\tlong startLine = this.line;\n\t\t\tthis.reader.mark(1000000);\n\t\t\tdo {\n\t\t\t\tc = this.next();\n\t\t\t\tif (c == 0) {\n\t\t\t\t\tthis.reader.reset();\n\t\t\t\t\tthis.index = startIndex;\n\t\t\t\t\tthis.character = startCharacter;\n\t\t\t\t\tthis.line = startLine;\n\t\t\t\t\treturn c;\n\t\t\t\t}\n\t\t\t} while (c != to);\n\t\t} catch (IOException exception) {\n\t\t\tthrow new JSONException(exception);\n\t\t}\n\t\tthis.back();\n\t\treturn c;\n\t}\n\n\t/**\n\t * Make a JSONException to signal a syntax error. <br>\n\t * 构建 JSONException 用于表示语法错误\n\t *\n\t * @param message 错误消息\n\t * @return A JSONException object, suitable for throwing\n\t */\n\tpublic JSONException syntaxError(String message) {\n\t\treturn new JSONException(message + this);\n\t}\n\n\t/**\n\t * 转为 {@link JSONArray}\n\t *\n\t * @return {@link JSONArray}\n\t */\n\tpublic JSONArray toJSONArray() {\n\t\tJSONArray jsonArray = new JSONArray(this.config);\n\t\tif (this.nextClean() != '[') {\n\t\t\tthrow this.syntaxError(\"A JSONArray text must start with '['\");\n\t\t}\n\t\tif (this.nextClean() != ']') {\n\t\t\tthis.back();\n\t\t\twhile (true) {\n\t\t\t\tif (this.nextClean() == ',') {\n\t\t\t\t\tthis.back();\n\t\t\t\t\tjsonArray.add(JSONNull.NULL);\n\t\t\t\t} else {\n\t\t\t\t\tthis.back();\n\t\t\t\t\tjsonArray.add(this.nextValue());\n\t\t\t\t}\n\t\t\t\tswitch (this.nextClean()) {\n\t\t\t\t\tcase ',':\n\t\t\t\t\t\tif (this.nextClean() == ']') {\n\t\t\t\t\t\t\treturn jsonArray;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.back();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase ']':\n\t\t\t\t\t\treturn jsonArray;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow this.syntaxError(\"Expected a ',' or ']'\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn jsonArray;\n\t}\n\n\t/**\n\t * Make a printable string of this JSONTokener.\n\t *\n\t * @return \" at {index} [character {character} line {line}]\"\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \" at \" + this.index + \" [character \" + this.character + \" line \" + this.line + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/JSONUtil.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.convert.NumberWithFormat;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.file.FileReader;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.MapWrapper;\nimport cn.hutool.core.util.*;\nimport cn.hutool.json.serialize.GlobalSerializeMapping;\nimport cn.hutool.json.serialize.JSONArraySerializer;\nimport cn.hutool.json.serialize.JSONDeserializer;\nimport cn.hutool.json.serialize.JSONObjectSerializer;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\nimport java.sql.SQLException;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.*;\n\n/**\n * JSON工具类\n *\n * @author Looly\n */\npublic class JSONUtil {\n\n\t// -------------------------------------------------------------------- Pause start\n\n\t/**\n\t * 创建JSONObject\n\t *\n\t * @return JSONObject\n\t */\n\tpublic static JSONObject createObj() {\n\t\treturn new JSONObject();\n\t}\n\n\t/**\n\t * 创建JSONObject\n\t *\n\t * @param config JSON配置\n\t * @return JSONObject\n\t * @since 5.2.5\n\t */\n\tpublic static JSONObject createObj(JSONConfig config) {\n\t\treturn new JSONObject(config);\n\t}\n\n\t/**\n\t * 创建 JSONArray\n\t *\n\t * @return JSONArray\n\t */\n\tpublic static JSONArray createArray() {\n\t\treturn new JSONArray();\n\t}\n\n\t/**\n\t * 创建 JSONArray\n\t *\n\t * @param config JSON配置\n\t * @return JSONArray\n\t * @since 5.2.5\n\t */\n\tpublic static JSONArray createArray(JSONConfig config) {\n\t\treturn new JSONArray(config);\n\t}\n\n\t/**\n\t * JSON字符串转JSONObject对象\n\t *\n\t * @param jsonStr JSON字符串\n\t * @return JSONObject\n\t */\n\tpublic static JSONObject parseObj(String jsonStr) {\n\t\treturn new JSONObject(jsonStr);\n\t}\n\n\t/**\n\t * JSON字符串转JSONObject对象<br>\n\t * 此方法会忽略空值，但是对JSON字符串不影响\n\t *\n\t * @param obj Bean对象或者Map\n\t * @return JSONObject\n\t */\n\tpublic static JSONObject parseObj(Object obj) {\n\t\treturn parseObj(obj, null);\n\t}\n\n\t/**\n\t * JSON字符串转JSONObject对象<br>\n\t * 此方法会忽略空值，但是对JSON字符串不影响\n\t *\n\t * @param obj    Bean对象或者Map\n\t * @param config JSON配置\n\t * @return JSONObject\n\t * @since 5.3.1\n\t */\n\tpublic static JSONObject parseObj(Object obj, JSONConfig config) {\n\t\treturn new JSONObject(obj, ObjectUtil.defaultIfNull(config, JSONConfig::create));\n\t}\n\n\t/**\n\t * JSON字符串转JSONObject对象\n\t *\n\t * @param obj             Bean对象或者Map\n\t * @param ignoreNullValue 是否忽略空值，如果source为JSON字符串，不忽略空值\n\t * @return JSONObject\n\t * @since 3.0.9\n\t */\n\tpublic static JSONObject parseObj(Object obj, boolean ignoreNullValue) {\n\t\treturn new JSONObject(obj, ignoreNullValue);\n\t}\n\n\t/**\n\t * JSON字符串转JSONObject对象\n\t *\n\t * @param obj             Bean对象或者Map\n\t * @param ignoreNullValue 是否忽略空值，如果source为JSON字符串，不忽略空值\n\t * @param isOrder         是否有序\n\t * @return JSONObject\n\t * @since 4.2.2\n\t * @deprecated isOrder参数不再有效\n\t */\n\t@SuppressWarnings(\"unused\")\n\t@Deprecated\n\tpublic static JSONObject parseObj(Object obj, boolean ignoreNullValue, boolean isOrder) {\n\t\treturn new JSONObject(obj, ignoreNullValue);\n\t}\n\n\t/**\n\t * JSON字符串转JSONArray\n\t *\n\t * @param jsonStr JSON字符串\n\t * @return JSONArray\n\t */\n\tpublic static JSONArray parseArray(String jsonStr) {\n\t\treturn new JSONArray(jsonStr);\n\t}\n\n\t/**\n\t * JSON字符串转JSONArray\n\t *\n\t * @param arrayOrCollection 数组或集合对象\n\t * @return JSONArray\n\t * @since 3.0.8\n\t */\n\tpublic static JSONArray parseArray(Object arrayOrCollection) {\n\t\treturn parseArray(arrayOrCollection, null);\n\t}\n\n\t/**\n\t * JSON字符串转JSONArray\n\t *\n\t * @param arrayOrCollection 数组或集合对象\n\t * @param config            JSON配置\n\t * @return JSONArray\n\t * @since 5.3.1\n\t */\n\tpublic static JSONArray parseArray(Object arrayOrCollection, JSONConfig config) {\n\t\treturn new JSONArray(arrayOrCollection, config);\n\t}\n\n\t/**\n\t * JSON字符串转JSONArray\n\t *\n\t * @param arrayOrCollection 数组或集合对象\n\t * @param ignoreNullValue   是否忽略空值\n\t * @return JSONArray\n\t * @since 3.2.3\n\t */\n\tpublic static JSONArray parseArray(Object arrayOrCollection, boolean ignoreNullValue) {\n\t\treturn new JSONArray(arrayOrCollection, ignoreNullValue);\n\t}\n\n\t/**\n\t * 转换对象为JSON，如果用户不配置JSONConfig，则JSON的有序与否与传入对象有关。<br>\n\t * 支持的对象：\n\t * <ul>\n\t *     <li>String: 转换为相应的对象</li>\n\t *     <li>Array、Iterable、Iterator：转换为JSONArray</li>\n\t *     <li>Bean对象：转为JSONObject</li>\n\t * </ul>\n\t *\n\t * @param obj 对象\n\t * @return JSON\n\t */\n\tpublic static JSON parse(Object obj) {\n\t\treturn parse(obj, null);\n\t}\n\n\t/**\n\t * 转换对象为JSON，如果用户不配置JSONConfig，则JSON的有序与否与传入对象有关。<br>\n\t * 支持的对象：\n\t * <ul>\n\t *     <li>String: 转换为相应的对象</li>\n\t *     <li>Array、Iterable、Iterator：转换为JSONArray</li>\n\t *     <li>Bean对象：转为JSONObject</li>\n\t * </ul>\n\t *\n\t * @param obj    对象\n\t * @param config JSON配置，{@code null}使用默认配置\n\t * @return JSON\n\t * @since 5.3.1\n\t */\n\tpublic static JSON parse(Object obj, JSONConfig config) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tJSON json;\n\t\tif (obj instanceof JSON) {\n\t\t\tjson = (JSON) obj;\n\t\t} else if (obj instanceof CharSequence) {\n\t\t\tfinal String jsonStr = StrUtil.trim((CharSequence) obj);\n\t\t\tjson = isTypeJSONArray(jsonStr) ? parseArray(jsonStr, config) : parseObj(jsonStr, config);\n\t\t} else if (obj instanceof MapWrapper) {\n\t\t\t// MapWrapper实现了Iterable会被当作JSONArray，此处做修正\n\t\t\tjson = parseObj(obj, config);\n\t\t} else if (obj instanceof Iterable || obj instanceof Iterator || ArrayUtil.isArray(obj)) {// 列表\n\t\t\tjson = parseArray(obj, config);\n\t\t} else {// 对象\n\t\t\tjson = parseObj(obj, config);\n\t\t}\n\n\t\treturn json;\n\t}\n\n\t/**\n\t * XML字符串转为JSONObject\n\t *\n\t * @param xmlStr XML字符串\n\t * @return JSONObject\n\t */\n\tpublic static JSONObject parseFromXml(String xmlStr) {\n\t\treturn XML.toJSONObject(xmlStr);\n\t}\n\n\t// -------------------------------------------------------------------- Parse end\n\n\t// -------------------------------------------------------------------- Read start\n\n\t/**\n\t * 读取JSON\n\t *\n\t * @param file    JSON文件\n\t * @param charset 编码\n\t * @return JSON（包括JSONObject和JSONArray）\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static JSON readJSON(File file, Charset charset) throws IORuntimeException {\n\t\treturn parse(FileReader.create(file, charset).readString());\n\t}\n\n\t/**\n\t * 读取JSONObject\n\t *\n\t * @param file    JSON文件\n\t * @param charset 编码\n\t * @return JSONObject\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static JSONObject readJSONObject(File file, Charset charset) throws IORuntimeException {\n\t\treturn parseObj(FileReader.create(file, charset).readString());\n\t}\n\n\t/**\n\t * 读取JSONArray\n\t *\n\t * @param file    JSON文件\n\t * @param charset 编码\n\t * @return JSONArray\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static JSONArray readJSONArray(File file, Charset charset) throws IORuntimeException {\n\t\treturn parseArray(FileReader.create(file, charset).readString());\n\t}\n\t// -------------------------------------------------------------------- Read end\n\n\t// -------------------------------------------------------------------- toString start\n\n\t/**\n\t * 转为JSON字符串\n\t *\n\t * @param json         JSON\n\t * @param indentFactor 每一级别的缩进\n\t * @return JSON字符串\n\t */\n\tpublic static String toJsonStr(JSON json, int indentFactor) {\n\t\tif (null == json) {\n\t\t\treturn null;\n\t\t}\n\t\treturn json.toJSONString(indentFactor);\n\t}\n\n\t/**\n\t * 转为JSON字符串\n\t *\n\t * @param json JSON\n\t * @return JSON字符串\n\t */\n\tpublic static String toJsonStr(JSON json) {\n\t\tif (null == json) {\n\t\t\treturn null;\n\t\t}\n\t\treturn json.toJSONString(0);\n\t}\n\n\t/**\n\t * 转为JSON字符串，并写出到write\n\t *\n\t * @param json JSON\n\t * @param writer Writer\n\t * @since 5.3.3\n\t */\n\tpublic static void toJsonStr(JSON json, Writer writer) {\n\t\tif (null != json) {\n\t\t\tjson.write(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 转为JSON字符串\n\t *\n\t * @param json JSON\n\t * @return JSON字符串\n\t */\n\tpublic static String toJsonPrettyStr(JSON json) {\n\t\tif (null == json) {\n\t\t\treturn null;\n\t\t}\n\t\treturn json.toJSONString(4);\n\t}\n\n\t/**\n\t * 转换为JSON字符串\n\t *\n\t * @param obj 被转为JSON的对象\n\t * @return JSON字符串\n\t */\n\tpublic static String toJsonStr(Object obj) {\n\t\treturn toJsonStr(obj, (JSONConfig) null);\n\t}\n\n\t/**\n\t * 转换为JSON字符串\n\t *\n\t * @param obj 被转为JSON的对象\n\t * @param jsonConfig JSON配置\n\t * @return JSON字符串\n\t * @since 5.7.12\n\t */\n\tpublic static String toJsonStr(Object obj, JSONConfig jsonConfig) {\n\t\tif (null == obj) {\n\t\t\treturn null;\n\t\t}\n\t\tif (obj instanceof CharSequence) {\n\t\t\treturn StrUtil.str((CharSequence) obj);\n\t\t}else if(obj instanceof Boolean || obj instanceof Number) {\n\t\t\treturn obj.toString();\n\t\t}\n\t\treturn toJsonStr(parse(obj, jsonConfig));\n\t}\n\n\t/**\n\t * 转换为JSON字符串并写出到writer\n\t *\n\t * @param obj 被转为JSON的对象\n\t * @param writer Writer\n\t * @since 5.3.3\n\t */\n\tpublic static void toJsonStr(Object obj, Writer writer) {\n\t\tif (null != obj) {\n\t\t\ttoJsonStr(parse(obj), writer);\n\t\t}\n\t}\n\n\t/**\n\t * 转换为格式化后的JSON字符串\n\t *\n\t * @param obj Bean对象\n\t * @return JSON字符串\n\t */\n\tpublic static String toJsonPrettyStr(Object obj) {\n\t\treturn toJsonPrettyStr(parse(obj));\n\t}\n\n\t/**\n\t * 转换为XML字符串\n\t *\n\t * @param json JSON\n\t * @return XML字符串\n\t */\n\tpublic static String toXmlStr(JSON json) {\n\t\treturn XML.toXml(json);\n\t}\n\t// -------------------------------------------------------------------- toString end\n\n\t// -------------------------------------------------------------------- toBean start\n\n\t/**\n\t * JSON字符串转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>        Bean类型\n\t * @param jsonString JSON字符串\n\t * @param beanClass  实体类对象\n\t * @return 实体类对象\n\t * @since 3.1.2\n\t */\n\tpublic static <T> T toBean(String jsonString, Class<T> beanClass) {\n\t\treturn toBean(parseObj(jsonString), beanClass);\n\t}\n\n\t/**\n\t * JSON字符串转为实体类对象，转换异常将被抛出<br>\n\t * 通过{@link JSONConfig}可选是否忽略大小写、忽略null等配置\n\t *\n\t * @param <T>        Bean类型\n\t * @param jsonString JSON字符串\n\t * @param config     JSON配置\n\t * @param beanClass  实体类对象\n\t * @return 实体类对象\n\t * @since 5.8.0\n\t */\n\tpublic static <T> T toBean(String jsonString, JSONConfig config, Class<T> beanClass) {\n\t\treturn toBean(parseObj(jsonString, config), beanClass);\n\t}\n\n\t/**\n\t * 转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>       Bean类型\n\t * @param json      JSONObject\n\t * @param beanClass 实体类对象\n\t * @return 实体类对象\n\t */\n\tpublic static <T> T toBean(JSONObject json, Class<T> beanClass) {\n\t\treturn null == json ? null : json.toBean(beanClass);\n\t}\n\n\t/**\n\t * JSON字符串转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>           Bean类型\n\t * @param jsonString    JSON字符串\n\t * @param typeReference {@link TypeReference}类型参考子类，可以获取其泛型参数中的Type类型\n\t * @param ignoreError   是否忽略错误\n\t * @return 实体类对象\n\t * @since 4.3.2\n\t */\n\tpublic static <T> T toBean(String jsonString, TypeReference<T> typeReference, boolean ignoreError) {\n\t\treturn toBean(jsonString, typeReference.getType(), ignoreError);\n\t}\n\n\t/**\n\t * JSON字符串转为实体类对象，转换异常将被抛出\n\t *\n\t * @param <T>         Bean类型\n\t * @param jsonString  JSON字符串\n\t * @param beanType    实体类对象类型\n\t * @param ignoreError 是否忽略错误\n\t * @return 实体类对象\n\t * @since 4.3.2\n\t */\n\tpublic static <T> T toBean(String jsonString, Type beanType, boolean ignoreError) {\n\t\tfinal JSON json = parse(jsonString, JSONConfig.create().setIgnoreError(ignoreError));\n\t\tif(null == json){\n\t\t\treturn null;\n\t\t}\n\t\treturn json.toBean(beanType);\n\t}\n\n\t/**\n\t * 转为实体类对象\n\t *\n\t * @param <T>           Bean类型\n\t * @param json          JSONObject\n\t * @param typeReference {@link TypeReference}类型参考子类，可以获取其泛型参数中的Type类型\n\t * @param ignoreError   是否忽略转换错误\n\t * @return 实体类对象\n\t * @since 4.6.2\n\t */\n\tpublic static <T> T toBean(JSON json, TypeReference<T> typeReference, boolean ignoreError) {\n\t\treturn toBean(json, typeReference.getType(), ignoreError);\n\t}\n\n\t/**\n\t * 转为实体类对象\n\t *\n\t * @param <T>         Bean类型\n\t * @param json        JSONObject\n\t * @param beanType    实体类对象类型\n\t * @param ignoreError 是否忽略转换错误\n\t * @return 实体类对象\n\t * @since 4.3.2\n\t */\n\t@SuppressWarnings(\"deprecation\")\n\tpublic static <T> T toBean(JSON json, Type beanType, boolean ignoreError) {\n\t\tif (null == json) {\n\t\t\treturn null;\n\t\t}\n\t\treturn json.toBean(beanType, ignoreError);\n\t}\n\t// -------------------------------------------------------------------- toBean end\n\n\t/**\n\t * 将JSONArray字符串转换为Bean的List，默认为ArrayList\n\t *\n\t * @param <T>         Bean类型\n\t * @param jsonArray   JSONArray字符串\n\t * @param elementType List中元素类型\n\t * @return List\n\t * @since 5.5.2\n\t */\n\tpublic static <T> List<T> toList(String jsonArray, Class<T> elementType) {\n\t\treturn toList(parseArray(jsonArray), elementType);\n\t}\n\n\t/**\n\t * 将JSONArray转换为Bean的List，默认为ArrayList\n\t *\n\t * @param <T>         Bean类型\n\t * @param jsonArray   {@link JSONArray}\n\t * @param elementType List中元素类型\n\t * @return List\n\t * @since 4.0.7\n\t */\n\tpublic static <T> List<T> toList(JSONArray jsonArray, Class<T> elementType) {\n\t\treturn null == jsonArray ? null : jsonArray.toList(elementType);\n\t}\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param json       {@link JSON}\n\t * @param expression 表达式\n\t * @return 对象\n\t * @see JSON#getByPath(String)\n\t */\n\tpublic static Object getByPath(JSON json, String expression) {\n\t\treturn getByPath(json, expression, null);\n\t}\n\n\t/**\n\t * 通过表达式获取JSON中嵌套的对象<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param <T> 值类型\n\t * @param json       {@link JSON}\n\t * @param expression 表达式\n\t * @param defaultValue 默认值\n\t * @return 对象\n\t * @see JSON#getByPath(String)\n\t * @since 5.6.0\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getByPath(JSON json, String expression, T defaultValue) {\n\t\tif((null == json || StrUtil.isBlank(expression))){\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\tif(null != defaultValue){\n\t\t\tfinal Class<T> type = (Class<T>) defaultValue.getClass();\n\t\t\treturn ObjectUtil.defaultIfNull(json.getByPath(expression, type), defaultValue);\n\t\t}\n\t\treturn (T) json.getByPath(expression);\n\t}\n\n\t/**\n\t * 设置表达式指定位置（或filed对应）的值<br>\n\t * 若表达式指向一个JSONArray则设置其坐标对应位置的值，若指向JSONObject则put对应key的值<br>\n\t * 注意：如果为JSONArray，则设置值得下标不能大于已有JSONArray的长度<br>\n\t * <ol>\n\t * <li>.表达式，可以获取Bean对象中的属性（字段）值或者Map中key对应的值</li>\n\t * <li>[]表达式，可以获取集合等对象中对应index的值</li>\n\t * </ol>\n\t * <p>\n\t * 表达式栗子：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * </pre>\n\t *\n\t * @param json       JSON，可以为JSONObject或JSONArray\n\t * @param expression 表达式\n\t * @param value      值\n\t */\n\tpublic static void putByPath(JSON json, String expression, Object value) {\n\t\tjson.putByPath(expression, value);\n\t}\n\n\t/**\n\t * 对所有双引号做转义处理（使用双反斜杠做转义）<br>\n\t * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\\/<br>\n\t * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠\n\t *\n\t * @param string 字符串\n\t * @return 适合在JSON中显示的字符串\n\t */\n\tpublic static String quote(String string) {\n\t\treturn quote(string, true);\n\t}\n\n\t/**\n\t * 对所有双引号做转义处理（使用双反斜杠做转义）<br>\n\t * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\\/<br>\n\t * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠\n\t *\n\t * @param string 字符串\n\t * @param isWrap 是否使用双引号包装字符串\n\t * @return 适合在JSON中显示的字符串\n\t * @since 3.3.1\n\t */\n\tpublic static String quote(String string, boolean isWrap) {\n\t\tStringWriter sw = new StringWriter();\n\t\ttry {\n\t\t\treturn quote(string, sw, isWrap).toString();\n\t\t} catch (IOException ignored) {\n\t\t\t// will never happen - we are writing to a string writer\n\t\t\treturn StrUtil.EMPTY;\n\t\t}\n\t}\n\n\t/**\n\t * 对所有双引号做转义处理（使用双反斜杠做转义）<br>\n\t * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\\/<br>\n\t * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠\n\t *\n\t * @param str    字符串\n\t * @param writer Writer\n\t * @return Writer\n\t * @throws IOException IO异常\n\t */\n\tpublic static Writer quote(String str, Writer writer) throws IOException {\n\t\treturn quote(str, writer, true);\n\t}\n\n\t/**\n\t * 对所有双引号做转义处理（使用双反斜杠做转义）<br>\n\t * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\\/<br>\n\t * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠\n\t *\n\t * @param str    字符串\n\t * @param writer Writer\n\t * @param isWrap 是否使用双引号包装字符串\n\t * @return Writer\n\t * @throws IOException IO异常\n\t * @since 3.3.1\n\t */\n\tpublic static Writer quote(String str, Writer writer, boolean isWrap) throws IOException {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\tif (isWrap) {\n\t\t\t\twriter.write(\"\\\"\\\"\");\n\t\t\t}\n\t\t\treturn writer;\n\t\t}\n\n\t\tchar c; // 当前字符\n\t\tint len = str.length();\n\t\tif (isWrap) {\n\t\t\twriter.write('\"');\n\t\t}\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tswitch (c) {\n\t\t\t\tcase '\\\\':\n\t\t\t\tcase '\"':\n\t\t\t\t\twriter.write(\"\\\\\");\n\t\t\t\t\twriter.write(c);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\twriter.write(escape(c));\n\t\t\t}\n\t\t}\n\t\tif (isWrap) {\n\t\t\twriter.write('\"');\n\t\t}\n\t\treturn writer;\n\t}\n\n\t/**\n\t * 转义显示不可见字符\n\t *\n\t * @param str 字符串\n\t * @return 转义后的字符串\n\t */\n\tpublic static String escape(String str) {\n\t\tif (StrUtil.isEmpty(str)) {\n\t\t\treturn str;\n\t\t}\n\n\t\tfinal int len = str.length();\n\t\tfinal StringBuilder builder = new StringBuilder(len);\n\t\tchar c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = str.charAt(i);\n\t\t\tbuilder.append(escape(c));\n\t\t}\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 在需要的时候包装对象<br>\n\t * 包装包括：\n\t * <ul>\n\t * <li>{@code null} =》 {@code JSONNull.NULL}</li>\n\t * <li>array or collection =》 JSONArray</li>\n\t * <li>map =》 JSONObject</li>\n\t * <li>standard property (Double, String, et al) =》 原对象</li>\n\t * <li>来自于java包 =》 字符串</li>\n\t * <li>其它 =》 尝试包装为JSONObject，如果{@link JSONConfig#isIgnoreError()}为true时返回{@code null}, 否则抛出异常</li>\n\t * </ul>\n\t *\n\t * @param object     被包装的对象\n\t * @param jsonConfig JSON选项\n\t * @return 包装后的值，null表示此值需被忽略\n\t */\n\tpublic static Object wrap(Object object, JSONConfig jsonConfig) {\n\t\tif (object == null) {\n\t\t\treturn jsonConfig.isIgnoreNullValue() ? null : JSONNull.NULL;\n\t\t}\n\t\tif (object instanceof JSON //\n\t\t\t\t|| ObjectUtil.isNull(object) //\n\t\t\t\t|| object instanceof JSONString //\n\t\t\t\t|| object instanceof CharSequence //\n\t\t\t\t|| object instanceof Number //\n\t\t\t\t|| ObjectUtil.isBasicType(object) //\n\t\t) {\n\t\t\tif(object instanceof Number && null != jsonConfig.getDateFormat()){\n\t\t\t\t// 当JSONConfig中设置了日期格式，则包装为NumberWithFormat，以便在Converter中使用自定义格式转换日期时间\n\t\t\t\treturn new NumberWithFormat((Number) object, jsonConfig.getDateFormat());\n\t\t\t}\n\t\t\treturn object;\n\t\t}\n\n\t\ttry {\n\t\t\t// fix issue#1399@Github\n\t\t\tif(object instanceof SQLException){\n\t\t\t\treturn object.toString();\n\t\t\t}\n\n\t\t\t// JSONArray\n\t\t\tif (object instanceof Iterable || ArrayUtil.isArray(object)) {\n\t\t\t\treturn new JSONArray(object, jsonConfig);\n\t\t\t}\n\t\t\t// JSONObject\n\t\t\tif (object instanceof Map || object instanceof Map.Entry) {\n\t\t\t\treturn new JSONObject(object, jsonConfig);\n\t\t\t}\n\n\t\t\t// 日期类型原样保存，便于格式化\n\t\t\tif (object instanceof Date\n\t\t\t\t\t|| object instanceof Calendar\n\t\t\t\t\t|| object instanceof TemporalAccessor\n\t\t\t) {\n\t\t\t\treturn object;\n\t\t\t}\n\t\t\t// 枚举类保存其字符串形式（4.0.2新增）\n\t\t\tif (object instanceof Enum) {\n\t\t\t\treturn object.toString();\n\t\t\t}\n\n\t\t\t// pr#3507\n\t\t\t// Class类型保存类名\n\t\t\tif (object instanceof Class<?>) {\n\t\t\t\treturn ((Class<?>) object).getName();\n\t\t\t}\n\n\t\t\t// Java内部类不做转换\n\t\t\tif (ClassUtil.isJdkClass(object.getClass())) {\n\t\t\t\treturn object.toString();\n\t\t\t}\n\n\t\t\t// 默认按照JSONObject对待\n\t\t\treturn new JSONObject(object, jsonConfig);\n\t\t} catch (final Exception exception) {\n\t\t\t// issue#4210 只有设置忽略错误时才返回null\n\t\t\tif(jsonConfig.isIgnoreError()){\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tthrow exception;\n\t\t}\n\t}\n\n\t/**\n\t * 格式化JSON字符串，此方法并不严格检查JSON的格式正确与否\n\t *\n\t * @param jsonStr JSON字符串\n\t * @return 格式化后的字符串\n\t * @since 3.1.2\n\t */\n\tpublic static String formatJsonStr(String jsonStr) {\n\t\treturn JSONStrFormatter.format(jsonStr);\n\t}\n\n\t/**\n\t * 是否为JSON字符串，首尾都为大括号或中括号判定为JSON字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSON字符串\n\t * @since 3.3.0\n\t * @deprecated 方法名称有歧义，请使用 {@link #isTypeJSON(String)}\n\t */\n\t@Deprecated\n\tpublic static boolean isJson(String str) {\n\t\treturn isTypeJSON(str);\n\t}\n\n\t/**\n\t * 是否为JSON类型字符串，首尾都为大括号或中括号判定为JSON字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSON类型字符串\n\t * @since 5.7.22\n\t */\n\tpublic static boolean isTypeJSON(String str) {\n\t\treturn isTypeJSONObject(str) || isTypeJSONArray(str);\n\t}\n\n\t/**\n\t * 是否为JSONObject字符串，首尾都为大括号判定为JSONObject字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSON字符串\n\t * @since 3.3.0\n\t * @deprecated 方法名称有歧义，请使用 {@link #isTypeJSONObject(String)}\n\t */\n\t@Deprecated\n\tpublic static boolean isJsonObj(String str) {\n\t\treturn isTypeJSONObject(str);\n\t}\n\n\t/**\n\t * 是否为JSONObject类型字符串，首尾都为大括号判定为JSONObject字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSON字符串\n\t * @since 5.7.22\n\t */\n\tpublic static boolean isTypeJSONObject(String str) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn StrUtil.isWrap(StrUtil.trim(str), '{', '}');\n\t}\n\n\t/**\n\t * 是否为JSONArray字符串，首尾都为中括号判定为JSONArray字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSON字符串\n\t * @since 3.3.0\n\t * @deprecated 方法名称有歧义，请使用 {@link #isTypeJSONArray(String)}\n\t */\n\t@Deprecated\n\tpublic static boolean isJsonArray(String str) {\n\t\treturn isTypeJSONArray(str);\n\t}\n\n\t/**\n\t * 是否为JSONArray类型的字符串，首尾都为中括号判定为JSONArray字符串\n\t *\n\t * @param str 字符串\n\t * @return 是否为JSONArray类型字符串\n\t * @since 5.7.22\n\t */\n\tpublic static boolean isTypeJSONArray(String str) {\n\t\tif (StrUtil.isBlank(str)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn StrUtil.isWrap(StrUtil.trim(str), '[', ']');\n\t}\n\n\t/**\n\t * 是否为null对象，null的情况包括：\n\t *\n\t * <pre>\n\t * 1. {@code null}\n\t * 2. {@link JSONNull}\n\t * </pre>\n\t *\n\t * @param obj 对象\n\t * @return 是否为null\n\t * @since 4.5.7\n\t */\n\tpublic static boolean isNull(Object obj) {\n\t\treturn null == obj || obj instanceof JSONNull;\n\t}\n\n\t/**\n\t * XML转JSONObject<br>\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t *\n\t * @param xml XML字符串\n\t * @return JSONObject\n\t * @since 4.0.8\n\t */\n\tpublic static JSONObject xmlToJson(String xml) {\n\t\treturn XML.toJSONObject(xml);\n\t}\n\n\t/**\n\t * 加入自定义的序列化器\n\t *\n\t * @param type       对象类型\n\t * @param serializer 序列化器实现\n\t * @see GlobalSerializeMapping#put(Type, JSONArraySerializer)\n\t * @since 4.6.5\n\t */\n\tpublic static void putSerializer(Type type, JSONArraySerializer<?> serializer) {\n\t\tGlobalSerializeMapping.put(type, serializer);\n\t}\n\n\t/**\n\t * 加入自定义的序列化器\n\t *\n\t * @param type       对象类型\n\t * @param serializer 序列化器实现\n\t * @see GlobalSerializeMapping#put(Type, JSONObjectSerializer)\n\t * @since 4.6.5\n\t */\n\tpublic static void putSerializer(Type type, JSONObjectSerializer<?> serializer) {\n\t\tGlobalSerializeMapping.put(type, serializer);\n\t}\n\n\t/**\n\t * 加入自定义的反序列化器\n\t *\n\t * @param type         对象类型\n\t * @param deserializer 反序列化器实现\n\t * @see GlobalSerializeMapping#put(Type, JSONDeserializer)\n\t * @since 4.6.5\n\t */\n\tpublic static void putDeserializer(Type type, JSONDeserializer<?> deserializer) {\n\t\tGlobalSerializeMapping.put(type, deserializer);\n\t}\n\n\t// --------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 转义不可见字符<br>\n\t * 见：<a href=\"https://en.wikibooks.org/wiki/Unicode/Character_reference/0000-0FFF\">https://en.wikibooks.org/wiki/Unicode/Character_reference/0000-0FFF</a>\n\t *\n\t * @param c 字符\n\t * @return 转义后的字符串\n\t */\n\tprivate static String escape(char c) {\n\t\tswitch (c) {\n\t\t\tcase '\\b':\n\t\t\t\treturn \"\\\\b\";\n\t\t\tcase '\\t':\n\t\t\t\treturn \"\\\\t\";\n\t\t\tcase '\\n':\n\t\t\t\treturn \"\\\\n\";\n\t\t\tcase '\\f':\n\t\t\t\treturn \"\\\\f\";\n\t\t\tcase '\\r':\n\t\t\t\treturn \"\\\\r\";\n\t\t\tdefault:\n\t\t\t\tif (c < StrUtil.C_SPACE || //\n\t\t\t\t\t\t(c >= '\\u0080' && c <= '\\u00a0') || //\n\t\t\t\t\t\t(c >= '\\u2000' && c <= '\\u2010') || //\n\t\t\t\t\t\t(c >= '\\u2028' && c <= '\\u202F') || //\n\t\t\t\t\t\t(c >= '\\u2066' && c <= '\\u206F')//\n\t\t\t\t) {\n\t\t\t\t\treturn HexUtil.toUnicodeHex(c);\n\t\t\t\t} else {\n\t\t\t\t\treturn Character.toString(c);\n\t\t\t\t}\n\t\t}\n\t}\n\t// --------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/ObjectMapper.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.RecordUtil;\nimport cn.hutool.core.collection.ArrayIter;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.mutable.Mutable;\nimport cn.hutool.core.lang.mutable.MutablePair;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.TypeUtil;\nimport cn.hutool.json.serialize.GlobalSerializeMapping;\nimport cn.hutool.json.serialize.JSONObjectSerializer;\nimport cn.hutool.json.serialize.JSONSerializer;\n\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.util.Enumeration;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\n/**\n * 对象和JSON映射器，用于转换对象为JSON，支持：\n * <ul>\n *     <li>Map 转 JSONObject，将键值对加入JSON对象</li>\n *     <li>Map.Entry 转 JSONObject</li>\n *     <li>CharSequence 转 JSONObject，使用JSONTokener解析</li>\n *     <li>{@link Reader} 转 JSONObject，使用JSONTokener解析</li>\n *     <li>{@link InputStream} 转 JSONObject，使用JSONTokener解析</li>\n *     <li>JSONTokener 转 JSONObject，直接解析</li>\n *     <li>ResourceBundle 转 JSONObject</li>\n *     <li>Bean 转 JSONObject，调用其getters方法（getXXX或者isXXX）获得值，加入到JSON对象。例如：如果JavaBean对象中有个方法getName()，值为\"张三\"，获得的键值对为：name: \"张三\"</li>\n * </ul>\n *\n * @author looly\n * @since 5.8.0\n */\npublic class ObjectMapper {\n\n\t/**\n\t * 创建ObjectMapper\n\t *\n\t * @param source 来源对象\n\t * @return ObjectMapper\n\t */\n\tpublic static ObjectMapper of(Object source) {\n\t\treturn new ObjectMapper(source);\n\t}\n\n\tprivate final Object source;\n\n\t/**\n\t * 构造\n\t *\n\t * @param source 来源对象\n\t */\n\tpublic ObjectMapper(Object source) {\n\t\tthis.source = source;\n\t}\n\n\t/**\n\t * 将给定对象转换为{@link JSONObject}\n\t *\n\t * @param jsonObject 目标{@link JSONObject}\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic void map(JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {\n\t\tfinal Object source = this.source;\n\t\tif (null == source) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 自定义序列化\n\t\tfinal JSONSerializer serializer = GlobalSerializeMapping.getSerializer(source.getClass());\n\t\tif (serializer instanceof JSONObjectSerializer) {\n\t\t\tserializer.serialize(jsonObject, source);\n\t\t\treturn;\n\t\t}\n\n\t\tif (source instanceof JSONArray) {\n\t\t\t// 不支持集合类型转换为JSONObject\n\t\t\tthrow new JSONException(\"Unsupported type [{}] to JSONObject!\", source.getClass());\n\t\t}\n\n\t\tif (source instanceof Map) {\n\t\t\t// Map\n\t\t\tfor (final Map.Entry<?, ?> e : ((Map<?, ?>) source).entrySet()) {\n\t\t\t\tjsonObject.set(Convert.toStr(e.getKey()), e.getValue(), filter, jsonObject.getConfig().isCheckDuplicate());\n\t\t\t}\n\t\t} else if (source instanceof Map.Entry) {\n\t\t\tfinal Map.Entry entry = (Map.Entry) source;\n\t\t\tjsonObject.set(Convert.toStr(entry.getKey()), entry.getValue(), filter, jsonObject.getConfig().isCheckDuplicate());\n\t\t} else if (source instanceof CharSequence) {\n\t\t\t// 可能为JSON字符串\n\t\t\tmapFromStr((CharSequence) source, jsonObject, filter);\n\t\t} else if (source instanceof Reader) {\n\t\t\tmapFromTokener(new JSONTokener((Reader) source, jsonObject.getConfig()), jsonObject, filter);\n\t\t} else if (source instanceof InputStream) {\n\t\t\tmapFromTokener(new JSONTokener((InputStream) source, jsonObject.getConfig()), jsonObject, filter);\n\t\t} else if (source instanceof byte[]) {\n\t\t\tmapFromTokener(new JSONTokener(IoUtil.toStream((byte[]) source), jsonObject.getConfig()), jsonObject, filter);\n\t\t} else if (source instanceof JSONTokener) {\n\t\t\t// JSONTokener\n\t\t\tmapFromTokener((JSONTokener) source, jsonObject, filter);\n\t\t} else if (source instanceof ResourceBundle) {\n\t\t\t// JSONTokener\n\t\t\tmapFromResourceBundle((ResourceBundle) source, jsonObject, filter);\n\t\t} else if (RecordUtil.isRecord(source.getClass())) {\n\t\t\tmapFromBean(source, jsonObject,filter);\n\t\t} else if (BeanUtil.isReadableBean(source.getClass())) {\n\t\t\t// 普通Bean\n\t\t\tmapFromBean(source, jsonObject,filter);\n\t\t}\n\n\t\t// 跳过空对象\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param jsonArray 目标{@link JSONArray}\n\t * @param filter    键值对过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null}表示不过滤\n\t * @throws JSONException 非数组或集合\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic void map(JSONArray jsonArray, Filter<Mutable<Object>> filter) throws JSONException {\n\t\tfinal Object source = this.source;\n\t\tif (null == source) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal JSONSerializer serializer = GlobalSerializeMapping.getSerializer(source.getClass());\n\t\tif (null != serializer && JSONArray.class.equals(TypeUtil.getTypeArgument(serializer.getClass()))) {\n\t\t\t// 自定义序列化\n\t\t\tserializer.serialize(jsonArray, source);\n\t\t} else if (source instanceof CharSequence) {\n\t\t\t// JSON字符串\n\t\t\tmapFromStr((CharSequence) source, jsonArray, filter);\n\t\t}else if (source instanceof Reader) {\n\t\t\tmapFromTokener(new JSONTokener((Reader) source, jsonArray.getConfig()), jsonArray, filter);\n\t\t} else if (source instanceof InputStream) {\n\t\t\tmapFromTokener(new JSONTokener((InputStream) source, jsonArray.getConfig()), jsonArray, filter);\n\t\t} else if (source instanceof byte[]) {\n\t\t\tfinal byte[] bytesSource = (byte[]) source;\n\t\t\t// 如果是普通的的byte[], 要避免下标越界\n\t\t\tif (bytesSource.length > 1 && '[' == bytesSource[0] && ']' == bytesSource[bytesSource.length - 1]) {\n\t\t\t\tmapFromTokener(new JSONTokener(IoUtil.toStream(bytesSource), jsonArray.getConfig()), jsonArray, filter);\n\t\t\t}else{\n\t\t\t\t// https://github.com/chinabugotech/hutool/issues/2369\n\t\t\t\t// 非标准的二进制流，则按照普通数组对待\n\t\t\t\tfor(final byte b : bytesSource){\n\t\t\t\t\tjsonArray.add(b);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (source instanceof JSONTokener) {\n\t\t\tmapFromTokener((JSONTokener) source, jsonArray, filter);\n\t\t} else {\n\t\t\tfinal Iterator<?> iter;\n\t\t\tif (ArrayUtil.isArray(source)) {// 数组\n\t\t\t\titer = new ArrayIter<>(source);\n\t\t\t} else if (source instanceof Iterator<?>) {// Iterator\n\t\t\t\titer = ((Iterator<?>) source);\n\t\t\t} else if (source instanceof Iterable<?>) {// Iterable\n\t\t\t\titer = ((Iterable<?>) source).iterator();\n\t\t\t} else {\n\t\t\t\tif(false == jsonArray.getConfig().isIgnoreError()){\n\t\t\t\t\tthrow new JSONException(\"JSONArray initial value should be a string or collection or array.\");\n\t\t\t\t}\n\t\t\t\t// 如果用户选择跳过异常，则跳过此值转换\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfinal JSONConfig config = jsonArray.getConfig();\n\t\t\tObject next;\n\t\t\twhile (iter.hasNext()) {\n\t\t\t\tnext = iter.next();\n\t\t\t\t// 检查循环引用\n\t\t\t\tif (next != source) {\n\t\t\t\t\tjsonArray.addRaw(JSONUtil.wrap(next, config), filter);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从{@link ResourceBundle}转换\n\t *\n\t * @param bundle     ResourceBundle\n\t * @param jsonObject {@link JSONObject}\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t * @since 5.3.1\n\t */\n\tprivate static void mapFromResourceBundle(ResourceBundle bundle, JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {\n\t\tEnumeration<String> keys = bundle.getKeys();\n\t\twhile (keys.hasMoreElements()) {\n\t\t\tString key = keys.nextElement();\n\t\t\tif (key != null) {\n\t\t\t\tInternalJSONUtil.propertyPut(jsonObject, key, bundle.getString(key), filter);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 从字符串转换\n\t *\n\t * @param source     JSON字符串\n\t * @param jsonObject {@link JSONObject}\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作，{@code null}表示不过滤\n\t */\n\tprivate static void mapFromStr(CharSequence source, JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {\n\t\tfinal String jsonStr = StrUtil.trim(source);\n\t\tif (StrUtil.startWith(jsonStr, '<')) {\n\t\t\t// 可能为XML\n\t\t\tXML.toJSONObject(jsonObject, jsonStr, false);\n\t\t\treturn;\n\t\t}\n\t\tmapFromTokener(new JSONTokener(jsonStr, jsonObject.getConfig()), jsonObject, filter);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param source    JSON字符串\n\t * @param jsonArray {@link JSONArray}\n\t * @param filter    值过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null}表示不过滤\n\t */\n\tprivate void mapFromStr(CharSequence source, JSONArray jsonArray, Filter<Mutable<Object>> filter) {\n\t\tif (null != source) {\n\t\t\tmapFromTokener(new JSONTokener(StrUtil.trim(source), jsonArray.getConfig()), jsonArray, filter);\n\t\t}\n\t}\n\n\t/**\n\t * 从{@link JSONTokener}转换\n\t *\n\t * @param x          JSONTokener\n\t * @param jsonObject {@link JSONObject}\n\t * @param filter     键值对过滤编辑器，可以通过实现此接口，完成解析前对键值对的过滤和修改操作\n\t */\n\tprivate static void mapFromTokener(JSONTokener x, JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {\n\t\tJSONParser.of(x).parseTo(jsonObject, filter);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param x         {@link JSONTokener}\n\t * @param jsonArray {@link JSONArray}\n\t * @param filter    值过滤编辑器，可以通过实现此接口，完成解析前对值的过滤和修改操作，{@code null}表示不过滤\n\t */\n\tprivate static void mapFromTokener(JSONTokener x, JSONArray jsonArray, Filter<Mutable<Object>> filter) {\n\t\tJSONParser.of(x).parseTo(jsonArray, filter);\n\t}\n\n\t/**\n\t * 从Bean转换\n\t *\n\t * @param bean       Bean对象\n\t * @param jsonObject {@link JSONObject}\n\t */\n\t@Deprecated\n\tprivate static void mapFromBean(Object bean, JSONObject jsonObject) {\n\t\tBeanUtil.beanToMap(bean, jsonObject, InternalJSONUtil.toCopyOptions(jsonObject.getConfig()));\n\t}\n\n\t/**\n\t * 从Bean转换（可添加过滤器）\n\t * @param bean Bean对象\n\t * @param jsonObject {@link JSONObject}\n\t * @param filter {@link Filter}\n\t */\n\tprivate static void mapFromBean(Object bean, JSONObject jsonObject,Filter<MutablePair<String,Object>> filter) {\n\t\tBeanUtil.beanToMap(bean, jsonObject, InternalJSONUtil.toCopyOptions(jsonObject.getConfig(), filter));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/XML.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.json.xml.JSONXMLParser;\nimport cn.hutool.json.xml.JSONXMLSerializer;\nimport cn.hutool.json.xml.ParseConfig;\n\n/**\n * 提供静态方法在XML和JSONObject之间转换\n *\n * @author JSON.org, looly\n * @see JSONXMLParser\n * @see JSONXMLSerializer\n */\npublic class XML {\n\n\t/**\n\t * The Character '&amp;'.\n\t */\n\tpublic static final Character AMP = CharUtil.AMP;\n\n\t/**\n\t * The Character '''.\n\t */\n\tpublic static final Character APOS = CharUtil.SINGLE_QUOTE;\n\n\t/**\n\t * The Character '!'.\n\t */\n\tpublic static final Character BANG = '!';\n\n\t/**\n\t * The Character '='.\n\t */\n\tpublic static final Character EQ = '=';\n\n\t/**\n\t * The Character '&gt;'.\n\t */\n\tpublic static final Character GT = '>';\n\n\t/**\n\t * The Character '&lt;'.\n\t */\n\tpublic static final Character LT = '<';\n\n\t/**\n\t * The Character '?'.\n\t */\n\tpublic static final Character QUEST = '?';\n\n\t/**\n\t * The Character '\"'.\n\t */\n\tpublic static final Character QUOT = CharUtil.DOUBLE_QUOTES;\n\n\t/**\n\t * The Character '/'.\n\t */\n\tpublic static final Character SLASH = CharUtil.SLASH;\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t * Content text may be placed in a \"content\" member. Comments, prologs, DTDs, and {@code <[ [ ]]>} are ignored.\n\t *\n\t * @param string The source string.\n\t * @return A JSONObject containing the structured data from the XML string.\n\t * @throws JSONException Thrown if there is an errors while parsing the string\n\t */\n\tpublic static JSONObject toJSONObject(String string) throws JSONException {\n\t\treturn toJSONObject(string, false);\n\t}\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t * Content text may be placed in a \"content\" member. Comments, prologs, DTDs, and {@code <[ [ ]]>} are ignored.\n\t * All values are converted as strings, for 1, 01, 29.0 will not be coerced to numbers but will instead be the exact value as seen in the XML document.\n\t *\n\t * @param string      The source string.\n\t * @param keepStrings If true, then values will not be coerced into boolean or numeric values and will instead be left as strings\n\t * @return A JSONObject containing the structured data from the XML string.\n\t * @throws JSONException Thrown if there is an errors while parsing the string\n\t */\n\tpublic static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException {\n\t\treturn toJSONObject(new JSONObject(), string, keepStrings);\n\t}\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t * Content text may be placed in a \"content\" member. Comments, prologs, DTDs, and {@code <[ [ ]]>} are ignored.\n\t * All values are converted as strings, for 1, 01, 29.0 will not be coerced to numbers but will instead be the exact value as seen in the XML document.\n\t *\n\t * @param string      XML字符串\n\t * @param parseConfig XML解析选项\n\t * @return A JSONObject containing the structured data from the XML string.\n\t * @throws JSONException Thrown if there is an errors while parsing the string\n\t * @since 5.8.25\n\t */\n\tpublic static JSONObject toJSONObject(final String string, final ParseConfig parseConfig) throws JSONException {\n\t\treturn toJSONObject(new JSONObject(), string, parseConfig);\n\t}\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t *\n\t * @param jo          JSONObject\n\t * @param xmlStr      XML字符串\n\t * @param keepStrings 如果为{@code true}，则值保持String类型，不转换为数字或boolean\n\t * @return A JSONObject 解析后的JSON对象，与传入的jo为同一对象\n\t * @throws JSONException 解析异常\n\t * @since 5.3.1\n\t */\n\tpublic static JSONObject toJSONObject(JSONObject jo, String xmlStr, boolean keepStrings) throws JSONException {\n\t\tJSONXMLParser.parseJSONObject(jo, xmlStr, keepStrings);\n\t\treturn jo;\n\t}\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t *\n\t * @param jo          JSONObject\n\t * @param xmlStr      XML字符串\n\t * @param parseConfig XML解析选项\n\t * @return A JSONObject 解析后的JSON对象，与传入的jo为同一对象\n\t * @throws JSONException 解析异常\n\t * @since 5.8.25\n\t */\n\tpublic static JSONObject toJSONObject(final JSONObject jo, final String xmlStr, final ParseConfig parseConfig) throws JSONException {\n\t\tJSONXMLParser.parseJSONObject(jo, xmlStr, parseConfig);\n\t\treturn jo;\n\t}\n\n\t/**\n\t * 转换JSONObject为XML\n\t *\n\t * @param object JSON对象或数组\n\t * @return XML字符串\n\t * @throws JSONException JSON解析异常\n\t */\n\tpublic static String toXml(Object object) throws JSONException {\n\t\treturn toXml(object, null);\n\t}\n\n\t/**\n\t * 转换JSONObject为XML\n\t *\n\t * @param object  JSON对象或数组\n\t * @param tagName 可选标签名称，名称为空时忽略标签\n\t * @return A string.\n\t * @throws JSONException JSON解析异常\n\t */\n\tpublic static String toXml(Object object, String tagName) throws JSONException {\n\t\treturn toXml(object, tagName, \"content\");\n\t}\n\n\t/**\n\t * 转换JSONObject为XML\n\t *\n\t * @param object      JSON对象或数组\n\t * @param tagName     可选标签名称，名称为空时忽略标签\n\t * @param contentKeys 标识为内容的key,遇到此key直接解析内容而不增加对应名称标签\n\t * @return A string.\n\t * @throws JSONException JSON解析异常\n\t */\n\tpublic static String toXml(Object object, String tagName, String... contentKeys) throws JSONException {\n\t\treturn JSONXMLSerializer.toXml(object, tagName, contentKeys);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/XMLTokener.java",
    "content": "package cn.hutool.json;\n\n/**\n * XML分析器，继承自JSONTokener，提供XML的语法分析\n *\n * @author JSON.org\n */\npublic class XMLTokener extends JSONTokener {\n\n\t/**\n\t * The table of entity values.\n\t * It initially contains Character values for amp, apos, gt, lt, quot.\n\t */\n\tpublic static final java.util.HashMap<String, Character> entity;\n\n\tstatic {\n\t\tentity = new java.util.HashMap<>(8);\n\t\tentity.put(\"amp\", XML.AMP);\n\t\tentity.put(\"apos\", XML.APOS);\n\t\tentity.put(\"gt\", XML.GT);\n\t\tentity.put(\"lt\", XML.LT);\n\t\tentity.put(\"quot\", XML.QUOT);\n\t}\n\n\t/**\n\t * Construct an XMLTokener from a string.\n\t *\n\t * @param s      A source string.\n\t * @param config JSON配置\n\t */\n\tpublic XMLTokener(CharSequence s, JSONConfig config) {\n\t\tsuper(s, config);\n\t}\n\n\t/**\n\t * Get the text in the CDATA block.\n\t *\n\t * @return The string up to the {@code ]]>}.\n\t * @throws JSONException If the {@code ]]>} is not found.\n\t */\n\tpublic String nextCDATA() throws JSONException {\n\t\tchar c;\n\t\tint i;\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tfor (; ; ) {\n\t\t\tc = next();\n\t\t\tif (end()) {\n\t\t\t\tthrow syntaxError(\"Unclosed CDATA\");\n\t\t\t}\n\t\t\tsb.append(c);\n\t\t\ti = sb.length() - 3;\n\t\t\tif (i >= 0 && sb.charAt(i) == ']' && sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') {\n\t\t\t\tsb.setLength(i);\n\t\t\t\treturn sb.toString();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the next XML outer token, trimming whitespace.\n\t * There are two kinds of tokens: the '&gt;' character which begins a markup tag, and the content text between markup tags.\n\t *\n\t * @return A string, or a '&gt;' Character, or null if there is no more source text.\n\t * @throws JSONException JSON\n\t */\n\tpublic Object nextContent() throws JSONException {\n\t\tchar c;\n\t\tfinal StringBuilder sb;\n\t\tdo {\n\t\t\tc = next();\n\t\t} while (Character.isWhitespace(c));\n\t\tif (c == 0) {\n\t\t\treturn null;\n\t\t}\n\t\tif (c == '<') {\n\t\t\treturn XML.LT;\n\t\t}\n\t\tsb = new StringBuilder();\n\t\tfor (; ; ) {\n\t\t\tif (c == '<' || c == 0) {\n\t\t\t\tback();\n\t\t\t\treturn sb.toString().trim();\n\t\t\t}\n\t\t\tif (c == '&') {\n\t\t\t\tsb.append(nextEntity(c));\n\t\t\t} else {\n\t\t\t\tsb.append(c);\n\t\t\t}\n\t\t\tc = next();\n\t\t}\n\t}\n\n\t/**\n\t * Return the next entity. These entities are translated to Characters: {@code &  '  >  <  \"}.\n\t *\n\t * @param ampersand An ampersand character.\n\t * @return A Character or an entity String if the entity is not recognized.\n\t * @throws JSONException If missing ';' in XML entity.\n\t */\n\tpublic Object nextEntity(char ampersand) throws JSONException {\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tchar c;\n\t\tfor (; ; ) {\n\t\t\tc = next();\n\t\t\tif (Character.isLetterOrDigit(c) || c == '#') {\n\t\t\t\tsb.append(Character.toLowerCase(c));\n\t\t\t} else if (c == ';') {\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tthrow syntaxError(\"Missing ';' in XML entity: &\" + sb);\n\t\t\t}\n\t\t}\n\t\treturn unescapeEntity(sb.toString());\n\t}\n\n\t/**\n\t * Unescape an XML entity encoding;\n\t *\n\t * @param e entity (only the actual entity value, not the preceding & or ending ;\n\t * @return Unescape str\n\t */\n\tstatic String unescapeEntity(final String e) {\n\t\t// validate\n\t\tif (e == null || e.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\t// if our entity is an encoded unicode point, parse it.\n\t\tif (e.charAt(0) == '#') {\n\t\t\tfinal int cp;\n\t\t\tif (e.charAt(1) == 'x' || e.charAt(1) == 'X') {\n\t\t\t\t// hex encoded unicode\n\t\t\t\tcp = Integer.parseInt(e.substring(2), 16);\n\t\t\t} else {\n\t\t\t\t// decimal encoded unicode\n\t\t\t\tcp = Integer.parseInt(e.substring(1));\n\t\t\t}\n\t\t\treturn new String(new int[]{cp}, 0, 1);\n\t\t}\n\t\tfinal Character knownEntity = entity.get(e);\n\t\tif (knownEntity == null) {\n\t\t\t// we don't know the entity so keep it encoded\n\t\t\treturn '&' + e + ';';\n\t\t}\n\t\treturn knownEntity.toString();\n\t}\n\n\t/**\n\t * Returns the next XML meta token. This is used for skipping over &lt;!...&gt; and &lt;?...?&gt; structures.\n\t *\n\t * @return Syntax characters ({@code < > / = ! ?}) are returned as Character, and strings and names are returned as Boolean. We don't care what the values actually are.\n\t * @throws JSONException 字符串中属性未关闭或XML结构错误抛出此异常。If a string is not properly closed or if the XML is badly structured.\n\t */\n\tpublic Object nextMeta() throws JSONException {\n\t\tchar c;\n\t\tchar q;\n\t\tdo {\n\t\t\tc = next();\n\t\t} while (Character.isWhitespace(c));\n\t\tswitch (c) {\n\t\t\tcase 0:\n\t\t\t\tthrow syntaxError(\"Misshaped meta tag\");\n\t\t\tcase '<':\n\t\t\t\treturn XML.LT;\n\t\t\tcase '>':\n\t\t\t\treturn XML.GT;\n\t\t\tcase '/':\n\t\t\t\treturn XML.SLASH;\n\t\t\tcase '=':\n\t\t\t\treturn XML.EQ;\n\t\t\tcase '!':\n\t\t\t\treturn XML.BANG;\n\t\t\tcase '?':\n\t\t\t\treturn XML.QUEST;\n\t\t\tcase '\"':\n\t\t\tcase '\\'':\n\t\t\t\tq = c;\n\t\t\t\tfor (; ; ) {\n\t\t\t\t\tc = next();\n\t\t\t\t\tif (c == 0) {\n\t\t\t\t\t\tthrow syntaxError(\"Unterminated string\");\n\t\t\t\t\t}\n\t\t\t\t\tif (c == q) {\n\t\t\t\t\t\treturn Boolean.TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tfor (; ; ) {\n\t\t\t\t\tc = next();\n\t\t\t\t\tif (Character.isWhitespace(c)) {\n\t\t\t\t\t\treturn Boolean.TRUE;\n\t\t\t\t\t}\n\t\t\t\t\tswitch (c) {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\tcase '<':\n\t\t\t\t\t\tcase '>':\n\t\t\t\t\t\tcase '/':\n\t\t\t\t\t\tcase '=':\n\t\t\t\t\t\tcase '!':\n\t\t\t\t\t\tcase '?':\n\t\t\t\t\t\tcase '\"':\n\t\t\t\t\t\tcase '\\'':\n\t\t\t\t\t\t\tback();\n\t\t\t\t\t\t\treturn Boolean.TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the next XML Token. These tokens are found inside of angle brackets. <br>\n\t * It may be one of these characters: {@code / > = ! ?} or it may be a string wrapped in single quotes or double\n\t * quotes, or it may be a name.\n\t *\n\t * @return a String or a Character.\n\t * @throws JSONException If the XML is not well formed.\n\t */\n\tpublic Object nextToken() throws JSONException {\n\t\tchar c;\n\t\tchar q;\n\t\tStringBuilder sb;\n\t\tdo {\n\t\t\tc = next();\n\t\t} while (Character.isWhitespace(c));\n\t\tswitch (c) {\n\t\t\tcase 0:\n\t\t\t\tthrow syntaxError(\"Misshaped element\");\n\t\t\tcase '<':\n\t\t\t\tthrow syntaxError(\"Misplaced '<'\");\n\t\t\tcase '>':\n\t\t\t\treturn XML.GT;\n\t\t\tcase '/':\n\t\t\t\treturn XML.SLASH;\n\t\t\tcase '=':\n\t\t\t\treturn XML.EQ;\n\t\t\tcase '!':\n\t\t\t\treturn XML.BANG;\n\t\t\tcase '?':\n\t\t\t\treturn XML.QUEST;\n\n\t\t\t// Quoted string\n\n\t\t\tcase '\"':\n\t\t\tcase '\\'':\n\t\t\t\tq = c;\n\t\t\t\tsb = new StringBuilder();\n\t\t\t\tfor (; ; ) {\n\t\t\t\t\tc = next();\n\t\t\t\t\tif (c == 0) {\n\t\t\t\t\t\tthrow syntaxError(\"Unterminated string\");\n\t\t\t\t\t}\n\t\t\t\t\tif (c == q) {\n\t\t\t\t\t\treturn sb.toString();\n\t\t\t\t\t}\n\t\t\t\t\tif (c == '&') {\n\t\t\t\t\t\tsb.append(nextEntity(c));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsb.append(c);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tdefault:\n\n\t\t\t\t// Name\n\n\t\t\t\tsb = new StringBuilder();\n\t\t\t\tfor (; ; ) {\n\t\t\t\t\tsb.append(c);\n\t\t\t\t\tc = next();\n\t\t\t\t\tif (Character.isWhitespace(c)) {\n\t\t\t\t\t\treturn sb.toString();\n\t\t\t\t\t}\n\t\t\t\t\tswitch (c) {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\treturn sb.toString();\n\t\t\t\t\t\tcase '>':\n\t\t\t\t\t\tcase '/':\n\t\t\t\t\t\tcase '=':\n\t\t\t\t\t\tcase '!':\n\t\t\t\t\t\tcase '?':\n\t\t\t\t\t\tcase '[':\n\t\t\t\t\t\tcase ']':\n\t\t\t\t\t\t\tback();\n\t\t\t\t\t\t\treturn sb.toString();\n\t\t\t\t\t\tcase '<':\n\t\t\t\t\t\tcase '\"':\n\t\t\t\t\t\tcase '\\'':\n\t\t\t\t\t\t\tthrow syntaxError(\"Bad character in a name\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Skip characters until past the requested string. If it is not found, we are left at the end of the source with a result of false.\n\t *\n\t * @param to A string to skip past.\n\t * @return 是否成功skip\n\t * @throws JSONException JSON异常\n\t */\n\tpublic boolean skipPast(String to) throws JSONException {\n\t\tboolean b;\n\t\tchar c;\n\t\tint i;\n\t\tint j;\n\t\tint offset = 0;\n\t\tint length = to.length();\n\t\tchar[] circle = new char[length];\n\n\t\t/*\n\t\t * First fill the circle buffer with as many characters as are in the to string. If we reach an early end, bail.\n\t\t */\n\n\t\tfor (i = 0; i < length; i += 1) {\n\t\t\tc = next();\n\t\t\tif (c == 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcircle[i] = c;\n\t\t}\n\n\t\t/* We will loop, possibly for all of the remaining characters. */\n\n\t\tfor (; ; ) {\n\t\t\tj = offset;\n\t\t\tb = true;\n\n\t\t\t/* Compare the circle buffer with the to string. */\n\n\t\t\tfor (i = 0; i < length; i += 1) {\n\t\t\t\tif (circle[j] != to.charAt(i)) {\n\t\t\t\t\tb = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tj += 1;\n\t\t\t\tif (j >= length) {\n\t\t\t\t\tj -= length;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* If we exit the loop with b intact, then victory is ours. */\n\n\t\t\tif (b) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t/* Get the next character. If there isn't one, then defeat is ours. */\n\n\t\t\tc = next();\n\t\t\tif (c == 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t/*\n\t\t\t * Shove the character in the circle buffer and advance the circle offset. The offset is mod n.\n\t\t\t */\n\t\t\tcircle[offset] = c;\n\t\t\toffset += 1;\n\t\t\tif (offset >= length) {\n\t\t\t\toffset -= length;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/package-info.java",
    "content": "/**\n * JSON封装，基于json.org官方库改造\n * \n * @author looly\n *\n */\npackage cn.hutool.json;"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/GlobalSerializeMapping.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.json.JSON;\n\nimport java.lang.reflect.Type;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 全局的序列化和反序列化器映射<br>\n * 在JSON和Java对象转换过程中，优先使用注册于此处的自定义转换\n *\n * @author Looly\n *\n */\npublic class GlobalSerializeMapping {\n\n\tprivate static Map<Type, JSONSerializer<? extends JSON, ?>> serializerMap;\n\tprivate static Map<Type, JSONDeserializer<?>> deserializerMap;\n\n\tstatic {\n\t\tserializerMap = new SafeConcurrentHashMap<>();\n\t\tdeserializerMap = new SafeConcurrentHashMap<>();\n\n\t\tfinal TemporalAccessorSerializer localDateSerializer = new TemporalAccessorSerializer(LocalDate.class);\n\t\tserializerMap.put(LocalDate.class, localDateSerializer);\n\t\tdeserializerMap.put(LocalDate.class, localDateSerializer);\n\n\t\tfinal TemporalAccessorSerializer localDateTimeSerializer = new TemporalAccessorSerializer(LocalDateTime.class);\n\t\tserializerMap.put(LocalDateTime.class, localDateTimeSerializer);\n\t\tdeserializerMap.put(LocalDateTime.class, localDateTimeSerializer);\n\n\t\tfinal TemporalAccessorSerializer localTimeSerializer = new TemporalAccessorSerializer(LocalTime.class);\n\t\tserializerMap.put(LocalTime.class, localTimeSerializer);\n\t\tdeserializerMap.put(LocalTime.class, localTimeSerializer);\n\t}\n\n\t/**\n\t * 加入自定义的序列化器\n\t *\n\t * @param type 对象类型\n\t * @param serializer 序列化器实现\n\t */\n\tpublic static void put(Type type, JSONArraySerializer<?> serializer) {\n\t\tputInternal(type, serializer);\n\t}\n\n\t/**\n\t * 加入自定义的序列化器\n\t *\n\t * @param type 对象类型\n\t * @param serializer 序列化器实现\n\t */\n\tpublic static void put(Type type, JSONObjectSerializer<?> serializer) {\n\t\tputInternal(type, serializer);\n\t}\n\n\t/**\n\t * 加入自定义的序列化器\n\t *\n\t * @param type 对象类型\n\t * @param serializer 序列化器实现\n\t */\n\tsynchronized private static void putInternal(Type type, JSONSerializer<? extends JSON, ?> serializer) {\n\t\tif(null == serializerMap) {\n\t\t\tserializerMap = new SafeConcurrentHashMap<>();\n\t\t}\n\t\tserializerMap.put(type, serializer);\n\t}\n\n\t/**\n\t * 加入自定义的反序列化器\n\t *\n\t * @param type 对象类型\n\t * @param deserializer 反序列化器实现\n\t */\n\tsynchronized public static void put(Type type, JSONDeserializer<?> deserializer) {\n\t\tif(null == deserializerMap) {\n\t\t\tdeserializerMap = new ConcurrentHashMap<>();\n\t\t}\n\t\tdeserializerMap.put(type, deserializer);\n\t}\n\n\t/**\n\t * 获取自定义的序列化器，如果未定义返回{@code null}\n\t * @param type 类型\n\t * @return 自定义的序列化器或者{@code null}\n\t */\n\tpublic static JSONSerializer<? extends JSON, ?> getSerializer(Type type){\n\t\tif(null == type || null == serializerMap) {\n\t\t\treturn null;\n\t\t}\n\t\treturn serializerMap.get(type);\n\t}\n\n\t/**\n\t * 获取自定义的反序列化器，如果未定义返回{@code null}\n\t * @param type 类型\n\t * @return 自定义的反序列化器或者{@code null}\n\t */\n\tpublic static JSONDeserializer<?> getDeserializer(Type type){\n\t\tif(null == type || null == deserializerMap) {\n\t\t\treturn null;\n\t\t}\n\t\treturn deserializerMap.get(type);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/JSONArraySerializer.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.json.JSONArray;\n\n/**\n * JSON列表的序列化接口，用于将特定对象序列化为{@link JSONArray}\n * \n * @param <V> 对象类型\n * \n * @author Looly\n */\n@FunctionalInterface\npublic interface JSONArraySerializer<V> extends JSONSerializer<JSONArray, V>{}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/JSONDeserializer.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.json.JSON;\n\n/**\n * JSON反序列话自定义实现类\n * \n * @author Looly\n *\n * @param <T> 反序列化后的类型\n */\n@FunctionalInterface\npublic interface JSONDeserializer<T> {\n\t\n\t/**\n\t * 反序列化，通过实现此方法，自定义实现JSON转换为指定类型的逻辑\n\t * \n\t * @param json {@link JSON}\n\t * @return 目标对象\n\t */\n\tT deserialize(JSON json);\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/JSONObjectSerializer.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.json.JSONObject;\n\n/**\n * 对象的序列化接口，用于将特定对象序列化为{@link JSONObject}\n * @param <V> 对象类型\n * \n * @author Looly\n */\n@FunctionalInterface\npublic interface JSONObjectSerializer<V> extends JSONSerializer<JSONObject, V>{}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/JSONSerializer.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.json.JSON;\n\n/**\n * 序列化接口，通过实现此接口，实现自定义的对象转换为JSON的操作\n *\n * @param <T> JSON类型，可以是JSONObject或者JSONArray\n * @param <V> 对象类型\n * @author Looly\n */\n@FunctionalInterface\npublic interface JSONSerializer<T extends JSON, V> {\n\n\t/**\n\t * 序列化实现，通过实现此方法，将指定类型的对象转换为{@link JSON}对象<br>\n\t * 转换后的对象可以为JSONObject也可以为JSONArray，首先new一个空的JSON，然后将需要的数据字段put到JSON对象中去即可。\n\t *\n\t * @param json JSON，可以为JSONObject或者JSONArray\n\t * @param bean 指定类型对象\n\t */\n\tvoid serialize(T json, V bean);\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/JSONWriter.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.convert.NumberWithFormat;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.TemporalAccessorUtil;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.lang.Filter;\nimport cn.hutool.core.lang.mutable.MutablePair;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.*;\n\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.time.MonthDay;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * JSON数据写出器<br>\n * 通过简单的append方式将JSON的键值对等信息写出到{@link Writer}中。\n *\n * @author looly\n * @since 5.7.3\n */\npublic class JSONWriter extends Writer {\n\n\t/**\n\t * 缩进因子，定义每一级别增加的缩进量\n\t */\n\tprivate final int indentFactor;\n\t/**\n\t * 本级别缩进量\n\t */\n\tprivate final int indent;\n\t/**\n\t * Writer\n\t */\n\tprivate final Writer writer;\n\t/**\n\t * JSON选项\n\t */\n\tprivate final JSONConfig config;\n\n\t/**\n\t * 写出当前值是否需要分隔符\n\t */\n\tprivate boolean needSeparator;\n\t/**\n\t * 是否为JSONArray模式\n\t */\n\tprivate boolean arrayMode;\n\n\t/**\n\t * 创建JSONWriter\n\t *\n\t * @param writer       {@link Writer}\n\t * @param indentFactor 缩进因子，定义每一级别增加的缩进量\n\t * @param indent       本级别缩进量\n\t * @param config       JSON选项\n\t * @return JSONWriter\n\t */\n\tpublic static JSONWriter of(Writer writer, int indentFactor, int indent, JSONConfig config) {\n\t\treturn new JSONWriter(writer, indentFactor, indent, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param writer       {@link Writer}\n\t * @param indentFactor 缩进因子，定义每一级别增加的缩进量\n\t * @param indent       本级别缩进量\n\t * @param config       JSON选项\n\t */\n\tpublic JSONWriter(Writer writer, int indentFactor, int indent, JSONConfig config) {\n\t\tthis.writer = writer;\n\t\tthis.indentFactor = indentFactor;\n\t\tthis.indent = indent;\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * JSONObject写出开始，默认写出\"{\"\n\t *\n\t * @return this\n\t */\n\tpublic JSONWriter beginObj() {\n\t\twriteRaw(CharUtil.DELIM_START);\n\t\treturn this;\n\t}\n\n\t/**\n\t * JSONArray写出开始，默认写出\"[\"\n\t *\n\t * @return this\n\t */\n\tpublic JSONWriter beginArray() {\n\t\twriteRaw(CharUtil.BRACKET_START);\n\t\tarrayMode = true;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 结束，默认根据开始的类型，补充\"}\"或\"]\"\n\t *\n\t * @return this\n\t */\n\tpublic JSONWriter end() {\n\t\t// 换行缩进\n\t\twriteLF().writeSpace(indent);\n\t\twriteRaw(arrayMode ? CharUtil.BRACKET_END : CharUtil.DELIM_END);\n\t\tflush();\n\t\tarrayMode = false;\n\t\t// 当前对象或数组结束，当新的\n\t\tneedSeparator = true;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出键，自动处理分隔符和缩进，并包装键名\n\t *\n\t * @param key 键名\n\t * @return this\n\t */\n\tpublic JSONWriter writeKey(String key) {\n\t\tif (needSeparator) {\n\t\t\twriteRaw(CharUtil.COMMA);\n\t\t}\n\t\t// 换行缩进\n\t\twriteLF().writeSpace(indentFactor + indent);\n\t\treturn writeRaw(JSONUtil.quote(key));\n\t}\n\n\t/**\n\t * 写出值，自动处理分隔符和缩进，自动判断类型，并根据不同类型写出特定格式的值<br>\n\t * 如果写出的值为{@code null}或者{@link JSONNull}，且配置忽略null，则跳过。\n\t *\n\t * @param value 值\n\t * @return this\n\t */\n\tpublic JSONWriter writeValue(Object value) {\n\t\tif(JSONUtil.isNull(value) && config.isIgnoreNullValue()){\n\t\t\treturn this;\n\t\t}\n\t\treturn writeValueDirect(value, null);\n\t}\n\n\t/**\n\t * 写出字段名及字段值，如果字段值是{@code null}且忽略null值，则不写出任何内容\n\t *\n\t * @param key 字段名\n\t * @param value 字段值\n\t * @return this\n\t * @since 5.7.6\n\t * @deprecated 请使用 {@link #writeField(MutablePair, Filter)}\n\t */\n\t@Deprecated\n\tpublic JSONWriter writeField(String key, Object value){\n\t\tif(JSONUtil.isNull(value) && config.isIgnoreNullValue()){\n\t\t\treturn this;\n\t\t}\n\t\treturn writeKey(key).writeValueDirect(value, null);\n\t}\n\n\t/**\n\t * 写出字段名及字段值，如果字段值是{@code null}且忽略null值，则不写出任何内容\n\t *\n\t * @param pair 键值对\n\t * @param filter 键值对的过滤器，可以编辑键值对\n\t * @return this\n\t * @since 5.8.6\n\t */\n\tpublic JSONWriter writeField(MutablePair<Object, Object> pair, Filter<MutablePair<Object, Object>> filter){\n\t\tif(JSONUtil.isNull(pair.getValue()) && config.isIgnoreNullValue()){\n\t\t\treturn this;\n\t\t}\n\n\t\tif (null == filter || filter.accept(pair)) {\n\t\t\tif(false == arrayMode){\n\t\t\t\t// JSONArray只写值，JSONObject写键值对\n\t\t\t\twriteKey(StrUtil.toString(pair.getKey()));\n\t\t\t}\n\t\t\treturn writeValueDirect(pair.getValue(), filter);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void write(char[] cbuf, int off, int len) throws IOException {\n\t\tthis.writer.write(cbuf, off, len);\n\t}\n\n\t@Override\n\tpublic void flush() {\n\t\ttry {\n\t\t\tthis.writer.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() throws IOException {\n\t\tthis.writer.close();\n\t}\n\n\t// ------------------------------------------------------------------------------ Private methods\n\t/**\n\t * 写出值，自动处理分隔符和缩进，自动判断类型，并根据不同类型写出特定格式的值\n\t *\n\t * @param value 值\n\t * @param filter 键值对过滤器\n\t * @return this\n\t */\n\tprivate JSONWriter writeValueDirect(Object value, Filter<MutablePair<Object, Object>> filter) {\n\t\tif (arrayMode) {\n\t\t\tif (needSeparator) {\n\t\t\t\twriteRaw(CharUtil.COMMA);\n\t\t\t}\n\t\t\t// 换行缩进\n\t\t\twriteLF().writeSpace(indentFactor + indent);\n\t\t} else {\n\t\t\twriteRaw(CharUtil.COLON).writeSpace(1);\n\t\t}\n\t\tneedSeparator = true;\n\t\treturn writeObjValue(value, filter);\n\t}\n\n\t/**\n\t * 写出JSON的值，根据值类型不同，输出不同内容\n\t *\n\t * @param value 值\n\t * @param filter 过滤器\n\t * @return this\n\t */\n\tprivate JSONWriter writeObjValue(Object value, Filter<MutablePair<Object, Object>> filter) {\n\t\tfinal int indent = indentFactor + this.indent;\n\t\tif (value == null || value instanceof JSONNull) {\n\t\t\twriteRaw(JSONNull.NULL.toString());\n\t\t} else if (value instanceof JSON) {\n\t\t\tif(value instanceof JSONObject){\n\t\t\t\t((JSONObject) value).write(writer, indentFactor, indent, filter);\n\t\t\t}else if(value instanceof JSONArray){\n\t\t\t\t((JSONArray) value).write(writer, indentFactor, indent, filter);\n\t\t\t}\n\t\t} else if (value instanceof Map || value instanceof Map.Entry) {\n\t\t\tnew JSONObject(value).write(writer, indentFactor, indent);\n\t\t} else if (value instanceof Iterable || value instanceof Iterator || ArrayUtil.isArray(value)) {\n\t\t\tnew JSONArray(value).write(writer, indentFactor, indent);\n\t\t} else if (value instanceof Number) {\n\t\t\t// issue#IALQ0N，避免设置日期格式后writeLongAsString失效\n\t\t\tif(value instanceof NumberWithFormat){\n\t\t\t\tvalue = ((NumberWithFormat) value).getNumber();\n\t\t\t}\n\n\t\t\tif(value instanceof Long && config.isWriteLongAsString()){\n\t\t\t\t// issue#3541\n\t\t\t\t// long可能溢出，此时可选是否将long写出为字符串类型\n\t\t\t\twriteStrValue(value.toString());\n\t\t\t} else {\n\t\t\t\twriteNumberValue((Number) value);\n\t\t\t}\n\t\t} else if (value instanceof Date || value instanceof Calendar || value instanceof TemporalAccessor) {\n\t\t\t// issue#2572@Github\n\t\t\tif(value instanceof MonthDay){\n\t\t\t\twriteStrValue(value.toString());\n\t\t\t\treturn this;\n\t\t\t}\n\n\t\t\tfinal String format = (null == config) ? null : config.getDateFormat();\n\t\t\twriteRaw(formatDate(value, format));\n\t\t} else if (value instanceof Boolean) {\n\t\t\twriteBooleanValue((Boolean) value);\n\t\t} else if (value instanceof JSONString) {\n\t\t\twriteJSONStringValue((JSONString) value);\n\t\t} else {\n\t\t\twriteStrValue(value.toString());\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出数字，根据{@link JSONConfig#isStripTrailingZeros()} 配置不同，写出不同数字<br>\n\t * 主要针对Double型是否去掉小数点后多余的0<br>\n\t * 此方法输出的值不包装引号。\n\t *\n\t * @param number 数字\n\t */\n\tprivate void writeNumberValue(Number number) {\n\t\t// since 5.6.2可配置是否去除末尾多余0，例如如果为true,5.0返回5\n\t\tfinal boolean isStripTrailingZeros = null == config || config.isStripTrailingZeros();\n\t\twriteRaw(NumberUtil.toStr(number, isStripTrailingZeros));\n\t}\n\n\t/**\n\t * 写出Boolean值，直接写出true或false,不适用引号包装\n\t *\n\t * @param value Boolean值\n\t */\n\tprivate void writeBooleanValue(Boolean value) {\n\t\twriteRaw(value.toString());\n\t}\n\n\t/**\n\t * 输出实现了{@link JSONString}接口的对象，通过调用{@link JSONString#toJSONString()}获取JSON字符串<br>\n\t * {@link JSONString}按照JSON对象对待，此方法输出的JSON字符串不包装引号。<br>\n\t * 如果toJSONString()返回null，调用toString()方法并使用双引号包装。\n\t *\n\t * @param jsonString {@link JSONString}\n\t */\n\tprivate void writeJSONStringValue(JSONString jsonString) {\n\t\tString valueStr;\n\t\ttry {\n\t\t\tvalueStr = jsonString.toJSONString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new JSONException(e);\n\t\t}\n\t\tif (null != valueStr) {\n\t\t\twriteRaw(valueStr);\n\t\t} else {\n\t\t\twriteStrValue(jsonString.toString());\n\t\t}\n\t}\n\n\t/**\n\t * 写出字符串值，并包装引号并转义字符<br>\n\t * 对所有双引号做转义处理（使用双反斜杠做转义）<br>\n\t * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\\/<br>\n\t * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠\n\t *\n\t * @param csq 字符串\n\t */\n\tprivate void writeStrValue(String csq) {\n\t\ttry {\n\t\t\tJSONUtil.quote(csq, writer);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 写出空格\n\t *\n\t * @param count 空格数\n\t */\n\tprivate void writeSpace(int count) {\n\t\tif (indentFactor > 0) {\n\t\t\tfor (int i = 0; i < count; i++) {\n\t\t\t\twriteRaw(CharUtil.SPACE);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 写出换换行符\n\t *\n\t * @return this\n\t */\n\tprivate JSONWriter writeLF() {\n\t\tif (indentFactor > 0) {\n\t\t\twriteRaw(CharUtil.LF);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入原始字符串值，不做任何处理\n\t *\n\t * @param csq 字符串\n\t * @return this\n\t */\n\tprivate JSONWriter writeRaw(String csq) {\n\t\ttry {\n\t\t\twriter.append(csq);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入原始字符值，不做任何处理\n\t *\n\t * @param c 字符串\n\t * @return this\n\t */\n\tprivate JSONWriter writeRaw(char c) {\n\t\ttry {\n\t\t\twriter.write(c);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 按照给定格式格式化日期，格式为空时返回时间戳字符串\n\t *\n\t * @param dateObj Date或者Calendar对象\n\t * @param format  格式\n\t * @return 日期字符串\n\t */\n\tprivate static String formatDate(Object dateObj, String format) {\n\t\tif (StrUtil.isNotBlank(format)) {\n\t\t\tfinal String dateStr;\n\t\t\tif (dateObj instanceof TemporalAccessor) {\n\t\t\t\tdateStr = TemporalAccessorUtil.format((TemporalAccessor) dateObj, format);\n\t\t\t} else {\n\t\t\t\tdateStr = DateUtil.format(Convert.toDate(dateObj), format);\n\t\t\t}\n\n\t\t\tif (GlobalCustomFormat.FORMAT_SECONDS.equals(format)\n\t\t\t\t\t|| GlobalCustomFormat.FORMAT_MILLISECONDS.equals(format)) {\n\t\t\t\t// Hutool自定义的秒和毫秒表示，默认不包装双引号\n\t\t\t\treturn dateStr;\n\t\t\t}\n\t\t\t//用户定义了日期格式\n\t\t\treturn JSONUtil.quote(dateStr);\n\t\t}\n\n\t\t//默认使用时间戳\n\t\tlong timeMillis;\n\t\tif (dateObj instanceof TemporalAccessor) {\n\t\t\ttimeMillis = TemporalAccessorUtil.toEpochMilli((TemporalAccessor) dateObj);\n\t\t} else if (dateObj instanceof Date) {\n\t\t\ttimeMillis = ((Date) dateObj).getTime();\n\t\t} else if (dateObj instanceof Calendar) {\n\t\t\ttimeMillis = ((Calendar) dateObj).getTimeInMillis();\n\t\t} else {\n\t\t\tthrow new UnsupportedOperationException(\"Unsupported Date type: \" + dateObj.getClass());\n\t\t}\n\t\treturn String.valueOf(timeMillis);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/TemporalAccessorSerializer.java",
    "content": "package cn.hutool.json.serialize;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.json.JSON;\nimport cn.hutool.json.JSONException;\nimport cn.hutool.json.JSONObject;\n\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.Month;\nimport java.time.temporal.TemporalAccessor;\n\n/**\n * {@link TemporalAccessor}的JSON自定义序列化实现\n *\n * @author looly\n * @since 5.7.22\n */\npublic class TemporalAccessorSerializer implements JSONObjectSerializer<TemporalAccessor>, JSONDeserializer<TemporalAccessor> {\n\n\tprivate static final String YEAR_KEY = \"year\";\n\tprivate static final String MONTH_KEY = \"month\";\n\tprivate static final String DAY_KEY = \"day\";\n\tprivate static final String HOUR_KEY = \"hour\";\n\tprivate static final String MINUTE_KEY = \"minute\";\n\tprivate static final String SECOND_KEY = \"second\";\n\tprivate static final String NANO_KEY = \"nano\";\n\n\tprivate final Class<? extends TemporalAccessor> temporalAccessorClass;\n\n\tpublic TemporalAccessorSerializer(Class<? extends TemporalAccessor> temporalAccessorClass) {\n\t\tthis.temporalAccessorClass = temporalAccessorClass;\n\t}\n\n\t@Override\n\tpublic void serialize(JSONObject json, TemporalAccessor bean) {\n\t\tif (bean instanceof LocalDate) {\n\t\t\tfinal LocalDate localDate = (LocalDate) bean;\n\t\t\tjson.set(YEAR_KEY, localDate.getYear());\n\t\t\tjson.set(MONTH_KEY, localDate.getMonthValue());\n\t\t\tjson.set(DAY_KEY, localDate.getDayOfMonth());\n\t\t} else if (bean instanceof LocalDateTime) {\n\t\t\tfinal LocalDateTime localDateTime = (LocalDateTime) bean;\n\t\t\tjson.set(YEAR_KEY, localDateTime.getYear());\n\t\t\tjson.set(MONTH_KEY, localDateTime.getMonthValue());\n\t\t\tjson.set(DAY_KEY, localDateTime.getDayOfMonth());\n\t\t\tjson.set(HOUR_KEY, localDateTime.getHour());\n\t\t\tjson.set(MINUTE_KEY, localDateTime.getMinute());\n\t\t\tjson.set(SECOND_KEY, localDateTime.getSecond());\n\t\t\tjson.set(NANO_KEY, localDateTime.getNano());\n\t\t} else if (bean instanceof LocalTime) {\n\t\t\tfinal LocalTime localTime = (LocalTime) bean;\n\t\t\tjson.set(HOUR_KEY, localTime.getHour());\n\t\t\tjson.set(MINUTE_KEY, localTime.getMinute());\n\t\t\tjson.set(SECOND_KEY, localTime.getSecond());\n\t\t\tjson.set(NANO_KEY, localTime.getNano());\n\t\t} else {\n\t\t\tthrow new JSONException(\"Unsupported type to JSON: {}\", bean.getClass().getName());\n\t\t}\n\t}\n\n\t@Override\n\tpublic TemporalAccessor deserialize(JSON json) {\n\t\tfinal JSONObject jsonObject = (JSONObject) json;\n\t\tif (LocalDate.class.equals(this.temporalAccessorClass) || LocalDateTime.class.equals(this.temporalAccessorClass)) {\n\t\t\tfinal Integer year = jsonObject.getInt(YEAR_KEY);\n\t\t\tAssert.notNull(year, \"Field 'year' must be not null\");\n\t\t\tInteger month = jsonObject.getInt(MONTH_KEY);\n\t\t\tif (null == month) {\n\t\t\t\tfinal Month monthEnum = Month.valueOf(jsonObject.getStr(MONTH_KEY));\n\t\t\t\tAssert.notNull(monthEnum, \"Field 'month' must be not null\");\n\t\t\t\tmonth = monthEnum.getValue();\n\t\t\t}\n\t\t\tInteger day = jsonObject.getInt(DAY_KEY);\n\t\t\tif (null == day) {\n\t\t\t\tday = jsonObject.getInt(\"dayOfMonth\");\n\t\t\t\tAssert.notNull(day, \"Field 'day' or 'dayOfMonth' must be not null\");\n\t\t\t}\n\n\t\t\tfinal LocalDate localDate = LocalDate.of(year, month, day);\n\t\t\tif (LocalDate.class.equals(this.temporalAccessorClass)) {\n\t\t\t\treturn localDate;\n\t\t\t}\n\n\t\t\tfinal LocalTime localTime = LocalTime.of(\n\t\t\t\tjsonObject.getInt(HOUR_KEY, 0),\n\t\t\t\tjsonObject.getInt(MINUTE_KEY, 0),\n\t\t\t\tjsonObject.getInt(SECOND_KEY, 0),\n\t\t\t\tjsonObject.getInt(NANO_KEY, 0));\n\n\t\t\treturn LocalDateTime.of(localDate, localTime);\n\t\t} else if (LocalTime.class.equals(this.temporalAccessorClass)) {\n\t\t\treturn LocalTime.of(jsonObject.getInt(HOUR_KEY), jsonObject.getInt(MINUTE_KEY), jsonObject.getInt(SECOND_KEY), jsonObject.getInt(NANO_KEY));\n\t\t}\n\n\t\tthrow new JSONException(\"Unsupported type from JSON: {}\", this.temporalAccessorClass);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/serialize/package-info.java",
    "content": "/**\n * JSON自定义序列化和反序列化接口和默认实现\n * \n * @author Looly\n *\n */\npackage cn.hutool.json.serialize;"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.json.InternalJSONUtil;\nimport cn.hutool.json.JSONException;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.XML;\nimport cn.hutool.json.XMLTokener;\n\n/**\n * XML解析器，将XML解析为JSON对象\n *\n * @author JSON.org, looly\n * @since 5.7.11\n */\npublic class JSONXMLParser {\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t *\n\t * @param jo          JSONObject\n\t * @param xmlStr      XML字符串\n\t * @param keepStrings 如果为{@code true}，则值保持String类型，不转换为数字或boolean\n\t * @throws JSONException 解析异常\n\t */\n\tpublic static void parseJSONObject(JSONObject jo, String xmlStr, boolean keepStrings) throws JSONException {\n\t\tparseJSONObject(jo, xmlStr, ParseConfig.of().setKeepStrings(keepStrings));\n\t}\n\n\t/**\n\t * 转换XML为JSONObject\n\t * 转换过程中一些信息可能会丢失，JSON中无法区分节点和属性，相同的节点将被处理为JSONArray。\n\t *\n\t * @param xmlStr      XML字符串\n\t * @param jo          JSONObject\n\t * @param parseConfig 解析选项\n\t * @throws JSONException 解析异常\n\t */\n\tpublic static void parseJSONObject(final JSONObject jo, final String xmlStr, final ParseConfig parseConfig) throws JSONException {\n\t\tfinal XMLTokener x = new XMLTokener(xmlStr, jo.getConfig());\n\t\twhile (x.more() && x.skipPast(\"<\")) {\n\t\t\tparse(x, jo, null, parseConfig, 0);\n\t\t}\n\t}\n\n\t/**\n\t * Scan the content following the named tag, attaching it to the context.\n\t *\n\t * @param x       The XMLTokener containing the source string.\n\t * @param context The JSONObject that will include the new material.\n\t * @param name    The tag name.\n\t * @param parseConfig 解析选项\n\t * @param currentNestingDepth 当前层级\n\t * @return true if the close tag is processed.\n\t * @throws JSONException JSON异常\n\t */\n\tprivate static boolean parse(XMLTokener x, JSONObject context, String name, ParseConfig parseConfig, int currentNestingDepth) throws JSONException {\n\t\tchar c;\n\t\tint i;\n\t\tJSONObject jsonobject;\n\t\tString string;\n\t\tString tagName;\n\t\tObject token;\n\n\t\ttoken = x.nextToken();\n\n\t\tif (token == XML.BANG) {\n\t\t\tc = x.next();\n\t\t\tif (c == '-') {\n\t\t\t\tif (x.next() == '-') {\n\t\t\t\t\tx.skipPast(\"-->\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tx.back();\n\t\t\t} else if (c == '[') {\n\t\t\t\ttoken = x.nextToken();\n\t\t\t\tif (\"CDATA\".equals(token)) {\n\t\t\t\t\tif (x.next() == '[') {\n\t\t\t\t\t\tstring = x.nextCDATA();\n\t\t\t\t\t\tif (string.length() > 0) {\n\t\t\t\t\t\t\tcontext.accumulate(\"content\", string);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthrow x.syntaxError(\"Expected 'CDATA['\");\n\t\t\t}\n\t\t\ti = 1;\n\t\t\tdo {\n\t\t\t\ttoken = x.nextMeta();\n\t\t\t\tif (token == null) {\n\t\t\t\t\tthrow x.syntaxError(\"Missing '>' after '<!'.\");\n\t\t\t\t} else if (token == XML.LT) {\n\t\t\t\t\ti += 1;\n\t\t\t\t} else if (token == XML.GT) {\n\t\t\t\t\ti -= 1;\n\t\t\t\t}\n\t\t\t} while (i > 0);\n\t\t\treturn false;\n\t\t} else if (token == XML.QUEST) {\n\n\t\t\t// <?\n\t\t\tx.skipPast(\"?>\");\n\t\t\treturn false;\n\t\t} else if (token == XML.SLASH) {\n\n\t\t\t// Close tag </\n\n\t\t\ttoken = x.nextToken();\n\t\t\tif (name == null) {\n\t\t\t\tthrow x.syntaxError(\"Mismatched close tag \" + token);\n\t\t\t}\n\t\t\tif (!token.equals(name)) {\n\t\t\t\tthrow x.syntaxError(\"Mismatched \" + name + \" and \" + token);\n\t\t\t}\n\t\t\tif (x.nextToken() != XML.GT) {\n\t\t\t\tthrow x.syntaxError(\"Misshaped close tag\");\n\t\t\t}\n\t\t\treturn true;\n\n\t\t} else if (token instanceof Character) {\n\t\t\tthrow x.syntaxError(\"Misshaped tag\");\n\n\t\t\t// Open tag <\n\n\t\t} else {\n\t\t\ttagName = (String) token;\n\t\t\ttoken = null;\n\t\t\tjsonobject = new JSONObject();\n\t\t\tfinal boolean keepStrings = parseConfig.isKeepStrings();\n\t\t\tfor (; ; ) {\n\t\t\t\tif (token == null) {\n\t\t\t\t\ttoken = x.nextToken();\n\t\t\t\t}\n\n\t\t\t\t// attribute = value\n\t\t\t\tif (token instanceof String) {\n\t\t\t\t\tstring = (String) token;\n\t\t\t\t\ttoken = x.nextToken();\n\t\t\t\t\tif (token == XML.EQ) {\n\t\t\t\t\t\ttoken = x.nextToken();\n\t\t\t\t\t\tif (!(token instanceof String)) {\n\t\t\t\t\t\t\tthrow x.syntaxError(\"Missing value\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjsonobject.accumulate(string, keepStrings ? token : InternalJSONUtil.stringToValue((String) token));\n\t\t\t\t\t\ttoken = null;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjsonobject.accumulate(string, \"\");\n\t\t\t\t\t}\n\n\t\t\t\t} else if (token == XML.SLASH) {\n\t\t\t\t\t// Empty tag <.../>\n\t\t\t\t\tif (x.nextToken() != XML.GT) {\n\t\t\t\t\t\tthrow x.syntaxError(\"Misshaped tag\");\n\t\t\t\t\t}\n\t\t\t\t\tif (jsonobject.size() > 0) {\n\t\t\t\t\t\tcontext.accumulate(tagName, jsonobject);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.accumulate(tagName, \"\");\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\n\t\t\t\t} else if (token == XML.GT) {\n\t\t\t\t\t// Content, between <...> and </...>\n\t\t\t\t\tfor (; ; ) {\n\t\t\t\t\t\ttoken = x.nextContent();\n\t\t\t\t\t\tif (token == null) {\n\t\t\t\t\t\t\tif (tagName != null) {\n\t\t\t\t\t\t\t\tthrow x.syntaxError(\"Unclosed tag \" + tagName);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t} else if (token instanceof String) {\n\t\t\t\t\t\t\tstring = (String) token;\n\t\t\t\t\t\t\tif (!string.isEmpty()) {\n\t\t\t\t\t\t\t\tjsonobject.accumulate(\"content\", keepStrings ? token : InternalJSONUtil.stringToValue(string));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else if (token == XML.LT) {\n\t\t\t\t\t\t\t// Nested element\n\t\t\t\t\t\t\t// issue#2748 of CVE-2022-45688\n\t\t\t\t\t\t\tfinal int maxNestingDepth = parseConfig.getMaxNestingDepth();\n\t\t\t\t\t\t\tif (maxNestingDepth > -1 && currentNestingDepth >= maxNestingDepth) {\n\t\t\t\t\t\t\t\tthrow x.syntaxError(\"Maximum nesting depth of \" + maxNestingDepth + \" reached\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Nested element\n\t\t\t\t\t\t\tif (parse(x, jsonobject, tagName, parseConfig, currentNestingDepth + 1)) {\n\t\t\t\t\t\t\t\tif (jsonobject.isEmpty()) {\n\t\t\t\t\t\t\t\t\tcontext.accumulate(tagName, \"\");\n\t\t\t\t\t\t\t\t} else if (jsonobject.size() == 1 && jsonobject.get(\"content\") != null) {\n\t\t\t\t\t\t\t\t\tcontext.accumulate(tagName, jsonobject.get(\"content\"));\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcontext.accumulate(tagName, jsonobject);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthrow x.syntaxError(\"Misshaped tag\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLSerializer.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.EscapeUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONArray;\nimport cn.hutool.json.JSONException;\nimport cn.hutool.json.JSONObject;\n\n/**\n * JSON转XML字符串工具\n *\n * @author looly\n * @since 5.7.11\n */\npublic class JSONXMLSerializer {\n\t/**\n\t * 转换JSONObject为XML\n\t * Convert a JSONObject into a well-formed, element-normal XML string.\n\t *\n\t * @param object A JSONObject.\n\t * @return A string.\n\t * @throws JSONException Thrown if there is an error parsing the string\n\t */\n\tpublic static String toXml(Object object) throws JSONException {\n\t\treturn toXml(object, null);\n\t}\n\n\t/**\n\t * 转换JSONObject为XML\n\t *\n\t * @param object  JSON对象或数组\n\t * @param tagName 可选标签名称，名称为空时忽略标签\n\t * @return A string.\n\t * @throws JSONException JSON解析异常\n\t */\n\tpublic static String toXml(Object object, String tagName) throws JSONException {\n\t\treturn toXml(object, tagName, \"content\");\n\t}\n\n\t/**\n\t * 转换JSONObject为XML\n\t *\n\t * @param object      JSON对象或数组\n\t * @param tagName     可选标签名称，名称为空时忽略标签\n\t * @param contentKeys 标识为内容的key,遇到此key直接解析内容而不增加对应名称标签\n\t * @return A string.\n\t * @throws JSONException JSON解析异常\n\t */\n\tpublic static String toXml(Object object, String tagName, String... contentKeys) throws JSONException {\n\t\tif (null == object) {\n\t\t\treturn null;\n\t\t}\n\n\t\tfinal StringBuilder sb = new StringBuilder();\n\t\tif (object instanceof JSONObject) {\n\n\t\t\t// Emit <tagName>\n\t\t\tappendTag(sb, tagName, false);\n\n\t\t\t// Loop thru the keys.\n\t\t\t((JSONObject) object).forEach((key, value) -> {\n\t\t\t\tif (ArrayUtil.isArray(value)) {\n\t\t\t\t\tvalue = new JSONArray(value);\n\t\t\t\t}\n\n\t\t\t\t// Emit content in body\n\t\t\t\tif (ArrayUtil.contains(contentKeys, key)) {\n\t\t\t\t\tif (value instanceof JSONArray) {\n\t\t\t\t\t\tint i = 0;\n\t\t\t\t\t\tfor (Object val : (JSONArray) value) {\n\t\t\t\t\t\t\tif (i > 0) {\n\t\t\t\t\t\t\t\tsb.append(CharUtil.LF);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsb.append(EscapeUtil.escapeXml(val.toString()));\n\t\t\t\t\t\t\ti++;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsb.append(EscapeUtil.escapeXml(value.toString()));\n\t\t\t\t\t}\n\n\t\t\t\t\t// Emit an array of similar keys\n\n\t\t\t\t} else if (StrUtil.isEmptyIfStr(value)) {\n\t\t\t\t\tsb.append(wrapWithTag(null, key));\n\t\t\t\t} else if (value instanceof JSONArray) {\n\t\t\t\t\tfor (Object val : (JSONArray) value) {\n\t\t\t\t\t\tif (val instanceof JSONArray) {\n\t\t\t\t\t\t\tsb.append(wrapWithTag(toXml(val, null, contentKeys), key));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsb.append(toXml(val, key, contentKeys));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsb.append(toXml(value, key, contentKeys));\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Emit the </tagname> close tag\n\t\t\tappendTag(sb, tagName, true);\n\t\t\treturn sb.toString();\n\t\t}\n\n\t\tif (ArrayUtil.isArray(object)) {\n\t\t\tobject = new JSONArray(object);\n\t\t}\n\n\t\tif (object instanceof JSONArray) {\n\t\t\tfor (Object val : (JSONArray) object) {\n\t\t\t\t// XML does not have good support for arrays. If an array\n\t\t\t\t// appears in a place where XML is lacking, synthesize an\n\t\t\t\t// <array> element.\n\t\t\t\tsb.append(toXml(val, tagName == null ? \"array\" : tagName, contentKeys));\n\t\t\t}\n\t\t\treturn sb.toString();\n\t\t}\n\n\t\treturn wrapWithTag(EscapeUtil.escapeXml(object.toString()), tagName);\n\t}\n\n\t/**\n\t * 追加标签\n\t *\n\t * @param sb       XML内容\n\t * @param tagName  标签名\n\t * @param isEndTag 是否结束标签\n\t * @since 5.7.11\n\t */\n\tprivate static void appendTag(StringBuilder sb, String tagName, boolean isEndTag) {\n\t\tif (StrUtil.isNotBlank(tagName)) {\n\t\t\tsb.append('<');\n\t\t\tif (isEndTag) {\n\t\t\t\tsb.append('/');\n\t\t\t}\n\t\t\tsb.append(tagName).append('>');\n\t\t}\n\t}\n\n\t/**\n\t * 将内容使用标签包装为XML\n\t *\n\t * @param tagName 标签名\n\t * @param content 内容\n\t * @return 包装后的XML\n\t * @since 5.7.11\n\t */\n\tprivate static String wrapWithTag(String content, String tagName) {\n\t\tif (StrUtil.isBlank(tagName)) {\n\t\t\treturn StrUtil.wrap(content, \"\\\"\");\n\t\t}\n\n\t\tif (StrUtil.isEmpty(content)) {\n\t\t\treturn \"<\" + tagName + \"/>\";\n\t\t} else {\n\t\t\treturn \"<\" + tagName + \">\" + content + \"</\" + tagName + \">\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java",
    "content": "/*\n * Copyright (c) 2024. looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          https://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json.xml;\n\nimport java.io.Serializable;\n\n/**\n * XML解析为JSON的可选选项<br>\n * 参考：https://github.com/stleary/JSON-java/blob/master/src/main/java/org/json/ParserConfiguration.java\n *\n * @author AylwardJ, Looly\n */\npublic class ParseConfig implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 默认最大嵌套深度\n\t */\n\tpublic static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512;\n\n\t/**\n\t * 创建ParseConfig\n\t *\n\t * @return ParseConfig\n\t */\n\tpublic static ParseConfig of() {\n\t\treturn new ParseConfig();\n\t}\n\n\t/**\n\t * 是否保持值为String类型，如果为{@code false}，则尝试转换为对应类型(numeric, boolean, string)\n\t */\n\tprivate boolean keepStrings;\n\t/**\n\t * 最大嵌套深度，用于解析时限制解析层级，当大于这个层级时抛出异常，-1表示无限制\n\t */\n\tprivate int maxNestingDepth = -1;\n\n\t/**\n\t * 是否保持值为String类型，如果为{@code false}，则尝试转换为对应类型(numeric, boolean, string)\n\t *\n\t * @return 是否保持值为String类型\n\t */\n\tpublic boolean isKeepStrings() {\n\t\treturn keepStrings;\n\t}\n\n\t/**\n\t * 设置是否保持值为String类型，如果为{@code false}，则尝试转换为对应类型(numeric, boolean, string)\n\t *\n\t * @param keepStrings 是否保持值为String类型\n\t * @return this\n\t */\n\tpublic ParseConfig setKeepStrings(final boolean keepStrings) {\n\t\tthis.keepStrings = keepStrings;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取最大嵌套深度，用于解析时限制解析层级，当大于这个层级时抛出异常，-1表示无限制\n\t *\n\t * @return 最大嵌套深度\n\t */\n\tpublic int getMaxNestingDepth() {\n\t\treturn maxNestingDepth;\n\t}\n\n\t/**\n\t * 设置最大嵌套深度，用于解析时限制解析层级，当大于这个层级时抛出异常，-1表示无限制\n\t *\n\t * @param maxNestingDepth 最大嵌套深度\n\t * @return this\n\t */\n\tpublic ParseConfig setMaxNestingDepth(final int maxNestingDepth) {\n\t\tthis.maxNestingDepth = maxNestingDepth;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/main/java/cn/hutool/json/xml/package-info.java",
    "content": "/**\n * JSON与XML相互转换封装，基于json.org官方库改造\n *\n * @author looly\n */\npackage cn.hutool.json.xml;\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/BeanToJsonTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class BeanToJsonTest {\n\t@Test\n\tpublic void toJsonStrTest() {\n\t\tfinal ReadParam readParam = new ReadParam();\n\t\treadParam.setInitSpikeMac(\"a\");\n\t\treadParam.setMac(\"b\");\n\t\treadParam.setSpikeMac(\"c\");\n\t\treadParam.setBag(\"d\");\n\t\treadParam.setProjectId(123);\n\n\t\t//Console.log(JSONUtil.toJsonStr(readParam));\n\t\tassertEquals(\"{\\\"initSpikeMac\\\":\\\"a\\\",\\\"mac\\\":\\\"b\\\",\\\"spikeMac\\\":\\\"c\\\",\\\"bag\\\":\\\"d\\\",\\\"projectId\\\":123}\", JSONUtil.toJsonStr(readParam));\n\t}\n\n\t@Data\n\tprivate static class ReadParam{\n\t\tprivate String initSpikeMac;\n\t\tprivate String mac;\n\t\tprivate String spikeMac;\n\t\tprivate String bag;\n\t\tprivate Integer projectId;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/CustomSerializeTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.json.serialize.JSONObjectSerializer;\nimport lombok.ToString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\npublic class CustomSerializeTest {\n\n\t@Test\n\tpublic void serializeTest() {\n\t\tJSONUtil.putSerializer(CustomBean.class, (JSONObjectSerializer<CustomBean>) (json, bean) -> json.set(\"customName\", bean.name));\n\n\t\tCustomBean customBean = new CustomBean();\n\t\tcustomBean.name = \"testName\";\n\n\t\tJSONObject obj = JSONUtil.parseObj(customBean);\n\t\tassertEquals(\"testName\", obj.getStr(\"customName\"));\n\t}\n\n\t@Test\n\tpublic void deserializeTest() {\n\t\tJSONUtil.putDeserializer(CustomBean.class, json -> {\n\t\t\tCustomBean customBean = new CustomBean();\n\t\t\tcustomBean.name = ((JSONObject)json).getStr(\"customName\");\n\t\t\treturn customBean;\n\t\t});\n\n\t\tString jsonStr = \"{\\\"customName\\\":\\\"testName\\\"}\";\n\t\tCustomBean bean = JSONUtil.parseObj(jsonStr).toBean(CustomBean.class);\n\t\tassertEquals(\"testName\", bean.name);\n\t}\n\n\t@ToString\n\tpublic static class CustomBean {\n\t\tpublic String name;\n\t\tpublic String b;\n\t\tpublic Date date;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue1075Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue1075Test {\n\n\tfinal String jsonStr = \"{\\\"f1\\\":\\\"f1\\\",\\\"f2\\\":\\\"f2\\\",\\\"fac\\\":\\\"fac\\\"}\";\n\n\t@Test\n\tpublic void testToBean() {\n\t\t// 在不忽略大小写的情况下，f2、fac都不匹配\n\t\tObjA o2 = JSONUtil.toBean(jsonStr, ObjA.class);\n\t\tassertNull(o2.getFAC());\n\t\tassertNull(o2.getF2());\n\t}\n\n\t@Test\n\tpublic void testToBeanIgnoreCase() {\n\t\t// 在忽略大小写的情况下，f2、fac都匹配\n\t\tObjA o2 = JSONUtil.parseObj(jsonStr, JSONConfig.create().setIgnoreCase(true)).toBean(ObjA.class);\n\n\t\tassertEquals(\"fac\", o2.getFAC());\n\t\tassertEquals(\"f2\", o2.getF2());\n\t}\n\n\t@Data\n\tpublic static class ObjA {\n\t\tprivate String f1;\n\t\tprivate String F2;\n\t\tprivate String FAC;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue1101Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.TypeReference;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Comparator;\nimport java.util.TreeSet;\n\n/**\n * 测试转换为TreeSet是否成功。 TreeSet必须有默认的比较器\n */\npublic class Issue1101Test {\n\n\t@Test\n\tpublic void treeMapConvertTest(){\n\t\tString json = \"[{\\\"nodeName\\\":\\\"admin\\\",\\\"treeNodeId\\\":\\\"00010001_52c95b83-2083-4138-99fb-e6e21f0c1277\\\",\\\"sort\\\":0,\\\"type\\\":10,\\\"parentId\\\":\\\"00010001\\\",\\\"children\\\":[],\\\"id\\\":\\\"52c95b83-2083-4138-99fb-e6e21f0c1277\\\",\\\"status\\\":true},{\\\"nodeName\\\":\\\"test\\\",\\\"treeNodeId\\\":\\\"00010001_97054a82-f8ff-46a1-b76c-cbacf6d18045\\\",\\\"sort\\\":0,\\\"type\\\":10,\\\"parentId\\\":\\\"00010001\\\",\\\"children\\\":[],\\\"id\\\":\\\"97054a82-f8ff-46a1-b76c-cbacf6d18045\\\",\\\"status\\\":true}]\";\n\t\tfinal JSONArray objects = JSONUtil.parseArray(json);\n\t\tfinal TreeSet<TreeNodeDto> convert = Convert.convert(new TypeReference<TreeSet<TreeNodeDto>>() {\n\t\t}, objects);\n\t\tassertEquals(2, convert.size());\n\t}\n\n\t@Test\n\tpublic void test(){\n\t\tString json = \"{\\n\" +\n\t\t\t\t\"\\t\\\"children\\\": [{\\n\" +\n\t\t\t\t\"\\t\\t\\\"children\\\": [],\\n\" +\n\t\t\t\t\"\\t\\t\\\"id\\\": \\\"52c95b83-2083-4138-99fb-e6e21f0c1277\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"nodeName\\\": \\\"admin\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"parentId\\\": \\\"00010001\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"sort\\\": 0,\\n\" +\n\t\t\t\t\"\\t\\t\\\"status\\\": true,\\n\" +\n\t\t\t\t\"\\t\\t\\\"treeNodeId\\\": \\\"00010001_52c95b83-2083-4138-99fb-e6e21f0c1277\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"type\\\": 10\\n\" +\n\t\t\t\t\"\\t}, {\\n\" +\n\t\t\t\t\"\\t\\t\\\"children\\\": [],\\n\" +\n\t\t\t\t\"\\t\\t\\\"id\\\": \\\"97054a82-f8ff-46a1-b76c-cbacf6d18045\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"nodeName\\\": \\\"test\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"parentId\\\": \\\"00010001\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"sort\\\": 0,\\n\" +\n\t\t\t\t\"\\t\\t\\\"status\\\": true,\\n\" +\n\t\t\t\t\"\\t\\t\\\"treeNodeId\\\": \\\"00010001_97054a82-f8ff-46a1-b76c-cbacf6d18045\\\",\\n\" +\n\t\t\t\t\"\\t\\t\\\"type\\\": 10\\n\" +\n\t\t\t\t\"\\t}],\\n\" +\n\t\t\t\t\"\\t\\\"id\\\": \\\"00010001\\\",\\n\" +\n\t\t\t\t\"\\t\\\"nodeName\\\": \\\"测试\\\",\\n\" +\n\t\t\t\t\"\\t\\\"parentId\\\": \\\"0001\\\",\\n\" +\n\t\t\t\t\"\\t\\\"sort\\\": 0,\\n\" +\n\t\t\t\t\"\\t\\\"status\\\": true,\\n\" +\n\t\t\t\t\"\\t\\\"treeNodeId\\\": \\\"00010001\\\",\\n\" +\n\t\t\t\t\"\\t\\\"type\\\": 0\\n\" +\n\t\t\t\t\"}\";\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(json);\n\n\t\tfinal TreeNode treeNode = JSONUtil.toBean(jsonObject, TreeNode.class);\n\t\tassertEquals(2, treeNode.getChildren().size());\n\n\t\tTreeNodeDto dto = new TreeNodeDto();\n\t\tBeanUtil.copyProperties(treeNode, dto, true);\n\t\tassertEquals(2, dto.getChildren().size());\n\t}\n\n\t@Data\n\tpublic static class TreeNodeDto {\n\t\tprivate String id;\n\t\tprivate String parentId;\n\t\tprivate int sort;\n\t\tprivate String nodeName;\n\t\tprivate int type;\n\t\tprivate Boolean status;\n\t\tprivate String treeNodeId;\n\t\tprivate TreeSet<TreeNodeDto> children = new TreeSet<>(Comparator.comparing(o -> o.id));\n\t}\n\n\t@Data\n\tpublic static class TreeNode implements Comparable<TreeNode> {\n\t\tprivate String id;\n\t\tprivate String parentId;\n\t\tprivate int sort;\n\t\tprivate String nodeName;\n\t\tprivate int type;\n\t\tprivate Boolean status;\n\t\tprivate String treeNodeId;\n\t\tprivate TreeSet<TreeNode> children = new TreeSet<>();\n\n\t\t@Override\n\t\tpublic int compareTo(TreeNode o) {\n\t\t\treturn id.compareTo(o.getId());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue1200Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.json.test.bean.ResultBean;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 测试在bean转换时使用BeanConverter，默认忽略转换失败的字段。\n * 现阶段Converter的问题在于，无法更细粒度的控制转换失败的范围，例如Bean的一个字段为List，\n * list任意一个item转换失败都会导致这个list为null。\n *\n * TODO 需要在Converter中添加ConvertOption，用于更细粒度的控制转换规则\n */\npublic class Issue1200Test {\n\n\t@Test\n\t@Disabled\n\tpublic void toBeanTest(){\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str(\"issue1200.json\"));\n\t\tConsole.log(jsonObject);\n\n\t\tfinal ResultBean resultBean = jsonObject.toBean(ResultBean.class);\n\t\tConsole.log(resultBean);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.Month;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/2090\n */\npublic class Issue2090Test {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tfinal TestBean test = new TestBean();\n\t\ttest.setLocalDate(LocalDate.now());\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(test);\n\t\tfinal TestBean test1 = json.toBean(TestBean.class);\n\t\tassertEquals(test, test1);\n\t}\n\n\t@Test\n\tpublic void parseLocalDateTest(){\n\t\tLocalDate localDate = LocalDate.now();\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(localDate);\n\t\tassertNotNull(jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void toBeanLocalDateTest(){\n\t\tLocalDate d = LocalDate.now();\n\t\tfinal JSONObject obj = JSONUtil.parseObj(d);\n\t\tLocalDate d2 = obj.toBean(LocalDate.class);\n\t\tassertEquals(d, d2);\n\t}\n\n\t@Test\n\tpublic void toBeanLocalDateTimeTest(){\n\t\tLocalDateTime d = LocalDateTime.now();\n\t\tfinal JSONObject obj = JSONUtil.parseObj(d);\n\t\tLocalDateTime d2 = obj.toBean(LocalDateTime.class);\n\t\tassertEquals(d, d2);\n\t}\n\n\t@Test\n\tpublic void toBeanLocalTimeTest(){\n\t\tLocalTime d = LocalTime.now();\n\t\tfinal JSONObject obj = JSONUtil.parseObj(d);\n\t\tLocalTime d2 = obj.toBean(LocalTime.class);\n\t\tassertEquals(d, d2);\n\t}\n\n\t@Test\n\tpublic void monthTest(){\n\t\tfinal JSONObject jsonObject = new JSONObject();\n\t\tjsonObject.set(\"month\", Month.JANUARY);\n\t\tassertEquals(\"{\\\"month\\\":1}\", jsonObject.toString());\n\t}\n\n\t@Data\n\tpublic static class TestBean{\n\t\tprivate LocalDate localDate;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2131Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.collection.ListUtil;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.beans.Transient;\nimport java.util.List;\nimport java.util.stream.Stream;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/2131<br>\n * 字段定义成final，意味着setCollections无效，因此JSON转Bean的时候无法调用setCollections注入，所以是空的。\n */\npublic class Issue2131Test {\n\n\t@Test\n\tpublic void strToBean() {\n\t\tGoodsResponse goodsResponse = new GoodsResponse();\n\t\tGoodsItem apple = new GoodsItem().setGoodsId(1L).setGoodsName(\"apple\").setChannel(\"wechat\");\n\t\tGoodsItem pear = new GoodsItem().setGoodsId(2L).setGoodsName(\"pear\").setChannel(\"jd\");\n\t\tfinal List<GoodsItem> collections = goodsResponse.getCollections();\n\t\tStream.of(apple, pear).forEach(collections::add);\n\n\t\tString jsonStr = JSONUtil.toJsonStr(goodsResponse);\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(jsonStr);\n\n\t\tGoodsResponse result = jsonObject.toBean(GoodsResponse.class);\n\t\tassertEquals(0, result.getCollections().size());\n\t}\n\n\t@Data\n\tstatic class BaseResponse {\n\n\t\t@SuppressWarnings(\"unused\")\n\t\t@Transient\n\t\tpublic final boolean successful() {\n\t\t\treturn code == 200 || code == 201;\n\t\t}\n\n\t\tprivate int code = 200;\n\t\tprivate String message;\n\t}\n\n\t@EqualsAndHashCode(callSuper = true)\n\t@Data\n\tstatic class GoodsResponse extends BaseResponse {\n\t\t// 由于定义成了final形式，setXXX无效，导致无法注入。\n\t\tprivate final List<GoodsItem> collections = ListUtil.list(false);\n\t}\n\n\t@Data\n\t@Accessors(chain = true)\n\tstatic class GoodsItem{\n\t\tprivate long goodsId;\n\t\tprivate String goodsName;\n\t\tprivate String channel;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2223Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class Issue2223Test {\n\n\t@Test\n\tpublic void toStrOrderTest() {\n\t\tMap<String, Long> m1 = new LinkedHashMap<>();\n\t\tfor (long i = 0; i < 5; i++) {\n\t\t\tm1.put(\"2022/\" + i, i);\n\t\t}\n\n\t\tassertEquals(\"{\\\"2022/0\\\":0,\\\"2022/1\\\":1,\\\"2022/2\\\":2,\\\"2022/3\\\":3,\\\"2022/4\\\":4}\", JSONUtil.toJsonStr(m1));\n\n\t\tMap<String, Map<String, Long>> map1 = new HashMap<>();\n\t\tmap1.put(\"m1\", m1);\n\n\t\tassertEquals(\"{\\\"m1\\\":{\\\"2022/0\\\":0,\\\"2022/1\\\":1,\\\"2022/2\\\":2,\\\"2022/3\\\":3,\\\"2022/4\\\":4}}\",\n\t\t\t\tJSONUtil.toJsonStr(map1, JSONConfig.create()));\n\n\t\tfinal BeanDemo beanDemo = new BeanDemo();\n\t\tbeanDemo.setMap1(map1);\n\t\tassertEquals(\"{\\\"map1\\\":{\\\"m1\\\":{\\\"2022/0\\\":0,\\\"2022/1\\\":1,\\\"2022/2\\\":2,\\\"2022/3\\\":3,\\\"2022/4\\\":4}}}\",\n\t\t\t\tJSONUtil.toJsonStr(beanDemo, JSONConfig.create()));\n\t}\n\n\t@Data\n\tstatic class BeanDemo {\n\t\tMap<String, Map<String, Long>> map1;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2365Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2365Test {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tString jsonStr = \"{\\\"fileName\\\":\\\"aaa\\\",\\\"fileBytes\\\":\\\"AQ==\\\"}\";\n\t\tfinal FileInfo fileInfo = JSONUtil.toBean(jsonStr, FileInfo.class);\n\t\tassertEquals(\"aaa\", fileInfo.getFileName());\n\t\tassertArrayEquals(new byte[]{1}, fileInfo.getFileBytes());\n\t}\n\n\t@Data\n\tpublic static class FileInfo {\n\t\tprivate String fileName;\n\t\tprivate byte[] fileBytes;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2369Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2369Test {\n\n\t@Test\n\tpublic void toJsonStrTest(){\n\t\t//https://github.com/chinabugotech/hutool/issues/2369\n\t\t// byte[]数组对于JSONArray来说，即可能是一个JSON字符串的二进制流，也可能是普通数组，因此需要做双向兼容\n\t\tfinal byte[] bytes = {10, 11};\n\t\tfinal String s = JSONUtil.toJsonStr(bytes);\n\t\tassertEquals(\"[10,11]\", s);\n\n\t\tfinal Object o = JSONUtil.toBean(s, byte[].class, false);\n\t\tassertArrayEquals(bytes, (byte[])o);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2377Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class Issue2377Test {\n\n\t@Test\n\tpublic void bytesTest() {\n\t\tfinal Object[] paramArray = new Object[]{1, new byte[]{10, 11}, \"报表.xlsx\"};\n\t\tfinal String paramsStr = JSONUtil.toJsonStr(paramArray);\n\t\tassertEquals(\"[1,[10,11],\\\"报表.xlsx\\\"]\", paramsStr);\n\n\t\tfinal List<Object> paramList = JSONUtil.toList(paramsStr, Object.class);\n\n\t\tfinal String paramBytesStr = JSONUtil.toJsonStr(paramList.get(1));\n\t\tassertEquals(\"[10,11]\", paramBytesStr);\n\n\t\tfinal byte[] paramBytes = JSONUtil.toBean(paramBytesStr, byte[].class, false);\n\t\tassertArrayEquals((byte[]) paramArray[1], paramBytes);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2447Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\n\npublic class Issue2447Test {\n\n\t@Test\n\tpublic void addInteger() {\n\t\tTime time = new Time();\n\t\ttime.setTime(LocalDateTime.of(1970, 1, 2, 10, 0, 1, 0));\n\t\tString timeStr = JSONUtil.toJsonStr(time);\n\t\tassertEquals(\"{\\\"time\\\":93601000}\", timeStr);\n\t\tassertEquals(JSONUtil.toBean(timeStr, Time.class).getTime(), time.getTime());\n\t}\n\n\tstatic class Time {\n\t\tprivate LocalDateTime time;\n\n\t\tpublic LocalDateTime getTime() {\n\t\t\treturn time;\n\t\t}\n\n\t\tpublic void setTime(LocalDateTime time) {\n\t\t\tthis.time = time;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn time.toString();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2555Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.json.serialize.JSONDeserializer;\nimport cn.hutool.json.serialize.JSONObjectSerializer;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2555Test {\n\n\t@Test\n\tpublic void serAndDeserTest(){\n\t\tJSONUtil.putSerializer(MyType.class, new MySerializer());\n\t\tJSONUtil.putDeserializer(MyType.class, new MyDeserializer());\n\n\t\tfinal SimpleObj simpleObj = new SimpleObj();\n\t\tfinal MyType child = new MyType();\n\t\tchild.setAddress(\"addrValue1\");\n\t\tsimpleObj.setMyType(child);\n\n\t\tfinal String json = JSONUtil.toJsonStr(simpleObj);\n\t\tassertEquals(\"{\\\"myType\\\":{\\\"addr\\\":\\\"addrValue1\\\"}}\", json);\n\n\t\t//MyDeserializer不会被调用\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(json, JSONConfig.create().setIgnoreError(false));\n\t\tfinal SimpleObj simpleObj2 = jsonObject.toBean(SimpleObj.class);\n\t\tassertEquals(\"addrValue1\", simpleObj2.getMyType().getAddress());\n\t}\n\n\t@Test\n\tpublic void deserTest(){\n\t\tJSONUtil.putDeserializer(MyType.class, new MyDeserializer());\n\n\t\tfinal String jsonStr = \"{\\\"myType\\\":{\\\"addr\\\":\\\"addrValue1\\\"}}\";\n\n\t\t//MyDeserializer不会被调用\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(jsonStr, JSONConfig.create().setIgnoreError(false));\n\t\tfinal SimpleObj simpleObj2 = jsonObject.toBean(SimpleObj.class);\n\t\tassertEquals(\"addrValue1\", simpleObj2.getMyType().getAddress());\n\t}\n\n\t@Data\n\tpublic static class MyType {\n\t\tprivate String address;\n\t}\n\t@Data\n\tpublic static class SimpleObj {\n\t\tprivate MyType myType;\n\t}\n\n\tpublic static class MySerializer implements JSONObjectSerializer<MyType> {\n\t\t@Override\n\t\tpublic void serialize(JSONObject json, MyType bean) {\n\t\t\tjson.set(\"addr\", bean.getAddress());\n\t\t}\n\t}\n\n\tpublic static class MyDeserializer implements JSONDeserializer<MyType>{\n\t\t@Override\n\t\tpublic MyType deserialize(JSON json) {\n\t\t\tfinal MyType myType = new MyType();\n\t\t\tmyType.setAddress(((JSONObject)json).getStr(\"addr\"));\n\t\t\treturn myType;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2572Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.TypeReference;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.DayOfWeek;\nimport java.time.Month;\nimport java.time.MonthDay;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class Issue2572Test {\n\n\t@Test\n\tpublic void putDayOfWeekTest(){\n\t\tfinal Set<DayOfWeek> weeks = new HashSet<>();\n\t\tweeks.add(DayOfWeek.MONDAY);\n\t\tfinal JSONObject obj = new JSONObject();\n\t\tobj.set(\"weeks\", weeks);\n\t\tassertEquals(\"{\\\"weeks\\\":[1]}\", obj.toString());\n\n\t\tfinal Map<String, Set<DayOfWeek>> monthDays1 = obj.toBean(new TypeReference<Map<String, Set<DayOfWeek>>>() {\n\t\t});\n\t\tassertEquals(\"{weeks=[MONDAY]}\", monthDays1.toString());\n\t}\n\n\t@Test\n\tpublic void putMonthTest(){\n\t\tfinal Set<Month> months = new HashSet<>();\n\t\tmonths.add(Month.DECEMBER);\n\t\tfinal JSONObject obj = new JSONObject();\n\t\tobj.set(\"months\", months);\n\t\tassertEquals(\"{\\\"months\\\":[12]}\", obj.toString());\n\n\t\tfinal Map<String, Set<Month>> monthDays1 = obj.toBean(new TypeReference<Map<String, Set<Month>>>() {\n\t\t});\n\t\tassertEquals(\"{months=[DECEMBER]}\", monthDays1.toString());\n\t}\n\n\t@Test\n\tpublic void putMonthDayTest(){\n\t\tfinal Set<MonthDay> monthDays = new HashSet<>();\n\t\tmonthDays.add(MonthDay.of(Month.DECEMBER, 1));\n\t\tfinal JSONObject obj = new JSONObject();\n\t\tobj.set(\"monthDays\", monthDays);\n\t\tassertEquals(\"{\\\"monthDays\\\":[\\\"--12-01\\\"]}\", obj.toString());\n\n\t\tfinal Map<String, Set<MonthDay>> monthDays1 = obj.toBean(new TypeReference<Map<String, Set<MonthDay>>>() {\n\t\t});\n\t\tassertEquals(\"{monthDays=[--12-01]}\", monthDays1.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2746Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class Issue2746Test {\n\t@Test\n\tpublic void parseObjTest() {\n\t\tfinal String str = StrUtil.repeat(\"{\", 1500) + StrUtil.repeat(\"}\", 1500);\n\t\ttry{\n\t\t\tJSONUtil.parseObj(str);\n\t\t} catch (final JSONException e){\n\t\t\tassertTrue(e.getMessage().startsWith(\"A JSONObject can not directly nest another JSONObject or JSONArray\"));\n\t\t}\n\t}\n\n\t@Test\n\tpublic void parseTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tfinal String str = StrUtil.repeat(\"[\", 1500) + StrUtil.repeat(\"]\", 1500);\n\t\t\tJSONUtil.parseArray(str);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2749Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Issue2749Test {\n\n\t@Test\n\t@Disabled\n\tpublic void jsonObjectTest() {\n\t\tfinal Map<String, Object> map = new HashMap<>(1, 1f);\n\t\tMap<String, Object> node = map;\n\t\tfor (int i = 0; i < 1000; i++) {\n\t\t\t//noinspection unchecked\n\t\t\tnode = (Map<String, Object>) node.computeIfAbsent(\"a\", k -> new HashMap<String, Object>(1, 1f));\n\t\t}\n\t\tnode.put(\"a\", 1);\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(map);\n\n\t\t@SuppressWarnings(\"MismatchedQueryAndUpdateOfCollection\")\n\t\tfinal JSONObject jsonObject = new JSONObject(jsonStr);\n\t\tassertNotNull(jsonObject);\n\n\t\t// 栈溢出\n\t\t//noinspection ResultOfMethodCallIgnored\n\t\tjsonObject.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2953Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2953Test {\n\t@Test\n\tpublic void parseObjWithBigNumberTest() {\n\t\tfinal String a = \"{\\\"a\\\": 114793903847679990000000000000000000000}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(a, JSONConfig.create());\n\t\tassertEquals(\"{\\\"a\\\":\\\"114793903847679990000000000000000000000\\\"}\", jsonObject.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue2997Test.java",
    "content": "package cn.hutool.json;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class Issue2997Test {\n\t@Test\n\tpublic void toBeanTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/2997\n\t\tfinal Object o = JSONUtil.toBean(\"{}\", Object.class);\n\t\tassertEquals(JSONObject.class, o.getClass());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3051Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/3051\n */\npublic class Issue3051Test {\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(new EmptyBean(),\n\t\t\tJSONConfig.create().setIgnoreError(true));\n\n\t\tassertEquals(\"{}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void parseTest2() {\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(new EmptyBean());\n\n\t\tassertEquals(\"{}\", jsonObject.toString());\n\t}\n\n\t@Data\n\tstatic class EmptyBean {\n\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3086Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.json.serialize.JSONObjectSerializer;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class Issue3086Test {\n\n\t@Test\n\tpublic void serializeTest() {\n\t\tJSONUtil.putSerializer(TestBean.class, new TestBean());\n\n\t\tfinal List<SimpleGrantedAuthority> strings = ListUtil.of(\n\t\t\tnew SimpleGrantedAuthority(\"ROLE_admin\"),\n\t\t\tnew SimpleGrantedAuthority(\"ROLE_normal\")\n\t\t);\n\t\tfinal TestBean testBean = new TestBean();\n\t\ttestBean.setAuthorities(strings);\n\n\t\tassertEquals(\"{\\\"authorities\\\":[\\\"ROLE_admin\\\",\\\"ROLE_normal\\\"]}\",\n\t\t\tJSONUtil.toJsonStr(testBean));\n\t}\n\n\tstatic class SimpleGrantedAuthority {\n\t\tprivate final String role;\n\n\t\tpublic SimpleGrantedAuthority(final String role) {\n\t\t\tthis.role = role;\n\t\t}\n\n\t\tpublic String getAuthority() {\n\t\t\treturn this.role;\n\t\t}\n\n\t\tpublic String toString() {\n\t\t\treturn this.role;\n\t\t}\n\t}\n\n\t@Data\n\tstatic class TestBean implements JSONObjectSerializer<TestBean>{\n\t\tprivate Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();\n\n\t\t@Override\n\t\tpublic void serialize(final JSONObject json, final TestBean testBean) {\n\t\t\tfinal List<String> strings = testBean.getAuthorities()\n\t\t\t\t.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());\n\t\t\tjson.set(\"authorities\",strings);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3139Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport cn.hutool.core.util.XmlUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class Issue3139Test {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal String xml = \"<r>\\n\" +\n\t\t\t\"  <c>\\n\" +\n\t\t\t\"     <s>1</s>\\n\" +\n\t\t\t\"     <p>str</p>\\n\" +\n\t\t\t\"  </c>\\n\" +\n\t\t\t\"</r>\";\n\n\t\tfinal JSONObject jsonObject = XmlUtil.xmlToBean(XmlUtil.parseXml(xml).getDocumentElement(), JSONObject.class);\n\t\tfinal R bean = jsonObject.toBean(R.class);\n\t\tfinal List<C> c = bean.getC();\n\t\tassertEquals(1, c.size());\n\t\tassertEquals(\"1\", c.get(0).getS());\n\t\tassertEquals(\"str\", c.get(0).getP());\n\t}\n\n\t@Data\n\tstatic class C {\n\t\tString s;\n\t\tString p;\n\t}\n\n\t@Data\n\tstatic class R {\n\t\tList<C> c;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3274Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport lombok.Data;\nimport lombok.Getter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3274Test {\n\t@Test\n\tpublic void toBeanTest(){\n\t\tfinal JSONObject entries = new JSONObject(\"{\\n\" +\n\t\t\t\"    \\n\" +\n\t\t\t\"    \\\"age\\\": 36,\\n\" +\n\t\t\t\"    \\\"gender\\\": \\\"\\\",\\n\" +\n\t\t\t\"    \\\"id\\\": \\\"123123123\\\"\\n\" +\n\t\t\t\"}\", JSONConfig.create().setIgnoreError(true));\n\t\tfinal LarkCoreHrPersonal larkCoreHrPersonal = entries.toBean(LarkCoreHrPersonal.class);\n\t\tassertNotNull(larkCoreHrPersonal);\n\t}\n\n\t@Data\n\tstatic class LarkCoreHrPersonal {\n\t\tprivate String id;\n\t\tprivate String age=\"\";\n\t\tprivate Gender gender;\n\t}\n\n\t@Getter\n\tenum Gender {\n\t\tmale(\"male\",\"Male\",\"男\"),\n\t\tfemale(\"female\",\"Female\",\"女\"),\n\t\tother(\"other\",\"Other\",\"其他\");\n\t\tprivate JSONArray display;\n\t\tprivate  String enum_name;\n\t\tGender(final String enum_name, final String en_Us, final String zh_CN){\n\t\t\tthis.enum_name=enum_name;\n\t\t\tthis.display=new JSONArray(\"[{\\\"lang\\\": \\\"en-US\\\",\\\"value\\\": \\\"\"+en_Us+\"\\\"},{\\\"lang\\\": \\\"zh-CN\\\",\\\"value\\\": \\\"\"+zh_CN+\"\\\"}]\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3289Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class Issue3289Test {\n\t@Test\n\tpublic void parseTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tfinal String s = \"{\\\"a\\\":1,[6E962756779]}\";\n\t\t\tJSONUtil.parse(s);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3504Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/3504\n */\npublic class Issue3504Test {\n\n\t@Test\n\tpublic void test3504() {\n\t\tJsonBean jsonBean = new JsonBean();\n\t\tjsonBean.setName(\"test\");\n\t\tjsonBean.setClasses(new Class[]{String.class});\n\t\tString huToolJsonStr = JSONUtil.toJsonStr(jsonBean);\n\t\tfinal JsonBean bean = JSONUtil.toBean(huToolJsonStr, JsonBean.class);\n\t\tassertNotNull(bean);\n\t\tassertEquals(\"test\", bean.getName());\n\t}\n\n\t@Data\n\tpublic static class JsonBean {\n\t\tprivate String name;\n\t\tprivate Class<?>[] classes;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3506Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/3506\n */\npublic class Issue3506Test {\n\n\t@Test\n\tpublic void test3506() {\n\t\tLanguages languages = new Languages();\n\t\tlanguages.setLanguageType(Java.class);\n\t\tString hutoolJSONString = JSONUtil.toJsonStr(languages);\n\t\tfinal Languages bean = JSONUtil.toBean(hutoolJSONString, Languages.class);\n\t\tassertNotNull(bean);\n\t\tassertEquals(bean.getLanguageType(), Java.class);\n\t}\n\n\t@Data\n\tpublic static class Languages {\n\t\tprivate Class<? extends Language> languageType;\n\t}\n\n\tpublic interface Language {\n\t}\n\n\tpublic static class Java implements Language {\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3541Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3541Test {\n\t@Test\n\tpublic void longToStringTest() {\n\t\tDemo demo = new Demo();\n\t\tdemo.setId(1227690722069581409L);\n\t\tdemo.setName(\"hutool\");\n\t\tString jsonStr = JSONUtil.toJsonStr(demo, JSONConfig.create().setWriteLongAsString(true));\n\t\tassertEquals(\"{\\\"id\\\":\\\"1227690722069581409\\\",\\\"name\\\":\\\"hutool\\\"}\", jsonStr);\n\t}\n\n\t@Data\n\tpublic static class Demo {\n\t\tprivate Long id;\n\t\tprivate String name;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3588Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Issue3588Test {\n\n\t@Test\n\tpublic void toBeanIgnoreCaseTest() {\n\t\tString json = \"{id: 1, code: 1122, tsemaphores: [{type: 1, status: 12}]}\";\n\t\tAttrData attrData = JSONUtil.toBean(json, JSONConfig.create().setIgnoreCase(true), AttrData.class);\n\t\tassertEquals(\"1\", attrData.getId());\n\t\tassertEquals(\"1122\", attrData.getCode());\n\t\tassertEquals(\"1\", attrData.getTSemaphores().get(0).getType());\n\t\tassertEquals(\"12\", attrData.getTSemaphores().get(0).getStatus());\n\t}\n\n\t@Data\n\tstatic class AttrData {\n\t\tprivate String id;\n\t\tprivate String code;\n\t\tprivate List<TSemaphore> tSemaphores = new ArrayList<>();\n\t}\n\n\t@Data\n\tstatic class TSemaphore{\n\t\tprivate String type;\n\t\tprivate String status;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3619Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3619Test {\n\t@Test\n\tpublic void parseObjTest() {\n\t\tString json = \"{\\\"@timestamp\\\":\\\"2024-06-14T00:02:06.438Z\\\",\\\"@version\\\":\\\"1\\\",\\\"int_arr\\\":[-4]}\";\n\t\tJSONConfig jsonConfig = JSONConfig.create().setKeyComparator(String.CASE_INSENSITIVE_ORDER);\n\t\tJSONObject jsonObject = JSONUtil.parseObj(json, jsonConfig);\n\n\t\tfinal String jsonStr = jsonObject.toJSONString(0, pair -> {\n\t\t\tfinal Object key = pair.getKey();\n\t\t\tif(key instanceof String){\n\t\t\t\t// 只有key为String时才检查并过滤，其它类型的key，如int类型的key跳过\n\t\t\t\treturn key.toString().equals(\"int_arr\");\n\t\t\t}else{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t});\n\n\t\tassertEquals(\"{\\\"int_arr\\\":[-4]}\", jsonStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3649Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class Issue3649Test {\n\t@Test\n\tpublic void toEmptyBeanTest() {\n\t\tfinal Object bean = JSONUtil.toBean(\"{}\", JSONConfig.create().setIgnoreError(true), EmptyBean.class);\n\t\tassertEquals(new EmptyBean(), bean);\n\t}\n\n\t@Data\n\tpublic static class EmptyBean {}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3713Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.ParseException;\nimport java.util.Date;\n\npublic class Issue3713Test {\n\t@Test\n\tvoid toBeanTest() throws ParseException {\n\t\tString jsonStr = \"{\\\"operDate\\\":\\\"Aug 22, 2024, 4:21:21 PM\\\"}\";\n\t\tfinal CustomerCreateLog bean = JSONUtil.toBean(jsonStr, JSONConfig.create().setDateFormat(\"MMM dd, yyyy, h:mm:ss a\"), CustomerCreateLog.class);\n\t\tAssertions.assertEquals(\"Thu Aug 22 16:21:21 CST 2024\", bean.getOperDate().toString());\n\t}\n\n\t@Data\n\tprivate static class CustomerCreateLog{\n\t\tprivate Date operDate;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3759Test.java",
    "content": "package cn.hutool.json;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3759Test {\n\t@Test\n\tvoid parseTest() {\n\t\tString jsonArrayStr = \"[null]\";\n\t\tfinal JSONArray objects = JSONUtil.parseArray(jsonArrayStr,\n\t\t\tJSONConfig.create().setIgnoreNullValue(true));\n\t\tAssertions.assertTrue(objects.isEmpty());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3790Test.java",
    "content": "package cn.hutool.json;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\n\npublic class Issue3790Test {\n\t@Test\n\tvoid bigDecimalToStringTest() {\n\t\tBigDecimal bigDecimal = new BigDecimal(\"0.01\");\n\t\tbigDecimal = bigDecimal.setScale(4, RoundingMode.HALF_UP);\n\n\t\tDto dto = new Dto();\n\t\tdto.remain = bigDecimal;\n\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(dto, JSONConfig.create().setStripTrailingZeros(false));\n\t\tAssertions.assertEquals(\"{\\\"remain\\\":0.0100}\", jsonStr);\n\t}\n\n\tstatic class Dto {\n\t\tpublic BigDecimal remain;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue3795Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.TypeReference;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class Issue3795Test {\n\t@Test\n\tvoid toBeanTest() {\n\t\tString fieldMapping = \"[{\\\"lable\\\":\\\"id\\\",\\\"value\\\":\\\"id\\\"},{\\\"lable\\\":\\\"name\\\",\\\"value\\\":\\\"name\\\"},{\\\"lable\\\":\\\"age\\\",\\\"value\\\":\\\"age\\\"}]\";\n\t\tAssertions.assertThrows(UnsupportedOperationException.class, ()->{\n\t\t\tJSONUtil.toBean(fieldMapping, new TypeReference<Map<String, String>>() {}, false);\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue4197Test.java",
    "content": "package cn.hutool.json;\r\n\r\nimport lombok.AllArgsConstructor;\r\nimport lombok.Data;\r\nimport lombok.NoArgsConstructor;\r\nimport org.junit.jupiter.api.Assertions;\r\nimport org.junit.jupiter.api.Test;\r\n\r\nimport java.math.BigDecimal;\r\n\r\npublic class Issue4197Test {\r\n\t@Data\r\n\t@NoArgsConstructor\r\n\t@AllArgsConstructor\r\n\tstatic\r\n\tclass TestDTO {\r\n\r\n\t\tprivate BigDecimal h;\r\n\t}\r\n\r\n\t@Test\r\n\tvoid toBeanTest() {\r\n\t\tfinal TestDTO bean = JSONUtil.toBean(\"{\\\"h\\\":\\\"123，456，789\\\"}\", TestDTO.class);\r\n\t\tAssertions.assertEquals(new BigDecimal(\"123456789\"), bean.getH());\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue4210Test.java",
    "content": "package cn.hutool.json;\r\n\r\nimport cn.hutool.core.exceptions.InvocationTargetRuntimeException;\r\nimport cn.hutool.core.lang.Console;\r\nimport cn.hutool.core.lang.ObjectId;\r\nimport lombok.AllArgsConstructor;\r\nimport lombok.Data;\r\nimport lombok.EqualsAndHashCode;\r\nimport org.junit.jupiter.api.Assertions;\r\nimport org.junit.jupiter.api.Test;\r\n\r\nimport java.io.Serializable;\r\n\r\npublic class Issue4210Test {\r\n\r\n\t@Test\r\n\tvoid setValueTest(){\r\n\t\tfinal JSONObject jsonObject = new JSONObject();\r\n\r\n\t\tfinal TestEntity entity = new TestEntity(\"张三\", \"社畜\");\r\n\t\tAssertions.assertThrows(InvocationTargetRuntimeException.class, () -> jsonObject.set(\"entity\",entity));\r\n\t}\r\n\r\n\t@Data\r\n\tpublic static class BaseEntity implements Serializable {\r\n\r\n\t\t/**\r\n\t\t * 实体唯一标识符\r\n\t\t */\r\n\t\tprivate String _id;\r\n\t\tprivate ObjectId id;\r\n\r\n\t\tpublic String get_id() {\r\n\t\t\treturn _id == null ? id.toString() : _id;\r\n\t\t}\r\n\t}\r\n\r\n\t@EqualsAndHashCode(callSuper = true)\r\n\t@Data\r\n\t@AllArgsConstructor\r\n\tpublic class TestEntity extends BaseEntity{\r\n\t\tprivate String name;\r\n\t\tprivate String desc;\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue4214Test.java",
    "content": "package cn.hutool.json;\r\n\r\nimport lombok.Data;\r\nimport org.junit.jupiter.api.Test;\r\n\r\nimport static org.junit.jupiter.api.Assertions.assertEquals;\r\n\r\npublic class Issue4214Test {\r\n\r\n\t@Test\r\n\tvoid toBeanTest(){\r\n\t\tLicenseInfo licenseInfo = new LicenseInfo();\r\n\t\tlicenseInfo.setAuthTypeEnum(AuthTypeEnum.OFFICIAL);\r\n\r\n\t\tString jsonStr = JSONUtil.toJsonStr(licenseInfo);\r\n\t\tassertEquals(\"{\\\"authTypeEnum\\\":\\\"OFFICIAL\\\"}\", jsonStr);\r\n\r\n\t\t// 这里反序列化会报错\r\n\t\tLicenseInfo bean = JSONUtil.toBean(jsonStr, LicenseInfo.class);\r\n\t\tassertEquals(AuthTypeEnum.OFFICIAL, bean.getAuthTypeEnum());\r\n\t}\r\n\r\n\t@Data\r\n\tstatic class LicenseInfo{\r\n\t\tprivate AuthTypeEnum authTypeEnum;\r\n\t}\r\n\r\n\tenum AuthTypeEnum{\r\n\t\tOFFICIAL,\r\n\t\tSELF_BUILD\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue488Test.java",
    "content": "package cn.hutool.json;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport lombok.Data;\n\npublic class Issue488Test {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tString jsonStr = ResourceUtil.readUtf8Str(\"issue488.json\");\n\n\t\tResultSuccess<List<EmailAddress>> result = JSONUtil.toBean(jsonStr,\n\t\t\t\tnew TypeReference<ResultSuccess<List<EmailAddress>>>() {}, false);\n\n\t\tassertEquals(\"https://graph.microsoft.com/beta/$metadata#Collection(microsoft.graph.emailAddress)\", result.getContext());\n\n\t\tList<EmailAddress> adds = result.getValue();\n\t\tassertEquals(\"会议室101\", adds.get(0).getName());\n\t\tassertEquals(\"MeetingRoom101@abc.com\", adds.get(0).getAddress());\n\t\tassertEquals(\"会议室102\", adds.get(1).getName());\n\t\tassertEquals(\"MeetingRoom102@abc.com\", adds.get(1).getAddress());\n\t\tassertEquals(\"会议室103\", adds.get(2).getName());\n\t\tassertEquals(\"MeetingRoom103@abc.com\", adds.get(2).getAddress());\n\t\tassertEquals(\"会议室219\", adds.get(3).getName());\n\t\tassertEquals(\"MeetingRoom219@abc.com\", adds.get(3).getAddress());\n\t}\n\n\t@Test\n\tpublic void toCollctionBeanTest() {\n\t\tString jsonStr = ResourceUtil.readUtf8Str(\"issue488Array.json\");\n\n\t\tList<ResultSuccess<List<EmailAddress>>> resultList = JSONUtil.toBean(jsonStr,\n\t\t\t\tnew TypeReference<List<ResultSuccess<List<EmailAddress>>>>() {}, false);\n\n\t\tResultSuccess<List<EmailAddress>> result = resultList.get(0);\n\n\t\tassertEquals(\"https://graph.microsoft.com/beta/$metadata#Collection(microsoft.graph.emailAddress)\", result.getContext());\n\n\t\tList<EmailAddress> adds = result.getValue();\n\t\tassertEquals(\"会议室101\", adds.get(0).getName());\n\t\tassertEquals(\"MeetingRoom101@abc.com\", adds.get(0).getAddress());\n\t\tassertEquals(\"会议室102\", adds.get(1).getName());\n\t\tassertEquals(\"MeetingRoom102@abc.com\", adds.get(1).getAddress());\n\t\tassertEquals(\"会议室103\", adds.get(2).getName());\n\t\tassertEquals(\"MeetingRoom103@abc.com\", adds.get(2).getAddress());\n\t\tassertEquals(\"会议室219\", adds.get(3).getName());\n\t\tassertEquals(\"MeetingRoom219@abc.com\", adds.get(3).getAddress());\n\t}\n\n\t@Data\n\tpublic static class ResultSuccess<T> {\n\t\tprivate String context;\n\t\tprivate T value;\n\t}\n\n\t@Data\n\tpublic static class EmailAddress {\n\t\tprivate String name;\n\t\tprivate String address;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue644Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.date.LocalDateTimeUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\n\n/**\n * 问题反馈对象中有JDK8日期对象时转换失败，5.0.7修复\n */\npublic class Issue644Test {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tfinal BeanWithDate beanWithDate = new BeanWithDate();\n\t\tbeanWithDate.setDate(LocalDateTime.now());\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(beanWithDate);\n\n\t\tBeanWithDate beanWithDate2 = JSONUtil.toBean(jsonObject, BeanWithDate.class);\n\t\tassertEquals(LocalDateTimeUtil.formatNormal(beanWithDate.getDate()),\n\t\t\t\tLocalDateTimeUtil.formatNormal(beanWithDate2.getDate()));\n\n\t\tbeanWithDate2 = JSONUtil.toBean(jsonObject.toString(), BeanWithDate.class);\n\t\tassertEquals(LocalDateTimeUtil.formatNormal(beanWithDate.getDate()),\n\t\t\t\tLocalDateTimeUtil.formatNormal(beanWithDate2.getDate()));\n\t}\n\n\t@Data\n\tstatic class BeanWithDate{\n\t\tprivate LocalDateTime date;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue677Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.date.DateUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\n/**\n * 用于测试1970年前的日期（负数）还有int类型的数字转日期可能导致的转换失败问题。\n */\npublic class Issue677Test {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tfinal AuditResultDto dto = new AuditResultDto();\n\t\tdto.setDate(DateUtil.date(-1497600000));\n\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(dto);\n\t\tfinal AuditResultDto auditResultDto = JSONUtil.toBean(jsonStr, AuditResultDto.class);\n\t\tassertEquals(\"Mon Dec 15 00:00:00 CST 1969\", auditResultDto.getDate().toString().replace(\"GMT+08:00\", \"CST\"));\n\t}\n\n\t@Data\n\tpublic static class AuditResultDto{\n\t\tprivate Date date;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issue867Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.annotation.Alias;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue867Test {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tString json = \"{\\\"abc_1d\\\":\\\"123\\\",\\\"abc_d\\\":\\\"456\\\",\\\"abc_de\\\":\\\"789\\\"}\";\n\t\tTest02 bean = JSONUtil.toBean(JSONUtil.parseObj(json),Test02.class);\n\t\tassertEquals(\"123\", bean.getAbc1d());\n\t\tassertEquals(\"456\", bean.getAbcD());\n\t\tassertEquals(\"789\", bean.getAbcDe());\n\t}\n\n\t@Data\n\tstatic class Test02 {\n\t\t@Alias(\"abc_1d\")\n\t\tprivate String abc1d;\n\t\tprivate String abcD;\n\t\tprivate String abcDe;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI1AU86Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.collection.CollUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 问题反馈解析为null<br>\n * 此问题出在用户定义了String类型的List，parseArray时，Hutool无法知道这个String是普通值还是JSON字符串<br>\n * 就算知道也不能盲目解析，否则可能违背用户的初衷。<br>\n * 因此遇到List中的元素是String的情况下，用户需手动单独parse。\n */\npublic class IssueI1AU86Test {\n\n\t@Test\n\tpublic void toListTest() {\n\t\tList<String> redisList = CollUtil.newArrayList(\n\t\t\t\t\"{\\\"updateDate\\\":1583376342000,\\\"code\\\":\\\"move\\\",\\\"id\\\":1,\\\"sort\\\":1,\\\"name\\\":\\\"电影大全\\\"}\",\n\t\t\t\t\"{\\\"updateDate\\\":1583378882000,\\\"code\\\":\\\"zy\\\",\\\"id\\\":3,\\\"sort\\\":5,\\\"name\\\":\\\"综艺会\\\"}\"\n\t\t);\n\n\t\t// 手动parse\n\t\tfinal JSONArray jsonArray = new JSONArray();\n\t\tfor (String str : redisList) {\n\t\t\tjsonArray.add(JSONUtil.parse(str));\n\t\t}\n\n\t\tfinal List<Vcc> vccs = jsonArray.toList(Vcc.class);\n\t\tfor (Vcc vcc : vccs) {\n\t\t\tassertNotNull(vcc);\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class Vcc implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\t\tprivate Long id;\n\t\tprivate Date updateDate;\n\t\tprivate String code;\n\t\tprivate String name;\n\t\tprivate Integer sort;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI1F8M2.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\n\n/**\n * https://gitee.com/loolly/dashboard/issues?id=I1F8M2\n */\npublic class IssueI1F8M2 {\n\t@Test\n\tpublic void toBeanTest() {\n\t\tString jsonStr = \"{\\\"eventType\\\":\\\"fee\\\",\\\"fwdAlertingTime\\\":\\\"2020-04-22 16:34:13\\\",\\\"fwdAnswerTime\\\":\\\"\\\"}\";\n\t\tParam param = JSONUtil.toBean(jsonStr, Param.class);\n\t\tassertEquals(\"2020-04-22T16:34:13\", param.getFwdAlertingTime().toString());\n\t\tassertNull(param.getFwdAnswerTime());\n\t}\n\n\t// Param类的字段\n\t@Data\n\tstatic class Param {\n\t\t/**\n\t\t * fee表示话单事件\n\t\t */\n\t\tprivate String eventType;\n\t\t/**\n\t\t * 转接呼叫后振铃时间\n\t\t */\n\t\tprivate LocalDateTime fwdAlertingTime;\n\t\t/**\n\t\t * 转接呼叫后应答时间\n\t\t */\n\t\tprivate LocalDateTime fwdAnswerTime;\n\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI1H2VN.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * 测试同一对象作为对象的字段是否会有null的问题，\n * 此问题原来出在BeanCopier中，判断循环引用使用了equals，并不严谨。\n * 修复后使用==判断循环引用。\n */\npublic class IssueI1H2VN {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tString jsonStr = \"{'conditionsVo':[{'column':'StockNo','value':'abc','type':'='},{'column':'CheckIncoming','value':'1','type':'='}],\" +\n\t\t\t\t\"'queryVo':{'conditionsVo':[{'column':'StockNo','value':'abc','type':'='},{'column':'CheckIncoming','value':'1','type':'='}],'queryVo':null}}\";\n\t\tQueryVo vo = JSONUtil.toBean(jsonStr, QueryVo.class);\n\t\tassertEquals(2, vo.getConditionsVo().size());\n\t\tfinal QueryVo subVo = vo.getQueryVo();\n\t\tassertNotNull(subVo);\n\t\tassertEquals(2, subVo.getConditionsVo().size());\n\t\tassertNull(subVo.getQueryVo());\n\t}\n\n\t@Data\n\tpublic static class ConditionVo {\n\t\tprivate String column;\n\t\tprivate String value;\n\t\tprivate String type;\n\t}\n\n\t@Data\n\tpublic static class QueryVo {\n\t\tprivate List<ConditionVo> conditionsVo;\n\t\tprivate QueryVo queryVo;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI3BS4S.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\n\n/**\n * 测试带毫秒的日期转换\n */\npublic class IssueI3BS4S {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tString jsonStr = \"{date: '2021-03-17T06:31:33.99'}\";\n\t\tfinal Bean1 bean1 = new Bean1();\n\t\tBeanUtil.copyProperties(JSONUtil.parseObj(jsonStr), bean1);\n\t\tassertEquals(\"2021-03-17T06:31:33.099\", bean1.getDate().toString());\n\t}\n\n\t@Data\n\tpublic static class Bean1{\n\t\tprivate LocalDateTime date;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI3EGJP.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI3EGJP {\n\n\t@Test\n\tpublic void hutoolMapToBean() {\n\t\tJSONObject paramJson = new JSONObject();\n\t\tparamJson.set(\"is_booleana\", \"1\");\n\t\tparamJson.set(\"is_booleanb\", true);\n\t\tConvertDO convertDO = BeanUtil.toBean(paramJson, ConvertDO.class);\n\n\t\tassertTrue(convertDO.isBooleana());\n\t\tassertTrue(convertDO.getIsBooleanb());\n\t}\n\n\t@Data\n\tpublic static class ConvertDO {\n\t\tprivate boolean isBooleana;\n\t\tprivate Boolean isBooleanb;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI49VZBTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.convert.Convert;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I49VZB\n */\npublic class IssueI49VZBTest {\n\tpublic enum NBCloudKeyType {\n\t\t/**\n\t\t * 指纹\n\t\t */\n\t\tfingerPrint,\n\t\t/**\n\t\t * 密码\n\t\t */\n\t\tpassword,\n\t\t/**\n\t\t * 卡片\n\t\t */\n\t\tcard,\n\t\t/**\n\t\t * 临时密码\n\t\t */\n\t\tsnapKey;\n\n\t\tpublic static NBCloudKeyType find(String value) {\n\t\t\treturn Stream.of(values()).filter(e -> e.getValue().equalsIgnoreCase(value)).findFirst()\n\t\t\t\t\t.orElse(null);\n\t\t}\n\n\n\t\tpublic static NBCloudKeyType downFind(String keyType) {\n\t\t\tif (fingerPrint.name().equals(keyType.toLowerCase())) {\n\t\t\t\treturn NBCloudKeyType.fingerPrint;\n\t\t\t} else {\n\t\t\t\treturn find(keyType);\n\t\t\t}\n\t\t}\n\n\t\tpublic String getValue() {\n\t\t\treturn super.toString().toLowerCase();\n\t\t}\n\n\t}\n\n\t@Data\n\t@EqualsAndHashCode(callSuper = false)\n\tpublic static class UPOpendoor  {\n\n\t\tprivate String keyId;\n\t\tprivate NBCloudKeyType type;\n\t\tprivate String time;\n\t\tprivate int result;\n\n\t}\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tString str = \"{type: \\\"password\\\"}\";\n\t\tfinal UPOpendoor upOpendoor = JSONUtil.toBean(str, UPOpendoor.class);\n\t\tassertEquals(NBCloudKeyType.password, upOpendoor.getType());\n\t}\n\n\t@Test\n\tpublic void enumConvertTest(){\n\t\tfinal NBCloudKeyType type = Convert.toEnum(NBCloudKeyType.class, \"snapKey\");\n\t\tassertEquals(NBCloudKeyType.snapKey, type);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI4RBZ4Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I4RBZ4\n */\npublic class IssueI4RBZ4Test {\n\n\t@Test\n\tpublic void sortTest(){\n\t\tString jsonStr = \"{\\\"id\\\":\\\"123\\\",\\\"array\\\":[1,2,3],\\\"outNum\\\":356,\\\"body\\\":{\\\"ava1\\\":\\\"20220108\\\",\\\"use\\\":1,\\\"ava2\\\":\\\"20230108\\\"},\\\"name\\\":\\\"John\\\"}\";\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(jsonStr, JSONConfig.create().setNatureKeyComparator());\n\t\tassertEquals(\"{\\\"array\\\":[1,2,3],\\\"body\\\":{\\\"ava1\\\":\\\"20220108\\\",\\\"ava2\\\":\\\"20230108\\\",\\\"use\\\":1},\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"John\\\",\\\"outNum\\\":356}\", jsonObject.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI4XFMWTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.annotation.Alias;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I4XFMW\n */\npublic class IssueI4XFMWTest {\n\n\t@Test\n\tpublic void test() {\n\t\tList<TestEntity> entityList = new ArrayList<>();\n\t\tTestEntity entityA = new TestEntity();\n\t\tentityA.setId(\"123\");\n\t\tentityA.setPassword(\"456\");\n\t\tentityList.add(entityA);\n\n\t\tTestEntity entityB = new TestEntity();\n\t\tentityB.setId(\"789\");\n\t\tentityB.setPassword(\"098\");\n\t\tentityList.add(entityB);\n\n\t\tString jsonStr = JSONUtil.toJsonStr(entityList);\n\t\tassertEquals(\"[{\\\"uid\\\":\\\"123\\\",\\\"password\\\":\\\"456\\\"},{\\\"uid\\\":\\\"789\\\",\\\"password\\\":\\\"098\\\"}]\", jsonStr);\n\t\tList<TestEntity> testEntities = JSONUtil.toList(jsonStr, TestEntity.class);\n\t\tassertEquals(\"123\", testEntities.get(0).getId());\n\t\tassertEquals(\"789\", testEntities.get(1).getId());\n\t}\n\n\t@Data\n\tstatic class TestEntity {\n\t\t@Alias(\"uid\")\n\t\tprivate String id;\n\t\tprivate String password;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI50EGGTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI50EGGTest {\n\n\t@Test\n\tpublic void toBeanTest(){\n\t\tString data = \"{\\\"return_code\\\": 1, \\\"return_msg\\\": \\\"成功\\\", \\\"return_data\\\" : null}\";\n\t\tfinal ApiResult<?> apiResult = JSONUtil.toBean(data, JSONConfig.create().setIgnoreCase(true), ApiResult.class);\n\t\tassertEquals(1, apiResult.getReturn_code());\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class ApiResult<T>{\n\t\tprivate long Return_code;\n\t\tprivate String Return_msg;\n\t\tprivate T Return_data;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI59LW4Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI59LW4Test {\n\n\t@Test\n\tpublic void bytesTest(){\n\t\tfinal JSONObject jsonObject = JSONUtil.createObj().set(\"bytes\", new byte[]{1});\n\t\tassertEquals(\"{\\\"bytes\\\":[1]}\", jsonObject.toString());\n\n\t\tfinal byte[] bytes = jsonObject.getBytes(\"bytes\");\n\t\tassertArrayEquals(new byte[]{1}, bytes);\n\t}\n\n\t@Test\n\tpublic void bytesInJSONArrayTest(){\n\t\tfinal JSONArray jsonArray = JSONUtil.createArray().set(new byte[]{1});\n\t\tassertEquals(\"[[1]]\", jsonArray.toString());\n\n\t\tfinal byte[] bytes = jsonArray.getBytes(0);\n\t\tassertArrayEquals(new byte[]{1}, bytes);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI5OMSCTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Predicate多层过滤\n */\npublic class IssueI5OMSCTest {\n\n\t@Test\n\tpublic void filterTest(){\n\t\tfinal JSONObject json = JSONUtil.parseObj(ResourceUtil.readUtf8Str(\"issueI5OMSC.json\"));\n\n\t\tfinal String s = json.toJSONString(0, (entry) -> {\n\t\t\tfinal Object key = entry.getKey();\n\t\t\tif(key instanceof String){\n\t\t\t\treturn ListUtil.of(\"store\", \"bicycle\", \"color\", \"book\", \"author\").contains(key);\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tassertEquals(\"{\\\"store\\\":{\\\"bicycle\\\":{\\\"color\\\":\\\"red\\\"},\\\"book\\\":[{\\\"author\\\":\\\"Evelyn Waugh\\\"},{\\\"author\\\":\\\"Evelyn Waugh02\\\"}]}}\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI676IT.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.util.XmlUtil;\nimport cn.hutool.json.xml.JSONXMLSerializer;\nimport org.junit.jupiter.api.Test;\n\nimport static javax.xml.xpath.XPathConstants.STRING;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueI676IT {\n\n\t@Test\n\tpublic void parseXMLTest() {\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str(\"IssueI676IT.json\"));\n\t\tString xmlStr = JSONXMLSerializer.toXml(jsonObject, null, (String) null);\n\t\tString content = String.valueOf(XmlUtil.getByXPath(\"/page/orderItems[1]/content\", XmlUtil.readXML(xmlStr), STRING));\n\t\tassertEquals(\"bar1\", content);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI6H0XFTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI6H0XFTest {\n\t@Test\n\tpublic void toBeanTest(){\n\t\tfinal Demo demo = JSONUtil.toBean(\"{\\\"biz\\\":\\\"A\\\",\\\"isBiz\\\":true}\", Demo.class);\n\t\tassertEquals(\"A\", demo.getBiz());\n\t\tassertEquals(\"{\\\"biz\\\":\\\"A\\\"}\", JSONUtil.toJsonStr(demo));\n\t}\n\n\t@Data\n\tstatic class Demo {\n\t\tString biz;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI6SZYBTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.Pair;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.AbstractMap;\nimport java.util.Map;\n\npublic class IssueI6SZYBTest {\n\t@Test\n\tpublic void pairTest() {\n\t\tPair<Integer,Integer> pair = Pair.of(1, 2);\n\t\tString jsonStr = JSONUtil.toJsonStr(pair);\n\t\tassertEquals(\"{\\\"key\\\":1,\\\"value\\\":2}\", jsonStr);\n\n\t\tfinal Pair bean = JSONUtil.toBean(jsonStr, Pair.class);\n\t\tassertEquals(pair, bean);\n\t}\n\n\t@Test\n\tpublic void entryTest() {\n\t\tMap.Entry<String,Integer> pair = new AbstractMap.SimpleEntry<>(\"1\", 2);\n\t\tString jsonStr = JSONUtil.toJsonStr(pair);\n\t\tassertEquals(\"{\\\"1\\\":2}\", jsonStr);\n\n\t\tfinal Map.Entry bean = JSONUtil.toBean(jsonStr, AbstractMap.SimpleEntry.class);\n\t\tassertEquals(pair, bean);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI6TPIFTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\n\npublic class IssueI6TPIFTest {\n\n\t@Test\n\t@Disabled\n\tpublic void toStringTest() {\n\t\tTestVo test = new TestVo();\n\t\ttest.setBigValue(new BigDecimal(\"1234567899876543210.000000000000000000000000001\"));\n\t\ttest.setSmallValue(new BigDecimal(\"0.00000000000000000005\"));\n\t\tSystem.out.println(\"Bean To JSON\");\n\t\tSystem.out.println(JSONUtil.toJsonStr(test));\n\t\tSystem.out.println(\"\\n\");\n\n\t\tJSONObject jsonObject = new JSONObject();\n\t\tjsonObject.put(\"bigValue\", \"1234567899876543210.000000000000000000000000001\");\n\t\tjsonObject.put(\"smallValue\", \"0.00000000000000000005\");\n\t\tSystem.out.println(\"JSON TO Bean\");\n\t\tSystem.out.println(JSONUtil.toBean(jsonObject, TestVo.class));\n\t}\n\n\t@Data\n\tstatic class TestVo {\n\n\t\tprivate BigDecimal bigValue;\n\t\tprivate BigDecimal smallValue;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI6YN2ATest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class IssueI6YN2ATest {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal String str = \"{\\\"conditions\\\":{\\\"user\\\":\\\"test\\\",\\\"sex\\\":\\\"男\\\"},\\\"headers\\\":{\\\"name\\\":\\\"姓名\\\",\\\"age\\\":\\\"年龄\\\",\\\"email\\\":\\\"邮箱\\\",\\\"number\\\":\\\"号码\\\",\\\"pwd\\\":\\\"密码\\\"}}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(str);\n\n\t\tfinal PageQuery<?> bean = jsonObject.toBean(PageQuery.class);\n\t\tassertEquals(\"{name=姓名, age=年龄, email=邮箱, number=号码, pwd=密码}\", bean.headers.toString());\n\t}\n\n\t@Data\n\tpublic static class PageQuery<T> {\n\t\t/**\n\t\t * 要导出的字段\n\t\t */\n\t\tprivate Map<String, String> headers = new LinkedHashMap<>();\n\t\tprivate T conditions;\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn JSONUtil.parse(this, JSONConfig.create()).toJSONString(0);\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class User {\n\t\tprivate String name;\n\t\tprivate String sex;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI71BE6Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI71BE6Test {\n\n\t@Test\n\tpublic void toArrayTest() {\n\n\t\tfinal String jsonStr = \"[50.0,50.0,50.0,50.0]\";\n\n\t\tfinal Double[] doubles = (Double[]) JSONUtil.parseArray(jsonStr).toArray(Double[].class);\n\t\tassertArrayEquals(new Double[]{50.0,50.0,50.0,50.0}, doubles);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI7FQ29Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport cn.hutool.core.lang.TypeReference;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class IssueI7FQ29Test {\n\t@Test\n\tpublic void toMapTest() {\n\t\tfinal String jsonStr = \"{\\\"trans_no\\\": \\\"java.lang.String\\\"}\";\n\t\tfinal Map<String, Class<?>> map = JSONUtil.toBean(jsonStr, new TypeReference<Map<String, Class<?>>>() {\n\t\t}, false);\n\n\t\tassertNotNull(map);\n\t\tassertEquals(String.class, map.get(\"trans_no\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI7GPGXTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.json;\n\nimport cn.hutool.core.lang.Pair;\nimport cn.hutool.core.lang.TypeReference;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI7GPGXTest {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal Pair<String, Boolean> hutoolPair = new Pair<>(\"test1\", true);\n\t\tfinal String a = JSONUtil.toJsonStr(hutoolPair);\n\t\tfinal Pair<String, Boolean> pair = JSONUtil.toBean(a, new TypeReference<Pair<String, Boolean>>() {}, false);\n\t\tassertEquals(hutoolPair, pair);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI7M2GZTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.TypeReference;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I7M2GZ\n */\npublic class IssueI7M2GZTest {\n\n\t@Data\n\t@AllArgsConstructor\n\tpublic static class JSONBeanParserImpl implements JSONBeanParser<Object> {\n\t\tprivate String name;\n\t\tprivate Integer parsed;\n\n\t\t@Override\n\t\tpublic void parse(final Object object) {\n\t\t\tsetName(\"new Object\");\n\t\t\tsetParsed(12);\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class MyEntity<T> {\n\t\tprivate List<T> list;\n\t}\n\n\t@Test\n\tpublic void toListTest() {\n\t\tfinal List<JSONBeanParserImpl> list = new ArrayList<>();\n\t\tlist.add(new JSONBeanParserImpl(\"Object1\", 1));\n\n\t\tfinal MyEntity<JSONBeanParserImpl> entity = new MyEntity<>();\n\t\tentity.setList(list);\n\n\t\tfinal String json = JSONUtil.toJsonStr(entity);\n\t\t//Console.log(json);\n\t\tfinal MyEntity<JSONBeanParserImpl> result = JSONUtil.toBean(json, new TypeReference<MyEntity<JSONBeanParserImpl>>() {\n\t\t}, false);\n\t\tassertEquals(\"new Object\", result.getList().get(0).getName());\n\t\tassertNotNull(result.getList().get(0).getParsed());\n\t\tassertEquals(Integer.valueOf(12), result.getList().get(0).getParsed());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI82AM8Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.LocalDateTime;\nimport java.util.List;\nimport java.util.Map;\n\npublic class IssueI82AM8Test {\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal String json = ResourceUtil.readUtf8Str(\"issueI82AM8.json\");\n\n\t\tMap<String, MedicalCenter.MedicalCenterLocalized> bean1 =\n\t\t\tJSONUtil.toBean(json, new TypeReference<Map<String, MedicalCenter.MedicalCenterLocalized>>() {\n\t\t\t}, false);\n\n\t\tbean1.forEach((k, v) -> assertNotNull(v.getTestimonials()));\n\t}\n\n\t// 对象\n\t@Data\n\tpublic static class MedicalCenter {\n\n\t\tprivate Map<String, MedicalCenterLocalized> medicalCenterLocalized;\n\n\t\t@Data\n\t\tpublic static class MedicalCenterLocalized {\n\n\t\t\tprivate List<Testimonial> testimonials;\n\n\t\t\t@Data\n\t\t\tpublic static class Testimonial {\n\t\t\t\tprivate LocalDateTime createTime;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI84V6ITest.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI84V6ITest {\n\t@Test\n\tpublic void formatTest() {\n\t\tfinal String a1 = \"{'x':'\\\\n','y':','}\";\n\t\tfinal String formatJsonStr = JSONUtil.formatJsonStr(a1);\n//\t\tConsole.log(formatJsonStr);\n\t\tassertEquals(\n\t\t\t\"{\\n\" +\n\t\t\t\t\"    'x': '\\\\n',\\n\" +\n\t\t\t\t\"    'y': ','\\n\" +\n\t\t\t\t\"}\", formatJsonStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI8NMP7Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport lombok.ToString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\npublic class IssueI8NMP7Test {\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal String jsonString = \"{\\\"enableTime\\\":\\\"1702262524444\\\"}\";\n\t\tfinal DemoModel bean = JSONUtil.toBean(jsonString, JSONConfig.create(), DemoModel.class);\n\t\tassertNotNull(bean.getEnableTime());\n\t}\n\n\t@Data\n\t@ToString\n\tstatic class DemoModel{\n\t\tprivate Date enableTime;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI8PC9FTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class IssueI8PC9FTest {\n\n\t@Test\n\tpublic void toBeanIgnoreErrorTest() {\n\t\tfinal String testJson = \"{\\\"testMap\\\":\\\"\\\"}\";\n\t\tfinal TestBean test = JSONUtil.parseObj(testJson, JSONConfig.create().setIgnoreError(true))\n\t\t\t.toBean(TestBean.class);\n\t\tassertNotNull(test);\n\t\tassertNull(test.getTestMap());\n\t}\n\n\t@Data\n\tstatic class TestBean{\n\t\tprivate Map<?, ?> testMap;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueI90ADXTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Getter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI90ADXTest {\n\n\t/**\n\t * 测试只有getter方法时是否可以解析\n\t */\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal TestBean testBean = new TestBean();\n\t\ttestBean.name = \"aaaa\";\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(testBean);\n\t\tassertEquals(\"{\\\"name\\\":\\\"aaaa\\\"}\", json.toString());\n\t}\n\n\t@Getter\n\tstatic class TestBean{\n\t\tprivate String name;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueIA5YOETest.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIA5YOETest {\n\t@Test\n\tpublic void parseObjTest() {\n\t\tString res =\"{\\\"id\\\":\\\"chatcmpl-9azySxFBm5hjtaMCccREpCHF3gbQ3\\\",\\\"object\\\":\\\"chat.completion.chunk\\\",\\\"created\\\":1718605064,\\\"model\\\":\\\"gpt-4o-2024-05-13\\\",\\\"system_fingerprint\\\":\\\"fp_319be4768e\\\",\\\"choices\\\":[{\\\"index\\\":0,\\\"delta\\\":{\\\"content\\\":\\\"\\\\\\\"\\\"},\\\"logprobs\\\":null,\\\"finish_reason\\\":null}]}\";\n\t\tassertNotNull(JSONUtil.parseObj(res));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueIALQ0NTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.date.DateUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class IssueIALQ0NTest {\n\t@Test\n\tvoid toJsonStrTest() {\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"id\", 1826166955313201152L);\n\t\tmap.put(\"createdDate\", DateUtil.parse(\"2024-08-24\"));\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(map, JSONConfig.create().setDateFormat(\"yyyy-MM-dd HH:mm:ss\").setWriteLongAsString(true));\n\t\tAssertions.assertEquals(\"{\\\"createdDate\\\":\\\"2024-08-24 00:00:00\\\",\\\"id\\\":\\\"1826166955313201152\\\"}\", jsonStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueIAOPI9Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIAOPI9Test {\n\t@Test\n\tvoid toBeanTest() {\n\t\tString jsonStr = \"{\\\"chatCommandType\\\":\\\"CHAT_MSG\\\"}\";\n\t\tChatPublicAppReceiveMq mqChatRequest = JSONUtil.toBean(jsonStr, ChatPublicAppReceiveMq.class);\n\t\tAssertions.assertEquals(\"CHAT_MSG\", mqChatRequest.getChatCommandType().name());\n\t}\n\n\t@Data\n\tstatic class ChatPublicAppReceiveMq {\n\t\tprivate ChatAppMqCommandTypeEnum chatCommandType;\n\t}\n\n\tpublic enum ChatAppMqCommandTypeEnum {\n\t\t/**\n\t\t * 对话消息\n\t\t */\n\t\tCHAT_MSG(\"chat_msg\"),\n\n\t\t/**\n\t\t * 对话消息(批量-群发)\n\t\t */\n\t\tCHAT_MSG_BATCH(\"chat_msg_batch\"),\n\t\t/**\n\t\t * 命令消息\n\t\t */\n\t\tCOMMAND_MSG(\"command_msg\")\n\t\t;\n\t\tChatAppMqCommandTypeEnum(String type) {\n\t\t\tthis.type = type;\n\t\t}\n\n\t\tprivate String type;\n\t\tpublic String getType() {\n\t\t\treturn type;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueIAP4GMTest.java",
    "content": "package cn.hutool.json;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIAP4GMTest {\n\t@Test\n\tvoid parse() {\n\t\tString res = \"{\\\"uid\\\":\\\"asdf\\\\n\\\"}\";\n\t\tfinal JSONObject entries = JSONUtil.parseObj(res);\n\t\tAssertions.assertEquals(\"asdf\\n\", entries.getStr(\"uid\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueIB9MH0Test.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.json.serialize.GlobalSerializeMapping;\nimport cn.hutool.json.serialize.JSONObjectSerializer;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIB9MH0Test {\n\n\t@Test\n\tvoid parseTest() {\n\t\tGlobalSerializeMapping.put(TabTypeEnum.class, (JSONObjectSerializer<TabTypeEnum>) (json, bean) -> json.set(\"code\", bean.getCode())\n\t\t\t.set(\"title\", bean.getTitle()));\n\t\tfinal JSON parse = JSONUtil.parse(TabTypeEnum._01);\n\t\tAssertions.assertEquals(\"{\\\"code\\\":\\\"tab_people_home\\\",\\\"title\\\":\\\"首页\\\"}\", parse.toString());\n\t}\n\n\tpublic enum TabTypeEnum  {\n\t\t_01(\"tab_people_home\",\"首页\"),\n\t\t_02(\"tab_people_hospital\",\"医院\");\n\n\t\tprivate String code;\n\t\tprivate String title;\n\n\t\tTabTypeEnum(String code, String title) {\n\t\t\tthis.code = code;\n\t\t\tthis.title = title;\n\t\t}\n\n\t\tpublic String getCode() {\n\t\t\treturn code;\n\t\t}\n\n\t\tpublic String getTitle() {\n\t\t\treturn title;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueID418BTest.java",
    "content": "package cn.hutool.json;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueID418BTest {\n\n\t@Test\n\tvoid booleanToJsonTest() {\n\t\tBoolean dd = true;\n\t\tString jsonStr = JSONUtil.toJsonStr(dd);\n\t\tAssertions.assertEquals(\"true\", jsonStr);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssueID61QRTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.map.MapUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class IssueID61QRTest {\n\t@Test\n\tpublic void testName() {\n\t\tJSONObject map1 = JSONUtil.createObj(new JSONConfig().setDateFormat(\"yyyy\"));\n\t\t// JSONObject map1 = JSONUtil.createObj();\n\t\tmap1.set(\"a\", 3);\n\t\tmap1.set(\"b\", 5);\n\t\tmap1.set(\"c\", 5432);\n\t\tAssertions.assertEquals(\"{c=5432, b=5, a=3}\", MapUtil.sortByValue(JSONUtil.toBean(map1, Map.class), true).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Issues1881Test.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Issues1881Test {\n\n\t@Accessors(chain = true)\n\t@Data\n\tstatic class ThingsHolderContactVO implements Serializable {\n\n\t\tprivate static final long serialVersionUID = -8727337936070932370L;\n\t\tprivate Long id;\n\t\tprivate Integer type;\n\t\tprivate String memberId;\n\t\tprivate String name;\n\t\tprivate String phone;\n\t\tprivate String avatar;\n\t\tprivate Long createTime;\n\t}\n\n\t@Test\n\tpublic void parseTest(){\n\t\tList<ThingsHolderContactVO> holderContactVOList = new ArrayList<>();\n\t\tholderContactVOList.add(new ThingsHolderContactVO().setId(1L).setName(\"1\"));\n\t\tholderContactVOList.add(new ThingsHolderContactVO().setId(2L).setName(\"2\"));\n\n\t\tassertEquals(\"[{\\\"id\\\":1,\\\"name\\\":\\\"1\\\"},{\\\"id\\\":2,\\\"name\\\":\\\"2\\\"}]\", JSONUtil.parseArray(holderContactVOList).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssuesI44E4HTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.json.serialize.GlobalSerializeMapping;\nimport cn.hutool.json.serialize.JSONDeserializer;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.Setter;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 测试自定义反序列化\n */\npublic class IssuesI44E4HTest {\n\n\t@Test\n\tpublic void deserializerTest(){\n\t\tGlobalSerializeMapping.put(TestDto.class, (JSONDeserializer<TestDto>) json -> {\n\t\t\tfinal TestDto testDto = new TestDto();\n\t\t\ttestDto.setMd(new AcBizModuleMd(\"name1\", ((JSONObject)json).getStr(\"md\")));\n\t\t\treturn testDto;\n\t\t});\n\n\t\tString jsonStr = \"{\\\"md\\\":\\\"value1\\\"}\";\n\t\tfinal TestDto testDto = JSONUtil.toBean(jsonStr, TestDto.class);\n\t\tassertEquals(\"value1\", testDto.getMd().getValue());\n\t}\n\n\t@Getter\n\t@Setter\n\t@AllArgsConstructor\n\tpublic static class AcBizModuleMd {\n\t\tprivate String name;\n\t\tprivate String value;\n\t\t// 值列表\n\t\tpublic static final AcBizModuleMd Value1 = new AcBizModuleMd(\"value1\", \"name1\");\n\t\tpublic static final AcBizModuleMd Value2 = new AcBizModuleMd(\"value2\", \"name2\");\n\t\tpublic static final AcBizModuleMd Value3 = new AcBizModuleMd(\"value3\", \"name3\");\n\t}\n\n\t@Getter\n\t@Setter\n\tpublic static class TestDto {\n\t\tprivate AcBizModuleMd md;\n\t}\n}\n\n\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/IssuesI4V14NTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.TypeReference;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\npublic class IssuesI4V14NTest {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tString str = \"{\\\"A\\\" : \\\"A\\\\nb\\\"}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(str);\n\t\tassertEquals(\"A\\nb\", jsonObject.getStr(\"A\"));\n\n\t\tfinal Map<String, String> map = jsonObject.toBean(new TypeReference<Map<String, String>>() {});\n\t\tassertEquals(\"A\\nb\", map.get(\"A\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONArrayTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.ConvertException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.json.test.bean.Exam;\nimport cn.hutool.json.test.bean.JsonNode;\nimport cn.hutool.json.test.bean.KeyBean;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * JSONArray单元测试\n *\n * @author Looly\n */\npublic class JSONArrayTest {\n\n\t@Test()\n\tpublic void createJSONArrayFromJSONObjectTest() {\n\t\t// JSONObject实现了Iterable接口，可以转换为JSONArray\n\t\tfinal JSONObject jsonObject = new JSONObject();\n\n\t\tJSONArray jsonArray = new JSONArray(jsonObject, JSONConfig.create());\n\t\tassertEquals(new JSONArray(), jsonArray);\n\n\t\tjsonObject.set(\"key1\", \"value1\");\n\t\tjsonArray = new JSONArray(jsonObject, JSONConfig.create());\n\t\tassertEquals(1, jsonArray.size());\n\t\tassertEquals(\"[{\\\"key1\\\":\\\"value1\\\"}]\", jsonArray.toString());\n\t}\n\n\t@Test\n\tpublic void addNullTest() {\n\t\tfinal List<String> aaa = ListUtil.of(\"aaa\", null);\n\t\tString jsonStr = JSONUtil.toJsonStr(JSONUtil.parse(aaa,\n\t\t\tJSONConfig.create().setIgnoreNullValue(false)));\n\t\tassertEquals(\"[\\\"aaa\\\",null]\", jsonStr);\n\t}\n\n\t@Test\n\tpublic void addTest() {\n\t\t// 方法1\n\t\tJSONArray array = JSONUtil.createArray();\n\t\t// 方法2\n\t\t// JSONArray array = new JSONArray();\n\t\tarray.add(\"value1\");\n\t\tarray.add(\"value2\");\n\t\tarray.add(\"value3\");\n\n\t\tassertEquals(array.get(0), \"value1\");\n\t}\n\n\t@Test\n\tpublic void parseTest() {\n\t\tString jsonStr = \"[\\\"value1\\\", \\\"value2\\\", \\\"value3\\\"]\";\n\t\tJSONArray array = JSONUtil.parseArray(jsonStr);\n\t\tassertEquals(array.get(0), \"value1\");\n\t}\n\n\t@Test\n\tpublic void parseWithNullTest() {\n\t\tString jsonStr = \"[{\\\"grep\\\":\\\"4.8\\\",\\\"result\\\":\\\"右\\\"},{\\\"grep\\\":\\\"4.8\\\",\\\"result\\\":null}]\";\n\t\tJSONArray jsonArray = JSONUtil.parseArray(jsonStr);\n\t\tassertFalse(jsonArray.getJSONObject(1).containsKey(\"result\"));\n\n\t\t// 不忽略null，则null的键值对被保留\n\t\tjsonArray = new JSONArray(jsonStr, false);\n\t\tassertTrue(jsonArray.getJSONObject(1).containsKey(\"result\"));\n\t}\n\n\t@Test\n\tpublic void parseFileTest() {\n\t\tJSONArray array = JSONUtil.readJSONArray(FileUtil.file(\"exam_test.json\"), CharsetUtil.CHARSET_UTF_8);\n\n\t\tJSONObject obj0 = array.getJSONObject(0);\n\t\tExam exam = JSONUtil.toBean(obj0, Exam.class);\n\t\tassertEquals(\"0\", exam.getAnswerArray()[0].getSeq());\n\t}\n\n\t@Test\n\tpublic void parseBeanListTest() {\n\t\tKeyBean b1 = new KeyBean();\n\t\tb1.setAkey(\"aValue1\");\n\t\tb1.setBkey(\"bValue1\");\n\t\tKeyBean b2 = new KeyBean();\n\t\tb2.setAkey(\"aValue2\");\n\t\tb2.setBkey(\"bValue2\");\n\n\t\tArrayList<KeyBean> list = CollUtil.newArrayList(b1, b2);\n\n\t\tJSONArray jsonArray = JSONUtil.parseArray(list);\n\t\tassertEquals(\"aValue1\", jsonArray.getJSONObject(0).getStr(\"akey\"));\n\t\tassertEquals(\"bValue2\", jsonArray.getJSONObject(1).getStr(\"bkey\"));\n\t}\n\n\t@Test\n\tpublic void toListTest() {\n\t\tString jsonStr = FileUtil.readString(\"exam_test.json\", CharsetUtil.CHARSET_UTF_8);\n\t\tJSONArray array = JSONUtil.parseArray(jsonStr);\n\n\t\tList<Exam> list = array.toList(Exam.class);\n\t\tassertFalse(list.isEmpty());\n\t\tassertSame(Exam.class, list.get(0).getClass());\n\t}\n\n\t@Test\n\tpublic void toListTest2() {\n\t\tString jsonArr = \"[{\\\"id\\\":111,\\\"name\\\":\\\"test1\\\"},{\\\"id\\\":112,\\\"name\\\":\\\"test2\\\"}]\";\n\n\t\tJSONArray array = JSONUtil.parseArray(jsonArr);\n\t\tList<User> userList = JSONUtil.toList(array, User.class);\n\n\t\tassertFalse(userList.isEmpty());\n\t\tassertSame(User.class, userList.get(0).getClass());\n\n\t\tassertEquals(Integer.valueOf(111), userList.get(0).getId());\n\t\tassertEquals(Integer.valueOf(112), userList.get(1).getId());\n\n\t\tassertEquals(\"test1\", userList.get(0).getName());\n\t\tassertEquals(\"test2\", userList.get(1).getName());\n\t}\n\n\t@Test\n\tpublic void toDictListTest() {\n\t\tString jsonArr = \"[{\\\"id\\\":111,\\\"name\\\":\\\"test1\\\"},{\\\"id\\\":112,\\\"name\\\":\\\"test2\\\"}]\";\n\n\t\tJSONArray array = JSONUtil.parseArray(jsonArr, JSONConfig.create().setIgnoreError(false));\n\n\t\tList<Dict> list = JSONUtil.toList(array, Dict.class);\n\n\t\tassertFalse(list.isEmpty());\n\t\tassertSame(Dict.class, list.get(0).getClass());\n\n\t\tassertEquals(Integer.valueOf(111), list.get(0).getInt(\"id\"));\n\t\tassertEquals(Integer.valueOf(112), list.get(1).getInt(\"id\"));\n\n\t\tassertEquals(\"test1\", list.get(0).getStr(\"name\"));\n\t\tassertEquals(\"test2\", list.get(1).getStr(\"name\"));\n\t}\n\n\t@Test\n\tpublic void toArrayTest() {\n\t\tString jsonStr = FileUtil.readString(\"exam_test.json\", CharsetUtil.CHARSET_UTF_8);\n\t\tJSONArray array = JSONUtil.parseArray(jsonStr);\n\n\t\t//noinspection SuspiciousToArrayCall\n\t\tExam[] list = array.toArray(new Exam[0]);\n\t\tassertNotEquals(0, list.length);\n\t\tassertSame(Exam.class, list[0].getClass());\n\t}\n\n\t/**\n\t * 单元测试用于测试在列表元素中有null时的情况下是否出错\n\t */\n\t@Test\n\tpublic void toListWithNullTest() {\n\t\tString json = \"[null,{'akey':'avalue','bkey':'bvalue'}]\";\n\t\tJSONArray ja = JSONUtil.parseArray(json, JSONConfig.create().setIgnoreNullValue(false));\n\n\t\tList<KeyBean> list = ja.toList(KeyBean.class);\n\t\tassertNull(list.get(0));\n\t\tassertEquals(\"avalue\", list.get(1).getAkey());\n\t\tassertEquals(\"bvalue\", list.get(1).getBkey());\n\t}\n\n\t@Test\n\tpublic void toListWithErrorTest() {\n\t\tassertThrows(ConvertException.class, () -> {\n\t\t\tString json = \"[['aaa',{'akey':'avalue','bkey':'bvalue'}]]\";\n\t\t\tJSONArray ja = JSONUtil.parseArray(json);\n\t\t\tja.toBean(new TypeReference<List<List<KeyBean>>>() {\n\t\t\t});\n\t\t});\n\t}\n\n\t@Test\n\tpublic void toBeanListTest() {\n\t\tList<Map<String, String>> mapList = new ArrayList<>();\n\t\tmapList.add(buildMap(\"0\", \"0\", \"0\"));\n\t\tmapList.add(buildMap(\"1\", \"1\", \"1\"));\n\t\tmapList.add(buildMap(\"+0\", \"+0\", \"+0\"));\n\t\tmapList.add(buildMap(\"-0\", \"-0\", \"-0\"));\n\t\tJSONArray jsonArray = JSONUtil.parseArray(mapList);\n\t\tList<JsonNode> nodeList = jsonArray.toList(JsonNode.class);\n\n\t\tassertEquals(Long.valueOf(0L), nodeList.get(0).getId());\n\t\tassertEquals(Long.valueOf(1L), nodeList.get(1).getId());\n\t\tassertEquals(Long.valueOf(0L), nodeList.get(2).getId());\n\t\tassertEquals(Long.valueOf(0L), nodeList.get(3).getId());\n\n\t\tassertEquals(Integer.valueOf(0), nodeList.get(0).getParentId());\n\t\tassertEquals(Integer.valueOf(1), nodeList.get(1).getParentId());\n\t\tassertEquals(Integer.valueOf(0), nodeList.get(2).getParentId());\n\t\tassertEquals(Integer.valueOf(0), nodeList.get(3).getParentId());\n\n\t\tassertEquals(\"0\", nodeList.get(0).getName());\n\t\tassertEquals(\"1\", nodeList.get(1).getName());\n\t\tassertEquals(\"+0\", nodeList.get(2).getName());\n\t\tassertEquals(\"-0\", nodeList.get(3).getName());\n\t}\n\n\t@Test\n\tpublic void getByPathTest() {\n\t\tString jsonStr = \"[{\\\"id\\\": \\\"1\\\",\\\"name\\\": \\\"a\\\"},{\\\"id\\\": \\\"2\\\",\\\"name\\\": \\\"b\\\"}]\";\n\t\tfinal JSONArray jsonArray = JSONUtil.parseArray(jsonStr);\n\t\tassertEquals(\"b\", jsonArray.getByPath(\"[1].name\"));\n\t\tassertEquals(\"b\", JSONUtil.getByPath(jsonArray, \"[1].name\"));\n\t}\n\n\t@Test\n\tpublic void putToIndexTest() {\n\t\tJSONArray jsonArray = new JSONArray(JSONConfig.create().setIgnoreNullValue(false));\n\t\tjsonArray.put(3, \"test\");\n\t\t// 第三个位置插入值，0~2都是null\n\t\tassertEquals(4, jsonArray.size());\n\n\t\tjsonArray = new JSONArray(JSONConfig.create().setIgnoreNullValue(true));\n\t\tjsonArray.put(3, \"test\");\n\t\t// 第三个位置插入值，忽略null，则追加\n\t\tassertEquals(1, jsonArray.size());\n\t}\n\n\t// https://github.com/chinabugotech/hutool/issues/1858\n\t@Test\n\tpublic void putTest2() {\n\t\tfinal JSONArray jsonArray = new JSONArray();\n\t\tjsonArray.put(0, 1);\n\t\tassertEquals(1, jsonArray.size());\n\t\tassertEquals(1, jsonArray.get(0));\n\t}\n\n\tprivate static Map<String, String> buildMap(String id, String parentId, String name) {\n\t\tMap<String, String> map = new HashMap<>();\n\t\tmap.put(\"id\", id);\n\t\tmap.put(\"parentId\", parentId);\n\t\tmap.put(\"name\", name);\n\t\treturn map;\n\t}\n\n\t@Data\n\tstatic class User {\n\t\tprivate Integer id;\n\t\tprivate String name;\n\t}\n\n\t@Test\n\tpublic void filterIncludeTest() {\n\t\tJSONArray json1 = JSONUtil.createArray()\n\t\t\t.set(\"value1\")\n\t\t\t.set(\"value2\")\n\t\t\t.set(\"value3\")\n\t\t\t.set(true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> pair.getValue().equals(\"value2\"));\n\t\tassertEquals(\"[\\\"value2\\\"]\", s);\n\t}\n\n\t@Test\n\tpublic void filterExcludeTest() {\n\t\tJSONArray json1 = JSONUtil.createArray()\n\t\t\t.set(\"value1\")\n\t\t\t.set(\"value2\")\n\t\t\t.set(\"value3\")\n\t\t\t.set(true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> false == pair.getValue().equals(\"value2\"));\n\t\tassertEquals(\"[\\\"value1\\\",\\\"value3\\\",true]\", s);\n\t}\n\n\t@Test\n\tpublic void putNullTest() {\n\t\tfinal JSONArray array = JSONUtil.createArray(JSONConfig.create().setIgnoreNullValue(false));\n\t\tarray.set(null);\n\n\t\tassertEquals(\"[null]\", array.toString());\n\t}\n\n\t@Test\n\tpublic void parseFilterTest() {\n\t\tString jsonArr = \"[{\\\"id\\\":111,\\\"name\\\":\\\"test1\\\"},{\\\"id\\\":112,\\\"name\\\":\\\"test2\\\"}]\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONArray array = new JSONArray(jsonArr, null, (mutable) -> mutable.get().toString().contains(\"111\"));\n\t\tassertEquals(1, array.size());\n\t\tassertTrue(array.getJSONObject(0).containsKey(\"id\"));\n\t}\n\n\t@Test\n\tpublic void parseFilterEditTest() {\n\t\tString jsonArr = \"[{\\\"id\\\":111,\\\"name\\\":\\\"test1\\\"},{\\\"id\\\":112,\\\"name\\\":\\\"test2\\\"}]\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONArray array = new JSONArray(jsonArr, null, (mutable) -> {\n\t\t\tfinal JSONObject o = new JSONObject(mutable.get());\n\t\t\tif (\"111\".equals(o.getStr(\"id\"))) {\n\t\t\t\to.set(\"name\", \"test1_edit\");\n\t\t\t}\n\t\t\tmutable.set(o);\n\t\t\treturn true;\n\t\t});\n\t\tassertEquals(2, array.size());\n\t\tassertTrue(array.getJSONObject(0).containsKey(\"id\"));\n\t\tassertEquals(\"test1_edit\", array.getJSONObject(0).get(\"name\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONBeanParserTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class JSONBeanParserTest {\n\n\t@Test\n\tpublic void parseTest(){\n\t\tString jsonStr = \"{\\\"customName\\\": \\\"customValue\\\", \\\"customAddress\\\": \\\"customAddressValue\\\"}\";\n\t\tfinal TestBean testBean = JSONUtil.toBean(jsonStr, TestBean.class);\n\t\tassertNotNull(testBean);\n\t\tassertEquals(\"customValue\", testBean.getName());\n\t\tassertEquals(\"customAddressValue\", testBean.getAddress());\n\t}\n\n\t@Data\n\tstatic class TestBean implements JSONBeanParser<JSONObject>{\n\n\t\tprivate String name;\n\t\tprivate String address;\n\n\t\t@Override\n\t\tpublic void parse(JSONObject value) {\n\t\t\tthis.name = value.getStr(\"customName\");\n\t\t\tthis.address = value.getStr(\"customAddress\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONConvertTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.json.test.bean.ExamInfoDict;\nimport cn.hutool.json.test.bean.PerfectEvaluationProductResVo;\nimport cn.hutool.json.test.bean.UserInfoDict;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * JSON转换单元测试\n *\n * @author Looly，质量过关\n *\n */\npublic class JSONConvertTest {\n\n\t@Test\n\tpublic void testBean2Json() {\n\n\t\tUserInfoDict userInfoDict = new UserInfoDict();\n\t\tuserInfoDict.setId(1);\n\t\tuserInfoDict.setPhotoPath(\"yx.mm.com\");\n\t\tuserInfoDict.setRealName(\"质量过关\");\n\n\t\tExamInfoDict examInfoDict = new ExamInfoDict();\n\t\texamInfoDict.setId(1);\n\t\texamInfoDict.setExamType(0);\n\t\texamInfoDict.setAnswerIs(1);\n\n\t\tExamInfoDict examInfoDict1 = new ExamInfoDict();\n\t\texamInfoDict1.setId(2);\n\t\texamInfoDict1.setExamType(0);\n\t\texamInfoDict1.setAnswerIs(0);\n\n\t\tExamInfoDict examInfoDict2 = new ExamInfoDict();\n\t\texamInfoDict2.setId(3);\n\t\texamInfoDict2.setExamType(1);\n\t\texamInfoDict2.setAnswerIs(0);\n\n\t\tList<ExamInfoDict> examInfoDicts = new ArrayList<>();\n\t\texamInfoDicts.add(examInfoDict);\n\t\texamInfoDicts.add(examInfoDict1);\n\t\texamInfoDicts.add(examInfoDict2);\n\n\t\tuserInfoDict.setExamInfoDict(examInfoDicts);\n\n\t\tMap<String, Object> tempMap = new HashMap<>();\n\t\ttempMap.put(\"userInfoDict\", userInfoDict);\n\t\ttempMap.put(\"toSendManIdCard\", 1);\n\n\t\tJSONObject obj = JSONUtil.parseObj(tempMap);\n\t\tassertEquals(new Integer(1), obj.getInt(\"toSendManIdCard\"));\n\n\t\tJSONObject examInfoDictsJson = obj.getJSONObject(\"userInfoDict\");\n\t\tassertEquals(new Integer(1), examInfoDictsJson.getInt(\"id\"));\n\t\tassertEquals(\"质量过关\", examInfoDictsJson.getStr(\"realName\"));\n\n\t\tObject id = JSONUtil.getByPath(obj, \"userInfoDict.examInfoDict[0].id\");\n\t\tassertEquals(1, id);\n\t}\n\n\t@Test\n\tpublic void testJson2Bean() {\n\t\t// language=JSON\n\t\tString examJson = \"{\\n\" + \"  \\\"examInfoDicts\\\": {\\n\" + \"    \\\"id\\\": 1,\\n\" + \"    \\\"realName\\\": \\\"质量过关\\\",\\n\" //\n\t\t\t\t+ \"    \\\"examInfoDict\\\": [\\n\" + \"      {\\n\" + \"        \\\"id\\\": 1,\\n\" + \"        \\\"answerIs\\\": 1,\\n\" + \"        \\\"examType\\\": 0\\n\" //\n\t\t\t\t+ \"      },\\n\" + \"      {\\n\" + \"        \\\"id\\\": 2,\\n\" + \"        \\\"answerIs\\\": 0,\\n\" + \"        \\\"examType\\\": 0\\n\" + \"      },\\n\" //\n\t\t\t\t+ \"      {\\n\" + \"        \\\"id\\\": 3,\\n\" + \"        \\\"answerIs\\\": 0,\\n\" + \"        \\\"examType\\\": 1\\n\" + \"      }\\n\" + \"    ],\\n\" //\n\t\t\t\t+ \"    \\\"photoPath\\\": \\\"yx.mm.com\\\"\\n\" + \"  },\\n\" + \"  \\\"toSendManIdCard\\\": 1\\n\" + \"}\";\n\n\t\tJSONObject jsonObject = JSONUtil.parseObj(examJson).getJSONObject(\"examInfoDicts\");\n\t\tUserInfoDict userInfoDict = jsonObject.toBean(UserInfoDict.class);\n\n\t\tassertEquals(userInfoDict.getId(), new Integer(1));\n\t\tassertEquals(userInfoDict.getRealName(), \"质量过关\");\n\n\t\t//============\n\n\t\tString jsonStr = \"{\\\"id\\\":null,\\\"examInfoDict\\\":[{\\\"answerIs\\\":1, \\\"id\\\":null}]}\";//JSONUtil.toJsonStr(userInfoDict1);\n\t\tJSONObject jsonObject2 = JSONUtil.parseObj(jsonStr);//.getJSONObject(\"examInfoDicts\");\n\t\tUserInfoDict userInfoDict2 = jsonObject2.toBean(UserInfoDict.class);\n\t\tassertNull(userInfoDict2.getId());\n\t}\n\n\t/**\n\t * 针对Bean中Setter返回this测试是否可以成功调用Setter方法并注入\n\t */\n\t@Test\n\tpublic void testJson2Bean2() {\n\t\tString jsonStr = ResourceUtil.readUtf8Str(\"evaluation.json\");\n\t\tJSONObject obj = JSONUtil.parseObj(jsonStr);\n\t\tPerfectEvaluationProductResVo vo = obj.toBean(PerfectEvaluationProductResVo.class);\n\n\t\tassertEquals(obj.getStr(\"HA001\"), vo.getHA001());\n\t\tassertEquals(obj.getInt(\"costTotal\"), vo.getCostTotal());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONNullTest.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class JSONNullTest {\n\n\t@Test\n\tpublic void parseNullTest(){\n\t\tJSONObject bodyjson = JSONUtil.parseObj(\"{\\n\" +\n\t\t\t\t\"            \\\"device_model\\\": null,\\n\" +\n\t\t\t\t\"            \\\"device_status_date\\\": null,\\n\" +\n\t\t\t\t\"            \\\"imsi\\\": null,\\n\" +\n\t\t\t\t\"            \\\"act_date\\\": \\\"2021-07-23T06:23:26.000+00:00\\\"}\");\n\t\tassertEquals(JSONNull.class, bodyjson.get(\"device_model\").getClass());\n\t\tassertEquals(JSONNull.class, bodyjson.get(\"device_status_date\").getClass());\n\t\tassertEquals(JSONNull.class, bodyjson.get(\"imsi\").getClass());\n\n\t\tbodyjson.getConfig().setIgnoreNullValue(true);\n\t\tassertEquals(\"{\\\"act_date\\\":\\\"2021-07-23T06:23:26.000+00:00\\\"}\", bodyjson.toString());\n\t}\n\n\t@Test\n\tpublic void parseNullTest2(){\n\t\tJSONObject bodyjson = JSONUtil.parseObj(\"{\\n\" +\n\t\t\t\t\"            \\\"device_model\\\": null,\\n\" +\n\t\t\t\t\"            \\\"device_status_date\\\": null,\\n\" +\n\t\t\t\t\"            \\\"imsi\\\": null,\\n\" +\n\t\t\t\t\"            \\\"act_date\\\": \\\"2021-07-23T06:23:26.000+00:00\\\"}\", true);\n\t\tassertFalse(bodyjson.containsKey(\"device_model\"));\n\t\tassertFalse(bodyjson.containsKey(\"device_status_date\"));\n\t\tassertFalse(bodyjson.containsKey(\"imsi\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.annotation.PropIgnore;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.date.DatePattern;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.test.bean.*;\nimport cn.hutool.json.test.bean.report.CaseReport;\nimport cn.hutool.json.test.bean.report.StepReport;\nimport cn.hutool.json.test.bean.report.SuiteReport;\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.StringReader;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.sql.Timestamp;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * JSONObject单元测试\n *\n * @author Looly\n */\npublic class JSONObjectTest {\n\n\t@Test\n\t@Disabled\n\tpublic void toStringTest() {\n\t\tfinal String str = \"{\\\"code\\\": 500, \\\"data\\\":null}\";\n\t\tfinal JSONObject jsonObject = new JSONObject(str);\n\t\tConsole.log(jsonObject);\n\t\tjsonObject.getConfig().setIgnoreNullValue(true);\n\t\tConsole.log(jsonObject.toStringPretty());\n\t}\n\n\t@Test\n\tpublic void toStringTest2() {\n\t\tfinal String str = \"{\\\"test\\\":\\\"关于开展2018年度“文明集体”、“文明职工”评选表彰活动的通知\\\"}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(str);\n\t\tassertEquals(str, json.toString());\n\t}\n\n\t/**\n\t * 测试JSON中自定义日期格式输出正确性\n\t */\n\t@Test\n\tpublic void toStringTest3() {\n\t\tfinal JSONObject json = Objects.requireNonNull(JSONUtil.createObj()//\n\t\t\t\t.set(\"dateTime\", DateUtil.parse(\"2019-05-02 22:12:01\")))//\n\t\t\t.setDateFormat(DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"{\\\"dateTime\\\":\\\"2019-05-02\\\"}\", json.toString());\n\t}\n\n\t@Test\n\tpublic void toStringWithDateTest() {\n\t\tJSONObject json = JSONUtil.createObj().set(\"date\", DateUtil.parse(\"2019-05-08 19:18:21\"));\n\t\tassert json != null;\n\t\tassertEquals(\"{\\\"date\\\":1557314301000}\", json.toString());\n\n\t\tjson = Objects.requireNonNull(JSONUtil.createObj().set(\"date\", DateUtil.parse(\"2019-05-08 19:18:21\"))).setDateFormat(DatePattern.NORM_DATE_PATTERN);\n\t\tassertEquals(\"{\\\"date\\\":\\\"2019-05-08\\\"}\", json.toString());\n\t}\n\n\n\t@Test\n\tpublic void putAllTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj()\n\t\t\t.set(\"a\", \"value1\")\n\t\t\t.set(\"b\", \"value2\")\n\t\t\t.set(\"c\", \"value3\")\n\t\t\t.set(\"d\", true);\n\n\t\tfinal JSONObject json2 = JSONUtil.createObj()\n\t\t\t.set(\"a\", \"value21\")\n\t\t\t.set(\"b\", \"value22\");\n\n\t\t// putAll操作会覆盖相同key的值，因此a,b两个key的值改变，c的值不变\n\t\tjson1.putAll(json2);\n\n\t\tassertEquals(json1.get(\"a\"), \"value21\");\n\t\tassertEquals(json1.get(\"b\"), \"value22\");\n\t\tassertEquals(json1.get(\"c\"), \"value3\");\n\t}\n\n\t@Test\n\tpublic void parseStringTest() {\n\t\tfinal String jsonStr = \"{\\\"b\\\":\\\"value2\\\",\\\"c\\\":\\\"value3\\\",\\\"a\\\":\\\"value1\\\", \\\"d\\\": true, \\\"e\\\": null}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(jsonStr);\n\t\tassertEquals(jsonObject.get(\"a\"), \"value1\");\n\t\tassertEquals(jsonObject.get(\"b\"), \"value2\");\n\t\tassertEquals(jsonObject.get(\"c\"), \"value3\");\n\t\tassertEquals(jsonObject.get(\"d\"), true);\n\n\t\tassertTrue(jsonObject.containsKey(\"e\"));\n\t\tassertEquals(jsonObject.get(\"e\"), JSONNull.NULL);\n\t}\n\n\t@Test\n\tpublic void parseStringTest2() {\n\t\tfinal String jsonStr = \"{\\\"file_name\\\":\\\"RMM20180127009_731.000\\\",\\\"error_data\\\":\\\"201121151350701001252500000032 18973908335 18973908335 13601893517 201711211700152017112115135420171121 6594000000010100000000000000000000000043190101701001910072 100001100 \\\",\\\"error_code\\\":\\\"F140\\\",\\\"error_info\\\":\\\"最早发送时间格式错误，该字段可以为空，当不为空时正确填写格式为“YYYYMMDDHHMISS”\\\",\\\"app_name\\\":\\\"inter-pre-check\\\"}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(jsonStr);\n\t\tassertEquals(\"F140\", json.getStr(\"error_code\"));\n\t\tassertEquals(\"最早发送时间格式错误，该字段可以为空，当不为空时正确填写格式为“YYYYMMDDHHMISS”\", json.getStr(\"error_info\"));\n\t}\n\n\t@Test\n\tpublic void parseStringTest3() {\n\t\tfinal String jsonStr = \"{\\\"test\\\":\\\"体”、“文\\\"}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(jsonStr);\n\t\tassertEquals(\"体”、“文\", json.getStr(\"test\"));\n\t}\n\n\t@Test\n\tpublic void parseStringTest4() {\n\t\tfinal String jsonStr = \"{'msg':'这里还没有内容','data':{'cards':[]},'ok':0}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(jsonStr);\n\t\tassertEquals(new Integer(0), json.getInt(\"ok\"));\n\t\tassertEquals(new JSONArray(), json.getJSONObject(\"data\").getJSONArray(\"cards\"));\n\t}\n\n\t@Test\n\tpublic void parseBytesTest() {\n\t\tfinal String jsonStr = \"{'msg':'这里还没有内容','data':{'cards':[]},'ok':0}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(jsonStr.getBytes(StandardCharsets.UTF_8));\n\t\tassertEquals(new Integer(0), json.getInt(\"ok\"));\n\t\tassertEquals(new JSONArray(), json.getJSONObject(\"data\").getJSONArray(\"cards\"));\n\t}\n\n\t@Test\n\tpublic void parseReaderTest() {\n\t\tfinal String jsonStr = \"{'msg':'这里还没有内容','data':{'cards':[]},'ok':0}\";\n\t\tfinal StringReader stringReader = new StringReader(jsonStr);\n\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(stringReader);\n\t\tassertEquals(new Integer(0), json.getInt(\"ok\"));\n\t\tassertEquals(new JSONArray(), json.getJSONObject(\"data\").getJSONArray(\"cards\"));\n\t}\n\n\t@Test\n\tpublic void parseInputStreamTest() {\n\t\tfinal String jsonStr = \"{'msg':'这里还没有内容','data':{'cards':[]},'ok':0}\";\n\t\tfinal ByteArrayInputStream in = new ByteArrayInputStream(jsonStr.getBytes(StandardCharsets.UTF_8));\n\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(in);\n\t\tassertEquals(new Integer(0), json.getInt(\"ok\"));\n\t\tassertEquals(new JSONArray(), json.getJSONObject(\"data\").getJSONArray(\"cards\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void parseStringWithBomTest() {\n\t\tfinal String jsonStr = FileUtil.readUtf8String(\"f:/test/jsontest.txt\");\n\t\tfinal JSONObject json = new JSONObject(jsonStr);\n\t\tfinal JSONObject json2 = JSONUtil.parseObj(json);\n\t\tConsole.log(json);\n\t\tConsole.log(json2);\n\t}\n\n\t@Test\n\tpublic void parseStringWithSlashTest() {\n\t\t//在5.3.2之前，</div>中的/会被转义，修复此bug的单元测试\n\t\tfinal String jsonStr = \"{\\\"a\\\":\\\"<div>aaa</div>\\\"}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject json = new JSONObject(jsonStr);\n\t\tassertEquals(\"<div>aaa</div>\", json.get(\"a\"));\n\t\tassertEquals(jsonStr, json.toString());\n\t}\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal JSONObject subJson = JSONUtil.createObj().set(\"value1\", \"strValue1\").set(\"value2\", \"234\");\n\t\tfinal JSONObject json = JSONUtil.createObj().set(\"strValue\", \"strTest\").set(\"intValue\", 123)\n\t\t\t// 测试空字符串转对象\n\t\t\t.set(\"doubleValue\", \"\")\n\t\t\t.set(\"beanValue\", subJson)\n\t\t\t.set(\"list\", JSONUtil.createArray().set(\"a\").set(\"b\")).set(\"testEnum\", \"TYPE_A\");\n\n\t\tfinal TestBean bean = json.toBean(TestBean.class);\n\t\tassertEquals(\"a\", bean.getList().get(0));\n\t\tassertEquals(\"b\", bean.getList().get(1));\n\n\t\tassertEquals(\"strValue1\", bean.getBeanValue().getValue1());\n\t\t// BigDecimal转换检查\n\t\tassertEquals(new BigDecimal(\"234\"), bean.getBeanValue().getValue2());\n\t\t// 枚举转换检查\n\t\tassertEquals(TestEnum.TYPE_A, bean.getTestEnum());\n\t}\n\n\t@Test\n\tpublic void toBeanNullStrTest() {\n\t\tfinal JSONObject json = JSONUtil.createObj(JSONConfig.create().setIgnoreError(true))//\n\t\t\t.set(\"strValue\", \"null\")//\n\t\t\t.set(\"intValue\", 123)//\n\t\t\t// 子对象对应\"null\"字符串，如果忽略错误，跳过，否则抛出转换异常\n\t\t\t.set(\"beanValue\", \"null\")//\n\t\t\t.set(\"list\", JSONUtil.createArray().set(\"a\").set(\"b\"));\n\n\t\tfinal TestBean bean = json.toBean(TestBean.class);\n\t\t// 当JSON中为字符串\"null\"时应被当作字符串处理\n\t\tassertEquals(\"null\", bean.getStrValue());\n\t\t// 当JSON中为字符串\"null\"时Bean中的字段类型不匹配应在ignoreError模式下忽略注入\n\t\tassertNull(bean.getBeanValue());\n\t}\n\n\t@Test\n\tpublic void toBeanTest2() {\n\t\tfinal UserA userA = new UserA();\n\t\tuserA.setA(\"A user\");\n\t\tuserA.setName(\"{\\n\\t\\\"body\\\":{\\n\\t\\t\\\"loginId\\\":\\\"id\\\",\\n\\t\\t\\\"password\\\":\\\"pwd\\\"\\n\\t}\\n}\");\n\t\tuserA.setDate(new Date());\n\t\tuserA.setSqs(CollectionUtil.newArrayList(new Seq(\"seq1\"), new Seq(\"seq2\")));\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(userA);\n\t\tfinal UserA userA2 = json.toBean(UserA.class);\n\t\t// 测试数组\n\t\tassertEquals(\"seq1\", userA2.getSqs().get(0).getSeq());\n\t\t// 测试带换行符等特殊字符转换是否成功\n\t\tassertTrue(StrUtil.isNotBlank(userA2.getName()));\n\t}\n\n\t@Test\n\tpublic void toBeanWithNullTest() {\n\t\tfinal String jsonStr = \"{'data':{'userName':'ak','password': null}}\";\n\t\tfinal UserWithMap user = JSONUtil.toBean(JSONUtil.parseObj(jsonStr), UserWithMap.class);\n\t\tassertTrue(user.getData().containsKey(\"password\"));\n\t}\n\n\t@Test\n\tpublic void toBeanTest4() {\n\t\tfinal String json = \"{\\\"data\\\":{\\\"b\\\": \\\"c\\\"}}\";\n\n\t\tfinal UserWithMap map = JSONUtil.toBean(json, UserWithMap.class);\n\t\tassertEquals(\"c\", map.getData().get(\"b\"));\n\t}\n\n\t@Test\n\tpublic void toBeanTest5() {\n\t\tfinal String readUtf8Str = ResourceUtil.readUtf8Str(\"suiteReport.json\");\n\t\tfinal JSONObject json = JSONUtil.parseObj(readUtf8Str);\n\t\tfinal SuiteReport bean = json.toBean(SuiteReport.class);\n\n\t\t// 第一层\n\t\tfinal List<CaseReport> caseReports = bean.getCaseReports();\n\t\tfinal CaseReport caseReport = caseReports.get(0);\n\t\tassertNotNull(caseReport);\n\n\t\t// 第二层\n\t\tfinal List<StepReport> stepReports = caseReports.get(0).getStepReports();\n\t\tfinal StepReport stepReport = stepReports.get(0);\n\t\tassertNotNull(stepReport);\n\t}\n\n\t/**\n\t * 在JSON转Bean过程中，Bean中字段如果为父类定义的泛型类型，则应正确转换，此方法用于测试这类情况\n\t */\n\t@Test\n\tpublic void toBeanTest6() {\n\t\tfinal JSONObject json = JSONUtil.createObj()\n\t\t\t.set(\"targetUrl\", \"http://test.com\")\n\t\t\t.set(\"success\", \"true\")\n\t\t\t.set(\"result\", JSONUtil.createObj()\n\t\t\t\t.set(\"token\", \"tokenTest\")\n\t\t\t\t.set(\"userId\", \"测试用户1\"));\n\n\t\tfinal TokenAuthWarp2 bean = json.toBean(TokenAuthWarp2.class);\n\t\tassertEquals(\"http://test.com\", bean.getTargetUrl());\n\t\tassertEquals(\"true\", bean.getSuccess());\n\n\t\tfinal TokenAuthResponse result = bean.getResult();\n\t\tassertNotNull(result);\n\t\tassertEquals(\"tokenTest\", result.getToken());\n\t\tassertEquals(\"测试用户1\", result.getUserId());\n\t}\n\n\t/**\n\t * 泛型对象中的泛型参数如果未定义具体类型，按照JSON处理<br>\n\t * 此处用于测试获取泛型类型实际类型错误导致的空指针问题\n\t */\n\t@Test\n\tpublic void toBeanTest7() {\n\t\tfinal String jsonStr = \" {\\\"result\\\":{\\\"phone\\\":\\\"15926297342\\\",\\\"appKey\\\":\\\"e1ie12e1ewsdqw1\\\",\" +\n\t\t\t\"\\\"secret\\\":\\\"dsadadqwdqs121d1e2\\\",\\\"message\\\":\\\"hello world\\\"},\\\"code\\\":100,\\\"\" +\n\t\t\t\"message\\\":\\\"validate message\\\"}\";\n\t\tfinal ResultDto<?> dto = JSONUtil.toBean(jsonStr, ResultDto.class);\n\t\tassertEquals(\"validate message\", dto.getMessage());\n\t}\n\n\t@Test\n\tpublic void parseBeanTest() {\n\t\tfinal UserA userA = new UserA();\n\t\tuserA.setName(\"nameTest\");\n\t\tuserA.setDate(new Date());\n\t\tuserA.setSqs(CollectionUtil.newArrayList(new Seq(null), new Seq(\"seq2\")));\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(userA, false);\n\n\t\tassertTrue(json.containsKey(\"a\"));\n\t\tassertTrue(json.getJSONArray(\"sqs\").getJSONObject(0).containsKey(\"seq\"));\n\t}\n\n\t@Test\n\tpublic void parseBeanTest2() {\n\t\tfinal TestBean bean = new TestBean();\n\t\tbean.setDoubleValue(111.1);\n\t\tbean.setIntValue(123);\n\t\tbean.setList(CollUtil.newArrayList(\"a\", \"b\", \"c\"));\n\t\tbean.setStrValue(\"strTest\");\n\t\tbean.setTestEnum(TestEnum.TYPE_B);\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(bean, false);\n\t\t// 枚举转换检查\n\t\tassertEquals(\"TYPE_B\", json.get(\"testEnum\"));\n\n\t\tfinal TestBean bean2 = json.toBean(TestBean.class);\n\t\tassertEquals(bean.toString(), bean2.toString());\n\t}\n\n\t@Test\n\tpublic void parseBeanTest3() {\n\t\tfinal JSONObject json = JSONUtil.createObj()\n\t\t\t.set(\"code\", 22)\n\t\t\t.set(\"data\", \"{\\\"jobId\\\": \\\"abc\\\", \\\"videoUrl\\\": \\\"http://a.com/a.mp4\\\"}\");\n\n\t\tfinal JSONBean bean = json.toBean(JSONBean.class);\n\t\tassertEquals(22, bean.getCode());\n\t\tassertEquals(\"abc\", bean.getData().getObj(\"jobId\"));\n\t\tassertEquals(\"http://a.com/a.mp4\", bean.getData().getObj(\"videoUrl\"));\n\t}\n\n\t@Test\n\tpublic void beanTransTest() {\n\t\tfinal UserA userA = new UserA();\n\t\tuserA.setA(\"A user\");\n\t\tuserA.setName(\"nameTest\");\n\t\tuserA.setDate(new Date());\n\n\t\tfinal JSONObject userAJson = JSONUtil.parseObj(userA);\n\t\tfinal UserB userB = JSONUtil.toBean(userAJson, UserB.class);\n\n\t\tassertEquals(userA.getName(), userB.getName());\n\t\tassertEquals(userA.getDate(), userB.getDate());\n\t}\n\n\t@Test\n\tpublic void beanTransTest2() {\n\t\tfinal UserA userA = new UserA();\n\t\tuserA.setA(\"A user\");\n\t\tuserA.setName(\"nameTest\");\n\t\tuserA.setDate(DateUtil.parse(\"2018-10-25\"));\n\n\t\tfinal JSONObject userAJson = JSONUtil.parseObj(userA);\n\t\t// 自定义日期格式\n\t\tuserAJson.setDateFormat(\"yyyy-MM-dd\");\n\n\t\tfinal UserA bean = JSONUtil.toBean(userAJson.toString(), UserA.class);\n\t\tassertEquals(DateUtil.parse(\"2018-10-25\"), bean.getDate());\n\t}\n\n\t@Test\n\tpublic void beanTransTest3() {\n\t\tfinal JSONObject userAJson = JSONUtil.createObj()\n\t\t\t.set(\"a\", \"AValue\")\n\t\t\t.set(\"name\", \"nameValue\")\n\t\t\t.set(\"date\", \"08:00:00\");\n\t\tfinal UserA bean = JSONUtil.toBean(userAJson.toString(), UserA.class);\n\t\tassertEquals(DateUtil.today() + \" 08:00:00\", DateUtil.date(bean.getDate()).toString());\n\t}\n\n\t@Test\n\tpublic void parseFromBeanTest() {\n\t\tfinal UserA userA = new UserA();\n\t\tuserA.setA(null);\n\t\tuserA.setName(\"nameTest\");\n\t\tuserA.setDate(new Date());\n\n\t\tfinal JSONObject userAJson = JSONUtil.parseObj(userA);\n\t\tassertFalse(userAJson.containsKey(\"a\"));\n\n\t\tfinal JSONObject userAJsonWithNullValue = JSONUtil.parseObj(userA, false);\n\t\tassertTrue(userAJsonWithNullValue.containsKey(\"a\"));\n\t\tassertTrue(userAJsonWithNullValue.containsKey(\"sqs\"));\n\t}\n\n\t@Test\n\tpublic void specialCharTest() {\n\t\tfinal String json = \"{\\\"pattern\\\": \\\"[abc]\\b\\u2001\\\", \\\"pattern2Json\\\": {\\\"patternText\\\": \\\"[ab]\\\\b\\\"}}\";\n\t\tfinal JSONObject obj = JSONUtil.parseObj(json);\n\t\tassertEquals(\"[abc]\\\\b\\\\u2001\", obj.getStrEscaped(\"pattern\"));\n\t\tassertEquals(\"{\\\"patternText\\\":\\\"[ab]\\\\b\\\"}\", obj.getStrEscaped(\"pattern2Json\"));\n\t}\n\n\t@Test\n\tpublic void getStrTest() {\n\t\tfinal String json = \"{\\\"name\\\": \\\"yyb\\\\nbbb\\\"}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(json);\n\n\t\t// 没有转义按照默认规则显示\n\t\tassertEquals(\"yyb\\nbbb\", jsonObject.getStr(\"name\"));\n\t\t// 转义按照字符串显示\n\t\tassertEquals(\"yyb\\\\nbbb\", jsonObject.getStrEscaped(\"name\"));\n\n\t\tfinal String bbb = jsonObject.getStr(\"bbb\", \"defaultBBB\");\n\t\tassertEquals(\"defaultBBB\", bbb);\n\t}\n\n\t@Test\n\tpublic void aliasTest() {\n\t\tfinal BeanWithAlias beanWithAlias = new BeanWithAlias();\n\t\tbeanWithAlias.setValue1(\"张三\");\n\t\tbeanWithAlias.setValue2(35);\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(beanWithAlias);\n\t\tassertEquals(\"张三\", jsonObject.getStr(\"name\"));\n\t\tassertEquals(new Integer(35), jsonObject.getInt(\"age\"));\n\n\t\tfinal JSONObject json = JSONUtil.createObj()\n\t\t\t.set(\"name\", \"张三\")\n\t\t\t.set(\"age\", 35);\n\t\tfinal BeanWithAlias bean = JSONUtil.toBean(Objects.requireNonNull(json).toString(), BeanWithAlias.class);\n\t\tassertEquals(\"张三\", bean.getValue1());\n\t\tassertEquals(new Integer(35), bean.getValue2());\n\t}\n\n\t@Test\n\tpublic void setDateFormatTest() {\n\t\tfinal JSONConfig jsonConfig = JSONConfig.create();\n\t\tjsonConfig.setDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n\n\t\tfinal JSONObject json = new JSONObject(jsonConfig);\n\t\tjson.append(\"date\", DateUtil.parse(\"2020-06-05 11:16:11\"));\n\t\tjson.append(\"bbb\", \"222\");\n\t\tjson.append(\"aaa\", \"123\");\n\t\tassertEquals(\"{\\\"date\\\":[\\\"2020-06-05 11:16:11\\\"],\\\"bbb\\\":[\\\"222\\\"],\\\"aaa\\\":[\\\"123\\\"]}\", json.toString());\n\t}\n\n\t@Test\n\tpublic void setDateFormatTest2() {\n\t\tfinal JSONConfig jsonConfig = JSONConfig.create();\n\t\tjsonConfig.setDateFormat(\"yyyy#MM#dd\");\n\n\t\tfinal Date date = DateUtil.parse(\"2020-06-05 11:16:11\");\n\t\tfinal JSONObject json = new JSONObject(jsonConfig);\n\t\tjson.set(\"date\", date);\n\t\tjson.set(\"bbb\", \"222\");\n\t\tjson.set(\"aaa\", \"123\");\n\n\t\tfinal String jsonStr = \"{\\\"date\\\":\\\"2020#06#05\\\",\\\"bbb\\\":\\\"222\\\",\\\"aaa\\\":\\\"123\\\"}\";\n\n\t\tassertEquals(jsonStr, json.toString());\n\n\t\t// 解析测试\n\t\tfinal JSONObject parse = JSONUtil.parseObj(jsonStr, jsonConfig);\n\t\tassertEquals(DateUtil.beginOfDay(date), parse.getDate(\"date\"));\n\t}\n\n\t@Test\n\tpublic void setCustomDateFormatTest() {\n\t\tfinal JSONConfig jsonConfig = JSONConfig.create();\n\t\tjsonConfig.setDateFormat(\"#sss\");\n\n\t\tfinal Date date = DateUtil.parse(\"2020-06-05 11:16:11\");\n\t\tfinal JSONObject json = new JSONObject(jsonConfig);\n\t\tjson.set(\"date\", date);\n\t\tjson.set(\"bbb\", \"222\");\n\t\tjson.set(\"aaa\", \"123\");\n\n\t\tfinal String jsonStr = \"{\\\"date\\\":1591326971,\\\"bbb\\\":\\\"222\\\",\\\"aaa\\\":\\\"123\\\"}\";\n\n\t\tassertEquals(jsonStr, json.toString());\n\n\t\t// 解析测试\n\t\tfinal JSONObject parse = JSONUtil.parseObj(jsonStr, jsonConfig);\n\t\tassertEquals(date, parse.getDate(\"date\"));\n\t}\n\n\t@Test\n\tpublic void getTimestampTest() {\n\t\tfinal String timeStr = \"1970-01-01 00:00:00\";\n\t\tfinal JSONObject jsonObject = JSONUtil.createObj().set(\"time\", timeStr);\n\t\tfinal Timestamp time = jsonObject.get(\"time\", Timestamp.class);\n\t\tassertEquals(\"1970-01-01 00:00:00.0\", time.toString());\n\t}\n\n\tpublic enum TestEnum {\n\t\tTYPE_A, TYPE_B\n\t}\n\n\t/**\n\t * 测试Bean\n\t *\n\t * @author Looly\n\t */\n\t@Data\n\tpublic static class TestBean {\n\t\tprivate String strValue;\n\t\tprivate int intValue;\n\t\tprivate Double doubleValue;\n\t\tprivate SubBean beanValue;\n\t\tprivate List<String> list;\n\t\tprivate TestEnum testEnum;\n\t}\n\n\t/**\n\t * 测试子Bean\n\t *\n\t * @author Looly\n\t */\n\t@Data\n\tpublic static class SubBean {\n\t\tprivate String value1;\n\t\tprivate BigDecimal value2;\n\t}\n\n\t@Data\n\tpublic static class BeanWithAlias {\n\t\t@Alias(\"name\")\n\t\tprivate String value1;\n\t\t@Alias(\"age\")\n\t\tprivate Integer value2;\n\t}\n\n\t@Test\n\tpublic void parseBeanSameNameTest() {\n\t\tfinal SameNameBean sameNameBean = new SameNameBean();\n\t\tfinal JSONObject parse = JSONUtil.parseObj(sameNameBean);\n\t\tassertEquals(\"123\", parse.getStr(\"username\"));\n\t\tassertEquals(\"abc\", parse.getStr(\"userName\"));\n\n\t\t// 测试ignore注解是否有效\n\t\tassertNull(parse.getStr(\"fieldToIgnore\"));\n\t}\n\n\t/**\n\t * 测试子Bean\n\t *\n\t * @author Looly\n\t */\n\t@SuppressWarnings(\"FieldCanBeLocal\")\n\tpublic static class SameNameBean {\n\t\tprivate final String username = \"123\";\n\t\tprivate final String userName = \"abc\";\n\n\t\tpublic String getUsername() {\n\t\t\treturn username;\n\t\t}\n\n\t\t@PropIgnore\n\t\tprivate final String fieldToIgnore = \"sfdsdads\";\n\n\t\tpublic String getUserName() {\n\t\t\treturn userName;\n\t\t}\n\n\t\tpublic String getFieldToIgnore() {\n\t\t\treturn this.fieldToIgnore;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void setEntryTest() {\n\t\tfinal HashMap<String, String> of = MapUtil.of(\"test\", \"testValue\");\n\t\tfinal Set<Map.Entry<String, String>> entries = of.entrySet();\n\t\tfinal Map.Entry<String, String> next = entries.iterator().next();\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(next);\n\t\tassertEquals(\"{\\\"test\\\":\\\"testValue\\\"}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void createJSONObjectTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\t// 集合类不支持转为JSONObject\n\t\t\tnew JSONObject(new JSONArray(), JSONConfig.create());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void floatTest() {\n\t\tfinal Map<String, Object> map = new HashMap<>();\n\t\tmap.put(\"c\", 2.0F);\n\n\t\tfinal String s = JSONUtil.toJsonStr(map);\n\t\tassertEquals(\"{\\\"c\\\":2}\", s);\n\t}\n\n\t@Test\n\tpublic void accumulateTest() {\n\t\tfinal JSONObject jsonObject = JSONUtil.createObj().accumulate(\"key1\", \"value1\");\n\t\tassertEquals(\"{\\\"key1\\\":\\\"value1\\\"}\", jsonObject.toString());\n\n\t\tjsonObject.accumulate(\"key1\", \"value2\");\n\t\tassertEquals(\"{\\\"key1\\\":[\\\"value1\\\",\\\"value2\\\"]}\", jsonObject.toString());\n\n\t\tjsonObject.accumulate(\"key1\", \"value3\");\n\t\tassertEquals(\"{\\\"key1\\\":[\\\"value1\\\",\\\"value2\\\",\\\"value3\\\"]}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void putByPathTest() {\n\t\tfinal JSONObject json = new JSONObject();\n\t\tjson.putByPath(\"aa.bb\", \"BB\");\n\t\tassertEquals(\"{\\\"aa\\\":{\\\"bb\\\":\\\"BB\\\"}}\", json.toString());\n\t}\n\n\n\t@Test\n\tpublic void bigDecimalTest() {\n\t\tfinal String jsonStr = \"{\\\"orderId\\\":\\\"1704747698891333662002277\\\"}\";\n\t\tfinal BigDecimalBean bigDecimalBean = JSONUtil.toBean(jsonStr, BigDecimalBean.class);\n\t\tassertEquals(\"{\\\"orderId\\\":1704747698891333662002277}\", JSONUtil.toJsonStr(bigDecimalBean));\n\t}\n\n\t@Data\n\tstatic\n\tclass BigDecimalBean {\n\t\tprivate BigDecimal orderId;\n\t}\n\n\t@Test\n\tpublic void filterIncludeTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj(JSONConfig.create())\n\t\t\t.set(\"a\", \"value1\")\n\t\t\t.set(\"b\", \"value2\")\n\t\t\t.set(\"c\", \"value3\")\n\t\t\t.set(\"d\", true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> pair.getKey().equals(\"b\"));\n\t\tassertEquals(\"{\\\"b\\\":\\\"value2\\\"}\", s);\n\t}\n\n\t@Test\n\tpublic void filterExcludeTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj(JSONConfig.create())\n\t\t\t.set(\"a\", \"value1\")\n\t\t\t.set(\"b\", \"value2\")\n\t\t\t.set(\"c\", \"value3\")\n\t\t\t.set(\"d\", true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> false == pair.getKey().equals(\"b\"));\n\t\tassertEquals(\"{\\\"a\\\":\\\"value1\\\",\\\"c\\\":\\\"value3\\\",\\\"d\\\":true}\", s);\n\t}\n\n\t@Test\n\tpublic void editTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj(JSONConfig.create())\n\t\t\t.set(\"a\", \"value1\")\n\t\t\t.set(\"b\", \"value2\")\n\t\t\t.set(\"c\", \"value3\")\n\t\t\t.set(\"d\", true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> {\n\t\t\tif (\"b\".equals(pair.getKey())) {\n\t\t\t\t// 修改值为新值\n\t\t\t\tpair.setValue(pair.getValue() + \"_edit\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t// 除了\"b\"，其他都去掉\n\t\t\treturn false;\n\t\t});\n\t\tassertEquals(\"{\\\"b\\\":\\\"value2_edit\\\"}\", s);\n\t}\n\n\t@Test\n\tpublic void toUnderLineCaseTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj(JSONConfig.create())\n\t\t\t.set(\"aKey\", \"value1\")\n\t\t\t.set(\"bJob\", \"value2\")\n\t\t\t.set(\"cGood\", \"value3\")\n\t\t\t.set(\"d\", true);\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> {\n\t\t\tpair.setKey(StrUtil.toUnderlineCase(pair.getKey().toString()));\n\t\t\treturn true;\n\t\t});\n\t\tassertEquals(\"{\\\"a_key\\\":\\\"value1\\\",\\\"b_job\\\":\\\"value2\\\",\\\"c_good\\\":\\\"value3\\\",\\\"d\\\":true}\", s);\n\t}\n\n\t@Test\n\tpublic void nullToEmptyTest() {\n\t\tfinal JSONObject json1 = JSONUtil.createObj(JSONConfig.create().setIgnoreNullValue(false))\n\t\t\t.set(\"a\", null)\n\t\t\t.set(\"b\", \"value2\");\n\n\t\tfinal String s = json1.toJSONString(0, (pair) -> {\n\t\t\tpair.setValue(ObjectUtil.defaultIfNull(pair.getValue(), StrUtil.EMPTY));\n\t\t\treturn true;\n\t\t});\n\t\tassertEquals(\"{\\\"a\\\":\\\"\\\",\\\"b\\\":\\\"value2\\\"}\", s);\n\t}\n\n\t@Test\n\tpublic void parseFilterTest() {\n\t\tfinal String jsonStr = \"{\\\"b\\\":\\\"value2\\\",\\\"c\\\":\\\"value3\\\",\\\"a\\\":\\\"value1\\\", \\\"d\\\": true, \\\"e\\\": null}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject jsonObject = new JSONObject(jsonStr, null, (pair) -> \"b\".equals(pair.getKey()));\n\t\tassertEquals(1, jsonObject.size());\n\t\tassertEquals(\"value2\", jsonObject.get(\"b\"));\n\t}\n\n\t@Test\n\tpublic void parseFilterEditTest() {\n\t\tfinal String jsonStr = \"{\\\"b\\\":\\\"value2\\\",\\\"c\\\":\\\"value3\\\",\\\"a\\\":\\\"value1\\\", \\\"d\\\": true, \\\"e\\\": null}\";\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject jsonObject = new JSONObject(jsonStr, null, (pair) -> {\n\t\t\tif (\"b\".equals(pair.getKey())) {\n\t\t\t\tpair.setValue(pair.getValue() + \"_edit\");\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tassertEquals(\"value2_edit\", jsonObject.get(\"b\"));\n\t}\n\n\t@Test\n\tvoid issue3844Test(){\n\t\tString camelCaseStr = \"{\\\"userAge\\\":\\\"123\\\"}\";\n\t\tfinal JSONObject entries = new JSONObject(camelCaseStr, null, (pair) -> {\n\t\t\tpair.setKey(StrUtil.toUnderlineCase(pair.getKey()));\n\t\t\treturn true;\n\t\t});\n\n\t\tConsole.log(entries);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONPathTest.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport cn.hutool.core.lang.TypeReference;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * JSON路径单元测试\n *\n * @author looly\n *\n */\npublic class JSONPathTest {\n\n\t@Test\n\tpublic void getByPathTest() {\n\t\tString json = \"[{\\\"id\\\":\\\"1\\\",\\\"name\\\":\\\"xingming\\\"},{\\\"id\\\":\\\"2\\\",\\\"name\\\":\\\"mingzi\\\"}]\";\n\t\tObject value = JSONUtil.parseArray(json).getByPath(\"[0].name\");\n\t\tassertEquals(\"xingming\", value);\n\t\tvalue = JSONUtil.parseArray(json).getByPath(\"[1].name\");\n\t\tassertEquals(\"mingzi\", value);\n\t}\n\n\t@Test\n\tpublic void getByPathTest2(){\n\t\tString str = \"{'accountId':111}\";\n\t\tJSON json = JSONUtil.parse(str);\n\t\tLong accountId = JSONUtil.getByPath(json, \"$.accountId\", 0L);\n\t\tassertEquals(111L, accountId.longValue());\n\t}\n\n\t@Test\n\tpublic void getByPathTest3(){\n\t\tString str = \"[{'accountId':1},{'accountId':2},{'accountId':3}]\";\n\t\tJSON json = JSONUtil.parse(str);\n\t\t// 返回指定泛型的对象 List<Long>\n\t\tList<Long> accountIds = json.getByPath(\"$.accountId\", new TypeReference<List<Long>>() {\n\t\t});\n\t\tassertNotNull(accountIds);\n\t\tassertArrayEquals(new Long[]{1L, 2L, 3L}, accountIds.toArray());\n\n\t\tstr = \"{\\\"accountInfos\\\": [{\\\"accountId\\\":1},{\\\"accountId\\\":2},{\\\"accountId\\\":3}]}\";\n\t\tjson = JSONUtil.parse(str);\n\t\t// 返回指定泛型的对象 List<Long>\n\t\taccountIds = json.getByPath(\"$.accountInfos.accountId\", new TypeReference<List<Long>>() {\n\t\t});\n\t\tassertNotNull(accountIds);\n\t\tassertArrayEquals(new Long[]{1L, 2L, 3L}, accountIds.toArray());\n\t}\n\n\t@Test\n\tpublic void getByPathWithWildcardTest() {\n\t\t// 测试通配符 [*] 语法\n\t\tJSONObject root = new JSONObject()\n\t\t\t.put(\"actionMessage\", new JSONObject()\n\t\t\t\t.put(\"alertResults\", new JSONArray())\n\t\t\t\t.put(\"decodeFeas\", new JSONArray()\n\t\t\t\t\t.put(new JSONObject()\n\t\t\t\t\t\t.put(\"body\", new JSONObject()\n\t\t\t\t\t\t\t.put(\"lats\", new JSONArray()\n\t\t\t\t\t\t\t\t.put(new JSONObject().put(\"begin\", 4260).put(\"text\", \"呵呵\"))\n\t\t\t\t\t\t\t\t.put(new JSONObject().put(\"begin\", 4260).put(\"text\", \"你好 \"))\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\tObject byPath = JSONUtil.getByPath(root, \"$.actionMessage.decodeFeas[0].body.lats[*].text\");\n\t\tassertNotNull(byPath);\n\t\tassertEquals(\"[呵呵, 你好 ]\", byPath.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONStrFormatterTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * JSON字符串格式化单元测试\n * @author looly\n *\n */\npublic class JSONStrFormatterTest {\n\n\t@Test\n\tpublic void formatTest() {\n\t\tString json = \"{'age':23,'aihao':['pashan','movies'],'name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies','name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies']}]}}\";\n\t\tString result = JSONStrFormatter.format(json);\n\t\tassertNotNull(result);\n\t}\n\n\t@Test\n\tpublic void formatTest2() {\n\t\tString json = \"{\\\"abc\\\":{\\\"def\\\":\\\"\\\\\\\"[ghi]\\\"}}\";\n\t\tString result = JSONStrFormatter.format(json);\n\t\tassertNotNull(result);\n\t}\n\n\t@Test\n\tpublic void formatTest3() {\n\t\tString json = \"{\\\"id\\\":13,\\\"title\\\":\\\"《标题》\\\",\\\"subtitle\\\":\\\"副标题z'c'z'xv'c'xv\\\",\\\"user_id\\\":6,\\\"type\\\":0}\";\n\t\tString result = JSONStrFormatter.format(json);\n\t\tassertNotNull(result);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void formatTest4(){\n\t\tString jsonStr = \"{\\\"employees\\\":[{\\\"firstName\\\":\\\"Bill\\\",\\\"lastName\\\":\\\"Gates\\\"},{\\\"firstName\\\":\\\"George\\\",\\\"lastName\\\":\\\"Bush\\\"},{\\\"firstName\\\":\\\"Thomas\\\",\\\"lastName\\\":\\\"Carter\\\"}]}\";\n\t\tConsole.log(JSONUtil.formatJsonStr(jsonStr));\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONSupportTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class JSONSupportTest {\n\n\t/**\n\t * https://github.com/chinabugotech/hutool/issues/1779\n\t * 在JSONSupport的JSONBeanParse中，如果使用json.toBean，会导致JSONBeanParse.parse方法反复递归调用，最终栈溢出<br>\n\t * 因此parse方法默认实现必须避开JSONBeanParse.parse调用。\n\t */\n\t@Test\n\tpublic void parseTest() {\n\t\tString jsonstr = \"{\\n\" +\n\t\t\t\t\"    \\\"location\\\": \\\"https://hutool.cn\\\",\\n\" +\n\t\t\t\t\"    \\\"message\\\": \\\"这是一条测试消息\\\",\\n\" +\n\t\t\t\t\"    \\\"requestId\\\": \\\"123456789\\\",\\n\" +\n\t\t\t\t\"    \\\"traceId\\\": \\\"987654321\\\"\\n\" +\n\t\t\t\t\"}\";\n\n\n\t\tfinal TestBean testBean = JSONUtil.toBean(jsonstr, TestBean.class);\n\t\tassertEquals(\"https://hutool.cn\", testBean.getLocation());\n\t\tassertEquals(\"这是一条测试消息\", testBean.getMessage());\n\t\tassertEquals(\"123456789\", testBean.getRequestId());\n\t\tassertEquals(\"987654321\", testBean.getTraceId());\n\t}\n\n\t@EqualsAndHashCode(callSuper = true)\n\t@Data\n\tstatic class TestBean  extends JSONSupport{\n\n\t\tprivate String location;\n\n\t\tprivate String message;\n\n\t\tprivate String requestId;\n\n\t\tprivate String traceId;\n\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java",
    "content": "package cn.hutool.json;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.json.test.bean.Price;\nimport cn.hutool.json.test.bean.UserA;\nimport cn.hutool.json.test.bean.UserC;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.SQLException;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JSONUtilTest {\n\n\t/**\n\t * 出现语法错误时报错，检查解析\\x字符时是否会导致死循环异常\n\t */\n\t@Test\n\tpublic void parseTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tfinal JSONArray jsonArray = JSONUtil.parseArray(\"[{\\\"a\\\":\\\"a\\\\x]\");\n\t\t\tConsole.log(jsonArray);\n\t\t});\n\t}\n\n\t/**\n\t * 数字解析为JSONArray报错\n\t */\n\t@Test\n\tpublic void parseNumberToJSONArrayTest2() {\n\t\tfinal JSONArray json = JSONUtil.parseArray(123L,\n\t\t\tJSONConfig.create().setIgnoreError(true));\n\t\tassertNotNull(json);\n\t}\n\n\t/**\n\t * 数字解析为JSONArray报错\n\t */\n\t@Test\n\tpublic void parseNumberTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tfinal JSONArray json = JSONUtil.parseArray(123L);\n\t\t\tassertNotNull(json);\n\t\t});\n\t}\n\n\t/**\n\t * 数字解析为JSONObject忽略\n\t */\n\t@Test\n\tpublic void parseNumberTest2() {\n\t\tfinal JSONObject json = JSONUtil.parseObj(123L, JSONConfig.create().setIgnoreError(true));\n\t\tassertEquals(new JSONObject(), json);\n\t}\n\n\t@Test\n\tpublic void toJsonStrTest() {\n\t\tfinal UserA a1 = new UserA();\n\t\ta1.setA(\"aaaa\");\n\t\ta1.setDate(DateUtil.date());\n\t\ta1.setName(\"AAAAName\");\n\t\tfinal UserA a2 = new UserA();\n\t\ta2.setA(\"aaaa222\");\n\t\ta2.setDate(DateUtil.date());\n\t\ta2.setName(\"AAAA222Name\");\n\n\t\tfinal ArrayList<UserA> list = CollectionUtil.newArrayList(a1, a2);\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"total\", 13);\n\t\tmap.put(\"rows\", list);\n\n\t\tfinal String str = JSONUtil.toJsonPrettyStr(map);\n\t\tJSONUtil.parse(str);\n\t\tassertNotNull(str);\n\t}\n\n\t@Test\n\tpublic void toJsonStrTest2() {\n\t\tfinal Map<String, Object> model = new HashMap<>();\n\t\tmodel.put(\"mobile\", \"17610836523\");\n\t\tmodel.put(\"type\", 1);\n\n\t\tfinal Map<String, Object> data = new HashMap<>();\n\t\tdata.put(\"model\", model);\n\t\tdata.put(\"model2\", model);\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(data);\n\n\t\tassertTrue(jsonObject.containsKey(\"model\"));\n\t\tassertEquals(1, jsonObject.getJSONObject(\"model\").getInt(\"type\").intValue());\n\t\tassertEquals(\"17610836523\", jsonObject.getJSONObject(\"model\").getStr(\"mobile\"));\n\t\t// assertEquals(\"{\\\"model\\\":{\\\"type\\\":1,\\\"mobile\\\":\\\"17610836523\\\"}}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void toJsonStrTest3() {\n\t\t// 验证某个字段为JSON字符串时转义是否规范\n\t\tfinal JSONObject object = new JSONObject(true);\n\t\tobject.set(\"name\", \"123123\");\n\t\tobject.set(\"value\", \"\\\\\");\n\t\tobject.set(\"value2\", \"</\");\n\n\t\tfinal HashMap<String, String> map = MapUtil.newHashMap();\n\t\tmap.put(\"user\", object.toString());\n\n\t\tfinal JSONObject json = JSONUtil.parseObj(map);\n\t\tassertEquals(\"{\\\"name\\\":\\\"123123\\\",\\\"value\\\":\\\"\\\\\\\\\\\",\\\"value2\\\":\\\"</\\\"}\", json.get(\"user\"));\n\t\tassertEquals(\"{\\\"user\\\":\\\"{\\\\\\\"name\\\\\\\":\\\\\\\"123123\\\\\\\",\\\\\\\"value\\\\\\\":\\\\\\\"\\\\\\\\\\\\\\\\\\\\\\\",\\\\\\\"value2\\\\\\\":\\\\\\\"</\\\\\\\"}\\\"}\", json.toString());\n\n\t\tfinal JSONObject json2 = JSONUtil.parseObj(json.toString());\n\t\tassertEquals(\"{\\\"name\\\":\\\"123123\\\",\\\"value\\\":\\\"\\\\\\\\\\\",\\\"value2\\\":\\\"</\\\"}\", json2.get(\"user\"));\n\t}\n\n\t@Test\n\tpublic void toJsonStrFromSortedTest() {\n\t\tfinal SortedMap<Object, Object> sortedMap = new TreeMap<Object, Object>() {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t{\n\t\t\t\tput(\"attributes\", \"a\");\n\t\t\t\tput(\"b\", \"b\");\n\t\t\t\tput(\"c\", \"c\");\n\t\t\t}\n\t\t};\n\n\t\tassertEquals(\"{\\\"attributes\\\":\\\"a\\\",\\\"b\\\":\\\"b\\\",\\\"c\\\":\\\"c\\\"}\", JSONUtil.toJsonStr(sortedMap));\n\t}\n\n\t/**\n\t * 泛型多层嵌套测试\n\t */\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal String json = \"{\\\"ADT\\\":[[{\\\"BookingCode\\\":[\\\"N\\\",\\\"N\\\"]}]]}\";\n\n\t\tfinal Price price = JSONUtil.toBean(json, Price.class);\n\t\tassertEquals(\"N\", price.getADT().get(0).get(0).getBookingCode().get(0));\n\t}\n\n\t@Test\n\tpublic void toBeanTest2() {\n\t\t// 测试JSONObject转为Bean中字符串字段的情况\n\t\tfinal String json = \"{\\\"id\\\":123,\\\"name\\\":\\\"张三\\\",\\\"prop\\\":{\\\"gender\\\":\\\"男\\\", \\\"age\\\":18}}\";\n\t\tfinal UserC user = JSONUtil.toBean(json, UserC.class);\n\t\tassertNotNull(user.getProp());\n\t\tfinal String prop = user.getProp();\n\t\tfinal JSONObject propJson = JSONUtil.parseObj(prop);\n\t\tassertEquals(\"男\", propJson.getStr(\"gender\"));\n\t\tassertEquals(18, propJson.getInt(\"age\").intValue());\n\t\t// assertEquals(\"{\\\"age\\\":18,\\\"gender\\\":\\\"男\\\"}\", user.getProp());\n\t}\n\n\t@Test\n\tpublic void getStrTest() {\n\t\tfinal String html = \"{\\\"name\\\":\\\"Something must have been changed since you leave\\\"}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(html);\n\t\tassertEquals(\"Something must have been changed since you leave\", jsonObject.getStr(\"name\"));\n\t}\n\n\t@Test\n\tpublic void getStrTest2() {\n\t\tfinal String html = \"{\\\"name\\\":\\\"Something\\\\u00a0must have been changed since you leave\\\"}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(html);\n\t\tassertEquals(\"Something\\\\u00a0must\\\\u00a0have\\\\u00a0been\\\\u00a0changed\\\\u00a0since\\\\u00a0you\\\\u00a0leave\", jsonObject.getStrEscaped(\"name\"));\n\t}\n\n\t@Test\n\tpublic void parseFromXmlTest() {\n\t\tfinal String s = \"<sfzh>640102197312070614</sfzh><sfz>640102197312070614X</sfz><name>aa</name><gender>1</gender>\";\n\t\tfinal JSONObject json = JSONUtil.parseFromXml(s);\n\t\tassertEquals(640102197312070614L, json.get(\"sfzh\"));\n\t\tassertEquals(\"640102197312070614X\", json.get(\"sfz\"));\n\t\tassertEquals(\"aa\", json.get(\"name\"));\n\t\tassertEquals(1, json.get(\"gender\"));\n\t}\n\n\t@Test\n\tpublic void doubleTest() {\n\t\tfinal String json = \"{\\\"test\\\": 12.00}\";\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(json);\n\t\t//noinspection BigDecimalMethodWithoutRoundingCalled\n\t\tassertEquals(\"12.00\", jsonObject.getBigDecimal(\"test\").setScale(2).toString());\n\t}\n\n\t@Test\n\tpublic void customValueTest() {\n\t\tfinal JSONObject jsonObject = JSONUtil.createObj()\n\t\t\t.set(\"test2\", (JSONString) () -> NumberUtil.decimalFormat(\"#.0\", 12.00D));\n\n\t\tassertEquals(\"{\\\"test2\\\":12.0}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void setStripTrailingZerosTest() {\n\t\t// 默认去除多余的0\n\t\tfinal JSONObject jsonObjectDefault = JSONUtil.createObj()\n\t\t\t.set(\"test2\", 12.00D);\n\t\tassertEquals(\"{\\\"test2\\\":12}\", jsonObjectDefault.toString());\n\n\t\t// 不去除多余的0\n\t\tfinal JSONObject jsonObject = JSONUtil.createObj(JSONConfig.create().setStripTrailingZeros(false))\n\t\t\t.set(\"test2\", 12.00D);\n\t\tassertEquals(\"{\\\"test2\\\":12.0}\", jsonObject.toString());\n\n\t\t// 去除多余的0\n\t\tjsonObject.getConfig().setStripTrailingZeros(true);\n\t\tassertEquals(\"{\\\"test2\\\":12}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void parseObjTest() {\n\t\t// 测试转义\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(\"{\\n\" +\n\t\t\t\"    \\\"test\\\": \\\"\\\\\\\\地库地库\\\",\\n\" +\n\t\t\t\"}\");\n\n\t\tassertEquals(\"\\\\地库地库\", jsonObject.getObj(\"test\"));\n\t}\n\n\t@Test\n\tpublic void sqlExceptionTest() {\n\t\t//https://github.com/chinabugotech/hutool/issues/1399\n\t\t// SQLException实现了Iterable接口，默认是遍历之，会栈溢出，修正后只返回string\n\t\tfinal JSONObject set = JSONUtil.createObj().set(\"test\", new SQLException(\"test\"));\n\t\tassertEquals(\"{\\\"test\\\":\\\"java.sql.SQLException: test\\\"}\", set.toString());\n\t}\n\n\t@Test\n\tpublic void parseBigNumberTest() {\n\t\t// 科学计数法使用BigDecimal处理，默认输出非科学计数形式\n\t\tfinal String str = \"{\\\"test\\\":100000054128897953e4}\";\n\t\tassertEquals(\"{\\\"test\\\":1000000541288979530000}\", JSONUtil.parseObj(str).toString());\n\t}\n\n\t@Test\n\tpublic void toXmlTest() {\n\t\tfinal JSONObject obj = JSONUtil.createObj();\n\t\tobj.set(\"key1\", \"v1\")\n\t\t\t.set(\"key2\", ListUtil.of(\"a\", \"b\", \"c\"));\n\t\tfinal String xmlStr = JSONUtil.toXmlStr(obj);\n\t\tassertEquals(\"<key1>v1</key1><key2>a</key2><key2>b</key2><key2>c</key2>\", xmlStr);\n\t}\n\n\t@Test\n\tpublic void duplicateKeyFalseTest() {\n\t\tfinal String str = \"{id:123, name:\\\"张三\\\", name:\\\"李四\\\"}\";\n\n\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(str, JSONConfig.create().setCheckDuplicate(false));\n\t\tassertEquals(\"{\\\"id\\\":123,\\\"name\\\":\\\"李四\\\"}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void duplicateKeyTrueTest() {\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tfinal String str = \"{id:123, name:\\\"张三\\\", name:\\\"李四\\\"}\";\n\n\t\t\tfinal JSONObject jsonObject = JSONUtil.parseObj(str, JSONConfig.create().setCheckDuplicate(true));\n\t\t\tassertEquals(\"{\\\"id\\\":123,\\\"name\\\":\\\"李四\\\"}\", jsonObject.toString());\n\t\t});\n\t}\n\n\t/**\n\t * 测试普通数组转JSONArray时是否异常, 尤其是byte[]数组, 可能是普通的byte[]数组, 也可能是二进制流\n\t */\n\t@Test\n\tpublic void testArrayEntity() {\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(new ArrayEntity());\n\t\tassertEquals(\"{\\\"a\\\":[],\\\"b\\\":[0],\\\"c\\\":[],\\\"d\\\":[],\\\"e\\\":[]}\", jsonStr);\n\t}\n\n\t@Data\n\tstatic class ArrayEntity {\n\t\tprivate byte[] a = new byte[0];\n\t\tprivate byte[] b = new byte[1];\n\t\tprivate int[] c = new int[0];\n\t\tprivate Byte[] d = new Byte[0];\n\t\tprivate Byte[] e = new Byte[1];\n\t}\n\n\t@Test\n\tpublic void issue3540Test() {\n\t\tLong userId = 10101010L;\n\t\tfinal String jsonStr = JSONUtil.toJsonStr(userId);\n\t\tassertEquals(\"10101010\", jsonStr);\n\t}\n\n\t/**\n\t * 类型引用数组泛型丢失\n\t */\n\t@Test\n\tpublic void issue3873Test() {\n\t\tString json = \"{\\\"results\\\":[{\\\"uid\\\":\\\"1\\\"}],\\\"offset\\\":0,\\\"limit\\\":20,\\\"total\\\":0}\";\n\t\tResults<Index> deserialize = JSONUtil.toBean(json, (new TypeReference<Results<Index>>() {\n\t\t}), false);\n\n\t\tassertEquals(Results.class, deserialize.getClass());\n\t\tassertEquals(ArrayUtil.getArrayType(Index.class), deserialize.results.getClass());\n\t}\n\n\t@Data\n\tpublic static class Results<T> {\n\t\tpublic T[] results;\n\t}\n\n\t@Data\n\tpublic static class Index {\n\t\tpublic String uid;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/ParseBeanTest.java",
    "content": "package cn.hutool.json;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.collection.CollUtil;\n\n/**\n * 测试Bean中嵌套List等对象时是否完整转换<br>\n * 同时测试私有class是否可以有效实例化\n *\n * @author looly\n *\n */\npublic class ParseBeanTest {\n\n\t@Test\n\tpublic void parseBeanTest() {\n\n\t\tC c1 = new C();\n\t\tc1.setTest(\"test1\");\n\t\tC c2 = new C();\n\t\tc2.setTest(\"test2\");\n\n\t\tB b1 = new B();\n\t\tb1.setCs(CollUtil.newArrayList(c1, c2));\n\t\tB b2 = new B();\n\t\tb2.setCs(CollUtil.newArrayList(c1, c2));\n\n\t\tA a = new A();\n\t\ta.setBs(CollUtil.newArrayList(b1, b2));\n\n\t\tJSONObject json = JSONUtil.parseObj(a);\n\t\tA a1 = JSONUtil.toBean(json, A.class);\n\t\tassertEquals(json.toString(), JSONUtil.toJsonStr(a1));\n\t}\n\n}\n\nclass A {\n\n\tprivate List<B> bs;\n\n\tpublic List<B> getBs() {\n\t\treturn bs;\n\t}\n\n\tpublic void setBs(List<B> bs) {\n\t\tthis.bs = bs;\n\t}\n}\n\nclass B {\n\n\tprivate List<C> cs;\n\n\tpublic List<C> getCs() {\n\t\treturn cs;\n\t}\n\n\tpublic void setCs(List<C> cs) {\n\t\tthis.cs = cs;\n\t}\n}\n\nclass C {\n\tprivate String test;\n\n\tpublic String getTest() {\n\t\treturn test;\n\t}\n\n\tpublic void setTest(String test) {\n\t\tthis.test = test;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Pr1431Test.java",
    "content": "package cn.hutool.json;\r\n\r\nimport lombok.Data;\r\nimport org.junit.jupiter.api.Test;\r\n\r\nimport static org.junit.jupiter.api.Assertions.assertEquals;\r\nimport static org.junit.jupiter.api.Assertions.assertTrue;\r\n\r\npublic class Pr1431Test {\r\n\t@SuppressWarnings(\"MismatchedQueryAndUpdateOfCollection\")\r\n\t@Test\r\n\tvoid filterTest() {\r\n\t\tUserC user = new UserC();\r\n\t\tuser.setId(1);\r\n\t\tuser.setName(\"张三\");\r\n\t\tuser.setProp(\"123456\");\r\n\r\n\t\tfinal JSONObject entries = new JSONObject(user, JSONConfig.create(), pair -> !\"prop\".equals(pair.getKey()));\r\n\t\tassertEquals(2, entries.size());\r\n\t\tassertTrue(entries.containsKey(\"id\"));\r\n\t\tassertTrue(entries.containsKey(\"name\"));\r\n\t}\r\n\r\n\t@Data\r\n\tstatic class UserC {\r\n\t\tprivate int id;\r\n\t\tprivate String name;\r\n\t\tprivate String prop;\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/Pr192Test.java",
    "content": "package cn.hutool.json;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.util.List;\n\npublic class Pr192Test {\n\n\t@Test\n\tpublic void toBeanTest3() {\n\t\t//\t\t测试数字类型精度丢失的情况\n\t\tString number = \"1234.123456789123456\";\n\t\tString jsonString = \"{\\\"create\\\":{\\\"details\\\":[{\\\"price\\\":\" + number + \"}]}}\";\n\t\tWebCreate create = JSONUtil.toBean(jsonString, WebCreate.class);\n\t\tassertEquals(number,create.getCreate().getDetails().get(0).getPrice().toString());\n\t}\n\n\tstatic class WebCreate {\n\t\tprivate Create create;\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"WebCreate{\" +\n\t\t\t\t\t\"create=\" + create +\n\t\t\t\t\t'}';\n\t\t}\n\n\t\tpublic void setCreate(Create create) {\n\t\t\tthis.create = create;\n\t\t}\n\n\t\tpublic Create getCreate() {\n\t\t\treturn create;\n\t\t}\n\t}\n\n\tstatic class Create {\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"Create{\" +\n\t\t\t\t\t\"details=\" + details +\n\t\t\t\t\t'}';\n\t\t}\n\n\t\tprivate List<Detail> details;\n\n\t\tpublic void setDetails(List<Detail> details) {\n\t\t\tthis.details = details;\n\t\t}\n\n\t\tpublic List<Detail> getDetails() {\n\t\t\treturn details;\n\t\t}\n\t}\n\n\tstatic class Detail {\n\t\tprivate BigDecimal price;\n\n\t\tpublic void setPrice(BigDecimal price) {\n\t\t\tthis.price = price;\n\t\t}\n\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"Detail{\" +\n\t\t\t\t\t\"price=\" + price +\n\t\t\t\t\t'}';\n\t\t}\n\n\t\tpublic BigDecimal getPrice() {\n\t\t\treturn price;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/TransientTest.java",
    "content": "package cn.hutool.json;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class TransientTest {\n\n\t@Data\n\tstatic class Bill{\n\t\tprivate transient String id;\n\t\tprivate String bizNo;\n\t}\n\n\t@Test\n\tpublic void beanWithoutTransientTest(){\n\t\tBill detailBill = new Bill();\n\t\tdetailBill.setId(\"3243\");\n\t\tdetailBill.setBizNo(\"bizNo\");\n\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject jsonObject = new JSONObject(detailBill,\n\t\t\t\tJSONConfig.create().setTransientSupport(false));\n\t\tassertEquals(\"{\\\"id\\\":\\\"3243\\\",\\\"bizNo\\\":\\\"bizNo\\\"}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void beanWithTransientTest(){\n\t\tBill detailBill = new Bill();\n\t\tdetailBill.setId(\"3243\");\n\t\tdetailBill.setBizNo(\"bizNo\");\n\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal JSONObject jsonObject = new JSONObject(detailBill,\n\t\t\t\tJSONConfig.create().setTransientSupport(true));\n\t\tassertEquals(\"{\\\"bizNo\\\":\\\"bizNo\\\"}\", jsonObject.toString());\n\t}\n\n\t@Test\n\tpublic void beanWithoutTransientToBeanTest(){\n\t\tBill detailBill = new Bill();\n\t\tdetailBill.setId(\"3243\");\n\t\tdetailBill.setBizNo(\"bizNo\");\n\n\t\tfinal JSONObject jsonObject = new JSONObject(detailBill,\n\t\t\t\tJSONConfig.create().setTransientSupport(false));\n\n\t\tfinal Bill bill = jsonObject.toBean(Bill.class);\n\t\tassertEquals(\"3243\", bill.getId());\n\t\tassertEquals(\"bizNo\", bill.getBizNo());\n\t}\n\n\t@Test\n\tpublic void beanWithTransientToBeanTest(){\n\t\tBill detailBill = new Bill();\n\t\tdetailBill.setId(\"3243\");\n\t\tdetailBill.setBizNo(\"bizNo\");\n\n\t\tfinal JSONObject jsonObject = new JSONObject(detailBill,\n\t\t\t\tJSONConfig.create().setTransientSupport(true));\n\n\t\tfinal Bill bill = jsonObject.toBean(Bill.class);\n\t\tassertNull(bill.getId());\n\t\tassertEquals(\"bizNo\", bill.getBizNo());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/issueIVMD5/BaseResult.java",
    "content": "package cn.hutool.json.issueIVMD5;\n\nimport java.util.List;\n\nimport lombok.Data;\n\n@Data\npublic class BaseResult<E> {\n\t\n\tpublic BaseResult() {\n\t}\n\t\n\tprivate int result;\n\tprivate List<E> data;\n\tprivate E data2;\n\tprivate String nextDataUri;\n\tprivate String message;\n\tprivate int dataCount;\n\t\n}"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/issueIVMD5/IssueIVMD5Test.java",
    "content": "package cn.hutool.json.issueIVMD5;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.TypeReference;\nimport cn.hutool.json.JSONUtil;\n\npublic class IssueIVMD5Test {\n\n\t/**\n\t * 测试泛型对象中有泛型字段的转换成功与否\n\t */\n\t@Test\n\tpublic void toBeanTest() {\n\t\tString jsonStr = ResourceUtil.readUtf8Str(\"issueIVMD5.json\");\n\n\t\tTypeReference<BaseResult<StudentInfo>> typeReference = new TypeReference<BaseResult<StudentInfo>>() {};\n\t\tBaseResult<StudentInfo> bean = JSONUtil.toBean(jsonStr, typeReference.getType(), false);\n\n\t\tStudentInfo data2 = bean.getData2();\n\t\tassertEquals(\"B4DDF491FDF34074AE7A819E1341CB6C\", data2.getAccountId());\n\t}\n\n\t/**\n\t * 测试泛型对象中有包含泛型字段的类型的转换成功与否，比如List&lt;T&gt; list\n\t */\n\t@Test\n\tpublic void toBeanTest2() {\n\t\tString jsonStr = ResourceUtil.readUtf8Str(\"issueIVMD5.json\");\n\n\t\tTypeReference<BaseResult<StudentInfo>> typeReference = new TypeReference<BaseResult<StudentInfo>>() {};\n\t\tBaseResult<StudentInfo> bean = JSONUtil.toBean(jsonStr, typeReference.getType(), false);\n\n\t\tList<StudentInfo> data = bean.getData();\n\t\tStudentInfo studentInfo = data.get(0);\n\t\tassertEquals(\"B4DDF491FDF34074AE7A819E1341CB6C\", studentInfo.getAccountId());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/issueIVMD5/StudentInfo.java",
    "content": "package cn.hutool.json.issueIVMD5;\n\nimport lombok.Data;\n\n@Data\npublic class StudentInfo {\n\tprivate String birthday;\n\tprivate String linkPhone;\n\tprivate String nation;\n\tprivate String studentCode;\n\tprivate String unitiveCode;\n\tprivate String sex;\n\tprivate String linkAddress;\n\tprivate String identityCard;\n\tprivate String accountId;\n\tprivate String classId;\n\tprivate String password;\n\tprivate String modifyTime;\n\tprivate String isDeleted;\n\tprivate String postalcode;\n\tprivate String background;\n\tprivate String schoolId;\n\tprivate String studentName;\n\tprivate String sequenceIntId;\n\tprivate String nativePlace;\n\tprivate String id;\n\tprivate String username;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/ADT.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.List;\n\n@Data\npublic class ADT {\n\tprivate List<String> BookingCode;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/Data.java",
    "content": "package cn.hutool.json.test.bean;\n\n@lombok.Data\npublic class Data {\n\tprivate Price Price;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/Exam.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\n@Data\npublic class Exam {\n\tprivate String id;\n\tprivate String examNumber;\n\tprivate String isAnswer;\n\tprivate Seq[] answerArray;\n\tprivate String isRight;\n\tprivate String isSubject;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/ExamInfoDict.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n *\n * @author 质量过关\n *\n */\n@Data\npublic class ExamInfoDict implements Serializable {\n\tprivate static final long serialVersionUID = 3640936499125004525L;\n\n\t// 主键\n\tprivate Integer id; // 可当作题号\n\t// 试题类型 客观题 0主观题 1\n\tprivate Integer examType;\n\t// 试题是否作答\n\tprivate Integer answerIs;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/JSONBean.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport cn.hutool.json.JSONObject;\nimport lombok.Data;\n\n@Data\npublic class JSONBean {\n\tprivate int code;\n\tprivate JSONObject data;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/JsonNode.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n@Data\npublic class JsonNode implements Serializable {\n\tprivate static final long serialVersionUID = -2280206942803550272L;\n\n\tprivate Long id;\n\tprivate Integer parentId;\n\tprivate String name;\n\n\tpublic JsonNode() {\n\t}\n\n\tpublic JsonNode(Long id, Integer parentId, String name) {\n\t\tthis.id = id;\n\t\tthis.parentId = parentId;\n\t\tthis.name = name;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/KeyBean.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\n@Data\npublic class KeyBean{\n\tprivate String akey;\n\tprivate String bkey;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/PerfectEvaluationProductResVo.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * @author wangyan E-mail:wangyan@pospt.cn\n * @version 创建时间：2017年9月13日 下午5:16:32\n * 类说明\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class PerfectEvaluationProductResVo extends ProductResBase {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic static final Map<String, String> KEY_TO_KEY = new HashMap<String, String>() {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t{\n\t\t\tput(\"HA001\", \"CDCA005\");\n\t\t\tput(\"HA002\", \"CDCA002\");\n\t\t\tput(\"HA003\", \"CDCA004\");\n\t\t\tput(\"HA004\", \"CSSP002\");\n\t\t\tput(\"HB001\", \"CSWC001\");\n\t\t\tput(\"HB002\", \"CSRL001\");\n\t\t\tput(\"HB003\", \"CSRL003\");\n\t\t\tput(\"HC005\", \"CDTB016\");\n\t\t\tput(\"HC006\", \"CDTB020\");\n\t\t\tput(\"HC007\", \"CDTB021\");\n\t\t\tput(\"HC008\", \"CDTB022\");\n\t\t\tput(\"HC009\", \"CDTB023\");\n\t\t\tput(\"HC010\", \"CDTB024\");\n\t\t\tput(\"HC011\", \"CDTB001\");\n\t\t\tput(\"HC012\", \"CDTB005\");\n\t\t\tput(\"HC013\", \"CDTB006\");\n\t\t\tput(\"HC014\", \"CDTB007\");\n\t\t\tput(\"HC015\", \"CDTB008\");\n\t\t\tput(\"HC016\", \"CDTB009\");\n\t\t\tput(\"HC001\", \"CDTB018\");\n\t\t\tput(\"HC002\", \"CDTB003\");\n\t\t\tput(\"HC003\", \"CDTB063\");\n\t\t\tput(\"HC004\", \"CDTB060\");\n\t\t\tput(\"HD001\", \"CDMC005\");\n\t\t\tput(\"HD007\", \"CDMC082\");\n\t\t\tput(\"HD010\", \"CDMC164\");\n\t\t\tput(\"HD016\", \"CSSP003\");\n\t\t\tput(\"HD017\", \"CSSS003\");\n\t\t\tput(\"HE001\", \"CDTT028\");\n\t\t\tput(\"HE002\", \"CDTT029\");\n\t\t\tput(\"HE003\", \"CDTT030\");\n\t\t\tput(\"HE004\", \"CDTT031\");\n\t\t\tput(\"HE005\", \"CDTT032\");\n\t\t\tput(\"HE006\", \"CDTT033\");\n\t\t\tput(\"HE007\", \"CDTT015\");\n\t\t\tput(\"HE008\", \"CDTT016\");\n\t\t\tput(\"HE009\", \"CDTT017\");\n\t\t\tput(\"HE010\", \"CDTT018\");\n\t\t\tput(\"HE011\", \"CDTT019\");\n\t\t\tput(\"HE012\", \"CDTT020\");\n\t\t\tput(\"HE013\", \"CDTT055\");\n\t\t\tput(\"HE014\", \"CDTT056\");\n\t\t\tput(\"HE015\", \"CDTT057\");\n\t\t\tput(\"HE016\", \"CDTT058\");\n\t\t\tput(\"HE017\", \"CDTT059\");\n\t\t\tput(\"HE018\", \"CDTT060\");\n\t\t\tput(\"HE019\", \"CDTT043\");\n\t\t\tput(\"HE020\", \"CDTT044\");\n\t\t\tput(\"HE021\", \"CDTT045\");\n\t\t\tput(\"HE022\", \"CDTT046\");\n\t\t\tput(\"HE023\", \"CDTT047\");\n\t\t\tput(\"HE024\", \"CDTT048\");\n\t\t\tput(\"HE025\", \"CDTT080\");\n\t\t\tput(\"HE026\", \"CDTT081\");\n\t\t\tput(\"HE027\", \"CDTT082\");\n\t\t\tput(\"HE028\", \"CDTT083\");\n\t\t\tput(\"HE029\", \"CDTT084\");\n\t\t\tput(\"HE030\", \"CDTT085\");\n\t\t\tput(\"HE031\", \"CDTT067\");\n\t\t\tput(\"HE032\", \"CDTT068\");\n\t\t\tput(\"HE033\", \"CDTT069\");\n\t\t\tput(\"HE034\", \"CDTT070\");\n\t\t\tput(\"HE035\", \"CDTT071\");\n\t\t\tput(\"HE036\", \"CDTT072\");\n\t\t\tput(\"HF001\", \"CDTB272\");\n\t\t\tput(\"HF002\", \"CDTB273\");\n\t\t\tput(\"HF003\", \"CDTB277\");\n\t\t\tput(\"HF004\", \"CDTB278\");\n\t\t\tput(\"HF005\", \"CDTB282\");\n\t\t\tput(\"HF006\", \"CDTB283\");\n\t\t\tput(\"HF007\", \"CDTC058\");\n\t\t\tput(\"HF008\", \"CDTC059\");\n\t\t\tput(\"HF009\", \"CDTC060\");\n\t\t\tput(\"HF010\", \"CDTC014\");\n\t\t\tput(\"HG001\", \"CDMC294\");\n\t\t\tput(\"HG002\", \"CDMC293\");\n\t\t\tput(\"HG003\", \"CDMC301\");\n\t\t\tput(\"HG004\", \"CDMC300\");\n\t\t\tput(\"HG005\", \"CDMC308\");\n\t\t\tput(\"HG006\", \"CDMC307\");\n\t\t\tput(\"HG007\", \"CDMC315\");\n\t\t\tput(\"HG008\", \"CDMC314\");\n\t\t\tput(\"HG009\", \"CDMC322\");\n\t\t\tput(\"HG010\", \"CDMC321\");\n\t\t\tput(\"HG011\", \"CDMC287\");\n\t\t\tput(\"HH001\", \"CDTT001\");\n\t\t\tput(\"HH002\", \"CDTT002\");\n\t\t\tput(\"HH003\", \"CDTT003\");\n\t\t\tput(\"HH004\", \"CDTT004\");\n\t\t\tput(\"HH005\", \"CDTT005\");\n\t\t\tput(\"HH006\", \"CDTT006\");\n\t\t\tput(\"HH007\", \"CDTT007\");\n\t\t\tput(\"HH008\", \"CDTT008\");\n\t\t\tput(\"HH009\", \"CDTT009\");\n\t\t\tput(\"HH010\", \"CDTT010\");\n\t\t\tput(\"HH011\", \"CDTT011\");\n\t\t\tput(\"HH012\", \"CDTT012\");\n\t\t\tput(\"HH013\", \"CDTT013\");\n\t\t\tput(\"HH014\", \"CDTT014\");\n\t\t}\n\t};\n\n\tprivate String XT_NO;\n\tprivate String CARD_HOLDER;\n\tprivate String CARD_NO;\n\tprivate String HA001;\n\tprivate String HA002;\n\tprivate String HA003;\n\tprivate String HA004;\n\tprivate String HB001;\n\tprivate String HB002;\n\tprivate String HB003;\n\tprivate String HC005;\n\tprivate String HC006;\n\tprivate String HC007;\n\tprivate String HC008;\n\tprivate String HC009;\n\tprivate String HC010;\n\tprivate String HC011;\n\tprivate String HC012;\n\tprivate String HC013;\n\tprivate String HC014;\n\tprivate String HC015;\n\tprivate String HC016;\n\tprivate String HC001;\n\tprivate String HC002;\n\tprivate String HC003;\n\tprivate String HC004;\n\tprivate String HD001;\n\tprivate String HD002;\n\tprivate String HD004;\n\tprivate String HD005;\n\tprivate String HD007;\n\tprivate String HD008;\n\tprivate String HD010;\n\tprivate String HD011;\n\tprivate String HD013;\n\tprivate String HD014;\n\tprivate String HD016;\n\tprivate String HD017;\n\tprivate String HE001;\n\tprivate String HE002;\n\tprivate String HE003;\n\tprivate String HE004;\n\tprivate String HE005;\n\tprivate String HE006;\n\tprivate String HE007;\n\tprivate String HE008;\n\tprivate String HE009;\n\tprivate String HE010;\n\tprivate String HE011;\n\tprivate String HE012;\n\tprivate String HE013;\n\tprivate String HE014;\n\tprivate String HE015;\n\tprivate String HE016;\n\tprivate String HE017;\n\tprivate String HE018;\n\tprivate String HE019;\n\tprivate String HE020;\n\tprivate String HE021;\n\tprivate String HE022;\n\tprivate String HE023;\n\tprivate String HE024;\n\tprivate String HE025;\n\tprivate String HE026;\n\tprivate String HE027;\n\tprivate String HE028;\n\tprivate String HE029;\n\tprivate String HE030;\n\tprivate String HE031;\n\tprivate String HE032;\n\tprivate String HE033;\n\tprivate String HE034;\n\tprivate String HE035;\n\tprivate String HE036;\n\tprivate String HF001;\n\tprivate String HF002;\n\tprivate String HF003;\n\tprivate String HF004;\n\tprivate String HF005;\n\tprivate String HF006;\n\tprivate String HF007;\n\tprivate String HF008;\n\tprivate String HF009;\n\tprivate String HF010;\n\tprivate String HG001;\n\tprivate String HG002;\n\tprivate String HG003;\n\tprivate String HG004;\n\tprivate String HG005;\n\tprivate String HG006;\n\tprivate String HG007;\n\tprivate String HG008;\n\tprivate String HG009;\n\tprivate String HG010;\n\tprivate String HG011;\n\tprivate String HG012;\n\tprivate String HG013;\n\tprivate String HG014;\n\tprivate String HG015;\n\tprivate String HG016;\n\tprivate String HH001;\n\tprivate String HH002;\n\tprivate String HH003;\n\tprivate String HH004;\n\tprivate String HH005;\n\tprivate String HH006;\n\tprivate String HH007;\n\tprivate String HH008;\n\tprivate String HH009;\n\tprivate String HH010;\n\tprivate String HH011;\n\tprivate String HH012;\n\tprivate String HH013;\n\tprivate String HH014;\n\tprivate String XT_MONTH01;\n\tprivate String XT_MONTH02;\n\tprivate String XT_MONTH03;\n\tprivate String XT_MONTH04;\n\tprivate String XT_MONTH05;\n\tprivate String XT_MONTH06;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/Price.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.List;\n\n@Data\npublic class Price {\n\tprivate List<List<ADT>> ADT;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/ProductResBase.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * @author wangyan E-mail:wangyan@pospt.cn\n * @version 创建时间：2017年9月11日 上午9:33:01 类说明\n */\n@Data\npublic class ProductResBase implements Serializable {\n\tprivate static final long serialVersionUID = -6708040074002451511L;\n\t/**\n\t * 请求结果成功0\n\t */\n\tpublic static final int REQUEST_RESULT_SUCCESS = 0;\n\t/**\n\t * 请求结果失败 1\n\t */\n\tpublic static final int REQUEST_RESULT_FIAL = 1;\n\t/**\n\t * 成功code\n\t */\n\tpublic static final String REQUEST_CODE_SUCCESS = \"0000\";\n\n\t/**\n\t * 结果 成功0 失败1\n\t */\n\tprivate int resResult = 0;\n\tprivate String resCode = \"0000\";\n\tprivate String resMsg = \"success\";\n\n\t/**\n\t * 成本总计\n\t */\n\tprivate Integer costTotal;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/ResultBean.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.List;\n\n@Data\npublic class ResultBean {\n\tpublic List<List<List<ItemsBean>>> items;\n\n\t@Data\n\tpublic static class ItemsBean {\n\t\tpublic DetailBean detail;\n\n\t\t@Data\n\t\tpublic static class DetailBean {\n\t\t\tpublic String visitorStatus;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/ResultDto.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n@Data\npublic class ResultDto<T> implements Serializable {\n\tprivate static final long serialVersionUID = -1417999729205654379L;\n\n\t/**\n\t * 成功码.\n\t */\n\tpublic static final int SUCCESS_CODE = 200;\n\n\t/**\n\t * 成功信息.\n\t */\n\tpublic static final String SUCCESS_MESSAGE = \"操作成功\";\n\n\t/**\n\t * 错误码.\n\t */\n\tpublic static final int ERROR_CODE = 500;\n\n\t/**\n\t * 错误信息.\n\t */\n\tpublic static final String ERROR_MESSAGE = \"内部异常\";\n\n\t/**\n\t * 错误码：参数非法\n\t */\n\tpublic static final int ILLEGAL_ARGUMENT_CODE_ = 100;\n\n\t/**\n\t * 错误信息：参数非法\n\t */\n\tpublic static final String ILLEGAL_ARGUMENT_MESSAGE = \"参数非法\";\n\n\t/**\n\t * 编号.\n\t */\n\tprivate int code;\n\n\t/**\n\t * 信息.\n\t */\n\tprivate String message;\n\n\t/**\n\t * 结果数据\n\t */\n\tprivate T result;\n\n\t/**\n\t * Instantiates a new wrapper. default code=200\n\t */\n\tpublic ResultDto() {\n\t\tthis(SUCCESS_CODE, SUCCESS_MESSAGE);\n\t}\n\n\t/**\n\t * Instantiates a new wrapper.\n\t *\n\t * @param code the code\n\t * @param message the message\n\t */\n\tpublic ResultDto(int code, String message) {\n\t\tthis(code, message, null);\n\t}\n\n\t/**\n\t * Instantiates a new wrapper.\n\t *\n\t * @param code the code\n\t * @param message the message\n\t * @param result the result\n\t */\n\tpublic ResultDto(int code, String message, T result) {\n\t\tthis.code(code).message(message).result(result);\n\t}\n\n\t/**\n\t * Sets the 编号 , 返回自身的引用.\n\t *\n\t * @param code the new 编号\n\t * @return the wrapper\n\t */\n\tprivate ResultDto<T> code(int code) {\n\t\tthis.setCode(code);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the 信息 , 返回自身的引用.\n\t *\n\t * @param message the new 信息\n\t * @return the wrapper\n\t */\n\tprivate ResultDto<T> message(String message) {\n\t\tthis.setMessage(message);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the 结果数据 , 返回自身的引用.\n\t *\n\t * @param result the new 结果数据\n\t * @return the wrapper\n\t */\n\tpublic ResultDto<T> result(T result) {\n\t\tthis.setResult(result);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 判断是否成功： 依据 ResultDto.SUCCESS_CODE == this.code\n\t *\n\t * @return code =200,true;否则 false.\n\t */\n\tpublic boolean success() {\n\t\treturn ResultDto.SUCCESS_CODE == this.code;\n\t}\n\n\t/**\n\t * 判断是否成功： 依据 ResultDto.SUCCESS_CODE != this.code\n\t *\n\t * @return code !=200,true;否则 false.\n\t */\n\tpublic boolean error() {\n\t\treturn !success();\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/Seq.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\n@Data\npublic class Seq {\n\tprivate String seq;\n\n\tpublic Seq() {\n\t}\n\n\tpublic Seq(String seq) {\n\t\tthis.seq = seq;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthResponse.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\n@Data\npublic class TokenAuthResponse {\n\tprivate String token;\n\tprivate String userId;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Getter\n@Setter\npublic class TokenAuthWarp extends UUMap<TokenAuthResponse> {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate String targetUrl;\n\tprivate String success;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp2.java",
    "content": "package cn.hutool.json.test.bean;\n\npublic class TokenAuthWarp2 extends TokenAuthWarp {\n\tprivate static final long serialVersionUID = 1L;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UUMap.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n@Data\npublic class UUMap<T> implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate T result;\n\n\tpublic static long getSerialversionuid() {\n\t\treturn serialVersionUID;\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserA.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.Date;\nimport java.util.List;\n\n@Data\npublic class UserA {\n\tprivate String name;\n\tprivate String a;\n\tprivate Date date;\n\tprivate List<Seq> sqs;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserB.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.Date;\n\n@Data\npublic class UserB {\n\tprivate String name;\n\tprivate String b;\n\tprivate Date date;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserC.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\n@Data\npublic class UserC {\n\tprivate Integer id;\n\tprivate String name;\n\tprivate String prop;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoDict.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 用户信息\n * @author 质量过关\n *\n */\n@Data\npublic class UserInfoDict implements Serializable {\n\tprivate static final long serialVersionUID = -936213991463284306L;\n\t// 用户Id\n\tprivate Integer id;\n\t// 要展示的名字\n\tprivate String realName;\n\t// 头像地址\n\tprivate String photoPath;\n\tprivate List<ExamInfoDict> examInfoDict;\n\tprivate UserInfoRedundCount userInfoRedundCount;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoRedundCount.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n@Data\npublic class UserInfoRedundCount implements Serializable {\n\tprivate static final long serialVersionUID = -8397291070139255181L;\n\n\tprivate String finishedRatio; // 完成率\n\tprivate Integer ownershipExamCount; // 自己有多少道题\n\tprivate Integer answeredExamCount; // 当前回答了多少道题\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/UserWithMap.java",
    "content": "package cn.hutool.json.test.bean;\n\nimport lombok.Data;\n\nimport java.util.Map;\n\n@Data\npublic class UserWithMap {\n\tprivate Map<String, String> data;\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/report/CaseReport.java",
    "content": "package cn.hutool.json.test.bean.report;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 测试用例报告\n * @author xuwangcheng\n * @version 20181012\n *\n */\npublic class CaseReport {\n\t\n\t/**\n\t * 包含的测试步骤报告\n\t */\n\tprivate List<StepReport> stepReports = new ArrayList<>();\n\t\n\tpublic List<StepReport> getStepReports() {\n\t\treturn stepReports;\n\t}\n\n\tpublic void setStepReports(List<StepReport> stepReports) {\n\t\tthis.stepReports = stepReports;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CaseReport [stepReports=\" + stepReports + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/report/EnvSettingInfo.java",
    "content": "package cn.hutool.json.test.bean.report;\n\nimport java.util.Collection;\n\n/**\n * 测试环境信息\n * @author xuwangcheng\n * @version 20181012\n *\n */\npublic class EnvSettingInfo {\n\t\n\tpublic static boolean DEV_MODE = true;\n\t\n\tprivate boolean remoteMode;\n\t\n\tprivate String hubRemoteUrl;\n\t\n\tprivate String reportFolder = \"/report\";\n\tprivate String screenshotFolder = \"/screenshot\";\n\n\tprivate String elementFolder = \"/config/element/\";\n\tprivate String suiteFolder = \"/config/suite/\";\n\t\n\tprivate String chromeDriverPath = \"/src/main/resources/chromedriver.exe\";\n\tprivate String ieDriverPath = \"/src/main/resources/IEDriverServer.exe\";\n\tprivate String operaDriverPath = \"/src/main/resources/operadriver.exe\";\n\tprivate String firefoxDriverPath = \"/src/main/resources/geckodriver.exe\";\n\t\n\tprivate Double defaultSleepSeconds;\n\t\n\tprivate Integer elementLocationRetryCount;\n\tprivate Double elementLocationTimeouts;\n\t\n\t/**\n\t * 收件人列表\n\t */\n\tprivate Collection<String> tos;\n\t/**\n\t * 抄送人列表\n\t */\n\tprivate Collection<String> ccs;\n\t/**\n\t * 密送人列表\n\t */\n\tprivate Collection<String> bccs;\n\t\n\t/**\n\t * 是否可以开启定时任务\n\t */\n\tprivate boolean cronEnabled = false;\n\t\n\t/**\n\t * 定时执行：suite文件\n\t */\n\tprivate String cronSuite;\n\t\n\t/**\n\t * 定时执行：cron表达式，支持linux crontab格式(5位)和Quartz的cron格式(6位)\n\t */\n\tprivate String cronExpression;\n\t\n\t/**\n\t * 存储测试报告数据的轻量级数据库，路径\n\t */\n\tprivate String sqlitePath;\n\t\n\tpublic EnvSettingInfo() {\n\t}\n\n\tpublic void setSqlitePath(String sqlitePath) {\n\t\tthis.sqlitePath = sqlitePath;\n\t}\n\t\n\tpublic String getSqlitePath() {\n\t\treturn sqlitePath;\n\t}\n\t\n\tpublic void setCronEnabled(boolean cronEnabled) {\n\t\tthis.cronEnabled = cronEnabled;\n\t}\n\t\n\tpublic boolean isCronEnabled() {\n\t\treturn cronEnabled;\n\t}\n\t\n\tpublic String getCronSuite() {\n\t\treturn cronSuite;\n\t}\n\n\tpublic void setCronSuite(String cronSuite) {\n\t\tthis.cronSuite = cronSuite;\n\t}\n\n\tpublic String getCronExpression() {\n\t\treturn cronExpression;\n\t}\n\n\tpublic void setCronExpression(String cronExpression) {\n\t\tthis.cronExpression = cronExpression;\n\t}\n\n\tpublic Integer getElementLocationRetryCount() {\n\t\treturn elementLocationRetryCount;\n\t}\n\n\tpublic void setElementLocationRetryCount(Integer elementLocationRetryCount) {\n\t\tthis.elementLocationRetryCount = elementLocationRetryCount;\n\t}\n\n\tpublic Double getElementLocationTimeouts() {\n\t\treturn elementLocationTimeouts;\n\t}\n\n\tpublic void setElementLocationTimeouts(Double elementLocationTimeouts) {\n\t\tthis.elementLocationTimeouts = elementLocationTimeouts;\n\t}\n\n\tpublic String getElementFolder() {\n\t\treturn elementFolder;\n\t}\n\n\tpublic void setElementFolder(String elementFolder) {\n\t\tthis.elementFolder = elementFolder;\n\t}\n\n\tpublic String getSuiteFolder() {\n\t\treturn suiteFolder;\n\t}\n\n\tpublic void setSuiteFolder(String suiteFolder) {\n\t\tthis.suiteFolder = suiteFolder;\n\t}\n\n\tpublic boolean isRemoteMode() {\n\t\treturn remoteMode;\n\t}\n\n\tpublic void setRemoteMode(boolean remoteMode) {\n\t\tthis.remoteMode = remoteMode;\n\t}\n\n\tpublic String getHubRemoteUrl() {\n\t\treturn hubRemoteUrl;\n\t}\n\n\tpublic void setHubRemoteUrl(String hubRemoteUrl) {\n\t\tthis.hubRemoteUrl = hubRemoteUrl;\n\t}\n\n\tpublic String getReportFolder() {\n\t\treturn reportFolder;\n\t}\n\n\tpublic void setReportFolder(String reportFolder) {\n\t\tthis.reportFolder = reportFolder;\n\t}\n\n\tpublic String getScreenshotFolder() {\n\t\treturn screenshotFolder;\n\t}\n\n\tpublic void setScreenshotFolder(String screenshotFolder) {\n\t\tthis.screenshotFolder = screenshotFolder;\n\t}\n\n\tpublic String getChromeDriverPath() {\n\t\treturn chromeDriverPath;\n\t}\n\n\tpublic void setChromeDriverPath(String chromeDriverPath) {\n\t\tthis.chromeDriverPath = chromeDriverPath;\n\t}\n\n\tpublic String getIeDriverPath() {\n\t\treturn ieDriverPath;\n\t}\n\n\tpublic void setIeDriverPath(String ieDriverPath) {\n\t\tthis.ieDriverPath = ieDriverPath;\n\t}\n\n\tpublic String getOperaDriverPath() {\n\t\treturn operaDriverPath;\n\t}\n\n\tpublic void setOperaDriverPath(String operaDriverPath) {\n\t\tthis.operaDriverPath = operaDriverPath;\n\t}\n\n\tpublic String getFirefoxDriverPath() {\n\t\treturn firefoxDriverPath;\n\t}\n\n\tpublic void setFirefoxDriverPath(String firefoxDriverPath) {\n\t\tthis.firefoxDriverPath = firefoxDriverPath;\n\t}\n\n\tpublic Double getDefaultSleepSeconds() {\n\t\treturn defaultSleepSeconds;\n\t}\n\n\tpublic void setDefaultSleepSeconds(Double defaultSleepSeconds) {\n\t\tthis.defaultSleepSeconds = defaultSleepSeconds;\n\t}\n\n\tpublic Collection<String> getTos() {\n\t\treturn tos;\n\t}\n\n\tpublic void setTos(Collection<String> tos) {\n\t\tthis.tos = tos;\n\t}\n\n\tpublic Collection<String> getCcs() {\n\t\treturn ccs;\n\t}\n\n\tpublic void setCcs(Collection<String> ccs) {\n\t\tthis.ccs = ccs;\n\t}\n\n\tpublic Collection<String> getBccs() {\n\t\treturn bccs;\n\t}\n\n\tpublic void setBccs(Collection<String> bccs) {\n\t\tthis.bccs = bccs;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"EnvSettingInfo [remoteMode=\" + remoteMode + \", hubRemoteUrl=\" + hubRemoteUrl + \", reportFolder=\"\n\t\t\t\t+ reportFolder + \", screenshotFolder=\" + screenshotFolder + \", elementFolder=\" + elementFolder\n\t\t\t\t+ \", suiteFolder=\" + suiteFolder + \", chromeDriverPath=\" + chromeDriverPath + \", ieDriverPath=\"\n\t\t\t\t+ ieDriverPath + \", operaDriverPath=\" + operaDriverPath + \", firefoxDriverPath=\" + firefoxDriverPath\n\t\t\t\t+ \", defaultSleepSeconds=\" + defaultSleepSeconds + \", elementLocationRetryCount=\"\n\t\t\t\t+ elementLocationRetryCount + \", elementLocationTimeouts=\" + elementLocationTimeouts + \", mailAccount=\"\n\t\t\t\t+ 1 + \", tos=\" + tos + \", ccs=\" + ccs + \", bccs=\" + bccs + \", cronEnabled=\" + cronEnabled\n\t\t\t\t+ \", cronSuite=\" + cronSuite + \", cronExpression=\" + cronExpression + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/report/StepReport.java",
    "content": "package cn.hutool.json.test.bean.report;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 测试步骤报告\n * @author xuwangcheng\n * @version 20181012\n *\n */\npublic class StepReport {\n\tprivate static int step_id = 0;\n\t\n\tprivate int stepId = getId();\n\t/**\n\t * 步骤名称\n\t */\n\tprivate String stepName;\n\t/**\n\t * 元素名称\n\t */\n\tprivate String elementName;\n\t/**\n\t * 元素定位器\n\t */\n\tprivate String location;\n\t/**\n\t * 参数\n\t */\n\tprivate String params;\n\t/**\n\t * 结果\n\t */\n\tprivate String result;\n\t/**\n\t * 操作名称，中文\n\t */\n\tprivate String actionName;\n\t/**\n\t * 测试时间\n\t */\n\tprivate String testTime;\n\t\n\t/**\n\t * 测试状态：true-成功 false-失败\n\t */\n\tprivate boolean status = true;\n\t/**\n\t * 备注信息\n\t */\n\tprivate String mark;\n\t/**\n\t * 截图路径：相对路径\n\t */\n\tprivate String screenshot;\n\t\n\tprivate static synchronized int getId() {\n\t\treturn step_id++;\n\t}\n\n\tpublic int getStepId() {\n\t\treturn stepId;\n\t}\n\n\tpublic void setStepId(int stepId) {\n\t\tthis.stepId = stepId;\n\t}\n\n\tpublic void setResult(String result) {\n\t\tthis.result = result;\n\t}\n\t\n\tpublic String getResult() {\n\t\treturn result;\n\t}\n\t\n\tpublic String getStepName() {\n\t\treturn stepName;\n\t}\n\n\tpublic void setStepName(String stepName) {\n\t\tthis.stepName = stepName;\n\t}\n\n\tpublic void setStepName() {\n\t\tthis.stepName = this.actionName + (StrUtil.isBlank(this.elementName) ? \"\" : \" => \" + this.elementName);\n\t}\n\t\n\tpublic String getElementName() {\n\t\treturn elementName;\n\t}\n\n\tpublic void setElementName(String elementName) {\n\t\tthis.elementName = elementName;\n\t}\n\n\tpublic String getLocation() {\n\t\treturn location;\n\t}\n\n\tpublic void setLocation(String location) {\n\t\tthis.location = location;\n\t}\n\n\tpublic String getParams() {\n\t\treturn params;\n\t}\n\n\tpublic void setParams(String params) {\n\t\tthis.params = params;\n\t}\n\n\tpublic String getActionName() {\n\t\treturn actionName;\n\t}\n\n\tpublic void setActionName(String actionName) {\n\t\tthis.actionName = actionName;\n\t}\n\n\tpublic String getTestTime() {\n\t\treturn testTime;\n\t}\n\n\tpublic void setTestTime(String testTime) {\n\t\tthis.testTime = testTime;\n\t}\n\n\tpublic boolean isStatus() {\n\t\treturn status;\n\t}\n\n\tpublic void setStatus(boolean status) {\n\t\tthis.status = status;\n\t}\n\n\tpublic String getMark() {\n\t\treturn mark;\n\t}\n\n\tpublic void setMark(String mark) {\n\t\tthis.mark = mark;\n\t}\n\n\tpublic String getScreenshot() {\n\t\treturn screenshot;\n\t}\n\n\tpublic void setScreenshot(String screenshot) {\n\t\tthis.screenshot = screenshot;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"StepReport [stepId=\" + stepId + \", stepName=\" + stepName + \", elementName=\" + elementName\n\t\t\t\t+ \", location=\" + location + \", params=\" + params + \", result=\" + result + \", actionName=\" + actionName\n\t\t\t\t+ \", testTime=\" + testTime + \", status=\" + status + \", mark=\" + mark + \", screenshot=\" + screenshot\n\t\t\t\t+ \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/test/bean/report/SuiteReport.java",
    "content": "package cn.hutool.json.test.bean.report;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 测试套件报告\n * @author xuwangcheng\n * @version 20181012\n *\n */\npublic class SuiteReport {\n\t\n\t/**\n\t * 包含的用例测试报告\n\t */\n\tprivate List<CaseReport> caseReports = new ArrayList<>();\n\t\n\tpublic List<CaseReport> getCaseReports() {\n\t\treturn caseReports;\n\t}\n\n\tpublic void setCaseReports(List<CaseReport> caseReports) {\n\t\tthis.caseReports = caseReports;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SuiteReport [caseReports=\" + caseReports + \"]\";\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONException;\nimport cn.hutool.json.XML;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2748Test {\n\n\t@Test\n\tpublic void toJSONObjectTest() {\n\t\tfinal String s = StrUtil.repeat(\"<a>\", 600);\n\n\t\tassertThrows(JSONException.class, () -> {\n\t\t\tXML.toJSONObject(s, ParseConfig.of().setMaxNestingDepth(512));\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/xml/Issue3560Test.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.XML;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3560Test {\n\t@Test\n\tpublic void toJSONObjectTest() {\n\t\tString inPara= \"<ROOT><ID>002317479934367853</ID><CONTENT><![CDATA[asdfadf&amp;21sdgzxv&amp;aasfasf]]></CONTENT></ROOT>\";\n\t\tJSONObject json = XML.toJSONObject(inPara, true);\n\t\tassertEquals(\"{\\\"ROOT\\\":{\\\"ID\\\":\\\"002317479934367853\\\",\\\"CONTENT\\\":\\\"asdfadf&amp;21sdgzxv&amp;aasfasf\\\"}}\", json.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/xml/IssueID0HP2Test.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.json.JSONConfig;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueID0HP2Test {\n\n\t/**\n\t * JSON转换为XML时使用默认的日期格式，并不能自定义格式，日期格式只用于生成JSON字符串\n\t */\n\t@Test\n\tvoid jsonWithDateToXmlTest() {\n\t\tfinal JSONObject json = JSONUtil.createObj(JSONConfig.create().setDateFormat(\"yyyy/MM/dd\"))\n\t\t\t.set(\"date\", DateUtil.parse(\"2025-10-03\"));\n\t\tString xml = JSONUtil.toXmlStr(json);\n\t\tAssertions.assertEquals(\"<date>2025-10-03 00:00:00</date>\", xml);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/java/cn/hutool/json/xml/XMLTest.java",
    "content": "package cn.hutool.json.xml;\n\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport cn.hutool.json.XML;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class XMLTest {\n\n\t@Test\n\tpublic void toXmlTest() {\n\t\tfinal JSONObject put = JSONUtil.createObj()\n\t\t\t.set(\"aaa\", \"你好\")\n\t\t\t.set(\"键2\", \"test\");\n\t\tfinal String s = JSONUtil.toXmlStr(put);\n\t\tAssertions.assertEquals(\"<aaa>你好</aaa><键2>test</键2>\", s);\n\t}\n\n\t@Test\n\tpublic void escapeTest() {\n\t\tString xml = \"<a>•</a>\";\n\t\tJSONObject jsonObject = XML.toJSONObject(xml);\n\n\t\tassertEquals(\"{\\\"a\\\":\\\"•\\\"}\", jsonObject.toString());\n\n\t\tString xml2 = XML.toXml(JSONUtil.parseObj(jsonObject));\n\t\tassertEquals(xml, xml2);\n\t}\n\n\t@Test\n\tpublic void xmlContentTest() {\n\t\tJSONObject jsonObject = JSONUtil.createObj().set(\"content\", \"123456\");\n\n\t\tString xml = XML.toXml(jsonObject);\n\t\tassertEquals(\"123456\", xml);\n\n\t\txml = XML.toXml(jsonObject, null, new String[0]);\n\t\tassertEquals(\"<content>123456</content>\", xml);\n\t}\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/evaluation.json",
    "content": "{\n  \"HF003\": \"武汉市\",\n  \"HF002\": \"585.37\",\n  \"HF005\": \"\",\n  \"HF004\": \"154\",\n  \"costTotal\": 12,\n  \"HF001\": \"北京市\",\n  \"HF007\": \"北京市\",\n  \"HF006\": \"0\",\n  \"HF009\": \"\",\n  \"HF008\": \"武汉市\",\n  \"HH010\": \"0\",\n  \"resResult\": 0,\n  \"HH011\": \"394.3\",\n  \"HH012\": \"7\",\n  \"HH013\": \"0\",\n  \"HH014\": \"0\",\n  \"XT_NO\": \"2017101714250003\",\n  \"resMsg\": \"success\",\n  \"HH008\": \"0\",\n  \"HH009\": \"0\",\n  \"HH006\": \"0\",\n  \"HH007\": \"0\",\n  \"HH004\": \"0\",\n  \"HH005\": \"0\",\n  \"HH002\": \"1\",\n  \"HH003\": \"0\",\n  \"HH001\": \"38.93\",\n  \"resCode\": \"0000\",\n  \"HD002\": \"77\",\n  \"HD001\": \"1\",\n  \"HG008\": \"0\",\n  \"HE004\": \"0\",\n  \"HG007\": \"0\",\n  \"HE003\": \"0\",\n  \"HE002\": \"2\",\n  \"HG009\": \"0\",\n  \"HE001\": \"1\",\n  \"HG004\": \"4\",\n  \"HE008\": \"139.55\",\n  \"HG003\": \"164.18\",\n  \"HE007\": \"50\",\n  \"HG006\": \"1\",\n  \"HD008\": \"0\",\n  \"HE006\": \"0\",\n  \"HG005\": \"77\",\n  \"HE005\": \"0\",\n  \"HD007\": \"0\",\n  \"HD004\": \"0.0\",\n  \"HG002\": \"7\",\n  \"HE009\": \"0\",\n  \"HD005\": \"0.0\",\n  \"HG001\": \"259.4\",\n  \"HE011\": \"0\",\n  \"HE010\": \"0\",\n  \"HE013\": \"1\",\n  \"HE012\": \"0\",\n  \"HE015\": \"0\",\n  \"HE014\": \"0\",\n  \"HE017\": \"3\",\n  \"HG016\": \"商户\",\n  \"HE016\": \"0\",\n  \"HG015\": \"商户\",\n  \"HE019\": \"20\",\n  \"HG014\": \"餐馆\",\n  \"HE018\": \"2\",\n  \"HG013\": \"计算机商店\",\n  \"HG012\": \"仓储大超市\",\n  \"HG011\": \"3\",\n  \"HG010\": \"0\",\n  \"HB001\": \"1\",\n  \"HB003\": \"631\",\n  \"HB002\": \"492\",\n  \"HE026\": \"1\",\n  \"HE025\": \"1\",\n  \"HE024\": \"59.76\",\n  \"HE023\": \"93.83\",\n  \"HE022\": \"0\",\n  \"HE021\": \"0\",\n  \"XT_MONTH02\": \"2017/04\",\n  \"HE020\": \"0\",\n  \"XT_MONTH01\": \"2017/03\",\n  \"HE029\": \"0\",\n  \"CARD_HOLDER\": \"王雁\",\n  \"HE028\": \"0\",\n  \"HE027\": \"0\",\n  \"CARD_NO\": \"6225768752646560\",\n  \"HC016\": \"103.02\",\n  \"HC015\": \"93.83\",\n  \"HC014\": \"0\",\n  \"HC013\": \"0\",\n  \"HC012\": \"213.73\",\n  \"HC011\": \"90\",\n  \"HC010\": \"3\",\n  \"HE035\": \"0\",\n  \"HE034\": \"0\",\n  \"HE036\": \"43.26\",\n  \"HD010\": \"0\",\n  \"HE031\": \"20\",\n  \"HD011\": \"0\",\n  \"HE030\": \"1\",\n  \"HE033\": \"0\",\n  \"HD013\": \"11.00\",\n  \"HE032\": \"74.18\",\n  \"HD014\": \"423.58\",\n  \"HA002\": \"金卡\",\n  \"HA001\": \"招商银行\",\n  \"HD016\": \"8\",\n  \"HA004\": \"4\",\n  \"HD017\": \"7\",\n  \"HF010\": \"8\",\n  \"HA003\": \"招商银行信用卡\",\n  \"HC008\": \"0\",\n  \"HC007\": \"0\",\n  \"HC009\": \"3\",\n  \"HC004\": \"83.43\",\n  \"HC003\": \"2.0\",\n  \"HC006\": \"3\",\n  \"HC005\": \"3\",\n  \"XT_MONTH05\": \"2017/07\",\n  \"XT_MONTH06\": \"2017/08\",\n  \"HC002\": \"500.58\",\n  \"XT_MONTH03\": \"2017/05\",\n  \"HC001\": \"12\",\n  \"XT_MONTH04\": \"2017/06\"\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/exam_test.json",
    "content": "[\n  {\n    \"id\": 1753,\n    \"examNumber\": 1,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 0\n      }\n    ],\n    \"isRight\": 1,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1754,\n    \"examNumber\": 2,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1756,\n    \"examNumber\": 3,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      },\n      {\n        \"seq\": 2\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1757,\n    \"examNumber\": 4,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      },\n      {\n        \"seq\": 3\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1759,\n    \"examNumber\": 5,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 0\n      }\n    ],\n    \"isRight\": 1,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1760,\n    \"examNumber\": 6,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1762,\n    \"examNumber\": 7,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"公司的发生地方\"\n      },\n      {\n        \"detailContent\": \"告诉对方\"\n      },\n      {\n        \"detailContent\": \"二位水电费\"\n      },\n      {\n        \"detailContent\": \"公司东方闪电\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1763,\n    \"examNumber\": 8,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"告诉对方s\"\n      },\n      {\n        \"detailContent\": \"公司垫付多少\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1765,\n    \"examNumber\": 9,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg,http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg\"\n      }\n    ],\n    \"isSubject\": 1\n  },\n  {\n    \"id\": 1766,\n    \"examNumber\": 10,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg,http://…_1.jpg,http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg\"\n      }\n    ],\n    \"isSubject\": 1\n  },\n  {\n    \"id\": 0,\n    \"examNumber\": 11,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 0\n      }\n    ],\n    \"isRight\": 1,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1,\n    \"examNumber\": 12,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 2,\n    \"examNumber\": 13,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 0\n      }\n    ],\n    \"isRight\": 1,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 3,\n    \"examNumber\": 14,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"seq\": 1\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 4,\n    \"examNumber\": 15,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"公司东方闪电\"\n      },\n      {\n        \"detailContent\": \"国顺副食店\"\n      },\n      {\n        \"detailContent\": \"无二水电费是的\"\n      },\n      {\n        \"detailContent\": \"公司东方闪电\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 5,\n    \"examNumber\": 16,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"公司东方闪电\"\n      },\n      {\n        \"detailContent\": \"格式的维尔士大夫\"\n      },\n      {\n        \"detailContent\": \"公司东风\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 6,\n    \"examNumber\": 17,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg\"\n      }\n    ],\n    \"isSubject\": 1\n  },\n  {\n    \"id\": 0,\n    \"examNumber\": 18,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"告诉对方\"\n      },\n      {\n        \"detailContent\": \"个涉外收电费\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 0,\n    \"examNumber\": 19,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"公司发的\"\n      },\n      {\n        \"detailContent\": \"公司东方闪电\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  },\n  {\n    \"id\": 1774,\n    \"examNumber\": 20,\n    \"isAnswer\": true,\n    \"answerArray\": [\n      {\n        \"detailContent\": \"好改送的非官方的\"\n      },\n      {\n        \"detailContent\": \"和是发送到发送到\"\n      }\n    ],\n    \"isRight\": 0,\n    \"isSubject\": 0\n  }\n]\n"
  },
  {
    "path": "hutool-json/src/test/resources/issue1200.json",
    "content": "{\n  \"items\": [\n    [\n      [\n        {\n          \"fromAccount\": \"test禾信-cd09dss\",\n          \"url\": \"https://m.baidu.com/s?wd=aaa\",\n          \"fromType\": \"搜索推广\"\n        },\n        \"去杭州旅游旅游攻略\",\n        \"杭州旅游攻略\"\n      ]\n    ]\n  ]\n}"
  },
  {
    "path": "hutool-json/src/test/resources/issue488.json",
    "content": "{\n  \"context\": \"https://graph.microsoft.com/beta/$metadata#Collection(microsoft.graph.emailAddress)\",\n  \"value\": [\n    {\n      \"name\": \"\\u4f1a\\u8bae\\u5ba4101\",\n      \"address\": \"MeetingRoom101@abc.com\"\n    },\n    {\n      \"name\": \"\\u4f1a\\u8bae\\u5ba4102\",\n      \"address\": \"MeetingRoom102@abc.com\"\n    },\n    {\n      \"name\": \"\\u4f1a\\u8bae\\u5ba4103\",\n      \"address\": \"MeetingRoom103@abc.com\"\n    },\n    {\n      \"name\": \"\\u4f1a\\u8bae\\u5ba4219\",\n      \"address\": \"MeetingRoom219@abc.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/issue488Array.json",
    "content": "[\n  {\n    \"context\": \"https://graph.microsoft.com/beta/$metadata#Collection(microsoft.graph.emailAddress)\",\n    \"value\": [\n      {\n        \"name\": \"\\u4f1a\\u8bae\\u5ba4101\",\n        \"address\": \"MeetingRoom101@abc.com\"\n      },\n      {\n        \"name\": \"\\u4f1a\\u8bae\\u5ba4102\",\n        \"address\": \"MeetingRoom102@abc.com\"\n      },\n      {\n        \"name\": \"\\u4f1a\\u8bae\\u5ba4103\",\n        \"address\": \"MeetingRoom103@abc.com\"\n      },\n      {\n        \"name\": \"\\u4f1a\\u8bae\\u5ba4219\",\n        \"address\": \"MeetingRoom219@abc.com\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "hutool-json/src/test/resources/issueI5OMSC.json",
    "content": "{\n  \"store\": {\n    \"bicycle\": {\n      \"color\": \"red\",\n      \"price\": 19.95\n    },\n    \"book\": [\n      {\n        \"category\": \"fiction\",\n        \"author\": \"Evelyn Waugh\",\n        \"title\": \"Sword of Honour\",\n        \"price\": 12.99,\n        \"items\": [\n          {\n            \"name\": \"wujing001\",\n            \"age\": 18\n          },\n          {\n            \"name\": \"wujing002\",\n            \"age\": 18\n          }\n        ]\n      },\n      {\n        \"category\": \"fiction02\",\n        \"author\": \"Evelyn Waugh02\",\n        \"title\": \"Sword of Honour02\",\n        \"price\": 12.99\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/issueI676IT.json",
    "content": "{\n  \"page\": {\n    \"pageSize\": 33,\n    \"currentPage\": 81,\n    \"totalSize\": 49,\n    \"orderItems\": [\n      {\n        \"asc\": true,\n        \"columnName\": \"foo\",\n        \"content\": \"bar1\"\n      },\n      {\n        \"asc\": false,\n        \"columnName\": \"foo\",\n        \"content\": \"bar2\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/issueI82AM8.json",
    "content": "{\n  \"en\": {\n    \"testimonials\": [\n      {\n        \"createTime\": {\n          \"dayOfYear\": 261,\n          \"dayOfWeek\": \"MONDAY\",\n          \"year\": 2023,\n          \"month\": \"SEPTEMBER\",\n          \"nano\": 0,\n          \"monthValue\": 9,\n          \"dayOfMonth\": 18,\n          \"hour\": 15,\n          \"minute\": 18,\n          \"second\": 0,\n          \"chronology\": {\n            \"id\": \"ISO\",\n            \"calendarType\": \"iso8601\"\n          }\n        }\n      }\n    ]\n  },\n  \"zh\": {\n    \"testimonials\": [\n      {\n        \"createTime\": {\n          \"dayOfYear\": 261,\n          \"dayOfWeek\": \"MONDAY\",\n          \"year\": 2023,\n          \"month\": \"SEPTEMBER\",\n          \"nano\": 0,\n          \"monthValue\": 9,\n          \"dayOfMonth\": 18,\n          \"hour\": 15,\n          \"minute\": 18,\n          \"second\": 0,\n          \"chronology\": {\n            \"id\": \"ISO\",\n            \"calendarType\": \"iso8601\"\n          }\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/issueIVMD5.json",
    "content": "{\n  \"result\": 1,\n  \"pagination\": {\n    \"maxPage\": \"2057\",\n    \"totalDataCount\": \"4114\"\n  },\n  \"data\": [\n    {\n      \"birthday\": \"\",\n      \"linkPhone\": \"\",\n      \"nation\": \"01\",\n      \"studentCode\": \"20170001\",\n      \"unitiveCode\": \"20170001\",\n      \"sex\": \"2\",\n      \"linkAddress\": \"\",\n      \"identityCard\": \"\",\n      \"accountId\": \"B4DDF491FDF34074AE7A819E1341CB6C\",\n      \"classId\": \"8c38f49c4362479cad59fdd5966fed04\",\n      \"password\": \"25d55ad283aa400af464c76d713c07ad\",\n      \"modifyTime\": \"20190311152351126\",\n      \"isDeleted\": \"0\",\n      \"postalcode\": \"\",\n      \"background\": \"\",\n      \"schoolId\": \"D23E00EBA6364220A17BBDB92A361ACA\",\n      \"studentName\": \"蓝有城\",\n      \"sequenceIntId\": \"1\",\n      \"nativePlace\": \"\",\n      \"id\": \"2C90488969524E9901696BA2E3D60005\",\n      \"username\": \"s0000000002894659\"\n    },\n    {\n      \"birthday\": \"20010421000000\",\n      \"linkPhone\": \"\",\n      \"nation\": \"01\",\n      \"studentCode\": \"16000001310001\",\n      \"unitiveCode\": \"G452226200104219223\",\n      \"sex\": \"2\",\n      \"linkAddress\": \"\",\n      \"identityCard\": \"\",\n      \"accountId\": \"\",\n      \"classId\": \"8dbcc4906abc484a83cc0e1e1f0d91a3\",\n      \"password\": \"\",\n      \"modifyTime\": \"20190327201832000\",\n      \"isDeleted\": \"0\",\n      \"postalcode\": \"546100\",\n      \"background\": \"\",\n      \"schoolId\": \"D23E00EBA6364220A17BBDB92A361ACA\",\n      \"studentName\": \"韦奇秀\",\n      \"sequenceIntId\": \"21\",\n      \"nativePlace\": \"451302\",\n      \"id\": \"2C90488969BD091C0169BEF12988002A\",\n      \"username\": \"\"\n    }\n  ],\n  \"data2\": {\n    \"birthday\": \"\",\n    \"linkPhone\": \"\",\n    \"nation\": \"01\",\n    \"studentCode\": \"20170001\",\n    \"unitiveCode\": \"20170001\",\n    \"sex\": \"2\",\n    \"linkAddress\": \"\",\n    \"identityCard\": \"\",\n    \"accountId\": \"B4DDF491FDF34074AE7A819E1341CB6C\",\n    \"classId\": \"8c38f49c4362479cad59fdd5966fed04\",\n    \"password\": \"25d55ad283aa400af464c76d713c07ad\",\n    \"modifyTime\": \"20190311152351126\",\n    \"isDeleted\": \"0\",\n    \"postalcode\": \"\",\n    \"background\": \"\",\n    \"schoolId\": \"D23E00EBA6364220A17BBDB92A361ACA\",\n    \"studentName\": \"蓝有城\",\n    \"sequenceIntId\": \"1\",\n    \"nativePlace\": \"\",\n    \"id\": \"2C90488969524E9901696BA2E3D60005\",\n    \"username\": \"s0000000002894659\"\n  },\n  \"nextDataUri\": \"\",\n  \"message\": \"ok\",\n  \"dataCount\": 2\n}\n"
  },
  {
    "path": "hutool-json/src/test/resources/suiteReport.json",
    "content": "{\n  \"reportName\": \"Web自动化_20181104194718\",\n  \"failCount\": 0,\n  \"finished\": true,\n  \"title\": \"Web自动化\",\n  \"totalCount\": 1,\n  \"env\": {\n    \"cronSuite\": \"testsuite\",\n    \"firefoxDriverPath\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/src/main/resources/geckodriver.exe\",\n    \"mailAccount\": {\n      \"charset\": \"UTF-8\",\n      \"debug\": false,\n      \"auth\": true,\n      \"pass\": \"123456\",\n      \"socketFactoryFallback\": false,\n      \"socketFactoryPort\": 465,\n      \"startttlsEnable\": false,\n      \"port\": 25,\n      \"socketFactoryClass\": \"javax.net.ssl.SSLSocketFactory\",\n      \"host\": \"smtp.yeah.net\",\n      \"splitlongparameters\": false,\n      \"from\": \"hutool@yeah.net\",\n      \"user\": \"hutool\"\n    },\n    \"remoteMode\": false,\n    \"hubRemoteUrl\": \"http://192.168.2.40:4444/wd/hub\",\n    \"screenshotFolder\": \"/screenshot\",\n    \"cronEnabled\": true,\n    \"cronExpression\": \"*/2 * * * *\",\n    \"elementFolder\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/config/element/\",\n    \"ccs\": [\n      \"\"\n    ],\n    \"elementLocationRetryCount\": 3,\n    \"sqlitePath\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest\\\\report.db\",\n    \"defaultSleepSeconds\": 0.5,\n    \"suiteFolder\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/config/suite/\",\n    \"tos\": [\n      \"309873223@qq.com\",\n      \"610421185@qq.com\",\n      \"xuwangcheng14@163.com\"\n    ],\n    \"ieDriverPath\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/src/main/resources/IEDriverServer.exe\",\n    \"elementLocationTimeouts\": 9,\n    \"reportFolder\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/report\",\n    \"operaDriverPath\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/src/main/resources/operadriver.exe\",\n    \"chromeDriverPath\": \"F:\\\\Eclipse2017Workplace\\\\MasterYIUITest/src/main/resources/chromedriver.exe\",\n    \"bccs\": [\n      \"\"\n    ]\n  },\n  \"browserName\": [\n    \"chrome\"\n  ],\n  \"successCount\": 1,\n  \"useTime\": 11069,\n  \"testTime\": \"2018-11-04 19:47:07\",\n  \"endTime\": \"2018-11-04 19:47:18\",\n  \"skipCount\": 0,\n  \"caseReports\": [\n    {\n      \"finishTime\": \"2018-11-04 19:47:18\",\n      \"caseName\": \"百度搜索\",\n      \"useTime\": \"6320\",\n      \"runCount\": 0,\n      \"caseMethodPath\": \"com.dcits.test.baidu.usecase.Baidu.search\",\n      \"stepReports\": [\n        {\n          \"stepName\": \"打开Url地址\",\n          \"stepId\": 0,\n          \"testTime\": \"2018-11-04 19:47:11\",\n          \"params\": \"https://www.baidu.com/\",\n          \"actionName\": \"打开Url地址\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"输入 => 搜索框\",\n          \"stepId\": 1,\n          \"location\": \"id => kw\",\n          \"testTime\": \"2018-11-04 19:47:12\",\n          \"params\": \"xuwangcheng.com\",\n          \"elementName\": \"搜索框\",\n          \"actionName\": \"输入\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"点击 => 搜索按钮\",\n          \"stepId\": 2,\n          \"location\": \"id => su\",\n          \"testTime\": \"2018-11-04 19:47:14\",\n          \"params\": \"\",\n          \"elementName\": \"搜索按钮\",\n          \"actionName\": \"点击\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"点击 => 搜索结果\",\n          \"stepId\": 3,\n          \"location\": \"xpath => //*[@id=\\\"1\\\"]/h3/a\",\n          \"testTime\": \"2018-11-04 19:47:15\",\n          \"params\": \"\",\n          \"elementName\": \"搜索结果\",\n          \"actionName\": \"点击\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"切换到指定窗口\",\n          \"stepId\": 4,\n          \"testTime\": \"2018-11-04 19:47:17\",\n          \"params\": \"1\",\n          \"actionName\": \"切换到指定窗口\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"切换到指定窗口\",\n          \"stepId\": 5,\n          \"testTime\": \"2018-11-04 19:47:17\",\n          \"params\": \"0\",\n          \"actionName\": \"切换到指定窗口\",\n          \"status\": true\n        },\n        {\n          \"stepName\": \"刷新页面\",\n          \"stepId\": 6,\n          \"testTime\": \"2018-11-04 19:47:17\",\n          \"params\": \"\",\n          \"actionName\": \"刷新页面\",\n          \"status\": true\n        }\n      ],\n      \"browserType\": \"chrome\",\n      \"status\": \"success\"\n    }\n  ]\n}\n"
  },
  {
    "path": "hutool-jwt/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-jwt</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>JWT生成、解析和验证实现</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.jwt</Automatic-Module-Name>\n\n\t\t<!-- versions -->\n\t\t<jjwt.version>0.13.0</jjwt.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-json</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-crypto</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<!-- 测试特殊算法 -->\n\t\t<dependency>\n\t\t\t<groupId>org.bouncycastle</groupId>\n\t\t\t<artifactId>bcpkix-jdk18on</artifactId>\n\t\t\t<version>${bouncycastle.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>io.jsonwebtoken</groupId>\n\t\t\t<artifactId>jjwt-impl</artifactId>\n\t\t\t<version>${jjwt.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>io.jsonwebtoken</groupId>\n\t\t\t<artifactId>jjwt-gson</artifactId>\n\t\t\t<version>${jjwt.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/Claims.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.date.format.GlobalCustomFormat;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.json.JSONConfig;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\n/**\n * Claims 认证，简单的JSONObject包装\n *\n * @author looly\n * @since 5.7.0\n */\npublic class Claims implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 时间使用秒级时间戳表示\n\tprivate final JSONConfig CONFIG = JSONConfig.create().setDateFormat(GlobalCustomFormat.FORMAT_SECONDS);\n\n\tprivate JSONObject claimJSON;\n\n\t/**\n\t * 增加Claims属性，如果属性值为{@code null}，则移除这个属性\n\t *\n\t * @param name  属性名\n\t * @param value 属性值\n\t */\n\tprotected void setClaim(String name, Object value) {\n\t\tinit();\n\t\tAssert.notNull(name, \"Name must be not null!\");\n\t\tif (value == null) {\n\t\t\tclaimJSON.remove(name);\n\t\t\treturn;\n\t\t}\n\t\tclaimJSON.set(name, value);\n\t}\n\n\t/**\n\t * 加入多个Claims属性\n\t *\n\t * @param headerClaims 多个Claims属性\n\t */\n\tprotected void putAll(Map<String, ?> headerClaims) {\n\t\tif (MapUtil.isNotEmpty(headerClaims)) {\n\t\t\tfor (Map.Entry<String, ?> entry : headerClaims.entrySet()) {\n\t\t\t\tsetClaim(entry.getKey(), entry.getValue());\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定名称属性\n\t *\n\t * @param name 名称\n\t * @return 属性\n\t */\n\tpublic Object getClaim(String name) {\n\t\tinit();\n\t\treturn this.claimJSON.getObj(name);\n\t}\n\n\t/**\n\t * 获取Claims的JSON字符串形式\n\t *\n\t * @return JSON字符串\n\t */\n\tpublic JSONObject getClaimsJson() {\n\t\tinit();\n\t\treturn this.claimJSON;\n\t}\n\n\t/**\n\t * 解析JWT JSON\n\t *\n\t * @param tokenPart JWT JSON\n\t * @param charset   编码\n\t */\n\tpublic void parse(String tokenPart, Charset charset) {\n\t\tthis.claimJSON = JSONUtil.parseObj(Base64.decodeStr(tokenPart, charset), CONFIG);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tinit();\n\t\treturn this.claimJSON.toString();\n\t}\n\n\tprivate void init() {\n\t\tif (null == this.claimJSON) {\n\t\t\tthis.claimJSON = new JSONObject(CONFIG);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWT.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.signers.AlgorithmUtil;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport cn.hutool.jwt.signers.NoneJWTSigner;\n\nimport java.nio.charset.Charset;\nimport java.security.Key;\nimport java.security.KeyPair;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * JSON Web Token (JWT)，基于JSON的开放标准（(RFC 7519)用于在网络应用环境间传递声明。<br>\n * <p>\n * 结构：header.payload.signature\n * <ul>\n *     <li>header：主要声明了JWT的签名算法</li>\n *     <li>payload：主要承载了各种声明并传递明文数据</li>\n *     <li>signature：拥有该部分的JWT被称为JWS，也就是签了名的JWS</li>\n * </ul>\n *\n * <p>\n * 详细介绍见：https://www.jianshu.com/p/576dbf44b2ae\n * </p>\n *\n * @author looly\n * @since 5.7.0\n */\npublic class JWT implements RegisteredPayload<JWT> {\n\n\tprivate final JWTHeader header;\n\tprivate final JWTPayload payload;\n\n\tprivate Charset charset;\n\tprivate JWTSigner signer;\n\n\tprivate List<String> tokens;\n\n\t/**\n\t * 创建空的JWT对象\n\t *\n\t * @return JWT\n\t */\n\tpublic static JWT create() {\n\t\treturn new JWT();\n\t}\n\n\t/**\n\t * 创建并解析JWT对象\n\t *\n\t * @param token JWT Token字符串，格式为xxxx.yyyy.zzzz\n\t * @return JWT\n\t */\n\tpublic static JWT of(String token) {\n\t\treturn new JWT(token);\n\t}\n\n\t/**\n\t * 构造\n\t */\n\tpublic JWT() {\n\t\tthis.header = new JWTHeader();\n\t\tthis.payload = new JWTPayload();\n\t\tthis.charset = CharsetUtil.CHARSET_UTF_8;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param token JWT Token字符串，格式为xxxx.yyyy.zzzz\n\t */\n\tpublic JWT(String token) {\n\t\tthis();\n\t\tparse(token);\n\t}\n\n\t/**\n\t * 解析JWT内容\n\t *\n\t * @param token JWT Token字符串，格式为xxxx.yyyy.zzzz\n\t * @return this\n\t */\n\tpublic JWT parse(String token) {\n\t\tAssert.notBlank(token, \"Token String must be not blank!\");\n\t\tfinal List<String> tokens = splitToken(token);\n\t\tthis.tokens = tokens;\n\t\tthis.header.parse(tokens.get(0), this.charset);\n\t\tthis.payload.parse(tokens.get(1), this.charset);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return this\n\t */\n\tpublic JWT setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置密钥，如果头部指定了算法，直接使用，否则默认算法是：HS256(HmacSHA256)\n\t *\n\t * @param key 密钥\n\t * @return this\n\t */\n\tpublic JWT setKey(byte[] key) {\n\t\treturn setSigner(StrUtil.nullToDefault(getAlgorithm(), \"HS256\"), key);\n\t}\n\n\t/**\n\t * 设置签名算法\n\t *\n\t * @param algorithmId 签名算法ID，如HS256\n\t * @param key         密钥\n\t * @return this\n\t */\n\tpublic JWT setSigner(String algorithmId, byte[] key) {\n\t\treturn setSigner(JWTSignerUtil.createSigner(algorithmId, key));\n\t}\n\n\t/**\n\t * 设置签名算法\n\t *\n\t * @param algorithmId 签名算法ID，如HS256\n\t * @param key         密钥\n\t * @return this\n\t * @since 5.7.2\n\t */\n\tpublic JWT setSigner(String algorithmId, Key key) {\n\t\treturn setSigner(JWTSignerUtil.createSigner(algorithmId, key));\n\t}\n\n\t/**\n\t * 设置非对称签名算法\n\t *\n\t * @param algorithmId 签名算法ID，如HS256\n\t * @param keyPair     密钥对\n\t * @return this\n\t * @since 5.7.2\n\t */\n\tpublic JWT setSigner(String algorithmId, KeyPair keyPair) {\n\t\treturn setSigner(JWTSignerUtil.createSigner(algorithmId, keyPair));\n\t}\n\n\t/**\n\t * 设置签名算法\n\t *\n\t * @param signer 签名算法\n\t * @return this\n\t */\n\tpublic JWT setSigner(JWTSigner signer) {\n\t\tthis.signer = signer;\n\n\t\t// 检查头信息中是否有算法信息\n\t\tfinal String algorithm = (String) this.header.getClaim(JWTHeader.ALGORITHM);\n\t\tif (StrUtil.isBlank(algorithm)) {\n\t\t\tthis.header.setAlgorithm(AlgorithmUtil.getId(signer.getAlgorithm()));\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取JWT算法签名器\n\t *\n\t * @return JWT算法签名器\n\t */\n\tpublic JWTSigner getSigner() {\n\t\treturn this.signer;\n\t}\n\n\t/**\n\t * 获取所有头信息\n\t *\n\t * @return 头信息\n\t */\n\tpublic JSONObject getHeaders() {\n\t\treturn this.header.getClaimsJson();\n\t}\n\n\t/**\n\t * 获取头\n\t *\n\t * @return 头信息\n\t * @since 5.7.2\n\t */\n\tpublic JWTHeader getHeader() {\n\t\treturn this.header;\n\t}\n\n\t/**\n\t * 获取头信息\n\t *\n\t * @param name 头信息名称\n\t * @return 头信息\n\t */\n\tpublic Object getHeader(String name) {\n\t\treturn this.header.getClaim(name);\n\t}\n\n\t/**\n\t * 获取算法ID(alg)头信息\n\t *\n\t * @return 算法头信息\n\t * @see JWTHeader#ALGORITHM\n\t */\n\tpublic String getAlgorithm() {\n\t\treturn (String) this.header.getClaim(JWTHeader.ALGORITHM);\n\t}\n\n\t/**\n\t * 设置JWT头信息\n\t *\n\t * @param name  头名\n\t * @param value 头\n\t * @return this\n\t */\n\tpublic JWT setHeader(String name, Object value) {\n\t\tthis.header.setClaim(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加JWT头信息\n\t *\n\t * @param headers 头信息\n\t * @return this\n\t */\n\tpublic JWT addHeaders(Map<String, ?> headers) {\n\t\tthis.header.addHeaders(headers);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取所有载荷信息\n\t *\n\t * @return 载荷信息\n\t */\n\tpublic JSONObject getPayloads() {\n\t\treturn this.payload.getClaimsJson();\n\t}\n\n\t/**\n\t * 获取载荷对象\n\t *\n\t * @return 载荷信息\n\t * @since 5.7.2\n\t */\n\tpublic JWTPayload getPayload() {\n\t\treturn this.payload;\n\t}\n\n\t/**\n\t * 获取载荷信息\n\t *\n\t * @param name 载荷信息名称\n\t * @return 载荷信息\n\t */\n\tpublic Object getPayload(String name) {\n\t\treturn getPayload().getClaim(name);\n\t}\n\n\t/**\n\t * 设置JWT载荷信息\n\t *\n\t * @param name  载荷名\n\t * @param value 头\n\t * @return this\n\t */\n\t@Override\n\tpublic JWT setPayload(String name, Object value) {\n\t\tthis.payload.setClaim(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加JWT载荷信息\n\t *\n\t * @param payloads 载荷信息\n\t * @return this\n\t */\n\tpublic JWT addPayloads(Map<String, ?> payloads) {\n\t\tthis.payload.addPayloads(payloads);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 签名生成JWT字符串\n\t *\n\t * @return JWT字符串\n\t */\n\tpublic String sign() {\n\t\treturn sign(true);\n\t}\n\n\t/**\n\t * 签名生成JWT字符串\n\t *\n\t * @param addTypeIfNot 如果'typ'头不存在，是否赋值默认值\n\t * @return JWT字符串\n\t * @since 5.8.24\n\t */\n\tpublic String sign(boolean addTypeIfNot) {\n\t\treturn sign(this.signer, addTypeIfNot);\n\t}\n\n\t/**\n\t * 签名生成JWT字符串\n\t *\n\t * @param signer JWT签名器\n\t * @return JWT字符串\n\t */\n\tpublic String sign(JWTSigner signer) {\n\t\treturn sign(signer, true);\n\t}\n\n\t/**\n\t * 签名生成JWT字符串\n\t *\n\t * @param signer       JWT签名器\n\t * @param addTypeIfNot 如果'typ'头不存在，是否赋值默认值\n\t * @return JWT字符串\n\t * @since 5.8.24\n\t */\n\tpublic String sign(JWTSigner signer, boolean addTypeIfNot) {\n\t\tAssert.notNull(signer, () -> new JWTException(\"No Signer provided!\"));\n\n\t\t// 检查tye信息\n\t\tif (addTypeIfNot) {\n\t\t\tfinal String type = (String) this.header.getClaim(JWTHeader.TYPE);\n\t\t\tif (StrUtil.isBlank(type)) {\n\t\t\t\tthis.header.setClaim(JWTHeader.TYPE, \"JWT\");\n\t\t\t}\n\t\t}\n\n\t\t// 检查头信息中是否有算法信息\n\t\tfinal String algorithm = getAlgorithm();\n\t\tif (StrUtil.isBlank(algorithm)) {\n\t\t\tthis.header.setClaim(JWTHeader.ALGORITHM,\n\t\t\t\tAlgorithmUtil.getId(signer.getAlgorithm()));\n\t\t}\n\n\t\tfinal String headerBase64 = Base64.encodeUrlSafe(this.header.toString(), charset);\n\t\tfinal String payloadBase64 = Base64.encodeUrlSafe(this.payload.toString(), charset);\n\t\tfinal String sign = signer.sign(headerBase64, payloadBase64);\n\n\t\treturn StrUtil.format(\"{}.{}.{}\", headerBase64, payloadBase64, sign);\n\t}\n\n\t/**\n\t * 验证JWT Token是否有效\n\t *\n\t * @return 是否有效\n\t */\n\tpublic boolean verify() {\n\t\treturn verify(this.signer);\n\t}\n\n\t/**\n\t * 验证JWT是否有效，验证包括：\n\t *\n\t * <ul>\n\t *     <li>Token是否正确</li>\n\t *     <li>{@link JWTPayload#NOT_BEFORE}：生效时间不能晚于当前时间</li>\n\t *     <li>{@link JWTPayload#EXPIRES_AT}：失效时间不能早于当前时间</li>\n\t *     <li>{@link JWTPayload#ISSUED_AT}： 签发时间不能晚于当前时间</li>\n\t * </ul>\n\t *\n\t * @param leeway 容忍空间，单位：秒。当不能晚于当前时间时，向后容忍；不能早于向前容忍。\n\t * @return 是否有效\n\t * @see JWTValidator\n\t * @since 5.7.4\n\t */\n\tpublic boolean validate(long leeway) {\n\t\tif (false == verify()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 校验时间字段\n\t\ttry {\n\t\t\tJWTValidator.of(this).validateDate(DateUtil.date(), leeway);\n\t\t} catch (ValidateException e) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * 验证JWT Token是否有效\n\t *\n\t * @param signer 签名器（签名算法）\n\t * @return 是否有效\n\t */\n\tpublic boolean verify(JWTSigner signer) {\n\t\tif (null == signer) {\n\t\t\t// 如果无签名器提供，默认认为是无签名JWT信息\n\t\t\tsigner = NoneJWTSigner.NONE;\n\t\t}\n\n\t\t// 用户定义alg为none但是签名器不是NoneJWTSigner\n\t\tif(NoneJWTSigner.isNone(getAlgorithm()) && !(signer instanceof NoneJWTSigner)){\n\t\t\tthrow new JWTException(\"Alg is 'none' but use: {} !\", signer.getClass());\n\t\t}\n\n\t\t// alg非none，但签名器是NoneJWTSigner\n\t\tif(signer instanceof NoneJWTSigner && !NoneJWTSigner.isNone(getAlgorithm())){\n\t\t\tthrow new JWTException(\"Alg is not 'none' but use NoneJWTSigner!\");\n\t\t}\n\n\t\tfinal List<String> tokens = this.tokens;\n\t\tif (CollUtil.isEmpty(tokens)) {\n\t\t\tthrow new JWTException(\"No token to verify!\");\n\t\t}\n\t\treturn signer.verify(tokens.get(0), tokens.get(1), tokens.get(2));\n\t}\n\n\t/**\n\t * 将JWT字符串拆分为3部分，无加密算法则最后一部分是\"\"\n\t *\n\t * @param token JWT Token\n\t * @return 三部分内容\n\t */\n\tprivate static List<String> splitToken(String token) {\n\t\tfinal List<String> tokens = StrUtil.split(token, CharUtil.DOT);\n\t\tif (3 != tokens.size()) {\n\t\t\tthrow new JWTException(\"The token was expected 3 parts, but got {}.\", tokens.size());\n\t\t}\n\t\treturn tokens;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWTException.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * JWT异常\n *\n * @author looly\n * @since 5.7.0\n */\npublic class JWTException extends RuntimeException {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic JWTException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic JWTException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic JWTException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic JWTException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic JWTException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic JWTException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWTHeader.java",
    "content": "package cn.hutool.jwt;\n\nimport java.util.Map;\n\n/**\n * JWT头部信息\n *\n * @author looly\n * @since 5.7.0\n */\npublic class JWTHeader extends Claims {\n\tprivate static final long serialVersionUID = 1L;\n\n\t//Header names\n\t/**\n\t * 加密算法，通常为HMAC SHA256（HS256）\n\t */\n\tpublic static String ALGORITHM = \"alg\";\n\t/**\n\t * 声明类型，一般为jwt\n\t */\n\tpublic static String TYPE = \"typ\";\n\t/**\n\t * 内容类型（content type）\n\t */\n\tpublic static String CONTENT_TYPE = \"cty\";\n\t/**\n\t * jwk的ID编号\n\t */\n\tpublic static String KEY_ID = \"kid\";\n\n\t/**\n\t * 构造，初始化默认(typ=JWT)\n\t */\n\tpublic JWTHeader() {}\n\n\t/**\n\t * 增加“alg”头信息\n\t *\n\t * @param algorithm 算法ID，如HS265\n\t * @return this\n\t * @since 5.8.42\n\t */\n\tpublic JWTHeader setAlgorithm(final String algorithm) {\n\t\tsetClaim(ALGORITHM, algorithm);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加“typ”头信息\n\t *\n\t * @param type 类型，如JWT\n\t * @return this\n\t * @since 5.8.42\n\t */\n\tpublic JWTHeader setType(final String type) {\n\t\tsetClaim(TYPE, type);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加“cty”头信息\n\t *\n\t * @param contentType 内容类型\n\t * @return this\n\t * @since 5.8.42\n\t */\n\tpublic JWTHeader setContentType(final String contentType) {\n\t\tsetClaim(CONTENT_TYPE, contentType);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加“kid”头信息\n\t *\n\t * @param keyId kid\n\t * @return this\n\t */\n\tpublic JWTHeader setKeyId(String keyId) {\n\t\tsetClaim(KEY_ID, keyId);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加自定义JWT认证头\n\t *\n\t * @param headerClaims 头信息\n\t * @return this\n\t */\n\tpublic JWTHeader addHeaders(Map<String, ?> headerClaims) {\n\t\tputAll(headerClaims);\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWTPayload.java",
    "content": "package cn.hutool.jwt;\n\nimport java.util.Map;\n\n/**\n * JWT载荷信息<br>\n * 载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品，这些有效信息包含三个部分:\n *\n * <ul>\n *     <li>标准中注册的声明</li>\n *     <li>公共的声明</li>\n *     <li>私有的声明</li>\n * </ul>\n * <p>\n * 详细介绍见：https://www.jianshu.com/p/576dbf44b2ae\n *\n * @author looly\n * @since 5.7.0\n */\npublic class JWTPayload extends Claims implements RegisteredPayload<JWTPayload>{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 增加自定义JWT认证载荷信息\n\t *\n\t * @param payloadClaims 载荷信息\n\t * @return this\n\t */\n\tpublic JWTPayload addPayloads(Map<String, ?> payloadClaims) {\n\t\tputAll(payloadClaims);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic JWTPayload setPayload(String name, Object value) {\n\t\tsetClaim(name, value);\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWTUtil.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.jwt.signers.JWTSigner;\n\nimport java.util.Map;\n\n/**\n * JSON Web Token (JWT)工具类\n */\npublic class JWTUtil {\n\n\t/**\n\t * 创建HS256(HmacSHA256) JWT Token\n\t *\n\t * @param payload 荷载信息\n\t * @param key     HS256(HmacSHA256)密钥\n\t * @return JWT Token\n\t */\n\tpublic static String createToken(Map<String, Object> payload, byte[] key) {\n\t\treturn createToken(null, payload, key);\n\t}\n\n\t/**\n\t * 创建HS256(HmacSHA256) JWT Token\n\t *\n\t * @param headers 头信息\n\t * @param payload 荷载信息\n\t * @param key     HS256(HmacSHA256)密钥\n\t * @return JWT Token\n\t */\n\tpublic static String createToken(Map<String, Object> headers, Map<String, Object> payload, byte[] key) {\n\t\treturn JWT.create()\n\t\t\t\t.addHeaders(headers)\n\t\t\t\t.addPayloads(payload)\n\t\t\t\t.setKey(key)\n\t\t\t\t.sign();\n\t}\n\n\t/**\n\t * 创建JWT Token\n\t *\n\t * @param payload 荷载信息\n\t * @param signer  签名算法\n\t * @return JWT Token\n\t */\n\tpublic static String createToken(Map<String, Object> payload, JWTSigner signer) {\n\t\treturn createToken(null, payload, signer);\n\t}\n\n\t/**\n\t * 创建JWT Token\n\t *\n\t * @param headers 头信息\n\t * @param payload 荷载信息\n\t * @param signer  签名算法\n\t * @return JWT Token\n\t */\n\tpublic static String createToken(Map<String, Object> headers, Map<String, Object> payload, JWTSigner signer) {\n\t\treturn JWT.create()\n\t\t\t\t.addHeaders(headers)\n\t\t\t\t.addPayloads(payload)\n\t\t\t\t.setSigner(signer)\n\t\t\t\t.sign();\n\t}\n\n\t/**\n\t * 解析JWT Token\n\t *\n\t * @param token token\n\t * @return {@link JWT}\n\t */\n\tpublic static JWT parseToken(String token) {\n\t\treturn JWT.of(token);\n\t}\n\n\t/**\n\t * 验证JWT Token有效性\n\t *\n\t * @param token JWT Token\n\t * @param key   HS256(HmacSHA256)密钥\n\t * @return 是否有效\n\t */\n\tpublic static boolean verify(String token, byte[] key) {\n\t\treturn JWT.of(token).setKey(key).verify();\n\t}\n\n\t/**\n\t * 验证JWT Token有效性\n\t *\n\t * @param token  JWT Token\n\t * @param signer 签名器\n\t * @return 是否有效\n\t */\n\tpublic static boolean verify(String token, JWTSigner signer) {\n\t\treturn JWT.of(token).verify(signer);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/JWTValidator.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.NoneJWTSigner;\n\nimport java.util.Date;\n\n/**\n * JWT数据校验器，用于校验包括：\n * <ul>\n *     <li>算法是否一致</li>\n *     <li>算法签名是否正确</li>\n *     <li>字段值是否有效（例如时间未过期等）</li>\n * </ul>\n *\n * @author looly\n * @since 5.7.2\n */\npublic class JWTValidator {\n\n\tprivate final JWT jwt;\n\n\t/**\n\t * 创建JWT验证器\n\t *\n\t * @param token JWT Token\n\t * @return JWTValidator\n\t */\n\tpublic static JWTValidator of(String token) {\n\t\treturn new JWTValidator(JWT.of(token));\n\t}\n\n\t/**\n\t * 创建JWT验证器\n\t *\n\t * @param jwt JWT对象\n\t * @return JWTValidator\n\t */\n\tpublic static JWTValidator of(JWT jwt) {\n\t\treturn new JWTValidator(jwt);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param jwt JWT对象\n\t */\n\tpublic JWTValidator(JWT jwt) {\n\t\tthis.jwt = jwt;\n\t}\n\n\t/**\n\t * 验证算法，使用JWT对象自带的{@link JWTSigner}\n\t *\n\t * @return this\n\t * @throws ValidateException 验证失败的异常\n\t */\n\tpublic JWTValidator validateAlgorithm() throws ValidateException {\n\t\treturn validateAlgorithm(null);\n\t}\n\n\t/**\n\t * 验证算法，使用自定义的{@link JWTSigner}\n\t *\n\t * @param signer 用于验证算法的签名器\n\t * @return this\n\t * @throws ValidateException 验证失败的异常\n\t */\n\tpublic JWTValidator validateAlgorithm(JWTSigner signer) throws ValidateException {\n\t\tvalidateAlgorithm(this.jwt, signer);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 检查JWT的以下三两个时间：\n\t *\n\t * <ul>\n\t *     <li>{@link JWTPayload#NOT_BEFORE}：被检查时间必须晚于生效时间</li>\n\t *     <li>{@link JWTPayload#EXPIRES_AT}：被检查时间必须早于失效时间</li>\n\t *     <li>{@link JWTPayload#ISSUED_AT}：签发时间必须早于失效时间</li>\n\t * </ul>\n\t * <p>\n\t * 如果某个时间没有设置，则不检查（表示无限制）\n\t *\n\t * @return this\n\t * @throws ValidateException 验证失败的异常\n\t * @since 5.7.3\n\t */\n\tpublic JWTValidator validateDate() throws ValidateException {\n\t\treturn validateDate(DateUtil.beginOfSecond(DateUtil.date()));\n\t}\n\n\t/**\n\t * 检查JWT的以下三两个时间：\n\t *\n\t * <ul>\n\t *     <li>{@link JWTPayload#NOT_BEFORE}：生效时间不能晚于当前时间</li>\n\t *     <li>{@link JWTPayload#EXPIRES_AT}：失效时间不能早于当前时间</li>\n\t *     <li>{@link JWTPayload#ISSUED_AT}： 签发时间不能晚于当前时间</li>\n\t * </ul>\n\t * <p>\n\t * 如果某个时间没有设置，则不检查（表示无限制）\n\t *\n\t * @param dateToCheck 被检查的时间，一般为当前时间\n\t * @return this\n\t * @throws ValidateException 验证失败的异常\n\t */\n\tpublic JWTValidator validateDate(Date dateToCheck) throws ValidateException {\n\t\tvalidateDate(this.jwt.getPayload(), dateToCheck, 0L);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 检查JWT的以下三两个时间：\n\t *\n\t * <ul>\n\t *     <li>{@link JWTPayload#NOT_BEFORE}：生效时间不能晚于当前时间</li>\n\t *     <li>{@link JWTPayload#EXPIRES_AT}：失效时间不能早于当前时间</li>\n\t *     <li>{@link JWTPayload#ISSUED_AT}： 签发时间不能晚于当前时间</li>\n\t * </ul>\n\t * <p>\n\t * 如果某个时间没有设置，则不检查（表示无限制）\n\t *\n\t * @param dateToCheck 被检查的时间，一般为当前时间\n\t * @param leeway      容忍空间，单位：秒。当不能晚于当前时间时，向后容忍；不能早于向前容忍。\n\t * @return this\n\t * @throws ValidateException 验证失败的异常\n\t */\n\tpublic JWTValidator validateDate(Date dateToCheck, long leeway) throws ValidateException {\n\t\tvalidateDate(this.jwt.getPayload(), dateToCheck, leeway);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 验证算法\n\t *\n\t * @param jwt    {@link JWT}对象\n\t * @param signer 用于验证的签名器\n\t * @throws ValidateException 验证异常\n\t */\n\tprivate static void validateAlgorithm(JWT jwt, JWTSigner signer) throws ValidateException {\n\t\tfinal String algorithmId = jwt.getAlgorithm();\n\t\tif (null == signer) {\n\t\t\tsigner = jwt.getSigner();\n\t\t}\n\n\t\tif (StrUtil.isEmpty(algorithmId)) {\n\t\t\t// 可能无签名\n\t\t\tif (null == signer || signer instanceof NoneJWTSigner) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new ValidateException(\"No algorithm defined in header!\");\n\t\t}\n\n\t\tif (null == signer) {\n\t\t\tthrow new IllegalArgumentException(\"No Signer for validate algorithm!\");\n\t\t}\n\n\t\tfinal String algorithmIdInSigner = signer.getAlgorithmId();\n\t\tif (false == StrUtil.equals(algorithmId, algorithmIdInSigner)) {\n\t\t\tthrow new ValidateException(\"Algorithm [{}] defined in header doesn't match to [{}]!\"\n\t\t\t\t\t, algorithmId, algorithmIdInSigner);\n\t\t}\n\n\t\t// 通过算法验证签名是否正确\n\t\tif (false == jwt.verify(signer)) {\n\t\t\tthrow new ValidateException(\"Signature verification failed!\");\n\t\t}\n\t}\n\n\t/**\n\t * 检查JWT的以下三两个时间：\n\t *\n\t * <ul>\n\t *     <li>{@link JWTPayload#NOT_BEFORE}：生效时间不能晚于当前时间</li>\n\t *     <li>{@link JWTPayload#EXPIRES_AT}：失效时间不能早于当前时间</li>\n\t *     <li>{@link JWTPayload#ISSUED_AT}： 签发时间不能晚于当前时间</li>\n\t * </ul>\n\t * <p>\n\t * 如果某个时间没有设置，则不检查（表示无限制）\n\t *\n\t * @param payload {@link JWTPayload}\n\t * @param now     当前时间\n\t * @param leeway  容忍空间，单位：秒。当不能晚于当前时间时，向后容忍；不能早于向前容忍。\n\t * @throws ValidateException 验证异常\n\t */\n\tprivate static void validateDate(JWTPayload payload, Date now, long leeway) throws ValidateException {\n\t\tif (null == now) {\n\t\t\t// 默认当前时间\n\t\t\tnow = DateUtil.date();\n\t\t\t// truncate millis\n\t\t\tnow.setTime(now.getTime() / 1000 * 1000);\n\t\t}\n\n\t\t// 检查生效时间（生效时间不能晚于当前时间）\n\t\tfinal Date notBefore = payload.getClaimsJson().getDate(JWTPayload.NOT_BEFORE);\n\t\tvalidateNotAfter(JWTPayload.NOT_BEFORE, notBefore, now, leeway);\n\n\t\t// 检查失效时间（失效时间不能早于当前时间）\n\t\tfinal Date expiresAt = payload.getClaimsJson().getDate(JWTPayload.EXPIRES_AT);\n\t\tvalidateNotBefore(JWTPayload.EXPIRES_AT, expiresAt, now, leeway);\n\n\t\t// 检查签发时间（签发时间不能晚于当前时间）\n\t\tfinal Date issueAt = payload.getClaimsJson().getDate(JWTPayload.ISSUED_AT);\n\t\tvalidateNotAfter(JWTPayload.ISSUED_AT, issueAt, now, leeway);\n\t}\n\n\t/**\n\t * 验证指定字段的时间不能晚于当前时间<br>\n\t * 被检查的日期不存在则跳过\n\t *\n\t * @param fieldName   字段名\n\t * @param dateToCheck 被检查的字段日期\n\t * @param now         当前时间\n\t * @param leeway      容忍空间，单位：秒。向后容忍\n\t * @throws ValidateException 验证异常\n\t */\n\tprivate static void validateNotAfter(String fieldName, Date dateToCheck, Date now, long leeway) throws ValidateException {\n\t\tif (null == dateToCheck) {\n\t\t\treturn;\n\t\t}\n\t\tif(leeway > 0){\n\t\t\tnow = DateUtil.date(now.getTime() + leeway * 1000);\n\t\t}\n\t\tif (dateToCheck.after(now)) {\n\t\t\tthrow new ValidateException(\"'{}':[{}] is after now:[{}]\",\n\t\t\t\t\tfieldName, DateUtil.date(dateToCheck), DateUtil.date(now));\n\t\t}\n\t}\n\n\t/**\n\t * 验证指定字段的时间不能早于当前时间<br>\n\t * 被检查的日期不存在则跳过\n\t *\n\t * @param fieldName   字段名\n\t * @param dateToCheck 被检查的字段日期\n\t * @param now         当前时间\n\t * @param leeway      容忍空间，单位：秒。。向前容忍\n\t * @throws ValidateException 验证异常\n\t */\n\t@SuppressWarnings(\"SameParameterValue\")\n\tprivate static void validateNotBefore(String fieldName, Date dateToCheck, Date now, long leeway) throws ValidateException {\n\t\tif (null == dateToCheck) {\n\t\t\treturn;\n\t\t}\n\t\tif(leeway > 0){\n\t\t\tnow = DateUtil.date(now.getTime() - leeway * 1000);\n\t\t}\n\t\tif (dateToCheck.before(now)) {\n\t\t\tthrow new ValidateException(\"'{}':[{}] is before now:[{}]\",\n\t\t\t\t\tfieldName, DateUtil.date(dateToCheck), DateUtil.date(now));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/RegisteredPayload.java",
    "content": "package cn.hutool.jwt;\n\nimport java.util.Date;\n\n/**\n * 注册的标准载荷（Payload）声明\n *\n * @param <T> 实现此接口的类的类型\n * @author looly\n * @since 5.7.2\n */\npublic interface RegisteredPayload<T extends RegisteredPayload<T>> {\n\n\t/**\n\t * jwt签发者\n\t */\n\tString ISSUER = \"iss\";\n\t/**\n\t * jwt所面向的用户\n\t */\n\tString SUBJECT = \"sub\";\n\t/**\n\t * 接收jwt的一方\n\t */\n\tString AUDIENCE = \"aud\";\n\t/**\n\t * jwt的过期时间，这个过期时间必须要大于签发时间\n\t */\n\tString EXPIRES_AT = \"exp\";\n\t/**\n\t * 生效时间，定义在什么时间之前，该jwt都是不可用的.\n\t */\n\tString NOT_BEFORE = \"nbf\";\n\t/**\n\t * jwt的签发时间\n\t */\n\tString ISSUED_AT = \"iat\";\n\t/**\n\t * jwt的唯一身份标识，主要用来作为一次性token,从而回避重放攻击。\n\t */\n\tString JWT_ID = \"jti\";\n\n\t/**\n\t * 设置 jwt签发者(\"iss\")的Payload值\n\t *\n\t * @param issuer jwt签发者\n\t * @return this\n\t */\n\tdefault T setIssuer(String issuer) {\n\t\treturn setPayload(ISSUER, issuer);\n\t}\n\n\t/**\n\t * 设置jwt所面向的用户(\"sub\")的Payload值\n\t *\n\t * @param subject jwt所面向的用户\n\t * @return this\n\t */\n\tdefault T setSubject(String subject) {\n\t\treturn setPayload(SUBJECT, subject);\n\t}\n\n\t/**\n\t * 设置接收jwt的一方(\"aud\")的Payload值\n\t *\n\t * @param audience 接收jwt的一方\n\t * @return this\n\t */\n\tdefault T setAudience(String... audience) {\n\t\treturn setPayload(AUDIENCE, audience);\n\t}\n\n\t/**\n\t * 设置jwt的过期时间(\"exp\")的Payload值，这个过期时间必须要大于签发时间\n\t *\n\t * @param expiresAt jwt的过期时间\n\t * @return this\n\t * @see #setIssuedAt(Date)\n\t */\n\tdefault T setExpiresAt(Date expiresAt) {\n\t\treturn setPayload(EXPIRES_AT, expiresAt);\n\t}\n\n\t/**\n\t * 设置不可用时间点界限(\"nbf\")的Payload值\n\t *\n\t * @param notBefore 不可用时间点界限，在这个时间点之前，jwt不可用\n\t * @return this\n\t */\n\tdefault T setNotBefore(Date notBefore) {\n\t\treturn setPayload(NOT_BEFORE, notBefore);\n\t}\n\n\t/**\n\t * 设置jwt的签发时间(\"iat\")\n\t *\n\t * @param issuedAt 签发时间\n\t * @return this\n\t */\n\tdefault T setIssuedAt(Date issuedAt) {\n\t\treturn setPayload(ISSUED_AT, issuedAt);\n\t}\n\n\t/**\n\t * 设置jwt的唯一身份标识(\"jti\")\n\t *\n\t * @param jwtId 唯一身份标识\n\t * @return this\n\t */\n\tdefault T setJWTId(String jwtId) {\n\t\treturn setPayload(JWT_ID, jwtId);\n\t}\n\n\t/**\n\t * 设置Payload值\n\t *\n\t * @param name  payload名\n\t * @param value payload值\n\t * @return this\n\t */\n\tT setPayload(String name, Object value);\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/package-info.java",
    "content": "/**\n * JSON Web Token (JWT)封装\n */\npackage cn.hutool.jwt;\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/AlgorithmUtil.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.core.map.BiMap;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.crypto.asymmetric.SignAlgorithm;\nimport cn.hutool.crypto.digest.HmacAlgorithm;\n\nimport java.util.HashMap;\n\n/**\n * 算法工具类，算法和JWT算法ID对应表\n *\n * @author looly\n * @since 5.7.0\n */\npublic class AlgorithmUtil {\n\n\tprivate static final BiMap<String, String> map;\n\n\tstatic {\n\t\tmap = new BiMap<>(new HashMap<>());\n\t\tmap.put(\"HS256\", HmacAlgorithm.HmacSHA256.getValue());\n\t\tmap.put(\"HS384\", HmacAlgorithm.HmacSHA384.getValue());\n\t\tmap.put(\"HS512\", HmacAlgorithm.HmacSHA512.getValue());\n\n\t\tmap.put(\"HMD5\", HmacAlgorithm.HmacMD5.getValue());\n\t\tmap.put(\"HSHA1\", HmacAlgorithm.HmacSHA1.getValue());\n\t\tmap.put(\"SM4CMAC\", HmacAlgorithm.SM4CMAC.getValue());\n\n\n\t\tmap.put(\"RS256\", SignAlgorithm.SHA256withRSA.getValue());\n\t\tmap.put(\"RS384\", SignAlgorithm.SHA384withRSA.getValue());\n\t\tmap.put(\"RS512\", SignAlgorithm.SHA512withRSA.getValue());\n\n\t\tmap.put(\"ES256\", SignAlgorithm.SHA256withECDSA.getValue());\n\t\tmap.put(\"ES384\", SignAlgorithm.SHA384withECDSA.getValue());\n\t\tmap.put(\"ES512\", SignAlgorithm.SHA512withECDSA.getValue());\n\n\t\tmap.put(\"PS256\", SignAlgorithm.SHA256withRSA_PSS.getValue());\n\t\tmap.put(\"PS384\", SignAlgorithm.SHA384withRSA_PSS.getValue());\n\t\tmap.put(\"PS512\", SignAlgorithm.SHA512withRSA_PSS.getValue());\n\n\t\tmap.put(\"RMD2\", SignAlgorithm.MD2withRSA.getValue());\n\t\tmap.put(\"RMD5\", SignAlgorithm.MD5withRSA.getValue());\n\t\tmap.put(\"RSHA1\", SignAlgorithm.SHA1withRSA.getValue());\n\t\tmap.put(\"DNONE\", SignAlgorithm.NONEwithDSA.getValue());\n\t\tmap.put(\"DSHA1\", SignAlgorithm.SHA1withDSA.getValue());\n\t\tmap.put(\"ENONE\", SignAlgorithm.NONEwithECDSA.getValue());\n\t\tmap.put(\"ESHA1\", SignAlgorithm.SHA1withECDSA.getValue());\n\t}\n\n\t/**\n\t * 获取算法，用户传入算法ID返回算法名，传入算法名返回本身\n\t * @param idOrAlgorithm 算法ID或算法名\n\t * @return 算法名\n\t */\n\tpublic static String getAlgorithm(String idOrAlgorithm){\n\t\treturn ObjectUtil.defaultIfNull(getAlgorithmById(idOrAlgorithm), idOrAlgorithm);\n\t}\n\n\t/**\n\t * 获取算法ID，用户传入算法名返回ID，传入算法ID返回本身\n\t * @param idOrAlgorithm 算法ID或算法名\n\t * @return 算法ID\n\t */\n\tpublic static String getId(String idOrAlgorithm){\n\t\treturn ObjectUtil.defaultIfNull(getIdByAlgorithm(idOrAlgorithm), idOrAlgorithm);\n\t}\n\n\t/**\n\t * 根据JWT算法ID获取算法\n\t *\n\t * @param id JWT算法ID\n\t * @return 算法\n\t */\n\tprivate static String getAlgorithmById(String id) {\n\t\treturn map.get(id.toUpperCase());\n\t}\n\n\t/**\n\t * 根据算法获取JWT算法ID\n\t *\n\t * @param algorithm 算法\n\t * @return JWT算法ID\n\t */\n\tprivate static String getIdByAlgorithm(String algorithm) {\n\t\treturn map.getKey(algorithm);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/AsymmetricJWTSigner.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.asymmetric.Sign;\n\nimport java.nio.charset.Charset;\nimport java.security.Key;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * 非对称加密JWT签名封装\n *\n * @author looly\n * @since 5.7.0\n */\npublic class AsymmetricJWTSigner implements JWTSigner {\n\n\tprivate Charset charset = CharsetUtil.CHARSET_UTF_8;\n\tprivate final Sign sign;\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法字符串表示\n\t * @param key       公钥{@link PublicKey}或私钥{@link PrivateKey}，公钥用于验证签名，私钥用于产生签名\n\t */\n\tpublic AsymmetricJWTSigner(String algorithm, Key key) {\n\t\tfinal PublicKey publicKey = key instanceof PublicKey ? (PublicKey) key : null;\n\t\tfinal PrivateKey privateKey = key instanceof PrivateKey ? (PrivateKey) key : null;\n\t\tthis.sign = new Sign(algorithm, privateKey, publicKey);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法字符串表示\n\t * @param keyPair   密钥对\n\t */\n\tpublic AsymmetricJWTSigner(String algorithm, KeyPair keyPair) {\n\t\tthis.sign = new Sign(algorithm, keyPair);\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return 编码\n\t */\n\tpublic AsymmetricJWTSigner setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String sign(String headerBase64, String payloadBase64) {\n\t\tfinal String dataStr = StrUtil.format(\"{}.{}\", headerBase64, payloadBase64);\n\t\treturn Base64.encodeUrlSafe(sign(StrUtil.bytes(dataStr, charset)));\n\t}\n\n\t/**\n\t * 签名字符串数据\n\t *\n\t * @param data 数据\n\t * @return 签名\n\t */\n\tprotected byte[] sign(byte[] data) {\n\t\treturn sign.sign(data);\n\t}\n\n\t@Override\n\tpublic boolean verify(String headerBase64, String payloadBase64, String signBase64) {\n\t\treturn verify(\n\t\t\tStrUtil.bytes(StrUtil.format(\"{}.{}\", headerBase64, payloadBase64), charset),\n\t\t\tBase64.decode(signBase64));\n\t}\n\n\t/**\n\t * 验签数据\n\t *\n\t * @param data   数据\n\t * @param signed 签名\n\t * @return 是否通过\n\t */\n\tprotected boolean verify(byte[] data, byte[] signed) {\n\t\treturn sign.verify(data, signed);\n\t}\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn this.sign.getSignature().getAlgorithm();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/EllipticCurveJWTSigner.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.jwt.JWTException;\n\nimport java.security.Key;\nimport java.security.KeyPair;\n\n/**\n * 椭圆曲线（Elliptic Curve）的JWT签名器。<br>\n * 按照https://datatracker.ietf.org/doc/html/rfc7518#section-3.4,<br>\n * Elliptic Curve Digital Signature Algorithm (ECDSA)算法签名需要转换DER格式为pair (R, S)\n *\n * @author looly\n * @since 5.8.21\n */\npublic class EllipticCurveJWTSigner extends AsymmetricJWTSigner {\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param key       密钥\n\t */\n\tpublic EllipticCurveJWTSigner(String algorithm, Key key) {\n\t\tsuper(algorithm, key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm 算法\n\t * @param keyPair   密钥对\n\t */\n\tpublic EllipticCurveJWTSigner(String algorithm, KeyPair keyPair) {\n\t\tsuper(algorithm, keyPair);\n\t}\n\n\t@Override\n\tprotected byte[] sign(final byte[] data) {\n\t\t// https://datatracker.ietf.org/doc/html/rfc7518#section-3.4\n\t\treturn derToConcat(super.sign(data), getSignatureByteArrayLength(getAlgorithm()));\n\t}\n\n\t@Override\n\tprotected boolean verify(final byte[] data, final byte[] signed) {\n\t\t// https://datatracker.ietf.org/doc/html/rfc7518#section-3.4\n\t\treturn super.verify(data, concatToDER(signed));\n\t}\n\n\t/**\n\t * 获取签名长度\n\t * @param alg 算法\n\t * @return 长度\n\t * @throws JWTException JWT异常\n\t */\n\tprivate static int getSignatureByteArrayLength(final String alg) throws JWTException {\n\t\tswitch (alg) {\n\t\t\tcase \"ES256\":\n\t\t\tcase \"SHA256withECDSA\":\n\t\t\t\treturn 64;\n\t\t\tcase \"ES384\":\n\t\t\tcase \"SHA384withECDSA\":\n\t\t\t\treturn 96;\n\t\t\tcase \"ES512\":\n\t\t\tcase \"SHA512withECDSA\":\n\t\t\t\treturn 132;\n\t\t\tdefault:\n\t\t\t\tthrow new JWTException(\"Unsupported Algorithm: {}\", alg);\n\t\t}\n\t}\n\n\tprivate static byte[] derToConcat(final byte[] derSignature, int outputLength) throws JWTException {\n\n\t\tif (derSignature.length < 8 || derSignature[0] != 48) {\n\t\t\tthrow new JWTException(\"Invalid ECDSA signature format\");\n\t\t}\n\n\t\tfinal int offset;\n\t\tif (derSignature[1] > 0) {\n\t\t\toffset = 2;\n\t\t} else if (derSignature[1] == (byte) 0x81) {\n\t\t\toffset = 3;\n\t\t} else {\n\t\t\tthrow new JWTException(\"Invalid ECDSA signature format\");\n\t\t}\n\n\t\tfinal byte rLength = derSignature[offset + 1];\n\n\t\tint i = rLength;\n\t\twhile ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) {\n\t\t\ti--;\n\t\t}\n\n\t\tfinal byte sLength = derSignature[offset + 2 + rLength + 1];\n\n\t\tint j = sLength;\n\t\twhile ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) {\n\t\t\tj--;\n\t\t}\n\n\t\tint rawLen = Math.max(i, j);\n\t\trawLen = Math.max(rawLen, outputLength / 2);\n\n\t\tif ((derSignature[offset - 1] & 0xff) != derSignature.length - offset\n\t\t\t|| (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength\n\t\t\t|| derSignature[offset] != 2\n\t\t\t|| derSignature[offset + 2 + rLength] != 2) {\n\t\t\tthrow new JWTException(\"Invalid ECDSA signature format\");\n\t\t}\n\n\t\tfinal byte[] concatSignature = new byte[2 * rawLen];\n\n\t\tSystem.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i);\n\t\tSystem.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j);\n\n\t\treturn concatSignature;\n\t}\n\n\tprivate static byte[] concatToDER(byte[] jwsSignature) throws ArrayIndexOutOfBoundsException {\n\n\t\tint rawLen = jwsSignature.length / 2;\n\n\t\tint i = rawLen;\n\n\t\twhile ((i > 0) && (jwsSignature[rawLen - i] == 0)) {\n\t\t\ti--;\n\t\t}\n\n\t\tint j = i;\n\n\t\tif (jwsSignature[rawLen - i] < 0) {\n\t\t\tj += 1;\n\t\t}\n\n\t\tint k = rawLen;\n\n\t\twhile ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) {\n\t\t\tk--;\n\t\t}\n\n\t\tint l = k;\n\n\t\tif (jwsSignature[2 * rawLen - k] < 0) {\n\t\t\tl += 1;\n\t\t}\n\n\t\tint len = 2 + j + 2 + l;\n\n\t\tif (len > 255) {\n\t\t\tthrow new JWTException(\"Invalid ECDSA signature format\");\n\t\t}\n\n\t\tint offset;\n\n\t\tfinal byte[] derSignature;\n\n\t\tif (len < 128) {\n\t\t\tderSignature = new byte[2 + 2 + j + 2 + l];\n\t\t\toffset = 1;\n\t\t} else {\n\t\t\tderSignature = new byte[3 + 2 + j + 2 + l];\n\t\t\tderSignature[1] = (byte) 0x81;\n\t\t\toffset = 2;\n\t\t}\n\n\t\tderSignature[0] = 48;\n\t\tderSignature[offset++] = (byte) len;\n\t\tderSignature[offset++] = 2;\n\t\tderSignature[offset++] = (byte) j;\n\n\t\tSystem.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i);\n\n\t\toffset += j;\n\n\t\tderSignature[offset++] = 2;\n\t\tderSignature[offset++] = (byte) l;\n\n\t\tSystem.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k);\n\n\t\treturn derSignature;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/HMacJWTSigner.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.digest.HMac;\n\nimport java.nio.charset.Charset;\nimport java.security.Key;\n\n/**\n * HMac算法签名实现\n *\n * @author looly\n * @since 5.7.0\n */\npublic class HMacJWTSigner implements JWTSigner {\n\n\tprivate Charset charset = CharsetUtil.CHARSET_UTF_8;\n\tprivate final HMac hMac;\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm HMAC签名算法\n\t * @param key       密钥\n\t */\n\tpublic HMacJWTSigner(String algorithm, byte[] key) {\n\t\tthis.hMac = new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param algorithm HMAC签名算法\n\t * @param key       密钥\n\t */\n\tpublic HMacJWTSigner(String algorithm, Key key) {\n\t\tthis.hMac = new HMac(algorithm, key);\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return 编码\n\t */\n\tpublic HMacJWTSigner setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String sign(String headerBase64, String payloadBase64) {\n\t\treturn hMac.digestBase64(StrUtil.format(\"{}.{}\", headerBase64, payloadBase64), charset, true);\n\t}\n\n\t@Override\n\tpublic boolean verify(String headerBase64, String payloadBase64, String signBase64) {\n\t\tfinal String sign = sign(headerBase64, payloadBase64);\n\t\treturn hMac.verify(\n\t\t\t\tStrUtil.bytes(sign, charset),\n\t\t\t\tStrUtil.bytes(signBase64, charset));\n\t}\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn this.hMac.getAlgorithm();\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSigner.java",
    "content": "package cn.hutool.jwt.signers;\n\n/**\n * JWT签名接口封装，通过实现此接口，完成不同算法的签名功能\n *\n * @author looly\n */\npublic interface JWTSigner {\n\n\t/**\n\t * 签名\n\t *\n\t * @param headerBase64  JWT头的JSON字符串的Base64表示\n\t * @param payloadBase64 JWT载荷的JSON字符串Base64表示\n\t * @return 签名结果Base64，即JWT的第三部分\n\t */\n\tString sign(String headerBase64, String payloadBase64);\n\n\t/**\n\t * 验签\n\t *\n\t * @param headerBase64  JWT头的JSON字符串Base64表示\n\t * @param payloadBase64 JWT载荷的JSON字符串Base64表示\n\t * @param signBase64    被验证的签名Base64表示\n\t * @return 签名是否一致\n\t */\n\tboolean verify(String headerBase64, String payloadBase64, String signBase64);\n\n\t/**\n\t * 获取算法\n\t *\n\t * @return 算法\n\t */\n\tString getAlgorithm();\n\n\t/**\n\t * 获取算法ID，即算法的简写形式，如HS256\n\t *\n\t * @return 算法ID\n\t * @since 5.7.2\n\t */\n\tdefault String getAlgorithmId() {\n\t\treturn AlgorithmUtil.getId(getAlgorithm());\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSignerUtil.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.jwt.JWTException;\n\nimport java.security.Key;\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n/**\n * JWT签名器工具类\n *\n * @author looly\n * @since 5.7.0\n */\npublic class JWTSignerUtil {\n\n\t/**\n\t * 无签名\n\t *\n\t * @return 无签名的签名器\n\t */\n\tpublic static JWTSigner none() {\n\t\treturn NoneJWTSigner.NONE;\n\t}\n\n\t//------------------------------------------------------------------------- HSxxx\n\n\t/**\n\t * HS256(HmacSHA256)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner hs256(byte[] key) {\n\t\treturn createSigner(\"HS256\", key);\n\t}\n\n\t/**\n\t * HS384(HmacSHA384)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner hs384(byte[] key) {\n\t\treturn createSigner(\"HS384\", key);\n\t}\n\n\t/**\n\t * HS512(HmacSHA512)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner hs512(byte[] key) {\n\t\treturn createSigner(\"HS512\", key);\n\t}\n\n\t//------------------------------------------------------------------------- RSxxx\n\n\t/**\n\t * RS256(SHA256withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rs256(Key key) {\n\t\treturn createSigner(\"RS256\", key);\n\t}\n\n\t/**\n\t * RS384(SHA384withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rs384(Key key) {\n\t\treturn createSigner(\"RS384\", key);\n\t}\n\n\t/**\n\t * RS512(SHA512withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rs512(Key key) {\n\t\treturn createSigner(\"RS512\", key);\n\t}\n\n\t//------------------------------------------------------------------------- ESxxx\n\n\t/**\n\t * ES256(SHA256withECDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner es256(Key key) {\n\t\treturn createSigner(\"ES256\", key);\n\t}\n\n\t/**\n\t * ES384(SHA383withECDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner es384(Key key) {\n\t\treturn createSigner(\"ES384\", key);\n\t}\n\n\t/**\n\t * ES512(SHA512withECDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner es512(Key key) {\n\t\treturn createSigner(\"ES512\", key);\n\t}\n\n\t/**\n\t * HMD5(HmacMD5)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner hmd5(Key key) {\n\t\treturn createSigner(\"HMD5\",key);\n\t}\n\n\t/**\n\t * HSHA1(HmacSHA1)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner hsha1(Key key) {\n\t\treturn createSigner(\"HSHA1\",key);\n\t}\n\n\t/**\n\t * SM4CMAC(SM4CMAC)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner sm4cmac(Key key) {\n\t\treturn createSigner(\"SM4CMAC\",key);\n\t}\n\n\t/**\n\t * RMD2(MD2withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rmd2(Key key) {\n\t\treturn createSigner(\"RMD2\",key);\n\t}\n\n\t/**\n\t * RMD5(MD5withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rmd5(Key key) {\n\t\treturn createSigner(\"RMD5\",key);\n\t}\n\n\t/**\n\t * RSHA1(SHA1withRSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner rsha1(Key key) {\n\t\treturn createSigner(\"RSHA1\",key);\n\t}\n\n\t/**\n\t * DNONE(NONEwithDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner dnone(Key key) {\n\t\treturn createSigner(\"DNONE\",key);\n\t}\n\n\t/**\n\t * DSHA1(SHA1withDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner dsha1(Key key) {\n\t\treturn createSigner(\"DSHA1\",key);\n\t}\n\n\t/**\n\t * ENONE(NONEwithECDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner enone(Key key) {\n\t\treturn createSigner(\"ENONE\",key);\n\t}\n\n\t/**\n\t * ESHA1(SHA1withECDSA)签名器\n\t *\n\t * @param key 密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner esha1(Key key) {\n\t\treturn createSigner(\"ESHA1\",key);\n\t}\n\n\n\n\n\t/**\n\t * 创建签名器\n\t *\n\t * @param algorithmId 算法ID，见{@link AlgorithmUtil}\n\t * @param key         密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner createSigner(String algorithmId, byte[] key) {\n\t\tif (NoneJWTSigner.isNone(algorithmId)) {\n\t\t\tif(null ==  key){\n\t\t\t\treturn none();\n\t\t\t}else{\n\t\t\t\tthrow new JWTException(\"When key is not null, algorithmId must not be none.\");\n\t\t\t}\n\t\t}\n\t\treturn new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);\n\t}\n\n\t/**\n\t * 创建签名器\n\t *\n\t * @param algorithmId 算法ID，见{@link AlgorithmUtil}\n\t * @param keyPair     密钥对\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner createSigner(String algorithmId, KeyPair keyPair) {\n\t\tif (NoneJWTSigner.isNone(algorithmId)) {\n\t\t\tif(null ==  keyPair){\n\t\t\t\treturn none();\n\t\t\t}else{\n\t\t\t\tthrow new JWTException(\"When key is not null, algorithmId must not be none.\");\n\t\t\t}\n\t\t}\n\n\t\t// issue3205@Github\n\t\tif(ReUtil.isMatch(\"es\\\\d{3}\", algorithmId.toLowerCase())){\n\t\t\treturn new EllipticCurveJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), keyPair);\n\t\t}\n\n\t\treturn new AsymmetricJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), keyPair);\n\t}\n\n\t/**\n\t * 创建签名器\n\t *\n\t * @param algorithmId 算法ID，见{@link AlgorithmUtil}\n\t * @param key         密钥\n\t * @return 签名器\n\t */\n\tpublic static JWTSigner createSigner(String algorithmId, Key key) {\n\t\tif (NoneJWTSigner.isNone(algorithmId)) {\n\t\t\tif(null ==  key){\n\t\t\t\treturn none();\n\t\t\t}else{\n\t\t\t\tthrow new JWTException(\"When key is not null, algorithmId must not be none.\");\n\t\t\t}\n\t\t}\n\n\t\tif (key instanceof PrivateKey || key instanceof PublicKey) {\n\t\t\t// issue3205@Github\n\t\t\tif(ReUtil.isMatch(\"ES\\\\d{3}\", algorithmId)){\n\t\t\t\treturn new EllipticCurveJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);\n\t\t\t}\n\n\t\t\treturn new AsymmetricJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);\n\t\t}\n\t\treturn new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/NoneJWTSigner.java",
    "content": "package cn.hutool.jwt.signers;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 无需签名的JWT签名器\n *\n * @author looly\n * @since 5.7.0\n */\npublic class NoneJWTSigner implements JWTSigner {\n\n\t/**\n\t * 定义一个常量ID_NONE，表示没有ID的情况\n\t */\n\tpublic static final String ID_NONE = \"none\";\n\n\t/**\n\t * 创建一个NoneJWTSigner实例，用于处理没有签名的JWT\n\t */\n\tpublic static NoneJWTSigner NONE = new NoneJWTSigner();\n\n\t/**\n\t * 判断给定的算法是否为无签名的算法\n\t *\n\t * @param alg 算法\n\t * @return 如果是无签名的算法，则返回true；否则返回false\n\t * @since 5.8.42\n\t */\n\tpublic static boolean isNone(final String alg) {\n\t\treturn StrUtil.isBlank( alg) || StrUtil.equalsIgnoreCase(alg, ID_NONE);\n\t}\n\n\t@Override\n\tpublic String sign(String headerBase64, String payloadBase64) {\n\t\treturn StrUtil.EMPTY;\n\t}\n\n\t@Override\n\tpublic boolean verify(String headerBase64, String payloadBase64, String signBase64) {\n\t\treturn StrUtil.isEmpty(signBase64);\n\t}\n\n\t@Override\n\tpublic String getAlgorithm() {\n\t\treturn ID_NONE;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/main/java/cn/hutool/jwt/signers/package-info.java",
    "content": "/**\n * JWT签名封装\n */\npackage cn.hutool.jwt.signers;\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/Issue3205Test.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.jwt.signers.AlgorithmUtil;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport io.jsonwebtoken.Jwts;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.security.KeyPair;\n\n/**\n *https://github.com/chinabugotech/hutool/issues/3205\n */\npublic class Issue3205Test {\n\t@Test\n\tpublic void es256Test() {\n\t\tfinal String id = \"es256\";\n\t\tfinal KeyPair keyPair = KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id));\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, keyPair);\n\n\t\tfinal JWT jwt = JWT.create()\n\t\t\t.setPayload(\"sub\", \"1234567890\")\n\t\t\t.setPayload(\"name\", \"looly\")\n\t\t\t.setPayload(\"admin\", true)\n\t\t\t.setExpiresAt(DateUtil.tomorrow())\n\t\t\t.setSigner(signer);\n\n\t\tfinal String token = jwt.sign();\n\n\t\tfinal boolean signed = Jwts.parser().verifyWith(keyPair.getPublic()).build().isSigned(token);\n\n\t\tassertTrue(signed);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/Issue3732Test.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class Issue3732Test {\n\t@Test\n\tvoid hmacTest() {\n\t\tfinal JWTSigner SIGNER = JWTSignerUtil.hs256(\"6sf2f5j2a62a3s8f9032hsf\".getBytes());\n\t\tMap<String, Object> payload = new HashMap<>();\n\t\tpayload.put(\"name\", \"test\");\n\t\tpayload.put(\"role\", \"admin\");\n\n\t\t// 创建 JWT token\n\t\tString token = JWTUtil.createToken(payload, SIGNER);\n\t\tassertEquals(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoidGVzdCJ9.eS1hjkb2ympf7Gtnh_Xmzmb29bXt3J-1SyNTLMBipbY\", token);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/Issue4105Test.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.codec.Base64;\nimport cn.hutool.core.util.StrUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\n\npublic class Issue4105Test {\n\n\t@Test\n\tvoid verifyNoneTest() {\n\t\t// {\"alg\": \"none\"}.{\"exp\": 1642196407}\n\t\t// 当定义alg为none时，校验总是成功\n\t\tString head = Base64.encode(\"{\\\"alg\\\": \\\"none\\\"}\");\n\t\tString payload = Base64.encode(\"{\\\"exp\\\": 1642196407}\");\n\t\tString token = StrUtil.format(\"{}.{}.\", head, payload);\n\n\t\tfinal JWT jwt = JWTUtil.parseToken(token);\n\t\tAssertions.assertNull(jwt.getSigner());\n\t\t// 对于签名为none的JWT，verify()方法总是返回true\n\t\tAssertions.assertTrue(jwt.verify());\n\n\t\t// 对于签名为none的JWT，但是定义了key，不一致报错\n\t\tfinal JWT jwt2 = JWTUtil.parseToken(token);\n\t\tAssertions.assertThrows(JWTException.class, ()-> jwt2.setKey(\"123\".getBytes(StandardCharsets.UTF_8)).verify());\n\t}\n\n\t@Test\n\tvoid verifyEmptyTest() {\n\t\t// {\"alg\": \"none\"}.{\"exp\": 1642196407}\n\t\t// 当定义alg为none时，校验总是成功\n\t\tString head = Base64.encode(\"{\\\"alg\\\": \\\"\\\"}\");\n\t\tString payload = Base64.encode(\"{\\\"exp\\\": 1642196407}\");\n\t\tString token = StrUtil.format(\"{}.{}.\", head, payload);\n\n\t\tfinal JWT jwt = JWTUtil.parseToken(token);\n\t\tAssertions.assertNull(jwt.getSigner());\n\t\t// 对于签名为none的JWT，verify()方法总是返回true\n\t\tAssertions.assertTrue(jwt.verify());\n\n\t\t// 对于签名为none的JWT，但是定义了key，不一致报错\n\t\tfinal JWT jwt2 = JWTUtil.parseToken(token);\n\t\tAssertions.assertThrows(JWTException.class, ()-> jwt2.setKey(\"123\".getBytes(StandardCharsets.UTF_8)).verify());\n\t}\n\n\t@Test\n\tvoid verifyHs256Test() {\n\t\t// {\"alg\": \"none\"}.{\"exp\": 1642196407}\n\t\t// 当定义alg为none时，校验总是成功\n\t\tString head = Base64.encode(\"{\\\"alg\\\": \\\"HS256\\\"}\");\n\t\tString payload = Base64.encode(\"{\\\"exp\\\": 1642196407}\");\n\t\tString token = StrUtil.format(\"{}.{}.\", head, payload);\n\n\t\tfinal JWT jwt = JWTUtil.parseToken(token);\n\t\tAssertions.assertNull(jwt.getSigner());\n\n\t\t// 未定义签名器或key，但是JWT中要求了签名算法，异常\n\t\tAssertions.assertThrows(JWTException.class, jwt::verify);\n\n\t\t// 手动定义签名器，但是签名部分为空或不一致，返回false\n\t\tfinal JWT jwt2 = JWTUtil.parseToken(token);\n\t\tAssertions.assertFalse(jwt2.setKey(\"123\".getBytes(StandardCharsets.UTF_8)).verify());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/IssueI5QRUOTest.java",
    "content": "package cn.hutool.jwt;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class IssueI5QRUOTest {\n\n\t@Test\n\tpublic void createTokenTest(){\n\t\t// https://jwt.io/\n\n\t\t// 自定义header顺序\n\t\tfinal Map<String, Object> header = new LinkedHashMap<String, Object>(){\n\t\t\t{\n\t\t\t\tput(JWTHeader.ALGORITHM, \"HS384\");\n\t\t\t\tput(JWTHeader.TYPE, \"JWT\");\n\t\t\t}\n\t\t};\n\n\t\tfinal Map<String, Object> payload = new LinkedHashMap<String, Object>(){\n\t\t\t{\n\t\t\t\tput(\"sub\", \"1234567890\");\n\t\t\t\tput(\"name\", \"John Doe\");\n\t\t\t\tput(\"iat\", 1516239022);\n\t\t\t}\n\t\t};\n\n\t\tfinal String token = JWTUtil.createToken(header, payload, \"123456\".getBytes());\n\t\tassertEquals(\"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.\" +\n\t\t\t\t\"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.\" +\n\t\t\t\t\"3Ywq9NlR3cBST4nfcdbR-fcZ8374RHzU50X6flKvG-tnWFMalMaHRm3cMpXs1NrZ\", token);\n\n\t\tfinal boolean verify = JWT.of(token).setKey(\"123456\".getBytes()).verify();\n\t\tassertTrue(verify);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/IssueI6IS5BTest.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.date.LocalDateTimeUtil;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDateTime;\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueI6IS5BTest {\n\t@Test\n\tpublic void payloadToBeanTest() {\n\t\tfinal LocalDateTime iat = LocalDateTimeUtil.of(DateUtil.parse(\"2023-03-03\"));\n\t\tfinal JwtToken jwtToken = new JwtToken();\n\t\tjwtToken.setIat(iat);\n\t\tfinal String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), \"123\".getBytes(StandardCharsets.UTF_8));\n\t\tassertEquals(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.W88PB2ovAqCXV4QdbeKbdFW-P057xOTXEosD8hbOa9U\", token);\n\t\tfinal JSONObject payloads = JWTUtil.parseToken(token).getPayloads();\n\t\tassertEquals(\"{\\\"iat\\\":1677772800}\", payloads.toString());\n\t\tfinal JwtToken o = payloads.toBean(JwtToken.class);\n\t\tassertEquals(iat, o.getIat());\n\t}\n\n\t@Data\n\tstatic class JwtToken {\n\t\tprivate LocalDateTime iat;\n\t}\n\n\t@Test\n\tpublic void payloadToBeanTest2() {\n\t\tfinal Date iat = DateUtil.parse(\"2023-03-03\");\n\t\tfinal JwtToken2 jwtToken = new JwtToken2();\n\t\tjwtToken.setIat(iat);\n\t\tfinal String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), \"123\".getBytes(StandardCharsets.UTF_8));\n\t\tassertEquals(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.W88PB2ovAqCXV4QdbeKbdFW-P057xOTXEosD8hbOa9U\", token);\n\t\tfinal JSONObject payloads = JWTUtil.parseToken(token).getPayloads();\n\t\tassertEquals(\"{\\\"iat\\\":1677772800}\", payloads.toString());\n\t\tfinal JwtToken2 o = payloads.toBean(JwtToken2.class);\n\t\tassertEquals(iat, o.getIat());\n\t}\n\n\t@Data\n\tstatic class JwtToken2 {\n\t\tprivate Date iat;\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/JWTSignerTest.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.crypto.KeyUtil;\nimport cn.hutool.jwt.signers.AlgorithmUtil;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class JWTSignerTest {\n\n\t@Test\n\tpublic void hs256Test(){\n\t\tString id = \"hs256\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void hs256Test2(){\n\t\tfinal JWTSigner signer = JWTSignerUtil.hs256(\"123456\".getBytes());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void hs384Test(){\n\t\tString id = \"hs384\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void hs512Test(){\n\t\tString id = \"hs512\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rs256Test(){\n\t\tString id = \"rs256\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rs384Test(){\n\t\tString id = \"rs384\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rs512Test(){\n\t\tString id = \"rs512\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void es256Test(){\n\t\tString id = \"es256\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void es384Test(){\n\t\tString id = \"es384\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void es512Test(){\n\t\tString id = \"es512\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void ps256Test(){\n\t\tString id = \"ps256\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void ps384Test(){\n\t\tString id = \"ps384\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void hmd5Test(){\n\t\tString id = \"hmd5\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void hsha1Test(){\n\t\tString id = \"hsha1\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void sm4cmacTest(){\n\t\tString id = \"sm4cmac\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKey(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rmd2Test(){\n\t\tString id = \"rmd2\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rmd5Test(){\n\t\tString id = \"rmd5\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void rsha1Test(){\n\t\tString id = \"rsha1\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void dnoneTest(){\n\t\tString id = \"dnone\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void dsha1Test(){\n\t\tString id = \"dsha1\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void enoneTest(){\n\t\tString id = \"enone\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\t@Test\n\tpublic void esha1Test(){\n\t\tString id = \"esha1\";\n\t\tfinal JWTSigner signer = JWTSignerUtil.createSigner(id, KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));\n\t\tassertEquals(AlgorithmUtil.getAlgorithm(id), signer.getAlgorithm());\n\n\t\tsignAndVerify(signer);\n\t}\n\n\n\n\tprivate static void signAndVerify(JWTSigner signer){\n\t\tJWT jwt = JWT.create()\n\t\t\t\t.setPayload(\"sub\", \"1234567890\")\n\t\t\t\t.setPayload(\"name\", \"looly\")\n\t\t\t\t.setPayload(\"admin\", true)\n\t\t\t\t.setExpiresAt(DateUtil.tomorrow())\n\t\t\t\t.setSigner(signer);\n\n\t\tString token = jwt.sign();\n\t\tassertTrue(JWT.of(token).verify(signer));\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/JWTTest.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JWTTest {\n\n\t@Test\n\tpublic void createHs256Test() {\n\t\tfinal byte[] key = \"1234567890\".getBytes();\n\t\tfinal JWT jwt = JWT.create()\n\t\t\t.setPayload(\"sub\", \"1234567890\")\n\t\t\t.setPayload(\"name\", \"looly\")\n\t\t\t.setPayload(\"admin\", true)\n\t\t\t.setExpiresAt(DateUtil.parse(\"2022-01-01\"))\n\t\t\t.setKey(key);\n\n\t\tfinal String rightToken = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\" +\n\t\t\t\"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWUsImV4cCI6MTY0MDk2NjQwMH0.\" +\n\t\t\t\"8siIwEMHf-DRyUjVElS_yipb6Mo3c1z0wFiheGXWGQw\";\n\n\t\tfinal String token = jwt.sign();\n\t\tassertEquals(rightToken, token);\n\n\t\tassertTrue(JWT.of(rightToken).setKey(key).verify());\n\t}\n\n\t@Test\n\tpublic void parseTest() {\n\t\tfinal String rightToken = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.\" +\n\t\t\t\"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.\" +\n\t\t\t\"U2aQkC2THYV9L0fTN-yBBI7gmo5xhmvMhATtu8v0zEA\";\n\n\t\tfinal JWT jwt = JWT.of(rightToken);\n\n\t\tassertTrue(jwt.setKey(\"1234567890\".getBytes()).verify());\n\n\t\t//header\n\t\tassertEquals(\"JWT\", jwt.getHeader(JWTHeader.TYPE));\n\t\tassertEquals(\"HS256\", jwt.getHeader(JWTHeader.ALGORITHM));\n\t\tassertNull(jwt.getHeader(JWTHeader.CONTENT_TYPE));\n\n\t\t//payload\n\t\tassertEquals(\"1234567890\", jwt.getPayload(\"sub\"));\n\t\tassertEquals(\"looly\", jwt.getPayload(\"name\"));\n\t\tassertEquals(true, jwt.getPayload(\"admin\"));\n\t}\n\n\t@Test\n\tpublic void createNoneTest() {\n\t\tfinal JWT jwt = JWT.create()\n\t\t\t.setPayload(\"sub\", \"1234567890\")\n\t\t\t.setPayload(\"name\", \"looly\")\n\t\t\t.setPayload(\"admin\", true)\n\t\t\t.setSigner(JWTSignerUtil.none());\n\n\t\tfinal String rightToken = \"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.\" +\n\t\t\t\"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWV9.\";\n\n\t\tfinal String token = jwt.sign();\n\t\tassertEquals(rightToken, token);\n\n\t\tassertTrue(JWT.of(rightToken).setSigner(JWTSignerUtil.none()).verify());\n\t}\n\n\t/**\n\t * 必须定义签名器\n\t */\n\t@Test\n\tpublic void needSignerTest() {\n\t\tassertThrows(JWTException.class, () -> {\n\t\t\tfinal JWT jwt = JWT.create()\n\t\t\t\t.setPayload(\"sub\", \"1234567890\")\n\t\t\t\t.setPayload(\"name\", \"looly\")\n\t\t\t\t.setPayload(\"admin\", true);\n\n\t\t\tjwt.sign();\n\t\t});\n\t}\n\n\t@Test\n\tpublic void verifyTest() {\n\t\tfinal String token = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\" +\n\t\t\t\"eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MjQwMDQ4MjIsInVzZXJJZCI6MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV_op5LoibLkuozlj7ciLCJzeXNfbWVudV8xIiwiUk9MRV_op5LoibLkuIDlj7ciLCJzeXNfbWVudV8yIl0sImp0aSI6ImQ0YzVlYjgwLTA5ZTctNGU0ZC1hZTg3LTVkNGI5M2FhNmFiNiIsImNsaWVudF9pZCI6ImhhbmR5LXNob3AifQ.\" +\n\t\t\t\"aixF1eKlAKS_k3ynFnStE7-IRGiD5YaqznvK2xEjBew\";\n\n\t\tfinal boolean verify = JWT.of(token).setKey(StrUtil.utf8Bytes(\"123456\")).verify();\n\t\tassertTrue(verify);\n\t}\n\n\t@Test\n\tpublic void getLongTest() {\n\t\tfinal String rightToken = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9\"\n\t\t\t+ \".eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhZG1pbiIsImRldmljZSI6ImRlZmF1bHQtZGV2aWNlIiwiZWZmIjoxNjc4Mjg1NzEzOTM1LCJyblN0ciI6IkVuMTczWFhvWUNaaVZUWFNGOTNsN1pabGtOalNTd0pmIn0\"\n\t\t\t+ \".wRe2soTaWYPhwcjxdzesDi1BgEm9D61K-mMT3fPc4YM\";\n\n\t\tfinal JWT jwt = JWTUtil.parseToken(rightToken);\n\n\t\tassertEquals(\n\t\t\t\"{\\\"loginType\\\":\\\"login\\\",\\\"loginId\\\":\\\"admin\\\",\\\"device\\\":\\\"default-device\\\",\" +\n\t\t\t\t\"\\\"eff\\\":1678285713935,\\\"rnStr\\\":\\\"En173XXoYCZiVTXSF93l7ZZlkNjSSwJf\\\"}\",\n\t\t\tjwt.getPayloads().toString());\n\t\tassertEquals(Long.valueOf(1678285713935L), jwt.getPayloads().getLong(\"eff\"));\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/JWTUtilTest.java",
    "content": "package cn.hutool.jwt;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JWTUtilTest {\n\n\t@Test\n\tpublic void createTest(){\n\t\tbyte[] key = \"1234\".getBytes();\n\t\tMap<String, Object> map = new HashMap<String, Object>() {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\t\t\t{\n\t\t\t\tput(\"uid\", Integer.parseInt(\"123\"));\n\t\t\t\tput(\"expire_time\", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);\n\n\t\t\t}\n\t\t};\n\n\t\tJWTUtil.createToken(map, key);\n\t}\n\n\t@Test\n\tpublic void parseTest(){\n\t\tString rightToken = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.\" +\n\t\t\t\t\"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.\" +\n\t\t\t\t\"U2aQkC2THYV9L0fTN-yBBI7gmo5xhmvMhATtu8v0zEA\";\n\t\tfinal JWT jwt = JWTUtil.parseToken(rightToken);\n\n\t\tassertTrue(jwt.setKey(\"1234567890\".getBytes()).verify());\n\n\t\t//header\n\t\tassertEquals(\"JWT\", jwt.getHeader(JWTHeader.TYPE));\n\t\tassertEquals(\"HS256\", jwt.getHeader(JWTHeader.ALGORITHM));\n\t\tassertNull(jwt.getHeader(JWTHeader.CONTENT_TYPE));\n\n\t\t//payload\n\t\tassertEquals(\"1234567890\", jwt.getPayload(\"sub\"));\n\t\tassertEquals(\"looly\", jwt.getPayload(\"name\"));\n\t\tassertEquals(true, jwt.getPayload(\"admin\"));\n\t}\n\n\t@Test\n\tpublic void parseNullTest(){\n\t\tassertThrows(IllegalArgumentException.class, () -> {\n\t\t\t// https://gitee.com/chinabugotech/hutool/issues/I5OCQB\n\t\t\tJWTUtil.parseToken(null);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void verifyTest(){\n\t\tString token = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\" +\n\t\t\t\t\"eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MjQwMDQ4MjIsInVzZXJJZCI6MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV_op5LoibLkuozlj7ciLCJzeXNfbWVudV8xIiwiUk9MRV_op5LoibLkuIDlj7ciLCJzeXNfbWVudV8yIl0sImp0aSI6ImQ0YzVlYjgwLTA5ZTctNGU0ZC1hZTg3LTVkNGI5M2FhNmFiNiIsImNsaWVudF9pZCI6ImhhbmR5LXNob3AifQ.\" +\n\t\t\t\t\"aixF1eKlAKS_k3ynFnStE7-IRGiD5YaqznvK2xEjBew\";\n\n\t\tfinal boolean verify = JWTUtil.verify(token, \"123456\".getBytes());\n\t\tassertTrue(verify);\n\t}\n}\n"
  },
  {
    "path": "hutool-jwt/src/test/java/cn/hutool/jwt/JWTValidatorTest.java",
    "content": "package cn.hutool.jwt;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\npublic class JWTValidatorTest {\n\n\t@Test\n\tpublic void expiredAtTest() {\n\t\tassertThrows(ValidateException.class, () -> {\n\t\t\tString token = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo\";\n\t\t\tJWTValidator.of(token).validateDate(DateUtil.date());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issueAtTest() {\n\t\tassertThrows(ValidateException.class, () -> {\n\t\t\tfinal String token = JWT.create()\n\t\t\t\t.setIssuedAt(DateUtil.date())\n\t\t\t\t.setKey(\"123456\".getBytes())\n\t\t\t\t.sign();\n\n\t\t\t// 签发时间早于被检查的时间\n\t\t\tJWTValidator.of(token).validateDate(DateUtil.yesterday());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issueAtPassTest() {\n\t\tfinal String token = JWT.create()\n\t\t\t.setIssuedAt(DateUtil.date())\n\t\t\t.setKey(\"123456\".getBytes())\n\t\t\t.sign();\n\n\t\t// 签发时间早于被检查的时间\n\t\tJWTValidator.of(token).validateDate(DateUtil.date());\n\t}\n\n\t@Test\n\tpublic void notBeforeTest() {\n\t\tassertThrows(ValidateException.class, () -> {\n\t\t\tfinal JWT jwt = JWT.create()\n\t\t\t\t.setNotBefore(DateUtil.date());\n\t\t\tJWTValidator.of(jwt).validateDate(DateUtil.yesterday());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void notBeforePassTest() {\n\t\tfinal JWT jwt = JWT.create()\n\t\t\t.setNotBefore(DateUtil.date());\n\t\tJWTValidator.of(jwt).validateDate(DateUtil.date());\n\t}\n\n\t@Test\n\tpublic void validateAlgorithmTest() {\n\t\tfinal String token = JWT.create()\n\t\t\t.setNotBefore(DateUtil.date())\n\t\t\t.setKey(\"123456\".getBytes())\n\t\t\t.sign();\n\n\t\t// 验证算法\n\t\tJWTValidator.of(token).validateAlgorithm(JWTSignerUtil.hs256(\"123456\".getBytes()));\n\t}\n\n\t@Test\n\tpublic void validateTest() {\n\t\tString token = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNb0xpIiwiZXhwIjoxNjI0OTU4MDk0NTI4LCJpYXQiOjE2MjQ5NTgwMzQ1MjAsInVzZXIiOiJ1c2VyIn0.L0uB38p9sZrivbmP0VlDe--j_11YUXTu3TfHhfQhRKc\";\n\t\tbyte[] key = \"1234567890\".getBytes();\n\t\tboolean validate = JWT.of(token).setKey(key).validate(0);\n\t\tassertFalse(validate);\n\t}\n\n\t@Test\n\tpublic void validateDateTest() {\n\t\tassertThrows(ValidateException.class, () -> {\n\t\t\tfinal JWT jwt = JWT.create()\n\t\t\t\t.setPayload(\"id\", 123)\n\t\t\t\t.setPayload(\"username\", \"hutool\")\n\t\t\t\t.setExpiresAt(DateUtil.parse(\"2021-10-13 09:59:00\"));\n\n\t\t\tJWTValidator.of(jwt).validateDate(DateUtil.date());\n\t\t});\n\t}\n\n\t@Test\n\tpublic void issue2329Test() {\n\t\tfinal long NOW = System.currentTimeMillis();\n\t\tfinal Date NOW_TIME = new Date(NOW);\n\t\tfinal long EXPIRED = 3 * 1000L;\n\t\tfinal Date EXPIRED_TIME = new Date(NOW + EXPIRED);\n\n\t\t// 使用这种方式生成token\n\t\tfinal String token = JWT.create().setPayload(\"sub\", \"blue-light\").setIssuedAt(NOW_TIME).setNotBefore(EXPIRED_TIME)\n\t\t\t.setExpiresAt(EXPIRED_TIME).setKey(\"123456\".getBytes()).sign();\n\n\t\t// 使用这种方式验证token\n\t\tJWTValidator.of(JWT.of(token)).validateDate(DateUtil.date(NOW - 4000), 10);\n\t\tJWTValidator.of(JWT.of(token)).validateDate(DateUtil.date(NOW + 4000), 10);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-log</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 日志封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.log</Automatic-Module-Name>\n\n\t\t<!-- versions -->\n\t\t<slf4j.version>1.7.36</slf4j.version>\n\t\t<logback.version>1.4.14</logback.version>\n\t\t<log4j.version>1.2.17</log4j.version>\n\t\t<log4j2.version>2.25.3</log4j2.version>\n\t\t<commons-logging.version>1.3.3</commons-logging.version>\n\t\t<tinylog.version>1.3.6</tinylog.version>\n\t\t<tinylog2.version>2.7.0</tinylog2.version>\n\t\t<!-- 此处固定3.4.x，支持到jdk8 -->\n\t\t<jboss-logging.version>3.4.3.Final</jboss-logging.version>\n\t\t<logtube.version>0.45.0</logtube.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Logs -->\n\t\t<dependency>\n\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t<artifactId>slf4j-api</artifactId>\n\t\t\t<version>${slf4j.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>ch.qos.logback</groupId>\n\t\t\t<artifactId>logback-classic</artifactId>\n\t\t\t<version>${logback.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>log4j</groupId>\n\t\t\t<artifactId>log4j</artifactId>\n\t\t\t<version>${log4j.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.logging.log4j</groupId>\n\t\t\t<artifactId>log4j-api</artifactId>\n\t\t\t<version>${log4j2.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>commons-logging</groupId>\n\t\t\t<artifactId>commons-logging</artifactId>\n\t\t\t<version>${commons-logging.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.tinylog</groupId>\n\t\t\t<artifactId>tinylog</artifactId>\n\t\t\t<version>${tinylog.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.tinylog</groupId>\n\t\t\t<artifactId>tinylog-api</artifactId>\n\t\t\t<version>${tinylog2.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.jboss.logging</groupId>\n\t\t\t<artifactId>jboss-logging</artifactId>\n\t\t\t<version>${jboss-logging.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>io.github.logtube</groupId>\n\t\t\t<artifactId>logtube</artifactId>\n\t\t\t<version>${logtube.version}</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 仅用于测试 -->\n\t\t<dependency>\n\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t<artifactId>slf4j-simple</artifactId>\n\t\t\t<version>${slf4j.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.tinylog</groupId>\n\t\t\t<artifactId>tinylog-impl</artifactId>\n\t\t\t<version>${tinylog2.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.logging.log4j</groupId>\n\t\t\t<artifactId>log4j-core</artifactId>\n\t\t\t<version>${log4j2.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/AbstractLog.java",
    "content": "package cn.hutool.log;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.level.Level;\n\nimport java.io.Serializable;\n\n/**\n * 抽象日志类<br>\n * 实现了一些通用的接口\n * \n * @author Looly\n *\n */\npublic abstract class AbstractLog implements Log, Serializable{\n\t\n\tprivate static final long serialVersionUID = -3211115409504005616L;\n\tprivate static final String FQCN = AbstractLog.class.getName();\n\t\n\t@Override\n\tpublic boolean isEnabled(Level level) {\n\t\tswitch (level) {\n\t\t\tcase TRACE:\n\t\t\t\treturn isTraceEnabled();\n\t\t\tcase DEBUG:\n\t\t\t\treturn isDebugEnabled();\n\t\t\tcase INFO:\n\t\t\t\treturn isInfoEnabled();\n\t\t\tcase WARN:\n\t\t\t\treturn isWarnEnabled();\n\t\t\tcase ERROR:\n\t\t\t\treturn isErrorEnabled();\n\t\t\tdefault:\n\t\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void trace(Throwable t) {\n\t\ttrace(t, ExceptionUtil.getSimpleMessage(t));\n\t}\n\t\n\t@Override\n\tpublic void trace(String format, Object... arguments) {\n\t\ttrace(null, format, arguments);\n\t}\n\n\t@Override\n\tpublic void trace(Throwable t, String format, Object... arguments) {\n\t\ttrace(FQCN, t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic void debug(Throwable t) {\n\t\tdebug(t, ExceptionUtil.getSimpleMessage(t));\n\t}\n\t\n\t@Override\n\tpublic void debug(String format, Object... arguments) {\n\t\tif(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) {\n\t\t\t// 兼容Slf4j中的xxx(String message, Throwable e)\n\t\t\tdebug((Throwable)arguments[0], format);\n\t\t} else {\n\t\t\tdebug(null, format, arguments);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void debug(Throwable t, String format, Object... arguments) {\n\t\tdebug(FQCN, t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic void info(Throwable t) {\n\t\tinfo(t, ExceptionUtil.getSimpleMessage(t));\n\t}\n\t\n\t@Override\n\tpublic void info(String format, Object... arguments) {\n\t\tif(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) {\n\t\t\t// 兼容Slf4j中的xxx(String message, Throwable e)\n\t\t\tinfo((Throwable)arguments[0], format);\n\t\t} else {\n\t\t\tinfo(null, format, arguments);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void info(Throwable t, String format, Object... arguments) {\n\t\tinfo(FQCN, t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic void warn(Throwable t) {\n\t\twarn(t, ExceptionUtil.getSimpleMessage(t));\n\t}\n\t\n\t@Override\n\tpublic void warn(String format, Object... arguments) {\n\t\tif(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) {\n\t\t\t// 兼容Slf4j中的xxx(String message, Throwable e)\n\t\t\twarn((Throwable)arguments[0], format);\n\t\t} else {\n\t\t\twarn(null, format, arguments);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void warn(Throwable t, String format, Object... arguments) {\n\t\twarn(FQCN, t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic void error(Throwable t) {\n\t\tthis.error(t, ExceptionUtil.getSimpleMessage(t));\n\t}\n\t\n\t@Override\n\tpublic void error(String format, Object... arguments) {\n\t\tif(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) {\n\t\t\t// 兼容Slf4j中的xxx(String message, Throwable e)\n\t\t\terror((Throwable)arguments[0], format);\n\t\t} else {\n\t\t\terror(null, format, arguments);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void error(Throwable t, String format, Object... arguments) {\n\t\terror(FQCN, t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic void log(Level level, String format, Object... arguments) {\n\t\tif(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) {\n\t\t\t// 兼容Slf4j中的xxx(String message, Throwable e)\n\t\t\tlog(level, (Throwable)arguments[0], format);\n\t\t} else {\n\t\t\tlog(level, null, format, arguments);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void log(Level level, Throwable t, String format, Object... arguments) {\n\t\tthis.log(FQCN, level, t, format, arguments);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/GlobalLogFactory.java",
    "content": "package cn.hutool.log;\n\n/**\n * 全局日志工厂类<br>\n * 用于减少日志工厂创建，减少日志库探测\n * \n * @author looly\n * @since 4.0.3\n */\npublic class GlobalLogFactory {\n\tprivate static volatile LogFactory currentLogFactory;\n\tprivate static final Object lock = new Object();\n\n\t/**\n\t * 获取单例日志工厂类，如果不存在创建之\n\t * \n\t * @return 当前使用的日志工厂\n\t */\n\tpublic static LogFactory get() {\n\t\tif (null == currentLogFactory) {\n\t\t\tsynchronized (lock) {\n\t\t\t\tif (null == currentLogFactory) {\n\t\t\t\t\tcurrentLogFactory = LogFactory.create();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn currentLogFactory;\n\t}\n\n\t/**\n\t * 自定义日志实现\n\t * \n\t * @see cn.hutool.log.dialect.slf4j.Slf4jLogFactory\n\t * @see cn.hutool.log.dialect.log4j.Log4jLogFactory\n\t * @see cn.hutool.log.dialect.log4j2.Log4j2LogFactory\n\t * @see cn.hutool.log.dialect.commons.ApacheCommonsLogFactory\n\t * @see cn.hutool.log.dialect.jdk.JdkLogFactory\n\t * @see cn.hutool.log.dialect.console.ConsoleLogFactory\n\t * \n\t * @param logFactoryClass 日志工厂类\n\t * @return 自定义的日志工厂类\n\t */\n\tpublic static LogFactory set(Class<? extends LogFactory> logFactoryClass) {\n\t\ttry {\n\t\t\treturn set(logFactoryClass.newInstance());\n\t\t} catch (Exception e) {\n\t\t\tthrow new IllegalArgumentException(\"Can not instance LogFactory class!\", e);\n\t\t}\n\t}\n\n\t/**\n\t * 自定义日志实现\n\t *\n\t * @see cn.hutool.log.dialect.slf4j.Slf4jLogFactory\n\t * @see cn.hutool.log.dialect.log4j.Log4jLogFactory\n\t * @see cn.hutool.log.dialect.log4j2.Log4j2LogFactory\n\t * @see cn.hutool.log.dialect.commons.ApacheCommonsLogFactory\n\t * @see cn.hutool.log.dialect.jdk.JdkLogFactory\n\t * @see cn.hutool.log.dialect.console.ConsoleLogFactory\n\t *\n\t * @param logFactory 日志工厂类对象\n\t * @return 自定义的日志工厂类\n\t */\n\tpublic static LogFactory set(LogFactory logFactory) {\n\t\tlogFactory.getLog(GlobalLogFactory.class).debug(\"Custom Use [{}] Logger.\", logFactory.name);\n\t\tcurrentLogFactory = logFactory;\n\t\treturn currentLogFactory;\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/Log.java",
    "content": "package cn.hutool.log;\n\nimport cn.hutool.core.lang.caller.CallerUtil;\nimport cn.hutool.log.level.DebugLog;\nimport cn.hutool.log.level.ErrorLog;\nimport cn.hutool.log.level.InfoLog;\nimport cn.hutool.log.level.Level;\nimport cn.hutool.log.level.TraceLog;\nimport cn.hutool.log.level.WarnLog;\n\n/**\n * 日志统一接口\n * \n * @author Looly\n *\n */\npublic interface Log extends TraceLog, DebugLog, InfoLog, WarnLog, ErrorLog {\n\n\t//------------------------------------------------------------------------ Static method start\n\t/**\n\t * 获得Log\n\t *\n\t * @param clazz 日志发出的类\n\t * @return Log\n\t */\n\tstatic Log get(Class<?> clazz) {\n\t\treturn LogFactory.get(clazz);\n\t}\n\n\t/**\n\t * 获得Log\n\t *\n\t * @param name 自定义的日志发出者名称\n\t * @return Log\n\t * @since 5.0.0\n\t */\n\tstatic Log get(String name) {\n\t\treturn LogFactory.get(name);\n\t}\n\n\t/**\n\t * @return 获得日志，自动判定日志发出者\n\t * @since 5.0.0\n\t */\n\tstatic Log get() {\n\t\treturn LogFactory.get(CallerUtil.getCallerCaller());\n\t}\n\t//------------------------------------------------------------------------ Static method end\n\n\t/**\n\t * @return 日志对象的Name\n\t */\n\tString getName();\n\n\t/**\n\t * 是否开启指定日志\n\t * @param level 日志级别\n\t * @return 是否开启指定级别\n\t */\n\tboolean isEnabled(Level level);\n\n\t/**\n\t * 打印指定级别的日志\n\t * @param level 级别\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid log(Level level, String format, Object... arguments);\n\n\t/**\n\t * 打印 指定级别的日志\n\t *\n\t * @param level 级别\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid log(Level level, Throwable t, String format, Object... arguments);\n\n\t/**\n\t * 打印 ERROR 等级的日志\n\t *\n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param level 级别\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid log(String fqcn, Level level, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/LogFactory.java",
    "content": "package cn.hutool.log;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.caller.CallerUtil;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.ServiceLoaderUtil;\nimport cn.hutool.log.dialect.console.ConsoleLogFactory;\nimport cn.hutool.log.dialect.jdk.JdkLogFactory;\n\nimport java.net.URL;\nimport java.util.Map;\n\n/**\n * 日志工厂类\n *\n * @author Looly\n */\npublic abstract class LogFactory {\n\n\t/**\n\t * 日志框架名，用于打印当前所用日志框架\n\t */\n\tprotected String name;\n\t/**\n\t * 日志对象缓存\n\t */\n\tprivate final Map<Object, Log> logCache;\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 日志框架名\n\t */\n\tpublic LogFactory(String name) {\n\t\tthis.name = name;\n\t\tlogCache = new SafeConcurrentHashMap<>();\n\t}\n\n\t/**\n\t * 获取日志框架名，用于打印当前所用日志框架\n\t *\n\t * @return 日志框架名\n\t * @since 4.1.21\n\t */\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t/**\n\t * 获得日志对象\n\t *\n\t * @param name 日志对象名\n\t * @return 日志对象\n\t */\n\tpublic Log getLog(String name) {\n\t\treturn logCache.computeIfAbsent(name, o -> createLog((String)o));\n\t}\n\n\t/**\n\t * 获得日志对象\n\t *\n\t * @param clazz 日志对应类\n\t * @return 日志对象\n\t */\n\tpublic Log getLog(Class<?> clazz) {\n\t\treturn logCache.computeIfAbsent(clazz, o -> createLog((Class<?>)o));\n\t}\n\n\t/**\n\t * 创建日志对象\n\t *\n\t * @param name 日志对象名\n\t * @return 日志对象\n\t */\n\tpublic abstract Log createLog(String name);\n\n\t/**\n\t * 创建日志对象\n\t *\n\t * @param clazz 日志对应类\n\t * @return 日志对象\n\t */\n\tpublic abstract Log createLog(Class<?> clazz);\n\n\t/**\n\t * 检查日志实现是否存在<br>\n\t * 此方法仅用于检查所提供的日志相关类是否存在，当传入的日志类类不存在时抛出ClassNotFoundException<br>\n\t * 此方法的作用是在detectLogFactory方法自动检测所用日志时，如果实现类不存在，调用此方法会自动抛出异常，从而切换到下一种日志的检测。\n\t *\n\t * @param logClassName 日志实现相关类\n\t */\n\tprotected void checkLogExist(Class<?> logClassName) {\n\t\t// 不做任何操作\n\t}\n\n\t// ------------------------------------------------------------------------- Static start\n\n\t/**\n\t * @return 当前使用的日志工厂\n\t */\n\tpublic static LogFactory getCurrentLogFactory() {\n\t\treturn GlobalLogFactory.get();\n\t}\n\n\t/**\n\t * 自定义日志实现\n\t *\n\t * @param logFactoryClass 日志工厂类\n\t * @return 自定义的日志工厂类\n\t */\n\tpublic static LogFactory setCurrentLogFactory(Class<? extends LogFactory> logFactoryClass) {\n\t\treturn GlobalLogFactory.set(logFactoryClass);\n\t}\n\n\t/**\n\t * 自定义日志实现\n\t *\n\t * @param logFactory 日志工厂类对象\n\t * @return 自定义的日志工厂类\n\t */\n\tpublic static LogFactory setCurrentLogFactory(LogFactory logFactory) {\n\t\treturn GlobalLogFactory.set(logFactory);\n\t}\n\n\t/**\n\t * 获得日志对象\n\t *\n\t * @param name 日志对象名\n\t * @return 日志对象\n\t */\n\tpublic static Log get(String name) {\n\t\treturn getCurrentLogFactory().getLog(name);\n\t}\n\n\t/**\n\t * 获得日志对象\n\t *\n\t * @param clazz 日志对应类\n\t * @return 日志对象\n\t */\n\tpublic static Log get(Class<?> clazz) {\n\t\treturn getCurrentLogFactory().getLog(clazz);\n\t}\n\n\t/**\n\t * @return 获得调用者的日志\n\t */\n\tpublic static Log get() {\n\t\treturn get(CallerUtil.getCallerCaller());\n\t}\n\n\t/**\n\t * 决定日志实现\n\t * <p>\n\t * 依次按照顺序检查日志库的jar是否被引入，如果未引入任何日志库，则检查ClassPath下的logging.properties，存在则使用JdkLogFactory，否则使用ConsoleLogFactory\n\t *\n\t * @return 日志实现类\n\t */\n\tpublic static LogFactory create() {\n\t\tfinal LogFactory factory = doCreate();\n\t\tfactory.getLog(LogFactory.class).debug(\"Use [{}] Logger As Default.\", factory.name);\n\t\treturn factory;\n\t}\n\n\t/**\n\t * 决定日志实现\n\t * <p>\n\t * 依次按照顺序检查日志库的jar是否被引入，如果未引入任何日志库，则检查ClassPath下的logging.properties，存在则使用JdkLogFactory，否则使用ConsoleLogFactory\n\t *\n\t * @return 日志实现类\n\t */\n\tprivate static LogFactory doCreate() {\n\t\tfinal LogFactory factory = ServiceLoaderUtil.loadFirstAvailable(LogFactory.class);\n\t\tif (null != factory) {\n\t\t\treturn factory;\n\t\t}\n\n\t\t// 未找到任何可支持的日志库时判断依据：当JDK Logging的配置文件位于classpath中，使用JDK Logging，否则使用Console\n\t\tfinal URL url = ResourceUtil.getResource(\"logging.properties\");\n\t\treturn (null != url) ? new JdkLogFactory() : new ConsoleLogFactory();\n\t}\n\t// ------------------------------------------------------------------------- Static end\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/StaticLog.java",
    "content": "package cn.hutool.log;\n\nimport cn.hutool.core.lang.caller.CallerUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.level.Level;\n\n/**\n * 静态日志类，用于在不引入日志对象的情况下打印日志\n * \n * @author Looly\n *\n */\npublic final class StaticLog {\n\tprivate static final String FQCN = StaticLog.class.getName();\n\n\tprivate StaticLog() {\n\t}\n\n\t// ----------------------------------------------------------- Log method start\n\t// ------------------------ Trace\n\t/**\n\t * Trace等级日志，小于debug<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void trace(String format, Object... arguments) {\n\t\ttrace(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments);\n\t}\n\n\t/**\n\t * Trace等级日志，小于Debug\n\t * \n\t * @param log 日志对象\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void trace(Log log, String format, Object... arguments) {\n\t\tlog.trace(FQCN, null, format, arguments);\n\t}\n\n\t// ------------------------ debug\n\t/**\n\t * Debug等级日志，小于Info<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void debug(String format, Object... arguments) {\n\t\tdebug(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments);\n\t}\n\n\t/**\n\t * Debug等级日志，小于Info\n\t * \n\t * @param log 日志对象\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void debug(Log log, String format, Object... arguments) {\n\t\tlog.debug(FQCN, null, format, arguments);\n\t}\n\n\t// ------------------------ info\n\t/**\n\t * Info等级日志，小于Warn<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void info(String format, Object... arguments) {\n\t\tinfo(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments);\n\t}\n\n\t/**\n\t * Info等级日志，小于Warn\n\t * \n\t * @param log 日志对象\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void info(Log log, String format, Object... arguments) {\n\t\tlog.info(FQCN, null, format, arguments);\n\t}\n\n\t// ------------------------ warn\n\t/**\n\t * Warn等级日志，小于Error<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void warn(String format, Object... arguments) {\n\t\twarn(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments);\n\t}\n\n\t/**\n\t * Warn等级日志，小于Error<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param e 需在日志中堆栈打印的异常\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void warn(Throwable e, String format, Object... arguments) {\n\t\twarn(LogFactory.get(CallerUtil.getCallerCaller()), e, StrUtil.format(format, arguments));\n\t}\n\n\t/**\n\t * Warn等级日志，小于Error\n\t * \n\t * @param log 日志对象\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void warn(Log log, String format, Object... arguments) {\n\t\twarn(log, null, format, arguments);\n\t}\n\n\t/**\n\t * Warn等级日志，小于Error\n\t * \n\t * @param log 日志对象\n\t * @param e 需在日志中堆栈打印的异常\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void warn(Log log, Throwable e, String format, Object... arguments) {\n\t\tlog.warn(FQCN, e, format, arguments);\n\t}\n\n\t// ------------------------ error\n\t/**\n\t * Error等级日志<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param e 需在日志中堆栈打印的异常\n\t */\n\tpublic static void error(Throwable e) {\n\t\terror(LogFactory.get(CallerUtil.getCallerCaller()), e);\n\t}\n\n\t/**\n\t * Error等级日志<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void error(String format, Object... arguments) {\n\t\terror(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments);\n\t}\n\n\t/**\n\t * Error等级日志<br>\n\t * 由于动态获取Log，效率较低，建议在非频繁调用的情况下使用！！\n\t * \n\t * @param e 需在日志中堆栈打印的异常\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void error(Throwable e, String format, Object... arguments) {\n\t\terror(LogFactory.get(CallerUtil.getCallerCaller()), e, format, arguments);\n\t}\n\n\t/**\n\t * Error等级日志<br>\n\t * \n\t * @param log 日志对象\n\t * @param e 需在日志中堆栈打印的异常\n\t */\n\tpublic static void error(Log log, Throwable e) {\n\t\terror(log, e, e.getMessage());\n\t}\n\n\t/**\n\t * Error等级日志<br>\n\t * \n\t * @param log 日志对象\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void error(Log log, String format, Object... arguments) {\n\t\terror(log, null, format, arguments);\n\t}\n\n\t/**\n\t * Error等级日志<br>\n\t * \n\t * @param log 日志对象\n\t * @param e 需在日志中堆栈打印的异常\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void error(Log log, Throwable e, String format, Object... arguments) {\n\t\tlog.error(FQCN, e, format, arguments);\n\t}\n\n\t// ------------------------ Log\n\t/**\n\t * 打印日志<br>\n\t * \n\t * @param level 日志级别\n\t * @param t 需在日志中堆栈打印的异常\n\t * @param format 格式文本，{} 代表变量\n\t * @param arguments 变量对应的参数\n\t */\n\tpublic static void log(Level level, Throwable t, String format, Object... arguments) {\n\t\tLogFactory.get(CallerUtil.getCallerCaller()).log(FQCN, level, t, format, arguments);\n\t}\n\n\t// ----------------------------------------------------------- Log method end\n\n\t/**\n\t * 获得Log\n\t * \n\t * @param clazz 日志发出的类\n\t * @return Log\n\t * @deprecated 请使用 {@link Log#get(Class)}\n\t */\n\t@Deprecated\n\tpublic static Log get(Class<?> clazz) {\n\t\treturn LogFactory.get(clazz);\n\t}\n\n\t/**\n\t * 获得Log\n\t * \n\t * @param name 自定义的日志发出者名称\n\t * @return Log\n\t * @deprecated 请使用 {@link Log#get(String)}\n\t */\n\t@Deprecated\n\tpublic static Log get(String name) {\n\t\treturn LogFactory.get(name);\n\t}\n\n\t/**\n\t * @return 获得日志，自动判定日志发出者\n\t * @deprecated 请使用 {@link Log#get()}\n\t */\n\t@Deprecated\n\tpublic static Log get() {\n\t\treturn LogFactory.get(CallerUtil.getCallerCaller());\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog.java",
    "content": "package cn.hutool.log.dialect.commons;\n\nimport org.apache.commons.logging.Log;\nimport org.apache.commons.logging.LogFactory;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport cn.hutool.log.level.Level;\n\n/**\n * Apache Commons Logging\n * @author Looly\n *\n */\npublic class ApacheCommonsLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\t\n\tprivate final transient Log logger;\n\tprivate final String name;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic ApacheCommonsLog(Log logger, String name) {\n\t\tthis.logger = logger;\n\t\tthis.name = name;\n\t}\n\n\tpublic ApacheCommonsLog(Class<?> clazz) {\n\t\tthis(LogFactory.getLog(clazz), null == clazz ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic ApacheCommonsLog(String name) {\n\t\tthis(LogFactory.getLog(name), name);\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\t// fqcn此处无效\n\t\tif(isTraceEnabled()){\n\t\t\tlogger.trace(StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\t// fqcn此处无效\n\t\tif(isDebugEnabled()){\n\t\t\tlogger.debug(StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\t// fqcn此处无效\n\t\tif(isInfoEnabled()){\n\t\t\tlogger.info(StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isWarnEnabled();\n\t}\n\n\t@Override\n\tpublic void warn(String format, Object... arguments) {\n\t\tif(isWarnEnabled()){\n\t\t\tlogger.warn(StrUtil.format(format, arguments));\n\t\t}\n\t}\n\n\t@Override\n\tpublic void warn(Throwable t, String format, Object... arguments) {\n\t}\n\t\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\t// fqcn此处无效\n\t\tif(isWarnEnabled()){\n\t\t\tlogger.warn(StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isErrorEnabled();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\t// fqcn此处无效\n\t\tif(isErrorEnabled()){\n\t\t\tlogger.warn(StrUtil.format(format, arguments), t);\n\t\t}\n\t\t\n\t}\n\t\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\tswitch (level) {\n\t\tcase TRACE:\n\t\t\ttrace(t, format, arguments);\n\t\t\tbreak;\n\t\tcase DEBUG:\n\t\t\tdebug(t, format, arguments);\n\t\t\tbreak;\n\t\tcase INFO:\n\t\t\tinfo(t, format, arguments);\n\t\t\tbreak;\n\t\tcase WARN:\n\t\t\twarn(t, format, arguments);\n\t\t\tbreak;\n\t\tcase ERROR:\n\t\t\terror(t, format, arguments);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------- Private method\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog4JLog.java",
    "content": "package cn.hutool.log.dialect.commons;\n\nimport org.apache.commons.logging.Log;\nimport org.apache.commons.logging.impl.Log4JLogger;\n\nimport cn.hutool.log.dialect.log4j.Log4jLog;\n\n/**\n * Apache Commons Logging for Log4j\n * @author Looly\n *\n */\npublic class ApacheCommonsLog4JLog extends Log4jLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\t\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic ApacheCommonsLog4JLog(Log logger) {\n\t\tsuper(((Log4JLogger) logger).getLogger());\n\t}\n\n\tpublic ApacheCommonsLog4JLog(Class<?> clazz) {\n\t\tsuper(clazz);\n\t}\n\n\tpublic ApacheCommonsLog4JLog(String name) {\n\t\tsuper(name);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLogFactory.java",
    "content": "package cn.hutool.log.dialect.commons;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n *  Apache Commons Logging\n * @author Looly\n *\n */\npublic class ApacheCommonsLogFactory extends LogFactory{\n\t\n\tpublic ApacheCommonsLogFactory() {\n\t\tsuper(\"Apache Common Logging\");\n\t\tcheckLogExist(org.apache.commons.logging.LogFactory.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\ttry {\n\t\t\treturn new ApacheCommonsLog4JLog(name);\n\t\t} catch (Exception e) {\n\t\t\treturn new ApacheCommonsLog(name);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\ttry {\n\t\t\treturn new ApacheCommonsLog4JLog(clazz);\n\t\t} catch (Exception e) {\n\t\t\treturn new ApacheCommonsLog(clazz);\n\t\t}\n\t}\n\n\t@Override\n\tprotected void checkLogExist(Class<?> logClassName) {\n\t\tsuper.checkLogExist(logClassName);\n\t\t//Commons Logging在调用getLog时才检查是否有日志实现，在此提前检查，如果没有实现则跳过之\n\t\tgetLog(ApacheCommonsLogFactory.class);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/commons/package-info.java",
    "content": "/**\n * Apache-Commons-Logging日志库的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.commons;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleColorLog.java",
    "content": "package cn.hutool.log.dialect.console;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.ansi.AnsiColor;\nimport cn.hutool.core.lang.ansi.AnsiEncoder;\nimport cn.hutool.core.util.ClassUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.level.Level;\n\nimport java.util.function.Function;\n\n/**\n * 利用System.out.println()打印彩色日志\n *\n * @author hongda.li, looly\n * @since 5.8.0\n */\npublic class ConsoleColorLog extends ConsoleLog {\n\n\t/**\n\t * 控制台打印类名的颜色代码\n\t */\n\tprivate static final AnsiColor COLOR_CLASSNAME = AnsiColor.CYAN;\n\n\t/**\n\t * 控制台打印时间的颜色代码\n\t */\n\tprivate static final AnsiColor COLOR_TIME = AnsiColor.WHITE;\n\n\t/**\n\t * 控制台打印正常信息的颜色代码\n\t */\n\tprivate static final AnsiColor COLOR_NONE = AnsiColor.DEFAULT;\n\n\tprivate static Function<Level, AnsiColor> colorFactory = (level -> {\n\t\tswitch (level) {\n\t\t\tcase DEBUG:\n\t\t\tcase INFO:\n\t\t\t\treturn AnsiColor.GREEN;\n\t\t\tcase WARN:\n\t\t\t\treturn AnsiColor.YELLOW;\n\t\t\tcase ERROR:\n\t\t\t\treturn AnsiColor.RED;\n\t\t\tcase TRACE:\n\t\t\t\treturn AnsiColor.MAGENTA;\n\t\t\tdefault:\n\t\t\t\treturn COLOR_NONE;\n\t\t}\n\t});\n\n\t/**\n\t * 设置颜色工厂，根据日志级别，定义不同的颜色\n\t *\n\t * @param colorFactory 颜色工厂函数\n\t */\n\tpublic static void setColorFactory(Function<Level, AnsiColor> colorFactory) {\n\t\tConsoleColorLog.colorFactory = colorFactory;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 类名\n\t */\n\tpublic ConsoleColorLog(String name) {\n\t\tsuper(name);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param clazz 类\n\t */\n\tpublic ConsoleColorLog(Class<?> clazz) {\n\t\tsuper(clazz);\n\t}\n\n\t@Override\n\tpublic synchronized void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\tif (false == isEnabled(level)) {\n\t\t\treturn;\n\t\t}\n\n\t\tfinal String template = AnsiEncoder.encode(COLOR_TIME, \"[%s]\", colorFactory.apply(level), \"[%-5s]%s\", COLOR_CLASSNAME, \"%-30s: \", COLOR_NONE, \"%s%n\");\n\t\tSystem.out.format(template, DateUtil.now(), level.name(), \" - \", ClassUtil.getShortClassName(getName()), StrUtil.format(format, arguments));\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleColorLogFactory.java",
    "content": "package cn.hutool.log.dialect.console;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * 利用System.out.println()打印彩色日志\n *\n * @author hongda.li\n * @since 5.8.0\n */\npublic class ConsoleColorLogFactory extends LogFactory {\n\n\tpublic ConsoleColorLogFactory() {\n\t\tsuper(\"Hutool Console Color Logging\");\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new ConsoleColorLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new ConsoleColorLog(clazz);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLog.java",
    "content": "package cn.hutool.log.dialect.console;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport cn.hutool.log.level.Level;\n\n/**\n * 利用System.out.println()打印日志\n *\n * @author Looly\n */\npublic class ConsoleLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\n\tprivate static final String logFormat = \"[{date}] [{level}] {name}: {msg}\";\n\tprivate static Level currentLevel = Level.DEBUG;\n\n\tprivate final String name;\n\n\t//------------------------------------------------------------------------- Constructor\n\n\t/**\n\t * 构造\n\t *\n\t * @param clazz 类\n\t */\n\tpublic ConsoleLog(Class<?> clazz) {\n\t\tthis.name = (null == clazz) ? StrUtil.NULL : clazz.getName();\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param name 类名\n\t */\n\tpublic ConsoleLog(String name) {\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t/**\n\t * 设置自定义的日志显示级别\n\t *\n\t * @param customLevel 自定义级别\n\t * @since 4.1.10\n\t */\n\tpublic static void setLevel(Level customLevel) {\n\t\tAssert.notNull(customLevel);\n\t\tcurrentLevel = customLevel;\n\t}\n\n\t//------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn isEnabled(Level.TRACE);\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.TRACE, t, format, arguments);\n\t}\n\n\t//------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn isEnabled(Level.DEBUG);\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.DEBUG, t, format, arguments);\n\t}\n\n\t//------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn isEnabled(Level.INFO);\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t//------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn isEnabled(Level.WARN);\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.WARN, t, format, arguments);\n\t}\n\n\t//------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn isEnabled(Level.ERROR);\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.ERROR, t, format, arguments);\n\t}\n\n\t//------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\t// fqcn 无效\n\t\tif (false == isEnabled(level)) {\n\t\t\treturn;\n\t\t}\n\n\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"date\", DateUtil.now())\n\t\t\t\t.set(\"level\", level.toString())\n\t\t\t\t.set(\"name\", this.name)\n\t\t\t\t.set(\"msg\", StrUtil.format(format, arguments));\n\n\t\tfinal String logMsg = StrUtil.format(logFormat, dict);\n\n\t\t//WARN以上级别打印至System.err\n\t\tif (level.ordinal() >= Level.WARN.ordinal()) {\n\t\t\tConsole.error(t, logMsg);\n\t\t} else {\n\t\t\tConsole.log(t, logMsg);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean isEnabled(Level level) {\n\t\treturn currentLevel.compareTo(level) <= 0;\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLogFactory.java",
    "content": "package cn.hutool.log.dialect.console;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * 利用System.out.println()打印日志\n * @author Looly\n *\n */\npublic class ConsoleLogFactory extends LogFactory {\n\t\n\tpublic ConsoleLogFactory() {\n\t\tsuper(\"Hutool Console Logging\");\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new ConsoleLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new ConsoleLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/console/package-info.java",
    "content": "/**\n * 控制台输出的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.console;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLog.java",
    "content": "package cn.hutool.log.dialect.jboss;\n\nimport org.jboss.logging.Logger;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport cn.hutool.log.level.Level;\n\n/**\n * <a href=\"https://github.com/jboss-logging\">Jboss-Logging</a> log.\n * \n * @author Looly\n *\n */\npublic class JbossLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\n\tprivate final transient Logger logger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\t/**\n\t * 构造\n\t * \n\t * @param logger {@link Logger}\n\t */\n\tpublic JbossLog(Logger logger) {\n\t\tthis.logger = logger;\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param clazz 日志打印所在类\n\t */\n\tpublic JbossLog(Class<?> clazz) {\n\t\tthis((null == clazz) ? StrUtil.NULL : clazz.getName());\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param name 日志打印所在类名\n\t */\n\tpublic JbossLog(String name) {\n\t\tthis(Logger.getLogger(name));\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isTraceEnabled()) {\n\t\t\tlogger.trace(fqcn, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isDebugEnabled()) {\n\t\t\tlogger.debug(fqcn, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isInfoEnabled()) {\n\t\t\tlogger.info(fqcn, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isEnabled(Logger.Level.WARN);\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isWarnEnabled()) {\n\t\t\tlogger.warn(fqcn, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isEnabled(Logger.Level.ERROR);\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isErrorEnabled()) {\n\t\t\tlogger.error(fqcn, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\tswitch (level) {\n\t\tcase TRACE:\n\t\t\ttrace(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase DEBUG:\n\t\t\tdebug(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase INFO:\n\t\t\tinfo(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase WARN:\n\t\t\twarn(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase ERROR:\n\t\t\terror(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLogFactory.java",
    "content": "package cn.hutool.log.dialect.jboss;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"https://github.com/jboss-logging\">Jboss-Logging</a> log.\n * \n * @author Looly\n * @since 4.1.21\n */\npublic class JbossLogFactory extends LogFactory {\n\n\t/**\n\t * 构造\n\t */\n\tpublic JbossLogFactory() {\n\t\tsuper(\"JBoss Logging\");\n\t\tcheckLogExist(org.jboss.logging.Logger.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new JbossLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new JbossLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jboss/package-info.java",
    "content": "/**\n * jboss-logging实现\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.jboss;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLog.java",
    "content": "package cn.hutool.log.dialect.jdk;\n\nimport java.util.logging.Level;\nimport java.util.logging.LogRecord;\nimport java.util.logging.Logger;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\n\n/**\n * <a href=\"http://java.sun.com/javase/6/docs/technotes/guides/logging/index.html\">java.util.logging</a> log.\n * \n * @author Looly\n *\n */\npublic class JdkLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\t\n\tprivate final transient Logger logger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic JdkLog(Logger logger) {\n\t\tthis.logger = logger;\n\t}\n\n\tpublic JdkLog(Class<?> clazz) {\n\t\tthis((null == clazz) ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic JdkLog(String name) {\n\t\tthis(Logger.getLogger(name));\n\t}\n\t\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isLoggable(Level.FINEST);\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.FINEST, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isLoggable(Level.FINE);\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.FINE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isLoggable(Level.INFO);\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isLoggable(Level.WARNING);\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.WARNING, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isLoggable(Level.SEVERE);\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.SEVERE, t, format, arguments);\n\t}\n\t\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) {\n\t\tLevel jdkLevel;\n\t\tswitch (level) {\n\t\t\tcase TRACE:\n\t\t\t\tjdkLevel = Level.FINEST;\n\t\t\t\tbreak;\n\t\t\tcase DEBUG:\n\t\t\t\tjdkLevel = Level.FINE;\n\t\t\t\tbreak;\n\t\t\tcase INFO:\n\t\t\t\tjdkLevel = Level.INFO;\n\t\t\t\tbreak;\n\t\t\tcase WARN:\n\t\t\t\tjdkLevel = Level.WARNING;\n\t\t\t\tbreak;\n\t\t\tcase ERROR:\n\t\t\t\tjdkLevel = Level.SEVERE;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t\tlogIfEnabled(fqcn, jdkLevel, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Private method\n\t/**\n\t * 打印对应等级的日志\n\t * \n\t * @param callerFQCN 调用者的完全限定类名(Fully Qualified Class Name)\n\t * @param level 等级\n\t * @param throwable 异常对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tprivate void logIfEnabled(String callerFQCN, Level level, Throwable throwable, String format, Object[] arguments){\n\t\tif(logger.isLoggable(level)){\n\t\t\tLogRecord record = new LogRecord(level, StrUtil.format(format, arguments));\n\t\t\trecord.setLoggerName(getName());\n\t\t\trecord.setThrown(throwable);\n\t\t\tfillCallerData(callerFQCN, record);\n\t\t\tlogger.log(record);\n\t\t}\n\t}\n\t\n\t/**\n\t * 传入调用日志类的信息\n\t * @param callerFQCN 调用者全限定类名\n\t * @param record The record to update\n\t */\n\tprivate static void fillCallerData(String callerFQCN, LogRecord record) {\n\t\tStackTraceElement[] steArray = Thread.currentThread().getStackTrace();\n\n\t\tint found = -1;\n\t\tString className;\n\t\tfor (int i = steArray.length -2; i > -1; i--) {\n\t\t\t// 此处初始值为length-2，表示从倒数第二个堆栈开始检查，如果是倒数第一个，那调用者就获取不到\n\t\t\tclassName = steArray[i].getClassName();\n\t\t\tif (callerFQCN.equals(className)) {\n\t\t\t\tfound = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (found > -1) {\n\t\t\tStackTraceElement ste = steArray[found+1];\n\t\t\trecord.setSourceClassName(ste.getClassName());\n\t\t\trecord.setSourceMethodName(ste.getMethodName());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLogFactory.java",
    "content": "package cn.hutool.log.dialect.jdk;\n\nimport java.io.InputStream;\nimport java.util.logging.LogManager;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * JDK日志工厂类\n *  <a href=\"http://java.sun.com/javase/6/docs/technotes/guides/logging/index.html\">java.util.logging</a> log.\n * @author Looly\n *\n */\npublic class JdkLogFactory extends LogFactory{\n\t\n\tpublic JdkLogFactory() {\n\t\tsuper(\"JDK Logging\");\n\t\treadConfig();\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new JdkLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new JdkLog(clazz);\n\t}\n\t\n\t/**\n\t * 读取ClassPath下的logging.properties配置文件\n\t */\n\tprivate void readConfig() {\n\t\t//避免循环引用，Log初始化的时候不使用相关工具类\n\t\tInputStream in = ResourceUtil.getStreamSafe(\"logging.properties\");\n\t\tif(null == in){\n\t\t\tSystem.err.println(\"[WARN] Can not find [logging.properties], use [%JRE_HOME%/lib/logging.properties] as default!\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tLogManager.getLogManager().readConfiguration(in);\n\t\t} catch (Exception e) {\n\t\t\tConsole.error(e, \"Read [logging.properties] from classpath error!\");\n\t\t\ttry {\n\t\t\t\tLogManager.getLogManager().readConfiguration();\n\t\t\t} catch (Exception e1) {\n\t\t\t\tConsole.error(e, \"Read [logging.properties] from [%JRE_HOME%/lib/logging.properties] error!\");\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/jdk/package-info.java",
    "content": "/**\n * JDK-Logging的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.jdk;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLog.java",
    "content": "package cn.hutool.log.dialect.log4j;\n\nimport org.apache.log4j.Level;\nimport org.apache.log4j.Logger;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\n\n/**\n * <a href=\"http://logging.apache.org/log4j/1.2/index.html\">Apache Log4J</a> log.<br>\n * \n * @author Looly\n *\n */\npublic class Log4jLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\n\tprivate final Logger logger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic Log4jLog(Logger logger) {\n\t\tthis.logger = logger;\n\t}\n\n\tpublic Log4jLog(Class<?> clazz) {\n\t\tthis((null == clazz) ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic Log4jLog(String name) {\n\t\tthis(Logger.getLogger(name));\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, cn.hutool.log.level.Level.TRACE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, cn.hutool.log.level.Level.DEBUG, t, format, arguments);\n\t}\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, cn.hutool.log.level.Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isEnabledFor(Level.WARN);\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, cn.hutool.log.level.Level.WARN, t, format, arguments);\n\t}\n\t\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isEnabledFor(Level.ERROR);\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, cn.hutool.log.level.Level.ERROR, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) {\n\t\tLevel log4jLevel;\n\t\tswitch (level) {\n\t\t\tcase TRACE:\n\t\t\t\tlog4jLevel = Level.TRACE;\n\t\t\t\tbreak;\n\t\t\tcase DEBUG:\n\t\t\t\tlog4jLevel = Level.DEBUG;\n\t\t\t\tbreak;\n\t\t\tcase INFO:\n\t\t\t\tlog4jLevel = Level.INFO;\n\t\t\t\tbreak;\n\t\t\tcase WARN:\n\t\t\t\tlog4jLevel = Level.WARN;\n\t\t\t\tbreak;\n\t\t\tcase ERROR:\n\t\t\t\tlog4jLevel = Level.ERROR;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t\t\n\t\tif(logger.isEnabledFor(log4jLevel)) {\n\t\t\tlogger.log(fqcn, log4jLevel, StrUtil.format(format, arguments), t);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLogFactory.java",
    "content": "package cn.hutool.log.dialect.log4j;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"http://logging.apache.org/log4j/1.2/index.html\">Apache Log4J</a> log.<br>\n * @author Looly\n *\n */\npublic class Log4jLogFactory extends LogFactory{\n\t\n\tpublic Log4jLogFactory() {\n\t\tsuper(\"Log4j\");\n\t\tcheckLogExist(org.apache.log4j.Logger.class);\n\t}\n\t\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new Log4jLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new Log4jLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j/package-info.java",
    "content": "/**\n * Log4j的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.log4j;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java",
    "content": "package cn.hutool.log.dialect.log4j2;\n\nimport org.apache.logging.log4j.Level;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.apache.logging.log4j.spi.AbstractLogger;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\n\n/**\n * <a href=\"http://logging.apache.org/log4j/2.x/index.html\">Apache Log4J 2</a> log.<br>\n * \n * @author Looly\n *\n */\npublic class Log4j2Log extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\t\n\tprivate final transient Logger logger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic Log4j2Log(Logger logger) {\n\t\tthis.logger = logger;\n\t}\n\n\tpublic Log4j2Log(Class<?> clazz) {\n\t\tthis(LogManager.getLogger(clazz));\n\t}\n\n\tpublic Log4j2Log(String name) {\n\t\tthis(LogManager.getLogger(name));\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.TRACE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.DEBUG, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isWarnEnabled();\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.WARN, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isErrorEnabled();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.ERROR, t, format, arguments);\n\t}\n\t\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) {\n\t\tLevel log4j2Level;\n\t\tswitch (level) {\n\t\t\tcase TRACE:\n\t\t\t\tlog4j2Level = Level.TRACE;\n\t\t\t\tbreak;\n\t\t\tcase DEBUG:\n\t\t\t\tlog4j2Level = Level.DEBUG;\n\t\t\t\tbreak;\n\t\t\tcase INFO:\n\t\t\t\tlog4j2Level = Level.INFO;\n\t\t\t\tbreak;\n\t\t\tcase WARN:\n\t\t\t\tlog4j2Level = Level.WARN;\n\t\t\t\tbreak;\n\t\t\tcase ERROR:\n\t\t\t\tlog4j2Level = Level.ERROR;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t\tlogIfEnabled(fqcn, log4j2Level, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Private method\n\t/**\n\t * 打印日志<br>\n\t * 此方法用于兼容底层日志实现，通过传入当前包装类名，以解决打印日志中行号错误问题\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于纠正定位错误行号\n\t * @param level 日志级别，使用org.apache.logging.log4j.Level中的常量\n\t * @param t 异常\n\t * @param msgTemplate 消息模板\n\t * @param arguments 参数\n\t */\n\tprivate void logIfEnabled(String fqcn, Level level, Throwable t, String msgTemplate, Object... arguments) {\n\t\tif(this.logger.isEnabled(level)) {\n\t\t\tif(this.logger instanceof AbstractLogger){\n\t\t\t\t((AbstractLogger)this.logger).logIfEnabled(fqcn, level, null, StrUtil.format(msgTemplate, arguments), t);\n\t\t\t} else {\n\t\t\t\t// FQCN无效\n\t\t\t\tthis.logger.log(level, StrUtil.format(msgTemplate, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2LogFactory.java",
    "content": "package cn.hutool.log.dialect.log4j2;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"http://logging.apache.org/log4j/2.x/index.html\">Apache Log4J 2</a> log.<br>\n * @author Looly\n *\n */\npublic class Log4j2LogFactory extends LogFactory{\n\t\n\tpublic Log4j2LogFactory() {\n\t\tsuper(\"Log4j2\");\n\t\tcheckLogExist(org.apache.logging.log4j.LogManager.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new Log4j2Log(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new Log4j2Log(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/package-info.java",
    "content": "/**\n * Log4j2的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.log4j2;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/logtube/LogTubeLog.java",
    "content": "package cn.hutool.log.dialect.logtube;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport cn.hutool.log.level.Level;\nimport io.github.logtube.Logtube;\nimport io.github.logtube.core.IEventLogger;\n\n/**\n * <a href=\"https://github.com/logtube/logtube-java\">LogTube</a> log.封装<br>\n *\n * @author looly\n * @since 5.6.6\n */\npublic class LogTubeLog extends AbstractLog {\n\n\tprivate final IEventLogger logger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic LogTubeLog(IEventLogger logger) {\n\t\tthis.logger = logger;\n\t}\n\n\tpublic LogTubeLog(Class<?> clazz) {\n\t\tthis((null == clazz) ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic LogTubeLog(String name) {\n\t\tthis(Logtube.getLogger(name));\n\t}\n\n\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.TRACE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.DEBUG, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isWarnEnabled();\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.WARN, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isErrorEnabled();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlog(fqcn, Level.ERROR, t, format, arguments);\n\t}\n\n\t@Override\n\tpublic void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\tfinal String topic = level.name().toLowerCase();\n\t\tlogger.topic(topic)\n\t\t\t\t.xStackTraceElement(ExceptionUtil.getStackElement(6), null)\n\t\t\t\t.message(StrUtil.format(format, arguments))\n\t\t\t\t.xException(t)\n\t\t\t\t.commit();\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/logtube/LogTubeLogFactory.java",
    "content": "package cn.hutool.log.dialect.logtube;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"https://github.com/logtube/logtube-java\">LogTube</a> log. 封装<br>\n *\n * @author Looly\n * @since 5.6.6\n */\npublic class LogTubeLogFactory extends LogFactory {\n\n\tpublic LogTubeLogFactory() {\n\t\tsuper(\"LogTube\");\n\t\tcheckLogExist(io.github.logtube.Logtube.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new LogTubeLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new LogTubeLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/logtube/package-info.java",
    "content": "/**\n * LogTube的实现封装\n *\n * @author looly\n *\n */\npackage cn.hutool.log.dialect.logtube;\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/package-info.java",
    "content": "/**\n * 第三方日志库的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLog.java",
    "content": "package cn.hutool.log.dialect.slf4j;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.spi.LocationAwareLogger;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport cn.hutool.log.level.Level;\n\n/**\n * <a href=\"http://www.slf4j.org/\">SLF4J</a> log.<br>\n * 同样无缝支持 <a href=\"http://logback.qos.ch/\">LogBack</a>\n * \n * @author Looly\n *\n */\npublic class Slf4jLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -6843151523380063975L;\n\n\tprivate final transient Logger logger;\n\t/** 是否为 LocationAwareLogger ，用于判断是否可以传递FQCN */\n\tprivate final boolean isLocationAwareLogger;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic Slf4jLog(Logger logger) {\n\t\tthis.logger = logger;\n\t\tthis.isLocationAwareLogger = (logger instanceof LocationAwareLogger);\n\t}\n\n\tpublic Slf4jLog(Class<?> clazz) {\n\t\tthis(getSlf4jLogger(clazz));\n\t}\n\n\tpublic Slf4jLog(String name) {\n\t\tthis(LoggerFactory.getLogger(name));\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn logger.getName();\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn logger.isTraceEnabled();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isTraceEnabled()) {\n\t\t\tif(this.isLocationAwareLogger) {\n\t\t\t\tlocationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.TRACE_INT, t, format, arguments);\n\t\t\t} else {\n\t\t\t\tlogger.trace(StrUtil.format(format, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn logger.isDebugEnabled();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isDebugEnabled()) {\n\t\t\tif(this.isLocationAwareLogger) {\n\t\t\t\tlocationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.DEBUG_INT, t, format, arguments);\n\t\t\t} else {\n\t\t\t\tlogger.debug(StrUtil.format(format, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn logger.isInfoEnabled();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isInfoEnabled()) {\n\t\t\tif(this.isLocationAwareLogger) {\n\t\t\t\tlocationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.INFO_INT, t, format, arguments);\n\t\t\t} else {\n\t\t\t\tlogger.info(StrUtil.format(format, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn logger.isWarnEnabled();\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isWarnEnabled()) {\n\t\t\tif(this.isLocationAwareLogger) {\n\t\t\t\tlocationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.WARN_INT, t, format, arguments);\n\t\t\t} else {\n\t\t\t\tlogger.warn(StrUtil.format(format, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn logger.isErrorEnabled();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tif (isErrorEnabled()) {\n\t\t\tif(this.isLocationAwareLogger) {\n\t\t\t\tlocationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.ERROR_INT, t, format, arguments);\n\t\t\t} else {\n\t\t\t\tlogger.error(StrUtil.format(format, arguments), t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\tswitch (level) {\n\t\tcase TRACE:\n\t\t\ttrace(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase DEBUG:\n\t\t\tdebug(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase INFO:\n\t\t\tinfo(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase WARN:\n\t\t\twarn(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tcase ERROR:\n\t\t\terror(fqcn, t, format, arguments);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------------------- Private method\n\t/**\n\t * 打印日志<br>\n\t * 此方法用于兼容底层日志实现，通过传入当前包装类名，以解决打印日志中行号错误问题\n\t * \n\t * @param logger {@link LocationAwareLogger} 实现\n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于纠正定位错误行号\n\t * @param level_int 日志级别，使用LocationAwareLogger中的常量\n\t * @param t 异常\n\t * @param msgTemplate 消息模板\n\t * @param arguments 参数\n\t */\n\tprivate void locationAwareLog(LocationAwareLogger logger, String fqcn, int level_int, Throwable t, String msgTemplate, Object[] arguments) {\n\t\t// ((LocationAwareLogger)this.logger).log(null, fqcn, level_int, msgTemplate, arguments, t);\n\t\t// 由于slf4j-log4j12中此方法的实现存在bug，故在此拼接参数\n\t\tlogger.log(null, fqcn, level_int, StrUtil.format(msgTemplate, arguments), null, t);\n\t}\n\n\t/**\n\t * 获取Slf4j Logger对象\n\t * \n\t * @param clazz 打印日志所在类，当为{@code null}时使用“null”表示\n\t * @return {@link Logger}\n\t */\n\tprivate static Logger getSlf4jLogger(Class<?> clazz) {\n\t\treturn (null == clazz) ? LoggerFactory.getLogger(StrUtil.EMPTY) : LoggerFactory.getLogger(clazz);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLogFactory.java",
    "content": "package cn.hutool.log.dialect.slf4j;\n\nimport java.io.OutputStream;\nimport java.io.PrintStream;\nimport java.io.UnsupportedEncodingException;\n\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.helpers.NOPLoggerFactory;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"http://www.slf4j.org/\">SLF4J</a> log.<br>\n * 同样无缝支持 <a href=\"http://logback.qos.ch/\">LogBack</a>\n * \n * @author Looly\n *\n */\npublic class Slf4jLogFactory extends LogFactory {\n\t\n\tpublic Slf4jLogFactory() {\n\t\tthis(true);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param failIfNOP 如果未找到桥接包是否报错\n\t */\n\tpublic Slf4jLogFactory(boolean failIfNOP) {\n\t\tsuper(\"Slf4j\");\n\t\tcheckLogExist(LoggerFactory.class);\n\t\tif(false == failIfNOP){\n\t\t\treturn;\n\t\t}\n\n\t\t// SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on\n\t\t// the console during automatic detection.\n\t\tfinal StringBuilder buf = new StringBuilder();\n\t\tfinal PrintStream err = System.err;\n\t\ttry {\n\t\t\tSystem.setErr(new PrintStream(new OutputStream(){\n\t\t\t\t@Override\n\t\t\t\tpublic void write(int b) {\n\t\t\t\t\tbuf.append((char) b);\n\t\t\t\t}\n\t\t\t}, true, \"US-ASCII\"));\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new Error(e);\n\t\t}\n\n\t\ttry {\n\t\t\tif (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) {\n\t\t\t\tthrow new NoClassDefFoundError(buf.toString());\n\t\t\t} else {\n\t\t\t\terr.print(buf);\n\t\t\t\terr.flush();\n\t\t\t}\n\t\t} finally {\n\t\t\tSystem.setErr(err);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new Slf4jLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new Slf4jLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/package-info.java",
    "content": "/**\n * Slf4j的实现封装\n * \n * @author looly\n *\n */\npackage cn.hutool.log.dialect.slf4j;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog.java",
    "content": "package cn.hutool.log.dialect.tinylog;\n\nimport org.pmw.tinylog.Level;\nimport org.pmw.tinylog.LogEntryForwarder;\nimport org.pmw.tinylog.Logger;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\n\n/**\n * <a href=\"http://www.tinylog.org/\">tinylog</a> log.<br>\n * \n * @author Looly\n *\n */\npublic class TinyLog extends AbstractLog {\n\tprivate static final long serialVersionUID = -4848042277045993735L;\n\t\n\t/** 堆栈增加层数，因为封装因此多了两层，此值用于正确获取当前类名 */\n\tprivate static final int DEPTH = 4;\n\n\tprivate final int level;\n\tprivate final String name;\n\n\t// ------------------------------------------------------------------------- Constructor\n\tpublic TinyLog(Class<?> clazz) {\n\t\tthis(null == clazz ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic TinyLog(String name) {\n\t\tthis.name = name;\n\t\tthis.level = Logger.getLevel(name).ordinal();\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn this.level <= Level.TRACE.ordinal();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.TRACE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn this.level <= Level.DEBUG.ordinal();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.DEBUG, t, format, arguments);\n\t}\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn this.level <= Level.INFO.ordinal();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn this.level <= org.pmw.tinylog.Level.WARNING.ordinal();\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.WARNING, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn this.level <= Level.ERROR.ordinal();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.ERROR, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, toTinyLevel(level), t, format, arguments);\n\t}\n\t\n\t@Override\n\tpublic boolean isEnabled(cn.hutool.log.level.Level level) {\n\t\treturn this.level <= toTinyLevel(level).ordinal();\n\t}\n\t\n\t/**\n\t * 在对应日志级别打开情况下打印日志\n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param level 日志级别\n\t * @param t 异常，null则检查最后一个参数是否为Throwable类型，是则取之，否则不打印堆栈\n\t * @param format 日志消息模板\n\t * @param arguments 日志消息参数\n\t */\n\tprivate void logIfEnabled(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\t// fqcn 无效\n\t\tif(null == t){\n\t\t\tt = getLastArgumentIfThrowable(arguments);\n\t\t}\n\t\tLogEntryForwarder.forward(DEPTH, level, t, StrUtil.toString(format), arguments);\n\t}\n\t\n\t/**\n\t * 将Hutool的Level等级转换为Tinylog的Level等级\n\t * \n\t * @param level Hutool的Level等级\n\t * @return Tinylog的Level\n\t * @since 4.0.3\n\t */\n\tprivate Level toTinyLevel(cn.hutool.log.level.Level level) {\n\t\tLevel tinyLevel;\n\t\tswitch (level) {\n\t\tcase TRACE:\n\t\t\ttinyLevel = Level.TRACE;\n\t\t\tbreak;\n\t\tcase DEBUG:\n\t\t\ttinyLevel = Level.DEBUG;\n\t\t\tbreak;\n\t\tcase INFO:\n\t\t\ttinyLevel = Level.INFO;\n\t\t\tbreak;\n\t\tcase WARN:\n\t\t\ttinyLevel = Level.WARNING;\n\t\t\tbreak;\n\t\tcase ERROR:\n\t\t\ttinyLevel = Level.ERROR;\n\t\t\tbreak;\n\t\tcase OFF:\n\t\t\ttinyLevel = Level.OFF;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t\treturn tinyLevel;\n\t}\n\n\t/**\n\t * 如果最后一个参数为异常参数，则获取之，否则返回null\n\t * \n\t * @param arguments 参数\n\t * @return 最后一个异常参数\n\t * @since 4.0.3\n\t */\n\tprivate static Throwable getLastArgumentIfThrowable(Object... arguments) {\n\t\tif (ArrayUtil.isNotEmpty(arguments) && arguments[arguments.length - 1] instanceof Throwable) {\n\t\t\treturn (Throwable) arguments[arguments.length - 1];\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog2.java",
    "content": "package cn.hutool.log.dialect.tinylog;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.AbstractLog;\nimport org.tinylog.Level;\nimport org.tinylog.configuration.Configuration;\nimport org.tinylog.format.AdvancedMessageFormatter;\nimport org.tinylog.format.MessageFormatter;\nimport org.tinylog.provider.LoggingProvider;\nimport org.tinylog.provider.ProviderRegistry;\n\n/**\n * <a href=\"http://www.tinylog.org/\">tinylog</a> log.<br>\n *\n * @author Looly\n *\n */\npublic class TinyLog2 extends AbstractLog {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** 堆栈增加层数，因为封装因此多了两层，此值用于正确获取当前类名 */\n\tprivate static final int DEPTH = 5;\n\n\tprivate final int level;\n\tprivate final String name;\n\tprivate static final LoggingProvider provider = ProviderRegistry.getLoggingProvider();\n\t// ------------------------------------------------------------------------- Constructor\n\n\tprivate static final MessageFormatter formatter = new AdvancedMessageFormatter(\n\t\t\tConfiguration.getLocale(),\n\t\t\tConfiguration.isEscapingEnabled()\n\t);\n\tpublic TinyLog2(Class<?> clazz) {\n\t\tthis(null == clazz ? StrUtil.NULL : clazz.getName());\n\t}\n\n\tpublic TinyLog2(String name) {\n\t\tthis.name = name;\n\t\tthis.level = provider.getMinimumLevel().ordinal();\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n\t// ------------------------------------------------------------------------- Trace\n\t@Override\n\tpublic boolean isTraceEnabled() {\n\t\treturn this.level <= Level.TRACE.ordinal();\n\t}\n\n\t@Override\n\tpublic void trace(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.TRACE, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Debug\n\t@Override\n\tpublic boolean isDebugEnabled() {\n\t\treturn this.level <= Level.DEBUG.ordinal();\n\t}\n\n\t@Override\n\tpublic void debug(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.DEBUG, t, format, arguments);\n\t}\n\t// ------------------------------------------------------------------------- Info\n\t@Override\n\tpublic boolean isInfoEnabled() {\n\t\treturn this.level <= Level.INFO.ordinal();\n\t}\n\n\t@Override\n\tpublic void info(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.INFO, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Warn\n\t@Override\n\tpublic boolean isWarnEnabled() {\n\t\treturn this.level <= Level.WARN.ordinal();\n\t}\n\n\t@Override\n\tpublic void warn(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.WARN, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Error\n\t@Override\n\tpublic boolean isErrorEnabled() {\n\t\treturn this.level <= Level.ERROR.ordinal();\n\t}\n\n\t@Override\n\tpublic void error(String fqcn, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, Level.ERROR, t, format, arguments);\n\t}\n\n\t// ------------------------------------------------------------------------- Log\n\t@Override\n\tpublic void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) {\n\t\tlogIfEnabled(fqcn, toTinyLevel(level), t, format, arguments);\n\t}\n\n\t@Override\n\tpublic boolean isEnabled(cn.hutool.log.level.Level level) {\n\t\treturn this.level <= toTinyLevel(level).ordinal();\n\t}\n\n\t/**\n\t * 在对应日志级别打开情况下打印日志\n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param level 日志级别\n\t * @param t 异常，null则检查最后一个参数是否为Throwable类型，是则取之，否则不打印堆栈\n\t * @param format 日志消息模板\n\t * @param arguments 日志消息参数\n\t */\n\tprivate void logIfEnabled(String fqcn, Level level, Throwable t, String format, Object... arguments) {\n\t\t// fqcn 无效\n\t\tif(null == t){\n\t\t\tt = getLastArgumentIfThrowable(arguments);\n\t\t}\n\t\tprovider.log(DEPTH, null, level, t, formatter, StrUtil.toString(format), arguments);\n\t}\n\n\t/**\n\t * 将Hutool的Level等级转换为Tinylog的Level等级\n\t *\n\t * @param level Hutool的Level等级\n\t * @return Tinylog的Level\n\t * @since 4.0.3\n\t */\n\tprivate Level toTinyLevel(cn.hutool.log.level.Level level) {\n\t\tLevel tinyLevel;\n\t\tswitch (level) {\n\t\tcase TRACE:\n\t\t\ttinyLevel = Level.TRACE;\n\t\t\tbreak;\n\t\tcase DEBUG:\n\t\t\ttinyLevel = Level.DEBUG;\n\t\t\tbreak;\n\t\tcase INFO:\n\t\t\ttinyLevel = Level.INFO;\n\t\t\tbreak;\n\t\tcase WARN:\n\t\t\ttinyLevel = Level.WARN;\n\t\t\tbreak;\n\t\tcase ERROR:\n\t\t\ttinyLevel = Level.ERROR;\n\t\t\tbreak;\n\t\tcase OFF:\n\t\t\ttinyLevel = Level.OFF;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error(StrUtil.format(\"Can not identify level: {}\", level));\n\t\t}\n\t\treturn tinyLevel;\n\t}\n\n\t/**\n\t * 如果最后一个参数为异常参数，则获取之，否则返回null\n\t *\n\t * @param arguments 参数\n\t * @return 最后一个异常参数\n\t * @since 4.0.3\n\t */\n\tprivate static Throwable getLastArgumentIfThrowable(Object... arguments) {\n\t\tif (ArrayUtil.isNotEmpty(arguments) && arguments[arguments.length - 1] instanceof Throwable) {\n\t\t\treturn (Throwable) arguments[arguments.length - 1];\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog2Factory.java",
    "content": "package cn.hutool.log.dialect.tinylog;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"http://www.tinylog.org/\">TinyLog2</a> log.<br>\n * \n * @author Looly\n *\n */\npublic class TinyLog2Factory extends LogFactory {\n\n\t/**\n\t * 构造\n\t */\n\tpublic TinyLog2Factory() {\n\t\tsuper(\"TinyLog\");\n\t\tcheckLogExist(org.tinylog.Logger.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new TinyLog2(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new TinyLog2(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLogFactory.java",
    "content": "package cn.hutool.log.dialect.tinylog;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\n/**\n * <a href=\"http://www.tinylog.org/\">TinyLog</a> log.<br>\n * \n * @author Looly\n *\n */\npublic class TinyLogFactory extends LogFactory {\n\t\n\t/**\n\t * 构造\n\t */\n\tpublic TinyLogFactory() {\n\t\tsuper(\"TinyLog\");\n\t\tcheckLogExist(org.pmw.tinylog.Logger.class);\n\t}\n\n\t@Override\n\tpublic Log createLog(String name) {\n\t\treturn new TinyLog(name);\n\t}\n\n\t@Override\n\tpublic Log createLog(Class<?> clazz) {\n\t\treturn new TinyLog(clazz);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/package-info.java",
    "content": "/**\n * <a href=\"http://www.tinylog.org/\">tinylog</a>的实现封装<br>\n * 封装包括TinyLog和TinyLog2\n *\n * @author looly\n */\npackage cn.hutool.log.dialect.tinylog;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/DebugLog.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * DEBUG级别日志接口\n * @author Looly\n *\n */\npublic interface DebugLog {\n\t/**\n\t * @return DEBUG 等级是否开启\n\t */\n\tboolean isDebugEnabled();\n\n\t/**\n\t * 打印 DEBUG 等级的日志\n\t * \n\t * @param t 错误对象\n\t */\n\tvoid debug(Throwable t);\n\n\t/**\n\t * 打印 DEBUG 等级的日志\n\t * \n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid debug(String format, Object... arguments);\n\n\t/**\n\t * 打印 DEBUG 等级的日志\n\t * \n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid debug(Throwable t, String format, Object... arguments);\n\t\n\t/**\n\t * 打印 DEBUG 等级的日志\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid debug(String fqcn, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/ErrorLog.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * ERROR级别日志接口\n * @author Looly\n *\n */\npublic interface ErrorLog {\n\t/**\n\t * @return ERROR 等级是否开启\n\t */\n\tboolean isErrorEnabled();\n\n\t/**\n\t * 打印 ERROR 等级的日志\n\t * \n\t * @param t 错误对象\n\t */\n\tvoid error(Throwable t);\n\n\t/**\n\t * 打印 ERROR 等级的日志\n\t * \n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid error(String format, Object... arguments);\n\n\t/**\n\t * 打印 ERROR 等级的日志\n\t * \n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid error(Throwable t, String format, Object... arguments);\n\t\n\t/**\n\t * 打印 ERROR 等级的日志\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid error(String fqcn, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/InfoLog.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * INFO级别日志接口\n * @author Looly\n *\n */\npublic interface InfoLog {\n\t/**\n\t * @return INFO 等级是否开启\n\t */\n\tboolean isInfoEnabled();\n\n\t/**\n\t * 打印 INFO 等级的日志\n\t * \n\t * @param t 错误对象\n\t */\n\tvoid info(Throwable t);\n\n\t/**\n\t * 打印 INFO 等级的日志\n\t * \n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid info(String format, Object... arguments);\n\n\t/**\n\t * 打印 INFO 等级的日志\n\t * \n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid info(Throwable t, String format, Object... arguments);\n\t\n\t/**\n\t * 打印 INFO 等级的日志\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid info(String fqcn, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/Level.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * 日志等级\n * @author Looly\n *\n */\npublic enum Level{\n\t/**\n\t * 'ALL' log level.\n\t */\n\tALL,\n\t/**\n\t * 'TRACE' log level.\n\t */\n\tTRACE,\n\t/**\n\t * 'DEBUG' log level.\n\t */\n\tDEBUG,\n\t/**\n\t * 'INFO' log level.\n\t */\n\tINFO,\n\t/**\n\t * 'WARN' log level.\n\t */\n\tWARN,\n\t/**\n\t * 'ERROR' log level.\n\t */\n\tERROR,\n\t/**\n\t * 'FATAL' log level.\n\t */\n\tFATAL,\n\t/**\n\t * 'OFF' log.\n\t */\n\tOFF\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/TraceLog.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * TRACE级别日志接口\n * @author Looly\n *\n */\npublic interface TraceLog {\n\t/**\n\t * @return TRACE 等级是否开启\n\t */\n\tboolean isTraceEnabled();\n\n\t/**\n\t * 打印 TRACE 等级的日志\n\t * \n\t * @param t 错误对象\n\t */\n\tvoid trace(Throwable t);\n\n\t/**\n\t * 打印 TRACE 等级的日志\n\t * \n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid trace(String format, Object... arguments);\n\n\t/**\n\t * 打印 TRACE 等级的日志\n\t * \n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid trace(Throwable t, String format, Object... arguments);\n\t\n\t/**\n\t * 打印 TRACE 等级的日志\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid trace(String fqcn, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/WarnLog.java",
    "content": "package cn.hutool.log.level;\n\n/**\n * WARN级别日志接口\n * @author Looly\n *\n */\npublic interface WarnLog {\n\t/**\n\t * @return WARN 等级是否开启\n\t */\n\tboolean isWarnEnabled();\n\n\t/**\n\t * 打印 WARN 等级的日志\n\t * \n\t * @param t 错误对象\n\t */\n\tvoid warn(Throwable t);\n\n\t/**\n\t * 打印 WARN 等级的日志\n\t * \n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid warn(String format, Object... arguments);\n\n\t/**\n\t * 打印 WARN 等级的日志\n\t * \n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid warn(Throwable t, String format, Object... arguments);\n\t\n\t/**\n\t * 打印 WARN 等级的日志\n\t * \n\t * @param fqcn 完全限定类名(Fully Qualified Class Name)，用于定位日志位置\n\t * @param t 错误对象\n\t * @param format 消息模板\n\t * @param arguments 参数\n\t */\n\tvoid warn(String fqcn, Throwable t, String format, Object... arguments);\n}\n"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/level/package-info.java",
    "content": "/**\n * 按照日志级别定义的日志打印接口定义\n * \n * @author looly\n *\n */\npackage cn.hutool.log.level;"
  },
  {
    "path": "hutool-log/src/main/java/cn/hutool/log/package-info.java",
    "content": "/**\n * Hutool-log只是一个日志的通用门面，功能类似于Slf4j。<br>\n * 根据加入ClassPath中的jar包动态检测日志实现的方式，使日志使用更加便利灵活。\n *\n * @author looly\n *\n */\npackage cn.hutool.log;\n"
  },
  {
    "path": "hutool-log/src/main/resources/META-INF/services/cn.hutool.log.LogFactory",
    "content": "cn.hutool.log.dialect.logtube.LogTubeLogFactory\ncn.hutool.log.dialect.slf4j.Slf4jLogFactory\ncn.hutool.log.dialect.log4j2.Log4j2LogFactory\ncn.hutool.log.dialect.log4j.Log4jLogFactory\ncn.hutool.log.dialect.commons.ApacheCommonsLogFactory\ncn.hutool.log.dialect.tinylog.TinyLogFactory\ncn.hutool.log.dialect.tinylog.TinyLog2Factory\ncn.hutool.log.dialect.jboss.JbossLogFactory\n"
  },
  {
    "path": "hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java",
    "content": "package cn.hutool.log.test;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.dialect.commons.ApacheCommonsLogFactory;\nimport cn.hutool.log.dialect.console.ConsoleLogFactory;\nimport cn.hutool.log.dialect.jboss.JbossLogFactory;\nimport cn.hutool.log.dialect.jdk.JdkLogFactory;\nimport cn.hutool.log.dialect.log4j.Log4jLogFactory;\nimport cn.hutool.log.dialect.log4j2.Log4j2LogFactory;\nimport cn.hutool.log.dialect.slf4j.Slf4jLogFactory;\nimport cn.hutool.log.dialect.tinylog.TinyLog2Factory;\nimport cn.hutool.log.dialect.tinylog.TinyLogFactory;\nimport org.junit.jupiter.api.Test;\n\n/**\n * 日志门面单元测试\n * @author Looly\n *\n */\npublic class CustomLogTest {\n\n\tprivate static final String LINE = \"----------------------------------------------------------------------\";\n\n\t@Test\n\tpublic void consoleLogTest(){\n\t\tLogFactory factory = new ConsoleLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void consoleLogNullTest(){\n\t\tLogFactory factory = new ConsoleLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t}\n\n\t@Test\n\tpublic void commonsLogTest(){\n\t\tLogFactory factory = new ApacheCommonsLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void tinyLogTest(){\n\t\tLogFactory factory = new TinyLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void tinyLog2Test(){\n\t\tLogFactory factory = new TinyLog2Factory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void log4j2LogTest(){\n\t\tLogFactory factory = new Log4j2LogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.debug(null);\n\t\tlog.debug(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void log4jLogTest(){\n\t\tLogFactory factory = new Log4jLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\n\t}\n\n\t@Test\n\tpublic void jbossLogTest(){\n\t\tLogFactory factory = new JbossLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void jdkLogTest(){\n\t\tLogFactory factory = new JdkLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n\n\t@Test\n\tpublic void slf4jTest(){\n\t\tLogFactory factory = new Slf4jLogFactory(false);\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\n\t\tlog.info(null);\n\t\tlog.info((String)null);\n\t\tlog.info(\"This is custom '{}' log\\n{}\", factory.getName(), LINE);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/test/java/cn/hutool/log/test/LogTest.java",
    "content": "package cn.hutool.log.test;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.level.Level;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 日志门面单元测试\n * @author Looly\n *\n */\npublic class LogTest {\n\n\t@Test\n\tpublic void logTest(){\n\t\tLog log = LogFactory.get();\n\n\t\t// 自动选择日志实现\n\t\tlog.debug(\"This is {} log\", Level.DEBUG);\n\t\tlog.info(\"This is {} log\", Level.INFO);\n\t\tlog.warn(\"This is {} log\", Level.WARN);\n\n//\t\tException e = new Exception(\"test Exception\");\n//\t\tlog.error(e, \"This is {} log\", Level.ERROR);\n\t}\n\n\t/**\n\t * 兼容slf4j日志消息格式测试，即第二个参数是异常对象时正常输出异常信息\n\t */\n\t@Test\n\t@Disabled\n\tpublic void logWithExceptionTest() {\n\t\tLog log = LogFactory.get();\n\t\tException e = new Exception(\"test Exception\");\n\t\tlog.error(\"我是错误消息\", e);\n\t}\n\n\t@Test\n\tpublic void logNullTest(){\n\t\tfinal Log log = Log.get();\n\t\tlog.debug(null);\n\t\tlog.info(null);\n\t\tlog.warn(null);\n\t}\n\n\t@Test\n\tpublic void parameterizedMessageEdgeCasesTest() {\n\t\tLog log = LogFactory.get();\n\n\t\t// 测试不同数量的参数\n\t\tlog.info(\"No parameters\");\n\t\tlog.info(\"One: {}\", \"param1\");\n\t\tlog.info(\"Two: {} and {}\", \"param1\", \"param2\");\n\t\tlog.info(\"Three: {}, {}, {}\", \"param1\", \"param2\", \"param3\");\n\t\tlog.info(\"Four: {}, {}, {}, {}\", \"param1\", \"param2\", \"param3\", \"param4\");\n\n\t\t// 测试参数不足的情况\n\t\tlog.info(\"Missing param: {} and {}\", \"only_one\");\n\n\t\t// 测试参数过多的情况\n\t\tlog.info(\"Extra param: {}\", \"param1\", \"extra_param\");\n\t}\n\n\t@Test\n\tpublic void i18nMessageTest() {\n\t\tLog log = LogFactory.get();\n\t\t// 国际化消息测试\n\t\tlog.info(\"中文消息测试\");\n\t\tlog.info(\"Message with unicode: {}\", \"特殊字符©®™✓✗★☆\");\n\t\tlog.info(\"多语言混排: 中文, English, 日本語, 한글\");\n\t\tlog.info(\"Emoji测试: 😀🚀🌏\");\n\t}\n\n\t@Test\n\tpublic void complexObjectTest() {\n\t\tLog log = LogFactory.get();\n\t\t// 复杂对象参数测试\n\t\tList<String> list = Arrays.asList(\"item1\", \"item2\");\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"key\", \"value\");\n\n\t\tlog.info(\"List: {}\", list);\n\t\tlog.info(\"Map: {}\", map);\n\t\tlog.info(\"Null object: {}\", (Object)null);\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/test/java/cn/hutool/log/test/LogTubeTest.java",
    "content": "package cn.hutool.log.test;\n\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.dialect.logtube.LogTubeLogFactory;\nimport org.junit.jupiter.api.Test;\n\npublic class LogTubeTest {\n\n\t@Test\n\tpublic void logTest(){\n\t\tLogFactory factory = new LogTubeLogFactory();\n\t\tLogFactory.setCurrentLogFactory(factory);\n\t\tLog log = LogFactory.get();\n\t\tlog.debug(\"LogTube debug test.\");\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/test/java/cn/hutool/log/test/StaticLogTest.java",
    "content": "package cn.hutool.log.test;\n\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.StaticLog;\nimport cn.hutool.log.dialect.console.ConsoleColorLogFactory;\nimport cn.hutool.log.dialect.console.ConsoleLogFactory;\nimport org.junit.jupiter.api.Test;\n\npublic class StaticLogTest {\n\t@Test\n\tpublic void test() {\n\t\tLogFactory.setCurrentLogFactory(ConsoleLogFactory.class);\n\t\tStaticLog.debug(\"This is static {} log\", \"debug\");\n\t\tStaticLog.info(\"This is static {} log\", \"info\");\n\t}\n\n\t@Test\n\tpublic void colorTest(){\n\t\tLogFactory.setCurrentLogFactory(ConsoleColorLogFactory.class);\n\t\tStaticLog.debug(\"This is static {} log\", \"debug\");\n\t\tStaticLog.info(\"This is static {} log\", \"info\");\n\t\tStaticLog.error(\"This is static {} log\", \"error\");\n\t\tStaticLog.warn(\"This is static {} log\", \"warn\");\n\t\tStaticLog.trace(\"This is static {} log\", \"trace\");\n\t}\n}\n"
  },
  {
    "path": "hutool-log/src/test/resources/example/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- @see http://www.cnblogs.com/hanfight/p/5721855.html -->\n<!-- status : 这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,会看到log4j2内部各种详细输出 monitorInterval : Log4j能够自动检测修改配置文件和重新配置本身, 设置间隔秒数。 -->\n<Configuration status=\"WARN\" monitorInterval=\"600\">\n\n\t<Properties>\n\t\t<!-- 配置日志文件输出目录 -->\n\t\t<Property name=\"LOG_HOME\">/home/logs</Property>\n\t</Properties>\n\n\t<Appenders>\n\n\t\t<!--这个输出控制台的配置 -->\n\t\t<Console name=\"Console\" target=\"SYSTEM_OUT\">\n\t\t\t<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->\n\t\t\t<ThresholdFilter level=\"debug\" onMatch=\"ACCEPT\" onMismatch=\"DENY\" />\n\t\t\t<!-- 输出日志的格式 -->\n\t\t\t<PatternLayout pattern=\"[%d{HH:mm:ss.SSS}][%-5level] %class{36}:%L %M - %msg%xEx%n\" />\n\t\t</Console>\n\n\t\t<!-- 设置日志格式并配置日志压缩格式(service.log.年份.gz) -->\n\t\t<RollingRandomAccessFile name=\"service_appender\" immediateFlush=\"false\" fileName=\"${LOG_HOME}/service.log\" filePattern=\"${LOG_HOME}/service.log.%d{yyyy-MM-dd}.log.gz\">\n\t\t\t<!-- %d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间 %p : 日志输出格式 %c : logger的名称 %m : 日志内容，即 logger.info(\"message\") %n : 换行符 %C : Java类名 %L : 日志输出所在行数 %M : 日志输出所在方法名 hostName : 本地机器名 hostAddress : 本地ip地址 -->\n\t\t\t<PatternLayout>\n\t\t\t\t<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36}:%L %M -- %msg%xEx%n</pattern>\n\t\t\t</PatternLayout>\n\t\t\t<Policies>\n\t\t\t\t<TimeBasedTriggeringPolicy interval=\"1\" modulate=\"true\" />\n\t\t\t</Policies>\n\t\t</RollingRandomAccessFile>\n\n\n\t\t<!-- DEBUG日志格式 -->\n\t\t<RollingRandomAccessFile name=\"service_debug_appender\" immediateFlush=\"false\" fileName=\"${LOG_HOME}/service.log\" filePattern=\"${LOG_HOME}/service.log.%d{yyyy-MM-dd}.debug.gz\">\n\t\t\t<PatternLayout>\n\t\t\t\t<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36}:%L %M -- %msg%xEx%n</pattern>\n\t\t\t</PatternLayout>\n\t\t\t<Policies>\n\t\t\t\t<TimeBasedTriggeringPolicy interval=\"1\" modulate=\"true\" />\n\t\t\t</Policies>\n\t\t</RollingRandomAccessFile>\n\t</Appenders>\n\n\t<Loggers>\n\t\t<!-- 配置日志的根节点 -->\n\t\t<root level=\"debug\">\n\t\t\t<appender-ref ref=\"Console\" />\n\t\t</root>\n\n\t\t<!-- 第三方日志系统 -->\n\t\t<logger name=\"org.springframework.core\" level=\"info\" />\n\t\t<logger name=\"org.springframework.beans\" level=\"info\" />\n\t\t<logger name=\"org.springframework.context\" level=\"info\" />\n\t\t<logger name=\"org.springframework.web\" level=\"info\" />\n\t\t<logger name=\"org.jboss.netty\" level=\"warn\" />\n\t\t<logger name=\"org.apache.http\" level=\"warn\" />\n\n\t\t<!-- 日志实例(info),其中'service-log'继承root,但是root将日志输出控制台,而'service-log'将日志输出到文件,通过属性'additivity=\"false\"'将'service-log'的 的日志不再输出到控制台 -->\n\t\t<!-- logger name类似于java package一样 -->\n\t\t<logger name=\"service_log\" level=\"info\" includeLocation=\"true\" additivity=\"true\">\n\t\t\t<appender-ref ref=\"service_appender\" />\n\t\t</logger>\n\n\t\t<!-- 日志实例(debug) -->\n\t\t<logger name=\"service_log\" level=\"debug\" includeLocation=\"true\" additivity=\"false\">\n\t\t\t<appender-ref ref=\"service_debug_appender\" />\n\t\t</logger>\n\n\t</Loggers>\n\n</Configuration>"
  },
  {
    "path": "hutool-log/src/test/resources/log4j.properties",
    "content": "log4j.rootLogger=debug,STDOUT\n\nlog4j.additivity.org.apache=true\nlog4j.appender.STDOUT=org.apache.log4j.ConsoleAppender\nlog4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout\nlog4j.appender.STDOUT.layout.ConversionPattern=[%d{HH:mm:ss,SSS}][%5p] %c:%L - %m%n"
  },
  {
    "path": "hutool-log/src/test/resources/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- @see http://www.cnblogs.com/hanfight/p/5721855.html -->\n<!-- status : 这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,会看到log4j2内部各种详细输出 monitorInterval : Log4j能够自动检测修改配置文件和重新配置本身, 设置间隔秒数。 -->\n<Configuration status=\"WARN\" monitorInterval=\"600\">\n\n\t<Appenders>\n\t\t<!--这个输出控制台的配置 -->\n\t\t<Console name=\"Console\" target=\"SYSTEM_OUT\">\n\t\t\t<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->\n\t\t\t<ThresholdFilter level=\"debug\" onMatch=\"ACCEPT\" onMismatch=\"DENY\" />\n\t\t\t<!-- 输出日志的格式 -->\n\t\t\t<PatternLayout pattern=\"[%d{HH:mm:ss.SSS}][%highlight{%-5level}] %class{36}:%L %M - %msg%xEx%n\" disableAnsi=\"false\" />\n\t\t</Console>\n\t</Appenders>\n\n\t<Loggers>\n\t\t<!-- 配置日志的根节点 -->\n\t\t<root level=\"debug\">\n\t\t\t<appender-ref ref=\"Console\" />\n\t\t</root>\n\t</Loggers>\n\n</Configuration>"
  },
  {
    "path": "hutool-log/src/test/resources/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<configuration scan=\"false\">\n\t<property name=\"format\" value=\"%d{HH:mm:ss.SSS} [%thread] %-5level %c:%L- %msg%n\" />\n\n\t<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n\t\t<!-- encoder 默认配置为PatternLayoutEncoder -->\n\t\t<encoder>\n\t\t\t<pattern>${format}</pattern>\n\t\t</encoder>\n\t</appender>\n\t\n\t<root level=\"debug\">\n\t\t<appender-ref ref=\"STDOUT\" />\n\t</root>\n\n</configuration>"
  },
  {
    "path": "hutool-log/src/test/resources/logging.properties",
    "content": "#-----------------------------------------------------------------------------------------------------------\n# == JDK Logging \\u914d\\u7f6e\\u6587\\u4ef6 ==\n#Level\\u7684\\u4e94\\u4e2a\\u7b49\\u7ea7\n# SEVERE\n# WARNING\n# INFO\n# CONFIG\n# FINE\n# FINER\n# FINEST\n# \n#-----------------------------------------------------------------------------------------------------------\n\n# \\u65e5\\u5fd7\\u683c\\u5f0f\n#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n\n\n#\\u6307\\u5b9aRoot Logger\\u7ea7\\u522b\n.level= ALL\n#\\u4e3a Handler \\u6307\\u5b9a\\u9ed8\\u8ba4\\u7684\\u7ea7\\u522b\\uff08\\u9ed8\\u8ba4\\u4e3a Level.INFO\\uff09\\u3002\njava.util.logging.ConsoleHandler.level=ALL\n# \\u6307\\u5b9a\\u8981\\u4f7f\\u7528\\u7684 Formatter \\u7c7b\\u7684\\u540d\\u79f0\\uff08\\u9ed8\\u8ba4\\u4e3a java.util.logging.SimpleFormatter\\uff09\\u3002\njava.util.logging.ConsoleHandler.formatter=logging.formatter.MySimpleFormatter\n\n# \\u4e3a Handler \\u6307\\u5b9a\\u9ed8\\u8ba4\\u7684\\u7ea7\\u522b\\uff08\\u9ed8\\u8ba4\\u4e3a Level.ALL\\uff09\\u3002\njava.util.logging.FileHandler.level=ALL\n# \\u6307\\u5b9a\\u8981\\u4f7f\\u7528\\u7684 Formatter \\u7c7b\\u7684\\u540d\\u79f0\\uff08\\u9ed8\\u8ba4\\u4e3a java.util.logging.XMLFormatter\\uff09\\u3002\njava.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter\n# \\u6307\\u5b9a\\u8981\\u5199\\u5165\\u5230\\u4efb\\u610f\\u6587\\u4ef6\\u7684\\u8fd1\\u4f3c\\u6700\\u5927\\u91cf\\uff08\\u4ee5\\u5b57\\u8282\\u4e3a\\u5355\\u4f4d\\uff09\\u3002\\u5982\\u679c\\u8be5\\u6570\\u4e3a 0\\uff0c\\u5219\\u6ca1\\u6709\\u9650\\u5236\\uff08\\u9ed8\\u8ba4\\u4e3a\\u65e0\\u9650\\u5236\\uff09\\u3002\njava.util.logging.FileHandler.limit=1024000\n# \\u6307\\u5b9a\\u6709\\u591a\\u5c11\\u8f93\\u51fa\\u6587\\u4ef6\\u53c2\\u4e0e\\u5faa\\u73af\\uff08\\u9ed8\\u8ba4\\u4e3a 1\\uff09\\u3002\njava.util.logging.FileHandler.count=1\n# \\u4e3a\\u751f\\u6210\\u7684\\u8f93\\u51fa\\u6587\\u4ef6\\u540d\\u79f0\\u6307\\u5b9a\\u4e00\\u4e2a\\u6a21\\u5f0f\\u3002\\u6709\\u5173\\u7ec6\\u8282\\u8bf7\\u53c2\\u89c1\\u4ee5\\u4e0b\\u5185\\u5bb9\\uff08\\u9ed8\\u8ba4\\u4e3a \"%h/java%u.log\"\\uff09\\u3002\njava.util.logging.FileHandler.pattern=demo.log\n# \\u6307\\u5b9a\\u662f\\u5426\\u5e94\\u8be5\\u5c06 FileHandler \\u8ffd\\u52a0\\u5230\\u4efb\\u4f55\\u73b0\\u6709\\u6587\\u4ef6\\u4e0a\\uff08\\u9ed8\\u8ba4\\u4e3a false\\uff09\\u3002\njava.util.logging.FileHandler.append=true\n\n# \\u6267\\u884c\\u7684LogHandler\\uff0c\\u4f7f\\u7528\\u9017\\u53f7\\u9694\\u5f00\n#handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler\nhandlers= java.util.logging.ConsoleHandler"
  },
  {
    "path": "hutool-log/src/test/resources/logtube.properties",
    "content": "##########################################\n#\n# 项目基本信息\n#\n##########################################\n#\n# 项目名 !!!按照实际需要修改!!! 不允许点号，使用 - 分割\nlogtube.project=test-hutool\n#\n# 项目环境 !!!按照实际需要修改!!! 允许的值为 'dev', 'test', 'staging' 和 'prod'\nlogtube.env=test\n#\n# 全局主题过滤器，默认设置为不包括 trace 和 debug，调试环境可以酌情打开\nlogtube.topics.root=ALL,-trace\n#\n# 分包主题过滤器\n#\n# logtube.topics.com.google.gson=error\n# logtube.topics.com.google.gson=error\n#\n# 全局主题映射，trace 合并进入 debug, error 重命名为 err, warn 合并进入 info\nlogtube.topic-mappings=trace=debug,error=err,warn=info\n#\n#\n##########################################\n#\n# 命令行输出\n#\n##########################################\n#\n# 是否开启命令行输出，设置为开启\nlogtube.console.enabled=true\n#\n# 命令行输出设置为包括所有主题 （仍然受制于全局过滤器）\nlogtube.console.topics=ALL\n#\n#\n##########################################\n#\n# 日志文件输出\n#\n##########################################\n#\n# 启动日志文件输出\n#\nlogtube.file.enabled=false\n#\n# 日志文件输出包含所有主题（仍然受制于全局过滤器）\nlogtube.file.topics=ALL\n#\n# 日志文件重新打开信号文件，用于 logrotate\nlogtube.file.signal=/tmp/xlog.reopen.txt\n#\n# 日志文件路径\nlogtube.file.dir=logs\n#\n# 日志子文件夹，除了 trace 和 debug 日志进入 others 子文件夹，剩下的全部进入 xlog 子文件夹\n# 一般情况下，xlog 子文件夹的日志文件会被收集，然后进入 xlog 系统\nlogtube.file.subdir-mappings=ALL=xlog,trace=others,debug=others\n#\n#\n##########################################\n#\n# 远程输出，Redis 协议\n#\n##########################################\n#\n# 是否开启远程输出，设置为关闭\nlogtube.redis.enabled=false\n#\n# 远程输出设置为包含所有主题（仍然受制于全局主题过滤器）\nlogtube.redis.topics=ALL\n#\n# 远程输出目标主机，设置为为 127.0.0.1:6379，可使用逗号分隔多个主机\nlogtube.redis.hosts=127.0.0.1:6379\n#\n# 远程输出 Redis 键值\nlogtube.redis.key=xlog\n#\n#########################################\n#\n# 组件配置\n#\n#########################################\n#\n# 响应时间 > 100 ms 的 Redis 操作会被 LogtubeRedis 组件汇报\nlogtube.filter.redis-min-duration=100\n#\n# 结果集 > 1000 bytes 的 Redis 操作会被 LogtubeRedis 组件汇报\nlogtube.filter.redis-min-result-size=1000\n"
  },
  {
    "path": "hutool-log/src/test/resources/tinylog.properties",
    "content": "#-----------------------------------------------------------------------------------------------------------\n# == TinyLog \\u914d\\u7f6e\\u6587\\u4ef6 ==\n# \\u914d\\u7f6e\\u89c1\\uff1ahttp://www.tinylog.org/configuration#file\n#-----------------------------------------------------------------------------------------------------------\n\n# \\u65e5\\u5fd7\\u5199\\u51fa\\u65b9\\u5f0f\\uff1a[console] [file] [jdbc] [logcat] [rollingfile] [sharedfile] [null]\ntinylog.writer = console\nwriter = console\n# \\u65e5\\u5fd7\\u5199\\u51fa\\u7ea7\\u522b\\uff1aTRACE < DEBUG < INFO < WARNING < ERROR < OFF\ntinylog.level = debug\n# \\u65e5\\u5fd7\\u6253\\u5370\\u683c\\u5f0f\ntinylog.format = [{date:HH:mm:ss}][{level}] {class}:{line} - {message}\nwriter.format = [{date:HH:mm:ss}][{level}] {class}:{line} - {message}"
  },
  {
    "path": "hutool-poi/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-poi</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool POI工具类（对Office文档、OFD等操作）</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.poi</Automatic-Module-Name>\n\n\t\t<!-- versions -->\n\t\t<poi.version>4.1.2</poi.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\n\t\t<!-- POI -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.poi</groupId>\n\t\t\t<artifactId>poi-ooxml</artifactId>\n\t\t\t<version>${poi.version}</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- OFD -->\n\t\t<dependency>\n\t\t\t<groupId>org.ofdrw</groupId>\n\t\t\t<artifactId>ofdrw-full</artifactId>\n\t\t\t<version>2.3.1</version>\n\t\t\t<scope>compile</scope>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/GlobalPoiConfig.java",
    "content": "package cn.hutool.poi;\n\nimport org.apache.poi.openxml4j.util.ZipSecureFile;\n\n/**\n * POI的全局设置\n *\n * @author Looly\n * @see ZipSecureFile\n * @since 5.8.30\n */\npublic class GlobalPoiConfig {\n\n\t/**\n\t * 设置解压时的最小压缩比例<br>\n\t * 为了避免`Zip Bomb`，POI中设置了最小压缩比例，这个比例为：\n\t * <pre>\n\t *     压缩后的大小/解压后的大小\n\t * </pre>\n\t * <p>\n\t * POI的默认值是0.01（即最小压缩到1%），如果文档中的文件压缩比例小于这个值，就会报错。<br>\n\t * 如果文件中确实存在高压缩比的文件，可以通过这个全局方法自定义比例，从而避免错误。\n\t *\n\t * @param ratio 解压后的文件大小与原始文件大小的最小比率，小于等于0表示不检查\n\t */\n\tpublic static void setMinInflateRatio(final double ratio) {\n\t\tZipSecureFile.setMinInflateRatio(ratio);\n\t}\n\n\t/**\n\t * 设置单个Zip文件中最大文件大小，默认为4GB，即32位zip格式的最大值。\n\t *\n\t * @param maxEntrySize 单个Zip文件中最大文件大小，必须大于0\n\t */\n\tpublic static void setMaxEntrySize(final long maxEntrySize) {\n\t\tZipSecureFile.setMaxEntrySize(maxEntrySize);\n\t}\n\n\t/**\n\t * 设置解压前文本的最大字符数，超过抛出异常。\n\t *\n\t * @param maxTextSize 文本的最大字符数\n\t * @throws IllegalArgumentException for negative maxTextSize\n\t */\n\tpublic static void setMaxTextSize(final long maxTextSize) {\n\t\tZipSecureFile.setMaxTextSize(maxTextSize);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/PoiChecker.java",
    "content": "package cn.hutool.poi;\n\nimport cn.hutool.core.exceptions.DependencyException;\nimport cn.hutool.core.util.ClassLoaderUtil;\n\n/**\n * POI引入检查器\n *\n * @author looly\n * @since 4.0.10\n */\npublic class PoiChecker {\n\n\t/** 没有引入POI的错误消息 */\n\tpublic static final String NO_POI_ERROR_MSG = \"You need to add dependency of 'poi-ooxml' to your project, and version >= 4.1.2\";\n\n\t/**\n\t * 检查POI包的引入情况\n\t */\n\tpublic static void checkPoiImport() {\n\t\ttry {\n\t\t\tClass.forName(\"org.apache.poi.ss.usermodel.Workbook\", false, ClassLoaderUtil.getClassLoader());\n\t\t} catch (ClassNotFoundException | NoClassDefFoundError | NoSuchMethodError e) {\n\t\t\tthrow new DependencyException(e, NO_POI_ERROR_MSG);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/BigExcelWriter.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.xssf.streaming.SXSSFSheet;\nimport org.apache.poi.xssf.streaming.SXSSFWorkbook;\n\nimport java.io.File;\nimport java.io.OutputStream;\n\n/**\n * 大数据量Excel写出，只支持XLSX（Excel07版本）<br>\n * 通过封装{@link SXSSFWorkbook}，限制对滑动窗口中的行的访问来实现其低内存使用。<br>\n * 注意如果写出数据大于滑动窗口大小，就会写出到临时文件，此时写出的数据无法访问和编辑。\n *\n * @author looly\n * @since 4.1.13\n */\npublic class BigExcelWriter extends ExcelWriter {\n\n\tpublic static final int DEFAULT_WINDOW_SIZE = SXSSFWorkbook.DEFAULT_WINDOW_SIZE;\n\n\t/**\n\t * BigExcelWriter只能flush一次，因此调用后不再重复写出\n\t */\n\tprivate boolean isFlushed;\n\n\t// -------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，默认生成xlsx格式的Excel文件<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t */\n\tpublic BigExcelWriter() {\n\t\tthis(DEFAULT_WINDOW_SIZE);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，需要调用{@link #flush(File)} 写出到文件\n\t *\n\t * @param rowAccessWindowSize 在内存中的行数\n\t */\n\tpublic BigExcelWriter(int rowAccessWindowSize) {\n\t\tthis(WorkbookUtil.createSXSSFBook(rowAccessWindowSize), null);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，需要调用{@link #flush(File)} 写出到文件\n\t *\n\t * @param rowAccessWindowSize   在内存中的行数，-1表示不限制，此时需要手动刷出\n\t * @param compressTmpFiles      是否使用Gzip压缩临时文件\n\t * @param useSharedStringsTable 是否使用共享字符串表，一般大量重复字符串时开启可节省内存\n\t * @param sheetName             写出的sheet名称\n\t * @since 5.7.23\n\t */\n\tpublic BigExcelWriter(int rowAccessWindowSize, boolean compressTmpFiles, boolean useSharedStringsTable, String sheetName) {\n\t\tthis(WorkbookUtil.createSXSSFBook(rowAccessWindowSize, compressTmpFiles, useSharedStringsTable), sheetName);\n\t}\n\n\t/**\n\t * 构造，默认写出到第一个sheet，第一个sheet名为sheet1\n\t *\n\t * @param destFilePath 目标文件路径，可以不存在\n\t */\n\tpublic BigExcelWriter(String destFilePath) {\n\t\tthis(destFilePath, null);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，需要调用{@link #flush(File)} 写出到文件\n\t *\n\t * @param rowAccessWindowSize 在内存中的行数\n\t * @param sheetName           sheet名，第一个sheet名并写出到此sheet，例如sheet1\n\t * @since 4.1.8\n\t */\n\tpublic BigExcelWriter(int rowAccessWindowSize, String sheetName) {\n\t\tthis(WorkbookUtil.createSXSSFBook(rowAccessWindowSize), sheetName);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFilePath 目标文件路径，可以不存在\n\t * @param sheetName    sheet名，第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic BigExcelWriter(String destFilePath, String sheetName) {\n\t\tthis(FileUtil.file(destFilePath), sheetName);\n\t}\n\n\t/**\n\t * 构造，默认写出到第一个sheet，第一个sheet名为sheet1\n\t *\n\t * @param destFile 目标文件，可以不存在\n\t */\n\tpublic BigExcelWriter(File destFile) {\n\t\tthis(destFile, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile  目标文件，可以不存在\n\t * @param sheetName sheet名，做为第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic BigExcelWriter(File destFile, String sheetName) {\n\t\tthis(destFile.exists() ? WorkbookUtil.createSXSSFBook(destFile) : WorkbookUtil.createSXSSFBook(), sheetName);\n\t\tthis.destFile = destFile;\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t *\n\t * @param workbook  {@link SXSSFWorkbook}\n\t * @param sheetName sheet名，做为第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic BigExcelWriter(SXSSFWorkbook workbook, String sheetName) {\n\t\tthis(WorkbookUtil.getOrCreateSheet(workbook, sheetName));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(java.io.OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t *\n\t * @param sheet {@link Sheet}\n\t * @since 4.0.6\n\t */\n\tpublic BigExcelWriter(Sheet sheet) {\n\t\tsuper(sheet);\n\t}\n\n\t// -------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic BigExcelWriter autoSizeColumn(int columnIndex) {\n\t\tfinal SXSSFSheet sheet = (SXSSFSheet) this.sheet;\n\t\tsheet.trackColumnForAutoSizing(columnIndex);\n\t\tsuper.autoSizeColumn(columnIndex);\n\t\tsheet.untrackColumnForAutoSizing(columnIndex);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic BigExcelWriter autoSizeColumnAll() {\n\t\treturn autoSizeColumnAll(0);\n\t}\n\n\t@Override\n\tpublic BigExcelWriter autoSizeColumnAll(float widthRatio) {\n\t\tfinal SXSSFSheet sheet = (SXSSFSheet) this.sheet;\n\t\tsheet.trackAllColumnsForAutoSizing();\n\t\tsuper.autoSizeColumnAll(widthRatio);\n\t\tsheet.untrackAllColumnsForAutoSizing();\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic ExcelWriter flush(OutputStream out, boolean isCloseOut) throws IORuntimeException {\n\t\tif (!isFlushed) {\n\t\t\tisFlushed = true;\n\t\t\treturn super.flush(out, isCloseOut);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tif (null != this.destFile && !isFlushed) {\n\t\t\tflush();\n\t\t}\n\n\t\t// 清理临时文件\n\t\t((SXSSFWorkbook) this.workbook).dispose();\n\t\tsuper.closeWithoutFlush();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelBase.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.poi.excel.cell.CellLocation;\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.common.usermodel.HyperlinkType;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.Hyperlink;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.apache.poi.xssf.streaming.SXSSFSheet;\nimport org.apache.poi.xssf.usermodel.XSSFSheet;\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Excel基础类，用于抽象ExcelWriter和ExcelReader中共用部分的对象和方法\n *\n * @param <T> 子类类型，用于返回this\n * @author looly\n * @since 4.1.4\n */\npublic class ExcelBase<T extends ExcelBase<T>> implements Closeable {\n\t/**\n\t * 是否被关闭\n\t */\n\tprotected boolean isClosed;\n\t/**\n\t * 目标文件，如果用户读取为流或自行创建的Workbook或Sheet,此参数为{@code null}\n\t */\n\tprotected File destFile;\n\t/**\n\t * 工作簿\n\t */\n\tprotected Workbook workbook;\n\t/**\n\t * Excel中对应的Sheet\n\t */\n\tprotected Sheet sheet;\n\t/**\n\t * 标题行别名\n\t */\n\tprotected Map<String, String> headerAlias;\n\n\t/**\n\t * 构造\n\t *\n\t * @param sheet Excel中的sheet\n\t */\n\tpublic ExcelBase(Sheet sheet) {\n\t\tAssert.notNull(sheet, \"No Sheet provided.\");\n\t\tthis.sheet = sheet;\n\t\tthis.workbook = sheet.getWorkbook();\n\t}\n\n\t/**\n\t * 获取Workbook\n\t *\n\t * @return Workbook\n\t */\n\tpublic Workbook getWorkbook() {\n\t\treturn this.workbook;\n\t}\n\n\t/**\n\t * 返回工作簿表格数\n\t *\n\t * @return 工作簿表格数\n\t * @since 4.0.10\n\t */\n\tpublic int getSheetCount() {\n\t\treturn this.workbook.getNumberOfSheets();\n\t}\n\n\t/**\n\t * 获取此工作簿所有Sheet表\n\t *\n\t * @return sheet表列表\n\t * @since 4.0.3\n\t */\n\tpublic List<Sheet> getSheets() {\n\t\tfinal int totalSheet = getSheetCount();\n\t\tfinal List<Sheet> result = new ArrayList<>(totalSheet);\n\t\tfor (int i = 0; i < totalSheet; i++) {\n\t\t\tresult.add(this.workbook.getSheetAt(i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取表名列表\n\t *\n\t * @return 表名列表\n\t * @since 4.0.3\n\t */\n\tpublic List<String> getSheetNames() {\n\t\tfinal int totalSheet = workbook.getNumberOfSheets();\n\t\tList<String> result = new ArrayList<>(totalSheet);\n\t\tfor (int i = 0; i < totalSheet; i++) {\n\t\t\tresult.add(this.workbook.getSheetAt(i).getSheetName());\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 获取当前Sheet\n\t *\n\t * @return {@link Sheet}\n\t */\n\tpublic Sheet getSheet() {\n\t\treturn this.sheet;\n\t}\n\n\n\t/**\n\t * 重命名当前sheet\n\t *\n\t * @param newName 新名字\n\t * @return this\n\t * @see Workbook#setSheetName(int, String)\n\t * @since 5.7.10\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T renameSheet(String newName) {\n\t\tthis.workbook.setSheetName(this.workbook.getSheetIndex(this.sheet), newName);\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 自定义需要读取或写出的Sheet，如果给定的sheet不存在，创建之。<br>\n\t * 在读取中，此方法用于切换读取的sheet，在写出时，此方法用于新建或者切换sheet。\n\t *\n\t * @param sheetName sheet名\n\t * @return this\n\t * @since 4.0.10\n\t */\n\tpublic T setSheet(String sheetName) {\n\t\treturn setSheet(WorkbookUtil.getOrCreateSheet(this.workbook, sheetName));\n\t}\n\n\t/**\n\t * 自定义需要读取或写出的Sheet，如果给定的sheet不存在，创建之（命名为默认）<br>\n\t * 在读取中，此方法用于切换读取的sheet，在写出时，此方法用于新建或者切换sheet\n\t *\n\t * @param sheetIndex sheet序号，从0开始计数\n\t * @return this\n\t * @since 4.0.10\n\t */\n\tpublic T setSheet(int sheetIndex) {\n\t\treturn setSheet(WorkbookUtil.getOrCreateSheet(this.workbook, sheetIndex));\n\t}\n\n\t/**\n\t * 设置自定义Sheet\n\t *\n\t * @param sheet 自定义sheet，可以通过{@link WorkbookUtil#getOrCreateSheet(Workbook, String)} 创建\n\t * @return this\n\t * @since 5.2.1\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic T setSheet(Sheet sheet) {\n\t\tthis.sheet = sheet;\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 复制当前sheet为新sheet\n\t *\n\t * @param sheetIndex        sheet位置\n\t * @param newSheetName      新sheet名\n\t * @param setAsCurrentSheet 是否切换为当前sheet\n\t * @return this\n\t * @since 5.7.10\n\t */\n\tpublic T cloneSheet(int sheetIndex, String newSheetName, boolean setAsCurrentSheet) {\n\t\tSheet sheet;\n\t\tif (this.workbook instanceof XSSFWorkbook) {\n\t\t\tXSSFWorkbook workbook = (XSSFWorkbook) this.workbook;\n\t\t\tsheet = workbook.cloneSheet(sheetIndex, newSheetName);\n\t\t} else {\n\t\t\tsheet = this.workbook.cloneSheet(sheetIndex);\n\t\t\t// issue#I8QIBB，clone后的sheet的index应该重新获取\n\t\t\tthis.workbook.setSheetName(workbook.getSheetIndex(sheet), newSheetName);\n\t\t}\n\t\tif (setAsCurrentSheet) {\n\t\t\tthis.sheet = sheet;\n\t\t}\n\t\t//noinspection unchecked\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 获取指定坐标单元格，单元格不存在时返回{@code null}\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return {@link Cell}\n\t * @since 5.1.4\n\t */\n\tpublic Cell getCell(String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getCell(cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 获取指定坐标单元格，单元格不存在时返回{@code null}\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link Cell}\n\t * @since 4.0.5\n\t */\n\tpublic Cell getCell(int x, int y) {\n\t\treturn getCell(x, y, false);\n\t}\n\n\t/**\n\t * 获取或创建指定坐标单元格\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return {@link Cell}\n\t * @since 5.1.4\n\t */\n\tpublic Cell getOrCreateCell(String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getOrCreateCell(cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 获取或创建指定坐标单元格\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link Cell}\n\t * @since 4.0.6\n\t */\n\tpublic Cell getOrCreateCell(int x, int y) {\n\t\treturn getCell(x, y, true);\n\t}\n\n\t/**\n\t * 获取指定坐标单元格，如果isCreateIfNotExist为false，则在单元格不存在时返回{@code null}\n\t *\n\t * @param locationRef        单元格地址标识符，例如A11，B5\n\t * @param isCreateIfNotExist 单元格不存在时是否创建\n\t * @return {@link Cell}\n\t * @since 5.1.4\n\t */\n\tpublic Cell getCell(String locationRef, boolean isCreateIfNotExist) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getCell(cellLocation.getX(), cellLocation.getY(), isCreateIfNotExist);\n\t}\n\n\t/**\n\t * 获取指定坐标单元格，如果isCreateIfNotExist为false，则在单元格不存在时返回{@code null}\n\t *\n\t * @param x                  X坐标，从0计数，即列号\n\t * @param y                  Y坐标，从0计数，即行号\n\t * @param isCreateIfNotExist 单元格不存在时是否创建\n\t * @return {@link Cell}\n\t * @since 4.0.6\n\t */\n\tpublic Cell getCell(int x, int y, boolean isCreateIfNotExist) {\n\t\tfinal Row row = isCreateIfNotExist ? RowUtil.getOrCreateRow(this.sheet, y) : this.sheet.getRow(y);\n\t\tif (null != row) {\n\t\t\treturn isCreateIfNotExist ? CellUtil.getOrCreateCell(row, x) : row.getCell(x);\n\t\t}\n\t\treturn null;\n\t}\n\n\n\t/**\n\t * 获取或者创建行\n\t *\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link Row}\n\t * @since 4.1.4\n\t */\n\tpublic Row getOrCreateRow(int y) {\n\t\treturn RowUtil.getOrCreateRow(this.sheet, y);\n\t}\n\n\t/**\n\t * 为指定单元格获取或者创建样式，返回样式后可以设置样式内容\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return {@link CellStyle}\n\t * @since 5.1.4\n\t */\n\tpublic CellStyle getOrCreateCellStyle(String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getOrCreateCellStyle(cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 为指定单元格获取或者创建样式，返回样式后可以设置样式内容\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link CellStyle}\n\t * @since 4.1.4\n\t */\n\tpublic CellStyle getOrCreateCellStyle(int x, int y) {\n\t\tfinal CellStyle cellStyle = getOrCreateCell(x, y).getCellStyle();\n\t\treturn StyleUtil.isNullOrDefaultStyle(this.workbook, cellStyle) ? createCellStyle(x, y) : cellStyle;\n\t}\n\n\t/**\n\t * 为指定单元格创建样式，返回样式后可以设置样式内容\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return {@link CellStyle}\n\t * @since 5.1.4\n\t */\n\tpublic CellStyle createCellStyle(String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn createCellStyle(cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 为指定单元格创建样式，返回样式后可以设置样式内容\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link CellStyle}\n\t * @since 4.6.3\n\t */\n\tpublic CellStyle createCellStyle(int x, int y) {\n\t\tfinal Cell cell = getOrCreateCell(x, y);\n\t\tfinal CellStyle cellStyle = this.workbook.createCellStyle();\n\t\tcell.setCellStyle(cellStyle);\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 创建单元格样式\n\t *\n\t * @return {@link CellStyle}\n\t * @see Workbook#createCellStyle()\n\t * @since 5.4.0\n\t */\n\tpublic CellStyle createCellStyle() {\n\t\treturn StyleUtil.createCellStyle(this.workbook);\n\t}\n\n\t/**\n\t * 获取或创建某一行的样式，返回样式后可以设置样式内容<br>\n\t * 需要注意，此方法返回行样式，设置背景色在单元格设置值后会被覆盖，需要单独设置其单元格的样式。\n\t *\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link CellStyle}\n\t * @since 4.1.4\n\t */\n\tpublic CellStyle getOrCreateRowStyle(int y) {\n\t\tCellStyle rowStyle = getOrCreateRow(y).getRowStyle();\n\t\treturn StyleUtil.isNullOrDefaultStyle(this.workbook, rowStyle) ? createRowStyle(y) : rowStyle;\n\t}\n\n\t/**\n\t * 创建某一行的样式，返回样式后可以设置样式内容\n\t *\n\t * @param y Y坐标，从0计数，即行号\n\t * @return {@link CellStyle}\n\t * @since 4.6.3\n\t */\n\tpublic CellStyle createRowStyle(int y) {\n\t\tfinal CellStyle rowStyle = this.workbook.createCellStyle();\n\t\tgetOrCreateRow(y).setRowStyle(rowStyle);\n\t\treturn rowStyle;\n\t}\n\n\t/**\n\t * 获取或创建某一列的样式，返回样式后可以设置样式内容<br>\n\t * 需要注意，此方法返回行样式，设置背景色在单元格设置值后会被覆盖，需要单独设置其单元格的样式。\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @return {@link CellStyle}\n\t * @since 4.1.4\n\t */\n\tpublic CellStyle getOrCreateColumnStyle(int x) {\n\t\tfinal CellStyle columnStyle = this.sheet.getColumnStyle(x);\n\t\treturn StyleUtil.isNullOrDefaultStyle(this.workbook, columnStyle) ? createColumnStyle(x) : columnStyle;\n\t}\n\n\t/**\n\t * 创建某一列的样式，返回样式后可以设置样式内容\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @return {@link CellStyle}\n\t * @since 4.6.3\n\t */\n\tpublic CellStyle createColumnStyle(int x) {\n\t\tfinal CellStyle columnStyle = this.workbook.createCellStyle();\n\t\tthis.sheet.setDefaultColumnStyle(x, columnStyle);\n\t\treturn columnStyle;\n\t}\n\n\t/**\n\t * 创建 {@link Hyperlink}，默认内容（标签为链接地址本身）\n\t * @param type 链接类型\n\t * @param address 链接地址\n\t * @return 链接\n\t * @since 5.7.13\n\t */\n\tpublic Hyperlink createHyperlink(HyperlinkType type, String address){\n\t\treturn createHyperlink(type, address, address);\n\t}\n\n\t/**\n\t * 创建 {@link Hyperlink}，默认内容\n\t * @param type 链接类型\n\t * @param address 链接地址\n\t * @param label 标签，即单元格中显示的内容\n\t * @return 链接\n\t * @since 5.7.13\n\t */\n\tpublic Hyperlink createHyperlink(HyperlinkType type, String address, String label){\n\t\tfinal Hyperlink hyperlink = this.workbook.getCreationHelper().createHyperlink(type);\n\t\thyperlink.setAddress(address);\n\t\thyperlink.setLabel(label);\n\t\treturn hyperlink;\n\t}\n\n\t/**\n\t * 获取总行数，计算方法为：\n\t *\n\t * <pre>\n\t * 最后一行序号 + 1\n\t * </pre>\n\t *\n\t * @return 行数\n\t * @since 4.5.4\n\t */\n\tpublic int getRowCount() {\n\t\treturn this.sheet.getLastRowNum() + 1;\n\t}\n\n\t/**\n\t * 获取有记录的行数，计算方法为：\n\t *\n\t * <pre>\n\t * 最后一行序号 - 第一行序号 + 1\n\t * </pre>\n\t *\n\t * @return 行数\n\t * @since 4.5.4\n\t */\n\tpublic int getPhysicalRowCount() {\n\t\treturn this.sheet.getPhysicalNumberOfRows();\n\t}\n\n\t/**\n\t * 获取第一行总列数，计算方法为：\n\t *\n\t * <pre>\n\t * 最后一列序号 + 1\n\t * </pre>\n\t *\n\t * @return 列数\n\t */\n\tpublic int getColumnCount() {\n\t\treturn getColumnCount(0);\n\t}\n\n\t/**\n\t * 获取总列数，计算方法为：\n\t *\n\t * <pre>\n\t * 最后一列序号 + 1\n\t * </pre>\n\t *\n\t * @param rowNum 行号\n\t * @return 列数，-1表示获取失败\n\t */\n\tpublic int getColumnCount(int rowNum) {\n\t\tfinal Row row = this.sheet.getRow(rowNum);\n\t\tif (null != row) {\n\t\t\t// getLastCellNum方法返回序号+1的值\n\t\t\treturn row.getLastCellNum();\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 判断是否为xlsx格式的Excel表（Excel07格式）\n\t *\n\t * @return 是否为xlsx格式的Excel表（Excel07格式）\n\t * @since 4.6.2\n\t */\n\tpublic boolean isXlsx() {\n\t\treturn this.sheet instanceof XSSFSheet || this.sheet instanceof SXSSFSheet;\n\t}\n\n\t/**\n\t * 关闭工作簿<br>\n\t * 如果用户设定了目标文件，先写出目标文件后给关闭工作簿\n\t */\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.workbook);\n\t\tthis.sheet = null;\n\t\tthis.workbook = null;\n\t\tthis.isClosed = true;\n\t}\n\n\t/**\n\t * 获得标题行的别名Map\n\t *\n\t * @return 别名Map\n\t */\n\tpublic Map<String, String> getHeaderAlias() {\n\t\treturn headerAlias;\n\t}\n\n\t/**\n\t * 设置标题行的别名Map\n\t *\n\t * @param headerAlias 别名Map\n\t * @return this\n\t */\n\tpublic T setHeaderAlias(Map<String, String> headerAlias) {\n\t\tthis.headerAlias = headerAlias;\n\t\t//noinspection unchecked\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 增加标题别名\n\t *\n\t * @param header 标题\n\t * @param alias  别名\n\t * @return this\n\t */\n\tpublic T addHeaderAlias(String header, String alias) {\n\t\tMap<String, String> headerAlias = this.headerAlias;\n\t\tif (null == headerAlias) {\n\t\t\theaderAlias = new LinkedHashMap<>();\n\t\t}\n\t\tthis.headerAlias = headerAlias;\n\t\tthis.headerAlias.put(header, alias);\n\t\t//noinspection unchecked\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 去除标题别名\n\t *\n\t * @param header 标题\n\t * @return this\n\t */\n\tpublic T removeHeaderAlias(String header) {\n\t\tthis.headerAlias.remove(header);\n\t\t//noinspection unchecked\n\t\treturn (T) this;\n\t}\n\n\t/**\n\t * 清空标题别名，key为Map中的key，value为别名\n\t *\n\t * @return this\n\t */\n\tpublic T clearHeaderAlias() {\n\t\tthis.headerAlias = null;\n\t\t//noinspection unchecked\n\t\treturn (T) this;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelDateUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.poi.ss.formula.ConditionalFormattingEvaluator;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.ExcelNumberFormat;\n\n/**\n * Excel中日期判断、读取、处理等补充工具类\n *\n * @author looly\n * @since 5.5.5\n */\npublic class ExcelDateUtil {\n\n\t/**\n\t * 某些特殊的自定义日期格式\n\t */\n\tprivate static final int[] customFormats = new int[]{28, 30, 31, 32, 33, 55, 56, 57, 58};\n\n\tpublic static boolean isDateFormat(Cell cell){\n\t\treturn isDateFormat(cell, null);\n\t}\n\n\t/**\n\t * 判断是否日期格式\n\t * @param cell 单元格\n\t * @param cfEvaluator {@link ConditionalFormattingEvaluator}\n\t * @return 是否日期格式\n\t */\n\tpublic static boolean isDateFormat(Cell cell, ConditionalFormattingEvaluator cfEvaluator){\n\t\tfinal ExcelNumberFormat nf = ExcelNumberFormat.from(cell, cfEvaluator);\n\t\treturn isDateFormat(nf);\n\t}\n\n\t/**\n\t * 判断是否日期格式\n\t * @param numFmt {@link ExcelNumberFormat}\n\t * @return 是否日期格式\n\t */\n\tpublic static boolean isDateFormat(ExcelNumberFormat numFmt) {\n\t\treturn isDateFormat(numFmt.getIdx(), numFmt.getFormat());\n\t}\n\n\t/**\n\t * 判断日期格式\n\t *\n\t * @param formatIndex  格式索引，一般用于内建格式\n\t * @param formatString 格式字符串\n\t * @return 是否为日期格式\n\t * @since 5.5.3\n\t */\n\tpublic static boolean isDateFormat(int formatIndex, String formatString) {\n\t\t// issue#1283@Github\n\t\tif (ArrayUtil.contains(customFormats, formatIndex)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// 自定义格式判断\n\t\tif (StrUtil.isNotEmpty(formatString) &&\n\t\t\t\tStrUtil.containsAny(formatString, \"周\", \"星期\", \"aa\")) {\n\t\t\t// aa  -> 周一\n\t\t\t// aaa -> 星期一\n\t\t\treturn true;\n\t\t}\n\n\t\treturn org.apache.poi.ss.usermodel.DateUtil.isADateFormat(formatIndex, formatString);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelExtractorUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport org.apache.poi.hssf.usermodel.HSSFWorkbook;\nimport org.apache.poi.ss.extractor.ExcelExtractor;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.apache.poi.xssf.extractor.XSSFExcelExtractor;\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook;\n\n/**\n * {@link ExcelExtractor}工具封装\n *\n * @author looly\n * @since 5.4.4\n */\npublic class ExcelExtractorUtil {\n\t/**\n\t * 获取 {@link ExcelExtractor} 对象\n\t *\n\t * @param wb {@link Workbook}\n\t * @return {@link ExcelExtractor}\n\t */\n\tpublic static ExcelExtractor getExtractor(Workbook wb) {\n\t\tExcelExtractor extractor;\n\t\tif (wb instanceof HSSFWorkbook) {\n\t\t\textractor = new org.apache.poi.hssf.extractor.ExcelExtractor((HSSFWorkbook) wb);\n\t\t} else {\n\t\t\textractor = new XSSFExcelExtractor((XSSFWorkbook) wb);\n\t\t}\n\t\treturn extractor;\n\t}\n\n\t/**\n\t * 读取为文本格式<br>\n\t * 使用{@link ExcelExtractor} 提取Excel内容\n\t *\n\t * @param wb            {@link Workbook}\n\t * @param withSheetName 是否附带sheet名\n\t * @return Excel文本\n\t * @since 4.1.0\n\t */\n\tpublic static String readAsText(Workbook wb, boolean withSheetName) {\n\t\tfinal ExcelExtractor extractor = getExtractor(wb);\n\t\textractor.setIncludeSheetNames(withSheetName);\n\t\treturn extractor.getText();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelFileUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport org.apache.poi.poifs.filesystem.FileMagic;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Excel文件工具类\n *\n * @author looly\n * @since 4.2.1\n */\npublic class ExcelFileUtil {\n\t// ------------------------------------------------------------------------------------------------ isXls\n\n\t/**\n\t * 是否为XLS格式的Excel文件（HSSF）<br>\n\t * XLS文件主要用于Excel 97~2003创建<br>\n\t * 此方法会自动调用{@link InputStream#reset()}方法\n\t *\n\t * @param in excel输入流\n\t * @return 是否为XLS格式的Excel文件（HSSF）\n\t */\n\tpublic static boolean isXls(InputStream in) {\n\t\treturn FileMagic.OLE2 == getFileMagic(in);\n\t}\n\n\t/**\n\t * 是否为XLSX格式的Excel文件（XSSF）<br>\n\t * XLSX文件主要用于Excel 2007+创建<br>\n\t * 此方法会自动调用{@link InputStream#reset()}方法\n\t *\n\t * @param in excel输入流\n\t * @return 是否为XLSX格式的Excel文件（XSSF）\n\t */\n\tpublic static boolean isXlsx(InputStream in) {\n\t\treturn FileMagic.OOXML == getFileMagic(in);\n\t}\n\n\t/**\n\t * 是否为XLSX格式的Excel文件（XSSF）<br>\n\t * XLSX文件主要用于Excel 2007+创建\n\t *\n\t * @param file excel文件\n\t * @return 是否为XLSX格式的Excel文件（XSSF）\n\t * @since 5.4.4\n\t */\n\tpublic static boolean isXlsx(File file) {\n\t\ttry {\n\t\t\treturn FileMagic.valueOf(file) == FileMagic.OOXML;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * {@link java.io.PushbackInputStream}\n\t * PushbackInputStream的markSupported()为false，并不支持mark和reset\n\t * 如果强转成PushbackInputStream在调用FileMagic.valueOf(inputStream)时会报错\n\t * {@link FileMagic}\n\t * 报错内容：getFileMagic() only operates on streams which support mark(int)\n\t * 此处修改成 final InputStream in = FileMagic.prepareToCheckMagic(in)\n\t *\n\t * @param in {@link InputStream}\n\t * @author kefan.qu\n\t */\n\tprivate static FileMagic getFileMagic(InputStream in) {\n\t\tFileMagic magic;\n\t\tin = FileMagic.prepareToCheckMagic(in);\n\t\ttry {\n\t\t\tmagic = FileMagic.valueOf(in);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\treturn magic;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelPicUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.poi.hssf.usermodel.HSSFClientAnchor;\nimport org.apache.poi.hssf.usermodel.HSSFPicture;\nimport org.apache.poi.hssf.usermodel.HSSFPictureData;\nimport org.apache.poi.hssf.usermodel.HSSFShape;\nimport org.apache.poi.hssf.usermodel.HSSFSheet;\nimport org.apache.poi.hssf.usermodel.HSSFWorkbook;\nimport org.apache.poi.ooxml.POIXMLDocumentPart;\nimport org.apache.poi.ss.usermodel.PictureData;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.apache.poi.xssf.usermodel.XSSFDrawing;\nimport org.apache.poi.xssf.usermodel.XSSFPicture;\nimport org.apache.poi.xssf.usermodel.XSSFShape;\nimport org.apache.poi.xssf.usermodel.XSSFSheet;\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook;\nimport org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Excel图片工具类\n *\n * @author looly\n * @since 4.0.7\n */\npublic class ExcelPicUtil {\n\t/**\n\t * 获取工作簿指定sheet中图片列表\n\t *\n\t * @param workbook 工作簿{@link Workbook}\n\t * @param sheetIndex sheet的索引\n\t * @return 图片映射，键格式：行_列，值：{@link PictureData}\n\t */\n\tpublic static Map<String, PictureData> getPicMap(Workbook workbook, int sheetIndex) {\n\t\tAssert.notNull(workbook, \"Workbook must be not null !\");\n\t\tif (sheetIndex < 0) {\n\t\t\tsheetIndex = 0;\n\t\t}\n\n\t\tif (workbook instanceof HSSFWorkbook) {\n\t\t\treturn getPicMapXls((HSSFWorkbook) workbook, sheetIndex);\n\t\t} else if (workbook instanceof XSSFWorkbook) {\n\t\t\treturn getPicMapXlsx((XSSFWorkbook) workbook, sheetIndex);\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(StrUtil.format(\"Workbook type [{}] is not supported!\", workbook.getClass()));\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 获取XLS工作簿指定sheet中图片列表\n\t *\n\t * @param workbook 工作簿{@link Workbook}\n\t * @param sheetIndex sheet的索引\n\t * @return 图片映射，键格式：行_列，值：{@link PictureData}\n\t */\n\tprivate static Map<String, PictureData> getPicMapXls(HSSFWorkbook workbook, int sheetIndex) {\n\t\tfinal Map<String, PictureData> picMap = new HashMap<>();\n\t\tfinal List<HSSFPictureData> pictures = workbook.getAllPictures();\n\t\tif (CollectionUtil.isNotEmpty(pictures)) {\n\t\t\tfinal HSSFSheet sheet = workbook.getSheetAt(sheetIndex);\n\t\t\tHSSFClientAnchor anchor;\n\t\t\tint pictureIndex;\n\t\t\tfor (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) {\n\t\t\t\tif (shape instanceof HSSFPicture) {\n\t\t\t\t\tpictureIndex = ((HSSFPicture) shape).getPictureIndex() - 1;\n\t\t\t\t\tanchor = (HSSFClientAnchor) shape.getAnchor();\n\t\t\t\t\tpicMap.put(StrUtil.format(\"{}_{}\", anchor.getRow1(), anchor.getCol1()), pictures.get(pictureIndex));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn picMap;\n\t}\n\n\t/**\n\t * 获取XLSX工作簿指定sheet中图片列表\n\t *\n\t * @param workbook 工作簿{@link Workbook}\n\t * @param sheetIndex sheet的索引\n\t * @return 图片映射，键格式：行_列，值：{@link PictureData}\n\t */\n\tprivate static Map<String, PictureData> getPicMapXlsx(XSSFWorkbook workbook, int sheetIndex) {\n\t\tfinal Map<String, PictureData> sheetIndexPicMap = new LinkedHashMap<>();\n\t\tfinal XSSFSheet sheet = workbook.getSheetAt(sheetIndex);\n\t\tXSSFDrawing drawing;\n\t\tfor (POIXMLDocumentPart dr : sheet.getRelations()) {\n\t\t\tif (dr instanceof XSSFDrawing) {\n\t\t\t\tdrawing = (XSSFDrawing) dr;\n\t\t\t\tfinal List<XSSFShape> shapes = drawing.getShapes();\n\t\t\t\tXSSFPicture pic;\n\t\t\t\tCTMarker ctMarker;\n\t\t\t\tfor (XSSFShape shape : shapes) {\n\t\t\t\t\tif(shape instanceof XSSFPicture){\n\t\t\t\t\t\tpic = (XSSFPicture) shape;\n\t\t\t\t\t\ttry{\n\t\t\t\t\t\t\tctMarker = pic.getPreferredSize().getFrom();\n\t\t\t\t\t\t}catch (Exception e){\n\t\t\t\t\t\t\t// 此处可能有空指针异常，跳过之\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsheetIndexPicMap.put(StrUtil.format(\"{}_{}\", ctMarker.getRow(), ctMarker.getCol()), pic.getPictureData());\n\t\t\t\t\t}\n\t\t\t\t\t// 其他类似于图表等忽略，see: https://gitee.com/chinabugotech/hutool/issues/I38857\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn sheetIndexPicMap;\n\t}\n\t// -------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.poi.excel.cell.CellEditor;\nimport cn.hutool.poi.excel.cell.CellHandler;\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport cn.hutool.poi.excel.reader.BeanSheetReader;\nimport cn.hutool.poi.excel.reader.ColumnSheetReader;\nimport cn.hutool.poi.excel.reader.ListSheetReader;\nimport cn.hutool.poi.excel.reader.MapSheetReader;\nimport cn.hutool.poi.excel.reader.SheetReader;\nimport org.apache.poi.ss.extractor.ExcelExtractor;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.usermodel.Workbook;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Excel读取器<br>\n * 读取Excel工作簿\n *\n * @author Looly\n * @since 3.1.0\n */\npublic class ExcelReader extends ExcelBase<ExcelReader> {\n\n\t/**\n\t * 是否忽略空行\n\t */\n\tprivate boolean ignoreEmptyRow = true;\n\t/**\n\t * 单元格值处理接口\n\t */\n\tprivate CellEditor cellEditor;\n\t// ------------------------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param sheetIndex    sheet序号，0表示第一个sheet\n\t */\n\tpublic ExcelReader(String excelFilePath, int sheetIndex) {\n\t\tthis(FileUtil.file(excelFilePath), sheetIndex);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param sheetName     sheet名，第一个默认是sheet1\n\t * @since 5.8.0\n\t */\n\tpublic ExcelReader(String excelFilePath, String sheetName) {\n\t\tthis(FileUtil.file(excelFilePath), sheetName);\n\t}\n\n\t/**\n\t * 构造（读写方式读取）\n\t *\n\t * @param bookFile   Excel文件\n\t * @param sheetIndex sheet序号，0表示第一个sheet\n\t */\n\tpublic ExcelReader(File bookFile, int sheetIndex) {\n\t\tthis(WorkbookUtil.createBook(bookFile, true), sheetIndex);\n\t\tthis.destFile = bookFile;\n\t}\n\n\t/**\n\t * 构造（读写方式读取）\n\t *\n\t * @param bookFile  Excel文件\n\t * @param sheetName sheet名，第一个默认是sheet1\n\t */\n\tpublic ExcelReader(File bookFile, String sheetName) {\n\t\tthis(WorkbookUtil.createBook(bookFile, true), sheetName);\n\t\tthis.destFile = bookFile;\n\t}\n\n\t/**\n\t * 构造（只读方式读取）\n\t *\n\t * @param bookStream Excel文件的流\n\t * @param sheetIndex sheet序号，0表示第一个sheet\n\t */\n\tpublic ExcelReader(InputStream bookStream, int sheetIndex) {\n\t\tthis(WorkbookUtil.createBook(bookStream), sheetIndex);\n\t}\n\n\t/**\n\t * 构造（只读方式读取）\n\t *\n\t * @param bookStream Excel文件的流\n\t * @param sheetName  sheet名，第一个默认是sheet1\n\t */\n\tpublic ExcelReader(InputStream bookStream, String sheetName) {\n\t\tthis(WorkbookUtil.createBook(bookStream), sheetName);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param book       {@link Workbook} 表示一个Excel文件\n\t * @param sheetIndex sheet序号，0表示第一个sheet\n\t */\n\tpublic ExcelReader(Workbook book, int sheetIndex) {\n\t\tthis(getSheetOrCloseWorkbook(book, sheetIndex));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param book      {@link Workbook} 表示一个Excel文件\n\t * @param sheetName sheet名，第一个默认是sheet1\n\t */\n\tpublic ExcelReader(Workbook book, String sheetName) {\n\t\tthis(getSheetOrCloseWorkbook(book, sheetName));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param sheet Excel中的sheet\n\t */\n\tpublic ExcelReader(Sheet sheet) {\n\t\tsuper(sheet);\n\t}\n\t// ------------------------------------------------------------------------------------------------------- Constructor end\n\n\t// ------------------------------------------------------------------------------------------------------- Getters and Setters start\n\n\t/**\n\t * 是否忽略空行\n\t *\n\t * @return 是否忽略空行\n\t */\n\tpublic boolean isIgnoreEmptyRow() {\n\t\treturn ignoreEmptyRow;\n\t}\n\n\t/**\n\t * 设置是否忽略空行\n\t *\n\t * @param ignoreEmptyRow 是否忽略空行\n\t * @return this\n\t */\n\tpublic ExcelReader setIgnoreEmptyRow(boolean ignoreEmptyRow) {\n\t\tthis.ignoreEmptyRow = ignoreEmptyRow;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置单元格值处理逻辑<br>\n\t * 当Excel中的值并不能满足我们的读取要求时，通过传入一个编辑接口，可以对单元格值自定义，例如对数字和日期类型值转换为字符串等\n\t *\n\t * @param cellEditor 单元格值处理接口\n\t * @return this\n\t */\n\tpublic ExcelReader setCellEditor(CellEditor cellEditor) {\n\t\tthis.cellEditor = cellEditor;\n\t\treturn this;\n\t}\n\t// ------------------------------------------------------------------------------------------------------- Getters and Setters end\n\n\t/**\n\t * 读取工作簿中指定的Sheet的所有行列数据\n\t *\n\t * @return 行的集合，一行使用List表示\n\t */\n\tpublic List<List<Object>> read() {\n\t\treturn read(0);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet\n\t *\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @return 行的集合，一行使用List表示\n\t * @since 4.0.0\n\t */\n\tpublic List<List<Object>> read(int startRowIndex) {\n\t\treturn read(startRowIndex, Integer.MAX_VALUE);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet，此方法会把第一行作为标题行，替换标题别名\n\t *\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t * @return 行的集合，一行使用List表示\n\t */\n\tpublic List<List<Object>> read(int startRowIndex, int endRowIndex) {\n\t\treturn read(startRowIndex, endRowIndex, true);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet\n\t *\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param endRowIndex    结束行（包含，从0开始计数）\n\t * @param aliasFirstLine 是否首行作为标题行转换别名\n\t * @return 行的集合，一行使用List表示\n\t * @since 5.4.4\n\t */\n\tpublic List<List<Object>> read(int startRowIndex, int endRowIndex, boolean aliasFirstLine) {\n\t\tfinal ListSheetReader reader = new ListSheetReader(startRowIndex, endRowIndex, aliasFirstLine);\n\t\treader.setCellEditor(this.cellEditor);\n\t\treader.setIgnoreEmptyRow(this.ignoreEmptyRow);\n\t\treader.setHeaderAlias(headerAlias);\n\t\treturn read(reader);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet中指定列\n\t *\n\t * @param columnIndex   列号，从0开始计数\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @return 列的集合\n\t * @since 5.7.17\n\t */\n\tpublic List<Object> readColumn(int columnIndex, int startRowIndex) {\n\t\treturn readColumn(columnIndex, startRowIndex, Integer.MAX_VALUE);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet中指定列\n\t *\n\t * @param columnIndex   列号，从0开始计数\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t * @return 列的集合\n\t * @since 5.7.17\n\t */\n\tpublic List<Object> readColumn(int columnIndex, int startRowIndex, int endRowIndex) {\n\t\tfinal ColumnSheetReader reader = new ColumnSheetReader(columnIndex, startRowIndex, endRowIndex);\n\t\treader.setCellEditor(this.cellEditor);\n\t\treader.setIgnoreEmptyRow(this.ignoreEmptyRow);\n\t\treader.setHeaderAlias(headerAlias);\n\t\treturn read(reader);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet，此方法为类流处理方式，当读到指定单元格时，会调用CellEditor接口<br>\n\t * 用户通过实现此接口，可以更加灵活地处理每个单元格的数据。\n\t *\n\t * @param cellHandler 单元格处理器，用于处理读到的单元格及其数据\n\t * @since 5.3.8\n\t */\n\tpublic void read(CellHandler cellHandler) {\n\t\tread(0, Integer.MAX_VALUE, cellHandler);\n\t}\n\n\t/**\n\t * 读取工作簿中指定的Sheet，此方法为类流处理方式，当读到指定单元格时，会调用CellEditor接口<br>\n\t * 用户通过实现此接口，可以更加灵活地处理每个单元格的数据。\n\t *\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t * @param cellHandler   单元格处理器，用于处理读到的单元格及其数据\n\t * @since 5.3.8\n\t */\n\tpublic void read(int startRowIndex, int endRowIndex, CellHandler cellHandler) {\n\t\tcheckNotClosed();\n\n\t\tstartRowIndex = Math.max(startRowIndex, this.sheet.getFirstRowNum());// 读取起始行（包含）\n\t\tendRowIndex = Math.min(endRowIndex, this.sheet.getLastRowNum());// 读取结束行（包含）\n\n\t\tRow row;\n\t\tshort columnSize;\n\t\tfor (int y = startRowIndex; y <= endRowIndex; y++) {\n\t\t\trow = this.sheet.getRow(y);\n\t\t\tif (null != row) {\n\t\t\t\tcolumnSize = row.getLastCellNum();\n\t\t\t\tCell cell;\n\t\t\t\tfor (short x = 0; x < columnSize; x++) {\n\t\t\t\t\tcell = row.getCell(x);\n\t\t\t\t\tcellHandler.handle(cell, CellUtil.getCellValue(cell));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 读取Excel为Map的列表，读取所有行，默认第一行做为标题，数据从第二行开始<br>\n\t * Map表示一行，标题为key，单元格内容为value\n\t *\n\t * @return Map的列表\n\t */\n\tpublic List<Map<String, Object>> readAll() {\n\t\treturn read(0, 1, Integer.MAX_VALUE);\n\t}\n\n\t/**\n\t * 读取Excel为Map的列表<br>\n\t * Map表示一行，标题为key，单元格内容为value\n\t *\n\t * @param headerRowIndex 标题所在行，如果标题行在读取的内容行中间，这行做为数据将忽略\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param endRowIndex    读取结束行（包含，从0开始计数）\n\t * @return Map的列表\n\t */\n\tpublic List<Map<String, Object>> read(int headerRowIndex, int startRowIndex, int endRowIndex) {\n\t\tfinal MapSheetReader reader = new MapSheetReader(headerRowIndex, startRowIndex, endRowIndex);\n\t\treader.setCellEditor(this.cellEditor);\n\t\treader.setIgnoreEmptyRow(this.ignoreEmptyRow);\n\t\treader.setHeaderAlias(headerAlias);\n\t\treturn read(reader);\n\t}\n\n\t/**\n\t * 读取Excel为Bean的列表，读取所有行，默认第一行做为标题，数据从第二行开始\n\t *\n\t * @param <T>      Bean类型\n\t * @param beanType 每行对应Bean的类型\n\t * @return Map的列表\n\t */\n\tpublic <T> List<T> readAll(Class<T> beanType) {\n\t\treturn read(0, 1, Integer.MAX_VALUE, beanType);\n\t}\n\n\t/**\n\t * 读取Excel为Bean的列表\n\t *\n\t * @param <T>            Bean类型\n\t * @param headerRowIndex 标题所在行，如果标题行在读取的内容行中间，这行做为数据将忽略，从0开始计数\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param beanType       每行对应Bean的类型\n\t * @return Map的列表\n\t * @since 4.0.1\n\t */\n\tpublic <T> List<T> read(int headerRowIndex, int startRowIndex, Class<T> beanType) {\n\t\treturn read(headerRowIndex, startRowIndex, Integer.MAX_VALUE, beanType);\n\t}\n\n\t/**\n\t * 读取Excel为Bean的列表\n\t *\n\t * @param <T>            Bean类型\n\t * @param headerRowIndex 标题所在行，如果标题行在读取的内容行中间，这行做为数据将忽略，从0开始计数\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param endRowIndex    读取结束行（包含，从0开始计数）\n\t * @param beanType       每行对应Bean的类型\n\t * @return Map的列表\n\t */\n\tpublic <T> List<T> read(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> beanType) {\n\t\tfinal BeanSheetReader<T> reader = new BeanSheetReader<>(headerRowIndex, startRowIndex, endRowIndex, beanType);\n\t\treader.setCellEditor(this.cellEditor);\n\t\treader.setIgnoreEmptyRow(this.ignoreEmptyRow);\n\t\treader.setHeaderAlias(headerAlias);\n\t\treturn read(reader);\n\t}\n\n\t/**\n\t * 读取数据为指定类型\n\t *\n\t * @param <T>         读取数据类型\n\t * @param sheetReader {@link SheetReader}实现\n\t * @return 数据读取结果\n\t * @since 5.4.4\n\t */\n\tpublic <T> T read(SheetReader<T> sheetReader) {\n\t\tcheckNotClosed();\n\t\treturn Assert.notNull(sheetReader).read(this.sheet);\n\t}\n\n\t/**\n\t * 读取为文本格式<br>\n\t * 使用{@link ExcelExtractor} 提取Excel内容\n\t *\n\t * @param withSheetName 是否附带sheet名\n\t * @return Excel文本\n\t * @since 4.1.0\n\t */\n\tpublic String readAsText(boolean withSheetName) {\n\t\treturn ExcelExtractorUtil.readAsText(this.workbook, withSheetName);\n\t}\n\n\t/**\n\t * 获取 {@link ExcelExtractor} 对象\n\t *\n\t * @return {@link ExcelExtractor}\n\t * @since 4.1.0\n\t */\n\tpublic ExcelExtractor getExtractor() {\n\t\treturn ExcelExtractorUtil.getExtractor(this.workbook);\n\t}\n\n\t/**\n\t * 读取某一行数据\n\t *\n\t * @param rowIndex 行号，从0开始\n\t * @return 一行数据\n\t * @since 4.0.3\n\t */\n\tpublic List<Object> readRow(int rowIndex) {\n\t\treturn readRow(this.sheet.getRow(rowIndex));\n\t}\n\n\t/**\n\t * 读取某个单元格的值\n\t *\n\t * @param x X坐标，从0计数，即列号\n\t * @param y Y坐标，从0计数，即行号\n\t * @return 值，如果单元格无值返回null\n\t * @since 4.0.3\n\t */\n\tpublic Object readCellValue(int x, int y) {\n\t\treturn CellUtil.getCellValue(getCell(x, y), this.cellEditor);\n\t}\n\n\t/**\n\t * 获取Excel写出器<br>\n\t * 在读取Excel并做一定编辑后，获取写出器写出，规则如下：\n\t * <ul>\n\t *     <li>1. 当从流中读取时，转换为Writer直接使用Sheet对象，此时修改不会影响源文件，Writer中flush需要指定新的路径。</li>\n\t *     <li>2. 当从文件读取时，直接获取文件及sheet名称，此时可以修改原文件。</li>\n\t * </ul>\n\t *\n\t * @return {@link ExcelWriter}\n\t * @since 4.0.6\n\t */\n\tpublic ExcelWriter getWriter() {\n\t\tif(null == this.destFile){\n\t\t\t// 非读取文件形式，直接获取sheet操作。\n\t\t\treturn new ExcelWriter(this.sheet);\n\t\t}\n\t\treturn ExcelUtil.getWriter(this.destFile, this.sheet.getSheetName());\n\t}\n\n\t// ------------------------------------------------------------------------------------------------------- Private methods start\n\n\t/**\n\t * 读取一行\n\t *\n\t * @param row 行\n\t * @return 单元格值列表\n\t */\n\tprivate List<Object> readRow(Row row) {\n\t\treturn RowUtil.readRow(row, this.cellEditor);\n\t}\n\n\t/**\n\t * 检查是否未关闭状态\n\t */\n\tprivate void checkNotClosed() {\n\t\tAssert.isFalse(this.isClosed, \"ExcelReader has been closed!\");\n\t}\n\n\t/**\n\t * 获取Sheet，如果不存在则关闭{@link Workbook}并抛出异常，解决当sheet不存在时，文件依旧被占用问题<br>\n\t * 见：Issue#I8ZIQC\n\t * @param workbook {@link Workbook}，非空\n\t * @param name sheet名称，不存在抛出异常\n\t * @return {@link Sheet}\n\t * @throws IllegalArgumentException workbook为空或sheet不能存在\n\t */\n\tprivate static Sheet getSheetOrCloseWorkbook(final Workbook workbook, String name) throws IllegalArgumentException{\n\t\tAssert.notNull(workbook);\n\t\tif(null == name){\n\t\t\tname = \"sheet1\";\n\t\t}\n\t\tfinal Sheet sheet = workbook.getSheet(name);\n\t\tif(null == sheet){\n\t\t\tIoUtil.close(workbook);\n\t\t\tthrow new IllegalArgumentException(\"Sheet [\" + name + \"] not exist!\");\n\t\t}\n\t\treturn sheet;\n\t}\n\n\t/**\n\t * 获取Sheet，如果不存在则关闭{@link Workbook}并抛出异常，解决当sheet不存在时，文件依旧被占用问题<br>\n\t * 见：Issue#I8ZIQC\n\t * @param workbook {@link Workbook}，非空\n\t * @param sheetIndex sheet index\n\t * @return {@link Sheet}\n\t * @throws IllegalArgumentException workbook为空或sheet不能存在\n\t */\n\tprivate static Sheet getSheetOrCloseWorkbook(final Workbook workbook, final int sheetIndex) throws IllegalArgumentException{\n\t\tAssert.notNull(workbook);\n\t\tfinal Sheet sheet;\n\t\ttry {\n\t\t\tsheet = workbook.getSheetAt(sheetIndex);\n\t\t} catch (final IllegalArgumentException e){\n\t\t\tIoUtil.close(workbook);\n\t\t\tthrow e;\n\t\t}\n\t\tif(null == sheet){\n\t\t\tIoUtil.close(workbook);\n\t\t\tthrow new IllegalArgumentException(\"Sheet at [\" + sheetIndex + \"] not exist!\");\n\t\t}\n\t\treturn sheet;\n\t}\n\t// ------------------------------------------------------------------------------------------------------- Private methods end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.exceptions.DependencyException;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.PoiChecker;\nimport cn.hutool.poi.excel.cell.CellLocation;\nimport cn.hutool.poi.excel.sax.ExcelSaxReader;\nimport cn.hutool.poi.excel.sax.ExcelSaxUtil;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\n\nimport java.io.File;\nimport java.io.InputStream;\n\n/**\n * Excel工具类,不建议直接使用index直接操作sheet，在wps/excel中sheet显示顺序与index无关，还有隐藏sheet\n *\n * @author Looly\n */\npublic class ExcelUtil {\n\n\t/**\n\t * xls的ContentType\n\t */\n\tpublic static final String XLS_CONTENT_TYPE = \"application/vnd.ms-excel\";\n\n\t/**\n\t * xlsx的ContentType\n\t */\n\tpublic static final String XLSX_CONTENT_TYPE = \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";\n\n\t// ------------------------------------------------------------------------------------ Read by Sax start\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param path       Excel文件路径\n\t * @param rid        sheet rid，-1表示全部Sheet, 0表示第一个Sheet\n\t * @param rowHandler 行处理器\n\t * @since 3.2.0\n\t */\n\tpublic static void readBySax(String path, int rid, RowHandler rowHandler) {\n\t\treadBySax(FileUtil.file(path), rid, rowHandler);\n\t}\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param path       Excel文件路径\n\t * @param idOrRid    Excel中的sheet id或者rid编号，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @param rowHandler 行处理器\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(String path, String idOrRid, RowHandler rowHandler) {\n\t\treadBySax(FileUtil.file(path), idOrRid, rowHandler);\n\t}\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param file       Excel文件\n\t * @param rid        sheet rid，-1表示全部Sheet, 0表示第一个Sheet\n\t * @param rowHandler 行处理器\n\t * @since 3.2.0\n\t */\n\tpublic static void readBySax(File file, int rid, RowHandler rowHandler) {\n\t\tfinal ExcelSaxReader<?> reader = ExcelSaxUtil.createSaxReader(ExcelFileUtil.isXlsx(file), rowHandler);\n\t\treader.read(file, rid);\n\t}\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param file               Excel文件\n\t * @param idOrRidOrSheetName Excel中的sheet id或rid编号或sheet名称，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @param rowHandler         行处理器\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(File file, String idOrRidOrSheetName, RowHandler rowHandler) {\n\t\tfinal ExcelSaxReader<?> reader = ExcelSaxUtil.createSaxReader(ExcelFileUtil.isXlsx(file), rowHandler);\n\t\treader.read(file, idOrRidOrSheetName);\n\t}\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param in         Excel流\n\t * @param rid        sheet rid，-1表示全部Sheet, 0表示第一个Sheet\n\t * @param rowHandler 行处理器\n\t * @since 3.2.0\n\t */\n\tpublic static void readBySax(InputStream in, int rid, RowHandler rowHandler) {\n\t\tin = IoUtil.toMarkSupportStream(in);\n\t\tfinal ExcelSaxReader<?> reader = ExcelSaxUtil.createSaxReader(ExcelFileUtil.isXlsx(in), rowHandler);\n\t\treader.read(in, rid);\n\t}\n\n\t/**\n\t * 通过Sax方式读取Excel，同时支持03和07格式\n\t *\n\t * @param in                 Excel流\n\t * @param idOrRidOrSheetName Excel中的sheet id或rid编号或sheet名称，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @param rowHandler         行处理器\n\t * @since 5.4.4\n\t */\n\tpublic static void readBySax(InputStream in, String idOrRidOrSheetName, RowHandler rowHandler) {\n\t\tin = IoUtil.toMarkSupportStream(in);\n\t\tfinal ExcelSaxReader<?> reader = ExcelSaxUtil.createSaxReader(ExcelFileUtil.isXlsx(in), rowHandler);\n\t\treader.read(in, idOrRidOrSheetName);\n\t}\n\t// ------------------------------------------------------------------------------------ Read by Sax end\n\n\t// ------------------------------------------------------------------------------------------------ getReader\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容<br>\n\t * 默认调用第一个sheet\n\t *\n\t * @param bookFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @return {@link ExcelReader}\n\t * @since 3.1.1\n\t */\n\tpublic static ExcelReader getReader(String bookFilePath) {\n\t\treturn getReader(bookFilePath, 0);\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容<br>\n\t * 默认调用第一个sheet\n\t *\n\t * @param bookFile Excel文件\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(File bookFile) {\n\t\treturn getReader(bookFile, 0);\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容\n\t *\n\t * @param bookFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param sheetIndex   sheet序号，0表示第一个sheet\n\t * @return {@link ExcelReader}\n\t * @since 3.1.1\n\t */\n\tpublic static ExcelReader getReader(String bookFilePath, int sheetIndex) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookFilePath, sheetIndex);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容\n\t *\n\t * @param bookFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param sheetName    sheet名，第一个默认是sheet1\n\t * @return {@link ExcelReader}\n\t * @since 5.8.0\n\t */\n\tpublic static ExcelReader getReader(String bookFilePath, String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookFilePath, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容\n\t *\n\t * @param bookFile   Excel文件\n\t * @param sheetIndex sheet序号，0表示第一个sheet\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(File bookFile, int sheetIndex) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookFile, sheetIndex);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容\n\t *\n\t * @param bookFile  Excel文件\n\t * @param sheetName sheet名，第一个默认是sheet1\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(File bookFile, String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookFile, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容<br>\n\t * 默认调用第一个sheet，读取结束自动关闭流\n\t *\n\t * @param bookStream Excel文件的流\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(InputStream bookStream) {\n\t\treturn getReader(bookStream, 0);\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容<br>\n\t * 读取结束自动关闭流\n\t *\n\t * @param bookStream Excel文件的流\n\t * @param sheetIndex sheet序号，0表示第一个sheet\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(InputStream bookStream, int sheetIndex) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookStream, sheetIndex);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获取Excel读取器，通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容<br>\n\t * 读取结束自动关闭流\n\t *\n\t * @param bookStream Excel文件的流\n\t * @param sheetName  sheet名，第一个默认是sheet1\n\t * @return {@link ExcelReader}\n\t */\n\tpublic static ExcelReader getReader(InputStream bookStream, String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelReader(bookStream, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------------------------------ getWriter\n\n\t/**\n\t * 获得{@link ExcelWriter}，默认写出到第一个sheet<br>\n\t * 不传入写出的Excel文件路径，只能调用ExcelWriter#flush(OutputStream)方法写出到流<br>\n\t * 若写出到文件，还需调用{@link ExcelWriter#setDestFile(File)}方法自定义写出的文件，然后调用{@link ExcelWriter#flush()}方法写出到文件\n\t *\n\t * @return {@link ExcelWriter}\n\t * @since 3.2.1\n\t */\n\tpublic static ExcelWriter getWriter() {\n\t\ttry {\n\t\t\treturn new ExcelWriter();\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}，默认写出到第一个sheet<br>\n\t * 不传入写出的Excel文件路径，只能调用ExcelWriter#flush(OutputStream)方法写出到流<br>\n\t * 若写出到文件，还需调用{@link ExcelWriter#setDestFile(File)}方法自定义写出的文件，然后调用{@link ExcelWriter#flush()}方法写出到文件\n\t *\n\t * @param isXlsx 是否为xlsx格式\n\t * @return {@link ExcelWriter}\n\t * @since 3.2.1\n\t */\n\tpublic static ExcelWriter getWriter(boolean isXlsx) {\n\t\ttry {\n\t\t\treturn new ExcelWriter(isXlsx);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}，默认写出到第一个sheet\n\t *\n\t * @param destFilePath 目标文件路径\n\t * @return {@link ExcelWriter}\n\t */\n\tpublic static ExcelWriter getWriter(String destFilePath) {\n\t\ttry {\n\t\t\treturn new ExcelWriter(destFilePath);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}，默认写出到第一个sheet\n\t *\n\t * @param sheetName Sheet名\n\t * @return {@link ExcelWriter}\n\t * @since 4.5.18\n\t */\n\tpublic static ExcelWriter getWriterWithSheet(String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelWriter((File) null, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}，默认写出到第一个sheet，名字为sheet1\n\t *\n\t * @param destFile 目标文件\n\t * @return {@link ExcelWriter}\n\t */\n\tpublic static ExcelWriter getWriter(File destFile) {\n\t\ttry {\n\t\t\treturn new ExcelWriter(destFile);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}\n\t *\n\t * @param destFilePath 目标文件路径\n\t * @param sheetName    sheet表名\n\t * @return {@link ExcelWriter}\n\t */\n\tpublic static ExcelWriter getWriter(String destFilePath, String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelWriter(destFilePath, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link ExcelWriter}\n\t *\n\t * @param destFile  目标文件\n\t * @param sheetName sheet表名\n\t * @return {@link ExcelWriter}\n\t */\n\tpublic static ExcelWriter getWriter(File destFile, String sheetName) {\n\t\ttry {\n\t\t\treturn new ExcelWriter(destFile, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------------------------------ getBigWriter\n\n\t/**\n\t * 获得{@link BigExcelWriter}，默认写出到第一个sheet<br>\n\t * 不传入写出的Excel文件路径，只能调用ExcelWriter#flush(OutputStream)方法写出到流<br>\n\t * 若写出到文件，还需调用{@link BigExcelWriter#setDestFile(File)}方法自定义写出的文件，然后调用{@link BigExcelWriter#flush()}方法写出到文件\n\t *\n\t * @return {@link BigExcelWriter}\n\t * @since 4.1.13\n\t */\n\tpublic static BigExcelWriter getBigWriter() {\n\t\ttry {\n\t\t\treturn new BigExcelWriter();\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link BigExcelWriter}，默认写出到第一个sheet<br>\n\t * 不传入写出的Excel文件路径，只能调用ExcelWriter#flush(OutputStream)方法写出到流<br>\n\t * 若写出到文件，还需调用{@link BigExcelWriter#setDestFile(File)}方法自定义写出的文件，然后调用{@link BigExcelWriter#flush()}方法写出到文件\n\t *\n\t * @param rowAccessWindowSize 在内存中的行数\n\t * @return {@link BigExcelWriter}\n\t * @since 4.1.13\n\t */\n\tpublic static BigExcelWriter getBigWriter(int rowAccessWindowSize) {\n\t\ttry {\n\t\t\treturn new BigExcelWriter(rowAccessWindowSize);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link BigExcelWriter}，默认写出到第一个sheet\n\t *\n\t * @param destFilePath 目标文件路径\n\t * @return {@link BigExcelWriter}\n\t */\n\tpublic static BigExcelWriter getBigWriter(String destFilePath) {\n\t\ttry {\n\t\t\treturn new BigExcelWriter(destFilePath);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link BigExcelWriter}，默认写出到第一个sheet，名字为sheet1\n\t *\n\t * @param destFile 目标文件\n\t * @return {@link BigExcelWriter}\n\t */\n\tpublic static BigExcelWriter getBigWriter(File destFile) {\n\t\ttry {\n\t\t\treturn new BigExcelWriter(destFile);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link BigExcelWriter}\n\t *\n\t * @param destFilePath 目标文件路径\n\t * @param sheetName    sheet表名\n\t * @return {@link BigExcelWriter}\n\t */\n\tpublic static BigExcelWriter getBigWriter(String destFilePath, String sheetName) {\n\t\ttry {\n\t\t\treturn new BigExcelWriter(destFilePath, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 获得{@link BigExcelWriter}\n\t *\n\t * @param destFile  目标文件\n\t * @param sheetName sheet表名\n\t * @return {@link BigExcelWriter}\n\t */\n\tpublic static BigExcelWriter getBigWriter(File destFile, String sheetName) {\n\t\ttry {\n\t\t\treturn new BigExcelWriter(destFile, sheetName);\n\t\t} catch (NoClassDefFoundError e) {\n\t\t\tthrow new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);\n\t\t}\n\t}\n\n\t/**\n\t * 将Sheet列号变为列名\n\t *\n\t * @param index 列号, 从0开始\n\t * @return 0-》A; 1-》B...26-》AA\n\t * @since 4.1.20\n\t */\n\tpublic static String indexToColName(int index) {\n\t\tif (index < 0) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal StringBuilder colName = StrUtil.builder();\n\t\tdo {\n\t\t\tif (colName.length() > 0) {\n\t\t\t\tindex--;\n\t\t\t}\n\t\t\tint remainder = index % 26;\n\t\t\tcolName.append((char) (remainder + 'A'));\n\t\t\tindex = (index - remainder) / 26;\n\t\t} while (index > 0);\n\t\treturn colName.reverse().toString();\n\t}\n\n\t/**\n\t * 根据表元的列名转换为列号\n\t *\n\t * @param colName 列名, 从A开始\n\t * @return A1-》0; B1-》1...AA1-》26\n\t * @since 4.1.20\n\t */\n\tpublic static int colNameToIndex(String colName) {\n\t\tint length = colName.length();\n\t\tchar c;\n\t\tint index = -1;\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tc = Character.toUpperCase(colName.charAt(i));\n\t\t\tif (Character.isDigit(c)) {\n\t\t\t\tbreak;// 确定指定的char值是否为数字\n\t\t\t}\n\t\t\tindex = (index + 1) * 26 + (int) c - 'A';\n\t\t}\n\t\treturn index;\n\t}\n\n\t/**\n\t * 将Excel中地址标识符（例如A11，B5）等转换为行列表示<br>\n\t * 例如：A11 -》 x:0,y:10，B5-》x:1,y:4\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return 坐标点，x表示行，从0开始，y表示列，从0开始\n\t * @since 5.1.4\n\t */\n\tpublic static CellLocation toLocation(String locationRef) {\n\t\tfinal int x = colNameToIndex(locationRef);\n\t\tfinal int y = ReUtil.getFirstNumber(locationRef) - 1;\n\t\treturn new CellLocation(x, y);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelWriter.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.comparator.IndexedComparator;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.map.TableMap;\nimport cn.hutool.core.map.multi.RowKeyTable;\nimport cn.hutool.core.map.multi.Table;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\nimport cn.hutool.poi.excel.cell.CellLocation;\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport cn.hutool.poi.excel.style.Align;\nimport org.apache.poi.common.usermodel.Hyperlink;\nimport org.apache.poi.ss.usermodel.*;\nimport org.apache.poi.ss.util.CellRangeAddress;\nimport org.apache.poi.ss.util.CellRangeAddressList;\nimport org.apache.poi.xssf.usermodel.XSSFDataValidation;\nimport org.apache.poi.xssf.usermodel.XSSFSheet;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * Excel 写入器<br>\n * 此工具用于通过POI将数据写出到Excel，此对象可完成以下两个功能\n *\n * <pre>\n * 1. 编辑已存在的Excel，可写出原Excel文件，也可写出到其它地方（到文件或到流）\n * 2. 新建一个空的Excel工作簿，完成数据填充后写出（到文件或到流）\n * </pre>\n *\n * @author Looly\n * @since 3.2.0\n */\npublic class ExcelWriter extends ExcelBase<ExcelWriter> {\n\n\t/**\n\t * 当前行\n\t */\n\tprivate final AtomicInteger currentRow;\n\t/**\n\t * 是否只保留别名对应的字段\n\t */\n\tprivate boolean onlyAlias;\n\t/**\n\t * 标题顺序比较器\n\t */\n\tprivate Comparator<String> aliasComparator;\n\t/**\n\t * 样式集，定义不同类型数据样式\n\t */\n\tprivate StyleSet styleSet;\n\t/**\n\t * 标题项对应列号缓存，每次写标题更新此缓存\n\t */\n\tprivate Map<String, Integer> headLocationCache;\n\n\t// -------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 构造，默认生成xls格式的Excel文件<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t *\n\t * @since 3.2.1\n\t */\n\tpublic ExcelWriter() {\n\t\tthis(false);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(OutputStream)}方法写出到流<br>\n\t * 若写出到文件，需要调用{@link #flush(File)} 写出到文件\n\t *\n\t * @param isXlsx 是否为xlsx格式\n\t * @since 3.2.1\n\t */\n\tpublic ExcelWriter(boolean isXlsx) {\n\t\tthis(WorkbookUtil.createBook(isXlsx), null);\n\t}\n\n\t/**\n\t * 构造，默认写出到第一个sheet，第一个sheet名为sheet1\n\t *\n\t * @param destFilePath 目标文件路径，可以不存在\n\t */\n\tpublic ExcelWriter(String destFilePath) {\n\t\tthis(destFilePath, null);\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(OutputStream)}方法写出到流<br>\n\t * 若写出到文件，需要调用{@link #flush(File)} 写出到文件\n\t *\n\t * @param isXlsx    是否为xlsx格式\n\t * @param sheetName sheet名，第一个sheet名并写出到此sheet，例如sheet1\n\t * @since 4.1.8\n\t */\n\tpublic ExcelWriter(boolean isXlsx, String sheetName) {\n\t\tthis(WorkbookUtil.createBook(isXlsx), sheetName);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFilePath 目标文件路径，可以不存在\n\t * @param sheetName    sheet名，第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic ExcelWriter(String destFilePath, String sheetName) {\n\t\tthis(FileUtil.file(destFilePath), sheetName);\n\t}\n\n\t/**\n\t * 构造，默认写出到第一个sheet，第一个sheet名为sheet1\n\t *\n\t * @param destFile 目标文件，可以不存在\n\t */\n\tpublic ExcelWriter(File destFile) {\n\t\tthis(destFile, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile  目标文件，可以不存在\n\t * @param sheetName sheet名，做为第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic ExcelWriter(File destFile, String sheetName) {\n\t\tthis(WorkbookUtil.createBookForWriter(destFile), sheetName);\n\t\tthis.destFile = destFile;\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t *\n\t * @param workbook  {@link Workbook}\n\t * @param sheetName sheet名，做为第一个sheet名并写出到此sheet，例如sheet1\n\t */\n\tpublic ExcelWriter(Workbook workbook, String sheetName) {\n\t\tthis(WorkbookUtil.getOrCreateSheet(workbook, sheetName));\n\t}\n\n\t/**\n\t * 构造<br>\n\t * 此构造不传入写出的Excel文件路径，只能调用{@link #flush(OutputStream)}方法写出到流<br>\n\t * 若写出到文件，还需调用{@link #setDestFile(File)}方法自定义写出的文件，然后调用{@link #flush()}方法写出到文件\n\t *\n\t * @param sheet {@link Sheet}\n\t * @since 4.0.6\n\t */\n\tpublic ExcelWriter(Sheet sheet) {\n\t\tsuper(sheet);\n\t\tthis.styleSet = new StyleSet(workbook);\n\t\tthis.currentRow = new AtomicInteger(0);\n\t}\n\n\t// -------------------------------------------------------------------------- Constructor end\n\n\t@Override\n\tpublic ExcelWriter setSheet(int sheetIndex) {\n\t\t// 切换到新sheet需要重置开始行\n\t\treset();\n\t\treturn super.setSheet(sheetIndex);\n\t}\n\n\t@Override\n\tpublic ExcelWriter setSheet(String sheetName) {\n\t\t// 切换到新sheet需要重置开始行\n\t\treset();\n\t\treturn super.setSheet(sheetName);\n\t}\n\n\t/**\n\t * 重置Writer，包括：\n\t *\n\t * <pre>\n\t * 1. 当前行游标归零\n\t * 2. 清空别名比较器\n\t * 3. 清除标题缓存\n\t * </pre>\n\t *\n\t * @return this\n\t */\n\tpublic ExcelWriter reset() {\n\t\tresetRow();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重命名当前sheet\n\t *\n\t * @param sheetName 新的sheet名\n\t * @return this\n\t * @since 4.1.8\n\t */\n\tpublic ExcelWriter renameSheet(String sheetName) {\n\t\treturn renameSheet(this.workbook.getSheetIndex(this.sheet), sheetName);\n\t}\n\n\t/**\n\t * 重命名sheet\n\t *\n\t * @param sheet     sheet序号，0表示第一个sheet\n\t * @param sheetName 新的sheet名\n\t * @return this\n\t * @since 4.1.8\n\t */\n\tpublic ExcelWriter renameSheet(int sheet, String sheetName) {\n\t\tthis.workbook.setSheetName(sheet, sheetName);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置所有列为自动宽度，不考虑合并单元格<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。<br>\n\t * 列数计算是通过第一行计算的\n\t *\n\t * @return this\n\t * @since 4.0.12\n\t */\n\tpublic ExcelWriter autoSizeColumnAll() {\n\t\treturn autoSizeColumnAll(0f);\n\t}\n\n\t/**\n\t * 设置所有列为自动宽度，不考虑合并单元格<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。<br>\n\t * 列数计算是通过第一行计算的\n\t *\n\t * @param widthRatio 列宽的倍数。如果所有内容都是英文，可以设为1，如果有中文，建议设置为 1.6-2.0之间。\n\t * @return this\n\t * @since 5.8.30\n\t */\n\tpublic ExcelWriter autoSizeColumnAll(float widthRatio) {\n\t\tfinal int columnCount = this.getColumnCount();\n\t\tfor (int i = 0; i < columnCount; i++) {\n\t\t\tautoSizeColumn(i, widthRatio);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置某列为自动宽度，不考虑合并单元格<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。\n\t *\n\t * @param columnIndex 第几列，从0计数\n\t * @return this\n\t * @since 4.0.12\n\t */\n\tpublic ExcelWriter autoSizeColumn(int columnIndex) {\n\t\treturn autoSizeColumn(columnIndex, false);\n\t}\n\n\t/**\n\t * 设置某列为自动宽度<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。\n\t *\n\t * @param columnIndex    第几列，从0计数\n\t * @param useMergedCells 是否适用于合并单元格\n\t * @return this\n\t * @since 3.3.0\n\t */\n\tpublic ExcelWriter autoSizeColumn(int columnIndex, boolean useMergedCells) {\n\t\treturn autoSizeColumn(columnIndex, useMergedCells, 0f);\n\t}\n\n\t/**\n\t * 设置某列为自动宽度。注意有中文的情况下，需要根据需求调整宽度扩大比例。<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。\n\t *\n\t * @param columnIndex 第几列，从0计数\n\t * @param widthRatio  列宽的倍数。如果所有内容都是英文，可以设为1，如果有中文，建议设置为 1.6-2.0之间。\n\t * @return this\n\t * @since 5.8.30\n\t */\n\tpublic ExcelWriter autoSizeColumn(int columnIndex, float widthRatio) {\n\t\treturn autoSizeColumn(columnIndex, false, widthRatio);\n\t}\n\n\t/**\n\t * 设置某列为自动宽度。注意有中文的情况下，需要根据需求调整宽度扩大比例。<br>\n\t * 此方法必须在指定列数据完全写出后调用才有效。\n\t *\n\t * @param columnIndex    第几列，从0计数\n\t * @param useMergedCells 是否适用于合并单元格\n\t * @param widthRatio     列宽的倍数。如果所有内容都是英文，可以设为1，如果有中文，建议设置为 1.6-2.0之间。\n\t * @return this\n\t * @since 5.8.30\n\t */\n\tpublic ExcelWriter autoSizeColumn(int columnIndex, boolean useMergedCells, float widthRatio) {\n\t\tif (widthRatio > 0) {\n\t\t\tsheet.setColumnWidth(columnIndex, (int) (sheet.getColumnWidth(columnIndex) * widthRatio));\n\t\t} else {\n\t\t\tsheet.autoSizeColumn(columnIndex, useMergedCells);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 禁用默认样式\n\t *\n\t * @return this\n\t * @see #setStyleSet(StyleSet)\n\t * @since 4.6.3\n\t */\n\tpublic ExcelWriter disableDefaultStyle() {\n\t\treturn setStyleSet(null);\n\t}\n\n\t/**\n\t * 设置样式集，如果不使用样式，传入{@code null}\n\t *\n\t * @param styleSet 样式集，{@code null}表示无样式\n\t * @return this\n\t * @since 4.1.11\n\t */\n\tpublic ExcelWriter setStyleSet(StyleSet styleSet) {\n\t\tthis.styleSet = styleSet;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取样式集，样式集可以自定义包括：<br>\n\t *\n\t * <pre>\n\t * 1. 头部样式\n\t * 2. 一般单元格样式\n\t * 3. 默认数字样式\n\t * 4. 默认日期样式\n\t * </pre>\n\t *\n\t * @return 样式集\n\t * @since 4.0.0\n\t */\n\tpublic StyleSet getStyleSet() {\n\t\treturn this.styleSet;\n\t}\n\n\t/**\n\t * 获取头部样式，获取样式后可自定义样式\n\t *\n\t * @return 头部样式\n\t */\n\tpublic CellStyle getHeadCellStyle() {\n\t\treturn this.styleSet.headCellStyle;\n\t}\n\n\t/**\n\t * 获取单元格样式，获取样式后可自定义样式\n\t *\n\t * @return 单元格样式\n\t */\n\tpublic CellStyle getCellStyle() {\n\t\tif (null == this.styleSet) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.styleSet.cellStyle;\n\t}\n\n\t/**\n\t * 获得当前行\n\t *\n\t * @return 当前行\n\t */\n\tpublic int getCurrentRow() {\n\t\treturn this.currentRow.get();\n\t}\n\n\t/**\n\t * 获取Content-Disposition头对应的值，可以通过调用以下方法快速设置下载Excel的头信息：\n\t *\n\t * <pre>\n\t * response.setHeader(\"Content-Disposition\", excelWriter.getDisposition(\"test.xlsx\", CharsetUtil.CHARSET_UTF_8));\n\t * </pre>\n\t *\n\t * @param fileName 文件名，如果文件名没有扩展名，会自动按照生成Excel类型补齐扩展名，如果提供空，使用随机UUID\n\t * @param charset  编码，null则使用默认UTF-8编码\n\t * @return Content-Disposition值\n\t */\n\tpublic String getDisposition(String fileName, Charset charset) {\n\t\tif (null == charset) {\n\t\t\tcharset = CharsetUtil.CHARSET_UTF_8;\n\t\t}\n\n\t\tif (StrUtil.isBlank(fileName)) {\n\t\t\t// 未提供文件名使用随机UUID作为文件名\n\t\t\tfileName = IdUtil.fastSimpleUUID();\n\t\t}\n\n\t\tfileName = StrUtil.addSuffixIfNot(URLUtil.encodeAll(fileName, charset), isXlsx() ? \".xlsx\" : \".xls\");\n\t\treturn StrUtil.format(\"attachment; filename=\\\"{}\\\"\", fileName);\n\t}\n\n\t/**\n\t * 获取Content-Type头对应的值，可以通过调用以下方法快速设置下载Excel的头信息：\n\t *\n\t * <pre>\n\t * response.setContentType(excelWriter.getContentType());\n\t * </pre>\n\t *\n\t * @return Content-Type值\n\t * @since 5.6.7\n\t */\n\tpublic String getContentType() {\n\t\treturn isXlsx() ? ExcelUtil.XLSX_CONTENT_TYPE : ExcelUtil.XLS_CONTENT_TYPE;\n\t}\n\n\t/**\n\t * 设置当前所在行\n\t *\n\t * @param rowIndex 行号\n\t * @return this\n\t */\n\tpublic ExcelWriter setCurrentRow(int rowIndex) {\n\t\tthis.currentRow.set(rowIndex);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 定位到最后一行的后边，用于追加数据\n\t *\n\t * @return this\n\t * @since 5.5.0\n\t */\n\tpublic ExcelWriter setCurrentRowToEnd() {\n\t\treturn setCurrentRow(getRowCount());\n\t}\n\n\t/**\n\t * 跳过当前行\n\t *\n\t * @return this\n\t */\n\tpublic ExcelWriter passCurrentRow() {\n\t\tthis.currentRow.incrementAndGet();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 跳过指定行数\n\t *\n\t * @param rows 跳过的行数\n\t * @return this\n\t */\n\tpublic ExcelWriter passRows(int rows) {\n\t\tthis.currentRow.addAndGet(rows);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置当前行为0\n\t *\n\t * @return this\n\t */\n\tpublic ExcelWriter resetRow() {\n\t\tthis.currentRow.set(0);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置写出的目标文件\n\t *\n\t * @param destFile 目标文件\n\t * @return this\n\t */\n\tpublic ExcelWriter setDestFile(File destFile) {\n\t\tthis.destFile = destFile;\n\t\treturn this;\n\t}\n\n\t//region header alias\n\t@Override\n\tpublic ExcelWriter setHeaderAlias(Map<String, String> headerAlias) {\n\t\t// 新增别名时清除比较器缓存\n\t\tthis.aliasComparator = null;\n\t\treturn super.setHeaderAlias(headerAlias);\n\t}\n\n\t@Override\n\tpublic ExcelWriter clearHeaderAlias() {\n\t\t// 清空别名时清除比较器缓存\n\t\tthis.aliasComparator = null;\n\t\treturn super.clearHeaderAlias();\n\t}\n\n\t@Override\n\tpublic ExcelWriter addHeaderAlias(String name, String alias) {\n\t\t// 新增别名时清除比较器缓存\n\t\tthis.aliasComparator = null;\n\t\treturn super.addHeaderAlias(name, alias);\n\t}\n\n\t/**\n\t * 设置是否只保留别名中的字段值，如果为true，则不设置alias的字段将不被输出，false表示原样输出\n\t * Bean中设置@Alias时，setOnlyAlias是无效的，这个参数只和addHeaderAlias配合使用，原因是注解是Bean内部的操作，而addHeaderAlias是Writer的操作，不互通。\n\t *\n\t * @param isOnlyAlias 是否只保留别名中的字段值\n\t * @return this\n\t * @since 4.1.22\n\t */\n\tpublic ExcelWriter setOnlyAlias(boolean isOnlyAlias) {\n\t\tthis.onlyAlias = isOnlyAlias;\n\t\treturn this;\n\t}\n\t//endregion\n\n\t/**\n\t * 设置窗口冻结，之前冻结的窗口会被覆盖，如果rowSplit为0表示取消冻结\n\t *\n\t * @param rowSplit 冻结的行及行数，2表示前两行\n\t * @return this\n\t * @since 5.2.5\n\t */\n\tpublic ExcelWriter setFreezePane(int rowSplit) {\n\t\treturn setFreezePane(0, rowSplit);\n\t}\n\n\t/**\n\t * 设置窗口冻结，之前冻结的窗口会被覆盖，如果colSplit和rowSplit为0表示取消冻结\n\t *\n\t * @param colSplit 冻结的列及列数，2表示前两列\n\t * @param rowSplit 冻结的行及行数，2表示前两行\n\t * @return this\n\t * @since 5.2.5\n\t */\n\tpublic ExcelWriter setFreezePane(int colSplit, int rowSplit) {\n\t\tgetSheet().createFreezePane(colSplit, rowSplit);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置列宽（单位为一个字符的宽度，例如传入width为10，表示10个字符的宽度）\n\t *\n\t * @param columnIndex 列号（从0开始计数，-1表示所有列的默认宽度）\n\t * @param width       宽度（单位1~255个字符宽度）\n\t * @return this\n\t * @since 4.0.8\n\t */\n\tpublic ExcelWriter setColumnWidth(int columnIndex, int width) {\n\t\tif (columnIndex < 0) {\n\t\t\tthis.sheet.setDefaultColumnWidth(width);\n\t\t} else {\n\t\t\tthis.sheet.setColumnWidth(columnIndex, width * 256);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置默认行高，值为一个点的高度\n\t *\n\t * @param height 高度\n\t * @return this\n\t * @since 4.6.5\n\t */\n\tpublic ExcelWriter setDefaultRowHeight(int height) {\n\t\treturn setRowHeight(-1, height);\n\t}\n\n\t/**\n\t * 设置行高，值为一个点的高度\n\t *\n\t * @param rownum 行号（从0开始计数，-1表示所有行的默认高度）\n\t * @param height 高度\n\t * @return this\n\t * @since 4.0.8\n\t */\n\tpublic ExcelWriter setRowHeight(int rownum, int height) {\n\t\tif (rownum < 0) {\n\t\t\tthis.sheet.setDefaultRowHeightInPoints(height);\n\t\t} else {\n\t\t\tfinal Row row = this.sheet.getRow(rownum);\n\t\t\tif (null != row) {\n\t\t\t\trow.setHeightInPoints(height);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置Excel页眉或页脚\n\t *\n\t * @param text     页脚的文本\n\t * @param align    对齐方式枚举 {@link Align}\n\t * @param isFooter 是否为页脚，false表示页眉，true表示页脚\n\t * @return this\n\t * @since 4.1.0\n\t */\n\tpublic ExcelWriter setHeaderOrFooter(String text, Align align, boolean isFooter) {\n\t\tfinal HeaderFooter headerFooter = isFooter ? this.sheet.getFooter() : this.sheet.getHeader();\n\t\tswitch (align) {\n\t\t\tcase LEFT:\n\t\t\t\theaderFooter.setLeft(text);\n\t\t\t\tbreak;\n\t\t\tcase RIGHT:\n\t\t\t\theaderFooter.setRight(text);\n\t\t\t\tbreak;\n\t\t\tcase CENTER:\n\t\t\t\theaderFooter.setCenter(text);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置忽略错误，即Excel中的绿色警告小标，只支持XSSFSheet<br>\n\t * 见：https://stackoverflow.com/questions/23488221/how-to-remove-warning-in-excel-using-apache-poi-in-java\n\t *\n\t * @param cellRangeAddress  指定单元格范围\n\t * @param ignoredErrorTypes 忽略的错误类型列表\n\t * @return this\n\t * @throws UnsupportedOperationException 如果sheet不是XSSFSheet\n\t * @since 5.8.28\n\t */\n\tpublic ExcelWriter addIgnoredErrors(final CellRangeAddress cellRangeAddress, final IgnoredErrorType... ignoredErrorTypes) throws UnsupportedOperationException {\n\t\tfinal Sheet sheet = this.sheet;\n\t\tif (sheet instanceof XSSFSheet) {\n\t\t\t((XSSFSheet) sheet).addIgnoredErrors(cellRangeAddress, ignoredErrorTypes);\n\t\t\treturn this;\n\t\t}\n\n\t\tthrow new UnsupportedOperationException(\"Only XSSFSheet supports addIgnoredErrors\");\n\t}\n\n\t/**\n\t * 增加下拉列表\n\t *\n\t * @param x          x坐标，列号，从0开始\n\t * @param y          y坐标，行号，从0开始\n\t * @param selectList 下拉列表\n\t * @return this\n\t * @since 4.6.2\n\t */\n\tpublic ExcelWriter addSelect(int x, int y, String... selectList) {\n\t\treturn addSelect(new CellRangeAddressList(y, y, x, x), selectList);\n\t}\n\n\t/**\n\t * 增加下拉列表\n\t *\n\t * @param regions    {@link CellRangeAddressList} 指定下拉列表所占的单元格范围\n\t * @param selectList 下拉列表内容\n\t * @return this\n\t * @since 4.6.2\n\t */\n\tpublic ExcelWriter addSelect(CellRangeAddressList regions, String... selectList) {\n\t\tfinal DataValidationHelper validationHelper = this.sheet.getDataValidationHelper();\n\t\tfinal DataValidationConstraint constraint = validationHelper.createExplicitListConstraint(selectList);\n\n\t\t//设置下拉框数据\n\t\tfinal DataValidation dataValidation = validationHelper.createValidation(constraint, regions);\n\n\t\t//处理Excel兼容性问题\n\t\tif (dataValidation instanceof XSSFDataValidation) {\n\t\t\tdataValidation.setSuppressDropDownArrow(true);\n\t\t\tdataValidation.setShowErrorBox(true);\n\t\t} else {\n\t\t\tdataValidation.setSuppressDropDownArrow(false);\n\t\t}\n\n\t\treturn addValidationData(dataValidation);\n\t}\n\n\t/**\n\t * 增加单元格控制，比如下拉列表、日期验证、数字范围验证等\n\t *\n\t * @param dataValidation {@link DataValidation}\n\t * @return this\n\t * @since 4.6.2\n\t */\n\tpublic ExcelWriter addValidationData(DataValidation dataValidation) {\n\t\tthis.sheet.addValidationData(dataValidation);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 合并当前行的单元格<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param lastColumn 合并到的最后一个列号\n\t * @return this\n\t */\n\tpublic ExcelWriter merge(int lastColumn) {\n\t\treturn merge(lastColumn, null);\n\t}\n\n\t/**\n\t * 合并当前行的单元格，并写入对象到单元格<br>\n\t * 如果写到单元格中的内容非null，行号自动+1，否则当前行号不变<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param lastColumn 合并到的最后一个列号\n\t * @param content    合并单元格后的内容\n\t * @return this\n\t */\n\tpublic ExcelWriter merge(int lastColumn, Object content) {\n\t\treturn merge(lastColumn, content, true);\n\t}\n\n\t/**\n\t * 合并某行的单元格，并写入对象到单元格<br>\n\t * 如果写到单元格中的内容非null，行号自动+1，否则当前行号不变<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param lastColumn       合并到的最后一个列号\n\t * @param content          合并单元格后的内容\n\t * @param isSetHeaderStyle 是否为合并后的单元格设置默认标题样式，只提取边框样式\n\t * @return this\n\t * @since 4.0.10\n\t */\n\tpublic ExcelWriter merge(int lastColumn, Object content, boolean isSetHeaderStyle) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\n\t\tfinal int rowIndex = this.currentRow.get();\n\t\tmerge(rowIndex, rowIndex, 0, lastColumn, content, isSetHeaderStyle);\n\n\t\t// 设置内容后跳到下一行\n\t\tif (null != content) {\n\t\t\tthis.currentRow.incrementAndGet();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 合并某行的单元格，并写入对象到单元格<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param firstRow         起始行，0开始\n\t * @param lastRow          结束行，0开始\n\t * @param firstColumn      起始列，0开始\n\t * @param lastColumn       结束列，0开始\n\t * @param content          合并单元格后的内容\n\t * @param isSetHeaderStyle 是否为合并后的单元格设置默认标题样式，只提取边框样式\n\t * @return this\n\t * @since 4.0.10\n\t */\n\tpublic ExcelWriter merge(int firstRow, int lastRow, int firstColumn, int lastColumn, Object content, boolean isSetHeaderStyle) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\n\t\tCellStyle style = null;\n\t\tif (null != this.styleSet) {\n\t\t\tstyle = styleSet.getStyleByValueType(content, isSetHeaderStyle);\n\t\t}\n\n\t\treturn merge(firstRow, lastRow, firstColumn, lastColumn, content, style);\n\t}\n\n\t/**\n\t * 合并单元格，并写入对象到单元格,使用指定的样式<br>\n\t * 指定样式传入null，则不使用任何样式\n\t *\n\t * @param firstRow    起始行，0开始\n\t * @param lastRow     结束行，0开始\n\t * @param firstColumn 起始列，0开始\n\t * @param lastColumn  结束列，0开始\n\t * @param content     合并单元格后的内容\n\t * @param cellStyle   合并后单元格使用的样式，可以为null\n\t * @return this\n\t * @since 5.6.5\n\t */\n\tpublic ExcelWriter merge(int firstRow, int lastRow, int firstColumn, int lastColumn, Object content, CellStyle cellStyle) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\n\t\tCellUtil.mergingCells(this.getSheet(), firstRow, lastRow, firstColumn, lastColumn, cellStyle);\n\n\t\t// 设置内容\n\t\tif (null != content) {\n\t\t\tfinal Cell cell = getOrCreateCell(firstColumn, firstRow);\n\t\t\tCellUtil.setCellValue(cell, content, cellStyle);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动增加<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式<br>\n\t * 默认的，当当前行号为0时，写出标题（如果为Map或Bean），否则不写标题\n\t *\n\t * <p>\n\t * data中元素支持的类型有：\n\t *\n\t * <pre>\n\t * 1. Iterable，即元素为一个集合，元素被当作一行，data表示多行<br>\n\t * 2. Map，即元素为一个Map，第一个Map的keys作为首行，剩下的行为Map的values，data表示多行 <br>\n\t * 3. Bean，即元素为一个Bean，第一个Bean的字段名列表会作为首行，剩下的行为Bean的字段值列表，data表示多行 <br>\n\t * 4. 其它类型，按照基本类型输出（例如字符串）\n\t * </pre>\n\t *\n\t * @param data 数据\n\t * @return this\n\t */\n\tpublic ExcelWriter write(Iterable<?> data) {\n\t\treturn write(data, 0 == getCurrentRow());\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动增加<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * <p>\n\t * data中元素支持的类型有：\n\t *\n\t * <pre>\n\t * 1. Iterable，即元素为一个集合，元素被当作一行，data表示多行<br>\n\t * 2. Map，即元素为一个Map，第一个Map的keys作为首行，剩下的行为Map的values，data表示多行 <br>\n\t * 3. Bean，即元素为一个Bean，第一个Bean的字段名列表会作为首行，剩下的行为Bean的字段值列表，data表示多行 <br>\n\t * 4. 其它类型，按照基本类型输出（例如字符串）\n\t * </pre>\n\t *\n\t * @param data             数据\n\t * @param isWriteKeyAsHead 是否强制写出标题行（Map或Bean）\n\t * @return this\n\t */\n\tpublic ExcelWriter write(Iterable<?> data, boolean isWriteKeyAsHead) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tboolean isFirst = true;\n\t\tfor (Object object : data) {\n\t\t\twriteRow(object, isFirst && isWriteKeyAsHead);\n\t\t\tif (isFirst) {\n\t\t\t\tisFirst = false;\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动增加<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式<br>\n\t * data中元素支持的类型有：\n\t *\n\t * <p>\n\t * 1. Map，即元素为一个Map，第一个Map的keys作为首行，剩下的行为Map的values，data表示多行 <br>\n\t * 2. Bean，即元素为一个Bean，第一个Bean的字段名列表会作为首行，剩下的行为Bean的字段值列表，data表示多行 <br>\n\t * </p>\n\t *\n\t * @param data       数据\n\t * @param comparator 比较器，用于字段名的排序\n\t * @return this\n\t * @since 3.2.3\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic ExcelWriter write(Iterable<?> data, Comparator<String> comparator) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tboolean isFirstRow = true;\n\t\tMap<?, ?> map;\n\t\tfor (Object obj : data) {\n\t\t\t// 只第一行使用比较器排序\n\t\t\tif (isFirstRow) {\n\t\t\t\tif (obj instanceof Map) {\n\t\t\t\t\tmap = new TreeMap<>(comparator);\n\t\t\t\t\tmap.putAll((Map) obj);\n\t\t\t\t} else {\n\t\t\t\t\tmap = BeanUtil.beanToMap(obj, new TreeMap<>(comparator), false, false);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (obj instanceof Map) {\n\t\t\t\t\tmap = (Map) obj;\n\t\t\t\t} else {\n\t\t\t\t\tmap = BeanUtil.beanToMap(obj, new HashMap<>(), false, false);\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteRow(map, isFirstRow);\n\t\t\tif (isFirstRow) {\n\t\t\t\tisFirstRow = false;\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 添加图片到当前sheet中 / 默认图片类型png / 默认的起始坐标和结束坐标都为0\n\t *\n\t * @param imgFile 图片文件\n\t * @param col1    指定起始的列，下标从0开始\n\t * @param row1    指定起始的行，下标从0开始\n\t * @param col2    指定结束的列，下标从0开始\n\t * @param row2    指定结束的行，下标从0开始\n\t * @return this\n\t * @author vhukze\n\t * @since 5.7.18\n\t */\n\tpublic ExcelWriter writeImg(File imgFile, int col1, int row1, int col2, int row2) {\n\t\treturn this.writeImg(imgFile, 0, 0, 0, 0, col1, row1, col2, row2);\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 添加图片到当前sheet中 / 默认图片类型png\n\t *\n\t * @param imgFile 图片文件\n\t * @param dx1     起始单元格中的x坐标\n\t * @param dy1     起始单元格中的y坐标\n\t * @param dx2     结束单元格中的x坐标\n\t * @param dy2     结束单元格中的y坐标\n\t * @param col1    指定起始的列，下标从0开始\n\t * @param row1    指定起始的行，下标从0开始\n\t * @param col2    指定结束的列，下标从0开始\n\t * @param row2    指定结束的行，下标从0开始\n\t * @return this\n\t * @author vhukze\n\t * @since 5.7.18\n\t */\n\tpublic ExcelWriter writeImg(File imgFile, int dx1, int dy1, int dx2, int dy2, int col1, int row1,\n\t\t\t\t\t\t\t\tint col2, int row2) {\n\t\treturn this.writeImg(imgFile, Workbook.PICTURE_TYPE_PNG, dx1, dy1, dx2, dy2, col1, row1, col2, row2);\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 添加图片到当前sheet中\n\t *\n\t * @param imgFile 图片文件\n\t * @param imgType 图片类型，对应poi中Workbook类中的图片类型2-7变量\n\t * @param dx1     起始单元格中的x坐标\n\t * @param dy1     起始单元格中的y坐标\n\t * @param dx2     结束单元格中的x坐标\n\t * @param dy2     结束单元格中的y坐标\n\t * @param col1    指定起始的列，下标从0开始\n\t * @param row1    指定起始的行，下标从0开始\n\t * @param col2    指定结束的列，下标从0开始\n\t * @param row2    指定结束的行，下标从0开始\n\t * @return this\n\t * @author vhukze\n\t * @since 5.7.18\n\t */\n\tpublic ExcelWriter writeImg(File imgFile, int imgType, int dx1, int dy1, int dx2,\n\t\t\t\t\t\t\t\tint dy2, int col1, int row1, int col2, int row2) {\n\t\treturn writeImg(FileUtil.readBytes(imgFile), imgType, dx1,\n\t\t\tdy1, dx2, dy2, col1, row1, col2, row2);\n\t}\n\n\t/**\n\t * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 添加图片到当前sheet中\n\t *\n\t * @param pictureData 数据bytes\n\t * @param imgType     图片类型，对应poi中Workbook类中的图片类型2-7变量\n\t * @param dx1         起始单元格中的x坐标\n\t * @param dy1         起始单元格中的y坐标\n\t * @param dx2         结束单元格中的x坐标\n\t * @param dy2         结束单元格中的y坐标\n\t * @param col1        指定起始的列，下标从0开始\n\t * @param row1        指定起始的行，下标从0开始\n\t * @param col2        指定结束的列，下标从0开始\n\t * @param row2        指定结束的行，下标从0开始\n\t * @return this\n\t * @author vhukze\n\t * @since 5.8.0\n\t */\n\tpublic ExcelWriter writeImg(byte[] pictureData, int imgType, int dx1, int dy1, int dx2,\n\t\t\t\t\t\t\t\tint dy2, int col1, int row1, int col2, int row2) {\n\t\tDrawing<?> patriarch = this.sheet.createDrawingPatriarch();\n\t\tClientAnchor anchor = this.workbook.getCreationHelper().createClientAnchor();\n\t\tanchor.setDx1(dx1);\n\t\tanchor.setDy1(dy1);\n\t\tanchor.setDx2(dx2);\n\t\tanchor.setDy2(dy2);\n\t\tanchor.setCol1(col1);\n\t\tanchor.setRow1(row1);\n\t\tanchor.setCol2(col2);\n\t\tanchor.setRow2(row2);\n\n\t\tpatriarch.createPicture(anchor, this.workbook.addPicture(pictureData, imgType));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出一行标题数据<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param rowData 一行的数据\n\t * @return this\n\t */\n\tpublic ExcelWriter writeHeadRow(Iterable<?> rowData) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tthis.headLocationCache = new SafeConcurrentHashMap<>();\n\t\tfinal Row row = this.sheet.createRow(this.currentRow.getAndIncrement());\n\t\tint i = 0;\n\t\tCell cell;\n\t\tfor (Object value : rowData) {\n\t\t\tcell = row.createCell(i);\n\t\t\tCellUtil.setCellValue(cell, value, this.styleSet, true);\n\t\t\tthis.headLocationCache.put(StrUtil.toString(value), i);\n\t\t\ti++;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出复杂标题的第二行标题数据<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认标题样式，可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式\n\t *\n\t * <p>\n\t * 此方法的逻辑是：将一行数据写出到当前行，遇到已存在的单元格跳过，不存在的创建并赋值。\n\t * </p>\n\t *\n\t * @param rowData 一行的数据\n\t * @return this\n\t */\n\tpublic ExcelWriter writeSecHeadRow(Iterable<?> rowData) {\n\t\tfinal Row row = RowUtil.getOrCreateRow(this.sheet, this.currentRow.getAndIncrement());\n\t\tIterator<?> iterator = rowData.iterator();\n\t\t//如果获取的row存在单元格，则执行复杂表头逻辑，否则直接调用writeHeadRow(Iterable<?> rowData)\n\t\tif (row.getLastCellNum() != 0) {\n\t\t\tfor (int i = 0; i < this.workbook.getSpreadsheetVersion().getMaxColumns(); i++) {\n\t\t\t\tCell cell = row.getCell(i);\n\t\t\t\tif (cell != null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (iterator.hasNext()) {\n\t\t\t\t\tcell = row.createCell(i);\n\t\t\t\t\tCellUtil.setCellValue(cell, iterator.next(), this.styleSet, true);\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twriteHeadRow(rowData);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出一行，根据rowBean数据类型不同，写出情况如下：\n\t *\n\t * <pre>\n\t * 1、如果为Iterable，直接写出一行\n\t * 2、如果为Map，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values\n\t * 3、如果为Bean，转为Map写出，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values\n\t * </pre>\n\t *\n\t * @param rowBean          写出的Bean\n\t * @param isWriteKeyAsHead 为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values\n\t * @return this\n\t * @see #writeRow(Iterable)\n\t * @see #writeRow(Map, boolean)\n\t * @since 4.1.5\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic ExcelWriter writeRow(Object rowBean, boolean isWriteKeyAsHead) {\n\t\tMap rowMap;\n\t\tif (rowBean instanceof Map) {\n\t\t\tif (MapUtil.isNotEmpty(this.headerAlias)) {\n\t\t\t\trowMap = MapUtil.newTreeMap((Map) rowBean, getCachedAliasComparator());\n\t\t\t} else {\n\t\t\t\trowMap = (Map) rowBean;\n\t\t\t}\n\t\t} else if (rowBean instanceof Iterable) {\n\t\t\t// issue#2398@Github\n\t\t\t// MapWrapper由于实现了Iterable接口，应该优先按照Map处理\n\t\t\treturn writeRow((Iterable<?>) rowBean);\n\t\t} else if (rowBean instanceof Hyperlink) {\n\t\t\t// Hyperlink当成一个值\n\t\t\treturn writeRow(CollUtil.newArrayList(rowBean), isWriteKeyAsHead);\n\t\t} else if (BeanUtil.isReadableBean(rowBean.getClass())) {\n\t\t\tif (MapUtil.isEmpty(this.headerAlias)) {\n\t\t\t\trowMap = BeanUtil.beanToMap(rowBean, new LinkedHashMap<>(), false, false);\n\t\t\t} else {\n\t\t\t\t// 别名存在情况下按照别名的添加顺序排序Bean数据\n\t\t\t\trowMap = BeanUtil.beanToMap(rowBean, new TreeMap<>(getCachedAliasComparator()), false, false);\n\t\t\t}\n\t\t} else {\n\t\t\t// 其它转为字符串默认输出\n\t\t\treturn writeRow(CollUtil.newArrayList(rowBean), isWriteKeyAsHead);\n\t\t}\n\t\treturn writeRow(rowMap, isWriteKeyAsHead);\n\t}\n\n\t/**\n\t * 将一个Map写入到Excel，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values<br>\n\t * 如果rowMap为空（包括null），则写出空行\n\t *\n\t * @param rowMap           写出的Map，为空（包括null），则写出空行\n\t * @param isWriteKeyAsHead 为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values\n\t * @return this\n\t */\n\tpublic ExcelWriter writeRow(Map<?, ?> rowMap, boolean isWriteKeyAsHead) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tif (MapUtil.isEmpty(rowMap)) {\n\t\t\t// 如果写出数据为null或空，跳过当前行\n\t\t\treturn passCurrentRow();\n\t\t}\n\n\t\tfinal Table<?, ?, ?> aliasTable = aliasTable(rowMap);\n\t\tif (isWriteKeyAsHead) {\n\t\t\t// 写出标题行，并记录标题别名和列号的关系\n\t\t\twriteHeadRow(aliasTable.columnKeys());\n\t\t\t// 记录原数据key对应列号\n\t\t\tint i = 0;\n\t\t\tfor (Object key : aliasTable.rowKeySet()) {\n\t\t\t\tthis.headLocationCache.putIfAbsent(StrUtil.toString(key), i);\n\t\t\t\ti++;\n\t\t\t}\n\t\t}\n\n\t\t// 如果已经写出标题行，根据标题行找对应的值写入\n\t\tif (MapUtil.isNotEmpty(this.headLocationCache)) {\n\t\t\tfinal Row row = RowUtil.getOrCreateRow(this.sheet, this.currentRow.getAndIncrement());\n\t\t\tInteger location;\n\t\t\tfor (Table.Cell<?, ?, ?> cell : aliasTable) {\n\t\t\t\t// 首先查找原名对应的列号\n\t\t\t\tlocation = this.headLocationCache.get(StrUtil.toString(cell.getRowKey()));\n\t\t\t\tif (null == location) {\n\t\t\t\t\t// 未找到，则查找别名对应的列号\n\t\t\t\t\tlocation = this.headLocationCache.get(StrUtil.toString(cell.getColumnKey()));\n\t\t\t\t}\n\t\t\t\tif (null != location) {\n\t\t\t\t\tCellUtil.setCellValue(CellUtil.getOrCreateCell(row, location), cell.getValue(), this.styleSet, false);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twriteRow(aliasTable.values());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写出一行数据<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param rowData 一行的数据\n\t * @return this\n\t */\n\tpublic ExcelWriter writeRow(Iterable<?> rowData) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tRowUtil.writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData, this.styleSet, false);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 从第1列开始按列写入数据(index 从0开始)<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param colMap           一列的数据\n\t * @param isWriteKeyAsHead 是否将Map的Key作为表头输出，如果为True第一行为表头，紧接着为values\n\t * @return this\n\t */\n\tpublic ExcelWriter writeCol(Map<?, ? extends Iterable<?>> colMap, boolean isWriteKeyAsHead) {\n\t\treturn writeCol(colMap, 0, isWriteKeyAsHead);\n\t}\n\n\t/**\n\t * 从指定列开始按列写入数据(index 从0开始)<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param colMap           一列的数据\n\t * @param startColIndex    起始的列号，从0开始\n\t * @param isWriteKeyAsHead 是否将Map的Key作为表头输出，如果为True第一行为表头，紧接着为values\n\t * @return this\n\t */\n\tpublic ExcelWriter writeCol(Map<?, ? extends Iterable<?>> colMap, int startColIndex, boolean isWriteKeyAsHead) {\n\t\tfor (Object k : colMap.keySet()) {\n\t\t\tIterable<?> v = colMap.get(k);\n\t\t\tif (v != null) {\n\t\t\t\twriteCol(isWriteKeyAsHead ? k : null, startColIndex, v, startColIndex != colMap.size() - 1);\n\t\t\t\tstartColIndex++;\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 为第一列写入数据<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param headerVal       表头名称,如果为null则不写入\n\t * @param colData         需要写入的列数据\n\t * @param isResetRowIndex 如果为true，写入完毕后Row index 将会重置为写入之前的未知，如果为false，写入完毕后Row index将会在写完的数据下方\n\t * @return this\n\t */\n\tpublic ExcelWriter writeCol(Object headerVal, Iterable<?> colData, boolean isResetRowIndex) {\n\t\treturn writeCol(headerVal, 0, colData, isResetRowIndex);\n\t}\n\n\t/**\n\t * 为第指定列写入数据<br>\n\t * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>\n\t * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1<br>\n\t * 样式为默认样式，可使用{@link #getCellStyle()}方法调用后自定义默认样式\n\t *\n\t * @param headerVal       表头名称,如果为null则不写入\n\t * @param colIndex        列index\n\t * @param colData         需要写入的列数据\n\t * @param isResetRowIndex 如果为true，写入完毕后Row index 将会重置为写入之前的未知，如果为false，写入完毕后Row index将会在写完的数据下方\n\t * @return this\n\t */\n\tpublic ExcelWriter writeCol(Object headerVal, int colIndex, Iterable<?> colData, boolean isResetRowIndex) {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\tint currentRowIndex = currentRow.get();\n\t\tif (null != headerVal) {\n\t\t\twriteCellValue(colIndex, currentRowIndex, headerVal, true);\n\t\t\tcurrentRowIndex++;\n\t\t}\n\t\tfor (Object colDatum : colData) {\n\t\t\twriteCellValue(colIndex, currentRowIndex, colDatum);\n\t\t\tcurrentRowIndex++;\n\t\t}\n\t\tif (!isResetRowIndex) {\n\t\t\tcurrentRow.set(currentRowIndex);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 给指定单元格赋值，使用默认单元格样式，默认不是Header\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @param value       值\n\t * @return this\n\t * @since 5.1.4\n\t */\n\tpublic ExcelWriter writeCellValue(String locationRef, Object value) {\n\t\treturn writeCellValue(locationRef, value, false);\n\t}\n\n\t/**\n\t * 给指定单元格赋值，使用默认单元格样式\n\t *\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @param value       值\n\t * @param isHeader    是否为Header\n\t * @return this\n\t * @since 5.1.4\n\t */\n\tpublic ExcelWriter writeCellValue(String locationRef, Object value, boolean isHeader) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn writeCellValue(cellLocation.getX(), cellLocation.getY(), value, isHeader);\n\t}\n\n\t/**\n\t * 给指定单元格赋值，使用默认单元格样式，默认不是Header\n\t *\n\t * @param x     X坐标，从0计数，即列号\n\t * @param y     Y坐标，从0计数，即行号\n\t * @param value 值\n\t * @return this\n\t * @since 4.0.2\n\t */\n\tpublic ExcelWriter writeCellValue(int x, int y, Object value) {\n\t\treturn writeCellValue(x, y, value, false);\n\t}\n\n\t/**\n\t * 给指定单元格赋值，使用默认单元格样式\n\t *\n\t * @param x        X坐标，从0计数，即列号\n\t * @param y        Y坐标，从0计数，即行号\n\t * @param isHeader 是否为Header\n\t * @param value    值\n\t * @return this\n\t * @since 4.0.2\n\t */\n\tpublic ExcelWriter writeCellValue(int x, int y, Object value, boolean isHeader) {\n\t\tfinal Cell cell = getOrCreateCell(x, y);\n\t\tCellUtil.setCellValue(cell, value, this.styleSet, isHeader);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置某个单元格的样式<br>\n\t * 此方法用于多个单元格共享样式的情况<br>\n\t * 可以调用{@link #getOrCreateCellStyle(int, int)} 方法创建或取得一个样式对象。\n\t *\n\t * <p>\n\t * 需要注意的是，共享样式会共享同一个{@link CellStyle}，一个单元格样式改变，全部改变。\n\t *\n\t * @param style       单元格样式\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return this\n\t * @since 5.1.4\n\t */\n\tpublic ExcelWriter setStyle(CellStyle style, String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn setStyle(style, cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 设置某个单元格的样式<br>\n\t * 此方法用于多个单元格共享样式的情况<br>\n\t * 可以调用{@link #getOrCreateCellStyle(int, int)} 方法创建或取得一个样式对象。\n\t *\n\t * <p>\n\t * 需要注意的是，共享样式会共享同一个{@link CellStyle}，一个单元格样式改变，全部改变。\n\t *\n\t * @param style 单元格样式\n\t * @param x     X坐标，从0计数，即列号\n\t * @param y     Y坐标，从0计数，即行号\n\t * @return this\n\t * @since 4.6.3\n\t */\n\tpublic ExcelWriter setStyle(CellStyle style, int x, int y) {\n\t\tfinal Cell cell = getOrCreateCell(x, y);\n\t\tcell.setCellStyle(style);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置行样式\n\t *\n\t * @param y     Y坐标，从0计数，即行号\n\t * @param style 样式\n\t * @return this\n\t * @see Row#setRowStyle(CellStyle)\n\t * @since 5.4.5\n\t */\n\tpublic ExcelWriter setRowStyle(int y, CellStyle style) {\n\t\tgetOrCreateRow(y).setRowStyle(style);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 对数据行整行加自定义样式 仅对数据单元格设置 write后调用\n\t * <p>\n\t * {@link cn.hutool.poi.excel.ExcelWriter#setRowStyle(int, org.apache.poi.ss.usermodel.CellStyle)}\n\t * 这个方法加的样式会使整行没有数据的单元格也有样式\n\t * 特别是加背景色时很不美观 且有数据的单元格样式会被StyleSet中的样式覆盖掉\n\t *\n\t * @param y     行坐标\n\t * @param style 自定义的样式\n\t * @return this\n\t * @since 5.7.3\n\t */\n\tpublic ExcelWriter setRowStyleIfHasData(int y, CellStyle style) {\n\t\tif (y < 0) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid row number (\" + y + \")\");\n\t\t}\n\t\tint columnCount = this.getColumnCount();\n\t\tfor (int i = 0; i < columnCount; i++) {\n\t\t\tthis.setStyle(style, i, y);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置列的默认样式\n\t *\n\t * @param x     列号，从0开始\n\t * @param style 样式\n\t * @return this\n\t * @since 5.6.4\n\t */\n\tpublic ExcelWriter setColumnStyle(int x, CellStyle style) {\n\t\tthis.sheet.setDefaultColumnStyle(x, style);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置整个列的样式 仅对数据单元格设置 write后调用\n\t * <p>\n\t * {@link cn.hutool.poi.excel.ExcelWriter#setColumnStyle(int, org.apache.poi.ss.usermodel.CellStyle)}\n\t * 这个方法加的样式会使整列没有数据的单元格也有样式\n\t * 特别是加背景色时很不美观 且有数据的单元格样式会被StyleSet中的样式覆盖掉\n\t *\n\t * @param x     列的索引\n\t * @param y     起始行\n\t * @param style 样式\n\t * @return this\n\t * @since 5.7.3\n\t */\n\tpublic ExcelWriter setColumnStyleIfHasData(int x, int y, CellStyle style) {\n\t\tif (x < 0) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid column number (\" + x + \")\");\n\t\t}\n\t\tif (y < 0) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid row number (\" + y + \")\");\n\t\t}\n\t\tint rowCount = this.getRowCount();\n\t\tfor (int i = y; i < rowCount; i++) {\n\t\t\tthis.setStyle(style, x, i);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建字体\n\t *\n\t * @return 字体\n\t * @since 4.1.0\n\t */\n\tpublic Font createFont() {\n\t\treturn getWorkbook().createFont();\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到预定义的文件<br>\n\t * 如果用户未自定义输出的文件，将抛出{@link NullPointerException}<br>\n\t * 预定义文件可以通过{@link #setDestFile(File)} 方法预定义，或者通过构造定义\n\t *\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic ExcelWriter flush() throws IORuntimeException {\n\t\treturn flush(this.destFile);\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到文件<br>\n\t * 如果用户未自定义输出的文件，将抛出{@link NullPointerException}\n\t *\n\t * @param destFile 写出到的文件\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t * @since 4.0.6\n\t */\n\tpublic ExcelWriter flush(File destFile) throws IORuntimeException {\n\t\tAssert.notNull(destFile, \"[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream).\");\n\t\treturn flush(FileUtil.getOutputStream(destFile), true);\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到输出流\n\t *\n\t * @param out 输出流\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic ExcelWriter flush(OutputStream out) throws IORuntimeException {\n\t\treturn flush(out, false);\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到输出流\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 是否关闭输出流\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t * @since 4.4.1\n\t */\n\tpublic ExcelWriter flush(OutputStream out, boolean isCloseOut) throws IORuntimeException {\n\t\tAssert.isFalse(this.isClosed, \"ExcelWriter has been closed!\");\n\t\ttry {\n\t\t\tthis.workbook.write(out);\n\t\t\tout.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isCloseOut) {\n\t\t\t\tIoUtil.close(out);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭工作簿<br>\n\t * 如果用户设定了目标文件，先写出目标文件后给关闭工作簿\n\t */\n\t@Override\n\tpublic void close() {\n\t\tif (null != this.destFile) {\n\t\t\tflush();\n\t\t}\n\t\tcloseWithoutFlush();\n\t}\n\n\t/**\n\t * 关闭工作簿但是不写出\n\t */\n\tprotected void closeWithoutFlush() {\n\t\tsuper.close();\n\n\t\t// 清空对象\n\t\tthis.currentRow.set(0);\n\t\tthis.styleSet = null;\n\t}\n\n\t// -------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 为指定的key列表添加标题别名，如果没有定义key的别名，在onlyAlias为false时使用原key<br>\n\t * key为别名，value为字段值\n\t *\n\t * @param rowMap 一行数据\n\t * @return 别名列表\n\t */\n\tprivate Table<?, ?, ?> aliasTable(Map<?, ?> rowMap) {\n\t\tfinal Table<Object, Object, Object> filteredTable = new RowKeyTable<>(new LinkedHashMap<>(), TableMap::new);\n\t\tif (MapUtil.isEmpty(this.headerAlias)) {\n\t\t\trowMap.forEach((key, value) -> filteredTable.put(key, key, value));\n\t\t} else {\n\t\t\trowMap.forEach((key, value) -> {\n\t\t\t\tfinal String aliasName = this.headerAlias.get(StrUtil.toString(key));\n\t\t\t\tif (null != aliasName) {\n\t\t\t\t\t// 别名键值对加入\n\t\t\t\t\tfilteredTable.put(key, aliasName, value);\n\t\t\t\t} else if (false == this.onlyAlias) {\n\t\t\t\t\t// 保留无别名设置的键值对\n\t\t\t\t\tfilteredTable.put(key, key, value);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\treturn filteredTable;\n\t}\n\n\t/**\n\t * 获取单例的别名比较器，比较器的顺序为别名加入的顺序\n\t *\n\t * @return Comparator\n\t * @since 4.1.5\n\t */\n\tprivate Comparator<String> getCachedAliasComparator() {\n\t\tif (MapUtil.isEmpty(this.headerAlias)) {\n\t\t\treturn null;\n\t\t}\n\t\tComparator<String> aliasComparator = this.aliasComparator;\n\t\tif (null == aliasComparator) {\n\t\t\tSet<String> keySet = this.headerAlias.keySet();\n\t\t\taliasComparator = new IndexedComparator<>(keySet.toArray(new String[0]));\n\t\t\tthis.aliasComparator = aliasComparator;\n\t\t}\n\t\treturn aliasComparator;\n\t}\n\t// -------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/RowUtil.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.cell.CellEditor;\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.util.CellRangeAddress;\nimport org.apache.poi.ss.util.CellRangeUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\n/**\n * Excel中的行{@link Row}封装工具类\n *\n * @author looly\n * @since 4.0.7\n */\npublic class RowUtil {\n\t/**\n\t * 获取已有行或创建新行\n\t *\n\t * @param sheet    Excel表\n\t * @param rowIndex 行号\n\t * @return {@link Row}\n\t * @since 4.0.2\n\t */\n\tpublic static Row getOrCreateRow(Sheet sheet, int rowIndex) {\n\t\tRow row = sheet.getRow(rowIndex);\n\t\tif (null == row) {\n\t\t\trow = sheet.createRow(rowIndex);\n\t\t}\n\t\treturn row;\n\t}\n\n\t/**\n\t * 读取一行\n\t *\n\t * @param row        行\n\t * @param cellEditor 单元格编辑器\n\t * @return 单元格值列表\n\t */\n\tpublic static List<Object> readRow(Row row, CellEditor cellEditor) {\n\t\treturn readRow(row, 0, Short.MAX_VALUE, cellEditor);\n\t}\n\n\t/**\n\t * 读取一行\n\t *\n\t * @param row                 行\n\t * @param startCellNumInclude 起始单元格号，0开始（包含）\n\t * @param endCellNumInclude   结束单元格号，0开始（包含）\n\t * @param cellEditor          单元格编辑器\n\t * @return 单元格值列表\n\t */\n\tpublic static List<Object> readRow(Row row, int startCellNumInclude, int endCellNumInclude, CellEditor cellEditor) {\n\t\tif (null == row) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\t\tfinal short rowLength = row.getLastCellNum();\n\t\tif (rowLength < 0) {\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tfinal int size = Math.min(endCellNumInclude + 1, rowLength);\n\t\tfinal List<Object> cellValues = new ArrayList<>(size);\n\t\tObject cellValue;\n\t\tboolean isAllNull = true;\n\t\tfor (int i = startCellNumInclude; i < size; i++) {\n\t\t\tcellValue = CellUtil.getCellValue(CellUtil.getCell(row, i), cellEditor);\n\t\t\tisAllNull &= StrUtil.isEmptyIfStr(cellValue);\n\t\t\tcellValues.add(cellValue);\n\t\t}\n\n\t\tif (isAllNull) {\n\t\t\t// 如果每个元素都为空，则定义为空行\n\t\t\treturn ListUtil.empty();\n\t\t}\n\t\treturn cellValues;\n\t}\n\n\t/**\n\t * 写一行数据，无样式，非标题\n\t *\n\t * @param row      行\n\t * @param rowData  一行的数据\n\t */\n\tpublic static void writeRow(Row row, Iterable<?> rowData) {\n\t\twriteRow(row, rowData, null, false);\n\t}\n\n\t/**\n\t * 写一行数据\n\t *\n\t * @param row      行\n\t * @param rowData  一行的数据\n\t * @param styleSet 单元格样式集，包括日期等样式，null表示无样式\n\t * @param isHeader 是否为标题行\n\t */\n\tpublic static void writeRow(Row row, Iterable<?> rowData, StyleSet styleSet, boolean isHeader) {\n\t\tint i = 0;\n\t\tCell cell;\n\t\tfor (Object value : rowData) {\n\t\t\tcell = row.createCell(i);\n\t\t\tCellUtil.setCellValue(cell, value, styleSet, isHeader);\n\t\t\ti++;\n\t\t}\n\t}\n\n\t/**\n\t * 插入行\n\t *\n\t * @param sheet        工作表\n\t * @param startRow     插入的起始行\n\t * @param insertNumber 插入的行数\n\t * @since 5.4.2\n\t */\n\tpublic static void insertRow(Sheet sheet, int startRow, int insertNumber) {\n\t\tif (insertNumber <= 0) {\n\t\t\treturn;\n\t\t}\n\t\t// 插入位置的行，如果插入的行不存在则创建新行\n\t\tRow sourceRow = getOrCreateRow(sheet, startRow);\n\t\t// 从插入行开始到最后一行向下移动\n\t\tsheet.shiftRows(startRow, sheet.getLastRowNum(), insertNumber, true, false);\n\n\t\t// 填充移动后留下的空行\n\t\tIntStream.range(startRow, startRow + insertNumber).forEachOrdered(i -> {\n\t\t\tRow row = sheet.createRow(i);\n\t\t\trow.setHeightInPoints(sourceRow.getHeightInPoints());\n\t\t\tshort lastCellNum = sourceRow.getLastCellNum();\n\t\t\tIntStream.range(0, lastCellNum).forEachOrdered(j -> {\n\t\t\t\tCell cell = row.createCell(j);\n\t\t\t\tcell.setCellStyle(sourceRow.getCell(j).getCellStyle());\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * 从工作表中删除指定的行，此方法修复sheet.shiftRows删除行时会拆分合并的单元格的问题\n\t *\n\t * @param row 需要删除的行\n\t * @see <a href=\"https://bz.apache.org/bugzilla/show_bug.cgi?id=56454\">sheet.shiftRows的bug</a>\n\t * @since 5.4.2\n\t */\n\tpublic static void removeRow(Row row) {\n\t\tif (row == null) {\n\t\t\treturn;\n\t\t}\n\t\tint rowIndex = row.getRowNum();\n\t\tSheet sheet = row.getSheet();\n\t\tint lastRow = sheet.getLastRowNum();\n\t\tif (rowIndex >= 0 && rowIndex < lastRow) {\n\t\t\tList<CellRangeAddress> updateMergedRegions = new ArrayList<>();\n\t\t\t// 找出需要调整的合并单元格\n\t\t\tIntStream.range(0, sheet.getNumMergedRegions())\n\t\t\t\t\t.forEach(i -> {\n\t\t\t\t\t\tCellRangeAddress mr = sheet.getMergedRegion(i);\n\t\t\t\t\t\tif (!mr.containsRow(rowIndex)) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// 缩减以后变成单个单元格则删除合并单元格\n\t\t\t\t\t\tif (mr.getFirstRow() == mr.getLastRow() - 1 && mr.getFirstColumn() == mr.getLastColumn()) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tupdateMergedRegions.add(mr);\n\t\t\t\t\t});\n\n\t\t\t// 将行上移\n\t\t\tsheet.shiftRows(rowIndex + 1, lastRow, -1);\n\n\t\t\t// 找出删除行所在的合并单元格\n\t\t\tList<Integer> removeMergedRegions = IntStream.range(0, sheet.getNumMergedRegions())\n\t\t\t\t\t.filter(i -> updateMergedRegions.stream().\n\t\t\t\t\t\t\tanyMatch(umr -> CellRangeUtil.contains(umr, sheet.getMergedRegion(i))))\n\t\t\t\t\t.boxed()\n\t\t\t\t\t.collect(Collectors.toList());\n\n\t\t\tsheet.removeMergedRegions(removeMergedRegions);\n\t\t\tupdateMergedRegions.forEach(mr -> {\n\t\t\t\tmr.setLastRow(mr.getLastRow() - 1);\n\t\t\t\tsheet.addMergedRegion(mr);\n\t\t\t});\n\t\t\tsheet.validateMergedRegions();\n\t\t}\n\t\tif (rowIndex == lastRow) {\n\t\t\tRow removingRow = sheet.getRow(rowIndex);\n\t\t\tif (removingRow != null) {\n\t\t\t\tsheet.removeRow(removingRow);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/StyleSet.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.hssf.util.HSSFColor;\nimport org.apache.poi.ss.usermodel.*;\n\nimport java.io.Serializable;\nimport java.math.BigDecimal;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 样式集合，此样式集合汇集了整个工作簿的样式，用于减少样式的创建和冗余\n *\n * @author looly\n */\npublic class StyleSet implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 工作簿引用\n\t */\n\tprivate final Workbook workbook;\n\t/**\n\t * 标题样式\n\t */\n\tprotected final CellStyle headCellStyle;\n\t/**\n\t * 默认样式\n\t */\n\tprotected final CellStyle cellStyle;\n\t/**\n\t * 默认数字样式\n\t */\n\tprotected final CellStyle cellStyleForNumber;\n\t/**\n\t * 默认日期样式\n\t */\n\tprotected final CellStyle cellStyleForDate;\n\t/**\n\t * 默认链接样式\n\t */\n\tprotected final CellStyle cellStyleForHyperlink;\n\n\t/**\n\t * 构造\n\t *\n\t * @param workbook 工作簿\n\t */\n\tpublic StyleSet(Workbook workbook) {\n\t\tthis.workbook = workbook;\n\t\tthis.headCellStyle = StyleUtil.createHeadCellStyle(workbook);\n\t\tthis.cellStyle = StyleUtil.createDefaultCellStyle(workbook);\n\n\t\t// 默认数字格式\n\t\tcellStyleForNumber = StyleUtil.cloneCellStyle(workbook, this.cellStyle);\n\t\t// 2表示：0.00\n\t\tcellStyleForNumber.setDataFormat((short) 2);\n\n\t\t// 默认日期格式\n\t\tthis.cellStyleForDate = StyleUtil.cloneCellStyle(workbook, this.cellStyle);\n\t\t// 22表示：m/d/yy h:mm\n\t\tthis.cellStyleForDate.setDataFormat((short) 22);\n\n\n\t\t// 默认链接样式\n\t\tthis.cellStyleForHyperlink = StyleUtil.cloneCellStyle(workbook, this.cellStyle);\n\t\tfinal Font font = this.workbook.createFont();\n\t\tfont.setUnderline((byte) 1);\n\t\tfont.setColor(HSSFColor.HSSFColorPredefined.BLUE.getIndex());\n\t\tthis.cellStyleForHyperlink.setFont(font);\n\t}\n\n\t/**\n\t * 获取头部样式，获取后可以定义整体头部样式\n\t *\n\t * @return 头部样式\n\t */\n\tpublic CellStyle getHeadCellStyle() {\n\t\treturn this.headCellStyle;\n\t}\n\n\t/**\n\t * 获取常规单元格样式，获取后可以定义整体头部样式\n\t *\n\t * @return 常规单元格样式\n\t */\n\tpublic CellStyle getCellStyle() {\n\t\treturn this.cellStyle;\n\t}\n\n\t/**\n\t * 获取数字（带小数点）单元格样式，获取后可以定义整体数字样式\n\t *\n\t * @return 数字（带小数点）单元格样式\n\t */\n\tpublic CellStyle getCellStyleForNumber() {\n\t\treturn this.cellStyleForNumber;\n\t}\n\n\t/**\n\t * 获取日期单元格样式，获取后可以定义整体日期样式\n\t *\n\t * @return 日期单元格样式\n\t */\n\tpublic CellStyle getCellStyleForDate() {\n\t\treturn this.cellStyleForDate;\n\t}\n\n\t/**\n\t * 获取链接单元格样式，获取后可以定义整体链接样式\n\t *\n\t * @return 链接单元格样式\n\t * @since 5.7.13\n\t */\n\tpublic CellStyle getCellStyleForHyperlink() {\n\t\treturn this.cellStyleForHyperlink;\n\t}\n\n\t/**\n\t * 定义所有单元格的边框类型\n\t *\n\t * @param borderSize 边框粗细{@link BorderStyle}枚举\n\t * @param colorIndex 颜色的short值\n\t * @return this\n\t * @since 4.0.0\n\t */\n\tpublic StyleSet setBorder(BorderStyle borderSize, IndexedColors colorIndex) {\n\t\tStyleUtil.setBorder(this.headCellStyle, borderSize, colorIndex);\n\t\tStyleUtil.setBorder(this.cellStyle, borderSize, colorIndex);\n\t\tStyleUtil.setBorder(this.cellStyleForNumber, borderSize, colorIndex);\n\t\tStyleUtil.setBorder(this.cellStyleForDate, borderSize, colorIndex);\n\t\tStyleUtil.setBorder(this.cellStyleForHyperlink, borderSize, colorIndex);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置cell文本对齐样式\n\t *\n\t * @param halign 横向位置\n\t * @param valign 纵向位置\n\t * @return this\n\t * @since 4.0.0\n\t */\n\tpublic StyleSet setAlign(HorizontalAlignment halign, VerticalAlignment valign) {\n\t\tStyleUtil.setAlign(this.headCellStyle, halign, valign);\n\t\tStyleUtil.setAlign(this.cellStyle, halign, valign);\n\t\tStyleUtil.setAlign(this.cellStyleForNumber, halign, valign);\n\t\tStyleUtil.setAlign(this.cellStyleForDate, halign, valign);\n\t\tStyleUtil.setAlign(this.cellStyleForHyperlink, halign, valign);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置单元格背景样式\n\t *\n\t * @param backgroundColor 背景色\n\t * @param withHeadCell    是否也定义头部样式\n\t * @return this\n\t * @since 4.0.0\n\t */\n\tpublic StyleSet setBackgroundColor(IndexedColors backgroundColor, boolean withHeadCell) {\n\t\tif (withHeadCell) {\n\t\t\tStyleUtil.setColor(this.headCellStyle, backgroundColor, FillPatternType.SOLID_FOREGROUND);\n\t\t}\n\t\tStyleUtil.setColor(this.cellStyle, backgroundColor, FillPatternType.SOLID_FOREGROUND);\n\t\tStyleUtil.setColor(this.cellStyleForNumber, backgroundColor, FillPatternType.SOLID_FOREGROUND);\n\t\tStyleUtil.setColor(this.cellStyleForDate, backgroundColor, FillPatternType.SOLID_FOREGROUND);\n\t\tStyleUtil.setColor(this.cellStyleForHyperlink, backgroundColor, FillPatternType.SOLID_FOREGROUND);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置全局字体\n\t *\n\t * @param color      字体颜色\n\t * @param fontSize   字体大小，-1表示默认大小\n\t * @param fontName   字体名，null表示默认字体\n\t * @param ignoreHead 是否跳过头部样式\n\t * @return this\n\t */\n\tpublic StyleSet setFont(short color, short fontSize, String fontName, boolean ignoreHead) {\n\t\tfinal Font font = StyleUtil.createFont(this.workbook, color, fontSize, fontName);\n\t\treturn setFont(font, ignoreHead);\n\t}\n\n\t/**\n\t * 设置全局字体\n\t *\n\t * @param font       字体，可以通过{@link StyleUtil#createFont(Workbook, short, short, String)}创建\n\t * @param ignoreHead 是否跳过头部样式\n\t * @return this\n\t * @since 4.1.0\n\t */\n\tpublic StyleSet setFont(Font font, boolean ignoreHead) {\n\t\tif (false == ignoreHead) {\n\t\t\tthis.headCellStyle.setFont(font);\n\t\t}\n\t\tthis.cellStyle.setFont(font);\n\t\tthis.cellStyleForNumber.setFont(font);\n\t\tthis.cellStyleForDate.setFont(font);\n\t\tthis.cellStyleForHyperlink.setFont(font);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置单元格文本自动换行\n\t *\n\t * @return this\n\t * @since 4.5.16\n\t */\n\tpublic StyleSet setWrapText() {\n\t\tthis.cellStyle.setWrapText(true);\n\t\tthis.cellStyleForNumber.setWrapText(true);\n\t\tthis.cellStyleForDate.setWrapText(true);\n\t\tthis.cellStyleForHyperlink.setWrapText(true);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取值对应的公共单元格样式\n\t *\n\t * @param value    值\n\t * @param isHeader 是否为标题单元格\n\t * @return 值对应单元格样式\n\t * @since 5.7.16\n\t */\n\tpublic CellStyle getStyleByValueType(Object value, boolean isHeader) {\n\t\tCellStyle style = null;\n\n\t\tif (isHeader && null != this.headCellStyle) {\n\t\t\tstyle = headCellStyle;\n\t\t} else if (null != cellStyle) {\n\t\t\tstyle = cellStyle;\n\t\t}\n\n\t\tif (value instanceof Date\n\t\t\t|| value instanceof TemporalAccessor\n\t\t\t|| value instanceof Calendar) {\n\t\t\t// 日期单独定义格式\n\t\t\tif (null != this.cellStyleForDate) {\n\t\t\t\tstyle = this.cellStyleForDate;\n\t\t\t}\n\t\t} else if (value instanceof Number) {\n\t\t\t// 数字单独定义格式\n\t\t\tif ((value instanceof Double || value instanceof Float || value instanceof BigDecimal) &&\n\t\t\t\tnull != this.cellStyleForNumber) {\n\t\t\t\tstyle = this.cellStyleForNumber;\n\t\t\t}\n\t\t} else if (value instanceof Hyperlink) {\n\t\t\t// 自定义超链接样式\n\t\t\tif (null != this.cellStyleForHyperlink) {\n\t\t\t\tstyle = this.cellStyleForHyperlink;\n\t\t\t}\n\t\t}\n\n\t\treturn style;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/WorkbookUtil.java",
    "content": "\npackage cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.apache.poi.ss.usermodel.WorkbookFactory;\nimport org.apache.poi.xssf.streaming.SXSSFWorkbook;\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * Excel工作簿{@link Workbook}相关工具类\n *\n * @author looly\n * @since 4.0.7\n */\npublic class WorkbookUtil {\n\n\t/**\n\t * 创建或加载工作簿（读写模式）\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @return {@link Workbook}\n\t * @since 3.1.1\n\t */\n\tpublic static Workbook createBook(String excelFilePath) {\n\t\treturn createBook(excelFilePath, false);\n\t}\n\n\t/**\n\t * 创建或加载工作簿\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param readOnly      是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link Workbook}\n\t * @since 3.1.1\n\t */\n\tpublic static Workbook createBook(String excelFilePath, boolean readOnly) {\n\t\treturn createBook(FileUtil.file(excelFilePath), null, readOnly);\n\t}\n\n\t/**\n\t * 创建或加载工作簿（读写模式）\n\t *\n\t * @param excelFile Excel文件\n\t * @return {@link Workbook}\n\t */\n\tpublic static Workbook createBook(File excelFile) {\n\t\treturn createBook(excelFile, false);\n\t}\n\n\n\t/**\n\t * 创建或加载工作簿\n\t *\n\t * @param excelFile Excel文件\n\t * @param readOnly  是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link Workbook}\n\t */\n\tpublic static Workbook createBook(File excelFile, boolean readOnly) {\n\t\treturn createBook(excelFile, null, readOnly);\n\t}\n\n\t/**\n\t * 创建工作簿，用于Excel写出（读写模式）\n\t *\n\t * <pre>\n\t * 1. excelFile为null时直接返回一个空的工作簿，默认xlsx格式\n\t * 2. 文件已存在则通过流的方式读取到这个工作簿\n\t * 3. 文件不存在则检查传入文件路径是否以xlsx为扩展名，是则创建xlsx工作簿，否则创建xls工作簿\n\t * </pre>\n\t *\n\t * @param excelFile Excel文件\n\t * @return {@link Workbook}\n\t * @since 4.5.18\n\t */\n\tpublic static Workbook createBookForWriter(File excelFile) {\n\t\tif (null == excelFile) {\n\t\t\treturn createBook(true);\n\t\t}\n\n\t\tif (excelFile.exists()) {\n\t\t\treturn createBook(FileUtil.getInputStream(excelFile));\n\t\t}\n\n\t\treturn createBook(StrUtil.endWithIgnoreCase(excelFile.getName(), \".xlsx\"));\n\t}\n\n\t/**\n\t * 创建或加载工作簿（读写模式）\n\t *\n\t * @param excelFile Excel文件\n\t * @param password  Excel工作簿密码，如果无密码传{@code null}\n\t * @return {@link Workbook}\n\t */\n\tpublic static Workbook createBook(File excelFile, String password) {\n\t\treturn createBook(excelFile, password, false);\n\t}\n\n\t/**\n\t * 创建或加载工作簿\n\t *\n\t * @param excelFile Excel文件\n\t * @param password  Excel工作簿密码，如果无密码传{@code null}\n\t * @param readOnly  是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link Workbook}\n\t * @since 5.7.23\n\t */\n\tpublic static Workbook createBook(File excelFile, String password, boolean readOnly) {\n\t\ttry {\n\t\t\treturn WorkbookFactory.create(excelFile, password, readOnly);\n\t\t} catch (Exception e) {\n\t\t\tthrow new POIException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建或加载工作簿（只读模式）\n\t *\n\t * @param in Excel输入流\n\t * @return {@link Workbook}\n\t */\n\tpublic static Workbook createBook(InputStream in) {\n\t\treturn createBook(in, null);\n\t}\n\n\t/**\n\t * 创建或加载工作簿（只读模式）\n\t *\n\t * @param in       Excel输入流，使用完毕自动关闭流\n\t * @param password 密码\n\t * @return {@link Workbook}\n\t * @since 4.0.3\n\t */\n\tpublic static Workbook createBook(InputStream in, String password) {\n\t\ttry {\n\t\t\treturn WorkbookFactory.create(IoUtil.toMarkSupportStream(in), password);\n\t\t} catch (Exception e) {\n\t\t\tthrow new POIException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t/**\n\t * 创建新的空白Excel工作簿\n\t *\n\t * @param isXlsx 是否为xlsx格式的Excel\n\t * @return {@link Workbook}\n\t * @since 4.1.0\n\t */\n\tpublic static Workbook createBook(boolean isXlsx) {\n\t\ttry {\n\t\t\treturn WorkbookFactory.create(isXlsx);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 创建或加载SXSSFWorkbook工作簿（读写模式）\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(String excelFilePath) {\n\t\treturn createSXSSFBook(excelFilePath, false);\n\t}\n\n\t/**\n\t * 创建或加载SXSSFWorkbook工作簿\n\t *\n\t * @param excelFilePath Excel文件路径，绝对路径或相对于ClassPath路径\n\t * @param readOnly      是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link SXSSFWorkbook}\n\t * @since 5.7.23\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(String excelFilePath, boolean readOnly) {\n\t\treturn createSXSSFBook(FileUtil.file(excelFilePath), null, readOnly);\n\t}\n\n\t/**\n\t * 创建或加载SXSSFWorkbook工作簿（读写模式）\n\t *\n\t * @param excelFile Excel文件\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(File excelFile) {\n\t\treturn createSXSSFBook(excelFile, false);\n\t}\n\n\t/**\n\t * 创建或加载SXSSFWorkbook工作簿\n\t *\n\t * @param excelFile Excel文件\n\t * @param readOnly  是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link SXSSFWorkbook}\n\t * @since 5.7.23\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(File excelFile, boolean readOnly) {\n\t\treturn createSXSSFBook(excelFile, null, readOnly);\n\t}\n\n\n\t/**\n\t * 创建或加载SXSSFWorkbook工作簿（读写模式）\n\t *\n\t * @param excelFile Excel文件\n\t * @param password  Excel工作簿密码，如果无密码传{@code null}\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(File excelFile, String password) {\n\t\treturn createSXSSFBook(excelFile, password, false);\n\t}\n\n\n\t/**\n\t * 创建或加载{@link SXSSFWorkbook}工作簿\n\t *\n\t * @param excelFile Excel文件\n\t * @param password  Excel工作簿密码，如果无密码传{@code null}\n\t * @param readOnly  是否只读模式打开，true:是（不可编辑），false:否（可编辑）\n\t * @return {@link SXSSFWorkbook}\n\t * @since 5.7.23\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(File excelFile, String password, boolean readOnly) {\n\t\treturn toSXSSFBook(createBook(excelFile, password, readOnly));\n\t}\n\n\t/**\n\t * 创建或加载{@link SXSSFWorkbook}工作簿（只读模式）\n\t *\n\t * @param in Excel输入流\n\t * @return {@link SXSSFWorkbook}\n\t * @since 5.7.1\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(InputStream in) {\n\t\treturn createSXSSFBook(in, null);\n\t}\n\n\t/**\n\t * 创建或加载{@link SXSSFWorkbook}工作簿（只读模式）\n\t *\n\t * @param in       Excel输入流\n\t * @param password 密码\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(InputStream in, String password) {\n\t\treturn toSXSSFBook(createBook(in, password));\n\t}\n\n\t/**\n\t * 创建空的{@link SXSSFWorkbook}，用于大批量数据写出\n\t *\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook() {\n\t\treturn new SXSSFWorkbook();\n\t}\n\n\t/**\n\t * 创建空的{@link SXSSFWorkbook}，用于大批量数据写出\n\t *\n\t * @param rowAccessWindowSize 在内存中的行数，-1表示不限制，此时需要手动刷出\n\t * @return {@link SXSSFWorkbook}\n\t * @since 4.1.13\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(int rowAccessWindowSize) {\n\t\treturn new SXSSFWorkbook(rowAccessWindowSize);\n\t}\n\n\t/**\n\t * 创建空的{@link SXSSFWorkbook}，用于大批量数据写出\n\t *\n\t * @param rowAccessWindowSize 在内存中的行数，-1表示不限制，此时需要手动刷出\n\t * @param compressTmpFiles     是否使用Gzip压缩临时文件\n\t * @param useSharedStringsTable 是否使用共享字符串表，一般大量重复字符串时开启可节省内存\n\t * @return {@link SXSSFWorkbook}\n\t * @since 5.7.23\n\t */\n\tpublic static SXSSFWorkbook createSXSSFBook(int rowAccessWindowSize, boolean compressTmpFiles, boolean useSharedStringsTable) {\n\t\treturn new SXSSFWorkbook(null, rowAccessWindowSize, compressTmpFiles, useSharedStringsTable);\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到输出流，不关闭流\n\t *\n\t * @param book {@link Workbook}\n\t * @param out  输出流\n\t * @throws IORuntimeException IO异常\n\t * @since 3.2.0\n\t */\n\tpublic static void writeBook(Workbook book, OutputStream out) throws IORuntimeException {\n\t\ttry {\n\t\t\tbook.write(out);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 获取或者创建sheet表<br>\n\t * 如果sheet表在Workbook中已经存在，则获取之，否则创建之\n\t *\n\t * @param book      工作簿{@link Workbook}\n\t * @param sheetName 工作表名\n\t * @return 工作表{@link Sheet}\n\t * @since 4.0.2\n\t */\n\tpublic static Sheet getOrCreateSheet(Workbook book, String sheetName) {\n\t\tif (null == book) {\n\t\t\treturn null;\n\t\t}\n\t\tsheetName = StrUtil.isBlank(sheetName) ? \"sheet1\" : sheetName;\n\t\tSheet sheet = book.getSheet(sheetName);\n\t\tif (null == sheet) {\n\t\t\tsheet = book.createSheet(sheetName);\n\t\t}\n\t\treturn sheet;\n\t}\n\n\t/**\n\t * 获取或者创建sheet表<br>\n\t * 自定义需要读取或写出的Sheet，如果给定的sheet不存在，创建之（命名为默认）<br>\n\t * 在读取中，此方法用于切换读取的sheet，在写出时，此方法用于新建或者切换sheet\n\t *\n\t * @param book       工作簿{@link Workbook}\n\t * @param sheetIndex 工作表序号\n\t * @return 工作表{@link Sheet}\n\t * @since 5.2.1\n\t */\n\tpublic static Sheet getOrCreateSheet(Workbook book, int sheetIndex) {\n\t\tSheet sheet = null;\n\t\ttry {\n\t\t\tsheet = book.getSheetAt(sheetIndex);\n\t\t} catch (IllegalArgumentException ignore) {\n\t\t\t//ignore\n\t\t}\n\t\tif (null == sheet) {\n\t\t\tsheet = book.createSheet();\n\t\t}\n\t\treturn sheet;\n\t}\n\n\t/**\n\t * sheet是否为空\n\t *\n\t * @param sheet {@link Sheet}\n\t * @return sheet是否为空\n\t * @since 4.0.1\n\t */\n\tpublic static boolean isEmpty(Sheet sheet) {\n\t\treturn null == sheet || (sheet.getLastRowNum() == 0 && sheet.getPhysicalNumberOfRows() == 0);\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 将普通工作簿转换为SXSSFWorkbook\n\t *\n\t * @param book 工作簿\n\t * @return SXSSFWorkbook\n\t * @since 4.1.13\n\t */\n\tprivate static SXSSFWorkbook toSXSSFBook(Workbook book) {\n\t\tif (book instanceof SXSSFWorkbook) {\n\t\t\treturn (SXSSFWorkbook) book;\n\t\t}\n\t\tif (book instanceof XSSFWorkbook) {\n\t\t\treturn new SXSSFWorkbook((XSSFWorkbook) book);\n\t\t}\n\t\tthrow new POIException(\"The input is not a [xlsx] format.\");\n\t}\n\t// -------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellEditor.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * 单元格编辑器接口<br>\n * 在读取Excel值时，有时我们需要针对所有单元格统一处理结果值（如null转默认值）的情况，实现接口并调用<br>\n * reader.setCellEditor()设置编辑器\n *\n * @author Looly\n */\n@FunctionalInterface\npublic interface CellEditor {\n\n\t/**\n\t * 编辑，根据单元格信息处理结果值，返回处理后的结果\n\t *\n\t * @param cell  单元格对象，可以获取单元格行、列样式等信息\n\t * @param value 单元格值\n\t * @return 编辑后的对象\n\t */\n\tObject edit(Cell cell, Object value);\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellHandler.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * 单元格处理器接口<br>\n * 用于在读取Excel单元格值时自定义结果值的获取，如在获取值的同时，获取单元格样式、坐标等信息，或根据单元格信息，装饰转换结果值\n *\n * @author Looly\n */\n@FunctionalInterface\npublic interface CellHandler {\n\n\t/**\n\t * 处理\n\t *\n\t * @param cell  单元格对象，可以获取单元格行、列样式等信息\n\t * @param value 单元格值\n\t */\n\tvoid handle(Cell cell, Object value);\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellLocation.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport java.io.Serializable;\nimport java.util.Objects;\n\n/**\n * 单元格位置\n *\n * @author Looly\n * @since 5.1.4\n */\npublic class CellLocation implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate int x;\n\tprivate int y;\n\n\t/**\n\t * 构造\n\t *\n\t * @param x 列号，从0开始\n\t * @param y 行号，从0开始\n\t */\n\tpublic CellLocation(int x, int y) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t}\n\n\tpublic int getX() {\n\t\treturn x;\n\t}\n\n\tpublic void setX(int x) {\n\t\tthis.x = x;\n\t}\n\n\tpublic int getY() {\n\t\treturn y;\n\t}\n\n\tpublic void setY(int y) {\n\t\tthis.y = y;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object o) {\n\t\tif (this == o) {\n\t\t\treturn true;\n\t\t}\n\t\tif (o == null || getClass() != o.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tfinal CellLocation that = (CellLocation) o;\n\t\treturn x == that.x && y == that.y;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn Objects.hash(x, y);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CellLocation{\" +\n\t\t\t\t\"x=\" + x +\n\t\t\t\t\", y=\" + y +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellSetter.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * 单元格值自定义设置器，主要用于Excel数据导出，用户通过自定义此接口，实现可定制化的单元格值设定\n *\n * @author looly\n * @since 5.7.8\n */\n@FunctionalInterface\npublic interface CellSetter {\n\n\t/**\n\t * 自定义单元格值设置，同时可以设置单元格样式、格式等信息\n\t * @param cell 单元格\n\t */\n\tvoid setValue(Cell cell);\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellUtil.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.ExcelUtil;\nimport cn.hutool.poi.excel.StyleSet;\nimport cn.hutool.poi.excel.cell.setters.CellSetterFactory;\nimport cn.hutool.poi.excel.cell.values.ErrorCellValue;\nimport cn.hutool.poi.excel.cell.values.NumericCellValue;\nimport cn.hutool.poi.excel.editors.TrimEditor;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.CellType;\nimport org.apache.poi.ss.usermodel.ClientAnchor;\nimport org.apache.poi.ss.usermodel.Comment;\nimport org.apache.poi.ss.usermodel.CreationHelper;\nimport org.apache.poi.ss.usermodel.Drawing;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.apache.poi.ss.util.CellRangeAddress;\nimport org.apache.poi.ss.util.RegionUtil;\nimport org.apache.poi.ss.util.SheetUtil;\n\n/**\n * Excel表格中单元格工具类\n *\n * @author looly\n * @since 4.0.7\n */\npublic class CellUtil {\n\n\t/**\n\t * 获取单元格值\n\t *\n\t * @param cell {@link Cell}单元格\n\t * @return 值，类型可能为：Date、Double、Boolean、String\n\t * @since 4.6.3\n\t */\n\tpublic static Object getCellValue(Cell cell) {\n\t\treturn getCellValue(cell, false);\n\t}\n\n\t/**\n\t * 获取单元格值\n\t *\n\t * @param cell            {@link Cell}单元格\n\t * @param isTrimCellValue 如果单元格类型为字符串，是否去掉两边空白符\n\t * @return 值，类型可能为：Date、Double、Boolean、String\n\t */\n\tpublic static Object getCellValue(Cell cell, boolean isTrimCellValue) {\n\t\tif (null == cell) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getCellValue(cell, cell.getCellType(), isTrimCellValue);\n\t}\n\n\t/**\n\t * 获取单元格值\n\t *\n\t * @param cell       {@link Cell}单元格\n\t * @param cellEditor 单元格值编辑器。可以通过此编辑器对单元格值做自定义操作\n\t * @return 值，类型可能为：Date、Double、Boolean、String\n\t */\n\tpublic static Object getCellValue(Cell cell, CellEditor cellEditor) {\n\t\treturn getCellValue(cell, null, cellEditor);\n\t}\n\n\t/**\n\t * 获取单元格值\n\t *\n\t * @param cell            {@link Cell}单元格\n\t * @param cellType        单元格值类型{@link CellType}枚举\n\t * @param isTrimCellValue 如果单元格类型为字符串，是否去掉两边空白符\n\t * @return 值，类型可能为：Date、Double、Boolean、String\n\t */\n\tpublic static Object getCellValue(Cell cell, CellType cellType, final boolean isTrimCellValue) {\n\t\treturn getCellValue(cell, cellType, isTrimCellValue ? new TrimEditor() : null);\n\t}\n\n\t/**\n\t * 获取单元格值<br>\n\t * 如果单元格值为数字格式，则判断其格式中是否有小数部分，无则返回Long类型，否则返回Double类型\n\t *\n\t * @param cell       {@link Cell}单元格\n\t * @param cellType   单元格值类型{@link CellType}枚举，如果为{@code null}默认使用cell的类型\n\t * @param cellEditor 单元格值编辑器。可以通过此编辑器对单元格值做自定义操作\n\t * @return 值，类型可能为：Date、Double、Boolean、String\n\t */\n\tpublic static Object getCellValue(Cell cell, CellType cellType, CellEditor cellEditor) {\n\t\tif (null == cell) {\n\t\t\treturn null;\n\t\t}\n\t\tif (cell instanceof NullCell) {\n\t\t\treturn null == cellEditor ? null : cellEditor.edit(cell, null);\n\t\t}\n\t\tif (null == cellType) {\n\t\t\tcellType = cell.getCellType();\n\t\t}\n\n\t\t// 尝试获取合并单元格，如果是合并单元格，则重新获取单元格类型\n\t\tfinal Cell mergedCell = getMergedRegionCell(cell);\n\t\tif (mergedCell != cell) {\n\t\t\tcell = mergedCell;\n\t\t\tcellType = cell.getCellType();\n\t\t}\n\n\t\tfinal Object value;\n\t\tswitch (cellType) {\n\t\t\tcase NUMERIC:\n\t\t\t\tvalue = new NumericCellValue(cell).getValue();\n\t\t\t\tbreak;\n\t\t\tcase BOOLEAN:\n\t\t\t\tvalue = cell.getBooleanCellValue();\n\t\t\t\tbreak;\n\t\t\tcase FORMULA:\n\t\t\t\tvalue = getCellValue(cell, cell.getCachedFormulaResultType(), cellEditor);\n\t\t\t\tbreak;\n\t\t\tcase BLANK:\n\t\t\t\tvalue = StrUtil.EMPTY;\n\t\t\t\tbreak;\n\t\t\tcase ERROR:\n\t\t\t\tvalue = new ErrorCellValue(cell).getValue();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tvalue = cell.getStringCellValue();\n\t\t}\n\n\t\treturn null == cellEditor ? value : cellEditor.edit(cell, value);\n\t}\n\n\t/**\n\t * 设置单元格值<br>\n\t * 根据传入的styleSet自动匹配样式<br>\n\t * 当为头部样式时默认赋值头部样式，但是头部中如果有数字、日期等类型，将按照数字、日期样式设置\n\t *\n\t * @param cell     单元格\n\t * @param value    值\n\t * @param styleSet 单元格样式集，包括日期等样式，null表示无样式\n\t * @param isHeader 是否为标题单元格\n\t */\n\tpublic static void setCellValue(Cell cell, Object value, StyleSet styleSet, boolean isHeader) {\n\t\tif (null == cell) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (null != styleSet) {\n\t\t\tcell.setCellStyle(styleSet.getStyleByValueType(value, isHeader));\n\t\t}\n\n\t\tsetCellValue(cell, value);\n\t}\n\n\t/**\n\t * 设置单元格值<br>\n\t * 根据传入的styleSet自动匹配样式<br>\n\t * 当为头部样式时默认赋值头部样式，但是头部中如果有数字、日期等类型，将按照数字、日期样式设置\n\t *\n\t * @param cell  单元格\n\t * @param value 值\n\t * @param style 自定义样式，null表示无样式\n\t */\n\tpublic static void setCellValue(Cell cell, Object value, CellStyle style) {\n\t\tsetCellValue(cell, (CellSetter) cell1 -> {\n\t\t\tsetCellValue(cell, value);\n\t\t\tif (null != style) {\n\t\t\t\tcell1.setCellStyle(style);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 设置单元格值<br>\n\t * 根据传入的styleSet自动匹配样式<br>\n\t * 当为头部样式时默认赋值头部样式，但是头部中如果有数字、日期等类型，将按照数字、日期样式设置\n\t *\n\t * @param cell  单元格\n\t * @param value 值或{@link CellSetter}\n\t * @since 5.6.4\n\t */\n\tpublic static void setCellValue(Cell cell, Object value) {\n\t\tif (null == cell) {\n\t\t\treturn;\n\t\t}\n\n\t\t// issue#1659@Github\n\t\t// 在使用BigWriter(SXSSF)模式写出数据时，单元格值为直接值，非引用值（is标签）\n\t\t// 而再使用ExcelWriter(XSSF)编辑时，会写出引用值，导致失效。\n\t\t// 此处做法是先清空单元格值，再写入\n\t\tif(CellType.BLANK != cell.getCellType()){\n\t\t\tcell.setBlank();\n\t\t}\n\n\t\tCellSetterFactory.createCellSetter(value).setValue(cell);\n\t}\n\n\t/**\n\t * 获取单元格，如果单元格不存在，返回{@link NullCell}\n\t *\n\t * @param row       Excel表的行\n\t * @param cellIndex 列号\n\t * @return {@link Cell}\n\t * @since 5.5.0\n\t */\n\tpublic static Cell getCell(Row row, int cellIndex) {\n\t\tif (null == row) {\n\t\t\treturn null;\n\t\t}\n\t\tfinal Cell cell = row.getCell(cellIndex);\n\t\tif (null == cell) {\n\t\t\treturn new NullCell(row, cellIndex);\n\t\t}\n\t\treturn cell;\n\t}\n\n\t/**\n\t * 获取已有单元格或创建新单元格\n\t *\n\t * @param row       Excel表的行\n\t * @param cellIndex 列号\n\t * @return {@link Cell}\n\t * @since 4.0.2\n\t */\n\tpublic static Cell getOrCreateCell(Row row, int cellIndex) {\n\t\tif (null == row) {\n\t\t\treturn null;\n\t\t}\n\t\tCell cell = row.getCell(cellIndex);\n\t\tif (null == cell) {\n\t\t\tcell = row.createCell(cellIndex);\n\t\t}\n\t\treturn cell;\n\t}\n\n\t/**\n\t * 判断指定的单元格是否是合并单元格\n\t *\n\t * @param sheet       {@link Sheet}\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return 是否是合并单元格\n\t * @since 5.1.5\n\t */\n\tpublic static boolean isMergedRegion(Sheet sheet, String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn isMergedRegion(sheet, cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 判断指定的单元格是否是合并单元格\n\t *\n\t * @param cell {@link Cell}\n\t * @return 是否是合并单元格\n\t * @since 5.1.5\n\t */\n\tpublic static boolean isMergedRegion(Cell cell) {\n\t\treturn isMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex());\n\t}\n\n\t/**\n\t * 判断指定的单元格是否是合并单元格\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param x     列号，从0开始\n\t * @param y     行号，从0开始\n\t * @return 是否是合并单元格，如果提供的sheet为{@code null}，返回{@code false}\n\t */\n\tpublic static boolean isMergedRegion(Sheet sheet, int x, int y) {\n\t\tif (sheet != null) {\n\t\t\tfinal int sheetMergeCount = sheet.getNumMergedRegions();\n\t\t\tCellRangeAddress ca;\n\t\t\tfor (int i = 0; i < sheetMergeCount; i++) {\n\t\t\t\tca = sheet.getMergedRegion(i);\n\t\t\t\tif (y >= ca.getFirstRow() && y <= ca.getLastRow()\n\t\t\t\t\t\t&& x >= ca.getFirstColumn() && x <= ca.getLastColumn()) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取合并单元格{@link CellRangeAddress}，如果不是返回null\n\t *\n\t * @param sheet       {@link Sheet}\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return {@link CellRangeAddress}\n\t * @since 5.8.0\n\t */\n\tpublic static CellRangeAddress getCellRangeAddress(Sheet sheet, String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getCellRangeAddress(sheet, cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 获取合并单元格{@link CellRangeAddress}，如果不是返回null\n\t *\n\t * @param cell {@link Cell}\n\t * @return {@link CellRangeAddress}\n\t * @since 5.8.0\n\t */\n\tpublic static CellRangeAddress getCellRangeAddress(Cell cell) {\n\t\treturn getCellRangeAddress(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex());\n\t}\n\n\t/**\n\t * 获取合并单元格{@link CellRangeAddress}，如果不是返回null\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param x     列号，从0开始\n\t * @param y     行号，从0开始\n\t * @return {@link CellRangeAddress}\n\t * @since 5.8.0\n\t */\n\tpublic static CellRangeAddress getCellRangeAddress(Sheet sheet, int x, int y) {\n\t\tif (sheet != null) {\n\t\t\tfinal int sheetMergeCount = sheet.getNumMergedRegions();\n\t\t\tCellRangeAddress ca;\n\t\t\tfor (int i = 0; i < sheetMergeCount; i++) {\n\t\t\t\tca = sheet.getMergedRegion(i);\n\t\t\t\tif (y >= ca.getFirstRow() && y <= ca.getLastRow()\n\t\t\t\t\t\t&& x >= ca.getFirstColumn() && x <= ca.getLastColumn()) {\n\t\t\t\t\treturn ca;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 设置合并单元格样式，如果不是则不设置\n\t *\n\t * @param cell      {@link Cell}\n\t * @param cellStyle {@link CellStyle}\n\t */\n\tpublic static void setMergedRegionStyle(Cell cell, CellStyle cellStyle) {\n\t\tfinal CellRangeAddress cellRangeAddress = getCellRangeAddress(cell);\n\t\tif (cellRangeAddress != null) {\n\t\t\tsetMergeCellStyle(cellStyle, cellRangeAddress, cell.getSheet());\n\t\t}\n\t}\n\n\t/**\n\t * 合并单元格，可以根据设置的值来合并行和列\n\t *\n\t * @param sheet       表对象\n\t * @param firstRow    起始行，0开始\n\t * @param lastRow     结束行，0开始\n\t * @param firstColumn 起始列，0开始\n\t * @param lastColumn  结束列，0开始\n\t * @return 合并后的单元格号\n\t */\n\tpublic static int mergingCells(Sheet sheet, int firstRow, int lastRow, int firstColumn, int lastColumn) {\n\t\treturn mergingCells(sheet, firstRow, lastRow, firstColumn, lastColumn, null);\n\t}\n\n\t/**\n\t * 合并单元格，可以根据设置的值来合并行和列\n\t *\n\t * @param sheet       表对象\n\t * @param firstRow    起始行，0开始\n\t * @param lastRow     结束行，0开始\n\t * @param firstColumn 起始列，0开始\n\t * @param lastColumn  结束列，0开始\n\t * @param cellStyle   单元格样式，只提取边框样式，null表示无样式\n\t * @return 合并后的单元格号\n\t */\n\tpublic static int mergingCells(Sheet sheet, int firstRow, int lastRow, int firstColumn, int lastColumn, CellStyle cellStyle) {\n\t\tfinal CellRangeAddress cellRangeAddress = new CellRangeAddress(//\n\t\t\t\tfirstRow, // first row (0-based)\n\t\t\t\tlastRow, // last row (0-based)\n\t\t\t\tfirstColumn, // first column (0-based)\n\t\t\t\tlastColumn // last column (0-based)\n\t\t);\n\n\t\tsetMergeCellStyle(cellStyle, cellRangeAddress, sheet);\n\t\treturn sheet.addMergedRegion(cellRangeAddress);\n\t}\n\n\t/**\n\t * 获取合并单元格的值<br>\n\t * 传入的x,y坐标（列行数）可以是合并单元格范围内的任意一个单元格\n\t *\n\t * @param sheet       {@link Sheet}\n\t * @param locationRef 单元格地址标识符，例如A11，B5\n\t * @return 合并单元格的值\n\t * @since 5.1.5\n\t */\n\tpublic static Object getMergedRegionValue(Sheet sheet, String locationRef) {\n\t\tfinal CellLocation cellLocation = ExcelUtil.toLocation(locationRef);\n\t\treturn getMergedRegionValue(sheet, cellLocation.getX(), cellLocation.getY());\n\t}\n\n\t/**\n\t * 获取合并单元格的值<br>\n\t * 传入的x,y坐标（列行数）可以是合并单元格范围内的任意一个单元格\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param x     列号，从0开始，可以是合并单元格范围中的任意一列\n\t * @param y     行号，从0开始，可以是合并单元格范围中的任意一行\n\t * @return 合并单元格的值\n\t * @since 4.6.3\n\t */\n\tpublic static Object getMergedRegionValue(Sheet sheet, int x, int y) {\n\t\t// 合并单元格的识别在getCellValue已经集成，无需重复获取合并单元格\n\t\treturn getCellValue(SheetUtil.getCell(sheet, x, y));\n\t}\n\n\t/**\n\t * 获取合并单元格<br>\n\t * 传入的x,y坐标（列行数）可以是合并单元格范围内的任意一个单元格\n\t *\n\t * @param cell {@link Cell}\n\t * @return 合并单元格\n\t * @since 5.1.5\n\t */\n\tpublic static Cell getMergedRegionCell(Cell cell) {\n\t\tif (null == cell) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ObjectUtil.defaultIfNull(\n\t\t\t\tgetCellIfMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex()),\n\t\t\t\tcell);\n\t}\n\n\t/**\n\t * 获取合并单元格<br>\n\t * 传入的x,y坐标（列行数）可以是合并单元格范围内的任意一个单元格\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param x     列号，从0开始，可以是合并单元格范围中的任意一列\n\t * @param y     行号，从0开始，可以是合并单元格范围中的任意一行\n\t * @return 合并单元格，如果非合并单元格，返回坐标对应的单元格\n\t * @since 5.1.5\n\t */\n\tpublic static Cell getMergedRegionCell(Sheet sheet, int x, int y) {\n\t\treturn ObjectUtil.defaultIfNull(\n\t\t\t\tgetCellIfMergedRegion(sheet, x, y),\n\t\t\t\t() -> SheetUtil.getCell(sheet, y, x));\n\t}\n\n\t/**\n\t * 为特定单元格添加批注\n\t *\n\t * @param cell          单元格\n\t * @param commentText   批注内容\n\t * @param commentAuthor 作者\n\t * @param anchor        批注的位置、大小等信息，null表示使用默认\n\t * @since 5.4.8\n\t */\n\tpublic static void setComment(Cell cell, String commentText, String commentAuthor, ClientAnchor anchor) {\n\t\tfinal Sheet sheet = cell.getSheet();\n\t\tfinal Workbook wb = sheet.getWorkbook();\n\t\tfinal Drawing<?> drawing = sheet.createDrawingPatriarch();\n\t\tfinal CreationHelper factory = wb.getCreationHelper();\n\t\tif (anchor == null) {\n\t\t\tanchor = factory.createClientAnchor();\n\t\t\t// 默认位置，在注释的单元格的右方\n\t\t\tanchor.setCol1(cell.getColumnIndex() + 1);\n\t\t\tanchor.setCol2(cell.getColumnIndex() + 3);\n\t\t\tanchor.setRow1(cell.getRowIndex());\n\t\t\tanchor.setRow2(cell.getRowIndex() + 2);\n\t\t\t// 自适应\n\t\t\tanchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);\n\t\t}\n\t\tfinal Comment comment = drawing.createCellComment(anchor);\n\t\t// https://stackoverflow.com/questions/28169011/using-sxssfapache-poi-and-adding-comment-does-not-generate-proper-excel-file\n\t\t// 修正在XSSFCell中未设置地址导致错位问题\n\t\tcomment.setAddress(cell.getAddress());\n\t\tcomment.setString(factory.createRichTextString(commentText));\n\t\tcomment.setAuthor(StrUtil.nullToEmpty(commentAuthor));\n\t\tcell.setCellComment(comment);\n\t}\n\n\t// -------------------------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 获取合并单元格，非合并单元格返回{@code null}<br>\n\t * 传入的x,y坐标（列行数）可以是合并单元格范围内的任意一个单元格\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param x     列号，从0开始，可以是合并单元格范围中的任意一列\n\t * @param y     行号，从0开始，可以是合并单元格范围中的任意一行\n\t * @return 合并单元格，如果非合并单元格，返回{@code null}\n\t * @since 5.4.5\n\t */\n\tprivate static Cell getCellIfMergedRegion(Sheet sheet, int x, int y) {\n\t\tfor (final CellRangeAddress ca : sheet.getMergedRegions()) {\n\t\t\tif (ca.isInRange(y, x)) {\n\t\t\t\treturn SheetUtil.getCell(sheet, ca.getFirstRow(), ca.getFirstColumn());\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据{@link CellStyle}设置合并单元格边框样式\n\t *\n\t * @param cellStyle        {@link CellStyle}\n\t * @param cellRangeAddress {@link CellRangeAddress}\n\t * @param sheet            {@link Sheet}\n\t */\n\tprivate static void setMergeCellStyle(CellStyle cellStyle, CellRangeAddress cellRangeAddress, Sheet sheet) {\n\t\tif (null != cellStyle) {\n\t\t\tRegionUtil.setBorderTop(cellStyle.getBorderTop(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setBorderRight(cellStyle.getBorderRight(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setBorderBottom(cellStyle.getBorderBottom(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setBorderLeft(cellStyle.getBorderLeft(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setTopBorderColor(cellStyle.getTopBorderColor(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setRightBorderColor(cellStyle.getRightBorderColor(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setLeftBorderColor(cellStyle.getLeftBorderColor(), cellRangeAddress, sheet);\n\t\t\tRegionUtil.setBottomBorderColor(cellStyle.getBottomBorderColor(), cellRangeAddress, sheet);\n\t\t}\n\t}\n\t// -------------------------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellValue.java",
    "content": "package cn.hutool.poi.excel.cell;\n\n/**\n * 抽象的单元格值接口，用于判断不同类型的单元格值\n *\n * @param <T> 值得类型\n * @author looly\n * @since 4.0.11\n */\npublic interface CellValue<T> {\n\n\t/**\n\t * 获取单元格值\n\t *\n\t * @return 值\n\t */\n\tT getValue();\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/FormulaCellValue.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * 公式类型的值<br>\n *\n * <ul>\n *     <li>在Sax读取模式时，此对象用于接收单元格的公式以及公式结果值信息</li>\n *     <li>在写出模式时，用于定义写出的单元格类型为公式</li>\n * </ul>\n *\n * @author looly\n * @since 4.0.11\n */\npublic class FormulaCellValue implements CellValue<String>, CellSetter {\n\n\t/**\n\t * 公式\n\t */\n\tprivate final String formula;\n\t/**\n\t * 结果，使用ExcelWriter时可以不用\n\t */\n\tprivate final Object result;\n\n\t/**\n\t * 构造\n\t *\n\t * @param formula 公式\n\t */\n\tpublic FormulaCellValue(String formula) {\n\t\tthis(formula, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param formula 公式\n\t * @param result  结果\n\t */\n\tpublic FormulaCellValue(String formula, Object result) {\n\t\tthis.formula = formula;\n\t\tthis.result = result;\n\t}\n\n\t@Override\n\tpublic String getValue() {\n\t\treturn this.formula;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellFormula(this.formula);\n\t}\n\n\t/**\n\t * 获取结果\n\t * @return 结果\n\t */\n\tpublic Object getResult() {\n\t\treturn this.result;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn getResult().toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/NullCell.java",
    "content": "package cn.hutool.poi.excel.cell;\n\nimport org.apache.poi.ss.formula.FormulaParseException;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.CellType;\nimport org.apache.poi.ss.usermodel.Comment;\nimport org.apache.poi.ss.usermodel.Hyperlink;\nimport org.apache.poi.ss.usermodel.RichTextString;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.util.CellAddress;\nimport org.apache.poi.ss.util.CellRangeAddress;\n\nimport java.time.LocalDateTime;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 当单元格不存在时使用此对象表示，得到的值都为null,此对象只用于标注单元格所在位置信息。\n *\n * @author looly\n * @since 5.5.0\n */\npublic class NullCell implements Cell {\n\n\tprivate final Row row;\n\tprivate final int columnIndex;\n\n\t/**\n\t * 构造\n\t *\n\t * @param row         行\n\t * @param columnIndex 列号，从0开始\n\t */\n\tpublic NullCell(Row row, int columnIndex) {\n\t\tthis.row = row;\n\t\tthis.columnIndex = columnIndex;\n\t}\n\n\t@Override\n\tpublic int getColumnIndex() {\n\t\treturn this.columnIndex;\n\t}\n\n\t@Override\n\tpublic int getRowIndex() {\n\t\treturn getRow().getRowNum();\n\t}\n\n\t@Override\n\tpublic Sheet getSheet() {\n\t\treturn getRow().getSheet();\n\t}\n\n\t@Override\n\tpublic Row getRow() {\n\t\treturn this.row;\n\t}\n\n\tpublic void setCellType(CellType cellType) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setBlank() {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic CellType getCellType() {\n\t\treturn null;\n\t}\n\n\tpublic CellType getCellTypeEnum() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic CellType getCachedFormulaResultType() {\n\t\treturn null;\n\t}\n\n\tpublic CellType getCachedFormulaResultTypeEnum() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void setCellValue(double value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellValue(Date value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellValue(LocalDateTime value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellValue(Calendar value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellValue(RichTextString value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellValue(String value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellFormula(String formula) throws FormulaParseException, IllegalStateException {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void removeFormula() throws IllegalStateException {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic String getCellFormula() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic double getNumericCellValue() {\n\t\tthrow new UnsupportedOperationException(\"Cell value is null!\");\n\t}\n\n\t@Override\n\tpublic Date getDateCellValue() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic LocalDateTime getLocalDateTimeCellValue() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic RichTextString getRichStringCellValue() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic String getStringCellValue() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void setCellValue(boolean value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void setCellErrorValue(byte value) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic boolean getBooleanCellValue() {\n\t\tthrow new UnsupportedOperationException(\"Cell value is null!\");\n\t}\n\n\t@Override\n\tpublic byte getErrorCellValue() {\n\t\tthrow new UnsupportedOperationException(\"Cell value is null!\");\n\t}\n\n\t@Override\n\tpublic void setCellStyle(CellStyle style) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic CellStyle getCellStyle() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void setAsActiveCell() {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic CellAddress getAddress() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void setCellComment(Comment comment) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic Comment getCellComment() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void removeCellComment() {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic Hyperlink getHyperlink() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic void setHyperlink(Hyperlink link) {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic void removeHyperlink() {\n\t\tthrow new UnsupportedOperationException(\"Can not set any thing to null cell!\");\n\t}\n\n\t@Override\n\tpublic CellRangeAddress getArrayFormulaRange() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic boolean isPartOfArrayFormulaGroup() {\n\t\tthrow new UnsupportedOperationException(\"Cell value is null!\");\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/package-info.java",
    "content": "/**\n * Excel中单元格相关类，入口为CellUtil\n * @author looly\n *\n */\npackage cn.hutool.poi.excel.cell;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/BooleanCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * {@link Boolean} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class BooleanCellSetter implements CellSetter {\n\n\tprivate final Boolean value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tBooleanCellSetter(Boolean value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/CalendarCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\nimport java.util.Calendar;\n\n/**\n * {@link Calendar} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class CalendarCellSetter implements CellSetter {\n\n\tprivate final Calendar value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tCalendarCellSetter(Calendar value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/CellSetterFactory.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Hyperlink;\nimport org.apache.poi.ss.usermodel.RichTextString;\n\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * {@link CellSetter} 简单静态工厂类，用于根据值类型创建对应的{@link CellSetter}\n *\n * @author looly\n * @since 5.7.8\n */\npublic class CellSetterFactory {\n\n\t/**\n\t * 创建值对应类型的{@link CellSetter}\n\t *\n\t * @param value 值\n\t * @return {@link CellSetter}\n\t */\n\tpublic static CellSetter createCellSetter(Object value) {\n\t\tif (null == value) {\n\t\t\treturn NullCellSetter.INSTANCE;\n\t\t} else if (value instanceof CellSetter) {\n\t\t\treturn (CellSetter) value;\n\t\t} else if (value instanceof Date) {\n\t\t\treturn new DateCellSetter((Date) value);\n\t\t} else if (value instanceof TemporalAccessor) {\n\t\t\treturn new TemporalAccessorCellSetter((TemporalAccessor) value);\n\t\t} else if (value instanceof Calendar) {\n\t\t\treturn new CalendarCellSetter((Calendar) value);\n\t\t} else if (value instanceof Boolean) {\n\t\t\treturn new BooleanCellSetter((Boolean) value);\n\t\t} else if (value instanceof RichTextString) {\n\t\t\treturn new RichTextCellSetter((RichTextString) value);\n\t\t} else if (value instanceof Number) {\n\t\t\treturn new NumberCellSetter((Number) value);\n\t\t}else if (value instanceof Hyperlink) {\n\t\t\treturn new HyperlinkCellSetter((Hyperlink) value);\n\t\t} else {\n\t\t\treturn new CharSequenceCellSetter(value.toString());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/CharSequenceCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * {@link CharSequence} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class CharSequenceCellSetter implements CellSetter {\n\n\tprivate final CharSequence value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tCharSequenceCellSetter(CharSequence value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(value.toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/DateCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\nimport java.util.Date;\n\n/**\n * {@link Date} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class DateCellSetter implements CellSetter {\n\n\tprivate final Date value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tDateCellSetter(Date value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/EscapeStrCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.core.util.ReUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.regex.Pattern;\n\n/**\n * 字符串转义Cell值设置器<br>\n * 使用 _x005F前缀转义_xXXXX_，避免被decode的问题<br>\n * 如用户传入'_x5116_'会导致乱码，使用此设置器转义为'_x005F_x5116_'\n *\n * @author looly\n * @since 5.7.10\n */\npublic class EscapeStrCellSetter extends CharSequenceCellSetter {\n\n\tprivate static final Pattern utfPtrn = Pattern.compile(\"_x[0-9A-Fa-f]{4}_\");\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tpublic EscapeStrCellSetter(CharSequence value) {\n\t\tsuper(escape(StrUtil.str(value)));\n\t}\n\n\t/**\n\t * 使用 _x005F前缀转义_xXXXX_，避免被decode的问题<br>\n\t * issue#I466ZZ@Gitee\n\t *\n\t * @param value 被转义的字符串\n\t * @return 转义后的字符串\n\t */\n\tprivate static String escape(String value) {\n\t\tif (value == null || false == value.contains(\"_x\")) {\n\t\t\treturn value;\n\t\t}\n\n\t\t// 使用 _x005F前缀转义_xXXXX_，避免被decode的问题\n\t\t// issue#I466ZZ@Gitee\n\t\treturn ReUtil.replaceAll(value, utfPtrn, \"_x005F$0\");\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/HyperlinkCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.Hyperlink;\n\n/**\n * {@link Hyperlink} 值单元格设置器\n *\n * @author looly\n * @since 5.7.13\n */\npublic class HyperlinkCellSetter implements CellSetter {\n\n\tprivate final Hyperlink value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tHyperlinkCellSetter(Hyperlink value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setHyperlink(this.value);\n\t\tcell.setCellValue(this.value.getLabel());\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/NullCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * {@link Number} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class NullCellSetter implements CellSetter {\n\n\tpublic static final NullCellSetter INSTANCE = new NullCellSetter();\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(StrUtil.EMPTY);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/NumberCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\n/**\n * {@link Number} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class NumberCellSetter implements CellSetter {\n\n\tprivate final Number value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tNumberCellSetter(Number value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\t// issue https://gitee.com/chinabugotech/hutool/issues/I43U9G\n\t\t// 避免float到double的精度问题\n\t\tcell.setCellValue(NumberUtil.toDouble(value));\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/RichTextCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.RichTextString;\n\n/**\n * {@link RichTextString} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class RichTextCellSetter implements CellSetter {\n\n\tprivate final RichTextString value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tRichTextCellSetter(RichTextString value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tcell.setCellValue(value);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/TemporalAccessorCellSetter.java",
    "content": "package cn.hutool.poi.excel.cell.setters;\n\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport org.apache.poi.ss.usermodel.Cell;\n\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.temporal.TemporalAccessor;\nimport java.util.Date;\n\n/**\n * {@link TemporalAccessor} 值单元格设置器\n *\n * @author looly\n * @since 5.7.8\n */\npublic class TemporalAccessorCellSetter implements CellSetter {\n\n\tprivate final TemporalAccessor value;\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 值\n\t */\n\tTemporalAccessorCellSetter(TemporalAccessor value) {\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic void setValue(Cell cell) {\n\t\tif (value instanceof Instant) {\n\t\t\tcell.setCellValue(Date.from((Instant) value));\n\t\t} else if (value instanceof LocalDateTime) {\n\t\t\tcell.setCellValue((LocalDateTime) value);\n\t\t} else if (value instanceof LocalDate) {\n\t\t\tcell.setCellValue((LocalDate) value);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/setters/package-info.java",
    "content": "/**\n * Excel中单元格设置相关类，一些{@link cn.hutool.poi.excel.cell.CellSetter}的实现类\n *\n * @author looly\n */\npackage cn.hutool.poi.excel.cell.setters;\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/values/ErrorCellValue.java",
    "content": "package cn.hutool.poi.excel.cell.values;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.cell.CellValue;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.FormulaError;\n\n/**\n * ERROR类型单元格值\n *\n * @author looly\n * @since 5.7.8\n */\npublic class ErrorCellValue implements CellValue<String> {\n\n\tprivate final Cell cell;\n\n\t/**\n\t * 构造\n\t *\n\t * @param cell {@link Cell}\n\t */\n\tpublic ErrorCellValue(Cell cell){\n\t\tthis.cell = cell;\n\t}\n\n\t@Override\n\tpublic String getValue() {\n\t\tfinal FormulaError error = FormulaError.forInt(cell.getErrorCellValue());\n\t\treturn (null == error) ? StrUtil.EMPTY : error.getString();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/cell/values/NumericCellValue.java",
    "content": "package cn.hutool.poi.excel.cell.values;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.ExcelDateUtil;\nimport cn.hutool.poi.excel.cell.CellValue;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.util.NumberToTextConverter;\n\nimport java.time.LocalDateTime;\n\n/**\n * 数字类型单元格值<br>\n * 单元格值可能为Long、Double、Date\n *\n * @author looly\n * @since 5.7.8\n */\npublic class NumericCellValue implements CellValue<Object> {\n\n\tprivate final Cell cell;\n\n\t/**\n\t * 构造\n\t *\n\t * @param cell {@link Cell}\n\t */\n\tpublic NumericCellValue(Cell cell) {\n\t\tthis.cell = cell;\n\t}\n\n\t@Override\n\tpublic Object getValue() {\n\t\tfinal double value = cell.getNumericCellValue();\n\n\t\tfinal CellStyle style = cell.getCellStyle();\n\t\tif (null != style) {\n\t\t\t// 判断是否为日期\n\t\t\tif (ExcelDateUtil.isDateFormat(cell)) {\n\t\t\t\tfinal LocalDateTime dateCellValue = cell.getLocalDateTimeCellValue();\n\t\t\t\tif(1899 == dateCellValue.getYear()){\n\t\t\t\t\t// 1899年写入会导致数据错乱，读取到1899年证明这个单元格的信息不关注年月日\n\t\t\t\t\treturn dateCellValue.toLocalTime();\n\t\t\t\t}\n\t\t\t\t// 使用Hutool的DateTime包装\n\t\t\t\treturn DateUtil.date(dateCellValue);\n\t\t\t}\n\n\t\t\tfinal String format = style.getDataFormatString();\n\t\t\t// 普通数字\n\t\t\tif (null != format && format.indexOf(StrUtil.C_DOT) < 0) {\n\t\t\t\tfinal long longPart = (long) value;\n\t\t\t\tif (((double) longPart) == value) {\n\t\t\t\t\t// 对于无小数部分的数字类型，转为Long\n\t\t\t\t\treturn longPart;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 某些Excel单元格值为double计算结果，可能导致精度问题，通过转换解决精度问题。\n\t\treturn Double.parseDouble(NumberToTextConverter.toText(value));\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/editors/NumericToIntEditor.java",
    "content": "package cn.hutool.poi.excel.editors;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\nimport cn.hutool.poi.excel.cell.CellEditor;\n\n/**\n * POI中NUMRIC类型的值默认返回的是Double类型，此编辑器用于转换其为int型\n *\n * @author Looly\n */\npublic class NumericToIntEditor implements CellEditor {\n\n\t@Override\n\tpublic Object edit(Cell cell, Object value) {\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).intValue();\n\t\t}\n\t\treturn value;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/editors/TrimEditor.java",
    "content": "package cn.hutool.poi.excel.editors;\n\nimport org.apache.poi.ss.usermodel.Cell;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.cell.CellEditor;\n\n/**\n * 去除String类型的单元格值两边的空格\n * @author Looly\n *\n */\npublic class TrimEditor implements CellEditor{\n\n\t@Override\n\tpublic Object edit(Cell cell, Object value) {\n\t\tif(value instanceof String) {\n\t\t\treturn StrUtil.trim((String)value);\n\t\t}\n\t\treturn value;\n\t}\n\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/editors/package-info.java",
    "content": "/**\n * 单元格值编辑器，内部使用\n * @author looly\n *\n */\npackage cn.hutool.poi.excel.editors;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/package-info.java",
    "content": "/**\n * POI中对Excel读写的封装，入口为ExcelUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.excel;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/AbstractSheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.poi.excel.ExcelUtil;\nimport cn.hutool.poi.excel.RowUtil;\nimport cn.hutool.poi.excel.cell.CellEditor;\nimport org.apache.poi.ss.usermodel.Sheet;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 抽象{@link Sheet}数据读取实现\n *\n * @param <T> 读取类型\n * @author looly\n * @since 5.4.4\n */\npublic abstract class AbstractSheetReader<T> implements SheetReader<T> {\n\n\t/**\n\t * 读取起始行（包含，从0开始计数）\n\t */\n\tprotected final int startRowIndex;\n\t/**\n\t * 读取结束行（包含，从0开始计数）\n\t */\n\tprotected final int endRowIndex;\n\t/**\n\t * 是否忽略空行\n\t */\n\tprotected boolean ignoreEmptyRow = true;\n\t/**\n\t * 单元格值处理接口\n\t */\n\tprotected CellEditor cellEditor;\n\t/**\n\t * 标题别名\n\t */\n\tprivate Map<String, String> headerAlias;\n\n\t/**\n\t * 构造\n\t *\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t */\n\tpublic AbstractSheetReader(int startRowIndex, int endRowIndex) {\n\t\tthis.startRowIndex = startRowIndex;\n\t\tthis.endRowIndex = endRowIndex;\n\t}\n\n\t/**\n\t * 设置单元格值处理逻辑<br>\n\t * 当Excel中的值并不能满足我们的读取要求时，通过传入一个编辑接口，可以对单元格值自定义，例如对数字和日期类型值转换为字符串等\n\t *\n\t * @param cellEditor 单元格值处理接口\n\t */\n\tpublic void setCellEditor(CellEditor cellEditor) {\n\t\tthis.cellEditor = cellEditor;\n\t}\n\n\t/**\n\t * 设置是否忽略空行\n\t *\n\t * @param ignoreEmptyRow 是否忽略空行\n\t */\n\tpublic void setIgnoreEmptyRow(boolean ignoreEmptyRow) {\n\t\tthis.ignoreEmptyRow = ignoreEmptyRow;\n\t}\n\n\t/**\n\t * 设置标题行的别名Map\n\t *\n\t * @param headerAlias 别名Map\n\t */\n\tpublic void setHeaderAlias(Map<String, String> headerAlias) {\n\t\tthis.headerAlias = headerAlias;\n\t}\n\n\t/**\n\t * 增加标题别名\n\t *\n\t * @param header 标题\n\t * @param alias  别名\n\t */\n\tpublic void addHeaderAlias(String header, String alias) {\n\t\tMap<String, String> headerAlias = this.headerAlias;\n\t\tif (null == headerAlias) {\n\t\t\theaderAlias = new LinkedHashMap<>();\n\t\t}\n\t\tthis.headerAlias = headerAlias;\n\t\tthis.headerAlias.put(header, alias);\n\t}\n\n\t/**\n\t * 转换标题别名，如果没有别名则使用原标题，当标题为空时，列号对应的字母便是header\n\t *\n\t * @param headerList 原标题列表\n\t * @return 转换别名列表\n\t */\n\tprotected List<String> aliasHeader(List<Object> headerList) {\n\t\tif (CollUtil.isEmpty(headerList)) {\n\t\t\treturn new ArrayList<>(0);\n\t\t}\n\n\t\tfinal int size = headerList.size();\n\t\tfinal ArrayList<String> result = new ArrayList<>(size);\n\t\tfor (int i = 0; i < size; i++) {\n\t\t\tresult.add(aliasHeader(headerList.get(i), i));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 转换标题别名，如果没有别名则使用原标题，当标题为空时，列号对应的字母便是header\n\t *\n\t * @param headerObj 原标题\n\t * @param index     标题所在列号，当标题为空时，列号对应的字母便是header\n\t * @return 转换别名列表\n\t * @since 4.3.2\n\t */\n\tprotected String aliasHeader(Object headerObj, int index) {\n\t\tif (null == headerObj) {\n\t\t\treturn ExcelUtil.indexToColName(index);\n\t\t}\n\n\t\tfinal String header = headerObj.toString();\n\t\tif(null != this.headerAlias){\n\t\t\treturn ObjectUtil.defaultIfNull(this.headerAlias.get(header), header);\n\t\t}\n\t\treturn header;\n\t}\n\n\t/**\n\t * 读取某一行数据\n\t *\n\t * @param sheet {@link Sheet}\n\t * @param rowIndex 行号，从0开始\n\t * @return 一行数据\n\t */\n\tprotected List<Object> readRow(Sheet sheet, int rowIndex) {\n\t\treturn RowUtil.readRow(sheet.getRow(rowIndex), this.cellEditor);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/BeanSheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.poi.excel.cell.CellEditor;\nimport org.apache.poi.ss.usermodel.Sheet;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 读取{@link Sheet}为bean的List列表形式\n *\n * @author looly\n * @since 5.4.4\n */\npublic class BeanSheetReader<T> implements SheetReader<List<T>> {\n\n\tprivate final Class<T> beanClass;\n\tprivate final MapSheetReader mapSheetReader;\n\n\t/**\n\t * 构造\n\t *\n\t * @param headerRowIndex 标题所在行，如果标题行在读取的内容行中间，这行做为数据将忽略\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param endRowIndex    结束行（包含，从0开始计数）\n\t * @param beanClass      每行对应Bean的类型\n\t */\n\tpublic BeanSheetReader(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> beanClass) {\n\t\tmapSheetReader = new MapSheetReader(headerRowIndex, startRowIndex, endRowIndex);\n\t\tthis.beanClass = beanClass;\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<T> read(Sheet sheet) {\n\t\tfinal List<Map<String, Object>> mapList = mapSheetReader.read(sheet);\n\t\tif (Map.class.isAssignableFrom(this.beanClass)) {\n\t\t\treturn (List<T>) mapList;\n\t\t}\n\n\t\tfinal List<T> beanList = new ArrayList<>(mapList.size());\n\t\tfinal CopyOptions copyOptions = CopyOptions.create().setIgnoreError(true);\n\t\tfor (Map<String, Object> map : mapList) {\n\t\t\tbeanList.add(BeanUtil.toBean(map, this.beanClass, copyOptions));\n\t\t}\n\t\treturn beanList;\n\t}\n\n\t/**\n\t * 设置单元格值处理逻辑<br>\n\t * 当Excel中的值并不能满足我们的读取要求时，通过传入一个编辑接口，可以对单元格值自定义，例如对数字和日期类型值转换为字符串等\n\t *\n\t * @param cellEditor 单元格值处理接口\n\t */\n\tpublic void setCellEditor(CellEditor cellEditor) {\n\t\tthis.mapSheetReader.setCellEditor(cellEditor);\n\t}\n\n\t/**\n\t * 设置是否忽略空行\n\t *\n\t * @param ignoreEmptyRow 是否忽略空行\n\t */\n\tpublic void setIgnoreEmptyRow(boolean ignoreEmptyRow) {\n\t\tthis.mapSheetReader.setIgnoreEmptyRow(ignoreEmptyRow);\n\t}\n\n\t/**\n\t * 设置标题行的别名Map\n\t *\n\t * @param headerAlias 别名Map\n\t */\n\tpublic void setHeaderAlias(Map<String, String> headerAlias) {\n\t\tthis.mapSheetReader.setHeaderAlias(headerAlias);\n\t}\n\n\t/**\n\t * 增加标题别名\n\t *\n\t * @param header 标题\n\t * @param alias  别名\n\t */\n\tpublic void addHeaderAlias(String header, String alias) {\n\t\tthis.mapSheetReader.addHeaderAlias(header, alias);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/ColumnSheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport org.apache.poi.ss.usermodel.Sheet;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 读取单独一列\n *\n * @author looly\n * @since 5.7.17\n */\npublic class ColumnSheetReader extends AbstractSheetReader<List<Object>> {\n\n\tprivate final int columnIndex;\n\n\t/**\n\t * 构造\n\t *\n\t * @param columnIndex   列号，从0开始计数\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t */\n\tpublic ColumnSheetReader(int columnIndex, int startRowIndex, int endRowIndex) {\n\t\tsuper(startRowIndex, endRowIndex);\n\t\tthis.columnIndex = columnIndex;\n\t}\n\n\t@Override\n\tpublic List<Object> read(Sheet sheet) {\n\t\tfinal List<Object> resultList = new ArrayList<>();\n\n\t\tint startRowIndex = Math.max(this.startRowIndex, sheet.getFirstRowNum());// 读取起始行（包含）\n\t\tint endRowIndex = Math.min(this.endRowIndex, sheet.getLastRowNum());// 读取结束行（包含）\n\n\t\tObject value;\n\t\tfor (int i = startRowIndex; i <= endRowIndex; i++) {\n\t\t\tvalue = CellUtil.getCellValue(CellUtil.getCell(sheet.getRow(i), columnIndex), cellEditor);\n\t\t\tif(null != value || false == ignoreEmptyRow){\n\t\t\t\tresultList.add(value);\n\t\t\t}\n\t\t}\n\n\t\treturn resultList;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/ListSheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport org.apache.poi.ss.usermodel.Sheet;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 读取{@link Sheet}为List列表形式\n *\n * @author looly\n * @since 5.4.4\n */\npublic class ListSheetReader extends AbstractSheetReader<List<List<Object>>> {\n\n\t/** 是否首行作为标题行转换别名 */\n\tprivate final boolean aliasFirstLine;\n\n\t/**\n\t * 构造\n\t *\n\t * @param startRowIndex  起始行（包含，从0开始计数）\n\t * @param endRowIndex    结束行（包含，从0开始计数）\n\t * @param aliasFirstLine 是否首行作为标题行转换别名\n\t */\n\tpublic ListSheetReader(int startRowIndex, int endRowIndex, boolean aliasFirstLine) {\n\t\tsuper(startRowIndex, endRowIndex);\n\t\tthis.aliasFirstLine = aliasFirstLine;\n\t}\n\n\t@Override\n\tpublic List<List<Object>> read(Sheet sheet) {\n\t\tfinal List<List<Object>> resultList = new ArrayList<>();\n\n\t\tint startRowIndex = Math.max(this.startRowIndex, sheet.getFirstRowNum());// 读取起始行（包含）\n\t\tint endRowIndex = Math.min(this.endRowIndex, sheet.getLastRowNum());// 读取结束行（包含）\n\t\tList<Object> rowList;\n\t\tfor (int i = startRowIndex; i <= endRowIndex; i++) {\n\t\t\trowList = readRow(sheet, i);\n\t\t\tif (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {\n\t\t\t\tif (aliasFirstLine && i == startRowIndex) {\n\t\t\t\t\t// 第一行作为标题行，替换别名\n\t\t\t\t\trowList = Convert.toList(Object.class, aliasHeader(rowList));\n\t\t\t\t}\n\t\t\t\tresultList.add(rowList);\n\t\t\t}\n\t\t}\n\t\treturn resultList;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/MapSheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.poi.ss.usermodel.Sheet;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 读取{@link Sheet}为Map的List列表形式\n *\n * @author looly\n * @since 5.4.4\n */\npublic class MapSheetReader extends AbstractSheetReader<List<Map<String, Object>>> {\n\n\tprivate final int headerRowIndex;\n\n\t/**\n\t * 构造\n\t *\n\t * @param headerRowIndex 标题所在行，如果标题行在读取的内容行中间，这行做为数据将忽略\n\t * @param startRowIndex 起始行（包含，从0开始计数）\n\t * @param endRowIndex   结束行（包含，从0开始计数）\n\t */\n\tpublic MapSheetReader(int headerRowIndex, int startRowIndex, int endRowIndex) {\n\t\tsuper(startRowIndex, endRowIndex);\n\t\tthis.headerRowIndex = headerRowIndex;\n\t}\n\n\t@Override\n\tpublic List<Map<String, Object>> read(Sheet sheet) {\n\t\t// 边界判断\n\t\tfinal int firstRowNum = sheet.getFirstRowNum();\n\t\tfinal int lastRowNum = sheet.getLastRowNum();\n\t\tif(lastRowNum < 0){\n\t\t\treturn ListUtil.empty();\n\t\t}\n\n\t\tif (headerRowIndex < firstRowNum) {\n\t\t\tthrow new IndexOutOfBoundsException(StrUtil.format(\"Header row index {} is lower than first row index {}.\", headerRowIndex, firstRowNum));\n\t\t} else if (headerRowIndex > lastRowNum) {\n\t\t\tthrow new IndexOutOfBoundsException(StrUtil.format(\"Header row index {} is greater than last row index {}.\", headerRowIndex, lastRowNum));\n\t\t} else if (startRowIndex > lastRowNum) {\n\t\t\t// issue#I5U1JA 只有标题行的Excel，起始行是1，标题行（最后的行号是0）\n\t\t\treturn ListUtil.empty();\n\t\t}\n\t\tfinal int startRowIndex = Math.max(this.startRowIndex, firstRowNum);// 读取起始行（包含）\n\t\tfinal int endRowIndex = Math.min(this.endRowIndex, lastRowNum);// 读取结束行（包含）\n\n\t\t// 读取header\n\t\tfinal List<String> headerList = aliasHeader(readRow(sheet, headerRowIndex));\n\n\t\tfinal List<Map<String, Object>> result = new ArrayList<>(endRowIndex - startRowIndex + 1);\n\t\tList<Object> rowList;\n\t\tfor (int i = startRowIndex; i <= endRowIndex; i++) {\n\t\t\t// 跳过标题行\n\t\t\tif (i != headerRowIndex) {\n\t\t\t\trowList = readRow(sheet, i);\n\t\t\t\tif (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {\n\t\t\t\t\tresult.add(IterUtil.toMap(headerList, rowList, true));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/SheetReader.java",
    "content": "package cn.hutool.poi.excel.reader;\n\nimport org.apache.poi.ss.usermodel.Sheet;\n\n/**\n * Excel {@link Sheet}读取接口，通过实现此接口，将{@link Sheet}中的数据读取为不同类型。\n *\n * @param <T> 读取的数据类型\n */\n@FunctionalInterface\npublic interface SheetReader<T> {\n\n\t/**\n\t * 读取数据\n\t *\n\t * @param sheet {@link Sheet}\n\t * @return 读取结果\n\t */\n\tT read(Sheet sheet);\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/reader/package-info.java",
    "content": "/**\n * 数据读取接口及实现，此包中定义了SheetReader，通过实现此接口，实现sheet中的数据读取为不同类型。\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.excel.reader;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/AttributeName.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport org.xml.sax.Attributes;\n\n/**\n * Excel的XML中属性名枚举\n *\n * @author looly\n * @since 5.3.6\n */\npublic enum AttributeName {\n\n\t/**\n\t * 行列号属性，行标签下此为行号属性名，cell标签下下为列号属性名\n\t */\n\tr,\n\t/**\n\t * ST（StylesTable） 的索引，样式index，用于获取行或单元格样式\n\t */\n\ts,\n\t/**\n\t * Type类型，单元格类型属性，见{@link CellDataType}\n\t */\n\tt;\n\n\t/**\n\t * 是否匹配给定属性\n\t *\n\t * @param attributeName 属性\n\t * @return 是否匹配\n\t */\n\tpublic boolean match(String attributeName) {\n\t\treturn this.name().equals(attributeName);\n\t}\n\n\t/**\n\t * 从属性里列表中获取对应属性值\n\t *\n\t * @param attributes 属性列表\n\t * @return 属性值\n\t */\n\tpublic String getValue(Attributes attributes){\n\t\treturn attributes.getValue(name());\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/CellDataType.java",
    "content": "package cn.hutool.poi.excel.sax;\n\n/**\n * 单元格数据类型枚举\n * \n * @author Looly\n *\n */\npublic enum CellDataType {\n\t/** Boolean类型 */\n\tBOOL(\"b\"),\n\t/** 类型错误 */\n\tERROR(\"e\"),\n\t/** 计算结果类型，此类型使用f标签辅助判断，而非属性 */\n\tFORMULA(\"formula\"),\n\t/** 富文本类型 */\n\tINLINESTR(\"inlineStr\"),\n\t/** 共享字符串索引类型 */\n\tSSTINDEX(\"s\"),\n\t/** 数字类型 */\n\tNUMBER(\"\"),\n\t/** 日期类型，此类型使用值判断，而非属性 */\n\tDATE(\"m/d/yy\"),\n\t/** 空类型 */\n\tNULL(\"\");\n\n\t/** 属性值 */\n\tprivate final String name;\n\n\t/**\n\t * 构造\n\t * \n\t * @param name 类型属性值\n\t */\n\tCellDataType(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * 获取对应类型的属性值\n\t * \n\t * @return 属性值\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * 类型字符串转为枚举\n\t * @param name 类型字符串\n\t * @return 类型枚举\n\t */\n\tpublic static CellDataType of(String name) {\n\t\tif(null == name) {\n\t\t\t//默认数字\n\t\t\treturn NUMBER;\n\t\t}\n\t\t\n\t\tif(BOOL.name.equals(name)) {\n\t\t\treturn BOOL;\n\t\t}else if(ERROR.name.equals(name)) {\n\t\t\treturn ERROR;\n\t\t}else if(INLINESTR.name.equals(name)) {\n\t\t\treturn INLINESTR;\n\t\t}else if(SSTINDEX.name.equals(name)) {\n\t\t\treturn SSTINDEX;\n\t\t}else if(FORMULA.name.equals(name)) {\n\t\t\treturn FORMULA;\n\t\t}else {\n\t\t\treturn NULL;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ElementName.java",
    "content": "package cn.hutool.poi.excel.sax;\n\n/**\n * 标签名枚举\n *\n * @author looly\n * @since 5.3.6\n */\npublic enum ElementName {\n\t/**\n\t * 行标签名，表示一行\n\t */\n\trow,\n\t/**\n\t * Cell单元格标签名，表示一个单元格\n\t */\n\tc,\n\t/**\n\t * Value单元格值的标签，表示单元格内的值\n\t */\n\tv,\n\t/**\n\t * Formula公式，表示一个存放公式的单元格\n\t */\n\tf;\n\n\t/**\n\t * 给定标签名是否匹配当前标签\n\t *\n\t * @param elementName 标签名\n\t * @return 是否匹配\n\t */\n\tpublic boolean match(String elementName){\n\t\treturn this.name().equals(elementName);\n\t}\n\n\t/**\n\t * 解析支持的节点名枚举\n\t * @param elementName 节点名\n\t * @return 节点名枚举\n\t */\n\tpublic static ElementName of(String elementName){\n\t\ttry {\n\t\t\treturn valueOf(elementName);\n\t\t} catch (Exception ignore){\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.hssf.eventusermodel.EventWorkbookBuilder.SheetRecordCollectingListener;\nimport org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener;\nimport org.apache.poi.hssf.eventusermodel.HSSFEventFactory;\nimport org.apache.poi.hssf.eventusermodel.HSSFListener;\nimport org.apache.poi.hssf.eventusermodel.HSSFRequest;\nimport org.apache.poi.hssf.eventusermodel.MissingRecordAwareHSSFListener;\nimport org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord;\nimport org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord;\nimport org.apache.poi.hssf.model.HSSFFormulaParser;\nimport org.apache.poi.hssf.record.BOFRecord;\nimport org.apache.poi.hssf.record.BlankRecord;\nimport org.apache.poi.hssf.record.BoolErrRecord;\nimport org.apache.poi.hssf.record.BoundSheetRecord;\nimport org.apache.poi.hssf.record.CellValueRecordInterface;\nimport org.apache.poi.hssf.record.EOFRecord;\nimport org.apache.poi.hssf.record.FormulaRecord;\nimport org.apache.poi.hssf.record.LabelRecord;\nimport org.apache.poi.hssf.record.LabelSSTRecord;\nimport org.apache.poi.hssf.record.NumberRecord;\nimport org.apache.poi.hssf.record.Record;\nimport org.apache.poi.hssf.record.SSTRecord;\nimport org.apache.poi.hssf.record.StringRecord;\nimport org.apache.poi.hssf.usermodel.HSSFWorkbook;\nimport org.apache.poi.poifs.filesystem.POIFSFileSystem;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Excel2003格式的事件-用户模型方式读取器，在Hutool中，统一将此归类为Sax读取<br>\n * 参考：http://www.cnblogs.com/wshsdlau/p/5643862.html\n *\n * @author looly\n */\npublic class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03SaxReader> {\n\n\t/**\n\t * 如果为公式，true表示输出公式计算后的结果值，false表示输出公式本身\n\t */\n\tprivate final boolean isOutputFormulaValues = true;\n\n\t/**\n\t * 用于解析公式\n\t */\n\tprivate SheetRecordCollectingListener workbookBuildingListener;\n\t/**\n\t * 子工作簿，用于公式计算\n\t */\n\tprivate HSSFWorkbook stubWorkbook;\n\n\t/**\n\t * 静态字符串表\n\t */\n\tprivate SSTRecord sstRecord;\n\n\tprivate FormatTrackingHSSFListener formatListener;\n\n\t/**\n\t * Sheet边界记录，此Record中可以获得Sheet名\n\t */\n\tprivate final List<BoundSheetRecord> boundSheetRecords = new ArrayList<>();\n\n\tprivate boolean isOutputNextStringRecord;\n\n\t// 存储行记录的容器\n\tprivate List<Object> rowCellList = new ArrayList<>();\n\n\t/**\n\t * 自定义需要处理的sheet编号，如果-1表示处理所有sheet\n\t */\n\tprivate int sheetIndex = -1;\n\t/**\n\t * sheet名称，主要用于使用sheet名读取的情况\n\t */\n\tprivate String sheetName;\n\n\t/**\n\t * 当前rid索引\n \t */\n\tprivate int curRid = -1;\n\n\t/**\n\t * 行处理器\n\t */\n\tprivate final RowHandler rowHandler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param rowHandler 行处理器\n\t */\n\tpublic Excel03SaxReader(RowHandler rowHandler) {\n\t\tthis.rowHandler = rowHandler;\n\t}\n\n\t// ------------------------------------------------------------------------------ Read start\n\t@Override\n\tpublic Excel03SaxReader read(File file, String idOrRidOrSheetName) throws POIException {\n\t\ttry (POIFSFileSystem poifsFileSystem = new POIFSFileSystem(file, true)) {\n\t\t\treturn read(poifsFileSystem, idOrRidOrSheetName);\n\t\t} catch (IOException e) {\n\t\t\tthrow new POIException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Excel03SaxReader read(InputStream excelStream, String idOrRidOrSheetName) throws POIException {\n\t\ttry {\n\t\t\treturn read(new POIFSFileSystem(excelStream), idOrRidOrSheetName);\n\t\t} catch (IOException e) {\n\t\t\tthrow new POIException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 读取\n\t *\n\t * @param fs  {@link POIFSFileSystem}\n\t * @param idOrRidOrSheetName sheet id或者rid编号或sheet名称，从0开始，rid必须加rId前缀，例如rId0，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException IO异常包装\n\t */\n\tpublic Excel03SaxReader read(POIFSFileSystem fs, String idOrRidOrSheetName) throws POIException {\n\t\tthis.sheetIndex = getSheetIndex(idOrRidOrSheetName);\n\n\t\tformatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));\n\t\tfinal HSSFRequest request = new HSSFRequest();\n\t\tif (isOutputFormulaValues) {\n\t\t\trequest.addListenerForAllRecords(formatListener);\n\t\t} else {\n\t\t\tworkbookBuildingListener = new SheetRecordCollectingListener(formatListener);\n\t\t\trequest.addListenerForAllRecords(workbookBuildingListener);\n\t\t}\n\t\tfinal HSSFEventFactory factory = new HSSFEventFactory();\n\t\ttry {\n\t\t\tfactory.processWorkbookEvents(request, fs);\n\t\t} catch (IOException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (final StopReadException e) {\n\t\t\t// issue#3820 跳过，用户抛出此异常，表示强制结束读取\n\t\t} finally {\n\t\t\tIoUtil.close(fs);\n\t\t}\n\t\treturn this;\n\t}\n\t// ------------------------------------------------------------------------------ Read end\n\n\t/**\n\t * 获得Sheet序号，如果处理所有sheet，获得最大的Sheet序号，从0开始\n\t *\n\t * @return sheet序号\n\t */\n\tpublic int getSheetIndex() {\n\t\treturn this.sheetIndex;\n\t}\n\n\t/**\n\t * 获得Sheet名，如果处理所有sheet，获得后一个Sheet名，从0开始\n\t *\n\t * @return Sheet名\n\t */\n\tpublic String getSheetName() {\n\t\tif(null != this.sheetName){\n\t\t\treturn this.sheetName;\n\t\t}\n\n\t\tif (this.boundSheetRecords.size() > this.sheetIndex) {\n\t\t\treturn this.boundSheetRecords.get(this.sheetIndex > -1 ? this.sheetIndex : this.curRid).getSheetname();\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * HSSFListener 监听方法，处理 Record\n\t *\n\t * @param record 记录\n\t */\n\t@Override\n\tpublic void processRecord(Record record) {\n\t\tif (this.sheetIndex > -1 && this.curRid > this.sheetIndex) {\n\t\t\t// 指定Sheet之后的数据不再处理\n\t\t\treturn;\n\t\t}\n\n\t\tif (record instanceof BoundSheetRecord) {\n\t\t\t// Sheet边界记录，此Record中可以获得Sheet名\n\t\t\tfinal BoundSheetRecord boundSheetRecord = (BoundSheetRecord) record;\n\t\t\tboundSheetRecords.add(boundSheetRecord);\n\t\t\tfinal String currentSheetName = boundSheetRecord.getSheetname();\n\t\t\tif(null != this.sheetName && StrUtil.equals(this.sheetName, currentSheetName)){\n\t\t\t\tthis.sheetIndex = this.boundSheetRecords.size() -1;\n\t\t\t}\n\t\t} else if (record instanceof SSTRecord) {\n\t\t\t// 静态字符串表\n\t\t\tsstRecord = (SSTRecord) record;\n\t\t} else if (record instanceof BOFRecord) {\n\t\t\tBOFRecord bofRecord = (BOFRecord) record;\n\t\t\tif (bofRecord.getType() == BOFRecord.TYPE_WORKSHEET) {\n\t\t\t\t// 如果有需要，则建立子工作薄\n\t\t\t\tif (workbookBuildingListener != null && stubWorkbook == null) {\n\t\t\t\t\tstubWorkbook = workbookBuildingListener.getStubHSSFWorkbook();\n\t\t\t\t}\n\t\t\t\tcurRid++;\n\t\t\t}\n\t\t} else if (record instanceof EOFRecord){\n\t\t\tif(this.sheetIndex < 0 && null != this.sheetName){\n\t\t\t\tthrow new POIException(\"Sheet [{}] not exist!\", this.sheetName);\n\t\t\t}\n\t\t\tif(this.curRid != -1 && isProcessCurrentSheet()) {\n\t\t\t\t//只有在当前指定的sheet中，才触发结束事件，且curId=-1时也不处理，避免重复调用\n\t\t\t\tprocessLastCellSheet();\n\t\t\t}\n\t\t} else if (isProcessCurrentSheet()) {\n\t\t\tif (record instanceof MissingCellDummyRecord) {\n\t\t\t\t// 空值的操作\n\t\t\t\tMissingCellDummyRecord mc = (MissingCellDummyRecord) record;\n\t\t\t\taddToRowCellList(mc);\n\t\t\t} else if (record instanceof LastCellOfRowDummyRecord) {\n\t\t\t\t// 行结束\n\t\t\t\tprocessLastCell((LastCellOfRowDummyRecord) record);\n\t\t\t} else {\n\t\t\t\t// 处理单元格值\n\t\t\t\tprocessCellValue(record);\n\t\t\t}\n\t\t}\n\n\t}\n\n\t// ---------------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 将空数据加入到行列表中\n\t *\n\t * @param record MissingCellDummyRecord\n\t */\n\tprivate void addToRowCellList(MissingCellDummyRecord record) {\n\t\taddToRowCellList(record.getRow(), record.getColumn(), StrUtil.EMPTY);\n\t}\n\n\t/**\n\t * 将单元格数据加入到行列表中\n\t *\n\t * @param record 单元格\n\t * @param value  值\n\t */\n\tprivate void addToRowCellList(CellValueRecordInterface record, Object value) {\n\t\taddToRowCellList(record.getRow(), record.getColumn(), value);\n\t}\n\n\t/**\n\t * 将单元格数据加入到行列表中\n\t *\n\t * @param row    行号\n\t * @param column 单元格\n\t * @param value  值\n\t */\n\tprivate void addToRowCellList(int row, int column, Object value) {\n\t\twhile (column > this.rowCellList.size()) {\n\t\t\t// 对于中间无数据的单元格补齐空白\n\t\t\tthis.rowCellList.add(StrUtil.EMPTY);\n\t\t\tthis.rowHandler.handleCell(this.curRid, row, rowCellList.size() - 1, value, null);\n\t\t}\n\n\t\tthis.rowCellList.add(column, value);\n\t\tthis.rowHandler.handleCell(this.curRid, row, column, value, null);\n\t}\n\n\t/**\n\t * 处理单元格值\n\t *\n\t * @param record 单元格\n\t */\n\tprivate void processCellValue(Record record) {\n\t\tObject value = null;\n\n\t\tswitch (record.getSid()) {\n\t\t\tcase BlankRecord.sid:\n\t\t\t\t// 空白记录\n\t\t\t\taddToRowCellList(((BlankRecord) record), StrUtil.EMPTY);\n\t\t\t\tbreak;\n\t\t\tcase BoolErrRecord.sid:\n\t\t\t\t// 布尔类型\n\t\t\t\tfinal BoolErrRecord berec = (BoolErrRecord) record;\n\t\t\t\taddToRowCellList(berec, berec.getBooleanValue());\n\t\t\t\tbreak;\n\t\t\tcase FormulaRecord.sid:\n\t\t\t\t// 公式类型\n\t\t\t\tfinal FormulaRecord formulaRec = (FormulaRecord) record;\n\t\t\t\tif (isOutputFormulaValues) {\n\t\t\t\t\tif (Double.isNaN(formulaRec.getValue())) {\n\t\t\t\t\t\t// Formula result is a string\n\t\t\t\t\t\t// This is stored in the next record\n\t\t\t\t\t\tisOutputNextStringRecord = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvalue = ExcelSaxUtil.getNumberOrDateValue(formulaRec, formulaRec.getValue(), this.formatListener);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tvalue = HSSFFormulaParser.toFormulaString(stubWorkbook, formulaRec.getParsedExpression());\n\t\t\t\t}\n\t\t\t\taddToRowCellList(formulaRec, value);\n\t\t\t\tbreak;\n\t\t\tcase StringRecord.sid:\n\t\t\t\t// 单元格中公式的字符串\n\t\t\t\tif (isOutputNextStringRecord) {\n\t\t\t\t\t// String for formula\n\t\t\t\t\t// value = ((StringRecord) record).getString();\n\t\t\t\t\tisOutputNextStringRecord = false;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase LabelRecord.sid:\n\t\t\t\tfinal LabelRecord lrec = (LabelRecord) record;\n\t\t\t\tvalue = lrec.getValue();\n\t\t\t\taddToRowCellList(lrec, value);\n\t\t\t\tbreak;\n\t\t\tcase LabelSSTRecord.sid:\n\t\t\t\t// 字符串类型\n\t\t\t\tLabelSSTRecord lsrec = (LabelSSTRecord) record;\n\t\t\t\tif (null != sstRecord) {\n\t\t\t\t\tvalue = sstRecord.getString(lsrec.getSSTIndex()).toString();\n\t\t\t\t}\n\t\t\t\taddToRowCellList(lsrec, ObjectUtil.defaultIfNull(value, StrUtil.EMPTY));\n\t\t\t\tbreak;\n\t\t\tcase NumberRecord.sid: // 数字类型\n\t\t\t\tfinal NumberRecord numrec = (NumberRecord) record;\n\t\t\t\tvalue = ExcelSaxUtil.getNumberOrDateValue(numrec, numrec.getValue(), this.formatListener);\n\t\t\t\t// 向容器加入列值\n\t\t\t\taddToRowCellList(numrec, value);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * 处理行结束后的操作，{@link LastCellOfRowDummyRecord}是行结束的标识Record\n\t *\n\t * @param lastCell 行结束的标识Record\n\t */\n\tprivate void processLastCell(LastCellOfRowDummyRecord lastCell) {\n\t\t// 每行结束时， 调用handle() 方法\n\t\tthis.rowHandler.handle(curRid, lastCell.getRow(), this.rowCellList);\n\t\t// 清空行Cache\n\t\tthis.rowCellList = new ArrayList<>(this.rowCellList.size());\n\t}\n\n\t/**\n\t * 处理sheet结束后的操作\n\t */\n\tprivate void processLastCellSheet(){\n\t\tthis.rowHandler.doAfterAllAnalysed();\n\t}\n\n\t/**\n\t * 是否处理当前sheet\n\t *\n\t * @return 是否处理当前sheet\n\t */\n\tprivate boolean isProcessCurrentSheet() {\n\t\t// rid < 0 且 sheet名称存在，说明没有匹配到sheet名称\n\t\treturn (this.sheetIndex < 0 && null == this.sheetName) || this.sheetIndex == this.curRid;\n\t}\n\n\t/**\n\t * 获取sheet索引，从0开始\n\t * <ul>\n\t *     <li>传入'rId'开头，直接去除rId前缀</li>\n\t *     <li>传入纯数字，表示sheetIndex，直接使用</li>\n\t * </ul>\n\t *\n\t * @param sheetIndexOrSheetName Excel中的sheet 编号或sheet名称，从0开始，如果为-1处理所有编号的sheet\n\t * @return sheet索引，从0开始\n\t * @since 5.5.5\n\t */\n\tprivate int getSheetIndex(String sheetIndexOrSheetName) {\n\t\tAssert.notBlank(sheetIndexOrSheetName, \"id or rid or sheetName must be not blank!\");\n\n\t\t// rid直接处理\n\t\tif (StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, RID_PREFIX)) {\n\t\t\treturn Integer.parseInt(StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, RID_PREFIX));\n\t\t} else if(StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX)){\n\t\t\t// since 5.7.10，支持任意名称\n\t\t\tthis.sheetName = StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX);\n\t\t} else {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(sheetIndexOrSheetName);\n\t\t\t} catch (NumberFormatException ignore) {\n\t\t\t\t// 如果用于传入非数字，按照sheet名称对待\n\t\t\t\tthis.sheetName = sheetIndexOrSheetName;\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t}\n\t// ---------------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.openxml4j.exceptions.InvalidFormatException;\nimport org.apache.poi.openxml4j.exceptions.OpenXML4JException;\nimport org.apache.poi.openxml4j.opc.OPCPackage;\nimport org.apache.poi.openxml4j.opc.PackageAccess;\nimport org.apache.poi.xssf.eventusermodel.XSSFReader;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Iterator;\n\n/**\n * Sax方式读取Excel文件<br>\n * Excel2007格式说明见：http://www.cnblogs.com/wangmingshun/p/6654143.html\n *\n * @author Looly\n * @since 3.1.2\n */\npublic class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {\n\n\tprivate final SheetDataSaxHandler handler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param rowHandler 行处理器\n\t */\n\tpublic Excel07SaxReader(RowHandler rowHandler) {\n\t\tthis.handler = new SheetDataSaxHandler(rowHandler);\n\t}\n\n\t/**\n\t * 设置行处理器\n\t *\n\t * @param rowHandler 行处理器\n\t * @return this\n\t */\n\tpublic Excel07SaxReader setRowHandler(RowHandler rowHandler) {\n\t\tthis.handler.setRowHandler(rowHandler);\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------------------------------------ Read start\n\t@Override\n\tpublic Excel07SaxReader read(File file, int rid) throws POIException {\n\t\treturn read(file, RID_PREFIX + rid);\n\t}\n\n\t@Override\n\tpublic Excel07SaxReader read(File file, String idOrRidOrSheetName) throws POIException {\n\t\ttry (OPCPackage open = OPCPackage.open(file, PackageAccess.READ)){\n\t\t\treturn read(open, idOrRidOrSheetName);\n\t\t} catch (InvalidFormatException | IOException e) {\n\t\t\tthrow new POIException(e);\n\t\t}\n\t}\n\n\t@Override\n\tpublic Excel07SaxReader read(InputStream in, int rid) throws POIException {\n\t\treturn read(in, RID_PREFIX + rid);\n\t}\n\n\t@Override\n\tpublic Excel07SaxReader read(InputStream in, String idOrRidOrSheetName) throws POIException {\n\t\ttry (final OPCPackage opcPackage = OPCPackage.open(in)) {\n\t\t\treturn read(opcPackage, idOrRidOrSheetName);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (InvalidFormatException e) {\n\t\t\tthrow new POIException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 开始读取Excel，Sheet编号从0开始计数\n\t *\n\t * @param opcPackage {@link OPCPackage}，Excel包，读取后不关闭\n\t * @param rid        Excel中的sheet rid编号，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tpublic Excel07SaxReader read(OPCPackage opcPackage, int rid) throws POIException {\n\t\treturn read(opcPackage, RID_PREFIX + rid);\n\t}\n\n\t/**\n\t * 开始读取Excel，Sheet编号从0开始计数\n\t *\n\t * @param opcPackage {@link OPCPackage}，Excel包，读取后不关闭\n\t * @param idOrRidOrSheetName    Excel中的sheet id或者rid编号或sheet名，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tpublic Excel07SaxReader read(OPCPackage opcPackage, String idOrRidOrSheetName) throws POIException {\n\t\ttry {\n\t\t\treturn read(new XSSFReader(opcPackage), idOrRidOrSheetName);\n\t\t} catch (OpenXML4JException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 开始读取Excel，Sheet编号从0开始计数\n\t *\n\t * @param xssfReader {@link XSSFReader}，Excel读取器\n\t * @param idOrRidOrSheetName    Excel中的sheet id或者rid编号或sheet名，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t * @since 5.4.4\n\t */\n\tpublic Excel07SaxReader read(XSSFReader xssfReader, String idOrRidOrSheetName) throws POIException {\n\t\t// 获取共享样式表，样式非必须\n\t\ttry {\n\t\t\tthis.handler.stylesTable = xssfReader.getStylesTable();\n\t\t} catch (IOException | InvalidFormatException ignore) {\n\t\t\t// ignore\n\t\t}\n\n\t\t// 获取共享字符串表\n\t\t// POI-5.2.0开始返回值有所变更，导致实际使用时提示方法未找到，此处使用反射调用，解决不同版本返回值变更问题\n\t\t//this.handler.sharedStrings = xssfReader.getSharedStringsTable();\n\t\tthis.handler.sharedStrings = ReflectUtil.invoke(xssfReader, \"getSharedStringsTable\");\n\n\t\treturn readSheets(xssfReader, idOrRidOrSheetName);\n\t}\n\t// ------------------------------------------------------------------------------ Read end\n\n\t// --------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 开始读取Excel，Sheet编号从0开始计数\n\t *\n\t * @param xssfReader         {@link XSSFReader}，Excel读取器\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名，从0开始，rid必须加rId前缀，例如rId0，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t * @since 5.4.4\n\t */\n\tprivate Excel07SaxReader readSheets(XSSFReader xssfReader, String idOrRidOrSheetName) throws POIException {\n\t\tthis.handler.sheetIndex = getSheetIndex(xssfReader, idOrRidOrSheetName);\n\t\tInputStream sheetInputStream = null;\n\t\ttry {\n\t\t\tif (this.handler.sheetIndex > -1) {\n\t\t\t\t// 根据 rId# 或 rSheet# 查找sheet\n\t\t\t\tsheetInputStream = xssfReader.getSheet(RID_PREFIX + (this.handler.sheetIndex + 1));\n\t\t\t\tExcelSaxUtil.readFrom(sheetInputStream, this.handler);\n\t\t\t\tthis.handler.rowHandler.doAfterAllAnalysed();\n\t\t\t} else {\n\t\t\t\tthis.handler.sheetIndex = -1;\n\t\t\t\t// 遍历所有sheet\n\t\t\t\tfinal Iterator<InputStream> sheetInputStreams = xssfReader.getSheetsData();\n\t\t\t\twhile (sheetInputStreams.hasNext()) {\n\t\t\t\t\t// 重新读取一个sheet时行归零\n\t\t\t\t\tthis.handler.index = 0;\n\t\t\t\t\tthis.handler.sheetIndex++;\n\t\t\t\t\tsheetInputStream = sheetInputStreams.next();\n\t\t\t\t\tExcelSaxUtil.readFrom(sheetInputStream, this.handler);\n\t\t\t\t\tthis.handler.rowHandler.doAfterAllAnalysed();\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (RuntimeException e) {\n\t\t\tthrow e;\n\t\t} catch (Exception e) {\n\t\t\tthrow new POIException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(sheetInputStream);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取sheet索引，从0开始\n\t * <ul>\n\t *     <li>传入'rId'开头，直接去除rId前缀</li>\n\t *     <li>传入纯数字，表示sheetIndex，通过{@link SheetRidReader}转换为rId</li>\n\t *     <li>传入其它字符串，表示sheetName，通过{@link SheetRidReader}转换为rId</li>\n\t * </ul>\n\t *\n\t * @param xssfReader         {@link XSSFReader}，Excel读取器\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称，从0开始，rid必须加rId前缀，例如rId0，如果为-1处理所有编号的sheet\n\t * @return sheet索引，从0开始\n\t * @since 5.5.5\n\t */\n\tprivate int getSheetIndex(XSSFReader xssfReader, String idOrRidOrSheetName) {\n\t\t// rid直接处理\n\t\tif (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) {\n\t\t\treturn Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX));\n\t\t}\n\n\t\t// sheetIndex需转换为rid\n\t\tfinal SheetRidReader ridReader = SheetRidReader.parse(xssfReader);\n\n\t\tif (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX)) {\n\t\t\t// name:开头的被认为是sheet名称直接处理\n\t\t\tidOrRidOrSheetName = StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX);\n\t\t\tfinal Integer rid = ridReader.getRidByNameBase0(idOrRidOrSheetName);\n\t\t\tif (null != rid) {\n\t\t\t\treturn rid;\n\t\t\t}\n\t\t} else {\n\t\t\t// 尝试查找名称\n\t\t\tInteger rid = ridReader.getRidByNameBase0(idOrRidOrSheetName);\n\t\t\tif (null != rid) {\n\t\t\t\treturn rid;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tfinal int sheetIndex = Integer.parseInt(idOrRidOrSheetName);\n\t\t\t\trid = ridReader.getRidBySheetIdBase0(sheetIndex);\n\t\t\t\t// 如果查找不到对应index，则认为用户传入的直接是rid\n\t\t\t\treturn ObjectUtil.defaultIfNull(rid, sheetIndex);\n\t\t\t} catch (NumberFormatException ignore) {\n\t\t\t\t// 非数字，说明非index，且没有对应名称，抛出异常\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Invalid rId or id or sheetName: \" + idOrRidOrSheetName);\n\t}\n\t// --------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxReader.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.poi.exceptions.POIException;\n\nimport java.io.File;\nimport java.io.InputStream;\n\n/**\n * Sax方式读取Excel接口，提供一些共用方法\n * @author looly\n *\n * @param <T> 子对象类型，用于标记返回值this\n * @since 3.2.0\n */\npublic interface ExcelSaxReader<T> {\n\n\t// sheet r:Id前缀\n\tString RID_PREFIX = \"rId\";\n\t// sheet name前缀\n\tString SHEET_NAME_PREFIX = \"sheetName:\";\n\n\t/**\n\t * 开始读取Excel\n\t *\n\t * @param file Excel文件\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tT read(File file, String idOrRidOrSheetName) throws POIException;\n\n\t/**\n\t * 开始读取Excel，读取结束后并不关闭流\n\t *\n\t * @param in Excel流\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tT read(InputStream in, String idOrRidOrSheetName) throws POIException;\n\n\t/**\n\t * 开始读取Excel，读取所有sheet\n\t *\n\t * @param path Excel文件路径\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(String path) throws POIException {\n\t\treturn read(FileUtil.file(path));\n\t}\n\n\t/**\n\t * 开始读取Excel，读取所有sheet\n\t *\n\t * @param file Excel文件\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(File file) throws POIException {\n\t\treturn read(file, -1);\n\t}\n\n\t/**\n\t * 开始读取Excel，读取所有sheet，读取结束后并不关闭流\n\t *\n\t * @param in Excel包流\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(InputStream in) throws POIException {\n\t\treturn read(in, -1);\n\t}\n\n\t/**\n\t * 开始读取Excel\n\t *\n\t * @param path 文件路径\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(String path, int idOrRidOrSheetName) throws POIException {\n\t\treturn read(FileUtil.file(path), idOrRidOrSheetName);\n\t}\n\n\t/**\n\t * 开始读取Excel\n\t *\n\t * @param path 文件路径\n\t * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称，rid必须加rId前缀，例如rId1，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(String path, String idOrRidOrSheetName) throws POIException {\n\t\treturn read(FileUtil.file(path), idOrRidOrSheetName);\n\t}\n\n\t/**\n\t * 开始读取Excel\n\t *\n\t * @param file Excel文件\n\t * @param rid Excel中的sheet rid编号，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(File file, int rid) throws POIException{\n\t\treturn read(file, String.valueOf(rid));\n\t}\n\n\t/**\n\t * 开始读取Excel，读取结束后并不关闭流\n\t *\n\t * @param in Excel流\n\t * @param rid Excel中的sheet rid编号，如果为-1处理所有编号的sheet\n\t * @return this\n\t * @throws POIException POI异常\n\t */\n\tdefault T read(InputStream in, int rid) throws POIException{\n\t\treturn read(in, String.valueOf(rid));\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxUtil.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.date.DateTime;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.exceptions.DependencyException;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.NumberUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.ExcelDateUtil;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener;\nimport org.apache.poi.hssf.record.CellValueRecordInterface;\nimport org.apache.poi.ss.usermodel.DataFormatter;\nimport org.apache.poi.util.XMLHelper;\nimport org.apache.poi.xssf.model.SharedStrings;\nimport org.apache.poi.xssf.usermodel.XSSFRichTextString;\nimport org.xml.sax.ContentHandler;\nimport org.xml.sax.InputSource;\nimport org.xml.sax.SAXException;\nimport org.xml.sax.XMLReader;\n\nimport javax.xml.parsers.ParserConfigurationException;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Sax方式读取Excel相关工具类\n *\n * @author looly\n */\npublic class ExcelSaxUtil {\n\n\t// 填充字符串\n\tpublic static final char CELL_FILL_CHAR = '@';\n\t// 列的最大位数\n\tpublic static final int MAX_CELL_BIT = 3;\n\n\t/**\n\t * 创建 {@link ExcelSaxReader}\n\t *\n\t * @param isXlsx     是否为xlsx格式（07格式）\n\t * @param rowHandler 行处理器\n\t * @return {@link ExcelSaxReader}\n\t * @since 5.4.4\n\t */\n\tpublic static ExcelSaxReader<?> createSaxReader(boolean isXlsx, RowHandler rowHandler) {\n\t\treturn isXlsx\n\t\t\t? new Excel07SaxReader(rowHandler)\n\t\t\t: new Excel03SaxReader(rowHandler);\n\t}\n\n\t/**\n\t * 根据数据类型获取数据\n\t *\n\t * @param cellDataType  数据类型枚举\n\t * @param value         数据值\n\t * @param sharedStrings {@link SharedStrings}\n\t * @param numFmtString  数字格式名\n\t * @return 数据值\n\t */\n\tpublic static Object getDataValue(CellDataType cellDataType, String value, SharedStrings sharedStrings, String numFmtString) {\n\t\tif (null == value) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (null == cellDataType) {\n\t\t\tcellDataType = CellDataType.NULL;\n\t\t}\n\n\t\tObject result;\n\t\tswitch (cellDataType) {\n\t\t\tcase BOOL:\n\t\t\t\tresult = (value.charAt(0) != '0');\n\t\t\t\tbreak;\n\t\t\tcase ERROR:\n\t\t\t\tresult = StrUtil.format(\"\\\\\\\"ERROR: {} \", value);\n\t\t\t\tbreak;\n\t\t\tcase FORMULA:\n\t\t\t\tresult = StrUtil.format(\"\\\"{}\\\"\", value);\n\t\t\t\tbreak;\n\t\t\tcase INLINESTR:\n\t\t\t\tresult = new XSSFRichTextString(value).toString();\n\t\t\t\tbreak;\n\t\t\tcase SSTINDEX:\n\t\t\t\ttry {\n\t\t\t\t\tfinal int index = Integer.parseInt(value);\n\t\t\t\t\tresult = sharedStrings.getItemAt(index).getString();\n\t\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t\tresult = value;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase NUMBER:\n\t\t\t\ttry {\n\t\t\t\t\tresult = getNumberValue(value, numFmtString);\n\t\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t\tresult = value;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase DATE:\n\t\t\t\ttry {\n\t\t\t\t\tresult = getDateValue(value);\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tresult = value;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tresult = value;\n\t\t\t\tbreak;\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * 格式化数字或日期值\n\t *\n\t * @param value        值\n\t * @param numFmtIndex  数字格式索引\n\t * @param numFmtString 数字格式名\n\t * @return 格式化后的值\n\t */\n\tpublic static String formatCellContent(String value, int numFmtIndex, String numFmtString) {\n\t\tif (null != numFmtString) {\n\t\t\ttry {\n\t\t\t\tvalue = new DataFormatter().formatRawCellContents(Double.parseDouble(value), numFmtIndex, numFmtString);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 计算两个单元格之间的单元格数目(同一行)\n\t *\n\t * @param preRef 前一个单元格位置，例如A1\n\t * @param ref    当前单元格位置，例如A8\n\t * @return 同一行中两个单元格之间的空单元格数\n\t */\n\tpublic static int countNullCell(String preRef, String ref) {\n\t\t// excel2007最大行数是1048576，最大列数是16384，最后一列列名是XFD\n\t\t// 数字代表列，去掉列信息\n\t\tString preXfd = StrUtil.nullToDefault(preRef, \"@\").replaceAll(\"\\\\d+\", \"\");\n\t\tString xfd = StrUtil.nullToDefault(ref, \"@\").replaceAll(\"\\\\d+\", \"\");\n\n\t\t// A表示65，@表示64，如果A算作1，那@代表0\n\t\t// 填充最大位数3\n\t\tpreXfd = StrUtil.fillBefore(preXfd, CELL_FILL_CHAR, MAX_CELL_BIT);\n\t\txfd = StrUtil.fillBefore(xfd, CELL_FILL_CHAR, MAX_CELL_BIT);\n\n\t\tchar[] preLetter = preXfd.toCharArray();\n\t\tchar[] letter = xfd.toCharArray();\n\t\t// 用字母表示则最多三位，每26个字母进一位\n\t\tint res = (letter[0] - preLetter[0]) * 26 * 26 + (letter[1] - preLetter[1]) * 26 + (letter[2] - preLetter[2]);\n\t\treturn res - 1;\n\t}\n\n\t/**\n\t * 从Excel的XML文档中读取内容，并使用{@link ContentHandler}处理\n\t *\n\t * @param xmlDocStream Excel的XML文档流\n\t * @param handler      文档内容处理接口，实现此接口用于回调处理数据\n\t * @throws DependencyException 依赖异常\n\t * @throws POIException        POI异常，包装了SAXException\n\t * @throws IORuntimeException  IO异常，如流关闭或异常等\n\t * @since 5.1.4\n\t */\n\tpublic static void readFrom(InputStream xmlDocStream, ContentHandler handler) throws DependencyException, POIException, IORuntimeException {\n\t\tXMLReader xmlReader;\n\t\ttry {\n\t\t\txmlReader = XMLHelper.newXMLReader();\n\t\t} catch (SAXException | ParserConfigurationException e) {\n\t\t\tif (e.getMessage().contains(\"org.apache.xerces.parsers.SAXParser\")) {\n\t\t\t\tthrow new DependencyException(e, \"You need to add 'xerces:xercesImpl' to your project and version >= 2.11.0\");\n\t\t\t} else {\n\t\t\t\tthrow new POIException(e);\n\t\t\t}\n\t\t}\n\t\txmlReader.setContentHandler(handler);\n\t\ttry {\n\t\t\txmlReader.parse(new InputSource(xmlDocStream));\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} catch (SAXException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (final StopReadException e) {\n\t\t\t// issue#3820 跳过，用户抛出此异常，表示强制结束读取\n\t\t}\n\t}\n\n\t/**\n\t * 判断数字Record中是否为日期格式\n\t *\n\t * @param cell           单元格记录\n\t * @param formatListener {@link FormatTrackingHSSFListener}\n\t * @return 是否为日期格式\n\t * @since 5.4.8\n\t */\n\tpublic static boolean isDateFormat(CellValueRecordInterface cell, FormatTrackingHSSFListener formatListener) {\n\t\tfinal int formatIndex = formatListener.getFormatIndex(cell);\n\t\tfinal String formatString = formatListener.getFormatString(cell);\n\t\treturn isDateFormat(formatIndex, formatString);\n\t}\n\n\t/**\n\t * 判断日期格式\n\t *\n\t * @param formatIndex  格式索引，一般用于内建格式\n\t * @param formatString 格式字符串\n\t * @return 是否为日期格式\n\t * @see ExcelDateUtil#isDateFormat(int, String)\n\t * @since 5.5.3\n\t */\n\tpublic static boolean isDateFormat(int formatIndex, String formatString) {\n\t\treturn ExcelDateUtil.isDateFormat(formatIndex, formatString);\n\t}\n\n\t/**\n\t * 获取日期\n\t *\n\t * @param value 单元格值\n\t * @return 日期\n\t * @since 5.3.6\n\t */\n\tpublic static DateTime getDateValue(String value) {\n\t\treturn getDateValue(Double.parseDouble(value));\n\t}\n\n\t/**\n\t * 获取日期\n\t *\n\t * @param value 单元格值\n\t * @return 日期\n\t * @since 4.1.0\n\t */\n\tpublic static DateTime getDateValue(double value) {\n\t\treturn DateUtil.date(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(value, false));\n\t}\n\n\t/**\n\t * 在Excel03 sax读取中获取日期或数字类型的结果值\n\t *\n\t * @param cell           记录单元格\n\t * @param value          值\n\t * @param formatListener {@link FormatTrackingHSSFListener}\n\t * @return 值，可能为Date或Double或Long\n\t * @since 5.5.0\n\t */\n\tpublic static Object getNumberOrDateValue(CellValueRecordInterface cell, double value, FormatTrackingHSSFListener formatListener) {\n\t\tif (isDateFormat(cell, formatListener)) {\n\t\t\t// 可能为日期格式\n\t\t\treturn getDateValue(value);\n\t\t}\n\t\treturn getNumberValue(value, formatListener.getFormatString(cell));\n\t}\n\n\t/**\n\t * 获取数字类型值\n\t *\n\t * @param value        值\n\t * @param numFmtString 格式\n\t * @return 数字，可以是Double、Long\n\t * @since 4.1.0\n\t */\n\tprivate static Number getNumberValue(String value, String numFmtString) {\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// issue#IB0EJ9 可能精度丢失，对含有小数的value判断并转为BigDecimal\n\t\tfinal double number = Double.parseDouble(value);\n\t\tif (StrUtil.contains(value, CharUtil.DOT) && !value.equals(Double.toString(number))) {\n\t\t\t// 精度丢失\n\t\t\treturn NumberUtil.toBigDecimal(value);\n\t\t}\n\n\t\treturn getNumberValue(number, numFmtString);\n\t}\n\n\t/**\n\t * 获取数字类型值，除非格式中明确数字保留小数，否则无小数情况下按照long返回\n\t *\n\t * @param numValue     值\n\t * @param numFmtString 格式\n\t * @return 数字，可以是Double、Long\n\t * @since 5.5.3\n\t */\n\tprivate static Number getNumberValue(double numValue, String numFmtString) {\n\t\t// 普通数字\n\t\tif (null != numFmtString && false == StrUtil.contains(numFmtString, CharUtil.DOT)) {\n\t\t\tfinal long longPart = (long) numValue;\n\t\t\t//noinspection RedundantIfStatement\n\t\t\tif (longPart == numValue) {\n\t\t\t\t// 对于无小数部分的数字类型，转为Long\n\t\t\t\treturn longPart;\n\t\t\t}\n\t\t}\n\t\treturn numValue;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.text.StrBuilder;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.cell.FormulaCellValue;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport org.apache.poi.ss.usermodel.BuiltinFormats;\nimport org.apache.poi.xssf.model.SharedStrings;\nimport org.apache.poi.xssf.model.StylesTable;\nimport org.apache.poi.xssf.usermodel.XSSFCellStyle;\nimport org.xml.sax.Attributes;\nimport org.xml.sax.helpers.DefaultHandler;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * sheetData标签内容读取处理器\n *\n * <pre>{@code\n *   <sheetData></sheetData>\n * }</pre>\n *\n * @since 5.5.3\n */\npublic class SheetDataSaxHandler extends DefaultHandler {\n\n\t// 单元格的格式表，对应style.xml\n\tprotected StylesTable stylesTable;\n\t// excel 2007 的共享字符串表,对应sharedString.xml\n\tprotected SharedStrings sharedStrings;\n\t// sheet的索引，从0开始\n\tprotected int sheetIndex;\n\n\t// 当前非空行\n\tprotected int index;\n\t// 当前列\n\tprivate int curCell;\n\t// 单元数据类型\n\tprivate CellDataType cellDataType;\n\t// 当前行号，从0开始\n\tprivate long rowNumber;\n\t// 当前列坐标， 如A1，B5\n\tprivate String curCoordinate;\n\t// 当前节点名称\n\tprivate ElementName curElementName;\n\t// 前一个列的坐标\n\tprivate String preCoordinate;\n\t// 行的最大列坐标\n\tprivate String maxCellCoordinate;\n\t// 单元格样式\n\tprivate XSSFCellStyle xssfCellStyle;\n\t// 单元格存储的格式化字符串，nmtFmt的formatCode属性的值\n\tprivate String numFmtString;\n\t// 是否处于sheetData标签内，sax只解析此标签内的内容，其它标签忽略\n\tprivate boolean isInSheetData;\n\n\t// 上一次的内容\n\tprivate final StrBuilder lastContent = StrUtil.strBuilder();\n\t// 上一次的内容\n\tprivate final StrBuilder lastFormula = StrUtil.strBuilder();\n\t// 存储每行的列元素\n\tprivate List<Object> rowCellList = new ArrayList<>();\n\n\t/**\n\t * 构造\n\t *\n\t * @param rowHandler 行处理器\n\t */\n\tpublic SheetDataSaxHandler(RowHandler rowHandler) {\n\t\tthis.rowHandler = rowHandler;\n\t}\n\n\t/**\n\t * 行处理器\n\t */\n\tprotected RowHandler rowHandler;\n\n\t/**\n\t * 设置行处理器\n\t *\n\t * @param rowHandler 行处理器\n\t */\n\tpublic void setRowHandler(RowHandler rowHandler) {\n\t\tthis.rowHandler = rowHandler;\n\t}\n\n\t/**\n\t * 读到一个xml开始标签时的回调处理方法\n\t */\n\t@Override\n\tpublic void startElement(String uri, String localName, String qName, Attributes attributes) {\n\t\tif (\"sheetData\".equals(qName)) {\n\t\t\tthis.isInSheetData = true;\n\t\t\treturn;\n\t\t}\n\n\t\tif (false == this.isInSheetData) {\n\t\t\t// 非sheetData标签，忽略解析\n\t\t\treturn;\n\t\t}\n\n\t\tfinal ElementName name = ElementName.of(qName);\n\t\tthis.curElementName = name;\n\n\t\tif (null != name) {\n\t\t\tswitch (name) {\n\t\t\t\tcase row:\n\t\t\t\t\t// 行开始\n\t\t\t\t\tstartRow(attributes);\n\t\t\t\t\tbreak;\n\t\t\t\tcase c:\n\t\t\t\t\t// 单元格元素\n\t\t\t\t\tstartCell(attributes);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 标签结束的回调处理方法\n\t */\n\t@Override\n\tpublic void endElement(String uri, String localName, String qName) {\n\t\tif (\"sheetData\".equals(qName)) {\n\t\t\t// sheetData结束，不再解析别的标签\n\t\t\tthis.isInSheetData = false;\n\t\t\treturn;\n\t\t}\n\n\t\tif (false == this.isInSheetData) {\n\t\t\t// 非sheetData标签，忽略解析\n\t\t\treturn;\n\t\t}\n\n\t\tthis.curElementName = null;\n\t\tif (ElementName.c.match(qName)) { // 单元格结束\n\t\t\tendCell();\n\t\t} else if (ElementName.row.match(qName)) {// 行结束\n\t\t\tendRow();\n\t\t}\n\t\t// 其它标签忽略\n\t}\n\n\t@Override\n\tpublic void characters(char[] ch, int start, int length) {\n\t\tif (false == this.isInSheetData) {\n\t\t\t// 非sheetData标签，忽略解析\n\t\t\treturn;\n\t\t}\n\n\t\tfinal ElementName elementName = this.curElementName;\n\t\tif (null != elementName) {\n\t\t\tswitch (elementName) {\n\t\t\t\tcase v:\n\t\t\t\t\t// 得到单元格内容的值\n\t\t\t\t\tlastContent.append(ch, start, length);\n\t\t\t\t\tbreak;\n\t\t\t\tcase f:\n\t\t\t\t\t// 得到单元格内容的值\n\t\t\t\t\tlastFormula.append(ch, start, length);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\t// 按理说内容应该为\"<v>内容</v>\"，但是某些特别的XML内容不在v或f标签中，此处做一些兼容\n\t\t\t// issue#1303@Github\n\t\t\tlastContent.append(ch, start, length);\n\t\t}\n\t}\n\n\t// --------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 行开始\n\t *\n\t * @param attributes 属性列表\n\t */\n\tprivate void startRow(Attributes attributes) {\n\t\tfinal String rValue = AttributeName.r.getValue(attributes);\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I6WYF6\n\t\tthis.rowNumber = (null == rValue) ? -1 : Long.parseLong(rValue) - 1;\n\t}\n\n\t/**\n\t * 单元格开始\n\t *\n\t * @param attributes 属性列表\n\t */\n\tprivate void startCell(Attributes attributes) {\n\t\t// 获取当前列坐标\n\t\tfinal String tempCurCoordinate = AttributeName.r.getValue(attributes);\n\t\t// 前一列为null，则将其设置为\"@\",A为第一列，ascii码为65，前一列即为@，ascii码64\n\t\tif (preCoordinate == null) {\n\t\t\tpreCoordinate = String.valueOf(ExcelSaxUtil.CELL_FILL_CHAR);\n\t\t} else {\n\t\t\t// 存在，则前一列要设置为上一列的坐标\n\t\t\tpreCoordinate = curCoordinate;\n\t\t}\n\t\t// 重置当前列\n\t\tcurCoordinate = tempCurCoordinate;\n\t\t// 设置单元格类型\n\t\tsetCellType(attributes);\n\n\t\t// 清空之前的数据\n\t\tlastContent.reset();\n\t\tlastFormula.reset();\n\t}\n\n\t/**\n\t * 一行结尾\n\t */\n\tprivate void endRow() {\n\t\t// 最大列坐标以第一个非空行的为准\n\t\tif (index == 0) {\n\t\t\tmaxCellCoordinate = curCoordinate;\n\t\t}\n\n\t\t// 补全一行尾部可能缺失的单元格\n\t\tif (maxCellCoordinate != null) {\n\t\t\tfillBlankCell(curCoordinate, maxCellCoordinate, true);\n\t\t}\n\n\t\trowHandler.handle(sheetIndex, rowNumber, rowCellList);\n\n\t\t// 一行结束\n\t\t// 新建一个新列，之前的列抛弃（可能被回收或rowHandler处理）\n\t\trowCellList = new ArrayList<>(curCell + 1);\n\t\t// 行数增加\n\t\tindex++;\n\t\t// 当前列置0\n\t\tcurCell = 0;\n\t\t// 置空当前列坐标和前一列坐标\n\t\tcurCoordinate = null;\n\t\tpreCoordinate = null;\n\t}\n\n\t/**\n\t * 一个单元格结尾\n\t */\n\tprivate void endCell() {\n\t\t// 补全单元格之间的空格\n\t\tfillBlankCell(preCoordinate, curCoordinate, false);\n\n\t\tfinal String contentStr = StrUtil.trim(lastContent);\n\t\tObject value = ExcelSaxUtil.getDataValue(this.cellDataType, contentStr, this.sharedStrings, this.numFmtString);\n\t\tif (false == this.lastFormula.isEmpty()) {\n\t\t\tvalue = new FormulaCellValue(StrUtil.trim(lastFormula), value);\n\t\t}\n\t\taddCellValue(curCell++, value);\n\t}\n\n\t/**\n\t * 在一行中的指定列增加值\n\t *\n\t * @param index 位置\n\t * @param value 值\n\t */\n\tprivate void addCellValue(int index, Object value) {\n\t\tthis.rowCellList.add(index, value);\n\t\tthis.rowHandler.handleCell(this.sheetIndex, this.rowNumber, index, value, this.xssfCellStyle);\n\t}\n\n\t/**\n\t * 填充空白单元格，如果前一个单元格大于后一个，不需要填充<br>\n\t *\n\t * @param preCoordinate 前一个单元格坐标\n\t * @param curCoordinate 当前单元格坐标\n\t * @param isEnd         是否为最后一个单元格\n\t */\n\tprivate void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) {\n\t\tif (false == curCoordinate.equals(preCoordinate)) {\n\t\t\tint len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate);\n\t\t\tif (isEnd) {\n\t\t\t\tlen++;\n\t\t\t}\n\t\t\twhile (len-- > 0) {\n\t\t\t\taddCellValue(curCell++, StrUtil.EMPTY);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 设置单元格的类型\n\t *\n\t * @param attributes 属性\n\t */\n\tprivate void setCellType(Attributes attributes) {\n\t\t// numFmtString的值\n\t\tnumFmtString = StrUtil.EMPTY;\n\t\tthis.cellDataType = CellDataType.of(AttributeName.t.getValue(attributes));\n\n\t\t// 获取单元格的xf索引，对应style.xml中cellXfs的子元素xf\n\t\tif (null != this.stylesTable) {\n\t\t\tfinal String xfIndexStr = AttributeName.s.getValue(attributes);\n\t\t\tif (null != xfIndexStr) {\n\t\t\t\tthis.xssfCellStyle = stylesTable.getStyleAt(Integer.parseInt(xfIndexStr));\n\t\t\t\t// 单元格存储格式的索引，对应style.xml中的numFmts元素的子元素索引\n\t\t\t\tfinal int numFmtIndex = xssfCellStyle.getDataFormat();\n\t\t\t\tthis.numFmtString = ObjectUtil.defaultIfNull(\n\t\t\t\t\txssfCellStyle.getDataFormatString(),\n\t\t\t\t\t() -> BuiltinFormats.getBuiltinFormat(numFmtIndex));\n\t\t\t\tif (CellDataType.NUMBER == this.cellDataType && ExcelSaxUtil.isDateFormat(numFmtIndex, numFmtString)) {\n\t\t\t\t\tcellDataType = CellDataType.DATE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t// --------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetRidReader.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.openxml4j.exceptions.InvalidFormatException;\nimport org.apache.poi.xssf.eventusermodel.XSSFReader;\nimport org.xml.sax.Attributes;\nimport org.xml.sax.helpers.DefaultHandler;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 在Sax方式读取Excel时，读取sheet标签中sheetId和rid的对应关系，类似于:\n * <pre>\n * &lt;sheet name=\"Sheet6\" sheetId=\"4\" r:id=\"rId6\"/&gt;\n * </pre>\n * <p>\n * 读取结果为：\n *\n * <pre>\n *     {\"4\": \"6\"}\n * </pre>\n *\n * @author looly\n * @since 5.4.4\n */\npublic class SheetRidReader extends DefaultHandler {\n\n\t/**\n\t * 从{@link XSSFReader}中解析sheet名、sheet id等相关信息\n\t *\n\t * @param reader {@link XSSFReader}\n\t * @return SheetRidReader\n\t * @since 5.7.17\n\t */\n\tpublic static SheetRidReader parse(XSSFReader reader) {\n\t\treturn new SheetRidReader().read(reader);\n\t}\n\n\tprivate final static String TAG_NAME = \"sheet\";\n\tprivate final static String RID_ATTR = \"r:id\";\n\tprivate final static String SHEET_ID_ATTR = \"sheetId\";\n\tprivate final static String NAME_ATTR = \"name\";\n\n\tprivate final Map<Integer, Integer> ID_RID_MAP = new LinkedHashMap<>();\n\tprivate final Map<String, Integer> NAME_RID_MAP = new LinkedHashMap<>();\n\n\t/**\n\t * 读取Wordkbook的XML中sheet标签中sheetId和rid的对应关系\n\t *\n\t * @param xssfReader XSSF读取器\n\t * @return this\n\t */\n\tpublic SheetRidReader read(XSSFReader xssfReader) {\n\t\tInputStream workbookData = null;\n\t\ttry {\n\t\t\tworkbookData = xssfReader.getWorkbookData();\n\t\t\tExcelSaxUtil.readFrom(workbookData, this);\n\t\t} catch (InvalidFormatException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(workbookData);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据sheetId获取rid，从1开始\n\t *\n\t * @param sheetId Sheet的ID，从1开始\n\t * @return rid，从1开始\n\t */\n\tpublic Integer getRidBySheetId(int sheetId) {\n\t\treturn ID_RID_MAP.get(sheetId);\n\t}\n\n\t/**\n\t * 根据sheetId获取rid，从0开始\n\t *\n\t * @param sheetId Sheet的ID，从0开始\n\t * @return rid，从0开始\n\t * @since 5.5.5\n\t */\n\tpublic Integer getRidBySheetIdBase0(int sheetId) {\n\t\tfinal Integer rid = getRidBySheetId(sheetId + 1);\n\t\tif (null != rid) {\n\t\t\treturn rid - 1;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 根据sheet name获取rid，从1开始\n\t *\n\t * @param sheetName Sheet的name\n\t * @return rid，从1开始\n\t */\n\tpublic Integer getRidByName(String sheetName) {\n\t\treturn NAME_RID_MAP.get(sheetName);\n\t}\n\n\t/**\n\t * 根据sheet name获取rid，从0开始\n\t *\n\t * @param sheetName Sheet的name\n\t * @return rid，从0开始\n\t * @since 5.5.5\n\t */\n\tpublic Integer getRidByNameBase0(String sheetName) {\n\t\tfinal Integer rid = getRidByName(sheetName);\n\t\tif (null != rid) {\n\t\t\treturn rid - 1;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 通过sheet的序号获取rid\n\t *\n\t * @param index 序号，从0开始\n\t * @return rid\n\t * @since 5.5.7\n\t */\n\tpublic Integer getRidByIndex(int index) {\n\t\treturn CollUtil.get(this.NAME_RID_MAP.values(), index);\n\t}\n\n\t/**\n\t * 通过sheet的序号获取rid\n\t *\n\t * @param index 序号，从0开始\n\t * @return rid，从0开始\n\t * @since 5.5.7\n\t */\n\tpublic Integer getRidByIndexBase0(int index) {\n\t\tfinal Integer rid = CollUtil.get(this.NAME_RID_MAP.values(), index);\n\t\tif (null != rid) {\n\t\t\treturn rid - 1;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取所有sheet名称\n\t *\n\t * @return sheet名称\n\t * @since 5.7.17\n\t */\n\tpublic List<String> getSheetNames() {\n\t\treturn ListUtil.toList(this.NAME_RID_MAP.keySet());\n\t}\n\n\t@Override\n\tpublic void startElement(String uri, String localName, String qName, Attributes attributes) {\n\t\tif (TAG_NAME.equalsIgnoreCase(localName)) {\n\t\t\tfinal String ridStr = attributes.getValue(RID_ATTR);\n\t\t\tif (StrUtil.isEmpty(ridStr)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfinal int rid = Integer.parseInt(StrUtil.removePrefixIgnoreCase(ridStr, Excel07SaxReader.RID_PREFIX));\n\n\t\t\t// sheet名和rid映射\n\t\t\tfinal String name = attributes.getValue(NAME_ATTR);\n\t\t\tif (StrUtil.isNotEmpty(name)) {\n\t\t\t\tNAME_RID_MAP.put(name, rid);\n\t\t\t}\n\n\t\t\t// sheetId和rid映射\n\t\t\tfinal String sheetIdStr = attributes.getValue(SHEET_ID_ATTR);\n\t\t\tif (StrUtil.isNotEmpty(sheetIdStr)) {\n\t\t\t\tID_RID_MAP.put(Integer.parseInt(sheetIdStr), rid);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/StopReadException.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.poi.exceptions.POIException;\n\n/**\n * 读取结束异常，用于标记读取结束<br>\n * Sax方式读取时，如果用户在RowHandler中抛出此异常，表示读取结束，此时不再读取其他数据\n *\n * @author Looly\n * @since 5.8.35\n */\npublic class StopReadException extends POIException {\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 构造\n\t *\n\t */\n\tpublic StopReadException() {\n\t\tthis(\"Stop read by user.\");\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param message 消息\n\t */\n\tpublic StopReadException(final String message) {\n\t\tsuper(message);\n\t\t// 去除堆栈\n\t\tsetStackTrace(new StackTraceElement[0]);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/AbstractRowHandler.java",
    "content": "package cn.hutool.poi.excel.sax.handler;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.func.Func1;\n\nimport java.util.List;\n\n/**\n * 抽象行数据处理器，通过实现{@link #handle(int, long, List)} 处理原始数据<br>\n * 并调用{@link #handleData(int, long, Object)}处理经过转换后的数据。\n *\n * @param <T> 转换后的数据类型\n * @author looly\n * @since 5.4.4\n */\npublic abstract class AbstractRowHandler<T> implements RowHandler {\n\n\t/**\n\t * 读取起始行（包含，从0开始计数）\n\t */\n\tprotected final int startRowIndex;\n\t/**\n\t * 读取结束行（包含，从0开始计数）\n\t */\n\tprotected final int endRowIndex;\n\t/**\n\t * 行数据转换函数\n\t */\n\tprotected Func1<List<Object>, T> convertFunc;\n\n\t/**\n\t * 构造\n\t *\n\t * @param startRowIndex 读取起始行（包含，从0开始计数）\n\t * @param endRowIndex 读取结束行（包含，从0开始计数）\n\t */\n\tpublic AbstractRowHandler(int startRowIndex, int endRowIndex) {\n\t\tthis.startRowIndex = startRowIndex;\n\t\tthis.endRowIndex = endRowIndex;\n\t}\n\n\t@Override\n\tpublic void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {\n\t\tAssert.notNull(convertFunc);\n\t\tif (rowIndex < this.startRowIndex || rowIndex > this.endRowIndex) {\n\t\t\treturn;\n\t\t}\n\t\thandleData(sheetIndex, rowIndex, convertFunc.callWithRuntimeException(rowCells));\n\t}\n\n\t/**\n\t * 处理转换后的数据\n\t *\n\t * @param sheetIndex 当前Sheet序号\n\t * @param rowIndex   当前行号，从0开始计数\n\t * @param data       行数据\n\t */\n\tpublic abstract void handleData(int sheetIndex, long rowIndex, T data);\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/BeanRowHandler.java",
    "content": "package cn.hutool.poi.excel.sax.handler;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\n\nimport java.util.List;\n\n/**\n * Bean形式的行处理器<br>\n * 将一行数据转换为Map，key为指定行，value为当前行对应位置的值\n *\n * @author looly\n * @since 5.4.4\n */\npublic abstract class BeanRowHandler<T> extends AbstractRowHandler<T> {\n\n\t/**\n\t * 标题所在行（从0开始计数）\n\t */\n\tprivate final int headerRowIndex;\n\t/**\n\t * 标题行\n\t */\n\tList<String> headerList;\n\n\t/**\n\t * 构造\n\t *\n\t * @param headerRowIndex 标题所在行（从0开始计数）\n\t * @param startRowIndex  读取起始行（包含，从0开始计数）\n\t * @param endRowIndex    读取结束行（包含，从0开始计数）\n\t * @param clazz          Bean类型\n\t */\n\tpublic BeanRowHandler(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> clazz) {\n\t\tsuper(startRowIndex, endRowIndex);\n\t\tAssert.isTrue(headerRowIndex <= startRowIndex, \"Header row must before the start row!\");\n\t\tthis.headerRowIndex = headerRowIndex;\n\t\tthis.convertFunc = (rowList) -> BeanUtil.toBean(IterUtil.toMap(headerList, rowList), clazz);\n\t}\n\n\t@Override\n\tpublic void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {\n\t\tif (rowIndex == this.headerRowIndex) {\n\t\t\tthis.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowCells));\n\t\t\treturn;\n\t\t}\n\t\tsuper.handle(sheetIndex, rowIndex, rowCells);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/MapRowHandler.java",
    "content": "package cn.hutool.poi.excel.sax.handler;\n\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.convert.Convert;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Map形式的行处理器<br>\n * 将一行数据转换为Map，key为指定行，value为当前行对应位置的值\n *\n * @author looly\n * @since 5.4.4\n */\npublic abstract class MapRowHandler extends AbstractRowHandler<Map<String, Object>> {\n\n\t/**\n\t * 标题所在行（从0开始计数）\n\t */\n\tprivate final int headerRowIndex;\n\t/**\n\t * 标题行\n\t */\n\tList<String> headerList;\n\n\t/**\n\t * 构造\n\t *\n\t * @param headerRowIndex 标题所在行（从0开始计数）\n\t * @param startRowIndex 读取起始行（包含，从0开始计数）\n\t * @param endRowIndex 读取结束行（包含，从0开始计数）\n\t */\n\tpublic MapRowHandler(int headerRowIndex, int startRowIndex, int endRowIndex){\n\t\tsuper(startRowIndex, endRowIndex);\n\t\tthis.headerRowIndex = headerRowIndex;\n\t\tthis.convertFunc = (rowList)-> IterUtil.toMap(headerList, rowList, true);\n\t}\n\n\t@Override\n\tpublic void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {\n\t\tif (rowIndex == this.headerRowIndex) {\n\t\t\tthis.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowCells));\n\t\t\treturn;\n\t\t}\n\t\tsuper.handle(sheetIndex, rowIndex, rowCells);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/RowHandler.java",
    "content": "package cn.hutool.poi.excel.sax.handler;\n\nimport org.apache.poi.ss.usermodel.CellStyle;\n\nimport java.util.List;\n\n/**\n * Sax方式读取Excel行处理器\n *\n * @author looly\n */\n@FunctionalInterface\npublic interface RowHandler {\n\n\t/**\n\t * 处理一行数据\n\t *\n\t * @param sheetIndex 当前Sheet序号\n\t * @param rowIndex   当前行号，从0开始计数\n\t * @param rowCells   行数据，每个Object表示一个单元格的值\n\t */\n\tvoid handle(int sheetIndex, long rowIndex, List<Object> rowCells);\n\n\t/**\n\t * 处理一个单元格的数据\n\t *\n\t * @param sheetIndex    当前Sheet序号\n\t * @param rowIndex      当前行号\n\t * @param cellIndex     当前列号\n\t * @param value         单元格的值\n\t * @param xssfCellStyle 单元格样式\n\t */\n\tdefault void handleCell(int sheetIndex, long rowIndex, int cellIndex, Object value, CellStyle xssfCellStyle) {\n\t\t//pass\n\t}\n\n\t/**\n\t * 处理一个sheet页完成的操作\n\t */\n\tdefault void doAfterAllAnalysed() {\n\t\t//pass\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/package-info.java",
    "content": "/**\n * Sax读取中行处理器的定义和实现\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.excel.sax.handler;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/sax/package-info.java",
    "content": "/**\n * Sax方式操作Excel方式的封装\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.excel.sax;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/style/Align.java",
    "content": "package cn.hutool.poi.excel.style;\n\n/**\n * 对齐方式枚举\n * \n * @author looly\n * @since 4.1.0\n */\npublic enum Align {\n\tLEFT, RIGHT, CENTER\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/style/StyleUtil.java",
    "content": "package cn.hutool.poi.excel.style;\n\nimport cn.hutool.core.util.StrUtil;\nimport org.apache.poi.ss.usermodel.BorderStyle;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.DataFormat;\nimport org.apache.poi.ss.usermodel.FillPatternType;\nimport org.apache.poi.ss.usermodel.Font;\nimport org.apache.poi.ss.usermodel.HorizontalAlignment;\nimport org.apache.poi.ss.usermodel.IndexedColors;\nimport org.apache.poi.ss.usermodel.VerticalAlignment;\nimport org.apache.poi.ss.usermodel.Workbook;\n\n/**\n * Excel样式工具类\n *\n * @author looly\n * @since 4.0.0\n */\npublic class StyleUtil {\n\n\t/**\n\t * 克隆新的{@link CellStyle}\n\t *\n\t * @param cell      单元格\n\t * @param cellStyle 被复制的样式\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle cloneCellStyle(Cell cell, CellStyle cellStyle) {\n\t\treturn cloneCellStyle(cell.getSheet().getWorkbook(), cellStyle);\n\t}\n\n\t/**\n\t * 克隆新的{@link CellStyle}\n\t *\n\t * @param workbook  工作簿\n\t * @param cellStyle 被复制的样式\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle cloneCellStyle(Workbook workbook, CellStyle cellStyle) {\n\t\tfinal CellStyle newCellStyle = createCellStyle(workbook);\n\t\tnewCellStyle.cloneStyleFrom(cellStyle);\n\t\treturn newCellStyle;\n\t}\n\n\t/**\n\t * 设置cell文本对齐样式\n\t *\n\t * @param cellStyle {@link CellStyle}\n\t * @param halign    横向位置\n\t * @param valign    纵向位置\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle setAlign(CellStyle cellStyle, HorizontalAlignment halign, VerticalAlignment valign) {\n\t\tcellStyle.setAlignment(halign);\n\t\tcellStyle.setVerticalAlignment(valign);\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 设置cell的四个边框粗细和颜色\n\t *\n\t * @param cellStyle  {@link CellStyle}\n\t * @param borderSize 边框粗细{@link BorderStyle}枚举\n\t * @param colorIndex 颜色的short值\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle setBorder(CellStyle cellStyle, BorderStyle borderSize, IndexedColors colorIndex) {\n\t\tcellStyle.setBorderBottom(borderSize);\n\t\tcellStyle.setBottomBorderColor(colorIndex.index);\n\n\t\tcellStyle.setBorderLeft(borderSize);\n\t\tcellStyle.setLeftBorderColor(colorIndex.index);\n\n\t\tcellStyle.setBorderRight(borderSize);\n\t\tcellStyle.setRightBorderColor(colorIndex.index);\n\n\t\tcellStyle.setBorderTop(borderSize);\n\t\tcellStyle.setTopBorderColor(colorIndex.index);\n\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 给cell设置颜色\n\t *\n\t * @param cellStyle   {@link CellStyle}\n\t * @param color       背景颜色\n\t * @param fillPattern 填充方式 {@link FillPatternType}枚举\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle setColor(CellStyle cellStyle, IndexedColors color, FillPatternType fillPattern) {\n\t\treturn setColor(cellStyle, color.index, fillPattern);\n\t}\n\n\t/**\n\t * 给cell设置颜色\n\t *\n\t * @param cellStyle   {@link CellStyle}\n\t * @param color       背景颜色\n\t * @param fillPattern 填充方式 {@link FillPatternType}枚举\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle setColor(CellStyle cellStyle, short color, FillPatternType fillPattern) {\n\t\tcellStyle.setFillForegroundColor(color);\n\t\tcellStyle.setFillPattern(fillPattern);\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 创建字体\n\t *\n\t * @param workbook {@link Workbook}\n\t * @param color    字体颜色\n\t * @param fontSize 字体大小\n\t * @param fontName 字体名称，可以为null使用默认字体\n\t * @return {@link Font}\n\t */\n\tpublic static Font createFont(Workbook workbook, short color, short fontSize, String fontName) {\n\t\tfinal Font font = workbook.createFont();\n\t\treturn setFontStyle(font, color, fontSize, fontName);\n\t}\n\n\t/**\n\t * 设置字体样式\n\t *\n\t * @param font     字体{@link Font}\n\t * @param color    字体颜色\n\t * @param fontSize 字体大小\n\t * @param fontName 字体名称，可以为null使用默认字体\n\t * @return {@link Font}\n\t */\n\tpublic static Font setFontStyle(Font font, short color, short fontSize, String fontName) {\n\t\tif (color > 0) {\n\t\t\tfont.setColor(color);\n\t\t}\n\t\tif (fontSize > 0) {\n\t\t\tfont.setFontHeightInPoints(fontSize);\n\t\t}\n\t\tif (StrUtil.isNotBlank(fontName)) {\n\t\t\tfont.setFontName(fontName);\n\t\t}\n\t\treturn font;\n\t}\n\n\t/**\n\t * 创建单元格样式\n\t *\n\t * @param workbook {@link Workbook} 工作簿\n\t * @return {@link CellStyle}\n\t * @see Workbook#createCellStyle()\n\t * @since 5.4.0\n\t */\n\tpublic static CellStyle createCellStyle(Workbook workbook) {\n\t\tif (null == workbook) {\n\t\t\treturn null;\n\t\t}\n\t\treturn workbook.createCellStyle();\n\t}\n\n\t/**\n\t * 创建默认普通单元格样式\n\t *\n\t * <pre>\n\t * 1. 文字上下左右居中\n\t * 2. 细边框，黑色\n\t * </pre>\n\t *\n\t * @param workbook {@link Workbook} 工作簿\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle createDefaultCellStyle(Workbook workbook) {\n\t\tfinal CellStyle cellStyle = createCellStyle(workbook);\n\t\tsetAlign(cellStyle, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);\n\t\tsetBorder(cellStyle, BorderStyle.THIN, IndexedColors.BLACK);\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 创建默认头部样式\n\t *\n\t * @param workbook {@link Workbook} 工作簿\n\t * @return {@link CellStyle}\n\t */\n\tpublic static CellStyle createHeadCellStyle(Workbook workbook) {\n\t\tfinal CellStyle cellStyle = createCellStyle(workbook);\n\t\tsetAlign(cellStyle, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);\n\t\tsetBorder(cellStyle, BorderStyle.THIN, IndexedColors.BLACK);\n\t\tsetColor(cellStyle, IndexedColors.GREY_25_PERCENT, FillPatternType.SOLID_FOREGROUND);\n\t\treturn cellStyle;\n\t}\n\n\t/**\n\t * 给定样式是否为null（无样式）或默认样式，默认样式为{@code workbook.getCellStyleAt(0)}\n\t *\n\t * @param workbook 工作簿\n\t * @param style    被检查的样式\n\t * @return 是否为null（无样式）或默认样式\n\t * @since 4.6.3\n\t */\n\tpublic static boolean isNullOrDefaultStyle(Workbook workbook, CellStyle style) {\n\t\treturn (null == style) || style.equals(workbook.getCellStyleAt(0));\n\t}\n\n\t/**\n\t * 创建数据格式并获取格式\n\t *\n\t * @param workbook {@link Workbook}\n\t * @param format   数据格式\n\t * @return 数据格式\n\t * @since 5.5.5\n\t */\n\tpublic static Short getFormat(Workbook workbook, String format) {\n\t\tfinal DataFormat dataFormat = workbook.createDataFormat();\n\t\treturn dataFormat.getFormat(format);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/excel/style/package-info.java",
    "content": "/**\n * Excel样式封装，入口为：StyleUtil\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.excel.style;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/exceptions/POIException.java",
    "content": "package cn.hutool.poi.exceptions;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * POI异常\n *\n * @author xiaoleilu\n */\npublic class POIException extends RuntimeException {\n\tprivate static final long serialVersionUID = 2711633732613506552L;\n\n\tpublic POIException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic POIException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic POIException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic POIException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic POIException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic POIException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/exceptions/package-info.java",
    "content": "/**\n * POI相关异常\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.exceptions;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java",
    "content": "package cn.hutool.poi.ofd;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.file.PathUtil;\nimport org.ofdrw.font.Font;\nimport org.ofdrw.layout.OFDDoc;\nimport org.ofdrw.layout.edit.Annotation;\nimport org.ofdrw.layout.element.Div;\nimport org.ofdrw.layout.element.Img;\nimport org.ofdrw.layout.element.Paragraph;\nimport org.ofdrw.reader.OFDReader;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.nio.file.Path;\n\n/**\n * OFD文件生成器\n *\n * @author looly\n * @since 5.5.3\n */\npublic class OfdWriter implements Serializable, Closeable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final OFDDoc doc;\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 生成的文件\n\t */\n\tpublic OfdWriter(File file) {\n\t\tthis(file.toPath());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param file 生成的文件\n\t */\n\tpublic OfdWriter(Path file) {\n\t\ttry {\n\t\t\tif(PathUtil.exists(file, true)){\n\t\t\t\tthis.doc = new OFDDoc(new OFDReader(file), file);\n\t\t\t} else{\n\t\t\t\tthis.doc = new OFDDoc(file);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param out 需要输出的流\n\t */\n\tpublic OfdWriter(OutputStream out) {\n\t\tthis.doc = new OFDDoc(out);\n\t}\n\n\t/**\n\t * 增加文本内容\n\t *\n\t * @param font  字体\n\t * @param texts 文本\n\t * @return this\n\t */\n\tpublic OfdWriter addText(Font font, String... texts) {\n\t\tfinal Paragraph paragraph = new Paragraph();\n\t\tif (null != font) {\n\t\t\tparagraph.setDefaultFont(font);\n\t\t}\n\t\tfor (String text : texts) {\n\t\t\tparagraph.add(text);\n\t\t}\n\t\treturn add(paragraph);\n\t}\n\n\t/**\n\t * 追加图片\n\t *\n\t * @param picFile 图片文件\n\t * @param width   宽度\n\t * @param height  高度\n\t * @return this\n\t */\n\tpublic OfdWriter addPicture(File picFile, int width, int height) {\n\t\treturn addPicture(picFile.toPath(), width, height);\n\t}\n\n\t/**\n\t * 追加图片\n\t *\n\t * @param picFile 图片文件\n\t * @param width   宽度\n\t * @param height  高度\n\t * @return this\n\t */\n\tpublic OfdWriter addPicture(Path picFile, int width, int height) {\n\t\tfinal Img img;\n\t\ttry {\n\t\t\timg = new Img(width, height, picFile);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn add(img);\n\t}\n\n\t/**\n\t * 增加节点\n\t *\n\t * @param div 节点，可以是段落、Canvas、Img或者填充\n\t * @return this\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic OfdWriter add(Div div) {\n\t\tthis.doc.add(div);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加注释，比如水印等\n\t *\n\t * @param page 页码\n\t * @param annotation 节点，可以是段落、Canvas、Img或者填充\n\t * @return this\n\t */\n\tpublic OfdWriter add(int page, Annotation annotation) {\n\t\ttry {\n\t\t\tthis.doc.addAnnotation(page, annotation);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.doc);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/ofd/package-info.java",
    "content": "/**\n * 开放版式文档（Open Fixed-layout Document ）封装，基于ofdrw（https://gitee.com/Trisia/ofdrw）\n * \n * @author looly\n */\npackage cn.hutool.poi.ofd;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/package-info.java",
    "content": "/**\n * POI封装实现<br>\n * Java针对MS Office的操作的库屈指可数，比较有名的就是Apache的POI库。<br>\n * 这个库异常强大，但是使用起来也并不容易。Hutool针对POI封装一些常用工具，使Java操作Excel等文件变得异常简单。\n * \n * @author looly\n *\n */\npackage cn.hutool.poi;"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/DocUtil.java",
    "content": "package cn.hutool.poi.word;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.openxml4j.exceptions.InvalidFormatException;\nimport org.apache.poi.openxml4j.opc.OPCPackage;\nimport org.apache.poi.xwpf.usermodel.XWPFDocument;\n\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * Word Document工具\n *\n * @author looly\n * @since 4.4.1\n */\npublic class DocUtil {\n\n\t/**\n\t * 创建{@link XWPFDocument}，如果文件已存在则读取之，否则创建新的\n\t *\n\t * @param file docx文件\n\t * @return {@link XWPFDocument}\n\t */\n\tpublic static XWPFDocument create(File file) {\n\t\ttry {\n\t\t\treturn FileUtil.exist(file) ? new XWPFDocument(OPCPackage.open(file)) : new XWPFDocument();\n\t\t} catch (InvalidFormatException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/PicType.java",
    "content": "package cn.hutool.poi.word;\n\nimport org.apache.poi.xwpf.usermodel.Document;\n\n/**\n * Word中的图片类型\n *\n * @author looly\n * @since 5.1.6\n */\npublic enum PicType {\n\tEMF(Document.PICTURE_TYPE_EMF),\n\tWMF(Document.PICTURE_TYPE_WMF),\n\tPICT(Document.PICTURE_TYPE_PICT),\n\tJPEG(Document.PICTURE_TYPE_JPEG),\n\tPNG(Document.PICTURE_TYPE_PNG),\n\tDIB(Document.PICTURE_TYPE_DIB),\n\tGIF(Document.PICTURE_TYPE_GIF),\n\tTIFF(Document.PICTURE_TYPE_TIFF),\n\tEPS(Document.PICTURE_TYPE_EPS),\n\tWPG(Document.PICTURE_TYPE_WPG);\n\n\t/**\n\t * 构造\n\t *\n\t * @param value 图片类型值\n\t */\n\tPicType(int value) {\n\t\tthis.value = value;\n\t}\n\n\tprivate final int value;\n\n\t/**\n\t * 获取图片类型对应值\n\t *\n\t * @return 图片值\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/TableUtil.java",
    "content": "package cn.hutool.poi.word;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.IterUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport org.apache.poi.xwpf.usermodel.XWPFDocument;\nimport org.apache.poi.xwpf.usermodel.XWPFTable;\nimport org.apache.poi.xwpf.usermodel.XWPFTableCell;\nimport org.apache.poi.xwpf.usermodel.XWPFTableRow;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Word中表格相关工具\n * \n * @author Looly\n * @since 4.5.14\n */\npublic class TableUtil {\n\t\n\t/**\n\t * 创建空表，只有一行\n\t * \n\t * @param doc {@link XWPFDocument}\n\t * @return {@link XWPFTable}\n\t */\n\tpublic static XWPFTable createTable(XWPFDocument doc) {\n\t\treturn createTable(doc, null);\n\t}\n\n\t/**\n\t * 创建表格并填充数据，默认表格\n\t * \n\t * @param doc {@link XWPFDocument}\n\t * @param data 数据\n\t * @return {@link XWPFTable}\n\t */\n\tpublic static XWPFTable createTable(XWPFDocument doc, Iterable<?> data) {\n\t\tAssert.notNull(doc, \"XWPFDocument must be not null !\");\n\t\tfinal XWPFTable table = doc.createTable();\n\t\t// 新建table的时候默认会新建一行，此处移除之\n\t\ttable.removeRow(0);\n\t\treturn writeTable(table, data);\n\t}\n\n\t/**\n\t * 为table填充数据\n\t *\n\t * @param table {@link XWPFTable}\n\t * @param data 数据\n\t * @return {@link XWPFTable}\n\t * @since 5.5.6\n\t */\n\tpublic static XWPFTable writeTable(XWPFTable table, Iterable<?> data){\n\t\tAssert.notNull(table, \"XWPFTable must be not null !\");\n\t\tif (IterUtil.isEmpty(data)) {\n\t\t\t// 数据为空，返回空表\n\t\t\treturn table;\n\t\t}\n\n\t\tboolean isFirst = true;\n\t\tfor (Object rowData : data) {\n\t\t\twriteRow(table.createRow(), rowData, isFirst);\n\t\t\tif(isFirst){\n\t\t\t\tisFirst = false;\n\t\t\t}\n\t\t}\n\n\t\treturn table;\n\t}\n\t\n\t/**\n\t * 写一行数据\n\t * \n\t * @param row 行\n\t * @param rowBean 行数据\n\t * @param isWriteKeyAsHead 如果为Map或者Bean，是否写标题\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\tpublic static void writeRow(XWPFTableRow row, Object rowBean, boolean isWriteKeyAsHead) {\n\t\tif (rowBean instanceof Iterable) {\n\t\t\twriteRow(row, (Iterable<?>) rowBean);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tMap rowMap;\n\t\tif(rowBean instanceof Map) {\n\t\t\trowMap = (Map) rowBean;\n\t\t} else if (BeanUtil.isBean(rowBean.getClass())) {\n\t\t\trowMap = BeanUtil.beanToMap(rowBean, new LinkedHashMap<>(), false, false);\n\t\t} else {\n\t\t\t// 其它转为字符串默认输出\n\t\t\twriteRow(row, CollUtil.newArrayList(rowBean), isWriteKeyAsHead);\n\t\t\treturn;\n\t\t}\n\n\t\twriteRow(row, rowMap, isWriteKeyAsHead);\n\t}\n\t\n\t/**\n\t * 写行数据\n\t * \n\t * @param row 行\n\t * @param rowMap 行数据\n\t * @param isWriteKeyAsHead 是否写标题\n\t */\n\tpublic static void writeRow(XWPFTableRow row, Map<?, ?> rowMap, boolean isWriteKeyAsHead) {\n\t\tif (MapUtil.isEmpty(rowMap)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (isWriteKeyAsHead) {\n\t\t\twriteRow(row, rowMap.keySet());\n\t\t\trow = row.getTable().createRow();\n\t\t}\n\t\twriteRow(row, rowMap.values());\n\t}\n\n\t/**\n\t * 写行数据\n\t * \n\t * @param row 行\n\t * @param rowData 行数据\n\t */\n\tpublic static void writeRow(XWPFTableRow row, Iterable<?> rowData) {\n\t\tXWPFTableCell cell;\n\t\tint index = 0;\n\t\tfor (Object cellData : rowData) {\n\t\t\tcell = getOrCreateCell(row, index);\n\t\t\tcell.setText(Convert.toStr(cellData));\n\t\t\tindex++;\n\t\t}\n\t}\n\n\t/**\n\t * 获取或创建新行<br>\n\t * 存在则直接返回，不存在创建新的行\n\t * \n\t * @param table {@link XWPFTable}\n\t * @param index 索引（行号），从0开始\n\t * @return {@link XWPFTableRow}\n\t */\n\tpublic static XWPFTableRow getOrCreateRow(XWPFTable table, int index) {\n\t\tXWPFTableRow row = table.getRow(index);\n\t\tif (null == row) {\n\t\t\trow = table.createRow();\n\t\t}\n\n\t\treturn row;\n\t}\n\n\t/**\n\t * 获取或创建新单元格<br>\n\t * 存在则直接返回，不存在创建新的单元格\n\t * \n\t * @param row {@link XWPFTableRow} 行\n\t * @param index index 索引（列号），从0开始\n\t * @return {@link XWPFTableCell}\n\t */\n\tpublic static XWPFTableCell getOrCreateCell(XWPFTableRow row, int index) {\n\t\tXWPFTableCell cell = row.getCell(index);\n\t\tif (null == cell) {\n\t\t\tcell = row.createCell();\n\t\t}\n\t\treturn cell;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/Word07Writer.java",
    "content": "package cn.hutool.poi.word;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.openxml4j.exceptions.InvalidFormatException;\nimport org.apache.poi.util.Units;\nimport org.apache.poi.xwpf.usermodel.ParagraphAlignment;\nimport org.apache.poi.xwpf.usermodel.XWPFDocument;\nimport org.apache.poi.xwpf.usermodel.XWPFParagraph;\nimport org.apache.poi.xwpf.usermodel.XWPFRun;\n\nimport java.awt.*;\nimport java.io.*;\n\n/**\n * Word docx生成器\n *\n * @author looly\n * @since 4.4.1\n */\npublic class Word07Writer implements Closeable {\n\n\tprivate final XWPFDocument doc;\n\t/**\n\t * 目标文件\n\t */\n\tprotected File destFile;\n\t/**\n\t * 是否被关闭\n\t */\n\tprotected boolean isClosed;\n\n\t// -------------------------------------------------------------------------- Constructor start\n\tpublic Word07Writer() {\n\t\tthis(new XWPFDocument());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param destFile 写出的文件\n\t */\n\tpublic Word07Writer(File destFile) {\n\t\tthis(DocUtil.create(destFile), destFile);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param doc {@link XWPFDocument}\n\t */\n\tpublic Word07Writer(XWPFDocument doc) {\n\t\tthis(doc, null);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param doc      {@link XWPFDocument}\n\t * @param destFile 写出的文件\n\t */\n\tpublic Word07Writer(XWPFDocument doc, File destFile) {\n\t\tthis.doc = doc;\n\t\tthis.destFile = destFile;\n\t}\n\n\t// -------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获取{@link XWPFDocument}\n\t *\n\t * @return {@link XWPFDocument}\n\t */\n\tpublic XWPFDocument getDoc() {\n\t\treturn this.doc;\n\t}\n\n\t/**\n\t * 设置写出的目标文件\n\t *\n\t * @param destFile 目标文件\n\t * @return this\n\t */\n\tpublic Word07Writer setDestFile(File destFile) {\n\t\tthis.destFile = destFile;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加一个段落\n\t *\n\t * @param font  字体信息{@link Font}\n\t * @param texts 段落中的文本，支持多个文本作为一个段落\n\t * @return this\n\t */\n\tpublic Word07Writer addText(Font font, String... texts) {\n\t\treturn addText(null, font, null, texts);\n\t}\n\n\t/**\n\t * 增加一个段落\n\t *\n\t * @param font  字体信息{@link Font}\n     * @param color 字体颜色{@link Color}\n\t * @param texts 段落中的文本，支持多个文本作为一个段落\n\t * @return this\n\t */\n\tpublic Word07Writer addText(Font font, Color color, String... texts) {\n\t\treturn addText(null, font, color, texts);\n\t}\n\n\t/**\n\t * 增加一个段落\n\t *\n\t * @param align 段落对齐方式{@link ParagraphAlignment}\n\t * @param font  字体信息{@link Font}\n\t * @param texts 段落中的文本，支持多个文本作为一个段落\n\t * @return this\n\t */\n\tpublic Word07Writer addText(ParagraphAlignment align, Font font, String... texts) {\n\t\treturn addText(align, font, null, texts);\n\t}\n\n\t/**\n\t * 增加一个段落\n\t *\n\t * @param align 段落对齐方式{@link ParagraphAlignment}\n\t * @param font  字体信息{@link Font}\n\t * @param color 字体颜色{@link Color}\n\t * @param texts 段落中的文本，支持多个文本作为一个段落\n\t * @return this\n\t * @since 5.8.42\n\t */\n\tpublic Word07Writer addText(ParagraphAlignment align, Font font, Color color, String... texts) {\n\t\tfinal XWPFParagraph p = this.doc.createParagraph();\n\t\tif (null != align) {\n\t\t\tp.setAlignment(align);\n\t\t}\n\t\tif (ArrayUtil.isNotEmpty(texts)) {\n\t\t\tXWPFRun run;\n\t\t\tfor (String text : texts) {\n\t\t\t\trun = p.createRun();\n\t\t\t\trun.setText(text);\n\t\t\t\tif (null != font) {\n\t\t\t\t\trun.setFontFamily(font.getFamily());\n\t\t\t\t\trun.setFontSize(font.getSize());\n\t\t\t\t\trun.setBold(font.isBold());\n\t\t\t\t\trun.setItalic(font.isItalic());\n\t\t\t\t}\n\t\t\t\tif (null != color) {\n\t\t\t\t\t// setColor expects a pure RGB hex string (no alpha channel)\n\t\t\t\t\tString hexColor = String.format(\"%06X\", color.getRGB() & 0xFFFFFF);\n\t\t\t\t\trun.setColor(hexColor);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加表格数据\n\t *\n\t * @param data 表格数据，多行数据。元素表示一行数据，当为集合或者数组时，为一行；当为Map或者Bean时key表示标题，values为数据\n\t * @return this\n\t * @since 4.5.16\n\t * @see TableUtil#createTable(XWPFDocument, Iterable)\n\t */\n\tpublic Word07Writer addTable(Iterable<?> data) {\n\t\tTableUtil.createTable(this.doc, data);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 增加图片，单独成段落\n\t *\n\t * @param picFile 图片文件\n\t * @param width   宽度\n\t * @param height  高度\n\t * @return this\n\t * @since 5.1.6\n\t */\n\tpublic Word07Writer addPicture(File picFile, int width, int height) {\n\t\tfinal String fileName = picFile.getName();\n\t\tfinal String extName = FileUtil.extName(fileName).toUpperCase();\n\t\tPicType picType;\n\t\ttry {\n\t\t\tpicType = PicType.valueOf(extName);\n\t\t} catch (IllegalArgumentException e) {\n\t\t\t// 默认值\n\t\t\tpicType = PicType.JPEG;\n\t\t}\n\t\treturn addPicture(FileUtil.getInputStream(picFile), picType, fileName, width, height);\n\t}\n\n\t/**\n\t * 增加图片，单独成段落，增加后图片流关闭，默认居中对齐\n\t *\n\t * @param in       图片流\n\t * @param picType  图片类型，见Document.PICTURE_TYPE_XXX\n\t * @param fileName 文件名\n\t * @param width    宽度\n\t * @param height   高度\n\t * @return this\n\t * @since 5.1.6\n\t */\n\tpublic Word07Writer addPicture(InputStream in, PicType picType, String fileName, int width, int height) {\n\t\treturn addPicture(in, picType, fileName, width, height, ParagraphAlignment.CENTER);\n\t}\n\n\t/**\n\t * 增加图片，单独成段落，增加后图片流关闭\n\t *\n\t * @param in       图片流\n\t * @param picType  图片类型，见Document.PICTURE_TYPE_XXX\n\t * @param fileName 文件名\n\t * @param width    宽度\n\t * @param height   高度\n\t * @param align    图片的对齐方式\n\t * @return this\n\t * @since 5.2.4\n\t */\n\tpublic Word07Writer addPicture(InputStream in, PicType picType, String fileName, int width, int height, ParagraphAlignment align) {\n\t\tfinal XWPFParagraph paragraph = doc.createParagraph();\n\t\tparagraph.setAlignment(align);\n\t\tfinal XWPFRun run = paragraph.createRun();\n\t\ttry {\n\t\t\trun.addPicture(in, picType.getValue(), fileName, Units.toEMU(width), Units.toEMU(height));\n\t\t} catch (InvalidFormatException e) {\n\t\t\tthrow new POIException(e);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到预定义的文件<br>\n\t * 如果用户未自定义输出的文件，将抛出{@link NullPointerException}<br>\n\t * 预定义文件可以通过{@link #setDestFile(File)} 方法预定义，或者通过构造定义\n\t *\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic Word07Writer flush() throws IORuntimeException {\n\t\treturn flush(this.destFile);\n\t}\n\n\t/**\n\t * 将Excel Workbook刷出到文件<br>\n\t * 如果用户未自定义输出的文件，将抛出{@link NullPointerException}\n\t *\n\t * @param destFile 写出到的文件\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic Word07Writer flush(File destFile) throws IORuntimeException {\n\t\tAssert.notNull(destFile, \"[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream).\");\n\t\treturn flush(FileUtil.getOutputStream(destFile), true);\n\t}\n\n\t/**\n\t * 将Word Workbook刷出到输出流\n\t *\n\t * @param out 输出流\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic Word07Writer flush(OutputStream out) throws IORuntimeException {\n\t\treturn flush(out, false);\n\t}\n\n\t/**\n\t * 将Word Document刷出到输出流\n\t *\n\t * @param out        输出流\n\t * @param isCloseOut 是否关闭输出流\n\t * @return this\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic Word07Writer flush(OutputStream out, boolean isCloseOut) throws IORuntimeException {\n\t\tAssert.isFalse(this.isClosed, \"WordWriter has been closed!\");\n\t\ttry {\n\t\t\tthis.doc.write(out);\n\t\t\tout.flush();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t} finally {\n\t\t\tif (isCloseOut) {\n\t\t\t\tIoUtil.close(out);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭Word文档<br>\n\t * 如果用户设定了目标文件，先写出目标文件后给关闭工作簿\n\t */\n\t@Override\n\tpublic void close() {\n\t\tif (null != this.destFile) {\n\t\t\tflush();\n\t\t}\n\t\tcloseWithoutFlush();\n\t}\n\n\t/**\n\t * 关闭Word文档但是不写出\n\t */\n\tprotected void closeWithoutFlush() {\n\t\tIoUtil.close(this.doc);\n\t\tthis.isClosed = true;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/WordUtil.java",
    "content": "package cn.hutool.poi.word;\n\nimport java.io.File;\n\n/**\n * Word工具类\n * \n * @author Looly\n * @since 4.5.16\n */\npublic class WordUtil {\n\t/**\n\t * 创建Word 07格式的生成器\n\t * \n\t * @return {@link Word07Writer}\n\t */\n\tpublic static Word07Writer getWriter() {\n\t\treturn new Word07Writer();\n\t}\n\n\t/**\n\t * 创建Word 07格式的生成器\n\t * \n\t * @param destFile 目标文件\n\t * @return {@link Word07Writer}\n\t */\n\tpublic static Word07Writer getWriter(File destFile) {\n\t\treturn new Word07Writer(destFile);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/main/java/cn/hutool/poi/word/package-info.java",
    "content": "/**\n * POI中对Word操作封装\n * \n * @author looly\n *\n */\npackage cn.hutool.poi.word;"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/IssueI5Q1TWTest.java",
    "content": "package cn.hutool.poi;\n\nimport cn.hutool.poi.excel.ExcelReader;\nimport cn.hutool.poi.excel.ExcelUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class IssueI5Q1TWTest {\n\n\t@Test\n\tpublic void readTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"I5Q1TW.xlsx\");\n\n\t\t// 自定义时间格式1\n\t\tassertEquals(\"18:56\", reader.readCellValue(0, 0).toString());\n\n\t\t// 自定义时间格式2\n\t\tassertEquals(\"18:56\", reader.readCellValue(1, 0).toString());\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.FillPatternType;\nimport org.apache.poi.ss.usermodel.Font;\nimport org.apache.poi.ss.usermodel.IndexedColors;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 写出Excel单元测试\n *\n * @author looly\n */\npublic class BigExcelWriteTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest2() {\n\t\tList<String> row = CollUtil.newArrayList(\"姓名\", \"加班日期\", \"下班时间\", \"加班时长\", \"餐补\", \"车补次数\", \"车补\", \"总计\");\n\t\tBigExcelWriter overtimeWriter = ExcelUtil.getBigWriter(\"e:/excel/single_line.xlsx\");\n\t\tovertimeWriter.write(row);\n\t\tovertimeWriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aaaaa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\t\tfor(int i=0; i < 400000; i++) {\n\t\t\t//超大列表写出测试\n\t\t\trows.add(ObjectUtil.clone(row1));\n\t\t}\n\n\t\tString filePath = \"e:/bigWriteTest.xlsx\";\n\t\tFileUtil.del(filePath);\n\t\t// 通过工具类创建writer\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(filePath);\n\n//\t\t// 跳过当前行，即第一行，非必须，在此演示用\n//\t\twriter.passCurrentRow();\n//\t\t// 合并单元格后的标题行，使用默认标题样式\n//\t\twriter.merge(row1.size() - 1, \"大数据测试标题\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n//\t\twriter.autoSizeColumn(0, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\n\t\t// 通过工具类创建writer\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(\"e:/mergeTest.xlsx\");\n\t\tCellStyle style = writer.getStyleSet().getHeadCellStyle();\n\t\tStyleUtil.setColor(style, IndexedColors.RED, FillPatternType.SOLID_FOREGROUND);\n\n\t\t// 跳过当前行，即第一行，非必须，在此演示用\n\t\twriter.passCurrentRow();\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(row1.size() - 1, \"测试标题\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(7, 10, 4, 10, \"测试Merge\", false);\n\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapTest() {\n\t\tMap<String, Object> row1 = new LinkedHashMap<>();\n\t\trow1.put(\"姓名\", \"张三\");\n\t\trow1.put(\"年龄\", 23);\n\t\trow1.put(\"成绩\", 88.32);\n\t\trow1.put(\"是否合格\", true);\n\t\trow1.put(\"考试日期\", DateUtil.date());\n\n\t\tMap<String, Object> row2 = new LinkedHashMap<>();\n\t\trow2.put(\"姓名\", \"李四\");\n\t\trow2.put(\"年龄\", 33);\n\t\trow2.put(\"成绩\", 59.50);\n\t\trow2.put(\"是否合格\", false);\n\t\trow2.put(\"考试日期\", DateUtil.date());\n\n\t\tArrayList<Map<String, Object>> rows = CollUtil.newArrayList(row1, row2);\n\n\t\t// 通过工具类创建writer\n\t\tString path = \"e:/bigWriteMapTest.xlsx\";\n\t\tFileUtil.del(path);\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(path);\n\n\t\t//设置内容字体\n\t\tFont font = writer.createFont();\n\t\tfont.setBold(true);\n\t\tfont.setColor(Font.COLOR_RED);\n\t\tfont.setItalic(true);\n\t\twriter.getStyleSet().setFont(font, true);\n\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(row1.size() - 1, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapTest2() {\n\t\tMap<String, Object> row1 = MapUtil.newHashMap(true);\n\t\trow1.put(\"姓名\", \"张三\");\n\t\trow1.put(\"年龄\", 23);\n\t\trow1.put(\"成绩\", 88.32);\n\t\trow1.put(\"是否合格\", true);\n\t\trow1.put(\"考试日期\", DateUtil.date());\n\n\t\t// 通过工具类创建writer\n\t\tString path = \"e:/bigWriteMapTest2.xlsx\";\n\t\tFileUtil.del(path);\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(path);\n\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.writeRow(row1, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeBeanTest() {\n\t\tcn.hutool.poi.excel.TestBean bean1 = new cn.hutool.poi.excel.TestBean();\n\t\tbean1.setName(\"张三\");\n\t\tbean1.setAge(22);\n\t\tbean1.setPass(true);\n\t\tbean1.setScore(66.30);\n\t\tbean1.setExamDate(DateUtil.date());\n\n\t\tcn.hutool.poi.excel.TestBean bean2 = new cn.hutool.poi.excel.TestBean();\n\t\tbean2.setName(\"李四\");\n\t\tbean2.setAge(28);\n\t\tbean2.setPass(false);\n\t\tbean2.setScore(38.50);\n\t\tbean2.setExamDate(DateUtil.date());\n\n\t\tList<cn.hutool.poi.excel.TestBean> rows = CollUtil.newArrayList(bean1, bean2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"e:/bigWriteBeanTest.xlsx\";\n\t\tFileUtil.del(file);\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(file);\n\t\t//自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\twriter.addHeaderAlias(\"score\", \"分数\");\n\t\twriter.addHeaderAlias(\"isPass\", \"是否通过\");\n\t\twriter.addHeaderAlias(\"examDate\", \"考试时间\");\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(4, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeCellValueTest() {\n\t\tString path = \"d:/test/cellValueTest.xlsx\";\n\t\tFileUtil.del(path);\n\t\tBigExcelWriter writer = new BigExcelWriter(path);\n\t\twriter.writeCellValue(3, 5, \"aaa\");\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void closeTest() {\n\t\tfinal Map<String, ?> map1 = MapUtil.of(\"id\", \"123456\");\n\t\tfinal Map<String, ?> map2 = MapUtil.of(\"id\", \"123457\");\n\t\tfinal List<?> data = Arrays.asList(map1, map2);\n\t\tfinal String destFilePath = \"d:/test/closeTest.xlsx\";//略\n\t\tFileUtil.del(destFilePath);\n\t\ttry (ExcelWriter writer = ExcelUtil.getBigWriter(destFilePath)) {\n\t\t\twriter.write(data).flush();\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issue1210() {\n\t\t// 通过工具类创建writer\n\t\tString path = \"d:/test/issue1210.xlsx\";\n\t\tFileUtil.del(path);\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(path);\n\t\twriter.addHeaderAlias(\"id\", \"SN\");\n\t\twriter.addHeaderAlias(\"userName\", \"User Name\");\n\n\t\tList<Map<String, Object>> list = new ArrayList<>();\n\t\tlist.add(new HashMap<String, Object>() {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t{\n\t\t\tput(\"id\", 1);\n\t\t\tput(\"userName\", \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\");\n\t\t}});\n\n\t\tlist.add(new HashMap<String, Object>() {\n\t\t\tprivate static final long serialVersionUID = 1L;\n\n\t\t\t{\n\t\t\tput(\"id\", 2);\n\t\t\tput(\"userName\", \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\");\n\t\t}});\n\t\twriter.write(list, true);\n\t\twriter.autoSizeColumnAll();\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/CellEditorTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.poi.excel.cell.CellEditor;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CellEditorTest {\n\n\t@Test\n\tpublic void readTest(){\n\t\tExcelReader excelReader= ExcelUtil.getReader(\"cell_editor_test.xlsx\");\n\t\texcelReader.setCellEditor(new ExcelHandler());\n\t\tList<TestBean> excelReaderObjects=excelReader.readAll(TestBean.class);\n\n\t\tassertEquals(\"0\", excelReaderObjects.get(0).getTest1());\n\t\tassertEquals(\"b\", excelReaderObjects.get(0).getTest2());\n\t\tassertEquals(\"0\", excelReaderObjects.get(1).getTest1());\n\t\tassertEquals(\"b1\", excelReaderObjects.get(1).getTest2());\n\t\tassertEquals(\"0\", excelReaderObjects.get(2).getTest1());\n\t\tassertEquals(\"c2\", excelReaderObjects.get(2).getTest2());\n\t}\n\n\t@AllArgsConstructor\n\t@Data\n\tpublic static class TestBean implements Serializable {\n\t\tprivate static final long serialVersionUID = 1L;\n\n\t\tprivate String test1;\n\t\tprivate String test2;\n\t}\n\n\tpublic static class ExcelHandler implements CellEditor {\n\t\t@ Override\n\t\tpublic Object edit(Cell cell, Object o) {\n\t\t\tif (cell.getColumnIndex()==0 && cell.getRowIndex() != 0){\n\t\t\t\to=\"0\";\n\t\t\t}\n\t\t\treturn o;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport org.apache.poi.ss.usermodel.BuiltinFormats;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport cn.hutool.core.lang.Console;\n\npublic class CellUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void isDateTest() {\n\t\tString[] all = BuiltinFormats.getAll();\n\t\tfor(int i = 0 ; i < all.length; i++) {\n\t\t\tConsole.log(\"{} {}\", i, all[i]);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelFileUtilTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\n\npublic class ExcelFileUtilTest {\n\n\t@Test\n\tpublic void xlsTest(){\n\t\tInputStream in = FileUtil.getInputStream(\"aaa.xls\");\n\t\ttry{\n\t\t\tassertTrue(ExcelFileUtil.isXls(in));\n\t\t\tassertFalse(ExcelFileUtil.isXlsx(in));\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void xlsxTest(){\n\t\tInputStream in = FileUtil.getInputStream(\"aaa.xlsx\");\n\t\ttry{\n\t\t\tassertFalse(ExcelFileUtil.isXls(in));\n\t\t\tassertTrue(ExcelFileUtil.isXlsx(in));\n\t\t} finally {\n\t\t\tIoUtil.close(in);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.poi.excel.cell.CellHandler;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Excel读取单元测试\n *\n * @author Looly\n */\npublic class ExcelReadTest {\n\n\t@Test\n\tpublic void aliasTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"alias.xlsx\"));\n\n\t\t//读取单个单元格内容测试\n\t\tObject value = reader.readCellValue(1, 2);\n\t\tassertEquals(\"仓库\", value);\n\n\t\tMap<String, String> headerAlias = MapUtil.newHashMap();\n\t\theaderAlias.put(\"用户姓名\", \"userName\");\n\t\theaderAlias.put(\"库房\", \"storageName\");\n\t\theaderAlias.put(\"盘点权限\", \"checkPerm\");\n\t\theaderAlias.put(\"领料审批权限\", \"allotAuditPerm\");\n\t\treader.setHeaderAlias(headerAlias);\n\n\t\t// 读取list时默认首个非空行为标题\n\t\tList<List<Object>> read = reader.read();\n\t\tassertEquals(\"userName\", read.get(0).get(0));\n\t\tassertEquals(\"storageName\", read.get(0).get(1));\n\t\tassertEquals(\"checkPerm\", read.get(0).get(2));\n\t\tassertEquals(\"allotAuditPerm\", read.get(0).get(3));\n\n\t\tList<Map<String, Object>> readAll = reader.readAll();\n\t\tfor (Map<String, Object> map : readAll) {\n\t\t\tassertTrue(map.containsKey(\"userName\"));\n\t\t\tassertTrue(map.containsKey(\"storageName\"));\n\t\t\tassertTrue(map.containsKey(\"checkPerm\"));\n\t\t\tassertTrue(map.containsKey(\"allotAuditPerm\"));\n\t\t}\n\t}\n\n\t@Test\n\tpublic void excelReadTestOfEmptyLine() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"priceIndex.xls\"));\n\t\tList<Map<String, Object>> readAll = reader.readAll();\n\n\t\tassertEquals(4, readAll.size());\n\t}\n\n\t@Test\n\tpublic void excelReadTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xlsx\"));\n\t\tList<List<Object>> readAll = reader.read();\n\n\t\t// 标题\n\t\tassertEquals(\"姓名\", readAll.get(0).get(0));\n\t\tassertEquals(\"性别\", readAll.get(0).get(1));\n\t\tassertEquals(\"年龄\", readAll.get(0).get(2));\n\t\tassertEquals(\"鞋码\", readAll.get(0).get(3));\n\n\t\t// 第一行\n\t\tassertEquals(\"张三\", readAll.get(1).get(0));\n\t\tassertEquals(\"男\", readAll.get(1).get(1));\n\t\tassertEquals(11L, readAll.get(1).get(2));\n\t\tassertEquals(41.5D, readAll.get(1).get(3));\n\t}\n\n\t@Test\n\tpublic void excelReadAsTextTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xlsx\"));\n\t\tassertNotNull(reader.readAsText(false));\n\t}\n\n\t@Test\n\tpublic void excel03ReadTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xls\"));\n\t\tList<List<Object>> readAll = reader.read();\n\n\t\t// for (List<Object> list : readAll) {\n\t\t// Console.log(list);\n\t\t// }\n\n\t\t// 标题\n\t\tassertEquals(\"姓名\", readAll.get(0).get(0));\n\t\tassertEquals(\"性别\", readAll.get(0).get(1));\n\t\tassertEquals(\"年龄\", readAll.get(0).get(2));\n\t\tassertEquals(\"分数\", readAll.get(0).get(3));\n\n\t\t// 第一行\n\t\tassertEquals(\"张三\", readAll.get(1).get(0));\n\t\tassertEquals(\"男\", readAll.get(1).get(1));\n\t\tassertEquals(11L, readAll.get(1).get(2));\n\t\tassertEquals(33.2D, readAll.get(1).get(3));\n\t}\n\n\t@Test\n\tpublic void excel03ReadTest2() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xls\"), \"校园入学\");\n\t\tList<List<Object>> readAll = reader.read();\n\n\t\t// 标题\n\t\tassertEquals(\"班级\", readAll.get(0).get(0));\n\t\tassertEquals(\"年级\", readAll.get(0).get(1));\n\t\tassertEquals(\"学校\", readAll.get(0).get(2));\n\t\tassertEquals(\"入学时间\", readAll.get(0).get(3));\n\t\tassertEquals(\"更新时间\", readAll.get(0).get(4));\n\t}\n\n\t@Test\n\tpublic void excelReadToMapListTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xlsx\"));\n\t\tList<Map<String, Object>> readAll = reader.readAll();\n\n\t\tassertEquals(\"张三\", readAll.get(0).get(\"姓名\"));\n\t\tassertEquals(\"男\", readAll.get(0).get(\"性别\"));\n\t\tassertEquals(11L, readAll.get(0).get(\"年龄\"));\n\t}\n\n\t@Test\n\tpublic void excelReadToBeanListTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xlsx\"));\n\t\treader.addHeaderAlias(\"姓名\", \"name\");\n\t\treader.addHeaderAlias(\"年龄\", \"age\");\n\t\treader.addHeaderAlias(\"性别\", \"gender\");\n\t\treader.addHeaderAlias(\"鞋码\", \"shoeSize\");\n\n\t\tList<Person> all = reader.readAll(Person.class);\n\t\tassertEquals(\"张三\", all.get(0).getName());\n\t\tassertEquals(\"男\", all.get(0).getGender());\n\t\tassertEquals(Integer.valueOf(11), all.get(0).getAge());\n\t\tassertEquals(new BigDecimal(\"41.5\"), all.get(0).getShoeSize());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void excelReadToBeanListTest2() {\n\t\tExcelReader reader = ExcelUtil.getReader(\"f:/test/toBean.xlsx\");\n\t\treader.addHeaderAlias(\"姓名\", \"name\");\n\t\treader.addHeaderAlias(\"年龄\", \"age\");\n\t\treader.addHeaderAlias(\"性别\", \"gender\");\n\n\t\tList<Person> all = reader.read(0, 2, Person.class);\n\t\tfor (Person person : all) {\n\t\t\tConsole.log(person);\n\t\t}\n\t}\n\n\t@Data\n\tpublic static class Person {\n\t\tprivate String name;\n\t\tprivate String gender;\n\t\tprivate Integer age;\n\t\tprivate BigDecimal shoeSize;\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readDoubleTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(\"f:/test/doubleTest.xls\");\n\t\tfinal List<List<Object>> read = reader.read();\n\t\tfor (List<Object> list : read) {\n\t\t\tConsole.log(list.get(8));\n\t\t}\n\t}\n\n\t@Test\n\tpublic void mergeReadTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"merge_test.xlsx\");\n\t\tfinal List<List<Object>> read = reader.read();\n\t\t// 验证合并单元格在两行中都可以取到值\n\t\tassertEquals(11L, read.get(1).get(2));\n\t\tassertEquals(11L, read.get(2).get(2));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readCellsTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"merge_test.xlsx\");\n\t\treader.read((cell, value)-> Console.log(\"{}, {} {}\", cell.getRowIndex(), cell.getColumnIndex(), value));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readTest() {\n\t\t// 测试合并单元格是否可以正常读到第一个单元格的值\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"d:/test/人员体检信息表.xlsx\");\n\t\tfinal List<List<Object>> read = reader.read();\n\t\tfor (List<Object> list : read) {\n\t\t\tConsole.log(list);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void nullValueEditTest(){\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"null_cell_test.xlsx\");\n\t\treader.setCellEditor((cell, value)-> ObjectUtil.defaultIfNull(value, \"#\"));\n\t\tfinal List<List<Object>> read = reader.read();\n\n\t\t// 对于任意一个单元格有值的情况下，之前的单元格值按照null处理\n\t\tassertEquals(1, read.get(1).size());\n\t\tassertEquals(2, read.get(2).size());\n\t\tassertEquals(3, read.get(3).size());\n\n\t\tassertEquals(\"#\", read.get(2).get(0));\n\t\tassertEquals(\"#\", read.get(3).get(0));\n\t\tassertEquals(\"#\", read.get(3).get(1));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readEmptyTest(){\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"d:/test/issue.xlsx\");\n\t\tfinal List<Map<String, Object>> maps = reader.readAll();\n\t\tConsole.log(maps);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readNullRowTest(){\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"d:/test/1.-.xls\");\n\t\treader.read((CellHandler) Console::log);\n\t}\n\n\t@Test\n\tpublic void readColumnTest(){\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"aaa.xlsx\"));\n\t\tfinal List<Object> objects = reader.readColumn(0, 1);\n\n\t\tassertEquals(3, objects.size());\n\t\tassertEquals(\"张三\", objects.get(0));\n\t\tassertEquals(\"李四\", objects.get(1));\n\t\tassertEquals(\"\", objects.get(2));\n\t}\n\n\t@Test\n\tpublic void readColumnNPETest() {\n\t\t// https://github.com/chinabugotech/hutool/pull/2234\n\t\tExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream(\"read_row_npe.xlsx\"));\n\t\treader.readColumn(0, 1);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReaderToWriterTest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ExcelReaderToWriterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void getWriterTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"d:/test/issue3120.xlsx\");\n\t\tfinal ExcelWriter writer = reader.getWriter();\n\t\twriter.writeCellValue(0, 0, \"替换内容222222\");\n\n\t\twriter.flush(FileUtil.file(\"d:/test/2.xlsx\"));\n\t\twriter.closeWithoutFlush();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.poi.excel.cell.CellLocation;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class ExcelUtilTest {\n\n\t@Test\n\tpublic void indexToColNameTest() {\n\t\tassertEquals(\"A\", ExcelUtil.indexToColName(0));\n\t\tassertEquals(\"B\", ExcelUtil.indexToColName(1));\n\t\tassertEquals(\"C\", ExcelUtil.indexToColName(2));\n\n\t\tassertEquals(\"AA\", ExcelUtil.indexToColName(26));\n\t\tassertEquals(\"AB\", ExcelUtil.indexToColName(27));\n\t\tassertEquals(\"AC\", ExcelUtil.indexToColName(28));\n\n\t\tassertEquals(\"AAA\", ExcelUtil.indexToColName(702));\n\t\tassertEquals(\"AAB\", ExcelUtil.indexToColName(703));\n\t\tassertEquals(\"AAC\", ExcelUtil.indexToColName(704));\n\t}\n\n\t@Test\n\tpublic void colNameToIndexTest() {\n\t\tassertEquals(704, ExcelUtil.colNameToIndex(\"AAC\"));\n\t\tassertEquals(703, ExcelUtil.colNameToIndex(\"AAB\"));\n\t\tassertEquals(702, ExcelUtil.colNameToIndex(\"AAA\"));\n\n\t\tassertEquals(28, ExcelUtil.colNameToIndex(\"AC\"));\n\t\tassertEquals(27, ExcelUtil.colNameToIndex(\"AB\"));\n\t\tassertEquals(26, ExcelUtil.colNameToIndex(\"AA\"));\n\n\t\tassertEquals(2, ExcelUtil.colNameToIndex(\"C\"));\n\t\tassertEquals(1, ExcelUtil.colNameToIndex(\"B\"));\n\t\tassertEquals(0, ExcelUtil.colNameToIndex(\"A\"));\n\t}\n\n\t@Test\n\tpublic void toLocationTest() {\n\t\tfinal CellLocation a11 = ExcelUtil.toLocation(\"A11\");\n\t\tassertEquals(0, a11.getX());\n\t\tassertEquals(10, a11.getY());\n\t}\n\n\t@Test\n\tpublic void readAndWriteTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"aaa.xlsx\");\n\t\tfinal ExcelWriter writer = reader.getWriter();\n\t\twriter.writeCellValue(1, 2, \"设置值\");\n\t\twriter.close();\n\t}\n\n\t@Test\n\tpublic void getReaderByBookFilePathAndSheetNameTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"aaa.xlsx\", \"12\");\n\t\tfinal List<Map<String, Object>> list = reader.readAll();\n\t\treader.close();\n\t\tassertEquals(1L, list.get(1).get(\"鞋码\"));\n\t}\n\n\t@Test\n\tpublic void doAfterAllAnalysedTest() {\n\t\tfinal String path = \"readBySax.xls\";\n\t\tfinal AtomicInteger doAfterAllAnalysedTime = new AtomicInteger(0);\n\t\tExcelUtil.readBySax(path, -1, new RowHandler() {\n\t\t\t@Override\n\t\t\tpublic void handle(final int sheetIndex, final long rowIndex, final List<Object> rowCells) {\n\t\t\t\t//Console.log(\"sheetIndex={};rowIndex={},rowCells={}\",sheetIndex,rowIndex,rowCells);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void doAfterAllAnalysed() {\n\t\t\t\tdoAfterAllAnalysedTime.addAndGet(1);\n\t\t\t}\n\t\t});\n\t\t//总共2个sheet页，读取所有sheet时，一共执行doAfterAllAnalysed2次。\n\t\tassertEquals(2, doAfterAllAnalysedTime.intValue());\n\t}\n\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteBeanTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport lombok.Getter;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ExcelWriteBeanTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeRowTest() {\n\t\tMyBean bean = new MyBean(\"value1\", \"value2\");\n\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/writeRowTest.xlsx\");\n\t\twriter.writeRow(bean, true);\n\t\twriter.close();\n\t}\n\n\t@Getter\n\tstatic class MyBean {\n\t\tprivate final String property1;\n\t\tprivate final String property2;\n\n\t\tpublic MyBean(String property1, String property2) {\n\t\t\tthis.property1 = property1;\n\t\t\tthis.property2 = property2;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.comparator.IndexedComparator;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.poi.excel.cell.setters.EscapeStrCellSetter;\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.common.usermodel.HyperlinkType;\nimport org.apache.poi.ss.usermodel.*;\nimport org.apache.poi.ss.util.CellRangeAddressList;\nimport org.apache.poi.xssf.usermodel.XSSFRichTextString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.*;\n\n/**\n * 写出Excel单元测试\n *\n * @author looly\n */\npublic class ExcelWriteTest {\n\n\tpublic static Map<String, Object> MAP_DATA_1 = new LinkedHashMap<>();\n\tpublic static Map<String, Object> MAP_DATA_2 = new LinkedHashMap<>();\n\n\tstatic {\n\t\tMAP_DATA_1.put(\"姓名\", \"张三\");\n\t\tMAP_DATA_1.put(\"年龄\", 23);\n\t\tMAP_DATA_1.put(\"成绩\", 88.32);\n\t\tMAP_DATA_1.put(\"是否合格\", true);\n\t\tMAP_DATA_1.put(\"考试日期\", DateUtil.date());\n\n\t\tMAP_DATA_2.put(\"姓名\", \"张三\");\n\t\tMAP_DATA_2.put(\"年龄\", 23);\n\t\tMAP_DATA_2.put(\"成绩\", 88.32);\n\t\tMAP_DATA_2.put(\"是否合格\", true);\n\t\tMAP_DATA_2.put(\"考试日期\", DateUtil.date());\n\t}\n\n\t@Test\n\tpublic void writeNoFlushTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aaaaa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter();\n\t\twriter.write(rows);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void testRowOrColumnCellStyle() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aaaaa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\t\tBigExcelWriter overtimeWriter = ExcelUtil.getBigWriter(\"d:/test/style_line.xlsx\");\n\n\t\tovertimeWriter.write(rows, true);\n\n\t\tCellStyle cellStyle = overtimeWriter.getWorkbook().createCellStyle();\n\t\tStyleUtil.setBorder(cellStyle, BorderStyle.THIN, IndexedColors.BLACK);\n\t\tcellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);\n\t\tcellStyle.setFillForegroundColor((short) 13);\n\t\tcellStyle.setDataFormat((short) 22);//时间格式\n\t\tcellStyle.setAlignment(HorizontalAlignment.CENTER);\n\t\tcellStyle.setVerticalAlignment(VerticalAlignment.CENTER);\n\n\t\t//原设置行、列样式的方法\n//\t\tovertimeWriter.setRowStyle(2,cellStyle);\n//\t\tovertimeWriter.setColumnStyle(1,cellStyle);\n\n\t\t//现增加的设置行、列样式的方法\n\t\t//给第三行加背景色\n\t\tovertimeWriter.setRowStyleIfHasData(2, cellStyle);\n\t\t//给第二列加背景色 从第一行开始加（用于控制有表头时）\n\t\tovertimeWriter.setColumnStyleIfHasData(1, 0, cellStyle);\n\n\t\tCellStyle cellStyle1 = overtimeWriter.getWorkbook().createCellStyle();\n\t\tStyleUtil.setBorder(cellStyle1, BorderStyle.THIN, IndexedColors.BLACK);\n\t\tcellStyle1.setFillPattern(FillPatternType.SOLID_FOREGROUND);\n\t\tcellStyle1.setFillForegroundColor((short) 13);\n\t\tcellStyle1.setDataFormat((short) 2);//小数保留两位\n\t\tcellStyle1.setAlignment(HorizontalAlignment.CENTER);\n\t\tcellStyle1.setVerticalAlignment(VerticalAlignment.CENTER);\n\t\tovertimeWriter.setStyle(cellStyle1, 5, 2);//由于第6列是数字 上面应用了日期格式会错乱，这里单独设置下第六列的格式\n\n\t\tovertimeWriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest2() {\n\t\tList<String> row = CollUtil.newArrayList(\"姓名\", \"加班日期\", \"下班时间\", \"加班时长\", \"餐补\", \"车补次数\", \"车补\", \"总计\");\n\t\tExcelWriter overtimeWriter = ExcelUtil.getWriter(\"e:/excel/single_line.xlsx\");\n\t\tovertimeWriter.writeRow(row);\n\t\tovertimeWriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeWithSheetTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriterWithSheet(\"表格1\");\n\n\t\t// 写出第一张表\n\t\tList<String> row = CollUtil.newArrayList(\"姓名\", \"加班日期\", \"下班时间\", \"加班时长\", \"餐补\", \"车补次数\", \"车补\", \"总计\");\n\t\twriter.writeRow(row);\n\n\t\t// 写出第二张表\n\t\twriter.setSheet(\"表格2\");\n\t\tList<String> row2 = CollUtil.newArrayList(\"姓名2\", \"加班日期2\", \"下班时间2\", \"加班时长2\", \"餐补2\", \"车补次数2\", \"车补2\", \"总计2\");\n\t\twriter.writeRow(row2);\n\n\t\t// 生成文件或导出Excel\n\t\twriter.flush(FileUtil.file(\"f:/test/writeWithSheetTest.xlsx\"));\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aaaaa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\t\tfor (int i = 0; i < 400; i++) {\n\t\t\t// 超大列表写出测试\n\t\t\trows.add(ObjectUtil.clone(row1));\n\t\t}\n\n\t\tString filePath = \"d:/test/writeTest.xlsx\";\n\t\tFileUtil.del(filePath);\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(filePath);\n\t\t// 通过构造方法创建writer\n\t\t// ExcelWriter writer = new ExcelWriter(\"d:/writeTest.xls\");\n\n\t\t// 跳过当前行，即第一行，非必须，在此演示用\n\t\twriter.passCurrentRow();\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(row1.size() - 1, \"测试标题\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\t\twriter.autoSizeColumn(0, true);\n\t\t//冻结前两行\n\t\twriter.setFreezePane(0, 2);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/mergeTest.xlsx\");\n\t\tCellStyle style = writer.getStyleSet().getHeadCellStyle();\n\t\tStyleUtil.setColor(style, IndexedColors.RED, FillPatternType.SOLID_FOREGROUND);\n\n\t\t// 跳过当前行，即第一行，非必须，在此演示用\n\t\twriter.passCurrentRow();\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(row1.size() - 1, \"测试标题\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(7, 10, 4, 10, \"测试Merge\", false);\n\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeTest2() {\n\n\t\tArrayList<Map<String, Object>> rows = CollUtil.newArrayList(MAP_DATA_1, MAP_DATA_2);\n\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/writeMapTest.xlsx\");\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(MAP_DATA_1.size() - 1, \"一班成绩单\");\n\n\t\t// 一次性写出内容，使用默认样式，强制输出标题\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapTest() {\n\n\t\tArrayList<Map<String, Object>> rows = CollUtil.newArrayList(MAP_DATA_1, MAP_DATA_2);\n\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"e:/excel/writeMapTest.xlsx\");\n\n\t\t// 设置内容字体\n\t\tFont font = writer.createFont();\n\t\tfont.setBold(true);\n\t\tfont.setColor(Font.COLOR_RED);\n\t\tfont.setItalic(true);\n\t\twriter.getStyleSet().setFont(font, true);\n\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(MAP_DATA_1.size() - 1, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapTest2() {\n\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/writeMapTest2.xlsx\");\n\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.writeRow(MAP_DATA_1, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapWithStyleTest() {\n\n\t\t// 通过工具类创建writer\n\t\tString path = \"f:/test/writeMapWithStyleTest.xlsx\";\n\t\tFileUtil.del(path);\n\t\tExcelWriter writer = ExcelUtil.getWriter(path);\n\t\twriter.setStyleSet(null);\n\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.writeRow(MAP_DATA_1, true);\n\n\t\t// 设置某个单元格样式\n\t\tCellStyle orCreateRowStyle = writer.getOrCreateCellStyle(0, 1);\n\t\tStyleUtil.setColor(orCreateRowStyle, IndexedColors.RED.getIndex(), FillPatternType.SOLID_FOREGROUND);\n\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapAliasTest() {\n\t\tMap<Object, Object> row1 = new LinkedHashMap<>();\n\t\trow1.put(\"name\", \"张三\");\n\t\trow1.put(\"age\", 22);\n\t\trow1.put(\"isPass\", true);\n\t\trow1.put(\"score\", 66.30);\n\t\trow1.put(\"examDate\", DateUtil.date());\n\t\tMap<Object, Object> row2 = new LinkedHashMap<>();\n\t\trow2.put(\"name\", \"李四\");\n\t\trow2.put(\"age\", 233);\n\t\trow2.put(\"isPass\", false);\n\t\trow2.put(\"score\", 32.30);\n\t\trow2.put(\"examDate\", DateUtil.date());\n\n\t\tList<Map<Object, Object>> rows = CollUtil.newArrayList(row1, row2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"d:/test/writeMapAlias.xlsx\";\n\t\tFileUtil.del(file);\n\t\tExcelWriter writer = ExcelUtil.getWriter(file);\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\twriter.addHeaderAlias(\"score\", \"分数\");\n\t\twriter.addHeaderAlias(\"isPass\", \"是否合格\");\n\t\twriter.addHeaderAlias(\"examDate\", \"考试日期\");\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(4, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapOnlyAliasTest() {\n\t\tMap<Object, Object> row1 = new LinkedHashMap<>();\n\t\trow1.put(\"name\", \"张三\");\n\t\trow1.put(\"age\", 22);\n\t\trow1.put(\"isPass\", true);\n\t\trow1.put(\"score\", 66.30);\n\t\trow1.put(\"examDate\", DateUtil.date());\n\t\tMap<Object, Object> row2 = new LinkedHashMap<>();\n\t\trow2.put(\"name\", \"李四\");\n\t\trow2.put(\"age\", 233);\n\t\trow2.put(\"isPass\", false);\n\t\trow2.put(\"score\", 32.30);\n\t\trow2.put(\"examDate\", DateUtil.date());\n\n\t\tList<Map<Object, Object>> rows = CollUtil.newArrayList(row1, row2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"f:/test/test_alias.xlsx\";\n\t\tFileUtil.del(file);\n\t\tExcelWriter writer = ExcelUtil.getWriter(file);\n\t\twriter.setOnlyAlias(true);\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(4, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapOnlyAliasTest2() {\n\t\tMap<Object, Object> row1 = new LinkedHashMap<>();\n\t\trow1.put(\"name\", \"张三\");\n\t\trow1.put(\"age\", 22);\n\t\trow1.put(\"isPass\", true);\n\t\trow1.put(\"score\", 66.30);\n\t\trow1.put(\"examDate\", DateUtil.date());\n\t\tMap<Object, Object> row2 = new LinkedHashMap<>();\n\t\trow2.put(\"name\", \"李四\");\n\t\trow2.put(\"age\", 233);\n\t\trow2.put(\"isPass\", false);\n\t\trow2.put(\"score\", 32.30);\n\t\trow2.put(\"examDate\", DateUtil.date());\n\n\t\tList<Map<Object, Object>> rows = CollUtil.newArrayList(row1, row2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"d:/test/test_alias.xls\";\n\t\tExcelWriter writer = ExcelUtil.getWriter(file, \"test1\");\n//\t\twriter.setOnlyAlias(true);\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapOnlyAliasTest3() {\n\t\tMap<Object, Object> row1 = new LinkedHashMap<>();\n\t\trow1.put(\"name\", \"张三\");\n\t\trow1.put(\"age\", 22);\n\t\trow1.put(\"isPass\", true);\n\t\trow1.put(\"score\", 66.30);\n\t\trow1.put(\"examDate\", DateUtil.date());\n\n\t\tMap<Object, Object> row2 = new LinkedHashMap<>();\n\t\trow2.put(\"name\", \"李四\");\n//\t\trow2.put(\"age\", 233);\n\t\trow2.put(\"isPass\", false);\n\t\trow2.put(\"score\", 32.30);\n\t\trow2.put(\"examDate\", DateUtil.date());\n\n\t\tList<Map<Object, Object>> rows = CollUtil.newArrayList(row1, row2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"d:/test/test_alias.xls\";\n\t\tExcelWriter writer = ExcelUtil.getWriter(file, \"test1\");\n\t\twriter.setOnlyAlias(true);\n\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\twriter.addHeaderAlias(\"examDate\", \"考试时间\");\n\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeBeanTest() {\n\t\tTestBean bean1 = new TestBean();\n\t\tbean1.setName(\"张三\");\n\t\tbean1.setAge(22);\n\t\tbean1.setPass(true);\n\t\tbean1.setScore(66.30);\n\t\tbean1.setExamDate(DateUtil.date());\n\n\t\tTestBean bean2 = new TestBean();\n\t\tbean2.setName(\"李四\");\n\t\tbean2.setAge(28);\n\t\tbean2.setPass(false);\n\t\tbean2.setScore(38.50);\n\t\tbean2.setExamDate(DateUtil.date());\n\n\t\tList<TestBean> rows = CollUtil.newArrayList(bean1, bean2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"e:/writeBeanTest.xlsx\";\n\t\tFileUtil.del(file);\n\t\tExcelWriter writer = ExcelUtil.getWriter(file);\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\twriter.addHeaderAlias(\"score\", \"分数\");\n\t\twriter.addHeaderAlias(\"isPass\", \"是否通过\");\n\t\twriter.addHeaderAlias(\"examDate\", \"考试时间\");\n\t\t// 合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(4, \"一班成绩单\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeBeanTest2() {\n\t\tOrderExcel order1 = new OrderExcel();\n\t\torder1.setId(\"1\");\n\t\torder1.setNum(\"123\");\n\t\torder1.setBody(\"body1\");\n\n\t\tOrderExcel order2 = new OrderExcel();\n\t\torder1.setId(\"2\");\n\t\torder1.setNum(\"456\");\n\t\torder1.setBody(\"body2\");\n\n\t\tList<OrderExcel> rows = CollUtil.newArrayList(order1, order2);\n\t\t// 通过工具类创建writer\n\t\tString file = \"f:/test/writeBeanTest2.xlsx\";\n\t\tFileUtil.del(file);\n\t\tExcelWriter writer = ExcelUtil.getWriter(file);\n\t\t// 自定义标题\n\t\twriter.addHeaderAlias(\"id\", \"编号\");\n\t\twriter.addHeaderAlias(\"num\", \"序号\");\n\t\twriter.addHeaderAlias(\"body\", \"内容\");\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows, true);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeCellValueTest() {\n\t\tExcelWriter writer = new ExcelWriter(\"d:/cellValueTest.xls\");\n\t\twriter.writeCellValue(3, 5, \"aaa\");\n\t\twriter.writeCellValue(3, 5, \"aaa\");\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addSelectTest() {\n\t\tList<String> row = CollUtil.newArrayList(\"姓名\", \"加班日期\", \"下班时间\", \"加班时长\", \"餐补\", \"车补次数\", \"车补\", \"总计\");\n\t\tExcelWriter overtimeWriter = ExcelUtil.getWriter(\"d:/test/single_line.xlsx\");\n\t\tovertimeWriter.writeCellValue(3, 4, \"AAAA\");\n\t\tovertimeWriter.addSelect(3, 4, row.toArray(new String[0]));\n\t\tovertimeWriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void addSelectTest2() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/select.xls\");\n\t\twriter.writeCellValue(0, 0, \"请选择科目\");\n\t\tint firstRow = 0;\n\t\tint lastRow = 0;\n\t\tint firstCol = 0;\n\t\tint lastCol = 0;\n\t\tCellRangeAddressList addressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);\n\t\twriter.addSelect(addressList, \"1001\", \"1002\", \"1003\");\n\n\t\tList<?> rows = new ArrayList<>();\n\t\twriter.write(rows, true);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMultiSheetTest() {\n\t\tList<Map<String, Object>> rows = new LinkedList<>();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tMap<String, Object> tempList = new TreeMap<>();\n\t\t\tfor (int j = 0; j < 10; j++) {\n\t\t\t\ttempList.put(j + \"\", IdUtil.randomUUID());\n\t\t\t}\n\t\t\trows.add(tempList);\n\t\t}\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"D:\\\\test\\\\multiSheet.xlsx\", \"正常数据\");\n\t\twriter.addHeaderAlias(\"1\", \"row1\");\n\t\twriter.addHeaderAlias(\"3\", \"row2\");\n\t\twriter.setOnlyAlias(true);\n\n\t\twriter.write(rows, true);\n\t\twriter.autoSizeColumnAll();\n\n\t\t//表2\n\t\twriter.setSheet(\"当前重复数据\");\n\t\twriter.clearHeaderAlias();\n\t\twriter.addHeaderAlias(\"3\", \"行3\");\n\t\twriter.addHeaderAlias(\"1\", \"行1\");\n\t\twriter.write(rows, true);\n\t\twriter.autoSizeColumnAll();\n\n\t\t//表3\n\t\twriter.setSheet(\"历史重复数据\");\n\t\twriter.write(rows, true);\n\t\twriter.autoSizeColumnAll();\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMultiSheetTest2() {\n\t\tList<Map<String, Object>> rows = new LinkedList<>();\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"k1\", \"v1\");\n\t\tmap.put(\"k2\", \"v2\");\n\t\tmap.put(\"k3\", \"v3\");\n\t\trows.add(map);\n\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"D:\\\\test\\\\multiSheet2.xlsx\", \"正常数据\");\n\t\twriter.write(rows);\n\n\t\t//表2\n\t\twriter.setSheet(\"表2\");\n\t\tList<Map<String, Object>> rows2 = new LinkedList<>();\n\t\tfinal HashMap<String, Object> map2 = MapUtil.newHashMap();\n\t\tmap2.put(\"x1\", \"v1\");\n\t\trows2.add(map2);\n\t\twriter.write(rows2);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMultiSheetWithStyleTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"D:\\\\test\\\\multiSheetWithStyle.xlsx\", \"表格1\");\n\n\t\t// 表1\n\t\tList<Map<String, Object>> rows = new LinkedList<>();\n\t\tfinal HashMap<String, Object> map = MapUtil.newHashMap();\n\t\tmap.put(\"k1\", \"v1\");\n\t\tmap.put(\"k2\", \"v2\");\n\t\tmap.put(\"k3\", \"v3\");\n\t\trows.add(map);\n\t\twriter.write(rows);\n\n\t\tFont headFont = writer.createFont();\n\t\theadFont.setBold(true);\n\t\theadFont.setFontHeightInPoints((short) 50);\n\t\theadFont.setFontName(\"Microsoft YaHei\");\n\t\twriter.getStyleSet().getHeadCellStyle().setFont(headFont);\n\n\t\t//表2\n\t\twriter.setSheet(\"表2\");\n\t\tList<Map<String, Object>> rows2 = new LinkedList<>();\n\t\tfinal HashMap<String, Object> map2 = MapUtil.newHashMap();\n\t\tmap2.put(\"x1\", \"v1\");\n\t\trows2.add(map2);\n\t\twriter.write(rows2);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapsTest() {\n\t\tList<Map<String, Object>> rows = new ArrayList<>();\n\n\t\tMap<String, Object> map1 = new HashMap<>();\n\t\tmap1.put(\"a\", 1);\n\t\tmap1.put(\"b\", 2);\n\t\tmap1.put(\"c\", 3);\n\t\tmap1.put(\"d\", 4);\n\t\tmap1.put(\"e\", 5);\n\t\tMap<String, Object> map2 = new HashMap<>();\n\t\tmap2.put(\"c\", 3);\n\t\tmap2.put(\"d\", 4);\n\t\tmap2.put(\"e\", 5);\n\t\tMap<String, Object> map3 = new HashMap<>();\n\t\tmap3.put(\"d\", 4);\n\t\tmap3.put(\"e\", 5);\n\n\t\trows.add(map1);\n\t\trows.add(map2);\n\t\trows.add(map3);\n\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/rows.xlsx\");\n\t\twriter.write(rows);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void formatTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/formatTest.xlsx\");\n\t\tfinal CellStyle cellStyle = writer.createCellStyle(0, 0);\n\t\tcellStyle.setDataFormat(writer.getWorkbook().createDataFormat().getFormat(\"yyyy-mm-dd\"));\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeNumberFormatTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/formatTest.xlsx\");\n\t\twriter.disableDefaultStyle();\n\t\twriter.writeRow(ListUtil.toList(51.33333333, 90.111111111));\n\t\tfinal CellStyle columnStyle = writer.createCellStyle(0, 0);\n\t\tcolumnStyle.setDataFormat((short) BuiltinFormats.getBuiltinFormat(\"0.00\"));\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeSecHeadRowTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(1, \"aa\", \"bb\", \"cc\", \"dd\", \"ee\");\n\t\tList<?> row2 = CollUtil.newArrayList(2, \"aa1\", \"bb1\", \"cc1\", \"dd1\", \"ee1\");\n\t\tList<?> row3 = CollUtil.newArrayList(3, \"aa2\", \"bb2\", \"cc2\", \"dd2\", \"ee2\");\n\t\tList<?> row4 = CollUtil.newArrayList(4, \"aa3\", \"bb3\", \"cc3\", \"dd3\", \"ee3\");\n\t\tList<?> row5 = CollUtil.newArrayList(5, \"aa4\", \"bb4\", \"cc4\", \"dd4\", \"ee4\");\n\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\n\t\t// 通过工具类创建writer\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/writeSecHeadRowTest.xlsx\");\n\n\t\tCellStyle cellStyle = writer.getWorkbook().createCellStyle();\n\t\tcellStyle.setWrapText(false);\n\t\tcellStyle.setAlignment(HorizontalAlignment.CENTER);\n\t\tcellStyle.setVerticalAlignment(VerticalAlignment.CENTER);\n\t\t//设置标题内容字体\n\t\tFont font = writer.createFont();\n\t\tfont.setBold(true);\n\t\tfont.setFontHeightInPoints((short) 15);\n\t\tfont.setFontName(\"Arial\");\n\t\t//设置边框样式\n\t\tStyleUtil.setBorder(cellStyle, BorderStyle.THICK, IndexedColors.RED);\n\t\tcellStyle.setFont(font);\n\n\t\t// 合并单元格后的标题行，使用设置好的样式\n\t\twriter.merge(0, 1, 0, row1.size() - 1, \"标题XXXXXXXX\", cellStyle);\n\t\tConsole.log(writer.getCurrentRow());\n\n\t\t//设置复杂表头\n\t\twriter.merge(2, 3, 0, 0, \"序号\", true);\n\t\twriter.merge(2, 2, 1, 2, \"AABB\", true);\n\t\twriter.merge(2, 3, 3, 3, \"CCCC\", true);\n\t\twriter.merge(2, 2, 4, 5, \"DDEE\", true);\n\t\twriter.setCurrentRow(3);\n\n\t\tList<String> sechead = CollUtil.newArrayList(\"AA\", \"BB\", \"DD\", \"EE\");\n\t\twriter.writeSecHeadRow(sechead);\n\t\t// 一次性写出内容，使用默认样式\n\t\twriter.write(rows);\n\t\t// 关闭writer，释放内存\n\t\twriter.close();\n\t}\n\n\t/**\n\t * issue#1659@Github\n\t * 测试使用BigWriter写出，ExcelWriter修改失败\n\t */\n\t@Test\n\t@Disabled\n\tpublic void editTest() {\n\t\t// 生成文件\n\t\tFile file = new File(\"d:/test/100_.xlsx\");\n\t\tFileUtil.del(file);\n\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(file);\n\t\twriter.disableDefaultStyle();\n\t\tList<List<String>> rows = Collections.singletonList(Arrays.asList(\"哈哈\", \"嘿嘿\"));\n\t\twriter.write(rows);\n\t\twriter.close();\n\n\t\t// 修改文件\n\t\tExcelWriter writer2 = ExcelUtil.getWriter(file);\n\t\twriter2.disableDefaultStyle();\n\t\twriter2.writeCellValue(0, 0, \"a\");\n\t\twriter2.close();\n\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(file);\n\t\tConsole.log(reader.read());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeTest3() {\n\t\t// https://github.com/chinabugotech/hutool/issues/1696\n\n\t\tList<Map<String, Object>> list = new ArrayList<>();\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"xmnf\", \"2021\");\n\t\tlist.add(map);\n\n\t\tMap<String, Object> map1 = new HashMap<>();\n\t\tmap1.put(\"xmnf\", new XSSFRichTextString(\"9999\"));\n\t\tlist.add(map1);\n\n\t\tMap<String, Object> map2 = new HashMap<>();\n\t\tmap2.put(\"xmnf\", \"2019\");\n\t\tlist.add(map2);\n\n\t\t//通过工具类创建writer\n\t\tFileUtil.del(\"d:/test/writeTest2123.xlsx\");\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/writeTest2123.xlsx\");\n\t\twriter.addHeaderAlias(\"xmnf\", \"项目年份\");//1\n\n\t\t//合并单元格后的标题行，使用默认标题样式\n\t\twriter.merge(7, \"测试标题\");\n\t\twriter.merge(3, 4, 0, 0, new XSSFRichTextString(\"9999\"), true);\n\t\twriter.write(list, true);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeForDateTest() {\n\t\t// https://github.com/chinabugotech/hutool/issues/1911\n\n\t\t//通过工具类创建writer\n\t\tString path = \"d:/test/mergeForDate.xlsx\";\n\t\tFileUtil.del(path);\n\t\tExcelWriter writer = ExcelUtil.getWriter(path);\n\t\twriter.merge(0, 3, 0, 2, DateUtil.date(), false);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void changeHeaderStyleTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/headerStyle.xlsx\");\n\t\twriter.writeHeadRow(ListUtil.of(\"姓名\", \"性别\", \"年龄\"));\n\t\tfinal CellStyle headCellStyle = writer.getStyleSet().getHeadCellStyle();\n\t\theadCellStyle.setFillForegroundColor(IndexedColors.YELLOW1.index);\n\t\theadCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeFloatTest() {\n\t\t//issue https://gitee.com/chinabugotech/hutool/issues/I43U9G\n\t\tString path = \"d:/test/floatTest.xlsx\";\n\t\tFileUtil.del(path);\n\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(path);\n\t\twriter.writeRow(ListUtil.of(22.9f));\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void issueI466ZZTest() {\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I466ZZ\n\t\t// 需要输出S_20000314_x5116_0004\n\t\t// 此处加入一个转义前缀：_x005F\n\t\tList<Object> row = ListUtil.of(new EscapeStrCellSetter(\"S_20000314_x5116_0004\"));\n\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/_x.xlsx\");\n\t\twriter.writeRow(row);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeLongTest() {\n\t\t//https://gitee.com/chinabugotech/hutool/issues/I49R6U\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/long.xlsx\");\n\t\twriter.write(ListUtil.of(1427545395336093698L));\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeHyperlinkTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/hyperlink.xlsx\");\n\n\t\tfinal Hyperlink hyperlink = writer.createHyperlink(HyperlinkType.URL, \"https://hutool.cn\");\n\n\t\twriter.write(ListUtil.of(hyperlink));\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void mergeNumberTest() {\n\t\tFile tempFile = new File(\"d:/test/mergeNumber.xlsx\");\n\t\tFileUtil.del(tempFile);\n\n\t\tBigExcelWriter writer = new BigExcelWriter(tempFile);\n\t\twriter.merge(0, 1, 2, 2, 3.99, false);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeImgTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(true);\n\n\t\tFile file = new File(\"C:\\\\Users\\\\zsz\\\\Desktop\\\\1.jpg\");\n\n\t\twriter.writeImg(file, 0, 0, 5, 10);\n\n\t\twriter.flush(new File(\"C:\\\\Users\\\\zsz\\\\Desktop\\\\2.xlsx\"));\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\tpublic void getDispositionTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(true);\n\t\tfinal String disposition = writer.getDisposition(\"测试A12.xlsx\", CharsetUtil.CHARSET_UTF_8);\n\t\tassertEquals(\"attachment; filename=\\\"%E6%B5%8B%E8%AF%95A12.xlsx\\\"\", disposition);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeColTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"G:\\\\test.xlsx\");\n\t\tList<Integer> list = new ArrayList<>();\n\t\tfor (int i = 0; i < 20; i++) {\n\t\t\tlist.add(i);\n\t\t}\n\t\twriter.writeCol(\"header1\", 0, list, true);\n\t\twriter.writeCol(\"header2\", 1, list, false);\n\t\twriter.writeCol(\"header3\", 0, list, true);\n\t\twriter.writeCol(\"header4\", 1, list, false);\n\n\t\tMap<String, List<Integer>> map1 = new LinkedHashMap<>();\n\t\tmap1.put(\"map1_header1\", list);\n\t\tmap1.put(\"map1_header2\", list);\n\t\tmap1.put(\"map1_header3\", list);\n\t\tmap1.put(\"map1_header4\", list);\n\n\t\tMap<String, List<Integer>> map2 = new LinkedHashMap<>();\n\t\tmap2.put(\"map2_header1\", list);\n\t\tmap2.put(\"map2_header2\", list);\n\t\tmap2.put(\"map2_header3\", list);\n\t\tmap2.put(\"map2_header4\", list);\n\n\t\twriter.writeCol(map1, true);\n\t\twriter.writeCol(map2, false);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void autoSizeColumnTest() {\n\n\t\tMap<String, Object> map = new LinkedHashMap<>(MAP_DATA_1);\n\t\tmap.put(\"中文长度测试（符号）\", \"abc\");\n\n\t\tString file1 = \"d:/autoSizeColumnTest.xlsx\";\n\t\tString file2 = \"d:/autoSizeColumnTest2.xlsx\";\n\n\t\tFileUtil.del(file1);\n\t\tFileUtil.del(file2);\n\n\t\ttry (ExcelWriter writer = new ExcelWriter(file1)) {\n\t\t\twriter.writeRow(map, true);\n\t\t\twriter.autoSizeColumnAll(2f);\n\t\t}\n\n\t\ttry (BigExcelWriter writer = new BigExcelWriter(file2)) {\n\t\t\twriter.writeRow(map, true);\n\t\t\twriter.autoSizeColumnAll(2f);\n\t\t}\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeWithComparatorTest() {\n\t\t// 生成测试数据, 10w行50列\n\t\tList<Map<String, Object>> dataList = new ArrayList<>();\n\t\tfor (int i = 1; i <= 100000; i++) {\n\t\t\tMap<String, Object> map = new HashMap<>();\n\t\t\tmap.put(\"test11\", \"test11_\" + i);\n\t\t\tmap.put(\"test12\", \"test12_\" + i);\n\t\t\tmap.put(\"test13\", \"test13_\" + i);\n\t\t\tmap.put(\"test14\", \"test14_\" + i);\n\t\t\tmap.put(\"test15\", \"test15_\" + i);\n\t\t\tmap.put(\"test16\", \"test16_\" + i);\n\t\t\tmap.put(\"test17\", \"test17_\" + i);\n\t\t\tmap.put(\"test18\", \"test18_\" + i);\n\t\t\tmap.put(\"test19\", \"test19_\" + i);\n\t\t\tmap.put(\"test1\", \"test1_\" + i);\n\t\t\tmap.put(\"test2\", \"test2_\" + i);\n\t\t\tmap.put(\"test3\", \"test3_\" + i);\n\t\t\tmap.put(\"test4\", \"test4_\" + i);\n\t\t\tmap.put(\"test5\", \"test5_\" + i);\n\t\t\tmap.put(\"test6\", \"test6_\" + i);\n\t\t\tmap.put(\"test7\", \"test7_\" + i);\n\t\t\tmap.put(\"test8\", \"test8_\" + i);\n\t\t\tmap.put(\"test9\", \"test9_\" + i);\n\t\t\tmap.put(\"test10\", \"test10_\" + i);\n\t\t\tmap.put(\"test20\", \"test20_\" + i);\n\t\t\tmap.put(\"test21\", \"test21_\" + i);\n\t\t\tmap.put(\"test22\", \"test22_\" + i);\n\t\t\tmap.put(\"test23\", \"test23_\" + i);\n\t\t\tmap.put(\"test24\", \"test24_\" + i);\n\t\t\tmap.put(\"test25\", \"test25_\" + i);\n\t\t\tmap.put(\"test26\", \"test26_\" + i);\n\t\t\tmap.put(\"test27\", \"test27_\" + i);\n\t\t\tmap.put(\"test28\", \"test28_\" + i);\n\t\t\tmap.put(\"test29\", \"test29_\" + i);\n\t\t\tmap.put(\"test30\", \"test30_\" + i);\n\t\t\tmap.put(\"test41\", \"test41_\" + i);\n\t\t\tmap.put(\"test42\", \"test42_\" + i);\n\t\t\tmap.put(\"test43\", \"test43_\" + i);\n\t\t\tmap.put(\"test44\", \"test44_\" + i);\n\t\t\tmap.put(\"test45\", \"test45_\" + i);\n\t\t\tmap.put(\"test46\", \"test46_\" + i);\n\t\t\tmap.put(\"test47\", \"test47_\" + i);\n\t\t\tmap.put(\"test48\", \"test48_\" + i);\n\t\t\tmap.put(\"test49\", \"test49_\" + i);\n\t\t\tmap.put(\"test50\", \"test50_\" + i);\n\t\t\tmap.put(\"test31\", \"test31_\" + i);\n\t\t\tmap.put(\"test32\", \"test32_\" + i);\n\t\t\tmap.put(\"test33\", \"test33_\" + i);\n\t\t\tmap.put(\"test34\", \"test34_\" + i);\n\t\t\tmap.put(\"test35\", \"test35_\" + i);\n\t\t\tmap.put(\"test36\", \"test36_\" + i);\n\t\t\tmap.put(\"test37\", \"test37_\" + i);\n\t\t\tmap.put(\"test38\", \"test38_\" + i);\n\t\t\tmap.put(\"test39\", \"test39_\" + i);\n\t\t\tmap.put(\"test40\", \"test40_\" + i);\n\t\t\tdataList.add(map);\n\t\t}\n\t\t// 使用比较器写出\n\t\ttry (BigExcelWriter excelWriter = ExcelUtil.getBigWriter(\"d:/writeWithComparatorTest.xlsx\")) {\n\t\t\texcelWriter.write(dataList, new IndexedComparator<>(\n\t\t\t\t\"test1\", \"test2\", \"test3\", \"test4\", \"test5\", \"test6\", \"test7\", \"test8\", \"test9\", \"test10\",\n\t\t\t\t\"test11\", \"test12\", \"test13\", \"test14\", \"test15\", \"test16\", \"test17\", \"test18\", \"test19\", \"test20\",\n\t\t\t\t\"test21\", \"test22\", \"test23\", \"test24\", \"test25\", \"test26\", \"test27\", \"test28\", \"test29\", \"test30\",\n\t\t\t\t\"test31\", \"test32\", \"test33\", \"test34\", \"test35\", \"test36\", \"test37\", \"test38\", \"test39\", \"test40\",\n\t\t\t\t\"test41\", \"test42\", \"test43\", \"test44\", \"test45\", \"test46\", \"test47\", \"test48\", \"test49\", \"test50\"\n\t\t\t));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue1729Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * issue#1729@Github<br>\n * 日期为空时返回\"\"而非null，因此会导致日期等字段的转换错误，此处转bean时忽略错误。\n */\npublic class Issue1729Test {\n\n\t@Test\n\tpublic void readTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"UserProjectDO.xlsx\");\n\t\tfinal List<UserProjectDO> read = reader.read(0, 1, UserProjectDO.class);\n\t\tassertEquals(\"aa\", read.get(0).getProjectName());\n\t\tassertNull(read.get(0).getEndTrainTime());\n\t\tassertEquals(\"2020-02-02\", read.get(0).getEndTestTime().toString());\n\t}\n\n\t@Data\n\tpublic static class UserProjectDO {\n\t\tprivate String projectName;\n\t\tprivate java.sql.Date endTrainTime;\n\t\tprivate java.sql.Date endTestTime;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue2221Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.ss.usermodel.FillPatternType;\nimport org.apache.poi.ss.usermodel.Font;\nimport org.apache.poi.ss.usermodel.IndexedColors;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class Issue2221Test {\n\n\t/**\n\t * 设置重复别名的时候，通过原key获取写出位置\n\t */\n\t@Test\n\t@Disabled\n\tpublic void writeDuplicateHeaderAliasTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/duplicateAlias.xlsx\");\n\t\t// 设置别名\n\t\twriter.addHeaderAlias(\"androidLc\", \"安卓\");\n\t\twriter.addHeaderAlias(\"androidAc\", \"安卓\");\n\t\twriter.setOnlyAlias(true);\n\n\t\t// 写入数据\n\t\tList<Map<Object, Object>> data = ListUtil.of(\n\t\t\t\tMapUtil.ofEntries(MapUtil.entry(\"androidLc\", \"1次\"), MapUtil.entry(\"androidAc\", \"3人\")),\n\t\t\t\tMapUtil.ofEntries(MapUtil.entry(\"androidLc\", \"1次\"), MapUtil.entry(\"androidAc\", \"3人\"))\n\t\t);\n\n\t\twriter.write(data, true);\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeDuplicateHeaderAliasTest2(){\n\t\t// 获取写Excel的流\n\t\tExcelWriter writer = ExcelUtil.getBigWriter(\"d:/test/duplicateAlias2.xlsx\");\n\n\t\t// 设置头部的背景颜色\n\t\tStyleUtil.setColor(writer.getHeadCellStyle(), IndexedColors.GREY_50_PERCENT, FillPatternType.SOLID_FOREGROUND);\n\n\t\t//设置全局字体\n\t\tFont font = writer.createFont();\n\t\tfont.setFontName(\"Microsoft YaHei\");\n\t\twriter.getStyleSet().setFont(font, false);\n\n\t\t// 设置头部的字体为白颜色\n\t\tFont headerFont = writer.createFont();\n\t\theaderFont.setColor(IndexedColors.WHITE.getIndex());\n\t\twriter.getHeadCellStyle().setFont(headerFont);\n\n\t\t// 跳过多少行\n\t\twriter.passRows(1);\n\n\t\t// 冻结多少行\n\t\twriter.setFreezePane(2);\n\n\t\t// 设置别名\n\t\twriter.addHeaderAlias(\"date\", \"日期\");\n\t\twriter.addHeaderAlias(\"androidLc\", \"安卓\");\n\t\twriter.addHeaderAlias(\"iosLc\", \"iOS\");\n\t\twriter.addHeaderAlias(\"androidAc\", \" 安卓\");\n\t\twriter.addHeaderAlias(\"iosAc\", \" iOS\");\n\t\twriter.setOnlyAlias(true);\n\n\t\t// 设置合并的单元格\n\t\twriter.merge(0, 1, 0, 0, \"日期\", true);\n\t\twriter.merge(0, 0, 1, 2, \"运行次数\", true);\n\t\twriter.merge(0, 0, 3, 4, \"新增人数\", true);\n\n\t\t// 写入数据\n\t\tList<Map<Object, Object>> data = ListUtil.of(\n\t\t\t\tMapUtil.ofEntries(\n\t\t\t\t\t\tMapUtil.entry(\"date\", \"2022-01-01\"),\n\t\t\t\t\t\tMapUtil.entry(\"androidLc\", \"1次\"),\n\t\t\t\t\t\tMapUtil.entry(\"iosLc\", \"2次\"),\n\t\t\t\t\t\tMapUtil.entry(\"androidAc\", \"3次\"),\n\t\t\t\t\t\tMapUtil.entry(\"iosAc\", \"4人\")),\n\t\t\t\tMapUtil.ofEntries(\n\t\t\t\t\t\tMapUtil.entry(\"date\", \"2022-01-02\"),\n\t\t\t\t\t\tMapUtil.entry(\"androidLc\", \"5次\"),\n\t\t\t\t\t\tMapUtil.entry(\"iosLc\", \"6次\"),\n\t\t\t\t\t\tMapUtil.entry(\"androidAc\", \"7次\"),\n\t\t\t\t\t\tMapUtil.entry(\"iosAc\", \"8人\"))\n\t\t);\n\n\t\t// 自动尺寸\n\t\twriter.autoSizeColumnAll();\n\n\t\twriter.write(data, true);\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue2706Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class Issue2706Test {\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tString path = \"d:/test/issue2706.xlsx\";\n\t\tBigExcelWriter writer = ExcelUtil.getBigWriter(path, \"表格名称\");\n\n\t\tSet<String> headSet1 = new HashSet<>(Arrays.asList(\"A\", \"B\", \"C\", \"D\", \"E\"));\n\n\t\tList<Map<String, String>> datas = new ArrayList<>();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tMap<String, String> map = new HashMap<>();\n\t\t\tfor (String s : headSet1) {\n\t\t\t\tmap.put(s, \"h\" + i);\n\t\t\t}\n\t\t\tdatas.add(map);\n\t\t}\n\t\twriter.write(datas);\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue2899Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.annotation.Alias;\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.io.FileUtil;\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue2899Test {\n\n\t@Test\n\t@Disabled\n\tpublic void aliasWriteTest() {\n\t\t// Bean中设置@Alias时，setOnlyAlias是无效的，这个参数只和addHeaderAlias配合使用，原因是注解是Bean内部的操作，而addHeaderAlias是Writer的操作，不互通。\n\t\tfinal TestBean testBean1 = new TestBean();\n\t\ttestBean1.setName(\"张三\");\n\t\ttestBean1.setScore(12);\n\n\t\tfinal TestBean testBean2 = new TestBean();\n\t\ttestBean2.setName(\"李四\");\n\t\ttestBean2.setScore(23);\n\n\t\tFileUtil.del(\"d:/test/aliasTest.xlsx\");\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/aliasTest.xlsx\");\n\n\t\twriter.addHeaderAlias(\"姓名\", \"姓名\");\n\t\twriter.setOnlyAlias(true);\n\t\twriter.merge(2, \"成绩单\");\n\t\twriter.write(CollUtil.newArrayList(testBean1, testBean2), true);\n\t\twriter.close();\n\t}\n\n\t@Data\n\tstatic class TestBean{\n\t\t@Alias(\"姓名\")\n\t\tprivate String name;\n\t\tprivate double score;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue2941Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class Issue2941Test {\n\t@Test\n\t@Disabled\n\tpublic void excelReadDateTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"d:/test/1677649503673.xlsx\");\n\t\tfinal List<Map<String, Object>> maps = reader.readAll();\n\t\tfor (Map<String, Object> map : maps) {\n\t\t\tConsole.log(map);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue3048Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * https://github.com/chinabugotech/hutool/issues/3048\n * Excel导出javaBean中有BigDecimal类型精度流失\n *\n */\npublic class Issue3048Test {\n\t@Test\n\t@Disabled\n\tpublic void excelOutPutBeanListToExcel(){\n\t\tList<TestBean> excelExportList = new ArrayList<>();\n\t\texcelExportList.add(new TestBean(\"1\", new BigDecimal(\"1.22\")));\n\t\texcelExportList.add(new TestBean(\"2\", new BigDecimal(\"2.342\")));\n\t\texcelExportList.add(new TestBean(\"3\", new BigDecimal(\"1.2346453453534534543545\")));\n\t\tExcelWriter excelWriter = ExcelUtil.getWriter(true);\n\t\t//excelWriter.setNumberAutoPrecision(true);\n\t\texcelWriter.write(excelExportList, true);\n\t\texcelWriter.getStyleSet().getCellStyleForNumber().setDataFormat((short) 0);\n\t\texcelWriter.flush(new File(\"d:/test/test.xlsx\"));\n\t\texcelWriter.close();\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class TestBean{\n\t\tprivate String testKey;\n\t\tprivate BigDecimal testValue;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue3698Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class Issue3698Test {\n\t@Test\n\t@Disabled\n\tvoid readTest() {\n\t\tExcelReader reader = ExcelUtil.getReader(\"d:/test/default.xlsx\", 0);\n\t\tList<List<Object>> list = reader.read(0, Integer.MAX_VALUE, false);\n\t\tfor (List<Object> row : list) {\n\t\t\tConsole.log(row.get(1).getClass());\n\t\t}\n\t\treader.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue3965Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Issue3965Test {\n\t@Test\n\t@Disabled\n\tvoid writeTest() {\n\t\tArrayList<List<String>> arrayList = new ArrayList<>();\n\n\t\tarrayList.add(ListUtil.of(\"a\"));\n\t\tarrayList.add(ListUtil.of(\"b\"));\n\t\tarrayList.add(ListUtil.of(\"c\"));\n\t\tarrayList.add(ListUtil.of(\"d\"));\n\n\t\tExcelWriter writer = ExcelUtil.getWriter(FileUtil.file(\"d:/test/123.xlsx\"));\n\t\twriter.setColumnWidth(0, 50);\n\t\twriter.write(arrayList);\n\t\twriter.flush();\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/Issue4146Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.apache.poi.ss.usermodel.BorderStyle;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.FillPatternType;\nimport org.apache.poi.ss.usermodel.IndexedColors;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Issue4146Test {\n\t@Test\n\t@Disabled\n\tpublic void writeSheetWithStyleTest() {\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:\\\\test\\\\issue4146.xlsx\", \"表格1\");\n\n\t\tList<TestUser> list = new ArrayList<>();\n\t\tTestUser test = new TestUser(\"张三\", 18, 90.0, 0.9878);\n\t\tlist.add(test);\n\t\ttest = new TestUser(\"李四\", 18, 79.5, 0.8311);\n\t\tlist.add(test);\n\t\ttest = new TestUser(\"王五\", 18, 89.9, 0.6932);\n\t\tlist.add(test);\n\t\ttest = new TestUser(\"赵六\", 18, 69.9, 0.7912);\n\t\tlist.add(test);\n\t\ttest = new TestUser(\"孙七\", 18, 79.9, 0.6432);\n\t\tlist.add(test);\n\n\t\twriter.addHeaderAlias(\"name\", \"姓名\");\n\t\twriter.addHeaderAlias(\"age\", \"年龄\");\n\t\twriter.addHeaderAlias(\"score\", \"分数\");\n\t\twriter.addHeaderAlias(\"zb\", \"占比\");\n\n\t\twriter.setOnlyAlias(true);\n\t\twriter.write(list, true);\n\n\t\t// 百分比的单元格样式必须单独创建，使用StyleSet中的样式修改则会修改全局样式\n\t\tCellStyle percentCellStyle = writer.createCellStyle();\n\t\tpercentCellStyle.setDataFormat(writer.getWorkbook().createDataFormat().getFormat(\"0.00%\"));\n\t\t// 填充背景颜色，必须指定FillPatternType才有效\n\t\tStyleUtil.setColor(percentCellStyle, IndexedColors.YELLOW, FillPatternType.SOLID_FOREGROUND);\n\t\t// 设置边框颜色和粗细\n\t\tStyleUtil.setBorder(percentCellStyle, BorderStyle.THIN, IndexedColors.BLACK);\n\t\tfinal int rowCount = writer.getRowCount();\n\t\t// 设置列样式无效，除非将默认样式清除，因此必须在写出数据后为单元格指定自定义的样式\n\t\tfor (int i = 1; i < rowCount; i++) {\n\t\t\twriter.setStyle(percentCellStyle, 3, i);\n\t\t}\n\n\t\twriter.close();\n\t}\n\n\t@Data\n\t@AllArgsConstructor\n\tstatic class TestUser {\n\t\tprivate String name;\n\t\tprivate Integer age;\n\t\tprivate Double score;\n\t\tprivate Double zb;\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/IssueI53OSTTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Sax方式读取合并单元格，只有第一个单元格有值，其余为null\n */\npublic class IssueI53OSTTest {\n\n\t@Test\n\t@Disabled\n\tpublic void readTest(){\n\t\tMap<String, Object> result = new HashMap<>();\n\t\tList<Object> header = new ArrayList<>();\n\n\t\tExcelUtil.readBySax(\"d:/test/sax_merge.xlsx\", -1, (sheetIndex, rowIndex, rowCells) -> {\n\t\t\tif(rowIndex == 0){\n\t\t\t\theader.addAll(rowCells);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (int i = 0; i < rowCells.size(); i++) {\n\t\t\t\tresult.put((String) header.get(i), rowCells.get(i));\n\t\t\t}\n\t\t\tConsole.log(result);\n\t\t\tresult.clear();\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/IssueI64P2KTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.ListUtil;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.Font;\nimport org.apache.poi.ss.usermodel.IndexedColors;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI64P2KTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeWithColumnStyleTest() {\n\t\t// 设置默认列样式无效，暂时无解。\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/columnColorTest.xlsx\");\n\t\twriter.disableDefaultStyle();\n\n\t\tfinal Font font = writer.createFont();\n\t\tfont.setColor(Font.COLOR_RED);\n\t\tfinal CellStyle style = writer.createColumnStyle(0);\n\t\tstyle.setFont(font);\n\t\tstyle.setFillBackgroundColor(IndexedColors.YELLOW.getIndex());\n\n\t\twriter.writeRow(ListUtil.toList(\"aaa\"));\n\t\twriter.writeRow(ListUtil.toList(\"aaa\"));\n\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/IssueI6MBS5Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.poi.excel.cell.CellUtil;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.Row;\nimport org.apache.poi.ss.usermodel.Sheet;\nimport org.apache.poi.ss.usermodel.Workbook;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\n\n/**\n * https://gitee.com/chinabugotech/hutool/issues/I6MBS5<br>\n * 经过测试，发现BigExcelWriter中的comment会错位<br>\n * 修正方式见: https://stackoverflow.com/questions/28169011/using-sxssfapache-poi-and-adding-comment-does-not-generate-proper-excel-file\n */\npublic class IssueI6MBS5Test {\n\n\t@Test\n\t@Disabled\n\tpublic void setCommentTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getBigWriter(\"d:/test/setCommentTest.xlsx\");\n\t\tfinal Cell cell = writer.getOrCreateCell(0, 0);\n\t\tCellUtil.setCellValue(cell, \"cellValue\");\n\t\tCellUtil.setComment(cell, \"commonText\", \"ascend\", null);\n\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void setCommentTest2() {\n\t\tfinal File file = new File(\"D:\\\\test\\\\CellUtilTest.xlsx\");\n\t\ttry (final Workbook workbook = WorkbookUtil.createBook(true)) {\n\t\t\tfinal Sheet sheet = workbook.createSheet();\n\t\t\tfinal Row row = sheet.createRow(0);\n\t\t\tfinal Cell cell = row.createCell(0);\n\t\t\tCellUtil.setCellValue(cell, \"cellValue\");\n\t\t\tCellUtil.setComment(cell, \"commonText\", \"ascend\", null);\n\t\t\tworkbook.write(Files.newOutputStream(file.toPath()));\n\t\t} catch (final IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/IssueI6URF3Test.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.poi.excel;\n\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.HorizontalAlignment;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI6URF3Test {\n\n\t@Test\n\t@Disabled\n\tpublic void setCellStyleTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getWriter(\"d:/test/issueI6URF3.xlsx\");\n\t\twriter.writeCellValue(0, 0, 1);\n\t\tfinal CellStyle cellStyle = writer.createCellStyle(0, 0);\n\t\tcellStyle.setAlignment(HorizontalAlignment.LEFT);\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/IssueIB0EJ9Test.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueIB0EJ9Test {\n\t@Test\n\t@Disabled\n\tvoid saxReadTest() {\n\t\tExcelUtil.readBySax(FileUtil.file(\"d:/test/数值型测试.xlsx\"), \"hcm工资表\",\n\t\t\t(sheetIndex, rowIndex, rowlist) -> Console.log(\"[{}] [{}] {}\", sheetIndex, rowIndex, rowlist));\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/NumericCellValueTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.poi.excel.cell.values.NumericCellValue;\nimport java.util.Date;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.junit.jupiter.api.Test;\n\npublic class NumericCellValueTest {\n\n\t@Test\n\tpublic void writeTest() {\n\t\tfinal ExcelReader reader = ExcelUtil.getReader(\"1899bug_demo.xlsx\");\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"1899bug_write.xlsx\");\n\t\tCell cell = reader.getCell(0, 0);\n\t\t// 直接取值\n\t\t// 和CellUtil.getCellValue(org.apache.poi.ss.usermodel.Cell)方法的结果一样\n\t\t// 1899-12-31 04:39:00\n\t\tDate cellValue = cell.getDateCellValue();\n\t\t// 将这个值写入EXCEL中自定义样式的单元格，结果会是-1\n\t\twriter.writeCellValue(0, 0, cellValue);\n\t\t// 修改后的写入，单元格内容正常\n\t\twriter.writeCellValue(1, 0, new NumericCellValue(cell).getValue());\n\t\twriter.close();\n\t\treader.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java",
    "content": "package cn.hutool.poi.excel;\n\nimport lombok.Data;\n\n@Data\npublic class OrderExcel {\n\tprivate String id;\n\tprivate String num;\n\tprivate String body;\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java",
    "content": "package cn.hutool.poi.excel;\n\nimport lombok.Data;\n\nimport java.util.Date;\n\n@Data\npublic class TestBean {\n\tprivate String name;\n\tprivate int age;\n\tprivate double score;\n\tprivate boolean isPass;\n\tprivate Date examDate;\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/WorkbookUtilTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport org.apache.poi.ss.usermodel.Workbook;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class WorkbookUtilTest {\n\n\t@Test\n\tpublic void createBookTest(){\n\t\tWorkbook book = WorkbookUtil.createBook(true);\n\t\tassertNotNull(book);\n\n\t\tbook = WorkbookUtil.createBook(false);\n\t\tassertNotNull(book);\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/WriteNumberToStringTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.poi.excel.cell.FormulaCellValue;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class WriteNumberToStringTest {\n\t@Test\n\t@Disabled\n\tpublic void writeNumberTest() {\n\t\tfinal ExcelWriter writer = ExcelUtil.getBigWriter(\"d:/test/dataWithNumber.xlsx\");\n\n\t\twriter.writeRow(ListUtil.of(\"姓名\", \"编号\"));\n\t\twriter.writeRow(ListUtil.of(\"张三\", new FormulaCellValue(\"010001\")));\n\t\twriter.writeRow(ListUtil.of(\"李四\", new FormulaCellValue(\"120001\")));\n\t\twriter.writeRow(ListUtil.of(\"王五\", 123456));\n\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/WriteStyleTest.java",
    "content": "package cn.hutool.poi.excel;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.poi.excel.cell.CellSetter;\nimport cn.hutool.poi.excel.style.StyleUtil;\nimport org.apache.poi.ss.usermodel.Cell;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.apache.poi.ss.usermodel.Font;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * 设置列样式测试\n */\npublic class WriteStyleTest {\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tList<?> row1 = CollUtil.newArrayList(\"aaaaa\", \"bb\", \"cc\", \"dd\", DateUtil.date(), 3.22676575765);\n\t\tList<?> row2 = CollUtil.newArrayList(\"aa1\", \"bb1\", \"cc1\", \"dd1\", DateUtil.date(), 250.7676);\n\t\tList<?> row3 = CollUtil.newArrayList(\"aa2\", \"bb2\", \"cc2\", \"dd2\", DateUtil.date(), 0.111);\n\t\tList<?> row4 = CollUtil.newArrayList(\"aa3\", \"bb3\", \"cc3\", \"dd3\", DateUtil.date(), 35);\n\t\tList<?> row5 = CollUtil.newArrayList(\"aa4\", \"bb4\", \"cc4\", \"dd4\", DateUtil.date(), 28.00);\n\t\tList<List<?>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);\n\n\t\tExcelWriter writer = ExcelUtil.getWriter(\"d:/test/writeTest.xlsx\");\n\t\twriter.setStyleSet(null);\n\n\t\tfinal CellStyle cellStyle = StyleUtil.createDefaultCellStyle(writer.getWorkbook());\n\t\tcellStyle.setFont(StyleUtil.createFont(writer.getWorkbook(), Font.COLOR_RED, (short) 12, \"宋体\"));\n\n\t\twriter.write(rows);\n\n\t\twriter.writeCellValue(0, 0, (CellSetter) cell -> {\n\t\t\tcell.setCellStyle(cellStyle);\n\t\t\tcell.setCellValue(\"1234\");\n\t\t});\n\n\t\tfor (int i = 0; i < writer.getRowCount(); i++) {\n\t\t\twriter.setStyle(cellStyle, 2, i);\n\t\t}\n\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/sax/ExcelSaxReadTest.java",
    "content": "package cn.hutool.poi.excel.sax;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.poi.excel.ExcelUtil;\nimport cn.hutool.poi.excel.cell.FormulaCellValue;\nimport cn.hutool.poi.excel.sax.handler.RowHandler;\nimport cn.hutool.poi.exceptions.POIException;\nimport org.apache.poi.ss.usermodel.CellStyle;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * Excel sax方式读取\n *\n * @author looly\n */\npublic class ExcelSaxReadTest {\n\n\t@Test\n\tpublic void excel07Test() {\n\t\t// 工具化快速读取\n\t\tExcelUtil.readBySax(\"aaa.xlsx\", 0, createRowHandler());\n\t}\n\n\t@Test\n\tvoid readEndByExceptionTest(){\n\t\tExcelUtil.readBySax(\"aaa.xlsx\", 0, (sheetIndex, rowIndex, rowList) -> {\n\t\t\tif (rowIndex == 1) {\n\t\t\t\tthrow new StopReadException();\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\tvoid readEndByException03Test(){\n\t\tExcelUtil.readBySax(\"aaa.xls\", 0, (sheetIndex, rowIndex, rowList) -> {\n\t\t\tif (rowIndex == 1) {\n\t\t\t\tthrow new StopReadException();\n\t\t\t}\n\t\t});\n\t}\n\n\t@Test\n\tpublic void excel07ByNameTest() {\n\t\t// 工具化快速读取\n\t\t// sheet名称是区分大小写的\n\t\tExcelUtil.readBySax(\"aaa.xlsx\", \"Sheet1\", createRowHandler());\n\t\t// 纯数字名称也支持\n\t\tExcelUtil.readBySax(\"aaa.xlsx\", \"12\", createRowHandler());\n\t\t// 前缀支持\n\t\tExcelUtil.readBySax(\"aaa.xlsx\", \"sheetName:12\", createRowHandler());\n\t}\n\n\t@Test\n\tpublic void excel07FromStreamTest() {\n\t\t// issue#1225 非markSupport的流读取会错误\n\t\tExcelUtil.readBySax(IoUtil.toStream(FileUtil.file(\"aaa.xlsx\")), 0, createRowHandler());\n\t}\n\n\t@Test\n\tpublic void excel03Test() {\n\t\tfinal Excel03SaxReader reader = new Excel03SaxReader(createRowHandler());\n\t\treader.read(\"aaa.xls\", 1);\n\n\t\t// Console.log(\"Sheet index: [{}], Sheet name: [{}]\", reader.getSheetIndex(), reader.getSheetName());\n\t\tExcelUtil.readBySax(\"aaa.xls\", 1, createRowHandler());\n\t}\n\n\t@Test\n\tpublic void excel03ByNameTest() {\n\t\tfinal Excel03SaxReader reader = new Excel03SaxReader(createRowHandler());\n\t\treader.read(\"aaa.xls\", \"校园入学\");\n\t\treader.read(\"aaa.xls\", \"sheetName:校园入学\");\n\t}\n\n\t@Test\n\tpublic void excel03ByNameErrorTest() {\n\t\tassertThrows(POIException.class, () -> {\n\t\t\t// sheet名称不存在则报错\n\t\t\tfinal Excel03SaxReader reader = new Excel03SaxReader(createRowHandler());\n\t\t\treader.read(\"aaa.xls\", \"校园入学1\");\n\t\t});\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readBlankLineTest() {\n\t\tExcelUtil.readBySax(\"e:/ExcelBlankLine.xlsx\", 0, (sheetIndex, rowIndex, rowList) -> {\n\t\t\tif (StrUtil.isAllEmpty(Convert.toStrArray(rowList))) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tConsole.log(rowList);\n\t\t});\n\t}\n\n\t@Test\n\tpublic void readBySaxTest() {\n\t\tExcelUtil.readBySax(\"blankAndDateTest.xlsx\", \"0\", createRowHandler());\n\t}\n\n\t@Test\n\tpublic void readBySaxByRidTest() {\n\t\tExcelUtil.readBySax(\"blankAndDateTest.xlsx\", 0, createRowHandler());\n\t}\n\n\t@Test\n\tpublic void readBySaxByNameTest() {\n\t\tExcelUtil.readBySax(\"blankAndDateTest.xlsx\", \"Sheet1\", createRowHandler());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readBySaxTest2() {\n\t\tExcelUtil.readBySax(\"d:/test/456789.xlsx\", \"0\", (sheetIndex, rowIndex, rowList) -> Console.log(rowList));\n\t}\n\n\tprivate RowHandler createRowHandler() {\n\t\treturn (sheetIndex, rowIndex, rowlist) -> {\n//\t\t\tConsole.log(\"[{}] [{}] {}\", sheetIndex, rowIndex, rowlist);\n\t\t\tif (5 != rowIndex && 6 != rowIndex) {\n\t\t\t\t// 测试样例中除第五行、第六行都为非空行\n\t\t\t\tassertTrue(CollUtil.isNotEmpty(rowlist));\n\t\t\t}\n\t\t};\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void handle07CellTest() {\n\t\tExcelUtil.readBySax(\"d:/test/test.xlsx\", -1, new RowHandler() {\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void handleCell(final int sheetIndex, final long rowIndex, final int cellIndex, final Object value, final CellStyle xssfCellStyle) {\n\t\t\t\t\t\tConsole.log(\"{} {} {}\", rowIndex, cellIndex, value);\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void handle(final int sheetIndex, final long rowIndex, final List<Object> rowCells) {\n\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t);\n\t}\n\n\t@Test\n\tpublic void handle03CellTest() {\n\t\tExcelUtil.readBySax(\"test.xls\", -1, new RowHandler() {\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void handleCell(final int sheetIndex, final long rowIndex, final int cellIndex, final Object value, final CellStyle xssfCellStyle) {\n\t\t\t\t\t\t//Console.log(\"{} {} {}\", rowIndex, cellIndex, value);\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void handle(final int sheetIndex, final long rowIndex, final List<Object> rowCells) {\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t);\n\t}\n\n\t@Test\n\tpublic void formulaRead03Test() {\n\t\tfinal List<Object> rows = new ArrayList<>();\n\t\tExcelUtil.readBySax(\"data_for_sax_test.xls\", -1, (i, i1, list) -> {\n\t\t\tif (list.size() > 1) {\n\t\t\t\trows.add(list.get(1));\n\t\t\t} else {\n\t\t\t\trows.add(\"\");\n\t\t\t}\n\t\t});\n\t\tassertEquals(50L, rows.get(3));\n\t}\n\n\t@Test\n\tpublic void formulaRead07Test() {\n\t\tfinal List<Object> rows = new ArrayList<>();\n\t\tExcelUtil.readBySax(\"data_for_sax_test.xlsx\", 0, (i, i1, list) ->\n\t\t\t\trows.add(list.get(1)));\n\n\t\tfinal FormulaCellValue value = (FormulaCellValue) rows.get(3);\n\t\tassertEquals(50L, value.getResult());\n\t}\n\n\t@Test\n\tpublic void dateReadXlsTest() {\n\t\tfinal List<String> rows = new ArrayList<>();\n\t\tExcelUtil.readBySax(\"data_for_sax_test.xls\", 0,\n\t\t\t\t(i, i1, list) -> rows.add(StrUtil.toString(list.get(0)))\n\t\t);\n\n\t\tassertEquals(\"2020-10-09 00:00:00\", rows.get(1));\n\t\t// 非日期格式不做转换\n\t\tassertEquals(\"112233\", rows.get(2));\n\t\tassertEquals(\"1000.0\", rows.get(3));\n\t\tassertEquals(\"2012-12-21 00:00:00\", rows.get(4));\n\t}\n\n\t@Test\n\tpublic void dateReadXlsxTest() {\n\t\tfinal List<String> rows = new ArrayList<>();\n\t\tExcelUtil.readBySax(\"data_for_sax_test.xlsx\", 0,\n\t\t\t\t(i, i1, list) -> rows.add(StrUtil.toString(list.get(0)))\n\t\t);\n\n\t\tassertEquals(\"2020-10-09 00:00:00\", rows.get(1));\n\t\t// 非日期格式不做转换\n\t\tassertEquals(\"112233\", rows.get(2));\n\t\tassertEquals(\"1000.0\", rows.get(3));\n\t\tassertEquals(\"2012-12-21 00:00:00\", rows.get(4));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void dateReadXlsxTest2() {\n\t\tExcelUtil.readBySax(\"d:/test/custom_date_format2.xlsx\", 0,\n\t\t\t\t(i, i1, list) -> Console.log(list)\n\t\t);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readBlankTest() {\n\t\tfinal File file = new File(\"D:/test/b.xlsx\");\n\n\t\tExcelUtil.readBySax(file, 0, (sheetIndex, rowIndex, rowList) -> rowList.forEach(Console::log));\n\n\t\tExcelUtil.getReader(file).read().forEach(Console::log);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void readXlsmTest() {\n\t\tExcelUtil.readBySax(\"d:/test/WhiteListTemplate.xlsm\", -1,\n\t\t\t\t(sheetIndex, rowIndex, rowlist) -> Console.log(\"[{}] [{}] {}\", sheetIndex, rowIndex, rowlist));\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/excel/sax/Issue4195Test.java",
    "content": "package cn.hutool.poi.excel.sax;\r\n\r\nimport cn.hutool.core.lang.Console;\r\nimport cn.hutool.poi.excel.ExcelUtil;\r\nimport cn.hutool.poi.excel.cell.FormulaCellValue;\r\nimport org.junit.jupiter.api.Disabled;\r\nimport org.junit.jupiter.api.Test;\r\n\r\npublic class Issue4195Test {\r\n\t@Test\r\n\t@Disabled\r\n\tvoid saxReadFormulaTest() {\r\n\t\t// 测试公式读取\r\n\t\tExcelUtil.readBySax(\"formula_test.xlsx\", -1, (sheetIndex, rowIndex, rowCells) -> {\r\n\t\t\tfinal Object value = rowCells.get(2);\r\n\t\t\tif(value instanceof FormulaCellValue) {\r\n\t\t\t\tfinal FormulaCellValue result = ((FormulaCellValue) value);\r\n\t\t\t\tConsole.log(\"公式 {} 结果: {}\", result.getValue(), result.getResult());\r\n\t\t\t}else{\r\n\t\t\t\tConsole.log(\"非公式: {}\", value.getClass());\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n}\r\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/ofd/OfdWriterTest.java",
    "content": "package cn.hutool.poi.ofd;\n\nimport cn.hutool.core.io.FileUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class OfdWriterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest(){\n\t\tfinal OfdWriter ofdWriter = new OfdWriter(FileUtil.file(\"d:/test/test.ofd\"));\n\t\tofdWriter.addText(null, \"测试文本\");\n\t\tofdWriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java",
    "content": "package cn.hutool.poi.word;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Console;\nimport org.apache.poi.xwpf.usermodel.XWPFParagraph;\nimport org.apache.poi.xwpf.usermodel.XWPFRun;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.awt.*;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WordWriterTest {\n\n\t@Test\n\t@Disabled\n\tpublic void writeTest() {\n\t\tWord07Writer writer = new Word07Writer();\n\t\twriter.addText(new Font(\"方正小标宋简体\", Font.PLAIN, 22), \"我是第一部分\", \"我是第二部分\");\n\t\twriter.addText(new Font(\"宋体\", Font.PLAIN, 22), \"我是正文第一部分\", \"我是正文第二部分\");\n\t\twriter.addText(new Font(\"宋体\", Font.PLAIN, 22), Color.RED, \"我是正文第三部分\", \"我是正文第四部分\");\n\t\twriter.flush(FileUtil.file(\"e:/wordWrite.docx\"));\n\t\twriter.close();\n\t\tConsole.log(\"OK\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writePicTest() {\n\t\tWord07Writer writer = new Word07Writer();\n\t\twriter.addPicture(new File(\"d:\\\\test\\\\qrcodeCustom.jpg\"), 100, 200);\n\t\t// 写出到文件\n\t\twriter.flush(FileUtil.file(\"d:/test/writePic.docx\"));\n\t\t// 关闭\n\t\twriter.close();\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeTableTest(){\n\t\tfinal Word07Writer writer = new Word07Writer();\n\t\tMap<String, Object> map = new LinkedHashMap<>();\n\t\tmap.put(\"姓名\", \"张三\");\n\t\tmap.put(\"年龄\", \"23\");\n\t\tmap.put(\"成绩\", 88.32);\n\t\tmap.put(\"是否合格\", true);\n\n\t\twriter.addTable(CollUtil.newArrayList(map));\n\t\twriter.flush(FileUtil.file(\"d:/test/test.docx\"));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void writeMapAsTableTest() {\n\t\tWord07Writer writer = new Word07Writer();\n\n\t\tMap<String, Object> data = new LinkedHashMap<>();\n\t\tdata.put(\"姓名\", \"张三\");\n\t\tdata.put(\"年龄\", 23);\n\t\tdata.put(\"成绩\", 80.5);\n\t\tdata.put(\"是否合格\", true);\n\t\tdata.put(\"考试日期\", DateUtil.date());\n\n\t\tMap<String, Object> data2 = new LinkedHashMap<>();\n\t\tdata2.put(\"姓名\", \"李四\");\n\t\tdata2.put(\"年龄\", 4);\n\t\tdata2.put(\"成绩\", 59);\n\t\tdata2.put(\"是否合格\", false);\n\t\tdata2.put(\"考试日期\", DateUtil.date());\n\n\t\tArrayList<Map<String, Object>> mapArrayList = CollUtil.newArrayList(data, data2);\n\n\t\t// 添加段落（标题）\n\t\twriter.addText(new Font(\"方正小标宋简体\", Font.PLAIN, 22), \"我是第一部分\");\n\t\t// 添加段落（正文）\n\t\twriter.addText(new Font(\"宋体\", Font.PLAIN, 13), \"我是正文第一部分\");\n\t\twriter.addTable(mapArrayList);\n\t\t// 写出到文件\n\t\twriter.flush(FileUtil.file(\"d:/test/a.docx\"));\n\t\t// 关闭\n\t\twriter.close();\n\t}\n\n\t@Test\n\tpublic void overflowTest(){\n\t\tfinal Word07Writer word07Writer = new Word07Writer();\n\t\tfinal List<Object> list = ListUtil.list(false);\n\t\tfinal List<Object> list2 = ListUtil.list(false);\n\t\tlist.add(\"溢出测试\");\n\t\tlist2.add(list);\n\t\tword07Writer.addTable(list);\n\t\tword07Writer.close();\n\t}\n\n\t@Test\n\tpublic void addTextShouldStripAlphaAndUseRgbHex() {\n\t\tfinal Word07Writer writer = new Word07Writer();\n\t\tfinal Color colorWithAlpha = new Color(0x12, 0x34, 0x56, 0x7F);\n\n\t\twriter.addText(new Font(\"宋体\", Font.PLAIN, 12), colorWithAlpha, \"带颜色的段落\");\n\n\t\tfinal XWPFParagraph paragraph = writer.getDoc().getParagraphArray(0);\n\t\tfinal XWPFRun run = paragraph.getRuns().get(0);\n\t\tassertEquals(\"123456\", run.getColor());\n\t\twriter.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-script/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-script</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 脚本执行封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.script</Automatic-Module-Name>\n\n\t\t<jython.version>2.7.3</jython.version>\n\t\t<luaj.version>3.0.1</luaj.version>\n\t\t<groovy.version>3.0.22</groovy.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.python</groupId>\n\t\t\t<artifactId>jython</artifactId>\n\t\t\t<version>${jython.version}</version>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.luaj</groupId>\n\t\t\t<artifactId>luaj-jse</artifactId>\n\t\t\t<version>${luaj.version}</version>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.codehaus.groovy</groupId>\n\t\t\t<artifactId>groovy-all</artifactId>\n\t\t\t<version>${groovy.version}</version>\n\t\t\t<type>pom</type>\n\t\t\t<scope>provided</scope>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<artifactId>junit</artifactId>\n\t\t\t\t\t<groupId>junit</groupId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-script/src/main/java/cn/hutool/script/FullSupportScriptEngine.java",
    "content": "package cn.hutool.script;\n\nimport java.io.Reader;\n\nimport javax.script.Bindings;\nimport javax.script.Compilable;\nimport javax.script.CompiledScript;\nimport javax.script.Invocable;\nimport javax.script.ScriptContext;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineFactory;\nimport javax.script.ScriptEngineManager;\nimport javax.script.ScriptException;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 全功能引擎类，支持Compilable和Invocable\n * \n * @author Looly\n *\n */\npublic class FullSupportScriptEngine implements ScriptEngine, Compilable, Invocable {\n\n\tScriptEngine engine;\n\n\t/**\n\t * 构造\n\t * \n\t * @param engine 脚本引擎\n\t */\n\tpublic FullSupportScriptEngine(ScriptEngine engine) {\n\t\tthis.engine = engine;\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param nameOrExtOrMime 脚本名或者脚本语言扩展名或者MineType\n\t */\n\tpublic FullSupportScriptEngine(String nameOrExtOrMime) {\n\t\tfinal ScriptEngineManager manager = new ScriptEngineManager();\n\t\tScriptEngine engine = manager.getEngineByName(nameOrExtOrMime);\n\t\tif (null == engine) {\n\t\t\tengine = manager.getEngineByExtension(nameOrExtOrMime);\n\t\t}\n\t\tif (null == engine) {\n\t\t\tengine = manager.getEngineByMimeType(nameOrExtOrMime);\n\t\t}\n\t\tif (null == engine) {\n\t\t\tthrow new NullPointerException(StrUtil.format(\"Script for [{}] not support !\", nameOrExtOrMime));\n\t\t}\n\t\tthis.engine = engine;\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- Invocable\n\t@Override\n\tpublic Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {\n\t\treturn ((Invocable) engine).invokeMethod(thiz, name, args);\n\t}\n\n\t@Override\n\tpublic Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {\n\t\treturn ((Invocable) engine).invokeFunction(name, args);\n\t}\n\n\t@Override\n\tpublic <T> T getInterface(Class<T> clasz) {\n\t\treturn ((Invocable) engine).getInterface(clasz);\n\t}\n\n\t@Override\n\tpublic <T> T getInterface(Object thiz, Class<T> clasz) {\n\t\treturn ((Invocable) engine).getInterface(thiz, clasz);\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- Compilable\n\t@Override\n\tpublic CompiledScript compile(String script) throws ScriptException {\n\t\treturn ((Compilable) engine).compile(script);\n\t}\n\n\t@Override\n\tpublic CompiledScript compile(Reader script) throws ScriptException {\n\t\treturn ((Compilable) engine).compile(script);\n\t}\n\n\t// ----------------------------------------------------------------------------------------------- ScriptEngine\n\t@Override\n\tpublic Object eval(String script, ScriptContext context) throws ScriptException {\n\t\treturn engine.eval(script, context);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader, ScriptContext context) throws ScriptException {\n\t\treturn engine.eval(reader, context);\n\t}\n\n\t@Override\n\tpublic Object eval(String script) throws ScriptException {\n\t\treturn engine.eval(script);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader) throws ScriptException {\n\t\treturn engine.eval(reader);\n\t}\n\n\t@Override\n\tpublic Object eval(String script, Bindings n) throws ScriptException {\n\t\treturn engine.eval(script, n);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader, Bindings n) throws ScriptException {\n\t\treturn engine.eval(reader, n);\n\t}\n\n\t@Override\n\tpublic void put(String key, Object value) {\n\t\tengine.put(key, value);\n\t}\n\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn engine.get(key);\n\t}\n\n\t@Override\n\tpublic Bindings getBindings(int scope) {\n\t\treturn engine.getBindings(scope);\n\t}\n\n\t@Override\n\tpublic void setBindings(Bindings bindings, int scope) {\n\t\tengine.setBindings(bindings, scope);\n\t}\n\n\t@Override\n\tpublic Bindings createBindings() {\n\t\treturn engine.createBindings();\n\t}\n\n\t@Override\n\tpublic ScriptContext getContext() {\n\t\treturn engine.getContext();\n\t}\n\n\t@Override\n\tpublic void setContext(ScriptContext context) {\n\t\tengine.setContext(context);\n\t}\n\n\t@Override\n\tpublic ScriptEngineFactory getFactory() {\n\t\treturn engine.getFactory();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-script/src/main/java/cn/hutool/script/JavaScriptEngine.java",
    "content": "package cn.hutool.script;\n\nimport javax.script.Bindings;\nimport javax.script.Compilable;\nimport javax.script.CompiledScript;\nimport javax.script.Invocable;\nimport javax.script.ScriptContext;\nimport javax.script.ScriptEngineFactory;\nimport javax.script.ScriptException;\nimport java.io.Reader;\n\n/**\n * Javascript引擎类\n *\n * @author Looly\n */\npublic class JavaScriptEngine extends FullSupportScriptEngine {\n\n\tpublic JavaScriptEngine() {\n\t\tsuper(ScriptUtil.createJsEngine());\n\t}\n\n\t/**\n\t * 引擎实例\n\t *\n\t * @return 引擎实例\n\t */\n\tpublic static JavaScriptEngine instance() {\n\t\treturn new JavaScriptEngine();\n\t}\n\n\t//----------------------------------------------------------------------------------------------- Invocable\n\t@Override\n\tpublic Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {\n\t\treturn ((Invocable) engine).invokeMethod(thiz, name, args);\n\t}\n\n\t@Override\n\tpublic Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {\n\t\treturn ((Invocable) engine).invokeFunction(name, args);\n\t}\n\n\t@Override\n\tpublic <T> T getInterface(Class<T> clasz) {\n\t\treturn ((Invocable) engine).getInterface(clasz);\n\t}\n\n\t@Override\n\tpublic <T> T getInterface(Object thiz, Class<T> clasz) {\n\t\treturn ((Invocable) engine).getInterface(thiz, clasz);\n\t}\n\n\t//----------------------------------------------------------------------------------------------- Compilable\n\t@Override\n\tpublic CompiledScript compile(String script) throws ScriptException {\n\t\treturn ((Compilable) engine).compile(script);\n\t}\n\n\t@Override\n\tpublic CompiledScript compile(Reader script) throws ScriptException {\n\t\treturn ((Compilable) engine).compile(script);\n\t}\n\n\t//----------------------------------------------------------------------------------------------- ScriptEngine\n\t@Override\n\tpublic Object eval(String script, ScriptContext context) throws ScriptException {\n\t\treturn engine.eval(script, context);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader, ScriptContext context) throws ScriptException {\n\t\treturn engine.eval(reader, context);\n\t}\n\n\t@Override\n\tpublic Object eval(String script) throws ScriptException {\n\t\treturn engine.eval(script);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader) throws ScriptException {\n\t\treturn engine.eval(reader);\n\t}\n\n\t@Override\n\tpublic Object eval(String script, Bindings n) throws ScriptException {\n\t\treturn engine.eval(script, n);\n\t}\n\n\t@Override\n\tpublic Object eval(Reader reader, Bindings n) throws ScriptException {\n\t\treturn engine.eval(reader, n);\n\t}\n\n\t@Override\n\tpublic void put(String key, Object value) {\n\t\tengine.put(key, value);\n\t}\n\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn engine.get(key);\n\t}\n\n\t@Override\n\tpublic Bindings getBindings(int scope) {\n\t\treturn engine.getBindings(scope);\n\t}\n\n\t@Override\n\tpublic void setBindings(Bindings bindings, int scope) {\n\t\tengine.setBindings(bindings, scope);\n\t}\n\n\t@Override\n\tpublic Bindings createBindings() {\n\t\treturn engine.createBindings();\n\t}\n\n\t@Override\n\tpublic ScriptContext getContext() {\n\t\treturn engine.getContext();\n\t}\n\n\t@Override\n\tpublic void setContext(ScriptContext context) {\n\t\tengine.setContext(context);\n\t}\n\n\t@Override\n\tpublic ScriptEngineFactory getFactory() {\n\t\treturn engine.getFactory();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java",
    "content": "package cn.hutool.script;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.script.ScriptException;\n\n/**\n * 脚本运行时异常\n *\n * @author xiaoleilu\n */\npublic class ScriptRuntimeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tprivate String fileName;\n\tprivate int lineNumber = -1;\n\tprivate int columnNumber = -1;\n\n\tpublic ScriptRuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic ScriptRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic ScriptRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic ScriptRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic ScriptRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic ScriptRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n\n\t/**\n\t * Creates a {@code ScriptException} with message, filename and linenumber to be used in error messages.\n\t *\n\t * @param message    The string to use in the message\n\t * @param fileName   The file or resource name describing the location of a script error causing the {@code ScriptException} to be thrown.\n\t * @param lineNumber A line number describing the location of a script error causing the {@code ScriptException} to be thrown.\n\t */\n\tpublic ScriptRuntimeException(String message, String fileName, int lineNumber) {\n\t\tsuper(message);\n\t\tthis.fileName = fileName;\n\t\tthis.lineNumber = lineNumber;\n\t}\n\n\t/**\n\t * {@code ScriptException} constructor specifying message, filename, line number and column number.\n\t *\n\t * @param message      The message.\n\t * @param fileName     The filename\n\t * @param lineNumber   the line number.\n\t * @param columnNumber the column number.\n\t */\n\tpublic ScriptRuntimeException(String message, String fileName, int lineNumber, int columnNumber) {\n\t\tsuper(message);\n\t\tthis.fileName = fileName;\n\t\tthis.lineNumber = lineNumber;\n\t\tthis.columnNumber = columnNumber;\n\t}\n\n\tpublic ScriptRuntimeException(ScriptException e) {\n\t\tsuper(e);\n\t\tthis.fileName = e.getFileName();\n\t\tthis.lineNumber = e.getLineNumber();\n\t\tthis.columnNumber = e.getColumnNumber();\n\t}\n\n\t/**\n\t * Returns a message containing the String passed to a constructor as well as line and column numbers and filename if any of these are known.\n\t *\n\t * @return The error message.\n\t */\n\t@Override\n\tpublic String getMessage() {\n\t\tStringBuilder ret = new StringBuilder().append(super.getMessage());\n\t\tif (fileName != null) {\n\t\t\tret.append(\" in \").append(fileName);\n\t\t\tif (lineNumber != -1) {\n\t\t\t\tret.append(\" at line number \").append(lineNumber);\n\t\t\t}\n\n\t\t\tif (columnNumber != -1) {\n\t\t\t\tret.append(\" at column number \").append(columnNumber);\n\t\t\t}\n\t\t}\n\n\t\treturn ret.toString();\n\t}\n\n\t/**\n\t * Get the line number on which an error occurred.\n\t *\n\t * @return The line number. Returns -1 if a line number is unavailable.\n\t */\n\tpublic int getLineNumber() {\n\t\treturn lineNumber;\n\t}\n\n\t/**\n\t * Get the column number on which an error occurred.\n\t *\n\t * @return The column number. Returns -1 if a column number is unavailable.\n\t */\n\tpublic int getColumnNumber() {\n\t\treturn columnNumber;\n\t}\n\n\t/**\n\t * Get the source of the script causing the error.\n\t *\n\t * @return The file name of the script or some other string describing the script source. May return some implementation-defined string such as <i>&lt;unknown&gt;</i> if a description of the\n\t * source is unavailable.\n\t */\n\tpublic String getFileName() {\n\t\treturn fileName;\n\t}\n}\n"
  },
  {
    "path": "hutool-script/src/main/java/cn/hutool/script/ScriptUtil.java",
    "content": "package cn.hutool.script;\n\nimport cn.hutool.core.map.reference.WeakKeyValueConcurrentMap;\nimport cn.hutool.core.util.StrUtil;\n\nimport javax.script.*;\n\n/**\n * 脚本工具类\n *\n * @author Looly\n */\npublic class ScriptUtil {\n\n\tprivate static final ScriptEngineManager MANAGER = new ScriptEngineManager();\n\tprivate static final WeakKeyValueConcurrentMap<String, ScriptEngine> CACHE = new WeakKeyValueConcurrentMap<>();\n\n\t/**\n\t * 获得单例的{@link ScriptEngine} 实例\n\t *\n\t * @param nameOrExtOrMime 脚本名称\n\t * @return {@link ScriptEngine} 实例\n\t */\n\tpublic static ScriptEngine getScript(String nameOrExtOrMime) {\n\t\treturn CACHE.computeIfAbsent(nameOrExtOrMime, (key) -> createScript(nameOrExtOrMime));\n\t}\n\n\t/**\n\t * 创建 {@link ScriptEngine} 实例\n\t *\n\t * @param nameOrExtOrMime 脚本名称\n\t * @return {@link ScriptEngine} 实例\n\t * @since 5.2.6\n\t */\n\tpublic static ScriptEngine createScript(String nameOrExtOrMime) {\n\t\tScriptEngine engine = MANAGER.getEngineByName(nameOrExtOrMime);\n\t\tif (null == engine) {\n\t\t\tengine = MANAGER.getEngineByExtension(nameOrExtOrMime);\n\t\t}\n\t\tif (null == engine) {\n\t\t\tengine = MANAGER.getEngineByMimeType(nameOrExtOrMime);\n\t\t}\n\t\tif (null == engine) {\n\t\t\tthrow new NullPointerException(StrUtil.format(\"Script for [{}] not support !\", nameOrExtOrMime));\n\t\t}\n\t\treturn engine;\n\t}\n\n\t/**\n\t * 获得非单例的 Javascript引擎 {@link JavaScriptEngine}\n\t *\n\t * @return {@link JavaScriptEngine}\n\t */\n\tpublic static JavaScriptEngine getJavaScriptEngine() {\n\t\treturn new JavaScriptEngine();\n\t}\n\n\t/**\n\t * 获得单例的JavaScript引擎\n\t *\n\t * @return Javascript引擎\n\t * @since 5.2.5\n\t */\n\tpublic static ScriptEngine getJsEngine() {\n\t\treturn getScript(\"js\");\n\t}\n\n\t/**\n\t * 创建新的JavaScript引擎\n\t *\n\t * @return Javascript引擎\n\t * @since 5.2.6\n\t */\n\tpublic static ScriptEngine createJsEngine() {\n\t\treturn createScript(\"js\");\n\t}\n\n\t/**\n\t * 获得单例的Python引擎<br>\n\t * 需要引入org.python:jython\n\t *\n\t * @return Python引擎\n\t * @since 5.2.5\n\t */\n\tpublic static ScriptEngine getPythonEngine() {\n\t\tSystem.setProperty(\"python.import.site\", \"false\");\n\t\treturn getScript(\"python\");\n\t}\n\n\t/**\n\t * 创建Python引擎<br>\n\t * 需要引入org.python:jython\n\t *\n\t * @return Python引擎\n\t * @since 5.2.6\n\t */\n\tpublic static ScriptEngine createPythonEngine() {\n\t\tSystem.setProperty(\"python.import.site\", \"false\");\n\t\treturn createScript(\"python\");\n\t}\n\n\t/**\n\t * 获得单例的Lua引擎<br>\n\t * 需要引入org.luaj:luaj-jse\n\t *\n\t * @return Lua引擎\n\t * @since 5.2.5\n\t */\n\tpublic static ScriptEngine getLuaEngine() {\n\t\treturn getScript(\"lua\");\n\t}\n\n\t/**\n\t * 创建Lua引擎<br>\n\t * 需要引入org.luaj:luaj-jse\n\t *\n\t * @return Lua引擎\n\t * @since 5.2.6\n\t */\n\tpublic static ScriptEngine createLuaEngine() {\n\t\treturn createScript(\"lua\");\n\t}\n\n\t/**\n\t * 获得单例的Groovy引擎<br>\n\t * 需要引入org.codehaus.groovy:groovy-all\n\t *\n\t * @return Groovy引擎\n\t * @since 5.2.5\n\t */\n\tpublic static ScriptEngine getGroovyEngine() {\n\t\treturn getScript(\"groovy\");\n\t}\n\n\t/**\n\t * 创建Groovy引擎<br>\n\t * 需要引入org.codehaus.groovy:groovy-all\n\t *\n\t * @return Groovy引擎\n\t * @since 5.2.6\n\t */\n\tpublic static ScriptEngine createGroovyEngine() {\n\t\treturn createScript(\"groovy\");\n\t}\n\n\t/**\n\t * 执行Javascript脚本，返回Invocable，此方法分为两种情况：\n\t *\n\t * <ol>\n\t *     <li>执行的脚本返回值是可执行的脚本方法</li>\n\t *     <li>脚本为函数库，则ScriptEngine本身为可执行方法</li>\n\t * </ol>\n\t *\n\t * @param script 脚本内容\n\t * @return 执行结果\n\t * @throws ScriptRuntimeException 脚本异常\n\t * @since 5.3.6\n\t */\n\tpublic static Invocable evalInvocable(String script) throws ScriptRuntimeException {\n\t\tfinal ScriptEngine jsEngine = getJsEngine();\n\t\tfinal Object eval;\n\t\ttry {\n\t\t\teval = jsEngine.eval(script);\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t\tif(eval instanceof Invocable){\n\t\t\treturn (Invocable)eval;\n\t\t} else if(jsEngine instanceof Invocable){\n\t\t\treturn (Invocable)jsEngine;\n\t\t}\n\t\tthrow new ScriptRuntimeException(\"Script is not invocable !\");\n\t}\n\n\t/**\n\t * 执行有返回值的Javascript脚本\n\t *\n\t * @param script 脚本内容\n\t * @return 执行结果\n\t * @throws ScriptRuntimeException 脚本异常\n\t * @since 3.2.0\n\t */\n\tpublic static Object eval(String script) throws ScriptRuntimeException {\n\t\ttry {\n\t\t\treturn getJsEngine().eval(script);\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行有返回值的脚本\n\t *\n\t * @param script  脚本内容\n\t * @param context 脚本上下文\n\t * @return 执行结果\n\t * @throws ScriptRuntimeException 脚本异常\n\t * @since 3.2.0\n\t */\n\tpublic static Object eval(String script, ScriptContext context) throws ScriptRuntimeException {\n\t\ttry {\n\t\t\treturn getJsEngine().eval(script, context);\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行有返回值的脚本\n\t *\n\t * @param script   脚本内容\n\t * @param bindings 绑定的参数\n\t * @return 执行结果\n\t * @throws ScriptRuntimeException 脚本异常\n\t * @since 3.2.0\n\t */\n\tpublic static Object eval(String script, Bindings bindings) throws ScriptRuntimeException {\n\t\ttry {\n\t\t\treturn getJsEngine().eval(script, bindings);\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 执行JS脚本中的指定方法\n\t *\n\t * @param script js脚本\n\t * @param func 方法名\n\t * @param args 方法参数\n\t * @return 结果\n\t * @since 5.3.6\n\t */\n\tpublic static Object invoke(String script, String func, Object... args) {\n\t\tfinal Invocable eval = evalInvocable(script);\n\t\ttry {\n\t\t\treturn eval.invokeFunction(func, args);\n\t\t} catch (ScriptException | NoSuchMethodException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 编译Javascript脚本\n\t *\n\t * @param script 脚本内容\n\t * @return {@link CompiledScript}\n\t * @throws ScriptRuntimeException 脚本异常\n\t * @since 3.2.0\n\t */\n\tpublic static CompiledScript compile(String script) throws ScriptRuntimeException {\n\t\ttry {\n\t\t\treturn compile(getJsEngine(), script);\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 编译Javascript脚本\n\t *\n\t * @param engine 引擎\n\t * @param script 脚本内容\n\t * @return {@link CompiledScript}\n\t * @throws ScriptException 脚本异常\n\t */\n\tpublic static CompiledScript compile(ScriptEngine engine, String script) throws ScriptException {\n\t\tif (engine instanceof Compilable) {\n\t\t\tfinal Compilable compEngine = (Compilable) engine;\n\t\t\treturn compEngine.compile(script);\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-script/src/main/java/cn/hutool/script/package-info.java",
    "content": "/**\n * Script模块主要针对Java的javax.script封装，可以运行Javascript脚本。\n * \n * @author looly\n *\n */\npackage cn.hutool.script;"
  },
  {
    "path": "hutool-script/src/test/java/cn/hutool/script/test/NashornDeepTest.java",
    "content": "package cn.hutool.script.test;\n\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport javax.script.Invocable;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\nimport javax.script.ScriptException;\n\npublic class NashornDeepTest {\n\n\tpublic static void main(String[] args) throws ScriptException, NoSuchMethodException {\n\t\tScriptEngine engine = new ScriptEngineManager().getEngineByName(\"js\");\n\n\t\tengine.eval(ResourceUtil.readUtf8Str(\"filter1.js\"));\n\n\t\tfinal Object filter1 = ((Invocable) engine).invokeFunction(\"filter1\", 1, 2);\n\t\tassertFalse((Boolean) filter1);\n\t}\n}\n"
  },
  {
    "path": "hutool-script/src/test/java/cn/hutool/script/test/ScriptUtilTest.java",
    "content": "package cn.hutool.script.test;\n\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.script.ScriptRuntimeException;\nimport cn.hutool.script.ScriptUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.script.CompiledScript;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptException;\n\n/**\n * 脚本单元测试类\n *\n * @author looly\n *\n */\npublic class ScriptUtilTest {\n\n\t@Test\n\tpublic void compileTest() {\n\t\tCompiledScript script = ScriptUtil.compile(\"print('Script test!');\");\n\t\ttry {\n\t\t\tscript.eval();\n\t\t} catch (ScriptException e) {\n\t\t\tthrow new ScriptRuntimeException(e);\n\t\t}\n\t}\n\n\t@Test\n\tpublic void evalTest() {\n\t\tScriptUtil.eval(\"print('Script test!');\");\n\t}\n\n\t@Test\n\tpublic void invokeTest() {\n\t\tfinal Object result = ScriptUtil.invoke(ResourceUtil.readUtf8Str(\"filter1.js\"), \"filter1\", 2, 1);\n\t\tassertTrue((Boolean) result);\n\t}\n\n\t@Test\n\tpublic void pythonTest() throws ScriptException {\n\t\tfinal ScriptEngine pythonEngine = ScriptUtil.getPythonEngine();\n\t\tpythonEngine.eval(\"print('Hello Python')\");\n\t}\n\n\t@Test\n\tpublic void luaTest() throws ScriptException {\n\t\tfinal ScriptEngine engine = ScriptUtil.getLuaEngine();\n\t\tengine.eval(\"print('Hello Lua')\");\n\t}\n\n\t@Test\n\tpublic void groovyTest() throws ScriptException {\n\t\tfinal ScriptEngine engine = ScriptUtil.getGroovyEngine();\n\t\tengine.eval(\"println 'Hello Groovy'\");\n\t}\n}\n"
  },
  {
    "path": "hutool-script/src/test/resources/filter1.js",
    "content": "function filter1(a, b) {\n    if (a > b) {\n        return a > b;\n    }\n    return false;\n}"
  },
  {
    "path": "hutool-setting/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-setting</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 配置文件增强</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.setting</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.yaml</groupId>\n\t\t\t<artifactId>snakeyaml</artifactId>\n\t\t\t<version>2.2</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/AbsSetting.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.bean.copier.CopyOptions;\nimport cn.hutool.core.bean.copier.ValueProvider;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.getter.OptNullBasicTypeFromStringGetter;\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Type;\n\n/**\n * Setting抽象类\n *\n * @author Looly\n */\npublic abstract class AbsSetting implements OptNullBasicTypeFromStringGetter<String>, Serializable {\n\tprivate static final long serialVersionUID = 6200156302595905863L;\n\tprivate final static Log log = LogFactory.get();\n\n\t/**\n\t * 数组类型值默认分隔符\n\t */\n\tpublic final static String DEFAULT_DELIMITER = \",\";\n\t/**\n\t * 默认分组\n\t */\n\tpublic final static String DEFAULT_GROUP = StrUtil.EMPTY;\n\n\t@Override\n\tpublic String getStr(String key, String defaultValue) {\n\t\treturn getStr(key, DEFAULT_GROUP, defaultValue);\n\t}\n\n\t/**\n\t * 获得字符串类型值\n\t *\n\t * @param key          KEY\n\t * @param group        分组\n\t * @param defaultValue 默认值\n\t * @return 值，如果字符串为{@code null}返回默认值\n\t */\n\tpublic String getStr(String key, String group, String defaultValue) {\n\t\tfinal String value = getByGroup(key, group);\n\t\treturn ObjectUtil.defaultIfNull(value, defaultValue);\n\t}\n\n\t/**\n\t * 获得字符串类型值，如果字符串为{@code null}或者\"\"返回默认值\n\t *\n\t * @param key          KEY\n\t * @param group        分组\n\t * @param defaultValue 默认值\n\t * @return 值，如果字符串为{@code null}或者\"\"返回默认值\n\t * @since 5.2。4\n\t */\n\tpublic String getStrNotEmpty(String key, String group, String defaultValue) {\n\t\tfinal String value = getByGroup(key, group);\n\t\treturn ObjectUtil.defaultIfEmpty(value, defaultValue);\n\t}\n\n\t/**\n\t * 获得指定分组的键对应值\n\t *\n\t * @param key   键\n\t * @param group 分组\n\t * @return 值\n\t */\n\tpublic abstract String getByGroup(String key, String group);\n\n\t// --------------------------------------------------------------- Get\n\n\t/**\n\t * 带有日志提示的get，如果没有定义指定的KEY，则打印debug日志\n\t *\n\t * @param key 键\n\t * @return 值\n\t */\n\tpublic String getWithLog(String key) {\n\t\tfinal String value = getStr(key);\n\t\tif (value == null) {\n\t\t\tlog.debug(\"No key define for [{}]!\", key);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 带有日志提示的get，如果没有定义指定的KEY，则打印debug日志\n\t *\n\t * @param key   键\n\t * @param group 分组\n\t * @return 值\n\t */\n\tpublic String getByGroupWithLog(String key, String group) {\n\t\tfinal String value = getByGroup(key, group);\n\t\tif (value == null) {\n\t\t\tlog.debug(\"No key define for [{}] of group [{}] !\", key, group);\n\t\t}\n\t\treturn value;\n\t}\n\n\t// --------------------------------------------------------------- Get string array\n\n\t/**\n\t * 获得数组型\n\t *\n\t * @param key 属性名\n\t * @return 属性值\n\t */\n\tpublic String[] getStrings(String key) {\n\t\treturn getStrings(key, null);\n\t}\n\n\t/**\n\t * 获得数组型\n\t *\n\t * @param key          属性名\n\t * @param defaultValue 默认的值\n\t * @return 属性值\n\t */\n\tpublic String[] getStringsWithDefault(String key, String[] defaultValue) {\n\t\tString[] value = getStrings(key, null);\n\t\tif (null == value) {\n\t\t\tvalue = defaultValue;\n\t\t}\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * 获得数组型\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic String[] getStrings(String key, String group) {\n\t\treturn getStrings(key, group, DEFAULT_DELIMITER);\n\t}\n\n\t/**\n\t * 获得数组型\n\t *\n\t * @param key       属性名\n\t * @param group     分组名\n\t * @param delimiter 分隔符\n\t * @return 属性值\n\t */\n\tpublic String[] getStrings(String key, String group, String delimiter) {\n\t\tfinal String value = getByGroup(key, group);\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn StrUtil.splitToArray(value, delimiter);\n\t}\n\n\t// --------------------------------------------------------------- Get int\n\n\t/**\n\t * 获取数字型型属性值\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic Integer getInt(String key, String group) {\n\t\treturn getInt(key, group, null);\n\t}\n\n\t/**\n\t * 获取数字型型属性值\n\t *\n\t * @param key          属性名\n\t * @param group        分组名\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t */\n\tpublic Integer getInt(String key, String group, Integer defaultValue) {\n\t\treturn Convert.toInt(getByGroup(key, group), defaultValue);\n\t}\n\n\t// --------------------------------------------------------------- Get bool\n\n\t/**\n\t * 获取布尔型属性值\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic Boolean getBool(String key, String group) {\n\t\treturn getBool(key, group, null);\n\t}\n\n\t/**\n\t * 获取布尔型属性值\n\t *\n\t * @param key          属性名\n\t * @param group        分组名\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t */\n\tpublic Boolean getBool(String key, String group, Boolean defaultValue) {\n\t\treturn Convert.toBool(getByGroup(key, group), defaultValue);\n\t}\n\n\t// --------------------------------------------------------------- Get long\n\n\t/**\n\t * 获取long类型属性值\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic Long getLong(String key, String group) {\n\t\treturn getLong(key, group, null);\n\t}\n\n\t/**\n\t * 获取long类型属性值\n\t *\n\t * @param key          属性名\n\t * @param group        分组名\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t */\n\tpublic Long getLong(String key, String group, Long defaultValue) {\n\t\treturn Convert.toLong(getByGroup(key, group), defaultValue);\n\t}\n\n\t// --------------------------------------------------------------- Get char\n\n\t/**\n\t * 获取char类型属性值\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic Character getChar(String key, String group) {\n\t\tfinal String value = getByGroup(key, group);\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn value.charAt(0);\n\t}\n\n\t// --------------------------------------------------------------- Get double\n\n\t/**\n\t * 获取double类型属性值\n\t *\n\t * @param key   属性名\n\t * @param group 分组名\n\t * @return 属性值\n\t */\n\tpublic Double getDouble(String key, String group) {\n\t\treturn getDouble(key, group, null);\n\t}\n\n\t/**\n\t * 获取double类型属性值\n\t *\n\t * @param key          属性名\n\t * @param group        分组名\n\t * @param defaultValue 默认值\n\t * @return 属性值\n\t */\n\tpublic Double getDouble(String key, String group, Double defaultValue) {\n\t\treturn Convert.toDouble(getByGroup(key, group), defaultValue);\n\t}\n\n\t/**\n\t * 将setting中的键值关系映射到对象中，原理是调用对象对应的set方法<br>\n\t * 只支持基本类型的转换\n\t *\n\t * @param <T>   Bean类型\n\t * @param group 分组\n\t * @param bean  Bean对象\n\t * @return Bean\n\t */\n\tpublic <T> T toBean(final String group, T bean) {\n\t\treturn BeanUtil.fillBean(bean, new ValueProvider<String>() {\n\n\t\t\t@Override\n\t\t\tpublic Object value(String key, Type valueType) {\n\t\t\t\treturn getByGroup(key, group);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean containsKey(String key) {\n\t\t\t\treturn null != getByGroup(key, group);\n\t\t\t}\n\t\t}, CopyOptions.create());\n\t}\n\n\t/**\n\t * 将setting中的键值关系映射到对象中，原理是调用对象对应的set方法<br>\n\t * 只支持基本类型的转换\n\t *\n\t * @param <T>       Bean类型\n\t * @param group     分组\n\t * @param beanClass Bean类型\n\t * @return Bean\n\t * @since 5.0.6\n\t */\n\tpublic <T> T toBean(String group, Class<T> beanClass) {\n\t\treturn toBean(group, ReflectUtil.newInstanceIfPossible(beanClass));\n\t}\n\n\t/**\n\t * 将setting中的键值关系映射到对象中，原理是调用对象对应的set方法<br>\n\t * 只支持基本类型的转换\n\t *\n\t * @param <T>  bean类型\n\t * @param bean Bean\n\t * @return Bean\n\t */\n\tpublic <T> T toBean(T bean) {\n\t\treturn toBean(null, bean);\n\t}\n\n\t/**\n\t * 将setting中的键值关系映射到对象中，原理是调用对象对应的set方法<br>\n\t * 只支持基本类型的转换\n\t *\n\t * @param <T>       bean类型\n\t * @param beanClass Bean类型\n\t * @return Bean\n\t * @since 5.0.6\n\t */\n\tpublic <T> T toBean(Class<T> beanClass) {\n\t\treturn toBean(null, beanClass);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/GroupedMap.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;\n\n/**\n * 基于分组的Map<br>\n * 此对象方法线程安全\n * \n * @author looly\n * @since 4.0.11\n */\npublic class GroupedMap extends LinkedHashMap<String, LinkedHashMap<String, String>> {\n\tprivate static final long serialVersionUID = -7777365130776081931L;\n\n\tprivate final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();\n\tprivate final ReadLock readLock = cacheLock.readLock();\n\tprivate final WriteLock writeLock = cacheLock.writeLock();\n\tprivate int size = -1;\n\n\t/**\n\t * 获取分组对应的值，如果分组不存在或者值不存在则返回null\n\t * \n\t * @param group 分组\n\t * @param key 键\n\t * @return 值，如果分组不存在或者值不存在则返回null\n\t */\n\tpublic String get(String group, String key) {\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tLinkedHashMap<String, String> map = this.get(StrUtil.nullToEmpty(group));\n\t\t\tif (MapUtil.isNotEmpty(map)) {\n\t\t\t\treturn map.get(key);\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic LinkedHashMap<String, String> get(Object key) {\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn super.get(key);\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 总的键值对数\n\t * \n\t * @return 总键值对数\n\t */\n\t@Override\n\tpublic int size() {\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tif (this.size < 0) {\n\t\t\t\tthis.size = 0;\n\t\t\t\tfor (LinkedHashMap<String, String> value : this.values()) {\n\t\t\t\t\tthis.size += value.size();\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn this.size;\n\t}\n\n\t/**\n\t * 将键值对加入到对应分组中\n\t * \n\t * @param group 分组\n\t * @param key 键\n\t * @param value 值\n\t * @return 此key之前存在的值，如果没有返回null\n\t */\n\tpublic String put(String group, String key, String value) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.computeIfAbsent(group, k -> new LinkedHashMap<>());\n\t\t\tthis.size = -1;\n\t\t\treturn valueMap.put(key, value);\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 加入多个键值对到某个分组下\n\t * \n\t * @param group 分组\n\t * @param m 键值对\n\t * @return this\n\t */\n\tpublic GroupedMap putAll(String group, Map<? extends String, ? extends String> m) {\n\t\tfor (Entry<? extends String, ? extends String> entry : m.entrySet()) {\n\t\t\tthis.put(group, entry.getKey(), entry.getValue());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 从指定分组中删除指定值\n\t * \n\t * @param group 分组\n\t * @param key 键\n\t * @return 被删除的值，如果值不存在，返回null\n\t */\n\tpublic String remove(String group, String key) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.remove(key);\n\t\t\t}\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 某个分组对应的键值对是否为空\n\t * \n\t * @param group 分组\n\t * @return 是否为空\n\t */\n\tpublic boolean isEmpty(String group) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.isEmpty();\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 是否为空，如果多个分组同时为空，也按照空处理\n\t * \n\t * @return 是否为空，如果多个分组同时为空，也按照空处理\n\t */\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn this.size() == 0;\n\t}\n\n\t/**\n\t * 指定分组中是否包含指定key\n\t * \n\t * @param group 分组\n\t * @param key 键\n\t * @return 是否包含key\n\t */\n\tpublic boolean containsKey(String group, String key) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.containsKey(key);\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 指定分组中是否包含指定值\n\t * \n\t * @param group 分组\n\t * @param value 值\n\t * @return 是否包含值\n\t */\n\tpublic boolean containsValue(String group, String value) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.containsValue(value);\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 清除指定分组下的所有键值对\n\t * \n\t * @param group 分组\n\t * @return this\n\t */\n\tpublic GroupedMap clear(String group) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\twriteLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\tvalueMap.clear();\n\t\t\t}\n\t\t} finally {\n\t\t\twriteLock.unlock();\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Set<String> keySet() {\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn super.keySet();\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 指定分组所有键的Set\n\t * \n\t * @param group 分组\n\t * @return 键Set\n\t */\n\tpublic Set<String> keySet(String group) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.keySet();\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn Collections.emptySet();\n\t}\n\n\t/**\n\t * 指定分组下所有值\n\t * \n\t * @param group 分组\n\t * @return 值\n\t */\n\tpublic Collection<String> values(String group) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.values();\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn Collections.emptyList();\n\t}\n\n\t@Override\n\tpublic Set<java.util.Map.Entry<String, LinkedHashMap<String, String>>> entrySet() {\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn super.entrySet();\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n\n\t/**\n\t * 指定分组下所有键值对\n\t * \n\t * @param group 分组\n\t * @return 键值对\n\t */\n\tpublic Set<Entry<String, String>> entrySet(String group) {\n\t\tgroup = StrUtil.nullToEmpty(group).trim();\n\t\treadLock.lock();\n\t\ttry {\n\t\t\tfinal LinkedHashMap<String, String> valueMap = this.get(group);\n\t\t\tif (MapUtil.isNotEmpty(valueMap)) {\n\t\t\t\treturn valueMap.entrySet();\n\t\t\t}\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t\treturn Collections.emptySet();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treadLock.lock();\n\t\ttry {\n\t\t\treturn super.toString();\n\t\t} finally {\n\t\t\treadLock.unlock();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/GroupedSet.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.collection.CollectionUtil;\nimport cn.hutool.core.collection.ListUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.URLUtil;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * 分组化的Set集合类<br>\n * 在配置文件中可以用中括号分隔不同的分组，每个分组会放在独立的Set中，用group区别<br>\n * 无分组的集合和`[]`分组集合会合并成员，重名的分组也会合并成员<br>\n * 分组配置文件如下：\n * \n * <pre>\n * [group1]\n * aaa\n * bbb\n * ccc\n * \n * [group2]\n * aaa\n * ccc\n * ddd\n * </pre>\n * \n * @author Looly\n * @since 3.1.0\n */\npublic class GroupedSet extends HashMap<String, LinkedHashSet<String>> {\n\tprivate static final long serialVersionUID = -8430706353275835496L;\n\t// private final static Log log = StaticLog.get();\n\n\t/** 注释符号（当有此符号在行首，表示此行为注释） */\n\tprivate final static String COMMENT_FLAG_PRE = \"#\";\n\t/** 分组行识别的环绕标记 */\n\tprivate final static char[] GROUP_SURROUND = { '[', ']' };\n\n\t/** 本设置对象的字符集 */\n\tprivate Charset charset;\n\t/** 设定文件的URL */\n\tprivate URL groupedSetUrl;\n\n\t/**\n\t * 基本构造<br>\n\t * 需自定义初始化配置文件\n\t * \n\t * @param charset 字符集\n\t */\n\tpublic GroupedSet(Charset charset) {\n\t\tthis.charset = charset;\n\t}\n\n\t/**\n\t * 构造，使用相对于Class文件根目录的相对路径\n\t * \n\t * @param pathBaseClassLoader 相对路径（相对于当前项目的classes路径）\n\t * @param charset 字符集\n\t */\n\tpublic GroupedSet(String pathBaseClassLoader, Charset charset) {\n\t\tif (null == pathBaseClassLoader) {\n\t\t\tpathBaseClassLoader = StrUtil.EMPTY;\n\t\t}\n\n\t\tfinal URL url = URLUtil.getURL(pathBaseClassLoader);\n\t\tif (url == null) {\n\t\t\tthrow new RuntimeException(StrUtil.format(\"Can not find GroupSet file: [{}]\", pathBaseClassLoader));\n\t\t}\n\t\tthis.init(url, charset);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param configFile 配置文件对象\n\t * @param charset 字符集\n\t */\n\tpublic GroupedSet(File configFile, Charset charset) {\n\t\tif (configFile == null) {\n\t\t\tthrow new RuntimeException(\"Null GroupSet file!\");\n\t\t}\n\t\tfinal URL url = URLUtil.getURL(configFile);\n\t\tthis.init(url, charset);\n\t}\n\n\t/**\n\t * 构造，相对于classes读取文件\n\t * \n\t * @param path 相对路径\n\t * @param clazz 基准类\n\t * @param charset 字符集\n\t */\n\tpublic GroupedSet(String path, Class<?> clazz, Charset charset) {\n\t\tfinal URL url = URLUtil.getURL(path, clazz);\n\t\tif (url == null) {\n\t\t\tthrow new RuntimeException(StrUtil.format(\"Can not find GroupSet file: [{}]\", path));\n\t\t}\n\t\tthis.init(url, charset);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param url 设定文件的URL\n\t * @param charset 字符集\n\t */\n\tpublic GroupedSet(URL url, Charset charset) {\n\t\tif (url == null) {\n\t\t\tthrow new RuntimeException(\"Null url define!\");\n\t\t}\n\t\tthis.init(url, charset);\n\t}\n\n\t/**\n\t * 构造\n\t * \n\t * @param pathBaseClassLoader 相对路径（相对于当前项目的classes路径）\n\t */\n\tpublic GroupedSet(String pathBaseClassLoader) {\n\t\tthis(pathBaseClassLoader, CharsetUtil.CHARSET_UTF_8);\n\t}\n\n\t/*--------------------------公有方法 start-------------------------------*/\n\t/**\n\t * 初始化设定文件\n\t * \n\t * @param groupedSetUrl 设定文件的URL\n\t * @param charset 字符集\n\t * @return 成功初始化与否\n\t */\n\tpublic boolean init(URL groupedSetUrl, Charset charset) {\n\t\tif (groupedSetUrl == null) {\n\t\t\tthrow new RuntimeException(\"Null GroupSet url or charset define!\");\n\t\t}\n\t\tthis.charset = charset;\n\t\tthis.groupedSetUrl = groupedSetUrl;\n\n\t\treturn this.load(groupedSetUrl);\n\t}\n\n\t/**\n\t * 加载设置文件\n\t * \n\t * @param groupedSetUrl 配置文件URL\n\t * @return 加载是否成功\n\t */\n\tsynchronized public boolean load(URL groupedSetUrl) {\n\t\tif (groupedSetUrl == null) {\n\t\t\tthrow new RuntimeException(\"Null GroupSet url define!\");\n\t\t}\n\t\t// log.debug(\"Load GroupSet file [{}]\", groupedSetUrl.getPath());\n\t\tInputStream settingStream = null;\n\t\ttry {\n\t\t\tsettingStream = groupedSetUrl.openStream();\n\t\t\tload(settingStream);\n\t\t} catch (IOException e) {\n\t\t\t// log.error(e, \"Load GroupSet error!\");\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tIoUtil.close(settingStream);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 重新加载配置文件\n\t */\n\tpublic void reload() {\n\t\tthis.load(groupedSetUrl);\n\t}\n\n\t/**\n\t * 加载设置文件。 此方法不会关闭流对象\n\t * \n\t * @param settingStream 文件流\n\t * @return 加载成功与否\n\t * @throws IOException IO异常\n\t */\n\tpublic boolean load(InputStream settingStream) throws IOException {\n\t\tsuper.clear();\n\t\tBufferedReader reader = null;\n\t\ttry {\n\t\t\treader = IoUtil.getReader(settingStream, charset);\n\t\t\t// 分组\n\t\t\tString group;\n\t\t\tLinkedHashSet<String> valueSet = null;\n\n\t\t\twhile (true) {\n\t\t\t\tString line = reader.readLine();\n\t\t\t\tif (line == null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tline = line.trim();\n\t\t\t\t// 跳过注释行和空行\n\t\t\t\tif (StrUtil.isBlank(line) || line.startsWith(COMMENT_FLAG_PRE)) {\n\t\t\t\t\t// 空行和注释忽略\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (line.startsWith(StrUtil.BACKSLASH + COMMENT_FLAG_PRE)) {\n\t\t\t\t\t// 对于值中出现开头为#的字符串，需要转义处理，在此做反转义\n\t\t\t\t\tline = line.substring(1);\n\t\t\t\t}\n\n\t\t\t\t// 记录分组名\n\t\t\t\tif (line.charAt(0) == GROUP_SURROUND[0] && line.charAt(line.length() - 1) == GROUP_SURROUND[1]) {\n\t\t\t\t\t// 开始新的分组取值，当出现重名分组时候，合并分组值\n\t\t\t\t\tgroup = line.substring(1, line.length() - 1).trim();\n\t\t\t\t\tvalueSet = super.get(group);\n\t\t\t\t\tif (null == valueSet) {\n\t\t\t\t\t\tvalueSet = new LinkedHashSet<>();\n\t\t\t\t\t}\n\t\t\t\t\tsuper.put(group, valueSet);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 添加值\n\t\t\t\tif (null == valueSet) {\n\t\t\t\t\t// 当出现无分组值的时候，会导致valueSet为空，此时group为\"\"\n\t\t\t\t\tvalueSet = new LinkedHashSet<>();\n\t\t\t\t\tsuper.put(StrUtil.EMPTY, valueSet);\n\t\t\t\t}\n\t\t\t\tvalueSet.add(line);\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * @return 获得设定文件的路径\n\t */\n\tpublic String getPath() {\n\t\treturn groupedSetUrl.getPath();\n\t}\n\n\t/**\n\t * @return 获得所有分组名\n\t */\n\tpublic Set<String> getGroups() {\n\t\treturn super.keySet();\n\t}\n\n\t/**\n\t * 获得对应分组的所有值\n\t * \n\t * @param group 分组名\n\t * @return 分组的值集合\n\t */\n\tpublic LinkedHashSet<String> getValues(String group) {\n\t\tif (group == null) {\n\t\t\tgroup = StrUtil.EMPTY;\n\t\t}\n\t\treturn super.get(group);\n\t}\n\n\t/**\n\t * 是否在给定分组的集合中包含指定值<br>\n\t * 如果给定分组对应集合不存在，则返回false\n\t * \n\t * @param group 分组名\n\t * @param value 测试的值\n\t * @param otherValues 其他值\n\t * @return 是否包含\n\t */\n\tpublic boolean contains(String group, String value, String... otherValues) {\n\t\tif (ArrayUtil.isNotEmpty(otherValues)) {\n\t\t\t// 需要测试多个值的情况\t\t\n\t\t\tfinal List<String> valueList = ListUtil.toList(otherValues);\n\t\t\tvalueList.add(value);\n\t\t\treturn contains(group, valueList);\n\t\t} else {\n\t\t\t// 测试单个值\n\t\t\tfinal LinkedHashSet<String> valueSet = getValues(group);\n\t\t\tif (CollectionUtil.isEmpty(valueSet)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn valueSet.contains(value);\n\t\t}\n\t}\n\n\t/**\n\t * 是否在给定分组的集合中全部包含指定值集合<br>\n\t * 如果给定分组对应集合不存在，则返回false\n\t * \n\t * @param group 分组名\n\t * @param values 测试的值集合\n\t * @return 是否包含\n\t */\n\tpublic boolean contains(String group, Collection<String> values) {\n\t\tfinal LinkedHashSet<String> valueSet = getValues(group);\n\t\tif (CollectionUtil.isEmpty(values) || CollectionUtil.isEmpty(valueSet)) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn valueSet.containsAll(values);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/Setting.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.collection.CollUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.*;\nimport cn.hutool.core.io.watch.SimpleWatcher;\nimport cn.hutool.core.io.watch.WatchMonitor;\nimport cn.hutool.core.io.watch.WatchUtil;\nimport cn.hutool.core.io.watch.watchers.DelayWatcher;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.CharUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.StaticLog;\nimport cn.hutool.setting.dialect.Props;\n\nimport java.io.File;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\nimport java.util.*;\nimport java.util.function.Consumer;\n\n/**\n * 设置工具类。 用于支持设置（配置）文件<br>\n * BasicSetting用于替换Properties类，提供功能更加强大的配置文件，同时对Properties文件向下兼容\n *\n * <pre>\n *  1、支持变量，默认变量命名为 ${变量名}，变量只能识别读入行的变量，例如第6行的变量在第三行无法读取\n *  2、支持分组，分组为中括号括起来的内容，中括号以下的行都为此分组的内容，无分组相当于空字符分组，若某个key是name，加上分组后的键相当于group.name\n *  3、注释以#开头，但是空行和不带“=”的行也会被跳过，但是建议加#\n *  4、store方法不会保存注释内容，慎重使用\n * </pre>\n *\n * @author looly\n */\npublic class Setting extends AbsSetting implements Map<String, String> {\n\tprivate static final long serialVersionUID = 3618305164959883393L;\n\n\t/**\n\t * 默认字符集\n\t */\n\tpublic static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;\n\t/**\n\t * 默认配置文件扩展名\n\t */\n\tpublic static final String EXT_NAME = \"setting\";\n\n\t/**\n\t * 构建一个空的Setting，用于手动加入参数\n\t *\n\t * @return Setting\n\t * @since 5.4.3\n\t */\n\tpublic static Setting create() {\n\t\treturn new Setting();\n\t}\n\n\t/**\n\t * 附带分组的键值对存储\n\t */\n\tprivate final GroupedMap groupedMap = new GroupedMap();\n\n\t/**\n\t * 本设置对象的字符集\n\t */\n\tprotected Charset charset;\n\t/**\n\t * 是否使用变量\n\t */\n\tprotected boolean isUseVariable;\n\t/**\n\t * 设定文件的资源\n\t */\n\tprotected Resource resource;\n\n\tprivate SettingLoader settingLoader;\n\tprivate WatchMonitor watchMonitor;\n\n\t// ------------------------------------------------------------------------------------- Constructor start\n\n\t/**\n\t * 空构造\n\t */\n\tpublic Setting() {\n\t\tthis.charset = DEFAULT_CHARSET;\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path 相对路径或绝对路径\n\t */\n\tpublic Setting(String path) {\n\t\tthis(path, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param path          相对路径或绝对路径\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic Setting(String path, boolean isUseVariable) {\n\t\tthis(path, DEFAULT_CHARSET, isUseVariable);\n\t}\n\n\t/**\n\t * 构造，使用相对于Class文件根目录的相对路径\n\t *\n\t * @param path          相对路径或绝对路径\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic Setting(String path, Charset charset, boolean isUseVariable) {\n\t\tAssert.notBlank(path, \"Blank setting path !\");\n\t\tthis.init(ResourceUtil.getResourceObj(path), charset, isUseVariable);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param configFile    配置文件对象\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic Setting(File configFile, Charset charset, boolean isUseVariable) {\n\t\tAssert.notNull(configFile, \"Null setting file define!\");\n\t\tthis.init(new FileResource(configFile), charset, isUseVariable);\n\t}\n\n\t/**\n\t * 构造，相对于classes读取文件\n\t *\n\t * @param path          相对ClassPath路径或绝对路径\n\t * @param clazz         基准类\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic Setting(String path, Class<?> clazz, Charset charset, boolean isUseVariable) {\n\t\tAssert.notBlank(path, \"Blank setting path !\");\n\t\tthis.init(new ClassPathResource(path, clazz), charset, isUseVariable);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param url           设定文件的URL\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic Setting(URL url, Charset charset, boolean isUseVariable) {\n\t\tAssert.notNull(url, \"Null setting url define!\");\n\t\tthis.init(new UrlResource(url), charset, isUseVariable);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param resource      Setting的Resource\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t * @since 5.4.4\n\t */\n\tpublic Setting(Resource resource, Charset charset, boolean isUseVariable) {\n\t\tthis.init(resource, charset, isUseVariable);\n\t}\n\t// ------------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 初始化设定文件\n\t *\n\t * @param resource      {@link Resource}\n\t * @param charset       字符集\n\t * @param isUseVariable 是否使用变量\n\t * @return 成功初始化与否\n\t */\n\tpublic boolean init(Resource resource, Charset charset, boolean isUseVariable) {\n\t\tAssert.notNull(resource, \"Setting resource must be not null!\");\n\t\tthis.resource = resource;\n\t\tthis.charset = charset;\n\t\tthis.isUseVariable = isUseVariable;\n\n\t\treturn load();\n\t}\n\n\t/**\n\t * 重新加载配置文件\n\t *\n\t * @return 是否加载成功\n\t */\n\tsynchronized public boolean load() {\n\t\tif (null == this.settingLoader) {\n\t\t\tsettingLoader = new SettingLoader(this.groupedMap, this.charset, this.isUseVariable);\n\t\t}\n\t\treturn settingLoader.load(this.resource);\n\t}\n\n\t/**\n\t * 在配置文件变更时自动加载\n\t *\n\t * @param autoReload 是否自动加载\n\t */\n\tpublic void autoLoad(boolean autoReload) {\n\t\tautoLoad(autoReload, null);\n\t}\n\n\t/**\n\t * 在配置文件变更时自动加载\n\t *\n\t * @param callback   加载完成回调\n\t * @param autoReload 是否自动加载\n\t */\n\tpublic void autoLoad(boolean autoReload, Consumer<Boolean> callback) {\n\t\tif (autoReload) {\n\t\t\tAssert.notNull(this.resource, \"Setting resource must be not null !\");\n\t\t\tif (null != this.watchMonitor) {\n\t\t\t\t// 先关闭之前的监听\n\t\t\t\tthis.watchMonitor.close();\n\t\t\t}\n\t\t\tthis.watchMonitor = WatchUtil.createModify(resource.getUrl(), new DelayWatcher(new SimpleWatcher() {\n\t\t\t\t@Override\n\t\t\t\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t\t\t\t\tboolean success = load();\n\t\t\t\t\t// 如果有回调，加载完毕则执行回调\n\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\tcallback.accept(success);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, 600));\n\n\t\t\tthis.watchMonitor.start();\n\t\t\tStaticLog.debug(\"Auto load for [{}] listenning...\", this.resource.getUrl());\n\t\t} else {\n\t\t\tIoUtil.close(this.watchMonitor);\n\t\t\tthis.watchMonitor = null;\n\t\t}\n\t}\n\n\t/**\n\t * 获得设定文件的URL\n\t *\n\t * @return 获得设定文件的路径\n\t * @since 5.4.3\n\t */\n\tpublic URL getSettingUrl() {\n\t\treturn (null == this.resource) ? null : this.resource.getUrl();\n\t}\n\n\t/**\n\t * 获得设定文件的路径\n\t *\n\t * @return 获得设定文件的路径\n\t */\n\tpublic String getSettingPath() {\n\t\tfinal URL settingUrl = getSettingUrl();\n\t\treturn (null == settingUrl) ? null : settingUrl.getPath();\n\t}\n\n\t/**\n\t * 键值总数\n\t *\n\t * @return 键值总数\n\t */\n\t@Override\n\tpublic int size() {\n\t\treturn this.groupedMap.size();\n\t}\n\n\t@Override\n\tpublic String getByGroup(String key, String group) {\n\t\treturn this.groupedMap.get(group, key);\n\t}\n\n\t/**\n\t * 获取并删除键值对，当指定键对应值非空时，返回并删除这个值，后边的键对应的值不再查找\n\t *\n\t * @param keys 键列表，常用于别名\n\t * @return 值\n\t * @since 3.1.2\n\t */\n\tpublic Object getAndRemove(String... keys) {\n\t\tObject value = null;\n\t\tfor (String key : keys) {\n\t\t\tvalue = remove(key);\n\t\t\tif (null != value) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 获取并删除键值对，当指定键对应值非空时，返回并删除这个值，后边的键对应的值不再查找\n\t *\n\t * @param keys 键列表，常用于别名\n\t * @return 字符串值\n\t * @since 3.1.2\n\t */\n\tpublic String getAndRemoveStr(String... keys) {\n\t\tString value = null;\n\t\tfor (String key : keys) {\n\t\t\tvalue = remove(key);\n\t\t\tif (null != value) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 获得指定分组的所有键值对，此方法获取的是原始键值对，获取的键值对可以被修改\n\t *\n\t * @param group 分组\n\t * @return map\n\t */\n\tpublic Map<String, String> getMap(String group) {\n\t\tfinal LinkedHashMap<String, String> map = this.groupedMap.get(group);\n\t\treturn (null != map) ? map : new LinkedHashMap<>(0);\n\t}\n\n\t/**\n\t * 获取group分组下所有配置键值对，组成新的Setting\n\t *\n\t * @param group 分组\n\t * @return Setting\n\t */\n\tpublic Setting getSetting(String group) {\n\t\tfinal Setting setting = new Setting();\n\t\tsetting.putAll(this.getMap(group));\n\t\treturn setting;\n\t}\n\n\t/**\n\t * 获取group分组下所有配置键值对，组成新的{@link Properties}\n\t *\n\t * @param group 分组\n\t * @return Properties对象\n\t */\n\tpublic Properties getProperties(String group) {\n\t\tfinal Properties properties = new Properties();\n\t\tproperties.putAll(getMap(group));\n\t\treturn properties;\n\t}\n\n\t/**\n\t * 获取group分组下所有配置键值对，组成新的{@link Props}\n\t *\n\t * @param group 分组\n\t * @return Props对象\n\t * @since 4.1.21\n\t */\n\tpublic Props getProps(String group) {\n\t\tfinal Props props = new Props();\n\t\tprops.putAll(getMap(group));\n\t\treturn props;\n\t}\n\n\t// --------------------------------------------------------------------------------- Functions\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置<br>\n\t * 持久化不会保留之前的分组，注意如果配置文件在jar内部或者在exe中，此方法会报错。\n\t *\n\t * @since 5.4.3\n\t */\n\tpublic void store() {\n\t\tfinal URL resourceUrl = getSettingUrl();\n\t\tAssert.notNull(resourceUrl, \"Setting path must be not null !\");\n\t\tstore(FileUtil.file(resourceUrl));\n\t}\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置<br>\n\t * 持久化不会保留之前的分组\n\t *\n\t * @param absolutePath 设置文件的绝对路径\n\t */\n\tpublic void store(String absolutePath) {\n\t\tstore(FileUtil.touch(absolutePath));\n\t}\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置<br>\n\t * 持久化不会保留之前的分组\n\t *\n\t * @param file 设置文件\n\t * @since 5.4.3\n\t */\n\tpublic void store(File file) {\n\t\tif (null == this.settingLoader) {\n\t\t\tsettingLoader = new SettingLoader(this.groupedMap, this.charset, this.isUseVariable);\n\t\t}\n\t\tsettingLoader.store(file);\n\t}\n\n\t/**\n\t * 转换为Properties对象，原分组变为前缀\n\t *\n\t * @return Properties对象\n\t */\n\tpublic Properties toProperties() {\n\t\tfinal Properties properties = new Properties();\n\t\tString group;\n\t\tfor (Entry<String, LinkedHashMap<String, String>> groupEntry : this.groupedMap.entrySet()) {\n\t\t\tgroup = groupEntry.getKey();\n\t\t\tfor (Entry<String, String> entry : groupEntry.getValue().entrySet()) {\n\t\t\t\tproperties.setProperty(StrUtil.isEmpty(group) ? entry.getKey() : group + CharUtil.DOT + entry.getKey(), entry.getValue());\n\t\t\t}\n\t\t}\n\t\treturn properties;\n\t}\n\n\t/**\n\t * 获取GroupedMap\n\t *\n\t * @return GroupedMap\n\t * @since 4.0.12\n\t */\n\tpublic GroupedMap getGroupedMap() {\n\t\treturn this.groupedMap;\n\t}\n\n\t/**\n\t * 获取所有分组\n\t *\n\t * @return 获得所有分组名\n\t */\n\tpublic List<String> getGroups() {\n\t\treturn CollUtil.newArrayList(this.groupedMap.keySet());\n\t}\n\n\t/**\n\t * 设置变量的正则<br>\n\t * 正则只能有一个group表示变量本身，剩余为字符 例如 \\$\\{(name)\\}表示${name}变量名为name的一个变量表示\n\t *\n\t * @param regex 正则\n\t * @return this\n\t */\n\tpublic Setting setVarRegex(String regex) {\n\t\tif (null == this.settingLoader) {\n\t\t\tthrow new NullPointerException(\"SettingLoader is null !\");\n\t\t}\n\t\tthis.settingLoader.setVarRegex(regex);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 自定义字符编码\n\t *\n\t * @param charset 字符编码\n\t * @return this\n\t * @since 4.6.2\n\t */\n\tpublic Setting setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------- Map interface with group\n\n\t/**\n\t * 某个分组对应的键值对是否为空\n\t *\n\t * @param group 分组\n\t * @return 是否为空\n\t */\n\tpublic boolean isEmpty(String group) {\n\t\treturn this.groupedMap.isEmpty(group);\n\t}\n\n\t/**\n\t * 指定分组中是否包含指定key\n\t *\n\t * @param group 分组\n\t * @param key   键\n\t * @return 是否包含key\n\t */\n\tpublic boolean containsKey(String group, String key) {\n\t\treturn this.groupedMap.containsKey(group, key);\n\t}\n\n\t/**\n\t * 指定分组中是否包含指定值\n\t *\n\t * @param group 分组\n\t * @param value 值\n\t * @return 是否包含值\n\t */\n\tpublic boolean containsValue(String group, String value) {\n\t\treturn this.groupedMap.containsValue(group, value);\n\t}\n\n\t/**\n\t * 获取分组对应的值，如果分组不存在或者值不存在则返回null\n\t *\n\t * @param group 分组\n\t * @param key   键\n\t * @return 值，如果分组不存在或者值不存在则返回null\n\t */\n\tpublic String get(String group, String key) {\n\t\treturn this.groupedMap.get(group, key);\n\t}\n\n\t/**\n\t * 将键值对加入到对应分组中\n\t *\n\t * @param key   键\n\t * @param group 分组\n\t * @param value 值\n\t * @return 此key之前存在的值，如果没有返回null\n\t */\n\tpublic String putByGroup(String key, String group, String value) {\n\t\treturn this.groupedMap.put(group, key, value);\n\t}\n\n\t/**\n\t * 从指定分组中删除指定值\n\t *\n\t * @param group 分组\n\t * @param key   键\n\t * @return 被删除的值，如果值不存在，返回null\n\t */\n\tpublic String remove(String group, Object key) {\n\t\treturn this.groupedMap.remove(group, Convert.toStr(key));\n\t}\n\n\t/**\n\t * 加入多个键值对到某个分组下\n\t *\n\t * @param group 分组\n\t * @param m     键值对\n\t * @return this\n\t */\n\tpublic Setting putAll(String group, Map<? extends String, ? extends String> m) {\n\t\tthis.groupedMap.putAll(group, m);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加一个Stting到主配置中\n\t *\n\t * @param setting Setting配置\n\t * @return this\n\t * @since 5.2.4\n\t */\n\tpublic Setting addSetting(Setting setting) {\n\t\tfor (Entry<String, LinkedHashMap<String, String>> e : setting.getGroupedMap().entrySet()) {\n\t\t\tthis.putAll(e.getKey(), e.getValue());\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清除指定分组下的所有键值对\n\t *\n\t * @param group 分组\n\t * @return this\n\t */\n\tpublic Setting clear(String group) {\n\t\tthis.groupedMap.clear(group);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定分组所有键的Set\n\t *\n\t * @param group 分组\n\t * @return 键Set\n\t */\n\tpublic Set<String> keySet(String group) {\n\t\treturn this.groupedMap.keySet(group);\n\t}\n\n\t/**\n\t * 指定分组下所有值\n\t *\n\t * @param group 分组\n\t * @return 值\n\t */\n\tpublic Collection<String> values(String group) {\n\t\treturn this.groupedMap.values(group);\n\t}\n\n\t/**\n\t * 指定分组下所有键值对\n\t *\n\t * @param group 分组\n\t * @return 键值对\n\t */\n\tpublic Set<Entry<String, String>> entrySet(String group) {\n\t\treturn this.groupedMap.entrySet(group);\n\t}\n\n\t/**\n\t * 设置值\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @return this\n\t * @since 3.3.1\n\t */\n\tpublic Setting set(String key, String value) {\n\t\tthis.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将键值对加入到对应分组中<br>\n\t * 此方法用于与getXXX统一参数顺序\n\t *\n\t * @param key   键\n\t * @param group 分组\n\t * @param value 值\n\t * @return 此key之前存在的值，如果没有返回null\n\t * @since 5.5.7\n\t */\n\tpublic Setting setByGroup(String key, String group, String value) {\n\t\tthis.putByGroup(key, group, value);\n\t\treturn this;\n\t}\n\n\t// ------------------------------------------------- Override Map interface\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn this.groupedMap.isEmpty();\n\t}\n\n\t/**\n\t * 默认分组（空分组）中是否包含指定key对应的值\n\t *\n\t * @param key 键\n\t * @return 默认分组中是否包含指定key对应的值\n\t */\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn this.groupedMap.containsKey(DEFAULT_GROUP, Convert.toStr(key));\n\t}\n\n\t/**\n\t * 默认分组（空分组）中是否包含指定值\n\t *\n\t * @param value 值\n\t * @return 默认分组中是否包含指定值\n\t */\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\treturn this.groupedMap.containsValue(DEFAULT_GROUP, Convert.toStr(value));\n\t}\n\n\t/**\n\t * 获取默认分组（空分组）中指定key对应的值\n\t *\n\t * @param key 键\n\t * @return 默认分组（空分组）中指定key对应的值\n\t */\n\t@Override\n\tpublic String get(Object key) {\n\t\treturn this.groupedMap.get(DEFAULT_GROUP, Convert.toStr(key));\n\t}\n\n\t/**\n\t * 将指定键值对加入到默认分组（空分组）中\n\t *\n\t * @param key   键\n\t * @param value 值\n\t * @return 加入的值\n\t */\n\t@Override\n\tpublic String put(String key, String value) {\n\t\treturn this.groupedMap.put(DEFAULT_GROUP, key, value);\n\t}\n\n\t/**\n\t * 移除默认分组（空分组）中指定值\n\t *\n\t * @param key 键\n\t * @return 移除的值\n\t */\n\t@Override\n\tpublic String remove(Object key) {\n\t\treturn this.groupedMap.remove(DEFAULT_GROUP, Convert.toStr(key));\n\t}\n\n\t/**\n\t * 将键值对Map加入默认分组（空分组）中\n\t *\n\t * @param m Map\n\t */\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic void putAll(Map<? extends String, ? extends String> m) {\n\t\tthis.groupedMap.putAll(DEFAULT_GROUP, m);\n\t}\n\n\t/**\n\t * 清空默认分组（空分组）中的所有键值对\n\t */\n\t@Override\n\tpublic void clear() {\n\t\tthis.groupedMap.clear(DEFAULT_GROUP);\n\t}\n\n\t/**\n\t * 获取默认分组（空分组）中的所有键列表\n\t *\n\t * @return 默认分组（空分组）中的所有键列表\n\t */\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Set<String> keySet() {\n\t\treturn this.groupedMap.keySet(DEFAULT_GROUP);\n\t}\n\n\t/**\n\t * 获取默认分组（空分组）中的所有值列表\n\t *\n\t * @return 默认分组（空分组）中的所有值列表\n\t */\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Collection<String> values() {\n\t\treturn this.groupedMap.values(DEFAULT_GROUP);\n\t}\n\n\t/**\n\t * 获取默认分组（空分组）中的所有键值对列表\n\t *\n\t * @return 默认分组（空分组）中的所有键值对列表\n\t */\n\t@SuppressWarnings(\"NullableProblems\")\n\t@Override\n\tpublic Set<Entry<String, String>> entrySet() {\n\t\treturn this.groupedMap.entrySet(DEFAULT_GROUP);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + ((charset == null) ? 0 : charset.hashCode());\n\t\tresult = prime * result + groupedMap.hashCode();\n\t\tresult = prime * result + (isUseVariable ? 1231 : 1237);\n\t\tresult = prime * result + ((this.resource == null) ? 0 : this.resource.hashCode());\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t}\n\t\tif (obj == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (getClass() != obj.getClass()) {\n\t\t\treturn false;\n\t\t}\n\t\tSetting other = (Setting) obj;\n\t\tif (charset == null) {\n\t\t\tif (other.charset != null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (false == charset.equals(other.charset)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (false == groupedMap.equals(other.groupedMap)) {\n\t\t\treturn false;\n\t\t}\n\t\tif (isUseVariable != other.isUseVariable) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.resource == null) {\n\t\t\treturn other.resource == null;\n\t\t} else {\n\t\t\treturn resource.equals(other.resource);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn groupedMap.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/SettingLoader.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.NoResourceException;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.util.*;\nimport cn.hutool.log.Log;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.Set;\n\n/**\n * Setting文件加载器\n *\n * @author Looly\n *\n */\npublic class SettingLoader {\n\tprivate static final Log log = Log.get();\n\n\t/** 注释符号（当有此符号在行首，表示此行为注释） */\n\tprivate final static char COMMENT_FLAG_PRE = '#';\n\t/** 赋值分隔符（用于分隔键值对） */\n\tprivate char assignFlag = '=';\n\t/** 变量名称的正则 */\n\tprivate String varRegex = \"\\\\$\\\\{(.*?)\\\\}\";\n\n\t/** 本设置对象的字符集 */\n\tprivate final Charset charset;\n\t/** 是否使用变量 */\n\tprivate final boolean isUseVariable;\n\t/** GroupedMap */\n\tprivate final GroupedMap groupedMap;\n\n\t/**\n\t * 构造\n\t *\n\t * @param groupedMap GroupedMap\n\t */\n\tpublic SettingLoader(GroupedMap groupedMap) {\n\t\tthis(groupedMap, CharsetUtil.CHARSET_UTF_8, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param groupedMap GroupedMap\n\t * @param charset 编码\n\t * @param isUseVariable 是否使用变量\n\t */\n\tpublic SettingLoader(GroupedMap groupedMap, Charset charset, boolean isUseVariable) {\n\t\tthis.groupedMap = groupedMap;\n\t\tthis.charset = charset;\n\t\tthis.isUseVariable = isUseVariable;\n\t}\n\n\t/**\n\t * 加载设置文件\n\t *\n\t * @param resource 配置文件URL\n\t * @return 加载是否成功\n\t * @throws NoResourceException 如果资源不存在，抛出此异常\n\t */\n\tpublic boolean load(Resource resource) throws NoResourceException{\n\t\tif (resource == null) {\n\t\t\tthrow new NullPointerException(\"Null setting url define!\");\n\t\t}\n\t\tInputStream settingStream = null;\n\t\ttry {\n\t\t\tsettingStream = resource.getStream();\n\t\t\tload(settingStream);\n\t\t\tlog.debug(\"Load setting file [{}]\", resource);\n\t\t} catch (Exception e) {\n\t\t\tif(e instanceof NoResourceException){\n\t\t\t\tthrow (NoResourceException)e;\n\t\t\t}\n\t\t\tthrow new NoResourceException(e);\n\t\t} finally {\n\t\t\tIoUtil.close(settingStream);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 加载设置文件。 此方法不会关闭流对象\n\t *\n\t * @param settingStream 文件流\n\t * @return 加载成功与否\n\t * @throws IOException IO异常\n\t */\n\tsynchronized public boolean load(InputStream settingStream) throws IOException {\n\t\tthis.groupedMap.clear();\n\t\tBufferedReader reader = null;\n\t\ttry {\n\t\t\treader = IoUtil.getReader(settingStream, this.charset);\n\t\t\t// 分组\n\t\t\tString group = null;\n\n\t\t\tString line;\n\t\t\twhile (true) {\n\t\t\t\tline = reader.readLine();\n\t\t\t\tif (line == null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tline = StrUtil.trim(line);\n\t\t\t\t// 跳过注释行和空行\n\t\t\t\tif (StrUtil.isBlank(line) || StrUtil.startWith(line, COMMENT_FLAG_PRE)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 记录分组名\n\t\t\t\tif (StrUtil.isSurround(line, CharUtil.BRACKET_START, CharUtil.BRACKET_END)) {\n\t\t\t\t\tgroup = StrUtil.trim(line.substring(1, line.length() - 1));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfinal String[] keyValue = StrUtil.splitToArray(line, this.assignFlag, 2);\n\t\t\t\t// 跳过不符合键值规范的行\n\t\t\t\tif (keyValue.length < 2) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tString value = StrUtil.trim(keyValue[1]);\n\t\t\t\t// 替换值中的所有变量变量（变量必须是此行之前定义的变量，否则无法找到）\n\t\t\t\tif (this.isUseVariable) {\n\t\t\t\t\tvalue = replaceVar(group, value);\n\t\t\t\t}\n\t\t\t\tthis.groupedMap.put(group, StrUtil.trim(keyValue[0]), value);\n\t\t\t}\n\t\t} finally {\n\t\t\tIoUtil.close(reader);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * 设置变量的正则<br>\n\t * 正则只能有一个group表示变量本身，剩余为字符 例如 \\$\\{(name)\\}表示${name}变量名为name的一个变量表示\n\t *\n\t * @param regex 正则\n\t */\n\tpublic void setVarRegex(String regex) {\n\t\tthis.varRegex = regex;\n\t}\n\n\t/**\n\t * 赋值分隔符（用于分隔键值对）\n\t *\n\t * @param assignFlag 正则\n\t * @since 4.6.5\n\t */\n\tpublic void setAssignFlag(char assignFlag) {\n\t\tthis.assignFlag = assignFlag;\n\t}\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置<br>\n\t * 持久化会不会保留之前的分组\n\t *\n\t * @param absolutePath 设置文件的绝对路径\n\t */\n\tpublic void store(String absolutePath) {\n\t\tstore(FileUtil.touch(absolutePath));\n\t}\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置<br>\n\t * 持久化会不会保留之前的分组\n\t *\n\t * @param file 设置文件\n\t * @since 5.4.3\n\t */\n\tpublic void store(File file) {\n\t\tAssert.notNull(file, \"File to store must be not null !\");\n\t\tlog.debug(\"Store Setting to [{}]...\", file.getAbsolutePath());\n\t\tPrintWriter writer = null;\n\t\ttry {\n\t\t\twriter = FileUtil.getPrintWriter(file, charset, false);\n\t\t\tstore(writer);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 存储到Writer\n\t *\n\t * @param writer Writer\n\t */\n\tsynchronized private void store(PrintWriter writer) {\n\t\tfor (Entry<String, LinkedHashMap<String, String>> groupEntry : this.groupedMap.entrySet()) {\n\t\t\twriter.println(StrUtil.format(\"{}{}{}\", CharUtil.BRACKET_START, groupEntry.getKey(), CharUtil.BRACKET_END));\n\t\t\tfor (Entry<String, String> entry : groupEntry.getValue().entrySet()) {\n\t\t\t\twriter.println(StrUtil.format(\"{} {} {}\", entry.getKey(), this.assignFlag, entry.getValue()));\n\t\t\t}\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 替换给定值中的变量标识\n\t *\n\t * @param group 所在分组\n\t * @param value 值\n\t * @return 替换后的字符串\n\t */\n\tprivate String replaceVar(String group, String value) {\n\t\t// 找到所有变量标识\n\t\tfinal Set<String> vars = ReUtil.findAll(varRegex, value, 0, new HashSet<>());\n\t\tString key;\n\t\tfor (String var : vars) {\n\t\t\tkey = ReUtil.get(varRegex, var, 1);\n\t\t\tif (StrUtil.isNotBlank(key)) {\n\t\t\t\t// 本分组中查找变量名对应的值\n\t\t\t\tString varValue = this.groupedMap.get(group, key);\n\t\t\t\t// 跨分组查找\n\t\t\t\tif (null == varValue) {\n\t\t\t\t\tfinal List<String> groupAndKey = StrUtil.split(key, CharUtil.DOT, 2);\n\t\t\t\t\tif (groupAndKey.size() > 1) {\n\t\t\t\t\t\tvarValue = this.groupedMap.get(groupAndKey.get(0), groupAndKey.get(1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// 系统参数和环境变量中查找\n\t\t\t\tif (null == varValue) {\n\t\t\t\t\tvarValue = SystemPropsUtil.get(key);\n\t\t\t\t}\n\n\t\t\t\tif (null != varValue) {\n\t\t\t\t\t// 替换标识\n\t\t\t\t\tvalue = value.replace(var, varValue);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\t// ----------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/SettingRuntimeException.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * 设置异常\n *\n * @author xiaoleilu\n */\npublic class SettingRuntimeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 7941096116780378387L;\n\n\tpublic SettingRuntimeException(Throwable e) {\n\t\tsuper(e);\n\t}\n\n\tpublic SettingRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic SettingRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic SettingRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic SettingRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic SettingRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/SettingUtil.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.io.file.FileNameUtil;\nimport cn.hutool.core.io.resource.NoResourceException;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Map;\n\n/**\n * Setting工具类<br>\n * 提供静态方法获取配置文件\n *\n * @author looly\n */\npublic class SettingUtil {\n\t/**\n\t * 配置文件缓存\n\t */\n\tprivate static final Map<String, Setting> SETTING_MAP = new SafeConcurrentHashMap<>();\n\n\t/**\n\t * 获取当前环境下的配置文件<br>\n\t * name可以为不包括扩展名的文件名（默认.setting为结尾），也可以是文件名全称\n\t *\n\t * @param name 文件名，如果没有扩展名，默认为.setting\n\t * @return 当前环境下配置文件\n\t */\n\tpublic static Setting get(String name) {\n\t\treturn SETTING_MAP.computeIfAbsent(name, (filePath)->{\n\t\t\tfinal String extName = FileNameUtil.extName(filePath);\n\t\t\tif (StrUtil.isEmpty(extName)) {\n\t\t\t\tfilePath = filePath + \".\" + Setting.EXT_NAME;\n\t\t\t}\n\t\t\treturn new Setting(filePath, true);\n\t\t});\n\t}\n\n\t/**\n\t * 获取给定路径找到的第一个配置文件<br>\n\t * * name可以为不包括扩展名的文件名（默认.setting为结尾），也可以是文件名全称\n\t *\n\t * @param names 文件名，如果没有扩展名，默认为.setting\n\t *\n\t * @return 当前环境下配置文件\n\t * @since 5.1.3\n\t */\n\tpublic static Setting getFirstFound(String... names) {\n\t\tfor (String name : names) {\n\t\t\ttry {\n\t\t\t\treturn get(name);\n\t\t\t} catch (NoResourceException e) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/dialect/Props.java",
    "content": "package cn.hutool.setting.dialect;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.getter.BasicTypeGetter;\nimport cn.hutool.core.getter.OptBasicTypeGetter;\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.ClassPathResource;\nimport cn.hutool.core.io.resource.FileResource;\nimport cn.hutool.core.io.resource.Resource;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.io.resource.UrlResource;\nimport cn.hutool.core.io.watch.SimpleWatcher;\nimport cn.hutool.core.io.watch.WatchMonitor;\nimport cn.hutool.core.io.watch.WatchUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.MapUtil;\nimport cn.hutool.core.util.CharsetUtil;\nimport cn.hutool.core.util.ReflectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.StaticLog;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\nimport java.nio.file.WatchEvent;\nimport java.util.Date;\nimport java.util.Properties;\n\n/**\n * Properties文件读取封装类\n *\n * @author loolly\n */\npublic final class Props extends Properties implements BasicTypeGetter<String>, OptBasicTypeGetter<String> {\n\tprivate static final long serialVersionUID = 1935981579709590740L;\n\n\t/**\n\t * 默认配置文件扩展名\n\t */\n\tpublic final static String EXT_NAME = \"properties\";\n\n\t/**\n\t * 构建一个空的Props，用于手动加入参数\n\t *\n\t * @return Setting\n\t * @since 5.4.3\n\t */\n\tpublic static Props create() {\n\t\treturn new Props();\n\t}\n\n\t// ----------------------------------------------------------------------- 私有属性 start\n\t/**\n\t * 属性文件的Resource\n\t */\n\tprivate Resource resource;\n\tprivate WatchMonitor watchMonitor;\n\t/**\n\t * properties文件编码<br>\n\t * issue#1701，此属性不能被序列化，故忽略序列化\n\t */\n\tprivate transient Charset charset = CharsetUtil.CHARSET_ISO_8859_1;\n\t// ----------------------------------------------------------------------- 私有属性 end\n\n\t/**\n\t * 获得Classpath下的Properties文件\n\t *\n\t * @param resource 资源（相对Classpath的路径）\n\t * @return Props\n\t */\n\tpublic static Props getProp(String resource) {\n\t\treturn new Props(resource);\n\t}\n\n\t/**\n\t * 获得Classpath下的Properties文件\n\t *\n\t * @param resource    资源（相对Classpath的路径）\n\t * @param charsetName 字符集\n\t * @return Properties\n\t */\n\tpublic static Props getProp(String resource, String charsetName) {\n\t\treturn new Props(resource, charsetName);\n\t}\n\n\t/**\n\t * 获得Classpath下的Properties文件\n\t *\n\t * @param resource 资源（相对Classpath的路径）\n\t * @param charset  字符集\n\t * @return Properties\n\t */\n\tpublic static Props getProp(String resource, Charset charset) {\n\t\treturn new Props(resource, charset);\n\t}\n\n\t// ----------------------------------------------------------------------- 构造方法 start\n\n\t/**\n\t * 构造\n\t */\n\tpublic Props() {\n\t}\n\n\t/**\n\t * 构造，使用相对于Class文件根目录的相对路径\n\t *\n\t * @param path 配置文件路径，相对于ClassPath，或者使用绝对路径\n\t */\n\tpublic Props(String path) {\n\t\tthis(path, CharsetUtil.CHARSET_ISO_8859_1);\n\t}\n\n\t/**\n\t * 构造，使用相对于Class文件根目录的相对路径\n\t *\n\t * @param path        相对或绝对路径\n\t * @param charsetName 字符集\n\t */\n\tpublic Props(String path, String charsetName) {\n\t\tthis(path, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 构造，使用相对于Class文件根目录的相对路径\n\t *\n\t * @param path    相对或绝对路径\n\t * @param charset 字符集\n\t */\n\tpublic Props(String path, Charset charset) {\n\t\tAssert.notBlank(path, \"Blank properties file path !\");\n\t\tif (null != charset) {\n\t\t\tthis.charset = charset;\n\t\t}\n\t\tthis.load(ResourceUtil.getResourceObj(path));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param propertiesFile 配置文件对象\n\t */\n\tpublic Props(File propertiesFile) {\n\t\tthis(propertiesFile, StandardCharsets.ISO_8859_1);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param propertiesFile 配置文件对象\n\t * @param charsetName    字符集\n\t */\n\tpublic Props(File propertiesFile, String charsetName) {\n\t\tthis(propertiesFile, Charset.forName(charsetName));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param propertiesFile 配置文件对象\n\t * @param charset        字符集\n\t */\n\tpublic Props(File propertiesFile, Charset charset) {\n\t\tAssert.notNull(propertiesFile, \"Null properties file!\");\n\t\tthis.charset = charset;\n\t\tthis.load(new FileResource(propertiesFile));\n\t}\n\n\t/**\n\t * 构造，相对于classes读取文件\n\t *\n\t * @param path  相对路径\n\t * @param clazz 基准类\n\t */\n\tpublic Props(String path, Class<?> clazz) {\n\t\tthis(path, clazz, CharsetUtil.ISO_8859_1);\n\t}\n\n\t/**\n\t * 构造，相对于classes读取文件\n\t *\n\t * @param path        相对路径\n\t * @param clazz       基准类\n\t * @param charsetName 字符集\n\t */\n\tpublic Props(String path, Class<?> clazz, String charsetName) {\n\t\tthis(path, clazz, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 构造，相对于classes读取文件\n\t *\n\t * @param path    相对路径\n\t * @param clazz   基准类\n\t * @param charset 字符集\n\t */\n\tpublic Props(String path, Class<?> clazz, Charset charset) {\n\t\tAssert.notBlank(path, \"Blank properties file path !\");\n\t\tif (null != charset) {\n\t\t\tthis.charset = charset;\n\t\t}\n\t\tthis.load(new ClassPathResource(path, clazz));\n\t}\n\n\t/**\n\t * 构造，使用URL读取\n\t *\n\t * @param propertiesUrl 属性文件路径\n\t */\n\tpublic Props(URL propertiesUrl) {\n\t\tthis(propertiesUrl, StandardCharsets.ISO_8859_1);\n\t}\n\n\t/**\n\t * 构造，使用URL读取\n\t *\n\t * @param propertiesUrl 属性文件路径\n\t * @param charsetName   字符集\n\t */\n\tpublic Props(URL propertiesUrl, String charsetName) {\n\t\tthis(propertiesUrl, CharsetUtil.charset(charsetName));\n\t}\n\n\t/**\n\t * 构造，使用URL读取\n\t *\n\t * @param propertiesUrl 属性文件路径\n\t * @param charset       字符集\n\t */\n\tpublic Props(URL propertiesUrl, Charset charset) {\n\t\tAssert.notNull(propertiesUrl, \"Null properties URL !\");\n\t\tif (null != charset) {\n\t\t\tthis.charset = charset;\n\t\t}\n\t\tthis.load(propertiesUrl);\n\t}\n\n\t/**\n\t * 构造，使用URL读取\n\t *\n\t * @param properties 属性文件路径\n\t */\n\tpublic Props(Properties properties) {\n\t\tif (MapUtil.isNotEmpty(properties)) {\n\t\t\tthis.putAll(properties);\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------- 构造方法 end\n\n\t/**\n\t * 初始化配置文件\n\t *\n\t * @param url {@link URL}\n\t * @since 5.5.2\n\t */\n\tpublic void load(URL url) {\n\t\tload(new UrlResource(url));\n\t}\n\n\t/**\n\t * 初始化配置文件\n\t *\n\t * @param resource {@link Resource}\n\t */\n\tpublic void load(Resource resource) {\n\t\tAssert.notNull(resource, \"Props resource must be not null!\");\n\t\tthis.resource = resource;\n\n\t\ttry (final BufferedReader reader = resource.getReader(charset)) {\n\t\t\tsuper.load(reader);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 重新加载配置文件\n\t */\n\tpublic void load() {\n\t\tthis.load(this.resource);\n\t}\n\n\t/**\n\t * 在配置文件变更时自动加载\n\t *\n\t * @param autoReload 是否自动加载\n\t */\n\tpublic void autoLoad(boolean autoReload) {\n\t\tif (autoReload) {\n\t\t\tAssert.notNull(this.resource, \"Properties resource must be not null!\");\n\t\t\tif (null != this.watchMonitor) {\n\t\t\t\t// 先关闭之前的监听\n\t\t\t\tthis.watchMonitor.close();\n\t\t\t}\n\t\t\tthis.watchMonitor = WatchUtil.createModify(this.resource.getUrl(), new SimpleWatcher() {\n\t\t\t\t@Override\n\t\t\t\tpublic void onModify(WatchEvent<?> event, Path currentPath) {\n\t\t\t\t\tload();\n\t\t\t\t}\n\t\t\t});\n\t\t\tthis.watchMonitor.start();\n\t\t} else {\n\t\t\tIoUtil.close(this.watchMonitor);\n\t\t\tthis.watchMonitor = null;\n\t\t}\n\t}\n\n\t// ----------------------------------------------------------------------- Get start\n\t@Override\n\tpublic Object getObj(String key, Object defaultValue) {\n\t\treturn getStr(key, null == defaultValue ? null : defaultValue.toString());\n\t}\n\n\t@Override\n\tpublic Object getObj(String key) {\n\t\treturn getObj(key, null);\n\t}\n\n\t@Override\n\tpublic String getStr(String key, String defaultValue) {\n\t\treturn super.getProperty(key, defaultValue);\n\t}\n\n\t@Override\n\tpublic String getStr(String key) {\n\t\treturn super.getProperty(key);\n\t}\n\n\t@Override\n\tpublic Integer getInt(String key, Integer defaultValue) {\n\t\treturn Convert.toInt(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Integer getInt(String key) {\n\t\treturn getInt(key, null);\n\t}\n\n\t@Override\n\tpublic Boolean getBool(String key, Boolean defaultValue) {\n\t\treturn Convert.toBool(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Boolean getBool(String key) {\n\t\treturn getBool(key, null);\n\t}\n\n\t@Override\n\tpublic Long getLong(String key, Long defaultValue) {\n\t\treturn Convert.toLong(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Long getLong(String key) {\n\t\treturn getLong(key, null);\n\t}\n\n\t@Override\n\tpublic Character getChar(String key, Character defaultValue) {\n\t\tfinal String value = getStr(key);\n\t\tif (StrUtil.isBlank(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn value.charAt(0);\n\t}\n\n\t@Override\n\tpublic Character getChar(String key) {\n\t\treturn getChar(key, null);\n\t}\n\n\t@Override\n\tpublic Float getFloat(String key) {\n\t\treturn getFloat(key, null);\n\t}\n\n\t@Override\n\tpublic Float getFloat(String key, Float defaultValue) {\n\t\treturn Convert.toFloat(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Double getDouble(String key, Double defaultValue) throws NumberFormatException {\n\t\treturn Convert.toDouble(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Double getDouble(String key) throws NumberFormatException {\n\t\treturn getDouble(key, null);\n\t}\n\n\t@Override\n\tpublic Short getShort(String key, Short defaultValue) {\n\t\treturn Convert.toShort(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Short getShort(String key) {\n\t\treturn getShort(key, null);\n\t}\n\n\t@Override\n\tpublic Byte getByte(String key, Byte defaultValue) {\n\t\treturn Convert.toByte(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Byte getByte(String key) {\n\t\treturn getByte(key, null);\n\t}\n\n\t@Override\n\tpublic BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {\n\t\tfinal String valueStr = getStr(key);\n\t\tif (StrUtil.isBlank(valueStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn new BigDecimal(valueStr);\n\t\t} catch (Exception e) {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n\n\t@Override\n\tpublic BigDecimal getBigDecimal(String key) {\n\t\treturn getBigDecimal(key, null);\n\t}\n\n\t@Override\n\tpublic BigInteger getBigInteger(String key, BigInteger defaultValue) {\n\t\tfinal String valueStr = getStr(key);\n\t\tif (StrUtil.isBlank(valueStr)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\ttry {\n\t\t\treturn new BigInteger(valueStr);\n\t\t} catch (Exception e) {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n\n\t@Override\n\tpublic BigInteger getBigInteger(String key) {\n\t\treturn getBigInteger(key, null);\n\t}\n\n\t@Override\n\tpublic <E extends Enum<E>> E getEnum(Class<E> clazz, String key, E defaultValue) {\n\t\treturn Convert.toEnum(clazz, getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic <E extends Enum<E>> E getEnum(Class<E> clazz, String key) {\n\t\treturn getEnum(clazz, key, null);\n\t}\n\n\t@Override\n\tpublic Date getDate(String key, Date defaultValue) {\n\t\treturn Convert.toDate(getStr(key), defaultValue);\n\t}\n\n\t@Override\n\tpublic Date getDate(String key) {\n\t\treturn getDate(key, null);\n\t}\n\n\t/**\n\t * 获取并删除键值对，当指定键对应值非空时，返回并删除这个值，后边的键对应的值不再查找\n\t *\n\t * @param keys 键列表，常用于别名\n\t * @return 字符串值\n\t * @since 4.1.21\n\t */\n\tpublic String getAndRemoveStr(String... keys) {\n\t\tObject value = null;\n\t\tfor (String key : keys) {\n\t\t\tvalue = remove(key);\n\t\t\tif (null != value) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn (String) value;\n\t}\n\n\t/**\n\t * 转换为标准的{@link Properties}对象\n\t *\n\t * @return {@link Properties}对象\n\t * @since 5.7.4\n\t */\n\tpublic Properties toProperties() {\n\t\tfinal Properties properties = new Properties();\n\t\tproperties.putAll(this);\n\t\treturn properties;\n\t}\n\n\t/**\n\t * 将配置文件转换为Bean，支持嵌套Bean<br>\n\t * 支持的表达式：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * ['person']['friends'][5]['name']\n\t * </pre>\n\t *\n\t * @param <T>       Bean类型\n\t * @param beanClass Bean类\n\t * @return Bean对象\n\t * @since 4.6.3\n\t */\n\tpublic <T> T toBean(Class<T> beanClass) {\n\t\treturn toBean(beanClass, null);\n\t}\n\n\t/**\n\t * 将配置文件转换为Bean，支持嵌套Bean<br>\n\t * 支持的表达式：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * ['person']['friends'][5]['name']\n\t * </pre>\n\t *\n\t * @param <T>       Bean类型\n\t * @param beanClass Bean类\n\t * @param prefix    公共前缀，不指定前缀传null，当指定前缀后非此前缀的属性被忽略\n\t * @return Bean对象\n\t * @since 4.6.3\n\t */\n\tpublic <T> T toBean(Class<T> beanClass, String prefix) {\n\t\tfinal T bean = ReflectUtil.newInstanceIfPossible(beanClass);\n\t\treturn fillBean(bean, prefix);\n\t}\n\n\t/**\n\t * 将配置文件转换为Bean，支持嵌套Bean<br>\n\t * 支持的表达式：\n\t *\n\t * <pre>\n\t * persion\n\t * persion.name\n\t * persons[3]\n\t * person.friends[5].name\n\t * ['person']['friends'][5]['name']\n\t * </pre>\n\t *\n\t * @param <T>    Bean类型\n\t * @param bean   Bean对象\n\t * @param prefix 公共前缀，不指定前缀传null，当指定前缀后非此前缀的属性被忽略\n\t * @return Bean对象\n\t * @since 4.6.3\n\t */\n\tpublic <T> T fillBean(T bean, String prefix) {\n\t\tprefix = StrUtil.nullToEmpty(StrUtil.addSuffixIfNot(prefix, StrUtil.DOT));\n\n\t\tString key;\n\t\tfor (java.util.Map.Entry<Object, Object> entry : this.entrySet()) {\n\t\t\tkey = (String) entry.getKey();\n\t\t\tif (false == StrUtil.startWith(key, prefix)) {\n\t\t\t\t// 非指定开头的属性忽略掉\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tBeanUtil.setProperty(bean, StrUtil.subSuf(key, prefix.length()), entry.getValue());\n\t\t\t} catch (Exception e) {\n\t\t\t\t// 忽略注入失败的字段（这些字段可能用于其它配置）\n\t\t\t\tStaticLog.debug(\"Ignore property: [{}],because of: {}\", key, e);\n\t\t\t}\n\t\t}\n\n\t\treturn bean;\n\t}\n\n\t// ----------------------------------------------------------------------- Get end\n\n\t// ----------------------------------------------------------------------- Set start\n\n\t/**\n\t * 设置值，无给定键创建之。设置后未持久化\n\t *\n\t * @param key   属性键\n\t * @param value 属性值\n\t */\n\tpublic void setProperty(String key, Object value) {\n\t\tsuper.setProperty(key, value.toString());\n\t}\n\n\t/**\n\t * 持久化当前设置，会覆盖掉之前的设置\n\t *\n\t * @param absolutePath 设置文件的绝对路径\n\t * @throws IORuntimeException IO异常，可能为文件未找到\n\t */\n\tpublic void store(String absolutePath) throws IORuntimeException {\n\t\tWriter writer = null;\n\t\ttry {\n\t\t\twriter = FileUtil.getWriter(absolutePath, charset, false);\n\t\t\tsuper.store(writer, null);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e, \"Store properties to [{}] error!\", absolutePath);\n\t\t} finally {\n\t\t\tIoUtil.close(writer);\n\t\t}\n\t}\n\n\t/**\n\t * 存储当前设置，会覆盖掉以前的设置\n\t *\n\t * @param path  相对路径\n\t * @param clazz 相对的类\n\t */\n\tpublic void store(String path, Class<?> clazz) {\n\t\tthis.store(FileUtil.getAbsolutePath(path, clazz));\n\t}\n\t// ----------------------------------------------------------------------- Set end\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/dialect/PropsUtil.java",
    "content": "package cn.hutool.setting.dialect;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.io.resource.NoResourceException;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.util.Map;\n\n/**\n * Props工具类<br>\n * 提供静态方法获取配置文件\n *\n * @author looly\n * @since 5.1.3\n */\npublic class PropsUtil {\n\n\t/**\n\t * 配置文件缓存\n\t */\n\tprivate static final Map<String, Props> propsMap = new SafeConcurrentHashMap<>();\n\n\t/**\n\t * 获取当前环境下的配置文件<br>\n\t * name可以为不包括扩展名的文件名（默认.properties），也可以是文件名全称\n\t *\n\t * @param name 文件名，如果没有扩展名，默认为.properties\n\t * @return 当前环境下配置文件\n\t */\n\tpublic static Props get(String name) {\n\t\treturn propsMap.computeIfAbsent(name, (filePath)->{\n\t\t\tfinal String extName = FileUtil.extName(filePath);\n\t\t\tif (StrUtil.isEmpty(extName)) {\n\t\t\t\tfilePath = filePath + \".\" + Props.EXT_NAME;\n\t\t\t}\n\t\t\treturn new Props(filePath);\n\t\t});\n\t}\n\n\t/**\n\t * 获取给定路径找到的第一个配置文件<br>\n\t * * name可以为不包括扩展名的文件名（默认.properties为结尾），也可以是文件名全称\n\t *\n\t * @param names 文件名，如果没有扩展名，默认为.properties\n\t *\n\t * @return 当前环境下配置文件\n\t */\n\tpublic static Props getFirstFound(String... names) {\n\t\tfor (String name : names) {\n\t\t\ttry {\n\t\t\t\treturn get(name);\n\t\t\t} catch (NoResourceException e) {\n\t\t\t\t//ignore\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取系统参数，例如用户在执行java命令时定义的 -Duse=hutool\n\t *\n\t * @return 系统参数Props\n\t * @since 5.5.2\n\t */\n\tpublic static Props getSystemProps(){\n\t\treturn new Props(System.getProperties());\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/dialect/package-info.java",
    "content": "/**\n * 配置文件实现封装，例如Properties封装Props\n *\n * @author looly\n *\n */\npackage cn.hutool.setting.dialect;\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/package-info.java",
    "content": "/**\n * Setting模块主要针对Properties文件读写做封装，同时定义一套自己的配置文件规范，实现兼容性良好的配置工具。\n * \n * @author looly\n *\n */\npackage cn.hutool.setting;"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/profile/GlobalProfile.java",
    "content": "package cn.hutool.setting.profile;\n\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.setting.Setting;\n\n/**\n * 全局的Profile配置中心\n * \n * @author Looly\n *\n */\npublic class GlobalProfile {\n\n\tprivate GlobalProfile() {\n\t}\n\n\t// -------------------------------------------------------------------------------- Static method start\n\t/**\n\t * 设置全局环境\n\t * @param profile 环境\n\t * @return {@link Profile}\n\t */\n\tpublic static Profile setProfile(String profile) {\n\t\treturn Singleton.get(Profile.class, profile);\n\t}\n\n\t/**\n\t * 获得全局的当前环境下对应的配置文件\n\t * @param settingName 配置文件名，可以忽略默认后者（.setting）\n\t * @return {@link Setting}\n\t */\n\tpublic static Setting getSetting(String settingName) {\n\t\treturn Singleton.get(Profile.class).getSetting(settingName);\n\t}\n\t// -------------------------------------------------------------------------------- Static method end\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/profile/Profile.java",
    "content": "package cn.hutool.setting.profile;\n\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.map.SafeConcurrentHashMap;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.setting.Setting;\n\nimport java.io.Serializable;\nimport java.nio.charset.Charset;\nimport java.util.Map;\n\n/**\n * Profile可以让我们定义一系列的配置信息，然后指定其激活条件。<br>\n * 此类中我们规范一套规则如下：<br>\n * 默认的，我们读取${classpath}/default下的配置文件(*.setting文件)，当调用setProfile方法时，指定一个profile，即可读取其目录下的配置文件。<br>\n * 比如我们定义几个profile：test，develop，production，分别代表测试环境、开发环境和线上环境，我希望读取数据库配置文件db.setting，那么：\n * <ol>\n * <li>test =》 ${classpath}/test/db.setting</li>\n * <li>develop =》 ${classpath}/develop/db.setting</li>\n * <li>production =》 ${classpath}/production/db.setting</li>\n * </ol>\n *\n * @author Looly\n *\n */\npublic class Profile implements Serializable {\n\tprivate static final long serialVersionUID = -4189955219454008744L;\n\n\t/** 默认环境 */\n\tpublic static final String DEFAULT_PROFILE = \"default\";\n\n\t/** 条件 */\n\tprivate String profile;\n\t/** 编码 */\n\tprivate Charset charset;\n\t/** 是否使用变量 */\n\tprivate boolean useVar;\n\t/** 配置文件缓存 */\n\tprivate final Map<String, Setting> settingMap = new SafeConcurrentHashMap<>();\n\n\t// -------------------------------------------------------------------------------- Constructor start\n\t/**\n\t * 默认构造，环境使用默认的：default，编码UTF-8，不使用变量\n\t */\n\tpublic Profile() {\n\t\tthis(DEFAULT_PROFILE);\n\t}\n\n\t/**\n\t * 构造，编码UTF-8，不使用变量\n\t *\n\t * @param profile 环境\n\t */\n\tpublic Profile(String profile) {\n\t\tthis(profile, Setting.DEFAULT_CHARSET, false);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param profile 环境\n\t * @param charset 编码\n\t * @param useVar 是否使用变量\n\t */\n\tpublic Profile(String profile, Charset charset, boolean useVar) {\n\t\tthis.profile = profile;\n\t\tthis.charset = charset;\n\t\tthis.useVar = useVar;\n\t}\n\t// -------------------------------------------------------------------------------- Constructor end\n\n\t/**\n\t * 获取当前环境下的配置文件\n\t *\n\t * @param name 文件名，如果没有扩展名，默认为.setting\n\t * @return 当前环境下配置文件\n\t */\n\tpublic Setting getSetting(String name) {\n\t\tString nameForProfile = fixNameForProfile(name);\n\t\tSetting setting = settingMap.get(nameForProfile);\n\t\tif (null == setting) {\n\t\t\tsetting = new Setting(nameForProfile, this.charset, this.useVar);\n\t\t\tsettingMap.put(nameForProfile, setting);\n\t\t}\n\t\treturn setting;\n\t}\n\n\t/**\n\t * 设置环境\n\t *\n\t * @param profile 环境\n\t * @return 自身\n\t */\n\tpublic Profile setProfile(String profile) {\n\t\tthis.profile = profile;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置编码\n\t *\n\t * @param charset 编码\n\t * @return 自身\n\t */\n\tpublic Profile setCharset(Charset charset) {\n\t\tthis.charset = charset;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置是否使用变量\n\t *\n\t * @param useVar 变量\n\t * @return 自身\n\t */\n\tpublic Profile setUseVar(boolean useVar) {\n\t\tthis.useVar = useVar;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 清空所有环境的配置文件\n\t *\n\t * @return 自身\n\t */\n\tpublic Profile clear() {\n\t\tthis.settingMap.clear();\n\t\treturn this;\n\t}\n\n\t// -------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 修正文件名\n\t *\n\t * @param name 文件名\n\t * @return 修正后的文件名\n\t */\n\tprivate String fixNameForProfile(String name) {\n\t\tAssert.notBlank(name, \"Setting name must be not blank !\");\n\t\tfinal String actralProfile = StrUtil.nullToEmpty(this.profile);\n\t\tif (false == name.contains(StrUtil.DOT)) {\n\t\t\treturn StrUtil.format(\"{}/{}.setting\", actralProfile, name);\n\t\t}\n\t\treturn StrUtil.format(\"{}/{}\", actralProfile, name);\n\t}\n\t// -------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/profile/package-info.java",
    "content": "/**\n * 配置环境封装，例如Profile可以自定义不同环境下的配置文件位置\n *\n * @author looly\n *\n */\npackage cn.hutool.setting.profile;\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/yaml/YamlUtil.java",
    "content": "package cn.hutool.setting.yaml;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.io.resource.ResourceUtil;\nimport cn.hutool.core.lang.Assert;\nimport cn.hutool.core.lang.Dict;\nimport org.yaml.snakeyaml.DumperOptions;\nimport org.yaml.snakeyaml.Yaml;\n\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.Writer;\n\n/**\n * 基于Snakeyaml的的YAML读写工具\n *\n * @author looly\n * @since 5.7.14\n */\npublic class YamlUtil {\n\n\t/**\n\t * 从classpath或绝对路径加载YAML文件\n\t *\n\t * @param path YAML路径，相对路径相对classpath\n\t * @return 加载的内容，默认Map\n\t */\n\tpublic static Dict loadByPath(String path) {\n\t\treturn loadByPath(path, Dict.class);\n\t}\n\n\t/**\n\t * 从classpath或绝对路径加载YAML文件\n\t *\n\t * @param <T>  Bean类型，默认map\n\t * @param path YAML路径，相对路径相对classpath\n\t * @param type 加载的Bean类型，即转换为的bean\n\t * @return 加载的内容，默认Map\n\t */\n\tpublic static <T> T loadByPath(String path, Class<T> type) {\n\t\treturn load(ResourceUtil.getStream(path), type);\n\t}\n\n\t/**\n\t * 从流中加载YAML\n\t *\n\t * @param <T>  Bean类型，默认map\n\t * @param in   流\n\t * @param type 加载的Bean类型，即转换为的bean\n\t * @return 加载的内容，默认Map\n\t */\n\tpublic static <T> T load(InputStream in, Class<T> type) {\n\t\treturn load(IoUtil.getBomReader(in), type);\n\t}\n\n\t/**\n\t * 加载YAML，加载完毕后关闭{@link Reader}\n\t *\n\t * @param reader {@link Reader}\n\t * @return 加载的Map\n\t */\n\tpublic static Dict load(Reader reader) {\n\t\treturn load(reader, Dict.class);\n\t}\n\n\t/**\n\t * 加载YAML，加载完毕后关闭{@link Reader}\n\t *\n\t * @param <T>    Bean类型，默认map\n\t * @param reader {@link Reader}\n\t * @param type   加载的Bean类型，即转换为的bean\n\t * @return 加载的内容，默认Map\n\t */\n\tpublic static <T> T load(Reader reader, Class<T> type) {\n\t\treturn load(reader, type, true);\n\t}\n\n\t/**\n\t * 加载YAML\n\t *\n\t * @param <T>           Bean类型，默认map\n\t * @param reader        {@link Reader}\n\t * @param type          加载的Bean类型，即转换为的bean\n\t * @param isCloseReader 加载完毕后是否关闭{@link Reader}\n\t * @return 加载的内容，默认Map\n\t */\n\tpublic static <T> T load(Reader reader, Class<T> type, boolean isCloseReader) {\n\t\tAssert.notNull(reader, \"Reader must be not null !\");\n\t\tif (null == type) {\n\t\t\t//noinspection unchecked\n\t\t\ttype = (Class<T>) Object.class;\n\t\t}\n\n\t\tfinal Yaml yaml = new Yaml();\n\t\ttry {\n\t\t\treturn yaml.loadAs(reader, type);\n\t\t} finally {\n\t\t\tif (isCloseReader) {\n\t\t\t\tIoUtil.close(reader);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 将Bean对象或者Map写出到{@link Writer}\n\t *\n\t * @param object 对象\n\t * @param writer {@link Writer}\n\t */\n\tpublic static void dump(Object object, Writer writer) {\n\t\tfinal DumperOptions options = new DumperOptions();\n\t\toptions.setIndent(2);\n\t\toptions.setPrettyFlow(true);\n\t\toptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);\n\n\t\tdump(object, writer, options);\n\t}\n\n\t/**\n\t * 将Bean对象或者Map写出到{@link Writer}\n\t *\n\t * @param object        对象\n\t * @param writer        {@link Writer}\n\t * @param dumperOptions 输出风格\n\t */\n\tpublic static void dump(Object object, Writer writer, DumperOptions dumperOptions) {\n\t\tif (null == dumperOptions) {\n\t\t\tdumperOptions = new DumperOptions();\n\t\t}\n\t\tfinal Yaml yaml = new Yaml(dumperOptions);\n\t\tyaml.dump(object, writer);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/main/java/cn/hutool/setting/yaml/package-info.java",
    "content": "/**\n * YAML文件的读写封装，基于snakeyaml\n *\n * @author looly\n *\n */\npackage cn.hutool.setting.yaml;\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/Issue3008Test.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.setting.dialect.Props;\nimport cn.hutool.setting.dialect.PropsUtil;\nimport lombok.Data;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class Issue3008Test {\n\n\t/**\n\t * 数组字段追加后生成新的数组，造成赋值丢失<br>\n\t * 修复见：BeanUtil.setFieldValue\n\t */\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal Props props = PropsUtil.get(\"issue3008\");\n\t\tfinal MyUser user = props.toBean(MyUser.class, \"person\");\n\t\tassertEquals(\"[LOL, KFC, COFFE]\", ArrayUtil.toString(user.getHobby()));\n\t}\n\n\t@Data\n\tstatic class MyUser {\n\t\tprivate String[] hobby;\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/IssueI7G34ETest.java",
    "content": "/*\n * Copyright (c) 2023 looly(loolly@aliyun.com)\n * Hutool is licensed under Mulan PSL v2.\n * You can use this software according to the terms and conditions of the Mulan PSL v2.\n * You may obtain a copy of Mulan PSL v2 at:\n *          http://license.coscl.org.cn/MulanPSL2\n * THIS SOFTWARE IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OF ANY KIND,\n * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,\n * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.\n * See the Mulan PSL v2 for more details.\n */\n\npackage cn.hutool.setting;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class IssueI7G34ETest {\n\n\t@Test\n\tpublic void readWithBomTest() {\n\t\tfinal Setting setting = new Setting(\"test_with_bom.setting\");\n\t\tfinal String s = setting.get(\"line1\", \"key1\");\n\n\t\tassertEquals(\"value1\", s);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/PropsTest.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.log.dialect.console.ConsoleLogFactory;\nimport cn.hutool.setting.dialect.Props;\nimport lombok.Data;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n/**\n * Setting单元测试\n *\n * @author Looly\n *\n */\npublic class PropsTest {\n\n\t@BeforeEach\n\tpublic void init() {\n\t\tLogFactory.setCurrentLogFactory(ConsoleLogFactory.class);\n\t}\n\n\t@Test\n\tpublic void propTest() {\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal Props props = new Props(\"test.properties\");\n\t\tfinal String user = props.getProperty(\"user\");\n\t\tassertEquals(user, \"root\");\n\n\t\tfinal String driver = props.getStr(\"driver\");\n\t\tassertEquals(driver, \"com.mysql.jdbc.Driver\");\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void propTestForAbsPAth() {\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tfinal Props props = new Props(\"d:/test.properties\");\n\t\tfinal String user = props.getProperty(\"user\");\n\t\tassertEquals(user, \"root\");\n\n\t\tfinal String driver = props.getStr(\"driver\");\n\t\tassertEquals(driver, \"com.mysql.jdbc.Driver\");\n\t}\n\n\t@Test\n\tpublic void toBeanTest() {\n\t\tfinal Props props = Props.getProp(\"to_bean_test.properties\");\n\n\t\tfinal ConfigProperties cfg = props.toBean(ConfigProperties.class, \"mail\");\n\t\tassertEquals(\"mailer@mail.com\", cfg.getHost());\n\t\tassertEquals(9000, cfg.getPort());\n\t\tassertEquals(\"mailer@mail.com\", cfg.getFrom());\n\n\t\tassertEquals(\"john\", cfg.getCredentials().getUsername());\n\t\tassertEquals(\"password\", cfg.getCredentials().getPassword());\n\t\tassertEquals(\"SHA1\", cfg.getCredentials().getAuthMethod());\n\n\t\tassertEquals(\"true\", cfg.getAdditionalHeaders().get(\"redelivery\"));\n\t\tassertEquals(\"true\", cfg.getAdditionalHeaders().get(\"secure\"));\n\n\t\tassertEquals(\"admin@mail.com\", cfg.getDefaultRecipients().get(0));\n\t\tassertEquals(\"owner@mail.com\", cfg.getDefaultRecipients().get(1));\n\t}\n\n\t@Test\n\tpublic void toBeanWithNullPrefixTest(){\n\t\tfinal Props configProp = new Props();\n\n\t\tconfigProp.setProperty(\"createTime\", Objects.requireNonNull(DateUtil.parse(\"2020-01-01\")));\n\t\tconfigProp.setProperty(\"isInit\", true);\n\t\tconfigProp.setProperty(\"stairPlan\", 1);\n\t\tconfigProp.setProperty(\"stageNum\", 2);\n\t\tconfigProp.setProperty(\"version\", 3);\n\t\tfinal SystemConfig systemConfig = configProp.toBean(SystemConfig.class);\n\n\t\tassertEquals(DateUtil.parse(\"2020-01-01\"), systemConfig.getCreateTime());\n\t\tassertEquals(true, systemConfig.getIsInit());\n\t\tassertEquals(\"1\", systemConfig.getStairPlan());\n\t\tassertEquals(new Integer(2), systemConfig.getStageNum());\n\t\tassertEquals(\"3\", systemConfig.getVersion());\n\t}\n\n\t@Data\n\tpublic static class ConfigProperties {\n\t\tprivate String host;\n\t\tprivate int port;\n\t\tprivate String from;\n\t\tprivate Credentials credentials;\n\t\tprivate List<String> defaultRecipients;\n\t\tprivate Map<String, String> additionalHeaders;\n\t}\n\n\t@Data\n\tpublic static class Credentials {\n\t\tprivate String authMethod;\n\t\tprivate String username;\n\t\tprivate String password;\n\t}\n\n\t@Data\n\tpublic static class SystemConfig {\n\t\tprivate Boolean isInit;//是否初始化\n\t\tprivate Date createTime;//系统创建时间\n\t\tprivate String version;//系统版本\n\t\tprivate String ServiceOS;//服务器系统\n\t\tprivate String stairPlan;//周期计划 1 自然年周期，2 自然月周期，3 季度周期 4 自定义周期\n\t\tprivate Date startDate;//周期开始日期\n\t\tprivate Date nextStartDate;//下一周期开始日期\n\t\tprivate Integer stageNum;//阶段数目\n\t\tprivate Integer[] stageContent;//阶段详情\n\t\tprivate Date theStageTime;//当前阶段开始日期\n\t\tprivate Date nextStageTime;//当前阶段结束日期/下一阶段开始日期\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/PropsUtilTest.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.setting.dialect.PropsUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Objects;\n\npublic class PropsUtilTest {\n\n\t@Test\n\tpublic void getTest() {\n\t\tString driver = PropsUtil.get(\"test\").getStr(\"driver\");\n\t\tassertEquals(\"com.mysql.jdbc.Driver\", driver);\n\t}\n\n\t@Test\n\tpublic void getFirstFoundTest() {\n\t\tString driver = Objects.requireNonNull(PropsUtil.getFirstFound(\"test2\", \"test\")).getStr(\"driver\");\n\t\tassertEquals(\"com.mysql.jdbc.Driver\", driver);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/SettingTest.java",
    "content": "package cn.hutool.setting;\n\nimport cn.hutool.core.lang.Console;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Setting单元测试\n *\n * @author Looly\n */\npublic class SettingTest {\n\n\t@Test\n\tpublic void settingTest() {\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tSetting setting = new Setting(\"test.setting\", true);\n\n\t\tString driver = setting.getByGroup(\"driver\", \"demo\");\n\t\tassertEquals(\"com.mysql.jdbc.Driver\", driver);\n\n\t\t//本分组变量替换\n\t\tString user = setting.getByGroup(\"user\", \"demo\");\n\t\tassertEquals(\"rootcom.mysql.jdbc.Driver\", user);\n\n\t\t//跨分组变量替换\n\t\tString user2 = setting.getByGroup(\"user2\", \"demo\");\n\t\tassertEquals(\"rootcom.mysql.jdbc.Driver\", user2);\n\n\t\t//默认值测试\n\t\tString value = setting.getStr(\"keyNotExist\", \"defaultTest\");\n\t\tassertEquals(\"defaultTest\", value);\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void settingTestForAbsPath() {\n\t\t//noinspection MismatchedQueryAndUpdateOfCollection\n\t\tSetting setting = new Setting(\"d:\\\\excel-plugin\\\\other.setting\", true);\n\t\tConsole.log(setting.getStr(\"a\"));\n\t}\n\n\t@Test\n\tpublic void settingTestForCustom() {\n\t\tSetting setting = new Setting();\n\n\t\tsetting.setByGroup(\"user\", \"group1\", \"root\");\n\t\tsetting.setByGroup(\"user\", \"group2\", \"root2\");\n\t\tsetting.setByGroup(\"user\", \"group3\", \"root3\");\n\t\tsetting.set(\"user\", \"root4\");\n\n\t\tassertEquals(\"root\", setting.getByGroup(\"user\", \"group1\"));\n\t\tassertEquals(\"root2\", setting.getByGroup(\"user\", \"group2\"));\n\t\tassertEquals(\"root3\", setting.getByGroup(\"user\", \"group3\"));\n\t\tassertEquals(\"root4\", setting.get(\"user\"));\n\t}\n\n\t/**\n\t * 测试写出是否正常\n\t */\n\t@Test\n\tpublic void storeTest() {\n\t\tSetting setting = new Setting(\"test.setting\");\n\t\tsetting.set(\"testKey\", \"testValue\");\n\n\t\tsetting.store();\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/SettingUtilTest.java",
    "content": "package cn.hutool.setting;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class SettingUtilTest {\n\n\t@Test\n\tpublic void getTest() {\n\t\tString driver = SettingUtil.get(\"test\").get(\"demo\", \"driver\");\n\t\tassertEquals(\"com.mysql.jdbc.Driver\", driver);\n\t}\n\n\t@Test\n\tpublic void getTest2() {\n\t\tString driver = SettingUtil.get(\"example/example\").get(\"demo\", \"key\");\n\t\tassertEquals(\"value\", driver);\n\t}\n\n\t@Test\n\tpublic void getFirstFoundTest() {\n\t\t//noinspection ConstantConditions\n\t\tString driver = SettingUtil.getFirstFound(\"test2\", \"test\")\n\t\t\t\t.get(\"demo\", \"driver\");\n\t\tassertEquals(\"com.mysql.jdbc.Driver\", driver);\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/java/cn/hutool/setting/yaml/YamlUtilTest.java",
    "content": "package cn.hutool.setting.yaml;\n\nimport cn.hutool.core.io.FileUtil;\nimport cn.hutool.core.lang.Dict;\nimport cn.hutool.core.util.CharsetUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\npublic class YamlUtilTest {\n\n\t@Test\n\tpublic void loadByPathTest() {\n\t\tfinal Dict result = YamlUtil.loadByPath(\"test.yaml\");\n\n\t\tassertEquals(\"John\", result.getStr(\"firstName\"));\n\n\t\tfinal List<Integer> numbers = result.getByPath(\"contactDetails.number\");\n\t\tassertEquals(123456789, (int) numbers.get(0));\n\t\tassertEquals(456786868, (int) numbers.get(1));\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void dumpTest() {\n\t\tfinal Dict dict = Dict.create()\n\t\t\t\t.set(\"name\", \"hutool\")\n\t\t\t\t.set(\"count\", 1000);\n\n\t\tYamlUtil.dump(\n\t\t\t\tdict\n\t\t\t\t, FileUtil.getWriter(\"d:/test/dump.yaml\", CharsetUtil.CHARSET_UTF_8, false));\n\t}\n}\n"
  },
  {
    "path": "hutool-setting/src/test/resources/example/example.set",
    "content": "# -------------------------------------------------------------\n# ----- GroupedSet File with UTF8-----\n# @see com.xiaoleilu.hutool.lang.GroupedSet\n# -------------------------------------------------------------\n\n无分组值1\n无分组值2\n\n[分组1]\n值1\n值2\n值3\n\n[特殊分组]\n1\n2\n3\n\\#转义注释符\n\n[]\n空分组值1\n空分组会合并到无分组里\n\n[没有值的分组]"
  },
  {
    "path": "hutool-setting/src/test/resources/example/example.setting",
    "content": "# -------------------------------------------------------------\n# ----- Setting File with UTF8-----\n# ----- 数据库配置文件 -----\n# -------------------------------------------------------------\n\n# 键值都支持中文，默认UTF-8编码，可以在new Setting的时候设置编码\nkey = value\n\n#中括表示一个分组，其下面的所有属性归属于这个分组，在此分组名为demo，也可以没有分组\n#分组后的键值对在Setting对象中表现形式是：demo.key，也可以使用相应的方法取值\n[demo]\n# 类似于Properties的键值对\nkey = value\n# 支持变量替换（在new Setting的时候需要设置isUseVariable为true）\nkey2 = value${key}\n\n#中括号开始表示新的分组开始，分组相互独立，键值与其他分组互不影响\n[demo2]\nkey = value2\nkey2 = value"
  },
  {
    "path": "hutool-setting/src/test/resources/example/group-set-example.set",
    "content": "#--------------------------------------\n# GroupSet配置文件样例\n#每一个分组下有一组set集合\n# author xiaoleilu\n#--------------------------------------\n\n[group1]\nvalue1\nvalue2\n\n[group2]\nvalue1\nvalue2"
  },
  {
    "path": "hutool-setting/src/test/resources/issue3008.properties",
    "content": "person.hobby[0]=LOL\nperson.hobby[1]=KFC\nperson.hobby[2]=COFFE\n"
  },
  {
    "path": "hutool-setting/src/test/resources/test.properties",
    "content": "# -------------------------------------------------------------\n# ----- Setting File with UTF8-----\n# ----- \\u6570\\u636e\\u5e93\\u914d\\u7f6e\\u6587\\u4ef6 -----\n# -------------------------------------------------------------\n\n#\\u6570\\u636e\\u5e93\\u9a71\\u52a8\\u540d\\uff0c\\u5982\\u679c\\u4e0d\\u6307\\u5b9a\\uff0c\\u5219\\u4f1a\\u6839\\u636eurl\\u81ea\\u52a8\\u5224\\u5b9a\ndriver = com.mysql.jdbc.Driver\n#JDBC url\\uff0c\\u5fc5\\u987b\nurl = jdbc:mysql://fedora.vmware:3306/extractor\n#\\u7528\\u6237\\u540d\\uff0c\\u5fc5\\u987b\nuser = root\n#\\u5bc6\\u7801\\uff0c\\u5fc5\\u987b\\uff0c\\u5982\\u679c\\u5bc6\\u7801\\u4e3a\\u7a7a\\uff0c\\u8bf7\\u586b\\u5199 pass = \npass = 123456"
  },
  {
    "path": "hutool-setting/src/test/resources/test.setting",
    "content": "# -------------------------------------------------------------\n# ----- Setting File with UTF8-----\n# ----- 数据库配置文件 -----\n# -------------------------------------------------------------\n\n#中括表示一个分组，其下面的所有属性归属于这个分组，在此分组名为demo，也可以没有分组\n[demo]\n#数据库驱动名，如果不指定，则会根据url自动判定\ndriver = com.mysql.jdbc.Driver\n#JDBC url，必须\nurl = jdbc:mysql://fedora.vmware:3306/extractor\n#用户名，必须\nuser = root${driver}\nuser2 = root${demo.driver}\n#密码，必须，如果密码为空，请填写 pass = \npass = 123456"
  },
  {
    "path": "hutool-setting/src/test/resources/test.yaml",
    "content": "firstName: \"John\"\nlastName: \"Doe\"\nage: 31\ncontactDetails:\n  - type: \"mobile\"\n    number: 123456789\n  - type: \"landline\"\n    number: 456786868\nhomeAddress:\n  line: \"Xyz, DEF Street\"\n  city: \"City Y\"\n  state: \"State Y\"\n  zip: 345657\n  123: 345\n"
  },
  {
    "path": "hutool-setting/src/test/resources/test_with_bom.setting",
    "content": "﻿[line1]\nkey1 = value1\nkey2 = value2\n"
  },
  {
    "path": "hutool-setting/src/test/resources/to_bean_test.properties",
    "content": "#Simple properties\nmail.host=mailer@mail.com\nmail.port=9000\nmail.from=mailer@mail.com\n\n#List properties\nmail.defaultRecipients[0]=admin@mail.com\nmail.defaultRecipients[1]=owner@mail.com\n\n#Map Properties\nmail.additionalHeaders.redelivery=true\nmail.additionalHeaders.secure=true\n\n#Object properties\nmail.credentials.username=john\nmail.credentials.password=password\nmail.credentials.authMethod=SHA1\n\n# ignore properties\nmail.ignore.filed = balabala"
  },
  {
    "path": "hutool-socket/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-socket</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool套接字，包括BIO、NIO、AIO封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.socket</Automatic-Module-Name>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-log</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/ChannelUtil.java",
    "content": "package cn.hutool.socket;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ThreadFactoryBuilder;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.channels.AsynchronousChannelGroup;\nimport java.nio.channels.AsynchronousSocketChannel;\nimport java.util.concurrent.ExecutionException;\n\n/**\n * Channel相关封装\n *\n * @author looly\n * @since 5.8.2\n */\npublic class ChannelUtil {\n\n\t/**\n\t * 创建{@link AsynchronousChannelGroup}\n\t *\n\t * @param poolSize 线程池大小\n\t * @return {@link AsynchronousChannelGroup}\n\t */\n\tpublic static AsynchronousChannelGroup createFixedGroup(int poolSize) {\n\n\t\ttry {\n\t\t\treturn AsynchronousChannelGroup.withFixedThreadPool(//\n\t\t\t\t\tpoolSize, // 默认线程池大小\n\t\t\t\t\tThreadFactoryBuilder.create().setNamePrefix(\"Huool-socket-\").build()//\n\t\t\t);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 连接到指定地址\n\t *\n\t * @param group   {@link AsynchronousChannelGroup}\n\t * @param address 地址信息，包括地址和端口\n\t * @return {@link AsynchronousSocketChannel}\n\t */\n\tpublic static AsynchronousSocketChannel connect(AsynchronousChannelGroup group, InetSocketAddress address) {\n\t\tAsynchronousSocketChannel channel;\n\t\ttry {\n\t\t\tchannel = AsynchronousSocketChannel.open(group);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\ttry {\n\t\t\tchannel.connect(address).get();\n\t\t} catch (InterruptedException | ExecutionException e) {\n\t\t\tIoUtil.close(channel);\n\t\t\tthrow new SocketRuntimeException(e);\n\t\t}\n\t\treturn channel;\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java",
    "content": "package cn.hutool.socket;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.RuntimeUtil;\n\nimport java.io.Serializable;\n\n/**\n * Socket通讯配置\n * \n * @author looly\n *\n */\npublic class SocketConfig implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\t/** CPU核心数 */\n\tprivate static final int CPU_COUNT = RuntimeUtil.getProcessorCount();\n\n\t/** 共享线程池大小，此线程池用于接收和处理用户连接 */\n\tprivate int threadPoolSize = CPU_COUNT;\n\n\t/** 读取超时时长，小于等于0表示默认 */\n\tprivate long readTimeout;\n\t/** 写出超时时长，小于等于0表示默认 */\n\tprivate long writeTimeout;\n\n\t/** 读取缓存大小 */\n\tprivate int readBufferSize = IoUtil.DEFAULT_BUFFER_SIZE;\n\t/** 写出缓存大小 */\n\tprivate int writeBufferSize = IoUtil.DEFAULT_BUFFER_SIZE;\n\t\n\t/**\n\t * 获取共享线程池大小，此线程池用于接收和处理用户连接\n\t * \n\t * @return 共享线程池大小，此线程池用于接收和处理用户连接\n\t */\n\tpublic int getThreadPoolSize() {\n\t\treturn threadPoolSize;\n\t}\n\n\t/**\n\t * 设置共享线程池大小，此线程池用于接收和处理用户连接\n\t * \n\t * @param threadPoolSize 共享线程池大小，此线程池用于接收和处理用户连接\n\t */\n\tpublic void setThreadPoolSize(int threadPoolSize) {\n\t\tthis.threadPoolSize = threadPoolSize;\n\t}\n\n\t/**\n\t * 获取读取超时时长，小于等于0表示默认\n\t * \n\t * @return 读取超时时长，小于等于0表示默认\n\t */\n\tpublic long getReadTimeout() {\n\t\treturn readTimeout;\n\t}\n\n\t/**\n\t * 设置读取超时时长，小于等于0表示默认\n\t * \n\t * @param readTimeout 读取超时时长，小于等于0表示默认\n\t */\n\tpublic void setReadTimeout(long readTimeout) {\n\t\tthis.readTimeout = readTimeout;\n\t}\n\n\t/**\n\t * 获取写出超时时长，小于等于0表示默认\n\t * \n\t * @return 写出超时时长，小于等于0表示默认\n\t */\n\tpublic long getWriteTimeout() {\n\t\treturn writeTimeout;\n\t}\n\n\t/**\n\t * 设置写出超时时长，小于等于0表示默认\n\t * \n\t * @param writeTimeout 写出超时时长，小于等于0表示默认\n\t */\n\tpublic void setWriteTimeout(long writeTimeout) {\n\t\tthis.writeTimeout = writeTimeout;\n\t}\n\n\t/**\n\t * 获取读取缓存大小\n\t * @return 读取缓存大小\n\t */\n\tpublic int getReadBufferSize() {\n\t\treturn readBufferSize;\n\t}\n\n\t/**\n\t * 设置读取缓存大小\n\t * @param readBufferSize 读取缓存大小\n\t */\n\tpublic void setReadBufferSize(int readBufferSize) {\n\t\tthis.readBufferSize = readBufferSize;\n\t}\n\n\t/**\n\t * 获取写出缓存大小\n\t * @return 写出缓存大小\n\t */\n\tpublic int getWriteBufferSize() {\n\t\treturn writeBufferSize;\n\t}\n\n\t/**\n\t * 设置写出缓存大小\n\t * @param writeBufferSize 写出缓存大小\n\t */\n\tpublic void setWriteBufferSize(int writeBufferSize) {\n\t\tthis.writeBufferSize = writeBufferSize;\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java",
    "content": "package cn.hutool.socket;\n\nimport cn.hutool.core.exceptions.ExceptionUtil;\nimport cn.hutool.core.util.StrUtil;\n\n/**\n * Socket异常\n *\n * @author xiaoleilu\n */\npublic class SocketRuntimeException extends RuntimeException {\n\tprivate static final long serialVersionUID = 8247610319171014183L;\n\n\tpublic SocketRuntimeException(Throwable e) {\n\t\tsuper(ExceptionUtil.getMessage(e), e);\n\t}\n\n\tpublic SocketRuntimeException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic SocketRuntimeException(String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params));\n\t}\n\n\tpublic SocketRuntimeException(String message, Throwable throwable) {\n\t\tsuper(message, throwable);\n\t}\n\n\tpublic SocketRuntimeException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {\n\t\tsuper(message, throwable, enableSuppression, writableStackTrace);\n\t}\n\n\tpublic SocketRuntimeException(Throwable throwable, String messageTemplate, Object... params) {\n\t\tsuper(StrUtil.format(messageTemplate, params), throwable);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java",
    "content": "package cn.hutool.socket;\n\nimport cn.hutool.core.io.IORuntimeException;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.net.Socket;\nimport java.net.SocketAddress;\nimport java.nio.channels.AsynchronousSocketChannel;\nimport java.nio.channels.ClosedChannelException;\n\n/**\n * Socket相关工具类\n *\n * @author looly\n * @since 4.5.0\n */\npublic class SocketUtil {\n\n\t/**\n\t * 获取远程端的地址信息，包括host和端口<br>\n\t * null表示channel为null或者远程主机未连接\n\t *\n\t * @param channel {@link AsynchronousSocketChannel}\n\t * @return 远程端的地址信息，包括host和端口，null表示channel为null或者远程主机未连接\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static SocketAddress getRemoteAddress(AsynchronousSocketChannel channel) throws IORuntimeException {\n\t\ttry {\n\t\t\treturn (null == channel) ? null : channel.getRemoteAddress();\n\t\t} catch (ClosedChannelException e) {\n\t\t\t// Channel未打开或已关闭，返回null表示未连接\n\t\t\treturn null;\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 远程主机是否处于连接状态<br>\n\t * 通过判断远程地址获取成功与否判断\n\t *\n\t * @param channel {@link AsynchronousSocketChannel}\n\t * @return 远程主机是否处于连接状态\n\t * @throws IORuntimeException IO异常\n\t */\n\tpublic static boolean isConnected(AsynchronousSocketChannel channel) throws IORuntimeException {\n\t\treturn null != getRemoteAddress(channel);\n\t}\n\n\t/**\n\t * 创建Socket并连接到指定地址的服务器\n\t *\n\t * @param hostname 地址\n\t * @param port     端口\n\t * @return {@link Socket}\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static Socket connect(String hostname, int port) throws IORuntimeException {\n\t\treturn connect(hostname, port, -1);\n\t}\n\n\t/**\n\t * 创建Socket并连接到指定地址的服务器\n\t *\n\t * @param hostname          地址\n\t * @param port              端口\n\t * @param connectionTimeout 连接超时\n\t * @return {@link Socket}\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static Socket connect(final String hostname, int port, int connectionTimeout) throws IORuntimeException {\n\t\treturn connect(new InetSocketAddress(hostname, port), connectionTimeout);\n\t}\n\n\t/**\n\t * 创建Socket并连接到指定地址的服务器\n\t *\n\t * @param address           地址\n\t * @param connectionTimeout 连接超时\n\t * @return {@link Socket}\n\t * @throws IORuntimeException IO异常\n\t * @since 5.7.8\n\t */\n\tpublic static Socket connect(InetSocketAddress address, int connectionTimeout) throws IORuntimeException {\n\t\tfinal Socket socket = new Socket();\n\t\ttry {\n\t\t\tif (connectionTimeout <= 0) {\n\t\t\t\tsocket.connect(address);\n\t\t\t} else {\n\t\t\t\tsocket.connect(address, connectionTimeout);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn socket;\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java",
    "content": "package cn.hutool.socket.aio;\n\nimport java.nio.ByteBuffer;\nimport java.nio.channels.AsynchronousSocketChannel;\nimport java.nio.channels.CompletionHandler;\n\nimport cn.hutool.log.StaticLog;\n\n/**\n * 接入完成回调，单例使用\n * \n * @author looly\n *\n */\npublic class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AioServer> {\n\n\t@Override\n\tpublic void completed(AsynchronousSocketChannel socketChannel, AioServer aioServer) {\n\t\t// 继续等待接入（异步）\n\t\taioServer.accept();\n\n\t\tfinal IoAction<ByteBuffer> ioAction = aioServer.ioAction;\n\t\t// 创建Session会话\n\t\tfinal AioSession session = new AioSession(socketChannel, ioAction, aioServer.config);\n\t\t// 处理请求接入（同步）\n\t\tioAction.accept(session);\n\n\t\t// 处理读（异步）\n\t\tsession.read();\n\t}\n\n\t@Override\n\tpublic void failed(Throwable exc, AioServer aioServer) {\n\t\tStaticLog.error(exc);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java",
    "content": "package cn.hutool.socket.aio;\n\nimport cn.hutool.socket.ChannelUtil;\nimport cn.hutool.socket.SocketConfig;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.net.SocketOption;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.AsynchronousSocketChannel;\n\n/**\n * Aio Socket客户端\n *\n * @author looly\n * @since 4.5.0\n */\npublic class AioClient implements Closeable{\n\n\tprivate final AioSession session;\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 地址\n\t * @param ioAction IO处理类\n\t */\n\tpublic AioClient(InetSocketAddress address, IoAction<ByteBuffer> ioAction) {\n\t\tthis(address, ioAction, new SocketConfig());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 地址\n\t * @param ioAction IO处理类\n\t * @param config 配置项\n\t */\n\tpublic AioClient(InetSocketAddress address, IoAction<ByteBuffer> ioAction, SocketConfig config) {\n\t\tthis(createChannel(address, config.getThreadPoolSize()), ioAction, config);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param channel {@link AsynchronousSocketChannel}\n\t * @param ioAction IO处理类\n\t * @param config 配置项\n\t */\n\tpublic AioClient(AsynchronousSocketChannel channel, IoAction<ByteBuffer> ioAction, SocketConfig config) {\n\t\tthis.session = new AioSession(channel, ioAction, config);\n\t\tioAction.accept(this.session);\n\t}\n\n\t/**\n\t * 设置 Socket 的 Option 选项<br>\n\t * 选项见：{@link java.net.StandardSocketOptions}\n\t *\n\t * @param <T> 选项泛型\n\t * @param name {@link SocketOption} 枚举\n\t * @param value SocketOption参数\n\t * @return this\n\t * @throws IOException IO异常\n\t */\n\tpublic <T> AioClient setOption(SocketOption<T> name, T value) throws IOException {\n\t\tthis.session.getChannel().setOption(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取IO处理器\n\t *\n\t * @return {@link IoAction}\n\t */\n\tpublic IoAction<ByteBuffer> getIoAction() {\n\t\treturn this.session.getIoAction();\n\t}\n\n\t/**\n\t * 从服务端读取数据\n\t *\n\t * @return this\n\t */\n\tpublic AioClient read() {\n\t\tthis.session.read();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写数据到服务端\n\t *\n\t * @param data 数据\n\t * @return this\n\t */\n\tpublic AioClient write(ByteBuffer data) {\n\t\tthis.session.write(data);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭客户端\n\t */\n\t@Override\n\tpublic void close() {\n\t\tthis.session.close();\n\t}\n\n\t// ------------------------------------------------------------------------------------- Private method start\n\t/**\n\t * 初始化\n\t *\n\t * @param address 地址和端口\n\t * @param poolSize 线程池大小\n\t * @return this\n\t */\n\tprivate static AsynchronousSocketChannel createChannel(InetSocketAddress address, int poolSize) {\n\t\treturn ChannelUtil.connect(ChannelUtil.createFixedGroup(poolSize), address);\n\t}\n\t// ------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java",
    "content": "package cn.hutool.socket.aio;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ThreadFactoryBuilder;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.log.Log;\nimport cn.hutool.log.LogFactory;\nimport cn.hutool.socket.SocketConfig;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.net.SocketOption;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.AsynchronousChannelGroup;\nimport java.nio.channels.AsynchronousServerSocketChannel;\n\n/**\n * 基于AIO的Socket服务端实现\n *\n * @author looly\n */\npublic class AioServer implements Closeable {\n\tprivate static final Log log = LogFactory.get();\n\tprivate static final AcceptHandler ACCEPT_HANDLER = new AcceptHandler();\n\n\tprivate AsynchronousChannelGroup group;\n\tprivate AsynchronousServerSocketChannel channel;\n\tprotected IoAction<ByteBuffer> ioAction;\n\tprotected final SocketConfig config;\n\n\n\t/**\n\t * 构造\n\t *\n\t * @param port 端口\n\t */\n\tpublic AioServer(int port) {\n\t\tthis(new InetSocketAddress(port), new SocketConfig());\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 地址\n\t * @param config  {@link SocketConfig} 配置项\n\t */\n\tpublic AioServer(InetSocketAddress address, SocketConfig config) {\n\t\tthis.config = config;\n\t\tinit(address);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param address 地址和端口\n\t * @return this\n\t */\n\tpublic AioServer init(InetSocketAddress address) {\n\t\ttry {\n\t\t\tthis.group = AsynchronousChannelGroup.withFixedThreadPool(//\n\t\t\t\t\tconfig.getThreadPoolSize(), // 默认线程池大小\n\t\t\t\t\tThreadFactoryBuilder.create().setNamePrefix(\"Hutool-socket-\").build()//\n\t\t\t);\n\t\t\tthis.channel = AsynchronousServerSocketChannel.open(group).bind(address);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 开始监听\n\t *\n\t * @param sync 是否阻塞\n\t */\n\tpublic void start(boolean sync) {\n\t\tdoStart(sync);\n\t}\n\n\t/**\n\t * 设置 Socket 的 Option 选项<br>\n\t * 选项见：{@link java.net.StandardSocketOptions}\n\t *\n\t * @param <T>   选项泛型\n\t * @param name  {@link SocketOption} 枚举\n\t * @param value SocketOption参数\n\t * @return this\n\t * @throws IOException IO异常\n\t */\n\tpublic <T> AioServer setOption(SocketOption<T> name, T value) throws IOException {\n\t\tthis.channel.setOption(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取IO处理器\n\t *\n\t * @return {@link IoAction}\n\t */\n\tpublic IoAction<ByteBuffer> getIoAction() {\n\t\treturn this.ioAction;\n\t}\n\n\t/**\n\t * 设置IO处理器，单例存在\n\t *\n\t * @param ioAction {@link IoAction}\n\t * @return this;\n\t */\n\tpublic AioServer setIoAction(IoAction<ByteBuffer> ioAction) {\n\t\tthis.ioAction = ioAction;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取{@link AsynchronousServerSocketChannel}\n\t *\n\t * @return {@link AsynchronousServerSocketChannel}\n\t */\n\tpublic AsynchronousServerSocketChannel getChannel() {\n\t\treturn this.channel;\n\t}\n\n\t/**\n\t * 处理接入的客户端\n\t *\n\t * @return this\n\t */\n\tpublic AioServer accept() {\n\t\tthis.channel.accept(this, ACCEPT_HANDLER);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 服务是否开启状态\n\t *\n\t * @return 服务是否开启状态\n\t */\n\tpublic boolean isOpen() {\n\t\treturn (null != this.channel) && this.channel.isOpen();\n\t}\n\n\t/**\n\t * 关闭服务\n\t */\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.channel);\n\n\t\tif (null != this.group && false == this.group.isShutdown()) {\n\t\t\ttry {\n\t\t\t\tthis.group.shutdownNow();\n\t\t\t} catch (IOException e) {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\n\t\t// 结束阻塞\n\t\tsynchronized (this) {\n\t\t\tthis.notify();\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------------------------------- Private method start\n\n\t/**\n\t * 开始监听\n\t *\n\t * @param sync 是否阻塞\n\t */\n\tprivate void doStart(boolean sync) {\n\t\tlog.debug(\"Aio Server started, waiting for accept.\");\n\n\t\t// 接收客户端连接\n\t\taccept();\n\n\t\tif (sync) {\n\t\t\tThreadUtil.sync(this);\n\t\t}\n\t}\n\t// ------------------------------------------------------------------------------------- Private method end\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java",
    "content": "package cn.hutool.socket.aio;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.socket.SocketConfig;\nimport cn.hutool.socket.SocketUtil;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.net.SocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.AsynchronousSocketChannel;\nimport java.nio.channels.CompletionHandler;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * AIO会话<br>\n * 每个客户端对应一个会话对象\n * \n * @author looly\n *\n */\npublic class AioSession implements Closeable{\n\n\tprivate static final ReadHandler READ_HANDLER = new ReadHandler();\n\n\tprivate final AsynchronousSocketChannel channel;\n\tprivate final IoAction<ByteBuffer> ioAction;\n\tprivate ByteBuffer readBuffer;\n\tprivate ByteBuffer writeBuffer;\n\t/** 读取超时时长，小于等于0表示默认 */\n\tprivate final long readTimeout;\n\t/** 写出超时时长，小于等于0表示默认 */\n\tprivate final long writeTimeout;\n\n\t/**\n\t * 构造\n\t * \n\t * @param channel {@link AsynchronousSocketChannel}\n\t * @param ioAction IO消息处理类\n\t * @param config 配置项\n\t */\n\tpublic AioSession(AsynchronousSocketChannel channel, IoAction<ByteBuffer> ioAction, SocketConfig config) {\n\t\tthis.channel = channel;\n\t\tthis.ioAction = ioAction;\n\n\t\tthis.readBuffer = ByteBuffer.allocate(config.getReadBufferSize());\n\t\tthis.writeBuffer = ByteBuffer.allocate(config.getWriteBufferSize());\n\t\tthis.readTimeout = config.getReadTimeout();\n\t\tthis.writeTimeout = config.getWriteTimeout();\n\t}\n\n\t/**\n\t * 获取{@link AsynchronousSocketChannel}\n\t * \n\t * @return {@link AsynchronousSocketChannel}\n\t */\n\tpublic AsynchronousSocketChannel getChannel() {\n\t\treturn this.channel;\n\t}\n\n\t/**\n\t * 获取读取Buffer\n\t * \n\t * @return 读取Buffer\n\t */\n\tpublic ByteBuffer getReadBuffer() {\n\t\treturn this.readBuffer;\n\t}\n\n\t/**\n\t * 获取写Buffer\n\t * \n\t * @return 写Buffer\n\t */\n\tpublic ByteBuffer getWriteBuffer() {\n\t\treturn this.writeBuffer;\n\t}\n\n\t/**\n\t * 获取消息处理器\n\t * \n\t * @return {@link IoAction}\n\t */\n\tpublic IoAction<ByteBuffer> getIoAction() {\n\t\treturn this.ioAction;\n\t}\n\n\t/**\n\t * 获取远程主机（客户端）地址和端口\n\t * \n\t * @return 远程主机（客户端）地址和端口\n\t */\n\tpublic SocketAddress getRemoteAddress() {\n\t\treturn SocketUtil.getRemoteAddress(this.channel);\n\t}\n\n\t/**\n\t * 读取数据到Buffer\n\t * \n\t * @return this\n\t */\n\tpublic AioSession read() {\n\t\treturn read(READ_HANDLER);\n\t}\n\n\t/**\n\t * 读取数据到Buffer\n\t * \n\t * @param handler {@link CompletionHandler}\n\t * @return this\n\t */\n\tpublic AioSession read(CompletionHandler<Integer, AioSession> handler) {\n\t\tif (isOpen()) {\n\t\t\tthis.readBuffer.clear();\n\t\t\tthis.channel.read(this.readBuffer, Math.max(this.readTimeout, 0L), TimeUnit.MILLISECONDS, this, handler);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写数据到目标端，并关闭输出\n\t *\n\t * @param data 数据\n\t * @return this\n\t */\n\tpublic AioSession writeAndClose(ByteBuffer data) {\n\t\twrite(data);\n\t\treturn closeOut();\n\t}\n\n\t/**\n\t * 写数据到目标端\n\t *\n\t * @param data 数据\n\t * @return {@link Future}\n\t */\n\tpublic Future<Integer> write(ByteBuffer data) {\n\t\treturn this.channel.write(data);\n\t}\n\n\t/**\n\t * 写数据到目标端\n\t *\n\t * @param data 数据\n\t * @param handler {@link CompletionHandler}\n\t * @return this\n\t */\n\tpublic AioSession write(ByteBuffer data, CompletionHandler<Integer, AioSession> handler) {\n\t\tthis.channel.write(data, Math.max(this.writeTimeout, 0L), TimeUnit.MILLISECONDS, this, handler);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 会话是否打开状态<br>\n\t * 当Socket保持连接时会话始终打开\n\t * \n\t * @return 会话是否打开状态\n\t */\n\tpublic boolean isOpen() {\n\t\treturn (null != this.channel) && this.channel.isOpen();\n\t}\n\n\t/**\n\t * 关闭输出\n\t * \n\t * @return this\n\t */\n\tpublic AioSession closeIn() {\n\t\tif (null != this.channel) {\n\t\t\ttry {\n\t\t\t\tthis.channel.shutdownInput();\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭输出\n\t * \n\t * @return this\n\t */\n\tpublic AioSession closeOut() {\n\t\tif (null != this.channel) {\n\t\t\ttry {\n\t\t\t\tthis.channel.shutdownOutput();\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 关闭会话\n\t */\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.channel);\n\t\tthis.readBuffer = null;\n\t\tthis.writeBuffer = null;\n\t}\n\n\t/**\n\t * 执行读，用于读取事件结束的回调\n\t */\n\tprotected void callbackRead() {\n\t\treadBuffer.flip();// 读模式\n\t\tioAction.doAction(this, readBuffer);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java",
    "content": "package cn.hutool.socket.aio;\n\n/**\n * Socket流处理接口<br>\n * 实现此接口用于处理接收到的消息，发送指定消息\n * \n * @author looly\n *\n * @param <T> 经过解码器解码后的数据类型\n */\npublic interface IoAction<T> {\n\n\t/**\n\t * 接收客户端连接（会话建立）事件处理\n\t * \n\t * @param session 会话\n\t */\n\tvoid accept(AioSession session);\n\n\t/**\n\t * 执行数据处理（消息读取）\n\t * \n\t * @param session Socket Session会话\n\t * @param data 解码后的数据\n\t */\n\tvoid doAction(AioSession session, T data);\n\n\t/**\n\t * 数据读取失败的回调事件处理（消息读取失败）\n\t * \n\t * @param exc 异常\n\t * @param session Session\n\t */\n\tvoid failed(Throwable exc, AioSession session);\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java",
    "content": "package cn.hutool.socket.aio;\n\nimport java.nio.channels.CompletionHandler;\n\nimport cn.hutool.socket.SocketRuntimeException;\n\n/**\n * 数据读取完成回调，调用Session中相应方法处理消息，单例使用\n * \n * @author looly\n *\n */\npublic class ReadHandler implements CompletionHandler<Integer, AioSession> {\n\n\t@Override\n\tpublic void completed(Integer result, AioSession session) {\n\t\tsession.callbackRead();\n\t}\n\n\t@Override\n\tpublic void failed(Throwable exc, AioSession session) {\n\t\tthrow new SocketRuntimeException(exc);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java",
    "content": "package cn.hutool.socket.aio;\n\nimport java.nio.ByteBuffer;\n\nimport cn.hutool.log.StaticLog;\n\n/**\n * 简易IO信息处理类<br>\n * 简单实现了accept和failed事件\n * \n * @author looly\n *\n */\npublic abstract class SimpleIoAction implements IoAction<ByteBuffer> {\n\t\n\t@Override\n\tpublic void accept(AioSession session) {\n\t}\n\n\t@Override\n\tpublic void failed(Throwable exc, AioSession session) {\n\t\tStaticLog.error(exc);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java",
    "content": "/**\n * AIO相关封装\n * \n * @author looly\n *\n */\npackage cn.hutool.socket.aio;"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/AcceptHandler.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.log.StaticLog;\n\nimport java.io.IOException;\nimport java.nio.channels.CompletionHandler;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\n\n/**\n * 接入完成回调，单例使用\n * \n * @author looly\n */\npublic class AcceptHandler implements CompletionHandler<ServerSocketChannel, NioServer> {\n\n\t@Override\n\tpublic void completed(ServerSocketChannel serverSocketChannel, NioServer nioServer) {\n\t\tSocketChannel socketChannel;\n\t\ttry {\n\t\t\t// 获取连接到此服务器的客户端通道\n\t\t\tsocketChannel = serverSocketChannel.accept();\n\t\t\tStaticLog.debug(\"Client [{}] accepted.\", socketChannel.getRemoteAddress());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\t// SocketChannel通道的可读事件注册到Selector中\n\t\tNioUtil.registerChannel(nioServer.getSelector(), socketChannel, Operation.READ);\n\t}\n\n\t@Override\n\tpublic void failed(Throwable exc, NioServer nioServer) {\n\t\tStaticLog.error(exc);\n\t}\n\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/ChannelHandler.java",
    "content": "package cn.hutool.socket.nio;\n\nimport java.nio.channels.SocketChannel;\n\n/**\n * NIO数据处理接口，通过实现此接口，可以从{@link SocketChannel}中读写数据\n *\n */\n@FunctionalInterface\npublic interface ChannelHandler {\n\n\t/**\n\t * 处理NIO数据\n\t *\n\t * @param socketChannel {@link SocketChannel}\n\t * @throws Exception 可能的处理异常\n\t */\n\tvoid handle(SocketChannel socketChannel) throws Exception;\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.thread.ThreadUtil;\nimport cn.hutool.log.Log;\nimport cn.hutool.socket.SocketRuntimeException;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.SocketChannel;\nimport java.util.Iterator;\n\n/**\n * NIO客户端\n *\n * @author looly\n * @since 4.4.5\n */\npublic class NioClient implements Closeable {\n\n\tprivate static final Log log = Log.get();\n\n\tprivate Selector selector;\n\tprivate SocketChannel channel;\n\tprivate ChannelHandler handler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param host 服务器地址\n\t * @param port 端口\n\t */\n\tpublic NioClient(String host, int port) {\n\t\tinit(new InetSocketAddress(host, port));\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param address 服务器地址\n\t */\n\tpublic NioClient(InetSocketAddress address) {\n\t\tinit(address);\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param address 地址和端口\n\t * @return this\n\t */\n\tpublic NioClient init(InetSocketAddress address) {\n\t\ttry {\n\t\t\t//创建一个SocketChannel对象，配置成非阻塞模式\n\t\t\tthis.channel = SocketChannel.open();\n\t\t\tchannel.configureBlocking(false);\n\t\t\tchannel.connect(address);\n\n\t\t\t//创建一个选择器，并把SocketChannel交给selector对象\n\t\t\tthis.selector = Selector.open();\n\t\t\tchannel.register(this.selector, SelectionKey.OP_READ);\n\n\t\t\t// 等待建立连接\n\t\t\t//noinspection StatementWithEmptyBody\n\t\t\twhile (false == channel.finishConnect()){}\n\t\t} catch (IOException e) {\n\t\t\tclose();\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置NIO数据处理器\n\t *\n\t * @param handler {@link ChannelHandler}\n\t * @return this\n\t */\n\tpublic NioClient setChannelHandler(ChannelHandler handler){\n\t\tthis.handler = handler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 开始监听\n\t */\n\tpublic void listen() {\n\t\tThreadUtil.execute(() -> {\n\t\t\ttry {\n\t\t\t\tdoListen();\n\t\t\t} catch (IOException e) {\n\t\t\t\tlog.error(\"Listen failed\", e);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * 开始监听\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void doListen() throws IOException {\n\t\twhile (this.selector.isOpen() && 0 != this.selector.select()) {\n\t\t\t// 返回已选择键的集合\n\t\t\tfinal Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();\n\t\t\twhile (keyIter.hasNext()) {\n\t\t\t\thandle(keyIter.next());\n\t\t\t\tkeyIter.remove();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 处理SelectionKey\n\t *\n\t * @param key SelectionKey\n\t */\n\tprivate void handle(SelectionKey key) {\n\t\t// 读事件就绪\n\t\tif (key.isReadable()) {\n\t\t\tfinal SocketChannel socketChannel = (SocketChannel) key.channel();\n\t\t\ttry{\n\t\t\t\thandler.handle(socketChannel);\n\t\t\t} catch (Exception e){\n\t\t\t\tthrow new SocketRuntimeException(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 实现写逻辑<br>\n\t * 当收到写出准备就绪的信号后，回调此方法，用户可向客户端发送消息\n\t *\n\t * @param datas 发送的数据\n\t * @return this\n\t */\n\tpublic NioClient write(ByteBuffer... datas) {\n\t\ttry {\n\t\t\tthis.channel.write(datas);\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取SocketChannel\n\t *\n\t * @return SocketChannel\n\t * @since 5.3.10\n\t */\n\tpublic SocketChannel getChannel() {\n\t\treturn this.channel;\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.selector);\n\t\tIoUtil.close(this.channel);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.log.Log;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\nimport java.util.Iterator;\n\n/**\n * 基于NIO的Socket服务端实现\n *\n * @author looly\n *\n */\npublic class NioServer implements Closeable {\n\tprivate static final Log log = Log.get();\n\n\tprivate static final AcceptHandler ACCEPT_HANDLER = new AcceptHandler();\n\n\tprivate Selector selector;\n\tprivate ServerSocketChannel serverSocketChannel;\n\tprivate ChannelHandler handler;\n\n\t/**\n\t * 构造\n\t *\n\t * @param port 端口\n\t */\n\tpublic NioServer(int port) {\n\t\tinit(new InetSocketAddress(port));\n\t}\n\n\t/**\n\t * 初始化\n\t *\n\t * @param address 地址和端口\n\t * @return this\n\t */\n\tpublic NioServer init(InetSocketAddress address) {\n\t\ttry {\n\t\t\t// 打开服务器套接字通道\n\t\t\tthis.serverSocketChannel = ServerSocketChannel.open();\n\t\t\t// 设置为非阻塞状态\n\t\t\tthis.serverSocketChannel.configureBlocking(false);\n\t\t\t// 绑定端口号\n\t\t\tthis.serverSocketChannel.bind(address);\n\n\t\t\t// 打开一个选择器\n\t\t\tthis.selector = Selector.open();\n\t\t\t// 服务器套接字注册到Selector中 并指定Selector监控连接事件\n\t\t\tthis.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);\n\t\t} catch (IOException e) {\n\t\t\tclose();\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\n\t\tlog.debug(\"Server listen on: [{}]...\", address);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 设置NIO数据处理器\n\t *\n\t * @param handler {@link ChannelHandler}\n\t * @return this\n\t */\n\tpublic NioServer setChannelHandler(ChannelHandler handler){\n\t\tthis.handler = handler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取{@link Selector}\n\t *\n\t * @return {@link Selector}\n\t */\n\tpublic Selector getSelector(){\n\t\treturn this.selector;\n\t}\n\n\t/**\n\t * 启动NIO服务端，即开始监听\n\t *\n\t * @see #listen()\n\t */\n\tpublic void start(){\n\t\tlisten();\n\t}\n\n\t/**\n\t * 开始监听\n\t */\n\tpublic void listen() {\n\t\ttry {\n\t\t\tdoListen();\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 开始监听\n\t *\n\t * @throws IOException IO异常\n\t */\n\tprivate void doListen() throws IOException {\n\t\twhile (this.selector.isOpen() && 0 != this.selector.select()) {\n\t\t\t// 返回已选择键的集合\n\t\t\tfinal Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();\n\t\t\twhile (keyIter.hasNext()) {\n\t\t\t\thandle(keyIter.next());\n\t\t\t\tkeyIter.remove();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 处理SelectionKey\n\t *\n\t * @param key SelectionKey\n\t */\n\tprivate void handle(SelectionKey key) {\n\t\t// 有客户端接入此服务端\n\t\tif (key.isAcceptable()) {\n\t\t\tACCEPT_HANDLER.completed((ServerSocketChannel) key.channel(), this);\n\t\t}\n\n\t\t// 读事件就绪\n\t\tif (key.isReadable()) {\n\t\t\tfinal SocketChannel socketChannel = (SocketChannel) key.channel();\n\t\t\ttry{\n\t\t\t\thandler.handle(socketChannel);\n\t\t\t} catch (Exception e){\n\t\t\t\tIoUtil.close(socketChannel);\n\t\t\t\tlog.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic void close() {\n\t\tIoUtil.close(this.selector);\n\t\tIoUtil.close(this.serverSocketChannel);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/NioUtil.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.IORuntimeException;\n\nimport java.io.IOException;\nimport java.nio.channels.SelectableChannel;\nimport java.nio.channels.Selector;\n\n/**\n * NIO工具类\n *\n * @since 5.4.0\n */\npublic class NioUtil {\n\n\t/**\n\t * 注册通道的指定操作到指定Selector上\n\t *\n\t * @param selector Selector\n\t * @param channel 通道\n\t * @param ops 注册的通道监听（操作）类型\n\t */\n\tpublic static void registerChannel(Selector selector, SelectableChannel channel, Operation ops) {\n\t\tif (channel == null) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tchannel.configureBlocking(false);\n\t\t\t// 注册通道\n\t\t\t//noinspection MagicConstant\n\t\t\tchannel.register(selector, ops.getValue());\n\t\t} catch (IOException e) {\n\t\t\tthrow new IORuntimeException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java",
    "content": "package cn.hutool.socket.nio;\n\nimport java.nio.channels.SelectionKey;\n\n/**\n * SelectionKey Operation的枚举封装\n * \n * @author looly\n */\npublic enum Operation {\n\n\t/** 读操作 */\n\tREAD(SelectionKey.OP_READ),\n\t/** 写操作 */\n\tWRITE(SelectionKey.OP_WRITE),\n\t/** 连接操作 */\n\tCONNECT(SelectionKey.OP_CONNECT),\n\t/** 接受连接操作 */\n\tACCEPT(SelectionKey.OP_ACCEPT);\n\n\tprivate final int value;\n\n\t/**\n\t * 构造\n\t * \n\t * @param value 值\n\t * @see SelectionKey#OP_READ\n\t * @see SelectionKey#OP_WRITE\n\t * @see SelectionKey#OP_CONNECT\n\t * @see SelectionKey#OP_ACCEPT\n\t */\n\tOperation(int value) {\n\t\tthis.value = value;\n\t}\n\n\t/**\n\t * 获取值\n\t * \n\t * @return 值\n\t * @see SelectionKey#OP_READ\n\t * @see SelectionKey#OP_WRITE\n\t * @see SelectionKey#OP_CONNECT\n\t * @see SelectionKey#OP_ACCEPT\n\t */\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java",
    "content": "/**\n * NIO相关封装\n * \n * @author looly\n *\n */\npackage cn.hutool.socket.nio;"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/package-info.java",
    "content": "/**\n * Socket套接字相关工具类封装\n * \n * @author looly\n *\n */\npackage cn.hutool.socket;"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java",
    "content": "package cn.hutool.socket.protocol;\n\nimport java.nio.ByteBuffer;\n\nimport cn.hutool.socket.aio.AioSession;\n\n/**\n * 消息解码器\n * \n * @author looly\n *\n * @param <T> 解码后的目标类型\n */\npublic interface MsgDecoder<T> {\n\t/**\n\t * 对于从Socket流中获取到的数据采用当前MsgDecoder的实现类协议进行解析。\n\t *\n\t *\n\t * @param session 本次需要解码的session\n\t * @param readBuffer 待处理的读buffer\n\t * @return 本次解码成功后封装的业务消息对象, 返回null则表示解码未完成\n\t */\n\tT decode(AioSession session, ByteBuffer readBuffer);\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java",
    "content": "package cn.hutool.socket.protocol;\n\nimport java.nio.ByteBuffer;\n\nimport cn.hutool.socket.aio.AioSession;\n\n/**\n * 消息编码器\n * \n * @author looly\n *\n * @param <T> 编码前后的数据类型\n */\npublic interface MsgEncoder<T> {\n\t/**\n\t * 编码数据用于写出\n\t *\n\t * @param session 本次需要解码的session\n\t * @param writeBuffer 待处理的读buffer\n\t * @param data 写出的数据\n\t */\n\tvoid encode(AioSession session, ByteBuffer writeBuffer, T data);\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java",
    "content": "/*\n * Copyright (c) 2017, org.smartboot. All rights reserved.\n * project name: smart-socket\n * file name: Protocol.java\n * Date: 2017-11-25\n * Author: sandao\n */\n\npackage cn.hutool.socket.protocol;\n\n/**\n * 协议接口<br>\n * 通过实现此接口完成消息的编码和解码\n * \n * <p>\n * 所有Socket使用相同协议对象，类成员变量和对象成员变量易造成并发读写问题。\n * </p>\n *\n * @author Looly\n */\npublic interface Protocol<T> extends MsgEncoder<T>, MsgDecoder<T> {\n\n}\n"
  },
  {
    "path": "hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java",
    "content": "/**\n * 消息协议接口及实现\n * \n * @author looly\n *\n */\npackage cn.hutool.socket.protocol;"
  },
  {
    "path": "hutool-socket/src/test/java/cn/hutool/socket/aio/AioClientTest.java",
    "content": "package cn.hutool.socket.aio;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.thread.ThreadFactoryBuilder;\nimport cn.hutool.core.util.RuntimeUtil;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.AsynchronousChannelGroup;\n\npublic class AioClientTest {\n\tpublic static void main(String[] args) throws IOException {\n\t\tfinal AsynchronousChannelGroup GROUP = AsynchronousChannelGroup.withFixedThreadPool(//\n\t\t\t\tRuntimeUtil.getProcessorCount(), // 默认线程池大小\n\t\t\t\tThreadFactoryBuilder.create().setNamePrefix(\"Hutool-socket-\").build()//\n\t\t);\n\n\t\tAioClient client = new AioClient(new InetSocketAddress(\"localhost\", 8899), new SimpleIoAction() {\n\n\t\t\t@Override\n\t\t\tpublic void doAction(AioSession session, ByteBuffer data) {\n\t\t\t\tif(data.hasRemaining()) {\n\t\t\t\t\tConsole.log(StrUtil.utf8Str(data));\n\t\t\t\t\tsession.read();\n\t\t\t\t}\n\t\t\t\tConsole.log(\"OK\");\n\t\t\t}\n\t\t});\n\n\t\tclient.write(ByteBuffer.wrap(\"Hello\".getBytes()));\n\t\tclient.read();\n\n\t\tclient.close();\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/test/java/cn/hutool/socket/aio/AioServerTest.java",
    "content": "package cn.hutool.socket.aio;\n\nimport cn.hutool.core.date.DateUtil;\nimport cn.hutool.core.io.BufferUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.log.StaticLog;\n\nimport java.nio.ByteBuffer;\n\npublic class AioServerTest {\n\t\n\tpublic static void main(String[] args) {\n\t\t\n\t\t@SuppressWarnings(\"resource\")\n\t\tAioServer aioServer = new AioServer(8899);\n\t\taioServer.setIoAction(new SimpleIoAction() {\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void accept(AioSession session) {\n\t\t\t\tStaticLog.debug(\"【客户端】：{} 连接。\", session.getRemoteAddress());\n\t\t\t\tsession.write(BufferUtil.createUtf8(\"=== Welcome to Hutool socket server. ===\"));\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void doAction(AioSession session, ByteBuffer data) {\n\t\t\t\tConsole.log(data);\n\t\t\t\t\n\t\t\t\tif(false == data.hasRemaining()) {\n\t\t\t\t\tStringBuilder response = StrUtil.builder()//\n\t\t\t\t\t\t\t.append(\"HTTP/1.1 200 OK\\r\\n\")//\n\t\t\t\t\t\t\t.append(\"Date: \").append(DateUtil.formatHttpDate(DateUtil.date())).append(\"\\r\\n\")//\n\t\t\t\t\t\t\t.append(\"Content-Type: text/html; charset=UTF-8\\r\\n\")//\n\t\t\t\t\t\t\t.append(\"\\r\\n\")\n\t\t\t\t\t\t\t.append(\"Hello Hutool socket\");//\n\t\t\t\t\tsession.writeAndClose(BufferUtil.createUtf8(response));\n\t\t\t\t}else {\n\t\t\t\t\tsession.read();\n\t\t\t\t}\n\t\t\t}\n\t\t}).start(true);\n\t}\n}\n"
  },
  {
    "path": "hutool-socket/src/test/java/cn/hutool/socket/nio/NioClientTest.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.BufferUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.StrUtil;\nimport lombok.SneakyThrows;\n\nimport java.nio.ByteBuffer;\nimport java.util.Scanner;\n\npublic class NioClientTest {\n\n\t@SneakyThrows\n\tpublic static void main(String[] args) {\n\t\tNioClient client = new NioClient(\"127.0.0.1\", 8080);\n\t\tclient.setChannelHandler((sc)->{\n\t\t\tByteBuffer readBuffer = ByteBuffer.allocate(1024);\n\t\t\t//从channel读数据到缓冲区\n\t\t\tint readBytes = sc.read(readBuffer);\n\t\t\tif (readBytes > 0) {\n\t\t\t\t//Flips this buffer.  The limit is set to the current position and then\n\t\t\t\t// the position is set to zero，就是表示要从起始位置开始读取数据\n\t\t\t\treadBuffer.flip();\n\t\t\t\t//returns the number of elements between the current position and the  limit.\n\t\t\t\t// 要读取的字节长度\n\t\t\t\tbyte[] bytes = new byte[readBuffer.remaining()];\n\t\t\t\t//将缓冲区的数据读到bytes数组\n\t\t\t\treadBuffer.get(bytes);\n\t\t\t\tString body = StrUtil.utf8Str(bytes);\n\t\t\t\tConsole.log(\"[{}]: {}\", sc.getRemoteAddress(), body);\n\t\t\t} else if (readBytes < 0) {\n\t\t\t\tsc.close();\n\t\t\t}\n\t\t});\n\n\t\tclient.listen();\n\t\tclient.write(BufferUtil.createUtf8(\"你好。\\n\"));\n\t\tclient.write(BufferUtil.createUtf8(\"你好2。\"));\n\n\t\t// 在控制台向服务器端发送数据\n\t\tConsole.log(\"请输入发送的消息：\");\n\t\tScanner scanner = new Scanner(System.in);\n\t\twhile (scanner.hasNextLine()) {\n\t\t\tString request = scanner.nextLine();\n\t\t\tif (request != null && request.trim().length() > 0) {\n\t\t\t\tclient.write(BufferUtil.createUtf8(request));\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "hutool-socket/src/test/java/cn/hutool/socket/nio/NioServerTest.java",
    "content": "package cn.hutool.socket.nio;\n\nimport cn.hutool.core.io.BufferUtil;\nimport cn.hutool.core.io.IORuntimeException;\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\n\npublic class NioServerTest {\n\n\tpublic static void main(String[] args) {\n\t\tNioServer server = new NioServer(8080);\n\t\tserver.setChannelHandler((sc)->{\n\t\t\tByteBuffer readBuffer = ByteBuffer.allocate(1024);\n\t\t\ttry{\n\t\t\t\t// 从channel读数据到缓冲区\n\t\t\t\tint readBytes = sc.read(readBuffer);\n\t\t\t\tif (readBytes > 0) {\n\t\t\t\t\t// Flips this buffer.  The limit is set to the current position and then\n\t\t\t\t\t// the position is set to zero，就是表示要从起始位置开始读取数据\n\t\t\t\t\treadBuffer.flip();\n\t\t\t\t\t// returns the number of elements between the current position and the  limit.\n\t\t\t\t\t// 要读取的字节长度\n\t\t\t\t\tbyte[] bytes = new byte[readBuffer.remaining()];\n\t\t\t\t\t// 将缓冲区的数据读到bytes数组\n\t\t\t\t\treadBuffer.get(bytes);\n\t\t\t\t\tString body = StrUtil.utf8Str(bytes);\n\t\t\t\t\tConsole.log(\"[{}]: {}\", sc.getRemoteAddress(), body);\n\n\t\t\t\t\tdoWrite(sc, body);\n\t\t\t\t} else if (readBytes < 0) {\n\t\t\t\t\tIoUtil.close(sc);\n\t\t\t\t}\n\t\t\t} catch (IOException e){\n\t\t\t\tthrow new IORuntimeException(e);\n\t\t\t}\n\t\t});\n\t\tserver.listen();\n\t}\n\n\tpublic static void doWrite(SocketChannel channel, String response) throws IOException {\n\t\tresponse = \"收到消息：\" + response;\n\t\t// 将缓冲数据写入渠道，返回给客户端\n\t\tchannel.write(BufferUtil.createUtf8(response));\n\t}\n}\n"
  },
  {
    "path": "hutool-system/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>jar</packaging>\n\n\t<parent>\n\t\t<groupId>cn.hutool</groupId>\n\t\t<artifactId>hutool-parent</artifactId>\n\t\t<version>5.8.44</version>\n\t</parent>\n\n\t<artifactId>hutool-system</artifactId>\n\t<name>${project.artifactId}</name>\n\t<description>Hutool 系统调用（Runtime）、系统监控封装</description>\n\n\t<properties>\n\t\t<Automatic-Module-Name>cn.hutool.system</Automatic-Module-Name>\n\n\t\t<slf4j.version>1.7.36</slf4j.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-core</artifactId>\n\t\t\t<version>${project.parent.version}</version>\n\t\t</dependency>\n\t\t<!-- 跨平台的系统及硬件信息库 -->\n\t\t<dependency>\n\t\t\t<groupId>com.github.oshi</groupId>\n\t\t\t<artifactId>oshi-core</artifactId>\n\t\t\t<version>6.6.2</version>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.slf4j</groupId>\n\t\t\t<artifactId>slf4j-simple</artifactId>\n\t\t\t<version>${slf4j.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/HostInfo.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.net.NetUtil;\n\nimport java.io.Serializable;\nimport java.net.InetAddress;\n\n/**\n * 代表当前主机的信息。\n */\npublic class HostInfo implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String HOST_NAME;\n\tprivate final String HOST_ADDRESS;\n\n\tpublic HostInfo() {\n\t\tfinal InetAddress localhost = NetUtil.getLocalhost();\n\t\tif(null != localhost){\n\t\t\tHOST_NAME = localhost.getHostName();\n\t\t\tHOST_ADDRESS = localhost.getHostAddress();\n\t\t} else{\n\t\t\tHOST_NAME = null;\n\t\t\tHOST_ADDRESS = null;\n\t\t}\n\t}\n\n\t/**\n\t * 取得当前主机的名称。\n\t *\n\t * <p>\n\t * 例如：<code>\"webserver1\"</code>\n\t * </p>\n\t *\n\t * @return 主机名\n\t */\n\tpublic final String getName() {\n\t\treturn HOST_NAME;\n\t}\n\n\t/**\n\t * 取得当前主机的地址。\n\t *\n\t * <p>\n\t * 例如：<code>\"192.168.0.1\"</code>\n\t * </p>\n\t *\n\t * @return 主机地址\n\t */\n\tpublic final String getAddress() {\n\t\treturn HOST_ADDRESS;\n\t}\n\n\t/**\n\t * 将当前主机的信息转换成字符串。\n\t *\n\t * @return 主机信息的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"Host Name:    \", getName());\n\t\tSystemUtil.append(builder, \"Host Address: \", getAddress());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/JavaInfo.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.util.ArrayUtil;\nimport cn.hutool.core.util.ReUtil;\n\nimport java.io.Serializable;\n\n/**\n * 代表Java Implementation的信息。\n */\npublic class JavaInfo implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String JAVA_VERSION = SystemUtil.get(\"java.version\", false);\n\tprivate final float JAVA_VERSION_FLOAT = getJavaVersionAsFloat();\n\tprivate final int JAVA_VERSION_INT = getJavaVersionAsInt();\n\tprivate final String JAVA_VENDOR = SystemUtil.get(\"java.vendor\", false);\n\tprivate final String JAVA_VENDOR_URL = SystemUtil.get(\"java.vendor.url\", false);\n\n\tprivate final boolean IS_JAVA_1_8 = getJavaVersionMatches(\"1.8\");\n\tprivate final boolean IS_JAVA_9 = getJavaVersionMatches(\"9\");\n\tprivate final boolean IS_JAVA_10 = getJavaVersionMatches(\"10\");\n\tprivate final boolean IS_JAVA_11 = getJavaVersionMatches(\"11\");\n\tprivate final boolean IS_JAVA_12 = getJavaVersionMatches(\"12\");\n\tprivate final boolean IS_JAVA_13 = getJavaVersionMatches(\"13\");\n\tprivate final boolean IS_JAVA_14 = getJavaVersionMatches(\"14\");\n\tprivate final boolean IS_JAVA_15 = getJavaVersionMatches(\"15\");\n\tprivate final boolean IS_JAVA_16 = getJavaVersionMatches(\"16\");\n\tprivate final boolean IS_JAVA_17 = getJavaVersionMatches(\"17\");\n\tprivate final boolean IS_JAVA_18 = getJavaVersionMatches(\"18\");\n\n\n\t/**\n\t * 取得当前Java impl.的版本（取自系统属性：{@code java.version}）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：{@code \"1.4.2\"}\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t * @since Java 1.1\n\t */\n\tpublic final String getVersion() {\n\t\treturn JAVA_VERSION;\n\t}\n\n\t/**\n\t * 取得当前Java impl.的版本（取自系统属性：{@code java.version}）。\n\t *\n\t * <p>\n\t * 例如：\n\t *\n\t * <ul>\n\t * <li>JDK 1.2：{@code 1.2f}。</li>\n\t * <li>JDK 1.3.1：{@code 1.31f}</li>\n\t * </ul>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code 0}。\n\t */\n\tpublic final float getVersionFloat() {\n\t\treturn JAVA_VERSION_FLOAT;\n\t}\n\n\t/**\n\t * 取得当前Java impl.的版本（取自系统属性：{@code java.version}），java10及其之后的版本返回值为4位。\n\t *\n\t * <p>\n\t * 例如：\n\t *\n\t * <ul>\n\t * <li>JDK 1.2：{@code 120}。</li>\n\t * <li>JDK 1.3.1：{@code 131}</li>\n\t * <li>JDK 11.0.2：{@code 1102}</li>\n\t * </ul>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code 0}。\n\t * @since Java 1.1\n\t */\n\tpublic final int getVersionInt() {\n\t\treturn JAVA_VERSION_INT;\n\t}\n\n\t/**\n\t * 取得当前Java impl.的版本的{@code float}值。\n\t *\n\t * @return Java版本的<code>float</code>值或{@code 0}\n\t */\n\tprivate float getJavaVersionAsFloat() {\n\t\tif (JAVA_VERSION == null) {\n\t\t\treturn 0f;\n\t\t}\n\n\t\tString str = JAVA_VERSION;\n\n\t\tstr = ReUtil.get(\"^[0-9]{1,2}(\\\\.[0-9]{1,2})?\", str, 0);\n\n\t\treturn Float.parseFloat(str);\n\t}\n\n\t/**\n\t * 取得当前Java impl.的版本的{@code int}值。\n\t *\n\t * @return Java版本的<code>int</code>值或{@code 0}\n\t */\n\tprivate int getJavaVersionAsInt() {\n\t\tif (JAVA_VERSION == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tfinal String javaVersion = ReUtil.get(\"^[0-9]{1,2}(\\\\.[0-9]{1,2}){0,2}\", JAVA_VERSION, 0);\n\n\t\tfinal String[] split = javaVersion.split(\"\\\\.\");\n\t\tString result = ArrayUtil.join(split, \"\");\n\n\t\t//保证java10及其之后的版本返回的值为4位\n\t\tif (split[0].length() > 1) {\n\t\t\tresult = (result + \"0000\").substring(0, 4);\n\t\t}\n\n\t\treturn Integer.parseInt(result);\n\t}\n\n\t/**\n\t * 取得当前Java impl.的厂商（取自系统属性：{@code java.vendor}）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：{@code \"Sun Microsystems Inc.\"}\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t * @since Java 1.1\n\t */\n\tpublic final String getVendor() {\n\t\treturn JAVA_VENDOR;\n\t}\n\n\t/**\n\t * 取得当前Java impl.的厂商网站的URL（取自系统属性：{@code java.vendor.url}）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：{@code \"<a href=\"http://java.sun.com/\">http://java.sun.com/</a>\"}\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t * @since Java 1.1\n\t */\n\tpublic final String getVendorURL() {\n\t\treturn JAVA_VENDOR_URL;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.1，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_1() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.2，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_2() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.3，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_3() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.4，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_4() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.5，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_5() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.6，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_6() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.7，则返回{@code true}\n\t */\n\t@Deprecated\n\tpublic final boolean isJava1_7() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为1.8，则返回{@code true}\n\t */\n\tpublic final boolean isJava1_8() {\n\t\treturn IS_JAVA_1_8;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为9，则返回{@code true}\n\t */\n\tpublic final boolean isJava9() {\n\t\treturn IS_JAVA_9;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为10，则返回{@code true}\n\t */\n\tpublic final boolean isJava10() {\n\t\treturn IS_JAVA_10;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为11，则返回{@code true}\n\t */\n\tpublic final boolean isJava11() {\n\t\treturn IS_JAVA_11;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为12，则返回{@code true}\n\t */\n\tpublic final boolean isJava12() {\n\t\treturn IS_JAVA_12;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为13，则返回{@code true}\n\t */\n\tpublic final boolean isJava13() {\n\t\treturn IS_JAVA_13;\n\t}\n\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为14，则返回{@code true}\n\t */\n\tpublic final boolean isJava14() {\n\t\treturn IS_JAVA_14;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为15，则返回{@code true}\n\t */\n\tpublic final boolean isJava15() {\n\t\treturn IS_JAVA_15;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为16，则返回{@code true}\n\t */\n\tpublic final boolean isJava16() {\n\t\treturn IS_JAVA_16;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为17，则返回{@code true}\n\t */\n\tpublic final boolean isJava17() {\n\t\treturn IS_JAVA_17;\n\t}\n\n\t/**\n\t * 判断当前Java的版本。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code java.version}（因为Java安全限制），则总是返回 {@code false}\n\t *\n\t * @return 如果当前Java版本为18，则返回{@code true}\n\t */\n\tpublic final boolean isJava18() {\n\t\treturn IS_JAVA_18;\n\t}\n\n\t/**\n\t * 匹配当前Java的版本。\n\t *\n\t * @param versionPrefix Java版本前缀\n\t * @return 如果版本匹配，则返回{@code true}\n\t */\n\tprivate boolean getJavaVersionMatches(final String versionPrefix) {\n\t\tif (JAVA_VERSION == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn JAVA_VERSION.startsWith(versionPrefix);\n\t}\n\n\t/**\n\t * 判定当前Java的版本是否大于等于指定的版本号。\n\t *\n\t * <p>\n\t * 例如：\n\t *\n\t *\n\t * <ul>\n\t * <li>测试JDK 1.2：{@code isJavaVersionAtLeast(1.2f)}</li>\n\t * <li>测试JDK 1.2.1：{@code isJavaVersionAtLeast(1.31f)}</li>\n\t * </ul>\n\t *\n\t * @param requiredVersion 需要的版本\n\t * @return 如果当前Java版本大于或等于指定的版本，则返回{@code true}\n\t */\n\tpublic final boolean isJavaVersionAtLeast(final float requiredVersion) {\n\t\treturn getVersionFloat() >= requiredVersion;\n\t}\n\n\t/**\n\t * 判定当前Java的版本是否大于等于指定的版本号。\n\t *\n\t * <p>\n\t * 例如：\n\t *\n\t *\n\t * <ul>\n\t * <li>测试JDK 1.2：{@code isJavaVersionAtLeast(120)}</li>\n\t * <li>测试JDK 1.2.1：{@code isJavaVersionAtLeast(131)}</li>\n\t * </ul>\n\t *\n\t * @param requiredVersion 需要的版本\n\t * @return 如果当前Java版本大于或等于指定的版本，则返回{@code true}\n\t */\n\tpublic final boolean isJavaVersionAtLeast(final int requiredVersion) {\n\t\treturn getVersionInt() >= requiredVersion;\n\t}\n\n\t/**\n\t * 将Java Implementation的信息转换成字符串。\n\t *\n\t * @return JVM impl.的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tfinal StringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"Java Version:    \", getVersion());\n\t\tSystemUtil.append(builder, \"Java Vendor:     \", getVendor());\n\t\tSystemUtil.append(builder, \"Java Vendor URL: \", getVendorURL());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/JavaRuntimeInfo.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.Serializable;\n\n/**\n * 代表当前运行的JRE的信息。\n */\npublic class JavaRuntimeInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String JAVA_RUNTIME_NAME = SystemUtil.get(\"java.runtime.name\", false);\n\tprivate final String JAVA_RUNTIME_VERSION = SystemUtil.get(\"java.runtime.version\", false);\n\tprivate final String JAVA_HOME = SystemUtil.get(\"java.home\", false);\n\tprivate final String JAVA_EXT_DIRS = SystemUtil.get(\"java.ext.dirs\", false);\n\tprivate final String JAVA_ENDORSED_DIRS = SystemUtil.get(\"java.endorsed.dirs\", false);\n\tprivate final String JAVA_CLASS_PATH = SystemUtil.get(\"java.class.path\", false);\n\tprivate final String JAVA_CLASS_VERSION = SystemUtil.get(\"java.class.version\", false);\n\tprivate final String JAVA_LIBRARY_PATH = SystemUtil.get(\"java.library.path\", false);\n\n\tprivate final String SUN_BOOT_CLASS_PATH = SystemUtil.get(\"sun.boot.class.path\", false);\n\n\tprivate final String SUN_ARCH_DATA_MODEL = SystemUtil.get(\"sun.arch.data.model\", false);\n\n\tpublic final String getSunBoothClassPath() {\n\t\treturn SUN_BOOT_CLASS_PATH;\n\t}\n\n\t/**\n\t * JVM is 32M <code>or</code> 64M\n\t *\n\t * @return 32 <code>or</code> 64\n\t */\n\tpublic final String getSunArchDataModel() {\n\t\treturn SUN_ARCH_DATA_MODEL;\n\t}\n\n\t/**\n\t * 取得当前JRE的名称（取自系统属性：<code>java.runtime.name</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2： <code>\"Java(TM) 2 Runtime Environment, Standard Edition\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.3\n\t */\n\tpublic final String getName() {\n\t\treturn JAVA_RUNTIME_NAME;\n\t}\n\n\t/**\n\t * 取得当前JRE的版本（取自系统属性：<code>java.runtime.version</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"1.4.2-b28\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.3\n\t */\n\tpublic final String getVersion() {\n\t\treturn JAVA_RUNTIME_VERSION;\n\t}\n\n\t/**\n\t * 取得当前JRE的安装目录（取自系统属性：<code>java.home</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"/opt/jdk1.4.2/jre\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getHomeDir() {\n\t\treturn JAVA_HOME;\n\t}\n\n\t/**\n\t * 取得当前JRE的扩展目录列表（取自系统属性：<code>java.ext.dirs</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"/opt/jdk1.4.2/jre/lib/ext:...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.3\n\t */\n\tpublic final String getExtDirs() {\n\t\treturn JAVA_EXT_DIRS;\n\t}\n\n\t/**\n\t * 取得当前JRE的endorsed目录列表（取自系统属性：<code>java.endorsed.dirs</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"/opt/jdk1.4.2/jre/lib/endorsed:...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.4\n\t */\n\tpublic final String getEndorsedDirs() {\n\t\treturn JAVA_ENDORSED_DIRS;\n\t}\n\n\t/**\n\t * 取得当前JRE的系统classpath（取自系统属性：<code>java.class.path</code>）。\n\t *\n\t * <p>\n\t * 例如：<code>\"/home/admin/myclasses:/home/admin/...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getClassPath() {\n\t\treturn JAVA_CLASS_PATH;\n\t}\n\n\t/**\n\t * 取得当前JRE的系统classpath（取自系统属性：<code>java.class.path</code>）。\n\t *\n\t * <p>\n\t * 例如：<code>\"/home/admin/myclasses:/home/admin/...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String[] getClassPathArray() {\n\t\treturn StrUtil.splitToArray(getClassPath(), SystemUtil.get(\"path.separator\", false));\n\t}\n\n\t/**\n\t * 取得当前JRE的class文件格式的版本（取自系统属性：<code>java.class.version</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"48.0\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getClassVersion() {\n\t\treturn JAVA_CLASS_VERSION;\n\t}\n\n\t/**\n\t * 取得当前JRE的library搜索路径（取自系统属性：<code>java.library.path</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"/opt/jdk1.4.2/bin:...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getLibraryPath() {\n\t\treturn JAVA_LIBRARY_PATH;\n\t}\n\n\t/**\n\t * 取得当前JRE的library搜索路径（取自系统属性：<code>java.library.path</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"/opt/jdk1.4.2/bin:...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t *\n\t */\n\tpublic final String[] getLibraryPathArray() {\n\t\treturn StrUtil.splitToArray(getLibraryPath(), SystemUtil.get(\"path.separator\", false));\n\t}\n\n\t/**\n\t * 取得当前JRE的URL协议packages列表（取自系统属性：<code>java.library.path</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"sun.net.www.protocol|...\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t *\n\t */\n\tpublic final String getProtocolPackages() {\n\t\treturn SystemUtil.get(\"java.protocol.handler.pkgs\", true);\n\t}\n\n\t/**\n\t * 将当前运行的JRE信息转换成字符串。\n\t *\n\t * @return JRE信息的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"Java Runtime Name:      \", getName());\n\t\tSystemUtil.append(builder, \"Java Runtime Version:   \", getVersion());\n\t\tSystemUtil.append(builder, \"Java Home Dir:          \", getHomeDir());\n\t\tSystemUtil.append(builder, \"Java Extension Dirs:    \", getExtDirs());\n\t\tSystemUtil.append(builder, \"Java Endorsed Dirs:     \", getEndorsedDirs());\n\t\tSystemUtil.append(builder, \"Java Class Path:        \", getClassPath());\n\t\tSystemUtil.append(builder, \"Java Class Version:     \", getClassVersion());\n\t\tSystemUtil.append(builder, \"Java Library Path:      \", getLibraryPath());\n\t\tSystemUtil.append(builder, \"Java Protocol Packages: \", getProtocolPackages());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/JavaSpecInfo.java",
    "content": "package cn.hutool.system;\n\nimport java.io.Serializable;\n\n/**\n * 代表Java Specification的信息。\n */\npublic class JavaSpecInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String JAVA_SPECIFICATION_NAME = SystemUtil.get(\"java.specification.name\", false);\n\tprivate final String JAVA_SPECIFICATION_VERSION = SystemUtil.get(\"java.specification.version\", false);\n\tprivate final String JAVA_SPECIFICATION_VENDOR = SystemUtil.get(\"java.specification.vendor\", false);\n\n\t/**\n\t * 取得当前Java Spec.的名称（取自系统属性：<code>java.specification.name</code>）。\n\t * \n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Java Platform API Specification\"</code>\n\t * </p>\n\t * \n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t * \n\t */\n\tpublic final String getName() {\n\t\treturn JAVA_SPECIFICATION_NAME;\n\t}\n\n\t/**\n\t * 取得当前Java Spec.的版本（取自系统属性：<code>java.specification.version</code>）。\n\t * \n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"1.4\"</code>\n\t * </p>\n\t * \n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t * \n\t * @since Java 1.3\n\t */\n\tpublic final String getVersion() {\n\t\treturn JAVA_SPECIFICATION_VERSION;\n\t}\n\n\t/**\n\t * 取得当前Java Spec.的厂商（取自系统属性：<code>java.specification.vendor</code>）。\n\t * \n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Sun Microsystems Inc.\"</code>\n\t * </p>\n\t * \n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t * \n\t */\n\tpublic final String getVendor() {\n\t\treturn JAVA_SPECIFICATION_VENDOR;\n\t}\n\n\t/**\n\t * 将Java Specification的信息转换成字符串。\n\t * \n\t * @return JVM spec.的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"Java Spec. Name:    \", getName());\n\t\tSystemUtil.append(builder, \"Java Spec. Version: \", getVersion());\n\t\tSystemUtil.append(builder, \"Java Spec. Vendor:  \", getVendor());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/JvmInfo.java",
    "content": "package cn.hutool.system;\n\nimport java.io.Serializable;\n\n/**\n * 代表Java Virtual Machine Implementation的信息。\n */\npublic class JvmInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String JAVA_VM_NAME = SystemUtil.get(\"java.vm.name\", false);\n\tprivate final String JAVA_VM_VERSION = SystemUtil.get(\"java.vm.version\", false);\n\tprivate final String JAVA_VM_VENDOR = SystemUtil.get(\"java.vm.vendor\", false);\n\tprivate final String JAVA_VM_INFO = SystemUtil.get(\"java.vm.info\", false);\n\n\t/**\n\t * 取得当前JVM impl.的名称（取自系统属性：<code>java.vm.name</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Java HotSpot(TM) Client VM\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getName() {\n\t\treturn JAVA_VM_NAME;\n\t}\n\n\t/**\n\t * 取得当前JVM impl.的版本（取自系统属性：<code>java.vm.version</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"1.4.2-b28\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getVersion() {\n\t\treturn JAVA_VM_VERSION;\n\t}\n\n\t/**\n\t * 取得当前JVM impl.的厂商（取自系统属性：<code>java.vm.vendor</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Sun Microsystems Inc.\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getVendor() {\n\t\treturn JAVA_VM_VENDOR;\n\t}\n\n\t/**\n\t * 取得当前JVM impl.的信息（取自系统属性：<code>java.vm.info</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"mixed mode\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getInfo() {\n\t\treturn JAVA_VM_INFO;\n\t}\n\n\t/**\n\t * 将Java Virtual Machine Implementation的信息转换成字符串。\n\t *\n\t * @return JVM impl.的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"JavaVM Name:    \", getName());\n\t\tSystemUtil.append(builder, \"JavaVM Version: \", getVersion());\n\t\tSystemUtil.append(builder, \"JavaVM Vendor:  \", getVendor());\n\t\tSystemUtil.append(builder, \"JavaVM Info:    \", getInfo());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/JvmSpecInfo.java",
    "content": "package cn.hutool.system;\n\nimport java.io.Serializable;\n\n/**\n * 代表Java Virtual Machine Specification的信息。\n */\npublic class JvmSpecInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String JAVA_VM_SPECIFICATION_NAME = SystemUtil.get(\"java.vm.specification.name\", false);\n\tprivate final String JAVA_VM_SPECIFICATION_VERSION = SystemUtil.get(\"java.vm.specification.version\", false);\n\tprivate final String JAVA_VM_SPECIFICATION_VENDOR = SystemUtil.get(\"java.vm.specification.vendor\", false);\n\n\t/**\n\t * 取得当前JVM spec.的名称（取自系统属性：<code>java.vm.specification.name</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Java Virtual Machine Specification\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getName() {\n\t\treturn JAVA_VM_SPECIFICATION_NAME;\n\t}\n\n\t/**\n\t * 取得当前JVM spec.的版本（取自系统属性：<code>java.vm.specification.version</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"1.0\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getVersion() {\n\t\treturn JAVA_VM_SPECIFICATION_VERSION;\n\t}\n\n\t/**\n\t * 取得当前JVM spec.的厂商（取自系统属性：<code>java.vm.specification.vendor</code>）。\n\t *\n\t * <p>\n\t * 例如Sun JDK 1.4.2：<code>\"Sun Microsystems Inc.\"</code>\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回<code>null</code>。\n\t *\n\t */\n\tpublic final String getVendor() {\n\t\treturn JAVA_VM_SPECIFICATION_VENDOR;\n\t}\n\n\t/**\n\t * 将Java Virtual Machine Specification的信息转换成字符串。\n\t *\n\t * @return JVM spec.的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"JavaVM Spec. Name:    \", getName());\n\t\tSystemUtil.append(builder, \"JavaVM Spec. Version: \", getVersion());\n\t\tSystemUtil.append(builder, \"JavaVM Spec. Vendor:  \", getVendor());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/OsInfo.java",
    "content": "package cn.hutool.system;\n\nimport java.io.Serializable;\n\n/**\n * 代表当前OS的信息。\n */\npublic class OsInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String OS_VERSION = SystemUtil.get(\"os.version\", false);\n\tprivate final String OS_ARCH = SystemUtil.get(\"os.arch\", false);\n\tprivate final String OS_NAME = SystemUtil.get(\"os.name\", false);\n\tprivate final boolean IS_OS_AIX = getOSMatches(\"AIX\");\n\tprivate final boolean IS_OS_HP_UX = getOSMatches(\"HP-UX\");\n\tprivate final boolean IS_OS_IRIX = getOSMatches(\"Irix\");\n\tprivate final boolean IS_OS_LINUX = getOSMatches(\"Linux\") || getOSMatches(\"LINUX\");\n\tprivate final boolean IS_OS_MAC = getOSMatches(\"Mac\");\n\tprivate final boolean IS_OS_MAC_OSX = getOSMatches(\"Mac OS X\");\n\tprivate final boolean IS_OS_OS2 = getOSMatches(\"OS/2\");\n\tprivate final boolean IS_OS_SOLARIS = getOSMatches(\"Solaris\");\n\tprivate final boolean IS_OS_SUN_OS = getOSMatches(\"SunOS\");\n\tprivate final boolean IS_OS_WINDOWS = getOSMatches(\"Windows\");\n\tprivate final boolean IS_OS_WINDOWS_2000 = getOSMatches(\"Windows\", \"5.0\");\n\tprivate final boolean IS_OS_WINDOWS_95 = getOSMatches(\"Windows 9\", \"4.0\");\n\tprivate final boolean IS_OS_WINDOWS_98 = getOSMatches(\"Windows 9\", \"4.1\");\n\tprivate final boolean IS_OS_WINDOWS_ME = getOSMatches(\"Windows\", \"4.9\");\n\tprivate final boolean IS_OS_WINDOWS_NT = getOSMatches(\"Windows NT\");\n\tprivate final boolean IS_OS_WINDOWS_XP = getOSMatches(\"Windows\", \"5.1\");\n\n\tprivate final boolean IS_OS_WINDOWS_7 = getOSMatches(\"Windows\", \"6.1\");\n\tprivate final boolean IS_OS_WINDOWS_8 = getOSMatches(\"Windows\", \"6.2\");\n\tprivate final boolean IS_OS_WINDOWS_8_1 = getOSMatches(\"Windows\", \"6.3\");\n\tprivate final boolean IS_OS_WINDOWS_10 = getOSMatches(\"Windows\", \"10.0\");\n\tprivate final boolean IS_OS_WINDOWS_11 = getOSMatches(\"Windows 11\");\n\n\t// 由于改变file.encoding属性并不会改变系统字符编码，为了保持一致，通过LocaleUtil取系统默认编码。\n\tprivate final String FILE_SEPARATOR = SystemUtil.get(\"file.separator\", false);\n\tprivate final String LINE_SEPARATOR = SystemUtil.get(\"line.separator\", false);\n\tprivate final String PATH_SEPARATOR = SystemUtil.get(\"path.separator\", false);\n\n\t/**\n\t * 取得当前OS的架构（取自系统属性：{@code os.arch}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"x86\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getArch() {\n\t\treturn OS_ARCH;\n\t}\n\n\t/**\n\t * 取得当前OS的名称（取自系统属性：{@code os.name}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"Windows XP\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getName() {\n\t\treturn OS_NAME;\n\t}\n\n\t/**\n\t * 取得当前OS的版本（取自系统属性：{@code os.version}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"5.1\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getVersion() {\n\t\treturn OS_VERSION;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为AIX，则返回{@code true}\n\t */\n\tpublic final boolean isAix() {\n\t\treturn IS_OS_AIX;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为HP-UX，则返回{@code true}\n\t */\n\tpublic final boolean isHpUx() {\n\t\treturn IS_OS_HP_UX;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为IRIX，则返回{@code true}\n\t */\n\tpublic final boolean isIrix() {\n\t\treturn IS_OS_IRIX;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Linux，则返回{@code true}\n\t */\n\tpublic final boolean isLinux() {\n\t\treturn IS_OS_LINUX;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Mac，则返回{@code true}\n\t */\n\tpublic final boolean isMac() {\n\t\treturn IS_OS_MAC;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为MacOS X，则返回{@code true}\n\t */\n\tpublic final boolean isMacOsX() {\n\t\treturn IS_OS_MAC_OSX;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为OS2，则返回{@code true}\n\t */\n\tpublic final boolean isOs2() {\n\t\treturn IS_OS_OS2;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Solaris，则返回{@code true}\n\t */\n\tpublic final boolean isSolaris() {\n\t\treturn IS_OS_SOLARIS;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Sun OS，则返回{@code true}\n\t */\n\tpublic final boolean isSunOS() {\n\t\treturn IS_OS_SUN_OS;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows，则返回{@code true}\n\t */\n\tpublic final boolean isWindows() {\n\t\treturn IS_OS_WINDOWS;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 2000，则返回{@code true}\n\t */\n\tpublic final boolean isWindows2000() {\n\t\treturn IS_OS_WINDOWS_2000;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 95，则返回{@code true}\n\t */\n\tpublic final boolean isWindows95() {\n\t\treturn IS_OS_WINDOWS_95;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 98，则返回{@code true}\n\t */\n\tpublic final boolean isWindows98() {\n\t\treturn IS_OS_WINDOWS_98;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows ME，则返回{@code true}\n\t */\n\tpublic final boolean isWindowsME() {\n\t\treturn IS_OS_WINDOWS_ME;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows NT，则返回{@code true}\n\t */\n\tpublic final boolean isWindowsNT() {\n\t\treturn IS_OS_WINDOWS_NT;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows XP，则返回{@code true}\n\t */\n\tpublic final boolean isWindowsXP() {\n\t\treturn IS_OS_WINDOWS_XP;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 7，则返回{@code true}\n\t */\n\tpublic final boolean isWindows7() {\n\t\treturn IS_OS_WINDOWS_7;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 8，则返回{@code true}\n\t */\n\tpublic final boolean isWindows8() {\n\t\treturn IS_OS_WINDOWS_8;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 8.1，则返回{@code true}\n\t */\n\tpublic final boolean isWindows8_1() {\n\t\treturn IS_OS_WINDOWS_8_1;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 10，则返回{@code true}\n\t */\n\tpublic final boolean isWindows10() {\n\t\treturn IS_OS_WINDOWS_10 && !IS_OS_WINDOWS_11;\n\t}\n\n\t/**\n\t * 判断当前OS的类型。\n\t *\n\t * <p>\n\t * 如果不能取得系统属性{@code os.name}（因为Java安全限制），则总是返回{@code false}\n\t * </p>\n\t *\n\t * @return 如果当前OS类型为Windows 11，则返回{@code true}\n\t * @since 5.8.41\n\t */\n\tpublic final boolean isWindows11() {\n\t\treturn IS_OS_WINDOWS_11;\n\t}\n\n\t/**\n\t * 匹配OS名称。\n\t *\n\t * @param osNamePrefix OS名称前缀\n\t *\n\t * @return 如果匹配，则返回{@code true}\n\t */\n\tprivate boolean getOSMatches(String osNamePrefix) {\n\t\tif (OS_NAME == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn OS_NAME.startsWith(osNamePrefix);\n\t}\n\n\t/**\n\t * 匹配OS名称。\n\t *\n\t * @param osNamePrefix OS名称前缀\n\t * @param osVersionPrefix OS版本前缀\n\t *\n\t * @return 如果匹配，则返回{@code true}\n\t */\n\tprivate boolean getOSMatches(String osNamePrefix, String osVersionPrefix) {\n\t\tif ((OS_NAME == null) || (OS_VERSION == null)) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn OS_NAME.startsWith(osNamePrefix) && OS_VERSION.startsWith(osVersionPrefix);\n\t}\n\n\t/**\n\t * 取得OS的文件路径的分隔符（取自系统属性：{@code file.separator}）。\n\t *\n\t * <p>\n\t * 例如：Unix为{@code \"/\"}，Windows为{@code \"\\\\\"}。\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getFileSeparator() {\n\t\treturn FILE_SEPARATOR;\n\t}\n\n\t/**\n\t * 取得OS的文本文件换行符（取自系统属性：{@code line.separator}）。\n\t *\n\t * <p>\n\t * 例如：Unix为{@code \"\\n\"}，Windows为{@code \"\\r\\n\"}。\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getLineSeparator() {\n\t\treturn LINE_SEPARATOR;\n\t}\n\n\t/**\n\t * 取得OS的搜索路径分隔符（取自系统属性：{@code path.separator}）。\n\t *\n\t * <p>\n\t * 例如：Unix为{@code \":\"}，Windows为{@code \";\"}。\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getPathSeparator() {\n\t\treturn PATH_SEPARATOR;\n\t}\n\n\t/**\n\t * 将OS的信息转换成字符串。\n\t *\n\t * @return OS的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"OS Arch:        \", getArch());\n\t\tSystemUtil.append(builder, \"OS Name:        \", getName());\n\t\tSystemUtil.append(builder, \"OS Version:     \", getVersion());\n\t\tSystemUtil.append(builder, \"File Separator: \", getFileSeparator());\n\t\tSystemUtil.append(builder, \"Line Separator: \", getLineSeparator());\n\t\tSystemUtil.append(builder, \"Path Separator: \", getPathSeparator());\n\n\t\treturn builder.toString();\n\t}\n\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/RuntimeInfo.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.io.FileUtil;\n\nimport java.io.Serializable;\n\n/**\n * 运行时信息，包括内存总大小、已用大小、可用大小等\n *\n * @author looly\n */\npublic class RuntimeInfo implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final Runtime currentRuntime = Runtime.getRuntime();\n\n\t/**\n\t * 获得运行时对象\n\t *\n\t * @return {@link Runtime}\n\t */\n\tpublic final Runtime getRuntime() {\n\t\treturn currentRuntime;\n\t}\n\n\t/**\n\t * 获得JVM最大内存\n\t *\n\t * @return 最大内存\n\t */\n\tpublic final long getMaxMemory() {\n\t\treturn currentRuntime.maxMemory();\n\t}\n\n\t/**\n\t * 获得JVM已分配内存\n\t *\n\t * @return 已分配内存\n\t */\n\tpublic final long getTotalMemory() {\n\t\treturn currentRuntime.totalMemory();\n\t}\n\n\t/**\n\t * 获得JVM已分配内存中的剩余空间\n\t *\n\t * @return 已分配内存中的剩余空间\n\t */\n\tpublic final long getFreeMemory() {\n\t\treturn currentRuntime.freeMemory();\n\t}\n\n\t/**\n\t * 获得JVM最大可用内存\n\t *\n\t * @return 最大可用内存\n\t */\n\tpublic final long getUsableMemory() {\n\t\treturn currentRuntime.maxMemory() - currentRuntime.totalMemory() + currentRuntime.freeMemory();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"Max Memory:    \", FileUtil.readableFileSize(getMaxMemory()));\n\t\tSystemUtil.append(builder, \"Total Memory:     \", FileUtil.readableFileSize(getTotalMemory()));\n\t\tSystemUtil.append(builder, \"Free Memory:     \", FileUtil.readableFileSize(getFreeMemory()));\n\t\tSystemUtil.append(builder, \"Usable Memory:     \", FileUtil.readableFileSize(getUsableMemory()));\n\n\t\treturn builder.toString();\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/SystemPropsKeys.java",
    "content": "package cn.hutool.system;\n\n/**\n * 系统属性名称常量池\n *\n * <p>\n * 封装了包括Java运行时环境信息、Java虚拟机信息、Java类信息、OS信息、用户信息等<br>\n *\n *\n * @author Looly\n * @since 4.6.7\n */\npublic interface SystemPropsKeys {\n\n\t// ----- Java运行时环境信息 -----/\n\t/**\n\t * Java 运行时环境规范名称\n\t */\n\tString SPECIFICATION_NAME = \"java.specification.name\";\n\t/**\n\t * Java 运行时环境版本\n\t */\n\tString VERSION = \"java.version\";\n\t/**\n\t * Java 运行时环境规范版本\n\t */\n\tString SPECIFICATION_VERSION = \"java.specification.version\";\n\t/**\n\t * Java 运行时环境供应商\n\t */\n\tString VENDOR = \"java.vendor\";\n\t/**\n\t * Java 运行时环境规范供应商\n\t */\n\tString SPECIFICATION_VENDOR = \"java.specification.vendor\";\n\t/**\n\t * Java 供应商的 URL\n\t */\n\tString VENDOR_URL = \"java.vendor.url\";\n\t/**\n\t * Java 安装目录\n\t */\n\tString HOME = \"java.home\";\n\t/**\n\t * 加载库时搜索的路径列表\n\t */\n\tString LIBRARY_PATH = \"java.library.path\";\n\t/**\n\t * 默认的临时文件路径\n\t */\n\tString TMPDIR = \"java.io.tmpdir\";\n\t/**\n\t * 要使用的 JIT 编译器的名称\n\t */\n\tString COMPILER = \"java.compiler\";\n\t/**\n\t * 一个或多个扩展目录的路径\n\t */\n\tString EXT_DIRS = \"java.ext.dirs\";\n\n\t// ----- Java虚拟机信息 -----/\n\t/**\n\t * Java 虚拟机实现名称\n\t */\n\tString VM_NAME = \"java.vm.name\";\n\t/**\n\t * Java 虚拟机规范名称\n\t */\n\tString VM_SPECIFICATION_NAME = \"java.vm.specification.name\";\n\t/**\n\t * Java 虚拟机实现版本\n\t */\n\tString VM_VERSION = \"java.vm.version\";\n\t/**\n\t * Java 虚拟机规范版本\n\t */\n\tString VM_SPECIFICATION_VERSION = \"java.vm.specification.version\";\n\t/**\n\t * Java 虚拟机实现供应商\n\t */\n\tString VM_VENDOR = \"java.vm.vendor\";\n\t/**\n\t * Java 虚拟机规范供应商\n\t */\n\tString VM_SPECIFICATION_VENDOR = \"java.vm.specification.vendor\";\n\n\t// ----- Java类信息 -----/\n\t/**\n\t * Java 类格式版本号\n\t */\n\tString CLASS_VERSION = \"java.class.version\";\n\t/**\n\t * Java 类路径\n\t */\n\tString CLASS_PATH = \"java.class.path\";\n\n\t// ----- OS信息 -----/\n\t/**\n\t * 操作系统的名称\n\t */\n\tString OS_NAME = \"os.name\";\n\t/**\n\t * 操作系统的架构\n\t */\n\tString OS_ARCH = \"os.arch\";\n\t/**\n\t * 操作系统的版本\n\t */\n\tString OS_VERSION = \"os.version\";\n\t/**\n\t * 文件分隔符（在 UNIX 系统中是“/”）\n\t */\n\tString FILE_SEPARATOR = \"file.separator\";\n\t/**\n\t * 路径分隔符（在 UNIX 系统中是“:”）\n\t */\n\tString PATH_SEPARATOR = \"path.separator\";\n\t/**\n\t * 行分隔符（在 UNIX 系统中是“\\n”）\n\t */\n\tString LINE_SEPARATOR = \"line.separator\";\n\n\t// ----- 用户信息 -----/\n\t/**\n\t * 用户的账户名称\n\t */\n\tString USER_NAME = \"user.name\";\n\t/**\n\t * 用户的主目录\n\t */\n\tString USER_HOME = \"user.home\";\n\t/**\n\t * 用户的当前工作目录\n\t */\n\tString USER_DIR = \"user.dir\";\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/SystemUtil.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.convert.Convert;\nimport cn.hutool.core.lang.Singleton;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.core.util.SystemPropsUtil;\n\nimport java.io.PrintWriter;\nimport java.lang.management.ClassLoadingMXBean;\nimport java.lang.management.CompilationMXBean;\nimport java.lang.management.GarbageCollectorMXBean;\nimport java.lang.management.ManagementFactory;\nimport java.lang.management.MemoryMXBean;\nimport java.lang.management.MemoryManagerMXBean;\nimport java.lang.management.MemoryPoolMXBean;\nimport java.lang.management.OperatingSystemMXBean;\nimport java.lang.management.RuntimeMXBean;\nimport java.lang.management.ThreadMXBean;\nimport java.util.List;\n\n/**\n * Java的System类封装工具类。<br>\n * 参考：http://blog.csdn.net/zhongweijian/article/details/7619383\n *\n * @author Looly\n */\npublic class SystemUtil extends SystemPropsUtil {\n\n\t// ----- Java运行时环境信息 -----/\n\t/**\n\t * Java 运行时环境规范名称的KEY\n\t */\n\tpublic final static String SPECIFICATION_NAME = SystemPropsKeys.SPECIFICATION_NAME;\n\t/**\n\t * Java 运行时环境版本的KEY\n\t */\n\tpublic final static String VERSION = SystemPropsKeys.VERSION;\n\t/**\n\t * Java 运行时环境规范版本的KEY\n\t */\n\tpublic final static String SPECIFICATION_VERSION = SystemPropsKeys.SPECIFICATION_VERSION;\n\t/**\n\t * Java 运行时环境供应商的KEY\n\t */\n\tpublic final static String VENDOR = SystemPropsKeys.VENDOR;\n\t/**\n\t * Java 运行时环境规范供应商的KEY\n\t */\n\tpublic final static String SPECIFICATION_VENDOR = SystemPropsKeys.SPECIFICATION_VENDOR;\n\t/**\n\t * Java 供应商的 URL的KEY\n\t */\n\tpublic final static String VENDOR_URL = SystemPropsKeys.VENDOR_URL;\n\t/**\n\t * Java 安装目录的KEY\n\t */\n\tpublic final static String HOME = SystemPropsKeys.HOME;\n\t/**\n\t * 加载库时搜索的路径列表的KEY\n\t */\n\tpublic final static String LIBRARY_PATH = SystemPropsKeys.LIBRARY_PATH;\n\t/**\n\t * 默认的临时文件路径的KEY\n\t */\n\tpublic final static String TMPDIR = SystemPropsKeys.TMPDIR;\n\t/**\n\t * 要使用的 JIT 编译器的名称的KEY\n\t */\n\tpublic final static String COMPILER = SystemPropsKeys.COMPILER;\n\t/**\n\t * 一个或多个扩展目录的路径的KEY\n\t */\n\tpublic final static String EXT_DIRS = SystemPropsKeys.EXT_DIRS;\n\n\t// ----- Java虚拟机信息 -----/\n\t/**\n\t * Java 虚拟机实现名称的KEY\n\t */\n\tpublic final static String VM_NAME = SystemPropsKeys.VM_NAME;\n\t/**\n\t * Java 虚拟机规范名称的KEY\n\t */\n\tpublic final static String VM_SPECIFICATION_NAME = SystemPropsKeys.VM_SPECIFICATION_NAME;\n\t/**\n\t * Java 虚拟机实现版本的KEY\n\t */\n\tpublic final static String VM_VERSION = SystemPropsKeys.VM_VERSION;\n\t/**\n\t * Java 虚拟机规范版本的KEY\n\t */\n\tpublic final static String VM_SPECIFICATION_VERSION = SystemPropsKeys.VM_SPECIFICATION_VERSION;\n\t/**\n\t * Java 虚拟机实现供应商的KEY\n\t */\n\tpublic final static String VM_VENDOR = SystemPropsKeys.VM_VENDOR;\n\t/**\n\t * Java 虚拟机规范供应商的KEY\n\t */\n\tpublic final static String VM_SPECIFICATION_VENDOR = SystemPropsKeys.VM_SPECIFICATION_VENDOR;\n\n\t// ----- Java类信息 -----/\n\t/**\n\t * Java 类格式版本号的KEY\n\t */\n\tpublic final static String CLASS_VERSION = SystemPropsKeys.CLASS_VERSION;\n\t/**\n\t * Java 类路径的KEY\n\t */\n\tpublic final static String CLASS_PATH = SystemPropsKeys.CLASS_PATH;\n\n\t// ----- OS信息 -----/\n\t/**\n\t * 操作系统的名称的KEY\n\t */\n\tpublic final static String OS_NAME = SystemPropsKeys.OS_NAME;\n\t/**\n\t * 操作系统的架构的KEY\n\t */\n\tpublic final static String OS_ARCH = SystemPropsKeys.OS_ARCH;\n\t/**\n\t * 操作系统的版本的KEY\n\t */\n\tpublic final static String OS_VERSION = SystemPropsKeys.OS_VERSION;\n\t/**\n\t * 文件分隔符（在 UNIX 系统中是“/”）的KEY\n\t */\n\tpublic final static String FILE_SEPARATOR = SystemPropsKeys.FILE_SEPARATOR;\n\t/**\n\t * 路径分隔符（在 UNIX 系统中是“:”）的KEY\n\t */\n\tpublic final static String PATH_SEPARATOR = SystemPropsKeys.PATH_SEPARATOR;\n\t/**\n\t * 行分隔符（在 UNIX 系统中是“\\n”）的KEY\n\t */\n\tpublic final static String LINE_SEPARATOR = SystemPropsKeys.LINE_SEPARATOR;\n\n\t// ----- 用户信息 -----/\n\t/**\n\t * 用户的账户名称的KEY\n\t */\n\tpublic final static String USER_NAME = SystemPropsKeys.USER_NAME;\n\t/**\n\t * 用户的主目录的KEY\n\t */\n\tpublic final static String USER_HOME = SystemPropsKeys.USER_HOME;\n\t/**\n\t * 用户的当前工作目录的KEY\n\t */\n\tpublic final static String USER_DIR = SystemPropsKeys.USER_DIR;\n\n\t/**\n\t * 获取当前进程 PID\n\t *\n\t * @return 当前进程 ID\n\t */\n\tpublic static long getCurrentPID() {\n\t\treturn Long.parseLong(getRuntimeMXBean().getName().split(\"@\")[0]);\n\t}\n\n\t/**\n\t * 返回Java虚拟机类加载系统相关属性\n\t *\n\t * @return {@link ClassLoadingMXBean}\n\t * @since 4.1.4\n\t */\n\tpublic static ClassLoadingMXBean getClassLoadingMXBean() {\n\t\treturn ManagementFactory.getClassLoadingMXBean();\n\t}\n\n\t/**\n\t * 返回Java虚拟机内存系统相关属性\n\t *\n\t * @return {@link MemoryMXBean}\n\t * @since 4.1.4\n\t */\n\tpublic static MemoryMXBean getMemoryMXBean() {\n\t\treturn ManagementFactory.getMemoryMXBean();\n\t}\n\n\t/**\n\t * 返回Java虚拟机线程系统相关属性\n\t *\n\t * @return {@link ThreadMXBean}\n\t * @since 4.1.4\n\t */\n\tpublic static ThreadMXBean getThreadMXBean() {\n\t\treturn ManagementFactory.getThreadMXBean();\n\t}\n\n\t/**\n\t * 返回Java虚拟机运行时系统相关属性\n\t *\n\t * @return {@link RuntimeMXBean}\n\t * @since 4.1.4\n\t */\n\tpublic static RuntimeMXBean getRuntimeMXBean() {\n\t\treturn ManagementFactory.getRuntimeMXBean();\n\t}\n\n\t/**\n\t * 返回Java虚拟机编译系统相关属性<br>\n\t * 如果没有编译系统，则返回{@code null}\n\t *\n\t * @return  {@link CompilationMXBean} ，如果没有编译系统，则返回{@code null}\n\t * @since 4.1.4\n\t */\n\tpublic static CompilationMXBean getCompilationMXBean() {\n\t\treturn ManagementFactory.getCompilationMXBean();\n\t}\n\n\t/**\n\t * 返回Java虚拟机运行下的操作系统相关信息属性\n\t *\n\t * @return {@link OperatingSystemMXBean}\n\t * @since 4.1.4\n\t */\n\tpublic static OperatingSystemMXBean getOperatingSystemMXBean() {\n\t\treturn ManagementFactory.getOperatingSystemMXBean();\n\t}\n\n\t/**\n\t * 获取Java虚拟机中的{@link MemoryPoolMXBean}列表<br>\n\t * The Java virtual machine can have one or more memory pools. It may add or remove memory pools during execution.\n\t *\n\t * @return a list of <tt>MemoryPoolMXBean</tt> objects.\n\t */\n\tpublic static List<MemoryPoolMXBean> getMemoryPoolMXBeans() {\n\t\treturn ManagementFactory.getMemoryPoolMXBeans();\n\t}\n\n\t/**\n\t * 获取Java虚拟机中的{@link MemoryManagerMXBean}列表<br>\n\t * The Java virtual machine can have one or more memory managers. It may add or remove memory managers during execution.\n\t *\n\t * @return a list of <tt>MemoryManagerMXBean</tt> objects.\n\t */\n\tpublic static List<MemoryManagerMXBean> getMemoryManagerMXBeans() {\n\t\treturn ManagementFactory.getMemoryManagerMXBeans();\n\t}\n\n\t/**\n\t * 获取Java虚拟机中的{@link GarbageCollectorMXBean}列表\n\t *\n\t * @return {@link GarbageCollectorMXBean}列表\n\t */\n\tpublic static List<GarbageCollectorMXBean> getGarbageCollectorMXBeans() {\n\t\treturn ManagementFactory.getGarbageCollectorMXBeans();\n\t}\n\n\t/**\n\t * 取得Java Virtual Machine Specification的信息。\n\t *\n\t * @return {@link JvmSpecInfo}对象\n\t */\n\tpublic static JvmSpecInfo getJvmSpecInfo() {\n\t\treturn Singleton.get(JvmSpecInfo.class);\n\t}\n\n\t/**\n\t * 取得Java Virtual Machine Implementation的信息。\n\t *\n\t * @return {@link JvmInfo}对象\n\t */\n\tpublic static JvmInfo getJvmInfo() {\n\t\treturn Singleton.get(JvmInfo.class);\n\t}\n\n\t/**\n\t * 取得Java Specification的信息。\n\t *\n\t * @return {@link JavaSpecInfo}对象\n\t */\n\tpublic static JavaSpecInfo getJavaSpecInfo() {\n\t\treturn Singleton.get(JavaSpecInfo.class);\n\t}\n\n\t/**\n\t * 取得Java Implementation的信息。\n\t *\n\t * @return {@link JavaInfo}对象\n\t */\n\tpublic static JavaInfo getJavaInfo() {\n\t\treturn Singleton.get(JavaInfo.class);\n\t}\n\n\t/**\n\t * 取得当前运行的JRE的信息。\n\t *\n\t * @return {@link JavaRuntimeInfo}对象\n\t */\n\tpublic static JavaRuntimeInfo getJavaRuntimeInfo() {\n\t\treturn Singleton.get(JavaRuntimeInfo.class);\n\t}\n\n\t/**\n\t * 取得OS的信息。\n\t *\n\t * @return {@code OsInfo}对象\n\t */\n\tpublic static OsInfo getOsInfo() {\n\t\treturn Singleton.get(OsInfo.class);\n\t}\n\n\t/**\n\t * 取得User的信息。\n\t *\n\t * @return {@code UserInfo}对象\n\t */\n\tpublic static UserInfo getUserInfo() {\n\t\treturn Singleton.get(UserInfo.class);\n\t}\n\n\t/**\n\t * 取得Host的信息。\n\t *\n\t * @return {@link HostInfo}对象\n\t */\n\tpublic static HostInfo getHostInfo() {\n\t\treturn Singleton.get(HostInfo.class);\n\t}\n\n\t/**\n\t * 取得Runtime的信息。\n\t *\n\t * @return {@link RuntimeInfo}对象\n\t */\n\tpublic static RuntimeInfo getRuntimeInfo() {\n\t\treturn Singleton.get(RuntimeInfo.class);\n\t}\n\n\t/**\n\t * 获取JVM中内存总大小\n\t *\n\t * @return 内存总大小\n\t * @since 4.5.4\n\t */\n\tpublic static long getTotalMemory() {\n\t\treturn Runtime.getRuntime().totalMemory();\n\t}\n\n\t/**\n\t * 获取JVM中内存剩余大小\n\t *\n\t * @return 内存剩余大小\n\t * @since 4.5.4\n\t */\n\tpublic static long getFreeMemory() {\n\t\treturn Runtime.getRuntime().freeMemory();\n\t}\n\n\t/**\n\t * 获取JVM可用的内存总大小\n\t *\n\t * @return JVM可用的内存总大小\n\t * @since 4.5.4\n\t */\n\tpublic static long getMaxMemory() {\n\t\treturn Runtime.getRuntime().maxMemory();\n\t}\n\n\t/**\n\t * 获取总线程数\n\t *\n\t * @return 总线程数\n\t */\n\tpublic static int getTotalThreadCount() {\n\t\tThreadGroup parentThread = Thread.currentThread().getThreadGroup();\n\t\twhile (null != parentThread.getParent()) {\n\t\t\tparentThread = parentThread.getParent();\n\t\t}\n\t\treturn parentThread.activeCount();\n\t}\n\n\t// ------------------------------------------------------------------ Dump\n\n\t/**\n\t * 将系统信息输出到{@link System#out}中。\n\t */\n\tpublic static void dumpSystemInfo() {\n\t\tdumpSystemInfo(new PrintWriter(System.out));\n\t}\n\n\t/**\n\t * 将系统信息输出到指定{@link PrintWriter}中。\n\t *\n\t * @param out {@link PrintWriter}输出流\n\t */\n\tpublic static void dumpSystemInfo(PrintWriter out) {\n\t\tout.println(\"--------------\");\n\t\tout.println(getJvmSpecInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getJvmInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getJavaSpecInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getJavaInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getJavaRuntimeInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getOsInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getUserInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getHostInfo());\n\t\tout.println(\"--------------\");\n\t\tout.println(getRuntimeInfo());\n\t\tout.println(\"--------------\");\n\t\tout.flush();\n\t}\n\n\t/**\n\t * 输出到{@link StringBuilder}。\n\t *\n\t * @param builder {@link StringBuilder}对象\n\t * @param caption 标题\n\t * @param value   值\n\t */\n\tprotected static void append(StringBuilder builder, String caption, Object value) {\n\t\tbuilder.append(caption).append(StrUtil.nullToDefault(Convert.toStr(value), \"[n/a]\")).append(\"\\n\");\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/UserInfo.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.util.StrUtil;\n\nimport java.io.File;\nimport java.io.Serializable;\n\n/**\n * 代表当前用户的信息。\n */\npublic class UserInfo implements Serializable{\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate final String USER_NAME;\n\tprivate final String USER_HOME;\n\tprivate final String USER_DIR;\n\tprivate final String JAVA_IO_TMPDIR;\n\tprivate final String USER_LANGUAGE;\n\tprivate final String USER_COUNTRY;\n\n\tpublic UserInfo(){\n\t\tUSER_NAME = SystemUtil.get(\"user.name\", false);\n\t\tUSER_HOME = fixPath(SystemUtil.get(\"user.home\", false));\n\t\tUSER_DIR = fixPath(SystemUtil.get(\"user.dir\", false));\n\t\tJAVA_IO_TMPDIR = fixPath(SystemUtil.get(\"java.io.tmpdir\", false));\n\t\tUSER_LANGUAGE = SystemUtil.get(\"user.language\", false);\n\n\t\t// JDK1.4 {@code user.country}，JDK1.2 {@code user.region}\n\t\tString userCountry = SystemUtil.get(\"user.country\", false);\n\t\tif(null == userCountry){\n\t\t\tuserCountry = SystemUtil.get(\"user.region\", false);\n\t\t}\n\t\tUSER_COUNTRY = userCountry;\n\t}\n\n\t/**\n\t * 取得当前登录用户的名字（取自系统属性：{@code user.name}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"admin\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getName() {\n\t\treturn USER_NAME;\n\t}\n\n\t/**\n\t * 取得当前登录用户的home目录（取自系统属性：{@code user.home}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"/home/admin/\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getHomeDir() {\n\t\treturn USER_HOME;\n\t}\n\n\t/**\n\t * 取得当前目录（取自系统属性：{@code user.dir}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"/home/admin/working/\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t * @since Java 1.1\n\t */\n\tpublic final String getCurrentDir() {\n\t\treturn USER_DIR;\n\t}\n\n\t/**\n\t * 取得临时目录（取自系统属性：{@code java.io.tmpdir}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"/tmp/\"}\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t *\n\t */\n\tpublic final String getTempDir() {\n\t\treturn JAVA_IO_TMPDIR;\n\t}\n\n\t/**\n\t * 取得当前登录用户的语言设置（取自系统属性：{@code user.language}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"zh\"}、{@code \"en\"}等\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t */\n\tpublic final String getLanguage() {\n\t\treturn USER_LANGUAGE;\n\t}\n\n\t/**\n\t * 取得当前登录用户的国家或区域设置（取自系统属性：JDK1.4 {@code user.country}或JDK1.2 {@code user.region}）。\n\t *\n\t * <p>\n\t * 例如：{@code \"CN\"}、{@code \"US\"}等\n\t * </p>\n\t *\n\t * @return 属性值，如果不能取得（因为Java安全限制）或值不存在，则返回{@code null}。\n\t *\n\t */\n\tpublic final String getCountry() {\n\t\treturn USER_COUNTRY;\n\t}\n\n\t/**\n\t * 将当前用户的信息转换成字符串。\n\t *\n\t * @return 用户信息的字符串表示\n\t */\n\t@Override\n\tpublic final String toString() {\n\t\tStringBuilder builder = new StringBuilder();\n\n\t\tSystemUtil.append(builder, \"User Name:        \", getName());\n\t\tSystemUtil.append(builder, \"User Home Dir:    \", getHomeDir());\n\t\tSystemUtil.append(builder, \"User Current Dir: \", getCurrentDir());\n\t\tSystemUtil.append(builder, \"User Temp Dir:    \", getTempDir());\n\t\tSystemUtil.append(builder, \"User Language:    \", getLanguage());\n\t\tSystemUtil.append(builder, \"User Country:     \", getCountry());\n\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * 修正路径，包括：\n\t *\n\t * <ul>\n\t *     <li>1. 末尾补充 /</li>\n\t * </ul>\n\t * @param path 路径\n\t * @return 修正后的路径\n\t * @since 5.6.4\n\t */\n\tprivate static String fixPath(String path){\n\t\treturn StrUtil.addSuffixIfNot(path, File.separator);\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/oshi/CpuInfo.java",
    "content": "package cn.hutool.system.oshi;\n\nimport cn.hutool.core.util.NumberUtil;\nimport oshi.hardware.CentralProcessor;\n\nimport java.text.DecimalFormat;\n\n/**\n * <p>CPU相关信息</p>\n *\n * @author Dai Yuanchuan\n **/\npublic class CpuInfo {\n\n\tprivate static final DecimalFormat LOAD_FORMAT = new DecimalFormat(\"#.00\");\n\n\t/**\n\t * CPU核心数\n\t */\n\tprivate Integer cpuNum;\n\n\t/**\n\t * CPU总的使用率\n\t */\n\tprivate double toTal;\n\n\t/**\n\t * CPU系统使用率\n\t */\n\tprivate double sys;\n\n\t/**\n\t * CPU用户使用率\n\t */\n\tprivate double user;\n\n\t/**\n\t * CPU当前等待率\n\t */\n\tprivate double wait;\n\n\t/**\n\t * CPU当前空闲率\n\t */\n\tprivate double free;\n\n\t/**\n\t * CPU型号信息\n\t */\n\tprivate String cpuModel;\n\n\t/**\n\t * CPU型号信息\n\t */\n\tprivate CpuTicks ticks;\n\n\t/**\n\t * 空构造\n\t */\n\tpublic CpuInfo() {\n\t}\n\n\t/**\n\t * 构造，等待时间为用于计算在一定时长内的CPU负载情况，如传入1000表示最近1秒的负载情况\n\t *\n\t * @param processor   {@link CentralProcessor}\n\t * @param waitingTime 设置等待时间，单位毫秒\n\t */\n\tpublic CpuInfo(CentralProcessor processor, long waitingTime) {\n\t\tinit(processor, waitingTime);\n\t}\n\n\t/**\n\t * 构造\n\t *\n\t * @param cpuNum   CPU核心数\n\t * @param toTal    CPU总的使用率\n\t * @param sys      CPU系统使用率\n\t * @param user     CPU用户使用率\n\t * @param wait     CPU当前等待率\n\t * @param free     CPU当前空闲率\n\t * @param cpuModel CPU型号信息\n\t */\n\tpublic CpuInfo(Integer cpuNum, double toTal, double sys, double user, double wait, double free, String cpuModel) {\n\t\tthis.cpuNum = cpuNum;\n\t\tthis.toTal = toTal;\n\t\tthis.sys = sys;\n\t\tthis.user = user;\n\t\tthis.wait = wait;\n\t\tthis.free = free;\n\t\tthis.cpuModel = cpuModel;\n\t}\n\n\tpublic Integer getCpuNum() {\n\t\treturn cpuNum;\n\t}\n\n\tpublic void setCpuNum(Integer cpuNum) {\n\t\tthis.cpuNum = cpuNum;\n\t}\n\n\tpublic double getToTal() {\n\t\treturn toTal;\n\t}\n\n\tpublic void setToTal(double toTal) {\n\t\tthis.toTal = toTal;\n\t}\n\n\tpublic double getSys() {\n\t\treturn sys;\n\t}\n\n\tpublic void setSys(double sys) {\n\t\tthis.sys = sys;\n\t}\n\n\tpublic double getUser() {\n\t\treturn user;\n\t}\n\n\tpublic void setUser(double user) {\n\t\tthis.user = user;\n\t}\n\n\tpublic double getWait() {\n\t\treturn wait;\n\t}\n\n\tpublic void setWait(double wait) {\n\t\tthis.wait = wait;\n\t}\n\n\tpublic double getFree() {\n\t\treturn free;\n\t}\n\n\tpublic void setFree(double free) {\n\t\tthis.free = free;\n\t}\n\n\tpublic String getCpuModel() {\n\t\treturn cpuModel;\n\t}\n\n\tpublic void setCpuModel(String cpuModel) {\n\t\tthis.cpuModel = cpuModel;\n\t}\n\n\tpublic CpuTicks getTicks() {\n\t\treturn ticks;\n\t}\n\n\tpublic void setTicks(CpuTicks ticks) {\n\t\tthis.ticks = ticks;\n\t}\n\n\t/**\n\t * 获取用户+系统的总的CPU使用率\n\t *\n\t * @return 总CPU使用率\n\t */\n\tpublic double getUsed() {\n\t\treturn NumberUtil.sub(100, this.free);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CpuInfo{\" +\n\t\t\t\t\"CPU核心数=\" + cpuNum +\n\t\t\t\t\", CPU总的使用率=\" + toTal +\n\t\t\t\t\", CPU系统使用率=\" + sys +\n\t\t\t\t\", CPU用户使用率=\" + user +\n\t\t\t\t\", CPU当前等待率=\" + wait +\n\t\t\t\t\", CPU当前空闲率=\" + free +\n\t\t\t\t\", CPU利用率=\" + getUsed() +\n\t\t\t\t\", CPU型号信息='\" + cpuModel + '\\'' +\n\t\t\t\t'}';\n\t}\n\n\t/**\n\t * 获取指定等待时间内系统CPU 系统使用率、用户使用率、利用率等等 相关信息\n\t *\n\t * @param processor   {@link CentralProcessor}\n\t * @param waitingTime 设置等待时间，单位毫秒\n\t * @since 5.7.12\n\t */\n\tprivate void init(CentralProcessor processor, long waitingTime) {\n\t\tfinal CpuTicks ticks = new CpuTicks(processor, waitingTime);\n\t\tthis.ticks = ticks;\n\n\t\tthis.cpuNum = processor.getLogicalProcessorCount();\n\t\tthis.cpuModel = processor.toString();\n\n\t\tfinal long totalCpu = ticks.totalCpu();\n\t\tthis.toTal = totalCpu;\n\t\tthis.sys = formatDouble(ticks.cSys, totalCpu);\n\t\tthis.user = formatDouble(ticks.user, totalCpu);\n\t\tthis.wait = formatDouble(ticks.ioWait, totalCpu);\n\t\tthis.free = formatDouble(ticks.idle, totalCpu);\n\t}\n\n\t/**\n\t * 获取每个CPU核心的tick，计算方式为 100 * tick / totalCpu\n\t *\n\t * @param tick     tick\n\t * @param totalCpu CPU总数\n\t * @return 平均每个CPU核心的tick\n\t * @since 5.7.12\n\t */\n\tprivate static double formatDouble(long tick, long totalCpu) {\n\t\tif (0 == totalCpu) {\n\t\t\treturn 0D;\n\t\t}\n\t\treturn Double.parseDouble(LOAD_FORMAT.format(tick <= 0 ? 0 : (100d * tick / totalCpu)));\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/oshi/CpuTicks.java",
    "content": "package cn.hutool.system.oshi;\n\nimport oshi.hardware.CentralProcessor;\nimport oshi.util.Util;\n\n/**\n * CPU负载时间信息\n *\n * @author looly\n * @since 5.7.12\n */\npublic class CpuTicks {\n\n\tlong idle;\n\tlong nice;\n\tlong irq;\n\tlong softIrq;\n\tlong steal;\n\tlong cSys;\n\tlong user;\n\tlong ioWait;\n\n\t/**\n\t * 构造，等待时间为用于计算在一定时长内的CPU负载情况，如传入1000表示最近1秒的负载情况\n\t *\n\t * @param processor   {@link CentralProcessor}\n\t * @param waitingTime 设置等待时间，单位毫秒\n\t */\n\tpublic CpuTicks(CentralProcessor processor, long waitingTime) {\n\t\t// CPU信息\n\t\tfinal long[] prevTicks = processor.getSystemCpuLoadTicks();\n\t\t// 这里必须要设置延迟\n\t\tUtil.sleep(waitingTime);\n\t\tfinal long[] ticks = processor.getSystemCpuLoadTicks();\n\n\t\tthis.idle = tick(prevTicks, ticks, CentralProcessor.TickType.IDLE);\n\t\tthis.nice = tick(prevTicks, ticks, CentralProcessor.TickType.NICE);\n\t\tthis.irq = tick(prevTicks, ticks, CentralProcessor.TickType.IRQ);\n\t\tthis.softIrq = tick(prevTicks, ticks, CentralProcessor.TickType.SOFTIRQ);\n\t\tthis.steal = tick(prevTicks, ticks, CentralProcessor.TickType.STEAL);\n\t\tthis.cSys = tick(prevTicks, ticks, CentralProcessor.TickType.SYSTEM);\n\t\tthis.user = tick(prevTicks, ticks, CentralProcessor.TickType.USER);\n\t\tthis.ioWait = tick(prevTicks, ticks, CentralProcessor.TickType.IOWAIT);\n\t}\n\n\tpublic long getIdle() {\n\t\treturn idle;\n\t}\n\n\tpublic void setIdle(long idle) {\n\t\tthis.idle = idle;\n\t}\n\n\tpublic long getNice() {\n\t\treturn nice;\n\t}\n\n\tpublic void setNice(long nice) {\n\t\tthis.nice = nice;\n\t}\n\n\tpublic long getIrq() {\n\t\treturn irq;\n\t}\n\n\tpublic void setIrq(long irq) {\n\t\tthis.irq = irq;\n\t}\n\n\tpublic long getSoftIrq() {\n\t\treturn softIrq;\n\t}\n\n\tpublic void setSoftIrq(long softIrq) {\n\t\tthis.softIrq = softIrq;\n\t}\n\n\tpublic long getSteal() {\n\t\treturn steal;\n\t}\n\n\tpublic void setSteal(long steal) {\n\t\tthis.steal = steal;\n\t}\n\n\tpublic long getcSys() {\n\t\treturn cSys;\n\t}\n\n\tpublic void setcSys(long cSys) {\n\t\tthis.cSys = cSys;\n\t}\n\n\tpublic long getUser() {\n\t\treturn user;\n\t}\n\n\tpublic void setUser(long user) {\n\t\tthis.user = user;\n\t}\n\n\tpublic long getIoWait() {\n\t\treturn ioWait;\n\t}\n\n\tpublic void setIoWait(long ioWait) {\n\t\tthis.ioWait = ioWait;\n\t}\n\n\t/**\n\t * 获取CPU总的使用率\n\t *\n\t * @return CPU总使用率\n\t */\n\tpublic long totalCpu() {\n\t\treturn Math.max(user + nice + cSys + idle + ioWait + irq + softIrq + steal, 0);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CpuTicks{\" +\n\t\t\t\t\"idle=\" + idle +\n\t\t\t\t\", nice=\" + nice +\n\t\t\t\t\", irq=\" + irq +\n\t\t\t\t\", softIrq=\" + softIrq +\n\t\t\t\t\", steal=\" + steal +\n\t\t\t\t\", cSys=\" + cSys +\n\t\t\t\t\", user=\" + user +\n\t\t\t\t\", ioWait=\" + ioWait +\n\t\t\t\t'}';\n\t}\n\n\t/**\n\t * 获取一段时间内的CPU负载标记差\n\t *\n\t * @param prevTicks 开始的ticks\n\t * @param ticks     结束的ticks\n\t * @param tickType  tick类型\n\t * @return 标记差\n\t * @since 5.7.12\n\t */\n\tprivate static long tick(long[] prevTicks, long[] ticks, CentralProcessor.TickType tickType) {\n\t\treturn ticks[tickType.getIndex()] - prevTicks[tickType.getIndex()];\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/oshi/OshiUtil.java",
    "content": "package cn.hutool.system.oshi;\n\nimport oshi.SystemInfo;\nimport oshi.hardware.CentralProcessor;\nimport oshi.hardware.ComputerSystem;\nimport oshi.hardware.GlobalMemory;\nimport oshi.hardware.HWDiskStore;\nimport oshi.hardware.HardwareAbstractionLayer;\nimport oshi.hardware.NetworkIF;\nimport oshi.hardware.Sensors;\nimport oshi.software.os.OSProcess;\nimport oshi.software.os.OperatingSystem;\n\nimport java.util.List;\n\n/**\n * Oshi库封装的工具类，通过此工具类，可获取系统、硬件相关信息\n *\n * <pre>\n * 1、系统信息\n * 2、硬件信息\n * </pre>\n * <p>\n * 相关内容见：https://github.com/oshi/oshi\n *\n * @author Looly\n * @since 4.6.4\n */\npublic class OshiUtil {\n\n\tprivate static final SystemInfo systemInfo;\n\t/**\n\t * 硬件信息\n\t */\n\tprivate static final HardwareAbstractionLayer hardware;\n\t/**\n\t * 系统信息\n\t */\n\tprivate static final OperatingSystem os;\n\n\tstatic {\n\t\tsystemInfo = new SystemInfo();\n\t\thardware = systemInfo.getHardware();\n\t\tos = systemInfo.getOperatingSystem();\n\t}\n\n\t/**\n\t * 获取操作系统相关信息，包括系统版本、文件系统、进程等\n\t *\n\t * @return 操作系统相关信息\n\t */\n\tpublic static OperatingSystem getOs() {\n\t\treturn os;\n\t}\n\n\t/**\n\t * 获取当前进程信息{@link OSProcess}\n\t *\n\t * @return 进程信息 {@link OSProcess}\n\t * @since 5.7.12\n\t */\n\tpublic static OSProcess getCurrentProcess() {\n\t\treturn os.getProcess(os.getProcessId());\n\t}\n\n\t/**\n\t * 获取硬件相关信息，包括内存、硬盘、网络设备、显示器、USB、声卡等\n\t *\n\t * @return 硬件相关信息\n\t */\n\tpublic static HardwareAbstractionLayer getHardware() {\n\t\treturn hardware;\n\t}\n\n\t/**\n\t * 获取BIOS中计算机相关信息，比如序列号、固件版本等\n\t *\n\t * @return 获取BIOS中计算机相关信息\n\t */\n\tpublic static ComputerSystem getSystem() {\n\t\treturn hardware.getComputerSystem();\n\t}\n\n\t/**\n\t * 获取内存相关信息，比如总内存、可用内存等\n\t *\n\t * @return 内存相关信息\n\t */\n\tpublic static GlobalMemory getMemory() {\n\t\treturn hardware.getMemory();\n\t}\n\n\t/**\n\t * 获取CPU（处理器）相关信息，比如CPU负载等\n\t *\n\t * @return CPU（处理器）相关信息\n\t */\n\tpublic static CentralProcessor getProcessor() {\n\t\treturn hardware.getProcessor();\n\t}\n\n\t/**\n\t * 获取传感器相关信息，例如CPU温度、风扇转速等，传感器可能有多个\n\t *\n\t * @return 传感器相关信息\n\t */\n\tpublic static Sensors getSensors() {\n\t\treturn hardware.getSensors();\n\t}\n\n\t/**\n\t * 获取磁盘相关信息，可能有多个磁盘（包括可移动磁盘等）\n\t *\n\t * @return 磁盘相关信息\n\t * @since 5.3.6\n\t */\n\tpublic static List<HWDiskStore> getDiskStores() {\n\t\treturn hardware.getDiskStores();\n\t}\n\n\t/**\n\t * 获取网络相关信息，可能多块网卡\n\t *\n\t * @return 网络相关信息\n\t * @since 5.3.6\n\t */\n\tpublic static List<NetworkIF> getNetworkIFs() {\n\t\treturn hardware.getNetworkIFs();\n\t}\n\n\t// ------------------------------------------------------------------ cpu\n\n\t/**\n\t * 获取系统CPU 系统使用率、用户使用率、利用率等等 相关信息<br>\n\t * 默认间隔1秒\n\t *\n\t * @return 系统 CPU 使用率 等信息\n\t */\n\tpublic static CpuInfo getCpuInfo() {\n\t\treturn getCpuInfo(1000);\n\t}\n\n\t/**\n\t * 获取系统CPU 系统使用率、用户使用率、利用率等等 相关信息\n\t *\n\t * @param waitingTime 设置等待时间，单位毫秒\n\t * @return 系统 CPU 使用率 等信息\n\t */\n\tpublic static CpuInfo getCpuInfo(long waitingTime) {\n\t\treturn new CpuInfo(OshiUtil.getProcessor(), waitingTime);\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/oshi/package-info.java",
    "content": "/**\n * Oshi库封装<br>\n * https://github.com/oshi/oshi\n * \n * @author Looly\n * @since 4.6.4\n */\npackage cn.hutool.system.oshi;"
  },
  {
    "path": "hutool-system/src/main/java/cn/hutool/system/package-info.java",
    "content": "/**\n * System模块主要获取系统、JVM、内存、CPU等信息，以便动态监测系统状态\n * \n * @author looly\n *\n */\npackage cn.hutool.system;"
  },
  {
    "path": "hutool-system/src/test/java/cn/hutool/system/OshiPrintTest.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.system.oshi.OshiUtil;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n@Disabled\npublic class OshiPrintTest {\n\n\t@Test\n\t@Disabled\n\tpublic void printCpuInfo(){\n\t\tConsole.log(OshiUtil.getCpuInfo());\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/test/java/cn/hutool/system/OshiTest.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.lang.Console;\nimport cn.hutool.system.oshi.CpuInfo;\nimport cn.hutool.system.oshi.OshiUtil;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport oshi.software.os.OSProcess;\n\n/**\n * 测试参考：https://github.com/oshi/oshi/blob/master/oshi-core/src/test/java/oshi/SystemInfoTest.java\n */\npublic class OshiTest {\n\n\t@Test\n\tpublic void getMemoryTest() {\n\t\tlong total = OshiUtil.getMemory().getTotal();\n\t\tassertTrue(total > 0);\n\t}\n\n\t@Test\n\tpublic void getCupInfo() {\n\t\tCpuInfo cpuInfo = OshiUtil.getCpuInfo();\n\t\tassertNotNull(cpuInfo);\n\t}\n\n\t@Test\n\tpublic void getCurrentProcessTest() {\n\t\tfinal OSProcess currentProcess = OshiUtil.getCurrentProcess();\n\t\tassertEquals(\"java\", currentProcess.getName());\n\t}\n\n\t@Test\n\t@Disabled\n\tpublic void getUsedTest(){\n\t\twhile (true){\n\t\t\tConsole.log(OshiUtil.getCpuInfo().getUsed());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hutool-system/src/test/java/cn/hutool/system/SystemUtilTest.java",
    "content": "package cn.hutool.system;\n\nimport cn.hutool.core.lang.Console;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class SystemUtilTest {\n\n\t@Test\n\t@Disabled\n\tpublic void dumpTest() {\n\t\tSystemUtil.dumpSystemInfo();\n\t}\n\n\t@Test\n\tpublic void getCurrentPidTest() {\n\t\tfinal long pid = SystemUtil.getCurrentPID();\n\t\tassertTrue(pid > 0);\n\t}\n\n\t@Test\n\tpublic void getJavaInfoTest() {\n\t\tfinal JavaInfo javaInfo = SystemUtil.getJavaInfo();\n\t\tassertNotNull(javaInfo);\n\t}\n\n\t@Test\n\tpublic void getJavaRuntimeInfoTest() {\n\t\tfinal JavaRuntimeInfo info = SystemUtil.getJavaRuntimeInfo();\n\t\tassertNotNull(info);\n\t}\n\n\t@Test\n\tpublic void getOsInfoTest() {\n\t\tfinal OsInfo osInfo = SystemUtil.getOsInfo();\n\t\tassertNotNull(osInfo);\n\n\t\tConsole.log(osInfo.getName());\n\t}\n\n\t@Test\n\tpublic void getHostInfo() {\n\t\tfinal HostInfo hostInfo = SystemUtil.getHostInfo();\n\t\tassertNotNull(hostInfo);\n\t}\n\n\t@Test\n\tpublic void getUserInfoTest(){\n\t\t// https://gitee.com/chinabugotech/hutool/issues/I3NM39\n\t\tfinal UserInfo userInfo = SystemUtil.getUserInfo();\n\t\tassertTrue(userInfo.getTempDir().endsWith(File.separator));\n\t}\n}\n"
  },
  {
    "path": "hutool.sh",
    "content": "#!/bin/bash\n\n\n# Help info function\nhelp(){\n  echo \"--------------------------------------------------------------------------\"\n  echo \"\"\n  echo \"usage: ./hutool.sh [install | doc | pack]\"\n  echo \"\"\n  echo \"-install    Install Hutool to your local Maven repository.\"\n  echo \"-doc        Generate Java doc api for Hutool, you can see it in target dir\"\n  echo \"-pack       Make jar package by Maven\"\n  echo \"\"\n  echo \"--------------------------------------------------------------------------\"\n}\n\n\n# Start\n./bin/logo.sh\ncase \"$1\" in\n  'install')\n    bin/install.sh\n\t;;\n  'doc')\n    bin/javadoc.sh\n\t;;\n  'pack')\n    bin/package.sh\n\t;;\n  *)\n    help\nesac\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\t xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<packaging>pom</packaging>\n\n\t<groupId>cn.hutool</groupId>\n\t<artifactId>hutool-parent</artifactId>\n\t<version>5.8.44</version>\n\t<name>hutool</name>\n\t<description>Hutool是一个小而全的Java工具类库，通过静态方法封装，降低相关API的学习成本，提高工作效率，使Java拥有函数式语言般的优雅，让Java语言也可以“甜甜的”。</description>\n\t<url>https://github.com/chinabugotech/hutool</url>\n\n\t<modules>\n\t\t<module>hutool-all</module>\n\t\t<module>hutool-bom</module>\n\t\t<module>hutool-aop</module>\n\t\t<module>hutool-bloomFilter</module>\n\t\t<module>hutool-cache</module>\n\t\t<module>hutool-core</module>\n\t\t<module>hutool-cron</module>\n\t\t<module>hutool-crypto</module>\n\t\t<module>hutool-db</module>\n\t\t<module>hutool-dfa</module>\n\t\t<module>hutool-extra</module>\n\t\t<module>hutool-http</module>\n\t\t<module>hutool-log</module>\n\t\t<module>hutool-script</module>\n\t\t<module>hutool-setting</module>\n\t\t<module>hutool-system</module>\n\t\t<module>hutool-json</module>\n\t\t<module>hutool-poi</module>\n\t\t<module>hutool-captcha</module>\n\t\t<module>hutool-socket</module>\n\t\t<module>hutool-jwt</module>\n\t\t<module>hutool-ai</module>\n\t</modules>\n\n\t<properties>\n\t\t<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>\n\t\t<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>\n\n\t\t<Automatic-Module-Name>cn.hutool</Automatic-Module-Name>\n\n\t\t<!-- versions -->\n\t\t<compile.version>8</compile.version>\n\t\t<junit.version>5.10.3</junit.version>\n\t\t<lombok.version>1.18.34</lombok.version>\n\t\t<bouncycastle.version>1.82</bouncycastle.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<!-- 全局单元测试 -->\n\t\t<dependency>\n\t\t\t<groupId>org.junit.jupiter</groupId>\n\t\t\t<artifactId>junit-jupiter-engine</artifactId>\n\t\t\t<version>${junit.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.junit.jupiter</groupId>\n\t\t\t<artifactId>junit-jupiter-params</artifactId>\n\t\t\t<version>${junit.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.projectlombok</groupId>\n\t\t\t<artifactId>lombok</artifactId>\n\t\t\t<version>${lombok.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n\n\t<issueManagement>\n\t\t<system>Github Issue</system>\n\t\t<url>https://github.com/chinabugotech/hutool/issues</url>\n\t</issueManagement>\n\n\t<licenses>\n\t\t<license>\n\t\t\t<name>Mulan Permissive Software License，Version 2</name>\n\t\t\t<url>https://license.coscl.org.cn/MulanPSL2</url>\n\t\t</license>\n\t</licenses>\n\n\t<developers>\n\t\t<developer>\n\t\t\t<name>bugo</name>\n\t\t\t<email>bugo@bugotech.cn</email>\n\t\t</developer>\n\t\t<developer>\n\t\t\t<name>Looly</name>\n\t\t\t<email>loolly@gmail.com</email>\n\t\t</developer>\n\t</developers>\n\n\t<scm>\n\t\t<connection>scm:git@github.com:chinabugotech/hutool.git</connection>\n\t\t<developerConnection>scm:git@github.com:chinabugotech/hutool.git</developerConnection>\n\t\t<url>git@github.com:chinabugotech/hutool.git</url>\n\t</scm>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<version>3.13.0</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<source>${compile.version}</source>\n\t\t\t\t\t<target>${compile.version}</target>\n\t\t\t\t\t<compilerArgument>-Xlint:unchecked</compilerArgument>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t\t<!-- 单元测试 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-surefire-plugin</artifactId>\n\t\t\t\t<version>3.5.3</version>\n\t\t\t</plugin>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-jar-plugin</artifactId>\n\t\t\t\t<version>3.4.2</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<archive>\n\t\t\t\t\t\t<index>true</index>\n\t\t\t\t\t\t<manifest>\n\t\t\t\t\t\t\t<addDefaultImplementationEntries>true</addDefaultImplementationEntries>\n\t\t\t\t\t\t</manifest>\n\t\t\t\t\t\t<manifestEntries>\n\t\t\t\t\t\t\t<Build-OS>${os.name}</Build-OS>\n\t\t\t\t\t\t\t<Built-By>${user.name}</Built-By>\n\t\t\t\t\t\t\t<Build-Jdk>${java.version}</Build-Jdk>\n\t\t\t\t\t\t\t<Build-Timestamp>${maven.build.timestamp}</Build-Timestamp>\n\t\t\t\t\t\t\t<Automatic-Module-Name>${Automatic-Module-Name}</Automatic-Module-Name>\n\t\t\t\t\t\t</manifestEntries>\n\t\t\t\t\t</archive>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t\t<!-- Javadoc -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-javadoc-plugin</artifactId>\n\t\t\t\t<version>3.11.2</version>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>jar</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\t\t\t<!-- 统一更新pom版本 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.codehaus.mojo</groupId>\n\t\t\t\t<artifactId>versions-maven-plugin</artifactId>\n\t\t\t\t<version>2.17.1</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<generateBackupPoms>false</generateBackupPoms>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n\t<profiles>\n\t\t<profile>\n\t\t\t<id>release</id>\n\t\t\t<distributionManagement>\n\t\t\t\t<snapshotRepository>\n\t\t\t\t\t<id>oss</id>\n\t\t\t\t\t<url>https://oss.sonatype.org/content/repositories/snapshots/</url>\n\t\t\t\t</snapshotRepository>\n\t\t\t\t<repository>\n\t\t\t\t\t<id>oss</id>\n\t\t\t\t\t<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n\t\t\t\t</repository>\n\t\t\t</distributionManagement>\n\t\t\t<build>\n\t\t\t\t<plugins>\n\t\t\t\t\t<!-- Source -->\n\t\t\t\t\t<plugin>\n\t\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t\t<artifactId>maven-source-plugin</artifactId>\n\t\t\t\t\t\t<version>3.3.1</version>\n\t\t\t\t\t\t<executions>\n\t\t\t\t\t\t\t<execution>\n\t\t\t\t\t\t\t\t<id>oss</id>\n\t\t\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t\t<goal>jar-no-fork</goal>\n\t\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t\t</execution>\n\t\t\t\t\t\t</executions>\n\t\t\t\t\t</plugin>\n\t\t\t\t\t<!-- 测试覆盖度 -->\n\t\t\t\t\t<plugin>\n\t\t\t\t\t\t<groupId>org.codehaus.mojo</groupId>\n\t\t\t\t\t\t<artifactId>cobertura-maven-plugin</artifactId>\n\t\t\t\t\t\t<version>2.7</version>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<formats>\n\t\t\t\t\t\t\t\t<format>html</format>\n\t\t\t\t\t\t\t\t<format>xml</format>\n\t\t\t\t\t\t\t</formats>\n\t\t\t\t\t\t\t<check/>\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</plugin>\n\t\t\t\t\t<!-- Gpg Signature -->\n\t\t\t\t\t<plugin>\n\t\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t\t<artifactId>maven-gpg-plugin</artifactId>\n\t\t\t\t\t\t<version>1.6</version>\n\t\t\t\t\t\t<executions>\n\t\t\t\t\t\t\t<execution>\n\t\t\t\t\t\t\t\t<id>oss</id>\n\t\t\t\t\t\t\t\t<phase>verify</phase>\n\t\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t\t<goal>sign</goal>\n\t\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t\t</execution>\n\t\t\t\t\t\t</executions>\n\t\t\t\t\t</plugin>\n\t\t\t\t\t<plugin>\n\t\t\t\t\t\t<groupId>org.sonatype.central</groupId>\n\t\t\t\t\t\t<artifactId>central-publishing-maven-plugin</artifactId>\n\t\t\t\t\t\t<version>0.8.0</version>\n\t\t\t\t\t\t<extensions>true</extensions>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<publishingServerId>central</publishingServerId>\n\t\t\t\t\t\t\t<!-- 自动发布 -->\n\t\t\t\t\t\t\t<autoPublish>false</autoPublish>\n\t\t\t\t\t\t\t<!-- 等待发布 -->\n\t\t\t\t\t\t\t<!--<waitUntil>published</waitUntil>-->\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</plugin>\n\t\t\t\t</plugins>\n\t\t\t</build>\n\t\t</profile>\n\t</profiles>\n\n</project>\n"
  }
]